forked from darkweak/souin
/
standalone.go
329 lines (287 loc) · 9.18 KB
/
standalone.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
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
package rfc
import (
"bufio"
"bytes"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"github.com/darkweak/souin/cache/providers"
"github.com/darkweak/souin/context"
"github.com/pquerna/cachecontrol/cacheobject"
)
// GetVariedCacheKey returns the varied cache key for req and resp.
func GetVariedCacheKey(req *http.Request, headers []string) string {
for i, v := range headers {
h := req.Header.Get(v)
if strings.Contains(h, ";") || strings.Contains(h, ":") {
h = url.QueryEscape(h)
}
headers[i] = fmt.Sprintf("%s:%s", v, h)
}
return req.Context().Value(context.Key).(string) + providers.VarySeparator + strings.Join(headers, providers.DecodedHeaderSeparator)
}
func validateMaxAgeCachedResponse(req *http.Request, res *http.Response, header string, maxAge int, addTime int) *http.Response {
a, _ := strconv.Atoi(res.Header.Get("Age"))
if maxAge >= 0 && (maxAge+addTime) < a {
return nil
}
return res
}
func ValidateMaxAgeCachedResponse(req *http.Request, res *http.Response) *http.Response {
co := req.Context().Value(context.RequestCacheControl).(*cacheobject.RequestCacheDirectives)
if co == nil {
return nil
}
return validateMaxAgeCachedResponse(req, res, "max-age", int(co.MaxAge), 0)
}
func ValidateMaxAgeCachedStaleResponse(req *http.Request, res *http.Response, addTime int) *http.Response {
co := req.Context().Value(context.RequestCacheControl).(*cacheobject.RequestCacheDirectives)
if !co.MaxStaleSet && co.MaxStale < 0 {
return nil
}
return validateMaxAgeCachedResponse(req, res, "max-stale", int(co.MaxStale), addTime)
}
// getFreshness will return one of fresh/stale/transparent based on the cache-control
// values of the request and the response
//
// fresh indicates the response can be returned
// stale indicates that the response needs validating before it is returned
// transparent indicates the response should not be used to fulfill the request
//
// Because this is only a private cache, 'public' and 'private' in cache-control aren't
// significant. Similarly, smax-age isn't used.
func getFreshness(respHeaders, reqHeaders http.Header) (freshness int) {
respCacheControl := parseCacheControl(respHeaders)
reqCacheControl := parseCacheControl(reqHeaders)
if _, ok := reqCacheControl["no-cache"]; ok {
return transparent
}
if _, ok := respCacheControl["no-cache"]; ok {
return stale
}
if _, ok := reqCacheControl["only-if-cached"]; ok {
return fresh
}
date, err := date(respHeaders)
if err != nil {
return stale
}
currentAge := clock.since(date)
var lifetime time.Duration
var zeroDuration time.Duration
// If a response includes both an Expires header and a max-age directive,
// the max-age directive overrides the Expires header, even if the Expires header is more restrictive.
if maxAge, ok := respCacheControl["max-age"]; ok {
lifetime, err = time.ParseDuration(maxAge + "s")
if err != nil {
lifetime = zeroDuration
}
} else {
expiresHeader := respHeaders.Get("Expires")
if expiresHeader != "" {
expires, e := time.Parse(time.RFC1123, expiresHeader)
if e != nil {
lifetime = zeroDuration
} else {
lifetime = expires.Sub(date)
}
}
}
if maxAge, ok := reqCacheControl["max-age"]; ok {
// the client is willing to accept a response whose age is no greater than the specified time in seconds
lifetime, err = time.ParseDuration(maxAge + "s")
if err != nil {
lifetime = zeroDuration
}
}
if minfresh, ok := reqCacheControl["min-fresh"]; ok {
// the client wants a response that will still be fresh for at least the specified number of seconds.
minfreshDuration, e := time.ParseDuration(minfresh + "s")
if e == nil {
currentAge = currentAge + minfreshDuration
}
}
if maxstale, ok := reqCacheControl["max-stale"]; ok {
// Indicates that the client is willing to accept a response that has exceeded its expiration time.
// If max-stale is assigned a value, then the client is willing to accept a response that has exceeded
// its expiration time by no more than the specified number of seconds.
// If no value is assigned to max-stale, then the client is willing to accept a stale response of any age.
//
// Responses served only because of a max-stale value are supposed to have a Warning header added to them,
// but that seems like a hassle, and is it actually useful? If so, then there needs to be a different
// return-value available here.
if maxstale == "" {
return fresh
}
maxstaleDuration, e := time.ParseDuration(maxstale + "s")
if e == nil {
currentAge = currentAge - maxstaleDuration
}
}
if lifetime > currentAge {
return fresh
}
return stale
}
// Returns true if either the request or the response includes the stale-if-error
// cache control extension: https://tools.ietf.org/html/rfc5861
func canStaleOnError(respHeaders, reqHeaders http.Header) bool {
respCacheControl := parseCacheControl(respHeaders)
reqCacheControl := parseCacheControl(reqHeaders)
var err error
lifetime := time.Duration(-1)
for _, cc := range []cacheControl{respCacheControl, reqCacheControl} {
if staleMaxAge, ok := cc["stale-if-error"]; ok {
if staleMaxAge != "" {
lifetime, err = time.ParseDuration(staleMaxAge + "s")
if err != nil {
return false
}
} else {
return true
}
}
}
if lifetime >= 0 {
date, err := date(respHeaders)
if err != nil {
return false
}
currentAge := clock.since(date)
if lifetime > currentAge {
return true
}
}
return false
}
func canStore(reqCacheControl cacheControl, respCacheControl cacheControl, status int) (canStore bool) {
if !CachableStatusCode(status) {
return false
}
for _, t := range []string{"no-cache", "no-store"} {
if _, ok := respCacheControl[t]; ok {
return false
}
if _, ok := reqCacheControl[t]; ok {
return false
}
}
return true
}
func CachableStatusCode(statusCode int) bool {
switch statusCode {
case 200, 203, 204, 206, 300, 301, 404, 405, 410, 414, 501:
return true
default:
return false
}
}
func newGatewayTimeoutResponse(req *http.Request) *http.Response {
var b bytes.Buffer
b.WriteString("HTTP/1.1 504 Gateway Timeout\r\n\r\n")
resp, _ := http.ReadResponse(bufio.NewReader(&b), req)
return resp
}
type cacheControl map[string]string
func parseCacheControl(headers http.Header) cacheControl {
cc := cacheControl{}
ccHeader := headers.Get("Cache-Control")
for _, part := range strings.Split(ccHeader, ",") {
part = strings.Trim(part, " ")
if part == "" {
continue
}
if strings.ContainsRune(part, '=') {
keyval := strings.Split(part, "=")
cc[strings.Trim(keyval[0], " ")] = strings.Trim(keyval[1], ",")
} else {
cc[part] = ""
}
}
return cc
}
// ErrNoDateHeader indicates that the HTTP headers contained no Date header.
var ErrNoDateHeader = errors.New("no Date header")
// Date parses and returns the value of the Date header.
func date(respHeaders http.Header) (date time.Time, err error) {
dateHeader := respHeaders.Get("date")
if dateHeader == "" {
err = ErrNoDateHeader
return
}
return time.Parse(time.RFC1123, dateHeader)
}
type realClock struct{}
func (c *realClock) since(d time.Time) time.Duration {
return time.Since(d)
}
type timer interface {
since(d time.Time) time.Duration
}
var clock timer = &realClock{}
// cloneRequest returns a clone of the provided *http.Request.
// The clone is a shallow copy of the struct and its Header map.
// (This function copyright goauth2 authors: https://code.google.com/p/goauth2)
func cloneRequest(r *http.Request) *http.Request {
// shallow copy of the struct
r2 := new(http.Request)
*r2 = *r
// deep copy of the Header
r2.Header = make(http.Header)
for k, s := range r.Header {
r2.Header[k] = s
}
return r2
}
// headerAllCommaSepValues returns all comma-separated values (each
// with whitespace trimmed) for header name in headers. According to
// Section 4.2 of the HTTP/1.1 spec
// (http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2),
// values from multiple occurrences of a header should be concatenated, if
// the header's value is a comma-separated list.
func headerAllCommaSepValues(headers http.Header) []string {
var vals []string
for _, val := range headers[http.CanonicalHeaderKey("vary")] {
fields := strings.Split(val, ",")
for i, f := range fields {
fields[i] = strings.TrimSpace(f)
}
vals = append(vals, fields...)
}
return vals
}
// cachingReadCloser is a wrapper around ReadCloser R that calls OnEOF
// handler with a full copy of the content read from R when EOF is
// reached.
type cachingReadCloser struct {
// Underlying ReadCloser.
R io.ReadCloser
// OnEOF is called with a copy of the content of R when EOF is reached.
OnEOF func(io.Reader)
buf bytes.Buffer // buf stores a copy of the content of R.
}
// Read reads the next len(p) bytes from R or until R is drained. The
// return value n is the number of bytes read. If R has no data to
// return, err is io.EOF and OnEOF is called with a full copy of what
// has been read so far.
func (r *cachingReadCloser) Read(p []byte) (n int, err error) {
if r.R == nil {
r.OnEOF(bytes.NewReader(p))
return 0, io.EOF
}
n, err = r.R.Read(p)
r.buf.Write(p[:n])
if err == io.EOF {
r.Close()
r.OnEOF(bytes.NewReader(r.buf.Bytes()))
}
return
}
func (r *cachingReadCloser) Close() error {
return r.R.Close()
}