新增 anytls、tuic(tuic-v5)、hysteria2、socks5/socks5-tls 解析器, 修复 vmess WebSocket path 处理,并优化 Docker 构建配置。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
96 lines
3.0 KiB
TypeScript
96 lines
3.0 KiB
TypeScript
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;
|
|
}
|