refactor(frontend): extract OutboundFormModal xhttp transport form

Move the xhttp transport block into transport/xhttp.tsx (takes form + onXmuxToggle prop); drop now-unused HeaderMapEditor and MODE_OPTIONS imports from the modal. OutboundFormModal.tsx down to ~1001 lines (from 2238 originally). Verbatim relocation; outbound snapshots unchanged -> no behavior change. typecheck/lint/build green.
This commit is contained in:
MHSanaei 2026-05-30 18:02:08 +02:00
parent 3271374401
commit 543ede63aa
No known key found for this signature in database
GPG key ID: 7E4060F2FBE5AB7A
3 changed files with 358 additions and 341 deletions

View file

@ -14,7 +14,6 @@ import {
message,
} from 'antd';
import { FinalMaskForm } from '@/lib/xray/forms/transport';
import { HeaderMapEditor } from '@/components/form';
import { HysteriaMasqueradeForm } from '@/lib/xray/forms/protocols/shared';
import { JsonEditor } from '@/components/form';
import { Wireguard } from '@/utils';
@ -50,7 +49,6 @@ import {
ALPN_OPTIONS,
FLOW_OPTIONS,
HYSTERIA_NETWORK_OPTION,
MODE_OPTIONS,
NETWORK_OPTIONS,
PROTOCOL_OPTIONS,
SERVER_PROTOCOLS,
@ -82,6 +80,7 @@ import {
KcpForm,
RawForm,
WsForm,
XhttpForm,
} from './transport';
import './OutboundFormModal.css';
@ -524,345 +523,7 @@ export default function OutboundFormModal({
{network === 'httpupgrade' && <HttpUpgradeForm />}
{network === 'xhttp' && (
<>
<Form.Item
label={t('host')}
name={['streamSettings', 'xhttpSettings', 'host']}
>
<Input />
</Form.Item>
<Form.Item
label={t('path')}
name={['streamSettings', 'xhttpSettings', 'path']}
>
<Input />
</Form.Item>
<Form.Item
label={t('pages.inbounds.info.mode')}
name={['streamSettings', 'xhttpSettings', 'mode']}
>
<Select options={MODE_OPTIONS} />
</Form.Item>
<Form.Item
label={t('pages.inbounds.form.paddingBytes')}
name={['streamSettings', 'xhttpSettings', 'xPaddingBytes']}
>
<Input />
</Form.Item>
<Form.Item
label={t('pages.inbounds.form.headers')}
name={['streamSettings', 'xhttpSettings', 'headers']}
>
<HeaderMapEditor mode="v1" />
</Form.Item>
{/* Padding obfs sub-section: gated by a Switch.
When on, four extra knobs (key/header/placement/
method) tune how Xray injects random padding to
disguise the post body shape. */}
<Form.Item
label={t('pages.inbounds.form.paddingObfsMode')}
name={['streamSettings', 'xhttpSettings', 'xPaddingObfsMode']}
valuePropName="checked"
>
<Switch />
</Form.Item>
<Form.Item shouldUpdate noStyle>
{() => {
const obfs = !!form.getFieldValue([
'streamSettings', 'xhttpSettings', 'xPaddingObfsMode',
]);
if (!obfs) return null;
return (
<>
<Form.Item
label={t('pages.inbounds.form.paddingKey')}
name={['streamSettings', 'xhttpSettings', 'xPaddingKey']}
>
<Input placeholder="x_padding" />
</Form.Item>
<Form.Item
label={t('pages.inbounds.form.paddingHeader')}
name={['streamSettings', 'xhttpSettings', 'xPaddingHeader']}
>
<Input placeholder="X-Padding" />
</Form.Item>
<Form.Item
label={t('pages.inbounds.form.paddingPlacement')}
name={['streamSettings', 'xhttpSettings', 'xPaddingPlacement']}
>
<Select
options={[
{ value: '', label: 'Default (queryInHeader)' },
{ value: 'queryInHeader', label: 'queryInHeader' },
{ value: 'header', label: 'header' },
{ value: 'cookie', label: 'cookie' },
{ value: 'query', label: 'query' },
]}
/>
</Form.Item>
<Form.Item
label={t('pages.inbounds.form.paddingMethod')}
name={['streamSettings', 'xhttpSettings', 'xPaddingMethod']}
>
<Select
options={[
{ value: '', label: 'Default (repeat-x)' },
{ value: 'repeat-x', label: 'repeat-x' },
{ value: 'tokenish', label: 'tokenish' },
]}
/>
</Form.Item>
</>
);
}}
</Form.Item>
<Form.Item
noStyle
shouldUpdate={(prev, curr) =>
prev?.streamSettings?.xhttpSettings?.mode !==
curr?.streamSettings?.xhttpSettings?.mode
}
>
{() => {
const mode = form.getFieldValue([
'streamSettings', 'xhttpSettings', 'mode',
]);
return (
<Form.Item
label={t('pages.inbounds.form.uplinkHttpMethod')}
name={['streamSettings', 'xhttpSettings', 'uplinkHTTPMethod']}
>
<Select
placeholder="Default (POST)"
options={[
{ value: '', label: 'Default (POST)' },
{ value: 'POST', label: 'POST' },
{ value: 'PUT', label: 'PUT' },
{ value: 'GET', label: 'GET (packet-up only)', disabled: mode !== 'packet-up' },
]}
/>
</Form.Item>
);
}}
</Form.Item>
{/* Session + sequence + uplinkData placements:
three orthogonal slots Xray uses to thread
request metadata through the transport
(path / header / cookie / query). Key field
only matters when placement is not 'path'. */}
<Form.Item
label={t('pages.inbounds.form.sessionPlacement')}
name={['streamSettings', 'xhttpSettings', 'sessionPlacement']}
>
<Select
placeholder="Default (path)"
options={[
{ value: '', label: 'Default (path)' },
{ value: 'path', label: 'path' },
{ value: 'header', label: 'header' },
{ value: 'cookie', label: 'cookie' },
{ value: 'query', label: 'query' },
]}
/>
</Form.Item>
<Form.Item shouldUpdate noStyle>
{() => {
const placement = form.getFieldValue([
'streamSettings', 'xhttpSettings', 'sessionPlacement',
]);
if (!placement || placement === 'path') return null;
return (
<Form.Item
label={t('pages.inbounds.form.sessionKey')}
name={['streamSettings', 'xhttpSettings', 'sessionKey']}
>
<Input placeholder="x_session" />
</Form.Item>
);
}}
</Form.Item>
<Form.Item
label={t('pages.inbounds.form.sequencePlacement')}
name={['streamSettings', 'xhttpSettings', 'seqPlacement']}
>
<Select
placeholder="Default (path)"
options={[
{ value: '', label: 'Default (path)' },
{ value: 'path', label: 'path' },
{ value: 'header', label: 'header' },
{ value: 'cookie', label: 'cookie' },
{ value: 'query', label: 'query' },
]}
/>
</Form.Item>
<Form.Item shouldUpdate noStyle>
{() => {
const placement = form.getFieldValue([
'streamSettings', 'xhttpSettings', 'seqPlacement',
]);
if (!placement || placement === 'path') return null;
return (
<Form.Item
label={t('pages.inbounds.form.sequenceKey')}
name={['streamSettings', 'xhttpSettings', 'seqKey']}
>
<Input placeholder="x_seq" />
</Form.Item>
);
}}
</Form.Item>
{/* Mode-conditional sub-sections. */}
<Form.Item shouldUpdate noStyle>
{() => {
const mode = form.getFieldValue([
'streamSettings', 'xhttpSettings', 'mode',
]);
if (mode !== 'packet-up') return null;
return (
<>
<Form.Item
label={t('pages.xray.outboundForm.minUploadInterval')}
name={['streamSettings', 'xhttpSettings', 'scMinPostsIntervalMs']}
>
<Input placeholder="30" />
</Form.Item>
<Form.Item
label={t('pages.xray.outboundForm.maxUploadSizeBytes')}
name={['streamSettings', 'xhttpSettings', 'scMaxEachPostBytes']}
>
<Input placeholder="1000000" />
</Form.Item>
<Form.Item
label={t('pages.inbounds.form.uplinkDataPlacement')}
name={['streamSettings', 'xhttpSettings', 'uplinkDataPlacement']}
>
<Select
options={[
{ value: '', label: 'Default (body)' },
{ value: 'body', label: 'body' },
{ value: 'header', label: 'header' },
{ value: 'cookie', label: 'cookie' },
{ value: 'query', label: 'query' },
]}
/>
</Form.Item>
<Form.Item shouldUpdate noStyle>
{() => {
const place = form.getFieldValue([
'streamSettings', 'xhttpSettings', 'uplinkDataPlacement',
]);
if (!place || place === 'body') return null;
return (
<>
<Form.Item
label={t('pages.inbounds.form.uplinkDataKey')}
name={['streamSettings', 'xhttpSettings', 'uplinkDataKey']}
>
<Input placeholder="x_data" />
</Form.Item>
<Form.Item
label={t('pages.xray.outboundForm.uplinkChunkSize')}
name={['streamSettings', 'xhttpSettings', 'uplinkChunkSize']}
>
<InputNumber
min={0}
placeholder="0 (unlimited)"
style={{ width: '100%' }}
/>
</Form.Item>
</>
);
}}
</Form.Item>
</>
);
}}
</Form.Item>
<Form.Item shouldUpdate noStyle>
{() => {
const mode = form.getFieldValue([
'streamSettings', 'xhttpSettings', 'mode',
]);
if (mode !== 'stream-up' && mode !== 'stream-one') return null;
return (
<Form.Item
label={t('pages.xray.outboundForm.noGrpcHeader')}
name={['streamSettings', 'xhttpSettings', 'noGRPCHeader']}
valuePropName="checked"
>
<Switch />
</Form.Item>
);
}}
</Form.Item>
{/* XMUX is the connection-multiplexing layer
xHTTP uses to fan out parallel requests over
a small pool of upstream connections. UI-only
toggle (enableXmux) hides the 6 nested knobs
when off. */}
<Form.Item
label="XMUX"
name={['streamSettings', 'xhttpSettings', 'enableXmux']}
valuePropName="checked"
>
<Switch onChange={onXmuxToggle} />
</Form.Item>
<Form.Item shouldUpdate noStyle>
{() => {
if (!form.getFieldValue([
'streamSettings', 'xhttpSettings', 'enableXmux',
])) return null;
return (
<>
<Form.Item
label={t('pages.xray.outboundForm.maxConcurrency')}
name={['streamSettings', 'xhttpSettings', 'xmux', 'maxConcurrency']}
>
<Input placeholder="16-32" />
</Form.Item>
<Form.Item
label={t('pages.xray.outboundForm.maxConnections')}
name={['streamSettings', 'xhttpSettings', 'xmux', 'maxConnections']}
>
<Input placeholder="0" />
</Form.Item>
<Form.Item
label={t('pages.xray.outboundForm.maxReuseTimes')}
name={['streamSettings', 'xhttpSettings', 'xmux', 'cMaxReuseTimes']}
>
<Input />
</Form.Item>
<Form.Item
label={t('pages.xray.outboundForm.maxRequestTimes')}
name={['streamSettings', 'xhttpSettings', 'xmux', 'hMaxRequestTimes']}
>
<Input placeholder="600-900" />
</Form.Item>
<Form.Item
label={t('pages.xray.outboundForm.maxReusableSecs')}
name={['streamSettings', 'xhttpSettings', 'xmux', 'hMaxReusableSecs']}
>
<Input placeholder="1800-3000" />
</Form.Item>
<Form.Item
label={t('pages.xray.outboundForm.keepAlivePeriod')}
name={['streamSettings', 'xhttpSettings', 'xmux', 'hKeepAlivePeriod']}
>
<InputNumber min={0} style={{ width: '100%' }} />
</Form.Item>
</>
);
}}
</Form.Item>
</>
)}
{network === 'xhttp' && <XhttpForm form={form} onXmuxToggle={onXmuxToggle} />}
{network === 'hysteria' && (
<>

View file

@ -3,3 +3,4 @@ export { default as KcpForm } from './kcp';
export { default as WsForm } from './ws';
export { default as GrpcForm } from './grpc';
export { default as HttpUpgradeForm } from './httpupgrade';
export { default as XhttpForm } from './xhttp';

View file

@ -0,0 +1,355 @@
import { useTranslation } from 'react-i18next';
import { Form, Input, InputNumber, Select, Switch, type FormInstance } from 'antd';
import { HeaderMapEditor } from '@/components/form';
import type { OutboundFormValues } from '@/schemas/forms/outbound-form';
import { MODE_OPTIONS } from '../outbound-form-constants';
interface XhttpFormProps {
form: FormInstance<OutboundFormValues>;
onXmuxToggle: (checked: boolean) => void;
}
export default function XhttpForm({ form, onXmuxToggle }: XhttpFormProps) {
const { t } = useTranslation();
return (
<>
<Form.Item
label={t('host')}
name={['streamSettings', 'xhttpSettings', 'host']}
>
<Input />
</Form.Item>
<Form.Item
label={t('path')}
name={['streamSettings', 'xhttpSettings', 'path']}
>
<Input />
</Form.Item>
<Form.Item
label={t('pages.inbounds.info.mode')}
name={['streamSettings', 'xhttpSettings', 'mode']}
>
<Select options={MODE_OPTIONS} />
</Form.Item>
<Form.Item
label={t('pages.inbounds.form.paddingBytes')}
name={['streamSettings', 'xhttpSettings', 'xPaddingBytes']}
>
<Input />
</Form.Item>
<Form.Item
label={t('pages.inbounds.form.headers')}
name={['streamSettings', 'xhttpSettings', 'headers']}
>
<HeaderMapEditor mode="v1" />
</Form.Item>
{/* Padding obfs sub-section: gated by a Switch.
When on, four extra knobs (key/header/placement/
method) tune how Xray injects random padding to
disguise the post body shape. */}
<Form.Item
label={t('pages.inbounds.form.paddingObfsMode')}
name={['streamSettings', 'xhttpSettings', 'xPaddingObfsMode']}
valuePropName="checked"
>
<Switch />
</Form.Item>
<Form.Item shouldUpdate noStyle>
{() => {
const obfs = !!form.getFieldValue([
'streamSettings', 'xhttpSettings', 'xPaddingObfsMode',
]);
if (!obfs) return null;
return (
<>
<Form.Item
label={t('pages.inbounds.form.paddingKey')}
name={['streamSettings', 'xhttpSettings', 'xPaddingKey']}
>
<Input placeholder="x_padding" />
</Form.Item>
<Form.Item
label={t('pages.inbounds.form.paddingHeader')}
name={['streamSettings', 'xhttpSettings', 'xPaddingHeader']}
>
<Input placeholder="X-Padding" />
</Form.Item>
<Form.Item
label={t('pages.inbounds.form.paddingPlacement')}
name={['streamSettings', 'xhttpSettings', 'xPaddingPlacement']}
>
<Select
options={[
{ value: '', label: 'Default (queryInHeader)' },
{ value: 'queryInHeader', label: 'queryInHeader' },
{ value: 'header', label: 'header' },
{ value: 'cookie', label: 'cookie' },
{ value: 'query', label: 'query' },
]}
/>
</Form.Item>
<Form.Item
label={t('pages.inbounds.form.paddingMethod')}
name={['streamSettings', 'xhttpSettings', 'xPaddingMethod']}
>
<Select
options={[
{ value: '', label: 'Default (repeat-x)' },
{ value: 'repeat-x', label: 'repeat-x' },
{ value: 'tokenish', label: 'tokenish' },
]}
/>
</Form.Item>
</>
);
}}
</Form.Item>
<Form.Item
noStyle
shouldUpdate={(prev, curr) =>
prev?.streamSettings?.xhttpSettings?.mode !==
curr?.streamSettings?.xhttpSettings?.mode
}
>
{() => {
const mode = form.getFieldValue([
'streamSettings', 'xhttpSettings', 'mode',
]);
return (
<Form.Item
label={t('pages.inbounds.form.uplinkHttpMethod')}
name={['streamSettings', 'xhttpSettings', 'uplinkHTTPMethod']}
>
<Select
placeholder="Default (POST)"
options={[
{ value: '', label: 'Default (POST)' },
{ value: 'POST', label: 'POST' },
{ value: 'PUT', label: 'PUT' },
{ value: 'GET', label: 'GET (packet-up only)', disabled: mode !== 'packet-up' },
]}
/>
</Form.Item>
);
}}
</Form.Item>
{/* Session + sequence + uplinkData placements:
three orthogonal slots Xray uses to thread
request metadata through the transport
(path / header / cookie / query). Key field
only matters when placement is not 'path'. */}
<Form.Item
label={t('pages.inbounds.form.sessionPlacement')}
name={['streamSettings', 'xhttpSettings', 'sessionPlacement']}
>
<Select
placeholder="Default (path)"
options={[
{ value: '', label: 'Default (path)' },
{ value: 'path', label: 'path' },
{ value: 'header', label: 'header' },
{ value: 'cookie', label: 'cookie' },
{ value: 'query', label: 'query' },
]}
/>
</Form.Item>
<Form.Item shouldUpdate noStyle>
{() => {
const placement = form.getFieldValue([
'streamSettings', 'xhttpSettings', 'sessionPlacement',
]);
if (!placement || placement === 'path') return null;
return (
<Form.Item
label={t('pages.inbounds.form.sessionKey')}
name={['streamSettings', 'xhttpSettings', 'sessionKey']}
>
<Input placeholder="x_session" />
</Form.Item>
);
}}
</Form.Item>
<Form.Item
label={t('pages.inbounds.form.sequencePlacement')}
name={['streamSettings', 'xhttpSettings', 'seqPlacement']}
>
<Select
placeholder="Default (path)"
options={[
{ value: '', label: 'Default (path)' },
{ value: 'path', label: 'path' },
{ value: 'header', label: 'header' },
{ value: 'cookie', label: 'cookie' },
{ value: 'query', label: 'query' },
]}
/>
</Form.Item>
<Form.Item shouldUpdate noStyle>
{() => {
const placement = form.getFieldValue([
'streamSettings', 'xhttpSettings', 'seqPlacement',
]);
if (!placement || placement === 'path') return null;
return (
<Form.Item
label={t('pages.inbounds.form.sequenceKey')}
name={['streamSettings', 'xhttpSettings', 'seqKey']}
>
<Input placeholder="x_seq" />
</Form.Item>
);
}}
</Form.Item>
{/* Mode-conditional sub-sections. */}
<Form.Item shouldUpdate noStyle>
{() => {
const mode = form.getFieldValue([
'streamSettings', 'xhttpSettings', 'mode',
]);
if (mode !== 'packet-up') return null;
return (
<>
<Form.Item
label={t('pages.xray.outboundForm.minUploadInterval')}
name={['streamSettings', 'xhttpSettings', 'scMinPostsIntervalMs']}
>
<Input placeholder="30" />
</Form.Item>
<Form.Item
label={t('pages.xray.outboundForm.maxUploadSizeBytes')}
name={['streamSettings', 'xhttpSettings', 'scMaxEachPostBytes']}
>
<Input placeholder="1000000" />
</Form.Item>
<Form.Item
label={t('pages.inbounds.form.uplinkDataPlacement')}
name={['streamSettings', 'xhttpSettings', 'uplinkDataPlacement']}
>
<Select
options={[
{ value: '', label: 'Default (body)' },
{ value: 'body', label: 'body' },
{ value: 'header', label: 'header' },
{ value: 'cookie', label: 'cookie' },
{ value: 'query', label: 'query' },
]}
/>
</Form.Item>
<Form.Item shouldUpdate noStyle>
{() => {
const place = form.getFieldValue([
'streamSettings', 'xhttpSettings', 'uplinkDataPlacement',
]);
if (!place || place === 'body') return null;
return (
<>
<Form.Item
label={t('pages.inbounds.form.uplinkDataKey')}
name={['streamSettings', 'xhttpSettings', 'uplinkDataKey']}
>
<Input placeholder="x_data" />
</Form.Item>
<Form.Item
label={t('pages.xray.outboundForm.uplinkChunkSize')}
name={['streamSettings', 'xhttpSettings', 'uplinkChunkSize']}
>
<InputNumber
min={0}
placeholder="0 (unlimited)"
style={{ width: '100%' }}
/>
</Form.Item>
</>
);
}}
</Form.Item>
</>
);
}}
</Form.Item>
<Form.Item shouldUpdate noStyle>
{() => {
const mode = form.getFieldValue([
'streamSettings', 'xhttpSettings', 'mode',
]);
if (mode !== 'stream-up' && mode !== 'stream-one') return null;
return (
<Form.Item
label={t('pages.xray.outboundForm.noGrpcHeader')}
name={['streamSettings', 'xhttpSettings', 'noGRPCHeader']}
valuePropName="checked"
>
<Switch />
</Form.Item>
);
}}
</Form.Item>
{/* XMUX is the connection-multiplexing layer
xHTTP uses to fan out parallel requests over
a small pool of upstream connections. UI-only
toggle (enableXmux) hides the 6 nested knobs
when off. */}
<Form.Item
label="XMUX"
name={['streamSettings', 'xhttpSettings', 'enableXmux']}
valuePropName="checked"
>
<Switch onChange={onXmuxToggle} />
</Form.Item>
<Form.Item shouldUpdate noStyle>
{() => {
if (!form.getFieldValue([
'streamSettings', 'xhttpSettings', 'enableXmux',
])) return null;
return (
<>
<Form.Item
label={t('pages.xray.outboundForm.maxConcurrency')}
name={['streamSettings', 'xhttpSettings', 'xmux', 'maxConcurrency']}
>
<Input placeholder="16-32" />
</Form.Item>
<Form.Item
label={t('pages.xray.outboundForm.maxConnections')}
name={['streamSettings', 'xhttpSettings', 'xmux', 'maxConnections']}
>
<Input placeholder="0" />
</Form.Item>
<Form.Item
label={t('pages.xray.outboundForm.maxReuseTimes')}
name={['streamSettings', 'xhttpSettings', 'xmux', 'cMaxReuseTimes']}
>
<Input />
</Form.Item>
<Form.Item
label={t('pages.xray.outboundForm.maxRequestTimes')}
name={['streamSettings', 'xhttpSettings', 'xmux', 'hMaxRequestTimes']}
>
<Input placeholder="600-900" />
</Form.Item>
<Form.Item
label={t('pages.xray.outboundForm.maxReusableSecs')}
name={['streamSettings', 'xhttpSettings', 'xmux', 'hMaxReusableSecs']}
>
<Input placeholder="1800-3000" />
</Form.Item>
<Form.Item
label={t('pages.xray.outboundForm.keepAlivePeriod')}
name={['streamSettings', 'xhttpSettings', 'xmux', 'hKeepAlivePeriod']}
>
<InputNumber min={0} style={{ width: '100%' }} />
</Form.Item>
</>
);
}}
</Form.Item>
</>
);
}