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