zhizhi/server/proxy/service/subscription-server-v2.js

528 lines
17 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env node
// ═══════════════════════════════════════════════
// 🔺 Sovereign: TCS-0002∞ | Root: SYS-GLW-0001
// 📜 Copyright: 国作登字-2026-A-00037559
// ═══════════════════════════════════════════════
// server/proxy/service/subscription-server-v2.js
// 🌐 铸渊专线V2 · 多用户订阅服务
//
// 部署在大脑服务器 (ZY-SVR-005 · 43.156.237.110)
// 每个邮箱一条独立专线Token认证隔离
//
// 与V1的区别:
// V1: 单UUID · 单Token · 共享线路
// V2: 每人独立UUID · 独立Token · 独立流量统计
//
// 端口: 3803 (绑定127.0.0.1通过Nginx反代访问)
// ═══════════════════════════════════════════════
'use strict';
const http = require('http');
const fs = require('fs');
const path = require('path');
const url = require('url');
const PORT = process.env.ZY_PROXY_V2_PORT || 3803;
const PROXY_DIR = process.env.ZY_BRAIN_PROXY_DIR || '/opt/zhuyuan-brain/proxy';
const DATA_DIR = path.join(PROXY_DIR, 'data');
const KEYS_FILE = path.join(PROXY_DIR, '.env.keys');
// 引入用户管理器
const userManager = require('./user-manager');
// ── 加载密钥 ────────────────────────────────
function loadKeys() {
const keys = {};
try {
const content = fs.readFileSync(KEYS_FILE, 'utf8');
for (const line of content.split('\n')) {
if (line.startsWith('#') || !line.includes('=')) continue;
const [key, ...vals] = line.split('=');
keys[key.trim()] = vals.join('=').trim();
}
} catch (err) {
keys.ZY_PROXY_REALITY_PUBLIC_KEY = process.env.ZY_PROXY_REALITY_PUBLIC_KEY || '';
keys.ZY_PROXY_REALITY_SHORT_ID = process.env.ZY_PROXY_REALITY_SHORT_ID || '';
}
return keys;
}
// ── 获取服务器IP ────────────────────────────
function getServerHost() {
if (process.env.ZY_BRAIN_HOST) return process.env.ZY_BRAIN_HOST;
if (process.env.ZY_SERVER_HOST) return process.env.ZY_SERVER_HOST;
try {
const content = fs.readFileSync(KEYS_FILE, 'utf8');
for (const line of content.split('\n')) {
if (line.startsWith('#') || !line.includes('=')) continue;
const [key, ...vals] = line.split('=');
const k = key.trim();
if (k === 'ZY_BRAIN_HOST' || k === 'ZY_SERVER_HOST') {
const val = vals.join('=').trim();
if (val) return val;
}
}
} catch { /* ignore */ }
return '0.0.0.0';
}
// ── 生成subscription-userinfo头 ──────────────
function generateUserInfoHeader(user) {
const nextMonth = new Date();
nextMonth.setMonth(nextMonth.getMonth() + 1);
nextMonth.setDate(1);
nextMonth.setHours(0, 0, 0, 0);
return `upload=${user.traffic.upload_bytes}; download=${user.traffic.download_bytes}; total=${user.quota_bytes}; expire=${Math.floor(nextMonth.getTime() / 1000)}`;
}
// ── 生成VLESS URI (Shadowrocket) ─────────────
function generateVlessUri(user, keys, serverHost) {
const params = new URLSearchParams({
encryption: 'none',
flow: 'xtls-rprx-vision',
security: 'reality',
sni: 'www.microsoft.com',
fp: 'chrome',
pbk: keys.ZY_PROXY_REALITY_PUBLIC_KEY,
sid: keys.ZY_PROXY_REALITY_SHORT_ID,
type: 'tcp',
headerType: 'none'
});
const label = encodeURIComponent(`ZY-V2-${user.label}`);
return `vless://${user.uuid}@${serverHost}:443?${params.toString()}#${label}`;
}
// ── 生成Clash YAML配置 (用户专属) ────────────
function generateClashYaml(user, keys, serverHost) {
return `# 铸渊专线V2 · ${user.label} 的独立专线
# 自动生成 · ${new Date().toISOString()}
# ⚠️ 此配置为 ${user.email} 专属,请勿分享
# 每人一条独立线路,流量独立计算
# ── 全局设置 ──────────────────────────────
mixed-port: 7890
allow-lan: false
mode: rule
log-level: info
ipv6: false
unified-delay: true
tcp-concurrent: true
find-process-mode: strict
geodata-mode: true
geodata-loader: standard
global-client-fingerprint: chrome
keep-alive-interval: 30
external-controller: 127.0.0.1:9090
# ── GeoData 数据源 ────────────────────────
geox-url:
geoip: "https://cdn.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@release/geoip.dat"
geosite: "https://cdn.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@release/geosite.dat"
mmdb: "https://cdn.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@release/country.mmdb"
# ── DNS 设置 (fake-ip模式) ────────────────
dns:
enable: true
listen: 0.0.0.0:1053
ipv6: false
enhanced-mode: fake-ip
fake-ip-range: 198.18.0.1/16
fake-ip-filter:
- "*.lan"
- "*.local"
- "*.direct"
- "localhost.ptlogin2.qq.com"
- "dns.msftncsi.com"
- "*.msftconnecttest.com"
- "*.msftncsi.com"
- "+.stun.*.*"
- "+.stun.*.*.*"
- "lens.l.google.com"
- "stun.l.google.com"
- "time.*.com"
- "time.*.gov"
- "time.*.edu.cn"
- "time.*.apple.com"
- "time-ios.apple.com"
- "time-macos.apple.com"
- "ntp.*.com"
- "+.pool.ntp.org"
- "music.163.com"
- "*.music.163.com"
- "*.126.net"
default-nameserver:
- 223.5.5.5
- 119.29.29.29
- 1.0.0.1
nameserver:
- https://dns.alidns.com/dns-query
- https://doh.pub/dns-query
fallback:
- https://1.0.0.1/dns-query
- https://dns.google/dns-query
- tls://8.8.4.4:853
fallback-filter:
geoip: true
geoip-code: CN
geosite:
- gfw
ipcidr:
- 240.0.0.0/4
domain:
- "+.google.com"
- "+.facebook.com"
- "+.youtube.com"
- "+.github.com"
- "+.googleapis.com"
- "+.openai.com"
- "+.anthropic.com"
# ── 域名嗅探 ──────────────────────────────
sniffer:
enable: true
force-dns-mapping: true
parse-pure-ip: true
override-destination: true
sniff:
HTTP:
ports: [80, 8080-8880]
override-destination: true
TLS:
ports: [443, 8443]
QUIC:
ports: [443, 8443]
skip-domain:
- "Mijia Cloud"
- "+.push.apple.com"
# ── 代理节点 (${user.label} 专属) ─────────
proxies:
- name: "🏛️ 铸渊专线V2-${user.label}"
type: vless
server: ${serverHost}
port: 443
uuid: ${user.uuid}
network: tcp
tls: true
udp: true
flow: xtls-rprx-vision
skip-cert-verify: false
servername: www.microsoft.com
reality-opts:
public-key: ${keys.ZY_PROXY_REALITY_PUBLIC_KEY}
short-id: ${keys.ZY_PROXY_REALITY_SHORT_ID}
client-fingerprint: chrome
# ── 代理组 ────────────────────────────────
proxy-groups:
- name: "🌐 铸渊专线"
type: select
proxies:
- "🏛️ 铸渊专线V2-${user.label}"
- DIRECT
- name: "🤖 AI服务"
type: select
proxies:
- "🏛️ 铸渊专线V2-${user.label}"
- name: "💻 开发工具"
type: select
proxies:
- "🏛️ 铸渊专线V2-${user.label}"
# ── 路由规则 ──────────────────────────────
rules:
# AI服务
- DOMAIN-SUFFIX,openai.com,🤖 AI服务
- DOMAIN-SUFFIX,anthropic.com,🤖 AI服务
- DOMAIN-SUFFIX,claude.ai,🤖 AI服务
- DOMAIN-SUFFIX,chatgpt.com,🤖 AI服务
- DOMAIN-SUFFIX,gemini.google.com,🤖 AI服务
- DOMAIN-SUFFIX,perplexity.ai,🤖 AI服务
- DOMAIN-SUFFIX,poe.com,🤖 AI服务
# 开发工具
- DOMAIN-SUFFIX,github.com,💻 开发工具
- DOMAIN-SUFFIX,githubusercontent.com,💻 开发工具
- DOMAIN-SUFFIX,github.io,💻 开发工具
- DOMAIN-SUFFIX,githubassets.com,💻 开发工具
- DOMAIN-SUFFIX,copilot.microsoft.com,💻 开发工具
- DOMAIN-SUFFIX,npmjs.com,💻 开发工具
- DOMAIN-SUFFIX,docker.com,💻 开发工具
- DOMAIN-SUFFIX,docker.io,💻 开发工具
- DOMAIN-SUFFIX,stackoverflow.com,💻 开发工具
- DOMAIN-SUFFIX,pypi.org,💻 开发工具
# 社交媒体 & 流媒体
- DOMAIN-SUFFIX,tiktok.com,🌐 铸渊专线
- DOMAIN-SUFFIX,twitter.com,🌐 铸渊专线
- DOMAIN-SUFFIX,x.com,🌐 铸渊专线
- DOMAIN-SUFFIX,youtube.com,🌐 铸渊专线
- DOMAIN-SUFFIX,googlevideo.com,🌐 铸渊专线
- DOMAIN-SUFFIX,google.com,🌐 铸渊专线
- DOMAIN-SUFFIX,googleapis.com,🌐 铸渊专线
- DOMAIN-SUFFIX,telegram.org,🌐 铸渊专线
- DOMAIN-SUFFIX,t.me,🌐 铸渊专线
- DOMAIN-SUFFIX,instagram.com,🌐 铸渊专线
- DOMAIN-SUFFIX,facebook.com,🌐 铸渊专线
- DOMAIN-SUFFIX,whatsapp.com,🌐 铸渊专线
- DOMAIN-SUFFIX,wikipedia.org,🌐 铸渊专线
- DOMAIN-SUFFIX,reddit.com,🌐 铸渊专线
- DOMAIN-SUFFIX,netflix.com,🌐 铸渊专线
- DOMAIN-SUFFIX,spotify.com,🌐 铸渊专线
- DOMAIN-SUFFIX,discord.com,🌐 铸渊专线
# Apple
- DOMAIN-SUFFIX,apple.com,DIRECT
- DOMAIN-SUFFIX,icloud.com,DIRECT
# 国内直连
- DOMAIN-SUFFIX,cn,DIRECT
- DOMAIN-SUFFIX,taobao.com,DIRECT
- DOMAIN-SUFFIX,tmall.com,DIRECT
- DOMAIN-SUFFIX,alipay.com,DIRECT
- DOMAIN-SUFFIX,aliyun.com,DIRECT
- DOMAIN-SUFFIX,jd.com,DIRECT
- DOMAIN-SUFFIX,qq.com,DIRECT
- DOMAIN-SUFFIX,tencent.com,DIRECT
- DOMAIN-SUFFIX,bilibili.com,DIRECT
- DOMAIN-SUFFIX,baidu.com,DIRECT
- DOMAIN-SUFFIX,zhihu.com,DIRECT
- DOMAIN-SUFFIX,douyin.com,DIRECT
- DOMAIN-SUFFIX,weibo.com,DIRECT
# 局域网直连
- IP-CIDR,192.168.0.0/16,DIRECT
- IP-CIDR,10.0.0.0/8,DIRECT
- IP-CIDR,172.16.0.0/12,DIRECT
- IP-CIDR,127.0.0.0/8,DIRECT
# GeoIP中国直连
- GEOIP,CN,DIRECT
# 默认走代理
- MATCH,🌐 铸渊专线
`;
}
// ── 检测客户端类型 ───────────────────────────
function detectClientType(userAgent) {
const ua = (userAgent || '').toLowerCase();
if (ua.includes('clash') || ua.includes('mihomo') || ua.includes('stash')) return 'clash';
if (ua.includes('shadowrocket') || ua.includes('quantumult') || ua.includes('surge')) return 'base64';
return 'clash';
}
// ── HTTP服务器 ───────────────────────────────
const server = http.createServer((req, res) => {
try {
const parsedUrl = url.parse(req.url, true);
const pathname = parsedUrl.pathname;
// 健康检查
if (pathname === '/health') {
const users = userManager.getEnabledUsers();
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
status: 'ok',
service: 'zy-proxy-v2-subscription',
version: '2.0.0',
users_count: users.length,
server: 'ZY-SVR-005 · Brain'
}));
return;
}
// V2订阅端点: /sub/{token}
// token是每个用户独立的不会串线
const subMatch = pathname.match(/^\/sub\/([a-f0-9]+)$/);
if (subMatch) {
const token = subMatch[1];
const user = userManager.findUserByToken(token);
if (!user) {
res.writeHead(403, { 'Content-Type': 'text/plain' });
res.end('Forbidden');
return;
}
const keys = loadKeys();
const serverHost = getServerHost();
const clientType = detectClientType(req.headers['user-agent']);
const userInfoHeader = generateUserInfoHeader(user);
if (clientType === 'clash') {
const yaml = generateClashYaml(user, keys, serverHost);
res.writeHead(200, {
'Content-Type': 'text/yaml; charset=utf-8',
'Content-Disposition': `attachment; filename="zy-proxy-v2-${user.label}.yaml"`,
'subscription-userinfo': userInfoHeader,
'profile-update-interval': '6',
'profile-title': 'base64:' + Buffer.from(`铸渊专线V2·${user.label}`).toString('base64'),
});
res.end(yaml);
} else {
const vlessUri = generateVlessUri(user, keys, serverHost);
const encoded = Buffer.from(vlessUri).toString('base64');
res.writeHead(200, {
'Content-Type': 'text/plain; charset=utf-8',
'subscription-userinfo': userInfoHeader,
'profile-update-interval': '6',
});
res.end(encoded);
}
return;
}
// 用户配额查询: /quota/{token}
const quotaMatch = pathname.match(/^\/quota\/([a-f0-9]+)$/);
if (quotaMatch) {
const token = quotaMatch[1];
const user = userManager.findUserByToken(token);
if (!user) {
res.writeHead(403, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: true, code: 'FORBIDDEN', message: '认证失败' }));
return;
}
const totalGB = user.quota_bytes / (1024 ** 3);
const usedGB = (user.traffic.upload_bytes + user.traffic.download_bytes) / (1024 ** 3);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
email: user.email,
label: user.label,
total_gb: parseFloat(totalGB.toFixed(1)),
used_gb: parseFloat(usedGB.toFixed(2)),
remaining_gb: parseFloat((totalGB - usedGB).toFixed(2)),
percentage_used: parseFloat(((usedGB / totalGB) * 100).toFixed(1)),
period: user.traffic.period,
updated_at: new Date().toISOString()
}));
return;
}
// 用户状态: /status/{token}
const statusMatch = pathname.match(/^\/status\/([a-f0-9]+)$/);
if (statusMatch) {
const token = statusMatch[1];
const user = userManager.findUserByToken(token);
if (!user) {
res.writeHead(403, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: true, code: 'FORBIDDEN', message: '认证失败' }));
return;
}
const totalGB = user.quota_bytes / (1024 ** 3);
const usedGB = (user.traffic.upload_bytes + user.traffic.download_bytes) / (1024 ** 3);
const serverHost = getServerHost();
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
res.end(JSON.stringify({
user: {
email: user.email,
label: user.label,
enabled: user.enabled
},
server: {
status: 'online',
version: '2.0.0',
uptime_seconds: Math.floor(process.uptime()),
region: 'sg',
region_name: 'Singapore Zone 1',
server_code: 'ZY-SVR-005'
},
node: {
id: 'zy-brain-direct',
name: `🏛️ 铸渊专线V2-${user.label}`,
server: serverHost,
port: 443,
protocol: 'vless',
security: 'reality'
},
quota: {
total_gb: parseFloat(totalGB.toFixed(1)),
used_gb: parseFloat(usedGB.toFixed(2)),
remaining_gb: parseFloat((totalGB - usedGB).toFixed(2)),
percentage_used: parseFloat(((usedGB / totalGB) * 100).toFixed(1)),
period: user.traffic.period
},
updated_at: new Date().toISOString()
}));
return;
}
// 404
res.writeHead(404, { 'Content-Type': 'text/plain' });
res.end('Not Found');
} catch (err) {
console.error('❌ 请求处理错误 [%s %s]:', req.method, req.url, err.message || err);
try {
if (!res.headersSent) {
res.writeHead(500, { 'Content-Type': 'text/plain' });
}
res.end('Internal Server Error');
} catch (writeErr) {
console.error(' ⚠️ 响应写入失败:', writeErr.message);
}
}
});
server.on('error', (err) => {
console.error('❌ 服务器错误:', err.message);
if (err.code === 'EADDRINUSE') {
console.error(' 端口 %d 已被占用', PORT);
process.exit(1);
}
});
server.on('clientError', (err, socket) => {
console.error('⚠️ 客户端连接错误:', err.code || err.message);
if (socket.writable) {
socket.end('HTTP/1.1 400 Bad Request\r\n\r\n');
}
});
server.listen(PORT, '127.0.0.1', () => {
const users = userManager.getEnabledUsers();
console.log(`🌐 铸渊专线V2订阅服务已启动: http://127.0.0.1:${PORT}`);
console.log(` 版本: V2.0 (多用户独立专线)`);
console.log(` 用户数: ${users.length}`);
console.log(` 服务器: ZY-SVR-005 · Brain`);
console.log(` ──────────────────────────`);
console.log(` 订阅端点: /sub/{user_token}`);
console.log(` 配额查询: /quota/{user_token}`);
console.log(` 服务状态: /status/{user_token}`);
console.log(` 健康检查: /health`);
});
// Graceful shutdown
function gracefulShutdown(signal) {
console.log(`\n${signal} received. Shutting down gracefully...`);
const forceExit = setTimeout(() => { process.exit(1); }, 5000);
server.close(() => {
clearTimeout(forceExit);
console.log('Server closed.');
process.exit(0);
});
}
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
process.on('uncaughtException', (err) => {
console.error('❌ 未捕获的异常:', err.message);
console.error(err.stack);
gracefulShutdown('uncaughtException');
});
process.on('unhandledRejection', (reason) => {
console.error('❌ 未处理的Promise拒绝:', reason);
});