gitlab.com/jokerrs1/Sia@v1.3.2/node/api/client/client.go (about) 1 package client 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "io" 7 "io/ioutil" 8 "net/http" 9 "strings" 10 11 "github.com/NebulousLabs/Sia/node/api" 12 "github.com/NebulousLabs/errors" 13 ) 14 15 // A Client makes requests to the siad HTTP API. 16 type Client struct { 17 // Address is the API address of the siad server. 18 Address string 19 20 // Password must match the password of the siad server. 21 Password string 22 23 // UserAgent must match the User-Agent required by the siad server. If not 24 // set, it defaults to "Sia-Agent". 25 UserAgent string 26 } 27 28 // New creates a new Client using the provided address. 29 func New(address string) *Client { 30 return &Client{ 31 Address: address, 32 } 33 } 34 35 // NewRequest constructs a request to the siad HTTP API, setting the correct 36 // User-Agent and Basic Auth. The resource path must begin with /. 37 func (c *Client) NewRequest(method, resource string, body io.Reader) (*http.Request, error) { 38 url := "http://" + c.Address + resource 39 req, err := http.NewRequest(method, url, body) 40 if err != nil { 41 return nil, err 42 } 43 agent := c.UserAgent 44 if agent == "" { 45 agent = "Sia-Agent" 46 } 47 req.Header.Set("User-Agent", agent) 48 if c.Password != "" { 49 req.SetBasicAuth("", c.Password) 50 } 51 return req, nil 52 } 53 54 // drainAndClose reads rc until EOF and then closes it. drainAndClose should 55 // always be called on HTTP response bodies, because if the body is not fully 56 // read, the underlying connection can't be reused. 57 func drainAndClose(rc io.ReadCloser) { 58 io.Copy(ioutil.Discard, rc) 59 rc.Close() 60 } 61 62 // readAPIError decodes and returns an api.Error. 63 func readAPIError(r io.Reader) error { 64 var apiErr api.Error 65 if err := json.NewDecoder(r).Decode(&apiErr); err != nil { 66 return errors.AddContext(err, "could not read error response") 67 } 68 return apiErr 69 } 70 71 // getRawResponse requests the specified resource. The response, if provided, 72 // will be returned in a byte slice 73 func (c *Client) getRawResponse(resource string) ([]byte, error) { 74 req, err := c.NewRequest("GET", resource, nil) 75 if err != nil { 76 return nil, err 77 } 78 res, err := http.DefaultClient.Do(req) 79 if err != nil { 80 return nil, errors.AddContext(err, "request failed") 81 } 82 defer drainAndClose(res.Body) 83 84 if res.StatusCode == http.StatusNotFound { 85 return nil, errors.New("API call not recognized: " + resource) 86 } 87 88 // If the status code is not 2xx, decode and return the accompanying 89 // api.Error. 90 if res.StatusCode < 200 || res.StatusCode > 299 { 91 return nil, readAPIError(res.Body) 92 } 93 94 if res.StatusCode == http.StatusNoContent { 95 // no reason to read the response 96 return []byte{}, nil 97 } 98 return ioutil.ReadAll(res.Body) 99 } 100 101 // get requests the specified resource. The response, if provided, will be 102 // decoded into obj. The resource path must begin with /. 103 func (c *Client) get(resource string, obj interface{}) error { 104 // Request resource 105 data, err := c.getRawResponse(resource) 106 if err != nil { 107 return err 108 } 109 if obj == nil { 110 // No need to decode response 111 return nil 112 } 113 114 // Decode response 115 buf := bytes.NewBuffer(data) 116 err = json.NewDecoder(buf).Decode(obj) 117 if err != nil { 118 return errors.AddContext(err, "could not read response") 119 } 120 return nil 121 } 122 123 // postRawResponse requests the specified resource. The response, if provided, 124 // will be returned in a byte slice 125 func (c *Client) postRawResponse(resource string, data string) ([]byte, error) { 126 req, err := c.NewRequest("POST", resource, strings.NewReader(data)) 127 if err != nil { 128 return nil, err 129 } 130 // TODO: is this necessary? 131 req.Header.Set("Content-Type", "application/x-www-form-urlencoded") 132 res, err := http.DefaultClient.Do(req) 133 if err != nil { 134 return nil, errors.AddContext(err, "request failed") 135 } 136 defer drainAndClose(res.Body) 137 138 if res.StatusCode == http.StatusNotFound { 139 return nil, errors.New("API call not recognized: " + resource) 140 } 141 142 // If the status code is not 2xx, decode and return the accompanying 143 // api.Error. 144 if res.StatusCode < 200 || res.StatusCode > 299 { 145 return nil, readAPIError(res.Body) 146 } 147 148 if res.StatusCode == http.StatusNoContent { 149 // no reason to read the response 150 return []byte{}, nil 151 } 152 return ioutil.ReadAll(res.Body) 153 } 154 155 // post makes a POST request to the resource at `resource`, using `data` as the 156 // request body. The response, if provided, will be decoded into `obj`. 157 func (c *Client) post(resource string, data string, obj interface{}) error { 158 // Request resource 159 body, err := c.postRawResponse(resource, data) 160 if err != nil { 161 return err 162 } 163 if obj == nil { 164 // No need to decode response 165 return nil 166 } 167 168 // Decode response 169 buf := bytes.NewBuffer(body) 170 err = json.NewDecoder(buf).Decode(obj) 171 if err != nil { 172 return errors.AddContext(err, "could not read response") 173 } 174 return nil 175 }