From bba42b113c4b99590b01085ee611b409eb8f6caf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Calisto?= Date: Wed, 30 Nov 2022 17:12:34 +0000 Subject: [PATCH] Middleware: Add Custom Headers to HTTP responses (#59018) * Middleware: Add Custom Headers to HTTP responses * Update docs/sources/setup-grafana/configure-grafana/_index.md Co-authored-by: Christopher Moyer <35463610+chri2547@users.noreply.github.com> * Update conf/defaults.ini Co-authored-by: Dave Henderson * Update conf/sample.ini Co-authored-by: Dave Henderson * Update _index.md Co-authored-by: Christopher Moyer <35463610+chri2547@users.noreply.github.com> Co-authored-by: Dave Henderson --- conf/defaults.ini | 5 +++++ conf/sample.ini | 5 +++++ .../setup-grafana/configure-grafana/_index.md | 11 +++++++++++ pkg/api/http_server.go | 4 ++++ pkg/middleware/middleware.go | 18 ++++++++++++++++++ pkg/middleware/middleware_test.go | 12 ++++++++++++ pkg/setting/setting.go | 10 ++++++++++ 7 files changed, 65 insertions(+) diff --git a/conf/defaults.ini b/conf/defaults.ini index 115cbc20a73..1892554a067 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -85,6 +85,11 @@ cdn_url = # `0` means there is no timeout for reading the request. read_timeout = 0 +# This setting enables you to specify additional headers that the server adds to HTTP(S) responses. +[server.custom_response_headers] +#exampleHeader1 = exampleValue1 +#exampleHeader2 = exampleValue2 + #################################### Database ############################ [database] # You can configure the database connection by specifying type, host, name, user and password diff --git a/conf/sample.ini b/conf/sample.ini index d421d04c10a..8f880bd1ebc 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -86,6 +86,11 @@ # `0` means there is no timeout for reading the request. ;read_timeout = 0 +# This setting enables you to specify additional headers that the server adds to HTTP(S) responses. +[server.custom_response_headers] +#exampleHeader1 = exampleValue1 +#exampleHeader2 = exampleValue2 + #################################### Database #################################### [database] # You can configure the database connection by specifying type, host, name, user and password diff --git a/docs/sources/setup-grafana/configure-grafana/_index.md b/docs/sources/setup-grafana/configure-grafana/_index.md index 542f006729f..a258f2ab5b7 100644 --- a/docs/sources/setup-grafana/configure-grafana/_index.md +++ b/docs/sources/setup-grafana/configure-grafana/_index.md @@ -292,6 +292,17 @@ Sets the maximum time using a duration format (5s/5m/5ms) before timing out read
+## [server.custom_response_headers] + +This setting enables you to specify additional headers that the server adds to HTTP(S) responses. + +``` +exampleHeader1 = exampleValue1 +exampleHeader2 = exampleValue2 +``` + +
+ ## [database] Grafana needs a database to store users and dashboards (and other diff --git a/pkg/api/http_server.go b/pkg/api/http_server.go index ed99aa51c94..ae383a02732 100644 --- a/pkg/api/http_server.go +++ b/pkg/api/http_server.go @@ -598,6 +598,10 @@ func (hs *HTTPServer) addMiddlewaresAndStaticRoutes() { hs.mapStatic(m, hs.Cfg.ImagesDir, "", "/public/img/attachments") } + if len(hs.Cfg.CustomResponseHeaders) > 0 { + m.Use(middleware.AddCustomResponseHeaders(hs.Cfg)) + } + m.Use(middleware.AddDefaultResponseHeaders(hs.Cfg)) if hs.Cfg.ServeFromSubPath && hs.Cfg.AppSubURL != "" { diff --git a/pkg/middleware/middleware.go b/pkg/middleware/middleware.go index 22225cecd75..940fd5e36c1 100644 --- a/pkg/middleware/middleware.go +++ b/pkg/middleware/middleware.go @@ -77,3 +77,21 @@ func addNoCacheHeaders(w web.ResponseWriter) { func addXFrameOptionsDenyHeader(w web.ResponseWriter) { w.Header().Set("X-Frame-Options", "deny") } + +func AddCustomResponseHeaders(cfg *setting.Cfg) web.Handler { + return func(c *web.Context) { + c.Resp.Before(func(w web.ResponseWriter) { + if w.Written() { + return + } + + for header, value := range cfg.CustomResponseHeaders { + // do not override existing headers + if w.Header().Get(header) != "" { + continue + } + w.Header().Set(header, value) + } + }) + } +} diff --git a/pkg/middleware/middleware_test.go b/pkg/middleware/middleware_test.go index 8c029c2f597..1c45065c374 100644 --- a/pkg/middleware/middleware_test.go +++ b/pkg/middleware/middleware_test.go @@ -514,6 +514,17 @@ func TestMiddlewareContext(t *testing.T) { cfg.AnonymousOrgRole = string(org.RoleEditor) }) + middlewareScenario(t, "middleware should add custom response headers", func(t *testing.T, sc *scenarioContext) { + sc.fakeReq("GET", "/api/").exec() + assert.Regexp(t, "test", sc.resp.Header().Get("X-Custom-Header")) + assert.Regexp(t, "other-test", sc.resp.Header().Get("X-Other-Header")) + }, func(cfg *setting.Cfg) { + cfg.CustomResponseHeaders = map[string]string{ + "X-Custom-Header": "test", + "X-Other-Header": "other-test", + } + }) + t.Run("auth_proxy", func(t *testing.T) { const userID int64 = 33 const orgID int64 = 4 @@ -811,6 +822,7 @@ func middlewareScenario(t *testing.T, desc string, fn scenarioFunc, cbs ...func( require.Truef(t, exists, "Views directory should exist at %q", viewsPath) sc.m = web.New() + sc.m.Use(AddCustomResponseHeaders(cfg)) sc.m.Use(AddDefaultResponseHeaders(cfg)) sc.m.UseMiddleware(ContentSecurityPolicy(cfg, logger)) sc.m.UseMiddleware(web.Renderer(viewsPath, "[[", "]]")) diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index 70372fcd9f3..bc993a3e07d 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -486,6 +486,8 @@ type Cfg struct { GRPCServerNetwork string GRPCServerAddress string GRPCServerTLSConfig *tls.Config + + CustomResponseHeaders map[string]string } type CommandLineArgs struct { @@ -1692,6 +1694,14 @@ func (cfg *Cfg) readServerSettings(iniFile *ini.File) error { cfg.ReadTimeout = server.Key("read_timeout").MustDuration(0) + headersSection := cfg.Raw.Section("server.custom_response_headers") + keys := headersSection.Keys() + cfg.CustomResponseHeaders = make(map[string]string, len(keys)) + + for _, key := range keys { + cfg.CustomResponseHeaders[key.Name()] = key.Value() + } + return nil }