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

     1  // Copyright (c) 2017-2021 The Decred developers
     2  // Use of this source code is governed by an ISC
     3  // license that can be found in the LICENSE file.
     4  
     5  package shared
     6  
     7  import (
     8  	"encoding/json"
     9  	"errors"
    10  	"fmt"
    11  	"net/http"
    12  	"net/url"
    13  	"os"
    14  	"path/filepath"
    15  	"runtime"
    16  	"strings"
    17  
    18  	"github.com/decred/dcrd/dcrutil/v3"
    19  	"github.com/decred/politeia/politeiad/api/v1/identity"
    20  	"github.com/decred/politeia/politeiawww/config"
    21  	"github.com/decred/politeia/util"
    22  	"github.com/decred/politeia/util/version"
    23  	flags "github.com/jessevdk/go-flags"
    24  )
    25  
    26  const (
    27  	defaultHost              = "https://127.0.0.1:4443"
    28  	defaultFaucetHost        = "https://faucet.decred.org/requestfaucet"
    29  	defaultWalletHost        = "127.0.0.1"
    30  	defaultWalletTestnetPort = "19111"
    31  
    32  	userFile       = "user.txt"
    33  	csrfFile       = "csrf.txt"
    34  	cookieFile     = "cookies.json"
    35  	identityFile   = "identity.json"
    36  	clientCertFile = "client.pem"
    37  	clientKeyFile  = "client-key.pem"
    38  )
    39  
    40  var (
    41  	defaultHTTPSCert      = config.DefaultHTTPSCert
    42  	dcrwalletHomeDir      = dcrutil.AppDataDir("dcrwallet", false)
    43  	defaultWalletCertFile = filepath.Join(dcrwalletHomeDir, "rpc.cert")
    44  )
    45  
    46  // Config represents the CLI configuration settings.
    47  type Config struct {
    48  	ShowVersion bool   `short:"V" long:"version" description:"Display version information and exit"`
    49  	HomeDir     string `long:"appdata" description:"Path to application home directory"`
    50  	Host        string `long:"host" description:"politeiawww host"`
    51  	HTTPSCert   string `long:"httpscert" description:"politeiawww https cert"`
    52  	SkipVerify  bool   `long:"skipverify" description:"Skip verifying the server's certifcate chain and host name"`
    53  	RawJSON     bool   `short:"j" long:"json" description:"Print raw JSON output"`
    54  	Verbose     bool   `short:"v" long:"verbose" description:"Print verbose output"`
    55  	Silent      bool   `long:"silent" description:"Suppress all output"`
    56  	Timer       bool   `long:"timer" description:"Print command execution time stats"`
    57  
    58  	ClientCert string `long:"clientcert" description:"Path to TLS certificate for client authentication"`
    59  	ClientKey  string `long:"clientkey" description:"Path to TLS client authentication key"`
    60  
    61  	DataDir    string // Application data dir
    62  	Version    string // CLI version
    63  	WalletHost string // Wallet host
    64  	WalletCert string // Wallet GRPC certificate
    65  	FaucetHost string // Testnet faucet host
    66  	CSRF       string // CSRF header token
    67  
    68  	Identity *identity.FullIdentity // User identity
    69  	Cookies  []*http.Cookie         // User cookies
    70  }
    71  
    72  // LoadConfig initializes and parses the config using a config file and command
    73  // line options.
    74  //
    75  // The configuration proceeds as follows:
    76  //  1. Start with a default config with sane settings
    77  //  2. Pre-parse the command line to check for an alternative config file
    78  //  3. Load configuration file overwriting defaults with any specified options
    79  //  4. Parse CLI options and overwrite/add any specified options
    80  //
    81  // The above results in the cli functioning properly without any config
    82  // settings while still allowing the user to override settings with config
    83  // files and command line options. Command line options always take precedence.
    84  func LoadConfig(homeDir, dataDirname, configFilename string) (*Config, error) {
    85  	// Default config
    86  	cfg := Config{
    87  		HomeDir:    homeDir,
    88  		DataDir:    filepath.Join(homeDir, dataDirname),
    89  		Host:       defaultHost,
    90  		HTTPSCert:  defaultHTTPSCert,
    91  		WalletHost: defaultWalletHost + ":" + defaultWalletTestnetPort,
    92  		WalletCert: defaultWalletCertFile,
    93  		FaucetHost: defaultFaucetHost,
    94  		Version:    version.Version,
    95  	}
    96  
    97  	// Pre-parse the command line options to see if an alternative config
    98  	// file was specified.  The help message flag can be ignored since it
    99  	// will be caught when we parse for the command to execute.
   100  	var opts flags.Options = flags.PassDoubleDash | flags.IgnoreUnknown |
   101  		flags.PrintErrors
   102  	parser := flags.NewParser(&cfg, opts)
   103  	_, err := parser.Parse()
   104  	if err != nil {
   105  		return nil, fmt.Errorf("parsing CLI options: %v", err)
   106  	}
   107  
   108  	// Show the version and exit if the version flag was specified.
   109  	appName := filepath.Base(os.Args[0])
   110  	appName = strings.TrimSuffix(appName, filepath.Ext(appName))
   111  	if cfg.ShowVersion {
   112  		fmt.Printf("%s version %s (Go version %s %s/%s)\n", appName,
   113  			cfg.Version, runtime.Version(), runtime.GOOS,
   114  			runtime.GOARCH)
   115  		os.Exit(0)
   116  	}
   117  
   118  	// Update the application home directory if specified
   119  	if cfg.HomeDir != homeDir {
   120  		homeDir := util.CleanAndExpandPath(cfg.HomeDir)
   121  		cfg.HomeDir = homeDir
   122  		cfg.DataDir = filepath.Join(cfg.HomeDir, dataDirname)
   123  	}
   124  
   125  	// Load options from config file.  Ignore errors caused by
   126  	// the config file not existing.
   127  	cfgFile := filepath.Join(cfg.HomeDir, configFilename)
   128  	cfgParser := flags.NewParser(&cfg, flags.Default)
   129  	err = flags.NewIniParser(cfgParser).ParseFile(cfgFile)
   130  	if err != nil {
   131  		var e *os.PathError
   132  		if errors.As(err, &e) {
   133  			// No config file found. Do nothing.
   134  		} else {
   135  			return nil, fmt.Errorf("parsing config file: %v", err)
   136  		}
   137  	}
   138  
   139  	// Parse command line options again to ensure they take
   140  	// precedence
   141  	_, err = parser.Parse()
   142  	if err != nil {
   143  		return nil, fmt.Errorf("parsing CLI options: %v", err)
   144  	}
   145  
   146  	// Create home and data directories if they doesn't already
   147  	// exist
   148  	err = os.MkdirAll(cfg.HomeDir, 0700)
   149  	if err != nil {
   150  		return nil, fmt.Errorf("MkdirAll %v:  %v", cfg.HomeDir, err)
   151  	}
   152  	err = os.MkdirAll(cfg.DataDir, 0700)
   153  	if err != nil {
   154  		return nil, fmt.Errorf("MkdirAll %v:  %v", cfg.DataDir, err)
   155  	}
   156  
   157  	// Validate host
   158  	u, err := url.Parse(cfg.Host)
   159  	if err != nil {
   160  		return nil, fmt.Errorf("parse host: %v", err)
   161  	}
   162  	if u.Scheme != "http" && u.Scheme != "https" {
   163  		return nil, fmt.Errorf("host scheme must be http or https")
   164  	}
   165  
   166  	// Load cookies
   167  	cookies, err := cfg.loadCookies()
   168  	if err != nil {
   169  		return nil, fmt.Errorf("loadCookies: %v", err)
   170  	}
   171  	cfg.Cookies = cookies
   172  
   173  	// Load CSRF tokens
   174  	csrf, err := cfg.loadCSRF()
   175  	if err != nil {
   176  		return nil, fmt.Errorf("loadCSRF: %v", err)
   177  	}
   178  	cfg.CSRF = csrf
   179  
   180  	// Load identity for the logged in user
   181  	username, err := cfg.loadLoggedInUsername()
   182  	if err != nil {
   183  		return nil, fmt.Errorf("load username: %v", err)
   184  	}
   185  	id, err := cfg.LoadIdentity(username)
   186  	if err != nil {
   187  		return nil, fmt.Errorf("load identity: %v", err)
   188  	}
   189  	cfg.Identity = id
   190  
   191  	// Set path for the client key/cert depending on if they are set in options
   192  	cfg.ClientCert = util.CleanAndExpandPath(cfg.ClientCert)
   193  	cfg.ClientKey = util.CleanAndExpandPath(cfg.ClientKey)
   194  	if cfg.ClientCert == "" {
   195  		cfg.ClientCert = filepath.Join(cfg.HomeDir, clientCertFile)
   196  	}
   197  	if cfg.ClientKey == "" {
   198  		cfg.ClientKey = filepath.Join(cfg.HomeDir, clientKeyFile)
   199  	}
   200  
   201  	return &cfg, nil
   202  }
   203  
   204  // hostFilePath returns the host specific file path for the passed in file.
   205  // This means that the hostname is prepended to the filename. cli data is
   206  // segmented by host so that we can interact with multiple hosts
   207  // simultaneously.
   208  func (cfg *Config) hostFilePath(filename string) (string, error) {
   209  	u, err := url.Parse(cfg.Host)
   210  	if err != nil {
   211  		return "", fmt.Errorf("parse host: %v", err)
   212  	}
   213  
   214  	f := fmt.Sprintf("%v_%v", u.Hostname(), filename)
   215  	return filepath.Join(cfg.DataDir, f), nil
   216  }
   217  
   218  func (cfg *Config) loadCookies() ([]*http.Cookie, error) {
   219  	f, err := cfg.hostFilePath(cookieFile)
   220  	if err != nil {
   221  		return nil, fmt.Errorf("hostFilePath: %v", err)
   222  	}
   223  
   224  	if !fileExists(f) {
   225  		// Nothing to load
   226  		return nil, nil
   227  	}
   228  
   229  	b, err := os.ReadFile(f)
   230  	if err != nil {
   231  		return nil, fmt.Errorf("read file %v: %v", f, err)
   232  	}
   233  
   234  	var c []*http.Cookie
   235  	err = json.Unmarshal(b, &c)
   236  	if err != nil {
   237  		return nil, fmt.Errorf("unmarshal cookies: %v", err)
   238  	}
   239  
   240  	return c, nil
   241  }
   242  
   243  // SaveCookies writes the passed in cookies to the host specific cookie file.
   244  func (cfg *Config) SaveCookies(cookies []*http.Cookie) error {
   245  	b, err := json.Marshal(cookies)
   246  	if err != nil {
   247  		return fmt.Errorf("marshal cookies: %v", err)
   248  	}
   249  
   250  	f, err := cfg.hostFilePath(cookieFile)
   251  	if err != nil {
   252  		return fmt.Errorf("hostFilePath: %v", err)
   253  	}
   254  
   255  	err = os.WriteFile(f, b, 0600)
   256  	if err != nil {
   257  		return fmt.Errorf("write file %v: %v", f, err)
   258  	}
   259  
   260  	cfg.Cookies = cookies
   261  	return nil
   262  }
   263  
   264  func (cfg *Config) loadCSRF() (string, error) {
   265  	f, err := cfg.hostFilePath(csrfFile)
   266  	if err != nil {
   267  		return "", fmt.Errorf("hostFilePath: %v", err)
   268  	}
   269  
   270  	if !fileExists(f) {
   271  		// Nothing to load
   272  		return "", nil
   273  	}
   274  
   275  	b, err := os.ReadFile(f)
   276  	if err != nil {
   277  		return "", fmt.Errorf("read file %v: %v", f, err)
   278  	}
   279  
   280  	return string(b), nil
   281  }
   282  
   283  // SaveCSRF writes the passed in CSRF token to the host specific CSRF file.
   284  func (cfg *Config) SaveCSRF(csrf string) error {
   285  	f, err := cfg.hostFilePath(csrfFile)
   286  	if err != nil {
   287  		return fmt.Errorf("hostFilePath: %v", err)
   288  	}
   289  
   290  	err = os.WriteFile(f, []byte(csrf), 0600)
   291  	if err != nil {
   292  		return fmt.Errorf("write file %v: %v", f, err)
   293  	}
   294  
   295  	cfg.CSRF = csrf
   296  	return nil
   297  }
   298  
   299  // identityFilePath returns the file path for a specific user identity.  We
   300  // store identities in a user specific file so that we can keep track of the
   301  // identities of multiple users.
   302  func (cfg *Config) identityFilePath(username string) (string, error) {
   303  	return cfg.hostFilePath(fmt.Sprintf("%v_%v", username, identityFile))
   304  }
   305  
   306  func (cfg *Config) LoadIdentity(username string) (*identity.FullIdentity, error) {
   307  	if username == "" {
   308  		// No logged in user
   309  		return nil, nil
   310  	}
   311  
   312  	f, err := cfg.identityFilePath(username)
   313  	if err != nil {
   314  		return nil, fmt.Errorf("identityFilePath: %v", err)
   315  	}
   316  
   317  	if !fileExists(f) {
   318  		// User identity doesn't exist
   319  		return nil, nil
   320  	}
   321  
   322  	id, err := identity.LoadFullIdentity(f)
   323  	if err != nil {
   324  		return nil, fmt.Errorf("load identity %v: %v", f, err)
   325  	}
   326  
   327  	return id, nil
   328  }
   329  
   330  // SaveIdentity writes the passed in user identity to disk so that it can be
   331  // persisted between commands.  The prepend the hostname and the username onto
   332  // the idenity filename so that we can keep track of the identities for
   333  // multiple users per host.
   334  func (cfg *Config) SaveIdentity(user string, id *identity.FullIdentity) error {
   335  	f, err := cfg.identityFilePath(user)
   336  	if err != nil {
   337  		return fmt.Errorf("identityFilePath: %v", err)
   338  	}
   339  
   340  	err = id.Save(f)
   341  	if err != nil {
   342  		return fmt.Errorf("save idenity to %v: %v", f, err)
   343  	}
   344  
   345  	cfg.Identity = id
   346  	return nil
   347  }
   348  
   349  func (cfg *Config) loadLoggedInUsername() (string, error) {
   350  	f, err := cfg.hostFilePath(userFile)
   351  	if err != nil {
   352  		return "", fmt.Errorf("hostFilePath: %v", err)
   353  	}
   354  
   355  	if !fileExists(f) {
   356  		// Nothing to load
   357  		return "", nil
   358  	}
   359  
   360  	b, err := os.ReadFile(f)
   361  	if err != nil {
   362  		return "", fmt.Errorf("read file %v: %v", f, err)
   363  	}
   364  
   365  	return string(b), nil
   366  }
   367  
   368  // SaveLoggedInUsername saved the passed in username to the on-disk user file.
   369  // We persist the logged in username between commands so that we know which
   370  // identity to load.
   371  func (cfg *Config) SaveLoggedInUsername(username string) error {
   372  	f, err := cfg.hostFilePath(userFile)
   373  	if err != nil {
   374  		return fmt.Errorf("hostFilePath: %v", err)
   375  	}
   376  
   377  	err = os.WriteFile(f, []byte(username), 0600)
   378  	if err != nil {
   379  		return fmt.Errorf("write file %v: %v", f, err)
   380  	}
   381  
   382  	// The config identity is the identity of the logged in
   383  	// user so we need to update the identity when the logged
   384  	// in user changes.
   385  	id, err := cfg.LoadIdentity(username)
   386  	if err != nil {
   387  		return fmt.Errorf("load identity: %v", err)
   388  	}
   389  	cfg.Identity = id
   390  
   391  	return nil
   392  }
   393  
   394  // filesExists reports whether the named file or directory exists.
   395  func fileExists(name string) bool {
   396  	if _, err := os.Stat(name); err != nil {
   397  		if os.IsNotExist(err) {
   398  			return false
   399  		}
   400  	}
   401  
   402  	return true
   403  }