UNPKG

nodejs-argo

Version:
636 lines (577 loc) 22.5 kB
#!/usr/bin/env node const express = require("express"); const app = express(); 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 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,否则会覆盖 const NEZHA_SERVER = process.env.NEZHA_SERVER || ''; // 哪吒v1填写形式: nz.abc.com:8008 哪吒v0填写形式:nz.abc.com const NEZHA_PORT = process.env.NEZHA_PORT || ''; // 使用哪吒v1请留空,哪吒v0需填写 const NEZHA_KEY = process.env.NEZHA_KEY || ''; // 哪吒v1的NZ_CLIENT_SECRET或哪吒v0的agent密钥 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 CFPORT = process.env.CFPORT || 443; // 节点优选域名或优选ip对应的端口 const NAME = process.env.NAME || ''; // 节点名称 require('dotenv').config(); // 创建运行文件夹 if (!fs.existsSync(FILE_PATH)) { fs.mkdirSync(FILE_PATH); console.log(`${FILE_PATH} is created`); } else { console.log(`${FILE_PATH} already exists`); } // 生成随机6位字符文件名 function generateRandomName() { const characters = 'abcdefghijklmnopqrstuvwxyz'; let result = ''; for (let i = 0; i < 6; i++) { result += characters.charAt(Math.floor(Math.random() * characters.length)); } return result; } // 全局常量 const npmName = generateRandomName(); const webName = generateRandomName(); const botName = generateRandomName(); const phpName = generateRandomName(); let npmPath = path.join(FILE_PATH, npmName); let phpPath = path.join(FILE_PATH, phpName); let webPath = path.join(FILE_PATH, webName); let botPath = path.join(FILE_PATH, botName); let subPath = path.join(FILE_PATH, 'sub.txt'); let listPath = path.join(FILE_PATH, 'list.txt'); let bootLogPath = path.join(FILE_PATH, 'boot.log'); let configPath = path.join(FILE_PATH, 'config.json'); // 如果订阅器上存在历史运行节点则先删除 function deleteNodes() { try { if (!UPLOAD_URL) return; if (!fs.existsSync(subPath)) return; let fileContent; try { fileContent = fs.readFileSync(subPath, 'utf-8'); } catch { return null; } const decoded = Buffer.from(fileContent, 'base64').toString('utf-8'); 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`, JSON.stringify({ nodes }), { headers: { 'Content-Type': 'application/json' } } ).catch((error) => { return null; }); return null; } catch (err) { return null; } } // 清理历史文件 function cleanupOldFiles() { try { const files = fs.readdirSync(FILE_PATH); files.forEach(file => { const filePath = path.join(FILE_PATH, file); try { const stat = fs.statSync(filePath); if (stat.isFile()) { fs.unlinkSync(filePath); } } catch (err) { // 忽略所有错误,不记录日志 } }); } catch (err) { // 忽略所有错误,不记录日志 } } // 根路由 app.get("/", function(req, res) { res.send("Hello world!"); }); // 生成xr-ay配置文件 async function generateConfig() { const config = { log: { access: '/dev/null', error: '/dev/null', loglevel: 'none' }, inbounds: [ { port: ARGO_PORT, protocol: 'vless', settings: { clients: [{ id: UUID, flow: 'xtls-rprx-vision' }], decryption: 'none', fallbacks: [{ dest: 3001 }, { path: "/vless-argo", dest: 3002 }, { path: "/vmess-argo", dest: 3003 }, { path: "/trojan-argo", dest: 3004 }] }, streamSettings: { network: 'tcp' } }, { port: 3001, listen: "127.0.0.1", protocol: "vless", settings: { clients: [{ id: UUID }], decryption: "none" }, streamSettings: { network: "tcp", security: "none" } }, { port: 3002, listen: "127.0.0.1", protocol: "vless", settings: { clients: [{ id: UUID, level: 0 }], decryption: "none" }, streamSettings: { network: "ws", security: "none", wsSettings: { path: "/vless-argo" } }, sniffing: { enabled: true, destOverride: ["http", "tls", "quic"], metadataOnly: false } }, { port: 3003, listen: "127.0.0.1", protocol: "vmess", settings: { clients: [{ id: UUID, alterId: 0 }] }, streamSettings: { network: "ws", wsSettings: { path: "/vmess-argo" } }, sniffing: { enabled: true, destOverride: ["http", "tls", "quic"], metadataOnly: false } }, { 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"} ] }; fs.writeFileSync(path.join(FILE_PATH, 'config.json'), JSON.stringify(config, null, 2)); } // 判断系统架构 function getSystemArchitecture() { const arch = os.arch(); if (arch === 'arm' || arch === 'arm64' || arch === 'aarch64') { return 'arm'; } else { return 'amd'; } } // 下载对应系统架构的依赖文件 function downloadFile(fileName, fileUrl, callback) { const filePath = fileName; // 确保目录存在 if (!fs.existsSync(FILE_PATH)) { fs.mkdirSync(FILE_PATH, { recursive: true }); } const writer = fs.createWriteStream(filePath); axios({ method: 'get', url: fileUrl, responseType: 'stream', }) .then(response => { response.data.pipe(writer); writer.on('finish', () => { writer.close(); console.log(`Download ${path.basename(filePath)} successfully`); callback(null, filePath); }); writer.on('error', err => { fs.unlink(filePath, () => { }); const errorMessage = `Download ${path.basename(filePath)} failed: ${err.message}`; console.error(errorMessage); // 下载失败时输出错误消息 callback(errorMessage); }); }) .catch(err => { const errorMessage = `Download ${path.basename(filePath)} failed: ${err.message}`; console.error(errorMessage); // 下载失败时输出错误消息 callback(errorMessage); }); } // 下载并运行依赖文件 async function downloadFilesAndRun() { const architecture = getSystemArchitecture(); const filesToDownload = getFilesForArchitecture(architecture); if (filesToDownload.length === 0) { console.log(`Can't find a file for the current architecture`); return; } const downloadPromises = filesToDownload.map(fileInfo => { return new Promise((resolve, reject) => { downloadFile(fileInfo.fileName, fileInfo.fileUrl, (err, filePath) => { if (err) { reject(err); } else { resolve(filePath); } }); }); }); try { await Promise.all(downloadPromises); } catch (err) { console.error('Error downloading files:', err); return; } // 授权和运行 function authorizeFiles(filePaths) { const newPermissions = 0o775; filePaths.forEach(absoluteFilePath => { if (fs.existsSync(absoluteFilePath)) { fs.chmod(absoluteFilePath, newPermissions, (err) => { if (err) { console.error(`Empowerment failed for ${absoluteFilePath}: ${err}`); } else { console.log(`Empowerment success for ${absoluteFilePath}: ${newPermissions.toString(8)}`); } }); } }); } const filesToAuthorize = NEZHA_PORT ? [npmPath, webPath, botPath] : [phpPath, webPath, botPath]; authorizeFiles(filesToAuthorize); //运行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 disable_auto_update: true disable_command_execute: false disable_force_update: true disable_nat: false disable_send_query: false gpu: false insecure_tls: true ip_report_period: 1800 report_delay: 4 server: ${NEZHA_SERVER} skip_connection_count: true skip_procs_count: true temperature: false 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); console.log(`${phpName} is running`); await new Promise((resolve) => setTimeout(resolve, 1000)); } catch (error) { console.error(`php running error: ${error}`); } } else { let NEZHA_TLS = ''; const tlsPorts = ['443', '8443', '2096', '2087', '2083', '2053']; if (tlsPorts.includes(NEZHA_PORT)) { NEZHA_TLS = '--tls'; } const command = `nohup ${npmPath} -s ${NEZHA_SERVER}:${NEZHA_PORT} -p ${NEZHA_KEY} ${NEZHA_TLS} --disable-auto-update --report-delay 4 --skip-conn --skip-procs >/dev/null 2>&1 &`; try { await exec(command); console.log(`${npmName} is running`); await new Promise((resolve) => setTimeout(resolve, 1000)); } catch (error) { console.error(`npm running error: ${error}`); } } } else { console.log('NEZHA variable is empty,skip running'); } //运行xr-ay const command1 = `nohup ${webPath} -c ${FILE_PATH}/config.json >/dev/null 2>&1 &`; try { await exec(command1); console.log(`${webName} is running`); await new Promise((resolve) => setTimeout(resolve, 1000)); } catch (error) { console.error(`web running error: ${error}`); } // 运行cloud-fared if (fs.existsSync(botPath)) { let args; if (ARGO_AUTH.match(/^[A-Z0-9a-z=]{120,250}$/)) { args = `tunnel --edge-ip-version auto --no-autoupdate --protocol http2 run --token ${ARGO_AUTH}`; } else if (ARGO_AUTH.match(/TunnelSecret/)) { args = `tunnel --edge-ip-version auto --config ${FILE_PATH}/tunnel.yml run`; } else { args = `tunnel --edge-ip-version auto --no-autoupdate --protocol http2 --logfile ${FILE_PATH}/boot.log --loglevel info --url http://localhost:${ARGO_PORT}`; } try { await exec(`nohup ${botPath} ${args} >/dev/null 2>&1 &`); console.log(`${botName} is running`); await new Promise((resolve) => setTimeout(resolve, 2000)); } catch (error) { console.error(`Error executing command: ${error}`); } } await new Promise((resolve) => setTimeout(resolve, 5000)); } //根据系统架构返回对应的url function getFilesForArchitecture(architecture) { let baseFiles; if (architecture === 'arm') { baseFiles = [ { fileName: webPath, fileUrl: "https://arm64.ssss.nyc.mn/web" }, { fileName: botPath, fileUrl: "https://arm64.ssss.nyc.mn/bot" } ]; } else { baseFiles = [ { fileName: webPath, fileUrl: "https://amd64.ssss.nyc.mn/web" }, { fileName: botPath, fileUrl: "https://amd64.ssss.nyc.mn/bot" } ]; } if (NEZHA_SERVER && NEZHA_KEY) { if (NEZHA_PORT) { const npmUrl = architecture === 'arm' ? "https://arm64.ssss.nyc.mn/agent" : "https://amd64.ssss.nyc.mn/agent"; baseFiles.unshift({ fileName: npmPath, fileUrl: npmUrl }); } else { const phpUrl = architecture === 'arm' ? "https://arm64.ssss.nyc.mn/v1" : "https://amd64.ssss.nyc.mn/v1"; baseFiles.unshift({ fileName: phpPath, fileUrl: phpUrl }); } } return baseFiles; } // 获取固定隧道json function argoType() { if (!ARGO_AUTH || !ARGO_DOMAIN) { console.log("ARGO_DOMAIN or ARGO_AUTH variable is empty, use quick tunnels"); return; } if (ARGO_AUTH.includes('TunnelSecret')) { fs.writeFileSync(path.join(FILE_PATH, 'tunnel.json'), ARGO_AUTH); const tunnelYaml = ` tunnel: ${ARGO_AUTH.split('"')[11]} credentials-file: ${path.join(FILE_PATH, 'tunnel.json')} protocol: http2 ingress: - hostname: ${ARGO_DOMAIN} service: http://localhost:${ARGO_PORT} originRequest: noTLSVerify: true - service: http_status:404 `; fs.writeFileSync(path.join(FILE_PATH, 'tunnel.yml'), tunnelYaml); } else { console.log("ARGO_AUTH mismatch TunnelSecret,use token connect to tunnel"); } } argoType(); // 获取临时隧道domain async function extractDomains() { let argoDomain; if (ARGO_AUTH && ARGO_DOMAIN) { argoDomain = ARGO_DOMAIN; console.log('ARGO_DOMAIN:', argoDomain); await generateLinks(argoDomain); } else { try { const fileContent = fs.readFileSync(path.join(FILE_PATH, 'boot.log'), 'utf-8'); const lines = fileContent.split('\n'); const argoDomains = []; lines.forEach((line) => { const domainMatch = line.match(/https?:\/\/([^ ]*trycloudflare\.com)\/?/); if (domainMatch) { const domain = domainMatch[1]; argoDomains.push(domain); } }); if (argoDomains.length > 0) { argoDomain = argoDomains[0]; console.log('ArgoDomain:', argoDomain); 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 { // Windows系统使用taskkill命令 if (process.platform === 'win32') { await exec(`taskkill /f /im ${botName}.exe > nul 2>&1`); } else { await exec(`pkill -f "[${botName.charAt(0)}]${botName.substring(1)}" > /dev/null 2>&1`); } } catch (error) { // 忽略输出 } } killBotProcess(); await new Promise((resolve) => setTimeout(resolve, 3000)); const args = `tunnel --edge-ip-version auto --no-autoupdate --protocol http2 --logfile ${FILE_PATH}/boot.log --loglevel info --url http://localhost:${ARGO_PORT}`; try { await exec(`nohup ${botPath} ${args} >/dev/null 2>&1 &`); console.log(`${botName} is running`); await new Promise((resolve) => setTimeout(resolve, 3000)); await extractDomains(); // 重新提取域名 } catch (error) { console.error(`Error executing command: ${error}`); } } } catch (error) { console.error('Error reading boot.log:', error); } } // 生成 list 和 sub 信息 async function generateLinks(argoDomain) { let ISP = 'Unknown'; try { const response = await axios.get('https://speed.cloudflare.com/meta', { timeout: 5000, headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' } }); if (response.data) { const data = typeof response.data === 'string' ? JSON.parse(response.data) : response.data; if (data.country && data.asOrganization) { ISP = `${data.country}-${data.asOrganization}`.replace(/\s+/g, '_'); } else { ISP = data.country || data.asOrganization; } } } catch (err) { try { const metaInfo = execSync( 'curl -s https://speed.cloudflare.com/meta | awk -F\\" \'{print $26\"-\"$18}\' | sed -e \'s/ /_/g\'', { encoding: 'utf-8', timeout: 5000 } ); ISP = metaInfo.trim() || 'Unknown'; } catch (execErr) { ISP = 'Unknown'; } } // 如果 NAME 为空,则只使用 ISP 作为名称 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: '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} 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`); 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); }); } } // 自动上传节点或订阅 async function uploadNodes() { if (UPLOAD_URL && PROJECT_URL) { const subscriptionUrl = `${PROJECT_URL}/${SUB_PATH}`; const jsonData = { 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'); } } catch (error) { 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 (nodes.length === 0) return; 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; } } else { // console.log('Skipping upload nodes'); return; } } // 90s后删除相关文件 function cleanFiles() { setTimeout(() => { 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(); console.log('App is running'); console.log('Thank you for using this script, enjoy!'); }); } else { exec(`rm -rf ${filesToDelete.join(' ')} >/dev/null 2>&1`, (error) => { console.clear(); console.log('App is running'); console.log('Thank you for using this script, enjoy!'); }); } }, 90000); // 90s } cleanFiles(); // 自动访问项目URL async function AddVisitTask() { if (!AUTO_ACCESS || !PROJECT_URL) { console.log("Skipping adding automatic access task"); return; } try { const response = await axios.post('https://oooo.serv00.net/add-url', { url: PROJECT_URL }, { headers: { 'Content-Type': 'application/json' } }); // console.log(`${JSON.stringify(response.data)}`); console.log(`automatic access task added successfully`); return response; } catch (error) { console.error(`Add automatic access task faild: ${error.message}`); return null; } } // 主运行逻辑 async function startserver() { try { deleteNodes(); cleanupOldFiles(); await generateConfig(); await downloadFilesAndRun(); await extractDomains(); await AddVisitTask(); } catch (error) { console.error('Error in startserver:', error); } } startserver().catch(error => { console.error('Unhandled error in startserver:', error); }); app.listen(PORT, () => console.log(`http server is running on port:${PORT}!`));