From f3df6c8bc9e2d6f9b69112ce835ad6f2cb4a375a Mon Sep 17 00:00:00 2001 From: Josh Rickmar Date: Thu, 5 May 2016 10:29:29 -0400 Subject: [PATCH] Handle flags explicitly set to defaults. This prevents treating a flag that was explicitly set to the default as unchanged, since the explicit set is recorded in the new *cfgutil.ExplicitString flag type. --- btcwallet.go | 4 +- config.go | 119 +++++++++++++++--------------- internal/cfgutil/explicitflags.go | 36 +++++++++ rpcserver.go | 16 ++-- walletsetup.go | 6 +- 5 files changed, 109 insertions(+), 72 deletions(-) create mode 100644 internal/cfgutil/explicitflags.go diff --git a/btcwallet.go b/btcwallet.go index f262198..ec0dedf 100644 --- a/btcwallet.go +++ b/btcwallet.go @@ -61,7 +61,7 @@ func walletMain() error { }() } - dbDir := networkDir(cfg.AppDataDir, activeNet.Params) + dbDir := networkDir(cfg.AppDataDir.Value, activeNet.Params) loader := wallet.NewLoader(activeNet.Params, dbDir) // Create and start HTTP server to serve wallet client connections. @@ -197,7 +197,7 @@ func readCAFile() []byte { var certs []byte if !cfg.DisableClientTLS { var err error - certs, err = ioutil.ReadFile(cfg.CAFile) + certs, err = ioutil.ReadFile(cfg.CAFile.Value) if err != nil { log.Warnf("Cannot open CA file: %v", err) // If there's an error reading the CA file, continue diff --git a/config.go b/config.go index 0b0f958..d4b6695 100644 --- a/config.go +++ b/config.go @@ -45,30 +45,30 @@ var ( type config struct { // General application behavior - ConfigFile string `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 string `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 mainnet)"` - SimNet bool `long:"simnet" description:"Use the simulation test network (default mainnet)"` - NoInitialLoad bool `long:"noinitialload" description:"Defer wallet creation/opening on startup and enable loading wallets over RPC"` - 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"` + 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 mainnet)"` + SimNet bool `long:"simnet" description:"Use the simulation test network (default mainnet)"` + NoInitialLoad bool `long:"noinitialload" description:"Defer wallet creation/opening on startup and enable loading wallets over RPC"` + 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"` // Wallet options WalletPass string `long:"walletpass" default-mask:"-" description:"The public wallet password -- Only required if the wallet was created with one"` // RPC client options - RPCConnect string `short:"c" long:"rpcconnect" description:"Hostname/IP and port of btcd RPC server to connect to (default localhost:8334, testnet: localhost:18334, simnet: localhost:18556)"` - CAFile string `long:"cafile" description:"File containing root certificates to authenticate a TLS connections with btcd"` - DisableClientTLS bool `long:"noclienttls" description:"Disable TLS for the RPC client -- NOTE: This is only allowed if the RPC client is connecting to localhost"` - BtcdUsername string `long:"btcdusername" description:"Username for btcd authentication"` - BtcdPassword string `long:"btcdpassword" default-mask:"-" description:"Password for btcd authentication"` - 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"` + RPCConnect string `short:"c" long:"rpcconnect" description:"Hostname/IP and port of btcd RPC server to connect to (default localhost:8334, testnet: localhost:18334, simnet: localhost:18556)"` + CAFile *cfgutil.ExplicitString `long:"cafile" description:"File containing root certificates to authenticate a TLS connections with btcd"` + DisableClientTLS bool `long:"noclienttls" description:"Disable TLS for the RPC client -- NOTE: This is only allowed if the RPC client is connecting to localhost"` + BtcdUsername string `long:"btcdusername" description:"Username for btcd authentication"` + BtcdPassword string `long:"btcdpassword" default-mask:"-" description:"Password for btcd authentication"` + 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 // @@ -78,15 +78,15 @@ type config struct { // // Usernames can also be used for the consensus RPC client, so they // aren't considered legacy. - RPCCert string `long:"rpccert" description:"File containing the certificate file"` - RPCKey string `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 -- NOTE: This is only allowed if the RPC server is bound to localhost"` - LegacyRPCListeners []string `long:"rpclisten" description:"Listen for legacy RPC connections on this interface/port (default port: 8332, testnet: 18332, simnet: 18554)"` - LegacyRPCMaxClients int64 `long:"rpcmaxclients" description:"Max number of legacy RPC clients for standard connections"` - LegacyRPCMaxWebsockets int64 `long:"rpcmaxwebsockets" description:"Max number of legacy RPC websocket connections"` - Username string `short:"u" long:"username" description:"Username for legacy RPC and btcd authentication (if btcdusername is unset)"` - Password string `short:"P" long:"password" default-mask:"-" description:"Password for legacy RPC and btcd authentication (if btcdpassword is unset)"` + 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 -- NOTE: This is only allowed if the RPC server is bound to localhost"` + LegacyRPCListeners []string `long:"rpclisten" description:"Listen for legacy RPC connections on this interface/port (default port: 8332, testnet: 18332, simnet: 18554)"` + LegacyRPCMaxClients int64 `long:"rpcmaxclients" description:"Max number of legacy RPC clients for standard connections"` + LegacyRPCMaxWebsockets int64 `long:"rpcmaxwebsockets" description:"Max number of legacy RPC websocket connections"` + Username string `short:"u" long:"username" description:"Username for legacy RPC and btcd authentication (if btcdusername is unset)"` + Password string `short:"P" long:"password" default-mask:"-" description:"Password for legacy RPC and btcd authentication (if btcdpassword is unset)"` // EXPERIMENTAL RPC server options // @@ -95,7 +95,7 @@ type config struct { ExperimentalRPCListeners []string `long:"experimentalrpclisten" description:"Listen for RPC connections on this interface/port"` // Deprecated options - DataDir string `short:"b" long:"datadir" default-mask:"-" description:"DEPRECATED -- use appdata instead"` + DataDir *cfgutil.ExplicitString `short:"b" long:"datadir" default-mask:"-" description:"DEPRECATED -- use appdata instead"` } // cleanAndExpandPath expands environement variables and leading ~ in the @@ -247,15 +247,16 @@ func loadConfig() (*config, []string, error) { // Default config. cfg := config{ DebugLevel: defaultLogLevel, - ConfigFile: defaultConfigFile, - AppDataDir: defaultAppDataDir, + ConfigFile: cfgutil.NewExplicitString(defaultConfigFile), + AppDataDir: cfgutil.NewExplicitString(defaultAppDataDir), LogDir: defaultLogDir, WalletPass: wallet.InsecurePubPassphrase, - RPCKey: defaultRPCKeyFile, - RPCCert: defaultRPCCertFile, + CAFile: cfgutil.NewExplicitString(""), + RPCKey: cfgutil.NewExplicitString(defaultRPCKeyFile), + RPCCert: cfgutil.NewExplicitString(defaultRPCCertFile), LegacyRPCMaxClients: defaultRPCMaxClients, LegacyRPCMaxWebsockets: defaultRPCMaxWebsockets, - DataDir: defaultAppDataDir, + DataDir: cfgutil.NewExplicitString(defaultAppDataDir), } // Pre-parse the command line options to see if an alternative config @@ -283,17 +284,17 @@ func loadConfig() (*config, []string, error) { // Load additional config from file. var configFileError error parser := flags.NewParser(&cfg, flags.Default) - configFilePath := preCfg.ConfigFile - if configFilePath == defaultConfigFile { - appDataDir := preCfg.AppDataDir - if appDataDir == defaultAppDataDir && preCfg.DataDir != defaultAppDataDir { - appDataDir = cleanAndExpandPath(preCfg.DataDir) + 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) } - } else { - configFilePath = cleanAndExpandPath(configFilePath) } err = flags.NewIniParser(parser).ParseFile(configFilePath) if err != nil { @@ -323,24 +324,24 @@ func loadConfig() (*config, []string, error) { // Check deprecated aliases. The new options receive priority when both // are changed from the default. - if cfg.DataDir != defaultAppDataDir { + if cfg.DataDir.ExplicitlySet() { fmt.Fprintln(os.Stderr, "datadir option has been replaced by "+ "appdata -- please update your config") - if cfg.AppDataDir == defaultAppDataDir { - cfg.AppDataDir = cfg.DataDir + 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 != defaultAppDataDir { - cfg.AppDataDir = cleanAndExpandPath(cfg.AppDataDir) - if cfg.RPCKey == defaultRPCKeyFile { - cfg.RPCKey = filepath.Join(cfg.AppDataDir, "rpc.key") + 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 == defaultRPCCertFile { - cfg.RPCCert = filepath.Join(cfg.AppDataDir, "rpc.cert") + if !cfg.RPCCert.ExplicitlySet() { + cfg.RPCCert.Value = filepath.Join(cfg.AppDataDir.Value, "rpc.cert") } } @@ -389,7 +390,7 @@ func loadConfig() (*config, []string, error) { // Exit if you try to use a simulation wallet with a standard // data directory. - if cfg.AppDataDir == defaultAppDataDir && cfg.CreateTemp { + if (!cfg.AppDataDir.ExplicitlySet() || !cfg.DataDir.ExplicitlySet()) && cfg.CreateTemp { fmt.Fprintln(os.Stderr, "Tried to create a temporary simulation "+ "wallet, but failed to specify data directory!") os.Exit(0) @@ -404,7 +405,7 @@ func loadConfig() (*config, []string, error) { } // Ensure the wallet exists or create it when the create flag is set. - netDir := networkDir(cfg.AppDataDir, activeNet.Params) + netDir := networkDir(cfg.AppDataDir.Value, activeNet.Params) dbPath := filepath.Join(netDir, walletDbName) if cfg.CreateTemp && cfg.Create { @@ -519,12 +520,12 @@ func loadConfig() (*config, []string, error) { } } else { // If CAFile is unset, choose either the copy or local btcd cert. - if cfg.CAFile == "" { - cfg.CAFile = filepath.Join(cfg.AppDataDir, defaultCAFilename) + 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 btcd and switch to its RPC cert if it exists. - certExists, err := cfgutil.FileExists(cfg.CAFile) + certExists, err := cfgutil.FileExists(cfg.CAFile.Value) if err != nil { fmt.Fprintln(os.Stderr, err) return nil, nil, err @@ -538,7 +539,7 @@ func loadConfig() (*config, []string, error) { return nil, nil, err } if btcdCertExists { - cfg.CAFile = btcdDefaultCAFile + cfg.CAFile.Value = btcdDefaultCAFile } } } @@ -625,9 +626,9 @@ func loadConfig() (*config, []string, error) { } // Expand environment variable and leading ~ for filepaths. - cfg.CAFile = cleanAndExpandPath(cfg.CAFile) - cfg.RPCCert = cleanAndExpandPath(cfg.RPCCert) - cfg.RPCKey = cleanAndExpandPath(cfg.RPCKey) + cfg.CAFile.Value = cleanAndExpandPath(cfg.CAFile.Value) + cfg.RPCCert.Value = cleanAndExpandPath(cfg.RPCCert.Value) + cfg.RPCKey.Value = cleanAndExpandPath(cfg.RPCKey.Value) // If the btcd username or password are unset, use the same auth as for // the client. The two settings were previously shared for btcd and diff --git a/internal/cfgutil/explicitflags.go b/internal/cfgutil/explicitflags.go new file mode 100644 index 0000000..331e609 --- /dev/null +++ b/internal/cfgutil/explicitflags.go @@ -0,0 +1,36 @@ +// Copyright (c) 2016 The btcsuite developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package cfgutil + +// ExplicitString is a string value implementing the flags.Marshaler and +// flags.Unmarshaler interfaces so it may be used as a config struct field. It +// records whether the value was explicitly set by the flags package. This is +// useful when behavior must be modified depending on whether a flag was set by +// the user or left as a default. Without recording this, it would be +// impossible to determine whether flag with a default value was unmodified or +// explicitly set to the default. +type ExplicitString struct { + Value string + explicitlySet bool +} + +// NewExplicitString creates a string flag with the provided default value. +func NewExplicitString(defaultValue string) *ExplicitString { + return &ExplicitString{Value: defaultValue, explicitlySet: false} +} + +// ExplicitlySet returns whether the flag was explicitly set through the +// flags.Unmarshaler interface. +func (e *ExplicitString) ExplicitlySet() bool { return e.explicitlySet } + +// MarshalFlag implements the flags.Marshaler interface. +func (e *ExplicitString) MarshalFlag() (string, error) { return e.Value, nil } + +// UnmarshalFlag implements the flags.Unmarshaler interface. +func (e *ExplicitString) UnmarshalFlag(value string) error { + e.Value = value + e.explicitlySet = true + return nil +} diff --git a/rpcserver.go b/rpcserver.go index c51c339..5169b39 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -35,19 +35,19 @@ func openRPCKeyPair() (tls.Certificate, error) { // acceptable if the previous execution used a one time TLS key. // Otherwise, both the cert and key should be read from disk. If the // cert is missing, the read error will occur in LoadX509KeyPair. - _, e := os.Stat(cfg.RPCKey) + _, e := os.Stat(cfg.RPCKey.Value) keyExists := !os.IsNotExist(e) switch { case cfg.OneTimeTLSKey && keyExists: err := fmt.Errorf("one time TLS keys are enabled, but TLS key "+ - "`%s` already exists", cfg.RPCKey) + "`%s` already exists", cfg.RPCKey.Value) return tls.Certificate{}, err case cfg.OneTimeTLSKey: return generateRPCKeyPair(false) case !keyExists: return generateRPCKeyPair(true) default: - return tls.LoadX509KeyPair(cfg.RPCCert, cfg.RPCKey) + return tls.LoadX509KeyPair(cfg.RPCCert.Value, cfg.RPCKey.Value) } } @@ -58,8 +58,8 @@ func generateRPCKeyPair(writeKey bool) (tls.Certificate, error) { log.Infof("Generating TLS certificates...") // Create directories for cert and key files if they do not yet exist. - certDir, _ := filepath.Split(cfg.RPCCert) - keyDir, _ := filepath.Split(cfg.RPCKey) + certDir, _ := filepath.Split(cfg.RPCCert.Value) + keyDir, _ := filepath.Split(cfg.RPCKey.Value) err := os.MkdirAll(certDir, 0700) if err != nil { return tls.Certificate{}, err @@ -82,14 +82,14 @@ func generateRPCKeyPair(writeKey bool) (tls.Certificate, error) { } // Write cert and (potentially) the key files. - err = ioutil.WriteFile(cfg.RPCCert, cert, 0600) + err = ioutil.WriteFile(cfg.RPCCert.Value, cert, 0600) if err != nil { return tls.Certificate{}, err } if writeKey { - err = ioutil.WriteFile(cfg.RPCKey, key, 0600) + err = ioutil.WriteFile(cfg.RPCKey.Value, key, 0600) if err != nil { - rmErr := os.Remove(cfg.RPCCert) + rmErr := os.Remove(cfg.RPCCert.Value) if rmErr != nil { log.Warnf("Cannot remove written certificates: %v", rmErr) diff --git a/walletsetup.go b/walletsetup.go index 2f8d455..5f16c92 100644 --- a/walletsetup.go +++ b/walletsetup.go @@ -99,13 +99,13 @@ func convertLegacyKeystore(legacyKeyStore *keystore.Store, manager *waddrmgr.Man // and generates the wallet accordingly. The new wallet will reside at the // provided path. func createWallet(cfg *config) error { - dbDir := networkDir(cfg.AppDataDir, activeNet.Params) + dbDir := networkDir(cfg.AppDataDir.Value, activeNet.Params) loader := wallet.NewLoader(activeNet.Params, dbDir) // When there is a legacy keystore, open it now to ensure any errors // don't end up exiting the process after the user has spent time // entering a bunch of information. - netDir := networkDir(cfg.AppDataDir, activeNet.Params) + netDir := networkDir(cfg.AppDataDir.Value, activeNet.Params) keystorePath := filepath.Join(netDir, keystore.Filename) var legacyKeyStore *keystore.Store _, err := os.Stat(keystorePath) @@ -207,7 +207,7 @@ func createSimulationWallet(cfg *config) error { // Public passphrase is the default. pubPass := []byte(wallet.InsecurePubPassphrase) - netDir := networkDir(cfg.AppDataDir, activeNet.Params) + netDir := networkDir(cfg.AppDataDir.Value, activeNet.Params) // Create the wallet. dbPath := filepath.Join(netDir, walletDbName)