diff --git a/.eslintrc.yaml b/.eslintrc.yaml index d370e3d2e..45998d206 100644 --- a/.eslintrc.yaml +++ b/.eslintrc.yaml @@ -5,12 +5,14 @@ env: node: true extends: - eslint:recommended - - prettier + - plugin:prettier/recommended parserOptions: ecmaVersion: 9 -plugins: - - prettier rules: no-console: off + no-var: error prefer-const: error - prettier/prettier: error + quotes: + - error + - single + - avoidEscape: true diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..6313b56c5 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto eol=lf diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 000000000..2c4cd0207 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,2 @@ +github: + - lpinca diff --git a/ISSUE_TEMPLATE.md b/.github/issue_template.md similarity index 100% rename from ISSUE_TEMPLATE.md rename to .github/issue_template.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..158a50e32 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,50 @@ +name: CI + +on: + - push + - pull_request + +jobs: + test: + runs-on: ${{ matrix.os }} + strategy: + matrix: + node: + - 8 + - 10 + - 12 + - 14 + - 16 + os: + - macOS-latest + - ubuntu-latest + - windows-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v2 + with: + node-version: ${{ matrix.node }} + - run: npm install + - run: npm run lint + if: matrix.node == 16 && matrix.os == 'ubuntu-latest' + - run: npm test + - run: + echo ::set-output name=job_id::$(node -e + "console.log(crypto.randomBytes(16).toString('hex'))") + id: get_job_id + shell: bash + - uses: coverallsapp/github-action@v1.1.2 + with: + flag-name: + ${{ steps.get_job_id.outputs.job_id }} (Node.js ${{ matrix.node }} + on ${{ matrix.os }}) + github-token: ${{ secrets.GITHUB_TOKEN }} + parallel: true + coverage: + needs: test + runs-on: ubuntu-latest + steps: + - uses: coverallsapp/github-action@v1.1.2 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + parallel-finished: true diff --git a/.npmrc b/.npmrc new file mode 100644 index 000000000..43c97e719 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +package-lock=false diff --git a/.prettierrc.yaml b/.prettierrc.yaml index 754ed23e2..fe2f506e3 100644 --- a/.prettierrc.yaml +++ b/.prettierrc.yaml @@ -2,3 +2,4 @@ arrowParens: always endOfLine: lf proseWrap: always singleQuote: true +trailingComma: none diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index ebb063d65..000000000 --- a/.travis.yml +++ /dev/null @@ -1,9 +0,0 @@ -language: node_js -sudo: false -node_js: - - "11" - - "10" - - "8" - - "6" -after_success: - - "npm install coveralls@3 && nyc report --reporter=text-lcov | coveralls" diff --git a/README.md b/README.md index 7c87b11e1..9c6e5287c 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ # 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) +[![Version npm](https://img.shields.io/npm/v/ws.svg?logo=npm)](https://www.npmjs.com/package/ws) +[![Build](https://img.shields.io/github/workflow/status/websockets/ws/CI/master?label=build&logo=github)](https://github.com/websockets/ws/actions?query=workflow%3ACI+branch%3Amaster) +[![Windows x86 Build](https://img.shields.io/appveyor/ci/lpinca/ws/master.svg?logo=appveyor)](https://ci.appveyor.com/project/lpinca/ws) +[![Coverage Status](https://img.shields.io/coveralls/websockets/ws/master.svg)](https://coveralls.io/github/websockets/ws) ws is a simple to use, blazing fast, and thoroughly tested WebSocket client and server implementation. @@ -32,10 +32,11 @@ can use one of the many wrappers available on npm, like - [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) + - [Client authentication](#client-authentication) - [Server broadcast](#server-broadcast) - [echo.websocket.org demo](#echowebsocketorg-demo) + - [Use the Node.js streams API](#use-the-nodejs-streams-api) - [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) @@ -55,7 +56,7 @@ can use one of the many wrappers available on npm, like npm install ws ``` -### Opt-in for performance and spec compliance +### Opt-in for performance There are 2 optional modules that can be installed along side with the ws module. These modules are binary addons which improve certain operations. @@ -66,11 +67,12 @@ necessarily need to have a C++ compiler installed on your machine. 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. + message contains valid UTF-8. ## API docs -See [`/doc/ws.md`](./doc/ws.md) for Node.js-like docs for the ws classes. +See [`/doc/ws.md`](./doc/ws.md) for Node.js-like documentation of ws classes and +utility functions. ## WebSocket compression @@ -193,7 +195,7 @@ const fs = require('fs'); const https = require('https'); const WebSocket = require('ws'); -const server = new https.createServer({ +const server = https.createServer({ cert: fs.readFileSync('/path/to/cert.pem'), key: fs.readFileSync('/path/to/key.pem') }); @@ -215,6 +217,7 @@ server.listen(8080); ```js const http = require('http'); const WebSocket = require('ws'); +const url = require('url'); const server = http.createServer(); const wss1 = new WebSocket.Server({ noServer: true }); @@ -247,25 +250,72 @@ server.on('upgrade', function upgrade(request, socket, head) { server.listen(8080); ``` +### Client authentication + +```js +const http = require('http'); +const WebSocket = require('ws'); + +const server = http.createServer(); +const wss = new WebSocket.Server({ noServer: true }); + +wss.on('connection', function connection(ws, request, client) { + ws.on('message', function message(msg) { + console.log(`Received message ${msg} from user ${client}`); + }); +}); + +server.on('upgrade', function upgrade(request, socket, head) { + // This function is not defined on purpose. Implement it with your own logic. + authenticate(request, (err, client) => { + if (err || !client) { + socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n'); + socket.destroy(); + return; + } + + wss.handleUpgrade(request, socket, head, function done(ws) { + wss.emit('connection', ws, request, client); + }); + }); +}); + +server.listen(8080); +``` + +Also see the provided [example][session-parse-example] using `express-session`. + ### Server broadcast +A client WebSocket broadcasting to all connected WebSocket clients, including +itself. + ```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) { - if (client.readyState === WebSocket.OPEN) { - client.send(data); - } +wss.on('connection', function connection(ws) { + ws.on('message', function incoming(data) { + wss.clients.forEach(function each(client) { + if (client.readyState === WebSocket.OPEN) { + client.send(data); + } + }); }); -}; +}); +``` + +A client WebSocket broadcasting to every other connected WebSocket clients, +excluding itself. + +```js +const WebSocket = require('ws'); + +const wss = new WebSocket.Server({ port: 8080 }); 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); @@ -302,6 +352,21 @@ ws.on('message', function incoming(data) { }); ``` +### Use the Node.js streams API + +```js +const WebSocket = require('ws'); + +const ws = new WebSocket('wss://echo.websocket.org/', { + origin: 'https://websocket.org' +}); + +const duplex = WebSocket.createWebSocketStream(ws, { encoding: 'utf8' }); + +duplex.pipe(process.stdout); +process.stdin.pipe(duplex); +``` + ### Other examples For a full example with a browser client communicating with a ws server, see the @@ -309,30 +374,6 @@ examples folder. Otherwise, see the test cases. -## Error handling best practices - -```js -// If the WebSocket is closed before the following send is attempted -ws.send('something'); - -// Errors (both immediate and async write errors) can be detected in an optional -// 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. -}); - -// 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 */ -} -``` - ## FAQ ### How to get the IP address of the client? @@ -345,7 +386,7 @@ const WebSocket = require('ws'); const wss = new WebSocket.Server({ port: 8080 }); wss.on('connection', function connection(ws, req) { - const ip = req.connection.remoteAddress; + const ip = req.socket.remoteAddress; }); ``` @@ -391,6 +432,10 @@ const interval = setInterval(function ping() { ws.ping(noop); }); }, 30000); + +wss.on('close', function close() { + clearInterval(interval); +}); ``` Pong messages are automatically sent in response to ping messages as required by @@ -406,9 +451,10 @@ const WebSocket = require('ws'); function heartbeat() { clearTimeout(this.pingTimeout); - // Use `WebSocket#terminate()` and not `WebSocket#close()`. Delay should be - // equal to the interval at which your server sends out pings plus a - // conservative assumption of the latency. + // Use `WebSocket#terminate()`, which immediately destroys the connection, + // instead of `WebSocket#close()`, which waits for the close timer. + // Delay should be equal to the interval at which your server + // sends out pings plus a conservative assumption of the latency. this.pingTimeout = setTimeout(() => { this.terminate(); }, 30000 + 1000); @@ -436,14 +482,15 @@ We're using the GitHub [releases][changelog] for changelog entries. [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 +[client-report]: http://websockets.github.io/ws/autobahn/clients/ +[https-proxy-agent]: https://github.com/TooTallNate/node-https-proxy-agent [node-zlib-bug]: https://github.com/nodejs/node/issues/8871 [node-zlib-deflaterawdocs]: https://nodejs.org/api/zlib.html#zlib_zlib_createdeflateraw_options +[permessage-deflate]: https://tools.ietf.org/html/rfc7692 +[server-report]: http://websockets.github.io/ws/autobahn/servers/ +[session-parse-example]: ./examples/express-session-parse +[socks-proxy-agent]: https://github.com/TooTallNate/node-socks-proxy-agent [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 258ff59fd..eebb2a552 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -25,8 +25,9 @@ following methods: Once we have acknowledged receipt of your report and confirmed the bug ourselves we will work with you to fix the vulnerability and publicly acknowledge your -responsible disclosure, if you wish. In addition to that we will report all -vulnerabilities to the [Node Security Project](https://nodesecurity.io/). +responsible disclosure, if you wish. In addition to that we will create and +publish a security advisory to +[GitHub Security Advisories](https://github.com/websockets/ws/security/advisories). ## History diff --git a/appveyor.yml b/appveyor.yml index 917169ad0..f4c05fbf4 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,12 +1,12 @@ environment: matrix: - - nodejs_version: "11" - - nodejs_version: "10" - - nodejs_version: "8" - - nodejs_version: "6" + - nodejs_version: '16' + - nodejs_version: '14' + - nodejs_version: '12' + - nodejs_version: '10' + - nodejs_version: '8' platform: - x86 - - x64 matrix: fast_finish: true install: diff --git a/bench/parser.benchmark.js b/bench/parser.benchmark.js index c0a61675a..dd97701af 100644 --- a/bench/parser.benchmark.js +++ b/bench/parser.benchmark.js @@ -16,27 +16,27 @@ const options = { }; function createBinaryFrame(length) { - const list = Sender.frame( - crypto.randomBytes(length), - Object.assign({ opcode: 0x02 }, options) - ); + const list = Sender.frame(crypto.randomBytes(length), { + opcode: 0x02, + ...options + }); return Buffer.concat(list); } const pingFrame1 = Buffer.concat( - Sender.frame(crypto.randomBytes(5), Object.assign({ opcode: 0x09 }, options)) + Sender.frame(crypto.randomBytes(5), { opcode: 0x09, ...options }) ); const textFrame = Buffer.from('819461616161' + '61'.repeat(20), 'hex'); -const pingFrame2 = Buffer.from('8900', 'hex'); +const pingFrame2 = Buffer.from('8980146e915a', '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(); +const receiver = new Receiver('nodebuffer', {}, true); suite.add('ping frame (5 bytes payload)', { defer: true, diff --git a/bench/speed.js b/bench/speed.js index c87dc0b78..32ec0fb81 100644 --- a/bench/speed.js +++ b/bench/speed.js @@ -60,7 +60,7 @@ if (cluster.isMaster) { console.log('Generating %s of test data...', humanSize(largest)); const randomBytes = Buffer.allocUnsafe(largest); - for (var i = 0; i < largest; ++i) { + for (let i = 0; i < largest; ++i) { randomBytes[i] = ~~(Math.random() * 127); } @@ -72,8 +72,8 @@ if (cluster.isMaster) { const ws = new WebSocket(url, { maxPayload: 600 * 1024 * 1024 }); - var roundtrip = 0; - var time; + let roundtrip = 0; + let time; ws.on('error', (err) => { console.error(err.stack); @@ -87,7 +87,7 @@ if (cluster.isMaster) { if (++roundtrip !== roundtrips) return ws.send(data, { binary: useBinary }); - var elapsed = process.hrtime(time); + let elapsed = process.hrtime(time); elapsed = elapsed[0] * 1e9 + elapsed[1]; console.log( @@ -106,7 +106,7 @@ if (cluster.isMaster) { (function run() { if (configs.length === 0) return cluster.worker.disconnect(); - var config = configs.shift(); + const config = configs.shift(); config.push(run); runConfig.apply(null, config); })(); diff --git a/browser.js b/browser.js index 782077969..ca4f628ac 100644 --- a/browser.js +++ b/browser.js @@ -1,6 +1,6 @@ 'use strict'; -module.exports = function() { +module.exports = function () { throw new Error( 'ws does not work in the browser. Browser clients must use the native ' + 'WebSocket object' diff --git a/doc/ws.md b/doc/ws.md index 0b21101eb..de62e6756 100644 --- a/doc/ws.md +++ b/doc/ws.md @@ -1,5 +1,50 @@ # ws +## Table of Contents + +- [Class: WebSocket.Server](#class-websocketserver) + - [new WebSocket.Server(options[, callback])](#new-websocketserveroptions-callback) + - [Event: 'close'](#event-close) + - [Event: 'connection'](#event-connection) + - [Event: 'error'](#event-error) + - [Event: 'headers'](#event-headers) + - [Event: 'listening'](#event-listening) + - [server.address()](#serveraddress) + - [server.clients](#serverclients) + - [server.close([callback])](#serverclosecallback) + - [server.handleUpgrade(request, socket, head, callback)](#serverhandleupgraderequest-socket-head-callback) + - [server.shouldHandle(request)](#servershouldhandlerequest) +- [Class: WebSocket](#class-websocket) + - [Ready state constants](#ready-state-constants) + - [new WebSocket(address[, protocols][, options])](#new-websocketaddress-protocols-options) + - [UNIX Domain Sockets](#unix-domain-sockets) + - [Event: 'close'](#event-close-1) + - [Event: 'error'](#event-error-1) + - [Event: 'message'](#event-message) + - [Event: 'open'](#event-open) + - [Event: 'ping'](#event-ping) + - [Event: 'pong'](#event-pong) + - [Event: 'unexpected-response'](#event-unexpected-response) + - [Event: 'upgrade'](#event-upgrade) + - [websocket.addEventListener(type, listener[, options])](#websocketaddeventlistenertype-listener-options) + - [websocket.binaryType](#websocketbinarytype) + - [websocket.bufferedAmount](#websocketbufferedamount) + - [websocket.close([code[, reason]])](#websocketclosecode-reason) + - [websocket.extensions](#websocketextensions) + - [websocket.onclose](#websocketonclose) + - [websocket.onerror](#websocketonerror) + - [websocket.onmessage](#websocketonmessage) + - [websocket.onopen](#websocketonopen) + - [websocket.ping([data[, mask]][, callback])](#websocketpingdata-mask-callback) + - [websocket.pong([data[, mask]][, callback])](#websocketpongdata-mask-callback) + - [websocket.protocol](#websocketprotocol) + - [websocket.readyState](#websocketreadystate) + - [websocket.removeEventListener(type, listener)](#websocketremoveeventlistenertype-listener) + - [websocket.send(data[, options][, callback])](#websocketsenddata-options-callback) + - [websocket.terminate()](#websocketterminate) + - [websocket.url](#websocketurl) +- [WebSocket.createWebSocketStream(websocket[, options])](#websocketcreatewebsocketstreamwebsocket-options) + ## Class: WebSocket.Server This class represents a WebSocket server. It extends the `EventEmitter`. @@ -12,7 +57,8 @@ This class represents a WebSocket server. It extends the `EventEmitter`. - `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. + connections. See description below. (Usage is discouraged: see + [Issue #337](https://github.com/websockets/ws/issues/377#issuecomment-462152231)) - `handleProtocols` {Function} A function which can be used to handle the WebSocket subprotocols. See description below. - `path` {String} Accept only connections matching this path. @@ -30,16 +76,20 @@ 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. +> **NOTE:** Use of `verifyClient` is discouraged. Rather handle client +> authentication in the `upgrade` event of the HTTP server. See examples for +> more details. + 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. + - `secure` {Boolean} `true` if `req.socket.authorized` or + `req.socket.encrypted` is set. -The return value (Boolean) of the function determines whether or not to accept +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: @@ -104,7 +154,7 @@ emitted independently. ### Event: 'connection' -- `socket` {WebSocket} +- `websocket` {WebSocket} - `request` {http.IncomingMessage} Emitted when the handshake is complete. `request` is the http GET request sent @@ -129,13 +179,6 @@ handshake. This allows you to inspect/modify the headers before they are sent. Emitted when the underlying server has been bound. -### server.clients - -- {Set} - -A set that stores all connected clients. Please note that this property is only -added when the `clientTracking` is truthy. - ### server.address() Returns an object with `port`, `family`, and `address` properties specifying the @@ -143,6 +186,13 @@ 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.clients + +- {Set} + +A set that stores all connected clients. Please note that this property is only +added when the `clientTracking` is truthy. + ### server.close([callback]) Close the HTTP server if created internally, terminate all clients and call @@ -161,8 +211,10 @@ 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. -If the upgrade is successful, the `callback` is called with a `WebSocket` object -as parameter. +If the upgrade is successful, the `callback` is called with two arguments: + +- `websocket` {WebSocket} A `WebSocket` object. +- `request` {http.IncomingMessage} The client HTTP GET request. ### server.shouldHandle(request) @@ -190,17 +242,23 @@ This class represents a WebSocket. It extends the `EventEmitter`. ### new WebSocket(address[, protocols][, options]) -- `address` {String|url.Url|url.URL} The URL to which to connect. +- `address` {String|url.URL} The URL to which to connect. - `protocols` {String|Array} The list of subprotocols. - `options` {Object} + - `followRedirects` {Boolean} Whether or not to follow redirects. Defaults to + `false`. - `handshakeTimeout` {Number} Timeout in milliseconds for the handshake - request. + request. This is reset after every redirection. + - `maxRedirects` {Number} The maximum number of redirects allowed. Defaults + to 10. - `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`. - `maxPayload` {Number} The maximum allowed message size in bytes. - Any other option allowed in [http.request()][] or [https.request()][]. + Options given do not have any effect if parsed from the URL given with the + `address` parameter. `perMessageDeflate` default value is `true`. When using an object, parameters are the same of the server. The only difference is the direction of requests. @@ -282,10 +340,14 @@ 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.addEventListener(type, listener) +### websocket.addEventListener(type, listener[, options]) - `type` {String} A string representing the event type to listen for. - `listener` {Function} The listener to add. +- `options` {Object} + - `once` {Boolean} A `Boolean` indicating that the listener should be invoked + at most once after being added. If `true`, the listener would be + automatically removed when invoked. Register an event listener emulating the `EventTarget` interface. @@ -304,7 +366,11 @@ of binary protocols transferring large messages with multiple fragments. - {Number} The number of bytes of data that have been queued using calls to `send()` but -not yet transmitted to the network. +not yet transmitted to the network. This deviates from the HTML standard in the +following ways: + +1. If the data is immediately sent the value is `0`. +1. All framing bytes are included. ### websocket.close([code[, reason]]) @@ -351,7 +417,8 @@ receives an `OpenEvent` named "open". ### websocket.ping([data[, mask]][, callback]) -- `data` {Any} The data to send in the ping frame. +- `data` {Array|Number|Object|String|ArrayBuffer|Buffer|DataView|TypedArray} 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 @@ -361,7 +428,8 @@ Send a ping. ### websocket.pong([data[, mask]][, callback]) -- `data` {Any} The data to send in the pong frame. +- `data` {Array|Number|Object|String|ArrayBuffer|Buffer|DataView|TypedArray} 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 @@ -390,7 +458,8 @@ Removes an event listener emulating the `EventTarget` interface. ### websocket.send(data[, options][, callback]) -- `data` {Any} The data to send. +- `data` {Array|Number|Object|String|ArrayBuffer|Buffer|DataView|TypedArray} The + data to send. - `options` {Object} - `compress` {Boolean} Specifies whether `data` should be compressed or not. Defaults to `true` when permessage-deflate is enabled. @@ -407,7 +476,7 @@ Send `data` through the connection. ### websocket.terminate() -Forcibly close the connection. +Forcibly close the connection. Internally this calls [socket.destroy()][]. ### websocket.url @@ -415,11 +484,23 @@ Forcibly close the connection. The URL of the WebSocket server. Server clients don't have this attribute. +## WebSocket.createWebSocketStream(websocket[, options]) + +- `websocket` {WebSocket} A `WebSocket` object. +- `options` {Object} [Options][duplex-options] to pass to the `Duplex` + constructor. + +Returns a `Duplex` stream that allows to use the Node.js streams API on top of a +given `WebSocket`. + [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 +[duplex-options]: + https://nodejs.org/api/stream.html#stream_new_stream_duplex_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 +[permessage-deflate]: + https://tools.ietf.org/html/draft-ietf-hybi-permessage-compression-19 +[socket.destroy()]: https://nodejs.org/api/net.html#net_socket_destroy_error +[zlib-options]: https://nodejs.org/api/zlib.html#zlib_class_options diff --git a/examples/express-session-parse/index.js b/examples/express-session-parse/index.js index 5250b2cb6..8fc4ce029 100644 --- a/examples/express-session-parse/index.js +++ b/examples/express-session-parse/index.js @@ -8,6 +8,7 @@ const uuid = require('uuid'); const WebSocket = require('../..'); const app = express(); +const map = new Map(); // // We need the same instance of the session parser in express and @@ -25,7 +26,7 @@ const sessionParser = session({ app.use(express.static('public')); app.use(sessionParser); -app.post('/login', (req, res) => { +app.post('/login', function (req, res) { // // "Log in" user and set userId to session. // @@ -36,43 +37,65 @@ app.post('/login', (req, res) => { res.send({ result: 'OK', message: 'Session updated' }); }); -app.delete('/logout', (request, response) => { +app.delete('/logout', function (request, response) { + const ws = map.get(request.session.userId); + console.log('Destroying session'); - request.session.destroy(); - response.send({ result: 'OK', message: 'Session destroyed' }); + request.session.destroy(function () { + if (ws) ws.close(); + + response.send({ result: 'OK', message: 'Session destroyed' }); + }); }); // -// Create HTTP server by ourselves. +// Create an HTTP server. // 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); +// +// Create a WebSocket server completely detached from the HTTP server. +// +const wss = new WebSocket.Server({ clientTracking: false, noServer: true }); + +server.on('upgrade', function (request, socket, head) { + console.log('Parsing session from request...'); + + sessionParser(request, {}, () => { + if (!request.session.userId) { + socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n'); + socket.destroy(); + return; + } + + console.log('Session is parsed!'); + + wss.handleUpgrade(request, socket, head, function (ws) { + wss.emit('connection', ws, request); }); - }, - server + }); }); -wss.on('connection', (ws, req) => { - ws.on('message', (message) => { +wss.on('connection', function (ws, request) { + const userId = request.session.userId; + + map.set(userId, ws); + + ws.on('message', function (message) { // // Here we can now use session parameters. // - console.log(`WS message ${message} from user ${req.session.userId}`); + console.log(`Received message ${message} from user ${userId}`); + }); + + ws.on('close', function () { + map.delete(userId); }); }); // // Start the server. // -server.listen(8080, () => console.log('Listening on http://localhost:8080')); +server.listen(8080, function () { + console.log('Listening on http://localhost:8080'); +}); diff --git a/examples/express-session-parse/package.json b/examples/express-session-parse/package.json index 5390e7e0a..f8cd22e30 100644 --- a/examples/express-session-parse/package.json +++ b/examples/express-session-parse/package.json @@ -4,8 +4,8 @@ "version": "0.0.0", "repository": "websockets/ws", "dependencies": { - "express": "~4.16.3", - "express-session": "~1.15.6", - "uuid": "~3.3.2" + "express": "^4.16.4", + "express-session": "^1.16.1", + "uuid": "^3.3.2" } } diff --git a/examples/express-session-parse/public/app.js b/examples/express-session-parse/public/app.js index 916fafd41..f70dc2183 100644 --- a/examples/express-session-parse/public/app.js +++ b/examples/express-session-parse/public/app.js @@ -1,46 +1,67 @@ -/* global fetch, WebSocket, location */ -(() => { +(function () { const messages = document.querySelector('#messages'); const wsButton = document.querySelector('#wsButton'); + const wsSendButton = document.querySelector('#wsSendButton'); const logout = document.querySelector('#logout'); const login = document.querySelector('#login'); - const showMessage = (message) => { + function showMessage(message) { messages.textContent += `\n${message}`; messages.scrollTop = messages.scrollHeight; - }; + } - const handleResponse = (response) => { + function handleResponse(response) { return response.ok ? response.json().then((data) => JSON.stringify(data, null, 2)) : Promise.reject(new Error('Unexpected response')); - }; + } - login.onclick = () => { + login.onclick = function () { fetch('/login', { method: 'POST', credentials: 'same-origin' }) .then(handleResponse) .then(showMessage) - .catch((err) => showMessage(err.message)); + .catch(function (err) { + showMessage(err.message); + }); }; - logout.onclick = () => { + logout.onclick = function () { fetch('/logout', { method: 'DELETE', credentials: 'same-origin' }) .then(handleResponse) .then(showMessage) - .catch((err) => showMessage(err.message)); + .catch(function (err) { + showMessage(err.message); + }); }; let ws; - wsButton.onclick = () => { + wsButton.onclick = function () { 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'); + ws.onerror = function () { + showMessage('WebSocket error'); + }; + ws.onopen = function () { + showMessage('WebSocket connection established'); + }; + ws.onclose = function () { + showMessage('WebSocket connection closed'); + ws = null; + }; + }; + + wsSendButton.onclick = function () { + if (!ws) { + showMessage('No WebSocket connection'); + return; + } + + ws.send('Hello World!'); + showMessage('Sent "Hello World!"'); }; })(); diff --git a/examples/express-session-parse/public/index.html b/examples/express-session-parse/public/index.html index c99949c77..c07aa2e87 100644 --- a/examples/express-session-parse/public/index.html +++ b/examples/express-session-parse/public/index.html @@ -15,6 +15,9 @@
This example will upload an entire directory tree to the node.js server via a fast and persistent WebSocket connection.
-Note that the example is Chrome only for now.
-diff --git a/examples/fileapi/.gitignore b/examples/fileapi/.gitignore deleted file mode 100644 index dcd575688..000000000 --- a/examples/fileapi/.gitignore +++ /dev/null @@ -1 +0,0 @@ -uploaded diff --git a/examples/fileapi/package.json b/examples/fileapi/package.json deleted file mode 100644 index 770b5ebc1..000000000 --- a/examples/fileapi/package.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "author": "", - "name": "fileapi", - "version": "0.0.0", - "repository": "websockets/ws", - "dependencies": { - "express": "~4.16.3", - "ansi": "https://github.com/einaros/ansi.js/tarball/master" - } -} diff --git a/examples/fileapi/public/app.js b/examples/fileapi/public/app.js deleted file mode 100644 index 80300db52..000000000 --- a/examples/fileapi/public/app.js +++ /dev/null @@ -1,40 +0,0 @@ -/* global Uploader */ -function onFilesSelected(e) { - var button = e.srcElement; - button.disabled = true; - var progress = document.querySelector('div#progress'); - progress.innerHTML = '0%'; - var files = e.target.files; - var totalFiles = files.length; - 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 === '.') { - --totalFiles; - return; - } - uploader.sendFile(file, function(error) { - if (error) { - console.log(error); - return; - } - ++filesSent; - progress.innerHTML = ~~((filesSent / totalFiles) * 100) + '%'; - console.log('Sent: ' + file.name); - }); - }); - }); - } - uploader.ondone = function() { - uploader.close(); - progress.innerHTML = '100% done, ' + totalFiles + ' files sent.'; - }; -} - -window.onload = function() { - var importButtons = document.querySelectorAll('[type="file"]'); - Array.prototype.slice.call(importButtons, 0).forEach(function(importButton) { - importButton.addEventListener('change', onFilesSelected, false); - }); -}; diff --git a/examples/fileapi/public/index.html b/examples/fileapi/public/index.html deleted file mode 100644 index 0d463dd5a..000000000 --- a/examples/fileapi/public/index.html +++ /dev/null @@ -1,22 +0,0 @@ - - -
- - - - -
-