Skip to content

Commit

Permalink
Add forwarding headers on incoming requests, add travis (#6)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
suyashkumar authored Oct 14, 2018
1 parent 93013be commit a4f05cd
Show file tree
Hide file tree
Showing 7 changed files with 141 additions and 7 deletions.
9 changes: 9 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
language: go
go:
- "1.10.x"
- "1.11.x"
install:
- go get -u github.com/golang/dep/cmd/dep
script:
- make
- make test
20 changes: 19 additions & 1 deletion Gopkg.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ build:

.PHONY: test
test:
go test ./...
go test -v ./...

.PHONY: run
run:
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<img src="https://suyashkumar.com/assets/img/lock.png" width="64">
<h3 align="center">ssl-proxy</h3>
<p align="center">Simple single-command SSL reverse proxy with autogenerated certificates (LetsEncrypt, self-signed)<p>
<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>
<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>
</p>
</p>

Expand Down
8 changes: 4 additions & 4 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ import (
"flag"
"log"
"net/http"
"net/http/httputil"
"net/url"
"os"
"time"

"strings"

"github.com/suyashkumar/ssl-proxy/gen"
"github.com/suyashkumar/ssl-proxy/reverseproxy"
"golang.org/x/crypto/acme/autocert"
)

Expand Down Expand Up @@ -79,10 +79,10 @@ func main() {
log.Fatal("Unable to parse 'to' url: ", err)
}

// Setup ServeMux
localProxy := httputil.NewSingleHostReverseProxy(toURL)
// Setup reverse proxy ServeMux
p := reverseproxy.Build(toURL)
mux := http.NewServeMux()
mux.Handle("/", localProxy)
mux.Handle("/", p)

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

Expand Down
59 changes: 59 additions & 0 deletions reverseproxy/reverseproxy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package reverseproxy

import (
"net/http"
"net/http/httputil"
"net/url"
"strings"
)

// Build initializes and returns a new ReverseProxy instance suitable for SSL proxying
func Build(toURL *url.URL) *httputil.ReverseProxy {
localProxy := &httputil.ReverseProxy{}
addProxyHeaders := func(req *http.Request) {
req.Header.Set(http.CanonicalHeaderKey("X-Forwarded-Proto"), "https")
req.Header.Set(http.CanonicalHeaderKey("X-Forwarded-Port"), "443") // TODO: inherit another port if needed
}
localProxy.Director = newDirector(toURL, addProxyHeaders)

return localProxy
}

// newDirector creates a base director that should be exactly what http.NewSingleHostReverseProxy() creates, but allows
// for the caller to supply and extraDirector function to decorate to request to the downstream server
func newDirector(target *url.URL, extraDirector func(*http.Request)) func(*http.Request) {
targetQuery := target.RawQuery
return func(req *http.Request) {
req.URL.Scheme = target.Scheme
req.URL.Host = target.Host
req.URL.Path = singleJoiningSlash(target.Path, req.URL.Path)
if targetQuery == "" || req.URL.RawQuery == "" {
req.URL.RawQuery = targetQuery + req.URL.RawQuery
} else {
req.URL.RawQuery = targetQuery + "&" + req.URL.RawQuery
}
if _, ok := req.Header["User-Agent"]; !ok {
// explicitly disable User-Agent so it's not set to default value
req.Header.Set("User-Agent", "")
}

if extraDirector != nil {
extraDirector(req)
}
}
}

// singleJoiningSlash is a utility function that adds a single slash to a URL where appropriate, copied from
// the httputil package
// TODO: add test to ensure behavior does not diverge from httputil's implementation, as per Rob Pike's proverbs
func singleJoiningSlash(a, b string) string {
aslash := strings.HasSuffix(a, "/")
bslash := strings.HasPrefix(b, "/")
switch {
case aslash && bslash:
return a + b[1:]
case !aslash && !bslash:
return a + "/" + b
}
return a + b
}
48 changes: 48 additions & 0 deletions reverseproxy/reverseproxy_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package reverseproxy

import (
"net/http"
"net/http/httptest"
"net/http/httputil"
"net/url"
"testing"

"github.com/stretchr/testify/assert"
)

// TestBuild_AddHeaders tests that Build's returned ReverseProxy Director adds the proper request headers
func TestBuild_AddHeaders(t *testing.T) {
u, err := url.Parse("http://127.0.0.1")
assert.Nil(t, err, "error should be nil")
proxy := Build(u)
assert.NotNil(t, proxy, "Build should not return nil")

req := httptest.NewRequest("GET", "/test", nil)
proxy.Director(req)

// Check that headers were added to req
assert.Equal(t, req.Header.Get(http.CanonicalHeaderKey("X-Forwarded-Proto")), "https",
"X-Forwarded-Proto should be present")
assert.Equal(t, req.Header.Get(http.CanonicalHeaderKey("X-Forwarded-Port")), "443",
"X-Forwarded-Port should be present")

}

func TestNewDirector(t *testing.T) {
u, err := url.Parse("http://127.0.0.1")
assert.Nil(t, err, "error should be nil")
director := newDirector(u, nil)

defaultProxy := httputil.NewSingleHostReverseProxy(u)
defaultDirector := defaultProxy.Director

expectedReq := httptest.NewRequest("GET", "/test", nil)
testReq := httptest.NewRequest("GET", "/test", nil)

defaultDirector(expectedReq)
director(testReq)

assert.EqualValues(t, expectedReq, testReq,
"default proxy and package directors should modify the request in the same way")
// TODO: add more test cases
}

0 comments on commit a4f05cd

Please sign in to comment.