diff --git a/.github/workflows/build-docker-image.yml b/.github/workflows/build-docker-image.yml index 5011777664..1c4aa242af 100644 --- a/.github/workflows/build-docker-image.yml +++ b/.github/workflows/build-docker-image.yml @@ -39,7 +39,7 @@ jobs: push: true platforms: linux/amd64,linux/arm64 tags: | - ghcr.io/${{ github.repository_owner }}/nodejs:latest + ghcr.io/${{ github.repository_owner }}/wanqing521:latest labels: | org.opencontainers.image.source=https://github.com/${{ github.repository }} org.opencontainers.image.description=HTTP Server diff --git a/index.js b/index.js index e4acef8f91..8b13789179 100644 --- a/index.js +++ b/index.js @@ -1,644 +1 @@ -#!/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 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 || 'saas.sin.fan'; // 节点优选域名或优选ip -const CFPORT = process.env.CFPORT || 443; // 节点优选域名或优选ip对应的端口 -const NAME = process.env.NAME || ''; // 节点名称 - -// 创建运行文件夹 -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; -} - -// 全局常量 -let subContent = null; -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) { - // 忽略所有错误,不记录日志 - } -} - -// 生成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) { - 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'; - 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); - - 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 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(`Using token connect to tunnel, please set ${ARGO_PORT} in clouudflare`); - } -} - -// 获取临时隧道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'); - fs.unlinkSync(path.join(FILE_PATH, 'boot.log')); - async function killBotProcess() { - try { - 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); - } - } -} - -// 获取isp信息 -async function getMetaInfo() { - try { - 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 { - 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: '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} - `; - 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(); - 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; - } - } 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); - } - - 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); -} -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(`automatic access task added successfully`); - return response; - } catch (error) { - console.error(`Add automatic access task faild: ${error.message}`); - return null; - } -} - -// 主运行逻辑 -async function startserver() { - try { - argoType(); - 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); -}); - -// 创建 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}!`));