outbound-link-parser.test.ts 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159
  1. import { describe, expect, it } from 'vitest';
  2. import {
  3. parseOutboundLink,
  4. parseShadowsocksLink,
  5. parseTrojanLink,
  6. parseVlessLink,
  7. parseVmessLink,
  8. parseHysteria2Link,
  9. } from '@/lib/xray/outbound-link-parser';
  10. import { Base64 } from '@/utils';
  11. // Focused acceptance tests for the share-link parsers — one happy-path
  12. // case per protocol family, plus a few common edge cases. The parsers
  13. // produce wire-shape outbound rows; the modal hands them to
  14. // rawOutboundToFormValues to seed Form.useForm.
  15. describe('parseVmessLink', () => {
  16. it('parses a vmess:// link with ws + tls', () => {
  17. const json = {
  18. v: '2', ps: 'imported-vmess', add: '1.2.3.4', port: 8443,
  19. id: '11111111-2222-4333-8444-555555555555', aid: 0, scy: 'auto',
  20. net: 'ws', host: 'example.com', path: '/ws',
  21. tls: 'tls', sni: 'example.com', fp: 'chrome', alpn: 'h2,http/1.1',
  22. };
  23. const link = `vmess://${Base64.encode(JSON.stringify(json))}`;
  24. const out = parseVmessLink(link);
  25. expect(out).not.toBeNull();
  26. expect(out?.protocol).toBe('vmess');
  27. expect(out?.tag).toBe('imported-vmess');
  28. const settings = out?.settings as { vnext: Array<{ address: string; port: number; users: Array<{ id: string; security: string }> }> };
  29. expect(settings.vnext[0].address).toBe('1.2.3.4');
  30. expect(settings.vnext[0].port).toBe(8443);
  31. expect(settings.vnext[0].users[0].id).toBe('11111111-2222-4333-8444-555555555555');
  32. const stream = out?.streamSettings as Record<string, unknown>;
  33. expect(stream.network).toBe('ws');
  34. expect(stream.security).toBe('tls');
  35. expect((stream.wsSettings as Record<string, unknown>).path).toBe('/ws');
  36. expect((stream.tlsSettings as Record<string, unknown>).serverName).toBe('example.com');
  37. expect((stream.tlsSettings as Record<string, unknown>).alpn).toEqual(['h2', 'http/1.1']);
  38. });
  39. it('returns null for non-vmess links', () => {
  40. expect(parseVmessLink('vless://x@y:1')).toBeNull();
  41. });
  42. it('returns null for malformed base64', () => {
  43. expect(parseVmessLink('vmess://!!!not-base64!!!')).toBeNull();
  44. });
  45. });
  46. describe('parseVlessLink', () => {
  47. it('parses a vless:// link with reality', () => {
  48. const link
  49. = 'vless://[email protected]:443'
  50. + '?type=tcp&security=reality&pbk=pubkey&sid=abcd&fp=chrome&sni=cloudflare.com&flow=xtls-rprx-vision'
  51. + '#imported-vless';
  52. const out = parseVlessLink(link);
  53. expect(out?.protocol).toBe('vless');
  54. expect(out?.tag).toBe('imported-vless');
  55. const settings = out?.settings as { id: string; flow: string; address: string; port: number };
  56. expect(settings.id).toBe('11111111-2222-4333-8444-555555555555');
  57. expect(settings.address).toBe('srv.example');
  58. expect(settings.port).toBe(443);
  59. expect(settings.flow).toBe('xtls-rprx-vision');
  60. const stream = out?.streamSettings as Record<string, unknown>;
  61. expect(stream.security).toBe('reality');
  62. const reality = stream.realitySettings as Record<string, unknown>;
  63. expect(reality.publicKey).toBe('pubkey');
  64. expect(reality.shortId).toBe('abcd');
  65. expect(reality.serverName).toBe('cloudflare.com');
  66. });
  67. });
  68. describe('parseTrojanLink', () => {
  69. it('parses a trojan:// link with ws + tls', () => {
  70. const link = 'trojan://[email protected]:8443?type=ws&security=tls&host=example.com&path=/tj&sni=example.com#imported-trojan';
  71. const out = parseTrojanLink(link);
  72. expect(out?.protocol).toBe('trojan');
  73. const settings = out?.settings as { servers: Array<{ address: string; port: number; password: string }> };
  74. expect(settings.servers[0].address).toBe('srv.example');
  75. expect(settings.servers[0].port).toBe(8443);
  76. expect(settings.servers[0].password).toBe('secret-pw');
  77. const stream = out?.streamSettings as Record<string, unknown>;
  78. expect(stream.network).toBe('ws');
  79. expect((stream.wsSettings as Record<string, unknown>).path).toBe('/tj');
  80. });
  81. });
  82. describe('parseShadowsocksLink', () => {
  83. it('parses the modern userinfo@host:port form', () => {
  84. // ss://base64(method:password)@host:port#remark
  85. const userinfo = Base64.encode('2022-blake3-aes-128-gcm:supersecret');
  86. const link = `ss://${userinfo}@1.2.3.4:8388#imported-ss`;
  87. const out = parseShadowsocksLink(link);
  88. expect(out?.protocol).toBe('shadowsocks');
  89. expect(out?.tag).toBe('imported-ss');
  90. const settings = out?.settings as { servers: Array<{ address: string; port: number; method: string; password: string }> };
  91. expect(settings.servers[0].address).toBe('1.2.3.4');
  92. expect(settings.servers[0].port).toBe(8388);
  93. expect(settings.servers[0].method).toBe('2022-blake3-aes-128-gcm');
  94. expect(settings.servers[0].password).toBe('supersecret');
  95. });
  96. it('parses the legacy base64-of-whole form', () => {
  97. // ss://base64(method:password@host:port)#remark
  98. const inner = Base64.encode('aes-256-gcm:[email protected]:1080');
  99. const link = `ss://${inner}#imported-legacy`;
  100. const out = parseShadowsocksLink(link);
  101. const settings = out?.settings as { servers: Array<{ address: string; port: number; method: string; password: string }> };
  102. expect(settings.servers[0].address).toBe('10.0.0.1');
  103. expect(settings.servers[0].port).toBe(1080);
  104. expect(settings.servers[0].method).toBe('aes-256-gcm');
  105. expect(settings.servers[0].password).toBe('legacypw');
  106. });
  107. });
  108. describe('parseHysteria2Link', () => {
  109. it('parses a hysteria2:// link with sni', () => {
  110. const link = 'hysteria2://[email protected]:443?sni=example.com#imported-hy2';
  111. const out = parseHysteria2Link(link);
  112. expect(out?.protocol).toBe('hysteria');
  113. expect(out?.tag).toBe('imported-hy2');
  114. const settings = out?.settings as { address: string; port: number; version: number };
  115. expect(settings.address).toBe('srv.example');
  116. expect(settings.port).toBe(443);
  117. expect(settings.version).toBe(2);
  118. const stream = out?.streamSettings as Record<string, unknown>;
  119. const hys = stream.hysteriaSettings as Record<string, unknown>;
  120. expect(hys.auth).toBe('auth-secret');
  121. expect((stream.tlsSettings as Record<string, unknown>).serverName).toBe('example.com');
  122. });
  123. it('also accepts hy2:// alias', () => {
  124. const out = parseHysteria2Link('hy2://auth@srv:443?sni=example.com');
  125. expect(out?.protocol).toBe('hysteria');
  126. });
  127. });
  128. describe('parseOutboundLink dispatcher', () => {
  129. it('dispatches vmess via base64 JSON', () => {
  130. const json = { v: '2', ps: 'x', add: '1.1.1.1', port: 443, id: '11111111-2222-4333-8444-555555555555', net: 'tcp', tls: 'none' };
  131. const link = `vmess://${Base64.encode(JSON.stringify(json))}`;
  132. expect(parseOutboundLink(link)?.protocol).toBe('vmess');
  133. });
  134. it('dispatches vless via URL', () => {
  135. expect(parseOutboundLink('vless://uuid@host:443?type=tcp&security=none')?.protocol).toBe('vless');
  136. });
  137. it('returns null for an unknown scheme', () => {
  138. expect(parseOutboundLink('socks5://user:pass@host:1080')).toBeNull();
  139. });
  140. it('returns null for empty input', () => {
  141. expect(parseOutboundLink('')).toBeNull();
  142. expect(parseOutboundLink(' ')).toBeNull();
  143. });
  144. });