feat: init proj
This commit is contained in:
210
index.js
Normal file
210
index.js
Normal file
@@ -0,0 +1,210 @@
|
||||
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).');
|
||||
});
|
||||
Reference in New Issue
Block a user