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  }