diff options
Diffstat (limited to '')
-rw-r--r-- | testing/xpcshell/node-ws/test/websocket-server.test.js | 1284 |
1 files changed, 1284 insertions, 0 deletions
diff --git a/testing/xpcshell/node-ws/test/websocket-server.test.js b/testing/xpcshell/node-ws/test/websocket-server.test.js new file mode 100644 index 0000000000..12928ff495 --- /dev/null +++ b/testing/xpcshell/node-ws/test/websocket-server.test.js @@ -0,0 +1,1284 @@ +/* eslint no-unused-vars: ["error", { "varsIgnorePattern": "^ws$" }] */ + +'use strict'; + +const assert = require('assert'); +const crypto = require('crypto'); +const https = require('https'); +const http = require('http'); +const path = require('path'); +const net = require('net'); +const fs = require('fs'); +const os = require('os'); + +const Sender = require('../lib/sender'); +const WebSocket = require('..'); +const { NOOP } = require('../lib/constants'); + +describe('WebSocketServer', () => { + describe('#ctor', () => { + it('throws an error if no option object is passed', () => { + assert.throws( + () => new WebSocket.Server(), + new RegExp( + '^TypeError: One and only one of the "port", "server", or ' + + '"noServer" options must be specified$' + ) + ); + }); + + describe('options', () => { + it('throws an error if required options are not specified', () => { + assert.throws( + () => new WebSocket.Server({}), + new RegExp( + '^TypeError: One and only one of the "port", "server", or ' + + '"noServer" options must be specified$' + ) + ); + }); + + it('throws an error if mutually exclusive options are specified', () => { + const server = http.createServer(); + const variants = [ + { port: 0, noServer: true, server }, + { port: 0, noServer: true }, + { port: 0, server }, + { noServer: true, server } + ]; + + for (const options of variants) { + assert.throws( + () => new WebSocket.Server(options), + new RegExp( + '^TypeError: One and only one of the "port", "server", or ' + + '"noServer" options must be specified$' + ) + ); + } + }); + + it('exposes options passed to constructor', (done) => { + const wss = new WebSocket.Server({ port: 0 }, () => { + assert.strictEqual(wss.options.port, 0); + wss.close(done); + }); + }); + + it('accepts the `maxPayload` option', (done) => { + const maxPayload = 20480; + const wss = new WebSocket.Server( + { + perMessageDeflate: true, + maxPayload, + port: 0 + }, + () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + + ws.on('open', ws.close); + } + ); + + wss.on('connection', (ws) => { + assert.strictEqual(ws._receiver._maxPayload, maxPayload); + assert.strictEqual( + ws._receiver._extensions['permessage-deflate']._maxPayload, + maxPayload + ); + wss.close(done); + }); + }); + + it('honors the `WebSocket` option', (done) => { + class CustomWebSocket extends WebSocket.WebSocket { + get foo() { + return 'foo'; + } + } + + const wss = new WebSocket.Server( + { + port: 0, + WebSocket: CustomWebSocket + }, + () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + + ws.on('open', ws.close); + } + ); + + wss.on('connection', (ws) => { + assert.ok(ws instanceof CustomWebSocket); + assert.strictEqual(ws.foo, 'foo'); + wss.close(done); + }); + }); + }); + + it('emits an error if http server bind fails', (done) => { + const wss1 = new WebSocket.Server({ port: 0 }, () => { + const wss2 = new WebSocket.Server({ + port: wss1.address().port + }); + + wss2.on('error', () => wss1.close(done)); + }); + }); + + it('starts a server on a given port', (done) => { + const port = 1337; + const wss = new WebSocket.Server({ port }, () => { + const ws = new WebSocket(`ws://localhost:${port}`); + + ws.on('open', ws.close); + }); + + wss.on('connection', () => wss.close(done)); + }); + + it('binds the server on any IPv6 address when available', (done) => { + const wss = new WebSocket.Server({ port: 0 }, () => { + assert.strictEqual(wss._server.address().address, '::'); + wss.close(done); + }); + }); + + it('uses a precreated http server', (done) => { + const server = http.createServer(); + + server.listen(0, () => { + const wss = new WebSocket.Server({ server }); + + wss.on('connection', () => { + server.close(done); + }); + + const ws = new WebSocket(`ws://localhost:${server.address().port}`); + + ws.on('open', ws.close); + }); + }); + + it('426s for non-Upgrade requests', (done) => { + const wss = new WebSocket.Server({ port: 0 }, () => { + http.get(`http://localhost:${wss.address().port}`, (res) => { + let body = ''; + + assert.strictEqual(res.statusCode, 426); + res.on('data', (chunk) => { + body += chunk; + }); + res.on('end', () => { + assert.strictEqual(body, http.STATUS_CODES[426]); + wss.close(done); + }); + }); + }); + }); + + it('uses a precreated http server listening on unix socket', function (done) { + // + // Skip this test on Windows. The URL parser: + // + // - Throws an error if the named pipe uses backward slashes. + // - Incorrectly parses the path if the named pipe uses forward slashes. + // + if (process.platform === 'win32') return this.skip(); + + const server = http.createServer(); + const sockPath = path.join( + os.tmpdir(), + `ws.${crypto.randomBytes(16).toString('hex')}.sock` + ); + + server.listen(sockPath, () => { + const wss = new WebSocket.Server({ server }); + + wss.on('connection', (ws, req) => { + if (wss.clients.size === 1) { + assert.strictEqual(req.url, '/foo?bar=bar'); + } else { + assert.strictEqual(req.url, '/'); + + for (const client of wss.clients) { + client.close(); + } + + server.close(done); + } + }); + + const ws = new WebSocket(`ws+unix://${sockPath}:/foo?bar=bar`); + ws.on('open', () => new WebSocket(`ws+unix://${sockPath}`)); + }); + }); + }); + + describe('#address', () => { + it('returns the address of the server', (done) => { + const wss = new WebSocket.Server({ port: 0 }, () => { + const addr = wss.address(); + + assert.deepStrictEqual(addr, wss._server.address()); + wss.close(done); + }); + }); + + it('throws an error when operating in "noServer" mode', () => { + const wss = new WebSocket.Server({ noServer: true }); + + assert.throws(() => { + wss.address(); + }, /^Error: The server is operating in "noServer" mode$/); + }); + + it('returns `null` if called after close', (done) => { + const wss = new WebSocket.Server({ port: 0 }, () => { + wss.close(() => { + assert.strictEqual(wss.address(), null); + done(); + }); + }); + }); + }); + + describe('#close', () => { + it('does not throw if called multiple times', (done) => { + const wss = new WebSocket.Server({ port: 0 }, () => { + wss.on('close', done); + + wss.close(); + wss.close(); + wss.close(); + }); + }); + + it("doesn't close a precreated server", (done) => { + const server = http.createServer(); + const realClose = server.close; + + server.close = () => { + done(new Error('Must not close pre-created server')); + }; + + const wss = new WebSocket.Server({ server }); + + wss.on('connection', () => { + wss.close(); + server.close = realClose; + server.close(done); + }); + + server.listen(0, () => { + const ws = new WebSocket(`ws://localhost:${server.address().port}`); + + ws.on('open', ws.close); + }); + }); + + it('invokes the callback in noServer mode', (done) => { + const wss = new WebSocket.Server({ noServer: true }); + + wss.close(done); + }); + + it('cleans event handlers on precreated server', (done) => { + const server = http.createServer(); + const wss = new WebSocket.Server({ server }); + + server.listen(0, () => { + wss.close(() => { + assert.strictEqual(server.listenerCount('listening'), 0); + assert.strictEqual(server.listenerCount('upgrade'), 0); + assert.strictEqual(server.listenerCount('error'), 0); + + server.close(done); + }); + }); + }); + + it("emits the 'close' event after the server closes", (done) => { + let serverCloseEventEmitted = false; + + const wss = new WebSocket.Server({ port: 0 }, () => { + net.createConnection({ port: wss.address().port }); + }); + + wss._server.on('connection', (socket) => { + wss.close(); + + // + // The server is closing. Ensure this does not emit a `'close'` + // event before the server is actually closed. + // + wss.close(); + + process.nextTick(() => { + socket.end(); + }); + }); + + wss._server.on('close', () => { + serverCloseEventEmitted = true; + }); + + wss.on('close', () => { + assert.ok(serverCloseEventEmitted); + done(); + }); + }); + + it("emits the 'close' event if client tracking is disabled", (done) => { + const wss = new WebSocket.Server({ + noServer: true, + clientTracking: false + }); + + wss.on('close', done); + wss.close(); + }); + + it('calls the callback if the server is already closed', (done) => { + const wss = new WebSocket.Server({ port: 0 }, () => { + wss.close(() => { + assert.strictEqual(wss._state, 2); + + wss.close((err) => { + assert.ok(err instanceof Error); + assert.strictEqual(err.message, 'The server is not running'); + done(); + }); + }); + }); + }); + + it("emits the 'close' event if the server is already closed", (done) => { + const wss = new WebSocket.Server({ port: 0 }, () => { + wss.close(() => { + assert.strictEqual(wss._state, 2); + + wss.on('close', done); + wss.close(); + }); + }); + }); + }); + + describe('#clients', () => { + it('returns a list of connected clients', (done) => { + const wss = new WebSocket.Server({ port: 0 }, () => { + assert.strictEqual(wss.clients.size, 0); + + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + + ws.on('open', ws.close); + }); + + wss.on('connection', () => { + assert.strictEqual(wss.clients.size, 1); + wss.close(done); + }); + }); + + it('can be disabled', (done) => { + const wss = new WebSocket.Server( + { port: 0, clientTracking: false }, + () => { + assert.strictEqual(wss.clients, undefined); + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + + ws.on('open', () => ws.close()); + } + ); + + wss.on('connection', (ws) => { + assert.strictEqual(wss.clients, undefined); + ws.on('close', () => wss.close(done)); + }); + }); + + it('is updated when client terminates the connection', (done) => { + const wss = new WebSocket.Server({ port: 0 }, () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + + ws.on('open', () => ws.terminate()); + }); + + wss.on('connection', (ws) => { + ws.on('close', () => { + assert.strictEqual(wss.clients.size, 0); + wss.close(done); + }); + }); + }); + + it('is updated when client closes the connection', (done) => { + const wss = new WebSocket.Server({ port: 0 }, () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + + ws.on('open', () => ws.close()); + }); + + wss.on('connection', (ws) => { + ws.on('close', () => { + assert.strictEqual(wss.clients.size, 0); + wss.close(done); + }); + }); + }); + }); + + describe('#shouldHandle', () => { + it('returns true when the path matches', () => { + const wss = new WebSocket.Server({ noServer: true, path: '/foo' }); + + assert.strictEqual(wss.shouldHandle({ url: '/foo' }), true); + assert.strictEqual(wss.shouldHandle({ url: '/foo?bar=baz' }), true); + }); + + it("returns false when the path doesn't match", () => { + const wss = new WebSocket.Server({ noServer: true, path: '/foo' }); + + assert.strictEqual(wss.shouldHandle({ url: '/bar' }), false); + }); + }); + + describe('#handleUpgrade', () => { + it('can be used for a pre-existing server', (done) => { + const server = http.createServer(); + + server.listen(0, () => { + const wss = new WebSocket.Server({ noServer: true }); + + server.on('upgrade', (req, socket, head) => { + wss.handleUpgrade(req, socket, head, (ws) => { + ws.send('hello'); + ws.close(); + }); + }); + + const ws = new WebSocket(`ws://localhost:${server.address().port}`); + + ws.on('message', (message, isBinary) => { + assert.deepStrictEqual(message, Buffer.from('hello')); + assert.ok(!isBinary); + server.close(done); + }); + }); + }); + + it("closes the connection when path doesn't match", (done) => { + const wss = new WebSocket.Server({ port: 0, path: '/ws' }, () => { + const req = http.get({ + port: wss.address().port, + headers: { + Connection: 'Upgrade', + Upgrade: 'websocket', + 'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==', + 'Sec-WebSocket-Version': 13 + } + }); + + req.on('response', (res) => { + assert.strictEqual(res.statusCode, 400); + wss.close(done); + }); + }); + }); + + it('closes the connection when protocol version is Hixie-76', (done) => { + const wss = new WebSocket.Server({ port: 0 }, () => { + const req = http.get({ + port: wss.address().port, + headers: { + Connection: 'Upgrade', + Upgrade: 'WebSocket', + 'Sec-WebSocket-Key1': '4 @1 46546xW%0l 1 5', + 'Sec-WebSocket-Key2': '12998 5 Y3 1 .P00', + 'Sec-WebSocket-Protocol': 'sample' + } + }); + + req.on('response', (res) => { + assert.strictEqual(res.statusCode, 400); + + const chunks = []; + + res.on('data', (chunk) => { + chunks.push(chunk); + }); + + res.on('end', () => { + assert.strictEqual( + Buffer.concat(chunks).toString(), + 'Missing or invalid Sec-WebSocket-Key header' + ); + wss.close(done); + }); + }); + }); + }); + }); + + describe('#completeUpgrade', () => { + it('throws an error if called twice with the same socket', (done) => { + const server = http.createServer(); + + server.listen(0, () => { + const wss = new WebSocket.Server({ noServer: true }); + + server.on('upgrade', (req, socket, head) => { + wss.handleUpgrade(req, socket, head, (ws) => { + ws.close(); + }); + assert.throws( + () => wss.handleUpgrade(req, socket, head, NOOP), + (err) => { + assert.ok(err instanceof Error); + assert.strictEqual( + err.message, + 'server.handleUpgrade() was called more than once with the ' + + 'same socket, possibly due to a misconfiguration' + ); + return true; + } + ); + }); + + const ws = new WebSocket(`ws://localhost:${server.address().port}`); + + ws.on('open', () => { + ws.on('close', () => { + server.close(done); + }); + }); + }); + }); + }); + + describe('Connection establishing', () => { + it('fails if the HTTP method is not GET', (done) => { + const wss = new WebSocket.Server({ port: 0 }, () => { + const req = http.request({ + method: 'POST', + port: wss.address().port, + headers: { + Connection: 'Upgrade', + Upgrade: 'websocket' + } + }); + + req.on('response', (res) => { + assert.strictEqual(res.statusCode, 405); + + const chunks = []; + + res.on('data', (chunk) => { + chunks.push(chunk); + }); + + res.on('end', () => { + assert.strictEqual( + Buffer.concat(chunks).toString(), + 'Invalid HTTP method' + ); + wss.close(done); + }); + }); + + req.end(); + }); + + wss.on('connection', () => { + done(new Error("Unexpected 'connection' event")); + }); + }); + + it('fails if the Upgrade header field value is not "websocket"', (done) => { + const wss = new WebSocket.Server({ port: 0 }, () => { + const req = http.get({ + port: wss.address().port, + headers: { + Connection: 'Upgrade', + Upgrade: 'foo' + } + }); + + req.on('response', (res) => { + assert.strictEqual(res.statusCode, 400); + + const chunks = []; + + res.on('data', (chunk) => { + chunks.push(chunk); + }); + + res.on('end', () => { + assert.strictEqual( + Buffer.concat(chunks).toString(), + 'Invalid Upgrade header' + ); + wss.close(done); + }); + }); + }); + + wss.on('connection', () => { + done(new Error("Unexpected 'connection' event")); + }); + }); + + it('fails if the Sec-WebSocket-Key header is invalid (1/2)', (done) => { + const wss = new WebSocket.Server({ port: 0 }, () => { + const req = http.get({ + port: wss.address().port, + headers: { + Connection: 'Upgrade', + Upgrade: 'websocket' + } + }); + + req.on('response', (res) => { + assert.strictEqual(res.statusCode, 400); + + const chunks = []; + + res.on('data', (chunk) => { + chunks.push(chunk); + }); + + res.on('end', () => { + assert.strictEqual( + Buffer.concat(chunks).toString(), + 'Missing or invalid Sec-WebSocket-Key header' + ); + wss.close(done); + }); + }); + }); + + wss.on('connection', () => { + done(new Error("Unexpected 'connection' event")); + }); + }); + + it('fails if the Sec-WebSocket-Key header is invalid (2/2)', (done) => { + const wss = new WebSocket.Server({ port: 0 }, () => { + const req = http.get({ + port: wss.address().port, + headers: { + Connection: 'Upgrade', + Upgrade: 'websocket', + 'Sec-WebSocket-Key': 'P5l8BJcZwRc=' + } + }); + + req.on('response', (res) => { + assert.strictEqual(res.statusCode, 400); + + const chunks = []; + + res.on('data', (chunk) => { + chunks.push(chunk); + }); + + res.on('end', () => { + assert.strictEqual( + Buffer.concat(chunks).toString(), + 'Missing or invalid Sec-WebSocket-Key header' + ); + wss.close(done); + }); + }); + }); + + wss.on('connection', () => { + done(new Error("Unexpected 'connection' event")); + }); + }); + + it('fails if the Sec-WebSocket-Version header is invalid (1/2)', (done) => { + const wss = new WebSocket.Server({ port: 0 }, () => { + const req = http.get({ + port: wss.address().port, + headers: { + Connection: 'Upgrade', + Upgrade: 'websocket', + 'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==' + } + }); + + req.on('response', (res) => { + assert.strictEqual(res.statusCode, 400); + + const chunks = []; + + res.on('data', (chunk) => { + chunks.push(chunk); + }); + + res.on('end', () => { + assert.strictEqual( + Buffer.concat(chunks).toString(), + 'Missing or invalid Sec-WebSocket-Version header' + ); + wss.close(done); + }); + }); + }); + + wss.on('connection', () => { + done(new Error("Unexpected 'connection' event")); + }); + }); + + it('fails if the Sec-WebSocket-Version header is invalid (2/2)', (done) => { + const wss = new WebSocket.Server({ port: 0 }, () => { + const req = http.get({ + port: wss.address().port, + headers: { + Connection: 'Upgrade', + Upgrade: 'websocket', + 'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==', + 'Sec-WebSocket-Version': 12 + } + }); + + req.on('response', (res) => { + assert.strictEqual(res.statusCode, 400); + + const chunks = []; + + res.on('data', (chunk) => { + chunks.push(chunk); + }); + + res.on('end', () => { + assert.strictEqual( + Buffer.concat(chunks).toString(), + 'Missing or invalid Sec-WebSocket-Version header' + ); + wss.close(done); + }); + }); + }); + + wss.on('connection', () => { + done(new Error("Unexpected 'connection' event")); + }); + }); + + it('fails is the Sec-WebSocket-Protocol header is invalid', (done) => { + const wss = new WebSocket.Server({ port: 0 }, () => { + const req = http.get({ + port: wss.address().port, + headers: { + Connection: 'Upgrade', + Upgrade: 'websocket', + 'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==', + 'Sec-WebSocket-Version': 13, + 'Sec-WebSocket-Protocol': 'foo;bar' + } + }); + + req.on('response', (res) => { + assert.strictEqual(res.statusCode, 400); + + const chunks = []; + + res.on('data', (chunk) => { + chunks.push(chunk); + }); + + res.on('end', () => { + assert.strictEqual( + Buffer.concat(chunks).toString(), + 'Invalid Sec-WebSocket-Protocol header' + ); + wss.close(done); + }); + }); + }); + + wss.on('connection', () => { + done(new Error("Unexpected 'connection' event")); + }); + }); + + it('fails if the Sec-WebSocket-Extensions header is invalid', (done) => { + const wss = new WebSocket.Server( + { + perMessageDeflate: true, + port: 0 + }, + () => { + const req = http.get({ + port: wss.address().port, + headers: { + Connection: 'Upgrade', + Upgrade: 'websocket', + 'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==', + 'Sec-WebSocket-Version': 13, + 'Sec-WebSocket-Extensions': + 'permessage-deflate; server_max_window_bits=foo' + } + }); + + req.on('response', (res) => { + assert.strictEqual(res.statusCode, 400); + + const chunks = []; + + res.on('data', (chunk) => { + chunks.push(chunk); + }); + + res.on('end', () => { + assert.strictEqual( + Buffer.concat(chunks).toString(), + 'Invalid or unacceptable Sec-WebSocket-Extensions header' + ); + wss.close(done); + }); + }); + } + ); + + wss.on('connection', () => { + done(new Error("Unexpected 'connection' event")); + }); + }); + + it("emits the 'wsClientError' event", (done) => { + const wss = new WebSocket.Server({ port: 0 }, () => { + const req = http.request({ + method: 'POST', + port: wss.address().port, + headers: { + Connection: 'Upgrade', + Upgrade: 'websocket' + } + }); + + req.on('response', (res) => { + assert.strictEqual(res.statusCode, 400); + wss.close(done); + }); + + req.end(); + }); + + wss.on('wsClientError', (err, socket, request) => { + assert.ok(err instanceof Error); + assert.strictEqual(err.message, 'Invalid HTTP method'); + + assert.ok(request instanceof http.IncomingMessage); + assert.strictEqual(request.method, 'POST'); + + socket.end('HTTP/1.1 400 Bad Request\r\n\r\n'); + }); + + wss.on('connection', () => { + done(new Error("Unexpected 'connection' event")); + }); + }); + + it('fails if the WebSocket server is closing or closed', (done) => { + const server = http.createServer(); + const wss = new WebSocket.Server({ noServer: true }); + + server.on('upgrade', (req, socket, head) => { + wss.close(); + wss.handleUpgrade(req, socket, head, () => { + done(new Error('Unexpected callback invocation')); + }); + }); + + server.listen(0, () => { + const ws = new WebSocket(`ws://localhost:${server.address().port}`); + + ws.on('unexpected-response', (req, res) => { + assert.strictEqual(res.statusCode, 503); + res.resume(); + server.close(done); + }); + }); + }); + + it('handles unsupported extensions', (done) => { + const wss = new WebSocket.Server( + { + perMessageDeflate: true, + port: 0 + }, + () => { + const req = http.get({ + port: wss.address().port, + headers: { + Connection: 'Upgrade', + Upgrade: 'websocket', + 'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==', + 'Sec-WebSocket-Version': 13, + 'Sec-WebSocket-Extensions': 'foo; bar' + } + }); + + req.on('upgrade', (res, socket, head) => { + if (head.length) socket.unshift(head); + + socket.once('data', (chunk) => { + assert.strictEqual(chunk[0], 0x88); + socket.destroy(); + wss.close(done); + }); + }); + } + ); + + wss.on('connection', (ws) => { + assert.strictEqual(ws.extensions, ''); + ws.close(); + }); + }); + + describe('`verifyClient`', () => { + it('can reject client synchronously', (done) => { + const wss = new WebSocket.Server( + { + verifyClient: () => false, + port: 0 + }, + () => { + const req = http.get({ + port: wss.address().port, + headers: { + Connection: 'Upgrade', + Upgrade: 'websocket', + 'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==', + 'Sec-WebSocket-Version': 8 + } + }); + + req.on('response', (res) => { + assert.strictEqual(res.statusCode, 401); + wss.close(done); + }); + } + ); + + wss.on('connection', () => { + done(new Error("Unexpected 'connection' event")); + }); + }); + + it('can accept client synchronously', (done) => { + const server = https.createServer({ + cert: fs.readFileSync('test/fixtures/certificate.pem'), + key: fs.readFileSync('test/fixtures/key.pem') + }); + + const wss = new WebSocket.Server({ + verifyClient: (info) => { + assert.strictEqual(info.origin, 'https://example.com'); + assert.strictEqual(info.req.headers.foo, 'bar'); + assert.ok(info.secure, true); + return true; + }, + server + }); + + wss.on('connection', () => { + server.close(done); + }); + + server.listen(0, () => { + const ws = new WebSocket(`wss://localhost:${server.address().port}`, { + headers: { Origin: 'https://example.com', foo: 'bar' }, + rejectUnauthorized: false + }); + + ws.on('open', ws.close); + }); + }); + + it('can accept client asynchronously', (done) => { + const wss = new WebSocket.Server( + { + verifyClient: (o, cb) => process.nextTick(cb, true), + port: 0 + }, + () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + + ws.on('open', ws.close); + } + ); + + wss.on('connection', () => wss.close(done)); + }); + + it('can reject client asynchronously', (done) => { + const wss = new WebSocket.Server( + { + verifyClient: (info, cb) => process.nextTick(cb, false), + port: 0 + }, + () => { + const req = http.get({ + port: wss.address().port, + headers: { + Connection: 'Upgrade', + Upgrade: 'websocket', + 'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==', + 'Sec-WebSocket-Version': 8 + } + }); + + req.on('response', (res) => { + assert.strictEqual(res.statusCode, 401); + wss.close(done); + }); + } + ); + + wss.on('connection', () => { + done(new Error("Unexpected 'connection' event")); + }); + }); + + it('can reject client asynchronously w/ status code', (done) => { + const wss = new WebSocket.Server( + { + verifyClient: (info, cb) => process.nextTick(cb, false, 404), + port: 0 + }, + () => { + const req = http.get({ + port: wss.address().port, + headers: { + Connection: 'Upgrade', + Upgrade: 'websocket', + 'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==', + 'Sec-WebSocket-Version': 8 + } + }); + + req.on('response', (res) => { + assert.strictEqual(res.statusCode, 404); + wss.close(done); + }); + } + ); + + wss.on('connection', () => { + done(new Error("Unexpected 'connection' event")); + }); + }); + + it('can reject client asynchronously w/ custom headers', (done) => { + const wss = new WebSocket.Server( + { + verifyClient: (info, cb) => { + process.nextTick(cb, false, 503, '', { 'Retry-After': 120 }); + }, + port: 0 + }, + () => { + const req = http.get({ + port: wss.address().port, + headers: { + Connection: 'Upgrade', + Upgrade: 'websocket', + 'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==', + 'Sec-WebSocket-Version': 8 + } + }); + + req.on('response', (res) => { + assert.strictEqual(res.statusCode, 503); + assert.strictEqual(res.headers['retry-after'], '120'); + wss.close(done); + }); + } + ); + + wss.on('connection', () => { + done(new Error("Unexpected 'connection' event")); + }); + }); + }); + + it("doesn't emit the 'connection' event if socket is closed prematurely", (done) => { + const server = http.createServer(); + + server.listen(0, () => { + const wss = new WebSocket.Server({ + verifyClient: ({ req: { socket } }, cb) => { + assert.strictEqual(socket.readable, true); + assert.strictEqual(socket.writable, true); + + socket.on('end', () => { + assert.strictEqual(socket.readable, false); + assert.strictEqual(socket.writable, true); + cb(true); + }); + }, + server + }); + + wss.on('connection', () => { + done(new Error("Unexpected 'connection' event")); + }); + + const socket = net.connect( + { + port: server.address().port, + allowHalfOpen: true + }, + () => { + socket.end( + [ + 'GET / HTTP/1.1', + 'Host: localhost', + 'Upgrade: websocket', + 'Connection: Upgrade', + 'Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==', + 'Sec-WebSocket-Version: 13', + '\r\n' + ].join('\r\n') + ); + } + ); + + socket.on('end', () => { + wss.close(); + server.close(done); + }); + }); + }); + + it('handles data passed along with the upgrade request', (done) => { + const wss = new WebSocket.Server({ port: 0 }, () => { + const req = http.request({ + port: wss.address().port, + headers: { + Connection: 'Upgrade', + Upgrade: 'websocket', + 'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==', + 'Sec-WebSocket-Version': 13 + } + }); + + const list = Sender.frame(Buffer.from('Hello'), { + fin: true, + rsv1: false, + opcode: 0x01, + mask: true, + readOnly: false + }); + + req.write(Buffer.concat(list)); + req.end(); + }); + + wss.on('connection', (ws) => { + ws.on('message', (data, isBinary) => { + assert.deepStrictEqual(data, Buffer.from('Hello')); + assert.ok(!isBinary); + wss.close(done); + }); + }); + }); + + describe('`handleProtocols`', () => { + it('allows to select a subprotocol', (done) => { + const handleProtocols = (protocols, request) => { + assert.ok(request instanceof http.IncomingMessage); + assert.strictEqual(request.url, '/'); + return Array.from(protocols).pop(); + }; + const wss = new WebSocket.Server({ handleProtocols, port: 0 }, () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`, [ + 'foo', + 'bar' + ]); + + ws.on('open', () => { + assert.strictEqual(ws.protocol, 'bar'); + wss.close(done); + }); + }); + + wss.on('connection', (ws) => { + ws.close(); + }); + }); + }); + + it("emits the 'headers' event", (done) => { + const wss = new WebSocket.Server({ port: 0 }, () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + + ws.on('open', ws.close); + }); + + wss.on('headers', (headers, request) => { + assert.deepStrictEqual(headers.slice(0, 3), [ + 'HTTP/1.1 101 Switching Protocols', + 'Upgrade: websocket', + 'Connection: Upgrade' + ]); + assert.ok(request instanceof http.IncomingMessage); + assert.strictEqual(request.url, '/'); + + wss.on('connection', () => wss.close(done)); + }); + }); + }); + + describe('permessage-deflate', () => { + it('is disabled by default', (done) => { + const wss = new WebSocket.Server({ port: 0 }, () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + + ws.on('open', ws.close); + }); + + wss.on('connection', (ws, req) => { + assert.strictEqual( + req.headers['sec-websocket-extensions'], + 'permessage-deflate; client_max_window_bits' + ); + assert.strictEqual(ws.extensions, ''); + wss.close(done); + }); + }); + + it('uses configuration options', (done) => { + const wss = new WebSocket.Server( + { + perMessageDeflate: { clientMaxWindowBits: 8 }, + port: 0 + }, + () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + + ws.on('upgrade', (res) => { + assert.strictEqual( + res.headers['sec-websocket-extensions'], + 'permessage-deflate; client_max_window_bits=8' + ); + + wss.close(done); + }); + } + ); + + wss.on('connection', (ws) => { + ws.close(); + }); + }); + }); +}); |