Skip to content

Commit 4a0d730

Browse files
memJulien Pivotto
and
Julien Pivotto
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 Co-authored-by: Julien Pivotto <[email protected]> Signed-off-by: Marcelo E. Magallon <[email protected]>
1 parent befeabf commit 4a0d730

8 files changed

+330
-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 || *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: 192 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,190 @@ 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 TestHeaderYamlUnmarshal(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: "{}",
113+
expected: Header{},
114+
},
115+
"empty value": {
116+
input: "empty:\n",
117+
expected: Header{"empty": nil},
118+
},
119+
}
120+
121+
for name, tc := range testcases {
122+
t.Run(name, func(t *testing.T) {
123+
var actual Header
124+
err := yaml.Unmarshal([]byte(tc.input), &actual)
125+
if err != nil {
126+
t.Fatalf("error unmarshaling %s: %s", tc.input, err)
127+
}
128+
if !reflect.DeepEqual(actual, tc.expected) {
129+
t.Fatalf("expecting: %#v, actual: %#v", tc.expected, actual)
130+
}
131+
})
132+
}
133+
}
134+
135+
func TestHeaderYamlMarshal(t *testing.T) {
136+
testcases := map[string]struct {
137+
input Header
138+
expected []byte
139+
}{
140+
"void": {
141+
input: nil,
142+
expected: []byte("{}\n"),
143+
},
144+
"simple": {
145+
input: Header{"single": []Secret{"a"}},
146+
expected: []byte("single:\n- <secret>\n"),
147+
},
148+
"multi": {
149+
input: Header{"multi": []Secret{"a", "b"}},
150+
expected: []byte("multi:\n- <secret>\n- <secret>\n"),
151+
},
152+
"empty": {
153+
input: Header{"empty": nil},
154+
expected: []byte("empty: []\n"),
155+
},
156+
}
157+
158+
for name, tc := range testcases {
159+
t.Run(name, func(t *testing.T) {
160+
actual, err := yaml.Marshal(tc.input)
161+
if err != nil {
162+
t.Fatalf("error unmarshaling %#v: %s", tc.input, err)
163+
}
164+
if !bytes.Equal(actual, tc.expected) {
165+
t.Fatalf("expecting: %q, actual: %q", tc.expected, actual)
166+
}
167+
})
168+
}
169+
}
170+
171+
func TestHeaderJsonUnmarshal(t *testing.T) {
172+
testcases := map[string]struct {
173+
input string
174+
expected Header
175+
}{
176+
"void": {
177+
input: `null`,
178+
},
179+
"simple": {
180+
input: `{"single": ["a"]}`,
181+
expected: Header{"single": []Secret{"a"}},
182+
},
183+
"multi": {
184+
input: `{"multi": ["a", "b"]}`,
185+
expected: Header{"multi": []Secret{"a", "b"}},
186+
},
187+
"empty": {
188+
input: `{}`,
189+
expected: Header{},
190+
},
191+
"empty value": {
192+
input: `{"empty":null}`,
193+
expected: Header{"empty": nil},
194+
},
195+
}
196+
197+
for name, tc := range testcases {
198+
t.Run(name, func(t *testing.T) {
199+
var actual Header
200+
err := json.Unmarshal([]byte(tc.input), &actual)
201+
if err != nil {
202+
t.Fatalf("error unmarshaling %s: %s", tc.input, err)
203+
}
204+
if !reflect.DeepEqual(actual, tc.expected) {
205+
t.Fatalf("expecting: %#v, actual: %#v", tc.expected, actual)
206+
}
207+
})
208+
}
209+
}
210+
211+
func TestHeaderJsonMarshal(t *testing.T) {
212+
testcases := map[string]struct {
213+
input Header
214+
expected []byte
215+
}{
216+
"void": {
217+
input: nil,
218+
expected: []byte("null"),
219+
},
220+
"simple": {
221+
input: Header{"single": []Secret{"a"}},
222+
expected: []byte("{\"single\":[\"\\u003csecret\\u003e\"]}"),
223+
},
224+
"multi": {
225+
input: Header{"multi": []Secret{"a", "b"}},
226+
expected: []byte("{\"multi\":[\"\\u003csecret\\u003e\",\"\\u003csecret\\u003e\"]}"),
227+
},
228+
"empty": {
229+
input: Header{"empty": nil},
230+
expected: []byte(`{"empty":null}`),
231+
},
232+
}
233+
234+
for name, tc := range testcases {
235+
t.Run(name, func(t *testing.T) {
236+
actual, err := json.Marshal(tc.input)
237+
if err != nil {
238+
t.Fatalf("error marshaling %#v: %s", tc.input, err)
239+
}
240+
if !bytes.Equal(actual, tc.expected) {
241+
t.Fatalf("expecting: %q, actual: %q", tc.expected, actual)
242+
}
243+
})
244+
}
245+
}

config/http_config.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,11 @@ type HTTPClientConfig struct {
289289
BearerTokenFile string `yaml:"bearer_token_file,omitempty" json:"bearer_token_file,omitempty"`
290290
// HTTP proxy server to use to connect to the targets.
291291
ProxyURL URL `yaml:"proxy_url,omitempty" json:"proxy_url,omitempty"`
292+
// ProxyConnectHeader optionally specifies headers to send to
293+
// proxies during CONNECT requests. Assume that at least _some_ of
294+
// these headers are going to contain secrets and use Secret as the
295+
// value type instead of string.
296+
ProxyConnectHeader Header `yaml:"proxy_connect_header,omitempty" json:"proxy_connect_header,omitempty"`
292297
// TLSConfig to use to connect to the targets.
293298
TLSConfig TLSConfig `yaml:"tls_config,omitempty" json:"tls_config,omitempty"`
294299
// FollowRedirects specifies whether the client should follow HTTP 3xx redirects.
@@ -314,7 +319,8 @@ func (c *HTTPClientConfig) SetDirectory(dir string) {
314319
}
315320

316321
// Validate validates the HTTPClientConfig to check only one of BearerToken,
317-
// BasicAuth and BearerTokenFile is configured.
322+
// BasicAuth and BearerTokenFile is configured. It also validates that ProxyURL
323+
// is set if ProxyConnectHeader is set.
318324
func (c *HTTPClientConfig) Validate() error {
319325
// Backwards compatibility with the bearer_token field.
320326
if len(c.BearerToken) > 0 && len(c.BearerTokenFile) > 0 {
@@ -372,6 +378,9 @@ func (c *HTTPClientConfig) Validate() error {
372378
return fmt.Errorf("at most one of oauth2 client_secret & client_secret_file must be configured")
373379
}
374380
}
381+
if len(c.ProxyConnectHeader) > 0 && (c.ProxyURL.URL == nil || c.ProxyURL.String() == "") {
382+
return fmt.Errorf("if proxy_connect_header is configured proxy_url must also be configured")
383+
}
375384
return nil
376385
}
377386

@@ -500,6 +509,7 @@ func NewRoundTripperFromConfig(cfg HTTPClientConfig, name string, optFuncs ...HT
500509
// It is applied on request. So we leave out any timings here.
501510
var rt http.RoundTripper = &http.Transport{
502511
Proxy: http.ProxyURL(cfg.ProxyURL.URL),
512+
ProxyConnectHeader: cfg.ProxyConnectHeader.HTTPHeader(),
503513
MaxIdleConns: 20000,
504514
MaxIdleConnsPerHost: 1000, // see https://github.com/golang/go/issues/13801
505515
DisableKeepAlives: !opts.keepAlivesEnabled,

config/http_config_test.go

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

450+
func TestProxyConfiguration(t *testing.T) {
451+
testcases := map[string]struct {
452+
testFn string
453+
loader func(string) (*HTTPClientConfig, []byte, error)
454+
isValid bool
455+
}{
456+
"good yaml": {
457+
testFn: "testdata/http.conf.proxy-headers.good.yml",
458+
loader: LoadHTTPConfigFile,
459+
isValid: true,
460+
},
461+
"bad yaml": {
462+
testFn: "testdata/http.conf.proxy-headers.bad.yml",
463+
loader: LoadHTTPConfigFile,
464+
isValid: false,
465+
},
466+
"good json": {
467+
testFn: "testdata/http.conf.proxy-headers.good.json",
468+
loader: loadHTTPConfigJSONFile,
469+
isValid: true,
470+
},
471+
"bad json": {
472+
testFn: "testdata/http.conf.proxy-headers.bad.json",
473+
loader: loadHTTPConfigJSONFile,
474+
isValid: false,
475+
},
476+
}
477+
478+
for name, tc := range testcases {
479+
t.Run(name, func(t *testing.T) {
480+
_, _, err := tc.loader(tc.testFn)
481+
if tc.isValid {
482+
if err != nil {
483+
t.Fatalf("Error validating %s: %s", tc.testFn, err)
484+
}
485+
} else {
486+
if err == nil {
487+
t.Fatalf("Expecting error validating %s but got %s", tc.testFn, err)
488+
}
489+
}
490+
})
491+
}
492+
}
493+
450494
func TestNewClientFromInvalidConfig(t *testing.T) {
451495
var newClientInvalidConfig = []struct {
452496
clientConfig HTTPClientConfig
@@ -1622,3 +1666,26 @@ func TestModifyTLSCertificates(t *testing.T) {
16221666
})
16231667
}
16241668
}
1669+
1670+
// loadHTTPConfigJSON parses the JSON input s into a HTTPClientConfig.
1671+
func loadHTTPConfigJSON(buf []byte) (*HTTPClientConfig, error) {
1672+
cfg := &HTTPClientConfig{}
1673+
err := json.Unmarshal(buf, cfg)
1674+
if err != nil {
1675+
return nil, err
1676+
}
1677+
return cfg, nil
1678+
}
1679+
1680+
// loadHTTPConfigJSONFile parses the given JSON file into a HTTPClientConfig.
1681+
func loadHTTPConfigJSONFile(filename string) (*HTTPClientConfig, []byte, error) {
1682+
content, err := os.ReadFile(filename)
1683+
if err != nil {
1684+
return nil, nil, err
1685+
}
1686+
cfg, err := loadHTTPConfigJSON(content)
1687+
if err != nil {
1688+
return nil, nil, err
1689+
}
1690+
return cfg, content, nil
1691+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"proxy_connect_header": {
3+
"single": [
4+
"value_0"
5+
],
6+
"multi": [
7+
"value_1",
8+
"value_2"
9+
]
10+
}
11+
}
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: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"proxy_url": "http://remote.host",
3+
"proxy_connect_header": {
4+
"single": [
5+
"value_0"
6+
],
7+
"multi": [
8+
"value_1",
9+
"value_2"
10+
]
11+
}
12+
}

0 commit comments

Comments
 (0)