httpcache.go 2.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130
  1. // Copyright (C) 2023 The Syncthing Authors.
  2. //
  3. // This Source Code Form is subject to the terms of the Mozilla Public
  4. // License, v. 2.0. If a copy of the MPL was not distributed with this file,
  5. // You can obtain one at https://mozilla.org/MPL/2.0/.
  6. package httpcache
  7. import (
  8. "bytes"
  9. "compress/gzip"
  10. "context"
  11. "fmt"
  12. "net/http"
  13. "strings"
  14. "sync"
  15. "time"
  16. )
  17. type SinglePathCache struct {
  18. next http.Handler
  19. keep time.Duration
  20. mut sync.RWMutex
  21. resp *recordedResponse
  22. }
  23. func SinglePath(next http.Handler, keep time.Duration) *SinglePathCache {
  24. return &SinglePathCache{
  25. next: next,
  26. keep: keep,
  27. }
  28. }
  29. type recordedResponse struct {
  30. status int
  31. header http.Header
  32. data []byte
  33. gzip []byte
  34. when time.Time
  35. keep time.Duration
  36. }
  37. func (resp *recordedResponse) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  38. for k, v := range resp.header {
  39. w.Header()[k] = v
  40. }
  41. w.Header().Set("Cache-Control", fmt.Sprintf("public, max-age=%d", int(resp.keep.Seconds())))
  42. if strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
  43. w.Header().Set("Content-Encoding", "gzip")
  44. w.Header().Set("Content-Length", fmt.Sprint(len(resp.gzip)))
  45. w.WriteHeader(resp.status)
  46. _, _ = w.Write(resp.gzip)
  47. return
  48. }
  49. w.Header().Set("Content-Length", fmt.Sprint(len(resp.data)))
  50. w.WriteHeader(resp.status)
  51. _, _ = w.Write(resp.data)
  52. }
  53. type responseRecorder struct {
  54. resp *recordedResponse
  55. }
  56. func (r *responseRecorder) WriteHeader(status int) {
  57. r.resp.status = status
  58. }
  59. func (r *responseRecorder) Write(data []byte) (int, error) {
  60. r.resp.data = append(r.resp.data, data...)
  61. return len(data), nil
  62. }
  63. func (r *responseRecorder) Header() http.Header {
  64. return r.resp.header
  65. }
  66. func (s *SinglePathCache) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  67. if r.Method != http.MethodGet && r.Method != http.MethodHead {
  68. s.next.ServeHTTP(w, r)
  69. return
  70. }
  71. w.Header().Set("X-Cache", "MISS")
  72. s.mut.RLock()
  73. ok := s.serveCached(w, r)
  74. s.mut.RUnlock()
  75. if ok {
  76. return
  77. }
  78. s.mut.Lock()
  79. defer s.mut.Unlock()
  80. if s.serveCached(w, r) {
  81. return
  82. }
  83. rec := &recordedResponse{status: http.StatusOK, header: make(http.Header), when: time.Now(), keep: s.keep}
  84. childRec := r.Clone(context.Background())
  85. childRec.Header.Del("Accept-Encoding") // don't let the client dictate the encoding
  86. s.next.ServeHTTP(&responseRecorder{resp: rec}, childRec)
  87. if rec.status == http.StatusOK {
  88. buf := new(bytes.Buffer)
  89. gw := gzip.NewWriter(buf)
  90. _, _ = gw.Write(rec.data)
  91. gw.Close()
  92. rec.gzip = buf.Bytes()
  93. s.resp = rec
  94. }
  95. rec.ServeHTTP(w, r)
  96. }
  97. func (s *SinglePathCache) serveCached(w http.ResponseWriter, r *http.Request) bool {
  98. if s.resp == nil || time.Since(s.resp.when) > s.keep {
  99. return false
  100. }
  101. w.Header().Set("X-Cache", "HIT")
  102. w.Header().Set("X-Cache-From", s.resp.when.Format(time.RFC3339))
  103. s.resp.ServeHTTP(w, r)
  104. return true
  105. }