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