From e01acae843d66cc1022ade0fa6ebc44048f9ec02 Mon Sep 17 00:00:00 2001 From: MHSanaei Date: Tue, 26 May 2026 13:19:08 +0200 Subject: [PATCH] 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. --- .../src/lib/xray/outbound-form-adapter.ts | 18 +- frontend/src/pages/xray/OutboundFormModal.tsx | 307 +++++++++++++++++- .../src/schemas/protocols/stream/xhttp.ts | 24 ++ 3 files changed, 343 insertions(+), 6 deletions(-) diff --git a/frontend/src/lib/xray/outbound-form-adapter.ts b/frontend/src/lib/xray/outbound-form-adapter.ts index e6883ecf..3dd5852d 100644 --- a/frontend/src/lib/xray/outbound-form-adapter.ts +++ b/frontend/src/lib/xray/outbound-form-adapter.ts @@ -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 }; diff --git a/frontend/src/pages/xray/OutboundFormModal.tsx b/frontend/src/pages/xray/OutboundFormModal.tsx index b42ffca4..c8b74be8 100644 --- a/frontend/src/pages/xray/OutboundFormModal.tsx +++ b/frontend/src/pages/xray/OutboundFormModal.tsx @@ -1306,11 +1306,308 @@ export default function OutboundFormModal({ > -
- XHTTP advanced fields (XMUX, sequence/session placement, - padding obfs) are still being migrated — edit them via - the JSON tab. -
+ + + + + {/* 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. */} + + + + + {() => { + const obfs = !!form.getFieldValue([ + 'streamSettings', 'xhttpSettings', 'xPaddingObfsMode', + ]); + if (!obfs) return null; + return ( + <> + + + + + + + + + + + ); + }} + + + + + {() => { + const mode = form.getFieldValue([ + 'streamSettings', 'xhttpSettings', 'mode', + ]); + return ( + + + + {() => { + const placement = form.getFieldValue([ + 'streamSettings', 'xhttpSettings', 'sessionPlacement', + ]); + if (!placement || placement === 'path') return null; + return ( + + + + ); + }} + + + + + ); + }} + + + {/* Mode-conditional sub-sections. */} + + {() => { + const mode = form.getFieldValue([ + 'streamSettings', 'xhttpSettings', 'mode', + ]); + if (mode !== 'packet-up') return null; + return ( + <> + + + + + + + + + + + + + + ); + }} + + + ); + }} + + + {() => { + const mode = form.getFieldValue([ + 'streamSettings', 'xhttpSettings', 'mode', + ]); + if (mode !== 'stream-up' && mode !== 'stream-one') return null; + return ( + + + + ); + }} + + + {/* 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. */} + + + + + {() => { + if (!form.getFieldValue([ + 'streamSettings', 'xhttpSettings', 'enableXmux', + ])) return null; + return ( + <> + + + + + + + + + + + + + + + + + + + + ); + }} + )} diff --git a/frontend/src/schemas/protocols/stream/xhttp.ts b/frontend/src/schemas/protocols/stream/xhttp.ts index 56e38eca..7d2e79b8 100644 --- a/frontend/src/schemas/protocols/stream/xhttp.ts +++ b/frontend/src/schemas/protocols/stream/xhttp.ts @@ -12,6 +12,19 @@ export type XHttpMode = z.infer; // 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; + 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;