2020-07-29 22:45:26 +02:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
2021-07-25 10:53:12 +02:00
|
|
|
"context"
|
2022-02-23 12:24:11 +01:00
|
|
|
"log"
|
2020-07-29 22:45:26 +02:00
|
|
|
"net/http"
|
2022-02-21 18:47:41 +01:00
|
|
|
"net/url"
|
|
|
|
"sort"
|
2020-07-29 22:45:26 +02:00
|
|
|
"time"
|
2020-10-19 23:02:57 +02:00
|
|
|
|
2021-03-11 19:00:09 +01:00
|
|
|
"github.com/dgraph-io/ristretto"
|
2022-02-21 18:47:41 +01:00
|
|
|
"go.goblog.app/app/pkgs/bufferpool"
|
2020-10-19 23:02:57 +02:00
|
|
|
"golang.org/x/sync/singleflight"
|
2020-07-29 22:45:26 +02:00
|
|
|
)
|
|
|
|
|
2021-07-25 10:53:12 +02:00
|
|
|
const (
|
|
|
|
cacheLoggedInKey contextKey = "cacheLoggedIn"
|
|
|
|
cacheExpirationKey contextKey = "cacheExpiration"
|
2022-02-01 15:58:12 +01:00
|
|
|
|
|
|
|
cacheControl = "Cache-Control"
|
2021-07-25 10:53:12 +02:00
|
|
|
)
|
2020-11-20 15:33:20 +01:00
|
|
|
|
2021-06-06 14:39:42 +02:00
|
|
|
type cache struct {
|
2022-01-26 12:02:12 +01:00
|
|
|
g singleflight.Group
|
|
|
|
c *ristretto.Cache
|
2021-06-06 14:39:42 +02:00
|
|
|
}
|
2020-09-22 16:42:36 +02:00
|
|
|
|
2021-06-06 14:39:42 +02:00
|
|
|
func (a *goBlog) initCache() (err error) {
|
2022-01-26 12:02:12 +01:00
|
|
|
a.cache = &cache{}
|
|
|
|
if a.cfg.Cache != nil && !a.cfg.Cache.Enable {
|
|
|
|
// Cache disabled
|
2021-06-06 14:39:42 +02:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
a.cache.c, err = ristretto.NewCache(&ristretto.Config{
|
2021-07-23 17:26:14 +02:00
|
|
|
NumCounters: 40 * 1000, // 4000 items when full with 5 KB items -> x10 = 40.000
|
|
|
|
MaxCost: 20 * 1000 * 1000, // 20 MB
|
|
|
|
BufferItems: 64, // recommended
|
2022-02-23 12:24:11 +01:00
|
|
|
Metrics: true,
|
2021-03-11 19:00:09 +01:00
|
|
|
})
|
2022-02-23 12:24:11 +01:00
|
|
|
go func() {
|
|
|
|
ticker := time.NewTicker(15 * time.Minute)
|
|
|
|
for range ticker.C {
|
|
|
|
met := a.cache.c.Metrics
|
|
|
|
log.Println("\nCache:", met.String())
|
|
|
|
}
|
|
|
|
}()
|
2020-11-20 15:33:20 +01:00
|
|
|
return
|
2020-10-19 23:33:08 +02:00
|
|
|
}
|
2020-09-22 16:42:36 +02:00
|
|
|
|
2021-07-25 10:53:12 +02:00
|
|
|
func cacheLoggedIn(next http.Handler) http.Handler {
|
|
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), cacheLoggedInKey, true)))
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2021-07-24 13:35:26 +02:00
|
|
|
func (a *goBlog) cacheMiddleware(next http.Handler) http.Handler {
|
2020-07-29 22:45:26 +02:00
|
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
2021-06-06 14:39:42 +02:00
|
|
|
// Do checks
|
2022-01-26 12:02:12 +01:00
|
|
|
if a.cache.c == nil || !cacheable(r) {
|
2021-02-20 23:35:16 +01:00
|
|
|
next.ServeHTTP(w, r)
|
|
|
|
return
|
|
|
|
}
|
2021-07-25 10:53:12 +02:00
|
|
|
// Check login
|
|
|
|
if cli, ok := r.Context().Value(cacheLoggedInKey).(bool); ok && cli {
|
|
|
|
// Continue caching, but remove login
|
|
|
|
setLoggedIn(r, false)
|
2022-02-26 20:38:52 +01:00
|
|
|
} else if a.isLoggedIn(r) {
|
|
|
|
// Don't cache logged in requests
|
|
|
|
next.ServeHTTP(w, r)
|
|
|
|
return
|
2021-02-20 23:35:16 +01:00
|
|
|
}
|
|
|
|
// Search and serve cache
|
|
|
|
key := cacheKey(r)
|
|
|
|
// Get cache or render it
|
2022-03-16 08:28:03 +01:00
|
|
|
cacheInterface, _, _ := a.cache.g.Do(key, func() (any, error) {
|
2021-07-25 10:53:12 +02:00
|
|
|
return a.cache.getCache(key, next, r), nil
|
2021-02-20 23:35:16 +01:00
|
|
|
})
|
2021-06-06 14:39:42 +02:00
|
|
|
ci := cacheInterface.(*cacheItem)
|
2021-12-30 12:40:21 +01:00
|
|
|
// copy and set headers
|
2022-01-26 12:02:12 +01:00
|
|
|
a.setCacheHeaders(w, ci)
|
2021-02-20 23:35:16 +01:00
|
|
|
// check conditional request
|
2021-06-06 14:39:42 +02:00
|
|
|
if ifNoneMatchHeader := r.Header.Get("If-None-Match"); ifNoneMatchHeader != "" && ifNoneMatchHeader == ci.eTag {
|
2021-02-20 23:35:16 +01:00
|
|
|
// send 304
|
|
|
|
w.WriteHeader(http.StatusNotModified)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
// set status code
|
2021-06-06 14:39:42 +02:00
|
|
|
w.WriteHeader(ci.code)
|
2021-02-20 23:35:16 +01:00
|
|
|
// write cached body
|
2021-06-06 14:39:42 +02:00
|
|
|
_, _ = w.Write(ci.body)
|
2020-07-29 22:45:26 +02:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2021-12-30 12:40:21 +01:00
|
|
|
func cacheable(r *http.Request) bool {
|
2022-01-31 11:31:16 +01:00
|
|
|
if r.Method != http.MethodGet && r.Method != http.MethodHead {
|
2021-12-30 12:40:21 +01:00
|
|
|
return false
|
|
|
|
}
|
|
|
|
if r.URL.Query().Get("cache") == "0" || r.URL.Query().Get("cache") == "false" {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
2022-02-21 18:47:41 +01:00
|
|
|
func cacheKey(r *http.Request) (key string) {
|
|
|
|
buf := bufferpool.Get()
|
2021-02-24 13:16:33 +01:00
|
|
|
// Special cases
|
|
|
|
if asRequest, ok := r.Context().Value(asRequestKey).(bool); ok && asRequest {
|
2022-02-21 18:47:41 +01:00
|
|
|
_, _ = buf.WriteString("as-")
|
2021-02-24 13:16:33 +01:00
|
|
|
}
|
2021-05-09 14:42:53 +02:00
|
|
|
if torUsed, ok := r.Context().Value(torUsedKey).(bool); ok && torUsed {
|
2022-02-21 18:47:41 +01:00
|
|
|
_, _ = buf.WriteString("tor-")
|
2021-05-09 14:42:53 +02:00
|
|
|
}
|
2021-07-23 17:26:14 +02:00
|
|
|
// Add cache URL
|
|
|
|
_, _ = buf.WriteString(r.URL.EscapedPath())
|
2022-02-21 18:47:41 +01:00
|
|
|
if query := r.URL.Query(); len(query) > 0 {
|
2021-02-24 13:16:33 +01:00
|
|
|
_ = buf.WriteByte('?')
|
2022-02-21 18:47:41 +01:00
|
|
|
keys := make([]string, 0, len(query))
|
|
|
|
for k := range query {
|
|
|
|
keys = append(keys, k)
|
|
|
|
}
|
|
|
|
sort.Strings(keys)
|
|
|
|
for i, k := range keys {
|
|
|
|
keyEscaped := url.QueryEscape(k)
|
|
|
|
for j, val := range query[k] {
|
|
|
|
if i > 0 || j > 0 {
|
|
|
|
buf.WriteByte('&')
|
|
|
|
}
|
|
|
|
buf.WriteString(keyEscaped)
|
|
|
|
buf.WriteByte('=')
|
|
|
|
buf.WriteString(url.QueryEscape(val))
|
|
|
|
}
|
|
|
|
}
|
2021-02-24 13:16:33 +01:00
|
|
|
}
|
2022-02-21 18:47:41 +01:00
|
|
|
// Get key as string
|
|
|
|
key = buf.String()
|
|
|
|
// Return buffer to pool
|
|
|
|
bufferpool.Put(buf)
|
|
|
|
return
|
2020-10-19 23:33:08 +02:00
|
|
|
}
|
|
|
|
|
2022-01-26 12:02:12 +01:00
|
|
|
func (a *goBlog) setCacheHeaders(w http.ResponseWriter, cache *cacheItem) {
|
2021-12-30 12:40:21 +01:00
|
|
|
// Copy headers
|
|
|
|
for k, v := range cache.header.Clone() {
|
|
|
|
w.Header()[k] = v
|
|
|
|
}
|
|
|
|
// Set cache headers
|
2021-01-10 17:19:08 +01:00
|
|
|
w.Header().Set("ETag", cache.eTag)
|
2023-04-11 16:43:14 +02:00
|
|
|
w.Header().Set(cacheControl, "public,no-cache")
|
2020-07-30 21:08:41 +02:00
|
|
|
}
|
|
|
|
|
2020-10-19 23:02:57 +02:00
|
|
|
type cacheItem struct {
|
2023-04-11 16:43:14 +02:00
|
|
|
expiration int
|
|
|
|
eTag string
|
|
|
|
code int
|
|
|
|
header http.Header
|
|
|
|
body []byte
|
2020-10-07 17:35:52 +02:00
|
|
|
}
|
|
|
|
|
2022-02-21 18:47:41 +01:00
|
|
|
// Calculate byte size of cache item using size of header, body and etag
|
|
|
|
func (ci *cacheItem) cost() int {
|
|
|
|
headerBuf := bufferpool.Get()
|
|
|
|
_ = ci.header.Write(headerBuf)
|
|
|
|
headerSize := len(headerBuf.Bytes())
|
|
|
|
bufferpool.Put(headerBuf)
|
|
|
|
return headerSize + len(ci.body) + len(ci.eTag)
|
2021-07-23 17:26:14 +02:00
|
|
|
}
|
|
|
|
|
2022-02-21 18:47:41 +01:00
|
|
|
func (c *cache) getCache(key string, next http.Handler, r *http.Request) *cacheItem {
|
2021-06-06 14:39:42 +02:00
|
|
|
if rItem, ok := c.c.Get(key); ok {
|
2022-02-21 18:47:41 +01:00
|
|
|
return rItem.(*cacheItem)
|
2020-11-20 15:33:20 +01:00
|
|
|
}
|
2022-02-21 18:47:41 +01:00
|
|
|
// No cache available
|
|
|
|
// Make and use copy of r
|
|
|
|
cr := r.Clone(valueOnlyContext{r.Context()})
|
|
|
|
// Remove problematic headers
|
|
|
|
cr.Header.Del("If-Modified-Since")
|
|
|
|
cr.Header.Del("If-Unmodified-Since")
|
|
|
|
cr.Header.Del("If-None-Match")
|
|
|
|
cr.Header.Del("If-Match")
|
|
|
|
cr.Header.Del("If-Range")
|
|
|
|
cr.Header.Del("Range")
|
|
|
|
// Record request
|
|
|
|
rec := newCacheRecorder()
|
|
|
|
next.ServeHTTP(rec, cr)
|
|
|
|
item := rec.finish()
|
|
|
|
// Set expiration
|
|
|
|
item.expiration, _ = cr.Context().Value(cacheExpirationKey).(int)
|
|
|
|
// Remove problematic headers
|
|
|
|
item.header.Del("Accept-Ranges")
|
|
|
|
item.header.Del("ETag")
|
2023-04-11 16:43:14 +02:00
|
|
|
item.header.Del("Last-Modified")
|
2022-02-21 18:47:41 +01:00
|
|
|
// Save cache
|
|
|
|
if cch := item.header.Get(cacheControl); !containsStrings(cch, "no-store", "private", "no-cache") {
|
|
|
|
cost := int64(item.cost())
|
|
|
|
if item.expiration == 0 {
|
|
|
|
c.c.Set(key, item, cost)
|
|
|
|
} else {
|
|
|
|
c.c.SetWithTTL(key, item, cost, time.Duration(item.expiration)*time.Second)
|
2021-02-27 09:05:39 +01:00
|
|
|
}
|
2020-10-19 23:02:57 +02:00
|
|
|
}
|
|
|
|
return item
|
2020-07-29 22:45:26 +02:00
|
|
|
}
|
2020-08-01 15:16:21 +02:00
|
|
|
|
2021-06-06 14:39:42 +02:00
|
|
|
func (c *cache) purge() {
|
2023-01-30 23:24:22 +01:00
|
|
|
if c == nil {
|
|
|
|
return
|
|
|
|
}
|
2021-06-06 14:39:42 +02:00
|
|
|
c.c.Clear()
|
2020-11-20 15:33:20 +01:00
|
|
|
}
|
|
|
|
|
2021-07-25 10:53:12 +02:00
|
|
|
func (a *goBlog) defaultCacheExpiration() int {
|
|
|
|
if a.cfg.Cache != nil {
|
|
|
|
return a.cfg.Cache.Expiration
|
2021-05-08 21:22:48 +02:00
|
|
|
}
|
2021-07-25 10:53:12 +02:00
|
|
|
return 0
|
2020-08-01 15:16:21 +02:00
|
|
|
}
|