lbcwallet/config.go
Roy Lee 1fa143fa0e wallet: update passphrase user experience.
For users don't want to set/manage a passphrase a default passphrase
"passphrase" will be used during wallet creation.

At startup, the wallet tries to unlock itself using the default
passphrase, or a user provided one (using -p).

Users that prefer a passphrase can override the default one at wallet
creation time using -p, or use the walletpassphrase rpc when
the wallet is running. This will prevent the wallet from auto-unlock,
and preserve the lock-by-default behavior.
2022-10-31 19:53:16 -07:00

556 lines
19 KiB
Go

// Copyright (c) 2013-2017 The btcsuite developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
package main
import (
"fmt"
"net"
"os"
"os/user"
"path/filepath"
"runtime"
"sort"
"strings"
"time"
flags "github.com/jessevdk/go-flags"
"github.com/lbryio/lbcd/version"
btcutil "github.com/lbryio/lbcutil"
"github.com/lbryio/lbcwallet/internal/cfgutil"
"github.com/lbryio/lbcwallet/netparams"
"github.com/lbryio/lbcwallet/wallet"
)
const (
defaultCAFilename = "lbcd.cert"
defaultConfigFilename = "lbcwallet.conf"
defaultLogLevel = "info"
defaultLogDirname = "logs"
defaultLogFilename = "lbcwallet.log"
defaultRPCMaxClients = 10
defaultRPCMaxWebsockets = 25
defaultPassphrase = "password"
)
var (
lbcdDefaultCAFile = filepath.Join(btcutil.AppDataDir("lbcd", false), "rpc.cert")
defaultAppDataDir = btcutil.AppDataDir("lbcwallet", false)
defaultConfigFile = filepath.Join(defaultAppDataDir, defaultConfigFilename)
defaultRPCKeyFile = filepath.Join(defaultAppDataDir, "rpc.key")
defaultRPCCertFile = filepath.Join(defaultAppDataDir, "rpc.cert")
defaultLogDir = filepath.Join(defaultAppDataDir, defaultLogDirname)
)
type config struct {
// General application behavior
ConfigFile *cfgutil.ExplicitString `short:"C" long:"configfile" description:"Path to configuration file"`
ShowVersion bool `short:"V" long:"version" description:"Display version information and exit"`
Create bool `long:"create" description:"Create the wallet if it does not exist"`
CreateTemp bool `long:"createtemp" description:"Create a temporary simulation wallet (pass=password) in the data directory indicated; must call with --datadir"`
AppDataDir *cfgutil.ExplicitString `short:"A" long:"appdata" description:"Application data directory for wallet config, databases and logs"`
TestNet3 bool `long:"testnet" description:"Use the test Bitcoin network (version 3) (default client port: 19244, server port: 19245)"`
Regtest bool `long:"regtest" description:"Use the regression test network (default client port: 29244, server port: 29245)"`
DebugLevel string `short:"d" long:"debuglevel" description:"Logging level {trace, debug, info, warn, error, critical}"`
LogDir string `long:"logdir" description:"Directory to log output."`
Profile string `long:"profile" description:"Enable HTTP profiling on given port -- NOTE port must be between 1024 and 65536"`
DBTimeout time.Duration `long:"dbtimeout" description:"The timeout value to use when opening the wallet database."`
// Passphrase options
Passphrase string `short:"p" long:"passphrase" default-mask:"-" description:"The wallet passphrase (default: \"passphrase\")"`
// RPC client options
RPCConnect string `short:"c" long:"rpcconnect" description:"Hostname/IP and port of lbcd RPC server to connect to (default localhost:9245, testnet: localhost:19245, regtest: localhost:29245)"`
CAFile *cfgutil.ExplicitString `long:"cafile" description:"File containing root certificates to authenticate a TLS connections with lbcd"`
DisableClientTLS bool `long:"noclienttls" description:"Disable TLS for the RPC client"`
SkipVerify bool `long:"skipverify" description:"Skip verifying TLS for the RPC client"`
Proxy string `long:"proxy" description:"Connect via SOCKS5 proxy (eg. 127.0.0.1:9050)"`
ProxyUser string `long:"proxyuser" description:"Username for proxy server"`
ProxyPass string `long:"proxypass" default-mask:"-" description:"Password for proxy server"`
// RPC server options
RPCCert *cfgutil.ExplicitString `long:"rpccert" description:"File containing the certificate file"`
RPCKey *cfgutil.ExplicitString `long:"rpckey" description:"File containing the certificate key"`
OneTimeTLSKey bool `long:"onetimetlskey" description:"Generate a new TLS certpair at startup, but only write the certificate to disk"`
DisableServerTLS bool `long:"noservertls" description:"Disable TLS for the RPC server"`
LegacyRPCListeners []string `long:"rpclisten" description:"Listen for legacy RPC connections on this interface/port (default port: 9244, testnet: 19244, regtest: 29244)"`
LegacyRPCMaxClients int64 `long:"rpcmaxclients" description:"Max number of legacy RPC clients for standard connections"`
LegacyRPCMaxWebsockets int64 `long:"rpcmaxwebsockets" description:"Max number of RPC websocket connections"`
RPCUser string `short:"u" long:"rpcuser" description:"Username for RPC and lbcd authentication"`
RPCPass string `short:"P" long:"rpcpass" default-mask:"-" description:"Password for RPC and lbcd authentication"`
// Deprecated options
DataDir *cfgutil.ExplicitString `short:"b" long:"datadir" default-mask:"-" description:"DEPRECATED -- use appdata instead"`
}
// cleanAndExpandPath expands environement variables and leading ~ in the
// passed path, cleans the result, and returns it.
func cleanAndExpandPath(path string) string {
// NOTE: The os.ExpandEnv doesn't work with Windows cmd.exe-style
// %VARIABLE%, but they variables can still be expanded via POSIX-style
// $VARIABLE.
path = os.ExpandEnv(path)
if !strings.HasPrefix(path, "~") {
return filepath.Clean(path)
}
// Expand initial ~ to the current user's home directory, or ~otheruser
// to otheruser's home directory. On Windows, both forward and backward
// slashes can be used.
path = path[1:]
var pathSeparators string
if runtime.GOOS == "windows" {
pathSeparators = string(os.PathSeparator) + "/"
} else {
pathSeparators = string(os.PathSeparator)
}
userName := ""
if i := strings.IndexAny(path, pathSeparators); i != -1 {
userName = path[:i]
path = path[i:]
}
homeDir := ""
var u *user.User
var err error
if userName == "" {
u, err = user.Current()
} else {
u, err = user.Lookup(userName)
}
if err == nil {
homeDir = u.HomeDir
}
// Fallback to CWD if user lookup fails or user has no home directory.
if homeDir == "" {
homeDir = "."
}
return filepath.Join(homeDir, path)
}
// validLogLevel returns whether or not logLevel is a valid debug log level.
func validLogLevel(logLevel string) bool {
switch logLevel {
case "trace":
fallthrough
case "debug":
fallthrough
case "info":
fallthrough
case "warn":
fallthrough
case "error":
fallthrough
case "critical":
return true
}
return false
}
// supportedSubsystems returns a sorted slice of the supported subsystems for
// logging purposes.
func supportedSubsystems() []string {
// Convert the subsystemLoggers map keys to a slice.
subsystems := make([]string, 0, len(subsystemLoggers))
for subsysID := range subsystemLoggers {
subsystems = append(subsystems, subsysID)
}
// Sort the subsytems for stable display.
sort.Strings(subsystems)
return subsystems
}
// parseAndSetDebugLevels attempts to parse the specified debug level and set
// the levels accordingly. An appropriate error is returned if anything is
// invalid.
func parseAndSetDebugLevels(debugLevel string) error {
// When the specified string doesn't have any delimters, treat it as
// the log level for all subsystems.
if !strings.Contains(debugLevel, ",") && !strings.Contains(debugLevel, "=") {
// Validate debug log level.
if !validLogLevel(debugLevel) {
str := "the specified debug level [%v] is invalid"
return fmt.Errorf(str, debugLevel)
}
// Change the logging level for all subsystems.
setLogLevels(debugLevel)
return nil
}
// Split the specified string into subsystem/level pairs while detecting
// issues and update the log levels accordingly.
for _, logLevelPair := range strings.Split(debugLevel, ",") {
if !strings.Contains(logLevelPair, "=") {
str := "the specified debug level contains an invalid " +
"subsystem/level pair [%v]"
return fmt.Errorf(str, logLevelPair)
}
// Extract the specified subsystem and log level.
fields := strings.Split(logLevelPair, "=")
subsysID, logLevel := fields[0], fields[1]
// Validate subsystem.
if _, exists := subsystemLoggers[subsysID]; !exists {
str := "the specified subsystem [%v] is invalid -- " +
"supported subsytems %v"
return fmt.Errorf(str, subsysID, supportedSubsystems())
}
// Validate log level.
if !validLogLevel(logLevel) {
str := "the specified debug level [%v] is invalid"
return fmt.Errorf(str, logLevel)
}
setLogLevel(subsysID, logLevel)
}
return nil
}
// loadConfig initializes and parses the config using a config file and command
// line options.
//
// The configuration proceeds as follows:
// 1. Start with a default config with sane settings
// 2. Pre-parse the command line to check for an alternative config file
// 3. Load configuration file overwriting defaults with any specified options
// 4. Parse CLI options and overwrite/add any specified options
//
// The above results in lbcwallet functioning properly without any config
// settings while still allowing the user to override settings with config files
// and command line options. Command line options always take precedence.
func loadConfig() (*config, []string, error) {
// Default config.
cfg := config{
DebugLevel: defaultLogLevel,
ConfigFile: cfgutil.NewExplicitString(defaultConfigFile),
AppDataDir: cfgutil.NewExplicitString(defaultAppDataDir),
LogDir: defaultLogDir,
CAFile: cfgutil.NewExplicitString(""),
RPCKey: cfgutil.NewExplicitString(defaultRPCKeyFile),
RPCCert: cfgutil.NewExplicitString(defaultRPCCertFile),
LegacyRPCMaxClients: defaultRPCMaxClients,
LegacyRPCMaxWebsockets: defaultRPCMaxWebsockets,
DataDir: cfgutil.NewExplicitString(defaultAppDataDir),
DBTimeout: wallet.DefaultDBTimeout,
Passphrase: defaultPassphrase,
}
// Pre-parse the command line options to see if an alternative config
// file or the version flag was specified.
preCfg := cfg
preParser := flags.NewParser(&preCfg, flags.Default)
_, err := preParser.Parse()
if err != nil {
if e, ok := err.(*flags.Error); !ok || e.Type != flags.ErrHelp {
preParser.WriteHelp(os.Stderr)
}
return nil, nil, err
}
// Show the version and exit if the version flag was specified.
funcName := "loadConfig"
appName := filepath.Base(os.Args[0])
appName = strings.TrimSuffix(appName, filepath.Ext(appName))
usageMessage := fmt.Sprintf("Use %s -h to show usage", appName)
if preCfg.ShowVersion {
fmt.Println(appName, "version", version.Full())
os.Exit(0)
}
// Load additional config from file.
var configFileError error
parser := flags.NewParser(&cfg, flags.Default)
configFilePath := preCfg.ConfigFile.Value
if preCfg.ConfigFile.ExplicitlySet() {
configFilePath = cleanAndExpandPath(configFilePath)
} else {
appDataDir := preCfg.AppDataDir.Value
if !preCfg.AppDataDir.ExplicitlySet() && preCfg.DataDir.ExplicitlySet() {
appDataDir = cleanAndExpandPath(preCfg.DataDir.Value)
}
if appDataDir != defaultAppDataDir {
configFilePath = filepath.Join(appDataDir, defaultConfigFilename)
}
}
err = flags.NewIniParser(parser).ParseFile(configFilePath)
if err != nil {
if _, ok := err.(*os.PathError); !ok {
fmt.Fprintln(os.Stderr, err)
parser.WriteHelp(os.Stderr)
return nil, nil, err
}
configFileError = err
}
// Parse command line options again to ensure they take precedence.
remainingArgs, err := parser.Parse()
if err != nil {
if e, ok := err.(*flags.Error); !ok || e.Type != flags.ErrHelp {
parser.WriteHelp(os.Stderr)
}
return nil, nil, err
}
// Check deprecated aliases. The new options receive priority when both
// are changed from the default.
if cfg.DataDir.ExplicitlySet() {
fmt.Fprintln(os.Stderr, "datadir option has been replaced by "+
"appdata -- please update your config")
if !cfg.AppDataDir.ExplicitlySet() {
cfg.AppDataDir.Value = cfg.DataDir.Value
}
}
// 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.
if cfg.AppDataDir.ExplicitlySet() {
cfg.AppDataDir.Value = cleanAndExpandPath(cfg.AppDataDir.Value)
if !cfg.RPCKey.ExplicitlySet() {
cfg.RPCKey.Value = filepath.Join(cfg.AppDataDir.Value, "rpc.key")
}
if !cfg.RPCCert.ExplicitlySet() {
cfg.RPCCert.Value = filepath.Join(cfg.AppDataDir.Value, "rpc.cert")
}
}
// Choose the active network params based on the selected network.
// Multiple networks can't be selected simultaneously.
numNets := 0
if cfg.TestNet3 {
activeNet = &netparams.TestNet3Params
numNets++
}
if cfg.Regtest {
activeNet = &netparams.RegTestParams
numNets++
}
if numNets > 1 {
str := "%s: more than one networks has been specified"
err := fmt.Errorf(str, "loadConfig")
fmt.Fprintln(os.Stderr, err)
parser.WriteHelp(os.Stderr)
return nil, nil, err
}
// Append the network type to the log directory so it is "namespaced"
// per network.
cfg.LogDir = cleanAndExpandPath(cfg.LogDir)
cfg.LogDir = filepath.Join(cfg.LogDir, activeNet.Params.Name)
// Special show command to list supported subsystems and exit.
if cfg.DebugLevel == "show" {
fmt.Println("Supported subsystems", supportedSubsystems())
os.Exit(0)
}
// Initialize log rotation. After log rotation has been initialized, the
// logger variables may be used.
initLogRotator(filepath.Join(cfg.LogDir, defaultLogFilename))
// Parse, validate, and set debug log level(s).
if err := parseAndSetDebugLevels(cfg.DebugLevel); err != nil {
err := fmt.Errorf("%s: %v", "loadConfig", err.Error())
fmt.Fprintln(os.Stderr, err)
parser.WriteHelp(os.Stderr)
return nil, nil, err
}
if cfg.CreateTemp {
errMsg := "Tried to create a temporary simulation wallet"
// Exit if you try to use a simulation wallet with a standard
// data directory.
if !(cfg.AppDataDir.ExplicitlySet() || cfg.DataDir.ExplicitlySet()) {
errMsg += ", but failed to specify data directory!"
fmt.Fprintln(os.Stderr, errMsg)
os.Exit(0)
}
// Exit if you try to use a simulation wallet on anything other than
// regtest or testnet3.
if !(cfg.Regtest || cfg.TestNet3) {
errMsg += "for network other than regtest, or testnet3"
fmt.Fprintln(os.Stderr, errMsg)
os.Exit(0)
}
}
// Ensure the wallet exists or create it when the create flag is set.
netDir := networkDir(cfg.AppDataDir.Value, activeNet.Params)
dbPath := filepath.Join(netDir, wallet.WalletDBName)
if cfg.CreateTemp && cfg.Create {
err := fmt.Errorf("the flags --create and --createtemp can not " +
"be specified together. Use --help for more information")
fmt.Fprintln(os.Stderr, err)
return nil, nil, err
}
dbFileExists, err := cfgutil.FileExists(dbPath)
if err != nil {
fmt.Fprintln(os.Stderr, err)
return nil, nil, err
}
if cfg.CreateTemp { // nolint:gocritic
tempWalletExists := false
if dbFileExists {
str := fmt.Sprintf("The wallet already exists. Loading this " +
"wallet instead.")
fmt.Fprintln(os.Stdout, str)
tempWalletExists = true
}
// Ensure the data directory for the network exists.
if err := checkCreateDir(netDir); err != nil {
fmt.Fprintln(os.Stderr, err)
return nil, nil, err
}
if !tempWalletExists {
// Perform the initial wallet creation wizard.
if err := createSimulationWallet(&cfg); err != nil {
fmt.Fprintln(os.Stderr, "Unable to create wallet:", err)
return nil, nil, err
}
}
} else if cfg.Create {
// Error if the create flag is set and the wallet already
// exists.
if dbFileExists {
err := fmt.Errorf("the wallet database file `%v` "+
"already exists", dbPath)
fmt.Fprintln(os.Stderr, err)
return nil, nil, err
}
// Ensure the data directory for the network exists.
if err := checkCreateDir(netDir); err != nil {
fmt.Fprintln(os.Stderr, err)
return nil, nil, err
}
// Perform the initial wallet creation wizard.
if err := createWallet(&cfg); err != nil {
fmt.Fprintln(os.Stderr, "Unable to create wallet:", err)
return nil, nil, err
}
// Created successfully, so exit now with success.
os.Exit(0)
}
localhostListeners := map[string]struct{}{
"localhost": {},
"127.0.0.1": {},
"::1": {},
}
if cfg.RPCConnect == "" {
cfg.RPCConnect = net.JoinHostPort("localhost", activeNet.RPCClientPort)
}
// Add default port to connect flag if missing.
cfg.RPCConnect, err = cfgutil.NormalizeAddress(cfg.RPCConnect,
activeNet.RPCClientPort)
if err != nil {
fmt.Fprintf(os.Stderr,
"Invalid rpcconnect network address: %v\n", err)
return nil, nil, err
}
RPCHost, _, err := net.SplitHostPort(cfg.RPCConnect)
if err != nil {
return nil, nil, err
}
if !cfg.DisableClientTLS {
// If CAFile is unset, choose either the copy or local lbcd cert.
if !cfg.CAFile.ExplicitlySet() {
cfg.CAFile.Value = filepath.Join(cfg.AppDataDir.Value, defaultCAFilename)
// If the CA copy does not exist, check if we're connecting to
// a local lbcd and switch to its RPC cert if it exists.
certExists, err := cfgutil.FileExists(cfg.CAFile.Value)
if err != nil {
fmt.Fprintln(os.Stderr, err)
return nil, nil, err
}
if !certExists {
if _, ok := localhostListeners[RPCHost]; ok {
lbcdCertExists, err := cfgutil.FileExists(
lbcdDefaultCAFile)
if err != nil {
fmt.Fprintln(os.Stderr, err)
return nil, nil, err
}
if lbcdCertExists {
cfg.CAFile.Value = lbcdDefaultCAFile
}
}
}
}
}
if len(cfg.LegacyRPCListeners) == 0 {
addrs, err := net.LookupHost("localhost")
if err != nil {
return nil, nil, err
}
cfg.LegacyRPCListeners = make([]string, 0, len(addrs))
for _, addr := range addrs {
addr = net.JoinHostPort(addr, activeNet.RPCServerPort)
cfg.LegacyRPCListeners = append(cfg.LegacyRPCListeners, addr)
}
}
// Add default port to all rpc listener addresses if needed and remove
// duplicate addresses.
cfg.LegacyRPCListeners, err = cfgutil.NormalizeAddresses(
cfg.LegacyRPCListeners, activeNet.RPCServerPort)
if err != nil {
fmt.Fprintf(os.Stderr,
"Invalid network address in legacy RPC listeners: %v\n", err)
return nil, nil, err
}
if cfg.DisableServerTLS {
for _, addr := range cfg.LegacyRPCListeners {
_, _, err := net.SplitHostPort(addr)
if err != nil {
str := "%s: RPC listen interface '%s' is " +
"invalid: %v"
err := fmt.Errorf(str, funcName, addr, err)
fmt.Fprintln(os.Stderr, err)
fmt.Fprintln(os.Stderr, usageMessage)
return nil, nil, err
}
}
}
// Expand environment variable and leading ~ for filepaths.
cfg.CAFile.Value = cleanAndExpandPath(cfg.CAFile.Value)
cfg.RPCCert.Value = cleanAndExpandPath(cfg.RPCCert.Value)
cfg.RPCKey.Value = cleanAndExpandPath(cfg.RPCKey.Value)
// Warn about missing config file after the final command line parse
// succeeds. This prevents the warning on help messages and invalid
// options.
if configFileError != nil {
log.Warnf("%v", configFileError)
}
return &cfg, remainingArgs, nil
}