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 }