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  }