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 filepull/18667/head
parent
1e7a2e234a
commit
dde8231d5c
|
@ -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]
|
||||
###
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue