refactor(xhttp): split fields by direction, expand outbound coverage

Audit panel xhttp config against xray-core's runtime paths and split
fields per direction so each side carries only what it actually uses:

- Bidirectional (must match): host, path, mode, all xPadding*,
  session*/seq*, uplinkData*/Key, scMaxEachPostBytes
- Server-only (inbound): noSSEHeader, scMaxBufferedPosts,
  scStreamUpServerSecs, serverMaxHeaderBytes
- Client-only (outbound): uplinkHTTPMethod, uplinkChunkSize,
  noGRPCHeader, scMinPostsIntervalMs, xmux

The inbound previously held client-only fields and the outbound was
missing every must-match field beyond host/path/mode — meaning a
panel-built outbound couldn't connect to an inbound with a custom
xPaddingKey/sessionKey/etc.

Headers stay on the inbound for URL-share purposes only; xray's
listener ignores them at runtime, but they travel through the share
link's `extra` blob so the client picks them up.

Renames the URL helpers (applyXhttpPadding* -> applyXhttpExtra*) since
the blob now carries more than padding, and folds path/host/mode into
the helper so each link generator's xhttp branch is one line.

Adds two enforcement points for xray's "uplinkHTTPMethod=GET only in
packet-up" rule: the GET option is disabled when mode != packet-up,
and a watcher on the outbound modal auto-clears GET when the user
switches modes.

Hides the XMUX block behind an `enableXmux` switch on the outbound
form (mirrors the QUIC Params toggle) so the section doesn't clutter
the form by default; fromJson auto-flips it on for outbounds with
saved xmux config.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
MHSanaei
2026-05-07 19:26:40 +02:00
parent 3b64a62137
commit 42b2ebc00b
6 changed files with 482 additions and 156 deletions
+105 -1
View File
@@ -402,11 +402,35 @@ class HttpUpgradeStreamSettings extends CommonClass {
}
}
// Mirrors the outbound (client-side) view of Xray-core's SplitHTTPConfig
// (infra/conf/transport_internet.go). Only fields the client actually
// reads at runtime, plus the bidirectional fields the client must match
// against the server, live here. Server-only fields (noSSEHeader,
// scMaxBufferedPosts, scStreamUpServerSecs, serverMaxHeaderBytes) belong
// on the inbound class instead.
class xHTTPStreamSettings extends CommonClass {
constructor(
// Bidirectional — must match the inbound side
path = '/',
host = '',
mode = '',
xPaddingBytes = "100-1000",
xPaddingObfsMode = false,
xPaddingKey = '',
xPaddingHeader = '',
xPaddingPlacement = '',
xPaddingMethod = '',
sessionPlacement = '',
sessionKey = '',
seqPlacement = '',
seqKey = '',
uplinkDataPlacement = '',
uplinkDataKey = '',
scMaxEachPostBytes = "1000000",
// Client-side only
headers = [],
uplinkHTTPMethod = '',
uplinkChunkSize = 0,
noGRPCHeader = false,
scMinPostsIntervalMs = "30",
xmux = {
@@ -417,32 +441,112 @@ class xHTTPStreamSettings extends CommonClass {
hMaxReusableSecs: "1800-3000",
hKeepAlivePeriod: 0,
},
// UI-only toggle — controls whether the XMUX block is expanded in
// the form (mirrors the QUIC Params switch in stream_finalmask).
// Never serialized; toJson() only emits the xmux block itself.
enableXmux = false,
) {
super();
this.path = path;
this.host = host;
this.mode = mode;
this.xPaddingBytes = xPaddingBytes;
this.xPaddingObfsMode = xPaddingObfsMode;
this.xPaddingKey = xPaddingKey;
this.xPaddingHeader = xPaddingHeader;
this.xPaddingPlacement = xPaddingPlacement;
this.xPaddingMethod = xPaddingMethod;
this.sessionPlacement = sessionPlacement;
this.sessionKey = sessionKey;
this.seqPlacement = seqPlacement;
this.seqKey = seqKey;
this.uplinkDataPlacement = uplinkDataPlacement;
this.uplinkDataKey = uplinkDataKey;
this.scMaxEachPostBytes = scMaxEachPostBytes;
this.headers = headers;
this.uplinkHTTPMethod = uplinkHTTPMethod;
this.uplinkChunkSize = uplinkChunkSize;
this.noGRPCHeader = noGRPCHeader;
this.scMinPostsIntervalMs = scMinPostsIntervalMs;
this.xmux = xmux;
this.enableXmux = enableXmux;
}
addHeader(name, value) {
this.headers.push({ name: name, value: value });
}
removeHeader(index) {
this.headers.splice(index, 1);
}
static fromJson(json = {}) {
const headersInput = json.headers;
let headers = [];
if (Array.isArray(headersInput)) {
headers = headersInput;
} else if (headersInput && typeof headersInput === 'object') {
// Upstream uses a {name: value} map; convert to the panel's [{name, value}] form.
headers = Object.entries(headersInput).map(([name, value]) => ({ name, value }));
}
return new xHTTPStreamSettings(
json.path,
json.host,
json.mode,
json.xPaddingBytes,
json.xPaddingObfsMode,
json.xPaddingKey,
json.xPaddingHeader,
json.xPaddingPlacement,
json.xPaddingMethod,
json.sessionPlacement,
json.sessionKey,
json.seqPlacement,
json.seqKey,
json.uplinkDataPlacement,
json.uplinkDataKey,
json.scMaxEachPostBytes,
headers,
json.uplinkHTTPMethod,
json.uplinkChunkSize,
json.noGRPCHeader,
json.scMinPostsIntervalMs,
json.xmux
json.xmux,
// Auto-toggle the XMUX switch on when an existing outbound has
// the xmux key saved, so users editing such configs see their
// values immediately.
json.xmux !== undefined,
);
}
toJson() {
// Upstream expects headers as a {name: value} map, not a list of entries.
const headersMap = {};
if (Array.isArray(this.headers)) {
for (const h of this.headers) {
if (h && h.name) headersMap[h.name] = h.value || '';
}
}
return {
path: this.path,
host: this.host,
mode: this.mode,
xPaddingBytes: this.xPaddingBytes,
xPaddingObfsMode: this.xPaddingObfsMode,
xPaddingKey: this.xPaddingKey,
xPaddingHeader: this.xPaddingHeader,
xPaddingPlacement: this.xPaddingPlacement,
xPaddingMethod: this.xPaddingMethod,
sessionPlacement: this.sessionPlacement,
sessionKey: this.sessionKey,
seqPlacement: this.seqPlacement,
seqKey: this.seqKey,
uplinkDataPlacement: this.uplinkDataPlacement,
uplinkDataKey: this.uplinkDataKey,
scMaxEachPostBytes: this.scMaxEachPostBytes,
headers: headersMap,
uplinkHTTPMethod: this.uplinkHTTPMethod,
uplinkChunkSize: this.uplinkChunkSize,
noGRPCHeader: this.noGRPCHeader,
scMinPostsIntervalMs: this.scMinPostsIntervalMs,
xmux: {