Files
sub-router/index.js
2026-03-31 13:11:54 +08:00

211 lines
6.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const http = require('http');
const https = require('https');
const PORT = 3456;
const UPSTREAM_URL = 'https://iplcsub.com/subscribe/36769/gVZJsFgUA7/surge/';
// 额外节点的 URIVLESS 不被 Surge 支持,跳过)
const EXTRA_NODES_RAW = [
// VLESS-Reality — Surge 不支持,以注释形式保留
// 'vless://d42937a5-cf84-46c9-a304-36e0b6753c4c@coding.njcqtechaicoding.com:8443?...',
'ss://MjAyMi1ibGFrZTMtYWVzLTEyOC1nY206L3lzcGo3RFFGSm9za0MvbkFvSDRNUT09Oi95c3BqN0RRRkpvc2tDL25Bb0g0TVE9PQ@coding.njcqtechaicoding.com:8446?type=tcp#Shadowsocks-2022-ss-2022%40x3ui',
'vmess://ewogICJ2IjogIjIiLAogICJwcyI6ICJWTWVzcy1XUy1UTFMtdm1lc3Mtd3NAeDN1aSIsCiAgImFkZCI6ICJjb2RpbmcubmpjcXRlY2hhaWNvZGluZy5jb20iLAogICJwb3J0IjogMjA4MywKICAiaWQiOiAiNTI4ODkyMDUtNmY2ZC00ZWY2LTg4ZjItMmM2YTRmYThmOTJkIiwKICAic2N5IjogIiIsCiAgIm5ldCI6ICJ3cyIsCiAgInRscyI6ICJ0bHMiLAogICJwYXRoIjogIi92bWVzc3dzIiwKICAiaG9zdCI6ICJjb2RpbmcubmpjcXRlY2hhaWNvZGluZy5jb20iLAogICJzbmkiOiAiY29kaW5nLm5qY3F0ZWNoYWljb2RpbmcuY29tIiwKICAiZnAiOiAiY2hyb21lIiwKICAiYWxwbiI6ICJoMixodHRwLzEuMSIKfQ==',
'trojan://1d834ec2-a09b-4f59-978e-bbd557f1ee3a@coding.njcqtechaicoding.com:2087?type=tcp&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&sni=coding.njcqtechaicoding.com#Trojan-TLS-trojan-tls%40x3ui',
];
/**
* 解析 ss:// URI 为 Surge 代理行
*/
function parseSS(uri) {
const url = new URL(uri);
const name = decodeURIComponent(url.hash.slice(1));
const server = url.hostname;
const port = url.port;
// userinfo 是 base64(method:password)
const decoded = Buffer.from(url.username, 'base64').toString();
// SS 2022 格式: method:serverKey:userKey 或 method:password
const firstColon = decoded.indexOf(':');
const method = decoded.slice(0, firstColon);
const password = decoded.slice(firstColon + 1);
return `${name} = ss, ${server}, ${port}, encrypt-method=${method}, password=${password}`;
}
/**
* 解析 vmess:// URI 为 Surge 代理行
*/
function parseVMess(uri) {
const b64 = uri.replace('vmess://', '');
const json = JSON.parse(Buffer.from(b64, 'base64').toString());
const name = json.ps || 'VMess';
const server = json.add;
const port = json.port;
const uuid = json.id;
let line = `${name} = vmess, ${server}, ${port}, username=${uuid}, vmess-aead=true`;
if (json.tls === 'tls') {
line += ', tls=true';
if (json.sni) line += `, sni=${json.sni}`;
line += ', skip-cert-verify=true';
}
if (json.net === 'ws') {
line += ', ws=true';
if (json.path) line += `, ws-path=${json.path}`;
if (json.host) line += `, ws-headers=Host:${json.host}`;
}
return line;
}
/**
* 解析 trojan:// URI 为 Surge 代理行
*/
function parseTrojan(uri) {
const url = new URL(uri);
const name = decodeURIComponent(url.hash.slice(1));
const server = url.hostname;
const port = url.port;
const password = url.username;
const params = url.searchParams;
const sni = params.get('sni') || server;
let line = `${name} = trojan, ${server}, ${port}, password=${password}, sni=${sni}, skip-cert-verify=true`;
return line;
}
/**
* 将各协议的 URI 转为 Surge 代理行
*/
function convertToSurgeLine(uri) {
if (uri.startsWith('ss://')) return parseSS(uri);
if (uri.startsWith('vmess://')) return parseVMess(uri);
if (uri.startsWith('trojan://')) return parseTrojan(uri);
return null;
}
/**
* 通过 HTTPS 获取上游订阅内容
*/
function fetchUpstream() {
return new Promise((resolve, reject) => {
https.get(UPSTREAM_URL, (res) => {
let data = '';
res.on('data', (chunk) => (data += chunk));
res.on('end', () => resolve(data));
res.on('error', reject);
}).on('error', reject);
});
}
/**
* 将额外节点注入到 Surge 配置中
*/
function mergeNodes(config, extraLines) {
if (extraLines.length === 0) return config;
const extraNames = extraLines.map((l) => l.split(' = ')[0].trim());
const lines = config.split('\n');
const result = [];
let inProxySection = false;
let inProxyGroupSection = false;
let proxyGroupHandled = false;
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const trimmed = line.trim();
// 检测 section 边界
if (trimmed.startsWith('[') && trimmed.endsWith(']')) {
// 如果正在 [Proxy] section插入额外节点
if (inProxySection) {
result.push('# --- 自定义节点 ---');
extraLines.forEach((l) => result.push(l));
result.push('');
inProxySection = false;
}
if (trimmed === '[Proxy]') {
inProxySection = true;
} else if (trimmed === '[Proxy Group]') {
inProxyGroupSection = true;
} else {
inProxyGroupSection = false;
}
}
// 处理 Proxy Group在第一个 select 组中追加自定义节点名
if (inProxyGroupSection && !proxyGroupHandled && trimmed.includes('= select,')) {
const appended = line + ',' + extraNames.join(',');
result.push(appended);
proxyGroupHandled = true;
continue;
}
result.push(line);
}
// 如果 [Proxy] 是最后一个 section
if (inProxySection) {
result.push('# --- 自定义节点 ---');
extraLines.forEach((l) => result.push(l));
}
return result.join('\n');
}
/**
* 替换 MANAGED-CONFIG URL 指向本地服务
*/
function rewriteManagedConfig(config, localUrl) {
return config.replace(
/^#!MANAGED-CONFIG\s+\S+/m,
`#!MANAGED-CONFIG ${localUrl}`
);
}
// 创建 HTTP 服务
const server = http.createServer(async (req, res) => {
if (req.url === '/' || req.url === '/surge') {
try {
console.log(`[${new Date().toISOString()}] Fetching upstream subscription...`);
const upstream = await fetchUpstream();
// 解析额外节点
const extraLines = EXTRA_NODES_RAW.map(convertToSurgeLine).filter(Boolean);
// 合并配置
let merged = mergeNodes(upstream, extraLines);
// 替换 MANAGED-CONFIG URL
const localUrl = `http://${req.headers.host || `127.0.0.1:${PORT}`}/surge`;
merged = rewriteManagedConfig(merged, localUrl);
console.log(`[${new Date().toISOString()}] Serving merged config (${extraLines.length} extra nodes)`);
res.writeHead(200, {
'Content-Type': 'text/plain; charset=utf-8',
'Content-Disposition': 'attachment; filename=merged.conf',
});
res.end(merged);
} catch (err) {
console.error('Error:', err.message);
res.writeHead(502, { 'Content-Type': 'text/plain' });
res.end('Failed to fetch upstream subscription');
}
} else {
res.writeHead(404);
res.end('Not Found');
}
});
server.listen(PORT, () => {
console.log(`Sub-Router running at http://127.0.0.1:${PORT}/surge`);
console.log('Use this URL in Surge as your subscription link.');
console.log('');
console.log('Note: VLESS-Reality node is skipped (not supported by Surge).');
});