Files

95 lines
4.8 KiB
JavaScript

// WebSocket permessage-deflate (server/http/websocket.js WS_DEFLATE, host bandwidth layer 0): a browser-like client
// negotiates it with no server context takeover and a 2 KB server window, a large JSON frame goes out compressed, a
// frame under the 1 KB threshold and a client without the extension get plain frames, and an offer that caps the server
// window below ours is answered without compression instead of a refused handshake.
import { test, describe, before, after } from 'node:test';
import assert from 'node:assert/strict';
import http from 'node:http';
import WebSocket from 'ws';
import { attachWebSocket, narrowDeflateOffer, WS_DEFLATE } from '../server/http/websocket.js';
import { startServer } from '../server/index.js';
const BIG = JSON.stringify({ t: 'm.public', players: Array.from({ length: 200 }, (_, i) => ({ playerId: `p_${i % 4}`, name: '阿米娅', lp: 30, phase: 'PREP' })) });
const SMALL = JSON.stringify({ t: 'pong', at: 12345 });
/** A bare http server whose /ws sends BIG then SMALL to every socket (a stand-in for net.js Network). */
async function fakeServer() {
const server = http.createServer((req, res) => { res.writeHead(404); res.end(); });
const network = {
admission: () => null,
handleConnection: (ws) => { ws.send(BIG); ws.send(SMALL); },
};
const wss = attachWebSocket(server, { network, log: { error() {} } });
await new Promise((r) => server.listen(0, '127.0.0.1', r));
return { url: `ws://127.0.0.1:${server.address().port}/ws`, close: () => new Promise((r) => { wss.close(); server.close(() => r()); }) };
}
/** Connect, collect two messages, report the server's Sec-WebSocket-Extensions answer and the bytes the socket read. */
function receive(url, opts) {
return new Promise((resolve, reject) => {
const ws = new WebSocket(url, opts);
const msgs = [];
let negotiated = '';
ws.on('upgrade', (res) => { negotiated = res.headers['sec-websocket-extensions'] || ''; });
ws.on('error', reject);
ws.on('message', (data) => {
msgs.push(String(data));
if (msgs.length === 2) {
const read = ws._socket.bytesRead;
ws.close();
resolve({ msgs, read, extensions: negotiated });
}
});
});
}
describe('WebSocket permessage-deflate', () => {
let srv;
before(async () => { srv = await fakeServer(); });
after(async () => { await srv.close(); });
test('negotiated with no server context takeover and a small server window; a large frame is compressed', async () => {
const r = await receive(srv.url);
assert.match(r.extensions, /permessage-deflate/);
assert.match(r.extensions, /server_no_context_takeover/);
assert.match(r.extensions, new RegExp(`server_max_window_bits=${WS_DEFLATE.serverMaxWindowBits}`));
assert.deepEqual(r.msgs, [BIG, SMALL], 'the payloads arrive intact');
assert.ok(BIG.length > 10000);
assert.ok(r.read < BIG.length / 4, `compressed on the wire: ${r.read} bytes read for ${BIG.length} of payload`);
assert.equal(WS_DEFLATE.threshold, 1024);
});
test('a client without the extension gets the same messages uncompressed', async () => {
const r = await receive(srv.url, { perMessageDeflate: false });
assert.equal(r.extensions, '');
assert.deepEqual(r.msgs, [BIG, SMALL]);
assert.ok(r.read > BIG.length + SMALL.length, `plain frames: ${r.read}`);
});
test('an offer that caps the server window below ours connects uncompressed instead of a refused handshake', async () => {
const r = await receive(srv.url, { perMessageDeflate: { serverMaxWindowBits: 9 } });
assert.equal(r.extensions, '');
assert.deepEqual(r.msgs, [BIG, SMALL]);
assert.equal(narrowDeflateOffer('permessage-deflate; server_max_window_bits=9'), true);
assert.equal(narrowDeflateOffer('permessage-deflate; server_max_window_bits=12; client_max_window_bits'), false);
assert.equal(narrowDeflateOffer('permessage-deflate; server_max_window_bits=9, permessage-deflate'), false, 'another offer is acceptable');
assert.equal(narrowDeflateOffer('permessage-deflate; client_max_window_bits'), false);
assert.equal(narrowDeflateOffer(undefined), false);
});
});
test('the real server negotiates compression at /ws and keeps the 64 KB inflated frame limit', async () => {
const srv = await startServer({ port: 0, host: '127.0.0.1', quiet: true });
try {
const ws = new WebSocket(`ws://127.0.0.1:${srv.port}/ws`);
await new Promise((resolve, reject) => { ws.once('open', resolve); ws.once('error', reject); });
assert.match(ws.extensions, /permessage-deflate/);
// a compressed client frame whose inflated size is over the limit closes the socket (1009), like a plain one
const closed = new Promise((resolve) => ws.once('close', (code) => resolve(code)));
ws.send(JSON.stringify({ t: 'hello', name: 'x'.repeat(70 * 1024) }));
assert.equal(await closed, 1009);
} finally {
await srv.close();
}
});