| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340 |
- import { useTranslation } from 'react-i18next';
- import { AutoComplete, Input, InputNumber, Select, Switch } from 'antd';
- import { useFormContext, useWatch } from 'react-hook-form';
- import { HeaderMapEditor } from '@/components/form';
- import { FormField } from '@/components/form/rhf';
- import { validateSessionIDLength, validateSessionIDTable } from '@/lib/xray/xhttp-session-id';
- import { int32RangeUpper } from '@/lib/xray/stream-wire-normalize';
- import { XHTTP_SESSION_ID_TABLES } from '@/schemas/protocols/stream/xhttp';
- import { MODE_OPTIONS } from '../outbound-form-constants';
- interface XhttpFormProps {
- onXmuxToggle: (checked: boolean) => void;
- }
- function antdValidatorToRhf(fn: (rule: unknown, value: unknown) => Promise<void>) {
- return async (value: unknown): Promise<true | string> => {
- try {
- await fn(undefined, value);
- return true;
- } catch (e) {
- return (e as Error).message;
- }
- };
- }
- const XH = 'streamSettings.xhttpSettings';
- export default function XhttpForm({ onXmuxToggle }: XhttpFormProps) {
- const { t } = useTranslation();
- const { control, getValues, setValue } = useFormContext();
- const mode = useWatch({ control, name: `${XH}.mode` }) as string | undefined;
- const obfs = !!useWatch({ control, name: `${XH}.xPaddingObfsMode` });
- const sessionPlacement = useWatch({ control, name: `${XH}.sessionIDPlacement` }) as string | undefined;
- const table = useWatch({ control, name: `${XH}.sessionIDTable` });
- const seqPlacement = useWatch({ control, name: `${XH}.seqPlacement` }) as string | undefined;
- const uplinkDataPlacement = useWatch({ control, name: `${XH}.uplinkDataPlacement` }) as string | undefined;
- const enableXmux = !!useWatch({ control, name: `${XH}.enableXmux` });
- function onXmuxMaxConcurrencyChange(value: unknown) {
- if (int32RangeUpper(value) <= 0) return;
- if (int32RangeUpper(getValues(`${XH}.xmux.maxConnections`)) > 0) {
- setValue(`${XH}.xmux.maxConnections`, 0);
- }
- }
- function onXmuxMaxConnectionsChange(value: unknown) {
- if (int32RangeUpper(value) <= 0) return;
- if (int32RangeUpper(getValues(`${XH}.xmux.maxConcurrency`)) > 0) {
- setValue(`${XH}.xmux.maxConcurrency`, '');
- }
- }
- return (
- <>
- <FormField label={t('host')} name={['streamSettings', 'xhttpSettings', 'host']}>
- <Input />
- </FormField>
- <FormField label={t('path')} name={['streamSettings', 'xhttpSettings', 'path']}>
- <Input />
- </FormField>
- <FormField label={t('pages.inbounds.info.mode')} name={['streamSettings', 'xhttpSettings', 'mode']}>
- <Select options={MODE_OPTIONS} />
- </FormField>
- <FormField
- label={t('pages.inbounds.form.paddingBytes')}
- name={['streamSettings', 'xhttpSettings', 'xPaddingBytes']}
- >
- <Input />
- </FormField>
- <FormField
- label={t('pages.inbounds.form.headers')}
- name={['streamSettings', 'xhttpSettings', 'headers']}
- >
- <HeaderMapEditor mode="v1" />
- </FormField>
- {/* 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. */}
- <FormField
- label={t('pages.inbounds.form.paddingObfsMode')}
- name={['streamSettings', 'xhttpSettings', 'xPaddingObfsMode']}
- valueProp="checked"
- >
- <Switch />
- </FormField>
- {obfs && (
- <>
- <FormField
- label={t('pages.inbounds.form.paddingKey')}
- name={['streamSettings', 'xhttpSettings', 'xPaddingKey']}
- >
- <Input placeholder="x_padding" />
- </FormField>
- <FormField
- label={t('pages.inbounds.form.paddingHeader')}
- name={['streamSettings', 'xhttpSettings', 'xPaddingHeader']}
- >
- <Input placeholder="X-Padding" />
- </FormField>
- <FormField
- 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' },
- ]}
- />
- </FormField>
- <FormField
- 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' },
- ]}
- />
- </FormField>
- </>
- )}
- <FormField
- 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' },
- ]}
- />
- </FormField>
- {/* 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'. */}
- <FormField
- label={t('pages.inbounds.form.sessionPlacement')}
- name={['streamSettings', 'xhttpSettings', 'sessionIDPlacement']}
- >
- <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' },
- ]}
- />
- </FormField>
- {sessionPlacement && sessionPlacement !== 'path' && (
- <FormField
- label={t('pages.inbounds.form.sessionKey')}
- name={['streamSettings', 'xhttpSettings', 'sessionIDKey']}
- >
- <Input placeholder="x_session" />
- </FormField>
- )}
- <FormField
- label={t('pages.inbounds.form.sessionIDTable')}
- tooltip={t('pages.inbounds.form.sessionIDTableHint')}
- name={['streamSettings', 'xhttpSettings', 'sessionIDTable']}
- rules={{ validate: antdValidatorToRhf(validateSessionIDTable) }}
- >
- <AutoComplete
- allowClear
- options={XHTTP_SESSION_ID_TABLES.map((v) => ({ value: v }))}
- placeholder="Base62"
- />
- </FormField>
- {!!table && (
- <FormField
- label={t('pages.inbounds.form.sessionIDLength')}
- tooltip={t('pages.inbounds.form.sessionIDLengthHint')}
- name={['streamSettings', 'xhttpSettings', 'sessionIDLength']}
- rules={{ validate: antdValidatorToRhf(validateSessionIDLength) }}
- >
- <Input placeholder="8-16" />
- </FormField>
- )}
- <FormField
- 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' },
- ]}
- />
- </FormField>
- {seqPlacement && seqPlacement !== 'path' && (
- <FormField
- label={t('pages.inbounds.form.sequenceKey')}
- name={['streamSettings', 'xhttpSettings', 'seqKey']}
- >
- <Input placeholder="x_seq" />
- </FormField>
- )}
- {/* Mode-conditional sub-sections. */}
- {(mode === 'packet-up' || mode === 'auto') && (
- <>
- <FormField
- label={t('pages.xray.outboundForm.minUploadInterval')}
- name={['streamSettings', 'xhttpSettings', 'scMinPostsIntervalMs']}
- >
- <Input placeholder="e.g. 50-150" />
- </FormField>
- <FormField
- label={t('pages.xray.outboundForm.maxUploadSizeBytes')}
- name={['streamSettings', 'xhttpSettings', 'scMaxEachPostBytes']}
- >
- <Input placeholder="1000000" />
- </FormField>
- <FormField
- label={t('pages.inbounds.form.uplinkDataPlacement')}
- name={['streamSettings', 'xhttpSettings', 'uplinkDataPlacement']}
- >
- <Select
- options={[
- { value: '', label: 'Default (auto)' },
- { value: 'auto', label: 'auto' },
- { value: 'body', label: 'body' },
- { value: 'header', label: 'header' },
- { value: 'cookie', label: 'cookie' },
- ]}
- />
- </FormField>
- {uplinkDataPlacement && uplinkDataPlacement !== 'body' && (
- <>
- <FormField
- label={t('pages.inbounds.form.uplinkDataKey')}
- name={['streamSettings', 'xhttpSettings', 'uplinkDataKey']}
- >
- <Input placeholder="x_data" />
- </FormField>
- <FormField
- label={t('pages.xray.outboundForm.uplinkChunkSize')}
- name={['streamSettings', 'xhttpSettings', 'uplinkChunkSize']}
- >
- <InputNumber
- min={0}
- placeholder="0 (unlimited)"
- style={{ width: '100%' }}
- />
- </FormField>
- </>
- )}
- </>
- )}
- {(mode === 'stream-up' || mode === 'stream-one') && (
- <FormField
- label={t('pages.xray.outboundForm.noGrpcHeader')}
- name={['streamSettings', 'xhttpSettings', 'noGRPCHeader']}
- valueProp="checked"
- >
- <Switch />
- </FormField>
- )}
- {/* 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. */}
- <FormField
- label="XMUX"
- name={['streamSettings', 'xhttpSettings', 'enableXmux']}
- valueProp="checked"
- onAfterChange={(v) => onXmuxToggle(v as boolean)}
- >
- <Switch />
- </FormField>
- {enableXmux && (
- <>
- <FormField
- label={t('pages.xray.outboundForm.maxConcurrency')}
- name={['streamSettings', 'xhttpSettings', 'xmux', 'maxConcurrency']}
- onAfterChange={onXmuxMaxConcurrencyChange}
- >
- <Input placeholder="16-32" />
- </FormField>
- <FormField
- label={t('pages.xray.outboundForm.maxConnections')}
- name={['streamSettings', 'xhttpSettings', 'xmux', 'maxConnections']}
- onAfterChange={onXmuxMaxConnectionsChange}
- >
- <Input placeholder="0" />
- </FormField>
- <FormField
- label={t('pages.xray.outboundForm.maxReuseTimes')}
- name={['streamSettings', 'xhttpSettings', 'xmux', 'cMaxReuseTimes']}
- >
- <Input />
- </FormField>
- <FormField
- label={t('pages.xray.outboundForm.maxRequestTimes')}
- name={['streamSettings', 'xhttpSettings', 'xmux', 'hMaxRequestTimes']}
- >
- <Input placeholder="600-900" />
- </FormField>
- <FormField
- label={t('pages.xray.outboundForm.maxReusableSecs')}
- name={['streamSettings', 'xhttpSettings', 'xmux', 'hMaxReusableSecs']}
- >
- <Input placeholder="1800-3000" />
- </FormField>
- <FormField
- label={t('pages.xray.outboundForm.keepAlivePeriod')}
- name={['streamSettings', 'xhttpSettings', 'xmux', 'hKeepAlivePeriod']}
- >
- <InputNumber min={0} style={{ width: '100%' }} />
- </FormField>
- </>
- )}
- </>
- );
- }
|