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