diff --git a/.github/workflows/non-regression.yml b/.github/workflows/non-regression.yml index 0c5e7ae40..fb6f0102d 100644 --- a/.github/workflows/non-regression.yml +++ b/.github/workflows/non-regression.yml @@ -19,7 +19,7 @@ jobs: with: go-version: ${{ env.GO_VERSION }} - name: golangci-lint - uses: golangci/golangci-lint-action@v3 + uses: golangci/golangci-lint-action@v6 with: args: --timeout=240s unit-test-golang: diff --git a/.github/workflows/plugin_template.yml b/.github/workflows/plugin_template.yml index 45b45e14f..be9cd2140 100644 --- a/.github/workflows/plugin_template.yml +++ b/.github/workflows/plugin_template.yml @@ -34,10 +34,11 @@ jobs: uses: actions/checkout@v4 - name: golangci-lint - uses: golangci/golangci-lint-action@v3 + uses: golangci/golangci-lint-action@v6 with: + version: v1.59.1 working-directory: plugins/${{ inputs.LOWER_NAME }} - args: --skip-dirs=override --timeout=240s + args: --exclude-dirs=override --timeout=240s - name: Run ${{ inputs.CAPITALIZED_NAME }} tests run: cd plugins/${{ inputs.LOWER_NAME }} && go test -v . diff --git a/Makefile b/Makefile index 42a5a95ff..1e0183029 100644 --- a/Makefile +++ b/Makefile @@ -144,9 +144,9 @@ generate-workflow: ## Generate plugin workflow bash .github/workflows/workflow_plugins_generator.sh golangci-lint: ## Run golangci-lint to ensure the code quality - docker run --rm -v $(PWD):/app -w /app golangci/golangci-lint:v1.57.2 golangci-lint run -v --timeout 180s ./... + docker run --rm -v $(PWD):/app -w /app golangci/golangci-lint:v1.59.1 golangci-lint run -v --timeout 180s ./... for plugin in $(PLUGINS_LIST) ; do \ - echo "Starting lint $$plugin \n" && docker run --rm -v $(PWD):/app -w /app golangci/golangci-lint:v1.57.2 golangci-lint run -v --skip-dirs=override --timeout 240s ./plugins/$$plugin; \ + echo "Starting lint $$plugin \n" && docker run --rm -v $(PWD):/app -w /app golangci/golangci-lint:v1.59.1 golangci-lint run -v --exclude-dirs=override --timeout 240s ./plugins/$$plugin; \ done cd plugins/caddy && go mod tidy && go mod download diff --git a/docs/website/content/docs/middlewares/roadrunner.md b/docs/website/content/docs/middlewares/roadrunner.md index 630873541..b4fc2c8f9 100644 --- a/docs/website/content/docs/middlewares/roadrunner.md +++ b/docs/website/content/docs/middlewares/roadrunner.md @@ -7,7 +7,7 @@ tags = ["Beginners", "Advanced"] +++ ## Build the roadrunner binary -First you need to build your roadrunner instance with the cache dependency. You should use [velox](github.com/roadrunner-server/velox) for that. +First you need to build your roadrunner instance with the cache dependency. You should use [velox](https://github.com/roadrunner-server/velox) for that. Define a `configuration.toml` file to tell velox what and how it must build. ```toml diff --git a/pkg/api/souin.go b/pkg/api/souin.go index 0f11e2e09..c57393d4d 100644 --- a/pkg/api/souin.go +++ b/pkg/api/souin.go @@ -78,16 +78,25 @@ func (s *SouinAPI) BulkDelete(key string, purge bool) { } } - if purge { - current.Delete(core.MappingKeyPrefix + key) - } else { + if !purge { newFreshTime := time.Now() for k, v := range mapping.Mapping { v.FreshTime = timestamppb.New(newFreshTime) mapping.Mapping[k] = v } + + v, e := proto.Marshal(&mapping) + if e != nil { + fmt.Println("Impossible to re-encode the mapping", core.MappingKeyPrefix+key) + current.Delete(core.MappingKeyPrefix + key) + } + _ = current.Set(core.MappingKeyPrefix+key, v, storageToInfiniteTTLMap[current.Name()]) } } + + if purge { + current.Delete(core.MappingKeyPrefix + key) + } } s.Delete(key) @@ -96,13 +105,8 @@ func (s *SouinAPI) BulkDelete(key string, purge bool) { // Delete will delete a record into the provider cache system and will update the Souin API if enabled // The key can be a regexp to delete multiple items func (s *SouinAPI) Delete(key string) { - _, err := regexp.Compile(key) for _, current := range s.storers { - if err != nil { - current.DeleteMany(key) - } else { - current.Delete(key) - } + current.Delete(key) } } @@ -144,6 +148,7 @@ func (s *SouinAPI) listKeys(search string) []string { var storageToInfiniteTTLMap = map[string]time.Duration{ "BADGER": types.OneYearDuration, "ETCD": types.OneYearDuration, + "GO-REDIS": 0, "NUTS": 0, "OLRIC": types.OneYearDuration, "OTTER": types.OneYearDuration, diff --git a/pkg/middleware/middleware.go b/pkg/middleware/middleware.go index 840830448..147ef946e 100644 --- a/pkg/middleware/middleware.go +++ b/pkg/middleware/middleware.go @@ -190,6 +190,7 @@ func (s *SouinBaseHandler) Store( rq *http.Request, requestCc *cacheobject.RequestCacheDirectives, cachedKey string, + uri string, ) error { statusCode := customWriter.GetStatusCode() if !isCacheableCode(statusCode) { @@ -237,11 +238,30 @@ func (s *SouinBaseHandler) Store( } } + hasFreshness := false ma := currentMatchedURL.TTL.Duration if responseCc.SMaxAge >= 0 { ma = time.Duration(responseCc.SMaxAge) * time.Second } else if responseCc.MaxAge >= 0 { ma = time.Duration(responseCc.MaxAge) * time.Second + } else if customWriter.Header().Get("Expires") != "" { + exp, err := time.Parse(time.RFC1123, customWriter.Header().Get("Expires")) + if err != nil { + return nil + } + + duration := time.Until(exp) + if duration <= 0 || duration > 10*types.OneYearDuration { + return nil + } + + date, _ := time.Parse(time.RFC1123, customWriter.Header().Get("Date")) + if date.Sub(exp) > 0 { + return nil + } + + ma = duration + hasFreshness = true } now := rq.Context().Value(context.Now).(time.Time) @@ -249,16 +269,9 @@ func (s *SouinBaseHandler) Store( customWriter.Header().Set(rfc.StoredTTLHeader, ma.String()) ma = ma - time.Since(date) - if exp := customWriter.Header().Get("Expires"); exp != "" { - delta, _ := time.Parse(exp, time.RFC1123) - if sub := delta.Sub(now); sub > 0 { - ma = sub - } - } - status := fmt.Sprintf("%s; fwd=uri-miss", rq.Context().Value(context.CacheName)) if (modeContext.Bypass_request || !requestCc.NoStore) && - (modeContext.Bypass_response || !responseCc.NoStore) { + (modeContext.Bypass_response || !responseCc.NoStore || hasFreshness) { headers := customWriter.Header().Clone() for hname, shouldDelete := range responseCc.NoCache { if shouldDelete { @@ -329,6 +342,7 @@ func (s *SouinBaseHandler) Store( variedKey, ) == nil { s.Configuration.GetLogger().Debugf("Stored the key %s in the %s provider", variedKey, currentStorer.Name()) + res.Request = rq } else { mu.Lock() fails = append(fails, fmt.Sprintf("; detail=%s-INSERTION-ERROR", currentStorer.Name())) @@ -339,9 +353,9 @@ func (s *SouinBaseHandler) Store( wg.Wait() if len(fails) < s.storersLen { - go func(rs http.Response, key string) { - _ = s.SurrogateKeyStorer.Store(&rs, key) - }(res, variedKey) + go func(rs http.Response, key string, basekey string) { + _ = s.SurrogateKeyStorer.Store(&rs, key, uri, basekey) + }(res, variedKey, cachedKey) status += "; stored" } @@ -375,6 +389,7 @@ func (s *SouinBaseHandler) Upstream( next handlerFunc, requestCc *cacheobject.RequestCacheDirectives, cachedKey string, + uri string, ) error { s.Configuration.GetLogger().Debug("Request the upstream server") prometheus.Increment(prometheus.RequestCounter) @@ -422,7 +437,7 @@ func (s *SouinBaseHandler) Upstream( customWriter.Header().Set(headerName, s.DefaultMatchedUrl.DefaultCacheControl) } - err := s.Store(customWriter, rq, requestCc, cachedKey) + err := s.Store(customWriter, rq, requestCc, cachedKey, uri) defer customWriter.Buf.Reset() return singleflightValue{ @@ -446,7 +461,7 @@ func (s *SouinBaseHandler) Upstream( for _, vh := range variedHeaders { if rq.Header.Get(vh) != sfWriter.requestHeaders.Get(vh) { // cachedKey += rfc.GetVariedCacheKey(rq, variedHeaders) - return s.Upstream(customWriter, rq, next, requestCc, cachedKey) + return s.Upstream(customWriter, rq, next, requestCc, cachedKey, uri) } } } @@ -462,7 +477,7 @@ func (s *SouinBaseHandler) Upstream( return nil } -func (s *SouinBaseHandler) Revalidate(validator *core.Revalidator, next handlerFunc, customWriter *CustomWriter, rq *http.Request, requestCc *cacheobject.RequestCacheDirectives, cachedKey string) error { +func (s *SouinBaseHandler) Revalidate(validator *core.Revalidator, next handlerFunc, customWriter *CustomWriter, rq *http.Request, requestCc *cacheobject.RequestCacheDirectives, cachedKey string, uri string) error { s.Configuration.GetLogger().Debug("Revalidate the request with the upstream server") prometheus.Increment(prometheus.RequestRevalidationCounter) @@ -484,7 +499,7 @@ func (s *SouinBaseHandler) Revalidate(validator *core.Revalidator, next handlerF } if statusCode != http.StatusNotModified { - err = s.Store(customWriter, rq, requestCc, cachedKey) + err = s.Store(customWriter, rq, requestCc, cachedKey, uri) } } @@ -604,6 +619,8 @@ func (s *SouinBaseHandler) ServeHTTP(rw http.ResponseWriter, rq *http.Request, n } cachedKey := req.Context().Value(context.Key).(string) + // Need to copy URL path before calling next because it can alter the URI + uri := req.URL.Path bufPool := s.bufPool.Get().(*bytes.Buffer) bufPool.Reset() defer s.bufPool.Put(bufPool) @@ -618,6 +635,7 @@ func (s *SouinBaseHandler) ServeHTTP(rw http.ResponseWriter, rq *http.Request, n if modeContext.Bypass_request || !requestCc.NoCache { validator := rfc.ParseRequest(req) var fresh, stale *http.Response + var storerName string finalKey := cachedKey if req.Context().Value(context.Hashed).(bool) { finalKey = fmt.Sprint(xxhash.Sum64String(finalKey)) @@ -626,7 +644,8 @@ func (s *SouinBaseHandler) ServeHTTP(rw http.ResponseWriter, rq *http.Request, n fresh, stale = currentStorer.GetMultiLevel(finalKey, req, validator) if fresh != nil || stale != nil { - s.Configuration.GetLogger().Debugf("Found at least one valid response in the %s storage", currentStorer.Name()) + storerName = currentStorer.Name() + s.Configuration.GetLogger().Debugf("Found at least one valid response in the %s storage", storerName) break } } @@ -635,7 +654,7 @@ func (s *SouinBaseHandler) ServeHTTP(rw http.ResponseWriter, rq *http.Request, n if fresh != nil && (!modeContext.Strict || rfc.ValidateCacheControl(fresh, requestCc)) { response := fresh if validator.ResponseETag != "" && validator.Matched { - rfc.SetCacheStatusHeader(response) + rfc.SetCacheStatusHeader(response, storerName) for h, v := range response.Header { customWriter.Header()[h] = v } @@ -655,19 +674,19 @@ func (s *SouinBaseHandler) ServeHTTP(rw http.ResponseWriter, rq *http.Request, n } if validator.NeedRevalidation { - err := s.Revalidate(validator, next, customWriter, req, requestCc, cachedKey) + err := s.Revalidate(validator, next, customWriter, req, requestCc, cachedKey, uri) _, _ = customWriter.Send() return err } if resCc, _ := cacheobject.ParseResponseCacheControl(rfc.HeaderAllCommaSepValuesString(response.Header, headerName)); resCc.NoCachePresent { prometheus.Increment(prometheus.NoCachedResponseCounter) - err := s.Revalidate(validator, next, customWriter, req, requestCc, cachedKey) + err := s.Revalidate(validator, next, customWriter, req, requestCc, cachedKey, uri) _, _ = customWriter.Send() return err } - rfc.SetCacheStatusHeader(response) + rfc.SetCacheStatusHeader(response, storerName) if !modeContext.Strict || rfc.ValidateMaxAgeCachedResponse(requestCc, response) != nil { for h, v := range response.Header { customWriter.Header()[h] = v @@ -685,7 +704,7 @@ func (s *SouinBaseHandler) ServeHTTP(rw http.ResponseWriter, rq *http.Request, n if nil != response && (!modeContext.Strict || rfc.ValidateCacheControl(response, requestCc)) { addTime, _ := time.ParseDuration(response.Header.Get(rfc.StoredTTLHeader)) - rfc.SetCacheStatusHeader(response) + rfc.SetCacheStatusHeader(response, storerName) responseCc, _ := cacheobject.ParseResponseCacheControl(rfc.HeaderAllCommaSepValuesString(response.Header, "Cache-Control")) if responseCc.StaleWhileRevalidate > 0 { @@ -697,9 +716,9 @@ func (s *SouinBaseHandler) ServeHTTP(rw http.ResponseWriter, rq *http.Request, n _, _ = io.Copy(customWriter.Buf, response.Body) _, err := customWriter.Send() customWriter = NewCustomWriter(req, rw, bufPool) - go func(v *core.Revalidator, goCw *CustomWriter, goRq *http.Request, goNext func(http.ResponseWriter, *http.Request) error, goCc *cacheobject.RequestCacheDirectives, goCk string) { - _ = s.Revalidate(v, goNext, goCw, goRq, goCc, goCk) - }(validator, customWriter, req, next, requestCc, cachedKey) + go func(v *core.Revalidator, goCw *CustomWriter, goRq *http.Request, goNext func(http.ResponseWriter, *http.Request) error, goCc *cacheobject.RequestCacheDirectives, goCk string, goUri string) { + _ = s.Revalidate(v, goNext, goCw, goRq, goCc, goCk, goUri) + }(validator, customWriter, req, next, requestCc, cachedKey, uri) buf := s.bufPool.Get().(*bytes.Buffer) buf.Reset() defer s.bufPool.Put(buf) @@ -709,7 +728,7 @@ func (s *SouinBaseHandler) ServeHTTP(rw http.ResponseWriter, rq *http.Request, n if responseCc.MustRevalidate || responseCc.NoCachePresent || validator.NeedRevalidation { req.Header["If-None-Match"] = append(req.Header["If-None-Match"], validator.ResponseETag) - err := s.Revalidate(validator, next, customWriter, req, requestCc, cachedKey) + err := s.Revalidate(validator, next, customWriter, req, requestCc, cachedKey, uri) statusCode := customWriter.GetStatusCode() if err != nil { if responseCc.StaleIfError > -1 || requestCc.StaleIfError > 0 { @@ -732,7 +751,7 @@ func (s *SouinBaseHandler) ServeHTTP(rw http.ResponseWriter, rq *http.Request, n if statusCode == http.StatusNotModified { if !validator.Matched { - rfc.SetCacheStatusHeader(response) + rfc.SetCacheStatusHeader(response, storerName) customWriter.WriteHeader(response.StatusCode) maps.Copy(customWriter.Header(), response.Header) _, _ = io.Copy(customWriter.Buf, response.Body) @@ -771,7 +790,7 @@ func (s *SouinBaseHandler) ServeHTTP(rw http.ResponseWriter, rq *http.Request, n errorCacheCh := make(chan error) go func(vr *http.Request, cw *CustomWriter) { prometheus.Increment(prometheus.NoCachedResponseCounter) - errorCacheCh <- s.Upstream(cw, vr, next, requestCc, cachedKey) + errorCacheCh <- s.Upstream(cw, vr, next, requestCc, cachedKey, uri) }(req, customWriter) select { diff --git a/pkg/middleware/writer.go b/pkg/middleware/writer.go index d02546b17..32a5d0346 100644 --- a/pkg/middleware/writer.go +++ b/pkg/middleware/writer.go @@ -43,9 +43,11 @@ type CustomWriter struct { func (r *CustomWriter) Header() http.Header { r.mutex.Lock() defer r.mutex.Unlock() - if r.headersSent { + + if r.headersSent || r.Req.Context().Err() != nil { return http.Header{} } + return r.Rw.Header() } diff --git a/pkg/rfc/cache_status.go b/pkg/rfc/cache_status.go index 3d5a61b48..f02132fb7 100644 --- a/pkg/rfc/cache_status.go +++ b/pkg/rfc/cache_status.go @@ -80,7 +80,7 @@ func HitStaleCache(h *http.Header) { h.Set("Cache-Status", h.Get("Cache-Status")+"; fwd=stale") } -func manageAge(h *http.Header, ttl time.Duration, cacheName, key string) { +func manageAge(h *http.Header, ttl time.Duration, cacheName, key, storerName string) { utc1 := time.Now().UTC() dh := h.Get("Date") if dh == "" { @@ -119,7 +119,7 @@ func manageAge(h *http.Header, ttl time.Duration, cacheName, key string) { age := strconv.Itoa(oldAge + cage) h.Set("Age", age) ttlValue := strconv.Itoa(int(ttl.Seconds()) - cage) - h.Set("Cache-Status", cacheName+"; hit; ttl="+ttlValue+"; key="+key) + h.Set("Cache-Status", cacheName+"; hit; ttl="+ttlValue+"; key="+key+"; detail="+storerName) } func setMalformedHeader(headers *http.Header, header, cacheName string) { @@ -127,11 +127,11 @@ func setMalformedHeader(headers *http.Header, header, cacheName string) { } // SetCacheStatusHeader set the Cache-Status header -func SetCacheStatusHeader(resp *http.Response) *http.Response { +func SetCacheStatusHeader(resp *http.Response, storerName string) *http.Response { h := resp.Header cacheName := resp.Request.Context().Value(context.CacheName).(string) validateEmptyHeaders(&h, cacheName) - manageAge(&h, 0, cacheName, GetCacheKeyFromCtx(resp.Request.Context())) + manageAge(&h, 0, cacheName, GetCacheKeyFromCtx(resp.Request.Context()), storerName) resp.Header = h return resp diff --git a/pkg/surrogate/providers/akamai.go b/pkg/surrogate/providers/akamai.go index 2e35a9564..d33455b79 100644 --- a/pkg/surrogate/providers/akamai.go +++ b/pkg/surrogate/providers/akamai.go @@ -39,12 +39,12 @@ func (*AkamaiSurrogateStorage) getHeaderSeparator() string { } // Store stores the response tags located in the first non empty supported header -func (a *AkamaiSurrogateStorage) Store(response *http.Response, cacheKey string) error { +func (a *AkamaiSurrogateStorage) Store(response *http.Response, cacheKey, uri, basekey string) error { defer func() { response.Header.Del(surrogateKey) response.Header.Del(surrogateControl) }() - e := a.baseStorage.Store(response, cacheKey) + e := a.baseStorage.Store(response, cacheKey, uri, basekey) response.Header.Set(edgeCacheTag, response.Header.Get(surrogateKey)) return e diff --git a/pkg/surrogate/providers/cloudflare.go b/pkg/surrogate/providers/cloudflare.go index f131ac900..d733347b8 100644 --- a/pkg/surrogate/providers/cloudflare.go +++ b/pkg/surrogate/providers/cloudflare.go @@ -38,12 +38,12 @@ func (*CloudflareSurrogateStorage) getHeaderSeparator() string { } // Store stores the response tags located in the first non empty supported header -func (c *CloudflareSurrogateStorage) Store(response *http.Response, cacheKey string) error { +func (c *CloudflareSurrogateStorage) Store(response *http.Response, cacheKey, uri, basekey string) error { defer func() { response.Header.Del(surrogateKey) response.Header.Del(surrogateControl) }() - e := c.baseStorage.Store(response, cacheKey) + e := c.baseStorage.Store(response, cacheKey, uri, basekey) response.Header.Set(cacheTag, strings.Join(c.ParseHeaders(response.Header.Get(surrogateKey)), c.getHeaderSeparator())) return e diff --git a/pkg/surrogate/providers/common.go b/pkg/surrogate/providers/common.go index dc5786721..aaf88b610 100644 --- a/pkg/surrogate/providers/common.go +++ b/pkg/surrogate/providers/common.go @@ -41,7 +41,7 @@ var storageToInfiniteTTLMap = map[string]time.Duration{ } func (s *baseStorage) ParseHeaders(value string) []string { - return regexp.MustCompile(s.parent.getHeaderSeparator()+" *").Split(value, -1) + return strings.Split(value, s.parent.getHeaderSeparator()) } func getCandidateHeader(header http.Header, getCandidates func() []string) (string, string) { @@ -207,7 +207,7 @@ func (s *baseStorage) purgeTag(tag string) []string { } // Store will take the lead to store the cache key for each provided Surrogate-key -func (s *baseStorage) Store(response *http.Response, cacheKey string) error { +func (s *baseStorage) Store(response *http.Response, cacheKey, uri, basekey string) error { h := response.Header cacheKey = url.QueryEscape(cacheKey) @@ -226,6 +226,8 @@ func (s *baseStorage) Store(response *http.Response, cacheKey string) error { for _, control := range controls { if s.parent.candidateStore(control) { s.storeTag(key, cacheKey, urlRegexp) + + break } } } else { @@ -233,6 +235,9 @@ func (s *baseStorage) Store(response *http.Response, cacheKey string) error { } } + urlRegexp = regexp.MustCompile("(^|" + regexp.QuoteMeta(souinStorageSeparator) + ")" + regexp.QuoteMeta(basekey) + "(" + regexp.QuoteMeta(souinStorageSeparator) + "|$)") + s.storeTag(uri, basekey, urlRegexp) + return nil } diff --git a/pkg/surrogate/providers/common_test.go b/pkg/surrogate/providers/common_test.go index 0d0d3c97b..06fb90d66 100644 --- a/pkg/surrogate/providers/common_test.go +++ b/pkg/surrogate/providers/common_test.go @@ -106,7 +106,7 @@ func TestBaseStorage_Store(t *testing.T) { bs := mockCommonProvider() - e := bs.Store(&res, "((((invalid_key_but_escaped") + e := bs.Store(&res, "((((invalid_key_but_escaped", "", "") if e != nil { t.Error("It shouldn't throw an error with a valid key.") } @@ -116,7 +116,7 @@ func TestBaseStorage_Store(t *testing.T) { _ = bs.Storage.Set("test5", []byte("first,second,fifth"), storageToInfiniteTTLMap[bs.Storage.Name()]) _ = bs.Storage.Set("testInvalid", []byte("invalid"), storageToInfiniteTTLMap[bs.Storage.Name()]) - if e = bs.Store(&res, "stored"); e != nil { + if e = bs.Store(&res, "stored", "", ""); e != nil { t.Error("It shouldn't throw an error with a valid key.") } @@ -133,10 +133,10 @@ func TestBaseStorage_Store(t *testing.T) { } res.Header.Set(surrogateKey, "something") - _ = bs.Store(&res, "/something") - _ = bs.Store(&res, "/something") + _ = bs.Store(&res, "/something", "", "") + _ = bs.Store(&res, "/something", "", "") res.Header.Set(surrogateKey, "something") - _ = bs.Store(&res, "/some") + _ = bs.Store(&res, "/some", "", "") _ = len(bs.Storage.MapKeys(surrogatePrefix)) // if storageSize != 6 { @@ -161,7 +161,7 @@ func TestBaseStorage_Store_Load(t *testing.T) { wg.Add(1) go func(r http.Response, iteration int, group *sync.WaitGroup) { defer wg.Done() - _ = bs.Store(&r, fmt.Sprintf("my_dynamic_cache_key_%d", iteration)) + _ = bs.Store(&r, fmt.Sprintf("my_dynamic_cache_key_%d", iteration), "", "") }(res, i, &wg) } diff --git a/pkg/surrogate/providers/types.go b/pkg/surrogate/providers/types.go index 31c8e4666..999fe90ff 100644 --- a/pkg/surrogate/providers/types.go +++ b/pkg/surrogate/providers/types.go @@ -16,7 +16,7 @@ type SurrogateInterface interface { Purge(http.Header) (cacheKeys []string, surrogateKeys []string) Invalidate(method string, h http.Header) purgeTag(string) []string - Store(*http.Response, string) error + Store(*http.Response, string, string, string) error storeTag(string, string, *regexp.Regexp) ParseHeaders(string) []string List() map[string]string diff --git a/plugins/beego/souin_test.go b/plugins/beego/souin_test.go index 9a8445f62..5ada1ae60 100644 --- a/plugins/beego/souin_test.go +++ b/plugins/beego/souin_test.go @@ -62,7 +62,7 @@ func Test_SouinBeegoPlugin_Middleware(t *testing.T) { } web.BeeApp.Handlers.ServeHTTP(res2, req) - if res2.Result().Header.Get("Cache-Status") != "Souin; hit; ttl=4; key=GET-http-example.com-/handled" { + if res2.Result().Header.Get("Cache-Status") != "Souin; hit; ttl=4; key=GET-http-example.com-/handled; detail=DEFAULT" { t.Error("The response must contain a Cache-Status header with the hit and ttl directives.") } if res2.Result().Header.Get("Age") != "1" { diff --git a/plugins/caddy/httpcache_test.go b/plugins/caddy/httpcache_test.go index 2e1a972a0..d7258c014 100644 --- a/plugins/caddy/httpcache_test.go +++ b/plugins/caddy/httpcache_test.go @@ -4,8 +4,6 @@ import ( "context" "fmt" "net/http" - "os" - "path" "strings" "sync" "testing" @@ -36,13 +34,13 @@ func TestMinimal(t *testing.T) { } resp2, _ := tester.AssertGetResponse(`http://localhost:9080/cache-default`, 200, "Hello, default!") - if resp2.Header.Get("Cache-Status") != "Souin; hit; ttl=119; key=GET-http-localhost:9080-/cache-default" { + if resp2.Header.Get("Cache-Status") != "Souin; hit; ttl=119; key=GET-http-localhost:9080-/cache-default; detail=DEFAULT" { t.Errorf("unexpected Cache-Status header %v", resp2.Header.Get("Cache-Status")) } time.Sleep(2 * time.Second) resp3, _ := tester.AssertGetResponse(`http://localhost:9080/cache-default`, 200, "Hello, default!") - if resp3.Header.Get("Cache-Status") != "Souin; hit; ttl=117; key=GET-http-localhost:9080-/cache-default" { + if resp3.Header.Get("Cache-Status") != "Souin; hit; ttl=117; key=GET-http-localhost:9080-/cache-default; detail=DEFAULT" { t.Errorf("unexpected Cache-Status header %v", resp3.Header.Get("Cache-Status")) } } @@ -73,7 +71,7 @@ func TestHead(t *testing.T) { } resp2, _ := tester.AssertResponse(headReq, 200, "") - if resp2.Header.Get("Cache-Status") != "Souin; hit; ttl=119; key=HEAD-http-localhost:9080-/cache-head" { + if resp2.Header.Get("Cache-Status") != "Souin; hit; ttl=119; key=HEAD-http-localhost:9080-/cache-head; detail=DEFAULT" { t.Errorf("unexpected Cache-Status header %v", resp2.Header) } if resp2.Header.Get("Content-Length") != "12" { @@ -134,13 +132,13 @@ func TestMaxAge(t *testing.T) { } resp2, _ := tester.AssertGetResponse(`http://localhost:9080/cache-max-age`, 200, "Hello, max-age!") - if resp2.Header.Get("Cache-Status") != "Souin; hit; ttl=59; key=GET-http-localhost:9080-/cache-max-age" { + if resp2.Header.Get("Cache-Status") != "Souin; hit; ttl=59; key=GET-http-localhost:9080-/cache-max-age; detail=DEFAULT" { t.Errorf("unexpected Cache-Status header %v", resp2.Header.Get("Cache-Status")) } time.Sleep(2 * time.Second) resp3, _ := tester.AssertGetResponse(`http://localhost:9080/cache-max-age`, 200, "Hello, max-age!") - if resp3.Header.Get("Cache-Status") != "Souin; hit; ttl=57; key=GET-http-localhost:9080-/cache-max-age" { + if resp3.Header.Get("Cache-Status") != "Souin; hit; ttl=57; key=GET-http-localhost:9080-/cache-max-age; detail=DEFAULT" { t.Errorf("unexpected Cache-Status header %v", resp3.Header.Get("Cache-Status")) } } @@ -172,7 +170,7 @@ func TestMaxStale(t *testing.T) { } resp2, _ := tester.AssertGetResponse(maxStaleURL, 200, "Hello, max-stale!") - if resp2.Header.Get("Cache-Status") != "Souin; hit; ttl=2; key=GET-http-localhost:9080-/cache-max-stale" { + if resp2.Header.Get("Cache-Status") != "Souin; hit; ttl=2; key=GET-http-localhost:9080-/cache-max-stale; detail=DEFAULT" { t.Errorf("unexpected Cache-Status header %v", resp2.Header.Get("Cache-Status")) } @@ -180,7 +178,7 @@ func TestMaxStale(t *testing.T) { reqMaxStale, _ := http.NewRequest(http.MethodGet, maxStaleURL, nil) reqMaxStale.Header = http.Header{"Cache-Control": []string{"max-stale=3"}} resp3, _ := tester.AssertResponse(reqMaxStale, 200, "Hello, max-stale!") - if resp3.Header.Get("Cache-Status") != "Souin; hit; ttl=-1; key=GET-http-localhost:9080-/cache-max-stale; fwd=stale" { + if resp3.Header.Get("Cache-Status") != "Souin; hit; ttl=-1; key=GET-http-localhost:9080-/cache-max-stale; detail=DEFAULT; fwd=stale" { t.Errorf("unexpected Cache-Status header %v", resp3.Header.Get("Cache-Status")) } @@ -216,7 +214,7 @@ func TestSMaxAge(t *testing.T) { } resp2, _ := tester.AssertGetResponse(`http://localhost:9080/cache-s-maxage`, 200, "Hello, s-maxage!") - if resp2.Header.Get("Cache-Status") != "Souin; hit; ttl=4; key=GET-http-localhost:9080-/cache-s-maxage" { + if resp2.Header.Get("Cache-Status") != "Souin; hit; ttl=4; key=GET-http-localhost:9080-/cache-s-maxage; detail=DEFAULT" { t.Errorf("unexpected Cache-Status header with %v", resp2.Header.Get("Cache-Status")) } } @@ -403,7 +401,7 @@ func TestMaxBodyByte(t *testing.T) { t.Errorf("unexpected Age header %v", respStored1.Header.Get("Age")) } - if respStored2.Header.Get("Cache-Status") != "Souin; hit; ttl=4; key=GET-http-localhost:9080-/max-body-bytes-stored" { + if respStored2.Header.Get("Cache-Status") != "Souin; hit; ttl=4; key=GET-http-localhost:9080-/max-body-bytes-stored; detail=DEFAULT" { t.Errorf("unexpected Cache-Status header value %v", respStored2.Header.Get("Cache-Status")) } if respStored2.Header.Get("Age") == "" { @@ -427,83 +425,6 @@ func TestMaxBodyByte(t *testing.T) { } } -func TestMultiProvider(t *testing.T) { - var wg sync.WaitGroup - var responses []*http.Response - - for i := 0; i < 3; i++ { - wg.Add(1) - - go func(tt *testing.T) { - tester := caddytest.NewTester(t) - tester.InitServer(` - { - admin localhost:2999 - http_port 9080 - https_port 9443 - cache { - nuts { - path ./souin-nuts - } - ttl 1000s - storers badger nuts - } - } - localhost:9080 { - route /multi-storage { - cache - respond "Hello, multi-storage!" - } - }`, "caddyfile") - - resp, _ := tester.AssertGetResponse("http://localhost:9080/multi-storage", 200, "Hello, multi-storage!") - responses = append(responses, resp) - resp, _ = tester.AssertGetResponse("http://localhost:9080/multi-storage", 200, "Hello, multi-storage!") - responses = append(responses, resp) - wg.Done() - }(t) - - wg.Wait() - time.Sleep(time.Second) - } - - resp1 := responses[0] - if resp1.Header.Get("Cache-Status") != "Souin; fwd=uri-miss; stored; key=GET-http-localhost:9080-/multi-storage" { - t.Errorf("unexpected resp1 Cache-Status header %v", resp1.Header.Get("Cache-Status")) - } - if resp1.Header.Get("Age") != "" { - t.Errorf("unexpected resp1 Age header %v", resp1.Header.Get("Age")) - } - resp1 = responses[1] - if resp1.Header.Get("Cache-Status") != "Souin; hit; ttl=999; key=GET-http-localhost:9080-/multi-storage" { - t.Errorf("unexpected resp3 Cache-Status header %v", resp1.Header.Get("Cache-Status")) - } - if resp1.Header.Get("Age") != "1" { - t.Errorf("unexpected resp1 Age header %v", resp1.Header.Get("Age")) - } - - for i := 0; i < (len(responses)/2)-1; i++ { - currentIteration := 2 + (i * 2) - resp := responses[currentIteration] - if resp.Header.Get("Cache-Status") != "Souin; hit; ttl="+fmt.Sprint(998-i)+"; key=GET-http-localhost:9080-/multi-storage" { - t.Errorf("unexpected resp%d Cache-Status header %v", currentIteration, resp.Header.Get("Cache-Status")) - } - if resp.Header.Get("Age") != fmt.Sprint(2+i) { - t.Errorf("unexpected resp%d Age header %v", currentIteration, resp.Header.Get("Age")) - } - currentIteration++ - resp = responses[currentIteration] - if resp.Header.Get("Cache-Status") != "Souin; hit; ttl="+fmt.Sprint(998-i)+"; key=GET-http-localhost:9080-/multi-storage" { - t.Errorf("unexpected resp%d Cache-Status header %v", currentIteration, resp.Header.Get("Cache-Status")) - } - if resp.Header.Get("Age") != fmt.Sprint(2+i) { - t.Errorf("unexpected resp%d Age header %v", currentIteration, resp.Header.Get("Age")) - } - } - - os.RemoveAll(path.Join(".", "souin-nuts")) -} - func TestAuthenticatedRoute(t *testing.T) { tester := caddytest.NewTester(t) tester.InitServer(` @@ -558,7 +479,7 @@ func TestAuthenticatedRoute(t *testing.T) { t.Errorf("unexpected Cache-Status header %v", respAuthBypassAlice1.Header.Get("Cache-Status")) } respAuthBypassAlice2, _ := tester.AssertResponse(getRequestFor("/auth-bypass", "Alice"), 200, "Hello, auth bypass Bearer Alice!") - if respAuthBypassAlice2.Header.Get("Cache-Status") != "Souin; hit; ttl=4; key=GET-http-localhost:9080-/auth-bypass-Bearer Alice-text/plain" { + if respAuthBypassAlice2.Header.Get("Cache-Status") != "Souin; hit; ttl=4; key=GET-http-localhost:9080-/auth-bypass-Bearer Alice-text/plain; detail=DEFAULT" { t.Errorf("unexpected Cache-Status header %v", respAuthBypassAlice2.Header.Get("Cache-Status")) } @@ -567,7 +488,7 @@ func TestAuthenticatedRoute(t *testing.T) { t.Errorf("unexpected Cache-Status header %v", respAuthBypassBob1.Header.Get("Cache-Status")) } respAuthBypassBob2, _ := tester.AssertResponse(getRequestFor("/auth-bypass", "Bob"), 200, "Hello, auth bypass Bearer Bob!") - if respAuthBypassBob2.Header.Get("Cache-Status") != "Souin; hit; ttl=4; key=GET-http-localhost:9080-/auth-bypass-Bearer Bob-text/plain" { + if respAuthBypassBob2.Header.Get("Cache-Status") != "Souin; hit; ttl=4; key=GET-http-localhost:9080-/auth-bypass-Bearer Bob-text/plain; detail=DEFAULT" { t.Errorf("unexpected Cache-Status header %v", respAuthBypassBob2.Header.Get("Cache-Status")) } @@ -576,7 +497,7 @@ func TestAuthenticatedRoute(t *testing.T) { t.Errorf("unexpected Cache-Status header %v", respAuthVaryBypassAlice1.Header.Get("Cache-Status")) } respAuthVaryBypassAlice2, _ := tester.AssertResponse(getRequestFor("/auth-bypass-vary", "Alice"), 200, "Hello, auth vary bypass Bearer Alice!") - if respAuthVaryBypassAlice2.Header.Get("Cache-Status") != "Souin; hit; ttl=4; key=GET-http-localhost:9080-/auth-bypass-vary-Bearer Alice-text/plain" { + if respAuthVaryBypassAlice2.Header.Get("Cache-Status") != "Souin; hit; ttl=4; key=GET-http-localhost:9080-/auth-bypass-vary-Bearer Alice-text/plain; detail=DEFAULT" { t.Errorf("unexpected Cache-Status header %v", respAuthVaryBypassAlice2.Header.Get("Cache-Status")) } } @@ -640,7 +561,7 @@ func TestMustRevalidate(t *testing.T) { if resp2.Header.Get("Cache-Control") != "must-revalidate" { t.Errorf("unexpected resp2 Cache-Control header %v", resp2.Header.Get("Cache-Control")) } - if resp2.Header.Get("Cache-Status") != "Souin; hit; ttl=4; key=GET-http-localhost:9080-/cache-default" { + if resp2.Header.Get("Cache-Status") != "Souin; hit; ttl=4; key=GET-http-localhost:9080-/cache-default; detail=DEFAULT" { t.Errorf("unexpected resp2 Cache-Status header %v", resp2.Header.Get("Cache-Status")) } if resp2.Header.Get("Age") != "1" { @@ -650,7 +571,7 @@ func TestMustRevalidate(t *testing.T) { if resp3.Header.Get("Cache-Control") != "must-revalidate" { t.Errorf("unexpected resp3 Cache-Control header %v", resp3.Header.Get("Cache-Control")) } - if resp3.Header.Get("Cache-Status") != "Souin; hit; ttl=-2; key=GET-http-localhost:9080-/cache-default; fwd=stale; fwd-status=500" { + if resp3.Header.Get("Cache-Status") != "Souin; hit; ttl=-2; key=GET-http-localhost:9080-/cache-default; detail=DEFAULT; fwd=stale; fwd-status=500" { t.Errorf("unexpected resp3 Cache-Status header %v", resp3.Header.Get("Cache-Status")) } if resp3.Header.Get("Age") != "7" { @@ -775,7 +696,7 @@ func TestHugeMaxAgeHandler(t *testing.T) { if resp2.Header.Get("Age") != "1" { t.Error("Age header should be present") } - if resp2.Header.Get("Cache-Status") != "Souin; hit; ttl=599; key=GET-http-localhost:9080-/huge-max-age" { + if resp2.Header.Get("Cache-Status") != "Souin; hit; ttl=599; key=GET-http-localhost:9080-/huge-max-age; detail=DEFAULT" { t.Error("Cache-Status header should be present") } @@ -784,7 +705,7 @@ func TestHugeMaxAgeHandler(t *testing.T) { if resp3.Header.Get("Age") != "3" { t.Error("Age header should be present") } - if resp3.Header.Get("Cache-Status") != "Souin; hit; ttl=597; key=GET-http-localhost:9080-/huge-max-age" { + if resp3.Header.Get("Cache-Status") != "Souin; hit; ttl=597; key=GET-http-localhost:9080-/huge-max-age; detail=DEFAULT" { t.Error("Cache-Status header should be present") } } @@ -889,8 +810,9 @@ func TestVaryHandler(t *testing.T) { t.Error("The object is not type of *http.Response") } - if (rs.Header.Get("Cache-Status") != fmt.Sprintf("Souin; hit; ttl=%d; key=GET-http-localhost:9080-/vary-multiple", ttl) || rs.Header.Get("Age") != fmt.Sprint(120-ttl)) && - (rs.Header.Get("Cache-Status") != fmt.Sprintf("Souin; hit; ttl=%d; key=GET-http-localhost:9080-/vary-multiple", ttl-1) || rs.Header.Get("Age") != fmt.Sprint(120-ttl-1)) { + nextTTL := ttl - 1 + if (rs.Header.Get("Cache-Status") != fmt.Sprintf("Souin; hit; ttl=%d; key=GET-http-localhost:9080-/vary-multiple; detail=DEFAULT", ttl) || rs.Header.Get("Age") != fmt.Sprint(120-ttl)) && + (rs.Header.Get("Cache-Status") != fmt.Sprintf("Souin; hit; ttl=%d; key=GET-http-localhost:9080-/vary-multiple; detail=DEFAULT", nextTTL) || rs.Header.Get("Age") != fmt.Sprint(120-nextTTL)) { t.Errorf("The response doesn't match the expected header or age: %s => %s", rs.Header.Get("Cache-Status"), rs.Header.Get("Age")) } } @@ -1032,15 +954,17 @@ func TestCacheableStatusCode(t *testing.T) { resp1, _ = tester.AssertGetResponse("http://localhost:9080"+path, expectedStatusCode, "") cacheStatus = "Souin; " + detail := "" if expectedCached { if resp1.Header.Get("Age") != "1" { t.Errorf("unexpected Age header %v", resp1.Header.Get("Age")) } cacheStatus += "hit; ttl=9; " + detail = "; detail=DEFAULT" } else { cacheStatus += "fwd=uri-miss; detail=UPSTREAM-ERROR-OR-EMPTY-RESPONSE; " } - cacheStatus += "key=GET-http-localhost:9080-" + path + cacheStatus += "key=GET-http-localhost:9080-" + path + detail if resp1.Header.Get("Cache-Status") != cacheStatus { t.Errorf("unexpected second Cache-Status header %v", resp1.Header.Get("Cache-Status")) @@ -1052,3 +976,65 @@ func TestCacheableStatusCode(t *testing.T) { cacheChecker(caddyTester, "/cache-301", 301, true) cacheChecker(caddyTester, "/cache-405", 405, true) } + +func TestExpires(t *testing.T) { + expiresValue := time.Now().Add(time.Hour * 24) + caddyTester := caddytest.NewTester(t) + caddyTester.InitServer(fmt.Sprintf(` + { + admin localhost:2999 + http_port 9080 + https_port 9443 + cache { + ttl 10s + } + } + localhost:9080 { + route /expires-only { + cache + header Expires "%[1]s" + respond "Hello, expires-only!" + } + route /expires-with-max-age { + cache + header Expires "%[1]s" + header Cache-Control "max-age=60" + respond "Hello, expires-with-max-age!" + } + route /expires-with-s-maxage { + cache + header Expires "%[1]s" + header Cache-Control "s-maxage=5" + respond "Hello, expires-with-s-maxage!" + } + }`, expiresValue.Format(time.RFC1123)), "caddyfile") + + cacheChecker := func(tester *caddytest.Tester, path string, expectedBody string, expectedDuration int) { + resp1, _ := tester.AssertGetResponse("http://localhost:9080"+path, 200, expectedBody) + if resp1.Header.Get("Age") != "" { + t.Errorf("unexpected Age header %v", resp1.Header.Get("Age")) + } + + if resp1.Header.Get("Cache-Status") != "Souin; fwd=uri-miss; stored; key=GET-http-localhost:9080-"+path { + t.Errorf("unexpected first Cache-Status header %v", resp1.Header.Get("Cache-Status")) + } + + resp1, _ = tester.AssertGetResponse("http://localhost:9080"+path, 200, expectedBody) + + if resp1.Header.Get("Age") != "1" { + t.Errorf("unexpected Age header %v", resp1.Header.Get("Age")) + } + + if resp1.Header.Get("Cache-Status") != fmt.Sprintf("Souin; hit; ttl=%d; key=GET-http-localhost:9080-%s; detail=DEFAULT", expectedDuration, path) { + t.Errorf( + "unexpected second Cache-Status header %v, expected %s", + resp1.Header.Get("Cache-Status"), + fmt.Sprintf("Souin; hit; ttl=%d; key=GET-http-localhost:9080-%s; detail=DEFAULT", expectedDuration, path), + ) + } + } + + cacheChecker(caddyTester, "/expires-only", "Hello, expires-only!", int(time.Until(expiresValue).Seconds())-1) + cacheChecker(caddyTester, "/expires-with-max-age", "Hello, expires-with-max-age!", 59) + cacheChecker(caddyTester, "/expires-with-s-maxage", "Hello, expires-with-s-maxage!", 4) +} diff --git a/plugins/chi/souin_test.go b/plugins/chi/souin_test.go index 5a589c666..d71c1f546 100644 --- a/plugins/chi/souin_test.go +++ b/plugins/chi/souin_test.go @@ -62,7 +62,7 @@ func Test_SouinChiPlugin_Middleware(t *testing.T) { } router.ServeHTTP(res2, req) - if res2.Result().Header.Get("Cache-Status") != "Souin; hit; ttl=4; key=GET-http-example.com-/handled" { + if res2.Result().Header.Get("Cache-Status") != "Souin; hit; ttl=4; key=GET-http-example.com-/handled; detail=DEFAULT" { t.Error("The response must contain a Cache-Status header with the hit and ttl directives.") } if res2.Result().Header.Get("Age") != "1" { diff --git a/plugins/dotweb/souin_test.go b/plugins/dotweb/souin_test.go index 9cadb2b6b..c3efadb48 100644 --- a/plugins/dotweb/souin_test.go +++ b/plugins/dotweb/souin_test.go @@ -61,7 +61,7 @@ func Test_SouinDotwebPlugin_Middleware(t *testing.T) { router.HttpServer.ServeHTTP(res2, req) - if res2.Result().Header.Get("Cache-Status") != "Souin; hit; ttl=4; key=GET-http-example.com-/handled" { + if res2.Result().Header.Get("Cache-Status") != "Souin; hit; ttl=4; key=GET-http-example.com-/handled; detail=DEFAULT" { t.Error("The response must contain a Cache-Status header with the hit and ttl directives.") } if res2.Result().Header.Get("Age") != "1" { diff --git a/plugins/echo/souin_test.go b/plugins/echo/souin_test.go index be833c1dd..b4cbf1f50 100644 --- a/plugins/echo/souin_test.go +++ b/plugins/echo/souin_test.go @@ -51,7 +51,7 @@ func Test_SouinEchoPlugin_Process(t *testing.T) { if err := s.Process(handler)(c2); err != nil { t.Error("No error must be thrown on the second request if everything is good.") } - if res2.Result().Header.Get("Cache-Status") != "Souin; hit; ttl=4; key=GET-http-example.com-/handled" { + if res2.Result().Header.Get("Cache-Status") != "Souin; hit; ttl=4; key=GET-http-example.com-/handled; detail=DEFAULT" { t.Error("The response must contain a Cache-Status header with the hit and ttl directives.") } if res2.Result().Header.Get("Age") != "1" { diff --git a/plugins/fiber/souin_test.go b/plugins/fiber/souin_test.go index 2fda6918f..75152bfe3 100644 --- a/plugins/fiber/souin_test.go +++ b/plugins/fiber/souin_test.go @@ -70,7 +70,7 @@ func Test_SouinFiberPlugin_Middleware(t *testing.T) { t.Error(err) } - if res.Header.Get("Cache-Status") != "Souin; hit; ttl=4; key=GET-http-example.com-/handled" { + if res.Header.Get("Cache-Status") != "Souin; hit; ttl=4; key=GET-http-example.com-/handled; detail=DEFAULT" { t.Error("The response must contain a Cache-Status header with the hit and ttl directives.") } if res.Header.Get("Age") != "1" { diff --git a/plugins/gin/souin_test.go b/plugins/gin/souin_test.go index bbb3411a9..4a5cea854 100644 --- a/plugins/gin/souin_test.go +++ b/plugins/gin/souin_test.go @@ -51,7 +51,7 @@ func Test_SouinGinPlugin_Process(t *testing.T) { } r.ServeHTTP(res2, c.Request) - if res2.Result().Header.Get("Cache-Status") != "Souin; hit; ttl=4; key=GET-http-example.com-/handled" { + if res2.Result().Header.Get("Cache-Status") != "Souin; hit; ttl=4; key=GET-http-example.com-/handled; detail=DEFAULT" { t.Error("The response must contain a Cache-Status header with the hit and ttl directives.") } if res2.Result().Header.Get("Age") != "1" { diff --git a/plugins/go-zero/souin_test.go b/plugins/go-zero/souin_test.go index 272c4c8ef..80785b658 100644 --- a/plugins/go-zero/souin_test.go +++ b/plugins/go-zero/souin_test.go @@ -52,7 +52,7 @@ func Test_SouinGoZeroPlugin_Middleware(t *testing.T) { } router(res2, req) - if res2.Result().Header.Get("Cache-Status") != "Souin; hit; ttl=4; key=GET-http-example.com-/handled" { + if res2.Result().Header.Get("Cache-Status") != "Souin; hit; ttl=4; key=GET-http-example.com-/handled; detail=DEFAULT" { t.Error("The response must contain a Cache-Status header with the hit and ttl directives.") } if res2.Result().Header.Get("Age") != "1" { diff --git a/plugins/goa/souin_test.go b/plugins/goa/souin_test.go index 3080d9abe..564964de4 100644 --- a/plugins/goa/souin_test.go +++ b/plugins/goa/souin_test.go @@ -58,7 +58,7 @@ func Test_SouinGoaPlugin_Middleware(t *testing.T) { } handler.ServeHTTP(res2, req) - if res2.Result().Header.Get("Cache-Status") != "Souin; hit; ttl=4; key=GET-http-example.com-/handled" { + if res2.Result().Header.Get("Cache-Status") != "Souin; hit; ttl=4; key=GET-http-example.com-/handled; detail=DEFAULT" { t.Error("The response must contain a Cache-Status header with the hit and ttl directives.") } if res2.Result().Header.Get("Age") != "1" { diff --git a/plugins/goyave/souin_test.go b/plugins/goyave/souin_test.go index 4adf7367c..b496aedd2 100644 --- a/plugins/goyave/souin_test.go +++ b/plugins/goyave/souin_test.go @@ -76,7 +76,7 @@ func (suite *HttpCacheMiddlewareTestSuite) Test_SouinFiberPlugin_Middleware() { suite.T().Error("The response body must be equal to Hello, World 👋!.") } - if res.Header.Get("Cache-Status") != "Souin; hit; ttl=4; key=GET-http-example.com-/handled" { + if res.Header.Get("Cache-Status") != "Souin; hit; ttl=4; key=GET-http-example.com-/handled; detail=DEFAULT" { suite.T().Error("The response must contain a Cache-Status header with the hit and ttl directives.") } if res.Header.Get("Age") != "1" { diff --git a/plugins/hertz/httpcache_test.go b/plugins/hertz/httpcache_test.go index c03bb4bcd..23b7aefec 100644 --- a/plugins/hertz/httpcache_test.go +++ b/plugins/hertz/httpcache_test.go @@ -40,7 +40,7 @@ func TestGetDefaultRequest(t *testing.T) { } rr2 := ut.PerformRequest(engine, http.MethodGet, "http://domain.com/default", nil) - if rr2.Result().Header.Get("Cache-Status") != "Souin; hit; ttl=4; key=GET-http-domain.com-/default" { + if rr2.Result().Header.Get("Cache-Status") != "Souin; hit; ttl=4; key=GET-http-domain.com-/default; detail=DEFAULT" { t.Errorf("The Cache-Status header response mismatched the expectations, %s given.", rr2.Result().Header.Get("Cache-Status")) } if rr2.Result().Header.Get("Age") != "1" { @@ -84,7 +84,7 @@ func TestGetDefaultRequest(t *testing.T) { Key: "Cache-Control", Value: "no-store", }) - if rr1.Result().Header.Get("Cache-Status") != "Souin; hit; ttl=4; key=GET-http-domain.com-/default" { + if rr1.Result().Header.Get("Cache-Status") != "Souin; hit; ttl=4; key=GET-http-domain.com-/default; detail=DEFAULT" { t.Errorf("The Cache-Status header response mismatched the expectations, %s given.", rr1.Result().Header.Get("Cache-Status")) } if rr1.Result().Header.Get("Age") != "1" { @@ -150,7 +150,7 @@ func validateStoreAllowedMethods(t *testing.T, engine *route.Engine, method stri } rr2 := ut.PerformRequest(engine, method, "http://domain.com/allowed_methods", nil) - if rr2.Result().Header.Get("Cache-Status") != fmt.Sprintf("Souin; hit; ttl=4; key=%s-http-domain.com-/allowed_methods", method) { + if rr2.Result().Header.Get("Cache-Status") != fmt.Sprintf("Souin; hit; ttl=4; key=%s-http-domain.com-/allowed_methods; detail=DEFAULT", method) { t.Errorf("The Cache-Status header response mismatched the expectations, %s given.", rr2.Result().Header.Get("Cache-Status")) } if rr2.Result().Header.Get("Age") != "1" { diff --git a/plugins/kratos/souin_test.go b/plugins/kratos/souin_test.go index a1a1fb0dd..86541f323 100644 --- a/plugins/kratos/souin_test.go +++ b/plugins/kratos/souin_test.go @@ -71,7 +71,7 @@ func Test_HttpcacheKratosPlugin_NewHTTPCacheFilter(t *testing.T) { handler.ServeHTTP(res2, req) rs = res2.Result() rs.Body.Close() - if rs.Header.Get("Cache-Status") != "Souin; hit; ttl=4; key=GET-http-example.com-/handled" { + if rs.Header.Get("Cache-Status") != "Souin; hit; ttl=4; key=GET-http-example.com-/handled; detail=DEFAULT" { t.Error("The response must contain a Cache-Status header with the hit and ttl directives.") } if rs.Header.Get("Age") != "1" { diff --git a/plugins/roadrunner/httpcache_test.go b/plugins/roadrunner/httpcache_test.go index 957f4c2c1..0ed4fa457 100644 --- a/plugins/roadrunner/httpcache_test.go +++ b/plugins/roadrunner/httpcache_test.go @@ -94,7 +94,7 @@ func Test_Plugin_Middleware(t *testing.T) { if err != nil { t.Error("body close error") } - if rs.Header.Get("Cache-Status") != "Souin; hit; ttl=4; key=GET-http-example.com-/handled" { + if rs.Header.Get("Cache-Status") != "Souin; hit; ttl=4; key=GET-http-example.com-/handled; detail=DEFAULT" { t.Error("The response must contain a Cache-Status header with the hit and ttl directives.") } if rs.Header.Get("Age") != "1" { @@ -125,7 +125,7 @@ func Test_Plugin_Middleware_Stale(t *testing.T) { if err != nil { t.Error("body close error") } - if rs.Header.Get("Cache-Status") != "Souin; hit; ttl=4; key=GET-http-example.com-/stale-test" { + if rs.Header.Get("Cache-Status") != "Souin; hit; ttl=4; key=GET-http-example.com-/stale-test; detail=DEFAULT" { t.Error("The response must contain a Cache-Status header with the hit directive.") } if rs.Header.Get("Age") != "1" { diff --git a/plugins/traefik/Makefile b/plugins/traefik/Makefile index 7e11d01ce..21ce75631 100644 --- a/plugins/traefik/Makefile +++ b/plugins/traefik/Makefile @@ -27,15 +27,11 @@ replace: ## Replace sources in the vendor folder deeper than the go mod replace $(MAKE) copy-to base=$(SOUIN) target=configurationtypes $(MAKE) copy-to base=$(SOUIN) target=context $(MAKE) copy-file-to base=$(PKG) target=surrogate/providers/common.go - $(MAKE) copy-to base=$(CACHE) target=ykeys $(MAKE) copy-to base=$(API) target=api $(MAKE) copy-to base=$(PKG) target=api $(MAKE) copy-to base=$(PKG) target=storage - $(MAKE) copy-file-to base=$(PKG) target=rfc/vary.go $(MAKE) copy-file-to base=$(PKG) target=rfc/revalidation.go - $(MAKE) copy-file-to base=$(PKG) target=middleware/configuration.go - $(MAKE) copy-file-to base=$(PKG) target=middleware/middleware.go - $(MAKE) copy-file-to base=$(PKG) target=middleware/writer.go + $(MAKE) copy-to base=$(PKG) target=middleware vendor: ## Generate vendors for the plugin go mod tidy diff --git a/plugins/traefik/docker-compose.yml.test b/plugins/traefik/docker-compose.yml.test index 6d98deeea..504b65ecf 100644 --- a/plugins/traefik/docker-compose.yml.test +++ b/plugins/traefik/docker-compose.yml.test @@ -2,7 +2,7 @@ version: '3.4' services: traefik: - image: traefik:v3.0 + image: traefik:v3.1 volumes: - /var/run/docker.sock:/var/run/docker.sock - ../..:/plugins-local/src/github.com/darkweak/souin diff --git a/plugins/traefik/override/middleware/middleware.go b/plugins/traefik/override/middleware/middleware.go index 2fe9f0551..f4feccfa3 100644 --- a/plugins/traefik/override/middleware/middleware.go +++ b/plugins/traefik/override/middleware/middleware.go @@ -231,7 +231,7 @@ func (s *SouinBaseHandler) Store( wg.Wait() if len(fails) < s.storersLen { go func(rs http.Response, key string) { - _ = s.SurrogateKeyStorer.Store(&rs, key) + _ = s.SurrogateKeyStorer.Store(&rs, key, "", "") }(res, cachedKey) status += "; stored" } @@ -411,7 +411,7 @@ func (s *SouinBaseHandler) ServeHTTP(rw http.ResponseWriter, rq *http.Request, n if response != nil && (!modeContext.Strict || rfc.ValidateCacheControl(response, requestCc)) { if validator.ResponseETag != "" && validator.Matched { - rfc.SetCacheStatusHeader(response) + rfc.SetCacheStatusHeader(response, "DEFAULT") customWriter.Headers = response.Header if validator.NotModified { customWriter.statusCode = http.StatusNotModified @@ -440,7 +440,7 @@ func (s *SouinBaseHandler) ServeHTTP(rw http.ResponseWriter, rq *http.Request, n return err } - rfc.SetCacheStatusHeader(response) + rfc.SetCacheStatusHeader(response, "DEFAULT") if !modeContext.Strict || rfc.ValidateMaxAgeCachedResponse(requestCc, response) != nil { customWriter.Headers = response.Header customWriter.statusCode = response.StatusCode @@ -458,7 +458,7 @@ func (s *SouinBaseHandler) ServeHTTP(rw http.ResponseWriter, rq *http.Request, n } if nil != response && (!modeContext.Strict || rfc.ValidateCacheControl(response, requestCc)) { addTime, _ := time.ParseDuration(response.Header.Get(rfc.StoredTTLHeader)) - rfc.SetCacheStatusHeader(response) + rfc.SetCacheStatusHeader(response, "DEFAULT") responseCc, _ := cacheobject.ParseResponseCacheControl(response.Header.Get("Cache-Control")) if responseCc.StaleWhileRevalidate > 0 { @@ -502,7 +502,7 @@ func (s *SouinBaseHandler) ServeHTTP(rw http.ResponseWriter, rq *http.Request, n if customWriter.statusCode == http.StatusNotModified { if !validator.Matched { - rfc.SetCacheStatusHeader(response) + rfc.SetCacheStatusHeader(response, "DEFAULT") customWriter.statusCode = response.StatusCode customWriter.Headers = response.Header _, _ = io.Copy(customWriter.Buf, response.Body) diff --git a/plugins/traefik/override/rfc/vary.go b/plugins/traefik/override/rfc/vary.go deleted file mode 100644 index 6ac7a9e42..000000000 --- a/plugins/traefik/override/rfc/vary.go +++ /dev/null @@ -1,41 +0,0 @@ -package rfc - -import ( - "fmt" - "net/http" - "net/url" - "strings" -) - -const ( - VarySeparator = "{-VARY-}" - DecodedHeaderSeparator = ";" -) - -// GetVariedCacheKey returns the varied cache key for req and resp. -func GetVariedCacheKey(rq *http.Request, headers []string) string { - if len(headers) == 0 { - return "" - } - for i, v := range headers { - h := strings.TrimSpace(rq.Header.Get(v)) - if strings.Contains(h, ";") || strings.Contains(h, ":") { - h = url.QueryEscape(h) - } - headers[i] = fmt.Sprintf("%s:%s", v, h) - } - - return VarySeparator + strings.Join(headers, DecodedHeaderSeparator) -} - -// VariedHeaderAllCommaSepValues returns all comma-separated values -// or '*' alone when the header contains it. -func VariedHeaderAllCommaSepValues(headers http.Header) ([]string, bool) { - vals := HeaderAllCommaSepValues(headers, "Vary") - for _, v := range vals { - if v == "*" { - return []string{"*"}, true - } - } - return vals, false -} diff --git a/plugins/traefik/override/storage/abstractProvider_test.go b/plugins/traefik/override/storage/abstractProvider_test.go deleted file mode 100644 index a3905bbc6..000000000 --- a/plugins/traefik/override/storage/abstractProvider_test.go +++ /dev/null @@ -1,16 +0,0 @@ -package storage - -import ( - "testing" - - "github.com/darkweak/souin/tests" -) - -func TestInitializeProvider(t *testing.T) { - c := tests.MockConfiguration(tests.BaseConfiguration) - p := InitializeProvider(c) - err := p.Init() - if nil != err { - t.Error("Init shouldn't crash") - } -} diff --git a/plugins/traefik/override/surrogate/providers/common.go b/plugins/traefik/override/surrogate/providers/common.go index b6ccc973f..5ae49c91b 100644 --- a/plugins/traefik/override/surrogate/providers/common.go +++ b/plugins/traefik/override/surrogate/providers/common.go @@ -189,7 +189,7 @@ func (s *baseStorage) purgeTag(tag string) []string { } // Store will take the lead to store the cache key for each provided Surrogate-key -func (s *baseStorage) Store(response *http.Response, cacheKey string) error { +func (s *baseStorage) Store(response *http.Response, cacheKey, uri, basekey string) error { h := response.Header cacheKey = url.QueryEscape(cacheKey) diff --git a/plugins/traefik/override/ykeys/ykey.go b/plugins/traefik/override/ykeys/ykey.go deleted file mode 100644 index b710369c0..000000000 --- a/plugins/traefik/override/ykeys/ykey.go +++ /dev/null @@ -1,136 +0,0 @@ -package ykeys - -import ( - "fmt" - "net/http" - "regexp" - "strings" - "time" - - "github.com/akyoto/cache" - - "github.com/darkweak/souin/configurationtypes" -) - -// The YKey system is like the Varnish one. You can invalidate cache from ykey based instead of the regexp or the plain -// URL to invalidate. It will target the referred URLs to this tag -// e.g. -// Given YKey data as -// |---------------|-----------------------------------------------------------------------------------| -// | YKey | URLs | -// |---------------|-----------------------------------------------------------------------------------| -// | GROUP_KEY_ONE | http://domain.com/,http://domain.com/1,http://domain.com/2,http://domain.com/4 | -// | GROUP_KEY_TWO | http://domain.com/1,http://domain.com/2,http://domain.com/3,http://domain.com/xyz | -// |---------------|-----------------------------------------------------------------------------------| -// When I send a purge request to /ykey/GROUP_KEY_ONE -// Then the cache will be purged for the list -// * http://domain.com/ -// * http://domain.com/1 -// * http://domain.com/2 -// * http://domain.com/4 -// And the data in the YKey table storage will contain -// |---------------|-------------------------------------------| -// | YKey | URLs | -// |---------------|-------------------------------------------| -// | GROUP_KEY_ONE | | -// | GROUP_KEY_TWO | http://domain.com/3,http://domain.com/xyz | -// |---------------|-------------------------------------------| - -// YKeyStorage is the layer for YKey support storage -type YKeyStorage struct { - *cache.Cache - Keys map[string]configurationtypes.SurrogateKeys -} - -// InitializeYKeys will initialize the ykey storage system -func InitializeYKeys(keys map[string]configurationtypes.SurrogateKeys) *YKeyStorage { - if len(keys) == 0 { - return nil - } - - c := cache.New(1 * time.Second) - - for key := range keys { - c.Set(key, "", 1) - } - - return &YKeyStorage{Cache: c, Keys: keys} -} - -// GetValidatedTags returns the validated tags based on the key x headers -func (y *YKeyStorage) GetValidatedTags(key string, headers http.Header) []string { - var tags []string - for k, v := range y.Keys { - valid := true - if v.URL != "" { - if r, e := regexp.MatchString(v.URL, key); !r || e != nil { - continue - } - } - if v.Headers != nil { - for h, hValue := range v.Headers { - if res, err := regexp.MatchString(hValue, headers.Get(h)); !res || err != nil { - valid = false - break - } - } - } - if valid { - tags = append(tags, k) - } - } - - return tags -} - -// InvalidateTags invalidate a tag list -func (y *YKeyStorage) InvalidateTags(tags []string) []string { - var u []string - for _, tag := range tags { - if v, e := y.Cache.Get(tag); e { - u = append(u, y.InvalidateTagURLs(v.(string))...) - } - } - - return u -} - -// InvalidateTagURLs invalidate URLs in the stored map -func (y *YKeyStorage) InvalidateTagURLs(urls string) []string { - u := strings.Split(urls, ",") - for _, url := range u { - y.invalidateURL(url) - } - - return u -} - -func (y *YKeyStorage) invalidateURL(url string) { - urlRegexp := regexp.MustCompile(fmt.Sprintf("(%s,)|(,%s$)|(^%s$)", url, url, url)) - for key := range y.Keys { - v, found := y.Cache.Get(key) - if found && urlRegexp.MatchString(v.(string)) { - y.Set(key, urlRegexp.ReplaceAllString(v.(string), ""), 1) - } - } -} - -// AddToTags add an URL to a tag list -func (y *YKeyStorage) AddToTags(url string, tags []string) { - for _, tag := range tags { - y.addToTag(url, tag) - } -} - -func (y *YKeyStorage) addToTag(url string, tag string) { - if v, e := y.Cache.Get(tag); e { - urlRegexp := regexp.MustCompile(url) - tmpStr := v.(string) - if !urlRegexp.MatchString(tmpStr) { - if tmpStr != "" { - tmpStr += "," - } - y.Cache.Set(tag, tmpStr+url, 1) - } - } -} diff --git a/plugins/traefik/vendor/github.com/darkweak/souin/cache/ykey.go b/plugins/traefik/vendor/github.com/darkweak/souin/cache/ykey.go deleted file mode 100644 index b710369c0..000000000 --- a/plugins/traefik/vendor/github.com/darkweak/souin/cache/ykey.go +++ /dev/null @@ -1,136 +0,0 @@ -package ykeys - -import ( - "fmt" - "net/http" - "regexp" - "strings" - "time" - - "github.com/akyoto/cache" - - "github.com/darkweak/souin/configurationtypes" -) - -// The YKey system is like the Varnish one. You can invalidate cache from ykey based instead of the regexp or the plain -// URL to invalidate. It will target the referred URLs to this tag -// e.g. -// Given YKey data as -// |---------------|-----------------------------------------------------------------------------------| -// | YKey | URLs | -// |---------------|-----------------------------------------------------------------------------------| -// | GROUP_KEY_ONE | http://domain.com/,http://domain.com/1,http://domain.com/2,http://domain.com/4 | -// | GROUP_KEY_TWO | http://domain.com/1,http://domain.com/2,http://domain.com/3,http://domain.com/xyz | -// |---------------|-----------------------------------------------------------------------------------| -// When I send a purge request to /ykey/GROUP_KEY_ONE -// Then the cache will be purged for the list -// * http://domain.com/ -// * http://domain.com/1 -// * http://domain.com/2 -// * http://domain.com/4 -// And the data in the YKey table storage will contain -// |---------------|-------------------------------------------| -// | YKey | URLs | -// |---------------|-------------------------------------------| -// | GROUP_KEY_ONE | | -// | GROUP_KEY_TWO | http://domain.com/3,http://domain.com/xyz | -// |---------------|-------------------------------------------| - -// YKeyStorage is the layer for YKey support storage -type YKeyStorage struct { - *cache.Cache - Keys map[string]configurationtypes.SurrogateKeys -} - -// InitializeYKeys will initialize the ykey storage system -func InitializeYKeys(keys map[string]configurationtypes.SurrogateKeys) *YKeyStorage { - if len(keys) == 0 { - return nil - } - - c := cache.New(1 * time.Second) - - for key := range keys { - c.Set(key, "", 1) - } - - return &YKeyStorage{Cache: c, Keys: keys} -} - -// GetValidatedTags returns the validated tags based on the key x headers -func (y *YKeyStorage) GetValidatedTags(key string, headers http.Header) []string { - var tags []string - for k, v := range y.Keys { - valid := true - if v.URL != "" { - if r, e := regexp.MatchString(v.URL, key); !r || e != nil { - continue - } - } - if v.Headers != nil { - for h, hValue := range v.Headers { - if res, err := regexp.MatchString(hValue, headers.Get(h)); !res || err != nil { - valid = false - break - } - } - } - if valid { - tags = append(tags, k) - } - } - - return tags -} - -// InvalidateTags invalidate a tag list -func (y *YKeyStorage) InvalidateTags(tags []string) []string { - var u []string - for _, tag := range tags { - if v, e := y.Cache.Get(tag); e { - u = append(u, y.InvalidateTagURLs(v.(string))...) - } - } - - return u -} - -// InvalidateTagURLs invalidate URLs in the stored map -func (y *YKeyStorage) InvalidateTagURLs(urls string) []string { - u := strings.Split(urls, ",") - for _, url := range u { - y.invalidateURL(url) - } - - return u -} - -func (y *YKeyStorage) invalidateURL(url string) { - urlRegexp := regexp.MustCompile(fmt.Sprintf("(%s,)|(,%s$)|(^%s$)", url, url, url)) - for key := range y.Keys { - v, found := y.Cache.Get(key) - if found && urlRegexp.MatchString(v.(string)) { - y.Set(key, urlRegexp.ReplaceAllString(v.(string), ""), 1) - } - } -} - -// AddToTags add an URL to a tag list -func (y *YKeyStorage) AddToTags(url string, tags []string) { - for _, tag := range tags { - y.addToTag(url, tag) - } -} - -func (y *YKeyStorage) addToTag(url string, tag string) { - if v, e := y.Cache.Get(tag); e { - urlRegexp := regexp.MustCompile(url) - tmpStr := v.(string) - if !urlRegexp.MatchString(tmpStr) { - if tmpStr != "" { - tmpStr += "," - } - y.Cache.Set(tag, tmpStr+url, 1) - } - } -} diff --git a/plugins/traefik/vendor/github.com/darkweak/souin/pkg/middleware/middleware.go b/plugins/traefik/vendor/github.com/darkweak/souin/pkg/middleware/middleware.go index 2fe9f0551..f4feccfa3 100644 --- a/plugins/traefik/vendor/github.com/darkweak/souin/pkg/middleware/middleware.go +++ b/plugins/traefik/vendor/github.com/darkweak/souin/pkg/middleware/middleware.go @@ -231,7 +231,7 @@ func (s *SouinBaseHandler) Store( wg.Wait() if len(fails) < s.storersLen { go func(rs http.Response, key string) { - _ = s.SurrogateKeyStorer.Store(&rs, key) + _ = s.SurrogateKeyStorer.Store(&rs, key, "", "") }(res, cachedKey) status += "; stored" } @@ -411,7 +411,7 @@ func (s *SouinBaseHandler) ServeHTTP(rw http.ResponseWriter, rq *http.Request, n if response != nil && (!modeContext.Strict || rfc.ValidateCacheControl(response, requestCc)) { if validator.ResponseETag != "" && validator.Matched { - rfc.SetCacheStatusHeader(response) + rfc.SetCacheStatusHeader(response, "DEFAULT") customWriter.Headers = response.Header if validator.NotModified { customWriter.statusCode = http.StatusNotModified @@ -440,7 +440,7 @@ func (s *SouinBaseHandler) ServeHTTP(rw http.ResponseWriter, rq *http.Request, n return err } - rfc.SetCacheStatusHeader(response) + rfc.SetCacheStatusHeader(response, "DEFAULT") if !modeContext.Strict || rfc.ValidateMaxAgeCachedResponse(requestCc, response) != nil { customWriter.Headers = response.Header customWriter.statusCode = response.StatusCode @@ -458,7 +458,7 @@ func (s *SouinBaseHandler) ServeHTTP(rw http.ResponseWriter, rq *http.Request, n } if nil != response && (!modeContext.Strict || rfc.ValidateCacheControl(response, requestCc)) { addTime, _ := time.ParseDuration(response.Header.Get(rfc.StoredTTLHeader)) - rfc.SetCacheStatusHeader(response) + rfc.SetCacheStatusHeader(response, "DEFAULT") responseCc, _ := cacheobject.ParseResponseCacheControl(response.Header.Get("Cache-Control")) if responseCc.StaleWhileRevalidate > 0 { @@ -502,7 +502,7 @@ func (s *SouinBaseHandler) ServeHTTP(rw http.ResponseWriter, rq *http.Request, n if customWriter.statusCode == http.StatusNotModified { if !validator.Matched { - rfc.SetCacheStatusHeader(response) + rfc.SetCacheStatusHeader(response, "DEFAULT") customWriter.statusCode = response.StatusCode customWriter.Headers = response.Header _, _ = io.Copy(customWriter.Buf, response.Body) diff --git a/plugins/traefik/vendor/github.com/darkweak/souin/pkg/rfc/cache_status.go b/plugins/traefik/vendor/github.com/darkweak/souin/pkg/rfc/cache_status.go index 3d5a61b48..f02132fb7 100644 --- a/plugins/traefik/vendor/github.com/darkweak/souin/pkg/rfc/cache_status.go +++ b/plugins/traefik/vendor/github.com/darkweak/souin/pkg/rfc/cache_status.go @@ -80,7 +80,7 @@ func HitStaleCache(h *http.Header) { h.Set("Cache-Status", h.Get("Cache-Status")+"; fwd=stale") } -func manageAge(h *http.Header, ttl time.Duration, cacheName, key string) { +func manageAge(h *http.Header, ttl time.Duration, cacheName, key, storerName string) { utc1 := time.Now().UTC() dh := h.Get("Date") if dh == "" { @@ -119,7 +119,7 @@ func manageAge(h *http.Header, ttl time.Duration, cacheName, key string) { age := strconv.Itoa(oldAge + cage) h.Set("Age", age) ttlValue := strconv.Itoa(int(ttl.Seconds()) - cage) - h.Set("Cache-Status", cacheName+"; hit; ttl="+ttlValue+"; key="+key) + h.Set("Cache-Status", cacheName+"; hit; ttl="+ttlValue+"; key="+key+"; detail="+storerName) } func setMalformedHeader(headers *http.Header, header, cacheName string) { @@ -127,11 +127,11 @@ func setMalformedHeader(headers *http.Header, header, cacheName string) { } // SetCacheStatusHeader set the Cache-Status header -func SetCacheStatusHeader(resp *http.Response) *http.Response { +func SetCacheStatusHeader(resp *http.Response, storerName string) *http.Response { h := resp.Header cacheName := resp.Request.Context().Value(context.CacheName).(string) validateEmptyHeaders(&h, cacheName) - manageAge(&h, 0, cacheName, GetCacheKeyFromCtx(resp.Request.Context())) + manageAge(&h, 0, cacheName, GetCacheKeyFromCtx(resp.Request.Context()), storerName) resp.Header = h return resp diff --git a/plugins/traefik/vendor/github.com/darkweak/souin/pkg/rfc/vary.go b/plugins/traefik/vendor/github.com/darkweak/souin/pkg/rfc/vary.go index 6ac7a9e42..2c55dc3dd 100644 --- a/plugins/traefik/vendor/github.com/darkweak/souin/pkg/rfc/vary.go +++ b/plugins/traefik/vendor/github.com/darkweak/souin/pkg/rfc/vary.go @@ -4,6 +4,7 @@ import ( "fmt" "net/http" "net/url" + "slices" "strings" ) @@ -32,10 +33,8 @@ func GetVariedCacheKey(rq *http.Request, headers []string) string { // or '*' alone when the header contains it. func VariedHeaderAllCommaSepValues(headers http.Header) ([]string, bool) { vals := HeaderAllCommaSepValues(headers, "Vary") - for _, v := range vals { - if v == "*" { - return []string{"*"}, true - } + if slices.Contains(vals, "*") { + return []string{"*"}, true } return vals, false } diff --git a/plugins/traefik/vendor/github.com/darkweak/souin/pkg/storage/abstractProvider_test.go b/plugins/traefik/vendor/github.com/darkweak/souin/pkg/storage/abstractProvider_test.go deleted file mode 100644 index a3905bbc6..000000000 --- a/plugins/traefik/vendor/github.com/darkweak/souin/pkg/storage/abstractProvider_test.go +++ /dev/null @@ -1,16 +0,0 @@ -package storage - -import ( - "testing" - - "github.com/darkweak/souin/tests" -) - -func TestInitializeProvider(t *testing.T) { - c := tests.MockConfiguration(tests.BaseConfiguration) - p := InitializeProvider(c) - err := p.Init() - if nil != err { - t.Error("Init shouldn't crash") - } -} diff --git a/plugins/traefik/vendor/github.com/darkweak/souin/pkg/surrogate/providers/akamai.go b/plugins/traefik/vendor/github.com/darkweak/souin/pkg/surrogate/providers/akamai.go index 2e35a9564..d33455b79 100644 --- a/plugins/traefik/vendor/github.com/darkweak/souin/pkg/surrogate/providers/akamai.go +++ b/plugins/traefik/vendor/github.com/darkweak/souin/pkg/surrogate/providers/akamai.go @@ -39,12 +39,12 @@ func (*AkamaiSurrogateStorage) getHeaderSeparator() string { } // Store stores the response tags located in the first non empty supported header -func (a *AkamaiSurrogateStorage) Store(response *http.Response, cacheKey string) error { +func (a *AkamaiSurrogateStorage) Store(response *http.Response, cacheKey, uri, basekey string) error { defer func() { response.Header.Del(surrogateKey) response.Header.Del(surrogateControl) }() - e := a.baseStorage.Store(response, cacheKey) + e := a.baseStorage.Store(response, cacheKey, uri, basekey) response.Header.Set(edgeCacheTag, response.Header.Get(surrogateKey)) return e diff --git a/plugins/traefik/vendor/github.com/darkweak/souin/pkg/surrogate/providers/cloudflare.go b/plugins/traefik/vendor/github.com/darkweak/souin/pkg/surrogate/providers/cloudflare.go index f131ac900..d733347b8 100644 --- a/plugins/traefik/vendor/github.com/darkweak/souin/pkg/surrogate/providers/cloudflare.go +++ b/plugins/traefik/vendor/github.com/darkweak/souin/pkg/surrogate/providers/cloudflare.go @@ -38,12 +38,12 @@ func (*CloudflareSurrogateStorage) getHeaderSeparator() string { } // Store stores the response tags located in the first non empty supported header -func (c *CloudflareSurrogateStorage) Store(response *http.Response, cacheKey string) error { +func (c *CloudflareSurrogateStorage) Store(response *http.Response, cacheKey, uri, basekey string) error { defer func() { response.Header.Del(surrogateKey) response.Header.Del(surrogateControl) }() - e := c.baseStorage.Store(response, cacheKey) + e := c.baseStorage.Store(response, cacheKey, uri, basekey) response.Header.Set(cacheTag, strings.Join(c.ParseHeaders(response.Header.Get(surrogateKey)), c.getHeaderSeparator())) return e diff --git a/plugins/traefik/vendor/github.com/darkweak/souin/pkg/surrogate/providers/common.go b/plugins/traefik/vendor/github.com/darkweak/souin/pkg/surrogate/providers/common.go index b6ccc973f..5ae49c91b 100644 --- a/plugins/traefik/vendor/github.com/darkweak/souin/pkg/surrogate/providers/common.go +++ b/plugins/traefik/vendor/github.com/darkweak/souin/pkg/surrogate/providers/common.go @@ -189,7 +189,7 @@ func (s *baseStorage) purgeTag(tag string) []string { } // Store will take the lead to store the cache key for each provided Surrogate-key -func (s *baseStorage) Store(response *http.Response, cacheKey string) error { +func (s *baseStorage) Store(response *http.Response, cacheKey, uri, basekey string) error { h := response.Header cacheKey = url.QueryEscape(cacheKey) diff --git a/plugins/traefik/vendor/github.com/darkweak/souin/pkg/surrogate/providers/types.go b/plugins/traefik/vendor/github.com/darkweak/souin/pkg/surrogate/providers/types.go index 31c8e4666..999fe90ff 100644 --- a/plugins/traefik/vendor/github.com/darkweak/souin/pkg/surrogate/providers/types.go +++ b/plugins/traefik/vendor/github.com/darkweak/souin/pkg/surrogate/providers/types.go @@ -16,7 +16,7 @@ type SurrogateInterface interface { Purge(http.Header) (cacheKeys []string, surrogateKeys []string) Invalidate(method string, h http.Header) purgeTag(string) []string - Store(*http.Response, string) error + Store(*http.Response, string, string, string) error storeTag(string, string, *regexp.Regexp) ParseHeaders(string) []string List() map[string]string diff --git a/plugins/traefik/vendor/github.com/darkweak/souin/tests/mock.go b/plugins/traefik/vendor/github.com/darkweak/souin/tests/mock.go deleted file mode 100644 index d88e6f532..000000000 --- a/plugins/traefik/vendor/github.com/darkweak/souin/tests/mock.go +++ /dev/null @@ -1,523 +0,0 @@ -package tests - -import ( - "fmt" - "log" - "os" - - "github.com/darkweak/souin/configurationtypes" - "github.com/darkweak/storages/core" - "go.uber.org/zap" - "go.uber.org/zap/zapcore" - "gopkg.in/yaml.v3" -) - -// DOMAIN is the domain constant -const DOMAIN = "domain.com" - -// PATH is the path constant -const PATH = "/testing" - -type testConfiguration struct { - DefaultCache *configurationtypes.DefaultCache `yaml:"default_cache"` - CacheKeys configurationtypes.CacheKeys `yaml:"cache_keys"` - API configurationtypes.API `yaml:"api"` - ReverseProxyURL string `yaml:"reverse_proxy_url"` - SSLProviders []string `yaml:"ssl_providers"` - URLs map[string]configurationtypes.URL `yaml:"urls"` - LogLevel string `yaml:"log_level"` - logger core.Logger - PluginName string - Ykeys map[string]configurationtypes.SurrogateKeys `yaml:"ykeys"` - SurrogateKeys map[string]configurationtypes.SurrogateKeys `yaml:"surrogate_keys"` -} - -// GetUrls get the urls list in the configuration -func (c *testConfiguration) GetUrls() map[string]configurationtypes.URL { - return c.URLs -} - -// GetReverseProxyURL get the reverse proxy url -func (c *testConfiguration) GetReverseProxyURL() string { - return c.ReverseProxyURL -} - -// GetSSLProviders get the ssl providers -func (c *testConfiguration) GetSSLProviders() []string { - return c.SSLProviders -} - -// GetPluginName get the plugin name -func (c *testConfiguration) GetPluginName() string { - return c.PluginName -} - -// GetDefaultCache get the default cache -func (c *testConfiguration) GetDefaultCache() configurationtypes.DefaultCacheInterface { - return c.DefaultCache -} - -// GetAPI get the default cache -func (c *testConfiguration) GetAPI() configurationtypes.API { - return c.API -} - -// GetLogLevel get the log level -func (c *testConfiguration) GetLogLevel() string { - return c.LogLevel -} - -// GetLogger get the logger -func (c *testConfiguration) GetLogger() core.Logger { - return c.logger -} - -// SetLogger set the logger -func (c *testConfiguration) SetLogger(l core.Logger) { - c.logger = l -} - -// GetYkeys get the ykeys list -func (c *testConfiguration) GetYkeys() map[string]configurationtypes.SurrogateKeys { - return c.Ykeys -} - -// GetSurrogateKeys get the surrogate keys list -func (c *testConfiguration) GetSurrogateKeys() map[string]configurationtypes.SurrogateKeys { - return c.SurrogateKeys -} - -// GetCacheKeys get the cache keys rules to override -func (c *testConfiguration) GetCacheKeys() configurationtypes.CacheKeys { - return c.CacheKeys -} - -var _ configurationtypes.AbstractConfigurationInterface = (*testConfiguration)(nil) - -// BaseConfiguration is the legacy configuration -func BaseConfiguration() string { - return ` -api: - basepath: /souin-api - security: - secret: your_secret_key - enable: true - users: - - username: user1 - password: test - souin: - enable: true -default_cache: - headers: - - Authorization - port: - web: 80 - tls: 443 - regex: - exclude: 'ARegexHere' - ttl: 1000s -reverse_proxy_url: 'http://domain.com:81' -urls: - 'domain.com/': - ttl: 1000s - headers: - - Authorization - 'mysubdomain.domain.com': - ttl: 50s - headers: - - Authorization - - 'Content-Type' -ykeys: - The_First_Test: - headers: - Authorization: '.+' - Content-Type: '.+' - The_Second_Test: - url: 'the/second/.+' - The_Third_Test: -` -} - -// CDNConfiguration is the CDN configuration -func CDNConfiguration() string { - return ` -api: - basepath: /souin-api - souin: - enable: true -default_cache: - ttl: 1000s - cdn: - dynamic: true - strategy: hard -` -} - -// BadgerConfiguration simulate the configuration for the Badger storage -func BadgerConfiguration() string { - return ` -api: - basepath: /souin-api - security: - secret: your_secret_key - enable: true - users: - - username: user1 - password: test - souin: - enable: true -default_cache: - badger: - configuration: - syncWrites: true - readOnly: false - inMemory: false - metricsEnabled: true - distributed: true - headers: - - Authorization - port: - web: 80 - tls: 443 - regex: - exclude: 'ARegexHere' - ttl: 1000s -reverse_proxy_url: 'http://domain.com:81' -ssl_providers: - - traefik -urls: - 'domain.com/': - ttl: 1000s - headers: - - Authorization - 'mysubdomain.domain.com': - ttl: 50s - headers: - - Authorization - - 'Content-Type' -` -} - -// OtterConfiguration simulate the configuration for the Otter storage -func OtterConfiguration() string { - return ` -api: - basepath: /souin-api - security: - secret: your_secret_key - enable: true - users: - - username: user1 - password: test - souin: - enable: true -default_cache: - otter: - configuration: {} - headers: - - Authorization - port: - web: 80 - tls: 443 - regex: - exclude: 'ARegexHere' - ttl: 1000s -reverse_proxy_url: 'http://domain.com:81' -ssl_providers: - - traefik -urls: - 'domain.com/': - ttl: 1000s - headers: - - Authorization - 'mysubdomain.domain.com': - ttl: 50s - headers: - - Authorization - - 'Content-Type' -` -} - -// NutsConfiguration simulate the configuration for the Nuts storage -func NutsConfiguration() string { - return ` -api: - basepath: /souin-api - security: - secret: your_secret_key - enable: true - users: - - username: user1 - password: test - souin: - enable: true -default_cache: - nuts: - path: "./nuts" - headers: - - Authorization - port: - web: 80 - tls: 443 - regex: - exclude: 'ARegexHere' - ttl: 1000s -reverse_proxy_url: 'http://domain.com:81' -ssl_providers: - - traefik -urls: - 'domain.com/': - ttl: 1000s - headers: - - Authorization - 'mysubdomain.domain.com': - ttl: 50s - headers: - - Authorization - - 'Content-Type' -` -} - -// EtcdConfiguration simulate the configuration for the Nuts storage -func EtcdConfiguration() string { - return ` -api: - basepath: /souin-api - security: - secret: your_secret_key - enable: true - users: - - username: user1 - password: test - souin: - enable: true -default_cache: - etcd: - configuration: - endpoints: - - http://etcd:2379 - distributed: true - headers: - - Authorization - port: - web: 80 - tls: 443 - regex: - exclude: 'ARegexHere' - ttl: 1000s -reverse_proxy_url: 'http://domain.com:81' -ssl_providers: - - traefik -urls: - 'domain.com/': - ttl: 1000s - headers: - - Authorization - 'mysubdomain.domain.com': - ttl: 50s - headers: - - Authorization - - 'Content-Type' -` -} - -// RedisConfiguration simulate the configuration for the Nuts storage -func RedisConfiguration() string { - return ` -api: - basepath: /souin-api - security: - secret: your_secret_key - enable: true - users: - - username: user1 - password: test - souin: - enable: true -default_cache: - redis: - url: redis:6379 - distributed: true - headers: - - Authorization - port: - web: 80 - tls: 443 - regex: - exclude: 'ARegexHere' - ttl: 1000s -reverse_proxy_url: 'http://domain.com:81' -ssl_providers: - - traefik -urls: - 'domain.com/': - ttl: 1000s - headers: - - Authorization - 'mysubdomain.domain.com': - ttl: 50s - headers: - - Authorization - - 'Content-Type' -` -} - -func baseEmbeddedOlricConfiguration(path string) string { - return fmt.Sprintf(` -api: - basepath: /souin-api - security: - secret: your_secret_key - enable: true - users: - - username: user1 - password: test - souin: - enable: true -default_cache: - distributed: true - headers: - - Authorization - olric: - %s - port: - web: 80 - tls: 443 - regex: - exclude: 'ARegexHere' - ttl: 1000s -reverse_proxy_url: 'http://domain.com:81' -ssl_providers: - - traefik -urls: - 'domain.com/': - ttl: 1000s - headers: - - Authorization - 'mysubdomain.domain.com': - ttl: 50s - headers: - - Authorization - - 'Content-Type' -`, path) -} - -// OlricConfiguration is the olric included configuration -func OlricConfiguration() string { - return baseEmbeddedOlricConfiguration(fmt.Sprintf("url: '%s'", "olric:3320")) -} - -// EmbeddedOlricPlainConfigurationWithoutAdditionalYAML simulate the configuration for the embedded Olric storage -func EmbeddedOlricPlainConfigurationWithoutAdditionalYAML() string { - return baseEmbeddedOlricConfiguration(` - configuration: - olricd: - bindAddr: "0.0.0.0" - bindPort: 3320 - serializer: "msgpack" - keepAlivePeriod: "20s" - bootstrapTimeout: "5s" - partitionCount: 271 - replicaCount: 2 - writeQuorum: 1 - readQuorum: 1 - readRepair: false - replicationMode: 1 # sync mode. for async, set 1 - tableSize: 1048576 # 1MB in bytes - memberCountQuorum: 1 - - logging: - verbosity: 6 - level: "DEBUG" - output: "stderr" - - memberlist: - environment: "local" - bindAddr: "0.0.0.0" - bindPort: 3322 - enableCompression: false - joinRetryInterval: "10s" - maxJoinAttempts: 2 - - storageEngines: - config: - kvstore: - tableSize: 4096 -`) -} - -// EmbeddedOlricConfiguration is the olric included configuration -func EmbeddedOlricConfiguration() string { - path := "/tmp/olric.yml" - _ = os.WriteFile( - path, - []byte( - ` -olricd: - bindAddr: "0.0.0.0" - bindPort: 3320 - serializer: "msgpack" - keepAlivePeriod: "300s" - bootstrapTimeout: "5s" - partitionCount: 271 - replicaCount: 2 - writeQuorum: 1 - readQuorum: 1 - readRepair: false - replicationMode: 1 # sync mode. for async, set 1 - tableSize: 1048576 # 1MB in bytes - memberCountQuorum: 1 - -logging: - verbosity: 6 - level: "DEBUG" - output: "stderr" - -memberlist: - environment: "local" - bindAddr: "0.0.0.0" - bindPort: 3322 - enableCompression: false - joinRetryInterval: "1s" - maxJoinAttempts: 10 - -storageEngines: - config: - kvstore: - tableSize: 4096 -`), - 0600, - ) - - return baseEmbeddedOlricConfiguration(fmt.Sprintf("path: '%s'", path)) -} - -// MockConfiguration is an helper to mock the configuration -func MockConfiguration(configurationToLoad func() string) *testConfiguration { - var config testConfiguration - if e := yaml.Unmarshal([]byte(configurationToLoad()), &config); e != nil { - log.Fatal(e) - } - cfg := zap.Config{ - Encoding: "json", - Level: zap.NewAtomicLevelAt(zapcore.DebugLevel), - OutputPaths: []string{"stderr"}, - ErrorOutputPaths: []string{"stderr"}, - EncoderConfig: zapcore.EncoderConfig{ - MessageKey: "message", - - LevelKey: "level", - EncodeLevel: zapcore.CapitalLevelEncoder, - - TimeKey: "time", - EncodeTime: zapcore.ISO8601TimeEncoder, - - CallerKey: "caller", - EncodeCaller: zapcore.ShortCallerEncoder, - }, - } - logger, _ := cfg.Build() - config.SetLogger(logger.Sugar()) - - return &config -} diff --git a/plugins/traefik/vendor/modules.txt b/plugins/traefik/vendor/modules.txt index 625170d7d..9fe09e17b 100644 --- a/plugins/traefik/vendor/modules.txt +++ b/plugins/traefik/vendor/modules.txt @@ -80,7 +80,6 @@ github.com/darkweak/souin/pkg/storage github.com/darkweak/souin/pkg/storage/types github.com/darkweak/souin/pkg/surrogate github.com/darkweak/souin/pkg/surrogate/providers -github.com/darkweak/souin/tests # github.com/darkweak/storages/core v0.0.7 ## explicit; go 1.22.1 github.com/darkweak/storages/core diff --git a/plugins/tyk/main.go b/plugins/tyk/main.go index d24beb9a6..acc06f6df 100644 --- a/plugins/tyk/main.go +++ b/plugins/tyk/main.go @@ -190,6 +190,7 @@ func SouinRequestHandler(rw http.ResponseWriter, baseRq *http.Request) { defer s.bufPool.Put(bufPool) if !requestCc.NoCache { validator := rfc.ParseRequest(rq) + var storerName string var fresh, stale *http.Response finalKey := cachedKey if rq.Context().Value(context.Hashed).(bool) { @@ -199,6 +200,7 @@ func SouinRequestHandler(rw http.ResponseWriter, baseRq *http.Request) { fresh, stale = currentStorer.GetMultiLevel(finalKey, rq, validator) if fresh != nil || stale != nil { + storerName = currentStorer.Name() fmt.Printf("Found at least one valid response in the %s storage\n", currentStorer.Name()) break } @@ -206,7 +208,7 @@ func SouinRequestHandler(rw http.ResponseWriter, baseRq *http.Request) { if fresh != nil && (!modeContext.Strict || rfc.ValidateCacheControl(fresh, requestCc)) { response := fresh - rfc.SetCacheStatusHeader(response) + rfc.SetCacheStatusHeader(response, storerName) if rfc.ValidateMaxAgeCachedResponse(requestCc, response) != nil { for hn, hv := range response.Header { rw.Header().Set(hn, strings.Join(hv, ", ")) @@ -220,7 +222,7 @@ func SouinRequestHandler(rw http.ResponseWriter, baseRq *http.Request) { if nil != response && rfc.ValidateCacheControl(response, requestCc) { addTime, _ := time.ParseDuration(response.Header.Get(rfc.StoredTTLHeader)) - rfc.SetCacheStatusHeader(response) + rfc.SetCacheStatusHeader(response, storerName) responseCc, _ := cacheobject.ParseResponseCacheControl(response.Header.Get("Cache-Control")) if responseCc.StaleIfError > 0 { diff --git a/plugins/tyk/override/middleware/middleware.go b/plugins/tyk/override/middleware/middleware.go index e6a66e987..4ef318d5f 100644 --- a/plugins/tyk/override/middleware/middleware.go +++ b/plugins/tyk/override/middleware/middleware.go @@ -272,6 +272,7 @@ func (s *SouinBaseHandler) ServeHTTP(rw http.ResponseWriter, baseRq *http.Reques customWriter := NewCustomWriter(rq, rw, bufPool) if !requestCc.NoCache { validator := rfc.ParseRequest(rq) + var storerName string var response *http.Response var fresh, stale *http.Response finalKey := cachedKey @@ -279,6 +280,7 @@ func (s *SouinBaseHandler) ServeHTTP(rw http.ResponseWriter, baseRq *http.Reques finalKey = fmt.Sprint(xxhash.Sum64String(finalKey)) } for _, currentStorer := range s.Storers { + storerName = currentStorer.Name() fresh, stale = currentStorer.GetMultiLevel(finalKey, rq, validator) if fresh != nil || stale != nil { @@ -289,7 +291,7 @@ func (s *SouinBaseHandler) ServeHTTP(rw http.ResponseWriter, baseRq *http.Reques if rfc.ValidateCacheControl(response, requestCc) { response := fresh - rfc.SetCacheStatusHeader(response) + rfc.SetCacheStatusHeader(response, storerName) if rfc.ValidateMaxAgeCachedResponse(requestCc, response) != nil { customWriter.Headers = response.Header customWriter.statusCode = response.StatusCode @@ -303,7 +305,7 @@ func (s *SouinBaseHandler) ServeHTTP(rw http.ResponseWriter, baseRq *http.Reques if nil != response && rfc.ValidateCacheControl(response, requestCc) { addTime, _ := time.ParseDuration(response.Header.Get(rfc.StoredTTLHeader)) - rfc.SetCacheStatusHeader(response) + rfc.SetCacheStatusHeader(response, storerName) responseCc, _ := cacheobject.ParseResponseCacheControl(response.Header.Get("Cache-Control")) if responseCc.StaleWhileRevalidate > 0 { diff --git a/plugins/webgo/souin_test.go b/plugins/webgo/souin_test.go index 632c1af07..cb3ed2658 100644 --- a/plugins/webgo/souin_test.go +++ b/plugins/webgo/souin_test.go @@ -83,7 +83,7 @@ func Test_SouinWebgoPlugin_Middleware(t *testing.T) { } router.ServeHTTP(res2, req) - if res2.Result().Header.Get("Cache-Status") != "Souin; hit; ttl=4; key=GET-http-example.com-/handled" { + if res2.Result().Header.Get("Cache-Status") != "Souin; hit; ttl=4; key=GET-http-example.com-/handled; detail=DEFAULT" { t.Error("The response must contain a Cache-Status header with the hit and ttl directives.") } if res2.Result().Header.Get("Age") != "1" {