import { parseSS, type ParsedNode } from './ss.js'; import { parseVMess } from './vmess.js'; import { parseTrojan } from './trojan.js'; import { parseVless } from './vless.js'; import { parseSocks5, parseSocks5Tls } from './socks.js'; import { parseHysteria2 } from './hysteria2.js'; import { parseTuic } from './tuic.js'; import { parseAnytls } from './anytls.js'; export type { ParsedNode }; export function parseNodeUri(uri: string): ParsedNode | null { try { if (uri.startsWith('ss://')) return parseSS(uri); if (uri.startsWith('vmess://')) return parseVMess(uri); if (uri.startsWith('trojan://')) return parseTrojan(uri); if (uri.startsWith('vless://')) return parseVless(uri); if (uri.startsWith('hysteria2://') || uri.startsWith('hy2://')) return parseHysteria2(uri); if (uri.startsWith('tuic://')) return parseTuic(uri); if (uri.startsWith('anytls://')) return parseAnytls(uri); // socks5-over-tls variants must be checked before plain socks if (uri.startsWith('socks5-tls://') || uri.startsWith('socks5+tls://') || uri.startsWith('socks+tls://')) return parseSocks5Tls(uri); if (uri.startsWith('socks5://') || uri.startsWith('socks://')) return parseSocks5(uri); return null; } catch { return null; } } export function parseSubscriptionContent(content: string): ParsedNode[] { // Try base64 decode first (common subscription format) let text = content; try { const decoded = Buffer.from(content.trim(), 'base64').toString(); if (decoded.includes('://')) { text = decoded; } } catch { // Not base64, use as-is } // If it's a Surge config (contains sections), extract proxy lines if (text.includes('[Proxy]') || text.includes('[General]')) { return parseSurgeConfig(text); } // Otherwise treat as URI list const lines = text.split('\n').map(l => l.trim()).filter(l => l && !l.startsWith('#')); const nodes: ParsedNode[] = []; for (const line of lines) { const node = parseNodeUri(line); if (node) nodes.push(node); } return nodes; } function parseSurgeConfig(config: string): ParsedNode[] { const lines = config.split('\n'); const nodes: ParsedNode[] = []; let inProxySection = false; for (const line of lines) { const trimmed = line.trim(); if (trimmed.startsWith('[') && trimmed.endsWith(']')) { inProxySection = trimmed === '[Proxy]'; continue; } if (!inProxySection) continue; if (!trimmed || trimmed.startsWith('#') || trimmed.startsWith('//')) continue; // Parse "Name = type, server, port, ..." const eqIdx = trimmed.indexOf('='); if (eqIdx === -1) continue; const name = trimmed.slice(0, eqIdx).trim(); const rest = trimmed.slice(eqIdx + 1).trim(); const parts = rest.split(',').map(p => p.trim()); const type = parts[0] || ''; const server = parts[1] || ''; const port = parseInt(parts[2] || '0', 10); if (name && type && server) { nodes.push({ name, type, server, port, surgeLine: trimmed, }); } } return nodes; }