Skip to content

Commit

Permalink
add support for HSTS response headers
Browse files Browse the repository at this point in the history
  • Loading branch information
Aaron Hurt committed Jan 24, 2018
1 parent 2b7594a commit b121091
Show file tree
Hide file tree
Showing 6 changed files with 151 additions and 0 deletions.
7 changes: 7 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,13 @@ type Proxy struct {
TLSHeaderValue string
GZIPContentTypes *regexp.Regexp
RequestID string
STSHeader STSHeader
}

type STSHeader struct {
MaxAge int
Subdomains bool
Preload bool
}

type Runtime struct {
Expand Down
3 changes: 3 additions & 0 deletions config/load.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,9 @@ func load(cmdline, environ, envprefix []string, props *properties.Properties) (c
f.StringVar(&cfg.Proxy.TLSHeader, "proxy.header.tls", defaultConfig.Proxy.TLSHeader, "header for TLS connections")
f.StringVar(&cfg.Proxy.TLSHeaderValue, "proxy.header.tls.value", defaultConfig.Proxy.TLSHeaderValue, "value for TLS connection header")
f.StringVar(&cfg.Proxy.RequestID, "proxy.header.requestid", defaultConfig.Proxy.RequestID, "header for reqest id")
f.IntVar(&cfg.Proxy.STSHeader.MaxAge, "proxy.header.sts.maxage", defaultConfig.Proxy.STSHeader.MaxAge, "enable and set the max-age value for HSTS")
f.BoolVar(&cfg.Proxy.STSHeader.Subdomains, "proxy.header.sts.subdomains", defaultConfig.Proxy.STSHeader.Subdomains, "direct HSTS to include subdomains")
f.BoolVar(&cfg.Proxy.STSHeader.Preload, "proxy.header.sts.preload", defaultConfig.Proxy.STSHeader.Preload, "direct HSTS to pass the preload directive")
f.StringVar(&gzipContentTypesValue, "proxy.gzip.contenttype", defaultValues.GZIPContentTypesValue, "regexp of content types to compress")
f.StringVar(&listenerValue, "proxy.addr", defaultValues.ListenerValue, "listener config")
f.StringVar(&certSourcesValue, "proxy.cs", defaultValues.CertSourcesValue, "certificate sources")
Expand Down
32 changes: 32 additions & 0 deletions fabio.properties
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,38 @@
# proxy.header.requestid =


# proxy.header.sts.maxage enables and configures the max-age of HSTS for TLS requests.
# When set greater than zero this enables the Strict-Transport-Security header
# and sets the max-age value in the header.
#
# The default is
#
# proxy.header.sts.maxage = 0


# proxy.header.sts.subdomains instructs HSTS to include subdomains.
# When set to true, the 'includeSubDomains' option will be added to
# the Strict-Transport-Security header.
#
# The default is
#
# proxy.header.sts.subdomains = false


# proxy.header.sts.preload instructs HSTS to include the preload directive.
# When set to true, the 'preload' option will be added to the
# Strict-Transport-Security header.
#
# Sending the preload directive from your site can have PERMANENT CONSEQUENCES
# and prevent users from accessing your site and any of its subdomains if you
# find you need to switch back to HTTP. Please read the details at
# hstspreload.appspot.com/#removal before sending the header with "preload".
#
# The default is
#
# proxy.header.sts.preload = false


# proxy.gzip.contenttype configures which responses should be compressed.
#
# By default, responses sent to the client are not compressed even if the
Expand Down
39 changes: 39 additions & 0 deletions proxy/http_headers.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,22 @@ import (
"github.com/fabiolb/fabio/config"
)

// addResponseHeaders adds/updates headers in the response
func addResponseHeaders(w http.ResponseWriter, r *http.Request, cfg config.Proxy) error {
if r.TLS != nil && cfg.STSHeader.MaxAge > 0 {
sts := "max-age=" + i32toa(int32(cfg.STSHeader.MaxAge))
if cfg.STSHeader.Subdomains {
sts += "; includeSubdomains"
}
if cfg.STSHeader.Preload {
sts += "; preload"
}
w.Header().Set("Strict-Transport-Security", sts)
}

return nil
}

// addHeaders adds/updates headers in request
//
// * add/update `Forwarded` header
Expand Down Expand Up @@ -130,6 +146,29 @@ func uint16base16(n uint16) string {
return string(b)
}

// i32toa is a faster implentation of strconv.Itoa() without importing another library
// https://stackoverflow.com/a/39444005
func i32toa(n int32) string {
buf := [11]byte{}
pos := len(buf)
i := int64(n)
signed := i < 0
if signed {
i = -i
}
for {
pos--
buf[pos], i = '0'+byte(i%10), i/10
if i == 0 {
if signed {
pos--
buf[pos] = '-'
}
return string(buf[pos:])
}
}
}

// scheme derives the request scheme used on the initial
// request first from headers and then from the connection
// using the following heuristic:
Expand Down
65 changes: 65 additions & 0 deletions proxy/http_headers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"crypto/tls"
"fmt"
"net/http"
"net/http/httptest"
"testing"

"github.com/fabiolb/fabio/config"
Expand Down Expand Up @@ -386,6 +387,70 @@ func TestAddHeaders(t *testing.T) {
}
}

func TestAddResponseHeaders(t *testing.T) {
tests := []struct {
desc string
r *http.Request
cfg config.Proxy
hdrs http.Header
err string
}{
{"set Strict-Transport-Security for TLS, if MaxAge greater than 0",
&http.Request{RemoteAddr: "1.2.3.4:5555", TLS: &tls.ConnectionState{}},
config.Proxy{STSHeader: config.STSHeader{MaxAge: 31536000}},
http.Header{
"Strict-Transport-Security": []string{"max-age=31536000"},
},
"",
},

{"set Strict-Transport-Security with options for TLS, if MaxAge greater than 0 with options",
&http.Request{RemoteAddr: "1.2.3.4:5555", TLS: &tls.ConnectionState{}},
config.Proxy{STSHeader: config.STSHeader{MaxAge: 31536000, Preload: true, Subdomains: true}},
http.Header{
"Strict-Transport-Security": []string{"max-age=31536000; includeSubdomains; preload"},
},
"",
},

{"skip Strict-Transport-Security for non-TLS, if MaxAge greater than 0",
&http.Request{RemoteAddr: "1.2.3.4:5555"},
config.Proxy{STSHeader: config.STSHeader{MaxAge: 31536000}},
http.Header{},
"",
},
}

for i, tt := range tests {
tt := tt // capture loop var

t.Run(tt.desc, func(t *testing.T) {
if tt.r.Header == nil {
tt.r.Header = http.Header{}
}

w := httptest.NewRecorder()
err := addResponseHeaders(w, tt.r, tt.cfg)

if err != nil {
if got, want := err.Error(), tt.err; got != want {
t.Fatalf("%d: %s\ngot %q\nwant %q", i, tt.desc, got, want)
}
return
}

if tt.err != "" {
t.Fatalf("%d: got nil want %q", i, tt.err)
return
}

resp := w.Result()
got, want := resp.Header, tt.hdrs
verify.Values(t, "", got, want)
})
}
}

func TestLocalPort(t *testing.T) {
tests := []struct {
r *http.Request
Expand Down
5 changes: 5 additions & 0 deletions proxy/http_proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,11 @@ func (p *HTTPProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return
}

if err := addResponseHeaders(w, r, p.Config); err != nil {
http.Error(w, "cannot add response headers", http.StatusInternalServerError)
return
}

upgrade, accept := r.Header.Get("Upgrade"), r.Header.Get("Accept")

tr := p.Transport
Expand Down

0 comments on commit b121091

Please sign in to comment.