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}!`));