mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-05 12:44:22 +00:00
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.
This commit is contained in:
parent
1702b544f1
commit
ad3d3937b0
1 changed files with 117 additions and 1 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue