github.com/lbryio/lbcd@v0.22.119/cmd/lbcctl/config.go (about) 1 // Copyright (c) 2013-2015 The btcsuite 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 main 6 7 import ( 8 "fmt" 9 "io/ioutil" 10 "net" 11 "os" 12 "path/filepath" 13 "regexp" 14 "strings" 15 16 flags "github.com/jessevdk/go-flags" 17 "github.com/lbryio/lbcd/btcjson" 18 "github.com/lbryio/lbcd/chaincfg" 19 "github.com/lbryio/lbcd/version" 20 btcutil "github.com/lbryio/lbcutil" 21 ) 22 23 const ( 24 // unusableFlags are the command usage flags which this utility are not 25 // able to use. In particular it doesn't support websockets and 26 // consequently notifications. 27 unusableFlags = btcjson.UFWebsocketOnly | btcjson.UFNotification 28 ) 29 30 var ( 31 btcdHomeDir = btcutil.AppDataDir("lbcd", false) 32 btcctlHomeDir = btcutil.AppDataDir("lbcctl", false) 33 btcwalletHomeDir = btcutil.AppDataDir("lbcwallet", false) 34 defaultConfigFile = filepath.Join(btcctlHomeDir, "lbcctl.conf") 35 defaultRPCServer = "localhost" 36 defaultRPCCertFile = filepath.Join(btcdHomeDir, "rpc.cert") 37 defaultWalletCertFile = filepath.Join(btcwalletHomeDir, "rpc.cert") 38 ) 39 40 // listCommands categorizes and lists all of the usable commands along with 41 // their one-line usage. 42 func listCommands() { 43 const ( 44 categoryChain uint8 = iota 45 categoryWallet 46 numCategories 47 ) 48 49 // Get a list of registered commands and categorize and filter them. 50 cmdMethods := btcjson.RegisteredCmdMethods() 51 categorized := make([][]string, numCategories) 52 for _, method := range cmdMethods { 53 flags, err := btcjson.MethodUsageFlags(method) 54 if err != nil { 55 // This should never happen since the method was just 56 // returned from the package, but be safe. 57 continue 58 } 59 60 // Skip the commands that aren't usable from this utility. 61 if flags&unusableFlags != 0 { 62 continue 63 } 64 65 usage, err := btcjson.MethodUsageText(method) 66 if err != nil { 67 // This should never happen since the method was just 68 // returned from the package, but be safe. 69 continue 70 } 71 72 // Categorize the command based on the usage flags. 73 category := categoryChain 74 if flags&btcjson.UFWalletOnly != 0 { 75 category = categoryWallet 76 } 77 categorized[category] = append(categorized[category], usage) 78 } 79 80 // Display the command according to their categories. 81 categoryTitles := make([]string, numCategories) 82 categoryTitles[categoryChain] = "Chain Server Commands:" 83 categoryTitles[categoryWallet] = "Wallet Server Commands (--wallet):" 84 for category := uint8(0); category < numCategories; category++ { 85 fmt.Println(categoryTitles[category]) 86 for _, usage := range categorized[category] { 87 fmt.Println(usage) 88 } 89 fmt.Println() 90 } 91 } 92 93 // config defines the configuration options for btcctl. 94 // 95 // See loadConfig for details on the configuration load process. 96 type config struct { 97 ConfigFile string `short:"C" long:"configfile" description:"Path to configuration file"` 98 ListCommands bool `short:"l" long:"listcommands" description:"List all of the supported commands and exit"` 99 NoTLS bool `long:"notls" description:"Disable TLS"` 100 TLSSkipVerify bool `long:"skipverify" description:"Do not verify tls certificates (not recommended!)"` 101 Proxy string `long:"proxy" description:"Connect via SOCKS5 proxy (eg. 127.0.0.1:9050)"` 102 ProxyPass string `long:"proxypass" default-mask:"-" description:"Password for proxy server"` 103 ProxyUser string `long:"proxyuser" description:"Username for proxy server"` 104 RPCCert string `short:"c" long:"rpccert" description:"RPC server certificate chain for validation"` 105 RPCPassword string `short:"P" long:"rpcpass" default-mask:"-" description:"RPC password"` 106 RPCServer string `short:"s" long:"rpcserver" description:"RPC server to connect to"` 107 RPCUser string `short:"u" long:"rpcuser" description:"RPC username"` 108 TestNet3 bool `long:"testnet" description:"Connect to testnet (default RPC server: localhost:19245)"` 109 RegressionTest bool `long:"regtest" description:"Connect to the regression test network (default RPC server: localhost:29245)"` 110 SimNet bool `long:"simnet" description:"Connect to the simulation test network (default RPC server: localhost:39245)"` 111 SigNet bool `long:"signet" description:"Connect to signet (default RPC server: localhost:49245)"` 112 Wallet bool `long:"wallet" description:"Connect to wallet RPC server instead (default: localhost:9244, testnet: localhost:19244, regtest: localhost:29244)"` 113 ShowVersion bool `short:"V" long:"version" description:"Display version information and exit"` 114 Timed bool `short:"t" long:"timed" description:"Display RPC response time"` 115 Quiet bool `short:"q" long:"quiet" description:"Do not output results to stdout"` 116 } 117 118 // normalizeAddress returns addr with the passed default port appended if 119 // there is not already a port specified. 120 func normalizeAddress(addr string, chain *chaincfg.Params, useWallet bool) (string, error) { 121 _, _, err := net.SplitHostPort(addr) 122 if err != nil { 123 var defaultPort string 124 switch chain { 125 case &chaincfg.TestNet3Params: 126 if useWallet { 127 defaultPort = "19244" 128 } else { 129 defaultPort = "19245" 130 } 131 case &chaincfg.RegressionNetParams: 132 if useWallet { 133 defaultPort = "29244" 134 } else { 135 defaultPort = "29245" 136 } 137 case &chaincfg.SimNetParams: 138 if useWallet { 139 defaultPort = "39244" 140 } else { 141 defaultPort = "39245" 142 } 143 case &chaincfg.SigNetParams: 144 if useWallet { 145 defaultPort = "49244" 146 } else { 147 defaultPort = "49245" 148 } 149 default: 150 if useWallet { 151 defaultPort = "9244" 152 } else { 153 defaultPort = "9245" 154 } 155 } 156 157 return net.JoinHostPort(addr, defaultPort), nil 158 } 159 return addr, nil 160 } 161 162 // cleanAndExpandPath expands environement variables and leading ~ in the 163 // passed path, cleans the result, and returns it. 164 func cleanAndExpandPath(path string) string { 165 // Expand initial ~ to OS specific home directory. 166 if strings.HasPrefix(path, "~") { 167 homeDir := filepath.Dir(btcctlHomeDir) 168 path = strings.Replace(path, "~", homeDir, 1) 169 } 170 171 // NOTE: The os.ExpandEnv doesn't work with Windows-style %VARIABLE%, 172 // but they variables can still be expanded via POSIX-style $VARIABLE. 173 return filepath.Clean(os.ExpandEnv(path)) 174 } 175 176 // loadConfig initializes and parses the config using a config file and command 177 // line options. 178 // 179 // The configuration proceeds as follows: 180 // 1. Start with a default config with sane settings 181 // 2. Pre-parse the command line to check for an alternative config file 182 // 3. Load configuration file overwriting defaults with any specified options 183 // 4. Parse CLI options and overwrite/add any specified options 184 // 185 // The above results in functioning properly without any config settings 186 // while still allowing the user to override settings with config files and 187 // command line options. Command line options always take precedence. 188 func loadConfig() (*config, []string, error) { 189 // Default config. 190 cfg := config{ 191 ConfigFile: defaultConfigFile, 192 RPCServer: defaultRPCServer, 193 RPCCert: defaultRPCCertFile, 194 } 195 196 // Pre-parse the command line options to see if an alternative config 197 // file, the version flag, or the list commands flag was specified. Any 198 // errors aside from the help message error can be ignored here since 199 // they will be caught by the final parse below. 200 preCfg := cfg 201 preParser := flags.NewParser(&preCfg, flags.HelpFlag) 202 _, err := preParser.Parse() 203 if err != nil { 204 if e, ok := err.(*flags.Error); ok && e.Type == flags.ErrHelp { 205 fmt.Fprintln(os.Stderr, err) 206 fmt.Fprintln(os.Stderr, "") 207 fmt.Fprintln(os.Stderr, "The special parameter `-` "+ 208 "indicates that a parameter should be read "+ 209 "from the\nnext unread line from standard "+ 210 "input.") 211 return nil, nil, err 212 } 213 } 214 215 // Show the version and exit if the version flag was specified. 216 appName := filepath.Base(os.Args[0]) 217 appName = strings.TrimSuffix(appName, filepath.Ext(appName)) 218 usageMessage := fmt.Sprintf("Use %s -h to show options", appName) 219 if preCfg.ShowVersion { 220 fmt.Println(appName, "version", version.Full()) 221 os.Exit(0) 222 } 223 224 // Show the available commands and exit if the associated flag was 225 // specified. 226 if preCfg.ListCommands { 227 listCommands() 228 os.Exit(0) 229 } 230 231 if _, err := os.Stat(preCfg.ConfigFile); os.IsNotExist(err) { 232 // Use config file for RPC server to create default btcctl config 233 var serverConfigPath string 234 if preCfg.Wallet { 235 serverConfigPath = filepath.Join(btcwalletHomeDir, "lbcwallet.conf") 236 } else { 237 serverConfigPath = filepath.Join(btcdHomeDir, "lbcd.conf") 238 } 239 240 err := createDefaultConfigFile(preCfg.ConfigFile, serverConfigPath) 241 if err != nil { 242 fmt.Fprintf(os.Stderr, "Error creating a default config file: %v\n", err) 243 } 244 } 245 246 // Load additional config from file. 247 parser := flags.NewParser(&cfg, flags.Default) 248 err = flags.NewIniParser(parser).ParseFile(preCfg.ConfigFile) 249 if err != nil { 250 if _, ok := err.(*os.PathError); !ok { 251 fmt.Fprintf(os.Stderr, "Error parsing config file: %v\n", 252 err) 253 fmt.Fprintln(os.Stderr, usageMessage) 254 return nil, nil, err 255 } 256 } 257 258 // Parse command line options again to ensure they take precedence. 259 remainingArgs, err := parser.Parse() 260 if err != nil { 261 if e, ok := err.(*flags.Error); !ok || e.Type != flags.ErrHelp { 262 fmt.Fprintln(os.Stderr, usageMessage) 263 } 264 return nil, nil, err 265 } 266 267 // default network is mainnet 268 network := &chaincfg.MainNetParams 269 270 // Multiple networks can't be selected simultaneously. 271 numNets := 0 272 if cfg.TestNet3 { 273 numNets++ 274 network = &chaincfg.TestNet3Params 275 } 276 if cfg.SimNet { 277 numNets++ 278 network = &chaincfg.SimNetParams 279 } 280 if cfg.RegressionTest { 281 numNets++ 282 network = &chaincfg.RegressionNetParams 283 } 284 if cfg.SigNet { 285 numNets++ 286 network = &chaincfg.SigNetParams 287 } 288 289 if numNets > 1 { 290 str := "%s: Multiple network params can't be used " + 291 "together -- choose one" 292 err := fmt.Errorf(str, "loadConfig") 293 fmt.Fprintln(os.Stderr, err) 294 return nil, nil, err 295 } 296 297 // Override the RPC certificate if the --wallet flag was specified and 298 // the user did not specify one. 299 if cfg.Wallet && cfg.RPCCert == defaultRPCCertFile { 300 cfg.RPCCert = defaultWalletCertFile 301 } 302 303 // Handle environment variable expansion in the RPC certificate path. 304 cfg.RPCCert = cleanAndExpandPath(cfg.RPCCert) 305 306 // Add default port to RPC server based on --testnet and --wallet flags 307 // if needed. 308 cfg.RPCServer, err = normalizeAddress(cfg.RPCServer, network, cfg.Wallet) 309 if err != nil { 310 return nil, nil, err 311 } 312 313 return &cfg, remainingArgs, nil 314 } 315 316 // createDefaultConfig creates a basic config file at the given destination path. 317 // For this it tries to read the config file for the RPC server (either btcd or 318 // btcwallet), and extract the RPC user and password from it. 319 func createDefaultConfigFile(destinationPath, serverConfigPath string) error { 320 // Read the RPC server config 321 serverConfigFile, err := os.Open(serverConfigPath) 322 if err != nil { 323 return err 324 } 325 defer serverConfigFile.Close() 326 content, err := ioutil.ReadAll(serverConfigFile) 327 if err != nil { 328 return err 329 } 330 331 // Extract the rpcuser 332 rpcUserRegexp := regexp.MustCompile(`(?m)^\s*rpcuser=([^\s]+)`) 333 userSubmatches := rpcUserRegexp.FindSubmatch(content) 334 if userSubmatches == nil { 335 // No user found, nothing to do 336 return nil 337 } 338 339 // Extract the rpcpass 340 rpcPassRegexp := regexp.MustCompile(`(?m)^\s*rpcpass=([^\s]+)`) 341 passSubmatches := rpcPassRegexp.FindSubmatch(content) 342 if passSubmatches == nil { 343 // No password found, nothing to do 344 return nil 345 } 346 347 // Extract the notls 348 noTLSRegexp := regexp.MustCompile(`(?m)^\s*notls=(0|1)(?:\s|$)`) 349 noTLSSubmatches := noTLSRegexp.FindSubmatch(content) 350 351 // Create the destination directory if it does not exists 352 err = os.MkdirAll(filepath.Dir(destinationPath), 0700) 353 if err != nil { 354 return err 355 } 356 357 // Create the destination file and write the rpcuser and rpcpass to it 358 dest, err := os.OpenFile(destinationPath, 359 os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600) 360 if err != nil { 361 return err 362 } 363 defer dest.Close() 364 365 destString := fmt.Sprintf("rpcuser=%s\nrpcpass=%s\n", 366 string(userSubmatches[1]), string(passSubmatches[1])) 367 if noTLSSubmatches != nil { 368 destString += fmt.Sprintf("notls=%s\n", noTLSSubmatches[1]) 369 } 370 371 dest.WriteString(destString) 372 373 return nil 374 }