This repository has been archived by the owner on Apr 18, 2021. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 30
/
Copy pathhandler.go
291 lines (237 loc) · 7.19 KB
/
handler.go
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
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
package cache
import (
"context"
"net/http"
"strings"
"github.com/caddyserver/caddy"
"github.com/caddyserver/caddy/caddyhttp/httpserver"
)
// Handler is the main cache middleware
type Handler struct {
// Handler configuration
Config *Config
// Cache is where entries are stored
Cache *HTTPCache
// Next handler
Next httpserver.Handler
// Handles locking for different URLs
URLLocks *URLLock
}
const (
cacheHit = "hit"
cacheMiss = "miss"
cacheSkip = "skip"
cacheBypass = "bypass"
)
var (
contextKeysToPreserve = []caddy.CtxKey{
httpserver.OriginalURLCtxKey,
httpserver.ReplacerCtxKey,
httpserver.RemoteUserCtxKey,
httpserver.MitmCtxKey,
httpserver.RequestIDCtxKey,
"path_prefix",
"mitm",
}
)
func getKey(cacheKeyTemplate string, r *http.Request) string {
return httpserver.NewReplacer(r, nil, "").Replace(cacheKeyTemplate)
}
// NewHandler creates a new Handler using Next middleware
func NewHandler(Next httpserver.Handler, config *Config) *Handler {
return &Handler{
Config: config,
Cache: NewHTTPCache(config.CacheKeyTemplate),
URLLocks: NewURLLock(),
Next: Next,
}
}
/* Responses */
func copyHeaders(from http.Header, to http.Header) {
for k, values := range from {
for _, v := range values {
to.Add(k, v)
}
}
}
func (handler *Handler) addStatusHeaderIfConfigured(w http.ResponseWriter, status string) {
if rec, ok := w.(*httpserver.ResponseRecorder); ok {
rec.Replacer.Set("cache_status", status)
}
if handler.Config.StatusHeader != "" {
w.Header().Add(handler.Config.StatusHeader, status)
}
}
func (handler *Handler) respond(w http.ResponseWriter, entry *HTTPCacheEntry, cacheStatus string) (int, error) {
handler.addStatusHeaderIfConfigured(w, cacheStatus)
copyHeaders(entry.Response.snapHeader, w.Header())
w.WriteHeader(entry.Response.Code)
err := entry.WriteBodyTo(w)
return entry.Response.Code, err
}
/* Handler */
func shouldUseCache(req *http.Request) bool {
// TODO Add more logic like get params, ?nocache=true
if req.Method != "GET" && req.Method != "HEAD" {
// Only cache Get and head request
return false
}
// Range requests still not supported
// It may happen that the previous request for this url has a successful response
// but for another Range. So a special handling is needed
if req.Header.Get("range") != "" {
return false
}
if isWebSocket(req.Header) {
return false
}
return true
}
func popOrNil(errChan chan error) (err error) {
select {
case err = <-errChan:
default:
}
return
}
func (handler *Handler) fetchUpstream(req *http.Request) (*HTTPCacheEntry, error) {
// Create a new empty response
response := NewResponse()
errChan := make(chan error, 1)
// Do the upstream fetching in background
go func(req *http.Request, response *Response) {
// Create a new context to avoid terminating the Next.ServeHTTP when the original
// request is closed. Otherwise if the original request is cancelled the other requests
// will see a bad response that has the same contents the first request has
updatedContext := context.Background()
// The problem of cloning the context is that the original one has some values used by
// other middlewares. If those values are not present they break, #22 is an example.
// However there isn't a way to know which values a context has. I took the ones that
// I found on caddy code. If in a future there are new ones this might break.
// In that case this will have to change to another way
for _, key := range contextKeysToPreserve {
value := req.Context().Value(key)
if value != nil {
updatedContext = context.WithValue(updatedContext, key, value)
}
}
updatedReq := req.WithContext(updatedContext)
statusCode, upstreamError := handler.Next.ServeHTTP(response, updatedReq)
errChan <- upstreamError
// If status code was not set, this will not replace it
// It will only ensure status code IS send
response.WriteHeader(statusCode)
// Wait the response body to be set.
// If it is private it will be the original http.ResponseWriter
// It is required to wait the body to prevent closing the response
// before the body was set. If that happens the body will
// stay locked waiting the response to be closed
response.WaitBody()
response.Close()
}(req, response)
// Wait headers to be sent
response.WaitHeaders()
// Create a new CacheEntry
return NewHTTPCacheEntry(getKey(handler.Config.CacheKeyTemplate, req), req, response, handler.Config), popOrNil(errChan)
}
func (handler *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
if !shouldUseCache(r) {
handler.addStatusHeaderIfConfigured(w, cacheBypass)
return handler.Next.ServeHTTP(w, r)
}
lock := handler.URLLocks.Adquire(getKey(handler.Config.CacheKeyTemplate, r))
// Lookup correct entry
previousEntry, exists := handler.Cache.Get(r)
// First case: CACHE HIT
// The response exists in cache and is public
// It should be served as saved
if exists && previousEntry.isPublic {
lock.Unlock()
return handler.respond(w, previousEntry, cacheHit)
}
// Second case: CACHE SKIP
// The response is in cache but it is not public
// It should NOT be served from cache
// It should be fetched from upstream and check the new headers
// To check if the new response changes to public
if exists && !previousEntry.isPublic {
lock.Unlock()
entry, err := handler.fetchUpstream(r)
if err != nil {
return entry.Response.Code, err
}
// Case when response was private but now is public
if entry.isPublic {
err := entry.setStorage(handler.Config)
if err != nil {
return 500, err
}
handler.Cache.Put(r, entry)
return handler.respond(w, entry, cacheMiss)
}
return handler.respond(w, entry, cacheSkip)
}
// Third case: CACHE MISS
// The response is not in cache
// It should be fetched from upstream and save it in cache
entry, err := handler.fetchUpstream(r)
if err != nil {
lock.Unlock()
return entry.Response.Code, err
}
// Entry is always saved, even if it is not public
// This is to release the URL lock.
if entry.isPublic {
err := entry.setStorage(handler.Config)
if err != nil {
lock.Unlock()
return 500, err
}
}
handler.Cache.Put(r, entry)
lock.Unlock()
return handler.respond(w, entry, cacheMiss)
}
func isWebSocket(h http.Header) bool {
if h == nil {
return false
}
// Get gets the *first* value associated with the given key.
if strings.ToLower(h.Get("Upgrade")) != "websocket" {
return false
}
// To access multiple values of a key, access the map directly.
for _, value := range getHeaderValues(h, "Connection") {
if strings.ToLower(value) == "websocket" {
return true
}
}
return false
}
func getHeaderValues(h http.Header, name string) []string {
var values = []string{}
if h == nil {
return values
}
// If a server received a request with header lines,
//
// Host: example.com
// accept-encoding: gzip, deflate
// Accept-Language: en-us
// fOO: Bar
// foo: two
//
// then
//
// Header = map[string][]string{
// "Accept-Encoding": {"gzip, deflate"},
// "Accept-Language": {"en-us"},
// "Foo": {"Bar", "two"},
// }
for _, slice := range h[name] {
for _, value := range strings.Split(slice, ",") {
values = append(values, strings.Trim(value, " "))
}
}
return values
}