From db9c50a01f0d001f337c2c3cca006b61275935ca Mon Sep 17 00:00:00 2001 From: vufon Date: Thu, 9 Jan 2025 03:30:48 +0700 Subject: [PATCH 1/7] Generate config file automatically. Generate wallet automatically --- config.go | 119 ++++++++++++++++++++++++++++++++++++-- internal/prompt/prompt.go | 4 ++ sampleconfig.go | 24 ++++++++ walletsetup.go | 7 +++ 4 files changed, 149 insertions(+), 5 deletions(-) create mode 100644 sampleconfig.go diff --git a/config.go b/config.go index 7a1eeed5e..b60b07c72 100644 --- a/config.go +++ b/config.go @@ -6,12 +6,15 @@ package main import ( + "bufio" "context" "fmt" + "io" "net" "os" "os/user" "path/filepath" + "regexp" "runtime" "sort" "strconv" @@ -71,6 +74,7 @@ const ( var ( dcrdDefaultCAFile = filepath.Join(dcrutil.AppDataDir("dcrd", false), "rpc.cert") + dcrdDefaultConfigFile = filepath.Join(dcrutil.AppDataDir("dcrd", false), "dcrd.conf") defaultAppDataDir = dcrutil.AppDataDir("dcrwallet", false) defaultConfigFile = filepath.Join(defaultAppDataDir, defaultConfigFilename) defaultRPCKeyFile = filepath.Join(defaultAppDataDir, "rpc.key") @@ -434,7 +438,27 @@ func loadConfig(ctx context.Context) (*config, []string, error) { parser.WriteHelp(os.Stderr) return loadConfigError(err) } - configFileError = err + // if path error, create default config file, assign default dcrd rpc config data if any + createFileErr := createDefaultConfigFile(configFilePath, preCfg.DcrdAuthType) + if createFileErr != nil { + fmt.Fprintf(os.Stderr, "Error creating a default "+ + "config file: %v\n", createFileErr) + configFileError = createFileErr + } else { + log.Warnf("Config file does not exist. New default file created: %s", configFilePath) + configFileError = nil + // Reparse data on config file + err = flags.NewIniParser(parser).ParseFile(configFilePath) + if err != nil { + var e *os.PathError + if !errors.As(err, &e) { + fmt.Fprintln(os.Stderr, err) + parser.WriteHelp(os.Stderr) + return loadConfigError(err) + } + configFileError = err + } + } } // Parse command line options again to ensure they take precedence. @@ -660,10 +684,30 @@ func loadConfig(ctx context.Context) (*config, []string, error) { // Created successfully, so exit now with success. os.Exit(0) } else if !dbFileExists && !cfg.NoInitialLoad { - err := errors.Errorf("The wallet does not exist. Run with the " + - "--create option to initialize and create it.") - fmt.Fprintln(os.Stderr, err) - return loadConfigError(err) + isCreate, err := ConfirmBool("The wallet does not exist. Do you want to create a wallet now?", "y") + if err != nil { + log.Errorf("Error creating wallet: %v", err) + return loadConfigError(err) + } + if isCreate { + err := createWallet(ctx, &cfg) + if err != nil { + fmt.Fprintln(os.Stderr, "Unable to create wallet:", err) + return loadConfigError(err) + } + // Ask user whether to launch dcrwallet or exit + launch, err := ConfirmBool("Do you want to launch dcrwallet now?", "y") + if err != nil { + fmt.Fprintln(os.Stderr, "Unable to launch dcrwallet:", err) + return loadConfigError(err) + } + if !launch { + os.Exit(0) + } + } else { + log.Warnf("Can be run with --create option to initialize and create wallet.") + os.Exit(0) + } } ipNet := func(cidr string) net.IPNet { @@ -1049,3 +1093,68 @@ func loadConfig(ctx context.Context) (*config, []string, error) { return &cfg, remainingArgs, nil } + +// createDefaultConfig copies the file sample-dcrd.conf to the given destination path, +// and populates it with some randomly generated RPC username and password. +func createDefaultConfigFile(destPath string, authType string) error { + // Create the destination directory if it does not exist. + err := os.MkdirAll(filepath.Dir(destPath), 0700) + if err != nil { + return err + } + cfg := Dcrwallet() + // check and read dcrd config file + if authType == authTypeBasic && exists(dcrdDefaultConfigFile) { + // get rpc user, password info from dcrd + rpcUser, rpcPass, getRpcErr := getRpcConfigFromDcrdConfigFile(dcrdDefaultConfigFile) + if getRpcErr == nil { + // Replace the rpcuser and rpcpass lines in the sample configuration + // file contents with their generated values. + rpcUserRE := regexp.MustCompile(`(?m)^;\s*username=[^\s]*$`) + rpcPassRE := regexp.MustCompile(`(?m)^;\s*password=[^\s]*$`) + updatedCfg := rpcUserRE.ReplaceAllString(cfg, strings.ReplaceAll(rpcUser, "rpcuser", "username")) + updatedCfg = rpcPassRE.ReplaceAllString(updatedCfg, strings.ReplaceAll(rpcPass, "rpcpass", "password")) + cfg = updatedCfg + } + } + // Create config file at the provided path. + dest, err := os.OpenFile(destPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600) + if err != nil { + return err + } + defer dest.Close() + _, err = dest.WriteString(cfg) + return err +} +func getRpcConfigFromDcrdConfigFile(filePath string) (string, string, error) { + f, err := os.Open(filePath) + if err != nil { + return "", "", err + } + defer f.Close() + br := bufio.NewReader(f) + username := "" + password := "" + for { + line, err := br.ReadString('\n') + if errors.Is(err, io.EOF) { + break + } + if strings.HasPrefix(line, "rpcuser=") { + username = strings.TrimSpace(line) + } + if strings.HasPrefix(line, "rpcpass=") { + password = strings.TrimSpace(line) + } + } + if username == "" || password == "" { + return "", "", fmt.Errorf("get dcrd rpc info failed") + } + return username, password, nil +} + +// exists return true if the provided path exists. +func exists(path string) bool { + _, err := os.Stat(path) + return err == nil +} diff --git a/internal/prompt/prompt.go b/internal/prompt/prompt.go index 349ec73ed..e995b398c 100644 --- a/internal/prompt/prompt.go +++ b/internal/prompt/prompt.go @@ -104,6 +104,10 @@ func promptList(reader *bufio.Reader, prefix string, validResponses []string, de } } +func AskConfirmBool(reader *bufio.Reader, prefix string, defaultEntry string) (bool, error) { + return promptListBool(reader, prefix, defaultEntry) +} + // promptListBool prompts the user for a boolean (yes/no) with the given prefix. // The function will repeat the prompt to the user until they enter a valid // response. diff --git a/sampleconfig.go b/sampleconfig.go new file mode 100644 index 000000000..617d48da3 --- /dev/null +++ b/sampleconfig.go @@ -0,0 +1,24 @@ +// Copyright (c) 2017-2022 The Decred developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. +package main + +import ( + _ "embed" +) + +//go:embed sample-dcrwallet.conf +var sampleDcrwalletConf string + +// Dcrd returns a string containing the commented example config for dcrd. +func Dcrwallet() string { + return sampleDcrwalletConf +} + +// FileContents returns a string containing the commented example config for +// dcrwallet. +// +// Deprecated: Use the [Dcrwallet] function instead. +func FileContents() string { + return Dcrwallet() +} diff --git a/walletsetup.go b/walletsetup.go index a42474612..d4834b3e7 100644 --- a/walletsetup.go +++ b/walletsetup.go @@ -105,6 +105,13 @@ func displaySimnetMiningAddrs(seed []byte, imported bool) error { return nil } +// Confirm yes/no/y/n with confirm question and default confirmation value +func ConfirmBool(prefix, defaultEntry string) (bool, error) { + r := bufio.NewReader(os.Stdin) + confirmYes, err := prompt.AskConfirmBool(r, prefix, defaultEntry) + return confirmYes, err +} + // createWallet prompts the user for information needed to generate a new wallet // and generates the wallet accordingly. The new wallet will reside at the // provided path. The bool passed back gives whether or not the wallet was From 8bda852b1ce9298a8b844219530f29662310b9e8 Mon Sep 17 00:00:00 2001 From: vufon Date: Thu, 9 Jan 2025 03:34:16 +0700 Subject: [PATCH 2/7] Match rpc username/password attribute names with dcrd rpc attributes --- config.go | 21 +++++++++++++++++---- sample-dcrwallet.conf | 5 +++++ 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/config.go b/config.go index b60b07c72..97c1501a3 100644 --- a/config.go +++ b/config.go @@ -163,6 +163,8 @@ type config struct { LegacyRPCMaxWebsockets int64 `long:"rpcmaxwebsockets" description:"Max JSON-RPC websocket clients"` Username string `short:"u" long:"username" description:"JSON-RPC username and default dcrd RPC username"` Password string `short:"P" long:"password" default-mask:"-" description:"JSON-RPC password and default dcrd RPC password"` + RPCUser string `long:"rpcuser" description:"JSON-RPC username and default dcrd RPC username"` + RPCPass string `long:"rpcpass" default-mask:"-" description:"JSON-RPC password and default dcrd RPC password"` JSONRPCAuthType string `long:"jsonrpcauthtype" description:"Method for JSON-RPC client authentication (basic or clientcert)"` // IPC options @@ -461,6 +463,17 @@ func loadConfig(ctx context.Context) (*config, []string, error) { } } + if cfg.RPCUser != "" { + cfg.Username = cfg.RPCUser + } else { + log.Warn("The 'username' attribute in the config file is outdated. You should update it to 'rpcuser'") + } + if cfg.RPCPass != "" { + cfg.Password = cfg.RPCPass + } else { + log.Warn("The 'password' attribute in the config file is outdated. You should update it to 'rpcpass'") + } + // Parse command line options again to ensure they take precedence. remainingArgs, err := parser.Parse() if err != nil { @@ -1110,10 +1123,10 @@ func createDefaultConfigFile(destPath string, authType string) error { if getRpcErr == nil { // Replace the rpcuser and rpcpass lines in the sample configuration // file contents with their generated values. - rpcUserRE := regexp.MustCompile(`(?m)^;\s*username=[^\s]*$`) - rpcPassRE := regexp.MustCompile(`(?m)^;\s*password=[^\s]*$`) - updatedCfg := rpcUserRE.ReplaceAllString(cfg, strings.ReplaceAll(rpcUser, "rpcuser", "username")) - updatedCfg = rpcPassRE.ReplaceAllString(updatedCfg, strings.ReplaceAll(rpcPass, "rpcpass", "password")) + rpcUserRE := regexp.MustCompile(`(?m)^;\s*rpcuser=[^\s]*$`) + rpcPassRE := regexp.MustCompile(`(?m)^;\s*rpcpass=[^\s]*$`) + updatedCfg := rpcUserRE.ReplaceAllString(cfg, strings.ReplaceAll(rpcUser, "rpcuser", "rpcuser")) + updatedCfg = rpcPassRE.ReplaceAllString(updatedCfg, strings.ReplaceAll(rpcPass, "rpcpass", "rpcpass")) cfg = updatedCfg } } diff --git a/sample-dcrwallet.conf b/sample-dcrwallet.conf index e2ec3d3ba..fae900042 100644 --- a/sample-dcrwallet.conf +++ b/sample-dcrwallet.conf @@ -185,6 +185,11 @@ ; username= ; password= +; The official authentication information connects to the dcrd rpc server and authenticate +; new client connections to dcrwallet. +; rpcuser= +; rpcpass= + ; Alternative username and password for dcrd. If set, these will be used ; instead of the username and password set above for authentication to a ; dcrd RPC server. From 1092431e55d10bec8b3d96714415c2def2933401 Mon Sep 17 00:00:00 2001 From: vufon Date: Thu, 9 Jan 2025 03:35:19 +0700 Subject: [PATCH 3/7] Fix logic for no trusted CA exist warning in case rpc auth type is basic --- rpcserver.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rpcserver.go b/rpcserver.go index 6e3870f90..80610d7c8 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -297,11 +297,11 @@ func startRPCServers(walletLoader *loader.Loader) (*grpc.Server, *jsonrpc.Server } clientCAsExist = clientCAsExist || cfg.IssueClientCert - if !clientCAsExist && len(cfg.GRPCListeners) != 0 { + if !clientCAsExist && len(cfg.GRPCListeners) != 0 && cfg.JSONRPCAuthType == "clientcert" { log.Warnf("gRPC server is configured with listeners, but no "+ "trusted client certificates exist (looked in %v)", cfg.ClientCAFile) - } else if clientCAsExist && len(cfg.GRPCListeners) != 0 { + } else if clientCAsExist && len(cfg.GRPCListeners) != 0 && cfg.JSONRPCAuthType == "clientcert" { tlsConfig := tlsConfig.Clone() tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert listeners := makeListeners(cfg.GRPCListeners, net.Listen) From e4d7a526c044aa2d1481a8af729d9c042e15fcc0 Mon Sep 17 00:00:00 2001 From: vufon Date: Thu, 9 Jan 2025 03:38:33 +0700 Subject: [PATCH 4/7] Update rpc params from default dcrd rpc params on config file --- config.go | 71 ++++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 60 insertions(+), 11 deletions(-) diff --git a/config.go b/config.go index 97c1501a3..939edb002 100644 --- a/config.go +++ b/config.go @@ -461,6 +461,18 @@ func loadConfig(ctx context.Context) (*config, []string, error) { configFileError = err } } + } else { + // If rpc parameter is not set. Check dcrd rpc info and update config file + if cfg.RPCUser == "" && cfg.RPCPass == "" && cfg.Username == "" && cfg.Password == "" && preCfg.DcrdAuthType == authTypeBasic { + rpcUser, rpcPass, err := updateDefaultDcrdRPCInfos(configFilePath) + if err != nil { + log.Warnf("Updating rpc params from dcrd failed. \n %v", err) + } else { + cfg.RPCUser = rpcUser + cfg.RPCPass = rpcPass + log.Info("Update rpc params from dcrd successfully") + } + } } if cfg.RPCUser != "" { @@ -1107,6 +1119,32 @@ func loadConfig(ctx context.Context) (*config, []string, error) { return &cfg, remainingArgs, nil } +func updateDefaultDcrdRPCInfos(destPath string) (string, string, error) { + // get rpc user, password info from dcrd + dcrdRpcUser, dcrdRpcPass, getRpcErr := getRpcConfigFromDcrdConfigFile(dcrdDefaultConfigFile) + if getRpcErr != nil { + return "", "", getRpcErr + } + if dcrdRpcUser == "" && dcrdRpcPass == "" { + return "", "", fmt.Errorf("get dcrd rpc info failed") + } + cfgBuff, err := os.ReadFile(destPath) + if err != nil { + return "", "", err + } + cfg := string(cfgBuff) + rpcUserRE := regexp.MustCompile(`(?m)^;\s*rpcuser=[^\s]*$`) + rpcPassRE := regexp.MustCompile(`(?m)^;\s*rpcpass=[^\s]*$`) + updatedCfg := rpcUserRE.ReplaceAllString(cfg, strings.ReplaceAll(dcrdRpcUser, "rpcuser", "rpcuser")) + updatedCfg = rpcPassRE.ReplaceAllString(updatedCfg, strings.ReplaceAll(dcrdRpcPass, "rpcpass", "rpcpass")) + cfg = updatedCfg + err = os.WriteFile(destPath, []byte(cfg), 0644) + if err != nil { + return "", "", err + } + return dcrdRpcUser, dcrdRpcPass, nil +} + // createDefaultConfig copies the file sample-dcrd.conf to the given destination path, // and populates it with some randomly generated RPC username and password. func createDefaultConfigFile(destPath string, authType string) error { @@ -1117,17 +1155,28 @@ func createDefaultConfigFile(destPath string, authType string) error { } cfg := Dcrwallet() // check and read dcrd config file - if authType == authTypeBasic && exists(dcrdDefaultConfigFile) { - // get rpc user, password info from dcrd - rpcUser, rpcPass, getRpcErr := getRpcConfigFromDcrdConfigFile(dcrdDefaultConfigFile) - if getRpcErr == nil { - // Replace the rpcuser and rpcpass lines in the sample configuration - // file contents with their generated values. - rpcUserRE := regexp.MustCompile(`(?m)^;\s*rpcuser=[^\s]*$`) - rpcPassRE := regexp.MustCompile(`(?m)^;\s*rpcpass=[^\s]*$`) - updatedCfg := rpcUserRE.ReplaceAllString(cfg, strings.ReplaceAll(rpcUser, "rpcuser", "rpcuser")) - updatedCfg = rpcPassRE.ReplaceAllString(updatedCfg, strings.ReplaceAll(rpcPass, "rpcpass", "rpcpass")) - cfg = updatedCfg + if authType == authTypeBasic { + dcrdRpcUser := "" + dcrdRpcPass := "" + if exists(dcrdDefaultConfigFile) { + var getRpcErr error + // get rpc user, password info from dcrd + dcrdRpcUser, dcrdRpcPass, getRpcErr = getRpcConfigFromDcrdConfigFile(dcrdDefaultConfigFile) + if getRpcErr == nil { + // Replace the rpcuser and rpcpass lines in the sample configuration + // file contents with their generated values. + rpcUserRE := regexp.MustCompile(`(?m)^;\s*rpcuser=[^\s]*$`) + rpcPassRE := regexp.MustCompile(`(?m)^;\s*rpcpass=[^\s]*$`) + updatedCfg := rpcUserRE.ReplaceAllString(cfg, strings.ReplaceAll(dcrdRpcUser, "rpcuser", "rpcuser")) + updatedCfg = rpcPassRE.ReplaceAllString(updatedCfg, strings.ReplaceAll(dcrdRpcPass, "rpcpass", "rpcpass")) + cfg = updatedCfg + } + } + // If dcrd rpc info cannot be obtained, the dcrwallet.conf file is still created, but warns the user about setting rpc parameters manually. + if dcrdRpcUser == "" && dcrdRpcPass == "" { + log.Warnf("Unable to get rpc informations from dcrd. You need to set these parameters manually for the program to run correctly.\n"+ + "Config file path: %s", destPath) + } } // Create config file at the provided path. From e1ad8a379259f30be153362cfb5700760b5233d4 Mon Sep 17 00:00:00 2001 From: vufon Date: Thu, 9 Jan 2025 03:42:03 +0700 Subject: [PATCH 5/7] Edit some notifications --- config.go | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/config.go b/config.go index 939edb002..9e74e3dca 100644 --- a/config.go +++ b/config.go @@ -443,11 +443,10 @@ func loadConfig(ctx context.Context) (*config, []string, error) { // if path error, create default config file, assign default dcrd rpc config data if any createFileErr := createDefaultConfigFile(configFilePath, preCfg.DcrdAuthType) if createFileErr != nil { - fmt.Fprintf(os.Stderr, "Error creating a default "+ + fmt.Fprintf(os.Stderr, "Error creating default "+ "config file: %v\n", createFileErr) configFileError = createFileErr } else { - log.Warnf("Config file does not exist. New default file created: %s", configFilePath) configFileError = nil // Reparse data on config file err = flags.NewIniParser(parser).ParseFile(configFilePath) @@ -477,12 +476,12 @@ func loadConfig(ctx context.Context) (*config, []string, error) { if cfg.RPCUser != "" { cfg.Username = cfg.RPCUser - } else { + } else if cfg.Username != "" { log.Warn("The 'username' attribute in the config file is outdated. You should update it to 'rpcuser'") } if cfg.RPCPass != "" { cfg.Password = cfg.RPCPass - } else { + } else if cfg.Password != "" { log.Warn("The 'password' attribute in the config file is outdated. You should update it to 'rpcpass'") } @@ -1154,10 +1153,10 @@ func createDefaultConfigFile(destPath string, authType string) error { return err } cfg := Dcrwallet() + dcrdRpcUser := "" + dcrdRpcPass := "" // check and read dcrd config file if authType == authTypeBasic { - dcrdRpcUser := "" - dcrdRpcPass := "" if exists(dcrdDefaultConfigFile) { var getRpcErr error // get rpc user, password info from dcrd @@ -1172,12 +1171,6 @@ func createDefaultConfigFile(destPath string, authType string) error { cfg = updatedCfg } } - // If dcrd rpc info cannot be obtained, the dcrwallet.conf file is still created, but warns the user about setting rpc parameters manually. - if dcrdRpcUser == "" && dcrdRpcPass == "" { - log.Warnf("Unable to get rpc informations from dcrd. You need to set these parameters manually for the program to run correctly.\n"+ - "Config file path: %s", destPath) - - } } // Create config file at the provided path. dest, err := os.OpenFile(destPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600) @@ -1186,6 +1179,13 @@ func createDefaultConfigFile(destPath string, authType string) error { } defer dest.Close() _, err = dest.WriteString(cfg) + if err == nil { + log.Warnf("Config file does not exist. New default file created: %s", destPath) + } + // If dcrd rpc info cannot be obtained, the dcrwallet.conf file is still created, but warns the user about setting rpc parameters manually. + if dcrdRpcUser == "" && dcrdRpcPass == "" { + log.Warnf("Unable to get rpc informations from dcrd. Launch dcrd to automatically update or set it manually on the config file.") + } return err } func getRpcConfigFromDcrdConfigFile(filePath string) (string, string, error) { From 1a81b9a10b393d0f79beb6e7c3f36917712c8aab Mon Sep 17 00:00:00 2001 From: vufon Date: Tue, 14 Jan 2025 17:36:23 +0700 Subject: [PATCH 6/7] Provide instructions for running SPV mode when rpc information cannot be retrieved from dcrd automatically. Clean up sample config file --- config.go | 86 +++++++++++++++++++++++++++++++++++++------ sample-dcrwallet.conf | 5 --- 2 files changed, 75 insertions(+), 16 deletions(-) diff --git a/config.go b/config.go index 9e74e3dca..05c63e6dd 100644 --- a/config.go +++ b/config.go @@ -8,6 +8,8 @@ package main import ( "bufio" "context" + "crypto/rand" + "encoding/base64" "fmt" "io" "net" @@ -441,7 +443,7 @@ func loadConfig(ctx context.Context) (*config, []string, error) { return loadConfigError(err) } // if path error, create default config file, assign default dcrd rpc config data if any - createFileErr := createDefaultConfigFile(configFilePath, preCfg.DcrdAuthType) + dcrdExist, createFileErr := createDefaultConfigFile(configFilePath, preCfg.DcrdAuthType) if createFileErr != nil { fmt.Fprintf(os.Stderr, "Error creating default "+ "config file: %v\n", createFileErr) @@ -459,13 +461,27 @@ func loadConfig(ctx context.Context) (*config, []string, error) { } configFileError = err } + if !dcrdExist { + cfg, err = startWithSPVMode(cfg, configFilePath) + if err != nil { + log.Errorf("Start with SPV mode error: %v", err) + parser.WriteHelp(os.Stderr) + return loadConfigError(err) + } + } } } else { // If rpc parameter is not set. Check dcrd rpc info and update config file if cfg.RPCUser == "" && cfg.RPCPass == "" && cfg.Username == "" && cfg.Password == "" && preCfg.DcrdAuthType == authTypeBasic { rpcUser, rpcPass, err := updateDefaultDcrdRPCInfos(configFilePath) if err != nil { - log.Warnf("Updating rpc params from dcrd failed. \n %v", err) + log.Warnf("Updating rpc params from dcrd failed: %v", err) + cfg, err = startWithSPVMode(cfg, configFilePath) + if err != nil { + log.Errorf("Start with SPV mode error: %v", err) + parser.WriteHelp(os.Stderr) + return loadConfigError(err) + } } else { cfg.RPCUser = rpcUser cfg.RPCPass = rpcPass @@ -1118,6 +1134,51 @@ func loadConfig(ctx context.Context) (*config, []string, error) { return &cfg, remainingArgs, nil } +// In case RPC information is not automatically fetched from dcrd, ask the user whether they would like to run SPV mode. +// The RPC username and password will be automatically generated. +func startWithSPVMode(cfg config, cfgFilePath string) (config, error) { + spvSelect, err := ConfirmBool("Would you like to launch SPV mode? (If SPV mode is selected, the username and password will be automatically generated.)", "y") + if err != nil { + log.Errorf("spv mode setup error: %v", err) + return cfg, err + } + if spvSelect { + cfg.SPV = true + cfg.Offline = false + // Generate a random user and password for the RPC server credentials. + randomBytes := make([]byte, 20) + _, err = rand.Read(randomBytes) + if err != nil { + return cfg, err + } + randomUsername := base64.StdEncoding.EncodeToString(randomBytes) + _, err = rand.Read(randomBytes) + if err != nil { + return cfg, err + } + generatedRPCPass := base64.StdEncoding.EncodeToString(randomBytes) + cfgBuff, err := os.ReadFile(cfgFilePath) + if err != nil { + return cfg, err + } + cfgStr := string(cfgBuff) + rpcUserRE := regexp.MustCompile(`(?m)^;\s*rpcuser=[^\s]*$`) + rpcPassRE := regexp.MustCompile(`(?m)^;\s*rpcpass=[^\s]*$`) + updatedCfg := rpcUserRE.ReplaceAllString(cfgStr, fmt.Sprintf("rpcuser=%s", randomUsername)) + updatedCfg = rpcPassRE.ReplaceAllString(updatedCfg, fmt.Sprintf("rpcpass=%s", generatedRPCPass)) + cfgStr = updatedCfg + err = os.WriteFile(cfgFilePath, []byte(cfgStr), 0644) + if err != nil { + return cfg, err + } + cfg.RPCUser = randomUsername + cfg.Username = randomUsername + cfg.RPCPass = generatedRPCPass + cfg.Password = generatedRPCPass + } + return cfg, nil +} + func updateDefaultDcrdRPCInfos(destPath string) (string, string, error) { // get rpc user, password info from dcrd dcrdRpcUser, dcrdRpcPass, getRpcErr := getRpcConfigFromDcrdConfigFile(dcrdDefaultConfigFile) @@ -1134,8 +1195,8 @@ func updateDefaultDcrdRPCInfos(destPath string) (string, string, error) { cfg := string(cfgBuff) rpcUserRE := regexp.MustCompile(`(?m)^;\s*rpcuser=[^\s]*$`) rpcPassRE := regexp.MustCompile(`(?m)^;\s*rpcpass=[^\s]*$`) - updatedCfg := rpcUserRE.ReplaceAllString(cfg, strings.ReplaceAll(dcrdRpcUser, "rpcuser", "rpcuser")) - updatedCfg = rpcPassRE.ReplaceAllString(updatedCfg, strings.ReplaceAll(dcrdRpcPass, "rpcpass", "rpcpass")) + updatedCfg := rpcUserRE.ReplaceAllString(cfg, dcrdRpcUser) + updatedCfg = rpcPassRE.ReplaceAllString(updatedCfg, dcrdRpcPass) cfg = updatedCfg err = os.WriteFile(destPath, []byte(cfg), 0644) if err != nil { @@ -1146,11 +1207,11 @@ func updateDefaultDcrdRPCInfos(destPath string) (string, string, error) { // createDefaultConfig copies the file sample-dcrd.conf to the given destination path, // and populates it with some randomly generated RPC username and password. -func createDefaultConfigFile(destPath string, authType string) error { +func createDefaultConfigFile(destPath string, authType string) (dcrdExist bool, err error) { // Create the destination directory if it does not exist. - err := os.MkdirAll(filepath.Dir(destPath), 0700) + err = os.MkdirAll(filepath.Dir(destPath), 0700) if err != nil { - return err + return false, err } cfg := Dcrwallet() dcrdRpcUser := "" @@ -1166,8 +1227,8 @@ func createDefaultConfigFile(destPath string, authType string) error { // file contents with their generated values. rpcUserRE := regexp.MustCompile(`(?m)^;\s*rpcuser=[^\s]*$`) rpcPassRE := regexp.MustCompile(`(?m)^;\s*rpcpass=[^\s]*$`) - updatedCfg := rpcUserRE.ReplaceAllString(cfg, strings.ReplaceAll(dcrdRpcUser, "rpcuser", "rpcuser")) - updatedCfg = rpcPassRE.ReplaceAllString(updatedCfg, strings.ReplaceAll(dcrdRpcPass, "rpcpass", "rpcpass")) + updatedCfg := rpcUserRE.ReplaceAllString(cfg, dcrdRpcUser) + updatedCfg = rpcPassRE.ReplaceAllString(updatedCfg, dcrdRpcPass) cfg = updatedCfg } } @@ -1175,18 +1236,21 @@ func createDefaultConfigFile(destPath string, authType string) error { // Create config file at the provided path. dest, err := os.OpenFile(destPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600) if err != nil { - return err + return false, err } defer dest.Close() _, err = dest.WriteString(cfg) if err == nil { log.Warnf("Config file does not exist. New default file created: %s", destPath) + } else { + return false, err } // If dcrd rpc info cannot be obtained, the dcrwallet.conf file is still created, but warns the user about setting rpc parameters manually. if dcrdRpcUser == "" && dcrdRpcPass == "" { log.Warnf("Unable to get rpc informations from dcrd. Launch dcrd to automatically update or set it manually on the config file.") + return false, nil } - return err + return true, nil } func getRpcConfigFromDcrdConfigFile(filePath string) (string, string, error) { f, err := os.Open(filePath) diff --git a/sample-dcrwallet.conf b/sample-dcrwallet.conf index fae900042..f58ee48e0 100644 --- a/sample-dcrwallet.conf +++ b/sample-dcrwallet.conf @@ -180,11 +180,6 @@ ; RPC settings (both client and server) ; ------------------------------------------------------------------------------ -; Username and password to authenticate to a dcrd RPC server and authenticate -; new client connections to dcrwallet. -; username= -; password= - ; The official authentication information connects to the dcrd rpc server and authenticate ; new client connections to dcrwallet. ; rpcuser= From 8e17c47030c6536cac4a866a6cac6f33817e1512 Mon Sep 17 00:00:00 2001 From: vufon Date: Tue, 14 Jan 2025 18:46:30 +0700 Subject: [PATCH 7/7] Adjust notification message when configuring for SPV mode. Set rpcuser and rpcpass automatically when enable SPV mode in launch command --- config.go | 105 ++++++++++++++++++++++++++++++++---------------------- 1 file changed, 63 insertions(+), 42 deletions(-) diff --git a/config.go b/config.go index 05c63e6dd..54fc7b5fe 100644 --- a/config.go +++ b/config.go @@ -435,6 +435,7 @@ func loadConfig(ctx context.Context) (*config, []string, error) { } } err = flags.NewIniParser(parser).ParseFile(configFilePath) + manualSPVConfig := false if err != nil { var e *os.PathError if !errors.As(err, &e) { @@ -462,12 +463,7 @@ func loadConfig(ctx context.Context) (*config, []string, error) { configFileError = err } if !dcrdExist { - cfg, err = startWithSPVMode(cfg, configFilePath) - if err != nil { - log.Errorf("Start with SPV mode error: %v", err) - parser.WriteHelp(os.Stderr) - return loadConfigError(err) - } + manualSPVConfig = true } } } else { @@ -476,12 +472,7 @@ func loadConfig(ctx context.Context) (*config, []string, error) { rpcUser, rpcPass, err := updateDefaultDcrdRPCInfos(configFilePath) if err != nil { log.Warnf("Updating rpc params from dcrd failed: %v", err) - cfg, err = startWithSPVMode(cfg, configFilePath) - if err != nil { - log.Errorf("Start with SPV mode error: %v", err) - parser.WriteHelp(os.Stderr) - return loadConfigError(err) - } + manualSPVConfig = true } else { cfg.RPCUser = rpcUser cfg.RPCPass = rpcPass @@ -510,7 +501,14 @@ func loadConfig(ctx context.Context) (*config, []string, error) { } return loadConfigError(err) } - + if manualSPVConfig { + cfg, err = configWithSPVMode(cfg, configFilePath) + if err != nil { + log.Errorf("SPV mode setting error: %v", err) + parser.WriteHelp(os.Stderr) + return loadConfigError(err) + } + } // If an alternate data directory was specified, and paths with defaults // relative to the data dir are unchanged, modify each path to be // relative to the new data dir. @@ -1134,51 +1132,74 @@ func loadConfig(ctx context.Context) (*config, []string, error) { return &cfg, remainingArgs, nil } +// Config with SPV mode +func configWithSPVMode(cfg config, cfgFilePath string) (config, error) { + var err error + if cfg.SPV { + cfg, err = autoGenerateRpcUserPass(cfg, cfgFilePath) + } else { + cfg, err = spvConfigWithQuestion(cfg, cfgFilePath) + } + if err != nil { + return cfg, err + } + return cfg, nil +} + // In case RPC information is not automatically fetched from dcrd, ask the user whether they would like to run SPV mode. // The RPC username and password will be automatically generated. -func startWithSPVMode(cfg config, cfgFilePath string) (config, error) { +func spvConfigWithQuestion(cfg config, cfgFilePath string) (config, error) { spvSelect, err := ConfirmBool("Would you like to launch SPV mode? (If SPV mode is selected, the username and password will be automatically generated.)", "y") if err != nil { - log.Errorf("spv mode setup error: %v", err) return cfg, err } if spvSelect { cfg.SPV = true cfg.Offline = false - // Generate a random user and password for the RPC server credentials. - randomBytes := make([]byte, 20) - _, err = rand.Read(randomBytes) - if err != nil { - return cfg, err - } - randomUsername := base64.StdEncoding.EncodeToString(randomBytes) - _, err = rand.Read(randomBytes) + cfg, err = autoGenerateRpcUserPass(cfg, cfgFilePath) if err != nil { return cfg, err } - generatedRPCPass := base64.StdEncoding.EncodeToString(randomBytes) - cfgBuff, err := os.ReadFile(cfgFilePath) - if err != nil { - return cfg, err - } - cfgStr := string(cfgBuff) - rpcUserRE := regexp.MustCompile(`(?m)^;\s*rpcuser=[^\s]*$`) - rpcPassRE := regexp.MustCompile(`(?m)^;\s*rpcpass=[^\s]*$`) - updatedCfg := rpcUserRE.ReplaceAllString(cfgStr, fmt.Sprintf("rpcuser=%s", randomUsername)) - updatedCfg = rpcPassRE.ReplaceAllString(updatedCfg, fmt.Sprintf("rpcpass=%s", generatedRPCPass)) - cfgStr = updatedCfg - err = os.WriteFile(cfgFilePath, []byte(cfgStr), 0644) - if err != nil { - return cfg, err - } - cfg.RPCUser = randomUsername - cfg.Username = randomUsername - cfg.RPCPass = generatedRPCPass - cfg.Password = generatedRPCPass } return cfg, nil } +// Automatically generate rpcuser and rpcpassword and save to config file +func autoGenerateRpcUserPass(cfg config, configFilePath string) (config, error) { + // Generate a random user and password for the RPC server credentials. + randomBytes := make([]byte, 20) + _, err := rand.Read(randomBytes) + if err != nil { + return cfg, err + } + randomUsername := base64.StdEncoding.EncodeToString(randomBytes) + _, err = rand.Read(randomBytes) + if err != nil { + return cfg, err + } + generatedRPCPass := base64.StdEncoding.EncodeToString(randomBytes) + cfgBuff, err := os.ReadFile(configFilePath) + if err != nil { + return cfg, err + } + cfgStr := string(cfgBuff) + rpcUserRE := regexp.MustCompile(`(?m)^;\s*rpcuser=[^\s]*$`) + rpcPassRE := regexp.MustCompile(`(?m)^;\s*rpcpass=[^\s]*$`) + updatedCfg := rpcUserRE.ReplaceAllString(cfgStr, fmt.Sprintf("rpcuser=%s", randomUsername)) + updatedCfg = rpcPassRE.ReplaceAllString(updatedCfg, fmt.Sprintf("rpcpass=%s", generatedRPCPass)) + cfgStr = updatedCfg + err = os.WriteFile(configFilePath, []byte(cfgStr), 0644) + if err != nil { + return cfg, err + } + cfg.RPCUser = randomUsername + cfg.Username = randomUsername + cfg.RPCPass = generatedRPCPass + cfg.Password = generatedRPCPass + log.Info("The RPC username and password were randomly generated and saved in the configuration file.") + return cfg, nil +} + func updateDefaultDcrdRPCInfos(destPath string) (string, string, error) { // get rpc user, password info from dcrd dcrdRpcUser, dcrdRpcPass, getRpcErr := getRpcConfigFromDcrdConfigFile(dcrdDefaultConfigFile)