Skip to content

Commit 7d6e36a

Browse files
committed
Add support for proxy connect headers
Some proxy configurations require additional headers to be able to use them (e.g. authorization token specific to the proxy). Fixes: #402 Signed-off-by: Marcelo E. Magallon <[email protected]>
1 parent bebc731 commit 7d6e36a

File tree

6 files changed

+191
-1
lines changed

6 files changed

+191
-1
lines changed

config/config.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package config
1818

1919
import (
2020
"encoding/json"
21+
"net/http"
2122
"path/filepath"
2223
)
2324

@@ -48,6 +49,29 @@ func (s Secret) MarshalJSON() ([]byte, error) {
4849
return json.Marshal(secretToken)
4950
}
5051

52+
type Header map[string][]Secret
53+
54+
func (h Header) HttpHeader() http.Header {
55+
if h == nil {
56+
return nil
57+
}
58+
59+
header := make(http.Header)
60+
61+
for name, values := range h {
62+
var s []string
63+
if values != nil {
64+
s = make([]string, 0, len(values))
65+
for _, value := range values {
66+
s = append(s, string(value))
67+
}
68+
}
69+
header[name] = s
70+
}
71+
72+
return header
73+
}
74+
5175
// DirectorySetter is a config type that contains file paths that may
5276
// be relative to the file containing the config.
5377
type DirectorySetter interface {

config/config_test.go

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,13 @@
1414
package config
1515

1616
import (
17+
"bytes"
1718
"encoding/json"
19+
"net/http"
20+
"reflect"
1821
"testing"
22+
23+
"gopkg.in/yaml.v2"
1924
)
2025

2126
func TestJSONMarshalSecret(t *testing.T) {
@@ -51,3 +56,110 @@ func TestJSONMarshalSecret(t *testing.T) {
5156
})
5257
}
5358
}
59+
60+
func TestHeaderHttpHeader(t *testing.T) {
61+
testcases := map[string]struct {
62+
header Header
63+
expected http.Header
64+
}{
65+
"basic": {
66+
header: Header{
67+
"single": []Secret{"v1"},
68+
"multi": []Secret{"v1", "v2"},
69+
"empty": []Secret{},
70+
"nil": nil,
71+
},
72+
expected: http.Header{
73+
"single": []string{"v1"},
74+
"multi": []string{"v1", "v2"},
75+
"empty": []string{},
76+
"nil": nil,
77+
},
78+
},
79+
"nil": {
80+
header: nil,
81+
expected: nil,
82+
},
83+
}
84+
85+
for name, tc := range testcases {
86+
t.Run(name, func(t *testing.T) {
87+
actual := tc.header.HttpHeader()
88+
if !reflect.DeepEqual(actual, tc.expected) {
89+
t.Fatalf("expecting: %#v, actual: %#v", tc.expected, actual)
90+
}
91+
})
92+
}
93+
}
94+
95+
func TestHeaderUnmarshal(t *testing.T) {
96+
testcases := map[string]struct {
97+
input string
98+
expected Header
99+
}{
100+
"void": {
101+
input: ``,
102+
},
103+
"simple": {
104+
input: "single:\n- a\n",
105+
expected: Header{"single": []Secret{"a"}},
106+
},
107+
"multi": {
108+
input: "multi:\n- a\n- b\n",
109+
expected: Header{"multi": []Secret{"a", "b"}},
110+
},
111+
"empty": {
112+
input: "empty:\n",
113+
expected: Header{"empty": nil},
114+
},
115+
}
116+
117+
for name, tc := range testcases {
118+
t.Run(name, func(t *testing.T) {
119+
var actual Header
120+
err := yaml.Unmarshal([]byte(tc.input), &actual)
121+
if err != nil {
122+
t.Fatalf("error unmarshaling %s: %s", tc.input, err)
123+
}
124+
if !reflect.DeepEqual(actual, tc.expected) {
125+
t.Fatalf("expecting: %#v, actual: %#v", tc.expected, actual)
126+
}
127+
})
128+
}
129+
}
130+
131+
func TestHeaderMarshal(t *testing.T) {
132+
testcases := map[string]struct {
133+
input Header
134+
expected []byte
135+
}{
136+
"void": {
137+
input: nil,
138+
expected: []byte("{}\n"),
139+
},
140+
"simple": {
141+
input: Header{"single": []Secret{"a"}},
142+
expected: []byte("single:\n- <secret>\n"),
143+
},
144+
"multi": {
145+
input: Header{"multi": []Secret{"a", "b"}},
146+
expected: []byte("multi:\n- <secret>\n- <secret>\n"),
147+
},
148+
"empty": {
149+
input: Header{"empty": nil},
150+
expected: []byte("empty: []\n"),
151+
},
152+
}
153+
154+
for name, tc := range testcases {
155+
t.Run(name, func(t *testing.T) {
156+
actual, err := yaml.Marshal(tc.input)
157+
if err != nil {
158+
t.Fatalf("error unmarshaling %#v: %s", tc.input, err)
159+
}
160+
if !bytes.Equal(actual, tc.expected) {
161+
t.Fatalf("expecting: %q, actual: %q", tc.expected, actual)
162+
}
163+
})
164+
}
165+
}

config/http_config.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,11 @@ type HTTPClientConfig struct {
264264
BearerTokenFile string `yaml:"bearer_token_file,omitempty" json:"bearer_token_file,omitempty"`
265265
// HTTP proxy server to use to connect to the targets.
266266
ProxyURL URL `yaml:"proxy_url,omitempty" json:"proxy_url,omitempty"`
267+
// ProxyConnectHeader optionally specifies headers to send to
268+
// proxies during CONNECT requests. Assume that at least _some_ of
269+
// these headers are going to contain secrets and use Secret as the
270+
// value type instead of string.
271+
ProxyConnectHeader Header `yaml:"proxy_connect_header,omitempty" json:"proxy_connect_header,omitempty"`
267272
// TLSConfig to use to connect to the targets.
268273
TLSConfig TLSConfig `yaml:"tls_config,omitempty" json:"tls_config,omitempty"`
269274
// FollowRedirects specifies whether the client should follow HTTP 3xx redirects.
@@ -289,7 +294,8 @@ func (c *HTTPClientConfig) SetDirectory(dir string) {
289294
}
290295

291296
// Validate validates the HTTPClientConfig to check only one of BearerToken,
292-
// BasicAuth and BearerTokenFile is configured.
297+
// BasicAuth and BearerTokenFile is configured. It also validates that ProxyURL
298+
// is set if ProxyConnectHeader is set.
293299
func (c *HTTPClientConfig) Validate() error {
294300
// Backwards compatibility with the bearer_token field.
295301
if len(c.BearerToken) > 0 && len(c.BearerTokenFile) > 0 {
@@ -347,6 +353,9 @@ func (c *HTTPClientConfig) Validate() error {
347353
return fmt.Errorf("at most one of oauth2 client_secret & client_secret_file must be configured")
348354
}
349355
}
356+
if len(c.ProxyConnectHeader) > 0 && (c.ProxyURL.URL == nil || c.ProxyURL.String() == "") {
357+
return fmt.Errorf("if proxy_connect_header is configured proxy_url must also be configured")
358+
}
350359
return nil
351360
}
352361

@@ -475,6 +484,7 @@ func NewRoundTripperFromConfig(cfg HTTPClientConfig, name string, optFuncs ...HT
475484
// It is applied on request. So we leave out any timings here.
476485
var rt http.RoundTripper = &http.Transport{
477486
Proxy: http.ProxyURL(cfg.ProxyURL.URL),
487+
ProxyConnectHeader: cfg.ProxyConnectHeader.HttpHeader(),
478488
MaxIdleConns: 20000,
479489
MaxIdleConnsPerHost: 1000, // see https://github.com/golang/go/issues/13801
480490
DisableKeepAlives: !opts.keepAlivesEnabled,

config/http_config_test.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -447,6 +447,37 @@ func TestNewClientFromConfig(t *testing.T) {
447447
}
448448
}
449449

450+
func TestProxyConfiguration(t *testing.T) {
451+
testcases := map[string]struct {
452+
testdata string
453+
isValid bool
454+
}{
455+
"good": {
456+
testdata: "testdata/http.conf.proxy-headers.good.yml",
457+
isValid: true,
458+
},
459+
"bad": {
460+
testdata: "testdata/http.conf.proxy-headers.bad.yml",
461+
isValid: false,
462+
},
463+
}
464+
465+
for name, tc := range testcases {
466+
t.Run(name, func(t *testing.T) {
467+
_, _, err := LoadHTTPConfigFile(tc.testdata)
468+
if tc.isValid {
469+
if err != nil {
470+
t.Fatalf("Error validating %s: %s", tc.testdata, err)
471+
}
472+
} else {
473+
if err == nil {
474+
t.Fatalf("Expecting error validating %s but got %s", tc.testdata, err)
475+
}
476+
}
477+
})
478+
}
479+
}
480+
450481
func TestNewClientFromInvalidConfig(t *testing.T) {
451482
var newClientInvalidConfig = []struct {
452483
clientConfig HTTPClientConfig
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
proxy_connect_header:
2+
single:
3+
- value_0
4+
multi:
5+
- value_1
6+
- value_2
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
proxy_url: "http://remote.host"
2+
proxy_connect_header:
3+
single:
4+
- value_0
5+
multi:
6+
- value_1
7+
- value_2

0 commit comments

Comments
 (0)