summaryrefslogtreecommitdiff
path: root/modules/caddyhttp/httpcache/httpcache.go
blob: f8bdde8859d0be75f94bdac6587b0264e1771202 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
// Copyright 2015 Matthew Holt and The Caddy Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package httpcache

import (
	"bytes"
	"encoding/gob"
	"fmt"
	"io"
	"log"
	"net/http"
	"sync"

	"github.com/caddyserver/caddy/v2"
	"github.com/caddyserver/caddy/v2/modules/caddyhttp"
	"github.com/golang/groupcache"
)

func init() {
	caddy.RegisterModule(Cache{})
}

// Cache implements a simple distributed cache.
//
// NOTE: This module is a work-in-progress. It is
// not finished and is NOT ready for production use.
// [We need your help to finish it! Please volunteer
// in this issue.](https://github.com/caddyserver/caddy/issues/2820)
// Until it is finished, this module is subject to
// breaking changes.
//
// Caches only GET and HEAD requests. Honors the Cache-Control: no-cache header.
//
// Still TODO:
//
// - Eviction policies and API
// - Use single cache per-process
// - Preserve cache through config reloads
// - More control over what gets cached
type Cache struct {
	// The network address of this cache instance; required.
	Self string `json:"self,omitempty"`

	// A list of network addresses of cache instances in the group.
	Peers []string `json:"peers,omitempty"`

	// Maximum size of the cache, in bytes. Default is 512 MB.
	MaxSize int64 `json:"max_size,omitempty"`

	group *groupcache.Group
}

// CaddyModule returns the Caddy module information.
func (Cache) CaddyModule() caddy.ModuleInfo {
	return caddy.ModuleInfo{
		ID:  "http.handlers.cache",
		New: func() caddy.Module { return new(Cache) },
	}
}

// Provision provisions c.
func (c *Cache) Provision(ctx caddy.Context) error {
	// TODO: use UsagePool so that cache survives config reloads - TODO: a single cache for whole process?
	maxSize := c.MaxSize
	if maxSize == 0 {
		const maxMB = 512
		maxSize = int64(maxMB << 20)
	}
	poolMu.Lock()
	if pool == nil {
		pool = groupcache.NewHTTPPool(c.Self)
		c.group = groupcache.NewGroup(groupName, maxSize, groupcache.GetterFunc(c.getter))
	} else {
		c.group = groupcache.GetGroup(groupName)
	}
	pool.Set(append(c.Peers, c.Self)...)
	poolMu.Unlock()

	return nil
}

// Validate validates c.
func (c *Cache) Validate() error {
	if c.Self == "" {
		return fmt.Errorf("address of this instance (self) is required")
	}
	if c.MaxSize < 0 {
		return fmt.Errorf("size must be greater than 0")
	}
	return nil
}

func (c *Cache) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
	// TODO: proper RFC implementation of cache control headers...
	if r.Header.Get("Cache-Control") == "no-cache" || (r.Method != "GET" && r.Method != "HEAD") {
		return next.ServeHTTP(w, r)
	}

	ctx := getterContext{w, r, next}

	// TODO: rigorous performance testing

	// TODO: pretty much everything else to handle the nuances of HTTP caching...

	// TODO: groupcache has no explicit cache eviction, so we need to embed
	// all information related to expiring cache entries into the key; right
	// now we just use the request URI as a proof-of-concept
	key := r.RequestURI

	var cachedBytes []byte
	err := c.group.Get(ctx, key, groupcache.AllocatingByteSliceSink(&cachedBytes))
	if err == errUncacheable {
		return nil
	}
	if err != nil {
		return err
	}

	// the cached bytes consists of two parts: first a
	// gob encoding of the status and header, immediately
	// followed by the raw bytes of the response body
	rdr := bytes.NewReader(cachedBytes)

	// read the header and status first
	var hs headerAndStatus
	err = gob.NewDecoder(rdr).Decode(&hs)
	if err != nil {
		return err
	}

	// set and write the cached headers
	for k, v := range hs.Header {
		w.Header()[k] = v
	}
	w.WriteHeader(hs.Status)

	// write the cached response body
	io.Copy(w, rdr)

	return nil
}

func (c *Cache) getter(ctx groupcache.Context, key string, dest groupcache.Sink) error {
	combo := ctx.(getterContext)

	// the buffer will store the gob-encoded header, then the body
	buf := bufPool.Get().(*bytes.Buffer)
	buf.Reset()
	defer bufPool.Put(buf)

	// we need to record the response if we are to cache it; only cache if
	// request is successful (TODO: there's probably much more nuance needed here)
	rr := caddyhttp.NewResponseRecorder(combo.rw, buf, func(status int, header http.Header) bool {
		shouldBuf := status < 300

		if shouldBuf {
			// store the header before the body, so we can efficiently
			// and conveniently use a single buffer for both; gob
			// decoder will only read up to end of gob message, and
			// the rest will be the body, which will be written
			// implicitly for us by the recorder
			err := gob.NewEncoder(buf).Encode(headerAndStatus{
				Header: header,
				Status: status,
			})
			if err != nil {
				log.Printf("[ERROR] Encoding headers for cache entry: %v; not caching this request", err)
				return false
			}
		}

		return shouldBuf
	})

	// execute next handlers in chain
	err := combo.next.ServeHTTP(rr, combo.req)
	if err != nil {
		return err
	}

	// if response body was not buffered, response was
	// already written and we are unable to cache
	if !rr.Buffered() {
		return errUncacheable
	}

	// add to cache
	dest.SetBytes(buf.Bytes())

	return nil
}

type headerAndStatus struct {
	Header http.Header
	Status int
}

type getterContext struct {
	rw   http.ResponseWriter
	req  *http.Request
	next caddyhttp.Handler
}

var bufPool = sync.Pool{
	New: func() interface{} {
		return new(bytes.Buffer)
	},
}

var (
	pool   *groupcache.HTTPPool
	poolMu sync.Mutex
)

var errUncacheable = fmt.Errorf("uncacheable")

const groupName = "http_requests"

// Interface guards
var (
	_ caddy.Provisioner           = (*Cache)(nil)
	_ caddy.Validator             = (*Cache)(nil)
	_ caddyhttp.MiddlewareHandler = (*Cache)(nil)
)