/* 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)); }); }); }); });