xhttp.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340
  1. import { useTranslation } from 'react-i18next';
  2. import { AutoComplete, Input, InputNumber, Select, Switch } from 'antd';
  3. import { useFormContext, useWatch } from 'react-hook-form';
  4. import { HeaderMapEditor } from '@/components/form';
  5. import { FormField } from '@/components/form/rhf';
  6. import { validateSessionIDLength, validateSessionIDTable } from '@/lib/xray/xhttp-session-id';
  7. import { int32RangeUpper } from '@/lib/xray/stream-wire-normalize';
  8. import { XHTTP_SESSION_ID_TABLES } from '@/schemas/protocols/stream/xhttp';
  9. import { MODE_OPTIONS } from '../outbound-form-constants';
  10. interface XhttpFormProps {
  11. onXmuxToggle: (checked: boolean) => void;
  12. }
  13. function antdValidatorToRhf(fn: (rule: unknown, value: unknown) => Promise<void>) {
  14. return async (value: unknown): Promise<true | string> => {
  15. try {
  16. await fn(undefined, value);
  17. return true;
  18. } catch (e) {
  19. return (e as Error).message;
  20. }
  21. };
  22. }
  23. const XH = 'streamSettings.xhttpSettings';
  24. export default function XhttpForm({ onXmuxToggle }: XhttpFormProps) {
  25. const { t } = useTranslation();
  26. const { control, getValues, setValue } = useFormContext();
  27. const mode = useWatch({ control, name: `${XH}.mode` }) as string | undefined;
  28. const obfs = !!useWatch({ control, name: `${XH}.xPaddingObfsMode` });
  29. const sessionPlacement = useWatch({ control, name: `${XH}.sessionIDPlacement` }) as string | undefined;
  30. const table = useWatch({ control, name: `${XH}.sessionIDTable` });
  31. const seqPlacement = useWatch({ control, name: `${XH}.seqPlacement` }) as string | undefined;
  32. const uplinkDataPlacement = useWatch({ control, name: `${XH}.uplinkDataPlacement` }) as string | undefined;
  33. const enableXmux = !!useWatch({ control, name: `${XH}.enableXmux` });
  34. function onXmuxMaxConcurrencyChange(value: unknown) {
  35. if (int32RangeUpper(value) <= 0) return;
  36. if (int32RangeUpper(getValues(`${XH}.xmux.maxConnections`)) > 0) {
  37. setValue(`${XH}.xmux.maxConnections`, 0);
  38. }
  39. }
  40. function onXmuxMaxConnectionsChange(value: unknown) {
  41. if (int32RangeUpper(value) <= 0) return;
  42. if (int32RangeUpper(getValues(`${XH}.xmux.maxConcurrency`)) > 0) {
  43. setValue(`${XH}.xmux.maxConcurrency`, '');
  44. }
  45. }
  46. return (
  47. <>
  48. <FormField label={t('host')} name={['streamSettings', 'xhttpSettings', 'host']}>
  49. <Input />
  50. </FormField>
  51. <FormField label={t('path')} name={['streamSettings', 'xhttpSettings', 'path']}>
  52. <Input />
  53. </FormField>
  54. <FormField label={t('pages.inbounds.info.mode')} name={['streamSettings', 'xhttpSettings', 'mode']}>
  55. <Select options={MODE_OPTIONS} />
  56. </FormField>
  57. <FormField
  58. label={t('pages.inbounds.form.paddingBytes')}
  59. name={['streamSettings', 'xhttpSettings', 'xPaddingBytes']}
  60. >
  61. <Input />
  62. </FormField>
  63. <FormField
  64. label={t('pages.inbounds.form.headers')}
  65. name={['streamSettings', 'xhttpSettings', 'headers']}
  66. >
  67. <HeaderMapEditor mode="v1" />
  68. </FormField>
  69. {/* Padding obfs sub-section: gated by a Switch.
  70. When on, four extra knobs (key/header/placement/
  71. method) tune how Xray injects random padding to
  72. disguise the post body shape. */}
  73. <FormField
  74. label={t('pages.inbounds.form.paddingObfsMode')}
  75. name={['streamSettings', 'xhttpSettings', 'xPaddingObfsMode']}
  76. valueProp="checked"
  77. >
  78. <Switch />
  79. </FormField>
  80. {obfs && (
  81. <>
  82. <FormField
  83. label={t('pages.inbounds.form.paddingKey')}
  84. name={['streamSettings', 'xhttpSettings', 'xPaddingKey']}
  85. >
  86. <Input placeholder="x_padding" />
  87. </FormField>
  88. <FormField
  89. label={t('pages.inbounds.form.paddingHeader')}
  90. name={['streamSettings', 'xhttpSettings', 'xPaddingHeader']}
  91. >
  92. <Input placeholder="X-Padding" />
  93. </FormField>
  94. <FormField
  95. label={t('pages.inbounds.form.paddingPlacement')}
  96. name={['streamSettings', 'xhttpSettings', 'xPaddingPlacement']}
  97. >
  98. <Select
  99. options={[
  100. { value: '', label: 'Default (queryInHeader)' },
  101. { value: 'queryInHeader', label: 'queryInHeader' },
  102. { value: 'header', label: 'header' },
  103. { value: 'cookie', label: 'cookie' },
  104. { value: 'query', label: 'query' },
  105. ]}
  106. />
  107. </FormField>
  108. <FormField
  109. label={t('pages.inbounds.form.paddingMethod')}
  110. name={['streamSettings', 'xhttpSettings', 'xPaddingMethod']}
  111. >
  112. <Select
  113. options={[
  114. { value: '', label: 'Default (repeat-x)' },
  115. { value: 'repeat-x', label: 'repeat-x' },
  116. { value: 'tokenish', label: 'tokenish' },
  117. ]}
  118. />
  119. </FormField>
  120. </>
  121. )}
  122. <FormField
  123. label={t('pages.inbounds.form.uplinkHttpMethod')}
  124. name={['streamSettings', 'xhttpSettings', 'uplinkHTTPMethod']}
  125. >
  126. <Select
  127. placeholder="Default (POST)"
  128. options={[
  129. { value: '', label: 'Default (POST)' },
  130. { value: 'POST', label: 'POST' },
  131. { value: 'PUT', label: 'PUT' },
  132. { value: 'GET', label: 'GET (packet-up only)', disabled: mode !== 'packet-up' },
  133. ]}
  134. />
  135. </FormField>
  136. {/* Session + sequence + uplinkData placements:
  137. three orthogonal slots Xray uses to thread
  138. request metadata through the transport
  139. (path / header / cookie / query). Key field
  140. only matters when placement is not 'path'. */}
  141. <FormField
  142. label={t('pages.inbounds.form.sessionPlacement')}
  143. name={['streamSettings', 'xhttpSettings', 'sessionIDPlacement']}
  144. >
  145. <Select
  146. placeholder="Default (path)"
  147. options={[
  148. { value: '', label: 'Default (path)' },
  149. { value: 'path', label: 'path' },
  150. { value: 'header', label: 'header' },
  151. { value: 'cookie', label: 'cookie' },
  152. { value: 'query', label: 'query' },
  153. ]}
  154. />
  155. </FormField>
  156. {sessionPlacement && sessionPlacement !== 'path' && (
  157. <FormField
  158. label={t('pages.inbounds.form.sessionKey')}
  159. name={['streamSettings', 'xhttpSettings', 'sessionIDKey']}
  160. >
  161. <Input placeholder="x_session" />
  162. </FormField>
  163. )}
  164. <FormField
  165. label={t('pages.inbounds.form.sessionIDTable')}
  166. tooltip={t('pages.inbounds.form.sessionIDTableHint')}
  167. name={['streamSettings', 'xhttpSettings', 'sessionIDTable']}
  168. rules={{ validate: antdValidatorToRhf(validateSessionIDTable) }}
  169. >
  170. <AutoComplete
  171. allowClear
  172. options={XHTTP_SESSION_ID_TABLES.map((v) => ({ value: v }))}
  173. placeholder="Base62"
  174. />
  175. </FormField>
  176. {!!table && (
  177. <FormField
  178. label={t('pages.inbounds.form.sessionIDLength')}
  179. tooltip={t('pages.inbounds.form.sessionIDLengthHint')}
  180. name={['streamSettings', 'xhttpSettings', 'sessionIDLength']}
  181. rules={{ validate: antdValidatorToRhf(validateSessionIDLength) }}
  182. >
  183. <Input placeholder="8-16" />
  184. </FormField>
  185. )}
  186. <FormField
  187. label={t('pages.inbounds.form.sequencePlacement')}
  188. name={['streamSettings', 'xhttpSettings', 'seqPlacement']}
  189. >
  190. <Select
  191. placeholder="Default (path)"
  192. options={[
  193. { value: '', label: 'Default (path)' },
  194. { value: 'path', label: 'path' },
  195. { value: 'header', label: 'header' },
  196. { value: 'cookie', label: 'cookie' },
  197. { value: 'query', label: 'query' },
  198. ]}
  199. />
  200. </FormField>
  201. {seqPlacement && seqPlacement !== 'path' && (
  202. <FormField
  203. label={t('pages.inbounds.form.sequenceKey')}
  204. name={['streamSettings', 'xhttpSettings', 'seqKey']}
  205. >
  206. <Input placeholder="x_seq" />
  207. </FormField>
  208. )}
  209. {/* Mode-conditional sub-sections. */}
  210. {(mode === 'packet-up' || mode === 'auto') && (
  211. <>
  212. <FormField
  213. label={t('pages.xray.outboundForm.minUploadInterval')}
  214. name={['streamSettings', 'xhttpSettings', 'scMinPostsIntervalMs']}
  215. >
  216. <Input placeholder="e.g. 50-150" />
  217. </FormField>
  218. <FormField
  219. label={t('pages.xray.outboundForm.maxUploadSizeBytes')}
  220. name={['streamSettings', 'xhttpSettings', 'scMaxEachPostBytes']}
  221. >
  222. <Input placeholder="1000000" />
  223. </FormField>
  224. <FormField
  225. label={t('pages.inbounds.form.uplinkDataPlacement')}
  226. name={['streamSettings', 'xhttpSettings', 'uplinkDataPlacement']}
  227. >
  228. <Select
  229. options={[
  230. { value: '', label: 'Default (auto)' },
  231. { value: 'auto', label: 'auto' },
  232. { value: 'body', label: 'body' },
  233. { value: 'header', label: 'header' },
  234. { value: 'cookie', label: 'cookie' },
  235. ]}
  236. />
  237. </FormField>
  238. {uplinkDataPlacement && uplinkDataPlacement !== 'body' && (
  239. <>
  240. <FormField
  241. label={t('pages.inbounds.form.uplinkDataKey')}
  242. name={['streamSettings', 'xhttpSettings', 'uplinkDataKey']}
  243. >
  244. <Input placeholder="x_data" />
  245. </FormField>
  246. <FormField
  247. label={t('pages.xray.outboundForm.uplinkChunkSize')}
  248. name={['streamSettings', 'xhttpSettings', 'uplinkChunkSize']}
  249. >
  250. <InputNumber
  251. min={0}
  252. placeholder="0 (unlimited)"
  253. style={{ width: '100%' }}
  254. />
  255. </FormField>
  256. </>
  257. )}
  258. </>
  259. )}
  260. {(mode === 'stream-up' || mode === 'stream-one') && (
  261. <FormField
  262. label={t('pages.xray.outboundForm.noGrpcHeader')}
  263. name={['streamSettings', 'xhttpSettings', 'noGRPCHeader']}
  264. valueProp="checked"
  265. >
  266. <Switch />
  267. </FormField>
  268. )}
  269. {/* XMUX is the connection-multiplexing layer
  270. xHTTP uses to fan out parallel requests over
  271. a small pool of upstream connections. UI-only
  272. toggle (enableXmux) hides the 6 nested knobs
  273. when off. */}
  274. <FormField
  275. label="XMUX"
  276. name={['streamSettings', 'xhttpSettings', 'enableXmux']}
  277. valueProp="checked"
  278. onAfterChange={(v) => onXmuxToggle(v as boolean)}
  279. >
  280. <Switch />
  281. </FormField>
  282. {enableXmux && (
  283. <>
  284. <FormField
  285. label={t('pages.xray.outboundForm.maxConcurrency')}
  286. name={['streamSettings', 'xhttpSettings', 'xmux', 'maxConcurrency']}
  287. onAfterChange={onXmuxMaxConcurrencyChange}
  288. >
  289. <Input placeholder="16-32" />
  290. </FormField>
  291. <FormField
  292. label={t('pages.xray.outboundForm.maxConnections')}
  293. name={['streamSettings', 'xhttpSettings', 'xmux', 'maxConnections']}
  294. onAfterChange={onXmuxMaxConnectionsChange}
  295. >
  296. <Input placeholder="0" />
  297. </FormField>
  298. <FormField
  299. label={t('pages.xray.outboundForm.maxReuseTimes')}
  300. name={['streamSettings', 'xhttpSettings', 'xmux', 'cMaxReuseTimes']}
  301. >
  302. <Input />
  303. </FormField>
  304. <FormField
  305. label={t('pages.xray.outboundForm.maxRequestTimes')}
  306. name={['streamSettings', 'xhttpSettings', 'xmux', 'hMaxRequestTimes']}
  307. >
  308. <Input placeholder="600-900" />
  309. </FormField>
  310. <FormField
  311. label={t('pages.xray.outboundForm.maxReusableSecs')}
  312. name={['streamSettings', 'xhttpSettings', 'xmux', 'hMaxReusableSecs']}
  313. >
  314. <Input placeholder="1800-3000" />
  315. </FormField>
  316. <FormField
  317. label={t('pages.xray.outboundForm.keepAlivePeriod')}
  318. name={['streamSettings', 'xhttpSettings', 'xmux', 'hKeepAlivePeriod']}
  319. >
  320. <InputNumber min={0} style={{ width: '100%' }} />
  321. </FormField>
  322. </>
  323. )}
  324. </>
  325. );
  326. }