1package api
2
3import (
4	"bytes"
5	"context"
6	"crypto/tls"
7	"encoding/json"
8	"fmt"
9	"io"
10	"io/ioutil"
11	"log"
12	"net"
13	"net/http"
14	"net/url"
15	"os"
16	"strconv"
17	"strings"
18	"time"
19
20	"github.com/hashicorp/go-cleanhttp"
21	"github.com/hashicorp/go-rootcerts"
22)
23
24const (
25	// HTTPAddrEnvName defines an environment variable name which sets
26	// the HTTP address if there is no -http-addr specified.
27	HTTPAddrEnvName = "CONSUL_HTTP_ADDR"
28
29	// HTTPTokenEnvName defines an environment variable name which sets
30	// the HTTP token.
31	HTTPTokenEnvName = "CONSUL_HTTP_TOKEN"
32
33	// HTTPAuthEnvName defines an environment variable name which sets
34	// the HTTP authentication header.
35	HTTPAuthEnvName = "CONSUL_HTTP_AUTH"
36
37	// HTTPSSLEnvName defines an environment variable name which sets
38	// whether or not to use HTTPS.
39	HTTPSSLEnvName = "CONSUL_HTTP_SSL"
40
41	// HTTPCAFile defines an environment variable name which sets the
42	// CA file to use for talking to Consul over TLS.
43	HTTPCAFile = "CONSUL_CACERT"
44
45	// HTTPCAPath defines an environment variable name which sets the
46	// path to a directory of CA certs to use for talking to Consul over TLS.
47	HTTPCAPath = "CONSUL_CAPATH"
48
49	// HTTPClientCert defines an environment variable name which sets the
50	// client cert file to use for talking to Consul over TLS.
51	HTTPClientCert = "CONSUL_CLIENT_CERT"
52
53	// HTTPClientKey defines an environment variable name which sets the
54	// client key file to use for talking to Consul over TLS.
55	HTTPClientKey = "CONSUL_CLIENT_KEY"
56
57	// HTTPTLSServerName defines an environment variable name which sets the
58	// server name to use as the SNI host when connecting via TLS
59	HTTPTLSServerName = "CONSUL_TLS_SERVER_NAME"
60
61	// HTTPSSLVerifyEnvName defines an environment variable name which sets
62	// whether or not to disable certificate checking.
63	HTTPSSLVerifyEnvName = "CONSUL_HTTP_SSL_VERIFY"
64
65	// GRPCAddrEnvName defines an environment variable name which sets the gRPC
66	// address for consul connect envoy. Note this isn't actually used by the api
67	// client in this package but is defined here for consistency with all the
68	// other ENV names we use.
69	GRPCAddrEnvName = "CONSUL_GRPC_ADDR"
70)
71
72// QueryOptions are used to parameterize a query
73type QueryOptions struct {
74	// Providing a datacenter overwrites the DC provided
75	// by the Config
76	Datacenter string
77
78	// AllowStale allows any Consul server (non-leader) to service
79	// a read. This allows for lower latency and higher throughput
80	AllowStale bool
81
82	// RequireConsistent forces the read to be fully consistent.
83	// This is more expensive but prevents ever performing a stale
84	// read.
85	RequireConsistent bool
86
87	// UseCache requests that the agent cache results locally. See
88	// https://www.consul.io/api/index.html#agent-caching for more details on the
89	// semantics.
90	UseCache bool
91
92	// MaxAge limits how old a cached value will be returned if UseCache is true.
93	// If there is a cached response that is older than the MaxAge, it is treated
94	// as a cache miss and a new fetch invoked. If the fetch fails, the error is
95	// returned. Clients that wish to allow for stale results on error can set
96	// StaleIfError to a longer duration to change this behavior. It is ignored
97	// if the endpoint supports background refresh caching. See
98	// https://www.consul.io/api/index.html#agent-caching for more details.
99	MaxAge time.Duration
100
101	// StaleIfError specifies how stale the client will accept a cached response
102	// if the servers are unavailable to fetch a fresh one. Only makes sense when
103	// UseCache is true and MaxAge is set to a lower, non-zero value. It is
104	// ignored if the endpoint supports background refresh caching. See
105	// https://www.consul.io/api/index.html#agent-caching for more details.
106	StaleIfError time.Duration
107
108	// WaitIndex is used to enable a blocking query. Waits
109	// until the timeout or the next index is reached
110	WaitIndex uint64
111
112	// WaitHash is used by some endpoints instead of WaitIndex to perform blocking
113	// on state based on a hash of the response rather than a monotonic index.
114	// This is required when the state being blocked on is not stored in Raft, for
115	// example agent-local proxy configuration.
116	WaitHash string
117
118	// WaitTime is used to bound the duration of a wait.
119	// Defaults to that of the Config, but can be overridden.
120	WaitTime time.Duration
121
122	// Token is used to provide a per-request ACL token
123	// which overrides the agent's default token.
124	Token string
125
126	// Near is used to provide a node name that will sort the results
127	// in ascending order based on the estimated round trip time from
128	// that node. Setting this to "_agent" will use the agent's node
129	// for the sort.
130	Near string
131
132	// NodeMeta is used to filter results by nodes with the given
133	// metadata key/value pairs. Currently, only one key/value pair can
134	// be provided for filtering.
135	NodeMeta map[string]string
136
137	// RelayFactor is used in keyring operations to cause responses to be
138	// relayed back to the sender through N other random nodes. Must be
139	// a value from 0 to 5 (inclusive).
140	RelayFactor uint8
141
142	// Connect filters prepared query execution to only include Connect-capable
143	// services. This currently affects prepared query execution.
144	Connect bool
145
146	// ctx is an optional context pass through to the underlying HTTP
147	// request layer. Use Context() and WithContext() to manage this.
148	ctx context.Context
149}
150
151func (o *QueryOptions) Context() context.Context {
152	if o != nil && o.ctx != nil {
153		return o.ctx
154	}
155	return context.Background()
156}
157
158func (o *QueryOptions) WithContext(ctx context.Context) *QueryOptions {
159	o2 := new(QueryOptions)
160	if o != nil {
161		*o2 = *o
162	}
163	o2.ctx = ctx
164	return o2
165}
166
167// WriteOptions are used to parameterize a write
168type WriteOptions struct {
169	// Providing a datacenter overwrites the DC provided
170	// by the Config
171	Datacenter string
172
173	// Token is used to provide a per-request ACL token
174	// which overrides the agent's default token.
175	Token string
176
177	// RelayFactor is used in keyring operations to cause responses to be
178	// relayed back to the sender through N other random nodes. Must be
179	// a value from 0 to 5 (inclusive).
180	RelayFactor uint8
181
182	// ctx is an optional context pass through to the underlying HTTP
183	// request layer. Use Context() and WithContext() to manage this.
184	ctx context.Context
185}
186
187func (o *WriteOptions) Context() context.Context {
188	if o != nil && o.ctx != nil {
189		return o.ctx
190	}
191	return context.Background()
192}
193
194func (o *WriteOptions) WithContext(ctx context.Context) *WriteOptions {
195	o2 := new(WriteOptions)
196	if o != nil {
197		*o2 = *o
198	}
199	o2.ctx = ctx
200	return o2
201}
202
203// QueryMeta is used to return meta data about a query
204type QueryMeta struct {
205	// LastIndex. This can be used as a WaitIndex to perform
206	// a blocking query
207	LastIndex uint64
208
209	// LastContentHash. This can be used as a WaitHash to perform a blocking query
210	// for endpoints that support hash-based blocking. Endpoints that do not
211	// support it will return an empty hash.
212	LastContentHash string
213
214	// Time of last contact from the leader for the
215	// server servicing the request
216	LastContact time.Duration
217
218	// Is there a known leader
219	KnownLeader bool
220
221	// How long did the request take
222	RequestTime time.Duration
223
224	// Is address translation enabled for HTTP responses on this agent
225	AddressTranslationEnabled bool
226
227	// CacheHit is true if the result was served from agent-local cache.
228	CacheHit bool
229
230	// CacheAge is set if request was ?cached and indicates how stale the cached
231	// response is.
232	CacheAge time.Duration
233}
234
235// WriteMeta is used to return meta data about a write
236type WriteMeta struct {
237	// How long did the request take
238	RequestTime time.Duration
239}
240
241// HttpBasicAuth is used to authenticate http client with HTTP Basic Authentication
242type HttpBasicAuth struct {
243	// Username to use for HTTP Basic Authentication
244	Username string
245
246	// Password to use for HTTP Basic Authentication
247	Password string
248}
249
250// Config is used to configure the creation of a client
251type Config struct {
252	// Address is the address of the Consul server
253	Address string
254
255	// Scheme is the URI scheme for the Consul server
256	Scheme string
257
258	// Datacenter to use. If not provided, the default agent datacenter is used.
259	Datacenter string
260
261	// Transport is the Transport to use for the http client.
262	Transport *http.Transport
263
264	// HttpClient is the client to use. Default will be
265	// used if not provided.
266	HttpClient *http.Client
267
268	// HttpAuth is the auth info to use for http access.
269	HttpAuth *HttpBasicAuth
270
271	// WaitTime limits how long a Watch will block. If not provided,
272	// the agent default values will be used.
273	WaitTime time.Duration
274
275	// Token is used to provide a per-request ACL token
276	// which overrides the agent's default token.
277	Token string
278
279	TLSConfig TLSConfig
280}
281
282// TLSConfig is used to generate a TLSClientConfig that's useful for talking to
283// Consul using TLS.
284type TLSConfig struct {
285	// Address is the optional address of the Consul server. The port, if any
286	// will be removed from here and this will be set to the ServerName of the
287	// resulting config.
288	Address string
289
290	// CAFile is the optional path to the CA certificate used for Consul
291	// communication, defaults to the system bundle if not specified.
292	CAFile string
293
294	// CAPath is the optional path to a directory of CA certificates to use for
295	// Consul communication, defaults to the system bundle if not specified.
296	CAPath string
297
298	// CertFile is the optional path to the certificate for Consul
299	// communication. If this is set then you need to also set KeyFile.
300	CertFile string
301
302	// KeyFile is the optional path to the private key for Consul communication.
303	// If this is set then you need to also set CertFile.
304	KeyFile string
305
306	// InsecureSkipVerify if set to true will disable TLS host verification.
307	InsecureSkipVerify bool
308}
309
310// DefaultConfig returns a default configuration for the client. By default this
311// will pool and reuse idle connections to Consul. If you have a long-lived
312// client object, this is the desired behavior and should make the most efficient
313// use of the connections to Consul. If you don't reuse a client object, which
314// is not recommended, then you may notice idle connections building up over
315// time. To avoid this, use the DefaultNonPooledConfig() instead.
316func DefaultConfig() *Config {
317	return defaultConfig(cleanhttp.DefaultPooledTransport)
318}
319
320// DefaultNonPooledConfig returns a default configuration for the client which
321// does not pool connections. This isn't a recommended configuration because it
322// will reconnect to Consul on every request, but this is useful to avoid the
323// accumulation of idle connections if you make many client objects during the
324// lifetime of your application.
325func DefaultNonPooledConfig() *Config {
326	return defaultConfig(cleanhttp.DefaultTransport)
327}
328
329// defaultConfig returns the default configuration for the client, using the
330// given function to make the transport.
331func defaultConfig(transportFn func() *http.Transport) *Config {
332	config := &Config{
333		Address:   "127.0.0.1:8500",
334		Scheme:    "http",
335		Transport: transportFn(),
336	}
337
338	if addr := os.Getenv(HTTPAddrEnvName); addr != "" {
339		config.Address = addr
340	}
341
342	if token := os.Getenv(HTTPTokenEnvName); token != "" {
343		config.Token = token
344	}
345
346	if auth := os.Getenv(HTTPAuthEnvName); auth != "" {
347		var username, password string
348		if strings.Contains(auth, ":") {
349			split := strings.SplitN(auth, ":", 2)
350			username = split[0]
351			password = split[1]
352		} else {
353			username = auth
354		}
355
356		config.HttpAuth = &HttpBasicAuth{
357			Username: username,
358			Password: password,
359		}
360	}
361
362	if ssl := os.Getenv(HTTPSSLEnvName); ssl != "" {
363		enabled, err := strconv.ParseBool(ssl)
364		if err != nil {
365			log.Printf("[WARN] client: could not parse %s: %s", HTTPSSLEnvName, err)
366		}
367
368		if enabled {
369			config.Scheme = "https"
370		}
371	}
372
373	if v := os.Getenv(HTTPTLSServerName); v != "" {
374		config.TLSConfig.Address = v
375	}
376	if v := os.Getenv(HTTPCAFile); v != "" {
377		config.TLSConfig.CAFile = v
378	}
379	if v := os.Getenv(HTTPCAPath); v != "" {
380		config.TLSConfig.CAPath = v
381	}
382	if v := os.Getenv(HTTPClientCert); v != "" {
383		config.TLSConfig.CertFile = v
384	}
385	if v := os.Getenv(HTTPClientKey); v != "" {
386		config.TLSConfig.KeyFile = v
387	}
388	if v := os.Getenv(HTTPSSLVerifyEnvName); v != "" {
389		doVerify, err := strconv.ParseBool(v)
390		if err != nil {
391			log.Printf("[WARN] client: could not parse %s: %s", HTTPSSLVerifyEnvName, err)
392		}
393		if !doVerify {
394			config.TLSConfig.InsecureSkipVerify = true
395		}
396	}
397
398	return config
399}
400
401// TLSConfig is used to generate a TLSClientConfig that's useful for talking to
402// Consul using TLS.
403func SetupTLSConfig(tlsConfig *TLSConfig) (*tls.Config, error) {
404	tlsClientConfig := &tls.Config{
405		InsecureSkipVerify: tlsConfig.InsecureSkipVerify,
406	}
407
408	if tlsConfig.Address != "" {
409		server := tlsConfig.Address
410		hasPort := strings.LastIndex(server, ":") > strings.LastIndex(server, "]")
411		if hasPort {
412			var err error
413			server, _, err = net.SplitHostPort(server)
414			if err != nil {
415				return nil, err
416			}
417		}
418		tlsClientConfig.ServerName = server
419	}
420
421	if tlsConfig.CertFile != "" && tlsConfig.KeyFile != "" {
422		tlsCert, err := tls.LoadX509KeyPair(tlsConfig.CertFile, tlsConfig.KeyFile)
423		if err != nil {
424			return nil, err
425		}
426		tlsClientConfig.Certificates = []tls.Certificate{tlsCert}
427	}
428
429	if tlsConfig.CAFile != "" || tlsConfig.CAPath != "" {
430		rootConfig := &rootcerts.Config{
431			CAFile: tlsConfig.CAFile,
432			CAPath: tlsConfig.CAPath,
433		}
434		if err := rootcerts.ConfigureTLS(tlsClientConfig, rootConfig); err != nil {
435			return nil, err
436		}
437	}
438
439	return tlsClientConfig, nil
440}
441
442func (c *Config) GenerateEnv() []string {
443	env := make([]string, 0, 10)
444
445	env = append(env,
446		fmt.Sprintf("%s=%s", HTTPAddrEnvName, c.Address),
447		fmt.Sprintf("%s=%s", HTTPTokenEnvName, c.Token),
448		fmt.Sprintf("%s=%t", HTTPSSLEnvName, c.Scheme == "https"),
449		fmt.Sprintf("%s=%s", HTTPCAFile, c.TLSConfig.CAFile),
450		fmt.Sprintf("%s=%s", HTTPCAPath, c.TLSConfig.CAPath),
451		fmt.Sprintf("%s=%s", HTTPClientCert, c.TLSConfig.CertFile),
452		fmt.Sprintf("%s=%s", HTTPClientKey, c.TLSConfig.KeyFile),
453		fmt.Sprintf("%s=%s", HTTPTLSServerName, c.TLSConfig.Address),
454		fmt.Sprintf("%s=%t", HTTPSSLVerifyEnvName, !c.TLSConfig.InsecureSkipVerify))
455
456	if c.HttpAuth != nil {
457		env = append(env, fmt.Sprintf("%s=%s:%s", HTTPAuthEnvName, c.HttpAuth.Username, c.HttpAuth.Password))
458	} else {
459		env = append(env, fmt.Sprintf("%s=", HTTPAuthEnvName))
460	}
461
462	return env
463}
464
465// Client provides a client to the Consul API
466type Client struct {
467	config Config
468}
469
470// NewClient returns a new client
471func NewClient(config *Config) (*Client, error) {
472	// bootstrap the config
473	defConfig := DefaultConfig()
474
475	if len(config.Address) == 0 {
476		config.Address = defConfig.Address
477	}
478
479	if len(config.Scheme) == 0 {
480		config.Scheme = defConfig.Scheme
481	}
482
483	if config.Transport == nil {
484		config.Transport = defConfig.Transport
485	}
486
487	if config.TLSConfig.Address == "" {
488		config.TLSConfig.Address = defConfig.TLSConfig.Address
489	}
490
491	if config.TLSConfig.CAFile == "" {
492		config.TLSConfig.CAFile = defConfig.TLSConfig.CAFile
493	}
494
495	if config.TLSConfig.CAPath == "" {
496		config.TLSConfig.CAPath = defConfig.TLSConfig.CAPath
497	}
498
499	if config.TLSConfig.CertFile == "" {
500		config.TLSConfig.CertFile = defConfig.TLSConfig.CertFile
501	}
502
503	if config.TLSConfig.KeyFile == "" {
504		config.TLSConfig.KeyFile = defConfig.TLSConfig.KeyFile
505	}
506
507	if !config.TLSConfig.InsecureSkipVerify {
508		config.TLSConfig.InsecureSkipVerify = defConfig.TLSConfig.InsecureSkipVerify
509	}
510
511	if config.HttpClient == nil {
512		var err error
513		config.HttpClient, err = NewHttpClient(config.Transport, config.TLSConfig)
514		if err != nil {
515			return nil, err
516		}
517	}
518
519	parts := strings.SplitN(config.Address, "://", 2)
520	if len(parts) == 2 {
521		switch parts[0] {
522		case "http":
523			config.Scheme = "http"
524		case "https":
525			config.Scheme = "https"
526		case "unix":
527			trans := cleanhttp.DefaultTransport()
528			trans.DialContext = func(_ context.Context, _, _ string) (net.Conn, error) {
529				return net.Dial("unix", parts[1])
530			}
531			config.HttpClient = &http.Client{
532				Transport: trans,
533			}
534		default:
535			return nil, fmt.Errorf("Unknown protocol scheme: %s", parts[0])
536		}
537		config.Address = parts[1]
538	}
539
540	if config.Token == "" {
541		config.Token = defConfig.Token
542	}
543
544	return &Client{config: *config}, nil
545}
546
547// NewHttpClient returns an http client configured with the given Transport and TLS
548// config.
549func NewHttpClient(transport *http.Transport, tlsConf TLSConfig) (*http.Client, error) {
550	client := &http.Client{
551		Transport: transport,
552	}
553
554	// TODO (slackpad) - Once we get some run time on the HTTP/2 support we
555	// should turn it on by default if TLS is enabled. We would basically
556	// just need to call http2.ConfigureTransport(transport) here. We also
557	// don't want to introduce another external dependency on
558	// golang.org/x/net/http2 at this time. For a complete recipe for how
559	// to enable HTTP/2 support on a transport suitable for the API client
560	// library see agent/http_test.go:TestHTTPServer_H2.
561
562	if transport.TLSClientConfig == nil {
563		tlsClientConfig, err := SetupTLSConfig(&tlsConf)
564
565		if err != nil {
566			return nil, err
567		}
568
569		transport.TLSClientConfig = tlsClientConfig
570	}
571
572	return client, nil
573}
574
575// request is used to help build up a request
576type request struct {
577	config *Config
578	method string
579	url    *url.URL
580	params url.Values
581	body   io.Reader
582	header http.Header
583	obj    interface{}
584	ctx    context.Context
585}
586
587// setQueryOptions is used to annotate the request with
588// additional query options
589func (r *request) setQueryOptions(q *QueryOptions) {
590	if q == nil {
591		return
592	}
593	if q.Datacenter != "" {
594		r.params.Set("dc", q.Datacenter)
595	}
596	if q.AllowStale {
597		r.params.Set("stale", "")
598	}
599	if q.RequireConsistent {
600		r.params.Set("consistent", "")
601	}
602	if q.WaitIndex != 0 {
603		r.params.Set("index", strconv.FormatUint(q.WaitIndex, 10))
604	}
605	if q.WaitTime != 0 {
606		r.params.Set("wait", durToMsec(q.WaitTime))
607	}
608	if q.WaitHash != "" {
609		r.params.Set("hash", q.WaitHash)
610	}
611	if q.Token != "" {
612		r.header.Set("X-Consul-Token", q.Token)
613	}
614	if q.Near != "" {
615		r.params.Set("near", q.Near)
616	}
617	if len(q.NodeMeta) > 0 {
618		for key, value := range q.NodeMeta {
619			r.params.Add("node-meta", key+":"+value)
620		}
621	}
622	if q.RelayFactor != 0 {
623		r.params.Set("relay-factor", strconv.Itoa(int(q.RelayFactor)))
624	}
625	if q.Connect {
626		r.params.Set("connect", "true")
627	}
628	if q.UseCache && !q.RequireConsistent {
629		r.params.Set("cached", "")
630
631		cc := []string{}
632		if q.MaxAge > 0 {
633			cc = append(cc, fmt.Sprintf("max-age=%.0f", q.MaxAge.Seconds()))
634		}
635		if q.StaleIfError > 0 {
636			cc = append(cc, fmt.Sprintf("stale-if-error=%.0f", q.StaleIfError.Seconds()))
637		}
638		if len(cc) > 0 {
639			r.header.Set("Cache-Control", strings.Join(cc, ", "))
640		}
641	}
642	r.ctx = q.ctx
643}
644
645// durToMsec converts a duration to a millisecond specified string. If the
646// user selected a positive value that rounds to 0 ms, then we will use 1 ms
647// so they get a short delay, otherwise Consul will translate the 0 ms into
648// a huge default delay.
649func durToMsec(dur time.Duration) string {
650	ms := dur / time.Millisecond
651	if dur > 0 && ms == 0 {
652		ms = 1
653	}
654	return fmt.Sprintf("%dms", ms)
655}
656
657// serverError is a string we look for to detect 500 errors.
658const serverError = "Unexpected response code: 500"
659
660// IsRetryableError returns true for 500 errors from the Consul servers, and
661// network connection errors. These are usually retryable at a later time.
662// This applies to reads but NOT to writes. This may return true for errors
663// on writes that may have still gone through, so do not use this to retry
664// any write operations.
665func IsRetryableError(err error) bool {
666	if err == nil {
667		return false
668	}
669
670	if _, ok := err.(net.Error); ok {
671		return true
672	}
673
674	// TODO (slackpad) - Make a real error type here instead of using
675	// a string check.
676	return strings.Contains(err.Error(), serverError)
677}
678
679// setWriteOptions is used to annotate the request with
680// additional write options
681func (r *request) setWriteOptions(q *WriteOptions) {
682	if q == nil {
683		return
684	}
685	if q.Datacenter != "" {
686		r.params.Set("dc", q.Datacenter)
687	}
688	if q.Token != "" {
689		r.header.Set("X-Consul-Token", q.Token)
690	}
691	if q.RelayFactor != 0 {
692		r.params.Set("relay-factor", strconv.Itoa(int(q.RelayFactor)))
693	}
694	r.ctx = q.ctx
695}
696
697// toHTTP converts the request to an HTTP request
698func (r *request) toHTTP() (*http.Request, error) {
699	// Encode the query parameters
700	r.url.RawQuery = r.params.Encode()
701
702	// Check if we should encode the body
703	if r.body == nil && r.obj != nil {
704		b, err := encodeBody(r.obj)
705		if err != nil {
706			return nil, err
707		}
708		r.body = b
709	}
710
711	// Create the HTTP request
712	req, err := http.NewRequest(r.method, r.url.RequestURI(), r.body)
713	if err != nil {
714		return nil, err
715	}
716
717	req.URL.Host = r.url.Host
718	req.URL.Scheme = r.url.Scheme
719	req.Host = r.url.Host
720	req.Header = r.header
721
722	// Setup auth
723	if r.config.HttpAuth != nil {
724		req.SetBasicAuth(r.config.HttpAuth.Username, r.config.HttpAuth.Password)
725	}
726	if r.ctx != nil {
727		return req.WithContext(r.ctx), nil
728	}
729
730	return req, nil
731}
732
733// newRequest is used to create a new request
734func (c *Client) newRequest(method, path string) *request {
735	r := &request{
736		config: &c.config,
737		method: method,
738		url: &url.URL{
739			Scheme: c.config.Scheme,
740			Host:   c.config.Address,
741			Path:   path,
742		},
743		params: make(map[string][]string),
744		header: make(http.Header),
745	}
746	if c.config.Datacenter != "" {
747		r.params.Set("dc", c.config.Datacenter)
748	}
749	if c.config.WaitTime != 0 {
750		r.params.Set("wait", durToMsec(r.config.WaitTime))
751	}
752	if c.config.Token != "" {
753		r.header.Set("X-Consul-Token", r.config.Token)
754	}
755	return r
756}
757
758// doRequest runs a request with our client
759func (c *Client) doRequest(r *request) (time.Duration, *http.Response, error) {
760	req, err := r.toHTTP()
761	if err != nil {
762		return 0, nil, err
763	}
764	start := time.Now()
765	resp, err := c.config.HttpClient.Do(req)
766	diff := time.Since(start)
767	return diff, resp, err
768}
769
770// Query is used to do a GET request against an endpoint
771// and deserialize the response into an interface using
772// standard Consul conventions.
773func (c *Client) query(endpoint string, out interface{}, q *QueryOptions) (*QueryMeta, error) {
774	r := c.newRequest("GET", endpoint)
775	r.setQueryOptions(q)
776	rtt, resp, err := c.doRequest(r)
777	if err != nil {
778		return nil, err
779	}
780	defer resp.Body.Close()
781
782	qm := &QueryMeta{}
783	parseQueryMeta(resp, qm)
784	qm.RequestTime = rtt
785
786	if err := decodeBody(resp, out); err != nil {
787		return nil, err
788	}
789	return qm, nil
790}
791
792// write is used to do a PUT request against an endpoint
793// and serialize/deserialized using the standard Consul conventions.
794func (c *Client) write(endpoint string, in, out interface{}, q *WriteOptions) (*WriteMeta, error) {
795	r := c.newRequest("PUT", endpoint)
796	r.setWriteOptions(q)
797	r.obj = in
798	rtt, resp, err := requireOK(c.doRequest(r))
799	if err != nil {
800		return nil, err
801	}
802	defer resp.Body.Close()
803
804	wm := &WriteMeta{RequestTime: rtt}
805	if out != nil {
806		if err := decodeBody(resp, &out); err != nil {
807			return nil, err
808		}
809	} else if _, err := ioutil.ReadAll(resp.Body); err != nil {
810		return nil, err
811	}
812	return wm, nil
813}
814
815// parseQueryMeta is used to help parse query meta-data
816func parseQueryMeta(resp *http.Response, q *QueryMeta) error {
817	header := resp.Header
818
819	// Parse the X-Consul-Index (if it's set - hash based blocking queries don't
820	// set this)
821	if indexStr := header.Get("X-Consul-Index"); indexStr != "" {
822		index, err := strconv.ParseUint(indexStr, 10, 64)
823		if err != nil {
824			return fmt.Errorf("Failed to parse X-Consul-Index: %v", err)
825		}
826		q.LastIndex = index
827	}
828	q.LastContentHash = header.Get("X-Consul-ContentHash")
829
830	// Parse the X-Consul-LastContact
831	last, err := strconv.ParseUint(header.Get("X-Consul-LastContact"), 10, 64)
832	if err != nil {
833		return fmt.Errorf("Failed to parse X-Consul-LastContact: %v", err)
834	}
835	q.LastContact = time.Duration(last) * time.Millisecond
836
837	// Parse the X-Consul-KnownLeader
838	switch header.Get("X-Consul-KnownLeader") {
839	case "true":
840		q.KnownLeader = true
841	default:
842		q.KnownLeader = false
843	}
844
845	// Parse X-Consul-Translate-Addresses
846	switch header.Get("X-Consul-Translate-Addresses") {
847	case "true":
848		q.AddressTranslationEnabled = true
849	default:
850		q.AddressTranslationEnabled = false
851	}
852
853	// Parse Cache info
854	if cacheStr := header.Get("X-Cache"); cacheStr != "" {
855		q.CacheHit = strings.EqualFold(cacheStr, "HIT")
856	}
857	if ageStr := header.Get("Age"); ageStr != "" {
858		age, err := strconv.ParseUint(ageStr, 10, 64)
859		if err != nil {
860			return fmt.Errorf("Failed to parse Age Header: %v", err)
861		}
862		q.CacheAge = time.Duration(age) * time.Second
863	}
864
865	return nil
866}
867
868// decodeBody is used to JSON decode a body
869func decodeBody(resp *http.Response, out interface{}) error {
870	dec := json.NewDecoder(resp.Body)
871	return dec.Decode(out)
872}
873
874// encodeBody is used to encode a request body
875func encodeBody(obj interface{}) (io.Reader, error) {
876	buf := bytes.NewBuffer(nil)
877	enc := json.NewEncoder(buf)
878	if err := enc.Encode(obj); err != nil {
879		return nil, err
880	}
881	return buf, nil
882}
883
884// requireOK is used to wrap doRequest and check for a 200
885func requireOK(d time.Duration, resp *http.Response, e error) (time.Duration, *http.Response, error) {
886	if e != nil {
887		if resp != nil {
888			resp.Body.Close()
889		}
890		return d, nil, e
891	}
892	if resp.StatusCode != 200 {
893		var buf bytes.Buffer
894		io.Copy(&buf, resp.Body)
895		resp.Body.Close()
896		return d, nil, fmt.Errorf("Unexpected response code: %d (%s)", resp.StatusCode, buf.Bytes())
897	}
898	return d, resp, nil
899}
900