mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-05-08 14:36:13 +00:00
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:
@@ -582,28 +582,18 @@ func applyShareNetworkParams(stream map[string]any, streamNetwork string, params
|
||||
applyPathAndHostParams(httpupgrade, params)
|
||||
case "xhttp":
|
||||
xhttp, _ := stream["xhttpSettings"].(map[string]any)
|
||||
applyPathAndHostParams(xhttp, params)
|
||||
params["mode"], _ = xhttp["mode"].(string)
|
||||
applyXhttpPaddingParams(xhttp, params)
|
||||
applyXhttpExtraParams(xhttp, params)
|
||||
}
|
||||
}
|
||||
|
||||
func applyXhttpPaddingObj(xhttp map[string]any, obj map[string]any) {
|
||||
// VMess base64 JSON supports arbitrary keys; copy the padding
|
||||
// settings through so clients can match the server's xhttp
|
||||
// xPaddingBytes range and, when the admin opted into obfs
|
||||
// mode, the custom key / header / placement / method.
|
||||
// applyXhttpExtraObj copies the bidirectional xhttp settings into the
|
||||
// VMess base64 JSON link object. VMess supports arbitrary keys, so we
|
||||
// flatten the SplitHTTPConfig "extra" fields directly onto obj.
|
||||
func applyXhttpExtraObj(xhttp map[string]any, obj map[string]any) {
|
||||
if xpb, ok := xhttp["xPaddingBytes"].(string); ok && len(xpb) > 0 {
|
||||
obj["x_padding_bytes"] = xpb
|
||||
}
|
||||
if obfs, ok := xhttp["xPaddingObfsMode"].(bool); ok && obfs {
|
||||
obj["xPaddingObfsMode"] = true
|
||||
for _, field := range []string{"xPaddingKey", "xPaddingHeader", "xPaddingPlacement", "xPaddingMethod"} {
|
||||
if v, ok := xhttp[field].(string); ok && len(v) > 0 {
|
||||
obj[field] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
maps.Copy(obj, buildXhttpExtra(xhttp))
|
||||
}
|
||||
|
||||
func applyVmessNetworkParams(stream map[string]any, network string, obj map[string]any) {
|
||||
@@ -639,8 +629,10 @@ func applyVmessNetworkParams(stream map[string]any, network string, obj map[stri
|
||||
case "xhttp":
|
||||
xhttp, _ := stream["xhttpSettings"].(map[string]any)
|
||||
applyPathAndHostObj(xhttp, obj)
|
||||
obj["mode"], _ = xhttp["mode"].(string)
|
||||
applyXhttpPaddingObj(xhttp, obj)
|
||||
if mode, ok := xhttp["mode"].(string); ok {
|
||||
obj["mode"] = mode
|
||||
}
|
||||
applyXhttpExtraObj(xhttp, obj)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -928,45 +920,33 @@ func searchKey(data any, key string) (any, bool) {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// applyXhttpPaddingParams copies the xPadding* fields from an xhttpSettings
|
||||
// map into the URL query params of a vless:// / trojan:// / ss:// link.
|
||||
// buildXhttpExtra walks an xhttpSettings map and returns the JSON blob
|
||||
// that goes into the URL's `extra` param (or, for VMess, the link
|
||||
// object). Carries ONLY the bidirectional fields from xray-core's
|
||||
// SplitHTTPConfig — i.e. the ones the server enforces and the client
|
||||
// must match. Strictly one-sided fields are excluded:
|
||||
//
|
||||
// Before this helper existed, only path / host / mode were propagated,
|
||||
// so a server configured with a non-default xPaddingBytes (e.g. 80-600)
|
||||
// or with xPaddingObfsMode=true + custom xPaddingKey / xPaddingHeader
|
||||
// would silently diverge from the client: the client kept defaults,
|
||||
// hit the server, and was rejected by its padding validation
|
||||
// ("invalid padding" in the inbound log) — the client-visible symptom
|
||||
// was "xhttp doesn't connect" on OpenWRT / sing-box.
|
||||
// - server-only (noSSEHeader, scMaxBufferedPosts, scStreamUpServerSecs,
|
||||
// serverMaxHeaderBytes) — client wouldn't read them, so emitting
|
||||
// them just bloats the URL.
|
||||
// - client-only (headers, uplinkHTTPMethod, uplinkChunkSize,
|
||||
// noGRPCHeader, scMinPostsIntervalMs, xmux, downloadSettings) — the
|
||||
// inbound config doesn't have them; the client configures them
|
||||
// locally.
|
||||
//
|
||||
// Two encodings are written so every popular client can read at least one:
|
||||
//
|
||||
// - x_padding_bytes=<range> — flat param, understood by sing-box and its
|
||||
// derivatives (Podkop, OpenWRT sing-box, Karing, NekoBox, …).
|
||||
// - extra=<url-encoded-json> — full xhttp settings blob, which is how
|
||||
// xray-core clients (v2rayNG, Happ, Furious, Exclave, …) pick up the
|
||||
// obfs-mode key / header / placement / method.
|
||||
//
|
||||
// Anything that doesn't map to a non-empty value is skipped, so simple
|
||||
// inbounds (no custom padding) produce exactly the same URL as before.
|
||||
func applyXhttpPaddingParams(xhttp map[string]any, params map[string]string) {
|
||||
// Truthy-only guards keep default inbounds emitting the same compact URL
|
||||
// they did before this helper grew.
|
||||
func buildXhttpExtra(xhttp map[string]any) map[string]any {
|
||||
if xhttp == nil {
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
if xpb, ok := xhttp["xPaddingBytes"].(string); ok && len(xpb) > 0 {
|
||||
params["x_padding_bytes"] = xpb
|
||||
}
|
||||
|
||||
extra := map[string]any{}
|
||||
|
||||
if xpb, ok := xhttp["xPaddingBytes"].(string); ok && len(xpb) > 0 {
|
||||
extra["xPaddingBytes"] = xpb
|
||||
}
|
||||
if obfs, ok := xhttp["xPaddingObfsMode"].(bool); ok && obfs {
|
||||
extra["xPaddingObfsMode"] = true
|
||||
// The obfs-mode-only fields: only populate the ones the admin
|
||||
// actually set, so xray-core falls back to its own defaults for
|
||||
// the rest instead of seeing spurious empty strings.
|
||||
for _, field := range []string{"xPaddingKey", "xPaddingHeader", "xPaddingPlacement", "xPaddingMethod"} {
|
||||
if v, ok := xhttp[field].(string); ok && len(v) > 0 {
|
||||
extra[field] = v
|
||||
@@ -974,7 +954,74 @@ func applyXhttpPaddingParams(xhttp map[string]any, params map[string]string) {
|
||||
}
|
||||
}
|
||||
|
||||
if len(extra) > 0 {
|
||||
stringFields := []string{
|
||||
"sessionPlacement", "sessionKey",
|
||||
"seqPlacement", "seqKey",
|
||||
"uplinkDataPlacement", "uplinkDataKey",
|
||||
"scMaxEachPostBytes",
|
||||
}
|
||||
for _, field := range stringFields {
|
||||
if v, ok := xhttp[field].(string); ok && len(v) > 0 {
|
||||
extra[field] = v
|
||||
}
|
||||
}
|
||||
|
||||
// Headers — emitted as the {name: value} map upstream's struct
|
||||
// expects. The server runtime ignores this field, but the client
|
||||
// (consuming the share link) honors it. Drop any "host" entry —
|
||||
// host already wins as a top-level URL param.
|
||||
if rawHeaders, ok := xhttp["headers"].(map[string]any); ok && len(rawHeaders) > 0 {
|
||||
out := map[string]any{}
|
||||
for k, v := range rawHeaders {
|
||||
if strings.EqualFold(k, "host") {
|
||||
continue
|
||||
}
|
||||
out[k] = v
|
||||
}
|
||||
if len(out) > 0 {
|
||||
extra["headers"] = out
|
||||
}
|
||||
}
|
||||
|
||||
if len(extra) == 0 {
|
||||
return nil
|
||||
}
|
||||
return extra
|
||||
}
|
||||
|
||||
// applyXhttpExtraParams emits the full xhttp config into the URL query
|
||||
// params of a vless:// / trojan:// / ss:// link. Sets path/host/mode at
|
||||
// top level (xray's Build() always lets these win over `extra`) and packs
|
||||
// everything else into a JSON `extra` param. Also writes the flat
|
||||
// `x_padding_bytes` param sing-box-family clients understand.
|
||||
//
|
||||
// Without this, the admin's custom xPaddingBytes / sessionKey / etc. never
|
||||
// reach the client and handshakes are silently rejected with
|
||||
// `invalid padding (...) length: 0` — the client-visible symptom is
|
||||
// "xhttp doesn't connect" on OpenWRT / sing-box.
|
||||
//
|
||||
// Two encodings are written so every popular client can read at least one:
|
||||
//
|
||||
// - x_padding_bytes=<range> — flat param, understood by sing-box and its
|
||||
// derivatives (Podkop, OpenWRT sing-box, Karing, NekoBox, …).
|
||||
// - extra=<url-encoded-json> — full xhttp settings blob, which is how
|
||||
// xray-core clients (v2rayNG, Happ, Furious, Exclave, …) pick up the
|
||||
// bidirectional fields beyond path/host/mode.
|
||||
func applyXhttpExtraParams(xhttp map[string]any, params map[string]string) {
|
||||
if xhttp == nil {
|
||||
return
|
||||
}
|
||||
applyPathAndHostParams(xhttp, params)
|
||||
if mode, ok := xhttp["mode"].(string); ok {
|
||||
params["mode"] = mode
|
||||
}
|
||||
|
||||
if xpb, ok := xhttp["xPaddingBytes"].(string); ok && len(xpb) > 0 {
|
||||
params["x_padding_bytes"] = xpb
|
||||
}
|
||||
|
||||
extra := buildXhttpExtra(xhttp)
|
||||
if extra != nil {
|
||||
if b, err := json.Marshal(extra); err == nil {
|
||||
params["extra"] = string(b)
|
||||
}
|
||||
|
||||
@@ -472,54 +472,67 @@ class HTTPUpgradeStreamSettings extends XrayCommonClass {
|
||||
}
|
||||
}
|
||||
|
||||
// Mirrors the inbound (server-side) view of Xray-core's SplitHTTPConfig
|
||||
// (infra/conf/transport_internet.go). Only fields the server actually
|
||||
// reads at runtime, plus the bidirectional fields the server enforces,
|
||||
// live here. Client-only fields (uplinkHTTPMethod, uplinkChunkSize,
|
||||
// noGRPCHeader, scMinPostsIntervalMs, xmux, downloadSettings) belong on
|
||||
// the outbound class instead.
|
||||
//
|
||||
// `headers` is technically client-only at runtime (xray's listener
|
||||
// doesn't read it) but we keep it here so the admin can set request
|
||||
// headers that get embedded into the share link's `extra` blob — the
|
||||
// client picks them up from there.
|
||||
class xHTTPStreamSettings extends XrayCommonClass {
|
||||
constructor(
|
||||
// Bidirectional — must match between client and server
|
||||
path = '/',
|
||||
host = '',
|
||||
headers = [],
|
||||
scMaxBufferedPosts = 30,
|
||||
scMaxEachPostBytes = "1000000",
|
||||
scStreamUpServerSecs = "20-80",
|
||||
noSSEHeader = false,
|
||||
xPaddingBytes = "100-1000",
|
||||
mode = MODE_OPTION.AUTO,
|
||||
xPaddingBytes = "100-1000",
|
||||
xPaddingObfsMode = false,
|
||||
xPaddingKey = '',
|
||||
xPaddingHeader = '',
|
||||
xPaddingPlacement = '',
|
||||
xPaddingMethod = '',
|
||||
uplinkHTTPMethod = '',
|
||||
sessionPlacement = '',
|
||||
sessionKey = '',
|
||||
seqPlacement = '',
|
||||
seqKey = '',
|
||||
uplinkDataPlacement = '',
|
||||
uplinkDataKey = '',
|
||||
uplinkChunkSize = 0,
|
||||
scMaxEachPostBytes = "1000000",
|
||||
// Server-side only
|
||||
noSSEHeader = false,
|
||||
scMaxBufferedPosts = 30,
|
||||
scStreamUpServerSecs = "20-80",
|
||||
serverMaxHeaderBytes = 0,
|
||||
// URL-share only — embedded in the link's `extra` blob so clients
|
||||
// pick them up; xray's listener ignores them at runtime.
|
||||
headers = [],
|
||||
) {
|
||||
super();
|
||||
this.path = path;
|
||||
this.host = host;
|
||||
this.headers = headers;
|
||||
this.scMaxBufferedPosts = scMaxBufferedPosts;
|
||||
this.scMaxEachPostBytes = scMaxEachPostBytes;
|
||||
this.scStreamUpServerSecs = scStreamUpServerSecs;
|
||||
this.noSSEHeader = noSSEHeader;
|
||||
this.xPaddingBytes = xPaddingBytes;
|
||||
this.mode = mode;
|
||||
this.xPaddingBytes = xPaddingBytes;
|
||||
this.xPaddingObfsMode = xPaddingObfsMode;
|
||||
this.xPaddingKey = xPaddingKey;
|
||||
this.xPaddingHeader = xPaddingHeader;
|
||||
this.xPaddingPlacement = xPaddingPlacement;
|
||||
this.xPaddingMethod = xPaddingMethod;
|
||||
this.uplinkHTTPMethod = uplinkHTTPMethod;
|
||||
this.sessionPlacement = sessionPlacement;
|
||||
this.sessionKey = sessionKey;
|
||||
this.seqPlacement = seqPlacement;
|
||||
this.seqKey = seqKey;
|
||||
this.uplinkDataPlacement = uplinkDataPlacement;
|
||||
this.uplinkDataKey = uplinkDataKey;
|
||||
this.uplinkChunkSize = uplinkChunkSize;
|
||||
this.scMaxEachPostBytes = scMaxEachPostBytes;
|
||||
this.noSSEHeader = noSSEHeader;
|
||||
this.scMaxBufferedPosts = scMaxBufferedPosts;
|
||||
this.scStreamUpServerSecs = scStreamUpServerSecs;
|
||||
this.serverMaxHeaderBytes = serverMaxHeaderBytes;
|
||||
this.headers = headers;
|
||||
}
|
||||
|
||||
addHeader(name, value) {
|
||||
@@ -534,26 +547,25 @@ class xHTTPStreamSettings extends XrayCommonClass {
|
||||
return new xHTTPStreamSettings(
|
||||
json.path,
|
||||
json.host,
|
||||
XrayCommonClass.toHeaders(json.headers),
|
||||
json.scMaxBufferedPosts,
|
||||
json.scMaxEachPostBytes,
|
||||
json.scStreamUpServerSecs,
|
||||
json.noSSEHeader,
|
||||
json.xPaddingBytes,
|
||||
json.mode,
|
||||
json.xPaddingBytes,
|
||||
json.xPaddingObfsMode,
|
||||
json.xPaddingKey,
|
||||
json.xPaddingHeader,
|
||||
json.xPaddingPlacement,
|
||||
json.xPaddingMethod,
|
||||
json.uplinkHTTPMethod,
|
||||
json.sessionPlacement,
|
||||
json.sessionKey,
|
||||
json.seqPlacement,
|
||||
json.seqKey,
|
||||
json.uplinkDataPlacement,
|
||||
json.uplinkDataKey,
|
||||
json.uplinkChunkSize,
|
||||
json.scMaxEachPostBytes,
|
||||
json.noSSEHeader,
|
||||
json.scMaxBufferedPosts,
|
||||
json.scStreamUpServerSecs,
|
||||
json.serverMaxHeaderBytes,
|
||||
XrayCommonClass.toHeaders(json.headers),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -561,26 +573,25 @@ class xHTTPStreamSettings extends XrayCommonClass {
|
||||
return {
|
||||
path: this.path,
|
||||
host: this.host,
|
||||
headers: XrayCommonClass.toV2Headers(this.headers, false),
|
||||
scMaxBufferedPosts: this.scMaxBufferedPosts,
|
||||
scMaxEachPostBytes: this.scMaxEachPostBytes,
|
||||
scStreamUpServerSecs: this.scStreamUpServerSecs,
|
||||
noSSEHeader: this.noSSEHeader,
|
||||
xPaddingBytes: this.xPaddingBytes,
|
||||
mode: this.mode,
|
||||
xPaddingBytes: this.xPaddingBytes,
|
||||
xPaddingObfsMode: this.xPaddingObfsMode,
|
||||
xPaddingKey: this.xPaddingKey,
|
||||
xPaddingHeader: this.xPaddingHeader,
|
||||
xPaddingPlacement: this.xPaddingPlacement,
|
||||
xPaddingMethod: this.xPaddingMethod,
|
||||
uplinkHTTPMethod: this.uplinkHTTPMethod,
|
||||
sessionPlacement: this.sessionPlacement,
|
||||
sessionKey: this.sessionKey,
|
||||
seqPlacement: this.seqPlacement,
|
||||
seqKey: this.seqKey,
|
||||
uplinkDataPlacement: this.uplinkDataPlacement,
|
||||
uplinkDataKey: this.uplinkDataKey,
|
||||
uplinkChunkSize: this.uplinkChunkSize,
|
||||
scMaxEachPostBytes: this.scMaxEachPostBytes,
|
||||
noSSEHeader: this.noSSEHeader,
|
||||
scMaxBufferedPosts: this.scMaxBufferedPosts,
|
||||
scStreamUpServerSecs: this.scStreamUpServerSecs,
|
||||
serverMaxHeaderBytes: this.serverMaxHeaderBytes,
|
||||
headers: XrayCommonClass.toV2Headers(this.headers, false),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1523,26 +1534,39 @@ class Inbound extends XrayCommonClass {
|
||||
return this.clientStats;
|
||||
}
|
||||
|
||||
// Copy the xPadding* settings into the query-string of a vless/trojan/ss
|
||||
// link. Without this, the admin's custom xPaddingBytes range and (in
|
||||
// obfs mode) the custom xPaddingKey / xPaddingHeader / placement /
|
||||
// method never reach the client — the client keeps xray / sing-box's
|
||||
// internal defaults and the server rejects every handshake with
|
||||
// `invalid padding (...) length: 0`.
|
||||
//
|
||||
// Two encodings are emitted so each client family can pick at least
|
||||
// one up:
|
||||
// - x_padding_bytes=<range> flat, for sing-box-family clients
|
||||
// - extra=<url-encoded-json> full blob, for xray-core clients
|
||||
//
|
||||
// Fields are only included when they actually have a value, so a
|
||||
// default inbound yields the same URL it did before this helper.
|
||||
static applyXhttpPaddingToParams(xhttp, params) {
|
||||
if (!xhttp) return;
|
||||
if (typeof xhttp.xPaddingBytes === 'string' && xhttp.xPaddingBytes.length > 0) {
|
||||
params.set("x_padding_bytes", xhttp.xPaddingBytes);
|
||||
// Looks for a "host"-named entry in xhttp.headers and returns its value,
|
||||
// or '' if not found. Used as a fallback when xhttp.host is empty so the
|
||||
// share URL still carries a usable Host hint.
|
||||
static xhttpHostFallback(xhttp) {
|
||||
if (!xhttp || !Array.isArray(xhttp.headers)) return '';
|
||||
for (const h of xhttp.headers) {
|
||||
if (h && typeof h.name === 'string' && h.name.toLowerCase() === 'host') {
|
||||
return h.value || '';
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
// Build the JSON blob that goes into the URL's `extra` param (or, for
|
||||
// VMess, into the base64-encoded link object). Carries ONLY the
|
||||
// bidirectional fields from xray-core's SplitHTTPConfig — i.e. the
|
||||
// ones the server enforces and the client must match. Strictly
|
||||
// one-sided fields are excluded:
|
||||
//
|
||||
// - server-only (noSSEHeader, scMaxBufferedPosts,
|
||||
// scStreamUpServerSecs, serverMaxHeaderBytes) — client wouldn't
|
||||
// read them, so emitting them just bloats the URL.
|
||||
// - client-only (headers, uplinkHTTPMethod, uplinkChunkSize,
|
||||
// noGRPCHeader, scMinPostsIntervalMs, xmux, downloadSettings) —
|
||||
// not on the inbound class at all; the client configures them
|
||||
// locally.
|
||||
//
|
||||
// Truthy-only guards keep default inbounds emitting the same compact
|
||||
// URL they did before this helper grew.
|
||||
static buildXhttpExtra(xhttp) {
|
||||
if (!xhttp) return null;
|
||||
const extra = {};
|
||||
|
||||
if (typeof xhttp.xPaddingBytes === 'string' && xhttp.xPaddingBytes.length > 0) {
|
||||
extra.xPaddingBytes = xhttp.xPaddingBytes;
|
||||
}
|
||||
@@ -1554,26 +1578,73 @@ class Inbound extends XrayCommonClass {
|
||||
}
|
||||
});
|
||||
}
|
||||
if (Object.keys(extra).length > 0) {
|
||||
params.set("extra", JSON.stringify(extra));
|
||||
|
||||
const stringFields = [
|
||||
"sessionPlacement", "sessionKey",
|
||||
"seqPlacement", "seqKey",
|
||||
"uplinkDataPlacement", "uplinkDataKey",
|
||||
"scMaxEachPostBytes",
|
||||
];
|
||||
for (const k of stringFields) {
|
||||
const v = xhttp[k];
|
||||
if (typeof v === 'string' && v.length > 0) extra[k] = v;
|
||||
}
|
||||
|
||||
// Headers — emitted as the {name: value} map upstream's struct
|
||||
// expects. The server runtime ignores this field, but the client
|
||||
// (consuming the share link) honors it.
|
||||
if (Array.isArray(xhttp.headers) && xhttp.headers.length > 0) {
|
||||
const headersMap = {};
|
||||
for (const h of xhttp.headers) {
|
||||
if (h && h.name && h.name.toLowerCase() !== 'host') {
|
||||
headersMap[h.name] = h.value || '';
|
||||
}
|
||||
}
|
||||
if (Object.keys(headersMap).length > 0) extra.headers = headersMap;
|
||||
}
|
||||
|
||||
return Object.keys(extra).length > 0 ? extra : null;
|
||||
}
|
||||
|
||||
// Inject the inbound-side xhttp config into URL query params for
|
||||
// vless/trojan/ss links. Sets path/host/mode at top level (xray's
|
||||
// Build() always lets these win over `extra`) and packs the
|
||||
// bidirectional fields into a JSON `extra` param. Also writes the
|
||||
// flat `x_padding_bytes` param sing-box-family clients understand.
|
||||
//
|
||||
// Without this, the admin's custom xPaddingBytes / sessionKey / etc.
|
||||
// never reach the client and handshakes are silently rejected with
|
||||
// `invalid padding (...) length: 0`.
|
||||
static applyXhttpExtraToParams(xhttp, params) {
|
||||
if (!xhttp) return;
|
||||
params.set("path", xhttp.path);
|
||||
const host = xhttp.host?.length > 0 ? xhttp.host : Inbound.xhttpHostFallback(xhttp);
|
||||
params.set("host", host);
|
||||
params.set("mode", xhttp.mode);
|
||||
|
||||
// Flat fallback for sing-box-family clients that don't read `extra`.
|
||||
if (typeof xhttp.xPaddingBytes === 'string' && xhttp.xPaddingBytes.length > 0) {
|
||||
params.set("x_padding_bytes", xhttp.xPaddingBytes);
|
||||
}
|
||||
|
||||
const extra = Inbound.buildXhttpExtra(xhttp);
|
||||
if (extra) params.set("extra", JSON.stringify(extra));
|
||||
}
|
||||
|
||||
// VMess variant: VMess links are a base64-encoded JSON object, so we
|
||||
// copy the padding fields directly into the JSON instead of building
|
||||
// a query string.
|
||||
static applyXhttpPaddingToObj(xhttp, obj) {
|
||||
// copy the same bidirectional fields directly into the JSON instead
|
||||
// of building a query string. (The base VMess link generator already
|
||||
// sets net/type/path/host, so we only contribute the SplitHTTPConfig
|
||||
// extra side here.)
|
||||
static applyXhttpExtraToObj(xhttp, obj) {
|
||||
if (!xhttp || !obj) return;
|
||||
if (typeof xhttp.xPaddingBytes === 'string' && xhttp.xPaddingBytes.length > 0) {
|
||||
obj.x_padding_bytes = xhttp.xPaddingBytes;
|
||||
}
|
||||
if (xhttp.xPaddingObfsMode === true) {
|
||||
obj.xPaddingObfsMode = true;
|
||||
["xPaddingKey", "xPaddingHeader", "xPaddingPlacement", "xPaddingMethod"].forEach(k => {
|
||||
if (typeof xhttp[k] === 'string' && xhttp[k].length > 0) {
|
||||
obj[k] = xhttp[k];
|
||||
}
|
||||
});
|
||||
const extra = Inbound.buildXhttpExtra(xhttp);
|
||||
if (!extra) return;
|
||||
for (const [k, v] of Object.entries(extra)) {
|
||||
obj[k] = v;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1839,7 +1910,7 @@ class Inbound extends XrayCommonClass {
|
||||
obj.path = xhttp.path;
|
||||
obj.host = xhttp.host?.length > 0 ? xhttp.host : this.getHeader(xhttp, 'host');
|
||||
obj.type = xhttp.mode;
|
||||
Inbound.applyXhttpPaddingToObj(xhttp, obj);
|
||||
Inbound.applyXhttpExtraToObj(xhttp, obj);
|
||||
}
|
||||
|
||||
Inbound.applyFinalMaskToObj(this.stream.finalmask, obj);
|
||||
@@ -1904,11 +1975,7 @@ class Inbound extends XrayCommonClass {
|
||||
params.set("host", httpupgrade.host?.length > 0 ? httpupgrade.host : this.getHeader(httpupgrade, 'host'));
|
||||
break;
|
||||
case "xhttp":
|
||||
const xhttp = this.stream.xhttp;
|
||||
params.set("path", xhttp.path);
|
||||
params.set("host", xhttp.host?.length > 0 ? xhttp.host : this.getHeader(xhttp, 'host'));
|
||||
params.set("mode", xhttp.mode);
|
||||
Inbound.applyXhttpPaddingToParams(xhttp, params);
|
||||
Inbound.applyXhttpExtraToParams(this.stream.xhttp, params);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -2009,11 +2076,7 @@ class Inbound extends XrayCommonClass {
|
||||
params.set("host", httpupgrade.host?.length > 0 ? httpupgrade.host : this.getHeader(httpupgrade, 'host'));
|
||||
break;
|
||||
case "xhttp":
|
||||
const xhttp = this.stream.xhttp;
|
||||
params.set("path", xhttp.path);
|
||||
params.set("host", xhttp.host?.length > 0 ? xhttp.host : this.getHeader(xhttp, 'host'));
|
||||
params.set("mode", xhttp.mode);
|
||||
Inbound.applyXhttpPaddingToParams(xhttp, params);
|
||||
Inbound.applyXhttpExtraToParams(this.stream.xhttp, params);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -2090,11 +2153,7 @@ class Inbound extends XrayCommonClass {
|
||||
params.set("host", httpupgrade.host?.length > 0 ? httpupgrade.host : this.getHeader(httpupgrade, 'host'));
|
||||
break;
|
||||
case "xhttp":
|
||||
const xhttp = this.stream.xhttp;
|
||||
params.set("path", xhttp.path);
|
||||
params.set("host", xhttp.host?.length > 0 ? xhttp.host : this.getHeader(xhttp, 'host'));
|
||||
params.set("mode", xhttp.mode);
|
||||
Inbound.applyXhttpPaddingToParams(xhttp, params);
|
||||
Inbound.applyXhttpExtraToParams(this.stream.xhttp, params);
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -566,36 +566,150 @@
|
||||
<a-form-item label='{{ i18n "path" }}'>
|
||||
<a-input v-model.trim="outbound.stream.xhttp.path"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.requestHeader" }}'>
|
||||
<a-button icon="plus" size="small" @click="outbound.stream.xhttp.addHeader('', '')"></a-button>
|
||||
</a-form-item>
|
||||
<a-form-item :wrapper-col="{span:24}">
|
||||
<a-input-group compact v-for="(header, index) in outbound.stream.xhttp.headers">
|
||||
<a-input :style="{ width: '50%' }" v-model.trim="header.name"
|
||||
placeholder='{{ i18n "pages.inbounds.stream.general.name"}}'>
|
||||
<template slot="addonBefore">[[ index+1 ]]</template>
|
||||
</a-input>
|
||||
<a-input :style="{ width: '50%' }" v-model.trim="header.value"
|
||||
placeholder='{{ i18n "pages.inbounds.stream.general.value" }}'>
|
||||
<a-button icon="minus" slot="addonAfter" size="small"
|
||||
@click="outbound.stream.xhttp.removeHeader(index)"></a-button>
|
||||
</a-input>
|
||||
</a-input-group>
|
||||
</a-form-item>
|
||||
<a-form-item label="Mode">
|
||||
<a-select v-model="outbound.stream.xhttp.mode" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="key in MODE_OPTION" :value="key">[[ key ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="No gRPC Header"
|
||||
v-if="outbound.stream.xhttp.mode === 'stream-up' || outbound.stream.xhttp.mode === 'stream-one'">
|
||||
<a-switch v-model="outbound.stream.xhttp.noGRPCHeader"></a-switch>
|
||||
<a-form-item label="Max Upload Size (Byte)" v-if="outbound.stream.xhttp.mode === 'packet-up'">
|
||||
<a-input v-model.trim="outbound.stream.xhttp.scMaxEachPostBytes"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="Min Upload Interval (Ms)" v-if="outbound.stream.xhttp.mode === 'packet-up'">
|
||||
<a-input v-model.trim="outbound.stream.xhttp.scMinPostsIntervalMs"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="Max Concurrency" v-if="!outbound.stream.xhttp.xmux.maxConnections">
|
||||
<a-input v-model="outbound.stream.xhttp.xmux.maxConcurrency"></a-input>
|
||||
<a-form-item label="Padding Bytes">
|
||||
<a-input v-model.trim="outbound.stream.xhttp.xPaddingBytes"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="Max Connections" v-if="!outbound.stream.xhttp.xmux.maxConcurrency">
|
||||
<a-input v-model="outbound.stream.xhttp.xmux.maxConnections"></a-input>
|
||||
<a-form-item label="Padding Obfs Mode">
|
||||
<a-switch v-model="outbound.stream.xhttp.xPaddingObfsMode"></a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item label="Max Reuse Times">
|
||||
<a-input v-model="outbound.stream.xhttp.xmux.cMaxReuseTimes"></a-input>
|
||||
<template v-if="outbound.stream.xhttp.xPaddingObfsMode">
|
||||
<a-form-item label="Padding Key">
|
||||
<a-input v-model.trim="outbound.stream.xhttp.xPaddingKey" placeholder="x_padding"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="Padding Header">
|
||||
<a-input v-model.trim="outbound.stream.xhttp.xPaddingHeader" placeholder="X-Padding"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="Padding Placement">
|
||||
<a-select v-model="outbound.stream.xhttp.xPaddingPlacement"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value>Default (queryInHeader)</a-select-option>
|
||||
<a-select-option value="queryInHeader">queryInHeader</a-select-option>
|
||||
<a-select-option value="header">header</a-select-option>
|
||||
<a-select-option value="cookie">cookie</a-select-option>
|
||||
<a-select-option value="query">query</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="Padding Method">
|
||||
<a-select v-model="outbound.stream.xhttp.xPaddingMethod"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value>Default (repeat-x)</a-select-option>
|
||||
<a-select-option value="repeat-x">repeat-x</a-select-option>
|
||||
<a-select-option value="tokenish">tokenish</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</template>
|
||||
<a-form-item label="Uplink HTTP Method">
|
||||
<a-select v-model="outbound.stream.xhttp.uplinkHTTPMethod"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value>Default (POST)</a-select-option>
|
||||
<a-select-option value="POST">POST</a-select-option>
|
||||
<a-select-option value="PUT">PUT</a-select-option>
|
||||
<a-select-option value="GET" :disabled="outbound.stream.xhttp.mode !== 'packet-up'">
|
||||
GET (packet-up only)
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="Max Request Times">
|
||||
<a-input v-model="outbound.stream.xhttp.xmux.hMaxRequestTimes"></a-input>
|
||||
<a-form-item label="Session Placement">
|
||||
<a-select v-model="outbound.stream.xhttp.sessionPlacement"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value>Default (path)</a-select-option>
|
||||
<a-select-option value="path">path</a-select-option>
|
||||
<a-select-option value="header">header</a-select-option>
|
||||
<a-select-option value="cookie">cookie</a-select-option>
|
||||
<a-select-option value="query">query</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="Max Reusable Secs">
|
||||
<a-input v-model="outbound.stream.xhttp.xmux.hMaxReusableSecs"></a-input>
|
||||
<a-form-item label="Session Key"
|
||||
v-if="outbound.stream.xhttp.sessionPlacement && outbound.stream.xhttp.sessionPlacement !== 'path'">
|
||||
<a-input v-model.trim="outbound.stream.xhttp.sessionKey" placeholder="x_session"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="Keep Alive Period">
|
||||
<a-input-number v-model.number="outbound.stream.xhttp.xmux.hKeepAlivePeriod"></a-input-number>
|
||||
<a-form-item label="Sequence Placement">
|
||||
<a-select v-model="outbound.stream.xhttp.seqPlacement"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value>Default (path)</a-select-option>
|
||||
<a-select-option value="path">path</a-select-option>
|
||||
<a-select-option value="header">header</a-select-option>
|
||||
<a-select-option value="cookie">cookie</a-select-option>
|
||||
<a-select-option value="query">query</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="Sequence Key"
|
||||
v-if="outbound.stream.xhttp.seqPlacement && outbound.stream.xhttp.seqPlacement !== 'path'">
|
||||
<a-input v-model.trim="outbound.stream.xhttp.seqKey" placeholder="x_seq"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="Uplink Data Placement" v-if="outbound.stream.xhttp.mode === 'packet-up'">
|
||||
<a-select v-model="outbound.stream.xhttp.uplinkDataPlacement"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value>Default (body)</a-select-option>
|
||||
<a-select-option value="body">body</a-select-option>
|
||||
<a-select-option value="header">header</a-select-option>
|
||||
<a-select-option value="cookie">cookie</a-select-option>
|
||||
<a-select-option value="query">query</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="Uplink Data Key"
|
||||
v-if="outbound.stream.xhttp.mode === 'packet-up' && outbound.stream.xhttp.uplinkDataPlacement && outbound.stream.xhttp.uplinkDataPlacement !== 'body'">
|
||||
<a-input v-model.trim="outbound.stream.xhttp.uplinkDataKey" placeholder="x_data"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="Uplink Chunk Size"
|
||||
v-if="outbound.stream.xhttp.mode === 'packet-up' && outbound.stream.xhttp.uplinkDataPlacement && outbound.stream.xhttp.uplinkDataPlacement !== 'body'">
|
||||
<a-input-number v-model.number="outbound.stream.xhttp.uplinkChunkSize" :min="0"
|
||||
placeholder="0 (unlimited)"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label="No gRPC Header"
|
||||
v-if="outbound.stream.xhttp.mode === 'stream-up' || outbound.stream.xhttp.mode === 'stream-one'">
|
||||
<a-switch v-model="outbound.stream.xhttp.noGRPCHeader"></a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item label="XMUX">
|
||||
<a-switch v-model="outbound.stream.xhttp.enableXmux"></a-switch>
|
||||
</a-form-item>
|
||||
<template v-if="outbound.stream.xhttp.enableXmux">
|
||||
<a-form-item label="Max Concurrency" v-if="!outbound.stream.xhttp.xmux.maxConnections">
|
||||
<a-input v-model="outbound.stream.xhttp.xmux.maxConcurrency"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="Max Connections" v-if="!outbound.stream.xhttp.xmux.maxConcurrency">
|
||||
<a-input v-model="outbound.stream.xhttp.xmux.maxConnections"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="Max Reuse Times">
|
||||
<a-input v-model="outbound.stream.xhttp.xmux.cMaxReuseTimes"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="Max Request Times">
|
||||
<a-input v-model="outbound.stream.xhttp.xmux.hMaxRequestTimes"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="Max Reusable Secs">
|
||||
<a-input v-model="outbound.stream.xhttp.xmux.hMaxReusableSecs"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="Keep Alive Period">
|
||||
<a-input-number v-model.number="outbound.stream.xhttp.xmux.hKeepAlivePeriod"></a-input-number>
|
||||
</a-form-item>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<!-- hysteria -->
|
||||
|
||||
@@ -37,6 +37,10 @@
|
||||
<a-form-item label="Stream-Up Server" v-if="inbound.stream.xhttp.mode === 'stream-up'">
|
||||
<a-input v-model.trim="inbound.stream.xhttp.scStreamUpServerSecs"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="Server Max Header Bytes">
|
||||
<a-input-number v-model.number="inbound.stream.xhttp.serverMaxHeaderBytes" :min="0"
|
||||
placeholder="0 (default)"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label="Padding Bytes">
|
||||
<a-input v-model.trim="inbound.stream.xhttp.xPaddingBytes"></a-input>
|
||||
</a-form-item>
|
||||
@@ -67,14 +71,6 @@
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</template>
|
||||
<a-form-item label="Uplink HTTP Method">
|
||||
<a-select v-model="inbound.stream.xhttp.uplinkHTTPMethod" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value>Default (POST)</a-select-option>
|
||||
<a-select-option value="POST">POST</a-select-option>
|
||||
<a-select-option value="PUT">PUT</a-select-option>
|
||||
<a-select-option value="GET">GET (packet-up only)</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="Session Placement">
|
||||
<a-select v-model="inbound.stream.xhttp.sessionPlacement" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value>Default (path)</a-select-option>
|
||||
@@ -114,11 +110,6 @@
|
||||
v-if="inbound.stream.xhttp.mode === 'packet-up' && inbound.stream.xhttp.uplinkDataPlacement && inbound.stream.xhttp.uplinkDataPlacement !== 'body'">
|
||||
<a-input v-model.trim="inbound.stream.xhttp.uplinkDataKey" placeholder="x_data"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="Uplink Chunk Size"
|
||||
v-if="inbound.stream.xhttp.mode === 'packet-up' && inbound.stream.xhttp.uplinkDataPlacement && inbound.stream.xhttp.uplinkDataPlacement !== 'body'">
|
||||
<a-input-number v-model.number="inbound.stream.xhttp.uplinkChunkSize" :min="0"
|
||||
placeholder="0 (unlimited)"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label="No SSE Header">
|
||||
<a-switch v-model="inbound.stream.xhttp.noSSEHeader"></a-switch>
|
||||
</a-form-item>
|
||||
|
||||
@@ -104,6 +104,17 @@
|
||||
return outModal.outbound;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
// xray-core's SplitHTTPConfig.Build() rejects "GET" as
|
||||
// uplinkHTTPMethod outside packet-up mode. Clear the field
|
||||
// instead of carrying an invalid combination through.
|
||||
"outModal.outbound.stream.xhttp.mode"(newMode) {
|
||||
const xhttp = outModal.outbound.stream && outModal.outbound.stream.xhttp;
|
||||
if (xhttp && xhttp.uplinkHTTPMethod === "GET" && newMode !== "packet-up") {
|
||||
xhttp.uplinkHTTPMethod = "";
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
streamNetworkChange() {
|
||||
if (this.outModal.outbound.protocol == Protocols.VLESS && !outModal.outbound
|
||||
|
||||
Reference in New Issue
Block a user