Files

138 lines
7.0 KiB
JavaScript

// The server's traffic counters (server/http/traffic.js, GET /healthz?traffic=1; host bandwidth layer 0): the body
// bytes every HTTP response wrote, by category, 304s apart, and the WebSocket's wire and payload bytes both ways.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import http from 'node:http';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { randomBytes } from 'node:crypto';
import { startServer } from '../server/index.js';
import { createTrafficCounter } from '../server/http/traffic.js';
import { TestClient } from './helpers/wsClient.js';
function get(port, p, headers = {}) {
return new Promise((resolve, reject) => {
const req = http.request({ host: '127.0.0.1', port, path: p, headers, agent: false }, (res) => {
const chunks = [];
res.on('data', (c) => chunks.push(c));
res.on('end', () => resolve({ status: res.statusCode, headers: res.headers, body: Buffer.concat(chunks) }));
});
req.on('error', reject);
req.end();
});
}
test('HTTP: body bytes by category as written (compressed), 304s counted, /healthz reports them only when asked', async () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'sp-traffic-'));
const w = (rel, content) => { fs.mkdirSync(path.dirname(path.join(tmp, rel)), { recursive: true }); fs.writeFileSync(path.join(tmp, rel), content); };
const js = `// app\n${'export const x = 1;\n'.repeat(400)}`;
const png = randomBytes(3000);
const mp3 = randomBytes(5000);
w('public/index.html', '<!doctype html><p>hi</p>');
w('public/js/app.js', js);
w('public/assets/img/a.png', png);
w('public/assets/audio/bgm/a.mp3', mp3);
w('data/config.json', JSON.stringify({ a: 1 }));
w('shared/x.js', 'export const y = 2;');
const srv = await startServer({ port: 0, host: '127.0.0.1', quiet: true, publicDir: path.join(tmp, 'public'), dataDir: path.join(tmp, 'data'), sharedDir: path.join(tmp, 'shared') });
try {
const plain = await get(srv.port, '/health');
assert.equal(plain.status, 404);
const gz = await get(srv.port, '/js/app.js', { 'accept-encoding': 'gzip' });
assert.ok(gz.body.length < js.length, 'compressed');
await get(srv.port, '/js/app.js', { 'if-none-match': (await get(srv.port, '/js/app.js')).headers.etag });
await get(srv.port, '/assets/img/a.png');
await get(srv.port, '/media/bgm/a');
await get(srv.port, '/data/config.json');
const before = await get(srv.port, '/healthz');
assert.equal(JSON.parse(before.body).traffic, undefined, 'plain /healthz stays as it was');
const t = JSON.parse((await get(srv.port, '/healthz?traffic=1')).body).traffic;
assert.equal(t.http.code.requests, 3);
assert.equal(t.http.code.bytes, gz.body.length + js.length, 'what was written: the gzip body, the plain body, nothing for the 304');
assert.equal(t.http.code.notModified, 1);
assert.deepEqual(t.http.assets, { requests: 1, bytes: png.length, notModified: 0 });
assert.deepEqual(t.http.audio, { requests: 1, bytes: mp3.length, notModified: 0 });
assert.equal(t.http.data.requests, 1);
assert.equal(t.http.other.requests, 2, 'the 404 page and the first /healthz');
assert.ok(t.http.other.bytes > 0);
assert.equal(t.http.total.requests, 8);
assert.equal(typeof t.since, 'string');
assert.equal(srv.traffic.snapshot().http.total.requests, 9, 'the traffic request itself once it has finished');
} finally {
await srv.close();
fs.rmSync(tmp, { recursive: true, force: true });
}
});
test('WebSocket: wire bytes both ways (after permessage-deflate) and the payloads, live and after the close', async () => {
const srv = await startServer({ port: 0, host: '127.0.0.1', quiet: true });
try {
const c = await TestClient.connect(`ws://127.0.0.1:${srv.port}/ws`);
await c.hello('流量计数');
const live = srv.traffic.snapshot().ws;
assert.equal(live.connections, 1);
assert.equal(live.open, 1);
assert.ok(live.messagesOut >= 1 && live.payloadOut > 0, JSON.stringify(live));
assert.ok(live.messagesIn >= 1 && live.payloadIn > 0);
assert.ok(live.wireOut > 0 && live.wireIn > 0);
await c.close();
for (let i = 0; i < 50 && srv.traffic.snapshot().ws.open; i++) await new Promise((r) => setTimeout(r, 20));
const done = srv.traffic.snapshot().ws;
assert.equal(done.open, 0);
assert.ok(done.wireOut >= live.wireOut, 'the closed socket\'s bytes stay in the totals');
} finally {
await srv.close();
}
});
test('the counter on its own: a string body in another encoding, an aborted response, the payload of a fragmented message', () => {
const tc = createTrafficCounter({ now: () => 1000 });
const listeners = {};
const res = { statusCode: 200, write() { return true; }, end() {}, once(ev, fn) { listeners[ev] = fn; } };
tc.meter(res, '/data/x.json');
res.write('aGk=', 'base64');
res.end('é');
listeners.close();
const s = tc.snapshot();
assert.deepEqual(s.http.data, { requests: 1, bytes: 2 + 2, notModified: 0 });
tc.wsSent('{"t":"pong"}');
tc.wsReceived([Buffer.alloc(3), Buffer.alloc(4)]);
const ws = tc.snapshot().ws;
assert.equal(ws.payloadOut, 12);
assert.equal(ws.payloadIn, 7);
assert.equal(tc.snapshot().seconds, 0);
});
test('/healthz?traffic=1 answers the counters to a loopback / LAN client only (maintainer, 2026-10-12)', async () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'sp-traffic-lan-'));
fs.mkdirSync(path.join(tmp, 'public'), { recursive: true });
fs.mkdirSync(path.join(tmp, 'data'), { recursive: true });
fs.mkdirSync(path.join(tmp, 'shared'), { recursive: true });
fs.writeFileSync(path.join(tmp, 'public/index.html'), '<!doctype html><p>hi</p>');
const dirs = { publicDir: path.join(tmp, 'public'), dataDir: path.join(tmp, 'data'), sharedDir: path.join(tmp, 'shared') };
const has = async (port, headers) => {
const r = await get(port, '/healthz?traffic=1', headers);
assert.equal(r.status, 200);
const body = JSON.parse(r.body);
assert.equal(body.ok, true, 'the plain report still answers');
return 'traffic' in body;
};
const auto = await startServer({ port: 0, host: '127.0.0.1', quiet: true, ...dirs });
try {
assert.equal(await has(auto.port), true, 'the host itself (loopback)');
assert.equal(await has(auto.port, { 'x-forwarded-for': '192.168.1.20' }), true, 'a LAN client behind a local proxy');
assert.equal(await has(auto.port, { 'x-forwarded-for': '100.101.102.103' }), true, 'a virtual-LAN (CGNAT range) address');
assert.equal(await has(auto.port, { 'x-forwarded-for': '203.0.113.9' }), false, 'a public visitor through a tunnel / proxy on this machine');
assert.equal(await has(auto.port, { 'cf-connecting-ip': '2001:db8::7' }), false, 'a public IPv6 visitor through cloudflared');
} finally {
await auto.close();
}
const ignore = await startServer({ port: 0, host: '127.0.0.1', quiet: true, trustProxy: false, ...dirs });
try {
assert.equal(await has(ignore.port, { 'x-forwarded-for': '203.0.113.9' }), true, 'TRUST_PROXY=0: the loopback peer itself counts');
} finally {
await ignore.close();
}
});