Skip to content

Commit 66b493f

Browse files
author
Julien Pivotto
authored
Merge pull request #462 from roidelapluie/proxyfromenv
Add support to use Proxy From Environment
2 parents 6a5f4db + c8ca1fb commit 66b493f

5 files changed

+257
-16
lines changed

config/http_config.go

+103-16
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import (
3131
"time"
3232

3333
"github.com/mwitkow/go-conntrack"
34+
"golang.org/x/net/http/httpproxy"
3435
"golang.org/x/net/http2"
3536
"golang.org/x/oauth2"
3637
"golang.org/x/oauth2/clientcredentials"
@@ -227,11 +228,26 @@ type OAuth2 struct {
227228
Scopes []string `yaml:"scopes,omitempty" json:"scopes,omitempty"`
228229
TokenURL string `yaml:"token_url" json:"token_url"`
229230
EndpointParams map[string]string `yaml:"endpoint_params,omitempty" json:"endpoint_params,omitempty"`
231+
TLSConfig TLSConfig `yaml:"tls_config,omitempty"`
232+
ProxyConfig `yaml:",inline"`
233+
}
230234

231-
// HTTP proxy server to use to connect to the targets.
232-
ProxyURL URL `yaml:"proxy_url,omitempty" json:"proxy_url,omitempty"`
233-
// TLSConfig is used to connect to the token URL.
234-
TLSConfig TLSConfig `yaml:"tls_config,omitempty"`
235+
// UnmarshalYAML implements the yaml.Unmarshaler interface
236+
func (o *OAuth2) UnmarshalYAML(unmarshal func(interface{}) error) error {
237+
type plain OAuth2
238+
if err := unmarshal((*plain)(o)); err != nil {
239+
return err
240+
}
241+
return o.ProxyConfig.Validate()
242+
}
243+
244+
// UnmarshalJSON implements the json.Marshaler interface for URL.
245+
func (o *OAuth2) UnmarshalJSON(data []byte) error {
246+
type plain OAuth2
247+
if err := json.Unmarshal(data, (*plain)(o)); err != nil {
248+
return err
249+
}
250+
return o.ProxyConfig.Validate()
235251
}
236252

237253
// SetDirectory joins any relative file paths with dir.
@@ -281,13 +297,6 @@ type HTTPClientConfig struct {
281297
// The bearer token file for the targets. Deprecated in favour of
282298
// Authorization.CredentialsFile.
283299
BearerTokenFile string `yaml:"bearer_token_file,omitempty" json:"bearer_token_file,omitempty"`
284-
// HTTP proxy server to use to connect to the targets.
285-
ProxyURL URL `yaml:"proxy_url,omitempty" json:"proxy_url,omitempty"`
286-
// ProxyConnectHeader optionally specifies headers to send to
287-
// proxies during CONNECT requests. Assume that at least _some_ of
288-
// these headers are going to contain secrets and use Secret as the
289-
// value type instead of string.
290-
ProxyConnectHeader Header `yaml:"proxy_connect_header,omitempty" json:"proxy_connect_header,omitempty"`
291300
// TLSConfig to use to connect to the targets.
292301
TLSConfig TLSConfig `yaml:"tls_config,omitempty" json:"tls_config,omitempty"`
293302
// FollowRedirects specifies whether the client should follow HTTP 3xx redirects.
@@ -298,6 +307,8 @@ type HTTPClientConfig struct {
298307
// The omitempty flag is not set, because it would be hidden from the
299308
// marshalled configuration when set to false.
300309
EnableHTTP2 bool `yaml:"enable_http2" json:"enable_http2"`
310+
// Proxy configuration.
311+
ProxyConfig `yaml:",inline"`
301312
}
302313

303314
// SetDirectory joins any relative file paths with dir.
@@ -372,8 +383,8 @@ func (c *HTTPClientConfig) Validate() error {
372383
return fmt.Errorf("at most one of oauth2 client_secret & client_secret_file must be configured")
373384
}
374385
}
375-
if len(c.ProxyConnectHeader) > 0 && (c.ProxyURL.URL == nil || c.ProxyURL.String() == "") {
376-
return fmt.Errorf("if proxy_connect_header is configured proxy_url must also be configured")
386+
if err := c.ProxyConfig.Validate(); err != nil {
387+
return err
377388
}
378389
return nil
379390
}
@@ -502,8 +513,8 @@ func NewRoundTripperFromConfig(cfg HTTPClientConfig, name string, optFuncs ...HT
502513
// The only timeout we care about is the configured scrape timeout.
503514
// It is applied on request. So we leave out any timings here.
504515
var rt http.RoundTripper = &http.Transport{
505-
Proxy: http.ProxyURL(cfg.ProxyURL.URL),
506-
ProxyConnectHeader: cfg.ProxyConnectHeader.HTTPHeader(),
516+
Proxy: cfg.ProxyConfig.Proxy(),
517+
ProxyConnectHeader: cfg.ProxyConfig.GetProxyConnectHeader(),
507518
MaxIdleConns: 20000,
508519
MaxIdleConnsPerHost: 1000, // see https://github.com/golang/go/issues/13801
509520
DisableKeepAlives: !opts.keepAlivesEnabled,
@@ -724,7 +735,8 @@ func (rt *oauth2RoundTripper) RoundTrip(req *http.Request) (*http.Response, erro
724735
tlsTransport := func(tlsConfig *tls.Config) (http.RoundTripper, error) {
725736
return &http.Transport{
726737
TLSClientConfig: tlsConfig,
727-
Proxy: http.ProxyURL(rt.config.ProxyURL.URL),
738+
Proxy: rt.config.ProxyConfig.Proxy(),
739+
ProxyConnectHeader: rt.config.ProxyConfig.GetProxyConnectHeader(),
728740
DisableKeepAlives: !rt.opts.keepAlivesEnabled,
729741
MaxIdleConns: 20,
730742
MaxIdleConnsPerHost: 1, // see https://github.com/golang/go/issues/13801
@@ -1072,3 +1084,78 @@ func (c HTTPClientConfig) String() string {
10721084
}
10731085
return string(b)
10741086
}
1087+
1088+
type ProxyConfig struct {
1089+
// HTTP proxy server to use to connect to the targets.
1090+
ProxyURL URL `yaml:"proxy_url,omitempty" json:"proxy_url,omitempty"`
1091+
// NoProxy contains addresses that should not use a proxy.
1092+
NoProxy string `yaml:"no_proxy,omitempty" json:"no_proxy,omitempty"`
1093+
// ProxyFromEnvironment makes use of net/http ProxyFromEnvironment function
1094+
// to determine proxies.
1095+
ProxyFromEnvironment bool `yaml:"proxy_from_environment,omitempty" json:"proxy_from_environment,omitempty"`
1096+
// ProxyConnectHeader optionally specifies headers to send to
1097+
// proxies during CONNECT requests. Assume that at least _some_ of
1098+
// these headers are going to contain secrets and use Secret as the
1099+
// value type instead of string.
1100+
ProxyConnectHeader Header `yaml:"proxy_connect_header,omitempty" json:"proxy_connect_header,omitempty"`
1101+
1102+
proxyFunc func(*http.Request) (*url.URL, error)
1103+
}
1104+
1105+
// UnmarshalYAML implements the yaml.Unmarshaler interface.
1106+
func (c *ProxyConfig) Validate() error {
1107+
if len(c.ProxyConnectHeader) > 0 && (!c.ProxyFromEnvironment && (c.ProxyURL.URL == nil || c.ProxyURL.String() == "")) {
1108+
return fmt.Errorf("if proxy_connect_header is configured, proxy_url or proxy_from_environment must also be configured")
1109+
}
1110+
if c.ProxyFromEnvironment && c.ProxyURL.URL != nil && c.ProxyURL.String() != "" {
1111+
return fmt.Errorf("if proxy_from_environment is configured, proxy_url must not be configured")
1112+
}
1113+
if c.ProxyFromEnvironment && c.NoProxy != "" {
1114+
return fmt.Errorf("if proxy_from_environment is configured, no_proxy must not be configured")
1115+
}
1116+
if c.ProxyURL.URL == nil && c.NoProxy != "" {
1117+
return fmt.Errorf("if no_proxy is configured, proxy_url must also be configured")
1118+
}
1119+
return nil
1120+
}
1121+
1122+
// Proxy returns the Proxy URL for a request.
1123+
func (c *ProxyConfig) Proxy() (fn func(*http.Request) (*url.URL, error)) {
1124+
if c == nil {
1125+
return nil
1126+
}
1127+
defer func() {
1128+
fn = c.proxyFunc
1129+
}()
1130+
if c.proxyFunc != nil {
1131+
return
1132+
}
1133+
if c.ProxyFromEnvironment {
1134+
proxyFn := httpproxy.FromEnvironment().ProxyFunc()
1135+
c.proxyFunc = func(req *http.Request) (*url.URL, error) {
1136+
return proxyFn(req.URL)
1137+
}
1138+
return
1139+
}
1140+
if c.ProxyURL.URL != nil && c.ProxyURL.URL.String() != "" {
1141+
if c.NoProxy == "" {
1142+
c.proxyFunc = http.ProxyURL(c.ProxyURL.URL)
1143+
return
1144+
}
1145+
proxy := &httpproxy.Config{
1146+
HTTPProxy: c.ProxyURL.String(),
1147+
HTTPSProxy: c.ProxyURL.String(),
1148+
NoProxy: c.NoProxy,
1149+
}
1150+
proxyFn := proxy.ProxyFunc()
1151+
c.proxyFunc = func(req *http.Request) (*url.URL, error) {
1152+
return proxyFn(req.URL)
1153+
}
1154+
}
1155+
return
1156+
}
1157+
1158+
// ProxyConnectHeader() return the Proxy Connext Headers.
1159+
func (c *ProxyConfig) GetProxyConnectHeader() http.Header {
1160+
return c.ProxyConnectHeader.HTTPHeader()
1161+
}

config/http_config_test.go

+149
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,18 @@ var invalidHTTPClientConfigs = []struct {
119119
httpClientConfigFile: "testdata/http.conf.oauth2-no-token-url.bad.yaml",
120120
errMsg: "oauth2 token_url must be configured",
121121
},
122+
{
123+
httpClientConfigFile: "testdata/http.conf.proxy-from-env.bad.yaml",
124+
errMsg: "if proxy_from_environment is configured, proxy_url must not be configured",
125+
},
126+
{
127+
httpClientConfigFile: "testdata/http.conf.no-proxy.bad.yaml",
128+
errMsg: "if proxy_from_environment is configured, no_proxy must not be configured",
129+
},
130+
{
131+
httpClientConfigFile: "testdata/http.conf.no-proxy-without-proxy-url.bad.yaml",
132+
errMsg: "if no_proxy is configured, proxy_url must also be configured",
133+
},
122134
}
123135

124136
func newTestServer(handler func(w http.ResponseWriter, r *http.Request)) (*httptest.Server, error) {
@@ -1689,3 +1701,140 @@ func loadHTTPConfigJSONFile(filename string) (*HTTPClientConfig, []byte, error)
16891701
}
16901702
return cfg, content, nil
16911703
}
1704+
1705+
func TestProxyConfig_Proxy(t *testing.T) {
1706+
var proxyServer *httptest.Server
1707+
1708+
defer func() {
1709+
if proxyServer != nil {
1710+
proxyServer.Close()
1711+
}
1712+
}()
1713+
1714+
proxyServerHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
1715+
fmt.Fprintf(w, "Hello, %s", r.URL.Path)
1716+
})
1717+
1718+
proxyServer = httptest.NewServer(proxyServerHandler)
1719+
1720+
testCases := []struct {
1721+
name string
1722+
proxyConfig string
1723+
expectedProxyURL string
1724+
targetURL string
1725+
proxyEnv string
1726+
noProxyEnv string
1727+
}{
1728+
{
1729+
name: "proxy from environment",
1730+
proxyConfig: `proxy_from_environment: true`,
1731+
expectedProxyURL: proxyServer.URL,
1732+
proxyEnv: proxyServer.URL,
1733+
targetURL: "http://prometheus.io/",
1734+
},
1735+
{
1736+
name: "proxy_from_environment with no_proxy",
1737+
proxyConfig: `proxy_from_environment: true`,
1738+
expectedProxyURL: "",
1739+
proxyEnv: proxyServer.URL,
1740+
noProxyEnv: "prometheus.io",
1741+
targetURL: "http://prometheus.io/",
1742+
},
1743+
{
1744+
name: "proxy_from_environment and localhost",
1745+
proxyConfig: `proxy_from_environment: true`,
1746+
expectedProxyURL: "",
1747+
proxyEnv: proxyServer.URL,
1748+
targetURL: "http://localhost/",
1749+
},
1750+
{
1751+
name: "valid proxy_url and localhost",
1752+
proxyConfig: fmt.Sprintf(`proxy_url: %s`, proxyServer.URL),
1753+
expectedProxyURL: proxyServer.URL,
1754+
targetURL: "http://localhost/",
1755+
},
1756+
{
1757+
name: "valid proxy_url and no_proxy and localhost",
1758+
proxyConfig: fmt.Sprintf(`proxy_url: %s
1759+
no_proxy: prometheus.io`, proxyServer.URL),
1760+
expectedProxyURL: "",
1761+
targetURL: "http://localhost/",
1762+
},
1763+
{
1764+
name: "valid proxy_url",
1765+
proxyConfig: fmt.Sprintf(`proxy_url: %s`, proxyServer.URL),
1766+
expectedProxyURL: proxyServer.URL,
1767+
targetURL: "http://prometheus.io/",
1768+
},
1769+
{
1770+
name: "valid proxy url and no_proxy",
1771+
proxyConfig: fmt.Sprintf(`proxy_url: %s
1772+
no_proxy: prometheus.io`, proxyServer.URL),
1773+
expectedProxyURL: "",
1774+
targetURL: "http://prometheus.io/",
1775+
},
1776+
{
1777+
name: "valid proxy url and no_proxies",
1778+
proxyConfig: fmt.Sprintf(`proxy_url: %s
1779+
no_proxy: promcon.io,prometheus.io,cncf.io`, proxyServer.URL),
1780+
expectedProxyURL: "",
1781+
targetURL: "http://prometheus.io/",
1782+
},
1783+
{
1784+
name: "valid proxy url and no_proxies that do not include target",
1785+
proxyConfig: fmt.Sprintf(`proxy_url: %s
1786+
no_proxy: promcon.io,cncf.io`, proxyServer.URL),
1787+
expectedProxyURL: proxyServer.URL,
1788+
targetURL: "http://prometheus.io/",
1789+
},
1790+
}
1791+
1792+
for _, tc := range testCases {
1793+
t.Run(tc.name, func(t *testing.T) {
1794+
if proxyServer != nil {
1795+
defer proxyServer.Close()
1796+
}
1797+
1798+
var proxyConfig ProxyConfig
1799+
1800+
err := yaml.Unmarshal([]byte(tc.proxyConfig), &proxyConfig)
1801+
if err != nil {
1802+
t.Errorf("failed to unmarshal proxy config: %v", err)
1803+
return
1804+
}
1805+
1806+
if tc.proxyEnv != "" {
1807+
currentProxy := os.Getenv("HTTP_PROXY")
1808+
t.Cleanup(func() { os.Setenv("HTTP_PROXY", currentProxy) })
1809+
os.Setenv("HTTP_PROXY", tc.proxyEnv)
1810+
}
1811+
1812+
if tc.noProxyEnv != "" {
1813+
currentProxy := os.Getenv("NO_PROXY")
1814+
t.Cleanup(func() { os.Setenv("NO_PROXY", currentProxy) })
1815+
os.Setenv("NO_PROXY", tc.noProxyEnv)
1816+
}
1817+
1818+
req := httptest.NewRequest("GET", tc.targetURL, nil)
1819+
1820+
proxyFunc := proxyConfig.Proxy()
1821+
resultURL, err := proxyFunc(req)
1822+
1823+
if err != nil {
1824+
t.Fatalf("expected no error, but got: %v", err)
1825+
return
1826+
}
1827+
if tc.expectedProxyURL == "" && resultURL != nil {
1828+
t.Fatalf("expected no result URL, but got: %s", resultURL.String())
1829+
return
1830+
}
1831+
if tc.expectedProxyURL != "" && resultURL == nil {
1832+
t.Fatalf("expected result URL, but got nil")
1833+
return
1834+
}
1835+
if tc.expectedProxyURL != "" && resultURL.String() != tc.expectedProxyURL {
1836+
t.Fatalf("expected result URL: %s, but got: %s", tc.expectedProxyURL, resultURL.String())
1837+
}
1838+
})
1839+
}
1840+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
no_proxy: 127.0.0.1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
proxy_from_environment: true
2+
no_proxy: 127.0.0.1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
proxy_from_environment: true
2+
proxy_url: foo

0 commit comments

Comments
 (0)