decred.org/dcrwallet/v3@v3.1.0/config.go (about) 1 // Copyright (c) 2013-2016 The btcsuite developers 2 // Copyright (c) 2015-2020 The Decred developers 3 // Use of this source code is governed by an ISC 4 // license that can be found in the LICENSE file. 5 6 package main 7 8 import ( 9 "context" 10 "crypto/tls" 11 "crypto/x509" 12 "fmt" 13 "net" 14 "os" 15 "os/user" 16 "path/filepath" 17 "runtime" 18 "sort" 19 "strconv" 20 "strings" 21 22 "decred.org/dcrwallet/v3/errors" 23 "decred.org/dcrwallet/v3/internal/cfgutil" 24 "decred.org/dcrwallet/v3/internal/loggers" 25 "decred.org/dcrwallet/v3/internal/netparams" 26 "decred.org/dcrwallet/v3/version" 27 "decred.org/dcrwallet/v3/wallet" 28 "decred.org/dcrwallet/v3/wallet/txrules" 29 "github.com/decred/dcrd/connmgr/v3" 30 "github.com/decred/dcrd/dcrutil/v4" 31 "github.com/decred/dcrd/txscript/v4/stdaddr" 32 "github.com/decred/go-socks/socks" 33 "github.com/decred/slog" 34 flags "github.com/jessevdk/go-flags" 35 ) 36 37 const ( 38 defaultCAFilename = "dcrd.cert" 39 defaultConfigFilename = "dcrwallet.conf" 40 defaultLogLevel = "info" 41 defaultLogDirname = "logs" 42 defaultLogFilename = "dcrwallet.log" 43 defaultLogSize = "10M" 44 defaultRPCMaxClients = 10 45 defaultRPCMaxWebsockets = 25 46 defaultAuthType = "basic" 47 defaultEnableTicketBuyer = false 48 defaultEnableVoting = false 49 defaultPurchaseAccount = "default" 50 defaultPromptPass = false 51 defaultPass = "" 52 defaultPromptPublicPass = false 53 defaultGapLimit = wallet.DefaultGapLimit 54 defaultStakePoolColdExtKey = "" 55 defaultAllowHighFees = false 56 defaultAccountGapLimit = wallet.DefaultAccountGapLimit 57 defaultDisableCoinTypeUpgrades = false 58 defaultCircuitLimit = 32 59 defaultMixSplitLimit = 10 60 defaultVSPMaxFee = dcrutil.Amount(0.2e8) 61 62 // ticket buyer options 63 defaultBalanceToMaintainAbsolute = 0 64 defaultTicketbuyerLimit = 1 65 66 walletDbName = "wallet.db" 67 ) 68 69 var ( 70 dcrdDefaultCAFile = filepath.Join(dcrutil.AppDataDir("dcrd", false), "rpc.cert") 71 defaultAppDataDir = dcrutil.AppDataDir("dcrwallet", false) 72 defaultConfigFile = filepath.Join(defaultAppDataDir, defaultConfigFilename) 73 defaultRPCKeyFile = filepath.Join(defaultAppDataDir, "rpc.key") 74 defaultRPCCertFile = filepath.Join(defaultAppDataDir, "rpc.cert") 75 defaultRPCClientCAFile = filepath.Join(defaultAppDataDir, "clients.pem") 76 defaultLogDir = filepath.Join(defaultAppDataDir, defaultLogDirname) 77 ) 78 79 type config struct { 80 // General application behavior 81 ConfigFile *cfgutil.ExplicitString `short:"C" long:"configfile" description:"Path to configuration file"` 82 ShowVersion bool `short:"V" long:"version" description:"Display version information and exit"` 83 Create bool `long:"create" description:"Create new wallet"` 84 CreateTemp bool `long:"createtemp" description:"Create simulation wallet in nonstandard --appdata; private passphrase is 'password'"` 85 CreateWatchingOnly bool `long:"createwatchingonly" description:"Create watching wallet from account extended pubkey"` 86 AppDataDir *cfgutil.ExplicitString `short:"A" long:"appdata" description:"Application data directory for wallet config, databases and logs"` 87 TestNet bool `long:"testnet" description:"Use the test network"` 88 SimNet bool `long:"simnet" description:"Use the simulation test network"` 89 NoInitialLoad bool `long:"noinitialload" description:"Defer wallet creation/opening on startup and enable loading wallets over RPC"` 90 DebugLevel string `short:"d" long:"debuglevel" description:"Logging level {trace, debug, info, warn, error, critical}"` 91 LogDir *cfgutil.ExplicitString `long:"logdir" description:"Directory to log output."` 92 LogSize string `long:"logsize" description:"Maximum size of log file before it is rotated"` 93 NoFileLogging bool `long:"nofilelogging" description:"Disable file logging"` 94 Profile []string `long:"profile" description:"Enable HTTP profiling this interface/port"` 95 MemProfile string `long:"memprofile" description:"Write mem profile to the specified file"` 96 CPUProfile string `long:"cpuprofile" description:"Write cpu profile to the specified file"` 97 98 // Wallet options 99 WalletPass string `long:"walletpass" default-mask:"-" description:"Public wallet password; required when created with one"` 100 PromptPass bool `long:"promptpass" description:"Prompt for private passphase from terminal and unlock without timeout"` 101 Pass string `long:"pass" description:"Unlock with private passphrase"` 102 PromptPublicPass bool `long:"promptpublicpass" description:"Prompt for public passphrase from terminal"` 103 EnableTicketBuyer bool `long:"enableticketbuyer" description:"Enable the automatic ticket buyer"` 104 EnableVoting bool `long:"enablevoting" description:"Automatically create votes and revocations"` 105 PurchaseAccount string `long:"purchaseaccount" description:"Account to autobuy tickets from"` 106 PoolAddress *cfgutil.AddressFlag `long:"pooladdress" description:"VSP fee address"` 107 poolAddress stdaddr.StakeAddress 108 PoolFees float64 `long:"poolfees" description:"VSP fee percentage (1.00 equals 1.00% fee)"` 109 GapLimit uint32 `long:"gaplimit" description:"Allowed unused address gap between used addresses of accounts"` 110 WatchLast uint32 `long:"watchlast" description:"Limit watched previous addresses of each HD account branch"` 111 StakePoolColdExtKey string `long:"stakepoolcoldextkey" description:"xpub:maxindex for fee addresses (VSP-only option)"` 112 ManualTickets bool `long:"manualtickets" description:"Do not discover new tickets through network synchronization"` 113 AllowHighFees bool `long:"allowhighfees" description:"Do not perform high fee checks"` 114 RelayFee *cfgutil.AmountFlag `long:"txfee" description:"Transaction fee per kilobyte"` 115 AccountGapLimit int `long:"accountgaplimit" description:"Allowed gap of unused accounts"` 116 DisableCoinTypeUpgrades bool `long:"disablecointypeupgrades" description:"Never upgrade from legacy to SLIP0044 coin type keys"` 117 118 // RPC client options 119 RPCConnect string `short:"c" long:"rpcconnect" description:"Network address of dcrd RPC server"` 120 CAFile *cfgutil.ExplicitString `long:"cafile" description:"dcrd RPC Certificate Authority"` 121 ClientCAFile *cfgutil.ExplicitString `long:"clientcafile" description:"Certficate Authority to verify TLS client certificates"` 122 DisableClientTLS bool `long:"noclienttls" description:"Disable TLS for dcrd RPC; only allowed when connecting to localhost"` 123 DcrdUsername string `long:"dcrdusername" description:"dcrd RPC username; overrides --username"` 124 DcrdPassword string `long:"dcrdpassword" default-mask:"-" description:"dcrd RPC password; overrides --password"` 125 126 // Proxy and Tor settings 127 Proxy string `long:"proxy" description:"Establish network connections and DNS lookups through a SOCKS5 proxy (e.g. 127.0.0.1:9050)"` 128 ProxyUser string `long:"proxyuser" description:"Proxy server username"` 129 ProxyPass string `long:"proxypass" default-mask:"-" description:"Proxy server password"` 130 CircuitLimit int `long:"circuitlimit" description:"Set maximum number of open Tor circuits; used only when --torisolation is enabled"` 131 TorIsolation bool `long:"torisolation" description:"Enable Tor stream isolation by randomizing user credentials for each connection"` 132 NoDcrdProxy bool `long:"nodcrdproxy" description:"Never use configured proxy to dial dcrd websocket connectons"` 133 dial func(ctx context.Context, network, address string) (net.Conn, error) 134 lookup func(name string) ([]net.IP, error) 135 136 // SPV options 137 SPV bool `long:"spv" description:"Sync using simplified payment verification"` 138 SPVConnect []string `long:"spvconnect" description:"SPV sync only with specified peers; disables DNS seeding"` 139 140 // RPC server options 141 RPCCert *cfgutil.ExplicitString `long:"rpccert" description:"RPC server TLS certificate"` 142 RPCKey *cfgutil.ExplicitString `long:"rpckey" description:"RPC server TLS key"` 143 TLSCurve *cfgutil.CurveFlag `long:"tlscurve" description:"Curve to use when generating TLS keypairs"` 144 OneTimeTLSKey bool `long:"onetimetlskey" description:"Generate self-signed TLS keypairs each startup; only write certificate file"` 145 DisableServerTLS bool `long:"noservertls" description:"Disable TLS for the RPC servers; only allowed when binding to localhost"` 146 GRPCListeners []string `long:"grpclisten" description:"Listen for gRPC connections on this interface"` 147 LegacyRPCListeners []string `long:"rpclisten" description:"Listen for JSON-RPC connections on this interface"` 148 NoGRPC bool `long:"nogrpc" description:"Disable gRPC server"` 149 NoLegacyRPC bool `long:"nolegacyrpc" description:"Disable JSON-RPC server"` 150 LegacyRPCMaxClients int64 `long:"rpcmaxclients" description:"Max JSON-RPC HTTP POST clients"` 151 LegacyRPCMaxWebsockets int64 `long:"rpcmaxwebsockets" description:"Max JSON-RPC websocket clients"` 152 Username string `short:"u" long:"username" description:"JSON-RPC username and default dcrd RPC username"` 153 Password string `short:"P" long:"password" default-mask:"-" description:"JSON-RPC password and default dcrd RPC password"` 154 JSONRPCAuthType string `long:"jsonrpcauthtype" description:"Method for JSON-RPC client authentication (basic or clientcert)"` 155 156 // IPC options 157 PipeTx *uint `long:"pipetx" description:"File descriptor or handle of write end pipe to enable child -> parent process communication"` 158 PipeRx *uint `long:"piperx" description:"File descriptor or handle of read end pipe to enable parent -> child process communication"` 159 RPCListenerEvents bool `long:"rpclistenerevents" description:"Notify JSON-RPC and gRPC listener addresses over the TX pipe"` 160 IssueClientCert bool `long:"issueclientcert" description:"Notify a client cert and key over the TX pipe for RPC authentication"` 161 162 // CSPP 163 CSPPServer string `long:"csppserver" description:"Network address of CoinShuffle++ server"` 164 CSPPServerCA string `long:"csppserver.ca" description:"CoinShuffle++ Certificate Authority"` 165 dialCSPPServer func(ctx context.Context, network, addr string) (net.Conn, error) 166 MixedAccount string `long:"mixedaccount" description:"Account/branch used to derive CoinShuffle++ mixed outputs and voting rewards"` 167 mixedAccount string 168 mixedBranch uint32 169 TicketSplitAccount string `long:"ticketsplitaccount" description:"Account to derive fresh addresses from for mixed ticket splits; uses mixedaccount if unset"` 170 ChangeAccount string `long:"changeaccount" description:"Account used to derive unmixed CoinJoin outputs in CoinShuffle++ protocol"` 171 MixChange bool `long:"mixchange" description:"Use CoinShuffle++ to mix change account outputs into mix account"` 172 MixSplitLimit int `long:"mixsplitlimit" description:"Connection limit to CoinShuffle++ server per change amount"` 173 174 TBOpts ticketBuyerOptions `group:"Ticket Buyer Options" namespace:"ticketbuyer"` 175 176 VSPOpts vspOptions `group:"VSP Options" namespace:"vsp"` 177 } 178 179 type ticketBuyerOptions struct { 180 BalanceToMaintainAbsolute *cfgutil.AmountFlag `long:"balancetomaintainabsolute" description:"Amount of funds to keep in wallet when purchasing tickets"` 181 VotingAddress *cfgutil.AddressFlag `long:"votingaddress" description:"Purchase tickets with voting rights assigned to this address"` 182 votingAddress stdaddr.StakeAddress 183 Limit uint `long:"limit" description:"Buy no more than specified number of tickets per block"` 184 VotingAccount string `long:"votingaccount" description:"Account used to derive addresses specifying voting rights"` 185 } 186 187 type vspOptions struct { 188 // VSP - TODO: VSPServer to a []string to support multiple VSPs 189 URL string `long:"url" description:"Base URL of the VSP server"` 190 PubKey string `long:"pubkey" description:"VSP server pubkey"` 191 Sync bool `long:"sync" description:"sync tickets to vsp"` 192 MaxFee *cfgutil.AmountFlag `long:"maxfee" description:"Maximum VSP fee"` 193 } 194 195 // cleanAndExpandPath expands environement variables and leading ~ in the 196 // passed path, cleans the result, and returns it. 197 func cleanAndExpandPath(path string) string { 198 // Do not try to clean the empty string 199 if path == "" { 200 return "" 201 } 202 203 // NOTE: The os.ExpandEnv doesn't work with Windows cmd.exe-style 204 // %VARIABLE%, but they variables can still be expanded via POSIX-style 205 // $VARIABLE. 206 path = os.ExpandEnv(path) 207 208 if !strings.HasPrefix(path, "~") { 209 return filepath.Clean(path) 210 } 211 212 // Expand initial ~ to the current user's home directory, or ~otheruser 213 // to otheruser's home directory. On Windows, both forward and backward 214 // slashes can be used. 215 path = path[1:] 216 217 var pathSeparators string 218 if runtime.GOOS == "windows" { 219 pathSeparators = string(os.PathSeparator) + "/" 220 } else { 221 pathSeparators = string(os.PathSeparator) 222 } 223 224 userName := "" 225 if i := strings.IndexAny(path, pathSeparators); i != -1 { 226 userName = path[:i] 227 path = path[i:] 228 } 229 230 homeDir := "" 231 var u *user.User 232 var err error 233 if userName == "" { 234 u, err = user.Current() 235 } else { 236 u, err = user.Lookup(userName) 237 } 238 if err == nil { 239 homeDir = u.HomeDir 240 } 241 // Fallback to CWD if user lookup fails or user has no home directory. 242 if homeDir == "" { 243 homeDir = "." 244 } 245 246 return filepath.Join(homeDir, path) 247 } 248 249 // validLogLevel returns whether or not logLevel is a valid debug log level. 250 func validLogLevel(logLevel string) bool { 251 _, ok := slog.LevelFromString(logLevel) 252 return ok 253 } 254 255 // supportedSubsystems returns a sorted slice of the supported subsystems for 256 // logging purposes. 257 func supportedSubsystems() []string { 258 // Convert the subsystemLoggers map keys to a slice. 259 subsystems := make([]string, 0, len(subsystemLoggers)) 260 for subsysID := range subsystemLoggers { 261 subsystems = append(subsystems, subsysID) 262 } 263 264 // Sort the subsytems for stable display. 265 sort.Strings(subsystems) 266 return subsystems 267 } 268 269 // parseAndSetDebugLevels attempts to parse the specified debug level and set 270 // the levels accordingly. An appropriate error is returned if anything is 271 // invalid. 272 func parseAndSetDebugLevels(debugLevel string) error { 273 // When the specified string doesn't have any delimters, treat it as 274 // the log level for all subsystems. 275 if !strings.Contains(debugLevel, ",") && !strings.Contains(debugLevel, "=") { 276 // Validate debug log level. 277 if !validLogLevel(debugLevel) { 278 str := "The specified debug level [%v] is invalid" 279 return errors.Errorf(str, debugLevel) 280 } 281 282 // Change the logging level for all subsystems. 283 setLogLevels(debugLevel) 284 285 return nil 286 } 287 288 // Split the specified string into subsystem/level pairs while detecting 289 // issues and update the log levels accordingly. 290 for _, logLevelPair := range strings.Split(debugLevel, ",") { 291 if !strings.Contains(logLevelPair, "=") { 292 str := "The specified debug level contains an invalid " + 293 "subsystem/level pair [%v]" 294 return errors.Errorf(str, logLevelPair) 295 } 296 297 // Extract the specified subsystem and log level. 298 fields := strings.Split(logLevelPair, "=") 299 subsysID, logLevel := fields[0], fields[1] 300 301 // Validate subsystem. 302 if _, exists := subsystemLoggers[subsysID]; !exists { 303 str := "The specified subsystem [%v] is invalid -- " + 304 "supported subsytems %v" 305 return errors.Errorf(str, subsysID, supportedSubsystems()) 306 } 307 308 // Validate log level. 309 if !validLogLevel(logLevel) { 310 str := "The specified debug level [%v] is invalid" 311 return errors.Errorf(str, logLevel) 312 } 313 314 setLogLevel(subsysID, logLevel) 315 } 316 317 return nil 318 } 319 320 // loadConfig initializes and parses the config using a config file and command 321 // line options. 322 // 323 // The configuration proceeds as follows: 324 // 1. Start with a default config with sane settings 325 // 2. Pre-parse the command line to check for an alternative config file 326 // 3. Load configuration file overwriting defaults with any specified options 327 // 4. Parse CLI options and overwrite/add any specified options 328 // 329 // The above results in dcrwallet functioning properly without any config 330 // settings while still allowing the user to override settings with config files 331 // and command line options. Command line options always take precedence. 332 // The bool returned indicates whether or not the wallet was recreated from a 333 // seed and needs to perform the initial resync. The []byte is the private 334 // passphrase required to do the sync for this special case. 335 func loadConfig(ctx context.Context) (*config, []string, error) { 336 loadConfigError := func(err error) (*config, []string, error) { 337 return nil, nil, err 338 } 339 340 // Default config. 341 cfg := config{ 342 DebugLevel: defaultLogLevel, 343 ConfigFile: cfgutil.NewExplicitString(defaultConfigFile), 344 AppDataDir: cfgutil.NewExplicitString(defaultAppDataDir), 345 LogDir: cfgutil.NewExplicitString(defaultLogDir), 346 LogSize: defaultLogSize, 347 WalletPass: wallet.InsecurePubPassphrase, 348 CAFile: cfgutil.NewExplicitString(""), 349 ClientCAFile: cfgutil.NewExplicitString(defaultRPCClientCAFile), 350 dial: new(net.Dialer).DialContext, 351 lookup: net.LookupIP, 352 PromptPass: defaultPromptPass, 353 Pass: defaultPass, 354 PromptPublicPass: defaultPromptPublicPass, 355 RPCKey: cfgutil.NewExplicitString(defaultRPCKeyFile), 356 RPCCert: cfgutil.NewExplicitString(defaultRPCCertFile), 357 TLSCurve: cfgutil.NewCurveFlag(cfgutil.PreferredCurve), 358 LegacyRPCMaxClients: defaultRPCMaxClients, 359 LegacyRPCMaxWebsockets: defaultRPCMaxWebsockets, 360 JSONRPCAuthType: defaultAuthType, 361 EnableTicketBuyer: defaultEnableTicketBuyer, 362 EnableVoting: defaultEnableVoting, 363 PurchaseAccount: defaultPurchaseAccount, 364 GapLimit: defaultGapLimit, 365 StakePoolColdExtKey: defaultStakePoolColdExtKey, 366 AllowHighFees: defaultAllowHighFees, 367 RelayFee: cfgutil.NewAmountFlag(txrules.DefaultRelayFeePerKb), 368 PoolAddress: cfgutil.NewAddressFlag(), 369 AccountGapLimit: defaultAccountGapLimit, 370 DisableCoinTypeUpgrades: defaultDisableCoinTypeUpgrades, 371 CircuitLimit: defaultCircuitLimit, 372 MixSplitLimit: defaultMixSplitLimit, 373 374 // Ticket Buyer Options 375 TBOpts: ticketBuyerOptions{ 376 BalanceToMaintainAbsolute: cfgutil.NewAmountFlag(defaultBalanceToMaintainAbsolute), 377 VotingAddress: cfgutil.NewAddressFlag(), 378 Limit: defaultTicketbuyerLimit, 379 }, 380 381 VSPOpts: vspOptions{ 382 MaxFee: cfgutil.NewAmountFlag(defaultVSPMaxFee), 383 }, 384 } 385 386 // Pre-parse the command line options to see if an alternative config 387 // file or the version flag was specified. 388 preCfg := cfg 389 preParser := flags.NewParser(&preCfg, flags.Default) 390 _, err := preParser.Parse() 391 if err != nil { 392 var e *flags.Error 393 if errors.As(err, &e) && e.Type == flags.ErrHelp { 394 os.Exit(0) 395 } 396 preParser.WriteHelp(os.Stderr) 397 return loadConfigError(err) 398 } 399 400 // Show the version and exit if the version flag was specified. 401 funcName := "loadConfig" 402 appName := filepath.Base(os.Args[0]) 403 appName = strings.TrimSuffix(appName, filepath.Ext(appName)) 404 usageMessage := fmt.Sprintf("Use %s -h to show usage", appName) 405 if preCfg.ShowVersion { 406 fmt.Printf("%s version %s (Go version %s %s/%s)\n", appName, 407 version.String(), runtime.Version(), runtime.GOOS, runtime.GOARCH) 408 os.Exit(0) 409 } 410 411 // Load additional config from file. 412 var configFileError error 413 parser := flags.NewParser(&cfg, flags.Default) 414 configFilePath := preCfg.ConfigFile.Value 415 if preCfg.ConfigFile.ExplicitlySet() { 416 configFilePath = cleanAndExpandPath(configFilePath) 417 } else { 418 appDataDir := preCfg.AppDataDir.Value 419 if appDataDir != defaultAppDataDir { 420 configFilePath = filepath.Join(appDataDir, defaultConfigFilename) 421 } 422 } 423 err = flags.NewIniParser(parser).ParseFile(configFilePath) 424 if err != nil { 425 var e *os.PathError 426 if !errors.As(err, &e) { 427 fmt.Fprintln(os.Stderr, err) 428 parser.WriteHelp(os.Stderr) 429 return loadConfigError(err) 430 } 431 configFileError = err 432 } 433 434 // Parse command line options again to ensure they take precedence. 435 remainingArgs, err := parser.Parse() 436 if err != nil { 437 var e *flags.Error 438 if !errors.As(err, &e) || e.Type != flags.ErrHelp { 439 parser.WriteHelp(os.Stderr) 440 } 441 return loadConfigError(err) 442 } 443 444 // If an alternate data directory was specified, and paths with defaults 445 // relative to the data dir are unchanged, modify each path to be 446 // relative to the new data dir. 447 if cfg.AppDataDir.ExplicitlySet() { 448 cfg.AppDataDir.Value = cleanAndExpandPath(cfg.AppDataDir.Value) 449 if !cfg.RPCKey.ExplicitlySet() { 450 cfg.RPCKey.Value = filepath.Join(cfg.AppDataDir.Value, "rpc.key") 451 } 452 if !cfg.RPCCert.ExplicitlySet() { 453 cfg.RPCCert.Value = filepath.Join(cfg.AppDataDir.Value, "rpc.cert") 454 } 455 if !cfg.ClientCAFile.ExplicitlySet() { 456 cfg.ClientCAFile.Value = filepath.Join(cfg.AppDataDir.Value, "clients.pem") 457 } 458 if !cfg.LogDir.ExplicitlySet() { 459 cfg.LogDir.Value = filepath.Join(cfg.AppDataDir.Value, defaultLogDirname) 460 } 461 } 462 463 // Choose the active network params based on the selected network. 464 // Multiple networks can't be selected simultaneously. 465 numNets := 0 466 if cfg.TestNet { 467 activeNet = &netparams.TestNet3Params 468 numNets++ 469 } 470 if cfg.SimNet { 471 activeNet = &netparams.SimNetParams 472 numNets++ 473 } 474 if numNets > 1 { 475 str := "%s: The testnet and simnet params can't be used " + 476 "together -- choose one" 477 err := errors.Errorf(str, "loadConfig") 478 fmt.Fprintln(os.Stderr, err) 479 return loadConfigError(err) 480 } 481 482 if !cfg.NoFileLogging { 483 // Append the network type to the log directory so it is 484 // "namespaced" per network. 485 cfg.LogDir.Value = cleanAndExpandPath(cfg.LogDir.Value) 486 cfg.LogDir.Value = filepath.Join(cfg.LogDir.Value, 487 activeNet.Params.Name) 488 489 var units int 490 for i, r := range cfg.LogSize { 491 if r < '0' || r > '9' { 492 units = i 493 break 494 } 495 } 496 invalidSize := func() error { 497 str := "%s: Invalid logsize: %v " 498 err := errors.Errorf(str, funcName, cfg.LogSize) 499 fmt.Fprintln(os.Stderr, err) 500 return err 501 } 502 if units == 0 { 503 return loadConfigError(invalidSize()) 504 } 505 // Parsing a 32-bit number prevents 64-bit overflow after unit 506 // multiplication. 507 logsize, err := strconv.ParseInt(cfg.LogSize[:units], 10, 32) 508 if err != nil { 509 return loadConfigError(invalidSize()) 510 } 511 switch cfg.LogSize[units:] { 512 case "k", "K", "KiB": 513 case "m", "M", "MiB": 514 logsize <<= 10 515 case "g", "G", "GiB": 516 logsize <<= 20 517 default: 518 return loadConfigError(invalidSize()) 519 } 520 521 // Initialize log rotation. After log rotation has been initialized, the 522 // logger variables may be used. 523 loggers.InitLogRotator(filepath.Join(cfg.LogDir.Value, defaultLogFilename), logsize) 524 } 525 526 // Special show command to list supported subsystems and exit. 527 if cfg.DebugLevel == "show" { 528 fmt.Println("Supported subsystems", supportedSubsystems()) 529 os.Exit(0) 530 } 531 532 // Check that no addresses were created for the wrong network 533 for _, a := range []struct { 534 flag *cfgutil.AddressFlag 535 addr *stdaddr.StakeAddress 536 }{ 537 {cfg.PoolAddress, &cfg.poolAddress}, 538 {cfg.TBOpts.VotingAddress, &cfg.TBOpts.votingAddress}, 539 } { 540 addr, err := a.flag.StakeAddress(activeNet.Params) 541 if err != nil { 542 log.Error(err) 543 return loadConfigError(err) 544 } 545 *a.addr = addr 546 } 547 548 // Parse, validate, and set debug log level(s). 549 if err := parseAndSetDebugLevels(cfg.DebugLevel); err != nil { 550 err := errors.Errorf("%s: %v", "loadConfig", err.Error()) 551 fmt.Fprintln(os.Stderr, err) 552 parser.WriteHelp(os.Stderr) 553 return loadConfigError(err) 554 } 555 556 // Error and shutdown if config file is specified on the command line 557 // but cannot be found. 558 if configFileError != nil && cfg.ConfigFile.ExplicitlySet() { 559 if preCfg.ConfigFile.ExplicitlySet() || cfg.ConfigFile.ExplicitlySet() { 560 log.Errorf("%v", configFileError) 561 return loadConfigError(configFileError) 562 } 563 } 564 565 // Warn about missing config file after the final command line parse 566 // succeeds. This prevents the warning on help messages and invalid 567 // options. 568 if configFileError != nil { 569 log.Warnf("%v", configFileError) 570 } 571 572 // Sanity check BalanceToMaintainAbsolute 573 if cfg.TBOpts.BalanceToMaintainAbsolute.ToCoin() < 0 { 574 str := "%s: balancetomaintainabsolute cannot be negative: %v" 575 err := errors.Errorf(str, funcName, cfg.TBOpts.BalanceToMaintainAbsolute) 576 fmt.Fprintln(os.Stderr, err) 577 return loadConfigError(err) 578 } 579 580 // Exit if you try to use a simulation wallet with a standard 581 // data directory. 582 if !cfg.AppDataDir.ExplicitlySet() && cfg.CreateTemp { 583 fmt.Fprintln(os.Stderr, "Tried to create a temporary simulation "+ 584 "wallet, but failed to specify data directory!") 585 os.Exit(0) 586 } 587 588 // Exit if you try to use a simulation wallet on anything other than 589 // simnet or testnet. 590 if !cfg.SimNet && cfg.CreateTemp { 591 fmt.Fprintln(os.Stderr, "Tried to create a temporary simulation "+ 592 "wallet for network other than simnet!") 593 os.Exit(0) 594 } 595 596 // Ensure the wallet exists or create it when the create flag is set. 597 netDir := networkDir(cfg.AppDataDir.Value, activeNet.Params) 598 dbPath := filepath.Join(netDir, walletDbName) 599 600 if cfg.CreateTemp && cfg.Create { 601 err := errors.Errorf("The flags --create and --createtemp can not " + 602 "be specified together. Use --help for more information.") 603 fmt.Fprintln(os.Stderr, err) 604 return loadConfigError(err) 605 } 606 607 dbFileExists, err := cfgutil.FileExists(dbPath) 608 if err != nil { 609 fmt.Fprintln(os.Stderr, err) 610 return loadConfigError(err) 611 } 612 613 if cfg.CreateTemp { 614 tempWalletExists := false 615 616 if dbFileExists { 617 str := fmt.Sprintf("The wallet already exists. Loading this " + 618 "wallet instead.") 619 fmt.Fprintln(os.Stdout, str) 620 tempWalletExists = true 621 } 622 623 // Ensure the data directory for the network exists. 624 if err := checkCreateDir(netDir); err != nil { 625 fmt.Fprintln(os.Stderr, err) 626 return loadConfigError(err) 627 } 628 629 if !tempWalletExists { 630 // Perform the initial wallet creation wizard. 631 if err := createSimulationWallet(ctx, &cfg); err != nil { 632 fmt.Fprintln(os.Stderr, "Unable to create wallet:", err) 633 return loadConfigError(err) 634 } 635 } 636 } else if cfg.Create || cfg.CreateWatchingOnly { 637 // Error if the create flag is set and the wallet already 638 // exists. 639 if dbFileExists { 640 err := errors.Errorf("The wallet database file `%v` "+ 641 "already exists.", dbPath) 642 fmt.Fprintln(os.Stderr, err) 643 return loadConfigError(err) 644 } 645 646 // Ensure the data directory for the network exists. 647 if err := checkCreateDir(netDir); err != nil { 648 fmt.Fprintln(os.Stderr, err) 649 return loadConfigError(err) 650 } 651 652 // Perform the initial wallet creation wizard. 653 os.Stdout.Sync() 654 if cfg.CreateWatchingOnly { 655 err = createWatchingOnlyWallet(ctx, &cfg) 656 } else { 657 err = createWallet(ctx, &cfg) 658 } 659 if err != nil { 660 fmt.Fprintln(os.Stderr, "Unable to create wallet:", err) 661 return loadConfigError(err) 662 } 663 664 // Created successfully, so exit now with success. 665 os.Exit(0) 666 } else if !dbFileExists && !cfg.NoInitialLoad { 667 err := errors.Errorf("The wallet does not exist. Run with the " + 668 "--create option to initialize and create it.") 669 fmt.Fprintln(os.Stderr, err) 670 return loadConfigError(err) 671 } 672 673 if cfg.PoolFees != 0.0 { 674 if !txrules.ValidPoolFeeRate(cfg.PoolFees) { 675 err := errors.E(errors.Invalid, errors.Errorf("pool fee rate %v", cfg.PoolFees)) 676 fmt.Fprintln(os.Stderr, err.Error()) 677 fmt.Fprintln(os.Stderr, usageMessage) 678 return loadConfigError(err) 679 } 680 } 681 682 ipNet := func(cidr string) net.IPNet { 683 _, ipNet, err := net.ParseCIDR(cidr) 684 if err != nil { 685 panic(err) 686 } 687 return *ipNet 688 } 689 privNets := []net.IPNet{ 690 // IPv4 loopback 691 ipNet("127.0.0.0/8"), 692 693 // IPv6 loopback 694 ipNet("::1/128"), 695 696 // RFC 1918 697 ipNet("10.0.0.0/8"), 698 ipNet("172.16.0.0/12"), 699 ipNet("192.168.0.0/16"), 700 701 // RFC 4193 702 ipNet("fc00::/7"), 703 } 704 705 // Set dialer and DNS lookup functions if proxy settings are provided. 706 if cfg.Proxy != "" { 707 proxy := socks.Proxy{ 708 Addr: cfg.Proxy, 709 Username: cfg.ProxyUser, 710 Password: cfg.ProxyPass, 711 TorIsolation: cfg.TorIsolation, 712 } 713 714 var proxyDialer func(context.Context, string, string) (net.Conn, error) 715 var noproxyDialer net.Dialer 716 if cfg.TorIsolation { 717 proxyDialer = socks.NewPool(proxy, uint32(cfg.CircuitLimit)).DialContext 718 } else { 719 proxyDialer = proxy.DialContext 720 } 721 722 cfg.dial = func(ctx context.Context, network, address string) (net.Conn, error) { 723 host, _, err := net.SplitHostPort(address) 724 if err != nil { 725 host = address 726 } 727 if host == "localhost" { 728 return noproxyDialer.DialContext(ctx, network, address) 729 } 730 ip := net.ParseIP(host) 731 if len(ip) == 4 || len(ip) == 16 { 732 for i := range privNets { 733 if privNets[i].Contains(ip) { 734 return noproxyDialer.DialContext(ctx, network, address) 735 } 736 } 737 } 738 conn, err := proxyDialer(ctx, network, address) 739 if err != nil { 740 return nil, errors.Errorf("proxy dial %v %v: %w", network, address, err) 741 } 742 return conn, nil 743 } 744 cfg.lookup = func(host string) ([]net.IP, error) { 745 ip, err := connmgr.TorLookupIP(context.Background(), host, cfg.Proxy) 746 if err != nil { 747 return nil, errors.Errorf("proxy lookup for %v: %w", host, err) 748 } 749 return ip, nil 750 } 751 } 752 753 // Create CoinShuffle++ TLS dialer based on server name and certificate 754 // authority settings. 755 csppTLSConfig := new(tls.Config) 756 if cfg.CSPPServer != "" { 757 csppTLSConfig.ServerName, _, err = net.SplitHostPort(cfg.CSPPServer) 758 if err != nil { 759 err := errors.Errorf("Cannot parse CoinShuffle++ "+ 760 "server name %q: %v", cfg.CSPPServer, err) 761 fmt.Fprintln(os.Stderr, err.Error()) 762 return loadConfigError(err) 763 } 764 } 765 if cfg.CSPPServerCA != "" { 766 cfg.CSPPServerCA = cleanAndExpandPath(cfg.CSPPServerCA) 767 ca, err := os.ReadFile(cfg.CSPPServerCA) 768 if err != nil { 769 err := errors.Errorf("Cannot read CoinShuffle++ "+ 770 "Certificate Authority file: %v", err) 771 fmt.Fprintln(os.Stderr, err.Error()) 772 return loadConfigError(err) 773 } 774 pool := x509.NewCertPool() 775 pool.AppendCertsFromPEM(ca) 776 csppTLSConfig.RootCAs = pool 777 } 778 cfg.dialCSPPServer = func(ctx context.Context, network, addr string) (net.Conn, error) { 779 conn, err := cfg.dial(ctx, network, addr) 780 if err != nil { 781 return nil, err 782 } 783 conn = tls.Client(conn, csppTLSConfig) 784 return conn, nil 785 } 786 787 // Parse mixedaccount account/branch 788 if cfg.MixedAccount != "" { 789 indexSlash := strings.LastIndex(cfg.MixedAccount, "/") 790 if indexSlash == -1 { 791 err := errors.Errorf("--mixedaccount must have form 'accountname/branch'") 792 fmt.Fprintln(os.Stderr, err) 793 return loadConfigError(err) 794 } 795 cfg.mixedAccount = cfg.MixedAccount[:indexSlash] 796 switch cfg.MixedAccount[indexSlash+1:] { 797 case "0": 798 cfg.mixedBranch = 0 799 case "1": 800 cfg.mixedBranch = 1 801 default: 802 err := errors.Errorf("--mixedaccount branch must be 0 or 1") 803 fmt.Fprintln(os.Stderr, err) 804 return loadConfigError(err) 805 } 806 } 807 // Use mixedaccount as default ticketsplitaccount if unset. 808 if cfg.TicketSplitAccount == "" { 809 cfg.TicketSplitAccount = cfg.mixedAccount 810 } 811 812 if cfg.RPCConnect == "" { 813 cfg.RPCConnect = net.JoinHostPort("localhost", activeNet.JSONRPCClientPort) 814 } 815 816 // Add default port to connect flag if missing. 817 cfg.RPCConnect, err = cfgutil.NormalizeAddress(cfg.RPCConnect, 818 activeNet.JSONRPCClientPort) 819 if err != nil { 820 fmt.Fprintf(os.Stderr, 821 "Invalid rpcconnect network address: %v\n", err) 822 return loadConfigError(err) 823 } 824 825 localhostListeners := map[string]struct{}{ 826 "localhost": {}, 827 "127.0.0.1": {}, 828 "::1": {}, 829 } 830 RPCHost, _, err := net.SplitHostPort(cfg.RPCConnect) 831 if err != nil { 832 return loadConfigError(err) 833 } 834 if cfg.DisableClientTLS { 835 if _, ok := localhostListeners[RPCHost]; !ok { 836 str := "%s: the --noclienttls option may not be used " + 837 "when connecting RPC to non localhost " + 838 "addresses: %s" 839 err := errors.Errorf(str, funcName, cfg.RPCConnect) 840 fmt.Fprintln(os.Stderr, err) 841 fmt.Fprintln(os.Stderr, usageMessage) 842 return loadConfigError(err) 843 } 844 } else { 845 // If CAFile is unset, choose either the copy or local dcrd cert. 846 if !cfg.CAFile.ExplicitlySet() { 847 cfg.CAFile.Value = filepath.Join(cfg.AppDataDir.Value, defaultCAFilename) 848 849 // If the CA copy does not exist, check if we're connecting to 850 // a local dcrd and switch to its RPC cert if it exists. 851 certExists, err := cfgutil.FileExists(cfg.CAFile.Value) 852 if err != nil { 853 fmt.Fprintln(os.Stderr, err) 854 return loadConfigError(err) 855 } 856 if !certExists { 857 if _, ok := localhostListeners[RPCHost]; ok { 858 dcrdCertExists, err := cfgutil.FileExists( 859 dcrdDefaultCAFile) 860 if err != nil { 861 fmt.Fprintln(os.Stderr, err) 862 return loadConfigError(err) 863 } 864 if dcrdCertExists { 865 cfg.CAFile.Value = dcrdDefaultCAFile 866 } 867 } 868 } 869 } 870 } 871 872 if cfg.SPV && cfg.EnableVoting { 873 err := errors.E("SPV voting is not possible: disable --spv or --enablevoting") 874 fmt.Fprintln(os.Stderr, err) 875 return loadConfigError(err) 876 } 877 if !cfg.SPV && len(cfg.SPVConnect) > 0 { 878 err := errors.E("--spvconnect requires --spv") 879 fmt.Fprintln(os.Stderr, err) 880 return loadConfigError(err) 881 } 882 for i, p := range cfg.SPVConnect { 883 cfg.SPVConnect[i], err = cfgutil.NormalizeAddress(p, activeNet.Params.DefaultPort) 884 if err != nil { 885 return loadConfigError(err) 886 } 887 } 888 889 // Default to localhost listen addresses if no listeners were manually 890 // specified. When the RPC server is configured to be disabled, remove all 891 // listeners so it is not started. 892 localhostAddrs, err := net.LookupHost("localhost") 893 if err != nil { 894 return loadConfigError(err) 895 } 896 if len(cfg.GRPCListeners) == 0 && !cfg.NoGRPC { 897 cfg.GRPCListeners = make([]string, 0, len(localhostAddrs)) 898 for _, addr := range localhostAddrs { 899 cfg.GRPCListeners = append(cfg.GRPCListeners, 900 net.JoinHostPort(addr, activeNet.GRPCServerPort)) 901 } 902 } else if cfg.NoGRPC { 903 cfg.GRPCListeners = nil 904 } 905 if len(cfg.LegacyRPCListeners) == 0 && !cfg.NoLegacyRPC { 906 cfg.LegacyRPCListeners = make([]string, 0, len(localhostAddrs)) 907 for _, addr := range localhostAddrs { 908 cfg.LegacyRPCListeners = append(cfg.LegacyRPCListeners, 909 net.JoinHostPort(addr, activeNet.JSONRPCServerPort)) 910 } 911 } else if cfg.NoLegacyRPC { 912 cfg.LegacyRPCListeners = nil 913 } 914 915 // Add default port to all rpc listener addresses if needed and remove 916 // duplicate addresses. 917 cfg.LegacyRPCListeners, err = cfgutil.NormalizeAddresses( 918 cfg.LegacyRPCListeners, activeNet.JSONRPCServerPort) 919 if err != nil { 920 fmt.Fprintf(os.Stderr, 921 "Invalid network address in legacy RPC listeners: %v\n", err) 922 return loadConfigError(err) 923 } 924 cfg.GRPCListeners, err = cfgutil.NormalizeAddresses( 925 cfg.GRPCListeners, activeNet.GRPCServerPort) 926 if err != nil { 927 fmt.Fprintf(os.Stderr, 928 "Invalid network address in RPC listeners: %v\n", err) 929 return loadConfigError(err) 930 } 931 932 // Both RPC servers may not listen on the same interface/port, with the 933 // exception of listeners using port 0. 934 if len(cfg.LegacyRPCListeners) > 0 && len(cfg.GRPCListeners) > 0 { 935 seenAddresses := make(map[string]struct{}, len(cfg.LegacyRPCListeners)) 936 for _, addr := range cfg.LegacyRPCListeners { 937 seenAddresses[addr] = struct{}{} 938 } 939 for _, addr := range cfg.GRPCListeners { 940 _, seen := seenAddresses[addr] 941 if seen && !strings.HasSuffix(addr, ":0") { 942 err := errors.Errorf("Address `%s` may not be "+ 943 "used as a listener address for both "+ 944 "RPC servers", addr) 945 fmt.Fprintln(os.Stderr, err) 946 return loadConfigError(err) 947 } 948 } 949 } 950 951 // Only allow server TLS to be disabled if the RPC server is bound to 952 // localhost addresses. 953 if cfg.DisableServerTLS { 954 allListeners := append(cfg.LegacyRPCListeners, cfg.GRPCListeners...) 955 for _, addr := range allListeners { 956 host, _, err := net.SplitHostPort(addr) 957 if err != nil { 958 str := "%s: RPC listen interface '%s' is " + 959 "invalid: %v" 960 err := errors.Errorf(str, funcName, addr, err) 961 fmt.Fprintln(os.Stderr, err) 962 fmt.Fprintln(os.Stderr, usageMessage) 963 return loadConfigError(err) 964 } 965 if _, ok := localhostListeners[host]; !ok { 966 str := "%s: the --noservertls option may not be used " + 967 "when binding RPC to non localhost " + 968 "addresses: %s" 969 err := errors.Errorf(str, funcName, addr) 970 fmt.Fprintln(os.Stderr, err) 971 fmt.Fprintln(os.Stderr, usageMessage) 972 return loadConfigError(err) 973 } 974 } 975 } 976 977 // If either VSP pubkey or URL are specified, validate VSP options. 978 if cfg.VSPOpts.PubKey != "" || cfg.VSPOpts.URL != "" { 979 if cfg.VSPOpts.PubKey == "" { 980 err := errors.New("vsp pubkey can not be null") 981 fmt.Fprintln(os.Stderr, err) 982 return loadConfigError(err) 983 } 984 if cfg.VSPOpts.URL == "" { 985 err := errors.New("vsp URL can not be null") 986 fmt.Fprintln(os.Stderr, err) 987 return loadConfigError(err) 988 } 989 if cfg.VSPOpts.MaxFee.Amount == 0 { 990 err := errors.New("vsp max fee must be greater than zero") 991 fmt.Fprintln(os.Stderr, err) 992 return loadConfigError(err) 993 } 994 } 995 996 // Expand environment variable and leading ~ for filepaths. 997 cfg.CAFile.Value = cleanAndExpandPath(cfg.CAFile.Value) 998 cfg.RPCCert.Value = cleanAndExpandPath(cfg.RPCCert.Value) 999 cfg.RPCKey.Value = cleanAndExpandPath(cfg.RPCKey.Value) 1000 cfg.ClientCAFile.Value = cleanAndExpandPath(cfg.ClientCAFile.Value) 1001 1002 // If the dcrd username or password are unset, use the same auth as for 1003 // the client. The two settings were previously shared for dcrd and 1004 // client auth, so this avoids breaking backwards compatibility while 1005 // allowing users to use different auth settings for dcrd and wallet. 1006 if cfg.DcrdUsername == "" { 1007 cfg.DcrdUsername = cfg.Username 1008 } 1009 if cfg.DcrdPassword == "" { 1010 cfg.DcrdPassword = cfg.Password 1011 } 1012 1013 switch cfg.JSONRPCAuthType { 1014 case "basic", "clientcert": 1015 default: 1016 err := fmt.Errorf("unknown authtype %q", cfg.JSONRPCAuthType) 1017 fmt.Fprintln(os.Stderr, err) 1018 fmt.Fprintln(os.Stderr, usageMessage) 1019 return loadConfigError(err) 1020 } 1021 1022 // Warn if user still has an old ticket buyer configuration file. 1023 oldTBConfigFile := filepath.Join(cfg.AppDataDir.Value, "ticketbuyer.conf") 1024 if _, err := os.Stat(oldTBConfigFile); err == nil { 1025 log.Warnf("%s is no longer used and should be removed. "+ 1026 "Please prepend 'ticketbuyer.' to each option and "+ 1027 "move it under the [Ticket Buyer Options] section "+ 1028 "of %s\n", 1029 oldTBConfigFile, configFilePath) 1030 } 1031 1032 // Make list of old versions of testnet directories. 1033 var oldTestNets []string 1034 oldTestNets = append(oldTestNets, filepath.Join(cfg.AppDataDir.Value, "testnet")) 1035 // Warn if old testnet directory is present. 1036 for _, oldDir := range oldTestNets { 1037 oldDirExists, _ := cfgutil.FileExists(oldDir) 1038 if oldDirExists { 1039 log.Warnf("Wallet data from previous testnet"+ 1040 " found (%v) and can probably be removed.", 1041 oldDir) 1042 } 1043 } 1044 1045 return &cfg, remainingArgs, nil 1046 }