code.gitea.io/gitea@v1.22.3/modules/httpcache/httpcache.go (about) 1 // Copyright 2020 The Gitea Authors. All rights reserved. 2 // SPDX-License-Identifier: MIT 3 4 package httpcache 5 6 import ( 7 "io" 8 "net/http" 9 "strconv" 10 "strings" 11 "time" 12 13 "code.gitea.io/gitea/modules/setting" 14 ) 15 16 // SetCacheControlInHeader sets suitable cache-control headers in the response 17 func SetCacheControlInHeader(h http.Header, maxAge time.Duration, additionalDirectives ...string) { 18 directives := make([]string, 0, 2+len(additionalDirectives)) 19 20 // "max-age=0 + must-revalidate" (aka "no-cache") is preferred instead of "no-store" 21 // because browsers may restore some input fields after navigate-back / reload a page. 22 if setting.IsProd { 23 if maxAge == 0 { 24 directives = append(directives, "max-age=0", "private", "must-revalidate") 25 } else { 26 directives = append(directives, "private", "max-age="+strconv.Itoa(int(maxAge.Seconds()))) 27 } 28 } else { 29 directives = append(directives, "max-age=0", "private", "must-revalidate") 30 31 // to remind users they are using non-prod setting. 32 h.Set("X-Gitea-Debug", "RUN_MODE="+setting.RunMode) 33 } 34 35 h.Set("Cache-Control", strings.Join(append(directives, additionalDirectives...), ", ")) 36 } 37 38 func ServeContentWithCacheControl(w http.ResponseWriter, req *http.Request, name string, modTime time.Time, content io.ReadSeeker) { 39 SetCacheControlInHeader(w.Header(), setting.StaticCacheTime) 40 http.ServeContent(w, req, name, modTime, content) 41 } 42 43 // HandleGenericETagCache handles ETag-based caching for a HTTP request. 44 // It returns true if the request was handled. 45 func HandleGenericETagCache(req *http.Request, w http.ResponseWriter, etag string) (handled bool) { 46 if len(etag) > 0 { 47 w.Header().Set("Etag", etag) 48 if checkIfNoneMatchIsValid(req, etag) { 49 w.WriteHeader(http.StatusNotModified) 50 return true 51 } 52 } 53 SetCacheControlInHeader(w.Header(), setting.StaticCacheTime) 54 return false 55 } 56 57 // checkIfNoneMatchIsValid tests if the header If-None-Match matches the ETag 58 func checkIfNoneMatchIsValid(req *http.Request, etag string) bool { 59 ifNoneMatch := req.Header.Get("If-None-Match") 60 if len(ifNoneMatch) > 0 { 61 for _, item := range strings.Split(ifNoneMatch, ",") { 62 item = strings.TrimPrefix(strings.TrimSpace(item), "W/") // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag#directives 63 if item == etag { 64 return true 65 } 66 } 67 } 68 return false 69 } 70 71 // HandleGenericETagTimeCache handles ETag-based caching with Last-Modified caching for a HTTP request. 72 // It returns true if the request was handled. 73 func HandleGenericETagTimeCache(req *http.Request, w http.ResponseWriter, etag string, lastModified *time.Time) (handled bool) { 74 if len(etag) > 0 { 75 w.Header().Set("Etag", etag) 76 } 77 if lastModified != nil && !lastModified.IsZero() { 78 // http.TimeFormat required a UTC time, refer to https://pkg.go.dev/net/http#TimeFormat 79 w.Header().Set("Last-Modified", lastModified.UTC().Format(http.TimeFormat)) 80 } 81 82 if len(etag) > 0 { 83 if checkIfNoneMatchIsValid(req, etag) { 84 w.WriteHeader(http.StatusNotModified) 85 return true 86 } 87 } 88 if lastModified != nil && !lastModified.IsZero() { 89 ifModifiedSince := req.Header.Get("If-Modified-Since") 90 if ifModifiedSince != "" { 91 t, err := time.Parse(http.TimeFormat, ifModifiedSince) 92 if err == nil && lastModified.Unix() <= t.Unix() { 93 w.WriteHeader(http.StatusNotModified) 94 return true 95 } 96 } 97 } 98 SetCacheControlInHeader(w.Header(), setting.StaticCacheTime) 99 return false 100 }