Skip to content

Commit 560c3ba

Browse files
Merge pull request #23 from stripe/pspieker-add-per-request-https-proxy-support
Add support for per-request HTTPS traffic Proxying
2 parents dbbdf2f + 22108c1 commit 560c3ba

File tree

2 files changed

+100
-10
lines changed

2 files changed

+100
-10
lines changed

https.go

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,12 @@ const (
3333
)
3434

3535
var (
36-
OkConnect = &ConnectAction{Action: ConnectAccept, TLSConfig: TLSConfigFromCA(&GoproxyCa)}
37-
MitmConnect = &ConnectAction{Action: ConnectMitm, TLSConfig: TLSConfigFromCA(&GoproxyCa)}
38-
HTTPMitmConnect = &ConnectAction{Action: ConnectHTTPMitm, TLSConfig: TLSConfigFromCA(&GoproxyCa)}
39-
RejectConnect = &ConnectAction{Action: ConnectReject, TLSConfig: TLSConfigFromCA(&GoproxyCa)}
40-
httpsRegexp = regexp.MustCompile(`^https:\/\/`)
36+
OkConnect = &ConnectAction{Action: ConnectAccept, TLSConfig: TLSConfigFromCA(&GoproxyCa)}
37+
MitmConnect = &ConnectAction{Action: ConnectMitm, TLSConfig: TLSConfigFromCA(&GoproxyCa)}
38+
HTTPMitmConnect = &ConnectAction{Action: ConnectHTTPMitm, TLSConfig: TLSConfigFromCA(&GoproxyCa)}
39+
RejectConnect = &ConnectAction{Action: ConnectReject, TLSConfig: TLSConfigFromCA(&GoproxyCa)}
40+
httpsRegexp = regexp.MustCompile(`^https:\/\/`)
41+
PerRequestHTTPSProxyHeaderKey = "X-Upstream-Https-Proxy"
4142
)
4243

4344
type ConnectAction struct {
@@ -84,8 +85,8 @@ func (proxy *ProxyHttpServer) connectDialContext(ctx *ProxyCtx, network, addr st
8485
}
8586

8687
func (proxy *ProxyHttpServer) handleHttps(w http.ResponseWriter, r *http.Request) {
87-
ctx := &ProxyCtx{Req: r, Session: atomic.AddInt64(&proxy.sess, 1), proxy: proxy}
8888

89+
ctx := &ProxyCtx{Req: r, Session: atomic.AddInt64(&proxy.sess, 1), proxy: proxy}
8990
hij, ok := w.(http.Hijacker)
9091
if !ok {
9192
panic("httpserver does not support hijacking")
@@ -118,7 +119,12 @@ func (proxy *ProxyHttpServer) handleHttps(w http.ResponseWriter, r *http.Request
118119
host += ":80"
119120
}
120121

121-
httpsProxy, err := httpsProxyAddr(r.URL, proxy.HttpsProxyAddr)
122+
var httpsProxyURL string = proxy.HttpsProxyAddr
123+
if r.Header.Get(PerRequestHTTPSProxyHeaderKey) != "" {
124+
httpsProxyURL = r.Header.Get(PerRequestHTTPSProxyHeaderKey)
125+
}
126+
127+
httpsProxy, err := httpsProxyAddr(r.URL, httpsProxyURL)
122128
if err != nil {
123129
ctx.Warnf("Error configuring HTTPS proxy err=%q url=%q", err, r.URL.String())
124130
}
@@ -565,7 +571,7 @@ func (proxy *ProxyHttpServer) connectDialProxyWithContext(ctx *ProxyCtx, proxyHo
565571
return c, nil
566572
}
567573

568-
// httpsProxyAddr function uses the address in httpsProxy parameter.
574+
// httpsProxyAddr function uses the address in httpsProxy parameter.
569575
// When the httpProxyAddr parameter is empty, uses the HTTPS_PROXY, https_proxy from environment variables.
570576
// httpsProxyAddr function allows goproxy to respect no_proxy env vars
571577
// https://github.com/stripe/goproxy/pull/5

proxy_test.go

Lines changed: 86 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -722,7 +722,7 @@ func TestHttpProxyAddrsFromEnv(t *testing.T) {
722722
os.Setenv("http_proxy", l.URL)
723723
os.Setenv("https_proxy", l.URL)
724724
proxy2 := goproxy.NewProxyHttpServer()
725-
725+
726726
client, l2 := oneShotProxy(proxy2, t)
727727
defer l2.Close()
728728
if r := string(getOrFail(https.URL+"/bobo", client, t)); r != "bobo bobo" {
@@ -733,6 +733,90 @@ func TestHttpProxyAddrsFromEnv(t *testing.T) {
733733
os.Unsetenv("https_proxy")
734734
}
735735

736+
func TestOverrideHttpsProxyAddrsFromEnvWithRequest(t *testing.T) {
737+
// The request essentially does:
738+
// Client -> FakeStripeEgressProxy -> FakeExternalProxy -> FinalDestination
739+
finalDestinationUrl := "https://httpbin.org/get"
740+
741+
// We'll use this counter to mark whether FakeStripeEgressProxy has been called
742+
c := 0
743+
744+
// TODO(pspieker): figure out why this doesn't work - for now, this is fine,
745+
// but we should fix this in the medium term before a wider launch
746+
//
747+
// We should set the env vars here to ensure that our per-request config overrides these
748+
// os.Setenv("http_proxy", "http://incorrectproxy.com")
749+
// os.Setenv("https_proxy", "http://incorrectproxy.com")
750+
751+
fakeExternalProxy := goproxy.NewProxyHttpServer()
752+
fakeExternalProxyTestStruct := httptest.NewServer(fakeExternalProxy)
753+
fakeExternalProxy.OnRequest().HandleConnect(goproxy.AlwaysMitm)
754+
tagExternalProxyPassthrough := func(resp *http.Response, ctx *goproxy.ProxyCtx) *http.Response {
755+
b, err := ioutil.ReadAll(resp.Body)
756+
panicOnErr(err, "readAll resp")
757+
resp.Body = ioutil.NopCloser(bytes.NewBufferString(string(b) + "-externalproxy"))
758+
return resp
759+
}
760+
fakeExternalProxy.OnResponse().DoFunc(tagExternalProxyPassthrough)
761+
762+
fakeStripeEgressProxy := goproxy.NewProxyHttpServer()
763+
// We set the CONNECT response handler function to increment our counter such that we can tell
764+
// if our FakeStripeEgressProxy was actually called
765+
fakeStripeEgressProxy.ConnectRespHandler = func(ctx *goproxy.ProxyCtx, resp *http.Response) error {
766+
c += 1
767+
return nil
768+
}
769+
fakeStripeEgressProxyTestStruct := httptest.NewServer(fakeStripeEgressProxy)
770+
771+
// Next, we construct the client that we'll be using to talk to our 2 proxies
772+
egressProxyUrl, _ := url.Parse(fakeStripeEgressProxyTestStruct.URL)
773+
tr := &http.Transport{
774+
TLSClientConfig: acceptAllCerts,
775+
Proxy: http.ProxyURL(egressProxyUrl),
776+
ProxyConnectHeader: map[string][]string{
777+
goproxy.PerRequestHTTPSProxyHeaderKey: {fakeExternalProxyTestStruct.URL},
778+
},
779+
}
780+
client := &http.Client{Transport: tr}
781+
782+
req, err := http.NewRequest("GET", finalDestinationUrl, nil)
783+
if err != nil {
784+
t.Fatal("Unable to construct request!")
785+
}
786+
787+
req.Header.Set("X-Test-Header-Key", "Test-Header-Value")
788+
789+
res, err := client.Do(req)
790+
if err != nil {
791+
t.Fatal("Unable to make the request!")
792+
}
793+
794+
bodyBytes, err := io.ReadAll(res.Body)
795+
if err != nil {
796+
t.Fatal("Unable to parse the response bytes!")
797+
}
798+
resBody := string(bodyBytes)
799+
800+
// Making sure we received the response we expected from the final destination
801+
if !strings.Contains(resBody, "\"X-Test-Header-Key\": \"Test-Header-Value\"") {
802+
t.Error("Expected the passed request headers to be present in the response body!")
803+
}
804+
805+
// Ensuring the external proxy was routed through
806+
if !strings.Contains(resBody, "-externalproxy") {
807+
t.Error("Expected the request have been passed through the external proxy on the way to the final destination!")
808+
}
809+
810+
// Ensuring the "internal" egress proxy was routed through
811+
if c != 1 {
812+
t.Error("Expected the internal egress proxy to have been passed through!")
813+
}
814+
815+
// TODO(pspieker): see comment above
816+
// os.Unsetenv("http_proxy")
817+
// os.Unsetenv("https_proxy")
818+
}
819+
736820
func TestCustomHttpProxyAddrs(t *testing.T) {
737821
proxy := goproxy.NewProxyHttpServer()
738822
doubleString := func(resp *http.Response, ctx *goproxy.ProxyCtx) *http.Response {
@@ -748,7 +832,7 @@ func TestCustomHttpProxyAddrs(t *testing.T) {
748832
defer l.Close()
749833

750834
proxy2 := goproxy.NewProxyHttpServer(goproxy.WithHttpProxyAddr(l.URL), goproxy.WithHttpsProxyAddr(l.URL))
751-
835+
752836
client, l2 := oneShotProxy(proxy2, t)
753837
defer l2.Close()
754838
if r := string(getOrFail(https.URL+"/bobo", client, t)); r != "bobo bobo" {

0 commit comments

Comments
 (0)