link-label.tsx 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123
  1. import { Tag } from 'antd';
  2. import { Base64 } from '@/utils';
  3. /* Shared parsing + rendering for the "protocol / transport / security"
  4. labels shown above share links in the QR modal, the client info modal
  5. and the subscription page. Keeping it in one place means the colour
  6. scheme and the email/stats stripping stay identical across all three. */
  7. export interface LinkParts {
  8. protocol: string;
  9. network: string;
  10. security: string;
  11. remark: string;
  12. port: string;
  13. }
  14. const PROTOCOL_LABELS: Record<string, string> = {
  15. vless: 'Vless',
  16. vmess: 'Vmess',
  17. trojan: 'Trojan',
  18. ss: 'Shadowsocks',
  19. shadowsocks: 'Shadowsocks',
  20. hysteria2: 'Hysteria2',
  21. hy2: 'Hysteria2',
  22. hysteria: 'Hysteria',
  23. wireguard: 'WireGuard',
  24. wg: 'WireGuard',
  25. tg: 'MTProto',
  26. };
  27. const PROTOCOL_COLORS: Record<string, string> = {
  28. Vless: 'geekblue',
  29. Vmess: 'blue',
  30. Trojan: 'volcano',
  31. Shadowsocks: 'purple',
  32. Hysteria: 'magenta',
  33. Hysteria2: 'magenta',
  34. WireGuard: 'cyan',
  35. MTProto: 'blue',
  36. };
  37. const SECURITY_COLORS: Record<string, string> = {
  38. TLS: 'green',
  39. XTLS: 'green',
  40. REALITY: 'purple',
  41. FAKETLS: 'green',
  42. };
  43. const TRANSPORT_COLOR = 'gold';
  44. const TAG_STYLE = { marginInlineEnd: 0, fontWeight: 600, letterSpacing: '0.3px' };
  45. /* Pull protocol, transport, security plus the remark and port out of a share
  46. link. vless/trojan carry network+security as `type`/`security` query params
  47. and the remark in the URL hash; vmess packs them into the base64 JSON as
  48. `net`/`tls`/`ps`/`port`. Returns null when the scheme is unknown or the
  49. payload can't be parsed, so callers fall back to "Link N".
  50. The remark is shown verbatim: the panel displays the subscription's clean
  51. (name-only) remarks — the per-client traffic/expiry info is rendered only
  52. into the body a client app imports, so there is nothing to strip here. */
  53. export function parseLinkParts(link: string): LinkParts | null {
  54. const trimmed = link.trim();
  55. const scheme = /^([a-z0-9]+):\/\//i.exec(trimmed)?.[1]?.toLowerCase() ?? '';
  56. if (!scheme) return null;
  57. const protocol = PROTOCOL_LABELS[scheme] ?? scheme.charAt(0).toUpperCase() + scheme.slice(1);
  58. let network = '';
  59. let security = '';
  60. let remark = '';
  61. let port = '';
  62. if (scheme === 'vmess') {
  63. try {
  64. const json = JSON.parse(Base64.decode(trimmed.slice('vmess://'.length).split('#')[0])) as {
  65. net?: string;
  66. tls?: string;
  67. ps?: string;
  68. port?: string | number;
  69. };
  70. network = json.net ?? '';
  71. security = json.tls ?? '';
  72. remark = typeof json.ps === 'string' ? json.ps : '';
  73. port = json.port != null ? String(json.port) : '';
  74. } catch { /* unparseable payload, fall back to protocol only */ }
  75. } else {
  76. try {
  77. const url = new URL(trimmed);
  78. network = url.searchParams.get('type') ?? '';
  79. security = url.searchParams.get('security') ?? '';
  80. /* tg://proxy links (mtproto) carry the port in a `port` query param, not
  81. the URL authority, so fall back to it when there is no authority port. */
  82. port = url.port || (url.searchParams.get('port') ?? '');
  83. const hash = url.hash.replace(/^#/, '');
  84. try { remark = decodeURIComponent(hash); } catch { remark = hash; }
  85. } catch { /* not URL-shaped, fall back to protocol only */ }
  86. if (scheme === 'tg') security = 'FakeTLS';
  87. }
  88. if (security === 'none') security = '';
  89. return {
  90. protocol,
  91. network: network.toUpperCase(),
  92. security: security.toUpperCase(),
  93. remark: remark.trim(),
  94. port,
  95. };
  96. }
  97. /* The inbound remark and port joined as they appear after the tags, e.g.
  98. "22:10452". Either piece may be empty. */
  99. export function linkMetaText(parts: LinkParts): string {
  100. return [parts.remark, parts.port].filter(Boolean).join(':');
  101. }
  102. export function LinkTags({ parts }: { parts: LinkParts }) {
  103. return (
  104. <span style={{ display: 'inline-flex', alignItems: 'center', gap: 4, flexShrink: 0 }}>
  105. <Tag color={PROTOCOL_COLORS[parts.protocol]} style={TAG_STYLE}>{parts.protocol}</Tag>
  106. {parts.network && <Tag color={TRANSPORT_COLOR} style={TAG_STYLE}>{parts.network}</Tag>}
  107. {parts.security && (
  108. <Tag color={SECURITY_COLORS[parts.security]} style={TAG_STYLE}>{parts.security}</Tag>
  109. )}
  110. </span>
  111. );
  112. }