Rewrite btcctl to use the new features of btcjson.
This commit contains what is essentially a complete rewrite of the btcctl utility to make use of the new features provided by the latest version btcjson and improve several things along the way. The following summarizes the changes: - The supported commands and handling now come directly from btcjson, so it is no longer necessary to manually add new commands. Once a command has been registered with btcjson, it will automatically become usable by btcctl complete with full error handling (once it is re-compiled of course) - Rather than dumping the entire list of commands on every error, the user now must specifically request the list of command via the -l option - The list of commands is now categorized by chain and wallet and alphabetized - The help flag now only shows the help options instead of also dumping all of the commands - The error display on valid commands with invalid parameters has been greatly improved to show the specific parameter number, reason, and error code - When a valid command is specified with invalid parameter, only the usage for that specific command is shown now - It is now possible to use a SOCKS5 proxy for connection - The output of commands has been improved in the following ways: - Strings on commands such as getbestblockhash no longer have quotes wrapped around them - Fields that are integers no longer show in scientific notation when they are large (timestamps for example) This closes #305 as a side effect.
This commit is contained in:
parent
c0428f6f9f
commit
5a800b9580
3 changed files with 346 additions and 1076 deletions
1183
cmd/btcctl/btcctl.go
1183
cmd/btcctl/btcctl.go
File diff suppressed because it is too large
Load diff
|
@ -1,3 +1,7 @@
|
|||
// Copyright (c) 2013-2015 Conformal Systems LLC.
|
||||
// Use of this source code is governed by an ISC
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
|
@ -7,10 +11,18 @@ import (
|
|||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/btcsuite/btcd/btcjson/v2/btcjson"
|
||||
"github.com/btcsuite/btcutil"
|
||||
flags "github.com/btcsuite/go-flags"
|
||||
)
|
||||
|
||||
const (
|
||||
// unusableFlags are the command usage flags which this utility are not
|
||||
// able to use. In particular it doesn't support websockets and
|
||||
// consequently notifications.
|
||||
unusableFlags = btcjson.UFWebsocketOnly | btcjson.UFNotification
|
||||
)
|
||||
|
||||
var (
|
||||
btcdHomeDir = btcutil.AppDataDir("btcd", false)
|
||||
btcctlHomeDir = btcutil.AppDataDir("btcctl", false)
|
||||
|
@ -21,17 +33,74 @@ var (
|
|||
defaultWalletCertFile = filepath.Join(btcwalletHomeDir, "rpc.cert")
|
||||
)
|
||||
|
||||
// listCommands categorizes and lists all of the usable commands along with
|
||||
// their one-line usage.
|
||||
func listCommands() {
|
||||
const (
|
||||
categoryChain uint8 = iota
|
||||
categoryWallet
|
||||
numCategories
|
||||
)
|
||||
|
||||
// Get a list of registered commands and categorize and filter them.
|
||||
cmdMethods := btcjson.RegisteredCmdMethods()
|
||||
categorized := make([][]string, numCategories)
|
||||
for _, method := range cmdMethods {
|
||||
flags, err := btcjson.MethodUsageFlags(method)
|
||||
if err != nil {
|
||||
// This should never happen since the method was just
|
||||
// returned from the package, but be safe.
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip the commands that aren't usable from this utility.
|
||||
if flags&unusableFlags != 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
usage, err := btcjson.MethodUsageText(method)
|
||||
if err != nil {
|
||||
// This should never happen since the method was just
|
||||
// returned from the package, but be safe.
|
||||
continue
|
||||
}
|
||||
|
||||
// Categorize the command based on the usage flags.
|
||||
category := categoryChain
|
||||
if flags&btcjson.UFWalletOnly != 0 {
|
||||
category = categoryWallet
|
||||
}
|
||||
categorized[category] = append(categorized[category], usage)
|
||||
}
|
||||
|
||||
// Display the command according to their categories.
|
||||
categoryTitles := make([]string, numCategories)
|
||||
categoryTitles[categoryChain] = "Chain Server Commands:"
|
||||
categoryTitles[categoryWallet] = "Wallet Server Commands (--wallet):"
|
||||
for category := uint8(0); category < numCategories; category++ {
|
||||
fmt.Println(categoryTitles[category])
|
||||
for _, usage := range categorized[category] {
|
||||
fmt.Println(usage)
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
}
|
||||
|
||||
// config defines the configuration options for btcctl.
|
||||
//
|
||||
// See loadConfig for details on the configuration load process.
|
||||
type config struct {
|
||||
ShowVersion bool `short:"V" long:"version" description:"Display version information and exit"`
|
||||
ListCommands bool `short:"l" long:"listcommands" description:"List all of the supported commands and exit"`
|
||||
ConfigFile string `short:"C" long:"configfile" description:"Path to configuration file"`
|
||||
RPCUser string `short:"u" long:"rpcuser" description:"RPC username"`
|
||||
RPCPassword string `short:"P" long:"rpcpass" default-mask:"-" description:"RPC password"`
|
||||
RPCServer string `short:"s" long:"rpcserver" description:"RPC server to connect to"`
|
||||
RPCCert string `short:"c" long:"rpccert" description:"RPC server certificate chain for validation"`
|
||||
NoTLS bool `long:"notls" description:"Disable TLS"`
|
||||
Proxy string `long:"proxy" description:"Connect via SOCKS5 proxy (eg. 127.0.0.1:9050)"`
|
||||
ProxyUser string `long:"proxyuser" description:"Username for proxy server"`
|
||||
ProxyPass string `long:"proxypass" default-mask:"-" description:"Password for proxy server"`
|
||||
TestNet3 bool `long:"testnet" description:"Connect to testnet"`
|
||||
SimNet bool `long:"simnet" description:"Connect to the simulation test network"`
|
||||
TLSSkipVerify bool `long:"skipverify" description:"Do not verify tls certificates (not recommended!)"`
|
||||
|
@ -96,7 +165,7 @@ func cleanAndExpandPath(path string) string {
|
|||
// The above results in functioning properly without any config settings
|
||||
// while still allowing the user to override settings with config files and
|
||||
// command line options. Command line options always take precedence.
|
||||
func loadConfig() (*flags.Parser, *config, []string, error) {
|
||||
func loadConfig() (*config, []string, error) {
|
||||
// Default config.
|
||||
cfg := config{
|
||||
ConfigFile: defaultConfigFile,
|
||||
|
@ -112,34 +181,54 @@ func loadConfig() (*flags.Parser, *config, []string, error) {
|
|||
}
|
||||
|
||||
// Pre-parse the command line options to see if an alternative config
|
||||
// file or the version flag was specified. Any errors can be ignored
|
||||
// here since they will be caught be the final parse below.
|
||||
// file, the version flag, or the list commands flag was specified. Any
|
||||
// errors aside from the help message error can be ignored here since
|
||||
// they will be caught by the final parse below.
|
||||
preCfg := cfg
|
||||
preParser := flags.NewParser(&preCfg, flags.None)
|
||||
_, _ = preParser.Parse()
|
||||
preParser := flags.NewParser(&preCfg, flags.HelpFlag)
|
||||
_, err = preParser.Parse()
|
||||
if err != nil {
|
||||
if e, ok := err.(*flags.Error); ok && e.Type == flags.ErrHelp {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Show the version and exit if the version flag was specified.
|
||||
if preCfg.ShowVersion {
|
||||
appName := filepath.Base(os.Args[0])
|
||||
appName = strings.TrimSuffix(appName, filepath.Ext(appName))
|
||||
usageMessage := fmt.Sprintf("Use %s -h to show options", appName)
|
||||
if preCfg.ShowVersion {
|
||||
fmt.Println(appName, "version", version())
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// Show the available commands and exit if the associated flag was
|
||||
// specified.
|
||||
if preCfg.ListCommands {
|
||||
listCommands()
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// Load additional config from file.
|
||||
parser := flags.NewParser(&cfg, flags.PassDoubleDash|flags.HelpFlag)
|
||||
parser := flags.NewParser(&cfg, flags.Default)
|
||||
err = flags.NewIniParser(parser).ParseFile(preCfg.ConfigFile)
|
||||
if err != nil {
|
||||
if _, ok := err.(*os.PathError); !ok {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
return parser, nil, nil, err
|
||||
fmt.Fprintf(os.Stderr, "Error parsing config file: %v\n",
|
||||
err)
|
||||
fmt.Fprintln(os.Stderr, usageMessage)
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Parse command line options again to ensure they take precedence.
|
||||
remainingArgs, err := parser.Parse()
|
||||
if err != nil {
|
||||
return parser, nil, nil, err
|
||||
if e, ok := err.(*flags.Error); !ok || e.Type != flags.ErrHelp {
|
||||
fmt.Fprintln(os.Stderr, usageMessage)
|
||||
}
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// Multiple networks can't be selected simultaneously.
|
||||
|
@ -155,7 +244,7 @@ func loadConfig() (*flags.Parser, *config, []string, error) {
|
|||
"together -- choose one of the two"
|
||||
err := fmt.Errorf(str, "loadConfig")
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
return parser, nil, nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// Override the RPC certificate if the --wallet flag was specified and
|
||||
|
@ -172,5 +261,5 @@ func loadConfig() (*flags.Parser, *config, []string, error) {
|
|||
cfg.RPCServer = normalizeAddress(cfg.RPCServer, cfg.TestNet3,
|
||||
cfg.SimNet, cfg.Wallet)
|
||||
|
||||
return parser, &cfg, remainingArgs, nil
|
||||
return &cfg, remainingArgs, nil
|
||||
}
|
||||
|
|
128
cmd/btcctl/httpclient.go
Normal file
128
cmd/btcctl/httpclient.go
Normal file
|
@ -0,0 +1,128 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
"github.com/btcsuite/btcd/btcjson/v2/btcjson"
|
||||
"github.com/btcsuite/go-socks/socks"
|
||||
)
|
||||
|
||||
// newHTTPClient returns a new HTTP client that is configured according to the
|
||||
// proxy and TLS settings in the associated connection configuration.
|
||||
func newHTTPClient(cfg *config) (*http.Client, error) {
|
||||
// Configure proxy if needed.
|
||||
var dial func(network, addr string) (net.Conn, error)
|
||||
if cfg.Proxy != "" {
|
||||
proxy := &socks.Proxy{
|
||||
Addr: cfg.Proxy,
|
||||
Username: cfg.ProxyUser,
|
||||
Password: cfg.ProxyPass,
|
||||
}
|
||||
dial = func(network, addr string) (net.Conn, error) {
|
||||
c, err := proxy.Dial(network, addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Configure TLS if needed.
|
||||
var tlsConfig *tls.Config
|
||||
if !cfg.NoTLS && cfg.RPCCert != "" {
|
||||
pem, err := ioutil.ReadFile(cfg.RPCCert)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pool := x509.NewCertPool()
|
||||
pool.AppendCertsFromPEM(pem)
|
||||
tlsConfig = &tls.Config{
|
||||
RootCAs: pool,
|
||||
InsecureSkipVerify: cfg.TLSSkipVerify,
|
||||
}
|
||||
}
|
||||
|
||||
// Create and return the new HTTP client potentially configured with a
|
||||
// proxy and TLS.
|
||||
client := http.Client{
|
||||
Transport: &http.Transport{
|
||||
Dial: dial,
|
||||
TLSClientConfig: tlsConfig,
|
||||
},
|
||||
}
|
||||
return &client, nil
|
||||
}
|
||||
|
||||
// sendPostRequest sends the marshalled JSON-RPC command using HTTP-POST mode
|
||||
// to the server described in the passed config struct. It also attempts to
|
||||
// unmarshal the response as a JSON-RPC response and returns either the result
|
||||
// field or the error field depending on whether or not there is an error.
|
||||
func sendPostRequest(marshalledJSON []byte, cfg *config) ([]byte, error) {
|
||||
// Generate a request to the configured RPC server.
|
||||
protocol := "http"
|
||||
if !cfg.NoTLS {
|
||||
protocol = "https"
|
||||
}
|
||||
url := protocol + "://" + cfg.RPCServer
|
||||
bodyReader := bytes.NewReader(marshalledJSON)
|
||||
httpRequest, err := http.NewRequest("POST", url, bodyReader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
httpRequest.Close = true
|
||||
httpRequest.Header.Set("Content-Type", "application/json")
|
||||
|
||||
// Configure basic access authorization.
|
||||
httpRequest.SetBasicAuth(cfg.RPCUser, cfg.RPCPassword)
|
||||
|
||||
// Create the new HTTP client that is configured according to the user-
|
||||
// specified options and submit the request.
|
||||
httpClient, err := newHTTPClient(cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
httpResponse, err := httpClient.Do(httpRequest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Read the raw bytes and close the response.
|
||||
respBytes, err := ioutil.ReadAll(httpResponse.Body)
|
||||
httpResponse.Body.Close()
|
||||
if err != nil {
|
||||
err = fmt.Errorf("error reading json reply: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Handle unsuccessful HTTP responses
|
||||
if httpResponse.StatusCode < 200 || httpResponse.StatusCode >= 300 {
|
||||
// Generate a standard error to return if the server body is
|
||||
// empty. This should not happen very often, but it's better
|
||||
// than showing nothing in case the target server has a poor
|
||||
// implementation.
|
||||
if len(respBytes) == 0 {
|
||||
return nil, fmt.Errorf("%d %s", httpResponse.StatusCode,
|
||||
http.StatusText(httpResponse.StatusCode))
|
||||
}
|
||||
return nil, fmt.Errorf("%s", respBytes)
|
||||
}
|
||||
|
||||
// Unmarshal the response.
|
||||
var resp btcjson.Response
|
||||
if err := json.Unmarshal(respBytes, &resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if resp.Error != nil {
|
||||
return nil, resp.Error
|
||||
}
|
||||
return resp.Result, nil
|
||||
}
|
Loading…
Reference in a new issue