Skip to content

Commit

Permalink
feat: Add beacon node and output server validation
Browse files Browse the repository at this point in the history
  • Loading branch information
mattevans committed Dec 19, 2024
1 parent 0ee627b commit 2db7191
Show file tree
Hide file tree
Showing 9 changed files with 323 additions and 354 deletions.
74 changes: 11 additions & 63 deletions cmd/cli/commands/config/config_network.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
package config

import (
"fmt"
"net/http"
"strings"
"time"

"github.com/ethpandaops/contributoor-installer/internal/service"
"github.com/ethpandaops/contributoor-installer/internal/tui"
"github.com/ethpandaops/contributoor-installer/internal/validate"
"github.com/gdamore/tcell/v2"
"github.com/rivo/tview"
)
Expand Down Expand Up @@ -160,70 +156,22 @@ func (p *NetworkConfigPage) initPage() {
p.content = mainFlex
}

func validateBeaconNode(address string) error {
// Check if URL is valid
if !strings.HasPrefix(address, "http://") && !strings.HasPrefix(address, "https://") {
return fmt.Errorf("beacon node address must start with http:// or https://")
}

// Try to connect to the beacon node
client := &http.Client{Timeout: 5 * time.Second}
func validateAndUpdate(p *NetworkConfigPage, input *tview.InputField) {
if err := validate.ValidateBeaconNodeAddress(input.GetText()); err != nil {
p.openErrorModal(err)

resp, err := client.Get(fmt.Sprintf("%s/eth/v1/node/health", address))
if err != nil {
return fmt.Errorf("we're unable to connect to your beacon node: %w", err)
return
}

defer resp.Body.Close()
if err := p.display.configService.Update(func(cfg *service.ContributoorConfig) {
cfg.BeaconNodeAddress = input.GetText()
}); err != nil {
p.openErrorModal(err)

if resp.StatusCode != http.StatusOK {
return fmt.Errorf("beacon node returned status %d", resp.StatusCode)
return
}

return nil
}

func validateAndUpdate(p *NetworkConfigPage, input *tview.InputField) {
var (
text = input.GetText()
dropdown, _ = p.form.GetFormItem(0).(*tview.DropDown)
index, networkOption = dropdown.GetCurrentOption()
)

// Show loading modal while validating, we reach out to the beacon node
// to validate the address, which can lock-up the UI while it does it.
// Better to show a loading modal than the user seeing a blank screen.
loadingModal := tui.CreateLoadingModal(
p.display.app,
"\n[yellow]Validating configuration\nPlease wait...[white]",
)
p.display.app.SetRoot(loadingModal, true)

go func() {
err := validateBeaconNode(text)

p.display.app.QueueUpdateDraw(func() {
if err != nil {
p.openErrorModal(err)

return
}

if err := p.display.configService.Update(func(cfg *service.ContributoorConfig) {
if index != -1 {
cfg.NetworkName = networkOption
}

cfg.BeaconNodeAddress = text
}); err != nil {
p.openErrorModal(err)

return
}

p.display.setPage(p.display.homePage)
})
}()
p.display.setPage(p.display.homePage)
}

func (p *NetworkConfigPage) openErrorModal(err error) {
Expand Down
178 changes: 55 additions & 123 deletions cmd/cli/commands/config/config_output_server.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
package config

import (
"encoding/base64"
"fmt"
"strings"

"github.com/ethpandaops/contributoor-installer/internal/service"
"github.com/ethpandaops/contributoor-installer/internal/tui"
"github.com/ethpandaops/contributoor-installer/internal/validate"
"github.com/gdamore/tcell/v2"
"github.com/rivo/tview"
)
Expand Down Expand Up @@ -232,7 +231,7 @@ func (p *OutputServerConfigPage) initPage() {
}

func validateAndUpdateOutputServer(p *OutputServerConfigPage) {
// Get the currently selected server.
// Get the currently selected server
dropdown, _ := p.form.GetFormItem(0).(*tview.DropDown)
_, serverLabel := dropdown.GetCurrentOption()

Expand All @@ -247,141 +246,64 @@ func validateAndUpdateOutputServer(p *OutputServerConfigPage) {
}
}

// If it's a custom server, we need to get the server address and validate it.
if serverAddress == "custom" {
// Get and validate custom address
customAddress := p.form.GetFormItem(1).(*tview.InputField).GetText()
if customAddress == "" {
errorModal := tui.CreateErrorModal(
p.display.app,
"Server address is required for custom server",
func() {
p.display.app.SetRoot(p.display.frame, true)
p.display.app.SetFocus(p.form)
},
)

p.display.app.SetRoot(errorModal, true)

return
}

// Validate URL format.
if !strings.HasPrefix(customAddress, "http://") && !strings.HasPrefix(customAddress, "https://") {
errorModal := tui.CreateErrorModal(
p.display.app,
"Server address must start with http:// or https://",
func() {
p.display.app.SetRoot(p.display.frame, true)
p.display.app.SetFocus(p.form)
},
)

p.display.app.SetRoot(errorModal, true)

return
}

// Set the server address.
serverAddress = customAddress

// Get credentials, these are optional for custom servers.
var (
username, _ = p.form.GetFormItem(2).(*tview.InputField)
password, _ = p.form.GetFormItem(3).(*tview.InputField)
usernameText = username.GetText()
passwordText = password.GetText()
)

// Only set credentials if both username and password are provided
if usernameText != "" && passwordText != "" {
credentials := base64.StdEncoding.EncodeToString(
[]byte(fmt.Sprintf("%s:%s", usernameText, passwordText)),
)

if err := p.display.configService.Update(func(cfg *service.ContributoorConfig) {
cfg.OutputServer.Address = serverAddress
cfg.OutputServer.Credentials = credentials
}); err != nil {
p.openErrorModal(err)

return
}
} else if usernameText == "" && passwordText == "" {
// Both empty - clear credentials
if err := p.display.configService.Update(func(cfg *service.ContributoorConfig) {
cfg.OutputServer.Address = serverAddress
cfg.OutputServer.Credentials = ""
}); err != nil {
p.openErrorModal(err)

return
}
} else {
// One is empty but not both
p.openErrorModal(fmt.Errorf("both username and password must be provided if using credentials"))

return
}
} else {
// Get and validate credentials, these are required for ethPandaOps servers.
var (
username, _ = p.form.GetFormItem(1).(*tview.InputField)
password, _ = p.form.GetFormItem(2).(*tview.InputField)
usernameText = username.GetText()
passwordText = password.GetText()
)

if usernameText == "" || passwordText == "" {
errorModal := tui.CreateErrorModal(
p.display.app,
"Username and password are required for ethPandaOps servers",
func() {
p.display.app.SetRoot(p.display.frame, true)
p.display.app.SetFocus(p.form)
},
)

p.display.app.SetRoot(errorModal, true)

return
}
// Get form values based on server type
var (
isCustom = serverAddress == "custom"
username string
password string
formStart = 1
)

// Update credentials.
credentials := base64.StdEncoding.EncodeToString(
[]byte(fmt.Sprintf("%s:%s", usernameText, passwordText)),
)
if isCustom {
// For custom servers, get address from input field.
address := p.form.GetFormItem(formStart).(*tview.InputField).GetText()
serverAddress = address
formStart++

if err := p.display.configService.Update(func(cfg *service.ContributoorConfig) {
cfg.OutputServer.Address = serverAddress
cfg.OutputServer.Credentials = credentials
}); err != nil {
// Only validate the address if it's a custom server, otherwise its a
// pre-defined pandaops server.
if err := validate.ValidateOutputServerAddress(serverAddress); err != nil {
p.openErrorModal(err)

return
}
}

p.display.setPage(p.display.homePage)
}
// Get credentials from form.
if formItem := p.form.GetFormItem(formStart); formItem != nil {
username = formItem.(*tview.InputField).GetText()
}

// getCredentialsFromConfig is a helper function to get the user/pass from the config.
func getCredentialsFromConfig(cfg *service.ContributoorConfig) (username, password string) {
if cfg.OutputServer.Credentials == "" {
return "", ""
if formItem := p.form.GetFormItem(formStart + 1); formItem != nil {
password = formItem.(*tview.InputField).GetText()
}

decoded, err := base64.StdEncoding.DecodeString(cfg.OutputServer.Credentials)
if err != nil {
return "", ""
// Validate credentials. These are optional for custom servers.
if err := validate.ValidateOutputServerCredentials(
username,
password,
validate.IsEthPandaOpsServer(serverAddress),
); err != nil {
p.openErrorModal(err)

return
}

parts := strings.Split(string(decoded), ":")
if len(parts) != 2 {
return "", ""
// Update config with validated values.
if err := p.display.configService.Update(func(cfg *service.ContributoorConfig) {
cfg.OutputServer.Address = serverAddress
if username != "" && password != "" {
cfg.OutputServer.Credentials = validate.EncodeCredentials(username, password)
} else {
cfg.OutputServer.Credentials = ""
}
}); err != nil {
p.openErrorModal(err)

return
}

return parts[0], parts[1]
p.display.setPage(p.display.homePage)
}

func (p *OutputServerConfigPage) openErrorModal(err error) {
Expand All @@ -393,3 +315,13 @@ func (p *OutputServerConfigPage) openErrorModal(err error) {
},
), true)
}

// Update getCredentialsFromConfig to use the validation package.
func getCredentialsFromConfig(cfg *service.ContributoorConfig) (username, password string) {
username, password, err := validate.DecodeCredentials(cfg.OutputServer.Credentials)
if err != nil {
return "", ""
}

return username, password
}
Loading

0 comments on commit 2db7191

Please sign in to comment.