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  }