mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-05-28 16:09:36 +00:00
feat(frontend): XHTTP advanced fields on outbound modal
Replace the 'edit via JSON' deferred-features hint with the full XHTTP sub-form matching the legacy modal's XhttpFields helper. schemas/protocols/stream/xhttp.ts: - New XHttpXmuxSchema: 6 connection-multiplexing knobs (maxConcurrency, maxConnections, cMaxReuseTimes, hMaxRequestTimes, hMaxReusableSecs, hKeepAlivePeriod). - XHttpStreamSettingsSchema gains 5 outbound-only fields and one UI-only toggle: scMinPostsIntervalMs, uplinkChunkSize, noGRPCHeader, xmux, enableXmux. outbound-form-adapter.ts: - New stripUiOnlyStreamFields() drops xhttpSettings.enableXmux on the way to wire so the panel never embeds the UI toggle into the saved config. xray-core ignores unknown fields anyway, but the panel reads back its own emitted JSON, so a clean wire shape matters. OutboundFormModal.tsx: - Headers editor (HeaderMapEditor v1) for xhttpSettings.headers. - Padding obfs Switch + 4 conditional fields (key/header/placement/ method) when on. - Uplink HTTP method Select with GET disabled outside packet-up. - Session placement + session key (key shown when placement != path). - Sequence placement + sequence key (same pattern). - packet-up mode: scMinPostsIntervalMs, scMaxEachPostBytes, uplink data placement + key + chunk size (key/chunk-size shown when placement != body). - stream-up / stream-one mode: noGRPCHeader Switch. - XMUX Switch + 6 nested fields when on.
This commit is contained in:
@@ -554,6 +554,22 @@ function loopbackToWire(s: LoopbackOutboundFormSettings) {
|
||||
const MUX_PROTOCOLS = new Set(['vmess', 'vless', 'trojan', 'shadowsocks', 'http', 'socks']);
|
||||
const STREAM_PROTOCOLS = new Set(['vmess', 'vless', 'trojan', 'shadowsocks', 'hysteria']);
|
||||
|
||||
// Strip UI-only fields the form layered into streamSettings (e.g. the
|
||||
// XHTTP modal's enableXmux toggle that controls section visibility but
|
||||
// has no meaning on the wire). xray-core would ignore unknown fields
|
||||
// anyway but the panel reads back its own emitted JSON, so we keep
|
||||
// the wire shape clean.
|
||||
function stripUiOnlyStreamFields(stream: unknown): Raw {
|
||||
const next = { ...(stream as Raw) };
|
||||
const xh = next.xhttpSettings;
|
||||
if (xh && typeof xh === 'object') {
|
||||
const cleaned = { ...(xh as Raw) };
|
||||
delete cleaned.enableXmux;
|
||||
next.xhttpSettings = cleaned;
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
function muxAllowed(values: OutboundFormValues): boolean {
|
||||
if (!MUX_PROTOCOLS.has(values.protocol)) return false;
|
||||
const flow = values.protocol === 'vless'
|
||||
@@ -596,7 +612,7 @@ export function formValuesToWirePayload(values: OutboundFormValues): WireOutboun
|
||||
// still emit just `sockopt` if that key is present (legacy behavior).
|
||||
if (values.streamSettings) {
|
||||
if (STREAM_PROTOCOLS.has(values.protocol)) {
|
||||
result.streamSettings = values.streamSettings;
|
||||
result.streamSettings = stripUiOnlyStreamFields(values.streamSettings);
|
||||
} else {
|
||||
const sockopt = (values.streamSettings as { sockopt?: unknown }).sockopt;
|
||||
if (sockopt) result.streamSettings = { sockopt };
|
||||
|
||||
@@ -1306,11 +1306,308 @@ export default function OutboundFormModal({
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<div style={{ marginTop: 4, opacity: 0.6, fontStyle: 'italic' }}>
|
||||
XHTTP advanced fields (XMUX, sequence/session placement,
|
||||
padding obfs) are still being migrated — edit them via
|
||||
the JSON tab.
|
||||
</div>
|
||||
<Form.Item
|
||||
label="Headers"
|
||||
name={['streamSettings', 'xhttpSettings', 'headers']}
|
||||
>
|
||||
<HeaderMapEditor mode="v1" />
|
||||
</Form.Item>
|
||||
|
||||
{/* Padding obfs sub-section: gated by a Switch.
|
||||
When on, four extra knobs (key/header/placement/
|
||||
method) tune how Xray injects random padding to
|
||||
disguise the post body shape. */}
|
||||
<Form.Item
|
||||
label="Padding obfs mode"
|
||||
name={['streamSettings', 'xhttpSettings', 'xPaddingObfsMode']}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item shouldUpdate noStyle>
|
||||
{() => {
|
||||
const obfs = !!form.getFieldValue([
|
||||
'streamSettings', 'xhttpSettings', 'xPaddingObfsMode',
|
||||
]);
|
||||
if (!obfs) return null;
|
||||
return (
|
||||
<>
|
||||
<Form.Item
|
||||
label="Padding key"
|
||||
name={['streamSettings', 'xhttpSettings', 'xPaddingKey']}
|
||||
>
|
||||
<Input placeholder="x_padding" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="Padding header"
|
||||
name={['streamSettings', 'xhttpSettings', 'xPaddingHeader']}
|
||||
>
|
||||
<Input placeholder="X-Padding" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="Padding placement"
|
||||
name={['streamSettings', 'xhttpSettings', 'xPaddingPlacement']}
|
||||
>
|
||||
<Select
|
||||
options={[
|
||||
{ value: '', label: 'Default (queryInHeader)' },
|
||||
{ value: 'queryInHeader', label: 'queryInHeader' },
|
||||
{ value: 'header', label: 'header' },
|
||||
{ value: 'cookie', label: 'cookie' },
|
||||
{ value: 'query', label: 'query' },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="Padding method"
|
||||
name={['streamSettings', 'xhttpSettings', 'xPaddingMethod']}
|
||||
>
|
||||
<Select
|
||||
options={[
|
||||
{ value: '', label: 'Default (repeat-x)' },
|
||||
{ value: 'repeat-x', label: 'repeat-x' },
|
||||
{ value: 'tokenish', label: 'tokenish' },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="Uplink HTTP method"
|
||||
name={['streamSettings', 'xhttpSettings', 'uplinkHTTPMethod']}
|
||||
>
|
||||
<Form.Item shouldUpdate noStyle>
|
||||
{() => {
|
||||
const mode = form.getFieldValue([
|
||||
'streamSettings', 'xhttpSettings', 'mode',
|
||||
]);
|
||||
return (
|
||||
<Select
|
||||
options={[
|
||||
{ value: '', label: 'Default (POST)' },
|
||||
{ value: 'POST', label: 'POST' },
|
||||
{ value: 'PUT', label: 'PUT' },
|
||||
{ value: 'GET', label: 'GET (packet-up only)', disabled: mode !== 'packet-up' },
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
</Form.Item>
|
||||
|
||||
{/* Session + sequence + uplinkData placements:
|
||||
three orthogonal slots Xray uses to thread
|
||||
request metadata through the transport
|
||||
(path / header / cookie / query). Key field
|
||||
only matters when placement is not 'path'. */}
|
||||
<Form.Item
|
||||
label="Session placement"
|
||||
name={['streamSettings', 'xhttpSettings', 'sessionPlacement']}
|
||||
>
|
||||
<Select
|
||||
options={[
|
||||
{ value: '', label: 'Default (path)' },
|
||||
{ value: 'path', label: 'path' },
|
||||
{ value: 'header', label: 'header' },
|
||||
{ value: 'cookie', label: 'cookie' },
|
||||
{ value: 'query', label: 'query' },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item shouldUpdate noStyle>
|
||||
{() => {
|
||||
const placement = form.getFieldValue([
|
||||
'streamSettings', 'xhttpSettings', 'sessionPlacement',
|
||||
]);
|
||||
if (!placement || placement === 'path') return null;
|
||||
return (
|
||||
<Form.Item
|
||||
label="Session key"
|
||||
name={['streamSettings', 'xhttpSettings', 'sessionKey']}
|
||||
>
|
||||
<Input placeholder="x_session" />
|
||||
</Form.Item>
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="Sequence placement"
|
||||
name={['streamSettings', 'xhttpSettings', 'seqPlacement']}
|
||||
>
|
||||
<Select
|
||||
options={[
|
||||
{ value: '', label: 'Default (path)' },
|
||||
{ value: 'path', label: 'path' },
|
||||
{ value: 'header', label: 'header' },
|
||||
{ value: 'cookie', label: 'cookie' },
|
||||
{ value: 'query', label: 'query' },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item shouldUpdate noStyle>
|
||||
{() => {
|
||||
const placement = form.getFieldValue([
|
||||
'streamSettings', 'xhttpSettings', 'seqPlacement',
|
||||
]);
|
||||
if (!placement || placement === 'path') return null;
|
||||
return (
|
||||
<Form.Item
|
||||
label="Sequence key"
|
||||
name={['streamSettings', 'xhttpSettings', 'seqKey']}
|
||||
>
|
||||
<Input placeholder="x_seq" />
|
||||
</Form.Item>
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
|
||||
{/* Mode-conditional sub-sections. */}
|
||||
<Form.Item shouldUpdate noStyle>
|
||||
{() => {
|
||||
const mode = form.getFieldValue([
|
||||
'streamSettings', 'xhttpSettings', 'mode',
|
||||
]);
|
||||
if (mode !== 'packet-up') return null;
|
||||
return (
|
||||
<>
|
||||
<Form.Item
|
||||
label="Min upload interval (ms)"
|
||||
name={['streamSettings', 'xhttpSettings', 'scMinPostsIntervalMs']}
|
||||
>
|
||||
<Input placeholder="30" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="Max upload size (bytes)"
|
||||
name={['streamSettings', 'xhttpSettings', 'scMaxEachPostBytes']}
|
||||
>
|
||||
<Input placeholder="1000000" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="Uplink data placement"
|
||||
name={['streamSettings', 'xhttpSettings', 'uplinkDataPlacement']}
|
||||
>
|
||||
<Select
|
||||
options={[
|
||||
{ value: '', label: 'Default (body)' },
|
||||
{ value: 'body', label: 'body' },
|
||||
{ value: 'header', label: 'header' },
|
||||
{ value: 'cookie', label: 'cookie' },
|
||||
{ value: 'query', label: 'query' },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item shouldUpdate noStyle>
|
||||
{() => {
|
||||
const place = form.getFieldValue([
|
||||
'streamSettings', 'xhttpSettings', 'uplinkDataPlacement',
|
||||
]);
|
||||
if (!place || place === 'body') return null;
|
||||
return (
|
||||
<>
|
||||
<Form.Item
|
||||
label="Uplink data key"
|
||||
name={['streamSettings', 'xhttpSettings', 'uplinkDataKey']}
|
||||
>
|
||||
<Input placeholder="x_data" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="Uplink chunk size"
|
||||
name={['streamSettings', 'xhttpSettings', 'uplinkChunkSize']}
|
||||
>
|
||||
<InputNumber
|
||||
min={0}
|
||||
placeholder="0 (unlimited)"
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</Form.Item>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
<Form.Item shouldUpdate noStyle>
|
||||
{() => {
|
||||
const mode = form.getFieldValue([
|
||||
'streamSettings', 'xhttpSettings', 'mode',
|
||||
]);
|
||||
if (mode !== 'stream-up' && mode !== 'stream-one') return null;
|
||||
return (
|
||||
<Form.Item
|
||||
label="No gRPC header"
|
||||
name={['streamSettings', 'xhttpSettings', 'noGRPCHeader']}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
|
||||
{/* XMUX is the connection-multiplexing layer
|
||||
xHTTP uses to fan out parallel requests over
|
||||
a small pool of upstream connections. UI-only
|
||||
toggle (enableXmux) hides the 6 nested knobs
|
||||
when off. */}
|
||||
<Form.Item
|
||||
label="XMUX"
|
||||
name={['streamSettings', 'xhttpSettings', 'enableXmux']}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item shouldUpdate noStyle>
|
||||
{() => {
|
||||
if (!form.getFieldValue([
|
||||
'streamSettings', 'xhttpSettings', 'enableXmux',
|
||||
])) return null;
|
||||
return (
|
||||
<>
|
||||
<Form.Item
|
||||
label="Max concurrency"
|
||||
name={['streamSettings', 'xhttpSettings', 'xmux', 'maxConcurrency']}
|
||||
>
|
||||
<Input placeholder="16-32" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="Max connections"
|
||||
name={['streamSettings', 'xhttpSettings', 'xmux', 'maxConnections']}
|
||||
>
|
||||
<Input placeholder="0" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="Max reuse times"
|
||||
name={['streamSettings', 'xhttpSettings', 'xmux', 'cMaxReuseTimes']}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="Max request times"
|
||||
name={['streamSettings', 'xhttpSettings', 'xmux', 'hMaxRequestTimes']}
|
||||
>
|
||||
<Input placeholder="600-900" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="Max reusable secs"
|
||||
name={['streamSettings', 'xhttpSettings', 'xmux', 'hMaxReusableSecs']}
|
||||
>
|
||||
<Input placeholder="1800-3000" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="Keep alive period"
|
||||
name={['streamSettings', 'xhttpSettings', 'xmux', 'hKeepAlivePeriod']}
|
||||
>
|
||||
<InputNumber min={0} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
@@ -12,6 +12,19 @@ export type XHttpMode = z.infer<typeof XHttpModeSchema>;
|
||||
// server ignores them at runtime. Outbound has additional fields (uplinkChunk
|
||||
// sizes, noGRPCHeader, scMinPostsIntervalMs, xmux, downloadSettings) which
|
||||
// belong on the outbound class instead, not modeled here.
|
||||
// XMUX is the connection-multiplexing layer xHTTP uses to fan out
|
||||
// parallel requests over a small pool of upstream connections. Fields
|
||||
// are strings because they accept dash-range values like '16-32'.
|
||||
export const XHttpXmuxSchema = z.object({
|
||||
maxConcurrency: z.string().default('16-32'),
|
||||
maxConnections: z.union([z.string(), z.number()]).default(0),
|
||||
cMaxReuseTimes: z.union([z.string(), z.number()]).default(0),
|
||||
hMaxRequestTimes: z.string().default('600-900'),
|
||||
hMaxReusableSecs: z.string().default('1800-3000'),
|
||||
hKeepAlivePeriod: z.number().int().min(0).default(0),
|
||||
});
|
||||
export type XHttpXmux = z.infer<typeof XHttpXmuxSchema>;
|
||||
|
||||
export const XHttpStreamSettingsSchema = z.object({
|
||||
path: z.string().default('/'),
|
||||
host: z.string().default(''),
|
||||
@@ -35,5 +48,16 @@ export const XHttpStreamSettingsSchema = z.object({
|
||||
serverMaxHeaderBytes: z.number().int().min(0).default(0),
|
||||
uplinkHTTPMethod: z.string().default(''),
|
||||
headers: WsHeaderMapSchema.default({}),
|
||||
// Outbound-only fields. Server (inbound) listener ignores these. The
|
||||
// panel embeds them in share-link `extra` blobs so the same xhttp
|
||||
// config can roundtrip on both sides.
|
||||
scMinPostsIntervalMs: z.string().default('30'),
|
||||
uplinkChunkSize: z.number().int().min(0).default(0),
|
||||
noGRPCHeader: z.boolean().default(false),
|
||||
xmux: XHttpXmuxSchema.optional(),
|
||||
// UI-only toggle controlling whether the XMUX sub-form is expanded.
|
||||
// Never present on the wire — outbound modal strips it via the
|
||||
// form-to-wire adapter.
|
||||
enableXmux: z.boolean().default(false),
|
||||
});
|
||||
export type XHttpStreamSettings = z.infer<typeof XHttpStreamSettingsSchema>;
|
||||
|
||||
Reference in New Issue
Block a user