diff --git a/frontend/src/components/FinalMaskForm.tsx b/frontend/src/components/FinalMaskForm.tsx index 961a2c22..6f4c86e2 100644 --- a/frontend/src/components/FinalMaskForm.tsx +++ b/frontend/src/components/FinalMaskForm.tsx @@ -1,532 +1,503 @@ -import { useMemo } from 'react'; import { Button, Divider, Form, Input, InputNumber, Select, Switch } from 'antd'; import { DeleteOutlined, PlusOutlined, ReloadOutlined } from '@ant-design/icons'; +import type { FormInstance } from 'antd/es/form'; +import type { NamePath } from 'antd/es/form/interface'; import { RandomUtil } from '@/utils'; import { OutboundProtocols } from '@/schemas/primitives'; -interface StreamShape { - network?: string; - kcp?: { mtu?: number }; - finalmask: { - tcp?: MaskRow[]; - udp?: MaskRow[]; - enableQuicParams?: boolean; - quicParams?: QuicParams; - }; - addTcpMask: (type?: string) => void; - delTcpMask: (index: number) => void; - addUdpMask: (type?: string) => void; - delUdpMask: (index: number) => void; -} +// Pattern A FinalMaskForm. Renders a Fragment of Form.Items at absolute +// paths under `name`; the parent modal owns the Form instance and the +// surrounding layout. The legacy class-coupled component (which mutated +// `stream.finalmask.*` via .addTcpMask/.delTcpMask methods) is gone — all +// state lives in the parent form values, accessed via the `form` and +// `name` props. -interface MaskRow { - type: string; - settings: Record; - _getDefaultSettings: (type: string, settings: Record) => Record; -} - -interface ItemRow { - type: string; - packet: string | unknown[]; - delay?: number | string; - rand?: number | string; - randRange?: string; -} - -interface QuicParams { - congestion: string; - debug?: boolean; - brutalUp?: number | string; - brutalDown?: number | string; - hasUdpHop?: boolean; - udpHop?: { ports: string; interval: string | number }; - maxIdleTimeout?: number; - keepAlivePeriod?: number; - disablePathMTUDiscovery?: boolean; - maxIncomingStreams?: number; - initStreamReceiveWindow?: number; - maxStreamReceiveWindow?: number; - initConnectionReceiveWindow?: number; - maxConnectionReceiveWindow?: number; -} - -interface FinalMaskFormProps { - stream: StreamShape; +export interface FinalMaskFormProps { + name: NamePath; + network: string; protocol: string; - onChange: () => void; + form: FormInstance; } -function changeMaskType(mask: MaskRow, type: string) { - mask.type = type; - mask.settings = mask._getDefaultSettings(type, {}); +const TCP_NETWORKS = ['raw', 'tcp', 'httpupgrade', 'ws', 'grpc', 'xhttp']; + +function asPath(name: NamePath): (string | number)[] { + return Array.isArray(name) ? [...name] : [name]; } -function changeItemType(item: ItemRow, type: string) { - item.type = type; - if (type === 'base64') item.packet = RandomUtil.randomBase64(); - else if (type === 'array') { - item.rand = 0; - item.packet = []; - } else item.packet = ''; +function defaultTcpMaskSettings(type: string): Record { + switch (type) { + case 'fragment': + return { packets: '1-3', length: '', delay: '', maxSplit: '' }; + case 'sudoku': + return { + password: '', ascii: '', customTable: '', customTables: '', + paddingMin: 0, paddingMax: 0, + }; + case 'header-custom': + return { clients: [], servers: [] }; + default: + return {}; + } } -function newClientServerItem(): ItemRow { +function defaultUdpMaskSettings(type: string): Record { + switch (type) { + case 'salamander': + case 'mkcp-aes128gcm': + return { password: '' }; + case 'header-dns': + return { domain: '' }; + case 'xdns': + return { domains: [] }; + case 'xicmp': + return { ip: '0.0.0.0', id: 0 }; + case 'header-custom': + return { client: [], server: [] }; + case 'noise': + return { reset: 0, noise: [] }; + default: + return {}; + } +} + +function defaultClientServerItem(): Record { return { delay: 0, rand: 0, randRange: '0-255', type: 'array', packet: [] }; } -function newUdpClientServerItem(): ItemRow { +function defaultUdpClientServerItem(): Record { return { rand: 0, randRange: '0-255', type: 'array', packet: [] }; } -function newNoiseItem(): ItemRow { - return { rand: '1-8192', randRange: '0-255', type: 'array', packet: [], delay: '10-20' }; +function defaultNoiseItem(): Record { + return { + rand: '1-8192', randRange: '0-255', type: 'array', packet: [], delay: '10-20', + }; } -export default function FinalMaskForm({ stream, protocol, onChange }: FinalMaskFormProps) { - const isHysteria = protocol === OutboundProtocols.Hysteria || protocol === 'hysteria'; - const network = stream?.network || ''; +function defaultQuicParams(): Record { + return { + congestion: 'bbr', + debug: false, + udpHop: { ports: '20000-50000', interval: 5 }, + }; +} - const showTcp = useMemo( - () => ['raw', 'tcp', 'httpupgrade', 'ws', 'grpc', 'xhttp'].includes(network), - [network], - ); +export default function FinalMaskForm({ name, network, protocol, form }: FinalMaskFormProps) { + const base = asPath(name); + const isHysteria = protocol === OutboundProtocols.Hysteria || protocol === 'hysteria'; + const showTcp = TCP_NETWORKS.includes(network); const showUdp = isHysteria || network === 'kcp'; const showQuic = isHysteria || network === 'xhttp'; - - function notify() { - onChange(); - } - - function changeUdpMaskType(mask: MaskRow, type: string) { - changeMaskType(mask, type); - if (network === 'kcp' && stream.kcp) { - stream.kcp.mtu = type === 'xdns' ? 900 : 1350; - } - notify(); - } - - function addUdpMaskWithDefault() { - const def = isHysteria ? 'salamander' : 'mkcp-aes128gcm'; - stream.addUdpMask(def); - notify(); - } - - const tcpMasks = stream.finalmask.tcp || []; - const udpMasks = stream.finalmask.udp || []; + const enableQuic = Form.useWatch([...base, 'enableQuicParams'], form); if (!showTcp && !showUdp && !showQuic) return null; return ( -
- {showTcp && ( + <> + {showTcp && } + {showUdp && } + {showQuic && ( + <> + + { + if (v) { + const current = form.getFieldValue([...base, 'quicParams']); + if (!current) form.setFieldValue([...base, 'quicParams'], defaultQuicParams()); + } + }} + /> + + {enableQuic && } + + )} + + ); +} + +function TcpMasksList({ base, form }: { base: (string | number)[]; form: FormInstance }) { + return ( + + {(fields, { add, remove }) => ( <>