diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 000000000..25fbf5a1c --- /dev/null +++ b/.eslintignore @@ -0,0 +1,2 @@ +node_modules/ +coverage/ diff --git a/.eslintrc.yaml b/.eslintrc.yaml new file mode 100644 index 000000000..2524511f9 --- /dev/null +++ b/.eslintrc.yaml @@ -0,0 +1,8 @@ +extends: standard +env: + mocha: true +rules: + no-extra-semi: error + semi: + - error + - always diff --git a/.gitignore b/.gitignore index 182c7b910..1f0951bf2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,5 @@ +node_modules/ +.nyc_output/ +coverage/ +.vscode/ npm-debug.log -node_modules -.*.swp -.lock-* -build -coverage - -builderror.log diff --git a/.npmignore b/.npmignore deleted file mode 100644 index 1eba800f8..000000000 --- a/.npmignore +++ /dev/null @@ -1,11 +0,0 @@ -npm-debug.log -node_modules -.*.swp -.lock-* -build - -bench -doc -examples -test - diff --git a/.travis.yml b/.travis.yml index 5002b4984..0ec5464e1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,15 +1,10 @@ language: node_js sudo: false node_js: - - "5" + - "10" + - "9" + - "8" + - "6" - "4" - - "0.12" -addons: - apt: - sources: - - ubuntu-toolchain-r-test - packages: - - gcc-4.9 - - g++-4.9 -before_install: - - export CC="gcc-4.9" CXX="g++-4.9" +after_success: + - "npm install coveralls@3 && nyc report --reporter=text-lcov | coveralls" diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md new file mode 100644 index 000000000..f75647683 --- /dev/null +++ b/ISSUE_TEMPLATE.md @@ -0,0 +1,38 @@ + + +- [ ] I've searched for any related issues and avoided creating a duplicate issue. + +#### Description + + + +#### Reproducible in: + +version: +Node.js version(s): +OS version(s): + +#### Steps to reproduce: + +1. +2. +3. + +### Expected result: + + + +### Actual result: + + + +### Attachments: + + diff --git a/LICENSE b/LICENSE new file mode 100644 index 000000000..a145cd1df --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2011 Einar Otto Stangvik + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile deleted file mode 100644 index 94612c5ce..000000000 --- a/Makefile +++ /dev/null @@ -1,45 +0,0 @@ -ALL_TESTS = $(shell find test/ -name '*.test.js') -ALL_INTEGRATION = $(shell find test/ -name '*.integration.js') - -run-tests: - @./node_modules/.bin/mocha \ - -t 5000 \ - -s 2400 \ - $(TESTFLAGS) \ - $(TESTS) - -run-integrationtests: - @./node_modules/.bin/mocha \ - -t 5000 \ - -s 6000 \ - $(TESTFLAGS) \ - $(TESTS) - -run-coverage: - @./node_modules/.bin/istanbul cover --report html \ - ./node_modules/.bin/_mocha -- \ - -t 5000 \ - -s 6000 \ - $(TESTFLAGS) \ - $(TESTS) - -test: - @$(MAKE) NODE_TLS_REJECT_UNAUTHORIZED=0 NODE_PATH=lib TESTS="$(ALL_TESTS)" run-tests - -integrationtest: - @$(MAKE) NODE_TLS_REJECT_UNAUTHORIZED=0 NODE_PATH=lib TESTS="$(ALL_INTEGRATION)" run-integrationtests - -coverage: - @$(MAKE) NODE_TLS_REJECT_UNAUTHORIZED=0 NODE_PATH=lib TESTS="$(ALL_TESTS)" run-coverage - -benchmark: - @node bench/sender.benchmark.js - @node bench/parser.benchmark.js - -autobahn: - @NODE_PATH=lib node test/autobahn.js - -autobahn-server: - @NODE_PATH=lib node test/autobahn-server.js - -.PHONY: test coverage diff --git a/README.md b/README.md index 93106d7a3..3fd9a8cc0 100644 --- a/README.md +++ b/README.md @@ -1,86 +1,181 @@ -# ws: a node.js websocket library - -[![Build Status](https://travis-ci.org/websockets/ws.svg?branch=master)](https://travis-ci.org/websockets/ws) - -`ws` is a simple to use WebSocket implementation, up-to-date against RFC-6455, -and [probably the fastest WebSocket library for node.js][archive]. - -Passes the quite extensive Autobahn test suite. See http://websockets.github.com/ws -for the full reports. +# ws: a Node.js WebSocket library + +[![Version npm](https://img.shields.io/npm/v/ws.svg)](https://www.npmjs.com/package/ws) +[![Linux Build](https://img.shields.io/travis/websockets/ws/master.svg)](https://travis-ci.org/websockets/ws) +[![Windows Build](https://ci.appveyor.com/api/projects/status/github/websockets/ws?branch=master&svg=true)](https://ci.appveyor.com/project/lpinca/ws) +[![Coverage Status](https://img.shields.io/coveralls/websockets/ws/master.svg)](https://coveralls.io/r/websockets/ws?branch=master) + +ws is a simple to use, blazing fast, and thoroughly tested WebSocket client +and server implementation. + +Passes the quite extensive Autobahn test suite: [server][server-report], +[client][client-report]. + +**Note**: This module does not work in the browser. The client in the docs is a +reference to a back end with the role of a client in the WebSocket +communication. Browser clients must use the native +[`WebSocket`](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) object. +To make the same code work seamlessly on Node.js and the browser, you can use +one of the many wrappers available on npm, like +[isomorphic-ws](https://github.com/heineiuo/isomorphic-ws). + +## Table of Contents + +* [Protocol support](#protocol-support) +* [Installing](#installing) + + [Opt-in for performance and spec compliance](#opt-in-for-performance-and-spec-compliance) +* [API docs](#api-docs) +* [WebSocket compression](#websocket-compression) +* [Usage examples](#usage-examples) + + [Sending and receiving text data](#sending-and-receiving-text-data) + + [Sending binary data](#sending-binary-data) + + [Simple server](#simple-server) + + [External HTTP/S server](#external-https-server) + + [Multiple servers sharing a single HTTP/S server](#multiple-servers-sharing-a-single-https-server) + + [Server broadcast](#server-broadcast) + + [echo.websocket.org demo](#echowebsocketorg-demo) + + [Other examples](#other-examples) +* [Error handling best practices](#error-handling-best-practices) +* [FAQ](#faq) + + [How to get the IP address of the client?](#how-to-get-the-ip-address-of-the-client) + + [How to detect and close broken connections?](#how-to-detect-and-close-broken-connections) + + [How to connect via a proxy?](#how-to-connect-via-a-proxy) +* [Changelog](#changelog) +* [License](#license) ## Protocol support -* **Hixie draft 76** (Old and deprecated, but still in use by Safari and Opera. - Added to ws version 0.4.2, but server only. Can be disabled by setting the - `disableHixie` option to true.) * **HyBi drafts 07-12** (Use the option `protocolVersion: 8`) * **HyBi drafts 13-17** (Current default, alternatively option `protocolVersion: 13`) -### Installing +## Installing ``` npm install --save ws ``` -### Opt-in for performance +### Opt-in for performance and spec compliance + +There are 2 optional modules that can be installed along side with the ws +module. These modules are binary addons which improve certain operations. +Prebuilt binaries are available for the most popular platforms so you don't +necessarily need to have a C++ compiler installed on your machine. + +- `npm install --save-optional bufferutil`: Allows to efficiently perform + operations such as masking and unmasking the data payload of the WebSocket + frames. +- `npm install --save-optional utf-8-validate`: Allows to efficiently check + if a message contains valid UTF-8 as required by the spec. + +## API docs + +See [`/doc/ws.md`](./doc/ws.md) for Node.js-like docs for the ws classes. + +## WebSocket compression + +ws supports the [permessage-deflate extension][permessage-deflate] which +enables the client and server to negotiate a compression algorithm and its +parameters, and then selectively apply it to the data payloads of each +WebSocket message. + +The extension is disabled by default on the server and enabled by default on +the client. It adds a significant overhead in terms of performance and memory +consumption so we suggest to enable it only if it is really needed. + +Note that Node.js has a variety of issues with high-performance compression, +where increased concurrency, especially on Linux, can lead to +[catastrophic memory fragmentation][node-zlib-bug] and slow performance. +If you intend to use permessage-deflate in production, it is worthwhile to set +up a test representative of your workload and ensure Node.js/zlib will handle +it with acceptable performance and memory usage. + +Tuning of permessage-deflate can be done via the options defined below. You can +also use `zlibDeflateOptions` and `zlibInflateOptions`, which is passed directly +into the creation of [raw deflate/inflate streams][node-zlib-deflaterawdocs]. + +See [the docs][ws-server-options] for more options. + +```js +const WebSocket = require('ws'); + +const wss = new WebSocket.Server({ + port: 8080, + perMessageDeflate: { + zlibDeflateOptions: { // See zlib defaults. + chunkSize: 1024, + memLevel: 7, + level: 3, + }, + zlibInflateOptions: { + chunkSize: 10 * 1024 + }, + // Other options settable: + clientNoContextTakeover: true, // Defaults to negotiated value. + serverNoContextTakeover: true, // Defaults to negotiated value. + clientMaxWindowBits: 10, // Defaults to negotiated value. + serverMaxWindowBits: 10, // Defaults to negotiated value. + // Below options specified as default values. + concurrencyLimit: 10, // Limits zlib concurrency for perf. + threshold: 1024, // Size (in bytes) below which messages + // should not be compressed. + } +}); +``` + +The client will only use the extension if it is supported and enabled on the +server. To always disable the extension on the client set the +`perMessageDeflate` option to `false`. -There are 2 optional modules that can be installed along side with the `ws` -module. These modules are binary addons which improve certain operations, but as -they are binary addons they require compilation which can fail if no c++ -compiler is installed on the host system. +```js +const WebSocket = require('ws'); -- `npm install --save bufferutil`: Improves internal buffer operations which - allows for faster processing of masked WebSocket frames and general buffer - operations. -- `npm install --save utf-8-validate`: The specification requires validation of - invalid UTF-8 chars, some of these validations could not be done in JavaScript - hence the need for a binary addon. In most cases you will already be - validating the input that you receive for security purposes leading to double - validation. But if you want to be 100% spec-conforming and have fast - validation of UTF-8 then this module is a must. +const ws = new WebSocket('ws://www.host.com/path', { + perMessageDeflate: false +}); +``` + +## Usage examples ### Sending and receiving text data ```js -var WebSocket = require('ws'); -var ws = new WebSocket('ws://www.host.com/path'); +const WebSocket = require('ws'); + +const ws = new WebSocket('ws://www.host.com/path'); ws.on('open', function open() { ws.send('something'); }); -ws.on('message', function(data, flags) { - // flags.binary will be set if a binary data is received. - // flags.masked will be set if the data was masked. +ws.on('message', function incoming(data) { + console.log(data); }); ``` ### Sending binary data ```js -var WebSocket = require('ws'); -var ws = new WebSocket('ws://www.host.com/path'); +const WebSocket = require('ws'); + +const ws = new WebSocket('ws://www.host.com/path'); ws.on('open', function open() { - var array = new Float32Array(5); + const array = new Float32Array(5); for (var i = 0; i < array.length; ++i) { array[i] = i / 2; } - ws.send(array, { binary: true, mask: true }); + ws.send(array); }); ``` -Setting `mask`, as done for the send options above, will cause the data to be -masked according to the WebSocket protocol. The same option applies for text -data. - -### Server example +### Simple server ```js -var WebSocketServer = require('ws').Server - , wss = new WebSocketServer({ port: 8080 }); +const WebSocket = require('ws'); + +const wss = new WebSocket.Server({ port: 8080 }); wss.on('connection', function connection(ws) { ws.on('message', function incoming(message) { @@ -91,26 +186,20 @@ wss.on('connection', function connection(ws) { }); ``` -### ExpressJS example +### External HTTP/S server ```js -var server = require('http').createServer() - , url = require('url') - , WebSocketServer = require('ws').Server - , wss = new WebSocketServer({ server: server }) - , express = require('express') - , app = express() - , port = 4080; +const fs = require('fs'); +const https = require('https'); +const WebSocket = require('ws'); -app.use(function (req, res) { - res.send({ msg: "hello" }); +const server = new https.createServer({ + cert: fs.readFileSync('/path/to/cert.pem'), + key: fs.readFileSync('/path/to/key.pem') }); +const wss = new WebSocket.Server({ server }); wss.on('connection', function connection(ws) { - var location = url.parse(ws.upgradeReq.url, true); - // you might use location.query.access_token to authenticate or share sessions - // or ws.upgradeReq.headers.cookie (see http://stackoverflow.com/a/16395220/151312) - ws.on('message', function incoming(message) { console.log('received: %s', message); }); @@ -118,24 +207,109 @@ wss.on('connection', function connection(ws) { ws.send('something'); }); -server.on('request', app); -server.listen(port, function () { console.log('Listening on ' + server.address().port) }); +server.listen(8080); ``` -### Server sending broadcast data +### Multiple servers sharing a single HTTP/S server ```js -var WebSocketServer = require('ws').Server - , wss = new WebSocketServer({ port: 8080 }); +const http = require('http'); +const WebSocket = require('ws'); + +const server = http.createServer(); +const wss1 = new WebSocket.Server({ noServer: true }); +const wss2 = new WebSocket.Server({ noServer: true }); + +wss1.on('connection', function connection(ws) { + // ... +}); + +wss2.on('connection', function connection(ws) { + // ... +}); + +server.on('upgrade', function upgrade(request, socket, head) { + const pathname = url.parse(request.url).pathname; + + if (pathname === '/foo') { + wss1.handleUpgrade(request, socket, head, function done(ws) { + wss1.emit('connection', ws, request); + }); + } else if (pathname === '/bar') { + wss2.handleUpgrade(request, socket, head, function done(ws) { + wss2.emit('connection', ws, request); + }); + } else { + socket.destroy(); + } +}); + +server.listen(8080); +``` + +### Server broadcast +```js +const WebSocket = require('ws'); + +const wss = new WebSocket.Server({ port: 8080 }); + +// Broadcast to all. wss.broadcast = function broadcast(data) { wss.clients.forEach(function each(client) { - client.send(data); + if (client.readyState === WebSocket.OPEN) { + client.send(data); + } }); }; + +wss.on('connection', function connection(ws) { + ws.on('message', function incoming(data) { + // Broadcast to everyone else. + wss.clients.forEach(function each(client) { + if (client !== ws && client.readyState === WebSocket.OPEN) { + client.send(data); + } + }); + }); +}); ``` -### Error handling best practices +### echo.websocket.org demo + +```js +const WebSocket = require('ws'); + +const ws = new WebSocket('wss://echo.websocket.org/', { + origin: 'https://websocket.org' +}); + +ws.on('open', function open() { + console.log('connected'); + ws.send(Date.now()); +}); + +ws.on('close', function close() { + console.log('disconnected'); +}); + +ws.on('message', function incoming(data) { + console.log(`Roundtrip time: ${Date.now() - data} ms`); + + setTimeout(function timeout() { + ws.send(Date.now()); + }, 500); +}); +``` + +### Other examples + +For a full example with a browser client communicating with a ws server, see the +examples folder. + +Otherwise, see the test cases. + +## Error handling best practices ```js // If the WebSocket is closed before the following send is attempted @@ -145,91 +319,99 @@ ws.send('something'); // callback. The callback is also the only way of being notified that data has // actually been sent. ws.send('something', function ack(error) { - // if error is not defined, the send has been completed, - // otherwise the error object will indicate what failed. + // If error is not defined, the send has been completed, otherwise the error + // object will indicate what failed. }); -// Immediate errors can also be handled with try/catch-blocks, but **note** that +// Immediate errors can also be handled with `try...catch`, but **note** that // since sends are inherently asynchronous, socket write failures will *not* be // captured when this technique is used. try { ws.send('something'); } catch (e) { /* handle error */ } ``` -### echo.websocket.org demo +## FAQ + +### How to get the IP address of the client? + +The remote IP address can be obtained from the raw socket. ```js -var WebSocket = require('ws'); -var ws = new WebSocket('ws://echo.websocket.org/', { - protocolVersion: 8, - origin: 'http://websocket.org' -}); +const WebSocket = require('ws'); -ws.on('open', function open() { - console.log('connected'); - ws.send(Date.now().toString(), {mask: true}); -}); +const wss = new WebSocket.Server({ port: 8080 }); -ws.on('close', function close() { - console.log('disconnected'); +wss.on('connection', function connection(ws, req) { + const ip = req.connection.remoteAddress; }); +``` -ws.on('message', function message(data, flags) { - console.log('Roundtrip time: ' + (Date.now() - parseInt(data)) + 'ms', flags); +When the server runs behind a proxy like NGINX, the de-facto standard is to use +the `X-Forwarded-For` header. - setTimeout(function timeout() { - ws.send(Date.now().toString(), {mask: true}); - }, 500); +```js +wss.on('connection', function connection(ws, req) { + const ip = req.headers['x-forwarded-for'].split(/\s*,\s*/)[0]; }); ``` -### Other examples +### How to detect and close broken connections? -For a full example with a browser client communicating with a ws server, see the -examples folder. +Sometimes the link between the server and the client can be interrupted in a +way that keeps both the server and the client unaware of the broken state of the +connection (e.g. when pulling the cord). -Note that the usage together with Express 3.0 is quite different from Express -2.x. The difference is expressed in the two different serverstats-examples. +In these cases ping messages can be used as a means to verify that the remote +endpoint is still responsive. -Otherwise, see the test cases. +```js +const WebSocket = require('ws'); -### Running the tests +const wss = new WebSocket.Server({ port: 8080 }); -``` -make test -``` +function noop() {} -## API Docs +function heartbeat() { + this.isAlive = true; +} -See [`/doc/ws.md`](https://github.com/websockets/ws/blob/master/doc/ws.md) for Node.js-like docs for the ws classes. +wss.on('connection', function connection(ws) { + ws.isAlive = true; + ws.on('pong', heartbeat); +}); -## Changelog +const interval = setInterval(function ping() { + wss.clients.forEach(function each(ws) { + if (ws.isAlive === false) return ws.terminate(); -We're using the GitHub [`releases`](https://github.com/websockets/ws/releases) for changelog entries. + ws.isAlive = false; + ws.ping(noop); + }); +}, 30000); +``` -## License +Pong messages are automatically sent in response to ping messages as required +by the spec. -(The MIT License) +### How to connect via a proxy? -Copyright (c) 2011 Einar Otto Stangvik <einaros@gmail.com> +Use a custom `http.Agent` implementation like [https-proxy-agent][] or +[socks-proxy-agent][]. -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -'Software'), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: +## Changelog -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. +We're using the GitHub [releases][changelog] for changelog entries. -THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +## License -[archive]: http://web.archive.org/web/20130314230536/http://hobbycoding.posterous.com/the-fastest-websocket-module-for-nodejs +[MIT](LICENSE) + +[https-proxy-agent]: https://github.com/TooTallNate/node-https-proxy-agent +[socks-proxy-agent]: https://github.com/TooTallNate/node-socks-proxy-agent +[client-report]: http://websockets.github.io/ws/autobahn/clients/ +[server-report]: http://websockets.github.io/ws/autobahn/servers/ +[permessage-deflate]: https://tools.ietf.org/html/rfc7692 +[changelog]: https://github.com/websockets/ws/releases +[node-zlib-bug]: https://github.com/nodejs/node/issues/8871 +[node-zlib-deflaterawdocs]: https://nodejs.org/api/zlib.html#zlib_zlib_createdeflateraw_options +[ws-server-options]: https://github.com/websockets/ws/blob/master/doc/ws.md#new-websocketserveroptions-callback diff --git a/SECURITY.md b/SECURITY.md index fd8e07bc5..8e063cca5 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -30,4 +30,5 @@ all vulnerabilities to the [Node Security Project](https://nodesecurity.io/). ## History -04 Jan 2016: [Buffer vulnerablity](https://github.com/websockets/ws/releases/tag/1.0.1) +- 04 Jan 2016: [Buffer vulnerability](https://github.com/websockets/ws/releases/tag/1.0.1) +- 08 Nov 2017: [DoS vulnerability](https://github.com/websockets/ws/releases/tag/3.3.1) diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 000000000..f797a9607 --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,20 @@ +environment: + matrix: + - nodejs_version: "10" + - nodejs_version: "9" + - nodejs_version: "8" + - nodejs_version: "6" + - nodejs_version: "4" +platform: + - x86 + - x64 +matrix: + fast_finish: true +install: + - ps: Install-Product node $env:nodejs_version $env:platform + - npm install +test_script: + - node --version + - npm --version + - npm test +build: off diff --git a/bench/parser.benchmark.js b/bench/parser.benchmark.js index ff5f737c0..43cc1ea4e 100644 --- a/bench/parser.benchmark.js +++ b/bench/parser.benchmark.js @@ -1,115 +1,91 @@ -/*! - * ws: a node.js websocket client - * Copyright(c) 2011 Einar Otto Stangvik - * MIT Licensed - */ +'use strict'; -/** - * Benchmark dependencies. - */ +const benchmark = require('benchmark'); +const crypto = require('crypto'); -var benchmark = require('benchmark') - , Receiver = require('../').Receiver - , suite = new benchmark.Suite('Receiver'); -require('tinycolor'); -require('./util'); +const WebSocket = require('..'); -/** - * Setup receiver. - */ - -suite.on('start', function () { - receiver = new Receiver(); -}); +const Receiver = WebSocket.Receiver; +const Sender = WebSocket.Sender; -suite.on('cycle', function () { - receiver = new Receiver(); -}); +const options = { + fin: true, + rsv1: false, + mask: true, + readOnly: false +}; -/** - * Benchmarks. - */ +function createBinaryFrame (length) { + const list = Sender.frame( + crypto.randomBytes(length), + Object.assign({ opcode: 0x02 }, options) + ); -var pingMessage = 'Hello' - , pingPacket1 = getBufferFromHexString('89 ' + (pack(2, 0x80 | pingMessage.length)) + - ' 34 83 a8 68 '+ getHexStringFromBuffer(mask(pingMessage, '34 83 a8 68'))); -suite.add('ping message', function () { - receiver.add(pingPacket1); -}); + return Buffer.concat(list); +} -var pingPacket2 = getBufferFromHexString('89 00') -suite.add('ping with no data', function () { - receiver.add(pingPacket2); +const pingFrame1 = Buffer.concat(Sender.frame( + crypto.randomBytes(5), + Object.assign({ opcode: 0x09 }, options) +)); + +const textFrame = Buffer.from('819461616161' + '61'.repeat(20), 'hex'); +const pingFrame2 = Buffer.from('8900', 'hex'); +const binaryFrame1 = createBinaryFrame(125); +const binaryFrame2 = createBinaryFrame(65535); +const binaryFrame3 = createBinaryFrame(200 * 1024); +const binaryFrame4 = createBinaryFrame(1024 * 1024); + +const suite = new benchmark.Suite(); +const receiver = new Receiver(); + +suite.add('ping frame (5 bytes payload)', { + defer: true, + fn: (deferred) => { + receiver.write(pingFrame1, deferred.resolve.bind(deferred)); + } }); - -var closePacket = getBufferFromHexString('88 00'); -suite.add('close message', function () { - receiver.add(closePacket); - receiver.endPacket(); +suite.add('ping frame (no payload)', { + defer: true, + fn: (deferred) => { + receiver.write(pingFrame2, deferred.resolve.bind(deferred)); + } }); - -var maskedTextPacket = getBufferFromHexString('81 93 34 83 a8 68 01 b9 92 52 4f a1 c6 09 59 e6 8a 52 16 e6 cb 00 5b a1 d5'); -suite.add('masked text message', function () { - receiver.add(maskedTextPacket); +suite.add('text frame (20 bytes payload)', { + defer: true, + fn: (deferred) => { + receiver.write(textFrame, deferred.resolve.bind(deferred)); + } }); - -binaryDataPacket = (function() { - var length = 125 - , message = new Buffer(length) - for (var i = 0; i < length; ++i) message[i] = i % 10; - return getBufferFromHexString('82 ' + getHybiLengthAsHexString(length, true) + ' 34 83 a8 68 ' - + getHexStringFromBuffer(mask(message), '34 83 a8 68')); -})(); -suite.add('binary data (125 bytes)', function () { - try { - receiver.add(binaryDataPacket); - +suite.add('binary frame (125 bytes payload)', { + defer: true, + fn: (deferred) => { + receiver.write(binaryFrame1, deferred.resolve.bind(deferred)); } - catch(e) {console.log(e)} }); - -binaryDataPacket2 = (function() { - var length = 65535 - , message = new Buffer(length) - for (var i = 0; i < length; ++i) message[i] = i % 10; - return getBufferFromHexString('82 ' + getHybiLengthAsHexString(length, true) + ' 34 83 a8 68 ' - + getHexStringFromBuffer(mask(message), '34 83 a8 68')); -})(); -suite.add('binary data (65535 bytes)', function () { - receiver.add(binaryDataPacket2); +suite.add('binary frame (65535 bytes payload)', { + defer: true, + fn: (deferred) => { + receiver.write(binaryFrame2, deferred.resolve.bind(deferred)); + } }); - -binaryDataPacket3 = (function() { - var length = 200*1024 - , message = new Buffer(length) - for (var i = 0; i < length; ++i) message[i] = i % 10; - return getBufferFromHexString('82 ' + getHybiLengthAsHexString(length, true) + ' 34 83 a8 68 ' - + getHexStringFromBuffer(mask(message), '34 83 a8 68')); -})(); -suite.add('binary data (200 kB)', function () { - receiver.add(binaryDataPacket3); +suite.add('binary frame (200 KiB payload)', { + defer: true, + fn: (deferred) => { + receiver.write(binaryFrame3, deferred.resolve.bind(deferred)); + } }); - -/** - * Output progress. - */ - -suite.on('cycle', function (bench, details) { - console.log('\n ' + suite.name.grey, details.name.white.bold); - console.log(' ' + [ - details.hz.toFixed(2).cyan + ' ops/sec'.grey - , details.count.toString().white + ' times executed'.grey - , 'benchmark took '.grey + details.times.elapsed.toString().white + ' sec.'.grey - , - ].join(', '.grey)); +suite.add('binary frame (1 MiB payload)', { + defer: true, + fn: (deferred) => { + receiver.write(binaryFrame4, deferred.resolve.bind(deferred)); + } }); -/** - * Run/export benchmarks. - */ +suite.on('cycle', (e) => console.log(e.target.toString())); -if (!module.parent) { - suite.run(); +if (require.main === module) { + suite.run({ async: true }); } else { module.exports = suite; } diff --git a/bench/sender.benchmark.js b/bench/sender.benchmark.js index 20c171a50..89d3be24b 100644 --- a/bench/sender.benchmark.js +++ b/bench/sender.benchmark.js @@ -1,66 +1,48 @@ -/*! - * ws: a node.js websocket client - * Copyright(c) 2011 Einar Otto Stangvik - * MIT Licensed - */ - -/** - * Benchmark dependencies. - */ - -var benchmark = require('benchmark') - , Sender = require('../').Sender - , suite = new benchmark.Suite('Sender'); -require('tinycolor'); -require('./util'); - -/** - * Setup sender. - */ - -suite.on('start', function () { - sender = new Sender(); - sender._socket = { write: function() {} }; -}); - -suite.on('cycle', function () { - sender = new Sender(); - sender._socket = { write: function() {} }; -}); - -/** - * Benchmarks - */ - -framePacket = new Buffer(200*1024); -framePacket.fill(99); -suite.add('frameAndSend, unmasked (200 kB)', function () { - sender.frameAndSend(0x2, framePacket, true, false); -}); -suite.add('frameAndSend, masked (200 kB)', function () { - sender.frameAndSend(0x2, framePacket, true, true); -}); - -/** - * Output progress. - */ - -suite.on('cycle', function (bench, details) { - console.log('\n ' + suite.name.grey, details.name.white.bold); - console.log(' ' + [ - details.hz.toFixed(2).cyan + ' ops/sec'.grey - , details.count.toString().white + ' times executed'.grey - , 'benchmark took '.grey + details.times.elapsed.toString().white + ' sec.'.grey - , - ].join(', '.grey)); -}); - -/** - * Run/export benchmarks. - */ - -if (!module.parent) { - suite.run(); +'use strict'; + +const benchmark = require('benchmark'); +const crypto = require('crypto'); + +const Sender = require('../').Sender; + +const data1 = crypto.randomBytes(64); +const data2 = crypto.randomBytes(16 * 1024); +const data3 = crypto.randomBytes(64 * 1024); +const data4 = crypto.randomBytes(200 * 1024); +const data5 = crypto.randomBytes(1024 * 1024); + +const opts1 = { + readOnly: false, + mask: false, + rsv1: false, + opcode: 2, + fin: true +}; +const opts2 = { + readOnly: true, + rsv1: false, + mask: true, + opcode: 2, + fin: true +}; + +const suite = new benchmark.Suite(); + +suite.add('frame, unmasked (64 B)', () => Sender.frame(data1, opts1)); +suite.add('frame, masked (64 B)', () => Sender.frame(data1, opts2)); +suite.add('frame, unmasked (16 KiB)', () => Sender.frame(data2, opts1)); +suite.add('frame, masked (16 KiB)', () => Sender.frame(data2, opts2)); +suite.add('frame, unmasked (64 KiB)', () => Sender.frame(data3, opts1)); +suite.add('frame, masked (64 KiB)', () => Sender.frame(data3, opts2)); +suite.add('frame, unmasked (200 KiB)', () => Sender.frame(data4, opts1)); +suite.add('frame, masked (200 KiB)', () => Sender.frame(data4, opts2)); +suite.add('frame, unmasked (1 MiB)', () => Sender.frame(data5, opts1)); +suite.add('frame, masked (1 MiB)', () => Sender.frame(data5, opts2)); + +suite.on('cycle', (e) => console.log(e.target.toString())); + +if (require.main === module) { + suite.run({ async: true }); } else { module.exports = suite; } diff --git a/bench/speed.js b/bench/speed.js index 3ce641461..e5d1591dd 100644 --- a/bench/speed.js +++ b/bench/speed.js @@ -1,105 +1,95 @@ -var cluster = require('cluster') - , WebSocket = require('../') - , WebSocketServer = WebSocket.Server - , crypto = require('crypto') - , util = require('util') - , ansi = require('ansi'); -require('tinycolor'); - -function roundPrec(num, prec) { - var mul = Math.pow(10, prec); - return Math.round(num * mul) / mul; -} +'use strict'; -function humanSize(bytes) { - if (bytes >= 1048576) return roundPrec(bytes / 1048576, 2) + ' MB'; - if (bytes >= 1024) return roundPrec(bytes / 1024, 2) + ' kB'; - return roundPrec(bytes, 2) + ' B'; -} +const cluster = require('cluster'); -function generateRandomData(size) { - var buffer = new Buffer(size); - for (var i = 0; i < size; ++i) { - buffer[i] = ~~(Math.random() * 127); - } - return buffer; -} +const WebSocket = require('..'); + +const port = 8181; if (cluster.isMaster) { - var wss = new WebSocketServer({port: 8181}, function() { - cluster.fork(); - }); - wss.on('connection', function(ws) { - ws.on('message', function(data, flags) { - ws.send(data, {binary: flags&&flags.binary}); - }); - ws.on('close', function() {}); - }); - cluster.on('death', function(worker) { - wss.close(); + const wss = new WebSocket.Server({ + maxPayload: 600 * 1024 * 1024, + perMessageDeflate: false, + clientTracking: false, + port + }, () => cluster.fork()); + + wss.on('connection', (ws) => { + ws.on('message', (data) => ws.send(data)); }); -} -else { - var cursor = ansi(process.stdout); - var configs = [ + cluster.on('exit', () => wss.close()); +} else { + const configs = [ [true, 10000, 64], - [true, 5000, 16*1024], - [true, 1000, 128*1024], - [true, 100, 1024*1024], - [true, 1, 500*1024*1024], + [true, 5000, 16 * 1024], + [true, 1000, 128 * 1024], + [true, 100, 1024 * 1024], + [true, 1, 500 * 1024 * 1024], [false, 10000, 64], - [false, 5000, 16*1024], - [false, 1000, 128*1024], - [false, 100, 1024*1024], + [false, 5000, 16 * 1024], + [false, 1000, 128 * 1024], + [false, 100, 1024 * 1024] ]; - var largest = configs[0][1]; - for (var i = 0, l = configs.length; i < l; ++i) { - if (configs[i][2] > largest) largest = configs[i][2]; - } + const roundPrec = (num, prec) => { + const mul = Math.pow(10, prec); + return Math.round(num * mul) / mul; + }; + + const humanSize = (bytes) => { + if (bytes >= 1073741824) return roundPrec(bytes / 1073741824, 2) + ' GiB'; + if (bytes >= 1048576) return roundPrec(bytes / 1048576, 2) + ' MiB'; + if (bytes >= 1024) return roundPrec(bytes / 1024, 2) + ' KiB'; + return roundPrec(bytes, 2) + ' B'; + }; - console.log('Generating %s of test data ...', humanSize(largest)); - var randomBytes = generateRandomData(largest); + const largest = configs.reduce((prev, curr) => curr[2] > prev ? curr[2] : prev, 0); + console.log('Generating %s of test data...', humanSize(largest)); + const randomBytes = Buffer.allocUnsafe(largest); - function roundtrip(useBinary, roundtrips, size, cb) { - var data = randomBytes.slice(0, size); - var prefix = util.format('Running %d roundtrips of %s %s data', roundtrips, humanSize(size), useBinary ? 'binary' : 'text'); - console.log(prefix); - var client = new WebSocket('ws://localhost:' + '8181'); - var dt; + for (var i = 0; i < largest; ++i) { + randomBytes[i] = ~~(Math.random() * 127); + } + + const runConfig = (useBinary, roundtrips, size, cb) => { + const data = randomBytes.slice(0, size); + const ws = new WebSocket(`ws://localhost:${port}`); var roundtrip = 0; - function send() { - client.send(data, {binary: useBinary}); - } - client.on('error', function(e) { - console.error(e); - process.exit(); + var time; + + ws.on('error', (err) => { + console.error(err.stack); + cluster.worker.disconnect(); }); - client.on('open', function() { - dt = Date.now(); - send(); + ws.on('open', () => { + time = process.hrtime(); + ws.send(data, { binary: useBinary }); }); - client.on('message', function(data, flags) { - if (++roundtrip == roundtrips) { - var elapsed = Date.now() - dt; - cursor.up(); - console.log('%s:\t%ss\t%s' - , useBinary ? prefix.green : prefix.cyan - , roundPrec(elapsed / 1000, 1).toString().green.bold - , (humanSize((size * roundtrips) / elapsed * 1000) + '/s').blue.bold); - client.close(); - cb(); - return; - } - process.nextTick(send); + ws.on('message', () => { + if (++roundtrip !== roundtrips) return ws.send(data, { binary: useBinary }); + + var elapsed = process.hrtime(time); + elapsed = (elapsed[0] * 1e9) + elapsed[1]; + + console.log( + '%d roundtrips of %s %s data:\t%ss\t%s', + roundtrips, + humanSize(size), + useBinary ? 'binary' : 'text', + roundPrec(elapsed / 1e9, 1), + humanSize(size * 2 * roundtrips / elapsed * 1e9) + '/s' + ); + + ws.close(); + cb(); }); - } + }; - (function run() { - if (configs.length == 0) process.exit(); + (function run () { + if (configs.length === 0) return cluster.worker.disconnect(); var config = configs.shift(); config.push(run); - roundtrip.apply(null, config); + runConfig.apply(null, config); })(); -} \ No newline at end of file +} diff --git a/bench/util.js b/bench/util.js deleted file mode 100644 index 5f0128190..000000000 --- a/bench/util.js +++ /dev/null @@ -1,105 +0,0 @@ -/*! - * ws: a node.js websocket client - * Copyright(c) 2011 Einar Otto Stangvik - * MIT Licensed - */ - -/** - * Returns a Buffer from a "ff 00 ff"-type hex string. - */ - -getBufferFromHexString = function(byteStr) { - var bytes = byteStr.split(' '); - var buf = new Buffer(bytes.length); - for (var i = 0; i < bytes.length; ++i) { - buf[i] = parseInt(bytes[i], 16); - } - return buf; -} - -/** - * Returns a hex string from a Buffer. - */ - -getHexStringFromBuffer = function(data) { - var s = ''; - for (var i = 0; i < data.length; ++i) { - s += padl(data[i].toString(16), 2, '0') + ' '; - } - return s.trim(); -} - -/** - * Splits a buffer in two parts. - */ - -splitBuffer = function(buffer) { - var b1 = new Buffer(Math.ceil(buffer.length / 2)); - buffer.copy(b1, 0, 0, b1.length); - var b2 = new Buffer(Math.floor(buffer.length / 2)); - buffer.copy(b2, 0, b1.length, b1.length + b2.length); - return [b1, b2]; -} - -/** - * Performs hybi07+ type masking on a hex string or buffer. - */ - -mask = function(buf, maskString) { - if (typeof buf == 'string') buf = new Buffer(buf); - var mask = getBufferFromHexString(maskString || '34 83 a8 68'); - for (var i = 0; i < buf.length; ++i) { - buf[i] ^= mask[i % 4]; - } - return buf; -} - -/** - * Returns a hex string representing the length of a message - */ - -getHybiLengthAsHexString = function(len, masked) { - if (len < 126) { - var buf = new Buffer(1); - buf[0] = (masked ? 0x80 : 0) | len; - } - else if (len < 65536) { - var buf = new Buffer(3); - buf[0] = (masked ? 0x80 : 0) | 126; - getBufferFromHexString(pack(4, len)).copy(buf, 1); - } - else { - var buf = new Buffer(9); - buf[0] = (masked ? 0x80 : 0) | 127; - getBufferFromHexString(pack(16, len)).copy(buf, 1); - } - return getHexStringFromBuffer(buf); -} - -/** - * Unpacks a Buffer into a number. - */ - -unpack = function(buffer) { - var n = 0; - for (var i = 0; i < buffer.length; ++i) { - n = (i == 0) ? buffer[i] : (n * 256) + buffer[i]; - } - return n; -} - -/** - * Returns a hex string, representing a specific byte count 'length', from a number. - */ - -pack = function(length, number) { - return padl(number.toString(16), length, '0').replace(/([0-9a-f][0-9a-f])/gi, '$1 ').trim(); -} - -/** - * Left pads the string 's' to a total length of 'n' with char 'c'. - */ - -padl = function(s, n, c) { - return new Array(1 + n - s.length).join(c) + s; -} diff --git a/doc/ws.md b/doc/ws.md index 25d08fec8..47d8abeda 100644 --- a/doc/ws.md +++ b/doc/ws.md @@ -1,234 +1,421 @@ # ws -## Class: ws.Server +## Class: WebSocket.Server + +This class represents a WebSocket server. It extends the `EventEmitter`. + +### new WebSocket.Server(options[, callback]) + +- `options` {Object} + - `host` {String} The hostname where to bind the server. + - `port` {Number} The port where to bind the server. + - `backlog` {Number} The maximum length of the queue of pending connections. + - `server` {http.Server|https.Server} A pre-created Node.js HTTP/S server. + - `verifyClient` {Function} A function which can be used to validate incoming + connections. See description below. + - `handleProtocols` {Function} A function which can be used to handle the + WebSocket subprotocols. See description below. + - `path` {String} Accept only connections matching this path. + - `noServer` {Boolean} Enable no server mode. + - `clientTracking` {Boolean} Specifies whether or not to track clients. + - `perMessageDeflate` {Boolean|Object} Enable/disable permessage-deflate. + - `maxPayload` {Number} The maximum allowed message size in bytes. +- `callback` {Function} + +Create a new server instance. One of `port`, `server` or `noServer` must be +provided or an error is thrown. An HTTP server is automatically created, +started, and used if `port` is set. To use an external HTTP/S server instead, +specify only `server` or `noServer`. In this case the HTTP/S server must be +started manually. The "noServer" mode allows the WebSocket server to be +completly detached from the HTTP/S server. This makes it possible, for example, +to share a single HTTP/S server between multiple WebSocket servers. + + +If `verifyClient` is not set then the handshake is automatically accepted. If +it is provided with a single argument then that is: + +- `info` {Object} + - `origin` {String} The value in the Origin header indicated by the client. + - `req` {http.IncomingMessage} The client HTTP GET request. + - `secure` {Boolean} `true` if `req.connection.authorized` or + `req.connection.encrypted` is set. + +The return value (Boolean) of the function determines whether or not to accept +the handshake. + +if `verifyClient` is provided with two arguments then those are: + +- `info` {Object} Same as above. +- `cb` {Function} A callback that must be called by the user upon inspection + of the `info` fields. Arguments in this callback are: + - `result` {Boolean} Whether or not to accept the handshake. + - `code` {Number} When `result` is `false` this field determines the HTTP + error status code to be sent to the client. + - `name` {String} When `result` is `false` this field determines the HTTP + reason phrase. + - `headers` {Object} When `result` is `false` this field determines additional + HTTP headers to be sent to the client. For example, `{ 'Retry-After': 120 }`. + + +`handleProtocols` takes two arguments: + +- `protocols` {Array} The list of WebSocket subprotocols indicated by the + client in the `Sec-WebSocket-Protocol` header. +- `request` {http.IncomingMessage} The client HTTP GET request. + +The returned value sets the value of the `Sec-WebSocket-Protocol` header in the +HTTP 101 response. If returned value is `false` the header is not added in the +response. + +If `handleProtocols` is not set then the first of the client's requested +subprotocols is used. + + +`perMessageDeflate` can be used to control the behavior of +[permessage-deflate extension][permessage-deflate]. +The extension is disabled when `false` (default value). If an object is +provided then that is extension parameters: + +- `serverNoContextTakeover` {Boolean} Whether to use context takeover or not. +- `clientNoContextTakeover` {Boolean} Acknowledge disabling of client context + takeover. +- `serverMaxWindowBits` {Number} The value of `windowBits`. +- `clientMaxWindowBits` {Number} Request a custom client window size. +- `zlibDeflateOptions` {Object} [Additional options][zlib-options] to pass to + zlib on deflate. +- `zlibInflateOptions` {Object} [Additional options][zlib-options] to pass to + zlib on inflate. +- `threshold` {Number} Payloads smaller than this will not be compressed. + Defaults to 1024 bytes. +- `concurrencyLimit` {Number} The number of concurrent calls to zlib. + Calls above this limit will be queued. Default 10. You usually won't + need to touch this option. See [this issue][concurrency-limit] for more + details. + +If a property is empty then either an offered configuration or a default value +is used. +When sending a fragmented message the length of the first fragment is compared +to the threshold. This determines if compression is used for the entire message. + + +`callback` will be added as a listener for the `listening` event on the HTTP +server when not operating in "noServer" mode. -This class is a WebSocket server. It is an `EventEmitter`. - -### new ws.Server([options], [callback]) +### Event: 'connection' -* `options` Object - * `host` String - * `port` Number - * `server` http.Server - * `verifyClient` Function - * `handleProtocols` Function - * `path` String - * `noServer` Boolean - * `disableHixie` Boolean - * `clientTracking` Boolean - * `perMessageDeflate` Boolean|Object -* `callback` Function +- `socket` {WebSocket} +- `request` {http.IncomingMessage} -Construct a new server object. +Emitted when the handshake is complete. `request` is the http GET request sent +by the client. Useful for parsing authority headers, cookie headers, and other +information. -Either `port` or `server` must be provided, otherwise you might enable -`noServer` if you want to pass the requests directly. Please note that the -`callback` is only used when you supply the a `port` number in the options. +### Event: 'error' -### options.verifyClient +- `error` {Error} -`verifyClient` can be used in two different ways. If it is provided with two arguments then those are: -* `info` Object: - * `origin` String: The value in the Origin header indicated by the client. - * `req` http.ClientRequest: The client HTTP GET request. - * `secure` Boolean: `true` if `req.connection.authorized` or `req.connection.encrypted` is set. -* `cb` Function: A callback that must be called by the user upon inspection of the `info` fields. Arguments in this callback are: - * `result` Boolean: Whether the user accepts or not the handshake. - * `code` Number: If `result` is `false` this field determines the HTTP error status code to be sent to the client. - * `name` String: If `result` is `false` this field determines the HTTP reason phrase. +Emitted when an error occurs on the underlying server. -If `verifyClient` is provided with a single argument then that is: -* `info` Object: Same as above. +### Event: 'headers' -In this case the return code (Boolean) of the function determines whether the handshake is accepted or not. +- `headers` {Array} +- `request` {http.IncomingMessage} -If `verifyClient` is not set then the handshake is automatically accepted. +Emitted before the response headers are written to the socket as part of the +handshake. This allows you to inspect/modify the headers before they are sent. -### options.handleProtocols +### Event: 'listening' -`handleProtocols` receives two arguments: -* `protocols` Array: The list of WebSocket sub-protocols indicated by the client in the Sec-WebSocket-Protocol header. -* `cb` Function: A callback that must be called by the user upon inspection of the protocols. Arguments in this callback are: - * `result` Boolean: Whether the user accepts or not the handshake. - * `protocol` String: If `result` is `true` then this field sets the value of the Sec-WebSocket-Protocol header in the HTTP 101 response. +Emitted when the underlying server has been bound. -If `handleProtocols` is not set then the handshake is accepted regardless the value of Sec-WebSocket-Protocol header. If it is set but the user does not invoke the `cb` callback then the handshake is rejected with error HTTP 501. +### server.clients -### options.perMessageDeflate +- {Set} -`perMessageDeflate` can be used to control the behavior of [permessage-deflate extension](https://tools.ietf.org/html/draft-ietf-hybi-permessage-compression-19). The extension is disabled when `false`. Defaults to `true`. If an object is provided then that is extension parameters: +A set that stores all connected clients. Please note that this property is only +added when the `clientTracking` is truthy. -* `serverNoContextTakeover` Boolean: Whether to use context take over or not. -* `clientNoContextTakeover` Boolean: The value to be requested to clients whether to use context take over or not. -* `serverMaxWindowBits` Number: The value of windowBits. -* `clientMaxWindowBits` Number: The value of max windowBits to be requested to clients. -* `memLevel` Number: The value of memLevel. +### server.address() -If a property is empty then either an offered configuration or a default value is used. +Returns an object with `port`, `family`, and `address` properties specifying +the bound address, the address family name, and port of the server as reported +by the operating system if listening on an IP socket. +If the server is listening on a pipe or UNIX domain socket, the name is +returned as a string. ### server.close([callback]) -Close the server and terminate all clients, calls callback when done with an error if one occured. +Close the HTTP server if created internally, terminate all clients and call +callback when done. If an external HTTP server is used via the `server` or +`noServer` constructor options, it must be closed manually. -### server.handleUpgrade(request, socket, upgradeHead, callback) +### server.handleUpgrade(request, socket, head, callback) -Handles a HTTP Upgrade request. `request` is an instance of `http.ServerRequest`, `socket` is an instance of `net.Socket`. +- `request` {http.IncomingMessage} The client HTTP GET request. +- `socket` {net.Socket} The network socket between the server and client. +- `head` {Buffer} The first packet of the upgraded stream. +- `callback` {Function}. -When the Upgrade was successfully, the `callback` will be called with a `ws.WebSocket` object as parameter. +Handle a HTTP upgrade request. When the HTTP server is created internally or +when the HTTP server is passed via the `server` option, this method is called +automatically. When operating in "noServer" mode, this method must be called +manually. -### Event: 'error' +If the upgrade is successful, the `callback` is called with a `WebSocket` +object as parameter. -`function (error) { }` +### server.shouldHandle(request) -If the underlying server emits an error, it will be forwarded here. +- `request` {http.IncomingMessage} The client HTTP GET request. -### Event: 'headers' +See if a given request should be handled by this server. +By default this method validates the pathname of the request, matching it +against the `path` option if provided. +The return value, `true` or `false`, determines whether or not to accept the +handshake. -`function (headers) { }` +This method can be overridden when a custom handling logic is required. -Emitted with the object of HTTP headers that are going to be written to the `Stream` as part of the handshake. +## Class: WebSocket -### Event: 'connection' +This class represents a WebSocket. It extends the `EventEmitter`. -`function (socket) { }` +### Ready state constants -When a new WebSocket connection is established. `socket` is an object of type `ws.WebSocket`. +|Constant | Value | Description | +|-----------|-------|--------------------------------------------------| +|CONNECTING | 0 | The connection is not yet open. | +|OPEN | 1 | The connection is open and ready to communicate. | +|CLOSING | 2 | The connection is in the process of closing. | +|CLOSED | 3 | The connection is closed. | +### new WebSocket(address[, protocols][, options]) -## Class: ws.WebSocket +- `address` {String|url.Url|url.URL} The URL to which to connect. +- `protocols` {String|Array} The list of subprotocols. +- `options` {Object} + - `handshakeTimeout` {Number} Timeout in milliseconds for the handshake request. + - `perMessageDeflate` {Boolean|Object} Enable/disable permessage-deflate. + - `protocolVersion` {Number} Value of the `Sec-WebSocket-Version` header. + - `origin` {String} Value of the `Origin` or `Sec-WebSocket-Origin` header + depending on the `protocolVersion`. + - Any other option allowed in [http.request()][] or [https.request()][]. -This class represents a WebSocket connection. It is an `EventEmitter`. +`perMessageDeflate` default value is `true`. When using an object, parameters +are the same of the server. The only difference is the direction of requests. +For example, `serverNoContextTakeover` can be used to ask the server to +disable context takeover. -### new ws.WebSocket(address, [protocols], [options]) +Create a new WebSocket instance. -* `address` String -* `protocols` String|Array -* `options` Object - * `protocol` String - * `agent` Agent - * `headers` Object - * `protocolVersion` Number|String - -- the following only apply if `address` is a String - * `host` String - * `origin` String - * `pfx` String|Buffer - * `key` String|Buffer - * `passphrase` String - * `cert` String|Buffer - * `ca` Array - * `ciphers` String - * `rejectUnauthorized` Boolean - * `perMessageDeflate` Boolean|Object - * `localAddress` String +#### UNIX Domain Sockets -Instantiating with an `address` creates a new WebSocket client object. If `address` is an Array (request, socket, rest), it is instantiated as a Server client (e.g. called from the `ws.Server`). +`ws` supports making requests to UNIX domain sockets. To make one, use the +following URL scheme: -### options.perMessageDeflate +``` +ws+unix:///absolute/path/to/uds_socket:/pathname?search_params +``` -Parameters of permessage-deflate extension which have the same form with the one for `ws.Server` except the direction of requests. (e.g. `serverNoContextTakeover` is the value to be requested to the server) +Note that `:` is the separator between the socket path and the URL path. If +the URL path is omitted -### websocket.bytesReceived +``` +ws+unix:///absolute/path/to/uds_socket +``` -Received bytes count. +it defaults to `/`. -### websocket.readyState +### Event: 'close' -Possible states are `WebSocket.CONNECTING`, `WebSocket.OPEN`, `WebSocket.CLOSING`, `WebSocket.CLOSED`. +- `code` {Number} +- `reason` {String} -### websocket.protocolVersion +Emitted when the connection is closed. `code` is a numeric value indicating the +status code explaining why the connection has been closed. `reason` is a +human-readable string explaining why the connection has been closed. -The WebSocket protocol version used for this connection, `8`, `13` or `hixie-76` (the latter only for server clients). +### Event: 'error' -### websocket.url +- `error` {Error} + +Emitted when an error occurs. + +### Event: 'message' -The URL of the WebSocket server (only for clients) +- `data` {String|Buffer|ArrayBuffer|Buffer[]} -### websocket.supports +Emitted when a message is received from the server. -Describes the feature of the used protocol version. E.g. `supports.binary` is a boolean that describes if the connection supports binary messages. +### Event: 'open' -### websocket.upgradeReq +Emitted when the connection is established. -The http request that initiated the upgrade. Useful for parsing authorty headers, cookie headers and other information to associate a specific Websocket to a specific Client. This is only available for WebSockets constructed by a Server. +### Event: 'ping' -### websocket.close([code], [data]) +- `data` {Buffer} -Gracefully closes the connection, after sending a description message +Emitted when a ping is received from the server. -### websocket.pause() +### Event: 'pong' -Pause the client stream +- `data` {Buffer} -### websocket.ping([data], [options], [dontFailWhenClosed]) +Emitted when a pong is received from the server. -Sends a ping. `data` is sent, `options` is an object with members `mask` and `binary`. `dontFailWhenClosed` indicates whether or not to throw if the connection isnt open. +### Event: 'unexpected-response' -### websocket.pong([data], [options], [dontFailWhenClosed]) +- `request` {http.ClientRequest} +- `response` {http.IncomingMessage} -Sends a pong. `data` is sent, `options` is an object with members `mask` and `binary`. `dontFailWhenClosed` indicates whether or not to throw if the connection isnt open. +Emitted when the server response is not the expected one, for example a 401 +response. This event gives the ability to read the response in order to extract +useful information. If the server sends an invalid response and there isn't a +listener for this event, an error is emitted. +### Event: 'upgrade' -### websocket.resume() +- `response` {http.IncomingMessage} -Resume the client stream +Emitted when response headers are received from the server as part of the +handshake. This allows you to read headers from the server, for example +'set-cookie' headers. -### websocket.send(data, [options], [callback]) +### websocket.addEventListener(type, listener) -Sends `data` through the connection. `options` can be an object with members `mask`, `binary` and `compress`. The optional `callback` is executed after the send completes. +- `type` {String} A string representing the event type to listen for. +- `listener` {Function} The listener to add. -### websocket.stream([options], callback) +Register an event listener emulating the `EventTarget` interface. -Streams data through calls to a user supplied function. `options` can be an object with members `mask` and `binary`. `callback` is executed on successive ticks of which send is `function (data, final)`. +### websocket.binaryType -### websocket.terminate() +- {String} -Immediately shuts down the connection +A string indicating the type of binary data being transmitted by the connection. +This should be one of "nodebuffer", "arraybuffer" or "fragments". Defaults to +"nodebuffer". Type "fragments" will emit the array of fragments as received from +the sender, without copyfull concatenation, which is useful for the performance +of binary protocols transferring large messages with multiple fragments. + +### websocket.bufferedAmount + +- {Number} + +The number of bytes of data that have been queued using calls to `send()` but +not yet transmitted to the network. + +### websocket.close([code[, reason]]) + +- `code` {Number} A numeric value indicating the status code explaining why + the connection is being closed. +- `reason` {String} A human-readable string explaining why the connection is + closing. + +Initiate a closing handshake. + +### websocket.extensions + +- {Object} + +An object containing the negotiated extensions. -### websocket.onopen -### websocket.onerror ### websocket.onclose + +- {Function} + +An event listener to be called when connection is closed. The listener receives +a `CloseEvent` named "close". + +### websocket.onerror + +- {Function} + +An event listener to be called when an error occurs. The listener receives +an `ErrorEvent` named "error". + ### websocket.onmessage -Emulates the W3C Browser based WebSocket interface using function members. +- {Function} -### websocket.addEventListener(method, listener) +An event listener to be called when a message is received from the server. The +listener receives a `MessageEvent` named "message". -Emulates the W3C Browser based WebSocket interface using addEventListener. +### websocket.onopen -### Event: 'error' +- {Function} -`function (error) { }` +An event listener to be called when the connection is established. The listener +receives an `OpenEvent` named "open". -If the client emits an error, this event is emitted (errors from the underlying `net.Socket` are forwarded here). +### websocket.ping([data[, mask]][, callback]) -### Event: 'close' +- `data` {Any} The data to send in the ping frame. +- `mask` {Boolean} Specifies whether `data` should be masked or not. Defaults + to `true` when `websocket` is not a server client. +- `callback` {Function} An optional callback which is invoked when the ping + frame is written out. -`function (code, message) { }` +Send a ping. -Is emitted when the connection is closed. `code` is defined in the WebSocket specification. +### websocket.pong([data[, mask]][, callback]) -The `close` event is also emitted when then underlying `net.Socket` closes the connection (`end` or `close`). +- `data` {Any} The data to send in the pong frame. +- `mask` {Boolean} Specifies whether `data` should be masked or not. Defaults + to `true` when `websocket` is not a server client. +- `callback` {Function} An optional callback which is invoked when the pong + frame is written out. -### Event: 'message' +Send a pong. -`function (data, flags) { }` +### websocket.protocol -Is emitted when data is received. `flags` is an object with member `binary`. +- {String} -### Event: 'ping' +The subprotocol selected by the server. -`function (data, flags) { }` +### websocket.readyState -Is emitted when a ping is received. `flags` is an object with member `binary`. +- {Number} -### Event: 'pong' +The current state of the connection. This is one of the ready state constants. -`function (data, flags) { }` +### websocket.removeEventListener(type, listener) -Is emitted when a pong is received. `flags` is an object with member `binary`. +- `type` {String} A string representing the event type to remove. +- `listener` {Function} The listener to remove. -### Event: 'open' +Removes an event listener emulating the `EventTarget` interface. -`function () { }` +### websocket.send(data[, options][, callback]) -Emitted when the connection is established. +- `data` {Any} The data to send. +- `options` {Object} + - `compress` {Boolean} Specifies whether `data` should be compressed or not. + Defaults to `true` when permessage-deflate is enabled. + - `binary` {Boolean} Specifies whether `data` should be sent as a binary or not. + Default is autodetected. + - `mask` {Boolean} Specifies whether `data` should be masked or not. Defaults + to `true` when `websocket` is not a server client. + - `fin` {Boolean} Specifies whether `data` is the last fragment of a message or + not. Defaults to `true`. +- `callback` {Function} An optional callback which is invoked when `data` is + written out. + +Send `data` through the connection. + +### websocket.terminate() + +Forcibly close the connection. + +### websocket.url + +- {String} + +The URL of the WebSocket server. Server clients don't have this attribute. + +[concurrency-limit]: https://github.com/websockets/ws/issues/1202 +[permessage-deflate]: https://tools.ietf.org/html/draft-ietf-hybi-permessage-compression-19 +[zlib-options]: https://nodejs.org/api/zlib.html#zlib_class_options +[http.request()]: https://nodejs.org/api/http.html#http_http_request_options_callback +[https.request()]: https://nodejs.org/api/https.html#https_https_request_options_callback diff --git a/examples/express-session-parse/index.js b/examples/express-session-parse/index.js new file mode 100644 index 000000000..5250b2cb6 --- /dev/null +++ b/examples/express-session-parse/index.js @@ -0,0 +1,78 @@ +'use strict'; + +const session = require('express-session'); +const express = require('express'); +const http = require('http'); +const uuid = require('uuid'); + +const WebSocket = require('../..'); + +const app = express(); + +// +// We need the same instance of the session parser in express and +// WebSocket server. +// +const sessionParser = session({ + saveUninitialized: false, + secret: '$eCuRiTy', + resave: false +}); + +// +// Serve static files from the 'public' folder. +// +app.use(express.static('public')); +app.use(sessionParser); + +app.post('/login', (req, res) => { + // + // "Log in" user and set userId to session. + // + const id = uuid.v4(); + + console.log(`Updating session for user ${id}`); + req.session.userId = id; + res.send({ result: 'OK', message: 'Session updated' }); +}); + +app.delete('/logout', (request, response) => { + console.log('Destroying session'); + request.session.destroy(); + response.send({ result: 'OK', message: 'Session destroyed' }); +}); + +// +// Create HTTP server by ourselves. +// +const server = http.createServer(app); + +const wss = new WebSocket.Server({ + verifyClient: (info, done) => { + console.log('Parsing session from request...'); + sessionParser(info.req, {}, () => { + console.log('Session is parsed!'); + + // + // We can reject the connection by returning false to done(). For example, + // reject here if user is unknown. + // + done(info.req.session.userId); + }); + }, + server +}); + +wss.on('connection', (ws, req) => { + ws.on('message', (message) => { + // + // Here we can now use session parameters. + // + console.log(`WS message ${message} from user ${req.session.userId}`); + }); +}); + +// +// Start the server. +// +server.listen(8080, () => console.log('Listening on http://localhost:8080')); diff --git a/examples/express-session-parse/package.json b/examples/express-session-parse/package.json new file mode 100644 index 000000000..cf96cbc49 --- /dev/null +++ b/examples/express-session-parse/package.json @@ -0,0 +1,11 @@ +{ + "author": "", + "name": "express-session-parse", + "version": "0.0.0", + "repository": "websockets/ws", + "dependencies": { + "express": "~4.14.1", + "express-session": "~1.15.1", + "uuid": "~3.0.1" + } +} diff --git a/examples/express-session-parse/public/app.js b/examples/express-session-parse/public/app.js new file mode 100644 index 000000000..916fafd41 --- /dev/null +++ b/examples/express-session-parse/public/app.js @@ -0,0 +1,46 @@ +/* global fetch, WebSocket, location */ +(() => { + const messages = document.querySelector('#messages'); + const wsButton = document.querySelector('#wsButton'); + const logout = document.querySelector('#logout'); + const login = document.querySelector('#login'); + + const showMessage = (message) => { + messages.textContent += `\n${message}`; + messages.scrollTop = messages.scrollHeight; + }; + + const handleResponse = (response) => { + return response.ok + ? response.json().then((data) => JSON.stringify(data, null, 2)) + : Promise.reject(new Error('Unexpected response')); + }; + + login.onclick = () => { + fetch('/login', { method: 'POST', credentials: 'same-origin' }) + .then(handleResponse) + .then(showMessage) + .catch((err) => showMessage(err.message)); + }; + + logout.onclick = () => { + fetch('/logout', { method: 'DELETE', credentials: 'same-origin' }) + .then(handleResponse) + .then(showMessage) + .catch((err) => showMessage(err.message)); + }; + + let ws; + + wsButton.onclick = () => { + if (ws) { + ws.onerror = ws.onopen = ws.onclose = null; + ws.close(); + } + + ws = new WebSocket(`ws://${location.host}`); + ws.onerror = () => showMessage('WebSocket error'); + ws.onopen = () => showMessage('WebSocket connection established'); + ws.onclose = () => showMessage('WebSocket connection closed'); + }; +})(); diff --git a/examples/express-session-parse/public/index.html b/examples/express-session-parse/public/index.html new file mode 100644 index 000000000..c99949c77 --- /dev/null +++ b/examples/express-session-parse/public/index.html @@ -0,0 +1,21 @@ + + + + + Express session demo + + +

Choose an action.

+ + + +

+    
+  
+
diff --git a/examples/fileapi/package.json b/examples/fileapi/package.json
index 7816f2737..1341f59e9 100644
--- a/examples/fileapi/package.json
+++ b/examples/fileapi/package.json
@@ -2,17 +2,9 @@
   "author": "",
   "name": "fileapi",
   "version": "0.0.0",
-  "repository": {
-    "type": "git",
-    "url": "git://github.com/einaros/ws.git"
-  },
-  "engines": {
-    "node": "~0.6.8"
-  },
+  "repository": "websockets/ws",
   "dependencies": {
-    "express": "latest",
+    "express": "~4.14.0",
     "ansi": "https://github.com/einaros/ansi.js/tarball/master"
-  },
-  "devDependencies": {},
-  "optionalDependencies": {}
+  }
 }
diff --git a/examples/fileapi/public/app.js b/examples/fileapi/public/app.js
index e812cc3ea..f1a9ee32c 100644
--- a/examples/fileapi/public/app.js
+++ b/examples/fileapi/public/app.js
@@ -1,4 +1,5 @@
-function onFilesSelected(e) {
+/* global Uploader */
+function onFilesSelected (e) {
   var button = e.srcElement;
   button.disabled = true;
   var progress = document.querySelector('div#progress');
@@ -8,12 +9,12 @@ function onFilesSelected(e) {
   var filesSent = 0;
   if (totalFiles) {
     var uploader = new Uploader('ws://localhost:8080', function () {
-      Array.prototype.slice.call(files, 0).forEach(function(file) {
-        if (file.name == '.') {
+      Array.prototype.slice.call(files, 0).forEach(function (file) {
+        if (file.name === '.') {
           --totalFiles;
           return;
         }
-        uploader.sendFile(file, function(error) {
+        uploader.sendFile(file, function (error) {
           if (error) {
             console.log(error);
             return;
@@ -25,15 +26,15 @@ function onFilesSelected(e) {
       });
     });
   }
-  uploader.ondone = function() {
+  uploader.ondone = function () {
     uploader.close();
     progress.innerHTML = '100% done, ' + totalFiles + ' files sent.';
-  }
+  };
 }
 
-window.onload = function() {
+window.onload = function () {
   var importButtons = document.querySelectorAll('[type="file"]');
-  Array.prototype.slice.call(importButtons, 0).forEach(function(importButton) {
+  Array.prototype.slice.call(importButtons, 0).forEach(function (importButton) {
     importButton.addEventListener('change', onFilesSelected, false);
   });
-}
+};
diff --git a/examples/fileapi/public/uploader.js b/examples/fileapi/public/uploader.js
index 0c34a7fae..9b4f98e2f 100644
--- a/examples/fileapi/public/uploader.js
+++ b/examples/fileapi/public/uploader.js
@@ -1,4 +1,5 @@
-function Uploader(url, cb) {
+/* global WebSocket */
+function Uploader (url, cb) {
   this.ws = new WebSocket(url);
   if (cb) this.ws.onopen = cb;
   this.sendQueue = [];
@@ -6,39 +7,39 @@ function Uploader(url, cb) {
   this.sendCallback = null;
   this.ondone = null;
   var self = this;
-  this.ws.onmessage = function(event) {
+  this.ws.onmessage = function (event) {
     var data = JSON.parse(event.data);
-    if (data.event == 'complete') {
-      if (data.path != self.sending.path) {
+    var callback;
+    if (data.event === 'complete') {
+      if (data.path !== self.sending.path) {
         self.sendQueue = [];
         self.sending = null;
         self.sendCallback = null;
         throw new Error('Got message for wrong file!');
       }
       self.sending = null;
-      var callback = self.sendCallback;
+      callback = self.sendCallback;
       self.sendCallback = null;
       if (callback) callback();
       if (self.sendQueue.length === 0 && self.ondone) self.ondone(null);
       if (self.sendQueue.length > 0) {
         var args = self.sendQueue.pop();
-        setTimeout(function() { self.sendFile.apply(self, args); }, 0);
+        setTimeout(function () { self.sendFile.apply(self, args); }, 0);
       }
-    }
-    else if (data.event == 'error') {
+    } else if (data.event === 'error') {
       self.sendQueue = [];
       self.sending = null;
-      var callback = self.sendCallback;
+      callback = self.sendCallback;
       self.sendCallback = null;
       var error = new Error('Server reported send error for file ' + data.path);
       if (callback) callback(error);
       if (self.ondone) self.ondone(error);
     }
-  }
+  };
 }
 
-Uploader.prototype.sendFile = function(file, cb) {
-  if (this.ws.readyState != WebSocket.OPEN) throw new Error('Not connected');
+Uploader.prototype.sendFile = function (file, cb) {
+  if (this.ws.readyState !== WebSocket.OPEN) throw new Error('Not connected');
   if (this.sending) {
     this.sendQueue.push(arguments);
     return;
@@ -48,8 +49,8 @@ Uploader.prototype.sendFile = function(file, cb) {
   this.sendCallback = cb;
   this.ws.send(JSON.stringify(fileData));
   this.ws.send(file);
-}
+};
 
-Uploader.prototype.close = function() {
+Uploader.prototype.close = function () {
   this.ws.close();
-}
+};
diff --git a/examples/fileapi/server.js b/examples/fileapi/server.js
index badfeba7a..54e6128f8 100644
--- a/examples/fileapi/server.js
+++ b/examples/fileapi/server.js
@@ -1,41 +1,41 @@
-var WebSocketServer = require('../../').Server
-  , express = require('express')
-  , fs = require('fs')
-  , http = require('http')
-  , util = require('util')
-  , path = require('path')
-  , app = express.createServer()
-  , events = require('events')
-  , ansi = require('ansi')
-  , cursor = ansi(process.stdout);
+var WebSocketServer = require('../../').Server;
+var express = require('express');
+var fs = require('fs');
+var util = require('util');
+var path = require('path');
+var app = express();
+var server = require('http').Server(app);
+var events = require('events');
+var ansi = require('ansi');
+var cursor = ansi(process.stdout);
 
-function BandwidthSampler(ws, interval) {
+function BandwidthSampler (ws, interval) {
   interval = interval || 2000;
   var previousByteCount = 0;
   var self = this;
-  var intervalId = setInterval(function() {
+  var intervalId = setInterval(function () {
     var byteCount = ws.bytesReceived;
     var bytesPerSec = (byteCount - previousByteCount) / (interval / 1000);
     previousByteCount = byteCount;
     self.emit('sample', bytesPerSec);
   }, interval);
-  ws.on('close', function() {
+  ws.on('close', function () {
     clearInterval(intervalId);
   });
 }
 util.inherits(BandwidthSampler, events.EventEmitter);
 
-function makePathForFile(filePath, prefix, cb) {
+function makePathForFile (filePath, prefix, cb) {
   if (typeof cb !== 'function') throw new Error('callback is required');
   filePath = path.dirname(path.normalize(filePath)).replace(/^(\/|\\)+/, '');
   var pieces = filePath.split(/(\\|\/)/);
   var incrementalPath = prefix;
-  function step(error) {
+  function step (error) {
     if (error) return cb(error);
-    if (pieces.length == 0) return cb(null, incrementalPath);
+    if (pieces.length === 0) return cb(null, incrementalPath);
     incrementalPath += '/' + pieces.shift();
-    fs.exists(incrementalPath, function(exists) {
-      if (!exists) fs.mkdir(incrementalPath, step);
+    fs.access(incrementalPath, function (err) {
+      if (err) fs.mkdir(incrementalPath, step);
       else process.nextTick(step);
     });
   }
@@ -43,37 +43,41 @@ function makePathForFile(filePath, prefix, cb) {
 }
 
 cursor.eraseData(2).goto(1, 1);
-app.use(express.static(__dirname + '/public'));
+app.use(express.static(path.join(__dirname, '/public')));
 
 var clientId = 0;
-var wss = new WebSocketServer({server: app});
-wss.on('connection', function(ws) {
+var wss = new WebSocketServer({server: server});
+wss.on('connection', function (ws) {
   var thisId = ++clientId;
   cursor.goto(1, 4 + thisId).eraseLine();
   console.log('Client #%d connected', thisId);
 
   var sampler = new BandwidthSampler(ws);
-  sampler.on('sample', function(bps) {
+  sampler.on('sample', function (bps) {
     cursor.goto(1, 4 + thisId).eraseLine();
-    console.log('WebSocket #%d incoming bandwidth: %d MB/s', thisId, Math.round(bps / (1024*1024)));
+    console.log('WebSocket #%d incoming bandwidth: %d MB/s', thisId, Math.round(bps / (1024 * 1024)));
   });
 
   var filesReceived = 0;
   var currentFile = null;
-  ws.on('message', function(data, flags) {
-    if (!flags.binary) {
+  ws.on('message', function (data) {
+    if (typeof data === 'string') {
       currentFile = JSON.parse(data);
       // note: a real-world app would want to sanity check the data
-    }
-    else {
+    } else {
       if (currentFile == null) return;
-      makePathForFile(currentFile.path, __dirname + '/uploaded', function(error, path) {
+      makePathForFile(currentFile.path, path.join(__dirname, '/uploaded'), function (error, path) {
         if (error) {
           console.log(error);
           ws.send(JSON.stringify({event: 'error', path: currentFile.path, message: error.message}));
           return;
         }
-        fs.writeFile(path + '/' + currentFile.name, data, function(error) {
+        fs.writeFile(path + '/' + currentFile.name, data, function (error) {
+          if (error) {
+            console.log(error);
+            ws.send(JSON.stringify({event: 'error', path: currentFile.path, message: error.message}));
+            return;
+          }
           ++filesReceived;
           // console.log('received %d bytes long file, %s', data.length, currentFile.path);
           ws.send(JSON.stringify({event: 'complete', path: currentFile.path}));
@@ -83,21 +87,22 @@ wss.on('connection', function(ws) {
     }
   });
 
-  ws.on('close', function() {
+  ws.on('close', function () {
     cursor.goto(1, 4 + thisId).eraseLine();
     console.log('Client #%d disconnected. %d files received.', thisId, filesReceived);
   });
 
-  ws.on('error', function(e) {
+  ws.on('error', function (e) {
     cursor.goto(1, 4 + thisId).eraseLine();
     console.log('Client #%d error: %s', thisId, e.message);
   });
 });
 
-fs.mkdir(__dirname + '/uploaded', function(error) {
+fs.mkdir(path.join(__dirname, '/uploaded'), function () {
   // ignore errors, most likely means directory exists
   console.log('Uploaded files will be saved to %s/uploaded.', __dirname);
   console.log('Remember to wipe this directory if you upload lots and lots.');
-  app.listen(8080);
-  console.log('Listening on http://localhost:8080');
+  server.listen(8080, function () {
+    console.log('Listening on http://localhost:8080');
+  });
 });
diff --git a/examples/serverstats-express_3/package.json b/examples/serverstats-express_3/package.json
deleted file mode 100644
index 99722c422..000000000
--- a/examples/serverstats-express_3/package.json
+++ /dev/null
@@ -1,17 +0,0 @@
-{
-  "author": "",
-  "name": "serverstats",
-  "version": "0.0.0",
-  "repository": {
-    "type": "git",
-    "url": "git://github.com/einaros/ws.git"
-  },
-  "engines": {
-    "node": ">0.4.0"
-  },
-  "dependencies": {
-    "express": "~3.0.0"
-  },
-  "devDependencies": {},
-  "optionalDependencies": {}
-}
diff --git a/examples/serverstats-express_3/public/index.html b/examples/serverstats-express_3/public/index.html
deleted file mode 100644
index 24d84e120..000000000
--- a/examples/serverstats-express_3/public/index.html
+++ /dev/null
@@ -1,33 +0,0 @@
-
-
-  
-    
-    
-  
-  
-    Server Stats
- RSS:

- Heap total:

- Heap used:

- - diff --git a/examples/serverstats-express_3/server.js b/examples/serverstats-express_3/server.js deleted file mode 100644 index 88bbc9ebf..000000000 --- a/examples/serverstats-express_3/server.js +++ /dev/null @@ -1,21 +0,0 @@ -var WebSocketServer = require('../../').Server - , http = require('http') - , express = require('express') - , app = express(); - -app.use(express.static(__dirname + '/public')); - -var server = http.createServer(app); -server.listen(8080); - -var wss = new WebSocketServer({server: server}); -wss.on('connection', function(ws) { - var id = setInterval(function() { - ws.send(JSON.stringify(process.memoryUsage()), function() { /* ignore errors */ }); - }, 100); - console.log('started client interval'); - ws.on('close', function() { - console.log('stopping client interval'); - clearInterval(id); - }); -}); diff --git a/examples/serverstats/package.json b/examples/serverstats/package.json index 65c900ab1..321049ab5 100644 --- a/examples/serverstats/package.json +++ b/examples/serverstats/package.json @@ -2,16 +2,8 @@ "author": "", "name": "serverstats", "version": "0.0.0", - "repository": { - "type": "git", - "url": "git://github.com/einaros/ws.git" - }, - "engines": { - "node": ">0.4.0" - }, + "repository": "websockets/ws", "dependencies": { - "express": "2.x" - }, - "devDependencies": {}, - "optionalDependencies": {} + "express": "~4.14.0" + } } diff --git a/examples/serverstats/server.js b/examples/serverstats/server.js index d7845e0cb..b1e167806 100644 --- a/examples/serverstats/server.js +++ b/examples/serverstats/server.js @@ -1,19 +1,24 @@ -var WebSocketServer = require('../../').Server - , http = require('http') - , express = require('express') - , app = express.createServer(); +var WebSocketServer = require('../../').Server; +var express = require('express'); +var path = require('path'); +var app = express(); +var server = require('http').createServer(); -app.use(express.static(__dirname + '/public')); -app.listen(8080); +app.use(express.static(path.join(__dirname, '/public'))); -var wss = new WebSocketServer({server: app}); -wss.on('connection', function(ws) { - var id = setInterval(function() { - ws.send(JSON.stringify(process.memoryUsage()), function() { /* ignore errors */ }); +var wss = new WebSocketServer({server: server}); +wss.on('connection', function (ws) { + var id = setInterval(function () { + ws.send(JSON.stringify(process.memoryUsage()), function () { /* ignore errors */ }); }, 100); console.log('started client interval'); - ws.on('close', function() { + ws.on('close', function () { console.log('stopping client interval'); clearInterval(id); }); }); + +server.on('request', app); +server.listen(8080, function () { + console.log('Listening on http://localhost:8080'); +}); diff --git a/examples/ssl.js b/examples/ssl.js index bf1bf5303..496daea80 100644 --- a/examples/ssl.js +++ b/examples/ssl.js @@ -1,59 +1,37 @@ - -(function(){ - - "use strict"; - - var fs = require('fs'); - - // you'll probably load configuration from config - var cfg = { - ssl: true, - port: 8080, - ssl_key: '/path/to/you/ssl.key', - ssl_cert: '/path/to/you/ssl.crt' - }; - - var httpServ = ( cfg.ssl ) ? require('https') : require('http'); - - var WebSocketServer = require('../').Server; - - var app = null; - - // dummy request processing - var processRequest = function( req, res ) { - - res.writeHead(200); - res.end("All glory to WebSockets!\n"); - }; - - if ( cfg.ssl ) { - - app = httpServ.createServer({ - - // providing server with SSL key/cert - key: fs.readFileSync( cfg.ssl_key ), - cert: fs.readFileSync( cfg.ssl_cert ) - - }, processRequest ).listen( cfg.port ); - - } else { - - app = httpServ.createServer( processRequest ).listen( cfg.port ); - } - - // passing or reference to web server so WS would knew port and SSL capabilities - var wss = new WebSocketServer( { server: app } ); - - - wss.on( 'connection', function ( wsConnect ) { - - wsConnect.on( 'message', function ( message ) { - - console.log( message ); - - }); - - }); - - -}()); \ No newline at end of file +'use strict'; + +const https = require('https'); +const fs = require('fs'); + +const WebSocket = require('..'); + +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', function connection (ws) { + ws.on('message', function message (msg) { + console.log(msg); + }); +}); + +server.listen(function listening () { + // + // If the `rejectUnauthorized` option is not `false`, the server certificate + // is verified against a list of well-known CAs. An 'error' event is emitted + // if verification fails. + // + // The certificate used in this example is self-signed so `rejectUnauthorized` + // is set to `false`. + // + const ws = new WebSocket(`wss://localhost:${server.address().port}`, { + rejectUnauthorized: false + }); + + ws.on('open', function open () { + ws.send('All glory to WebSockets!'); + }); +}); diff --git a/index.js b/index.js index a7e8644b9..b8d6be1c9 100644 --- a/index.js +++ b/index.js @@ -1,49 +1,9 @@ 'use strict'; -/*! - * ws: a node.js websocket client - * Copyright(c) 2011 Einar Otto Stangvik - * MIT Licensed - */ +const WebSocket = require('./lib/websocket'); -var WS = module.exports = require('./lib/WebSocket'); +WebSocket.Server = require('./lib/websocket-server'); +WebSocket.Receiver = require('./lib/receiver'); +WebSocket.Sender = require('./lib/sender'); -WS.Server = require('./lib/WebSocketServer'); -WS.Sender = require('./lib/Sender'); -WS.Receiver = require('./lib/Receiver'); - -/** - * Create a new WebSocket server. - * - * @param {Object} options Server options - * @param {Function} fn Optional connection listener. - * @returns {WS.Server} - * @api public - */ -WS.createServer = function createServer(options, fn) { - var server = new WS.Server(options); - - if (typeof fn === 'function') { - server.on('connection', fn); - } - - return server; -}; - -/** - * Create a new WebSocket connection. - * - * @param {String} address The URL/address we need to connect to. - * @param {Function} fn Open listener. - * @returns {WS} - * @api public - */ -WS.connect = WS.createConnection = function connect(address, fn) { - var client = new WS(address); - - if (typeof fn === 'function') { - client.on('open', fn); - } - - return client; -}; +module.exports = WebSocket; diff --git a/lib/BufferPool.js b/lib/BufferPool.js deleted file mode 100644 index 8ee599057..000000000 --- a/lib/BufferPool.js +++ /dev/null @@ -1,63 +0,0 @@ -/*! - * ws: a node.js websocket client - * Copyright(c) 2011 Einar Otto Stangvik - * MIT Licensed - */ - -var util = require('util'); - -function BufferPool(initialSize, growStrategy, shrinkStrategy) { - if (this instanceof BufferPool === false) { - throw new TypeError("Classes can't be function-called"); - } - - if (typeof initialSize === 'function') { - shrinkStrategy = growStrategy; - growStrategy = initialSize; - initialSize = 0; - } - else if (typeof initialSize === 'undefined') { - initialSize = 0; - } - this._growStrategy = (growStrategy || function(db, size) { - return db.used + size; - }).bind(null, this); - this._shrinkStrategy = (shrinkStrategy || function(db) { - return initialSize; - }).bind(null, this); - this._buffer = initialSize ? new Buffer(initialSize) : null; - this._offset = 0; - this._used = 0; - this._changeFactor = 0; - this.__defineGetter__('size', function(){ - return this._buffer == null ? 0 : this._buffer.length; - }); - this.__defineGetter__('used', function(){ - return this._used; - }); -} - -BufferPool.prototype.get = function(length) { - if (this._buffer == null || this._offset + length > this._buffer.length) { - var newBuf = new Buffer(this._growStrategy(length)); - this._buffer = newBuf; - this._offset = 0; - } - this._used += length; - var buf = this._buffer.slice(this._offset, this._offset + length); - this._offset += length; - return buf; -} - -BufferPool.prototype.reset = function(forceNewBuffer) { - var len = this._shrinkStrategy(); - if (len < this.size) this._changeFactor -= 1; - if (forceNewBuffer || this._changeFactor < -2) { - this._changeFactor = 0; - this._buffer = len ? new Buffer(len) : null; - } - this._offset = 0; - this._used = 0; -} - -module.exports = BufferPool; diff --git a/lib/BufferUtil.fallback.js b/lib/BufferUtil.fallback.js deleted file mode 100644 index 7abd0d8a6..000000000 --- a/lib/BufferUtil.fallback.js +++ /dev/null @@ -1,47 +0,0 @@ -/*! - * ws: a node.js websocket client - * Copyright(c) 2011 Einar Otto Stangvik - * MIT Licensed - */ - -exports.BufferUtil = { - merge: function(mergedBuffer, buffers) { - var offset = 0; - for (var i = 0, l = buffers.length; i < l; ++i) { - var buf = buffers[i]; - buf.copy(mergedBuffer, offset); - offset += buf.length; - } - }, - mask: function(source, mask, output, offset, length) { - var maskNum = mask.readUInt32LE(0, true); - var i = 0; - for (; i < length - 3; i += 4) { - var num = maskNum ^ source.readUInt32LE(i, true); - if (num < 0) num = 4294967296 + num; - output.writeUInt32LE(num, offset + i, true); - } - switch (length % 4) { - case 3: output[offset + i + 2] = source[i + 2] ^ mask[2]; - case 2: output[offset + i + 1] = source[i + 1] ^ mask[1]; - case 1: output[offset + i] = source[i] ^ mask[0]; - case 0:; - } - }, - unmask: function(data, mask) { - var maskNum = mask.readUInt32LE(0, true); - var length = data.length; - var i = 0; - for (; i < length - 3; i += 4) { - var num = maskNum ^ data.readUInt32LE(i, true); - if (num < 0) num = 4294967296 + num; - data.writeUInt32LE(num, i, true); - } - switch (length % 4) { - case 3: data[i + 2] = data[i + 2] ^ mask[2]; - case 2: data[i + 1] = data[i + 1] ^ mask[1]; - case 1: data[i] = data[i] ^ mask[0]; - case 0:; - } - } -} diff --git a/lib/BufferUtil.js b/lib/BufferUtil.js deleted file mode 100644 index 18c699894..000000000 --- a/lib/BufferUtil.js +++ /dev/null @@ -1,13 +0,0 @@ -'use strict'; - -/*! - * ws: a node.js websocket client - * Copyright(c) 2011 Einar Otto Stangvik - * MIT Licensed - */ - -try { - module.exports = require('bufferutil'); -} catch (e) { - module.exports = require('./BufferUtil.fallback'); -} diff --git a/lib/ErrorCodes.js b/lib/ErrorCodes.js deleted file mode 100644 index 55ebd529b..000000000 --- a/lib/ErrorCodes.js +++ /dev/null @@ -1,24 +0,0 @@ -/*! - * ws: a node.js websocket client - * Copyright(c) 2011 Einar Otto Stangvik - * MIT Licensed - */ - -module.exports = { - isValidErrorCode: function(code) { - return (code >= 1000 && code <= 1011 && code != 1004 && code != 1005 && code != 1006) || - (code >= 3000 && code <= 4999); - }, - 1000: 'normal', - 1001: 'going away', - 1002: 'protocol error', - 1003: 'unsupported data', - 1004: 'reserved', - 1005: 'reserved for extensions', - 1006: 'reserved for extensions', - 1007: 'inconsistent or invalid data', - 1008: 'policy violation', - 1009: 'message too big', - 1010: 'extension handshake missing', - 1011: 'an unexpected condition prevented the request from being fulfilled', -}; \ No newline at end of file diff --git a/lib/Extensions.js b/lib/Extensions.js deleted file mode 100644 index a465ace2b..000000000 --- a/lib/Extensions.js +++ /dev/null @@ -1,70 +0,0 @@ - -var util = require('util'); - -/** - * Module exports. - */ - -exports.parse = parse; -exports.format = format; - -/** - * Parse extensions header value - */ - -function parse(value) { - value = value || ''; - - var extensions = {}; - - value.split(',').forEach(function(v) { - var params = v.split(';'); - var token = params.shift().trim(); - var paramsList = extensions[token] = extensions[token] || []; - var parsedParams = {}; - - params.forEach(function(param) { - var parts = param.trim().split('='); - var key = parts[0]; - var value = parts[1]; - if (typeof value === 'undefined') { - value = true; - } else { - // unquote value - if (value[0] === '"') { - value = value.slice(1); - } - if (value[value.length - 1] === '"') { - value = value.slice(0, value.length - 1); - } - } - (parsedParams[key] = parsedParams[key] || []).push(value); - }); - - paramsList.push(parsedParams); - }); - - return extensions; -} - -/** - * Format extensions header value - */ - -function format(value) { - return Object.keys(value).map(function(token) { - var paramsList = value[token]; - if (!util.isArray(paramsList)) { - paramsList = [paramsList]; - } - return paramsList.map(function(params) { - return [token].concat(Object.keys(params).map(function(k) { - var p = params[k]; - if (!util.isArray(p)) p = [p]; - return p.map(function(v) { - return v === true ? k : k + '=' + v; - }).join('; '); - })).join('; '); - }).join(', '); - }).join(', '); -} diff --git a/lib/PerMessageDeflate.js b/lib/PerMessageDeflate.js deleted file mode 100644 index 00a6ea62a..000000000 --- a/lib/PerMessageDeflate.js +++ /dev/null @@ -1,337 +0,0 @@ - -var zlib = require('zlib'); - -var AVAILABLE_WINDOW_BITS = [8, 9, 10, 11, 12, 13, 14, 15]; -var DEFAULT_WINDOW_BITS = 15; -var DEFAULT_MEM_LEVEL = 8; - -PerMessageDeflate.extensionName = 'permessage-deflate'; - -/** - * Per-message Compression Extensions implementation - */ - -function PerMessageDeflate(options, isServer,maxPayload) { - if (this instanceof PerMessageDeflate === false) { - throw new TypeError("Classes can't be function-called"); - } - - this._options = options || {}; - this._isServer = !!isServer; - this._inflate = null; - this._deflate = null; - this.params = null; - this._maxPayload = maxPayload || 0; -} - -/** - * Create extension parameters offer - * - * @api public - */ - -PerMessageDeflate.prototype.offer = function() { - var params = {}; - if (this._options.serverNoContextTakeover) { - params.server_no_context_takeover = true; - } - if (this._options.clientNoContextTakeover) { - params.client_no_context_takeover = true; - } - if (this._options.serverMaxWindowBits) { - params.server_max_window_bits = this._options.serverMaxWindowBits; - } - if (this._options.clientMaxWindowBits) { - params.client_max_window_bits = this._options.clientMaxWindowBits; - } else if (this._options.clientMaxWindowBits == null) { - params.client_max_window_bits = true; - } - return params; -}; - -/** - * Accept extension offer - * - * @api public - */ - -PerMessageDeflate.prototype.accept = function(paramsList) { - paramsList = this.normalizeParams(paramsList); - - var params; - if (this._isServer) { - params = this.acceptAsServer(paramsList); - } else { - params = this.acceptAsClient(paramsList); - } - - this.params = params; - return params; -}; - -/** - * Releases all resources used by the extension - * - * @api public - */ - -PerMessageDeflate.prototype.cleanup = function() { - if (this._inflate) { - if (this._inflate.writeInProgress) { - this._inflate.pendingClose = true; - } else { - if (this._inflate.close) this._inflate.close(); - this._inflate = null; - } - } - if (this._deflate) { - if (this._deflate.writeInProgress) { - this._deflate.pendingClose = true; - } else { - if (this._deflate.close) this._deflate.close(); - this._deflate = null; - } - } -}; - -/** - * Accept extension offer from client - * - * @api private - */ - -PerMessageDeflate.prototype.acceptAsServer = function(paramsList) { - var accepted = {}; - var result = paramsList.some(function(params) { - accepted = {}; - if (this._options.serverNoContextTakeover === false && params.server_no_context_takeover) { - return; - } - if (this._options.serverMaxWindowBits === false && params.server_max_window_bits) { - return; - } - if (typeof this._options.serverMaxWindowBits === 'number' && - typeof params.server_max_window_bits === 'number' && - this._options.serverMaxWindowBits > params.server_max_window_bits) { - return; - } - if (typeof this._options.clientMaxWindowBits === 'number' && !params.client_max_window_bits) { - return; - } - - if (this._options.serverNoContextTakeover || params.server_no_context_takeover) { - accepted.server_no_context_takeover = true; - } - if (this._options.clientNoContextTakeover) { - accepted.client_no_context_takeover = true; - } - if (this._options.clientNoContextTakeover !== false && params.client_no_context_takeover) { - accepted.client_no_context_takeover = true; - } - if (typeof this._options.serverMaxWindowBits === 'number') { - accepted.server_max_window_bits = this._options.serverMaxWindowBits; - } else if (typeof params.server_max_window_bits === 'number') { - accepted.server_max_window_bits = params.server_max_window_bits; - } - if (typeof this._options.clientMaxWindowBits === 'number') { - accepted.client_max_window_bits = this._options.clientMaxWindowBits; - } else if (this._options.clientMaxWindowBits !== false && typeof params.client_max_window_bits === 'number') { - accepted.client_max_window_bits = params.client_max_window_bits; - } - return true; - }, this); - - if (!result) { - throw new Error('Doesn\'t support the offered configuration'); - } - - return accepted; -}; - -/** - * Accept extension response from server - * - * @api privaye - */ - -PerMessageDeflate.prototype.acceptAsClient = function(paramsList) { - var params = paramsList[0]; - if (this._options.clientNoContextTakeover != null) { - if (this._options.clientNoContextTakeover === false && params.client_no_context_takeover) { - throw new Error('Invalid value for "client_no_context_takeover"'); - } - } - if (this._options.clientMaxWindowBits != null) { - if (this._options.clientMaxWindowBits === false && params.client_max_window_bits) { - throw new Error('Invalid value for "client_max_window_bits"'); - } - if (typeof this._options.clientMaxWindowBits === 'number' && - (!params.client_max_window_bits || params.client_max_window_bits > this._options.clientMaxWindowBits)) { - throw new Error('Invalid value for "client_max_window_bits"'); - } - } - return params; -}; - -/** - * Normalize extensions parameters - * - * @api private - */ - -PerMessageDeflate.prototype.normalizeParams = function(paramsList) { - return paramsList.map(function(params) { - Object.keys(params).forEach(function(key) { - var value = params[key]; - if (value.length > 1) { - throw new Error('Multiple extension parameters for ' + key); - } - - value = value[0]; - - switch (key) { - case 'server_no_context_takeover': - case 'client_no_context_takeover': - if (value !== true) { - throw new Error('invalid extension parameter value for ' + key + ' (' + value + ')'); - } - params[key] = true; - break; - case 'server_max_window_bits': - case 'client_max_window_bits': - if (typeof value === 'string') { - value = parseInt(value, 10); - if (!~AVAILABLE_WINDOW_BITS.indexOf(value)) { - throw new Error('invalid extension parameter value for ' + key + ' (' + value + ')'); - } - } - if (!this._isServer && value === true) { - throw new Error('Missing extension parameter value for ' + key); - } - params[key] = value; - break; - default: - throw new Error('Not defined extension parameter (' + key + ')'); - } - }, this); - return params; - }, this); -}; - -/** - * Decompress message - * - * @api public - */ - -PerMessageDeflate.prototype.decompress = function (data, fin, callback) { - var endpoint = this._isServer ? 'client' : 'server'; - - if (!this._inflate) { - var maxWindowBits = this.params[endpoint + '_max_window_bits']; - this._inflate = zlib.createInflateRaw({ - windowBits: 'number' === typeof maxWindowBits ? maxWindowBits : DEFAULT_WINDOW_BITS - }); - } - this._inflate.writeInProgress = true; - - var self = this; - var buffers = []; - var cumulativeBufferLength=0; - - this._inflate.on('error', onError).on('data', onData); - this._inflate.write(data); - if (fin) { - this._inflate.write(new Buffer([0x00, 0x00, 0xff, 0xff])); - } - this._inflate.flush(function() { - cleanup(); - callback(null, Buffer.concat(buffers)); - }); - - function onError(err) { - cleanup(); - callback(err); - } - - function onData(data) { - if(self._maxPayload!==undefined && self._maxPayload!==null && self._maxPayload>0){ - cumulativeBufferLength+=data.length; - if(cumulativeBufferLength>self._maxPayload){ - buffers=[]; - cleanup(); - var err={type:1009}; - callback(err); - return; - } - } - buffers.push(data); - } - - function cleanup() { - if (!self._inflate) return; - self._inflate.removeListener('error', onError); - self._inflate.removeListener('data', onData); - self._inflate.writeInProgress = false; - if ((fin && self.params[endpoint + '_no_context_takeover']) || self._inflate.pendingClose) { - if (self._inflate.close) self._inflate.close(); - self._inflate = null; - } - } -}; - -/** - * Compress message - * - * @api public - */ - -PerMessageDeflate.prototype.compress = function (data, fin, callback) { - var endpoint = this._isServer ? 'server' : 'client'; - - if (!this._deflate) { - var maxWindowBits = this.params[endpoint + '_max_window_bits']; - this._deflate = zlib.createDeflateRaw({ - flush: zlib.Z_SYNC_FLUSH, - windowBits: 'number' === typeof maxWindowBits ? maxWindowBits : DEFAULT_WINDOW_BITS, - memLevel: this._options.memLevel || DEFAULT_MEM_LEVEL - }); - } - this._deflate.writeInProgress = true; - - var self = this; - var buffers = []; - - this._deflate.on('error', onError).on('data', onData); - this._deflate.write(data); - this._deflate.flush(function() { - cleanup(); - var data = Buffer.concat(buffers); - if (fin) { - data = data.slice(0, data.length - 4); - } - callback(null, data); - }); - - function onError(err) { - cleanup(); - callback(err); - } - - function onData(data) { - buffers.push(data); - } - - function cleanup() { - if (!self._deflate) return; - self._deflate.removeListener('error', onError); - self._deflate.removeListener('data', onData); - self._deflate.writeInProgress = false; - if ((fin && self.params[endpoint + '_no_context_takeover']) || self._deflate.pendingClose) { - if (self._deflate.close) self._deflate.close(); - self._deflate = null; - } - } -}; - -module.exports = PerMessageDeflate; diff --git a/lib/Receiver.hixie.js b/lib/Receiver.hixie.js deleted file mode 100644 index 598ccbdaf..000000000 --- a/lib/Receiver.hixie.js +++ /dev/null @@ -1,194 +0,0 @@ -/*! - * ws: a node.js websocket client - * Copyright(c) 2011 Einar Otto Stangvik - * MIT Licensed - */ - -var util = require('util'); - -/** - * State constants - */ - -var EMPTY = 0 - , BODY = 1; -var BINARYLENGTH = 2 - , BINARYBODY = 3; - -/** - * Hixie Receiver implementation - */ - -function Receiver () { - if (this instanceof Receiver === false) { - throw new TypeError("Classes can't be function-called"); - } - - this.state = EMPTY; - this.buffers = []; - this.messageEnd = -1; - this.spanLength = 0; - this.dead = false; - - this.onerror = function() {}; - this.ontext = function() {}; - this.onbinary = function() {}; - this.onclose = function() {}; - this.onping = function() {}; - this.onpong = function() {}; -} - -module.exports = Receiver; - -/** - * Add new data to the parser. - * - * @api public - */ - -Receiver.prototype.add = function(data) { - if (this.dead) return; - var self = this; - function doAdd() { - if (self.state === EMPTY) { - if (data.length == 2 && data[0] == 0xFF && data[1] == 0x00) { - self.reset(); - self.onclose(); - return; - } - if (data[0] === 0x80) { - self.messageEnd = 0; - self.state = BINARYLENGTH; - data = data.slice(1); - } else { - - if (data[0] !== 0x00) { - self.error('payload must start with 0x00 byte', true); - return; - } - data = data.slice(1); - self.state = BODY; - - } - } - if (self.state === BINARYLENGTH) { - var i = 0; - while ((i < data.length) && (data[i] & 0x80)) { - self.messageEnd = 128 * self.messageEnd + (data[i] & 0x7f); - ++i; - } - if (i < data.length) { - self.messageEnd = 128 * self.messageEnd + (data[i] & 0x7f); - self.state = BINARYBODY; - ++i; - } - if (i > 0) - data = data.slice(i); - } - if (self.state === BINARYBODY) { - var dataleft = self.messageEnd - self.spanLength; - if (data.length >= dataleft) { - // consume the whole buffer to finish the frame - self.buffers.push(data); - self.spanLength += dataleft; - self.messageEnd = dataleft; - return self.parse(); - } - // frame's not done even if we consume it all - self.buffers.push(data); - self.spanLength += data.length; - return; - } - self.buffers.push(data); - if ((self.messageEnd = bufferIndex(data, 0xFF)) != -1) { - self.spanLength += self.messageEnd; - return self.parse(); - } - else self.spanLength += data.length; - } - while(data) data = doAdd(); -}; - -/** - * Releases all resources used by the receiver. - * - * @api public - */ - -Receiver.prototype.cleanup = function() { - this.dead = true; - this.state = EMPTY; - this.buffers = []; -}; - -/** - * Process buffered data. - * - * @api public - */ - -Receiver.prototype.parse = function() { - var output = new Buffer(this.spanLength); - var outputIndex = 0; - for (var bi = 0, bl = this.buffers.length; bi < bl - 1; ++bi) { - var buffer = this.buffers[bi]; - buffer.copy(output, outputIndex); - outputIndex += buffer.length; - } - var lastBuffer = this.buffers[this.buffers.length - 1]; - if (this.messageEnd > 0) lastBuffer.copy(output, outputIndex, 0, this.messageEnd); - if (this.state !== BODY) --this.messageEnd; - var tail = null; - if (this.messageEnd < lastBuffer.length - 1) { - tail = lastBuffer.slice(this.messageEnd + 1); - } - this.reset(); - this.ontext(output.toString('utf8')); - return tail; -}; - -/** - * Handles an error - * - * @api private - */ - -Receiver.prototype.error = function (reason, terminate) { - if (this.dead) return; - this.reset(); - if(typeof reason == 'string'){ - this.onerror(new Error(reason), terminate); - } - else if(reason.constructor == Error){ - this.onerror(reason, terminate); - } - else{ - this.onerror(new Error("An error occured"),terminate); - } - return this; -}; - -/** - * Reset parser state - * - * @api private - */ - -Receiver.prototype.reset = function (reason) { - if (this.dead) return; - this.state = EMPTY; - this.buffers = []; - this.messageEnd = -1; - this.spanLength = 0; -}; - -/** - * Internal api - */ - -function bufferIndex(buffer, byte) { - for (var i = 0, l = buffer.length; i < l; ++i) { - if (buffer[i] === byte) return i; - } - return -1; -} diff --git a/lib/Receiver.js b/lib/Receiver.js deleted file mode 100644 index 0bf29d800..000000000 --- a/lib/Receiver.js +++ /dev/null @@ -1,793 +0,0 @@ -/*! - * ws: a node.js websocket client - * Copyright(c) 2011 Einar Otto Stangvik - * MIT Licensed - */ - -var util = require('util') - , Validation = require('./Validation').Validation - , ErrorCodes = require('./ErrorCodes') - , BufferPool = require('./BufferPool') - , bufferUtil = require('./BufferUtil').BufferUtil - , PerMessageDeflate = require('./PerMessageDeflate'); - -/** - * HyBi Receiver implementation - */ - -function Receiver (extensions,maxPayload) { - if (this instanceof Receiver === false) { - throw new TypeError("Classes can't be function-called"); - } - if(typeof extensions==='number'){ - maxPayload=extensions; - extensions={}; - } - - - // memory pool for fragmented messages - var fragmentedPoolPrevUsed = -1; - this.fragmentedBufferPool = new BufferPool(1024, function(db, length) { - return db.used + length; - }, function(db) { - return fragmentedPoolPrevUsed = fragmentedPoolPrevUsed >= 0 ? - Math.ceil((fragmentedPoolPrevUsed + db.used) / 2) : - db.used; - }); - - // memory pool for unfragmented messages - var unfragmentedPoolPrevUsed = -1; - this.unfragmentedBufferPool = new BufferPool(1024, function(db, length) { - return db.used + length; - }, function(db) { - return unfragmentedPoolPrevUsed = unfragmentedPoolPrevUsed >= 0 ? - Math.ceil((unfragmentedPoolPrevUsed + db.used) / 2) : - db.used; - }); - this.extensions = extensions || {}; - this.maxPayload = maxPayload || 0; - this.currentPayloadLength = 0; - this.state = { - activeFragmentedOperation: null, - lastFragment: false, - masked: false, - opcode: 0, - fragmentedOperation: false - }; - this.overflow = []; - this.headerBuffer = new Buffer(10); - this.expectOffset = 0; - this.expectBuffer = null; - this.expectHandler = null; - this.currentMessage = []; - this.currentMessageLength = 0; - this.messageHandlers = []; - this.expectHeader(2, this.processPacket); - this.dead = false; - this.processing = false; - - this.onerror = function() {}; - this.ontext = function() {}; - this.onbinary = function() {}; - this.onclose = function() {}; - this.onping = function() {}; - this.onpong = function() {}; -} - -module.exports = Receiver; - -/** - * Add new data to the parser. - * - * @api public - */ - -Receiver.prototype.add = function(data) { - if (this.dead) return; - var dataLength = data.length; - if (dataLength == 0) return; - if (this.expectBuffer == null) { - this.overflow.push(data); - return; - } - var toRead = Math.min(dataLength, this.expectBuffer.length - this.expectOffset); - fastCopy(toRead, data, this.expectBuffer, this.expectOffset); - this.expectOffset += toRead; - if (toRead < dataLength) { - this.overflow.push(data.slice(toRead)); - } - while (this.expectBuffer && this.expectOffset == this.expectBuffer.length) { - var bufferForHandler = this.expectBuffer; - this.expectBuffer = null; - this.expectOffset = 0; - this.expectHandler.call(this, bufferForHandler); - } -}; - -/** - * Releases all resources used by the receiver. - * - * @api public - */ - -Receiver.prototype.cleanup = function() { - this.dead = true; - this.overflow = null; - this.headerBuffer = null; - this.expectBuffer = null; - this.expectHandler = null; - this.unfragmentedBufferPool = null; - this.fragmentedBufferPool = null; - this.state = null; - this.currentMessage = null; - this.onerror = null; - this.ontext = null; - this.onbinary = null; - this.onclose = null; - this.onping = null; - this.onpong = null; -}; - -/** - * Waits for a certain amount of header bytes to be available, then fires a callback. - * - * @api private - */ - -Receiver.prototype.expectHeader = function(length, handler) { - if (length == 0) { - handler(null); - return; - } - this.expectBuffer = this.headerBuffer.slice(this.expectOffset, this.expectOffset + length); - this.expectHandler = handler; - var toRead = length; - while (toRead > 0 && this.overflow.length > 0) { - var fromOverflow = this.overflow.pop(); - if (toRead < fromOverflow.length) this.overflow.push(fromOverflow.slice(toRead)); - var read = Math.min(fromOverflow.length, toRead); - fastCopy(read, fromOverflow, this.expectBuffer, this.expectOffset); - this.expectOffset += read; - toRead -= read; - } -}; - -/** - * Waits for a certain amount of data bytes to be available, then fires a callback. - * - * @api private - */ - -Receiver.prototype.expectData = function(length, handler) { - if (length == 0) { - handler(null); - return; - } - this.expectBuffer = this.allocateFromPool(length, this.state.fragmentedOperation); - this.expectHandler = handler; - var toRead = length; - while (toRead > 0 && this.overflow.length > 0) { - var fromOverflow = this.overflow.pop(); - if (toRead < fromOverflow.length) this.overflow.push(fromOverflow.slice(toRead)); - var read = Math.min(fromOverflow.length, toRead); - fastCopy(read, fromOverflow, this.expectBuffer, this.expectOffset); - this.expectOffset += read; - toRead -= read; - } -}; - -/** - * Allocates memory from the buffer pool. - * - * @api private - */ - -Receiver.prototype.allocateFromPool = function(length, isFragmented) { - return (isFragmented ? this.fragmentedBufferPool : this.unfragmentedBufferPool).get(length); -}; - -/** - * Start processing a new packet. - * - * @api private - */ - -Receiver.prototype.processPacket = function (data) { - if (this.extensions[PerMessageDeflate.extensionName]) { - if ((data[0] & 0x30) != 0) { - this.error('reserved fields (2, 3) must be empty', 1002); - return; - } - } else { - if ((data[0] & 0x70) != 0) { - this.error('reserved fields must be empty', 1002); - return; - } - } - this.state.lastFragment = (data[0] & 0x80) == 0x80; - this.state.masked = (data[1] & 0x80) == 0x80; - var compressed = (data[0] & 0x40) == 0x40; - var opcode = data[0] & 0xf; - if (opcode === 0) { - if (compressed) { - this.error('continuation frame cannot have the Per-message Compressed bits', 1002); - return; - } - // continuation frame - this.state.fragmentedOperation = true; - this.state.opcode = this.state.activeFragmentedOperation; - if (!(this.state.opcode == 1 || this.state.opcode == 2)) { - this.error('continuation frame cannot follow current opcode', 1002); - return; - } - } - else { - if (opcode < 3 && this.state.activeFragmentedOperation != null) { - this.error('data frames after the initial data frame must have opcode 0', 1002); - return; - } - if (opcode >= 8 && compressed) { - this.error('control frames cannot have the Per-message Compressed bits', 1002); - return; - } - this.state.compressed = compressed; - this.state.opcode = opcode; - if (this.state.lastFragment === false) { - this.state.fragmentedOperation = true; - this.state.activeFragmentedOperation = opcode; - } - else this.state.fragmentedOperation = false; - } - var handler = opcodes[this.state.opcode]; - if (typeof handler == 'undefined') this.error('no handler for opcode ' + this.state.opcode, 1002); - else { - handler.start.call(this, data); - } -}; - -/** - * Endprocessing a packet. - * - * @api private - */ - -Receiver.prototype.endPacket = function() { - if (this.dead) return; - if (!this.state.fragmentedOperation) this.unfragmentedBufferPool.reset(true); - else if (this.state.lastFragment) this.fragmentedBufferPool.reset(true); - this.expectOffset = 0; - this.expectBuffer = null; - this.expectHandler = null; - if (this.state.lastFragment && this.state.opcode === this.state.activeFragmentedOperation) { - // end current fragmented operation - this.state.activeFragmentedOperation = null; - } - this.currentPayloadLength = 0; - this.state.lastFragment = false; - this.state.opcode = this.state.activeFragmentedOperation != null ? this.state.activeFragmentedOperation : 0; - this.state.masked = false; - this.expectHeader(2, this.processPacket); -}; - -/** - * Reset the parser state. - * - * @api private - */ - -Receiver.prototype.reset = function() { - if (this.dead) return; - this.state = { - activeFragmentedOperation: null, - lastFragment: false, - masked: false, - opcode: 0, - fragmentedOperation: false - }; - this.fragmentedBufferPool.reset(true); - this.unfragmentedBufferPool.reset(true); - this.expectOffset = 0; - this.expectBuffer = null; - this.expectHandler = null; - this.overflow = []; - this.currentMessage = []; - this.currentMessageLength = 0; - this.messageHandlers = []; - this.currentPayloadLength = 0; -}; - -/** - * Unmask received data. - * - * @api private - */ - -Receiver.prototype.unmask = function (mask, buf, binary) { - if (mask != null && buf != null) bufferUtil.unmask(buf, mask); - if (binary) return buf; - return buf != null ? buf.toString('utf8') : ''; -}; - -/** - * Handles an error - * - * @api private - */ - -Receiver.prototype.error = function (reason, protocolErrorCode) { - if (this.dead) return; - this.reset(); - if(typeof reason == 'string'){ - this.onerror(new Error(reason), protocolErrorCode); - } - else if(reason.constructor == Error){ - this.onerror(reason, protocolErrorCode); - } - else{ - this.onerror(new Error("An error occured"),protocolErrorCode); - } - return this; -}; - -/** - * Execute message handler buffers - * - * @api private - */ - -Receiver.prototype.flush = function() { - if (this.processing || this.dead) return; - - var handler = this.messageHandlers.shift(); - if (!handler) return; - - this.processing = true; - var self = this; - - handler(function() { - self.processing = false; - self.flush(); - }); -}; - -/** - * Apply extensions to message - * - * @api private - */ - -Receiver.prototype.applyExtensions = function(messageBuffer, fin, compressed, callback) { - var self = this; - if (compressed) { - this.extensions[PerMessageDeflate.extensionName].decompress(messageBuffer, fin, function(err, buffer) { - if (self.dead) return; - if (err) { - callback(new Error('invalid compressed data')); - return; - } - callback(null, buffer); - }); - } else { - callback(null, messageBuffer); - } -}; - -/** -* Checks payload size, disconnects socket when it exceeds maxPayload -* -* @api private -*/ -Receiver.prototype.maxPayloadExceeded = function(length) { - if (this.maxPayload=== undefined || this.maxPayload === null || this.maxPayload < 1) { - return false; - } - var fullLength = this.currentPayloadLength + length; - if (fullLength < this.maxPayload) { - this.currentPayloadLength = fullLength; - return false; - } - this.error('payload cannot exceed ' + this.maxPayload + ' bytes', 1009); - this.messageBuffer=[]; - this.cleanup(); - - return true; -}; - -/** - * Buffer utilities - */ - -function readUInt16BE(start) { - return (this[start]<<8) + - this[start+1]; -} - -function readUInt32BE(start) { - return (this[start]<<24) + - (this[start+1]<<16) + - (this[start+2]<<8) + - this[start+3]; -} - -function fastCopy(length, srcBuffer, dstBuffer, dstOffset) { - switch (length) { - default: srcBuffer.copy(dstBuffer, dstOffset, 0, length); break; - case 16: dstBuffer[dstOffset+15] = srcBuffer[15]; - case 15: dstBuffer[dstOffset+14] = srcBuffer[14]; - case 14: dstBuffer[dstOffset+13] = srcBuffer[13]; - case 13: dstBuffer[dstOffset+12] = srcBuffer[12]; - case 12: dstBuffer[dstOffset+11] = srcBuffer[11]; - case 11: dstBuffer[dstOffset+10] = srcBuffer[10]; - case 10: dstBuffer[dstOffset+9] = srcBuffer[9]; - case 9: dstBuffer[dstOffset+8] = srcBuffer[8]; - case 8: dstBuffer[dstOffset+7] = srcBuffer[7]; - case 7: dstBuffer[dstOffset+6] = srcBuffer[6]; - case 6: dstBuffer[dstOffset+5] = srcBuffer[5]; - case 5: dstBuffer[dstOffset+4] = srcBuffer[4]; - case 4: dstBuffer[dstOffset+3] = srcBuffer[3]; - case 3: dstBuffer[dstOffset+2] = srcBuffer[2]; - case 2: dstBuffer[dstOffset+1] = srcBuffer[1]; - case 1: dstBuffer[dstOffset] = srcBuffer[0]; - } -} - -function clone(obj) { - var cloned = {}; - for (var k in obj) { - if (obj.hasOwnProperty(k)) { - cloned[k] = obj[k]; - } - } - return cloned; -} - -/** - * Opcode handlers - */ - -var opcodes = { - // text - '1': { - start: function(data) { - var self = this; - // decode length - var firstLength = data[1] & 0x7f; - if (firstLength < 126) { - if (self.maxPayloadExceeded(firstLength)){ - self.error('Maximumpayload exceeded in compressed text message. Aborting...', 1009); - return; - } - opcodes['1'].getData.call(self, firstLength); - } - else if (firstLength == 126) { - self.expectHeader(2, function(data) { - var length = readUInt16BE.call(data, 0); - if (self.maxPayloadExceeded(length)){ - self.error('Maximumpayload exceeded in compressed text message. Aborting...', 1009); - return; - } - opcodes['1'].getData.call(self, length); - }); - } - else if (firstLength == 127) { - self.expectHeader(8, function(data) { - if (readUInt32BE.call(data, 0) != 0) { - self.error('packets with length spanning more than 32 bit is currently not supported', 1008); - return; - } - var length = readUInt32BE.call(data, 4); - if (self.maxPayloadExceeded(length)){ - self.error('Maximumpayload exceeded in compressed text message. Aborting...', 1009); - return; - } - opcodes['1'].getData.call(self, readUInt32BE.call(data, 4)); - }); - } - }, - getData: function(length) { - var self = this; - if (self.state.masked) { - self.expectHeader(4, function(data) { - var mask = data; - self.expectData(length, function(data) { - opcodes['1'].finish.call(self, mask, data); - }); - }); - } - else { - self.expectData(length, function(data) { - opcodes['1'].finish.call(self, null, data); - }); - } - }, - finish: function(mask, data) { - var self = this; - var packet = this.unmask(mask, data, true) || new Buffer(0); - var state = clone(this.state); - this.messageHandlers.push(function(callback) { - self.applyExtensions(packet, state.lastFragment, state.compressed, function(err, buffer) { - if (err) { - if(err.type===1009){ - return self.error('Maximumpayload exceeded in compressed text message. Aborting...', 1009); - } - return self.error(err.message, 1007); - } - if (buffer != null) { - if( self.maxPayload==0 || (self.maxPayload > 0 && (self.currentMessageLength + buffer.length) < self.maxPayload) ){ - self.currentMessage.push(buffer); - } - else{ - self.currentMessage=null; - self.currentMessage = []; - self.currentMessageLength = 0; - self.error(new Error('Maximum payload exceeded. maxPayload: '+self.maxPayload), 1009); - return; - } - self.currentMessageLength += buffer.length; - } - if (state.lastFragment) { - var messageBuffer = Buffer.concat(self.currentMessage); - self.currentMessage = []; - self.currentMessageLength = 0; - if (!Validation.isValidUTF8(messageBuffer)) { - self.error('invalid utf8 sequence', 1007); - return; - } - self.ontext(messageBuffer.toString('utf8'), {masked: state.masked, buffer: messageBuffer}); - } - callback(); - }); - }); - this.flush(); - this.endPacket(); - } - }, - // binary - '2': { - start: function(data) { - var self = this; - // decode length - var firstLength = data[1] & 0x7f; - if (firstLength < 126) { - if (self.maxPayloadExceeded(firstLength)){ - self.error('Max payload exceeded in compressed text message. Aborting...', 1009); - return; - } - opcodes['2'].getData.call(self, firstLength); - } - else if (firstLength == 126) { - self.expectHeader(2, function(data) { - var length = readUInt16BE.call(data, 0); - if (self.maxPayloadExceeded(length)){ - self.error('Max payload exceeded in compressed text message. Aborting...', 1009); - return; - } - opcodes['2'].getData.call(self, length); - }); - } - else if (firstLength == 127) { - self.expectHeader(8, function(data) { - if (readUInt32BE.call(data, 0) != 0) { - self.error('packets with length spanning more than 32 bit is currently not supported', 1008); - return; - } - var length = readUInt32BE.call(data, 4, true); - if (self.maxPayloadExceeded(length)){ - self.error('Max payload exceeded in compressed text message. Aborting...', 1009); - return; - } - opcodes['2'].getData.call(self, length); - }); - } - }, - getData: function(length) { - var self = this; - if (self.state.masked) { - self.expectHeader(4, function(data) { - var mask = data; - self.expectData(length, function(data) { - opcodes['2'].finish.call(self, mask, data); - }); - }); - } - else { - self.expectData(length, function(data) { - opcodes['2'].finish.call(self, null, data); - }); - } - }, - finish: function(mask, data) { - var self = this; - var packet = this.unmask(mask, data, true) || new Buffer(0); - var state = clone(this.state); - this.messageHandlers.push(function(callback) { - self.applyExtensions(packet, state.lastFragment, state.compressed, function(err, buffer) { - if (err) { - if(err.type===1009){ - return self.error('Max payload exceeded in compressed binary message. Aborting...', 1009); - } - return self.error(err.message, 1007); - } - if (buffer != null) { - if( self.maxPayload==0 || (self.maxPayload > 0 && (self.currentMessageLength + buffer.length) < self.maxPayload) ){ - self.currentMessage.push(buffer); - } - else{ - self.currentMessage=null; - self.currentMessage = []; - self.currentMessageLength = 0; - self.error(new Error('Maximum payload exceeded'), 1009); - return; - } - self.currentMessageLength += buffer.length; - } - if (state.lastFragment) { - var messageBuffer = Buffer.concat(self.currentMessage); - self.currentMessage = []; - self.currentMessageLength = 0; - self.onbinary(messageBuffer, {masked: state.masked, buffer: messageBuffer}); - } - callback(); - }); - }); - this.flush(); - this.endPacket(); - } - }, - // close - '8': { - start: function(data) { - var self = this; - if (self.state.lastFragment == false) { - self.error('fragmented close is not supported', 1002); - return; - } - - // decode length - var firstLength = data[1] & 0x7f; - if (firstLength < 126) { - opcodes['8'].getData.call(self, firstLength); - } - else { - self.error('control frames cannot have more than 125 bytes of data', 1002); - } - }, - getData: function(length) { - var self = this; - if (self.state.masked) { - self.expectHeader(4, function(data) { - var mask = data; - self.expectData(length, function(data) { - opcodes['8'].finish.call(self, mask, data); - }); - }); - } - else { - self.expectData(length, function(data) { - opcodes['8'].finish.call(self, null, data); - }); - } - }, - finish: function(mask, data) { - var self = this; - data = self.unmask(mask, data, true); - - var state = clone(this.state); - this.messageHandlers.push(function() { - if (data && data.length == 1) { - self.error('close packets with data must be at least two bytes long', 1002); - return; - } - var code = data && data.length > 1 ? readUInt16BE.call(data, 0) : 1000; - if (!ErrorCodes.isValidErrorCode(code)) { - self.error('invalid error code', 1002); - return; - } - var message = ''; - if (data && data.length > 2) { - var messageBuffer = data.slice(2); - if (!Validation.isValidUTF8(messageBuffer)) { - self.error('invalid utf8 sequence', 1007); - return; - } - message = messageBuffer.toString('utf8'); - } - self.onclose(code, message, {masked: state.masked}); - self.reset(); - }); - this.flush(); - }, - }, - // ping - '9': { - start: function(data) { - var self = this; - if (self.state.lastFragment == false) { - self.error('fragmented ping is not supported', 1002); - return; - } - - // decode length - var firstLength = data[1] & 0x7f; - if (firstLength < 126) { - opcodes['9'].getData.call(self, firstLength); - } - else { - self.error('control frames cannot have more than 125 bytes of data', 1002); - } - }, - getData: function(length) { - var self = this; - if (self.state.masked) { - self.expectHeader(4, function(data) { - var mask = data; - self.expectData(length, function(data) { - opcodes['9'].finish.call(self, mask, data); - }); - }); - } - else { - self.expectData(length, function(data) { - opcodes['9'].finish.call(self, null, data); - }); - } - }, - finish: function(mask, data) { - var self = this; - data = this.unmask(mask, data, true); - var state = clone(this.state); - this.messageHandlers.push(function(callback) { - self.onping(data, {masked: state.masked, binary: true}); - callback(); - }); - this.flush(); - this.endPacket(); - } - }, - // pong - '10': { - start: function(data) { - var self = this; - if (self.state.lastFragment == false) { - self.error('fragmented pong is not supported', 1002); - return; - } - - // decode length - var firstLength = data[1] & 0x7f; - if (firstLength < 126) { - opcodes['10'].getData.call(self, firstLength); - } - else { - self.error('control frames cannot have more than 125 bytes of data', 1002); - } - }, - getData: function(length) { - var self = this; - if (this.state.masked) { - this.expectHeader(4, function(data) { - var mask = data; - self.expectData(length, function(data) { - opcodes['10'].finish.call(self, mask, data); - }); - }); - } - else { - this.expectData(length, function(data) { - opcodes['10'].finish.call(self, null, data); - }); - } - }, - finish: function(mask, data) { - var self = this; - data = self.unmask(mask, data, true); - var state = clone(this.state); - this.messageHandlers.push(function(callback) { - self.onpong(data, {masked: state.masked, binary: true}); - callback(); - }); - this.flush(); - this.endPacket(); - } - } -} diff --git a/lib/Sender.hixie.js b/lib/Sender.hixie.js deleted file mode 100644 index b87d9dd93..000000000 --- a/lib/Sender.hixie.js +++ /dev/null @@ -1,124 +0,0 @@ -/*! - * ws: a node.js websocket client - * Copyright(c) 2011 Einar Otto Stangvik - * MIT Licensed - */ - -var events = require('events') - , util = require('util') - , EventEmitter = events.EventEmitter; - -/** - * Hixie Sender implementation - */ - -function Sender(socket) { - if (this instanceof Sender === false) { - throw new TypeError("Classes can't be function-called"); - } - - events.EventEmitter.call(this); - - this.socket = socket; - this.continuationFrame = false; - this.isClosed = false; -} - -module.exports = Sender; - -/** - * Inherits from EventEmitter. - */ - -util.inherits(Sender, events.EventEmitter); - -/** - * Frames and writes data. - * - * @api public - */ - -Sender.prototype.send = function(data, options, cb) { - if (this.isClosed) return; - - var isString = typeof data == 'string' - , length = isString ? Buffer.byteLength(data) : data.length - , lengthbytes = (length > 127) ? 2 : 1 // assume less than 2**14 bytes - , writeStartMarker = this.continuationFrame == false - , writeEndMarker = !options || !(typeof options.fin != 'undefined' && !options.fin) - , buffer = new Buffer((writeStartMarker ? ((options && options.binary) ? (1 + lengthbytes) : 1) : 0) + length + ((writeEndMarker && !(options && options.binary)) ? 1 : 0)) - , offset = writeStartMarker ? 1 : 0; - - if (writeStartMarker) { - if (options && options.binary) { - buffer.write('\x80', 'binary'); - // assume length less than 2**14 bytes - if (lengthbytes > 1) - buffer.write(String.fromCharCode(128+length/128), offset++, 'binary'); - buffer.write(String.fromCharCode(length&0x7f), offset++, 'binary'); - } else - buffer.write('\x00', 'binary'); - } - - if (isString) buffer.write(data, offset, 'utf8'); - else data.copy(buffer, offset, 0); - - if (writeEndMarker) { - if (options && options.binary) { - // sending binary, not writing end marker - } else - buffer.write('\xff', offset + length, 'binary'); - this.continuationFrame = false; - } - else this.continuationFrame = true; - - try { - this.socket.write(buffer, 'binary', cb); - } catch (e) { - this.error(e.toString()); - } -}; - -/** - * Sends a close instruction to the remote party. - * - * @api public - */ - -Sender.prototype.close = function(code, data, mask, cb) { - if (this.isClosed) return; - this.isClosed = true; - try { - if (this.continuationFrame) this.socket.write(new Buffer([0xff], 'binary')); - this.socket.write(new Buffer([0xff, 0x00]), 'binary', cb); - } catch (e) { - this.error(e.toString()); - } -}; - -/** - * Sends a ping message to the remote party. Not available for hixie. - * - * @api public - */ - -Sender.prototype.ping = function(data, options) {}; - -/** - * Sends a pong message to the remote party. Not available for hixie. - * - * @api public - */ - -Sender.prototype.pong = function(data, options) {}; - -/** - * Handles an error - * - * @api private - */ - -Sender.prototype.error = function (reason) { - this.emit('error', reason); - return this; -}; diff --git a/lib/Sender.js b/lib/Sender.js deleted file mode 100644 index 6ef2ea271..000000000 --- a/lib/Sender.js +++ /dev/null @@ -1,324 +0,0 @@ -/*! - * ws: a node.js websocket client - * Copyright(c) 2011 Einar Otto Stangvik - * MIT Licensed - */ - -var events = require('events') - , util = require('util') - , EventEmitter = events.EventEmitter - , ErrorCodes = require('./ErrorCodes') - , bufferUtil = require('./BufferUtil').BufferUtil - , PerMessageDeflate = require('./PerMessageDeflate'); - -/** - * HyBi Sender implementation - */ - -function Sender(socket, extensions) { - if (this instanceof Sender === false) { - throw new TypeError("Classes can't be function-called"); - } - - events.EventEmitter.call(this); - - this._socket = socket; - this.extensions = extensions || {}; - this.firstFragment = true; - this.compress = false; - this.messageHandlers = []; - this.processing = false; -} - -/** - * Inherits from EventEmitter. - */ - -util.inherits(Sender, events.EventEmitter); - -/** - * Sends a close instruction to the remote party. - * - * @api public - */ - -Sender.prototype.close = function(code, data, mask, cb) { - if (typeof code !== 'undefined') { - if (typeof code !== 'number' || - !ErrorCodes.isValidErrorCode(code)) throw new Error('first argument must be a valid error code number'); - } - code = code || 1000; - var dataBuffer = new Buffer(2 + (data ? Buffer.byteLength(data) : 0)); - writeUInt16BE.call(dataBuffer, code, 0); - if (dataBuffer.length > 2) dataBuffer.write(data, 2); - - var self = this; - this.messageHandlers.push(function(callback) { - self.frameAndSend(0x8, dataBuffer, true, mask); - callback(); - if (typeof cb == 'function') cb(); - }); - this.flush(); -}; - -/** - * Sends a ping message to the remote party. - * - * @api public - */ - -Sender.prototype.ping = function(data, options) { - var mask = options && options.mask; - var self = this; - this.messageHandlers.push(function(callback) { - self.frameAndSend(0x9, data || '', true, mask); - callback(); - }); - this.flush(); -}; - -/** - * Sends a pong message to the remote party. - * - * @api public - */ - -Sender.prototype.pong = function(data, options) { - var mask = options && options.mask; - var self = this; - this.messageHandlers.push(function(callback) { - self.frameAndSend(0xa, data || '', true, mask); - callback(); - }); - this.flush(); -}; - -/** - * Sends text or binary data to the remote party. - * - * @api public - */ - -Sender.prototype.send = function(data, options, cb) { - var finalFragment = options && options.fin === false ? false : true; - var mask = options && options.mask; - var compress = options && options.compress; - var opcode = options && options.binary ? 2 : 1; - if (this.firstFragment === false) { - opcode = 0; - compress = false; - } else { - this.firstFragment = false; - this.compress = compress; - } - if (finalFragment) this.firstFragment = true - - var compressFragment = this.compress; - - var self = this; - this.messageHandlers.push(function(callback) { - self.applyExtensions(data, finalFragment, compressFragment, function(err, data) { - if (err) { - if (typeof cb == 'function') cb(err); - else self.emit('error', err); - return; - } - self.frameAndSend(opcode, data, finalFragment, mask, compress, cb); - callback(); - }); - }); - this.flush(); -}; - -/** - * Frames and sends a piece of data according to the HyBi WebSocket protocol. - * - * @api private - */ - -Sender.prototype.frameAndSend = function(opcode, data, finalFragment, maskData, compressed, cb) { - var canModifyData = false; - - if (!data) { - try { - this._socket.write(new Buffer([opcode | (finalFragment ? 0x80 : 0), 0 | (maskData ? 0x80 : 0)].concat(maskData ? [0, 0, 0, 0] : [])), 'binary', cb); - } - catch (e) { - if (typeof cb == 'function') cb(e); - else this.emit('error', e); - } - return; - } - - if (!Buffer.isBuffer(data)) { - canModifyData = true; - if (data && (typeof data.byteLength !== 'undefined' || typeof data.buffer !== 'undefined')) { - data = getArrayBuffer(data); - } else { - // - // If people want to send a number, this would allocate the number in - // bytes as memory size instead of storing the number as buffer value. So - // we need to transform it to string in order to prevent possible - // vulnerabilities / memory attacks. - // - if (typeof data === 'number') data = data.toString(); - - data = new Buffer(data); - } - } - - var dataLength = data.length - , dataOffset = maskData ? 6 : 2 - , secondByte = dataLength; - - if (dataLength >= 65536) { - dataOffset += 8; - secondByte = 127; - } - else if (dataLength > 125) { - dataOffset += 2; - secondByte = 126; - } - - var mergeBuffers = dataLength < 32768 || (maskData && !canModifyData); - var totalLength = mergeBuffers ? dataLength + dataOffset : dataOffset; - var outputBuffer = new Buffer(totalLength); - outputBuffer[0] = finalFragment ? opcode | 0x80 : opcode; - if (compressed) outputBuffer[0] |= 0x40; - - switch (secondByte) { - case 126: - writeUInt16BE.call(outputBuffer, dataLength, 2); - break; - case 127: - writeUInt32BE.call(outputBuffer, 0, 2); - writeUInt32BE.call(outputBuffer, dataLength, 6); - } - - if (maskData) { - outputBuffer[1] = secondByte | 0x80; - var mask = getRandomMask(); - outputBuffer[dataOffset - 4] = mask[0]; - outputBuffer[dataOffset - 3] = mask[1]; - outputBuffer[dataOffset - 2] = mask[2]; - outputBuffer[dataOffset - 1] = mask[3]; - if (mergeBuffers) { - bufferUtil.mask(data, mask, outputBuffer, dataOffset, dataLength); - try { - this._socket.write(outputBuffer, 'binary', cb); - } - catch (e) { - if (typeof cb == 'function') cb(e); - else this.emit('error', e); - } - } - else { - bufferUtil.mask(data, mask, data, 0, dataLength); - try { - this._socket.write(outputBuffer, 'binary'); - this._socket.write(data, 'binary', cb); - } - catch (e) { - if (typeof cb == 'function') cb(e); - else this.emit('error', e); - } - } - } - else { - outputBuffer[1] = secondByte; - if (mergeBuffers) { - data.copy(outputBuffer, dataOffset); - try { - this._socket.write(outputBuffer, 'binary', cb); - } - catch (e) { - if (typeof cb == 'function') cb(e); - else this.emit('error', e); - } - } - else { - try { - this._socket.write(outputBuffer, 'binary'); - this._socket.write(data, 'binary', cb); - } - catch (e) { - if (typeof cb == 'function') cb(e); - else this.emit('error', e); - } - } - } -}; - -/** - * Execute message handler buffers - * - * @api private - */ - -Sender.prototype.flush = function() { - if (this.processing) return; - - var handler = this.messageHandlers.shift(); - if (!handler) return; - - this.processing = true; - - var self = this; - - handler(function() { - self.processing = false; - self.flush(); - }); -}; - -/** - * Apply extensions to message - * - * @api private - */ - -Sender.prototype.applyExtensions = function(data, fin, compress, callback) { - if (compress && data) { - if ((data.buffer || data) instanceof ArrayBuffer) { - data = getArrayBuffer(data); - } - this.extensions[PerMessageDeflate.extensionName].compress(data, fin, callback); - } else { - callback(null, data); - } -}; - -module.exports = Sender; - -function writeUInt16BE(value, offset) { - this[offset] = (value & 0xff00)>>8; - this[offset+1] = value & 0xff; -} - -function writeUInt32BE(value, offset) { - this[offset] = (value & 0xff000000)>>24; - this[offset+1] = (value & 0xff0000)>>16; - this[offset+2] = (value & 0xff00)>>8; - this[offset+3] = value & 0xff; -} - -function getArrayBuffer(data) { - // data is either an ArrayBuffer or ArrayBufferView. - var array = new Uint8Array(data.buffer || data) - , l = data.byteLength || data.length - , o = data.byteOffset || 0 - , buffer = new Buffer(l); - for (var i = 0; i < l; ++i) { - buffer[i] = array[o+i]; - } - return buffer; -} - -function getRandomMask() { - return new Buffer([ - ~~(Math.random() * 255), - ~~(Math.random() * 255), - ~~(Math.random() * 255), - ~~(Math.random() * 255) - ]); -} diff --git a/lib/Validation.fallback.js b/lib/Validation.fallback.js deleted file mode 100644 index 639b0d316..000000000 --- a/lib/Validation.fallback.js +++ /dev/null @@ -1,11 +0,0 @@ -/*! - * ws: a node.js websocket client - * Copyright(c) 2011 Einar Otto Stangvik - * MIT Licensed - */ - -exports.Validation = { - isValidUTF8: function(buffer) { - return true; - } -}; diff --git a/lib/Validation.js b/lib/Validation.js deleted file mode 100644 index 0795fb7f0..000000000 --- a/lib/Validation.js +++ /dev/null @@ -1,13 +0,0 @@ -'use strict'; - -/*! - * ws: a node.js websocket client - * Copyright(c) 2011 Einar Otto Stangvik - * MIT Licensed - */ - -try { - module.exports = require('utf-8-validate'); -} catch (e) { - module.exports = require('./Validation.fallback'); -} diff --git a/lib/WebSocket.js b/lib/WebSocket.js deleted file mode 100644 index bb09e851b..000000000 --- a/lib/WebSocket.js +++ /dev/null @@ -1,987 +0,0 @@ -'use strict'; - -/*! - * ws: a node.js websocket client - * Copyright(c) 2011 Einar Otto Stangvik - * MIT Licensed - */ - -var url = require('url') - , util = require('util') - , http = require('http') - , https = require('https') - , crypto = require('crypto') - , stream = require('stream') - , Ultron = require('ultron') - , Options = require('options') - , Sender = require('./Sender') - , Receiver = require('./Receiver') - , SenderHixie = require('./Sender.hixie') - , ReceiverHixie = require('./Receiver.hixie') - , Extensions = require('./Extensions') - , PerMessageDeflate = require('./PerMessageDeflate') - , EventEmitter = require('events').EventEmitter; - -/** - * Constants - */ - -// Default protocol version - -var protocolVersion = 13; - -// Close timeout - -var closeTimeout = 30 * 1000; // Allow 30 seconds to terminate the connection cleanly - -/** - * WebSocket implementation - * - * @constructor - * @param {String} address Connection address. - * @param {String|Array} protocols WebSocket protocols. - * @param {Object} options Additional connection options. - * @api public - */ -function WebSocket(address, protocols, options) { - if (this instanceof WebSocket === false) { - return new WebSocket(address, protocols, options); - } - - EventEmitter.call(this); - - if (protocols && !Array.isArray(protocols) && 'object' === typeof protocols) { - // accept the "options" Object as the 2nd argument - options = protocols; - protocols = null; - } - - if ('string' === typeof protocols) { - protocols = [ protocols ]; - } - - if (!Array.isArray(protocols)) { - protocols = []; - } - - this._socket = null; - this._ultron = null; - this._closeReceived = false; - this.bytesReceived = 0; - this.readyState = null; - this.supports = {}; - this.extensions = {}; - this._binaryType = 'nodebuffer'; - - if (Array.isArray(address)) { - initAsServerClient.apply(this, address.concat(options)); - } else { - initAsClient.apply(this, [address, protocols, options]); - } -} - -/** - * Inherits from EventEmitter. - */ -util.inherits(WebSocket, EventEmitter); - -/** - * Ready States - */ -["CONNECTING", "OPEN", "CLOSING", "CLOSED"].forEach(function each(state, index) { - WebSocket.prototype[state] = WebSocket[state] = index; -}); - -/** - * Gracefully closes the connection, after sending a description message to the server - * - * @param {Object} data to be sent to the server - * @api public - */ -WebSocket.prototype.close = function close(code, data) { - if (this.readyState === WebSocket.CLOSED) return; - - if (this.readyState === WebSocket.CONNECTING) { - this.readyState = WebSocket.CLOSED; - return; - } - - if (this.readyState === WebSocket.CLOSING) { - if (this._closeReceived && this._isServer) { - this.terminate(); - } - return; - } - - var self = this; - try { - this.readyState = WebSocket.CLOSING; - this._closeCode = code; - this._closeMessage = data; - var mask = !this._isServer; - this._sender.close(code, data, mask, function(err) { - if (err) self.emit('error', err); - - if (self._closeReceived && self._isServer) { - self.terminate(); - } else { - // ensure that the connection is cleaned up even when no response of closing handshake. - clearTimeout(self._closeTimer); - self._closeTimer = setTimeout(cleanupWebsocketResources.bind(self, true), closeTimeout); - } - }); - } catch (e) { - this.emit('error', e); - } -}; - -/** - * Pause the client stream - * - * @api public - */ -WebSocket.prototype.pause = function pauser() { - if (this.readyState !== WebSocket.OPEN) throw new Error('not opened'); - - return this._socket.pause(); -}; - -/** - * Sends a ping - * - * @param {Object} data to be sent to the server - * @param {Object} Members - mask: boolean, binary: boolean - * @param {boolean} dontFailWhenClosed indicates whether or not to throw if the connection isnt open - * @api public - */ -WebSocket.prototype.ping = function ping(data, options, dontFailWhenClosed) { - if (this.readyState !== WebSocket.OPEN) { - if (dontFailWhenClosed === true) return; - throw new Error('not opened'); - } - - options = options || {}; - - if (typeof options.mask === 'undefined') options.mask = !this._isServer; - - this._sender.ping(data, options); -}; - -/** - * Sends a pong - * - * @param {Object} data to be sent to the server - * @param {Object} Members - mask: boolean, binary: boolean - * @param {boolean} dontFailWhenClosed indicates whether or not to throw if the connection isnt open - * @api public - */ -WebSocket.prototype.pong = function(data, options, dontFailWhenClosed) { - if (this.readyState !== WebSocket.OPEN) { - if (dontFailWhenClosed === true) return; - throw new Error('not opened'); - } - - options = options || {}; - - if (typeof options.mask === 'undefined') options.mask = !this._isServer; - - this._sender.pong(data, options); -}; - -/** - * Resume the client stream - * - * @api public - */ -WebSocket.prototype.resume = function resume() { - if (this.readyState !== WebSocket.OPEN) throw new Error('not opened'); - - return this._socket.resume(); -}; - -/** - * Sends a piece of data - * - * @param {Object} data to be sent to the server - * @param {Object} Members - mask: boolean, binary: boolean, compress: boolean - * @param {function} Optional callback which is executed after the send completes - * @api public - */ - -WebSocket.prototype.send = function send(data, options, cb) { - if (typeof options === 'function') { - cb = options; - options = {}; - } - - if (this.readyState !== WebSocket.OPEN) { - if (typeof cb === 'function') cb(new Error('not opened')); - else throw new Error('not opened'); - return; - } - - if (!data) data = ''; - if (this._queue) { - var self = this; - this._queue.push(function() { self.send(data, options, cb); }); - return; - } - - options = options || {}; - options.fin = true; - - if (typeof options.binary === 'undefined') { - options.binary = (data instanceof ArrayBuffer || data instanceof Buffer || - data instanceof Uint8Array || - data instanceof Uint16Array || - data instanceof Uint32Array || - data instanceof Int8Array || - data instanceof Int16Array || - data instanceof Int32Array || - data instanceof Float32Array || - data instanceof Float64Array); - } - - if (typeof options.mask === 'undefined') options.mask = !this._isServer; - if (typeof options.compress === 'undefined') options.compress = true; - if (!this.extensions[PerMessageDeflate.extensionName]) { - options.compress = false; - } - - var readable = typeof stream.Readable === 'function' - ? stream.Readable - : stream.Stream; - - if (data instanceof readable) { - startQueue(this); - var self = this; - - sendStream(this, data, options, function send(error) { - process.nextTick(function tock() { - executeQueueSends(self); - }); - - if (typeof cb === 'function') cb(error); - }); - } else { - this._sender.send(data, options, cb); - } -}; - -/** - * Streams data through calls to a user supplied function - * - * @param {Object} Members - mask: boolean, binary: boolean, compress: boolean - * @param {function} 'function (error, send)' which is executed on successive ticks of which send is 'function (data, final)'. - * @api public - */ -WebSocket.prototype.stream = function stream(options, cb) { - if (typeof options === 'function') { - cb = options; - options = {}; - } - - var self = this; - - if (typeof cb !== 'function') throw new Error('callback must be provided'); - - if (this.readyState !== WebSocket.OPEN) { - if (typeof cb === 'function') cb(new Error('not opened')); - else throw new Error('not opened'); - return; - } - - if (this._queue) { - this._queue.push(function () { self.stream(options, cb); }); - return; - } - - options = options || {}; - - if (typeof options.mask === 'undefined') options.mask = !this._isServer; - if (typeof options.compress === 'undefined') options.compress = true; - if (!this.extensions[PerMessageDeflate.extensionName]) { - options.compress = false; - } - - startQueue(this); - - function send(data, final) { - try { - if (self.readyState !== WebSocket.OPEN) throw new Error('not opened'); - options.fin = final === true; - self._sender.send(data, options); - if (!final) process.nextTick(cb.bind(null, null, send)); - else executeQueueSends(self); - } catch (e) { - if (typeof cb === 'function') cb(e); - else { - delete self._queue; - self.emit('error', e); - } - } - } - - process.nextTick(cb.bind(null, null, send)); -}; - -/** - * Immediately shuts down the connection - * - * @api public - */ -WebSocket.prototype.terminate = function terminate() { - if (this.readyState === WebSocket.CLOSED) return; - - if (this._socket) { - this.readyState = WebSocket.CLOSING; - - // End the connection - try { this._socket.end(); } - catch (e) { - // Socket error during end() call, so just destroy it right now - cleanupWebsocketResources.call(this, true); - return; - } - - // Add a timeout to ensure that the connection is completely - // cleaned up within 30 seconds, even if the clean close procedure - // fails for whatever reason - // First cleanup any pre-existing timeout from an earlier "terminate" call, - // if one exists. Otherwise terminate calls in quick succession will leak timeouts - // and hold the program open for `closeTimout` time. - if (this._closeTimer) { clearTimeout(this._closeTimer); } - this._closeTimer = setTimeout(cleanupWebsocketResources.bind(this, true), closeTimeout); - } else if (this.readyState === WebSocket.CONNECTING) { - cleanupWebsocketResources.call(this, true); - } -}; - -/** - * Expose bufferedAmount - * - * @api public - */ -Object.defineProperty(WebSocket.prototype, 'bufferedAmount', { - get: function get() { - var amount = 0; - if (this._socket) { - amount = this._socket.bufferSize || 0; - } - return amount; - } -}); - -/** - * Expose binaryType - * - * This deviates from the W3C interface since ws doesn't support the required - * default "blob" type (instead we define a custom "nodebuffer" type). - * - * @see http://dev.w3.org/html5/websockets/#the-websocket-interface - * @api public - */ -Object.defineProperty(WebSocket.prototype, 'binaryType', { - get: function get() { - return this._binaryType; - }, - set: function set(type) { - if (type === 'arraybuffer' || type === 'nodebuffer') - this._binaryType = type; - else - throw new SyntaxError('unsupported binaryType: must be either "nodebuffer" or "arraybuffer"'); - } -}); - -/** - * Emulates the W3C Browser based WebSocket interface using function members. - * - * @see http://dev.w3.org/html5/websockets/#the-websocket-interface - * @api public - */ -['open', 'error', 'close', 'message'].forEach(function(method) { - Object.defineProperty(WebSocket.prototype, 'on' + method, { - /** - * Returns the current listener - * - * @returns {Mixed} the set function or undefined - * @api public - */ - get: function get() { - var listener = this.listeners(method)[0]; - return listener ? (listener._listener ? listener._listener : listener) : undefined; - }, - - /** - * Start listening for events - * - * @param {Function} listener the listener - * @returns {Mixed} the set function or undefined - * @api public - */ - set: function set(listener) { - this.removeAllListeners(method); - this.addEventListener(method, listener); - } - }); -}); - -/** - * Emulates the W3C Browser based WebSocket interface using addEventListener. - * - * @see https://developer.mozilla.org/en/DOM/element.addEventListener - * @see http://dev.w3.org/html5/websockets/#the-websocket-interface - * @api public - */ -WebSocket.prototype.addEventListener = function(method, listener) { - var target = this; - - function onMessage (data, flags) { - if (flags.binary && this.binaryType === 'arraybuffer') - data = new Uint8Array(data).buffer; - listener.call(target, new MessageEvent(data, !!flags.binary, target)); - } - - function onClose (code, message) { - listener.call(target, new CloseEvent(code, message, target)); - } - - function onError (event) { - event.type = 'error'; - event.target = target; - listener.call(target, event); - } - - function onOpen () { - listener.call(target, new OpenEvent(target)); - } - - if (typeof listener === 'function') { - if (method === 'message') { - // store a reference so we can return the original function from the - // addEventListener hook - onMessage._listener = listener; - this.on(method, onMessage); - } else if (method === 'close') { - // store a reference so we can return the original function from the - // addEventListener hook - onClose._listener = listener; - this.on(method, onClose); - } else if (method === 'error') { - // store a reference so we can return the original function from the - // addEventListener hook - onError._listener = listener; - this.on(method, onError); - } else if (method === 'open') { - // store a reference so we can return the original function from the - // addEventListener hook - onOpen._listener = listener; - this.on(method, onOpen); - } else { - this.on(method, listener); - } - } -}; - -module.exports = WebSocket; -module.exports.buildHostHeader = buildHostHeader - -/** - * W3C MessageEvent - * - * @see http://www.w3.org/TR/html5/comms.html - * @constructor - * @api private - */ -function MessageEvent(dataArg, isBinary, target) { - this.type = 'message'; - this.data = dataArg; - this.target = target; - this.binary = isBinary; // non-standard. -} - -/** - * W3C CloseEvent - * - * @see http://www.w3.org/TR/html5/comms.html - * @constructor - * @api private - */ -function CloseEvent(code, reason, target) { - this.type = 'close'; - this.wasClean = (typeof code === 'undefined' || code === 1000); - this.code = code; - this.reason = reason; - this.target = target; -} - -/** - * W3C OpenEvent - * - * @see http://www.w3.org/TR/html5/comms.html - * @constructor - * @api private - */ -function OpenEvent(target) { - this.type = 'open'; - this.target = target; -} - -// Append port number to Host header, only if specified in the url -// and non-default -function buildHostHeader(isSecure, hostname, port) { - var headerHost = hostname; - if (hostname) { - if ((isSecure && (port != 443)) || (!isSecure && (port != 80))){ - headerHost = headerHost + ':' + port; - } - } - return headerHost; -} - -/** - * Entirely private apis, - * which may or may not be bound to a sepcific WebSocket instance. - */ -function initAsServerClient(req, socket, upgradeHead, options) { - options = new Options({ - protocolVersion: protocolVersion, - protocol: null, - extensions: {}, - maxPayload: 0 - }).merge(options); - - // expose state properties - this.protocol = options.value.protocol; - this.protocolVersion = options.value.protocolVersion; - this.extensions = options.value.extensions; - this.supports.binary = (this.protocolVersion !== 'hixie-76'); - this.upgradeReq = req; - this.readyState = WebSocket.CONNECTING; - this._isServer = true; - this.maxPayload = options.value.maxPayload; - // establish connection - if (options.value.protocolVersion === 'hixie-76') { - establishConnection.call(this, ReceiverHixie, SenderHixie, socket, upgradeHead); - } else { - establishConnection.call(this, Receiver, Sender, socket, upgradeHead); - } -} - -function initAsClient(address, protocols, options) { - options = new Options({ - origin: null, - protocolVersion: protocolVersion, - host: null, - headers: null, - protocol: protocols.join(','), - agent: null, - - // ssl-related options - pfx: null, - key: null, - passphrase: null, - cert: null, - ca: null, - ciphers: null, - rejectUnauthorized: null, - perMessageDeflate: true, - localAddress: null - }).merge(options); - - if (options.value.protocolVersion !== 8 && options.value.protocolVersion !== 13) { - throw new Error('unsupported protocol version'); - } - - // verify URL and establish http class - var serverUrl = url.parse(address); - var isUnixSocket = serverUrl.protocol === 'ws+unix:'; - if (!serverUrl.host && !isUnixSocket) throw new Error('invalid url'); - var isSecure = serverUrl.protocol === 'wss:' || serverUrl.protocol === 'https:'; - var httpObj = isSecure ? https : http; - var port = serverUrl.port || (isSecure ? 443 : 80); - var auth = serverUrl.auth; - - // prepare extensions - var extensionsOffer = {}; - var perMessageDeflate; - if (options.value.perMessageDeflate) { - perMessageDeflate = new PerMessageDeflate(typeof options.value.perMessageDeflate !== true ? options.value.perMessageDeflate : {}, false); - extensionsOffer[PerMessageDeflate.extensionName] = perMessageDeflate.offer(); - } - - // expose state properties - this._isServer = false; - this.url = address; - this.protocolVersion = options.value.protocolVersion; - this.supports.binary = (this.protocolVersion !== 'hixie-76'); - - // begin handshake - var key = new Buffer(options.value.protocolVersion + '-' + Date.now()).toString('base64'); - var shasum = crypto.createHash('sha1'); - shasum.update(key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'); - var expectedServerKey = shasum.digest('base64'); - - var agent = options.value.agent; - - var headerHost = buildHostHeader(isSecure, serverUrl.hostname, port) - - var requestOptions = { - port: port, - host: serverUrl.hostname, - headers: { - 'Connection': 'Upgrade', - 'Upgrade': 'websocket', - 'Host': headerHost, - 'Sec-WebSocket-Version': options.value.protocolVersion, - 'Sec-WebSocket-Key': key - } - }; - - // If we have basic auth. - if (auth) { - requestOptions.headers.Authorization = 'Basic ' + new Buffer(auth).toString('base64'); - } - - if (options.value.protocol) { - requestOptions.headers['Sec-WebSocket-Protocol'] = options.value.protocol; - } - - if (options.value.host) { - requestOptions.headers.Host = options.value.host; - } - - if (options.value.headers) { - for (var header in options.value.headers) { - if (options.value.headers.hasOwnProperty(header)) { - requestOptions.headers[header] = options.value.headers[header]; - } - } - } - - if (Object.keys(extensionsOffer).length) { - requestOptions.headers['Sec-WebSocket-Extensions'] = Extensions.format(extensionsOffer); - } - - if (options.isDefinedAndNonNull('pfx') - || options.isDefinedAndNonNull('key') - || options.isDefinedAndNonNull('passphrase') - || options.isDefinedAndNonNull('cert') - || options.isDefinedAndNonNull('ca') - || options.isDefinedAndNonNull('ciphers') - || options.isDefinedAndNonNull('rejectUnauthorized')) { - - if (options.isDefinedAndNonNull('pfx')) requestOptions.pfx = options.value.pfx; - if (options.isDefinedAndNonNull('key')) requestOptions.key = options.value.key; - if (options.isDefinedAndNonNull('passphrase')) requestOptions.passphrase = options.value.passphrase; - if (options.isDefinedAndNonNull('cert')) requestOptions.cert = options.value.cert; - if (options.isDefinedAndNonNull('ca')) requestOptions.ca = options.value.ca; - if (options.isDefinedAndNonNull('ciphers')) requestOptions.ciphers = options.value.ciphers; - if (options.isDefinedAndNonNull('rejectUnauthorized')) requestOptions.rejectUnauthorized = options.value.rejectUnauthorized; - - if (!agent) { - // global agent ignores client side certificates - agent = new httpObj.Agent(requestOptions); - } - } - - requestOptions.path = serverUrl.path || '/'; - - if (agent) { - requestOptions.agent = agent; - } - - if (isUnixSocket) { - requestOptions.socketPath = serverUrl.pathname; - } - - if (options.value.localAddress) { - requestOptions.localAddress = options.value.localAddress; - } - - if (options.value.origin) { - if (options.value.protocolVersion < 13) requestOptions.headers['Sec-WebSocket-Origin'] = options.value.origin; - else requestOptions.headers.Origin = options.value.origin; - } - - var self = this; - var req = httpObj.request(requestOptions); - - req.on('error', function onerror(error) { - self.emit('error', error); - cleanupWebsocketResources.call(self, error); - }); - - req.once('response', function response(res) { - var error; - - if (!self.emit('unexpected-response', req, res)) { - error = new Error('unexpected server response (' + res.statusCode + ')'); - req.abort(); - self.emit('error', error); - } - - cleanupWebsocketResources.call(self, error); - }); - - req.once('upgrade', function upgrade(res, socket, upgradeHead) { - if (self.readyState === WebSocket.CLOSED) { - // client closed before server accepted connection - self.emit('close'); - self.removeAllListeners(); - socket.end(); - return; - } - - var serverKey = res.headers['sec-websocket-accept']; - if (typeof serverKey === 'undefined' || serverKey !== expectedServerKey) { - self.emit('error', 'invalid server key'); - self.removeAllListeners(); - socket.end(); - return; - } - - var serverProt = res.headers['sec-websocket-protocol']; - var protList = (options.value.protocol || "").split(/, */); - var protError = null; - - if (!options.value.protocol && serverProt) { - protError = 'server sent a subprotocol even though none requested'; - } else if (options.value.protocol && !serverProt) { - protError = 'server sent no subprotocol even though requested'; - } else if (serverProt && protList.indexOf(serverProt) === -1) { - protError = 'server responded with an invalid protocol'; - } - - if (protError) { - self.emit('error', protError); - self.removeAllListeners(); - socket.end(); - return; - } else if (serverProt) { - self.protocol = serverProt; - } - - var serverExtensions = Extensions.parse(res.headers['sec-websocket-extensions']); - if (perMessageDeflate && serverExtensions[PerMessageDeflate.extensionName]) { - try { - perMessageDeflate.accept(serverExtensions[PerMessageDeflate.extensionName]); - } catch (err) { - self.emit('error', 'invalid extension parameter'); - self.removeAllListeners(); - socket.end(); - return; - } - self.extensions[PerMessageDeflate.extensionName] = perMessageDeflate; - } - - establishConnection.call(self, Receiver, Sender, socket, upgradeHead); - - // perform cleanup on http resources - req.removeAllListeners(); - req = null; - agent = null; - }); - - req.end(); - this.readyState = WebSocket.CONNECTING; -} - -function establishConnection(ReceiverClass, SenderClass, socket, upgradeHead) { - var ultron = this._ultron = new Ultron(socket) - , called = false - , self = this; - - socket.setTimeout(0); - socket.setNoDelay(true); - - this._receiver = new ReceiverClass(this.extensions,this.maxPayload); - this._socket = socket; - - // socket cleanup handlers - ultron.on('end', cleanupWebsocketResources.bind(this)); - ultron.on('close', cleanupWebsocketResources.bind(this)); - ultron.on('error', cleanupWebsocketResources.bind(this)); - - // ensure that the upgradeHead is added to the receiver - function firstHandler(data) { - if (called || self.readyState === WebSocket.CLOSED) return; - - called = true; - socket.removeListener('data', firstHandler); - ultron.on('data', realHandler); - - if (upgradeHead && upgradeHead.length > 0) { - realHandler(upgradeHead); - upgradeHead = null; - } - - if (data) realHandler(data); - } - - // subsequent packets are pushed straight to the receiver - function realHandler(data) { - self.bytesReceived += data.length; - self._receiver.add(data); - } - - ultron.on('data', firstHandler); - - // if data was passed along with the http upgrade, - // this will schedule a push of that on to the receiver. - // this has to be done on next tick, since the caller - // hasn't had a chance to set event handlers on this client - // object yet. - process.nextTick(firstHandler); - - // receiver event handlers - self._receiver.ontext = function ontext(data, flags) { - flags = flags || {}; - - self.emit('message', data, flags); - }; - - self._receiver.onbinary = function onbinary(data, flags) { - flags = flags || {}; - - flags.binary = true; - self.emit('message', data, flags); - }; - - self._receiver.onping = function onping(data, flags) { - flags = flags || {}; - - self.pong(data, { - mask: !self._isServer, - binary: flags.binary === true - }, true); - - self.emit('ping', data, flags); - }; - - self._receiver.onpong = function onpong(data, flags) { - self.emit('pong', data, flags || {}); - }; - - self._receiver.onclose = function onclose(code, data, flags) { - flags = flags || {}; - - self._closeReceived = true; - self.close(code, data); - }; - - self._receiver.onerror = function onerror(reason, errorCode) { - // close the connection when the receiver reports a HyBi error code - self.close(typeof errorCode !== 'undefined' ? errorCode : 1002, ''); - self.emit('error', (reason instanceof Error) ? reason : (new Error(reason))); - }; - - // finalize the client - this._sender = new SenderClass(socket, this.extensions); - this._sender.on('error', function onerror(error) { - self.close(1002, ''); - self.emit('error', error); - }); - - this.readyState = WebSocket.OPEN; - this.emit('open'); -} - -function startQueue(instance) { - instance._queue = instance._queue || []; -} - -function executeQueueSends(instance) { - var queue = instance._queue; - if (typeof queue === 'undefined') return; - - delete instance._queue; - for (var i = 0, l = queue.length; i < l; ++i) { - queue[i](); - } -} - -function sendStream(instance, stream, options, cb) { - stream.on('data', function incoming(data) { - if (instance.readyState !== WebSocket.OPEN) { - if (typeof cb === 'function') cb(new Error('not opened')); - else { - delete instance._queue; - instance.emit('error', new Error('not opened')); - } - return; - } - - options.fin = false; - instance._sender.send(data, options); - }); - - stream.on('end', function end() { - if (instance.readyState !== WebSocket.OPEN) { - if (typeof cb === 'function') cb(new Error('not opened')); - else { - delete instance._queue; - instance.emit('error', new Error('not opened')); - } - return; - } - - options.fin = true; - instance._sender.send(null, options); - - if (typeof cb === 'function') cb(null); - }); -} - -function cleanupWebsocketResources(error) { - if (this.readyState === WebSocket.CLOSED) return; - - this.readyState = WebSocket.CLOSED; - - clearTimeout(this._closeTimer); - this._closeTimer = null; - - // If the connection was closed abnormally (with an error), or if - // the close control frame was not received then the close code - // must default to 1006. - if (error || !this._closeReceived) { - this._closeCode = 1006; - } - this.emit('close', this._closeCode || 1000, this._closeMessage || ''); - - if (this._socket) { - if (this._ultron) this._ultron.destroy(); - this._socket.on('error', function onerror() { - try { this.destroy(); } - catch (e) {} - }); - - try { - if (!error) this._socket.end(); - else this._socket.destroy(); - } catch (e) { /* Ignore termination errors */ } - - this._socket = null; - this._ultron = null; - } - - if (this._sender) { - this._sender.removeAllListeners(); - this._sender = null; - } - - if (this._receiver) { - this._receiver.cleanup(); - this._receiver = null; - } - - if (this.extensions[PerMessageDeflate.extensionName]) { - this.extensions[PerMessageDeflate.extensionName].cleanup(); - } - - this.extensions = null; - - this.removeAllListeners(); - this.on('error', function onerror() {}); // catch all errors after this - delete this._queue; -} diff --git a/lib/WebSocketServer.js b/lib/WebSocketServer.js deleted file mode 100644 index 92077cd5a..000000000 --- a/lib/WebSocketServer.js +++ /dev/null @@ -1,554 +0,0 @@ -/*! - * ws: a node.js websocket client - * Copyright(c) 2011 Einar Otto Stangvik - * MIT Licensed - */ - -var util = require('util') - , events = require('events') - , http = require('http') - , crypto = require('crypto') - , Options = require('options') - , WebSocket = require('./WebSocket') - , Extensions = require('./Extensions') - , PerMessageDeflate = require('./PerMessageDeflate') - , tls = require('tls') - , url = require('url'); - -/** - * WebSocket Server implementation - */ - -function WebSocketServer(options, callback) { - if (this instanceof WebSocketServer === false) { - return new WebSocketServer(options, callback); - } - - events.EventEmitter.call(this); - - options = new Options({ - host: '0.0.0.0', - port: null, - server: null, - verifyClient: null, - handleProtocols: null, - path: null, - noServer: false, - disableHixie: false, - clientTracking: true, - perMessageDeflate: true, - maxPayload: 100 * 1024 * 1024 - }).merge(options); - - if (!options.isDefinedAndNonNull('port') && !options.isDefinedAndNonNull('server') && !options.value.noServer) { - throw new TypeError('`port` or a `server` must be provided'); - } - - var self = this; - - if (options.isDefinedAndNonNull('port')) { - this._server = http.createServer(function (req, res) { - var body = http.STATUS_CODES[426]; - res.writeHead(426, { - 'Content-Length': body.length, - 'Content-Type': 'text/plain' - }); - res.end(body); - }); - this._server.allowHalfOpen = false; - this._server.listen(options.value.port, options.value.host, callback); - this._closeServer = function() { if (self._server) self._server.close(); }; - } - else if (options.value.server) { - this._server = options.value.server; - if (options.value.path) { - // take note of the path, to avoid collisions when multiple websocket servers are - // listening on the same http server - if (this._server._webSocketPaths && options.value.server._webSocketPaths[options.value.path]) { - throw new Error('two instances of WebSocketServer cannot listen on the same http server path'); - } - if (typeof this._server._webSocketPaths !== 'object') { - this._server._webSocketPaths = {}; - } - this._server._webSocketPaths[options.value.path] = 1; - } - } - if (this._server) { - this._onceServerListening = function() { self.emit('listening'); }; - this._server.once('listening', this._onceServerListening); - } - - if (typeof this._server != 'undefined') { - this._onServerError = function(error) { self.emit('error', error) }; - this._server.on('error', this._onServerError); - this._onServerUpgrade = function(req, socket, upgradeHead) { - //copy upgradeHead to avoid retention of large slab buffers used in node core - var head = new Buffer(upgradeHead.length); - upgradeHead.copy(head); - - self.handleUpgrade(req, socket, head, function(client) { - self.emit('connection'+req.url, client); - self.emit('connection', client); - }); - }; - this._server.on('upgrade', this._onServerUpgrade); - } - - this.options = options.value; - this.path = options.value.path; - this.clients = []; -} - -/** - * Inherits from EventEmitter. - */ - -util.inherits(WebSocketServer, events.EventEmitter); - -/** - * Immediately shuts down the connection. - * - * @api public - */ - -WebSocketServer.prototype.close = function(callback) { - // terminate all associated clients - var error = null; - try { - for (var i = 0, l = this.clients.length; i < l; ++i) { - this.clients[i].terminate(); - } - } - catch (e) { - error = e; - } - - // remove path descriptor, if any - if (this.path && this._server._webSocketPaths) { - delete this._server._webSocketPaths[this.path]; - if (Object.keys(this._server._webSocketPaths).length == 0) { - delete this._server._webSocketPaths; - } - } - - // close the http server if it was internally created - try { - if (typeof this._closeServer !== 'undefined') { - this._closeServer(); - } - } - finally { - if (this._server) { - this._server.removeListener('listening', this._onceServerListening); - this._server.removeListener('error', this._onServerError); - this._server.removeListener('upgrade', this._onServerUpgrade); - } - delete this._server; - } - if(callback) - callback(error); - else if(error) - throw error; -} - -/** - * Handle a HTTP Upgrade request. - * - * @api public - */ - -WebSocketServer.prototype.handleUpgrade = function(req, socket, upgradeHead, cb) { - // check for wrong path - if (this.options.path) { - var u = url.parse(req.url); - if (u && u.pathname !== this.options.path) return; - } - - if (typeof req.headers.upgrade === 'undefined' || req.headers.upgrade.toLowerCase() !== 'websocket') { - abortConnection(socket, 400, 'Bad Request'); - return; - } - - if (req.headers['sec-websocket-key1']) handleHixieUpgrade.apply(this, arguments); - else handleHybiUpgrade.apply(this, arguments); -} - -module.exports = WebSocketServer; - -/** - * Entirely private apis, - * which may or may not be bound to a sepcific WebSocket instance. - */ - -function handleHybiUpgrade(req, socket, upgradeHead, cb) { - // handle premature socket errors - var errorHandler = function() { - try { socket.destroy(); } catch (e) {} - } - socket.on('error', errorHandler); - - // verify key presence - if (!req.headers['sec-websocket-key']) { - abortConnection(socket, 400, 'Bad Request'); - return; - } - - // verify version - var version = parseInt(req.headers['sec-websocket-version']); - if ([8, 13].indexOf(version) === -1) { - abortConnection(socket, 400, 'Bad Request'); - return; - } - - // verify protocol - var protocols = req.headers['sec-websocket-protocol']; - - // verify client - var origin = version < 13 ? - req.headers['sec-websocket-origin'] : - req.headers['origin']; - - // handle extensions offer - var extensionsOffer = Extensions.parse(req.headers['sec-websocket-extensions']); - - // handler to call when the connection sequence completes - var self = this; - var completeHybiUpgrade2 = function(protocol) { - - // calc key - var key = req.headers['sec-websocket-key']; - var shasum = crypto.createHash('sha1'); - shasum.update(key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"); - key = shasum.digest('base64'); - - var headers = [ - 'HTTP/1.1 101 Switching Protocols' - , 'Upgrade: websocket' - , 'Connection: Upgrade' - , 'Sec-WebSocket-Accept: ' + key - ]; - - if (typeof protocol != 'undefined') { - headers.push('Sec-WebSocket-Protocol: ' + protocol); - } - - var extensions = {}; - try { - extensions = acceptExtensions.call(self, extensionsOffer); - } catch (err) { - abortConnection(socket, 400, 'Bad Request'); - return; - } - - if (Object.keys(extensions).length) { - var serverExtensions = {}; - Object.keys(extensions).forEach(function(token) { - serverExtensions[token] = [extensions[token].params] - }); - headers.push('Sec-WebSocket-Extensions: ' + Extensions.format(serverExtensions)); - } - - // allows external modification/inspection of handshake headers - self.emit('headers', headers); - - socket.setTimeout(0); - socket.setNoDelay(true); - try { - socket.write(headers.concat('', '').join('\r\n')); - } - catch (e) { - // if the upgrade write fails, shut the connection down hard - try { socket.destroy(); } catch (e) {} - return; - } - - var client = new WebSocket([req, socket, upgradeHead], { - protocolVersion: version, - protocol: protocol, - extensions: extensions, - maxPayload: self.options.maxPayload - }); - - if (self.options.clientTracking) { - self.clients.push(client); - client.on('close', function() { - var index = self.clients.indexOf(client); - if (index != -1) { - self.clients.splice(index, 1); - } - }); - } - - // signal upgrade complete - socket.removeListener('error', errorHandler); - cb(client); - } - - // optionally call external protocol selection handler before - // calling completeHybiUpgrade2 - var completeHybiUpgrade1 = function() { - // choose from the sub-protocols - if (typeof self.options.handleProtocols == 'function') { - var protList = (protocols || "").split(/, */); - var callbackCalled = false; - var res = self.options.handleProtocols(protList, function(result, protocol) { - callbackCalled = true; - if (!result) abortConnection(socket, 401, 'Unauthorized'); - else completeHybiUpgrade2(protocol); - }); - if (!callbackCalled) { - // the handleProtocols handler never called our callback - abortConnection(socket, 501, 'Could not process protocols'); - } - return; - } else { - if (typeof protocols !== 'undefined') { - completeHybiUpgrade2(protocols.split(/, */)[0]); - } - else { - completeHybiUpgrade2(); - } - } - } - - // optionally call external client verification handler - if (typeof this.options.verifyClient == 'function') { - var info = { - origin: origin, - secure: typeof req.connection.authorized !== 'undefined' || typeof req.connection.encrypted !== 'undefined', - req: req - }; - if (this.options.verifyClient.length == 2) { - this.options.verifyClient(info, function(result, code, name) { - if (typeof code === 'undefined') code = 401; - if (typeof name === 'undefined') name = http.STATUS_CODES[code]; - - if (!result) abortConnection(socket, code, name); - else completeHybiUpgrade1(); - }); - return; - } - else if (!this.options.verifyClient(info)) { - abortConnection(socket, 401, 'Unauthorized'); - return; - } - } - - completeHybiUpgrade1(); -} - -function handleHixieUpgrade(req, socket, upgradeHead, cb) { - // handle premature socket errors - var errorHandler = function() { - try { socket.destroy(); } catch (e) {} - } - socket.on('error', errorHandler); - - // bail if options prevent hixie - if (this.options.disableHixie) { - abortConnection(socket, 401, 'Hixie support disabled'); - return; - } - - // verify key presence - if (!req.headers['sec-websocket-key2']) { - abortConnection(socket, 400, 'Bad Request'); - return; - } - - var origin = req.headers['origin'] - , self = this; - - // setup handshake completion to run after client has been verified - var onClientVerified = function() { - var wshost; - if (!req.headers['x-forwarded-host']) - wshost = req.headers.host; - else - wshost = req.headers['x-forwarded-host']; - var location = ((req.headers['x-forwarded-proto'] === 'https' || socket.encrypted) ? 'wss' : 'ws') + '://' + wshost + req.url - , protocol = req.headers['sec-websocket-protocol']; - - // build the response header and return a Buffer - var buildResponseHeader = function() { - var headers = [ - 'HTTP/1.1 101 Switching Protocols' - , 'Upgrade: WebSocket' - , 'Connection: Upgrade' - , 'Sec-WebSocket-Location: ' + location - ]; - if (typeof protocol != 'undefined') headers.push('Sec-WebSocket-Protocol: ' + protocol); - if (typeof origin != 'undefined') headers.push('Sec-WebSocket-Origin: ' + origin); - - return new Buffer(headers.concat('', '').join('\r\n')); - }; - - // send handshake response before receiving the nonce - var handshakeResponse = function() { - - socket.setTimeout(0); - socket.setNoDelay(true); - - var headerBuffer = buildResponseHeader(); - - try { - socket.write(headerBuffer, 'binary', function(err) { - // remove listener if there was an error - if (err) socket.removeListener('data', handler); - return; - }); - } catch (e) { - try { socket.destroy(); } catch (e) {} - return; - }; - }; - - // handshake completion code to run once nonce has been successfully retrieved - var completeHandshake = function(nonce, rest, headerBuffer) { - // calculate key - var k1 = req.headers['sec-websocket-key1'] - , k2 = req.headers['sec-websocket-key2'] - , md5 = crypto.createHash('md5'); - - [k1, k2].forEach(function (k) { - var n = parseInt(k.replace(/[^\d]/g, '')) - , spaces = k.replace(/[^ ]/g, '').length; - if (spaces === 0 || n % spaces !== 0){ - abortConnection(socket, 400, 'Bad Request'); - return; - } - n /= spaces; - md5.update(String.fromCharCode( - n >> 24 & 0xFF, - n >> 16 & 0xFF, - n >> 8 & 0xFF, - n & 0xFF)); - }); - md5.update(nonce.toString('binary')); - - socket.setTimeout(0); - socket.setNoDelay(true); - - try { - var hashBuffer = new Buffer(md5.digest('binary'), 'binary'); - var handshakeBuffer = new Buffer(headerBuffer.length + hashBuffer.length); - headerBuffer.copy(handshakeBuffer, 0); - hashBuffer.copy(handshakeBuffer, headerBuffer.length); - - // do a single write, which - upon success - causes a new client websocket to be setup - socket.write(handshakeBuffer, 'binary', function(err) { - if (err) return; // do not create client if an error happens - var client = new WebSocket([req, socket, rest], { - protocolVersion: 'hixie-76', - protocol: protocol - }); - if (self.options.clientTracking) { - self.clients.push(client); - client.on('close', function() { - var index = self.clients.indexOf(client); - if (index != -1) { - self.clients.splice(index, 1); - } - }); - } - - // signal upgrade complete - socket.removeListener('error', errorHandler); - cb(client); - }); - } - catch (e) { - try { socket.destroy(); } catch (e) {} - return; - } - } - - // retrieve nonce - var nonceLength = 8; - if (upgradeHead && upgradeHead.length >= nonceLength) { - var nonce = upgradeHead.slice(0, nonceLength); - var rest = upgradeHead.length > nonceLength ? upgradeHead.slice(nonceLength) : null; - completeHandshake.call(self, nonce, rest, buildResponseHeader()); - } - else { - // nonce not present in upgradeHead - var nonce = new Buffer(nonceLength); - upgradeHead.copy(nonce, 0); - var received = upgradeHead.length; - var rest = null; - var handler = function (data) { - var toRead = Math.min(data.length, nonceLength - received); - if (toRead === 0) return; - data.copy(nonce, received, 0, toRead); - received += toRead; - if (received == nonceLength) { - socket.removeListener('data', handler); - if (toRead < data.length) rest = data.slice(toRead); - - // complete the handshake but send empty buffer for headers since they have already been sent - completeHandshake.call(self, nonce, rest, new Buffer(0)); - } - } - - // handle additional data as we receive it - socket.on('data', handler); - - // send header response before we have the nonce to fix haproxy buffering - handshakeResponse(); - } - } - - // verify client - if (typeof this.options.verifyClient == 'function') { - var info = { - origin: origin, - secure: typeof req.connection.authorized !== 'undefined' || typeof req.connection.encrypted !== 'undefined', - req: req - }; - if (this.options.verifyClient.length == 2) { - var self = this; - this.options.verifyClient(info, function(result, code, name) { - if (typeof code === 'undefined') code = 401; - if (typeof name === 'undefined') name = http.STATUS_CODES[code]; - - if (!result) abortConnection(socket, code, name); - else onClientVerified.apply(self); - }); - return; - } - else if (!this.options.verifyClient(info)) { - abortConnection(socket, 401, 'Unauthorized'); - return; - } - } - - // no client verification required - onClientVerified(); -} - -function acceptExtensions(offer) { - var extensions = {}; - var options = this.options.perMessageDeflate; - var maxPayload = this.options.maxPayload; - if (options && offer[PerMessageDeflate.extensionName]) { - var perMessageDeflate = new PerMessageDeflate(options !== true ? options : {}, true, maxPayload); - perMessageDeflate.accept(offer[PerMessageDeflate.extensionName]); - extensions[PerMessageDeflate.extensionName] = perMessageDeflate; - } - return extensions; -} - -function abortConnection(socket, code, name) { - try { - var response = [ - 'HTTP/1.1 ' + code + ' ' + name, - 'Content-type: text/html' - ]; - socket.write(response.concat('', '').join('\r\n')); - } - catch (e) { /* ignore errors - we've aborted this connection */ } - finally { - // ensure that an early aborted connection is shut down completely - try { socket.destroy(); } catch (e) {} - } -} diff --git a/lib/buffer-util.js b/lib/buffer-util.js new file mode 100644 index 000000000..6974dd6a8 --- /dev/null +++ b/lib/buffer-util.js @@ -0,0 +1,72 @@ +'use strict'; + +/** + * Merges an array of buffers into a new buffer. + * + * @param {Buffer[]} list The array of buffers to concat + * @param {Number} totalLength The total length of buffers in the list + * @return {Buffer} The resulting buffer + * @public + */ +function concat (list, totalLength) { + const target = Buffer.allocUnsafe(totalLength); + var offset = 0; + + for (var i = 0; i < list.length; i++) { + const buf = list[i]; + buf.copy(target, offset); + offset += buf.length; + } + + return target; +} + +/** + * Masks a buffer using the given mask. + * + * @param {Buffer} source The buffer to mask + * @param {Buffer} mask The mask to use + * @param {Buffer} output The buffer where to store the result + * @param {Number} offset The offset at which to start writing + * @param {Number} length The number of bytes to mask. + * @public + */ +function _mask (source, mask, output, offset, length) { + for (var i = 0; i < length; i++) { + output[offset + i] = source[i] ^ mask[i & 3]; + } +} + +/** + * Unmasks a buffer using the given mask. + * + * @param {Buffer} buffer The buffer to unmask + * @param {Buffer} mask The mask to use + * @public + */ +function _unmask (buffer, mask) { + // Required until https://github.com/nodejs/node/issues/9006 is resolved. + const length = buffer.length; + for (var i = 0; i < length; i++) { + buffer[i] ^= mask[i & 3]; + } +} + +try { + const bufferUtil = require('bufferutil'); + const bu = bufferUtil.BufferUtil || bufferUtil; + + module.exports = { + mask (source, mask, output, offset, length) { + if (length < 48) _mask(source, mask, output, offset, length); + else bu.mask(source, mask, output, offset, length); + }, + unmask (buffer, mask) { + if (buffer.length < 32) _unmask(buffer, mask); + else bu.unmask(buffer, mask); + }, + concat + }; +} catch (e) /* istanbul ignore next */ { + module.exports = { concat, mask: _mask, unmask: _unmask }; +} diff --git a/lib/constants.js b/lib/constants.js new file mode 100644 index 000000000..4082981f8 --- /dev/null +++ b/lib/constants.js @@ -0,0 +1,10 @@ +'use strict'; + +module.exports = { + BINARY_TYPES: ['nodebuffer', 'arraybuffer', 'fragments'], + GUID: '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', + kStatusCode: Symbol('status-code'), + kWebSocket: Symbol('websocket'), + EMPTY_BUFFER: Buffer.alloc(0), + NOOP: () => {} +}; diff --git a/lib/event-target.js b/lib/event-target.js new file mode 100644 index 000000000..574e9080e --- /dev/null +++ b/lib/event-target.js @@ -0,0 +1,170 @@ +'use strict'; + +/** + * Class representing an event. + * + * @private + */ +class Event { + /** + * Create a new `Event`. + * + * @param {String} type The name of the event + * @param {Object} target A reference to the target to which the event was dispatched + */ + constructor (type, target) { + this.target = target; + this.type = type; + } +} + +/** + * Class representing a message event. + * + * @extends Event + * @private + */ +class MessageEvent extends Event { + /** + * Create a new `MessageEvent`. + * + * @param {(String|Buffer|ArrayBuffer|Buffer[])} data The received data + * @param {WebSocket} target A reference to the target to which the event was dispatched + */ + constructor (data, target) { + super('message', target); + + this.data = data; + } +} + +/** + * Class representing a close event. + * + * @extends Event + * @private + */ +class CloseEvent extends Event { + /** + * Create a new `CloseEvent`. + * + * @param {Number} code The status code explaining why the connection is being closed + * @param {String} reason A human-readable string explaining why the connection is closing + * @param {WebSocket} target A reference to the target to which the event was dispatched + */ + constructor (code, reason, target) { + super('close', target); + + this.wasClean = target._closeFrameReceived && target._closeFrameSent; + this.reason = reason; + this.code = code; + } +} + +/** + * Class representing an open event. + * + * @extends Event + * @private + */ +class OpenEvent extends Event { + /** + * Create a new `OpenEvent`. + * + * @param {WebSocket} target A reference to the target to which the event was dispatched + */ + constructor (target) { + super('open', target); + } +} + +/** + * Class representing an error event. + * + * @extends Event + * @private + */ +class ErrorEvent extends Event { + /** + * Create a new `ErrorEvent`. + * + * @param {Object} error The error that generated this event + * @param {WebSocket} target A reference to the target to which the event was dispatched + */ + constructor (error, target) { + super('error', target); + + this.message = error.message; + this.error = error; + } +} + +/** + * This provides methods for emulating the `EventTarget` interface. It's not + * meant to be used directly. + * + * @mixin + */ +const EventTarget = { + /** + * Register an event listener. + * + * @param {String} method A string representing the event type to listen for + * @param {Function} listener The listener to add + * @public + */ + addEventListener (method, listener) { + if (typeof listener !== 'function') return; + + function onMessage (data) { + listener.call(this, new MessageEvent(data, this)); + } + + function onClose (code, message) { + listener.call(this, new CloseEvent(code, message, this)); + } + + function onError (error) { + listener.call(this, new ErrorEvent(error, this)); + } + + function onOpen () { + listener.call(this, new OpenEvent(this)); + } + + if (method === 'message') { + onMessage._listener = listener; + this.on(method, onMessage); + } else if (method === 'close') { + onClose._listener = listener; + this.on(method, onClose); + } else if (method === 'error') { + onError._listener = listener; + this.on(method, onError); + } else if (method === 'open') { + onOpen._listener = listener; + this.on(method, onOpen); + } else { + this.on(method, listener); + } + }, + + /** + * Remove an event listener. + * + * @param {String} method A string representing the event type to remove + * @param {Function} listener The listener to remove + * @public + */ + removeEventListener (method, listener) { + const listeners = this.listeners(method); + + for (var i = 0; i < listeners.length; i++) { + if (listeners[i] === listener || listeners[i]._listener === listener) { + this.removeListener(method, listeners[i]); + } + } + } +}; + +module.exports = EventTarget; diff --git a/lib/extension.js b/lib/extension.js new file mode 100644 index 000000000..3f48d7517 --- /dev/null +++ b/lib/extension.js @@ -0,0 +1,211 @@ +'use strict'; + +// +// Allowed token characters: +// +// '!', '#', '$', '%', '&', ''', '*', '+', '-', +// '.', 0-9, A-Z, '^', '_', '`', a-z, '|', '~' +// +// tokenChars[32] === 0 // ' ' +// tokenChars[33] === 1 // '!' +// tokenChars[34] === 0 // '"' +// ... +// +const tokenChars = [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0 - 15 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 16 - 31 + 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, // 32 - 47 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, // 48 - 63 + 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 64 - 79 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, // 80 - 95 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 96 - 111 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0 // 112 - 127 +]; + +/** + * Adds an offer to the map of extension offers or a parameter to the map of + * parameters. + * + * @param {Object} dest The map of extension offers or parameters + * @param {String} name The extension or parameter name + * @param {(Object|Boolean|String)} elem The extension parameters or the + * parameter value + * @private + */ +function push (dest, name, elem) { + if (Object.prototype.hasOwnProperty.call(dest, name)) dest[name].push(elem); + else dest[name] = [elem]; +} + +/** + * Parses the `Sec-WebSocket-Extensions` header into an object. + * + * @param {String} header The field value of the header + * @return {Object} The parsed object + * @public + */ +function parse (header) { + const offers = {}; + + if (header === undefined || header === '') return offers; + + var params = {}; + var mustUnescape = false; + var isEscaping = false; + var inQuotes = false; + var extensionName; + var paramName; + var start = -1; + var end = -1; + + for (var i = 0; i < header.length; i++) { + const code = header.charCodeAt(i); + + if (extensionName === undefined) { + if (end === -1 && tokenChars[code] === 1) { + if (start === -1) start = i; + } else if (code === 0x20/* ' ' */|| code === 0x09/* '\t' */) { + if (end === -1 && start !== -1) end = i; + } else if (code === 0x3b/* ';' */ || code === 0x2c/* ',' */) { + if (start === -1) { + throw new SyntaxError(`Unexpected character at index ${i}`); + } + + if (end === -1) end = i; + const name = header.slice(start, end); + if (code === 0x2c) { + push(offers, name, params); + params = {}; + } else { + extensionName = name; + } + + start = end = -1; + } else { + throw new SyntaxError(`Unexpected character at index ${i}`); + } + } else if (paramName === undefined) { + if (end === -1 && tokenChars[code] === 1) { + if (start === -1) start = i; + } else if (code === 0x20 || code === 0x09) { + if (end === -1 && start !== -1) end = i; + } else if (code === 0x3b || code === 0x2c) { + if (start === -1) { + throw new SyntaxError(`Unexpected character at index ${i}`); + } + + if (end === -1) end = i; + push(params, header.slice(start, end), true); + if (code === 0x2c) { + push(offers, extensionName, params); + params = {}; + extensionName = undefined; + } + + start = end = -1; + } else if (code === 0x3d/* '=' */&& start !== -1 && end === -1) { + paramName = header.slice(start, i); + start = end = -1; + } else { + throw new SyntaxError(`Unexpected character at index ${i}`); + } + } else { + // + // The value of a quoted-string after unescaping must conform to the + // token ABNF, so only token characters are valid. + // Ref: https://tools.ietf.org/html/rfc6455#section-9.1 + // + if (isEscaping) { + if (tokenChars[code] !== 1) { + throw new SyntaxError(`Unexpected character at index ${i}`); + } + if (start === -1) start = i; + else if (!mustUnescape) mustUnescape = true; + isEscaping = false; + } else if (inQuotes) { + if (tokenChars[code] === 1) { + if (start === -1) start = i; + } else if (code === 0x22/* '"' */ && start !== -1) { + inQuotes = false; + end = i; + } else if (code === 0x5c/* '\' */) { + isEscaping = true; + } else { + throw new SyntaxError(`Unexpected character at index ${i}`); + } + } else if (code === 0x22 && header.charCodeAt(i - 1) === 0x3d) { + inQuotes = true; + } else if (end === -1 && tokenChars[code] === 1) { + if (start === -1) start = i; + } else if (start !== -1 && (code === 0x20 || code === 0x09)) { + if (end === -1) end = i; + } else if (code === 0x3b || code === 0x2c) { + if (start === -1) { + throw new SyntaxError(`Unexpected character at index ${i}`); + } + + if (end === -1) end = i; + var value = header.slice(start, end); + if (mustUnescape) { + value = value.replace(/\\/g, ''); + mustUnescape = false; + } + push(params, paramName, value); + if (code === 0x2c) { + push(offers, extensionName, params); + params = {}; + extensionName = undefined; + } + + paramName = undefined; + start = end = -1; + } else { + throw new SyntaxError(`Unexpected character at index ${i}`); + } + } + } + + if (start === -1 || inQuotes) { + throw new SyntaxError('Unexpected end of input'); + } + + if (end === -1) end = i; + const token = header.slice(start, end); + if (extensionName === undefined) { + push(offers, token, {}); + } else { + if (paramName === undefined) { + push(params, token, true); + } else if (mustUnescape) { + push(params, paramName, token.replace(/\\/g, '')); + } else { + push(params, paramName, token); + } + push(offers, extensionName, params); + } + + return offers; +} + +/** + * Builds the `Sec-WebSocket-Extensions` header field value. + * + * @param {Object} extensions The map of extensions and parameters to format + * @return {String} A string representing the given object + * @public + */ +function format (extensions) { + return Object.keys(extensions).map((extension) => { + var configurations = extensions[extension]; + if (!Array.isArray(configurations)) configurations = [configurations]; + return configurations.map((params) => { + return [extension].concat(Object.keys(params).map((k) => { + var values = params[k]; + if (!Array.isArray(values)) values = [values]; + return values.map((v) => v === true ? k : `${k}=${v}`).join('; '); + })).join('; '); + }).join(', '); + }).join(', '); +} + +module.exports = { format, parse }; diff --git a/lib/permessage-deflate.js b/lib/permessage-deflate.js new file mode 100644 index 000000000..0d54f035b --- /dev/null +++ b/lib/permessage-deflate.js @@ -0,0 +1,516 @@ +'use strict'; + +const Limiter = require('async-limiter'); +const zlib = require('zlib'); + +const bufferUtil = require('./buffer-util'); +const constants = require('./constants'); + +const TRAILER = Buffer.from([0x00, 0x00, 0xff, 0xff]); +const EMPTY_BLOCK = Buffer.from([0x00]); + +const kPerMessageDeflate = Symbol('permessage-deflate'); +const kWriteInProgress = Symbol('write-in-progress'); +const kPendingClose = Symbol('pending-close'); +const kTotalLength = Symbol('total-length'); +const kCallback = Symbol('callback'); +const kBuffers = Symbol('buffers'); +const kError = Symbol('error'); + +// +// We limit zlib concurrency, which prevents severe memory fragmentation +// as documented in https://github.com/nodejs/node/issues/8871#issuecomment-250915913 +// and https://github.com/websockets/ws/issues/1202 +// +// Intentionally global; it's the global thread pool that's an issue. +// +let zlibLimiter; + +/** + * permessage-deflate implementation. + */ +class PerMessageDeflate { + /** + * Creates a PerMessageDeflate instance. + * + * @param {Object} options Configuration options + * @param {Boolean} options.serverNoContextTakeover Request/accept disabling + * of server context takeover + * @param {Boolean} options.clientNoContextTakeover Advertise/acknowledge + * disabling of client context takeover + * @param {(Boolean|Number)} options.serverMaxWindowBits Request/confirm the + * use of a custom server window size + * @param {(Boolean|Number)} options.clientMaxWindowBits Advertise support + * for, or request, a custom client window size + * @param {Object} options.zlibDeflateOptions Options to pass to zlib on deflate + * @param {Object} options.zlibInflateOptions Options to pass to zlib on inflate + * @param {Number} options.threshold Size (in bytes) below which messages + * should not be compressed + * @param {Number} options.concurrencyLimit The number of concurrent calls to + * zlib + * @param {Boolean} isServer Create the instance in either server or client + * mode + * @param {Number} maxPayload The maximum allowed message length + */ + constructor (options, isServer, maxPayload) { + this._maxPayload = maxPayload | 0; + this._options = options || {}; + this._threshold = this._options.threshold !== undefined + ? this._options.threshold + : 1024; + this._isServer = !!isServer; + this._deflate = null; + this._inflate = null; + + this.params = null; + + if (!zlibLimiter) { + const concurrency = this._options.concurrencyLimit !== undefined + ? this._options.concurrencyLimit + : 10; + zlibLimiter = new Limiter({ concurrency }); + } + } + + /** + * @type {String} + */ + static get extensionName () { + return 'permessage-deflate'; + } + + /** + * Create an extension negotiation offer. + * + * @return {Object} Extension parameters + * @public + */ + offer () { + const params = {}; + + if (this._options.serverNoContextTakeover) { + params.server_no_context_takeover = true; + } + if (this._options.clientNoContextTakeover) { + params.client_no_context_takeover = true; + } + if (this._options.serverMaxWindowBits) { + params.server_max_window_bits = this._options.serverMaxWindowBits; + } + if (this._options.clientMaxWindowBits) { + params.client_max_window_bits = this._options.clientMaxWindowBits; + } else if (this._options.clientMaxWindowBits == null) { + params.client_max_window_bits = true; + } + + return params; + } + + /** + * Accept an extension negotiation offer/response. + * + * @param {Array} configurations The extension negotiation offers/reponse + * @return {Object} Accepted configuration + * @public + */ + accept (configurations) { + configurations = this.normalizeParams(configurations); + + this.params = this._isServer + ? this.acceptAsServer(configurations) + : this.acceptAsClient(configurations); + + return this.params; + } + + /** + * Releases all resources used by the extension. + * + * @public + */ + cleanup () { + if (this._inflate) { + if (this._inflate[kWriteInProgress]) { + this._inflate[kPendingClose] = true; + } else { + this._inflate.close(); + this._inflate = null; + } + } + if (this._deflate) { + if (this._deflate[kWriteInProgress]) { + this._deflate[kPendingClose] = true; + } else { + this._deflate.close(); + this._deflate = null; + } + } + } + + /** + * Accept an extension negotiation offer. + * + * @param {Array} offers The extension negotiation offers + * @return {Object} Accepted configuration + * @private + */ + acceptAsServer (offers) { + const opts = this._options; + const accepted = offers.find((params) => { + if ( + (opts.serverNoContextTakeover === false && + params.server_no_context_takeover) || + (params.server_max_window_bits && + (opts.serverMaxWindowBits === false || + (typeof opts.serverMaxWindowBits === 'number' && + opts.serverMaxWindowBits > params.server_max_window_bits))) || + (typeof opts.clientMaxWindowBits === 'number' && + !params.client_max_window_bits) + ) { + return false; + } + + return true; + }); + + if (!accepted) { + throw new Error('None of the extension offers can be accepted'); + } + + if (opts.serverNoContextTakeover) { + accepted.server_no_context_takeover = true; + } + if (opts.clientNoContextTakeover) { + accepted.client_no_context_takeover = true; + } + if (typeof opts.serverMaxWindowBits === 'number') { + accepted.server_max_window_bits = opts.serverMaxWindowBits; + } + if (typeof opts.clientMaxWindowBits === 'number') { + accepted.client_max_window_bits = opts.clientMaxWindowBits; + } else if ( + accepted.client_max_window_bits === true || + opts.clientMaxWindowBits === false + ) { + delete accepted.client_max_window_bits; + } + + return accepted; + } + + /** + * Accept the extension negotiation response. + * + * @param {Array} response The extension negotiation response + * @return {Object} Accepted configuration + * @private + */ + acceptAsClient (response) { + const params = response[0]; + + if ( + this._options.clientNoContextTakeover === false && + params.client_no_context_takeover + ) { + throw new Error('Unexpected parameter "client_no_context_takeover"'); + } + + if (!params.client_max_window_bits) { + if (typeof this._options.clientMaxWindowBits === 'number') { + params.client_max_window_bits = this._options.clientMaxWindowBits; + } + } else if ( + this._options.clientMaxWindowBits === false || + (typeof this._options.clientMaxWindowBits === 'number' && + params.client_max_window_bits > this._options.clientMaxWindowBits) + ) { + throw new Error( + 'Unexpected or invalid parameter "client_max_window_bits"' + ); + } + + return params; + } + + /** + * Normalize parameters. + * + * @param {Array} configurations The extension negotiation offers/reponse + * @return {Array} The offers/response with normalized parameters + * @private + */ + normalizeParams (configurations) { + configurations.forEach((params) => { + Object.keys(params).forEach((key) => { + var value = params[key]; + + if (value.length > 1) { + throw new Error(`Parameter "${key}" must have only a single value`); + } + + value = value[0]; + + if (key === 'client_max_window_bits') { + if (value !== true) { + const num = +value; + if (!Number.isInteger(num) || num < 8 || num > 15) { + throw new TypeError( + `Invalid value for parameter "${key}": ${value}` + ); + } + value = num; + } else if (!this._isServer) { + throw new TypeError( + `Invalid value for parameter "${key}": ${value}` + ); + } + } else if (key === 'server_max_window_bits') { + const num = +value; + if (!Number.isInteger(num) || num < 8 || num > 15) { + throw new TypeError( + `Invalid value for parameter "${key}": ${value}` + ); + } + value = num; + } else if ( + key === 'client_no_context_takeover' || + key === 'server_no_context_takeover' + ) { + if (value !== true) { + throw new TypeError( + `Invalid value for parameter "${key}": ${value}` + ); + } + } else { + throw new Error(`Unknown parameter "${key}"`); + } + + params[key] = value; + }); + }); + + return configurations; + } + + /** + * Decompress data. Concurrency limited by async-limiter. + * + * @param {Buffer} data Compressed data + * @param {Boolean} fin Specifies whether or not this is the last fragment + * @param {Function} callback Callback + * @public + */ + decompress (data, fin, callback) { + zlibLimiter.push((done) => { + this._decompress(data, fin, (err, result) => { + done(); + callback(err, result); + }); + }); + } + + /** + * Compress data. Concurrency limited by async-limiter. + * + * @param {Buffer} data Data to compress + * @param {Boolean} fin Specifies whether or not this is the last fragment + * @param {Function} callback Callback + * @public + */ + compress (data, fin, callback) { + zlibLimiter.push((done) => { + this._compress(data, fin, (err, result) => { + done(); + callback(err, result); + }); + }); + } + + /** + * Decompress data. + * + * @param {Buffer} data Compressed data + * @param {Boolean} fin Specifies whether or not this is the last fragment + * @param {Function} callback Callback + * @private + */ + _decompress (data, fin, callback) { + const endpoint = this._isServer ? 'client' : 'server'; + + if (!this._inflate) { + const key = `${endpoint}_max_window_bits`; + const windowBits = typeof this.params[key] !== 'number' + ? zlib.Z_DEFAULT_WINDOWBITS + : this.params[key]; + + this._inflate = zlib.createInflateRaw( + Object.assign({}, this._options.zlibInflateOptions, { windowBits }) + ); + this._inflate[kPerMessageDeflate] = this; + this._inflate[kTotalLength] = 0; + this._inflate[kBuffers] = []; + this._inflate.on('error', inflateOnError); + this._inflate.on('data', inflateOnData); + } + + this._inflate[kCallback] = callback; + this._inflate[kWriteInProgress] = true; + + this._inflate.write(data); + if (fin) this._inflate.write(TRAILER); + + this._inflate.flush(() => { + const err = this._inflate[kError]; + + if (err) { + this._inflate.close(); + this._inflate = null; + callback(err); + return; + } + + const data = bufferUtil.concat( + this._inflate[kBuffers], + this._inflate[kTotalLength] + ); + + if ( + (fin && this.params[`${endpoint}_no_context_takeover`]) || + this._inflate[kPendingClose] + ) { + this._inflate.close(); + this._inflate = null; + } else { + this._inflate[kWriteInProgress] = false; + this._inflate[kTotalLength] = 0; + this._inflate[kBuffers] = []; + } + + callback(null, data); + }); + } + + /** + * Compress data. + * + * @param {Buffer} data Data to compress + * @param {Boolean} fin Specifies whether or not this is the last fragment + * @param {Function} callback Callback + * @private + */ + _compress (data, fin, callback) { + if (!data || data.length === 0) { + process.nextTick(callback, null, EMPTY_BLOCK); + return; + } + + const endpoint = this._isServer ? 'server' : 'client'; + + if (!this._deflate) { + const key = `${endpoint}_max_window_bits`; + const windowBits = typeof this.params[key] !== 'number' + ? zlib.Z_DEFAULT_WINDOWBITS + : this.params[key]; + + this._deflate = zlib.createDeflateRaw( + Object.assign( + // TODO deprecate memLevel/level and recommend zlibDeflateOptions instead + { + memLevel: this._options.memLevel, + level: this._options.level + }, + this._options.zlibDeflateOptions, + { windowBits } + ) + ); + + this._deflate[kTotalLength] = 0; + this._deflate[kBuffers] = []; + + // + // `zlib.DeflateRaw` emits an `'error'` event only when an attempt to use + // it is made after it has already been closed. This cannot happen here, + // so we only add a listener for the `'data'` event. + // + this._deflate.on('data', deflateOnData); + } + + this._deflate[kWriteInProgress] = true; + + this._deflate.write(data); + this._deflate.flush(zlib.Z_SYNC_FLUSH, () => { + var data = bufferUtil.concat( + this._deflate[kBuffers], + this._deflate[kTotalLength] + ); + + if (fin) data = data.slice(0, data.length - 4); + + if ( + (fin && this.params[`${endpoint}_no_context_takeover`]) || + this._deflate[kPendingClose] + ) { + this._deflate.close(); + this._deflate = null; + } else { + this._deflate[kWriteInProgress] = false; + this._deflate[kTotalLength] = 0; + this._deflate[kBuffers] = []; + } + + callback(null, data); + }); + } +} + +module.exports = PerMessageDeflate; + +/** + * The listener of the `zlib.DeflateRaw` stream `'data'` event. + * + * @param {Buffer} chunk A chunk of data + * @private + */ +function deflateOnData (chunk) { + this[kBuffers].push(chunk); + this[kTotalLength] += chunk.length; +} + +/** + * The listener of the `zlib.InflateRaw` stream `'data'` event. + * + * @param {Buffer} chunk A chunk of data + * @private + */ +function inflateOnData (chunk) { + this[kTotalLength] += chunk.length; + + if ( + this[kPerMessageDeflate]._maxPayload < 1 || + this[kTotalLength] <= this[kPerMessageDeflate]._maxPayload + ) { + this[kBuffers].push(chunk); + return; + } + + this[kError] = new RangeError('Max payload size exceeded'); + this[kError][constants.kStatusCode] = 1009; + this.removeListener('data', inflateOnData); + this.reset(); +} + +/** + * The listener of the `zlib.InflateRaw` stream `'error'` event. + * + * @param {Error} err The emitted error + * @private + */ +function inflateOnError (err) { + // + // There is no need to call `Zlib#close()` as the handle is automatically + // closed when an error is emitted. + // + this[kPerMessageDeflate]._inflate = null; + err[constants.kStatusCode] = 1007; + this[kCallback](err); +} diff --git a/lib/receiver.js b/lib/receiver.js new file mode 100644 index 000000000..81dc0bf8a --- /dev/null +++ b/lib/receiver.js @@ -0,0 +1,513 @@ +'use strict'; + +const stream = require('stream'); + +const PerMessageDeflate = require('./permessage-deflate'); +const bufferUtil = require('./buffer-util'); +const validation = require('./validation'); +const constants = require('./constants'); + +const GET_INFO = 0; +const GET_PAYLOAD_LENGTH_16 = 1; +const GET_PAYLOAD_LENGTH_64 = 2; +const GET_MASK = 3; +const GET_DATA = 4; +const INFLATING = 5; + +/** + * HyBi Receiver implementation. + * + * @extends stream.Writable + */ +class Receiver extends stream.Writable { + /** + * Creates a Receiver instance. + * + * @param {String} binaryType The type for binary data + * @param {Object} extensions An object containing the negotiated extensions + * @param {Number} maxPayload The maximum allowed message length + */ + constructor (binaryType, extensions, maxPayload) { + super(); + + this._binaryType = binaryType || constants.BINARY_TYPES[0]; + this[constants.kWebSocket] = undefined; + this._extensions = extensions || {}; + this._maxPayload = maxPayload | 0; + + this._bufferedBytes = 0; + this._buffers = []; + + this._compressed = false; + this._payloadLength = 0; + this._mask = undefined; + this._fragmented = 0; + this._masked = false; + this._fin = false; + this._opcode = 0; + + this._totalPayloadLength = 0; + this._messageLength = 0; + this._fragments = []; + + this._state = GET_INFO; + this._loop = false; + } + + /** + * Implements `Writable.prototype._write()`. + * + * @param {Buffer} chunk The chunk of data to write + * @param {String} encoding The character encoding of `chunk` + * @param {Function} cb Callback + */ + _write (chunk, encoding, cb) { + if (this._opcode === 0x08) return cb(); + + this._bufferedBytes += chunk.length; + this._buffers.push(chunk); + this.startLoop(cb); + } + + /** + * Consumes `n` bytes from the buffered data. + * + * @param {Number} n The number of bytes to consume + * @return {Buffer} The consumed bytes + * @private + */ + consume (n) { + this._bufferedBytes -= n; + + if (n === this._buffers[0].length) return this._buffers.shift(); + + if (n < this._buffers[0].length) { + const buf = this._buffers[0]; + this._buffers[0] = buf.slice(n); + return buf.slice(0, n); + } + + const dst = Buffer.allocUnsafe(n); + + do { + const buf = this._buffers[0]; + + if (n >= buf.length) { + this._buffers.shift().copy(dst, dst.length - n); + } else { + buf.copy(dst, dst.length - n, 0, n); + this._buffers[0] = buf.slice(n); + } + + n -= buf.length; + } while (n > 0); + + return dst; + } + + /** + * Starts the parsing loop. + * + * @param {Function} cb Callback + * @private + */ + startLoop (cb) { + var err; + this._loop = true; + + do { + switch (this._state) { + case GET_INFO: + err = this.getInfo(); + break; + case GET_PAYLOAD_LENGTH_16: + err = this.getPayloadLength16(); + break; + case GET_PAYLOAD_LENGTH_64: + err = this.getPayloadLength64(); + break; + case GET_MASK: + this.getMask(); + break; + case GET_DATA: + err = this.getData(cb); + break; + default: // `INFLATING` + this._loop = false; + return; + } + } while (this._loop); + + cb(err); + } + + /** + * Reads the first two bytes of a frame. + * + * @return {(RangeError|undefined)} A possible error + * @private + */ + getInfo () { + if (this._bufferedBytes < 2) { + this._loop = false; + return; + } + + const buf = this.consume(2); + + if ((buf[0] & 0x30) !== 0x00) { + this._loop = false; + return error(RangeError, 'RSV2 and RSV3 must be clear', true, 1002); + } + + const compressed = (buf[0] & 0x40) === 0x40; + + if (compressed && !this._extensions[PerMessageDeflate.extensionName]) { + this._loop = false; + return error(RangeError, 'RSV1 must be clear', true, 1002); + } + + this._fin = (buf[0] & 0x80) === 0x80; + this._opcode = buf[0] & 0x0f; + this._payloadLength = buf[1] & 0x7f; + + if (this._opcode === 0x00) { + if (compressed) { + this._loop = false; + return error(RangeError, 'RSV1 must be clear', true, 1002); + } + + if (!this._fragmented) { + this._loop = false; + return error(RangeError, 'invalid opcode 0', true, 1002); + } + + this._opcode = this._fragmented; + } else if (this._opcode === 0x01 || this._opcode === 0x02) { + if (this._fragmented) { + this._loop = false; + return error(RangeError, `invalid opcode ${this._opcode}`, true, 1002); + } + + this._compressed = compressed; + } else if (this._opcode > 0x07 && this._opcode < 0x0b) { + if (!this._fin) { + this._loop = false; + return error(RangeError, 'FIN must be set', true, 1002); + } + + if (compressed) { + this._loop = false; + return error(RangeError, 'RSV1 must be clear', true, 1002); + } + + if (this._payloadLength > 0x7d) { + this._loop = false; + return error( + RangeError, + `invalid payload length ${this._payloadLength}`, + true, + 1002 + ); + } + } else { + this._loop = false; + return error(RangeError, `invalid opcode ${this._opcode}`, true, 1002); + } + + if (!this._fin && !this._fragmented) this._fragmented = this._opcode; + this._masked = (buf[1] & 0x80) === 0x80; + + if (this._payloadLength === 126) this._state = GET_PAYLOAD_LENGTH_16; + else if (this._payloadLength === 127) this._state = GET_PAYLOAD_LENGTH_64; + else return this.haveLength(); + } + + /** + * Gets extended payload length (7+16). + * + * @return {(RangeError|undefined)} A possible error + * @private + */ + getPayloadLength16 () { + if (this._bufferedBytes < 2) { + this._loop = false; + return; + } + + this._payloadLength = this.consume(2).readUInt16BE(0); + return this.haveLength(); + } + + /** + * Gets extended payload length (7+64). + * + * @return {(RangeError|undefined)} A possible error + * @private + */ + getPayloadLength64 () { + if (this._bufferedBytes < 8) { + this._loop = false; + return; + } + + const buf = this.consume(8); + const num = buf.readUInt32BE(0); + + // + // The maximum safe integer in JavaScript is 2^53 - 1. An error is returned + // if payload length is greater than this number. + // + if (num > Math.pow(2, 53 - 32) - 1) { + this._loop = false; + return error( + RangeError, + 'Unsupported WebSocket frame: payload length > 2^53 - 1', + false, + 1009 + ); + } + + this._payloadLength = num * Math.pow(2, 32) + buf.readUInt32BE(4); + return this.haveLength(); + } + + /** + * Payload length has been read. + * + * @return {(RangeError|undefined)} A possible error + * @private + */ + haveLength () { + if (this._payloadLength && this._opcode < 0x08) { + this._totalPayloadLength += this._payloadLength; + if (this._totalPayloadLength > this._maxPayload && this._maxPayload > 0) { + this._loop = false; + return error(RangeError, 'Max payload size exceeded', false, 1009); + } + } + + if (this._masked) this._state = GET_MASK; + else this._state = GET_DATA; + } + + /** + * Reads mask bytes. + * + * @private + */ + getMask () { + if (this._bufferedBytes < 4) { + this._loop = false; + return; + } + + this._mask = this.consume(4); + this._state = GET_DATA; + } + + /** + * Reads data bytes. + * + * @param {Function} cb Callback + * @return {(Error|RangeError|undefined)} A possible error + * @private + */ + getData (cb) { + var data = constants.EMPTY_BUFFER; + + if (this._payloadLength) { + if (this._bufferedBytes < this._payloadLength) { + this._loop = false; + return; + } + + data = this.consume(this._payloadLength); + if (this._masked) bufferUtil.unmask(data, this._mask); + } + + if (this._opcode > 0x07) return this.controlMessage(data); + + if (this._compressed) { + this._state = INFLATING; + this.decompress(data, cb); + return; + } + + if (data.length) { + // + // This message is not compressed so its lenght is the sum of the payload + // length of all fragments. + // + this._messageLength = this._totalPayloadLength; + this._fragments.push(data); + } + + return this.dataMessage(); + } + + /** + * Decompresses data. + * + * @param {Buffer} data Compressed data + * @param {Function} cb Callback + * @private + */ + decompress (data, cb) { + const perMessageDeflate = this._extensions[PerMessageDeflate.extensionName]; + + perMessageDeflate.decompress(data, this._fin, (err, buf) => { + if (err) return cb(err); + + if (buf.length) { + this._messageLength += buf.length; + if (this._messageLength > this._maxPayload && this._maxPayload > 0) { + return cb(error(RangeError, 'Max payload size exceeded', false, 1009)); + } + + this._fragments.push(buf); + } + + const er = this.dataMessage(); + if (er) return cb(er); + + this.startLoop(cb); + }); + } + + /** + * Handles a data message. + * + * @return {(Error|undefined)} A possible error + * @private + */ + dataMessage () { + if (this._fin) { + const messageLength = this._messageLength; + const fragments = this._fragments; + + this._totalPayloadLength = 0; + this._messageLength = 0; + this._fragmented = 0; + this._fragments = []; + + if (this._opcode === 2) { + var data; + + if (this._binaryType === 'nodebuffer') { + data = toBuffer(fragments, messageLength); + } else if (this._binaryType === 'arraybuffer') { + data = toArrayBuffer(toBuffer(fragments, messageLength)); + } else { + data = fragments; + } + + this.emit('message', data); + } else { + const buf = toBuffer(fragments, messageLength); + + if (!validation.isValidUTF8(buf)) { + this._loop = false; + return error(Error, 'invalid UTF-8 sequence', true, 1007); + } + + this.emit('message', buf.toString()); + } + } + + this._state = GET_INFO; + } + + /** + * Handles a control message. + * + * @param {Buffer} data Data to handle + * @return {(Error|RangeError|undefined)} A possible error + * @private + */ + controlMessage (data) { + if (this._opcode === 0x08) { + this._loop = false; + + if (data.length === 0) { + this.emit('conclude', 1005, ''); + this.end(); + } else if (data.length === 1) { + return error(RangeError, 'invalid payload length 1', true, 1002); + } else { + const code = data.readUInt16BE(0); + + if (!validation.isValidStatusCode(code)) { + return error(RangeError, `invalid status code ${code}`, true, 1002); + } + + const buf = data.slice(2); + + if (!validation.isValidUTF8(buf)) { + return error(Error, 'invalid UTF-8 sequence', true, 1007); + } + + this.emit('conclude', code, buf.toString()); + this.end(); + } + + return; + } + + if (this._opcode === 0x09) this.emit('ping', data); + else this.emit('pong', data); + + this._state = GET_INFO; + } +} + +module.exports = Receiver; + +/** + * Builds an error object. + * + * @param {(Error|RangeError)} ErrorCtor The error constructor + * @param {String} message The error message + * @param {Boolean} prefix Specifies whether or not to add a default prefix to + * `message` + * @param {Number} statusCode The status code + * @return {(Error|RangeError)} The error + * @private + */ +function error (ErrorCtor, message, prefix, statusCode) { + const err = new ErrorCtor( + prefix ? `Invalid WebSocket frame: ${message}` : message + ); + + Error.captureStackTrace(err, error); + err[constants.kStatusCode] = statusCode; + return err; +} + +/** + * Makes a buffer from a list of fragments. + * + * @param {Buffer[]} fragments The list of fragments composing the message + * @param {Number} messageLength The length of the message + * @return {Buffer} + * @private + */ +function toBuffer (fragments, messageLength) { + if (fragments.length === 1) return fragments[0]; + if (fragments.length > 1) return bufferUtil.concat(fragments, messageLength); + return constants.EMPTY_BUFFER; +} + +/** + * Converts a buffer to an `ArrayBuffer`. + * + * @param {Buffer} The buffer to convert + * @return {ArrayBuffer} Converted buffer + */ +function toArrayBuffer (buf) { + if (buf.byteOffset === 0 && buf.byteLength === buf.buffer.byteLength) { + return buf.buffer; + } + + return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength); +} diff --git a/lib/sender.js b/lib/sender.js new file mode 100644 index 000000000..060e55392 --- /dev/null +++ b/lib/sender.js @@ -0,0 +1,401 @@ +'use strict'; + +const crypto = require('crypto'); + +const PerMessageDeflate = require('./permessage-deflate'); +const bufferUtil = require('./buffer-util'); +const validation = require('./validation'); +const constants = require('./constants'); + +/** + * HyBi Sender implementation. + */ +class Sender { + /** + * Creates a Sender instance. + * + * @param {net.Socket} socket The connection socket + * @param {Object} extensions An object containing the negotiated extensions + */ + constructor (socket, extensions) { + this._extensions = extensions || {}; + this._socket = socket; + + this._firstFragment = true; + this._compress = false; + + this._bufferedBytes = 0; + this._deflating = false; + this._queue = []; + } + + /** + * Frames a piece of data according to the HyBi WebSocket protocol. + * + * @param {Buffer} data The data to frame + * @param {Object} options Options object + * @param {Number} options.opcode The opcode + * @param {Boolean} options.readOnly Specifies whether `data` can be modified + * @param {Boolean} options.fin Specifies whether or not to set the FIN bit + * @param {Boolean} options.mask Specifies whether or not to mask `data` + * @param {Boolean} options.rsv1 Specifies whether or not to set the RSV1 bit + * @return {Buffer[]} The framed data as a list of `Buffer` instances + * @public + */ + static frame (data, options) { + const merge = data.length < 1024 || (options.mask && options.readOnly); + var offset = options.mask ? 6 : 2; + var payloadLength = data.length; + + if (data.length >= 65536) { + offset += 8; + payloadLength = 127; + } else if (data.length > 125) { + offset += 2; + payloadLength = 126; + } + + const target = Buffer.allocUnsafe(merge ? data.length + offset : offset); + + target[0] = options.fin ? options.opcode | 0x80 : options.opcode; + if (options.rsv1) target[0] |= 0x40; + + if (payloadLength === 126) { + target.writeUInt16BE(data.length, 2); + } else if (payloadLength === 127) { + target.writeUInt32BE(0, 2); + target.writeUInt32BE(data.length, 6); + } + + if (!options.mask) { + target[1] = payloadLength; + if (merge) { + data.copy(target, offset); + return [target]; + } + + return [target, data]; + } + + const mask = crypto.randomBytes(4); + + target[1] = payloadLength | 0x80; + target[offset - 4] = mask[0]; + target[offset - 3] = mask[1]; + target[offset - 2] = mask[2]; + target[offset - 1] = mask[3]; + + if (merge) { + bufferUtil.mask(data, mask, target, offset, data.length); + return [target]; + } + + bufferUtil.mask(data, mask, data, 0, data.length); + return [target, data]; + } + + /** + * Sends a close message to the other peer. + * + * @param {(Number|undefined)} code The status code component of the body + * @param {String} data The message component of the body + * @param {Boolean} mask Specifies whether or not to mask the message + * @param {Function} cb Callback + * @public + */ + close (code, data, mask, cb) { + var buf; + + if (code === undefined) { + buf = constants.EMPTY_BUFFER; + } else if (typeof code !== 'number' || !validation.isValidStatusCode(code)) { + throw new TypeError('First argument must be a valid error code number'); + } else if (data === undefined || data === '') { + buf = Buffer.allocUnsafe(2); + buf.writeUInt16BE(code, 0); + } else { + buf = Buffer.allocUnsafe(2 + Buffer.byteLength(data)); + buf.writeUInt16BE(code, 0); + buf.write(data, 2); + } + + if (this._deflating) { + this.enqueue([this.doClose, buf, mask, cb]); + } else { + this.doClose(buf, mask, cb); + } + } + + /** + * Frames and sends a close message. + * + * @param {Buffer} data The message to send + * @param {Boolean} mask Specifies whether or not to mask `data` + * @param {Function} cb Callback + * @private + */ + doClose (data, mask, cb) { + this.sendFrame(Sender.frame(data, { + fin: true, + rsv1: false, + opcode: 0x08, + mask, + readOnly: false + }), cb); + } + + /** + * Sends a ping message to the other peer. + * + * @param {*} data The message to send + * @param {Boolean} mask Specifies whether or not to mask `data` + * @param {Function} cb Callback + * @public + */ + ping (data, mask, cb) { + var readOnly = true; + + if (!Buffer.isBuffer(data)) { + if (data instanceof ArrayBuffer) { + data = Buffer.from(data); + } else if (ArrayBuffer.isView(data)) { + data = viewToBuffer(data); + } else { + data = Buffer.from(data); + readOnly = false; + } + } + + if (this._deflating) { + this.enqueue([this.doPing, data, mask, readOnly, cb]); + } else { + this.doPing(data, mask, readOnly, cb); + } + } + + /** + * Frames and sends a ping message. + * + * @param {*} data The message to send + * @param {Boolean} mask Specifies whether or not to mask `data` + * @param {Boolean} readOnly Specifies whether `data` can be modified + * @param {Function} cb Callback + * @private + */ + doPing (data, mask, readOnly, cb) { + this.sendFrame(Sender.frame(data, { + fin: true, + rsv1: false, + opcode: 0x09, + mask, + readOnly + }), cb); + } + + /** + * Sends a pong message to the other peer. + * + * @param {*} data The message to send + * @param {Boolean} mask Specifies whether or not to mask `data` + * @param {Function} cb Callback + * @public + */ + pong (data, mask, cb) { + var readOnly = true; + + if (!Buffer.isBuffer(data)) { + if (data instanceof ArrayBuffer) { + data = Buffer.from(data); + } else if (ArrayBuffer.isView(data)) { + data = viewToBuffer(data); + } else { + data = Buffer.from(data); + readOnly = false; + } + } + + if (this._deflating) { + this.enqueue([this.doPong, data, mask, readOnly, cb]); + } else { + this.doPong(data, mask, readOnly, cb); + } + } + + /** + * Frames and sends a pong message. + * + * @param {*} data The message to send + * @param {Boolean} mask Specifies whether or not to mask `data` + * @param {Boolean} readOnly Specifies whether `data` can be modified + * @param {Function} cb Callback + * @private + */ + doPong (data, mask, readOnly, cb) { + this.sendFrame(Sender.frame(data, { + fin: true, + rsv1: false, + opcode: 0x0a, + mask, + readOnly + }), cb); + } + + /** + * Sends a data message to the other peer. + * + * @param {*} data The message to send + * @param {Object} options Options object + * @param {Boolean} options.compress Specifies whether or not to compress `data` + * @param {Boolean} options.binary Specifies whether `data` is binary or text + * @param {Boolean} options.fin Specifies whether the fragment is the last one + * @param {Boolean} options.mask Specifies whether or not to mask `data` + * @param {Function} cb Callback + * @public + */ + send (data, options, cb) { + var opcode = options.binary ? 2 : 1; + var rsv1 = options.compress; + var readOnly = true; + + if (!Buffer.isBuffer(data)) { + if (data instanceof ArrayBuffer) { + data = Buffer.from(data); + } else if (ArrayBuffer.isView(data)) { + data = viewToBuffer(data); + } else { + data = Buffer.from(data); + readOnly = false; + } + } + + const perMessageDeflate = this._extensions[PerMessageDeflate.extensionName]; + + if (this._firstFragment) { + this._firstFragment = false; + if (rsv1 && perMessageDeflate) { + rsv1 = data.length >= perMessageDeflate._threshold; + } + this._compress = rsv1; + } else { + rsv1 = false; + opcode = 0; + } + + if (options.fin) this._firstFragment = true; + + if (perMessageDeflate) { + const opts = { + fin: options.fin, + rsv1, + opcode, + mask: options.mask, + readOnly + }; + + if (this._deflating) { + this.enqueue([this.dispatch, data, this._compress, opts, cb]); + } else { + this.dispatch(data, this._compress, opts, cb); + } + } else { + this.sendFrame(Sender.frame(data, { + fin: options.fin, + rsv1: false, + opcode, + mask: options.mask, + readOnly + }), cb); + } + } + + /** + * Dispatches a data message. + * + * @param {Buffer} data The message to send + * @param {Boolean} compress Specifies whether or not to compress `data` + * @param {Object} options Options object + * @param {Number} options.opcode The opcode + * @param {Boolean} options.readOnly Specifies whether `data` can be modified + * @param {Boolean} options.fin Specifies whether or not to set the FIN bit + * @param {Boolean} options.mask Specifies whether or not to mask `data` + * @param {Boolean} options.rsv1 Specifies whether or not to set the RSV1 bit + * @param {Function} cb Callback + * @private + */ + dispatch (data, compress, options, cb) { + if (!compress) { + this.sendFrame(Sender.frame(data, options), cb); + return; + } + + const perMessageDeflate = this._extensions[PerMessageDeflate.extensionName]; + + this._deflating = true; + perMessageDeflate.compress(data, options.fin, (_, buf) => { + options.readOnly = false; + this.sendFrame(Sender.frame(buf, options), cb); + this._deflating = false; + this.dequeue(); + }); + } + + /** + * Executes queued send operations. + * + * @private + */ + dequeue () { + while (!this._deflating && this._queue.length) { + const params = this._queue.shift(); + + this._bufferedBytes -= params[1].length; + params[0].apply(this, params.slice(1)); + } + } + + /** + * Enqueues a send operation. + * + * @param {Array} params Send operation parameters. + * @private + */ + enqueue (params) { + this._bufferedBytes += params[1].length; + this._queue.push(params); + } + + /** + * Sends a frame. + * + * @param {Buffer[]} list The frame to send + * @param {Function} cb Callback + * @private + */ + sendFrame (list, cb) { + if (list.length === 2) { + this._socket.write(list[0]); + this._socket.write(list[1], cb); + } else { + this._socket.write(list[0], cb); + } + } +} + +module.exports = Sender; + +/** + * Converts an `ArrayBuffer` view into a buffer. + * + * @param {(DataView|TypedArray)} view The view to convert + * @return {Buffer} Converted view + * @private + */ +function viewToBuffer (view) { + const buf = Buffer.from(view.buffer); + + if (view.byteLength !== view.buffer.byteLength) { + return buf.slice(view.byteOffset, view.byteOffset + view.byteLength); + } + + return buf; +} diff --git a/lib/validation.js b/lib/validation.js new file mode 100644 index 000000000..06269fcf1 --- /dev/null +++ b/lib/validation.js @@ -0,0 +1,29 @@ +'use strict'; + +try { + const isValidUTF8 = require('utf-8-validate'); + + exports.isValidUTF8 = typeof isValidUTF8 === 'object' + ? isValidUTF8.Validation.isValidUTF8 // utf-8-validate@<3.0.0 + : isValidUTF8; +} catch (e) /* istanbul ignore next */ { + exports.isValidUTF8 = () => true; +} + +/** + * Checks if a status code is allowed in a close frame. + * + * @param {Number} code The status code + * @return {Boolean} `true` if the status code is valid, else `false` + * @public + */ +exports.isValidStatusCode = (code) => { + return ( + (code >= 1000 && + code <= 1013 && + code !== 1004 && + code !== 1005 && + code !== 1006) || + (code >= 3000 && code <= 4999) + ); +}; diff --git a/lib/websocket-server.js b/lib/websocket-server.js new file mode 100644 index 000000000..70513edf2 --- /dev/null +++ b/lib/websocket-server.js @@ -0,0 +1,357 @@ +'use strict'; + +const EventEmitter = require('events'); +const crypto = require('crypto'); +const http = require('http'); +const url = require('url'); + +const PerMessageDeflate = require('./permessage-deflate'); +const extension = require('./extension'); +const constants = require('./constants'); +const WebSocket = require('./websocket'); + +/** + * Class representing a WebSocket server. + * + * @extends EventEmitter + */ +class WebSocketServer extends EventEmitter { + /** + * Create a `WebSocketServer` instance. + * + * @param {Object} options Configuration options + * @param {String} options.host The hostname where to bind the server + * @param {Number} options.port The port where to bind the server + * @param {http.Server} options.server A pre-created HTTP/S server to use + * @param {Function} options.verifyClient An hook to reject connections + * @param {Function} options.handleProtocols An hook to handle protocols + * @param {String} options.path Accept only connections matching this path + * @param {Boolean} options.noServer Enable no server mode + * @param {Boolean} options.clientTracking Specifies whether or not to track clients + * @param {(Boolean|Object)} options.perMessageDeflate Enable/disable permessage-deflate + * @param {Number} options.maxPayload The maximum allowed message size + * @param {Function} callback A listener for the `listening` event + */ + constructor (options, callback) { + super(); + + options = Object.assign({ + maxPayload: 100 * 1024 * 1024, + perMessageDeflate: false, + handleProtocols: null, + clientTracking: true, + verifyClient: null, + noServer: false, + backlog: null, // use default (511 as implemented in net.js) + server: null, + host: null, + path: null, + port: null + }, options); + + if (options.port == null && !options.server && !options.noServer) { + throw new TypeError( + 'One of the "port", "server", or "noServer" options must be specified' + ); + } + + if (options.port != null) { + this._server = http.createServer((req, res) => { + const body = http.STATUS_CODES[426]; + + res.writeHead(426, { + 'Content-Length': body.length, + 'Content-Type': 'text/plain' + }); + res.end(body); + }); + this._server.listen(options.port, options.host, options.backlog, callback); + } else if (options.server) { + this._server = options.server; + } + + if (this._server) { + this._removeListeners = addListeners(this._server, { + listening: this.emit.bind(this, 'listening'), + error: this.emit.bind(this, 'error'), + upgrade: (req, socket, head) => { + this.handleUpgrade(req, socket, head, (ws) => { + this.emit('connection', ws, req); + }); + } + }); + } + + if (options.perMessageDeflate === true) options.perMessageDeflate = {}; + if (options.clientTracking) this.clients = new Set(); + this.options = options; + } + + /** + * Returns the bound address, the address family name, and port of the server + * as reported by the operating system if listening on an IP socket. + * If the server is listening on a pipe or UNIX domain socket, the name is + * returned as a string. + * + * @return {(Object|String|null)} The address of the server + * @public + */ + address () { + if (this.options.noServer) { + throw new Error('The server is operating in "noServer" mode'); + } + + if (!this._server) return null; + return this._server.address(); + } + + /** + * Close the server. + * + * @param {Function} cb Callback + * @public + */ + close (cb) { + // + // Terminate all associated clients. + // + if (this.clients) { + for (const client of this.clients) client.terminate(); + } + + const server = this._server; + + if (server) { + this._removeListeners(); + this._removeListeners = this._server = null; + + // + // Close the http server if it was internally created. + // + if (this.options.port != null) return server.close(cb); + } + + if (cb) cb(); + } + + /** + * See if a given request should be handled by this server instance. + * + * @param {http.IncomingMessage} req Request object to inspect + * @return {Boolean} `true` if the request is valid, else `false` + * @public + */ + shouldHandle (req) { + if (this.options.path && url.parse(req.url).pathname !== this.options.path) { + return false; + } + + return true; + } + + /** + * Handle a HTTP Upgrade request. + * + * @param {http.IncomingMessage} req The request object + * @param {net.Socket} socket The network socket between the server and client + * @param {Buffer} head The first packet of the upgraded stream + * @param {Function} cb Callback + * @public + */ + handleUpgrade (req, socket, head, cb) { + socket.on('error', socketOnError); + + const version = +req.headers['sec-websocket-version']; + const extensions = {}; + + if ( + req.method !== 'GET' || req.headers.upgrade.toLowerCase() !== 'websocket' || + !req.headers['sec-websocket-key'] || (version !== 8 && version !== 13) || + !this.shouldHandle(req) + ) { + return abortHandshake(socket, 400); + } + + if (this.options.perMessageDeflate) { + const perMessageDeflate = new PerMessageDeflate( + this.options.perMessageDeflate, + true, + this.options.maxPayload + ); + + try { + const offers = extension.parse( + req.headers['sec-websocket-extensions'] + ); + + if (offers[PerMessageDeflate.extensionName]) { + perMessageDeflate.accept(offers[PerMessageDeflate.extensionName]); + extensions[PerMessageDeflate.extensionName] = perMessageDeflate; + } + } catch (err) { + return abortHandshake(socket, 400); + } + } + + // + // Optionally call external client verification handler. + // + if (this.options.verifyClient) { + const info = { + origin: req.headers[`${version === 8 ? 'sec-websocket-origin' : 'origin'}`], + secure: !!(req.connection.authorized || req.connection.encrypted), + req + }; + + if (this.options.verifyClient.length === 2) { + this.options.verifyClient(info, (verified, code, message, headers) => { + if (!verified) { + return abortHandshake(socket, code || 401, message, headers); + } + + this.completeUpgrade(extensions, req, socket, head, cb); + }); + return; + } + + if (!this.options.verifyClient(info)) return abortHandshake(socket, 401); + } + + this.completeUpgrade(extensions, req, socket, head, cb); + } + + /** + * Upgrade the connection to WebSocket. + * + * @param {Object} extensions The accepted extensions + * @param {http.IncomingMessage} req The request object + * @param {net.Socket} socket The network socket between the server and client + * @param {Buffer} head The first packet of the upgraded stream + * @param {Function} cb Callback + * @private + */ + completeUpgrade (extensions, req, socket, head, cb) { + // + // Destroy the socket if the client has already sent a FIN packet. + // + if (!socket.readable || !socket.writable) return socket.destroy(); + + const key = crypto.createHash('sha1') + .update(req.headers['sec-websocket-key'] + constants.GUID, 'binary') + .digest('base64'); + + const headers = [ + 'HTTP/1.1 101 Switching Protocols', + 'Upgrade: websocket', + 'Connection: Upgrade', + `Sec-WebSocket-Accept: ${key}` + ]; + + const ws = new WebSocket(null); + var protocol = req.headers['sec-websocket-protocol']; + + if (protocol) { + protocol = protocol.trim().split(/ *, */); + + // + // Optionally call external protocol selection handler. + // + if (this.options.handleProtocols) { + protocol = this.options.handleProtocols(protocol, req); + } else { + protocol = protocol[0]; + } + + if (protocol) { + headers.push(`Sec-WebSocket-Protocol: ${protocol}`); + ws.protocol = protocol; + } + } + + if (extensions[PerMessageDeflate.extensionName]) { + const params = extensions[PerMessageDeflate.extensionName].params; + const value = extension.format({ + [PerMessageDeflate.extensionName]: [params] + }); + headers.push(`Sec-WebSocket-Extensions: ${value}`); + ws._extensions = extensions; + } + + // + // Allow external modification/inspection of handshake headers. + // + this.emit('headers', headers, req); + + socket.write(headers.concat('\r\n').join('\r\n')); + socket.removeListener('error', socketOnError); + + ws.setSocket(socket, head, this.options.maxPayload); + + if (this.clients) { + this.clients.add(ws); + ws.on('close', () => this.clients.delete(ws)); + } + + cb(ws); + } +} + +module.exports = WebSocketServer; + +/** + * Add event listeners on an `EventEmitter` using a map of + * pairs. + * + * @param {EventEmitter} server The event emitter + * @param {Object.} map The listeners to add + * @return {Function} A function that will remove the added listeners when called + * @private + */ +function addListeners (server, map) { + for (const event of Object.keys(map)) server.on(event, map[event]); + + return function removeListeners () { + for (const event of Object.keys(map)) { + server.removeListener(event, map[event]); + } + }; +} + +/** + * Handle premature socket errors. + * + * @private + */ +function socketOnError () { + this.destroy(); +} + +/** + * Close the connection when preconditions are not fulfilled. + * + * @param {net.Socket} socket The socket of the upgrade request + * @param {Number} code The HTTP response status code + * @param {String} [message] The HTTP response body + * @param {Object} [headers] Additional HTTP response headers + * @private + */ +function abortHandshake (socket, code, message, headers) { + if (socket.writable) { + message = message || http.STATUS_CODES[code]; + headers = Object.assign({ + 'Connection': 'close', + 'Content-type': 'text/html', + 'Content-Length': Buffer.byteLength(message) + }, headers); + + socket.write( + `HTTP/1.1 ${code} ${http.STATUS_CODES[code]}\r\n` + + Object.keys(headers).map(h => `${h}: ${headers[h]}`).join('\r\n') + + '\r\n\r\n' + + message + ); + } + + socket.removeListener('error', socketOnError); + socket.destroy(); +} diff --git a/lib/websocket.js b/lib/websocket.js new file mode 100644 index 000000000..f2bdf0d74 --- /dev/null +++ b/lib/websocket.js @@ -0,0 +1,828 @@ +'use strict'; + +const EventEmitter = require('events'); +const crypto = require('crypto'); +const https = require('https'); +const http = require('http'); +const net = require('net'); +const tls = require('tls'); +const url = require('url'); + +const PerMessageDeflate = require('./permessage-deflate'); +const EventTarget = require('./event-target'); +const extension = require('./extension'); +const constants = require('./constants'); +const Receiver = require('./receiver'); +const Sender = require('./sender'); + +const readyStates = ['CONNECTING', 'OPEN', 'CLOSING', 'CLOSED']; +const kWebSocket = constants.kWebSocket; +const protocolVersions = [8, 13]; +const closeTimeout = 30 * 1000; // Allow 30 seconds to terminate the connection cleanly. + +/** + * Class representing a WebSocket. + * + * @extends EventEmitter + */ +class WebSocket extends EventEmitter { + /** + * Create a new `WebSocket`. + * + * @param {(String|url.Url|url.URL)} address The URL to which to connect + * @param {(String|String[])} protocols The subprotocols + * @param {Object} options Connection options + */ + constructor (address, protocols, options) { + super(); + + this.readyState = WebSocket.CONNECTING; + this.protocol = ''; + + this._binaryType = constants.BINARY_TYPES[0]; + this._closeFrameReceived = false; + this._closeFrameSent = false; + this._closeMessage = ''; + this._closeTimer = null; + this._closeCode = 1006; + this._extensions = {}; + this._isServer = true; + this._receiver = null; + this._sender = null; + this._socket = null; + + if (address !== null) { + if (Array.isArray(protocols)) { + protocols = protocols.join(', '); + } else if (typeof protocols === 'object' && protocols !== null) { + options = protocols; + protocols = undefined; + } + + initAsClient.call(this, address, protocols, options); + } + } + + get CONNECTING () { return WebSocket.CONNECTING; } + get CLOSING () { return WebSocket.CLOSING; } + get CLOSED () { return WebSocket.CLOSED; } + get OPEN () { return WebSocket.OPEN; } + + /** + * This deviates from the WHATWG interface since ws doesn't support the required + * default "blob" type (instead we define a custom "nodebuffer" type). + * + * @type {String} + */ + get binaryType () { + return this._binaryType; + } + + set binaryType (type) { + if (constants.BINARY_TYPES.indexOf(type) < 0) return; + + this._binaryType = type; + + // + // Allow to change `binaryType` on the fly. + // + if (this._receiver) this._receiver._binaryType = type; + } + + /** + * @type {Number} + */ + get bufferedAmount () { + if (!this._socket) return 0; + + // + // `socket.bufferSize` is `undefined` if the socket is closed. + // + return (this._socket.bufferSize || 0) + this._sender._bufferedBytes; + } + + /** + * @type {String} + */ + get extensions () { + return Object.keys(this._extensions).join(); + } + + /** + * Set up the socket and the internal resources. + * + * @param {net.Socket} socket The network socket between the server and client + * @param {Buffer} head The first packet of the upgraded stream + * @param {Number} maxPayload The maximum allowed message size + * @private + */ + setSocket (socket, head, maxPayload) { + const receiver = new Receiver( + this._binaryType, + this._extensions, + maxPayload + ); + + this._sender = new Sender(socket, this._extensions); + this._receiver = receiver; + this._socket = socket; + + receiver[kWebSocket] = this; + socket[kWebSocket] = this; + + receiver.on('conclude', receiverOnConclude); + receiver.on('drain', receiverOnDrain); + receiver.on('error', receiverOnError); + receiver.on('message', receiverOnMessage); + receiver.on('ping', receiverOnPing); + receiver.on('pong', receiverOnPong); + + socket.setTimeout(0); + socket.setNoDelay(); + + if (head.length > 0) socket.unshift(head); + + socket.on('close', socketOnClose); + socket.on('data', socketOnData); + socket.on('end', socketOnEnd); + socket.on('error', socketOnError); + + this.readyState = WebSocket.OPEN; + this.emit('open'); + } + + /** + * Emit the `'close'` event. + * + * @private + */ + emitClose () { + this.readyState = WebSocket.CLOSED; + + if (!this._socket) { + this.emit('close', this._closeCode, this._closeMessage); + return; + } + + if (this._extensions[PerMessageDeflate.extensionName]) { + this._extensions[PerMessageDeflate.extensionName].cleanup(); + } + + this._receiver.removeAllListeners(); + this.emit('close', this._closeCode, this._closeMessage); + } + + /** + * Start a closing handshake. + * + * +----------+ +-----------+ +----------+ + * - - -|ws.close()|-->|close frame|-->|ws.close()|- - - + * | +----------+ +-----------+ +----------+ | + * +----------+ +-----------+ | + * CLOSING |ws.close()|<--|close frame|<--+-----+ CLOSING + * +----------+ +-----------+ | + * | | | +---+ | + * +------------------------+-->|fin| - - - - + * | +---+ | +---+ + * - - - - -|fin|<---------------------+ + * +---+ + * + * @param {Number} code Status code explaining why the connection is closing + * @param {String} data A string explaining why the connection is closing + * @public + */ + close (code, data) { + if (this.readyState === WebSocket.CLOSED) return; + if (this.readyState === WebSocket.CONNECTING) { + const msg = 'WebSocket was closed before the connection was established'; + return abortHandshake(this, this._req, msg); + } + + if (this.readyState === WebSocket.CLOSING) { + if (this._closeFrameSent && this._closeFrameReceived) this._socket.end(); + return; + } + + this.readyState = WebSocket.CLOSING; + this._sender.close(code, data, !this._isServer, (err) => { + // + // This error is handled by the `'error'` listener on the socket. We only + // want to know if the close frame has been sent here. + // + if (err) return; + + this._closeFrameSent = true; + + if (this._socket.writable) { + if (this._closeFrameReceived) this._socket.end(); + + // + // Ensure that the connection is closed even if the closing handshake + // fails. + // + this._closeTimer = setTimeout( + this._socket.destroy.bind(this._socket), + closeTimeout + ); + } + }); + } + + /** + * Send a ping. + * + * @param {*} data The data to send + * @param {Boolean} mask Indicates whether or not to mask `data` + * @param {Function} cb Callback which is executed when the ping is sent + * @public + */ + ping (data, mask, cb) { + if (typeof data === 'function') { + cb = data; + data = mask = undefined; + } else if (typeof mask === 'function') { + cb = mask; + mask = undefined; + } + + if (this.readyState !== WebSocket.OPEN) { + const err = new Error( + `WebSocket is not open: readyState ${this.readyState} ` + + `(${readyStates[this.readyState]})` + ); + + if (cb) return cb(err); + throw err; + } + + if (typeof data === 'number') data = data.toString(); + if (mask === undefined) mask = !this._isServer; + this._sender.ping(data || constants.EMPTY_BUFFER, mask, cb); + } + + /** + * Send a pong. + * + * @param {*} data The data to send + * @param {Boolean} mask Indicates whether or not to mask `data` + * @param {Function} cb Callback which is executed when the pong is sent + * @public + */ + pong (data, mask, cb) { + if (typeof data === 'function') { + cb = data; + data = mask = undefined; + } else if (typeof mask === 'function') { + cb = mask; + mask = undefined; + } + + if (this.readyState !== WebSocket.OPEN) { + const err = new Error( + `WebSocket is not open: readyState ${this.readyState} ` + + `(${readyStates[this.readyState]})` + ); + + if (cb) return cb(err); + throw err; + } + + if (typeof data === 'number') data = data.toString(); + if (mask === undefined) mask = !this._isServer; + this._sender.pong(data || constants.EMPTY_BUFFER, mask, cb); + } + + /** + * Send a data message. + * + * @param {*} data The message to send + * @param {Object} options Options object + * @param {Boolean} options.compress Specifies whether or not to compress `data` + * @param {Boolean} options.binary Specifies whether `data` is binary or text + * @param {Boolean} options.fin Specifies whether the fragment is the last one + * @param {Boolean} options.mask Specifies whether or not to mask `data` + * @param {Function} cb Callback which is executed when data is written out + * @public + */ + send (data, options, cb) { + if (typeof options === 'function') { + cb = options; + options = {}; + } + + if (this.readyState !== WebSocket.OPEN) { + const err = new Error( + `WebSocket is not open: readyState ${this.readyState} ` + + `(${readyStates[this.readyState]})` + ); + + if (cb) return cb(err); + throw err; + } + + if (typeof data === 'number') data = data.toString(); + + const opts = Object.assign({ + binary: typeof data !== 'string', + mask: !this._isServer, + compress: true, + fin: true + }, options); + + if (!this._extensions[PerMessageDeflate.extensionName]) { + opts.compress = false; + } + + this._sender.send(data || constants.EMPTY_BUFFER, opts, cb); + } + + /** + * Forcibly close the connection. + * + * @public + */ + terminate () { + if (this.readyState === WebSocket.CLOSED) return; + if (this.readyState === WebSocket.CONNECTING) { + const msg = 'WebSocket was closed before the connection was established'; + return abortHandshake(this, this._req, msg); + } + + if (this._socket) { + this.readyState = WebSocket.CLOSING; + this._socket.destroy(); + } + } +} + +readyStates.forEach((readyState, i) => { + WebSocket[readyStates[i]] = i; +}); + +// +// Add the `onopen`, `onerror`, `onclose`, and `onmessage` attributes. +// See https://html.spec.whatwg.org/multipage/comms.html#the-websocket-interface +// +['open', 'error', 'close', 'message'].forEach((method) => { + Object.defineProperty(WebSocket.prototype, `on${method}`, { + /** + * Return the listener of the event. + * + * @return {(Function|undefined)} The event listener or `undefined` + * @public + */ + get () { + const listeners = this.listeners(method); + for (var i = 0; i < listeners.length; i++) { + if (listeners[i]._listener) return listeners[i]._listener; + } + }, + /** + * Add a listener for the event. + * + * @param {Function} listener The listener to add + * @public + */ + set (listener) { + const listeners = this.listeners(method); + for (var i = 0; i < listeners.length; i++) { + // + // Remove only the listeners added via `addEventListener`. + // + if (listeners[i]._listener) this.removeListener(method, listeners[i]); + } + this.addEventListener(method, listener); + } + }); +}); + +WebSocket.prototype.addEventListener = EventTarget.addEventListener; +WebSocket.prototype.removeEventListener = EventTarget.removeEventListener; + +module.exports = WebSocket; + +/** + * Initialize a WebSocket client. + * + * @param {(String|url.Url|url.URL)} address The URL to which to connect + * @param {String} protocols The subprotocols + * @param {Object} options Connection options + * @param {(Boolean|Object)} options.perMessageDeflate Enable/disable permessage-deflate + * @param {Number} options.handshakeTimeout Timeout in milliseconds for the handshake request + * @param {Number} options.protocolVersion Value of the `Sec-WebSocket-Version` header + * @param {String} options.origin Value of the `Origin` or `Sec-WebSocket-Origin` header + * @private + */ +function initAsClient (address, protocols, options) { + options = Object.assign({ + protocolVersion: protocolVersions[1], + perMessageDeflate: true + }, options, { + createConnection: undefined, + socketPath: undefined, + hostname: undefined, + protocol: undefined, + timeout: undefined, + method: undefined, + auth: undefined, + host: undefined, + path: undefined, + port: undefined + }); + + if (protocolVersions.indexOf(options.protocolVersion) === -1) { + throw new RangeError( + `Unsupported protocol version: ${options.protocolVersion} ` + + `(supported versions: ${protocolVersions.join(', ')})` + ); + } + + this._isServer = false; + + var parsedUrl; + + if (typeof address === 'object' && address.href !== undefined) { + parsedUrl = address; + this.url = address.href; + } else { + parsedUrl = url.parse(address); + this.url = address; + } + + const isUnixSocket = parsedUrl.protocol === 'ws+unix:'; + + if (!parsedUrl.host && (!isUnixSocket || !parsedUrl.pathname)) { + throw new Error(`Invalid URL: ${this.url}`); + } + + const isSecure = parsedUrl.protocol === 'wss:' || parsedUrl.protocol === 'https:'; + const key = crypto.randomBytes(16).toString('base64'); + const httpObj = isSecure ? https : http; + const path = parsedUrl.search + ? `${parsedUrl.pathname || '/'}${parsedUrl.search}` + : parsedUrl.pathname || '/'; + var perMessageDeflate; + + options.createConnection = isSecure ? tlsConnect : netConnect; + options.port = parsedUrl.port || (isSecure ? 443 : 80); + options.host = parsedUrl.hostname.startsWith('[') + ? parsedUrl.hostname.slice(1, -1) + : parsedUrl.hostname; + options.headers = Object.assign({ + 'Sec-WebSocket-Version': options.protocolVersion, + 'Sec-WebSocket-Key': key, + 'Connection': 'Upgrade', + 'Upgrade': 'websocket' + }, options.headers); + options.path = path; + + if (options.perMessageDeflate) { + perMessageDeflate = new PerMessageDeflate( + options.perMessageDeflate !== true ? options.perMessageDeflate : {}, + false + ); + options.headers['Sec-WebSocket-Extensions'] = extension.format({ + [PerMessageDeflate.extensionName]: perMessageDeflate.offer() + }); + } + if (protocols) { + options.headers['Sec-WebSocket-Protocol'] = protocols; + } + if (options.origin) { + if (options.protocolVersion < 13) { + options.headers['Sec-WebSocket-Origin'] = options.origin; + } else { + options.headers.Origin = options.origin; + } + } + if (parsedUrl.auth) { + options.auth = parsedUrl.auth; + } else if (parsedUrl.username || parsedUrl.password) { + options.auth = `${parsedUrl.username}:${parsedUrl.password}`; + } + + if (isUnixSocket) { + const parts = path.split(':'); + + if (options.agent == null && process.versions.modules < 57) { + // + // Setting `socketPath` in conjunction with `createConnection` without an + // agent throws an error on Node.js < 8. Work around the issue by using a + // different property. + // + options._socketPath = parts[0]; + } else { + options.socketPath = parts[0]; + } + + options.path = parts[1]; + } + + var req = this._req = httpObj.get(options); + + if (options.handshakeTimeout) { + req.setTimeout( + options.handshakeTimeout, + () => abortHandshake(this, req, 'Opening handshake has timed out') + ); + } + + req.on('error', (err) => { + if (this._req.aborted) return; + + req = this._req = null; + this.readyState = WebSocket.CLOSING; + this.emit('error', err); + this.emitClose(); + }); + + req.on('response', (res) => { + if (this.emit('unexpected-response', req, res)) return; + + abortHandshake(this, req, `Unexpected server response: ${res.statusCode}`); + }); + + req.on('upgrade', (res, socket, head) => { + this.emit('upgrade', res); + + // + // The user may have closed the connection from a listener of the `upgrade` + // event. + // + if (this.readyState !== WebSocket.CONNECTING) return; + + req = this._req = null; + + const digest = crypto.createHash('sha1') + .update(key + constants.GUID, 'binary') + .digest('base64'); + + if (res.headers['sec-websocket-accept'] !== digest) { + abortHandshake(this, socket, 'Invalid Sec-WebSocket-Accept header'); + return; + } + + const serverProt = res.headers['sec-websocket-protocol']; + const protList = (protocols || '').split(/, */); + var protError; + + if (!protocols && serverProt) { + protError = 'Server sent a subprotocol but none was requested'; + } else if (protocols && !serverProt) { + protError = 'Server sent no subprotocol'; + } else if (serverProt && protList.indexOf(serverProt) === -1) { + protError = 'Server sent an invalid subprotocol'; + } + + if (protError) { + abortHandshake(this, socket, protError); + return; + } + + if (serverProt) this.protocol = serverProt; + + if (perMessageDeflate) { + try { + const extensions = extension.parse( + res.headers['sec-websocket-extensions'] + ); + + if (extensions[PerMessageDeflate.extensionName]) { + perMessageDeflate.accept( + extensions[PerMessageDeflate.extensionName] + ); + this._extensions[PerMessageDeflate.extensionName] = perMessageDeflate; + } + } catch (err) { + abortHandshake(this, socket, 'Invalid Sec-WebSocket-Extensions header'); + return; + } + } + + this.setSocket(socket, head, 0); + }); +} + +/** + * Create a `net.Socket` and initiate a connection. + * + * @param {Object} options Connection options + * @return {net.Socket} The newly created socket used to start the connection + * @private + */ +function netConnect (options) { + options.path = options.socketPath || options._socketPath || undefined; + return net.connect(options); +} + +/** + * Create a `tls.TLSSocket` and initiate a connection. + * + * @param {Object} options Connection options + * @return {tls.TLSSocket} The newly created socket used to start the connection + * @private + */ +function tlsConnect (options) { + options.path = options.socketPath || options._socketPath || undefined; + options.servername = options.servername || options.host; + return tls.connect(options); +} + +/** + * Abort the handshake and emit an error. + * + * @param {WebSocket} websocket The WebSocket instance + * @param {(http.ClientRequest|net.Socket)} stream The request to abort or the + * socket to destroy + * @param {String} message The error message + * @private + */ +function abortHandshake (websocket, stream, message) { + websocket.readyState = WebSocket.CLOSING; + + const err = new Error(message); + Error.captureStackTrace(err, abortHandshake); + + if (stream.setHeader) { + stream.abort(); + stream.once('abort', websocket.emitClose.bind(websocket)); + websocket.emit('error', err); + } else { + stream.destroy(err); + stream.once('error', websocket.emit.bind(websocket, 'error')); + stream.once('close', websocket.emitClose.bind(websocket)); + } +} + +/** + * The listener of the `Receiver` `'conclude'` event. + * + * @param {Number} code The status code + * @param {String} reason The reason for closing + * @private + */ +function receiverOnConclude (code, reason) { + const websocket = this[kWebSocket]; + + websocket._socket.removeListener('data', socketOnData); + websocket._socket.resume(); + + websocket._closeFrameReceived = true; + websocket._closeMessage = reason; + websocket._closeCode = code; + + if (code === 1005) websocket.close(); + else websocket.close(code, reason); +} + +/** + * The listener of the `Receiver` `'drain'` event. + * + * @private + */ +function receiverOnDrain () { + this[kWebSocket]._socket.resume(); +} + +/** + * The listener of the `Receiver` `'error'` event. + * + * @param {(RangeError|Error)} err The emitted error + * @private + */ +function receiverOnError (err) { + const websocket = this[kWebSocket]; + + websocket._socket.removeListener('data', socketOnData); + + websocket.readyState = WebSocket.CLOSING; + websocket._closeCode = err[constants.kStatusCode]; + websocket.emit('error', err); + websocket._socket.destroy(); +} + +/** + * The listener of the `Receiver` `'finish'` event. + * + * @private + */ +function receiverOnFinish () { + this[kWebSocket].emitClose(); +} + +/** + * The listener of the `Receiver` `'message'` event. + * + * @param {(String|Buffer|ArrayBuffer|Buffer[])} data The message + * @private + */ +function receiverOnMessage (data) { + this[kWebSocket].emit('message', data); +} + +/** + * The listener of the `Receiver` `'ping'` event. + * + * @param {Buffer} data The data included in the ping frame + * @private + */ +function receiverOnPing (data) { + const websocket = this[kWebSocket]; + + websocket.pong(data, !websocket._isServer, constants.NOOP); + websocket.emit('ping', data); +} + +/** + * The listener of the `Receiver` `'pong'` event. + * + * @param {Buffer} data The data included in the pong frame + * @private + */ +function receiverOnPong (data) { + this[kWebSocket].emit('pong', data); +} + +/** + * The listener of the `net.Socket` `'close'` event. + * + * @private + */ +function socketOnClose () { + const websocket = this[kWebSocket]; + + this.removeListener('close', socketOnClose); + this.removeListener('end', socketOnEnd); + + websocket.readyState = WebSocket.CLOSING; + + // + // The close frame might not have been received or the `'end'` event emitted, + // for example, if the socket was destroyed due to an error. Ensure that the + // `receiver` stream is closed after writing any remaining buffered data to + // it. If the readable side of the socket is in flowing mode then there is no + // buffered data as everything has been already written and `readable.read()` + // will return `null`. If instead, the socket is paused, any possible buffered + // data will be read as a single chunk and emitted synchronously in a single + // `'data'` event. + // + websocket._socket.read(); + websocket._receiver.end(); + + this.removeListener('data', socketOnData); + this[kWebSocket] = undefined; + + clearTimeout(websocket._closeTimer); + + if ( + websocket._receiver._writableState.finished || + websocket._receiver._writableState.errorEmitted + ) { + websocket.emitClose(); + } else { + websocket._receiver.on('error', receiverOnFinish); + websocket._receiver.on('finish', receiverOnFinish); + } +} + +/** + * The listener of the `net.Socket` `'data'` event. + * + * @param {Buffer} chunk A chunk of data + * @private + */ +function socketOnData (chunk) { + if (!this[kWebSocket]._receiver.write(chunk)) { + this.pause(); + } +} + +/** + * The listener of the `net.Socket` `'end'` event. + * + * @private + */ +function socketOnEnd () { + const websocket = this[kWebSocket]; + + websocket.readyState = WebSocket.CLOSING; + websocket._receiver.end(); + this.end(); +} + +/** + * The listener of the `net.Socket` `'error'` event. + * + * @private + */ +function socketOnError () { + const websocket = this[kWebSocket]; + + this.removeListener('error', socketOnError); + this.on('error', constants.NOOP); + + if (websocket) { + websocket.readyState = WebSocket.CLOSING; + this.destroy(); + } +} diff --git a/package.json b/package.json index 1a7b69893..df8629a82 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,8 @@ { - "author": "Einar Otto Stangvik (http://2x.io)", "name": "ws", - "description": "simple to use, blazing fast and thoroughly tested websocket client, server and console for node.js, up-to-date against RFC-6455", - "version": "1.1.1", - "license": "MIT", - "main": "index.js", + "version": "5.2.2", + "description": "Simple to use, blazing fast and thoroughly tested websocket client and server for Node.js", "keywords": [ - "Hixie", "HyBi", "Push", "RFC-6455", @@ -14,27 +10,35 @@ "WebSockets", "real-time" ], - "repository": { - "type": "git", - "url": "git://github.com/websockets/ws.git" - }, + "homepage": "https://github.com/websockets/ws", + "bugs": "https://github.com/websockets/ws/issues", + "repository": "websockets/ws", + "author": "Einar Otto Stangvik (http://2x.io)", + "license": "MIT", + "main": "index.js", + "files": [ + "index.js", + "lib" + ], "scripts": { - "test": "make test" + "test": "eslint . && nyc --reporter=html --reporter=text mocha test/*.test.js", + "integration": "eslint . && mocha test/*.integration.js", + "lint": "eslint ." }, "dependencies": { - "options": ">=0.0.5", - "ultron": "1.0.x" + "async-limiter": "~1.0.0" }, "devDependencies": { - "ansi": "0.3.x", - "benchmark": "0.3.x", - "bufferutil": "1.2.x", - "expect.js": "0.3.x", - "istanbul": "^0.4.1", - "mocha": "2.3.x", - "should": "8.0.x", - "tinycolor": "0.0.x", - "utf-8-validate": "1.2.x" - }, - "gypfile": true + "benchmark": "~2.1.2", + "bufferutil": "~3.0.0", + "eslint": "~4.19.0", + "eslint-config-standard": "~11.0.0", + "eslint-plugin-import": "~2.12.0", + "eslint-plugin-node": "~6.0.0", + "eslint-plugin-promise": "~3.8.0", + "eslint-plugin-standard": "~3.0.0", + "mocha": "~5.2.0", + "nyc": "~12.0.2", + "utf-8-validate": "~4.0.0" + } } diff --git a/test/BufferPool.test.js b/test/BufferPool.test.js deleted file mode 100644 index ccd087ecb..000000000 --- a/test/BufferPool.test.js +++ /dev/null @@ -1,72 +0,0 @@ -var BufferPool = require('../lib/BufferPool'); -require('should'); - -describe('BufferPool', function() { - describe('#ctor', function() { - it('allocates pool', function() { - var db = new BufferPool(1000); - db.size.should.eql(1000); - }); - it('throws TypeError when called without new', function(done) { - try { - var db = BufferPool(1000); - } - catch (e) { - e.should.be.instanceof(TypeError); - done(); - } - }); - }); - describe('#get', function() { - it('grows the pool if necessary', function() { - var db = new BufferPool(1000); - var buf = db.get(2000); - db.size.should.be.above(1000); - db.used.should.eql(2000); - buf.length.should.eql(2000); - }); - it('grows the pool after the first call, if necessary', function() { - var db = new BufferPool(1000); - var buf = db.get(1000); - db.used.should.eql(1000); - db.size.should.eql(1000); - buf.length.should.eql(1000); - var buf2 = db.get(1000); - db.used.should.eql(2000); - db.size.should.be.above(1000); - buf2.length.should.eql(1000); - }); - it('grows the pool according to the growStrategy if necessary', function() { - var db = new BufferPool(1000, function(db, length) { - return db.size + 2345; - }); - var buf = db.get(2000); - db.size.should.eql(3345); - buf.length.should.eql(2000); - }); - it('doesnt grow the pool if theres enough room available', function() { - var db = new BufferPool(1000); - var buf = db.get(1000); - db.size.should.eql(1000); - buf.length.should.eql(1000); - }); - }); - describe('#reset', function() { - it('shinks the pool', function() { - var db = new BufferPool(1000); - var buf = db.get(2000); - db.reset(true); - db.size.should.eql(1000); - }); - it('shrinks the pool according to the shrinkStrategy', function() { - var db = new BufferPool(1000, function(db, length) { - return db.used + length; - }, function(db) { - return 0; - }); - var buf = db.get(2000); - db.reset(true); - db.size.should.eql(0); - }); - }); -}); diff --git a/test/Extensions.test.js b/test/Extensions.test.js deleted file mode 100644 index 84ec5edac..000000000 --- a/test/Extensions.test.js +++ /dev/null @@ -1,53 +0,0 @@ -var Extensions = require('../lib/Extensions'); -require('should'); - -describe('Extensions', function() { - describe('parse', function() { - it('should parse', function() { - var extensions = Extensions.parse('foo'); - extensions.should.eql({ foo: [{}] }); - }); - - it('should parse params', function() { - var extensions = Extensions.parse('foo; bar; baz=1; bar=2'); - extensions.should.eql({ - foo: [{ bar: [true, '2'], baz: ['1'] }] - }); - }); - - it('should parse multiple extensions', function() { - var extensions = Extensions.parse('foo, bar; baz, foo; baz'); - extensions.should.eql({ - foo: [{}, { baz: [true] }], - bar: [{ baz: [true] }] - }); - }); - - it('should parse quoted params', function() { - var extensions = Extensions.parse('foo; bar="hi"'); - extensions.should.eql({ - foo: [{ bar: ['hi'] }] - }); - }); - }); - - describe('format', function() { - it('should format', function() { - var extensions = Extensions.format({ foo: {} }); - extensions.should.eql('foo'); - }); - - it('should format params', function() { - var extensions = Extensions.format({ foo: { bar: [true, 2], baz: 1 } }); - extensions.should.eql('foo; bar; bar=2; baz=1'); - }); - - it('should format multiple extensions', function() { - var extensions = Extensions.format({ - foo: [{}, { baz: true }], - bar: { baz: true } - }); - extensions.should.eql('foo, foo; baz, bar; baz'); - }); - }); -}); diff --git a/test/PerMessageDeflate.test.js b/test/PerMessageDeflate.test.js deleted file mode 100644 index 6b70ccbdf..000000000 --- a/test/PerMessageDeflate.test.js +++ /dev/null @@ -1,279 +0,0 @@ -var PerMessageDeflate = require('../lib/PerMessageDeflate'); -var Extensions = require('../lib/Extensions'); -require('should'); - -describe('PerMessageDeflate', function() { - describe('#ctor', function() { - it('throws TypeError when called without new', function(done) { - try { - var perMessageDeflate = PerMessageDeflate(); - } - catch (e) { - e.should.be.instanceof(TypeError); - done(); - } - }); - }); - - describe('#offer', function() { - it('should create default params', function() { - var perMessageDeflate = new PerMessageDeflate(); - perMessageDeflate.offer().should.eql({ client_max_window_bits: true }); - }); - - it('should create params from options', function() { - var perMessageDeflate = new PerMessageDeflate({ - serverNoContextTakeover: true, - clientNoContextTakeover: true, - serverMaxWindowBits: 10, - clientMaxWindowBits: 11 - }); - perMessageDeflate.offer().should.eql({ - server_no_context_takeover: true, - client_no_context_takeover: true, - server_max_window_bits: 10, - client_max_window_bits: 11 - }); - }); - }); - - describe('#accept', function() { - describe('as server', function() { - it('should accept empty offer', function() { - var perMessageDeflate = new PerMessageDeflate({}, true); - perMessageDeflate.accept([{}]).should.eql({}); - }); - - it('should accept offer', function() { - var perMessageDeflate = new PerMessageDeflate({}, true); - var extensions = Extensions.parse('permessage-deflate; server_no_context_takeover; client_no_context_takeover; server_max_window_bits=10; client_max_window_bits=11'); - perMessageDeflate.accept(extensions['permessage-deflate']).should.eql({ - server_no_context_takeover: true, - client_no_context_takeover: true, - server_max_window_bits: 10, - client_max_window_bits: 11 - }); - }); - - it('should prefer configuration than offer', function() { - var perMessageDeflate = new PerMessageDeflate({ - serverNoContextTakeover: true, - clientNoContextTakeover: true, - serverMaxWindowBits: 12, - clientMaxWindowBits: 11 - }, true); - var extensions = Extensions.parse('permessage-deflate; server_max_window_bits=14; client_max_window_bits=13'); - perMessageDeflate.accept(extensions['permessage-deflate']).should.eql({ - server_no_context_takeover: true, - client_no_context_takeover: true, - server_max_window_bits: 12, - client_max_window_bits: 11 - }); - }); - - it('should fallback', function() { - var perMessageDeflate = new PerMessageDeflate({ serverMaxWindowBits: 11 }, true); - var extensions = Extensions.parse('permessage-deflate; server_max_window_bits=10, permessage-deflate'); - perMessageDeflate.accept(extensions['permessage-deflate']).should.eql({ - server_max_window_bits: 11 - }); - }); - - it('should throw an error if server_no_context_takeover is unsupported', function() { - var perMessageDeflate = new PerMessageDeflate({ serverNoContextTakeover: false }, true); - var extensions = Extensions.parse('permessage-deflate; server_no_context_takeover'); - (function() { - perMessageDeflate.accept(extensions['permessage-deflate']); - }).should.throw(); - }); - - it('should throw an error if server_max_window_bits is unsupported', function() { - var perMessageDeflate = new PerMessageDeflate({ serverMaxWindowBits: false }, true); - var extensions = Extensions.parse('permessage-deflate; server_max_window_bits=10'); - (function() { - perMessageDeflate.accept(extensions['permessage-deflate']); - }).should.throw(); - }); - - it('should throw an error if server_max_window_bits is less than configuration', function() { - var perMessageDeflate = new PerMessageDeflate({ serverMaxWindowBits: 11 }, true); - var extensions = Extensions.parse('permessage-deflate; server_max_window_bits=10'); - (function() { - perMessageDeflate.accept(extensions['permessage-deflate']); - }).should.throw(); - }); - - it('should throw an error if client_max_window_bits is unsupported on client', function() { - var perMessageDeflate = new PerMessageDeflate({ clientMaxWindowBits: 10 }, true); - var extensions = Extensions.parse('permessage-deflate'); - (function() { - perMessageDeflate.accept(extensions['permessage-deflate']); - }).should.throw(); - }); - }); - - describe('as client', function() { - it('should accept empty response', function() { - var perMessageDeflate = new PerMessageDeflate({}); - perMessageDeflate.accept([{}]).should.eql({}); - }); - - it('should accept response parameter', function() { - var perMessageDeflate = new PerMessageDeflate({}); - var extensions = Extensions.parse('permessage-deflate; server_no_context_takeover; client_no_context_takeover; server_max_window_bits=10; client_max_window_bits=11'); - perMessageDeflate.accept(extensions['permessage-deflate']).should.eql({ - server_no_context_takeover: true, - client_no_context_takeover: true, - server_max_window_bits: 10, - client_max_window_bits: 11 - }); - }); - - it('should throw an error if client_no_context_takeover is unsupported', function() { - var perMessageDeflate = new PerMessageDeflate({ clientNoContextTakeover: false }); - var extensions = Extensions.parse('permessage-deflate; client_no_context_takeover'); - (function() { - perMessageDeflate.accept(extensions['permessage-deflate']); - }).should.throw(); - }); - - it('should throw an error if client_max_window_bits is unsupported', function() { - var perMessageDeflate = new PerMessageDeflate({ clientMaxWindowBits: false }); - var extensions = Extensions.parse('permessage-deflate; client_max_window_bits=10'); - (function() { - perMessageDeflate.accept(extensions['permessage-deflate']); - }).should.throw(); - }); - - it('should throw an error if client_max_window_bits is greater than configuration', function() { - var perMessageDeflate = new PerMessageDeflate({ clientMaxWindowBits: 10 }); - var extensions = Extensions.parse('permessage-deflate; client_max_window_bits=11'); - (function() { - perMessageDeflate.accept(extensions['permessage-deflate']); - }).should.throw(); - }); - }); - - describe('validate parameters', function() { - it('should throw an error if a parameter has multiple values', function() { - var perMessageDeflate = new PerMessageDeflate(); - var extensions = Extensions.parse('permessage-deflate; server_no_context_takeover; server_no_context_takeover'); - (function() { - perMessageDeflate.accept(extensions['permessage-deflate']); - }).should.throw(); - }); - - it('should throw an error if a parameter is undefined', function() { - var perMessageDeflate = new PerMessageDeflate(); - var extensions = Extensions.parse('permessage-deflate; foo;'); - (function() { - perMessageDeflate.accept(extensions['permessage-deflate']); - }).should.throw(); - }); - - it('should throw an error if server_no_context_takeover has a value', function() { - var perMessageDeflate = new PerMessageDeflate(); - var extensions = Extensions.parse('permessage-deflate; server_no_context_takeover=10'); - (function() { - perMessageDeflate.accept(extensions['permessage-deflate']); - }).should.throw(); - }); - - it('should throw an error if client_no_context_takeover has a value', function() { - var perMessageDeflate = new PerMessageDeflate(); - var extensions = Extensions.parse('permessage-deflate; client_no_context_takeover=10'); - (function() { - perMessageDeflate.accept(extensions['permessage-deflate']); - }).should.throw(); - }); - - it('should throw an error if server_max_window_bits has an invalid value', function() { - var perMessageDeflate = new PerMessageDeflate(); - var extensions = Extensions.parse('permessage-deflate; server_max_window_bits=7'); - (function() { - perMessageDeflate.accept(extensions['permessage-deflate']); - }).should.throw(); - }); - - it('should throw an error if client_max_window_bits has an invalid value', function() { - var perMessageDeflate = new PerMessageDeflate(); - var extensions = Extensions.parse('permessage-deflate; client_max_window_bits=16'); - (function() { - perMessageDeflate.accept(extensions['permessage-deflate']); - }).should.throw(); - }); - }); - }); - - describe('#compress/#decompress', function() { - it('should compress/decompress data', function(done) { - var perMessageDeflate = new PerMessageDeflate(); - perMessageDeflate.accept([{}]); - perMessageDeflate.compress(new Buffer([1, 2, 3]), true, function(err, compressed) { - if (err) return done(err); - perMessageDeflate.decompress(compressed, true, function(err, data) { - if (err) return done(err); - data.should.eql(new Buffer([1, 2, 3])); - done(); - }); - }); - }); - - it('should compress/decompress fragments', function(done) { - var perMessageDeflate = new PerMessageDeflate(); - perMessageDeflate.accept([{}]); - - var buf = new Buffer([1, 2, 3, 4]); - perMessageDeflate.compress(buf.slice(0, 2), false, function(err, compressed1) { - if (err) return done(err); - perMessageDeflate.compress(buf.slice(2), true, function(err, compressed2) { - if (err) return done(err); - perMessageDeflate.decompress(compressed1, false, function(err, data1) { - if (err) return done(err); - perMessageDeflate.decompress(compressed2, true, function(err, data2) { - if (err) return done(err); - new Buffer.concat([data1, data2]).should.eql(new Buffer([1, 2, 3, 4])); - done(); - }); - }); - }); - }); - }); - - it('should compress/decompress data with parameters', function(done) { - var perMessageDeflate = new PerMessageDeflate({ memLevel: 5 }); - var extensions = Extensions.parse('permessage-deflate; server_no_context_takeover; client_no_context_takeover; server_max_window_bits=10; client_max_window_bits=11'); - perMessageDeflate.accept(extensions['permessage-deflate']); - perMessageDeflate.compress(new Buffer([1, 2, 3]), true, function(err, compressed) { - if (err) return done(err); - perMessageDeflate.decompress(compressed, true, function(err, data) { - if (err) return done(err); - data.should.eql(new Buffer([1, 2, 3])); - done(); - }); - }); - }); - - it('should compress/decompress data with no context takeover', function(done) { - var perMessageDeflate = new PerMessageDeflate(); - var extensions = Extensions.parse('permessage-deflate; server_no_context_takeover; client_no_context_takeover'); - perMessageDeflate.accept(extensions['permessage-deflate']); - var buf = new Buffer('foofoo'); - perMessageDeflate.compress(buf, true, function(err, compressed1) { - if (err) return done(err); - perMessageDeflate.decompress(compressed1, true, function(err, data) { - if (err) return done(err); - perMessageDeflate.compress(data, true, function(err, compressed2) { - if (err) return done(err); - perMessageDeflate.decompress(compressed2, true, function(err, data) { - if (err) return done(err); - compressed2.length.should.equal(compressed1.length); - data.should.eql(buf); - done(); - }); - }); - }); - }); - }); - }); -}); diff --git a/test/Receiver.hixie.test.js b/test/Receiver.hixie.test.js deleted file mode 100644 index 8646d7683..000000000 --- a/test/Receiver.hixie.test.js +++ /dev/null @@ -1,170 +0,0 @@ -var assert = require('assert') - , expect = require('expect.js') - , Receiver = require('../lib/Receiver.hixie'); -require('./hybi-common'); - -describe('Receiver', function() { - describe('#ctor', function() { - it('throws TypeError when called without new', function(done) { - try { - var p = Receiver(); - } - catch (e) { - e.should.be.instanceof(TypeError); - done(); - } - }); - }); - - it('can parse text message', function() { - var p = new Receiver(); - var packet = '00 48 65 6c 6c 6f ff'; - - var gotData = false; - p.ontext = function(data) { - gotData = true; - assert.equal('Hello', data); - }; - - p.add(getBufferFromHexString(packet)); - expect(gotData).to.equal(true); - }); - - it('can parse multiple text messages', function() { - var p = new Receiver(); - var packet = '00 48 65 6c 6c 6f ff 00 48 65 6c 6c 6f ff'; - - var gotData = false; - var messages = []; - p.ontext = function(data) { - gotData = true; - messages.push(data); - }; - - p.add(getBufferFromHexString(packet)); - expect(gotData).to.equal(true); - for (var i = 0; i < 2; ++i) { - expect(messages[i]).to.equal('Hello'); - } - }); - - it('can parse empty message', function() { - var p = new Receiver(); - var packet = '00 ff'; - - var gotData = false; - p.ontext = function(data) { - gotData = true; - assert.equal('', data); - }; - - p.add(getBufferFromHexString(packet)); - expect(gotData).to.equal(true); - }); - - it('can parse text messages delivered over multiple frames', function() { - var p = new Receiver(); - var packets = [ - '00 48', - '65 6c 6c', - '6f ff 00 48', - '65', - '6c 6c 6f', - 'ff' - ]; - - var gotData = false; - var messages = []; - p.ontext = function(data) { - gotData = true; - messages.push(data); - }; - - for (var i = 0; i < packets.length; ++i) { - p.add(getBufferFromHexString(packets[i])); - } - expect(gotData).to.equal(true); - for (var i = 0; i < 2; ++i) { - expect(messages[i]).to.equal('Hello'); - } - }); - - it('emits an error if a payload doesnt start with 0x00', function() { - var p = new Receiver(); - var packets = [ - '00 6c ff', - '00 6c ff ff', - 'ff 00 6c ff 00 6c ff', - '00', - '6c 6c 6f', - 'ff' - ]; - - var gotData = false; - var gotError = false; - var messages = []; - p.ontext = function(data) { - gotData = true; - messages.push(data); - }; - p.onerror = function(reason, code) { - gotError = code == true; - }; - - for (var i = 0; i < packets.length && !gotError; ++i) { - p.add(getBufferFromHexString(packets[i])); - } - expect(gotError).to.equal(true); - expect(messages[0]).to.equal('l'); - expect(messages[1]).to.equal('l'); - expect(messages.length).to.equal(2); - }); - - it('can parse close messages', function() { - var p = new Receiver(); - var packets = [ - 'ff 00' - ]; - - var gotClose = false; - var gotError = false; - p.onclose = function() { - gotClose = true; - }; - p.onerror = function(reason, code) { - gotError = code == true; - }; - - for (var i = 0; i < packets.length && !gotError; ++i) { - p.add(getBufferFromHexString(packets[i])); - } - expect(gotClose).to.equal(true); - expect(gotError).to.equal(false); - }); - - it('can parse binary messages delivered over multiple frames', function() { - var p = new Receiver(); - var packets = [ - '80 05 48', - '65 6c 6c', - '6f 80 80 05 48', - '65', - '6c 6c 6f' - ]; - - var gotData = false; - var messages = []; - p.ontext = function(data) { - gotData = true; - messages.push(data); - }; - - for (var i = 0; i < packets.length; ++i) { - p.add(getBufferFromHexString(packets[i])); - } - expect(gotData).to.equal(true); - for (var i = 0; i < 2; ++i) { - expect(messages[i]).to.equal('Hello'); - } - }); -}); diff --git a/test/Receiver.test.js b/test/Receiver.test.js deleted file mode 100644 index 9c3343bf7..000000000 --- a/test/Receiver.test.js +++ /dev/null @@ -1,428 +0,0 @@ -var assert = require('assert') - , Receiver = require('../lib/Receiver') - , PerMessageDeflate = require('../lib/PerMessageDeflate'); -require('should'); -require('./hybi-common'); - -describe('Receiver', function() { - describe('#ctor', function() { - it('throws TypeError when called without new', function(done) { - try { - var p = Receiver(); - } - catch (e) { - e.should.be.instanceof(TypeError); - done(); - } - }); - }); - - it('can parse unmasked text message', function() { - var p = new Receiver(); - var packet = '81 05 48 65 6c 6c 6f'; - - var gotData = false; - p.ontext = function(data) { - gotData = true; - assert.equal('Hello', data); - }; - - p.add(getBufferFromHexString(packet)); - gotData.should.be.ok; - }); - it('can parse close message', function() { - var p = new Receiver(); - var packet = '88 00'; - - var gotClose = false; - p.onclose = function(data) { - gotClose = true; - }; - - p.add(getBufferFromHexString(packet)); - gotClose.should.be.ok; - }); - it('can parse masked text message', function() { - var p = new Receiver(); - var packet = '81 93 34 83 a8 68 01 b9 92 52 4f a1 c6 09 59 e6 8a 52 16 e6 cb 00 5b a1 d5'; - - var gotData = false; - p.ontext = function(data) { - gotData = true; - assert.equal('5:::{"name":"echo"}', data); - }; - - p.add(getBufferFromHexString(packet)); - gotData.should.be.ok; - }); - it('can parse a masked text message longer than 125 bytes', function() { - var p = new Receiver(); - var message = 'A'; - for (var i = 0; i < 300; ++i) message += (i % 5).toString(); - var packet = '81 FE ' + pack(4, message.length) + ' 34 83 a8 68 ' + getHexStringFromBuffer(mask(message, '34 83 a8 68')); - - var gotData = false; - p.ontext = function(data) { - gotData = true; - assert.equal(message, data); - }; - - p.add(getBufferFromHexString(packet)); - gotData.should.be.ok; - }); - it('can parse a really long masked text message', function() { - var p = new Receiver(); - var message = 'A'; - for (var i = 0; i < 64*1024; ++i) message += (i % 5).toString(); - var packet = '81 FF ' + pack(16, message.length) + ' 34 83 a8 68 ' + getHexStringFromBuffer(mask(message, '34 83 a8 68')); - - var gotData = false; - p.ontext = function(data) { - gotData = true; - assert.equal(message, data); - }; - - p.add(getBufferFromHexString(packet)); - gotData.should.be.ok; - }); - it('can parse a fragmented masked text message of 300 bytes', function() { - var p = new Receiver(); - var message = 'A'; - for (var i = 0; i < 300; ++i) message += (i % 5).toString(); - var msgpiece1 = message.substr(0, 150); - var msgpiece2 = message.substr(150); - var packet1 = '01 FE ' + pack(4, msgpiece1.length) + ' 34 83 a8 68 ' + getHexStringFromBuffer(mask(msgpiece1, '34 83 a8 68')); - var packet2 = '80 FE ' + pack(4, msgpiece2.length) + ' 34 83 a8 68 ' + getHexStringFromBuffer(mask(msgpiece2, '34 83 a8 68')); - - var gotData = false; - p.ontext = function(data) { - gotData = true; - assert.equal(message, data); - }; - - p.add(getBufferFromHexString(packet1)); - p.add(getBufferFromHexString(packet2)); - gotData.should.be.ok; - }); - it('can parse a ping message', function() { - var p = new Receiver(); - var message = 'Hello'; - var packet = '89 ' + getHybiLengthAsHexString(message.length, true) + ' 34 83 a8 68 ' + getHexStringFromBuffer(mask(message, '34 83 a8 68')); - - var gotPing = false; - p.onping = function(data) { - gotPing = true; - assert.equal(message, data); - }; - - p.add(getBufferFromHexString(packet)); - gotPing.should.be.ok; - }); - it('can parse a ping with no data', function() { - var p = new Receiver(); - var packet = '89 00'; - - var gotPing = false; - p.onping = function(data) { - gotPing = true; - }; - - p.add(getBufferFromHexString(packet)); - gotPing.should.be.ok; - }); - it('can parse a fragmented masked text message of 300 bytes with a ping in the middle', function() { - var p = new Receiver(); - var message = 'A'; - for (var i = 0; i < 300; ++i) message += (i % 5).toString(); - - var msgpiece1 = message.substr(0, 150); - var packet1 = '01 FE ' + pack(4, msgpiece1.length) + ' 34 83 a8 68 ' + getHexStringFromBuffer(mask(msgpiece1, '34 83 a8 68')); - - var pingMessage = 'Hello'; - var pingPacket = '89 ' + getHybiLengthAsHexString(pingMessage.length, true) + ' 34 83 a8 68 ' + getHexStringFromBuffer(mask(pingMessage, '34 83 a8 68')); - - var msgpiece2 = message.substr(150); - var packet2 = '80 FE ' + pack(4, msgpiece2.length) + ' 34 83 a8 68 ' + getHexStringFromBuffer(mask(msgpiece2, '34 83 a8 68')); - - var gotData = false; - p.ontext = function(data) { - gotData = true; - assert.equal(message, data); - }; - var gotPing = false; - p.onping = function(data) { - gotPing = true; - assert.equal(pingMessage, data); - }; - - p.add(getBufferFromHexString(packet1)); - p.add(getBufferFromHexString(pingPacket)); - p.add(getBufferFromHexString(packet2)); - gotData.should.be.ok; - gotPing.should.be.ok; - }); - it('can parse a fragmented masked text message of 300 bytes with a ping in the middle, which is delievered over sevaral tcp packets', function() { - var p = new Receiver(); - var message = 'A'; - for (var i = 0; i < 300; ++i) message += (i % 5).toString(); - - var msgpiece1 = message.substr(0, 150); - var packet1 = '01 FE ' + pack(4, msgpiece1.length) + ' 34 83 a8 68 ' + getHexStringFromBuffer(mask(msgpiece1, '34 83 a8 68')); - - var pingMessage = 'Hello'; - var pingPacket = '89 ' + getHybiLengthAsHexString(pingMessage.length, true) + ' 34 83 a8 68 ' + getHexStringFromBuffer(mask(pingMessage, '34 83 a8 68')); - - var msgpiece2 = message.substr(150); - var packet2 = '80 FE ' + pack(4, msgpiece2.length) + ' 34 83 a8 68 ' + getHexStringFromBuffer(mask(msgpiece2, '34 83 a8 68')); - - var gotData = false; - p.ontext = function(data) { - gotData = true; - assert.equal(message, data); - }; - var gotPing = false; - p.onping = function(data) { - gotPing = true; - assert.equal(pingMessage, data); - }; - - var buffers = []; - buffers = buffers.concat(splitBuffer(getBufferFromHexString(packet1))); - buffers = buffers.concat(splitBuffer(getBufferFromHexString(pingPacket))); - buffers = buffers.concat(splitBuffer(getBufferFromHexString(packet2))); - for (var i = 0; i < buffers.length; ++i) { - p.add(buffers[i]); - } - gotData.should.be.ok; - gotPing.should.be.ok; - }); - it('can parse a 100 byte long masked binary message', function() { - var p = new Receiver(); - var length = 100; - var message = new Buffer(length); - for (var i = 0; i < length; ++i) message[i] = i % 256; - var originalMessage = getHexStringFromBuffer(message); - var packet = '82 ' + getHybiLengthAsHexString(length, true) + ' 34 83 a8 68 ' + getHexStringFromBuffer(mask(message, '34 83 a8 68')); - - var gotData = false; - p.onbinary = function(data) { - gotData = true; - assert.equal(originalMessage, getHexStringFromBuffer(data)); - }; - - p.add(getBufferFromHexString(packet)); - gotData.should.be.ok; - }); - it('can parse a 256 byte long masked binary message', function() { - var p = new Receiver(); - var length = 256; - var message = new Buffer(length); - for (var i = 0; i < length; ++i) message[i] = i % 256; - var originalMessage = getHexStringFromBuffer(message); - var packet = '82 ' + getHybiLengthAsHexString(length, true) + ' 34 83 a8 68 ' + getHexStringFromBuffer(mask(message, '34 83 a8 68')); - - var gotData = false; - p.onbinary = function(data) { - gotData = true; - assert.equal(originalMessage, getHexStringFromBuffer(data)); - }; - - p.add(getBufferFromHexString(packet)); - gotData.should.be.ok; - }); - it('can parse a 200kb long masked binary message', function() { - var p = new Receiver(); - var length = 200 * 1024; - var message = new Buffer(length); - for (var i = 0; i < length; ++i) message[i] = i % 256; - var originalMessage = getHexStringFromBuffer(message); - var packet = '82 ' + getHybiLengthAsHexString(length, true) + ' 34 83 a8 68 ' + getHexStringFromBuffer(mask(message, '34 83 a8 68')); - - var gotData = false; - p.onbinary = function(data) { - gotData = true; - assert.equal(originalMessage, getHexStringFromBuffer(data)); - }; - - p.add(getBufferFromHexString(packet)); - gotData.should.be.ok; - }); - it('can parse a 200kb long unmasked binary message', function() { - var p = new Receiver(); - var length = 200 * 1024; - var message = new Buffer(length); - for (var i = 0; i < length; ++i) message[i] = i % 256; - var originalMessage = getHexStringFromBuffer(message); - var packet = '82 ' + getHybiLengthAsHexString(length, false) + ' ' + getHexStringFromBuffer(message); - - var gotData = false; - p.onbinary = function(data) { - gotData = true; - assert.equal(originalMessage, getHexStringFromBuffer(data)); - }; - - p.add(getBufferFromHexString(packet)); - gotData.should.be.ok; - }); - it('can parse compressed message', function(done) { - var perMessageDeflate = new PerMessageDeflate(); - perMessageDeflate.accept([{}]); - - var p = new Receiver({ 'permessage-deflate': perMessageDeflate }); - var buf = new Buffer('Hello'); - - p.ontext = function(data) { - assert.equal('Hello', data); - done(); - }; - - perMessageDeflate.compress(buf, true, function(err, compressed) { - if (err) return done(err); - p.add(new Buffer([0xc1, compressed.length])); - p.add(compressed); - }); - }); - it('can parse compressed fragments', function(done) { - var perMessageDeflate = new PerMessageDeflate(); - perMessageDeflate.accept([{}]); - - var p = new Receiver({ 'permessage-deflate': perMessageDeflate }); - var buf1 = new Buffer('foo'); - var buf2 = new Buffer('bar'); - - p.ontext = function(data) { - assert.equal('foobar', data); - done(); - }; - - perMessageDeflate.compress(buf1, false, function(err, compressed1) { - if (err) return done(err); - p.add(new Buffer([0x41, compressed1.length])); - p.add(compressed1); - - perMessageDeflate.compress(buf2, true, function(err, compressed2) { - p.add(new Buffer([0x80, compressed2.length])); - p.add(compressed2); - }); - }); - }); - it('will raise an error on a 200kb long masked binary message when maxpayload is 20kb', function() { - var p = new Receiver(20480); - var length = 200 * 1024; - var message = new Buffer(length); - for (var i = 0; i < length; ++i) message[i] = i % 256; - var originalMessage = getHexStringFromBuffer(message); - var packet = '82 ' + getHybiLengthAsHexString(length, true) + ' 34 83 a8 68 ' + getHexStringFromBuffer(mask(message, '34 83 a8 68')); - - var gotError = false; - p.error = function(reason,code) { - gotError = true; - assert.equal(code, 1009); - }; - - p.add(getBufferFromHexString(packet)); - gotError.should.be.ok; - }); - it('will raise an error on a 200kb long unmasked binary message when maxpayload is 20kb', function() { - var p = new Receiver(20480); - var length = 200 * 1024; - var message = new Buffer(length); - for (var i = 0; i < length; ++i) message[i] = i % 256; - var originalMessage = getHexStringFromBuffer(message); - var packet = '82 ' + getHybiLengthAsHexString(length, false) + ' ' + getHexStringFromBuffer(message); - - var gotError = false; - p.error = function(reason,code) { - gotError = true; - assert.equal(code, 1009); - }; - - p.add(getBufferFromHexString(packet)); - gotError.should.be.ok; - }); - it('will raise an error on a compressed message that exceeds maxpayload of 3bytes', function(done) { - var perMessageDeflate = new PerMessageDeflate({},false,3); - perMessageDeflate.accept([{}]); - - var p = new Receiver({ 'permessage-deflate': perMessageDeflate },3); - var buf = new Buffer('Hellooooooooooooooooooooooooooooooooooooooo'); - - p.onerror = function(reason,code) { - assert.equal(code, 1009); - done(); - }; - - perMessageDeflate.compress(buf, true, function(err, compressed) { - if (err) return done(err); - p.add(new Buffer([0xc1, compressed.length])); - p.add(compressed); - }); - }); - it('will raise an error on a compressed fragment that exceeds maxpayload of 2 bytes', function(done) { - var perMessageDeflate = new PerMessageDeflate({},false,2); - perMessageDeflate.accept([{}]); - - var p = new Receiver({ 'permessage-deflate': perMessageDeflate },2); - var buf1 = new Buffer('fooooooooooooooooooooooooooooooooooooooooooooooooooooooo'); - var buf2 = new Buffer('baaaarrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr'); - - p.onerror = function(reason,code) { - assert.equal(code, 1009); - done(); - }; - - perMessageDeflate.compress(buf1, false, function(err, compressed1) { - if (err) return done(err); - p.add(new Buffer([0x41, compressed1.length])); - p.add(compressed1); - - perMessageDeflate.compress(buf2, true, function(err, compressed2) { - p.add(new Buffer([0x80, compressed2.length])); - p.add(compressed2); - }); - }); - }); - it('will not crash if another message is received after receiving a message that exceeds maxpayload', function(done) { - var perMessageDeflate = new PerMessageDeflate({},false,2); - perMessageDeflate.accept([{}]); - - var p = new Receiver({ 'permessage-deflate': perMessageDeflate },2); - var buf1 = new Buffer('fooooooooooooooooooooooooooooooooooooooooooooooooooooooo'); - var buf2 = new Buffer('baaaarrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr'); - - p.onerror = function(reason,code) { - assert.equal(code, 1009); - }; - - perMessageDeflate.compress(buf1, false, function(err, compressed1) { - if (err) return done(err); - p.add(new Buffer([0x41, compressed1.length])); - p.add(compressed1); - - assert.equal(p.onerror,null); - - perMessageDeflate.compress(buf2, true, function(err, compressed2) { - p.add(new Buffer([0x80, compressed2.length])); - p.add(compressed2); - done(); - }); - }); - }); - it('can cleanup during consuming data', function(done) { - var perMessageDeflate = new PerMessageDeflate(); - perMessageDeflate.accept([{}]); - - var p = new Receiver({ 'permessage-deflate': perMessageDeflate }); - var buf = new Buffer('Hello'); - - perMessageDeflate.compress(buf, true, function(err, compressed) { - if (err) return done(err); - var data = Buffer.concat([new Buffer([0xc1, compressed.length]), compressed]); - p.add(data); - p.add(data); - p.add(data); - p.cleanup(); - setTimeout(done, 1000); - }); - }); -}); diff --git a/test/Sender.hixie.test.js b/test/Sender.hixie.test.js deleted file mode 100644 index 3bf3e6474..000000000 --- a/test/Sender.hixie.test.js +++ /dev/null @@ -1,146 +0,0 @@ -var assert = require('assert') - , Sender = require('../lib/Sender.hixie'); -require('should'); -require('./hybi-common'); - -describe('Sender', function() { - describe('#ctor', function() { - it('throws TypeError when called without new', function(done) { - try { - var sender = Sender({ write: function() {} }); - } - catch (e) { - e.should.be.instanceof(TypeError); - done(); - } - }); - }); - - describe('#send', function() { - it('frames and sends a text message', function(done) { - var message = 'Hello world'; - var received; - var socket = { - write: function(data, encoding, cb) { - received = data; - process.nextTick(cb); - } - }; - var sender = new Sender(socket, {}); - sender.send(message, {}, function() { - received.toString('utf8').should.eql('\u0000' + message + '\ufffd'); - done(); - }); - }); - - it('frames and sends an empty message', function(done) { - var socket = { - write: function(data, encoding, cb) { - done(); - } - }; - var sender = new Sender(socket, {}); - sender.send('', {}, function() {}); - }); - - it('frames and sends a buffer', function(done) { - var received; - var socket = { - write: function(data, encoding, cb) { - received = data; - process.nextTick(cb); - } - }; - var sender = new Sender(socket, {}); - sender.send(new Buffer('foobar'), {}, function() { - received.toString('utf8').should.eql('\u0000foobar\ufffd'); - done(); - }); - }); - - it('frames and sends a binary message', function(done) { - var message = 'Hello world'; - var received; - var socket = { - write: function(data, encoding, cb) { - received = data; - process.nextTick(cb); - } - }; - var sender = new Sender(socket, {}); - sender.send(message, {binary: true}, function() { - received.toString('hex').should.eql( - // 0x80 0x0b H e l l o w o r l d - '800b48656c6c6f20776f726c64'); - done(); - }); - }); -/* - it('throws an exception for binary data', function(done) { - var socket = { - write: function(data, encoding, cb) { - process.nextTick(cb); - } - }; - var sender = new Sender(socket, {}); - sender.on('error', function() { - done(); - }); - sender.send(new Buffer(100), {binary: true}, function() {}); - }); -*/ - it('can fauxe stream data', function(done) { - var received = []; - var socket = { - write: function(data, encoding, cb) { - received.push(data); - process.nextTick(cb); - } - }; - var sender = new Sender(socket, {}); - sender.send(new Buffer('foobar'), { fin: false }, function() {}); - sender.send('bazbar', { fin: false }, function() {}); - sender.send(new Buffer('end'), { fin: true }, function() { - received[0].toString('utf8').should.eql('\u0000foobar'); - received[1].toString('utf8').should.eql('bazbar'); - received[2].toString('utf8').should.eql('end\ufffd'); - done(); - }); - }); - }); - - describe('#close', function() { - it('sends a hixie close frame', function(done) { - var received; - var socket = { - write: function(data, encoding, cb) { - received = data; - process.nextTick(cb); - } - }; - var sender = new Sender(socket, {}); - sender.close(null, null, null, function() { - received.toString('utf8').should.eql('\ufffd\u0000'); - done(); - }); - }); - - it('sends a message end marker if fauxe streaming has started, before hixie close frame', function(done) { - var received = []; - var socket = { - write: function(data, encoding, cb) { - received.push(data); - if (cb) process.nextTick(cb); - } - }; - var sender = new Sender(socket, {}); - sender.send(new Buffer('foobar'), { fin: false }, function() {}); - sender.close(null, null, null, function() { - received[0].toString('utf8').should.eql('\u0000foobar'); - received[1].toString('utf8').should.eql('\ufffd'); - received[2].toString('utf8').should.eql('\ufffd\u0000'); - done(); - }); - }); - }); -}); diff --git a/test/Sender.test.js b/test/Sender.test.js deleted file mode 100644 index 8b5ccc06b..000000000 --- a/test/Sender.test.js +++ /dev/null @@ -1,87 +0,0 @@ -var Sender = require('../lib/Sender') - , PerMessageDeflate = require('../lib/PerMessageDeflate'); -require('should'); - -describe('Sender', function() { - describe('#ctor', function() { - it('throws TypeError when called without new', function(done) { - try { - var sender = Sender({ write: function() {} }); - } - catch (e) { - e.should.be.instanceof(TypeError); - done(); - } - }); - }); - - describe('#frameAndSend', function() { - it('does not modify a masked binary buffer', function() { - var sender = new Sender({ write: function() {} }); - var buf = new Buffer([1, 2, 3, 4, 5]); - sender.frameAndSend(2, buf, true, true); - buf[0].should.eql(1); - buf[1].should.eql(2); - buf[2].should.eql(3); - buf[3].should.eql(4); - buf[4].should.eql(5); - }); - - it('does not modify a masked text buffer', function() { - var sender = new Sender({ write: function() {} }); - var text = 'hi there'; - sender.frameAndSend(1, text, true, true); - text.should.eql('hi there'); - }); - - it('sets rsv1 flag if compressed', function(done) { - var sender = new Sender({ - write: function(data) { - (data[0] & 0x40).should.equal(0x40); - done(); - } - }); - sender.frameAndSend(1, 'hi', true, false, true); - }); - }); - - describe('#send', function() { - it('compresses data if compress option is enabled', function(done) { - var perMessageDeflate = new PerMessageDeflate(); - perMessageDeflate.accept([{}]); - - var sender = new Sender({ - write: function(data) { - (data[0] & 0x40).should.equal(0x40); - done(); - } - }, { - 'permessage-deflate': perMessageDeflate - }); - sender.send('hi', { compress: true }); - }); - }); - - describe('#close', function() { - it('should consume all data before closing', function(done) { - var perMessageDeflate = new PerMessageDeflate(); - perMessageDeflate.accept([{}]); - - var count = 0; - var sender = new Sender({ - write: function(data) { - count++; - } - }, { - 'permessage-deflate': perMessageDeflate - }); - sender.send('foo', {compress: true}); - sender.send('bar', {compress: true}); - sender.send('baz', {compress: true}); - sender.close(1000, null, false, function(err) { - count.should.be.equal(4); - done(err); - }); - }); - }); -}); diff --git a/test/Validation.test.js b/test/Validation.test.js deleted file mode 100644 index 37c339935..000000000 --- a/test/Validation.test.js +++ /dev/null @@ -1,23 +0,0 @@ -var Validation = require('../lib/Validation').Validation; -require('should'); - -describe('Validation', function() { - describe('isValidUTF8', function() { - it('should return true for a valid utf8 string', function() { - var validBuffer = new Buffer('Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque gravida mattis rhoncus. Donec iaculis, metus quis varius accumsan, erat mauris condimentum diam, et egestas erat enim ut ligula. Praesent sollicitudin tellus eget dolor euismod euismod. Nullam ac augue nec neque varius luctus. Curabitur elit mi, consequat ultricies adipiscing mollis, scelerisque in erat. Phasellus facilisis fermentum ullamcorper. Nulla et sem eu arcu pharetra pellentesque. Praesent consectetur tempor justo, vel iaculis dui ullamcorper sit amet. Integer tristique viverra ullamcorper. Vivamus laoreet, nulla eget suscipit eleifend, lacus lectus feugiat libero, non fermentum erat nisi at risus. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut pulvinar dignissim tellus, eu dignissim lorem vulputate quis. Morbi ut pulvinar augue.'); - Validation.isValidUTF8(validBuffer).should.be.ok; - }); - it('should return false for an erroneous string', function() { - var invalidBuffer = new Buffer([0xce, 0xba, 0xe1, 0xbd, 0xb9, 0xcf, 0x83, 0xce, 0xbc, 0xce, 0xb5, 0xed, 0xa0, 0x80, 0x65, 0x64, 0x69, 0x74, 0x65, 0x64]); - Validation.isValidUTF8(invalidBuffer).should.not.be.ok; - }); - it('should return true for valid cases from the autobahn test suite', function() { - Validation.isValidUTF8(new Buffer('\xf0\x90\x80\x80')).should.be.ok; - Validation.isValidUTF8(new Buffer([0xf0, 0x90, 0x80, 0x80])).should.be.ok; - }); - it('should return false for erroneous autobahn strings', function() { - Validation.isValidUTF8(new Buffer([0xce, 0xba, 0xe1, 0xbd])).should.not.be.ok; - }); - }); -}); - diff --git a/test/WebSocket.integration.js b/test/WebSocket.integration.js deleted file mode 100644 index 5d4f426f4..000000000 --- a/test/WebSocket.integration.js +++ /dev/null @@ -1,44 +0,0 @@ -var assert = require('assert') - , WebSocket = require('../') - , server = require('./testserver'); - -var port = 20000; - -function getArrayBuffer(buf) { - var l = buf.length; - var arrayBuf = new ArrayBuffer(l); - var uint8View = new Uint8Array(arrayBuf); - - for (var i = 0; i < l; i++) { - uint8View[i] = buf[i]; - } - return uint8View.buffer; -} - -function areArraysEqual(x, y) { - if (x.length != y.length) return false; - for (var i = 0, l = x.length; i < l; ++i) { - if (x[i] !== y[i]) return false; - } - return true; -} - -describe('WebSocket', function() { - it('communicates successfully with echo service', function(done) { - var ws = new WebSocket('ws://echo.websocket.org/', {protocolVersion: 13, origin: 'http://websocket.org'}); - var str = Date.now().toString(); - var dataReceived = false; - ws.on('open', function() { - ws.send(str, {mask: true}); - }); - ws.on('close', function() { - assert.equal(true, dataReceived); - done(); - }); - ws.on('message', function(data, flags) { - assert.equal(str, data); - ws.terminate(); - dataReceived = true; - }); - }); -}); diff --git a/test/WebSocket.test.js b/test/WebSocket.test.js deleted file mode 100644 index 9a696411f..000000000 --- a/test/WebSocket.test.js +++ /dev/null @@ -1,2267 +0,0 @@ -var assert = require('assert') - , https = require('https') - , http = require('http') - , should = require('should') - , WebSocket = require('../') - , WebSocketServer = require('../').Server - , fs = require('fs') - , os = require('os') - , server = require('./testserver') - , crypto = require('crypto'); - -var port = 20000; - -function getArrayBuffer(buf) { - var l = buf.length; - var arrayBuf = new ArrayBuffer(l); - var uint8View = new Uint8Array(arrayBuf); - for (var i = 0; i < l; i++) { - uint8View[i] = buf[i]; - } - return uint8View.buffer; -} - - -function areArraysEqual(x, y) { - if (x.length != y.length) return false; - for (var i = 0, l = x.length; i < l; ++i) { - if (x[i] !== y[i]) return false; - } - return true; -} - -describe('WebSocket', function() { - describe('#ctor', function() { - it('throws exception for invalid url', function(done) { - try { - var ws = new WebSocket('echo.websocket.org'); - } - catch (e) { - done(); - } - }); - - it('should return a new instance if called without new', function(done) { - var ws = WebSocket('ws://localhost:' + port); - ws.should.be.an.instanceOf(WebSocket); - done(); - }); - - it('should emit an error object when the receiver throws an error string', function(done) { - - var wss = new WebSocketServer({port: ++port}, function() { - - var ws = new WebSocket('ws://localhost:' + port); - - ws.on('open', function () { - ws._receiver.error('This is an error string', 1002); - }); - - ws.on('error', function (error) { - error.should.be.an.instanceof(Error); - done(); - }); - }); - }); - - it('should emit an error object when the receiver throws an error object', function(done) { - - var wss = new WebSocketServer({port: ++port}, function() { - - var ws = new WebSocket('ws://localhost:' + port); - - ws.on('open', function () { - ws._receiver.error(new Error('This is an error object'), 1002); - }); - - ws.on('error', function (error) { - error.should.be.an.instanceof(Error); - done(); - }); - }); - }); - }); - - describe('options', function() { - it('should accept an `agent` option', function(done) { - var wss = new WebSocketServer({port: ++port}, function() { - var agent = { - addRequest: function() { - wss.close(); - done(); - } - }; - var ws = new WebSocket('ws://localhost:' + port, { agent: agent }); - }); - }); - // GH-227 - it('should accept the `options` object as the 3rd argument', function(done) { - var wss = new WebSocketServer({port: ++port}, function() { - var agent = { - addRequest: function() { - wss.close(); - done(); - } - }; - var ws = new WebSocket('ws://localhost:' + port, [], { agent: agent }); - }); - }); - - it('should accept the localAddress option', function(done) { - // explore existing interfaces - var devs = os.networkInterfaces() - , localAddresses = [] - , j, ifc, dev, devname; - for ( devname in devs ) { - dev = devs[devname]; - for ( j=0;j 0) break; - ws.send((new Array(10000)).join('hello')); - } - ws.terminate(); - ws.on('close', function() { - wss.close(); - done(); - }); - }); - }); - }); - - describe('Custom headers', function() { - it('request has an authorization header', function (done) { - var auth = 'test:testpass'; - var srv = http.createServer(function (req, res) {}); - var wss = new WebSocketServer({server: srv}); - srv.listen(++port); - var ws = new WebSocket('ws://' + auth + '@localhost:' + port); - srv.on('upgrade', function (req, socket, head) { - assert(req.headers.authorization, 'auth header exists'); - assert.equal(req.headers.authorization, 'Basic ' + new Buffer(auth).toString('base64')); - ws.terminate(); - ws.on('close', function () { - srv.close(); - wss.close(); - done(); - }); - }); - }); - - it('accepts custom headers', function (done) { - var srv = http.createServer(function (req, res) {}); - var wss = new WebSocketServer({server: srv}); - srv.listen(++port); - - var ws = new WebSocket('ws://localhost:' + port, { - headers: { - 'Cookie': 'foo=bar' - } - }); - - srv.on('upgrade', function (req, socket, head) { - assert(req.headers.cookie, 'auth header exists'); - assert.equal(req.headers.cookie, 'foo=bar'); - - ws.terminate(); - ws.on('close', function () { - srv.close(); - wss.close(); - done(); - }); - }); - }); - }); - - describe('#readyState', function() { - it('defaults to connecting', function(done) { - server.createServer(++port, function(srv) { - var ws = new WebSocket('ws://localhost:' + port); - assert.equal(WebSocket.CONNECTING, ws.readyState); - ws.terminate(); - ws.on('close', function() { - srv.close(); - done(); - }); - }); - }); - - it('set to open once connection is established', function(done) { - server.createServer(++port, function(srv) { - var ws = new WebSocket('ws://localhost:' + port); - ws.on('open', function() { - assert.equal(WebSocket.OPEN, ws.readyState); - srv.close(); - done(); - }); - }); - }); - - it('set to closed once connection is closed', function(done) { - server.createServer(++port, function(srv) { - var ws = new WebSocket('ws://localhost:' + port); - ws.close(1001); - ws.on('close', function() { - assert.equal(WebSocket.CLOSED, ws.readyState); - srv.close(); - done(); - }); - }); - }); - - it('set to closed once connection is terminated', function(done) { - server.createServer(++port, function(srv) { - var ws = new WebSocket('ws://localhost:' + port); - ws.terminate(); - ws.on('close', function() { - assert.equal(WebSocket.CLOSED, ws.readyState); - srv.close(); - done(); - }); - }); - }); - }); - - /* - * Ready state constants - */ - - var readyStates = { - CONNECTING: 0, - OPEN: 1, - CLOSING: 2, - CLOSED: 3 - }; - - /* - * Ready state constant tests - */ - - Object.keys(readyStates).forEach(function(state) { - describe('.' + state, function() { - it('is enumerable property of class', function() { - var propertyDescripter = Object.getOwnPropertyDescriptor(WebSocket, state) - assert.equal(readyStates[state], propertyDescripter.value); - assert.equal(true, propertyDescripter.enumerable); - }); - }); - }); - - server.createServer(++port, function(srv) { - var ws = new WebSocket('ws://localhost:' + port); - Object.keys(readyStates).forEach(function(state) { - describe('.' + state, function() { - it('is property of instance', function() { - assert.equal(readyStates[state], ws[state]); - }); - }); - }); - }); - }); - - describe('events', function() { - it('emits a ping event', function(done) { - var wss = new WebSocketServer({port: ++port}); - wss.on('connection', function(client) { - client.ping(); - }); - var ws = new WebSocket('ws://localhost:' + port); - ws.on('ping', function() { - ws.terminate(); - wss.close(); - done(); - }); - }); - - it('emits a pong event', function(done) { - var wss = new WebSocketServer({port: ++port}); - wss.on('connection', function(client) { - client.pong(); - }); - var ws = new WebSocket('ws://localhost:' + port); - ws.on('pong', function() { - ws.terminate(); - wss.close(); - done(); - }); - }); - }); - - describe('connection establishing', function() { - it('can disconnect before connection is established', function(done) { - server.createServer(++port, function(srv) { - var ws = new WebSocket('ws://localhost:' + port); - ws.terminate(); - ws.on('open', function() { - assert.fail('connect shouldnt be raised here'); - }); - ws.on('close', function() { - srv.close(); - done(); - }); - }); - }); - - it('can close before connection is established', function(done) { - server.createServer(++port, function(srv) { - var ws = new WebSocket('ws://localhost:' + port); - ws.close(1001); - ws.on('open', function() { - assert.fail('connect shouldnt be raised here'); - }); - ws.on('close', function() { - srv.close(); - done(); - }); - }); - }); - - it('can handle error before request is upgraded', function(done) { - // Here, we don't create a server, to guarantee that the connection will - // fail before the request is upgraded - ++port; - var ws = new WebSocket('ws://localhost:' + port); - ws.on('open', function() { - assert.fail('connect shouldnt be raised here'); - }); - var errorCallBackFired = false; - ws.on('error', function() { - errorCallBackFired = true; - }); - ws.on('close', function() { - setTimeout(function() { - assert.equal(true, errorCallBackFired); - assert.equal(ws.readyState, WebSocket.CLOSED); - done(); - }, 50) - }); - }); - - it('invalid server key is denied', function(done) { - server.createServer(++port, server.handlers.invalidKey, function(srv) { - var ws = new WebSocket('ws://localhost:' + port); - ws.on('error', function() { - srv.close(); - done(); - }); - }); - }); - - it('close event is raised when server closes connection', function(done) { - server.createServer(++port, server.handlers.closeAfterConnect, function(srv) { - var ws = new WebSocket('ws://localhost:' + port); - ws.on('close', function() { - srv.close(); - done(); - }); - }); - }); - - it('error is emitted if server aborts connection', function(done) { - server.createServer(++port, server.handlers.return401, function(srv) { - var ws = new WebSocket('ws://localhost:' + port); - ws.on('open', function() { - assert.fail('connect shouldnt be raised here'); - }); - ws.on('error', function() { - srv.close(); - done(); - }); - }); - }); - - it('unexpected response can be read when sent by server', function(done) { - server.createServer(++port, server.handlers.return401, function(srv) { - var ws = new WebSocket('ws://localhost:' + port); - ws.on('open', function() { - assert.fail('connect shouldnt be raised here'); - }); - ws.on('unexpected-response', function(req, res) { - assert.equal(res.statusCode, 401); - - var data = ''; - - res.on('data', function (v) { - data += v; - }); - - res.on('end', function () { - assert.equal(data, 'Not allowed!'); - srv.close(); - done(); - }); - }); - ws.on('error', function () { - assert.fail('error shouldnt be raised here'); - }); - }); - }); - - it('request can be aborted when unexpected response is sent by server', function(done) { - server.createServer(++port, server.handlers.return401, function(srv) { - var ws = new WebSocket('ws://localhost:' + port); - ws.on('open', function() { - assert.fail('connect shouldnt be raised here'); - }); - ws.on('unexpected-response', function(req, res) { - assert.equal(res.statusCode, 401); - - res.on('end', function () { - srv.close(); - done(); - }); - - req.abort(); - }); - ws.on('error', function () { - assert.fail('error shouldnt be raised here'); - }); - }); - }); - }); - - describe('#pause and #resume', function() { - it('pauses the underlying stream', function(done) { - // this test is sort-of racecondition'y, since an unlikely slow connection - // to localhost can cause the test to succeed even when the stream pausing - // isn't working as intended. that is an extremely unlikely scenario, though - // and an acceptable risk for the test. - var client; - var serverClient; - var openCount = 0; - function onOpen() { - if (++openCount == 2) { - var paused = true; - serverClient.on('message', function() { - paused.should.not.be.ok; - wss.close(); - done(); - }); - serverClient.pause(); - setTimeout(function() { - paused = false; - serverClient.resume(); - }, 200); - client.send('foo'); - } - } - var wss = new WebSocketServer({port: ++port}, function() { - var ws = new WebSocket('ws://localhost:' + port); - serverClient = ws; - serverClient.on('open', onOpen); - }); - wss.on('connection', function(ws) { - client = ws; - onOpen(); - }); - }); - }); - - describe('#ping', function() { - it('before connect should fail', function(done) { - server.createServer(++port, function(srv) { - var ws = new WebSocket('ws://localhost:' + port); - ws.on('error', function() {}); - try { - ws.ping(); - } - catch (e) { - srv.close(); - ws.terminate(); - done(); - } - }); - }); - - it('before connect can silently fail', function(done) { - server.createServer(++port, function(srv) { - var ws = new WebSocket('ws://localhost:' + port); - ws.on('error', function() {}); - ws.ping('', {}, true); - srv.close(); - ws.terminate(); - done(); - }); - }); - - it('without message is successfully transmitted to the server', function(done) { - server.createServer(++port, function(srv) { - var ws = new WebSocket('ws://localhost:' + port); - ws.on('open', function() { - ws.ping(); - }); - srv.on('ping', function(message) { - srv.close(); - ws.terminate(); - done(); - }); - }); - }); - - it('with message is successfully transmitted to the server', function(done) { - server.createServer(++port, function(srv) { - var ws = new WebSocket('ws://localhost:' + port); - ws.on('open', function() { - ws.ping('hi'); - }); - srv.on('ping', function(message) { - assert.equal('hi', message); - srv.close(); - ws.terminate(); - done(); - }); - }); - }); - - it('can send safely receive numbers as ping payload', function(done) { - server.createServer(++port, function(srv) { - var ws = new WebSocket('ws://localhost:' + port); - - ws.on('open', function() { - ws.ping(200); - }); - - srv.on('ping', function(message) { - assert.equal('200', message); - srv.close(); - ws.terminate(); - done(); - }); - }); - }); - - it('with encoded message is successfully transmitted to the server', function(done) { - server.createServer(++port, function(srv) { - var ws = new WebSocket('ws://localhost:' + port); - ws.on('open', function() { - ws.ping('hi', {mask: true}); - }); - srv.on('ping', function(message, flags) { - assert.ok(flags.masked); - assert.equal('hi', message); - srv.close(); - ws.terminate(); - done(); - }); - }); - }); - }); - - describe('#pong', function() { - it('before connect should fail', function(done) { - server.createServer(++port, function(srv) { - var ws = new WebSocket('ws://localhost:' + port); - ws.on('error', function() {}); - try { - ws.pong(); - } - catch (e) { - srv.close(); - ws.terminate(); - done(); - } - }); - }); - - it('before connect can silently fail', function(done) { - server.createServer(++port, function(srv) { - var ws = new WebSocket('ws://localhost:' + port); - ws.on('error', function() {}); - ws.pong('', {}, true); - srv.close(); - ws.terminate(); - done(); - }); - }); - - it('without message is successfully transmitted to the server', function(done) { - server.createServer(++port, function(srv) { - var ws = new WebSocket('ws://localhost:' + port); - ws.on('open', function() { - ws.pong(); - }); - srv.on('pong', function(message) { - srv.close(); - ws.terminate(); - done(); - }); - }); - }); - - it('with message is successfully transmitted to the server', function(done) { - server.createServer(++port, function(srv) { - var ws = new WebSocket('ws://localhost:' + port); - ws.on('open', function() { - ws.pong('hi'); - }); - srv.on('pong', function(message) { - assert.equal('hi', message); - srv.close(); - ws.terminate(); - done(); - }); - }); - }); - - it('with encoded message is successfully transmitted to the server', function(done) { - server.createServer(++port, function(srv) { - var ws = new WebSocket('ws://localhost:' + port); - ws.on('open', function() { - ws.pong('hi', {mask: true}); - }); - srv.on('pong', function(message, flags) { - assert.ok(flags.masked); - assert.equal('hi', message); - srv.close(); - ws.terminate(); - done(); - }); - }); - }); - }); - - describe('#send', function() { - it('very long binary data can be sent and received (with echoing server)', function(done) { - server.createServer(++port, function(srv) { - var ws = new WebSocket('ws://localhost:' + port); - var array = new Float32Array(5 * 1024 * 1024); - for (var i = 0; i < array.length; ++i) array[i] = i / 5; - ws.on('open', function() { - ws.send(array, {binary: true}); - }); - ws.on('message', function(message, flags) { - assert.ok(flags.binary); - assert.ok(areArraysEqual(array, new Float32Array(getArrayBuffer(message)))); - ws.terminate(); - srv.close(); - done(); - }); - }); - }); - - it('can send and receive text data', function(done) { - server.createServer(++port, function(srv) { - var ws = new WebSocket('ws://localhost:' + port); - ws.on('open', function() { - ws.send('hi'); - }); - ws.on('message', function(message, flags) { - assert.equal('hi', message); - ws.terminate(); - srv.close(); - done(); - }); - }); - }); - - it('send and receive binary data as an array', function(done) { - server.createServer(++port, function(srv) { - var ws = new WebSocket('ws://localhost:' + port); - var array = new Float32Array(6); - for (var i = 0; i < array.length; ++i) array[i] = i / 2; - var partial = array.subarray(2, 5); - ws.on('open', function() { - ws.send(partial, {binary: true}); - }); - ws.on('message', function(message, flags) { - assert.ok(flags.binary); - assert.ok(areArraysEqual(partial, new Float32Array(getArrayBuffer(message)))); - ws.terminate(); - srv.close(); - done(); - }); - }); - }); - - it('binary data can be sent and received as buffer', function(done) { - server.createServer(++port, function(srv) { - var ws = new WebSocket('ws://localhost:' + port); - var buf = new Buffer('foobar'); - ws.on('open', function() { - ws.send(buf, {binary: true}); - }); - ws.on('message', function(message, flags) { - assert.ok(flags.binary); - assert.ok(areArraysEqual(buf, message)); - ws.terminate(); - srv.close(); - done(); - }); - }); - }); - - it('ArrayBuffer is auto-detected without binary flag', function(done) { - server.createServer(++port, function(srv) { - var ws = new WebSocket('ws://localhost:' + port); - var array = new Float32Array(5); - for (var i = 0; i < array.length; ++i) array[i] = i / 2; - ws.on('open', function() { - ws.send(array.buffer); - }); - ws.onmessage = function (event) { - assert.ok(event.binary); - assert.ok(areArraysEqual(array, new Float32Array(getArrayBuffer(event.data)))); - ws.terminate(); - srv.close(); - done(); - }; - }); - }); - - it('Buffer is auto-detected without binary flag', function(done) { - server.createServer(++port, function(srv) { - var ws = new WebSocket('ws://localhost:' + port); - var buf = new Buffer('foobar'); - ws.on('open', function() { - ws.send(buf); - }); - ws.onmessage = function (event) { - assert.ok(event.binary); - assert.ok(areArraysEqual(event.data, buf)); - ws.terminate(); - srv.close(); - done(); - }; - }); - }); - - it('before connect should fail', function(done) { - server.createServer(++port, function(srv) { - var ws = new WebSocket('ws://localhost:' + port); - ws.on('error', function() {}); - try { - ws.send('hi'); - } - catch (e) { - ws.terminate(); - srv.close(); - done(); - } - }); - }); - - it('before connect should pass error through callback, if present', function(done) { - server.createServer(++port, function(srv) { - var ws = new WebSocket('ws://localhost:' + port); - ws.on('error', function() {}); - ws.send('hi', function(error) { - assert.ok(error instanceof Error); - ws.terminate(); - srv.close(); - done(); - }); - }); - }); - - it('without data should be successful', function(done) { - server.createServer(++port, function(srv) { - var ws = new WebSocket('ws://localhost:' + port); - ws.on('open', function() { - ws.send(); - }); - srv.on('message', function(message, flags) { - assert.equal('', message); - srv.close(); - ws.terminate(); - done(); - }); - }); - }); - - it('calls optional callback when flushed', function(done) { - server.createServer(++port, function(srv) { - var ws = new WebSocket('ws://localhost:' + port); - ws.on('open', function() { - ws.send('hi', function() { - srv.close(); - ws.terminate(); - done(); - }); - }); - }); - }); - - it('with unencoded message is successfully transmitted to the server', function(done) { - server.createServer(++port, function(srv) { - var ws = new WebSocket('ws://localhost:' + port); - ws.on('open', function() { - ws.send('hi'); - }); - srv.on('message', function(message, flags) { - assert.equal('hi', message); - srv.close(); - ws.terminate(); - done(); - }); - }); - }); - - it('with encoded message is successfully transmitted to the server', function(done) { - server.createServer(++port, function(srv) { - var ws = new WebSocket('ws://localhost:' + port); - ws.on('open', function() { - ws.send('hi', {mask: true}); - }); - srv.on('message', function(message, flags) { - assert.ok(flags.masked); - assert.equal('hi', message); - srv.close(); - ws.terminate(); - done(); - }); - }); - }); - - it('with unencoded binary message is successfully transmitted to the server', function(done) { - server.createServer(++port, function(srv) { - var ws = new WebSocket('ws://localhost:' + port); - var array = new Float32Array(5); - for (var i = 0; i < array.length; ++i) array[i] = i / 2; - ws.on('open', function() { - ws.send(array, {binary: true}); - }); - srv.on('message', function(message, flags) { - assert.ok(flags.binary); - assert.ok(areArraysEqual(array, new Float32Array(getArrayBuffer(message)))); - srv.close(); - ws.terminate(); - done(); - }); - }); - }); - - it('with encoded binary message is successfully transmitted to the server', function(done) { - server.createServer(++port, function(srv) { - var ws = new WebSocket('ws://localhost:' + port); - var array = new Float32Array(5); - for (var i = 0; i < array.length; ++i) array[i] = i / 2; - ws.on('open', function() { - ws.send(array, {mask: true, binary: true}); - }); - srv.on('message', function(message, flags) { - assert.ok(flags.binary); - assert.ok(flags.masked); - assert.ok(areArraysEqual(array, new Float32Array(getArrayBuffer(message)))); - srv.close(); - ws.terminate(); - done(); - }); - }); - }); - - it('with binary stream will send fragmented data', function(done) { - server.createServer(++port, function(srv) { - var ws = new WebSocket('ws://localhost:' + port); - var callbackFired = false; - ws.on('open', function() { - var fileStream = fs.createReadStream('test/fixtures/textfile'); - fileStream.bufferSize = 100; - ws.send(fileStream, {binary: true}, function(error) { - assert.equal(null, error); - callbackFired = true; - }); - }); - srv.on('message', function(data, flags) { - assert.ok(flags.binary); - assert.ok(areArraysEqual(fs.readFileSync('test/fixtures/textfile'), data)); - ws.terminate(); - }); - ws.on('close', function() { - assert.ok(callbackFired); - srv.close(); - done(); - }); - }); - }); - - it('with text stream will send fragmented data', function(done) { - server.createServer(++port, function(srv) { - var ws = new WebSocket('ws://localhost:' + port); - var callbackFired = false; - ws.on('open', function() { - var fileStream = fs.createReadStream('test/fixtures/textfile'); - fileStream.setEncoding('utf8'); - fileStream.bufferSize = 100; - ws.send(fileStream, {binary: false}, function(error) { - assert.equal(null, error); - callbackFired = true; - }); - }); - srv.on('message', function(data, flags) { - assert.ok(!flags.binary); - assert.ok(areArraysEqual(fs.readFileSync('test/fixtures/textfile', 'utf8'), data)); - ws.terminate(); - }); - ws.on('close', function() { - assert.ok(callbackFired); - srv.close(); - done(); - }); - }); - }); - - it('will cause intermittent send to be delayed in order', function(done) { - server.createServer(++port, function(srv) { - var ws = new WebSocket('ws://localhost:' + port); - ws.on('open', function() { - var fileStream = fs.createReadStream('test/fixtures/textfile'); - fileStream.setEncoding('utf8'); - fileStream.bufferSize = 100; - ws.send(fileStream); - ws.send('foobar'); - ws.send('baz'); - }); - var receivedIndex = 0; - srv.on('message', function(data, flags) { - ++receivedIndex; - if (receivedIndex == 1) { - assert.ok(!flags.binary); - assert.ok(areArraysEqual(fs.readFileSync('test/fixtures/textfile', 'utf8'), data)); - } - else if (receivedIndex == 2) { - assert.ok(!flags.binary); - assert.equal('foobar', data); - } - else { - assert.ok(!flags.binary); - assert.equal('baz', data); - srv.close(); - ws.terminate(); - done(); - } - }); - }); - }); - - it('will cause intermittent stream to be delayed in order', function(done) { - server.createServer(++port, function(srv) { - var ws = new WebSocket('ws://localhost:' + port); - ws.on('open', function() { - var fileStream = fs.createReadStream('test/fixtures/textfile'); - fileStream.setEncoding('utf8'); - fileStream.bufferSize = 100; - ws.send(fileStream); - var i = 0; - ws.stream(function(error, send) { - assert.ok(!error); - if (++i == 1) send('foo'); - else send('bar', true); - }); - }); - var receivedIndex = 0; - srv.on('message', function(data, flags) { - ++receivedIndex; - if (receivedIndex == 1) { - assert.ok(!flags.binary); - assert.ok(areArraysEqual(fs.readFileSync('test/fixtures/textfile', 'utf8'), data)); - } - else if (receivedIndex == 2) { - assert.ok(!flags.binary); - assert.equal('foobar', data); - srv.close(); - ws.terminate(); - done(); - } - }); - }); - }); - - it('will cause intermittent ping to be delivered', function(done) { - server.createServer(++port, function(srv) { - var ws = new WebSocket('ws://localhost:' + port); - ws.on('open', function() { - var fileStream = fs.createReadStream('test/fixtures/textfile'); - fileStream.setEncoding('utf8'); - fileStream.bufferSize = 100; - ws.send(fileStream); - ws.ping('foobar'); - }); - var receivedIndex = 0; - srv.on('message', function(data, flags) { - assert.ok(!flags.binary); - assert.ok(areArraysEqual(fs.readFileSync('test/fixtures/textfile', 'utf8'), data)); - if (++receivedIndex == 2) { - srv.close(); - ws.terminate(); - done(); - } - }); - srv.on('ping', function(data) { - assert.equal('foobar', data); - if (++receivedIndex == 2) { - srv.close(); - ws.terminate(); - done(); - } - }); - }); - }); - - it('will cause intermittent pong to be delivered', function(done) { - server.createServer(++port, function(srv) { - var ws = new WebSocket('ws://localhost:' + port); - ws.on('open', function() { - var fileStream = fs.createReadStream('test/fixtures/textfile'); - fileStream.setEncoding('utf8'); - fileStream.bufferSize = 100; - ws.send(fileStream); - ws.pong('foobar'); - }); - var receivedIndex = 0; - srv.on('message', function(data, flags) { - assert.ok(!flags.binary); - assert.ok(areArraysEqual(fs.readFileSync('test/fixtures/textfile', 'utf8'), data)); - if (++receivedIndex == 2) { - srv.close(); - ws.terminate(); - done(); - } - }); - srv.on('pong', function(data) { - assert.equal('foobar', data); - if (++receivedIndex == 2) { - srv.close(); - ws.terminate(); - done(); - } - }); - }); - }); - - it('will cause intermittent close to be delivered', function(done) { - server.createServer(++port, function(srv) { - var ws = new WebSocket('ws://localhost:' + port); - ws.on('open', function() { - var fileStream = fs.createReadStream('test/fixtures/textfile'); - fileStream.setEncoding('utf8'); - fileStream.bufferSize = 100; - ws.send(fileStream); - ws.close(1000, 'foobar'); - }); - ws.on('close', function() { - srv.close(); - ws.terminate(); - done(); - }); - ws.on('error', function() { /* That's quite alright -- a send was attempted after close */ }); - srv.on('message', function(data, flags) { - assert.ok(!flags.binary); - assert.ok(areArraysEqual(fs.readFileSync('test/fixtures/textfile', 'utf8'), data)); - }); - srv.on('close', function(code, data) { - assert.equal(1000, code); - assert.equal('foobar', data); - }); - }); - }); - }); - - describe('#stream', function() { - it('very long binary data can be streamed', function(done) { - server.createServer(++port, function(srv) { - var ws = new WebSocket('ws://localhost:' + port); - var buffer = new Buffer(10 * 1024); - for (var i = 0; i < buffer.length; ++i) buffer[i] = i % 0xff; - ws.on('open', function() { - var i = 0; - var blockSize = 800; - var bufLen = buffer.length; - ws.stream({binary: true}, function(error, send) { - assert.ok(!error); - var start = i * blockSize; - var toSend = Math.min(blockSize, bufLen - (i * blockSize)); - var end = start + toSend; - var isFinal = toSend < blockSize; - send(buffer.slice(start, end), isFinal); - i += 1; - }); - }); - srv.on('message', function(data, flags) { - assert.ok(flags.binary); - assert.ok(areArraysEqual(buffer, data)); - ws.terminate(); - srv.close(); - done(); - }); - }); - }); - - it('before connect should pass error through callback', function(done) { - server.createServer(++port, function(srv) { - var ws = new WebSocket('ws://localhost:' + port); - ws.on('error', function() {}); - ws.stream(function(error) { - assert.ok(error instanceof Error); - ws.terminate(); - srv.close(); - done(); - }); - }); - }); - - it('without callback should fail', function(done) { - server.createServer(++port, function(srv) { - var ws = new WebSocket('ws://localhost:' + port); - var payload = 'HelloWorld'; - ws.on('open', function() { - try { - ws.stream(); - } - catch (e) { - srv.close(); - ws.terminate(); - done(); - } - }); - }); - }); - - it('will cause intermittent send to be delayed in order', function(done) { - server.createServer(++port, function(srv) { - var ws = new WebSocket('ws://localhost:' + port); - var payload = 'HelloWorld'; - ws.on('open', function() { - var i = 0; - ws.stream(function(error, send) { - assert.ok(!error); - if (++i == 1) { - send(payload.substr(0, 5)); - ws.send('foobar'); - ws.send('baz'); - } - else { - send(payload.substr(5, 5), true); - } - }); - }); - var receivedIndex = 0; - srv.on('message', function(data, flags) { - ++receivedIndex; - if (receivedIndex == 1) { - assert.ok(!flags.binary); - assert.equal(payload, data); - } - else if (receivedIndex == 2) { - assert.ok(!flags.binary); - assert.equal('foobar', data); - } - else { - assert.ok(!flags.binary); - assert.equal('baz', data); - srv.close(); - ws.terminate(); - done(); - } - }); - }); - }); - - it('will cause intermittent stream to be delayed in order', function(done) { - server.createServer(++port, function(srv) { - var ws = new WebSocket('ws://localhost:' + port); - var payload = 'HelloWorld'; - ws.on('open', function() { - var i = 0; - ws.stream(function(error, send) { - assert.ok(!error); - if (++i == 1) { - send(payload.substr(0, 5)); - var i2 = 0; - ws.stream(function(error, send) { - assert.ok(!error); - if (++i2 == 1) send('foo'); - else send('bar', true); - }); - ws.send('baz'); - } - else send(payload.substr(5, 5), true); - }); - }); - var receivedIndex = 0; - srv.on('message', function(data, flags) { - ++receivedIndex; - if (receivedIndex == 1) { - assert.ok(!flags.binary); - assert.equal(payload, data); - } - else if (receivedIndex == 2) { - assert.ok(!flags.binary); - assert.equal('foobar', data); - } - else if (receivedIndex == 3){ - assert.ok(!flags.binary); - assert.equal('baz', data); - setTimeout(function() { - srv.close(); - ws.terminate(); - done(); - }, 1000); - } - else throw new Error('more messages than we actually sent just arrived'); - }); - }); - }); - - it('will cause intermittent ping to be delivered', function(done) { - server.createServer(++port, function(srv) { - var ws = new WebSocket('ws://localhost:' + port); - var payload = 'HelloWorld'; - ws.on('open', function() { - var i = 0; - ws.stream(function(error, send) { - assert.ok(!error); - if (++i == 1) { - send(payload.substr(0, 5)); - ws.ping('foobar'); - } - else { - send(payload.substr(5, 5), true); - } - }); - }); - var receivedIndex = 0; - srv.on('message', function(data, flags) { - assert.ok(!flags.binary); - assert.equal(payload, data); - if (++receivedIndex == 2) { - srv.close(); - ws.terminate(); - done(); - } - }); - srv.on('ping', function(data) { - assert.equal('foobar', data); - if (++receivedIndex == 2) { - srv.close(); - ws.terminate(); - done(); - } - }); - }); - }); - - it('will cause intermittent pong to be delivered', function(done) { - server.createServer(++port, function(srv) { - var ws = new WebSocket('ws://localhost:' + port); - var payload = 'HelloWorld'; - ws.on('open', function() { - var i = 0; - ws.stream(function(error, send) { - assert.ok(!error); - if (++i == 1) { - send(payload.substr(0, 5)); - ws.pong('foobar'); - } - else { - send(payload.substr(5, 5), true); - } - }); - }); - var receivedIndex = 0; - srv.on('message', function(data, flags) { - assert.ok(!flags.binary); - assert.equal(payload, data); - if (++receivedIndex == 2) { - srv.close(); - ws.terminate(); - done(); - } - }); - srv.on('pong', function(data) { - assert.equal('foobar', data); - if (++receivedIndex == 2) { - srv.close(); - ws.terminate(); - done(); - } - }); - }); - }); - - it('will cause intermittent close to be delivered', function(done) { - server.createServer(++port, function(srv) { - var ws = new WebSocket('ws://localhost:' + port); - var payload = 'HelloWorld'; - var errorGiven = false; - ws.on('open', function() { - var i = 0; - ws.stream(function(error, send) { - if (++i == 1) { - send(payload.substr(0, 5)); - ws.close(1000, 'foobar'); - } - else if(i == 2) { - send(payload.substr(5, 5), true); - } - else if (i == 3) { - assert.ok(error); - errorGiven = true; - } - }); - }); - ws.on('close', function() { - assert.ok(errorGiven); - srv.close(); - ws.terminate(); - done(); - }); - srv.on('message', function(data, flags) { - assert.ok(!flags.binary); - assert.equal(payload, data); - }); - srv.on('close', function(code, data) { - assert.equal(1000, code); - assert.equal('foobar', data); - }); - }); - }); - }); - - describe('#close', function() { - it('will raise error callback, if any, if called during send stream', function(done) { - server.createServer(++port, function(srv) { - var ws = new WebSocket('ws://localhost:' + port); - var errorGiven = false; - ws.on('open', function() { - var fileStream = fs.createReadStream('test/fixtures/textfile'); - fileStream.setEncoding('utf8'); - fileStream.bufferSize = 100; - ws.send(fileStream, function(error) { - errorGiven = error != null; - }); - ws.close(1000, 'foobar'); - }); - ws.on('close', function() { - setTimeout(function() { - assert.ok(errorGiven); - srv.close(); - ws.terminate(); - done(); - }, 1000); - }); - }); - }); - - it('without invalid first argument throws exception', function(done) { - server.createServer(++port, function(srv) { - var ws = new WebSocket('ws://localhost:' + port); - ws.on('open', function() { - try { - ws.close('error'); - } - catch (e) { - srv.close(); - ws.terminate(); - done(); - } - }); - }); - }); - - it('without reserved error code 1004 throws exception', function(done) { - server.createServer(++port, function(srv) { - var ws = new WebSocket('ws://localhost:' + port); - ws.on('open', function() { - try { - ws.close(1004); - } - catch (e) { - srv.close(); - ws.terminate(); - done(); - } - }); - }); - }); - - it('without message is successfully transmitted to the server', function(done) { - server.createServer(++port, function(srv) { - var ws = new WebSocket('ws://localhost:' + port); - ws.on('open', function() { - ws.close(1000); - }); - srv.on('close', function(code, message, flags) { - assert.equal('', message); - srv.close(); - ws.terminate(); - done(); - }); - }); - }); - - it('with message is successfully transmitted to the server', function(done) { - server.createServer(++port, function(srv) { - var ws = new WebSocket('ws://localhost:' + port); - ws.on('open', function() { - ws.close(1000, 'some reason'); - }); - srv.on('close', function(code, message, flags) { - assert.ok(flags.masked); - assert.equal('some reason', message); - srv.close(); - ws.terminate(); - done(); - }); - }); - }); - - it('with encoded message is successfully transmitted to the server', function(done) { - server.createServer(++port, function(srv) { - var ws = new WebSocket('ws://localhost:' + port); - ws.on('open', function() { - ws.close(1000, 'some reason', {mask: true}); - }); - srv.on('close', function(code, message, flags) { - assert.ok(flags.masked); - assert.equal('some reason', message); - srv.close(); - ws.terminate(); - done(); - }); - }); - }); - - it('ends connection to the server', function(done) { - server.createServer(++port, function(srv) { - var ws = new WebSocket('ws://localhost:' + port); - var connectedOnce = false; - ws.on('open', function() { - connectedOnce = true; - ws.close(1000, 'some reason', {mask: true}); - }); - ws.on('close', function() { - assert.ok(connectedOnce); - srv.close(); - ws.terminate(); - done(); - }); - }); - }); - - it('consumes all data when the server socket closed', function(done) { - var wss = new WebSocketServer({port: ++port}, function() { - wss.on('connection', function(conn) { - conn.send('foo'); - conn.send('bar'); - conn.send('baz'); - conn.close(); - }); - var ws = new WebSocket('ws://localhost:' + port); - var messages = []; - ws.on('message', function (message) { - messages.push(message); - if (messages.length === 3) { - assert.deepEqual(messages, ['foo', 'bar', 'baz']); - wss.close(); - ws.terminate(); - done(); - } - }); - }); - }); - }); - - describe('W3C API emulation', function() { - it('should not throw errors when getting and setting', function(done) { - server.createServer(++port, function(srv) { - var ws = new WebSocket('ws://localhost:' + port); - var listener = function () {}; - - ws.onmessage = listener; - ws.onerror = listener; - ws.onclose = listener; - ws.onopen = listener; - - assert.ok(ws.binaryType === 'nodebuffer'); - ws.binaryType = 'arraybuffer'; - assert.ok(ws.binaryType === 'arraybuffer'); - - assert.ok(ws.onopen === listener); - assert.ok(ws.onmessage === listener); - assert.ok(ws.onclose === listener); - assert.ok(ws.onerror === listener); - - srv.close(); - ws.terminate(); - done(); - }); - }); - - it('should work the same as the EventEmitter api', function(done) { - server.createServer(++port, function(srv) { - var ws = new WebSocket('ws://localhost:' + port); - var listener = function() {}; - var message = 0; - var close = 0; - var open = 0; - - ws.onmessage = function(messageEvent) { - assert.ok(!!messageEvent.data); - ++message; - ws.close(); - }; - - ws.onopen = function() { - ++open; - } - - ws.onclose = function() { - ++close; - } - - ws.on('open', function() { - ws.send('foo'); - }); - - ws.on('close', function() { - process.nextTick(function() { - assert.ok(message === 1); - assert.ok(open === 1); - assert.ok(close === 1); - - srv.close(); - ws.terminate(); - done(); - }); - }); - }); - }); - - it('should receive text data wrapped in a MessageEvent when using addEventListener', function(done) { - server.createServer(++port, function(srv) { - var ws = new WebSocket('ws://localhost:' + port); - ws.addEventListener('open', function() { - ws.send('hi'); - }); - ws.addEventListener('message', function(messageEvent) { - assert.equal('hi', messageEvent.data); - ws.terminate(); - srv.close(); - done(); - }); - }); - }); - - it('should receive valid CloseEvent when server closes with code 1000', function(done) { - var wss = new WebSocketServer({port: ++port}, function() { - var ws = new WebSocket('ws://localhost:' + port); - ws.addEventListener('close', function(closeEvent) { - assert.equal(true, closeEvent.wasClean); - assert.equal(1000, closeEvent.code); - ws.terminate(); - wss.close(); - done(); - }); - }); - wss.on('connection', function(client) { - client.close(1000); - }); - }); - - it('should receive valid CloseEvent when server closes with code 1001', function(done) { - var wss = new WebSocketServer({port: ++port}, function() { - var ws = new WebSocket('ws://localhost:' + port); - ws.addEventListener('close', function(closeEvent) { - assert.equal(false, closeEvent.wasClean); - assert.equal(1001, closeEvent.code); - assert.equal('some daft reason', closeEvent.reason); - ws.terminate(); - wss.close(); - done(); - }); - }); - wss.on('connection', function(client) { - client.close(1001, 'some daft reason'); - }); - }); - - it('should have target set on Events', function(done) { - var wss = new WebSocketServer({port: ++port}, function() { - var ws = new WebSocket('ws://localhost:' + port); - ws.addEventListener('open', function(openEvent) { - assert.equal(ws, openEvent.target); - }); - ws.addEventListener('message', function(messageEvent) { - assert.equal(ws, messageEvent.target); - wss.close(); - }); - ws.addEventListener('close', function(closeEvent) { - assert.equal(ws, closeEvent.target); - ws.emit('error', new Error('forced')); - }); - ws.addEventListener('error', function(errorEvent) { - assert.equal(errorEvent.message, 'forced'); - assert.equal(ws, errorEvent.target); - ws.terminate(); - done(); - }); - }); - wss.on('connection', function(client) { - client.send('hi') - }); - }); - - it('should have type set on Events', function(done) { - var wss = new WebSocketServer({port: ++port}, function() { - var ws = new WebSocket('ws://localhost:' + port); - ws.addEventListener('open', function(openEvent) { - assert.equal('open', openEvent.type); - }); - ws.addEventListener('message', function(messageEvent) { - assert.equal('message', messageEvent.type); - wss.close(); - }); - ws.addEventListener('close', function(closeEvent) { - assert.equal('close', closeEvent.type); - ws.emit('error', new Error('forced')); - }); - ws.addEventListener('error', function(errorEvent) { - assert.equal(errorEvent.message, 'forced'); - assert.equal('error', errorEvent.type); - ws.terminate(); - done(); - }); - }); - wss.on('connection', function(client) { - client.send('hi') - }); - }); - - it('should pass binary data as a node.js Buffer by default', function(done) { - server.createServer(++port, function(srv) { - var ws = new WebSocket('ws://localhost:' + port); - var array = new Uint8Array(4096); - - ws.onopen = function() { - ws.send(array, {binary: true}); - }; - ws.onmessage = function(messageEvent) { - assert.ok(messageEvent.binary); - assert.ok(ws.binaryType === 'nodebuffer'); - assert.ok(messageEvent.data instanceof Buffer); - ws.terminate(); - srv.close(); - done(); - }; - }); - }); - - it('should pass an ArrayBuffer for event.data if binaryType = arraybuffer', function(done) { - server.createServer(++port, function(srv) { - var ws = new WebSocket('ws://localhost:' + port); - ws.binaryType = 'arraybuffer'; - var array = new Uint8Array(4096); - - ws.onopen = function() { - ws.send(array, {binary: true}); - }; - ws.onmessage = function(messageEvent) { - assert.ok(messageEvent.binary); - assert.ok(messageEvent.data instanceof ArrayBuffer); - ws.terminate(); - srv.close(); - done(); - }; - }); - }); - - it('should ignore binaryType for text messages', function(done) { - server.createServer(++port, function(srv) { - var ws = new WebSocket('ws://localhost:' + port); - ws.binaryType = 'arraybuffer'; - - ws.onopen = function() { - ws.send('foobar'); - }; - ws.onmessage = function(messageEvent) { - assert.ok(!messageEvent.binary); - assert.ok(typeof messageEvent.data === 'string'); - ws.terminate(); - srv.close(); - done(); - }; - }); - }); - - }); - - describe('ssl', function() { - it('can connect to secure websocket server', function(done) { - var options = { - key: fs.readFileSync('test/fixtures/key.pem'), - cert: fs.readFileSync('test/fixtures/certificate.pem') - }; - var app = https.createServer(options, function (req, res) { - res.writeHead(200); - res.end(); - }); - var wss = new WebSocketServer({server: app}); - app.listen(++port, function() { - var ws = new WebSocket('wss://localhost:' + port); - }); - wss.on('connection', function(ws) { - app.close(); - ws.terminate(); - wss.close(); - done(); - }); - }); - - it('can connect to secure websocket server with client side certificate', function(done) { - var options = { - key: fs.readFileSync('test/fixtures/key.pem'), - cert: fs.readFileSync('test/fixtures/certificate.pem'), - ca: [fs.readFileSync('test/fixtures/ca1-cert.pem')], - requestCert: true - }; - var clientOptions = { - key: fs.readFileSync('test/fixtures/agent1-key.pem'), - cert: fs.readFileSync('test/fixtures/agent1-cert.pem') - }; - var app = https.createServer(options, function (req, res) { - res.writeHead(200); - res.end(); - }); - var success = false; - var wss = new WebSocketServer({ - server: app, - verifyClient: function(info) { - success = !!info.req.client.authorized; - return true; - } - }); - app.listen(++port, function() { - var ws = new WebSocket('wss://localhost:' + port, clientOptions); - }); - wss.on('connection', function(ws) { - app.close(); - ws.terminate(); - wss.close(); - success.should.be.ok; - done(); - }); - }); - - it('cannot connect to secure websocket server via ws://', function(done) { - var options = { - key: fs.readFileSync('test/fixtures/key.pem'), - cert: fs.readFileSync('test/fixtures/certificate.pem') - }; - var app = https.createServer(options, function (req, res) { - res.writeHead(200); - res.end(); - }); - var wss = new WebSocketServer({server: app}); - app.listen(++port, function() { - var ws = new WebSocket('ws://localhost:' + port, { rejectUnauthorized :false }); - ws.on('error', function() { - app.close(); - ws.terminate(); - wss.close(); - done(); - }); - }); - }); - - it('can send and receive text data', function(done) { - var options = { - key: fs.readFileSync('test/fixtures/key.pem'), - cert: fs.readFileSync('test/fixtures/certificate.pem') - }; - var app = https.createServer(options, function (req, res) { - res.writeHead(200); - res.end(); - }); - var wss = new WebSocketServer({server: app}); - app.listen(++port, function() { - var ws = new WebSocket('wss://localhost:' + port); - ws.on('open', function() { - ws.send('foobar'); - }); - }); - wss.on('connection', function(ws) { - ws.on('message', function(message, flags) { - message.should.eql('foobar'); - app.close(); - ws.terminate(); - wss.close(); - done(); - }); - }); - }); - - it('can send and receive very long binary data', function(done) { - var options = { - key: fs.readFileSync('test/fixtures/key.pem'), - cert: fs.readFileSync('test/fixtures/certificate.pem') - } - var app = https.createServer(options, function (req, res) { - res.writeHead(200); - res.end(); - }); - crypto.randomBytes(5 * 1024 * 1024, function(ex, buf) { - if (ex) throw ex; - var wss = new WebSocketServer({server: app}); - app.listen(++port, function() { - var ws = new WebSocket('wss://localhost:' + port); - ws.on('open', function() { - ws.send(buf, {binary: true}); - }); - ws.on('message', function(message, flags) { - flags.binary.should.be.ok; - areArraysEqual(buf, message).should.be.ok; - app.close(); - ws.terminate(); - wss.close(); - done(); - }); - }); - wss.on('connection', function(ws) { - ws.on('message', function(message, flags) { - ws.send(message, {binary: true}); - }); - }); - }); - }); - }); - - describe('protocol support discovery', function() { - describe('#supports', function() { - describe('#binary', function() { - it('returns true for hybi transport', function(done) { - var wss = new WebSocketServer({port: ++port}, function() { - var ws = new WebSocket('ws://localhost:' + port); - }); - wss.on('connection', function(client) { - assert.equal(true, client.supports.binary); - wss.close(); - done(); - }); - }); - - it('returns false for hixie transport', function(done) { - var wss = new WebSocketServer({port: ++port}, function() { - var options = { - port: port, - host: '127.0.0.1', - headers: { - 'Connection': 'Upgrade', - 'Upgrade': 'WebSocket', - 'Sec-WebSocket-Key1': '3e6b263 4 17 80', - 'Sec-WebSocket-Key2': '17 9 G`ZD9 2 2b 7X 3 /r90' - } - }; - var req = http.request(options); - req.write('WjN}|M(6'); - req.end(); - }); - wss.on('connection', function(client) { - assert.equal(false, client.supports.binary); - wss.close(); - done(); - }); - }); - }); - }); - }); - - describe('host and origin headers', function() { - it('includes the host header with port number', function(done) { - var srv = http.createServer(); - srv.listen(++port, function(){ - srv.on('upgrade', function(req, socket, upgradeHeade) { - assert.equal('localhost:' + port, req.headers['host']); - srv.close(); - done(); - }); - var ws = new WebSocket('ws://localhost:' + port); - }); - }); - - it('lacks default origin header', function(done) { - var srv = http.createServer(); - srv.listen(++port, function() { - srv.on('upgrade', function(req, socket, upgradeHeade) { - req.headers.should.not.have.property('origin'); - srv.close(); - done(); - }); - var ws = new WebSocket('ws://localhost:' + port); - }); - }); - - it('honors origin set in options', function(done) { - var srv = http.createServer(); - srv.listen(++port, function() { - var options = {origin: 'https://example.com:8000'} - srv.on('upgrade', function(req, socket, upgradeHeade) { - assert.equal(options.origin, req.headers['origin']); - srv.close(); - done(); - }); - var ws = new WebSocket('ws://localhost:' + port, options); - }); - }); - - it('excludes default ports from host header', function(done) { - // can't create a server listening on ports 80 or 443 - // so we need to expose the method that does this - var buildHostHeader = WebSocket.buildHostHeader - var host = buildHostHeader(false, 'localhost', 80) - assert.equal('localhost', host); - host = buildHostHeader(false, 'localhost', 88) - assert.equal('localhost:88', host); - host = buildHostHeader(true, 'localhost', 443) - assert.equal('localhost', host); - host = buildHostHeader(true, 'localhost', 8443) - assert.equal('localhost:8443', host); - done() - }); - }); - - describe('permessage-deflate', function() { - it('is enabled by default', function(done) { - var srv = http.createServer(function (req, res) {}); - var wss = new WebSocketServer({server: srv, perMessageDeflate: true}); - srv.listen(++port, function() { - var ws = new WebSocket('ws://localhost:' + port); - srv.on('upgrade', function(req, socket, head) { - assert.ok(~req.headers['sec-websocket-extensions'].indexOf('permessage-deflate')); - }); - ws.on('open', function() { - assert.ok(ws.extensions['permessage-deflate']); - ws.terminate(); - wss.close(); - done(); - }); - }); - }); - - it('can be disabled', function(done) { - var srv = http.createServer(function (req, res) {}); - var wss = new WebSocketServer({server: srv, perMessageDeflate: true}); - srv.listen(++port, function() { - var ws = new WebSocket('ws://localhost:' + port, {perMessageDeflate: false}); - srv.on('upgrade', function(req, socket, head) { - assert.ok(!req.headers['sec-websocket-extensions']); - ws.terminate(); - wss.close(); - done(); - }); - }); - }); - - it('can send extension parameters', function(done) { - var srv = http.createServer(function (req, res) {}); - var wss = new WebSocketServer({server: srv, perMessageDeflate: true}); - srv.listen(++port, function() { - var ws = new WebSocket('ws://localhost:' + port, { - perMessageDeflate: { - serverNoContextTakeover: true, - clientNoContextTakeover: true, - serverMaxWindowBits: 10, - clientMaxWindowBits: true - } - }); - srv.on('upgrade', function(req, socket, head) { - var extensions = req.headers['sec-websocket-extensions']; - assert.ok(~extensions.indexOf('permessage-deflate')); - assert.ok(~extensions.indexOf('server_no_context_takeover')); - assert.ok(~extensions.indexOf('client_no_context_takeover')); - assert.ok(~extensions.indexOf('server_max_window_bits=10')); - assert.ok(~extensions.indexOf('client_max_window_bits')); - ws.terminate(); - wss.close(); - done(); - }); - }); - }); - - it('can send and receive text data', function(done) { - var wss = new WebSocketServer({port: ++port, perMessageDeflate: true}, function() { - var ws = new WebSocket('ws://localhost:' + port, {perMessageDeflate: true}); - ws.on('open', function() { - ws.send('hi', {compress: true}); - }); - ws.on('message', function(message, flags) { - assert.equal('hi', message); - ws.terminate(); - wss.close(); - done(); - }); - }); - wss.on('connection', function(ws) { - ws.on('message', function(message, flags) { - ws.send(message, {compress: true}); - }); - }); - }); - - it('can send and receive a typed array', function(done) { - var array = new Float32Array(5); - for (var i = 0; i < array.length; i++) array[i] = i / 2; - var wss = new WebSocketServer({port: ++port, perMessageDeflate: true}, function() { - var ws = new WebSocket('ws://localhost:' + port, {perMessageDeflate: true}); - ws.on('open', function() { - ws.send(array, {compress: true}); - }); - ws.on('message', function(message, flags) { - assert.ok(areArraysEqual(array, new Float32Array(getArrayBuffer(message)))); - ws.terminate(); - wss.close(); - done(); - }); - }); - wss.on('connection', function(ws) { - ws.on('message', function(message, flags) { - ws.send(message, {compress: true}); - }); - }); - }); - - it('can send and receive ArrayBuffer', function(done) { - var array = new Float32Array(5); - for (var i = 0; i < array.length; i++) array[i] = i / 2; - var wss = new WebSocketServer({port: ++port, perMessageDeflate: true}, function() { - var ws = new WebSocket('ws://localhost:' + port, {perMessageDeflate: true}); - ws.on('open', function() { - ws.send(array.buffer, {compress: true}); - }); - ws.on('message', function(message, flags) { - assert.ok(areArraysEqual(array, new Float32Array(getArrayBuffer(message)))); - ws.terminate(); - wss.close(); - done(); - }); - }); - wss.on('connection', function(ws) { - ws.on('message', function(message, flags) { - ws.send(message, {compress: true}); - }); - }); - }); - - it('with binary stream will send fragmented data', function(done) { - var wss = new WebSocketServer({port: ++port, perMessageDeflate: true}, function() { - var ws = new WebSocket('ws://localhost:' + port, {perMessageDeflate: true}); - var callbackFired = false; - ws.on('open', function() { - var fileStream = fs.createReadStream('test/fixtures/textfile'); - fileStream.bufferSize = 100; - ws.send(fileStream, {binary: true, compress: true}, function(error) { - assert.equal(null, error); - callbackFired = true; - }); - }); - ws.on('close', function() { - assert.ok(callbackFired); - wss.close(); - done(); - }); - }); - wss.on('connection', function(ws) { - ws.on('message', function(data, flags) { - assert.ok(flags.binary); - assert.ok(areArraysEqual(fs.readFileSync('test/fixtures/textfile'), data)); - ws.terminate(); - }); - }); - }); - - describe('#send', function() { - it('can set the compress option true when perMessageDeflate is disabled', function(done) { - var wss = new WebSocketServer({port: ++port}, function() { - var ws = new WebSocket('ws://localhost:' + port, {perMessageDeflate: false}); - ws.on('open', function() { - ws.send('hi', {compress: true}); - }); - ws.on('message', function(message, flags) { - assert.equal('hi', message); - ws.terminate(); - wss.close(); - done(); - }); - }); - wss.on('connection', function(ws) { - ws.on('message', function(message, flags) { - ws.send(message, {compress: true}); - }); - }); - }); - }); - - describe('#close', function() { - it('should not raise error callback, if any, if called during send data', function(done) { - var wss = new WebSocketServer({port: ++port, perMessageDeflate: true}, function() { - var ws = new WebSocket('ws://localhost:' + port, {perMessageDeflate: true}); - var errorGiven = false; - ws.on('open', function() { - ws.send('hi', function(error) { - errorGiven = error != null; - }); - ws.close(); - }); - ws.on('close', function() { - setTimeout(function() { - assert.ok(!errorGiven); - wss.close(); - ws.terminate(); - done(); - }, 1000); - }); - }); - }); - }); - - describe('#terminate', function() { - it('will raise error callback, if any, if called during send data', function(done) { - var wss = new WebSocketServer({port: ++port, perMessageDeflate: true}, function() { - var ws = new WebSocket('ws://localhost:' + port, {perMessageDeflate: true}); - var errorGiven = false; - ws.on('open', function() { - ws.send('hi', function(error) { - errorGiven = error != null; - }); - ws.terminate(); - }); - ws.on('close', function() { - setTimeout(function() { - assert.ok(errorGiven); - wss.close(); - ws.terminate(); - done(); - }, 1000); - }); - }); - }); - - it('can call during receiving data', function(done) { - var wss = new WebSocketServer({port: ++port, perMessageDeflate: true}, function() { - var ws = new WebSocket('ws://localhost:' + port, {perMessageDeflate: true}); - wss.on('connection', function(client) { - for (var i = 0; i < 10; i++) { - client.send('hi'); - } - client.send('hi', function() { - ws.terminate(); - }); - }); - ws.on('close', function() { - setTimeout(function() { - wss.close(); - done(); - }, 1000); - }); - }); - }); - }); - }); -}); diff --git a/test/WebSocketServer.test.js b/test/WebSocketServer.test.js deleted file mode 100644 index 5dce73cae..000000000 --- a/test/WebSocketServer.test.js +++ /dev/null @@ -1,1410 +0,0 @@ -var http = require('http') - , https = require('https') - , WebSocket = require('../') - , WebSocketServer = WebSocket.Server - , fs = require('fs') - , should = require('should'); - -var port = 8000; - -function getArrayBuffer(buf) { - var l = buf.length; - var arrayBuf = new ArrayBuffer(l); - for (var i = 0; i < l; ++i) { - arrayBuf[i] = buf[i]; - } - return arrayBuf; -} - -function areArraysEqual(x, y) { - if (x.length != y.length) return false; - for (var i = 0, l = x.length; i < l; ++i) { - if (x[i] !== y[i]) return false; - } - return true; -} - -describe('WebSocketServer', function() { - describe('#ctor', function() { - it('should return a new instance if called without new', function(done) { - var ws = WebSocketServer({noServer: true}); - ws.should.be.an.instanceOf(WebSocketServer); - done(); - }); - - it('throws an error if no option object is passed', function() { - var gotException = false; - try { - var wss = new WebSocketServer(); - } - catch (e) { - gotException = true; - } - gotException.should.be.ok; - }); - - it('throws an error if no port or server is specified', function() { - var gotException = false; - try { - var wss = new WebSocketServer({}); - } - catch (e) { - gotException = true; - } - gotException.should.be.ok; - }); - - it('does not throw an error if no port or server is specified, when the noServer option is true', function() { - var gotException = false; - try { - var wss = new WebSocketServer({noServer: true}); - } - catch (e) { - gotException = true; - } - gotException.should.eql(false); - }); - - it('emits an error if http server bind fails', function(done) { - var wss1 = new WebSocketServer({port: 50003}); - var wss2 = new WebSocketServer({port: 50003}); - wss2.on('error', function() { - wss1.close(); - done(); - }); - }); - - it('starts a server on a given port', function(done) { - var wss = new WebSocketServer({port: ++port}, function() { - var ws = new WebSocket('ws://localhost:' + port); - }); - wss.on('connection', function(client) { - wss.close(); - done(); - }); - }); - - it('uses a precreated http server', function (done) { - var srv = http.createServer(); - srv.listen(++port, function () { - var wss = new WebSocketServer({server: srv}); - var ws = new WebSocket('ws://localhost:' + port); - - wss.on('connection', function(client) { - wss.close(); - srv.close(); - done(); - }); - }); - }); - - it('426s for non-Upgrade requests', function (done) { - var wss = new WebSocketServer({ port: ++port }, function () { - http.get('http://localhost:' + port, function (res) { - var body = ''; - - res.statusCode.should.equal(426); - res.on('data', function (chunk) { body += chunk; }); - res.on('end', function () { - body.should.equal(http.STATUS_CODES[426]); - wss.close(); - done(); - }); - }); - }); - }); - - // Don't test this on Windows. It throws errors for obvious reasons. - if(!/^win/i.test(process.platform)) { - it('uses a precreated http server listening on unix socket', function (done) { - var srv = http.createServer(); - var sockPath = '/tmp/ws_socket_'+new Date().getTime()+'.'+Math.floor(Math.random() * 1000); - srv.listen(sockPath, function () { - var wss = new WebSocketServer({server: srv}); - var ws = new WebSocket('ws+unix://'+sockPath); - - wss.on('connection', function(client) { - wss.close(); - srv.close(); - done(); - }); - }); - }); - } - - it('emits path specific connection event', function (done) { - var srv = http.createServer(); - srv.listen(++port, function () { - var wss = new WebSocketServer({server: srv}); - var ws = new WebSocket('ws://localhost:' + port+'/endpointName'); - - wss.on('connection/endpointName', function(client) { - wss.close(); - srv.close(); - done(); - }); - }); - }); - - it('can have two different instances listening on the same http server with two different paths', function(done) { - var srv = http.createServer(); - srv.listen(++port, function () { - var wss1 = new WebSocketServer({server: srv, path: '/wss1'}) - , wss2 = new WebSocketServer({server: srv, path: '/wss2'}); - var doneCount = 0; - wss1.on('connection', function(client) { - wss1.close(); - if (++doneCount == 2) { - srv.close(); - done(); - } - }); - wss2.on('connection', function(client) { - wss2.close(); - if (++doneCount == 2) { - srv.close(); - done(); - } - }); - var ws1 = new WebSocket('ws://localhost:' + port + '/wss1'); - var ws2 = new WebSocket('ws://localhost:' + port + '/wss2?foo=1'); - }); - }); - - it('cannot have two different instances listening on the same http server with the same path', function(done) { - var srv = http.createServer(); - srv.listen(++port, function () { - var wss1 = new WebSocketServer({server: srv, path: '/wss1'}); - try { - var wss2 = new WebSocketServer({server: srv, path: '/wss1'}); - } - catch (e) { - wss1.close(); - srv.close(); - done(); - } - }); - }); - it('will not crash when it receives an unhandled opcode', function(done) { - var wss = new WebSocketServer({ port: 8080 }); - wss.on('connection', function connection(ws) { - ws.onerror = function(error) { - done(); - }; - }); - - var socket = new WebSocket('ws://127.0.0.1:8080/'); - - socket.onopen = function() { - socket._socket.write(new Buffer([5])); - socket.send(''); - }; - }); - }); - - describe('#close', function() { - it('does not thrown when called twice', function(done) { - var wss = new WebSocketServer({port: ++port}, function() { - wss.close(); - wss.close(); - wss.close(); - - done(); - }); - }); - - it('will close all clients', function(done) { - var wss = new WebSocketServer({port: ++port}, function() { - var ws = new WebSocket('ws://localhost:' + port); - ws.on('close', function() { - if (++closes == 2) done(); - }); - }); - var closes = 0; - wss.on('connection', function(client) { - client.on('close', function() { - if (++closes == 2) done(); - }); - wss.close(); - }); - }); - - it('does not close a precreated server', function(done) { - var srv = http.createServer(); - var realClose = srv.close; - srv.close = function() { - should.fail('must not close pre-created server'); - } - srv.listen(++port, function () { - var wss = new WebSocketServer({server: srv}); - var ws = new WebSocket('ws://localhost:' + port); - wss.on('connection', function(client) { - wss.close(); - srv.close = realClose; - srv.close(); - done(); - }); - }); - }); - - it('cleans event handlers on precreated server', function(done) { - var srv = http.createServer(); - srv.listen(++port, function() { - var wss = new WebSocketServer({server: srv}); - wss.close(); - srv.emit('upgrade'); - srv.on('error', function() {}); - srv.emit('error'); - done() - }); - }); - - it('cleans up websocket data on a precreated server', function(done) { - var srv = http.createServer(); - srv.listen(++port, function () { - var wss1 = new WebSocketServer({server: srv, path: '/wss1'}) - , wss2 = new WebSocketServer({server: srv, path: '/wss2'}); - (typeof srv._webSocketPaths).should.eql('object'); - Object.keys(srv._webSocketPaths).length.should.eql(2); - wss1.close(); - Object.keys(srv._webSocketPaths).length.should.eql(1); - wss2.close(); - (typeof srv._webSocketPaths).should.eql('undefined'); - srv.close(); - done(); - }); - }); - }); - - describe('#clients', function() { - it('returns a list of connected clients', function(done) { - var wss = new WebSocketServer({port: ++port}, function() { - wss.clients.length.should.eql(0); - var ws = new WebSocket('ws://localhost:' + port); - }); - wss.on('connection', function(client) { - wss.clients.length.should.eql(1); - wss.close(); - done(); - }); - }); - - it('can be disabled', function(done) { - var wss = new WebSocketServer({port: ++port, clientTracking: false}, function() { - wss.clients.length.should.eql(0); - var ws = new WebSocket('ws://localhost:' + port); - }); - wss.on('connection', function(client) { - wss.clients.length.should.eql(0); - wss.close(); - done(); - }); - }); - - it('is updated when client terminates the connection', function(done) { - var ws; - var wss = new WebSocketServer({port: ++port}, function() { - ws = new WebSocket('ws://localhost:' + port); - }); - wss.on('connection', function(client) { - client.on('close', function() { - wss.clients.length.should.eql(0); - wss.close(); - done(); - }); - ws.terminate(); - }); - }); - - it('is updated when client closes the connection', function(done) { - var ws; - var wss = new WebSocketServer({port: ++port}, function() { - ws = new WebSocket('ws://localhost:' + port); - }); - wss.on('connection', function(client) { - client.on('close', function() { - wss.clients.length.should.eql(0); - wss.close(); - done(); - }); - ws.close(); - }); - }); - }); - - describe('#options', function() { - it('exposes options passed to constructor', function(done) { - var wss = new WebSocketServer({port: ++port}, function() { - wss.options.port.should.eql(port); - wss.close(); - done(); - }); - }); - }); - - describe('#maxpayload #hybiOnly', function() { - it('maxpayload is passed on to clients,', function(done) { - var _maxPayload = 20480; - var wss = new WebSocketServer({port: ++port,maxPayload:_maxPayload, disableHixie: true}, function() { - wss.clients.length.should.eql(0); - var ws = new WebSocket('ws://localhost:' + port); - }); - wss.on('connection', function(client) { - wss.clients.length.should.eql(1); - wss.clients[0].maxPayload.should.eql(_maxPayload); - wss.close(); - done(); - }); - }); - it('maxpayload is passed on to hybi receivers', function(done) { - var _maxPayload = 20480; - var wss = new WebSocketServer({port: ++port,maxPayload:_maxPayload, disableHixie: true}, function() { - wss.clients.length.should.eql(0); - var ws = new WebSocket('ws://localhost:' + port); - }); - wss.on('connection', function(client) { - wss.clients.length.should.eql(1); - wss.clients[0]._receiver.maxPayload.should.eql(_maxPayload); - wss.close(); - done(); - }); - }); - it('maxpayload is passed on to permessage-deflate', function(done) { - var PerMessageDeflate = require('../lib/PerMessageDeflate'); - var _maxPayload = 20480; - var wss = new WebSocketServer({port: ++port,maxPayload:_maxPayload, disableHixie: true}, function() { - wss.clients.length.should.eql(0); - var ws = new WebSocket('ws://localhost:' + port); - }); - wss.on('connection', function(client) { - wss.clients.length.should.eql(1); - wss.clients[0]._receiver.extensions[PerMessageDeflate.extensionName]._maxPayload.should.eql(_maxPayload); - wss.close(); - done(); - }); - }); - }); - - describe('#handleUpgrade', function() { - it('can be used for a pre-existing server', function (done) { - var srv = http.createServer(); - srv.listen(++port, function () { - var wss = new WebSocketServer({noServer: true}); - srv.on('upgrade', function(req, socket, upgradeHead) { - wss.handleUpgrade(req, socket, upgradeHead, function(client) { - client.send('hello'); - }); - }); - var ws = new WebSocket('ws://localhost:' + port); - ws.on('message', function(message) { - message.should.eql('hello'); - wss.close(); - srv.close(); - done(); - }); - }); - }); - }); - - describe('hybi mode', function() { - describe('connection establishing', function() { - it('does not accept connections with no sec-websocket-key', function(done) { - var wss = new WebSocketServer({port: ++port}, function() { - var options = { - port: port, - host: '127.0.0.1', - headers: { - 'Connection': 'Upgrade', - 'Upgrade': 'websocket' - } - }; - var req = http.request(options); - req.end(); - req.on('response', function(res) { - res.statusCode.should.eql(400); - wss.close(); - done(); - }); - }); - wss.on('connection', function(ws) { - done(new Error('connection must not be established')); - }); - wss.on('error', function() {}); - }); - - it('does not accept connections with no sec-websocket-version', function(done) { - var wss = new WebSocketServer({port: ++port}, function() { - var options = { - port: port, - host: '127.0.0.1', - headers: { - 'Connection': 'Upgrade', - 'Upgrade': 'websocket', - 'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==' - } - }; - var req = http.request(options); - req.end(); - req.on('response', function(res) { - res.statusCode.should.eql(400); - wss.close(); - done(); - }); - }); - wss.on('connection', function(ws) { - done(new Error('connection must not be established')); - }); - wss.on('error', function() {}); - }); - - it('does not accept connections with invalid sec-websocket-version', function(done) { - var wss = new WebSocketServer({port: ++port}, function() { - var options = { - port: port, - host: '127.0.0.1', - headers: { - 'Connection': 'Upgrade', - 'Upgrade': 'websocket', - 'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==', - 'Sec-WebSocket-Version': 12 - } - }; - var req = http.request(options); - req.end(); - req.on('response', function(res) { - res.statusCode.should.eql(400); - wss.close(); - done(); - }); - }); - wss.on('connection', function(ws) { - done(new Error('connection must not be established')); - }); - wss.on('error', function() {}); - }); - - it('client can be denied', function(done) { - var wss = new WebSocketServer({port: ++port, verifyClient: function(o) { - return false; - }}, function() { - var options = { - port: port, - host: '127.0.0.1', - headers: { - 'Connection': 'Upgrade', - 'Upgrade': 'websocket', - 'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==', - 'Sec-WebSocket-Version': 8, - 'Sec-WebSocket-Origin': 'http://foobar.com' - } - }; - var req = http.request(options); - req.end(); - req.on('response', function(res) { - res.statusCode.should.eql(401); - process.nextTick(function() { - wss.close(); - done(); - }); - }); - }); - wss.on('connection', function(ws) { - done(new Error('connection must not be established')); - }); - wss.on('error', function() {}); - }); - - it('client can be accepted', function(done) { - var wss = new WebSocketServer({port: ++port, verifyClient: function(o) { - return true; - }}, function() { - var options = { - port: port, - host: '127.0.0.1', - headers: { - 'Connection': 'Upgrade', - 'Upgrade': 'websocket', - 'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==', - 'Sec-WebSocket-Version': 13, - 'Origin': 'http://foobar.com' - } - }; - var req = http.request(options); - req.end(); - }); - wss.on('connection', function(ws) { - ws.terminate(); - wss.close(); - done(); - }); - wss.on('error', function() {}); - }); - - it('verifyClient gets client origin', function(done) { - var verifyClientCalled = false; - var wss = new WebSocketServer({port: ++port, verifyClient: function(info) { - info.origin.should.eql('http://foobarbaz.com'); - verifyClientCalled = true; - return false; - }}, function() { - var options = { - port: port, - host: '127.0.0.1', - headers: { - 'Connection': 'Upgrade', - 'Upgrade': 'websocket', - 'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==', - 'Sec-WebSocket-Version': 13, - 'Origin': 'http://foobarbaz.com' - } - }; - var req = http.request(options); - req.end(); - req.on('response', function(res) { - verifyClientCalled.should.be.ok; - wss.close(); - done(); - }); - }); - wss.on('error', function() {}); - }); - - it('verifyClient gets original request', function(done) { - var verifyClientCalled = false; - var wss = new WebSocketServer({port: ++port, verifyClient: function(info) { - info.req.headers['sec-websocket-key'].should.eql('dGhlIHNhbXBsZSBub25jZQ=='); - verifyClientCalled = true; - return false; - }}, function() { - var options = { - port: port, - host: '127.0.0.1', - headers: { - 'Connection': 'Upgrade', - 'Upgrade': 'websocket', - 'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==', - 'Sec-WebSocket-Version': 13, - 'Origin': 'http://foobarbaz.com' - } - }; - var req = http.request(options); - req.end(); - req.on('response', function(res) { - verifyClientCalled.should.be.ok; - wss.close(); - done(); - }); - }); - wss.on('error', function() {}); - }); - - it('verifyClient has secure:true for ssl connections', function(done) { - var options = { - key: fs.readFileSync('test/fixtures/key.pem'), - cert: fs.readFileSync('test/fixtures/certificate.pem') - }; - var app = https.createServer(options, function (req, res) { - res.writeHead(200); - res.end(); - }); - var success = false; - var wss = new WebSocketServer({ - server: app, - verifyClient: function(info) { - success = info.secure === true; - return true; - } - }); - app.listen(++port, function() { - var ws = new WebSocket('wss://localhost:' + port); - }); - wss.on('connection', function(ws) { - app.close(); - ws.terminate(); - wss.close(); - success.should.be.ok; - done(); - }); - }); - - it('verifyClient has secure:false for non-ssl connections', function(done) { - var app = http.createServer(function (req, res) { - res.writeHead(200); - res.end(); - }); - var success = false; - var wss = new WebSocketServer({ - server: app, - verifyClient: function(info) { - success = info.secure === false; - return true; - } - }); - app.listen(++port, function() { - var ws = new WebSocket('ws://localhost:' + port); - }); - wss.on('connection', function(ws) { - app.close(); - ws.terminate(); - wss.close(); - success.should.be.ok; - done(); - }); - }); - - it('client can be denied asynchronously', function(done) { - var wss = new WebSocketServer({port: ++port, verifyClient: function(o, cb) { - process.nextTick(function() { - cb(false); - }); - }}, function() { - var options = { - port: port, - host: '127.0.0.1', - headers: { - 'Connection': 'Upgrade', - 'Upgrade': 'websocket', - 'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==', - 'Sec-WebSocket-Version': 8, - 'Sec-WebSocket-Origin': 'http://foobar.com' - } - }; - var req = http.request(options); - req.end(); - req.on('response', function(res) { - res.statusCode.should.eql(401); - process.nextTick(function() { - wss.close(); - done(); - }); - }); - }); - wss.on('connection', function(ws) { - done(new Error('connection must not be established')); - }); - wss.on('error', function() {}); - }); - - it('client can be denied asynchronously with custom response code', function(done) { - var wss = new WebSocketServer({port: ++port, verifyClient: function(o, cb) { - process.nextTick(function() { - cb(false, 404); - }); - }}, function() { - var options = { - port: port, - host: '127.0.0.1', - headers: { - 'Connection': 'Upgrade', - 'Upgrade': 'websocket', - 'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==', - 'Sec-WebSocket-Version': 8, - 'Sec-WebSocket-Origin': 'http://foobar.com' - } - }; - var req = http.request(options); - req.end(); - req.on('response', function(res) { - res.statusCode.should.eql(404); - process.nextTick(function() { - wss.close(); - done(); - }); - }); - }); - wss.on('connection', function(ws) { - done(new Error('connection must not be established')); - }); - wss.on('error', function() {}); - }); - - it('client can be accepted asynchronously', function(done) { - var wss = new WebSocketServer({port: ++port, verifyClient: function(o, cb) { - process.nextTick(function() { - cb(true); - }); - }}, function() { - var options = { - port: port, - host: '127.0.0.1', - headers: { - 'Connection': 'Upgrade', - 'Upgrade': 'websocket', - 'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==', - 'Sec-WebSocket-Version': 13, - 'Origin': 'http://foobar.com' - } - }; - var req = http.request(options); - req.end(); - }); - wss.on('connection', function(ws) { - ws.terminate(); - wss.close(); - done(); - }); - wss.on('error', function() {}); - }); - - it('handles messages passed along with the upgrade request (upgrade head)', function(done) { - var wss = new WebSocketServer({port: ++port, verifyClient: function(o) { - return true; - }}, function() { - var options = { - port: port, - host: '127.0.0.1', - headers: { - 'Connection': 'Upgrade', - 'Upgrade': 'websocket', - 'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==', - 'Sec-WebSocket-Version': 13, - 'Origin': 'http://foobar.com' - } - }; - var req = http.request(options); - req.write(new Buffer([0x81, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f], 'binary')); - req.end(); - }); - wss.on('connection', function(ws) { - ws.on('message', function(data) { - data.should.eql('Hello'); - ws.terminate(); - wss.close(); - done(); - }); - }); - wss.on('error', function() {}); - }); - - it('selects the first protocol by default', function(done) { - var wss = new WebSocketServer({port: ++port}, function() { - var ws = new WebSocket('ws://localhost:' + port, ['prot1', 'prot2']); - ws.on('open', function(client) { - ws.protocol.should.eql('prot1'); - wss.close(); - done(); - }); - }); - }); - - it('selects the last protocol via protocol handler', function(done) { - var wss = new WebSocketServer({port: ++port, handleProtocols: function(ps, cb) { - cb(true, ps[ps.length-1]); }}, function() { - var ws = new WebSocket('ws://localhost:' + port, ['prot1', 'prot2']); - ws.on('open', function(client) { - ws.protocol.should.eql('prot2'); - wss.close(); - done(); - }); - }); - }); - - it('client detects invalid server protocol', function(done) { - var wss = new WebSocketServer({port: ++port, handleProtocols: function(ps, cb) { - cb(true, 'prot3'); }}, function() { - var ws = new WebSocket('ws://localhost:' + port, ['prot1', 'prot2']); - ws.on('open', function(client) { - done(new Error('connection must not be established')); - }); - ws.on('error', function() { - done(); - }); - }); - }); - - it('client detects no server protocol', function(done) { - var wss = new WebSocketServer({port: ++port, handleProtocols: function(ps, cb) { - cb(true); }}, function() { - var ws = new WebSocket('ws://localhost:' + port, ['prot1', 'prot2']); - ws.on('open', function(client) { - done(new Error('connection must not be established')); - }); - ws.on('error', function() { - done(); - }); - }); - }); - - it('client refuses server protocols', function(done) { - var wss = new WebSocketServer({port: ++port, handleProtocols: function(ps, cb) { - cb(false); }}, function() { - var ws = new WebSocket('ws://localhost:' + port, ['prot1', 'prot2']); - ws.on('open', function(client) { - done(new Error('connection must not be established')); - }); - ws.on('error', function() { - done(); - }); - }); - }); - - it('server detects unauthorized protocol handler', function(done) { - var wss = new WebSocketServer({port: ++port, handleProtocols: function(ps, cb) { - cb(false); - }}, function() { - var options = { - port: port, - host: '127.0.0.1', - headers: { - 'Connection': 'Upgrade', - 'Upgrade': 'websocket', - 'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==', - 'Sec-WebSocket-Version': 13, - 'Sec-WebSocket-Origin': 'http://foobar.com' - } - }; - options.port = port; - var req = http.request(options); - req.end(); - req.on('response', function(res) { - res.statusCode.should.eql(401); - wss.close(); - done(); - }); - }); - wss.on('connection', function(ws) { - done(new Error('connection must not be established')); - }); - wss.on('error', function() {}); - }); - - it('server detects invalid protocol handler', function(done) { - var wss = new WebSocketServer({port: ++port, handleProtocols: function(ps, cb) { - // not calling callback is an error and shouldn't timeout - }}, function() { - var options = { - port: port, - host: '127.0.0.1', - headers: { - 'Connection': 'Upgrade', - 'Upgrade': 'websocket', - 'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==', - 'Sec-WebSocket-Version': 13, - 'Sec-WebSocket-Origin': 'http://foobar.com' - } - }; - options.port = port; - var req = http.request(options); - req.end(); - req.on('response', function(res) { - res.statusCode.should.eql(501); - wss.close(); - done(); - }); - }); - wss.on('connection', function(ws) { - done(new Error('connection must not be established')); - }); - wss.on('error', function() {}); - }); - - it('accept connections with sec-websocket-extensions', function(done) { - var wss = new WebSocketServer({port: ++port}, function() { - var options = { - port: port, - host: '127.0.0.1', - headers: { - 'Connection': 'Upgrade', - 'Upgrade': 'websocket', - 'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==', - 'Sec-WebSocket-Version': 13, - 'Sec-WebSocket-Extensions': 'permessage-foo; x=10' - } - }; - var req = http.request(options); - req.end(); - }); - wss.on('connection', function(ws) { - ws.terminate(); - wss.close(); - done(); - }); - wss.on('error', function() {}); - }); - }); - - describe('messaging', function() { - it('can send and receive data', function(done) { - var data = new Array(65*1024); - for (var i = 0; i < data.length; ++i) { - data[i] = String.fromCharCode(65 + ~~(25 * Math.random())); - } - data = data.join(''); - var wss = new WebSocketServer({port: ++port}, function() { - var ws = new WebSocket('ws://localhost:' + port); - ws.on('message', function(message, flags) { - ws.send(message); - }); - }); - wss.on('connection', function(client) { - client.on('message', function(message) { - message.should.eql(data); - wss.close(); - done(); - }); - client.send(data); - }); - }); - }); - }); - - describe('hixie mode', function() { - it('can be disabled', function(done) { - var wss = new WebSocketServer({port: ++port, disableHixie: true}, function() { - var options = { - port: port, - host: '127.0.0.1', - headers: { - 'Connection': 'Upgrade', - 'Upgrade': 'WebSocket', - 'Sec-WebSocket-Key1': '3e6b263 4 17 80', - 'Sec-WebSocket-Key2': '17 9 G`ZD9 2 2b 7X 3 /r90' - } - }; - var req = http.request(options); - req.write('WjN}|M(6'); - req.end(); - req.on('response', function(res) { - res.statusCode.should.eql(401); - process.nextTick(function() { - wss.close(); - done(); - }); - }); - }); - wss.on('connection', function(ws) { - done(new Error('connection must not be established')); - }); - wss.on('error', function() {}); - }); - - describe('connection establishing', function() { - it('does not accept connections with no sec-websocket-key1', function(done) { - var wss = new WebSocketServer({port: ++port}, function() { - var options = { - port: port, - host: '127.0.0.1', - headers: { - 'Connection': 'Upgrade', - 'Upgrade': 'WebSocket', - 'Sec-WebSocket-Key1': '3e6b263 4 17 80' - } - }; - var req = http.request(options); - req.end(); - req.on('response', function(res) { - res.statusCode.should.eql(400); - wss.close(); - done(); - }); - }); - wss.on('connection', function(ws) { - done(new Error('connection must not be established')); - }); - wss.on('error', function() {}); - }); - - it('does not accept connections with no sec-websocket-key2', function(done) { - var wss = new WebSocketServer({port: ++port}, function() { - var options = { - port: port, - host: '127.0.0.1', - headers: { - 'Connection': 'Upgrade', - 'Upgrade': 'WebSocket', - 'Sec-WebSocket-Key2': '17 9 G`ZD9 2 2b 7X 3 /r90' - } - }; - var req = http.request(options); - req.end(); - req.on('response', function(res) { - res.statusCode.should.eql(400); - wss.close(); - done(); - }); - }); - wss.on('connection', function(ws) { - done(new Error('connection must not be established')); - }); - wss.on('error', function() {}); - }); - - it('accepts connections with valid handshake', function(done) { - var wss = new WebSocketServer({port: ++port}, function() { - var options = { - port: port, - host: '127.0.0.1', - headers: { - 'Connection': 'Upgrade', - 'Upgrade': 'WebSocket', - 'Sec-WebSocket-Key1': '3e6b263 4 17 80', - 'Sec-WebSocket-Key2': '17 9 G`ZD9 2 2b 7X 3 /r90' - } - }; - var req = http.request(options); - req.write('WjN}|M(6'); - req.end(); - }); - wss.on('connection', function(ws) { - ws.terminate(); - wss.close(); - done(); - }); - wss.on('error', function() {}); - }); - - it('client can be denied', function(done) { - var wss = new WebSocketServer({port: ++port, verifyClient: function(o) { - return false; - }}, function() { - var options = { - port: port, - host: '127.0.0.1', - headers: { - 'Connection': 'Upgrade', - 'Upgrade': 'WebSocket', - 'Sec-WebSocket-Key1': '3e6b263 4 17 80', - 'Sec-WebSocket-Key2': '17 9 G`ZD9 2 2b 7X 3 /r90' - } - }; - var req = http.request(options); - req.write('WjN}|M(6'); - req.end(); - req.on('response', function(res) { - res.statusCode.should.eql(401); - process.nextTick(function() { - wss.close(); - done(); - }); - }); - }); - wss.on('connection', function(ws) { - done(new Error('connection must not be established')); - }); - wss.on('error', function() {}); - }); - - it('client can be accepted', function(done) { - var wss = new WebSocketServer({port: ++port, verifyClient: function(o) { - return true; - }}, function() { - var options = { - port: port, - host: '127.0.0.1', - headers: { - 'Connection': 'Upgrade', - 'Upgrade': 'WebSocket', - 'Sec-WebSocket-Key1': '3e6b263 4 17 80', - 'Sec-WebSocket-Key2': '17 9 G`ZD9 2 2b 7X 3 /r90' - } - }; - var req = http.request(options); - req.write('WjN}|M(6'); - req.end(); - }); - wss.on('connection', function(ws) { - ws.terminate(); - wss.close(); - done(); - }); - wss.on('error', function() {}); - }); - - it('verifyClient gets client origin', function(done) { - var verifyClientCalled = false; - var wss = new WebSocketServer({port: ++port, verifyClient: function(info) { - info.origin.should.eql('http://foobarbaz.com'); - verifyClientCalled = true; - return false; - }}, function() { - var options = { - port: port, - host: '127.0.0.1', - headers: { - 'Connection': 'Upgrade', - 'Upgrade': 'WebSocket', - 'Origin': 'http://foobarbaz.com', - 'Sec-WebSocket-Key1': '3e6b263 4 17 80', - 'Sec-WebSocket-Key2': '17 9 G`ZD9 2 2b 7X 3 /r90' - } - }; - var req = http.request(options); - req.write('WjN}|M(6'); - req.end(); - req.on('response', function(res) { - verifyClientCalled.should.be.ok; - wss.close(); - done(); - }); - }); - wss.on('error', function() {}); - }); - - it('verifyClient gets original request', function(done) { - var verifyClientCalled = false; - var wss = new WebSocketServer({port: ++port, verifyClient: function(info) { - info.req.headers['sec-websocket-key1'].should.eql('3e6b263 4 17 80'); - verifyClientCalled = true; - return false; - }}, function() { - var options = { - port: port, - host: '127.0.0.1', - headers: { - 'Connection': 'Upgrade', - 'Upgrade': 'WebSocket', - 'Origin': 'http://foobarbaz.com', - 'Sec-WebSocket-Key1': '3e6b263 4 17 80', - 'Sec-WebSocket-Key2': '17 9 G`ZD9 2 2b 7X 3 /r90' - } - }; - var req = http.request(options); - req.write('WjN}|M(6'); - req.end(); - req.on('response', function(res) { - verifyClientCalled.should.be.ok; - wss.close(); - done(); - }); - }); - wss.on('error', function() {}); - }); - - it('client can be denied asynchronously', function(done) { - var wss = new WebSocketServer({port: ++port, verifyClient: function(o, cb) { - cb(false); - }}, function() { - var options = { - port: port, - host: '127.0.0.1', - headers: { - 'Connection': 'Upgrade', - 'Upgrade': 'WebSocket', - 'Origin': 'http://foobarbaz.com', - 'Sec-WebSocket-Key1': '3e6b263 4 17 80', - 'Sec-WebSocket-Key2': '17 9 G`ZD9 2 2b 7X 3 /r90' - } - }; - var req = http.request(options); - req.write('WjN}|M(6'); - req.end(); - req.on('response', function(res) { - res.statusCode.should.eql(401); - process.nextTick(function() { - wss.close(); - done(); - }); - }); - }); - wss.on('connection', function(ws) { - done(new Error('connection must not be established')); - }); - wss.on('error', function() {}); - }); - - it('client can be denied asynchronously with custom response code', function(done) { - var wss = new WebSocketServer({port: ++port, verifyClient: function(o, cb) { - cb(false, 404, 'Not Found'); - }}, function() { - var options = { - port: port, - host: '127.0.0.1', - headers: { - 'Connection': 'Upgrade', - 'Upgrade': 'WebSocket', - 'Origin': 'http://foobarbaz.com', - 'Sec-WebSocket-Key1': '3e6b263 4 17 80', - 'Sec-WebSocket-Key2': '17 9 G`ZD9 2 2b 7X 3 /r90' - } - }; - var req = http.request(options); - req.write('WjN}|M(6'); - req.end(); - req.on('response', function(res) { - res.statusCode.should.eql(404); - process.nextTick(function() { - wss.close(); - done(); - }); - }); - }); - wss.on('connection', function(ws) { - done(new Error('connection must not be established')); - }); - wss.on('error', function() {}); - }); - - it('client can be accepted asynchronously', function(done) { - var wss = new WebSocketServer({port: ++port, verifyClient: function(o, cb) { - cb(true); - }}, function() { - var options = { - port: port, - host: '127.0.0.1', - headers: { - 'Connection': 'Upgrade', - 'Upgrade': 'WebSocket', - 'Origin': 'http://foobarbaz.com', - 'Sec-WebSocket-Key1': '3e6b263 4 17 80', - 'Sec-WebSocket-Key2': '17 9 G`ZD9 2 2b 7X 3 /r90' - } - }; - var req = http.request(options); - req.write('WjN}|M(6'); - req.end(); - }); - wss.on('connection', function(ws) { - wss.close(); - done(); - }); - wss.on('error', function() {}); - }); - - it('handles messages passed along with the upgrade request (upgrade head)', function(done) { - var wss = new WebSocketServer({port: ++port, verifyClient: function(o) { - return true; - }}, function() { - var options = { - port: port, - host: '127.0.0.1', - headers: { - 'Connection': 'Upgrade', - 'Upgrade': 'WebSocket', - 'Sec-WebSocket-Key1': '3e6b263 4 17 80', - 'Sec-WebSocket-Key2': '17 9 G`ZD9 2 2b 7X 3 /r90', - 'Origin': 'http://foobar.com' - } - }; - var req = http.request(options); - req.write('WjN}|M(6'); - req.write(new Buffer([0x00, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0xff], 'binary')); - req.end(); - }); - wss.on('connection', function(ws) { - ws.on('message', function(data) { - data.should.eql('Hello'); - ws.terminate(); - wss.close(); - done(); - }); - }); - wss.on('error', function() {}); - }); - }); - }); - - describe('client properties', function() { - it('protocol is exposed', function(done) { - var wss = new WebSocketServer({port: ++port}, function() { - var ws = new WebSocket('ws://localhost:' + port, 'hi'); - }); - wss.on('connection', function(client) { - client.protocol.should.eql('hi'); - wss.close(); - done(); - }); - }); - - it('protocolVersion is exposed', function(done) { - var wss = new WebSocketServer({port: ++port}, function() { - var ws = new WebSocket('ws://localhost:' + port, {protocolVersion: 8}); - }); - wss.on('connection', function(client) { - client.protocolVersion.should.eql(8); - wss.close(); - done(); - }); - }); - - it('upgradeReq is the original request object', function(done) { - var wss = new WebSocketServer({port: ++port}, function() { - var ws = new WebSocket('ws://localhost:' + port, {protocolVersion: 8}); - }); - wss.on('connection', function(client) { - client.upgradeReq.httpVersion.should.eql('1.1'); - wss.close(); - done(); - }); - }); - }); - - describe('permessage-deflate', function() { - it('accept connections with permessage-deflate extension', function(done) { - var wss = new WebSocketServer({port: ++port}, function() { - var options = { - port: port, - host: '127.0.0.1', - headers: { - 'Connection': 'Upgrade', - 'Upgrade': 'websocket', - 'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==', - 'Sec-WebSocket-Version': 13, - 'Sec-WebSocket-Extensions': 'permessage-deflate; client_max_window_bits=8; server_max_window_bits=8; client_no_context_takeover; server_no_context_takeover' - } - }; - var req = http.request(options); - req.end(); - }); - wss.on('connection', function(ws) { - ws.terminate(); - wss.close(); - done(); - }); - wss.on('error', function() {}); - }); - - it('does not accept connections with not defined extension parameter', function(done) { - var wss = new WebSocketServer({port: ++port}, function() { - var options = { - port: port, - host: '127.0.0.1', - headers: { - 'Connection': 'Upgrade', - 'Upgrade': 'websocket', - 'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==', - 'Sec-WebSocket-Version': 13, - 'Sec-WebSocket-Extensions': 'permessage-deflate; foo=15' - } - }; - var req = http.request(options); - req.end(); - req.on('response', function(res) { - res.statusCode.should.eql(400); - wss.close(); - done(); - }); - }); - wss.on('connection', function(ws) { - done(new Error('connection must not be established')); - }); - wss.on('error', function() {}); - }); - - it('does not accept connections with invalid extension parameter', function(done) { - var wss = new WebSocketServer({port: ++port}, function() { - var options = { - port: port, - host: '127.0.0.1', - headers: { - 'Connection': 'Upgrade', - 'Upgrade': 'websocket', - 'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==', - 'Sec-WebSocket-Version': 13, - 'Sec-WebSocket-Extensions': 'permessage-deflate; server_max_window_bits=foo' - } - }; - var req = http.request(options); - req.end(); - req.on('response', function(res) { - res.statusCode.should.eql(400); - wss.close(); - done(); - }); - }); - wss.on('connection', function(ws) { - done(new Error('connection must not be established')); - }); - wss.on('error', function() {}); - }); - }); -}); diff --git a/test/autobahn-server.js b/test/autobahn-server.js index 36fe0c246..7d3e92915 100644 --- a/test/autobahn-server.js +++ b/test/autobahn-server.js @@ -1,29 +1,13 @@ -var WebSocketServer = require('../').Server; +'use strict'; -process.on('uncaughtException', function(err) { - console.log('Caught exception: ', err, err.stack); -}); +const WebSocket = require('../'); -process.on('SIGINT', function () { - try { - console.log('Updating reports and shutting down'); - var ws = new WebSocket('ws://localhost:9001/updateReports?agent=ws'); - ws.on('close', function() { - process.exit(); - }); - } - catch(e) { - process.exit(); - } +const port = process.argv.length > 2 ? parseInt(process.argv[2]) : 9001; +const wss = new WebSocket.Server({ port }, () => { + console.log(`Listening to port ${port}. Use extra argument to define the port`); }); -var wss = new WebSocketServer({port: 8181}); -wss.on('connection', function(ws) { - console.log('new connection'); - ws.on('message', function(data, flags) { - ws.send(flags.buffer, {binary: flags.binary === true}); - }); - ws.on('error', function() { - console.log('error', arguments); - }); +wss.on('connection', (ws) => { + ws.on('message', (data) => ws.send(data)); + ws.on('error', (e) => console.error(e)); }); diff --git a/test/autobahn.js b/test/autobahn.js index 048cc9041..cf2492494 100644 --- a/test/autobahn.js +++ b/test/autobahn.js @@ -1,52 +1,35 @@ -var WebSocket = require('../'); -var currentTest = 1; -var lastTest = -1; -var testCount = null; +'use strict'; -process.on('uncaughtException', function(err) { - console.log('Caught exception: ', err, err.stack); -}); +const WebSocket = require('../'); -process.on('SIGINT', function () { - try { - console.log('Updating reports and shutting down'); - var ws = new WebSocket('ws://localhost:9001/updateReports?agent=ws'); - ws.on('close', function() { - process.exit(); - }); - } - catch(e) { - process.exit(); - } -}); +let currentTest = 1; +let testCount; + +function nextTest () { + let ws; -function nextTest() { - if (currentTest > testCount || (lastTest != -1 && currentTest > lastTest)) { - console.log('Updating reports and shutting down'); - var ws = new WebSocket('ws://localhost:9001/updateReports?agent=ws'); - ws.on('close', function() { - process.exit(); - }); + if (currentTest > testCount) { + ws = new WebSocket('ws://localhost:9001/updateReports?agent=ws'); return; - }; - console.log('Running test case ' + currentTest + '/' + testCount); - var ws = new WebSocket('ws://localhost:9001/runCase?case=' + currentTest + '&agent=ws'); - ws.on('message', function(data, flags) { - ws.send(flags.buffer, {binary: flags.binary === true, mask: true}); - }); - ws.on('close', function(data) { - currentTest += 1; + } + + console.log(`Running test case ${currentTest}/${testCount}`); + + ws = new WebSocket(`ws://localhost:9001/runCase?case=${currentTest}&agent=ws`); + ws.on('message', (data) => ws.send(data)); + ws.on('close', () => { + currentTest++; process.nextTick(nextTest); }); - ws.on('error', function(e) {}); + ws.on('error', (e) => console.error(e)); } -var ws = new WebSocket('ws://localhost:9001/getCaseCount'); -ws.on('message', function(data, flags) { +const ws = new WebSocket('ws://localhost:9001/getCaseCount'); +ws.on('message', (data) => { testCount = parseInt(data); }); -ws.on('close', function() { +ws.on('close', () => { if (testCount > 0) { nextTest(); } -}); \ No newline at end of file +}); diff --git a/test/extension.test.js b/test/extension.test.js new file mode 100644 index 000000000..7e389811b --- /dev/null +++ b/test/extension.test.js @@ -0,0 +1,178 @@ +'use strict'; + +const assert = require('assert'); + +const extension = require('../lib/extension'); + +describe('extension', function () { + describe('parse', function () { + it('returns an empty object if the argument is `undefined`', function () { + assert.deepStrictEqual(extension.parse(), {}); + assert.deepStrictEqual(extension.parse(''), {}); + }); + + it('parses a single extension', function () { + const extensions = extension.parse('foo'); + + assert.deepStrictEqual(extensions, { foo: [{}] }); + }); + + it('parses params', function () { + const extensions = extension.parse('foo;bar;baz=1;bar=2'); + + assert.deepStrictEqual(extensions, { + foo: [{ bar: [true, '2'], baz: ['1'] }] + }); + }); + + it('parses multiple extensions', function () { + const extensions = extension.parse('foo,bar;baz,foo;baz'); + + assert.deepStrictEqual(extensions, { + foo: [{}, { baz: [true] }], + bar: [{ baz: [true] }] + }); + }); + + it('parses quoted params', function () { + assert.deepStrictEqual(extension.parse('foo;bar="hi"'), { + foo: [{ bar: ['hi'] }] + }); + assert.deepStrictEqual(extension.parse('foo;bar="\\0"'), { + foo: [{ bar: ['0'] }] + }); + assert.deepStrictEqual(extension.parse('foo;bar="b\\a\\z"'), { + foo: [{ bar: ['baz'] }] + }); + assert.deepStrictEqual(extension.parse('foo;bar="b\\az";bar'), { + foo: [{ bar: ['baz', true] }] + }); + assert.throws( + () => extension.parse('foo;bar="baz"qux'), + /^SyntaxError: Unexpected character at index 13$/ + ); + assert.throws( + () => extension.parse('foo;bar="baz" qux'), + /^SyntaxError: Unexpected character at index 14$/ + ); + }); + + it('works with names that match `Object.prototype` property names', function () { + const parse = extension.parse; + + assert.deepStrictEqual(parse('hasOwnProperty, toString'), { + hasOwnProperty: [{}], + toString: [{}] + }); + assert.deepStrictEqual(parse('foo;constructor'), { + foo: [{ constructor: [true] }] + }); + }); + + it('ignores the optional white spaces', function () { + const header = 'foo; bar\t; \tbaz=1\t ; bar="1"\t\t, \tqux\t ;norf '; + + assert.deepStrictEqual(extension.parse(header), { + foo: [{ bar: [true, '1'], baz: ['1'] }], + qux: [{ norf: [true] }] + }); + }); + + it('throws an error if a name is empty', function () { + [ + [',', 0], + ['foo,,', 4], + ['foo, ,', 6], + ['foo;=', 4], + ['foo; =', 5], + ['foo;;', 4], + ['foo; ;', 5], + ['foo;bar=,', 8], + ['foo;bar=""', 9] + ].forEach((element) => { + assert.throws( + () => extension.parse(element[0]), + new RegExp(`^SyntaxError: Unexpected character at index ${element[1]}$`) + ); + }); + }); + + it('throws an error if a white space is misplaced', function () { + [ + ['f oo', 2], + ['foo;ba r', 7], + ['foo;bar =', 8], + ['foo;bar= ', 8] + ].forEach((element) => { + assert.throws( + () => extension.parse(element[0]), + new RegExp(`^SyntaxError: Unexpected character at index ${element[1]}$`) + ); + }); + }); + + it('throws an error if a token contains invalid characters', function () { + [ + ['f@o', 1], + ['f\\oo', 1], + ['"foo"', 0], + ['f"oo"', 1], + ['foo;b@r', 5], + ['foo;b\\ar', 5], + ['foo;"bar"', 4], + ['foo;b"ar"', 5], + ['foo;bar=b@z', 9], + ['foo;bar=b\\az ', 9], + ['foo;bar="b@z"', 10], + ['foo;bar="baz;"', 12], + ['foo;bar=b"az"', 9], + ['foo;bar="\\\\"', 10] + ].forEach((element) => { + assert.throws( + () => extension.parse(element[0]), + new RegExp(`^SyntaxError: Unexpected character at index ${element[1]}$`) + ); + }); + }); + + it('throws an error if the header value ends prematurely', function () { + [ + 'foo, ', + 'foo;', + 'foo;bar,', + 'foo;bar; ', + 'foo;bar=', + 'foo;bar="baz', + 'foo;bar="1\\' + ].forEach((header) => { + assert.throws( + () => extension.parse(header), + /^SyntaxError: Unexpected end of input$/ + ); + }); + }); + }); + + describe('format', function () { + it('formats a single extension', function () { + const extensions = extension.format({ foo: {} }); + + assert.strictEqual(extensions, 'foo'); + }); + + it('formats params', function () { + const extensions = extension.format({ foo: { bar: [true, 2], baz: 1 } }); + + assert.strictEqual(extensions, 'foo; bar; bar=2; baz=1'); + }); + + it('formats multiple extensions', function () { + const extensions = extension.format({ + foo: [{}, { baz: true }], + bar: { baz: true } + }); + + assert.strictEqual(extensions, 'foo, foo; baz, bar; baz'); + }); + }); +}); diff --git a/test/fixtures/textfile b/test/fixtures/textfile deleted file mode 100644 index a10483b0e..000000000 --- a/test/fixtures/textfile +++ /dev/null @@ -1,9 +0,0 @@ -Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam egestas, massa at aliquam luctus, sapien erat viverra elit, nec pulvinar turpis eros sagittis urna. Pellentesque imperdiet tempor varius. Pellentesque blandit, ipsum in imperdiet venenatis, mi elit faucibus odio, id condimentum ante enim sed lectus. Aliquam et odio non odio pellentesque pulvinar. Vestibulum a erat dolor. Integer pretium risus sit amet nisl volutpat nec venenatis magna egestas. Ut bibendum felis eu tellus laoreet eleifend. Nam pulvinar auctor tortor, eu iaculis leo vestibulum quis. In euismod risus ac purus vehicula et fermentum ligula consectetur. Vivamus condimentum tempus lacinia. - -Curabitur sodales condimentum urna id dictum. Sed quis justo sit amet quam ultrices tincidunt vel laoreet nulla. Nullam quis ipsum sed nisi mollis bibendum at sit amet nisi. Donec laoreet consequat velit sit amet mollis. Nam sed sapien a massa iaculis dapibus. Sed dui nunc, ultricies et pellentesque ullamcorper, aliquet vitae ligula. Integer eu velit in neque iaculis venenatis. Ut rhoncus cursus est, ac dignissim leo vehicula a. Nulla ullamcorper vulputate mauris id blandit. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque eleifend, nisi a tempor sollicitudin, odio massa pretium urna, quis congue sapien elit at tortor. Curabitur ipsum orci, vehicula non commodo molestie, laoreet id enim. Pellentesque convallis ultrices congue. Pellentesque nec iaculis lorem. In sagittis pharetra ipsum eget sodales. - -Fusce id nulla odio. Nunc nibh justo, placerat vel tincidunt sed, ornare et enim. Nulla vel urna vel ante commodo bibendum in vitae metus. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Duis erat nunc, semper eget sagittis sit amet, ullamcorper eget lacus. Donec hendrerit ipsum vitae eros vestibulum eu gravida neque tincidunt. Ut molestie lacinia nulla. Donec mattis odio at magna egestas at pellentesque eros accumsan. Praesent interdum sem sit amet nibh commodo dignissim. Duis laoreet, enim ultricies fringilla suscipit, enim libero cursus nulla, sollicitudin adipiscing erat velit ut dui. Nulla eleifend mauris at velit fringilla a molestie lorem venenatis. - -Donec sit amet scelerisque metus. Cras ac felis a nulla venenatis vulputate. Duis porttitor eros ac neque rhoncus eget aliquet neque egestas. Quisque sed nunc est, vitae dapibus quam. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; In vehicula, est vitae posuere ultricies, diam purus pretium sapien, nec rhoncus dolor nisl eget arcu. Aliquam et nisi vitae risus tincidunt auctor. In vehicula, erat a cursus adipiscing, lorem orci congue est, nec ultricies elit dui in nunc. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Lorem ipsum dolor sit amet, consectetur adipiscing elit. - -Duis congue tempus elit sit amet auctor. Duis dignissim, risus ut sollicitudin ultricies, dolor ligula gravida odio, nec congue orci purus ut ligula. Fusce pretium dictum lectus at volutpat. Sed non auctor mauris. Etiam placerat vestibulum massa id blandit. Quisque consequat lacus ut nulla euismod facilisis. Sed aliquet ipsum nec mi imperdiet viverra. Pellentesque ullamcorper, lectus nec varius gravida, odio justo cursus risus, eu sagittis metus arcu quis felis. Phasellus consectetur vehicula libero, at condimentum orci euismod vel. Nunc purus tortor, suscipit nec fringilla nec, vulputate et nibh. Nam porta vehicula neque. Praesent porttitor, sapien eu auctor euismod, arcu quam elementum urna, sed hendrerit magna augue sed quam. \ No newline at end of file diff --git a/test/hybi-common.js b/test/hybi-common.js deleted file mode 100644 index 006f9c693..000000000 --- a/test/hybi-common.js +++ /dev/null @@ -1,99 +0,0 @@ -/** - * Returns a Buffer from a "ff 00 ff"-type hex string. - */ - -getBufferFromHexString = function(byteStr) { - var bytes = byteStr.split(' '); - var buf = new Buffer(bytes.length); - for (var i = 0; i < bytes.length; ++i) { - buf[i] = parseInt(bytes[i], 16); - } - return buf; -} - -/** - * Returns a hex string from a Buffer. - */ - -getHexStringFromBuffer = function(data) { - var s = ''; - for (var i = 0; i < data.length; ++i) { - s += padl(data[i].toString(16), 2, '0') + ' '; - } - return s.trim(); -} - -/** - * Splits a buffer in two parts. - */ - -splitBuffer = function(buffer) { - var b1 = new Buffer(Math.ceil(buffer.length / 2)); - buffer.copy(b1, 0, 0, b1.length); - var b2 = new Buffer(Math.floor(buffer.length / 2)); - buffer.copy(b2, 0, b1.length, b1.length + b2.length); - return [b1, b2]; -} - -/** - * Performs hybi07+ type masking on a hex string or buffer. - */ - -mask = function(buf, maskString) { - if (typeof buf == 'string') buf = new Buffer(buf); - var mask = getBufferFromHexString(maskString || '34 83 a8 68'); - for (var i = 0; i < buf.length; ++i) { - buf[i] ^= mask[i % 4]; - } - return buf; -} - -/** - * Returns a hex string representing the length of a message - */ - -getHybiLengthAsHexString = function(len, masked) { - if (len < 126) { - var buf = new Buffer(1); - buf[0] = (masked ? 0x80 : 0) | len; - } - else if (len < 65536) { - var buf = new Buffer(3); - buf[0] = (masked ? 0x80 : 0) | 126; - getBufferFromHexString(pack(4, len)).copy(buf, 1); - } - else { - var buf = new Buffer(9); - buf[0] = (masked ? 0x80 : 0) | 127; - getBufferFromHexString(pack(16, len)).copy(buf, 1); - } - return getHexStringFromBuffer(buf); -} - -/** - * Unpacks a Buffer into a number. - */ - -unpack = function(buffer) { - var n = 0; - for (var i = 0; i < buffer.length; ++i) { - n = (i == 0) ? buffer[i] : (n * 256) + buffer[i]; - } - return n; -} - -/** - * Returns a hex string, representing a specific byte count 'length', from a number. - */ - -pack = function(length, number) { - return padl(number.toString(16), length, '0').replace(/([0-9a-f][0-9a-f])/gi, '$1 ').trim(); -} - -/** - * Left pads the string 's' to a total length of 'n' with char 'c'. - */ - -padl = function(s, n, c) { - return new Array(1 + n - s.length).join(c) + s; -} diff --git a/test/permessage-deflate.test.js b/test/permessage-deflate.test.js new file mode 100644 index 000000000..3d51bd57e --- /dev/null +++ b/test/permessage-deflate.test.js @@ -0,0 +1,543 @@ +'use strict'; + +const assert = require('assert'); + +const PerMessageDeflate = require('../lib/permessage-deflate'); +const extension = require('../lib/extension'); + +describe('PerMessageDeflate', function () { + describe('#offer', function () { + it('creates an offer', function () { + const perMessageDeflate = new PerMessageDeflate(); + + assert.deepStrictEqual( + perMessageDeflate.offer(), + { client_max_window_bits: true } + ); + }); + + it('uses the configuration options', function () { + const perMessageDeflate = new PerMessageDeflate({ + serverNoContextTakeover: true, + clientNoContextTakeover: true, + serverMaxWindowBits: 10, + clientMaxWindowBits: 11 + }); + + assert.deepStrictEqual(perMessageDeflate.offer(), { + server_no_context_takeover: true, + client_no_context_takeover: true, + server_max_window_bits: 10, + client_max_window_bits: 11 + }); + }); + }); + + describe('#accept', function () { + it('throws an error if a parameter has multiple values', function () { + const perMessageDeflate = new PerMessageDeflate(); + const extensions = extension.parse( + 'permessage-deflate; server_no_context_takeover; server_no_context_takeover' + ); + + assert.throws( + () => perMessageDeflate.accept(extensions['permessage-deflate']), + /^Error: Parameter "server_no_context_takeover" must have only a single value$/ + ); + }); + + it('throws an error if a parameter has an invalid name', function () { + const perMessageDeflate = new PerMessageDeflate(); + const extensions = extension.parse('permessage-deflate;foo'); + + assert.throws( + () => perMessageDeflate.accept(extensions['permessage-deflate']), + /^Error: Unknown parameter "foo"$/ + ); + }); + + it('throws an error if client_no_context_takeover has a value', function () { + const perMessageDeflate = new PerMessageDeflate(); + const extensions = extension.parse('permessage-deflate; client_no_context_takeover=10'); + + assert.throws( + () => perMessageDeflate.accept(extensions['permessage-deflate']), + /^TypeError: Invalid value for parameter "client_no_context_takeover": 10$/ + ); + }); + + it('throws an error if server_no_context_takeover has a value', function () { + const perMessageDeflate = new PerMessageDeflate(); + const extensions = extension.parse('permessage-deflate; server_no_context_takeover=10'); + + assert.throws( + () => perMessageDeflate.accept(extensions['permessage-deflate']), + /^TypeError: Invalid value for parameter "server_no_context_takeover": 10$/ + ); + }); + + it('throws an error if server_max_window_bits has an invalid value', function () { + const perMessageDeflate = new PerMessageDeflate(); + + let extensions = extension.parse('permessage-deflate; server_max_window_bits=7'); + assert.throws( + () => perMessageDeflate.accept(extensions['permessage-deflate']), + /^TypeError: Invalid value for parameter "server_max_window_bits": 7$/ + ); + + extensions = extension.parse('permessage-deflate; server_max_window_bits'); + assert.throws( + () => perMessageDeflate.accept(extensions['permessage-deflate']), + /^TypeError: Invalid value for parameter "server_max_window_bits": true$/ + ); + }); + + describe('As server', function () { + it('accepts an offer with no parameters', function () { + const perMessageDeflate = new PerMessageDeflate({}, true); + + assert.deepStrictEqual(perMessageDeflate.accept([{}]), {}); + }); + + it('accepts an offer with parameters', function () { + const perMessageDeflate = new PerMessageDeflate({}, true); + const extensions = extension.parse( + 'permessage-deflate; server_no_context_takeover; ' + + 'client_no_context_takeover; server_max_window_bits=10; ' + + 'client_max_window_bits=11' + ); + + assert.deepStrictEqual(perMessageDeflate.accept(extensions['permessage-deflate']), { + server_no_context_takeover: true, + client_no_context_takeover: true, + server_max_window_bits: 10, + client_max_window_bits: 11 + }); + }); + + it('prefers the configuration options', function () { + const perMessageDeflate = new PerMessageDeflate({ + serverNoContextTakeover: true, + clientNoContextTakeover: true, + serverMaxWindowBits: 12, + clientMaxWindowBits: 11 + }, true); + const extensions = extension.parse( + 'permessage-deflate; server_max_window_bits=14; client_max_window_bits=13' + ); + + assert.deepStrictEqual(perMessageDeflate.accept(extensions['permessage-deflate']), { + server_no_context_takeover: true, + client_no_context_takeover: true, + server_max_window_bits: 12, + client_max_window_bits: 11 + }); + }); + + it('accepts the first supported offer', function () { + const perMessageDeflate = new PerMessageDeflate({ serverMaxWindowBits: 11 }, true); + const extensions = extension.parse( + 'permessage-deflate; server_max_window_bits=10, permessage-deflate' + ); + + assert.deepStrictEqual(perMessageDeflate.accept(extensions['permessage-deflate']), { + server_max_window_bits: 11 + }); + }); + + it('throws an error if server_no_context_takeover is unsupported', function () { + const perMessageDeflate = new PerMessageDeflate({ serverNoContextTakeover: false }, true); + const extensions = extension.parse('permessage-deflate; server_no_context_takeover'); + + assert.throws( + () => perMessageDeflate.accept(extensions['permessage-deflate']), + /^Error: None of the extension offers can be accepted$/ + ); + }); + + it('throws an error if server_max_window_bits is unsupported', function () { + const perMessageDeflate = new PerMessageDeflate({ serverMaxWindowBits: false }, true); + const extensions = extension.parse('permessage-deflate; server_max_window_bits=10'); + + assert.throws( + () => perMessageDeflate.accept(extensions['permessage-deflate']), + /^Error: None of the extension offers can be accepted$/ + ); + }); + + it('throws an error if server_max_window_bits is less than configuration', function () { + const perMessageDeflate = new PerMessageDeflate({ serverMaxWindowBits: 11 }, true); + const extensions = extension.parse('permessage-deflate; server_max_window_bits=10'); + + assert.throws( + () => perMessageDeflate.accept(extensions['permessage-deflate']), + /^Error: None of the extension offers can be accepted$/ + ); + }); + + it('throws an error if client_max_window_bits is unsupported on client', function () { + const perMessageDeflate = new PerMessageDeflate({ clientMaxWindowBits: 10 }, true); + const extensions = extension.parse('permessage-deflate'); + + assert.throws( + () => perMessageDeflate.accept(extensions['permessage-deflate']), + /^Error: None of the extension offers can be accepted$/ + ); + }); + + it('throws an error if client_max_window_bits has an invalid value', function () { + const perMessageDeflate = new PerMessageDeflate({}, true); + + const extensions = extension.parse('permessage-deflate; client_max_window_bits=16'); + assert.throws( + () => perMessageDeflate.accept(extensions['permessage-deflate']), + /^TypeError: Invalid value for parameter "client_max_window_bits": 16$/ + ); + }); + }); + + describe('As client', function () { + it('accepts a response with no parameters', function () { + const perMessageDeflate = new PerMessageDeflate({}); + + assert.deepStrictEqual(perMessageDeflate.accept([{}]), {}); + }); + + it('accepts a response with parameters', function () { + const perMessageDeflate = new PerMessageDeflate({}); + const extensions = extension.parse( + 'permessage-deflate; server_no_context_takeover; ' + + 'client_no_context_takeover; server_max_window_bits=10; ' + + 'client_max_window_bits=11' + ); + + assert.deepStrictEqual(perMessageDeflate.accept(extensions['permessage-deflate']), { + server_no_context_takeover: true, + client_no_context_takeover: true, + server_max_window_bits: 10, + client_max_window_bits: 11 + }); + }); + + it('throws an error if client_no_context_takeover is unsupported', function () { + const perMessageDeflate = new PerMessageDeflate({ clientNoContextTakeover: false }); + const extensions = extension.parse('permessage-deflate; client_no_context_takeover'); + + assert.throws( + () => perMessageDeflate.accept(extensions['permessage-deflate']), + /^Error: Unexpected parameter "client_no_context_takeover"$/ + ); + }); + + it('throws an error if client_max_window_bits is unsupported', function () { + const perMessageDeflate = new PerMessageDeflate({ clientMaxWindowBits: false }); + const extensions = extension.parse('permessage-deflate; client_max_window_bits=10'); + + assert.throws( + () => perMessageDeflate.accept(extensions['permessage-deflate']), + /^Error: Unexpected or invalid parameter "client_max_window_bits"$/ + ); + }); + + it('throws an error if client_max_window_bits is greater than configuration', function () { + const perMessageDeflate = new PerMessageDeflate({ clientMaxWindowBits: 10 }); + const extensions = extension.parse('permessage-deflate; client_max_window_bits=11'); + + assert.throws( + () => perMessageDeflate.accept(extensions['permessage-deflate']), + /^Error: Unexpected or invalid parameter "client_max_window_bits"$/ + ); + }); + + it('throws an error if client_max_window_bits has an invalid value', function () { + const perMessageDeflate = new PerMessageDeflate(); + + let extensions = extension.parse('permessage-deflate; client_max_window_bits=16'); + assert.throws( + () => perMessageDeflate.accept(extensions['permessage-deflate']), + /^TypeError: Invalid value for parameter "client_max_window_bits": 16$/ + ); + + extensions = extension.parse('permessage-deflate; client_max_window_bits'); + assert.throws( + () => perMessageDeflate.accept(extensions['permessage-deflate']), + /^TypeError: Invalid value for parameter "client_max_window_bits": true$/ + ); + }); + + it('uses the config value if client_max_window_bits is not specified', function () { + const perMessageDeflate = new PerMessageDeflate({ + clientMaxWindowBits: 10 + }); + + assert.deepStrictEqual(perMessageDeflate.accept([{}]), { + client_max_window_bits: 10 + }); + }); + }); + }); + + describe('#compress and #decompress', function () { + it('works with unfragmented messages', function (done) { + const perMessageDeflate = new PerMessageDeflate({ threshold: 0 }); + const buf = Buffer.from([1, 2, 3]); + + perMessageDeflate.accept([{}]); + perMessageDeflate.compress(buf, true, (err, data) => { + if (err) return done(err); + + perMessageDeflate.decompress(data, true, (err, data) => { + if (err) return done(err); + + assert.ok(data.equals(buf)); + done(); + }); + }); + }); + + it('works with fragmented messages', function (done) { + const perMessageDeflate = new PerMessageDeflate({ threshold: 0 }); + const buf = Buffer.from([1, 2, 3, 4]); + + perMessageDeflate.accept([{}]); + + perMessageDeflate.compress(buf.slice(0, 2), false, (err, compressed1) => { + if (err) return done(err); + + perMessageDeflate.compress(buf.slice(2), true, (err, compressed2) => { + if (err) return done(err); + + perMessageDeflate.decompress(compressed1, false, (err, data1) => { + if (err) return done(err); + + perMessageDeflate.decompress(compressed2, true, (err, data2) => { + if (err) return done(err); + + assert.ok(Buffer.concat([data1, data2]).equals(buf)); + done(); + }); + }); + }); + }); + }); + + it('works with the negotiated parameters', function (done) { + const perMessageDeflate = new PerMessageDeflate({ + threshold: 0, + memLevel: 5, + level: 9 + }); + const extensions = extension.parse( + 'permessage-deflate; server_no_context_takeover; ' + + 'client_no_context_takeover; server_max_window_bits=10; ' + + 'client_max_window_bits=11' + ); + const buf = Buffer.from("Some compressible data, it's compressible."); + + perMessageDeflate.accept(extensions['permessage-deflate']); + + perMessageDeflate.compress(buf, true, (err, data) => { + if (err) return done(err); + + perMessageDeflate.decompress(data, true, (err, data) => { + if (err) return done(err); + + assert.ok(data.equals(buf)); + done(); + }); + }); + }); + + it('honors the `level` option', function (done) { + const lev0 = new PerMessageDeflate({ threshold: 0, level: 0 }); + const lev9 = new PerMessageDeflate({ threshold: 0, level: 9 }); + const extensionStr = ( + 'permessage-deflate; server_no_context_takeover; ' + + 'client_no_context_takeover; server_max_window_bits=10; ' + + 'client_max_window_bits=11' + ); + const buf = Buffer.from("Some compressible data, it's compressible."); + + lev0.accept(extension.parse(extensionStr)['permessage-deflate']); + lev9.accept(extension.parse(extensionStr)['permessage-deflate']); + + lev0.compress(buf, true, (err, compressed1) => { + if (err) return done(err); + + lev0.decompress(compressed1, true, (err, decompressed1) => { + if (err) return done(err); + + lev9.compress(buf, true, (err, compressed2) => { + if (err) return done(err); + + lev9.decompress(compressed2, true, (err, decompressed2) => { + if (err) return done(err); + + // Level 0 compression actually adds a few bytes due to headers. + assert.ok(compressed1.length > buf.length); + // Level 9 should not, of course. + assert.ok(compressed2.length < buf.length); + // Ensure they both decompress back properly. + assert.ok(decompressed1.equals(buf)); + assert.ok(decompressed2.equals(buf)); + done(); + }); + }); + }); + }); + }); + + it('honors the `zlib{Deflate,Inflate}Options` option', function (done) { + const lev0 = new PerMessageDeflate({ + threshold: 0, + zlibDeflateOptions: { + level: 0, + chunkSize: 256 + }, + zlibInflateOptions: { + chunkSize: 2048 + } + }); + const lev9 = new PerMessageDeflate({ + threshold: 0, + zlibDeflateOptions: { + level: 9, + chunkSize: 128 + }, + zlibInflateOptions: { + chunkSize: 1024 + } + }); + + // Note no context takeover so we can get a hold of the raw streams after we do the dance + const extensionStr = ( + 'permessage-deflate; server_max_window_bits=10; ' + + 'client_max_window_bits=11' + ); + const buf = Buffer.from("Some compressible data, it's compressible."); + + lev0.accept(extension.parse(extensionStr)['permessage-deflate']); + lev9.accept(extension.parse(extensionStr)['permessage-deflate']); + + lev0.compress(buf, true, (err, compressed1) => { + if (err) return done(err); + + lev0.decompress(compressed1, true, (err, decompressed1) => { + if (err) return done(err); + + lev9.compress(buf, true, (err, compressed2) => { + if (err) return done(err); + + lev9.decompress(compressed2, true, (err, decompressed2) => { + if (err) return done(err); + // Level 0 compression actually adds a few bytes due to headers. + assert.ok(compressed1.length > buf.length); + // Level 9 should not, of course. + assert.ok(compressed2.length < buf.length); + // Ensure they both decompress back properly. + assert.ok(decompressed1.equals(buf)); + assert.ok(decompressed2.equals(buf)); + + // Assert options were set. + assert.ok(lev0._deflate._level === 0); + assert.ok(lev9._deflate._level === 9); + assert.ok(lev0._deflate._chunkSize === 256); + assert.ok(lev9._deflate._chunkSize === 128); + assert.ok(lev0._inflate._chunkSize === 2048); + assert.ok(lev9._inflate._chunkSize === 1024); + done(); + }); + }); + }); + }); + }); + + it("doesn't use contex takeover if not allowed", function (done) { + const perMessageDeflate = new PerMessageDeflate({ threshold: 0 }, true); + const extensions = extension.parse( + 'permessage-deflate;server_no_context_takeover' + ); + const buf = Buffer.from('foofoo'); + + perMessageDeflate.accept(extensions['permessage-deflate']); + + perMessageDeflate.compress(buf, true, (err, compressed1) => { + if (err) return done(err); + + perMessageDeflate.decompress(compressed1, true, (err, data) => { + if (err) return done(err); + + assert.ok(data.equals(buf)); + perMessageDeflate.compress(data, true, (err, compressed2) => { + if (err) return done(err); + + assert.strictEqual(compressed2.length, compressed1.length); + perMessageDeflate.decompress(compressed2, true, (err, data) => { + if (err) return done(err); + + assert.ok(data.equals(buf)); + done(); + }); + }); + }); + }); + }); + + it('uses contex takeover if allowed', function (done) { + const perMessageDeflate = new PerMessageDeflate({ threshold: 0 }, true); + const extensions = extension.parse('permessage-deflate'); + const buf = Buffer.from('foofoo'); + + perMessageDeflate.accept(extensions['permessage-deflate']); + + perMessageDeflate.compress(buf, true, (err, compressed1) => { + if (err) return done(err); + + perMessageDeflate.decompress(compressed1, true, (err, data) => { + if (err) return done(err); + + assert.ok(data.equals(buf)); + perMessageDeflate.compress(data, true, (err, compressed2) => { + if (err) return done(err); + + assert.ok(compressed2.length < compressed1.length); + perMessageDeflate.decompress(compressed2, true, (err, data) => { + if (err) return done(err); + + assert.ok(data.equals(buf)); + done(); + }); + }); + }); + }); + }); + + it('calls the callback when an error occurs (inflate)', function (done) { + const perMessageDeflate = new PerMessageDeflate({ threshold: 0 }); + const data = Buffer.from('something invalid'); + + perMessageDeflate.accept([{}]); + perMessageDeflate.decompress(data, true, (err) => { + assert.ok(err instanceof Error); + assert.strictEqual(err.errno, -3); + done(); + }); + }); + + it("doesn't call the callback twice when `maxPayload` is exceeded", function (done) { + const perMessageDeflate = new PerMessageDeflate({ threshold: 0 }, false, 25); + const buf = Buffer.from('A'.repeat(50)); + + perMessageDeflate.accept([{}]); + perMessageDeflate.compress(buf, true, (err, data) => { + if (err) return done(err); + + perMessageDeflate.decompress(data, true, (err) => { + assert.ok(err instanceof RangeError); + assert.strictEqual(err.message, 'Max payload size exceeded'); + done(); + }); + }); + }); + }); +}); diff --git a/test/receiver.test.js b/test/receiver.test.js new file mode 100644 index 000000000..21497e1ca --- /dev/null +++ b/test/receiver.test.js @@ -0,0 +1,905 @@ +'use strict'; + +const assert = require('assert'); +const crypto = require('crypto'); + +const PerMessageDeflate = require('../lib/permessage-deflate'); +const constants = require('../lib/constants'); +const Receiver = require('../lib/receiver'); +const Sender = require('../lib/sender'); + +const kStatusCode = constants.kStatusCode; + +describe('Receiver', function () { + it('parses an unmasked text message', function (done) { + const receiver = new Receiver(); + + receiver.on('message', (data) => { + assert.strictEqual(data, 'Hello'); + done(); + }); + + receiver.write(Buffer.from('810548656c6c6f', 'hex')); + }); + + it('parses a close message', function (done) { + const receiver = new Receiver(); + + receiver.on('conclude', (code, data) => { + assert.strictEqual(code, 1005); + assert.strictEqual(data, ''); + done(); + }); + + receiver.write(Buffer.from('8800', 'hex')); + }); + + it('parses a masked text message', function (done) { + const receiver = new Receiver(); + + receiver.on('message', (data) => { + assert.strictEqual(data, '5:::{"name":"echo"}'); + done(); + }); + + receiver.write( + Buffer.from('81933483a86801b992524fa1c60959e68a5216e6cb005ba1d5', 'hex') + ); + }); + + it('parses a masked text message longer than 125 B', function (done) { + const receiver = new Receiver(); + const msg = 'A'.repeat(200); + + const list = Sender.frame(Buffer.from(msg), { + fin: true, + rsv1: false, + opcode: 0x01, + mask: true, + readOnly: false + }); + + const frame = Buffer.concat(list); + + receiver.on('message', (data) => { + assert.strictEqual(data, msg); + done(); + }); + + receiver.write(frame.slice(0, 2)); + setImmediate(() => receiver.write(frame.slice(2))); + }); + + it('parses a really long masked text message', function (done) { + const receiver = new Receiver(); + const msg = 'A'.repeat(64 * 1024); + + const list = Sender.frame(Buffer.from(msg), { + fin: true, + rsv1: false, + opcode: 0x01, + mask: true, + readOnly: false + }); + + const frame = Buffer.concat(list); + + receiver.on('message', (data) => { + assert.strictEqual(data, msg); + done(); + }); + + receiver.write(frame); + }); + + it('parses a 300 B fragmented masked text message', function (done) { + const receiver = new Receiver(); + const msg = 'A'.repeat(300); + + const fragment1 = msg.substr(0, 150); + const fragment2 = msg.substr(150); + + const options = { rsv1: false, mask: true, readOnly: false }; + + const frame1 = Buffer.concat(Sender.frame( + Buffer.from(fragment1), + Object.assign({ fin: false, opcode: 0x01 }, options) + )); + const frame2 = Buffer.concat(Sender.frame( + Buffer.from(fragment2), + Object.assign({ fin: true, opcode: 0x00 }, options) + )); + + receiver.on('message', (data) => { + assert.strictEqual(data, msg); + done(); + }); + + receiver.write(frame1); + receiver.write(frame2); + }); + + it('parses a ping message', function (done) { + const receiver = new Receiver(); + const msg = 'Hello'; + + const list = Sender.frame(Buffer.from(msg), { + fin: true, + rsv1: false, + opcode: 0x09, + mask: true, + readOnly: false + }); + + const frame = Buffer.concat(list); + + receiver.on('ping', (data) => { + assert.strictEqual(data.toString(), msg); + done(); + }); + + receiver.write(frame); + }); + + it('parses a ping message with no data', function (done) { + const receiver = new Receiver(); + + receiver.on('ping', (data) => { + assert.ok(data.equals(Buffer.alloc(0))); + done(); + }); + + receiver.write(Buffer.from('8900', 'hex')); + }); + + it('parses a 300 B fragmented masked text message with a ping in the middle (1/2)', function (done) { + const receiver = new Receiver(); + const msg = 'A'.repeat(300); + const pingMessage = 'Hello'; + + const fragment1 = msg.substr(0, 150); + const fragment2 = msg.substr(150); + + const options = { rsv1: false, mask: true, readOnly: false }; + + const frame1 = Buffer.concat(Sender.frame( + Buffer.from(fragment1), + Object.assign({ fin: false, opcode: 0x01 }, options) + )); + const frame2 = Buffer.concat(Sender.frame( + Buffer.from(pingMessage), + Object.assign({ fin: true, opcode: 0x09 }, options) + )); + const frame3 = Buffer.concat(Sender.frame( + Buffer.from(fragment2), + Object.assign({ fin: true, opcode: 0x00 }, options) + )); + + let gotPing = false; + + receiver.on('message', (data) => { + assert.strictEqual(data, msg); + assert.ok(gotPing); + done(); + }); + receiver.on('ping', (data) => { + gotPing = true; + assert.strictEqual(data.toString(), pingMessage); + }); + + receiver.write(frame1); + receiver.write(frame2); + receiver.write(frame3); + }); + + it('parses a 300 B fragmented masked text message with a ping in the middle (2/2)', function (done) { + const receiver = new Receiver(); + const msg = 'A'.repeat(300); + const pingMessage = 'Hello'; + + const fragment1 = msg.substr(0, 150); + const fragment2 = msg.substr(150); + + const options = { rsv1: false, mask: true, readOnly: false }; + + const frame1 = Buffer.concat(Sender.frame( + Buffer.from(fragment1), + Object.assign({ fin: false, opcode: 0x01 }, options) + )); + const frame2 = Buffer.concat(Sender.frame( + Buffer.from(pingMessage), + Object.assign({ fin: true, opcode: 0x09 }, options) + )); + const frame3 = Buffer.concat(Sender.frame( + Buffer.from(fragment2), + Object.assign({ fin: true, opcode: 0x00 }, options) + )); + + let chunks = []; + const splitBuffer = (buf) => { + const i = Math.floor(buf.length / 2); + return [buf.slice(0, i), buf.slice(i)]; + }; + + chunks = chunks.concat(splitBuffer(frame1)); + chunks = chunks.concat(splitBuffer(frame2)); + chunks = chunks.concat(splitBuffer(frame3)); + + let gotPing = false; + + receiver.on('message', (data) => { + assert.strictEqual(data, msg); + assert.ok(gotPing); + done(); + }); + receiver.on('ping', (data) => { + gotPing = true; + assert.strictEqual(data.toString(), pingMessage); + }); + + for (let i = 0; i < chunks.length; ++i) { + receiver.write(chunks[i]); + } + }); + + it('parses a 100 B masked binary message', function (done) { + const receiver = new Receiver(); + const msg = crypto.randomBytes(100); + + const list = Sender.frame(msg, { + fin: true, + rsv1: false, + opcode: 0x02, + mask: true, + readOnly: true + }); + + const frame = Buffer.concat(list); + + receiver.on('message', (data) => { + assert.ok(data.equals(msg)); + done(); + }); + + receiver.write(frame); + }); + + it('parses a 256 B masked binary message', function (done) { + const receiver = new Receiver(); + const msg = crypto.randomBytes(256); + + const list = Sender.frame(msg, { + fin: true, + rsv1: false, + opcode: 0x02, + mask: true, + readOnly: true + }); + + const frame = Buffer.concat(list); + + receiver.on('message', (data) => { + assert.ok(data.equals(msg)); + done(); + }); + + receiver.write(frame); + }); + + it('parses a 200 KiB masked binary message', function (done) { + const receiver = new Receiver(); + const msg = crypto.randomBytes(200 * 1024); + + const list = Sender.frame(msg, { + fin: true, + rsv1: false, + opcode: 0x02, + mask: true, + readOnly: true + }); + + const frame = Buffer.concat(list); + + receiver.on('message', (data) => { + assert.ok(data.equals(msg)); + done(); + }); + + receiver.write(frame); + }); + + it('parses a 200 KiB unmasked binary message', function (done) { + const receiver = new Receiver(); + const msg = crypto.randomBytes(200 * 1024); + + const list = Sender.frame(msg, { + fin: true, + rsv1: false, + opcode: 0x02, + mask: false, + readOnly: true + }); + + const frame = Buffer.concat(list); + + receiver.on('message', (data) => { + assert.ok(data.equals(msg)); + done(); + }); + + receiver.write(frame); + }); + + it('parses a compressed message', function (done) { + const perMessageDeflate = new PerMessageDeflate(); + perMessageDeflate.accept([{}]); + + const receiver = new Receiver(undefined, { + 'permessage-deflate': perMessageDeflate + }); + const buf = Buffer.from('Hello'); + + receiver.on('message', (data) => { + assert.strictEqual(data, 'Hello'); + done(); + }); + + perMessageDeflate.compress(buf, true, (err, data) => { + if (err) return done(err); + + receiver.write(Buffer.from([0xc1, data.length])); + receiver.write(data); + }); + }); + + it('parses a compressed and fragmented message', function (done) { + const perMessageDeflate = new PerMessageDeflate(); + perMessageDeflate.accept([{}]); + + const receiver = new Receiver(undefined, { + 'permessage-deflate': perMessageDeflate + }); + const buf1 = Buffer.from('foo'); + const buf2 = Buffer.from('bar'); + + receiver.on('message', (data) => { + assert.strictEqual(data, 'foobar'); + done(); + }); + + perMessageDeflate.compress(buf1, false, function (err, fragment1) { + if (err) return done(err); + + receiver.write(Buffer.from([0x41, fragment1.length])); + receiver.write(fragment1); + + perMessageDeflate.compress(buf2, true, function (err, fragment2) { + if (err) return done(err); + + receiver.write(Buffer.from([0x80, fragment2.length])); + receiver.write(fragment2); + }); + }); + }); + + it('parses a buffer with thousands of frames', function (done) { + const buf = Buffer.allocUnsafe(40000); + + for (let i = 0; i < buf.length; i += 2) { + buf[i] = 0x81; + buf[i + 1] = 0x00; + } + + const receiver = new Receiver(); + let counter = 0; + + receiver.on('message', (data) => { + assert.strictEqual(data, ''); + if (++counter === 20000) done(); + }); + + receiver.write(buf); + }); + + it('resets `totalPayloadLength` only on final frame (unfragmented)', function (done) { + const receiver = new Receiver(undefined, {}, 10); + + receiver.on('message', (data) => { + assert.strictEqual(receiver._totalPayloadLength, 0); + assert.strictEqual(data, 'Hello'); + done(); + }); + + assert.strictEqual(receiver._totalPayloadLength, 0); + receiver.write(Buffer.from('810548656c6c6f', 'hex')); + }); + + it('resets `totalPayloadLength` only on final frame (fragmented)', function (done) { + const receiver = new Receiver(undefined, {}, 10); + + receiver.on('message', (data) => { + assert.strictEqual(receiver._totalPayloadLength, 0); + assert.strictEqual(data, 'Hello'); + done(); + }); + + assert.strictEqual(receiver._totalPayloadLength, 0); + receiver.write(Buffer.from('01024865', 'hex')); + assert.strictEqual(receiver._totalPayloadLength, 2); + receiver.write(Buffer.from('80036c6c6f', 'hex')); + }); + + it('resets `totalPayloadLength` only on final frame (fragmented + ping)', function (done) { + const receiver = new Receiver(undefined, {}, 10); + let data; + + receiver.on('ping', (buf) => { + assert.strictEqual(receiver._totalPayloadLength, 2); + data = buf.toString(); + }); + receiver.on('message', (buf) => { + assert.strictEqual(receiver._totalPayloadLength, 0); + assert.strictEqual(data, ''); + assert.strictEqual(buf.toString(), 'Hello'); + done(); + }); + + assert.strictEqual(receiver._totalPayloadLength, 0); + receiver.write(Buffer.from('02024865', 'hex')); + receiver.write(Buffer.from('8900', 'hex')); + receiver.write(Buffer.from('80036c6c6f', 'hex')); + }); + + it('ignores any data after a close frame', function (done) { + const perMessageDeflate = new PerMessageDeflate(); + perMessageDeflate.accept([{}]); + + const receiver = new Receiver(undefined, { + 'permessage-deflate': perMessageDeflate + }); + const results = []; + const push = results.push.bind(results); + + receiver.on('conclude', push).on('message', push); + receiver.on('finish', () => { + assert.deepStrictEqual(results, ['', 1005, '']); + done(); + }); + + receiver.write(Buffer.from([0xc1, 0x01, 0x00])); + receiver.write(Buffer.from([0x88, 0x00])); + receiver.write(Buffer.from([0x81, 0x00])); + }); + + it('emits an error if RSV1 is on and permessage-deflate is disabled', function (done) { + const receiver = new Receiver(); + + receiver.on('error', (err) => { + assert.ok(err instanceof RangeError); + assert.strictEqual( + err.message, + 'Invalid WebSocket frame: RSV1 must be clear' + ); + assert.strictEqual(err[kStatusCode], 1002); + done(); + }); + + receiver.write(Buffer.from([0xc2, 0x80, 0x00, 0x00, 0x00, 0x00])); + }); + + it('emits an error if RSV1 is on and opcode is 0', function (done) { + const perMessageDeflate = new PerMessageDeflate(); + perMessageDeflate.accept([{}]); + + const receiver = new Receiver(undefined, { + 'permessage-deflate': perMessageDeflate + }); + + receiver.on('error', (err) => { + assert.ok(err instanceof RangeError); + assert.strictEqual( + err.message, + 'Invalid WebSocket frame: RSV1 must be clear' + ); + assert.strictEqual(err[kStatusCode], 1002); + done(); + }); + + receiver.write(Buffer.from([0x40, 0x00])); + }); + + it('emits an error if RSV2 is on', function (done) { + const receiver = new Receiver(); + + receiver.on('error', (err) => { + assert.ok(err instanceof RangeError); + assert.strictEqual( + err.message, + 'Invalid WebSocket frame: RSV2 and RSV3 must be clear' + ); + assert.strictEqual(err[kStatusCode], 1002); + done(); + }); + + receiver.write(Buffer.from([0xa2, 0x00])); + }); + + it('emits an error if RSV3 is on', function (done) { + const receiver = new Receiver(); + + receiver.on('error', (err) => { + assert.ok(err instanceof RangeError); + assert.strictEqual( + err.message, + 'Invalid WebSocket frame: RSV2 and RSV3 must be clear' + ); + assert.strictEqual(err[kStatusCode], 1002); + done(); + }); + + receiver.write(Buffer.from([0x92, 0x00])); + }); + + it('emits an error if the first frame in a fragmented message has opcode 0', function (done) { + const receiver = new Receiver(); + + receiver.on('error', (err) => { + assert.ok(err instanceof RangeError); + assert.strictEqual( + err.message, + 'Invalid WebSocket frame: invalid opcode 0' + ); + assert.strictEqual(err[kStatusCode], 1002); + done(); + }); + + receiver.write(Buffer.from([0x00, 0x00])); + }); + + it('emits an error if a frame has opcode 1 in the middle of a fragmented message', function (done) { + const receiver = new Receiver(); + + receiver.on('error', (err) => { + assert.ok(err instanceof RangeError); + assert.strictEqual( + err.message, + 'Invalid WebSocket frame: invalid opcode 1' + ); + assert.strictEqual(err[kStatusCode], 1002); + done(); + }); + + receiver.write(Buffer.from([0x01, 0x00])); + receiver.write(Buffer.from([0x01, 0x00])); + }); + + it('emits an error if a frame has opcode 2 in the middle of a fragmented message', function (done) { + const receiver = new Receiver(); + + receiver.on('error', (err) => { + assert.ok(err instanceof RangeError); + assert.strictEqual( + err.message, + 'Invalid WebSocket frame: invalid opcode 2' + ); + assert.strictEqual(err[kStatusCode], 1002); + done(); + }); + + receiver.write(Buffer.from([0x01, 0x00])); + receiver.write(Buffer.from([0x02, 0x00])); + }); + + it('emits an error if a control frame has the FIN bit off', function (done) { + const receiver = new Receiver(); + + receiver.on('error', (err) => { + assert.ok(err instanceof RangeError); + assert.strictEqual( + err.message, + 'Invalid WebSocket frame: FIN must be set' + ); + assert.strictEqual(err[kStatusCode], 1002); + done(); + }); + + receiver.write(Buffer.from([0x09, 0x00])); + }); + + it('emits an error if a control frame has the RSV1 bit on', function (done) { + const perMessageDeflate = new PerMessageDeflate(); + perMessageDeflate.accept([{}]); + + const receiver = new Receiver(undefined, { + 'permessage-deflate': perMessageDeflate + }); + + receiver.on('error', (err) => { + assert.ok(err instanceof RangeError); + assert.strictEqual( + err.message, + 'Invalid WebSocket frame: RSV1 must be clear' + ); + assert.strictEqual(err[kStatusCode], 1002); + done(); + }); + + receiver.write(Buffer.from([0xc9, 0x00])); + }); + + it('emits an error if a control frame has the FIN bit off', function (done) { + const receiver = new Receiver(); + + receiver.on('error', (err) => { + assert.ok(err instanceof RangeError); + assert.strictEqual( + err.message, + 'Invalid WebSocket frame: FIN must be set' + ); + assert.strictEqual(err[kStatusCode], 1002); + done(); + }); + + receiver.write(Buffer.from([0x09, 0x00])); + }); + + it('emits an error if a control frame has a payload bigger than 125 B', function (done) { + const receiver = new Receiver(); + + receiver.on('error', (err) => { + assert.ok(err instanceof RangeError); + assert.strictEqual( + err.message, + 'Invalid WebSocket frame: invalid payload length 126' + ); + assert.strictEqual(err[kStatusCode], 1002); + done(); + }); + + receiver.write(Buffer.from([0x89, 0x7e])); + }); + + it('emits an error if a data frame has a payload bigger than 2^53 - 1 B', function (done) { + const receiver = new Receiver(); + + receiver.on('error', (err) => { + assert.ok(err instanceof RangeError); + assert.strictEqual( + err.message, + 'Unsupported WebSocket frame: payload length > 2^53 - 1' + ); + assert.strictEqual(err[kStatusCode], 1009); + done(); + }); + + receiver.write(Buffer.from([0x82, 0x7f])); + setImmediate(() => receiver.write(Buffer.from([ + 0x00, 0x20, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00 + ]))); + }); + + it('emits an error if a text frame contains invalid UTF-8 data', function (done) { + const receiver = new Receiver(); + + receiver.on('error', (err) => { + assert.ok(err instanceof Error); + assert.strictEqual( + err.message, + 'Invalid WebSocket frame: invalid UTF-8 sequence' + ); + assert.strictEqual(err[kStatusCode], 1007); + done(); + }); + + receiver.write(Buffer.from([0x81, 0x04, 0xce, 0xba, 0xe1, 0xbd])); + }); + + it('emits an error if a close frame has a payload of 1 B', function (done) { + const receiver = new Receiver(); + + receiver.on('error', (err) => { + assert.ok(err instanceof RangeError); + assert.strictEqual( + err.message, + 'Invalid WebSocket frame: invalid payload length 1' + ); + assert.strictEqual(err[kStatusCode], 1002); + done(); + }); + + receiver.write(Buffer.from([0x88, 0x01, 0x00])); + }); + + it('emits an error if a close frame contains an invalid close code', function (done) { + const receiver = new Receiver(); + + receiver.on('error', (err) => { + assert.ok(err instanceof RangeError); + assert.strictEqual( + err.message, + 'Invalid WebSocket frame: invalid status code 0' + ); + assert.strictEqual(err[kStatusCode], 1002); + done(); + }); + + receiver.write(Buffer.from([0x88, 0x02, 0x00, 0x00])); + }); + + it('emits an error if a close frame contains invalid UTF-8 data', function (done) { + const receiver = new Receiver(); + + receiver.on('error', (err) => { + assert.ok(err instanceof Error); + assert.strictEqual( + err.message, + 'Invalid WebSocket frame: invalid UTF-8 sequence' + ); + assert.strictEqual(err[kStatusCode], 1007); + done(); + }); + + receiver.write( + Buffer.from([0x88, 0x06, 0x03, 0xef, 0xce, 0xba, 0xe1, 0xbd]) + ); + }); + + it('emits an error if a frame payload length is bigger than `maxPayload`', function (done) { + const receiver = new Receiver(undefined, {}, 20 * 1024); + const msg = crypto.randomBytes(200 * 1024); + + const list = Sender.frame(msg, { + fin: true, + rsv1: false, + opcode: 0x02, + mask: true, + readOnly: true + }); + + const frame = Buffer.concat(list); + + receiver.on('error', (err) => { + assert.ok(err instanceof RangeError); + assert.strictEqual(err.message, 'Max payload size exceeded'); + assert.strictEqual(err[kStatusCode], 1009); + done(); + }); + + receiver.write(frame); + }); + + it('emits an error if the message length exceeds `maxPayload`', function (done) { + const perMessageDeflate = new PerMessageDeflate({}, false, 25); + perMessageDeflate.accept([{}]); + + const receiver = new Receiver(undefined, { + 'permessage-deflate': perMessageDeflate + }, 25); + const buf = Buffer.from('A'.repeat(50)); + + receiver.on('error', (err) => { + assert.ok(err instanceof RangeError); + assert.strictEqual(err.message, 'Max payload size exceeded'); + assert.strictEqual(err[kStatusCode], 1009); + done(); + }); + + perMessageDeflate.compress(buf, true, function (err, data) { + if (err) return done(err); + + receiver.write(Buffer.from([0xc1, data.length])); + receiver.write(data); + }); + }); + + it('emits an error if the sum of fragment lengths exceeds `maxPayload`', function (done) { + const perMessageDeflate = new PerMessageDeflate({}, false, 25); + perMessageDeflate.accept([{}]); + + const receiver = new Receiver(undefined, { + 'permessage-deflate': perMessageDeflate + }, 25); + const buf = Buffer.from('A'.repeat(15)); + + receiver.on('error', (err) => { + assert.ok(err instanceof RangeError); + assert.strictEqual(err.message, 'Max payload size exceeded'); + assert.strictEqual(err[kStatusCode], 1009); + done(); + }); + + perMessageDeflate.compress(buf, false, function (err, fragment1) { + if (err) return done(err); + + receiver.write(Buffer.from([0x41, fragment1.length])); + receiver.write(fragment1); + + perMessageDeflate.compress(buf, true, function (err, fragment2) { + if (err) return done(err); + + receiver.write(Buffer.from([0x80, fragment2.length])); + receiver.write(fragment2); + }); + }); + }); + + it("honors the 'nodebuffer' binary type", function (done) { + const receiver = new Receiver(); + const frags = [ + crypto.randomBytes(7321), + crypto.randomBytes(137), + crypto.randomBytes(285787), + crypto.randomBytes(3) + ]; + + receiver.on('message', (data) => { + assert.ok(Buffer.isBuffer(data)); + assert.ok(data.equals(Buffer.concat(frags))); + done(); + }); + + frags.forEach((frag, i) => { + Sender.frame(frag, { + fin: i === frags.length - 1, + opcode: i === 0 ? 2 : 0, + readOnly: true, + mask: false, + rsv1: false + }).forEach((buf) => receiver.write(buf)); + }); + }); + + it("honors the 'arraybuffer' binary type", function (done) { + const receiver = new Receiver(); + const frags = [ + crypto.randomBytes(19221), + crypto.randomBytes(954), + crypto.randomBytes(623987) + ]; + + receiver._binaryType = 'arraybuffer'; + receiver.on('message', (data) => { + assert.ok(data instanceof ArrayBuffer); + assert.ok(Buffer.from(data).equals(Buffer.concat(frags))); + done(); + }); + + frags.forEach((frag, i) => { + Sender.frame(frag, { + fin: i === frags.length - 1, + opcode: i === 0 ? 2 : 0, + readOnly: true, + mask: false, + rsv1: false + }).forEach((buf) => receiver.write(buf)); + }); + }); + + it("honors the 'fragments' binary type", function (done) { + const receiver = new Receiver(); + const frags = [ + crypto.randomBytes(17), + crypto.randomBytes(419872), + crypto.randomBytes(83), + crypto.randomBytes(9928), + crypto.randomBytes(1) + ]; + + receiver._binaryType = 'fragments'; + receiver.on('message', (data) => { + assert.deepStrictEqual(data, frags); + done(); + }); + + frags.forEach((frag, i) => { + Sender.frame(frag, { + fin: i === frags.length - 1, + opcode: i === 0 ? 2 : 0, + readOnly: true, + mask: false, + rsv1: false + }).forEach((buf) => receiver.write(buf)); + }); + }); +}); diff --git a/test/sender.test.js b/test/sender.test.js new file mode 100644 index 000000000..623ce5954 --- /dev/null +++ b/test/sender.test.js @@ -0,0 +1,273 @@ +'use strict'; + +const assert = require('assert'); + +const PerMessageDeflate = require('../lib/permessage-deflate'); +const Sender = require('../lib/sender'); + +describe('Sender', function () { + describe('.frame', function () { + it('does not mutate the input buffer if data is `readOnly`', function () { + const buf = Buffer.from([1, 2, 3, 4, 5]); + + Sender.frame(buf, { + readOnly: true, + rsv1: false, + mask: true, + opcode: 2, + fin: true + }); + + assert.ok(buf.equals(Buffer.from([1, 2, 3, 4, 5]))); + }); + + it('sets RSV1 bit if compressed', function () { + const list = Sender.frame(Buffer.from('hi'), { + readOnly: false, + mask: false, + rsv1: true, + opcode: 1, + fin: true + }); + + assert.strictEqual(list[0][0] & 0x40, 0x40); + }); + }); + + describe('#send', function () { + it('compresses data if compress option is enabled', function (done) { + const perMessageDeflate = new PerMessageDeflate({ threshold: 0 }); + let count = 0; + const sender = new Sender({ + write: (data) => { + assert.strictEqual(data[0] & 0x40, 0x40); + if (++count === 3) done(); + } + }, { + 'permessage-deflate': perMessageDeflate + }); + + perMessageDeflate.accept([{}]); + + const options = { compress: true, fin: true }; + const array = new Uint8Array([0x68, 0x69]); + + sender.send(array.buffer, options); + sender.send(array, options); + sender.send('hi', options); + }); + + it('does not compress data for small payloads', function (done) { + const perMessageDeflate = new PerMessageDeflate(); + const sender = new Sender({ + write: (data) => { + assert.notStrictEqual(data[0] & 0x40, 0x40); + done(); + } + }, { + 'permessage-deflate': perMessageDeflate + }); + + perMessageDeflate.accept([{}]); + + sender.send('hi', { compress: true, fin: true }); + }); + + it('compresses all frames in a fragmented message', function (done) { + const fragments = []; + const perMessageDeflate = new PerMessageDeflate({ threshold: 3 }); + const sender = new Sender({ + write: (data) => { + fragments.push(data); + if (fragments.length !== 2) return; + + assert.strictEqual(fragments[0][0] & 0x40, 0x40); + assert.strictEqual(fragments[0].length, 11); + assert.strictEqual(fragments[1][0] & 0x40, 0x00); + assert.strictEqual(fragments[1].length, 6); + done(); + } + }, { + 'permessage-deflate': perMessageDeflate + }); + + perMessageDeflate.accept([{}]); + + sender.send('123', { compress: true, fin: false }); + sender.send('12', { compress: true, fin: true }); + }); + + it('compresses no frames in a fragmented message', function (done) { + const fragments = []; + const perMessageDeflate = new PerMessageDeflate({ threshold: 3 }); + const sender = new Sender({ + write: (data) => { + fragments.push(data); + if (fragments.length !== 2) return; + + assert.strictEqual(fragments[0][0] & 0x40, 0x00); + assert.strictEqual(fragments[0].length, 4); + assert.strictEqual(fragments[1][0] & 0x40, 0x00); + assert.strictEqual(fragments[1].length, 5); + done(); + } + }, { + 'permessage-deflate': perMessageDeflate + }); + + perMessageDeflate.accept([{}]); + + sender.send('12', { compress: true, fin: false }); + sender.send('123', { compress: true, fin: true }); + }); + + it('compresses empty buffer as first fragment', function (done) { + const fragments = []; + const perMessageDeflate = new PerMessageDeflate({ threshold: 0 }); + const sender = new Sender({ + write: (data) => { + fragments.push(data); + if (fragments.length !== 2) return; + + assert.strictEqual(fragments[0][0] & 0x40, 0x40); + assert.strictEqual(fragments[0].length, 3); + assert.strictEqual(fragments[1][0] & 0x40, 0x00); + assert.strictEqual(fragments[1].length, 8); + done(); + } + }, { + 'permessage-deflate': perMessageDeflate + }); + + perMessageDeflate.accept([{}]); + + sender.send(Buffer.alloc(0), { compress: true, fin: false }); + sender.send('data', { compress: true, fin: true }); + }); + + it('compresses empty buffer as last fragment', function (done) { + const fragments = []; + const perMessageDeflate = new PerMessageDeflate({ threshold: 0 }); + const sender = new Sender({ + write: (data) => { + fragments.push(data); + if (fragments.length !== 2) return; + + assert.strictEqual(fragments[0][0] & 0x40, 0x40); + assert.strictEqual(fragments[0].length, 12); + assert.strictEqual(fragments[1][0] & 0x40, 0x00); + assert.strictEqual(fragments[1].length, 3); + done(); + } + }, { + 'permessage-deflate': perMessageDeflate + }); + + perMessageDeflate.accept([{}]); + + sender.send('data', { compress: true, fin: false }); + sender.send(Buffer.alloc(0), { compress: true, fin: true }); + }); + + it('handles many send calls while processing without crashing on flush', function (done) { + let count = 0; + const perMessageDeflate = new PerMessageDeflate(); + const sender = new Sender({ + write: () => { + if (++count > 1e4) done(); + } + }, { + 'permessage-deflate': perMessageDeflate + }); + + perMessageDeflate.accept([{}]); + + for (let i = 0; i < 1e4; i++) { + sender.processing = true; + sender.send('hi', { compress: false, fin: true }); + } + + sender.processing = false; + sender.send('hi', { compress: false, fin: true }); + }); + }); + + describe('#ping', function () { + it('works with multiple types of data', function (done) { + const perMessageDeflate = new PerMessageDeflate({ threshold: 0 }); + let count = 0; + const sender = new Sender({ + write: (data) => { + if (++count === 1) return; + + assert.ok(data.equals(Buffer.from([0x89, 0x02, 0x68, 0x69]))); + if (count === 4) done(); + } + }, { + 'permessage-deflate': perMessageDeflate + }); + + perMessageDeflate.accept([{}]); + + const array = new Uint8Array([0x68, 0x69]); + + sender.send('foo', { compress: true, fin: true }); + sender.ping(array.buffer, false); + sender.ping(array, false); + sender.ping('hi', false); + }); + }); + + describe('#pong', function () { + it('works with multiple types of data', function (done) { + const perMessageDeflate = new PerMessageDeflate({ threshold: 0 }); + let count = 0; + const sender = new Sender({ + write: (data) => { + if (++count === 1) return; + + assert.ok(data.equals(Buffer.from([0x8a, 0x02, 0x68, 0x69]))); + if (count === 4) done(); + } + }, { + 'permessage-deflate': perMessageDeflate + }); + + perMessageDeflate.accept([{}]); + + const array = new Uint8Array([0x68, 0x69]); + + sender.send('foo', { compress: true, fin: true }); + sender.pong(array.buffer, false); + sender.pong(array, false); + sender.pong('hi', false); + }); + }); + + describe('#close', function () { + it('should consume all data before closing', function (done) { + const perMessageDeflate = new PerMessageDeflate({ threshold: 0 }); + + let count = 0; + const sender = new Sender({ + write: (data, cb) => { + count++; + if (cb) cb(); + } + }, { + 'permessage-deflate': perMessageDeflate + }); + + perMessageDeflate.accept([{}]); + + sender.send('foo', { compress: true, fin: true }); + sender.send('bar', { compress: true, fin: true }); + sender.send('baz', { compress: true, fin: true }); + + sender.close(1000, undefined, false, () => { + assert.strictEqual(count, 4); + done(); + }); + }); + }); +}); diff --git a/test/testserver.js b/test/testserver.js deleted file mode 100644 index e17cbb8ea..000000000 --- a/test/testserver.js +++ /dev/null @@ -1,184 +0,0 @@ -var http = require('http') - , util = require('util') - , crypto = require('crypto') - , events = require('events') - , Sender = require('../lib/Sender') - , Receiver = require('../lib/Receiver'); - -module.exports = { - handlers: { - valid: validServer, - invalidKey: invalidRequestHandler, - closeAfterConnect: closeAfterConnectHandler, - return401: return401 - }, - createServer: function(port, handler, cb) { - if (handler && !cb) { - cb = handler; - handler = null; - } - var webServer = http.createServer(function (req, res) { - res.writeHead(200, {'Content-Type': 'text/plain'}); - res.end('okay'); - }); - var srv = new Server(webServer); - webServer.on('upgrade', function(req, socket) { - webServer._socket = socket; - (handler || validServer)(srv, req, socket); - }); - webServer.listen(port, '127.0.0.1', function() { cb(srv); }); - } -}; - -/** - * Test strategies - */ - -function validServer(server, req, socket) { - if (typeof req.headers.upgrade === 'undefined' || - req.headers.upgrade.toLowerCase() !== 'websocket') { - throw new Error('invalid headers'); - return; - } - - if (!req.headers['sec-websocket-key']) { - socket.end(); - throw new Error('websocket key is missing'); - } - - // calc key - var key = req.headers['sec-websocket-key']; - var shasum = crypto.createHash('sha1'); - shasum.update(key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"); - key = shasum.digest('base64'); - - var headers = [ - 'HTTP/1.1 101 Switching Protocols' - , 'Upgrade: websocket' - , 'Connection: Upgrade' - , 'Sec-WebSocket-Accept: ' + key - ]; - - socket.write(headers.concat('', '').join('\r\n')); - socket.setTimeout(0); - socket.setNoDelay(true); - - var sender = new Sender(socket); - var receiver = new Receiver(); - receiver.ontext = function (message, flags) { - server.emit('message', message, flags); - sender.send(message); - }; - receiver.onbinary = function (message, flags) { - flags = flags || {}; - flags.binary = true; - server.emit('message', message, flags); - sender.send(message, {binary: true}); - }; - receiver.onping = function (message, flags) { - flags = flags || {}; - server.emit('ping', message, flags); - }; - receiver.onpong = function (message, flags) { - flags = flags || {}; - server.emit('pong', message, flags); - }; - receiver.onclose = function (code, message, flags) { - flags = flags || {}; - sender.close(code, message, false, function(err) { - server.emit('close', code, message, flags); - socket.end(); - }); - }; - socket.on('data', function (data) { - receiver.add(data); - }); - socket.on('end', function() { - socket.end(); - }); -} - -function invalidRequestHandler(server, req, socket) { - if (typeof req.headers.upgrade === 'undefined' || - req.headers.upgrade.toLowerCase() !== 'websocket') { - throw new Error('invalid headers'); - return; - } - - if (!req.headers['sec-websocket-key']) { - socket.end(); - throw new Error('websocket key is missing'); - } - - // calc key - var key = req.headers['sec-websocket-key']; - var shasum = crypto.createHash('sha1'); - shasum.update(key + "bogus"); - key = shasum.digest('base64'); - - var headers = [ - 'HTTP/1.1 101 Switching Protocols' - , 'Upgrade: websocket' - , 'Connection: Upgrade' - , 'Sec-WebSocket-Accept: ' + key - ]; - - socket.write(headers.concat('', '').join('\r\n')); - socket.end(); -} - -function closeAfterConnectHandler(server, req, socket) { - if (typeof req.headers.upgrade === 'undefined' || - req.headers.upgrade.toLowerCase() !== 'websocket') { - throw new Error('invalid headers'); - return; - } - - if (!req.headers['sec-websocket-key']) { - socket.end(); - throw new Error('websocket key is missing'); - } - - // calc key - var key = req.headers['sec-websocket-key']; - var shasum = crypto.createHash('sha1'); - shasum.update(key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"); - key = shasum.digest('base64'); - - var headers = [ - 'HTTP/1.1 101 Switching Protocols' - , 'Upgrade: websocket' - , 'Connection: Upgrade' - , 'Sec-WebSocket-Accept: ' + key - ]; - - socket.write(headers.concat('', '').join('\r\n')); - socket.end(); -} - - -function return401(server, req, socket) { - var headers = [ - 'HTTP/1.1 401 Unauthorized' - , 'Content-type: text/html' - ]; - - socket.write(headers.concat('', '').join('\r\n')); - socket.write('Not allowed!'); - socket.end(); -} - -/** - * Server object, which will do the actual emitting - */ - -function Server(webServer) { - this.webServer = webServer; -} - -util.inherits(Server, events.EventEmitter); - -Server.prototype.close = function() { - this.webServer.close(); - if (this._socket) this._socket.end(); -} diff --git a/test/websocket-server.test.js b/test/websocket-server.test.js new file mode 100644 index 000000000..45d287978 --- /dev/null +++ b/test/websocket-server.test.js @@ -0,0 +1,745 @@ +/* eslint no-unused-vars: ["error", { "varsIgnorePattern": "^ws$", "args": "none" }] */ + +'use strict'; + +const assert = require('assert'); +const crypto = require('crypto'); +const https = require('https'); +const http = require('http'); +const net = require('net'); +const fs = require('fs'); + +const WebSocket = require('..'); + +describe('WebSocketServer', function () { + describe('#ctor', function () { + it('throws an error if no option object is passed', function () { + assert.throws(() => new WebSocket.Server()); + }); + + it('throws an error if no port or server is specified', function () { + assert.throws(() => new WebSocket.Server({})); + }); + + describe('options', function () { + it('exposes options passed to constructor', function (done) { + const wss = new WebSocket.Server({ port: 0 }, () => { + assert.strictEqual(wss.options.port, 0); + wss.close(done); + }); + }); + + it('accepts the `maxPayload` option', function (done) { + const maxPayload = 20480; + const wss = new WebSocket.Server({ + perMessageDeflate: true, + maxPayload, + port: 0 + }, () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + }); + + wss.on('connection', (ws) => { + assert.strictEqual(ws._receiver._maxPayload, maxPayload); + assert.strictEqual( + ws._receiver._extensions['permessage-deflate']._maxPayload, + maxPayload + ); + wss.close(done); + }); + }); + }); + + it('emits an error if http server bind fails', function (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', function (done) { + const port = 1337; + const wss = new WebSocket.Server({ port }, () => { + const ws = new WebSocket(`ws://localhost:${port}`); + }); + + wss.on('connection', (client) => wss.close(done)); + }); + + it('binds the server on any IPv6 address when available', function (done) { + const wss = new WebSocket.Server({ port: 0 }, () => { + assert.strictEqual(wss._server.address().address, '::'); + wss.close(done); + }); + }); + + it('uses a precreated http server', function (done) { + const server = http.createServer(); + + server.listen(0, () => { + const wss = new WebSocket.Server({ server }); + const ws = new WebSocket(`ws://localhost:${server.address().port}`); + + wss.on('connection', (client) => { + wss.close(); + server.close(done); + }); + }); + }); + + it('426s for non-Upgrade requests', function (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 as it throws errors for obvious reasons. + // + if (process.platform === 'win32') return this.skip(); + + const server = http.createServer(); + const sockPath = `/tmp/ws.${crypto.randomBytes(16).toString('hex')}.socket`; + + 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, '/'); + wss.close(); + server.close(done); + } + }); + + const ws = new WebSocket(`ws+unix://${sockPath}:/foo?bar=bar`); + ws.on('open', () => new WebSocket(`ws+unix://${sockPath}`)); + }); + }); + }); + + describe('#address', function () { + it('returns the address of the server', function (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', function () { + 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', function (done) { + const wss = new WebSocket.Server({ port: 0 }, () => { + wss.close(() => { + assert.strictEqual(wss.address(), null); + done(); + }); + }); + }); + }); + + describe('#close', function () { + it('does not throw when called twice', function (done) { + const wss = new WebSocket.Server({ port: 0 }, () => { + wss.close(); + wss.close(); + wss.close(); + + done(); + }); + }); + + it('closes all clients', function (done) { + let closes = 0; + const wss = new WebSocket.Server({ port: 0 }, () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + ws.on('close', () => { + if (++closes === 2) done(); + }); + }); + + wss.on('connection', (ws) => { + ws.on('close', () => { + if (++closes === 2) done(); + }); + wss.close(); + }); + }); + + it("doesn't close a precreated server", function (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', (ws) => { + wss.close(); + server.close = realClose; + server.close(done); + }); + + server.listen(0, () => { + const ws = new WebSocket(`ws://localhost:${server.address().port}`); + }); + }); + + it('invokes the callback in noServer mode', function (done) { + const wss = new WebSocket.Server({ noServer: true }); + + wss.close(done); + }); + + it('cleans event handlers on precreated server', function (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); + }); + }); + }); + }); + + describe('#clients', function () { + it('returns a list of connected clients', function (done) { + const wss = new WebSocket.Server({ port: 0 }, () => { + assert.strictEqual(wss.clients.size, 0); + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + }); + + wss.on('connection', (ws) => { + assert.strictEqual(wss.clients.size, 1); + wss.close(done); + }); + }); + + it('can be disabled', function (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', function (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', function (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', function () { + it('returns true when the path matches', function () { + const wss = new WebSocket.Server({ noServer: true, path: '/foo' }); + + assert.strictEqual(wss.shouldHandle({ url: '/foo' }), true); + }); + + it("returns false when the path doesn't match", function () { + const wss = new WebSocket.Server({ noServer: true, path: '/foo' }); + + assert.strictEqual(wss.shouldHandle({ url: '/bar' }), false); + }); + }); + + describe('#handleUpgrade', function () { + it('can be used for a pre-existing server', function (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, (client) => client.send('hello')); + }); + + const ws = new WebSocket(`ws://localhost:${server.address().port}`); + + ws.on('message', (message) => { + assert.strictEqual(message, 'hello'); + wss.close(); + server.close(done); + }); + }); + }); + + it("closes the connection when path doesn't match", function (done) { + const wss = new WebSocket.Server({ port: 0, path: '/ws' }, () => { + const req = http.get({ + port: wss.address().port, + headers: { + 'Connection': 'Upgrade', + 'Upgrade': 'websocket' + } + }); + + req.on('response', (res) => { + assert.strictEqual(res.statusCode, 400); + wss.close(done); + }); + }); + }); + + it('closes the connection when protocol version is Hixie-76', function (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); + wss.close(done); + }); + }); + }); + }); + + describe('Connection establishing', function () { + it('fails if the Sec-WebSocket-Key header is invalid', function (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); + wss.close(done); + }); + }); + + wss.on('connection', (ws) => { + done(new Error("Unexpected 'connection' event")); + }); + }); + + it('fails is the Sec-WebSocket-Version header is invalid (1/2)', function (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); + wss.close(done); + }); + }); + + wss.on('connection', (ws) => { + done(new Error("Unexpected 'connection' event")); + }); + }); + + it('fails is the Sec-WebSocket-Version header is invalid (2/2)', function (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); + wss.close(done); + }); + }); + + wss.on('connection', (ws) => { + done(new Error("Unexpected 'connection' event")); + }); + }); + + it('fails is the Sec-WebSocket-Extensions header is invalid', function (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); + wss.close(done); + }); + }); + + wss.on('connection', (ws) => { + done(new Error("Unexpected 'connection' event")); + }); + }); + + describe('`verifyClient`', function () { + it('can reject client synchronously', function (done) { + const wss = new WebSocket.Server({ + verifyClient: (info) => 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', (ws) => { + done(new Error("Unexpected 'connection' event")); + }); + }); + + it('can accept client synchronously', function (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', (ws) => { + wss.close(); + server.close(done); + }); + + server.listen(0, () => { + const ws = new WebSocket(`wss://localhost:${server.address().port}`, { + headers: { Origin: 'https://example.com', foo: 'bar' }, + rejectUnauthorized: false + }); + }); + }); + + it('can accept client asynchronously', function (done) { + const wss = new WebSocket.Server({ + verifyClient: (o, cb) => process.nextTick(cb, true), + port: 0 + }, () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + }); + + wss.on('connection', (ws) => wss.close(done)); + }); + + it('can reject client asynchronously', function (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', (ws) => { + done(new Error("Unexpected 'connection' event")); + }); + }); + + it('can reject client asynchronously w/ status code', function (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', (ws) => { + done(new Error("Unexpected 'connection' event")); + }); + }); + + it('can reject client asynchronously w/ custom headers', function (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', (ws) => { + done(new Error("Unexpected 'connection' event")); + }); + }); + }); + + it("doesn't emit the 'connection' event if socket is closed prematurely", function (done) { + const server = http.createServer(); + + server.listen(0, () => { + const wss = new WebSocket.Server({ + verifyClient: (o, cb) => setTimeout(cb, 100, true), + server + }); + + wss.on('connection', () => { + done(new Error("Unexpected 'connection' event")); + }); + + const socket = net.connect({ + port: server.address().port, + allowHalfOpen: true + }, () => { + socket.write([ + '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); + }); + + socket.setTimeout(50, () => socket.end()); + }); + }); + + it('handles data passed along with the upgrade request', function (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 + } + }); + + req.write(Buffer.from([0x81, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f])); + req.end(); + }); + + wss.on('connection', (ws) => { + ws.on('message', (data) => { + assert.strictEqual(data, 'Hello'); + wss.close(done); + }); + }); + }); + + describe('`handleProtocols`', function () { + it('allows to select a subprotocol', function (done) { + const handleProtocols = (protocols, request) => { + assert.ok(request instanceof http.IncomingMessage); + assert.strictEqual(request.url, '/'); + return 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); + }); + }); + }); + }); + + it('emits the `headers` event', function (done) { + const wss = new WebSocket.Server({ port: 0 }, () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + + 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', function () { + it('is disabled by default', function (done) { + const wss = new WebSocket.Server({ port: 0 }, () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + }); + + 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', function (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); + }); + }); + }); + }); +}); diff --git a/test/websocket.integration.js b/test/websocket.integration.js new file mode 100644 index 000000000..08810cfc9 --- /dev/null +++ b/test/websocket.integration.js @@ -0,0 +1,49 @@ +'use strict'; + +const assert = require('assert'); + +const WebSocket = require('..'); + +describe('WebSocket', function () { + it('communicates successfully with echo service (ws)', function (done) { + const ws = new WebSocket('ws://echo.websocket.org/', { + origin: 'http://www.websocket.org', + protocolVersion: 13 + }); + const str = Date.now().toString(); + + let dataReceived = false; + + ws.on('open', () => ws.send(str)); + ws.on('close', () => { + assert.ok(dataReceived); + done(); + }); + ws.on('message', (data) => { + dataReceived = true; + assert.strictEqual(data, str); + ws.close(); + }); + }); + + it('communicates successfully with echo service (wss)', function (done) { + const ws = new WebSocket('wss://echo.websocket.org/', { + origin: 'https://www.websocket.org', + protocolVersion: 13 + }); + const str = Date.now().toString(); + + let dataReceived = false; + + ws.on('open', () => ws.send(str)); + ws.on('close', () => { + assert.ok(dataReceived); + done(); + }); + ws.on('message', (data) => { + dataReceived = true; + assert.strictEqual(data, str); + ws.close(); + }); + }); +}); diff --git a/test/websocket.test.js b/test/websocket.test.js new file mode 100644 index 000000000..2807e59fb --- /dev/null +++ b/test/websocket.test.js @@ -0,0 +1,2089 @@ +/* eslint no-unused-vars: ["error", { "varsIgnorePattern": "^ws$", "args": "none" }] */ + +'use strict'; + +const assert = require('assert'); +const crypto = require('crypto'); +const https = require('https'); +const http = require('http'); +const url = require('url'); +const fs = require('fs'); + +const constants = require('../lib/constants'); +const WebSocket = require('..'); + +class CustomAgent extends http.Agent { + addRequest () {} +} + +describe('WebSocket', function () { + describe('#ctor', function () { + it('throws an error when using an invalid url', function () { + assert.throws( + () => new WebSocket('echo.websocket.org'), + /^Error: Invalid URL: echo\.websocket\.org$/ + ); + }); + + it('accepts `url.Url` objects as url', function (done) { + const agent = new CustomAgent(); + + agent.addRequest = (req) => { + assert.strictEqual(req.path, '/'); + done(); + }; + + const ws = new WebSocket(url.parse('ws://localhost'), { agent }); + }); + + it('accepts `url.URL` objects as url', function (done) { + if (!url.URL) return this.skip(); + + const agent = new CustomAgent(); + + agent.addRequest = (req, opts) => { + assert.strictEqual(opts.host, '::1'); + assert.strictEqual(req.path, '/'); + done(); + }; + + const ws = new WebSocket(new url.URL('ws://[::1]'), { agent }); + }); + + describe('options', function () { + it('accepts the `options` object as 3rd argument', function () { + const agent = new CustomAgent(); + let count = 0; + let ws; + + agent.addRequest = (req) => count++; + + ws = new WebSocket('ws://localhost', undefined, { agent }); + ws = new WebSocket('ws://localhost', null, { agent }); + ws = new WebSocket('ws://localhost', [], { agent }); + + assert.strictEqual(count, 3); + }); + + it('throws an error when using an invalid `protocolVersion`', function () { + const options = { agent: new CustomAgent(), protocolVersion: 1000 }; + + assert.throws( + () => new WebSocket('ws://localhost', options), + /^RangeError: Unsupported protocol version: 1000 \(supported versions: 8, 13\)$/ + ); + }); + }); + }); + + describe('Constants', function () { + const readyStates = { + CONNECTING: 0, + OPEN: 1, + CLOSING: 2, + CLOSED: 3 + }; + + Object.keys(readyStates).forEach((state) => { + describe(`\`${state}\``, function () { + it('is enumerable property of class', function () { + const propertyDescripter = Object.getOwnPropertyDescriptor(WebSocket, state); + + assert.strictEqual(propertyDescripter.value, readyStates[state]); + assert.strictEqual(propertyDescripter.enumerable, true); + }); + + it('is property of instance', function () { + const ws = new WebSocket('ws://localhost', { + agent: new CustomAgent() + }); + + assert.strictEqual(ws[state], readyStates[state]); + }); + }); + }); + }); + + describe('Attributes', function () { + describe('`binaryType`', function () { + it("defaults to 'nodebuffer'", function () { + const ws = new WebSocket('ws://localhost', { + agent: new CustomAgent() + }); + + assert.strictEqual(ws.binaryType, 'nodebuffer'); + }); + + it("can be changed to 'arraybuffer' or 'fragments'", function () { + 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`', function () { + it('defaults to zero', function () { + const ws = new WebSocket('ws://localhost', { + agent: new CustomAgent() + }); + + assert.strictEqual(ws.bufferedAmount, 0); + }); + + it('defaults to zero upon "open"', function (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); + }; + }); + }); + + it('takes into account the data in the sender queue', function (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'); + ws.send('bar', (err) => { + assert.ifError(err); + assert.strictEqual(ws.bufferedAmount, 0); + wss.close(done); + }); + + assert.strictEqual(ws.bufferedAmount, 3); + }); + }); + }); + + it('takes into account the data in the socket queue', function (done) { + const wss = new WebSocket.Server({ port: 0 }, () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + }); + + wss.on('connection', (ws) => { + while (true) { + if (ws._socket.bufferSize > 0) { + assert.strictEqual(ws.bufferedAmount, ws._socket.bufferSize); + break; + } + ws.send('hello'.repeat(1e4)); + } + wss.close(done); + }); + }); + }); + + describe('`extensions`', function () { + it('exposes the negotiated extensions names (1/2)', function (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)', function (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('`protocol`', function () { + it('exposes the subprotocol selected by the server', function (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`', function () { + it('defaults to `CONNECTING`', function () { + const ws = new WebSocket('ws://localhost', { + agent: new CustomAgent() + }); + + assert.strictEqual(ws.readyState, WebSocket.CONNECTING); + }); + + it('is set to `OPEN` once connection is established', function (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', function (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', function (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`', function () { + it('exposes the server url', function () { + const url = 'ws://localhost'; + const ws = new WebSocket(url, { agent: new CustomAgent() }); + + assert.strictEqual(ws.url, url); + }); + }); + }); + + describe('Events', function () { + it("emits an 'error' event if an error occurs", function (done) { + 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.message, + 'Invalid WebSocket frame: invalid opcode 5' + ); + + ws.on('close', (code, reason) => { + assert.strictEqual(code, 1002); + assert.strictEqual(reason, ''); + wss.close(done); + }); + }); + }); + + wss.on('connection', (ws) => { + ws._socket.write(Buffer.from([0x85, 0x00])); + }); + }); + + it('does not re-emit `net.Socket` errors', function (done) { + 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(err.message.startsWith('write E')); + ws.on('close', (code, message) => { + assert.strictEqual(message, ''); + assert.strictEqual(code, 1006); + wss.close(done); + }); + }); + + for (const client of wss.clients) client.terminate(); + ws.send('foo'); + ws.send('bar'); + }); + }); + }); + + it("emits an 'upgrade' event", function (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); + }); + }); + }); + + it("emits a 'ping' event", function (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()); + }); + + it("emits a 'pong' event", function (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()); + }); + }); + + describe('Connection establishing', function () { + const server = http.createServer(); + + beforeEach((done) => server.listen(0, done)); + afterEach((done) => server.close(done)); + + it('fails if the Sec-WebSocket-Accept header is invalid', function (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', function (done) { + server.once('upgrade', (req, socket) => { + const key = crypto.createHash('sha1') + .update(req.headers['sec-websocket-key'] + constants.GUID, 'binary') + .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, ''); + done(); + }); + }); + + it('error is emitted if server aborts connection', function (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', function (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' + + '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', function (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' + + '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', function (done) { + server.once('upgrade', (req, socket) => socket.on('end', socket.end)); + + const port = server.address().port; + const ws = new WebSocket(`ws://localhost:${port}`, null, { + 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 the Sec-WebSocket-Extensions response header is invalid', function (done) { + server.once('upgrade', (req, socket) => { + const key = crypto.createHash('sha1') + .update(req.headers['sec-websocket-key'] + constants.GUID, 'binary') + .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 server sends a subprotocol when none was requested', function (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', function (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 no subprotocol', function (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)); + }); + }); + }); + + describe('Connection with query string', function () { + it('connects when pathname is not null', function (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)); + }); + }); + + it('connects when pathname is null', function (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)); + }); + }); + }); + + describe('#ping', function () { + it('throws an error if `readyState` is not `OPEN`', function (done) { + const ws = new WebSocket('ws://localhost', { + agent: new CustomAgent() + }); + + assert.throws( + () => ws.ping(), + /^Error: WebSocket is not open: readyState 0 \(CONNECTING\)$/ + ); + + ws.ping((err) => { + assert.ok(err instanceof Error); + assert.strictEqual( + err.message, + 'WebSocket is not open: readyState 0 (CONNECTING)' + ); + done(); + }); + }); + + it('can send a ping with no data', function (done) { + const wss = new WebSocket.Server({ port: 0 }, () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + + ws.on('open', () => { + ws.ping(() => ws.ping()); + }); + }); + + 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', function (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)); + }); + }); + + 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', function (done) { + const wss = new WebSocket.Server({ port: 0 }, () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + + ws.on('open', () => ws.ping(0)); + }); + + wss.on('connection', (ws) => { + ws.on('ping', (message) => { + assert.strictEqual(message.toString(), '0'); + wss.close(done); + }); + }); + }); + }); + + describe('#pong', function () { + it('throws an error if `readyState` is not `OPEN`', (done) => { + const ws = new WebSocket('ws://localhost', { + agent: new CustomAgent() + }); + + assert.throws( + () => ws.pong(), + /^Error: WebSocket is not open: readyState 0 \(CONNECTING\)$/ + ); + + ws.pong((err) => { + assert.ok(err instanceof Error); + assert.strictEqual( + err.message, + 'WebSocket is not open: readyState 0 (CONNECTING)' + ); + done(); + }); + }); + + it('can send a pong with no data', function (done) { + const wss = new WebSocket.Server({ port: 0 }, () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + + ws.on('open', () => { + ws.pong(() => ws.pong()); + }); + }); + + 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', function (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)); + }); + }); + + 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', function (done) { + const wss = new WebSocket.Server({ port: 0 }, () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + + ws.on('open', () => ws.pong(0)); + }); + + wss.on('connection', (ws) => { + ws.on('pong', (message) => { + assert.strictEqual(message.toString(), '0'); + wss.close(done); + }); + }); + }); + }); + + describe('#send', function () { + it('can send a big binary message', function (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, { compress: false })); + ws.on('message', (msg) => { + assert.ok(msg.equals(Buffer.from(array.buffer))); + wss.close(done); + }); + }); + + wss.on('connection', (ws) => { + ws.on('message', (msg) => ws.send(msg, { compress: false })); + }); + }); + + it('can send text data', function (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) => { + assert.strictEqual(message, 'hi'); + wss.close(done); + }); + }); + + wss.on('connection', (ws) => { + ws.on('message', (msg) => ws.send(msg)); + }); + }); + + it('does not override the `fin` option', function (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 }); + }); + }); + + wss.on('connection', (ws) => { + ws.on('message', (msg) => { + assert.strictEqual(msg, 'fragmentfragment'); + wss.close(done); + }); + }); + }); + + it('sends numbers as strings', function (done) { + const wss = new WebSocket.Server({ port: 0 }, () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + + ws.on('open', () => ws.send(0)); + }); + + wss.on('connection', (ws) => { + ws.on('message', (msg) => { + assert.strictEqual(msg, '0'); + wss.close(done); + }); + }); + }); + + it('can send binary data as an array', function (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) + .slice(partial.byteOffset, partial.byteOffset + partial.byteLength); + + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + + ws.on('open', () => ws.send(partial, { binary: true })); + ws.on('message', (message) => { + assert.ok(message.equals(buf)); + wss.close(done); + }); + }); + + wss.on('connection', (ws) => { + ws.on('message', (msg) => ws.send(msg)); + }); + }); + + it('can send binary data as a buffer', function (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, { binary: true })); + ws.on('message', (message) => { + assert.ok(message.equals(buf)); + wss.close(done); + }); + }); + + wss.on('connection', (ws) => { + ws.on('message', (msg) => ws.send(msg)); + }); + }); + + it('can send an `ArrayBuffer`', function (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.onmessage = (event) => { + assert.ok(event.data.equals(Buffer.from(array.buffer))); + wss.close(done); + }; + }); + + wss.on('connection', (ws) => { + ws.on('message', (msg) => ws.send(msg)); + }); + }); + + it('can send a `Buffer`', function (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.onmessage = (event) => { + assert.ok(event.data.equals(buf)); + wss.close(done); + }; + }); + + wss.on('connection', (ws) => { + ws.on('message', (msg) => ws.send(msg)); + }); + }); + + it('throws an error if `readyState` is not `OPEN`', function () { + const ws = new WebSocket('ws://localhost', { + agent: new CustomAgent() + }); + + assert.throws( + () => ws.send('hi'), + /^Error: WebSocket is not open: readyState 0 \(CONNECTING\)$/ + ); + }); + + it('passes errors to the callback, if present', function () { + const ws = new WebSocket('ws://localhost', { + agent: new CustomAgent() + }); + + ws.send('hi', (err) => { + assert.ok(err instanceof Error); + assert.strictEqual( + err.message, + 'WebSocket is not open: readyState 0 (CONNECTING)' + ); + }); + }); + + it('calls the optional callback when data is written out', function (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); + }); + }); + }); + }); + + it('works when the `data` argument is falsy', function (done) { + const wss = new WebSocket.Server({ port: 0 }, () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + + ws.on('open', () => ws.send()); + }); + + wss.on('connection', (ws) => { + ws.on('message', (message) => { + assert.ok(message.equals(Buffer.alloc(0))); + wss.close(done); + }); + }); + }); + + it('can send text data with `mask` option set to `false`', function (done) { + const wss = new WebSocket.Server({ port: 0 }, () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + + ws.on('open', () => ws.send('hi', { mask: false })); + }); + + wss.on('connection', (ws) => { + ws.on('message', (message) => { + assert.strictEqual(message, 'hi'); + wss.close(done); + }); + }); + }); + + it('can send binary data with `mask` option set to `false`', function (done) { + const array = new Float32Array(5); + + for (let i = 0; i < array.length; ++i) { + array[i] = i / 2; + } + + const wss = new WebSocket.Server({ port: 0 }, () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + + ws.on('open', () => ws.send(array, { mask: false })); + }); + + wss.on('connection', (ws) => { + ws.on('message', (message) => { + assert.ok(message.equals(Buffer.from(array.buffer))); + wss.close(done); + }); + }); + }); + }); + + describe('#close', function () { + it('closes the connection if called while connecting (1/2)', function (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/2)', function (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('can be called from an error listener while connecting', function (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()); + }); + }); + + it("can be called from a listener of the 'upgrade' event", function (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('throws an error if the first argument is invalid (1/2)', function (done) { + const wss = new WebSocket.Server({ port: 0 }, () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + + ws.on('open', () => { + assert.throws( + () => ws.close('error'), + /^TypeError: First argument must be a valid error code number$/ + ); + + wss.close(done); + }); + }); + }); + + it('throws an error if the first argument is invalid (2/2)', function (done) { + const wss = new WebSocket.Server({ port: 0 }, () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + + ws.on('open', () => { + assert.throws( + () => ws.close(1004), + /^TypeError: First argument must be a valid error code number$/ + ); + + wss.close(done); + }); + }); + }); + + it('sends the close status code only when necessary', function (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.ok(received.slice(0, 2).equals(Buffer.from([0x88, 0x80]))); + assert.ok(sent.equals(Buffer.from([0x88, 0x00]))); + + ws.on('close', (code, reason) => { + assert.strictEqual(code, 1005); + assert.strictEqual(reason, ''); + wss.close(done); + }); + }); + ws.close(); + }); + }); + + it('works when close reason is not specified', function (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(message, ''); + assert.strictEqual(code, 1000); + wss.close(done); + }); + }); + }); + + it('works when close reason is specified', function (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(message, 'some reason'); + assert.strictEqual(code, 1000); + wss.close(done); + }); + }); + }); + + it('ends connection to the server', function (done) { + const wss = new WebSocket.Server({ + clientTracking: false, + port: 0 + }, () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + + ws.on('open', () => { + ws.on('close', (code, reason) => { + assert.strictEqual(reason, 'some reason'); + assert.strictEqual(code, 1000); + wss.close(done); + }); + ws.close(1000, 'some reason'); + }); + }); + }); + + it('permits all buffered data to be delivered', function (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) => messages.push(message)); + 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', function (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('does nothing if `readyState` is `CLOSED`', function (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()); + }); + }); + + describe('#terminate', function () { + it('closes the connection if called while connecting (1/2)', function (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)', function (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', function (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()); + }); + }); + + it("can be called from a listener of the 'upgrade' event", function (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`', function (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', function () { + it('supports the `on{close,error,message,open}` attributes', function () { + const listener = () => {}; + const ws = new WebSocket('ws://localhost', { agent: new CustomAgent() }); + + assert.strictEqual(ws.onmessage, undefined); + assert.strictEqual(ws.onclose, undefined); + assert.strictEqual(ws.onerror, undefined); + assert.strictEqual(ws.onopen, undefined); + + ws.onmessage = listener; + ws.onerror = listener; + ws.onclose = listener; + ws.onopen = listener; + + assert.strictEqual(ws.onmessage, listener); + assert.strictEqual(ws.onclose, listener); + assert.strictEqual(ws.onerror, listener); + assert.strictEqual(ws.onopen, listener); + }); + + it('works like the `EventEmitter` interface', function (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) => ws.send(msg)); + }); + }); + + it("doesn't return listeners added with `on`", function () { + const listener = () => {}; + const ws = new WebSocket('ws://localhost', { agent: new CustomAgent() }); + + ws.on('open', listener); + + assert.deepStrictEqual(ws.listeners('open'), [listener]); + assert.strictEqual(ws.onopen, undefined); + }); + + it("doesn't remove listeners added with `on`", function () { + const listener = () => {}; + const ws = new WebSocket('ws://localhost', { agent: new CustomAgent() }); + + ws.on('close', listener); + ws.onclose = listener; + + let listeners = ws.listeners('close'); + + assert.strictEqual(listeners.length, 2); + assert.strictEqual(listeners[0], listener); + assert.strictEqual(listeners[1]._listener, listener); + + ws.onclose = listener; + + listeners = ws.listeners('close'); + + assert.strictEqual(listeners.length, 2); + assert.strictEqual(listeners[0], listener); + assert.strictEqual(listeners[1]._listener, listener); + }); + + it('adds listeners for custom events with `addEventListener`', function () { + const listener = () => {}; + const ws = new WebSocket('ws://localhost', { agent: new CustomAgent() }); + + ws.addEventListener('foo', listener); + assert.strictEqual(ws.listeners('foo')[0], listener); + + // + // Fails silently when the `listener` is not a function. + // + ws.addEventListener('bar', {}); + assert.strictEqual(ws.listeners('bar').length, 0); + }); + + it('supports the `removeEventListener` method', function () { + const listener = () => {}; + const ws = new WebSocket('ws://localhost', { agent: new CustomAgent() }); + + ws.addEventListener('message', listener); + ws.addEventListener('open', listener); + ws.addEventListener('foo', listener); + + assert.strictEqual(ws.listeners('message')[0]._listener, listener); + assert.strictEqual(ws.listeners('open')[0]._listener, listener); + assert.strictEqual(ws.listeners('foo')[0], listener); + + ws.removeEventListener('message', () => {}); + + assert.strictEqual(ws.listeners('message')[0]._listener, listener); + + ws.removeEventListener('message', listener); + ws.removeEventListener('open', listener); + ws.removeEventListener('foo', listener); + + assert.strictEqual(ws.listenerCount('message'), 0); + assert.strictEqual(ws.listenerCount('open'), 0); + assert.strictEqual(ws.listenerCount('foo'), 0); + }); + + it('wraps text data in a `MessageEvent`', function (done) { + const wss = new WebSocket.Server({ port: 0 }, () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + + ws.addEventListener('open', () => ws.send('hi')); + ws.addEventListener('message', (messageEvent) => { + assert.strictEqual(messageEvent.data, 'hi'); + wss.close(done); + }); + }); + + wss.on('connection', (ws) => { + ws.on('message', (msg) => ws.send(msg)); + }); + }); + + it('receives a `CloseEvent` when server closes (1000)', function (done) { + const wss = new WebSocket.Server({ port: 0 }, () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + + ws.addEventListener('close', (closeEvent) => { + assert.ok(closeEvent.wasClean); + assert.strictEqual(closeEvent.reason, ''); + assert.strictEqual(closeEvent.code, 1000); + wss.close(done); + }); + }); + + wss.on('connection', (ws) => ws.close(1000)); + }); + + it('receives a `CloseEvent` when server closes (4000)', function (done) { + const wss = new WebSocket.Server({ port: 0 }, () => { + const ws = new WebSocket(`ws://localhost:${wss.address().port}`); + + ws.addEventListener('close', (closeEvent) => { + assert.ok(closeEvent.wasClean); + assert.strictEqual(closeEvent.reason, 'some daft reason'); + assert.strictEqual(closeEvent.code, 4000); + wss.close(done); + }); + }); + + wss.on('connection', (ws) => ws.close(4000, 'some daft reason')); + }); + + it('sets `target` and `type` on events', function (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', (openEvent) => { + assert.strictEqual(openEvent.type, 'open'); + assert.strictEqual(openEvent.target, ws); + }); + ws.addEventListener('message', (messageEvent) => { + assert.strictEqual(messageEvent.type, 'message'); + assert.strictEqual(messageEvent.target, ws); + wss.close(); + }); + ws.addEventListener('close', (closeEvent) => { + assert.strictEqual(closeEvent.type, 'close'); + assert.strictEqual(closeEvent.target, ws); + ws.emit('error', err); + }); + ws.addEventListener('error', (errorEvent) => { + assert.strictEqual(errorEvent.message, 'forced'); + assert.strictEqual(errorEvent.type, 'error'); + assert.strictEqual(errorEvent.target, ws); + assert.strictEqual(errorEvent.error, err); + + done(); + }); + }); + + wss.on('connection', (client) => client.send('hi')); + }); + + it('passes binary data as a Node.js `Buffer` by default', function (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))); + }); + + it('ignores `binaryType` for text messages', function (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')); + }); + + it('allows to update `binaryType` on the fly', function (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', () => wss.close(done)); + }); + }); + }; + }); + + wss.on('connection', (ws) => { + ws.on('message', (msg) => ws.send(msg)); + }); + }); + }); + + describe('SSL', function () { + it('connects to secure websocket server', function (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) => { + wss.close(); + server.close(done); + }); + + server.listen(0, () => { + const ws = new WebSocket(`wss://localhost:${server.address().port}`, { + rejectUnauthorized: false + }); + }); + }); + + it('connects to secure websocket server with client side certificate', function (done) { + const server = https.createServer({ + cert: fs.readFileSync('test/fixtures/certificate.pem'), + ca: [fs.readFileSync('test/fixtures/ca1-cert.pem')], + key: fs.readFileSync('test/fixtures/key.pem'), + requestCert: true + }); + + let success = false; + const wss = new WebSocket.Server({ + verifyClient: (info) => { + success = !!info.req.client.authorized; + return true; + }, + server + }); + + wss.on('connection', (ws) => { + assert.ok(success); + server.close(done); + wss.close(); + }); + + server.listen(0, () => { + const ws = new WebSocket(`wss://localhost:${server.address().port}`, { + cert: fs.readFileSync('test/fixtures/agent1-cert.pem'), + key: fs.readFileSync('test/fixtures/agent1-key.pem'), + rejectUnauthorized: false + }); + }); + }); + + it('cannot connect to secure websocket server via ws://', function (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', function (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) => { + assert.strictEqual(message, 'foobar'); + server.close(done); + wss.close(); + }); + }); + + server.listen(0, () => { + const ws = new WebSocket(`wss://localhost:${server.address().port}`, { + rejectUnauthorized: false + }); + + ws.on('open', () => ws.send('foobar')); + }); + }); + + it('can send a big binary message', function (done) { + this.timeout(4000); + + 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) => ws.send(message)); + }); + + server.listen(0, () => { + const ws = new WebSocket(`wss://localhost:${server.address().port}`, { + rejectUnauthorized: false + }); + + ws.on('open', () => ws.send(buf)); + ws.on('message', (message) => { + assert.ok(buf.equals(message)); + + server.close(done); + wss.close(); + }); + }); + }); + }); + + describe('Request headers', function () { + it('adds the authorization header if the url has userinfo (1/2)', function (done) { + const agent = new CustomAgent(); + const auth = 'test:testpass'; + + agent.addRequest = (req) => { + assert.strictEqual( + req._headers.authorization, + `Basic ${Buffer.from(auth).toString('base64')}` + ); + done(); + }; + + const ws = new WebSocket(`ws://${auth}@localhost`, { agent }); + }); + + it('adds the authorization header if the url has userinfo (2/2)', function (done) { + if (!url.URL) return this.skip(); + + const agent = new CustomAgent(); + const auth = 'test:testpass'; + + agent.addRequest = (req) => { + assert.strictEqual( + req._headers.authorization, + `Basic ${Buffer.from(auth).toString('base64')}` + ); + done(); + }; + + const ws = new WebSocket(new url.URL(`ws://${auth}@localhost`), { + agent + }); + }); + + it('adds custom headers', function (done) { + const agent = new CustomAgent(); + + agent.addRequest = (req) => { + assert.strictEqual(req._headers.cookie, 'foo=bar'); + done(); + }; + + const ws = new WebSocket('ws://localhost', { + headers: { 'Cookie': 'foo=bar' }, + agent + }); + }); + + it('includes the host header with port number', function (done) { + const agent = new CustomAgent(); + + agent.addRequest = (req) => { + assert.strictEqual(req._headers.host, 'localhost:1337'); + done(); + }; + + const ws = new WebSocket('ws://localhost:1337', { agent }); + }); + + it('excludes default ports from host header', function () { + const httpsAgent = new https.Agent(); + const httpAgent = new http.Agent(); + const values = []; + let ws; + + httpsAgent.addRequest = httpAgent.addRequest = (req) => { + values.push(req._headers.host); + }; + + ws = new WebSocket('wss://localhost:8443', { agent: httpsAgent }); + ws = new WebSocket('wss://localhost:443', { agent: httpsAgent }); + ws = new WebSocket('ws://localhost:88', { agent: httpAgent }); + ws = new WebSocket('ws://localhost:80', { agent: httpAgent }); + + assert.deepStrictEqual(values, [ + 'localhost:8443', + 'localhost', + 'localhost:88', + 'localhost' + ]); + }); + + it("doesn't add the origin header by default", function (done) { + const agent = new CustomAgent(); + + agent.addRequest = (req) => { + assert.strictEqual(req._headers.origin, undefined); + done(); + }; + + const ws = new WebSocket('ws://localhost', { agent }); + }); + + it('honors the `origin` option (1/2)', function (done) { + const agent = new CustomAgent(); + + agent.addRequest = (req) => { + assert.strictEqual(req._headers.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)', function (done) { + const agent = new CustomAgent(); + + agent.addRequest = (req) => { + assert.strictEqual( + req._headers['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', function () { + it('is enabled by default', (done) => { + const agent = new CustomAgent(); + + agent.addRequest = (req) => { + assert.strictEqual( + req._headers['sec-websocket-extensions'], + 'permessage-deflate; client_max_window_bits' + ); + done(); + }; + + const ws = new WebSocket('ws://localhost', { agent }); + }); + + it('can be disabled', function (done) { + const agent = new CustomAgent(); + + agent.addRequest = (req) => { + assert.strictEqual(req._headers['sec-websocket-extensions'], undefined); + done(); + }; + + const ws = new WebSocket('ws://localhost', { + perMessageDeflate: false, + agent + }); + }); + + it('can send extension parameters', function (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._headers['sec-websocket-extensions'], + value + ); + done(); + }; + + const ws = new WebSocket('ws://localhost', { + perMessageDeflate: { + clientNoContextTakeover: true, + serverNoContextTakeover: true, + clientMaxWindowBits: true, + serverMaxWindowBits: 10 + }, + agent + }); + }); + + it('can send and receive text data', function (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.on('message', (message) => { + assert.strictEqual(message, 'hi'); + wss.close(done); + }); + }); + + wss.on('connection', (ws) => { + ws.on('message', (message) => ws.send(message, { compress: true })); + }); + }); + + it('can send and receive a `TypedArray`', function (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.on('message', (message) => { + assert.ok(message.equals(Buffer.from(array.buffer))); + wss.close(done); + }); + }); + + wss.on('connection', (ws) => { + ws.on('message', (message) => ws.send(message, { compress: true })); + }); + }); + + it('can send and receive an `ArrayBuffer`', function (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.on('message', (message) => { + assert.ok(message.equals(Buffer.from(array.buffer))); + wss.close(done); + }); + }); + + wss.on('connection', (ws) => { + ws.on('message', (message) => ws.send(message, { compress: true })); + }); + }); + + it('consumes all received data when connection is closed abnormally', function (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) => messages.push(message)); + 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()); + }); + }); + + describe('#send', function () { + it('ignores the `compress` option if the extension is disabled', function (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.on('message', (message) => { + assert.strictEqual(message, 'hi'); + wss.close(done); + }); + }); + + wss.on('connection', (ws) => { + ws.on('message', (message) => ws.send(message, { compress: true })); + }); + }); + }); + + describe('#terminate', function () { + it('can be used while data is being compressed', function (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.ok(err instanceof Error); + wss.close(done); + }); + ws.terminate(); + }); + }); + }); + + it('can be used while data is being decompressed', function (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) => { + if (messages.push(message) > 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, ''); + wss.close(done); + }); + }); + + wss.on('connection', (ws) => { + const buf = Buffer.from('c10100c10100c10100c10100', 'hex'); + ws._socket.write(buf); + }); + }); + }); + }); +});