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.
This commit is contained in:
Josh Rickmar 2016-05-05 10:29:29 -04:00
parent 39fd9bd0f3
commit f3df6c8bc9
5 changed files with 109 additions and 72 deletions

View file

@ -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) loader := wallet.NewLoader(activeNet.Params, dbDir)
// Create and start HTTP server to serve wallet client connections. // Create and start HTTP server to serve wallet client connections.
@ -197,7 +197,7 @@ func readCAFile() []byte {
var certs []byte var certs []byte
if !cfg.DisableClientTLS { if !cfg.DisableClientTLS {
var err error var err error
certs, err = ioutil.ReadFile(cfg.CAFile) certs, err = ioutil.ReadFile(cfg.CAFile.Value)
if err != nil { if err != nil {
log.Warnf("Cannot open CA file: %v", err) log.Warnf("Cannot open CA file: %v", err)
// If there's an error reading the CA file, continue // If there's an error reading the CA file, continue

119
config.go
View file

@ -45,30 +45,30 @@ var (
type config struct { type config struct {
// General application behavior // General application behavior
ConfigFile string `short:"C" long:"configfile" description:"Path to configuration file"` ConfigFile *cfgutil.ExplicitString `short:"C" long:"configfile" description:"Path to configuration file"`
ShowVersion bool `short:"V" long:"version" description:"Display version information and exit"` 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"` 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"` 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"` 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)"` 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)"` 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"` 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}"` DebugLevel string `short:"d" long:"debuglevel" description:"Logging level {trace, debug, info, warn, error, critical}"`
LogDir string `long:"logdir" description:"Directory to log output."` 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"` Profile string `long:"profile" description:"Enable HTTP profiling on given port -- NOTE port must be between 1024 and 65536"`
// Wallet options // Wallet options
WalletPass string `long:"walletpass" default-mask:"-" description:"The public wallet password -- Only required if the wallet was created with one"` WalletPass string `long:"walletpass" default-mask:"-" description:"The public wallet password -- Only required if the wallet was created with one"`
// RPC client options // 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)"` 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"` 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"` 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"` BtcdUsername string `long:"btcdusername" description:"Username for btcd authentication"`
BtcdPassword string `long:"btcdpassword" default-mask:"-" description:"Password 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)"` Proxy string `long:"proxy" description:"Connect via SOCKS5 proxy (eg. 127.0.0.1:9050)"`
ProxyUser string `long:"proxyuser" description:"Username for proxy server"` ProxyUser string `long:"proxyuser" description:"Username for proxy server"`
ProxyPass string `long:"proxypass" default-mask:"-" description:"Password for proxy server"` ProxyPass string `long:"proxypass" default-mask:"-" description:"Password for proxy server"`
// RPC server options // RPC server options
// //
@ -78,15 +78,15 @@ type config struct {
// //
// Usernames can also be used for the consensus RPC client, so they // Usernames can also be used for the consensus RPC client, so they
// aren't considered legacy. // aren't considered legacy.
RPCCert string `long:"rpccert" description:"File containing the certificate file"` RPCCert *cfgutil.ExplicitString `long:"rpccert" description:"File containing the certificate file"`
RPCKey string `long:"rpckey" description:"File containing the certificate key"` 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"` 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"` 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)"` 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"` 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"` 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)"` 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)"` Password string `short:"P" long:"password" default-mask:"-" description:"Password for legacy RPC and btcd authentication (if btcdpassword is unset)"`
// EXPERIMENTAL RPC server options // EXPERIMENTAL RPC server options
// //
@ -95,7 +95,7 @@ type config struct {
ExperimentalRPCListeners []string `long:"experimentalrpclisten" description:"Listen for RPC connections on this interface/port"` ExperimentalRPCListeners []string `long:"experimentalrpclisten" description:"Listen for RPC connections on this interface/port"`
// Deprecated options // 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 // cleanAndExpandPath expands environement variables and leading ~ in the
@ -247,15 +247,16 @@ func loadConfig() (*config, []string, error) {
// Default config. // Default config.
cfg := config{ cfg := config{
DebugLevel: defaultLogLevel, DebugLevel: defaultLogLevel,
ConfigFile: defaultConfigFile, ConfigFile: cfgutil.NewExplicitString(defaultConfigFile),
AppDataDir: defaultAppDataDir, AppDataDir: cfgutil.NewExplicitString(defaultAppDataDir),
LogDir: defaultLogDir, LogDir: defaultLogDir,
WalletPass: wallet.InsecurePubPassphrase, WalletPass: wallet.InsecurePubPassphrase,
RPCKey: defaultRPCKeyFile, CAFile: cfgutil.NewExplicitString(""),
RPCCert: defaultRPCCertFile, RPCKey: cfgutil.NewExplicitString(defaultRPCKeyFile),
RPCCert: cfgutil.NewExplicitString(defaultRPCCertFile),
LegacyRPCMaxClients: defaultRPCMaxClients, LegacyRPCMaxClients: defaultRPCMaxClients,
LegacyRPCMaxWebsockets: defaultRPCMaxWebsockets, LegacyRPCMaxWebsockets: defaultRPCMaxWebsockets,
DataDir: defaultAppDataDir, DataDir: cfgutil.NewExplicitString(defaultAppDataDir),
} }
// Pre-parse the command line options to see if an alternative config // 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. // Load additional config from file.
var configFileError error var configFileError error
parser := flags.NewParser(&cfg, flags.Default) parser := flags.NewParser(&cfg, flags.Default)
configFilePath := preCfg.ConfigFile configFilePath := preCfg.ConfigFile.Value
if configFilePath == defaultConfigFile { if preCfg.ConfigFile.ExplicitlySet() {
appDataDir := preCfg.AppDataDir configFilePath = cleanAndExpandPath(configFilePath)
if appDataDir == defaultAppDataDir && preCfg.DataDir != defaultAppDataDir { } else {
appDataDir = cleanAndExpandPath(preCfg.DataDir) appDataDir := preCfg.AppDataDir.Value
if !preCfg.AppDataDir.ExplicitlySet() && preCfg.DataDir.ExplicitlySet() {
appDataDir = cleanAndExpandPath(preCfg.DataDir.Value)
} }
if appDataDir != defaultAppDataDir { if appDataDir != defaultAppDataDir {
configFilePath = filepath.Join(appDataDir, defaultConfigFilename) configFilePath = filepath.Join(appDataDir, defaultConfigFilename)
} }
} else {
configFilePath = cleanAndExpandPath(configFilePath)
} }
err = flags.NewIniParser(parser).ParseFile(configFilePath) err = flags.NewIniParser(parser).ParseFile(configFilePath)
if err != nil { if err != nil {
@ -323,24 +324,24 @@ func loadConfig() (*config, []string, error) {
// Check deprecated aliases. The new options receive priority when both // Check deprecated aliases. The new options receive priority when both
// are changed from the default. // are changed from the default.
if cfg.DataDir != defaultAppDataDir { if cfg.DataDir.ExplicitlySet() {
fmt.Fprintln(os.Stderr, "datadir option has been replaced by "+ fmt.Fprintln(os.Stderr, "datadir option has been replaced by "+
"appdata -- please update your config") "appdata -- please update your config")
if cfg.AppDataDir == defaultAppDataDir { if !cfg.AppDataDir.ExplicitlySet() {
cfg.AppDataDir = cfg.DataDir cfg.AppDataDir.Value = cfg.DataDir.Value
} }
} }
// If an alternate data directory was specified, and paths with defaults // 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 data dir are unchanged, modify each path to be
// relative to the new data dir. // relative to the new data dir.
if cfg.AppDataDir != defaultAppDataDir { if cfg.AppDataDir.ExplicitlySet() {
cfg.AppDataDir = cleanAndExpandPath(cfg.AppDataDir) cfg.AppDataDir.Value = cleanAndExpandPath(cfg.AppDataDir.Value)
if cfg.RPCKey == defaultRPCKeyFile { if !cfg.RPCKey.ExplicitlySet() {
cfg.RPCKey = filepath.Join(cfg.AppDataDir, "rpc.key") cfg.RPCKey.Value = filepath.Join(cfg.AppDataDir.Value, "rpc.key")
} }
if cfg.RPCCert == defaultRPCCertFile { if !cfg.RPCCert.ExplicitlySet() {
cfg.RPCCert = filepath.Join(cfg.AppDataDir, "rpc.cert") 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 // Exit if you try to use a simulation wallet with a standard
// data directory. // 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 "+ fmt.Fprintln(os.Stderr, "Tried to create a temporary simulation "+
"wallet, but failed to specify data directory!") "wallet, but failed to specify data directory!")
os.Exit(0) 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. // 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) dbPath := filepath.Join(netDir, walletDbName)
if cfg.CreateTemp && cfg.Create { if cfg.CreateTemp && cfg.Create {
@ -519,12 +520,12 @@ func loadConfig() (*config, []string, error) {
} }
} else { } else {
// If CAFile is unset, choose either the copy or local btcd cert. // If CAFile is unset, choose either the copy or local btcd cert.
if cfg.CAFile == "" { if !cfg.CAFile.ExplicitlySet() {
cfg.CAFile = filepath.Join(cfg.AppDataDir, defaultCAFilename) cfg.CAFile.Value = filepath.Join(cfg.AppDataDir.Value, defaultCAFilename)
// If the CA copy does not exist, check if we're connecting to // 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. // 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 { if err != nil {
fmt.Fprintln(os.Stderr, err) fmt.Fprintln(os.Stderr, err)
return nil, nil, err return nil, nil, err
@ -538,7 +539,7 @@ func loadConfig() (*config, []string, error) {
return nil, nil, err return nil, nil, err
} }
if btcdCertExists { 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. // Expand environment variable and leading ~ for filepaths.
cfg.CAFile = cleanAndExpandPath(cfg.CAFile) cfg.CAFile.Value = cleanAndExpandPath(cfg.CAFile.Value)
cfg.RPCCert = cleanAndExpandPath(cfg.RPCCert) cfg.RPCCert.Value = cleanAndExpandPath(cfg.RPCCert.Value)
cfg.RPCKey = cleanAndExpandPath(cfg.RPCKey) cfg.RPCKey.Value = cleanAndExpandPath(cfg.RPCKey.Value)
// If the btcd username or password are unset, use the same auth as for // 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 // the client. The two settings were previously shared for btcd and

View file

@ -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
}

View file

@ -35,19 +35,19 @@ func openRPCKeyPair() (tls.Certificate, error) {
// acceptable if the previous execution used a one time TLS key. // acceptable if the previous execution used a one time TLS key.
// Otherwise, both the cert and key should be read from disk. If the // Otherwise, both the cert and key should be read from disk. If the
// cert is missing, the read error will occur in LoadX509KeyPair. // 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) keyExists := !os.IsNotExist(e)
switch { switch {
case cfg.OneTimeTLSKey && keyExists: case cfg.OneTimeTLSKey && keyExists:
err := fmt.Errorf("one time TLS keys are enabled, but TLS key "+ 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 return tls.Certificate{}, err
case cfg.OneTimeTLSKey: case cfg.OneTimeTLSKey:
return generateRPCKeyPair(false) return generateRPCKeyPair(false)
case !keyExists: case !keyExists:
return generateRPCKeyPair(true) return generateRPCKeyPair(true)
default: 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...") log.Infof("Generating TLS certificates...")
// Create directories for cert and key files if they do not yet exist. // Create directories for cert and key files if they do not yet exist.
certDir, _ := filepath.Split(cfg.RPCCert) certDir, _ := filepath.Split(cfg.RPCCert.Value)
keyDir, _ := filepath.Split(cfg.RPCKey) keyDir, _ := filepath.Split(cfg.RPCKey.Value)
err := os.MkdirAll(certDir, 0700) err := os.MkdirAll(certDir, 0700)
if err != nil { if err != nil {
return tls.Certificate{}, err return tls.Certificate{}, err
@ -82,14 +82,14 @@ func generateRPCKeyPair(writeKey bool) (tls.Certificate, error) {
} }
// Write cert and (potentially) the key files. // 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 { if err != nil {
return tls.Certificate{}, err return tls.Certificate{}, err
} }
if writeKey { if writeKey {
err = ioutil.WriteFile(cfg.RPCKey, key, 0600) err = ioutil.WriteFile(cfg.RPCKey.Value, key, 0600)
if err != nil { if err != nil {
rmErr := os.Remove(cfg.RPCCert) rmErr := os.Remove(cfg.RPCCert.Value)
if rmErr != nil { if rmErr != nil {
log.Warnf("Cannot remove written certificates: %v", log.Warnf("Cannot remove written certificates: %v",
rmErr) rmErr)

View file

@ -99,13 +99,13 @@ func convertLegacyKeystore(legacyKeyStore *keystore.Store, manager *waddrmgr.Man
// and generates the wallet accordingly. The new wallet will reside at the // and generates the wallet accordingly. The new wallet will reside at the
// provided path. // provided path.
func createWallet(cfg *config) error { func createWallet(cfg *config) error {
dbDir := networkDir(cfg.AppDataDir, activeNet.Params) dbDir := networkDir(cfg.AppDataDir.Value, activeNet.Params)
loader := wallet.NewLoader(activeNet.Params, dbDir) loader := wallet.NewLoader(activeNet.Params, dbDir)
// When there is a legacy keystore, open it now to ensure any errors // 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 // don't end up exiting the process after the user has spent time
// entering a bunch of information. // entering a bunch of information.
netDir := networkDir(cfg.AppDataDir, activeNet.Params) netDir := networkDir(cfg.AppDataDir.Value, activeNet.Params)
keystorePath := filepath.Join(netDir, keystore.Filename) keystorePath := filepath.Join(netDir, keystore.Filename)
var legacyKeyStore *keystore.Store var legacyKeyStore *keystore.Store
_, err := os.Stat(keystorePath) _, err := os.Stat(keystorePath)
@ -207,7 +207,7 @@ func createSimulationWallet(cfg *config) error {
// Public passphrase is the default. // Public passphrase is the default.
pubPass := []byte(wallet.InsecurePubPassphrase) pubPass := []byte(wallet.InsecurePubPassphrase)
netDir := networkDir(cfg.AppDataDir, activeNet.Params) netDir := networkDir(cfg.AppDataDir.Value, activeNet.Params)
// Create the wallet. // Create the wallet.
dbPath := filepath.Join(netDir, walletDbName) dbPath := filepath.Join(netDir, walletDbName)