go.ligato.io/vpp-agent/v3@v3.5.0/cmd/agentctl/client/http.go (about) 1 package client 2 3 import ( 4 "bytes" 5 "context" 6 "encoding/json" 7 "fmt" 8 "io" 9 "net" 10 "net/http" 11 "net/url" 12 "os" 13 "strings" 14 15 "github.com/pkg/errors" 16 "github.com/sirupsen/logrus" 17 18 "go.ligato.io/vpp-agent/v3/cmd/agentctl/api/types" 19 ) 20 21 // serverResponse is a wrapper for http API responses. 22 type serverResponse struct { 23 body io.ReadCloser 24 contentLen int64 25 header http.Header 26 statusCode int 27 reqURL *url.URL 28 } 29 30 func (c *Client) get(ctx context.Context, path string, query url.Values, headers map[string][]string) (serverResponse, error) { 31 return c.sendRequest(ctx, "GET", path, query, nil, headers) 32 } 33 34 func (c *Client) post(ctx context.Context, path string, query url.Values, obj interface{}, headers map[string][]string) (serverResponse, error) { 35 body, headers, err := encodeBody(obj, headers) 36 if err != nil { 37 return serverResponse{}, err 38 } 39 return c.sendRequest(ctx, "POST", path, query, body, headers) 40 } 41 42 func (c *Client) put(ctx context.Context, path string, query url.Values, obj interface{}, headers map[string][]string) (serverResponse, error) { 43 body, headers, err := encodeBody(obj, headers) 44 if err != nil { 45 return serverResponse{}, err 46 } 47 return c.sendRequest(ctx, "PUT", path, query, body, headers) 48 } 49 50 type headers map[string][]string 51 52 func encodeBody(obj interface{}, headers headers) (io.Reader, headers, error) { 53 if obj == nil { 54 return nil, headers, nil 55 } 56 57 body, err := encodeData(obj) 58 if err != nil { 59 return nil, headers, err 60 } 61 if headers == nil { 62 headers = make(map[string][]string) 63 } 64 headers["Content-Type"] = []string{"application/json"} 65 return body, headers, nil 66 } 67 68 func (c *Client) buildRequest(method, path string, body io.Reader, headers headers) (*http.Request, error) { 69 expectedPayload := method == "POST" || method == "PUT" 70 if expectedPayload && body == nil { 71 body = bytes.NewReader([]byte{}) 72 } 73 74 req, err := http.NewRequest(method, path, body) 75 if err != nil { 76 return nil, err 77 } 78 req = c.addHeaders(req, headers) 79 80 if c.proto == "unix" || c.proto == "npipe" { 81 // For local communications, it doesn't matter what the host is. 82 // We just need a valid and meaningful host name. 83 req.Host = "ligato-agent" 84 } 85 86 req.URL.Host = c.httpAddr 87 req.URL.Scheme = c.scheme 88 89 if expectedPayload && req.Header.Get("Content-Type") == "" { 90 req.Header.Set("Content-Type", "text/plain") 91 } 92 return req, nil 93 } 94 95 func (c *Client) sendRequest(ctx context.Context, method, path string, query url.Values, body io.Reader, headers headers) (serverResponse, error) { 96 req, err := c.buildRequest(method, c.getAPIPath(ctx, path, query), body, headers) 97 if err != nil { 98 return serverResponse{}, err 99 } 100 resp, err := c.doRequest(ctx, req) 101 if err != nil { 102 return resp, err 103 } 104 err = c.checkResponseErr(resp) 105 return resp, err 106 } 107 108 func (c *Client) doRequest(ctx context.Context, req *http.Request) (serverResponse, error) { 109 serverResp := serverResponse{ 110 statusCode: -1, 111 reqURL: req.URL, 112 } 113 var ( 114 err error 115 resp *http.Response 116 ) 117 req = req.WithContext(ctx) 118 119 fields := map[string]interface{}{} 120 if req.ContentLength > 0 { 121 fields["contentLength"] = req.ContentLength 122 } 123 logrus.WithFields(fields).Debugf("=> sending http request: %s %s", req.Method, req.URL) 124 defer func() { 125 if err != nil { 126 logrus.Debugf("<- http response ERROR: %v", err) 127 } else { 128 logrus.Debugf("<- http response %v (%d bytes)", serverResp.statusCode, serverResp.contentLen) 129 } 130 }() 131 132 resp, err = c.HTTPClient().Do(req) 133 if err != nil { 134 if c.scheme != "https" && strings.Contains(err.Error(), "malformed HTTP response") { 135 return serverResp, fmt.Errorf("%v.\n* Are you trying to connect to a TLS-enabled daemon without TLS?", err) 136 } 137 if c.scheme == "https" && strings.Contains(err.Error(), "bad certificate") { 138 return serverResp, errors.Wrap(err, "The server probably has client authentication (--tlsverify) enabled. Please check your TLS client certification settings") 139 } 140 141 // Don't decorate context sentinel errors; users may be comparing to 142 // them directly. 143 switch err { 144 case context.Canceled, context.DeadlineExceeded: 145 return serverResp, err 146 } 147 if nErr, ok := err.(*url.Error); ok { 148 if nErr, ok := nErr.Err.(*net.OpError); ok { 149 if os.IsPermission(nErr.Err) { 150 return serverResp, errors.Wrapf(err, "Got permission denied while trying to connect to the agent socket at %v", c.host) 151 } 152 } 153 } 154 if err, ok := err.(net.Error); ok { 155 if err.Timeout() { 156 return serverResp, ErrorConnectionFailed(c.host) 157 } 158 if !err.Temporary() { 159 if strings.Contains(err.Error(), "connection refused") || strings.Contains(err.Error(), "dial unix") { 160 return serverResp, ErrorConnectionFailed(c.host) 161 } 162 } 163 } 164 return serverResp, errors.Wrap(err, "error during connect") 165 } 166 if logrus.IsLevelEnabled(logrus.DebugLevel) { 167 body, err := io.ReadAll(resp.Body) 168 if err != nil { 169 logrus.Debugf("reading body failed: %v", err) 170 } else { 171 logrus.Debugf("body: %s", body) 172 } 173 resp.Body = io.NopCloser(bytes.NewReader(body)) 174 } 175 if resp != nil { 176 serverResp.statusCode = resp.StatusCode 177 serverResp.body = resp.Body 178 serverResp.header = resp.Header 179 serverResp.contentLen = resp.ContentLength 180 } 181 return serverResp, nil 182 } 183 184 func (c *Client) checkResponseErr(serverResp serverResponse) error { 185 if serverResp.statusCode >= 200 && serverResp.statusCode < 400 { 186 return nil 187 } 188 var body []byte 189 var err error 190 if serverResp.body != nil { 191 bodyMax := 1 * 1024 * 1024 // 1 MiB 192 bodyR := &io.LimitedReader{ 193 R: serverResp.body, 194 N: int64(bodyMax), 195 } 196 body, err = io.ReadAll(bodyR) 197 if err != nil { 198 return err 199 } 200 if bodyR.N == 0 { 201 return fmt.Errorf("request returned %s with a message (> %d bytes) for API route and version %s, check if the server supports the requested API version", 202 http.StatusText(serverResp.statusCode), bodyMax, serverResp.reqURL) 203 } 204 } 205 if len(body) == 0 { 206 return fmt.Errorf("request returned %s for API route and version %s, check if the server supports the requested API version", 207 http.StatusText(serverResp.statusCode), serverResp.reqURL) 208 } 209 var ct string 210 if serverResp.header != nil { 211 ct = serverResp.header.Get("Content-Type") 212 } 213 var errorMsg string 214 if ct == "application/json" { 215 var errorResponse types.ErrorResponse 216 if err := json.Unmarshal(body, &errorResponse); err != nil { 217 return errors.Wrap(err, "Error unmarshaling JSON body") 218 } 219 errorMsg = errorResponse.Message 220 } else { 221 errorMsg = string(body) 222 } 223 errorMsg = fmt.Sprintf("[%d] %s", serverResp.statusCode, strings.TrimSpace(errorMsg)) 224 225 return errors.Wrap(errors.New(errorMsg), "Error response from daemon") 226 } 227 228 func (c *Client) addHeaders(req *http.Request, headers headers) *http.Request { 229 // Add CLI Config's HTTP Headers BEFORE we set the client headers 230 // then the user can't change OUR headers 231 for k, v := range c.customHTTPHeaders { 232 req.Header.Set(k, v) 233 } 234 for k, v := range headers { 235 req.Header[k] = v 236 } 237 return req 238 } 239 240 func encodeData(data interface{}) (*bytes.Buffer, error) { 241 params := bytes.NewBuffer(nil) 242 if data != nil { 243 if err := json.NewEncoder(params).Encode(data); err != nil { 244 return nil, err 245 } 246 } 247 return params, nil 248 } 249 250 func ensureReaderClosed(response serverResponse) { 251 if response.body != nil { 252 // Drain up to 512 bytes and close the body to let the Transport reuse the connection 253 _, _ = io.CopyN(io.Discard, response.body, 512) 254 _ = response.body.Close() 255 } 256 }