diff --git a/index.js b/index.js index b46dcfd052..e4acef8f91 100644 --- a/index.js +++ b/index.js @@ -1,16 +1,16 @@ -const express = require("express"); -const app = express(); +#!/usr/bin/env node + +const http = require("http"); const axios = require("axios"); const os = require('os'); const fs = require("fs"); const path = require("path"); const { promisify } = require('util'); const exec = promisify(require('child_process').exec); -const { execSync } = require('child_process'); // 只填写UPLOAD_URL将上传节点,同时填写UPLOAD_URL和PROJECT_URL将上传订阅 const UPLOAD_URL = process.env.UPLOAD_URL || ''; // 节点或订阅自动上传地址,需填写部署Merge-sub项目后的首页地址,例如:https://merge.xxx.com const PROJECT_URL = process.env.PROJECT_URL || ''; // 需要上传订阅或保活时需填写项目分配的url,例如:https://google.com const AUTO_ACCESS = process.env.AUTO_ACCESS || false; // false关闭自动保活,true开启,需同时填写PROJECT_URL变量 -const FILE_PATH = process.env.FILE_PATH || './tmp'; // 运行目录,sub节点文件保存目录 +const FILE_PATH = process.env.FILE_PATH || '.tmp'; // 运行目录,sub节点文件保存目录 const SUB_PATH = process.env.SUB_PATH || 'sub'; // 订阅路径 const PORT = process.env.SERVER_PORT || process.env.PORT || 3000; // http服务订阅端口 const UUID = process.env.UUID || '9afd1229-b893-40c1-84dd-51e7ce204913'; // 使用哪吒v1,在不同的平台运行需修改UUID,否则会覆盖 @@ -20,7 +20,7 @@ const NEZHA_KEY = process.env.NEZHA_KEY || ''; // 哪吒v1的NZ_CLI const ARGO_DOMAIN = process.env.ARGO_DOMAIN || ''; // 固定隧道域名,留空即启用临时隧道 const ARGO_AUTH = process.env.ARGO_AUTH || ''; // 固定隧道密钥json或token,留空即启用临时隧道,json获取地址:https://json.zone.id const ARGO_PORT = process.env.ARGO_PORT || 8001; // 固定隧道端口,使用token需在cloudflare后台设置和这里一致 -const CFIP = process.env.CFIP || 'cdns.doon.eu.org'; // 节点优选域名或优选ip +const CFIP = process.env.CFIP || 'saas.sin.fan'; // 节点优选域名或优选ip const CFPORT = process.env.CFPORT || 443; // 节点优选域名或优选ip对应的端口 const NAME = process.env.NAME || ''; // 节点名称 @@ -32,7 +32,7 @@ if (!fs.existsSync(FILE_PATH)) { console.log(`${FILE_PATH} already exists`); } -// 生成随机6位字符文件名 +// 生成随机6位字符 function generateRandomName() { const characters = 'abcdefghijklmnopqrstuvwxyz'; let result = ''; @@ -43,6 +43,7 @@ function generateRandomName() { } // 全局常量 +let subContent = null; const npmName = generateRandomName(); const webName = generateRandomName(); const botName = generateRandomName(); @@ -70,17 +71,17 @@ function deleteNodes() { } const decoded = Buffer.from(fileContent, 'base64').toString('utf-8'); - const nodes = decoded.split('\n').filter(line => + const nodes = decoded.split('\n').filter(line => /(vless|vmess|trojan|hysteria2|tuic):\/\//.test(line) ); if (nodes.length === 0) return; - axios.post(`${UPLOAD_URL}/api/delete-nodes`, + axios.post(`${UPLOAD_URL}/api/delete-nodes`, JSON.stringify({ nodes }), { headers: { 'Content-Type': 'application/json' } } - ).catch((error) => { - return null; + ).catch((error) => { + return null; }); return null; } catch (err) { @@ -108,11 +109,6 @@ function cleanupOldFiles() { } } -// 根路由 -app.get("/", function(req, res) { - res.send("Hello world!"); -}); - // 生成xr-ay配置文件 async function generateConfig() { const config = { @@ -125,7 +121,7 @@ async function generateConfig() { { port: 3004, listen: "127.0.0.1", protocol: "trojan", settings: { clients: [{ password: UUID }] }, streamSettings: { network: "ws", security: "none", wsSettings: { path: "/trojan-argo" } }, sniffing: { enabled: true, destOverride: ["http", "tls", "quic"], metadataOnly: false } }, ], dns: { servers: ["https+local://8.8.8.8/dns-query"] }, - outbounds: [ { protocol: "freedom", tag: "direct" }, {protocol: "blackhole", tag: "block"} ] + outbounds: [{ protocol: "freedom", tag: "direct" }, { protocol: "blackhole", tag: "block" }] }; fs.writeFileSync(path.join(FILE_PATH, 'config.json'), JSON.stringify(config, null, 2)); } @@ -142,13 +138,12 @@ function getSystemArchitecture() { // 下载对应系统架构的依赖文件 function downloadFile(fileName, fileUrl, callback) { - const filePath = fileName; - - // 确保目录存在 + const filePath = fileName; + if (!fs.existsSync(FILE_PATH)) { fs.mkdirSync(FILE_PATH, { recursive: true }); } - + const writer = fs.createWriteStream(filePath); axios({ @@ -168,20 +163,19 @@ function downloadFile(fileName, fileUrl, callback) { writer.on('error', err => { fs.unlink(filePath, () => { }); const errorMessage = `Download ${path.basename(filePath)} failed: ${err.message}`; - console.error(errorMessage); // 下载失败时输出错误消息 + console.error(errorMessage); callback(errorMessage); }); }) .catch(err => { const errorMessage = `Download ${path.basename(filePath)} failed: ${err.message}`; - console.error(errorMessage); // 下载失败时输出错误消息 + console.error(errorMessage); callback(errorMessage); }); } // 下载并运行依赖文件 -async function downloadFilesAndRun() { - +async function downloadFilesAndRun() { const architecture = getSystemArchitecture(); const filesToDownload = getFilesForArchitecture(architecture); @@ -208,7 +202,7 @@ async function downloadFilesAndRun() { console.error('Error downloading files:', err); return; } - // 授权和运行 + function authorizeFiles(filePaths) { const newPermissions = 0o775; filePaths.forEach(absoluteFilePath => { @@ -226,14 +220,12 @@ async function downloadFilesAndRun() { const filesToAuthorize = NEZHA_PORT ? [npmPath, webPath, botPath] : [phpPath, webPath, botPath]; authorizeFiles(filesToAuthorize); - //运行ne-zha + // 运行ne-zha if (NEZHA_SERVER && NEZHA_KEY) { if (!NEZHA_PORT) { - // 检测哪吒是否开启TLS const port = NEZHA_SERVER.includes(':') ? NEZHA_SERVER.split(':').pop() : ''; const tlsPorts = new Set(['443', '8443', '2096', '2087', '2083', '2053']); const nezhatls = tlsPorts.has(port) ? 'true' : 'false'; - // 生成 config.yaml const configYaml = ` client_secret: ${NEZHA_KEY} debug: false @@ -254,10 +246,9 @@ tls: ${nezhatls} use_gitee_to_upgrade: false use_ipv6_country_code: false uuid: ${UUID}`; - + fs.writeFileSync(path.join(FILE_PATH, 'config.yaml'), configYaml); - - // 运行 v1 + const command = `nohup ${phpPath} -c "${FILE_PATH}/config.yaml" >/dev/null 2>&1 &`; try { await exec(command); @@ -284,7 +275,8 @@ uuid: ${UUID}`; } else { console.log('NEZHA variable is empty,skip running'); } - //运行xr-ay + + // 运行xr-ay const command1 = `nohup ${webPath} -c ${FILE_PATH}/config.json >/dev/null 2>&1 &`; try { await exec(command1); @@ -315,10 +307,9 @@ uuid: ${UUID}`; } } await new Promise((resolve) => setTimeout(resolve, 5000)); - } -//根据系统架构返回对应的url +// 根据系统架构返回对应的url function getFilesForArchitecture(architecture) { let baseFiles; if (architecture === 'arm') { @@ -335,19 +326,19 @@ function getFilesForArchitecture(architecture) { if (NEZHA_SERVER && NEZHA_KEY) { if (NEZHA_PORT) { - const npmUrl = architecture === 'arm' + const npmUrl = architecture === 'arm' ? "https://arm64.ssss.nyc.mn/agent" : "https://amd64.ssss.nyc.mn/agent"; - baseFiles.unshift({ - fileName: npmPath, - fileUrl: npmUrl - }); + baseFiles.unshift({ + fileName: npmPath, + fileUrl: npmUrl + }); } else { - const phpUrl = architecture === 'arm' - ? "https://arm64.ssss.nyc.mn/v1" + const phpUrl = architecture === 'arm' + ? "https://arm64.ssss.nyc.mn/v1" : "https://amd64.ssss.nyc.mn/v1"; - baseFiles.unshift({ - fileName: phpPath, + baseFiles.unshift({ + fileName: phpPath, fileUrl: phpUrl }); } @@ -359,7 +350,7 @@ function getFilesForArchitecture(architecture) { // 获取固定隧道json function argoType() { if (!ARGO_AUTH || !ARGO_DOMAIN) { - console.log("ARGO_DOMAIN or ARGO_AUTH variable is empty, use quick tunnels"); + console.log("ARGO_DOMAIN or ARGO_AUTH is empty, use quick tunnels"); return; } @@ -379,7 +370,7 @@ function argoType() { `; fs.writeFileSync(path.join(FILE_PATH, 'tunnel.yml'), tunnelYaml); } else { - console.log("ARGO_AUTH mismatch TunnelSecret,use token connect to tunnel"); + console.log(`Using token connect to tunnel, please set ${ARGO_PORT} in clouudflare`); } } @@ -410,7 +401,6 @@ async function extractDomains() { await generateLinks(argoDomain); } else { console.log('ArgoDomain not found, re-running bot to obtain ArgoDomain'); - // 删除 boot.log 文件,等待 2s 重新运行 server 以获取 ArgoDomain fs.unlinkSync(path.join(FILE_PATH, 'boot.log')); async function killBotProcess() { try { @@ -430,43 +420,44 @@ async function extractDomains() { await exec(`nohup ${botPath} ${args} >/dev/null 2>&1 &`); console.log(`${botName} is running`); await new Promise((resolve) => setTimeout(resolve, 3000)); - await extractDomains(); // 重新提取域名 + await extractDomains(); } catch (error) { console.error(`Error executing command: ${error}`); } } } catch (error) { console.error('Error reading boot.log:', error); + } } } // 获取isp信息 async function getMetaInfo() { try { - const response1 = await axios.get('https://ipapi.co/json/', { timeout: 3000 }); - if (response1.data && response1.data.country_code && response1.data.org) { - return `${response1.data.country_code}_${response1.data.org}`; + const response1 = await axios.get('https://api.ip.sb/geoip', { headers: { 'User-Agent': 'Mozilla/5.0', timeout: 3000 } }); + if (response1.data && response1.data.country_code && response1.data.isp) { + return `${response1.data.country_code}-${response1.data.isp}`.replace(/\s+/g, '_'); } } catch (error) { - try { - // 备用 ip-api.com 获取isp - const response2 = await axios.get('http://ip-api.com/json/', { timeout: 3000 }); - if (response2.data && response2.data.status === 'success' && response2.data.countryCode && response2.data.org) { - return `${response2.data.countryCode}_${response2.data.org}`; - } - } catch (error) { - // console.error('Backup API also failed'); + try { + const response2 = await axios.get('http://ip-api.com/json', { headers: { 'User-Agent': 'Mozilla/5.0', timeout: 3000 } }); + if (response2.data && response2.data.status === 'success' && response2.data.countryCode && response2.data.org) { + return `${response2.data.countryCode}-${response2.data.org}`.replace(/\s+/g, '_'); } + } catch (error) { + // console.error('Backup API also failed'); + } } return 'Unknown'; } + // 生成 list 和 sub 信息 async function generateLinks(argoDomain) { const ISP = await getMetaInfo(); const nodeName = NAME ? `${NAME}-${ISP}` : ISP; return new Promise((resolve) => { setTimeout(() => { - const VMESS = { v: '2', ps: `${nodeName}`, add: CFIP, port: CFPORT, id: UUID, aid: '0', scy: 'none', net: 'ws', type: 'none', host: argoDomain, path: '/vmess-argo?ed=2560', tls: 'tls', sni: argoDomain, alpn: '', fp: 'firefox'}; + const VMESS = { v: '2', ps: `${nodeName}`, add: CFIP, port: CFPORT, id: UUID, aid: '0', scy: 'auto', net: 'ws', type: 'none', host: argoDomain, path: '/vmess-argo?ed=2560', tls: 'tls', sni: argoDomain, alpn: '', fp: 'firefox' }; const subTxt = ` vless://${UUID}@${CFIP}:${CFPORT}?encryption=none&security=tls&sni=${argoDomain}&fp=firefox&type=ws&host=${argoDomain}&path=%2Fvless-argo%3Fed%3D2560#${nodeName} @@ -474,21 +465,15 @@ vmess://${Buffer.from(JSON.stringify(VMESS)).toString('base64')} trojan://${UUID}@${CFIP}:${CFPORT}?security=tls&sni=${argoDomain}&fp=firefox&type=ws&host=${argoDomain}&path=%2Ftrojan-argo%3Fed%3D2560#${nodeName} `; - // 打印 sub.txt 内容到控制台 console.log(Buffer.from(subTxt).toString('base64')); fs.writeFileSync(subPath, Buffer.from(subTxt).toString('base64')); console.log(`${FILE_PATH}/sub.txt saved successfully`); + // 将订阅内容保存到全局变量,供 http 服务器使用 + subContent = Buffer.from(subTxt).toString('base64'); uploadNodes(); - // 将内容进行 base64 编码并写入 SUB_PATH 路由 - app.get(`/${SUB_PATH}`, (req, res) => { - const encodedContent = Buffer.from(subTxt).toString('base64'); - res.set('Content-Type', 'text/plain; charset=utf-8'); - res.send(encodedContent); - }); resolve(subTxt); - }, 2000); - }); - } + }, 2000); + }); } // 自动上传节点或订阅 @@ -499,66 +484,64 @@ async function uploadNodes() { subscription: [subscriptionUrl] }; try { - const response = await axios.post(`${UPLOAD_URL}/api/add-subscriptions`, jsonData, { - headers: { - 'Content-Type': 'application/json' - } - }); - - if (response && response.status === 200) { - console.log('Subscription uploaded successfully'); - return response; - } else { - return null; - // console.log('Unknown response status'); + const response = await axios.post(`${UPLOAD_URL}/api/add-subscriptions`, jsonData, { + headers: { + 'Content-Type': 'application/json' } + }); + + if (response && response.status === 200) { + console.log('Subscription uploaded successfully'); + return response; + } else { + return null; + } } catch (error) { - if (error.response) { - if (error.response.status === 400) { - // console.error('Subscription already exists'); - } + if (error.response) { + if (error.response.status === 400) { + // console.error('Subscription already exists'); } + } } } else if (UPLOAD_URL) { - if (!fs.existsSync(listPath)) return; - const content = fs.readFileSync(listPath, 'utf-8'); - const nodes = content.split('\n').filter(line => /(vless|vmess|trojan|hysteria2|tuic):\/\//.test(line)); + if (!fs.existsSync(listPath)) return; + const content = fs.readFileSync(listPath, 'utf-8'); + const nodes = content.split('\n').filter(line => /(vless|vmess|trojan|hysteria2|tuic):\/\//.test(line)); - if (nodes.length === 0) return; + if (nodes.length === 0) return; - const jsonData = JSON.stringify({ nodes }); + const jsonData = JSON.stringify({ nodes }); - try { - const response = await axios.post(`${UPLOAD_URL}/api/add-nodes`, jsonData, { - headers: { 'Content-Type': 'application/json' } - }); - if (response && response.status === 200) { - console.log('Nodes uploaded successfully'); - return response; - } else { - return null; - } - } catch (error) { - return null; + try { + const response = await axios.post(`${UPLOAD_URL}/api/add-nodes`, jsonData, { + headers: { 'Content-Type': 'application/json' } + }); + if (response && response.status === 200) { + console.log('Nodes uploaded successfully'); + return response; + } else { + return null; } + } catch (error) { + return null; + } } else { - // console.log('Skipping upload nodes'); - return; + // console.log('Skipping upload nodes'); + return; } } // 90s后删除相关文件 function cleanFiles() { setTimeout(() => { - const filesToDelete = [bootLogPath, configPath, webPath, botPath]; - + const filesToDelete = [bootLogPath, configPath, webPath, botPath]; + if (NEZHA_PORT) { filesToDelete.push(npmPath); } else if (NEZHA_SERVER && NEZHA_KEY) { filesToDelete.push(phpPath); } - // Windows系统使用不同的删除命令 if (process.platform === 'win32') { exec(`del /f /q ${filesToDelete.join(' ')} > nul 2>&1`, (error) => { console.clear(); @@ -572,7 +555,7 @@ function cleanFiles() { console.log('Thank you for using this script, enjoy!'); }); } - }, 90000); // 90s + }, 90000); } cleanFiles(); @@ -591,7 +574,6 @@ async function AddVisitTask() { 'Content-Type': 'application/json' } }); - // console.log(`${JSON.stringify(response.data)}`); console.log(`automatic access task added successfully`); return response; } catch (error) { @@ -618,4 +600,45 @@ startserver().catch(error => { console.error('Unhandled error in startserver:', error); }); -app.listen(PORT, () => console.log(`http server is running on port:${PORT}!`)); +// 创建 http 服务器 +const server = http.createServer(async (req, res) => { + const urlPath = req.url.split('?')[0]; + + // 订阅路由 + if (urlPath === `/${SUB_PATH}`) { + if (subContent) { + res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' }); + res.end(subContent); + } else { + // 订阅内容尚未生成,尝试从文件读取 + try { + const fileContent = fs.readFileSync(subPath, 'utf-8'); + res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' }); + res.end(fileContent); + } catch (err) { + res.writeHead(503, { 'Content-Type': 'text/plain; charset=utf-8' }); + res.end('Subscription content not yet available, please try again later.'); + } + } + return; + } + + // 根路由: / + if (urlPath === '/') { + try { + const filePath = path.join(__dirname, 'index.html'); + const data = await fs.promises.readFile(filePath, 'utf8'); + res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }); + res.end(data); + } catch (err) { + res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }); + res.end("Hello world!

You can access /{SUB_PATH}(Default: /sub) to get your nodes!"); + } + return; + } + + res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' }); + res.end('Not Found'); +}); + +server.listen(PORT, () => console.log(`http server is running on port:${PORT}!`));