tuic.tsx 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244
  1. import { useState } from 'react';
  2. import { useTranslation } from 'react-i18next';
  3. import {
  4. AutoComplete,
  5. Button,
  6. Collapse,
  7. Divider,
  8. Form,
  9. Input,
  10. InputNumber,
  11. Select,
  12. Space,
  13. Switch,
  14. message,
  15. } from 'antd';
  16. import { CloudDownloadOutlined, SyncOutlined } from '@ant-design/icons';
  17. import { useFormContext, useWatch } from 'react-hook-form';
  18. import { FormField } from '@/components/form/rhf';
  19. import { HttpUtil } from '@/utils';
  20. export default function TuicFields() {
  21. const { t } = useTranslation();
  22. const [messageApi, messageContextHolder] = message.useMessage();
  23. const { control, setValue } = useFormContext();
  24. const [loadingPanelCert, setLoadingPanelCert] = useState(false);
  25. const sni = (useWatch({ control, name: 'settings.server.sni' }) ?? '') as string;
  26. const certificate = (useWatch({ control, name: 'settings.server.certificate' }) ?? '') as string;
  27. const privateKey = (useWatch({ control, name: 'settings.server.private_key' }) ?? '') as string;
  28. const nodeId = useWatch({ control, name: 'nodeId' }) as number | null | undefined;
  29. const handleSniChange = (newSni: string) => {
  30. setValue('settings.server.sni', newSni);
  31. };
  32. const autofillFromSni = () => {
  33. const cleanSni = (sni || '').trim();
  34. if (!cleanSni) {
  35. messageApi.warning(t('pages.xray.tuic.sniRequired'));
  36. return;
  37. }
  38. setValue('settings.server.certificate', `/root/cert/${cleanSni}/fullchain.pem`);
  39. setValue('settings.server.private_key', `/root/cert/${cleanSni}/privkey.pem`);
  40. };
  41. const setCertFromPanel = async () => {
  42. setLoadingPanelCert(true);
  43. try {
  44. const msg =
  45. typeof nodeId === 'number'
  46. ? await HttpUtil.get(`/panel/api/nodes/webCert/${nodeId}`, undefined, { silent: true })
  47. : await HttpUtil.post('/panel/api/setting/all', undefined, { silent: true });
  48. if (!msg?.success) {
  49. messageApi.warning(msg?.msg || t('pages.inbounds.setDefaultCertEmpty'));
  50. return;
  51. }
  52. const obj = msg.obj as { webCertFile?: string; webKeyFile?: string };
  53. if (!obj?.webCertFile && !obj?.webKeyFile) {
  54. messageApi.warning(t('pages.inbounds.setDefaultCertEmpty'));
  55. return;
  56. }
  57. if (obj.webCertFile) {
  58. setValue('settings.server.certificate', obj.webCertFile);
  59. }
  60. if (obj.webKeyFile) {
  61. setValue('settings.server.private_key', obj.webKeyFile);
  62. }
  63. messageApi.success(t('pages.inbounds.setSuccess'));
  64. } catch {
  65. messageApi.error(t('somethingWentWrong'));
  66. } finally {
  67. setLoadingPanelCert(false);
  68. }
  69. };
  70. const certOptions = sni
  71. ? [
  72. { value: `/root/cert/${sni}/fullchain.pem` },
  73. { value: `/etc/letsencrypt/live/${sni}/fullchain.pem` },
  74. { value: '/root/cert.pem' },
  75. ]
  76. : [{ value: '/root/cert.pem' }];
  77. const keyOptions = sni
  78. ? [
  79. { value: `/root/cert/${sni}/privkey.pem` },
  80. { value: `/etc/letsencrypt/live/${sni}/privkey.pem` },
  81. { value: '/root/privkey.pem' },
  82. ]
  83. : [{ value: '/root/privkey.pem' }];
  84. const advancedItems = [
  85. {
  86. key: 'advanced',
  87. label: t('pages.inbounds.advancedTitle'),
  88. children: (
  89. <>
  90. <FormField
  91. name={['settings', 'server', 'zero_rtt_handshake']}
  92. label={t('pages.xray.tuic.zeroRttHandshake')}
  93. valueProp="checked"
  94. >
  95. <Switch />
  96. </FormField>
  97. <FormField
  98. name={['settings', 'server', 'log_level']}
  99. label={t('pages.xray.tuic.logLevel')}
  100. tooltip={t('pages.xray.tuic.logLevelHint')}
  101. >
  102. <Select
  103. options={[
  104. { label: 'Info', value: 'info' },
  105. { label: 'Warn', value: 'warn' },
  106. { label: 'Error', value: 'error' },
  107. { label: 'Debug', value: 'debug' },
  108. ]}
  109. />
  110. </FormField>
  111. <FormField
  112. name={['settings', 'server', 'max_idle_time']}
  113. label={t('pages.xray.tuic.maxIdleTime')}
  114. >
  115. <InputNumber min={1} style={{ width: '100%' }} />
  116. </FormField>
  117. <FormField
  118. name={['settings', 'server', 'authentication_timeout']}
  119. label={t('pages.xray.tuic.authTimeout')}
  120. >
  121. <InputNumber min={1} style={{ width: '100%' }} />
  122. </FormField>
  123. <FormField
  124. name={['settings', 'server', 'max_udp_relay_packet_size']}
  125. label={t('pages.xray.tuic.maxUdpRelayPacketSize')}
  126. >
  127. <InputNumber min={1} max={65245} style={{ width: '100%' }} />
  128. </FormField>
  129. </>
  130. ),
  131. },
  132. ];
  133. return (
  134. <>
  135. {messageContextHolder}
  136. <Form.Item label={t('pages.inbounds.publicKey')}>
  137. <AutoComplete
  138. value={certificate}
  139. options={certOptions}
  140. onChange={(v) => setValue('settings.server.certificate', v)}
  141. placeholder="/root/cert.pem"
  142. />
  143. </Form.Item>
  144. <Form.Item label={t('pages.inbounds.privatekey')}>
  145. <AutoComplete
  146. value={privateKey}
  147. options={keyOptions}
  148. onChange={(v) => setValue('settings.server.private_key', v)}
  149. placeholder="/root/privkey.pem"
  150. />
  151. </Form.Item>
  152. <Form.Item label=" ">
  153. <Space>
  154. <Button
  155. type="primary"
  156. icon={<CloudDownloadOutlined />}
  157. loading={loadingPanelCert}
  158. onClick={setCertFromPanel}
  159. >
  160. {t('pages.inbounds.setDefaultCert')}
  161. </Button>
  162. <Button icon={<SyncOutlined />} onClick={autofillFromSni}>
  163. {t('pages.inbounds.form.autoFill')}
  164. </Button>
  165. <Button
  166. danger
  167. onClick={() => {
  168. setValue('settings.server.certificate', '');
  169. setValue('settings.server.private_key', '');
  170. }}
  171. >
  172. {t('clear')}
  173. </Button>
  174. </Space>
  175. </Form.Item>
  176. <FormField
  177. name={['settings', 'server', 'congestion_control']}
  178. label={t('pages.xray.tuic.congestionControl')}
  179. tooltip={t('pages.xray.tuic.congestionControlHint')}
  180. >
  181. <Select
  182. options={[
  183. { label: 'BBR', value: 'bbr' },
  184. { label: 'CUBIC', value: 'cubic' },
  185. { label: 'New Reno', value: 'new_reno' },
  186. ]}
  187. />
  188. </FormField>
  189. <FormField name={['settings', 'server', 'alpn']} label={t('pages.xray.tuic.alpn')}>
  190. <Select
  191. mode="tags"
  192. tokenSeparators={[',']}
  193. options={[
  194. { label: 'h3', value: 'h3' },
  195. { label: 'spdy/3.1', value: 'spdy/3.1' },
  196. ]}
  197. />
  198. </FormField>
  199. <Divider titlePlacement="start">{t('pages.xray.tuic.profileOptions')}</Divider>
  200. <Form.Item label={t('pages.xray.tuic.sni')} tooltip={t('pages.xray.tuic.sniHint')}>
  201. <Input
  202. value={sni}
  203. placeholder="example.com"
  204. onChange={(e) => handleSniChange(e.target.value)}
  205. />
  206. </Form.Item>
  207. <FormField
  208. name={['settings', 'server', 'udp_relay_mode']}
  209. label={t('pages.xray.tuic.udpRelayMode')}
  210. tooltip={t('pages.xray.tuic.udpRelayModeHint')}
  211. >
  212. <Select
  213. options={[
  214. { label: 'Native (Recommended)', value: 'native' },
  215. { label: 'QUIC', value: 'quic' },
  216. ]}
  217. />
  218. </FormField>
  219. <Collapse style={{ marginTop: 16, marginBottom: 8 }} items={advancedItems} />
  220. </>
  221. );
  222. }