Skip to content

Commit b9a6417

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 <marcelo.magallon@grafana.com>
1 parent 8c9cb3f commit b9a6417

File tree

6 files changed

+193
-1
lines changed

6 files changed

+193
-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: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"crypto/x509"
2222
"encoding/json"
2323
"fmt"
24+
"io/ioutil"
2425
"net"
2526
"net/http"
2627
"net/url"
@@ -264,6 +265,11 @@ type HTTPClientConfig struct {
264265
BearerTokenFile string `yaml:"bearer_token_file,omitempty" json:"bearer_token_file,omitempty"`
265266
// HTTP proxy server to use to connect to the targets.
266267
ProxyURL URL `yaml:"proxy_url,omitempty" json:"proxy_url,omitempty"`
268+
// ProxyConnectHeader optionally specifies headers to send to
269+
// proxies during CONNECT requests. Assume that at least _some_ of
270+
// these headers are going to contain secrets and use Secret as the
271+
// value type instead of string.
272+
ProxyConnectHeader Header `yaml:"proxy_connect_header,omitempty" json:"proxy_connect_header,omitempty"`
267273
// TLSConfig to use to connect to the targets.
268274
TLSConfig TLSConfig `yaml:"tls_config,omitempty" json:"tls_config,omitempty"`
269275
// FollowRedirects specifies whether the client should follow HTTP 3xx redirects.
@@ -289,7 +295,8 @@ func (c *HTTPClientConfig) SetDirectory(dir string) {
289295
}
290296

291297
// Validate validates the HTTPClientConfig to check only one of BearerToken,
292-
// BasicAuth and BearerTokenFile is configured.
298+
// BasicAuth and BearerTokenFile is configured. It also validates that ProxyURL
299+
// is set if ProxyConnectHeader is set.
293300
func (c *HTTPClientConfig) Validate() error {
294301
// Backwards compatibility with the bearer_token field.
295302
if len(c.BearerToken) > 0 && len(c.BearerTokenFile) > 0 {
@@ -347,6 +354,9 @@ func (c *HTTPClientConfig) Validate() error {
347354
return fmt.Errorf("at most one of oauth2 client_secret & client_secret_file must be configured")
348355
}
349356
}
357+
if len(c.ProxyConnectHeader) > 0 && (c.ProxyURL.URL == nil || c.ProxyURL.String() == "") {
358+
return fmt.Errorf("if proxy_connect_header is configured proxy_url must also be configured")
359+
}
350360
return nil
351361
}
352362

@@ -475,6 +485,7 @@ func NewRoundTripperFromConfig(cfg HTTPClientConfig, name string, optFuncs ...HT
475485
// It is applied on request. So we leave out any timings here.
476486
var rt http.RoundTripper = &http.Transport{
477487
Proxy: http.ProxyURL(cfg.ProxyURL.URL),
488+
ProxyConnectHeader: cfg.ProxyConnectHeader.HttpHeader(),
478489
MaxIdleConns: 20000,
479490
MaxIdleConnsPerHost: 1000, // see https://github.com/golang/go/issues/13801
480491
DisableKeepAlives: !opts.keepAlivesEnabled,

config/http_config_test.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"errors"
2222
"fmt"
2323
"io"
24+
"io/ioutil"
2425
"net"
2526
"net/http"
2627
"net/http/httptest"
@@ -447,6 +448,37 @@ func TestNewClientFromConfig(t *testing.T) {
447448
}
448449
}
449450

451+
func TestProxyConfiguration(t *testing.T) {
452+
testcases := map[string]struct {
453+
testdata string
454+
isValid bool
455+
}{
456+
"good": {
457+
testdata: "testdata/http.conf.proxy-headers.good.yml",
458+
isValid: true,
459+
},
460+
"bad": {
461+
testdata: "testdata/http.conf.proxy-headers.bad.yml",
462+
isValid: false,
463+
},
464+
}
465+
466+
for name, tc := range testcases {
467+
t.Run(name, func(t *testing.T) {
468+
_, _, err := LoadHTTPConfigFile(tc.testdata)
469+
if tc.isValid {
470+
if err != nil {
471+
t.Fatalf("Error validating %s: %s", tc.testdata, err)
472+
}
473+
} else {
474+
if err == nil {
475+
t.Fatalf("Expecting error validating %s but got %s", tc.testdata, err)
476+
}
477+
}
478+
})
479+
}
480+
}
481+
450482
func TestNewClientFromInvalidConfig(t *testing.T) {
451483
var newClientInvalidConfig = []struct {
452484
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)