mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-29 05:56:38 +08:00
fix: 修复 WAF 规则组保存/绑定网站时报 of_waf_rule_group_bindings_pkey 冲突
This commit is contained in:
@@ -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 匹配地图质心。
|
||||
|
||||
+4
-9
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user