summaryrefslogtreecommitdiffstats
path: root/testing/xpcshell/node-ws/test
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--testing/xpcshell/node-ws/test/autobahn-server.js17
-rw-r--r--testing/xpcshell/node-ws/test/autobahn.js39
-rw-r--r--testing/xpcshell/node-ws/test/buffer-util.test.js15
-rw-r--r--testing/xpcshell/node-ws/test/create-websocket-stream.test.js598
-rw-r--r--testing/xpcshell/node-ws/test/event-target.test.js253
-rw-r--r--testing/xpcshell/node-ws/test/extension.test.js190
-rw-r--r--testing/xpcshell/node-ws/test/fixtures/ca-certificate.pem12
-rw-r--r--testing/xpcshell/node-ws/test/fixtures/ca-key.pem5
-rw-r--r--testing/xpcshell/node-ws/test/fixtures/certificate.pem12
-rw-r--r--testing/xpcshell/node-ws/test/fixtures/client-certificate.pem12
-rw-r--r--testing/xpcshell/node-ws/test/fixtures/client-key.pem5
-rw-r--r--testing/xpcshell/node-ws/test/fixtures/key.pem5
-rw-r--r--testing/xpcshell/node-ws/test/limiter.test.js41
-rw-r--r--testing/xpcshell/node-ws/test/permessage-deflate.test.js647
-rw-r--r--testing/xpcshell/node-ws/test/receiver.test.js1086
-rw-r--r--testing/xpcshell/node-ws/test/sender.test.js370
-rw-r--r--testing/xpcshell/node-ws/test/subprotocol.test.js91
-rw-r--r--testing/xpcshell/node-ws/test/validation.test.js52
-rw-r--r--testing/xpcshell/node-ws/test/websocket-server.test.js1284
-rw-r--r--testing/xpcshell/node-ws/test/websocket.integration.js55
-rw-r--r--testing/xpcshell/node-ws/test/websocket.test.js4514
21 files changed, 9303 insertions, 0 deletions
diff --git a/testing/xpcshell/node-ws/test/autobahn-server.js b/testing/xpcshell/node-ws/test/autobahn-server.js
new file mode 100644
index 0000000000..24ade11497
--- /dev/null
+++ b/testing/xpcshell/node-ws/test/autobahn-server.js
@@ -0,0 +1,17 @@
+'use strict';
+
+const WebSocket = require('../');
+
+const port = process.argv.length > 2 ? parseInt(process.argv[2]) : 9001;
+const wss = new WebSocket.Server({ port }, () => {
+ console.log(
+ `Listening to port ${port}. Use extra argument to define the port`
+ );
+});
+
+wss.on('connection', (ws) => {
+ ws.on('message', (data, isBinary) => {
+ ws.send(data, { binary: isBinary });
+ });
+ ws.on('error', (e) => console.error(e));
+});
diff --git a/testing/xpcshell/node-ws/test/autobahn.js b/testing/xpcshell/node-ws/test/autobahn.js
new file mode 100644
index 0000000000..51532fc52e
--- /dev/null
+++ b/testing/xpcshell/node-ws/test/autobahn.js
@@ -0,0 +1,39 @@
+'use strict';
+
+const WebSocket = require('../');
+
+let currentTest = 1;
+let testCount;
+
+function nextTest() {
+ let ws;
+
+ if (currentTest > testCount) {
+ ws = new WebSocket('ws://localhost:9001/updateReports?agent=ws');
+ return;
+ }
+
+ console.log(`Running test case ${currentTest}/${testCount}`);
+
+ ws = new WebSocket(
+ `ws://localhost:9001/runCase?case=${currentTest}&agent=ws`
+ );
+ ws.on('message', (data, isBinary) => {
+ ws.send(data, { binary: isBinary });
+ });
+ ws.on('close', () => {
+ currentTest++;
+ process.nextTick(nextTest);
+ });
+ ws.on('error', (e) => console.error(e));
+}
+
+const ws = new WebSocket('ws://localhost:9001/getCaseCount');
+ws.on('message', (data) => {
+ testCount = parseInt(data);
+});
+ws.on('close', () => {
+ if (testCount > 0) {
+ nextTest();
+ }
+});
diff --git a/testing/xpcshell/node-ws/test/buffer-util.test.js b/testing/xpcshell/node-ws/test/buffer-util.test.js
new file mode 100644
index 0000000000..a6b84c94b1
--- /dev/null
+++ b/testing/xpcshell/node-ws/test/buffer-util.test.js
@@ -0,0 +1,15 @@
+'use strict';
+
+const assert = require('assert');
+
+const { concat } = require('../lib/buffer-util');
+
+describe('bufferUtil', () => {
+ describe('concat', () => {
+ it('never returns uninitialized data', () => {
+ const buf = concat([Buffer.from([1, 2]), Buffer.from([3, 4])], 6);
+
+ assert.ok(buf.equals(Buffer.from([1, 2, 3, 4])));
+ });
+ });
+});
diff --git a/testing/xpcshell/node-ws/test/create-websocket-stream.test.js b/testing/xpcshell/node-ws/test/create-websocket-stream.test.js
new file mode 100644
index 0000000000..4d51958cd9
--- /dev/null
+++ b/testing/xpcshell/node-ws/test/create-websocket-stream.test.js
@@ -0,0 +1,598 @@
+'use strict';
+
+const assert = require('assert');
+const EventEmitter = require('events');
+const { createServer } = require('http');
+const { Duplex } = require('stream');
+const { randomBytes } = require('crypto');
+
+const createWebSocketStream = require('../lib/stream');
+const Sender = require('../lib/sender');
+const WebSocket = require('..');
+const { EMPTY_BUFFER } = require('../lib/constants');
+
+describe('createWebSocketStream', () => {
+ it('is exposed as a property of the `WebSocket` class', () => {
+ assert.strictEqual(WebSocket.createWebSocketStream, createWebSocketStream);
+ });
+
+ it('returns a `Duplex` stream', () => {
+ const duplex = createWebSocketStream(new EventEmitter());
+
+ assert.ok(duplex instanceof Duplex);
+ });
+
+ it('passes the options object to the `Duplex` constructor', (done) => {
+ const wss = new WebSocket.Server({ port: 0 }, () => {
+ const ws = new WebSocket(`ws://localhost:${wss.address().port}`);
+ const duplex = createWebSocketStream(ws, {
+ allowHalfOpen: false,
+ encoding: 'utf8'
+ });
+
+ duplex.on('data', (chunk) => {
+ assert.strictEqual(chunk, 'hi');
+
+ duplex.on('close', () => {
+ wss.close(done);
+ });
+ });
+ });
+
+ wss.on('connection', (ws) => {
+ ws.send(Buffer.from('hi'));
+ ws.close();
+ });
+ });
+
+ describe('The returned stream', () => {
+ it('buffers writes if `readyState` is `CONNECTING`', (done) => {
+ const chunk = randomBytes(1024);
+ const wss = new WebSocket.Server({ port: 0 }, () => {
+ const ws = new WebSocket(`ws://localhost:${wss.address().port}`);
+
+ assert.strictEqual(ws.readyState, WebSocket.CONNECTING);
+
+ const duplex = createWebSocketStream(ws);
+
+ duplex.write(chunk);
+ });
+
+ wss.on('connection', (ws) => {
+ ws.on('message', (message, isBinary) => {
+ ws.on('close', (code, reason) => {
+ assert.deepStrictEqual(message, chunk);
+ assert.ok(isBinary);
+ assert.strictEqual(code, 1005);
+ assert.strictEqual(reason, EMPTY_BUFFER);
+ wss.close(done);
+ });
+ });
+
+ ws.close();
+ });
+ });
+
+ it('errors if a write occurs when `readyState` is `CLOSING`', (done) => {
+ const wss = new WebSocket.Server({ port: 0 }, () => {
+ const ws = new WebSocket(`ws://localhost:${wss.address().port}`);
+ const duplex = createWebSocketStream(ws);
+
+ duplex.on('error', (err) => {
+ assert.ok(duplex.destroyed);
+ assert.ok(err instanceof Error);
+ assert.strictEqual(
+ err.message,
+ 'WebSocket is not open: readyState 2 (CLOSING)'
+ );
+
+ duplex.on('close', () => {
+ wss.close(done);
+ });
+ });
+
+ ws.on('open', () => {
+ ws._receiver.on('conclude', () => {
+ duplex.write('hi');
+ });
+ });
+ });
+
+ wss.on('connection', (ws) => {
+ ws.close();
+ });
+ });
+
+ it('errors if a write occurs when `readyState` is `CLOSED`', (done) => {
+ const wss = new WebSocket.Server({ port: 0 }, () => {
+ const ws = new WebSocket(`ws://localhost:${wss.address().port}`);
+ const duplex = createWebSocketStream(ws);
+
+ duplex.on('error', (err) => {
+ assert.ok(duplex.destroyed);
+ assert.ok(err instanceof Error);
+ assert.strictEqual(
+ err.message,
+ 'WebSocket is not open: readyState 3 (CLOSED)'
+ );
+
+ duplex.on('close', () => {
+ wss.close(done);
+ });
+ });
+
+ ws.on('close', () => {
+ duplex.write('hi');
+ });
+ });
+
+ wss.on('connection', (ws) => {
+ ws.close();
+ });
+ });
+
+ it('does not error if `_final()` is called while connecting', (done) => {
+ const wss = new WebSocket.Server({ port: 0 }, () => {
+ const ws = new WebSocket(`ws://localhost:${wss.address().port}`);
+
+ assert.strictEqual(ws.readyState, WebSocket.CONNECTING);
+
+ const duplex = createWebSocketStream(ws);
+
+ duplex.on('close', () => {
+ wss.close(done);
+ });
+
+ duplex.resume();
+ duplex.end();
+ });
+ });
+
+ it('makes `_final()` a noop if no socket is assigned', (done) => {
+ const server = createServer();
+
+ server.on('upgrade', (request, socket) => {
+ socket.on('end', socket.end);
+
+ const headers = [
+ 'HTTP/1.1 101 Switching Protocols',
+ 'Upgrade: websocket',
+ 'Connection: Upgrade',
+ 'Sec-WebSocket-Accept: foo'
+ ];
+
+ socket.write(headers.concat('\r\n').join('\r\n'));
+ });
+
+ server.listen(() => {
+ const called = [];
+ const ws = new WebSocket(`ws://localhost:${server.address().port}`);
+ const duplex = WebSocket.createWebSocketStream(ws);
+ const final = duplex._final;
+
+ duplex._final = (callback) => {
+ called.push('final');
+ assert.strictEqual(ws.readyState, WebSocket.CLOSING);
+ assert.strictEqual(ws._socket, null);
+
+ final(callback);
+ };
+
+ duplex.on('error', (err) => {
+ called.push('error');
+ assert.ok(err instanceof Error);
+ assert.strictEqual(
+ err.message,
+ 'Invalid Sec-WebSocket-Accept header'
+ );
+ });
+
+ duplex.on('finish', () => {
+ called.push('finish');
+ });
+
+ duplex.on('close', () => {
+ assert.deepStrictEqual(called, ['final', 'error']);
+ server.close(done);
+ });
+
+ ws.on('upgrade', () => {
+ process.nextTick(() => {
+ duplex.end();
+ });
+ });
+ });
+ });
+
+ it('reemits errors', (done) => {
+ let duplexCloseEventEmitted = false;
+ let serverClientCloseEventEmitted = false;
+
+ const wss = new WebSocket.Server({ port: 0 }, () => {
+ const ws = new WebSocket(`ws://localhost:${wss.address().port}`);
+ const duplex = createWebSocketStream(ws);
+
+ duplex.on('error', (err) => {
+ assert.ok(err instanceof RangeError);
+ assert.strictEqual(err.code, 'WS_ERR_INVALID_OPCODE');
+ assert.strictEqual(
+ err.message,
+ 'Invalid WebSocket frame: invalid opcode 5'
+ );
+
+ duplex.on('close', () => {
+ duplexCloseEventEmitted = true;
+ if (serverClientCloseEventEmitted) wss.close(done);
+ });
+ });
+ });
+
+ wss.on('connection', (ws) => {
+ ws._socket.write(Buffer.from([0x85, 0x00]));
+ ws.on('close', (code, reason) => {
+ assert.strictEqual(code, 1002);
+ assert.deepStrictEqual(reason, EMPTY_BUFFER);
+
+ serverClientCloseEventEmitted = true;
+ if (duplexCloseEventEmitted) wss.close(done);
+ });
+ });
+ });
+
+ it('does not swallow errors that may occur while destroying', (done) => {
+ const frame = Buffer.concat(
+ Sender.frame(Buffer.from([0x22, 0xfa, 0xec, 0x78]), {
+ fin: true,
+ rsv1: true,
+ opcode: 0x02,
+ mask: false,
+ readOnly: false
+ })
+ );
+
+ const wss = new WebSocket.Server(
+ {
+ perMessageDeflate: true,
+ port: 0
+ },
+ () => {
+ const ws = new WebSocket(`ws://localhost:${wss.address().port}`);
+ const duplex = createWebSocketStream(ws);
+
+ duplex.on('error', (err) => {
+ assert.ok(err instanceof Error);
+ assert.strictEqual(err.code, 'Z_DATA_ERROR');
+ assert.strictEqual(err.errno, -3);
+
+ duplex.on('close', () => {
+ wss.close(done);
+ });
+ });
+
+ let bytesRead = 0;
+
+ ws.on('open', () => {
+ ws._socket.on('data', (chunk) => {
+ bytesRead += chunk.length;
+ if (bytesRead === frame.length) duplex.destroy();
+ });
+ });
+ }
+ );
+
+ wss.on('connection', (ws) => {
+ ws._socket.write(frame);
+ });
+ });
+
+ it("does not suppress the throwing behavior of 'error' events", (done) => {
+ const wss = new WebSocket.Server({ port: 0 }, () => {
+ const ws = new WebSocket(`ws://localhost:${wss.address().port}`);
+ createWebSocketStream(ws);
+ });
+
+ wss.on('connection', (ws) => {
+ ws._socket.write(Buffer.from([0x85, 0x00]));
+ });
+
+ assert.strictEqual(process.listenerCount('uncaughtException'), 1);
+
+ const [listener] = process.listeners('uncaughtException');
+
+ process.removeAllListeners('uncaughtException');
+ process.once('uncaughtException', (err) => {
+ assert.ok(err instanceof Error);
+ assert.strictEqual(
+ err.message,
+ 'Invalid WebSocket frame: invalid opcode 5'
+ );
+
+ process.on('uncaughtException', listener);
+ wss.close(done);
+ });
+ });
+
+ it("is destroyed after 'end' and 'finish' are emitted (1/2)", (done) => {
+ const wss = new WebSocket.Server({ port: 0 }, () => {
+ const events = [];
+ const ws = new WebSocket(`ws://localhost:${wss.address().port}`);
+ const duplex = createWebSocketStream(ws);
+
+ duplex.on('end', () => {
+ events.push('end');
+ assert.ok(duplex.destroyed);
+ });
+
+ duplex.on('close', () => {
+ assert.deepStrictEqual(events, ['finish', 'end']);
+ wss.close(done);
+ });
+
+ duplex.on('finish', () => {
+ events.push('finish');
+ assert.ok(!duplex.destroyed);
+ assert.ok(duplex.readable);
+
+ duplex.resume();
+ });
+
+ ws.on('close', () => {
+ duplex.end();
+ });
+ });
+
+ wss.on('connection', (ws) => {
+ ws.send('foo');
+ ws.close();
+ });
+ });
+
+ it("is destroyed after 'end' and 'finish' are emitted (2/2)", (done) => {
+ const wss = new WebSocket.Server({ port: 0 }, () => {
+ const events = [];
+ const ws = new WebSocket(`ws://localhost:${wss.address().port}`);
+ const duplex = createWebSocketStream(ws);
+
+ duplex.on('end', () => {
+ events.push('end');
+ assert.ok(!duplex.destroyed);
+ assert.ok(duplex.writable);
+
+ duplex.end();
+ });
+
+ duplex.on('close', () => {
+ assert.deepStrictEqual(events, ['end', 'finish']);
+ wss.close(done);
+ });
+
+ duplex.on('finish', () => {
+ events.push('finish');
+ });
+
+ duplex.resume();
+ });
+
+ wss.on('connection', (ws) => {
+ ws.close();
+ });
+ });
+
+ it('handles backpressure (1/3)', (done) => {
+ const wss = new WebSocket.Server({ port: 0 }, () => {
+ // eslint-disable-next-line no-unused-vars
+ const ws = new WebSocket(`ws://localhost:${wss.address().port}`);
+ });
+
+ wss.on('connection', (ws) => {
+ const duplex = createWebSocketStream(ws);
+
+ duplex.resume();
+
+ duplex.on('drain', () => {
+ duplex.on('close', () => {
+ wss.close(done);
+ });
+
+ duplex.end();
+ });
+
+ const chunk = randomBytes(1024);
+ let ret;
+
+ do {
+ ret = duplex.write(chunk);
+ } while (ret !== false);
+ });
+ });
+
+ it('handles backpressure (2/3)', (done) => {
+ const wss = new WebSocket.Server(
+ { port: 0, perMessageDeflate: true },
+ () => {
+ const called = [];
+ const ws = new WebSocket(`ws://localhost:${wss.address().port}`);
+ const duplex = createWebSocketStream(ws);
+ const read = duplex._read;
+
+ duplex._read = () => {
+ duplex._read = read;
+ called.push('read');
+ assert.ok(ws._receiver._writableState.needDrain);
+ read();
+ assert.ok(ws._socket.isPaused());
+ };
+
+ ws.on('open', () => {
+ ws._socket.on('pause', () => {
+ duplex.resume();
+ });
+
+ ws._receiver.on('drain', () => {
+ called.push('drain');
+ assert.ok(!ws._socket.isPaused());
+ duplex.end();
+ });
+
+ const opts = {
+ fin: true,
+ opcode: 0x02,
+ mask: false,
+ readOnly: false
+ };
+
+ const list = [
+ ...Sender.frame(randomBytes(16 * 1024), { rsv1: false, ...opts }),
+ ...Sender.frame(Buffer.alloc(1), { rsv1: true, ...opts })
+ ];
+
+ // This hack is used because there is no guarantee that more than
+ // 16 KiB will be sent as a single TCP packet.
+ ws._socket.push(Buffer.concat(list));
+ });
+
+ duplex.on('close', () => {
+ assert.deepStrictEqual(called, ['read', 'drain']);
+ wss.close(done);
+ });
+ }
+ );
+ });
+
+ it('handles backpressure (3/3)', (done) => {
+ const wss = new WebSocket.Server(
+ { port: 0, perMessageDeflate: true },
+ () => {
+ const called = [];
+ const ws = new WebSocket(`ws://localhost:${wss.address().port}`);
+ const duplex = createWebSocketStream(ws);
+ const read = duplex._read;
+
+ duplex._read = () => {
+ called.push('read');
+ assert.ok(!ws._receiver._writableState.needDrain);
+ read();
+ assert.ok(!ws._socket.isPaused());
+ duplex.end();
+ };
+
+ ws.on('open', () => {
+ ws._receiver.on('drain', () => {
+ called.push('drain');
+ assert.ok(ws._socket.isPaused());
+ duplex.resume();
+ });
+
+ const opts = {
+ fin: true,
+ opcode: 0x02,
+ mask: false,
+ readOnly: false
+ };
+
+ const list = [
+ ...Sender.frame(randomBytes(16 * 1024), { rsv1: false, ...opts }),
+ ...Sender.frame(Buffer.alloc(1), { rsv1: true, ...opts })
+ ];
+
+ ws._socket.push(Buffer.concat(list));
+ });
+
+ duplex.on('close', () => {
+ assert.deepStrictEqual(called, ['drain', 'read']);
+ wss.close(done);
+ });
+ }
+ );
+ });
+
+ it('can be destroyed (1/2)', (done) => {
+ const wss = new WebSocket.Server({ port: 0 }, () => {
+ const error = new Error('Oops');
+ const ws = new WebSocket(`ws://localhost:${wss.address().port}`);
+ const duplex = createWebSocketStream(ws);
+
+ duplex.on('error', (err) => {
+ assert.strictEqual(err, error);
+
+ duplex.on('close', () => {
+ wss.close(done);
+ });
+ });
+
+ ws.on('open', () => {
+ duplex.destroy(error);
+ });
+ });
+ });
+
+ it('can be destroyed (2/2)', (done) => {
+ const wss = new WebSocket.Server({ port: 0 }, () => {
+ const ws = new WebSocket(`ws://localhost:${wss.address().port}`);
+ const duplex = createWebSocketStream(ws);
+
+ duplex.on('close', () => {
+ wss.close(done);
+ });
+
+ ws.on('open', () => {
+ duplex.destroy();
+ });
+ });
+ });
+
+ it('converts text messages to strings in readable object mode', (done) => {
+ const wss = new WebSocket.Server({ port: 0 }, () => {
+ const events = [];
+ const ws = new WebSocket(`ws://localhost:${wss.address().port}`);
+ const duplex = createWebSocketStream(ws, { readableObjectMode: true });
+
+ duplex.on('data', (data) => {
+ events.push('data');
+ assert.strictEqual(data, 'foo');
+ });
+
+ duplex.on('end', () => {
+ events.push('end');
+ duplex.end();
+ });
+
+ duplex.on('close', () => {
+ assert.deepStrictEqual(events, ['data', 'end']);
+ wss.close(done);
+ });
+ });
+
+ wss.on('connection', (ws) => {
+ ws.send('foo');
+ ws.close();
+ });
+ });
+
+ it('resumes the socket if `readyState` is `CLOSING`', (done) => {
+ const wss = new WebSocket.Server({ port: 0 }, () => {
+ const ws = new WebSocket(`ws://localhost:${wss.address().port}`);
+ const duplex = createWebSocketStream(ws);
+
+ ws.on('message', () => {
+ assert.ok(ws._socket.isPaused());
+
+ duplex.on('close', () => {
+ wss.close(done);
+ });
+
+ duplex.end();
+
+ process.nextTick(() => {
+ assert.strictEqual(ws.readyState, WebSocket.CLOSING);
+ duplex.resume();
+ });
+ });
+ });
+
+ wss.on('connection', (ws) => {
+ ws.send(randomBytes(16 * 1024));
+ });
+ });
+ });
+});
diff --git a/testing/xpcshell/node-ws/test/event-target.test.js b/testing/xpcshell/node-ws/test/event-target.test.js
new file mode 100644
index 0000000000..5caaa5c273
--- /dev/null
+++ b/testing/xpcshell/node-ws/test/event-target.test.js
@@ -0,0 +1,253 @@
+'use strict';
+
+const assert = require('assert');
+
+const {
+ CloseEvent,
+ ErrorEvent,
+ Event,
+ MessageEvent
+} = require('../lib/event-target');
+
+describe('Event', () => {
+ describe('#ctor', () => {
+ it('takes a `type` argument', () => {
+ const event = new Event('foo');
+
+ assert.strictEqual(event.type, 'foo');
+ });
+ });
+
+ describe('Properties', () => {
+ describe('`target`', () => {
+ it('is enumerable and configurable', () => {
+ const descriptor = Object.getOwnPropertyDescriptor(
+ Event.prototype,
+ 'target'
+ );
+
+ assert.strictEqual(descriptor.configurable, true);
+ assert.strictEqual(descriptor.enumerable, true);
+ assert.ok(descriptor.get !== undefined);
+ assert.ok(descriptor.set === undefined);
+ });
+
+ it('defaults to `null`', () => {
+ const event = new Event('foo');
+
+ assert.strictEqual(event.target, null);
+ });
+ });
+
+ describe('`type`', () => {
+ it('is enumerable and configurable', () => {
+ const descriptor = Object.getOwnPropertyDescriptor(
+ Event.prototype,
+ 'type'
+ );
+
+ assert.strictEqual(descriptor.configurable, true);
+ assert.strictEqual(descriptor.enumerable, true);
+ assert.ok(descriptor.get !== undefined);
+ assert.ok(descriptor.set === undefined);
+ });
+ });
+ });
+});
+
+describe('CloseEvent', () => {
+ it('inherits from `Event`', () => {
+ assert.ok(CloseEvent.prototype instanceof Event);
+ });
+
+ describe('#ctor', () => {
+ it('takes a `type` argument', () => {
+ const event = new CloseEvent('foo');
+
+ assert.strictEqual(event.type, 'foo');
+ });
+
+ it('takes an optional `options` argument', () => {
+ const event = new CloseEvent('close', {
+ code: 1000,
+ reason: 'foo',
+ wasClean: true
+ });
+
+ assert.strictEqual(event.type, 'close');
+ assert.strictEqual(event.code, 1000);
+ assert.strictEqual(event.reason, 'foo');
+ assert.strictEqual(event.wasClean, true);
+ });
+ });
+
+ describe('Properties', () => {
+ describe('`code`', () => {
+ it('is enumerable and configurable', () => {
+ const descriptor = Object.getOwnPropertyDescriptor(
+ CloseEvent.prototype,
+ 'code'
+ );
+
+ assert.strictEqual(descriptor.configurable, true);
+ assert.strictEqual(descriptor.enumerable, true);
+ assert.ok(descriptor.get !== undefined);
+ assert.ok(descriptor.set === undefined);
+ });
+
+ it('defaults to 0', () => {
+ const event = new CloseEvent('close');
+
+ assert.strictEqual(event.code, 0);
+ });
+ });
+
+ describe('`reason`', () => {
+ it('is enumerable and configurable', () => {
+ const descriptor = Object.getOwnPropertyDescriptor(
+ CloseEvent.prototype,
+ 'reason'
+ );
+
+ assert.strictEqual(descriptor.configurable, true);
+ assert.strictEqual(descriptor.enumerable, true);
+ assert.ok(descriptor.get !== undefined);
+ assert.ok(descriptor.set === undefined);
+ });
+
+ it('defaults to an empty string', () => {
+ const event = new CloseEvent('close');
+
+ assert.strictEqual(event.reason, '');
+ });
+ });
+
+ describe('`wasClean`', () => {
+ it('is enumerable and configurable', () => {
+ const descriptor = Object.getOwnPropertyDescriptor(
+ CloseEvent.prototype,
+ 'wasClean'
+ );
+
+ assert.strictEqual(descriptor.configurable, true);
+ assert.strictEqual(descriptor.enumerable, true);
+ assert.ok(descriptor.get !== undefined);
+ assert.ok(descriptor.set === undefined);
+ });
+
+ it('defaults to false', () => {
+ const event = new CloseEvent('close');
+
+ assert.strictEqual(event.wasClean, false);
+ });
+ });
+ });
+});
+
+describe('ErrorEvent', () => {
+ it('inherits from `Event`', () => {
+ assert.ok(ErrorEvent.prototype instanceof Event);
+ });
+
+ describe('#ctor', () => {
+ it('takes a `type` argument', () => {
+ const event = new ErrorEvent('foo');
+
+ assert.strictEqual(event.type, 'foo');
+ });
+
+ it('takes an optional `options` argument', () => {
+ const error = new Error('Oops');
+ const event = new ErrorEvent('error', { error, message: error.message });
+
+ assert.strictEqual(event.type, 'error');
+ assert.strictEqual(event.error, error);
+ assert.strictEqual(event.message, error.message);
+ });
+ });
+
+ describe('Properties', () => {
+ describe('`error`', () => {
+ it('is enumerable and configurable', () => {
+ const descriptor = Object.getOwnPropertyDescriptor(
+ ErrorEvent.prototype,
+ 'error'
+ );
+
+ assert.strictEqual(descriptor.configurable, true);
+ assert.strictEqual(descriptor.enumerable, true);
+ assert.ok(descriptor.get !== undefined);
+ assert.ok(descriptor.set === undefined);
+ });
+
+ it('defaults to `null`', () => {
+ const event = new ErrorEvent('error');
+
+ assert.strictEqual(event.error, null);
+ });
+ });
+
+ describe('`message`', () => {
+ it('is enumerable and configurable', () => {
+ const descriptor = Object.getOwnPropertyDescriptor(
+ ErrorEvent.prototype,
+ 'message'
+ );
+
+ assert.strictEqual(descriptor.configurable, true);
+ assert.strictEqual(descriptor.enumerable, true);
+ assert.ok(descriptor.get !== undefined);
+ assert.ok(descriptor.set === undefined);
+ });
+
+ it('defaults to an empty string', () => {
+ const event = new ErrorEvent('error');
+
+ assert.strictEqual(event.message, '');
+ });
+ });
+ });
+});
+
+describe('MessageEvent', () => {
+ it('inherits from `Event`', () => {
+ assert.ok(MessageEvent.prototype instanceof Event);
+ });
+
+ describe('#ctor', () => {
+ it('takes a `type` argument', () => {
+ const event = new MessageEvent('foo');
+
+ assert.strictEqual(event.type, 'foo');
+ });
+
+ it('takes an optional `options` argument', () => {
+ const event = new MessageEvent('message', { data: 'bar' });
+
+ assert.strictEqual(event.type, 'message');
+ assert.strictEqual(event.data, 'bar');
+ });
+ });
+
+ describe('Properties', () => {
+ describe('`data`', () => {
+ it('is enumerable and configurable', () => {
+ const descriptor = Object.getOwnPropertyDescriptor(
+ MessageEvent.prototype,
+ 'data'
+ );
+
+ assert.strictEqual(descriptor.configurable, true);
+ assert.strictEqual(descriptor.enumerable, true);
+ assert.ok(descriptor.get !== undefined);
+ assert.ok(descriptor.set === undefined);
+ });
+
+ it('defaults to `null`', () => {
+ const event = new MessageEvent('message');
+
+ assert.strictEqual(event.data, null);
+ });
+ });
+ });
+});
diff --git a/testing/xpcshell/node-ws/test/extension.test.js b/testing/xpcshell/node-ws/test/extension.test.js
new file mode 100644
index 0000000000..a4b3e749d0
--- /dev/null
+++ b/testing/xpcshell/node-ws/test/extension.test.js
@@ -0,0 +1,190 @@
+'use strict';
+
+const assert = require('assert');
+
+const { format, parse } = require('../lib/extension');
+
+describe('extension', () => {
+ describe('parse', () => {
+ it('parses a single extension', () => {
+ assert.deepStrictEqual(parse('foo'), {
+ foo: [{ __proto__: null }],
+ __proto__: null
+ });
+ });
+
+ it('parses params', () => {
+ assert.deepStrictEqual(parse('foo;bar;baz=1;bar=2'), {
+ foo: [{ bar: [true, '2'], baz: ['1'], __proto__: null }],
+ __proto__: null
+ });
+ });
+
+ it('parses multiple extensions', () => {
+ assert.deepStrictEqual(parse('foo,bar;baz,foo;baz'), {
+ foo: [{ __proto__: null }, { baz: [true], __proto__: null }],
+ bar: [{ baz: [true], __proto__: null }],
+ __proto__: null
+ });
+ });
+
+ it('parses quoted params', () => {
+ assert.deepStrictEqual(parse('foo;bar="hi"'), {
+ foo: [{ bar: ['hi'], __proto__: null }],
+ __proto__: null
+ });
+ assert.deepStrictEqual(parse('foo;bar="\\0"'), {
+ foo: [{ bar: ['0'], __proto__: null }],
+ __proto__: null
+ });
+ assert.deepStrictEqual(parse('foo;bar="b\\a\\z"'), {
+ foo: [{ bar: ['baz'], __proto__: null }],
+ __proto__: null
+ });
+ assert.deepStrictEqual(parse('foo;bar="b\\az";bar'), {
+ foo: [{ bar: ['baz', true], __proto__: null }],
+ __proto__: null
+ });
+ assert.throws(
+ () => parse('foo;bar="baz"qux'),
+ /^SyntaxError: Unexpected character at index 13$/
+ );
+ assert.throws(
+ () => parse('foo;bar="baz" qux'),
+ /^SyntaxError: Unexpected character at index 14$/
+ );
+ });
+
+ it('works with names that match `Object.prototype` property names', () => {
+ assert.deepStrictEqual(parse('hasOwnProperty, toString'), {
+ hasOwnProperty: [{ __proto__: null }],
+ toString: [{ __proto__: null }],
+ __proto__: null
+ });
+ assert.deepStrictEqual(parse('foo;constructor'), {
+ foo: [{ constructor: [true], __proto__: null }],
+ __proto__: null
+ });
+ });
+
+ it('ignores the optional white spaces', () => {
+ const header = 'foo; bar\t; \tbaz=1\t ; bar="1"\t\t, \tqux\t ;norf';
+
+ assert.deepStrictEqual(parse(header), {
+ foo: [{ bar: [true, '1'], baz: ['1'], __proto__: null }],
+ qux: [{ norf: [true], __proto__: null }],
+ __proto__: null
+ });
+ });
+
+ it('throws an error if a name is empty', () => {
+ [
+ [',', 0],
+ ['foo,,', 4],
+ ['foo, ,', 6],
+ ['foo;=', 4],
+ ['foo; =', 5],
+ ['foo;;', 4],
+ ['foo; ;', 5],
+ ['foo;bar=,', 8],
+ ['foo;bar=""', 9]
+ ].forEach((element) => {
+ assert.throws(
+ () => parse(element[0]),
+ new RegExp(
+ `^SyntaxError: Unexpected character at index ${element[1]}$`
+ )
+ );
+ });
+ });
+
+ it('throws an error if a white space is misplaced', () => {
+ [
+ [' foo', 0],
+ ['f oo', 2],
+ ['foo;ba r', 7],
+ ['foo;bar =', 8],
+ ['foo;bar= ', 8],
+ ['foo;bar=ba z', 11]
+ ].forEach((element) => {
+ assert.throws(
+ () => parse(element[0]),
+ new RegExp(
+ `^SyntaxError: Unexpected character at index ${element[1]}$`
+ )
+ );
+ });
+ });
+
+ it('throws an error if a token contains invalid characters', () => {
+ [
+ ['f@o', 1],
+ ['f\\oo', 1],
+ ['"foo"', 0],
+ ['f"oo"', 1],
+ ['foo;b@r', 5],
+ ['foo;b\\ar', 5],
+ ['foo;"bar"', 4],
+ ['foo;b"ar"', 5],
+ ['foo;bar=b@z', 9],
+ ['foo;bar=b\\az ', 9],
+ ['foo;bar="b@z"', 10],
+ ['foo;bar="baz;"', 12],
+ ['foo;bar=b"az"', 9],
+ ['foo;bar="\\\\"', 10]
+ ].forEach((element) => {
+ assert.throws(
+ () => parse(element[0]),
+ new RegExp(
+ `^SyntaxError: Unexpected character at index ${element[1]}$`
+ )
+ );
+ });
+ });
+
+ it('throws an error if the header value ends prematurely', () => {
+ [
+ '',
+ 'foo ',
+ 'foo\t',
+ 'foo, ',
+ 'foo;',
+ 'foo;bar ',
+ 'foo;bar,',
+ 'foo;bar; ',
+ 'foo;bar=',
+ 'foo;bar="baz',
+ 'foo;bar="1\\',
+ 'foo;bar="baz" '
+ ].forEach((header) => {
+ assert.throws(
+ () => parse(header),
+ /^SyntaxError: Unexpected end of input$/
+ );
+ });
+ });
+ });
+
+ describe('format', () => {
+ it('formats a single extension', () => {
+ const extensions = format({ foo: {} });
+
+ assert.strictEqual(extensions, 'foo');
+ });
+
+ it('formats params', () => {
+ const extensions = format({ foo: { bar: [true, 2], baz: 1 } });
+
+ assert.strictEqual(extensions, 'foo; bar; bar=2; baz=1');
+ });
+
+ it('formats multiple extensions', () => {
+ const extensions = format({
+ foo: [{}, { baz: true }],
+ bar: { baz: true }
+ });
+
+ assert.strictEqual(extensions, 'foo, foo; baz, bar; baz');
+ });
+ });
+});
diff --git a/testing/xpcshell/node-ws/test/fixtures/ca-certificate.pem b/testing/xpcshell/node-ws/test/fixtures/ca-certificate.pem
new file mode 100644
index 0000000000..0f1658821d
--- /dev/null
+++ b/testing/xpcshell/node-ws/test/fixtures/ca-certificate.pem
@@ -0,0 +1,12 @@
+-----BEGIN CERTIFICATE-----
+MIIBtTCCAVoCCQCXqK2FegDgiDAKBggqhkjOPQQDAjBhMQswCQYDVQQGEwJJVDEQ
+MA4GA1UECAwHUGVydWdpYTEQMA4GA1UEBwwHRm9saWdubzETMBEGA1UECgwKd2Vi
+c29ja2V0czELMAkGA1UECwwCd3MxDDAKBgNVBAMMA2NhMTAgFw0yMTA1MjYxOTA1
+MjdaGA8yMTIxMDUwMjE5MDUyN1owYTELMAkGA1UEBhMCSVQxEDAOBgNVBAgMB1Bl
+cnVnaWExEDAOBgNVBAcMB0ZvbGlnbm8xEzARBgNVBAoMCndlYnNvY2tldHMxCzAJ
+BgNVBAsMAndzMQwwCgYDVQQDDANjYTEwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNC
+AASHE75QDQN6XNo/711YSbckaa8r4lt0hGkgtADaBFT9Qn9gcm5omapePZT76Ff9
+rwjMcS+YPXS7J7bk+QHLihJMMAoGCCqGSM49BAMCA0kAMEYCIQCUMdUih+sE0ZTu
+ORlcKiM8DKyiKkGU4Ty+dslz6nVJjAIhAMcSy0SBsBDgsai1s9aCmAGJXCijNb6g
+vfWaatgq+ma2
+-----END CERTIFICATE-----
diff --git a/testing/xpcshell/node-ws/test/fixtures/ca-key.pem b/testing/xpcshell/node-ws/test/fixtures/ca-key.pem
new file mode 100644
index 0000000000..a9352fb6a2
--- /dev/null
+++ b/testing/xpcshell/node-ws/test/fixtures/ca-key.pem
@@ -0,0 +1,5 @@
+-----BEGIN EC PRIVATE KEY-----
+MHcCAQEEIAa/Onpk27cLkqzje69Bac8yG+LTBXIPWT8yGlyjEFbboAoGCCqGSM49
+AwEHoUQDQgAEhxO+UA0DelzaP+9dWEm3JGmvK+JbdIRpILQA2gRU/UJ/YHJuaJmq
+Xj2U++hX/a8IzHEvmD10uye25PkBy4oSTA==
+-----END EC PRIVATE KEY-----
diff --git a/testing/xpcshell/node-ws/test/fixtures/certificate.pem b/testing/xpcshell/node-ws/test/fixtures/certificate.pem
new file mode 100644
index 0000000000..538553ee08
--- /dev/null
+++ b/testing/xpcshell/node-ws/test/fixtures/certificate.pem
@@ -0,0 +1,12 @@
+-----BEGIN CERTIFICATE-----
+MIIBujCCAWACCQDjKdAMt3mZhDAKBggqhkjOPQQDAjBkMQswCQYDVQQGEwJJVDEQ
+MA4GA1UECAwHUGVydWdpYTEQMA4GA1UEBwwHRm9saWdubzETMBEGA1UECgwKd2Vi
+c29ja2V0czELMAkGA1UECwwCd3MxDzANBgNVBAMMBnNlcnZlcjAgFw0yMTA1MjYx
+OTEwMjlaGA8yMTIxMDUwMjE5MTAyOVowZDELMAkGA1UEBhMCSVQxEDAOBgNVBAgM
+B1BlcnVnaWExEDAOBgNVBAcMB0ZvbGlnbm8xEzARBgNVBAoMCndlYnNvY2tldHMx
+CzAJBgNVBAsMAndzMQ8wDQYDVQQDDAZzZXJ2ZXIwWTATBgcqhkjOPQIBBggqhkjO
+PQMBBwNCAAQKhyRhdSVOecbJU4O5XkB/iGodbnCOqmchs4TXmE3Prv5SrNDhODDv
+rOWTXwR3/HrrdNfOzPdb54amu8POwpohMAoGCCqGSM49BAMCA0gAMEUCIHMRUSPl
+8FGkDLl8KF1A+SbT2ds3zUOLdYvj30Z2SKSVAiEA84U/R1ly9wf5Rzv93sTHI99o
+KScsr/PHN8rT2pop5pk=
+-----END CERTIFICATE-----
diff --git a/testing/xpcshell/node-ws/test/fixtures/client-certificate.pem b/testing/xpcshell/node-ws/test/fixtures/client-certificate.pem
new file mode 100644
index 0000000000..0e20560b8c
--- /dev/null
+++ b/testing/xpcshell/node-ws/test/fixtures/client-certificate.pem
@@ -0,0 +1,12 @@
+-----BEGIN CERTIFICATE-----
+MIIBtzCCAV0CCQDDIX2dKuKP0zAKBggqhkjOPQQDAjBhMQswCQYDVQQGEwJJVDEQ
+MA4GA1UECAwHUGVydWdpYTEQMA4GA1UEBwwHRm9saWdubzETMBEGA1UECgwKd2Vi
+c29ja2V0czELMAkGA1UECwwCd3MxDDAKBgNVBAMMA2NhMTAgFw0yMTA1MjYxOTE3
+NDJaGA8yMTIxMDUwMjE5MTc0MlowZDELMAkGA1UEBhMCSVQxEDAOBgNVBAgMB1Bl
+cnVnaWExEDAOBgNVBAcMB0ZvbGlnbm8xEzARBgNVBAoMCndlYnNvY2tldHMxCzAJ
+BgNVBAsMAndzMQ8wDQYDVQQDDAZhZ2VudDEwWTATBgcqhkjOPQIBBggqhkjOPQMB
+BwNCAATwHlNS2b13TMhBTSWBXAn6TEPxrsvG93ZZyUlmrEMOXSMX2hI7sv660YNj
++eGyE2CV33XsQxV3TUqi51fUjIu8MAoGCCqGSM49BAMCA0gAMEUCIQCxsqBre+Do
+jnfg6XmCaB0fywNzcDlvdoVNuNAWfVNrSAIgDQmbM0mXZaSAkf4sgtKdXnpE3vrb
+MElb457Bi3B+rkE=
+-----END CERTIFICATE-----
diff --git a/testing/xpcshell/node-ws/test/fixtures/client-key.pem b/testing/xpcshell/node-ws/test/fixtures/client-key.pem
new file mode 100644
index 0000000000..e034f57fc2
--- /dev/null
+++ b/testing/xpcshell/node-ws/test/fixtures/client-key.pem
@@ -0,0 +1,5 @@
+-----BEGIN EC PRIVATE KEY-----
+MHcCAQEEIKVGskK0UR86WwMo5H0+hNAFGRBYsEevK3ye4y1YberVoAoGCCqGSM49
+AwEHoUQDQgAE8B5TUtm9d0zIQU0lgVwJ+kxD8a7Lxvd2WclJZqxDDl0jF9oSO7L+
+utGDY/nhshNgld917EMVd01KoudX1IyLvA==
+-----END EC PRIVATE KEY-----
diff --git a/testing/xpcshell/node-ws/test/fixtures/key.pem b/testing/xpcshell/node-ws/test/fixtures/key.pem
new file mode 100644
index 0000000000..05bfdb71ed
--- /dev/null
+++ b/testing/xpcshell/node-ws/test/fixtures/key.pem
@@ -0,0 +1,5 @@
+-----BEGIN EC PRIVATE KEY-----
+MHcCAQEEIIjLz7YEWIrsGem2+YV8eJhHhetsjYIrjuqJLbdG7B3zoAoGCCqGSM49
+AwEHoUQDQgAECockYXUlTnnGyVODuV5Af4hqHW5wjqpnIbOE15hNz67+UqzQ4Tgw
+76zlk18Ed/x663TXzsz3W+eGprvDzsKaIQ==
+-----END EC PRIVATE KEY-----
diff --git a/testing/xpcshell/node-ws/test/limiter.test.js b/testing/xpcshell/node-ws/test/limiter.test.js
new file mode 100644
index 0000000000..95141f0f5c
--- /dev/null
+++ b/testing/xpcshell/node-ws/test/limiter.test.js
@@ -0,0 +1,41 @@
+'use strict';
+
+const assert = require('assert');
+
+const Limiter = require('../lib/limiter');
+
+describe('Limiter', () => {
+ describe('#ctor', () => {
+ it('takes a `concurrency` argument', () => {
+ const limiter = new Limiter(0);
+
+ assert.strictEqual(limiter.concurrency, Infinity);
+ });
+ });
+
+ describe('#kRun', () => {
+ it('limits the number of jobs allowed to run concurrently', (done) => {
+ const limiter = new Limiter(1);
+
+ limiter.add((callback) => {
+ setImmediate(() => {
+ callback();
+
+ assert.strictEqual(limiter.jobs.length, 0);
+ assert.strictEqual(limiter.pending, 1);
+ });
+ });
+
+ limiter.add((callback) => {
+ setImmediate(() => {
+ callback();
+
+ assert.strictEqual(limiter.pending, 0);
+ done();
+ });
+ });
+
+ assert.strictEqual(limiter.jobs.length, 1);
+ });
+ });
+});
diff --git a/testing/xpcshell/node-ws/test/permessage-deflate.test.js b/testing/xpcshell/node-ws/test/permessage-deflate.test.js
new file mode 100644
index 0000000000..a9c9bf165c
--- /dev/null
+++ b/testing/xpcshell/node-ws/test/permessage-deflate.test.js
@@ -0,0 +1,647 @@
+'use strict';
+
+const assert = require('assert');
+
+const PerMessageDeflate = require('../lib/permessage-deflate');
+const extension = require('../lib/extension');
+
+describe('PerMessageDeflate', () => {
+ describe('#offer', () => {
+ it('creates an offer', () => {
+ const perMessageDeflate = new PerMessageDeflate();
+
+ assert.deepStrictEqual(perMessageDeflate.offer(), {
+ client_max_window_bits: true
+ });
+ });
+
+ it('uses the configuration options', () => {
+ const perMessageDeflate = new PerMessageDeflate({
+ serverNoContextTakeover: true,
+ clientNoContextTakeover: true,
+ serverMaxWindowBits: 10,
+ clientMaxWindowBits: 11
+ });
+
+ assert.deepStrictEqual(perMessageDeflate.offer(), {
+ server_no_context_takeover: true,
+ client_no_context_takeover: true,
+ server_max_window_bits: 10,
+ client_max_window_bits: 11
+ });
+ });
+ });
+
+ describe('#accept', () => {
+ it('throws an error if a parameter has multiple values', () => {
+ const perMessageDeflate = new PerMessageDeflate();
+ const extensions = extension.parse(
+ 'permessage-deflate; server_no_context_takeover; server_no_context_takeover'
+ );
+
+ assert.throws(
+ () => perMessageDeflate.accept(extensions['permessage-deflate']),
+ /^Error: Parameter "server_no_context_takeover" must have only a single value$/
+ );
+ });
+
+ it('throws an error if a parameter has an invalid name', () => {
+ const perMessageDeflate = new PerMessageDeflate();
+ const extensions = extension.parse('permessage-deflate;foo');
+
+ assert.throws(
+ () => perMessageDeflate.accept(extensions['permessage-deflate']),
+ /^Error: Unknown parameter "foo"$/
+ );
+ });
+
+ it('throws an error if client_no_context_takeover has a value', () => {
+ const perMessageDeflate = new PerMessageDeflate();
+ const extensions = extension.parse(
+ 'permessage-deflate; client_no_context_takeover=10'
+ );
+
+ assert.throws(
+ () => perMessageDeflate.accept(extensions['permessage-deflate']),
+ /^TypeError: Invalid value for parameter "client_no_context_takeover": 10$/
+ );
+ });
+
+ it('throws an error if server_no_context_takeover has a value', () => {
+ const perMessageDeflate = new PerMessageDeflate();
+ const extensions = extension.parse(
+ 'permessage-deflate; server_no_context_takeover=10'
+ );
+
+ assert.throws(
+ () => perMessageDeflate.accept(extensions['permessage-deflate']),
+ /^TypeError: Invalid value for parameter "server_no_context_takeover": 10$/
+ );
+ });
+
+ it('throws an error if server_max_window_bits has an invalid value', () => {
+ const perMessageDeflate = new PerMessageDeflate();
+
+ let extensions = extension.parse(
+ 'permessage-deflate; server_max_window_bits=7'
+ );
+ assert.throws(
+ () => perMessageDeflate.accept(extensions['permessage-deflate']),
+ /^TypeError: Invalid value for parameter "server_max_window_bits": 7$/
+ );
+
+ extensions = extension.parse(
+ 'permessage-deflate; server_max_window_bits'
+ );
+ assert.throws(
+ () => perMessageDeflate.accept(extensions['permessage-deflate']),
+ /^TypeError: Invalid value for parameter "server_max_window_bits": true$/
+ );
+ });
+
+ describe('As server', () => {
+ it('accepts an offer with no parameters', () => {
+ const perMessageDeflate = new PerMessageDeflate({}, true);
+
+ assert.deepStrictEqual(perMessageDeflate.accept([{}]), {});
+ });
+
+ it('accepts an offer with parameters', () => {
+ const perMessageDeflate = new PerMessageDeflate({}, true);
+ const extensions = extension.parse(
+ 'permessage-deflate; server_no_context_takeover; ' +
+ 'client_no_context_takeover; server_max_window_bits=10; ' +
+ 'client_max_window_bits=11'
+ );
+
+ assert.deepStrictEqual(
+ perMessageDeflate.accept(extensions['permessage-deflate']),
+ {
+ server_no_context_takeover: true,
+ client_no_context_takeover: true,
+ server_max_window_bits: 10,
+ client_max_window_bits: 11,
+ __proto__: null
+ }
+ );
+ });
+
+ it('prefers the configuration options', () => {
+ const perMessageDeflate = new PerMessageDeflate(
+ {
+ serverNoContextTakeover: true,
+ clientNoContextTakeover: true,
+ serverMaxWindowBits: 12,
+ clientMaxWindowBits: 11
+ },
+ true
+ );
+ const extensions = extension.parse(
+ 'permessage-deflate; server_max_window_bits=14; client_max_window_bits=13'
+ );
+
+ assert.deepStrictEqual(
+ perMessageDeflate.accept(extensions['permessage-deflate']),
+ {
+ server_no_context_takeover: true,
+ client_no_context_takeover: true,
+ server_max_window_bits: 12,
+ client_max_window_bits: 11,
+ __proto__: null
+ }
+ );
+ });
+
+ it('accepts the first supported offer', () => {
+ const perMessageDeflate = new PerMessageDeflate(
+ { serverMaxWindowBits: 11 },
+ true
+ );
+ const extensions = extension.parse(
+ 'permessage-deflate; server_max_window_bits=10, permessage-deflate'
+ );
+
+ assert.deepStrictEqual(
+ perMessageDeflate.accept(extensions['permessage-deflate']),
+ {
+ server_max_window_bits: 11,
+ __proto__: null
+ }
+ );
+ });
+
+ it('throws an error if server_no_context_takeover is unsupported', () => {
+ const perMessageDeflate = new PerMessageDeflate(
+ { serverNoContextTakeover: false },
+ true
+ );
+ const extensions = extension.parse(
+ 'permessage-deflate; server_no_context_takeover'
+ );
+
+ assert.throws(
+ () => perMessageDeflate.accept(extensions['permessage-deflate']),
+ /^Error: None of the extension offers can be accepted$/
+ );
+ });
+
+ it('throws an error if server_max_window_bits is unsupported', () => {
+ const perMessageDeflate = new PerMessageDeflate(
+ { serverMaxWindowBits: false },
+ true
+ );
+ const extensions = extension.parse(
+ 'permessage-deflate; server_max_window_bits=10'
+ );
+
+ assert.throws(
+ () => perMessageDeflate.accept(extensions['permessage-deflate']),
+ /^Error: None of the extension offers can be accepted$/
+ );
+ });
+
+ it('throws an error if server_max_window_bits is less than configuration', () => {
+ const perMessageDeflate = new PerMessageDeflate(
+ { serverMaxWindowBits: 11 },
+ true
+ );
+ const extensions = extension.parse(
+ 'permessage-deflate; server_max_window_bits=10'
+ );
+
+ assert.throws(
+ () => perMessageDeflate.accept(extensions['permessage-deflate']),
+ /^Error: None of the extension offers can be accepted$/
+ );
+ });
+
+ it('throws an error if client_max_window_bits is unsupported on client', () => {
+ const perMessageDeflate = new PerMessageDeflate(
+ { clientMaxWindowBits: 10 },
+ true
+ );
+ const extensions = extension.parse('permessage-deflate');
+
+ assert.throws(
+ () => perMessageDeflate.accept(extensions['permessage-deflate']),
+ /^Error: None of the extension offers can be accepted$/
+ );
+ });
+
+ it('throws an error if client_max_window_bits has an invalid value', () => {
+ const perMessageDeflate = new PerMessageDeflate({}, true);
+
+ const extensions = extension.parse(
+ 'permessage-deflate; client_max_window_bits=16'
+ );
+ assert.throws(
+ () => perMessageDeflate.accept(extensions['permessage-deflate']),
+ /^TypeError: Invalid value for parameter "client_max_window_bits": 16$/
+ );
+ });
+ });
+
+ describe('As client', () => {
+ it('accepts a response with no parameters', () => {
+ const perMessageDeflate = new PerMessageDeflate({});
+
+ assert.deepStrictEqual(perMessageDeflate.accept([{}]), {});
+ });
+
+ it('accepts a response with parameters', () => {
+ const perMessageDeflate = new PerMessageDeflate({});
+ const extensions = extension.parse(
+ 'permessage-deflate; server_no_context_takeover; ' +
+ 'client_no_context_takeover; server_max_window_bits=10; ' +
+ 'client_max_window_bits=11'
+ );
+
+ assert.deepStrictEqual(
+ perMessageDeflate.accept(extensions['permessage-deflate']),
+ {
+ server_no_context_takeover: true,
+ client_no_context_takeover: true,
+ server_max_window_bits: 10,
+ client_max_window_bits: 11,
+ __proto__: null
+ }
+ );
+ });
+
+ it('throws an error if client_no_context_takeover is unsupported', () => {
+ const perMessageDeflate = new PerMessageDeflate({
+ clientNoContextTakeover: false
+ });
+ const extensions = extension.parse(
+ 'permessage-deflate; client_no_context_takeover'
+ );
+
+ assert.throws(
+ () => perMessageDeflate.accept(extensions['permessage-deflate']),
+ /^Error: Unexpected parameter "client_no_context_takeover"$/
+ );
+ });
+
+ it('throws an error if client_max_window_bits is unsupported', () => {
+ const perMessageDeflate = new PerMessageDeflate({
+ clientMaxWindowBits: false
+ });
+ const extensions = extension.parse(
+ 'permessage-deflate; client_max_window_bits=10'
+ );
+
+ assert.throws(
+ () => perMessageDeflate.accept(extensions['permessage-deflate']),
+ /^Error: Unexpected or invalid parameter "client_max_window_bits"$/
+ );
+ });
+
+ it('throws an error if client_max_window_bits is greater than configuration', () => {
+ const perMessageDeflate = new PerMessageDeflate({
+ clientMaxWindowBits: 10
+ });
+ const extensions = extension.parse(
+ 'permessage-deflate; client_max_window_bits=11'
+ );
+
+ assert.throws(
+ () => perMessageDeflate.accept(extensions['permessage-deflate']),
+ /^Error: Unexpected or invalid parameter "client_max_window_bits"$/
+ );
+ });
+
+ it('throws an error if client_max_window_bits has an invalid value', () => {
+ const perMessageDeflate = new PerMessageDeflate();
+
+ let extensions = extension.parse(
+ 'permessage-deflate; client_max_window_bits=16'
+ );
+ assert.throws(
+ () => perMessageDeflate.accept(extensions['permessage-deflate']),
+ /^TypeError: Invalid value for parameter "client_max_window_bits": 16$/
+ );
+
+ extensions = extension.parse(
+ 'permessage-deflate; client_max_window_bits'
+ );
+ assert.throws(
+ () => perMessageDeflate.accept(extensions['permessage-deflate']),
+ /^TypeError: Invalid value for parameter "client_max_window_bits": true$/
+ );
+ });
+
+ it('uses the config value if client_max_window_bits is not specified', () => {
+ const perMessageDeflate = new PerMessageDeflate({
+ clientMaxWindowBits: 10
+ });
+
+ assert.deepStrictEqual(perMessageDeflate.accept([{}]), {
+ client_max_window_bits: 10
+ });
+ });
+ });
+ });
+
+ describe('#compress and #decompress', () => {
+ it('works with unfragmented messages', (done) => {
+ const perMessageDeflate = new PerMessageDeflate();
+ const buf = Buffer.from([1, 2, 3]);
+
+ perMessageDeflate.accept([{}]);
+ perMessageDeflate.compress(buf, true, (err, data) => {
+ if (err) return done(err);
+
+ perMessageDeflate.decompress(data, true, (err, data) => {
+ if (err) return done(err);
+
+ assert.ok(data.equals(buf));
+ done();
+ });
+ });
+ });
+
+ it('works with fragmented messages', (done) => {
+ const perMessageDeflate = new PerMessageDeflate();
+ const buf = Buffer.from([1, 2, 3, 4]);
+
+ perMessageDeflate.accept([{}]);
+
+ perMessageDeflate.compress(buf.slice(0, 2), false, (err, compressed1) => {
+ if (err) return done(err);
+
+ perMessageDeflate.compress(buf.slice(2), true, (err, compressed2) => {
+ if (err) return done(err);
+
+ perMessageDeflate.decompress(compressed1, false, (err, data1) => {
+ if (err) return done(err);
+
+ perMessageDeflate.decompress(compressed2, true, (err, data2) => {
+ if (err) return done(err);
+
+ assert.ok(Buffer.concat([data1, data2]).equals(buf));
+ done();
+ });
+ });
+ });
+ });
+ });
+
+ it('works with the negotiated parameters', (done) => {
+ const perMessageDeflate = new PerMessageDeflate({
+ memLevel: 5,
+ level: 9
+ });
+ const extensions = extension.parse(
+ 'permessage-deflate; server_no_context_takeover; ' +
+ 'client_no_context_takeover; server_max_window_bits=10; ' +
+ 'client_max_window_bits=11'
+ );
+ const buf = Buffer.from("Some compressible data, it's compressible.");
+
+ perMessageDeflate.accept(extensions['permessage-deflate']);
+
+ perMessageDeflate.compress(buf, true, (err, data) => {
+ if (err) return done(err);
+
+ perMessageDeflate.decompress(data, true, (err, data) => {
+ if (err) return done(err);
+
+ assert.ok(data.equals(buf));
+ done();
+ });
+ });
+ });
+
+ it('honors the `level` option', (done) => {
+ const lev0 = new PerMessageDeflate({
+ zlibDeflateOptions: { level: 0 }
+ });
+ const lev9 = new PerMessageDeflate({
+ zlibDeflateOptions: { level: 9 }
+ });
+ const extensionStr =
+ 'permessage-deflate; server_no_context_takeover; ' +
+ 'client_no_context_takeover; server_max_window_bits=10; ' +
+ 'client_max_window_bits=11';
+ const buf = Buffer.from("Some compressible data, it's compressible.");
+
+ lev0.accept(extension.parse(extensionStr)['permessage-deflate']);
+ lev9.accept(extension.parse(extensionStr)['permessage-deflate']);
+
+ lev0.compress(buf, true, (err, compressed1) => {
+ if (err) return done(err);
+
+ lev0.decompress(compressed1, true, (err, decompressed1) => {
+ if (err) return done(err);
+
+ lev9.compress(buf, true, (err, compressed2) => {
+ if (err) return done(err);
+
+ lev9.decompress(compressed2, true, (err, decompressed2) => {
+ if (err) return done(err);
+
+ // Level 0 compression actually adds a few bytes due to headers.
+ assert.ok(compressed1.length > buf.length);
+ // Level 9 should not, of course.
+ assert.ok(compressed2.length < buf.length);
+ // Ensure they both decompress back properly.
+ assert.ok(decompressed1.equals(buf));
+ assert.ok(decompressed2.equals(buf));
+ done();
+ });
+ });
+ });
+ });
+ });
+
+ it('honors the `zlib{Deflate,Inflate}Options` option', (done) => {
+ const lev0 = new PerMessageDeflate({
+ zlibDeflateOptions: {
+ level: 0,
+ chunkSize: 256
+ },
+ zlibInflateOptions: {
+ chunkSize: 2048
+ }
+ });
+ const lev9 = new PerMessageDeflate({
+ zlibDeflateOptions: {
+ level: 9,
+ chunkSize: 128
+ },
+ zlibInflateOptions: {
+ chunkSize: 1024
+ }
+ });
+
+ // Note no context takeover so we can get a hold of the raw streams after
+ // we do the dance.
+ const extensionStr =
+ 'permessage-deflate; server_max_window_bits=10; ' +
+ 'client_max_window_bits=11';
+ const buf = Buffer.from("Some compressible data, it's compressible.");
+
+ lev0.accept(extension.parse(extensionStr)['permessage-deflate']);
+ lev9.accept(extension.parse(extensionStr)['permessage-deflate']);
+
+ lev0.compress(buf, true, (err, compressed1) => {
+ if (err) return done(err);
+
+ lev0.decompress(compressed1, true, (err, decompressed1) => {
+ if (err) return done(err);
+
+ lev9.compress(buf, true, (err, compressed2) => {
+ if (err) return done(err);
+
+ lev9.decompress(compressed2, true, (err, decompressed2) => {
+ if (err) return done(err);
+ // Level 0 compression actually adds a few bytes due to headers.
+ assert.ok(compressed1.length > buf.length);
+ // Level 9 should not, of course.
+ assert.ok(compressed2.length < buf.length);
+ // Ensure they both decompress back properly.
+ assert.ok(decompressed1.equals(buf));
+ assert.ok(decompressed2.equals(buf));
+
+ // Assert options were set.
+ assert.ok(lev0._deflate._level === 0);
+ assert.ok(lev9._deflate._level === 9);
+ assert.ok(lev0._deflate._chunkSize === 256);
+ assert.ok(lev9._deflate._chunkSize === 128);
+ assert.ok(lev0._inflate._chunkSize === 2048);
+ assert.ok(lev9._inflate._chunkSize === 1024);
+ done();
+ });
+ });
+ });
+ });
+ });
+
+ it("doesn't use contex takeover if not allowed", (done) => {
+ const perMessageDeflate = new PerMessageDeflate({}, true);
+ const extensions = extension.parse(
+ 'permessage-deflate;server_no_context_takeover'
+ );
+ const buf = Buffer.from('foofoo');
+
+ perMessageDeflate.accept(extensions['permessage-deflate']);
+
+ perMessageDeflate.compress(buf, true, (err, compressed1) => {
+ if (err) return done(err);
+
+ perMessageDeflate.decompress(compressed1, true, (err, data) => {
+ if (err) return done(err);
+
+ assert.ok(data.equals(buf));
+ perMessageDeflate.compress(data, true, (err, compressed2) => {
+ if (err) return done(err);
+
+ assert.strictEqual(compressed2.length, compressed1.length);
+ perMessageDeflate.decompress(compressed2, true, (err, data) => {
+ if (err) return done(err);
+
+ assert.ok(data.equals(buf));
+ done();
+ });
+ });
+ });
+ });
+ });
+
+ it('uses contex takeover if allowed', (done) => {
+ const perMessageDeflate = new PerMessageDeflate({}, true);
+ const extensions = extension.parse('permessage-deflate');
+ const buf = Buffer.from('foofoo');
+
+ perMessageDeflate.accept(extensions['permessage-deflate']);
+
+ perMessageDeflate.compress(buf, true, (err, compressed1) => {
+ if (err) return done(err);
+
+ perMessageDeflate.decompress(compressed1, true, (err, data) => {
+ if (err) return done(err);
+
+ assert.ok(data.equals(buf));
+ perMessageDeflate.compress(data, true, (err, compressed2) => {
+ if (err) return done(err);
+
+ assert.ok(compressed2.length < compressed1.length);
+ perMessageDeflate.decompress(compressed2, true, (err, data) => {
+ if (err) return done(err);
+
+ assert.ok(data.equals(buf));
+ done();
+ });
+ });
+ });
+ });
+ });
+
+ it('calls the callback when an error occurs (inflate)', (done) => {
+ const perMessageDeflate = new PerMessageDeflate();
+ const data = Buffer.from('something invalid');
+
+ perMessageDeflate.accept([{}]);
+ perMessageDeflate.decompress(data, true, (err) => {
+ assert.ok(err instanceof Error);
+ assert.strictEqual(err.code, 'Z_DATA_ERROR');
+ assert.strictEqual(err.errno, -3);
+ done();
+ });
+ });
+
+ it("doesn't call the callback twice when `maxPayload` is exceeded", (done) => {
+ const perMessageDeflate = new PerMessageDeflate({}, false, 25);
+ const buf = Buffer.from('A'.repeat(50));
+
+ perMessageDeflate.accept([{}]);
+ perMessageDeflate.compress(buf, true, (err, data) => {
+ if (err) return done(err);
+
+ perMessageDeflate.decompress(data, true, (err) => {
+ assert.ok(err instanceof RangeError);
+ assert.strictEqual(err.message, 'Max payload size exceeded');
+ done();
+ });
+ });
+ });
+
+ it('calls the callback if the deflate stream is closed prematurely', (done) => {
+ const perMessageDeflate = new PerMessageDeflate();
+ const buf = Buffer.from('A'.repeat(50));
+
+ perMessageDeflate.accept([{}]);
+ perMessageDeflate.compress(buf, true, (err) => {
+ assert.ok(err instanceof Error);
+ assert.strictEqual(
+ err.message,
+ 'The deflate stream was closed while data was being processed'
+ );
+ done();
+ });
+
+ process.nextTick(() => perMessageDeflate.cleanup());
+ });
+
+ it('recreates the inflate stream if it ends', (done) => {
+ const perMessageDeflate = new PerMessageDeflate();
+ const extensions = extension.parse(
+ 'permessage-deflate; client_no_context_takeover; ' +
+ 'server_no_context_takeover'
+ );
+ const buf = Buffer.from('33343236313533b7000000', 'hex');
+ const expected = Buffer.from('12345678');
+
+ perMessageDeflate.accept(extensions['permessage-deflate']);
+
+ perMessageDeflate.decompress(buf, true, (err, data) => {
+ assert.ok(data.equals(expected));
+
+ perMessageDeflate.decompress(buf, true, (err, data) => {
+ assert.ok(data.equals(expected));
+ done();
+ });
+ });
+ });
+ });
+});
diff --git a/testing/xpcshell/node-ws/test/receiver.test.js b/testing/xpcshell/node-ws/test/receiver.test.js
new file mode 100644
index 0000000000..7ee35f7402
--- /dev/null
+++ b/testing/xpcshell/node-ws/test/receiver.test.js
@@ -0,0 +1,1086 @@
+'use strict';
+
+const assert = require('assert');
+const crypto = require('crypto');
+
+const PerMessageDeflate = require('../lib/permessage-deflate');
+const Receiver = require('../lib/receiver');
+const Sender = require('../lib/sender');
+const { EMPTY_BUFFER, kStatusCode } = require('../lib/constants');
+
+describe('Receiver', () => {
+ it('parses an unmasked text message', (done) => {
+ const receiver = new Receiver();
+
+ receiver.on('message', (data, isBinary) => {
+ assert.deepStrictEqual(data, Buffer.from('Hello'));
+ assert.ok(!isBinary);
+ done();
+ });
+
+ receiver.write(Buffer.from('810548656c6c6f', 'hex'));
+ });
+
+ it('parses a close message', (done) => {
+ const receiver = new Receiver();
+
+ receiver.on('conclude', (code, data) => {
+ assert.strictEqual(code, 1005);
+ assert.strictEqual(data, EMPTY_BUFFER);
+ done();
+ });
+
+ receiver.write(Buffer.from('8800', 'hex'));
+ });
+
+ it('parses a close message spanning multiple writes', (done) => {
+ const receiver = new Receiver();
+
+ receiver.on('conclude', (code, data) => {
+ assert.strictEqual(code, 1000);
+ assert.deepStrictEqual(data, Buffer.from('DONE'));
+ done();
+ });
+
+ receiver.write(Buffer.from('8806', 'hex'));
+ receiver.write(Buffer.from('03e8444F4E45', 'hex'));
+ });
+
+ it('parses a masked text message', (done) => {
+ const receiver = new Receiver({ isServer: true });
+
+ receiver.on('message', (data, isBinary) => {
+ assert.deepStrictEqual(data, Buffer.from('5:::{"name":"echo"}'));
+ assert.ok(!isBinary);
+ done();
+ });
+
+ receiver.write(
+ Buffer.from('81933483a86801b992524fa1c60959e68a5216e6cb005ba1d5', 'hex')
+ );
+ });
+
+ it('parses a masked text message longer than 125 B', (done) => {
+ const receiver = new Receiver({ isServer: true });
+ const msg = Buffer.from('A'.repeat(200));
+
+ const list = Sender.frame(msg, {
+ fin: true,
+ rsv1: false,
+ opcode: 0x01,
+ mask: true,
+ readOnly: true
+ });
+
+ const frame = Buffer.concat(list);
+
+ receiver.on('message', (data, isBinary) => {
+ assert.deepStrictEqual(data, msg);
+ assert.ok(!isBinary);
+ done();
+ });
+
+ receiver.write(frame.slice(0, 2));
+ setImmediate(() => receiver.write(frame.slice(2)));
+ });
+
+ it('parses a really long masked text message', (done) => {
+ const receiver = new Receiver({ isServer: true });
+ const msg = Buffer.from('A'.repeat(64 * 1024));
+
+ const list = Sender.frame(msg, {
+ fin: true,
+ rsv1: false,
+ opcode: 0x01,
+ mask: true,
+ readOnly: true
+ });
+
+ const frame = Buffer.concat(list);
+
+ receiver.on('message', (data, isBinary) => {
+ assert.deepStrictEqual(data, msg);
+ assert.ok(!isBinary);
+ done();
+ });
+
+ receiver.write(frame);
+ });
+
+ it('parses a 300 B fragmented masked text message', (done) => {
+ const receiver = new Receiver({ isServer: true });
+ const msg = Buffer.from('A'.repeat(300));
+
+ const fragment1 = msg.slice(0, 150);
+ const fragment2 = msg.slice(150);
+
+ const options = { rsv1: false, mask: true, readOnly: true };
+
+ const frame1 = Buffer.concat(
+ Sender.frame(fragment1, {
+ fin: false,
+ opcode: 0x01,
+ ...options
+ })
+ );
+ const frame2 = Buffer.concat(
+ Sender.frame(fragment2, {
+ fin: true,
+ opcode: 0x00,
+ ...options
+ })
+ );
+
+ receiver.on('message', (data, isBinary) => {
+ assert.deepStrictEqual(data, msg);
+ assert.ok(!isBinary);
+ done();
+ });
+
+ receiver.write(frame1);
+ receiver.write(frame2);
+ });
+
+ it('parses a ping message', (done) => {
+ const receiver = new Receiver({ isServer: true });
+ const msg = Buffer.from('Hello');
+
+ const list = Sender.frame(msg, {
+ fin: true,
+ rsv1: false,
+ opcode: 0x09,
+ mask: true,
+ readOnly: true
+ });
+
+ const frame = Buffer.concat(list);
+
+ receiver.on('ping', (data) => {
+ assert.deepStrictEqual(data, msg);
+ done();
+ });
+
+ receiver.write(frame);
+ });
+
+ it('parses a ping message with no data', (done) => {
+ const receiver = new Receiver();
+
+ receiver.on('ping', (data) => {
+ assert.strictEqual(data, EMPTY_BUFFER);
+ done();
+ });
+
+ receiver.write(Buffer.from('8900', 'hex'));
+ });
+
+ it('parses a 300 B fragmented masked text message with a ping in the middle (1/2)', (done) => {
+ const receiver = new Receiver({ isServer: true });
+ const msg = Buffer.from('A'.repeat(300));
+ const pingMessage = Buffer.from('Hello');
+
+ const fragment1 = msg.slice(0, 150);
+ const fragment2 = msg.slice(150);
+
+ const options = { rsv1: false, mask: true, readOnly: true };
+
+ const frame1 = Buffer.concat(
+ Sender.frame(fragment1, {
+ fin: false,
+ opcode: 0x01,
+ ...options
+ })
+ );
+ const frame2 = Buffer.concat(
+ Sender.frame(pingMessage, {
+ fin: true,
+ opcode: 0x09,
+ ...options
+ })
+ );
+ const frame3 = Buffer.concat(
+ Sender.frame(fragment2, {
+ fin: true,
+ opcode: 0x00,
+ ...options
+ })
+ );
+
+ let gotPing = false;
+
+ receiver.on('message', (data, isBinary) => {
+ assert.deepStrictEqual(data, msg);
+ assert.ok(!isBinary);
+ assert.ok(gotPing);
+ done();
+ });
+ receiver.on('ping', (data) => {
+ gotPing = true;
+ assert.ok(data.equals(pingMessage));
+ });
+
+ receiver.write(frame1);
+ receiver.write(frame2);
+ receiver.write(frame3);
+ });
+
+ it('parses a 300 B fragmented masked text message with a ping in the middle (2/2)', (done) => {
+ const receiver = new Receiver({ isServer: true });
+ const msg = Buffer.from('A'.repeat(300));
+ const pingMessage = Buffer.from('Hello');
+
+ const fragment1 = msg.slice(0, 150);
+ const fragment2 = msg.slice(150);
+
+ const options = { rsv1: false, mask: true, readOnly: false };
+
+ const frame1 = Buffer.concat(
+ Sender.frame(Buffer.from(fragment1), {
+ fin: false,
+ opcode: 0x01,
+ ...options
+ })
+ );
+ const frame2 = Buffer.concat(
+ Sender.frame(Buffer.from(pingMessage), {
+ fin: true,
+ opcode: 0x09,
+ ...options
+ })
+ );
+ const frame3 = Buffer.concat(
+ Sender.frame(Buffer.from(fragment2), {
+ fin: true,
+ opcode: 0x00,
+ ...options
+ })
+ );
+
+ let chunks = [];
+ const splitBuffer = (buf) => {
+ const i = Math.floor(buf.length / 2);
+ return [buf.slice(0, i), buf.slice(i)];
+ };
+
+ chunks = chunks.concat(splitBuffer(frame1));
+ chunks = chunks.concat(splitBuffer(frame2));
+ chunks = chunks.concat(splitBuffer(frame3));
+
+ let gotPing = false;
+
+ receiver.on('message', (data, isBinary) => {
+ assert.deepStrictEqual(data, msg);
+ assert.ok(!isBinary);
+ assert.ok(gotPing);
+ done();
+ });
+ receiver.on('ping', (data) => {
+ gotPing = true;
+ assert.ok(data.equals(pingMessage));
+ });
+
+ for (let i = 0; i < chunks.length; ++i) {
+ receiver.write(chunks[i]);
+ }
+ });
+
+ it('parses a 100 B masked binary message', (done) => {
+ const receiver = new Receiver({ isServer: true });
+ const msg = crypto.randomBytes(100);
+
+ const list = Sender.frame(msg, {
+ fin: true,
+ rsv1: false,
+ opcode: 0x02,
+ mask: true,
+ readOnly: true
+ });
+
+ const frame = Buffer.concat(list);
+
+ receiver.on('message', (data, isBinary) => {
+ assert.deepStrictEqual(data, msg);
+ assert.ok(isBinary);
+ done();
+ });
+
+ receiver.write(frame);
+ });
+
+ it('parses a 256 B masked binary message', (done) => {
+ const receiver = new Receiver({ isServer: true });
+ const msg = crypto.randomBytes(256);
+
+ const list = Sender.frame(msg, {
+ fin: true,
+ rsv1: false,
+ opcode: 0x02,
+ mask: true,
+ readOnly: true
+ });
+
+ const frame = Buffer.concat(list);
+
+ receiver.on('message', (data, isBinary) => {
+ assert.deepStrictEqual(data, msg);
+ assert.ok(isBinary);
+ done();
+ });
+
+ receiver.write(frame);
+ });
+
+ it('parses a 200 KiB masked binary message', (done) => {
+ const receiver = new Receiver({ isServer: true });
+ const msg = crypto.randomBytes(200 * 1024);
+
+ const list = Sender.frame(msg, {
+ fin: true,
+ rsv1: false,
+ opcode: 0x02,
+ mask: true,
+ readOnly: true
+ });
+
+ const frame = Buffer.concat(list);
+
+ receiver.on('message', (data, isBinary) => {
+ assert.deepStrictEqual(data, msg);
+ assert.ok(isBinary);
+ done();
+ });
+
+ receiver.write(frame);
+ });
+
+ it('parses a 200 KiB unmasked binary message', (done) => {
+ const receiver = new Receiver();
+ const msg = crypto.randomBytes(200 * 1024);
+
+ const list = Sender.frame(msg, {
+ fin: true,
+ rsv1: false,
+ opcode: 0x02,
+ mask: false,
+ readOnly: true
+ });
+
+ const frame = Buffer.concat(list);
+
+ receiver.on('message', (data, isBinary) => {
+ assert.deepStrictEqual(data, msg);
+ assert.ok(isBinary);
+ done();
+ });
+
+ receiver.write(frame);
+ });
+
+ it('parses a compressed message', (done) => {
+ const perMessageDeflate = new PerMessageDeflate();
+ perMessageDeflate.accept([{}]);
+
+ const receiver = new Receiver({
+ extensions: {
+ 'permessage-deflate': perMessageDeflate
+ }
+ });
+ const buf = Buffer.from('Hello');
+
+ receiver.on('message', (data, isBinary) => {
+ assert.deepStrictEqual(data, buf);
+ assert.ok(!isBinary);
+ done();
+ });
+
+ perMessageDeflate.compress(buf, true, (err, data) => {
+ if (err) return done(err);
+
+ receiver.write(Buffer.from([0xc1, data.length]));
+ receiver.write(data);
+ });
+ });
+
+ it('parses a compressed and fragmented message', (done) => {
+ const perMessageDeflate = new PerMessageDeflate();
+ perMessageDeflate.accept([{}]);
+
+ const receiver = new Receiver({
+ extensions: {
+ 'permessage-deflate': perMessageDeflate
+ }
+ });
+ const buf1 = Buffer.from('foo');
+ const buf2 = Buffer.from('bar');
+
+ receiver.on('message', (data, isBinary) => {
+ assert.deepStrictEqual(data, Buffer.concat([buf1, buf2]));
+ assert.ok(!isBinary);
+ done();
+ });
+
+ perMessageDeflate.compress(buf1, false, (err, fragment1) => {
+ if (err) return done(err);
+
+ receiver.write(Buffer.from([0x41, fragment1.length]));
+ receiver.write(fragment1);
+
+ perMessageDeflate.compress(buf2, true, (err, fragment2) => {
+ if (err) return done(err);
+
+ receiver.write(Buffer.from([0x80, fragment2.length]));
+ receiver.write(fragment2);
+ });
+ });
+ });
+
+ it('parses a buffer with thousands of frames', (done) => {
+ const buf = Buffer.allocUnsafe(40000);
+
+ for (let i = 0; i < buf.length; i += 2) {
+ buf[i] = 0x81;
+ buf[i + 1] = 0x00;
+ }
+
+ const receiver = new Receiver();
+ let counter = 0;
+
+ receiver.on('message', (data, isBinary) => {
+ assert.strictEqual(data, EMPTY_BUFFER);
+ assert.ok(!isBinary);
+ if (++counter === 20000) done();
+ });
+
+ receiver.write(buf);
+ });
+
+ it('resets `totalPayloadLength` only on final frame (unfragmented)', (done) => {
+ const receiver = new Receiver({ maxPayload: 10 });
+
+ receiver.on('message', (data, isBinary) => {
+ assert.strictEqual(receiver._totalPayloadLength, 0);
+ assert.deepStrictEqual(data, Buffer.from('Hello'));
+ assert.ok(!isBinary);
+ done();
+ });
+
+ assert.strictEqual(receiver._totalPayloadLength, 0);
+ receiver.write(Buffer.from('810548656c6c6f', 'hex'));
+ });
+
+ it('resets `totalPayloadLength` only on final frame (fragmented)', (done) => {
+ const receiver = new Receiver({ maxPayload: 10 });
+
+ receiver.on('message', (data, isBinary) => {
+ assert.strictEqual(receiver._totalPayloadLength, 0);
+ assert.deepStrictEqual(data, Buffer.from('Hello'));
+ assert.ok(!isBinary);
+ done();
+ });
+
+ assert.strictEqual(receiver._totalPayloadLength, 0);
+ receiver.write(Buffer.from('01024865', 'hex'));
+ assert.strictEqual(receiver._totalPayloadLength, 2);
+ receiver.write(Buffer.from('80036c6c6f', 'hex'));
+ });
+
+ it('resets `totalPayloadLength` only on final frame (fragmented + ping)', (done) => {
+ const receiver = new Receiver({ maxPayload: 10 });
+ let data;
+
+ receiver.on('ping', (buf) => {
+ assert.strictEqual(receiver._totalPayloadLength, 2);
+ data = buf;
+ });
+ receiver.on('message', (buf, isBinary) => {
+ assert.strictEqual(receiver._totalPayloadLength, 0);
+ assert.deepStrictEqual(data, EMPTY_BUFFER);
+ assert.deepStrictEqual(buf, Buffer.from('Hello'));
+ assert.ok(isBinary);
+ done();
+ });
+
+ assert.strictEqual(receiver._totalPayloadLength, 0);
+ receiver.write(Buffer.from('02024865', 'hex'));
+ receiver.write(Buffer.from('8900', 'hex'));
+ receiver.write(Buffer.from('80036c6c6f', 'hex'));
+ });
+
+ it('ignores any data after a close frame', (done) => {
+ const perMessageDeflate = new PerMessageDeflate();
+ perMessageDeflate.accept([{}]);
+
+ const receiver = new Receiver({
+ extensions: {
+ 'permessage-deflate': perMessageDeflate
+ }
+ });
+ const results = [];
+ const push = results.push.bind(results);
+
+ receiver.on('conclude', push).on('message', push);
+ receiver.on('finish', () => {
+ assert.deepStrictEqual(results, [
+ EMPTY_BUFFER,
+ false,
+ 1005,
+ EMPTY_BUFFER
+ ]);
+ done();
+ });
+
+ receiver.write(Buffer.from([0xc1, 0x01, 0x00]));
+ receiver.write(Buffer.from([0x88, 0x00]));
+ receiver.write(Buffer.from([0x81, 0x00]));
+ });
+
+ it('emits an error if RSV1 is on and permessage-deflate is disabled', (done) => {
+ const receiver = new Receiver();
+
+ receiver.on('error', (err) => {
+ assert.ok(err instanceof RangeError);
+ assert.strictEqual(err.code, 'WS_ERR_UNEXPECTED_RSV_1');
+ assert.strictEqual(
+ err.message,
+ 'Invalid WebSocket frame: RSV1 must be clear'
+ );
+ assert.strictEqual(err[kStatusCode], 1002);
+ done();
+ });
+
+ receiver.write(Buffer.from([0xc2, 0x80, 0x00, 0x00, 0x00, 0x00]));
+ });
+
+ it('emits an error if RSV1 is on and opcode is 0', (done) => {
+ const perMessageDeflate = new PerMessageDeflate();
+ perMessageDeflate.accept([{}]);
+
+ const receiver = new Receiver({
+ extensions: {
+ 'permessage-deflate': perMessageDeflate
+ }
+ });
+
+ receiver.on('error', (err) => {
+ assert.ok(err instanceof RangeError);
+ assert.strictEqual(err.code, 'WS_ERR_UNEXPECTED_RSV_1');
+ assert.strictEqual(
+ err.message,
+ 'Invalid WebSocket frame: RSV1 must be clear'
+ );
+ assert.strictEqual(err[kStatusCode], 1002);
+ done();
+ });
+
+ receiver.write(Buffer.from([0x40, 0x00]));
+ });
+
+ it('emits an error if RSV2 is on', (done) => {
+ const receiver = new Receiver();
+
+ receiver.on('error', (err) => {
+ assert.ok(err instanceof RangeError);
+ assert.strictEqual(err.code, 'WS_ERR_UNEXPECTED_RSV_2_3');
+ assert.strictEqual(
+ err.message,
+ 'Invalid WebSocket frame: RSV2 and RSV3 must be clear'
+ );
+ assert.strictEqual(err[kStatusCode], 1002);
+ done();
+ });
+
+ receiver.write(Buffer.from([0xa2, 0x00]));
+ });
+
+ it('emits an error if RSV3 is on', (done) => {
+ const receiver = new Receiver();
+
+ receiver.on('error', (err) => {
+ assert.ok(err instanceof RangeError);
+ assert.strictEqual(err.code, 'WS_ERR_UNEXPECTED_RSV_2_3');
+ assert.strictEqual(
+ err.message,
+ 'Invalid WebSocket frame: RSV2 and RSV3 must be clear'
+ );
+ assert.strictEqual(err[kStatusCode], 1002);
+ done();
+ });
+
+ receiver.write(Buffer.from([0x92, 0x00]));
+ });
+
+ it('emits an error if the first frame in a fragmented message has opcode 0', (done) => {
+ const receiver = new Receiver();
+
+ receiver.on('error', (err) => {
+ assert.ok(err instanceof RangeError);
+ assert.strictEqual(err.code, 'WS_ERR_INVALID_OPCODE');
+ assert.strictEqual(
+ err.message,
+ 'Invalid WebSocket frame: invalid opcode 0'
+ );
+ assert.strictEqual(err[kStatusCode], 1002);
+ done();
+ });
+
+ receiver.write(Buffer.from([0x00, 0x00]));
+ });
+
+ it('emits an error if a frame has opcode 1 in the middle of a fragmented message', (done) => {
+ const receiver = new Receiver();
+
+ receiver.on('error', (err) => {
+ assert.ok(err instanceof RangeError);
+ assert.strictEqual(err.code, 'WS_ERR_INVALID_OPCODE');
+ assert.strictEqual(
+ err.message,
+ 'Invalid WebSocket frame: invalid opcode 1'
+ );
+ assert.strictEqual(err[kStatusCode], 1002);
+ done();
+ });
+
+ receiver.write(Buffer.from([0x01, 0x00]));
+ receiver.write(Buffer.from([0x01, 0x00]));
+ });
+
+ it('emits an error if a frame has opcode 2 in the middle of a fragmented message', (done) => {
+ const receiver = new Receiver();
+
+ receiver.on('error', (err) => {
+ assert.ok(err instanceof RangeError);
+ assert.strictEqual(err.code, 'WS_ERR_INVALID_OPCODE');
+ assert.strictEqual(
+ err.message,
+ 'Invalid WebSocket frame: invalid opcode 2'
+ );
+ assert.strictEqual(err[kStatusCode], 1002);
+ done();
+ });
+
+ receiver.write(Buffer.from([0x01, 0x00]));
+ receiver.write(Buffer.from([0x02, 0x00]));
+ });
+
+ it('emits an error if a control frame has the FIN bit off', (done) => {
+ const receiver = new Receiver();
+
+ receiver.on('error', (err) => {
+ assert.ok(err instanceof RangeError);
+ assert.strictEqual(err.code, 'WS_ERR_EXPECTED_FIN');
+ assert.strictEqual(
+ err.message,
+ 'Invalid WebSocket frame: FIN must be set'
+ );
+ assert.strictEqual(err[kStatusCode], 1002);
+ done();
+ });
+
+ receiver.write(Buffer.from([0x09, 0x00]));
+ });
+
+ it('emits an error if a control frame has the RSV1 bit on', (done) => {
+ const perMessageDeflate = new PerMessageDeflate();
+ perMessageDeflate.accept([{}]);
+
+ const receiver = new Receiver({
+ extensions: {
+ 'permessage-deflate': perMessageDeflate
+ }
+ });
+
+ receiver.on('error', (err) => {
+ assert.ok(err instanceof RangeError);
+ assert.strictEqual(err.code, 'WS_ERR_UNEXPECTED_RSV_1');
+ assert.strictEqual(
+ err.message,
+ 'Invalid WebSocket frame: RSV1 must be clear'
+ );
+ assert.strictEqual(err[kStatusCode], 1002);
+ done();
+ });
+
+ receiver.write(Buffer.from([0xc9, 0x00]));
+ });
+
+ it('emits an error if a control frame has the FIN bit off', (done) => {
+ const receiver = new Receiver();
+
+ receiver.on('error', (err) => {
+ assert.ok(err instanceof RangeError);
+ assert.strictEqual(err.code, 'WS_ERR_EXPECTED_FIN');
+ assert.strictEqual(
+ err.message,
+ 'Invalid WebSocket frame: FIN must be set'
+ );
+ assert.strictEqual(err[kStatusCode], 1002);
+ done();
+ });
+
+ receiver.write(Buffer.from([0x09, 0x00]));
+ });
+
+ it('emits an error if a frame has the MASK bit off (server mode)', (done) => {
+ const receiver = new Receiver({ isServer: true });
+
+ receiver.on('error', (err) => {
+ assert.ok(err instanceof RangeError);
+ assert.strictEqual(err.code, 'WS_ERR_EXPECTED_MASK');
+ assert.strictEqual(
+ err.message,
+ 'Invalid WebSocket frame: MASK must be set'
+ );
+ assert.strictEqual(err[kStatusCode], 1002);
+ done();
+ });
+
+ receiver.write(Buffer.from([0x81, 0x02, 0x68, 0x69]));
+ });
+
+ it('emits an error if a frame has the MASK bit on (client mode)', (done) => {
+ const receiver = new Receiver();
+
+ receiver.on('error', (err) => {
+ assert.ok(err instanceof RangeError);
+ assert.strictEqual(err.code, 'WS_ERR_UNEXPECTED_MASK');
+ assert.strictEqual(
+ err.message,
+ 'Invalid WebSocket frame: MASK must be clear'
+ );
+ assert.strictEqual(err[kStatusCode], 1002);
+ done();
+ });
+
+ receiver.write(
+ Buffer.from([0x81, 0x82, 0x56, 0x3a, 0xac, 0x80, 0x3e, 0x53])
+ );
+ });
+
+ it('emits an error if a control frame has a payload bigger than 125 B', (done) => {
+ const receiver = new Receiver();
+
+ receiver.on('error', (err) => {
+ assert.ok(err instanceof RangeError);
+ assert.strictEqual(err.code, 'WS_ERR_INVALID_CONTROL_PAYLOAD_LENGTH');
+ assert.strictEqual(
+ err.message,
+ 'Invalid WebSocket frame: invalid payload length 126'
+ );
+ assert.strictEqual(err[kStatusCode], 1002);
+ done();
+ });
+
+ receiver.write(Buffer.from([0x89, 0x7e]));
+ });
+
+ it('emits an error if a data frame has a payload bigger than 2^53 - 1 B', (done) => {
+ const receiver = new Receiver();
+
+ receiver.on('error', (err) => {
+ assert.ok(err instanceof RangeError);
+ assert.strictEqual(err.code, 'WS_ERR_UNSUPPORTED_DATA_PAYLOAD_LENGTH');
+ assert.strictEqual(
+ err.message,
+ 'Unsupported WebSocket frame: payload length > 2^53 - 1'
+ );
+ assert.strictEqual(err[kStatusCode], 1009);
+ done();
+ });
+
+ receiver.write(Buffer.from([0x82, 0x7f]));
+ setImmediate(() =>
+ receiver.write(
+ Buffer.from([0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])
+ )
+ );
+ });
+
+ it('emits an error if a text frame contains invalid UTF-8 data (1/2)', (done) => {
+ const receiver = new Receiver();
+
+ receiver.on('error', (err) => {
+ assert.ok(err instanceof Error);
+ assert.strictEqual(err.code, 'WS_ERR_INVALID_UTF8');
+ assert.strictEqual(
+ err.message,
+ 'Invalid WebSocket frame: invalid UTF-8 sequence'
+ );
+ assert.strictEqual(err[kStatusCode], 1007);
+ done();
+ });
+
+ receiver.write(Buffer.from([0x81, 0x04, 0xce, 0xba, 0xe1, 0xbd]));
+ });
+
+ it('emits an error if a text frame contains invalid UTF-8 data (2/2)', (done) => {
+ const perMessageDeflate = new PerMessageDeflate();
+ perMessageDeflate.accept([{}]);
+
+ const receiver = new Receiver({
+ extensions: {
+ 'permessage-deflate': perMessageDeflate
+ }
+ });
+ const buf = Buffer.from([0xce, 0xba, 0xe1, 0xbd]);
+
+ receiver.on('error', (err) => {
+ assert.ok(err instanceof Error);
+ assert.strictEqual(err.code, 'WS_ERR_INVALID_UTF8');
+ assert.strictEqual(
+ err.message,
+ 'Invalid WebSocket frame: invalid UTF-8 sequence'
+ );
+ assert.strictEqual(err[kStatusCode], 1007);
+ done();
+ });
+
+ perMessageDeflate.compress(buf, true, (err, data) => {
+ if (err) return done(err);
+
+ receiver.write(Buffer.from([0xc1, data.length]));
+ receiver.write(data);
+ });
+ });
+
+ it('emits an error if a close frame has a payload of 1 B', (done) => {
+ const receiver = new Receiver();
+
+ receiver.on('error', (err) => {
+ assert.ok(err instanceof RangeError);
+ assert.strictEqual(err.code, 'WS_ERR_INVALID_CONTROL_PAYLOAD_LENGTH');
+ assert.strictEqual(
+ err.message,
+ 'Invalid WebSocket frame: invalid payload length 1'
+ );
+ assert.strictEqual(err[kStatusCode], 1002);
+ done();
+ });
+
+ receiver.write(Buffer.from([0x88, 0x01, 0x00]));
+ });
+
+ it('emits an error if a close frame contains an invalid close code', (done) => {
+ const receiver = new Receiver();
+
+ receiver.on('error', (err) => {
+ assert.ok(err instanceof RangeError);
+ assert.strictEqual(err.code, 'WS_ERR_INVALID_CLOSE_CODE');
+ assert.strictEqual(
+ err.message,
+ 'Invalid WebSocket frame: invalid status code 0'
+ );
+ assert.strictEqual(err[kStatusCode], 1002);
+ done();
+ });
+
+ receiver.write(Buffer.from([0x88, 0x02, 0x00, 0x00]));
+ });
+
+ it('emits an error if a close frame contains invalid UTF-8 data', (done) => {
+ const receiver = new Receiver();
+
+ receiver.on('error', (err) => {
+ assert.ok(err instanceof Error);
+ assert.strictEqual(err.code, 'WS_ERR_INVALID_UTF8');
+ assert.strictEqual(
+ err.message,
+ 'Invalid WebSocket frame: invalid UTF-8 sequence'
+ );
+ assert.strictEqual(err[kStatusCode], 1007);
+ done();
+ });
+
+ receiver.write(
+ Buffer.from([0x88, 0x06, 0x03, 0xef, 0xce, 0xba, 0xe1, 0xbd])
+ );
+ });
+
+ it('emits an error if a frame payload length is bigger than `maxPayload`', (done) => {
+ const receiver = new Receiver({ isServer: true, maxPayload: 20 * 1024 });
+ const msg = crypto.randomBytes(200 * 1024);
+
+ const list = Sender.frame(msg, {
+ fin: true,
+ rsv1: false,
+ opcode: 0x02,
+ mask: true,
+ readOnly: true
+ });
+
+ const frame = Buffer.concat(list);
+
+ receiver.on('error', (err) => {
+ assert.ok(err instanceof RangeError);
+ assert.strictEqual(err.code, 'WS_ERR_UNSUPPORTED_MESSAGE_LENGTH');
+ assert.strictEqual(err.message, 'Max payload size exceeded');
+ assert.strictEqual(err[kStatusCode], 1009);
+ done();
+ });
+
+ receiver.write(frame);
+ });
+
+ it('emits an error if the message length exceeds `maxPayload`', (done) => {
+ const perMessageDeflate = new PerMessageDeflate({}, false, 25);
+ perMessageDeflate.accept([{}]);
+
+ const receiver = new Receiver({
+ extensions: { 'permessage-deflate': perMessageDeflate },
+ isServer: false,
+ maxPayload: 25
+ });
+ const buf = Buffer.from('A'.repeat(50));
+
+ receiver.on('error', (err) => {
+ assert.ok(err instanceof RangeError);
+ assert.strictEqual(err.code, 'WS_ERR_UNSUPPORTED_MESSAGE_LENGTH');
+ assert.strictEqual(err.message, 'Max payload size exceeded');
+ assert.strictEqual(err[kStatusCode], 1009);
+ done();
+ });
+
+ perMessageDeflate.compress(buf, true, (err, data) => {
+ if (err) return done(err);
+
+ receiver.write(Buffer.from([0xc1, data.length]));
+ receiver.write(data);
+ });
+ });
+
+ it('emits an error if the sum of fragment lengths exceeds `maxPayload`', (done) => {
+ const perMessageDeflate = new PerMessageDeflate({}, false, 25);
+ perMessageDeflate.accept([{}]);
+
+ const receiver = new Receiver({
+ extensions: { 'permessage-deflate': perMessageDeflate },
+ isServer: false,
+ maxPayload: 25
+ });
+ const buf = Buffer.from('A'.repeat(15));
+
+ receiver.on('error', (err) => {
+ assert.ok(err instanceof RangeError);
+ assert.strictEqual(err.code, 'WS_ERR_UNSUPPORTED_MESSAGE_LENGTH');
+ assert.strictEqual(err.message, 'Max payload size exceeded');
+ assert.strictEqual(err[kStatusCode], 1009);
+ done();
+ });
+
+ perMessageDeflate.compress(buf, false, (err, fragment1) => {
+ if (err) return done(err);
+
+ receiver.write(Buffer.from([0x41, fragment1.length]));
+ receiver.write(fragment1);
+
+ perMessageDeflate.compress(buf, true, (err, fragment2) => {
+ if (err) return done(err);
+
+ receiver.write(Buffer.from([0x80, fragment2.length]));
+ receiver.write(fragment2);
+ });
+ });
+ });
+
+ it("honors the 'nodebuffer' binary type", (done) => {
+ const receiver = new Receiver();
+ const frags = [
+ crypto.randomBytes(7321),
+ crypto.randomBytes(137),
+ crypto.randomBytes(285787),
+ crypto.randomBytes(3)
+ ];
+
+ receiver.on('message', (data, isBinary) => {
+ assert.deepStrictEqual(data, Buffer.concat(frags));
+ assert.ok(isBinary);
+ done();
+ });
+
+ frags.forEach((frag, i) => {
+ Sender.frame(frag, {
+ fin: i === frags.length - 1,
+ opcode: i === 0 ? 2 : 0,
+ readOnly: true,
+ mask: false,
+ rsv1: false
+ }).forEach((buf) => receiver.write(buf));
+ });
+ });
+
+ it("honors the 'arraybuffer' binary type", (done) => {
+ const receiver = new Receiver({ binaryType: 'arraybuffer' });
+ const frags = [
+ crypto.randomBytes(19221),
+ crypto.randomBytes(954),
+ crypto.randomBytes(623987)
+ ];
+
+ receiver.on('message', (data, isBinary) => {
+ assert.ok(data instanceof ArrayBuffer);
+ assert.deepStrictEqual(Buffer.from(data), Buffer.concat(frags));
+ assert.ok(isBinary);
+ done();
+ });
+
+ frags.forEach((frag, i) => {
+ Sender.frame(frag, {
+ fin: i === frags.length - 1,
+ opcode: i === 0 ? 2 : 0,
+ readOnly: true,
+ mask: false,
+ rsv1: false
+ }).forEach((buf) => receiver.write(buf));
+ });
+ });
+
+ it("honors the 'fragments' binary type", (done) => {
+ const receiver = new Receiver({ binaryType: 'fragments' });
+ const frags = [
+ crypto.randomBytes(17),
+ crypto.randomBytes(419872),
+ crypto.randomBytes(83),
+ crypto.randomBytes(9928),
+ crypto.randomBytes(1)
+ ];
+
+ receiver.on('message', (data, isBinary) => {
+ assert.deepStrictEqual(data, frags);
+ assert.ok(isBinary);
+ done();
+ });
+
+ frags.forEach((frag, i) => {
+ Sender.frame(frag, {
+ fin: i === frags.length - 1,
+ opcode: i === 0 ? 2 : 0,
+ readOnly: true,
+ mask: false,
+ rsv1: false
+ }).forEach((buf) => receiver.write(buf));
+ });
+ });
+
+ it('honors the `skipUTF8Validation` option (1/2)', (done) => {
+ const receiver = new Receiver({ skipUTF8Validation: true });
+
+ receiver.on('message', (data, isBinary) => {
+ assert.deepStrictEqual(data, Buffer.from([0xf8]));
+ assert.ok(!isBinary);
+ done();
+ });
+
+ receiver.write(Buffer.from([0x81, 0x01, 0xf8]));
+ });
+
+ it('honors the `skipUTF8Validation` option (2/2)', (done) => {
+ const receiver = new Receiver({ skipUTF8Validation: true });
+
+ receiver.on('conclude', (code, data) => {
+ assert.strictEqual(code, 1000);
+ assert.deepStrictEqual(data, Buffer.from([0xf8]));
+ done();
+ });
+
+ receiver.write(Buffer.from([0x88, 0x03, 0x03, 0xe8, 0xf8]));
+ });
+});
diff --git a/testing/xpcshell/node-ws/test/sender.test.js b/testing/xpcshell/node-ws/test/sender.test.js
new file mode 100644
index 0000000000..532239fa1a
--- /dev/null
+++ b/testing/xpcshell/node-ws/test/sender.test.js
@@ -0,0 +1,370 @@
+'use strict';
+
+const assert = require('assert');
+
+const extension = require('../lib/extension');
+const PerMessageDeflate = require('../lib/permessage-deflate');
+const Sender = require('../lib/sender');
+const { EMPTY_BUFFER } = require('../lib/constants');
+
+class MockSocket {
+ constructor({ write } = {}) {
+ this.readable = true;
+ this.writable = true;
+
+ if (write) this.write = write;
+ }
+
+ cork() {}
+ write() {}
+ uncork() {}
+}
+
+describe('Sender', () => {
+ describe('.frame', () => {
+ it('does not mutate the input buffer if data is `readOnly`', () => {
+ const buf = Buffer.from([1, 2, 3, 4, 5]);
+
+ Sender.frame(buf, {
+ readOnly: true,
+ rsv1: false,
+ mask: true,
+ opcode: 2,
+ fin: true
+ });
+
+ assert.ok(buf.equals(Buffer.from([1, 2, 3, 4, 5])));
+ });
+
+ it('honors the `rsv1` option', () => {
+ const list = Sender.frame(EMPTY_BUFFER, {
+ readOnly: false,
+ mask: false,
+ rsv1: true,
+ opcode: 1,
+ fin: true
+ });
+
+ assert.strictEqual(list[0][0] & 0x40, 0x40);
+ });
+
+ it('accepts a string as first argument', () => {
+ const list = Sender.frame('€', {
+ readOnly: false,
+ rsv1: false,
+ mask: false,
+ opcode: 1,
+ fin: true
+ });
+
+ assert.deepStrictEqual(list[0], Buffer.from('8103', 'hex'));
+ assert.deepStrictEqual(list[1], Buffer.from('e282ac', 'hex'));
+ });
+ });
+
+ describe('#send', () => {
+ it('compresses data if compress option is enabled', (done) => {
+ const chunks = [];
+ const perMessageDeflate = new PerMessageDeflate();
+ const mockSocket = new MockSocket({
+ write: (chunk) => {
+ chunks.push(chunk);
+ if (chunks.length !== 6) return;
+
+ assert.strictEqual(chunks[0].length, 2);
+ assert.strictEqual(chunks[0][0] & 0x40, 0x40);
+
+ assert.strictEqual(chunks[2].length, 2);
+ assert.strictEqual(chunks[2][0] & 0x40, 0x40);
+
+ assert.strictEqual(chunks[4].length, 2);
+ assert.strictEqual(chunks[4][0] & 0x40, 0x40);
+ done();
+ }
+ });
+ const sender = new Sender(mockSocket, {
+ 'permessage-deflate': perMessageDeflate
+ });
+
+ perMessageDeflate.accept([{}]);
+
+ const options = { compress: true, fin: true };
+ const array = new Uint8Array([0x68, 0x69]);
+
+ sender.send(array.buffer, options);
+ sender.send(array, options);
+ sender.send('hi', options);
+ });
+
+ describe('when context takeover is disabled', () => {
+ it('honors the compression threshold', (done) => {
+ const chunks = [];
+ const perMessageDeflate = new PerMessageDeflate();
+ const mockSocket = new MockSocket({
+ write: (chunk) => {
+ chunks.push(chunk);
+ if (chunks.length !== 2) return;
+
+ assert.strictEqual(chunks[0].length, 2);
+ assert.notStrictEqual(chunk[0][0] & 0x40, 0x40);
+ assert.strictEqual(chunks[1], 'hi');
+ done();
+ }
+ });
+ const sender = new Sender(mockSocket, {
+ 'permessage-deflate': perMessageDeflate
+ });
+ const extensions = extension.parse(
+ 'permessage-deflate; client_no_context_takeover'
+ );
+
+ perMessageDeflate.accept(extensions['permessage-deflate']);
+
+ sender.send('hi', { compress: true, fin: true });
+ });
+
+ it('compresses all fragments of a fragmented message', (done) => {
+ const chunks = [];
+ const perMessageDeflate = new PerMessageDeflate({ threshold: 3 });
+ const mockSocket = new MockSocket({
+ write: (chunk) => {
+ chunks.push(chunk);
+ if (chunks.length !== 4) return;
+
+ assert.strictEqual(chunks[0].length, 2);
+ assert.strictEqual(chunks[0][0] & 0x40, 0x40);
+ assert.strictEqual(chunks[1].length, 9);
+
+ assert.strictEqual(chunks[2].length, 2);
+ assert.strictEqual(chunks[2][0] & 0x40, 0x00);
+ assert.strictEqual(chunks[3].length, 4);
+ done();
+ }
+ });
+ const sender = new Sender(mockSocket, {
+ 'permessage-deflate': perMessageDeflate
+ });
+ const extensions = extension.parse(
+ 'permessage-deflate; client_no_context_takeover'
+ );
+
+ perMessageDeflate.accept(extensions['permessage-deflate']);
+
+ sender.send('123', { compress: true, fin: false });
+ sender.send('12', { compress: true, fin: true });
+ });
+
+ it('does not compress any fragments of a fragmented message', (done) => {
+ const chunks = [];
+ const perMessageDeflate = new PerMessageDeflate({ threshold: 3 });
+ const mockSocket = new MockSocket({
+ write: (chunk) => {
+ chunks.push(chunk);
+ if (chunks.length !== 4) return;
+
+ assert.strictEqual(chunks[0].length, 2);
+ assert.strictEqual(chunks[0][0] & 0x40, 0x00);
+ assert.strictEqual(chunks[1].length, 2);
+
+ assert.strictEqual(chunks[2].length, 2);
+ assert.strictEqual(chunks[2][0] & 0x40, 0x00);
+ assert.strictEqual(chunks[3].length, 3);
+ done();
+ }
+ });
+ const sender = new Sender(mockSocket, {
+ 'permessage-deflate': perMessageDeflate
+ });
+ const extensions = extension.parse(
+ 'permessage-deflate; client_no_context_takeover'
+ );
+
+ perMessageDeflate.accept(extensions['permessage-deflate']);
+
+ sender.send('12', { compress: true, fin: false });
+ sender.send('123', { compress: true, fin: true });
+ });
+
+ it('compresses empty buffer as first fragment', (done) => {
+ const chunks = [];
+ const perMessageDeflate = new PerMessageDeflate({ threshold: 0 });
+ const mockSocket = new MockSocket({
+ write: (chunk) => {
+ chunks.push(chunk);
+ if (chunks.length !== 4) return;
+
+ assert.strictEqual(chunks[0].length, 2);
+ assert.strictEqual(chunks[0][0] & 0x40, 0x40);
+ assert.strictEqual(chunks[1].length, 5);
+
+ assert.strictEqual(chunks[2].length, 2);
+ assert.strictEqual(chunks[2][0] & 0x40, 0x00);
+ assert.strictEqual(chunks[3].length, 6);
+ done();
+ }
+ });
+ const sender = new Sender(mockSocket, {
+ 'permessage-deflate': perMessageDeflate
+ });
+ const extensions = extension.parse(
+ 'permessage-deflate; client_no_context_takeover'
+ );
+
+ perMessageDeflate.accept(extensions['permessage-deflate']);
+
+ sender.send(Buffer.alloc(0), { compress: true, fin: false });
+ sender.send('data', { compress: true, fin: true });
+ });
+
+ it('compresses empty buffer as last fragment', (done) => {
+ const chunks = [];
+ const perMessageDeflate = new PerMessageDeflate({ threshold: 0 });
+ const mockSocket = new MockSocket({
+ write: (chunk) => {
+ chunks.push(chunk);
+ if (chunks.length !== 4) return;
+
+ assert.strictEqual(chunks[0].length, 2);
+ assert.strictEqual(chunks[0][0] & 0x40, 0x40);
+ assert.strictEqual(chunks[1].length, 10);
+
+ assert.strictEqual(chunks[2].length, 2);
+ assert.strictEqual(chunks[2][0] & 0x40, 0x00);
+ assert.strictEqual(chunks[3].length, 1);
+ done();
+ }
+ });
+ const sender = new Sender(mockSocket, {
+ 'permessage-deflate': perMessageDeflate
+ });
+ const extensions = extension.parse(
+ 'permessage-deflate; client_no_context_takeover'
+ );
+
+ perMessageDeflate.accept(extensions['permessage-deflate']);
+
+ sender.send('data', { compress: true, fin: false });
+ sender.send(Buffer.alloc(0), { compress: true, fin: true });
+ });
+ });
+ });
+
+ describe('#ping', () => {
+ it('works with multiple types of data', (done) => {
+ const perMessageDeflate = new PerMessageDeflate();
+ let count = 0;
+ const mockSocket = new MockSocket({
+ write: (data) => {
+ if (++count < 3) return;
+
+ if (count % 2) {
+ assert.ok(data.equals(Buffer.from([0x89, 0x02])));
+ } else if (count < 8) {
+ assert.ok(data.equals(Buffer.from([0x68, 0x69])));
+ } else {
+ assert.strictEqual(data, 'hi');
+ done();
+ }
+ }
+ });
+ const sender = new Sender(mockSocket, {
+ 'permessage-deflate': perMessageDeflate
+ });
+
+ perMessageDeflate.accept([{}]);
+
+ const array = new Uint8Array([0x68, 0x69]);
+
+ sender.send('foo', { compress: true, fin: true });
+ sender.ping(array.buffer, false);
+ sender.ping(array, false);
+ sender.ping('hi', false);
+ });
+ });
+
+ describe('#pong', () => {
+ it('works with multiple types of data', (done) => {
+ const perMessageDeflate = new PerMessageDeflate();
+ let count = 0;
+ const mockSocket = new MockSocket({
+ write: (data) => {
+ if (++count < 3) return;
+
+ if (count % 2) {
+ assert.ok(data.equals(Buffer.from([0x8a, 0x02])));
+ } else if (count < 8) {
+ assert.ok(data.equals(Buffer.from([0x68, 0x69])));
+ } else {
+ assert.strictEqual(data, 'hi');
+ done();
+ }
+ }
+ });
+ const sender = new Sender(mockSocket, {
+ 'permessage-deflate': perMessageDeflate
+ });
+
+ perMessageDeflate.accept([{}]);
+
+ const array = new Uint8Array([0x68, 0x69]);
+
+ sender.send('foo', { compress: true, fin: true });
+ sender.pong(array.buffer, false);
+ sender.pong(array, false);
+ sender.pong('hi', false);
+ });
+ });
+
+ describe('#close', () => {
+ it('throws an error if the first argument is invalid', () => {
+ const mockSocket = new MockSocket();
+ const sender = new Sender(mockSocket);
+
+ assert.throws(
+ () => sender.close('error'),
+ /^TypeError: First argument must be a valid error code number$/
+ );
+
+ assert.throws(
+ () => sender.close(1004),
+ /^TypeError: First argument must be a valid error code number$/
+ );
+ });
+
+ it('throws an error if the message is greater than 123 bytes', () => {
+ const mockSocket = new MockSocket();
+ const sender = new Sender(mockSocket);
+
+ assert.throws(
+ () => sender.close(1000, 'a'.repeat(124)),
+ /^RangeError: The message must not be greater than 123 bytes$/
+ );
+ });
+
+ it('should consume all data before closing', (done) => {
+ const perMessageDeflate = new PerMessageDeflate();
+
+ let count = 0;
+ const mockSocket = new MockSocket({
+ write: (data, cb) => {
+ count++;
+ if (cb) cb();
+ }
+ });
+ const sender = new Sender(mockSocket, {
+ 'permessage-deflate': perMessageDeflate
+ });
+
+ perMessageDeflate.accept([{}]);
+
+ sender.send('foo', { compress: true, fin: true });
+ sender.send('bar', { compress: true, fin: true });
+ sender.send('baz', { compress: true, fin: true });
+
+ sender.close(1000, undefined, false, () => {
+ assert.strictEqual(count, 8);
+ done();
+ });
+ });
+ });
+});
diff --git a/testing/xpcshell/node-ws/test/subprotocol.test.js b/testing/xpcshell/node-ws/test/subprotocol.test.js
new file mode 100644
index 0000000000..91dd5d69d8
--- /dev/null
+++ b/testing/xpcshell/node-ws/test/subprotocol.test.js
@@ -0,0 +1,91 @@
+'use strict';
+
+const assert = require('assert');
+
+const { parse } = require('../lib/subprotocol');
+
+describe('subprotocol', () => {
+ describe('parse', () => {
+ it('parses a single subprotocol', () => {
+ assert.deepStrictEqual(parse('foo'), new Set(['foo']));
+ });
+
+ it('parses multiple subprotocols', () => {
+ assert.deepStrictEqual(
+ parse('foo,bar,baz'),
+ new Set(['foo', 'bar', 'baz'])
+ );
+ });
+
+ it('ignores the optional white spaces', () => {
+ const header = 'foo , bar\t, \tbaz\t , qux\t\t,norf';
+
+ assert.deepStrictEqual(
+ parse(header),
+ new Set(['foo', 'bar', 'baz', 'qux', 'norf'])
+ );
+ });
+
+ it('throws an error if a subprotocol is empty', () => {
+ [
+ [',', 0],
+ ['foo,,', 4],
+ ['foo, ,', 6]
+ ].forEach((element) => {
+ assert.throws(
+ () => parse(element[0]),
+ new RegExp(
+ `^SyntaxError: Unexpected character at index ${element[1]}$`
+ )
+ );
+ });
+ });
+
+ it('throws an error if a subprotocol is duplicated', () => {
+ ['foo,foo,bar', 'foo,bar,foo'].forEach((header) => {
+ assert.throws(
+ () => parse(header),
+ /^SyntaxError: The "foo" subprotocol is duplicated$/
+ );
+ });
+ });
+
+ it('throws an error if a white space is misplaced', () => {
+ [
+ ['f oo', 2],
+ [' foo', 0]
+ ].forEach((element) => {
+ assert.throws(
+ () => parse(element[0]),
+ new RegExp(
+ `^SyntaxError: Unexpected character at index ${element[1]}$`
+ )
+ );
+ });
+ });
+
+ it('throws an error if a subprotocol contains invalid characters', () => {
+ [
+ ['f@o', 1],
+ ['f\\oo', 1],
+ ['foo,b@r', 5]
+ ].forEach((element) => {
+ assert.throws(
+ () => parse(element[0]),
+ new RegExp(
+ `^SyntaxError: Unexpected character at index ${element[1]}$`
+ )
+ );
+ });
+ });
+
+ it('throws an error if the header value ends prematurely', () => {
+ ['foo ', 'foo, ', 'foo,bar ', 'foo,bar,'].forEach((header) => {
+ assert.throws(
+ () => parse(header),
+ /^SyntaxError: Unexpected end of input$/
+ );
+ });
+ });
+ });
+});
diff --git a/testing/xpcshell/node-ws/test/validation.test.js b/testing/xpcshell/node-ws/test/validation.test.js
new file mode 100644
index 0000000000..5718b12f02
--- /dev/null
+++ b/testing/xpcshell/node-ws/test/validation.test.js
@@ -0,0 +1,52 @@
+'use strict';
+
+const assert = require('assert');
+
+const { isValidUTF8 } = require('../lib/validation');
+
+describe('extension', () => {
+ describe('isValidUTF8', () => {
+ it('returns false if it finds invalid bytes', () => {
+ assert.strictEqual(isValidUTF8(Buffer.from([0xf8])), false);
+ });
+
+ it('returns false for overlong encodings', () => {
+ assert.strictEqual(isValidUTF8(Buffer.from([0xc0, 0xa0])), false);
+ assert.strictEqual(isValidUTF8(Buffer.from([0xe0, 0x80, 0xa0])), false);
+ assert.strictEqual(
+ isValidUTF8(Buffer.from([0xf0, 0x80, 0x80, 0xa0])),
+ false
+ );
+ });
+
+ it('returns false for code points in the range U+D800 - U+DFFF', () => {
+ for (let i = 0xa0; i < 0xc0; i++) {
+ for (let j = 0x80; j < 0xc0; j++) {
+ assert.strictEqual(isValidUTF8(Buffer.from([0xed, i, j])), false);
+ }
+ }
+ });
+
+ it('returns false for code points greater than U+10FFFF', () => {
+ assert.strictEqual(
+ isValidUTF8(Buffer.from([0xf4, 0x90, 0x80, 0x80])),
+ false
+ );
+ assert.strictEqual(
+ isValidUTF8(Buffer.from([0xf5, 0x80, 0x80, 0x80])),
+ false
+ );
+ });
+
+ it('returns true for a well-formed UTF-8 byte sequence', () => {
+ // prettier-ignore
+ const buf = Buffer.from([
+ 0xe2, 0x82, 0xAC, // €
+ 0xf0, 0x90, 0x8c, 0x88, // 𐍈
+ 0x24 // $
+ ]);
+
+ assert.strictEqual(isValidUTF8(buf), true);
+ });
+ });
+});
diff --git a/testing/xpcshell/node-ws/test/websocket-server.test.js b/testing/xpcshell/node-ws/test/websocket-server.test.js
new file mode 100644
index 0000000000..12928ff495
--- /dev/null
+++ b/testing/xpcshell/node-ws/test/websocket-server.test.js
@@ -0,0 +1,1284 @@
+/* eslint no-unused-vars: ["error", { "varsIgnorePattern": "^ws$" }] */
+
+'use strict';
+
+const assert = require('assert');
+const crypto = require('crypto');
+const https = require('https');
+const http = require('http');
+const path = require('path');
+const net = require('net');
+const fs = require('fs');
+const os = require('os');
+
+const Sender = require('../lib/sender');
+const WebSocket = require('..');
+const { NOOP } = require('../lib/constants');
+
+describe('WebSocketServer', () => {
+ describe('#ctor', () => {
+ it('throws an error if no option object is passed', () => {
+ assert.throws(
+ () => new WebSocket.Server(),
+ new RegExp(
+ '^TypeError: One and only one of the "port", "server", or ' +
+ '"noServer" options must be specified$'
+ )
+ );
+ });
+
+ describe('options', () => {
+ it('throws an error if required options are not specified', () => {
+ assert.throws(
+ () => new WebSocket.Server({}),
+ new RegExp(
+ '^TypeError: One and only one of the "port", "server", or ' +
+ '"noServer" options must be specified$'
+ )
+ );
+ });
+
+ it('throws an error if mutually exclusive options are specified', () => {
+ const server = http.createServer();
+ const variants = [
+ { port: 0, noServer: true, server },
+ { port: 0, noServer: true },
+ { port: 0, server },
+ { noServer: true, server }
+ ];
+
+ for (const options of variants) {
+ assert.throws(
+ () => new WebSocket.Server(options),
+ new RegExp(
+ '^TypeError: One and only one of the "port", "server", or ' +
+ '"noServer" options must be specified$'
+ )
+ );
+ }
+ });
+
+ it('exposes options passed to constructor', (done) => {
+ const wss = new WebSocket.Server({ port: 0 }, () => {
+ assert.strictEqual(wss.options.port, 0);
+ wss.close(done);
+ });
+ });
+
+ it('accepts the `maxPayload` option', (done) => {
+ const maxPayload = 20480;
+ const wss = new WebSocket.Server(
+ {
+ perMessageDeflate: true,
+ maxPayload,
+ port: 0
+ },
+ () => {
+ const ws = new WebSocket(`ws://localhost:${wss.address().port}`);
+
+ ws.on('open', ws.close);
+ }
+ );
+
+ wss.on('connection', (ws) => {
+ assert.strictEqual(ws._receiver._maxPayload, maxPayload);
+ assert.strictEqual(
+ ws._receiver._extensions['permessage-deflate']._maxPayload,
+ maxPayload
+ );
+ wss.close(done);
+ });
+ });
+
+ it('honors the `WebSocket` option', (done) => {
+ class CustomWebSocket extends WebSocket.WebSocket {
+ get foo() {
+ return 'foo';
+ }
+ }
+
+ const wss = new WebSocket.Server(
+ {
+ port: 0,
+ WebSocket: CustomWebSocket
+ },
+ () => {
+ const ws = new WebSocket(`ws://localhost:${wss.address().port}`);
+
+ ws.on('open', ws.close);
+ }
+ );
+
+ wss.on('connection', (ws) => {
+ assert.ok(ws instanceof CustomWebSocket);
+ assert.strictEqual(ws.foo, 'foo');
+ wss.close(done);
+ });
+ });
+ });
+
+ it('emits an error if http server bind fails', (done) => {
+ const wss1 = new WebSocket.Server({ port: 0 }, () => {
+ const wss2 = new WebSocket.Server({
+ port: wss1.address().port
+ });
+
+ wss2.on('error', () => wss1.close(done));
+ });
+ });
+
+ it('starts a server on a given port', (done) => {
+ const port = 1337;
+ const wss = new WebSocket.Server({ port }, () => {
+ const ws = new WebSocket(`ws://localhost:${port}`);
+
+ ws.on('open', ws.close);
+ });
+
+ wss.on('connection', () => wss.close(done));
+ });
+
+ it('binds the server on any IPv6 address when available', (done) => {
+ const wss = new WebSocket.Server({ port: 0 }, () => {
+ assert.strictEqual(wss._server.address().address, '::');
+ wss.close(done);
+ });
+ });
+
+ it('uses a precreated http server', (done) => {
+ const server = http.createServer();
+
+ server.listen(0, () => {
+ const wss = new WebSocket.Server({ server });
+
+ wss.on('connection', () => {
+ server.close(done);
+ });
+
+ const ws = new WebSocket(`ws://localhost:${server.address().port}`);
+
+ ws.on('open', ws.close);
+ });
+ });
+
+ it('426s for non-Upgrade requests', (done) => {
+ const wss = new WebSocket.Server({ port: 0 }, () => {
+ http.get(`http://localhost:${wss.address().port}`, (res) => {
+ let body = '';
+
+ assert.strictEqual(res.statusCode, 426);
+ res.on('data', (chunk) => {
+ body += chunk;
+ });
+ res.on('end', () => {
+ assert.strictEqual(body, http.STATUS_CODES[426]);
+ wss.close(done);
+ });
+ });
+ });
+ });
+
+ it('uses a precreated http server listening on unix socket', function (done) {
+ //
+ // Skip this test on Windows. The URL parser:
+ //
+ // - Throws an error if the named pipe uses backward slashes.
+ // - Incorrectly parses the path if the named pipe uses forward slashes.
+ //
+ if (process.platform === 'win32') return this.skip();
+
+ const server = http.createServer();
+ const sockPath = path.join(
+ os.tmpdir(),
+ `ws.${crypto.randomBytes(16).toString('hex')}.sock`
+ );
+
+ server.listen(sockPath, () => {
+ const wss = new WebSocket.Server({ server });
+
+ wss.on('connection', (ws, req) => {
+ if (wss.clients.size === 1) {
+ assert.strictEqual(req.url, '/foo?bar=bar');
+ } else {
+ assert.strictEqual(req.url, '/');
+
+ for (const client of wss.clients) {
+ client.close();
+ }
+
+ server.close(done);
+ }
+ });
+
+ const ws = new WebSocket(`ws+unix://${sockPath}:/foo?bar=bar`);
+ ws.on('open', () => new WebSocket(`ws+unix://${sockPath}`));
+ });
+ });
+ });
+
+ describe('#address', () => {
+ it('returns the address of the server', (done) => {
+ const wss = new WebSocket.Server({ port: 0 }, () => {
+ const addr = wss.address();
+
+ assert.deepStrictEqual(addr, wss._server.address());
+ wss.close(done);
+ });
+ });
+
+ it('throws an error when operating in "noServer" mode', () => {
+ const wss = new WebSocket.Server({ noServer: true });
+
+ assert.throws(() => {
+ wss.address();
+ }, /^Error: The server is operating in "noServer" mode$/);
+ });
+
+ it('returns `null` if called after close', (done) => {
+ const wss = new WebSocket.Server({ port: 0 }, () => {
+ wss.close(() => {
+ assert.strictEqual(wss.address(), null);
+ done();
+ });
+ });
+ });
+ });
+
+ describe('#close', () => {
+ it('does not throw if called multiple times', (done) => {
+ const wss = new WebSocket.Server({ port: 0 }, () => {
+ wss.on('close', done);
+
+ wss.close();
+ wss.close();
+ wss.close();
+ });
+ });
+
+ it("doesn't close a precreated server", (done) => {
+ const server = http.createServer();
+ const realClose = server.close;
+
+ server.close = () => {
+ done(new Error('Must not close pre-created server'));
+ };
+
+ const wss = new WebSocket.Server({ server });
+
+ wss.on('connection', () => {
+ wss.close();
+ server.close = realClose;
+ server.close(done);
+ });
+
+ server.listen(0, () => {
+ const ws = new WebSocket(`ws://localhost:${server.address().port}`);
+
+ ws.on('open', ws.close);
+ });
+ });
+
+ it('invokes the callback in noServer mode', (done) => {
+ const wss = new WebSocket.Server({ noServer: true });
+
+ wss.close(done);
+ });
+
+ it('cleans event handlers on precreated server', (done) => {
+ const server = http.createServer();
+ const wss = new WebSocket.Server({ server });
+
+ server.listen(0, () => {
+ wss.close(() => {
+ assert.strictEqual(server.listenerCount('listening'), 0);
+ assert.strictEqual(server.listenerCount('upgrade'), 0);
+ assert.strictEqual(server.listenerCount('error'), 0);
+
+ server.close(done);
+ });
+ });
+ });
+
+ it("emits the 'close' event after the server closes", (done) => {
+ let serverCloseEventEmitted = false;
+
+ const wss = new WebSocket.Server({ port: 0 }, () => {
+ net.createConnection({ port: wss.address().port });
+ });
+
+ wss._server.on('connection', (socket) => {
+ wss.close();
+
+ //
+ // The server is closing. Ensure this does not emit a `'close'`
+ // event before the server is actually closed.
+ //
+ wss.close();
+
+ process.nextTick(() => {
+ socket.end();
+ });
+ });
+
+ wss._server.on('close', () => {
+ serverCloseEventEmitted = true;
+ });
+
+ wss.on('close', () => {
+ assert.ok(serverCloseEventEmitted);
+ done();
+ });
+ });
+
+ it("emits the 'close' event if client tracking is disabled", (done) => {
+ const wss = new WebSocket.Server({
+ noServer: true,
+ clientTracking: false
+ });
+
+ wss.on('close', done);
+ wss.close();
+ });
+
+ it('calls the callback if the server is already closed', (done) => {
+ const wss = new WebSocket.Server({ port: 0 }, () => {
+ wss.close(() => {
+ assert.strictEqual(wss._state, 2);
+
+ wss.close((err) => {
+ assert.ok(err instanceof Error);
+ assert.strictEqual(err.message, 'The server is not running');
+ done();
+ });
+ });
+ });
+ });
+
+ it("emits the 'close' event if the server is already closed", (done) => {
+ const wss = new WebSocket.Server({ port: 0 }, () => {
+ wss.close(() => {
+ assert.strictEqual(wss._state, 2);
+
+ wss.on('close', done);
+ wss.close();
+ });
+ });
+ });
+ });
+
+ describe('#clients', () => {
+ it('returns a list of connected clients', (done) => {
+ const wss = new WebSocket.Server({ port: 0 }, () => {
+ assert.strictEqual(wss.clients.size, 0);
+
+ const ws = new WebSocket(`ws://localhost:${wss.address().port}`);
+
+ ws.on('open', ws.close);
+ });
+
+ wss.on('connection', () => {
+ assert.strictEqual(wss.clients.size, 1);
+ wss.close(done);
+ });
+ });
+
+ it('can be disabled', (done) => {
+ const wss = new WebSocket.Server(
+ { port: 0, clientTracking: false },
+ () => {
+ assert.strictEqual(wss.clients, undefined);
+ const ws = new WebSocket(`ws://localhost:${wss.address().port}`);
+
+ ws.on('open', () => ws.close());
+ }
+ );
+
+ wss.on('connection', (ws) => {
+ assert.strictEqual(wss.clients, undefined);
+ ws.on('close', () => wss.close(done));
+ });
+ });
+
+ it('is updated when client terminates the connection', (done) => {
+ const wss = new WebSocket.Server({ port: 0 }, () => {
+ const ws = new WebSocket(`ws://localhost:${wss.address().port}`);
+
+ ws.on('open', () => ws.terminate());
+ });
+
+ wss.on('connection', (ws) => {
+ ws.on('close', () => {
+ assert.strictEqual(wss.clients.size, 0);
+ wss.close(done);
+ });
+ });
+ });
+
+ it('is updated when client closes the connection', (done) => {
+ const wss = new WebSocket.Server({ port: 0 }, () => {
+ const ws = new WebSocket(`ws://localhost:${wss.address().port}`);
+
+ ws.on('open', () => ws.close());
+ });
+
+ wss.on('connection', (ws) => {
+ ws.on('close', () => {
+ assert.strictEqual(wss.clients.size, 0);
+ wss.close(done);
+ });
+ });
+ });
+ });
+
+ describe('#shouldHandle', () => {
+ it('returns true when the path matches', () => {
+ const wss = new WebSocket.Server({ noServer: true, path: '/foo' });
+
+ assert.strictEqual(wss.shouldHandle({ url: '/foo' }), true);
+ assert.strictEqual(wss.shouldHandle({ url: '/foo?bar=baz' }), true);
+ });
+
+ it("returns false when the path doesn't match", () => {
+ const wss = new WebSocket.Server({ noServer: true, path: '/foo' });
+
+ assert.strictEqual(wss.shouldHandle({ url: '/bar' }), false);
+ });
+ });
+
+ describe('#handleUpgrade', () => {
+ it('can be used for a pre-existing server', (done) => {
+ const server = http.createServer();
+
+ server.listen(0, () => {
+ const wss = new WebSocket.Server({ noServer: true });
+
+ server.on('upgrade', (req, socket, head) => {
+ wss.handleUpgrade(req, socket, head, (ws) => {
+ ws.send('hello');
+ ws.close();
+ });
+ });
+
+ const ws = new WebSocket(`ws://localhost:${server.address().port}`);
+
+ ws.on('message', (message, isBinary) => {
+ assert.deepStrictEqual(message, Buffer.from('hello'));
+ assert.ok(!isBinary);
+ server.close(done);
+ });
+ });
+ });
+
+ it("closes the connection when path doesn't match", (done) => {
+ const wss = new WebSocket.Server({ port: 0, path: '/ws' }, () => {
+ const req = http.get({
+ port: wss.address().port,
+ headers: {
+ Connection: 'Upgrade',
+ Upgrade: 'websocket',
+ 'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==',
+ 'Sec-WebSocket-Version': 13
+ }
+ });
+
+ req.on('response', (res) => {
+ assert.strictEqual(res.statusCode, 400);
+ wss.close(done);
+ });
+ });
+ });
+
+ it('closes the connection when protocol version is Hixie-76', (done) => {
+ const wss = new WebSocket.Server({ port: 0 }, () => {
+ const req = http.get({
+ port: wss.address().port,
+ headers: {
+ Connection: 'Upgrade',
+ Upgrade: 'WebSocket',
+ 'Sec-WebSocket-Key1': '4 @1 46546xW%0l 1 5',
+ 'Sec-WebSocket-Key2': '12998 5 Y3 1 .P00',
+ 'Sec-WebSocket-Protocol': 'sample'
+ }
+ });
+
+ req.on('response', (res) => {
+ assert.strictEqual(res.statusCode, 400);
+
+ const chunks = [];
+
+ res.on('data', (chunk) => {
+ chunks.push(chunk);
+ });
+
+ res.on('end', () => {
+ assert.strictEqual(
+ Buffer.concat(chunks).toString(),
+ 'Missing or invalid Sec-WebSocket-Key header'
+ );
+ wss.close(done);
+ });
+ });
+ });
+ });
+ });
+
+ describe('#completeUpgrade', () => {
+ it('throws an error if called twice with the same socket', (done) => {
+ const server = http.createServer();
+
+ server.listen(0, () => {
+ const wss = new WebSocket.Server({ noServer: true });
+
+ server.on('upgrade', (req, socket, head) => {
+ wss.handleUpgrade(req, socket, head, (ws) => {
+ ws.close();
+ });
+ assert.throws(
+ () => wss.handleUpgrade(req, socket, head, NOOP),
+ (err) => {
+ assert.ok(err instanceof Error);
+ assert.strictEqual(
+ err.message,
+ 'server.handleUpgrade() was called more than once with the ' +
+ 'same socket, possibly due to a misconfiguration'
+ );
+ return true;
+ }
+ );
+ });
+
+ const ws = new WebSocket(`ws://localhost:${server.address().port}`);
+
+ ws.on('open', () => {
+ ws.on('close', () => {
+ server.close(done);
+ });
+ });
+ });
+ });
+ });
+
+ describe('Connection establishing', () => {
+ it('fails if the HTTP method is not GET', (done) => {
+ const wss = new WebSocket.Server({ port: 0 }, () => {
+ const req = http.request({
+ method: 'POST',
+ port: wss.address().port,
+ headers: {
+ Connection: 'Upgrade',
+ Upgrade: 'websocket'
+ }
+ });
+
+ req.on('response', (res) => {
+ assert.strictEqual(res.statusCode, 405);
+
+ const chunks = [];
+
+ res.on('data', (chunk) => {
+ chunks.push(chunk);
+ });
+
+ res.on('end', () => {
+ assert.strictEqual(
+ Buffer.concat(chunks).toString(),
+ 'Invalid HTTP method'
+ );
+ wss.close(done);
+ });
+ });
+
+ req.end();
+ });
+
+ wss.on('connection', () => {
+ done(new Error("Unexpected 'connection' event"));
+ });
+ });
+
+ it('fails if the Upgrade header field value is not "websocket"', (done) => {
+ const wss = new WebSocket.Server({ port: 0 }, () => {
+ const req = http.get({
+ port: wss.address().port,
+ headers: {
+ Connection: 'Upgrade',
+ Upgrade: 'foo'
+ }
+ });
+
+ req.on('response', (res) => {
+ assert.strictEqual(res.statusCode, 400);
+
+ const chunks = [];
+
+ res.on('data', (chunk) => {
+ chunks.push(chunk);
+ });
+
+ res.on('end', () => {
+ assert.strictEqual(
+ Buffer.concat(chunks).toString(),
+ 'Invalid Upgrade header'
+ );
+ wss.close(done);
+ });
+ });
+ });
+
+ wss.on('connection', () => {
+ done(new Error("Unexpected 'connection' event"));
+ });
+ });
+
+ it('fails if the Sec-WebSocket-Key header is invalid (1/2)', (done) => {
+ const wss = new WebSocket.Server({ port: 0 }, () => {
+ const req = http.get({
+ port: wss.address().port,
+ headers: {
+ Connection: 'Upgrade',
+ Upgrade: 'websocket'
+ }
+ });
+
+ req.on('response', (res) => {
+ assert.strictEqual(res.statusCode, 400);
+
+ const chunks = [];
+
+ res.on('data', (chunk) => {
+ chunks.push(chunk);
+ });
+
+ res.on('end', () => {
+ assert.strictEqual(
+ Buffer.concat(chunks).toString(),
+ 'Missing or invalid Sec-WebSocket-Key header'
+ );
+ wss.close(done);
+ });
+ });
+ });
+
+ wss.on('connection', () => {
+ done(new Error("Unexpected 'connection' event"));
+ });
+ });
+
+ it('fails if the Sec-WebSocket-Key header is invalid (2/2)', (done) => {
+ const wss = new WebSocket.Server({ port: 0 }, () => {
+ const req = http.get({
+ port: wss.address().port,
+ headers: {
+ Connection: 'Upgrade',
+ Upgrade: 'websocket',
+ 'Sec-WebSocket-Key': 'P5l8BJcZwRc='
+ }
+ });
+
+ req.on('response', (res) => {
+ assert.strictEqual(res.statusCode, 400);
+
+ const chunks = [];
+
+ res.on('data', (chunk) => {
+ chunks.push(chunk);
+ });
+
+ res.on('end', () => {
+ assert.strictEqual(
+ Buffer.concat(chunks).toString(),
+ 'Missing or invalid Sec-WebSocket-Key header'
+ );
+ wss.close(done);
+ });
+ });
+ });
+
+ wss.on('connection', () => {
+ done(new Error("Unexpected 'connection' event"));
+ });
+ });
+
+ it('fails if the Sec-WebSocket-Version header is invalid (1/2)', (done) => {
+ const wss = new WebSocket.Server({ port: 0 }, () => {
+ const req = http.get({
+ port: wss.address().port,
+ headers: {
+ Connection: 'Upgrade',
+ Upgrade: 'websocket',
+ 'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ=='
+ }
+ });
+
+ req.on('response', (res) => {
+ assert.strictEqual(res.statusCode, 400);
+
+ const chunks = [];
+
+ res.on('data', (chunk) => {
+ chunks.push(chunk);
+ });
+
+ res.on('end', () => {
+ assert.strictEqual(
+ Buffer.concat(chunks).toString(),
+ 'Missing or invalid Sec-WebSocket-Version header'
+ );
+ wss.close(done);
+ });
+ });
+ });
+
+ wss.on('connection', () => {
+ done(new Error("Unexpected 'connection' event"));
+ });
+ });
+
+ it('fails if the Sec-WebSocket-Version header is invalid (2/2)', (done) => {
+ const wss = new WebSocket.Server({ port: 0 }, () => {
+ const req = http.get({
+ port: wss.address().port,
+ headers: {
+ Connection: 'Upgrade',
+ Upgrade: 'websocket',
+ 'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==',
+ 'Sec-WebSocket-Version': 12
+ }
+ });
+
+ req.on('response', (res) => {
+ assert.strictEqual(res.statusCode, 400);
+
+ const chunks = [];
+
+ res.on('data', (chunk) => {
+ chunks.push(chunk);
+ });
+
+ res.on('end', () => {
+ assert.strictEqual(
+ Buffer.concat(chunks).toString(),
+ 'Missing or invalid Sec-WebSocket-Version header'
+ );
+ wss.close(done);
+ });
+ });
+ });
+
+ wss.on('connection', () => {
+ done(new Error("Unexpected 'connection' event"));
+ });
+ });
+
+ it('fails is the Sec-WebSocket-Protocol header is invalid', (done) => {
+ const wss = new WebSocket.Server({ port: 0 }, () => {
+ const req = http.get({
+ port: wss.address().port,
+ headers: {
+ Connection: 'Upgrade',
+ Upgrade: 'websocket',
+ 'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==',
+ 'Sec-WebSocket-Version': 13,
+ 'Sec-WebSocket-Protocol': 'foo;bar'
+ }
+ });
+
+ req.on('response', (res) => {
+ assert.strictEqual(res.statusCode, 400);
+
+ const chunks = [];
+
+ res.on('data', (chunk) => {
+ chunks.push(chunk);
+ });
+
+ res.on('end', () => {
+ assert.strictEqual(
+ Buffer.concat(chunks).toString(),
+ 'Invalid Sec-WebSocket-Protocol header'
+ );
+ wss.close(done);
+ });
+ });
+ });
+
+ wss.on('connection', () => {
+ done(new Error("Unexpected 'connection' event"));
+ });
+ });
+
+ it('fails if the Sec-WebSocket-Extensions header is invalid', (done) => {
+ const wss = new WebSocket.Server(
+ {
+ perMessageDeflate: true,
+ port: 0
+ },
+ () => {
+ const req = http.get({
+ port: wss.address().port,
+ headers: {
+ Connection: 'Upgrade',
+ Upgrade: 'websocket',
+ 'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==',
+ 'Sec-WebSocket-Version': 13,
+ 'Sec-WebSocket-Extensions':
+ 'permessage-deflate; server_max_window_bits=foo'
+ }
+ });
+
+ req.on('response', (res) => {
+ assert.strictEqual(res.statusCode, 400);
+
+ const chunks = [];
+
+ res.on('data', (chunk) => {
+ chunks.push(chunk);
+ });
+
+ res.on('end', () => {
+ assert.strictEqual(
+ Buffer.concat(chunks).toString(),
+ 'Invalid or unacceptable Sec-WebSocket-Extensions header'
+ );
+ wss.close(done);
+ });
+ });
+ }
+ );
+
+ wss.on('connection', () => {
+ done(new Error("Unexpected 'connection' event"));
+ });
+ });
+
+ it("emits the 'wsClientError' event", (done) => {
+ const wss = new WebSocket.Server({ port: 0 }, () => {
+ const req = http.request({
+ method: 'POST',
+ port: wss.address().port,
+ headers: {
+ Connection: 'Upgrade',
+ Upgrade: 'websocket'
+ }
+ });
+
+ req.on('response', (res) => {
+ assert.strictEqual(res.statusCode, 400);
+ wss.close(done);
+ });
+
+ req.end();
+ });
+
+ wss.on('wsClientError', (err, socket, request) => {
+ assert.ok(err instanceof Error);
+ assert.strictEqual(err.message, 'Invalid HTTP method');
+
+ assert.ok(request instanceof http.IncomingMessage);
+ assert.strictEqual(request.method, 'POST');
+
+ socket.end('HTTP/1.1 400 Bad Request\r\n\r\n');
+ });
+
+ wss.on('connection', () => {
+ done(new Error("Unexpected 'connection' event"));
+ });
+ });
+
+ it('fails if the WebSocket server is closing or closed', (done) => {
+ const server = http.createServer();
+ const wss = new WebSocket.Server({ noServer: true });
+
+ server.on('upgrade', (req, socket, head) => {
+ wss.close();
+ wss.handleUpgrade(req, socket, head, () => {
+ done(new Error('Unexpected callback invocation'));
+ });
+ });
+
+ server.listen(0, () => {
+ const ws = new WebSocket(`ws://localhost:${server.address().port}`);
+
+ ws.on('unexpected-response', (req, res) => {
+ assert.strictEqual(res.statusCode, 503);
+ res.resume();
+ server.close(done);
+ });
+ });
+ });
+
+ it('handles unsupported extensions', (done) => {
+ const wss = new WebSocket.Server(
+ {
+ perMessageDeflate: true,
+ port: 0
+ },
+ () => {
+ const req = http.get({
+ port: wss.address().port,
+ headers: {
+ Connection: 'Upgrade',
+ Upgrade: 'websocket',
+ 'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==',
+ 'Sec-WebSocket-Version': 13,
+ 'Sec-WebSocket-Extensions': 'foo; bar'
+ }
+ });
+
+ req.on('upgrade', (res, socket, head) => {
+ if (head.length) socket.unshift(head);
+
+ socket.once('data', (chunk) => {
+ assert.strictEqual(chunk[0], 0x88);
+ socket.destroy();
+ wss.close(done);
+ });
+ });
+ }
+ );
+
+ wss.on('connection', (ws) => {
+ assert.strictEqual(ws.extensions, '');
+ ws.close();
+ });
+ });
+
+ describe('`verifyClient`', () => {
+ it('can reject client synchronously', (done) => {
+ const wss = new WebSocket.Server(
+ {
+ verifyClient: () => false,
+ port: 0
+ },
+ () => {
+ const req = http.get({
+ port: wss.address().port,
+ headers: {
+ Connection: 'Upgrade',
+ Upgrade: 'websocket',
+ 'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==',
+ 'Sec-WebSocket-Version': 8
+ }
+ });
+
+ req.on('response', (res) => {
+ assert.strictEqual(res.statusCode, 401);
+ wss.close(done);
+ });
+ }
+ );
+
+ wss.on('connection', () => {
+ done(new Error("Unexpected 'connection' event"));
+ });
+ });
+
+ it('can accept client synchronously', (done) => {
+ const server = https.createServer({
+ cert: fs.readFileSync('test/fixtures/certificate.pem'),
+ key: fs.readFileSync('test/fixtures/key.pem')
+ });
+
+ const wss = new WebSocket.Server({
+ verifyClient: (info) => {
+ assert.strictEqual(info.origin, 'https://example.com');
+ assert.strictEqual(info.req.headers.foo, 'bar');
+ assert.ok(info.secure, true);
+ return true;
+ },
+ server
+ });
+
+ wss.on('connection', () => {
+ server.close(done);
+ });
+
+ server.listen(0, () => {
+ const ws = new WebSocket(`wss://localhost:${server.address().port}`, {
+ headers: { Origin: 'https://example.com', foo: 'bar' },
+ rejectUnauthorized: false
+ });
+
+ ws.on('open', ws.close);
+ });
+ });
+
+ it('can accept client asynchronously', (done) => {
+ const wss = new WebSocket.Server(
+ {
+ verifyClient: (o, cb) => process.nextTick(cb, true),
+ port: 0
+ },
+ () => {
+ const ws = new WebSocket(`ws://localhost:${wss.address().port}`);
+
+ ws.on('open', ws.close);
+ }
+ );
+
+ wss.on('connection', () => wss.close(done));
+ });
+
+ it('can reject client asynchronously', (done) => {
+ const wss = new WebSocket.Server(
+ {
+ verifyClient: (info, cb) => process.nextTick(cb, false),
+ port: 0
+ },
+ () => {
+ const req = http.get({
+ port: wss.address().port,
+ headers: {
+ Connection: 'Upgrade',
+ Upgrade: 'websocket',
+ 'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==',
+ 'Sec-WebSocket-Version': 8
+ }
+ });
+
+ req.on('response', (res) => {
+ assert.strictEqual(res.statusCode, 401);
+ wss.close(done);
+ });
+ }
+ );
+
+ wss.on('connection', () => {
+ done(new Error("Unexpected 'connection' event"));
+ });
+ });
+
+ it('can reject client asynchronously w/ status code', (done) => {
+ const wss = new WebSocket.Server(
+ {
+ verifyClient: (info, cb) => process.nextTick(cb, false, 404),
+ port: 0
+ },
+ () => {
+ const req = http.get({
+ port: wss.address().port,
+ headers: {
+ Connection: 'Upgrade',
+ Upgrade: 'websocket',
+ 'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==',
+ 'Sec-WebSocket-Version': 8
+ }
+ });
+
+ req.on('response', (res) => {
+ assert.strictEqual(res.statusCode, 404);
+ wss.close(done);
+ });
+ }
+ );
+
+ wss.on('connection', () => {
+ done(new Error("Unexpected 'connection' event"));
+ });
+ });
+
+ it('can reject client asynchronously w/ custom headers', (done) => {
+ const wss = new WebSocket.Server(
+ {
+ verifyClient: (info, cb) => {
+ process.nextTick(cb, false, 503, '', { 'Retry-After': 120 });
+ },
+ port: 0
+ },
+ () => {
+ const req = http.get({
+ port: wss.address().port,
+ headers: {
+ Connection: 'Upgrade',
+ Upgrade: 'websocket',
+ 'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==',
+ 'Sec-WebSocket-Version': 8
+ }
+ });
+
+ req.on('response', (res) => {
+ assert.strictEqual(res.statusCode, 503);
+ assert.strictEqual(res.headers['retry-after'], '120');
+ wss.close(done);
+ });
+ }
+ );
+
+ wss.on('connection', () => {
+ done(new Error("Unexpected 'connection' event"));
+ });
+ });
+ });
+
+ it("doesn't emit the 'connection' event if socket is closed prematurely", (done) => {
+ const server = http.createServer();
+
+ server.listen(0, () => {
+ const wss = new WebSocket.Server({
+ verifyClient: ({ req: { socket } }, cb) => {
+ assert.strictEqual(socket.readable, true);
+ assert.strictEqual(socket.writable, true);
+
+ socket.on('end', () => {
+ assert.strictEqual(socket.readable, false);
+ assert.strictEqual(socket.writable, true);
+ cb(true);
+ });
+ },
+ server
+ });
+
+ wss.on('connection', () => {
+ done(new Error("Unexpected 'connection' event"));
+ });
+
+ const socket = net.connect(
+ {
+ port: server.address().port,
+ allowHalfOpen: true
+ },
+ () => {
+ socket.end(
+ [
+ 'GET / HTTP/1.1',
+ 'Host: localhost',
+ 'Upgrade: websocket',
+ 'Connection: Upgrade',
+ 'Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==',
+ 'Sec-WebSocket-Version: 13',
+ '\r\n'
+ ].join('\r\n')
+ );
+ }
+ );
+
+ socket.on('end', () => {
+ wss.close();
+ server.close(done);
+ });
+ });
+ });
+
+ it('handles data passed along with the upgrade request', (done) => {
+ const wss = new WebSocket.Server({ port: 0 }, () => {
+ const req = http.request({
+ port: wss.address().port,
+ headers: {
+ Connection: 'Upgrade',
+ Upgrade: 'websocket',
+ 'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==',
+ 'Sec-WebSocket-Version': 13
+ }
+ });
+
+ const list = Sender.frame(Buffer.from('Hello'), {
+ fin: true,
+ rsv1: false,
+ opcode: 0x01,
+ mask: true,
+ readOnly: false
+ });
+
+ req.write(Buffer.concat(list));
+ req.end();
+ });
+
+ wss.on('connection', (ws) => {
+ ws.on('message', (data, isBinary) => {
+ assert.deepStrictEqual(data, Buffer.from('Hello'));
+ assert.ok(!isBinary);
+ wss.close(done);
+ });
+ });
+ });
+
+ describe('`handleProtocols`', () => {
+ it('allows to select a subprotocol', (done) => {
+ const handleProtocols = (protocols, request) => {
+ assert.ok(request instanceof http.IncomingMessage);
+ assert.strictEqual(request.url, '/');
+ return Array.from(protocols).pop();
+ };
+ const wss = new WebSocket.Server({ handleProtocols, port: 0 }, () => {
+ const ws = new WebSocket(`ws://localhost:${wss.address().port}`, [
+ 'foo',
+ 'bar'
+ ]);
+
+ ws.on('open', () => {
+ assert.strictEqual(ws.protocol, 'bar');
+ wss.close(done);
+ });
+ });
+
+ wss.on('connection', (ws) => {
+ ws.close();
+ });
+ });
+ });
+
+ it("emits the 'headers' event", (done) => {
+ const wss = new WebSocket.Server({ port: 0 }, () => {
+ const ws = new WebSocket(`ws://localhost:${wss.address().port}`);
+
+ ws.on('open', ws.close);
+ });
+
+ wss.on('headers', (headers, request) => {
+ assert.deepStrictEqual(headers.slice(0, 3), [
+ 'HTTP/1.1 101 Switching Protocols',
+ 'Upgrade: websocket',
+ 'Connection: Upgrade'
+ ]);
+ assert.ok(request instanceof http.IncomingMessage);
+ assert.strictEqual(request.url, '/');
+
+ wss.on('connection', () => wss.close(done));
+ });
+ });
+ });
+
+ describe('permessage-deflate', () => {
+ it('is disabled by default', (done) => {
+ const wss = new WebSocket.Server({ port: 0 }, () => {
+ const ws = new WebSocket(`ws://localhost:${wss.address().port}`);
+
+ ws.on('open', ws.close);
+ });
+
+ wss.on('connection', (ws, req) => {
+ assert.strictEqual(
+ req.headers['sec-websocket-extensions'],
+ 'permessage-deflate; client_max_window_bits'
+ );
+ assert.strictEqual(ws.extensions, '');
+ wss.close(done);
+ });
+ });
+
+ it('uses configuration options', (done) => {
+ const wss = new WebSocket.Server(
+ {
+ perMessageDeflate: { clientMaxWindowBits: 8 },
+ port: 0
+ },
+ () => {
+ const ws = new WebSocket(`ws://localhost:${wss.address().port}`);
+
+ ws.on('upgrade', (res) => {
+ assert.strictEqual(
+ res.headers['sec-websocket-extensions'],
+ 'permessage-deflate; client_max_window_bits=8'
+ );
+
+ wss.close(done);
+ });
+ }
+ );
+
+ wss.on('connection', (ws) => {
+ ws.close();
+ });
+ });
+ });
+});
diff --git a/testing/xpcshell/node-ws/test/websocket.integration.js b/testing/xpcshell/node-ws/test/websocket.integration.js
new file mode 100644
index 0000000000..abd96c61e4
--- /dev/null
+++ b/testing/xpcshell/node-ws/test/websocket.integration.js
@@ -0,0 +1,55 @@
+'use strict';
+
+const assert = require('assert');
+
+const WebSocket = require('..');
+
+describe('WebSocket', () => {
+ it('communicates successfully with echo service (ws)', (done) => {
+ const ws = new WebSocket('ws://websocket-echo.com/', {
+ protocolVersion: 13
+ });
+
+ let dataReceived = false;
+
+ ws.on('open', () => {
+ ws.send('hello');
+ });
+
+ ws.on('close', () => {
+ assert.ok(dataReceived);
+ done();
+ });
+
+ ws.on('message', (message, isBinary) => {
+ dataReceived = true;
+ assert.ok(!isBinary);
+ assert.strictEqual(message.toString(), 'hello');
+ ws.close();
+ });
+ });
+
+ it('communicates successfully with echo service (wss)', (done) => {
+ const ws = new WebSocket('wss://websocket-echo.com/', {
+ protocolVersion: 13
+ });
+
+ let dataReceived = false;
+
+ ws.on('open', () => {
+ ws.send('hello');
+ });
+
+ ws.on('close', () => {
+ assert.ok(dataReceived);
+ done();
+ });
+
+ ws.on('message', (message, isBinary) => {
+ dataReceived = true;
+ assert.ok(!isBinary);
+ assert.strictEqual(message.toString(), 'hello');
+ ws.close();
+ });
+ });
+});
diff --git a/testing/xpcshell/node-ws/test/websocket.test.js b/testing/xpcshell/node-ws/test/websocket.test.js
new file mode 100644
index 0000000000..f5fbf16505
--- /dev/null
+++ b/testing/xpcshell/node-ws/test/websocket.test.js
@@ -0,0 +1,4514 @@
+/* eslint no-unused-vars: ["error", { "varsIgnorePattern": "^ws$" }] */
+
+'use strict';
+
+const assert = require('assert');
+const crypto = require('crypto');
+const https = require('https');
+const http = require('http');
+const path = require('path');
+const net = require('net');
+const tls = require('tls');
+const os = require('os');
+const fs = require('fs');
+const { URL } = require('url');
+
+const Sender = require('../lib/sender');
+const WebSocket = require('..');
+const {
+ CloseEvent,
+ ErrorEvent,
+ Event,
+ MessageEvent
+} = require('../lib/event-target');
+const { EMPTY_BUFFER, GUID, kListener, NOOP } = require('../lib/constants');
+
+class CustomAgent extends http.Agent {
+ addRequest() {}
+}
+
+describe('WebSocket', () => {
+ describe('#ctor', () => {
+ it('throws an error when using an invalid url', () => {
+ assert.throws(
+ () => new WebSocket('foo'),
+ /^SyntaxError: Invalid URL: foo$/
+ );
+
+ assert.throws(
+ () => new WebSocket('https://websocket-echo.com'),
+ /^SyntaxError: The URL's protocol must be one of "ws:", "wss:", or "ws\+unix:"$/
+ );
+
+ assert.throws(
+ () => new WebSocket('ws+unix:'),
+ /^SyntaxError: The URL's pathname is empty$/
+ );
+
+ assert.throws(
+ () => new WebSocket('wss://websocket-echo.com#foo'),
+ /^SyntaxError: The URL contains a fragment identifier$/
+ );
+ });
+
+ it('throws an error if a subprotocol is invalid or duplicated', () => {
+ for (const subprotocol of [null, '', 'a,b', ['a', 'a']]) {
+ assert.throws(
+ () => new WebSocket('ws://localhost', subprotocol),
+ /^SyntaxError: An invalid or duplicated subprotocol was specified$/
+ );
+ }
+ });
+
+ it('accepts `url.URL` objects as url', (done) => {
+ const agent = new CustomAgent();
+
+ agent.addRequest = (req, opts) => {
+ assert.strictEqual(opts.host, '::1');
+ assert.strictEqual(req.path, '/');
+ done();
+ };
+
+ const ws = new WebSocket(new URL('ws://[::1]'), { agent });
+ });
+
+ describe('options', () => {
+ it('accepts the `options` object as 3rd argument', () => {
+ const agent = new CustomAgent();
+ let count = 0;
+ let ws;
+
+ agent.addRequest = (req) => {
+ assert.strictEqual(
+ req.getHeader('sec-websocket-protocol'),
+ undefined
+ );
+ count++;
+ };
+
+ ws = new WebSocket('ws://localhost', undefined, { agent });
+ ws = new WebSocket('ws://localhost', [], { agent });
+
+ assert.strictEqual(count, 2);
+ });
+
+ it('accepts the `maxPayload` option', (done) => {
+ const maxPayload = 20480;
+ const wss = new WebSocket.Server(
+ {
+ perMessageDeflate: true,
+ port: 0
+ },
+ () => {
+ const ws = new WebSocket(`ws://localhost:${wss.address().port}`, {
+ perMessageDeflate: true,
+ maxPayload
+ });
+
+ ws.on('open', () => {
+ assert.strictEqual(ws._receiver._maxPayload, maxPayload);
+ assert.strictEqual(
+ ws._receiver._extensions['permessage-deflate']._maxPayload,
+ maxPayload
+ );
+ wss.close(done);
+ });
+ }
+ );
+
+ wss.on('connection', (ws) => {
+ ws.close();
+ });
+ });
+
+ it('throws an error when using an invalid `protocolVersion`', () => {
+ const options = { agent: new CustomAgent(), protocolVersion: 1000 };
+
+ assert.throws(
+ () => new WebSocket('ws://localhost', options),
+ /^RangeError: Unsupported protocol version: 1000 \(supported versions: 8, 13\)$/
+ );
+ });
+
+ it('honors the `generateMask` option', (done) => {
+ const data = Buffer.from('foo');
+ const wss = new WebSocket.Server({ port: 0 }, () => {
+ const ws = new WebSocket(`ws://localhost:${wss.address().port}`, {
+ generateMask() {}
+ });
+
+ ws.on('open', () => {
+ ws.send(data);
+ });
+
+ ws.on('close', (code, reason) => {
+ assert.strictEqual(code, 1005);
+ assert.deepStrictEqual(reason, EMPTY_BUFFER);
+
+ wss.close(done);
+ });
+ });
+
+ wss.on('connection', (ws) => {
+ const chunks = [];
+
+ ws._socket.prependListener('data', (chunk) => {
+ chunks.push(chunk);
+ });
+
+ ws.on('message', (message) => {
+ assert.deepStrictEqual(message, data);
+ assert.deepStrictEqual(
+ Buffer.concat(chunks).slice(2, 6),
+ Buffer.alloc(4)
+ );
+
+ ws.close();
+ });
+ });
+ });
+ });
+ });
+
+ describe('Constants', () => {
+ const readyStates = {
+ CONNECTING: 0,
+ OPEN: 1,
+ CLOSING: 2,
+ CLOSED: 3
+ };
+
+ Object.keys(readyStates).forEach((state) => {
+ describe(`\`${state}\``, () => {
+ it('is enumerable property of class', () => {
+ const descriptor = Object.getOwnPropertyDescriptor(WebSocket, state);
+
+ assert.deepStrictEqual(descriptor, {
+ configurable: false,
+ enumerable: true,
+ value: readyStates[state],
+ writable: false
+ });
+ });
+
+ it('is enumerable property of prototype', () => {
+ const descriptor = Object.getOwnPropertyDescriptor(
+ WebSocket.prototype,
+ state
+ );
+
+ assert.deepStrictEqual(descriptor, {
+ configurable: false,
+ enumerable: true,
+ value: readyStates[state],
+ writable: false
+ });
+ });
+ });
+ });
+ });
+
+ describe('Attributes', () => {
+ describe('`binaryType`', () => {
+ it('is enumerable and configurable', () => {
+ const descriptor = Object.getOwnPropertyDescriptor(
+ WebSocket.prototype,
+ 'binaryType'
+ );
+
+ assert.strictEqual(descriptor.configurable, true);
+ assert.strictEqual(descriptor.enumerable, true);
+ assert.ok(descriptor.get !== undefined);
+ assert.ok(descriptor.set !== undefined);
+ });
+
+ it("defaults to 'nodebuffer'", () => {
+ const ws = new WebSocket('ws://localhost', {
+ agent: new CustomAgent()
+ });
+
+ assert.strictEqual(ws.binaryType, 'nodebuffer');
+ });
+
+ it("can be changed to 'arraybuffer' or 'fragments'", () => {
+ const ws = new WebSocket('ws://localhost', {
+ agent: new CustomAgent()
+ });
+
+ ws.binaryType = 'arraybuffer';
+ assert.strictEqual(ws.binaryType, 'arraybuffer');
+
+ ws.binaryType = 'foo';
+ assert.strictEqual(ws.binaryType, 'arraybuffer');
+
+ ws.binaryType = 'fragments';
+ assert.strictEqual(ws.binaryType, 'fragments');
+
+ ws.binaryType = '';
+ assert.strictEqual(ws.binaryType, 'fragments');
+
+ ws.binaryType = 'nodebuffer';
+ assert.strictEqual(ws.binaryType, 'nodebuffer');
+ });
+ });
+
+ describe('`bufferedAmount`', () => {
+ it('is enumerable and configurable', () => {
+ const descriptor = Object.getOwnPropertyDescriptor(
+ WebSocket.prototype,
+ 'bufferedAmount'
+ );
+
+ assert.strictEqual(descriptor.configurable, true);
+ assert.strictEqual(descriptor.enumerable, true);
+ assert.ok(descriptor.get !== undefined);
+ assert.ok(descriptor.set === undefined);
+ });
+
+ it('defaults to zero', () => {
+ const ws = new WebSocket('ws://localhost', {
+ agent: new CustomAgent()
+ });
+
+ assert.strictEqual(ws.bufferedAmount, 0);
+ });
+
+ it('defaults to zero upon "open"', (done) => {
+ const wss = new WebSocket.Server({ port: 0 }, () => {
+ const ws = new WebSocket(`ws://localhost:${wss.address().port}`);
+
+ ws.onopen = () => {
+ assert.strictEqual(ws.bufferedAmount, 0);
+ wss.close(done);
+ };
+ });
+
+ wss.on('connection', (ws) => {
+ ws.close();
+ });
+ });
+
+ it('takes into account the data in the sender queue', (done) => {
+ const wss = new WebSocket.Server(
+ {
+ perMessageDeflate: true,
+ port: 0
+ },
+ () => {
+ const ws = new WebSocket(`ws://localhost:${wss.address().port}`, {
+ perMessageDeflate: { threshold: 0 }
+ });
+
+ ws.on('open', () => {
+ ws.send('foo');
+
+ assert.strictEqual(ws.bufferedAmount, 3);
+
+ ws.send('bar', (err) => {
+ assert.ifError(err);
+ assert.strictEqual(ws.bufferedAmount, 0);
+ wss.close(done);
+ });
+
+ assert.strictEqual(ws.bufferedAmount, 6);
+ });
+ }
+ );
+
+ wss.on('connection', (ws) => {
+ ws.close();
+ });
+ });
+
+ it('takes into account the data in the socket queue', (done) => {
+ const wss = new WebSocket.Server({ port: 0 }, () => {
+ const ws = new WebSocket(`ws://localhost:${wss.address().port}`);
+ });
+
+ wss.on('connection', (ws) => {
+ const data = Buffer.alloc(1024, 61);
+
+ while (ws.bufferedAmount === 0) {
+ ws.send(data);
+ }
+
+ assert.ok(ws.bufferedAmount > 0);
+ assert.strictEqual(
+ ws.bufferedAmount,
+ ws._socket._writableState.length
+ );
+
+ ws.on('close', () => wss.close(done));
+ ws.close();
+ });
+ });
+ });
+
+ describe('`extensions`', () => {
+ it('is enumerable and configurable', () => {
+ const descriptor = Object.getOwnPropertyDescriptor(
+ WebSocket.prototype,
+ 'bufferedAmount'
+ );
+
+ assert.strictEqual(descriptor.configurable, true);
+ assert.strictEqual(descriptor.enumerable, true);
+ assert.ok(descriptor.get !== undefined);
+ assert.ok(descriptor.set === undefined);
+ });
+
+ it('exposes the negotiated extensions names (1/2)', (done) => {
+ const wss = new WebSocket.Server({ port: 0 }, () => {
+ const ws = new WebSocket(`ws://localhost:${wss.address().port}`);
+
+ assert.strictEqual(ws.extensions, '');
+
+ ws.on('open', () => {
+ assert.strictEqual(ws.extensions, '');
+ ws.on('close', () => wss.close(done));
+ });
+ });
+
+ wss.on('connection', (ws) => {
+ assert.strictEqual(ws.extensions, '');
+ ws.close();
+ });
+ });
+
+ it('exposes the negotiated extensions names (2/2)', (done) => {
+ const wss = new WebSocket.Server(
+ {
+ perMessageDeflate: true,
+ port: 0
+ },
+ () => {
+ const ws = new WebSocket(`ws://localhost:${wss.address().port}`);
+
+ assert.strictEqual(ws.extensions, '');
+
+ ws.on('open', () => {
+ assert.strictEqual(ws.extensions, 'permessage-deflate');
+ ws.on('close', () => wss.close(done));
+ });
+ }
+ );
+
+ wss.on('connection', (ws) => {
+ assert.strictEqual(ws.extensions, 'permessage-deflate');
+ ws.close();
+ });
+ });
+ });
+
+ describe('`isPaused`', () => {
+ it('is enumerable and configurable', () => {
+ const descriptor = Object.getOwnPropertyDescriptor(
+ WebSocket.prototype,
+ 'isPaused'
+ );
+
+ assert.strictEqual(descriptor.configurable, true);
+ assert.strictEqual(descriptor.enumerable, true);
+ assert.ok(descriptor.get !== undefined);
+ assert.ok(descriptor.set === undefined);
+ });
+
+ it('indicates whether the websocket is paused', (done) => {
+ const wss = new WebSocket.Server({ port: 0 }, () => {
+ const ws = new WebSocket(`ws://localhost:${wss.address().port}`);
+
+ ws.on('open', () => {
+ ws.pause();
+ assert.ok(ws.isPaused);
+
+ ws.resume();
+ assert.ok(!ws.isPaused);
+
+ ws.close();
+ wss.close(done);
+ });
+
+ assert.ok(!ws.isPaused);
+ });
+ });
+ });
+
+ describe('`protocol`', () => {
+ it('is enumerable and configurable', () => {
+ const descriptor = Object.getOwnPropertyDescriptor(
+ WebSocket.prototype,
+ 'protocol'
+ );
+
+ assert.strictEqual(descriptor.configurable, true);
+ assert.strictEqual(descriptor.enumerable, true);
+ assert.ok(descriptor.get !== undefined);
+ assert.ok(descriptor.set === undefined);
+ });
+
+ it('exposes the subprotocol selected by the server', (done) => {
+ const wss = new WebSocket.Server({ port: 0 }, () => {
+ const port = wss.address().port;
+ const ws = new WebSocket(`ws://localhost:${port}`, 'foo');
+
+ assert.strictEqual(ws.extensions, '');
+
+ ws.on('open', () => {
+ assert.strictEqual(ws.protocol, 'foo');
+ ws.on('close', () => wss.close(done));
+ });
+ });
+
+ wss.on('connection', (ws) => {
+ assert.strictEqual(ws.protocol, 'foo');
+ ws.close();
+ });
+ });
+ });
+
+ describe('`readyState`', () => {
+ it('is enumerable and configurable', () => {
+ const descriptor = Object.getOwnPropertyDescriptor(
+ WebSocket.prototype,
+ 'readyState'
+ );
+
+ assert.strictEqual(descriptor.configurable, true);
+ assert.strictEqual(descriptor.enumerable, true);
+ assert.ok(descriptor.get !== undefined);
+ assert.ok(descriptor.set === undefined);
+ });
+
+ it('defaults to `CONNECTING`', () => {
+ const ws = new WebSocket('ws://localhost', {
+ agent: new CustomAgent()
+ });
+
+ assert.strictEqual(ws.readyState, WebSocket.CONNECTING);
+ });
+
+ it('is set to `OPEN` once connection is established', (done) => {
+ const wss = new WebSocket.Server({ port: 0 }, () => {
+ const ws = new WebSocket(`ws://localhost:${wss.address().port}`);
+
+ ws.on('open', () => {
+ assert.strictEqual(ws.readyState, WebSocket.OPEN);
+ ws.close();
+ });
+
+ ws.on('close', () => wss.close(done));
+ });
+ });
+
+ it('is set to `CLOSED` once connection is closed', (done) => {
+ const wss = new WebSocket.Server({ port: 0 }, () => {
+ const ws = new WebSocket(`ws://localhost:${wss.address().port}`);
+
+ ws.on('close', () => {
+ assert.strictEqual(ws.readyState, WebSocket.CLOSED);
+ wss.close(done);
+ });
+
+ ws.on('open', () => ws.close(1001));
+ });
+ });
+
+ it('is set to `CLOSED` once connection is terminated', (done) => {
+ const wss = new WebSocket.Server({ port: 0 }, () => {
+ const ws = new WebSocket(`ws://localhost:${wss.address().port}`);
+
+ ws.on('close', () => {
+ assert.strictEqual(ws.readyState, WebSocket.CLOSED);
+ wss.close(done);
+ });
+
+ ws.on('open', () => ws.terminate());
+ });
+ });
+ });
+
+ describe('`url`', () => {
+ it('is enumerable and configurable', () => {
+ const descriptor = Object.getOwnPropertyDescriptor(
+ WebSocket.prototype,
+ 'url'
+ );
+
+ assert.strictEqual(descriptor.configurable, true);
+ assert.strictEqual(descriptor.enumerable, true);
+ assert.ok(descriptor.get !== undefined);
+ assert.ok(descriptor.set === undefined);
+ });
+
+ it('exposes the server url', () => {
+ const url = 'ws://localhost';
+ const ws = new WebSocket(url, { agent: new CustomAgent() });
+
+ assert.strictEqual(ws.url, url);
+ });
+ });
+ });
+
+ describe('Events', () => {
+ it("emits an 'error' event if an error occurs", (done) => {
+ let clientCloseEventEmitted = false;
+ let serverClientCloseEventEmitted = false;
+
+ const wss = new WebSocket.Server({ port: 0 }, () => {
+ const ws = new WebSocket(`ws://localhost:${wss.address().port}`);
+
+ ws.on('error', (err) => {
+ assert.ok(err instanceof RangeError);
+ assert.strictEqual(err.code, 'WS_ERR_INVALID_OPCODE');
+ assert.strictEqual(
+ err.message,
+ 'Invalid WebSocket frame: invalid opcode 5'
+ );
+
+ ws.on('close', (code, reason) => {
+ assert.strictEqual(code, 1006);
+ assert.strictEqual(reason, EMPTY_BUFFER);
+
+ clientCloseEventEmitted = true;
+ if (serverClientCloseEventEmitted) wss.close(done);
+ });
+ });
+ });
+
+ wss.on('connection', (ws) => {
+ ws.on('close', (code, reason) => {
+ assert.strictEqual(code, 1002);
+ assert.deepStrictEqual(reason, EMPTY_BUFFER);
+
+ serverClientCloseEventEmitted = true;
+ if (clientCloseEventEmitted) wss.close(done);
+ });
+
+ ws._socket.write(Buffer.from([0x85, 0x00]));
+ });
+ });
+
+ it('does not re-emit `net.Socket` errors', (done) => {
+ const codes = ['EPIPE', 'ECONNABORTED', 'ECANCELED', 'ECONNRESET'];
+ const wss = new WebSocket.Server({ port: 0 }, () => {
+ const ws = new WebSocket(`ws://localhost:${wss.address().port}`);
+
+ ws.on('open', () => {
+ ws._socket.on('error', (err) => {
+ assert.ok(err instanceof Error);
+ assert.ok(codes.includes(err.code), `Unexpected code: ${err.code}`);
+ ws.on('close', (code, message) => {
+ assert.strictEqual(code, 1006);
+ assert.strictEqual(message, EMPTY_BUFFER);
+ wss.close(done);
+ });
+ });
+
+ for (const client of wss.clients) client.terminate();
+ ws.send('foo');
+ ws.send('bar');
+ });
+ });
+ });
+
+ it("emits an 'upgrade' event", (done) => {
+ const wss = new WebSocket.Server({ port: 0 }, () => {
+ const ws = new WebSocket(`ws://localhost:${wss.address().port}`);
+ ws.on('upgrade', (res) => {
+ assert.ok(res instanceof http.IncomingMessage);
+ wss.close(done);
+ });
+ });
+
+ wss.on('connection', (ws) => {
+ ws.close();
+ });
+ });
+
+ it("emits a 'ping' event", (done) => {
+ const wss = new WebSocket.Server({ port: 0 }, () => {
+ const ws = new WebSocket(`ws://localhost:${wss.address().port}`);
+ ws.on('ping', () => wss.close(done));
+ });
+
+ wss.on('connection', (ws) => {
+ ws.ping();
+ ws.close();
+ });
+ });
+
+ it("emits a 'pong' event", (done) => {
+ const wss = new WebSocket.Server({ port: 0 }, () => {
+ const ws = new WebSocket(`ws://localhost:${wss.address().port}`);
+ ws.on('pong', () => wss.close(done));
+ });
+
+ wss.on('connection', (ws) => {
+ ws.pong();
+ ws.close();
+ });
+ });
+
+ it("emits a 'redirect' event", (done) => {
+ const server = http.createServer();
+ const wss = new WebSocket.Server({ noServer: true, path: '/foo' });
+
+ server.once('upgrade', (req, socket) => {
+ socket.end('HTTP/1.1 302 Found\r\nLocation: /foo\r\n\r\n');
+ server.once('upgrade', (req, socket, head) => {
+ wss.handleUpgrade(req, socket, head, (ws) => {
+ ws.close();
+ });
+ });
+ });
+
+ server.listen(() => {
+ const port = server.address().port;
+ const ws = new WebSocket(`ws://localhost:${port}`, {
+ followRedirects: true
+ });
+
+ ws.on('redirect', (url, req) => {
+ assert.strictEqual(ws._redirects, 1);
+ assert.strictEqual(url, `ws://localhost:${port}/foo`);
+ assert.ok(req instanceof http.ClientRequest);
+
+ ws.on('close', (code) => {
+ assert.strictEqual(code, 1005);
+ server.close(done);
+ });
+ });
+ });
+ });
+ });
+
+ describe('Connection establishing', () => {
+ const server = http.createServer();
+
+ beforeEach((done) => server.listen(0, done));
+ afterEach((done) => server.close(done));
+
+ it('fails if the Upgrade header field value is not "websocket"', (done) => {
+ server.once('upgrade', (req, socket) => {
+ socket.on('end', socket.end);
+ socket.write(
+ 'HTTP/1.1 101 Switching Protocols\r\n' +
+ 'Connection: Upgrade\r\n' +
+ 'Upgrade: foo\r\n' +
+ '\r\n'
+ );
+ });
+
+ const ws = new WebSocket(`ws://localhost:${server.address().port}`);
+
+ ws.on('error', (err) => {
+ assert.ok(err instanceof Error);
+ assert.strictEqual(err.message, 'Invalid Upgrade header');
+ done();
+ });
+ });
+
+ it('fails if the Sec-WebSocket-Accept header is invalid', (done) => {
+ server.once('upgrade', (req, socket) => {
+ socket.on('end', socket.end);
+ socket.write(
+ 'HTTP/1.1 101 Switching Protocols\r\n' +
+ 'Upgrade: websocket\r\n' +
+ 'Connection: Upgrade\r\n' +
+ 'Sec-WebSocket-Accept: CxYS6+NgJSBG74mdgLvGscRvpns=\r\n' +
+ '\r\n'
+ );
+ });
+
+ const ws = new WebSocket(`ws://localhost:${server.address().port}`);
+
+ ws.on('error', (err) => {
+ assert.ok(err instanceof Error);
+ assert.strictEqual(err.message, 'Invalid Sec-WebSocket-Accept header');
+ done();
+ });
+ });
+
+ it('close event is raised when server closes connection', (done) => {
+ server.once('upgrade', (req, socket) => {
+ const key = crypto
+ .createHash('sha1')
+ .update(req.headers['sec-websocket-key'] + GUID)
+ .digest('base64');
+
+ socket.end(
+ 'HTTP/1.1 101 Switching Protocols\r\n' +
+ 'Upgrade: websocket\r\n' +
+ 'Connection: Upgrade\r\n' +
+ `Sec-WebSocket-Accept: ${key}\r\n` +
+ '\r\n'
+ );
+ });
+
+ const ws = new WebSocket(`ws://localhost:${server.address().port}`);
+
+ ws.on('close', (code, reason) => {
+ assert.strictEqual(code, 1006);
+ assert.strictEqual(reason, EMPTY_BUFFER);
+ done();
+ });
+ });
+
+ it('error is emitted if server aborts connection', (done) => {
+ server.once('upgrade', (req, socket) => {
+ socket.end(
+ `HTTP/1.1 401 ${http.STATUS_CODES[401]}\r\n` +
+ 'Connection: close\r\n' +
+ 'Content-type: text/html\r\n' +
+ `Content-Length: ${http.STATUS_CODES[401].length}\r\n` +
+ '\r\n'
+ );
+ });
+
+ const ws = new WebSocket(`ws://localhost:${server.address().port}`);
+
+ ws.on('open', () => done(new Error("Unexpected 'open' event")));
+ ws.on('error', (err) => {
+ assert.ok(err instanceof Error);
+ assert.strictEqual(err.message, 'Unexpected server response: 401');
+ done();
+ });
+ });
+
+ it('unexpected response can be read when sent by server', (done) => {
+ server.once('upgrade', (req, socket) => {
+ socket.end(
+ `HTTP/1.1 401 ${http.STATUS_CODES[401]}\r\n` +
+ 'Connection: close\r\n' +
+ 'Content-type: text/html\r\n' +
+ 'Content-Length: 3\r\n' +
+ '\r\n' +
+ 'foo'
+ );
+ });
+
+ const ws = new WebSocket(`ws://localhost:${server.address().port}`);
+
+ ws.on('open', () => done(new Error("Unexpected 'open' event")));
+ ws.on('error', () => done(new Error("Unexpected 'error' event")));
+ ws.on('unexpected-response', (req, res) => {
+ assert.strictEqual(res.statusCode, 401);
+
+ let data = '';
+
+ res.on('data', (v) => {
+ data += v;
+ });
+
+ res.on('end', () => {
+ assert.strictEqual(data, 'foo');
+ done();
+ });
+ });
+ });
+
+ it('request can be aborted when unexpected response is sent by server', (done) => {
+ server.once('upgrade', (req, socket) => {
+ socket.end(
+ `HTTP/1.1 401 ${http.STATUS_CODES[401]}\r\n` +
+ 'Connection: close\r\n' +
+ 'Content-type: text/html\r\n' +
+ 'Content-Length: 3\r\n' +
+ '\r\n' +
+ 'foo'
+ );
+ });
+
+ const ws = new WebSocket(`ws://localhost:${server.address().port}`);
+
+ ws.on('open', () => done(new Error("Unexpected 'open' event")));
+ ws.on('error', () => done(new Error("Unexpected 'error' event")));
+ ws.on('unexpected-response', (req, res) => {
+ assert.strictEqual(res.statusCode, 401);
+
+ res.on('end', done);
+ req.abort();
+ });
+ });
+
+ it('fails if the opening handshake timeout expires', (done) => {
+ server.once('upgrade', (req, socket) => socket.on('end', socket.end));
+
+ const port = server.address().port;
+ const ws = new WebSocket(`ws://localhost:${port}`, {
+ handshakeTimeout: 100
+ });
+
+ ws.on('open', () => done(new Error("Unexpected 'open' event")));
+ ws.on('error', (err) => {
+ assert.ok(err instanceof Error);
+ assert.strictEqual(err.message, 'Opening handshake has timed out');
+ done();
+ });
+ });
+
+ it('fails if an unexpected Sec-WebSocket-Extensions header is received', (done) => {
+ server.once('upgrade', (req, socket) => {
+ const key = crypto
+ .createHash('sha1')
+ .update(req.headers['sec-websocket-key'] + GUID)
+ .digest('base64');
+
+ socket.end(
+ 'HTTP/1.1 101 Switching Protocols\r\n' +
+ 'Upgrade: websocket\r\n' +
+ 'Connection: Upgrade\r\n' +
+ `Sec-WebSocket-Accept: ${key}\r\n` +
+ 'Sec-WebSocket-Extensions: foo\r\n' +
+ '\r\n'
+ );
+ });
+
+ const ws = new WebSocket(`ws://localhost:${server.address().port}`, {
+ perMessageDeflate: false
+ });
+
+ ws.on('open', () => done(new Error("Unexpected 'open' event")));
+ ws.on('error', (err) => {
+ assert.ok(err instanceof Error);
+ assert.strictEqual(
+ err.message,
+ 'Server sent a Sec-WebSocket-Extensions header but no extension ' +
+ 'was requested'
+ );
+ ws.on('close', () => done());
+ });
+ });
+
+ it('fails if the Sec-WebSocket-Extensions header is invalid (1/2)', (done) => {
+ server.once('upgrade', (req, socket) => {
+ const key = crypto
+ .createHash('sha1')
+ .update(req.headers['sec-websocket-key'] + GUID)
+ .digest('base64');
+
+ socket.end(
+ 'HTTP/1.1 101 Switching Protocols\r\n' +
+ 'Upgrade: websocket\r\n' +
+ 'Connection: Upgrade\r\n' +
+ `Sec-WebSocket-Accept: ${key}\r\n` +
+ 'Sec-WebSocket-Extensions: foo;=\r\n' +
+ '\r\n'
+ );
+ });
+
+ const ws = new WebSocket(`ws://localhost:${server.address().port}`);
+
+ ws.on('open', () => done(new Error("Unexpected 'open' event")));
+ ws.on('error', (err) => {
+ assert.ok(err instanceof Error);
+ assert.strictEqual(
+ err.message,
+ 'Invalid Sec-WebSocket-Extensions header'
+ );
+ ws.on('close', () => done());
+ });
+ });
+
+ it('fails if the Sec-WebSocket-Extensions header is invalid (2/2)', (done) => {
+ server.once('upgrade', (req, socket) => {
+ const key = crypto
+ .createHash('sha1')
+ .update(req.headers['sec-websocket-key'] + GUID)
+ .digest('base64');
+
+ socket.end(
+ 'HTTP/1.1 101 Switching Protocols\r\n' +
+ 'Upgrade: websocket\r\n' +
+ 'Connection: Upgrade\r\n' +
+ `Sec-WebSocket-Accept: ${key}\r\n` +
+ 'Sec-WebSocket-Extensions: ' +
+ 'permessage-deflate; client_max_window_bits=7\r\n' +
+ '\r\n'
+ );
+ });
+
+ const ws = new WebSocket(`ws://localhost:${server.address().port}`);
+
+ ws.on('open', () => done(new Error("Unexpected 'open' event")));
+ ws.on('error', (err) => {
+ assert.ok(err instanceof Error);
+ assert.strictEqual(
+ err.message,
+ 'Invalid Sec-WebSocket-Extensions header'
+ );
+ ws.on('close', () => done());
+ });
+ });
+
+ it('fails if an unexpected extension is received (1/2)', (done) => {
+ server.once('upgrade', (req, socket) => {
+ const key = crypto
+ .createHash('sha1')
+ .update(req.headers['sec-websocket-key'] + GUID)
+ .digest('base64');
+
+ socket.end(
+ 'HTTP/1.1 101 Switching Protocols\r\n' +
+ 'Upgrade: websocket\r\n' +
+ 'Connection: Upgrade\r\n' +
+ `Sec-WebSocket-Accept: ${key}\r\n` +
+ 'Sec-WebSocket-Extensions: foo\r\n' +
+ '\r\n'
+ );
+ });
+
+ const ws = new WebSocket(`ws://localhost:${server.address().port}`);
+
+ ws.on('open', () => done(new Error("Unexpected 'open' event")));
+ ws.on('error', (err) => {
+ assert.ok(err instanceof Error);
+ assert.strictEqual(
+ err.message,
+ 'Server indicated an extension that was not requested'
+ );
+ ws.on('close', () => done());
+ });
+ });
+
+ it('fails if an unexpected extension is received (2/2)', (done) => {
+ server.once('upgrade', (req, socket) => {
+ const key = crypto
+ .createHash('sha1')
+ .update(req.headers['sec-websocket-key'] + GUID)
+ .digest('base64');
+
+ socket.end(
+ 'HTTP/1.1 101 Switching Protocols\r\n' +
+ 'Upgrade: websocket\r\n' +
+ 'Connection: Upgrade\r\n' +
+ `Sec-WebSocket-Accept: ${key}\r\n` +
+ 'Sec-WebSocket-Extensions: permessage-deflate,foo\r\n' +
+ '\r\n'
+ );
+ });
+
+ const ws = new WebSocket(`ws://localhost:${server.address().port}`);
+
+ ws.on('open', () => done(new Error("Unexpected 'open' event")));
+ ws.on('error', (err) => {
+ assert.ok(err instanceof Error);
+ assert.strictEqual(
+ err.message,
+ 'Server indicated an extension that was not requested'
+ );
+ ws.on('close', () => done());
+ });
+ });
+
+ it('fails if server sends a subprotocol when none was requested', (done) => {
+ const wss = new WebSocket.Server({ server });
+
+ wss.on('headers', (headers) => {
+ headers.push('Sec-WebSocket-Protocol: foo');
+ });
+
+ const ws = new WebSocket(`ws://localhost:${server.address().port}`);
+
+ ws.on('open', () => done(new Error("Unexpected 'open' event")));
+ ws.on('error', (err) => {
+ assert.ok(err instanceof Error);
+ assert.strictEqual(
+ err.message,
+ 'Server sent a subprotocol but none was requested'
+ );
+ ws.on('close', () => wss.close(done));
+ });
+ });
+
+ it('fails if server sends an invalid subprotocol (1/2)', (done) => {
+ const wss = new WebSocket.Server({
+ handleProtocols: () => 'baz',
+ server
+ });
+
+ const ws = new WebSocket(`ws://localhost:${server.address().port}`, [
+ 'foo',
+ 'bar'
+ ]);
+
+ ws.on('open', () => done(new Error("Unexpected 'open' event")));
+ ws.on('error', (err) => {
+ assert.ok(err instanceof Error);
+ assert.strictEqual(err.message, 'Server sent an invalid subprotocol');
+ ws.on('close', () => wss.close(done));
+ });
+ });
+
+ it('fails if server sends an invalid subprotocol (2/2)', (done) => {
+ server.once('upgrade', (req, socket) => {
+ const key = crypto
+ .createHash('sha1')
+ .update(req.headers['sec-websocket-key'] + GUID)
+ .digest('base64');
+
+ socket.end(
+ 'HTTP/1.1 101 Switching Protocols\r\n' +
+ 'Upgrade: websocket\r\n' +
+ 'Connection: Upgrade\r\n' +
+ `Sec-WebSocket-Accept: ${key}\r\n` +
+ 'Sec-WebSocket-Protocol:\r\n' +
+ '\r\n'
+ );
+ });
+
+ const ws = new WebSocket(`ws://localhost:${server.address().port}`, [
+ 'foo',
+ 'bar'
+ ]);
+
+ ws.on('open', () => done(new Error("Unexpected 'open' event")));
+ ws.on('error', (err) => {
+ assert.ok(err instanceof Error);
+ assert.strictEqual(err.message, 'Server sent an invalid subprotocol');
+ ws.on('close', () => done());
+ });
+ });
+
+ it('fails if server sends no subprotocol', (done) => {
+ const wss = new WebSocket.Server({
+ handleProtocols() {},
+ server
+ });
+
+ const ws = new WebSocket(`ws://localhost:${server.address().port}`, [
+ 'foo',
+ 'bar'
+ ]);
+
+ ws.on('open', () => done(new Error("Unexpected 'open' event")));
+ ws.on('error', (err) => {
+ assert.ok(err instanceof Error);
+ assert.strictEqual(err.message, 'Server sent no subprotocol');
+ ws.on('close', () => wss.close(done));
+ });
+ });
+
+ it('does not follow redirects by default', (done) => {
+ server.once('upgrade', (req, socket) => {
+ socket.end(
+ 'HTTP/1.1 301 Moved Permanently\r\n' +
+ 'Location: ws://localhost:8080\r\n' +
+ '\r\n'
+ );
+ });
+
+ const ws = new WebSocket(`ws://localhost:${server.address().port}`);
+
+ ws.on('open', () => done(new Error("Unexpected 'open' event")));
+ ws.on('error', (err) => {
+ assert.ok(err instanceof Error);
+ assert.strictEqual(err.message, 'Unexpected server response: 301');
+ assert.strictEqual(ws._redirects, 0);
+ ws.on('close', () => done());
+ });
+ });
+
+ it('honors the `followRedirects` option', (done) => {
+ const wss = new WebSocket.Server({ noServer: true, path: '/foo' });
+
+ server.once('upgrade', (req, socket) => {
+ socket.end('HTTP/1.1 302 Found\r\nLocation: /foo\r\n\r\n');
+ server.once('upgrade', (req, socket, head) => {
+ wss.handleUpgrade(req, socket, head, NOOP);
+ });
+ });
+
+ const port = server.address().port;
+ const ws = new WebSocket(`ws://localhost:${port}`, {
+ followRedirects: true
+ });
+
+ ws.on('open', () => {
+ assert.strictEqual(ws.url, `ws://localhost:${port}/foo`);
+ assert.strictEqual(ws._redirects, 1);
+ ws.on('close', () => done());
+ ws.close();
+ });
+ });
+
+ it('honors the `maxRedirects` option', (done) => {
+ const onUpgrade = (req, socket) => {
+ socket.end('HTTP/1.1 302 Found\r\nLocation: /\r\n\r\n');
+ };
+
+ server.on('upgrade', onUpgrade);
+
+ const ws = new WebSocket(`ws://localhost:${server.address().port}`, {
+ followRedirects: true,
+ maxRedirects: 1
+ });
+
+ ws.on('open', () => done(new Error("Unexpected 'open' event")));
+ ws.on('error', (err) => {
+ assert.ok(err instanceof Error);
+ assert.strictEqual(err.message, 'Maximum redirects exceeded');
+ assert.strictEqual(ws._redirects, 2);
+
+ server.removeListener('upgrade', onUpgrade);
+ ws.on('close', () => done());
+ });
+ });
+
+ it('emits an error if the redirect URL is invalid (1/2)', (done) => {
+ server.once('upgrade', (req, socket) => {
+ socket.end('HTTP/1.1 302 Found\r\nLocation: ws://\r\n\r\n');
+ });
+
+ const ws = new WebSocket(`ws://localhost:${server.address().port}`, {
+ followRedirects: true
+ });
+
+ ws.on('open', () => done(new Error("Unexpected 'open' event")));
+ ws.on('error', (err) => {
+ assert.ok(err instanceof SyntaxError);
+ assert.strictEqual(err.message, 'Invalid URL: ws://');
+ assert.strictEqual(ws._redirects, 1);
+
+ ws.on('close', () => done());
+ });
+ });
+
+ it('emits an error if the redirect URL is invalid (2/2)', (done) => {
+ server.once('upgrade', (req, socket) => {
+ socket.end('HTTP/1.1 302 Found\r\nLocation: http://localhost\r\n\r\n');
+ });
+
+ const ws = new WebSocket(`ws://localhost:${server.address().port}`, {
+ followRedirects: true
+ });
+
+ ws.on('open', () => done(new Error("Unexpected 'open' event")));
+ ws.on('error', (err) => {
+ assert.ok(err instanceof SyntaxError);
+ assert.strictEqual(
+ err.message,
+ 'The URL\'s protocol must be one of "ws:", "wss:", or "ws+unix:"'
+ );
+ assert.strictEqual(ws._redirects, 1);
+
+ ws.on('close', () => done());
+ });
+ });
+
+ it('uses the first url userinfo when following redirects', (done) => {
+ const wss = new WebSocket.Server({ noServer: true, path: '/foo' });
+ const authorization = 'Basic Zm9vOmJhcg==';
+
+ server.once('upgrade', (req, socket) => {
+ socket.end(
+ 'HTTP/1.1 302 Found\r\n' +
+ `Location: ws://baz:qux@localhost:${port}/foo\r\n\r\n`
+ );
+ server.once('upgrade', (req, socket, head) => {
+ wss.handleUpgrade(req, socket, head, (ws, req) => {
+ assert.strictEqual(req.headers.authorization, authorization);
+ ws.close();
+ });
+ });
+ });
+
+ const port = server.address().port;
+ const ws = new WebSocket(`ws://foo:bar@localhost:${port}`, {
+ followRedirects: true
+ });
+
+ assert.strictEqual(ws._req.getHeader('Authorization'), authorization);
+
+ ws.on('close', (code) => {
+ assert.strictEqual(code, 1005);
+ assert.strictEqual(ws.url, `ws://baz:qux@localhost:${port}/foo`);
+ assert.strictEqual(ws._redirects, 1);
+
+ wss.close(done);
+ });
+ });
+
+ describe('When moving away from a secure context', () => {
+ function proxy(httpServer, httpsServer) {
+ const server = net.createServer({ allowHalfOpen: true });
+
+ server.on('connection', (socket) => {
+ socket.on('readable', function read() {
+ socket.removeListener('readable', read);
+
+ const buf = socket.read(1);
+ const target = buf[0] === 22 ? httpsServer : httpServer;
+
+ socket.unshift(buf);
+ target.emit('connection', socket);
+ });
+ });
+
+ return server;
+ }
+
+ describe("If there is no 'redirect' event listener", () => {
+ it('drops the `auth` option', (done) => {
+ const httpServer = http.createServer();
+ const httpsServer = https.createServer({
+ cert: fs.readFileSync('test/fixtures/certificate.pem'),
+ key: fs.readFileSync('test/fixtures/key.pem')
+ });
+ const server = proxy(httpServer, httpsServer);
+
+ server.listen(() => {
+ const port = server.address().port;
+
+ httpsServer.on('upgrade', (req, socket) => {
+ socket.on('error', NOOP);
+ socket.end(
+ 'HTTP/1.1 302 Found\r\n' +
+ `Location: ws://localhost:${port}/\r\n\r\n`
+ );
+ });
+
+ const wss = new WebSocket.Server({ server: httpServer });
+
+ wss.on('connection', (ws, req) => {
+ assert.strictEqual(req.headers.authorization, undefined);
+ ws.close();
+ });
+
+ const ws = new WebSocket(`wss://localhost:${port}`, {
+ auth: 'foo:bar',
+ followRedirects: true,
+ rejectUnauthorized: false
+ });
+
+ assert.strictEqual(
+ ws._req.getHeader('Authorization'),
+ 'Basic Zm9vOmJhcg=='
+ );
+
+ ws.on('close', (code) => {
+ assert.strictEqual(code, 1005);
+ assert.strictEqual(ws.url, `ws://localhost:${port}/`);
+ assert.strictEqual(ws._redirects, 1);
+
+ server.close(done);
+ });
+ });
+ });
+
+ it('drops the Authorization and Cookie headers', (done) => {
+ const httpServer = http.createServer();
+ const httpsServer = https.createServer({
+ cert: fs.readFileSync('test/fixtures/certificate.pem'),
+ key: fs.readFileSync('test/fixtures/key.pem')
+ });
+ const server = proxy(httpServer, httpsServer);
+
+ server.listen(() => {
+ const port = server.address().port;
+
+ httpsServer.on('upgrade', (req, socket) => {
+ socket.on('error', NOOP);
+ socket.end(
+ 'HTTP/1.1 302 Found\r\n' +
+ `Location: ws://localhost:${port}/\r\n\r\n`
+ );
+ });
+
+ const headers = {
+ authorization: 'Basic Zm9vOmJhcg==',
+ cookie: 'foo=bar',
+ host: 'foo'
+ };
+
+ const wss = new WebSocket.Server({ server: httpServer });
+
+ wss.on('connection', (ws, req) => {
+ assert.strictEqual(req.headers.authorization, undefined);
+ assert.strictEqual(req.headers.cookie, undefined);
+ assert.strictEqual(req.headers.host, headers.host);
+
+ ws.close();
+ });
+
+ const ws = new WebSocket(`wss://localhost:${port}`, {
+ followRedirects: true,
+ headers,
+ rejectUnauthorized: false
+ });
+
+ const firstRequest = ws._req;
+
+ assert.strictEqual(
+ firstRequest.getHeader('Authorization'),
+ headers.authorization
+ );
+ assert.strictEqual(
+ firstRequest.getHeader('Cookie'),
+ headers.cookie
+ );
+ assert.strictEqual(firstRequest.getHeader('Host'), headers.host);
+
+ ws.on('close', (code) => {
+ assert.strictEqual(code, 1005);
+ assert.strictEqual(ws.url, `ws://localhost:${port}/`);
+ assert.strictEqual(ws._redirects, 1);
+
+ server.close(done);
+ });
+ });
+ });
+ });
+
+ describe("If there is at least one 'redirect' event listener", () => {
+ it('does not drop any headers by default', (done) => {
+ const httpServer = http.createServer();
+ const httpsServer = https.createServer({
+ cert: fs.readFileSync('test/fixtures/certificate.pem'),
+ key: fs.readFileSync('test/fixtures/key.pem')
+ });
+ const server = proxy(httpServer, httpsServer);
+
+ server.listen(() => {
+ const port = server.address().port;
+
+ httpsServer.on('upgrade', (req, socket) => {
+ socket.on('error', NOOP);
+ socket.end(
+ 'HTTP/1.1 302 Found\r\n' +
+ `Location: ws://localhost:${port}/\r\n\r\n`
+ );
+ });
+
+ const headers = {
+ authorization: 'Basic Zm9vOmJhcg==',
+ cookie: 'foo=bar',
+ host: 'foo'
+ };
+
+ const wss = new WebSocket.Server({ server: httpServer });
+
+ wss.on('connection', (ws, req) => {
+ assert.strictEqual(
+ req.headers.authorization,
+ headers.authorization
+ );
+ assert.strictEqual(req.headers.cookie, headers.cookie);
+ assert.strictEqual(req.headers.host, headers.host);
+
+ ws.close();
+ });
+
+ const ws = new WebSocket(`wss://localhost:${port}`, {
+ followRedirects: true,
+ headers,
+ rejectUnauthorized: false
+ });
+
+ const firstRequest = ws._req;
+
+ assert.strictEqual(
+ firstRequest.getHeader('Authorization'),
+ headers.authorization
+ );
+ assert.strictEqual(
+ firstRequest.getHeader('Cookie'),
+ headers.cookie
+ );
+ assert.strictEqual(firstRequest.getHeader('Host'), headers.host);
+
+ ws.on('redirect', (url, req) => {
+ assert.strictEqual(ws._redirects, 1);
+ assert.strictEqual(url, `ws://localhost:${port}/`);
+ assert.notStrictEqual(firstRequest, req);
+ assert.strictEqual(
+ req.getHeader('Authorization'),
+ headers.authorization
+ );
+ assert.strictEqual(req.getHeader('Cookie'), headers.cookie);
+ assert.strictEqual(req.getHeader('Host'), headers.host);
+
+ ws.on('close', (code) => {
+ assert.strictEqual(code, 1005);
+ server.close(done);
+ });
+ });
+ });
+ });
+ });
+ });
+
+ describe('When the redirect host is different', () => {
+ describe("If there is no 'redirect' event listener", () => {
+ it('drops the `auth` option', (done) => {
+ const wss = new WebSocket.Server({ port: 0 }, () => {
+ const port = wss.address().port;
+
+ server.once('upgrade', (req, socket) => {
+ socket.end(
+ 'HTTP/1.1 302 Found\r\n' +
+ `Location: ws://localhost:${port}/\r\n\r\n`
+ );
+ });
+
+ const ws = new WebSocket(
+ `ws://localhost:${server.address().port}`,
+ {
+ auth: 'foo:bar',
+ followRedirects: true
+ }
+ );
+
+ assert.strictEqual(
+ ws._req.getHeader('Authorization'),
+ 'Basic Zm9vOmJhcg=='
+ );
+
+ ws.on('close', (code) => {
+ assert.strictEqual(code, 1005);
+ assert.strictEqual(ws.url, `ws://localhost:${port}/`);
+ assert.strictEqual(ws._redirects, 1);
+
+ wss.close(done);
+ });
+ });
+
+ wss.on('connection', (ws, req) => {
+ assert.strictEqual(req.headers.authorization, undefined);
+ ws.close();
+ });
+ });
+
+ it('drops the Authorization, Cookie and Host headers (1/4)', (done) => {
+ // Test the `ws:` to `ws:` case.
+
+ const wss = new WebSocket.Server({ port: 0 }, () => {
+ const port = wss.address().port;
+
+ server.once('upgrade', (req, socket) => {
+ socket.end(
+ 'HTTP/1.1 302 Found\r\n' +
+ `Location: ws://localhost:${port}/\r\n\r\n`
+ );
+ });
+
+ const headers = {
+ authorization: 'Basic Zm9vOmJhcg==',
+ cookie: 'foo=bar',
+ host: 'foo'
+ };
+
+ const ws = new WebSocket(
+ `ws://localhost:${server.address().port}`,
+ { followRedirects: true, headers }
+ );
+
+ const firstRequest = ws._req;
+
+ assert.strictEqual(
+ firstRequest.getHeader('Authorization'),
+ headers.authorization
+ );
+ assert.strictEqual(
+ firstRequest.getHeader('Cookie'),
+ headers.cookie
+ );
+ assert.strictEqual(firstRequest.getHeader('Host'), headers.host);
+
+ ws.on('close', (code) => {
+ assert.strictEqual(code, 1005);
+ assert.strictEqual(ws.url, `ws://localhost:${port}/`);
+ assert.strictEqual(ws._redirects, 1);
+
+ wss.close(done);
+ });
+ });
+
+ wss.on('connection', (ws, req) => {
+ assert.strictEqual(req.headers.authorization, undefined);
+ assert.strictEqual(req.headers.cookie, undefined);
+ assert.strictEqual(
+ req.headers.host,
+ `localhost:${wss.address().port}`
+ );
+
+ ws.close();
+ });
+ });
+
+ it('drops the Authorization, Cookie and Host headers (2/4)', function (done) {
+ if (process.platform === 'win32') return this.skip();
+
+ // Test the `ws:` to `ws+unix:` case.
+
+ const socketPath = path.join(
+ os.tmpdir(),
+ `ws.${crypto.randomBytes(16).toString('hex')}.sock`
+ );
+
+ server.once('upgrade', (req, socket) => {
+ socket.end(
+ `HTTP/1.1 302 Found\r\nLocation: ws+unix://${socketPath}\r\n\r\n`
+ );
+ });
+
+ const redirectedServer = http.createServer();
+ const wss = new WebSocket.Server({ server: redirectedServer });
+
+ wss.on('connection', (ws, req) => {
+ assert.strictEqual(req.headers.authorization, undefined);
+ assert.strictEqual(req.headers.cookie, undefined);
+ assert.strictEqual(req.headers.host, 'localhost');
+
+ ws.close();
+ });
+
+ redirectedServer.listen(socketPath, () => {
+ const headers = {
+ authorization: 'Basic Zm9vOmJhcg==',
+ cookie: 'foo=bar',
+ host: 'foo'
+ };
+
+ const ws = new WebSocket(
+ `ws://localhost:${server.address().port}`,
+ { followRedirects: true, headers }
+ );
+
+ const firstRequest = ws._req;
+
+ assert.strictEqual(
+ firstRequest.getHeader('Authorization'),
+ headers.authorization
+ );
+ assert.strictEqual(
+ firstRequest.getHeader('Cookie'),
+ headers.cookie
+ );
+ assert.strictEqual(firstRequest.getHeader('Host'), headers.host);
+
+ ws.on('close', (code) => {
+ assert.strictEqual(code, 1005);
+ assert.strictEqual(ws.url, `ws+unix://${socketPath}`);
+ assert.strictEqual(ws._redirects, 1);
+
+ redirectedServer.close(done);
+ });
+ });
+ });
+
+ it('drops the Authorization, Cookie and Host headers (3/4)', function (done) {
+ if (process.platform === 'win32') return this.skip();
+
+ // Test the `ws+unix:` to `ws+unix:` case.
+
+ const redirectingServerSocketPath = path.join(
+ os.tmpdir(),
+ `ws.${crypto.randomBytes(16).toString('hex')}.sock`
+ );
+ const redirectedServerSocketPath = path.join(
+ os.tmpdir(),
+ `ws.${crypto.randomBytes(16).toString('hex')}.sock`
+ );
+
+ const redirectingServer = http.createServer();
+
+ redirectingServer.on('upgrade', (req, socket) => {
+ socket.end(
+ 'HTTP/1.1 302 Found\r\n' +
+ `Location: ws+unix://${redirectedServerSocketPath}\r\n\r\n`
+ );
+ });
+
+ const redirectedServer = http.createServer();
+ const wss = new WebSocket.Server({ server: redirectedServer });
+
+ wss.on('connection', (ws, req) => {
+ assert.strictEqual(req.headers.authorization, undefined);
+ assert.strictEqual(req.headers.cookie, undefined);
+ assert.strictEqual(req.headers.host, 'localhost');
+
+ ws.close();
+ });
+
+ redirectingServer.listen(redirectingServerSocketPath, listening);
+ redirectedServer.listen(redirectedServerSocketPath, listening);
+
+ let callCount = 0;
+
+ function listening() {
+ if (++callCount !== 2) return;
+
+ const headers = {
+ authorization: 'Basic Zm9vOmJhcg==',
+ cookie: 'foo=bar',
+ host: 'foo'
+ };
+
+ const ws = new WebSocket(
+ `ws+unix://${redirectingServerSocketPath}`,
+ { followRedirects: true, headers }
+ );
+
+ const firstRequest = ws._req;
+
+ assert.strictEqual(
+ firstRequest.getHeader('Authorization'),
+ headers.authorization
+ );
+ assert.strictEqual(
+ firstRequest.getHeader('Cookie'),
+ headers.cookie
+ );
+ assert.strictEqual(firstRequest.getHeader('Host'), headers.host);
+
+ ws.on('close', (code) => {
+ assert.strictEqual(code, 1005);
+ assert.strictEqual(
+ ws.url,
+ `ws+unix://${redirectedServerSocketPath}`
+ );
+ assert.strictEqual(ws._redirects, 1);
+
+ redirectingServer.close();
+ redirectedServer.close(done);
+ });
+ }
+ });
+
+ it('drops the Authorization, Cookie and Host headers (4/4)', function (done) {
+ if (process.platform === 'win32') return this.skip();
+
+ // Test the `ws+unix:` to `ws:` case.
+
+ const redirectingServer = http.createServer();
+ const redirectedServer = http.createServer();
+ const wss = new WebSocket.Server({ server: redirectedServer });
+
+ wss.on('connection', (ws, req) => {
+ assert.strictEqual(req.headers.authorization, undefined);
+ assert.strictEqual(req.headers.cookie, undefined);
+ assert.strictEqual(
+ req.headers.host,
+ `localhost:${redirectedServer.address().port}`
+ );
+
+ ws.close();
+ });
+
+ const socketPath = path.join(
+ os.tmpdir(),
+ `ws.${crypto.randomBytes(16).toString('hex')}.sock`
+ );
+
+ redirectingServer.listen(socketPath, listening);
+ redirectedServer.listen(0, listening);
+
+ let callCount = 0;
+
+ function listening() {
+ if (++callCount !== 2) return;
+
+ const port = redirectedServer.address().port;
+
+ redirectingServer.on('upgrade', (req, socket) => {
+ socket.end(
+ `HTTP/1.1 302 Found\r\nLocation: ws://localhost:${port}\r\n\r\n`
+ );
+ });
+
+ const headers = {
+ authorization: 'Basic Zm9vOmJhcg==',
+ cookie: 'foo=bar',
+ host: 'foo'
+ };
+
+ const ws = new WebSocket(`ws+unix://${socketPath}`, {
+ followRedirects: true,
+ headers
+ });
+
+ const firstRequest = ws._req;
+
+ assert.strictEqual(
+ firstRequest.getHeader('Authorization'),
+ headers.authorization
+ );
+ assert.strictEqual(
+ firstRequest.getHeader('Cookie'),
+ headers.cookie
+ );
+ assert.strictEqual(firstRequest.getHeader('Host'), headers.host);
+
+ ws.on('close', (code) => {
+ assert.strictEqual(code, 1005);
+ assert.strictEqual(ws.url, `ws://localhost:${port}/`);
+ assert.strictEqual(ws._redirects, 1);
+
+ redirectingServer.close();
+ redirectedServer.close(done);
+ });
+ }
+ });
+ });
+
+ describe("If there is at least one 'redirect' event listener", () => {
+ it('does not drop any headers by default', (done) => {
+ const headers = {
+ authorization: 'Basic Zm9vOmJhcg==',
+ cookie: 'foo=bar',
+ host: 'foo'
+ };
+
+ const wss = new WebSocket.Server({ port: 0 }, () => {
+ const port = wss.address().port;
+
+ server.once('upgrade', (req, socket) => {
+ socket.end(
+ 'HTTP/1.1 302 Found\r\n' +
+ `Location: ws://localhost:${port}/\r\n\r\n`
+ );
+ });
+
+ const ws = new WebSocket(
+ `ws://localhost:${server.address().port}`,
+ { followRedirects: true, headers }
+ );
+
+ const firstRequest = ws._req;
+
+ assert.strictEqual(
+ firstRequest.getHeader('Authorization'),
+ headers.authorization
+ );
+ assert.strictEqual(
+ firstRequest.getHeader('Cookie'),
+ headers.cookie
+ );
+ assert.strictEqual(firstRequest.getHeader('Host'), headers.host);
+
+ ws.on('redirect', (url, req) => {
+ assert.strictEqual(ws._redirects, 1);
+ assert.strictEqual(url, `ws://localhost:${port}/`);
+ assert.notStrictEqual(firstRequest, req);
+ assert.strictEqual(
+ req.getHeader('Authorization'),
+ headers.authorization
+ );
+ assert.strictEqual(req.getHeader('Cookie'), headers.cookie);
+ assert.strictEqual(req.getHeader('Host'), headers.host);
+
+ ws.on('close', (code) => {
+ assert.strictEqual(code, 1005);
+ wss.close(done);
+ });
+ });
+ });
+
+ wss.on('connection', (ws, req) => {
+ assert.strictEqual(
+ req.headers.authorization,
+ headers.authorization
+ );
+ assert.strictEqual(req.headers.cookie, headers.cookie);
+ assert.strictEqual(req.headers.host, headers.host);
+ ws.close();
+ });
+ });
+ });
+ });
+
+ describe("In a listener of the 'redirect' event", () => {
+ it('allows to abort the request without swallowing errors', (done) => {
+ server.once('upgrade', (req, socket) => {
+ socket.end('HTTP/1.1 302 Found\r\nLocation: /foo\r\n\r\n');
+ });
+
+ const port = server.address().port;
+ const ws = new WebSocket(`ws://localhost:${port}`, {
+ followRedirects: true
+ });
+
+ ws.on('redirect', (url, req) => {
+ assert.strictEqual(ws._redirects, 1);
+ assert.strictEqual(url, `ws://localhost:${port}/foo`);
+
+ req.on('socket', () => {
+ req.abort();
+ });
+
+ ws.on('error', (err) => {
+ assert.ok(err instanceof Error);
+ assert.strictEqual(err.message, 'socket hang up');
+
+ ws.on('close', (code) => {
+ assert.strictEqual(code, 1006);
+ done();
+ });
+ });
+ });
+ });
+
+ it('allows to remove headers', (done) => {
+ const wss = new WebSocket.Server({ port: 0 }, () => {
+ const port = wss.address().port;
+
+ server.once('upgrade', (req, socket) => {
+ socket.end(
+ 'HTTP/1.1 302 Found\r\n' +
+ `Location: ws://localhost:${port}/\r\n\r\n`
+ );
+ });
+
+ const headers = {
+ authorization: 'Basic Zm9vOmJhcg==',
+ cookie: 'foo=bar'
+ };
+
+ const ws = new WebSocket(`ws://localhost:${server.address().port}`, {
+ followRedirects: true,
+ headers
+ });
+
+ ws.on('redirect', (url, req) => {
+ assert.strictEqual(ws._redirects, 1);
+ assert.strictEqual(url, `ws://localhost:${port}/`);
+ assert.strictEqual(
+ req.getHeader('Authorization'),
+ headers.authorization
+ );
+ assert.strictEqual(req.getHeader('Cookie'), headers.cookie);
+
+ req.removeHeader('authorization');
+ req.removeHeader('cookie');
+
+ ws.on('close', (code) => {
+ assert.strictEqual(code, 1005);
+ wss.close(done);
+ });
+ });
+ });
+
+ wss.on('connection', (ws, req) => {
+ assert.strictEqual(req.headers.authorization, undefined);
+ assert.strictEqual(req.headers.cookie, undefined);
+ ws.close();
+ });
+ });
+ });
+ });
+
+ describe('Connection with query string', () => {
+ it('connects when pathname is not null', (done) => {
+ const wss = new WebSocket.Server({ port: 0 }, () => {
+ const port = wss.address().port;
+ const ws = new WebSocket(`ws://localhost:${port}/?token=qwerty`);
+
+ ws.on('open', () => {
+ wss.close(done);
+ });
+ });
+
+ wss.on('connection', (ws) => {
+ ws.close();
+ });
+ });
+
+ it('connects when pathname is null', (done) => {
+ const wss = new WebSocket.Server({ port: 0 }, () => {
+ const port = wss.address().port;
+ const ws = new WebSocket(`ws://localhost:${port}?token=qwerty`);
+
+ ws.on('open', () => {
+ wss.close(done);
+ });
+ });
+
+ wss.on('connection', (ws) => {
+ ws.close();
+ });
+ });
+ });
+
+ describe('#pause', () => {
+ it('does nothing if `readyState` is `CONNECTING` or `CLOSED`', (done) => {
+ const wss = new WebSocket.Server({ port: 0 }, () => {
+ const ws = new WebSocket(`ws://localhost:${wss.address().port}`);
+
+ assert.strictEqual(ws.readyState, WebSocket.CONNECTING);
+ assert.ok(!ws.isPaused);
+
+ ws.pause();
+ assert.ok(!ws.isPaused);
+
+ ws.on('open', () => {
+ ws.on('close', () => {
+ assert.strictEqual(ws.readyState, WebSocket.CLOSED);
+
+ ws.pause();
+ assert.ok(!ws.isPaused);
+
+ wss.close(done);
+ });
+
+ ws.close();
+ });
+ });
+ });
+
+ it('pauses the socket', (done) => {
+ const wss = new WebSocket.Server({ port: 0 }, () => {
+ const ws = new WebSocket(`ws://localhost:${wss.address().port}`);
+ });
+
+ wss.on('connection', (ws) => {
+ assert.ok(!ws.isPaused);
+ assert.ok(!ws._socket.isPaused());
+
+ ws.pause();
+ assert.ok(ws.isPaused);
+ assert.ok(ws._socket.isPaused());
+
+ ws.terminate();
+ wss.close(done);
+ });
+ });
+ });
+
+ describe('#ping', () => {
+ it('throws an error if `readyState` is `CONNECTING`', () => {
+ const ws = new WebSocket('ws://localhost', {
+ lookup() {}
+ });
+
+ assert.throws(
+ () => ws.ping(),
+ /^Error: WebSocket is not open: readyState 0 \(CONNECTING\)$/
+ );
+
+ assert.throws(
+ () => ws.ping(NOOP),
+ /^Error: WebSocket is not open: readyState 0 \(CONNECTING\)$/
+ );
+ });
+
+ it('increases `bufferedAmount` if `readyState` is 2 or 3', (done) => {
+ const ws = new WebSocket('ws://localhost', {
+ lookup() {}
+ });
+
+ ws.on('error', (err) => {
+ assert.ok(err instanceof Error);
+ assert.strictEqual(
+ err.message,
+ 'WebSocket was closed before the connection was established'
+ );
+
+ assert.strictEqual(ws.readyState, WebSocket.CLOSING);
+ assert.strictEqual(ws.bufferedAmount, 0);
+
+ ws.ping('hi');
+ assert.strictEqual(ws.bufferedAmount, 2);
+
+ ws.ping();
+ assert.strictEqual(ws.bufferedAmount, 2);
+
+ ws.on('close', () => {
+ assert.strictEqual(ws.readyState, WebSocket.CLOSED);
+
+ ws.ping('hi');
+ assert.strictEqual(ws.bufferedAmount, 4);
+
+ ws.ping();
+ assert.strictEqual(ws.bufferedAmount, 4);
+
+ done();
+ });
+ });
+
+ ws.close();
+ });
+
+ it('calls the callback w/ an error if `readyState` is 2 or 3', (done) => {
+ const wss = new WebSocket.Server({ port: 0 }, () => {
+ const ws = new WebSocket(`ws://localhost:${wss.address().port}`);
+ });
+
+ wss.on('connection', (ws) => {
+ ws.close();
+
+ assert.strictEqual(ws.bufferedAmount, 0);
+
+ ws.ping('hi', (err) => {
+ assert.ok(err instanceof Error);
+ assert.strictEqual(
+ err.message,
+ 'WebSocket is not open: readyState 2 (CLOSING)'
+ );
+ assert.strictEqual(ws.bufferedAmount, 2);
+
+ ws.on('close', () => {
+ ws.ping((err) => {
+ assert.ok(err instanceof Error);
+ assert.strictEqual(
+ err.message,
+ 'WebSocket is not open: readyState 3 (CLOSED)'
+ );
+ assert.strictEqual(ws.bufferedAmount, 2);
+
+ wss.close(done);
+ });
+ });
+ });
+ });
+ });
+
+ it('can send a ping with no data', (done) => {
+ const wss = new WebSocket.Server({ port: 0 }, () => {
+ const ws = new WebSocket(`ws://localhost:${wss.address().port}`);
+
+ ws.on('open', () => {
+ ws.ping(() => {
+ ws.ping();
+ ws.close();
+ });
+ });
+ });
+
+ wss.on('connection', (ws) => {
+ let pings = 0;
+ ws.on('ping', (data) => {
+ assert.ok(Buffer.isBuffer(data));
+ assert.strictEqual(data.length, 0);
+ if (++pings === 2) wss.close(done);
+ });
+ });
+ });
+
+ it('can send a ping with data', (done) => {
+ const wss = new WebSocket.Server({ port: 0 }, () => {
+ const ws = new WebSocket(`ws://localhost:${wss.address().port}`);
+
+ ws.on('open', () => {
+ ws.ping('hi', () => {
+ ws.ping('hi', true);
+ ws.close();
+ });
+ });
+ });
+
+ wss.on('connection', (ws) => {
+ let pings = 0;
+ ws.on('ping', (message) => {
+ assert.strictEqual(message.toString(), 'hi');
+ if (++pings === 2) wss.close(done);
+ });
+ });
+ });
+
+ it('can send numbers as ping payload', (done) => {
+ const wss = new WebSocket.Server({ port: 0 }, () => {
+ const ws = new WebSocket(`ws://localhost:${wss.address().port}`);
+
+ ws.on('open', () => {
+ ws.ping(0);
+ ws.close();
+ });
+ });
+
+ wss.on('connection', (ws) => {
+ ws.on('ping', (message) => {
+ assert.strictEqual(message.toString(), '0');
+ wss.close(done);
+ });
+ });
+ });
+
+ it('throws an error if the data size is greater than 125 bytes', (done) => {
+ const wss = new WebSocket.Server({ port: 0 }, () => {
+ const ws = new WebSocket(`ws://localhost:${wss.address().port}`);
+
+ ws.on('open', () => {
+ assert.throws(
+ () => ws.ping(Buffer.alloc(126)),
+ /^RangeError: The data size must not be greater than 125 bytes$/
+ );
+
+ wss.close(done);
+ });
+ });
+
+ wss.on('connection', (ws) => {
+ ws.close();
+ });
+ });
+ });
+
+ describe('#pong', () => {
+ it('throws an error if `readyState` is `CONNECTING`', () => {
+ const ws = new WebSocket('ws://localhost', {
+ lookup() {}
+ });
+
+ assert.throws(
+ () => ws.pong(),
+ /^Error: WebSocket is not open: readyState 0 \(CONNECTING\)$/
+ );
+
+ assert.throws(
+ () => ws.pong(NOOP),
+ /^Error: WebSocket is not open: readyState 0 \(CONNECTING\)$/
+ );
+ });
+
+ it('increases `bufferedAmount` if `readyState` is 2 or 3', (done) => {
+ const ws = new WebSocket('ws://localhost', {
+ lookup() {}
+ });
+
+ ws.on('error', (err) => {
+ assert.ok(err instanceof Error);
+ assert.strictEqual(
+ err.message,
+ 'WebSocket was closed before the connection was established'
+ );
+
+ assert.strictEqual(ws.readyState, WebSocket.CLOSING);
+ assert.strictEqual(ws.bufferedAmount, 0);
+
+ ws.pong('hi');
+ assert.strictEqual(ws.bufferedAmount, 2);
+
+ ws.pong();
+ assert.strictEqual(ws.bufferedAmount, 2);
+
+ ws.on('close', () => {
+ assert.strictEqual(ws.readyState, WebSocket.CLOSED);
+
+ ws.pong('hi');
+ assert.strictEqual(ws.bufferedAmount, 4);
+
+ ws.pong();
+ assert.strictEqual(ws.bufferedAmount, 4);
+
+ done();
+ });
+ });
+
+ ws.close();
+ });
+
+ it('calls the callback w/ an error if `readyState` is 2 or 3', (done) => {
+ const wss = new WebSocket.Server({ port: 0 }, () => {
+ const ws = new WebSocket(`ws://localhost:${wss.address().port}`);
+ });
+
+ wss.on('connection', (ws) => {
+ ws.close();
+
+ assert.strictEqual(ws.bufferedAmount, 0);
+
+ ws.pong('hi', (err) => {
+ assert.ok(err instanceof Error);
+ assert.strictEqual(
+ err.message,
+ 'WebSocket is not open: readyState 2 (CLOSING)'
+ );
+ assert.strictEqual(ws.bufferedAmount, 2);
+
+ ws.on('close', () => {
+ ws.pong((err) => {
+ assert.ok(err instanceof Error);
+ assert.strictEqual(
+ err.message,
+ 'WebSocket is not open: readyState 3 (CLOSED)'
+ );
+ assert.strictEqual(ws.bufferedAmount, 2);
+
+ wss.close(done);
+ });
+ });
+ });
+ });
+ });
+
+ it('can send a pong with no data', (done) => {
+ const wss = new WebSocket.Server({ port: 0 }, () => {
+ const ws = new WebSocket(`ws://localhost:${wss.address().port}`);
+
+ ws.on('open', () => {
+ ws.pong(() => {
+ ws.pong();
+ ws.close();
+ });
+ });
+ });
+
+ wss.on('connection', (ws) => {
+ let pongs = 0;
+ ws.on('pong', (data) => {
+ assert.ok(Buffer.isBuffer(data));
+ assert.strictEqual(data.length, 0);
+ if (++pongs === 2) wss.close(done);
+ });
+ });
+ });
+
+ it('can send a pong with data', (done) => {
+ const wss = new WebSocket.Server({ port: 0 }, () => {
+ const ws = new WebSocket(`ws://localhost:${wss.address().port}`);
+
+ ws.on('open', () => {
+ ws.pong('hi', () => {
+ ws.pong('hi', true);
+ ws.close();
+ });
+ });
+ });
+
+ wss.on('connection', (ws) => {
+ let pongs = 0;
+ ws.on('pong', (message) => {
+ assert.strictEqual(message.toString(), 'hi');
+ if (++pongs === 2) wss.close(done);
+ });
+ });
+ });
+
+ it('can send numbers as pong payload', (done) => {
+ const wss = new WebSocket.Server({ port: 0 }, () => {
+ const ws = new WebSocket(`ws://localhost:${wss.address().port}`);
+
+ ws.on('open', () => {
+ ws.pong(0);
+ ws.close();
+ });
+ });
+
+ wss.on('connection', (ws) => {
+ ws.on('pong', (message) => {
+ assert.strictEqual(message.toString(), '0');
+ wss.close(done);
+ });
+ });
+ });
+
+ it('throws an error if the data size is greater than 125 bytes', (done) => {
+ const wss = new WebSocket.Server({ port: 0 }, () => {
+ const ws = new WebSocket(`ws://localhost:${wss.address().port}`);
+
+ ws.on('open', () => {
+ assert.throws(
+ () => ws.pong(Buffer.alloc(126)),
+ /^RangeError: The data size must not be greater than 125 bytes$/
+ );
+
+ wss.close(done);
+ });
+ });
+
+ wss.on('connection', (ws) => {
+ ws.close();
+ });
+ });
+ });
+
+ describe('#resume', () => {
+ it('does nothing if `readyState` is `CONNECTING` or `CLOSED`', (done) => {
+ const wss = new WebSocket.Server({ port: 0 }, () => {
+ const ws = new WebSocket(`ws://localhost:${wss.address().port}`);
+
+ assert.strictEqual(ws.readyState, WebSocket.CONNECTING);
+ assert.ok(!ws.isPaused);
+
+ // Verify that no exception is thrown.
+ ws.resume();
+
+ ws.on('open', () => {
+ ws.pause();
+ assert.ok(ws.isPaused);
+
+ ws.on('close', () => {
+ assert.strictEqual(ws.readyState, WebSocket.CLOSED);
+
+ ws.resume();
+ assert.ok(ws.isPaused);
+
+ wss.close(done);
+ });
+
+ ws.terminate();
+ });
+ });
+ });
+
+ it('resumes the socket', (done) => {
+ const wss = new WebSocket.Server({ port: 0 }, () => {
+ const ws = new WebSocket(`ws://localhost:${wss.address().port}`);
+ });
+
+ wss.on('connection', (ws) => {
+ assert.ok(!ws.isPaused);
+ assert.ok(!ws._socket.isPaused());
+
+ ws.pause();
+ assert.ok(ws.isPaused);
+ assert.ok(ws._socket.isPaused());
+
+ ws.resume();
+ assert.ok(!ws.isPaused);
+ assert.ok(!ws._socket.isPaused());
+
+ ws.close();
+ wss.close(done);
+ });
+ });
+ });
+
+ describe('#send', () => {
+ it('throws an error if `readyState` is `CONNECTING`', () => {
+ const ws = new WebSocket('ws://localhost', {
+ lookup() {}
+ });
+
+ assert.throws(
+ () => ws.send('hi'),
+ /^Error: WebSocket is not open: readyState 0 \(CONNECTING\)$/
+ );
+
+ assert.throws(
+ () => ws.send('hi', NOOP),
+ /^Error: WebSocket is not open: readyState 0 \(CONNECTING\)$/
+ );
+ });
+
+ it('increases `bufferedAmount` if `readyState` is 2 or 3', (done) => {
+ const ws = new WebSocket('ws://localhost', {
+ lookup() {}
+ });
+
+ ws.on('error', (err) => {
+ assert.ok(err instanceof Error);
+ assert.strictEqual(
+ err.message,
+ 'WebSocket was closed before the connection was established'
+ );
+
+ assert.strictEqual(ws.readyState, WebSocket.CLOSING);
+ assert.strictEqual(ws.bufferedAmount, 0);
+
+ ws.send('hi');
+ assert.strictEqual(ws.bufferedAmount, 2);
+
+ ws.send();
+ assert.strictEqual(ws.bufferedAmount, 2);
+
+ ws.on('close', () => {
+ assert.strictEqual(ws.readyState, WebSocket.CLOSED);
+
+ ws.send('hi');
+ assert.strictEqual(ws.bufferedAmount, 4);
+
+ ws.send();
+ assert.strictEqual(ws.bufferedAmount, 4);
+
+ done();
+ });
+ });
+
+ ws.close();
+ });
+
+ it('calls the callback w/ an error if `readyState` is 2 or 3', (done) => {
+ const wss = new WebSocket.Server({ port: 0 }, () => {
+ const ws = new WebSocket(`ws://localhost:${wss.address().port}`);
+ });
+
+ wss.on('connection', (ws) => {
+ ws.close();
+
+ assert.strictEqual(ws.bufferedAmount, 0);
+
+ ws.send('hi', (err) => {
+ assert.ok(err instanceof Error);
+ assert.strictEqual(
+ err.message,
+ 'WebSocket is not open: readyState 2 (CLOSING)'
+ );
+ assert.strictEqual(ws.bufferedAmount, 2);
+
+ ws.on('close', () => {
+ ws.send('hi', (err) => {
+ assert.ok(err instanceof Error);
+ assert.strictEqual(
+ err.message,
+ 'WebSocket is not open: readyState 3 (CLOSED)'
+ );
+ assert.strictEqual(ws.bufferedAmount, 4);
+
+ wss.close(done);
+ });
+ });
+ });
+ });
+ });
+
+ it('can send a big binary message', (done) => {
+ const wss = new WebSocket.Server({ port: 0 }, () => {
+ const array = new Float32Array(5 * 1024 * 1024);
+
+ for (let i = 0; i < array.length; i++) {
+ array[i] = i / 5;
+ }
+
+ const ws = new WebSocket(`ws://localhost:${wss.address().port}`);
+
+ ws.on('open', () => ws.send(array));
+ ws.on('message', (msg, isBinary) => {
+ assert.deepStrictEqual(msg, Buffer.from(array.buffer));
+ assert.ok(isBinary);
+ wss.close(done);
+ });
+ });
+
+ wss.on('connection', (ws) => {
+ ws.on('message', (msg, isBinary) => {
+ assert.ok(isBinary);
+ ws.send(msg);
+ ws.close();
+ });
+ });
+ });
+
+ it('can send text data', (done) => {
+ const wss = new WebSocket.Server({ port: 0 }, () => {
+ const ws = new WebSocket(`ws://localhost:${wss.address().port}`);
+
+ ws.on('open', () => ws.send('hi'));
+ ws.on('message', (message, isBinary) => {
+ assert.deepStrictEqual(message, Buffer.from('hi'));
+ assert.ok(!isBinary);
+ wss.close(done);
+ });
+ });
+
+ wss.on('connection', (ws) => {
+ ws.on('message', (msg, isBinary) => {
+ ws.send(msg, { binary: isBinary });
+ ws.close();
+ });
+ });
+ });
+
+ it('does not override the `fin` option', (done) => {
+ const wss = new WebSocket.Server({ port: 0 }, () => {
+ const ws = new WebSocket(`ws://localhost:${wss.address().port}`);
+
+ ws.on('open', () => {
+ ws.send('fragment', { fin: false });
+ ws.send('fragment', { fin: true });
+ ws.close();
+ });
+ });
+
+ wss.on('connection', (ws) => {
+ ws.on('message', (msg, isBinary) => {
+ assert.deepStrictEqual(msg, Buffer.from('fragmentfragment'));
+ assert.ok(!isBinary);
+ wss.close(done);
+ });
+ });
+ });
+
+ it('sends numbers as strings', (done) => {
+ const wss = new WebSocket.Server({ port: 0 }, () => {
+ const ws = new WebSocket(`ws://localhost:${wss.address().port}`);
+
+ ws.on('open', () => {
+ ws.send(0);
+ ws.close();
+ });
+ });
+
+ wss.on('connection', (ws) => {
+ ws.on('message', (msg, isBinary) => {
+ assert.deepStrictEqual(msg, Buffer.from('0'));
+ assert.ok(!isBinary);
+ wss.close(done);
+ });
+ });
+ });
+
+ it('can send a `TypedArray`', (done) => {
+ const wss = new WebSocket.Server({ port: 0 }, () => {
+ const array = new Float32Array(6);
+
+ for (let i = 0; i < array.length; ++i) {
+ array[i] = i / 2;
+ }
+
+ const partial = array.subarray(2, 5);
+ const buf = Buffer.from(
+ partial.buffer,
+ partial.byteOffset,
+ partial.byteLength
+ );
+
+ const ws = new WebSocket(`ws://localhost:${wss.address().port}`);
+
+ ws.on('open', () => {
+ ws.send(partial);
+ ws.close();
+ });
+
+ ws.on('message', (message, isBinary) => {
+ assert.deepStrictEqual(message, buf);
+ assert.ok(isBinary);
+ wss.close(done);
+ });
+ });
+
+ wss.on('connection', (ws) => {
+ ws.on('message', (msg, isBinary) => {
+ assert.ok(isBinary);
+ ws.send(msg);
+ });
+ });
+ });
+
+ it('can send an `ArrayBuffer`', (done) => {
+ const wss = new WebSocket.Server({ port: 0 }, () => {
+ const array = new Float32Array(5);
+
+ for (let i = 0; i < array.length; ++i) {
+ array[i] = i / 2;
+ }
+
+ const ws = new WebSocket(`ws://localhost:${wss.address().port}`);
+
+ ws.on('open', () => {
+ ws.send(array.buffer);
+ ws.close();
+ });
+
+ ws.onmessage = (event) => {
+ assert.ok(event.data.equals(Buffer.from(array.buffer)));
+ wss.close(done);
+ };
+ });
+
+ wss.on('connection', (ws) => {
+ ws.on('message', (msg, isBinary) => {
+ assert.ok(isBinary);
+ ws.send(msg);
+ });
+ });
+ });
+
+ it('can send a `Buffer`', (done) => {
+ const wss = new WebSocket.Server({ port: 0 }, () => {
+ const buf = Buffer.from('foobar');
+ const ws = new WebSocket(`ws://localhost:${wss.address().port}`);
+
+ ws.on('open', () => {
+ ws.send(buf);
+ ws.close();
+ });
+
+ ws.onmessage = (event) => {
+ assert.deepStrictEqual(event.data, buf);
+ wss.close(done);
+ };
+ });
+
+ wss.on('connection', (ws) => {
+ ws.on('message', (msg, isBinary) => {
+ assert.ok(isBinary);
+ ws.send(msg);
+ });
+ });
+ });
+
+ it('calls the callback when data is written out', (done) => {
+ const wss = new WebSocket.Server({ port: 0 }, () => {
+ const ws = new WebSocket(`ws://localhost:${wss.address().port}`);
+
+ ws.on('open', () => {
+ ws.send('hi', (err) => {
+ assert.ifError(err);
+ wss.close(done);
+ });
+ });
+ });
+
+ wss.on('connection', (ws) => {
+ ws.close();
+ });
+ });
+
+ it('works when the `data` argument is falsy', (done) => {
+ const wss = new WebSocket.Server({ port: 0 }, () => {
+ const ws = new WebSocket(`ws://localhost:${wss.address().port}`);
+
+ ws.on('open', () => {
+ ws.send();
+ ws.close();
+ });
+ });
+
+ wss.on('connection', (ws) => {
+ ws.on('message', (message, isBinary) => {
+ assert.strictEqual(message, EMPTY_BUFFER);
+ assert.ok(isBinary);
+ wss.close(done);
+ });
+ });
+ });
+
+ it('honors the `mask` option', (done) => {
+ let clientCloseEventEmitted = false;
+ let serverClientCloseEventEmitted = false;
+
+ const wss = new WebSocket.Server({ port: 0 }, () => {
+ const ws = new WebSocket(`ws://localhost:${wss.address().port}`);
+
+ ws.on('open', () => ws.send('hi', { mask: false }));
+ ws.on('close', (code, reason) => {
+ assert.strictEqual(code, 1002);
+ assert.deepStrictEqual(reason, EMPTY_BUFFER);
+
+ clientCloseEventEmitted = true;
+ if (serverClientCloseEventEmitted) wss.close(done);
+ });
+ });
+
+ wss.on('connection', (ws) => {
+ const chunks = [];
+
+ ws._socket.prependListener('data', (chunk) => {
+ chunks.push(chunk);
+ });
+
+ ws.on('error', (err) => {
+ assert.ok(err instanceof RangeError);
+ assert.strictEqual(
+ err.message,
+ 'Invalid WebSocket frame: MASK must be set'
+ );
+ assert.ok(
+ Buffer.concat(chunks).slice(0, 2).equals(Buffer.from('8102', 'hex'))
+ );
+
+ ws.on('close', (code, reason) => {
+ assert.strictEqual(code, 1006);
+ assert.strictEqual(reason, EMPTY_BUFFER);
+
+ serverClientCloseEventEmitted = true;
+ if (clientCloseEventEmitted) wss.close(done);
+ });
+ });
+ });
+ });
+ });
+
+ describe('#close', () => {
+ it('closes the connection if called while connecting (1/3)', (done) => {
+ const wss = new WebSocket.Server({ port: 0 }, () => {
+ const ws = new WebSocket(`ws://localhost:${wss.address().port}`);
+
+ ws.on('open', () => done(new Error("Unexpected 'open' event")));
+ ws.on('error', (err) => {
+ assert.ok(err instanceof Error);
+ assert.strictEqual(
+ err.message,
+ 'WebSocket was closed before the connection was established'
+ );
+ ws.on('close', () => wss.close(done));
+ });
+ ws.close(1001);
+ });
+ });
+
+ it('closes the connection if called while connecting (2/3)', (done) => {
+ const wss = new WebSocket.Server(
+ {
+ verifyClient: (info, cb) => setTimeout(cb, 300, true),
+ port: 0
+ },
+ () => {
+ const ws = new WebSocket(`ws://localhost:${wss.address().port}`);
+
+ ws.on('open', () => done(new Error("Unexpected 'open' event")));
+ ws.on('error', (err) => {
+ assert.ok(err instanceof Error);
+ assert.strictEqual(
+ err.message,
+ 'WebSocket was closed before the connection was established'
+ );
+ ws.on('close', () => wss.close(done));
+ });
+ setTimeout(() => ws.close(1001), 150);
+ }
+ );
+ });
+
+ it('closes the connection if called while connecting (3/3)', (done) => {
+ const server = http.createServer();
+
+ server.listen(0, () => {
+ const ws = new WebSocket(`ws://localhost:${server.address().port}`);
+
+ ws.on('open', () => done(new Error("Unexpected 'open' event")));
+ ws.on('error', (err) => {
+ assert.ok(err instanceof Error);
+ assert.strictEqual(
+ err.message,
+ 'WebSocket was closed before the connection was established'
+ );
+ ws.on('close', () => {
+ server.close(done);
+ });
+ });
+
+ ws.on('unexpected-response', (req, res) => {
+ assert.strictEqual(res.statusCode, 502);
+
+ const chunks = [];
+
+ res.on('data', (chunk) => {
+ chunks.push(chunk);
+ });
+
+ res.on('end', () => {
+ assert.strictEqual(Buffer.concat(chunks).toString(), 'foo');
+ ws.close();
+ });
+ });
+ });
+
+ server.on('upgrade', (req, socket) => {
+ socket.on('end', socket.end);
+
+ socket.write(
+ `HTTP/1.1 502 ${http.STATUS_CODES[502]}\r\n` +
+ 'Connection: keep-alive\r\n' +
+ 'Content-type: text/html\r\n' +
+ 'Content-Length: 3\r\n' +
+ '\r\n' +
+ 'foo'
+ );
+ });
+ });
+
+ it('can be called from an error listener while connecting', (done) => {
+ const ws = new WebSocket('ws://localhost:1337');
+
+ ws.on('open', () => done(new Error("Unexpected 'open' event")));
+ ws.on('error', (err) => {
+ assert.ok(err instanceof Error);
+ assert.strictEqual(err.code, 'ECONNREFUSED');
+ ws.close();
+ ws.on('close', () => done());
+ });
+ }).timeout(4000);
+
+ it("can be called from a listener of the 'redirect' event", (done) => {
+ const server = http.createServer();
+
+ server.once('upgrade', (req, socket) => {
+ socket.end('HTTP/1.1 302 Found\r\nLocation: /foo\r\n\r\n');
+ });
+
+ server.listen(() => {
+ const port = server.address().port;
+ const ws = new WebSocket(`ws://localhost:${port}`, {
+ followRedirects: true
+ });
+
+ ws.on('open', () => {
+ done(new Error("Unexpected 'open' event"));
+ });
+
+ ws.on('error', (err) => {
+ assert.ok(err instanceof Error);
+ assert.strictEqual(
+ err.message,
+ 'WebSocket was closed before the connection was established'
+ );
+
+ ws.on('close', (code) => {
+ assert.strictEqual(code, 1006);
+ server.close(done);
+ });
+ });
+
+ ws.on('redirect', () => {
+ ws.close();
+ });
+ });
+ });
+
+ it("can be called from a listener of the 'upgrade' event", (done) => {
+ const wss = new WebSocket.Server({ port: 0 }, () => {
+ const ws = new WebSocket(`ws://localhost:${wss.address().port}`);
+
+ ws.on('open', () => done(new Error("Unexpected 'open' event")));
+ ws.on('error', (err) => {
+ assert.ok(err instanceof Error);
+ assert.strictEqual(
+ err.message,
+ 'WebSocket was closed before the connection was established'
+ );
+ ws.on('close', () => wss.close(done));
+ });
+ ws.on('upgrade', () => ws.close());
+ });
+ });
+
+ it('sends the close status code only when necessary', (done) => {
+ let sent;
+ const wss = new WebSocket.Server({ port: 0 }, () => {
+ const ws = new WebSocket(`ws://localhost:${wss.address().port}`);
+
+ ws.on('open', () => {
+ ws._socket.once('data', (data) => {
+ sent = data;
+ });
+ });
+ });
+
+ wss.on('connection', (ws) => {
+ ws._socket.once('data', (received) => {
+ assert.deepStrictEqual(
+ received.slice(0, 2),
+ Buffer.from([0x88, 0x80])
+ );
+ assert.deepStrictEqual(sent, Buffer.from([0x88, 0x00]));
+
+ ws.on('close', (code, reason) => {
+ assert.strictEqual(code, 1005);
+ assert.strictEqual(reason, EMPTY_BUFFER);
+ wss.close(done);
+ });
+ });
+ ws.close();
+ });
+ });
+
+ it('works when close reason is not specified', (done) => {
+ const wss = new WebSocket.Server({ port: 0 }, () => {
+ const ws = new WebSocket(`ws://localhost:${wss.address().port}`);
+
+ ws.on('open', () => ws.close(1000));
+ });
+
+ wss.on('connection', (ws) => {
+ ws.on('close', (code, message) => {
+ assert.strictEqual(code, 1000);
+ assert.deepStrictEqual(message, EMPTY_BUFFER);
+ wss.close(done);
+ });
+ });
+ });
+
+ it('works when close reason is specified', (done) => {
+ const wss = new WebSocket.Server({ port: 0 }, () => {
+ const ws = new WebSocket(`ws://localhost:${wss.address().port}`);
+
+ ws.on('open', () => ws.close(1000, 'some reason'));
+ });
+
+ wss.on('connection', (ws) => {
+ ws.on('close', (code, message) => {
+ assert.strictEqual(code, 1000);
+ assert.deepStrictEqual(message, Buffer.from('some reason'));
+ wss.close(done);
+ });
+ });
+ });
+
+ it('permits all buffered data to be delivered', (done) => {
+ const wss = new WebSocket.Server(
+ {
+ perMessageDeflate: { threshold: 0 },
+ port: 0
+ },
+ () => {
+ const ws = new WebSocket(`ws://localhost:${wss.address().port}`);
+ const messages = [];
+
+ ws.on('message', (message, isBinary) => {
+ assert.ok(!isBinary);
+ messages.push(message.toString());
+ });
+ ws.on('close', (code) => {
+ assert.strictEqual(code, 1005);
+ assert.deepStrictEqual(messages, ['foo', 'bar', 'baz']);
+ wss.close(done);
+ });
+ }
+ );
+
+ wss.on('connection', (ws) => {
+ const callback = (err) => assert.ifError(err);
+
+ ws.send('foo', callback);
+ ws.send('bar', callback);
+ ws.send('baz', callback);
+ ws.close();
+ ws.close();
+ });
+ });
+
+ it('allows close code 1013', (done) => {
+ const wss = new WebSocket.Server({ port: 0 }, () => {
+ const ws = new WebSocket(`ws://localhost:${wss.address().port}`);
+
+ ws.on('close', (code) => {
+ assert.strictEqual(code, 1013);
+ wss.close(done);
+ });
+ });
+
+ wss.on('connection', (ws) => ws.close(1013));
+ });
+
+ it('allows close code 1014', (done) => {
+ const wss = new WebSocket.Server({ port: 0 }, () => {
+ const ws = new WebSocket(`ws://localhost:${wss.address().port}`);
+
+ ws.on('close', (code) => {
+ assert.strictEqual(code, 1014);
+ wss.close(done);
+ });
+ });
+
+ wss.on('connection', (ws) => ws.close(1014));
+ });
+
+ it('does nothing if `readyState` is `CLOSED`', (done) => {
+ const wss = new WebSocket.Server({ port: 0 }, () => {
+ const ws = new WebSocket(`ws://localhost:${wss.address().port}`);
+
+ ws.on('close', (code) => {
+ assert.strictEqual(code, 1005);
+ assert.strictEqual(ws.readyState, WebSocket.CLOSED);
+ ws.close();
+ wss.close(done);
+ });
+ });
+
+ wss.on('connection', (ws) => ws.close());
+ });
+
+ it('sets a timer for the closing handshake to complete', (done) => {
+ const wss = new WebSocket.Server({ port: 0 }, () => {
+ const ws = new WebSocket(`ws://localhost:${wss.address().port}`);
+
+ ws.on('close', (code, reason) => {
+ assert.strictEqual(code, 1000);
+ assert.deepStrictEqual(reason, Buffer.from('some reason'));
+ wss.close(done);
+ });
+
+ ws.on('open', () => {
+ let callbackCalled = false;
+
+ assert.strictEqual(ws._closeTimer, null);
+
+ ws.send('foo', () => {
+ callbackCalled = true;
+ });
+
+ ws.close(1000, 'some reason');
+
+ //
+ // Check that the close timer is set even if the `Sender.close()`
+ // callback is not called.
+ //
+ assert.strictEqual(callbackCalled, false);
+ assert.strictEqual(ws._closeTimer._idleTimeout, 30000);
+ });
+ });
+ });
+ });
+
+ describe('#terminate', () => {
+ it('closes the connection if called while connecting (1/2)', (done) => {
+ const wss = new WebSocket.Server({ port: 0 }, () => {
+ const ws = new WebSocket(`ws://localhost:${wss.address().port}`);
+
+ ws.on('open', () => done(new Error("Unexpected 'open' event")));
+ ws.on('error', (err) => {
+ assert.ok(err instanceof Error);
+ assert.strictEqual(
+ err.message,
+ 'WebSocket was closed before the connection was established'
+ );
+ ws.on('close', () => wss.close(done));
+ });
+ ws.terminate();
+ });
+ });
+
+ it('closes the connection if called while connecting (2/2)', (done) => {
+ const wss = new WebSocket.Server(
+ {
+ verifyClient: (info, cb) => setTimeout(cb, 300, true),
+ port: 0
+ },
+ () => {
+ const ws = new WebSocket(`ws://localhost:${wss.address().port}`);
+
+ ws.on('open', () => done(new Error("Unexpected 'open' event")));
+ ws.on('error', (err) => {
+ assert.ok(err instanceof Error);
+ assert.strictEqual(
+ err.message,
+ 'WebSocket was closed before the connection was established'
+ );
+ ws.on('close', () => wss.close(done));
+ });
+ setTimeout(() => ws.terminate(), 150);
+ }
+ );
+ });
+
+ it('can be called from an error listener while connecting', (done) => {
+ const ws = new WebSocket('ws://localhost:1337');
+
+ ws.on('open', () => done(new Error("Unexpected 'open' event")));
+ ws.on('error', (err) => {
+ assert.ok(err instanceof Error);
+ assert.strictEqual(err.code, 'ECONNREFUSED');
+ ws.terminate();
+ ws.on('close', () => done());
+ });
+ }).timeout(4000);
+
+ it("can be called from a listener of the 'redirect' event", (done) => {
+ const server = http.createServer();
+
+ server.once('upgrade', (req, socket) => {
+ socket.end('HTTP/1.1 302 Found\r\nLocation: /foo\r\n\r\n');
+ });
+
+ server.listen(() => {
+ const port = server.address().port;
+ const ws = new WebSocket(`ws://localhost:${port}`, {
+ followRedirects: true
+ });
+
+ ws.on('open', () => {
+ done(new Error("Unexpected 'open' event"));
+ });
+
+ ws.on('error', (err) => {
+ assert.ok(err instanceof Error);
+ assert.strictEqual(
+ err.message,
+ 'WebSocket was closed before the connection was established'
+ );
+
+ ws.on('close', (code) => {
+ assert.strictEqual(code, 1006);
+ server.close(done);
+ });
+ });
+
+ ws.on('redirect', () => {
+ ws.terminate();
+ });
+ });
+ });
+
+ it("can be called from a listener of the 'upgrade' event", (done) => {
+ const wss = new WebSocket.Server({ port: 0 }, () => {
+ const ws = new WebSocket(`ws://localhost:${wss.address().port}`);
+
+ ws.on('open', () => done(new Error("Unexpected 'open' event")));
+ ws.on('error', (err) => {
+ assert.ok(err instanceof Error);
+ assert.strictEqual(
+ err.message,
+ 'WebSocket was closed before the connection was established'
+ );
+ ws.on('close', () => wss.close(done));
+ });
+ ws.on('upgrade', () => ws.terminate());
+ });
+ });
+
+ it('does nothing if `readyState` is `CLOSED`', (done) => {
+ const wss = new WebSocket.Server({ port: 0 }, () => {
+ const ws = new WebSocket(`ws://localhost:${wss.address().port}`);
+
+ ws.on('close', (code) => {
+ assert.strictEqual(code, 1006);
+ assert.strictEqual(ws.readyState, WebSocket.CLOSED);
+ ws.terminate();
+ wss.close(done);
+ });
+ });
+
+ wss.on('connection', (ws) => ws.terminate());
+ });
+ });
+
+ describe('WHATWG API emulation', () => {
+ it('supports the `on{close,error,message,open}` attributes', () => {
+ for (const property of ['onclose', 'onerror', 'onmessage', 'onopen']) {
+ const descriptor = Object.getOwnPropertyDescriptor(
+ WebSocket.prototype,
+ property
+ );
+
+ assert.strictEqual(descriptor.configurable, true);
+ assert.strictEqual(descriptor.enumerable, true);
+ assert.ok(descriptor.get !== undefined);
+ assert.ok(descriptor.set !== undefined);
+ }
+
+ const ws = new WebSocket('ws://localhost', { agent: new CustomAgent() });
+
+ assert.strictEqual(ws.onmessage, null);
+ assert.strictEqual(ws.onclose, null);
+ assert.strictEqual(ws.onerror, null);
+ assert.strictEqual(ws.onopen, null);
+
+ ws.onmessage = NOOP;
+ ws.onerror = NOOP;
+ ws.onclose = NOOP;
+ ws.onopen = NOOP;
+
+ assert.strictEqual(ws.onmessage, NOOP);
+ assert.strictEqual(ws.onclose, NOOP);
+ assert.strictEqual(ws.onerror, NOOP);
+ assert.strictEqual(ws.onopen, NOOP);
+
+ ws.onmessage = 'foo';
+
+ assert.strictEqual(ws.onmessage, null);
+ assert.strictEqual(ws.listenerCount('message'), 0);
+ });
+
+ it('works like the `EventEmitter` interface', (done) => {
+ const wss = new WebSocket.Server({ port: 0 }, () => {
+ const ws = new WebSocket(`ws://localhost:${wss.address().port}`);
+
+ ws.onmessage = (messageEvent) => {
+ assert.strictEqual(messageEvent.data, 'foo');
+ ws.onclose = (closeEvent) => {
+ assert.strictEqual(closeEvent.wasClean, true);
+ assert.strictEqual(closeEvent.code, 1005);
+ assert.strictEqual(closeEvent.reason, '');
+ wss.close(done);
+ };
+ ws.close();
+ };
+
+ ws.onopen = () => ws.send('foo');
+ });
+
+ wss.on('connection', (ws) => {
+ ws.on('message', (msg, isBinary) => {
+ ws.send(msg, { binary: isBinary });
+ });
+ });
+ });
+
+ it("doesn't return listeners added with `on`", () => {
+ const ws = new WebSocket('ws://localhost', { agent: new CustomAgent() });
+
+ ws.on('open', NOOP);
+
+ assert.deepStrictEqual(ws.listeners('open'), [NOOP]);
+ assert.strictEqual(ws.onopen, null);
+ });
+
+ it("doesn't remove listeners added with `on`", () => {
+ const ws = new WebSocket('ws://localhost', { agent: new CustomAgent() });
+
+ ws.on('close', NOOP);
+ ws.onclose = NOOP;
+
+ let listeners = ws.listeners('close');
+
+ assert.strictEqual(listeners.length, 2);
+ assert.strictEqual(listeners[0], NOOP);
+ assert.strictEqual(listeners[1][kListener], NOOP);
+
+ ws.onclose = NOOP;
+
+ listeners = ws.listeners('close');
+
+ assert.strictEqual(listeners.length, 2);
+ assert.strictEqual(listeners[0], NOOP);
+ assert.strictEqual(listeners[1][kListener], NOOP);
+ });
+
+ it('supports the `addEventListener` method', () => {
+ const events = [];
+ const ws = new WebSocket('ws://localhost', { agent: new CustomAgent() });
+
+ ws.addEventListener('foo', () => {});
+ assert.strictEqual(ws.listenerCount('foo'), 0);
+
+ ws.addEventListener('open', () => {
+ events.push('open');
+ assert.strictEqual(ws.listenerCount('open'), 1);
+ });
+
+ assert.strictEqual(ws.listenerCount('open'), 1);
+
+ ws.addEventListener(
+ 'message',
+ () => {
+ events.push('message');
+ assert.strictEqual(ws.listenerCount('message'), 0);
+ },
+ { once: true }
+ );
+
+ assert.strictEqual(ws.listenerCount('message'), 1);
+
+ ws.emit('open');
+ ws.emit('message', EMPTY_BUFFER, false);
+
+ assert.deepStrictEqual(events, ['open', 'message']);
+ });
+
+ it("doesn't return listeners added with `addEventListener`", () => {
+ const ws = new WebSocket('ws://localhost', { agent: new CustomAgent() });
+
+ ws.addEventListener('open', NOOP);
+
+ const listeners = ws.listeners('open');
+
+ assert.strictEqual(listeners.length, 1);
+ assert.strictEqual(listeners[0][kListener], NOOP);
+
+ assert.strictEqual(ws.onopen, null);
+ });
+
+ it("doesn't remove listeners added with `addEventListener`", () => {
+ const ws = new WebSocket('ws://localhost', { agent: new CustomAgent() });
+
+ ws.addEventListener('close', NOOP);
+ ws.onclose = NOOP;
+
+ let listeners = ws.listeners('close');
+
+ assert.strictEqual(listeners.length, 2);
+ assert.strictEqual(listeners[0][kListener], NOOP);
+ assert.strictEqual(listeners[1][kListener], NOOP);
+
+ ws.onclose = NOOP;
+
+ listeners = ws.listeners('close');
+
+ assert.strictEqual(listeners.length, 2);
+ assert.strictEqual(listeners[0][kListener], NOOP);
+ assert.strictEqual(listeners[1][kListener], NOOP);
+ });
+
+ it('supports the `removeEventListener` method', () => {
+ const ws = new WebSocket('ws://localhost', { agent: new CustomAgent() });
+
+ ws.addEventListener('message', NOOP);
+ ws.addEventListener('open', NOOP);
+
+ assert.strictEqual(ws.listeners('message')[0][kListener], NOOP);
+ assert.strictEqual(ws.listeners('open')[0][kListener], NOOP);
+
+ ws.removeEventListener('message', () => {});
+
+ assert.strictEqual(ws.listeners('message')[0][kListener], NOOP);
+
+ ws.removeEventListener('message', NOOP);
+ ws.removeEventListener('open', NOOP);
+
+ assert.strictEqual(ws.listenerCount('message'), 0);
+ assert.strictEqual(ws.listenerCount('open'), 0);
+
+ ws.addEventListener('message', NOOP, { once: true });
+ ws.addEventListener('open', NOOP, { once: true });
+
+ assert.strictEqual(ws.listeners('message')[0][kListener], NOOP);
+ assert.strictEqual(ws.listeners('open')[0][kListener], NOOP);
+
+ ws.removeEventListener('message', () => {});
+
+ assert.strictEqual(ws.listeners('message')[0][kListener], NOOP);
+
+ ws.removeEventListener('message', NOOP);
+ ws.removeEventListener('open', NOOP);
+
+ assert.strictEqual(ws.listenerCount('message'), 0);
+ assert.strictEqual(ws.listenerCount('open'), 0);
+
+ // Multiple listeners.
+ ws.addEventListener('message', NOOP);
+ ws.addEventListener('message', NOOP);
+
+ assert.strictEqual(ws.listeners('message')[0][kListener], NOOP);
+ assert.strictEqual(ws.listeners('message')[1][kListener], NOOP);
+
+ ws.removeEventListener('message', NOOP);
+
+ assert.strictEqual(ws.listeners('message')[0][kListener], NOOP);
+
+ ws.removeEventListener('message', NOOP);
+
+ assert.strictEqual(ws.listenerCount('message'), 0);
+
+ // Listeners not added with `websocket.addEventListener()`.
+ ws.on('message', NOOP);
+
+ assert.deepStrictEqual(ws.listeners('message'), [NOOP]);
+
+ ws.removeEventListener('message', NOOP);
+
+ assert.deepStrictEqual(ws.listeners('message'), [NOOP]);
+
+ ws.onclose = NOOP;
+
+ assert.strictEqual(ws.listeners('close')[0][kListener], NOOP);
+
+ ws.removeEventListener('close', NOOP);
+
+ assert.strictEqual(ws.listeners('close')[0][kListener], NOOP);
+ });
+
+ it('wraps text data in a `MessageEvent`', (done) => {
+ const wss = new WebSocket.Server({ port: 0 }, () => {
+ const ws = new WebSocket(`ws://localhost:${wss.address().port}`);
+
+ ws.addEventListener('open', () => {
+ ws.send('hi');
+ ws.close();
+ });
+
+ ws.addEventListener('message', (event) => {
+ assert.ok(event instanceof MessageEvent);
+ assert.strictEqual(event.data, 'hi');
+ wss.close(done);
+ });
+ });
+
+ wss.on('connection', (ws) => {
+ ws.on('message', (msg, isBinary) => {
+ ws.send(msg, { binary: isBinary });
+ });
+ });
+ });
+
+ it('receives a `CloseEvent` when server closes (1000)', (done) => {
+ const wss = new WebSocket.Server({ port: 0 }, () => {
+ const ws = new WebSocket(`ws://localhost:${wss.address().port}`);
+
+ ws.addEventListener('close', (event) => {
+ assert.ok(event instanceof CloseEvent);
+ assert.ok(event.wasClean);
+ assert.strictEqual(event.reason, '');
+ assert.strictEqual(event.code, 1000);
+ wss.close(done);
+ });
+ });
+
+ wss.on('connection', (ws) => ws.close(1000));
+ });
+
+ it('receives a `CloseEvent` when server closes (4000)', (done) => {
+ const wss = new WebSocket.Server({ port: 0 }, () => {
+ const ws = new WebSocket(`ws://localhost:${wss.address().port}`);
+
+ ws.addEventListener('close', (event) => {
+ assert.ok(event instanceof CloseEvent);
+ assert.ok(event.wasClean);
+ assert.strictEqual(event.reason, 'some daft reason');
+ assert.strictEqual(event.code, 4000);
+ wss.close(done);
+ });
+ });
+
+ wss.on('connection', (ws) => ws.close(4000, 'some daft reason'));
+ });
+
+ it('sets `target` and `type` on events', (done) => {
+ const wss = new WebSocket.Server({ port: 0 }, () => {
+ const err = new Error('forced');
+ const ws = new WebSocket(`ws://localhost:${wss.address().port}`);
+
+ ws.addEventListener('open', (event) => {
+ assert.ok(event instanceof Event);
+ assert.strictEqual(event.type, 'open');
+ assert.strictEqual(event.target, ws);
+ });
+ ws.addEventListener('message', (event) => {
+ assert.ok(event instanceof MessageEvent);
+ assert.strictEqual(event.type, 'message');
+ assert.strictEqual(event.target, ws);
+ ws.close();
+ });
+ ws.addEventListener('close', (event) => {
+ assert.ok(event instanceof CloseEvent);
+ assert.strictEqual(event.type, 'close');
+ assert.strictEqual(event.target, ws);
+ ws.emit('error', err);
+ });
+ ws.addEventListener('error', (event) => {
+ assert.ok(event instanceof ErrorEvent);
+ assert.strictEqual(event.message, 'forced');
+ assert.strictEqual(event.type, 'error');
+ assert.strictEqual(event.target, ws);
+ assert.strictEqual(event.error, err);
+
+ wss.close(done);
+ });
+ });
+
+ wss.on('connection', (client) => client.send('hi'));
+ });
+
+ it('passes binary data as a Node.js `Buffer` by default', (done) => {
+ const wss = new WebSocket.Server({ port: 0 }, () => {
+ const ws = new WebSocket(`ws://localhost:${wss.address().port}`);
+
+ ws.onmessage = (evt) => {
+ assert.ok(Buffer.isBuffer(evt.data));
+ wss.close(done);
+ };
+ });
+
+ wss.on('connection', (ws) => {
+ ws.send(new Uint8Array(4096));
+ ws.close();
+ });
+ });
+
+ it('ignores `binaryType` for text messages', (done) => {
+ const wss = new WebSocket.Server({ port: 0 }, () => {
+ const ws = new WebSocket(`ws://localhost:${wss.address().port}`);
+
+ ws.binaryType = 'arraybuffer';
+
+ ws.onmessage = (evt) => {
+ assert.strictEqual(evt.data, 'foo');
+ wss.close(done);
+ };
+ });
+
+ wss.on('connection', (ws) => {
+ ws.send('foo');
+ ws.close();
+ });
+ });
+
+ it('allows to update `binaryType` on the fly', (done) => {
+ const wss = new WebSocket.Server({ port: 0 }, () => {
+ const ws = new WebSocket(`ws://localhost:${wss.address().port}`);
+
+ function testType(binaryType, next) {
+ const buf = Buffer.from(binaryType);
+ ws.binaryType = binaryType;
+
+ ws.onmessage = (evt) => {
+ if (binaryType === 'nodebuffer') {
+ assert.ok(Buffer.isBuffer(evt.data));
+ assert.ok(evt.data.equals(buf));
+ } else if (binaryType === 'arraybuffer') {
+ assert.ok(evt.data instanceof ArrayBuffer);
+ assert.ok(Buffer.from(evt.data).equals(buf));
+ } else if (binaryType === 'fragments') {
+ assert.deepStrictEqual(evt.data, [buf]);
+ }
+ next();
+ };
+
+ ws.send(buf);
+ }
+
+ ws.onopen = () => {
+ testType('nodebuffer', () => {
+ testType('arraybuffer', () => {
+ testType('fragments', () => {
+ ws.close();
+ wss.close(done);
+ });
+ });
+ });
+ };
+ });
+
+ wss.on('connection', (ws) => {
+ ws.on('message', (msg, isBinary) => {
+ assert.ok(isBinary);
+ ws.send(msg);
+ });
+ });
+ });
+ });
+
+ describe('SSL', () => {
+ it('connects to secure websocket server', (done) => {
+ const server = https.createServer({
+ cert: fs.readFileSync('test/fixtures/certificate.pem'),
+ key: fs.readFileSync('test/fixtures/key.pem')
+ });
+ const wss = new WebSocket.Server({ server });
+
+ wss.on('connection', () => {
+ server.close(done);
+ });
+
+ server.listen(0, () => {
+ const ws = new WebSocket(`wss://127.0.0.1:${server.address().port}`, {
+ rejectUnauthorized: false
+ });
+
+ ws.on('open', ws.close);
+ });
+ });
+
+ it('connects to secure websocket server with client side certificate', (done) => {
+ const server = https.createServer({
+ cert: fs.readFileSync('test/fixtures/certificate.pem'),
+ ca: [fs.readFileSync('test/fixtures/ca-certificate.pem')],
+ key: fs.readFileSync('test/fixtures/key.pem'),
+ requestCert: true
+ });
+
+ const wss = new WebSocket.Server({ noServer: true });
+
+ server.on('upgrade', (request, socket, head) => {
+ assert.ok(socket.authorized);
+
+ wss.handleUpgrade(request, socket, head, (ws) => {
+ ws.on('close', (code) => {
+ assert.strictEqual(code, 1005);
+ server.close(done);
+ });
+ });
+ });
+
+ server.listen(0, () => {
+ const ws = new WebSocket(`wss://localhost:${server.address().port}`, {
+ cert: fs.readFileSync('test/fixtures/client-certificate.pem'),
+ key: fs.readFileSync('test/fixtures/client-key.pem'),
+ rejectUnauthorized: false
+ });
+
+ ws.on('open', ws.close);
+ });
+ });
+
+ it('cannot connect to secure websocket server via ws://', (done) => {
+ const server = https.createServer({
+ cert: fs.readFileSync('test/fixtures/certificate.pem'),
+ key: fs.readFileSync('test/fixtures/key.pem')
+ });
+ const wss = new WebSocket.Server({ server });
+
+ server.listen(0, () => {
+ const ws = new WebSocket(`ws://localhost:${server.address().port}`, {
+ rejectUnauthorized: false
+ });
+
+ ws.on('error', () => {
+ server.close(done);
+ wss.close();
+ });
+ });
+ });
+
+ it('can send and receive text data', (done) => {
+ const server = https.createServer({
+ cert: fs.readFileSync('test/fixtures/certificate.pem'),
+ key: fs.readFileSync('test/fixtures/key.pem')
+ });
+ const wss = new WebSocket.Server({ server });
+
+ wss.on('connection', (ws) => {
+ ws.on('message', (message, isBinary) => {
+ assert.deepStrictEqual(message, Buffer.from('foobar'));
+ assert.ok(!isBinary);
+ server.close(done);
+ });
+ });
+
+ server.listen(0, () => {
+ const ws = new WebSocket(`wss://localhost:${server.address().port}`, {
+ rejectUnauthorized: false
+ });
+
+ ws.on('open', () => {
+ ws.send('foobar');
+ ws.close();
+ });
+ });
+ });
+
+ it('can send a big binary message', (done) => {
+ const buf = crypto.randomBytes(5 * 1024 * 1024);
+ const server = https.createServer({
+ cert: fs.readFileSync('test/fixtures/certificate.pem'),
+ key: fs.readFileSync('test/fixtures/key.pem')
+ });
+ const wss = new WebSocket.Server({ server });
+
+ wss.on('connection', (ws) => {
+ ws.on('message', (message, isBinary) => {
+ assert.ok(isBinary);
+ ws.send(message);
+ ws.close();
+ });
+ });
+
+ server.listen(0, () => {
+ const ws = new WebSocket(`wss://localhost:${server.address().port}`, {
+ rejectUnauthorized: false
+ });
+
+ ws.on('open', () => ws.send(buf));
+ ws.on('message', (message, isBinary) => {
+ assert.deepStrictEqual(message, buf);
+ assert.ok(isBinary);
+
+ server.close(done);
+ });
+ });
+ }).timeout(4000);
+
+ it('allows to disable sending the SNI extension', (done) => {
+ const original = tls.connect;
+
+ tls.connect = (options) => {
+ assert.strictEqual(options.servername, '');
+ tls.connect = original;
+ done();
+ };
+
+ const ws = new WebSocket('wss://127.0.0.1', { servername: '' });
+ });
+
+ it("works around a double 'error' event bug in Node.js", function (done) {
+ //
+ // The `minVersion` and `maxVersion` options are not supported in
+ // Node.js < 10.16.0.
+ //
+ if (process.versions.modules < 64) return this.skip();
+
+ //
+ // The `'error'` event can be emitted multiple times by the
+ // `http.ClientRequest` object in Node.js < 13. This test reproduces the
+ // issue in Node.js 12.
+ //
+ const server = https.createServer({
+ cert: fs.readFileSync('test/fixtures/certificate.pem'),
+ key: fs.readFileSync('test/fixtures/key.pem'),
+ minVersion: 'TLSv1.2'
+ });
+ const wss = new WebSocket.Server({ server });
+
+ server.listen(0, () => {
+ const ws = new WebSocket(`wss://localhost:${server.address().port}`, {
+ maxVersion: 'TLSv1.1',
+ rejectUnauthorized: false
+ });
+
+ ws.on('error', (err) => {
+ assert.ok(err instanceof Error);
+ server.close(done);
+ wss.close();
+ });
+ });
+ });
+ });
+
+ describe('Request headers', () => {
+ it('adds the authorization header if the url has userinfo', (done) => {
+ const agent = new CustomAgent();
+ const userinfo = 'test:testpass';
+
+ agent.addRequest = (req) => {
+ assert.strictEqual(
+ req.getHeader('authorization'),
+ `Basic ${Buffer.from(userinfo).toString('base64')}`
+ );
+ done();
+ };
+
+ const ws = new WebSocket(`ws://${userinfo}@localhost`, { agent });
+ });
+
+ it('honors the `auth` option', (done) => {
+ const agent = new CustomAgent();
+ const auth = 'user:pass';
+
+ agent.addRequest = (req) => {
+ assert.strictEqual(
+ req.getHeader('authorization'),
+ `Basic ${Buffer.from(auth).toString('base64')}`
+ );
+ done();
+ };
+
+ const ws = new WebSocket('ws://localhost', { agent, auth });
+ });
+
+ it('favors the url userinfo over the `auth` option', (done) => {
+ const agent = new CustomAgent();
+ const auth = 'foo:bar';
+ const userinfo = 'baz:qux';
+
+ agent.addRequest = (req) => {
+ assert.strictEqual(
+ req.getHeader('authorization'),
+ `Basic ${Buffer.from(userinfo).toString('base64')}`
+ );
+ done();
+ };
+
+ const ws = new WebSocket(`ws://${userinfo}@localhost`, { agent, auth });
+ });
+
+ it('adds custom headers', (done) => {
+ const agent = new CustomAgent();
+
+ agent.addRequest = (req) => {
+ assert.strictEqual(req.getHeader('cookie'), 'foo=bar');
+ done();
+ };
+
+ const ws = new WebSocket('ws://localhost', {
+ headers: { Cookie: 'foo=bar' },
+ agent
+ });
+ });
+
+ it('excludes default ports from host header', () => {
+ const options = { lookup() {} };
+ const variants = [
+ ['wss://localhost:8443', 'localhost:8443'],
+ ['wss://localhost:443', 'localhost'],
+ ['ws://localhost:88', 'localhost:88'],
+ ['ws://localhost:80', 'localhost']
+ ];
+
+ for (const [url, host] of variants) {
+ const ws = new WebSocket(url, options);
+ assert.strictEqual(ws._req.getHeader('host'), host);
+ }
+ });
+
+ it("doesn't add the origin header by default", (done) => {
+ const agent = new CustomAgent();
+
+ agent.addRequest = (req) => {
+ assert.strictEqual(req.getHeader('origin'), undefined);
+ done();
+ };
+
+ const ws = new WebSocket('ws://localhost', { agent });
+ });
+
+ it('honors the `origin` option (1/2)', (done) => {
+ const agent = new CustomAgent();
+
+ agent.addRequest = (req) => {
+ assert.strictEqual(req.getHeader('origin'), 'https://example.com:8000');
+ done();
+ };
+
+ const ws = new WebSocket('ws://localhost', {
+ origin: 'https://example.com:8000',
+ agent
+ });
+ });
+
+ it('honors the `origin` option (2/2)', (done) => {
+ const agent = new CustomAgent();
+
+ agent.addRequest = (req) => {
+ assert.strictEqual(
+ req.getHeader('sec-websocket-origin'),
+ 'https://example.com:8000'
+ );
+ done();
+ };
+
+ const ws = new WebSocket('ws://localhost', {
+ origin: 'https://example.com:8000',
+ protocolVersion: 8,
+ agent
+ });
+ });
+ });
+
+ describe('permessage-deflate', () => {
+ it('is enabled by default', (done) => {
+ const agent = new CustomAgent();
+
+ agent.addRequest = (req) => {
+ assert.strictEqual(
+ req.getHeader('sec-websocket-extensions'),
+ 'permessage-deflate; client_max_window_bits'
+ );
+ done();
+ };
+
+ const ws = new WebSocket('ws://localhost', { agent });
+ });
+
+ it('can be disabled', (done) => {
+ const agent = new CustomAgent();
+
+ agent.addRequest = (req) => {
+ assert.strictEqual(
+ req.getHeader('sec-websocket-extensions'),
+ undefined
+ );
+ done();
+ };
+
+ const ws = new WebSocket('ws://localhost', {
+ perMessageDeflate: false,
+ agent
+ });
+ });
+
+ it('can send extension parameters', (done) => {
+ const agent = new CustomAgent();
+
+ const value =
+ 'permessage-deflate; server_no_context_takeover;' +
+ ' client_no_context_takeover; server_max_window_bits=10;' +
+ ' client_max_window_bits';
+
+ agent.addRequest = (req) => {
+ assert.strictEqual(req.getHeader('sec-websocket-extensions'), value);
+ done();
+ };
+
+ const ws = new WebSocket('ws://localhost', {
+ perMessageDeflate: {
+ clientNoContextTakeover: true,
+ serverNoContextTakeover: true,
+ clientMaxWindowBits: true,
+ serverMaxWindowBits: 10
+ },
+ agent
+ });
+ });
+
+ it('consumes all received data when connection is closed (1/2)', (done) => {
+ const wss = new WebSocket.Server(
+ {
+ perMessageDeflate: { threshold: 0 },
+ port: 0
+ },
+ () => {
+ const messages = [];
+ const ws = new WebSocket(`ws://localhost:${wss.address().port}`);
+
+ ws.on('open', () => {
+ ws._socket.on('close', () => {
+ assert.strictEqual(ws._receiver._state, 5);
+ });
+ });
+
+ ws.on('message', (message, isBinary) => {
+ assert.ok(!isBinary);
+ messages.push(message.toString());
+ });
+
+ ws.on('close', (code) => {
+ assert.strictEqual(code, 1006);
+ assert.deepStrictEqual(messages, ['foo', 'bar', 'baz', 'qux']);
+ wss.close(done);
+ });
+ }
+ );
+
+ wss.on('connection', (ws) => {
+ ws.send('foo');
+ ws.send('bar');
+ ws.send('baz');
+ ws.send('qux', () => ws._socket.end());
+ });
+ });
+
+ it('consumes all received data when connection is closed (2/2)', (done) => {
+ const wss = new WebSocket.Server(
+ {
+ perMessageDeflate: true,
+ port: 0
+ },
+ () => {
+ const messageLengths = [];
+ const ws = new WebSocket(`ws://localhost:${wss.address().port}`);
+
+ ws.on('open', () => {
+ ws._socket.prependListener('close', () => {
+ assert.strictEqual(ws._receiver._state, 5);
+ assert.strictEqual(ws._socket._readableState.length, 3);
+ });
+
+ const push = ws._socket.push;
+
+ // Override `ws._socket.push()` to know exactly when data is
+ // received and call `ws.terminate()` immediately after that without
+ // relying on a timer.
+ ws._socket.push = (data) => {
+ ws._socket.push = push;
+ ws._socket.push(data);
+ ws.terminate();
+ };
+
+ const payload1 = Buffer.alloc(15 * 1024);
+ const payload2 = Buffer.alloc(1);
+
+ const opts = {
+ fin: true,
+ opcode: 0x02,
+ mask: false,
+ readOnly: false
+ };
+
+ const list = [
+ ...Sender.frame(payload1, { rsv1: false, ...opts }),
+ ...Sender.frame(payload2, { rsv1: true, ...opts })
+ ];
+
+ for (let i = 0; i < 399; i++) {
+ list.push(list[list.length - 2], list[list.length - 1]);
+ }
+
+ // This hack is used because there is no guarantee that more than
+ // 16 KiB will be sent as a single TCP packet.
+ push.call(ws._socket, Buffer.concat(list));
+
+ wss.clients
+ .values()
+ .next()
+ .value.send(payload2, { compress: false });
+ });
+
+ ws.on('message', (message, isBinary) => {
+ assert.ok(isBinary);
+ messageLengths.push(message.length);
+ });
+
+ ws.on('close', (code) => {
+ assert.strictEqual(code, 1006);
+ assert.strictEqual(messageLengths.length, 402);
+ assert.strictEqual(messageLengths[0], 15360);
+ assert.strictEqual(messageLengths[messageLengths.length - 1], 1);
+ wss.close(done);
+ });
+ }
+ );
+ });
+
+ it('handles a close frame received while compressing data', (done) => {
+ const wss = new WebSocket.Server(
+ {
+ perMessageDeflate: true,
+ port: 0
+ },
+ () => {
+ const ws = new WebSocket(`ws://localhost:${wss.address().port}`, {
+ perMessageDeflate: { threshold: 0 }
+ });
+
+ ws.on('open', () => {
+ ws._receiver.on('conclude', () => {
+ assert.ok(ws._sender._deflating);
+ });
+
+ ws.send('foo');
+ ws.send('bar');
+ ws.send('baz');
+ ws.send('qux');
+ });
+ }
+ );
+
+ wss.on('connection', (ws) => {
+ const messages = [];
+
+ ws.on('message', (message, isBinary) => {
+ assert.ok(!isBinary);
+ messages.push(message.toString());
+ });
+
+ ws.on('close', (code, reason) => {
+ assert.deepStrictEqual(messages, ['foo', 'bar', 'baz', 'qux']);
+ assert.strictEqual(code, 1000);
+ assert.deepStrictEqual(reason, EMPTY_BUFFER);
+ wss.close(done);
+ });
+
+ ws.close(1000);
+ });
+ });
+
+ describe('#close', () => {
+ it('can be used while data is being decompressed', (done) => {
+ const wss = new WebSocket.Server(
+ {
+ perMessageDeflate: true,
+ port: 0
+ },
+ () => {
+ const messages = [];
+ const ws = new WebSocket(`ws://localhost:${wss.address().port}`);
+
+ ws.on('open', () => {
+ ws._socket.on('end', () => {
+ assert.strictEqual(ws._receiver._state, 5);
+ });
+ });
+
+ ws.on('message', (message, isBinary) => {
+ assert.ok(!isBinary);
+
+ if (messages.push(message.toString()) > 1) return;
+
+ ws.close(1000);
+ });
+
+ ws.on('close', (code, reason) => {
+ assert.deepStrictEqual(messages, ['', '', '', '']);
+ assert.strictEqual(code, 1000);
+ assert.deepStrictEqual(reason, EMPTY_BUFFER);
+ wss.close(done);
+ });
+ }
+ );
+
+ wss.on('connection', (ws) => {
+ const buf = Buffer.from('c10100c10100c10100c10100', 'hex');
+ ws._socket.write(buf);
+ });
+ });
+ });
+
+ describe('#send', () => {
+ it('can send text data', (done) => {
+ const wss = new WebSocket.Server(
+ {
+ perMessageDeflate: { threshold: 0 },
+ port: 0
+ },
+ () => {
+ const ws = new WebSocket(`ws://localhost:${wss.address().port}`, {
+ perMessageDeflate: { threshold: 0 }
+ });
+
+ ws.on('open', () => {
+ ws.send('hi', { compress: true });
+ ws.close();
+ });
+
+ ws.on('message', (message, isBinary) => {
+ assert.deepStrictEqual(message, Buffer.from('hi'));
+ assert.ok(!isBinary);
+ wss.close(done);
+ });
+ }
+ );
+
+ wss.on('connection', (ws) => {
+ ws.on('message', (message, isBinary) => {
+ ws.send(message, { binary: isBinary, compress: true });
+ });
+ });
+ });
+
+ it('can send a `TypedArray`', (done) => {
+ const array = new Float32Array(5);
+
+ for (let i = 0; i < array.length; i++) {
+ array[i] = i / 2;
+ }
+
+ const wss = new WebSocket.Server(
+ {
+ perMessageDeflate: { threshold: 0 },
+ port: 0
+ },
+ () => {
+ const ws = new WebSocket(`ws://localhost:${wss.address().port}`, {
+ perMessageDeflate: { threshold: 0 }
+ });
+
+ ws.on('open', () => {
+ ws.send(array, { compress: true });
+ ws.close();
+ });
+
+ ws.on('message', (message, isBinary) => {
+ assert.deepStrictEqual(message, Buffer.from(array.buffer));
+ assert.ok(isBinary);
+ wss.close(done);
+ });
+ }
+ );
+
+ wss.on('connection', (ws) => {
+ ws.on('message', (message, isBinary) => {
+ assert.ok(isBinary);
+ ws.send(message, { compress: true });
+ });
+ });
+ });
+
+ it('can send an `ArrayBuffer`', (done) => {
+ const array = new Float32Array(5);
+
+ for (let i = 0; i < array.length; i++) {
+ array[i] = i / 2;
+ }
+
+ const wss = new WebSocket.Server(
+ {
+ perMessageDeflate: { threshold: 0 },
+ port: 0
+ },
+ () => {
+ const ws = new WebSocket(`ws://localhost:${wss.address().port}`, {
+ perMessageDeflate: { threshold: 0 }
+ });
+
+ ws.on('open', () => {
+ ws.send(array.buffer, { compress: true });
+ ws.close();
+ });
+
+ ws.on('message', (message, isBinary) => {
+ assert.deepStrictEqual(message, Buffer.from(array.buffer));
+ assert.ok(isBinary);
+ wss.close(done);
+ });
+ }
+ );
+
+ wss.on('connection', (ws) => {
+ ws.on('message', (message, isBinary) => {
+ assert.ok(isBinary);
+ ws.send(message, { compress: true });
+ });
+ });
+ });
+
+ it('ignores the `compress` option if the extension is disabled', (done) => {
+ const wss = new WebSocket.Server({ port: 0 }, () => {
+ const ws = new WebSocket(`ws://localhost:${wss.address().port}`, {
+ perMessageDeflate: false
+ });
+
+ ws.on('open', () => {
+ ws.send('hi', { compress: true });
+ ws.close();
+ });
+
+ ws.on('message', (message, isBinary) => {
+ assert.deepStrictEqual(message, Buffer.from('hi'));
+ assert.ok(!isBinary);
+ wss.close(done);
+ });
+ });
+
+ wss.on('connection', (ws) => {
+ ws.on('message', (message, isBinary) => {
+ ws.send(message, { binary: isBinary, compress: true });
+ });
+ });
+ });
+
+ it('calls the callback if the socket is closed prematurely', (done) => {
+ const called = [];
+ const wss = new WebSocket.Server(
+ { perMessageDeflate: true, port: 0 },
+ () => {
+ const ws = new WebSocket(`ws://localhost:${wss.address().port}`, {
+ perMessageDeflate: { threshold: 0 }
+ });
+
+ ws.on('open', () => {
+ ws.send('foo');
+ ws.send('bar', (err) => {
+ called.push(1);
+
+ assert.strictEqual(ws.readyState, WebSocket.CLOSING);
+ assert.ok(err instanceof Error);
+ assert.strictEqual(
+ err.message,
+ 'The socket was closed while data was being compressed'
+ );
+ });
+ ws.send('baz');
+ ws.send('qux', (err) => {
+ called.push(2);
+
+ assert.strictEqual(ws.readyState, WebSocket.CLOSING);
+ assert.ok(err instanceof Error);
+ assert.strictEqual(
+ err.message,
+ 'The socket was closed while data was being compressed'
+ );
+ });
+ });
+ }
+ );
+
+ wss.on('connection', (ws) => {
+ ws.on('close', () => {
+ assert.deepStrictEqual(called, [1, 2]);
+ wss.close(done);
+ });
+
+ ws._socket.end();
+ });
+ });
+ });
+
+ describe('#terminate', () => {
+ it('can be used while data is being compressed', (done) => {
+ const wss = new WebSocket.Server(
+ {
+ perMessageDeflate: { threshold: 0 },
+ port: 0
+ },
+ () => {
+ const ws = new WebSocket(`ws://localhost:${wss.address().port}`, {
+ perMessageDeflate: { threshold: 0 }
+ });
+
+ ws.on('open', () => {
+ ws.send('hi', (err) => {
+ assert.strictEqual(ws.readyState, WebSocket.CLOSING);
+ assert.ok(err instanceof Error);
+ assert.strictEqual(
+ err.message,
+ 'The socket was closed while data was being compressed'
+ );
+
+ ws.on('close', () => {
+ wss.close(done);
+ });
+ });
+ ws.terminate();
+ });
+ }
+ );
+ });
+
+ it('can be used while data is being decompressed', (done) => {
+ const wss = new WebSocket.Server(
+ {
+ perMessageDeflate: true,
+ port: 0
+ },
+ () => {
+ const ws = new WebSocket(`ws://localhost:${wss.address().port}`);
+ const messages = [];
+
+ ws.on('message', (message, isBinary) => {
+ assert.ok(!isBinary);
+
+ if (messages.push(message.toString()) > 1) return;
+
+ process.nextTick(() => {
+ assert.strictEqual(ws._receiver._state, 5);
+ ws.terminate();
+ });
+ });
+
+ ws.on('close', (code, reason) => {
+ assert.deepStrictEqual(messages, ['', '', '', '']);
+ assert.strictEqual(code, 1006);
+ assert.strictEqual(reason, EMPTY_BUFFER);
+ wss.close(done);
+ });
+ }
+ );
+
+ wss.on('connection', (ws) => {
+ const buf = Buffer.from('c10100c10100c10100c10100', 'hex');
+ ws._socket.write(buf);
+ });
+ });
+ });
+ });
+
+ describe('Connection close', () => {
+ it('closes cleanly after simultaneous errors (1/2)', (done) => {
+ let clientCloseEventEmitted = false;
+ let serverClientCloseEventEmitted = false;
+
+ const wss = new WebSocket.Server({ port: 0 }, () => {
+ const ws = new WebSocket(`ws://localhost:${wss.address().port}`);
+
+ ws.on('error', (err) => {
+ assert.ok(err instanceof RangeError);
+ assert.strictEqual(err.code, 'WS_ERR_INVALID_OPCODE');
+ assert.strictEqual(
+ err.message,
+ 'Invalid WebSocket frame: invalid opcode 5'
+ );
+
+ ws.on('close', (code, reason) => {
+ assert.strictEqual(code, 1006);
+ assert.strictEqual(reason, EMPTY_BUFFER);
+
+ clientCloseEventEmitted = true;
+ if (serverClientCloseEventEmitted) wss.close(done);
+ });
+ });
+
+ ws.on('open', () => {
+ // Write an invalid frame in both directions to trigger simultaneous
+ // failure.
+ const chunk = Buffer.from([0x85, 0x00]);
+
+ wss.clients.values().next().value._socket.write(chunk);
+ ws._socket.write(chunk);
+ });
+ });
+
+ wss.on('connection', (ws) => {
+ ws.on('error', (err) => {
+ assert.ok(err instanceof RangeError);
+ assert.strictEqual(err.code, 'WS_ERR_INVALID_OPCODE');
+ assert.strictEqual(
+ err.message,
+ 'Invalid WebSocket frame: invalid opcode 5'
+ );
+
+ ws.on('close', (code, reason) => {
+ assert.strictEqual(code, 1006);
+ assert.strictEqual(reason, EMPTY_BUFFER);
+
+ serverClientCloseEventEmitted = true;
+ if (clientCloseEventEmitted) wss.close(done);
+ });
+ });
+ });
+ });
+
+ it('closes cleanly after simultaneous errors (2/2)', (done) => {
+ let clientCloseEventEmitted = false;
+ let serverClientCloseEventEmitted = false;
+
+ const wss = new WebSocket.Server({ port: 0 }, () => {
+ const ws = new WebSocket(`ws://localhost:${wss.address().port}`);
+
+ ws.on('error', (err) => {
+ assert.ok(err instanceof RangeError);
+ assert.strictEqual(err.code, 'WS_ERR_INVALID_OPCODE');
+ assert.strictEqual(
+ err.message,
+ 'Invalid WebSocket frame: invalid opcode 5'
+ );
+
+ ws.on('close', (code, reason) => {
+ assert.strictEqual(code, 1006);
+ assert.strictEqual(reason, EMPTY_BUFFER);
+
+ clientCloseEventEmitted = true;
+ if (serverClientCloseEventEmitted) wss.close(done);
+ });
+ });
+
+ ws.on('open', () => {
+ // Write an invalid frame in both directions and change the
+ // `readyState` to `WebSocket.CLOSING`.
+ const chunk = Buffer.from([0x85, 0x00]);
+ const serverWs = wss.clients.values().next().value;
+
+ serverWs._socket.write(chunk);
+ serverWs.close();
+
+ ws._socket.write(chunk);
+ ws.close();
+ });
+ });
+
+ wss.on('connection', (ws) => {
+ ws.on('error', (err) => {
+ assert.ok(err instanceof RangeError);
+ assert.strictEqual(err.code, 'WS_ERR_INVALID_OPCODE');
+ assert.strictEqual(
+ err.message,
+ 'Invalid WebSocket frame: invalid opcode 5'
+ );
+
+ ws.on('close', (code, reason) => {
+ assert.strictEqual(code, 1006);
+ assert.strictEqual(reason, EMPTY_BUFFER);
+
+ serverClientCloseEventEmitted = true;
+ if (clientCloseEventEmitted) wss.close(done);
+ });
+ });
+ });
+ });
+
+ it('resumes the socket when an error occurs', (done) => {
+ const maxPayload = 16 * 1024;
+ const wss = new WebSocket.Server({ maxPayload, port: 0 }, () => {
+ const ws = new WebSocket(`ws://localhost:${wss.address().port}`);
+ });
+
+ wss.on('connection', (ws) => {
+ const list = [
+ ...Sender.frame(Buffer.alloc(maxPayload + 1), {
+ fin: true,
+ opcode: 0x02,
+ mask: true,
+ readOnly: false
+ })
+ ];
+
+ ws.on('error', (err) => {
+ assert.ok(err instanceof RangeError);
+ assert.strictEqual(err.code, 'WS_ERR_UNSUPPORTED_MESSAGE_LENGTH');
+ assert.strictEqual(err.message, 'Max payload size exceeded');
+
+ ws.on('close', (code, reason) => {
+ assert.strictEqual(code, 1006);
+ assert.strictEqual(reason, EMPTY_BUFFER);
+ wss.close(done);
+ });
+ });
+
+ ws._socket.push(Buffer.concat(list));
+ });
+ });
+
+ it('resumes the socket when the close frame is received', (done) => {
+ const wss = new WebSocket.Server({ port: 0 }, () => {
+ const ws = new WebSocket(`ws://localhost:${wss.address().port}`);
+ });
+
+ wss.on('connection', (ws) => {
+ const opts = { fin: true, mask: true, readOnly: false };
+ const list = [
+ ...Sender.frame(Buffer.alloc(16 * 1024), { opcode: 0x02, ...opts }),
+ ...Sender.frame(EMPTY_BUFFER, { opcode: 0x08, ...opts })
+ ];
+
+ ws.on('close', (code, reason) => {
+ assert.strictEqual(code, 1005);
+ assert.strictEqual(reason, EMPTY_BUFFER);
+ wss.close(done);
+ });
+
+ ws._socket.push(Buffer.concat(list));
+ });
+ });
+ });
+});