ctl.go raw

   1  package ctl
   2  
   3  import (
   4  	"bufio"
   5  	"bytes"
   6  	"context"
   7  	"crypto/tls"
   8  	"crypto/x509"
   9  	"encoding/json"
  10  	"errors"
  11  	"fmt"
  12  	"io"
  13  	"io/ioutil"
  14  	"net"
  15  	"net/http"
  16  	"os"
  17  	"strings"
  18  
  19  	"github.com/btcsuite/go-socks/socks"
  20  
  21  	"github.com/p9c/p9/pkg/btcjson"
  22  	"github.com/p9c/p9/pod/config"
  23  )
  24  
  25  // Call uses settings in the context to call the method with the given parameters and returns the raw json bytes
  26  func Call(
  27  	cx *config.Config, wallet bool, method string, params ...interface{},
  28  ) (result []byte, e error) {
  29  	// Ensure the specified method identifies a valid registered command and is one of the usable types.
  30  	var usageFlags btcjson.UsageFlag
  31  	usageFlags, e = btcjson.MethodUsageFlags(method)
  32  	if e != nil {
  33  		e = errors.New("Unrecognized command '" + method + "' : " + e.Error())
  34  		// HelpPrint()
  35  		return
  36  	}
  37  	if usageFlags&btcjson.UnusableFlags != 0 {
  38  		E.F("The '%s' command can only be used via websockets\n", method)
  39  		// HelpPrint()
  40  		return
  41  	}
  42  	// Attempt to create the appropriate command using the arguments provided by the user.
  43  	var cmd interface{}
  44  	cmd, e = btcjson.NewCmd(method, params...)
  45  	if e != nil {
  46  		// Show the error along with its error code when it's a json. BTCJSONError as it realistically will always be
  47  		// since the NewCmd function is only supposed to return errors of that type.
  48  		if jerr, ok := e.(btcjson.GeneralError); ok {
  49  			errText := fmt.Sprintf("%s command: %v (code: %s)\n", method, e, jerr.ErrorCode)
  50  			e = errors.New(errText)
  51  			// CommandUsage(method)
  52  			return
  53  		}
  54  		// The error is not a json.BTCJSONError and this really should not happen. Nevertheless fall back to just
  55  		// showing the error if it should happen due to a bug in the package.
  56  		errText := fmt.Sprintf("%s command: %v\n", method, e)
  57  		e = errors.New(errText)
  58  		// CommandUsage(method)
  59  		return
  60  	}
  61  	// Marshal the command into a JSON-RPC byte slice in preparation for sending it to the RPC server.
  62  	var marshalledJSON []byte
  63  	marshalledJSON, e = btcjson.MarshalCmd(1, cmd)
  64  	if e != nil {
  65  		return
  66  	}
  67  	// Send the JSON-RPC request to the server using the user-specified connection configuration.
  68  	result, e = sendPostRequest(marshalledJSON, cx, wallet)
  69  	if e != nil {
  70  		return
  71  	}
  72  	return
  73  }
  74  
  75  // newHTTPClient returns a new HTTP client that is configured according to the proxy and TLS settings in the associated
  76  // connection configuration.
  77  func newHTTPClient(cfg *config.Config) (*http.Client, func(), error) {
  78  	var dial func(ctx context.Context, network string,
  79  		addr string) (net.Conn, error)
  80  	ctx, cancel := context.WithCancel(context.Background())
  81  	// Configure proxy if needed.
  82  	if cfg.ProxyAddress.V() != "" {
  83  		proxy := &socks.Proxy{
  84  			Addr:     cfg.ProxyAddress.V(),
  85  			Username: cfg.ProxyUser.V(),
  86  			Password: cfg.ProxyPass.V(),
  87  		}
  88  		dial = func(_ context.Context, network string, addr string) (
  89  			net.Conn, error,
  90  		) {
  91  			c, e := proxy.Dial(network, addr)
  92  			if e != nil {
  93  				return nil, e
  94  			}
  95  			go func() {
  96  			out:
  97  				for {
  98  					select {
  99  					case <-ctx.Done():
 100  						if e := c.Close(); E.Chk(e) {
 101  						}
 102  						break out
 103  					}
 104  				}
 105  			}()
 106  			return c, nil
 107  		}
 108  	}
 109  	// Configure TLS if needed.
 110  	var tlsConfig *tls.Config
 111  	if cfg.ClientTLS.True() && cfg.RPCCert.V() != "" {
 112  		pem, e := ioutil.ReadFile(cfg.RPCCert.V())
 113  		if e != nil {
 114  			cancel()
 115  			return nil, nil, e
 116  		}
 117  		pool := x509.NewCertPool()
 118  		pool.AppendCertsFromPEM(pem)
 119  		tlsConfig = &tls.Config{
 120  			RootCAs:            pool,
 121  			InsecureSkipVerify: cfg.TLSSkipVerify.True(),
 122  		}
 123  	}
 124  	// Create and return the new HTTP client potentially configured with a proxy and TLS.
 125  	client := http.Client{
 126  		Transport: &http.Transport{
 127  			Proxy:                  nil,
 128  			DialContext:            dial,
 129  			TLSClientConfig:        tlsConfig,
 130  			TLSHandshakeTimeout:    0,
 131  			DisableKeepAlives:      false,
 132  			DisableCompression:     false,
 133  			MaxIdleConns:           0,
 134  			MaxIdleConnsPerHost:    0,
 135  			MaxConnsPerHost:        0,
 136  			IdleConnTimeout:        0,
 137  			ResponseHeaderTimeout:  0,
 138  			ExpectContinueTimeout:  0,
 139  			TLSNextProto:           nil,
 140  			ProxyConnectHeader:     nil,
 141  			MaxResponseHeaderBytes: 0,
 142  			WriteBufferSize:        0,
 143  			ReadBufferSize:         0,
 144  			ForceAttemptHTTP2:      false,
 145  		},
 146  	}
 147  	return &client, cancel, nil
 148  }
 149  
 150  // sendPostRequest sends the marshalled JSON-RPC command using HTTP-POST mode to the server described in the passed
 151  // config struct. It also attempts to unmarshal the response as a JSON-RPC response and returns either the result field
 152  // or the error field depending on whether or not there is an error.
 153  func sendPostRequest(
 154  	marshalledJSON []byte, cx *config.Config, wallet bool,
 155  ) ([]byte, error) {
 156  	// Generate a request to the configured RPC server.
 157  	protocol := "http"
 158  	if cx.ClientTLS.True() {
 159  		protocol = "https"
 160  	}
 161  	serverAddr := cx.RPCConnect.V()
 162  	if wallet {
 163  		serverAddr = cx.WalletServer.V()
 164  		_, _ = fmt.Fprintln(os.Stderr, "using wallet server", serverAddr)
 165  	}
 166  	url := protocol + "://" + serverAddr
 167  	bodyReader := bytes.NewReader(marshalledJSON)
 168  	httpRequest, e := http.NewRequest("POST", url, bodyReader)
 169  	if e != nil {
 170  		return nil, e
 171  	}
 172  	httpRequest.Close = true
 173  	httpRequest.Header.Set("Content-Type", "application/json")
 174  	// Configure basic access authorization.
 175  	httpRequest.SetBasicAuth(cx.Username.V(), cx.Password.V())
 176  	// T.Ln(cx.Username.V(), cx.Password.V())
 177  	// Create the new HTTP client that is configured according to the user - specified options and submit the request.
 178  	var httpClient *http.Client
 179  	var cancel func()
 180  	httpClient, cancel, e = newHTTPClient(cx)
 181  	if e != nil {
 182  		return nil, e
 183  	}
 184  	httpResponse, e := httpClient.Do(httpRequest)
 185  	if e != nil {
 186  		return nil, e
 187  	}
 188  	// close connection
 189  	cancel()
 190  	// Read the raw bytes and close the response.
 191  	respBytes, e := ioutil.ReadAll(httpResponse.Body)
 192  	if e := httpResponse.Body.Close(); E.Chk(e) {
 193  		e = fmt.Errorf("error reading json reply: %v", e)
 194  		return nil, e
 195  	}
 196  	// Handle unsuccessful HTTP responses
 197  	if httpResponse.StatusCode < 200 || httpResponse.StatusCode >= 300 {
 198  		// Generate a standard error to return if the server body is empty. This should not happen very often, but it's
 199  		// better than showing nothing in case the target server has a poor implementation.
 200  		if len(respBytes) == 0 {
 201  			return nil, fmt.Errorf("%d %s", httpResponse.StatusCode,
 202  				http.StatusText(httpResponse.StatusCode),
 203  			)
 204  		}
 205  		return nil, fmt.Errorf("%s", respBytes)
 206  	}
 207  	// Unmarshal the response.
 208  	var resp btcjson.Response
 209  	if e := json.Unmarshal(respBytes, &resp); E.Chk(e) {
 210  		return nil, e
 211  	}
 212  	if resp.Error != nil {
 213  		return nil, resp.Error
 214  	}
 215  	return resp.Result, nil
 216  }
 217  
 218  // ListCommands categorizes and lists all of the usable commands along with their one-line usage.
 219  func ListCommands() (s string) {
 220  	const (
 221  		categoryChain uint8 = iota
 222  		categoryWallet
 223  		numCategories
 224  	)
 225  	// Get a list of registered commands and categorize and filter them.
 226  	cmdMethods := btcjson.RegisteredCmdMethods()
 227  	categorized := make([][]string, numCategories)
 228  	for _, method := range cmdMethods {
 229  		var e error
 230  		var flags btcjson.UsageFlag
 231  		if flags, e = btcjson.MethodUsageFlags(method); E.Chk(e) {
 232  			continue
 233  		}
 234  		// Skip the commands that aren't usable from this utility.
 235  		if flags&btcjson.UnusableFlags != 0 {
 236  			continue
 237  		}
 238  		var usage string
 239  		if usage, e = btcjson.MethodUsageText(method); E.Chk(e) {
 240  			continue
 241  		}
 242  		// Categorize the command based on the usage flags.
 243  		category := categoryChain
 244  		if flags&btcjson.UFWalletOnly != 0 {
 245  			category = categoryWallet
 246  		}
 247  		categorized[category] = append(categorized[category], usage)
 248  	}
 249  	// Display the command according to their categories.
 250  	categoryTitles := make([]string, numCategories)
 251  	categoryTitles[categoryChain] = "Chain Server Commands:"
 252  	categoryTitles[categoryWallet] = "Wallet Server Commands (--wallet):"
 253  	for category := uint8(0); category < numCategories; category++ {
 254  		s += categoryTitles[category]
 255  		s += "\n"
 256  		for _, usage := range categorized[category] {
 257  			s += "\t" + usage + "\n"
 258  		}
 259  		s += "\n"
 260  	}
 261  	return
 262  }
 263  
 264  // HelpPrint is the uninitialized help print function
 265  var HelpPrint = func() {
 266  	fmt.Println("help has not been overridden")
 267  }
 268  
 269  // CtlMain is the entry point for the pod.Ctl component
 270  func CtlMain(cx *config.Config) {
 271  	args := cx.ExtraArgs
 272  	if len(args) < 1 {
 273  		ListCommands()
 274  		os.Exit(1)
 275  	}
 276  	// Ensure the specified method identifies a valid registered command and is one of the usable types.
 277  	method := args[0]
 278  	var usageFlags btcjson.UsageFlag
 279  	var e error
 280  	if usageFlags, e = btcjson.MethodUsageFlags(method); E.Chk(e) {
 281  		_, _ = fmt.Fprintf(os.Stderr, "Unrecognized command '%s'\n", method)
 282  		HelpPrint()
 283  		os.Exit(1)
 284  	}
 285  	if usageFlags&btcjson.UnusableFlags != 0 {
 286  		_, _ = fmt.Fprintf(os.Stderr, "The '%s' command can only be used via websockets\n", method)
 287  		HelpPrint()
 288  		os.Exit(1)
 289  	}
 290  	// Convert remaining command line args to a slice of interface values to be passed along as parameters to new
 291  	// command creation function. Since some commands, such as submitblock, can involve data which is too large for the
 292  	// Operating System to allow as a normal command line parameter, support using '-' as an argument to allow the
 293  	// argument to be read from a stdin pipe.
 294  	bio := bufio.NewReader(os.Stdin)
 295  	params := make([]interface{}, 0, len(args[1:]))
 296  	for _, arg := range args[1:] {
 297  		if arg == "-" {
 298  			var param string
 299  			if param, e = bio.ReadString('\n'); E.Chk(e) && e != io.EOF {
 300  				_, _ = fmt.Fprintf(os.Stderr, "Failed to read data from stdin: %v\n", e)
 301  				os.Exit(1)
 302  			}
 303  			if e == io.EOF && len(param) == 0 {
 304  				_, _ = fmt.Fprintln(os.Stderr, "Not enough lines provided on stdin")
 305  				os.Exit(1)
 306  			}
 307  			param = strings.TrimRight(param, "\r\n")
 308  			params = append(params, param)
 309  			continue
 310  		}
 311  		params = append(params, arg)
 312  	}
 313  	var result []byte
 314  	if result, e = Call(cx, cx.UseWallet.True(), method, params...); E.Chk(e) {
 315  		return
 316  	}
 317  	// // Attempt to create the appropriate command using the arguments provided by the user.
 318  	// cmd, e := btcjson.NewCmd(method, params...)
 319  	// if e != nil  {
 320  	// 	E.Ln(e)
 321  	// 	// Show the error along with its error code when it's a json. BTCJSONError as it realistically will always be
 322  	// 	// since the NewCmd function is only supposed to return errors of that type.
 323  	// 	if jerr, ok := err.(btcjson.BTCJSONError); ok {
 324  	// 		fmt.Fprintf(os.Stderr, "%s command: %v (code: %s)\n", method, e, jerr.ErrorCode)
 325  	// 		CommandUsage(method)
 326  	// 		os.Exit(1)
 327  	// 	}
 328  	// 	// The error is not a json.BTCJSONError and this really should not happen. Nevertheless fall back to just
 329  	// 	// showing the error if it should happen due to a bug in the package.
 330  	// 	fmt.Fprintf(os.Stderr, "%s command: %v\n", method, e)
 331  	// 	CommandUsage(method)
 332  	// 	os.Exit(1)
 333  	// }
 334  	// // Marshal the command into a JSON-RPC byte slice in preparation for sending it to the RPC server.
 335  	// marshalledJSON, e := btcjson.MarshalCmd(1, cmd)
 336  	// if e != nil  {
 337  	// 	E.Ln(e)
 338  	// 	fmt.Println(e)
 339  	// 	os.Exit(1)
 340  	// }
 341  	// // Send the JSON-RPC request to the server using the user-specified connection configuration.
 342  	// result, e := sendPostRequest(marshalledJSON, cx)
 343  	// if e != nil  {
 344  	// 	E.Ln(e)
 345  	// 	os.Exit(1)
 346  	// }
 347  	// Choose how to display the result based on its type.
 348  	strResult := string(result)
 349  	switch {
 350  	case strings.HasPrefix(strResult, "{") || strings.HasPrefix(strResult, "["):
 351  		var dst bytes.Buffer
 352  		if e = json.Indent(&dst, result, "", "  "); E.Chk(e) {
 353  			fmt.Printf("Failed to format result: %v", e)
 354  			os.Exit(1)
 355  		}
 356  		fmt.Println(dst.String())
 357  	case strings.HasPrefix(strResult, `"`):
 358  		var str string
 359  		if e = json.Unmarshal(result, &str); E.Chk(e) {
 360  			_, _ = fmt.Fprintf(os.Stderr, "Failed to unmarshal result: %v", e)
 361  			os.Exit(1)
 362  		}
 363  		fmt.Println(str)
 364  	case strResult != "null":
 365  		fmt.Println(strResult)
 366  	}
 367  }
 368  
 369  // CommandUsage display the usage for a specific command.
 370  func CommandUsage(method string) {
 371  	var usage string
 372  	var e error
 373  	if usage, e = btcjson.MethodUsageText(method); E.Chk(e) {
 374  		// This should never happen since the method was already checked before calling this function, but be safe.
 375  		fmt.Println("Failed to obtain command usage:", e)
 376  		return
 377  	}
 378  	fmt.Println("Usage:")
 379  	fmt.Printf("  %s\n", usage)
 380  }
 381