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