zhuyuan-ice-gl-zy001/TRAE-DEV-20260820/outputs-vpn-20260823/deploy-zy-sub-service/server.js

81 lines
3 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.

// 铸渊专线订阅服务 · 流量计数 + subscription-userinfo 头
// 部署:BS-GZ-006 · 2026-08-23 · 配额 1000GiB/自然月重置
// 计数源:iptables 计数规则(relay 端口 8443-8446 进出字节)
const http = require('http');
const fs = require('fs');
const path = require('path');
const { execFile } = require('child_process');
const PORT = 3944;
const DATA_DIR = '/var/www/guanghulab-front-door/zy-sub';
const STATE_FILE = '/opt/zy-sub-service/state.json';
const TOTAL_QUOTA = 1000 * 1024 * 1024 * 1024;
let state = { month: '', upload: 0, download: 0, lastIn: 0, lastOut: 0 };
try { Object.assign(state, JSON.parse(fs.readFileSync(STATE_FILE, 'utf8'))); } catch {}
function monthKey(d = new Date()) {
return d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0');
}
function save() {
try { fs.writeFileSync(STATE_FILE, JSON.stringify(state)); }
catch (e) { console.error('save fail:', e.message); }
}
function readCounter(chain, tag, cb) {
execFile('iptables-save', ['-c'], (err, out) => {
let bytes = 0;
if (!err) {
for (const line of out.split('\n')) {
if (line.includes('zy-sub-acct-' + tag) && line.includes('-A ' + chain + ' ')) {
const m = line.match(/^\[(\d+):(\d+)\]/);
if (m) bytes += parseInt(m[2], 10);
}
}
}
cb(bytes);
});
}
function poll() {
readCounter('INPUT', 'in', (inB) => {
readCounter('OUTPUT', 'out', (outB) => {
const mk = monthKey();
if (state.month !== mk) { state.month = mk; state.upload = 0; state.download = 0; }
if (inB < state.lastIn) state.lastIn = inB; // 计数器重启归零时对齐
if (outB < state.lastOut) state.lastOut = outB;
state.upload += inB - state.lastIn;
state.download += outB - state.lastOut;
state.lastIn = inB; state.lastOut = outB;
save();
});
});
}
setInterval(poll, 60 * 1000);
poll();
const used = () => state.upload + state.download;
const userinfo = () =>
`upload=${state.upload}; download=${state.download}; total=${TOTAL_QUOTA}; expire=0`;
http.createServer((req, res) => {
const url = req.url.split('?')[0];
const m = url.match(/^\/zy-sub\/([0-9a-f]{16,64})\/(clash\.yaml|shadowrocket\.txt|status)$/);
if (!m) { res.writeHead(404); return res.end('not found'); }
if (m[2] === 'status') {
res.writeHead(200, { 'Content-Type': 'application/json' });
return res.end(JSON.stringify({
month: state.month, upload: state.upload, download: state.download,
total: TOTAL_QUOTA, used: used(), remaining: Math.max(0, TOTAL_QUOTA - used())
}, null, 2));
}
fs.readFile(path.join(DATA_DIR, m[1], m[2]), (err, data) => {
if (err) { res.writeHead(404); return res.end('not found'); }
res.writeHead(200, {
'Content-Type': m[2].endsWith('.yaml') ? 'text/yaml; charset=utf-8' : 'text/plain; charset=utf-8',
'subscription-userinfo': userinfo(),
'profile-update-interval': '24'
});
res.end(data);
});
}).listen(PORT, '127.0.0.1', () => console.log('zy-sub-service on', PORT));