github.com/decred/politeia@v1.4.0/politeiad/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 "encoding/base64" 10 "errors" 11 "fmt" 12 "net" 13 "net/url" 14 "os" 15 "path/filepath" 16 "runtime" 17 "sort" 18 "strconv" 19 "strings" 20 21 "github.com/decred/dcrd/dcrutil/v3" 22 v1 "github.com/decred/dcrtime/api/v1" 23 "github.com/decred/politeia/util" 24 "github.com/decred/politeia/util/version" 25 flags "github.com/jessevdk/go-flags" 26 ) 27 28 const ( 29 defaultConfigFilename = "politeiad.conf" 30 defaultDataDirname = "data" 31 defaultLogLevel = "info" 32 defaultLogDirname = "logs" 33 defaultLogFilename = "politeiad.log" 34 defaultIdentityFilename = "identity.json" 35 36 defaultMainnetPort = "49374" 37 defaultTestnetPort = "59374" 38 39 defaultMainnetDcrdata = "dcrdata.decred.org:443" 40 defaultTestnetDcrdata = "testnet.decred.org:443" 41 42 // Backend options 43 backendGit = "git" 44 backendTstore = "tstore" 45 defaultBackend = backendTstore 46 47 // Tstore default settings 48 defaultDBHost = "localhost:3306" // MySQL default host 49 defaultTlogHost = "localhost:8090" 50 51 // Environment variables 52 envDBPass = "DBPASS" 53 ) 54 55 var ( 56 defaultHomeDir = dcrutil.AppDataDir("politeiad", false) 57 defaultConfigFile = filepath.Join(defaultHomeDir, defaultConfigFilename) 58 defaultDataDir = filepath.Join(defaultHomeDir, defaultDataDirname) 59 defaultHTTPSKeyFile = filepath.Join(defaultHomeDir, "https.key") 60 defaultHTTPSCertFile = filepath.Join(defaultHomeDir, "https.cert") 61 defaultLogDir = filepath.Join(defaultHomeDir, defaultLogDirname) 62 defaultIdentityFile = filepath.Join(defaultHomeDir, defaultIdentityFilename) 63 64 // defaultReadTimeout is the maximum duration in seconds that is spent 65 // reading the request headers and body. 66 defaultReadTimeout int64 = 5 67 68 // defaultWriteTimeout is the maximum duration in seconds that a request 69 // connection is kept open. 70 defaultWriteTimeout int64 = 60 71 72 // defaultReqBodySizeLimit is the maximum number of bytes allowed in a 73 // request body. 74 defaultReqBodySizeLimit int64 = 3 * 1024 * 1024 // 3 MiB 75 ) 76 77 // runServiceCommand is only set to a real function on Windows. It is used 78 // to parse and execute service commands specified via the -s flag. 79 var runServiceCommand func(string) error 80 81 // config defines the configuration options for dcrd. 82 // 83 // See loadConfig for details on the configuration load process. 84 type config struct { 85 HomeDir string `short:"A" long:"appdata" description:"Path to application home directory"` 86 ShowVersion bool `short:"V" long:"version" description:"Display version information and exit"` 87 ConfigFile string `short:"C" long:"configfile" description:"Path to configuration file"` 88 DataDir string `short:"b" long:"datadir" description:"Directory to store data"` 89 LogDir string `long:"logdir" description:"Directory to log output."` 90 TestNet bool `long:"testnet" description:"Use the test network"` 91 SimNet bool `long:"simnet" description:"Use the simulation test network"` 92 Profile string `long:"profile" description:"Enable HTTP profiling on given port -- NOTE port must be between 1024 and 65536"` 93 CPUProfile string `long:"cpuprofile" description:"Write CPU profile to the specified file"` 94 MemProfile string `long:"memprofile" description:"Write mem profile to the specified file"` 95 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"` 96 Listeners []string `long:"listen" description:"Add an interface/port to listen for connections (default all interfaces port: 49152, testnet: 59152)"` 97 Version string 98 HTTPSCert string `long:"httpscert" description:"File containing the https certificate file"` 99 HTTPSKey string `long:"httpskey" description:"File containing the https certificate key"` 100 RPCUser string `long:"rpcuser" description:"RPC user name for privileged commands"` 101 RPCPass string `long:"rpcpass" description:"RPC password for privileged commands"` 102 DcrtimeHost string `long:"dcrtimehost" description:"Dcrtime ip:port"` 103 DcrtimeCert string `long:"dcrtimecert" description:"Dcrtime HTTPS certificate"` 104 Identity string `long:"identity" description:"File containing the politeiad identity file"` 105 Backend string `long:"backend" description:"Backend type"` 106 Fsck bool `long:"fsck" description:"Perform filesystem checks on all record and plugin data"` 107 108 // Web server settings 109 ReadTimeout int64 `long:"readtimeout" description:"Maximum duration in seconds that is spent reading the request headers and body"` 110 WriteTimeout int64 `long:"writetimeout" description:"Maximum duration in seconds that a request connection is kept open"` 111 ReqBodySizeLimit int64 `long:"reqbodysizelimit" description:"Maximum number of bytes allowed for a request body from a http client"` 112 113 // Git backend options 114 GitTrace bool `long:"gittrace" description:"Enable git tracing in logs"` 115 DcrdataHost string `long:"dcrdatahost" description:"Dcrdata ip:port"` 116 117 // Tstore backend options 118 DBHost string `long:"dbhost" description:"Database ip:port"` 119 DBPass string // Provided in env variable "DBPASS" 120 TlogHost string `long:"tloghost" description:"Trillian log ip:port"` 121 122 // Plugin options 123 Plugins []string `long:"plugin" description:"Plugins"` 124 PluginSettings []string `long:"pluginsetting" description:"Plugin settings"` 125 } 126 127 // serviceOptions defines the configuration options for the daemon as a service 128 // on Windows. 129 type serviceOptions struct { 130 ServiceCommand string `short:"s" long:"service" description:"Service command {install, remove, start, stop}"` 131 } 132 133 // validLogLevel returns whether or not logLevel is a valid debug log level. 134 func validLogLevel(logLevel string) bool { 135 switch logLevel { 136 case "trace": 137 fallthrough 138 case "debug": 139 fallthrough 140 case "info": 141 fallthrough 142 case "warn": 143 fallthrough 144 case "error": 145 fallthrough 146 case "critical": 147 return true 148 } 149 return false 150 } 151 152 // supportedSubsystems returns a sorted slice of the supported subsystems for 153 // logging purposes. 154 func supportedSubsystems() []string { 155 // Convert the subsystemLoggers map keys to a slice. 156 subsystems := make([]string, 0, len(subsystemLoggers)) 157 for subsysID := range subsystemLoggers { 158 subsystems = append(subsystems, subsysID) 159 } 160 161 // Sort the subsytems for stable display. 162 sort.Strings(subsystems) 163 return subsystems 164 } 165 166 // parseAndSetDebugLevels attempts to parse the specified debug level and set 167 // the levels accordingly. An appropriate error is returned if anything is 168 // invalid. 169 func parseAndSetDebugLevels(debugLevel string) error { 170 // When the specified string doesn't have any delimters, treat it as 171 // the log level for all subsystems. 172 if !strings.Contains(debugLevel, ",") && !strings.Contains(debugLevel, "=") { 173 // Validate debug log level. 174 if !validLogLevel(debugLevel) { 175 str := "The specified debug level [%v] is invalid" 176 return fmt.Errorf(str, debugLevel) 177 } 178 179 // Change the logging level for all subsystems. 180 setLogLevels(debugLevel) 181 182 return nil 183 } 184 185 // Split the specified string into subsystem/level pairs while detecting 186 // issues and update the log levels accordingly. 187 for _, logLevelPair := range strings.Split(debugLevel, ",") { 188 if !strings.Contains(logLevelPair, "=") { 189 str := "The specified debug level contains an invalid " + 190 "subsystem/level pair [%v]" 191 return fmt.Errorf(str, logLevelPair) 192 } 193 194 // Extract the specified subsystem and log level. 195 fields := strings.Split(logLevelPair, "=") 196 subsysID, logLevel := fields[0], fields[1] 197 198 // Validate subsystem. 199 if _, exists := subsystemLoggers[subsysID]; !exists { 200 str := "The specified subsystem [%v] is invalid -- " + 201 "supported subsytems %v" 202 return fmt.Errorf(str, subsysID, supportedSubsystems()) 203 } 204 205 // Validate log level. 206 if !validLogLevel(logLevel) { 207 str := "The specified debug level [%v] is invalid" 208 return fmt.Errorf(str, logLevel) 209 } 210 211 setLogLevel(subsysID, logLevel) 212 } 213 214 return nil 215 } 216 217 // removeDuplicateAddresses returns a new slice with all duplicate entries in 218 // addrs removed. 219 func removeDuplicateAddresses(addrs []string) []string { 220 result := make([]string, 0, len(addrs)) 221 seen := map[string]struct{}{} 222 for _, val := range addrs { 223 if _, ok := seen[val]; !ok { 224 result = append(result, val) 225 seen[val] = struct{}{} 226 } 227 } 228 return result 229 } 230 231 // normalizeAddresses returns a new slice with all the passed peer addresses 232 // normalized with the given default port, and all duplicates removed. 233 func normalizeAddresses(addrs []string, defaultPort string) []string { 234 for i, addr := range addrs { 235 addrs[i] = util.NormalizeAddress(addr, defaultPort) 236 } 237 238 return removeDuplicateAddresses(addrs) 239 } 240 241 // newConfigParser returns a new command line flags parser. 242 func newConfigParser(cfg *config, so *serviceOptions, options flags.Options) *flags.Parser { 243 parser := flags.NewParser(cfg, options) 244 if runtime.GOOS == "windows" { 245 parser.AddGroup("Service Options", "Service Options", so) 246 } 247 return parser 248 } 249 250 // loadConfig initializes and parses the config using a config file and command 251 // line options. 252 // 253 // The configuration proceeds as follows: 254 // 1. Start with a default config with sane settings 255 // 2. Pre-parse the command line to check for an alternative config file 256 // 3. Load configuration file overwriting defaults with any specified options 257 // 4. Parse CLI options and overwrite/add any specified options 258 // 259 // The above results in daemon functioning properly without any config settings 260 // while still allowing the user to override settings with config files and 261 // command line options. Command line options always take precedence. 262 func loadConfig() (*config, []string, error) { 263 // Default config. 264 cfg := config{ 265 HomeDir: defaultHomeDir, 266 ConfigFile: defaultConfigFile, 267 DebugLevel: defaultLogLevel, 268 DataDir: defaultDataDir, 269 LogDir: defaultLogDir, 270 HTTPSKey: defaultHTTPSKeyFile, 271 HTTPSCert: defaultHTTPSCertFile, 272 Version: version.Version, 273 Backend: defaultBackend, 274 ReadTimeout: defaultReadTimeout, 275 WriteTimeout: defaultWriteTimeout, 276 ReqBodySizeLimit: defaultReqBodySizeLimit, 277 DBHost: defaultDBHost, 278 TlogHost: defaultTlogHost, 279 } 280 281 // Service options which are only added on Windows. 282 serviceOpts := serviceOptions{} 283 284 // Pre-parse the command line options to see if an alternative config 285 // file or the version flag was specified. Any errors aside from the 286 // help message error can be ignored here since they will be caught by 287 // the final parse below. 288 preCfg := cfg 289 preParser := newConfigParser(&preCfg, &serviceOpts, flags.HelpFlag) 290 _, err := preParser.Parse() 291 if err != nil { 292 var e *flags.Error 293 if errors.As(err, &e) && e.Type == flags.ErrHelp { 294 fmt.Fprintln(os.Stderr, err) 295 os.Exit(0) 296 } 297 } 298 299 // Show the version and exit if the version flag was specified. 300 appName := filepath.Base(os.Args[0]) 301 appName = strings.TrimSuffix(appName, filepath.Ext(appName)) 302 usageMessage := fmt.Sprintf("Use %s -h to show usage", appName) 303 if preCfg.ShowVersion { 304 fmt.Printf("%s version %s (Go version %s %s/%s)\n", appName, 305 cfg.Version, runtime.Version(), runtime.GOOS, 306 runtime.GOARCH) 307 os.Exit(0) 308 } 309 310 // Perform service command and exit if specified. Invalid service 311 // commands show an appropriate error. Only runs on Windows since 312 // the runServiceCommand function will be nil when not on Windows. 313 if serviceOpts.ServiceCommand != "" && runServiceCommand != nil { 314 err := runServiceCommand(serviceOpts.ServiceCommand) 315 if err != nil { 316 fmt.Fprintln(os.Stderr, err) 317 } 318 os.Exit(0) 319 } 320 321 // Update the home directory for stakepoold if specified. Since the 322 // home directory is updated, other variables need to be updated to 323 // reflect the new changes. 324 if preCfg.HomeDir != "" { 325 cfg.HomeDir, _ = filepath.Abs(preCfg.HomeDir) 326 327 if preCfg.ConfigFile == defaultConfigFile { 328 cfg.ConfigFile = filepath.Join(cfg.HomeDir, defaultConfigFilename) 329 } else { 330 cfg.ConfigFile = preCfg.ConfigFile 331 } 332 if preCfg.DataDir == defaultDataDir { 333 cfg.DataDir = filepath.Join(cfg.HomeDir, defaultDataDirname) 334 } else { 335 cfg.DataDir = preCfg.DataDir 336 } 337 if preCfg.HTTPSKey == defaultHTTPSKeyFile { 338 cfg.HTTPSKey = filepath.Join(cfg.HomeDir, "https.key") 339 } else { 340 cfg.HTTPSKey = preCfg.HTTPSKey 341 } 342 if preCfg.HTTPSCert == defaultHTTPSCertFile { 343 cfg.HTTPSCert = filepath.Join(cfg.HomeDir, "https.cert") 344 } else { 345 cfg.HTTPSCert = preCfg.HTTPSCert 346 } 347 if preCfg.LogDir == defaultLogDir { 348 cfg.LogDir = filepath.Join(cfg.HomeDir, defaultLogDirname) 349 } else { 350 cfg.LogDir = preCfg.LogDir 351 } 352 } 353 354 // Load additional config from file. 355 var configFileError error 356 parser := newConfigParser(&cfg, &serviceOpts, flags.Default) 357 if !(preCfg.SimNet) || cfg.ConfigFile != defaultConfigFile { 358 err := flags.NewIniParser(parser).ParseFile(cfg.ConfigFile) 359 if err != nil { 360 var e *os.PathError 361 if !errors.As(err, &e) { 362 fmt.Fprintf(os.Stderr, "Error parsing config "+ 363 "file: %v\n", err) 364 fmt.Fprintln(os.Stderr, usageMessage) 365 return nil, nil, err 366 } 367 configFileError = err 368 } 369 } 370 371 // Parse command line options again to ensure they take precedence. 372 remainingArgs, err := parser.Parse() 373 if err != nil { 374 var e *flags.Error 375 if !errors.As(err, &e) || e.Type != flags.ErrHelp { 376 fmt.Fprintln(os.Stderr, usageMessage) 377 } 378 return nil, nil, err 379 } 380 381 // Create the home directory if it doesn't already exist. 382 funcName := "loadConfig" 383 err = os.MkdirAll(defaultHomeDir, 0700) 384 if err != nil { 385 // Show a nicer error message if it's because a symlink is 386 // linked to a directory that does not exist (probably because 387 // it's not mounted). 388 var e *os.PathError 389 if errors.As(err, &e) && os.IsExist(err) { 390 if link, lerr := os.Readlink(e.Path); lerr == nil { 391 str := "is symlink %s -> %s mounted?" 392 err = fmt.Errorf(str, e.Path, link) 393 } 394 } 395 396 str := "%s: Failed to create home directory: %v" 397 err := fmt.Errorf(str, funcName, err) 398 fmt.Fprintln(os.Stderr, err) 399 return nil, nil, err 400 } 401 402 // Multiple networks can't be selected simultaneously. 403 numNets := 0 404 405 // Count number of network flags passed; assign active network params 406 // while we're at it 407 port := defaultMainnetPort 408 activeNetParams = &mainNetParams 409 if cfg.TestNet { 410 numNets++ 411 activeNetParams = &testNet3Params 412 port = defaultTestnetPort 413 } 414 if cfg.SimNet { 415 numNets++ 416 // Also disable dns seeding on the simulation test network. 417 activeNetParams = &simNetParams 418 } 419 if numNets > 1 { 420 str := "%s: The testnet and simnet params can't be " + 421 "used together -- choose one of the three" 422 err := fmt.Errorf(str, funcName) 423 fmt.Fprintln(os.Stderr, err) 424 fmt.Fprintln(os.Stderr, usageMessage) 425 return nil, nil, err 426 } 427 428 // Append the network type to the data directory so it is "namespaced" 429 // per network. In addition to the block database, there are other 430 // pieces of data that are saved to disk such as address manager state. 431 // All data is specific to a network, so namespacing the data directory 432 // means each individual piece of serialized data does not have to 433 // worry about changing names per network and such. 434 cfg.DataDir = util.CleanAndExpandPath(cfg.DataDir) 435 cfg.DataDir = filepath.Join(cfg.DataDir, netName(activeNetParams)) 436 437 // Append the network type to the log directory so it is "namespaced" 438 // per network in the same fashion as the data directory. 439 cfg.LogDir = util.CleanAndExpandPath(cfg.LogDir) 440 cfg.LogDir = filepath.Join(cfg.LogDir, netName(activeNetParams)) 441 442 cfg.HTTPSKey = util.CleanAndExpandPath(cfg.HTTPSKey) 443 cfg.HTTPSCert = util.CleanAndExpandPath(cfg.HTTPSCert) 444 445 // Special show command to list supported subsystems and exit. 446 if cfg.DebugLevel == "show" { 447 fmt.Println("Supported subsystems", supportedSubsystems()) 448 os.Exit(0) 449 } 450 451 // Initialize log rotation. After log rotation has been initialized, 452 // the logger variables may be used. 453 initLogRotator(filepath.Join(cfg.LogDir, defaultLogFilename)) 454 455 // Parse, validate, and set debug log level(s). 456 if err := parseAndSetDebugLevels(cfg.DebugLevel); err != nil { 457 err := fmt.Errorf("%s: %v", funcName, err.Error()) 458 fmt.Fprintln(os.Stderr, err) 459 fmt.Fprintln(os.Stderr, usageMessage) 460 return nil, nil, err 461 } 462 463 // Validate profile port number 464 if cfg.Profile != "" { 465 profilePort, err := strconv.Atoi(cfg.Profile) 466 if err != nil || profilePort < 1024 || profilePort > 65535 { 467 str := "%s: The profile port must be between 1024 and 65535" 468 err := fmt.Errorf(str, funcName) 469 fmt.Fprintln(os.Stderr, err) 470 fmt.Fprintln(os.Stderr, usageMessage) 471 return nil, nil, err 472 } 473 } 474 475 // Add the default listener if none were specified. The default 476 // listener is all addresses on the listen port for the network 477 // we are to connect to. 478 if len(cfg.Listeners) == 0 { 479 cfg.Listeners = []string{ 480 net.JoinHostPort("", port), 481 } 482 } 483 484 // Add default port to all listener addresses if needed and remove 485 // duplicate addresses. 486 cfg.Listeners = normalizeAddresses(cfg.Listeners, port) 487 488 if len(cfg.DcrdataHost) == 0 { 489 if cfg.TestNet { 490 cfg.DcrdataHost = defaultTestnetDcrdata 491 } else { 492 cfg.DcrdataHost = defaultMainnetDcrdata 493 } 494 } 495 cfg.DcrdataHost = "https://" + cfg.DcrdataHost 496 497 if cfg.TestNet { 498 var timeHost string 499 if len(cfg.DcrtimeHost) == 0 { 500 timeHost = v1.DefaultTestnetTimeHost 501 } else { 502 timeHost = cfg.DcrtimeHost 503 } 504 cfg.DcrtimeHost = util.NormalizeAddress(timeHost, 505 v1.DefaultTestnetTimePort) 506 } else { 507 var timeHost string 508 if len(cfg.DcrtimeHost) == 0 { 509 timeHost = v1.DefaultMainnetTimeHost 510 } else { 511 timeHost = cfg.DcrtimeHost 512 } 513 cfg.DcrtimeHost = util.NormalizeAddress(timeHost, 514 v1.DefaultMainnetTimePort) 515 } 516 cfg.DcrtimeHost = "https://" + cfg.DcrtimeHost 517 518 if len(cfg.DcrtimeCert) != 0 && !util.FileExists(cfg.DcrtimeCert) { 519 cfg.DcrtimeCert = util.CleanAndExpandPath(cfg.DcrtimeCert) 520 path := filepath.Join(cfg.HomeDir, cfg.DcrtimeCert) 521 if !util.FileExists(path) { 522 str := "%s: dcrtimecert " + cfg.DcrtimeCert + " and " + 523 path + " don't exist" 524 err := fmt.Errorf(str, funcName) 525 fmt.Fprintln(os.Stderr, err) 526 return nil, nil, err 527 } 528 529 cfg.DcrtimeCert = path 530 } 531 532 if cfg.Identity == "" { 533 cfg.Identity = defaultIdentityFile 534 } 535 cfg.Identity = util.CleanAndExpandPath(cfg.Identity) 536 537 // Set random username and password when not specified 538 if cfg.RPCUser == "" { 539 name, err := util.Random(32) 540 if err != nil { 541 return nil, nil, err 542 } 543 cfg.RPCUser = base64.StdEncoding.EncodeToString(name) 544 log.Warnf("RPC user name not set, using random value") 545 } 546 if cfg.RPCPass == "" { 547 pass, err := util.Random(32) 548 if err != nil { 549 return nil, nil, err 550 } 551 cfg.RPCPass = base64.StdEncoding.EncodeToString(pass) 552 log.Warnf("RPC password not set, using random value") 553 } 554 555 // Verify backend specific settings 556 switch cfg.Backend { 557 case backendGit: 558 // Nothing to do 559 case backendTstore: 560 err = verifyTstoreSettings(&cfg) 561 if err != nil { 562 return nil, nil, err 563 } 564 default: 565 return nil, nil, fmt.Errorf("invalid backend type '%v'", cfg.Backend) 566 } 567 568 // Warn about missing config file only after all other configuration is 569 // done. This prevents the warning on help messages and invalid 570 // options. Note this should go directly before the return. 571 if configFileError != nil { 572 log.Warnf("%v", configFileError) 573 } 574 575 return &cfg, remainingArgs, nil 576 } 577 578 // verifyTstoreSettings verifies the config settings that are specific to the 579 // tstore backend. 580 func verifyTstoreSettings(cfg *config) error { 581 // Parse the database password. It is provided in an env variable. 582 cfg.DBPass = os.Getenv(envDBPass) 583 if cfg.DBPass == "" { 584 return fmt.Errorf("dbpass not found; you must provide the " + 585 "database password for the politeiad user in the env " + 586 "variable DBPASS") 587 } 588 589 // Verify tlog options 590 _, err := url.Parse(cfg.TlogHost) 591 if err != nil { 592 return fmt.Errorf("invalid tlog host '%v': %v", cfg.TlogHost, err) 593 } 594 595 return nil 596 }