import { useEffect, useMemo, useRef } from 'react' import * as z from 'zod' import { useForm } from 'react-hook-form' import { zodResolver } from '@hookform/resolvers/zod' import { useTranslation } from 'react-i18next' import { toast } from 'sonner' import { Button } from '@/components/ui/button' import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, } from '@/components/ui/form' import { Input } from '@/components/ui/input' import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select' import { Switch } from '@/components/ui/switch' import { Textarea } from '@/components/ui/textarea' import { SettingsSection } from '../components/settings-section' import { useUpdateOption } from '../hooks/use-update-option' const ssrfSchema = z.object({ fetch_setting: z.object({ enable_ssrf_protection: z.boolean(), allow_private_ip: z.boolean(), domain_filter_mode: z.boolean(), ip_filter_mode: z.boolean(), domain_list: z.string(), ip_list: z.string(), allowed_ports: z.string(), apply_ip_filter_for_domain: z.boolean(), }), }) type SSRFFormValues = z.output type SSRFFormInput = z.input type NormalizedSSRFValues = { 'fetch_setting.enable_ssrf_protection': boolean 'fetch_setting.allow_private_ip': boolean 'fetch_setting.domain_filter_mode': boolean 'fetch_setting.ip_filter_mode': boolean 'fetch_setting.domain_list': string[] 'fetch_setting.ip_list': string[] 'fetch_setting.allowed_ports': number[] 'fetch_setting.apply_ip_filter_for_domain': boolean } type SSRFSectionProps = { defaultValues: { 'fetch_setting.enable_ssrf_protection': boolean 'fetch_setting.allow_private_ip': boolean 'fetch_setting.domain_filter_mode': boolean 'fetch_setting.ip_filter_mode': boolean 'fetch_setting.domain_list': string[] 'fetch_setting.ip_list': string[] 'fetch_setting.allowed_ports': number[] 'fetch_setting.apply_ip_filter_for_domain': boolean } } const splitLines = (value: string) => value .split('\n') .map((entry) => entry.trim()) .filter(Boolean) const parsePorts = (value: string) => value .split(',') .map((item) => Number.parseInt(item.trim(), 10)) .filter((port) => Number.isFinite(port)) const buildFormDefaults = ( defaults: SSRFSectionProps['defaultValues'] ): SSRFFormInput => ({ fetch_setting: { enable_ssrf_protection: defaults['fetch_setting.enable_ssrf_protection'], allow_private_ip: defaults['fetch_setting.allow_private_ip'], domain_filter_mode: defaults['fetch_setting.domain_filter_mode'], ip_filter_mode: defaults['fetch_setting.ip_filter_mode'], domain_list: defaults['fetch_setting.domain_list'].join('\n'), ip_list: defaults['fetch_setting.ip_list'].join('\n'), allowed_ports: defaults['fetch_setting.allowed_ports'].join(','), apply_ip_filter_for_domain: defaults['fetch_setting.apply_ip_filter_for_domain'], }, }) const normalizeDefaults = ( defaults: SSRFSectionProps['defaultValues'] ): NormalizedSSRFValues => ({ 'fetch_setting.enable_ssrf_protection': defaults['fetch_setting.enable_ssrf_protection'], 'fetch_setting.allow_private_ip': defaults['fetch_setting.allow_private_ip'], 'fetch_setting.domain_filter_mode': defaults['fetch_setting.domain_filter_mode'], 'fetch_setting.ip_filter_mode': defaults['fetch_setting.ip_filter_mode'], 'fetch_setting.domain_list': defaults['fetch_setting.domain_list'], 'fetch_setting.ip_list': defaults['fetch_setting.ip_list'], 'fetch_setting.allowed_ports': defaults['fetch_setting.allowed_ports'], 'fetch_setting.apply_ip_filter_for_domain': defaults['fetch_setting.apply_ip_filter_for_domain'], }) const normalizeFormValues = (values: SSRFFormValues): NormalizedSSRFValues => ({ 'fetch_setting.enable_ssrf_protection': values.fetch_setting.enable_ssrf_protection, 'fetch_setting.allow_private_ip': values.fetch_setting.allow_private_ip, 'fetch_setting.domain_filter_mode': values.fetch_setting.domain_filter_mode, 'fetch_setting.ip_filter_mode': values.fetch_setting.ip_filter_mode, 'fetch_setting.domain_list': splitLines(values.fetch_setting.domain_list), 'fetch_setting.ip_list': splitLines(values.fetch_setting.ip_list), 'fetch_setting.allowed_ports': parsePorts(values.fetch_setting.allowed_ports), 'fetch_setting.apply_ip_filter_for_domain': values.fetch_setting.apply_ip_filter_for_domain, }) const isEqual = (a: unknown, b: unknown) => { if (Array.isArray(a) && Array.isArray(b)) { return JSON.stringify(a) === JSON.stringify(b) } return a === b } export function SSRFSection({ defaultValues }: SSRFSectionProps) { const { t } = useTranslation() const updateOption = useUpdateOption() const baselineRef = useRef( normalizeDefaults(defaultValues) ) const formDefaults = useMemo( () => buildFormDefaults(defaultValues), [defaultValues] ) const form = useForm({ resolver: zodResolver(ssrfSchema), defaultValues: formDefaults, }) useEffect(() => { baselineRef.current = normalizeDefaults(defaultValues) form.reset(buildFormDefaults(defaultValues)) }, [defaultValues, form]) const onSubmit = async (data: SSRFFormValues) => { const normalized = normalizeFormValues(data) const updates = ( Object.keys(normalized) as Array ).filter((key) => !isEqual(normalized[key], baselineRef.current[key])) if (updates.length === 0) { toast.info(t('No changes to save')) return } for (const key of updates) { const value = normalized[key] await updateOption.mutateAsync({ key, value: Array.isArray(value) ? JSON.stringify(value) : value, }) } baselineRef.current = normalized } const domainFilterMode = form.watch('fetch_setting.domain_filter_mode') const ipFilterMode = form.watch('fetch_setting.ip_filter_mode') return (
(
{t('Enable SSRF Protection')} {t('Prevent server-side request forgery attacks')}
)} /> (
{t('Allow Private IPs')} {t( 'Allow requests to private IP ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16)' )}
)} /> ( {t('Domain Filter Mode')} {t('Choose how to filter domains')} )} /> ( {t('Domain')}{' '} {domainFilterMode ? t('Whitelist') : t('Blacklist')}