1 // Copyright 2018 Adam S Levy
2 //
3 // Permission is hereby granted, free of charge, to any person obtaining a copy
4 // of this software and associated documentation files (the "Software"), to
5 // deal in the Software without restriction, including without limitation the
6 // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
7 // sell copies of the Software, and to permit persons to whom the Software is
8 // furnished to do so, subject to the following conditions:
9 //
10 // The above copyright notice and this permission notice shall be included in
11 // all copies or substantial portions of the Software.
12 //
13 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
18 // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
19 // IN THE SOFTWARE.
20 21 package jsonrpc2
22 23 import (
24 "bytes"
25 "context"
26 "encoding/json"
27 "fmt"
28 "io/ioutil"
29 "log"
30 "math/rand"
31 "net/http"
32 "os"
33 )
34 35 // Logger allows custom log types to be used with the Client when
36 // Client.DebugRequest is true.
37 type Logger interface {
38 Println(...interface{})
39 Printf(string, ...interface{})
40 }
41 42 func newErrorUnexpectedHTTPResponse(err error, body []byte, res *http.Response) error {
43 return ErrorUnexpectedHTTPResponse{err, body, res}
44 }
45 46 // ErrorUnexpectedHTTPResponse wraps errors that occur during unmarshling of
47 // the http.Response.Body into a Response, along with the full bytes of the
48 // http.Response.Body and the http.Response itself.
49 type ErrorUnexpectedHTTPResponse struct {
50 UnmarshlingErr error
51 Body []byte
52 *http.Response
53 }
54 55 // Error returns err.UnmarshalingErr.Error().
56 func (err ErrorUnexpectedHTTPResponse) Error() string {
57 return err.UnmarshlingErr.Error()
58 }
59 60 // Unwrap return err.UnmarshalingErr.
61 func (err *ErrorUnexpectedHTTPResponse) Unwrap() error {
62 return err.UnmarshlingErr
63 }
64 65 // Client embeds http.Client and provides a convenient way to make JSON-RPC 2.0
66 // requests.
67 type Client struct {
68 http.Client
69 DebugRequest bool
70 Log Logger
71 72 BasicAuth bool
73 User string
74 Password string
75 Header http.Header
76 }
77 78 // Request uses c to make a JSON-RPC 2.0 Request to url with the given method
79 // and params, and then parses the Response using the provided result, which
80 // should be a pointer so that it may be populated.
81 //
82 // If ctx is not nil, it is added to the http.Request.
83 //
84 // If the http.Response is received without error, but cannot be parsed into a
85 // Response, then an ErrorUnexpectedHTTPResponse is returned containing the
86 // Unmarshaling error, the raw bytes of the http.Response.Body, and the
87 // http.Response.
88 //
89 // If the Response.HasError() is true, then the Error is returned.
90 //
91 // Other potential errors can result from json.Marshal and params,
92 // http.NewRequest and url, or network errors from c.Do.
93 //
94 // A pseudorandom uint between 1 and 5000 is used for the Request.ID.
95 //
96 // The "Content-Type":"application/json" header is added to the http.Request,
97 // and then headers in c.Header are added, which may override the
98 // "Content-Type".
99 //
100 // If c.BasicAuth is true then http.Request.SetBasicAuth(c.User, c.Password) is
101 // be called.
102 //
103 // If c.DebugRequest is true then the Request and Response are printed using
104 // c.Log. If c.Log == nil, then c.Log = log.New(os.Stderr, "", 0).
105 func (c *Client) Request(ctx context.Context, url, method string,
106 params, result interface{}) error {
107 108 // Generate a psuedo random ID for this request.
109 reqID := rand.Int()%5000 + 1
110 111 // Marshal the JSON RPC Request.
112 req := Request{ID: reqID, Method: method, Params: params}
113 if c.DebugRequest {
114 if c.Log == nil {
115 c.Log = log.New(os.Stderr, "", 0)
116 }
117 c.Log.Println(req)
118 }
119 reqData, err := req.MarshalJSON()
120 if err != nil {
121 return err
122 }
123 124 // Compose the HTTP request.
125 httpReq, err := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(reqData))
126 if err != nil {
127 return err
128 }
129 if ctx != nil {
130 httpReq = httpReq.WithContext(ctx)
131 }
132 httpReq.Header.Add(http.CanonicalHeaderKey("Content-Type"), "application/json")
133 for k, v := range c.Header {
134 httpReq.Header[http.CanonicalHeaderKey(k)] = v
135 }
136 if c.BasicAuth {
137 httpReq.SetBasicAuth(c.User, c.Password)
138 }
139 140 // Make the request.
141 httpRes, err := c.Do(httpReq)
142 if err != nil {
143 return err
144 }
145 defer httpRes.Body.Close()
146 147 // Read the HTTP response.
148 body, err := ioutil.ReadAll(httpRes.Body)
149 if err != nil {
150 return err
151 }
152 if c.DebugRequest {
153 fmt.Println("<--", string(body))
154 fmt.Println()
155 }
156 157 // Unmarshal the HTTP response into a JSON RPC response.
158 var resID int
159 res := Response{Result: result, ID: &resID}
160 if err := json.Unmarshal(body, &res); err != nil {
161 return newErrorUnexpectedHTTPResponse(err, body, httpRes)
162 }
163 164 if res.HasError() {
165 return res.Error
166 }
167 168 return nil
169 }
170