package request import ( "bufio" "bytes" "crypto/tls" "encoding/json" "fmt" "io" "io/ioutil" "net" "net/http" "net/http/httputil" "net/url" "os" "path/filepath" "strings" "time" "github.com/docker/docker/api" dclient "github.com/docker/docker/client" "github.com/docker/docker/opts" "github.com/docker/docker/pkg/ioutils" "github.com/docker/go-connections/sockets" "github.com/docker/go-connections/tlsconfig" "github.com/pkg/errors" ) // Method creates a modifier that sets the specified string as the request method func Method(method string) func(*http.Request) error { return func(req *http.Request) error { req.Method = method return nil } } // RawString sets the specified string as body for the request func RawString(content string) func(*http.Request) error { return RawContent(ioutil.NopCloser(strings.NewReader(content))) } // RawContent sets the specified reader as body for the request func RawContent(reader io.ReadCloser) func(*http.Request) error { return func(req *http.Request) error { req.Body = reader return nil } } // ContentType sets the specified Content-Type request header func ContentType(contentType string) func(*http.Request) error { return func(req *http.Request) error { req.Header.Set("Content-Type", contentType) return nil } } // JSON sets the Content-Type request header to json func JSON(req *http.Request) error { return ContentType("application/json")(req) } // JSONBody creates a modifier that encodes the specified data to a JSON string and set it as request body. It also sets // the Content-Type header of the request. func JSONBody(data interface{}) func(*http.Request) error { return func(req *http.Request) error { jsonData := bytes.NewBuffer(nil) if err := json.NewEncoder(jsonData).Encode(data); err != nil { return err } req.Body = ioutil.NopCloser(jsonData) req.Header.Set("Content-Type", "application/json") return nil } } // Post creates and execute a POST request on the specified host and endpoint, with the specified request modifiers func Post(endpoint string, modifiers ...func(*http.Request) error) (*http.Response, io.ReadCloser, error) { return Do(endpoint, append(modifiers, Method(http.MethodPost))...) } // Delete creates and execute a DELETE request on the specified host and endpoint, with the specified request modifiers func Delete(endpoint string, modifiers ...func(*http.Request) error) (*http.Response, io.ReadCloser, error) { return Do(endpoint, append(modifiers, Method(http.MethodDelete))...) } // Get creates and execute a GET request on the specified host and endpoint, with the specified request modifiers func Get(endpoint string, modifiers ...func(*http.Request) error) (*http.Response, io.ReadCloser, error) { return Do(endpoint, modifiers...) } // Do creates and execute a request on the specified endpoint, with the specified request modifiers func Do(endpoint string, modifiers ...func(*http.Request) error) (*http.Response, io.ReadCloser, error) { return DoOnHost(DaemonHost(), endpoint, modifiers...) } // DoOnHost creates and execute a request on the specified host and endpoint, with the specified request modifiers func DoOnHost(host, endpoint string, modifiers ...func(*http.Request) error) (*http.Response, io.ReadCloser, error) { req, err := New(host, endpoint, modifiers...) if err != nil { return nil, nil, err } client, err := NewHTTPClient(host) if err != nil { return nil, nil, err } resp, err := client.Do(req) var body io.ReadCloser if resp != nil { body = ioutils.NewReadCloserWrapper(resp.Body, func() error { defer resp.Body.Close() return nil }) } return resp, body, err } // New creates a new http Request to the specified host and endpoint, with the specified request modifiers func New(host, endpoint string, modifiers ...func(*http.Request) error) (*http.Request, error) { _, addr, _, err := dclient.ParseHost(host) if err != nil { return nil, err } if err != nil { return nil, errors.Wrapf(err, "could not parse url %q", host) } req, err := http.NewRequest("GET", endpoint, nil) if err != nil { return nil, fmt.Errorf("could not create new request: %v", err) } if os.Getenv("DOCKER_TLS_VERIFY") != "" { req.URL.Scheme = "https" } else { req.URL.Scheme = "http" } req.URL.Host = addr for _, config := range modifiers { if err := config(req); err != nil { return nil, err } } return req, nil } // NewHTTPClient creates an http client for the specific host func NewHTTPClient(host string) (*http.Client, error) { // FIXME(vdemeester) 10*time.Second timeout of SockRequest… ? proto, addr, _, err := dclient.ParseHost(host) if err != nil { return nil, err } transport := new(http.Transport) if proto == "tcp" && os.Getenv("DOCKER_TLS_VERIFY") != "" { // Setup the socket TLS configuration. tlsConfig, err := getTLSConfig() if err != nil { return nil, err } transport = &http.Transport{TLSClientConfig: tlsConfig} } transport.DisableKeepAlives = true err = sockets.ConfigureTransport(transport, proto, addr) return &http.Client{ Transport: transport, }, err } // NewClient returns a new Docker API client func NewClient() (dclient.APIClient, error) { return NewClientForHost(DaemonHost()) } // NewClientForHost returns a Docker API client for the host func NewClientForHost(host string) (dclient.APIClient, error) { httpClient, err := NewHTTPClient(host) if err != nil { return nil, err } return dclient.NewClient(host, api.DefaultVersion, httpClient, nil) } // FIXME(vdemeester) httputil.ClientConn is deprecated, use http.Client instead (closer to actual client) // Deprecated: Use New instead of NewRequestClient // Deprecated: use request.Do (or Get, Delete, Post) instead func newRequestClient(method, endpoint string, data io.Reader, ct, daemon string, modifiers ...func(*http.Request)) (*http.Request, *httputil.ClientConn, error) { c, err := SockConn(time.Duration(10*time.Second), daemon) if err != nil { return nil, nil, fmt.Errorf("could not dial docker daemon: %v", err) } client := httputil.NewClientConn(c, nil) req, err := http.NewRequest(method, endpoint, data) if err != nil { client.Close() return nil, nil, fmt.Errorf("could not create new request: %v", err) } for _, opt := range modifiers { opt(req) } if ct != "" { req.Header.Set("Content-Type", ct) } return req, client, nil } // SockRequest create a request against the specified host (with method, endpoint and other request modifier) and // returns the status code, and the content as an byte slice // Deprecated: use request.Do instead func SockRequest(method, endpoint string, data interface{}, daemon string, modifiers ...func(*http.Request)) (int, []byte, error) { jsonData := bytes.NewBuffer(nil) if err := json.NewEncoder(jsonData).Encode(data); err != nil { return -1, nil, err } res, body, err := SockRequestRaw(method, endpoint, jsonData, "application/json", daemon, modifiers...) if err != nil { return -1, nil, err } b, err := ReadBody(body) return res.StatusCode, b, err } // ReadBody read the specified ReadCloser content and returns it func ReadBody(b io.ReadCloser) ([]byte, error) { defer b.Close() return ioutil.ReadAll(b) } // SockRequestRaw create a request against the specified host (with method, endpoint and other request modifier) and // returns the http response, the output as a io.ReadCloser // Deprecated: use request.Do (or Get, Delete, Post) instead func SockRequestRaw(method, endpoint string, data io.Reader, ct, daemon string, modifiers ...func(*http.Request)) (*http.Response, io.ReadCloser, error) { req, client, err := newRequestClient(method, endpoint, data, ct, daemon, modifiers...) if err != nil { return nil, nil, err } resp, err := client.Do(req) if err != nil { client.Close() return resp, nil, err } body := ioutils.NewReadCloserWrapper(resp.Body, func() error { defer resp.Body.Close() return client.Close() }) return resp, body, err } // SockRequestHijack creates a connection to specified host (with method, contenttype, …) and returns a hijacked connection // and the output as a `bufio.Reader` func SockRequestHijack(method, endpoint string, data io.Reader, ct string, daemon string, modifiers ...func(*http.Request)) (net.Conn, *bufio.Reader, error) { req, client, err := newRequestClient(method, endpoint, data, ct, daemon, modifiers...) if err != nil { return nil, nil, err } client.Do(req) conn, br := client.Hijack() return conn, br, nil } // SockConn opens a connection on the specified socket func SockConn(timeout time.Duration, daemon string) (net.Conn, error) { daemonURL, err := url.Parse(daemon) if err != nil { return nil, errors.Wrapf(err, "could not parse url %q", daemon) } var c net.Conn switch daemonURL.Scheme { case "npipe": return npipeDial(daemonURL.Path, timeout) case "unix": return net.DialTimeout(daemonURL.Scheme, daemonURL.Path, timeout) case "tcp": if os.Getenv("DOCKER_TLS_VERIFY") != "" { // Setup the socket TLS configuration. tlsConfig, err := getTLSConfig() if err != nil { return nil, err } dialer := &net.Dialer{Timeout: timeout} return tls.DialWithDialer(dialer, daemonURL.Scheme, daemonURL.Host, tlsConfig) } return net.DialTimeout(daemonURL.Scheme, daemonURL.Host, timeout) default: return c, errors.Errorf("unknown scheme %v (%s)", daemonURL.Scheme, daemon) } } func getTLSConfig() (*tls.Config, error) { dockerCertPath := os.Getenv("DOCKER_CERT_PATH") if dockerCertPath == "" { return nil, errors.New("DOCKER_TLS_VERIFY specified, but no DOCKER_CERT_PATH environment variable") } option := &tlsconfig.Options{ CAFile: filepath.Join(dockerCertPath, "ca.pem"), CertFile: filepath.Join(dockerCertPath, "cert.pem"), KeyFile: filepath.Join(dockerCertPath, "key.pem"), } tlsConfig, err := tlsconfig.Client(*option) if err != nil { return nil, err } return tlsConfig, nil } // DaemonHost return the daemon host string for this test execution func DaemonHost() string { daemonURLStr := "unix://" + opts.DefaultUnixSocket if daemonHostVar := os.Getenv("DOCKER_HOST"); daemonHostVar != "" { daemonURLStr = daemonHostVar } return daemonURLStr } // NewEnvClientWithVersion returns a docker client with a specified version. // See: github.com/docker/docker/client `NewEnvClient()` func NewEnvClientWithVersion(version string) (*dclient.Client, error) { if version == "" { return nil, errors.New("version not specified") } var httpClient *http.Client if os.Getenv("DOCKER_CERT_PATH") != "" { tlsConfig, err := getTLSConfig() if err != nil { return nil, err } httpClient = &http.Client{ Transport: &http.Transport{ TLSClientConfig: tlsConfig, }, } } host := os.Getenv("DOCKER_HOST") if host == "" { host = dclient.DefaultDockerHost } cli, err := dclient.NewClient(host, version, httpClient, nil) if err != nil { return cli, err } return cli, nil }