summaryrefslogtreecommitdiffstats
path: root/testing/xpcshell/node-ws/test/websocket.test.js
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
commit36d22d82aa202bb199967e9512281e9a53db42c9 (patch)
tree105e8c98ddea1c1e4784a60a5a6410fa416be2de /testing/xpcshell/node-ws/test/websocket.test.js
parentInitial commit. (diff)
downloadfirefox-esr-upstream.tar.xz
firefox-esr-upstream.zip
Adding upstream version 115.7.0esr.upstream/115.7.0esrupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r--testing/xpcshell/node-ws/test/websocket.test.js4514
1 files changed, 4514 insertions, 0 deletions
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));
+ });
+ });
+ });
+});