From f9cfbaa77f066f81142b6c9b742979a9cff244b4 Mon Sep 17 00:00:00 2001 From: Nate Maninger Date: Wed, 10 Jan 2024 09:21:55 -0800 Subject: [PATCH 1/2] cmd: add config command --- cmd/hostd/main.go | 225 ++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 187 insertions(+), 38 deletions(-) diff --git a/cmd/hostd/main.go b/cmd/hostd/main.go index 5032b5a2..1a8a725c 100644 --- a/cmd/hostd/main.go +++ b/cmd/hostd/main.go @@ -1,6 +1,7 @@ package main import ( + "bufio" "errors" "flag" "fmt" @@ -13,6 +14,7 @@ import ( "os/signal" "path/filepath" "runtime" + "strconv" "strings" "syscall" "time" @@ -78,6 +80,17 @@ func readPasswordInput(context string) (string, error) { return string(input), err } +func readInput(context string) (string, error) { + fmt.Printf("%s: ", context) + r := bufio.NewReader(os.Stdin) + input, err := r.ReadString('\n') + if err != nil { + return "", err + } + fmt.Println() + return strings.TrimSpace(input), nil +} + // wrapANSI wraps the output in ANSI escape codes if enabled. func wrapANSI(prefix, output, suffix string) string { if cfg.Log.StdOut.EnableANSI { @@ -86,33 +99,33 @@ func wrapANSI(prefix, output, suffix string) string { return output } -// stdoutError prints an error message to stdout and exits with a 1 exit code. +// stdoutFatalError prints an error message to stdout and exits with a 1 exit code. +func stdoutFatalError(msg string) { + stdoutError(msg) + os.Exit(1) +} + +// stdoutError prints an error message to stdout func stdoutError(msg string) { if cfg.Log.StdOut.EnableANSI { fmt.Println(wrapANSI("\033[31m", msg, "\033[0m")) } else { fmt.Println(msg) } - os.Exit(1) } // mustSetAPIPassword prompts the user to enter an API password if one is not // already set via environment variable or config file. func mustSetAPIPassword() { - if len(cfg.HTTP.Password) != 0 { - return - } else if disableStdin { - stdoutError("API password must be set via environment variable or config file when --env flag is set") - } - + // retry until a valid API password is entered for { fmt.Println("Please choose a password to unlock hostd.") - fmt.Println("This password will be required to access the UI in your web browser.") + fmt.Println("This password will be required to access the admin UI in your web browser.") fmt.Println("(The password must be at least 4 characters.)") var err error cfg.HTTP.Password, err = readPasswordInput("Enter password") if err != nil { - stdoutError("Could not read password:" + err.Error()) + stdoutFatalError("Could not read password:" + err.Error()) } if len(cfg.HTTP.Password) >= 4 { @@ -124,14 +137,14 @@ func mustSetAPIPassword() { } } -func mustGetSeedPhrase() string { +func mustSetSeedPhrase() { // retry until a valid seed phrase is entered for { fmt.Println("") fmt.Println("Type in your 12-word seed phrase and press enter. If you do not have a seed phrase yet, type 'seed' to generate one.") phrase, err := readPasswordInput("Enter seed phrase") if err != nil { - stdoutError("Could not read seed phrase: " + err.Error()) + stdoutFatalError("Could not read seed phrase: " + err.Error()) } if strings.ToLower(strings.TrimSpace(phrase)) == "seed" { @@ -156,9 +169,10 @@ func mustGetSeedPhrase() string { fmt.Println(wrapANSI("\033[1m", "Please confirm your seed phrase to continue.", "\033[0m")) confirmPhrase, err := readPasswordInput("Enter seed phrase") if err != nil { - stdoutError("Could not read seed phrase: " + err.Error()) + stdoutFatalError("Could not read seed phrase: " + err.Error()) } else if confirmPhrase == phrase { - return phrase + cfg.RecoveryPhrase = phrase + return } fmt.Println(wrapANSI("\033[31m", "Seed phrases do not match!", "\033[0m")) @@ -171,13 +185,147 @@ func mustGetSeedPhrase() string { err = wallet.SeedFromPhrase(&seed, phrase) if err == nil { // valid seed phrase - return phrase + cfg.RecoveryPhrase = phrase + return } fmt.Println(wrapANSI("\033[31m", "Invalid seed phrase:", "\033[0m"), err) fmt.Println("You entered:", phrase) } } +func setListenAddress(context string, value *string) { + // will continue to prompt until a valid value is entered + for { + input, err := readInput(fmt.Sprintf("%s (currently %q)", context, *value)) + if err != nil { + stdoutFatalError(fmt.Sprintf("Could not read %s port: %s", context, err.Error())) + return + } + + if input == "" { + return + } + + host, port, err := net.SplitHostPort(input) + if err != nil { + stdoutError(fmt.Sprintf("Invalid %s port %q: %s", context, input, err.Error())) + continue + } + + n, err := strconv.Atoi(port) + if err != nil { + stdoutError(fmt.Sprintf("Invalid %s port %q: %s", context, input, err.Error())) + continue + } else if n < 0 || n > 65535 { + stdoutError(fmt.Sprintf("Invalid %s port %q: must be between 0 and 65535", context, input)) + continue + } + *value = net.JoinHostPort(host, port) + return + } +} + +func mustSetAdvancedConfig() { + input, err := readInput("Would you like to configure advanced settings? (yes/no)") + if err != nil { + stdoutFatalError("Could not read input: " + err.Error()) + } else if !strings.EqualFold(input, "yes") { + return + } + + fmt.Println("") + fmt.Println("Advanced settings are used to configure the host's behavior.") + fmt.Println("You can leave these settings blank to use the defaults.") + fmt.Println("") + + // http address + fmt.Println("The HTTP address is used to serve the host's admin API.") + fmt.Println("The admin API is used to configure the host.") + fmt.Println("It should not be exposed to the public internet without setting up a reverse proxy.") + setListenAddress("HTTP Address", &cfg.HTTP.Address) + + // gateway address + fmt.Println("") + fmt.Println("The gateway address is used to exchange blocks with other nodes in the Sia network") + fmt.Println("It should be exposed publicly to improve the host's connectivity.") + setListenAddress("Gateway Address", &cfg.Consensus.GatewayAddress) + + // rhp2 address + fmt.Println("") + fmt.Println("The RHP2 address is used by renters to connect to the host.") + fmt.Println("It is a legacy protocol, but still required for host discovery") + fmt.Println("It should be exposed publicly to allow renters to connect and upload data.") + setListenAddress("RHP2 Address", &cfg.RHP2.Address) + + // rhp3 TCP address + fmt.Println("") + fmt.Println("The RHP3 address is used by renters to connect to the host.") + fmt.Println("It is a newer protocol that is more efficient than RHP2.") + fmt.Println("It should be exposed publicly to allow renters to connect and upload data.") + setListenAddress("RHP3 TCP Address", &cfg.RHP3.TCPAddress) +} + +func mustBuildConfig() { + if _, err := os.Stat("hostd.yml"); err == nil { + input, err := readInput("hostd.yml already exists. Would you like to overwrite it? (yes/no)") + if err != nil { + stdoutFatalError("Could not read input: " + err.Error()) + return + } else if !strings.EqualFold(input, "yes") { + return + } + } + + if cfg.RecoveryPhrase != "" { + fmt.Println("") + fmt.Println(wrapANSI("\033[33m", "A wallet seed phrase is already set.", "\033[0m")) + fmt.Println("If you change your wallet seed phrase, your host will not be able to access Siacoin associated with this wallet.") + fmt.Println("Ensure that you have backed up your wallet seed phrase before continuing.") + input, err := readInput("Would you like to change your wallet seed phrase? (yes/no)") + if err != nil { + stdoutFatalError("Could not read input: " + err.Error()) + } else if strings.EqualFold(input, "yes") { + mustSetSeedPhrase() + fmt.Println("") + } + } else { + mustSetSeedPhrase() + fmt.Println("") + } + + if cfg.HTTP.Password != "" { + fmt.Println("") + fmt.Println(wrapANSI("\033[33m", "An admin password is already set.", "\033[0m")) + fmt.Println("If you change your admin password, you will need to update any scripts or applications that use the admin API.") + input, err := readInput("Would you like to change your admin password? (yes/no)") + if err != nil { + stdoutFatalError("Could not read input: " + err.Error()) + } else if strings.EqualFold(input, "yes") { + mustSetAPIPassword() + fmt.Println("") + } + } else { + mustSetAPIPassword() + fmt.Println("") + } + + mustSetAdvancedConfig() + + // write the config file + f, err := os.Create("hostd.yml") + if err != nil { + stdoutFatalError("failed to create config file: " + err.Error()) + return + } + defer f.Close() + + enc := yaml.NewEncoder(f) + if err := enc.Encode(cfg); err != nil { + stdoutFatalError("failed to encode config file: " + err.Error()) + return + } +} + func startAPIListener(log *zap.Logger) (l net.Listener, err error) { addr, port, err := net.SplitHostPort(cfg.HTTP.Address) if err != nil { @@ -220,18 +368,6 @@ func openBrowser(url string) error { } } -// mustSetWalletkey prompts the user to enter a wallet seed phrase if one is not -// already set via environment variable or config file. -func mustSetWalletkey() { - if len(cfg.RecoveryPhrase) != 0 { - return - } else if disableStdin { - stdoutError("Wallet seed must be set via environment variable or config file when --env flag is set") - } - - cfg.RecoveryPhrase = mustGetSeedPhrase() -} - // tryLoadConfig loads the config file specified by the HOSTD_CONFIG_PATH. If // the config file does not exist, it will not be loaded. func tryLoadConfig() { @@ -247,7 +383,7 @@ func tryLoadConfig() { f, err := os.Open(configPath) if err != nil { - stdoutError("failed to open config file: " + err.Error()) + stdoutFatalError("failed to open config file: " + err.Error()) return } defer f.Close() @@ -344,15 +480,32 @@ func main() { fmt.Println("Recovery Phrase:", phrase) fmt.Println("Address", types.StandardUnlockHash(key.PublicKey())) return + case "config": + mustBuildConfig() + return + } + + // check that the API password is set + if cfg.HTTP.Password == "" { + if disableStdin { + stdoutFatalError("API password must be set via environment variable or config file when --env flag is set") + return + } + mustSetAPIPassword() } - // check that the API password and wallet seed are set - mustSetAPIPassword() - mustSetWalletkey() + // check that the wallet seed is set + if cfg.RecoveryPhrase == "" { + if disableStdin { + stdoutFatalError("Wallet seed must be set via environment variable or config file when --env flag is set") + return + } + mustSetSeedPhrase() + } // configure the logger if !cfg.Log.StdOut.Enabled && !cfg.Log.File.Enabled { - stdoutError("At least one of stdout or file logging must be enabled") + stdoutFatalError("At least one of stdout or file logging must be enabled") return } @@ -409,7 +562,7 @@ func main() { fileWriter, closeFn, err := zap.Open(cfg.Log.File.Path) if err != nil { - stdoutError("failed to open log file: " + err.Error()) + stdoutFatalError("failed to open log file: " + err.Error()) return } defer closeFn() @@ -432,17 +585,13 @@ func main() { log.Info("hostd", zap.String("version", build.Version()), zap.String("network", build.NetworkName()), zap.String("commit", build.Commit()), zap.Time("buildDate", build.Time())) - // create the data directory if it does not already exist - if err := os.MkdirAll(cfg.Directory, 0700); err != nil { - log.Fatal("unable to create config directory", zap.Error(err)) - } - var seed [32]byte if err := wallet.SeedFromPhrase(&seed, cfg.RecoveryPhrase); err != nil { log.Fatal("failed to load wallet", zap.Error(err)) } walletKey := wallet.KeyFromSeed(&seed, 0) + // create the data directory if it does not already exist if err := os.MkdirAll(cfg.Directory, 0700); err != nil { log.Fatal("unable to create config directory", zap.Error(err)) } From 44bffaf6464b94bfd3005402192e7cf673672e68 Mon Sep 17 00:00:00 2001 From: Nate Maninger Date: Wed, 10 Jan 2024 09:34:32 -0800 Subject: [PATCH 2/2] ci: fix ui workflow --- .github/workflows/ui.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ui.yml b/.github/workflows/ui.yml index bd28b111..d51dfe2f 100644 --- a/.github/workflows/ui.yml +++ b/.github/workflows/ui.yml @@ -15,9 +15,9 @@ jobs: uses: actions/checkout@v3 - name: Set up Go - uses: actions/setup-go@v2 + uses: actions/setup-go@v3 with: - go-version: '1.20.0' + go-version: stable - name: Check for new hostd tag in SiaFoundation/web id: check-tag