mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-06 07:36:37 +08:00
[优化] POW 与 WAF 合并
This commit is contained in:
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user