github.com/gitbundle/modules@v0.0.0-20231025071548-85b91c5c3b01/httpcache/httpcache.go (about) 1 // Copyright 2023 The GitBundle Inc. All rights reserved. 2 // Copyright 2017 The Gitea Authors. All rights reserved. 3 // Use of this source code is governed by a MIT-style 4 // license that can be found in the LICENSE file. 5 6 package httpcache 7 8 import ( 9 "encoding/base64" 10 "fmt" 11 "net/http" 12 "os" 13 "strconv" 14 "strings" 15 "time" 16 17 "github.com/gitbundle/modules/setting" 18 ) 19 20 // AddCacheControlToHeader adds suitable cache-control headers to response 21 func AddCacheControlToHeader(h http.Header, maxAge time.Duration, additionalDirectives ...string) { 22 directives := make([]string, 0, 2+len(additionalDirectives)) 23 24 if setting.IsProd || setting.IsBeta { 25 if maxAge == 0 { 26 directives = append(directives, "no-store") 27 } else { 28 directives = append(directives, "private", "max-age="+strconv.Itoa(int(maxAge.Seconds()))) 29 } 30 } else { 31 directives = append(directives, "no-store") 32 33 // to remind users they are using non-prod setting. 34 // h.Add("X-Gitea-Debug", "RUN_MODE="+setting.RunMode) 35 } 36 37 h.Set("Cache-Control", strings.Join(append(directives, additionalDirectives...), ", ")) 38 } 39 40 // generateETag generates an ETag based on size, filename and file modification time 41 func generateETag(fi os.FileInfo) string { 42 etag := fmt.Sprint(fi.Size()) + fi.Name() + fi.ModTime().UTC().Format(http.TimeFormat) 43 return `"` + base64.StdEncoding.EncodeToString([]byte(etag)) + `"` 44 } 45 46 // HandleTimeCache handles time-based caching for a HTTP request 47 func HandleTimeCache(req *http.Request, w http.ResponseWriter, fi os.FileInfo) (handled bool) { 48 return HandleGenericTimeCache(req, w, fi.ModTime()) 49 } 50 51 // HandleGenericTimeCache handles time-based caching for a HTTP request 52 func HandleGenericTimeCache(req *http.Request, w http.ResponseWriter, lastModified time.Time) (handled bool) { 53 AddCacheControlToHeader(w.Header(), setting.StaticCacheTime) 54 55 ifModifiedSince := req.Header.Get("If-Modified-Since") 56 if ifModifiedSince != "" { 57 t, err := time.Parse(http.TimeFormat, ifModifiedSince) 58 if err == nil && lastModified.Unix() <= t.Unix() { 59 w.WriteHeader(http.StatusNotModified) 60 return true 61 } 62 } 63 64 w.Header().Set("Last-Modified", lastModified.Format(http.TimeFormat)) 65 return false 66 } 67 68 // HandleFileETagCache handles ETag-based caching for a HTTP request 69 func HandleFileETagCache(req *http.Request, w http.ResponseWriter, fi os.FileInfo) (handled bool) { 70 etag := generateETag(fi) 71 return HandleGenericETagCache(req, w, etag) 72 } 73 74 // HandleGenericETagCache handles ETag-based caching for a HTTP request. 75 // It returns true if the request was handled. 76 func HandleGenericETagCache(req *http.Request, w http.ResponseWriter, etag string) (handled bool) { 77 if len(etag) > 0 { 78 w.Header().Set("Etag", etag) 79 if checkIfNoneMatchIsValid(req, etag) { 80 w.WriteHeader(http.StatusNotModified) 81 return true 82 } 83 } 84 AddCacheControlToHeader(w.Header(), setting.StaticCacheTime) 85 return false 86 } 87 88 // checkIfNoneMatchIsValid tests if the header If-None-Match matches the ETag 89 func checkIfNoneMatchIsValid(req *http.Request, etag string) bool { 90 ifNoneMatch := req.Header.Get("If-None-Match") 91 if len(ifNoneMatch) > 0 { 92 for _, item := range strings.Split(ifNoneMatch, ",") { 93 item = strings.TrimSpace(item) 94 if item == etag { 95 return true 96 } 97 } 98 } 99 return false 100 } 101 102 // HandleGenericETagTimeCache handles ETag-based caching with Last-Modified caching for a HTTP request. 103 // It returns true if the request was handled. 104 func HandleGenericETagTimeCache(req *http.Request, w http.ResponseWriter, etag string, lastModified time.Time) (handled bool) { 105 if len(etag) > 0 { 106 w.Header().Set("Etag", etag) 107 } 108 if !lastModified.IsZero() { 109 w.Header().Set("Last-Modified", lastModified.Format(http.TimeFormat)) 110 } 111 112 if len(etag) > 0 { 113 if checkIfNoneMatchIsValid(req, etag) { 114 w.WriteHeader(http.StatusNotModified) 115 return true 116 } 117 } 118 if !lastModified.IsZero() { 119 ifModifiedSince := req.Header.Get("If-Modified-Since") 120 if ifModifiedSince != "" { 121 t, err := time.Parse(http.TimeFormat, ifModifiedSince) 122 if err == nil && lastModified.Unix() <= t.Unix() { 123 w.WriteHeader(http.StatusNotModified) 124 return true 125 } 126 } 127 } 128 AddCacheControlToHeader(w.Header(), setting.StaticCacheTime) 129 return false 130 }