github.com/decred/politeia@v1.4.0/politeiawww/client/client.go (about)

     1  // Copyright (c) 2020-2021 The Decred developers
     2  // Use of this source code is governed by an ISC
     3  // license that can be found in the LICENSE file.
     4  
     5  package client
     6  
     7  import (
     8  	"bytes"
     9  	"encoding/json"
    10  	"fmt"
    11  	"net/http"
    12  	"net/http/cookiejar"
    13  	"net/url"
    14  	"reflect"
    15  
    16  	"github.com/decred/politeia/util"
    17  	"github.com/gorilla/schema"
    18  	"golang.org/x/net/publicsuffix"
    19  )
    20  
    21  var (
    22  	// HTTP headers
    23  	headerCSRF = "X-CSRF-Token"
    24  )
    25  
    26  // Client provides a client for interacting with the politeiawww API.
    27  type Client struct {
    28  	host       string
    29  	headerCSRF string // Header csrf token
    30  	verbose    bool
    31  	rawJSON    bool
    32  	http       *http.Client
    33  }
    34  
    35  // makeReq makes a politeiawww http request to the method and route provided,
    36  // serializing the provided object as the request body, and returning a byte
    37  // slice of the response body. An ReqError is returned if politeiawww responds
    38  // with anything other than a 200 http status code.
    39  func (c *Client) makeReq(method string, api, route string, v interface{}) ([]byte, error) {
    40  	// Serialize body
    41  	var (
    42  		reqBody     []byte
    43  		queryParams string
    44  		err         error
    45  	)
    46  	if v != nil {
    47  		switch method {
    48  		case http.MethodGet:
    49  			// Use reflection in case the interface value is nil but the
    50  			// interface type is not. This can happen when query params
    51  			// exist but are not used.
    52  			if reflect.ValueOf(v).IsNil() {
    53  				break
    54  			}
    55  
    56  			// Populate GET request query params
    57  			form := url.Values{}
    58  			if err := schema.NewEncoder().Encode(v, form); err != nil {
    59  				return nil, err
    60  			}
    61  			queryParams = "?" + form.Encode()
    62  
    63  		case http.MethodPost, http.MethodPut:
    64  			reqBody, err = json.Marshal(v)
    65  			if err != nil {
    66  				return nil, err
    67  			}
    68  
    69  		default:
    70  			return nil, fmt.Errorf("unknown http method '%v'", method)
    71  		}
    72  	}
    73  
    74  	// Setup route
    75  	fullRoute := c.host + api + route + queryParams
    76  
    77  	// Print request details
    78  	switch {
    79  	case method == http.MethodGet && c.verbose:
    80  		fmt.Printf("Request: %v %v\n", method, fullRoute)
    81  	case method == http.MethodGet && c.rawJSON:
    82  		// No JSON to print
    83  	case c.verbose:
    84  		fmt.Printf("Request: %v %v\n", method, fullRoute)
    85  		if len(reqBody) > 0 {
    86  			fmt.Printf("%s\n", reqBody)
    87  		}
    88  	case c.rawJSON:
    89  		if len(reqBody) > 0 {
    90  			fmt.Printf("%s\n", reqBody)
    91  		}
    92  	}
    93  
    94  	// Send request
    95  	req, err := http.NewRequest(method, fullRoute, bytes.NewReader(reqBody))
    96  	if err != nil {
    97  		return nil, err
    98  	}
    99  	if c.headerCSRF != "" {
   100  		req.Header.Add(headerCSRF, c.headerCSRF)
   101  	}
   102  	r, err := c.http.Do(req)
   103  	if err != nil {
   104  		return nil, err
   105  	}
   106  	defer r.Body.Close()
   107  
   108  	// Print response code
   109  	if c.verbose {
   110  		fmt.Printf("Response: %v\n", r.StatusCode)
   111  	}
   112  
   113  	// Handle reply
   114  	if r.StatusCode != http.StatusOK {
   115  		switch r.StatusCode {
   116  		case http.StatusNotFound:
   117  			return nil, fmt.Errorf("404 not found")
   118  		case http.StatusForbidden:
   119  			return nil, fmt.Errorf("403 %s", util.RespBody(r))
   120  		default:
   121  			// All other http status codes should have a request body that
   122  			// decodes into a ErrorReply.
   123  			var e ErrorReply
   124  			decoder := json.NewDecoder(r.Body)
   125  			if err := decoder.Decode(&e); err != nil {
   126  				return nil, fmt.Errorf("status code %v: %v", r.StatusCode, err)
   127  			}
   128  			return nil, RespErr{
   129  				HTTPCode:   r.StatusCode,
   130  				API:        api,
   131  				ErrorReply: e,
   132  			}
   133  		}
   134  	}
   135  
   136  	// Decode response body
   137  	respBody := util.RespBody(r)
   138  
   139  	// Print response body
   140  	if c.verbose || c.rawJSON {
   141  		fmt.Printf("%s\n", respBody)
   142  	}
   143  
   144  	return respBody, nil
   145  }
   146  
   147  // Opts contains the politeiawww client options. All values are optional.
   148  //
   149  // Any provided HTTPSCert will be added to the http client's trusted cert
   150  // pool, allowing you to interact with a politeiawww instance that uses a
   151  // self signed cert.
   152  //
   153  // Authenticated routes require a CSRF cookie as well as the corresponding
   154  // CSRF header.
   155  type Opts struct {
   156  	HTTPSCert  string
   157  	Cookies    []*http.Cookie
   158  	HeaderCSRF string
   159  	Verbose    bool // Print verbose output
   160  	RawJSON    bool // Print raw json
   161  }
   162  
   163  // New returns a new politeiawww client.
   164  func New(host string, opts Opts) (*Client, error) {
   165  	// Setup http client
   166  	h, err := util.NewHTTPClient(false, opts.HTTPSCert)
   167  	if err != nil {
   168  		return nil, err
   169  	}
   170  
   171  	// Setup cookies
   172  	if opts.Cookies != nil {
   173  		copt := cookiejar.Options{
   174  			PublicSuffixList: publicsuffix.List,
   175  		}
   176  		jar, err := cookiejar.New(&copt)
   177  		if err != nil {
   178  			return nil, err
   179  		}
   180  		u, err := url.Parse(host)
   181  		if err != nil {
   182  			return nil, err
   183  		}
   184  		jar.SetCookies(u, opts.Cookies)
   185  		h.Jar = jar
   186  	}
   187  
   188  	return &Client{
   189  		host:       host,
   190  		headerCSRF: opts.HeaderCSRF,
   191  		verbose:    opts.Verbose,
   192  		rawJSON:    opts.RawJSON,
   193  		http:       h,
   194  	}, nil
   195  }