mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-03 15:06:36 +08:00
[优化] POW 与 WAF 合并
This commit is contained in:
@@ -4,7 +4,7 @@ import "time"
|
||||
|
||||
const (
|
||||
legacyDatabaseSchemaVersion = 1
|
||||
currentDatabaseSchemaVersion = 13
|
||||
currentDatabaseSchemaVersion = 14
|
||||
databaseSchemaVersionRowID = 1
|
||||
)
|
||||
|
||||
|
||||
@@ -1413,6 +1413,8 @@ func ensureDefaultWAFRuleGroup(db *gorm.DB) error {
|
||||
CountryBlacklist: "[]",
|
||||
RegionWhitelist: "[]",
|
||||
RegionBlacklist: "[]",
|
||||
PoWEnabled: false,
|
||||
PoWConfig: "{}",
|
||||
BlockResponseBody: "",
|
||||
}
|
||||
if err := db.Create(&group).Error; err != nil {
|
||||
@@ -1449,6 +1451,27 @@ func validateDatabaseSchemaV13(db *gorm.DB, backend string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// migrateV14 adds PoW policy fields to WAF rule groups.
|
||||
func migrateV14(db *gorm.DB, backend string) error {
|
||||
if err := applyCurrentSchema(db, backend); err != nil {
|
||||
return err
|
||||
}
|
||||
return ensureDefaultWAFRuleGroup(db)
|
||||
}
|
||||
|
||||
func validateDatabaseSchemaV14(db *gorm.DB, backend string) error {
|
||||
if err := validateDatabaseSchemaV13(db, backend); err != nil {
|
||||
return err
|
||||
}
|
||||
if !db.Migrator().HasColumn(&WAFRuleGroup{}, "pow_enabled") {
|
||||
return fmt.Errorf("column waf_rule_groups.pow_enabled is missing")
|
||||
}
|
||||
if !db.Migrator().HasColumn(&WAFRuleGroup{}, "pow_config") {
|
||||
return fmt.Errorf("column waf_rule_groups.pow_config is missing")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func databaseSchemaMigrations() []databaseSchemaMigration {
|
||||
return []databaseSchemaMigration{
|
||||
{fromVersion: 1, toVersion: 2, migrate: migrateV2, validate: validateDatabaseSchemaV2},
|
||||
@@ -1463,6 +1486,7 @@ func databaseSchemaMigrations() []databaseSchemaMigration {
|
||||
{fromVersion: 10, toVersion: 11, migrate: migrateV11, validate: validateDatabaseSchemaV11},
|
||||
{fromVersion: 11, toVersion: 12, migrate: migrateV12, validate: validateDatabaseSchemaV12},
|
||||
{fromVersion: 12, toVersion: 13, migrate: migrateV13, validate: validateDatabaseSchemaV13},
|
||||
{fromVersion: 13, toVersion: 14, migrate: migrateV14, validate: validateDatabaseSchemaV14},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,8 @@ type WAFRuleGroup struct {
|
||||
CountryBlacklist string `json:"country_blacklist" gorm:"type:text;not null;default:'[]'"`
|
||||
RegionWhitelist string `json:"region_whitelist" gorm:"type:text;not null;default:'[]'"`
|
||||
RegionBlacklist string `json:"region_blacklist" gorm:"type:text;not null;default:'[]'"`
|
||||
PoWEnabled bool `json:"pow_enabled" gorm:"not null;default:false"`
|
||||
PoWConfig string `json:"pow_config" gorm:"type:text;not null;default:'{}'"`
|
||||
Remark string `json:"remark" gorm:"size:255"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
@@ -62,6 +64,8 @@ func (group *WAFRuleGroup) Update() error {
|
||||
"country_blacklist": group.CountryBlacklist,
|
||||
"region_whitelist": group.RegionWhitelist,
|
||||
"region_blacklist": group.RegionBlacklist,
|
||||
"pow_enabled": group.PoWEnabled,
|
||||
"pow_config": group.PoWConfig,
|
||||
"remark": group.Remark,
|
||||
}).Error
|
||||
}
|
||||
|
||||
@@ -95,18 +95,20 @@ type snapshotRoute struct {
|
||||
}
|
||||
|
||||
type snapshotWAFRuleGroup struct {
|
||||
ID uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Enabled bool `json:"enabled"`
|
||||
IsGlobal bool `json:"is_global"`
|
||||
BlockStatusCode int `json:"block_status_code"`
|
||||
BlockResponseBody string `json:"block_response_body,omitempty"`
|
||||
IPWhitelist []string `json:"ip_whitelist,omitempty"`
|
||||
IPBlacklist []string `json:"ip_blacklist,omitempty"`
|
||||
CountryWhitelist []string `json:"country_whitelist,omitempty"`
|
||||
CountryBlacklist []string `json:"country_blacklist,omitempty"`
|
||||
RegionWhitelist []string `json:"region_whitelist,omitempty"`
|
||||
RegionBlacklist []string `json:"region_blacklist,omitempty"`
|
||||
ID uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Enabled bool `json:"enabled"`
|
||||
IsGlobal bool `json:"is_global"`
|
||||
BlockStatusCode int `json:"block_status_code"`
|
||||
BlockResponseBody string `json:"block_response_body,omitempty"`
|
||||
IPWhitelist []string `json:"ip_whitelist,omitempty"`
|
||||
IPBlacklist []string `json:"ip_blacklist,omitempty"`
|
||||
CountryWhitelist []string `json:"country_whitelist,omitempty"`
|
||||
CountryBlacklist []string `json:"country_blacklist,omitempty"`
|
||||
RegionWhitelist []string `json:"region_whitelist,omitempty"`
|
||||
RegionBlacklist []string `json:"region_blacklist,omitempty"`
|
||||
PoWEnabled bool `json:"pow_enabled,omitempty"`
|
||||
PoWConfig *ProxyRoutePoWConfig `json:"pow_config,omitempty"`
|
||||
}
|
||||
|
||||
type snapshotWAFBinding struct {
|
||||
@@ -504,7 +506,7 @@ func buildCurrentConfigBundle(requireRoutes bool) (*configBundle, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
routeConfig, supportFiles, err := renderRouteConfig(routes, openRestyConfig)
|
||||
routeConfig, supportFiles, err := renderRouteConfig(routes, openRestyConfig, wafSnapshot)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -512,7 +514,7 @@ func buildCurrentConfigBundle(requireRoutes bool) (*configBundle, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
powConfigJSON, powSupportFiles, err := renderPowConfigBundle(routes)
|
||||
powConfigJSON, powSupportFiles, err := renderPowConfigBundle(routes, wafSnapshot)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -617,6 +619,8 @@ func buildSnapshotWAFDocument(routes []*model.ProxyRoute) (snapshotWAFDocument,
|
||||
CountryBlacklist: view.CountryBlacklist,
|
||||
RegionWhitelist: view.RegionWhitelist,
|
||||
RegionBlacklist: view.RegionBlacklist,
|
||||
PoWEnabled: view.PoWEnabled,
|
||||
PoWConfig: view.PoWConfig,
|
||||
})
|
||||
}
|
||||
enabledRouteIDs := make(map[uint]string, len(routes))
|
||||
@@ -1029,10 +1033,11 @@ func openRestyOptionKeys() []string {
|
||||
}
|
||||
}
|
||||
|
||||
func renderRouteConfig(routes []*model.ProxyRoute, cfg openRestyConfigSnapshot) (string, []SupportFile, error) {
|
||||
func renderRouteConfig(routes []*model.ProxyRoute, cfg openRestyConfigSnapshot, wafSnapshot snapshotWAFDocument) (string, []SupportFile, error) {
|
||||
var builder strings.Builder
|
||||
builder.WriteString("# This file is generated by OpenFlare. Do not edit manually.\n")
|
||||
supportFiles := make([]SupportFile, 0)
|
||||
powEnabledByRoute := wafPowEnabledByRoute(wafSnapshot)
|
||||
for _, route := range routes {
|
||||
domains, err := decodeStoredDomains(route.Domains, route.Domain)
|
||||
if err != nil {
|
||||
|
||||
@@ -20,37 +20,41 @@ const (
|
||||
)
|
||||
|
||||
type WAFRuleGroupInput struct {
|
||||
Name string `json:"name"`
|
||||
Enabled bool `json:"enabled"`
|
||||
BlockStatusCode int `json:"block_status_code"`
|
||||
BlockResponseBody string `json:"block_response_body"`
|
||||
IPWhitelist []string `json:"ip_whitelist"`
|
||||
IPBlacklist []string `json:"ip_blacklist"`
|
||||
CountryWhitelist []string `json:"country_whitelist"`
|
||||
CountryBlacklist []string `json:"country_blacklist"`
|
||||
RegionWhitelist []string `json:"region_whitelist"`
|
||||
RegionBlacklist []string `json:"region_blacklist"`
|
||||
Remark string `json:"remark"`
|
||||
Name string `json:"name"`
|
||||
Enabled bool `json:"enabled"`
|
||||
BlockStatusCode int `json:"block_status_code"`
|
||||
BlockResponseBody string `json:"block_response_body"`
|
||||
IPWhitelist []string `json:"ip_whitelist"`
|
||||
IPBlacklist []string `json:"ip_blacklist"`
|
||||
CountryWhitelist []string `json:"country_whitelist"`
|
||||
CountryBlacklist []string `json:"country_blacklist"`
|
||||
RegionWhitelist []string `json:"region_whitelist"`
|
||||
RegionBlacklist []string `json:"region_blacklist"`
|
||||
Remark string `json:"remark"`
|
||||
PoWEnabled bool `json:"pow_enabled"`
|
||||
PoWConfig json.RawMessage `json:"pow_config"`
|
||||
}
|
||||
|
||||
type WAFRuleGroupView struct {
|
||||
ID uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Enabled bool `json:"enabled"`
|
||||
IsGlobal bool `json:"is_global"`
|
||||
BlockStatusCode int `json:"block_status_code"`
|
||||
BlockResponseBody string `json:"block_response_body"`
|
||||
IPWhitelist []string `json:"ip_whitelist"`
|
||||
IPBlacklist []string `json:"ip_blacklist"`
|
||||
CountryWhitelist []string `json:"country_whitelist"`
|
||||
CountryBlacklist []string `json:"country_blacklist"`
|
||||
RegionWhitelist []string `json:"region_whitelist"`
|
||||
RegionBlacklist []string `json:"region_blacklist"`
|
||||
Remark string `json:"remark"`
|
||||
AppliedSiteIDs []uint `json:"applied_site_ids"`
|
||||
AppliedSiteCount int `json:"applied_site_count"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
ID uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Enabled bool `json:"enabled"`
|
||||
IsGlobal bool `json:"is_global"`
|
||||
BlockStatusCode int `json:"block_status_code"`
|
||||
BlockResponseBody string `json:"block_response_body"`
|
||||
IPWhitelist []string `json:"ip_whitelist"`
|
||||
IPBlacklist []string `json:"ip_blacklist"`
|
||||
CountryWhitelist []string `json:"country_whitelist"`
|
||||
CountryBlacklist []string `json:"country_blacklist"`
|
||||
RegionWhitelist []string `json:"region_whitelist"`
|
||||
RegionBlacklist []string `json:"region_blacklist"`
|
||||
Remark string `json:"remark"`
|
||||
PoWEnabled bool `json:"pow_enabled"`
|
||||
PoWConfig *ProxyRoutePoWConfig `json:"pow_config"`
|
||||
AppliedSiteIDs []uint `json:"applied_site_ids"`
|
||||
AppliedSiteCount int `json:"applied_site_count"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
}
|
||||
|
||||
type WAFSiteRuleGroupsView struct {
|
||||
@@ -275,6 +279,8 @@ func EnsureDefaultWAFRuleGroup() error {
|
||||
CountryBlacklist: "[]",
|
||||
RegionWhitelist: "[]",
|
||||
RegionBlacklist: "[]",
|
||||
PoWEnabled: false,
|
||||
PoWConfig: "{}",
|
||||
BlockResponseBody: "",
|
||||
}
|
||||
return group.Insert()
|
||||
@@ -313,6 +319,15 @@ func buildWAFRuleGroup(group *model.WAFRuleGroup, input WAFRuleGroupInput) (*mod
|
||||
}
|
||||
regionWhitelist := normalizeStringList(input.RegionWhitelist)
|
||||
regionBlacklist := normalizeStringList(input.RegionBlacklist)
|
||||
powConfigRaw := strings.TrimSpace(string(input.PoWConfig))
|
||||
if powConfigRaw == "" {
|
||||
powConfigRaw = "{}"
|
||||
}
|
||||
powConfig, err := normalizePoWConfig(input.PoWEnabled, powConfigRaw)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
powConfigJSON, _ := json.Marshal(powConfig)
|
||||
|
||||
ipWhitelistJSON, _ := json.Marshal(ipWhitelist)
|
||||
ipBlacklistJSON, _ := json.Marshal(ipBlacklist)
|
||||
@@ -334,6 +349,8 @@ func buildWAFRuleGroup(group *model.WAFRuleGroup, input WAFRuleGroupInput) (*mod
|
||||
group.CountryBlacklist = string(countryBlacklistJSON)
|
||||
group.RegionWhitelist = string(regionWhitelistJSON)
|
||||
group.RegionBlacklist = string(regionBlacklistJSON)
|
||||
group.PoWEnabled = input.PoWEnabled
|
||||
group.PoWConfig = string(powConfigJSON)
|
||||
group.Remark = strings.TrimSpace(input.Remark)
|
||||
return group, nil
|
||||
}
|
||||
@@ -351,6 +368,7 @@ func buildWAFRuleGroupView(group *model.WAFRuleGroup, appliedSiteIDs []uint) (WA
|
||||
BlockStatusCode: group.BlockStatusCode,
|
||||
BlockResponseBody: group.BlockResponseBody,
|
||||
Remark: group.Remark,
|
||||
PoWEnabled: group.PoWEnabled,
|
||||
AppliedSiteIDs: appliedSiteIDs,
|
||||
AppliedSiteCount: len(appliedSiteIDs),
|
||||
CreatedAt: group.CreatedAt.Format(time.RFC3339),
|
||||
@@ -375,6 +393,9 @@ func buildWAFRuleGroupView(group *model.WAFRuleGroup, appliedSiteIDs []uint) (WA
|
||||
if view.RegionBlacklist, err = decodeStringList(group.RegionBlacklist); err != nil {
|
||||
return view, err
|
||||
}
|
||||
if view.PoWConfig, err = decodeStoredPoWConfig(group.PoWEnabled, group.PoWConfig); err != nil {
|
||||
return view, err
|
||||
}
|
||||
return view, nil
|
||||
}
|
||||
|
||||
|
||||
+121
-304
@@ -19,6 +19,10 @@ import {
|
||||
getProxyRoute,
|
||||
updateProxyRoute,
|
||||
} from '@/features/proxy-routes/api/proxy-routes';
|
||||
import {
|
||||
getWAFSiteRuleGroups,
|
||||
replaceWAFSiteRuleGroups,
|
||||
} from '@/features/waf/api/waf';
|
||||
import {
|
||||
buildDomainRowsFromRoute,
|
||||
DomainListInput,
|
||||
@@ -729,318 +733,137 @@ function CacheSection({
|
||||
);
|
||||
}
|
||||
|
||||
type PowListValues = {
|
||||
ips: string;
|
||||
ip_cidrs: string;
|
||||
paths: string;
|
||||
path_regexes: string;
|
||||
user_agents: string;
|
||||
};
|
||||
|
||||
const powSchema = z
|
||||
.object({
|
||||
pow_enabled: z.boolean(),
|
||||
difficulty: z.coerce.number().int().min(1).max(16),
|
||||
algorithm: z.enum(['fast', 'slow']),
|
||||
session_ttl: z.coerce.number().int().min(60),
|
||||
challenge_ttl: z.coerce.number().int().min(30),
|
||||
whitelist: z.object({
|
||||
ips: z.string(),
|
||||
ip_cidrs: z.string(),
|
||||
paths: z.string(),
|
||||
path_regexes: z.string(),
|
||||
user_agents: z.string(),
|
||||
}),
|
||||
blacklist: z.object({
|
||||
ips: z.string(),
|
||||
ip_cidrs: z.string(),
|
||||
paths: z.string(),
|
||||
path_regexes: z.string(),
|
||||
user_agents: z.string(),
|
||||
}),
|
||||
})
|
||||
.superRefine((value, context) => {
|
||||
if (!value.pow_enabled) return;
|
||||
const dimensions: { key: string; label: string }[] = [
|
||||
{ key: 'ips', label: 'IP' },
|
||||
{ key: 'ip_cidrs', label: 'IP CIDR' },
|
||||
{ key: 'paths', label: '路径' },
|
||||
{ key: 'path_regexes', label: '路径正则' },
|
||||
{ key: 'user_agents', label: 'User-Agent' },
|
||||
];
|
||||
for (const dim of dimensions) {
|
||||
const wl = linesFromTextarea(
|
||||
(value.whitelist as Record<string, string>)[dim.key] || '',
|
||||
);
|
||||
const bl = linesFromTextarea(
|
||||
(value.blacklist as Record<string, string>)[dim.key] || '',
|
||||
);
|
||||
if (wl.length > 0 && bl.length > 0) {
|
||||
context.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: `${dim.label} 不能同时配置白名单和黑名单`,
|
||||
path: ['blacklist', dim.key],
|
||||
});
|
||||
}
|
||||
}
|
||||
function WAFBindingSection({ route }: { route: ProxyRouteItem }) {
|
||||
const queryClient = useQueryClient();
|
||||
const [selectedIDs, setSelectedIDs] = useState<number[]>([]);
|
||||
const wafQuery = useQuery({
|
||||
queryKey: ['waf', 'site-rule-groups', route.id],
|
||||
queryFn: () => getWAFSiteRuleGroups(route.id),
|
||||
});
|
||||
|
||||
type PowValues = z.infer<typeof powSchema>;
|
||||
|
||||
function buildPowListFromConfig(
|
||||
list:
|
||||
| {
|
||||
ips?: string[];
|
||||
ip_cidrs?: string[];
|
||||
paths?: string[];
|
||||
path_regexes?: string[];
|
||||
user_agents?: string[];
|
||||
}
|
||||
| undefined,
|
||||
): PowListValues {
|
||||
return {
|
||||
ips: (list?.ips ?? []).join('\n'),
|
||||
ip_cidrs: (list?.ip_cidrs ?? []).join('\n'),
|
||||
paths: (list?.paths ?? []).join('\n'),
|
||||
path_regexes: (list?.path_regexes ?? []).join('\n'),
|
||||
user_agents: (list?.user_agents ?? []).join('\n'),
|
||||
};
|
||||
}
|
||||
|
||||
function PowSection({
|
||||
route,
|
||||
saving,
|
||||
onSave,
|
||||
}: {
|
||||
route: ProxyRouteItem;
|
||||
saving: boolean;
|
||||
onSave: SaveHandler;
|
||||
}) {
|
||||
const powConfig = route.pow_config;
|
||||
const form = useForm<PowValues>({
|
||||
resolver: zodResolver(powSchema),
|
||||
defaultValues: {
|
||||
pow_enabled: route.pow_enabled,
|
||||
difficulty: powConfig?.difficulty ?? 4,
|
||||
algorithm: powConfig?.algorithm ?? 'fast',
|
||||
session_ttl: powConfig?.session_ttl ?? 600,
|
||||
challenge_ttl: powConfig?.challenge_ttl ?? 300,
|
||||
whitelist: buildPowListFromConfig(powConfig?.whitelist),
|
||||
blacklist: buildPowListFromConfig(powConfig?.blacklist),
|
||||
const wafMutation = useMutation({
|
||||
mutationFn: (ids: number[]) => replaceWAFSiteRuleGroups(route.id, ids),
|
||||
onSuccess: async (result) => {
|
||||
setSelectedIDs(result.applied_ids);
|
||||
await Promise.all([
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ['waf', 'site-rule-groups', route.id],
|
||||
}),
|
||||
queryClient.invalidateQueries({ queryKey: ['waf', 'rule-groups'] }),
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ['config-versions', 'diff'],
|
||||
}),
|
||||
]);
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
form.reset({
|
||||
pow_enabled: route.pow_enabled,
|
||||
difficulty: powConfig?.difficulty ?? 4,
|
||||
algorithm: powConfig?.algorithm ?? 'fast',
|
||||
session_ttl: powConfig?.session_ttl ?? 600,
|
||||
challenge_ttl: powConfig?.challenge_ttl ?? 300,
|
||||
whitelist: buildPowListFromConfig(powConfig?.whitelist),
|
||||
blacklist: buildPowListFromConfig(powConfig?.blacklist),
|
||||
});
|
||||
}, [form, route, powConfig]);
|
||||
if (wafQuery.data) {
|
||||
setSelectedIDs(wafQuery.data.applied_ids);
|
||||
}
|
||||
}, [wafQuery.data]);
|
||||
|
||||
const watchedEnabled = form.watch('pow_enabled');
|
||||
|
||||
const parseList = (text: string): string[] =>
|
||||
linesFromTextarea(text).filter(Boolean);
|
||||
const selectedSet = useMemo(() => new Set(selectedIDs), [selectedIDs]);
|
||||
|
||||
return (
|
||||
<ConfigSectionShell
|
||||
title="PoW 防护"
|
||||
description="启用 Proof-of-Work 反爬虫验证。首次访问的浏览器需要完成计算挑战才能继续。"
|
||||
formId="proxy-route-pow-form"
|
||||
saving={saving}
|
||||
title="WAF"
|
||||
description="全局规则组始终生效;这里可以为当前网站叠加自定义规则组。PoW 规则请在 WAF 页面统一配置和批量应用。"
|
||||
formId="proxy-route-waf-form"
|
||||
saving={wafMutation.isPending}
|
||||
>
|
||||
<form
|
||||
id="proxy-route-pow-form"
|
||||
className="space-y-5"
|
||||
onSubmit={form.handleSubmit((values) => {
|
||||
const powConfigPayload = JSON.stringify({
|
||||
difficulty: values.difficulty,
|
||||
algorithm: values.algorithm,
|
||||
session_ttl: values.session_ttl,
|
||||
challenge_ttl: values.challenge_ttl,
|
||||
whitelist: {
|
||||
ips: parseList(values.whitelist.ips),
|
||||
ip_cidrs: parseList(values.whitelist.ip_cidrs),
|
||||
paths: parseList(values.whitelist.paths),
|
||||
path_regexes: parseList(values.whitelist.path_regexes),
|
||||
user_agents: parseList(values.whitelist.user_agents),
|
||||
},
|
||||
blacklist: {
|
||||
ips: parseList(values.blacklist.ips),
|
||||
ip_cidrs: parseList(values.blacklist.ip_cidrs),
|
||||
paths: parseList(values.blacklist.paths),
|
||||
path_regexes: parseList(values.blacklist.path_regexes),
|
||||
user_agents: parseList(values.blacklist.user_agents),
|
||||
},
|
||||
});
|
||||
onSave(
|
||||
buildPayloadFromRoute(route, {
|
||||
pow_enabled: values.pow_enabled,
|
||||
pow_config: powConfigPayload,
|
||||
}),
|
||||
{ message: 'PoW 防护设置已保存。' },
|
||||
);
|
||||
})}
|
||||
>
|
||||
<ToggleField
|
||||
label="启用 PoW 防护"
|
||||
description="对访问此站点的请求进行 Proof-of-Work 验证,阻止自动化爬虫。"
|
||||
checked={watchedEnabled}
|
||||
onChange={(checked) =>
|
||||
form.setValue('pow_enabled', checked, { shouldDirty: true })
|
||||
}
|
||||
{wafQuery.isLoading ? (
|
||||
<LoadingState />
|
||||
) : wafQuery.isError ? (
|
||||
<ErrorState
|
||||
title="WAF 规则加载失败"
|
||||
description={getErrorMessage(wafQuery.error)}
|
||||
/>
|
||||
) : (
|
||||
<div className="space-y-5">
|
||||
{wafMutation.isError ? (
|
||||
<InlineMessage
|
||||
tone="danger"
|
||||
message={getErrorMessage(wafMutation.error)}
|
||||
/>
|
||||
) : null}
|
||||
{wafMutation.isSuccess ? (
|
||||
<InlineMessage tone="success" message="WAF 规则组已更新。" />
|
||||
) : null}
|
||||
|
||||
<ResourceField label="验证算法">
|
||||
<ResourceSelect
|
||||
disabled={!watchedEnabled}
|
||||
{...form.register('algorithm')}
|
||||
>
|
||||
<option value="fast">Fast(WebCrypto SHA-256)</option>
|
||||
<option value="slow">Slow(兼容模式)</option>
|
||||
</ResourceSelect>
|
||||
</ResourceField>
|
||||
{wafQuery.data?.global_rule_group ? (
|
||||
<div className="rounded-[26px] border border-[var(--border-default)] bg-[var(--surface-elevated)] p-5">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<p className="text-xs font-medium tracking-[0.18em] text-[var(--foreground-muted)] uppercase">
|
||||
Global Rule Group
|
||||
</p>
|
||||
<p className="mt-2 text-sm font-semibold text-[var(--foreground-primary)]">
|
||||
{wafQuery.data.global_rule_group.name}
|
||||
</p>
|
||||
</div>
|
||||
<span className="rounded-full border border-[var(--border-default)] px-3 py-1.5 text-xs text-[var(--foreground-secondary)]">
|
||||
始终生效
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<ResourceField
|
||||
label="难度"
|
||||
hint="数值越高验证越慢,1-16。推荐 3-5。"
|
||||
error={form.formState.errors.difficulty?.message}
|
||||
>
|
||||
<ResourceInput
|
||||
type="number"
|
||||
min={1}
|
||||
max={16}
|
||||
disabled={!watchedEnabled}
|
||||
{...form.register('difficulty')}
|
||||
/>
|
||||
</ResourceField>
|
||||
<div className="grid gap-3 md:grid-cols-2">
|
||||
{(wafQuery.data?.rule_groups ?? []).map((group) => (
|
||||
<label
|
||||
key={group.id}
|
||||
className={cn(
|
||||
'flex cursor-pointer items-start gap-3 rounded-[22px] border p-4 transition',
|
||||
selectedSet.has(group.id)
|
||||
? 'border-[var(--border-strong)] bg-[var(--accent-soft)]'
|
||||
: 'border-[var(--border-default)] bg-[var(--surface-elevated)] hover:bg-[var(--surface-muted)]',
|
||||
)}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedSet.has(group.id)}
|
||||
onChange={(event) => {
|
||||
const checked = event.target.checked;
|
||||
setSelectedIDs((current) =>
|
||||
checked
|
||||
? [...current, group.id].sort((left, right) => left - right)
|
||||
: current.filter((id) => id !== group.id),
|
||||
);
|
||||
}}
|
||||
className="mt-1 h-4 w-4 rounded border-[var(--border-default)] accent-[var(--brand-primary)]"
|
||||
/>
|
||||
<span className="min-w-0">
|
||||
<span className="block text-sm font-semibold text-[var(--foreground-primary)]">
|
||||
{group.name}
|
||||
</span>
|
||||
<span className="mt-1 block text-xs leading-5 text-[var(--foreground-secondary)]">
|
||||
{group.enabled ? '启用中' : '已停用'} ·{' '}
|
||||
{group.ip_whitelist.length +
|
||||
group.ip_blacklist.length +
|
||||
group.country_whitelist.length +
|
||||
group.country_blacklist.length}{' '}
|
||||
条规则
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<ResourceField
|
||||
label="会话空闲有效期(秒)"
|
||||
hint="通过验证后,若在此时间内没有新请求,Cookie 会失效;每次访问会自动续期。默认 600 秒。"
|
||||
error={form.formState.errors.session_ttl?.message}
|
||||
>
|
||||
<ResourceInput
|
||||
type="number"
|
||||
min={60}
|
||||
disabled={!watchedEnabled}
|
||||
{...form.register('session_ttl')}
|
||||
/>
|
||||
</ResourceField>
|
||||
{(wafQuery.data?.rule_groups ?? []).length === 0 ? (
|
||||
<EmptyState title="暂无自定义 WAF 规则组" />
|
||||
) : null}
|
||||
|
||||
<ResourceField
|
||||
label="挑战有效期(秒)"
|
||||
hint="挑战令牌的有效期。"
|
||||
error={form.formState.errors.challenge_ttl?.message}
|
||||
>
|
||||
<ResourceInput
|
||||
type="number"
|
||||
min={30}
|
||||
disabled={!watchedEnabled}
|
||||
{...form.register('challenge_ttl')}
|
||||
/>
|
||||
</ResourceField>
|
||||
|
||||
<div className="grid grid-cols-1 gap-5 md:grid-cols-2">
|
||||
<fieldset disabled={!watchedEnabled} className="space-y-4">
|
||||
<legend className="mb-2 text-sm font-medium text-[var(--foreground-primary)]">
|
||||
白名单(匹配的请求跳过 PoW)
|
||||
</legend>
|
||||
<ResourceField label="IP" hint="每行一个 IP 地址">
|
||||
<ResourceTextarea
|
||||
className="min-h-20"
|
||||
placeholder="1.2.3.4 5.6.7.8"
|
||||
{...form.register('whitelist.ips')}
|
||||
/>
|
||||
</ResourceField>
|
||||
<ResourceField label="IP CIDR" hint="每行一个 CIDR 范围">
|
||||
<ResourceTextarea
|
||||
className="min-h-20"
|
||||
placeholder="10.0.0.0/8 192.168.0.0/16"
|
||||
{...form.register('whitelist.ip_cidrs')}
|
||||
/>
|
||||
</ResourceField>
|
||||
<ResourceField label="路径" hint="每行一个路径通配符">
|
||||
<ResourceTextarea
|
||||
className="min-h-20"
|
||||
placeholder="/.well-known/* /favicon.ico"
|
||||
{...form.register('whitelist.paths')}
|
||||
/>
|
||||
</ResourceField>
|
||||
<ResourceField label="路径正则" hint="每行一个正则表达式">
|
||||
<ResourceTextarea
|
||||
className="min-h-20"
|
||||
placeholder="^/api/public/"
|
||||
{...form.register('whitelist.path_regexes')}
|
||||
/>
|
||||
</ResourceField>
|
||||
<ResourceField label="User-Agent" hint="每行一个关键字">
|
||||
<ResourceTextarea
|
||||
className="min-h-20"
|
||||
placeholder="Googlebot bingbot"
|
||||
{...form.register('whitelist.user_agents')}
|
||||
/>
|
||||
</ResourceField>
|
||||
</fieldset>
|
||||
|
||||
<fieldset disabled={!watchedEnabled} className="space-y-4">
|
||||
<legend className="mb-2 text-sm font-medium text-[var(--foreground-primary)]">
|
||||
黑名单(匹配的请求必须 PoW)
|
||||
</legend>
|
||||
<ResourceField label="IP" hint="每行一个 IP 地址">
|
||||
<ResourceTextarea
|
||||
className="min-h-20"
|
||||
placeholder="1.2.3.4"
|
||||
{...form.register('blacklist.ips')}
|
||||
/>
|
||||
</ResourceField>
|
||||
<ResourceField label="IP CIDR" hint="每行一个 CIDR 范围">
|
||||
<ResourceTextarea
|
||||
className="min-h-20"
|
||||
placeholder="10.0.0.0/8"
|
||||
{...form.register('blacklist.ip_cidrs')}
|
||||
/>
|
||||
</ResourceField>
|
||||
<ResourceField label="路径" hint="每行一个路径通配符">
|
||||
<ResourceTextarea
|
||||
className="min-h-20"
|
||||
placeholder="/admin/*"
|
||||
{...form.register('blacklist.paths')}
|
||||
/>
|
||||
</ResourceField>
|
||||
<ResourceField label="路径正则" hint="每行一个正则表达式">
|
||||
<ResourceTextarea
|
||||
className="min-h-20"
|
||||
placeholder="^/private/"
|
||||
{...form.register('blacklist.path_regexes')}
|
||||
/>
|
||||
</ResourceField>
|
||||
<ResourceField label="User-Agent" hint="每行一个关键字">
|
||||
<ResourceTextarea
|
||||
className="min-h-20"
|
||||
placeholder="bot crawler"
|
||||
{...form.register('blacklist.user_agents')}
|
||||
/>
|
||||
</ResourceField>
|
||||
</fieldset>
|
||||
<div className="flex justify-end">
|
||||
<PrimaryButton
|
||||
type="button"
|
||||
disabled={wafMutation.isPending}
|
||||
onClick={() => wafMutation.mutate(selectedIDs)}
|
||||
>
|
||||
{wafMutation.isPending ? '保存中...' : '保存 WAF 绑定'}
|
||||
</PrimaryButton>
|
||||
</div>
|
||||
</div>
|
||||
{form.formState.errors.blacklist && (
|
||||
<p className="text-sm text-[var(--color-danger)]">
|
||||
{Object.values(form.formState.errors.blacklist)
|
||||
.flatMap((e) =>
|
||||
e && typeof e === 'object' && 'message' in e
|
||||
? [e.message as string]
|
||||
: [],
|
||||
)
|
||||
.join('; ')}
|
||||
</p>
|
||||
)}
|
||||
</form>
|
||||
)}
|
||||
</ConfigSectionShell>
|
||||
);
|
||||
}
|
||||
@@ -1370,14 +1193,8 @@ export function ProxyRouteConfigPage({
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{currentSection === 'pow' ? (
|
||||
<PowSection
|
||||
route={route}
|
||||
saving={saveMutation.isPending}
|
||||
onSave={(payload, context) =>
|
||||
saveMutation.mutate({ payload, context })
|
||||
}
|
||||
/>
|
||||
{currentSection === 'waf' ? (
|
||||
<WAFBindingSection route={route} />
|
||||
) : null}
|
||||
|
||||
{currentSection === 'auth' ? (
|
||||
|
||||
@@ -26,9 +26,9 @@ export const websiteConfigSections = [
|
||||
description: '配置站点缓存策略。',
|
||||
},
|
||||
{
|
||||
key: 'pow',
|
||||
label: 'PoW 防护',
|
||||
description: '配置 Proof-of-Work 反爬虫策略。',
|
||||
key: 'waf',
|
||||
label: 'WAF',
|
||||
description: '绑定 WAF 规则组,并查看当前站点生效策略。',
|
||||
},
|
||||
{
|
||||
key: 'auth',
|
||||
|
||||
@@ -5,8 +5,8 @@ import type { ReactNode } from 'react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import {
|
||||
Check,
|
||||
Cpu,
|
||||
Globe2,
|
||||
ListFilter,
|
||||
type LucideIcon,
|
||||
Plus,
|
||||
Save,
|
||||
@@ -23,8 +23,15 @@ import { PageHeader } from '@/components/layout/page-header';
|
||||
import { AppCard } from '@/components/ui/app-card';
|
||||
import { AppModal } from '@/components/ui/app-modal';
|
||||
import { Drawer } from '@/components/ui/drawer';
|
||||
import { getProxyRoutes } from '@/features/proxy-routes/api/proxy-routes';
|
||||
import type { ProxyRouteItem } from '@/features/proxy-routes/types';
|
||||
import {
|
||||
getProxyRoutes,
|
||||
updateProxyRoute,
|
||||
} from '@/features/proxy-routes/api/proxy-routes';
|
||||
import { buildPayloadFromRoute } from '@/features/proxy-routes/helpers';
|
||||
import type {
|
||||
ProxyRouteItem,
|
||||
ProxyRoutePoWConfig,
|
||||
} from '@/features/proxy-routes/types';
|
||||
import {
|
||||
DangerButton,
|
||||
PrimaryButton,
|
||||
@@ -49,7 +56,7 @@ type FeedbackState = {
|
||||
message: string;
|
||||
};
|
||||
|
||||
type WAFTab = 'basic' | 'lists' | 'block';
|
||||
type WAFTab = 'basic' | 'lists' | 'pow' | 'block';
|
||||
type RuleListType = 'whitelist' | 'blacklist';
|
||||
type RuleDimension = 'ip' | 'country';
|
||||
type ListFieldKey =
|
||||
@@ -83,6 +90,27 @@ type RuleListRenderable = Pick<
|
||||
| 'region_blacklist'
|
||||
>;
|
||||
|
||||
const defaultPowConfig: ProxyRoutePoWConfig = {
|
||||
difficulty: 4,
|
||||
algorithm: 'fast',
|
||||
session_ttl: 600,
|
||||
challenge_ttl: 300,
|
||||
whitelist: {
|
||||
ips: [],
|
||||
ip_cidrs: [],
|
||||
paths: [],
|
||||
path_regexes: [],
|
||||
user_agents: [],
|
||||
},
|
||||
blacklist: {
|
||||
ips: [],
|
||||
ip_cidrs: [],
|
||||
paths: [],
|
||||
path_regexes: [],
|
||||
user_agents: [],
|
||||
},
|
||||
};
|
||||
|
||||
const emptyDraft: WAFRuleGroupPayload = {
|
||||
name: '',
|
||||
enabled: true,
|
||||
@@ -94,6 +122,8 @@ const emptyDraft: WAFRuleGroupPayload = {
|
||||
country_blacklist: [],
|
||||
region_whitelist: [],
|
||||
region_blacklist: [],
|
||||
pow_enabled: false,
|
||||
pow_config: defaultPowConfig,
|
||||
remark: '',
|
||||
};
|
||||
|
||||
@@ -117,6 +147,10 @@ const tabItems: Array<{
|
||||
id: 'lists',
|
||||
label: '黑白名单',
|
||||
},
|
||||
{
|
||||
id: 'pow',
|
||||
label: 'PoW',
|
||||
},
|
||||
{
|
||||
id: 'block',
|
||||
label: '拦截返回',
|
||||
@@ -134,6 +168,17 @@ function textToList(text: string) {
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function listToText(items: string[] | undefined) {
|
||||
return (items ?? []).join('\n');
|
||||
}
|
||||
|
||||
function parseTextareaList(text: string) {
|
||||
return text
|
||||
.split(/\r?\n/)
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function normalizeItems(items: string[]) {
|
||||
return Array.from(
|
||||
new Set(items.map((item) => item.trim()).filter(Boolean)),
|
||||
@@ -155,6 +200,8 @@ function buildDraft(group: WAFRuleGroup | null): WAFRuleGroupPayload {
|
||||
country_blacklist: group.country_blacklist ?? [],
|
||||
region_whitelist: group.region_whitelist ?? [],
|
||||
region_blacklist: group.region_blacklist ?? [],
|
||||
pow_enabled: group.pow_enabled ?? false,
|
||||
pow_config: group.pow_config ?? defaultPowConfig,
|
||||
remark: group.remark ?? '',
|
||||
};
|
||||
}
|
||||
@@ -731,6 +778,214 @@ function SiteApplyDrawer({
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
function PowTabPanel({
|
||||
enabled,
|
||||
config,
|
||||
onChange,
|
||||
}: {
|
||||
enabled: boolean;
|
||||
config: ProxyRoutePoWConfig;
|
||||
onChange: (enabled: boolean, config: ProxyRoutePoWConfig) => void;
|
||||
}) {
|
||||
const [draft, setDraft] = useState(() => ({
|
||||
whitelist: {
|
||||
ips: listToText(config.whitelist?.ips),
|
||||
ip_cidrs: listToText(config.whitelist?.ip_cidrs),
|
||||
paths: listToText(config.whitelist?.paths),
|
||||
path_regexes: listToText(config.whitelist?.path_regexes),
|
||||
user_agents: listToText(config.whitelist?.user_agents),
|
||||
},
|
||||
blacklist: {
|
||||
ips: listToText(config.blacklist?.ips),
|
||||
ip_cidrs: listToText(config.blacklist?.ip_cidrs),
|
||||
paths: listToText(config.blacklist?.paths),
|
||||
path_regexes: listToText(config.blacklist?.path_regexes),
|
||||
user_agents: listToText(config.blacklist?.user_agents),
|
||||
},
|
||||
}));
|
||||
|
||||
useEffect(() => {
|
||||
setDraft({
|
||||
whitelist: {
|
||||
ips: listToText(config.whitelist?.ips),
|
||||
ip_cidrs: listToText(config.whitelist?.ip_cidrs),
|
||||
paths: listToText(config.whitelist?.paths),
|
||||
path_regexes: listToText(config.whitelist?.path_regexes),
|
||||
user_agents: listToText(config.whitelist?.user_agents),
|
||||
},
|
||||
blacklist: {
|
||||
ips: listToText(config.blacklist?.ips),
|
||||
ip_cidrs: listToText(config.blacklist?.ip_cidrs),
|
||||
paths: listToText(config.blacklist?.paths),
|
||||
path_regexes: listToText(config.blacklist?.path_regexes),
|
||||
user_agents: listToText(config.blacklist?.user_agents),
|
||||
},
|
||||
});
|
||||
}, [config]);
|
||||
|
||||
const updateConfig = (
|
||||
newEnabled: boolean,
|
||||
newConfig: Partial<ProxyRoutePoWConfig>,
|
||||
newDraft?: typeof draft,
|
||||
) => {
|
||||
const nextConfig = { ...config, ...newConfig };
|
||||
if (newDraft) {
|
||||
setDraft(newDraft);
|
||||
nextConfig.whitelist = {
|
||||
ips: parseTextareaList(newDraft.whitelist.ips),
|
||||
ip_cidrs: parseTextareaList(newDraft.whitelist.ip_cidrs),
|
||||
paths: parseTextareaList(newDraft.whitelist.paths),
|
||||
path_regexes: parseTextareaList(newDraft.whitelist.path_regexes),
|
||||
user_agents: parseTextareaList(newDraft.whitelist.user_agents),
|
||||
};
|
||||
nextConfig.blacklist = {
|
||||
ips: parseTextareaList(newDraft.blacklist.ips),
|
||||
ip_cidrs: parseTextareaList(newDraft.blacklist.ip_cidrs),
|
||||
paths: parseTextareaList(newDraft.blacklist.paths),
|
||||
path_regexes: parseTextareaList(newDraft.blacklist.path_regexes),
|
||||
user_agents: parseTextareaList(newDraft.blacklist.user_agents),
|
||||
};
|
||||
}
|
||||
onChange(newEnabled, nextConfig);
|
||||
};
|
||||
|
||||
const updateList = (
|
||||
scope: 'whitelist' | 'blacklist',
|
||||
key: keyof ProxyRoutePoWConfig['whitelist'],
|
||||
value: string,
|
||||
) => {
|
||||
const nextDraft = {
|
||||
...draft,
|
||||
[scope]: {
|
||||
...draft[scope],
|
||||
[key]: value,
|
||||
},
|
||||
};
|
||||
updateConfig(enabled, {}, nextDraft);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<ToggleField
|
||||
label="启用 PoW 防护"
|
||||
description="启用后,命中该规则组的请求需要完成浏览器计算挑战。"
|
||||
checked={enabled}
|
||||
onChange={(newEnabled) => updateConfig(newEnabled, {})}
|
||||
/>
|
||||
|
||||
<div className="grid gap-5 md:grid-cols-2 xl:grid-cols-4">
|
||||
<ResourceField label="算法">
|
||||
<select
|
||||
value={config.algorithm}
|
||||
onChange={(event) =>
|
||||
updateConfig(enabled, { algorithm: event.target.value as 'fast' | 'slow' })
|
||||
}
|
||||
className="w-full rounded-2xl border border-[var(--border-default)] bg-[var(--control-background)] px-4 py-3 text-sm text-[var(--foreground-primary)] outline-none transition focus:border-[var(--border-strong)]"
|
||||
>
|
||||
<option value="fast">Fast</option>
|
||||
<option value="slow">Slow</option>
|
||||
</select>
|
||||
</ResourceField>
|
||||
<ResourceField label="难度">
|
||||
<ResourceInput
|
||||
type="number"
|
||||
min={1}
|
||||
max={16}
|
||||
value={config.difficulty}
|
||||
onChange={(event) =>
|
||||
updateConfig(enabled, { difficulty: Number(event.target.value) })
|
||||
}
|
||||
/>
|
||||
</ResourceField>
|
||||
<ResourceField label="会话 TTL">
|
||||
<ResourceInput
|
||||
type="number"
|
||||
min={60}
|
||||
value={config.session_ttl}
|
||||
onChange={(event) =>
|
||||
updateConfig(enabled, { session_ttl: Number(event.target.value) })
|
||||
}
|
||||
/>
|
||||
</ResourceField>
|
||||
<ResourceField label="挑战 TTL">
|
||||
<ResourceInput
|
||||
type="number"
|
||||
min={30}
|
||||
value={config.challenge_ttl}
|
||||
onChange={(event) =>
|
||||
updateConfig(enabled, { challenge_ttl: Number(event.target.value) })
|
||||
}
|
||||
/>
|
||||
</ResourceField>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-5 xl:grid-cols-2">
|
||||
{(['whitelist', 'blacklist'] as const).map((scope) => (
|
||||
<div
|
||||
key={scope}
|
||||
className="rounded-[26px] border border-[var(--border-default)] bg-[var(--surface-elevated)] p-5"
|
||||
>
|
||||
<h3 className="text-sm font-semibold text-[var(--foreground-primary)]">
|
||||
{scope === 'whitelist'
|
||||
? '白名单(跳过 PoW)'
|
||||
: '黑名单(必须 PoW)'}
|
||||
</h3>
|
||||
<div className="mt-4 space-y-4">
|
||||
<ResourceField label="IP">
|
||||
<ResourceTextarea
|
||||
className="min-h-20"
|
||||
value={draft[scope].ips}
|
||||
onChange={(event) =>
|
||||
updateList(scope, 'ips', event.target.value)
|
||||
}
|
||||
/>
|
||||
</ResourceField>
|
||||
<ResourceField label="IP CIDR">
|
||||
<ResourceTextarea
|
||||
className="min-h-20"
|
||||
value={draft[scope].ip_cidrs}
|
||||
onChange={(event) =>
|
||||
updateList(scope, 'ip_cidrs', event.target.value)
|
||||
}
|
||||
/>
|
||||
</ResourceField>
|
||||
<ResourceField label="路径">
|
||||
<ResourceTextarea
|
||||
className="min-h-20"
|
||||
value={draft[scope].paths}
|
||||
onChange={(event) =>
|
||||
updateList(scope, 'paths', event.target.value)
|
||||
}
|
||||
/>
|
||||
</ResourceField>
|
||||
<ResourceField label="路径正则">
|
||||
<ResourceTextarea
|
||||
className="min-h-20"
|
||||
value={draft[scope].path_regexes}
|
||||
onChange={(event) =>
|
||||
updateList(scope, 'path_regexes', event.target.value)
|
||||
}
|
||||
/>
|
||||
</ResourceField>
|
||||
<ResourceField label="User-Agent">
|
||||
<ResourceTextarea
|
||||
className="min-h-20"
|
||||
value={draft[scope].user_agents}
|
||||
onChange={(event) =>
|
||||
updateList(scope, 'user_agents', event.target.value)
|
||||
}
|
||||
/>
|
||||
</ResourceField>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function WAFPage() {
|
||||
const queryClient = useQueryClient();
|
||||
const [selectedID, setSelectedID] = useState<number | null>(null);
|
||||
@@ -825,6 +1080,8 @@ export function WAFPage() {
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
|
||||
if (groupsQuery.isLoading || routesQuery.isLoading) {
|
||||
return <LoadingState />;
|
||||
}
|
||||
@@ -853,14 +1110,6 @@ export function WAFPage() {
|
||||
);
|
||||
}
|
||||
|
||||
const enabledGroups = groups.filter((group) => group.enabled);
|
||||
const protectedSites = new Set(
|
||||
groups.flatMap((group) => group.applied_site_ids),
|
||||
);
|
||||
const totalRules = groups.reduce(
|
||||
(sum, group) => sum + countRuleEntries(group),
|
||||
0,
|
||||
);
|
||||
const currentRuleCount = countRuleEntries(draft);
|
||||
const appliedSiteNames = selectedGroup?.is_global
|
||||
? ['全部网站']
|
||||
@@ -950,7 +1199,7 @@ export function WAFPage() {
|
||||
<div className="space-y-6">
|
||||
<PageHeader
|
||||
title="WAF"
|
||||
description="按规则组维护 IP 与地域黑白名单,全局规则始终应用到所有网站。"
|
||||
description="按规则组维护 WAF 与 PoW 防护规则,全局规则组始终应用到所有网站。"
|
||||
action={
|
||||
<PrimaryButton
|
||||
type="button"
|
||||
@@ -1039,7 +1288,7 @@ export function WAFPage() {
|
||||
}
|
||||
>
|
||||
<div className="space-y-6">
|
||||
<div className="grid gap-3 md:grid-cols-3">
|
||||
<div className="grid gap-3 md:grid-cols-4">
|
||||
{tabItems.map((tab) => (
|
||||
<TabButton
|
||||
key={tab.id}
|
||||
@@ -1210,6 +1459,21 @@ export function WAFPage() {
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{activeTab === 'pow' ? (
|
||||
<PowTabPanel
|
||||
key={selectedID}
|
||||
enabled={draft.pow_enabled}
|
||||
config={draft.pow_config}
|
||||
onChange={(enabled, config) =>
|
||||
setDraft((current) => ({
|
||||
...current,
|
||||
pow_enabled: enabled,
|
||||
pow_config: config,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{activeTab === 'block' ? (
|
||||
<div className="grid gap-5 xl:grid-cols-[360px_minmax(0,1fr)]">
|
||||
<div className="space-y-5">
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { ProxyRoutePoWConfig } from '@/features/proxy-routes/types';
|
||||
|
||||
export interface WAFRuleGroup {
|
||||
id: number;
|
||||
name: string;
|
||||
@@ -11,6 +13,8 @@ export interface WAFRuleGroup {
|
||||
country_blacklist: string[];
|
||||
region_whitelist: string[];
|
||||
region_blacklist: string[];
|
||||
pow_enabled: boolean;
|
||||
pow_config: ProxyRoutePoWConfig;
|
||||
remark: string;
|
||||
applied_site_ids: number[];
|
||||
applied_site_count: number;
|
||||
@@ -29,6 +33,8 @@ export interface WAFRuleGroupPayload {
|
||||
country_blacklist: string[];
|
||||
region_whitelist: string[];
|
||||
region_blacklist: string[];
|
||||
pow_enabled: boolean;
|
||||
pow_config: ProxyRoutePoWConfig;
|
||||
remark: string;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user