Skip to content

Commit

Permalink
Merge pull request #462 from roidelapluie/proxyfromenv
Browse files Browse the repository at this point in the history
Add support to use Proxy From Environment
  • Loading branch information
roidelapluie committed Mar 8, 2023
2 parents 6a5f4db + c8ca1fb commit 66b493f
Show file tree
Hide file tree
Showing 5 changed files with 257 additions and 16 deletions.
119 changes: 103 additions & 16 deletions config/http_config.go
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(),
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")
}
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()
}
149 changes: 149 additions & 0 deletions config/http_config_test.go
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())
}
})
}
}
@@ -0,0 +1 @@
no_proxy: 127.0.0.1
2 changes: 2 additions & 0 deletions config/testdata/http.conf.no-proxy.bad.yaml
@@ -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
@@ -0,0 +1,2 @@
proxy_from_environment: true
proxy_url: foo

0 comments on commit 66b493f

Please sign in to comment.