diff --git a/config/http_config.go b/config/http_config.go index 1bc2d06b..73163206 100644 --- a/config/http_config.go +++ b/config/http_config.go @@ -31,6 +31,7 @@ import ( "time" "github.com/mwitkow/go-conntrack" + "golang.org/x/net/http/httpproxy" "golang.org/x/net/http2" "golang.org/x/oauth2" "golang.org/x/oauth2/clientcredentials" @@ -227,11 +228,26 @@ type OAuth2 struct { Scopes []string `yaml:"scopes,omitempty" json:"scopes,omitempty"` TokenURL string `yaml:"token_url" json:"token_url"` EndpointParams map[string]string `yaml:"endpoint_params,omitempty" json:"endpoint_params,omitempty"` + TLSConfig TLSConfig `yaml:"tls_config,omitempty"` + ProxyConfig `yaml:",inline"` +} - // HTTP proxy server to use to connect to the targets. - ProxyURL URL `yaml:"proxy_url,omitempty" json:"proxy_url,omitempty"` - // TLSConfig is used to connect to the token URL. - TLSConfig TLSConfig `yaml:"tls_config,omitempty"` +// UnmarshalYAML implements the yaml.Unmarshaler interface +func (o *OAuth2) UnmarshalYAML(unmarshal func(interface{}) error) error { + type plain OAuth2 + if err := unmarshal((*plain)(o)); err != nil { + return err + } + return o.ProxyConfig.Validate() +} + +// UnmarshalJSON implements the json.Marshaler interface for URL. +func (o *OAuth2) UnmarshalJSON(data []byte) error { + type plain OAuth2 + if err := json.Unmarshal(data, (*plain)(o)); err != nil { + return err + } + return o.ProxyConfig.Validate() } // SetDirectory joins any relative file paths with dir. @@ -281,13 +297,6 @@ type HTTPClientConfig struct { // The bearer token file for the targets. Deprecated in favour of // Authorization.CredentialsFile. BearerTokenFile string `yaml:"bearer_token_file,omitempty" json:"bearer_token_file,omitempty"` - // HTTP proxy server to use to connect to the targets. - ProxyURL URL `yaml:"proxy_url,omitempty" json:"proxy_url,omitempty"` - // ProxyConnectHeader optionally specifies headers to send to - // proxies during CONNECT requests. Assume that at least _some_ of - // these headers are going to contain secrets and use Secret as the - // value type instead of string. - ProxyConnectHeader Header `yaml:"proxy_connect_header,omitempty" json:"proxy_connect_header,omitempty"` // TLSConfig to use to connect to the targets. TLSConfig TLSConfig `yaml:"tls_config,omitempty" json:"tls_config,omitempty"` // FollowRedirects specifies whether the client should follow HTTP 3xx redirects. @@ -298,6 +307,8 @@ type HTTPClientConfig struct { // The omitempty flag is not set, because it would be hidden from the // marshalled configuration when set to false. EnableHTTP2 bool `yaml:"enable_http2" json:"enable_http2"` + // Proxy configuration. + ProxyConfig `yaml:",inline"` } // SetDirectory joins any relative file paths with dir. @@ -372,8 +383,8 @@ func (c *HTTPClientConfig) Validate() error { return fmt.Errorf("at most one of oauth2 client_secret & client_secret_file must be configured") } } - if len(c.ProxyConnectHeader) > 0 && (c.ProxyURL.URL == nil || c.ProxyURL.String() == "") { - return fmt.Errorf("if proxy_connect_header is configured proxy_url must also be configured") + if err := c.ProxyConfig.Validate(); err != nil { + return err } return nil } @@ -502,8 +513,8 @@ func NewRoundTripperFromConfig(cfg HTTPClientConfig, name string, optFuncs ...HT // The only timeout we care about is the configured scrape timeout. // It is applied on request. So we leave out any timings here. var rt http.RoundTripper = &http.Transport{ - Proxy: http.ProxyURL(cfg.ProxyURL.URL), - ProxyConnectHeader: cfg.ProxyConnectHeader.HTTPHeader(), + Proxy: cfg.ProxyConfig.Proxy(), + ProxyConnectHeader: cfg.ProxyConfig.GetProxyConnectHeader(), MaxIdleConns: 20000, MaxIdleConnsPerHost: 1000, // see https://github.com/golang/go/issues/13801 DisableKeepAlives: !opts.keepAlivesEnabled, @@ -724,7 +735,8 @@ func (rt *oauth2RoundTripper) RoundTrip(req *http.Request) (*http.Response, erro tlsTransport := func(tlsConfig *tls.Config) (http.RoundTripper, error) { return &http.Transport{ TLSClientConfig: tlsConfig, - Proxy: http.ProxyURL(rt.config.ProxyURL.URL), + Proxy: rt.config.ProxyConfig.Proxy(), + ProxyConnectHeader: rt.config.ProxyConfig.GetProxyConnectHeader(), DisableKeepAlives: !rt.opts.keepAlivesEnabled, MaxIdleConns: 20, MaxIdleConnsPerHost: 1, // see https://github.com/golang/go/issues/13801 @@ -1072,3 +1084,78 @@ func (c HTTPClientConfig) String() string { } return string(b) } + +type ProxyConfig struct { + // HTTP proxy server to use to connect to the targets. + ProxyURL URL `yaml:"proxy_url,omitempty" json:"proxy_url,omitempty"` + // NoProxy contains addresses that should not use a proxy. + NoProxy string `yaml:"no_proxy,omitempty" json:"no_proxy,omitempty"` + // ProxyFromEnvironment makes use of net/http ProxyFromEnvironment function + // to determine proxies. + ProxyFromEnvironment bool `yaml:"proxy_from_environment,omitempty" json:"proxy_from_environment,omitempty"` + // ProxyConnectHeader optionally specifies headers to send to + // proxies during CONNECT requests. Assume that at least _some_ of + // these headers are going to contain secrets and use Secret as the + // value type instead of string. + ProxyConnectHeader Header `yaml:"proxy_connect_header,omitempty" json:"proxy_connect_header,omitempty"` + + proxyFunc func(*http.Request) (*url.URL, error) +} + +// UnmarshalYAML implements the yaml.Unmarshaler interface. +func (c *ProxyConfig) Validate() error { + if len(c.ProxyConnectHeader) > 0 && (!c.ProxyFromEnvironment && (c.ProxyURL.URL == nil || c.ProxyURL.String() == "")) { + return fmt.Errorf("if proxy_connect_header is configured, proxy_url or proxy_from_environment must also be configured") + } + if c.ProxyFromEnvironment && c.ProxyURL.URL != nil && c.ProxyURL.String() != "" { + return fmt.Errorf("if proxy_from_environment is configured, proxy_url must not be configured") + } + if c.ProxyFromEnvironment && c.NoProxy != "" { + return fmt.Errorf("if proxy_from_environment is configured, no_proxy must not be configured") + } + if c.ProxyURL.URL == nil && c.NoProxy != "" { + return fmt.Errorf("if no_proxy is configured, proxy_url must also be configured") + } + return nil +} + +// Proxy returns the Proxy URL for a request. +func (c *ProxyConfig) Proxy() (fn func(*http.Request) (*url.URL, error)) { + if c == nil { + return nil + } + defer func() { + fn = c.proxyFunc + }() + if c.proxyFunc != nil { + return + } + if c.ProxyFromEnvironment { + proxyFn := httpproxy.FromEnvironment().ProxyFunc() + c.proxyFunc = func(req *http.Request) (*url.URL, error) { + return proxyFn(req.URL) + } + return + } + if c.ProxyURL.URL != nil && c.ProxyURL.URL.String() != "" { + if c.NoProxy == "" { + c.proxyFunc = http.ProxyURL(c.ProxyURL.URL) + return + } + proxy := &httpproxy.Config{ + HTTPProxy: c.ProxyURL.String(), + HTTPSProxy: c.ProxyURL.String(), + NoProxy: c.NoProxy, + } + proxyFn := proxy.ProxyFunc() + c.proxyFunc = func(req *http.Request) (*url.URL, error) { + return proxyFn(req.URL) + } + } + return +} + +// ProxyConnectHeader() return the Proxy Connext Headers. +func (c *ProxyConfig) GetProxyConnectHeader() http.Header { + return c.ProxyConnectHeader.HTTPHeader() +} diff --git a/config/http_config_test.go b/config/http_config_test.go index fbe65037..638a1332 100644 --- a/config/http_config_test.go +++ b/config/http_config_test.go @@ -119,6 +119,18 @@ var invalidHTTPClientConfigs = []struct { httpClientConfigFile: "testdata/http.conf.oauth2-no-token-url.bad.yaml", errMsg: "oauth2 token_url must be configured", }, + { + httpClientConfigFile: "testdata/http.conf.proxy-from-env.bad.yaml", + errMsg: "if proxy_from_environment is configured, proxy_url must not be configured", + }, + { + httpClientConfigFile: "testdata/http.conf.no-proxy.bad.yaml", + errMsg: "if proxy_from_environment is configured, no_proxy must not be configured", + }, + { + httpClientConfigFile: "testdata/http.conf.no-proxy-without-proxy-url.bad.yaml", + errMsg: "if no_proxy is configured, proxy_url must also be configured", + }, } func newTestServer(handler func(w http.ResponseWriter, r *http.Request)) (*httptest.Server, error) { @@ -1689,3 +1701,140 @@ func loadHTTPConfigJSONFile(filename string) (*HTTPClientConfig, []byte, error) } return cfg, content, nil } + +func TestProxyConfig_Proxy(t *testing.T) { + var proxyServer *httptest.Server + + defer func() { + if proxyServer != nil { + proxyServer.Close() + } + }() + + proxyServerHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, "Hello, %s", r.URL.Path) + }) + + proxyServer = httptest.NewServer(proxyServerHandler) + + testCases := []struct { + name string + proxyConfig string + expectedProxyURL string + targetURL string + proxyEnv string + noProxyEnv string + }{ + { + name: "proxy from environment", + proxyConfig: `proxy_from_environment: true`, + expectedProxyURL: proxyServer.URL, + proxyEnv: proxyServer.URL, + targetURL: "http://prometheus.io/", + }, + { + name: "proxy_from_environment with no_proxy", + proxyConfig: `proxy_from_environment: true`, + expectedProxyURL: "", + proxyEnv: proxyServer.URL, + noProxyEnv: "prometheus.io", + targetURL: "http://prometheus.io/", + }, + { + name: "proxy_from_environment and localhost", + proxyConfig: `proxy_from_environment: true`, + expectedProxyURL: "", + proxyEnv: proxyServer.URL, + targetURL: "http://localhost/", + }, + { + name: "valid proxy_url and localhost", + proxyConfig: fmt.Sprintf(`proxy_url: %s`, proxyServer.URL), + expectedProxyURL: proxyServer.URL, + targetURL: "http://localhost/", + }, + { + name: "valid proxy_url and no_proxy and localhost", + proxyConfig: fmt.Sprintf(`proxy_url: %s +no_proxy: prometheus.io`, proxyServer.URL), + expectedProxyURL: "", + targetURL: "http://localhost/", + }, + { + name: "valid proxy_url", + proxyConfig: fmt.Sprintf(`proxy_url: %s`, proxyServer.URL), + expectedProxyURL: proxyServer.URL, + targetURL: "http://prometheus.io/", + }, + { + name: "valid proxy url and no_proxy", + proxyConfig: fmt.Sprintf(`proxy_url: %s +no_proxy: prometheus.io`, proxyServer.URL), + expectedProxyURL: "", + targetURL: "http://prometheus.io/", + }, + { + name: "valid proxy url and no_proxies", + proxyConfig: fmt.Sprintf(`proxy_url: %s +no_proxy: promcon.io,prometheus.io,cncf.io`, proxyServer.URL), + expectedProxyURL: "", + targetURL: "http://prometheus.io/", + }, + { + name: "valid proxy url and no_proxies that do not include target", + proxyConfig: fmt.Sprintf(`proxy_url: %s +no_proxy: promcon.io,cncf.io`, proxyServer.URL), + expectedProxyURL: proxyServer.URL, + targetURL: "http://prometheus.io/", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + if proxyServer != nil { + defer proxyServer.Close() + } + + var proxyConfig ProxyConfig + + err := yaml.Unmarshal([]byte(tc.proxyConfig), &proxyConfig) + if err != nil { + t.Errorf("failed to unmarshal proxy config: %v", err) + return + } + + if tc.proxyEnv != "" { + currentProxy := os.Getenv("HTTP_PROXY") + t.Cleanup(func() { os.Setenv("HTTP_PROXY", currentProxy) }) + os.Setenv("HTTP_PROXY", tc.proxyEnv) + } + + if tc.noProxyEnv != "" { + currentProxy := os.Getenv("NO_PROXY") + t.Cleanup(func() { os.Setenv("NO_PROXY", currentProxy) }) + os.Setenv("NO_PROXY", tc.noProxyEnv) + } + + req := httptest.NewRequest("GET", tc.targetURL, nil) + + proxyFunc := proxyConfig.Proxy() + resultURL, err := proxyFunc(req) + + if err != nil { + t.Fatalf("expected no error, but got: %v", err) + return + } + if tc.expectedProxyURL == "" && resultURL != nil { + t.Fatalf("expected no result URL, but got: %s", resultURL.String()) + return + } + if tc.expectedProxyURL != "" && resultURL == nil { + t.Fatalf("expected result URL, but got nil") + return + } + if tc.expectedProxyURL != "" && resultURL.String() != tc.expectedProxyURL { + t.Fatalf("expected result URL: %s, but got: %s", tc.expectedProxyURL, resultURL.String()) + } + }) + } +} diff --git a/config/testdata/http.conf.no-proxy-without-proxy-url.bad.yaml b/config/testdata/http.conf.no-proxy-without-proxy-url.bad.yaml new file mode 100644 index 00000000..9c9ef353 --- /dev/null +++ b/config/testdata/http.conf.no-proxy-without-proxy-url.bad.yaml @@ -0,0 +1 @@ +no_proxy: 127.0.0.1 diff --git a/config/testdata/http.conf.no-proxy.bad.yaml b/config/testdata/http.conf.no-proxy.bad.yaml new file mode 100644 index 00000000..3d7a1252 --- /dev/null +++ b/config/testdata/http.conf.no-proxy.bad.yaml @@ -0,0 +1,2 @@ +proxy_from_environment: true +no_proxy: 127.0.0.1 diff --git a/config/testdata/http.conf.proxy-from-env.bad.yaml b/config/testdata/http.conf.proxy-from-env.bad.yaml new file mode 100644 index 00000000..fd177a6e --- /dev/null +++ b/config/testdata/http.conf.proxy-from-env.bad.yaml @@ -0,0 +1,2 @@ +proxy_from_environment: true +proxy_url: foo