Skip to content

Commit c964ce1

Browse files
author
Dylan Reimerink
committed
Initial commit: what I have so far
0 parents  commit c964ce1

13 files changed

+1254
-0
lines changed

AUTHORS.txt

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# This is the official list of SharedHTTPCache authors for copyright purposes.
2+
#
3+
# Please keep the list sorted.
4+
5+
Dylan Reimerink (https://github.com/dylandreimerink)

LICENCE.txt

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2019 The ShareHTTPCache authors
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# HTTP caching
2+
3+
The goal of this project is to make a RFC 7234 compliant shared caching server in Go. Tho the main goal is to have a out-of-the-box working caching server it is also important that the functionality is exported so it can be used as library in bigger projects.
4+
5+
## Features
6+
7+
- Flexible configuration
8+
- Multi layer system
9+
- Customizable logging
10+
11+
## Examples
12+
13+
TODO make some usage examples
14+
15+
## TODO
16+
17+
- Make fully RFC7234 compliant
18+
- Adding tests, both unit and integration
19+
- Add project to CI pipeline with code standards
20+
- Store partial responses
21+
- Combining Partial Content
22+
- Calculating Heuristic Freshness based on past behavior
23+
- Refactor code to improve readability
24+
- Add informational headers about cache hit's ect.
25+
- Add HTTP/2 support
26+
- Add Cache-Control extensions (Or at least make a callback so someone can from outside the package)
27+
- Add metrics (prometheus)
28+
- Add user triggered cache invalidation
29+
- Add advanced cache replacement policies to inmemory layer (https://en.wikipedia.org/wiki/Cache_replacement_policies)
30+
- Add disk storage layer
31+
- Add redis storage layer
32+
- Add s3 storage layer

cacheability.go

+258
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
package caching
2+
3+
import (
4+
"net/http"
5+
"strconv"
6+
"strings"
7+
"time"
8+
)
9+
10+
//ShouldStoreResponse determins based on the cache config if this request should be stored
11+
// It determins this based on section 3 of RFC7234
12+
//
13+
// TODO restructure this function so common reasons for no storing a response are checked first
14+
// this can improve performance a lot
15+
func ShouldStoreResponse(config *CacheConfig, resp *http.Response) bool {
16+
req := resp.Request
17+
18+
//If the request method is unsafe the response should not be cached
19+
if !IsMethodSafe(config, req.Method) {
20+
return false
21+
}
22+
23+
//If the request method is marked as not cacheable the response should not be cached
24+
if !IsMethodCacheable(config, req.Method) {
25+
return false
26+
}
27+
28+
//If the response is partial and the configuration doesn't permit partial responses don't cache
29+
if resp.StatusCode == http.StatusPartialContent && !config.CacheIncompleteResponses {
30+
return false
31+
}
32+
33+
requestCacheControlDirectives := []string{}
34+
for _, directive := range strings.Split(strings.ToLower(req.Header.Get("Cache-Control")), ",") {
35+
requestCacheControlDirectives = append(requestCacheControlDirectives, strings.TrimSpace(directive))
36+
}
37+
38+
//if the request contains the cache-control header and it contains no-store the response should not be cached
39+
for _, directive := range requestCacheControlDirectives {
40+
if directive == "no-store" {
41+
return false
42+
}
43+
}
44+
45+
responseCacheControlDirectives := []string{}
46+
for _, directive := range strings.Split(strings.ToLower(resp.Header.Get("Cache-Control")), ",") {
47+
responseCacheControlDirectives = append(responseCacheControlDirectives, strings.TrimSpace(directive))
48+
}
49+
50+
for _, directive := range responseCacheControlDirectives {
51+
//if the response contains the cache-control header and it contains no-store the response should not be cached
52+
if directive == "no-store" {
53+
return false
54+
}
55+
56+
//if the response contains the cache-control header and it contains private the response should not be cached
57+
// because this is a shared cache server
58+
if directive == "private" {
59+
return false
60+
}
61+
}
62+
63+
//if the authorization header is set and the cache is shared(which it is)
64+
// https://tools.ietf.org/html/rfc7234#section-3.2
65+
if req.Header.Get("Authorization") != "" {
66+
67+
//Check if the cache-control header in the response allows this
68+
allowed := false
69+
70+
for _, directive := range responseCacheControlDirectives {
71+
if directive == "must-revalidate" || directive == "public" {
72+
allowed = true
73+
}
74+
75+
if strings.HasPrefix(directive, "s-maxage") {
76+
allowed = true
77+
}
78+
}
79+
80+
//Don't cache unless specificity allowed
81+
if !allowed {
82+
return false
83+
}
84+
}
85+
86+
//if the expires header is set (see Section 5.3 of RFC7234)
87+
if resp.Header.Get("Expires") != "" {
88+
89+
expires, err := http.ParseTime(resp.Header.Get("Expires"))
90+
if err != nil {
91+
92+
//If parsing the time gives a error it violates http/1.1
93+
return false
94+
}
95+
96+
//If the expires is in the future, the response is cacheable
97+
if expires.Sub(time.Now()) > 0 {
98+
return true
99+
}
100+
}
101+
102+
for _, directive := range responseCacheControlDirectives {
103+
104+
//if the Cache-Control header contains max-age the response is cacheable (see Section 5.2.2.8 of RFC7234)
105+
if strings.HasPrefix(directive, "max-age") {
106+
return true
107+
}
108+
109+
//if the response header Cache-Control contains a s-maxage response directive (see Section 5.2.2.9 of RFC7234)
110+
// and the cache is shared (which it is)
111+
// the response is cacheable
112+
if strings.HasPrefix(directive, "s-maxage") {
113+
return true
114+
}
115+
116+
//if the response contains a public response directive (see Section 5.2.2.5).
117+
if directive == "public" {
118+
return true
119+
}
120+
}
121+
122+
//if the response has a status code that is defined as cacheable by default (see
123+
// Section 4.2.2)
124+
if _, found := config.StatusCodeDefaultExpirationTimes[resp.StatusCode]; found {
125+
return true
126+
}
127+
128+
return false
129+
}
130+
131+
//GetResponseTTL checks what the ttl/freshness_lifetime of a response should be based on the config
132+
// and section 4.2.1 of RFC 7234
133+
// if the ttl is negative the response is already stale
134+
func GetResponseTTL(config *CacheConfig, resp *http.Response) time.Duration {
135+
136+
//The header value is comma seperated, so split it on the comma.
137+
// Lowercase the directive so string comparason is easier and trim the spaces from the directives
138+
directives := []string{}
139+
for _, directive := range strings.Split(strings.ToLower(resp.Header.Get("Cache-Control")), ",") {
140+
directives = append(directives, strings.TrimSpace(directive))
141+
}
142+
143+
//s-maxage has priority because this is a shared cache
144+
for _, directive := range directives {
145+
146+
//If the directive starts with s-maxage
147+
if strings.HasPrefix(directive, "s-maxage") {
148+
149+
//Remove the key and equals sign and attempt to parse the remainder as a number
150+
// This assumes the origin server adheres to the RFC and sends the argument form.
151+
// TODO check for the quoted-string form
152+
sMaxAgeString := strings.TrimPrefix(directive, "s-maxage=")
153+
sMaxAge, err := strconv.ParseInt(sMaxAgeString, 10, 0)
154+
155+
if err != nil {
156+
return time.Duration(sMaxAge) * time.Second
157+
}
158+
}
159+
}
160+
161+
for _, directive := range directives {
162+
//If the directive starts with max-age
163+
if strings.HasPrefix(directive, "max-age") {
164+
165+
//Remove the key and equals sign and attempt to parse the remainder as a number
166+
// This assumes the origin server adheres to the RFC and sends the argument form.
167+
// TODO check for the quoted-string form
168+
maxAgeString := strings.TrimPrefix(directive, "max-age=")
169+
maxAge, err := strconv.ParseInt(maxAgeString, 10, 0)
170+
171+
if err != nil {
172+
return time.Duration(maxAge) * time.Second
173+
}
174+
}
175+
}
176+
177+
//Get the date from the response, if not set or invalid make the date the current time
178+
date := time.Now()
179+
if dateString := resp.Header.Get("Date"); dateString != "" {
180+
if parsedDate, err := http.ParseTime(dateString); err != nil {
181+
date = parsedDate
182+
}
183+
}
184+
185+
if expiresString := resp.Header.Get("Expires"); expiresString != "" {
186+
expires, err := http.ParseTime(expiresString)
187+
188+
//If date is invalid it should be assumed to be in the past, Section 5.3 of RFC 7234
189+
if err != nil {
190+
return -1
191+
}
192+
193+
return expires.Sub(date)
194+
}
195+
196+
//Use default values instead of caluclating heuristic freshness
197+
if ttl, found := config.StatusCodeDefaultExpirationTimes[resp.StatusCode]; found {
198+
return ttl
199+
}
200+
201+
return -1
202+
}
203+
204+
func RequestOrResponseHasNoCache(resp *http.Response) bool {
205+
206+
for _, directive := range strings.Split(strings.ToLower(resp.Header.Get("Cache-Control")), ",") {
207+
if strings.TrimSpace(directive) == "no-cache" {
208+
return true
209+
}
210+
}
211+
212+
for _, directive := range strings.Split(strings.ToLower(resp.Request.Header.Get("Cache-Control")), ",") {
213+
if strings.TrimSpace(directive) == "no-cache" {
214+
return true
215+
}
216+
}
217+
218+
//Section 5.4 of RFC 7234
219+
if resp.Request.Header.Get("Cache-Control") == "" && resp.Request.Header.Get("Pragma") == "no-cache" {
220+
return true
221+
}
222+
223+
return false
224+
}
225+
226+
//IsMethodSafe checks if a request method is safe
227+
func IsMethodSafe(config *CacheConfig, method string) bool {
228+
//Check if the request method is safe
229+
//TODO This comparason may be faster with a string search algorithm like Aho–Corasick
230+
for _, safeMethod := range config.SafeMethods {
231+
if safeMethod == method {
232+
return true
233+
}
234+
}
235+
236+
return false
237+
}
238+
239+
//IsMethodCacheable checks if a request method is cacheable
240+
func IsMethodCacheable(config *CacheConfig, method string) bool {
241+
242+
//Check if the request method is in the list of cacheable methods
243+
//TODO This comparason may be faster with a string search algorithm like Aho–Corasick
244+
for _, configMethod := range config.CacheableMethods {
245+
if configMethod == method {
246+
return true
247+
}
248+
}
249+
250+
return false
251+
}
252+
253+
//IsCacheableByExtension checks if a response is cacheable based on supported Cache-Control extensions
254+
// https://tools.ietf.org/html/rfc7234#section-5.2.3
255+
func IsResponseCacheableByExtension(config *CacheConfig, resp *http.Response) bool {
256+
//TODO find and implement cache extension
257+
return false
258+
}

cmd/.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
test

0 commit comments

Comments
 (0)