Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions config/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"github.com/mholt/caddy/middleware"
"github.com/mholt/caddy/middleware/basicauth"
"github.com/mholt/caddy/middleware/browse"
"github.com/mholt/caddy/middleware/cache"
"github.com/mholt/caddy/middleware/errors"
"github.com/mholt/caddy/middleware/extensions"
"github.com/mholt/caddy/middleware/fastcgi"
Expand Down Expand Up @@ -41,6 +42,7 @@ import (
func init() {
register("log", log.New)
register("gzip", gzip.New)
register("cache", cache.New)
register("errors", errors.New)
register("header", headers.New)
register("rewrite", rewrite.New)
Expand Down
142 changes: 142 additions & 0 deletions middleware/cache/cache.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
// Package cache provides a simple middleware layer that remembers
// previously served requests and serves those from memory.
package cache

import (
"net/http"
"net/http/httptest"
"strconv"
"sync"
"time"

"github.com/dustin/go-humanize"
"github.com/mholt/caddy/middleware"
)

// Example line in CaddyFile: cache 60 128mb 10mb
// Arguments are max-age in seconds, max cache size, max size for an individual file in the cache

// Cache is an http.Handler that can remembers and sends back stored responses
type Cache struct {
Next middleware.Handler
Lifetime int
Entries map[string]CacheEntry // url -> entry
Rule Rule
Mutex sync.RWMutex
}

type CacheEntry struct {
created int64
lastUsed int64
Size int //bytes
Code int
HeaderMap http.Header
Body []byte
}

type Rule struct {
MaxAge int64 //seconds
MaxCacheSize int64 //bytes
MaxCacheEntrySize int //bytes
}

// New creates a new cache middleware instance.
func New(c middleware.Controller) (middleware.Middleware, error) {
rules, err := parse(c)
if err != nil {
return nil, err
}
return func(next middleware.Handler) middleware.Handler {
// TODO: handle more than one rule? handle first or last rule?
return Cache{Next: next, Lifetime: 60 * 10, Entries: make(map[string]CacheEntry), Rule: rules[0]}
}, nil
}

func parse(c middleware.Controller) ([]Rule, error) {
var rules []Rule

for c.Next() {

var ageString, cacheSizeString, cacheEntrySizeString string
if !c.Args(&ageString, &cacheSizeString, &cacheEntrySizeString) {
return rules, c.ArgErr()
}
age, err := strconv.Atoi(ageString)
if err != nil {
return rules, c.ArgErr()
}
cacheSize, err := humanize.ParseBytes(cacheSizeString)
if err != nil {
return rules, c.ArgErr()
}
cacheEntrySize, err := humanize.ParseBytes(cacheEntrySizeString)
if err != nil {
return rules, c.ArgErr()
}
rule := Rule{MaxAge: int64(age), MaxCacheSize: int64(cacheSize), MaxCacheEntrySize: int(cacheEntrySize)}
rules = append(rules, rule)
}

return rules, nil
}

// Writes the headers and body to the writer
func WriteEntry(w http.ResponseWriter, entry CacheEntry) {
w.WriteHeader(entry.Code)
for key, valueArray := range entry.HeaderMap {
for _, value := range valueArray {
w.Header().Set(key, value)
}
}
w.Write(entry.Body)
}

func ClientAllowsCaching(r *http.Request) bool {
// TODO: Actually parse the Cache-Control and Pragma header
// Currently this won't do any caching if these headers are present
return r.Header.Get("Cache-Control") == "" && r.Header.Get("Pragma") == ""
}

func ServerAllowsCaching(headers http.Header) bool {
// TODO: This is more strict than necessary. Better parsing needed.
cacheControl := headers.Get("Cache-Control")
return cacheControl == "" || cacheControl == "public"
}

// ServeHTTP serves a gzipped response if the client supports it.
func (c Cache) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
if r.Method == "GET" {
key := r.RequestURI
now := time.Now().Unix()

c.Mutex.RLock()
entry, inCache := c.Entries[key]
c.Mutex.RUnlock()

if inCache && now-entry.created < c.Rule.MaxAge && ClientAllowsCaching(r) {
entry.lastUsed = time.Now().Unix()
} else {
record := httptest.NewRecorder()
status, err := c.Next.ServeHTTP(record, r)
normalResponse := err == nil && status < 300 && record.Code < 300

body := record.Body.Bytes()
bodySize := len(body) + 100 // TODO: Better approximation of size of headers, etc. For now just 100 bytes.
entry = CacheEntry{created: now, lastUsed: now, Code: record.Code, HeaderMap: record.HeaderMap, Body: body, Size: bodySize}

if normalResponse && bodySize < c.Rule.MaxCacheEntrySize && ServerAllowsCaching(record.Header()) {
// adds response to cache
c.Mutex.Lock()
c.Entries[key] = entry
c.Mutex.Unlock()
}
}

WriteEntry(w, entry)
return entry.Code, nil
} else {
// skip caching entirely
return c.Next.ServeHTTP(w, r)
}

}