From 27d429e3b11e27a379aece7a90f55344fe0c803e Mon Sep 17 00:00:00 2001 From: Kyle Brandt Date: Thu, 2 Feb 2023 15:34:55 -0500 Subject: [PATCH] API: Permit Cache-Control (browser caching) for datasource resources (#62033) * Start work on allowing certain resources to pass through Cache-Control headers. --------- Co-authored-by: Marcus Efraimsson --- pkg/middleware/middleware.go | 32 +++++++++++++++++++++++++++---- pkg/middleware/middleware_test.go | 16 ++++++++++++++++ pkg/middleware/testing.go | 1 + 3 files changed, 45 insertions(+), 4 deletions(-) diff --git a/pkg/middleware/middleware.go b/pkg/middleware/middleware.go index 2bc0893e5b7..24ab4279997 100644 --- a/pkg/middleware/middleware.go +++ b/pkg/middleware/middleware.go @@ -26,21 +26,24 @@ func HandleNoCacheHeader(ctx *contextmodel.ReqContext) { } func AddDefaultResponseHeaders(cfg *setting.Cfg) web.Handler { + t := web.NewTree() + t.Add("/api/datasources/uid/:uid/resources/*", nil) + t.Add("/api/datasources/:id/resources/*", nil) return func(c *web.Context) { - c.Resp.Before(func(w web.ResponseWriter) { - // if response has already been written, skip. + c.Resp.Before(func(w web.ResponseWriter) { // if response has already been written, skip. if w.Written() { return } - if !strings.HasPrefix(c.Req.URL.Path, "/api/datasources/proxy/") { + _, _, resourceURLMatch := t.Match(c.Req.URL.Path) + resourceCachable := resourceURLMatch && allowCacheControl(c.Resp) + if !strings.HasPrefix(c.Req.URL.Path, "/api/datasources/proxy/") && !resourceCachable { addNoCacheHeaders(c.Resp) } if !cfg.AllowEmbedding { addXFrameOptionsDenyHeader(w) } - addSecurityHeaders(w, cfg) }) } @@ -95,3 +98,24 @@ func AddCustomResponseHeaders(cfg *setting.Cfg) web.Handler { }) } } + +func allowCacheControl(rw web.ResponseWriter) bool { + ccHeaderValues := rw.Header().Values("Cache-Control") + + if len(ccHeaderValues) == 0 { + return false + } + + foundPrivate := false + foundPublic := false + for _, val := range ccHeaderValues { + if val == "private" { + foundPrivate = true + } + if val == "public" { + foundPublic = true + } + } + + return foundPrivate && !foundPublic && rw.Header().Get("X-Grafana-Cache") != "" +} diff --git a/pkg/middleware/middleware_test.go b/pkg/middleware/middleware_test.go index bb606ebbf76..ce8fdddc4eb 100644 --- a/pkg/middleware/middleware_test.go +++ b/pkg/middleware/middleware_test.go @@ -154,6 +154,22 @@ func TestMiddlewareContext(t *testing.T) { assert.Empty(t, sc.resp.Header().Get("Expires")) }) + middlewareScenario(t, "middleware should pass cache-control on resources with private cache control", func(t *testing.T, sc *scenarioContext) { + sc = sc.fakeReq("GET", "/api/datasources/1/resources/foo") + sc.resp.Header().Add("Cache-Control", "private") + sc.resp.Header().Add("X-Grafana-Cache", "true") + sc.exec() + assert.Equal(t, "private", sc.resp.Header().Get("Cache-Control")) + }) + + middlewareScenario(t, "middleware should not pass cache-control on resources with public cache control", func(t *testing.T, sc *scenarioContext) { + sc = sc.fakeReq("GET", "/api/datasources/1/resources/foo") + sc.resp.Header().Add("Cache-Control", "public") + sc.resp.Header().Add("X-Grafana-Cache", "true") + sc.exec() + assert.Equal(t, noStore, sc.resp.Header().Get("Cache-Control")) + }) + middlewareScenario(t, "middleware should not add Cache-Control header for requests to datasource proxy API", func( t *testing.T, sc *scenarioContext) { sc.fakeReq("GET", "/api/datasources/proxy/1/test").exec() diff --git a/pkg/middleware/testing.go b/pkg/middleware/testing.go index 6caa397c4e8..555d4d4f727 100644 --- a/pkg/middleware/testing.go +++ b/pkg/middleware/testing.go @@ -135,6 +135,7 @@ func (sc *scenarioContext) exec() { Value: sc.tokenSessionCookie, }) } + sc.m.ServeHTTP(sc.resp, sc.req) if sc.resp.Header().Get("Content-Type") == "application/json; charset=UTF-8" {