// Copyright (c) 2013 Conformal Systems LLC. // Use of this source code is governed by an ISC // license that can be found in the LICENSE file. package main import ( "fmt" "github.com/conformal/btcdb" _ "github.com/conformal/btcdb/ldb" "github.com/conformal/btcutil" "github.com/conformal/btcwire" "github.com/conformal/go-flags" "net" "os" "path/filepath" "runtime" "sort" "strconv" "strings" "time" ) const ( defaultConfigFilename = "btcd.conf" defaultDataDirname = "data" defaultLogLevel = "info" defaultBtcnet = btcwire.MainNet defaultMaxPeers = 125 defaultBanDuration = time.Hour * 24 defaultVerifyEnabled = false defaultDbType = "leveldb" ) var ( btcdHomeDir = btcutil.AppDataDir("btcd", false) defaultConfigFile = filepath.Join(btcdHomeDir, defaultConfigFilename) defaultDataDir = filepath.Join(btcdHomeDir, defaultDataDirname) defaultListener = net.JoinHostPort("", netParams(defaultBtcnet).listenPort) knownDbTypes = btcdb.SupportedDBs() defaultRPCKeyFile = filepath.Join(btcdHomeDir, "rpc.key") defaultRPCCertFile = filepath.Join(btcdHomeDir, "rpc.cert") defaultLogFile = filepath.Join(btcdHomeDir, "logs", "btcd.log") ) // config defines the configuration options for btcd. // // See loadConfig for details on the configuration load process. type config struct { ShowVersion bool `short:"V" long:"version" description:"Display version information and exit"` ConfigFile string `short:"C" long:"configfile" description:"Path to configuration file"` DataDir string `short:"b" long:"datadir" description:"Directory to store data"` AddPeers []string `short:"a" long:"addpeer" description:"Add a peer to connect with at startup"` ConnectPeers []string `long:"connect" description:"Connect only to the specified peers at startup"` DisableListen bool `long:"nolisten" description:"Disable listening for incoming connections -- NOTE: Listening is automatically disabled if the --connect or --proxy options are used without also specifying listen interfaces via --listen"` Listeners []string `long:"listen" description:"Add an interface/port to listen for connections (default all interfaces port: 8333, testnet: 18333)"` MaxPeers int `long:"maxpeers" description:"Max number of inbound and outbound peers"` BanDuration time.Duration `long:"banduration" description:"How long to ban misbehaving peers. Valid time units are {s, m, h}. Minimum 1 second"` RPCUser string `short:"u" long:"rpcuser" description:"Username for RPC connections"` RPCPass string `short:"P" long:"rpcpass" default-mask:"-" description:"Password for RPC connections"` RPCListeners []string `long:"rpclisten" description:"Add an interface/port to listen for RPC connections (default port: 8334, testnet: 18334)"` RPCCert string `long:"rpccert" description:"File containing the certificate file"` RPCKey string `long:"rpckey" description:"File containing the certificate key"` DisableRPC bool `long:"norpc" description:"Disable built-in RPC server -- NOTE: The RPC server is disabled by default if no rpcuser/rpcpass is specified"` DisableDNSSeed bool `long:"nodnsseed" description:"Disable DNS seeding for peers"` 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"` UseTor bool `long:"tor" description:"Specifies the proxy server used is a Tor node"` TestNet3 bool `long:"testnet" description:"Use the test network"` RegressionTest bool `long:"regtest" description:"Use the regression test network"` DisableCheckpoints bool `long:"nocheckpoints" description:"Disable built-in checkpoints. Don't do this unless you know what you're doing."` DbType string `long:"dbtype" description:"Database backend to use for the Block Chain"` Profile string `long:"profile" description:"Enable HTTP profiling on given port -- NOTE port must be between 1024 and 65536"` CpuProfile string `long:"cpuprofile" description:"Write CPU profile to the specified file"` DebugLevel string `short:"d" long:"debuglevel" description:"Logging level for all subsystems {trace, debug, info, warn, error, critical} -- You may also specify =,=,... to set the log level for individual subsystems -- Use show to list available subsystems"` } // serviceOptions defines the configuration options for btcd as a service on // Windows. type serviceOptions struct { ServiceCommand string `short:"s" long:"service" description:"Service command {install, remove, start, stop}"` } // 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(btcdHomeDir) 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 } // validDbType returns whether or not dbType is a supported database type. func validDbType(dbType string) bool { for _, knownType := range knownDbTypes { if dbType == knownType { return true } } return false } // removeDuplicateAddresses returns a new slice with all duplicate entries in // addrs removed. func removeDuplicateAddresses(addrs []string) []string { result := make([]string, 0) seen := map[string]bool{} for _, val := range addrs { if _, ok := seen[val]; !ok { result = append(result, val) seen[val] = true } } return result } // 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 } // 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 } // newConfigParser returns a new command line flags parser. func newConfigParser(cfg *config, so *serviceOptions, options flags.Options) *flags.Parser { parser := flags.NewParser(cfg, options) if runtime.GOOS == "windows" { parser.AddGroup("Service Options", "Service Options", so) } return parser } // 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 btcd 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, MaxPeers: defaultMaxPeers, BanDuration: defaultBanDuration, ConfigFile: defaultConfigFile, DataDir: defaultDataDir, DbType: defaultDbType, RPCKey: defaultRPCKeyFile, RPCCert: defaultRPCCertFile, } // Service options which are only added on Windows. serviceOpts := serviceOptions{} var runServiceCommand func(string) error if runtime.GOOS == "windows" { runServiceCommand = performServiceCommand } // Create the home directory if it doesn't already exist. err := os.MkdirAll(btcdHomeDir, 0700) if err != nil { btcdLog.Errorf("%v", err) return nil, nil, err } // Pre-parse the command line options to see if an alternative config // file or the version flag was specified. Any errors can be ignored // here since they will be caught be the final parse below. preCfg := cfg preParser := newConfigParser(&preCfg, &serviceOpts, flags.None) preParser.Parse() // 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) } // Perform service command and exit if specified. Invalid service // commands show an appropriate error. Only runs on Windows since // the runServiceCommand function will be nil when not on Windows. if serviceOpts.ServiceCommand != "" && runServiceCommand != nil { err := runServiceCommand(serviceOpts.ServiceCommand) if err != nil { fmt.Fprintln(os.Stderr, err) } os.Exit(0) } // Load additional config from file. var configFileError error parser := newConfigParser(&cfg, &serviceOpts, flags.Default) if !preCfg.RegressionTest || preCfg.ConfigFile != defaultConfigFile { 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 } } // Don't add peers from the config file when in regression test mode. if preCfg.RegressionTest && len(cfg.AddPeers) > 0 { cfg.AddPeers = nil } // 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 } // The two test networks can't be selected simultaneously. if cfg.TestNet3 && cfg.RegressionTest { str := "%s: The testnet and regtest params can't be used " + "together -- choose one of the two" 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 testnet and regression // test net flags. if cfg.TestNet3 { activeNetParams = netParams(btcwire.TestNet3) } else if cfg.RegressionTest { activeNetParams = netParams(btcwire.TestNet) } // Special show command to list supported subsystems and exit. if cfg.DebugLevel == "show" { fmt.Println("Supported subsystems", supportedSubsystems()) os.Exit(0) } // 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 } // Validate database type. if !validDbType(cfg.DbType) { str := "%s: The specified database type [%v] is invalid -- " + "supported types %v" err := fmt.Errorf(str, "loadConfig", cfg.DbType, knownDbTypes) fmt.Fprintln(os.Stderr, err) parser.WriteHelp(os.Stderr) return nil, nil, err } // Validate profile port number if cfg.Profile != "" { profilePort, err := strconv.Atoi(cfg.Profile) if err != nil || profilePort < 1024 || profilePort > 65535 { str := "%s: The profile port must be between 1024 and 65535" err := fmt.Errorf(str, "loadConfig") fmt.Fprintln(os.Stderr, err) parser.WriteHelp(os.Stderr) return nil, nil, err } } // Append the network type to the data directory so it is "namespaced" // per network. In addition to the block database, there are other // pieces of data that are saved to disk such as address manager state. // All data is specific to a network, so namespacing the data directory // means each individual piece of serialized data does not have to // worry about changing names per network and such. cfg.DataDir = cleanAndExpandPath(cfg.DataDir) cfg.DataDir = filepath.Join(cfg.DataDir, activeNetParams.netName) // Don't allow ban durations that are too short. if cfg.BanDuration < time.Duration(time.Second) { str := "%s: The banduration option may not be less than 1s -- parsed [%v]" err := fmt.Errorf(str, "loadConfig", cfg.BanDuration) fmt.Fprintln(os.Stderr, err) parser.WriteHelp(os.Stderr) return nil, nil, err } // --addPeer and --connect do not mix. if len(cfg.AddPeers) > 0 && len(cfg.ConnectPeers) > 0 { str := "%s: the --addpeer and --connect options can not be " + "mixed" err := fmt.Errorf(str, "loadConfig") fmt.Fprintln(os.Stderr, err) parser.WriteHelp(os.Stderr) return nil, nil, err } // --tor requires --proxy to be set. if cfg.UseTor && cfg.Proxy == "" { str := "%s: the --tor option requires --proxy to be set" err := fmt.Errorf(str, "loadConfig") fmt.Fprintln(os.Stderr, err) parser.WriteHelp(os.Stderr) return nil, nil, err } // --proxy or --connect without --listen disables listening. if (cfg.Proxy != "" || len(cfg.ConnectPeers) > 0) && len(cfg.Listeners) == 0 { cfg.DisableListen = true } // Connect means no DNS seeding. if len(cfg.ConnectPeers) > 0 { cfg.DisableDNSSeed = true } // Add the default listener if none were specified. The default // listener is all addresses on the listen port for the network // we are to connect to. if len(cfg.Listeners) == 0 { cfg.Listeners = []string{ net.JoinHostPort("", activeNetParams.listenPort), } } // The RPC server is disabled if no username or password is provided. if cfg.RPCUser == "" || cfg.RPCPass == "" { cfg.DisableRPC = true } // Default RPC to listen on localhost only. if !cfg.DisableRPC && len(cfg.RPCListeners) == 0 { addrs, err := net.LookupHost("localhost") if err != nil { return nil, nil, err } cfg.RPCListeners = make([]string, 0, len(addrs)) for _, addr := range addrs { addr = net.JoinHostPort(addr, activeNetParams.rpcPort) cfg.RPCListeners = append(cfg.RPCListeners, addr) } } // Add default port to all listener addresses if needed and remove // duplicate addresses. cfg.Listeners = normalizeAddresses(cfg.Listeners, activeNetParams.listenPort) // Add default port to all rpc listener addresses if needed and remove // duplicate addresses. cfg.RPCListeners = normalizeAddresses(cfg.RPCListeners, activeNetParams.rpcPort) // Add default port to all added peer addresses if needed and remove // duplicate addresses. cfg.AddPeers = normalizeAddresses(cfg.AddPeers, activeNetParams.peerPort) cfg.ConnectPeers = normalizeAddresses(cfg.ConnectPeers, activeNetParams.peerPort) // 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 { btcdLog.Warnf("%v", configFileError) } return &cfg, remainingArgs, nil }