From b4e000c6c22acbfb75ba2743385e35208e63c88c Mon Sep 17 00:00:00 2001 From: Julien Pivotto Date: Mon, 1 Mar 2021 23:19:38 +0100 Subject: [PATCH] Update common - Add support for custom authorization scheme - Add support for not following redirects in http_client Signed-off-by: Julien Pivotto --- config/config.go | 3 +- config/config_test.go | 19 ++- config/testdata/conf.http-config.good.yml | 11 ++ docs/configuration.md | 25 ++-- go.mod | 2 +- go.sum | 2 + .../prometheus/common/config/http_config.go | 134 ++++++++++++++---- .../prometheus/common/model/time.go | 12 ++ vendor/modules.txt | 2 +- 9 files changed, 168 insertions(+), 42 deletions(-) create mode 100644 config/testdata/conf.http-config.good.yml diff --git a/config/config.go b/config/config.go index 440b505f..3af55ca8 100644 --- a/config/config.go +++ b/config/config.go @@ -486,9 +486,10 @@ func checkTimeInterval(r *Route, timeIntervals map[string]struct{}) error { // DefaultGlobalConfig returns GlobalConfig with default values. func DefaultGlobalConfig() GlobalConfig { + var defaultHTTPConfig = commoncfg.DefaultHTTPClientConfig return GlobalConfig{ ResolveTimeout: model.Duration(5 * time.Minute), - HTTPConfig: &commoncfg.HTTPClientConfig{}, + HTTPConfig: &defaultHTTPConfig, SMTPHello: "localhost", SMTPRequireTLS: true, diff --git a/config/config_test.go b/config/config_test.go index ce8c6fef..3317ef0f 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -675,7 +675,9 @@ func TestEmptyFieldsAndRegex(t *testing.T) { var expectedConf = Config{ Global: &GlobalConfig{ - HTTPConfig: &commoncfg.HTTPClientConfig{}, + HTTPConfig: &commoncfg.HTTPClientConfig{ + FollowRedirects: true, + }, ResolveTimeout: model.Duration(5 * time.Minute), SMTPSmarthost: HostPort{Host: "localhost", Port: "25"}, SMTPFrom: "alertmanager@example.org", @@ -755,6 +757,21 @@ func TestEmptyFieldsAndRegex(t *testing.T) { } } +func TestGlobalAndLocalHTTPConfig(t *testing.T) { + config, err := LoadFile("testdata/conf.http-config.good.yml") + if err != nil { + t.Fatalf("Error parsing %s: %s", "testdata/conf-http-config.good.yml", err) + } + + if config.Global.HTTPConfig.FollowRedirects { + t.Fatalf("global HTTP config should not follow redirects") + } + + if !config.Receivers[0].SlackConfigs[0].HTTPConfig.FollowRedirects { + t.Fatalf("global HTTP config should follow redirects") + } +} + func TestSMTPHello(t *testing.T) { c, err := LoadFile("testdata/conf.good.yml") if err != nil { diff --git a/config/testdata/conf.http-config.good.yml b/config/testdata/conf.http-config.good.yml new file mode 100644 index 00000000..879d7c2c --- /dev/null +++ b/config/testdata/conf.http-config.good.yml @@ -0,0 +1,11 @@ +global: + slack_api_url: 'https://slack.com/webhook' + http_config: + follow_redirects: false +route: + receiver: team-X-slack +receivers: +- name: 'team-X-slack' + slack_configs: + - http_config: + proxy_url: foo diff --git a/docs/configuration.md b/docs/configuration.md index 87645515..f872b147 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -319,8 +319,7 @@ A `http_config` allows configuring the HTTP client that the receiver uses to communicate with HTTP-based API services. ```yaml -# Note that `basic_auth`, `bearer_token` and `bearer_token_file` options are -# mutually exclusive. +# Note that `basic_auth` and `authorization` options are mutually exclusive. # Sets the `Authorization` header with the configured username and password. # password and password_file are mutually exclusive. @@ -329,18 +328,26 @@ basic_auth: [ password: ] [ password_file: ] -# Sets the `Authorization` header with the configured bearer token. -[ bearer_token: ] +# Optional the `Authorization` header configuration. +authorization: + # Sets the authentication type. + [ type: | default: Bearer ] + # Sets the credentials. It is mutually exclusive with + # `credentials_file`. + [ credentials: ] + # Sets the credentials with the credentials read from the configured file. + # It is mutually exclusive with `credentials`. + [ credentials_file: ] -# Sets the `Authorization` header with the bearer token read from the configured file. -[ bearer_token_file: ] +# Optional proxy URL. +[ proxy_url: ] + +# Configure whether HTTP requests follow HTTP 3xx redirects. +[ follow_redirects: | default = true ] # Configures the TLS settings. tls_config: [ ] - -# Optional proxy URL. -[ proxy_url: ] ``` ## `` diff --git a/go.mod b/go.mod index 09c54bba..add46ef5 100644 --- a/go.mod +++ b/go.mod @@ -22,7 +22,7 @@ require ( github.com/oklog/ulid v1.3.1 github.com/pkg/errors v0.9.1 github.com/prometheus/client_golang v1.7.1 - github.com/prometheus/common v0.15.0 + github.com/prometheus/common v0.18.0 github.com/prometheus/exporter-toolkit v0.5.0 github.com/rs/cors v1.7.0 github.com/satori/go.uuid v1.2.0 diff --git a/go.sum b/go.sum index 422121b5..8d2ebdde 100644 --- a/go.sum +++ b/go.sum @@ -427,6 +427,8 @@ github.com/prometheus/common v0.10.0 h1:RyRA7RzGXQZiW+tGMr7sxa85G1z0yOpM1qq5c8lN github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= github.com/prometheus/common v0.15.0 h1:4fgOnadei3EZvgRwxJ7RMpG1k1pOZth5Pc13tyspaKM= github.com/prometheus/common v0.15.0/go.mod h1:U+gB1OBLb1lF3O42bTCL+FK18tX9Oar16Clt/msog/s= +github.com/prometheus/common v0.18.0 h1:WCVKW7aL6LEe1uryfI9dnEc2ZqNB1Fn0ok930v0iL1Y= +github.com/prometheus/common v0.18.0/go.mod h1:U+gB1OBLb1lF3O42bTCL+FK18tX9Oar16Clt/msog/s= github.com/prometheus/exporter-toolkit v0.5.0 h1:GwrxhCviqOl8Mm0vKqkh7Xy54m+FPlHEJacFs48M3gY= github.com/prometheus/exporter-toolkit v0.5.0/go.mod h1:OCkM4805mmisBhLmVFw858QYi3v0wKdY6/UxrT0pZVg= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= diff --git a/vendor/github.com/prometheus/common/config/http_config.go b/vendor/github.com/prometheus/common/config/http_config.go index 4dd88758..af9e463b 100644 --- a/vendor/github.com/prometheus/common/config/http_config.go +++ b/vendor/github.com/prometheus/common/config/http_config.go @@ -33,6 +33,11 @@ import ( "gopkg.in/yaml.v2" ) +// DefaultHTTPClientConfig is the default HTTP client configuration. +var DefaultHTTPClientConfig = HTTPClientConfig{ + FollowRedirects: true, +} + type closeIdler interface { CloseIdleConnections() } @@ -52,6 +57,21 @@ func (a *BasicAuth) SetDirectory(dir string) { a.PasswordFile = JoinDir(dir, a.PasswordFile) } +// Authorization contains HTTP authorization credentials. +type Authorization struct { + Type string `yaml:"type,omitempty"` + Credentials Secret `yaml:"credentials,omitempty"` + CredentialsFile string `yaml:"credentials_file,omitempty"` +} + +// SetDirectory joins any relative file paths with dir. +func (a *Authorization) SetDirectory(dir string) { + if a == nil { + return + } + a.CredentialsFile = JoinDir(dir, a.CredentialsFile) +} + // URL is a custom URL type that allows validation at configuration load time. type URL struct { *url.URL @@ -84,14 +104,22 @@ func (u URL) MarshalYAML() (interface{}, error) { type HTTPClientConfig struct { // The HTTP basic authentication credentials for the targets. BasicAuth *BasicAuth `yaml:"basic_auth,omitempty"` - // The bearer token for the targets. + // The HTTP authorization credentials for the targets. + Authorization *Authorization `yaml:"authorization,omitempty"` + // The bearer token for the targets. Deprecated in favour of + // Authorization.Credentials. BearerToken Secret `yaml:"bearer_token,omitempty"` - // The bearer token file for the targets. + // The bearer token file for the targets. Deprecated in favour of + // Authorization.CredentialsFile. BearerTokenFile string `yaml:"bearer_token_file,omitempty"` // HTTP proxy server to use to connect to the targets. ProxyURL URL `yaml:"proxy_url,omitempty"` // TLSConfig to use to connect to the targets. TLSConfig TLSConfig `yaml:"tls_config,omitempty"` + // FollowRedirects specifies whether the client should follow HTTP 3xx redirects. + // The omitempty flag is not set, because it would be hidden from the + // marshalled configuration when set to false. + FollowRedirects bool `yaml:"follow_redirects"` } // SetDirectory joins any relative file paths with dir. @@ -101,12 +129,14 @@ func (c *HTTPClientConfig) SetDirectory(dir string) { } c.TLSConfig.SetDirectory(dir) c.BasicAuth.SetDirectory(dir) + c.Authorization.SetDirectory(dir) c.BearerTokenFile = JoinDir(dir, c.BearerTokenFile) } // Validate validates the HTTPClientConfig to check only one of BearerToken, // BasicAuth and BearerTokenFile is configured. func (c *HTTPClientConfig) Validate() error { + // Backwards compatibility with the bearer_token field. if len(c.BearerToken) > 0 && len(c.BearerTokenFile) > 0 { return fmt.Errorf("at most one of bearer_token & bearer_token_file must be configured") } @@ -116,12 +146,42 @@ func (c *HTTPClientConfig) Validate() error { if c.BasicAuth != nil && (string(c.BasicAuth.Password) != "" && c.BasicAuth.PasswordFile != "") { return fmt.Errorf("at most one of basic_auth password & password_file must be configured") } + if c.Authorization != nil { + if len(c.BearerToken) > 0 || len(c.BearerTokenFile) > 0 { + return fmt.Errorf("authorization is not compatible with bearer_token & bearer_token_file") + } + if string(c.Authorization.Credentials) != "" && c.Authorization.CredentialsFile != "" { + return fmt.Errorf("at most one of authorization credentials & credentials_file must be configured") + } + c.Authorization.Type = strings.TrimSpace(c.Authorization.Type) + if len(c.Authorization.Type) == 0 { + c.Authorization.Type = "Bearer" + } + if strings.ToLower(c.Authorization.Type) == "basic" { + return fmt.Errorf(`authorization type cannot be set to "basic", use "basic_auth" instead`) + } + if c.BasicAuth != nil { + return fmt.Errorf("at most one of basic_auth & authorization must be configured") + } + } else { + if len(c.BearerToken) > 0 { + c.Authorization = &Authorization{Credentials: c.BearerToken} + c.Authorization.Type = "Bearer" + c.BearerToken = "" + } + if len(c.BearerTokenFile) > 0 { + c.Authorization = &Authorization{CredentialsFile: c.BearerTokenFile} + c.Authorization.Type = "Bearer" + c.BearerTokenFile = "" + } + } return nil } // UnmarshalYAML implements the yaml.Unmarshaler interface func (c *HTTPClientConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { type plain HTTPClientConfig + *c = DefaultHTTPClientConfig if err := unmarshal((*plain)(c)); err != nil { return err } @@ -146,7 +206,13 @@ func NewClientFromConfig(cfg HTTPClientConfig, name string, disableKeepAlives, e if err != nil { return nil, err } - return newClient(rt), nil + client := newClient(rt) + if !cfg.FollowRedirects { + client.CheckRedirect = func(*http.Request, []*http.Request) error { + return http.ErrUseLastResponse + } + } + return client, nil } // NewRoundTripperFromConfig returns a new HTTP RoundTripper configured for the @@ -186,12 +252,19 @@ func NewRoundTripperFromConfig(cfg HTTPClientConfig, name string, disableKeepAli } } - // If a bearer token is provided, create a round tripper that will set the + // If a authorization_credentials is provided, create a round tripper that will set the // Authorization header correctly on each request. + if cfg.Authorization != nil && len(cfg.Authorization.Credentials) > 0 { + rt = NewAuthorizationCredentialsRoundTripper(cfg.Authorization.Type, cfg.Authorization.Credentials, rt) + } else if cfg.Authorization != nil && len(cfg.Authorization.CredentialsFile) > 0 { + rt = NewAuthorizationCredentialsFileRoundTripper(cfg.Authorization.Type, cfg.Authorization.CredentialsFile, rt) + } + // Backwards compatibility, be nice with importers who would not have + // called Validate(). if len(cfg.BearerToken) > 0 { - rt = NewBearerAuthRoundTripper(cfg.BearerToken, rt) + rt = NewAuthorizationCredentialsRoundTripper("Bearer", cfg.BearerToken, rt) } else if len(cfg.BearerTokenFile) > 0 { - rt = NewBearerAuthFileRoundTripper(cfg.BearerTokenFile, rt) + rt = NewAuthorizationCredentialsFileRoundTripper("Bearer", cfg.BearerTokenFile, rt) } if cfg.BasicAuth != nil { @@ -214,58 +287,61 @@ func NewRoundTripperFromConfig(cfg HTTPClientConfig, name string, disableKeepAli return newTLSRoundTripper(tlsConfig, cfg.TLSConfig.CAFile, newRT) } -type bearerAuthRoundTripper struct { - bearerToken Secret - rt http.RoundTripper +type authorizationCredentialsRoundTripper struct { + authType string + authCredentials Secret + rt http.RoundTripper } -// NewBearerAuthRoundTripper adds the provided bearer token to a request unless the authorization -// header has already been set. -func NewBearerAuthRoundTripper(token Secret, rt http.RoundTripper) http.RoundTripper { - return &bearerAuthRoundTripper{token, rt} +// NewAuthorizationCredentialsRoundTripper adds the provided credentials to a +// request unless the authorization header has already been set. +func NewAuthorizationCredentialsRoundTripper(authType string, authCredentials Secret, rt http.RoundTripper) http.RoundTripper { + return &authorizationCredentialsRoundTripper{authType, authCredentials, rt} } -func (rt *bearerAuthRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { +func (rt *authorizationCredentialsRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { if len(req.Header.Get("Authorization")) == 0 { req = cloneRequest(req) - req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", string(rt.bearerToken))) + req.Header.Set("Authorization", fmt.Sprintf("%s %s", rt.authType, string(rt.authCredentials))) } return rt.rt.RoundTrip(req) } -func (rt *bearerAuthRoundTripper) CloseIdleConnections() { +func (rt *authorizationCredentialsRoundTripper) CloseIdleConnections() { if ci, ok := rt.rt.(closeIdler); ok { ci.CloseIdleConnections() } } -type bearerAuthFileRoundTripper struct { - bearerFile string - rt http.RoundTripper +type authorizationCredentialsFileRoundTripper struct { + authType string + authCredentialsFile string + rt http.RoundTripper } -// NewBearerAuthFileRoundTripper adds the bearer token read from the provided file to a request unless -// the authorization header has already been set. This file is read for every request. -func NewBearerAuthFileRoundTripper(bearerFile string, rt http.RoundTripper) http.RoundTripper { - return &bearerAuthFileRoundTripper{bearerFile, rt} +// NewAuthorizationCredentialsFileRoundTripper adds the authorization +// credentials read from the provided file to a request unless the authorization +// header has already been set. This file is read for every request. +func NewAuthorizationCredentialsFileRoundTripper(authType, authCredentialsFile string, rt http.RoundTripper) http.RoundTripper { + return &authorizationCredentialsFileRoundTripper{authType, authCredentialsFile, rt} } -func (rt *bearerAuthFileRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { +func (rt *authorizationCredentialsFileRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { if len(req.Header.Get("Authorization")) == 0 { - b, err := ioutil.ReadFile(rt.bearerFile) + b, err := ioutil.ReadFile(rt.authCredentialsFile) if err != nil { - return nil, fmt.Errorf("unable to read bearer token file %s: %s", rt.bearerFile, err) + return nil, fmt.Errorf("unable to read authorization credentials file %s: %s", rt.authCredentialsFile, err) } - bearerToken := strings.TrimSpace(string(b)) + authCredentials := strings.TrimSpace(string(b)) req = cloneRequest(req) - req.Header.Set("Authorization", "Bearer "+bearerToken) + req.Header.Set("Authorization", fmt.Sprintf("%s %s", rt.authType, authCredentials)) } return rt.rt.RoundTrip(req) } -func (rt *bearerAuthFileRoundTripper) CloseIdleConnections() { +func (rt *authorizationCredentialsFileRoundTripper) CloseIdleConnections() { if ci, ok := rt.rt.(closeIdler); ok { ci.CloseIdleConnections() } diff --git a/vendor/github.com/prometheus/common/model/time.go b/vendor/github.com/prometheus/common/model/time.go index c40e6403..77b82b2b 100644 --- a/vendor/github.com/prometheus/common/model/time.go +++ b/vendor/github.com/prometheus/common/model/time.go @@ -254,6 +254,18 @@ func (d Duration) String() string { return r } +// MarshalText implements the encoding.TextMarshaler interface. +func (d *Duration) MarshalText() ([]byte, error) { + return []byte(d.String()), nil +} + +// UnmarshalText implements the encoding.TextUnmarshaler interface. +func (d *Duration) UnmarshalText(text []byte) error { + var err error + *d, err = ParseDuration(string(text)) + return err +} + // MarshalYAML implements the yaml.Marshaler interface. func (d Duration) MarshalYAML() (interface{}, error) { return d.String(), nil diff --git a/vendor/modules.txt b/vendor/modules.txt index 4c061972..ff17e71a 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -167,7 +167,7 @@ github.com/prometheus/client_golang/prometheus/internal github.com/prometheus/client_golang/prometheus/promhttp # github.com/prometheus/client_model v0.2.0 github.com/prometheus/client_model/go -# github.com/prometheus/common v0.15.0 +# github.com/prometheus/common v0.18.0 ## explicit github.com/prometheus/common/config github.com/prometheus/common/expfmt