├── .babelrc
├── .editorconfig
├── .eslintrc
├── .gitignore
├── .travis.yml
├── CHANGELOG.md
├── LICENSE
├── README.md
├── TODO.md
├── example.js
├── package-lock.json
├── package.json
├── src
├── Extension.js
├── buffers.js
├── constants.js
├── extensions
│ └── PerMessageDeflate.js
├── index.js
├── socket
│ ├── Receiver.js
│ ├── Sender.js
│ └── index.js
├── util.js
└── validate.js
└── test
├── extensions.js
├── server.js
└── validation.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | [
4 | "@babel/preset-env",
5 | {
6 | "targets": {
7 | "node": "8.10.0"
8 | }
9 | }
10 | ]
11 | ],
12 | "plugins": [
13 | "@babel/plugin-proposal-object-rest-spread"
14 | ]
15 | }
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # editorconfig.org
2 | root = true
3 |
4 | [*]
5 | charset = utf-8
6 | indent_style = space
7 | indent_size = 2
8 | end_of_line = lf
9 | insert_final_newline = true
10 | trim_trailing_whitespace = true
11 |
12 | [*.{js,json}]
13 | indent_style = space
14 | indent_size = 2
15 |
16 | [.md]
17 | insert_final_newline = false
18 | trim_trailing_whitespace = false
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "eslint:recommended",
3 | "env": {
4 | "node": true,
5 | "es6": true,
6 | "jest": true
7 | },
8 | "plugins": [
9 | "node"
10 | ],
11 | "rules": {
12 | "node/no-missing-require": [
13 | "error"
14 | ]
15 | },
16 | "parserOptions": {
17 | "ecmaVersion": 6,
18 | "sourceType": "module",
19 | "ecmaFeatures": {
20 | "experimentalObjectRestSpread": true
21 | }
22 | }
23 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | *.log
3 | .vscode
4 | lib
5 |
6 | logs
7 | npm-debug.log*
8 | .eslintcache
9 | /coverage
10 | /dist
11 | /local
12 | /reports
13 | /node_modules
14 | .DS_Store
15 | Thumbs.db
16 | .idea
17 | *.sublime-project
18 | *.sublime-workspace
19 |
20 | # TODO Remove
21 | /old
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - node
4 | - 8
5 |
6 | cache:
7 | directories:
8 | - node_modules
9 |
10 | notifications:
11 | email: false
12 |
13 | before_install: npm i -g npm@latest
14 | script: npm run test-ci
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Change Log
2 |
3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4 |
5 |
6 | # [0.1.0](https://github.com/hugmanrique/turbo-ws/compare/v0.0.6...v0.1.0) (2018-03-11)
7 |
8 |
9 | ### Features
10 |
11 | * **broadcast:** Send frames to open sockets ([d1012db](https://github.com/hugmanrique/turbo-ws/commit/d1012db))
12 |
13 |
14 |
15 |
16 | ## [0.0.6](https://github.com/hugmanrique/turbo-ws/compare/v0.0.5...v0.0.6) (2018-03-11)
17 |
18 |
19 |
20 |
21 | ## [0.0.5](https://github.com/hugmanrique/turbo-ws/compare/v0.0.4...v0.0.5) (2018-03-11)
22 |
23 |
24 |
25 |
26 | ## [0.0.4](https://github.com/hugmanrique/turbo-ws/compare/v0.0.3...v0.0.4) (2018-03-11)
27 |
28 |
29 |
30 |
31 | ## [0.0.3](https://github.com/hugmanrique/turbo-ws/compare/v0.0.2...v0.0.3) (2018-03-11)
32 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Hugo Manrique
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # :dash: turbo-ws
2 |
3 | [![npm][npm]][npm-url]
4 | [![node][node]][node-url]
5 | [![deps][deps]][deps-url]
6 | [![tests][tests]][tests-url]
7 | [![coverage][cover]][cover-url]
8 | [![license][license]][license-url]
9 |
10 | A blazing fast low-level WebSocket server based on [turbo-net](https://github.com/mafintosh/turbo-net) and the awesome [ws](https://github.com/websockets/ws) server library for Node.js
11 |
12 | ## Features
13 |
14 | * Supports thousands of concurrent connections with minimal CPU and RAM impact.
15 | * Binary and text frames are supported. That means you can directly send Node's `Buffer`s or `String`s if you prefer.
16 | * Built with reliability in mind.
17 |
18 | ## Getting Started
19 |
20 | Install turbo-ws using [`npm`](https://www.npmjs.com/):
21 |
22 | ```bash
23 | npm install --save @hugmanrique/turbo-ws
24 | ```
25 |
26 | Or via [`yarn`](https://yarnpkg.com/en/package/@hugmanrique/turbo-ws):
27 |
28 | ```bash
29 | yarn add @hugmanrique/turbo-ws
30 | ```
31 |
32 | The minimum supported Node version is `v8.10.0`.
33 |
34 | Let's get started by creating a WebSocket server:
35 |
36 | ```javascript
37 | import Server from '@hugmanrique/turbo-ws';
38 |
39 | const server = new Server();
40 | const port = 80;
41 | ```
42 |
43 | Then, add a `'connection'` listener. The callback will contain a [`Connection`](https://github.com/mafintosh/turbo-net#connectiononconnect) and a [`Request`](https://github.com/mafintosh/turbo-http#requrl) object:
44 |
45 | ```javascript
46 | server.on('connection', (connection, req) => {
47 | const userAgent = req.getHeader('User-Agent');
48 |
49 | console.log(`Using ${userAgent} browser`);
50 | connection.send('Hello world!');
51 | });
52 | ```
53 |
54 | Finally call the `#listen(port)` method and run your Node app:
55 |
56 | ```javascript
57 | server.listen(port).then(() => {
58 | console.log(`Listening on *:${port}`);
59 | });
60 | ```
61 |
62 | ## Methods
63 |
64 | #### `connection.send(data)`
65 |
66 | Sends data to the client. Depending on the type of the passed object, it will send the data in one or multiple frames:
67 |
68 | * Strings get sent directly in one frame.
69 | * Objects get converted to strings through `JSON.stringify`.
70 | * Node's [Buffers](https://nodejs.org/api/buffer.html) get sent as binary data and may be sent in multiple frames.
71 |
72 | #### `connection.ping([payload])`
73 |
74 | Sends a ping frame that may contain a payload. The client must send a Pong frame with the same payload in response. Check the [`connection.on('pong')`](#connectiononpong) method for more details.
75 |
76 | #### `connection.close([callback])`
77 |
78 | Closes the connection.
79 |
80 | #### `server.broadcast(data)`
81 |
82 | Sends data to all the clients. Follows the same logic as [`connection.send(data)`](#connectionsenddata)
83 |
84 | #### `server.getConnections()`
85 |
86 | Get an unordered array containing the current active [connections](https://github.com/mafintosh/turbo-net#connectiononconnect).
87 |
88 | #### `server.close()`
89 |
90 | Closes the server. Returns a `Promise` that will get completed when all the connections are closed.
91 |
92 | ## Events
93 |
94 | Both the `Server` and the `Connection` are [EventEmitters](https://nodejs.org/api/events.html#events_class_eventemitter), so you can listen to these events:
95 |
96 | #### `server.on('connection', connection)`
97 |
98 | Emitted when a new connection is established. The callback arguments are `connection, request`, where the first is a turbo-net [Connection](https://github.com/mafintosh/turbo-net#connectiononconnect) and the later is a turbo-http [Request](https://github.com/mafintosh/turbo-http#requrl) object. Check out their docs to see what fields and methods are available.
99 |
100 | #### `server.on('close')`
101 |
102 | Emitted when the server has terminated all the WebSocket connections and it is going to die.
103 |
104 | #### `connection.on('text', string)`
105 |
106 | Emitted when the client sends some text data.
107 |
108 | #### `connection.on('binary', binaryStream)`
109 |
110 | Emitted when the client sends some buffered binary data. Returns a [BinaryStream](src/binaryStream.js), which is a wrapper for Node's [`stream.Readable`](https://nodejs.org/api/stream.html#stream_readable_streams). You can then add listeners to the stream:
111 |
112 | ```javascript
113 | const writable = fs.createWriteStream('file.txt');
114 |
115 | connection.on('binary', stream => {
116 | stream.pipe(writable);
117 |
118 | stream.on('end', () => {
119 | console.log('Saved client logs to disk!');
120 | });
121 | });
122 | ```
123 |
124 | #### `connection.on('ping')`
125 |
126 | Emitted when the server received a [Ping frame](https://tools.ietf.org/html/rfc6455#section-5.5.2) from the client.
127 |
128 | #### `connection.on('pong')`
129 |
130 | Emitted when the server received a [Pong frame](https://tools.ietf.org/html/rfc6455#section-5.5.3) from the client.
131 |
132 | #### `connection.on('close')`
133 |
134 | Emitted when a connection is fully closed. No other events will be emitted after.
135 |
136 | # License
137 |
138 | [MIT](LICENSE) © [Hugo Manrique](https://hugmanrique.me)
139 |
140 | [npm]: https://img.shields.io/npm/v/@hugmanrique/turbo-ws.svg
141 | [npm-url]: https://npmjs.com/package/@hugmanrique/turbo-ws
142 | [node]: https://img.shields.io/node/v/@hugmanrique/turbo-ws.svg
143 | [node-url]: https://nodejs.org
144 | [deps]: https://img.shields.io/david/hugmanrique/turbo-ws.svg
145 | [deps-url]: https://david-dm.org/hugmanrique/turbo-ws
146 | [tests]: https://img.shields.io/travis/hugmanrique/turbo-ws/master.svg
147 | [tests-url]: https://travis-ci.org/hugmanrique/turbo-ws
148 | [license-url]: LICENSE
149 | [license]: https://img.shields.io/github/license/hugmanrique/turbo-ws.svg
150 | [cover]: https://img.shields.io/coveralls/hugmanrique/turbo-ws.svg
151 | [cover-url]: https://coveralls.io/r/hugmanrique/turbo-ws/
152 |
--------------------------------------------------------------------------------
/TODO.md:
--------------------------------------------------------------------------------
1 | # TODOs
2 |
3 | * Send binary data in multiple frames
4 | * Add a buffer size limit to avoid DoS attacks
5 | * Clean up the frame system
6 | * Add coverage data
7 | * Add objective performance data to README
8 |
--------------------------------------------------------------------------------
/example.js:
--------------------------------------------------------------------------------
1 | const Server = require('./dist/').default;
2 |
3 | const port = 5000;
4 | const server = new Server();
5 |
6 | /* eslint-disable no-console */
7 |
8 | server.listen(port).then(() => {
9 | console.log(`⚡ Listening on *:${port}`);
10 | });
11 |
12 | server.on('connection', socket => {
13 | socket.send('message');
14 |
15 | socket.on('text', message => {
16 | console.log(`Client says "${message}"`);
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@hugmanrique/turbo-ws",
3 | "version": "0.1.0",
4 | "license": "MIT",
5 | "description": "Blazing fast low-level WebSocket server",
6 | "keywords": [
7 | "turbo",
8 | "websocket",
9 | "server"
10 | ],
11 | "homepage": "https://github.com/hugmanrique/turbo-ws",
12 | "bugs": {
13 | "url": "https://github.com/hugmanrique/turbo-ws/issues"
14 | },
15 | "author": {
16 | "name": "Hugo Manrique",
17 | "url": "https://hugmanrique.me",
18 | "email": "npm@hugmanrique.me"
19 | },
20 | "files": [
21 | "dist/",
22 | "README.md",
23 | "LICENSE"
24 | ],
25 | "main": "dist/index.js",
26 | "repository": {
27 | "type": "git",
28 | "url": "https://github.com/hugmanrique/turbo-ws"
29 | },
30 | "scripts": {
31 | "prebuild": "npm run clean",
32 | "build": "babel src -d dist --ignore 'src/**/*.test.js'",
33 | "prepublish": "npm run prebuild && npm run build",
34 | "clean": "del-cli dist",
35 | "release": "standard-version",
36 | "watch": "npm run build -- -w",
37 | "test": "jest",
38 | "test-ci": "jest && cat ./coverage/lcov.info | coveralls"
39 | },
40 | "engines": {
41 | "node": ">=8.10.0"
42 | },
43 | "dependencies": {
44 | "@hugmanrique/ws-extensions": "0.0.2",
45 | "turbo-http": "^0.3.0",
46 | "utf-8-validate": "^4.0.0"
47 | },
48 | "devDependencies": {
49 | "@babel/cli": "^7.0.0-beta.40",
50 | "@babel/core": "^7.0.0-beta.40",
51 | "@babel/plugin-proposal-object-rest-spread": "^7.0.0-beta.40",
52 | "@babel/preset-env": "^7.0.0-beta.40",
53 | "babel-core": "^7.0.0-bridge.0",
54 | "babel-jest": "^22.4.1",
55 | "coveralls": "^3.0.0",
56 | "del-cli": "^1.1.0",
57 | "jest": "^22.4.2",
58 | "standard-version": "^4.3.0",
59 | "ws": "^5.0.0"
60 | },
61 | "jest": {
62 | "testEnvironment": "node",
63 | "collectCoverage": true,
64 | "coverageDirectory": "coverage/",
65 | "testMatch": [
66 | "**/test/*.js"
67 | ]
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/Extension.js:
--------------------------------------------------------------------------------
1 | import { parse, serialize } from '@hugmanrique/ws-extensions';
2 |
3 | /* eslint-disable no-unused-vars */
4 |
5 | export default class Extension {
6 | constructor(options = {}, maxPayload) {
7 | this.options = options;
8 | this.maxPayload = maxPayload;
9 | }
10 |
11 | /**
12 | * Get extension name
13 | */
14 | static get name() {}
15 |
16 | /**
17 | * Accept an extension negotiation offer.
18 | * @param {Array} offers Extension negotiation offers. Contains parsed args for each offer.
19 | * @return {Object} Accepted params
20 | */
21 | static accept(offers) {}
22 |
23 | processData(receiver, data, callback) {}
24 | }
25 |
26 | export function handleNegotiation(server, socket, req) {
27 | const { extensions, options: { maxPayload } } = server;
28 |
29 | if (!extensions.length) {
30 | return;
31 | }
32 |
33 | socket.extensions = new Map();
34 | const { extensions: negotiated } = socket;
35 |
36 | try {
37 | const offers = parse(req.getHeader('Sec-WebSocket-Extensions'));
38 |
39 | for (const Extension of extensions) {
40 | const extName = Extension.name;
41 | const extOffers = getOfferParams(offers, extName);
42 |
43 | const accepted = Extension.accept(extOffers);
44 |
45 | if (!accepted) {
46 | continue;
47 | }
48 |
49 | const instance = new Extension(accepted, maxPayload);
50 |
51 | negotiated.set(extName, instance);
52 | }
53 | } catch (err) {
54 | return err;
55 | }
56 | }
57 |
58 | function getOfferParams(offers, extensionName) {
59 | return offers
60 | .filter(({ name }) => name === extensionName)
61 | .map(offer => offer.params);
62 | }
63 |
64 | /**
65 | * Builds the Sec-WebSocket-Extension header field value.
66 | *
67 | * @param {Object} extensions A Map containing extName -> instance entries.
68 | * @return {String} A string representing the given extension map.
69 | */
70 | export function serializeExtensions(extensions) {
71 | let header = '';
72 |
73 | for (const [name, { options }] of extensions) {
74 | header += serialize(name, options) + ', ';
75 | }
76 |
77 | if (!header) {
78 | return header;
79 | }
80 |
81 | return header.substring(0, header.length - 2);
82 | }
83 |
--------------------------------------------------------------------------------
/src/buffers.js:
--------------------------------------------------------------------------------
1 | export function unmask(buffer, mask) {
2 | const { length } = buffer;
3 |
4 | for (let i = 0; i < length; i++) {
5 | buffer[i] ^= mask[i & 3];
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/src/constants.js:
--------------------------------------------------------------------------------
1 | export const magicValue = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
2 |
3 | export const EMPTY_BUFFER = Buffer.alloc(0);
4 |
5 | export const opCodes = {
6 | CONTINUATION: 0x0,
7 | TEXT: 0x1,
8 | BINARY: 0x2,
9 | CLOSE: 0x8,
10 | PING: 0x9,
11 | PONG: 0xa
12 | };
13 |
14 | export const states = {
15 | CONNECTING: 1,
16 | OPEN: 2,
17 | CLOSING: 3,
18 | CLOSED: 4
19 | };
20 |
21 | // Allow 30 seconds to complete the close handshake
22 | export const CLOSE_TIMEOUT = 30 * 1000;
23 |
24 | /**
25 | * Maximum safe integer in JavaScript is 2^53 - 1. turbo-ws
26 | * returns an error if payload length is greater than this number.
27 | */
28 | export const frameSizeLimit = Math.pow(2, 53 - 32) - 1;
29 | export const frameSizeMult = Math.pow(2, 32);
30 |
--------------------------------------------------------------------------------
/src/extensions/PerMessageDeflate.js:
--------------------------------------------------------------------------------
1 | import Extension from '../Extension';
2 | //import { states } from '../socket/Receiver';
3 |
4 | export default class PerMessageDeflate extends Extension {
5 | constructor() {
6 | throw new Error('Not supported yet');
7 | }
8 |
9 | static get name() {
10 | return 'permessage-deflate';
11 | }
12 |
13 | /*static accept(offers) {}
14 |
15 | processData(/*receiver, data, callback) {
16 | throw new Error('Not supported yet');
17 |
18 | if (this.compressed) {
19 | receiver.state = states.INFLATING;
20 | // TODO Decompress and call callback
21 | return true;
22 | }
23 | }*/
24 | }
25 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Modification of the ws project. Copyright (c) 2011 Einar Otto Stangvik.
3 | * Available on https://github.com/websockets/ws/
4 | *
5 | * This work is licensed under the terms of the MIT license.
6 | */
7 |
8 | import { EventEmitter } from 'events';
9 | import http from 'turbo-http';
10 |
11 | import {
12 | statusCodes,
13 | asksForUpgrade,
14 | shouldHandleRequest,
15 | pathEquals,
16 | getUpgradeKey,
17 | addListeners,
18 | forwardEvent
19 | } from './util';
20 |
21 | import WebSocket from './socket';
22 | import { handleNegotiation, serializeExtensions } from './Extension';
23 |
24 | import { EMPTY_BUFFER } from './constants';
25 |
26 | export default class Server extends EventEmitter {
27 | constructor({
28 | maxPayload = 100 * 1024 * 1024,
29 | extensions = [],
30 | server,
31 | host,
32 | port,
33 | path = ''
34 | } = {}) {
35 | super();
36 |
37 | if (!server && (!host || !port)) {
38 | throw new TypeError(
39 | 'Either the "server" or the "host" and "port" options must be specified'
40 | );
41 | }
42 |
43 | if (!server) {
44 | server = http.createServer(this.handleRequest);
45 | server.listen(port);
46 | }
47 |
48 | // TODO Document how to attach request callback for custom servers
49 |
50 | this.server = server;
51 | this.options = { path, maxPayload };
52 | this.extensions = new Set(extensions);
53 |
54 | for (const extension of this.extensions) {
55 | extension.setup(maxPayload);
56 | }
57 |
58 | addListeners(server, {
59 | listening: forwardEvent('listening'),
60 | error: forwardEvent('error')
61 | });
62 | }
63 |
64 | handleRequest(req, res) {
65 | const { socket } = req;
66 |
67 | // Handle premature socket errors
68 | socket.on('error', onSocketError);
69 |
70 | if (!asksForUpgrade(req)) {
71 | return this.askToUpgrade(res);
72 | }
73 |
74 | const version = req.getHeader('Sec-WebSocket-Version');
75 |
76 | if (!shouldHandleRequest(this, req, version)) {
77 | return closeConnection(socket, res, 400);
78 | }
79 |
80 | const negotiationErr = handleNegotiation(this, socket, req);
81 |
82 | if (negotiationErr) {
83 | return closeConnection(socket, res, 400);
84 | }
85 |
86 | this.upgradeConnection(req, res);
87 | }
88 |
89 | askToUpgrade(res) {
90 | const body = statusCodes[426];
91 |
92 | // Ask the client to upgrade its protocol
93 | res.statusCode = 426;
94 | res.setHeader('Content-Type', 'text/plain');
95 | res.end(body);
96 | }
97 |
98 | upgradeConnection(req, res) {
99 | const { socket } = req;
100 |
101 | // Destroy socket if client already snt a FIN packet
102 | if (!socket.readable || !socket.writable) {
103 | return socket.destroy();
104 | }
105 |
106 | const clientKey = req.getHeader('Sec-WebSocket-Key');
107 | const key = getUpgradeKey(clientKey);
108 |
109 | res.statusCode = 101;
110 |
111 | res.setHeader('Upgrade', 'websocket');
112 | res.setHeader('Connection', 'upgrade');
113 | res.setHeader('Sec-WebSocket-Accept', key);
114 |
115 | const ws = new WebSocket(this.options.maxPayload);
116 |
117 | const { extensions } = socket;
118 |
119 | if (extensions) {
120 | const value = serializeExtensions(extensions);
121 | res.setHeader('Sec-WebSocket-Extensions', value);
122 | }
123 |
124 | this.emit('headers', res);
125 |
126 | // Finish the handshake but keep connection open
127 | res.end(EMPTY_BUFFER, 0);
128 |
129 | // Remove connection error listener
130 | socket.removeListener('error', onSocketError);
131 |
132 | ws.start(socket, extensions);
133 | this.emit('connection', ws, req);
134 | }
135 |
136 | // See if request should be handled by this server
137 | shouldHandle(req) {
138 | const { path } = this.options;
139 |
140 | return path === '' || pathEquals(path, req);
141 | }
142 | }
143 |
144 | function closeConnection(socket, res, code, message) {
145 | message = message || statusCodes[code];
146 |
147 | res.statusCode = code;
148 | res.setHeader('Connection', 'close');
149 | res.setHeader('Content-Type', 'text/plain');
150 |
151 | res.end(message);
152 |
153 | socket.removeListener('error', onSocketError);
154 | socket.close();
155 | }
156 |
157 | function onSocketError() {
158 | this.destroy();
159 | }
160 |
--------------------------------------------------------------------------------
/src/socket/Receiver.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Modification of the ws project. Copyright (c) 2011 Einar Otto Stangvik.
3 | * Available on https://github.com/websockets/ws/
4 | *
5 | * This work is licensed under the terms of the MIT license.
6 | */
7 |
8 | import { Writable } from 'stream';
9 | import { isValidUtf8, isValidStatusCode } from '../validate';
10 | import { unmask } from '../buffers';
11 | import {
12 | opCodes,
13 | frameSizeLimit,
14 | frameSizeMult,
15 | EMPTY_BUFFER
16 | } from '../constants';
17 |
18 | export const statusCodeKey = Symbol('status-code');
19 | const stopLoopKey = Symbol('stop-loop');
20 |
21 | export const states = {
22 | GET: 0,
23 | GET_PAYLOAD_LENGTH_16: 1,
24 | GET_PAYLOAD_LENGTH_64: 2,
25 | GET_MASK: 3,
26 | GET_DATA: 4,
27 | INFLATING: 5
28 | };
29 |
30 | class Receiver extends Writable {
31 | constructor(extensions, maxPayload) {
32 | super();
33 |
34 | this.buffers = [];
35 | this.bufferedBytes = 0;
36 | this.maxPayload = maxPayload | 0;
37 |
38 | this.payloadLength = 0;
39 | this.fragmented = 0;
40 | this.masked = false;
41 | this.fin = false;
42 | this.opCode = 0;
43 |
44 | this.totalPayloadLength = 0;
45 | this.messageLength = 0;
46 | this.fragments = new Set();
47 |
48 | this.extensions = extensions;
49 | this.state = states.GET;
50 | this.loop = false;
51 | }
52 |
53 | consume(length) {
54 | this.bufferedBytes -= length;
55 | const buffer = this.buffers[0];
56 |
57 | if (length === buffer.length) {
58 | return this.buffers.shift();
59 | } else if (length < buffer.length) {
60 | this.buffers[0] = buffer.slice(length);
61 |
62 | return buffer.slice(0, length);
63 | }
64 |
65 | const dst = Buffer.allocUnsafe(length);
66 |
67 | do {
68 | if (length >= buffer.length) {
69 | this.buffers.shift().copy(dst, dst.length - length);
70 | } else {
71 | buffer.copy(dst, dst.length - length, 0, length);
72 | }
73 |
74 | length -= buffer.length;
75 | } while (length > 0);
76 |
77 | return dst;
78 | }
79 |
80 | initParseLoop(callback) {
81 | this.loop = true;
82 |
83 | try {
84 | while (this.loop) {
85 | switch (this.state) {
86 | case states.GET:
87 | this.getInfo();
88 | break;
89 | case states.GET_PAYLOAD_LENGTH_16:
90 | this.getPayloadLength16();
91 | break;
92 | case states.GET_PAYLOAD_LENGTH_64:
93 | this.getPayloadLength64();
94 | break;
95 | case states.GET_MASK:
96 | this.getMask();
97 | break;
98 | case states.GET_DATA:
99 | this.getData(callback);
100 | break;
101 | // Inflating state
102 | default:
103 | this.loop = false;
104 | return;
105 | }
106 | }
107 | } catch (err) {
108 | this.loop = false;
109 | callback(err);
110 | }
111 | }
112 |
113 | // Read first 2 bytes of frame
114 | getInfo() {
115 | if (this.bufferedBytes < 2) {
116 | this.loop = false;
117 | return;
118 | }
119 |
120 | const buffer = this.consume(2);
121 |
122 | if ((buffer[0] & 0x30) !== 0x0) {
123 | error('RSV2 and RSV3 must be clear', 1002);
124 | }
125 |
126 | const compressed = (buffer[0] & 0x40) === 0x40;
127 |
128 | if (compressed) {
129 | // && !decompressionEnabled
130 | error('RSV1 must be clear', 1002);
131 | }
132 |
133 | this.fin = (buffer[0] & 0x80) === 0x80;
134 | this.opCode = buffer[0] & 0xf;
135 | this.payloadLength = buffer[1] & 0x7f;
136 |
137 | if (this.opCode === opCodes.CONTINUATION) {
138 | if (compressed) {
139 | error('RSV1 must be clear', 1002);
140 | }
141 |
142 | if (!this.fragmented) {
143 | error('Invalid opCode 0', 1002);
144 | }
145 |
146 | this.opCode = this.fragmented;
147 | } else if (this.opCode === opCodes.TEXT || this.opCode === opCodes.BINARY) {
148 | if (this.fragmented) {
149 | error(`Invalid opCode ${this.opCode}`, 1002);
150 | }
151 |
152 | this.compressed = compressed;
153 | } else if (isControlOpCode(this.opCode)) {
154 | if (!this.fin) {
155 | error('FIN must be set', 1002);
156 | }
157 |
158 | if (compressed) {
159 | error('RSV1 must be clear', 1002);
160 | }
161 |
162 | if (this.payloadLength > 0x7d) {
163 | error(`Invalid payload length ${this.payloadLength}`, 1002);
164 | }
165 | } else {
166 | error(`Invalid opCode ${this.opCode}`, 1002);
167 | }
168 |
169 | if (!this.fin && !this.fragmented) {
170 | this.fragmented = this.opCode;
171 | }
172 |
173 | this.masked = (buffer[1] & 0x80) === 0x80;
174 | const length = this.payloadLength;
175 |
176 | if (length === 126) {
177 | this.state = states.GET_PAYLOAD_LENGTH_16;
178 | } else if (length === 127) {
179 | this.state = states.GET_PAYLOAD_LENGTH_64;
180 | } else {
181 | return this.haveLength();
182 | }
183 | }
184 |
185 | getPayloadLength16() {
186 | if (this.bufferedBytes < 2) {
187 | this.loop = false;
188 | return;
189 | }
190 |
191 | this.payloadLength = this.consume(2).readUInt16BE(0);
192 | return this.haveLength();
193 | }
194 |
195 | getPayloadLength64() {
196 | if (this.bufferedBytes < 8) {
197 | this.loop = false;
198 | return;
199 | }
200 |
201 | const buffer = this.consume(8);
202 | const num = buffer.readUInt32BE(0);
203 |
204 | if (num > frameSizeLimit) {
205 | error('Payload length > 2^53 - 1', 1009);
206 | }
207 |
208 | this.payloadLength = num * frameSizeMult + buffer.readUInt32BE(4);
209 | return this.haveLength();
210 | }
211 |
212 | haveLength() {
213 | if (this.payloadLength && !isControlOpCode(this.opCode)) {
214 | this.totalPayloadLength += this.payloadLength;
215 |
216 | // Check max payload size
217 | if (this.maxPayload > 0 && this.totalPayloadLength > this.maxPayload) {
218 | error('Maximum payload size exceeded', 1009);
219 | }
220 | }
221 |
222 | this.state = this.masked ? states.GET_MASK : states.GET_DATA;
223 | }
224 |
225 | getMask() {
226 | if (this.bufferedBytes < 4) {
227 | this.loop = false;
228 | return;
229 | }
230 |
231 | this.mask = this.consume(4);
232 | this.state = states.GET_DATA;
233 | }
234 |
235 | getData(callback) {
236 | let data = EMPTY_BUFFER;
237 |
238 | if (this.payloadLength) {
239 | if (this.bufferedBytes < this.payloadLength) {
240 | this.loop = false;
241 | return;
242 | }
243 |
244 | data = this.consume(this.payloadLength);
245 |
246 | if (this.masked) {
247 | unmask(data, this.mask);
248 | }
249 | }
250 |
251 | if (isControlOpCode(this.opCode)) {
252 | return this.controlMessage(data, this.mask);
253 | }
254 |
255 | // Let extensions process the data
256 | for (const [, extension] of this.extensions) {
257 | const stopRead = extension.processData(this, data, callback);
258 |
259 | if (stopRead) {
260 | return;
261 | }
262 | }
263 |
264 | if (data.length) {
265 | this.messageLength = this.totalPayloadLength;
266 | this.fragments.add(data);
267 | }
268 |
269 | return this.dataMessage();
270 | }
271 |
272 | dataMessage() {
273 | if (this.fin) {
274 | const { messageLength, fragments } = this;
275 |
276 | this.totalPayloadLength = 0;
277 | this.messageLength = 0;
278 | this.fragmented = 0;
279 | this.fragments.clear();
280 |
281 | const data = toBuffer(fragments, messageLength);
282 |
283 | if (this.opCode === opCodes.BINARY) {
284 | this.emit('message', data);
285 | } else {
286 | validateUtf8(data, true);
287 |
288 | this.emit('message', data.toString());
289 | }
290 | }
291 |
292 | this.state = states.GET_INFO;
293 | }
294 |
295 | controlMessage(data) {
296 | if (this.opCode === opCodes.CLOSE) {
297 | this.loop = false;
298 |
299 | if (!data.length) {
300 | this.emit('conclude', 1005);
301 | this.end();
302 | } else if (data.length === 1) {
303 | error('Invalid payload length 1', 1002, false);
304 | } else {
305 | const code = data.readUInt16BE(0);
306 |
307 | if (!isValidStatusCode(code)) {
308 | error(`Invalid status code ${code}`, 1002, false);
309 | }
310 |
311 | const buffer = data.slice(2);
312 |
313 | validateUtf8(buffer);
314 |
315 | this.emit('conclude', code, buffer.toString());
316 | this.end();
317 | }
318 |
319 | return;
320 | }
321 |
322 | this.emit(this.opCode === opCodes.PING ? 'ping' : 'pong', data);
323 | this.state = states.GET_INFO;
324 | }
325 | }
326 |
327 | function validateUtf8(buffer, stopLoop) {
328 | if (!isValidUtf8(buffer)) {
329 | error('Invalid UTF-8 sequence', 1007, stopLoop, Error);
330 | }
331 | }
332 |
333 | function error(message, statusCode, stopLoop = true, Constructor = RangeError) {
334 | const err = new Constructor(message);
335 | err[statusCodeKey] = statusCode;
336 | err[stopLoopKey] = stopLoop;
337 |
338 | throw err;
339 | }
340 |
341 | function isControlOpCode(opCode) {
342 | return opCode > 0x7 && opCode < 0xb;
343 | }
344 |
345 | function toBuffer(fragments, messageLength) {
346 | if (fragments.length === 1) {
347 | // TODO Avoid creating an entire iterator to get the first value
348 | return fragments.values().next().value;
349 | }
350 |
351 | if (fragments.length > 1) {
352 | return Buffer.concat(fragments, messageLength);
353 | }
354 |
355 | return EMPTY_BUFFER;
356 | }
357 |
358 | export default Receiver;
359 |
--------------------------------------------------------------------------------
/src/socket/Sender.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Modification of the ws project. Copyright (c) 2011 Einar Otto Stangvik.
3 | * Available on https://github.com/websockets/ws/
4 | *
5 | * This work is licensed under the terms of the MIT license.
6 | */
7 |
8 | import { EMPTY_BUFFER, opCodes } from '../constants';
9 |
10 | /**
11 | * HyBi sender implementation
12 | * @see https://tools.ietf.org/html/rfc6455
13 | */
14 | export default class Sender {
15 | /**
16 | * @param {Object} socket A turbo-net connection
17 | */
18 | constructor(socket) {
19 | this.socket = socket;
20 | this.firstFragment = true;
21 |
22 | // TODO Implement compression
23 | this.compress = false;
24 |
25 | this.bufferedBytes = 0;
26 | this.deflating = false;
27 | this.queue = [];
28 | }
29 |
30 | /**
31 | * Sends a close message.
32 | * @param {(Number|undefined)} code The status code of the body
33 | * @param {String} data The message of the body
34 | * @param {Function} callback
35 | */
36 | close(code, data, callback) {
37 | let buffer;
38 |
39 | if (!code) {
40 | buffer = EMPTY_BUFFER;
41 | } else if (typeof code !== 'number') {
42 | throw new TypeError('First argument must be a valid error code number');
43 | }
44 |
45 | buffer = Buffer.allocUnsafe(2 + (data ? Buffer.byteLength(data) : 0));
46 | buffer.writeUInt16BE(code, 0, true);
47 |
48 | if (data) {
49 | buffer.write(data, 2);
50 | }
51 |
52 | if (this.deflating) {
53 | this.queue([this._close, buffer, callback]);
54 | } else {
55 | this._close(buffer, callback);
56 | }
57 | }
58 |
59 | _close(data, callback) {
60 | this.sendFrame(
61 | frame(data, {
62 | fin: true,
63 | opCode: opCodes.CLOSE
64 | }),
65 | callback
66 | );
67 | }
68 |
69 | /**
70 | * Sends a ping message.
71 | * @param {*} data The message to send
72 | * @param {Function} callback
73 | */
74 | ping(data, callback) {
75 | if (!Buffer.isBuffer(data)) {
76 | data = Buffer.from(data);
77 | }
78 |
79 | if (this.deflating) {
80 | this.queue([this._ping, data, callback]);
81 | } else {
82 | this.ping(data, callback);
83 | }
84 | }
85 |
86 | _ping(data, callback) {
87 | this.sendFrame(
88 | frame(data, {
89 | fin: true,
90 | opCode: opCodes.PING
91 | }),
92 | callback
93 | );
94 | }
95 |
96 | /**
97 | * Sends a pong message
98 | * @param {*} data The message to send
99 | * @param {Function} callback
100 | */
101 | pong(data, callback) {
102 | if (!Buffer.isBuffer(data)) {
103 | data = Buffer.from(data);
104 | }
105 |
106 | if (this.deflating) {
107 | this.queue([this._pong, data, callback]);
108 | } else {
109 | this._pong(data, callback);
110 | }
111 | }
112 |
113 | _pong(data, callback) {
114 | this.sendFrame(
115 | frame(data, {
116 | fin: true,
117 | opCode: opCodes.PONG
118 | }),
119 | callback
120 | );
121 | }
122 |
123 | /**
124 | * Sends a data message.
125 | * @param {*} data The message to send
126 | * @param {Object} options
127 | * @param {Boolean} options.compress Whether or not to compress data.
128 | * @param {Boolean} options.binary Whether data is binary or text.
129 | * @param {Boolean} options.fin Whether the fragment is the last one.
130 | * @param {Function} callback
131 | */
132 | send(data, { compress, binary, fin }, callback) {
133 | let opCode = binary ? opCodes.BINARY : opCodes.TEXT;
134 | let rsv1 = compress;
135 |
136 | if (!Buffer.isBuffer(data)) {
137 | data = Buffer.from(data);
138 | }
139 |
140 | if (compress) {
141 | throw new Error('Unsupported operation, not implemented yet');
142 | }
143 |
144 | if (this.firstFragment) {
145 | this.firstFragment = false;
146 |
147 | // TODO Implement compression limit
148 |
149 | this.compress = rsv1;
150 | } else {
151 | rsv1 = false;
152 | opCode = opCodes.CONTINUATION;
153 | }
154 |
155 | if (fin) {
156 | this.firstFragment = true;
157 | }
158 |
159 | if (compress) {
160 | // TODO
161 | } else {
162 | this.sendFrame(
163 | frame(data, {
164 | fin,
165 | rsv1: false,
166 | opCode
167 | }),
168 | callback
169 | );
170 | }
171 | }
172 |
173 | /**
174 | * Executes queued send operations
175 | */
176 | dequeue() {
177 | while (!this.deflating && this.queue.length) {
178 | const [fn, ...params] = this.queue.shift();
179 |
180 | this.bufferedBytes = params[0].length;
181 | fn.apply(this, params);
182 | }
183 | }
184 |
185 | /**
186 | * Enqueues a send operation
187 | * @param {Array} params Send operation parameters.
188 | */
189 | queue(params) {
190 | this.bufferedBytes += params[1].length;
191 | this.queue.push(params);
192 | }
193 |
194 | /**
195 | * Sends a frame
196 | * @param {Buffer[]} buffers The frame to send
197 | * @param {Function} callback
198 | */
199 | sendFrame(buffers, callback) {
200 | const multiple = buffers.length === 2;
201 |
202 | // Attach callback to last write
203 | this.socket.write(buffers[0], !multiple && callback);
204 | this.socket.write(buffers[1], multiple && callback);
205 | }
206 | }
207 |
208 | /**
209 | * Frames a piece of data into multiple buffers.
210 | * @param {Buffer} data The data to frame.
211 | * @param {Number} options.opCode The opCode
212 | * @param {Boolean} options.fin Whether or not to set the FIN bit
213 | * @param {Boolean} options.rsv1 Whether or not to set the RSV1 bit
214 | * @return {Buffer[]} The framed data as a list of Buffer instances
215 | */
216 | function frame(data, { fin, opCode, rsv1 }) {
217 | const merge = data.length < 1024;
218 | let offset = 2;
219 | let payloadLength = data.length;
220 |
221 | if (data.length >= 65536) {
222 | offset += 8;
223 | payloadLength = 127;
224 | } else if (data.length > 125) {
225 | offset += 2;
226 | payloadLength = 126;
227 | }
228 |
229 | const target = Buffer.allocUnsafe(merge ? data.length + offset : offset);
230 |
231 | target[0] = fin ? opCode | 0x80 : opCode;
232 |
233 | if (rsv1) {
234 | target[0] |= 0x40;
235 | }
236 |
237 | if (payloadLength === 126) {
238 | target.writeUInt16BE(data.length, 2, true);
239 | } else if (payloadLength === 127) {
240 | target.writeUInt32BE(0, 2, true);
241 | target.writeUInt32BE(data.length, 6, true);
242 | }
243 |
244 | target[1] = payloadLength;
245 |
246 | if (merge) {
247 | data.copy(target, offset);
248 | return [target];
249 | }
250 |
251 | return [target, data];
252 | }
253 |
--------------------------------------------------------------------------------
/src/socket/index.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Modification of the ws project. Copyright (c) 2011 Einar Otto Stangvik.
3 | * Available on https://github.com/websockets/ws/
4 | *
5 | * This work is licensed under the terms of the MIT license.
6 | */
7 |
8 | import EventEmitter from 'events';
9 | import Receiver, { statusCodeKey } from './Receiver';
10 | import Sender from './Sender';
11 |
12 | import { states, CLOSE_TIMEOUT, EMPTY_BUFFER } from '../constants';
13 |
14 | export default class WebSocket extends EventEmitter {
15 | constructor(maxPayload) {
16 | super();
17 |
18 | this.state = states.CONNECTING;
19 | this.receivedCloseFrame = false;
20 | this.maxPayload = maxPayload;
21 | }
22 |
23 | get extensions() {
24 | return this._extensions.values();
25 | }
26 |
27 | start(socket, extensions) {
28 | const receiver = new Receiver(extensions, this.maxPayload);
29 |
30 | this.sender = new Sender(this.socket);
31 | this.receiver = receiver;
32 | this.socket = socket;
33 | this._extensions = extensions;
34 |
35 | // Attach all the internal listeners
36 | const onReceiverFinish = data => {
37 | this.emit('message', data);
38 | };
39 |
40 | const onSocketData = this.addSocketListeners(onReceiverFinish);
41 | this.addReceiverListeners(onSocketData, onReceiverFinish);
42 | }
43 |
44 | addSocketListeners(receiverOnFinish) {
45 | const { socket, receiver } = this;
46 |
47 | const onData = buffer => {
48 | if (!receiver.write(buffer)) {
49 | socket.pause();
50 | }
51 | };
52 |
53 | const onEnd = () => {
54 | this.state = states.CLOSING;
55 | receiver.end();
56 | this.end();
57 | };
58 |
59 | const onClose = () => {
60 | socket.removeListener('close', onClose);
61 | socket.removeListener('data', onData);
62 | socket.removeListener('end', onEnd);
63 |
64 | this.state = states.CLOSING;
65 | receiver.end();
66 |
67 | clearTimeout(this.closeTimer);
68 |
69 | const { _writableState: { finished, errorEmitted } } = receiver;
70 |
71 | if (finished || errorEmitted) {
72 | this.emitClose();
73 | } else {
74 | receiver.on('error', receiverOnFinish);
75 | receiver.on('finish', receiverOnFinish);
76 | }
77 | };
78 |
79 | const onError = () => {
80 | socket.removeListener('error', onError);
81 | socket.on('error', () => {});
82 |
83 | this.state = states.CLOSING;
84 | socket.destroy();
85 | };
86 |
87 | socket.on('close', onClose);
88 | socket.on('data', onData);
89 | socket.on('end', onEnd);
90 | socket.on('error', onError);
91 |
92 | return onData;
93 | }
94 |
95 | addReceiverListeners(socketOnData, onFinish) {
96 | const { socket, receiver } = this;
97 |
98 | receiver.on('conclude', (code, reason) => {
99 | socket.removeListener('data', socketOnData);
100 | socket.resume();
101 |
102 | this.receivedCloseFrame = true;
103 | this.closeMessage = reason;
104 | this.closeCode = code;
105 |
106 | if (code === 1005) {
107 | this.close();
108 | } else {
109 | this.close(code, reason);
110 | }
111 | });
112 |
113 | receiver.on('drain', () => {
114 | socket.resume();
115 | });
116 |
117 | receiver.on('error', err => {
118 | this.state = states.CLOSING;
119 |
120 | this.closeCode = err[statusCodeKey];
121 | this.emit('error', err);
122 | socket.destroy();
123 | });
124 |
125 | receiver.on('finish', onFinish);
126 |
127 | receiver.on('ping', data => {
128 | this.pong(data);
129 | this.emit('ping', data);
130 | });
131 |
132 | receiver.on('pong', data => {
133 | this.emit('pong', data);
134 | });
135 | }
136 |
137 | emitClose() {
138 | const { socket, closeCode, closeMessage } = this;
139 | this.state = states.CLOSED;
140 |
141 | if (socket) {
142 | for (const extension of this.extensions) {
143 | extension.cleanup();
144 | }
145 | }
146 |
147 | this.emit('close', closeCode, closeMessage);
148 | }
149 |
150 | /**
151 | * Start a closing handshake
152 | * @param {Number} code Status code explaining why the connection is closing
153 | * @param {String} data Message explaining why the connection is closing
154 | */
155 | close(code, data) {
156 | const { state, sender, socket, sentCloseFrame, receivedCloseFrame } = this;
157 |
158 | if (state === states.CLOSED) {
159 | return;
160 | }
161 |
162 | if (state === states.CONNECTING) {
163 | // TODO Fix
164 | return;
165 | /*return abortHandshake(
166 | this,
167 | 'WebSocket was closed before connection was established'
168 | );*/
169 | }
170 |
171 | if (state === states.CLOSING) {
172 | if (sentCloseFrame && receivedCloseFrame) {
173 | return socket.end();
174 | }
175 | }
176 |
177 | this.state = states.CLOSING;
178 |
179 | sender.close(code, data, err => {
180 | // Error handled by the socket
181 | if (err) return;
182 |
183 | this.sentCloseFrame = true;
184 |
185 | if (socket.writable) {
186 | if (this.receivedCloseFrame) {
187 | socket.end();
188 | }
189 | }
190 |
191 | // Ensure connection is closed even if the handshake fails
192 | this.closeTimer = setTimeout(socket.destroy.bind(socket), CLOSE_TIMEOUT);
193 | });
194 | }
195 |
196 | /**
197 | * Sends a ping frame
198 | * @param {*} data The data to send
199 | * @param {Function} callback Executed when the ping is sent
200 | */
201 | ping(data, callback) {
202 | if (!this._verifyOpen(callback)) {
203 | return;
204 | }
205 |
206 | this.sender.ping(data || EMPTY_BUFFER, callback);
207 | }
208 |
209 | /**
210 | * Sends a pong frame
211 | * @param {*} data The data to send
212 | * @param {Function} callback Execute when the pong is sent
213 | */
214 | pong(data, callback) {
215 | if (!this._verifyOpen(callback)) {
216 | return;
217 | }
218 |
219 | this.sender.pong(data, callback);
220 | }
221 |
222 | /**
223 | * Sends a data message
224 | * @param {*} data The message to send
225 | * @param {Object} options Options object
226 | * @param {Boolean} options.compress Whether or not to compress data.
227 | * @param {Boolean} options.binary Whether data is binary or text.
228 | * @param {Boolean} options.fin Whether the fragment is the last one.
229 | * @param {Function} callback Executed when data is written out.
230 | */
231 | send(data, options = {}, callback) {
232 | if (typeof options === 'function') {
233 | callback = options;
234 | options = {};
235 | }
236 |
237 | if (!this._verifyOpen(callback)) {
238 | return;
239 | }
240 |
241 | if (options.compress) {
242 | throw new Error('Unsupported operation: not implemented yet');
243 | }
244 |
245 | this.sender.send(data || EMPTY_BUFFER, {
246 | binary: typeof data !== 'string',
247 | fin: true,
248 | ...options
249 | });
250 | }
251 |
252 | _verifyOpen(callback) {
253 | const { state } = this;
254 |
255 | if (state === states.OPEN) {
256 | return true;
257 | }
258 |
259 | const err = new Error(
260 | `WebSocket connection is not open, state is ${state}`
261 | );
262 |
263 | if (callback) {
264 | callback(err);
265 | return;
266 | }
267 |
268 | throw err;
269 | }
270 | }
271 |
--------------------------------------------------------------------------------
/src/util.js:
--------------------------------------------------------------------------------
1 | import crypto from 'crypto';
2 | import { parse } from 'url';
3 | import { magicValue } from './constants';
4 |
5 | import statusCodes from 'turbo-http/http-status';
6 | export { statusCodes };
7 |
8 | // Upgrade utils
9 | export function asksForUpgrade(req) {
10 | return (
11 | req.method === 'GET' &&
12 | req.getHeader('Upgrade').toLowerCase() === 'websocket'
13 | );
14 | }
15 |
16 | export function shouldHandleRequest(server, req, version) {
17 | return (
18 | isValidVersion(version) && server.shouldHandle(req) // TODO Create version func
19 | );
20 | }
21 |
22 | function isValidVersion(version) {
23 | return version === 8 || version === 13;
24 | }
25 |
26 | export function pathEquals(path, req) {
27 | return parse(req.url).pathname === path;
28 | }
29 |
30 | export function getUpgradeKey(clientKey) {
31 | return crypto
32 | .createHash('sha1')
33 | .update(`${clientKey}${magicValue}`, 'binary')
34 | .digest('base64');
35 | }
36 |
37 | // EventEmitter utils
38 |
39 | export function addListeners(server, events) {
40 | const eventNames = Object.keys(events);
41 |
42 | for (const event of eventNames) {
43 | server.on(event, events[event]);
44 | }
45 |
46 | // Return anonymous function to remove all the added listeners when called
47 | return function() {
48 | for (const event of eventNames) {
49 | server.removeListener(event, events[event]);
50 | }
51 | };
52 | }
53 |
54 | export function forwardEvent(server, eventName) {
55 | return server.emit.bind(server, eventName);
56 | }
57 |
--------------------------------------------------------------------------------
/src/validate.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Modification of the ws project. Copyright (c) 2011 Einar Otto Stangvik.
3 | * Available on https://github.com/websockets/ws/
4 | *
5 | * This work is licensed under the terms of the MIT license.
6 | */
7 |
8 | import isValidUtf8 from 'utf-8-validate';
9 |
10 | export { isValidUtf8 };
11 |
12 | /**
13 | * Check if a status code is allowed in a CLOSE frame
14 | */
15 | export function isValidStatusCode(code) {
16 | return (
17 | (code >= 1000 &&
18 | code <= 1013 &&
19 | code !== 1004 &&
20 | code !== 1005 &&
21 | code !== 1006) ||
22 | (code >= 3000 && code <= 4999)
23 | );
24 | }
25 |
--------------------------------------------------------------------------------
/test/extensions.js:
--------------------------------------------------------------------------------
1 | import Extension, {
2 | handleNegotiation,
3 | serializeExtensions
4 | } from '../src/Extension';
5 |
6 | class DummyExtension extends Extension {
7 | static get name() {
8 | return 'Dummy';
9 | }
10 |
11 | static accept(offers) {
12 | // Always return the first one
13 | return offers[0];
14 | }
15 | }
16 |
17 | const mockServer = {
18 | options: {
19 | maxPayload: 100 * 1024 * 1024
20 | },
21 | extensions: []
22 | };
23 |
24 | function buildReq(returnOffer) {
25 | return {
26 | getHeader() {
27 | return returnOffer;
28 | }
29 | };
30 | }
31 |
32 | describe('extensions', () => {
33 | describe('negotiation', () => {
34 | it("should skip extension check if server doesn't have extensions", () => {
35 | const socket = {};
36 |
37 | handleNegotiation(mockServer, socket, buildReq('Dummy'));
38 |
39 | expect(socket.extensions).toBeUndefined();
40 | });
41 |
42 | it('should handle basic negotiation', () => {
43 | mockServer.extensions = [DummyExtension];
44 | const socket = {};
45 |
46 | const err = handleNegotiation(mockServer, socket, buildReq('Dummy'));
47 |
48 | expect(err).toBeUndefined();
49 | expect(socket.extensions).toBeDefined();
50 | expect(socket.extensions.has('Dummy')).toBe(true);
51 |
52 | const instance = socket.extensions.get('Dummy');
53 | expect(instance.constructor).toBe(DummyExtension);
54 | expect(instance.maxPayload).toBe(mockServer.options.maxPayload);
55 | });
56 |
57 | it('should handle multiple offers', () => {
58 | const socket = {};
59 |
60 | handleNegotiation(mockServer, socket, buildReq('Dummy; value=1, Dummy'));
61 |
62 | expect(socket.extensions.has('Dummy')).toBe(true);
63 |
64 | const instance = socket.extensions.get('Dummy');
65 | expect(instance.options).toEqual({ value: 1 });
66 | });
67 |
68 | it('should ignore not supported offer', () => {
69 | const socket = {};
70 |
71 | handleNegotiation(mockServer, socket, buildReq('NotSupported'));
72 |
73 | expect(socket.extensions.has('Dummy2')).toBe(false);
74 | });
75 | });
76 |
77 | describe('serialization', () => {
78 | it('should serialize single extension', () => {
79 | expect(serializeWrap(['Dummy', new DummyExtension()])).toBe('Dummy');
80 | });
81 |
82 | it('should serialize extension with params', () => {
83 | const ext = new DummyExtension({ value: 1 });
84 |
85 | expect(serializeWrap(['Dummy', ext])).toBe('Dummy; value=1');
86 | });
87 |
88 | it('should serialize multiple extensions', () => {
89 | expect(
90 | serializeWrap(
91 | ['Dummy', new DummyExtension()],
92 | ['Dummy2', new DummyExtension()]
93 | )
94 | ).toBe('Dummy, Dummy2');
95 | });
96 |
97 | it('should serialize multiple extensions with params', () => {
98 | const ext = new DummyExtension({ value: 'str' });
99 |
100 | expect(
101 | serializeWrap(['Dummy', ext], ['Dummy2', new DummyExtension()])
102 | ).toBe('Dummy; value=str, Dummy2');
103 | });
104 | });
105 | });
106 |
107 | function serializeWrap() {
108 | return serializeExtensions(new Map(arguments));
109 | }
110 |
--------------------------------------------------------------------------------
/test/server.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hugmanrique/turbo-ws/16123cacdbd3bfa362452ed08f7aaa2efc10c65d/test/server.js
--------------------------------------------------------------------------------
/test/validation.js:
--------------------------------------------------------------------------------
1 | import { isValidStatusCode } from '../src/validate';
2 |
3 | describe('validation', () => {
4 | test('close frame status codes', () => {
5 | const closeCodes = [
6 | 1000,
7 | 1001,
8 | 1002,
9 | 1003,
10 | 1007,
11 | 1008,
12 | 1009,
13 | 1010,
14 | 1011,
15 | 1012,
16 | 1013,
17 | 3000,
18 | 3604,
19 | 4505,
20 | 4999
21 | ];
22 |
23 | const value = reduceBoolArray(closeCodes.map(isValidStatusCode));
24 |
25 | expect(value).toBe(true);
26 | });
27 |
28 | test('invalid close frame status code fail', () => {
29 | const invalidCodes = [
30 | 0,
31 | 52,
32 | 484,
33 | 999,
34 | 1014,
35 | 1015,
36 | 2000,
37 | 2305,
38 | 2752,
39 | 2998,
40 | 5000,
41 | 7505
42 | ];
43 |
44 | const value = reduceBoolArray(
45 | invalidCodes.map(code => !isValidStatusCode(code))
46 | );
47 |
48 | expect(value).toBe(true);
49 | });
50 | });
51 |
52 | function reduceBoolArray(array) {
53 | return array.reduce((prev, current) => prev && current);
54 | }
55 |
--------------------------------------------------------------------------------