@marcel91200 / caddy Mirror · read-only / commits / a53b27c6
Fork 0

Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS

Mirrored from https://github.com/Slazee/caddy.git

Synced Nov 14, 05:15 UTC

http: Add work-in-progress cache handler module

This migrates a feature that was previously reserved for enterprise users, according to https://github.com/caddyserver/caddy/issues/2786. The cache HTTP handler will be a high-performing, distributed cache layer for HTTP requests. Right now, the implementation is a very basic proof-of-concept, and further development is required.

Matthew Holt committed Oct 9, 2019 at 19:22 UTC a53b27c62e792e09380876db916289c77f2ae2e0
2 files changed +206
cmd/caddy/main.go
+1
@@ -28,6 +28,7 @@ import (
28 _ "github.com/caddyserver/caddy/v2/modules/caddyhttp/encode/zstd"
29 _ "github.com/caddyserver/caddy/v2/modules/caddyhttp/fileserver"
30 _ "github.com/caddyserver/caddy/v2/modules/caddyhttp/headers"
31 + _ "github.com/caddyserver/caddy/v2/modules/caddyhttp/httpcache"
32 _ "github.com/caddyserver/caddy/v2/modules/caddyhttp/markdown"
33 _ "github.com/caddyserver/caddy/v2/modules/caddyhttp/requestbody"
34 _ "github.com/caddyserver/caddy/v2/modules/caddyhttp/reverseproxy"
modules/caddyhttp/httpcache/httpcache.go new
+205
@@ -0,0 +1,205 @@
1 +// Copyright 2015 Matthew Holt and The Caddy Authors
2 +//
3 +// Licensed under the Apache License, Version 2.0 (the "License");
4 +// you may not use this file except in compliance with the License.
5 +// You may obtain a copy of the License at
6 +//
7 +// http://www.apache.org/licenses/LICENSE-2.0
8 +//
9 +// Unless required by applicable law or agreed to in writing, software
10 +// distributed under the License is distributed on an "AS IS" BASIS,
11 +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 +// See the License for the specific language governing permissions and
13 +// limitations under the License.
14 +
15 +package httpcache
16 +
17 +import (
18 + "bytes"
19 + "encoding/gob"
20 + "fmt"
21 + "io"
22 + "log"
23 + "net/http"
24 + "sync"
25 +
26 + "github.com/caddyserver/caddy/v2"
27 + "github.com/caddyserver/caddy/v2/modules/caddyhttp"
28 + "github.com/golang/groupcache"
29 +)
30 +
31 +func init() {
32 + caddy.RegisterModule(Cache{})
33 +}
34 +
35 +// Cache implements a simple distributed cache.
36 +type Cache struct {
37 + group *groupcache.Group
38 +}
39 +
40 +// CaddyModule returns the Caddy module information.
41 +func (Cache) CaddyModule() caddy.ModuleInfo {
42 + return caddy.ModuleInfo{
43 + Name: "http.handlers.cache",
44 + New: func() caddy.Module { return new(Cache) },
45 + }
46 +}
47 +
48 +// Provision provisions c.
49 +func (c *Cache) Provision(ctx caddy.Context) error {
50 + // TODO: proper pool configuration
51 + me := "http://localhost:5555"
52 + // TODO: Make max size configurable
53 + maxSize := int64(512 << 20)
54 + poolMu.Lock()
55 + if pool == nil {
56 + pool = groupcache.NewHTTPPool(me)
57 + c.group = groupcache.NewGroup(groupName, maxSize, groupcache.GetterFunc(c.getter))
58 + } else {
59 + c.group = groupcache.GetGroup(groupName)
60 + }
61 + pool.Set(me)
62 + poolMu.Unlock()
63 +
64 + return nil
65 +}
66 +
67 +// Validate validates c.
68 +func (c *Cache) Validate() error {
69 + // TODO: implement
70 + return nil
71 +}
72 +
73 +func (c *Cache) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
74 + // TODO: proper RFC implementation of cache control headers...
75 + if r.Header.Get("Cache-Control") == "no-cache" || r.Method != "GET" {
76 + return next.ServeHTTP(w, r)
77 + }
78 +
79 + ctx := getterContext{w, r, next}
80 +
81 + // TODO: rigorous performance testing
82 +
83 + // TODO: pretty much everything else to handle the nuances of HTTP caching...
84 +
85 + // TODO: groupcache has no explicit cache eviction, so we need to embed
86 + // all information related to expiring cache entries into the key; right
87 + // now we just use the request URI as a proof-of-concept
88 + key := r.RequestURI
89 +
90 + var cachedBytes []byte
91 + err := c.group.Get(ctx, key, groupcache.AllocatingByteSliceSink(&cachedBytes))
92 + if err == errUncacheable {
93 + return nil
94 + }
95 + if err != nil {
96 + return err
97 + }
98 +
99 + // the cached bytes consists of two parts: first a
100 + // gob encoding of the status and header, immediately
101 + // followed by the raw bytes of the response body
102 + rdr := bytes.NewReader(cachedBytes)
103 +
104 + // read the header and status first
105 + var hs headerAndStatus
106 + err = gob.NewDecoder(rdr).Decode(&hs)
107 + if err != nil {
108 + return err
109 + }
110 +
111 + // set and write the cached headers
112 + for k, v := range hs.Header {
113 + w.Header()[k] = v
114 + }
115 + w.WriteHeader(hs.Status)
116 +
117 + // write the cached response body
118 + io.Copy(w, rdr)
119 +
120 + return nil
121 +}
122 +
123 +func (c *Cache) getter(ctx groupcache.Context, key string, dest groupcache.Sink) error {
124 + combo := ctx.(getterContext)
125 +
126 + // the buffer will store the gob-encoded header, then the body
127 + buf := bufPool.Get().(*bytes.Buffer)
128 + buf.Reset()
129 + defer bufPool.Put(buf)
130 +
131 + // we need to record the response if we are to cache it; only cache if
132 + // request is successful (TODO: there's probably much more nuance needed here)
133 + var rr caddyhttp.ResponseRecorder
134 + rr = caddyhttp.NewResponseRecorder(combo.rw, buf, func(status int) bool {
135 + shouldBuf := status < 300
136 +
137 + if shouldBuf {
138 + // store the header before the body, so we can efficiently
139 + // and conveniently use a single buffer for both; gob
140 + // decoder will only read up to end of gob message, and
141 + // the rest will be the body, which will be written
142 + // implicitly for us by the recorder
143 + err := gob.NewEncoder(buf).Encode(headerAndStatus{
144 + Header: rr.Header(),
145 + Status: status,
146 + })
147 + if err != nil {
148 + log.Printf("[ERROR] Encoding headers for cache entry: %v; not caching this request", err)
149 + return false
150 + }
151 + }
152 +
153 + return shouldBuf
154 + })
155 +
156 + // execute next handlers in chain
157 + err := combo.next.ServeHTTP(rr, combo.req)
158 + if err != nil {
159 + return err
160 + }
161 +
162 + // if response body was not buffered, response was
163 + // already written and we are unable to cache
164 + if !rr.Buffered() {
165 + return errUncacheable
166 + }
167 +
168 + // add to cache
169 + dest.SetBytes(buf.Bytes())
170 +
171 + return nil
172 +}
173 +
174 +type headerAndStatus struct {
175 + Header http.Header
176 + Status int
177 +}
178 +
179 +type getterContext struct {
180 + rw http.ResponseWriter
181 + req *http.Request
182 + next caddyhttp.Handler
183 +}
184 +
185 +var bufPool = sync.Pool{
186 + New: func() interface{} {
187 + return new(bytes.Buffer)
188 + },
189 +}
190 +
191 +var (
192 + pool *groupcache.HTTPPool
193 + poolMu sync.Mutex
194 +)
195 +
196 +var errUncacheable = fmt.Errorf("uncacheable")
197 +
198 +const groupName = "http_requests"
199 +
200 +// Interface guards
201 +var (
202 + _ caddy.Provisioner = (*Cache)(nil)
203 + _ caddy.Validator = (*Cache)(nil)
204 + _ caddyhttp.MiddlewareHandler = (*Cache)(nil)
205 +)