/* * Copyright (c) 2013, 2014 Conformal Systems LLC * * Permission to use, copy, modify, and distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ package main import ( "fmt" "net" "os" "path/filepath" "sort" "strings" "github.com/conformal/btcutil" "github.com/conformal/btcwire" "github.com/conformal/go-flags" ) const ( defaultCAFilename = "btcd.cert" defaultConfigFilename = "btcwallet.conf" defaultBtcNet = btcwire.TestNet3 defaultLogLevel = "info" defaultLogDirname = "logs" defaultLogFilename = "btcwallet.log" defaultKeypoolSize = 100 defaultDisallowFree = false defaultRPCMaxClients = 10 defaultRPCMaxWebsockets = 25 ) var ( btcdHomeDir = btcutil.AppDataDir("btcd", false) btcwalletHomeDir = btcutil.AppDataDir("btcwallet", false) btcdHomedirCAFile = filepath.Join(btcdHomeDir, "rpc.cert") defaultConfigFile = filepath.Join(btcwalletHomeDir, defaultConfigFilename) defaultDataDir = btcwalletHomeDir defaultRPCKeyFile = filepath.Join(btcwalletHomeDir, "rpc.key") defaultRPCCertFile = filepath.Join(btcwalletHomeDir, "rpc.cert") defaultLogDir = filepath.Join(btcwalletHomeDir, defaultLogDirname) ) type config struct { ShowVersion bool `short:"V" long:"version" description:"Display version information and exit"` CAFile string `long:"cafile" description:"File containing root certificates to authenticate a TLS connections with btcd"` RPCConnect string `short:"c" long:"rpcconnect" description:"Hostname/IP and port of btcd RPC server to connect to (default localhost:18334, mainnet: localhost:8334, simnet: localhost:18556)"` DebugLevel string `short:"d" long:"debuglevel" description:"Logging level {trace, debug, info, warn, error, critical}"` ConfigFile string `short:"C" long:"configfile" description:"Path to configuration file"` SvrListeners []string `long:"rpclisten" description:"Listen for RPC/websocket connections on this interface/port (default port: 18332, mainnet: 8332, simnet: 18554)"` DataDir string `short:"D" long:"datadir" description:"Directory to store wallets and transactions"` LogDir string `long:"logdir" description:"Directory to log output."` Username string `short:"u" long:"username" description:"Username for client and btcd authorization"` Password string `short:"P" long:"password" default-mask:"-" description:"Password for client and btcd authorization"` BtcdUsername string `long:"btcdusername" description:"Alternative username for btcd authorization"` BtcdPassword string `long:"btcdpassword" default-mask:"-" description:"Alternative password for btcd authorization"` RPCCert string `long:"rpccert" description:"File containing the certificate file"` RPCKey string `long:"rpckey" description:"File containing the certificate key"` RPCMaxClients int64 `long:"rpcmaxclients" description:"Max number of RPC clients for standard connections"` RPCMaxWebsockets int64 `long:"rpcmaxwebsockets" description:"Max number of RPC websocket connections"` MainNet bool `long:"mainnet" description:"Use the main Bitcoin network (default testnet3)"` SimNet bool `long:"simnet" description:"Use the simulation test network (default testnet3)"` KeypoolSize uint `short:"k" long:"keypoolsize" description:"Maximum number of addresses in keypool"` DisallowFree bool `long:"disallowfree" description:"Force transactions to always include a fee"` 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"` Profile string `long:"profile" description:"Enable HTTP profiling on given port -- NOTE port must be between 1024 and 65536"` } // cleanAndExpandPath expands environement variables and leading ~ in the // passed path, cleans the result, and returns it. func cleanAndExpandPath(path string) string { // Expand initial ~ to OS specific home directory. if strings.HasPrefix(path, "~") { homeDir := filepath.Dir(btcwalletHomeDir) path = strings.Replace(path, "~", homeDir, 1) } // NOTE: The os.ExpandEnv doesn't work with Windows-style %VARIABLE%, // but they variables can still be expanded via POSIX-style $VARIABLE. return filepath.Clean(os.ExpandEnv(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 } // removeDuplicateAddresses returns a new slice with all duplicate entries in // addrs removed. func removeDuplicateAddresses(addrs []string) []string { result := []string{} seen := map[string]bool{} for _, val := range addrs { if _, ok := seen[val]; !ok { result = append(result, val) seen[val] = true } } return result } // normalizeAddresses returns a new slice with all the passed peer addresses // normalized with the given default port, and all duplicates removed. func normalizeAddresses(addrs []string, defaultPort string) []string { for i, addr := range addrs { addrs[i] = normalizeAddress(addr, defaultPort) } return removeDuplicateAddresses(addrs) } // filesExists reports whether the named file or directory exists. func fileExists(name string) bool { if _, err := os.Stat(name); err != nil { if os.IsNotExist(err) { return false } } return true } // normalizeAddress returns addr with the passed default port appended if // there is not already a port specified. func normalizeAddress(addr, defaultPort string) string { _, _, err := net.SplitHostPort(addr) if err != nil { return net.JoinHostPort(addr, defaultPort) } return addr } // 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 btcwallet 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: defaultConfigFile, DataDir: defaultDataDir, LogDir: defaultLogDir, RPCKey: defaultRPCKeyFile, RPCCert: defaultRPCCertFile, KeypoolSize: defaultKeypoolSize, DisallowFree: defaultDisallowFree, RPCMaxClients: defaultRPCMaxClients, RPCMaxWebsockets: defaultRPCMaxWebsockets, } // A config file in the current directory takes precedence. if fileExists(defaultConfigFilename) { cfg.ConfigFile = defaultConfigFile } // 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. if preCfg.ShowVersion { appName := filepath.Base(os.Args[0]) appName = strings.TrimSuffix(appName, filepath.Ext(appName)) fmt.Println(appName, "version", version()) os.Exit(0) } // Load additional config from file. var configFileError error parser := flags.NewParser(&cfg, flags.Default) err = flags.NewIniParser(parser).ParseFile(preCfg.ConfigFile) 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 } // 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) } // 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.DataDir != defaultDataDir { if cfg.RPCKey == defaultRPCKeyFile { cfg.RPCKey = filepath.Join(cfg.DataDir, "rpc.key") } if cfg.RPCCert == defaultRPCCertFile { cfg.RPCCert = filepath.Join(cfg.DataDir, "rpc.cert") } } // Multiple networks can't be selected simultaneously. numNets := 0 if cfg.MainNet { numNets++ } if cfg.SimNet { numNets++ } if numNets > 1 { str := "%s: The mainnet and simnet params can't be used " + "together -- choose one" err := fmt.Errorf(str, "loadConfig") fmt.Fprintln(os.Stderr, err) parser.WriteHelp(os.Stderr) return nil, nil, err } // Choose the active network params based on the selected network. switch { case cfg.MainNet: activeNet = &mainNetParams case cfg.SimNet: activeNet = &simNetParams } // Special show command to list supported subsystems and exit. if cfg.DebugLevel == "show" { fmt.Println("Supported subsystems", supportedSubsystems()) os.Exit(0) } // Initialize logging at the default logging level. initSeelogLogger(filepath.Join(cfg.LogDir, defaultLogFilename)) setLogLevels(defaultLogLevel) // 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.RPCConnect == "" { cfg.RPCConnect = activeNet.connect } // Add default port to connect flag if missing. cfg.RPCConnect = normalizeAddress(cfg.RPCConnect, activeNet.btcdPort) // If CAFile is unset, choose either the copy or local btcd cert. if cfg.CAFile == "" { cfg.CAFile = filepath.Join(cfg.DataDir, 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. if !fileExists(cfg.CAFile) { host, _, err := net.SplitHostPort(cfg.RPCConnect) if err != nil { return nil, nil, err } switch host { case "localhost": fallthrough case "127.0.0.1": fallthrough case "::1": if fileExists(btcdHomedirCAFile) { cfg.CAFile = btcdHomedirCAFile } } } } if len(cfg.SvrListeners) == 0 { addrs, err := net.LookupHost("localhost") if err != nil { return nil, nil, err } cfg.SvrListeners = make([]string, 0, len(addrs)) for _, addr := range addrs { addr = net.JoinHostPort(addr, activeNet.svrPort) cfg.SvrListeners = append(cfg.SvrListeners, addr) } } // Add default port to all rpc listener addresses if needed and remove // duplicate addresses. cfg.SvrListeners = normalizeAddresses(cfg.SvrListeners, activeNet.svrPort) // Expand environment variable and leading ~ for filepaths. cfg.CAFile = cleanAndExpandPath(cfg.CAFile) // 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 // client auth, so this avoids breaking backwards compatibility while // allowing users to use different auth settings for btcd and wallet. if cfg.BtcdUsername == "" { cfg.BtcdUsername = cfg.Username } if cfg.BtcdPassword == "" { cfg.BtcdPassword = cfg.Password } return &cfg, remainingArgs, nil }