Skip to content

Commit bd94dfc

Browse files
authored
fix(multiple): december 2024 wave (#589)
* fix(multiple): december 2024 wave #584 * fix(now context): caddyserver/cache-handler#116 * fix(surrogate-key): unescape if the key contains % #583 * fix(rfc): Last-Modified handling #588 * feat(caddy): handle API directly using the caddy admin api, solves #585 * fix(ci): remove useless ttl check * fix(middleware): registering mapping eviction and run every minutes to purge IDX_ keys
1 parent 7649435 commit bd94dfc

File tree

6 files changed

+190
-11
lines changed

6 files changed

+190
-11
lines changed

Diff for: context/now.go

+10-2
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,16 @@ func (*nowContext) SetContextWithBaseRequest(req *http.Request, _ *http.Request)
1919
func (cc *nowContext) SetupContext(_ configurationtypes.AbstractConfigurationInterface) {}
2020

2121
func (cc *nowContext) SetContext(req *http.Request) *http.Request {
22-
now := time.Now().UTC()
23-
req.Header.Set("Date", now.Format(time.RFC1123))
22+
var now time.Time
23+
var e error
24+
25+
now, e = time.Parse(time.RFC1123, req.Header.Get("Date"))
26+
27+
if e != nil {
28+
now = time.Now().UTC()
29+
req.Header.Set("Date", now.Format(time.RFC1123))
30+
}
31+
2432
return req.WithContext(context.WithValue(req.Context(), Now, now))
2533
}
2634

Diff for: pkg/middleware/middleware.go

+28-2
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,19 @@ func reorderStorers(storers []types.Storer, expectedStorers []string) []types.St
5050
return newStorers
5151
}
5252

53+
func registerMappingKeysEviction(logger core.Logger, storers []types.Storer) {
54+
for _, storer := range storers {
55+
logger.Debugf("registering mapping eviction for storer %s", storer.Name())
56+
go func(current types.Storer) {
57+
for {
58+
logger.Debugf("run mapping eviction for storer %s", current.Name())
59+
current.MapKeys(core.MappingKeyPrefix)
60+
time.Sleep(time.Minute)
61+
}
62+
}(storer)
63+
}
64+
}
65+
5366
func NewHTTPCacheHandler(c configurationtypes.AbstractConfigurationInterface) *SouinBaseHandler {
5467
if c.GetLogger() == nil {
5568
var logLevel zapcore.Level
@@ -138,6 +151,8 @@ func NewHTTPCacheHandler(c configurationtypes.AbstractConfigurationInterface) *S
138151
}
139152
c.GetLogger().Info("Souin configuration is now loaded.")
140153

154+
registerMappingKeysEviction(c.GetLogger(), storers)
155+
141156
return &SouinBaseHandler{
142157
Configuration: c,
143158
Storers: storers,
@@ -556,6 +571,17 @@ func (s *SouinBaseHandler) Revalidate(validator *core.Revalidator, next handlerF
556571
return nil, errors.New("")
557572
}
558573

574+
if validator.IfModifiedSincePresent {
575+
if lastModified, err := time.Parse(time.RFC1123, customWriter.Header().Get("Last-Modified")); err == nil && validator.IfModifiedSince.Sub(lastModified) > 0 {
576+
customWriter.handleBuffer(func(b *bytes.Buffer) {
577+
b.Reset()
578+
})
579+
customWriter.Rw.WriteHeader(http.StatusNotModified)
580+
581+
return nil, errors.New("")
582+
}
583+
}
584+
559585
if statusCode != http.StatusNotModified {
560586
err = s.Store(customWriter, rq, requestCc, cachedKey, uri)
561587
}
@@ -922,10 +948,10 @@ func (s *SouinBaseHandler) ServeHTTP(rw http.ResponseWriter, rq *http.Request, n
922948
case <-req.Context().Done():
923949
switch req.Context().Err() {
924950
case baseCtx.DeadlineExceeded:
925-
customWriter.WriteHeader(http.StatusGatewayTimeout)
926-
s.Configuration.GetLogger().Infof("Internal server error on endpoint %s: %v", req.URL, s.Storers)
927951
rw.Header().Set("Cache-Status", cacheName+"; fwd=bypass; detail=DEADLINE-EXCEEDED")
952+
customWriter.Rw.WriteHeader(http.StatusGatewayTimeout)
928953
_, _ = customWriter.Rw.Write([]byte("Internal server error"))
954+
s.Configuration.GetLogger().Infof("Internal server error on endpoint %s: %v", req.URL, s.Storers)
929955
return baseCtx.DeadlineExceeded
930956
case baseCtx.Canceled:
931957
return baseCtx.Canceled

Diff for: pkg/surrogate/providers/common.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ func uniqueTag(values []string) []string {
8181
if _, found := tmp[item]; !found {
8282
tmp[item] = true
8383

84-
if strings.Contains(item, "%3B") || strings.Contains(item, "%3A") {
84+
if strings.Contains(item, "%") {
8585
item, _ = url.QueryUnescape(item)
8686
}
8787
list = append(list, item)
@@ -178,6 +178,7 @@ func (*baseStorage) candidateStore(tag string) bool {
178178

179179
func (*baseStorage) getOrderedSurrogateKeyHeadersCandidate() []string {
180180
return []string{
181+
cacheGroupKey,
181182
surrogateKey,
182183
edgeCacheTag,
183184
cacheTags,

Diff for: plugins/caddy/admin.go

+89
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package httpcache
2+
3+
import (
4+
"fmt"
5+
"github.com/caddyserver/caddy/v2"
6+
"github.com/darkweak/souin/configurationtypes"
7+
"github.com/darkweak/souin/pkg/api"
8+
"net/http"
9+
"strings"
10+
"time"
11+
12+
"github.com/darkweak/storages/core"
13+
)
14+
15+
func init() {
16+
caddy.RegisterModule(new(adminAPI))
17+
}
18+
19+
// adminAPI is a module that serves PKI endpoints to retrieve
20+
// information about the CAs being managed by Caddy.
21+
type adminAPI struct {
22+
ctx caddy.Context
23+
logger core.Logger
24+
app *SouinApp
25+
InternalEndpointHandlers *api.MapHandler
26+
}
27+
28+
// CaddyModule returns the Caddy module information.
29+
func (adminAPI) CaddyModule() caddy.ModuleInfo {
30+
return caddy.ModuleInfo{
31+
ID: "admin.api.souin",
32+
New: func() caddy.Module { return new(adminAPI) },
33+
}
34+
}
35+
36+
func (a *adminAPI) handleAPIEndpoints(writer http.ResponseWriter, request *http.Request) error {
37+
if a.InternalEndpointHandlers != nil {
38+
for k, handler := range *a.InternalEndpointHandlers.Handlers {
39+
if strings.Contains(request.RequestURI, k) {
40+
handler(writer, request)
41+
return nil
42+
}
43+
}
44+
}
45+
46+
return caddy.APIError{
47+
HTTPStatus: http.StatusNotFound,
48+
Err: fmt.Errorf("resource not found: %v", request.URL.Path),
49+
}
50+
}
51+
52+
// Provision sets up the adminAPI module.
53+
func (a *adminAPI) Provision(ctx caddy.Context) error {
54+
a.ctx = ctx
55+
a.logger = ctx.Logger(a).Sugar()
56+
57+
app, err := ctx.App(moduleName)
58+
if err != nil {
59+
return err
60+
}
61+
62+
a.app = app.(*SouinApp)
63+
config := Configuration{
64+
API: a.app.API,
65+
DefaultCache: DefaultCache{
66+
TTL: configurationtypes.Duration{
67+
Duration: 120 * time.Second,
68+
},
69+
},
70+
}
71+
a.InternalEndpointHandlers = api.GenerateHandlerMap(&config, a.app.Storers, a.app.SurrogateStorage)
72+
73+
return nil
74+
}
75+
76+
// Routes returns the admin routes for the PKI app.
77+
func (a *adminAPI) Routes() []caddy.AdminRoute {
78+
basepath := "/souin-api"
79+
if a.app != nil && a.app.API.BasePath != "" {
80+
basepath = a.app.API.BasePath
81+
}
82+
83+
return []caddy.AdminRoute{
84+
{
85+
Pattern: basepath + "/{params...}",
86+
Handler: caddy.AdminHandlerFunc(a.handleAPIEndpoints),
87+
},
88+
}
89+
}

Diff for: plugins/caddy/app.go

+2-6
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
package httpcache
22

33
import (
4-
"errors"
5-
64
"github.com/caddyserver/caddy/v2"
75
"github.com/darkweak/souin/configurationtypes"
86
"github.com/darkweak/souin/pkg/storage/types"
@@ -15,7 +13,7 @@ type SouinApp struct {
1513
DefaultCache
1614
// The provider to use.
1715
Storers []types.Storer
18-
// Surrogate storage to support th econfiguration reload without surrogate-key data loss.
16+
// Surrogate storage to support the configuration reload without surrogate-key data loss.
1917
SurrogateStorage providers.SurrogateInterface
2018
// Cache-key tweaking.
2119
CacheKeys configurationtypes.CacheKeys `json:"cache_keys,omitempty"`
@@ -39,9 +37,7 @@ func (s SouinApp) Start() error {
3937
core.ResetRegisteredStorages()
4038
_, _ = up.Delete(stored_providers_key)
4139
_, _ = up.LoadOrStore(stored_providers_key, newStorageProvider())
42-
if s.DefaultCache.GetTTL() == 0 {
43-
return errors.New("Invalid/Incomplete default cache declaration")
44-
}
40+
4541
return nil
4642
}
4743

Diff for: plugins/caddy/httpcache_test.go

+59
Original file line numberDiff line numberDiff line change
@@ -1281,3 +1281,62 @@ func TestAllowedAdditionalStatusCode(t *testing.T) {
12811281
t.Error("Age header should be present")
12821282
}
12831283
}
1284+
1285+
type testTimeoutHandler struct {
1286+
iterator int
1287+
}
1288+
1289+
func (t *testTimeoutHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
1290+
t.iterator++
1291+
if t.iterator%2 == 0 {
1292+
time.Sleep(5 * time.Second)
1293+
1294+
return
1295+
}
1296+
1297+
w.WriteHeader(http.StatusOK)
1298+
_, _ = w.Write([]byte("Hello timeout!"))
1299+
}
1300+
1301+
func TestTimeout(t *testing.T) {
1302+
tester := caddytest.NewTester(t)
1303+
tester.InitServer(`
1304+
{
1305+
admin localhost:2999
1306+
http_port 9080
1307+
cache {
1308+
ttl 1ns
1309+
stale 1ns
1310+
timeout {
1311+
backend 1s
1312+
}
1313+
}
1314+
}
1315+
localhost:9080 {
1316+
route /cache-timeout {
1317+
cache
1318+
reverse_proxy localhost:9086
1319+
}
1320+
}`, "caddyfile")
1321+
1322+
go func() {
1323+
errorHandler := testTimeoutHandler{}
1324+
_ = http.ListenAndServe(":9086", &errorHandler)
1325+
}()
1326+
time.Sleep(time.Second)
1327+
resp1, _ := tester.AssertGetResponse(`http://localhost:9080/cache-timeout`, http.StatusOK, "Hello timeout!")
1328+
time.Sleep(time.Millisecond)
1329+
resp2, _ := tester.AssertGetResponse(`http://localhost:9080/cache-timeout`, http.StatusGatewayTimeout, "Internal server error")
1330+
1331+
if resp1.Header.Get("Cache-Status") != "Souin; fwd=uri-miss; stored; key=GET-http-localhost:9080-/cache-timeout" {
1332+
t.Errorf("unexpected resp1 Cache-Status header %v", resp1.Header.Get("Cache-Status"))
1333+
}
1334+
1335+
if resp1.Header.Get("Age") != "" {
1336+
t.Errorf("unexpected resp1 Age header %v", resp1.Header.Get("Age"))
1337+
}
1338+
1339+
if resp2.Header.Get("Cache-Status") != "Souin; fwd=bypass; detail=DEADLINE-EXCEEDED" {
1340+
t.Errorf("unexpected resp2 Cache-Status header %v", resp2.Header.Get("Cache-Status"))
1341+
}
1342+
}

0 commit comments

Comments
 (0)