diff --git a/configuration.go b/configuration.go index 6a149c2..df18a10 100644 --- a/configuration.go +++ b/configuration.go @@ -52,6 +52,8 @@ type DefaultCache struct { Timeout configurationtypes.Timeout `json:"timeout"` // Time to live. TTL configurationtypes.Duration `json:"ttl"` + // SimpleFS provider configuration. + SimpleFS configurationtypes.CacheProvider `json:"simplefs"` // Stale time to live. Stale configurationtypes.Duration `json:"stale"` // Disable the coalescing system. @@ -103,7 +105,7 @@ func (d *DefaultCache) GetMode() string { return d.Mode } -// GetNats returns nuts configuration +// GetNats returns nats configuration func (d *DefaultCache) GetNats() configurationtypes.CacheProvider { return d.Nats } @@ -133,6 +135,11 @@ func (d *DefaultCache) GetRegex() configurationtypes.Regex { return d.Regex } +// GetSimpleFS returns simplefs configuration +func (d *DefaultCache) GetSimpleFS() configurationtypes.CacheProvider { + return d.SimpleFS +} + // GetStorers returns the chianed storers func (d *DefaultCache) GetStorers() []string { return d.Storers @@ -291,7 +298,7 @@ func parseRedisConfiguration(c map[string]interface{}) map[string]interface{} { } case "Username", "Password", "ClientName", "ClientSetInfo", "ClientTrackingOptions", "SentinelUsername", "SentinelPassword", "MasterName", "IdentitySuffix": c[k] = v - case "SendToReplicas", "ShuffleInit", "ClientNoTouch", "DisableRetry", "DisableCache", "AlwaysPipelining", "AlwaysRESP2", "ForceSingleClient", "ReplicaOnly", "ClientNoEvict": + case "SendToReplicas", "ShuffleInit", "ClientNoTouch", "DisableRetry", "DisableCache", "AlwaysPipelining", "AlwaysRESP2", "ForceSingleClient", "ReplicaOnly", "ClientNoEvict", "ContextTimeoutEnabled", "PoolFIFO", "ReadOnly", "RouteByLatency", "RouteRandomly", "DisableIndentity": c[k] = true case "SelectDB", "CacheSizeEachConn", "RingScaleEachConn", "ReadBufferEachConn", "WriteBufferEachConn", "BlockingPoolSize", "PipelineMultiplex", "DB", "Protocol", "MaxRetries", "PoolSize", "MinIdleConns", "MaxIdleConns", "MaxActiveConns", "MaxRedirects": if v == false { @@ -303,6 +310,45 @@ func parseRedisConfiguration(c map[string]interface{}) map[string]interface{} { } case "ConnWriteTimeout", "MaxFlushDelay", "MinRetryBackoff", "MaxRetryBackoff", "DialTimeout", "ReadTimeout", "WriteTimeout", "PoolTimeout", "ConnMaxIdleTime", "ConnMaxLifetime": c[k], _ = time.ParseDuration(v.(string)) + case "MaxVersion", "MinVersion": + strV, _ := v.(string) + if strings.HasPrefix(strV, "TLS") { + strV = strings.Trim(strings.TrimPrefix(strV, "TLS"), " ") + } + + switch strV { + case "0x0300", "SSLv3": + c[k] = 0x0300 + case "0x0301", "1.0": + c[k] = 0x0301 + case "0x0302", "1.1": + c[k] = 0x0302 + case "0x0303", "1.2": + c[k] = 0x0303 + case "0x0304", "1.3": + c[k] = 0x0304 + } + case "TLSConfig": + c[k] = parseRedisConfiguration(v.(map[string]interface{})) + } + } + + return c +} + +func parseSimpleFSConfiguration(c map[string]interface{}) map[string]interface{} { + for k, v := range c { + switch k { + case "path": + c[k] = v + case "size": + if v == false { + c[k] = 0 + } else if v == true { + c[k] = 1 + } else { + c[k], _ = strconv.Atoi(v.(string)) + } } } @@ -441,14 +487,20 @@ func parseConfiguration(cfg *Configuration, h *caddyfile.Dispenser, isGlobal boo if len(args) > 0 { cdn.Dynamic, _ = strconv.ParseBool(args[0]) } + case "email": + cdn.Email = h.RemainingArgs()[0] case "hostname": cdn.Hostname = h.RemainingArgs()[0] case "network": cdn.Network = h.RemainingArgs()[0] case "provider": cdn.Provider = h.RemainingArgs()[0] + case "service_id": + cdn.ServiceID = h.RemainingArgs()[0] case "strategy": cdn.Strategy = h.RemainingArgs()[0] + case "zone_id": + cdn.ZoneID = h.RemainingArgs()[0] default: return h.Errf("unsupported cdn directive: %s", directive) } @@ -531,7 +583,7 @@ func parseConfiguration(cfg *Configuration, h *caddyfile.Dispenser, isGlobal boo return h.Errf("unsupported nats directive: %s", directive) } } - cfg.DefaultCache.Nuts = provider + cfg.DefaultCache.Nats = provider case "nuts": provider := configurationtypes.CacheProvider{Found: true} for nesting := h.Nesting(); h.NextBlock(nesting); { @@ -611,6 +663,22 @@ func parseConfiguration(cfg *Configuration, h *caddyfile.Dispenser, isGlobal boo return h.Errf("unsupported regex directive: %s", directive) } } + case "simplefs": + provider := configurationtypes.CacheProvider{Found: true} + for nesting := h.Nesting(); h.NextBlock(nesting); { + directive := h.Val() + switch directive { + case "path": + urlArgs := h.RemainingArgs() + provider.Path = urlArgs[0] + case "configuration": + provider.Configuration = parseCaddyfileRecursively(h) + provider.Configuration = parseSimpleFSConfiguration(provider.Configuration.(map[string]interface{})) + default: + return h.Errf("unsupported simplefs directive: %s", directive) + } + } + cfg.DefaultCache.SimpleFS = provider case "stale": args := h.RemainingArgs() stale, err := time.ParseDuration(args[0]) diff --git a/dispatch.go b/dispatch.go index 2866921..76e5a75 100644 --- a/dispatch.go +++ b/dispatch.go @@ -2,6 +2,7 @@ package httpcache import ( "fmt" + "os" "strings" "github.com/caddyserver/caddy/v2" @@ -73,7 +74,7 @@ func (s *SouinCaddyMiddleware) parseStorages(ctx caddy.Context) { if e != nil { s.logger.Errorf("Error during Nats init, did you include the Nats storage (--with github.com/darkweak/storages/nats/caddy)? %v", e) } else { - s.Configuration.DefaultCache.Nuts.Uuid = fmt.Sprintf("NATS-%s-%s", s.Configuration.DefaultCache.Nats.URL, s.Configuration.DefaultCache.GetStale()) + s.Configuration.DefaultCache.Nats.Uuid = fmt.Sprintf("NATS-%s-%s", s.Configuration.DefaultCache.Nats.URL, s.Configuration.DefaultCache.GetStale()) } } if s.Configuration.DefaultCache.Nuts.Found { @@ -101,7 +102,7 @@ func (s *SouinCaddyMiddleware) parseStorages(ctx caddy.Context) { if e != nil { s.logger.Errorf("Error during Olric init, did you include the Olric storage (--with github.com/darkweak/storages/olric/caddy)? %v", e) } else { - s.Configuration.DefaultCache.Nuts.Uuid = fmt.Sprintf("OLRIC-%s-%s", s.Configuration.DefaultCache.Olric.URL, s.Configuration.DefaultCache.GetStale()) + s.Configuration.DefaultCache.Olric.Uuid = fmt.Sprintf("OLRIC-%s-%s", s.Configuration.DefaultCache.Olric.URL, s.Configuration.DefaultCache.GetStale()) } } if s.Configuration.DefaultCache.Otter.Found { @@ -121,7 +122,7 @@ func (s *SouinCaddyMiddleware) parseStorages(ctx caddy.Context) { address := redis.URL username := "" dbname := "0" - cname := "" + cname := "souin-redis" if c := redis.Configuration; c != nil { p, ok := c.(map[string]interface{}) if ok { @@ -172,4 +173,35 @@ func (s *SouinCaddyMiddleware) parseStorages(ctx caddy.Context) { ) } } + if s.Configuration.DefaultCache.SimpleFS.Found { + e := dispatchStorage(ctx, "simplefs", s.Configuration.DefaultCache.SimpleFS, s.Configuration.DefaultCache.GetStale()) + if e != nil { + s.logger.Errorf("Error during SimpleFS init, did you include the SimpleFS storage (--with github.com/darkweak/storages/simplefs/caddy)? %v", e) + } else { + simplefs := s.Configuration.DefaultCache.SimpleFS + path := simplefs.Path + size := "0" + if c := simplefs.Configuration; c != nil { + p, ok := c.(map[string]interface{}) + if ok { + if d, ok := p["path"]; path == "" && ok { + path = fmt.Sprint(d) + } + if d, ok := p["size"]; ok { + size = fmt.Sprint(d) + } + } + } + + if path == "" { + path, _ = os.Getwd() + } + + s.Configuration.DefaultCache.SimpleFS.Uuid = fmt.Sprintf( + "SIMPLEFS-%s-%s", + path, + size, + ) + } + } } diff --git a/go.mod b/go.mod index b6e324e..210f6cd 100644 --- a/go.mod +++ b/go.mod @@ -4,8 +4,8 @@ go 1.22.1 require ( github.com/caddyserver/caddy/v2 v2.8.4 - github.com/darkweak/souin v1.7.0 - github.com/darkweak/storages/core v0.0.8 + github.com/darkweak/souin v1.7.5 + github.com/darkweak/storages/core v0.0.11 ) require ( diff --git a/go.sum b/go.sum index 5d86c4e..a558a27 100644 --- a/go.sum +++ b/go.sum @@ -109,10 +109,10 @@ github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46t github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/darkweak/go-esi v0.0.5 h1:b9LHI8Tz46R+i6p8avKPHAIBRQUCZDebNmKm5w/Zrns= github.com/darkweak/go-esi v0.0.5/go.mod h1:koCJqwum1u6mslyZuq/Phm6hfG1K3ZK5Y7jrUBTH654= -github.com/darkweak/souin v1.7.0 h1:QeSxwHECzZPlYHTGYDw4xQ6EBJY94f/nfqW4BLc3YQ0= -github.com/darkweak/souin v1.7.0/go.mod h1:XXmhB+QIiZ/lkESd1izzqCI7QWxmX0of01QA+xxgogc= -github.com/darkweak/storages/core v0.0.8 h1:9e7rOxHiJwnvADDVCZ7LFRnUnOHGT+UMpNOFlR8BOiw= -github.com/darkweak/storages/core v0.0.8/go.mod h1:ajTpB9IFLRIRY0EEFLjM5vtsrcNTh+TJK9yRxgG5/wY= +github.com/darkweak/souin v1.7.5 h1:drNhZc0GhSbGcugiGfcYdLDTcx3DCZW6o13wwRj5o5Y= +github.com/darkweak/souin v1.7.5/go.mod h1:PcP+hhvYOdqn4OmeScKKvit0TihYVYS1o154mhfWT/s= +github.com/darkweak/storages/core v0.0.11 h1:IwvpAtkhOmxC5pIffJ8opW6erpTnIi5zqPveiAQs8ew= +github.com/darkweak/storages/core v0.0.11/go.mod h1:ajTpB9IFLRIRY0EEFLjM5vtsrcNTh+TJK9yRxgG5/wY= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= diff --git a/httpcache.go b/httpcache.go index 079d785..11bc0fa 100644 --- a/httpcache.go +++ b/httpcache.go @@ -47,6 +47,8 @@ type SouinCaddyMiddleware struct { Key configurationtypes.Key `json:"key,omitempty"` // Override the cache key generation matching the pattern. CacheKeys configurationtypes.CacheKeys `json:"cache_keys,omitempty"` + // Configure the Nats cache storage. + Nats configurationtypes.CacheProvider `json:"nats,omitempty"` // Configure the Nuts cache storage. Nuts configurationtypes.CacheProvider `json:"nuts,omitempty"` // Configure the Otter cache storage. @@ -61,6 +63,8 @@ type SouinCaddyMiddleware struct { Timeout configurationtypes.Timeout `json:"timeout,omitempty"` // Time to live for a key, using time.duration. TTL configurationtypes.Duration `json:"ttl,omitempty"` + // Configure the SimpleFS cache storage. + SimpleFS configurationtypes.CacheProvider `json:"simplefs,omitempty"` // Time to live for a stale key, using time.duration. Stale configurationtypes.Duration `json:"stale,omitempty"` // Storage providers chaining and order. @@ -90,7 +94,9 @@ func (s *SouinCaddyMiddleware) configurationPropertyMapper() error { if s.Configuration.GetDefaultCache() == nil { defaultCache := DefaultCache{ Badger: s.Badger, + Nats: s.Nats, Nuts: s.Nuts, + SimpleFS: s.SimpleFS, Otter: s.Otter, Key: s.Key, DefaultCacheControl: s.DefaultCacheControl, @@ -115,6 +121,10 @@ func (s *SouinCaddyMiddleware) configurationPropertyMapper() error { return nil } +func isProviderEmpty(c configurationtypes.CacheProvider) bool { + return !c.Found +} + // FromApp to initialize configuration from App structure. func (s *SouinCaddyMiddleware) FromApp(app *SouinApp) error { if s.Configuration.GetDefaultCache() == nil { @@ -198,14 +208,16 @@ func (s *SouinCaddyMiddleware) FromApp(app *SouinApp) error { if dc.CacheName == "" { s.Configuration.DefaultCache.CacheName = appDc.CacheName } - if !s.Configuration.DefaultCache.Distributed && !dc.Olric.Found && !dc.Redis.Found && !dc.Etcd.Found && !dc.Badger.Found && !dc.Nuts.Found && !dc.Otter.Found { + if isProviderEmpty(dc.Badger) && isProviderEmpty(dc.Etcd) && isProviderEmpty(dc.Nats) && isProviderEmpty(dc.Nuts) && isProviderEmpty(dc.Olric) && isProviderEmpty(dc.Otter) && isProviderEmpty(dc.Redis) && isProviderEmpty(dc.SimpleFS) { s.Configuration.DefaultCache.Distributed = appDc.Distributed s.Configuration.DefaultCache.Olric = appDc.Olric s.Configuration.DefaultCache.Redis = appDc.Redis s.Configuration.DefaultCache.Etcd = appDc.Etcd s.Configuration.DefaultCache.Badger = appDc.Badger + s.Configuration.DefaultCache.Nats = appDc.Nats s.Configuration.DefaultCache.Nuts = appDc.Nuts s.Configuration.DefaultCache.Otter = appDc.Otter + s.Configuration.DefaultCache.SimpleFS = appDc.SimpleFS } if dc.Regex.Exclude == "" { s.Configuration.DefaultCache.Regex.Exclude = appDc.Regex.Exclude diff --git a/httpcache_test.go b/httpcache_test.go index c5eed1f..b25a5c6 100644 --- a/httpcache_test.go +++ b/httpcache_test.go @@ -602,6 +602,106 @@ func TestMustRevalidate(t *testing.T) { } } +type staleIfErrorHandler struct { + iterator int +} + +func (t *staleIfErrorHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + if t.iterator > 0 { + w.WriteHeader(http.StatusInternalServerError) + return + } + + t.iterator++ + w.Header().Set("Cache-Control", "stale-if-error=86400") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte("Hello stale-if-error!")) +} + +func TestStaleIfError(t *testing.T) { + tester := caddytest.NewTester(t) + tester.InitServer(` + { + admin localhost:2999 + http_port 9080 + cache { + ttl 5s + stale 5s + } + } + localhost:9080 { + route /stale-if-error { + cache + reverse_proxy localhost:9085 + } + }`, "caddyfile") + + go func() { + staleIfErrorHandler := staleIfErrorHandler{} + _ = http.ListenAndServe(":9085", &staleIfErrorHandler) + }() + time.Sleep(time.Second) + resp1, _ := tester.AssertGetResponse(`http://localhost:9080/stale-if-error`, http.StatusOK, "Hello stale-if-error!") + resp2, _ := tester.AssertGetResponse(`http://localhost:9080/stale-if-error`, http.StatusOK, "Hello stale-if-error!") + + if resp1.Header.Get("Cache-Control") != "stale-if-error=86400" { + t.Errorf("unexpected resp1 Cache-Control header %v", resp1.Header.Get("Cache-Control")) + } + if resp1.Header.Get("Cache-Status") != "Souin; fwd=uri-miss; stored; key=GET-http-localhost:9080-/stale-if-error" { + 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")) + } + + if resp2.Header.Get("Cache-Control") != "stale-if-error=86400" { + 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-/stale-if-error; detail=DEFAULT" { + t.Errorf("unexpected resp2 Cache-Status header %v", resp2.Header.Get("Cache-Status")) + } + if resp2.Header.Get("Age") != "1" { + t.Errorf("unexpected resp2 Age header %v", resp2.Header.Get("Age")) + } + + time.Sleep(6 * time.Second) + staleReq, _ := http.NewRequest(http.MethodGet, "http://localhost:9080/stale-if-error", nil) + staleReq.Header = http.Header{"Cache-Control": []string{"stale-if-error=86400"}} + resp3, _ := tester.AssertResponse(staleReq, http.StatusOK, "Hello stale-if-error!") + + if resp3.Header.Get("Cache-Control") != "stale-if-error=86400" { + 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-/stale-if-error; 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" { + t.Errorf("unexpected resp3 Age header %v", resp3.Header.Get("Age")) + } + + resp4, _ := tester.AssertGetResponse(`http://localhost:9080/stale-if-error`, http.StatusOK, "Hello stale-if-error!") + + if resp4.Header.Get("Cache-Status") != "Souin; hit; ttl=-2; key=GET-http-localhost:9080-/stale-if-error; detail=DEFAULT; fwd=stale; fwd-status=500" && + resp4.Header.Get("Cache-Status") != "Souin; hit; ttl=-3; key=GET-http-localhost:9080-/stale-if-error; detail=DEFAULT; fwd=stale; fwd-status=500" { + t.Errorf("unexpected resp4 Cache-Status header %v", resp4.Header.Get("Cache-Status")) + } + + if resp4.Header.Get("Age") != "7" && resp4.Header.Get("Age") != "8" { + t.Errorf("unexpected resp4 Age header %v", resp4.Header.Get("Age")) + } + + time.Sleep(6 * time.Second) + resp5, _ := tester.AssertGetResponse(`http://localhost:9080/stale-if-error`, http.StatusInternalServerError, "") + + if resp5.Header.Get("Cache-Status") != "Souin; fwd=uri-miss; key=GET-http-localhost:9080-/stale-if-error; detail=UNCACHEABLE-STATUS-CODE" { + t.Errorf("unexpected resp5 Cache-Status header %v", resp5.Header.Get("Cache-Status")) + } + + if resp5.Header.Get("Age") != "" { + t.Errorf("unexpected resp5 Age header %v", resp5.Header.Get("Age")) + } +} + type testETagsHandler struct{} const etagValue = "AAA-BBB" @@ -1086,3 +1186,61 @@ func TestComplexQuery(t *testing.T) { cacheChecker(caddyTester, "fields[]=id&pagination=true", 9) cacheChecker(caddyTester, "fields[]=id&pagination=false", 9) } + +func TestBypassWithExpiresAndRevalidate(t *testing.T) { + tester := caddytest.NewTester(t) + tester.InitServer(` + { + debug + admin localhost:2999 + http_port 9080 + https_port 9443 + cache { + ttl 5s + stale 5s + mode bypass + } + } + localhost:9080 { + route /bypass-with-expires-and-revalidate { + cache + header Expires 0 + header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate" + respond "Hello, expires and revalidate!" + } + }`, "caddyfile") + + respStored1, _ := tester.AssertGetResponse(`http://localhost:9080/bypass-with-expires-and-revalidate`, 200, "Hello, expires and revalidate!") + if respStored1.Header.Get("Cache-Status") != "Souin; fwd=uri-miss; stored; key=GET-http-localhost:9080-/bypass-with-expires-and-revalidate" { + 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")) + } + + respStored2, _ := tester.AssertGetResponse(`http://localhost:9080/bypass-with-expires-and-revalidate`, 200, "Hello, expires and revalidate!") + if respStored2.Header.Get("Cache-Status") != "Souin; hit; ttl=4; key=GET-http-localhost:9080-/bypass-with-expires-and-revalidate; detail=DEFAULT" { + 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") + } + + time.Sleep(5 * time.Second) + respStored3, _ := tester.AssertGetResponse(`http://localhost:9080/bypass-with-expires-and-revalidate`, 200, "Hello, expires and revalidate!") + if respStored3.Header.Get("Cache-Status") != "Souin; hit; ttl=-1; key=GET-http-localhost:9080-/bypass-with-expires-and-revalidate; detail=DEFAULT; fwd=stale" { + t.Errorf("unexpected Cache-Status header value %v", respStored3.Header.Get("Cache-Status")) + } + if respStored3.Header.Get("Age") == "" { + t.Error("Age header should be present") + } + + time.Sleep(5 * time.Second) + respStored4, _ := tester.AssertGetResponse(`http://localhost:9080/bypass-with-expires-and-revalidate`, 200, "Hello, expires and revalidate!") + if respStored4.Header.Get("Cache-Status") != "Souin; fwd=uri-miss; stored; key=GET-http-localhost:9080-/bypass-with-expires-and-revalidate" { + t.Errorf("unexpected Cache-Status header value %v", respStored4.Header.Get("Cache-Status")) + } + if respStored4.Header.Get("Age") != "" { + t.Errorf("unexpected Age header %v", respStored4.Header.Get("Age")) + } +}