github.com/kinvolk/docker@v1.13.1/client/request.go (about)

     1  package client
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/json"
     6  	"fmt"
     7  	"io"
     8  	"io/ioutil"
     9  	"net"
    10  	"net/http"
    11  	"net/url"
    12  	"os"
    13  	"strings"
    14  
    15  	"github.com/docker/docker/api/types"
    16  	"github.com/docker/docker/api/types/versions"
    17  	"github.com/pkg/errors"
    18  	"golang.org/x/net/context"
    19  	"golang.org/x/net/context/ctxhttp"
    20  )
    21  
    22  // serverResponse is a wrapper for http API responses.
    23  type serverResponse struct {
    24  	body       io.ReadCloser
    25  	header     http.Header
    26  	statusCode int
    27  }
    28  
    29  // head sends an http request to the docker API using the method HEAD.
    30  func (cli *Client) head(ctx context.Context, path string, query url.Values, headers map[string][]string) (serverResponse, error) {
    31  	return cli.sendRequest(ctx, "HEAD", path, query, nil, headers)
    32  }
    33  
    34  // getWithContext sends an http request to the docker API using the method GET with a specific go context.
    35  func (cli *Client) get(ctx context.Context, path string, query url.Values, headers map[string][]string) (serverResponse, error) {
    36  	return cli.sendRequest(ctx, "GET", path, query, nil, headers)
    37  }
    38  
    39  // postWithContext sends an http request to the docker API using the method POST with a specific go context.
    40  func (cli *Client) post(ctx context.Context, path string, query url.Values, obj interface{}, headers map[string][]string) (serverResponse, error) {
    41  	body, headers, err := encodeBody(obj, headers)
    42  	if err != nil {
    43  		return serverResponse{}, err
    44  	}
    45  	return cli.sendRequest(ctx, "POST", path, query, body, headers)
    46  }
    47  
    48  func (cli *Client) postRaw(ctx context.Context, path string, query url.Values, body io.Reader, headers map[string][]string) (serverResponse, error) {
    49  	return cli.sendRequest(ctx, "POST", path, query, body, headers)
    50  }
    51  
    52  // put sends an http request to the docker API using the method PUT.
    53  func (cli *Client) put(ctx context.Context, path string, query url.Values, obj interface{}, headers map[string][]string) (serverResponse, error) {
    54  	body, headers, err := encodeBody(obj, headers)
    55  	if err != nil {
    56  		return serverResponse{}, err
    57  	}
    58  	return cli.sendRequest(ctx, "PUT", path, query, body, headers)
    59  }
    60  
    61  // put sends an http request to the docker API using the method PUT.
    62  func (cli *Client) putRaw(ctx context.Context, path string, query url.Values, body io.Reader, headers map[string][]string) (serverResponse, error) {
    63  	return cli.sendRequest(ctx, "PUT", path, query, body, headers)
    64  }
    65  
    66  // delete sends an http request to the docker API using the method DELETE.
    67  func (cli *Client) delete(ctx context.Context, path string, query url.Values, headers map[string][]string) (serverResponse, error) {
    68  	return cli.sendRequest(ctx, "DELETE", path, query, nil, headers)
    69  }
    70  
    71  type headers map[string][]string
    72  
    73  func encodeBody(obj interface{}, headers headers) (io.Reader, headers, error) {
    74  	if obj == nil {
    75  		return nil, headers, nil
    76  	}
    77  
    78  	body, err := encodeData(obj)
    79  	if err != nil {
    80  		return nil, headers, err
    81  	}
    82  	if headers == nil {
    83  		headers = make(map[string][]string)
    84  	}
    85  	headers["Content-Type"] = []string{"application/json"}
    86  	return body, headers, nil
    87  }
    88  
    89  func (cli *Client) buildRequest(method, path string, body io.Reader, headers headers) (*http.Request, error) {
    90  	expectedPayload := (method == "POST" || method == "PUT")
    91  	if expectedPayload && body == nil {
    92  		body = bytes.NewReader([]byte{})
    93  	}
    94  
    95  	req, err := http.NewRequest(method, path, body)
    96  	if err != nil {
    97  		return nil, err
    98  	}
    99  	req = cli.addHeaders(req, headers)
   100  
   101  	if cli.proto == "unix" || cli.proto == "npipe" {
   102  		// For local communications, it doesn't matter what the host is. We just
   103  		// need a valid and meaningful host name. (See #189)
   104  		req.Host = "docker"
   105  	}
   106  
   107  	req.URL.Host = cli.addr
   108  	req.URL.Scheme = cli.scheme
   109  
   110  	if expectedPayload && req.Header.Get("Content-Type") == "" {
   111  		req.Header.Set("Content-Type", "text/plain")
   112  	}
   113  	return req, nil
   114  }
   115  
   116  func (cli *Client) sendRequest(ctx context.Context, method, path string, query url.Values, body io.Reader, headers headers) (serverResponse, error) {
   117  	req, err := cli.buildRequest(method, cli.getAPIPath(path, query), body, headers)
   118  	if err != nil {
   119  		return serverResponse{}, err
   120  	}
   121  	return cli.doRequest(ctx, req)
   122  }
   123  
   124  func (cli *Client) doRequest(ctx context.Context, req *http.Request) (serverResponse, error) {
   125  	serverResp := serverResponse{statusCode: -1}
   126  
   127  	resp, err := ctxhttp.Do(ctx, cli.client, req)
   128  	if err != nil {
   129  		if cli.scheme != "https" && strings.Contains(err.Error(), "malformed HTTP response") {
   130  			return serverResp, fmt.Errorf("%v.\n* Are you trying to connect to a TLS-enabled daemon without TLS?", err)
   131  		}
   132  
   133  		if cli.scheme == "https" && strings.Contains(err.Error(), "bad certificate") {
   134  			return serverResp, fmt.Errorf("The server probably has client authentication (--tlsverify) enabled. Please check your TLS client certification settings: %v", err)
   135  		}
   136  
   137  		// Don't decorate context sentinel errors; users may be comparing to
   138  		// them directly.
   139  		switch err {
   140  		case context.Canceled, context.DeadlineExceeded:
   141  			return serverResp, err
   142  		}
   143  
   144  		if nErr, ok := err.(*url.Error); ok {
   145  			if nErr, ok := nErr.Err.(*net.OpError); ok {
   146  				if os.IsPermission(nErr.Err) {
   147  					return serverResp, errors.Wrapf(err, "Got permission denied while trying to connect to the Docker daemon socket at %v", cli.host)
   148  				}
   149  			}
   150  		}
   151  
   152  		if err, ok := err.(net.Error); ok {
   153  			if err.Timeout() {
   154  				return serverResp, ErrorConnectionFailed(cli.host)
   155  			}
   156  			if !err.Temporary() {
   157  				if strings.Contains(err.Error(), "connection refused") || strings.Contains(err.Error(), "dial unix") {
   158  					return serverResp, ErrorConnectionFailed(cli.host)
   159  				}
   160  			}
   161  		}
   162  
   163  		// Although there's not a strongly typed error for this in go-winio,
   164  		// lots of people are using the default configuration for the docker
   165  		// daemon on Windows where the daemon is listening on a named pipe
   166  		// `//./pipe/docker_engine, and the client must be running elevated.
   167  		// Give users a clue rather than the not-overly useful message
   168  		// such as `error during connect: Get http://%2F%2F.%2Fpipe%2Fdocker_engine/v1.25/info:
   169  		// open //./pipe/docker_engine: The system cannot find the file specified.`.
   170  		// Note we can't string compare "The system cannot find the file specified" as
   171  		// this is localised - for example in French the error would be
   172  		// `open //./pipe/docker_engine: Le fichier spécifié est introuvable.`
   173  		if strings.Contains(err.Error(), `open //./pipe/docker_engine`) {
   174  			err = errors.New(err.Error() + " In the default daemon configuration on Windows, the docker client must be run elevated to connect. This error may also indicate that the docker daemon is not running.")
   175  		}
   176  
   177  		return serverResp, errors.Wrap(err, "error during connect")
   178  	}
   179  
   180  	if resp != nil {
   181  		serverResp.statusCode = resp.StatusCode
   182  	}
   183  
   184  	if serverResp.statusCode < 200 || serverResp.statusCode >= 400 {
   185  		body, err := ioutil.ReadAll(resp.Body)
   186  		if err != nil {
   187  			return serverResp, err
   188  		}
   189  		if len(body) == 0 {
   190  			return serverResp, fmt.Errorf("Error: request returned %s for API route and version %s, check if the server supports the requested API version", http.StatusText(serverResp.statusCode), req.URL)
   191  		}
   192  
   193  		var errorMessage string
   194  		if (cli.version == "" || versions.GreaterThan(cli.version, "1.23")) &&
   195  			resp.Header.Get("Content-Type") == "application/json" {
   196  			var errorResponse types.ErrorResponse
   197  			if err := json.Unmarshal(body, &errorResponse); err != nil {
   198  				return serverResp, fmt.Errorf("Error reading JSON: %v", err)
   199  			}
   200  			errorMessage = errorResponse.Message
   201  		} else {
   202  			errorMessage = string(body)
   203  		}
   204  
   205  		return serverResp, fmt.Errorf("Error response from daemon: %s", strings.TrimSpace(errorMessage))
   206  	}
   207  
   208  	serverResp.body = resp.Body
   209  	serverResp.header = resp.Header
   210  	return serverResp, nil
   211  }
   212  
   213  func (cli *Client) addHeaders(req *http.Request, headers headers) *http.Request {
   214  	// Add CLI Config's HTTP Headers BEFORE we set the Docker headers
   215  	// then the user can't change OUR headers
   216  	for k, v := range cli.customHTTPHeaders {
   217  		if versions.LessThan(cli.version, "1.25") && k == "User-Agent" {
   218  			continue
   219  		}
   220  		req.Header.Set(k, v)
   221  	}
   222  
   223  	if headers != nil {
   224  		for k, v := range headers {
   225  			req.Header[k] = v
   226  		}
   227  	}
   228  	return req
   229  }
   230  
   231  func encodeData(data interface{}) (*bytes.Buffer, error) {
   232  	params := bytes.NewBuffer(nil)
   233  	if data != nil {
   234  		if err := json.NewEncoder(params).Encode(data); err != nil {
   235  			return nil, err
   236  		}
   237  	}
   238  	return params, nil
   239  }
   240  
   241  func ensureReaderClosed(response serverResponse) {
   242  	if body := response.body; body != nil {
   243  		// Drain up to 512 bytes and close the body to let the Transport reuse the connection
   244  		io.CopyN(ioutil.Discard, body, 512)
   245  		response.body.Close()
   246  	}
   247  }