nodejs-argo
Version:
636 lines (577 loc) • 22.5 kB
JavaScript
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}!`));