import { Tag } from 'antd'; import { Base64 } from '@/utils'; /* Shared parsing + rendering for the "protocol / transport / security" labels shown above share links in the QR modal, the client info modal and the subscription page. Keeping it in one place means the colour scheme and the email/stats stripping stay identical across all three. */ export interface LinkParts { protocol: string; network: string; security: string; remark: string; port: string; } const PROTOCOL_LABELS: Record = { vless: 'Vless', vmess: 'Vmess', trojan: 'Trojan', ss: 'Shadowsocks', shadowsocks: 'Shadowsocks', hysteria2: 'Hysteria2', hy2: 'Hysteria2', hysteria: 'Hysteria', wireguard: 'WireGuard', wg: 'WireGuard', tg: 'MTProto', vpn: 'AmneziaWG', tuic: 'TUIC', }; const PROTOCOL_COLORS: Record = { Vless: 'geekblue', Vmess: 'blue', Trojan: 'volcano', Shadowsocks: 'purple', Hysteria: 'magenta', Hysteria2: 'magenta', WireGuard: 'cyan', MTProto: 'blue', AmneziaWG: 'yellow', TUIC: 'orange', }; const SECURITY_COLORS: Record = { TLS: 'green', XTLS: 'green', REALITY: 'purple', FAKETLS: 'green', }; const TRANSPORT_COLOR = 'gold'; const TAG_STYLE = { marginInlineEnd: 0, fontWeight: 600, letterSpacing: '0.3px' }; // Reverse of inbound-link.ts's own toBase64Url โ€” base64url (RFC 4648 ยง5, no // padding) back to the original unicode text, needed to read the remark/ // endpoint back out of a vpn:// link's opaque payload below. function fromBase64Url(value: string): string { const b64 = value.replace(/-/g, '+').replace(/_/g, '/'); const padded = b64 + '='.repeat((4 - (b64.length % 4)) % 4); const binary = atob(padded); const bytes = new Uint8Array(binary.length); for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i); return new TextDecoder().decode(bytes); } /* Pull protocol, transport, security plus the remark and port out of a share link. vless/trojan carry network+security as `type`/`security` query params and the remark in the URL hash; vmess packs them into the base64 JSON as `net`/`tls`/`ps`/`port`. Returns null when the scheme is unknown or the payload can't be parsed, so callers fall back to "Link N". The remark is shown verbatim: the panel displays the subscription's clean (name-only) remarks โ€” the per-client traffic/expiry info is rendered only into the body a client app imports, so there is nothing to strip here. */ export function parseLinkParts(link: string): LinkParts | null { const trimmed = link.trim(); const scheme = /^([a-z0-9]+):\/\//i.exec(trimmed)?.[1]?.toLowerCase() ?? ''; if (!scheme) return null; const protocol = PROTOCOL_LABELS[scheme] ?? scheme.charAt(0).toUpperCase() + scheme.slice(1); let network = ''; let security = ''; let remark = ''; let port = ''; if (scheme === 'vmess') { try { const json = JSON.parse(Base64.decode(trimmed.slice('vmess://'.length).split('#')[0])) as { net?: string; tls?: string; ps?: string; port?: string | number; }; network = json.net ?? ''; security = json.tls ?? ''; remark = typeof json.ps === 'string' ? json.ps : ''; port = json.port != null ? String(json.port) : ''; } catch { /* unparseable payload, fall back to protocol only */ } } else if (scheme === 'vpn') { /* AmneziaWG's vpn:// links are base64url of a plain .conf text (matching the real AmneziaVPN app's own share-link scheme), not a structured URL โ€” there's no query string or #hash to read a remark/port from without corrupting the payload the app itself needs to decode. The remark and endpoint are still in there as plain .conf lines, though, so pull them back out directly. */ try { const cfgText = fromBase64Url(trimmed.slice('vpn://'.length)); remark = /^#\s?(.*)$/m.exec(cfgText)?.[1]?.trim() ?? ''; port = /^Endpoint\s*=\s*.+:(\d+)\s*$/m.exec(cfgText)?.[1] ?? ''; } catch { /* unparseable payload, fall back to protocol only */ } } else { try { const url = new URL(trimmed); network = url.searchParams.get('type') ?? ''; security = url.searchParams.get('security') ?? ''; /* tg://proxy links (mtproto) carry the port in a `port` query param, not the URL authority, so fall back to it when there is no authority port. */ port = url.port || (url.searchParams.get('port') ?? ''); const hash = url.hash.replace(/^#/, ''); try { remark = decodeURIComponent(hash); } catch { remark = hash; } } catch { /* not URL-shaped, fall back to protocol only */ } if (scheme === 'tg') security = 'FakeTLS'; if (scheme === 'tuic') { network = 'quic'; security = 'TLS'; } } if (security === 'none') security = ''; return { protocol, network: network.toUpperCase(), security: security.toUpperCase(), remark: remark.trim(), port, }; } /* The inbound remark and port joined as they appear after the tags, e.g. "22:10452". Either piece may be empty. */ export function linkMetaText(parts: LinkParts): string { return [parts.remark, parts.port].filter(Boolean).join(':'); } export function LinkTags({ parts }: { parts: LinkParts }) { return ( {parts.protocol} {parts.network && ( {parts.network} )} {parts.security && ( {parts.security} )} ); }