Просмотр исходного кода

feat(frontend): OutboundFormModal deferred features (Vision seed / TCP host+path / WG pubKey derive)

Three small wins from the post-atomic-swap deferred list:

- VLESS Vision testpre + testseed: shown only when flow ===
  'xtls-rprx-vision' (mirrors the legacy canEnableVisionSeed gate).
  testseed binds to a Select mode='tags' with a normalize() that
  coerces strings to positive integers and drops invalid entries.

- TCP HTTP camouflage host + path: when the TCP HTTP camouflage
  Switch is on, surface two inputs that read/write directly into
  streamSettings.tcpSettings.header.request.headers.Host and .path.
  Both fields are string[] on the wire; normalize + getValueProps
  translate to/from comma-joined strings in the UI (one entry per
  host or path the user wants camouflaged).

- Wireguard pubKey auto-derive: Form.useWatch on settings.secretKey
  + useEffect that runs Wireguard.generateKeypair(secret).publicKey
  on every change and writes the result into the disabled pubKey
  display field. Matches the legacy modal's per-keystroke derive.
MHSanaei 9 часов назад
Родитель
Сommit
ad3d3937b0
1 измененных файлов с 117 добавлено и 1 удалено
  1. 117 1
      frontend/src/pages/xray/OutboundFormModal.tsx

+ 117 - 1
frontend/src/pages/xray/OutboundFormModal.tsx

@@ -203,6 +203,26 @@ export default function OutboundFormModal({
     // eslint-disable-next-line react-hooks/exhaustive-deps
     // eslint-disable-next-line react-hooks/exhaustive-deps
   }, [streamAllowed, network]);
   }, [streamAllowed, network]);
 
 
+  // Wireguard pubKey is a UI-only field derived from secretKey on every
+  // edit. The legacy modal did the same on every keystroke. We re-derive
+  // here so paste-in secret keys immediately surface the matching pub.
+  const wgSecretKey = Form.useWatch(['settings', 'secretKey'], form) as string | undefined;
+  useEffect(() => {
+    if (protocol !== 'wireguard') return;
+    const sk = (wgSecretKey ?? '').trim();
+    if (!sk) {
+      form.setFieldValue(['settings', 'pubKey'], '');
+      return;
+    }
+    try {
+      const { publicKey } = Wireguard.generateKeypair(sk);
+      form.setFieldValue(['settings', 'pubKey'], publicKey);
+    } catch {
+      form.setFieldValue(['settings', 'pubKey'], '');
+    }
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, [protocol, wgSecretKey]);
+
   // Switching protocol resets the settings sub-object to fresh defaults
   // Switching protocol resets the settings sub-object to fresh defaults
   // so leftover fields from the previous protocol do not bleed through.
   // so leftover fields from the previous protocol do not bleed through.
   // The adapter's rawOutboundToFormValues seeds whatever the new protocol
   // The adapter's rawOutboundToFormValues seeds whatever the new protocol
@@ -1054,12 +1074,72 @@ export default function OutboundFormModal({
                                         form.setFieldValue(
                                         form.setFieldValue(
                                           ['streamSettings', 'tcpSettings', 'header'],
                                           ['streamSettings', 'tcpSettings', 'header'],
                                           checked
                                           checked
-                                            ? { type: 'http', request: undefined, response: undefined }
+                                            ? {
+                                                type: 'http',
+                                                request: {
+                                                  version: '1.1',
+                                                  method: 'GET',
+                                                  path: ['/'],
+                                                  headers: {},
+                                                },
+                                                response: undefined,
+                                              }
                                             : { type: 'none' },
                                             : { type: 'none' },
                                         )
                                         )
                                       }
                                       }
                                     />
                                     />
                                   </Form.Item>
                                   </Form.Item>
+                                  {type === 'http' && (
+                                    <>
+                                      {/* Host is stored as a string[] on the
+                                          wire (V2 header map: { Host: [...] }).
+                                          The form-level normalize/getValueProps
+                                          translate to/from a comma-joined input
+                                          so the user types one Host:contentReference[oaicite:0]{index=0} value per
+                                          server they want camouflaged. */}
+                                      <Form.Item
+                                        label={t('host')}
+                                        name={[
+                                          'streamSettings',
+                                          'tcpSettings',
+                                          'header',
+                                          'request',
+                                          'headers',
+                                          'Host',
+                                        ]}
+                                        normalize={(v: unknown) =>
+                                          typeof v === 'string'
+                                            ? v.split(',').map((s) => s.trim()).filter(Boolean)
+                                            : Array.isArray(v) ? v : []
+                                        }
+                                        getValueProps={(v: unknown) => ({
+                                          value: Array.isArray(v) ? v.join(',') : '',
+                                        })}
+                                      >
+                                        <Input placeholder="example.com,cdn.example.com" />
+                                      </Form.Item>
+                                      <Form.Item
+                                        label={t('path')}
+                                        name={[
+                                          'streamSettings',
+                                          'tcpSettings',
+                                          'header',
+                                          'request',
+                                          'path',
+                                        ]}
+                                        normalize={(v: unknown) =>
+                                          typeof v === 'string'
+                                            ? v.split(',').map((s) => s.trim()).filter(Boolean)
+                                            : Array.isArray(v) ? v : ['/']
+                                        }
+                                        getValueProps={(v: unknown) => ({
+                                          value: Array.isArray(v) ? v.join(',') : '/',
+                                        })}
+                                      >
+                                        <Input placeholder="/,/api,/static" />
+                                      </Form.Item>
+                                    </>
+                                  )}
                                 </>
                                 </>
                               );
                               );
                             }}
                             }}
@@ -1205,6 +1285,42 @@ export default function OutboundFormModal({
                       </Form.Item>
                       </Form.Item>
                     )}
                     )}
 
 
+                    {/* Vision seed knobs only meaningful for the exact
+                        xtls-rprx-vision flow, on TCP+(tls|reality). The
+                        legacy class gated this on `canEnableVisionSeed()`
+                        — same condition encoded inline here. */}
+                    <Form.Item shouldUpdate noStyle>
+                      {() => {
+                        const flow =
+                          (form.getFieldValue(['settings', 'flow']) ?? '') as string;
+                        if (!(tlsFlowAllowed && flow === 'xtls-rprx-vision')) return null;
+                        return (
+                          <>
+                            <Form.Item label="Vision testpre" name={['settings', 'testpre']}>
+                              <InputNumber min={0} style={{ width: '100%' }} />
+                            </Form.Item>
+                            <Form.Item
+                              label="Vision testseed"
+                              name={['settings', 'testseed']}
+                              normalize={(v: unknown) =>
+                                Array.isArray(v)
+                                  ? v
+                                      .map((x) => Number(x))
+                                      .filter((n) => Number.isInteger(n) && n > 0)
+                                  : []
+                              }
+                            >
+                              <Select
+                                mode="tags"
+                                tokenSeparators={[',', ' ']}
+                                placeholder="four positive integers"
+                              />
+                            </Form.Item>
+                          </>
+                        );
+                      }}
+                    </Form.Item>
+
                     {streamAllowed && network && (
                     {streamAllowed && network && (
                       <Form.Item label={t('security')}>
                       <Form.Item label={t('security')}>
                         <Radio.Group
                         <Radio.Group