github.com/ZuluSpl0it/Sia@v1.3.7/node/api/client/client.go (about)

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