Skip to content

Commit a4f05cd

Browse files
authored
Add forwarding headers on incoming requests, add travis (#6)
This change injects the X-Forwarded-Proto and X-Forwarded-Port headers to all incoming requests to the proxy. Useful for applications like Jenkins to behave well behind SSL proxies. This change also adds a Travis CI build.
1 parent 93013be commit a4f05cd

File tree

7 files changed

+141
-7
lines changed

7 files changed

+141
-7
lines changed

.travis.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
language: go
2+
go:
3+
- "1.10.x"
4+
- "1.11.x"
5+
install:
6+
- go get -u github.com/golang/dep/cmd/dep
7+
script:
8+
- make
9+
- make test

Gopkg.lock

Lines changed: 19 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ build:
77

88
.PHONY: test
99
test:
10-
go test ./...
10+
go test -v ./...
1111

1212
.PHONY: run
1313
run:

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<img src="https://suyashkumar.com/assets/img/lock.png" width="64">
33
<h3 align="center">ssl-proxy</h3>
44
<p align="center">Simple single-command SSL reverse proxy with autogenerated certificates (LetsEncrypt, self-signed)<p>
5-
<p align="center"> <a href="https://goreportcard.com/report/github.com/suyashkumar/ssl-proxy"><img src="https://goreportcard.com/badge/github.com/suyashkumar/ssl-proxy" alt=""></a> <a href="https://godoc.org/github.com/suyashkumar/ssl-proxy"><img src="https://godoc.org/github.com/suyashkumar/ssl-proxy?status.svg" alt=""></a>
5+
<p align="center"> <a href="https://goreportcard.com/report/github.com/suyashkumar/ssl-proxy"><img src="https://goreportcard.com/badge/github.com/suyashkumar/ssl-proxy" alt=""></a> <a href="https://travis-ci.com/suyashkumar/ssl-proxy"><img src="https://travis-ci.com/suyashkumar/ssl-proxy.svg?branch=master" /></a> <a href="https://godoc.org/github.com/suyashkumar/ssl-proxy"><img src="https://godoc.org/github.com/suyashkumar/ssl-proxy?status.svg" alt=""></a>
66
</p>
77
</p>
88

main.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,14 @@ import (
44
"flag"
55
"log"
66
"net/http"
7-
"net/http/httputil"
87
"net/url"
98
"os"
109
"time"
1110

1211
"strings"
1312

1413
"github.com/suyashkumar/ssl-proxy/gen"
14+
"github.com/suyashkumar/ssl-proxy/reverseproxy"
1515
"golang.org/x/crypto/acme/autocert"
1616
)
1717

@@ -79,10 +79,10 @@ func main() {
7979
log.Fatal("Unable to parse 'to' url: ", err)
8080
}
8181

82-
// Setup ServeMux
83-
localProxy := httputil.NewSingleHostReverseProxy(toURL)
82+
// Setup reverse proxy ServeMux
83+
p := reverseproxy.Build(toURL)
8484
mux := http.NewServeMux()
85-
mux.Handle("/", localProxy)
85+
mux.Handle("/", p)
8686

8787
log.Printf("Proxying calls from https://%s (SSL/TLS) to %s", *fromURL, toURL)
8888

reverseproxy/reverseproxy.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package reverseproxy
2+
3+
import (
4+
"net/http"
5+
"net/http/httputil"
6+
"net/url"
7+
"strings"
8+
)
9+
10+
// Build initializes and returns a new ReverseProxy instance suitable for SSL proxying
11+
func Build(toURL *url.URL) *httputil.ReverseProxy {
12+
localProxy := &httputil.ReverseProxy{}
13+
addProxyHeaders := func(req *http.Request) {
14+
req.Header.Set(http.CanonicalHeaderKey("X-Forwarded-Proto"), "https")
15+
req.Header.Set(http.CanonicalHeaderKey("X-Forwarded-Port"), "443") // TODO: inherit another port if needed
16+
}
17+
localProxy.Director = newDirector(toURL, addProxyHeaders)
18+
19+
return localProxy
20+
}
21+
22+
// newDirector creates a base director that should be exactly what http.NewSingleHostReverseProxy() creates, but allows
23+
// for the caller to supply and extraDirector function to decorate to request to the downstream server
24+
func newDirector(target *url.URL, extraDirector func(*http.Request)) func(*http.Request) {
25+
targetQuery := target.RawQuery
26+
return func(req *http.Request) {
27+
req.URL.Scheme = target.Scheme
28+
req.URL.Host = target.Host
29+
req.URL.Path = singleJoiningSlash(target.Path, req.URL.Path)
30+
if targetQuery == "" || req.URL.RawQuery == "" {
31+
req.URL.RawQuery = targetQuery + req.URL.RawQuery
32+
} else {
33+
req.URL.RawQuery = targetQuery + "&" + req.URL.RawQuery
34+
}
35+
if _, ok := req.Header["User-Agent"]; !ok {
36+
// explicitly disable User-Agent so it's not set to default value
37+
req.Header.Set("User-Agent", "")
38+
}
39+
40+
if extraDirector != nil {
41+
extraDirector(req)
42+
}
43+
}
44+
}
45+
46+
// singleJoiningSlash is a utility function that adds a single slash to a URL where appropriate, copied from
47+
// the httputil package
48+
// TODO: add test to ensure behavior does not diverge from httputil's implementation, as per Rob Pike's proverbs
49+
func singleJoiningSlash(a, b string) string {
50+
aslash := strings.HasSuffix(a, "/")
51+
bslash := strings.HasPrefix(b, "/")
52+
switch {
53+
case aslash && bslash:
54+
return a + b[1:]
55+
case !aslash && !bslash:
56+
return a + "/" + b
57+
}
58+
return a + b
59+
}

reverseproxy/reverseproxy_test.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package reverseproxy
2+
3+
import (
4+
"net/http"
5+
"net/http/httptest"
6+
"net/http/httputil"
7+
"net/url"
8+
"testing"
9+
10+
"github.com/stretchr/testify/assert"
11+
)
12+
13+
// TestBuild_AddHeaders tests that Build's returned ReverseProxy Director adds the proper request headers
14+
func TestBuild_AddHeaders(t *testing.T) {
15+
u, err := url.Parse("http://127.0.0.1")
16+
assert.Nil(t, err, "error should be nil")
17+
proxy := Build(u)
18+
assert.NotNil(t, proxy, "Build should not return nil")
19+
20+
req := httptest.NewRequest("GET", "/test", nil)
21+
proxy.Director(req)
22+
23+
// Check that headers were added to req
24+
assert.Equal(t, req.Header.Get(http.CanonicalHeaderKey("X-Forwarded-Proto")), "https",
25+
"X-Forwarded-Proto should be present")
26+
assert.Equal(t, req.Header.Get(http.CanonicalHeaderKey("X-Forwarded-Port")), "443",
27+
"X-Forwarded-Port should be present")
28+
29+
}
30+
31+
func TestNewDirector(t *testing.T) {
32+
u, err := url.Parse("http://127.0.0.1")
33+
assert.Nil(t, err, "error should be nil")
34+
director := newDirector(u, nil)
35+
36+
defaultProxy := httputil.NewSingleHostReverseProxy(u)
37+
defaultDirector := defaultProxy.Director
38+
39+
expectedReq := httptest.NewRequest("GET", "/test", nil)
40+
testReq := httptest.NewRequest("GET", "/test", nil)
41+
42+
defaultDirector(expectedReq)
43+
director(testReq)
44+
45+
assert.EqualValues(t, expectedReq, testReq,
46+
"default proxy and package directors should modify the request in the same way")
47+
// TODO: add more test cases
48+
}

0 commit comments

Comments
 (0)