From 117d473c27da22a679f67fed6db078af3d635115 Mon Sep 17 00:00:00 2001 From: ryan Date: Sat, 20 Jun 2026 21:52:33 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20WAF=20=E8=A7=84?= =?UTF-8?q?=E5=88=99=E7=BB=84=E4=BF=9D=E5=AD=98/=E7=BB=91=E5=AE=9A?= =?UTF-8?q?=E7=BD=91=E7=AB=99=E6=97=B6=E6=8A=A5=20`of=5Fwaf=5Frule=5Fgroup?= =?UTF-8?q?=5Fbindings=5Fpkey`=20=E5=86=B2=E7=AA=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/changelog/index.md | 8 +++ frontend/proxy.ts | 13 ++-- internal/apps/agent/nginx/pow_assets.go | 6 +- .../openflare/config_version/logics_test.go | 49 +++++++++++++ .../apps/openflare/config_version/snapshot.go | 10 ++- .../202606200006_migrate_legacy_data.go | 3 +- ...606200007_sync_of_waf_binding_sequence.sql | 3 +- internal/model/openflare_waf.go | 3 +- pkg/render/openresty/render.go | 46 ++++++++++--- pkg/render/openresty/render_test.go | 69 +++++++++++++++++++ pkg/render/openresty/types.go | 13 ++++ 11 files changed, 194 insertions(+), 29 deletions(-) diff --git a/docs/changelog/index.md b/docs/changelog/index.md index 78f3e56a..362d6013 100644 --- a/docs/changelog/index.md +++ b/docs/changelog/index.md @@ -16,14 +16,22 @@ sidebar: false ## [Unreleased] +### 变更 + +- 前端页面鉴权改为默认私域:除 `/login`、`/register`、`/callback` 外,未登录访问任意页面(含数据看板 `/`)均重定向至登录页。 + ### 修复 - 配置版本列表按 `created_at` 倒序展示,最新发布版本固定显示在列表顶部。 - 修复 WAF 规则组保存/绑定网站时报 `of_waf_rule_group_bindings_pkey` 冲突:PostgreSQL 在迁移导入显式 ID 后同步绑定表序列,并在写入前自动校正序列。 +- 修复 PostgreSQL 启动迁移失败:`of_waf_rule_group_bindings` 序列表为空时 `setval(0)` 越界,改为空表重置为 1、有数据时对齐 `MAX(id)`。 + - 修复 WAF 规则组 PoW 策略发布后边缘不生效:统一 WAF 绑定站点名与 OpenResty 路由 `site_name` 解析逻辑,并为所有已启用网站生成 `site_rule_groups` 条目(含仅依赖全局规则组的站点)。 +- 修复 WAF PoW 已启用但挑战页不弹出:`pow_enabled=true` 且 `pow_config` 为空时补齐默认配置写入 `waf_config.json`,OpenResty 按全局+已绑定规则组解析 PoW 注入,Lua 对空配置使用运行时默认值。 + - 收敛子代理站点标识双轨逻辑:新增 `routeidentity` 统一包,`proxy_route`、`config_version`、`uptimekuma`、`flared` 与 OpenResty 渲染共用 `ResolveSiteName` / `DecodeDomains`;移除废弃 `RenderPoWConfig`;PoW Lua 与 WAF 一致仅依赖 `$openflare_waf_site`。 - 修复全球态势板在仅有 `geo_name`(如 mmdb 的 Germany)而无经纬度时误用美国 fallback 坐标的问题;按国家名/ISO 匹配地图质心。 diff --git a/frontend/proxy.ts b/frontend/proxy.ts index 0518b953..90a30d9f 100644 --- a/frontend/proxy.ts +++ b/frontend/proxy.ts @@ -78,7 +78,7 @@ if (typeof setInterval !== 'undefined') { export function proxy(request: NextRequest) { const { pathname, search } = request.nextUrl - const sessionCookieName = process.env.WAVELET_SESSION_COOKIE_NAME || 'wavelet_session_id' + const sessionCookieName = process.env.WAVELET_SESSION_COOKIE_NAME || 'openflare_session_id' const sessionCookie = request.cookies.get(sessionCookieName) /* API 请求:速率限制后放行 */ @@ -102,15 +102,10 @@ export function proxy(request: NextRequest) { return NextResponse.next() } - /* 页面请求:公共路由放行 */ - const publicRoutes = ['/', '/login', '/register', '/callback', '/privacy', '/terms', '/icon'] - const publicPrefixes = ['/docs/', '/epay/'] + /* 页面请求:默认私域,仅登录流程入口无需 session */ + const authEntryRoutes = ['/login', '/register', '/callback'] - if (publicRoutes.includes(pathname) || publicPrefixes.some(p => pathname.startsWith(p))) { - return NextResponse.next() - } - - if (!sessionCookie) { + if (!authEntryRoutes.includes(pathname) && !sessionCookie) { const loginUrl = new URL('/login', request.url) loginUrl.searchParams.set('callbackUrl', pathname + search) return NextResponse.redirect(loginUrl) diff --git a/internal/apps/agent/nginx/pow_assets.go b/internal/apps/agent/nginx/pow_assets.go index e206e9fc..8d0ba2a8 100644 --- a/internal/apps/agent/nginx/pow_assets.go +++ b/internal/apps/agent/nginx/pow_assets.go @@ -71,7 +71,7 @@ local function load_pow_config() local groups = {} for _, group in ipairs(decoded.rule_groups) do if group.pow_enabled then - groups[tostring(group.id)] = group.pow_config + groups[tostring(group.id)] = group.pow_config or {} end end -- Build site name to pow_config map @@ -88,12 +88,12 @@ local function load_pow_config() if not pow_config then for _, group in ipairs(decoded.rule_groups) do if group.is_global and group.pow_enabled then - pow_config = group.pow_config + pow_config = group.pow_config or {} break end end end - if pow_config then + if pow_config ~= nil then pow_config_dict:set(site, cjson.encode({enabled = true, config = pow_config}), 0) domain_keys[#domain_keys+1] = site end diff --git a/internal/apps/openflare/config_version/logics_test.go b/internal/apps/openflare/config_version/logics_test.go index c80fb4f3..7498a7d6 100644 --- a/internal/apps/openflare/config_version/logics_test.go +++ b/internal/apps/openflare/config_version/logics_test.go @@ -177,3 +177,52 @@ func TestBuildSnapshotWAFDocumentUsesNormalizedSiteNames(t *testing.T) { assert.Contains(t, bundle.RouteConfig, `set $openflare_waf_site "example.com"`) assert.Contains(t, bundle.RouteConfig, `require("pow.runtime").check()`) } + +func TestBuildCurrentConfigBundleEnablesGlobalPoWWithoutExplicitBinding(t *testing.T) { + cleanup := setupConfigVersionTestDB(t) + defer cleanup() + ctx := context.Background() + + route := &model.ProxyRoute{ + Domain: "pow-global.example.com", + Domains: `["pow-global.example.com"]`, + OriginURL: "http://origin.example.com:8080", + Upstreams: `["http://origin.example.com:8080"]`, + Enabled: true, + } + require.NoError(t, model.CreateProxyRouteRecord(ctx, route)) + + require.NoError(t, waf.EnsureDefaultRuleGroup(ctx)) + globalGroup, err := model.GetGlobalOpenFlareWAFRuleGroup(ctx) + require.NoError(t, err) + globalGroup.PoWEnabled = true + globalGroup.PoWConfig = `{"difficulty":4,"algorithm":"fast","session_ttl":600,"challenge_ttl":300}` + require.NoError(t, model.UpdateOpenFlareWAFRuleGroup(ctx, globalGroup)) + + bundle, err := buildCurrentConfigBundle(ctx, true) + require.NoError(t, err) + assert.Contains(t, bundle.RouteConfig, `require("pow.runtime").check()`) + + var wafRuntime struct { + RuleGroups []struct { + ID uint `json:"id"` + PoWEnabled bool `json:"pow_enabled"` + PoWConfig *struct { + Difficulty int `json:"difficulty"` + } `json:"pow_config"` + } `json:"rule_groups"` + SiteRuleGroups map[string][]uint `json:"site_rule_groups"` + } + for _, file := range bundle.SupportFiles { + if file.Path != "waf_config.json" { + continue + } + require.NoError(t, json.Unmarshal([]byte(file.Content), &wafRuntime)) + } + require.Contains(t, wafRuntime.SiteRuleGroups, "pow-global.example.com") + require.Contains(t, wafRuntime.SiteRuleGroups["pow-global.example.com"], globalGroup.ID) + require.NotEmpty(t, wafRuntime.RuleGroups) + assert.True(t, wafRuntime.RuleGroups[0].PoWEnabled) + require.NotNil(t, wafRuntime.RuleGroups[0].PoWConfig) + assert.Equal(t, 4, wafRuntime.RuleGroups[0].PoWConfig.Difficulty) +} diff --git a/internal/apps/openflare/config_version/snapshot.go b/internal/apps/openflare/config_version/snapshot.go index 5915947a..3e0cd560 100644 --- a/internal/apps/openflare/config_version/snapshot.go +++ b/internal/apps/openflare/config_version/snapshot.go @@ -308,7 +308,7 @@ func buildSnapshotWAFDocument(ctx context.Context, routes []*model.ProxyRoute) ( RegionWhitelist: view.RegionWhitelist, RegionBlacklist: view.RegionBlacklist, PoWEnabled: view.PoWEnabled, - PoWConfig: convertPoWConfig(view.PoWConfig), + PoWConfig: convertPoWConfig(view.PoWEnabled, view.PoWConfig), }) } ipGroups, err := buildSnapshotWAFIPGroups(ctx, ruleGroups) @@ -415,10 +415,14 @@ func decodeIPList(raw string) ([]string, error) { return items, nil } -func convertPoWConfig(config *waf.PoWConfig) *openrestyrender.PoWConfig { - if config == nil { +func convertPoWConfig(enabled bool, config *waf.PoWConfig) *openrestyrender.PoWConfig { + if !enabled { return nil } + if config == nil { + defaultConfig := openrestyrender.DefaultPoWConfig() + return &defaultConfig + } return &openrestyrender.PoWConfig{ Difficulty: config.Difficulty, Algorithm: config.Algorithm, diff --git a/internal/db/migrator/202606200006_migrate_legacy_data.go b/internal/db/migrator/202606200006_migrate_legacy_data.go index ee6b1cc4..2f901a36 100644 --- a/internal/db/migrator/202606200006_migrate_legacy_data.go +++ b/internal/db/migrator/202606200006_migrate_legacy_data.go @@ -102,7 +102,8 @@ func up202606200006(ctx context.Context, tx *sql.Tx) error { if _, err := tx.ExecContext(ctx, ` SELECT setval( pg_get_serial_sequence('of_waf_rule_group_bindings', 'id'), - COALESCE((SELECT MAX(id) FROM of_waf_rule_group_bindings), 0) + GREATEST(COALESCE((SELECT MAX(id) FROM of_waf_rule_group_bindings), 0), 1), + COALESCE((SELECT MAX(id) FROM of_waf_rule_group_bindings), 0) > 0 ) `); err != nil { return fmt.Errorf("sync of_waf_rule_group_bindings sequence failed: %w", err) diff --git a/internal/db/migrator/goose/postgres/202606200007_sync_of_waf_binding_sequence.sql b/internal/db/migrator/goose/postgres/202606200007_sync_of_waf_binding_sequence.sql index cf7562d2..3ff439a5 100644 --- a/internal/db/migrator/goose/postgres/202606200007_sync_of_waf_binding_sequence.sql +++ b/internal/db/migrator/goose/postgres/202606200007_sync_of_waf_binding_sequence.sql @@ -1,7 +1,8 @@ -- +goose Up SELECT setval( pg_get_serial_sequence('of_waf_rule_group_bindings', 'id'), - COALESCE((SELECT MAX(id) FROM of_waf_rule_group_bindings), 0) + GREATEST(COALESCE((SELECT MAX(id) FROM of_waf_rule_group_bindings), 0), 1), + COALESCE((SELECT MAX(id) FROM of_waf_rule_group_bindings), 0) > 0 ); -- +goose Down diff --git a/internal/model/openflare_waf.go b/internal/model/openflare_waf.go index eaab6b58..c7c5aa39 100644 --- a/internal/model/openflare_waf.go +++ b/internal/model/openflare_waf.go @@ -319,7 +319,8 @@ func syncWAFBindingIDSequence(tx *gorm.DB) error { return tx.Exec(` SELECT setval( pg_get_serial_sequence('of_waf_rule_group_bindings', 'id'), - COALESCE((SELECT MAX(id) FROM of_waf_rule_group_bindings), 0) + GREATEST(COALESCE((SELECT MAX(id) FROM of_waf_rule_group_bindings), 0), 1), + COALESCE((SELECT MAX(id) FROM of_waf_rule_group_bindings), 0) > 0 ) `).Error } diff --git a/pkg/render/openresty/render.go b/pkg/render/openresty/render.go index 55e80347..44a47076 100644 --- a/pkg/render/openresty/render.go +++ b/pkg/render/openresty/render.go @@ -149,10 +149,7 @@ func RenderWAFConfig(snapshot WAFDocument) (string, error) { globalGroupIDs = append(globalGroupIDs, group.ID) } enabledGroupIDs[group.ID] = struct{}{} - powConfig := group.PoWConfig - if !group.PoWEnabled { - powConfig = nil - } + powConfig := ensurePoWConfig(group.PoWEnabled, group.PoWConfig) groups = append(groups, wafRuntimeRuleGroup{ ID: group.ID, Name: group.Name, @@ -773,27 +770,54 @@ func validateCertificateCoverage(certPEM string, domains []string) error { } func getPoWConfigForRoute(routeID uint, snapshot WAFDocument) (bool, *PoWConfig) { + enabledGroups := make(map[uint]WAFRuleGroup, len(snapshot.RuleGroups)) + globalGroupIDs := make([]uint, 0) + for _, group := range snapshot.RuleGroups { + if !group.Enabled { + continue + } + enabledGroups[group.ID] = group + if group.IsGlobal { + globalGroupIDs = append(globalGroupIDs, group.ID) + } + } + sort.Slice(globalGroupIDs, func(i, j int) bool { return globalGroupIDs[i] < globalGroupIDs[j] }) + + var boundGroupIDs []uint for _, binding := range snapshot.Bindings { if binding.RouteID != routeID { continue } for _, groupID := range binding.RuleGroupIDs { - for _, group := range snapshot.RuleGroups { - if group.ID == groupID && group.PoWEnabled { - return true, group.PoWConfig - } + if _, ok := enabledGroups[groupID]; ok { + boundGroupIDs = append(boundGroupIDs, groupID) } } break } - for _, group := range snapshot.RuleGroups { - if group.IsGlobal && group.PoWEnabled { - return true, group.PoWConfig + + activeGroupIDs := uniqueUintIDs(append(append([]uint{}, globalGroupIDs...), boundGroupIDs...)) + for _, groupID := range activeGroupIDs { + group := enabledGroups[groupID] + if group.PoWEnabled { + config := ensurePoWConfig(true, group.PoWConfig) + return true, config } } return false, nil } +func ensurePoWConfig(enabled bool, config *PoWConfig) *PoWConfig { + if !enabled { + return nil + } + if config != nil { + return config + } + defaultConfig := DefaultPoWConfig() + return &defaultConfig +} + func uniqueUintIDs(values []uint) []uint { seen := make(map[uint]struct{}, len(values)) result := make([]uint, 0, len(values)) diff --git a/pkg/render/openresty/render_test.go b/pkg/render/openresty/render_test.go index ba9f28a5..de6f2daf 100644 --- a/pkg/render/openresty/render_test.go +++ b/pkg/render/openresty/render_test.go @@ -60,6 +60,75 @@ func TestRenderWAFConfigIncludesAllRouteSiteNames(t *testing.T) { } } +func TestRenderWAFConfigUsesDefaultPoWConfigWhenEnabledWithoutPayload(t *testing.T) { + doc := WAFDocument{ + RuleGroups: []WAFRuleGroup{ + { + ID: 1, + Name: "global", + Enabled: true, + IsGlobal: true, + PoWEnabled: true, + }, + }, + Bindings: []WAFBinding{ + {RouteID: 1, SiteName: "example.com", RuleGroupIDs: []uint{}}, + }, + } + + wafConfig, err := RenderWAFConfig(doc) + if err != nil { + t.Fatalf("RenderWAFConfig() error = %v", err) + } + + var decoded struct { + RuleGroups []struct { + PoWEnabled bool `json:"pow_enabled"` + PoWConfig *PoWConfig `json:"pow_config"` + } `json:"rule_groups"` + } + if err := json.Unmarshal([]byte(wafConfig), &decoded); err != nil { + t.Fatalf("json.Unmarshal() error = %v", err) + } + if len(decoded.RuleGroups) != 1 { + t.Fatalf("expected 1 rule group, got %d", len(decoded.RuleGroups)) + } + if !decoded.RuleGroups[0].PoWEnabled { + t.Fatal("expected pow_enabled=true") + } + if decoded.RuleGroups[0].PoWConfig == nil { + t.Fatal("expected default pow_config to be emitted") + } + if decoded.RuleGroups[0].PoWConfig.Difficulty != 4 { + t.Fatalf("expected default difficulty 4, got %d", decoded.RuleGroups[0].PoWConfig.Difficulty) + } +} + +func TestGetPoWConfigForRouteUsesGlobalGroupWithoutExplicitBinding(t *testing.T) { + snapshot := WAFDocument{ + RuleGroups: []WAFRuleGroup{ + { + ID: 1, + Name: "global", + Enabled: true, + IsGlobal: true, + PoWEnabled: true, + }, + }, + Bindings: []WAFBinding{ + {RouteID: 42, SiteName: "example.com", RuleGroupIDs: []uint{}}, + }, + } + + enabled, config := getPoWConfigForRoute(42, snapshot) + if !enabled { + t.Fatal("expected pow to be enabled via global rule group") + } + if config == nil || config.Difficulty != 4 { + t.Fatalf("expected default pow config, got %#v", config) + } +} + func TestRenderPagesAPIProxyLocationBlock(t *testing.T) { tests := []struct { name string diff --git a/pkg/render/openresty/types.go b/pkg/render/openresty/types.go index 4ef6fa2a..3bcda857 100644 --- a/pkg/render/openresty/types.go +++ b/pkg/render/openresty/types.go @@ -100,6 +100,19 @@ type PoWConfig struct { Blacklist PoWListConfig `json:"blacklist"` } +// DefaultPoWConfig returns the canonical PoW defaults used when pow_enabled is +// true but no explicit pow_config payload is available. +func DefaultPoWConfig() PoWConfig { + return PoWConfig{ + Difficulty: 4, + Algorithm: "fast", + SessionTTL: 600, + ChallengeTTL: 300, + Whitelist: PoWListConfig{IPs: []string{}, IPCidrs: []string{}, Paths: []string{}, PathRegexes: []string{}, UserAgents: []string{}}, + Blacklist: PoWListConfig{IPs: []string{}, IPCidrs: []string{}, Paths: []string{}, PathRegexes: []string{}, UserAgents: []string{}}, + } +} + // Route describes a single proxy or pages site entry in the OpenFlare config // document, including upstream, TLS, caching, rate-limiting and WAF settings. type Route struct {