From a3125c8276b76b88b63e26b2bff2f845034776ee Mon Sep 17 00:00:00 2001 From: ryan Date: Sun, 19 Jul 2026 18:10:51 +0800 Subject: [PATCH] feat(openresty): merge global default limits at route render --- pkg/render/openresty/render.go | 38 ++++++++- pkg/render/openresty/render_test.go | 123 ++++++++++++++++++++++++++++ pkg/render/openresty/types.go | 3 + 3 files changed, 163 insertions(+), 1 deletion(-) diff --git a/pkg/render/openresty/render.go b/pkg/render/openresty/render.go index 8204bb27..e17f1963 100644 --- a/pkg/render/openresty/render.go +++ b/pkg/render/openresty/render.go @@ -94,7 +94,7 @@ func RenderRouteConfig(doc Document, certificateFiles []SupportFile) (string, er serverNames := renderServerNames(domains) displayName := resolveRouteSiteName(route) cacheConfig := routeCacheConfig{Enabled: route.CacheEnabled, Policy: route.CachePolicy, Rules: route.CacheRules} - limitConfig := routeLimitConfig{LimitConnPerServer: route.LimitConnPerServer, LimitConnPerIP: route.LimitConnPerIP, LimitRate: route.LimitRate} + limitConfig := mergeRouteLimitConfig(route, doc.OpenRestyConfig) powEnabled, _ := getPoWConfigForRoute(route.ID, doc.WAF) if normalizeRouteUpstreamType(route.UpstreamType) == routeUpstreamTypePages { if err := renderPagesRoute(&builder, route, displayName, serverNames, certificates, limitConfig, powEnabled, doc.OpenRestyConfig); err != nil { @@ -478,6 +478,42 @@ func renderRouteLimitBlock(limitConfig routeLimitConfig) string { return builder.String() } +func mergeRouteLimitConfig(route Route, cfg ConfigSnapshot) routeLimitConfig { + return routeLimitConfig{ + LimitConnPerServer: mergeLimitConn(route.LimitConnPerServer, cfg.DefaultLimitConnPerServer), + LimitConnPerIP: mergeLimitConn(route.LimitConnPerIP, cfg.DefaultLimitConnPerIP), + LimitRate: mergeLimitRate(route.LimitRate, cfg.DefaultLimitRate), + } +} + +func mergeLimitConn(route, def int) int { + if route == -1 { + return 0 + } + if route > 0 { + return route + } + if def > 0 { + return def + } + return 0 +} + +func mergeLimitRate(route, def string) string { + r := strings.ToLower(strings.TrimSpace(route)) + if r == "-1" { + return "" + } + if r != "" && r != "0" { + return r + } + d := strings.ToLower(strings.TrimSpace(def)) + if d != "" && d != "0" { + return d + } + return "" +} + func renderRouteCachePolicyCondition(cacheConfig routeCacheConfig) string { policy := normalizeRenderCachePolicy(cacheConfig.Policy) switch policy { diff --git a/pkg/render/openresty/render_test.go b/pkg/render/openresty/render_test.go index bcf605bf..d6dcfa7a 100644 --- a/pkg/render/openresty/render_test.go +++ b/pkg/render/openresty/render_test.go @@ -442,3 +442,126 @@ func TestRenderRouteCacheBlockIncludesStaticWhenEnabled(t *testing.T) { t.Fatalf("expected security bypass for non-GET, got:\n%s", block) } } + +func TestMergeRouteLimitConfig(t *testing.T) { + t.Parallel() + cases := []struct { + name string + route Route + cfg ConfigSnapshot + want routeLimitConfig + }{ + { + name: "both zero off", + route: Route{}, + cfg: ConfigSnapshot{}, + want: routeLimitConfig{}, + }, + { + name: "inherit all defaults", + route: Route{}, + cfg: ConfigSnapshot{ + DefaultLimitConnPerServer: 100, + DefaultLimitConnPerIP: 10, + DefaultLimitRate: "512k", + }, + want: routeLimitConfig{LimitConnPerServer: 100, LimitConnPerIP: 10, LimitRate: "512k"}, + }, + { + name: "explicit off ignores default", + route: Route{LimitConnPerServer: -1, LimitConnPerIP: -1, LimitRate: "-1"}, + cfg: ConfigSnapshot{ + DefaultLimitConnPerServer: 100, + DefaultLimitConnPerIP: 10, + DefaultLimitRate: "512k", + }, + want: routeLimitConfig{}, + }, + { + name: "route overrides default", + route: Route{LimitConnPerServer: 50, LimitConnPerIP: 5, LimitRate: "1m"}, + cfg: ConfigSnapshot{ + DefaultLimitConnPerServer: 100, + DefaultLimitConnPerIP: 10, + DefaultLimitRate: "512k", + }, + want: routeLimitConfig{LimitConnPerServer: 50, LimitConnPerIP: 5, LimitRate: "1m"}, + }, + { + name: "partial inherit", + route: Route{LimitConnPerServer: 0, LimitConnPerIP: -1, LimitRate: ""}, + cfg: ConfigSnapshot{ + DefaultLimitConnPerServer: 100, + DefaultLimitConnPerIP: 10, + DefaultLimitRate: "256k", + }, + want: routeLimitConfig{LimitConnPerServer: 100, LimitConnPerIP: 0, LimitRate: "256k"}, + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + got := mergeRouteLimitConfig(tc.route, tc.cfg) + if got != tc.want { + t.Fatalf("mergeRouteLimitConfig() = %#v, want %#v", got, tc.want) + } + }) + } +} + +func TestRenderRouteConfigAppliesDefaultLimits(t *testing.T) { + doc := Document{ + Routes: []Route{{ + SiteName: "example.com", + Domains: []string{"example.com"}, + Enabled: true, + OriginURL: "http://127.0.0.1:8080", + Upstreams: []string{"http://127.0.0.1:8080"}, + }}, + OpenRestyConfig: ConfigSnapshot{ + DefaultLimitConnPerServer: 120, + DefaultLimitConnPerIP: 12, + DefaultLimitRate: "512k", + }, + } + rendered, err := RenderRouteConfig(doc, nil) + if err != nil { + t.Fatalf("RenderRouteConfig() error = %v", err) + } + for _, want := range []string{ + "limit_conn openflare_conn_per_server 120;", + "limit_conn openflare_conn_per_ip 12;", + "limit_rate 512k;", + } { + if !strings.Contains(rendered, want) { + t.Fatalf("expected %q in route config, got:\n%s", want, rendered) + } + } +} + +func TestRenderRouteConfigExplicitOffSkipsDefaultLimits(t *testing.T) { + doc := Document{ + Routes: []Route{{ + SiteName: "example.com", + Domains: []string{"example.com"}, + Enabled: true, + OriginURL: "http://127.0.0.1:8080", + Upstreams: []string{"http://127.0.0.1:8080"}, + LimitConnPerServer: -1, + LimitConnPerIP: -1, + LimitRate: "-1", + }}, + OpenRestyConfig: ConfigSnapshot{ + DefaultLimitConnPerServer: 120, + DefaultLimitConnPerIP: 12, + DefaultLimitRate: "512k", + }, + } + rendered, err := RenderRouteConfig(doc, nil) + if err != nil { + t.Fatalf("RenderRouteConfig() error = %v", err) + } + if strings.Contains(rendered, "limit_conn") || strings.Contains(rendered, "limit_rate") { + t.Fatalf("expected no limit directives, got:\n%s", rendered) + } +} diff --git a/pkg/render/openresty/types.go b/pkg/render/openresty/types.go index 49c4ffef..86bb7714 100644 --- a/pkg/render/openresty/types.go +++ b/pkg/render/openresty/types.go @@ -309,6 +309,9 @@ type ConfigSnapshot struct { CacheLockTimeout string `json:"cache_lock_timeout"` CacheUseStale string `json:"cache_use_stale"` MainConfigTemplate string `json:"main_config_template,omitempty"` + DefaultLimitConnPerServer int `json:"default_limit_conn_per_server,omitempty"` + DefaultLimitConnPerIP int `json:"default_limit_conn_per_ip,omitempty"` + DefaultLimitRate string `json:"default_limit_rate,omitempty"` } // Document is the top-level input structure for the OpenResty renderer,