external-proxy.tsx 10.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209
  1. import type { ReactNode } from 'react';
  2. import { useTranslation } from 'react-i18next';
  3. import { Button, Form, Input, InputNumber, Select, Space, Switch } from 'antd';
  4. import { DeleteOutlined, PlusOutlined, ReloadOutlined } from '@ant-design/icons';
  5. import { ALPN_OPTION, UTLS_FINGERPRINT } from '@/schemas/primitives';
  6. import './external-proxy.css';
  7. const newEntry = () => ({
  8. forceTls: 'same',
  9. dest: '',
  10. port: 443,
  11. remark: '',
  12. sni: '',
  13. fingerprint: '',
  14. alpn: [],
  15. pinnedPeerCertSha256: [],
  16. echConfigList: '',
  17. });
  18. function Field({ label, children }: { label: ReactNode; children: ReactNode }) {
  19. return (
  20. <div className="ext-proxy-field">
  21. <span className="ext-proxy-flabel">{label}</span>
  22. {children}
  23. </div>
  24. );
  25. }
  26. export default function ExternalProxyForm({
  27. toggleExternalProxy,
  28. }: {
  29. toggleExternalProxy: (on: boolean) => void;
  30. }) {
  31. const { t } = useTranslation();
  32. const form = Form.useFormInstance();
  33. const generateRandomPin = (name: number) => {
  34. const bytes = new Uint8Array(32);
  35. crypto.getRandomValues(bytes);
  36. const hash = Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('');
  37. const path = ['streamSettings', 'externalProxy', name, 'pinnedPeerCertSha256'];
  38. const current = (form.getFieldValue(path) as string[] | undefined) ?? [];
  39. form.setFieldValue(path, [...current, hash]);
  40. };
  41. return (
  42. <Form.Item
  43. noStyle
  44. shouldUpdate={(prev, curr) => {
  45. const a = (prev.streamSettings as { externalProxy?: unknown[] } | undefined)?.externalProxy;
  46. const b = (curr.streamSettings as { externalProxy?: unknown[] } | undefined)?.externalProxy;
  47. return (Array.isArray(a) ? a.length : 0) !== (Array.isArray(b) ? b.length : 0);
  48. }}
  49. >
  50. {({ getFieldValue }) => {
  51. const arr = getFieldValue(['streamSettings', 'externalProxy']);
  52. const on = Array.isArray(arr) && arr.length > 0;
  53. return (
  54. <>
  55. <Form.Item label={t('pages.inbounds.form.externalProxy')}>
  56. <Switch checked={on} onChange={toggleExternalProxy} />
  57. </Form.Item>
  58. {on && (
  59. <Form.Item wrapperCol={{ span: 24 }}>
  60. <Form.List name={['streamSettings', 'externalProxy']}>
  61. {(fields, { add, remove }) => (
  62. <>
  63. <div className="ext-proxy-list">
  64. {fields.map((field, idx) => (
  65. <div key={field.key} className="ext-proxy-card">
  66. <div className="ext-proxy-card__head">
  67. <span className="ext-proxy-card__title">#{idx + 1}</span>
  68. <Button
  69. size="small"
  70. type="text"
  71. danger
  72. icon={<DeleteOutlined />}
  73. onClick={() => remove(field.name)}
  74. />
  75. </div>
  76. <div className="ext-proxy-grid ext-proxy-grid--dest">
  77. <Field label={t('pages.inbounds.form.forceTls')}>
  78. <Form.Item name={[field.name, 'forceTls']} noStyle>
  79. <Select
  80. style={{ width: '100%' }}
  81. options={[
  82. { value: 'same', label: t('pages.inbounds.same') },
  83. { value: 'none', label: t('none') },
  84. { value: 'tls', label: 'TLS' },
  85. ]}
  86. />
  87. </Form.Item>
  88. </Field>
  89. <Field label={t('pages.inbounds.address')}>
  90. <Form.Item name={[field.name, 'dest']} noStyle>
  91. <Input placeholder={t('pages.inbounds.address')} />
  92. </Form.Item>
  93. </Field>
  94. <Field label={t('pages.inbounds.port')}>
  95. <Form.Item name={[field.name, 'port']} noStyle>
  96. <InputNumber style={{ width: '100%' }} min={1} max={65535} />
  97. </Form.Item>
  98. </Field>
  99. </div>
  100. <Field label={t('pages.inbounds.remark')}>
  101. <Form.Item name={[field.name, 'remark']} noStyle>
  102. <Input placeholder={t('pages.inbounds.remark')} />
  103. </Form.Item>
  104. </Field>
  105. <Form.Item
  106. noStyle
  107. shouldUpdate={(prev, curr) =>
  108. prev.streamSettings?.externalProxy?.[field.name]?.forceTls
  109. !== curr.streamSettings?.externalProxy?.[field.name]?.forceTls
  110. }
  111. >
  112. {({ getFieldValue }) => {
  113. const ft = getFieldValue([
  114. 'streamSettings', 'externalProxy', field.name, 'forceTls',
  115. ]);
  116. if (ft !== 'tls') return null;
  117. return (
  118. <div className="ext-proxy-tls">
  119. <div className="ext-proxy-grid ext-proxy-grid--tls">
  120. <Field label="SNI">
  121. <Form.Item name={[field.name, 'sni']} noStyle>
  122. <Input placeholder={t('pages.inbounds.form.serverNameIndication')} />
  123. </Form.Item>
  124. </Field>
  125. <Field label={t('pages.inbounds.form.fingerprint')}>
  126. <Form.Item name={[field.name, 'fingerprint']} noStyle>
  127. <Select
  128. style={{ width: '100%' }}
  129. placeholder={t('pages.inbounds.form.fingerprint')}
  130. options={[
  131. { value: '', label: t('pages.inbounds.form.defaultOption') },
  132. ...Object.values(UTLS_FINGERPRINT).map((fp) => ({
  133. value: fp,
  134. label: fp,
  135. })),
  136. ]}
  137. />
  138. </Form.Item>
  139. </Field>
  140. <Field label="ALPN">
  141. <Form.Item name={[field.name, 'alpn']} noStyle>
  142. <Select
  143. mode="multiple"
  144. style={{ width: '100%' }}
  145. placeholder="ALPN"
  146. options={Object.values(ALPN_OPTION).map((a) => ({
  147. value: a,
  148. label: a,
  149. }))}
  150. />
  151. </Form.Item>
  152. </Field>
  153. </div>
  154. <Field label={t('pages.inbounds.form.echConfig')}>
  155. <Form.Item name={[field.name, 'echConfigList']} noStyle>
  156. <Input placeholder={t('pages.inbounds.form.echConfig')} />
  157. </Form.Item>
  158. </Field>
  159. <Field label={t('pages.inbounds.form.pinnedPeerCertSha256')}>
  160. <Space.Compact block>
  161. <Form.Item name={[field.name, 'pinnedPeerCertSha256']} noStyle>
  162. <Select
  163. mode="tags"
  164. tokenSeparators={[',', ' ']}
  165. placeholder={t('pages.inbounds.form.pinnedPeerCertSha256Placeholder')}
  166. style={{ width: 'calc(100% - 32px)' }}
  167. />
  168. </Form.Item>
  169. <Button
  170. icon={<ReloadOutlined />}
  171. onClick={() => generateRandomPin(field.name)}
  172. title={t('pages.inbounds.form.generateRandomPin')}
  173. />
  174. </Space.Compact>
  175. </Field>
  176. </div>
  177. );
  178. }}
  179. </Form.Item>
  180. </div>
  181. ))}
  182. </div>
  183. <Button
  184. className="ext-proxy-add"
  185. block
  186. type="dashed"
  187. icon={<PlusOutlined />}
  188. onClick={() => add(newEntry())}
  189. >
  190. {t('add')}
  191. </Button>
  192. </>
  193. )}
  194. </Form.List>
  195. </Form.Item>
  196. )}
  197. </>
  198. );
  199. }}
  200. </Form.Item>
  201. );
  202. }