diff --git a/configurationtypes/types.go b/configurationtypes/types.go index 8b5d5d8c5..a90848df2 100644 --- a/configurationtypes/types.go +++ b/configurationtypes/types.go @@ -55,6 +55,9 @@ func (c *CacheKeys) parseJSON(rootDecoder *json.Decoder) { case "disable_vary": val, _ := rootDecoder.Token() key.DisableVary, _ = strconv.ParseBool(fmt.Sprint(val)) + case "sort_query": + val, _ := rootDecoder.Token() + key.SortQuery, _ = strconv.ParseBool(fmt.Sprint(val)) case "hash": val, _ := rootDecoder.Token() key.Hash, _ = strconv.ParseBool(fmt.Sprint(val)) @@ -246,6 +249,7 @@ type Key struct { DisableQuery bool `json:"disable_query,omitempty" yaml:"disable_query,omitempty"` DisableScheme bool `json:"disable_scheme,omitempty" yaml:"disable_scheme,omitempty"` DisableVary bool `json:"disable_vary,omitempty" yaml:"disable_vary,omitempty"` + SortQuery bool `json:"sort_query,omitempty" yaml:"sort_query,omitempty"` Hash bool `json:"hash,omitempty" yaml:"hash,omitempty"` Hide bool `json:"hide,omitempty" yaml:"hide,omitempty"` Template string `json:"template,omitempty" yaml:"template,omitempty"` diff --git a/context/key.go b/context/key.go index ab0e99dbe..92ed21a87 100644 --- a/context/key.go +++ b/context/key.go @@ -3,7 +3,9 @@ package context import ( "context" "net/http" + "net/url" "regexp" + "sort" "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/modules/caddyhttp" @@ -23,6 +25,7 @@ type keyContext struct { disable_host bool disable_method bool disable_query bool + sort_query bool disable_vary bool disable_scheme bool displayable bool @@ -44,6 +47,7 @@ func (g *keyContext) SetupContext(c configurationtypes.AbstractConfigurationInte g.disable_host = k.DisableHost g.disable_method = k.DisableMethod g.disable_query = k.DisableQuery + g.sort_query = k.SortQuery g.disable_scheme = k.DisableScheme g.disable_vary = k.DisableVary g.hash = k.Hash @@ -60,6 +64,7 @@ func (g *keyContext) SetupContext(c configurationtypes.AbstractConfigurationInte disable_host: v.DisableHost, disable_method: v.DisableMethod, disable_query: v.DisableQuery, + sort_query: v.SortQuery, disable_scheme: v.DisableScheme, disable_vary: v.DisableVary, hash: v.Hash, @@ -89,7 +94,17 @@ func parseKeyInformations(req *http.Request, kCtx keyContext) (query, body, host hash = kCtx.hash if !kCtx.disable_query && len(req.URL.RawQuery) > 0 { - query += "?" + req.URL.RawQuery + queryPart := req.URL.RawQuery + + if kCtx.sort_query { + v, _ := url.ParseQuery(req.URL.RawQuery) + for _, values := range v { + sort.Strings(values) + } + queryPart = v.Encode() + } + + query += "?" + queryPart } if !kCtx.disable_body { diff --git a/context/key_test.go b/context/key_test.go index b0a72fb52..91bfe5b04 100644 --- a/context/key_test.go +++ b/context/key_test.go @@ -146,4 +146,25 @@ func Test_KeyContext_SetContext(t *testing.T) { t.Errorf("The Key context must be equal to GET-http-domain.com-/matched?query=string, %s given.", req6.Context().Value(Key).(string)) } + // Added tests for sort_query + ctx6 := keyContext{ + sort_query: true, + disable_query: false, + disable_method: false, + disable_host: false, + initializer: func(r *http.Request) *http.Request { + return r.WithContext(context.WithValue(r.Context(), caddy.ReplacerCtxKey, caddy.NewReplacer())) + }, + } + req7 := httptest.NewRequest(http.MethodGet, "http://domain.com/matched?b=2&a=1", nil) + req7 = ctx6.SetContext(req7.WithContext(context.WithValue(req7.Context(), HashBody, ""))) + if req7.Context().Value(Key).(string) != "GET-http-domain.com-/matched?a=1&b=2" { + t.Errorf("The Key context must be equal to GET-http-domain.com-/matched?a=1&b=2, %s given.", req7.Context().Value(Key).(string)) + } + + req8 := httptest.NewRequest(http.MethodGet, "http://domain.com/matched?word=beta&word=alpha", nil) + req8 = ctx6.SetContext(req8.WithContext(context.WithValue(req8.Context(), HashBody, ""))) + if req8.Context().Value(Key).(string) != "GET-http-domain.com-/matched?word=alpha&word=beta" { + t.Errorf("The Key context must be equal to GET-http-domain.com-/matched?word=alpha&word=beta, %s given.", req8.Context().Value(Key).(string)) + } } diff --git a/docs/website/content/docs/configuration.md b/docs/website/content/docs/configuration.md index 23b707b7a..fc077e9e4 100644 --- a/docs/website/content/docs/configuration.md +++ b/docs/website/content/docs/configuration.md @@ -70,6 +70,9 @@ default: `false` * **disable_query**: Prevent the URL query to be part of the generated key. default: `false` +* **sort_query**: Sort the query string parameters alphabetically by name, and parameters with the same name will be sorted by their values. This ensures consistent cache keys regardless of query parameter order. +default: `false` + * **disable_scheme**: Prevent the scheme to be part of the generated key. default: `false` @@ -163,6 +166,9 @@ default: `false` * **disable_query**: Prevent the URL query to be part of the generated key. default: `false` +* **sort_query**: Sort the query string parameters alphabetically by name, and parameters with the same name will be sorted by their values. This ensures consistent cache keys regardless of query parameter order. +default: `false` + * **disable_scheme**: Prevent the scheme to be part of the generated key. default: `false` diff --git a/plugins/caddy/configuration.go b/plugins/caddy/configuration.go index a4a774b79..4b1f4b0c5 100644 --- a/plugins/caddy/configuration.go +++ b/plugins/caddy/configuration.go @@ -497,6 +497,8 @@ func parseConfiguration(cfg *Configuration, h *caddyfile.Dispenser, isGlobal boo ck.DisableMethod = true case "disable_query": ck.DisableQuery = true + case "sort_query": + ck.SortQuery = true case "disable_scheme": ck.DisableScheme = true case "disable_vary": @@ -593,6 +595,8 @@ func parseConfiguration(cfg *Configuration, h *caddyfile.Dispenser, isGlobal boo config_key.DisableMethod = true case "disable_query": config_key.DisableQuery = true + case "sort_query": + config_key.SortQuery = true case "disable_scheme": config_key.DisableScheme = true case "disable_vary": diff --git a/plugins/caddy/httpcache_test.go b/plugins/caddy/httpcache_test.go index 8577b3b74..57c5eae22 100644 --- a/plugins/caddy/httpcache_test.go +++ b/plugins/caddy/httpcache_test.go @@ -116,6 +116,40 @@ func TestQueryString(t *testing.T) { } } +func TestQueryStringSort(t *testing.T) { + tester := caddytest.NewTester(t) + tester.InitServer(` + { + admin localhost:2999 + http_port 9080 + https_port 9443 + cache { + key { + sort_query + } + } + } + localhost:9080 { + route /query-string-sort { + cache + respond "Hello, query string sort!" + } + }`, "caddyfile") + + resp1, _ := tester.AssertGetResponse(`http://localhost:9080/query-string-sort?b=2&a=1`, 200, "Hello, query string sort!") + if resp1.Header.Get("Cache-Status") != "Souin; fwd=uri-miss; stored; key=GET-http-localhost:9080-/query-string-sort?a=1&b=2" { + t.Errorf("unexpected Cache-Status header %v", resp1.Header.Get("Cache-Status")) + } + + resp2, _ := tester.AssertGetResponse(`http://localhost:9080/query-string-sort?a=1&b=2`, 200, "Hello, query string sort!") + compareHit(t, resp2.Header, "GET-http-localhost:9080-/query-string-sort?a=1&b=2", "DEFAULT", 119) + + resp3, _ := tester.AssertGetResponse(`http://localhost:9080/query-string-sort?word=beta&word=alpha`, 200, "Hello, query string sort!") + if resp3.Header.Get("Cache-Status") != "Souin; fwd=uri-miss; stored; key=GET-http-localhost:9080-/query-string-sort?word=alpha&word=beta" { + t.Errorf("unexpected Cache-Status header %v", resp3.Header.Get("Cache-Status")) + } +} + func TestMaxAge(t *testing.T) { tester := caddytest.NewTester(t) tester.InitServer(` diff --git a/plugins/kratos/configuration.go b/plugins/kratos/configuration.go index 9ea290ac8..e7893bf0a 100644 --- a/plugins/kratos/configuration.go +++ b/plugins/kratos/configuration.go @@ -103,6 +103,8 @@ func parseCacheKeys(ccConfiguration map[string]config.Value) configurationtypes. ck.DisableMethod = true case "disable_query": ck.DisableQuery = true + case "sort_query": + ck.SortQuery = true case "disable_scheme": ck.DisableScheme = true case "disable_vary": diff --git a/plugins/souin/agnostic/configuration_parser.go b/plugins/souin/agnostic/configuration_parser.go index be3426d1a..a94432309 100644 --- a/plugins/souin/agnostic/configuration_parser.go +++ b/plugins/souin/agnostic/configuration_parser.go @@ -69,6 +69,8 @@ func parseCacheKeys(ccConfiguration map[string]interface{}) configurationtypes.C ck.DisableMethod = true case "disable_query": ck.DisableQuery = true + case "sort_query": + ck.SortQuery = true case "disable_scheme": ck.DisableScheme = true case "disable_vary": diff --git a/plugins/traefik/main.go b/plugins/traefik/main.go index f2430963d..abda0d69d 100644 --- a/plugins/traefik/main.go +++ b/plugins/traefik/main.go @@ -41,6 +41,8 @@ func configCacheKey(keyConfiguration map[string]interface{}) configurationtypes. key.DisableMethod = cast.ToBool(keyV) case "disable_query": key.DisableQuery = cast.ToBool(keyV) + case "sort_query": + key.SortQuery = cast.ToBool(keyV) case "disable_scheme": key.DisableScheme = cast.ToBool(keyV) case "disable_vary": diff --git a/plugins/traefik/override/configurationtypes/types.go b/plugins/traefik/override/configurationtypes/types.go index e03ed7f93..b2a1b2a03 100644 --- a/plugins/traefik/override/configurationtypes/types.go +++ b/plugins/traefik/override/configurationtypes/types.go @@ -48,6 +48,9 @@ func (c *CacheKeys) parseJSON(rootDecoder *json.Decoder) { case "disable_query": val, _ := rootDecoder.Token() key.DisableQuery, _ = strconv.ParseBool(fmt.Sprint(val)) + case "sort_query": + val, _ := rootDecoder.Token() + key.SortQuery, _ = strconv.ParseBool(fmt.Sprint(val)) case "disable_scheme": val, _ := rootDecoder.Token() key.DisableScheme, _ = strconv.ParseBool(fmt.Sprint(val)) @@ -223,6 +226,7 @@ type Key struct { DisableHost bool `json:"disable_host,omitempty" yaml:"disable_host,omitempty"` DisableMethod bool `json:"disable_method,omitempty" yaml:"disable_method,omitempty"` DisableQuery bool `json:"disable_query,omitempty" yaml:"disable_query,omitempty"` + SortQuery bool `json:"sort_query,omitempty" yaml:"sort_query,omitempty"` DisableScheme bool `json:"disable_scheme,omitempty" yaml:"disable_scheme,omitempty"` DisableVary bool `json:"disable_vary,omitempty" yaml:"disable_vary,omitempty"` Hash bool `json:"hash,omitempty" yaml:"hash,omitempty"` diff --git a/plugins/traefik/override/context/key.go b/plugins/traefik/override/context/key.go index d4edc3a85..9d4d27197 100644 --- a/plugins/traefik/override/context/key.go +++ b/plugins/traefik/override/context/key.go @@ -3,7 +3,9 @@ package context import ( "context" "net/http" + "net/url" "regexp" + "sort" "github.com/darkweak/souin/configurationtypes" ) @@ -20,6 +22,7 @@ type keyContext struct { disable_host bool disable_method bool disable_query bool + sort_query bool disable_scheme bool disable_vary bool displayable bool @@ -41,6 +44,7 @@ func (g *keyContext) SetupContext(c configurationtypes.AbstractConfigurationInte g.disable_host = k.DisableHost g.disable_method = k.DisableMethod g.disable_query = k.DisableQuery + g.sort_query = k.SortQuery g.disable_scheme = k.DisableScheme g.disable_vary = k.DisableVary g.hash = k.Hash @@ -76,7 +80,15 @@ func parseKeyInformations(req *http.Request, kCtx keyContext) (query, body, host hash = kCtx.hash if !kCtx.disable_query && len(req.URL.RawQuery) > 0 { - query += "?" + req.URL.RawQuery + if kCtx.sort_query { + v, _ := url.ParseQuery(req.URL.RawQuery) + for _, values := range v { + sort.Strings(values) + } + query += "?" + v.Encode() + } else { + query += "?" + req.URL.RawQuery + } } if !kCtx.disable_body { 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 e03ed7f93..24ea205f3 100644 --- a/plugins/traefik/vendor/github.com/darkweak/souin/configurationtypes/types.go +++ b/plugins/traefik/vendor/github.com/darkweak/souin/configurationtypes/types.go @@ -223,6 +223,7 @@ type Key struct { DisableHost bool `json:"disable_host,omitempty" yaml:"disable_host,omitempty"` DisableMethod bool `json:"disable_method,omitempty" yaml:"disable_method,omitempty"` DisableQuery bool `json:"disable_query,omitempty" yaml:"disable_query,omitempty"` + SortQuery bool `json:"sort_query,omitempty" yaml:"sort_query,omitempty"` DisableScheme bool `json:"disable_scheme,omitempty" yaml:"disable_scheme,omitempty"` DisableVary bool `json:"disable_vary,omitempty" yaml:"disable_vary,omitempty"` Hash bool `json:"hash,omitempty" yaml:"hash,omitempty"` diff --git a/plugins/traefik/vendor/github.com/darkweak/souin/context/key.go b/plugins/traefik/vendor/github.com/darkweak/souin/context/key.go index d4edc3a85..9d4d27197 100644 --- a/plugins/traefik/vendor/github.com/darkweak/souin/context/key.go +++ b/plugins/traefik/vendor/github.com/darkweak/souin/context/key.go @@ -3,7 +3,9 @@ package context import ( "context" "net/http" + "net/url" "regexp" + "sort" "github.com/darkweak/souin/configurationtypes" ) @@ -20,6 +22,7 @@ type keyContext struct { disable_host bool disable_method bool disable_query bool + sort_query bool disable_scheme bool disable_vary bool displayable bool @@ -41,6 +44,7 @@ func (g *keyContext) SetupContext(c configurationtypes.AbstractConfigurationInte g.disable_host = k.DisableHost g.disable_method = k.DisableMethod g.disable_query = k.DisableQuery + g.sort_query = k.SortQuery g.disable_scheme = k.DisableScheme g.disable_vary = k.DisableVary g.hash = k.Hash @@ -76,7 +80,15 @@ func parseKeyInformations(req *http.Request, kCtx keyContext) (query, body, host hash = kCtx.hash if !kCtx.disable_query && len(req.URL.RawQuery) > 0 { - query += "?" + req.URL.RawQuery + if kCtx.sort_query { + v, _ := url.ParseQuery(req.URL.RawQuery) + for _, values := range v { + sort.Strings(values) + } + query += "?" + v.Encode() + } else { + query += "?" + req.URL.RawQuery + } } if !kCtx.disable_body {