gitea源码

httpcache.go 3.9KB

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