decred.org/dcrdex@v1.0.3/client/cmd/bwctl/main.go (about)

     1  // This code is available on the terms of the project LICENSE.md file,
     2  // also available online at https://blueoakcouncil.org/license/1.0.0.
     3  
     4  package main
     5  
     6  import (
     7  	"bufio"
     8  	"bytes"
     9  	"context"
    10  	"encoding/json"
    11  	"errors"
    12  	"fmt"
    13  	"io"
    14  	"os"
    15  	"strings"
    16  
    17  	"decred.org/dcrdex/client/rpcserver"
    18  	"decred.org/dcrdex/dex"
    19  	"decred.org/dcrdex/dex/encode"
    20  	"decred.org/dcrdex/dex/msgjson"
    21  	"decred.org/dcrdex/server/admin"
    22  )
    23  
    24  const (
    25  	showHelpMessage = "Specify -h to show available options"
    26  	listCmdMessage  = "Specify -l to list available commands"
    27  )
    28  
    29  var (
    30  	// requiredRPCServerVersion is the least version of the bisonw RPC server
    31  	// that this bwctl package can work with. It should be updated whenever a
    32  	// bwctl change requires an updated RPC server.
    33  	requiredRPCServerVersion = dex.Semver{Major: 0, Minor: 3, Patch: 0}
    34  )
    35  
    36  func main() {
    37  	// Create a context that is canceled when a shutdown signal is received.
    38  	ctx := withShutdownCancel(context.Background())
    39  	// Listen for interrupt signals (e.g. CTRL+C).
    40  	go shutdownListener()
    41  	if err := run(ctx); err != nil {
    42  		fmt.Fprintf(os.Stderr, "%v\n", err)
    43  		os.Exit(1)
    44  	}
    45  }
    46  
    47  // promptPasswords is a map of routes to password prompts. Passwords are
    48  // prompted in the order given.
    49  var promptPasswords = map[string][]string{
    50  	"discoveracct":      {"App password:"},
    51  	"init":              {"Set new app password:"},
    52  	"login":             {"App password:"},
    53  	"newwallet":         {"App password:", "Wallet password:"},
    54  	"openwallet":        {"App password:"},
    55  	"register":          {"App password:"},
    56  	"postbond":          {"App password:"},
    57  	"trade":             {"App password:"},
    58  	"withdraw":          {"App password:"},
    59  	"send":              {"App password:"},
    60  	"appseed":           {"App password:"},
    61  	"startmarketmaking": {"App password:"},
    62  	"multitrade":        {"App password:"},
    63  	"purchasetickets":   {"App password:"},
    64  	"startmmbot":        {"App password:"},
    65  	"withdrawbchspv":    {"App password"},
    66  }
    67  
    68  // optionalTextFiles is a map of routes to arg index for routes that should read
    69  // the text content of a file, where the file path _may_ be found in the route's
    70  // cmd args at the specified index.
    71  var optionalTextFiles = map[string]int{
    72  	"discoveracct": 1,
    73  	"bondassets":   1,
    74  	"postbond":     4,
    75  	"getdexconfig": 1,
    76  	"register":     3,
    77  	"newwallet":    2,
    78  }
    79  
    80  // promptPWs prompts for passwords on stdin and returns an error if prompting
    81  // fails or a password is empty. Returns passwords as a slice of []byte. If
    82  // cmdPWs is provided, the passwords will be drawn from cmdPWs instead of stdin
    83  // prompts.
    84  func promptPWs(ctx context.Context, cmd string, cmdPWs []string) ([]encode.PassBytes, error) {
    85  	prompts, exists := promptPasswords[cmd]
    86  	if !exists {
    87  		return nil, nil
    88  	}
    89  	var err error
    90  	pws := make([]encode.PassBytes, len(prompts))
    91  
    92  	if len(cmdPWs) > 0 {
    93  		if len(prompts) != len(cmdPWs) {
    94  			return nil, fmt.Errorf("Wrong number of command-line passwords, expected %d, got %d. Prompts = %v", len(prompts), len(cmdPWs), prompts)
    95  		}
    96  		for i, strPW := range cmdPWs {
    97  			pws[i] = encode.PassBytes(strPW)
    98  		}
    99  		return pws, nil
   100  	}
   101  
   102  	// Prompt for passwords one at a time.
   103  	for i, prompt := range prompts {
   104  		pws[i], err = admin.PasswordPrompt(ctx, prompt)
   105  		if err != nil {
   106  			return nil, err
   107  		}
   108  	}
   109  	return pws, nil
   110  }
   111  
   112  // readTextFile reads the text content of the file whose path is specified at
   113  // args' index as expected for cmd and sets the args value at the expected index
   114  // to the file's text content. The passed args are modified.
   115  func readTextFile(cmd string, args []string) error {
   116  	fileArgIndx, readFile := optionalTextFiles[cmd]
   117  	// Not an error if file path arg is not provided for optional file args.
   118  	if !readFile || len(args) < fileArgIndx+1 || args[fileArgIndx] == "" {
   119  		return nil
   120  	}
   121  	path := dex.CleanAndExpandPath(args[fileArgIndx])
   122  	if !dex.FileExists(path) {
   123  		return fmt.Errorf("no file found at %s", path)
   124  	}
   125  	fileContents, err := os.ReadFile(path)
   126  	if err != nil {
   127  		return fmt.Errorf("error reading %s: %v", path, err)
   128  	}
   129  	args[fileArgIndx] = string(fileContents)
   130  	return nil
   131  }
   132  
   133  func run(ctx context.Context) error {
   134  	cfg, args, stop, err := configure()
   135  	if err != nil {
   136  		return fmt.Errorf("unable to configure: %v", err)
   137  	}
   138  
   139  	if stop {
   140  		return nil
   141  	}
   142  
   143  	if len(args) < 1 {
   144  		return fmt.Errorf("no command specified\n%s", listCmdMessage)
   145  	}
   146  
   147  	// Convert remaining command line args to a slice of interface values
   148  	// to be passed along as parameters to new command creation function.
   149  	//
   150  	// Support using '-' as an argument to allow the argument to be read
   151  	// from a stdin pipe.
   152  	bio := bufio.NewReader(os.Stdin)
   153  	params := make([]string, 0, len(args[1:]))
   154  	for _, arg := range args[1:] {
   155  		if arg == "-" {
   156  			param, err := bio.ReadString('\n')
   157  			if err != nil && err != io.EOF {
   158  				return fmt.Errorf("Failed to read data from stdin: %v", err)
   159  			}
   160  			if err == io.EOF && len(param) == 0 {
   161  				return errors.New("Not enough lines provided on stdin")
   162  			}
   163  			param = strings.TrimRight(param, "\r\n")
   164  			params = append(params, param)
   165  			continue
   166  		}
   167  		params = append(params, arg)
   168  	}
   169  
   170  	// Prompt for passwords.
   171  	pws, err := promptPWs(ctx, args[0], cfg.PasswordArgs)
   172  	if err != nil {
   173  		return err
   174  	}
   175  
   176  	// Attempt to read TLS certificates.
   177  	err = readTextFile(args[0], params)
   178  	if err != nil {
   179  		return err
   180  	}
   181  
   182  	payload := &rpcserver.RawParams{
   183  		PWArgs: pws,
   184  		Args:   params,
   185  	}
   186  
   187  	// Create a request using the parsedArgs.
   188  	msg, err := msgjson.NewRequest(1, args[0], payload)
   189  	if err != nil {
   190  		return fmt.Errorf("unable to create request: %v", err)
   191  	}
   192  
   193  	// Marshal the command into a JSON-RPC byte slice in preparation for
   194  	// sending it to the RPC server.
   195  	marshalledJSON, err := json.Marshal(msg)
   196  	if err != nil {
   197  		return fmt.Errorf("unable to marshal message: %v", err)
   198  	}
   199  
   200  	// Send the JSON-RPC request to the server using the user-specified
   201  	// connection configuration.
   202  	respMsg, err := sendPostRequest(marshalledJSON, cfg)
   203  	if err != nil {
   204  		return fmt.Errorf("unable to send request: %v", err)
   205  	}
   206  
   207  	// Retrieve the payload from the response.
   208  	resp, err := respMsg.Response()
   209  	if err != nil {
   210  		return fmt.Errorf("unable to unmarshal response payload: %v", err)
   211  	}
   212  
   213  	if resp.Error != nil {
   214  		return errors.New(resp.Error.Message)
   215  	}
   216  
   217  	// Choose how to display the result based on its type.
   218  	strResult := string(resp.Result)
   219  	if strings.HasPrefix(strResult, "{") || strings.HasPrefix(strResult, "[") {
   220  		var dst bytes.Buffer
   221  		if err := json.Indent(&dst, resp.Result, "", "  "); err != nil {
   222  			return fmt.Errorf("failed to format result: %v", err)
   223  		}
   224  		fmt.Println(dst.String())
   225  	} else if strings.HasPrefix(strResult, `"`) {
   226  		var str string
   227  		if err := json.Unmarshal(resp.Result, &str); err != nil {
   228  			return fmt.Errorf("failed to unmarshal result: %v", err)
   229  		}
   230  		fmt.Println(str)
   231  	} else if strResult != "null" {
   232  		fmt.Println(strResult)
   233  	}
   234  
   235  	// If this is a version check command, go the extra mile and check for
   236  	// compatibility.
   237  	if args[0] == "version" {
   238  		var verResp rpcserver.VersionResponse
   239  		err = json.Unmarshal(resp.Result, &verResp)
   240  		if err != nil {
   241  			return fmt.Errorf("unable to check RPC server compatibility: failed to unmarshal result: %v", err)
   242  		}
   243  
   244  		if !dex.SemverCompatible(requiredRPCServerVersion, *verResp.RPCServerVer) {
   245  			return fmt.Errorf("%s is not compatible with bisonw RPC server: required RPC server version - %s, bisonw RPC server version - %s",
   246  				appName, requiredRPCServerVersion.String(), verResp.RPCServerVer.String())
   247  		}
   248  	}
   249  	return nil
   250  }