| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261 |
- import { describe, expect, it } from 'vitest';
- import {
- parseOutboundLink,
- parseShadowsocksLink,
- parseTrojanLink,
- parseVlessLink,
- parseVmessLink,
- parseHysteria2Link,
- } from '@/lib/xray/outbound-link-parser';
- import { Base64 } from '@/utils';
- // Focused acceptance tests for the share-link parsers — one happy-path
- // case per protocol family, plus a few common edge cases. The parsers
- // produce wire-shape outbound rows; the modal hands them to
- // rawOutboundToFormValues to seed Form.useForm.
- describe('parseVmessLink', () => {
- it('parses a vmess:// link with ws + tls', () => {
- const json = {
- v: '2', ps: 'imported-vmess', add: '1.2.3.4', port: 8443,
- id: '11111111-2222-4333-8444-555555555555', aid: 0, scy: 'auto',
- net: 'ws', host: 'example.com', path: '/ws',
- tls: 'tls', sni: 'example.com', fp: 'chrome', alpn: 'h2,http/1.1',
- };
- const link = `vmess://${Base64.encode(JSON.stringify(json))}`;
- const out = parseVmessLink(link);
- expect(out).not.toBeNull();
- expect(out?.protocol).toBe('vmess');
- expect(out?.tag).toBe('imported-vmess');
- const settings = out?.settings as { vnext: Array<{ address: string; port: number; users: Array<{ id: string; security: string }> }> };
- expect(settings.vnext[0].address).toBe('1.2.3.4');
- expect(settings.vnext[0].port).toBe(8443);
- expect(settings.vnext[0].users[0].id).toBe('11111111-2222-4333-8444-555555555555');
- const stream = out?.streamSettings as Record<string, unknown>;
- expect(stream.network).toBe('ws');
- expect(stream.security).toBe('tls');
- expect((stream.wsSettings as Record<string, unknown>).path).toBe('/ws');
- expect((stream.tlsSettings as Record<string, unknown>).serverName).toBe('example.com');
- expect((stream.tlsSettings as Record<string, unknown>).alpn).toEqual(['h2', 'http/1.1']);
- });
- it('returns null for non-vmess links', () => {
- expect(parseVmessLink('vless://x@y:1')).toBeNull();
- });
- it('returns null for malformed base64', () => {
- expect(parseVmessLink('vmess://!!!not-base64!!!')).toBeNull();
- });
- });
- 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);
- });
- it('round-trips xhttp padding-obfs knobs from the vmess JSON', () => {
- const json = {
- v: '2', ps: 'imported-pad', add: '1.2.3.4', port: 443,
- id: '11111111-2222-4333-8444-555555555555', aid: 0, scy: 'auto',
- net: 'xhttp', host: 'edge.example', path: '/sp',
- xPaddingObfsMode: true,
- xPaddingKey: 'secret-key',
- xPaddingHeader: 'X-Pad',
- xPaddingPlacement: 'header',
- xPaddingMethod: 'random',
- sessionKey: 'X-Session',
- seqKey: 'X-Seq',
- noSSEHeader: true,
- scMaxBufferedPosts: 50,
- tls: 'tls',
- };
- const link = `vmess://${Base64.encode(JSON.stringify(json))}`;
- const out = parseVmessLink(link);
- const xhttp = (out?.streamSettings as Record<string, unknown>).xhttpSettings as Record<string, unknown>;
- expect(xhttp.xPaddingObfsMode).toBe(true);
- expect(xhttp.xPaddingKey).toBe('secret-key');
- expect(xhttp.xPaddingHeader).toBe('X-Pad');
- expect(xhttp.xPaddingPlacement).toBe('header');
- expect(xhttp.xPaddingMethod).toBe('random');
- expect(xhttp.sessionKey).toBe('X-Session');
- expect(xhttp.seqKey).toBe('X-Seq');
- expect(xhttp.noSSEHeader).toBe(true);
- expect(xhttp.scMaxBufferedPosts).toBe(50);
- });
- });
- describe('parseVlessLink — XHTTP advanced fields', () => {
- it('round-trips xhttp knobs from URL query params', () => {
- const link
- = 'vless://[email protected]: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);
- });
- it('round-trips xhttp padding-obfs knobs from URL query params', () => {
- const link
- = 'vless://[email protected]:443'
- + '?type=xhttp&security=tls&host=edge.example&path=%2Fsp'
- + '&xPaddingObfsMode=true&xPaddingKey=secret-key&xPaddingHeader=X-Pad'
- + '&xPaddingPlacement=header&xPaddingMethod=random'
- + '&sessionKey=X-Session&seqKey=X-Seq&noSSEHeader=true'
- + '&scMaxBufferedPosts=50'
- + '#imported-pad';
- const out = parseVlessLink(link);
- const xhttp = (out?.streamSettings as Record<string, unknown>).xhttpSettings as Record<string, unknown>;
- expect(xhttp.xPaddingObfsMode).toBe(true);
- expect(xhttp.xPaddingKey).toBe('secret-key');
- expect(xhttp.xPaddingHeader).toBe('X-Pad');
- expect(xhttp.xPaddingPlacement).toBe('header');
- expect(xhttp.xPaddingMethod).toBe('random');
- expect(xhttp.sessionKey).toBe('X-Session');
- expect(xhttp.seqKey).toBe('X-Seq');
- expect(xhttp.noSSEHeader).toBe(true);
- expect(xhttp.scMaxBufferedPosts).toBe(50);
- });
- });
- describe('parseVlessLink', () => {
- it('parses a vless:// link with reality', () => {
- const link
- = 'vless://[email protected]:443'
- + '?type=tcp&security=reality&pbk=pubkey&sid=abcd&fp=chrome&sni=cloudflare.com&flow=xtls-rprx-vision'
- + '#imported-vless';
- const out = parseVlessLink(link);
- expect(out?.protocol).toBe('vless');
- expect(out?.tag).toBe('imported-vless');
- const settings = out?.settings as { id: string; flow: string; address: string; port: number };
- expect(settings.id).toBe('11111111-2222-4333-8444-555555555555');
- expect(settings.address).toBe('srv.example');
- expect(settings.port).toBe(443);
- expect(settings.flow).toBe('xtls-rprx-vision');
- const stream = out?.streamSettings as Record<string, unknown>;
- expect(stream.security).toBe('reality');
- const reality = stream.realitySettings as Record<string, unknown>;
- expect(reality.publicKey).toBe('pubkey');
- expect(reality.shortId).toBe('abcd');
- expect(reality.serverName).toBe('cloudflare.com');
- });
- });
- describe('parseTrojanLink', () => {
- it('parses a trojan:// link with ws + tls', () => {
- const link = 'trojan://[email protected]:8443?type=ws&security=tls&host=example.com&path=/tj&sni=example.com#imported-trojan';
- const out = parseTrojanLink(link);
- expect(out?.protocol).toBe('trojan');
- const settings = out?.settings as { servers: Array<{ address: string; port: number; password: string }> };
- expect(settings.servers[0].address).toBe('srv.example');
- expect(settings.servers[0].port).toBe(8443);
- expect(settings.servers[0].password).toBe('secret-pw');
- const stream = out?.streamSettings as Record<string, unknown>;
- expect(stream.network).toBe('ws');
- expect((stream.wsSettings as Record<string, unknown>).path).toBe('/tj');
- });
- });
- describe('parseShadowsocksLink', () => {
- it('parses the modern userinfo@host:port form', () => {
- // ss://base64(method:password)@host:port#remark
- const userinfo = Base64.encode('2022-blake3-aes-128-gcm:supersecret');
- const link = `ss://${userinfo}@1.2.3.4:8388#imported-ss`;
- const out = parseShadowsocksLink(link);
- expect(out?.protocol).toBe('shadowsocks');
- expect(out?.tag).toBe('imported-ss');
- const settings = out?.settings as { servers: Array<{ address: string; port: number; method: string; password: string }> };
- expect(settings.servers[0].address).toBe('1.2.3.4');
- expect(settings.servers[0].port).toBe(8388);
- expect(settings.servers[0].method).toBe('2022-blake3-aes-128-gcm');
- expect(settings.servers[0].password).toBe('supersecret');
- });
- it('parses the legacy base64-of-whole form', () => {
- // ss://base64(method:password@host:port)#remark
- const inner = Base64.encode('aes-256-gcm:[email protected]:1080');
- const link = `ss://${inner}#imported-legacy`;
- const out = parseShadowsocksLink(link);
- const settings = out?.settings as { servers: Array<{ address: string; port: number; method: string; password: string }> };
- expect(settings.servers[0].address).toBe('10.0.0.1');
- expect(settings.servers[0].port).toBe(1080);
- expect(settings.servers[0].method).toBe('aes-256-gcm');
- expect(settings.servers[0].password).toBe('legacypw');
- });
- });
- describe('parseHysteria2Link', () => {
- it('parses a hysteria2:// link with sni', () => {
- const link = 'hysteria2://[email protected]:443?sni=example.com#imported-hy2';
- const out = parseHysteria2Link(link);
- expect(out?.protocol).toBe('hysteria');
- expect(out?.tag).toBe('imported-hy2');
- const settings = out?.settings as { address: string; port: number; version: number };
- expect(settings.address).toBe('srv.example');
- expect(settings.port).toBe(443);
- expect(settings.version).toBe(2);
- const stream = out?.streamSettings as Record<string, unknown>;
- const hys = stream.hysteriaSettings as Record<string, unknown>;
- expect(hys.auth).toBe('auth-secret');
- expect((stream.tlsSettings as Record<string, unknown>).serverName).toBe('example.com');
- });
- it('also accepts hy2:// alias', () => {
- const out = parseHysteria2Link('hy2://auth@srv:443?sni=example.com');
- expect(out?.protocol).toBe('hysteria');
- });
- });
- describe('parseOutboundLink dispatcher', () => {
- it('dispatches vmess via base64 JSON', () => {
- const json = { v: '2', ps: 'x', add: '1.1.1.1', port: 443, id: '11111111-2222-4333-8444-555555555555', net: 'tcp', tls: 'none' };
- const link = `vmess://${Base64.encode(JSON.stringify(json))}`;
- expect(parseOutboundLink(link)?.protocol).toBe('vmess');
- });
- it('dispatches vless via URL', () => {
- expect(parseOutboundLink('vless://uuid@host:443?type=tcp&security=none')?.protocol).toBe('vless');
- });
- it('returns null for an unknown scheme', () => {
- expect(parseOutboundLink('socks5://user:pass@host:1080')).toBeNull();
- });
- it('returns null for empty input', () => {
- expect(parseOutboundLink('')).toBeNull();
- expect(parseOutboundLink(' ')).toBeNull();
- });
- });
|