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