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