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