Files
p9/cmd/ctl/ctl.go
Loki Verloren 0e2bba237a initial commit
2021-05-03 10:43:10 +02:00

381 lines
12 KiB
Go

package ctl
import (
"bufio"
"bytes"
"context"
"crypto/tls"
"crypto/x509"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"net"
"net/http"
"os"
"strings"
"github.com/btcsuite/go-socks/socks"
"github.com/p9c/p9/pkg/btcjson"
"github.com/p9c/p9/pod/config"
)
// Call uses settings in the context to call the method with the given parameters and returns the raw json bytes
func Call(
cx *config.Config, wallet bool, method string, params ...interface{},
) (result []byte, e error) {
// Ensure the specified method identifies a valid registered command and is one of the usable types.
var usageFlags btcjson.UsageFlag
usageFlags, e = btcjson.MethodUsageFlags(method)
if e != nil {
e = errors.New("Unrecognized command '" + method + "' : " + e.Error())
// HelpPrint()
return
}
if usageFlags&btcjson.UnusableFlags != 0 {
E.F("The '%s' command can only be used via websockets\n", method)
// HelpPrint()
return
}
// Attempt to create the appropriate command using the arguments provided by the user.
var cmd interface{}
cmd, e = btcjson.NewCmd(method, params...)
if e != nil {
// Show the error along with its error code when it's a json. BTCJSONError as it realistically will always be
// since the NewCmd function is only supposed to return errors of that type.
if jerr, ok := e.(btcjson.GeneralError); ok {
errText := fmt.Sprintf("%s command: %v (code: %s)\n", method, e, jerr.ErrorCode)
e = errors.New(errText)
// CommandUsage(method)
return
}
// The error is not a json.BTCJSONError and this really should not happen. Nevertheless fall back to just
// showing the error if it should happen due to a bug in the package.
errText := fmt.Sprintf("%s command: %v\n", method, e)
e = errors.New(errText)
// CommandUsage(method)
return
}
// Marshal the command into a JSON-RPC byte slice in preparation for sending it to the RPC server.
var marshalledJSON []byte
marshalledJSON, e = btcjson.MarshalCmd(1, cmd)
if e != nil {
return
}
// Send the JSON-RPC request to the server using the user-specified connection configuration.
result, e = sendPostRequest(marshalledJSON, cx, wallet)
if e != nil {
return
}
return
}
// 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.Config) (*http.Client, func(), error) {
var dial func(ctx context.Context, network string,
addr string) (net.Conn, error)
ctx, cancel := context.WithCancel(context.Background())
// Configure proxy if needed.
if cfg.ProxyAddress.V() != "" {
proxy := &socks.Proxy{
Addr: cfg.ProxyAddress.V(),
Username: cfg.ProxyUser.V(),
Password: cfg.ProxyPass.V(),
}
dial = func(_ context.Context, network string, addr string) (
net.Conn, error,
) {
c, e := proxy.Dial(network, addr)
if e != nil {
return nil, e
}
go func() {
out:
for {
select {
case <-ctx.Done():
if e := c.Close(); E.Chk(e) {
}
break out
}
}
}()
return c, nil
}
}
// Configure TLS if needed.
var tlsConfig *tls.Config
if cfg.ClientTLS.True() && cfg.RPCCert.V() != "" {
pem, e := ioutil.ReadFile(cfg.RPCCert.V())
if e != nil {
cancel()
return nil, nil, e
}
pool := x509.NewCertPool()
pool.AppendCertsFromPEM(pem)
tlsConfig = &tls.Config{
RootCAs: pool,
InsecureSkipVerify: cfg.TLSSkipVerify.True(),
}
}
// Create and return the new HTTP client potentially configured with a proxy and TLS.
client := http.Client{
Transport: &http.Transport{
Proxy: nil,
DialContext: dial,
TLSClientConfig: tlsConfig,
TLSHandshakeTimeout: 0,
DisableKeepAlives: false,
DisableCompression: false,
MaxIdleConns: 0,
MaxIdleConnsPerHost: 0,
MaxConnsPerHost: 0,
IdleConnTimeout: 0,
ResponseHeaderTimeout: 0,
ExpectContinueTimeout: 0,
TLSNextProto: nil,
ProxyConnectHeader: nil,
MaxResponseHeaderBytes: 0,
WriteBufferSize: 0,
ReadBufferSize: 0,
ForceAttemptHTTP2: false,
},
}
return &client, cancel, 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, cx *config.Config, wallet bool,
) ([]byte, error) {
// Generate a request to the configured RPC server.
protocol := "http"
if cx.ClientTLS.True() {
protocol = "https"
}
serverAddr := cx.RPCConnect.V()
if wallet {
serverAddr = cx.WalletServer.V()
_, _ = fmt.Fprintln(os.Stderr, "using wallet server", serverAddr)
}
url := protocol + "://" + serverAddr
bodyReader := bytes.NewReader(marshalledJSON)
httpRequest, e := http.NewRequest("POST", url, bodyReader)
if e != nil {
return nil, e
}
httpRequest.Close = true
httpRequest.Header.Set("Content-Type", "application/json")
// Configure basic access authorization.
httpRequest.SetBasicAuth(cx.Username.V(), cx.Password.V())
// T.Ln(cx.Username.V(), cx.Password.V())
// Create the new HTTP client that is configured according to the user - specified options and submit the request.
var httpClient *http.Client
var cancel func()
httpClient, cancel, e = newHTTPClient(cx)
if e != nil {
return nil, e
}
httpResponse, e := httpClient.Do(httpRequest)
if e != nil {
return nil, e
}
// close connection
cancel()
// Read the raw bytes and close the response.
respBytes, e := ioutil.ReadAll(httpResponse.Body)
if e := httpResponse.Body.Close(); E.Chk(e) {
e = fmt.Errorf("error reading json reply: %v", e)
return nil, e
}
// 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 e := json.Unmarshal(respBytes, &resp); E.Chk(e) {
return nil, e
}
if resp.Error != nil {
return nil, resp.Error
}
return resp.Result, nil
}
// ListCommands categorizes and lists all of the usable commands along with their one-line usage.
func ListCommands() (s string) {
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 {
var e error
var flags btcjson.UsageFlag
if flags, e = btcjson.MethodUsageFlags(method); E.Chk(e) {
continue
}
// Skip the commands that aren't usable from this utility.
if flags&btcjson.UnusableFlags != 0 {
continue
}
var usage string
if usage, e = btcjson.MethodUsageText(method); E.Chk(e) {
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++ {
s += categoryTitles[category]
s += "\n"
for _, usage := range categorized[category] {
s += "\t" + usage + "\n"
}
s += "\n"
}
return
}
// HelpPrint is the uninitialized help print function
var HelpPrint = func() {
fmt.Println("help has not been overridden")
}
// CtlMain is the entry point for the pod.Ctl component
func CtlMain(cx *config.Config) {
args := cx.ExtraArgs
if len(args) < 1 {
ListCommands()
os.Exit(1)
}
// Ensure the specified method identifies a valid registered command and is one of the usable types.
method := args[0]
var usageFlags btcjson.UsageFlag
var e error
if usageFlags, e = btcjson.MethodUsageFlags(method); E.Chk(e) {
_, _ = fmt.Fprintf(os.Stderr, "Unrecognized command '%s'\n", method)
HelpPrint()
os.Exit(1)
}
if usageFlags&btcjson.UnusableFlags != 0 {
_, _ = fmt.Fprintf(os.Stderr, "The '%s' command can only be used via websockets\n", method)
HelpPrint()
os.Exit(1)
}
// Convert remaining command line args to a slice of interface values to be passed along as parameters to new
// command creation function. Since some commands, such as submitblock, can involve data which is too large for the
// Operating System to allow as a normal command line parameter, support using '-' as an argument to allow the
// argument to be read from a stdin pipe.
bio := bufio.NewReader(os.Stdin)
params := make([]interface{}, 0, len(args[1:]))
for _, arg := range args[1:] {
if arg == "-" {
var param string
if param, e = bio.ReadString('\n'); E.Chk(e) && e != io.EOF {
_, _ = fmt.Fprintf(os.Stderr, "Failed to read data from stdin: %v\n", e)
os.Exit(1)
}
if e == io.EOF && len(param) == 0 {
_, _ = fmt.Fprintln(os.Stderr, "Not enough lines provided on stdin")
os.Exit(1)
}
param = strings.TrimRight(param, "\r\n")
params = append(params, param)
continue
}
params = append(params, arg)
}
var result []byte
if result, e = Call(cx, cx.UseWallet.True(), method, params...); E.Chk(e) {
return
}
// // Attempt to create the appropriate command using the arguments provided by the user.
// cmd, e := btcjson.NewCmd(method, params...)
// if e != nil {
// E.Ln(e)
// // Show the error along with its error code when it's a json. BTCJSONError as it realistically will always be
// // since the NewCmd function is only supposed to return errors of that type.
// if jerr, ok := err.(btcjson.BTCJSONError); ok {
// fmt.Fprintf(os.Stderr, "%s command: %v (code: %s)\n", method, e, jerr.ErrorCode)
// CommandUsage(method)
// os.Exit(1)
// }
// // The error is not a json.BTCJSONError and this really should not happen. Nevertheless fall back to just
// // showing the error if it should happen due to a bug in the package.
// fmt.Fprintf(os.Stderr, "%s command: %v\n", method, e)
// CommandUsage(method)
// os.Exit(1)
// }
// // Marshal the command into a JSON-RPC byte slice in preparation for sending it to the RPC server.
// marshalledJSON, e := btcjson.MarshalCmd(1, cmd)
// if e != nil {
// E.Ln(e)
// fmt.Println(e)
// os.Exit(1)
// }
// // Send the JSON-RPC request to the server using the user-specified connection configuration.
// result, e := sendPostRequest(marshalledJSON, cx)
// if e != nil {
// E.Ln(e)
// os.Exit(1)
// }
// Choose how to display the result based on its type.
strResult := string(result)
switch {
case strings.HasPrefix(strResult, "{") || strings.HasPrefix(strResult, "["):
var dst bytes.Buffer
if e = json.Indent(&dst, result, "", " "); E.Chk(e) {
fmt.Printf("Failed to format result: %v", e)
os.Exit(1)
}
fmt.Println(dst.String())
case strings.HasPrefix(strResult, `"`):
var str string
if e = json.Unmarshal(result, &str); E.Chk(e) {
_, _ = fmt.Fprintf(os.Stderr, "Failed to unmarshal result: %v", e)
os.Exit(1)
}
fmt.Println(str)
case strResult != "null":
fmt.Println(strResult)
}
}
// CommandUsage display the usage for a specific command.
func CommandUsage(method string) {
var usage string
var e error
if usage, e = btcjson.MethodUsageText(method); E.Chk(e) {
// This should never happen since the method was already checked before calling this function, but be safe.
fmt.Println("Failed to obtain command usage:", e)
return
}
fmt.Println("Usage:")
fmt.Printf(" %s\n", usage)
}