mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-06 13:14:11 +00:00
refactor(frontend): extract inbound security forms into security/ folder
Move the inbound TLS and Reality stream-security blocks out of InboundFormModal into presentational components under inbounds/form/security/. The Radio.Group security selector stays in the modal; TlsForm and RealityForm receive their cert/key/ECH generation handlers and the saving flag as props. InboundFormModal drops from 2105 to 1708 lines. Add inbound-form-blocks.test.tsx: render-snapshot coverage for each extracted transport (raw/ws/grpc/kcp/httpupgrade/xhttp) and security (tls/reality) component in isolation inside a minimal Form. The full modal cannot exercise the stream/security tabs in jsdom because they are gated behind Form.useWatch values that do not propagate in the test harness, so component-level snapshots are the regression net for these blocks. No behavior change.
This commit is contained in:
parent
57d66ec9ff
commit
66348db37d
6 changed files with 663 additions and 419 deletions
|
|
@ -24,7 +24,6 @@ import {
|
||||||
DeleteOutlined,
|
DeleteOutlined,
|
||||||
MinusOutlined,
|
MinusOutlined,
|
||||||
PlusOutlined,
|
PlusOutlined,
|
||||||
ReloadOutlined,
|
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
|
|
||||||
import { HttpUtil, NumberFormatter, RandomUtil, SizeFormatter, Wireguard } from '@/utils';
|
import { HttpUtil, NumberFormatter, RandomUtil, SizeFormatter, Wireguard } from '@/utils';
|
||||||
|
|
@ -54,9 +53,6 @@ import {
|
||||||
Protocols,
|
Protocols,
|
||||||
SNIFFING_OPTION,
|
SNIFFING_OPTION,
|
||||||
TCP_CONGESTION_OPTION,
|
TCP_CONGESTION_OPTION,
|
||||||
TLS_CIPHER_OPTION,
|
|
||||||
TLS_VERSION_OPTION,
|
|
||||||
USAGE_OPTION,
|
|
||||||
UTLS_FINGERPRINT,
|
UTLS_FINGERPRINT,
|
||||||
} from '@/schemas/primitives';
|
} from '@/schemas/primitives';
|
||||||
import {
|
import {
|
||||||
|
|
@ -97,8 +93,8 @@ import {
|
||||||
WsForm,
|
WsForm,
|
||||||
XhttpForm,
|
XhttpForm,
|
||||||
} from './transport';
|
} from './transport';
|
||||||
|
import { RealityForm, TlsForm } from './security';
|
||||||
|
|
||||||
const { TextArea } = Input;
|
|
||||||
import { coerceInboundJsonField, type DBInbound } from '@/models/dbinbound';
|
import { coerceInboundJsonField, type DBInbound } from '@/models/dbinbound';
|
||||||
import type { NodeRecord } from '@/api/queries/useNodesQuery';
|
import type { NodeRecord } from '@/api/queries/useNodesQuery';
|
||||||
|
|
||||||
|
|
@ -1481,421 +1477,28 @@ export default function InboundFormModal({
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item
|
{security === 'tls' && (
|
||||||
noStyle
|
<TlsForm
|
||||||
shouldUpdate={(prev, curr) =>
|
saving={saving}
|
||||||
prev.streamSettings?.security !== curr.streamSettings?.security
|
setCertFromPanel={setCertFromPanel}
|
||||||
}
|
clearCertFiles={clearCertFiles}
|
||||||
>
|
generateRandomPinHash={generateRandomPinHash}
|
||||||
{({ getFieldValue }) => {
|
getNewEchCert={getNewEchCert}
|
||||||
const sec = getFieldValue(['streamSettings', 'security']);
|
clearEchCert={clearEchCert}
|
||||||
if (sec !== 'tls') return null;
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Form.Item name={['streamSettings', 'tlsSettings', 'serverName']} label="SNI">
|
|
||||||
<Input placeholder={t('pages.inbounds.form.serverNameIndication')} />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item name={['streamSettings', 'tlsSettings', 'cipherSuites']} label={t('pages.inbounds.form.cipherSuites')}>
|
|
||||||
<Select
|
|
||||||
options={[
|
|
||||||
{ value: '', label: t('pages.inbounds.form.autoOption') },
|
|
||||||
...Object.entries(TLS_CIPHER_OPTION).map(([k, v]) => ({ value: v, label: k })),
|
|
||||||
]}
|
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
|
||||||
<Form.Item label={t('pages.inbounds.form.minMaxVersion')}>
|
|
||||||
<Space.Compact block>
|
|
||||||
<Form.Item name={['streamSettings', 'tlsSettings', 'minVersion']} noStyle>
|
|
||||||
<Select
|
|
||||||
style={{ width: '50%' }}
|
|
||||||
options={Object.values(TLS_VERSION_OPTION).map((v) => ({ value: v, label: v }))}
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item name={['streamSettings', 'tlsSettings', 'maxVersion']} noStyle>
|
|
||||||
<Select
|
|
||||||
style={{ width: '50%' }}
|
|
||||||
options={Object.values(TLS_VERSION_OPTION).map((v) => ({ value: v, label: v }))}
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
</Space.Compact>
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
name={['streamSettings', 'tlsSettings', 'settings', 'fingerprint']}
|
|
||||||
label="uTLS"
|
|
||||||
>
|
|
||||||
<Select
|
|
||||||
options={[
|
|
||||||
{ value: '', label: 'None' },
|
|
||||||
...Object.values(UTLS_FINGERPRINT).map((fp) => ({ value: fp, label: fp })),
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item name={['streamSettings', 'tlsSettings', 'alpn']} label="ALPN">
|
|
||||||
<Select
|
|
||||||
mode="multiple"
|
|
||||||
tokenSeparators={[',']}
|
|
||||||
style={{ width: '100%' }}
|
|
||||||
options={Object.values(ALPN_OPTION).map((a) => ({ value: a, label: a }))}
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
name={['streamSettings', 'tlsSettings', 'rejectUnknownSni']}
|
|
||||||
label={t('pages.inbounds.form.rejectUnknownSni')}
|
|
||||||
valuePropName="checked"
|
|
||||||
>
|
|
||||||
<Switch />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
name={['streamSettings', 'tlsSettings', 'disableSystemRoot']}
|
|
||||||
label={t('pages.inbounds.form.disableSystemRoot')}
|
|
||||||
valuePropName="checked"
|
|
||||||
>
|
|
||||||
<Switch />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
name={['streamSettings', 'tlsSettings', 'enableSessionResumption']}
|
|
||||||
label={t('pages.inbounds.form.sessionResumption')}
|
|
||||||
valuePropName="checked"
|
|
||||||
>
|
|
||||||
<Switch />
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.List name={['streamSettings', 'tlsSettings', 'certificates']}>
|
|
||||||
{(certFields, { add, remove }) => (
|
|
||||||
<>
|
|
||||||
<Form.Item label={t('certificate')}>
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
size="small"
|
|
||||||
onClick={() => add({
|
|
||||||
useFile: true,
|
|
||||||
certificateFile: '',
|
|
||||||
keyFile: '',
|
|
||||||
certificate: [],
|
|
||||||
key: [],
|
|
||||||
oneTimeLoading: false,
|
|
||||||
usage: 'encipherment',
|
|
||||||
buildChain: false,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<PlusOutlined />
|
|
||||||
</Button>
|
|
||||||
</Form.Item>
|
|
||||||
{certFields.map((certField, idx) => (
|
|
||||||
<div key={certField.key}>
|
|
||||||
<Form.Item
|
|
||||||
name={[certField.name, 'useFile']}
|
|
||||||
label={`${t('certificate')} ${idx + 1}`}
|
|
||||||
>
|
|
||||||
<Radio.Group buttonStyle="solid">
|
|
||||||
<Radio.Button value={true}>
|
|
||||||
{t('pages.inbounds.certificatePath')}
|
|
||||||
</Radio.Button>
|
|
||||||
<Radio.Button value={false}>
|
|
||||||
{t('pages.inbounds.certificateContent')}
|
|
||||||
</Radio.Button>
|
|
||||||
</Radio.Group>
|
|
||||||
</Form.Item>
|
|
||||||
{certFields.length > 1 && (
|
|
||||||
<Form.Item label=" ">
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
danger
|
|
||||||
onClick={() => remove(certField.name)}
|
|
||||||
>
|
|
||||||
<MinusOutlined /> {t('remove')}
|
|
||||||
</Button>
|
|
||||||
</Form.Item>
|
|
||||||
)}
|
)}
|
||||||
<Form.Item
|
|
||||||
noStyle
|
{security === 'reality' && (
|
||||||
shouldUpdate={(prev, curr) =>
|
<RealityForm
|
||||||
prev.streamSettings?.tlsSettings?.certificates?.[certField.name]?.useFile
|
saving={saving}
|
||||||
!== curr.streamSettings?.tlsSettings?.certificates?.[certField.name]?.useFile
|
randomizeRealityTarget={randomizeRealityTarget}
|
||||||
}
|
randomizeShortIds={randomizeShortIds}
|
||||||
>
|
genRealityKeypair={genRealityKeypair}
|
||||||
{({ getFieldValue }) => {
|
clearRealityKeypair={clearRealityKeypair}
|
||||||
const useFile = getFieldValue([
|
genMldsa65={genMldsa65}
|
||||||
'streamSettings', 'tlsSettings', 'certificates',
|
clearMldsa65={clearMldsa65}
|
||||||
certField.name, 'useFile',
|
|
||||||
]);
|
|
||||||
return useFile ? (
|
|
||||||
<>
|
|
||||||
<Form.Item
|
|
||||||
name={[certField.name, 'certificateFile']}
|
|
||||||
label={t('pages.inbounds.publicKey')}
|
|
||||||
>
|
|
||||||
<Input />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
name={[certField.name, 'keyFile']}
|
|
||||||
label={t('pages.inbounds.privatekey')}
|
|
||||||
>
|
|
||||||
<Input />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item label=" ">
|
|
||||||
<Space>
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
loading={saving}
|
|
||||||
onClick={() => setCertFromPanel(certField.name)}
|
|
||||||
>
|
|
||||||
{t('pages.inbounds.setDefaultCert')}
|
|
||||||
</Button>
|
|
||||||
<Button danger onClick={() => clearCertFiles(certField.name)}>
|
|
||||||
{t('clear')}
|
|
||||||
</Button>
|
|
||||||
</Space>
|
|
||||||
</Form.Item>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Form.Item
|
|
||||||
name={[certField.name, 'certificate']}
|
|
||||||
label={t('pages.inbounds.publicKey')}
|
|
||||||
normalize={(v) => typeof v === 'string'
|
|
||||||
? v.split('\n')
|
|
||||||
: v}
|
|
||||||
getValueProps={(v) => ({
|
|
||||||
value: Array.isArray(v) ? v.join('\n') : v,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<TextArea autoSize={{ minRows: 3, maxRows: 8 }} />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
name={[certField.name, 'key']}
|
|
||||||
label={t('pages.inbounds.privatekey')}
|
|
||||||
normalize={(v) => typeof v === 'string'
|
|
||||||
? v.split('\n')
|
|
||||||
: v}
|
|
||||||
getValueProps={(v) => ({
|
|
||||||
value: Array.isArray(v) ? v.join('\n') : v,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<TextArea autoSize={{ minRows: 3, maxRows: 8 }} />
|
|
||||||
</Form.Item>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
name={[certField.name, 'oneTimeLoading']}
|
|
||||||
label={t('pages.inbounds.form.oneTimeLoading')}
|
|
||||||
valuePropName="checked"
|
|
||||||
>
|
|
||||||
<Switch />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
name={[certField.name, 'usage']}
|
|
||||||
label={t('pages.inbounds.form.usageOption')}
|
|
||||||
>
|
|
||||||
<Select
|
|
||||||
style={{ width: '50%' }}
|
|
||||||
options={Object.values(USAGE_OPTION).map((u) => ({ value: u, label: u }))}
|
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
noStyle
|
|
||||||
shouldUpdate={(prev, curr) =>
|
|
||||||
prev.streamSettings?.tlsSettings?.certificates?.[certField.name]?.usage
|
|
||||||
!== curr.streamSettings?.tlsSettings?.certificates?.[certField.name]?.usage
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{({ getFieldValue }) => {
|
|
||||||
const usage = getFieldValue([
|
|
||||||
'streamSettings', 'tlsSettings', 'certificates',
|
|
||||||
certField.name, 'usage',
|
|
||||||
]);
|
|
||||||
if (usage !== 'issue') return null;
|
|
||||||
return (
|
|
||||||
<Form.Item
|
|
||||||
name={[certField.name, 'buildChain']}
|
|
||||||
label={t('pages.inbounds.form.buildChain')}
|
|
||||||
valuePropName="checked"
|
|
||||||
>
|
|
||||||
<Switch />
|
|
||||||
</Form.Item>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
</Form.Item>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</Form.List>
|
|
||||||
|
|
||||||
<Form.Item name={['streamSettings', 'tlsSettings', 'echServerKeys']} label={t('pages.inbounds.form.echKey')}>
|
|
||||||
<Input />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
name={['streamSettings', 'tlsSettings', 'settings', 'echConfigList']}
|
|
||||||
label={t('pages.inbounds.form.echConfig')}
|
|
||||||
>
|
|
||||||
<Input />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
label={t('pages.inbounds.form.pinnedPeerCertSha256')}
|
|
||||||
tooltip={t('pages.inbounds.form.pinnedPeerCertSha256Tip')}
|
|
||||||
>
|
|
||||||
<Space.Compact block>
|
|
||||||
<Form.Item
|
|
||||||
name={['streamSettings', 'tlsSettings', 'settings', 'pinnedPeerCertSha256']}
|
|
||||||
noStyle
|
|
||||||
>
|
|
||||||
<Select
|
|
||||||
mode="tags"
|
|
||||||
tokenSeparators={[',', ' ']}
|
|
||||||
placeholder={t('pages.inbounds.form.pinnedPeerCertSha256Placeholder')}
|
|
||||||
style={{ width: 'calc(100% - 32px)' }}
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
<Button
|
|
||||||
icon={<ReloadOutlined />}
|
|
||||||
onClick={generateRandomPinHash}
|
|
||||||
title={t('pages.inbounds.form.generateRandomPin')}
|
|
||||||
/>
|
|
||||||
</Space.Compact>
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item label=" ">
|
|
||||||
<Space>
|
|
||||||
<Button type="primary" loading={saving} onClick={getNewEchCert}>
|
|
||||||
{t('pages.inbounds.form.getNewEchCert')}
|
|
||||||
</Button>
|
|
||||||
<Button danger onClick={clearEchCert}>{t('clear')}</Button>
|
|
||||||
</Space>
|
|
||||||
</Form.Item>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item
|
|
||||||
noStyle
|
|
||||||
shouldUpdate={(prev, curr) =>
|
|
||||||
prev.streamSettings?.security !== curr.streamSettings?.security
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{({ getFieldValue }) => {
|
|
||||||
const sec = getFieldValue(['streamSettings', 'security']);
|
|
||||||
if (sec !== 'reality') return null;
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Form.Item
|
|
||||||
name={['streamSettings', 'realitySettings', 'show']}
|
|
||||||
label={t('pages.inbounds.form.show')}
|
|
||||||
valuePropName="checked"
|
|
||||||
>
|
|
||||||
<Switch />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item name={['streamSettings', 'realitySettings', 'xver']} label={t('pages.inbounds.form.xver')}>
|
|
||||||
<InputNumber min={0} />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
name={['streamSettings', 'realitySettings', 'settings', 'fingerprint']}
|
|
||||||
label="uTLS"
|
|
||||||
>
|
|
||||||
<Select
|
|
||||||
options={Object.values(UTLS_FINGERPRINT).map((fp) => ({ value: fp, label: fp }))}
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item label={t('pages.inbounds.form.target')}>
|
|
||||||
<Space.Compact block>
|
|
||||||
<Form.Item name={['streamSettings', 'realitySettings', 'target']} noStyle>
|
|
||||||
<Input style={{ width: 'calc(100% - 32px)' }} />
|
|
||||||
</Form.Item>
|
|
||||||
<Button icon={<ReloadOutlined />} onClick={randomizeRealityTarget} />
|
|
||||||
</Space.Compact>
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item label="SNI">
|
|
||||||
<Space.Compact block style={{ display: 'flex' }}>
|
|
||||||
<Form.Item
|
|
||||||
name={['streamSettings', 'realitySettings', 'serverNames']}
|
|
||||||
noStyle
|
|
||||||
>
|
|
||||||
<Select mode="tags" tokenSeparators={[',']} style={{ flex: 1 }} />
|
|
||||||
</Form.Item>
|
|
||||||
<Button icon={<ReloadOutlined />} onClick={randomizeRealityTarget} />
|
|
||||||
</Space.Compact>
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
name={['streamSettings', 'realitySettings', 'maxTimediff']}
|
|
||||||
label={t('pages.inbounds.form.maxTimeDiff')}
|
|
||||||
>
|
|
||||||
<InputNumber min={0} />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
name={['streamSettings', 'realitySettings', 'minClientVer']}
|
|
||||||
label={t('pages.inbounds.form.minClientVer')}
|
|
||||||
>
|
|
||||||
<Input placeholder="25.9.11" />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
name={['streamSettings', 'realitySettings', 'maxClientVer']}
|
|
||||||
label={t('pages.inbounds.form.maxClientVer')}
|
|
||||||
>
|
|
||||||
<Input placeholder="25.9.11" />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item label={t('pages.inbounds.form.shortIds')}>
|
|
||||||
<Space.Compact block style={{ display: 'flex' }}>
|
|
||||||
<Form.Item
|
|
||||||
name={['streamSettings', 'realitySettings', 'shortIds']}
|
|
||||||
noStyle
|
|
||||||
>
|
|
||||||
<Select mode="tags" tokenSeparators={[',']} style={{ flex: 1 }} />
|
|
||||||
</Form.Item>
|
|
||||||
<Button icon={<ReloadOutlined />} onClick={randomizeShortIds} />
|
|
||||||
</Space.Compact>
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
name={['streamSettings', 'realitySettings', 'settings', 'spiderX']}
|
|
||||||
label={t('pages.inbounds.form.spiderX')}
|
|
||||||
>
|
|
||||||
<Input />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
name={['streamSettings', 'realitySettings', 'settings', 'publicKey']}
|
|
||||||
label={t('pages.inbounds.publicKey')}
|
|
||||||
>
|
|
||||||
<Input.TextArea autoSize={{ minRows: 1, maxRows: 4 }} />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
name={['streamSettings', 'realitySettings', 'privateKey']}
|
|
||||||
label={t('pages.inbounds.privatekey')}
|
|
||||||
>
|
|
||||||
<Input.TextArea autoSize={{ minRows: 1, maxRows: 4 }} />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item label=" ">
|
|
||||||
<Space>
|
|
||||||
<Button type="primary" loading={saving} onClick={genRealityKeypair}>
|
|
||||||
{t('pages.inbounds.form.getNewCert')}
|
|
||||||
</Button>
|
|
||||||
<Button danger onClick={clearRealityKeypair}>{t('clear')}</Button>
|
|
||||||
</Space>
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
name={['streamSettings', 'realitySettings', 'mldsa65Seed']}
|
|
||||||
label={t('pages.inbounds.form.mldsa65Seed')}
|
|
||||||
>
|
|
||||||
<Input.TextArea autoSize={{ minRows: 2, maxRows: 6 }} />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
name={['streamSettings', 'realitySettings', 'settings', 'mldsa65Verify']}
|
|
||||||
label={t('pages.inbounds.form.mldsa65Verify')}
|
|
||||||
>
|
|
||||||
<Input.TextArea autoSize={{ minRows: 2, maxRows: 6 }} />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item label=" ">
|
|
||||||
<Space>
|
|
||||||
<Button type="primary" loading={saving} onClick={genMldsa65}>
|
|
||||||
{t('pages.inbounds.form.getNewSeed')}
|
|
||||||
</Button>
|
|
||||||
<Button danger onClick={clearMldsa65}>{t('clear')}</Button>
|
|
||||||
</Space>
|
|
||||||
</Form.Item>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
</Form.Item>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
2
frontend/src/pages/inbounds/form/security/index.ts
Normal file
2
frontend/src/pages/inbounds/form/security/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
export { default as TlsForm } from './tls';
|
||||||
|
export { default as RealityForm } from './reality';
|
||||||
143
frontend/src/pages/inbounds/form/security/reality.tsx
Normal file
143
frontend/src/pages/inbounds/form/security/reality.tsx
Normal file
|
|
@ -0,0 +1,143 @@
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Button, Form, Input, InputNumber, Select, Space, Switch } from 'antd';
|
||||||
|
import { ReloadOutlined } from '@ant-design/icons';
|
||||||
|
|
||||||
|
import { UTLS_FINGERPRINT } from '@/schemas/primitives';
|
||||||
|
|
||||||
|
interface RealityFormProps {
|
||||||
|
saving: boolean;
|
||||||
|
randomizeRealityTarget: () => void;
|
||||||
|
randomizeShortIds: () => void;
|
||||||
|
genRealityKeypair: () => void;
|
||||||
|
clearRealityKeypair: () => void;
|
||||||
|
genMldsa65: () => void;
|
||||||
|
clearMldsa65: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function RealityForm({
|
||||||
|
saving,
|
||||||
|
randomizeRealityTarget,
|
||||||
|
randomizeShortIds,
|
||||||
|
genRealityKeypair,
|
||||||
|
clearRealityKeypair,
|
||||||
|
genMldsa65,
|
||||||
|
clearMldsa65,
|
||||||
|
}: RealityFormProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Form.Item
|
||||||
|
name={['streamSettings', 'realitySettings', 'show']}
|
||||||
|
label={t('pages.inbounds.form.show')}
|
||||||
|
valuePropName="checked"
|
||||||
|
>
|
||||||
|
<Switch />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item name={['streamSettings', 'realitySettings', 'xver']} label={t('pages.inbounds.form.xver')}>
|
||||||
|
<InputNumber min={0} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
name={['streamSettings', 'realitySettings', 'settings', 'fingerprint']}
|
||||||
|
label="uTLS"
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
options={Object.values(UTLS_FINGERPRINT).map((fp) => ({ value: fp, label: fp }))}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label={t('pages.inbounds.form.target')}>
|
||||||
|
<Space.Compact block>
|
||||||
|
<Form.Item name={['streamSettings', 'realitySettings', 'target']} noStyle>
|
||||||
|
<Input style={{ width: 'calc(100% - 32px)' }} />
|
||||||
|
</Form.Item>
|
||||||
|
<Button icon={<ReloadOutlined />} onClick={randomizeRealityTarget} />
|
||||||
|
</Space.Compact>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label="SNI">
|
||||||
|
<Space.Compact block style={{ display: 'flex' }}>
|
||||||
|
<Form.Item
|
||||||
|
name={['streamSettings', 'realitySettings', 'serverNames']}
|
||||||
|
noStyle
|
||||||
|
>
|
||||||
|
<Select mode="tags" tokenSeparators={[',']} style={{ flex: 1 }} />
|
||||||
|
</Form.Item>
|
||||||
|
<Button icon={<ReloadOutlined />} onClick={randomizeRealityTarget} />
|
||||||
|
</Space.Compact>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
name={['streamSettings', 'realitySettings', 'maxTimediff']}
|
||||||
|
label={t('pages.inbounds.form.maxTimeDiff')}
|
||||||
|
>
|
||||||
|
<InputNumber min={0} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
name={['streamSettings', 'realitySettings', 'minClientVer']}
|
||||||
|
label={t('pages.inbounds.form.minClientVer')}
|
||||||
|
>
|
||||||
|
<Input placeholder="25.9.11" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
name={['streamSettings', 'realitySettings', 'maxClientVer']}
|
||||||
|
label={t('pages.inbounds.form.maxClientVer')}
|
||||||
|
>
|
||||||
|
<Input placeholder="25.9.11" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label={t('pages.inbounds.form.shortIds')}>
|
||||||
|
<Space.Compact block style={{ display: 'flex' }}>
|
||||||
|
<Form.Item
|
||||||
|
name={['streamSettings', 'realitySettings', 'shortIds']}
|
||||||
|
noStyle
|
||||||
|
>
|
||||||
|
<Select mode="tags" tokenSeparators={[',']} style={{ flex: 1 }} />
|
||||||
|
</Form.Item>
|
||||||
|
<Button icon={<ReloadOutlined />} onClick={randomizeShortIds} />
|
||||||
|
</Space.Compact>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
name={['streamSettings', 'realitySettings', 'settings', 'spiderX']}
|
||||||
|
label={t('pages.inbounds.form.spiderX')}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
name={['streamSettings', 'realitySettings', 'settings', 'publicKey']}
|
||||||
|
label={t('pages.inbounds.publicKey')}
|
||||||
|
>
|
||||||
|
<Input.TextArea autoSize={{ minRows: 1, maxRows: 4 }} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
name={['streamSettings', 'realitySettings', 'privateKey']}
|
||||||
|
label={t('pages.inbounds.privatekey')}
|
||||||
|
>
|
||||||
|
<Input.TextArea autoSize={{ minRows: 1, maxRows: 4 }} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label=" ">
|
||||||
|
<Space>
|
||||||
|
<Button type="primary" loading={saving} onClick={genRealityKeypair}>
|
||||||
|
{t('pages.inbounds.form.getNewCert')}
|
||||||
|
</Button>
|
||||||
|
<Button danger onClick={clearRealityKeypair}>{t('clear')}</Button>
|
||||||
|
</Space>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
name={['streamSettings', 'realitySettings', 'mldsa65Seed']}
|
||||||
|
label={t('pages.inbounds.form.mldsa65Seed')}
|
||||||
|
>
|
||||||
|
<Input.TextArea autoSize={{ minRows: 2, maxRows: 6 }} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
name={['streamSettings', 'realitySettings', 'settings', 'mldsa65Verify']}
|
||||||
|
label={t('pages.inbounds.form.mldsa65Verify')}
|
||||||
|
>
|
||||||
|
<Input.TextArea autoSize={{ minRows: 2, maxRows: 6 }} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label=" ">
|
||||||
|
<Space>
|
||||||
|
<Button type="primary" loading={saving} onClick={genMldsa65}>
|
||||||
|
{t('pages.inbounds.form.getNewSeed')}
|
||||||
|
</Button>
|
||||||
|
<Button danger onClick={clearMldsa65}>{t('clear')}</Button>
|
||||||
|
</Space>
|
||||||
|
</Form.Item>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
309
frontend/src/pages/inbounds/form/security/tls.tsx
Normal file
309
frontend/src/pages/inbounds/form/security/tls.tsx
Normal file
|
|
@ -0,0 +1,309 @@
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Button, Form, Input, Radio, Select, Space, Switch } from 'antd';
|
||||||
|
import { MinusOutlined, PlusOutlined, ReloadOutlined } from '@ant-design/icons';
|
||||||
|
|
||||||
|
import {
|
||||||
|
ALPN_OPTION,
|
||||||
|
TLS_CIPHER_OPTION,
|
||||||
|
TLS_VERSION_OPTION,
|
||||||
|
USAGE_OPTION,
|
||||||
|
UTLS_FINGERPRINT,
|
||||||
|
} from '@/schemas/primitives';
|
||||||
|
|
||||||
|
const { TextArea } = Input;
|
||||||
|
|
||||||
|
interface TlsFormProps {
|
||||||
|
saving: boolean;
|
||||||
|
setCertFromPanel: (certName: number) => void;
|
||||||
|
clearCertFiles: (certName: number) => void;
|
||||||
|
generateRandomPinHash: () => void;
|
||||||
|
getNewEchCert: () => void;
|
||||||
|
clearEchCert: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function TlsForm({
|
||||||
|
saving,
|
||||||
|
setCertFromPanel,
|
||||||
|
clearCertFiles,
|
||||||
|
generateRandomPinHash,
|
||||||
|
getNewEchCert,
|
||||||
|
clearEchCert,
|
||||||
|
}: TlsFormProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Form.Item name={['streamSettings', 'tlsSettings', 'serverName']} label="SNI">
|
||||||
|
<Input placeholder={t('pages.inbounds.form.serverNameIndication')} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item name={['streamSettings', 'tlsSettings', 'cipherSuites']} label={t('pages.inbounds.form.cipherSuites')}>
|
||||||
|
<Select
|
||||||
|
options={[
|
||||||
|
{ value: '', label: t('pages.inbounds.form.autoOption') },
|
||||||
|
...Object.entries(TLS_CIPHER_OPTION).map(([k, v]) => ({ value: v, label: k })),
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label={t('pages.inbounds.form.minMaxVersion')}>
|
||||||
|
<Space.Compact block>
|
||||||
|
<Form.Item name={['streamSettings', 'tlsSettings', 'minVersion']} noStyle>
|
||||||
|
<Select
|
||||||
|
style={{ width: '50%' }}
|
||||||
|
options={Object.values(TLS_VERSION_OPTION).map((v) => ({ value: v, label: v }))}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item name={['streamSettings', 'tlsSettings', 'maxVersion']} noStyle>
|
||||||
|
<Select
|
||||||
|
style={{ width: '50%' }}
|
||||||
|
options={Object.values(TLS_VERSION_OPTION).map((v) => ({ value: v, label: v }))}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</Space.Compact>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
name={['streamSettings', 'tlsSettings', 'settings', 'fingerprint']}
|
||||||
|
label="uTLS"
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
options={[
|
||||||
|
{ value: '', label: 'None' },
|
||||||
|
...Object.values(UTLS_FINGERPRINT).map((fp) => ({ value: fp, label: fp })),
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item name={['streamSettings', 'tlsSettings', 'alpn']} label="ALPN">
|
||||||
|
<Select
|
||||||
|
mode="multiple"
|
||||||
|
tokenSeparators={[',']}
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
options={Object.values(ALPN_OPTION).map((a) => ({ value: a, label: a }))}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
name={['streamSettings', 'tlsSettings', 'rejectUnknownSni']}
|
||||||
|
label={t('pages.inbounds.form.rejectUnknownSni')}
|
||||||
|
valuePropName="checked"
|
||||||
|
>
|
||||||
|
<Switch />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
name={['streamSettings', 'tlsSettings', 'disableSystemRoot']}
|
||||||
|
label={t('pages.inbounds.form.disableSystemRoot')}
|
||||||
|
valuePropName="checked"
|
||||||
|
>
|
||||||
|
<Switch />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
name={['streamSettings', 'tlsSettings', 'enableSessionResumption']}
|
||||||
|
label={t('pages.inbounds.form.sessionResumption')}
|
||||||
|
valuePropName="checked"
|
||||||
|
>
|
||||||
|
<Switch />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.List name={['streamSettings', 'tlsSettings', 'certificates']}>
|
||||||
|
{(certFields, { add, remove }) => (
|
||||||
|
<>
|
||||||
|
<Form.Item label={t('certificate')}>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
onClick={() => add({
|
||||||
|
useFile: true,
|
||||||
|
certificateFile: '',
|
||||||
|
keyFile: '',
|
||||||
|
certificate: [],
|
||||||
|
key: [],
|
||||||
|
oneTimeLoading: false,
|
||||||
|
usage: 'encipherment',
|
||||||
|
buildChain: false,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<PlusOutlined />
|
||||||
|
</Button>
|
||||||
|
</Form.Item>
|
||||||
|
{certFields.map((certField, idx) => (
|
||||||
|
<div key={certField.key}>
|
||||||
|
<Form.Item
|
||||||
|
name={[certField.name, 'useFile']}
|
||||||
|
label={`${t('certificate')} ${idx + 1}`}
|
||||||
|
>
|
||||||
|
<Radio.Group buttonStyle="solid">
|
||||||
|
<Radio.Button value={true}>
|
||||||
|
{t('pages.inbounds.certificatePath')}
|
||||||
|
</Radio.Button>
|
||||||
|
<Radio.Button value={false}>
|
||||||
|
{t('pages.inbounds.certificateContent')}
|
||||||
|
</Radio.Button>
|
||||||
|
</Radio.Group>
|
||||||
|
</Form.Item>
|
||||||
|
{certFields.length > 1 && (
|
||||||
|
<Form.Item label=" ">
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
danger
|
||||||
|
onClick={() => remove(certField.name)}
|
||||||
|
>
|
||||||
|
<MinusOutlined /> {t('remove')}
|
||||||
|
</Button>
|
||||||
|
</Form.Item>
|
||||||
|
)}
|
||||||
|
<Form.Item
|
||||||
|
noStyle
|
||||||
|
shouldUpdate={(prev, curr) =>
|
||||||
|
prev.streamSettings?.tlsSettings?.certificates?.[certField.name]?.useFile
|
||||||
|
!== curr.streamSettings?.tlsSettings?.certificates?.[certField.name]?.useFile
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{({ getFieldValue }) => {
|
||||||
|
const useFile = getFieldValue([
|
||||||
|
'streamSettings', 'tlsSettings', 'certificates',
|
||||||
|
certField.name, 'useFile',
|
||||||
|
]);
|
||||||
|
return useFile ? (
|
||||||
|
<>
|
||||||
|
<Form.Item
|
||||||
|
name={[certField.name, 'certificateFile']}
|
||||||
|
label={t('pages.inbounds.publicKey')}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
name={[certField.name, 'keyFile']}
|
||||||
|
label={t('pages.inbounds.privatekey')}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label=" ">
|
||||||
|
<Space>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
loading={saving}
|
||||||
|
onClick={() => setCertFromPanel(certField.name)}
|
||||||
|
>
|
||||||
|
{t('pages.inbounds.setDefaultCert')}
|
||||||
|
</Button>
|
||||||
|
<Button danger onClick={() => clearCertFiles(certField.name)}>
|
||||||
|
{t('clear')}
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</Form.Item>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Form.Item
|
||||||
|
name={[certField.name, 'certificate']}
|
||||||
|
label={t('pages.inbounds.publicKey')}
|
||||||
|
normalize={(v) => typeof v === 'string'
|
||||||
|
? v.split('\n')
|
||||||
|
: v}
|
||||||
|
getValueProps={(v) => ({
|
||||||
|
value: Array.isArray(v) ? v.join('\n') : v,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<TextArea autoSize={{ minRows: 3, maxRows: 8 }} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
name={[certField.name, 'key']}
|
||||||
|
label={t('pages.inbounds.privatekey')}
|
||||||
|
normalize={(v) => typeof v === 'string'
|
||||||
|
? v.split('\n')
|
||||||
|
: v}
|
||||||
|
getValueProps={(v) => ({
|
||||||
|
value: Array.isArray(v) ? v.join('\n') : v,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<TextArea autoSize={{ minRows: 3, maxRows: 8 }} />
|
||||||
|
</Form.Item>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
name={[certField.name, 'oneTimeLoading']}
|
||||||
|
label={t('pages.inbounds.form.oneTimeLoading')}
|
||||||
|
valuePropName="checked"
|
||||||
|
>
|
||||||
|
<Switch />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
name={[certField.name, 'usage']}
|
||||||
|
label={t('pages.inbounds.form.usageOption')}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
style={{ width: '50%' }}
|
||||||
|
options={Object.values(USAGE_OPTION).map((u) => ({ value: u, label: u }))}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
noStyle
|
||||||
|
shouldUpdate={(prev, curr) =>
|
||||||
|
prev.streamSettings?.tlsSettings?.certificates?.[certField.name]?.usage
|
||||||
|
!== curr.streamSettings?.tlsSettings?.certificates?.[certField.name]?.usage
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{({ getFieldValue }) => {
|
||||||
|
const usage = getFieldValue([
|
||||||
|
'streamSettings', 'tlsSettings', 'certificates',
|
||||||
|
certField.name, 'usage',
|
||||||
|
]);
|
||||||
|
if (usage !== 'issue') return null;
|
||||||
|
return (
|
||||||
|
<Form.Item
|
||||||
|
name={[certField.name, 'buildChain']}
|
||||||
|
label={t('pages.inbounds.form.buildChain')}
|
||||||
|
valuePropName="checked"
|
||||||
|
>
|
||||||
|
<Switch />
|
||||||
|
</Form.Item>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Form.List>
|
||||||
|
|
||||||
|
<Form.Item name={['streamSettings', 'tlsSettings', 'echServerKeys']} label={t('pages.inbounds.form.echKey')}>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
name={['streamSettings', 'tlsSettings', 'settings', 'echConfigList']}
|
||||||
|
label={t('pages.inbounds.form.echConfig')}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t('pages.inbounds.form.pinnedPeerCertSha256')}
|
||||||
|
tooltip={t('pages.inbounds.form.pinnedPeerCertSha256Tip')}
|
||||||
|
>
|
||||||
|
<Space.Compact block>
|
||||||
|
<Form.Item
|
||||||
|
name={['streamSettings', 'tlsSettings', 'settings', 'pinnedPeerCertSha256']}
|
||||||
|
noStyle
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
mode="tags"
|
||||||
|
tokenSeparators={[',', ' ']}
|
||||||
|
placeholder={t('pages.inbounds.form.pinnedPeerCertSha256Placeholder')}
|
||||||
|
style={{ width: 'calc(100% - 32px)' }}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Button
|
||||||
|
icon={<ReloadOutlined />}
|
||||||
|
onClick={generateRandomPinHash}
|
||||||
|
title={t('pages.inbounds.form.generateRandomPin')}
|
||||||
|
/>
|
||||||
|
</Space.Compact>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label=" ">
|
||||||
|
<Space>
|
||||||
|
<Button type="primary" loading={saving} onClick={getNewEchCert}>
|
||||||
|
{t('pages.inbounds.form.getNewEchCert')}
|
||||||
|
</Button>
|
||||||
|
<Button danger onClick={clearEchCert}>{t('clear')}</Button>
|
||||||
|
</Space>
|
||||||
|
</Form.Item>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,98 @@
|
||||||
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||||
|
|
||||||
|
exports[`inbound security forms > RealityForm field structure is stable 1`] = `
|
||||||
|
[
|
||||||
|
"Show",
|
||||||
|
"Xver",
|
||||||
|
"uTLS",
|
||||||
|
"Target",
|
||||||
|
"SNI",
|
||||||
|
"Max Time Diff (ms)",
|
||||||
|
"Min Client Ver",
|
||||||
|
"Max Client Ver",
|
||||||
|
"Short IDs",
|
||||||
|
"SpiderX",
|
||||||
|
"Public Key",
|
||||||
|
"Private Key",
|
||||||
|
"mldsa65 Seed",
|
||||||
|
"mldsa65 Verify",
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`inbound security forms > TlsForm field structure is stable 1`] = `
|
||||||
|
[
|
||||||
|
"SNI",
|
||||||
|
"Cipher Suites",
|
||||||
|
"Min/Max Version",
|
||||||
|
"uTLS",
|
||||||
|
"ALPN",
|
||||||
|
"Reject Unknown SNI",
|
||||||
|
"Disable System Root",
|
||||||
|
"Session Resumption",
|
||||||
|
"Digital Certificate",
|
||||||
|
"ECH key",
|
||||||
|
"ECH config",
|
||||||
|
"Pinned Peer Cert SHA-256",
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`inbound transport forms > GrpcForm field structure is stable 1`] = `
|
||||||
|
[
|
||||||
|
"Service Name",
|
||||||
|
"Authority",
|
||||||
|
"Multi Mode",
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`inbound transport forms > HttpUpgradeForm field structure is stable 1`] = `
|
||||||
|
[
|
||||||
|
"Proxy Protocol",
|
||||||
|
"Host",
|
||||||
|
"Path",
|
||||||
|
"Headers",
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`inbound transport forms > KcpForm field structure is stable 1`] = `
|
||||||
|
[
|
||||||
|
"MTU",
|
||||||
|
"TTI (ms)",
|
||||||
|
"Uplink (MB/s)",
|
||||||
|
"Downlink (MB/s)",
|
||||||
|
"CWND Multiplier",
|
||||||
|
"Max Sending Window",
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`inbound transport forms > RawForm field structure is stable 1`] = `
|
||||||
|
[
|
||||||
|
"Proxy Protocol",
|
||||||
|
"HTTP Obfuscation",
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`inbound transport forms > WsForm field structure is stable 1`] = `
|
||||||
|
[
|
||||||
|
"Proxy Protocol",
|
||||||
|
"Host",
|
||||||
|
"Path",
|
||||||
|
"Heartbeat Period",
|
||||||
|
"Headers",
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`inbound transport forms > XhttpForm field structure is stable 1`] = `
|
||||||
|
[
|
||||||
|
"Host",
|
||||||
|
"Path",
|
||||||
|
"Mode",
|
||||||
|
"Server Max Header Bytes",
|
||||||
|
"Padding Bytes",
|
||||||
|
"Headers",
|
||||||
|
"Uplink HTTP Method",
|
||||||
|
"Padding Obfs Mode",
|
||||||
|
"Session Placement",
|
||||||
|
"Sequence Placement",
|
||||||
|
"No SSE Header",
|
||||||
|
]
|
||||||
|
`;
|
||||||
89
frontend/src/test/inbound-form-blocks.test.tsx
Normal file
89
frontend/src/test/inbound-form-blocks.test.tsx
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { Form, type FormInstance } from 'antd';
|
||||||
|
import type { ReactNode } from 'react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
GrpcForm,
|
||||||
|
HttpUpgradeForm,
|
||||||
|
KcpForm,
|
||||||
|
RawForm,
|
||||||
|
WsForm,
|
||||||
|
XhttpForm,
|
||||||
|
} from '@/pages/inbounds/form/transport';
|
||||||
|
import { RealityForm, TlsForm } from '@/pages/inbounds/form/security';
|
||||||
|
import type { InboundFormValues } from '@/schemas/forms/inbound-form';
|
||||||
|
import { renderWithProviders, fieldLabels } from './test-utils';
|
||||||
|
|
||||||
|
function FormHarness({ children }: { children: (form: FormInstance<InboundFormValues>) => ReactNode }) {
|
||||||
|
const [form] = Form.useForm<InboundFormValues>();
|
||||||
|
return <Form form={form}>{children(form)}</Form>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderInForm(node: (form: FormInstance<InboundFormValues>) => ReactNode) {
|
||||||
|
return renderWithProviders(<FormHarness>{node}</FormHarness>);
|
||||||
|
}
|
||||||
|
|
||||||
|
const noop = () => {};
|
||||||
|
|
||||||
|
describe('inbound transport forms', () => {
|
||||||
|
it('RawForm field structure is stable', () => {
|
||||||
|
renderInForm(() => <RawForm />);
|
||||||
|
expect(fieldLabels()).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('WsForm field structure is stable', () => {
|
||||||
|
renderInForm(() => <WsForm />);
|
||||||
|
expect(fieldLabels()).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('GrpcForm field structure is stable', () => {
|
||||||
|
renderInForm(() => <GrpcForm />);
|
||||||
|
expect(fieldLabels()).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('KcpForm field structure is stable', () => {
|
||||||
|
renderInForm(() => <KcpForm />);
|
||||||
|
expect(fieldLabels()).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('HttpUpgradeForm field structure is stable', () => {
|
||||||
|
renderInForm(() => <HttpUpgradeForm />);
|
||||||
|
expect(fieldLabels()).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('XhttpForm field structure is stable', () => {
|
||||||
|
renderInForm((form) => <XhttpForm form={form} />);
|
||||||
|
expect(fieldLabels()).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('inbound security forms', () => {
|
||||||
|
it('TlsForm field structure is stable', () => {
|
||||||
|
renderInForm(() => (
|
||||||
|
<TlsForm
|
||||||
|
saving={false}
|
||||||
|
setCertFromPanel={noop}
|
||||||
|
clearCertFiles={noop}
|
||||||
|
generateRandomPinHash={noop}
|
||||||
|
getNewEchCert={noop}
|
||||||
|
clearEchCert={noop}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
expect(fieldLabels()).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('RealityForm field structure is stable', () => {
|
||||||
|
renderInForm(() => (
|
||||||
|
<RealityForm
|
||||||
|
saving={false}
|
||||||
|
randomizeRealityTarget={noop}
|
||||||
|
randomizeShortIds={noop}
|
||||||
|
genRealityKeypair={noop}
|
||||||
|
clearRealityKeypair={noop}
|
||||||
|
genMldsa65={noop}
|
||||||
|
clearMldsa65={noop}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
expect(fieldLabels()).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Reference in a new issue