github.com/pyroscope-io/pyroscope@v0.37.3-0.20230725203016-5f6947968bd0/pkg/scrape/config/config_http.go (about) 1 // Copyright 2016 The Prometheus Authors 2 // Copyright 2021 The Pyroscope Authors 3 // 4 // Licensed under the Apache License, Version 2.0 (the "License"); 5 // you may not use this file except in compliance with the License. 6 // You may obtain a copy of the License at 7 // 8 // http://www.apache.org/licenses/LICENSE-2.0 9 // 10 // Unless required by applicable law or agreed to in writing, software 11 // distributed under the License is distributed on an "AS IS" BASIS, 12 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 // See the License for the specific language governing permissions and 14 // limitations under the License. 15 16 //go:build go1.8 17 // +build go1.8 18 19 package config 20 21 import ( 22 "bytes" 23 "context" 24 "crypto/sha256" 25 "crypto/tls" 26 "crypto/x509" 27 "encoding/json" 28 "fmt" 29 "net" 30 "net/http" 31 "net/url" 32 "os" 33 "path/filepath" 34 "strings" 35 "sync" 36 "time" 37 38 "github.com/mwitkow/go-conntrack" 39 "golang.org/x/net/http2" 40 "golang.org/x/oauth2" 41 "golang.org/x/oauth2/clientcredentials" 42 "gopkg.in/yaml.v2" 43 ) 44 45 // revive:disable:max-public-structs complex domain 46 47 // DefaultHTTPClientConfig is the default HTTP client configuration. 48 var DefaultHTTPClientConfig = HTTPClientConfig{ 49 FollowRedirects: true, 50 } 51 52 // defaultHTTPClientOptions holds the default HTTP client options. 53 var defaultHTTPClientOptions = httpClientOptions{ 54 keepAlivesEnabled: true, 55 http2Enabled: true, 56 // 5 minutes is typically above the maximum sane scrape interval. So we can 57 // use keepalive for all configurations. 58 idleConnTimeout: 5 * time.Minute, 59 } 60 61 type closeIdler interface { 62 CloseIdleConnections() 63 } 64 65 // BasicAuth contains basic HTTP authentication credentials. 66 type BasicAuth struct { 67 Username string `yaml:"username" json:"username"` 68 Password Secret `yaml:"password,omitempty" json:"password,omitempty"` 69 PasswordFile string `yaml:"password-file,omitempty" json:"password-file,omitempty"` 70 } 71 72 // SetDirectory joins any relative file paths with dir. 73 func (a *BasicAuth) SetDirectory(dir string) { 74 if a == nil { 75 return 76 } 77 a.PasswordFile = JoinDir(dir, a.PasswordFile) 78 } 79 80 // Authorization contains HTTP authorization credentials. 81 type Authorization struct { 82 Type string `yaml:"type,omitempty" json:"type,omitempty"` 83 Credentials Secret `yaml:"credentials,omitempty" json:"credentials,omitempty"` 84 CredentialsFile string `yaml:"credentials-file,omitempty" json:"credentials-file,omitempty"` 85 } 86 87 // SetDirectory joins any relative file paths with dir. 88 func (a *Authorization) SetDirectory(dir string) { 89 if a == nil { 90 return 91 } 92 a.CredentialsFile = JoinDir(dir, a.CredentialsFile) 93 } 94 95 // URL is a custom URL type that allows validation at configuration load time. 96 type URL struct { 97 *url.URL 98 } 99 100 // UnmarshalYAML implements the yaml.Unmarshaler interface for URLs. 101 func (u *URL) UnmarshalYAML(unmarshal func(interface{}) error) error { 102 var s string 103 if err := unmarshal(&s); err != nil { 104 return err 105 } 106 107 urlp, err := url.Parse(s) 108 if err != nil { 109 return err 110 } 111 u.URL = urlp 112 return nil 113 } 114 115 // MarshalYAML implements the yaml.Marshaler interface for URLs. 116 func (u URL) MarshalYAML() (interface{}, error) { 117 if u.URL != nil { 118 return u.Redacted(), nil 119 } 120 return nil, nil 121 } 122 123 // Redacted returns the URL but replaces any password with "xxxxx". 124 func (u URL) Redacted() string { 125 if u.URL == nil { 126 return "" 127 } 128 129 ru := *u.URL 130 if _, ok := ru.User.Password(); ok { 131 // We can not use secretToken because it would be escaped. 132 ru.User = url.UserPassword(ru.User.Username(), "xxxxx") 133 } 134 return ru.String() 135 } 136 137 // UnmarshalJSON implements the json.Marshaler interface for URL. 138 func (u *URL) UnmarshalJSON(data []byte) error { 139 var s string 140 if err := json.Unmarshal(data, &s); err != nil { 141 return err 142 } 143 urlp, err := url.Parse(s) 144 if err != nil { 145 return err 146 } 147 u.URL = urlp 148 return nil 149 } 150 151 // MarshalJSON implements the json.Marshaler interface for URL. 152 func (u URL) MarshalJSON() ([]byte, error) { 153 if u.URL != nil { 154 return json.Marshal(u.URL.String()) 155 } 156 return []byte("null"), nil 157 } 158 159 // OAuth2 is the oauth2 client configuration. 160 type OAuth2 struct { 161 ClientID string `yaml:"client-id" json:"client-id"` 162 ClientSecret Secret `yaml:"client-secret" json:"client-secret"` 163 ClientSecretFile string `yaml:"client-secret-file" json:"client-secret-file"` 164 Scopes []string `yaml:"scopes,omitempty" json:"scopes,omitempty"` 165 TokenURL string `yaml:"token-url" json:"token-url"` 166 EndpointParams map[string]string `yaml:"endpoint-params,omitempty" json:"endpoint-params,omitempty"` 167 168 // TLSConfig is used to connect to the token URL. 169 TLSConfig TLSConfig `yaml:"tls-config,omitempty"` 170 } 171 172 // SetDirectory joins any relative file paths with dir. 173 func (a *OAuth2) SetDirectory(dir string) { 174 if a == nil { 175 return 176 } 177 a.ClientSecretFile = JoinDir(dir, a.ClientSecretFile) 178 a.TLSConfig.SetDirectory(dir) 179 } 180 181 // HTTPClientConfig configures an HTTP client. 182 type HTTPClientConfig struct { 183 // The HTTP basic authentication credentials for the targets. 184 BasicAuth *BasicAuth `yaml:"basic-auth,omitempty" json:"basic-auth,omitempty"` 185 // The HTTP authorization credentials for the targets. 186 Authorization *Authorization `yaml:"authorization,omitempty" json:"authorization,omitempty"` 187 // The OAuth2 client credentials used to fetch a token for the targets. 188 OAuth2 *OAuth2 `yaml:"oauth2,omitempty" json:"oauth2,omitempty"` 189 // The bearer token for the targets. Deprecated in favour of 190 // Authorization.Credentials. 191 BearerToken Secret `yaml:"bearer-token,omitempty" json:"bearer-token,omitempty"` 192 // The bearer token file for the targets. Deprecated in favour of 193 // Authorization.CredentialsFile. 194 BearerTokenFile string `yaml:"bearer-token-file,omitempty" json:"bearer-token-file,omitempty"` 195 // HTTP proxy server to use to connect to the targets. 196 ProxyURL URL `yaml:"proxy-url,omitempty" json:"proxy-url,omitempty"` 197 // TLSConfig to use to connect to the targets. 198 TLSConfig TLSConfig `yaml:"tls-config,omitempty" json:"tls-config,omitempty"` 199 // FollowRedirects specifies whether the client should follow HTTP 3xx redirects. 200 // The omitempty flag is not set, because it would be hidden from the 201 // marshalled configuration when set to false. 202 FollowRedirects bool `yaml:"follow-redirects" json:"follow-redirects"` 203 } 204 205 // SetDirectory joins any relative file paths with dir. 206 func (c *HTTPClientConfig) SetDirectory(dir string) { 207 if c == nil { 208 return 209 } 210 c.TLSConfig.SetDirectory(dir) 211 c.BasicAuth.SetDirectory(dir) 212 c.Authorization.SetDirectory(dir) 213 c.OAuth2.SetDirectory(dir) 214 c.BearerTokenFile = JoinDir(dir, c.BearerTokenFile) 215 } 216 217 // Validate validates the HTTPClientConfig to check only one of BearerToken, 218 // BasicAuth and BearerTokenFile is configured. 219 func (c *HTTPClientConfig) Validate() error { 220 // Backwards compatibility with the bearer-token field. 221 if len(c.BearerToken) > 0 && len(c.BearerTokenFile) > 0 { 222 return fmt.Errorf("at most one of bearer-token & bearer-token-file must be configured") 223 } 224 if (c.BasicAuth != nil || c.OAuth2 != nil) && (len(c.BearerToken) > 0 || len(c.BearerTokenFile) > 0) { 225 return fmt.Errorf("at most one of basic-auth, oauth2, bearer-token & bearer-token-file must be configured") 226 } 227 if c.BasicAuth != nil && (string(c.BasicAuth.Password) != "" && c.BasicAuth.PasswordFile != "") { 228 return fmt.Errorf("at most one of basic-auth password & password-file must be configured") 229 } 230 if c.Authorization != nil { 231 if len(c.BearerToken) > 0 || len(c.BearerTokenFile) > 0 { 232 return fmt.Errorf("authorization is not compatible with bearer-token & bearer-token-file") 233 } 234 if string(c.Authorization.Credentials) != "" && c.Authorization.CredentialsFile != "" { 235 return fmt.Errorf("at most one of authorization credentials & credentials-file must be configured") 236 } 237 c.Authorization.Type = strings.TrimSpace(c.Authorization.Type) 238 if len(c.Authorization.Type) == 0 { 239 c.Authorization.Type = "Bearer" 240 } 241 if strings.ToLower(c.Authorization.Type) == "basic" { 242 return fmt.Errorf(`authorization type cannot be set to "basic", use "basic-auth" instead`) 243 } 244 if c.BasicAuth != nil || c.OAuth2 != nil { 245 return fmt.Errorf("at most one of basic-auth, oauth2 & authorization must be configured") 246 } 247 } else { 248 if len(c.BearerToken) > 0 { 249 c.Authorization = &Authorization{Credentials: c.BearerToken} 250 c.Authorization.Type = "Bearer" 251 c.BearerToken = "" 252 } 253 if len(c.BearerTokenFile) > 0 { 254 c.Authorization = &Authorization{CredentialsFile: c.BearerTokenFile} 255 c.Authorization.Type = "Bearer" 256 c.BearerTokenFile = "" 257 } 258 } 259 if c.OAuth2 != nil { 260 if c.BasicAuth != nil { 261 return fmt.Errorf("at most one of basic-auth, oauth2 & authorization must be configured") 262 } 263 if len(c.OAuth2.ClientID) == 0 { 264 return fmt.Errorf("oauth2 client-id must be configured") 265 } 266 if len(c.OAuth2.ClientSecret) == 0 && len(c.OAuth2.ClientSecretFile) == 0 { 267 return fmt.Errorf("either oauth2 client-secret or client-secret-file must be configured") 268 } 269 if len(c.OAuth2.TokenURL) == 0 { 270 return fmt.Errorf("oauth2 token-url must be configured") 271 } 272 if len(c.OAuth2.ClientSecret) > 0 && len(c.OAuth2.ClientSecretFile) > 0 { 273 return fmt.Errorf("at most one of oauth2 client-secret & client-secret-file must be configured") 274 } 275 } 276 return nil 277 } 278 279 // UnmarshalYAML implements the yaml.Unmarshaler interface 280 func (c *HTTPClientConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { 281 type plain HTTPClientConfig 282 *c = DefaultHTTPClientConfig 283 if err := unmarshal((*plain)(c)); err != nil { 284 return err 285 } 286 return c.Validate() 287 } 288 289 // UnmarshalJSON implements the json.Marshaler interface for URL. 290 func (c *HTTPClientConfig) UnmarshalJSON(data []byte) error { 291 type plain HTTPClientConfig 292 *c = DefaultHTTPClientConfig 293 if err := json.Unmarshal(data, (*plain)(c)); err != nil { 294 return err 295 } 296 return c.Validate() 297 } 298 299 // UnmarshalYAML implements the yaml.Unmarshaler interface. 300 func (a *BasicAuth) UnmarshalYAML(unmarshal func(interface{}) error) error { 301 type plain BasicAuth 302 return unmarshal((*plain)(a)) 303 } 304 305 // DialContextFunc defines the signature of the DialContext() function implemented 306 // by net.Dialer. 307 type DialContextFunc func(context.Context, string, string) (net.Conn, error) 308 309 type httpClientOptions struct { 310 dialContextFunc DialContextFunc 311 keepAlivesEnabled bool 312 http2Enabled bool 313 idleConnTimeout time.Duration 314 } 315 316 // HTTPClientOption defines an option that can be applied to the HTTP client. 317 type HTTPClientOption func(options *httpClientOptions) 318 319 // WithDialContextFunc allows you to override func gets used for the actual dialing. The default is `net.Dialer.DialContext`. 320 func WithDialContextFunc(fn DialContextFunc) HTTPClientOption { 321 return func(opts *httpClientOptions) { 322 opts.dialContextFunc = fn 323 } 324 } 325 326 // WithKeepAlivesDisabled allows to disable HTTP keepalive. 327 func WithKeepAlivesDisabled() HTTPClientOption { 328 return func(opts *httpClientOptions) { 329 opts.keepAlivesEnabled = false 330 } 331 } 332 333 // WithHTTP2Disabled allows to disable HTTP2. 334 func WithHTTP2Disabled() HTTPClientOption { 335 return func(opts *httpClientOptions) { 336 opts.http2Enabled = false 337 } 338 } 339 340 // WithIdleConnTimeout allows setting the idle connection timeout. 341 func WithIdleConnTimeout(timeout time.Duration) HTTPClientOption { 342 return func(opts *httpClientOptions) { 343 opts.idleConnTimeout = timeout 344 } 345 } 346 347 // newClient returns a http.Client using the specified http.RoundTripper. 348 func newClient(rt http.RoundTripper) *http.Client { 349 return &http.Client{Transport: rt} 350 } 351 352 // NewClientFromConfig returns a new HTTP client configured for the 353 // given config.HTTPClientConfig and config.HTTPClientOption. 354 // The name is used as go-conntrack metric label. 355 func NewClientFromConfig(cfg HTTPClientConfig, name string, optFuncs ...HTTPClientOption) (*http.Client, error) { 356 rt, err := NewRoundTripperFromConfig(cfg, name, optFuncs...) 357 if err != nil { 358 return nil, err 359 } 360 client := newClient(rt) 361 if !cfg.FollowRedirects { 362 client.CheckRedirect = func(*http.Request, []*http.Request) error { 363 return http.ErrUseLastResponse 364 } 365 } 366 return client, nil 367 } 368 369 // NewRoundTripperFromConfig returns a new HTTP RoundTripper configured for the 370 // given config.HTTPClientConfig and config.HTTPClientOption. 371 // The name is used as go-conntrack metric label. 372 func NewRoundTripperFromConfig(cfg HTTPClientConfig, name string, optFuncs ...HTTPClientOption) (http.RoundTripper, error) { 373 opts := defaultHTTPClientOptions 374 for _, f := range optFuncs { 375 f(&opts) 376 } 377 378 var dialContext func(ctx context.Context, network, addr string) (net.Conn, error) 379 380 if opts.dialContextFunc != nil { 381 dialContext = conntrack.NewDialContextFunc( 382 conntrack.DialWithDialContextFunc((func(context.Context, string, string) (net.Conn, error))(opts.dialContextFunc)), 383 conntrack.DialWithTracing(), 384 conntrack.DialWithName(name)) 385 } else { 386 dialContext = conntrack.NewDialContextFunc( 387 conntrack.DialWithTracing(), 388 conntrack.DialWithName(name)) 389 } 390 391 newRT := func(tlsConfig *tls.Config) (http.RoundTripper, error) { 392 // The only timeout we care about is the configured scrape timeout. 393 // It is applied on request. So we leave out any timings here. 394 var rt http.RoundTripper = &http.Transport{ 395 Proxy: http.ProxyURL(cfg.ProxyURL.URL), 396 MaxIdleConns: 20000, 397 MaxIdleConnsPerHost: 1000, // see https://github.com/golang/go/issues/13801 398 DisableKeepAlives: !opts.keepAlivesEnabled, 399 TLSClientConfig: tlsConfig, 400 DisableCompression: true, 401 IdleConnTimeout: opts.idleConnTimeout, 402 TLSHandshakeTimeout: 10 * time.Second, 403 ExpectContinueTimeout: 1 * time.Second, 404 DialContext: dialContext, 405 } 406 if opts.http2Enabled && os.Getenv("PYROSCOPE_DISABLE_HTTP2") == "" { 407 // HTTP/2 support is golang had many problematic cornercases where 408 // dead connections would be kept and used in connection pools. 409 // https://github.com/golang/go/issues/32388 410 // https://github.com/golang/go/issues/39337 411 // https://github.com/golang/go/issues/39750 412 413 // Do not enable HTTP2 if the environment variable 414 // PYROSCOPE_DISABLE_HTTP2 is set to a non-empty value. 415 // This allows users to easily disable HTTP2 in case they run into 416 // issues again, but will be removed once we are confident that 417 // things work as expected. 418 419 http2t, err := http2.ConfigureTransports(rt.(*http.Transport)) 420 if err != nil { 421 return nil, err 422 } 423 http2t.ReadIdleTimeout = time.Minute 424 } 425 426 // If a authorization-credentials is provided, create a round tripper that will set the 427 // Authorization header correctly on each request. 428 if cfg.Authorization != nil && len(cfg.Authorization.Credentials) > 0 { 429 rt = NewAuthorizationCredentialsRoundTripper(cfg.Authorization.Type, cfg.Authorization.Credentials, rt) 430 } else if cfg.Authorization != nil && len(cfg.Authorization.CredentialsFile) > 0 { 431 rt = NewAuthorizationCredentialsFileRoundTripper(cfg.Authorization.Type, cfg.Authorization.CredentialsFile, rt) 432 } 433 // Backwards compatibility, be nice with importers who would not have 434 // called Validate(). 435 if len(cfg.BearerToken) > 0 { 436 rt = NewAuthorizationCredentialsRoundTripper("Bearer", cfg.BearerToken, rt) 437 } else if len(cfg.BearerTokenFile) > 0 { 438 rt = NewAuthorizationCredentialsFileRoundTripper("Bearer", cfg.BearerTokenFile, rt) 439 } 440 441 if cfg.BasicAuth != nil { 442 rt = NewBasicAuthRoundTripper(cfg.BasicAuth.Username, cfg.BasicAuth.Password, cfg.BasicAuth.PasswordFile, rt) 443 } 444 445 if cfg.OAuth2 != nil { 446 rt = NewOAuth2RoundTripper(cfg.OAuth2, rt) 447 } 448 // Return a new configured RoundTripper. 449 return rt, nil 450 } 451 452 tlsConfig, err := NewTLSConfig(&cfg.TLSConfig) 453 if err != nil { 454 return nil, err 455 } 456 457 if len(cfg.TLSConfig.CAFile) == 0 { 458 // No need for a RoundTripper that reloads the CA file automatically. 459 return newRT(tlsConfig) 460 } 461 462 return NewTLSRoundTripper(tlsConfig, cfg.TLSConfig.CAFile, newRT) 463 } 464 465 type authorizationCredentialsRoundTripper struct { 466 authType string 467 authCredentials Secret 468 rt http.RoundTripper 469 } 470 471 // NewAuthorizationCredentialsRoundTripper adds the provided credentials to a 472 // request unless the authorization header has already been set. 473 func NewAuthorizationCredentialsRoundTripper(authType string, authCredentials Secret, rt http.RoundTripper) http.RoundTripper { 474 return &authorizationCredentialsRoundTripper{authType, authCredentials, rt} 475 } 476 477 func (rt *authorizationCredentialsRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { 478 if len(req.Header.Get("Authorization")) == 0 { 479 req = cloneRequest(req) 480 req.Header.Set("Authorization", fmt.Sprintf("%s %s", rt.authType, string(rt.authCredentials))) 481 } 482 return rt.rt.RoundTrip(req) 483 } 484 485 func (rt *authorizationCredentialsRoundTripper) CloseIdleConnections() { 486 if ci, ok := rt.rt.(closeIdler); ok { 487 ci.CloseIdleConnections() 488 } 489 } 490 491 type authorizationCredentialsFileRoundTripper struct { 492 authType string 493 authCredentialsFile string 494 rt http.RoundTripper 495 } 496 497 // NewAuthorizationCredentialsFileRoundTripper adds the authorization 498 // credentials read from the provided file to a request unless the authorization 499 // header has already been set. This file is read for every request. 500 func NewAuthorizationCredentialsFileRoundTripper(authType, authCredentialsFile string, rt http.RoundTripper) http.RoundTripper { 501 return &authorizationCredentialsFileRoundTripper{authType, authCredentialsFile, rt} 502 } 503 504 func (rt *authorizationCredentialsFileRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { 505 if len(req.Header.Get("Authorization")) == 0 { 506 b, err := os.ReadFile(rt.authCredentialsFile) 507 if err != nil { 508 return nil, fmt.Errorf("unable to read authorization credentials file %s: %s", rt.authCredentialsFile, err) 509 } 510 authCredentials := strings.TrimSpace(string(b)) 511 512 req = cloneRequest(req) 513 req.Header.Set("Authorization", fmt.Sprintf("%s %s", rt.authType, authCredentials)) 514 } 515 516 return rt.rt.RoundTrip(req) 517 } 518 519 func (rt *authorizationCredentialsFileRoundTripper) CloseIdleConnections() { 520 if ci, ok := rt.rt.(closeIdler); ok { 521 ci.CloseIdleConnections() 522 } 523 } 524 525 type basicAuthRoundTripper struct { 526 username string 527 password Secret 528 passwordFile string 529 rt http.RoundTripper 530 } 531 532 // NewBasicAuthRoundTripper will apply a BASIC auth authorization header to a request unless it has 533 // already been set. 534 func NewBasicAuthRoundTripper(username string, password Secret, passwordFile string, rt http.RoundTripper) http.RoundTripper { 535 return &basicAuthRoundTripper{username, password, passwordFile, rt} 536 } 537 538 func (rt *basicAuthRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { 539 if len(req.Header.Get("Authorization")) != 0 { 540 return rt.rt.RoundTrip(req) 541 } 542 req = cloneRequest(req) 543 if rt.passwordFile != "" { 544 bs, err := os.ReadFile(rt.passwordFile) 545 if err != nil { 546 return nil, fmt.Errorf("unable to read basic auth password file %s: %s", rt.passwordFile, err) 547 } 548 req.SetBasicAuth(rt.username, strings.TrimSpace(string(bs))) 549 } else { 550 req.SetBasicAuth(rt.username, strings.TrimSpace(string(rt.password))) 551 } 552 return rt.rt.RoundTrip(req) 553 } 554 555 func (rt *basicAuthRoundTripper) CloseIdleConnections() { 556 if ci, ok := rt.rt.(closeIdler); ok { 557 ci.CloseIdleConnections() 558 } 559 } 560 561 type oauth2RoundTripper struct { 562 config *OAuth2 563 rt http.RoundTripper 564 next http.RoundTripper 565 secret string 566 mtx sync.RWMutex 567 } 568 569 func NewOAuth2RoundTripper(config *OAuth2, next http.RoundTripper) http.RoundTripper { 570 return &oauth2RoundTripper{ 571 config: config, 572 next: next, 573 } 574 } 575 576 func (rt *oauth2RoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { 577 var ( 578 secret string 579 changed bool 580 ) 581 582 if rt.config.ClientSecretFile != "" { 583 data, err := os.ReadFile(rt.config.ClientSecretFile) 584 if err != nil { 585 return nil, fmt.Errorf("unable to read oauth2 client secret file %s: %s", rt.config.ClientSecretFile, err) 586 } 587 secret = strings.TrimSpace(string(data)) 588 rt.mtx.RLock() 589 changed = secret != rt.secret 590 rt.mtx.RUnlock() 591 } 592 593 if changed || rt.rt == nil { 594 if rt.config.ClientSecret != "" { 595 secret = string(rt.config.ClientSecret) 596 } 597 598 config := &clientcredentials.Config{ 599 ClientID: rt.config.ClientID, 600 ClientSecret: secret, 601 Scopes: rt.config.Scopes, 602 TokenURL: rt.config.TokenURL, 603 EndpointParams: mapToValues(rt.config.EndpointParams), 604 } 605 606 tlsConfig, err := NewTLSConfig(&rt.config.TLSConfig) 607 if err != nil { 608 return nil, err 609 } 610 611 var t http.RoundTripper 612 if len(rt.config.TLSConfig.CAFile) == 0 { 613 t = &http.Transport{TLSClientConfig: tlsConfig} 614 } else { 615 t, err = NewTLSRoundTripper(tlsConfig, rt.config.TLSConfig.CAFile, func(tls *tls.Config) (http.RoundTripper, error) { 616 return &http.Transport{TLSClientConfig: tls}, nil 617 }) 618 if err != nil { 619 return nil, err 620 } 621 } 622 623 ctx := context.WithValue(context.Background(), oauth2.HTTPClient, &http.Client{Transport: t}) 624 tokenSource := config.TokenSource(ctx) 625 626 rt.mtx.Lock() 627 rt.secret = secret 628 rt.rt = &oauth2.Transport{ 629 Base: rt.next, 630 Source: tokenSource, 631 } 632 rt.mtx.Unlock() 633 } 634 635 rt.mtx.RLock() 636 currentRT := rt.rt 637 rt.mtx.RUnlock() 638 return currentRT.RoundTrip(req) 639 } 640 641 func (rt *oauth2RoundTripper) CloseIdleConnections() { 642 // OAuth2 RT does not support CloseIdleConnections() but the next RT might. 643 if ci, ok := rt.next.(closeIdler); ok { 644 ci.CloseIdleConnections() 645 } 646 } 647 648 func mapToValues(m map[string]string) url.Values { 649 v := url.Values{} 650 for name, value := range m { 651 v.Set(name, value) 652 } 653 654 return v 655 } 656 657 // cloneRequest returns a clone of the provided *http.Request. 658 // The clone is a shallow copy of the struct and its Header map. 659 func cloneRequest(r *http.Request) *http.Request { 660 // Shallow copy of the struct. 661 r2 := new(http.Request) 662 *r2 = *r 663 // Deep copy of the Header. 664 r2.Header = make(http.Header) 665 for k, s := range r.Header { 666 r2.Header[k] = s 667 } 668 return r2 669 } 670 671 // NewTLSConfig creates a new tls.Config from the given TLSConfig. 672 func NewTLSConfig(cfg *TLSConfig) (*tls.Config, error) { 673 tlsConfig := &tls.Config{InsecureSkipVerify: cfg.InsecureSkipVerify} 674 675 // If a CA cert is provided then let's read it in so we can validate the 676 // scrape target's certificate properly. 677 if len(cfg.CAFile) > 0 { 678 b, err := readCAFile(cfg.CAFile) 679 if err != nil { 680 return nil, err 681 } 682 if !updateRootCA(tlsConfig, b) { 683 return nil, fmt.Errorf("unable to use specified CA cert %s", cfg.CAFile) 684 } 685 } 686 687 if len(cfg.ServerName) > 0 { 688 tlsConfig.ServerName = cfg.ServerName 689 } 690 // If a client cert & key is provided then configure TLS config accordingly. 691 if len(cfg.CertFile) > 0 && len(cfg.KeyFile) == 0 { 692 return nil, fmt.Errorf("client cert file %q specified without client key file", cfg.CertFile) 693 } else if len(cfg.KeyFile) > 0 && len(cfg.CertFile) == 0 { 694 return nil, fmt.Errorf("client key file %q specified without client cert file", cfg.KeyFile) 695 } else if len(cfg.CertFile) > 0 && len(cfg.KeyFile) > 0 { 696 // Verify that client cert and key are valid. 697 if _, err := cfg.getClientCertificate(nil); err != nil { 698 return nil, err 699 } 700 tlsConfig.GetClientCertificate = cfg.getClientCertificate 701 } 702 703 return tlsConfig, nil 704 } 705 706 // TLSConfig configures the options for TLS connections. 707 type TLSConfig struct { 708 // The CA cert to use for the targets. 709 CAFile string `yaml:"ca-file,omitempty" json:"ca-file,omitempty"` 710 // The client cert file for the targets. 711 CertFile string `yaml:"cert-file,omitempty" json:"cert-file,omitempty"` 712 // The client key file for the targets. 713 KeyFile string `yaml:"key-file,omitempty" json:"key-file,omitempty"` 714 // Used to verify the hostname for the targets. 715 ServerName string `yaml:"server-name,omitempty" json:"server-name,omitempty"` 716 // Disable target certificate validation. 717 InsecureSkipVerify bool `yaml:"insecure-skip-verify" json:"insecure-skip-verify"` 718 } 719 720 // SetDirectory joins any relative file paths with dir. 721 func (c *TLSConfig) SetDirectory(dir string) { 722 if c == nil { 723 return 724 } 725 c.CAFile = JoinDir(dir, c.CAFile) 726 c.CertFile = JoinDir(dir, c.CertFile) 727 c.KeyFile = JoinDir(dir, c.KeyFile) 728 } 729 730 // UnmarshalYAML implements the yaml.Unmarshaler interface. 731 func (c *TLSConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { 732 type plain TLSConfig 733 return unmarshal((*plain)(c)) 734 } 735 736 // getClientCertificate reads the pair of client cert and key from disk and returns a tls.Certificate. 737 func (c *TLSConfig) getClientCertificate(*tls.CertificateRequestInfo) (*tls.Certificate, error) { 738 cert, err := tls.LoadX509KeyPair(c.CertFile, c.KeyFile) 739 if err != nil { 740 return nil, fmt.Errorf("unable to use specified client cert (%s) & key (%s): %s", c.CertFile, c.KeyFile, err) 741 } 742 return &cert, nil 743 } 744 745 // readCAFile reads the CA cert file from disk. 746 func readCAFile(f string) ([]byte, error) { 747 data, err := os.ReadFile(f) 748 if err != nil { 749 return nil, fmt.Errorf("unable to load specified CA cert %s: %s", f, err) 750 } 751 return data, nil 752 } 753 754 // updateRootCA parses the given byte slice as a series of PEM encoded certificates and updates tls.Config.RootCAs. 755 func updateRootCA(cfg *tls.Config, b []byte) bool { 756 caCertPool := x509.NewCertPool() 757 if !caCertPool.AppendCertsFromPEM(b) { 758 return false 759 } 760 cfg.RootCAs = caCertPool 761 return true 762 } 763 764 // tlsRoundTripper is a RoundTripper that updates automatically its TLS 765 // configuration whenever the content of the CA file changes. 766 type tlsRoundTripper struct { 767 caFile string 768 // newRT returns a new RoundTripper. 769 newRT func(*tls.Config) (http.RoundTripper, error) 770 771 mtx sync.RWMutex 772 rt http.RoundTripper 773 hashCAFile []byte 774 tlsConfig *tls.Config 775 } 776 777 func NewTLSRoundTripper( 778 cfg *tls.Config, 779 caFile string, 780 newRT func(*tls.Config) (http.RoundTripper, error), 781 ) (http.RoundTripper, error) { 782 t := &tlsRoundTripper{ 783 caFile: caFile, 784 newRT: newRT, 785 tlsConfig: cfg, 786 } 787 788 rt, err := t.newRT(t.tlsConfig) 789 if err != nil { 790 return nil, err 791 } 792 t.rt = rt 793 _, t.hashCAFile, err = t.getCAWithHash() 794 if err != nil { 795 return nil, err 796 } 797 798 return t, nil 799 } 800 801 func (t *tlsRoundTripper) getCAWithHash() ([]byte, []byte, error) { 802 b, err := readCAFile(t.caFile) 803 if err != nil { 804 return nil, nil, err 805 } 806 h := sha256.Sum256(b) 807 return b, h[:], nil 808 } 809 810 // RoundTrip implements the http.RoundTrip interface. 811 func (t *tlsRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { 812 b, h, err := t.getCAWithHash() 813 if err != nil { 814 return nil, err 815 } 816 817 t.mtx.RLock() 818 equal := bytes.Equal(h[:], t.hashCAFile) 819 rt := t.rt 820 t.mtx.RUnlock() 821 if equal { 822 // The CA cert hasn't changed, use the existing RoundTripper. 823 return rt.RoundTrip(req) 824 } 825 826 // Create a new RoundTripper. 827 tlsConfig := t.tlsConfig.Clone() 828 if !updateRootCA(tlsConfig, b) { 829 return nil, fmt.Errorf("unable to use specified CA cert %s", t.caFile) 830 } 831 rt, err = t.newRT(tlsConfig) 832 if err != nil { 833 return nil, err 834 } 835 t.CloseIdleConnections() 836 837 t.mtx.Lock() 838 t.rt = rt 839 t.hashCAFile = h[:] 840 t.mtx.Unlock() 841 842 return rt.RoundTrip(req) 843 } 844 845 func (t *tlsRoundTripper) CloseIdleConnections() { 846 t.mtx.RLock() 847 defer t.mtx.RUnlock() 848 if ci, ok := t.rt.(closeIdler); ok { 849 ci.CloseIdleConnections() 850 } 851 } 852 853 func (c HTTPClientConfig) String() string { 854 b, err := yaml.Marshal(c) 855 if err != nil { 856 return fmt.Sprintf("<error creating http client config string: %s>", err) 857 } 858 return string(b) 859 } 860 861 const secretToken = "<secret>" 862 863 // Secret special type for storing secrets. 864 type Secret string 865 866 // MarshalYAML implements the yaml.Marshaler interface for Secrets. 867 func (s Secret) MarshalYAML() (interface{}, error) { 868 if s != "" { 869 return secretToken, nil 870 } 871 return nil, nil 872 } 873 874 // UnmarshalYAML implements the yaml.Unmarshaler interface for Secrets. 875 func (s *Secret) UnmarshalYAML(unmarshal func(interface{}) error) error { 876 type plain Secret 877 return unmarshal((*plain)(s)) 878 } 879 880 // MarshalJSON implements the json.Marshaler interface for Secret. 881 func (s Secret) MarshalJSON() ([]byte, error) { 882 if len(s) == 0 { 883 return json.Marshal("") 884 } 885 return json.Marshal(secretToken) 886 } 887 888 // DirectorySetter is a config type that contains file paths that may 889 // be relative to the file containing the config. 890 type DirectorySetter interface { 891 // SetDirectory joins any relative file paths with dir. 892 // Any paths that are empty or absolute remain unchanged. 893 SetDirectory(dir string) 894 } 895 896 // JoinDir joins dir and path if path is relative. 897 // If path is empty or absolute, it is returned unchanged. 898 func JoinDir(dir, path string) string { 899 if path == "" || filepath.IsAbs(path) { 900 return path 901 } 902 return filepath.Join(dir, path) 903 }