Browse Source

httpd: allow to configure cache control header

Signed-off-by: Nicola Murino <[email protected]>
Nicola Murino 1 năm trước cách đây
mục cha
commit
6f8bc59756

+ 7 - 0
internal/config/config.go

@@ -144,6 +144,7 @@ var (
 			ContentSecurityPolicy:   "",
 			PermissionsPolicy:       "",
 			CrossOriginOpenerPolicy: "",
+			CacheControl:            "",
 		},
 		Branding: httpd.Branding{},
 	}
@@ -1547,6 +1548,12 @@ func getHTTPDSecurityConfFromEnv(idx int) (httpd.SecurityConf, bool) { //nolint:
 		isSet = true
 	}
 
+	cacheControl, ok := os.LookupEnv(fmt.Sprintf("SFTPGO_HTTPD__BINDINGS__%v__SECURITY__CACHE_CONTROL", idx))
+	if ok {
+		result.CacheControl = cacheControl
+		isSet = true
+	}
+
 	return result, isSet
 }
 

+ 3 - 0
internal/config/config_test.go

@@ -1203,6 +1203,7 @@ func TestHTTPDBindingsFromEnv(t *testing.T) {
 	os.Setenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__CONTENT_SECURITY_POLICY", "script-src $NONCE")
 	os.Setenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__PERMISSIONS_POLICY", "fullscreen=(), geolocation=()")
 	os.Setenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__CROSS_ORIGIN_OPENER_POLICY", "same-origin")
+	os.Setenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__CACHE_CONTROL", "private")
 	os.Setenv("SFTPGO_HTTPD__BINDINGS__2__EXTRA_CSS__0__PATH", "path1")
 	os.Setenv("SFTPGO_HTTPD__BINDINGS__2__EXTRA_CSS__1__PATH", "path2")
 	os.Setenv("SFTPGO_HTTPD__BINDINGS__2__BRANDING__WEB_ADMIN__FAVICON_PATH", "favicon.ico")
@@ -1267,6 +1268,7 @@ func TestHTTPDBindingsFromEnv(t *testing.T) {
 		os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__CONTENT_SECURITY_POLICY")
 		os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__PERMISSIONS_POLICY")
 		os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__CROSS_ORIGIN_OPENER_POLICY")
+		os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__CACHE_CONTROL")
 		os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__EXTRA_CSS__0__PATH")
 		os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__EXTRA_CSS__1__PATH")
 		os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__BRANDING__WEB_ADMIN__FAVICON_PATH")
@@ -1376,6 +1378,7 @@ func TestHTTPDBindingsFromEnv(t *testing.T) {
 	require.Equal(t, "script-src $NONCE", bindings[2].Security.ContentSecurityPolicy)
 	require.Equal(t, "fullscreen=(), geolocation=()", bindings[2].Security.PermissionsPolicy)
 	require.Equal(t, "same-origin", bindings[2].Security.CrossOriginOpenerPolicy)
+	require.Equal(t, "private", bindings[2].Security.CacheControl)
 	require.Equal(t, "favicon.ico", bindings[2].Branding.WebAdmin.FaviconPath)
 	require.Equal(t, "logo.png", bindings[2].Branding.WebClient.LogoPath)
 	require.Equal(t, "disclaimer", bindings[2].Branding.WebClient.DisclaimerName)

+ 3 - 1
internal/httpd/httpd.go

@@ -342,7 +342,9 @@ type SecurityConf struct {
 	PermissionsPolicy string `json:"permissions_policy" mapstructure:"permissions_policy"`
 	// CrossOriginOpenerPolicy allows to set the `Cross-Origin-Opener-Policy` header value. Default is "".
 	CrossOriginOpenerPolicy string `json:"cross_origin_opener_policy" mapstructure:"cross_origin_opener_policy"`
-	proxyHeaders            []string
+	// CacheControl allow to set the Cache-Control header value.
+	CacheControl string `json:"cache_control" mapstructure:"cache_control"`
+	proxyHeaders []string
 }
 
 func (s *SecurityConf) updateProxyHeaders() {

+ 3 - 0
internal/httpd/httpd_test.go

@@ -414,6 +414,7 @@ func TestMain(m *testing.M) {
 				Value: "https",
 			},
 		},
+		CacheControl: "private",
 	}
 	httpdtest.SetBaseURL(httpBaseURL)
 	// required to test sftpfs
@@ -13025,12 +13026,14 @@ func TestDefender(t *testing.T) {
 	req.RemoteAddr = remoteAddr
 	rr = executeRequest(req)
 	checkResponseCode(t, http.StatusOK, rr)
+	assert.Empty(t, rr.Header().Get("Cache-Control"))
 
 	req, err = http.NewRequest(http.MethodGet, "/.well-known/acme-challenge/foo", nil)
 	assert.NoError(t, err)
 	req.RemoteAddr = remoteAddr
 	rr = executeRequest(req)
 	checkResponseCode(t, http.StatusNotFound, rr)
+	assert.Equal(t, "no-cache, no-store, max-age=0, must-revalidate, private", rr.Header().Get("Cache-Control"))
 
 	_, err = httpdtest.RemoveUser(user, http.StatusOK)
 	assert.NoError(t, err)

+ 2 - 0
internal/httpd/internal_test.go

@@ -2942,6 +2942,7 @@ func TestSecureMiddlewareIntegration(t *testing.T) {
 				STSIncludeSubdomains: true,
 				STSPreload:           true,
 				ContentTypeNosniff:   true,
+				CacheControl:         "private",
 			},
 		},
 		enableWebAdmin:  true,
@@ -2961,6 +2962,7 @@ func TestSecureMiddlewareIntegration(t *testing.T) {
 	r.Host = "127.0.0.1"
 	server.router.ServeHTTP(rr, r)
 	assert.Equal(t, http.StatusForbidden, rr.Code)
+	assert.Equal(t, "no-cache, no-store, max-age=0, must-revalidate, private", rr.Header().Get("Cache-Control"))
 
 	rr = httptest.NewRecorder()
 	r.Header.Set(forwardedHostHeader, "www.sftpgo.com")

+ 14 - 0
internal/httpd/middleware.go

@@ -586,3 +586,17 @@ func checkPartialAuth(w http.ResponseWriter, r *http.Request, audience string, t
 	}
 	return nil
 }
+
+func cacheControlMiddleware(next http.Handler) http.Handler {
+	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		w.Header().Set("Cache-Control", "no-cache, no-store, max-age=0, must-revalidate, private")
+		next.ServeHTTP(w, r)
+	})
+}
+
+func cleanCacheControlMiddleware(next http.Handler) http.Handler {
+	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		w.Header().Del("Cache-Control")
+		next.ServeHTTP(w, r)
+	})
+}

+ 6 - 1
internal/httpd/server.go

@@ -1242,7 +1242,6 @@ func (s *httpdServer) initializeRouter() {
 	s.router.Use(s.parseHeaders)
 	s.router.Use(logger.NewStructuredLogger(logger.GetLogger()))
 	s.router.Use(middleware.Recoverer)
-	s.router.Use(middleware.Maybe(s.checkConnection, s.mustCheckPath))
 	if s.binding.Security.Enabled {
 		secureMiddleware := secure.New(secure.Options{
 			AllowedHosts:            s.binding.Security.AllowedHosts,
@@ -1258,6 +1257,9 @@ func (s *httpdServer) initializeRouter() {
 			CrossOriginOpenerPolicy: s.binding.Security.CrossOriginOpenerPolicy,
 		})
 		secureMiddleware.SetBadHostHandler(http.HandlerFunc(s.badHostHandler))
+		if s.binding.Security.CacheControl == "private" {
+			s.router.Use(cacheControlMiddleware)
+		}
 		s.router.Use(secureMiddleware.Handler)
 		if s.binding.Security.HTTPSRedirect {
 			s.router.Use(s.binding.Security.redirectHandler)
@@ -1278,6 +1280,7 @@ func (s *httpdServer) initializeRouter() {
 		})
 		s.router.Use(c.Handler)
 	}
+	s.router.Use(middleware.Maybe(s.checkConnection, s.mustCheckPath))
 	s.router.Use(middleware.GetHead)
 	s.router.Use(middleware.Maybe(middleware.StripSlashes, s.mustStripSlash))
 
@@ -1487,6 +1490,7 @@ func (s *httpdServer) initializeRouter() {
 
 		if s.renderOpenAPI {
 			s.router.Group(func(router chi.Router) {
+				router.Use(cleanCacheControlMiddleware)
 				router.Use(compressor.Handler)
 				serveStaticDir(router, webOpenAPIPath, s.openAPIPath, false)
 			})
@@ -1495,6 +1499,7 @@ func (s *httpdServer) initializeRouter() {
 
 	if s.enableWebAdmin || s.enableWebClient {
 		s.router.Group(func(router chi.Router) {
+			router.Use(cleanCacheControlMiddleware)
 			router.Use(compressor.Handler)
 			serveStaticDir(router, webStaticFilesPath, s.staticFilesPath, true)
 		})

+ 2 - 1
sftpgo.json

@@ -311,7 +311,8 @@
           "content_type_nosniff": false,
           "content_security_policy": "",
           "permissions_policy": "",
-          "cross_origin_opener_policy": ""
+          "cross_origin_opener_policy": "",
+          "cache_control": ""
         },
         "branding": {
           "web_admin": {