github.com/decred/politeia@v1.4.0/politeiawww/cmd/politeiavoter/config.go (about) 1 // Copyright (c) 2013-2014 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 "errors" 10 "fmt" 11 "net" 12 "os" 13 "path/filepath" 14 "runtime" 15 "sort" 16 "strconv" 17 "strings" 18 "time" 19 20 "github.com/decred/dcrd/dcrutil/v3" 21 "github.com/decred/go-socks/socks" 22 "github.com/decred/politeia/util" 23 "github.com/decred/politeia/util/version" 24 flags "github.com/jessevdk/go-flags" 25 ) 26 27 const ( 28 defaultConfigFilename = "politeiavoter.conf" 29 defaultLogLevel = "info" 30 defaultLogDirname = "logs" 31 defaultVoteDirname = "vote" 32 defaultLogFilename = "politeiavoter.log" 33 defaultWalletHost = "127.0.0.1" 34 35 defaultWalletMainnetPort = "9111" 36 defaultWalletTestnetPort = "19111" 37 38 walletCertFile = "rpc.cert" 39 clientCertFile = "client.pem" 40 clientKeyFile = "client-key.pem" 41 42 defaultBunches = uint(1) 43 44 // Testing stuff 45 testFailUnrecoverable = 1 46 ) 47 48 var ( 49 defaultHomeDir = dcrutil.AppDataDir("politeiavoter", false) 50 defaultConfigFile = filepath.Join(defaultHomeDir, defaultConfigFilename) 51 defaultLogDir = filepath.Join(defaultHomeDir, defaultLogDirname) 52 defaultVoteDir = filepath.Join(defaultHomeDir, defaultVoteDirname) 53 dcrwalletHomeDir = dcrutil.AppDataDir("dcrwallet", false) 54 defaultWalletCert = filepath.Join(dcrwalletHomeDir, walletCertFile) 55 defaultClientCert = filepath.Join(defaultHomeDir, clientCertFile) 56 defaultClientKey = filepath.Join(defaultHomeDir, clientKeyFile) 57 58 // defaultHoursPrior is the default HoursPrior config value. It's required 59 // to be var and not a const since the HoursPrior setting is a pointer. 60 defaultHoursPrior = uint64(12) 61 ) 62 63 // runServiceCommand is only set to a real function on Windows. It is used 64 // to parse and execute service commands specified via the -s flag. 65 var runServiceCommand func(string) error 66 67 // config defines the configuration options for dcrd. 68 // 69 // See loadConfig for details on the configuration load process. 70 type config struct { 71 ListCommands bool `short:"l" long:"listcommands" description:"List available commands"` 72 ShowVersion bool `short:"V" long:"version" description:"Display version information and exit"` 73 Version string 74 HomeDir string `short:"A" long:"appdata" description:"Path to application home directory"` 75 ConfigFile string `short:"C" long:"configfile" description:"Path to configuration file"` 76 LogDir string `long:"logdir" description:"Directory to log output."` 77 TestNet bool `long:"testnet" description:"Use the test network"` 78 PoliteiaWWW string `long:"politeiawww" description:"Politeia WWW host"` 79 Profile string `long:"profile" description:"Enable HTTP profiling on given port -- NOTE port must be between 1024 and 65536"` 80 DebugLevel string `short:"d" long:"debuglevel" description:"Logging level for all subsystems {trace, debug, info, warn, error, critical} -- You may also specify <subsystem>=<level>,<subsystem2>=<level>,... to set the log level for individual subsystems -- Use show to list available subsystems"` 81 WalletHost string `long:"wallethost" description:"Wallet host"` 82 WalletCert string `long:"walletgrpccert" description:"Wallet GRPC certificate"` 83 WalletPassphrase string `long:"walletpassphrase" description:"Wallet decryption passphrase"` 84 BypassProxyCheck bool `long:"bypassproxycheck" description:"Don't use this unless you know what you're doing."` 85 Proxy string `long:"proxy" description:"Connect via SOCKS5 proxy (eg. 127.0.0.1:9050)"` 86 ProxyUser string `long:"proxyuser" description:"Username for proxy server"` 87 ProxyPass string `long:"proxypass" default-mask:"-" description:"Password for proxy server"` 88 VoteDuration string `long:"voteduration" description:"Duration to cast all votes in hours and minutes e.g. 5h10m (default 0s means autodetect duration)"` 89 Trickle bool `long:"trickle" description:"Enable vote trickling, requires --proxy."` 90 Bunches uint `long:"bunches" description:"Number of parallel bunches that start at random times."` 91 SkipVerify bool `long:"skipverify" description:"Skip verifying the server's certifcate chain and host name."` 92 93 // HoursPrior designates the hours to subtract from the end of the 94 // voting period and is set to a default of 12 hours. These extra 95 // hours, prior to expiration gives the user some additional margin to 96 // correct failures. 97 HoursPrior *uint64 `long:"hoursprior" description:"Number of hours prior to the end of the voting period that all votes will be trickled in by."` 98 99 ClientCert string `long:"clientcert" description:"Path to TLS certificate for client authentication"` 100 ClientKey string `long:"clientkey" description:"Path to TLS client authentication key"` 101 102 voteDir string 103 dial func(string, string) (net.Conn, error) 104 voteDuration time.Duration // Parsed VoteDuration 105 hoursPrior time.Duration // Converted HoursPrior 106 blocksPerHour uint64 107 108 // Test only 109 testing bool 110 testingCounter int 111 testingMode int // Type of failure 112 } 113 114 // serviceOptions defines the configuration options for the daemon as a service 115 // on Windows. 116 type serviceOptions struct { 117 ServiceCommand string `short:"s" long:"service" description:"Service command {install, remove, start, stop}"` 118 } 119 120 // validLogLevel returns whether or not logLevel is a valid debug log level. 121 func validLogLevel(logLevel string) bool { 122 switch logLevel { 123 case "trace": 124 fallthrough 125 case "debug": 126 fallthrough 127 case "info": 128 fallthrough 129 case "warn": 130 fallthrough 131 case "error": 132 fallthrough 133 case "critical": 134 return true 135 } 136 return false 137 } 138 139 // supportedSubsystems returns a sorted slice of the supported subsystems for 140 // logging purposes. 141 func supportedSubsystems() []string { 142 // Convert the subsystemLoggers map keys to a slice. 143 subsystems := make([]string, 0, len(subsystemLoggers)) 144 for subsysID := range subsystemLoggers { 145 subsystems = append(subsystems, subsysID) 146 } 147 148 // Sort the subsytems for stable display. 149 sort.Strings(subsystems) 150 return subsystems 151 } 152 153 // parseAndSetDebugLevels attempts to parse the specified debug level and set 154 // the levels accordingly. An appropriate error is returned if anything is 155 // invalid. 156 func parseAndSetDebugLevels(debugLevel string) error { 157 // When the specified string doesn't have any delimters, treat it as 158 // the log level for all subsystems. 159 if !strings.Contains(debugLevel, ",") && !strings.Contains(debugLevel, "=") { 160 // Validate debug log level. 161 if !validLogLevel(debugLevel) { 162 str := "The specified debug level [%v] is invalid" 163 return fmt.Errorf(str, debugLevel) 164 } 165 166 // Change the logging level for all subsystems. 167 setLogLevels(debugLevel) 168 169 return nil 170 } 171 172 // Split the specified string into subsystem/level pairs while detecting 173 // issues and update the log levels accordingly. 174 for _, logLevelPair := range strings.Split(debugLevel, ",") { 175 if !strings.Contains(logLevelPair, "=") { 176 str := "The specified debug level contains an invalid " + 177 "subsystem/level pair [%v]" 178 return fmt.Errorf(str, logLevelPair) 179 } 180 181 // Extract the specified subsystem and log level. 182 fields := strings.Split(logLevelPair, "=") 183 subsysID, logLevel := fields[0], fields[1] 184 185 // Validate subsystem. 186 if _, exists := subsystemLoggers[subsysID]; !exists { 187 str := "The specified subsystem [%v] is invalid -- " + 188 "supported subsytems %v" 189 return fmt.Errorf(str, subsysID, supportedSubsystems()) 190 } 191 192 // Validate log level. 193 if !validLogLevel(logLevel) { 194 str := "The specified debug level [%v] is invalid" 195 return fmt.Errorf(str, logLevel) 196 } 197 198 setLogLevel(subsysID, logLevel) 199 } 200 201 return nil 202 } 203 204 // newConfigParser returns a new command line flags parser. 205 func newConfigParser(cfg *config, so *serviceOptions, options flags.Options) *flags.Parser { 206 parser := flags.NewParser(cfg, options) 207 if runtime.GOOS == "windows" { 208 parser.AddGroup("Service Options", "Service Options", so) 209 } 210 return parser 211 } 212 213 // errSuppressUsage signifies that an error that happened during the initial 214 // configuration phase should suppress the usage output since it was not caused 215 // by the user. 216 type errSuppressUsage string 217 218 // Error implements the error interface. 219 func (e errSuppressUsage) Error() string { 220 return string(e) 221 } 222 223 // loadConfig initializes and parses the config using a config file and command 224 // line options. 225 // 226 // The configuration proceeds as follows: 227 // 1. Start with a default config with sane settings 228 // 2. Pre-parse the command line to check for an alternative config file 229 // 3. Load configuration file overwriting defaults with any specified options 230 // 4. Parse CLI options and overwrite/add any specified options 231 // 232 // The above results in daemon functioning properly without any config settings 233 // while still allowing the user to override settings with config files and 234 // command line options. Command line options always take precedence. 235 func loadConfig(appName string) (*config, []string, error) { 236 // Default config. 237 cfg := config{ 238 HomeDir: defaultHomeDir, 239 ConfigFile: defaultConfigFile, 240 DebugLevel: defaultLogLevel, 241 LogDir: defaultLogDir, 242 voteDir: defaultVoteDir, 243 Version: version.Version, 244 WalletCert: defaultWalletCert, 245 ClientCert: defaultClientCert, 246 ClientKey: defaultClientKey, 247 Bunches: defaultBunches, 248 // HoursPrior default is set below 249 } 250 251 // Service options which are only added on Windows. 252 serviceOpts := serviceOptions{} 253 254 // Pre-parse the command line options to see if an alternative config 255 // file or the version flag was specified. Any errors aside from the 256 // help message error can be ignored here since they will be caught by 257 // the final parse below. 258 preCfg := cfg 259 preParser := newConfigParser(&preCfg, &serviceOpts, flags.HelpFlag) 260 _, err := preParser.Parse() 261 if err != nil { 262 var e *flags.Error 263 if errors.As(err, &e) { 264 if e.Type != flags.ErrHelp { 265 fmt.Fprintln(os.Stderr, err) 266 os.Exit(1) 267 } else if e.Type == flags.ErrHelp { 268 fmt.Fprintln(os.Stdout, err) 269 os.Exit(0) 270 } 271 } 272 } 273 274 // Show the version and exit if the version flag was specified. 275 if preCfg.ShowVersion { 276 fmt.Printf("%s version %s (Go version %s %s/%s)\n", appName, 277 cfg.Version, runtime.Version(), runtime.GOOS, 278 runtime.GOARCH) 279 os.Exit(0) 280 } 281 282 // Print available commands if listcommands flag is specified 283 if preCfg.ListCommands { 284 fmt.Fprintln(os.Stderr, listCmdMessage) 285 os.Exit(0) 286 } 287 288 // Perform service command and exit if specified. Invalid service 289 // commands show an appropriate error. Only runs on Windows since 290 // the runServiceCommand function will be nil when not on Windows. 291 if serviceOpts.ServiceCommand != "" && runServiceCommand != nil { 292 err := runServiceCommand(serviceOpts.ServiceCommand) 293 if err != nil { 294 fmt.Fprintln(os.Stderr, err) 295 } 296 os.Exit(0) 297 } 298 299 // Update the home directory for politeavoter if specified. Since the 300 // home directory is updated, other variables need to be updated to 301 // reflect the new changes. 302 if preCfg.HomeDir != "" { 303 cfg.HomeDir = util.CleanAndExpandPath(preCfg.HomeDir) 304 305 if preCfg.ConfigFile == defaultConfigFile { 306 cfg.ConfigFile = filepath.Join(cfg.HomeDir, 307 defaultConfigFilename) 308 } else { 309 cfg.ConfigFile = util.CleanAndExpandPath(preCfg.ConfigFile) 310 } 311 if preCfg.LogDir == defaultLogDir { 312 cfg.LogDir = filepath.Join(cfg.HomeDir, defaultLogDirname) 313 } else { 314 cfg.LogDir = preCfg.LogDir 315 } 316 if preCfg.voteDir == defaultVoteDir { 317 cfg.voteDir = filepath.Join(cfg.HomeDir, defaultVoteDirname) 318 } else { 319 cfg.voteDir = preCfg.voteDir 320 } 321 322 // dcrwallet client key-pair 323 if preCfg.ClientCert == defaultClientCert { 324 cfg.ClientCert = filepath.Join(cfg.HomeDir, clientCertFile) 325 } else { 326 cfg.ClientCert = preCfg.ClientCert 327 } 328 if preCfg.ClientKey == defaultClientKey { 329 cfg.ClientKey = filepath.Join(cfg.HomeDir, clientKeyFile) 330 } else { 331 cfg.ClientKey = preCfg.ClientKey 332 } 333 } 334 335 // Load additional config from file. 336 hd := cfg.HomeDir 337 var configFileError error 338 parser := newConfigParser(&cfg, &serviceOpts, flags.Default) 339 err = flags.NewIniParser(parser).ParseFile(cfg.ConfigFile) 340 if err != nil { 341 var e *os.PathError 342 if !errors.As(err, &e) { 343 err = fmt.Errorf("Error parsing config file: %w\n", err) 344 return nil, nil, err 345 } 346 configFileError = err 347 } 348 349 // Print available commands if listcommands flag is specified 350 if cfg.ListCommands { 351 fmt.Fprintln(os.Stderr, listCmdMessage) 352 os.Exit(0) 353 } 354 355 // See if appdata was overridden 356 if hd != cfg.HomeDir { 357 cfg.LogDir = filepath.Join(cfg.HomeDir, defaultLogDirname) 358 cfg.voteDir = filepath.Join(cfg.HomeDir, defaultVoteDirname) 359 } 360 361 // Parse command line options again to ensure they take precedence. 362 remainingArgs, err := parser.Parse() 363 if err != nil { 364 return nil, nil, err 365 } 366 367 // Create the home directory if it doesn't already exist. 368 funcName := "loadConfig" 369 cfg.HomeDir = util.CleanAndExpandPath(cfg.HomeDir) 370 err = os.MkdirAll(cfg.HomeDir, 0700) 371 if err != nil { 372 // Show a nicer error message if it's because a symlink is 373 // linked to a directory that does not exist (probably because 374 // it's not mounted). 375 var e *os.PathError 376 if errors.As(err, &e) && os.IsExist(err) { 377 if link, lerr := os.Readlink(e.Path); lerr == nil { 378 str := "is symlink %s -> %s mounted?" 379 err = fmt.Errorf(str, e.Path, link) 380 } 381 } 382 383 str := "%s: Failed to create home directory: %w" 384 err := errSuppressUsage(fmt.Sprintf(str, funcName, err)) 385 return nil, nil, err 386 } 387 388 // Create vote directory if it doesn't already exist. 389 cfg.voteDir = util.CleanAndExpandPath(cfg.voteDir) 390 err = os.MkdirAll(cfg.voteDir, 0700) 391 if err != nil { 392 // Show a nicer error message if it's because a symlink is 393 // linked to a directory that does not exist (probably because 394 // it's not mounted). 395 var e *os.PathError 396 if errors.As(err, &e) && os.IsExist(err) { 397 if link, lerr := os.Readlink(e.Path); lerr == nil { 398 str := "is symlink %s -> %s mounted?" 399 err = fmt.Errorf(str, e.Path, link) 400 } 401 } 402 403 str := "%s: Failed to create vote directory: %v" 404 err := errSuppressUsage(fmt.Sprintf(str, funcName, err)) 405 return nil, nil, err 406 } 407 408 // Count number of network flags passed; assign active network params 409 // while we're at it 410 activeNetParams = &mainNetParams 411 if cfg.TestNet { 412 activeNetParams = &testNet3Params 413 } 414 415 // Calculate blocks per day 416 cfg.blocksPerHour = uint64(time.Hour / activeNetParams.TargetTimePerBlock) 417 418 // Determine default connections 419 if cfg.PoliteiaWWW == "" { 420 if activeNetParams.Name == "mainnet" { 421 cfg.PoliteiaWWW = "https://proposals.decred.org/api" 422 } else { 423 cfg.PoliteiaWWW = "https://test-proposals.decred.org/api" 424 } 425 } 426 427 if cfg.WalletHost == "" { 428 if activeNetParams.Name == "mainnet" { 429 cfg.WalletHost = defaultWalletHost + ":" + 430 defaultWalletMainnetPort 431 } else { 432 cfg.WalletHost = defaultWalletHost + ":" + 433 defaultWalletTestnetPort 434 } 435 } 436 // Append the network type to the log directory so it is "namespaced" 437 // per network in the same fashion as the data directory. 438 cfg.LogDir = util.CleanAndExpandPath(cfg.LogDir) 439 cfg.LogDir = filepath.Join(cfg.LogDir, netName(activeNetParams)) 440 441 // Special show command to list supported subsystems and exit. 442 if cfg.DebugLevel == "show" { 443 fmt.Println("Supported subsystems", supportedSubsystems()) 444 os.Exit(0) 445 } 446 447 // Initialize log rotation. After log rotation has been initialized, 448 // the logger variables may be used. 449 initLogRotator(filepath.Join(cfg.LogDir, defaultLogFilename)) 450 451 // Parse, validate, and set debug log level(s). 452 if err := parseAndSetDebugLevels(cfg.DebugLevel); err != nil { 453 err := fmt.Errorf("%s: %v", funcName, err) 454 return nil, nil, err 455 } 456 457 // Validate profile port number 458 if cfg.Profile != "" { 459 profilePort, err := strconv.Atoi(cfg.Profile) 460 if err != nil || profilePort < 1024 || profilePort > 65535 { 461 str := "%s: The profile port must be between 1024 and 65535" 462 err := fmt.Errorf(str, funcName) 463 return nil, nil, err 464 } 465 } 466 467 // Clean cert file paths 468 cfg.WalletCert = util.CleanAndExpandPath(cfg.WalletCert) 469 cfg.ClientCert = util.CleanAndExpandPath(cfg.ClientCert) 470 cfg.ClientKey = util.CleanAndExpandPath(cfg.ClientKey) 471 472 // Warn about missing config file only after all other configuration is 473 // done. This prevents the warning on help messages and invalid 474 // options. Note this should go directly before the return. 475 if configFileError != nil { 476 log.Warnf("%v", configFileError) 477 } 478 479 // Socks proxy 480 cfg.dial = net.Dial 481 if cfg.Proxy != "" { 482 _, _, err := net.SplitHostPort(cfg.Proxy) 483 if err != nil { 484 str := "%s: proxy address '%s' is invalid: %w" 485 err := fmt.Errorf(str, funcName, cfg.Proxy, err) 486 return nil, nil, err 487 } 488 proxy := &socks.Proxy{ 489 Addr: cfg.Proxy, 490 Username: cfg.ProxyUser, 491 Password: cfg.ProxyPass, 492 TorIsolation: true, 493 } 494 cfg.dial = proxy.Dial 495 } 496 497 // VoteDuration can only be set with trickle enable. 498 if cfg.VoteDuration != "" && !cfg.Trickle { 499 return nil, nil, fmt.Errorf("must use --trickle when " + 500 "--voteduration is set") 501 } 502 // Duration of the vote. 503 if cfg.VoteDuration != "" { 504 // Verify we can parse the duration 505 cfg.voteDuration, err = time.ParseDuration(cfg.VoteDuration) 506 if err != nil { 507 return nil, nil, fmt.Errorf("invalid --voteduration %w", err) 508 } 509 } 510 511 // Configure the hours prior setting 512 if cfg.HoursPrior != nil && cfg.VoteDuration != "" { 513 return nil, nil, fmt.Errorf("--hoursprior and " + 514 "--voteduration cannot both be set") 515 } 516 if cfg.HoursPrior == nil { 517 // Hours prior setting was not provided. Use the default. 518 cfg.HoursPrior = &defaultHoursPrior 519 } 520 cfg.hoursPrior = time.Duration(*cfg.HoursPrior) * time.Hour 521 522 // Number of bunches 523 if cfg.Bunches < 1 || cfg.Bunches > 100 { 524 str := "%s: number of bunches must be between 1 and 100" 525 err := fmt.Errorf(str, funcName) 526 return nil, nil, err 527 } 528 529 if !cfg.BypassProxyCheck { 530 if cfg.Trickle && cfg.Proxy == "" { 531 return nil, nil, fmt.Errorf("cannot use --trickle " + 532 "without --proxy") 533 } 534 } 535 536 return &cfg, remainingArgs, nil 537 }