diff --git a/api/client/cli.go b/api/client/cli.go index 834c47a4d3..7848fd9228 100644 --- a/api/client/cli.go +++ b/api/client/cli.go @@ -6,14 +6,14 @@ import ( "fmt" "io" "net/http" - "net/url" "os" - "strings" + "runtime" + "github.com/docker/docker/api/client/lib" "github.com/docker/docker/cli" "github.com/docker/docker/cliconfig" + "github.com/docker/docker/dockerversion" "github.com/docker/docker/opts" - "github.com/docker/docker/pkg/sockets" "github.com/docker/docker/pkg/term" "github.com/docker/docker/pkg/tlsconfig" ) @@ -24,13 +24,6 @@ type DockerCli struct { // initializing closure init func() error - // proto holds the client protocol i.e. unix. - proto string - // addr holds the client address. - addr string - // basePath holds the path to prepend to the requests - basePath string - // configFile has the client configuration file configFile *cliconfig.ConfigFile // in holds the input stream and closer (io.ReadCloser) for the client. @@ -41,11 +34,6 @@ type DockerCli struct { err io.Writer // keyFile holds the key file as a string. keyFile string - // tlsConfig holds the TLS configuration for the client, and will - // set the scheme to https in NewDockerCli if present. - tlsConfig *tls.Config - // scheme holds the scheme of the client i.e. https. - scheme string // inFd holds the file descriptor of the client's STDIN (if valid). inFd uintptr // outFd holds file descriptor of the client's STDOUT (if valid). @@ -54,6 +42,22 @@ type DockerCli struct { isTerminalIn bool // isTerminalOut indicates whether the client's STDOUT is a TTY isTerminalOut bool + // client is the http client that performs all API operations + client *lib.Client + + // DEPRECATED OPTIONS TO MAKE THE CLIENT COMPILE + // TODO: Remove + // proto holds the client protocol i.e. unix. + proto string + // addr holds the client address. + addr string + // basePath holds the path to prepend to the requests + basePath string + // tlsConfig holds the TLS configuration for the client, and will + // set the scheme to https in NewDockerCli if present. + tlsConfig *tls.Config + // scheme holds the scheme of the client i.e. https. + scheme string // transport holds the client transport instance. transport *http.Transport } @@ -98,50 +102,35 @@ func NewDockerCli(in io.ReadCloser, out, err io.Writer, clientFlags *cli.ClientF } cli.init = func() error { - clientFlags.PostParse() + configFile, e := cliconfig.Load(cliconfig.ConfigDir()) + if e != nil { + fmt.Fprintf(cli.err, "WARNING: Error loading config file:%v\n", e) + } + cli.configFile = configFile - hosts := clientFlags.Common.Hosts - - switch len(hosts) { - case 0: - hosts = []string{os.Getenv("DOCKER_HOST")} - case 1: - // only accept one host to talk to - default: - return errors.New("Please specify only one -H") + host, err := getServerHost(clientFlags.Common.Hosts, clientFlags.Common.TLSOptions) + if err != nil { + return err } - defaultHost := opts.DefaultTCPHost - if clientFlags.Common.TLSOptions != nil { - defaultHost = opts.DefaultTLSHost + customHeaders := cli.configFile.HTTPHeaders + if customHeaders == nil { + customHeaders = map[string]string{} } + customHeaders["User-Agent"] = "Docker-Client/" + dockerversion.Version + " (" + runtime.GOOS + ")" - var e error - if hosts[0], e = opts.ParseHost(defaultHost, hosts[0]); e != nil { - return e + client, err := lib.NewClient(host, clientFlags.Common.TLSOptions, customHeaders) + if err != nil { + return err } + cli.client = client - protoAddrParts := strings.SplitN(hosts[0], "://", 2) - cli.proto, cli.addr = protoAddrParts[0], protoAddrParts[1] - - if cli.proto == "tcp" { - // error is checked in pkg/parsers already - parsed, _ := url.Parse("tcp://" + cli.addr) - cli.addr = parsed.Host - cli.basePath = parsed.Path - } - - if clientFlags.Common.TLSOptions != nil { - cli.scheme = "https" - var e error - cli.tlsConfig, e = tlsconfig.Client(*clientFlags.Common.TLSOptions) - if e != nil { - return e - } - } else { - cli.scheme = "http" - } + // FIXME: Deprecated, only to keep the old code running. + cli.transport = client.HTTPClient.Transport.(*http.Transport) + cli.basePath = client.BasePath + cli.addr = client.Addr + cli.scheme = client.Scheme if cli.in != nil { cli.inFd, cli.isTerminalIn = term.GetFdInfo(cli.in) @@ -150,20 +139,27 @@ func NewDockerCli(in io.ReadCloser, out, err io.Writer, clientFlags *cli.ClientF cli.outFd, cli.isTerminalOut = term.GetFdInfo(cli.out) } - // The transport is created here for reuse during the client session. - cli.transport = &http.Transport{ - TLSClientConfig: cli.tlsConfig, - } - sockets.ConfigureTCPTransport(cli.transport, cli.proto, cli.addr) - - configFile, e := cliconfig.Load(cliconfig.ConfigDir()) - if e != nil { - fmt.Fprintf(cli.err, "WARNING: Error loading config file:%v\n", e) - } - cli.configFile = configFile - return nil } return cli } + +func getServerHost(hosts []string, tlsOptions *tlsconfig.Options) (host string, err error) { + switch len(hosts) { + case 0: + host = os.Getenv("DOCKER_HOST") + case 1: + host = hosts[0] + default: + return "", errors.New("Please specify only one -H") + } + + defaultHost := opts.DefaultTCPHost + if tlsOptions != nil { + defaultHost = opts.DefaultTLSHost + } + + host, err = opts.ParseHost(defaultHost, host) + return +} diff --git a/api/client/lib/client.go b/api/client/lib/client.go new file mode 100644 index 0000000000..dbb088a9ca --- /dev/null +++ b/api/client/lib/client.go @@ -0,0 +1,98 @@ +package lib + +import ( + "crypto/tls" + "fmt" + "net/http" + "net/url" + "strings" + + "github.com/docker/docker/api" + "github.com/docker/docker/pkg/sockets" + "github.com/docker/docker/pkg/tlsconfig" + "github.com/docker/docker/pkg/version" +) + +// Client is the API client that performs all operations +// against a docker server. +type Client struct { + // proto holds the client protocol i.e. unix. + Proto string + // addr holds the client address. + Addr string + // basePath holds the path to prepend to the requests + BasePath string + // scheme holds the scheme of the client i.e. https. + Scheme string + // httpClient holds the client transport instance. Exported to keep the old code running. + HTTPClient *http.Client + // version of the server to talk to. + version version.Version + // custom http headers configured by users + customHTTPHeaders map[string]string +} + +// NewClient initializes a new API client +// for the given host. It uses the tlsOptions +// to decide whether to use a secure connection or not. +// It also initializes the custom http headers to add to each request. +func NewClient(host string, tlsOptions *tlsconfig.Options, httpHeaders map[string]string) (*Client, error) { + return NewClientWithVersion(host, api.Version, tlsOptions, httpHeaders) +} + +// NewClientWithVersion initializes a new API client +// for the given host and API version. It uses the tlsOptions +// to decide whether to use a secure connection or not. +// It also initializes the custom http headers to add to each request. +func NewClientWithVersion(host string, version version.Version, tlsOptions *tlsconfig.Options, httpHeaders map[string]string) (*Client, error) { + var ( + basePath string + tlsConfig *tls.Config + scheme = "http" + protoAddrParts = strings.SplitN(host, "://", 2) + proto, addr = protoAddrParts[0], protoAddrParts[1] + ) + + if proto == "tcp" { + parsed, err := url.Parse("tcp://" + addr) + if err != nil { + return nil, err + } + addr = parsed.Host + basePath = parsed.Path + } + + if tlsOptions != nil { + scheme = "https" + var err error + tlsConfig, err = tlsconfig.Client(*tlsOptions) + if err != nil { + return nil, err + } + } + + // The transport is created here for reuse during the client session. + transport := &http.Transport{ + TLSClientConfig: tlsConfig, + } + sockets.ConfigureTCPTransport(transport, proto, addr) + + return &Client{ + Addr: addr, + BasePath: basePath, + Scheme: scheme, + HTTPClient: &http.Client{Transport: transport}, + version: version, + customHTTPHeaders: httpHeaders, + }, nil +} + +// getAPIPath returns the versioned request path to call the api. +// It appends the query parameters to the path if they are not empty. +func (cli *Client) getAPIPath(p string, query url.Values) string { + apiPath := fmt.Sprintf("%s/v%s%s", cli.BasePath, cli.version, p) + if len(query) > 0 { + apiPath += "?" + query.Encode() + } + return apiPath +} diff --git a/api/client/lib/request.go b/api/client/lib/request.go new file mode 100644 index 0000000000..328b087a50 --- /dev/null +++ b/api/client/lib/request.go @@ -0,0 +1,155 @@ +package lib + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "net/http" + "net/url" + "strings" + + "github.com/docker/docker/utils" +) + +// ServerResponse is a wrapper for http API responses. +type ServerResponse struct { + body io.ReadCloser + header http.Header + statusCode int +} + +// HEAD sends an http request to the docker API using the method HEAD. +func (cli *Client) HEAD(path string, query url.Values, headers map[string][]string) (*ServerResponse, error) { + return cli.sendRequest("HEAD", path, query, nil, headers) +} + +// GET sends an http request to the docker API using the method GET. +func (cli *Client) GET(path string, query url.Values, headers map[string][]string) (*ServerResponse, error) { + return cli.sendRequest("GET", path, query, nil, headers) +} + +// POST sends an http request to the docker API using the method POST. +func (cli *Client) POST(path string, query url.Values, body interface{}, headers map[string][]string) (*ServerResponse, error) { + return cli.sendRequest("POST", path, query, body, headers) +} + +// POSTRaw sends the raw input to the docker API using the method POST. +func (cli *Client) POSTRaw(path string, query url.Values, body io.Reader, headers map[string][]string) (*ServerResponse, error) { + return cli.sendClientRequest("POST", path, query, body, headers) +} + +// PUT sends an http request to the docker API using the method PUT. +func (cli *Client) PUT(path string, query url.Values, body interface{}, headers map[string][]string) (*ServerResponse, error) { + return cli.sendRequest("PUT", path, query, body, headers) +} + +// DELETE sends an http request to the docker API using the method DELETE. +func (cli *Client) DELETE(path string, query url.Values, headers map[string][]string) (*ServerResponse, error) { + return cli.sendRequest("DELETE", path, query, nil, headers) +} + +func (cli *Client) sendRequest(method, path string, query url.Values, body interface{}, headers map[string][]string) (*ServerResponse, error) { + params, err := encodeData(body) + if err != nil { + return nil, err + } + + if body != nil { + if headers == nil { + headers = make(map[string][]string) + } + headers["Content-Type"] = []string{"application/json"} + } + + return cli.sendClientRequest(method, path, query, params, headers) +} + +func (cli *Client) sendClientRequest(method, path string, query url.Values, in io.Reader, headers map[string][]string) (*ServerResponse, error) { + serverResp := &ServerResponse{ + body: nil, + statusCode: -1, + } + + expectedPayload := (method == "POST" || method == "PUT") + if expectedPayload && in == nil { + in = bytes.NewReader([]byte{}) + } + + apiPath := cli.getAPIPath(path, query) + req, err := http.NewRequest(method, apiPath, in) + if err != nil { + return serverResp, err + } + + // Add CLI Config's HTTP Headers BEFORE we set the Docker headers + // then the user can't change OUR headers + for k, v := range cli.customHTTPHeaders { + req.Header.Set(k, v) + } + + req.URL.Host = cli.Addr + req.URL.Scheme = cli.Scheme + + if headers != nil { + for k, v := range headers { + req.Header[k] = v + } + } + + if expectedPayload && req.Header.Get("Content-Type") == "" { + req.Header.Set("Content-Type", "text/plain") + } + + resp, err := cli.HTTPClient.Do(req) + if resp != nil { + serverResp.statusCode = resp.StatusCode + } + + if err != nil { + if utils.IsTimeout(err) || strings.Contains(err.Error(), "connection refused") || strings.Contains(err.Error(), "dial unix") { + return serverResp, errConnectionFailed + } + + if cli.Scheme == "http" && strings.Contains(err.Error(), "malformed HTTP response") { + return serverResp, fmt.Errorf("%v.\n* Are you trying to connect to a TLS-enabled daemon without TLS?", err) + } + if cli.Scheme == "https" && strings.Contains(err.Error(), "remote error: bad certificate") { + return serverResp, fmt.Errorf("The server probably has client authentication (--tlsverify) enabled. Please check your TLS client certification settings: %v", err) + } + + return serverResp, fmt.Errorf("An error occurred trying to connect: %v", err) + } + + if serverResp.statusCode < 200 || serverResp.statusCode >= 400 { + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return serverResp, err + } + if len(body) == 0 { + 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) + } + return serverResp, fmt.Errorf("Error response from daemon: %s", bytes.TrimSpace(body)) + } + + serverResp.body = resp.Body + serverResp.header = resp.Header + return serverResp, nil +} + +func encodeData(data interface{}) (*bytes.Buffer, error) { + params := bytes.NewBuffer(nil) + if data != nil { + if err := json.NewEncoder(params).Encode(data); err != nil { + return nil, err + } + } + return params, nil +} + +func ensureReaderClosed(response *ServerResponse) { + if response != nil && response.body != nil { + response.body.Close() + } +} diff --git a/cliconfig/config.go b/cliconfig/config.go index 87b92acdf8..b28d6e5bcf 100644 --- a/cliconfig/config.go +++ b/cliconfig/config.go @@ -192,7 +192,14 @@ func Load(configDir string) (*ConfigFile, error) { } defer file.Close() err = configFile.LegacyLoadFromReader(file) - return &configFile, err + if err != nil { + return &configFile, err + } + + if configFile.HTTPHeaders == nil { + configFile.HTTPHeaders = map[string]string{} + } + return &configFile, nil } // SaveToWriter encodes and writes out all the authorization information to