feat(http): Allow user supplied HTTP headers

This patch adds the [http.headers] subsection to the configuration file
that allows users to supply headers that will be returned in all HTTP
responses.

Applying this patch will:

* Add code to implement new configuration items.
* Add test to ensure configuration is properly parsed.
* Add test to ensure http response headers are set
* Update sample configuration file
pull/18667/head
Ayan George 2020-06-23 12:53:33 -04:00
parent 1e7a2e234a
commit dde8231d5c
4 changed files with 114 additions and 30 deletions

View File

@ -341,6 +341,12 @@
# Setting this to 0 or setting max-concurrent-write-limit to 0 disables the limit.
# enqueued-write-timeout = 0
# User supplied HTTP response headers
#
# [http.headers]
# X-Header-1 = "Header Value 1"
# X-Header-2 = "Header Value 2"
###
### [logging]
###

View File

@ -32,36 +32,38 @@ const (
// Config represents a configuration for a HTTP service.
type Config struct {
Enabled bool `toml:"enabled"`
BindAddress string `toml:"bind-address"`
AuthEnabled bool `toml:"auth-enabled"`
LogEnabled bool `toml:"log-enabled"`
SuppressWriteLog bool `toml:"suppress-write-log"`
WriteTracing bool `toml:"write-tracing"`
FluxEnabled bool `toml:"flux-enabled"`
FluxLogEnabled bool `toml:"flux-log-enabled"`
PprofEnabled bool `toml:"pprof-enabled"`
PprofAuthEnabled bool `toml:"pprof-auth-enabled"`
DebugPprofEnabled bool `toml:"debug-pprof-enabled"`
PingAuthEnabled bool `toml:"ping-auth-enabled"`
HTTPSEnabled bool `toml:"https-enabled"`
HTTPSCertificate string `toml:"https-certificate"`
HTTPSPrivateKey string `toml:"https-private-key"`
MaxRowLimit int `toml:"max-row-limit"`
MaxConnectionLimit int `toml:"max-connection-limit"`
SharedSecret string `toml:"shared-secret"`
Realm string `toml:"realm"`
UnixSocketEnabled bool `toml:"unix-socket-enabled"`
UnixSocketGroup *toml.Group `toml:"unix-socket-group"`
UnixSocketPermissions toml.FileMode `toml:"unix-socket-permissions"`
BindSocket string `toml:"bind-socket"`
MaxBodySize int `toml:"max-body-size"`
AccessLogPath string `toml:"access-log-path"`
AccessLogStatusFilters []StatusFilter `toml:"access-log-status-filters"`
MaxConcurrentWriteLimit int `toml:"max-concurrent-write-limit"`
MaxEnqueuedWriteLimit int `toml:"max-enqueued-write-limit"`
EnqueuedWriteTimeout time.Duration `toml:"enqueued-write-timeout"`
TLS *tls.Config `toml:"-"`
Enabled bool `toml:"enabled"`
BindAddress string `toml:"bind-address"`
AuthEnabled bool `toml:"auth-enabled"`
LogEnabled bool `toml:"log-enabled"`
SuppressWriteLog bool `toml:"suppress-write-log"`
WriteTracing bool `toml:"write-tracing"`
FluxEnabled bool `toml:"flux-enabled"`
FluxLogEnabled bool `toml:"flux-log-enabled"`
PprofEnabled bool `toml:"pprof-enabled"`
PprofAuthEnabled bool `toml:"pprof-auth-enabled"`
DebugPprofEnabled bool `toml:"debug-pprof-enabled"`
PingAuthEnabled bool `toml:"ping-auth-enabled"`
PromReadAuthEnabled bool `toml:"prom-read-auth-enabled"`
HTTPHeaders map[string]string `toml:"headers"`
HTTPSEnabled bool `toml:"https-enabled"`
HTTPSCertificate string `toml:"https-certificate"`
HTTPSPrivateKey string `toml:"https-private-key"`
MaxRowLimit int `toml:"max-row-limit"`
MaxConnectionLimit int `toml:"max-connection-limit"`
SharedSecret string `toml:"shared-secret"`
Realm string `toml:"realm"`
UnixSocketEnabled bool `toml:"unix-socket-enabled"`
UnixSocketGroup *toml.Group `toml:"unix-socket-group"`
UnixSocketPermissions toml.FileMode `toml:"unix-socket-permissions"`
BindSocket string `toml:"bind-socket"`
MaxBodySize int `toml:"max-body-size"`
AccessLogPath string `toml:"access-log-path"`
AccessLogStatusFilters []StatusFilter `toml:"access-log-status-filters"`
MaxConcurrentWriteLimit int `toml:"max-concurrent-write-limit"`
MaxEnqueuedWriteLimit int `toml:"max-enqueued-write-limit"`
EnqueuedWriteTimeout time.Duration `toml:"enqueued-write-timeout"`
TLS *tls.Config `toml:"-"`
}
// NewConfig returns a new Config with default settings.

View File

@ -414,6 +414,8 @@ func (h *Handler) AddRoutes(routes ...Route) {
if r.Gzipped {
handler = gzipFilter(handler)
}
handler = h.SetHeadersHandler(handler)
handler = cors(handler)
handler = requestID(handler)
if h.Config.LogEnabled && r.LoggingEnabled {
@ -1873,6 +1875,24 @@ func requestID(inner http.Handler) http.Handler {
})
}
func (h *Handler) SetHeadersHandler(handler http.Handler) http.Handler {
return http.HandlerFunc(h.SetHeadersWrapper(handler.ServeHTTP))
}
// wrapper that adds user supplied headers to the response.
func (h *Handler) SetHeadersWrapper(f func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) {
if len(h.Config.HTTPHeaders) == 0 {
return f
}
return func(w http.ResponseWriter, r *http.Request) {
for header, value := range h.Config.HTTPHeaders {
w.Header().Add(header, value)
}
f(w, r)
}
}
func (h *Handler) logging(inner http.Handler, name string) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()

View File

@ -2076,6 +2076,12 @@ func WithNoLog() configOption {
}
}
func WithHeaders(h map[string]string) configOption {
return func(c *httpd.Config) {
c.HTTPHeaders = h
}
}
// NewHandlerConfig returns a new instance of httpd.Config with
// authentication configured.
func NewHandlerConfig(opts ...configOption) httpd.Config {
@ -2209,3 +2215,53 @@ func MustJWTToken(username, secret string, expired bool) (*jwt.Token, string) {
}
return token, signed
}
// Ensure that user supplied headers are applied to responses.
func TestHandler_UserSuppliedHeaders(t *testing.T) {
endpoints := []struct {
method string
path string
}{
{method: "GET", path: "/ping"},
{method: "POST", path: "/api/v2/query"},
{method: "GET", path: "/query?db=foo&q=SELECT+*+FROM+bar"},
}
for _, endpoint := range endpoints {
t.Run(endpoint.method+endpoint.path, func(t *testing.T) {
headers := map[string]string{
"X-Best-Operating-System": "FreeBSD",
"X-Nana-Nana-Nana-Nana": "Batheader",
"X-Powered-By": "hamster in a wheel",
"X-Trek": "Live long and prosper",
}
// build a new handler with our headers as part of its configuration
h := NewHandlerWithConfig(NewHandlerConfig(WithHeaders(headers)))
w := httptest.NewRecorder()
// generate request request
req, err := http.NewRequest(endpoint.method, endpoint.path, nil)
if err != nil {
t.Fatal(err)
}
// serve the request
h.ServeHTTP(w, req)
response := w.Result()
// ensure we received the headers we supplied
for k, v := range headers {
val, found := response.Header[k]
if !found {
t.Fatalf("Could not find header field %q in response", k)
continue
}
if v != val[0] {
t.Fatalf("value for header %q in http response is %q; expected %q", k, val, v)
}
}
})
}
}