1package request // import "github.com/docker/docker/internal/test/request"
2
3import (
4	"context"
5	"crypto/tls"
6	"fmt"
7	"io"
8	"io/ioutil"
9	"net"
10	"net/http"
11	"net/url"
12	"os"
13	"path/filepath"
14	"time"
15
16	"github.com/docker/docker/client"
17	"github.com/docker/docker/internal/test"
18	"github.com/docker/docker/internal/test/environment"
19	"github.com/docker/docker/opts"
20	"github.com/docker/docker/pkg/ioutils"
21	"github.com/docker/go-connections/sockets"
22	"github.com/docker/go-connections/tlsconfig"
23	"github.com/pkg/errors"
24	"gotest.tools/assert"
25)
26
27// NewAPIClient returns a docker API client configured from environment variables
28func NewAPIClient(t assert.TestingT, ops ...func(*client.Client) error) client.APIClient {
29	if ht, ok := t.(test.HelperT); ok {
30		ht.Helper()
31	}
32	ops = append([]func(*client.Client) error{client.FromEnv}, ops...)
33	clt, err := client.NewClientWithOpts(ops...)
34	assert.NilError(t, err)
35	return clt
36}
37
38// DaemonTime provides the current time on the daemon host
39func DaemonTime(ctx context.Context, t assert.TestingT, client client.APIClient, testEnv *environment.Execution) time.Time {
40	if ht, ok := t.(test.HelperT); ok {
41		ht.Helper()
42	}
43	if testEnv.IsLocalDaemon() {
44		return time.Now()
45	}
46
47	info, err := client.Info(ctx)
48	assert.NilError(t, err)
49
50	dt, err := time.Parse(time.RFC3339Nano, info.SystemTime)
51	assert.NilError(t, err, "invalid time format in GET /info response")
52	return dt
53}
54
55// DaemonUnixTime returns the current time on the daemon host with nanoseconds precision.
56// It return the time formatted how the client sends timestamps to the server.
57func DaemonUnixTime(ctx context.Context, t assert.TestingT, client client.APIClient, testEnv *environment.Execution) string {
58	if ht, ok := t.(test.HelperT); ok {
59		ht.Helper()
60	}
61	dt := DaemonTime(ctx, t, client, testEnv)
62	return fmt.Sprintf("%d.%09d", dt.Unix(), int64(dt.Nanosecond()))
63}
64
65// Post creates and execute a POST request on the specified host and endpoint, with the specified request modifiers
66func Post(endpoint string, modifiers ...func(*Options)) (*http.Response, io.ReadCloser, error) {
67	return Do(endpoint, append(modifiers, Method(http.MethodPost))...)
68}
69
70// Delete creates and execute a DELETE request on the specified host and endpoint, with the specified request modifiers
71func Delete(endpoint string, modifiers ...func(*Options)) (*http.Response, io.ReadCloser, error) {
72	return Do(endpoint, append(modifiers, Method(http.MethodDelete))...)
73}
74
75// Get creates and execute a GET request on the specified host and endpoint, with the specified request modifiers
76func Get(endpoint string, modifiers ...func(*Options)) (*http.Response, io.ReadCloser, error) {
77	return Do(endpoint, modifiers...)
78}
79
80// Do creates and execute a request on the specified endpoint, with the specified request modifiers
81func Do(endpoint string, modifiers ...func(*Options)) (*http.Response, io.ReadCloser, error) {
82	opts := &Options{
83		host: DaemonHost(),
84	}
85	for _, mod := range modifiers {
86		mod(opts)
87	}
88	req, err := newRequest(endpoint, opts)
89	if err != nil {
90		return nil, nil, err
91	}
92	client, err := newHTTPClient(opts.host)
93	if err != nil {
94		return nil, nil, err
95	}
96	resp, err := client.Do(req)
97	var body io.ReadCloser
98	if resp != nil {
99		body = ioutils.NewReadCloserWrapper(resp.Body, func() error {
100			defer resp.Body.Close()
101			return nil
102		})
103	}
104	return resp, body, err
105}
106
107// ReadBody read the specified ReadCloser content and returns it
108func ReadBody(b io.ReadCloser) ([]byte, error) {
109	defer b.Close()
110	return ioutil.ReadAll(b)
111}
112
113// newRequest creates a new http Request to the specified host and endpoint, with the specified request modifiers
114func newRequest(endpoint string, opts *Options) (*http.Request, error) {
115	hostURL, err := client.ParseHostURL(opts.host)
116	if err != nil {
117		return nil, errors.Wrapf(err, "failed parsing url %q", opts.host)
118	}
119	req, err := http.NewRequest("GET", endpoint, nil)
120	if err != nil {
121		return nil, errors.Wrap(err, "failed to create request")
122	}
123
124	if os.Getenv("DOCKER_TLS_VERIFY") != "" {
125		req.URL.Scheme = "https"
126	} else {
127		req.URL.Scheme = "http"
128	}
129	req.URL.Host = hostURL.Host
130
131	for _, config := range opts.requestModifiers {
132		if err := config(req); err != nil {
133			return nil, err
134		}
135	}
136
137	return req, nil
138}
139
140// newHTTPClient creates an http client for the specific host
141// TODO: Share more code with client.defaultHTTPClient
142func newHTTPClient(host string) (*http.Client, error) {
143	// FIXME(vdemeester) 10*time.Second timeout of SockRequest… ?
144	hostURL, err := client.ParseHostURL(host)
145	if err != nil {
146		return nil, err
147	}
148	transport := new(http.Transport)
149	if hostURL.Scheme == "tcp" && os.Getenv("DOCKER_TLS_VERIFY") != "" {
150		// Setup the socket TLS configuration.
151		tlsConfig, err := getTLSConfig()
152		if err != nil {
153			return nil, err
154		}
155		transport = &http.Transport{TLSClientConfig: tlsConfig}
156	}
157	transport.DisableKeepAlives = true
158	err = sockets.ConfigureTransport(transport, hostURL.Scheme, hostURL.Host)
159	return &http.Client{Transport: transport}, err
160}
161
162func getTLSConfig() (*tls.Config, error) {
163	dockerCertPath := os.Getenv("DOCKER_CERT_PATH")
164
165	if dockerCertPath == "" {
166		return nil, errors.New("DOCKER_TLS_VERIFY specified, but no DOCKER_CERT_PATH environment variable")
167	}
168
169	option := &tlsconfig.Options{
170		CAFile:   filepath.Join(dockerCertPath, "ca.pem"),
171		CertFile: filepath.Join(dockerCertPath, "cert.pem"),
172		KeyFile:  filepath.Join(dockerCertPath, "key.pem"),
173	}
174	tlsConfig, err := tlsconfig.Client(*option)
175	if err != nil {
176		return nil, err
177	}
178
179	return tlsConfig, nil
180}
181
182// DaemonHost return the daemon host string for this test execution
183func DaemonHost() string {
184	daemonURLStr := "unix://" + opts.DefaultUnixSocket
185	if daemonHostVar := os.Getenv("DOCKER_HOST"); daemonHostVar != "" {
186		daemonURLStr = daemonHostVar
187	}
188	return daemonURLStr
189}
190
191// SockConn opens a connection on the specified socket
192func SockConn(timeout time.Duration, daemon string) (net.Conn, error) {
193	daemonURL, err := url.Parse(daemon)
194	if err != nil {
195		return nil, errors.Wrapf(err, "could not parse url %q", daemon)
196	}
197
198	var c net.Conn
199	switch daemonURL.Scheme {
200	case "npipe":
201		return npipeDial(daemonURL.Path, timeout)
202	case "unix":
203		return net.DialTimeout(daemonURL.Scheme, daemonURL.Path, timeout)
204	case "tcp":
205		if os.Getenv("DOCKER_TLS_VERIFY") != "" {
206			// Setup the socket TLS configuration.
207			tlsConfig, err := getTLSConfig()
208			if err != nil {
209				return nil, err
210			}
211			dialer := &net.Dialer{Timeout: timeout}
212			return tls.DialWithDialer(dialer, daemonURL.Scheme, daemonURL.Host, tlsConfig)
213		}
214		return net.DialTimeout(daemonURL.Scheme, daemonURL.Host, timeout)
215	default:
216		return c, errors.Errorf("unknown scheme %v (%s)", daemonURL.Scheme, daemon)
217	}
218}
219