1package api
2
3import (
4	"bytes"
5	"compress/gzip"
6	"crypto/tls"
7	"encoding/json"
8	"fmt"
9	"io"
10	"net"
11	"net/http"
12	"net/url"
13	"os"
14	"strconv"
15	"strings"
16	"time"
17
18	cleanhttp "github.com/hashicorp/go-cleanhttp"
19	rootcerts "github.com/hashicorp/go-rootcerts"
20)
21
22var (
23	// ClientConnTimeout is the timeout applied when attempting to contact a
24	// client directly before switching to a connection through the Nomad
25	// server.
26	ClientConnTimeout = 1 * time.Second
27)
28
29// QueryOptions are used to parametrize a query
30type QueryOptions struct {
31	// Providing a datacenter overwrites the region provided
32	// by the Config
33	Region string
34
35	// Namespace is the target namespace for the query.
36	Namespace string
37
38	// AllowStale allows any Nomad server (non-leader) to service
39	// a read. This allows for lower latency and higher throughput
40	AllowStale bool
41
42	// WaitIndex is used to enable a blocking query. Waits
43	// until the timeout or the next index is reached
44	WaitIndex uint64
45
46	// WaitTime is used to bound the duration of a wait.
47	// Defaults to that of the Config, but can be overridden.
48	WaitTime time.Duration
49
50	// If set, used as prefix for resource list searches
51	Prefix string
52
53	// Set HTTP parameters on the query.
54	Params map[string]string
55
56	// AuthToken is the secret ID of an ACL token
57	AuthToken string
58}
59
60// WriteOptions are used to parametrize a write
61type WriteOptions struct {
62	// Providing a datacenter overwrites the region provided
63	// by the Config
64	Region string
65
66	// Namespace is the target namespace for the write.
67	Namespace string
68
69	// AuthToken is the secret ID of an ACL token
70	AuthToken string
71}
72
73// QueryMeta is used to return meta data about a query
74type QueryMeta struct {
75	// LastIndex. This can be used as a WaitIndex to perform
76	// a blocking query
77	LastIndex uint64
78
79	// Time of last contact from the leader for the
80	// server servicing the request
81	LastContact time.Duration
82
83	// Is there a known leader
84	KnownLeader bool
85
86	// How long did the request take
87	RequestTime time.Duration
88}
89
90// WriteMeta is used to return meta data about a write
91type WriteMeta struct {
92	// LastIndex. This can be used as a WaitIndex to perform
93	// a blocking query
94	LastIndex uint64
95
96	// How long did the request take
97	RequestTime time.Duration
98}
99
100// HttpBasicAuth is used to authenticate http client with HTTP Basic Authentication
101type HttpBasicAuth struct {
102	// Username to use for HTTP Basic Authentication
103	Username string
104
105	// Password to use for HTTP Basic Authentication
106	Password string
107}
108
109// Config is used to configure the creation of a client
110type Config struct {
111	// Address is the address of the Nomad agent
112	Address string
113
114	// Region to use. If not provided, the default agent region is used.
115	Region string
116
117	// SecretID to use. This can be overwritten per request.
118	SecretID string
119
120	// Namespace to use. If not provided the default namespace is used.
121	Namespace string
122
123	// httpClient is the client to use. Default will be used if not provided.
124	httpClient *http.Client
125
126	// HttpAuth is the auth info to use for http access.
127	HttpAuth *HttpBasicAuth
128
129	// WaitTime limits how long a Watch will block. If not provided,
130	// the agent default values will be used.
131	WaitTime time.Duration
132
133	// TLSConfig provides the various TLS related configurations for the http
134	// client
135	TLSConfig *TLSConfig
136}
137
138// ClientConfig copies the configuration with a new client address, region, and
139// whether the client has TLS enabled.
140func (c *Config) ClientConfig(region, address string, tlsEnabled bool) *Config {
141	scheme := "http"
142	if tlsEnabled {
143		scheme = "https"
144	}
145	defaultConfig := DefaultConfig()
146	config := &Config{
147		Address:    fmt.Sprintf("%s://%s", scheme, address),
148		Region:     region,
149		Namespace:  c.Namespace,
150		httpClient: defaultConfig.httpClient,
151		SecretID:   c.SecretID,
152		HttpAuth:   c.HttpAuth,
153		WaitTime:   c.WaitTime,
154		TLSConfig:  c.TLSConfig.Copy(),
155	}
156
157	// Update the tls server name for connecting to a client
158	if tlsEnabled && config.TLSConfig != nil {
159		config.TLSConfig.TLSServerName = fmt.Sprintf("client.%s.nomad", region)
160	}
161
162	return config
163}
164
165// TLSConfig contains the parameters needed to configure TLS on the HTTP client
166// used to communicate with Nomad.
167type TLSConfig struct {
168	// CACert is the path to a PEM-encoded CA cert file to use to verify the
169	// Nomad server SSL certificate.
170	CACert string
171
172	// CAPath is the path to a directory of PEM-encoded CA cert files to verify
173	// the Nomad server SSL certificate.
174	CAPath string
175
176	// ClientCert is the path to the certificate for Nomad communication
177	ClientCert string
178
179	// ClientKey is the path to the private key for Nomad communication
180	ClientKey string
181
182	// TLSServerName, if set, is used to set the SNI host when connecting via
183	// TLS.
184	TLSServerName string
185
186	// Insecure enables or disables SSL verification
187	Insecure bool
188}
189
190func (t *TLSConfig) Copy() *TLSConfig {
191	if t == nil {
192		return nil
193	}
194
195	nt := new(TLSConfig)
196	*nt = *t
197	return nt
198}
199
200// DefaultConfig returns a default configuration for the client
201func DefaultConfig() *Config {
202	config := &Config{
203		Address:    "http://127.0.0.1:4646",
204		httpClient: cleanhttp.DefaultClient(),
205		TLSConfig:  &TLSConfig{},
206	}
207	transport := config.httpClient.Transport.(*http.Transport)
208	transport.TLSHandshakeTimeout = 10 * time.Second
209	transport.TLSClientConfig = &tls.Config{
210		MinVersion: tls.VersionTLS12,
211	}
212
213	if addr := os.Getenv("NOMAD_ADDR"); addr != "" {
214		config.Address = addr
215	}
216	if v := os.Getenv("NOMAD_REGION"); v != "" {
217		config.Region = v
218	}
219	if v := os.Getenv("NOMAD_NAMESPACE"); v != "" {
220		config.Namespace = v
221	}
222	if auth := os.Getenv("NOMAD_HTTP_AUTH"); auth != "" {
223		var username, password string
224		if strings.Contains(auth, ":") {
225			split := strings.SplitN(auth, ":", 2)
226			username = split[0]
227			password = split[1]
228		} else {
229			username = auth
230		}
231
232		config.HttpAuth = &HttpBasicAuth{
233			Username: username,
234			Password: password,
235		}
236	}
237
238	// Read TLS specific env vars
239	if v := os.Getenv("NOMAD_CACERT"); v != "" {
240		config.TLSConfig.CACert = v
241	}
242	if v := os.Getenv("NOMAD_CAPATH"); v != "" {
243		config.TLSConfig.CAPath = v
244	}
245	if v := os.Getenv("NOMAD_CLIENT_CERT"); v != "" {
246		config.TLSConfig.ClientCert = v
247	}
248	if v := os.Getenv("NOMAD_CLIENT_KEY"); v != "" {
249		config.TLSConfig.ClientKey = v
250	}
251	if v := os.Getenv("NOMAD_SKIP_VERIFY"); v != "" {
252		if insecure, err := strconv.ParseBool(v); err == nil {
253			config.TLSConfig.Insecure = insecure
254		}
255	}
256	if v := os.Getenv("NOMAD_TOKEN"); v != "" {
257		config.SecretID = v
258	}
259	return config
260}
261
262// SetTimeout is used to place a timeout for connecting to Nomad. A negative
263// duration is ignored, a duration of zero means no timeout, and any other value
264// will add a timeout.
265func (c *Config) SetTimeout(t time.Duration) error {
266	if c == nil {
267		return fmt.Errorf("nil config")
268	} else if c.httpClient == nil {
269		return fmt.Errorf("nil HTTP client")
270	} else if c.httpClient.Transport == nil {
271		return fmt.Errorf("nil HTTP client transport")
272	}
273
274	// Apply a timeout.
275	if t.Nanoseconds() >= 0 {
276		transport, ok := c.httpClient.Transport.(*http.Transport)
277		if !ok {
278			return fmt.Errorf("unexpected HTTP transport: %T", c.httpClient.Transport)
279		}
280
281		transport.DialContext = (&net.Dialer{
282			Timeout:   t,
283			KeepAlive: 30 * time.Second,
284		}).DialContext
285	}
286
287	return nil
288}
289
290// ConfigureTLS applies a set of TLS configurations to the the HTTP client.
291func (c *Config) ConfigureTLS() error {
292	if c.TLSConfig == nil {
293		return nil
294	}
295	if c.httpClient == nil {
296		return fmt.Errorf("config HTTP Client must be set")
297	}
298
299	var clientCert tls.Certificate
300	foundClientCert := false
301	if c.TLSConfig.ClientCert != "" || c.TLSConfig.ClientKey != "" {
302		if c.TLSConfig.ClientCert != "" && c.TLSConfig.ClientKey != "" {
303			var err error
304			clientCert, err = tls.LoadX509KeyPair(c.TLSConfig.ClientCert, c.TLSConfig.ClientKey)
305			if err != nil {
306				return err
307			}
308			foundClientCert = true
309		} else {
310			return fmt.Errorf("Both client cert and client key must be provided")
311		}
312	}
313
314	clientTLSConfig := c.httpClient.Transport.(*http.Transport).TLSClientConfig
315	rootConfig := &rootcerts.Config{
316		CAFile: c.TLSConfig.CACert,
317		CAPath: c.TLSConfig.CAPath,
318	}
319	if err := rootcerts.ConfigureTLS(clientTLSConfig, rootConfig); err != nil {
320		return err
321	}
322
323	clientTLSConfig.InsecureSkipVerify = c.TLSConfig.Insecure
324
325	if foundClientCert {
326		clientTLSConfig.Certificates = []tls.Certificate{clientCert}
327	}
328	if c.TLSConfig.TLSServerName != "" {
329		clientTLSConfig.ServerName = c.TLSConfig.TLSServerName
330	}
331
332	return nil
333}
334
335// Client provides a client to the Nomad API
336type Client struct {
337	config Config
338}
339
340// NewClient returns a new client
341func NewClient(config *Config) (*Client, error) {
342	// bootstrap the config
343	defConfig := DefaultConfig()
344
345	if config.Address == "" {
346		config.Address = defConfig.Address
347	} else if _, err := url.Parse(config.Address); err != nil {
348		return nil, fmt.Errorf("invalid address '%s': %v", config.Address, err)
349	}
350
351	if config.httpClient == nil {
352		config.httpClient = defConfig.httpClient
353	}
354
355	// Configure the TLS configurations
356	if err := config.ConfigureTLS(); err != nil {
357		return nil, err
358	}
359
360	client := &Client{
361		config: *config,
362	}
363	return client, nil
364}
365
366// Address return the address of the Nomad agent
367func (c *Client) Address() string {
368	return c.config.Address
369}
370
371// SetRegion sets the region to forward API requests to.
372func (c *Client) SetRegion(region string) {
373	c.config.Region = region
374}
375
376// SetNamespace sets the namespace to forward API requests to.
377func (c *Client) SetNamespace(namespace string) {
378	c.config.Namespace = namespace
379}
380
381// GetNodeClient returns a new Client that will dial the specified node. If the
382// QueryOptions is set, its region will be used.
383func (c *Client) GetNodeClient(nodeID string, q *QueryOptions) (*Client, error) {
384	return c.getNodeClientImpl(nodeID, -1, q, c.Nodes().Info)
385}
386
387// GetNodeClientWithTimeout returns a new Client that will dial the specified
388// node using the specified timeout. If the QueryOptions is set, its region will
389// be used.
390func (c *Client) GetNodeClientWithTimeout(
391	nodeID string, timeout time.Duration, q *QueryOptions) (*Client, error) {
392	return c.getNodeClientImpl(nodeID, timeout, q, c.Nodes().Info)
393}
394
395// nodeLookup is the definition of a function used to lookup a node. This is
396// largely used to mock the lookup in tests.
397type nodeLookup func(nodeID string, q *QueryOptions) (*Node, *QueryMeta, error)
398
399// getNodeClientImpl is the implementation of creating a API client for
400// contacting a node. It takes a function to lookup the node such that it can be
401// mocked during tests.
402func (c *Client) getNodeClientImpl(nodeID string, timeout time.Duration, q *QueryOptions, lookup nodeLookup) (*Client, error) {
403	node, _, err := lookup(nodeID, q)
404	if err != nil {
405		return nil, err
406	}
407	if node.Status == "down" {
408		return nil, NodeDownErr
409	}
410	if node.HTTPAddr == "" {
411		return nil, fmt.Errorf("http addr of node %q (%s) is not advertised", node.Name, nodeID)
412	}
413
414	var region string
415	switch {
416	case q != nil && q.Region != "":
417		// Prefer the region set in the query parameter
418		region = q.Region
419	case c.config.Region != "":
420		// If the client is configured for a particular region use that
421		region = c.config.Region
422	default:
423		// No region information is given so use the default.
424		region = "global"
425	}
426
427	// Get an API client for the node
428	conf := c.config.ClientConfig(region, node.HTTPAddr, node.TLSEnabled)
429
430	// Set the timeout
431	conf.SetTimeout(timeout)
432
433	return NewClient(conf)
434}
435
436// SetSecretID sets the ACL token secret for API requests.
437func (c *Client) SetSecretID(secretID string) {
438	c.config.SecretID = secretID
439}
440
441// request is used to help build up a request
442type request struct {
443	config *Config
444	method string
445	url    *url.URL
446	params url.Values
447	token  string
448	body   io.Reader
449	obj    interface{}
450}
451
452// setQueryOptions is used to annotate the request with
453// additional query options
454func (r *request) setQueryOptions(q *QueryOptions) {
455	if q == nil {
456		return
457	}
458	if q.Region != "" {
459		r.params.Set("region", q.Region)
460	}
461	if q.Namespace != "" {
462		r.params.Set("namespace", q.Namespace)
463	}
464	if q.AuthToken != "" {
465		r.token = q.AuthToken
466	}
467	if q.AllowStale {
468		r.params.Set("stale", "")
469	}
470	if q.WaitIndex != 0 {
471		r.params.Set("index", strconv.FormatUint(q.WaitIndex, 10))
472	}
473	if q.WaitTime != 0 {
474		r.params.Set("wait", durToMsec(q.WaitTime))
475	}
476	if q.Prefix != "" {
477		r.params.Set("prefix", q.Prefix)
478	}
479	for k, v := range q.Params {
480		r.params.Set(k, v)
481	}
482}
483
484// durToMsec converts a duration to a millisecond specified string
485func durToMsec(dur time.Duration) string {
486	return fmt.Sprintf("%dms", dur/time.Millisecond)
487}
488
489// setWriteOptions is used to annotate the request with
490// additional write options
491func (r *request) setWriteOptions(q *WriteOptions) {
492	if q == nil {
493		return
494	}
495	if q.Region != "" {
496		r.params.Set("region", q.Region)
497	}
498	if q.Namespace != "" {
499		r.params.Set("namespace", q.Namespace)
500	}
501	if q.AuthToken != "" {
502		r.token = q.AuthToken
503	}
504}
505
506// toHTTP converts the request to an HTTP request
507func (r *request) toHTTP() (*http.Request, error) {
508	// Encode the query parameters
509	r.url.RawQuery = r.params.Encode()
510
511	// Check if we should encode the body
512	if r.body == nil && r.obj != nil {
513		if b, err := encodeBody(r.obj); err != nil {
514			return nil, err
515		} else {
516			r.body = b
517		}
518	}
519
520	// Create the HTTP request
521	req, err := http.NewRequest(r.method, r.url.RequestURI(), r.body)
522	if err != nil {
523		return nil, err
524	}
525
526	// Optionally configure HTTP basic authentication
527	if r.url.User != nil {
528		username := r.url.User.Username()
529		password, _ := r.url.User.Password()
530		req.SetBasicAuth(username, password)
531	} else if r.config.HttpAuth != nil {
532		req.SetBasicAuth(r.config.HttpAuth.Username, r.config.HttpAuth.Password)
533	}
534
535	req.Header.Add("Accept-Encoding", "gzip")
536	if r.token != "" {
537		req.Header.Set("X-Nomad-Token", r.token)
538	}
539
540	req.URL.Host = r.url.Host
541	req.URL.Scheme = r.url.Scheme
542	req.Host = r.url.Host
543	return req, nil
544}
545
546// newRequest is used to create a new request
547func (c *Client) newRequest(method, path string) (*request, error) {
548	base, _ := url.Parse(c.config.Address)
549	u, err := url.Parse(path)
550	if err != nil {
551		return nil, err
552	}
553	r := &request{
554		config: &c.config,
555		method: method,
556		url: &url.URL{
557			Scheme: base.Scheme,
558			User:   base.User,
559			Host:   base.Host,
560			Path:   u.Path,
561		},
562		params: make(map[string][]string),
563	}
564	if c.config.Region != "" {
565		r.params.Set("region", c.config.Region)
566	}
567	if c.config.Namespace != "" {
568		r.params.Set("namespace", c.config.Namespace)
569	}
570	if c.config.WaitTime != 0 {
571		r.params.Set("wait", durToMsec(r.config.WaitTime))
572	}
573	if c.config.SecretID != "" {
574		r.token = r.config.SecretID
575	}
576
577	// Add in the query parameters, if any
578	for key, values := range u.Query() {
579		for _, value := range values {
580			r.params.Add(key, value)
581		}
582	}
583
584	return r, nil
585}
586
587// multiCloser is to wrap a ReadCloser such that when close is called, multiple
588// Closes occur.
589type multiCloser struct {
590	reader       io.Reader
591	inorderClose []io.Closer
592}
593
594func (m *multiCloser) Close() error {
595	for _, c := range m.inorderClose {
596		if err := c.Close(); err != nil {
597			return err
598		}
599	}
600	return nil
601}
602
603func (m *multiCloser) Read(p []byte) (int, error) {
604	return m.reader.Read(p)
605}
606
607// doRequest runs a request with our client
608func (c *Client) doRequest(r *request) (time.Duration, *http.Response, error) {
609	req, err := r.toHTTP()
610	if err != nil {
611		return 0, nil, err
612	}
613	start := time.Now()
614	resp, err := c.config.httpClient.Do(req)
615	diff := time.Now().Sub(start)
616
617	// If the response is compressed, we swap the body's reader.
618	if resp != nil && resp.Header != nil {
619		var reader io.ReadCloser
620		switch resp.Header.Get("Content-Encoding") {
621		case "gzip":
622			greader, err := gzip.NewReader(resp.Body)
623			if err != nil {
624				return 0, nil, err
625			}
626
627			// The gzip reader doesn't close the wrapped reader so we use
628			// multiCloser.
629			reader = &multiCloser{
630				reader:       greader,
631				inorderClose: []io.Closer{greader, resp.Body},
632			}
633		default:
634			reader = resp.Body
635		}
636		resp.Body = reader
637	}
638
639	return diff, resp, err
640}
641
642// rawQuery makes a GET request to the specified endpoint but returns just the
643// response body.
644func (c *Client) rawQuery(endpoint string, q *QueryOptions) (io.ReadCloser, error) {
645	r, err := c.newRequest("GET", endpoint)
646	if err != nil {
647		return nil, err
648	}
649	r.setQueryOptions(q)
650	_, resp, err := requireOK(c.doRequest(r))
651	if err != nil {
652		return nil, err
653	}
654
655	return resp.Body, nil
656}
657
658// query is used to do a GET request against an endpoint
659// and deserialize the response into an interface using
660// standard Nomad conventions.
661func (c *Client) query(endpoint string, out interface{}, q *QueryOptions) (*QueryMeta, error) {
662	r, err := c.newRequest("GET", endpoint)
663	if err != nil {
664		return nil, err
665	}
666	r.setQueryOptions(q)
667	rtt, resp, err := requireOK(c.doRequest(r))
668	if err != nil {
669		return nil, err
670	}
671	defer resp.Body.Close()
672
673	qm := &QueryMeta{}
674	parseQueryMeta(resp, qm)
675	qm.RequestTime = rtt
676
677	if err := decodeBody(resp, out); err != nil {
678		return nil, err
679	}
680	return qm, nil
681}
682
683// putQuery is used to do a PUT request when doing a read against an endpoint
684// and deserialize the response into an interface using standard Nomad
685// conventions.
686func (c *Client) putQuery(endpoint string, in, out interface{}, q *QueryOptions) (*QueryMeta, error) {
687	r, err := c.newRequest("PUT", endpoint)
688	if err != nil {
689		return nil, err
690	}
691	r.setQueryOptions(q)
692	r.obj = in
693	rtt, resp, err := requireOK(c.doRequest(r))
694	if err != nil {
695		return nil, err
696	}
697	defer resp.Body.Close()
698
699	qm := &QueryMeta{}
700	parseQueryMeta(resp, qm)
701	qm.RequestTime = rtt
702
703	if err := decodeBody(resp, out); err != nil {
704		return nil, err
705	}
706	return qm, nil
707}
708
709// write is used to do a PUT request against an endpoint
710// and serialize/deserialized using the standard Nomad conventions.
711func (c *Client) write(endpoint string, in, out interface{}, q *WriteOptions) (*WriteMeta, error) {
712	r, err := c.newRequest("PUT", endpoint)
713	if err != nil {
714		return nil, err
715	}
716	r.setWriteOptions(q)
717	r.obj = in
718	rtt, resp, err := requireOK(c.doRequest(r))
719	if err != nil {
720		return nil, err
721	}
722	defer resp.Body.Close()
723
724	wm := &WriteMeta{RequestTime: rtt}
725	parseWriteMeta(resp, wm)
726
727	if out != nil {
728		if err := decodeBody(resp, &out); err != nil {
729			return nil, err
730		}
731	}
732	return wm, nil
733}
734
735// delete is used to do a DELETE request against an endpoint
736// and serialize/deserialized using the standard Nomad conventions.
737func (c *Client) delete(endpoint string, out interface{}, q *WriteOptions) (*WriteMeta, error) {
738	r, err := c.newRequest("DELETE", endpoint)
739	if err != nil {
740		return nil, err
741	}
742	r.setWriteOptions(q)
743	rtt, resp, err := requireOK(c.doRequest(r))
744	if err != nil {
745		return nil, err
746	}
747	defer resp.Body.Close()
748
749	wm := &WriteMeta{RequestTime: rtt}
750	parseWriteMeta(resp, wm)
751
752	if out != nil {
753		if err := decodeBody(resp, &out); err != nil {
754			return nil, err
755		}
756	}
757	return wm, nil
758}
759
760// parseQueryMeta is used to help parse query meta-data
761func parseQueryMeta(resp *http.Response, q *QueryMeta) error {
762	header := resp.Header
763
764	// Parse the X-Nomad-Index
765	index, err := strconv.ParseUint(header.Get("X-Nomad-Index"), 10, 64)
766	if err != nil {
767		return fmt.Errorf("Failed to parse X-Nomad-Index: %v", err)
768	}
769	q.LastIndex = index
770
771	// Parse the X-Nomad-LastContact
772	last, err := strconv.ParseUint(header.Get("X-Nomad-LastContact"), 10, 64)
773	if err != nil {
774		return fmt.Errorf("Failed to parse X-Nomad-LastContact: %v", err)
775	}
776	q.LastContact = time.Duration(last) * time.Millisecond
777
778	// Parse the X-Nomad-KnownLeader
779	switch header.Get("X-Nomad-KnownLeader") {
780	case "true":
781		q.KnownLeader = true
782	default:
783		q.KnownLeader = false
784	}
785	return nil
786}
787
788// parseWriteMeta is used to help parse write meta-data
789func parseWriteMeta(resp *http.Response, q *WriteMeta) error {
790	header := resp.Header
791
792	// Parse the X-Nomad-Index
793	index, err := strconv.ParseUint(header.Get("X-Nomad-Index"), 10, 64)
794	if err != nil {
795		return fmt.Errorf("Failed to parse X-Nomad-Index: %v", err)
796	}
797	q.LastIndex = index
798	return nil
799}
800
801// decodeBody is used to JSON decode a body
802func decodeBody(resp *http.Response, out interface{}) error {
803	dec := json.NewDecoder(resp.Body)
804	return dec.Decode(out)
805}
806
807// encodeBody is used to encode a request body
808func encodeBody(obj interface{}) (io.Reader, error) {
809	buf := bytes.NewBuffer(nil)
810	enc := json.NewEncoder(buf)
811	if err := enc.Encode(obj); err != nil {
812		return nil, err
813	}
814	return buf, nil
815}
816
817// requireOK is used to wrap doRequest and check for a 200
818func requireOK(d time.Duration, resp *http.Response, e error) (time.Duration, *http.Response, error) {
819	if e != nil {
820		if resp != nil {
821			resp.Body.Close()
822		}
823		return d, nil, e
824	}
825	if resp.StatusCode != 200 {
826		var buf bytes.Buffer
827		io.Copy(&buf, resp.Body)
828		resp.Body.Close()
829		return d, nil, fmt.Errorf("Unexpected response code: %d (%s)", resp.StatusCode, buf.Bytes())
830	}
831	return d, resp, nil
832}
833