httpcache.go 2.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131
  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. "strconv"
  14. "strings"
  15. "sync"
  16. "time"
  17. )
  18. type SinglePathCache struct {
  19. next http.Handler
  20. keep time.Duration
  21. mut sync.RWMutex
  22. resp *recordedResponse
  23. }
  24. func SinglePath(next http.Handler, keep time.Duration) *SinglePathCache {
  25. return &SinglePathCache{
  26. next: next,
  27. keep: keep,
  28. }
  29. }
  30. type recordedResponse struct {
  31. status int
  32. header http.Header
  33. data []byte
  34. gzip []byte
  35. when time.Time
  36. keep time.Duration
  37. }
  38. func (resp *recordedResponse) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  39. for k, v := range resp.header {
  40. w.Header()[k] = v
  41. }
  42. w.Header().Set("Cache-Control", fmt.Sprintf("public, max-age=%d", int(resp.keep.Seconds())))
  43. if strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
  44. w.Header().Set("Content-Encoding", "gzip")
  45. w.Header().Set("Content-Length", strconv.Itoa(len(resp.gzip)))
  46. w.WriteHeader(resp.status)
  47. _, _ = w.Write(resp.gzip)
  48. return
  49. }
  50. w.Header().Set("Content-Length", strconv.Itoa(len(resp.data)))
  51. w.WriteHeader(resp.status)
  52. _, _ = w.Write(resp.data)
  53. }
  54. type responseRecorder struct {
  55. resp *recordedResponse
  56. }
  57. func (r *responseRecorder) WriteHeader(status int) {
  58. r.resp.status = status
  59. }
  60. func (r *responseRecorder) Write(data []byte) (int, error) {
  61. r.resp.data = append(r.resp.data, data...)
  62. return len(data), nil
  63. }
  64. func (r *responseRecorder) Header() http.Header {
  65. return r.resp.header
  66. }
  67. func (s *SinglePathCache) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  68. if r.Method != http.MethodGet && r.Method != http.MethodHead {
  69. s.next.ServeHTTP(w, r)
  70. return
  71. }
  72. w.Header().Set("X-Cache", "MISS")
  73. s.mut.RLock()
  74. ok := s.serveCached(w, r)
  75. s.mut.RUnlock()
  76. if ok {
  77. return
  78. }
  79. s.mut.Lock()
  80. defer s.mut.Unlock()
  81. if s.serveCached(w, r) {
  82. return
  83. }
  84. rec := &recordedResponse{status: http.StatusOK, header: make(http.Header), when: time.Now(), keep: s.keep}
  85. childRec := r.Clone(context.Background())
  86. childRec.Header.Del("Accept-Encoding") // don't let the client dictate the encoding
  87. s.next.ServeHTTP(&responseRecorder{resp: rec}, childRec)
  88. if rec.status == http.StatusOK {
  89. buf := new(bytes.Buffer)
  90. gw := gzip.NewWriter(buf)
  91. _, _ = gw.Write(rec.data)
  92. gw.Close()
  93. rec.gzip = buf.Bytes()
  94. s.resp = rec
  95. }
  96. rec.ServeHTTP(w, r)
  97. }
  98. func (s *SinglePathCache) serveCached(w http.ResponseWriter, r *http.Request) bool {
  99. if s.resp == nil || time.Since(s.resp.when) > s.keep {
  100. return false
  101. }
  102. w.Header().Set("X-Cache", "HIT")
  103. w.Header().Set("X-Cache-From", s.resp.when.Format(time.RFC3339))
  104. s.resp.ServeHTTP(w, r)
  105. return true
  106. }