github.com/decred/politeia@v1.4.0/politeiawww/config/config.go (about)

     1  // Copyright (c) 2013-2014 The btcsuite developers
     2  // Copyright (c) 2015-2022 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 config
     7  
     8  import (
     9  	"crypto/x509"
    10  	"encoding/pem"
    11  	"errors"
    12  	"fmt"
    13  	"net"
    14  	"net/mail"
    15  	"net/url"
    16  	"os"
    17  	"path/filepath"
    18  	"runtime"
    19  	"strings"
    20  
    21  	"github.com/decred/dcrd/dcrutil/v3"
    22  	"github.com/decred/politeia/politeiad/api/v1/identity"
    23  	"github.com/decred/politeia/politeiawww/logger"
    24  	"github.com/decred/politeia/util"
    25  	"github.com/decred/politeia/util/version"
    26  	"github.com/jessevdk/go-flags"
    27  )
    28  
    29  // The following defaults are exported so that they can be used by external
    30  // tools such as sysadmin and dev CLI tools.
    31  const (
    32  	// AppName is the politeiawww application name that is used to create the
    33  	// application home directory.
    34  	AppName = "politeiawww"
    35  
    36  	// DefaultMainnetPort is the default port that the HTTPS server will be
    37  	// listening on when politeiawww is configured to use the DCR mainnet.
    38  	DefaultMainnetPort = "4443"
    39  
    40  	// DefaultTestnetPort is the default port that the HTTPS server will be
    41  	// listening on when politeiawww is configured to use the DCR testnet.
    42  	DefaultTestnetPort = "4443"
    43  )
    44  
    45  // The following defaults are exported so that they can be used by external
    46  // tools such as sysadmin and dev CLI tools.
    47  var (
    48  	// DefaultHomeDir is the default path for the  politeiawww application home
    49  	// directory.
    50  	DefaultHomeDir = dcrutil.AppDataDir(AppName, false)
    51  
    52  	// DefaultDataDir is the default path for the politeiawww application data
    53  	// directory. In practice, the data directory is namespaced by network, so
    54  	// the network name must be appended onto this path at runtime in order to
    55  	// get the true data directory.
    56  	DefaultDataDir = filepath.Join(DefaultHomeDir, defaultDataDirname)
    57  
    58  	// DefaultHTTPSCert is the default path for the politeiawww HTTPS
    59  	// certificate.
    60  	DefaultHTTPSCert = filepath.Join(DefaultHomeDir, defaultHTTPSCertFilename)
    61  )
    62  
    63  const (
    64  	// General application settings
    65  	defaultDataDirname    = "data"
    66  	defaultLogDirname     = "logs"
    67  	defaultConfigFilename = "politeiawww.conf"
    68  	defaultLogFilename    = "politeiawww.log"
    69  	defaultLogLevel       = "info"
    70  
    71  	// HTTP server settings
    72  	defaultHTTPSCertFilename = "https.cert"
    73  	defaultHTTPSKeyFilename  = "https.key"
    74  	defaultCookieKeyFilename = "cookie.key"
    75  
    76  	defaultReadTimeout        int64  = 5               // In seconds
    77  	defaultWriteTimeout       int64  = 60              // In seconds
    78  	defaultReqBodySizeLimit   int64  = 3 * 1024 * 1024 // 3 MiB
    79  	defaultWebsocketReadLimit int64  = 4 * 1024 * 1024 // 4 KiB
    80  	defaultPluginBatchLimit   uint32 = 20
    81  
    82  	// politeiad RPC settings
    83  	defaultRPCHost          = "localhost"
    84  	defaultRPCMainnetPort   = "49374"
    85  	defaultRPCTestnetPort   = "59374"
    86  	defaultRPCCertFilename  = "rpc.cert"
    87  	defaultIdentityFilename = "identity.json"
    88  	allowInteractive        = "i-know-this-is-a-bad-idea"
    89  
    90  	// Database settings
    91  	LevelDB     = "leveldb"
    92  	CockroachDB = "cockroachdb"
    93  	MySQL       = "mysql"
    94  
    95  	defaultMySQLDBHost     = "localhost:3306"
    96  	defaultCockroachDBHost = "localhost:26257"
    97  
    98  	// SMTP settings
    99  	defaultMailAddress = "Politeia <noreply@example.org>"
   100  
   101  	// User layer settings
   102  	defaultUserPlugin = ""
   103  	defaultAuthPlugin = ""
   104  
   105  	// Environmental variable config settings
   106  	envDBPass = "DBPASS"
   107  )
   108  
   109  var (
   110  	// General application settings
   111  	defaultConfigFile = filepath.Join(DefaultHomeDir, defaultConfigFilename)
   112  	defaultLogDir     = filepath.Join(DefaultHomeDir, defaultLogDirname)
   113  )
   114  
   115  // Config defines the configuration options for politeiawww.
   116  type Config struct {
   117  	// General application settings
   118  	ShowVersion bool   `short:"V" long:"version" description:"Display version information and exit"`
   119  	HomeDir     string `short:"A" long:"appdata" description:"Path to application home directory"`
   120  	ConfigFile  string `short:"C" long:"configfile" description:"Path to configuration file"`
   121  	DataDir     string `short:"b" long:"datadir" description:"Directory to store data"`
   122  	LogDir      string `long:"logdir" description:"Directory to log output."`
   123  	TestNet     bool   `long:"testnet" description:"Use the test network"`
   124  	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"`
   125  
   126  	// HTTP server settings
   127  	Listeners          []string `long:"listen" description:"Add an interface/port to listen for connections (default all interfaces port: 4443)"`
   128  	HTTPSCert          string   `long:"httpscert" description:"File containing the https certificate file"`
   129  	HTTPSKey           string   `long:"httpskey" description:"File containing the https certificate key"`
   130  	CookieKeyFile      string   `long:"cookiekey" description:"File containing the secret cookies key"`
   131  	ReadTimeout        int64    `long:"readtimeout" description:"Maximum duration in seconds that is spent reading the request headers and body"`
   132  	WriteTimeout       int64    `long:"writetimeout" description:"Maximum duration in seconds that a request connection is kept open"`
   133  	ReqBodySizeLimit   int64    `long:"reqbodysizelimit" description:"Maximum number of bytes allowed in a request body submitted by a client"`
   134  	WebsocketReadLimit int64    `long:"websocketreadlimit" description:"Maximum number of bytes allowed for a message read from a websocket client"`
   135  	PluginBatchLimit   uint32   `long:"pluginbatchlimit" description:"Maximum number of plugins command allowed in a batch request."`
   136  
   137  	// politeiad RPC settings
   138  	RPCHost         string `long:"rpchost" description:"politeiad host <host>:<port>"`
   139  	RPCCert         string `long:"rpccert" description:"File containing the politeiad https certificate file"`
   140  	RPCIdentityFile string `long:"rpcidentityfile" description:"Path to file containing the politeiad identity"`
   141  	RPCUser         string `long:"rpcuser" description:"RPC username for privileged politeaid commands"`
   142  	RPCPass         string `long:"rpcpass" description:"RPC password for privileged politeiad commands"`
   143  	FetchIdentity   bool   `long:"fetchidentity" description:"Fetch the identity from politeiad"`
   144  	Interactive     string `long:"interactive" description:"Set to i-know-this-is-a-bad-idea to turn off interactive mode during --fetchidentity"`
   145  
   146  	// User database settings
   147  	UserDB string `long:"userdb" description:"Database choice for the user database"`
   148  	DBHost string `long:"dbhost" description:"Database ip:port"`
   149  	DBPass string // Provided in env variable "DBPASS"
   150  
   151  	// SMTP settings
   152  	MailHost       string `long:"mailhost" description:"Email server address <host>:<port>"`
   153  	MailCert       string `long:"mailcert" description:"Email server certificate file"`
   154  	MailSkipVerify bool   `long:"mailskipverify" description:"Skip email server TLS verification"`
   155  	MailUser       string `long:"mailuser" description:"Email server username"`
   156  	MailPass       string `long:"mailpass" description:"Email server password"`
   157  	MailAddress    string `long:"mailaddress" description:"Email address for outgoing email in the format: name <address>"`
   158  
   159  	// User layer settings
   160  	DisableUsers bool   `long:"disableusers" description:"Disable the user layer"`
   161  	UserPlugin   string `long:"userplugin" description:"ID of the plugin that manages user accounts"`
   162  	AuthPlugin   string `long:"authplugin" description:"ID of the plugin that handles user authorization"`
   163  
   164  	// Plugin settings
   165  	Plugins        []string `long:"plugin" description:"IDs of all plugins to be registered"`
   166  	PluginSettings []string `long:"pluginsetting" description:"Plugin settings"`
   167  
   168  	// Embedded legacy settings. This will be deleted soon.
   169  	DisableLegacy bool `long:"disablelegacy" description:"Disable legacy routes"`
   170  	LegacyConfig
   171  
   172  	Version     string
   173  	ActiveNet   *ChainParams             // Active DCR network
   174  	Identity    *identity.PublicIdentity // politeiad identity
   175  	SystemCerts *x509.CertPool
   176  }
   177  
   178  // Load initializes and parses the config using a config file and command line
   179  // options.
   180  //
   181  // The configuration proceeds as follows:
   182  //  1. Start with a default config with sane settings
   183  //  2. Pre-parse the command line to check for an alternative config file
   184  //  3. Load configuration file overwriting defaults with any specified options
   185  //  4. Parse CLI options and overwrite/add any specified options
   186  //
   187  // The above results in rpc functioning properly without any config settings
   188  // while still allowing the user to override settings with config files and
   189  // command line options.  Command line options always take precedence.
   190  func Load() (*Config, []string, error) {
   191  	// Setup the default config. Most of settings that contain file
   192  	// paths are not set to default values and handled later on, once
   193  	// the CLI args and config file have been fully parsed so that we
   194  	// don't need to worry about updating the default paths with any
   195  	// changes to the home dir.
   196  	cfg := &Config{
   197  		// General application settings
   198  		ShowVersion: false,
   199  		HomeDir:     DefaultHomeDir,
   200  		ConfigFile:  defaultConfigFile,
   201  		DataDir:     DefaultDataDir,
   202  		LogDir:      defaultLogDir,
   203  		TestNet:     false,
   204  		DebugLevel:  defaultLogLevel,
   205  
   206  		// HTTP server settings
   207  		Listeners:          []string{},
   208  		HTTPSCert:          "",
   209  		HTTPSKey:           "",
   210  		CookieKeyFile:      "",
   211  		ReadTimeout:        defaultReadTimeout,
   212  		WriteTimeout:       defaultWriteTimeout,
   213  		ReqBodySizeLimit:   defaultReqBodySizeLimit,
   214  		WebsocketReadLimit: defaultWebsocketReadLimit,
   215  		PluginBatchLimit:   defaultPluginBatchLimit,
   216  
   217  		// User database settings
   218  		UserDB: LevelDB,
   219  
   220  		// SMTP settings
   221  		MailAddress: defaultMailAddress,
   222  
   223  		// User settings
   224  		UserPlugin: defaultUserPlugin,
   225  		AuthPlugin: defaultAuthPlugin,
   226  
   227  		// Legacy settings. These are deprecated and will be removed soon.
   228  		LegacyConfig: LegacyConfig{
   229  			Mode:                     PiWWWMode,
   230  			PaywallAmount:            defaultPaywallAmount,
   231  			MinConfirmationsRequired: defaultPaywallMinConfirmations,
   232  			VoteDurationMin:          defaultVoteDurationMin,
   233  			VoteDurationMax:          defaultVoteDurationMax,
   234  			MailRateLimit:            defaultMailRateLimit,
   235  		},
   236  
   237  		Version: version.Version,
   238  	}
   239  
   240  	// Service options which are only added on Windows.
   241  	serviceOpts := serviceOptions{}
   242  
   243  	// Pre-parse the command line options to see if an alternative config
   244  	// file or the version flag was specified.  Any errors aside from the
   245  	// help message error can be ignored here since they will be caught by
   246  	// the final parse below.
   247  	var (
   248  		preCfg       = cfg
   249  		preParser    = newConfigParser(preCfg, &serviceOpts, flags.HelpFlag)
   250  		usageMessage = fmt.Sprintf("use %s -h to show usage", AppName)
   251  	)
   252  	_, err := preParser.Parse()
   253  	if err != nil {
   254  		var e *flags.Error
   255  		if errors.As(err, &e) {
   256  			fmt.Fprintln(os.Stderr, err)
   257  			fmt.Fprintln(os.Stderr, usageMessage)
   258  			os.Exit(0)
   259  		}
   260  		return nil, nil, err
   261  	}
   262  
   263  	// Show the version and exit if the version flag was specified.
   264  	if preCfg.ShowVersion {
   265  		fmt.Printf("%s version %s (Go version %s %s/%s)\n", AppName,
   266  			cfg.Version, runtime.Version(), runtime.GOOS,
   267  			runtime.GOARCH)
   268  		os.Exit(0)
   269  	}
   270  
   271  	// Perform service command and exit if specified.  Invalid service
   272  	// commands show an appropriate error.  Only runs on Windows since
   273  	// the runServiceCommand function will be nil when not on Windows.
   274  	if serviceOpts.ServiceCommand != "" && runServiceCommand != nil {
   275  		err := runServiceCommand(serviceOpts.ServiceCommand)
   276  		if err != nil {
   277  			fmt.Fprintln(os.Stderr, err)
   278  		}
   279  		os.Exit(0)
   280  	}
   281  
   282  	// Update the home directory if specified. Since the home directory is
   283  	// updated, other variables need to be updated to reflect the new changes.
   284  	if preCfg.HomeDir != "" {
   285  		cfg.HomeDir, _ = filepath.Abs(preCfg.HomeDir)
   286  
   287  		if preCfg.ConfigFile == defaultConfigFile {
   288  			cfg.ConfigFile = filepath.Join(cfg.HomeDir, defaultConfigFilename)
   289  		} else {
   290  			cfg.ConfigFile = preCfg.ConfigFile
   291  		}
   292  		if preCfg.DataDir == DefaultDataDir {
   293  			cfg.DataDir = filepath.Join(cfg.HomeDir, defaultDataDirname)
   294  		} else {
   295  			cfg.DataDir = preCfg.DataDir
   296  		}
   297  		if preCfg.LogDir == defaultLogDir {
   298  			cfg.LogDir = filepath.Join(cfg.HomeDir, defaultLogDirname)
   299  		} else {
   300  			cfg.LogDir = preCfg.LogDir
   301  		}
   302  	}
   303  
   304  	// Clean and exand the config file path.
   305  	cfg.ConfigFile = util.CleanAndExpandPath(cfg.ConfigFile)
   306  
   307  	// Load additional config from file.
   308  	var configFileError error
   309  	parser := newConfigParser(cfg, &serviceOpts, flags.Default)
   310  	err = flags.NewIniParser(parser).ParseFile(cfg.ConfigFile)
   311  	if err != nil {
   312  		var e *os.PathError
   313  		if !errors.As(err, &e) {
   314  			fmt.Fprintf(os.Stderr, "Error parsing config "+
   315  				"file: %v\n", err)
   316  			fmt.Fprintln(os.Stderr, usageMessage)
   317  			return nil, nil, err
   318  		}
   319  		configFileError = err
   320  	}
   321  
   322  	// Parse command line options again to ensure they take precedence.
   323  	remainingArgs, err := parser.Parse()
   324  	if err != nil {
   325  		return nil, nil, err
   326  	}
   327  
   328  	// Special show command to list supported subsystems and exit.
   329  	if cfg.DebugLevel == "show" {
   330  		fmt.Println("Supported subsystems", logger.SupportedSubsystems())
   331  		os.Exit(0)
   332  	}
   333  
   334  	// Clean and expand all file paths
   335  	cfg.HomeDir = util.CleanAndExpandPath(cfg.HomeDir)
   336  	cfg.ConfigFile = util.CleanAndExpandPath(cfg.ConfigFile)
   337  	cfg.DataDir = util.CleanAndExpandPath(cfg.DataDir)
   338  	cfg.LogDir = util.CleanAndExpandPath(cfg.LogDir)
   339  
   340  	// Create the home directory if it doesn't already exist.
   341  	funcName := "loadConfig"
   342  	err = os.MkdirAll(cfg.HomeDir, 0700)
   343  	if err != nil {
   344  		// Show a nicer error message if it's because a symlink
   345  		// is linked to a directory that does not exist (probably
   346  		// because it's not mounted).
   347  		var e *os.PathError
   348  		if errors.As(err, &e) && os.IsExist(err) {
   349  			if link, lerr := os.Readlink(e.Path); lerr == nil {
   350  				str := "is symlink %s -> %s mounted?"
   351  				err = fmt.Errorf(str, e.Path, link)
   352  			}
   353  		}
   354  
   355  		str := "%s: Failed to create home directory: %v"
   356  		err := fmt.Errorf(str, funcName, err)
   357  		fmt.Fprintln(os.Stderr, err)
   358  		return nil, nil, err
   359  	}
   360  
   361  	// Setup the active network
   362  	cfg.ActiveNet = &MainNetParams
   363  	if cfg.TestNet {
   364  		cfg.ActiveNet = &TestNet3Params
   365  	}
   366  
   367  	// Append the network type to the data and log directories
   368  	// so that they are "namespaced" per network.
   369  	cfg.DataDir = filepath.Join(cfg.DataDir, cfg.ActiveNet.Name)
   370  	cfg.LogDir = filepath.Join(cfg.LogDir, cfg.ActiveNet.Name)
   371  
   372  	// Parse, validate, and set debug log level(s).
   373  	if err := parseAndSetDebugLevels(cfg.DebugLevel); err != nil {
   374  		err := fmt.Errorf("%s: %v", funcName, err.Error())
   375  		fmt.Fprintln(os.Stderr, err)
   376  		fmt.Fprintln(os.Stderr, usageMessage)
   377  		return nil, nil, err
   378  	}
   379  
   380  	// Initialize log rotation. After the log rotation has
   381  	// been initialized, the logger variables may be used.
   382  	logger.InitLogRotator(filepath.Join(cfg.LogDir, defaultLogFilename))
   383  
   384  	// Load the system cert pool
   385  	cfg.SystemCerts, err = x509.SystemCertPool()
   386  	if err != nil {
   387  		return nil, nil, err
   388  	}
   389  
   390  	// Setup the various config settings
   391  	err = setupHTTPServerSettings(cfg)
   392  	if err != nil {
   393  		return nil, nil, err
   394  	}
   395  	err = setupRPCSettings(cfg)
   396  	if err != nil {
   397  		return nil, nil, err
   398  	}
   399  	err = setupUserDBSettings(cfg)
   400  	if err != nil {
   401  		return nil, nil, err
   402  	}
   403  	err = setupMailSettings(cfg)
   404  	if err != nil {
   405  		return nil, nil, err
   406  	}
   407  	err = setupLegacyConfig(cfg)
   408  	if err != nil {
   409  		return nil, nil, err
   410  	}
   411  
   412  	// Warn about missing config file only after all other
   413  	// configuration is done. This prevents the warning on
   414  	// help messages and invalid options. Note this should
   415  	// go directly before the return.
   416  	if configFileError != nil {
   417  		log.Warnf("%v", configFileError)
   418  	}
   419  
   420  	return cfg, remainingArgs, nil
   421  }
   422  
   423  // setupHTTPServerSettings sets up the politeiawww http server config settings.
   424  func setupHTTPServerSettings(cfg *Config) error {
   425  	// Setup default values if none were provided. Only the file path
   426  	// defaults need to be checked. All other defaults should be set
   427  	// on the original config initialization.
   428  	if cfg.HTTPSCert == "" {
   429  		cfg.HTTPSCert = DefaultHTTPSCert
   430  	}
   431  	if cfg.HTTPSKey == "" {
   432  		cfg.HTTPSKey = filepath.Join(cfg.HomeDir, defaultHTTPSKeyFilename)
   433  	}
   434  	if cfg.CookieKeyFile == "" {
   435  		cfg.CookieKeyFile = filepath.Join(cfg.HomeDir, defaultCookieKeyFilename)
   436  	}
   437  
   438  	// Clean file paths
   439  	cfg.HTTPSCert = util.CleanAndExpandPath(cfg.HTTPSCert)
   440  	cfg.HTTPSKey = util.CleanAndExpandPath(cfg.HTTPSKey)
   441  	cfg.CookieKeyFile = util.CleanAndExpandPath(cfg.CookieKeyFile)
   442  
   443  	// Add the default listener if none were specified. The
   444  	// default listener is all addresses on the listen port
   445  	// for the network we are to connect to.
   446  	port := DefaultMainnetPort
   447  	if cfg.TestNet {
   448  		port = DefaultTestnetPort
   449  	}
   450  	if len(cfg.Listeners) == 0 {
   451  		cfg.Listeners = []string{
   452  			net.JoinHostPort("", port),
   453  		}
   454  	}
   455  
   456  	// Add default port to all listener addresses if needed
   457  	// and remove duplicate addresses.
   458  	cfg.Listeners = normalizeAddresses(cfg.Listeners, port)
   459  
   460  	return nil
   461  }
   462  
   463  // setupRPCSettings sets up the politeiad RPC config settings.
   464  func setupRPCSettings(cfg *Config) error {
   465  	// Setup default values if none were provided
   466  	if cfg.RPCCert == "" {
   467  		cfg.RPCCert = filepath.Join(cfg.HomeDir, defaultRPCCertFilename)
   468  	}
   469  	if cfg.RPCIdentityFile == "" {
   470  		cfg.RPCIdentityFile = filepath.Join(cfg.HomeDir, defaultIdentityFilename)
   471  	}
   472  
   473  	// Clean file paths
   474  	cfg.RPCCert = util.CleanAndExpandPath(cfg.RPCCert)
   475  	cfg.RPCIdentityFile = util.CleanAndExpandPath(cfg.RPCIdentityFile)
   476  
   477  	// Setup the RPC host
   478  	if cfg.RPCHost == "" {
   479  		cfg.RPCHost = defaultRPCHost
   480  	}
   481  	port := defaultRPCMainnetPort
   482  	if cfg.TestNet {
   483  		port = defaultRPCTestnetPort
   484  	}
   485  	cfg.RPCHost = util.NormalizeAddress(cfg.RPCHost, port)
   486  	u, err := url.Parse("https://" + cfg.RPCHost)
   487  	if err != nil {
   488  		return fmt.Errorf("parse politeiad RPC host: %v", err)
   489  	}
   490  	cfg.RPCHost = u.String()
   491  
   492  	// Verify remaining RPC settings
   493  	if cfg.RPCUser == "" {
   494  		return fmt.Errorf("politeiad rpc user " +
   495  			"must be provided with --rpcuser")
   496  	}
   497  	if cfg.RPCPass == "" {
   498  		return fmt.Errorf("politeiad rpc pass " +
   499  			"must be provided with --rpcpass")
   500  	}
   501  	if cfg.Interactive != "" && cfg.Interactive != allowInteractive {
   502  		return fmt.Errorf("--interactive flag used incorrectly")
   503  	}
   504  
   505  	// Load the identity politeaid identity from disk
   506  	if cfg.FetchIdentity {
   507  		// Don't try to load the identity from the existing
   508  		// file if the caller is trying to fetch a new one.
   509  		return nil
   510  	}
   511  	if !util.FileExists(cfg.RPCIdentityFile) {
   512  		return fmt.Errorf("identity file not found; you must load the " +
   513  			"identity from politeiad first using the --fetchidentity flag")
   514  	}
   515  	cfg.Identity, err = identity.LoadPublicIdentity(cfg.RPCIdentityFile)
   516  	if err != nil {
   517  		return err
   518  	}
   519  
   520  	log.Infof("Identity loaded from: %v", cfg.RPCIdentityFile)
   521  
   522  	return nil
   523  }
   524  
   525  // setupUserDBSettings sets up the user database config settings.
   526  func setupUserDBSettings(cfg *Config) error {
   527  	// Verify database selection
   528  	switch cfg.UserDB {
   529  	case LevelDB, CockroachDB, MySQL:
   530  		// These are allowed
   531  	default:
   532  		return fmt.Errorf("invalid db selection '%v'",
   533  			cfg.UserDB)
   534  	}
   535  
   536  	// Verify individual database requirements
   537  	switch cfg.UserDB {
   538  	case LevelDB:
   539  		// LevelDB should not have a host
   540  		if cfg.DBHost != "" {
   541  			return fmt.Errorf("dbhost should not be set when using leveldb")
   542  		}
   543  
   544  	case CockroachDB:
   545  		// The CockroachDB option is deprecated. All CockroachDB
   546  		// validation is performed in the legacy config setup.
   547  
   548  	case MySQL:
   549  		// Verify database host
   550  		if cfg.DBHost == "" {
   551  			cfg.DBHost = defaultMySQLDBHost
   552  		}
   553  		_, err := url.Parse(cfg.DBHost)
   554  		if err != nil {
   555  			return fmt.Errorf("invalid dbhost '%v': %v",
   556  				cfg.DBHost, err)
   557  		}
   558  
   559  		// Pull password from env variable
   560  		cfg.DBPass = os.Getenv(envDBPass)
   561  		if cfg.DBPass == "" {
   562  			return fmt.Errorf("dbpass not found; you must provide "+
   563  				"the database password for the politeiawww user in "+
   564  				"the env variable %v", envDBPass)
   565  		}
   566  	}
   567  
   568  	return nil
   569  }
   570  
   571  // setupMailSettings sets up the SMTP mail server config settings.
   572  func setupMailSettings(cfg *Config) error {
   573  	// Clean file paths
   574  	cfg.MailCert = util.CleanAndExpandPath(cfg.MailCert)
   575  
   576  	// Verify the host
   577  	u, err := url.Parse(cfg.MailHost)
   578  	if err != nil {
   579  		return fmt.Errorf("unable to parse mail host: %v", err)
   580  	}
   581  	cfg.MailHost = u.String()
   582  
   583  	// Verify the certificate
   584  	if cfg.MailCert != "" {
   585  		if cfg.MailSkipVerify {
   586  			return fmt.Errorf("cannot set mailskipverify " +
   587  				"and provide a mailcert at the same time")
   588  		}
   589  		if !util.FileExists(cfg.MailCert) {
   590  			return fmt.Errorf("mail cert file '%v' not found",
   591  				cfg.MailCert)
   592  		}
   593  		b, err := os.ReadFile(cfg.MailCert)
   594  		if err != nil {
   595  			return fmt.Errorf("read mail cert: %v", err)
   596  		}
   597  		block, _ := pem.Decode(b)
   598  		cert, err := x509.ParseCertificate(block.Bytes)
   599  		if err != nil {
   600  			return fmt.Errorf("parse mail cert: %v", err)
   601  		}
   602  		cfg.SystemCerts.AddCert(cert)
   603  	}
   604  
   605  	// Verify the provided email address
   606  	a, err := mail.ParseAddress(cfg.MailAddress)
   607  	if err != nil {
   608  		return fmt.Errorf("cannot parse mail address '%v': %v",
   609  			cfg.MailAddress, err)
   610  	}
   611  	cfg.MailAddress = a.String()
   612  
   613  	return nil
   614  }
   615  
   616  // runServiceCommand is only set to a real function on Windows.  It is used
   617  // to parse and execute service commands specified via the -s flag.
   618  var runServiceCommand func(string) error
   619  
   620  // serviceOptions defines the configuration options for the rpc as a service
   621  // on Windows.
   622  type serviceOptions struct {
   623  	ServiceCommand string `short:"s" long:"service" description:"Service command {install, remove, start, stop}"`
   624  }
   625  
   626  // newConfigParser returns a new command line flags parser.
   627  func newConfigParser(cfg *Config, so *serviceOptions, options flags.Options) *flags.Parser {
   628  	parser := flags.NewParser(cfg, options)
   629  	if runtime.GOOS == "windows" {
   630  		parser.AddGroup("Service Options", "Service Options", so)
   631  	}
   632  	return parser
   633  }
   634  
   635  // validLogLevel returns whether or not logLevel is a valid debug log level.
   636  func validLogLevel(logLevel string) bool {
   637  	switch logLevel {
   638  	case "trace":
   639  		fallthrough
   640  	case "debug":
   641  		fallthrough
   642  	case "info":
   643  		fallthrough
   644  	case "warn":
   645  		fallthrough
   646  	case "error":
   647  		fallthrough
   648  	case "critical":
   649  		return true
   650  	}
   651  	return false
   652  }
   653  
   654  // parseAndSetDebugLevels attempts to parse the specified debug level and set
   655  // the levels accordingly.  An appropriate error is returned if anything is
   656  // invalid.
   657  func parseAndSetDebugLevels(debugLevel string) error {
   658  	// When the specified string doesn't have any delimters, treat it as
   659  	// the log level for all subsystems.
   660  	if !strings.Contains(debugLevel, ",") && !strings.Contains(debugLevel, "=") {
   661  		// Validate debug log level.
   662  		if !validLogLevel(debugLevel) {
   663  			str := "The specified debug level [%v] is invalid"
   664  			return fmt.Errorf(str, debugLevel)
   665  		}
   666  
   667  		// Change the logging level for all subsystems.
   668  		logger.SetLogLevels(debugLevel)
   669  
   670  		return nil
   671  	}
   672  
   673  	// Split the specified string into subsystem/level pairs while detecting
   674  	// issues and update the log levels accordingly.
   675  	for _, logLevelPair := range strings.Split(debugLevel, ",") {
   676  		if !strings.Contains(logLevelPair, "=") {
   677  			str := "The specified debug level contains an invalid " +
   678  				"subsystem/level pair [%v]"
   679  			return fmt.Errorf(str, logLevelPair)
   680  		}
   681  
   682  		// Extract the specified subsystem and log level.
   683  		fields := strings.Split(logLevelPair, "=")
   684  		subsysID, logLevel := fields[0], fields[1]
   685  
   686  		// Validate subsystem.
   687  		subsystems := make(map[string]struct{})
   688  		for _, v := range logger.SupportedSubsystems() {
   689  			subsystems[v] = struct{}{}
   690  		}
   691  		if _, exists := subsystems[subsysID]; !exists {
   692  			str := "The specified subsystem [%v] is invalid -- " +
   693  				"supported subsytems %v"
   694  			return fmt.Errorf(str, subsysID, logger.SupportedSubsystems())
   695  		}
   696  
   697  		// Validate log level.
   698  		if !validLogLevel(logLevel) {
   699  			str := "The specified debug level [%v] is invalid"
   700  			return fmt.Errorf(str, logLevel)
   701  		}
   702  
   703  		logger.SetLogLevel(subsysID, logLevel)
   704  	}
   705  
   706  	return nil
   707  }
   708  
   709  // normalizeAddresses returns a new slice with all the passed peer addresses
   710  // normalized with the given default port, and all duplicates removed.
   711  func normalizeAddresses(addrs []string, defaultPort string) []string {
   712  	for i, addr := range addrs {
   713  		addrs[i] = util.NormalizeAddress(addr, defaultPort)
   714  	}
   715  
   716  	return removeDuplicateAddresses(addrs)
   717  }
   718  
   719  // removeDuplicateAddresses returns a new slice with all duplicate entries in
   720  // addrs removed.
   721  func removeDuplicateAddresses(addrs []string) []string {
   722  	result := make([]string, 0, len(addrs))
   723  	seen := map[string]struct{}{}
   724  	for _, val := range addrs {
   725  		if _, ok := seen[val]; !ok {
   726  			result = append(result, val)
   727  			seen[val] = struct{}{}
   728  		}
   729  	}
   730  	return result
   731  }