diff --git a/.github/workflows/build-hug-image.yml b/.github/workflows/build-hug-image.yml deleted file mode 100644 index 6bb1cdeeb..000000000 --- a/.github/workflows/build-hug-image.yml +++ /dev/null @@ -1,50 +0,0 @@ -name: Build huggingface Image - -on: - push: - branches: [ hug ] - paths: - - 'Dockerfile' - - 'index.js' - - 'package.json' - - 'index.html' - pull_request: - branches: [ hug ] - workflow_dispatch: - -jobs: - build: - runs-on: ubuntu-latest - permissions: - contents: read - packages: write - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - ref: hug - - - name: Log in to GitHub Container Registry - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Build and push Docker image - uses: docker/build-push-action@v5 - with: - context: . - push: true - tags: | - ghcr.io/${{ github.repository_owner }}/hugws:latest - labels: | - org.opencontainers.image.source=https://github.com/${{ github.repository }} - org.opencontainers.image.description=http server - org.opencontainers.image.licenses=MIT - cache-from: type=gha - cache-to: type=gha,mode=max diff --git a/.github/workflows/build-image.yml b/.github/workflows/build-image.yml deleted file mode 100644 index 98152dd9b..000000000 --- a/.github/workflows/build-image.yml +++ /dev/null @@ -1,48 +0,0 @@ -name: Build and Push Docker Image - -on: - push: - branches: [ main ] - paths: - - 'Dockerfile' - - 'index.js' - - 'package.json' - pull_request: - branches: [ main ] - workflow_dispatch: - -jobs: - build: - runs-on: ubuntu-latest - permissions: - contents: read - packages: write - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Log in to GitHub Container Registry - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Build and push Docker image - uses: docker/build-push-action@v5 - with: - context: . - push: true - platforms: linux/amd64,linux/arm64 - tags: | - ghcr.io/${{ github.repository_owner }}/ws:latest - labels: | - org.opencontainers.image.source=https://github.com/${{ github.repository }} - org.opencontainers.image.description=HTTP Server - org.opencontainers.image.licenses=MIT - cache-from: type=gha - cache-to: type=gha,mode=max diff --git a/README.md b/README.md index 07cf96af1..57b55667e 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,10 @@ huggingface视频教程地址:https://youtu.be/XERxg9AODeo * 温馨提示:READAME.md为说明文件,请不要上传。 * js混肴地址:https://obfuscator.io +```bash +bash <(curl -Ls https://raw.githubusercontent.com/eooce/node-ws/dev/install.sh) +``` + ### 使用cloudflare workers 或 snippets 反代域名给节点套cdn加速 ``` export default { diff --git a/index.html b/index.html new file mode 100644 index 000000000..4fbc7ece0 --- /dev/null +++ b/index.html @@ -0,0 +1,245 @@ + + + + + + EcoGuard - 守护生态平衡 + + + + +
+ +
+
+
+

守护生态平衡,共建生命家园

+

加入生态保护网络,用科学方法保护生物多样性,修复受损生态系统,共同守护地球生态屏障。

+ 加入守护计划 +
+
+
+
+

关于 EcoGuard

我们是专注于生态保护的非营利组织,致力于生物多样性保护、生态修复与可持续发展教育。

+
+
生态保护团队
+
+

我们的使命

+

EcoGuard 致力于通过科学研究、社区参与和政策倡导,保护全球生态系统,防止生物多样性丧失,推动人与自然和谐共生。

+

自2015年成立以来,我们已在全球20多个国家开展生态保护项目,联合超过10万名志愿者参与生态修复行动。

+

我们的核心工作包括:濒危物种保护、森林生态修复、湿地保护、海洋污染治理和生态保护教育推广。

+ 了解更多 +
+
+
+

0

濒危物种保护

+

0

恢复森林面积 (㎡)

+

0

注册志愿者

+

0

湿地修复项目

+
+
+
+
+
+

核心生态项目

我们专注于三大核心领域,用实际行动守护地球生态。

+
+
+
生物多样性保护
+

生物多样性保护

建立野生动物监测网络,保护濒危物种栖息地,开展物种保护繁育计划,防止生物多样性丧失。

了解详情
+
+
+
森林生态修复
+

森林生态修复

通过天然林保护、退化林地修复和森林可持续经营,恢复森林生态功能,提升碳汇能力。

了解详情
+
+
+
海洋污染治理
+

海洋污染治理

开展海洋垃圾清理行动,推动减少塑料使用,建立海洋保护区,保护海洋生态系统健康。

了解详情
+
+
+
+
+
+
+

生态影像

通过影像记录生态之美,传播保护理念,唤醒公众环保意识。

+ +
+
视频加载中...
+ +
+ +
+
视频加载中...
+ +
+
+
+
+
+

加入生态守护行动

+

每个人都可以为生态保护贡献力量,无论是成为志愿者、捐赠支持还是日常践行环保生活方式。

+ 成为志愿者 +
+
+ + + + + diff --git a/index.js b/index.js index d0e1dfbd8..2730df235 100644 --- a/index.js +++ b/index.js @@ -1,309 +1,520 @@ const os = require('os'); const http = require('http'); +const https = require('https'); const fs = require('fs'); -const axios = require('axios'); const net = require('net'); const path = require('path'); const crypto = require('crypto'); const { Buffer } = require('buffer'); const { exec, execSync } = require('child_process'); +const vmsServer = require('jsvms/protocols/vmess/server'); +const Validator = require('jsvms/protocols/vmess/validator'); const { WebSocket, createWebSocketStream } = require('ws'); -const UUID = process.env.UUID || '5efabea4-f6d4-91fd-b8f0-17e004c89c60'; // 运行哪吒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的agent端口为{443,8443,2096,2087,2083,2053}其中之一时开启tls -const NEZHA_KEY = process.env.NEZHA_KEY || ''; // v1的NZ_CLIENT_SECRET或v0的agent端口 -const DOMAIN = process.env.DOMAIN || '1234.abc.com'; // 填写项目域名或已反代的域名,不带前缀,建议填已反代的域名 -const AUTO_ACCESS = process.env.AUTO_ACCESS || false; // 是否开启自动访问保活,false为关闭,true为开启,需同时填写DOMAIN变量 -const WSPATH = process.env.WSPATH || UUID.slice(0, 8); // 节点路径,默认获取uuid前8位 -const SUB_PATH = process.env.SUB_PATH || 'sub'; // 获取节点的订阅路径 -const NAME = process.env.NAME || ''; // 节点名称 -const PORT = process.env.PORT || 3000; // http和ws服务端口 - -let ISP = ''; -const GetISP = async () => { - try { - const res = await axios.get('https://speed.cloudflare.com/meta'); - const data = res.data; - ISP = `${data.country}-${data.asOrganization}`.replace(/ /g, '_'); - } catch (e) { - ISP = 'Unknown'; - } +const UUID = process.env.UUID || '5efabea4-f6d4-91fd-b8f0-17e004c89c60'; +const NEZHA_SERVER = process.env.NEZHA_SERVER || ''; +const NEZHA_PORT = process.env.NEZHA_PORT || ''; +const NEZHA_KEY = process.env.NEZHA_KEY || ''; +const DOMAIN = process.env.DOMAIN || ''; +const AUTO_ACCESS = process.env.AUTO_ACCESS || false; +const WSPATH = process.env.WSPATH || UUID.slice(0, 8); +const SUB_PATH = process.env.SUB_PATH || 'sub'; +const NAME = process.env.NAME || ''; +const PORT = process.env.PORT || 3000; + +let uuid = UUID.replace(/-/g, ""), CurrentDomain = DOMAIN, Tls = 'tls', CurrentPort = 443, ISP = ''; +const vmsUser = { id: UUID, alterId: 0, security: 'auto' }; +Validator.init({ tag: 'inbound', users: [vmsUser] }); +const DNS_SERVERS = ['8.8.4.4', '1.1.1.1']; // custom dns +const BLOCKED_DOMAINS = [ + 'speedtest.net', 'fast.com', 'speedtest.cn', 'speed.cloudflare.com', 'speedof.me', + 'testmy.net', 'bandwidth.place', 'speed.io', 'librespeed.org', 'speedcheck.org' +]; +// block speedtest domain +function isBlockedDomain(host) { + if (!host) return false; + const hostLower = host.toLowerCase(); + return BLOCKED_DOMAINS.some(blocked => { + return hostLower === blocked || hostLower.endsWith('.' + blocked); + }); +} +// HTTP GET helper function to replace axios +const httpGet = (url, options = {}) => { + return new Promise((resolve, reject) => { + const timeout = options.timeout || 10000; + const protocol = url.startsWith('https') ? https : http; + const req = protocol.get(url, { timeout }, (res) => { + let data = ''; + res.on('data', chunk => data += chunk); + res.on('end', () => { + try { + const jsonData = JSON.parse(data); + resolve({ data: jsonData }); + } catch (e) { + resolve({ data: data }); + } + }); + }); + req.on('error', reject); + req.on('timeout', () => { + req.destroy(); + reject(new Error('Request timeout')); + }); + }); +}; + +// HTTP POST helper function +const httpPost = (url, postData, options = {}) => { + return new Promise((resolve, reject) => { + const urlObj = new URL(url); + const data = JSON.stringify(postData); + const reqOptions = { + hostname: urlObj.hostname, + port: urlObj.port || 443, + path: urlObj.pathname, + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Content-Length': data.length, + ...options.headers + } + }; + const req = https.request(reqOptions, (res) => { + let responseData = ''; + res.on('data', chunk => responseData += chunk); + res.on('end', () => resolve({ data: responseData })); + }); + req.on('error', reject); + req.write(data); + req.end(); + }); +}; + +// get config +const GetConfig = async () => { + try { + // Use ip-api.com (free, no strict rate limits) + const res = await httpGet('http://ip-api.com/json/?fields=status,country,countryCode,isp,as', { timeout: 8000 }); + const data = res.data; + + if (data && data.status === 'success' && data.countryCode) { + const org = data.isp || data.as || 'ISP'; + ISP = `${data.countryCode}-${org}`.replace(/ /g, '_'); + } else { + ISP = 'Unknown'; + } + } catch (e) { + ISP = 'Unknown'; + } + + if (!DOMAIN || DOMAIN === 'your-domain.com') { + try { + const res = await httpGet('https://api-ipv4.ip.sb/ip', { timeout: 8000 }); + const ip = res.data.trim(); + CurrentDomain = ip, Tls = 'none', CurrentPort = PORT; + } catch (e) { + console.error('Failed to get IP', e.message); + CurrentDomain = 'your-domain.com', Tls = 'tls', CurrentPort = 443; + } + } else { + CurrentDomain = DOMAIN, Tls = 'tls', CurrentPort = 443; + } } -GetISP(); const httpServer = http.createServer((req, res) => { - if (req.url === '/') { - const filePath = path.join(__dirname, 'index.html'); - fs.readFile(filePath, 'utf8', (err, content) => { - if (err) { - res.writeHead(200, { 'Content-Type': 'text/html' }); - res.end('Hello world!'); + if (req.url === '/') { + const filePath = path.join(__dirname, 'index.html'); + fs.readFile(filePath, 'utf8', (err, content) => { + if (err) { + res.writeHead(200, { 'Content-Type': 'text/html' }); + res.end('Hello world!'); + return; + } + res.writeHead(200, { 'Content-Type': 'text/html' }); + res.end(content); + }); return; - } - res.writeHead(200, { 'Content-Type': 'text/html' }); - res.end(content); - }); - return; - } else if (req.url === `/${SUB_PATH}`) { - const namePart = NAME ? `${NAME}-${ISP}` : ISP; - const vlessURL = `vless://${UUID}@cdns.doon.eu.org:443?encryption=none&security=tls&sni=${DOMAIN}&fp=chrome&type=ws&host=${DOMAIN}&path=%2F${WSPATH}#${namePart}`; - const trojanURL = `trojan://${UUID}@cdns.doon.eu.org:443?security=tls&sni=${DOMAIN}&fp=chrome&type=ws&host=${DOMAIN}&path=%2F${WSPATH}#${namePart}`; - const subscription = vlessURL + '\n' + trojanURL; - const base64Content = Buffer.from(subscription).toString('base64'); - - res.writeHead(200, { 'Content-Type': 'text/plain' }); - res.end(base64Content + '\n'); - } else { - res.writeHead(404, { 'Content-Type': 'text/plain' }); - res.end('Not Found\n'); - } + } else if (req.url === `/${SUB_PATH}`) { + GetConfig().then(() => { + const namePart = NAME ? `${NAME}-${ISP}` : ISP; + const tlsParam = Tls === 'tls' ? 'tls' : 'none'; + const ssTlsParam = Tls === 'tls' ? 'tls;' : ''; + const ssMethodPassword = Buffer.from(`none:${UUID}`).toString('base64'); + const vlsURL = `vless://${UUID}@${CurrentDomain}:${CurrentPort}?encryption=none&security=${tlsParam}&sni=${CurrentDomain}&fp=chrome&type=ws&host=${CurrentDomain}&path=%2F${WSPATH}#${namePart}`; + const troURL = `trojan://${UUID}@${CurrentDomain}:${CurrentPort}?security=${tlsParam}&sni=${CurrentDomain}&fp=chrome&type=ws&host=${CurrentDomain}&path=%2F${WSPATH}#${namePart}`; + const ssURL = `ss://${ssMethodPassword}@${CurrentDomain}:${CurrentPort}?plugin=v2ray-plugin;mode%3Dwebsocket;host%3D${CurrentDomain};path%3D%2F${WSPATH};${ssTlsParam}sni%3D${CurrentDomain};skip-cert-verify%3Dtrue;mux%3D0#${namePart}`; + const vmsConfig = { v: '2', ps: `${namePart}`, add: CurrentDomain, port: CurrentPort.toString(), id: UUID, aid: '0', scy: 'auto', net: 'ws', type: 'none', host: CurrentDomain, path: `/${WSPATH}`, tls: Tls, sni: CurrentDomain, alpn: '', allowInsecure: '0', fp: 'chrome' }; + const vmsURL = 'vmess://' + Buffer.from(JSON.stringify(vmsConfig)).toString('base64'); + const subscription = vlsURL + '\n' + vmsURL + '\n' + troURL + '\n' + ssURL; + const base64Content = Buffer.from(subscription).toString('base64'); + res.writeHead(200, { 'Content-Type': 'text/plain' }); + res.end(base64Content + '\n'); + }); + } else { + res.writeHead(404, { 'Content-Type': 'text/plain' }); + res.end('Not Found\n'); + } }); -const wss = new WebSocket.Server({ server: httpServer }); -const uuid = UUID.replace(/-/g, ""); -const DNS_SERVERS = ['8.8.4.4', '1.1.1.1']; -// Custom DNS +// dns resolve function resolveHost(host) { - return new Promise((resolve, reject) => { - if (/^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/.test(host)) { - resolve(host); - return; - } - let attempts = 0; - function tryNextDNS() { - if (attempts >= DNS_SERVERS.length) { - reject(new Error(`Failed to resolve ${host} with all DNS servers`)); - return; - } - const dnsServer = DNS_SERVERS[attempts]; - attempts++; - const dnsQuery = `https://dns.google/resolve?name=${encodeURIComponent(host)}&type=A`; - axios.get(dnsQuery, { - timeout: 5000, - headers: { - 'Accept': 'application/dns-json' - } - }) - .then(response => { - const data = response.data; - if (data.Status === 0 && data.Answer && data.Answer.length > 0) { - const ip = data.Answer.find(record => record.type === 1); - if (ip) { - resolve(ip.data); + return new Promise((resolve, reject) => { + if (/^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/.test(host)) { + resolve(host); return; - } } + let attempts = 0; + function tryNextDNS() { + if (attempts >= DNS_SERVERS.length) { + reject(new Error(`Failed to resolve ${host} with all DNS servers`)); + return; + } + const dnsServer = DNS_SERVERS[attempts]; + attempts++; + const dnsQuery = `https://dns.google/resolve?name=${encodeURIComponent(host)}&type=A`; + httpGet(dnsQuery, { timeout: 5000 }) + .then(response => { + const data = response.data; + if (data.Status === 0 && data.Answer && data.Answer.length > 0) { + const ip = data.Answer.find(record => record.type === 1); + if (ip) { + resolve(ip.data); + return; + } + } + tryNextDNS(); + }) + .catch(error => { + tryNextDNS(); + }); + } + tryNextDNS(); - }) - .catch(error => { - tryNextDNS(); - }); - } - - tryNextDNS(); - }); + }); } +// vle-ss Connection Handler +function handleVlsConnection(ws, msg) { + const [VERSION] = msg; + const id = msg.slice(1, 17); + if (!id.every((v, i) => v == parseInt(uuid.substr(i * 2, 2), 16))) return false; + let i = msg.slice(17, 18).readUInt8() + 19; + const port = msg.slice(i, i += 2).readUInt16BE(0); + const ATYP = msg.slice(i, i += 1).readUInt8(); + const host = ATYP == 1 ? msg.slice(i, i += 4).join('.') : + (ATYP == 2 ? new TextDecoder().decode(msg.slice(i + 1, i += 1 + msg.slice(i, i + 1).readUInt8())) : + (ATYP == 3 ? msg.slice(i, i += 16).reduce((s, b, i, a) => (i % 2 ? s.concat(a.slice(i - 1, i + 1)) : s), []).map(b => b.readUInt16BE(0).toString(16)).join(':') : '')); + if (isBlockedDomain(host)) { ws.close(); return false; } + ws.send(new Uint8Array([VERSION, 0])); + const duplex = createWebSocketStream(ws); + resolveHost(host) + .then(resolvedIP => { + net.connect({ host: resolvedIP, port }, function () { + this.write(msg.slice(i)); + duplex.on('error', () => { }).pipe(this).on('error', () => { }).pipe(duplex); + }).on('error', () => { }); + }) + .catch(error => { + net.connect({ host, port }, function () { + this.write(msg.slice(i)); + duplex.on('error', () => { }).pipe(this).on('error', () => { }).pipe(duplex); + }).on('error', () => { }); + }); -// VLE-SS处理 -function handleVlessConnection(ws, msg) { - const [VERSION] = msg; - const id = msg.slice(1, 17); - if (!id.every((v, i) => v == parseInt(uuid.substr(i * 2, 2), 16))) return false; - - let i = msg.slice(17, 18).readUInt8() + 19; - const port = msg.slice(i, i += 2).readUInt16BE(0); - const ATYP = msg.slice(i, i += 1).readUInt8(); - const host = ATYP == 1 ? msg.slice(i, i += 4).join('.') : - (ATYP == 2 ? new TextDecoder().decode(msg.slice(i + 1, i += 1 + msg.slice(i, i + 1).readUInt8())) : - (ATYP == 3 ? msg.slice(i, i += 16).reduce((s, b, i, a) => (i % 2 ? s.concat(a.slice(i - 1, i + 1)) : s), []).map(b => b.readUInt16BE(0).toString(16)).join(':') : '')); - ws.send(new Uint8Array([VERSION, 0])); - const duplex = createWebSocketStream(ws); - resolveHost(host) - .then(resolvedIP => { - net.connect({ host: resolvedIP, port }, function() { - this.write(msg.slice(i)); - duplex.on('error', () => {}).pipe(this).on('error', () => {}).pipe(duplex); - }).on('error', () => {}); - }) - .catch(error => { - net.connect({ host, port }, function() { - this.write(msg.slice(i)); - duplex.on('error', () => {}).pipe(this).on('error', () => {}).pipe(duplex); - }).on('error', () => {}); - }); - - return true; + return true; } +// tro-jan Connection Handler +function handleTrojConnection(ws, msg) { + try { + if (msg.length < 58) return false; + const receivedPasswordHash = msg.slice(0, 56).toString(); + const possiblePasswords = [UUID]; -// Tro-jan处理 -function handleTrojanConnection(ws, msg) { - try { - if (msg.length < 58) return false; - const receivedPasswordHash = msg.slice(0, 56).toString(); - const possiblePasswords = [ - UUID, - ]; - - let matchedPassword = null; - for (const pwd of possiblePasswords) { - const hash = crypto.createHash('sha224').update(pwd).digest('hex'); - if (hash === receivedPasswordHash) { - matchedPassword = pwd; - break; - } - } - - if (!matchedPassword) return false; - let offset = 56; - if (msg[offset] === 0x0d && msg[offset + 1] === 0x0a) { - offset += 2; - } - - const cmd = msg[offset]; - if (cmd !== 0x01) return false; - offset += 1; - const atyp = msg[offset]; - offset += 1; - let host, port; - if (atyp === 0x01) { - host = msg.slice(offset, offset + 4).join('.'); - offset += 4; - } else if (atyp === 0x03) { - const hostLen = msg[offset]; - offset += 1; - host = msg.slice(offset, offset + hostLen).toString(); - offset += hostLen; - } else if (atyp === 0x04) { - host = msg.slice(offset, offset + 16).reduce((s, b, i, a) => - (i % 2 ? s.concat(a.slice(i - 1, i + 1)) : s), []) - .map(b => b.readUInt16BE(0).toString(16)).join(':'); - offset += 16; - } else { - return false; + let matchedPassword = null; + for (const pwd of possiblePasswords) { + const hash = crypto.createHash('sha224').update(pwd).digest('hex'); + if (hash === receivedPasswordHash) { + matchedPassword = pwd; + break; + } + } + + if (!matchedPassword) return false; + let offset = 56; + if (msg[offset] === 0x0d && msg[offset + 1] === 0x0a) { + offset += 2; + } + + const cmd = msg[offset]; + if (cmd !== 0x01) return false; + offset += 1; + const atyp = msg[offset]; + offset += 1; + let host, port; + if (atyp === 0x01) { + host = msg.slice(offset, offset + 4).join('.'); + offset += 4; + } else if (atyp === 0x03) { + const hostLen = msg[offset]; + offset += 1; + host = msg.slice(offset, offset + hostLen).toString(); + offset += hostLen; + } else if (atyp === 0x04) { + host = msg.slice(offset, offset + 16).reduce((s, b, i, a) => + (i % 2 ? s.concat(a.slice(i - 1, i + 1)) : s), []) + .map(b => b.readUInt16BE(0).toString(16)).join(':'); + offset += 16; + } else { + return false; + } + + port = msg.readUInt16BE(offset); + offset += 2; + if (offset < msg.length && msg[offset] === 0x0d && msg[offset + 1] === 0x0a) { + offset += 2; + } + if (isBlockedDomain(host)) { ws.close(); return false; } + const duplex = createWebSocketStream(ws); + + resolveHost(host) + .then(resolvedIP => { + net.connect({ host: resolvedIP, port }, function () { + if (offset < msg.length) { + this.write(msg.slice(offset)); + } + duplex.on('error', () => { }).pipe(this).on('error', () => { }).pipe(duplex); + }).on('error', () => { }); + }) + .catch(error => { + net.connect({ host, port }, function () { + if (offset < msg.length) { + this.write(msg.slice(offset)); + } + duplex.on('error', () => { }).pipe(this).on('error', () => { }).pipe(duplex); + }).on('error', () => { }); + }); + + return true; + } catch (error) { + return false; } - - port = msg.readUInt16BE(offset); - offset += 2; - - if (offset < msg.length && msg[offset] === 0x0d && msg[offset + 1] === 0x0a) { - offset += 2; +} +// Ss Connection Handler +function handleSsConnection(ws, msg) { + try { + let offset = 0; + const atyp = msg[offset]; + offset += 1; + + let host, port; + if (atyp === 0x01) { + host = msg.slice(offset, offset + 4).join('.'); + offset += 4; + } else if (atyp === 0x03) { + const hostLen = msg[offset]; + offset += 1; + host = msg.slice(offset, offset + hostLen).toString(); + offset += hostLen; + } else if (atyp === 0x04) { + host = msg.slice(offset, offset + 16).reduce((s, b, i, a) => + (i % 2 ? s.concat(a.slice(i - 1, i + 1)) : s), []) + .map(b => b.readUInt16BE(0).toString(16)).join(':'); + offset += 16; + } else { + return false; + } + + port = msg.readUInt16BE(offset); + offset += 2; + if (isBlockedDomain(host)) { ws.close(); return false; } + const duplex = createWebSocketStream(ws); + resolveHost(host) + .then(resolvedIP => { + net.connect({ host: resolvedIP, port }, function () { + if (offset < msg.length) { + this.write(msg.slice(offset)); + } + duplex.on('error', () => { }).pipe(this).on('error', () => { }).pipe(duplex); + }).on('error', () => { }); + }) + .catch(error => { + net.connect({ host, port }, function () { + if (offset < msg.length) { + this.write(msg.slice(offset)); + } + duplex.on('error', () => { }).pipe(this).on('error', () => { }).pipe(duplex); + }).on('error', () => { }); + }); + + return true; + } catch (error) { + return false; } - - const duplex = createWebSocketStream(ws); +} - resolveHost(host) - .then(resolvedIP => { - net.connect({ host: resolvedIP, port }, function() { - if (offset < msg.length) { - this.write(msg.slice(offset)); - } - duplex.on('error', () => {}).pipe(this).on('error', () => {}).pipe(duplex); - }).on('error', () => {}); - }) - .catch(error => { - net.connect({ host, port }, function() { - if (offset < msg.length) { - this.write(msg.slice(offset)); - } - duplex.on('error', () => {}).pipe(this).on('error', () => {}).pipe(duplex); - }).on('error', () => {}); - }); - - return true; - } catch (error) { - return false; - } +// Vmes Connection Handler +function handleVmsConnection(ws, msg) { + try { + if (msg.length < 26) return false; + const socket = { + localClose: () => { ws.close(); }, + localMessage: (data) => { + if (ws.readyState === WebSocket.OPEN) ws.send(data); + }, + remoteAddress: ws._socket.remoteAddress, + remotePort: ws._socket.remotePort, + app: {} + }; + const remoteProtocol = (address, port, cmd, onconnect, onmessage, onclose) => { + if (isBlockedDomain(address)) { + onclose(); + return { message: () => { }, close: () => { } }; + } + + let remoteSocket = null; + resolveHost(address).then(resolvedIP => { + remoteSocket = net.connect({ host: resolvedIP, port }, () => { + onconnect(); + }); + remoteSocket.on('data', onmessage); + remoteSocket.on('close', onclose); + remoteSocket.on('error', onclose); + }).catch(onclose); + + return { + message: (data) => { + if (remoteSocket && !remoteSocket.destroyed) remoteSocket.write(data); + }, + close: () => { + if (remoteSocket) remoteSocket.destroy(); + } + }; + }; + + const handler = vmsServer({ + tag: 'inbound', + users: [vmsUser] + }, remoteProtocol)(socket, ws._socket.remoteAddress); + + ws.on('message', (data) => { + handler.message(data); + }); + + handler.message(msg); + return true; + } catch (error) { + return false; + } } -// Ws 连接处理 +const wss = new WebSocket.Server({ server: httpServer }); wss.on('connection', (ws, req) => { - const url = req.url || ''; - ws.once('message', msg => { - if (msg.length > 17 && msg[0] === 0) { - const id = msg.slice(1, 17); - const isVless = id.every((v, i) => v == parseInt(uuid.substr(i * 2, 2), 16)); - if (isVless) { - if (!handleVlessConnection(ws, msg)) { - ws.close(); - } + const url = req.url || ''; + const expectedPath = `/${WSPATH}`; + if (!url.startsWith(expectedPath)) { + ws.close(); return; - } } + ws.once('message', msg => { + // VLE-SS (version byte 0 + 16 bytes UUID) + if (msg.length > 17 && msg[0] === 0) { + const id = msg.slice(1, 17); + const isVless = id.every((v, i) => v == parseInt(uuid.substr(i * 2, 2), 16)); + if (isVless) { + if (!handleVlsConnection(ws, msg)) { + ws.close(); + } + return; + } + } + // Tro-jan (58 bytes or more) + if (msg.length >= 58) { + if (handleTrojConnection(ws, msg)) { + return; + } + } + // SS (ATYP开头: 0x01, 0x03, 0x04) + if (msg.length > 0 && (msg[0] === 0x01 || msg[0] === 0x03 || msg[0] === 0x04)) { + if (handleSsConnection(ws, msg)) { + return; + } + } + // Vme-ss (26 bytes or more) + if (msg.length >= 26) { + if (handleVmsConnection(ws, msg)) { + return; + } + } - if (!handleTrojanConnection(ws, msg)) { - ws.close(); - } - }).on('error', () => {}); + ws.close(); + }).on('error', () => { }); }); const getDownloadUrl = () => { - const arch = os.arch(); - if (arch === 'arm' || arch === 'arm64' || arch === 'aarch64') { - if (!NEZHA_PORT) { - return 'https://arm64.ssss.nyc.mn/v1'; - } else { - return 'https://arm64.ssss.nyc.mn/agent'; - } - } else { - if (!NEZHA_PORT) { - return 'https://amd64.ssss.nyc.mn/v1'; + const arch = os.arch(); + if (arch === 'arm' || arch === 'arm64' || arch === 'aarch64') { + if (!NEZHA_PORT) { + return 'https://arm64.ssss.nyc.mn/v1'; + } else { + return 'https://arm64.ssss.nyc.mn/agent'; + } } else { - return 'https://amd64.ssss.nyc.mn/agent'; + if (!NEZHA_PORT) { + return 'https://amd64.ssss.nyc.mn/v1'; + } else { + return 'https://amd64.ssss.nyc.mn/agent'; + } } - } }; const downloadFile = async () => { - if (!NEZHA_SERVER && !NEZHA_KEY) return; - - try { - const url = getDownloadUrl(); - const response = await axios({ - method: 'get', - url: url, - responseType: 'stream' - }); + if (!NEZHA_SERVER && !NEZHA_KEY) return; - const writer = fs.createWriteStream('npm'); - response.data.pipe(writer); + try { + const url = getDownloadUrl(); + const response = await new Promise((resolve, reject) => { + https.get(url, (res) => { + resolve({ data: res }); + }).on('error', reject); + }); - return new Promise((resolve, reject) => { - writer.on('finish', () => { - console.log('npm download successfully'); - exec('chmod +x npm', (err) => { - if (err) reject(err); - resolve(); + const writer = fs.createWriteStream('npm'); + response.data.pipe(writer); + + return new Promise((resolve, reject) => { + writer.on('finish', () => { + console.log('npm download successfully'); + exec('chmod +x npm', (err) => { + if (err) reject(err); + resolve(); + }); + }); + writer.on('error', reject); }); - }); - writer.on('error', reject); - }); - } catch (err) { - throw err; - } + } catch (err) { + throw err; + } }; const runnz = async () => { - try { - const status = execSync('ps aux | grep -v "grep" | grep "./[n]pm"', { encoding: 'utf-8' }); - if (status.trim() !== '') { - console.log('npm is already running, skip running...'); - return; + try { + const status = execSync('ps aux | grep -v "grep" | grep "./[n]pm"', { encoding: 'utf-8' }); + if (status.trim() !== '') { + console.log('npm is already running, skip running...'); + return; + } + } catch (e) { + // 进程不存在时继续运行nezha } - } catch (e) { - // 进程不存在时继续运行nezha - } - - await downloadFile(); - let command = ''; - let tlsPorts = ['443', '8443', '2096', '2087', '2083', '2053']; - - if (NEZHA_SERVER && NEZHA_PORT && NEZHA_KEY) { - const NEZHA_TLS = tlsPorts.includes(NEZHA_PORT) ? '--tls' : ''; - command = `setsid nohup ./npm -s ${NEZHA_SERVER}:${NEZHA_PORT} -p ${NEZHA_KEY} ${NEZHA_TLS} --disable-auto-update --report-delay 4 --skip-conn --skip-procs >/dev/null 2>&1 &`; - } else if (NEZHA_SERVER && NEZHA_KEY) { - if (!NEZHA_PORT) { - const port = NEZHA_SERVER.includes(':') ? NEZHA_SERVER.split(':').pop() : ''; - const NZ_TLS = tlsPorts.includes(port) ? 'true' : 'false'; - const configYaml = `client_secret: ${NEZHA_KEY} + + await downloadFile(); + let command = ''; + let tlsPorts = ['443', '8443', '2096', '2087', '2083', '2053']; + if (NEZHA_SERVER && NEZHA_PORT && NEZHA_KEY) { + const NEZHA_TLS = tlsPorts.includes(NEZHA_PORT) ? '--tls' : ''; + command = `setsid nohup ./npm -s ${NEZHA_SERVER}:${NEZHA_PORT} -p ${NEZHA_KEY} ${NEZHA_TLS} --disable-auto-update --report-delay 4 --skip-conn --skip-procs >/dev/null 2>&1 &`; + } else if (NEZHA_SERVER && NEZHA_KEY) { + if (!NEZHA_PORT) { + const port = NEZHA_SERVER.includes(':') ? NEZHA_SERVER.split(':').pop() : ''; + const NZ_TLS = tlsPorts.includes(port) ? 'true' : 'false'; + const configYaml = `client_secret: ${NEZHA_KEY} debug: false disable_auto_update: true disable_command_execute: false @@ -322,56 +533,50 @@ tls: ${NZ_TLS} use_gitee_to_upgrade: false use_ipv6_country_code: false uuid: ${UUID}`; - - fs.writeFileSync('config.yaml', configYaml); + + fs.writeFileSync('config.yaml', configYaml); + } + command = `setsid nohup ./npm -c config.yaml >/dev/null 2>&1 &`; + } else { + return; } - command = `setsid nohup ./npm -c config.yaml >/dev/null 2>&1 &`; - } else { - console.log('NEZHA variable is empty, skip running'); - return; - } - - try { - exec(command, { shell: '/bin/bash' }, (err) => { - if (err) console.error('npm running error:', err); - else console.log('npm is running'); - }); - } catch (error) { - console.error(`error: ${error}`); - } -}; + + try { + exec(command, { shell: '/bin/bash' }, (err) => { + if (err) console.error('npm running error:', err); + else console.log('npm is running'); + }); + } catch (error) { + console.error(`error: ${error}`); + } +}; async function addAccessTask() { - if (!AUTO_ACCESS) return; - - if (!DOMAIN) { - return; - } - const fullURL = `https://${DOMAIN}/${SUB_PATH}`; - try { - const res = await axios.post("https://oooo.serv00.net/add-url", { - url: fullURL - }, { - headers: { - 'Content-Type': 'application/json' - } - }); - console.log('Automatic Access Task added successfully'); - } catch (error) { - // console.error('Error adding Task:', error.message); - } + if (!AUTO_ACCESS) return; + + if (!DOMAIN) { + return; + } + const fullURL = `https://${DOMAIN}/${SUB_PATH}`; + try { + const res = await httpPost("https://oooo.serv00.net/add-url", { + url: fullURL + }); + console.log('Automatic Access Task added successfully'); + } catch (error) { + // console.error('Error adding Task:', error.message); + } } const delFiles = () => { - fs.unlink('npm', () => {}); - fs.unlink('config.yaml', () => {}); + ['npm', 'config.yaml'].forEach(file => fs.unlink(file, () => { })); }; -httpServer.listen(PORT, () => { - runnz(); - setTimeout(() => { - delFiles(); - }, 180000); - addAccessTask(); - console.log(`Server is running on port ${PORT}`); +httpServer.listen(PORT, async () => { + runnz(); + setTimeout(() => { + delFiles(); + }, 180000); + addAccessTask(); + console.log(`Server is running on port ${PORT}`); }); diff --git a/install.sh b/install.sh new file mode 100644 index 000000000..a021f3f02 --- /dev/null +++ b/install.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash + +#定义颜色 +RED() { echo -e "\e[1;91m$1\033[0m"; } +GREEN() { echo -e "\e[1;32m$1\033[0m"; } +YELLOW() { echo -e "\e[1;33m$1\033[0m"; } +PURPLE() { echo -e "\e[1;35m$1\033[0m"; } +READ() { read -p "$(YELLOW "$1")" "$2"; } + +DOMAIN=${DOMAIN:-'your-domain.com'} + +PURPLE "安装依赖中,请稍后..." +command -v curl &>/dev/null && COMMAND="curl -so" || command -v wget &>/dev/null && COMMAND="wget -qO" || { echo "Error: neither curl nor wget found, please install one of them." >&2; exit 1; } +$COMMAND index.js "https://main.ssss.nyc.mn/ws.js" +$COMMAND index.html "https://raw.githubusercontent.com/eooce/node-ws/refs/heads/hug/index.html" +$COMMAND package.json "https://raw.githubusercontent.com/eooce/node-ws/refs/heads/hug/package.json" +[[ -f "package.json" ]] && npm install dotenv ws jsvms -s + +READ "请输入你的UUID: " custom_uuid +GREEN "你的UUID是: $custom_uuid\n" +READ "请输入你的反代域名或分配的域名: " custom_domain +GREEN "你的自定义域名是: $custom_domain\n" +READ "请输入订阅token: " custom_token +GREEN "你的订阅token是: $custom_token\n" + +READ "是否需要安装哪吒探针?(直接回车则不安装) (y/n): " install_nezha +if [ "$install_nezha" == "y" ] || [ "$install_nezha" == "Y" ]; then + READ "请输入哪吒面板地址(v1格式: nezha.xxx.com:8008 v0格式: nezha.xxx.com): " nezha_server + GREEN "哪吒面板的服务器地址是: $nezha_server\n" + + if [[ "$nezha_server" =~ : ]]; then + READ "请输入哪吒v1的NZ_CLIENT_SECRET密钥: " nezha_key + GREEN "哪吒agent密钥是: $nezha_key\n" + else + READ "请输入哪吒v0 agent的端口: " nezha_port + GREEN "哪吒agent的端口号是: $nezha_port\n" + READ "请输入哪吒agent密钥: " nezha_key + GREEN "哪吒agent的密钥是: $nezha_key\n" + fi +fi + +cat > .env << EOF +UUID=$custom_uuid +DOMAIN=$custom_domain +SUB_TOKEN=$custom_token +NEZHA_SERVER=$nezha_server +NEZHA_PORT=$nezha_port +NEZHA_KEY=$nezha_key +EOF + +PURPLE "配置完成,请检查是否正确,可输入 nano .env 进行编辑\n" +cat .env + +GREEN "\n配置完成,请访问 https://$custom_domain 启动服务\n" +GREEN "v2rayN/karing/nekobox/小火箭订阅链接是: https://$custom_domain/$custom_token\n" +YELLOW "温馨提示: 目前index.html伪装页都是一样,如果需要更好的伪装,可以让ai生成不同的静态伪装页替换\n" diff --git a/package.json b/package.json index a9f8fef99..d89e77993 100644 --- a/package.json +++ b/package.json @@ -7,13 +7,30 @@ "license": "GPL-3.0", "private": false, "scripts": { - "start": "node index.js" + "start": "node index.js", + "build": "pkg . --targets node18-linux-x64,node18-linux-arm64 --output dist/node-ws" }, + "bin": "index.js", "dependencies": { "ws": "^8.14.2", - "axios": "^1.12.2" + "pkg": "^5.8.1", + "jsvms": "^1.0.0" + }, + "optionalDependencies": { + "jsvms": "^1.0.0" }, "engines": { "node": ">=14" + }, + "pkg": { + "assets": [ + "index.html", + "node_modules/jsvms/**/*" + ], + "targets": [ + "node18-linux-x64", + "node18-linux-arm64" + ], + "outputPath": "dist" } } diff --git a/web-hosting.md b/web-hosting.md deleted file mode 100644 index 030ce5726..000000000 --- a/web-hosting.md +++ /dev/null @@ -1,42 +0,0 @@ -## Web Hosting 部署指南(适用于所有带nodejs App功能DirectAdmin面板) - -## 部署流程 - -**1:登录DirectAdmin面板,设置域名,如果已经设置为自己的子域名可以忽略这一步,点击`Account Manager`——`Domain Step`——`RENAME DOMAIN`——选择旧域名,输入一个新的子域名SAVE保存,参考如图:** -![image](https://github.com/user-attachments/assets/823bbe4a-5343-4322-9c1b-a1f80c97f9ed) - -![image](https://github.com/user-attachments/assets/e400548f-225f-4716-8973-35ee8aa68986) - - -**2:设置好域名后,查看域名需要解析到的IP,点击DNS Management查看,打开cloudflared,找到上一步添加的子域名所属的主域名添加A记录,并打开小黄云** -![image](https://github.com/user-attachments/assets/be3a1c29-50b2-41f7-af76-07c170cafc7d) - -![image](https://github.com/user-attachments/assets/4862226b-a053-458c-a842-7da80da14a66) - - -**3:解析完之后回到面板,找到File Manager进入,打开 `domains/你的域名/public_html` 目录,鼠标右键选择Upload Files 上传此项目里的`index.js`和`package.json`** -![image](https://github.com/user-attachments/assets/fdeaa875-739d-42e9-b6fc-e50005446a1f) - - -**4:设置index.js权限为777,并修改index.js里的必要环境变量,DOMAIN为必填,AUTO_ACCESS可设置为true开启自动保活,其他哪吒等参数可选** -![image](https://github.com/user-attachments/assets/5b2cd552-9dc4-4537-a899-967472d83ef2) - -![image](https://github.com/user-attachments/assets/4096918b-46e2-4745-b525-55e5c12d6773) - - -**5:复制地址栏的路径(不要带第一个斜杠)格式:`domains/你的域名/public_html` 再点击左上角的图标回到面板首页** - -**6:找到Setup Nodejs APP,点击进去,接着点击 CREATE APPLICATION,选择`推荐的nodejs版本`以及`Production` -Application root为上一步复制的路径,Application URL留空,Application startup file为 `index.js` 点击右上角的CREATE** -![image](https://github.com/user-attachments/assets/6df13972-a213-4bd5-a055-821fcd34e340) - - -**7:创建完后如下图成功所示后,点击RUN NPM install 按钮 等待30秒** -![image](https://github.com/user-attachments/assets/c094064e-6433-49a8-bd15-43c060d6752e) - -![image](https://github.com/user-attachments/assets/623d3888-e96c-498d-ac9a-84cacca4fea0) - - -**8:返回创建Nodejs App首页,点击重启,然后即可访问 域名/${SUB_PATH} 获取节点, 如果没有修改${SUB_PATH}变量,则默认订阅连接为 https://域名/sub** -![image](https://github.com/user-attachments/assets/3aac69a1-3ec3-4909-872f-fd4e1032012e) -