mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-05 12:44:22 +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:
parent
9f84859ff6
commit
2f1a146f45
2 changed files with 82 additions and 13 deletions
|
|
@ -7,12 +7,13 @@ import { Base64 } from '@/utils';
|
||||||
//
|
//
|
||||||
// Scope: address + port + auth + remark, plus the network/security
|
// Scope: address + port + auth + remark, plus the network/security
|
||||||
// fields the common vmess:// / vless:// links carry as query params.
|
// fields the common vmess:// / vless:// links carry as query params.
|
||||||
// Advanced transport fields (xmux, padding obfs, hysteria udphop,
|
// XHTTP advanced fields (xPaddingBytes, scMaxEachPostBytes,
|
||||||
// reality short IDs, etc.) are not parsed — the user finishes them
|
// scMinPostsIntervalMs, uplinkChunkSize, noGRPCHeader) round-trip when
|
||||||
// in the form after import. This is intentional: a focused parser
|
// present in either the JSON or URL params. xmux, reality shortIds,
|
||||||
// keeps the surface small; the legacy Outbound.fromLink was ~250
|
// padding obfs key/header/placement, hysteria udphop are still left
|
||||||
// lines of dense edge-case handling we don't need to replicate
|
// to the user to fill in after import — the legacy Outbound.fromLink
|
||||||
// verbatim for the common phone-to-panel workflow.
|
// 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>;
|
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).host = host;
|
||||||
(stream.httpupgradeSettings as Raw).path = path;
|
(stream.httpupgradeSettings as Raw).path = path;
|
||||||
break;
|
break;
|
||||||
case 'xhttp':
|
case 'xhttp': {
|
||||||
(stream.xhttpSettings as Raw).host = host;
|
const xhttp = stream.xhttpSettings as Raw;
|
||||||
(stream.xhttpSettings as Raw).path = path;
|
xhttp.host = host;
|
||||||
if (params.get('mode')) (stream.xhttpSettings as Raw).mode = params.get('mode');
|
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;
|
break;
|
||||||
|
}
|
||||||
case 'tcp':
|
case 'tcp':
|
||||||
// vless/trojan TCP HTTP camouflage rides on header=http+host+path
|
// vless/trojan TCP HTTP camouflage rides on header=http+host+path
|
||||||
if (params.get('headerType') === 'http' || params.get('type') === 'http') {
|
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).host = json.host ?? '';
|
||||||
(stream.httpupgradeSettings as Raw).path = json.path ?? '/';
|
(stream.httpupgradeSettings as Raw).path = json.path ?? '/';
|
||||||
} else if (network === 'xhttp') {
|
} else if (network === 'xhttp') {
|
||||||
(stream.xhttpSettings as Raw).host = json.host ?? '';
|
const xhttp = stream.xhttpSettings as Raw;
|
||||||
(stream.xhttpSettings as Raw).path = json.path ?? '/';
|
xhttp.host = json.host ?? '';
|
||||||
if (json.mode) (stream.xhttpSettings as Raw).mode = json.mode;
|
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') {
|
if (security === 'tls') {
|
||||||
const tls = stream.tlsSettings as Raw;
|
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', () => {
|
describe('parseVlessLink', () => {
|
||||||
it('parses a vless:// link with reality', () => {
|
it('parses a vless:// link with reality', () => {
|
||||||
const link
|
const link
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue