81 lines
3 KiB
JavaScript
81 lines
3 KiB
JavaScript
// 铸渊专线订阅服务 · 流量计数 + 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));
|