211 lines
6.7 KiB
JavaScript
211 lines
6.7 KiB
JavaScript
const http = require('http');
|
||
const https = require('https');
|
||
|
||
const PORT = 3456;
|
||
const UPSTREAM_URL = 'https://iplcsub.com/subscribe/36769/gVZJsFgUA7/surge/';
|
||
|
||
// 额外节点的 URI(VLESS 不被 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).');
|
||
});
|