mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-05-28 16:09:36 +00:00
feat(frontend): round-trip XHTTP advanced fields in outbound link parser
Pick up xPaddingBytes, scMaxEachPostBytes, scMinPostsIntervalMs, uplinkChunkSize, and noGRPCHeader from both vmess:// JSON and the URL query-param parsers (vless/trojan). The advanced xmux/padding-obfs/ reality-shortId knobs still wait on a follow-up; this slice unblocks the common case where a phone-issued xhttp link carries non-default padding or post sizes.
This commit is contained in:
@@ -7,12 +7,13 @@ import { Base64 } from '@/utils';
|
||||
//
|
||||
// Scope: address + port + auth + remark, plus the network/security
|
||||
// fields the common vmess:// / vless:// links carry as query params.
|
||||
// Advanced transport fields (xmux, padding obfs, hysteria udphop,
|
||||
// reality short IDs, etc.) are not parsed — the user finishes them
|
||||
// in the form after import. This is intentional: a focused parser
|
||||
// keeps the surface small; the legacy Outbound.fromLink was ~250
|
||||
// lines of dense edge-case handling we don't need to replicate
|
||||
// verbatim for the common phone-to-panel workflow.
|
||||
// XHTTP advanced fields (xPaddingBytes, scMaxEachPostBytes,
|
||||
// scMinPostsIntervalMs, uplinkChunkSize, noGRPCHeader) round-trip when
|
||||
// present in either the JSON or URL params. xmux, reality shortIds,
|
||||
// padding obfs key/header/placement, hysteria udphop are still left
|
||||
// to the user to fill in after import — the legacy Outbound.fromLink
|
||||
// was ~250 lines of dense edge-case handling we don't need to
|
||||
// replicate verbatim for the common phone-to-panel workflow.
|
||||
|
||||
type Raw = Record<string, unknown>;
|
||||
|
||||
@@ -81,11 +82,23 @@ function applyTransportParams(stream: Raw, params: URLSearchParams): void {
|
||||
(stream.httpupgradeSettings as Raw).host = host;
|
||||
(stream.httpupgradeSettings as Raw).path = path;
|
||||
break;
|
||||
case 'xhttp':
|
||||
(stream.xhttpSettings as Raw).host = host;
|
||||
(stream.xhttpSettings as Raw).path = path;
|
||||
if (params.get('mode')) (stream.xhttpSettings as Raw).mode = params.get('mode');
|
||||
case 'xhttp': {
|
||||
const xhttp = stream.xhttpSettings as Raw;
|
||||
xhttp.host = host;
|
||||
xhttp.path = path;
|
||||
if (params.get('mode')) xhttp.mode = params.get('mode');
|
||||
const xPad = params.get('xPaddingBytes');
|
||||
if (xPad) xhttp.xPaddingBytes = xPad;
|
||||
const scMax = params.get('scMaxEachPostBytes');
|
||||
if (scMax) xhttp.scMaxEachPostBytes = scMax;
|
||||
const scMin = params.get('scMinPostsIntervalMs');
|
||||
if (scMin) xhttp.scMinPostsIntervalMs = scMin;
|
||||
const upChunk = params.get('uplinkChunkSize');
|
||||
if (upChunk) xhttp.uplinkChunkSize = Number(upChunk) || 0;
|
||||
const noGrpc = params.get('noGRPCHeader');
|
||||
if (noGrpc) xhttp.noGRPCHeader = noGrpc === 'true' || noGrpc === '1';
|
||||
break;
|
||||
}
|
||||
case 'tcp':
|
||||
// vless/trojan TCP HTTP camouflage rides on header=http+host+path
|
||||
if (params.get('headerType') === 'http' || params.get('type') === 'http') {
|
||||
@@ -157,9 +170,15 @@ export function parseVmessLink(link: string): Raw | null {
|
||||
(stream.httpupgradeSettings as Raw).host = json.host ?? '';
|
||||
(stream.httpupgradeSettings as Raw).path = json.path ?? '/';
|
||||
} else if (network === 'xhttp') {
|
||||
(stream.xhttpSettings as Raw).host = json.host ?? '';
|
||||
(stream.xhttpSettings as Raw).path = json.path ?? '/';
|
||||
if (json.mode) (stream.xhttpSettings as Raw).mode = json.mode;
|
||||
const xhttp = stream.xhttpSettings as Raw;
|
||||
xhttp.host = json.host ?? '';
|
||||
xhttp.path = json.path ?? '/';
|
||||
if (json.mode) xhttp.mode = json.mode;
|
||||
if (typeof json.xPaddingBytes === 'string') xhttp.xPaddingBytes = json.xPaddingBytes;
|
||||
if (typeof json.scMaxEachPostBytes === 'string') xhttp.scMaxEachPostBytes = json.scMaxEachPostBytes;
|
||||
if (typeof json.scMinPostsIntervalMs === 'string') xhttp.scMinPostsIntervalMs = json.scMinPostsIntervalMs;
|
||||
if (typeof json.uplinkChunkSize === 'number') xhttp.uplinkChunkSize = json.uplinkChunkSize;
|
||||
if (typeof json.noGRPCHeader === 'boolean') xhttp.noGRPCHeader = json.noGRPCHeader;
|
||||
}
|
||||
if (security === 'tls') {
|
||||
const tls = stream.tlsSettings as Raw;
|
||||
|
||||
@@ -49,6 +49,56 @@ describe('parseVmessLink', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseVmessLink — XHTTP advanced fields', () => {
|
||||
it('round-trips xhttp knobs from the vmess JSON', () => {
|
||||
const json = {
|
||||
v: '2', ps: 'imported-xhttp', add: '1.2.3.4', port: 443,
|
||||
id: '11111111-2222-4333-8444-555555555555', aid: 0, scy: 'auto',
|
||||
net: 'xhttp', host: 'edge.example', path: '/sp', mode: 'stream-up',
|
||||
xPaddingBytes: '500-1500',
|
||||
scMaxEachPostBytes: '2000000',
|
||||
scMinPostsIntervalMs: '60',
|
||||
uplinkChunkSize: 8192,
|
||||
noGRPCHeader: true,
|
||||
tls: 'tls', sni: 'edge.example',
|
||||
};
|
||||
const link = `vmess://${Base64.encode(JSON.stringify(json))}`;
|
||||
const out = parseVmessLink(link);
|
||||
const stream = out?.streamSettings as Record<string, unknown>;
|
||||
const xhttp = stream.xhttpSettings as Record<string, unknown>;
|
||||
expect(xhttp.host).toBe('edge.example');
|
||||
expect(xhttp.path).toBe('/sp');
|
||||
expect(xhttp.mode).toBe('stream-up');
|
||||
expect(xhttp.xPaddingBytes).toBe('500-1500');
|
||||
expect(xhttp.scMaxEachPostBytes).toBe('2000000');
|
||||
expect(xhttp.scMinPostsIntervalMs).toBe('60');
|
||||
expect(xhttp.uplinkChunkSize).toBe(8192);
|
||||
expect(xhttp.noGRPCHeader).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseVlessLink — XHTTP advanced fields', () => {
|
||||
it('round-trips xhttp knobs from URL query params', () => {
|
||||
const link
|
||||
= 'vless://uuid@srv.example:443'
|
||||
+ '?type=xhttp&security=tls&host=edge.example&path=%2Fsp&mode=stream-up'
|
||||
+ '&xPaddingBytes=500-1500&scMaxEachPostBytes=2000000'
|
||||
+ '&scMinPostsIntervalMs=60&uplinkChunkSize=8192&noGRPCHeader=true'
|
||||
+ '#imported-xhttp';
|
||||
const out = parseVlessLink(link);
|
||||
const stream = out?.streamSettings as Record<string, unknown>;
|
||||
const xhttp = stream.xhttpSettings as Record<string, unknown>;
|
||||
expect(xhttp.host).toBe('edge.example');
|
||||
expect(xhttp.path).toBe('/sp');
|
||||
expect(xhttp.mode).toBe('stream-up');
|
||||
expect(xhttp.xPaddingBytes).toBe('500-1500');
|
||||
expect(xhttp.scMaxEachPostBytes).toBe('2000000');
|
||||
expect(xhttp.scMinPostsIntervalMs).toBe('60');
|
||||
expect(xhttp.uplinkChunkSize).toBe(8192);
|
||||
expect(xhttp.noGRPCHeader).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseVlessLink', () => {
|
||||
it('parses a vless:// link with reality', () => {
|
||||
const link
|
||||
|
||||
Reference in New Issue
Block a user