decred.org/dcrdex@v1.0.5/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 }