import { useState, useId } from 'react'; import { useFormContext, useWatch } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; import { Alert, Button, Collapse, Descriptions, Divider, Form, Input, InputNumber, Select, Space, Switch, } from 'antd'; import { RadarChartOutlined, ReloadOutlined, SearchOutlined } from '@ant-design/icons'; import dayjs from 'dayjs'; import { selectTagRender } from '@/components/form'; import { FormField } from '@/components/form/rhf'; import { UTLS_FINGERPRINT } from '@/schemas/primitives'; import { validateRealityClientVer, validateRealityMaxClientVer, validateRealityTarget, } from '@/lib/xray/stream-wire-normalize'; import type { RealityScanResult } from '@/generated/types'; import RealityTargetScannerModal, { MLDSA65_MIN_CERT_CHAIN_BYTES, } from './RealityTargetScannerModal'; interface RealityFormProps { saving: boolean; scanning: boolean; scanResult: RealityScanResult | null; scanRealityTarget: (allowPrivate?: boolean) => void; scanRealityCandidates: (targets?: string) => Promise; applyRealityScanResult: (result: RealityScanResult, replaceServerNames?: boolean) => void; randomizeShortIds: () => void; randomizeSpiderX: () => void; genRealityKeypair: () => void; clearRealityKeypair: () => void; genMldsa65: () => void; clearMldsa65: () => void; } export default function RealityForm({ saving, scanning, scanResult, scanRealityTarget, scanRealityCandidates, applyRealityScanResult, randomizeShortIds, randomizeSpiderX, genRealityKeypair, clearRealityKeypair, genMldsa65, clearMldsa65, }: RealityFormProps) { const { t } = useTranslation(); const uid = useId(); const { getFieldState, trigger } = useFormContext(); const [scannerOpen, setScannerOpen] = useState(false); const mldsa65Seed = useWatch({ name: 'streamSettings.realitySettings.mldsa65Seed' }); const mldsa65Verify = useWatch({ name: 'streamSettings.realitySettings.settings.mldsa65Verify', }); const mldsa65Enabled = (typeof mldsa65Seed === 'string' && mldsa65Seed.trim() !== '') || (typeof mldsa65Verify === 'string' && mldsa65Verify.trim() !== ''); const mldsaChainTooSmall = !!scanResult && mldsa65Enabled && scanResult.certChainBytes > 0 && scanResult.certChainBytes < MLDSA65_MIN_CERT_CHAIN_BYTES; /* * An untrusted certificate (self-signed fronting service on the LAN) is still * worth reading, so subject/issuer stay visible and only the verdict is added. */ const certSummary = (r: RealityScanResult) => { const who = r.certSubject && r.certIssuer ? `${r.certSubject} (${r.certIssuer})` : r.certSubject || r.certIssuer; if (!who) return '—'; return r.certValid ? who : `${who} — ${t('pages.inbounds.form.scanCertInvalid')}`; }; const maxClientVerPath = 'streamSettings.realitySettings.maxClientVer'; const revalidateMaxClientVer = () => { if (getFieldState(maxClientVerPath).error) { void trigger(maxClientVerPath); } }; return ( <> {scanResult && ( {mldsaChainTooSmall && (
{t('pages.inbounds.form.scanMldsaCertChainTooSmall', { length: scanResult.certChainBytes, min: MLDSA65_MIN_CERT_CHAIN_BYTES, })}
)} {scanResult.privateTarget && (
{t('pages.inbounds.form.scanPrivateNote')}
)} {scanResult.host || '—'} {scanResult.tlsVersion || '—'} {scanResult.alpn || '—'} {scanResult.curveID || '—'} {certSummary(scanResult)} {scanResult.notAfter ? dayjs(scanResult.notAfter).format('YYYY-MM-DD HH:mm') : '—'} {scanResult.certChainBytes > 0 ? `${scanResult.certChainBytes} B` : '—'} {scanResult.latencyMs > 0 ? `${scanResult.latencyMs} ms` : '—'} } />
)} { const max = typeof value === 'string' ? value : ''; const min = formValues?.streamSettings?.realitySettings?.minClientVer; const errKey = validateRealityMaxClientVer(max, typeof min === 'string' ? min : ''); return errKey ? errKey : true; }, }} > {(['limitFallbackUpload', 'limitFallbackDownload'] as const).map((dir) => (
{t(`pages.inbounds.form.${dir}`)}
))} ), }, ]} /> setScannerOpen(false)} scanRealityCandidates={scanRealityCandidates} onPick={(r) => applyRealityScanResult(r, true)} mldsa65Enabled={mldsa65Enabled} /> ); }