GoBlog/cache.go

229 lines
6.0 KiB
Go
Raw Normal View History

2020-07-29 20:45:26 +00:00
package main
import (
2020-11-25 13:59:48 +00:00
"bytes"
2021-07-25 08:53:12 +00:00
"context"
2020-11-25 13:59:48 +00:00
"crypto/sha256"
"encoding/binary"
2020-11-20 14:33:20 +00:00
"fmt"
2020-11-25 13:59:48 +00:00
"io"
2020-07-29 20:45:26 +00:00
"net/http"
"net/http/httptest"
2021-02-24 12:16:33 +00:00
"strings"
2020-07-29 20:45:26 +00:00
"time"
2020-11-26 08:44:44 +00:00
"github.com/araddon/dateparse"
"github.com/dgraph-io/ristretto"
"golang.org/x/sync/singleflight"
2020-07-29 20:45:26 +00:00
)
2021-07-25 08:53:12 +00:00
const (
cacheLoggedInKey contextKey = "cacheLoggedIn"
cacheExpirationKey contextKey = "cacheExpiration"
)
2020-11-20 14:33:20 +00:00
type cache struct {
g singleflight.Group
c *ristretto.Cache
cfg *configCache
}
2020-09-22 14:42:36 +00:00
func (a *goBlog) initCache() (err error) {
a.cache = &cache{
cfg: a.cfg.Cache,
}
if a.cache.cfg != nil && !a.cache.cfg.Enable {
return nil
}
a.cache.c, err = ristretto.NewCache(&ristretto.Config{
NumCounters: 40 * 1000, // 4000 items when full with 5 KB items -> x10 = 40.000
MaxCost: 20 * 1000 * 1000, // 20 MB
BufferItems: 64, // recommended
})
2020-11-20 14:33:20 +00:00
return
}
2020-09-22 14:42:36 +00:00
2021-07-25 08:53:12 +00: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)))
})
}
func (a *goBlog) cacheMiddleware(next http.Handler) http.Handler {
2020-07-29 20:45:26 +00:00
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
2021-07-25 08:53:12 +00:00
if a.cache.c == nil {
// No cache configured
2021-02-20 22:35:16 +00:00
next.ServeHTTP(w, r)
return
}
// Do checks
2021-02-20 22:35:16 +00:00
if !(r.Method == http.MethodGet || r.Method == http.MethodHead) {
next.ServeHTTP(w, r)
return
}
if r.URL.Query().Get("cache") == "0" || r.URL.Query().Get("cache") == "false" {
next.ServeHTTP(w, r)
return
}
2021-07-25 08:53:12 +00:00
// Check login
if cli, ok := r.Context().Value(cacheLoggedInKey).(bool); ok && cli {
// Continue caching, but remove login
setLoggedIn(r, false)
} else {
if a.isLoggedIn(r) {
next.ServeHTTP(w, r)
return
}
2021-02-20 22:35:16 +00:00
}
// Search and serve cache
key := cacheKey(r)
// Get cache or render it
2021-07-25 08:53:12 +00:00
cacheInterface, _, _ := a.cache.g.Do(key, func() (interface{}, error) {
return a.cache.getCache(key, next, r), nil
2021-02-20 22:35:16 +00:00
})
ci := cacheInterface.(*cacheItem)
2021-02-20 22:35:16 +00:00
// copy cached headers
for k, v := range ci.header {
2021-02-20 22:35:16 +00:00
w.Header()[k] = v
}
2021-07-25 08:53:12 +00:00
a.cache.setCacheHeaders(w, ci)
2021-02-20 22:35:16 +00:00
// check conditional request
if ifNoneMatchHeader := r.Header.Get("If-None-Match"); ifNoneMatchHeader != "" && ifNoneMatchHeader == ci.eTag {
2021-02-20 22:35:16 +00:00
// send 304
w.WriteHeader(http.StatusNotModified)
return
}
if ifModifiedSinceHeader := r.Header.Get("If-Modified-Since"); ifModifiedSinceHeader != "" {
if t, err := dateparse.ParseAny(ifModifiedSinceHeader); err == nil && t.After(ci.creationTime) {
2020-11-25 13:59:48 +00:00
// send 304
w.WriteHeader(http.StatusNotModified)
return
}
2020-07-29 20:45:26 +00:00
}
2021-02-20 22:35:16 +00:00
// set status code
w.WriteHeader(ci.code)
2021-02-20 22:35:16 +00:00
// write cached body
_, _ = w.Write(ci.body)
2020-07-29 20:45:26 +00:00
})
}
func cacheKey(r *http.Request) string {
var buf strings.Builder
2021-02-24 12:16:33 +00:00
// Special cases
if asRequest, ok := r.Context().Value(asRequestKey).(bool); ok && asRequest {
buf.WriteString("as-")
2021-02-24 12:16:33 +00:00
}
2021-05-09 12:42:53 +00:00
if torUsed, ok := r.Context().Value(torUsedKey).(bool); ok && torUsed {
buf.WriteString("tor-")
2021-05-09 12:42:53 +00:00
}
// Add cache URL
_, _ = buf.WriteString(r.URL.EscapedPath())
if q := r.URL.Query(); len(q) > 0 {
2021-02-24 12:16:33 +00:00
_ = buf.WriteByte('?')
2021-03-08 17:14:52 +00:00
_, _ = buf.WriteString(q.Encode())
2021-02-24 12:16:33 +00:00
}
// Return string
2021-02-24 12:16:33 +00:00
return buf.String()
}
func (c *cache) setCacheHeaders(w http.ResponseWriter, cache *cacheItem) {
w.Header().Set("ETag", cache.eTag)
2021-01-10 13:22:02 +00:00
w.Header().Set("Last-Modified", cache.creationTime.UTC().Format(http.TimeFormat))
2020-11-26 16:40:40 +00:00
if w.Header().Get("Cache-Control") == "" {
if cache.expiration != 0 {
2021-01-10 13:22:02 +00:00
w.Header().Set("Cache-Control", fmt.Sprintf("public,max-age=%d,stale-while-revalidate=%d", cache.expiration, cache.expiration))
2020-11-26 16:40:40 +00:00
} else {
w.Header().Set("Cache-Control", fmt.Sprintf("public,max-age=%d,s-max-age=%d,stale-while-revalidate=%d", c.cfg.Expiration, c.cfg.Expiration/3, c.cfg.Expiration))
2020-11-26 16:40:40 +00:00
}
2020-11-20 14:33:20 +00:00
}
2020-07-30 19:08:41 +00:00
}
type cacheItem struct {
2020-11-20 14:33:20 +00:00
expiration int
2021-01-10 13:22:02 +00:00
creationTime time.Time
eTag string
code int
header http.Header
body []byte
2020-10-07 15:35:52 +00:00
}
// Calculate byte size of cache item using size of body and header
func (ci *cacheItem) cost() int64 {
var headerBuf strings.Builder
_ = ci.header.Write(&headerBuf)
headerSize := int64(binary.Size(headerBuf.String()))
bodySize := int64(binary.Size(ci.body))
return headerSize + bodySize
}
func (c *cache) getCache(key string, next http.Handler, r *http.Request) (item *cacheItem) {
if rItem, ok := c.c.Get(key); ok {
item = rItem.(*cacheItem)
2020-11-20 14:33:20 +00:00
}
if item == nil {
// No cache available
2021-07-25 08:53:12 +00:00
// Make and use copy of r
cr := r.Clone(r.Context())
2021-01-10 14:45:34 +00:00
// Remove problematic headers
2021-07-25 08:53:12 +00:00
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
recorder := httptest.NewRecorder()
2021-07-25 08:53:12 +00:00
next.ServeHTTP(recorder, cr)
// Cache values from recorder
result := recorder.Result()
2021-02-17 07:23:03 +00:00
body, _ := io.ReadAll(result.Body)
2020-11-25 13:59:48 +00:00
_ = result.Body.Close()
eTag := result.Header.Get("ETag")
if eTag == "" {
h := sha256.New()
_, _ = io.Copy(h, bytes.NewReader(body))
eTag = fmt.Sprintf("%x", h.Sum(nil))
}
2021-01-10 14:45:34 +00:00
lastMod := time.Now()
if lm := result.Header.Get("Last-Modified"); lm != "" {
2021-01-10 15:11:53 +00:00
if parsedTime, te := dateparse.ParseLocal(lm); te == nil {
lastMod = parsedTime
}
2021-01-10 14:45:34 +00:00
}
2021-07-25 08:53:12 +00:00
exp, _ := cr.Context().Value(cacheExpirationKey).(int)
// Remove problematic headers
result.Header.Del("Accept-Ranges")
result.Header.Del("ETag")
result.Header.Del("Last-Modified")
// Create cache item
2020-10-20 16:15:15 +00:00
item = &cacheItem{
2020-11-20 14:33:20 +00:00
expiration: exp,
2021-01-10 14:45:34 +00:00
creationTime: lastMod,
eTag: eTag,
code: result.StatusCode,
header: result.Header,
body: body,
2020-10-20 16:15:15 +00:00
}
// Save cache
if cch := item.header.Get("Cache-Control"); !containsStrings(cch, "no-store", "private", "no-cache") {
if exp == 0 {
c.c.Set(key, item, item.cost())
} else {
c.c.SetWithTTL(key, item, item.cost(), time.Duration(exp)*time.Second)
}
}
}
return item
2020-07-29 20:45:26 +00:00
}
func (c *cache) purge() {
c.c.Clear()
2020-11-20 14:33:20 +00:00
}
2021-07-25 08:53:12 +00:00
func (a *goBlog) defaultCacheExpiration() int {
if a.cfg.Cache != nil {
return a.cfg.Cache.Expiration
2021-05-08 19:22:48 +00:00
}
2021-07-25 08:53:12 +00:00
return 0
}