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 }