Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support to use Proxy From Environment #462

Merged
merged 1 commit into from
Mar 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
119 changes: 103 additions & 16 deletions config/http_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand All @@ -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.
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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(),
roidelapluie marked this conversation as resolved.
Show resolved Hide resolved
DisableKeepAlives: !rt.opts.keepAlivesEnabled,
MaxIdleConns: 20,
MaxIdleConnsPerHost: 1, // see https://github.com/golang/go/issues/13801
Expand Down Expand Up @@ -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")
}
roidelapluie marked this conversation as resolved.
Show resolved Hide resolved
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)
}
}
roidelapluie marked this conversation as resolved.
Show resolved Hide resolved
return
}

// ProxyConnectHeader() return the Proxy Connext Headers.
func (c *ProxyConfig) GetProxyConnectHeader() http.Header {
return c.ProxyConnectHeader.HTTPHeader()
}
149 changes: 149 additions & 0 deletions config/http_config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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())
}
})
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
no_proxy: 127.0.0.1
2 changes: 2 additions & 0 deletions config/testdata/http.conf.no-proxy.bad.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
proxy_from_environment: true
no_proxy: 127.0.0.1
2 changes: 2 additions & 0 deletions config/testdata/http.conf.proxy-from-env.bad.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
proxy_from_environment: true
proxy_url: foo