diff options
Diffstat (limited to 'testing/xpcshell/node-ws/test/websocket.test.js')
-rw-r--r-- | testing/xpcshell/node-ws/test/websocket.test.js | 4514 |
1 files changed, 4514 insertions, 0 deletions
diff --git a/testing/xpcshell/node-ws/test/websocket.test.js b/testing/xpcshell/node-ws/test/websocket.test.js new file mode 100644 index 0000000000..f5fbf16505 --- /dev/null +++ b/testing/xpcshell/node-ws/test/websocket.test.js @@ -0,0 +1,4514 @@ +/* eslint no-unused-vars: ["error", { "varsIgnorePattern": "^ws$" }] */ + +'use strict'; + +const assert = require('assert'); +const crypto = require('crypto'); +const https = require('https'); +const http = require('http'); +const path = require('path'); +const net = require('net'); +const tls = require('tls'); +const os = require('os'); +const fs = require('fs'); +const { URL } = require('url'); + +const Sender = require('../lib/sender'); +const WebSocket = require('..'); +const { + CloseEvent, + ErrorEvent, + Event, + MessageEvent +} = require('../lib/event-target'); +const { EMPTY_BUFFER, GUID, kListener, NOOP } = require('../lib/constants'); + +class CustomAgent extends http.Agent { + addRequest() {} +} + +describe('WebSocket', () => { + describe('#ctor', () => { + it('throws an error when using an invalid url', () => { + assert.throws( + () => new WebSocket('foo'), + /^SyntaxError: Invalid URL: foo$/ + ); + + assert.throws( + () => new WebSocket('https://websocket-echo.com'), + /^SyntaxError: The URL's protocol must be one of "ws:", "wss:", or "ws\+unix:"$/ + ); + + assert.throws( + () => new WebSocket('ws+unix:'), + /^SyntaxError: The URL's pathname is empty$/ + ); + + assert.throws( + () => new WebSocket('wss://websocket-echo.com#foo'), + /^SyntaxError: The URL contains a fragment identifier$/ + ); + }); + + it('throws an error if a subprotocol is invalid or duplicated', () => { + for (const subprotocol of [null, '', 'a,b', ['a', 'a']]) { + assert.throws( + () => new WebSocket('ws://localhost', subprotocol), + /^SyntaxError: An invalid or duplicated subprotocol was specified$/ + ); + } + }); + + it('accepts `url.URL` objects as url', (done) => { + const agent = new CustomAgent(); + + agent.addRequest = (req, opts) => { + assert.strictEqual(opts.host, '::1'); + assert.strictEqual(req.path, '/'); + done(); + }; + + const ws = new WebSocket(new URL('ws://[::1]'), { agent }); + }); + + describe('options', () => { + it('accepts the `options` object as 3rd argument', () => { + const agent = new CustomAgent(); + let count = 0; + let ws; + + agent.addRequest = (req) => { + assert.strictEqual( + req.getHeader('sec-websocket-protocol'), + undefined + ); + count++; + }; + + ws = new WebSocket('ws://localhost', undefined, { agent }); + ws = new WebSocket('ws://localhost', [], { agent }); + + assert.strictEqual(count, 2); + }); + + it('accepts the `maxPayload` option', (done) => { + const maxPayload = 20480; + const wss = new WebSocket.Server( + { + perMessageDeflate: true, + port: 0 + }, + () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`, { + perMessageDeflate: true, + maxPayload + }); + + ws.on('open', () => { + assert.strictEqual(ws._receiver._maxPayload, maxPayload); + assert.strictEqual( + ws._receiver._extensions['permessage-deflate']._maxPayload, + maxPayload + ); + wss.close(done); + }); + } + ); + + wss.on('connection', (ws) => { + ws.close(); + }); + }); + + it('throws an error when using an invalid `protocolVersion`', () => { + const options = { agent: new CustomAgent(), protocolVersion: 1000 }; + + assert.throws( + () => new WebSocket('ws://localhost', options), + /^RangeError: Unsupported protocol version: 1000 \(supported versions: 8, 13\)$/ + ); + }); + + it('honors the `generateMask` option', (done) => { + const data = Buffer.from('foo'); + const wss = new WebSocket.Server({ port: 0 }, () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`, { + generateMask() {} + }); + + ws.on('open', () => { + ws.send(data); + }); + + ws.on('close', (code, reason) => { + assert.strictEqual(code, 1005); + assert.deepStrictEqual(reason, EMPTY_BUFFER); + + wss.close(done); + }); + }); + + wss.on('connection', (ws) => { + const chunks = []; + + ws._socket.prependListener('data', (chunk) => { + chunks.push(chunk); + }); + + ws.on('message', (message) => { + assert.deepStrictEqual(message, data); + assert.deepStrictEqual( + Buffer.concat(chunks).slice(2, 6), + Buffer.alloc(4) + ); + + ws.close(); + }); + }); + }); + }); + }); + + describe('Constants', () => { + const readyStates = { + CONNECTING: 0, + OPEN: 1, + CLOSING: 2, + CLOSED: 3 + }; + + Object.keys(readyStates).forEach((state) => { + describe(`\`${state}\``, () => { + it('is enumerable property of class', () => { + const descriptor = Object.getOwnPropertyDescriptor(WebSocket, state); + + assert.deepStrictEqual(descriptor, { + configurable: false, + enumerable: true, + value: readyStates[state], + writable: false + }); + }); + + it('is enumerable property of prototype', () => { + const descriptor = Object.getOwnPropertyDescriptor( + WebSocket.prototype, + state + ); + + assert.deepStrictEqual(descriptor, { + configurable: false, + enumerable: true, + value: readyStates[state], + writable: false + }); + }); + }); + }); + }); + + describe('Attributes', () => { + describe('`binaryType`', () => { + it('is enumerable and configurable', () => { + const descriptor = Object.getOwnPropertyDescriptor( + WebSocket.prototype, + 'binaryType' + ); + + assert.strictEqual(descriptor.configurable, true); + assert.strictEqual(descriptor.enumerable, true); + assert.ok(descriptor.get !== undefined); + assert.ok(descriptor.set !== undefined); + }); + + it("defaults to 'nodebuffer'", () => { + const ws = new WebSocket('ws://localhost', { + agent: new CustomAgent() + }); + + assert.strictEqual(ws.binaryType, 'nodebuffer'); + }); + + it("can be changed to 'arraybuffer' or 'fragments'", () => { + const ws = new WebSocket('ws://localhost', { + agent: new CustomAgent() + }); + + ws.binaryType = 'arraybuffer'; + assert.strictEqual(ws.binaryType, 'arraybuffer'); + + ws.binaryType = 'foo'; + assert.strictEqual(ws.binaryType, 'arraybuffer'); + + ws.binaryType = 'fragments'; + assert.strictEqual(ws.binaryType, 'fragments'); + + ws.binaryType = ''; + assert.strictEqual(ws.binaryType, 'fragments'); + + ws.binaryType = 'nodebuffer'; + assert.strictEqual(ws.binaryType, 'nodebuffer'); + }); + }); + + describe('`bufferedAmount`', () => { + it('is enumerable and configurable', () => { + const descriptor = Object.getOwnPropertyDescriptor( + WebSocket.prototype, + 'bufferedAmount' + ); + + assert.strictEqual(descriptor.configurable, true); + assert.strictEqual(descriptor.enumerable, true); + assert.ok(descriptor.get !== undefined); + assert.ok(descriptor.set === undefined); + }); + + it('defaults to zero', () => { + const ws = new WebSocket('ws://localhost', { + agent: new CustomAgent() + }); + + assert.strictEqual(ws.bufferedAmount, 0); + }); + + it('defaults to zero upon "open"', (done) => { + const wss = new WebSocket.Server({ port: 0 }, () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + + ws.onopen = () => { + assert.strictEqual(ws.bufferedAmount, 0); + wss.close(done); + }; + }); + + wss.on('connection', (ws) => { + ws.close(); + }); + }); + + it('takes into account the data in the sender queue', (done) => { + const wss = new WebSocket.Server( + { + perMessageDeflate: true, + port: 0 + }, + () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`, { + perMessageDeflate: { threshold: 0 } + }); + + ws.on('open', () => { + ws.send('foo'); + + assert.strictEqual(ws.bufferedAmount, 3); + + ws.send('bar', (err) => { + assert.ifError(err); + assert.strictEqual(ws.bufferedAmount, 0); + wss.close(done); + }); + + assert.strictEqual(ws.bufferedAmount, 6); + }); + } + ); + + wss.on('connection', (ws) => { + ws.close(); + }); + }); + + it('takes into account the data in the socket queue', (done) => { + const wss = new WebSocket.Server({ port: 0 }, () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + }); + + wss.on('connection', (ws) => { + const data = Buffer.alloc(1024, 61); + + while (ws.bufferedAmount === 0) { + ws.send(data); + } + + assert.ok(ws.bufferedAmount > 0); + assert.strictEqual( + ws.bufferedAmount, + ws._socket._writableState.length + ); + + ws.on('close', () => wss.close(done)); + ws.close(); + }); + }); + }); + + describe('`extensions`', () => { + it('is enumerable and configurable', () => { + const descriptor = Object.getOwnPropertyDescriptor( + WebSocket.prototype, + 'bufferedAmount' + ); + + assert.strictEqual(descriptor.configurable, true); + assert.strictEqual(descriptor.enumerable, true); + assert.ok(descriptor.get !== undefined); + assert.ok(descriptor.set === undefined); + }); + + it('exposes the negotiated extensions names (1/2)', (done) => { + const wss = new WebSocket.Server({ port: 0 }, () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + + assert.strictEqual(ws.extensions, ''); + + ws.on('open', () => { + assert.strictEqual(ws.extensions, ''); + ws.on('close', () => wss.close(done)); + }); + }); + + wss.on('connection', (ws) => { + assert.strictEqual(ws.extensions, ''); + ws.close(); + }); + }); + + it('exposes the negotiated extensions names (2/2)', (done) => { + const wss = new WebSocket.Server( + { + perMessageDeflate: true, + port: 0 + }, + () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + + assert.strictEqual(ws.extensions, ''); + + ws.on('open', () => { + assert.strictEqual(ws.extensions, 'permessage-deflate'); + ws.on('close', () => wss.close(done)); + }); + } + ); + + wss.on('connection', (ws) => { + assert.strictEqual(ws.extensions, 'permessage-deflate'); + ws.close(); + }); + }); + }); + + describe('`isPaused`', () => { + it('is enumerable and configurable', () => { + const descriptor = Object.getOwnPropertyDescriptor( + WebSocket.prototype, + 'isPaused' + ); + + assert.strictEqual(descriptor.configurable, true); + assert.strictEqual(descriptor.enumerable, true); + assert.ok(descriptor.get !== undefined); + assert.ok(descriptor.set === undefined); + }); + + it('indicates whether the websocket is paused', (done) => { + const wss = new WebSocket.Server({ port: 0 }, () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + + ws.on('open', () => { + ws.pause(); + assert.ok(ws.isPaused); + + ws.resume(); + assert.ok(!ws.isPaused); + + ws.close(); + wss.close(done); + }); + + assert.ok(!ws.isPaused); + }); + }); + }); + + describe('`protocol`', () => { + it('is enumerable and configurable', () => { + const descriptor = Object.getOwnPropertyDescriptor( + WebSocket.prototype, + 'protocol' + ); + + assert.strictEqual(descriptor.configurable, true); + assert.strictEqual(descriptor.enumerable, true); + assert.ok(descriptor.get !== undefined); + assert.ok(descriptor.set === undefined); + }); + + it('exposes the subprotocol selected by the server', (done) => { + const wss = new WebSocket.Server({ port: 0 }, () => { + const port = wss.address().port; + const ws = new WebSocket(`ws://localhost:${port}`, 'foo'); + + assert.strictEqual(ws.extensions, ''); + + ws.on('open', () => { + assert.strictEqual(ws.protocol, 'foo'); + ws.on('close', () => wss.close(done)); + }); + }); + + wss.on('connection', (ws) => { + assert.strictEqual(ws.protocol, 'foo'); + ws.close(); + }); + }); + }); + + describe('`readyState`', () => { + it('is enumerable and configurable', () => { + const descriptor = Object.getOwnPropertyDescriptor( + WebSocket.prototype, + 'readyState' + ); + + assert.strictEqual(descriptor.configurable, true); + assert.strictEqual(descriptor.enumerable, true); + assert.ok(descriptor.get !== undefined); + assert.ok(descriptor.set === undefined); + }); + + it('defaults to `CONNECTING`', () => { + const ws = new WebSocket('ws://localhost', { + agent: new CustomAgent() + }); + + assert.strictEqual(ws.readyState, WebSocket.CONNECTING); + }); + + it('is set to `OPEN` once connection is established', (done) => { + const wss = new WebSocket.Server({ port: 0 }, () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + + ws.on('open', () => { + assert.strictEqual(ws.readyState, WebSocket.OPEN); + ws.close(); + }); + + ws.on('close', () => wss.close(done)); + }); + }); + + it('is set to `CLOSED` once connection is closed', (done) => { + const wss = new WebSocket.Server({ port: 0 }, () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + + ws.on('close', () => { + assert.strictEqual(ws.readyState, WebSocket.CLOSED); + wss.close(done); + }); + + ws.on('open', () => ws.close(1001)); + }); + }); + + it('is set to `CLOSED` once connection is terminated', (done) => { + const wss = new WebSocket.Server({ port: 0 }, () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + + ws.on('close', () => { + assert.strictEqual(ws.readyState, WebSocket.CLOSED); + wss.close(done); + }); + + ws.on('open', () => ws.terminate()); + }); + }); + }); + + describe('`url`', () => { + it('is enumerable and configurable', () => { + const descriptor = Object.getOwnPropertyDescriptor( + WebSocket.prototype, + 'url' + ); + + assert.strictEqual(descriptor.configurable, true); + assert.strictEqual(descriptor.enumerable, true); + assert.ok(descriptor.get !== undefined); + assert.ok(descriptor.set === undefined); + }); + + it('exposes the server url', () => { + const url = 'ws://localhost'; + const ws = new WebSocket(url, { agent: new CustomAgent() }); + + assert.strictEqual(ws.url, url); + }); + }); + }); + + describe('Events', () => { + it("emits an 'error' event if an error occurs", (done) => { + let clientCloseEventEmitted = false; + let serverClientCloseEventEmitted = false; + + const wss = new WebSocket.Server({ port: 0 }, () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + + ws.on('error', (err) => { + assert.ok(err instanceof RangeError); + assert.strictEqual(err.code, 'WS_ERR_INVALID_OPCODE'); + assert.strictEqual( + err.message, + 'Invalid WebSocket frame: invalid opcode 5' + ); + + ws.on('close', (code, reason) => { + assert.strictEqual(code, 1006); + assert.strictEqual(reason, EMPTY_BUFFER); + + clientCloseEventEmitted = true; + if (serverClientCloseEventEmitted) wss.close(done); + }); + }); + }); + + wss.on('connection', (ws) => { + ws.on('close', (code, reason) => { + assert.strictEqual(code, 1002); + assert.deepStrictEqual(reason, EMPTY_BUFFER); + + serverClientCloseEventEmitted = true; + if (clientCloseEventEmitted) wss.close(done); + }); + + ws._socket.write(Buffer.from([0x85, 0x00])); + }); + }); + + it('does not re-emit `net.Socket` errors', (done) => { + const codes = ['EPIPE', 'ECONNABORTED', 'ECANCELED', 'ECONNRESET']; + const wss = new WebSocket.Server({ port: 0 }, () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + + ws.on('open', () => { + ws._socket.on('error', (err) => { + assert.ok(err instanceof Error); + assert.ok(codes.includes(err.code), `Unexpected code: ${err.code}`); + ws.on('close', (code, message) => { + assert.strictEqual(code, 1006); + assert.strictEqual(message, EMPTY_BUFFER); + wss.close(done); + }); + }); + + for (const client of wss.clients) client.terminate(); + ws.send('foo'); + ws.send('bar'); + }); + }); + }); + + it("emits an 'upgrade' event", (done) => { + const wss = new WebSocket.Server({ port: 0 }, () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + ws.on('upgrade', (res) => { + assert.ok(res instanceof http.IncomingMessage); + wss.close(done); + }); + }); + + wss.on('connection', (ws) => { + ws.close(); + }); + }); + + it("emits a 'ping' event", (done) => { + const wss = new WebSocket.Server({ port: 0 }, () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + ws.on('ping', () => wss.close(done)); + }); + + wss.on('connection', (ws) => { + ws.ping(); + ws.close(); + }); + }); + + it("emits a 'pong' event", (done) => { + const wss = new WebSocket.Server({ port: 0 }, () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + ws.on('pong', () => wss.close(done)); + }); + + wss.on('connection', (ws) => { + ws.pong(); + ws.close(); + }); + }); + + it("emits a 'redirect' event", (done) => { + const server = http.createServer(); + const wss = new WebSocket.Server({ noServer: true, path: '/foo' }); + + server.once('upgrade', (req, socket) => { + socket.end('HTTP/1.1 302 Found\r\nLocation: /foo\r\n\r\n'); + server.once('upgrade', (req, socket, head) => { + wss.handleUpgrade(req, socket, head, (ws) => { + ws.close(); + }); + }); + }); + + server.listen(() => { + const port = server.address().port; + const ws = new WebSocket(`ws://localhost:${port}`, { + followRedirects: true + }); + + ws.on('redirect', (url, req) => { + assert.strictEqual(ws._redirects, 1); + assert.strictEqual(url, `ws://localhost:${port}/foo`); + assert.ok(req instanceof http.ClientRequest); + + ws.on('close', (code) => { + assert.strictEqual(code, 1005); + server.close(done); + }); + }); + }); + }); + }); + + describe('Connection establishing', () => { + const server = http.createServer(); + + beforeEach((done) => server.listen(0, done)); + afterEach((done) => server.close(done)); + + it('fails if the Upgrade header field value is not "websocket"', (done) => { + server.once('upgrade', (req, socket) => { + socket.on('end', socket.end); + socket.write( + 'HTTP/1.1 101 Switching Protocols\r\n' + + 'Connection: Upgrade\r\n' + + 'Upgrade: foo\r\n' + + '\r\n' + ); + }); + + const ws = new WebSocket(`ws://localhost:${server.address().port}`); + + ws.on('error', (err) => { + assert.ok(err instanceof Error); + assert.strictEqual(err.message, 'Invalid Upgrade header'); + done(); + }); + }); + + it('fails if the Sec-WebSocket-Accept header is invalid', (done) => { + server.once('upgrade', (req, socket) => { + socket.on('end', socket.end); + socket.write( + 'HTTP/1.1 101 Switching Protocols\r\n' + + 'Upgrade: websocket\r\n' + + 'Connection: Upgrade\r\n' + + 'Sec-WebSocket-Accept: CxYS6+NgJSBG74mdgLvGscRvpns=\r\n' + + '\r\n' + ); + }); + + const ws = new WebSocket(`ws://localhost:${server.address().port}`); + + ws.on('error', (err) => { + assert.ok(err instanceof Error); + assert.strictEqual(err.message, 'Invalid Sec-WebSocket-Accept header'); + done(); + }); + }); + + it('close event is raised when server closes connection', (done) => { + server.once('upgrade', (req, socket) => { + const key = crypto + .createHash('sha1') + .update(req.headers['sec-websocket-key'] + GUID) + .digest('base64'); + + socket.end( + 'HTTP/1.1 101 Switching Protocols\r\n' + + 'Upgrade: websocket\r\n' + + 'Connection: Upgrade\r\n' + + `Sec-WebSocket-Accept: ${key}\r\n` + + '\r\n' + ); + }); + + const ws = new WebSocket(`ws://localhost:${server.address().port}`); + + ws.on('close', (code, reason) => { + assert.strictEqual(code, 1006); + assert.strictEqual(reason, EMPTY_BUFFER); + done(); + }); + }); + + it('error is emitted if server aborts connection', (done) => { + server.once('upgrade', (req, socket) => { + socket.end( + `HTTP/1.1 401 ${http.STATUS_CODES[401]}\r\n` + + 'Connection: close\r\n' + + 'Content-type: text/html\r\n' + + `Content-Length: ${http.STATUS_CODES[401].length}\r\n` + + '\r\n' + ); + }); + + const ws = new WebSocket(`ws://localhost:${server.address().port}`); + + ws.on('open', () => done(new Error("Unexpected 'open' event"))); + ws.on('error', (err) => { + assert.ok(err instanceof Error); + assert.strictEqual(err.message, 'Unexpected server response: 401'); + done(); + }); + }); + + it('unexpected response can be read when sent by server', (done) => { + server.once('upgrade', (req, socket) => { + socket.end( + `HTTP/1.1 401 ${http.STATUS_CODES[401]}\r\n` + + 'Connection: close\r\n' + + 'Content-type: text/html\r\n' + + 'Content-Length: 3\r\n' + + '\r\n' + + 'foo' + ); + }); + + const ws = new WebSocket(`ws://localhost:${server.address().port}`); + + ws.on('open', () => done(new Error("Unexpected 'open' event"))); + ws.on('error', () => done(new Error("Unexpected 'error' event"))); + ws.on('unexpected-response', (req, res) => { + assert.strictEqual(res.statusCode, 401); + + let data = ''; + + res.on('data', (v) => { + data += v; + }); + + res.on('end', () => { + assert.strictEqual(data, 'foo'); + done(); + }); + }); + }); + + it('request can be aborted when unexpected response is sent by server', (done) => { + server.once('upgrade', (req, socket) => { + socket.end( + `HTTP/1.1 401 ${http.STATUS_CODES[401]}\r\n` + + 'Connection: close\r\n' + + 'Content-type: text/html\r\n' + + 'Content-Length: 3\r\n' + + '\r\n' + + 'foo' + ); + }); + + const ws = new WebSocket(`ws://localhost:${server.address().port}`); + + ws.on('open', () => done(new Error("Unexpected 'open' event"))); + ws.on('error', () => done(new Error("Unexpected 'error' event"))); + ws.on('unexpected-response', (req, res) => { + assert.strictEqual(res.statusCode, 401); + + res.on('end', done); + req.abort(); + }); + }); + + it('fails if the opening handshake timeout expires', (done) => { + server.once('upgrade', (req, socket) => socket.on('end', socket.end)); + + const port = server.address().port; + const ws = new WebSocket(`ws://localhost:${port}`, { + handshakeTimeout: 100 + }); + + ws.on('open', () => done(new Error("Unexpected 'open' event"))); + ws.on('error', (err) => { + assert.ok(err instanceof Error); + assert.strictEqual(err.message, 'Opening handshake has timed out'); + done(); + }); + }); + + it('fails if an unexpected Sec-WebSocket-Extensions header is received', (done) => { + server.once('upgrade', (req, socket) => { + const key = crypto + .createHash('sha1') + .update(req.headers['sec-websocket-key'] + GUID) + .digest('base64'); + + socket.end( + 'HTTP/1.1 101 Switching Protocols\r\n' + + 'Upgrade: websocket\r\n' + + 'Connection: Upgrade\r\n' + + `Sec-WebSocket-Accept: ${key}\r\n` + + 'Sec-WebSocket-Extensions: foo\r\n' + + '\r\n' + ); + }); + + const ws = new WebSocket(`ws://localhost:${server.address().port}`, { + perMessageDeflate: false + }); + + ws.on('open', () => done(new Error("Unexpected 'open' event"))); + ws.on('error', (err) => { + assert.ok(err instanceof Error); + assert.strictEqual( + err.message, + 'Server sent a Sec-WebSocket-Extensions header but no extension ' + + 'was requested' + ); + ws.on('close', () => done()); + }); + }); + + it('fails if the Sec-WebSocket-Extensions header is invalid (1/2)', (done) => { + server.once('upgrade', (req, socket) => { + const key = crypto + .createHash('sha1') + .update(req.headers['sec-websocket-key'] + GUID) + .digest('base64'); + + socket.end( + 'HTTP/1.1 101 Switching Protocols\r\n' + + 'Upgrade: websocket\r\n' + + 'Connection: Upgrade\r\n' + + `Sec-WebSocket-Accept: ${key}\r\n` + + 'Sec-WebSocket-Extensions: foo;=\r\n' + + '\r\n' + ); + }); + + const ws = new WebSocket(`ws://localhost:${server.address().port}`); + + ws.on('open', () => done(new Error("Unexpected 'open' event"))); + ws.on('error', (err) => { + assert.ok(err instanceof Error); + assert.strictEqual( + err.message, + 'Invalid Sec-WebSocket-Extensions header' + ); + ws.on('close', () => done()); + }); + }); + + it('fails if the Sec-WebSocket-Extensions header is invalid (2/2)', (done) => { + server.once('upgrade', (req, socket) => { + const key = crypto + .createHash('sha1') + .update(req.headers['sec-websocket-key'] + GUID) + .digest('base64'); + + socket.end( + 'HTTP/1.1 101 Switching Protocols\r\n' + + 'Upgrade: websocket\r\n' + + 'Connection: Upgrade\r\n' + + `Sec-WebSocket-Accept: ${key}\r\n` + + 'Sec-WebSocket-Extensions: ' + + 'permessage-deflate; client_max_window_bits=7\r\n' + + '\r\n' + ); + }); + + const ws = new WebSocket(`ws://localhost:${server.address().port}`); + + ws.on('open', () => done(new Error("Unexpected 'open' event"))); + ws.on('error', (err) => { + assert.ok(err instanceof Error); + assert.strictEqual( + err.message, + 'Invalid Sec-WebSocket-Extensions header' + ); + ws.on('close', () => done()); + }); + }); + + it('fails if an unexpected extension is received (1/2)', (done) => { + server.once('upgrade', (req, socket) => { + const key = crypto + .createHash('sha1') + .update(req.headers['sec-websocket-key'] + GUID) + .digest('base64'); + + socket.end( + 'HTTP/1.1 101 Switching Protocols\r\n' + + 'Upgrade: websocket\r\n' + + 'Connection: Upgrade\r\n' + + `Sec-WebSocket-Accept: ${key}\r\n` + + 'Sec-WebSocket-Extensions: foo\r\n' + + '\r\n' + ); + }); + + const ws = new WebSocket(`ws://localhost:${server.address().port}`); + + ws.on('open', () => done(new Error("Unexpected 'open' event"))); + ws.on('error', (err) => { + assert.ok(err instanceof Error); + assert.strictEqual( + err.message, + 'Server indicated an extension that was not requested' + ); + ws.on('close', () => done()); + }); + }); + + it('fails if an unexpected extension is received (2/2)', (done) => { + server.once('upgrade', (req, socket) => { + const key = crypto + .createHash('sha1') + .update(req.headers['sec-websocket-key'] + GUID) + .digest('base64'); + + socket.end( + 'HTTP/1.1 101 Switching Protocols\r\n' + + 'Upgrade: websocket\r\n' + + 'Connection: Upgrade\r\n' + + `Sec-WebSocket-Accept: ${key}\r\n` + + 'Sec-WebSocket-Extensions: permessage-deflate,foo\r\n' + + '\r\n' + ); + }); + + const ws = new WebSocket(`ws://localhost:${server.address().port}`); + + ws.on('open', () => done(new Error("Unexpected 'open' event"))); + ws.on('error', (err) => { + assert.ok(err instanceof Error); + assert.strictEqual( + err.message, + 'Server indicated an extension that was not requested' + ); + ws.on('close', () => done()); + }); + }); + + it('fails if server sends a subprotocol when none was requested', (done) => { + const wss = new WebSocket.Server({ server }); + + wss.on('headers', (headers) => { + headers.push('Sec-WebSocket-Protocol: foo'); + }); + + const ws = new WebSocket(`ws://localhost:${server.address().port}`); + + ws.on('open', () => done(new Error("Unexpected 'open' event"))); + ws.on('error', (err) => { + assert.ok(err instanceof Error); + assert.strictEqual( + err.message, + 'Server sent a subprotocol but none was requested' + ); + ws.on('close', () => wss.close(done)); + }); + }); + + it('fails if server sends an invalid subprotocol (1/2)', (done) => { + const wss = new WebSocket.Server({ + handleProtocols: () => 'baz', + server + }); + + const ws = new WebSocket(`ws://localhost:${server.address().port}`, [ + 'foo', + 'bar' + ]); + + ws.on('open', () => done(new Error("Unexpected 'open' event"))); + ws.on('error', (err) => { + assert.ok(err instanceof Error); + assert.strictEqual(err.message, 'Server sent an invalid subprotocol'); + ws.on('close', () => wss.close(done)); + }); + }); + + it('fails if server sends an invalid subprotocol (2/2)', (done) => { + server.once('upgrade', (req, socket) => { + const key = crypto + .createHash('sha1') + .update(req.headers['sec-websocket-key'] + GUID) + .digest('base64'); + + socket.end( + 'HTTP/1.1 101 Switching Protocols\r\n' + + 'Upgrade: websocket\r\n' + + 'Connection: Upgrade\r\n' + + `Sec-WebSocket-Accept: ${key}\r\n` + + 'Sec-WebSocket-Protocol:\r\n' + + '\r\n' + ); + }); + + const ws = new WebSocket(`ws://localhost:${server.address().port}`, [ + 'foo', + 'bar' + ]); + + ws.on('open', () => done(new Error("Unexpected 'open' event"))); + ws.on('error', (err) => { + assert.ok(err instanceof Error); + assert.strictEqual(err.message, 'Server sent an invalid subprotocol'); + ws.on('close', () => done()); + }); + }); + + it('fails if server sends no subprotocol', (done) => { + const wss = new WebSocket.Server({ + handleProtocols() {}, + server + }); + + const ws = new WebSocket(`ws://localhost:${server.address().port}`, [ + 'foo', + 'bar' + ]); + + ws.on('open', () => done(new Error("Unexpected 'open' event"))); + ws.on('error', (err) => { + assert.ok(err instanceof Error); + assert.strictEqual(err.message, 'Server sent no subprotocol'); + ws.on('close', () => wss.close(done)); + }); + }); + + it('does not follow redirects by default', (done) => { + server.once('upgrade', (req, socket) => { + socket.end( + 'HTTP/1.1 301 Moved Permanently\r\n' + + 'Location: ws://localhost:8080\r\n' + + '\r\n' + ); + }); + + const ws = new WebSocket(`ws://localhost:${server.address().port}`); + + ws.on('open', () => done(new Error("Unexpected 'open' event"))); + ws.on('error', (err) => { + assert.ok(err instanceof Error); + assert.strictEqual(err.message, 'Unexpected server response: 301'); + assert.strictEqual(ws._redirects, 0); + ws.on('close', () => done()); + }); + }); + + it('honors the `followRedirects` option', (done) => { + const wss = new WebSocket.Server({ noServer: true, path: '/foo' }); + + server.once('upgrade', (req, socket) => { + socket.end('HTTP/1.1 302 Found\r\nLocation: /foo\r\n\r\n'); + server.once('upgrade', (req, socket, head) => { + wss.handleUpgrade(req, socket, head, NOOP); + }); + }); + + const port = server.address().port; + const ws = new WebSocket(`ws://localhost:${port}`, { + followRedirects: true + }); + + ws.on('open', () => { + assert.strictEqual(ws.url, `ws://localhost:${port}/foo`); + assert.strictEqual(ws._redirects, 1); + ws.on('close', () => done()); + ws.close(); + }); + }); + + it('honors the `maxRedirects` option', (done) => { + const onUpgrade = (req, socket) => { + socket.end('HTTP/1.1 302 Found\r\nLocation: /\r\n\r\n'); + }; + + server.on('upgrade', onUpgrade); + + const ws = new WebSocket(`ws://localhost:${server.address().port}`, { + followRedirects: true, + maxRedirects: 1 + }); + + ws.on('open', () => done(new Error("Unexpected 'open' event"))); + ws.on('error', (err) => { + assert.ok(err instanceof Error); + assert.strictEqual(err.message, 'Maximum redirects exceeded'); + assert.strictEqual(ws._redirects, 2); + + server.removeListener('upgrade', onUpgrade); + ws.on('close', () => done()); + }); + }); + + it('emits an error if the redirect URL is invalid (1/2)', (done) => { + server.once('upgrade', (req, socket) => { + socket.end('HTTP/1.1 302 Found\r\nLocation: ws://\r\n\r\n'); + }); + + const ws = new WebSocket(`ws://localhost:${server.address().port}`, { + followRedirects: true + }); + + ws.on('open', () => done(new Error("Unexpected 'open' event"))); + ws.on('error', (err) => { + assert.ok(err instanceof SyntaxError); + assert.strictEqual(err.message, 'Invalid URL: ws://'); + assert.strictEqual(ws._redirects, 1); + + ws.on('close', () => done()); + }); + }); + + it('emits an error if the redirect URL is invalid (2/2)', (done) => { + server.once('upgrade', (req, socket) => { + socket.end('HTTP/1.1 302 Found\r\nLocation: http://localhost\r\n\r\n'); + }); + + const ws = new WebSocket(`ws://localhost:${server.address().port}`, { + followRedirects: true + }); + + ws.on('open', () => done(new Error("Unexpected 'open' event"))); + ws.on('error', (err) => { + assert.ok(err instanceof SyntaxError); + assert.strictEqual( + err.message, + 'The URL\'s protocol must be one of "ws:", "wss:", or "ws+unix:"' + ); + assert.strictEqual(ws._redirects, 1); + + ws.on('close', () => done()); + }); + }); + + it('uses the first url userinfo when following redirects', (done) => { + const wss = new WebSocket.Server({ noServer: true, path: '/foo' }); + const authorization = 'Basic Zm9vOmJhcg=='; + + server.once('upgrade', (req, socket) => { + socket.end( + 'HTTP/1.1 302 Found\r\n' + + `Location: ws://baz:qux@localhost:${port}/foo\r\n\r\n` + ); + server.once('upgrade', (req, socket, head) => { + wss.handleUpgrade(req, socket, head, (ws, req) => { + assert.strictEqual(req.headers.authorization, authorization); + ws.close(); + }); + }); + }); + + const port = server.address().port; + const ws = new WebSocket(`ws://foo:bar@localhost:${port}`, { + followRedirects: true + }); + + assert.strictEqual(ws._req.getHeader('Authorization'), authorization); + + ws.on('close', (code) => { + assert.strictEqual(code, 1005); + assert.strictEqual(ws.url, `ws://baz:qux@localhost:${port}/foo`); + assert.strictEqual(ws._redirects, 1); + + wss.close(done); + }); + }); + + describe('When moving away from a secure context', () => { + function proxy(httpServer, httpsServer) { + const server = net.createServer({ allowHalfOpen: true }); + + server.on('connection', (socket) => { + socket.on('readable', function read() { + socket.removeListener('readable', read); + + const buf = socket.read(1); + const target = buf[0] === 22 ? httpsServer : httpServer; + + socket.unshift(buf); + target.emit('connection', socket); + }); + }); + + return server; + } + + describe("If there is no 'redirect' event listener", () => { + it('drops the `auth` option', (done) => { + const httpServer = http.createServer(); + const httpsServer = https.createServer({ + cert: fs.readFileSync('test/fixtures/certificate.pem'), + key: fs.readFileSync('test/fixtures/key.pem') + }); + const server = proxy(httpServer, httpsServer); + + server.listen(() => { + const port = server.address().port; + + httpsServer.on('upgrade', (req, socket) => { + socket.on('error', NOOP); + socket.end( + 'HTTP/1.1 302 Found\r\n' + + `Location: ws://localhost:${port}/\r\n\r\n` + ); + }); + + const wss = new WebSocket.Server({ server: httpServer }); + + wss.on('connection', (ws, req) => { + assert.strictEqual(req.headers.authorization, undefined); + ws.close(); + }); + + const ws = new WebSocket(`wss://localhost:${port}`, { + auth: 'foo:bar', + followRedirects: true, + rejectUnauthorized: false + }); + + assert.strictEqual( + ws._req.getHeader('Authorization'), + 'Basic Zm9vOmJhcg==' + ); + + ws.on('close', (code) => { + assert.strictEqual(code, 1005); + assert.strictEqual(ws.url, `ws://localhost:${port}/`); + assert.strictEqual(ws._redirects, 1); + + server.close(done); + }); + }); + }); + + it('drops the Authorization and Cookie headers', (done) => { + const httpServer = http.createServer(); + const httpsServer = https.createServer({ + cert: fs.readFileSync('test/fixtures/certificate.pem'), + key: fs.readFileSync('test/fixtures/key.pem') + }); + const server = proxy(httpServer, httpsServer); + + server.listen(() => { + const port = server.address().port; + + httpsServer.on('upgrade', (req, socket) => { + socket.on('error', NOOP); + socket.end( + 'HTTP/1.1 302 Found\r\n' + + `Location: ws://localhost:${port}/\r\n\r\n` + ); + }); + + const headers = { + authorization: 'Basic Zm9vOmJhcg==', + cookie: 'foo=bar', + host: 'foo' + }; + + const wss = new WebSocket.Server({ server: httpServer }); + + wss.on('connection', (ws, req) => { + assert.strictEqual(req.headers.authorization, undefined); + assert.strictEqual(req.headers.cookie, undefined); + assert.strictEqual(req.headers.host, headers.host); + + ws.close(); + }); + + const ws = new WebSocket(`wss://localhost:${port}`, { + followRedirects: true, + headers, + rejectUnauthorized: false + }); + + const firstRequest = ws._req; + + assert.strictEqual( + firstRequest.getHeader('Authorization'), + headers.authorization + ); + assert.strictEqual( + firstRequest.getHeader('Cookie'), + headers.cookie + ); + assert.strictEqual(firstRequest.getHeader('Host'), headers.host); + + ws.on('close', (code) => { + assert.strictEqual(code, 1005); + assert.strictEqual(ws.url, `ws://localhost:${port}/`); + assert.strictEqual(ws._redirects, 1); + + server.close(done); + }); + }); + }); + }); + + describe("If there is at least one 'redirect' event listener", () => { + it('does not drop any headers by default', (done) => { + const httpServer = http.createServer(); + const httpsServer = https.createServer({ + cert: fs.readFileSync('test/fixtures/certificate.pem'), + key: fs.readFileSync('test/fixtures/key.pem') + }); + const server = proxy(httpServer, httpsServer); + + server.listen(() => { + const port = server.address().port; + + httpsServer.on('upgrade', (req, socket) => { + socket.on('error', NOOP); + socket.end( + 'HTTP/1.1 302 Found\r\n' + + `Location: ws://localhost:${port}/\r\n\r\n` + ); + }); + + const headers = { + authorization: 'Basic Zm9vOmJhcg==', + cookie: 'foo=bar', + host: 'foo' + }; + + const wss = new WebSocket.Server({ server: httpServer }); + + wss.on('connection', (ws, req) => { + assert.strictEqual( + req.headers.authorization, + headers.authorization + ); + assert.strictEqual(req.headers.cookie, headers.cookie); + assert.strictEqual(req.headers.host, headers.host); + + ws.close(); + }); + + const ws = new WebSocket(`wss://localhost:${port}`, { + followRedirects: true, + headers, + rejectUnauthorized: false + }); + + const firstRequest = ws._req; + + assert.strictEqual( + firstRequest.getHeader('Authorization'), + headers.authorization + ); + assert.strictEqual( + firstRequest.getHeader('Cookie'), + headers.cookie + ); + assert.strictEqual(firstRequest.getHeader('Host'), headers.host); + + ws.on('redirect', (url, req) => { + assert.strictEqual(ws._redirects, 1); + assert.strictEqual(url, `ws://localhost:${port}/`); + assert.notStrictEqual(firstRequest, req); + assert.strictEqual( + req.getHeader('Authorization'), + headers.authorization + ); + assert.strictEqual(req.getHeader('Cookie'), headers.cookie); + assert.strictEqual(req.getHeader('Host'), headers.host); + + ws.on('close', (code) => { + assert.strictEqual(code, 1005); + server.close(done); + }); + }); + }); + }); + }); + }); + + describe('When the redirect host is different', () => { + describe("If there is no 'redirect' event listener", () => { + it('drops the `auth` option', (done) => { + const wss = new WebSocket.Server({ port: 0 }, () => { + const port = wss.address().port; + + server.once('upgrade', (req, socket) => { + socket.end( + 'HTTP/1.1 302 Found\r\n' + + `Location: ws://localhost:${port}/\r\n\r\n` + ); + }); + + const ws = new WebSocket( + `ws://localhost:${server.address().port}`, + { + auth: 'foo:bar', + followRedirects: true + } + ); + + assert.strictEqual( + ws._req.getHeader('Authorization'), + 'Basic Zm9vOmJhcg==' + ); + + ws.on('close', (code) => { + assert.strictEqual(code, 1005); + assert.strictEqual(ws.url, `ws://localhost:${port}/`); + assert.strictEqual(ws._redirects, 1); + + wss.close(done); + }); + }); + + wss.on('connection', (ws, req) => { + assert.strictEqual(req.headers.authorization, undefined); + ws.close(); + }); + }); + + it('drops the Authorization, Cookie and Host headers (1/4)', (done) => { + // Test the `ws:` to `ws:` case. + + const wss = new WebSocket.Server({ port: 0 }, () => { + const port = wss.address().port; + + server.once('upgrade', (req, socket) => { + socket.end( + 'HTTP/1.1 302 Found\r\n' + + `Location: ws://localhost:${port}/\r\n\r\n` + ); + }); + + const headers = { + authorization: 'Basic Zm9vOmJhcg==', + cookie: 'foo=bar', + host: 'foo' + }; + + const ws = new WebSocket( + `ws://localhost:${server.address().port}`, + { followRedirects: true, headers } + ); + + const firstRequest = ws._req; + + assert.strictEqual( + firstRequest.getHeader('Authorization'), + headers.authorization + ); + assert.strictEqual( + firstRequest.getHeader('Cookie'), + headers.cookie + ); + assert.strictEqual(firstRequest.getHeader('Host'), headers.host); + + ws.on('close', (code) => { + assert.strictEqual(code, 1005); + assert.strictEqual(ws.url, `ws://localhost:${port}/`); + assert.strictEqual(ws._redirects, 1); + + wss.close(done); + }); + }); + + wss.on('connection', (ws, req) => { + assert.strictEqual(req.headers.authorization, undefined); + assert.strictEqual(req.headers.cookie, undefined); + assert.strictEqual( + req.headers.host, + `localhost:${wss.address().port}` + ); + + ws.close(); + }); + }); + + it('drops the Authorization, Cookie and Host headers (2/4)', function (done) { + if (process.platform === 'win32') return this.skip(); + + // Test the `ws:` to `ws+unix:` case. + + const socketPath = path.join( + os.tmpdir(), + `ws.${crypto.randomBytes(16).toString('hex')}.sock` + ); + + server.once('upgrade', (req, socket) => { + socket.end( + `HTTP/1.1 302 Found\r\nLocation: ws+unix://${socketPath}\r\n\r\n` + ); + }); + + const redirectedServer = http.createServer(); + const wss = new WebSocket.Server({ server: redirectedServer }); + + wss.on('connection', (ws, req) => { + assert.strictEqual(req.headers.authorization, undefined); + assert.strictEqual(req.headers.cookie, undefined); + assert.strictEqual(req.headers.host, 'localhost'); + + ws.close(); + }); + + redirectedServer.listen(socketPath, () => { + const headers = { + authorization: 'Basic Zm9vOmJhcg==', + cookie: 'foo=bar', + host: 'foo' + }; + + const ws = new WebSocket( + `ws://localhost:${server.address().port}`, + { followRedirects: true, headers } + ); + + const firstRequest = ws._req; + + assert.strictEqual( + firstRequest.getHeader('Authorization'), + headers.authorization + ); + assert.strictEqual( + firstRequest.getHeader('Cookie'), + headers.cookie + ); + assert.strictEqual(firstRequest.getHeader('Host'), headers.host); + + ws.on('close', (code) => { + assert.strictEqual(code, 1005); + assert.strictEqual(ws.url, `ws+unix://${socketPath}`); + assert.strictEqual(ws._redirects, 1); + + redirectedServer.close(done); + }); + }); + }); + + it('drops the Authorization, Cookie and Host headers (3/4)', function (done) { + if (process.platform === 'win32') return this.skip(); + + // Test the `ws+unix:` to `ws+unix:` case. + + const redirectingServerSocketPath = path.join( + os.tmpdir(), + `ws.${crypto.randomBytes(16).toString('hex')}.sock` + ); + const redirectedServerSocketPath = path.join( + os.tmpdir(), + `ws.${crypto.randomBytes(16).toString('hex')}.sock` + ); + + const redirectingServer = http.createServer(); + + redirectingServer.on('upgrade', (req, socket) => { + socket.end( + 'HTTP/1.1 302 Found\r\n' + + `Location: ws+unix://${redirectedServerSocketPath}\r\n\r\n` + ); + }); + + const redirectedServer = http.createServer(); + const wss = new WebSocket.Server({ server: redirectedServer }); + + wss.on('connection', (ws, req) => { + assert.strictEqual(req.headers.authorization, undefined); + assert.strictEqual(req.headers.cookie, undefined); + assert.strictEqual(req.headers.host, 'localhost'); + + ws.close(); + }); + + redirectingServer.listen(redirectingServerSocketPath, listening); + redirectedServer.listen(redirectedServerSocketPath, listening); + + let callCount = 0; + + function listening() { + if (++callCount !== 2) return; + + const headers = { + authorization: 'Basic Zm9vOmJhcg==', + cookie: 'foo=bar', + host: 'foo' + }; + + const ws = new WebSocket( + `ws+unix://${redirectingServerSocketPath}`, + { followRedirects: true, headers } + ); + + const firstRequest = ws._req; + + assert.strictEqual( + firstRequest.getHeader('Authorization'), + headers.authorization + ); + assert.strictEqual( + firstRequest.getHeader('Cookie'), + headers.cookie + ); + assert.strictEqual(firstRequest.getHeader('Host'), headers.host); + + ws.on('close', (code) => { + assert.strictEqual(code, 1005); + assert.strictEqual( + ws.url, + `ws+unix://${redirectedServerSocketPath}` + ); + assert.strictEqual(ws._redirects, 1); + + redirectingServer.close(); + redirectedServer.close(done); + }); + } + }); + + it('drops the Authorization, Cookie and Host headers (4/4)', function (done) { + if (process.platform === 'win32') return this.skip(); + + // Test the `ws+unix:` to `ws:` case. + + const redirectingServer = http.createServer(); + const redirectedServer = http.createServer(); + const wss = new WebSocket.Server({ server: redirectedServer }); + + wss.on('connection', (ws, req) => { + assert.strictEqual(req.headers.authorization, undefined); + assert.strictEqual(req.headers.cookie, undefined); + assert.strictEqual( + req.headers.host, + `localhost:${redirectedServer.address().port}` + ); + + ws.close(); + }); + + const socketPath = path.join( + os.tmpdir(), + `ws.${crypto.randomBytes(16).toString('hex')}.sock` + ); + + redirectingServer.listen(socketPath, listening); + redirectedServer.listen(0, listening); + + let callCount = 0; + + function listening() { + if (++callCount !== 2) return; + + const port = redirectedServer.address().port; + + redirectingServer.on('upgrade', (req, socket) => { + socket.end( + `HTTP/1.1 302 Found\r\nLocation: ws://localhost:${port}\r\n\r\n` + ); + }); + + const headers = { + authorization: 'Basic Zm9vOmJhcg==', + cookie: 'foo=bar', + host: 'foo' + }; + + const ws = new WebSocket(`ws+unix://${socketPath}`, { + followRedirects: true, + headers + }); + + const firstRequest = ws._req; + + assert.strictEqual( + firstRequest.getHeader('Authorization'), + headers.authorization + ); + assert.strictEqual( + firstRequest.getHeader('Cookie'), + headers.cookie + ); + assert.strictEqual(firstRequest.getHeader('Host'), headers.host); + + ws.on('close', (code) => { + assert.strictEqual(code, 1005); + assert.strictEqual(ws.url, `ws://localhost:${port}/`); + assert.strictEqual(ws._redirects, 1); + + redirectingServer.close(); + redirectedServer.close(done); + }); + } + }); + }); + + describe("If there is at least one 'redirect' event listener", () => { + it('does not drop any headers by default', (done) => { + const headers = { + authorization: 'Basic Zm9vOmJhcg==', + cookie: 'foo=bar', + host: 'foo' + }; + + const wss = new WebSocket.Server({ port: 0 }, () => { + const port = wss.address().port; + + server.once('upgrade', (req, socket) => { + socket.end( + 'HTTP/1.1 302 Found\r\n' + + `Location: ws://localhost:${port}/\r\n\r\n` + ); + }); + + const ws = new WebSocket( + `ws://localhost:${server.address().port}`, + { followRedirects: true, headers } + ); + + const firstRequest = ws._req; + + assert.strictEqual( + firstRequest.getHeader('Authorization'), + headers.authorization + ); + assert.strictEqual( + firstRequest.getHeader('Cookie'), + headers.cookie + ); + assert.strictEqual(firstRequest.getHeader('Host'), headers.host); + + ws.on('redirect', (url, req) => { + assert.strictEqual(ws._redirects, 1); + assert.strictEqual(url, `ws://localhost:${port}/`); + assert.notStrictEqual(firstRequest, req); + assert.strictEqual( + req.getHeader('Authorization'), + headers.authorization + ); + assert.strictEqual(req.getHeader('Cookie'), headers.cookie); + assert.strictEqual(req.getHeader('Host'), headers.host); + + ws.on('close', (code) => { + assert.strictEqual(code, 1005); + wss.close(done); + }); + }); + }); + + wss.on('connection', (ws, req) => { + assert.strictEqual( + req.headers.authorization, + headers.authorization + ); + assert.strictEqual(req.headers.cookie, headers.cookie); + assert.strictEqual(req.headers.host, headers.host); + ws.close(); + }); + }); + }); + }); + + describe("In a listener of the 'redirect' event", () => { + it('allows to abort the request without swallowing errors', (done) => { + server.once('upgrade', (req, socket) => { + socket.end('HTTP/1.1 302 Found\r\nLocation: /foo\r\n\r\n'); + }); + + const port = server.address().port; + const ws = new WebSocket(`ws://localhost:${port}`, { + followRedirects: true + }); + + ws.on('redirect', (url, req) => { + assert.strictEqual(ws._redirects, 1); + assert.strictEqual(url, `ws://localhost:${port}/foo`); + + req.on('socket', () => { + req.abort(); + }); + + ws.on('error', (err) => { + assert.ok(err instanceof Error); + assert.strictEqual(err.message, 'socket hang up'); + + ws.on('close', (code) => { + assert.strictEqual(code, 1006); + done(); + }); + }); + }); + }); + + it('allows to remove headers', (done) => { + const wss = new WebSocket.Server({ port: 0 }, () => { + const port = wss.address().port; + + server.once('upgrade', (req, socket) => { + socket.end( + 'HTTP/1.1 302 Found\r\n' + + `Location: ws://localhost:${port}/\r\n\r\n` + ); + }); + + const headers = { + authorization: 'Basic Zm9vOmJhcg==', + cookie: 'foo=bar' + }; + + const ws = new WebSocket(`ws://localhost:${server.address().port}`, { + followRedirects: true, + headers + }); + + ws.on('redirect', (url, req) => { + assert.strictEqual(ws._redirects, 1); + assert.strictEqual(url, `ws://localhost:${port}/`); + assert.strictEqual( + req.getHeader('Authorization'), + headers.authorization + ); + assert.strictEqual(req.getHeader('Cookie'), headers.cookie); + + req.removeHeader('authorization'); + req.removeHeader('cookie'); + + ws.on('close', (code) => { + assert.strictEqual(code, 1005); + wss.close(done); + }); + }); + }); + + wss.on('connection', (ws, req) => { + assert.strictEqual(req.headers.authorization, undefined); + assert.strictEqual(req.headers.cookie, undefined); + ws.close(); + }); + }); + }); + }); + + describe('Connection with query string', () => { + it('connects when pathname is not null', (done) => { + const wss = new WebSocket.Server({ port: 0 }, () => { + const port = wss.address().port; + const ws = new WebSocket(`ws://localhost:${port}/?token=qwerty`); + + ws.on('open', () => { + wss.close(done); + }); + }); + + wss.on('connection', (ws) => { + ws.close(); + }); + }); + + it('connects when pathname is null', (done) => { + const wss = new WebSocket.Server({ port: 0 }, () => { + const port = wss.address().port; + const ws = new WebSocket(`ws://localhost:${port}?token=qwerty`); + + ws.on('open', () => { + wss.close(done); + }); + }); + + wss.on('connection', (ws) => { + ws.close(); + }); + }); + }); + + describe('#pause', () => { + it('does nothing if `readyState` is `CONNECTING` or `CLOSED`', (done) => { + const wss = new WebSocket.Server({ port: 0 }, () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + + assert.strictEqual(ws.readyState, WebSocket.CONNECTING); + assert.ok(!ws.isPaused); + + ws.pause(); + assert.ok(!ws.isPaused); + + ws.on('open', () => { + ws.on('close', () => { + assert.strictEqual(ws.readyState, WebSocket.CLOSED); + + ws.pause(); + assert.ok(!ws.isPaused); + + wss.close(done); + }); + + ws.close(); + }); + }); + }); + + it('pauses the socket', (done) => { + const wss = new WebSocket.Server({ port: 0 }, () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + }); + + wss.on('connection', (ws) => { + assert.ok(!ws.isPaused); + assert.ok(!ws._socket.isPaused()); + + ws.pause(); + assert.ok(ws.isPaused); + assert.ok(ws._socket.isPaused()); + + ws.terminate(); + wss.close(done); + }); + }); + }); + + describe('#ping', () => { + it('throws an error if `readyState` is `CONNECTING`', () => { + const ws = new WebSocket('ws://localhost', { + lookup() {} + }); + + assert.throws( + () => ws.ping(), + /^Error: WebSocket is not open: readyState 0 \(CONNECTING\)$/ + ); + + assert.throws( + () => ws.ping(NOOP), + /^Error: WebSocket is not open: readyState 0 \(CONNECTING\)$/ + ); + }); + + it('increases `bufferedAmount` if `readyState` is 2 or 3', (done) => { + const ws = new WebSocket('ws://localhost', { + lookup() {} + }); + + ws.on('error', (err) => { + assert.ok(err instanceof Error); + assert.strictEqual( + err.message, + 'WebSocket was closed before the connection was established' + ); + + assert.strictEqual(ws.readyState, WebSocket.CLOSING); + assert.strictEqual(ws.bufferedAmount, 0); + + ws.ping('hi'); + assert.strictEqual(ws.bufferedAmount, 2); + + ws.ping(); + assert.strictEqual(ws.bufferedAmount, 2); + + ws.on('close', () => { + assert.strictEqual(ws.readyState, WebSocket.CLOSED); + + ws.ping('hi'); + assert.strictEqual(ws.bufferedAmount, 4); + + ws.ping(); + assert.strictEqual(ws.bufferedAmount, 4); + + done(); + }); + }); + + ws.close(); + }); + + it('calls the callback w/ an error if `readyState` is 2 or 3', (done) => { + const wss = new WebSocket.Server({ port: 0 }, () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + }); + + wss.on('connection', (ws) => { + ws.close(); + + assert.strictEqual(ws.bufferedAmount, 0); + + ws.ping('hi', (err) => { + assert.ok(err instanceof Error); + assert.strictEqual( + err.message, + 'WebSocket is not open: readyState 2 (CLOSING)' + ); + assert.strictEqual(ws.bufferedAmount, 2); + + ws.on('close', () => { + ws.ping((err) => { + assert.ok(err instanceof Error); + assert.strictEqual( + err.message, + 'WebSocket is not open: readyState 3 (CLOSED)' + ); + assert.strictEqual(ws.bufferedAmount, 2); + + wss.close(done); + }); + }); + }); + }); + }); + + it('can send a ping with no data', (done) => { + const wss = new WebSocket.Server({ port: 0 }, () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + + ws.on('open', () => { + ws.ping(() => { + ws.ping(); + ws.close(); + }); + }); + }); + + wss.on('connection', (ws) => { + let pings = 0; + ws.on('ping', (data) => { + assert.ok(Buffer.isBuffer(data)); + assert.strictEqual(data.length, 0); + if (++pings === 2) wss.close(done); + }); + }); + }); + + it('can send a ping with data', (done) => { + const wss = new WebSocket.Server({ port: 0 }, () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + + ws.on('open', () => { + ws.ping('hi', () => { + ws.ping('hi', true); + ws.close(); + }); + }); + }); + + wss.on('connection', (ws) => { + let pings = 0; + ws.on('ping', (message) => { + assert.strictEqual(message.toString(), 'hi'); + if (++pings === 2) wss.close(done); + }); + }); + }); + + it('can send numbers as ping payload', (done) => { + const wss = new WebSocket.Server({ port: 0 }, () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + + ws.on('open', () => { + ws.ping(0); + ws.close(); + }); + }); + + wss.on('connection', (ws) => { + ws.on('ping', (message) => { + assert.strictEqual(message.toString(), '0'); + wss.close(done); + }); + }); + }); + + it('throws an error if the data size is greater than 125 bytes', (done) => { + const wss = new WebSocket.Server({ port: 0 }, () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + + ws.on('open', () => { + assert.throws( + () => ws.ping(Buffer.alloc(126)), + /^RangeError: The data size must not be greater than 125 bytes$/ + ); + + wss.close(done); + }); + }); + + wss.on('connection', (ws) => { + ws.close(); + }); + }); + }); + + describe('#pong', () => { + it('throws an error if `readyState` is `CONNECTING`', () => { + const ws = new WebSocket('ws://localhost', { + lookup() {} + }); + + assert.throws( + () => ws.pong(), + /^Error: WebSocket is not open: readyState 0 \(CONNECTING\)$/ + ); + + assert.throws( + () => ws.pong(NOOP), + /^Error: WebSocket is not open: readyState 0 \(CONNECTING\)$/ + ); + }); + + it('increases `bufferedAmount` if `readyState` is 2 or 3', (done) => { + const ws = new WebSocket('ws://localhost', { + lookup() {} + }); + + ws.on('error', (err) => { + assert.ok(err instanceof Error); + assert.strictEqual( + err.message, + 'WebSocket was closed before the connection was established' + ); + + assert.strictEqual(ws.readyState, WebSocket.CLOSING); + assert.strictEqual(ws.bufferedAmount, 0); + + ws.pong('hi'); + assert.strictEqual(ws.bufferedAmount, 2); + + ws.pong(); + assert.strictEqual(ws.bufferedAmount, 2); + + ws.on('close', () => { + assert.strictEqual(ws.readyState, WebSocket.CLOSED); + + ws.pong('hi'); + assert.strictEqual(ws.bufferedAmount, 4); + + ws.pong(); + assert.strictEqual(ws.bufferedAmount, 4); + + done(); + }); + }); + + ws.close(); + }); + + it('calls the callback w/ an error if `readyState` is 2 or 3', (done) => { + const wss = new WebSocket.Server({ port: 0 }, () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + }); + + wss.on('connection', (ws) => { + ws.close(); + + assert.strictEqual(ws.bufferedAmount, 0); + + ws.pong('hi', (err) => { + assert.ok(err instanceof Error); + assert.strictEqual( + err.message, + 'WebSocket is not open: readyState 2 (CLOSING)' + ); + assert.strictEqual(ws.bufferedAmount, 2); + + ws.on('close', () => { + ws.pong((err) => { + assert.ok(err instanceof Error); + assert.strictEqual( + err.message, + 'WebSocket is not open: readyState 3 (CLOSED)' + ); + assert.strictEqual(ws.bufferedAmount, 2); + + wss.close(done); + }); + }); + }); + }); + }); + + it('can send a pong with no data', (done) => { + const wss = new WebSocket.Server({ port: 0 }, () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + + ws.on('open', () => { + ws.pong(() => { + ws.pong(); + ws.close(); + }); + }); + }); + + wss.on('connection', (ws) => { + let pongs = 0; + ws.on('pong', (data) => { + assert.ok(Buffer.isBuffer(data)); + assert.strictEqual(data.length, 0); + if (++pongs === 2) wss.close(done); + }); + }); + }); + + it('can send a pong with data', (done) => { + const wss = new WebSocket.Server({ port: 0 }, () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + + ws.on('open', () => { + ws.pong('hi', () => { + ws.pong('hi', true); + ws.close(); + }); + }); + }); + + wss.on('connection', (ws) => { + let pongs = 0; + ws.on('pong', (message) => { + assert.strictEqual(message.toString(), 'hi'); + if (++pongs === 2) wss.close(done); + }); + }); + }); + + it('can send numbers as pong payload', (done) => { + const wss = new WebSocket.Server({ port: 0 }, () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + + ws.on('open', () => { + ws.pong(0); + ws.close(); + }); + }); + + wss.on('connection', (ws) => { + ws.on('pong', (message) => { + assert.strictEqual(message.toString(), '0'); + wss.close(done); + }); + }); + }); + + it('throws an error if the data size is greater than 125 bytes', (done) => { + const wss = new WebSocket.Server({ port: 0 }, () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + + ws.on('open', () => { + assert.throws( + () => ws.pong(Buffer.alloc(126)), + /^RangeError: The data size must not be greater than 125 bytes$/ + ); + + wss.close(done); + }); + }); + + wss.on('connection', (ws) => { + ws.close(); + }); + }); + }); + + describe('#resume', () => { + it('does nothing if `readyState` is `CONNECTING` or `CLOSED`', (done) => { + const wss = new WebSocket.Server({ port: 0 }, () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + + assert.strictEqual(ws.readyState, WebSocket.CONNECTING); + assert.ok(!ws.isPaused); + + // Verify that no exception is thrown. + ws.resume(); + + ws.on('open', () => { + ws.pause(); + assert.ok(ws.isPaused); + + ws.on('close', () => { + assert.strictEqual(ws.readyState, WebSocket.CLOSED); + + ws.resume(); + assert.ok(ws.isPaused); + + wss.close(done); + }); + + ws.terminate(); + }); + }); + }); + + it('resumes the socket', (done) => { + const wss = new WebSocket.Server({ port: 0 }, () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + }); + + wss.on('connection', (ws) => { + assert.ok(!ws.isPaused); + assert.ok(!ws._socket.isPaused()); + + ws.pause(); + assert.ok(ws.isPaused); + assert.ok(ws._socket.isPaused()); + + ws.resume(); + assert.ok(!ws.isPaused); + assert.ok(!ws._socket.isPaused()); + + ws.close(); + wss.close(done); + }); + }); + }); + + describe('#send', () => { + it('throws an error if `readyState` is `CONNECTING`', () => { + const ws = new WebSocket('ws://localhost', { + lookup() {} + }); + + assert.throws( + () => ws.send('hi'), + /^Error: WebSocket is not open: readyState 0 \(CONNECTING\)$/ + ); + + assert.throws( + () => ws.send('hi', NOOP), + /^Error: WebSocket is not open: readyState 0 \(CONNECTING\)$/ + ); + }); + + it('increases `bufferedAmount` if `readyState` is 2 or 3', (done) => { + const ws = new WebSocket('ws://localhost', { + lookup() {} + }); + + ws.on('error', (err) => { + assert.ok(err instanceof Error); + assert.strictEqual( + err.message, + 'WebSocket was closed before the connection was established' + ); + + assert.strictEqual(ws.readyState, WebSocket.CLOSING); + assert.strictEqual(ws.bufferedAmount, 0); + + ws.send('hi'); + assert.strictEqual(ws.bufferedAmount, 2); + + ws.send(); + assert.strictEqual(ws.bufferedAmount, 2); + + ws.on('close', () => { + assert.strictEqual(ws.readyState, WebSocket.CLOSED); + + ws.send('hi'); + assert.strictEqual(ws.bufferedAmount, 4); + + ws.send(); + assert.strictEqual(ws.bufferedAmount, 4); + + done(); + }); + }); + + ws.close(); + }); + + it('calls the callback w/ an error if `readyState` is 2 or 3', (done) => { + const wss = new WebSocket.Server({ port: 0 }, () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + }); + + wss.on('connection', (ws) => { + ws.close(); + + assert.strictEqual(ws.bufferedAmount, 0); + + ws.send('hi', (err) => { + assert.ok(err instanceof Error); + assert.strictEqual( + err.message, + 'WebSocket is not open: readyState 2 (CLOSING)' + ); + assert.strictEqual(ws.bufferedAmount, 2); + + ws.on('close', () => { + ws.send('hi', (err) => { + assert.ok(err instanceof Error); + assert.strictEqual( + err.message, + 'WebSocket is not open: readyState 3 (CLOSED)' + ); + assert.strictEqual(ws.bufferedAmount, 4); + + wss.close(done); + }); + }); + }); + }); + }); + + it('can send a big binary message', (done) => { + const wss = new WebSocket.Server({ port: 0 }, () => { + const array = new Float32Array(5 * 1024 * 1024); + + for (let i = 0; i < array.length; i++) { + array[i] = i / 5; + } + + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + + ws.on('open', () => ws.send(array)); + ws.on('message', (msg, isBinary) => { + assert.deepStrictEqual(msg, Buffer.from(array.buffer)); + assert.ok(isBinary); + wss.close(done); + }); + }); + + wss.on('connection', (ws) => { + ws.on('message', (msg, isBinary) => { + assert.ok(isBinary); + ws.send(msg); + ws.close(); + }); + }); + }); + + it('can send text data', (done) => { + const wss = new WebSocket.Server({ port: 0 }, () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + + ws.on('open', () => ws.send('hi')); + ws.on('message', (message, isBinary) => { + assert.deepStrictEqual(message, Buffer.from('hi')); + assert.ok(!isBinary); + wss.close(done); + }); + }); + + wss.on('connection', (ws) => { + ws.on('message', (msg, isBinary) => { + ws.send(msg, { binary: isBinary }); + ws.close(); + }); + }); + }); + + it('does not override the `fin` option', (done) => { + const wss = new WebSocket.Server({ port: 0 }, () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + + ws.on('open', () => { + ws.send('fragment', { fin: false }); + ws.send('fragment', { fin: true }); + ws.close(); + }); + }); + + wss.on('connection', (ws) => { + ws.on('message', (msg, isBinary) => { + assert.deepStrictEqual(msg, Buffer.from('fragmentfragment')); + assert.ok(!isBinary); + wss.close(done); + }); + }); + }); + + it('sends numbers as strings', (done) => { + const wss = new WebSocket.Server({ port: 0 }, () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + + ws.on('open', () => { + ws.send(0); + ws.close(); + }); + }); + + wss.on('connection', (ws) => { + ws.on('message', (msg, isBinary) => { + assert.deepStrictEqual(msg, Buffer.from('0')); + assert.ok(!isBinary); + wss.close(done); + }); + }); + }); + + it('can send a `TypedArray`', (done) => { + const wss = new WebSocket.Server({ port: 0 }, () => { + const array = new Float32Array(6); + + for (let i = 0; i < array.length; ++i) { + array[i] = i / 2; + } + + const partial = array.subarray(2, 5); + const buf = Buffer.from( + partial.buffer, + partial.byteOffset, + partial.byteLength + ); + + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + + ws.on('open', () => { + ws.send(partial); + ws.close(); + }); + + ws.on('message', (message, isBinary) => { + assert.deepStrictEqual(message, buf); + assert.ok(isBinary); + wss.close(done); + }); + }); + + wss.on('connection', (ws) => { + ws.on('message', (msg, isBinary) => { + assert.ok(isBinary); + ws.send(msg); + }); + }); + }); + + it('can send an `ArrayBuffer`', (done) => { + const wss = new WebSocket.Server({ port: 0 }, () => { + const array = new Float32Array(5); + + for (let i = 0; i < array.length; ++i) { + array[i] = i / 2; + } + + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + + ws.on('open', () => { + ws.send(array.buffer); + ws.close(); + }); + + ws.onmessage = (event) => { + assert.ok(event.data.equals(Buffer.from(array.buffer))); + wss.close(done); + }; + }); + + wss.on('connection', (ws) => { + ws.on('message', (msg, isBinary) => { + assert.ok(isBinary); + ws.send(msg); + }); + }); + }); + + it('can send a `Buffer`', (done) => { + const wss = new WebSocket.Server({ port: 0 }, () => { + const buf = Buffer.from('foobar'); + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + + ws.on('open', () => { + ws.send(buf); + ws.close(); + }); + + ws.onmessage = (event) => { + assert.deepStrictEqual(event.data, buf); + wss.close(done); + }; + }); + + wss.on('connection', (ws) => { + ws.on('message', (msg, isBinary) => { + assert.ok(isBinary); + ws.send(msg); + }); + }); + }); + + it('calls the callback when data is written out', (done) => { + const wss = new WebSocket.Server({ port: 0 }, () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + + ws.on('open', () => { + ws.send('hi', (err) => { + assert.ifError(err); + wss.close(done); + }); + }); + }); + + wss.on('connection', (ws) => { + ws.close(); + }); + }); + + it('works when the `data` argument is falsy', (done) => { + const wss = new WebSocket.Server({ port: 0 }, () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + + ws.on('open', () => { + ws.send(); + ws.close(); + }); + }); + + wss.on('connection', (ws) => { + ws.on('message', (message, isBinary) => { + assert.strictEqual(message, EMPTY_BUFFER); + assert.ok(isBinary); + wss.close(done); + }); + }); + }); + + it('honors the `mask` option', (done) => { + let clientCloseEventEmitted = false; + let serverClientCloseEventEmitted = false; + + const wss = new WebSocket.Server({ port: 0 }, () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + + ws.on('open', () => ws.send('hi', { mask: false })); + ws.on('close', (code, reason) => { + assert.strictEqual(code, 1002); + assert.deepStrictEqual(reason, EMPTY_BUFFER); + + clientCloseEventEmitted = true; + if (serverClientCloseEventEmitted) wss.close(done); + }); + }); + + wss.on('connection', (ws) => { + const chunks = []; + + ws._socket.prependListener('data', (chunk) => { + chunks.push(chunk); + }); + + ws.on('error', (err) => { + assert.ok(err instanceof RangeError); + assert.strictEqual( + err.message, + 'Invalid WebSocket frame: MASK must be set' + ); + assert.ok( + Buffer.concat(chunks).slice(0, 2).equals(Buffer.from('8102', 'hex')) + ); + + ws.on('close', (code, reason) => { + assert.strictEqual(code, 1006); + assert.strictEqual(reason, EMPTY_BUFFER); + + serverClientCloseEventEmitted = true; + if (clientCloseEventEmitted) wss.close(done); + }); + }); + }); + }); + }); + + describe('#close', () => { + it('closes the connection if called while connecting (1/3)', (done) => { + const wss = new WebSocket.Server({ port: 0 }, () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + + ws.on('open', () => done(new Error("Unexpected 'open' event"))); + ws.on('error', (err) => { + assert.ok(err instanceof Error); + assert.strictEqual( + err.message, + 'WebSocket was closed before the connection was established' + ); + ws.on('close', () => wss.close(done)); + }); + ws.close(1001); + }); + }); + + it('closes the connection if called while connecting (2/3)', (done) => { + const wss = new WebSocket.Server( + { + verifyClient: (info, cb) => setTimeout(cb, 300, true), + port: 0 + }, + () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + + ws.on('open', () => done(new Error("Unexpected 'open' event"))); + ws.on('error', (err) => { + assert.ok(err instanceof Error); + assert.strictEqual( + err.message, + 'WebSocket was closed before the connection was established' + ); + ws.on('close', () => wss.close(done)); + }); + setTimeout(() => ws.close(1001), 150); + } + ); + }); + + it('closes the connection if called while connecting (3/3)', (done) => { + const server = http.createServer(); + + server.listen(0, () => { + const ws = new WebSocket(`ws://localhost:${server.address().port}`); + + ws.on('open', () => done(new Error("Unexpected 'open' event"))); + ws.on('error', (err) => { + assert.ok(err instanceof Error); + assert.strictEqual( + err.message, + 'WebSocket was closed before the connection was established' + ); + ws.on('close', () => { + server.close(done); + }); + }); + + ws.on('unexpected-response', (req, res) => { + assert.strictEqual(res.statusCode, 502); + + const chunks = []; + + res.on('data', (chunk) => { + chunks.push(chunk); + }); + + res.on('end', () => { + assert.strictEqual(Buffer.concat(chunks).toString(), 'foo'); + ws.close(); + }); + }); + }); + + server.on('upgrade', (req, socket) => { + socket.on('end', socket.end); + + socket.write( + `HTTP/1.1 502 ${http.STATUS_CODES[502]}\r\n` + + 'Connection: keep-alive\r\n' + + 'Content-type: text/html\r\n' + + 'Content-Length: 3\r\n' + + '\r\n' + + 'foo' + ); + }); + }); + + it('can be called from an error listener while connecting', (done) => { + const ws = new WebSocket('ws://localhost:1337'); + + ws.on('open', () => done(new Error("Unexpected 'open' event"))); + ws.on('error', (err) => { + assert.ok(err instanceof Error); + assert.strictEqual(err.code, 'ECONNREFUSED'); + ws.close(); + ws.on('close', () => done()); + }); + }).timeout(4000); + + it("can be called from a listener of the 'redirect' event", (done) => { + const server = http.createServer(); + + server.once('upgrade', (req, socket) => { + socket.end('HTTP/1.1 302 Found\r\nLocation: /foo\r\n\r\n'); + }); + + server.listen(() => { + const port = server.address().port; + const ws = new WebSocket(`ws://localhost:${port}`, { + followRedirects: true + }); + + ws.on('open', () => { + done(new Error("Unexpected 'open' event")); + }); + + ws.on('error', (err) => { + assert.ok(err instanceof Error); + assert.strictEqual( + err.message, + 'WebSocket was closed before the connection was established' + ); + + ws.on('close', (code) => { + assert.strictEqual(code, 1006); + server.close(done); + }); + }); + + ws.on('redirect', () => { + ws.close(); + }); + }); + }); + + it("can be called from a listener of the 'upgrade' event", (done) => { + const wss = new WebSocket.Server({ port: 0 }, () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + + ws.on('open', () => done(new Error("Unexpected 'open' event"))); + ws.on('error', (err) => { + assert.ok(err instanceof Error); + assert.strictEqual( + err.message, + 'WebSocket was closed before the connection was established' + ); + ws.on('close', () => wss.close(done)); + }); + ws.on('upgrade', () => ws.close()); + }); + }); + + it('sends the close status code only when necessary', (done) => { + let sent; + const wss = new WebSocket.Server({ port: 0 }, () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + + ws.on('open', () => { + ws._socket.once('data', (data) => { + sent = data; + }); + }); + }); + + wss.on('connection', (ws) => { + ws._socket.once('data', (received) => { + assert.deepStrictEqual( + received.slice(0, 2), + Buffer.from([0x88, 0x80]) + ); + assert.deepStrictEqual(sent, Buffer.from([0x88, 0x00])); + + ws.on('close', (code, reason) => { + assert.strictEqual(code, 1005); + assert.strictEqual(reason, EMPTY_BUFFER); + wss.close(done); + }); + }); + ws.close(); + }); + }); + + it('works when close reason is not specified', (done) => { + const wss = new WebSocket.Server({ port: 0 }, () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + + ws.on('open', () => ws.close(1000)); + }); + + wss.on('connection', (ws) => { + ws.on('close', (code, message) => { + assert.strictEqual(code, 1000); + assert.deepStrictEqual(message, EMPTY_BUFFER); + wss.close(done); + }); + }); + }); + + it('works when close reason is specified', (done) => { + const wss = new WebSocket.Server({ port: 0 }, () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + + ws.on('open', () => ws.close(1000, 'some reason')); + }); + + wss.on('connection', (ws) => { + ws.on('close', (code, message) => { + assert.strictEqual(code, 1000); + assert.deepStrictEqual(message, Buffer.from('some reason')); + wss.close(done); + }); + }); + }); + + it('permits all buffered data to be delivered', (done) => { + const wss = new WebSocket.Server( + { + perMessageDeflate: { threshold: 0 }, + port: 0 + }, + () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + const messages = []; + + ws.on('message', (message, isBinary) => { + assert.ok(!isBinary); + messages.push(message.toString()); + }); + ws.on('close', (code) => { + assert.strictEqual(code, 1005); + assert.deepStrictEqual(messages, ['foo', 'bar', 'baz']); + wss.close(done); + }); + } + ); + + wss.on('connection', (ws) => { + const callback = (err) => assert.ifError(err); + + ws.send('foo', callback); + ws.send('bar', callback); + ws.send('baz', callback); + ws.close(); + ws.close(); + }); + }); + + it('allows close code 1013', (done) => { + const wss = new WebSocket.Server({ port: 0 }, () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + + ws.on('close', (code) => { + assert.strictEqual(code, 1013); + wss.close(done); + }); + }); + + wss.on('connection', (ws) => ws.close(1013)); + }); + + it('allows close code 1014', (done) => { + const wss = new WebSocket.Server({ port: 0 }, () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + + ws.on('close', (code) => { + assert.strictEqual(code, 1014); + wss.close(done); + }); + }); + + wss.on('connection', (ws) => ws.close(1014)); + }); + + it('does nothing if `readyState` is `CLOSED`', (done) => { + const wss = new WebSocket.Server({ port: 0 }, () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + + ws.on('close', (code) => { + assert.strictEqual(code, 1005); + assert.strictEqual(ws.readyState, WebSocket.CLOSED); + ws.close(); + wss.close(done); + }); + }); + + wss.on('connection', (ws) => ws.close()); + }); + + it('sets a timer for the closing handshake to complete', (done) => { + const wss = new WebSocket.Server({ port: 0 }, () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + + ws.on('close', (code, reason) => { + assert.strictEqual(code, 1000); + assert.deepStrictEqual(reason, Buffer.from('some reason')); + wss.close(done); + }); + + ws.on('open', () => { + let callbackCalled = false; + + assert.strictEqual(ws._closeTimer, null); + + ws.send('foo', () => { + callbackCalled = true; + }); + + ws.close(1000, 'some reason'); + + // + // Check that the close timer is set even if the `Sender.close()` + // callback is not called. + // + assert.strictEqual(callbackCalled, false); + assert.strictEqual(ws._closeTimer._idleTimeout, 30000); + }); + }); + }); + }); + + describe('#terminate', () => { + it('closes the connection if called while connecting (1/2)', (done) => { + const wss = new WebSocket.Server({ port: 0 }, () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + + ws.on('open', () => done(new Error("Unexpected 'open' event"))); + ws.on('error', (err) => { + assert.ok(err instanceof Error); + assert.strictEqual( + err.message, + 'WebSocket was closed before the connection was established' + ); + ws.on('close', () => wss.close(done)); + }); + ws.terminate(); + }); + }); + + it('closes the connection if called while connecting (2/2)', (done) => { + const wss = new WebSocket.Server( + { + verifyClient: (info, cb) => setTimeout(cb, 300, true), + port: 0 + }, + () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + + ws.on('open', () => done(new Error("Unexpected 'open' event"))); + ws.on('error', (err) => { + assert.ok(err instanceof Error); + assert.strictEqual( + err.message, + 'WebSocket was closed before the connection was established' + ); + ws.on('close', () => wss.close(done)); + }); + setTimeout(() => ws.terminate(), 150); + } + ); + }); + + it('can be called from an error listener while connecting', (done) => { + const ws = new WebSocket('ws://localhost:1337'); + + ws.on('open', () => done(new Error("Unexpected 'open' event"))); + ws.on('error', (err) => { + assert.ok(err instanceof Error); + assert.strictEqual(err.code, 'ECONNREFUSED'); + ws.terminate(); + ws.on('close', () => done()); + }); + }).timeout(4000); + + it("can be called from a listener of the 'redirect' event", (done) => { + const server = http.createServer(); + + server.once('upgrade', (req, socket) => { + socket.end('HTTP/1.1 302 Found\r\nLocation: /foo\r\n\r\n'); + }); + + server.listen(() => { + const port = server.address().port; + const ws = new WebSocket(`ws://localhost:${port}`, { + followRedirects: true + }); + + ws.on('open', () => { + done(new Error("Unexpected 'open' event")); + }); + + ws.on('error', (err) => { + assert.ok(err instanceof Error); + assert.strictEqual( + err.message, + 'WebSocket was closed before the connection was established' + ); + + ws.on('close', (code) => { + assert.strictEqual(code, 1006); + server.close(done); + }); + }); + + ws.on('redirect', () => { + ws.terminate(); + }); + }); + }); + + it("can be called from a listener of the 'upgrade' event", (done) => { + const wss = new WebSocket.Server({ port: 0 }, () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + + ws.on('open', () => done(new Error("Unexpected 'open' event"))); + ws.on('error', (err) => { + assert.ok(err instanceof Error); + assert.strictEqual( + err.message, + 'WebSocket was closed before the connection was established' + ); + ws.on('close', () => wss.close(done)); + }); + ws.on('upgrade', () => ws.terminate()); + }); + }); + + it('does nothing if `readyState` is `CLOSED`', (done) => { + const wss = new WebSocket.Server({ port: 0 }, () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + + ws.on('close', (code) => { + assert.strictEqual(code, 1006); + assert.strictEqual(ws.readyState, WebSocket.CLOSED); + ws.terminate(); + wss.close(done); + }); + }); + + wss.on('connection', (ws) => ws.terminate()); + }); + }); + + describe('WHATWG API emulation', () => { + it('supports the `on{close,error,message,open}` attributes', () => { + for (const property of ['onclose', 'onerror', 'onmessage', 'onopen']) { + const descriptor = Object.getOwnPropertyDescriptor( + WebSocket.prototype, + property + ); + + assert.strictEqual(descriptor.configurable, true); + assert.strictEqual(descriptor.enumerable, true); + assert.ok(descriptor.get !== undefined); + assert.ok(descriptor.set !== undefined); + } + + const ws = new WebSocket('ws://localhost', { agent: new CustomAgent() }); + + assert.strictEqual(ws.onmessage, null); + assert.strictEqual(ws.onclose, null); + assert.strictEqual(ws.onerror, null); + assert.strictEqual(ws.onopen, null); + + ws.onmessage = NOOP; + ws.onerror = NOOP; + ws.onclose = NOOP; + ws.onopen = NOOP; + + assert.strictEqual(ws.onmessage, NOOP); + assert.strictEqual(ws.onclose, NOOP); + assert.strictEqual(ws.onerror, NOOP); + assert.strictEqual(ws.onopen, NOOP); + + ws.onmessage = 'foo'; + + assert.strictEqual(ws.onmessage, null); + assert.strictEqual(ws.listenerCount('message'), 0); + }); + + it('works like the `EventEmitter` interface', (done) => { + const wss = new WebSocket.Server({ port: 0 }, () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + + ws.onmessage = (messageEvent) => { + assert.strictEqual(messageEvent.data, 'foo'); + ws.onclose = (closeEvent) => { + assert.strictEqual(closeEvent.wasClean, true); + assert.strictEqual(closeEvent.code, 1005); + assert.strictEqual(closeEvent.reason, ''); + wss.close(done); + }; + ws.close(); + }; + + ws.onopen = () => ws.send('foo'); + }); + + wss.on('connection', (ws) => { + ws.on('message', (msg, isBinary) => { + ws.send(msg, { binary: isBinary }); + }); + }); + }); + + it("doesn't return listeners added with `on`", () => { + const ws = new WebSocket('ws://localhost', { agent: new CustomAgent() }); + + ws.on('open', NOOP); + + assert.deepStrictEqual(ws.listeners('open'), [NOOP]); + assert.strictEqual(ws.onopen, null); + }); + + it("doesn't remove listeners added with `on`", () => { + const ws = new WebSocket('ws://localhost', { agent: new CustomAgent() }); + + ws.on('close', NOOP); + ws.onclose = NOOP; + + let listeners = ws.listeners('close'); + + assert.strictEqual(listeners.length, 2); + assert.strictEqual(listeners[0], NOOP); + assert.strictEqual(listeners[1][kListener], NOOP); + + ws.onclose = NOOP; + + listeners = ws.listeners('close'); + + assert.strictEqual(listeners.length, 2); + assert.strictEqual(listeners[0], NOOP); + assert.strictEqual(listeners[1][kListener], NOOP); + }); + + it('supports the `addEventListener` method', () => { + const events = []; + const ws = new WebSocket('ws://localhost', { agent: new CustomAgent() }); + + ws.addEventListener('foo', () => {}); + assert.strictEqual(ws.listenerCount('foo'), 0); + + ws.addEventListener('open', () => { + events.push('open'); + assert.strictEqual(ws.listenerCount('open'), 1); + }); + + assert.strictEqual(ws.listenerCount('open'), 1); + + ws.addEventListener( + 'message', + () => { + events.push('message'); + assert.strictEqual(ws.listenerCount('message'), 0); + }, + { once: true } + ); + + assert.strictEqual(ws.listenerCount('message'), 1); + + ws.emit('open'); + ws.emit('message', EMPTY_BUFFER, false); + + assert.deepStrictEqual(events, ['open', 'message']); + }); + + it("doesn't return listeners added with `addEventListener`", () => { + const ws = new WebSocket('ws://localhost', { agent: new CustomAgent() }); + + ws.addEventListener('open', NOOP); + + const listeners = ws.listeners('open'); + + assert.strictEqual(listeners.length, 1); + assert.strictEqual(listeners[0][kListener], NOOP); + + assert.strictEqual(ws.onopen, null); + }); + + it("doesn't remove listeners added with `addEventListener`", () => { + const ws = new WebSocket('ws://localhost', { agent: new CustomAgent() }); + + ws.addEventListener('close', NOOP); + ws.onclose = NOOP; + + let listeners = ws.listeners('close'); + + assert.strictEqual(listeners.length, 2); + assert.strictEqual(listeners[0][kListener], NOOP); + assert.strictEqual(listeners[1][kListener], NOOP); + + ws.onclose = NOOP; + + listeners = ws.listeners('close'); + + assert.strictEqual(listeners.length, 2); + assert.strictEqual(listeners[0][kListener], NOOP); + assert.strictEqual(listeners[1][kListener], NOOP); + }); + + it('supports the `removeEventListener` method', () => { + const ws = new WebSocket('ws://localhost', { agent: new CustomAgent() }); + + ws.addEventListener('message', NOOP); + ws.addEventListener('open', NOOP); + + assert.strictEqual(ws.listeners('message')[0][kListener], NOOP); + assert.strictEqual(ws.listeners('open')[0][kListener], NOOP); + + ws.removeEventListener('message', () => {}); + + assert.strictEqual(ws.listeners('message')[0][kListener], NOOP); + + ws.removeEventListener('message', NOOP); + ws.removeEventListener('open', NOOP); + + assert.strictEqual(ws.listenerCount('message'), 0); + assert.strictEqual(ws.listenerCount('open'), 0); + + ws.addEventListener('message', NOOP, { once: true }); + ws.addEventListener('open', NOOP, { once: true }); + + assert.strictEqual(ws.listeners('message')[0][kListener], NOOP); + assert.strictEqual(ws.listeners('open')[0][kListener], NOOP); + + ws.removeEventListener('message', () => {}); + + assert.strictEqual(ws.listeners('message')[0][kListener], NOOP); + + ws.removeEventListener('message', NOOP); + ws.removeEventListener('open', NOOP); + + assert.strictEqual(ws.listenerCount('message'), 0); + assert.strictEqual(ws.listenerCount('open'), 0); + + // Multiple listeners. + ws.addEventListener('message', NOOP); + ws.addEventListener('message', NOOP); + + assert.strictEqual(ws.listeners('message')[0][kListener], NOOP); + assert.strictEqual(ws.listeners('message')[1][kListener], NOOP); + + ws.removeEventListener('message', NOOP); + + assert.strictEqual(ws.listeners('message')[0][kListener], NOOP); + + ws.removeEventListener('message', NOOP); + + assert.strictEqual(ws.listenerCount('message'), 0); + + // Listeners not added with `websocket.addEventListener()`. + ws.on('message', NOOP); + + assert.deepStrictEqual(ws.listeners('message'), [NOOP]); + + ws.removeEventListener('message', NOOP); + + assert.deepStrictEqual(ws.listeners('message'), [NOOP]); + + ws.onclose = NOOP; + + assert.strictEqual(ws.listeners('close')[0][kListener], NOOP); + + ws.removeEventListener('close', NOOP); + + assert.strictEqual(ws.listeners('close')[0][kListener], NOOP); + }); + + it('wraps text data in a `MessageEvent`', (done) => { + const wss = new WebSocket.Server({ port: 0 }, () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + + ws.addEventListener('open', () => { + ws.send('hi'); + ws.close(); + }); + + ws.addEventListener('message', (event) => { + assert.ok(event instanceof MessageEvent); + assert.strictEqual(event.data, 'hi'); + wss.close(done); + }); + }); + + wss.on('connection', (ws) => { + ws.on('message', (msg, isBinary) => { + ws.send(msg, { binary: isBinary }); + }); + }); + }); + + it('receives a `CloseEvent` when server closes (1000)', (done) => { + const wss = new WebSocket.Server({ port: 0 }, () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + + ws.addEventListener('close', (event) => { + assert.ok(event instanceof CloseEvent); + assert.ok(event.wasClean); + assert.strictEqual(event.reason, ''); + assert.strictEqual(event.code, 1000); + wss.close(done); + }); + }); + + wss.on('connection', (ws) => ws.close(1000)); + }); + + it('receives a `CloseEvent` when server closes (4000)', (done) => { + const wss = new WebSocket.Server({ port: 0 }, () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + + ws.addEventListener('close', (event) => { + assert.ok(event instanceof CloseEvent); + assert.ok(event.wasClean); + assert.strictEqual(event.reason, 'some daft reason'); + assert.strictEqual(event.code, 4000); + wss.close(done); + }); + }); + + wss.on('connection', (ws) => ws.close(4000, 'some daft reason')); + }); + + it('sets `target` and `type` on events', (done) => { + const wss = new WebSocket.Server({ port: 0 }, () => { + const err = new Error('forced'); + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + + ws.addEventListener('open', (event) => { + assert.ok(event instanceof Event); + assert.strictEqual(event.type, 'open'); + assert.strictEqual(event.target, ws); + }); + ws.addEventListener('message', (event) => { + assert.ok(event instanceof MessageEvent); + assert.strictEqual(event.type, 'message'); + assert.strictEqual(event.target, ws); + ws.close(); + }); + ws.addEventListener('close', (event) => { + assert.ok(event instanceof CloseEvent); + assert.strictEqual(event.type, 'close'); + assert.strictEqual(event.target, ws); + ws.emit('error', err); + }); + ws.addEventListener('error', (event) => { + assert.ok(event instanceof ErrorEvent); + assert.strictEqual(event.message, 'forced'); + assert.strictEqual(event.type, 'error'); + assert.strictEqual(event.target, ws); + assert.strictEqual(event.error, err); + + wss.close(done); + }); + }); + + wss.on('connection', (client) => client.send('hi')); + }); + + it('passes binary data as a Node.js `Buffer` by default', (done) => { + const wss = new WebSocket.Server({ port: 0 }, () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + + ws.onmessage = (evt) => { + assert.ok(Buffer.isBuffer(evt.data)); + wss.close(done); + }; + }); + + wss.on('connection', (ws) => { + ws.send(new Uint8Array(4096)); + ws.close(); + }); + }); + + it('ignores `binaryType` for text messages', (done) => { + const wss = new WebSocket.Server({ port: 0 }, () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + + ws.binaryType = 'arraybuffer'; + + ws.onmessage = (evt) => { + assert.strictEqual(evt.data, 'foo'); + wss.close(done); + }; + }); + + wss.on('connection', (ws) => { + ws.send('foo'); + ws.close(); + }); + }); + + it('allows to update `binaryType` on the fly', (done) => { + const wss = new WebSocket.Server({ port: 0 }, () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + + function testType(binaryType, next) { + const buf = Buffer.from(binaryType); + ws.binaryType = binaryType; + + ws.onmessage = (evt) => { + if (binaryType === 'nodebuffer') { + assert.ok(Buffer.isBuffer(evt.data)); + assert.ok(evt.data.equals(buf)); + } else if (binaryType === 'arraybuffer') { + assert.ok(evt.data instanceof ArrayBuffer); + assert.ok(Buffer.from(evt.data).equals(buf)); + } else if (binaryType === 'fragments') { + assert.deepStrictEqual(evt.data, [buf]); + } + next(); + }; + + ws.send(buf); + } + + ws.onopen = () => { + testType('nodebuffer', () => { + testType('arraybuffer', () => { + testType('fragments', () => { + ws.close(); + wss.close(done); + }); + }); + }); + }; + }); + + wss.on('connection', (ws) => { + ws.on('message', (msg, isBinary) => { + assert.ok(isBinary); + ws.send(msg); + }); + }); + }); + }); + + describe('SSL', () => { + it('connects to secure websocket server', (done) => { + const server = https.createServer({ + cert: fs.readFileSync('test/fixtures/certificate.pem'), + key: fs.readFileSync('test/fixtures/key.pem') + }); + const wss = new WebSocket.Server({ server }); + + wss.on('connection', () => { + server.close(done); + }); + + server.listen(0, () => { + const ws = new WebSocket(`wss://127.0.0.1:${server.address().port}`, { + rejectUnauthorized: false + }); + + ws.on('open', ws.close); + }); + }); + + it('connects to secure websocket server with client side certificate', (done) => { + const server = https.createServer({ + cert: fs.readFileSync('test/fixtures/certificate.pem'), + ca: [fs.readFileSync('test/fixtures/ca-certificate.pem')], + key: fs.readFileSync('test/fixtures/key.pem'), + requestCert: true + }); + + const wss = new WebSocket.Server({ noServer: true }); + + server.on('upgrade', (request, socket, head) => { + assert.ok(socket.authorized); + + wss.handleUpgrade(request, socket, head, (ws) => { + ws.on('close', (code) => { + assert.strictEqual(code, 1005); + server.close(done); + }); + }); + }); + + server.listen(0, () => { + const ws = new WebSocket(`wss://localhost:${server.address().port}`, { + cert: fs.readFileSync('test/fixtures/client-certificate.pem'), + key: fs.readFileSync('test/fixtures/client-key.pem'), + rejectUnauthorized: false + }); + + ws.on('open', ws.close); + }); + }); + + it('cannot connect to secure websocket server via ws://', (done) => { + const server = https.createServer({ + cert: fs.readFileSync('test/fixtures/certificate.pem'), + key: fs.readFileSync('test/fixtures/key.pem') + }); + const wss = new WebSocket.Server({ server }); + + server.listen(0, () => { + const ws = new WebSocket(`ws://localhost:${server.address().port}`, { + rejectUnauthorized: false + }); + + ws.on('error', () => { + server.close(done); + wss.close(); + }); + }); + }); + + it('can send and receive text data', (done) => { + const server = https.createServer({ + cert: fs.readFileSync('test/fixtures/certificate.pem'), + key: fs.readFileSync('test/fixtures/key.pem') + }); + const wss = new WebSocket.Server({ server }); + + wss.on('connection', (ws) => { + ws.on('message', (message, isBinary) => { + assert.deepStrictEqual(message, Buffer.from('foobar')); + assert.ok(!isBinary); + server.close(done); + }); + }); + + server.listen(0, () => { + const ws = new WebSocket(`wss://localhost:${server.address().port}`, { + rejectUnauthorized: false + }); + + ws.on('open', () => { + ws.send('foobar'); + ws.close(); + }); + }); + }); + + it('can send a big binary message', (done) => { + const buf = crypto.randomBytes(5 * 1024 * 1024); + const server = https.createServer({ + cert: fs.readFileSync('test/fixtures/certificate.pem'), + key: fs.readFileSync('test/fixtures/key.pem') + }); + const wss = new WebSocket.Server({ server }); + + wss.on('connection', (ws) => { + ws.on('message', (message, isBinary) => { + assert.ok(isBinary); + ws.send(message); + ws.close(); + }); + }); + + server.listen(0, () => { + const ws = new WebSocket(`wss://localhost:${server.address().port}`, { + rejectUnauthorized: false + }); + + ws.on('open', () => ws.send(buf)); + ws.on('message', (message, isBinary) => { + assert.deepStrictEqual(message, buf); + assert.ok(isBinary); + + server.close(done); + }); + }); + }).timeout(4000); + + it('allows to disable sending the SNI extension', (done) => { + const original = tls.connect; + + tls.connect = (options) => { + assert.strictEqual(options.servername, ''); + tls.connect = original; + done(); + }; + + const ws = new WebSocket('wss://127.0.0.1', { servername: '' }); + }); + + it("works around a double 'error' event bug in Node.js", function (done) { + // + // The `minVersion` and `maxVersion` options are not supported in + // Node.js < 10.16.0. + // + if (process.versions.modules < 64) return this.skip(); + + // + // The `'error'` event can be emitted multiple times by the + // `http.ClientRequest` object in Node.js < 13. This test reproduces the + // issue in Node.js 12. + // + const server = https.createServer({ + cert: fs.readFileSync('test/fixtures/certificate.pem'), + key: fs.readFileSync('test/fixtures/key.pem'), + minVersion: 'TLSv1.2' + }); + const wss = new WebSocket.Server({ server }); + + server.listen(0, () => { + const ws = new WebSocket(`wss://localhost:${server.address().port}`, { + maxVersion: 'TLSv1.1', + rejectUnauthorized: false + }); + + ws.on('error', (err) => { + assert.ok(err instanceof Error); + server.close(done); + wss.close(); + }); + }); + }); + }); + + describe('Request headers', () => { + it('adds the authorization header if the url has userinfo', (done) => { + const agent = new CustomAgent(); + const userinfo = 'test:testpass'; + + agent.addRequest = (req) => { + assert.strictEqual( + req.getHeader('authorization'), + `Basic ${Buffer.from(userinfo).toString('base64')}` + ); + done(); + }; + + const ws = new WebSocket(`ws://${userinfo}@localhost`, { agent }); + }); + + it('honors the `auth` option', (done) => { + const agent = new CustomAgent(); + const auth = 'user:pass'; + + agent.addRequest = (req) => { + assert.strictEqual( + req.getHeader('authorization'), + `Basic ${Buffer.from(auth).toString('base64')}` + ); + done(); + }; + + const ws = new WebSocket('ws://localhost', { agent, auth }); + }); + + it('favors the url userinfo over the `auth` option', (done) => { + const agent = new CustomAgent(); + const auth = 'foo:bar'; + const userinfo = 'baz:qux'; + + agent.addRequest = (req) => { + assert.strictEqual( + req.getHeader('authorization'), + `Basic ${Buffer.from(userinfo).toString('base64')}` + ); + done(); + }; + + const ws = new WebSocket(`ws://${userinfo}@localhost`, { agent, auth }); + }); + + it('adds custom headers', (done) => { + const agent = new CustomAgent(); + + agent.addRequest = (req) => { + assert.strictEqual(req.getHeader('cookie'), 'foo=bar'); + done(); + }; + + const ws = new WebSocket('ws://localhost', { + headers: { Cookie: 'foo=bar' }, + agent + }); + }); + + it('excludes default ports from host header', () => { + const options = { lookup() {} }; + const variants = [ + ['wss://localhost:8443', 'localhost:8443'], + ['wss://localhost:443', 'localhost'], + ['ws://localhost:88', 'localhost:88'], + ['ws://localhost:80', 'localhost'] + ]; + + for (const [url, host] of variants) { + const ws = new WebSocket(url, options); + assert.strictEqual(ws._req.getHeader('host'), host); + } + }); + + it("doesn't add the origin header by default", (done) => { + const agent = new CustomAgent(); + + agent.addRequest = (req) => { + assert.strictEqual(req.getHeader('origin'), undefined); + done(); + }; + + const ws = new WebSocket('ws://localhost', { agent }); + }); + + it('honors the `origin` option (1/2)', (done) => { + const agent = new CustomAgent(); + + agent.addRequest = (req) => { + assert.strictEqual(req.getHeader('origin'), 'https://example.com:8000'); + done(); + }; + + const ws = new WebSocket('ws://localhost', { + origin: 'https://example.com:8000', + agent + }); + }); + + it('honors the `origin` option (2/2)', (done) => { + const agent = new CustomAgent(); + + agent.addRequest = (req) => { + assert.strictEqual( + req.getHeader('sec-websocket-origin'), + 'https://example.com:8000' + ); + done(); + }; + + const ws = new WebSocket('ws://localhost', { + origin: 'https://example.com:8000', + protocolVersion: 8, + agent + }); + }); + }); + + describe('permessage-deflate', () => { + it('is enabled by default', (done) => { + const agent = new CustomAgent(); + + agent.addRequest = (req) => { + assert.strictEqual( + req.getHeader('sec-websocket-extensions'), + 'permessage-deflate; client_max_window_bits' + ); + done(); + }; + + const ws = new WebSocket('ws://localhost', { agent }); + }); + + it('can be disabled', (done) => { + const agent = new CustomAgent(); + + agent.addRequest = (req) => { + assert.strictEqual( + req.getHeader('sec-websocket-extensions'), + undefined + ); + done(); + }; + + const ws = new WebSocket('ws://localhost', { + perMessageDeflate: false, + agent + }); + }); + + it('can send extension parameters', (done) => { + const agent = new CustomAgent(); + + const value = + 'permessage-deflate; server_no_context_takeover;' + + ' client_no_context_takeover; server_max_window_bits=10;' + + ' client_max_window_bits'; + + agent.addRequest = (req) => { + assert.strictEqual(req.getHeader('sec-websocket-extensions'), value); + done(); + }; + + const ws = new WebSocket('ws://localhost', { + perMessageDeflate: { + clientNoContextTakeover: true, + serverNoContextTakeover: true, + clientMaxWindowBits: true, + serverMaxWindowBits: 10 + }, + agent + }); + }); + + it('consumes all received data when connection is closed (1/2)', (done) => { + const wss = new WebSocket.Server( + { + perMessageDeflate: { threshold: 0 }, + port: 0 + }, + () => { + const messages = []; + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + + ws.on('open', () => { + ws._socket.on('close', () => { + assert.strictEqual(ws._receiver._state, 5); + }); + }); + + ws.on('message', (message, isBinary) => { + assert.ok(!isBinary); + messages.push(message.toString()); + }); + + ws.on('close', (code) => { + assert.strictEqual(code, 1006); + assert.deepStrictEqual(messages, ['foo', 'bar', 'baz', 'qux']); + wss.close(done); + }); + } + ); + + wss.on('connection', (ws) => { + ws.send('foo'); + ws.send('bar'); + ws.send('baz'); + ws.send('qux', () => ws._socket.end()); + }); + }); + + it('consumes all received data when connection is closed (2/2)', (done) => { + const wss = new WebSocket.Server( + { + perMessageDeflate: true, + port: 0 + }, + () => { + const messageLengths = []; + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + + ws.on('open', () => { + ws._socket.prependListener('close', () => { + assert.strictEqual(ws._receiver._state, 5); + assert.strictEqual(ws._socket._readableState.length, 3); + }); + + const push = ws._socket.push; + + // Override `ws._socket.push()` to know exactly when data is + // received and call `ws.terminate()` immediately after that without + // relying on a timer. + ws._socket.push = (data) => { + ws._socket.push = push; + ws._socket.push(data); + ws.terminate(); + }; + + const payload1 = Buffer.alloc(15 * 1024); + const payload2 = Buffer.alloc(1); + + const opts = { + fin: true, + opcode: 0x02, + mask: false, + readOnly: false + }; + + const list = [ + ...Sender.frame(payload1, { rsv1: false, ...opts }), + ...Sender.frame(payload2, { rsv1: true, ...opts }) + ]; + + for (let i = 0; i < 399; i++) { + list.push(list[list.length - 2], list[list.length - 1]); + } + + // This hack is used because there is no guarantee that more than + // 16 KiB will be sent as a single TCP packet. + push.call(ws._socket, Buffer.concat(list)); + + wss.clients + .values() + .next() + .value.send(payload2, { compress: false }); + }); + + ws.on('message', (message, isBinary) => { + assert.ok(isBinary); + messageLengths.push(message.length); + }); + + ws.on('close', (code) => { + assert.strictEqual(code, 1006); + assert.strictEqual(messageLengths.length, 402); + assert.strictEqual(messageLengths[0], 15360); + assert.strictEqual(messageLengths[messageLengths.length - 1], 1); + wss.close(done); + }); + } + ); + }); + + it('handles a close frame received while compressing data', (done) => { + const wss = new WebSocket.Server( + { + perMessageDeflate: true, + port: 0 + }, + () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`, { + perMessageDeflate: { threshold: 0 } + }); + + ws.on('open', () => { + ws._receiver.on('conclude', () => { + assert.ok(ws._sender._deflating); + }); + + ws.send('foo'); + ws.send('bar'); + ws.send('baz'); + ws.send('qux'); + }); + } + ); + + wss.on('connection', (ws) => { + const messages = []; + + ws.on('message', (message, isBinary) => { + assert.ok(!isBinary); + messages.push(message.toString()); + }); + + ws.on('close', (code, reason) => { + assert.deepStrictEqual(messages, ['foo', 'bar', 'baz', 'qux']); + assert.strictEqual(code, 1000); + assert.deepStrictEqual(reason, EMPTY_BUFFER); + wss.close(done); + }); + + ws.close(1000); + }); + }); + + describe('#close', () => { + it('can be used while data is being decompressed', (done) => { + const wss = new WebSocket.Server( + { + perMessageDeflate: true, + port: 0 + }, + () => { + const messages = []; + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + + ws.on('open', () => { + ws._socket.on('end', () => { + assert.strictEqual(ws._receiver._state, 5); + }); + }); + + ws.on('message', (message, isBinary) => { + assert.ok(!isBinary); + + if (messages.push(message.toString()) > 1) return; + + ws.close(1000); + }); + + ws.on('close', (code, reason) => { + assert.deepStrictEqual(messages, ['', '', '', '']); + assert.strictEqual(code, 1000); + assert.deepStrictEqual(reason, EMPTY_BUFFER); + wss.close(done); + }); + } + ); + + wss.on('connection', (ws) => { + const buf = Buffer.from('c10100c10100c10100c10100', 'hex'); + ws._socket.write(buf); + }); + }); + }); + + describe('#send', () => { + it('can send text data', (done) => { + const wss = new WebSocket.Server( + { + perMessageDeflate: { threshold: 0 }, + port: 0 + }, + () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`, { + perMessageDeflate: { threshold: 0 } + }); + + ws.on('open', () => { + ws.send('hi', { compress: true }); + ws.close(); + }); + + ws.on('message', (message, isBinary) => { + assert.deepStrictEqual(message, Buffer.from('hi')); + assert.ok(!isBinary); + wss.close(done); + }); + } + ); + + wss.on('connection', (ws) => { + ws.on('message', (message, isBinary) => { + ws.send(message, { binary: isBinary, compress: true }); + }); + }); + }); + + it('can send a `TypedArray`', (done) => { + const array = new Float32Array(5); + + for (let i = 0; i < array.length; i++) { + array[i] = i / 2; + } + + const wss = new WebSocket.Server( + { + perMessageDeflate: { threshold: 0 }, + port: 0 + }, + () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`, { + perMessageDeflate: { threshold: 0 } + }); + + ws.on('open', () => { + ws.send(array, { compress: true }); + ws.close(); + }); + + ws.on('message', (message, isBinary) => { + assert.deepStrictEqual(message, Buffer.from(array.buffer)); + assert.ok(isBinary); + wss.close(done); + }); + } + ); + + wss.on('connection', (ws) => { + ws.on('message', (message, isBinary) => { + assert.ok(isBinary); + ws.send(message, { compress: true }); + }); + }); + }); + + it('can send an `ArrayBuffer`', (done) => { + const array = new Float32Array(5); + + for (let i = 0; i < array.length; i++) { + array[i] = i / 2; + } + + const wss = new WebSocket.Server( + { + perMessageDeflate: { threshold: 0 }, + port: 0 + }, + () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`, { + perMessageDeflate: { threshold: 0 } + }); + + ws.on('open', () => { + ws.send(array.buffer, { compress: true }); + ws.close(); + }); + + ws.on('message', (message, isBinary) => { + assert.deepStrictEqual(message, Buffer.from(array.buffer)); + assert.ok(isBinary); + wss.close(done); + }); + } + ); + + wss.on('connection', (ws) => { + ws.on('message', (message, isBinary) => { + assert.ok(isBinary); + ws.send(message, { compress: true }); + }); + }); + }); + + it('ignores the `compress` option if the extension is disabled', (done) => { + const wss = new WebSocket.Server({ port: 0 }, () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`, { + perMessageDeflate: false + }); + + ws.on('open', () => { + ws.send('hi', { compress: true }); + ws.close(); + }); + + ws.on('message', (message, isBinary) => { + assert.deepStrictEqual(message, Buffer.from('hi')); + assert.ok(!isBinary); + wss.close(done); + }); + }); + + wss.on('connection', (ws) => { + ws.on('message', (message, isBinary) => { + ws.send(message, { binary: isBinary, compress: true }); + }); + }); + }); + + it('calls the callback if the socket is closed prematurely', (done) => { + const called = []; + const wss = new WebSocket.Server( + { perMessageDeflate: true, port: 0 }, + () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`, { + perMessageDeflate: { threshold: 0 } + }); + + ws.on('open', () => { + ws.send('foo'); + ws.send('bar', (err) => { + called.push(1); + + assert.strictEqual(ws.readyState, WebSocket.CLOSING); + assert.ok(err instanceof Error); + assert.strictEqual( + err.message, + 'The socket was closed while data was being compressed' + ); + }); + ws.send('baz'); + ws.send('qux', (err) => { + called.push(2); + + assert.strictEqual(ws.readyState, WebSocket.CLOSING); + assert.ok(err instanceof Error); + assert.strictEqual( + err.message, + 'The socket was closed while data was being compressed' + ); + }); + }); + } + ); + + wss.on('connection', (ws) => { + ws.on('close', () => { + assert.deepStrictEqual(called, [1, 2]); + wss.close(done); + }); + + ws._socket.end(); + }); + }); + }); + + describe('#terminate', () => { + it('can be used while data is being compressed', (done) => { + const wss = new WebSocket.Server( + { + perMessageDeflate: { threshold: 0 }, + port: 0 + }, + () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`, { + perMessageDeflate: { threshold: 0 } + }); + + ws.on('open', () => { + ws.send('hi', (err) => { + assert.strictEqual(ws.readyState, WebSocket.CLOSING); + assert.ok(err instanceof Error); + assert.strictEqual( + err.message, + 'The socket was closed while data was being compressed' + ); + + ws.on('close', () => { + wss.close(done); + }); + }); + ws.terminate(); + }); + } + ); + }); + + it('can be used while data is being decompressed', (done) => { + const wss = new WebSocket.Server( + { + perMessageDeflate: true, + port: 0 + }, + () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + const messages = []; + + ws.on('message', (message, isBinary) => { + assert.ok(!isBinary); + + if (messages.push(message.toString()) > 1) return; + + process.nextTick(() => { + assert.strictEqual(ws._receiver._state, 5); + ws.terminate(); + }); + }); + + ws.on('close', (code, reason) => { + assert.deepStrictEqual(messages, ['', '', '', '']); + assert.strictEqual(code, 1006); + assert.strictEqual(reason, EMPTY_BUFFER); + wss.close(done); + }); + } + ); + + wss.on('connection', (ws) => { + const buf = Buffer.from('c10100c10100c10100c10100', 'hex'); + ws._socket.write(buf); + }); + }); + }); + }); + + describe('Connection close', () => { + it('closes cleanly after simultaneous errors (1/2)', (done) => { + let clientCloseEventEmitted = false; + let serverClientCloseEventEmitted = false; + + const wss = new WebSocket.Server({ port: 0 }, () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + + ws.on('error', (err) => { + assert.ok(err instanceof RangeError); + assert.strictEqual(err.code, 'WS_ERR_INVALID_OPCODE'); + assert.strictEqual( + err.message, + 'Invalid WebSocket frame: invalid opcode 5' + ); + + ws.on('close', (code, reason) => { + assert.strictEqual(code, 1006); + assert.strictEqual(reason, EMPTY_BUFFER); + + clientCloseEventEmitted = true; + if (serverClientCloseEventEmitted) wss.close(done); + }); + }); + + ws.on('open', () => { + // Write an invalid frame in both directions to trigger simultaneous + // failure. + const chunk = Buffer.from([0x85, 0x00]); + + wss.clients.values().next().value._socket.write(chunk); + ws._socket.write(chunk); + }); + }); + + wss.on('connection', (ws) => { + ws.on('error', (err) => { + assert.ok(err instanceof RangeError); + assert.strictEqual(err.code, 'WS_ERR_INVALID_OPCODE'); + assert.strictEqual( + err.message, + 'Invalid WebSocket frame: invalid opcode 5' + ); + + ws.on('close', (code, reason) => { + assert.strictEqual(code, 1006); + assert.strictEqual(reason, EMPTY_BUFFER); + + serverClientCloseEventEmitted = true; + if (clientCloseEventEmitted) wss.close(done); + }); + }); + }); + }); + + it('closes cleanly after simultaneous errors (2/2)', (done) => { + let clientCloseEventEmitted = false; + let serverClientCloseEventEmitted = false; + + const wss = new WebSocket.Server({ port: 0 }, () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + + ws.on('error', (err) => { + assert.ok(err instanceof RangeError); + assert.strictEqual(err.code, 'WS_ERR_INVALID_OPCODE'); + assert.strictEqual( + err.message, + 'Invalid WebSocket frame: invalid opcode 5' + ); + + ws.on('close', (code, reason) => { + assert.strictEqual(code, 1006); + assert.strictEqual(reason, EMPTY_BUFFER); + + clientCloseEventEmitted = true; + if (serverClientCloseEventEmitted) wss.close(done); + }); + }); + + ws.on('open', () => { + // Write an invalid frame in both directions and change the + // `readyState` to `WebSocket.CLOSING`. + const chunk = Buffer.from([0x85, 0x00]); + const serverWs = wss.clients.values().next().value; + + serverWs._socket.write(chunk); + serverWs.close(); + + ws._socket.write(chunk); + ws.close(); + }); + }); + + wss.on('connection', (ws) => { + ws.on('error', (err) => { + assert.ok(err instanceof RangeError); + assert.strictEqual(err.code, 'WS_ERR_INVALID_OPCODE'); + assert.strictEqual( + err.message, + 'Invalid WebSocket frame: invalid opcode 5' + ); + + ws.on('close', (code, reason) => { + assert.strictEqual(code, 1006); + assert.strictEqual(reason, EMPTY_BUFFER); + + serverClientCloseEventEmitted = true; + if (clientCloseEventEmitted) wss.close(done); + }); + }); + }); + }); + + it('resumes the socket when an error occurs', (done) => { + const maxPayload = 16 * 1024; + const wss = new WebSocket.Server({ maxPayload, port: 0 }, () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + }); + + wss.on('connection', (ws) => { + const list = [ + ...Sender.frame(Buffer.alloc(maxPayload + 1), { + fin: true, + opcode: 0x02, + mask: true, + readOnly: false + }) + ]; + + ws.on('error', (err) => { + assert.ok(err instanceof RangeError); + assert.strictEqual(err.code, 'WS_ERR_UNSUPPORTED_MESSAGE_LENGTH'); + assert.strictEqual(err.message, 'Max payload size exceeded'); + + ws.on('close', (code, reason) => { + assert.strictEqual(code, 1006); + assert.strictEqual(reason, EMPTY_BUFFER); + wss.close(done); + }); + }); + + ws._socket.push(Buffer.concat(list)); + }); + }); + + it('resumes the socket when the close frame is received', (done) => { + const wss = new WebSocket.Server({ port: 0 }, () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + }); + + wss.on('connection', (ws) => { + const opts = { fin: true, mask: true, readOnly: false }; + const list = [ + ...Sender.frame(Buffer.alloc(16 * 1024), { opcode: 0x02, ...opts }), + ...Sender.frame(EMPTY_BUFFER, { opcode: 0x08, ...opts }) + ]; + + ws.on('close', (code, reason) => { + assert.strictEqual(code, 1005); + assert.strictEqual(reason, EMPTY_BUFFER); + wss.close(done); + }); + + ws._socket.push(Buffer.concat(list)); + }); + }); + }); +}); |