Skip to content

Commit

Permalink
Merge pull request #266 from SiaFoundation/nate/add-config-command
Browse files Browse the repository at this point in the history
Add config command
  • Loading branch information
n8maninger authored Jan 10, 2024
2 parents 085f43a + 44bffaf commit c8ee98e
Show file tree
Hide file tree
Showing 2 changed files with 189 additions and 40 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/ui.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
225 changes: 187 additions & 38 deletions cmd/hostd/main.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main

import (
"bufio"
"errors"
"flag"
"fmt"
Expand All @@ -13,6 +14,7 @@ import (
"os/signal"
"path/filepath"
"runtime"
"strconv"
"strings"
"syscall"
"time"
Expand Down Expand Up @@ -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 {
Expand All @@ -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 {
Expand All @@ -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" {
Expand All @@ -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"))
Expand All @@ -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 {
Expand Down Expand Up @@ -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() {
Expand All @@ -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()
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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()
Expand All @@ -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))
}
Expand Down

0 comments on commit c8ee98e

Please sign in to comment.