1
0

external-proxy.tsx 9.6 KB

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