diff --git a/README.md b/README.md index f8ff97243..ccc1ab892 100644 --- a/README.md +++ b/README.md @@ -214,6 +214,7 @@ surrogate_keys: | `default_cache.timeout.cache` | The timeout duration to consider the cache provider as unreachable | `10ms` | | `default_cache.ttl` | The TTL duration | `120s` | | `default_cache.default_cache_control` | Set the default value of `Cache-Control` response header if not set by upstream (Souin treats empty `Cache-Control` as `public` if omitted) | `no-store` | +| `default_cache.max_cachable_body_bytes` | Set the maximum size (in bytes) for a response body to be cached (unlimited if omited) | `1048576` (1MB) | | `log_level` | The log level | `One of DEBUG, INFO, WARN, ERROR, DPANIC, PANIC, FATAL it's case insensitive` | | `reverse_proxy_url` | The reverse-proxy's instance URL (Apache, Nginx, Træfik...) | - `http://yourservice` (Container way)
`http://localhost:81` (Local way)
`http://yourdomain.com:81` (Network way) | | `ssl_providers` | List of your providers handling certificates | `- traefik`

`- nginx`

`- apache` | diff --git a/configurationtypes/types.go b/configurationtypes/types.go index d346b0857..4c0c7d02f 100644 --- a/configurationtypes/types.go +++ b/configurationtypes/types.go @@ -233,6 +233,7 @@ type DefaultCache struct { Timeout Timeout `json:"timeout" yaml:"timeout"` TTL Duration `json:"ttl" yaml:"ttl"` DefaultCacheControl string `json:"default_cache_control" yaml:"default_cache_control"` + MaxBodyBytes uint64 `json:"max_cachable_body_bytes" yaml:"max_cachable_body_bytes"` } // GetAllowedHTTPVerbs returns the allowed verbs to cache @@ -325,6 +326,11 @@ func (d *DefaultCache) GetDefaultCacheControl() string { return d.DefaultCacheControl } +// GetMaxBodyBytes returns the default maximum body size (in bytes) for storing into cache +func (d *DefaultCache) GetMaxBodyBytes() uint64 { + return d.MaxBodyBytes +} + // DefaultCacheInterface interface type DefaultCacheInterface interface { GetAllowedHTTPVerbs() []string @@ -345,6 +351,7 @@ type DefaultCacheInterface interface { GetTimeout() Timeout GetTTL() time.Duration GetDefaultCacheControl() string + GetMaxBodyBytes() uint64 } // APIEndpoint is the minimal structure to define an endpoint diff --git a/pkg/middleware/middleware.go b/pkg/middleware/middleware.go index d23aa7eac..c01aafa0a 100644 --- a/pkg/middleware/middleware.go +++ b/pkg/middleware/middleware.go @@ -243,6 +243,12 @@ func (s *SouinBaseHandler) Store( if res.Header.Get("Content-Length") == "" { res.Header.Set("Content-Length", fmt.Sprint(bLen)) } + respBodyMaxSize := int(s.Configuration.GetDefaultCache().GetMaxBodyBytes()) + if respBodyMaxSize > 0 && bLen > respBodyMaxSize { + customWriter.Header().Set("Cache-Status", status+"; detail=UPSTREAM-RESPONSE-TOO-LARGE; key="+rfc.GetCacheKeyFromCtx(rq.Context())) + + return nil + } res.Header.Set(rfc.StoredLengthHeader, res.Header.Get("Content-Length")) response, err := httputil.DumpResponse(&res, true) if err == nil && bLen > 0 { diff --git a/pkg/middleware/writer.go b/pkg/middleware/writer.go index 89a099f3d..d02546b17 100644 --- a/pkg/middleware/writer.go +++ b/pkg/middleware/writer.go @@ -37,7 +37,6 @@ type CustomWriter struct { headersSent bool mutex *sync.Mutex statusCode int - // size int } // Header will write the response headers diff --git a/plugins/caddy/README.md b/plugins/caddy/README.md index 4553d9471..23852eb6f 100644 --- a/plugins/caddy/README.md +++ b/plugins/caddy/README.md @@ -379,6 +379,7 @@ What does these directives mean? | `cdn.service_id` | The service id if required, depending the provider | `123456_id` | | `cdn.zone_id` | The zone id if required, depending the provider | `anywhere_zone` | | `default_cache_control` | Set the default value of `Cache-Control` response header if not set by upstream (Souin treats empty `Cache-Control` as `public` if omitted) | `no-store` | +| `max_cachable_body_bytes` | Set the maximum size (in bytes) for a response body to be cached (unlimited if omited) | `1048576` (1MB) | | `key` | Override the key generation with the ability to disable unecessary parts | | | `key.disable_body` | Disable the body part in the key (GraphQL context) | `true`

`(default: false)` | | `key.disable_host` | Disable the host part in the key | `true`

`(default: false)` | diff --git a/plugins/caddy/configuration.go b/plugins/caddy/configuration.go index bc2827ccf..2cc033612 100644 --- a/plugins/caddy/configuration.go +++ b/plugins/caddy/configuration.go @@ -22,6 +22,8 @@ type DefaultCache struct { CDN configurationtypes.CDN `json:"cdn"` // The default Cache-Control header value if none set by the upstream server. DefaultCacheControl string `json:"default_cache_control"` + // The maximum body size (in bytes) to be stored into cache. + MaxBodyBytes uint64 `json:"max_cachable_body_bytes"` // Redis provider configuration. Distributed bool `json:"distributed"` // Headers to add to the cache key if they are present. @@ -140,6 +142,11 @@ func (d *DefaultCache) GetDefaultCacheControl() string { return d.DefaultCacheControl } +// GetMaxBodyBytes returns the maximum body size (in bytes) to be cached +func (d *DefaultCache) GetMaxBodyBytes() uint64 { + return d.MaxBodyBytes +} + // Configuration holder type Configuration struct { // Default cache to fallback on when none are redefined. @@ -412,6 +419,14 @@ func parseConfiguration(cfg *Configuration, h *caddyfile.Dispenser, isGlobal boo case "default_cache_control": args := h.RemainingArgs() cfg.DefaultCache.DefaultCacheControl = strings.Join(args, " ") + case "max_cachable_body_bytes": + args := h.RemainingArgs() + maxBodyBytes, err := strconv.ParseUint(args[0], 10, 64) + if err != nil { + return h.Errf("unsupported max_cachable_body_bytes: %s", args) + } else { + cfg.DefaultCache.MaxBodyBytes = maxBodyBytes + } case "etcd": cfg.DefaultCache.Distributed = true provider := configurationtypes.CacheProvider{} diff --git a/plugins/caddy/httpcache.go b/plugins/caddy/httpcache.go index 12c014f88..fcf48a3cd 100644 --- a/plugins/caddy/httpcache.go +++ b/plugins/caddy/httpcache.go @@ -192,6 +192,9 @@ func (s *SouinCaddyMiddleware) FromApp(app *SouinApp) error { if dc.DefaultCacheControl == "" { s.Configuration.DefaultCache.DefaultCacheControl = appDc.DefaultCacheControl } + if dc.MaxBodyBytes == 0 { + s.Configuration.DefaultCache.MaxBodyBytes = appDc.MaxBodyBytes + } if dc.CacheName == "" { s.Configuration.DefaultCache.CacheName = appDc.CacheName } diff --git a/plugins/caddy/httpcache_test.go b/plugins/caddy/httpcache_test.go index b2c12685e..b9f46fc43 100644 --- a/plugins/caddy/httpcache_test.go +++ b/plugins/caddy/httpcache_test.go @@ -294,6 +294,62 @@ func TestNotHandledRoute(t *testing.T) { } } +func TestMaxBodyByte(t *testing.T) { + tester := caddytest.NewTester(t) + tester.InitServer(` + { + admin localhost:2999 + http_port 9080 + https_port 9443 + cache { + ttl 5s + max_cachable_body_bytes 30 + } + } + localhost:9080 { + route /max-body-bytes-stored { + cache + respond "Hello, Max body bytes stored!" + } + route /max-body-bytes-not-stored { + cache + respond "Hello, Max body bytes not stored due to the response length!" + } + }`, "caddyfile") + + respStored1, _ := tester.AssertGetResponse(`http://localhost:9080/max-body-bytes-stored`, 200, "Hello, Max body bytes stored!") + respStored2, _ := tester.AssertGetResponse(`http://localhost:9080/max-body-bytes-stored`, 200, "Hello, Max body bytes stored!") + if respStored1.Header.Get("Cache-Status") != "Souin; fwd=uri-miss; stored; key=GET-http-localhost:9080-/max-body-bytes-stored" { + t.Errorf("unexpected Cache-Status header value %v", respStored1.Header.Get("Cache-Status")) + } + if respStored1.Header.Get("Age") != "" { + 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" { + t.Errorf("unexpected Cache-Status header value %v", respStored2.Header.Get("Cache-Status")) + } + if respStored2.Header.Get("Age") == "" { + t.Error("Age header should be present") + } + + respNotStored1, _ := tester.AssertGetResponse(`http://localhost:9080/max-body-bytes-not-stored`, 200, "Hello, Max body bytes not stored due to the response length!") + respNotStored2, _ := tester.AssertGetResponse(`http://localhost:9080/max-body-bytes-not-stored`, 200, "Hello, Max body bytes not stored due to the response length!") + if respNotStored1.Header.Get("Cache-Status") != "Souin; fwd=uri-miss; detail=UPSTREAM-RESPONSE-TOO-LARGE; key=GET-http-localhost:9080-/max-body-bytes-not-stored" { + t.Errorf("unexpected Cache-Status header value %v", respNotStored1.Header.Get("Cache-Status")) + } + if respNotStored1.Header.Get("Age") != "" { + t.Errorf("unexpected Age header %v", respNotStored1.Header.Get("Age")) + } + + if respNotStored2.Header.Get("Cache-Status") != "Souin; fwd=uri-miss; detail=UPSTREAM-RESPONSE-TOO-LARGE; key=GET-http-localhost:9080-/max-body-bytes-not-stored" { + t.Errorf("unexpected Cache-Status header value %v", respNotStored2.Header.Get("Cache-Status")) + } + if respNotStored2.Header.Get("Age") != "" { + t.Errorf("unexpected Age header %v", respNotStored2.Header.Get("Age")) + } +} + func TestMultiProvider(t *testing.T) { var wg sync.WaitGroup var responses []*http.Response diff --git a/plugins/kratos/configuration.go b/plugins/kratos/configuration.go index ea46eca6e..66e9975eb 100644 --- a/plugins/kratos/configuration.go +++ b/plugins/kratos/configuration.go @@ -311,6 +311,11 @@ func parseDefaultCache(dcConfiguration map[string]config.Value) *configurationty } case "default_cache_control": dc.DefaultCacheControl, _ = defaultCacheV.String() + case "max_cachable_body_bytes": + mbb, ok := defaultCacheV.Load().(uint64) + if ok { + dc.MaxBodyBytes = mbb + } } } diff --git a/plugins/souin/agnostic/configuration_parser.go b/plugins/souin/agnostic/configuration_parser.go index 149967cec..c5bc346ad 100644 --- a/plugins/souin/agnostic/configuration_parser.go +++ b/plugins/souin/agnostic/configuration_parser.go @@ -270,6 +270,8 @@ func parseDefaultCache(dcConfiguration map[string]interface{}) *configurationtyp } case "default_cache_control": dc.DefaultCacheControl, _ = defaultCacheV.(string) + case "max_cachable_body_bytes": + dc.MaxBodyBytes, _ = defaultCacheV.(uint64) } } diff --git a/plugins/traefik/main.go b/plugins/traefik/main.go index 130c1fb3c..bbda78b9f 100644 --- a/plugins/traefik/main.go +++ b/plugins/traefik/main.go @@ -203,6 +203,8 @@ func parseConfiguration(c map[string]interface{}) Configuration { dc.Storers = parseStringSlice(defaultCacheV) case "default_cache_control": dc.DefaultCacheControl = defaultCacheV.(string) + case "max_cachable_body_bytes": + dc.MaxBodyBytes, _ = defaultCacheV.(uint64) } } configuration.DefaultCache = &dc diff --git a/plugins/traefik/vendor/github.com/darkweak/souin/configurationtypes/types.go b/plugins/traefik/vendor/github.com/darkweak/souin/configurationtypes/types.go index d346b0857..4c0c7d02f 100644 --- a/plugins/traefik/vendor/github.com/darkweak/souin/configurationtypes/types.go +++ b/plugins/traefik/vendor/github.com/darkweak/souin/configurationtypes/types.go @@ -233,6 +233,7 @@ type DefaultCache struct { Timeout Timeout `json:"timeout" yaml:"timeout"` TTL Duration `json:"ttl" yaml:"ttl"` DefaultCacheControl string `json:"default_cache_control" yaml:"default_cache_control"` + MaxBodyBytes uint64 `json:"max_cachable_body_bytes" yaml:"max_cachable_body_bytes"` } // GetAllowedHTTPVerbs returns the allowed verbs to cache @@ -325,6 +326,11 @@ func (d *DefaultCache) GetDefaultCacheControl() string { return d.DefaultCacheControl } +// GetMaxBodyBytes returns the default maximum body size (in bytes) for storing into cache +func (d *DefaultCache) GetMaxBodyBytes() uint64 { + return d.MaxBodyBytes +} + // DefaultCacheInterface interface type DefaultCacheInterface interface { GetAllowedHTTPVerbs() []string @@ -345,6 +351,7 @@ type DefaultCacheInterface interface { GetTimeout() Timeout GetTTL() time.Duration GetDefaultCacheControl() string + GetMaxBodyBytes() uint64 } // APIEndpoint is the minimal structure to define an endpoint