refacto: Replaced useless DB queries by websocket calls + patching WS auth-token leak
This commit is contained in:
21
node_modules/.package-lock.json
generated
vendored
21
node_modules/.package-lock.json
generated
vendored
@@ -1114,6 +1114,27 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ws": {
|
||||||
|
"version": "8.20.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz",
|
||||||
|
"integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"bufferutil": "^4.0.1",
|
||||||
|
"utf-8-validate": ">=5.0.2"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"bufferutil": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"utf-8-validate": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/xtend": {
|
"node_modules/xtend": {
|
||||||
"version": "4.0.2",
|
"version": "4.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
||||||
|
|||||||
20
node_modules/ws/LICENSE
generated
vendored
Normal file
20
node_modules/ws/LICENSE
generated
vendored
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
Copyright (c) 2011 Einar Otto Stangvik <einaros@gmail.com>
|
||||||
|
Copyright (c) 2013 Arnout Kazemier and contributors
|
||||||
|
Copyright (c) 2016 Luigi Pinca and contributors
|
||||||
|
|
||||||
|
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.
|
||||||
548
node_modules/ws/README.md
generated
vendored
Normal file
548
node_modules/ws/README.md
generated
vendored
Normal file
@@ -0,0 +1,548 @@
|
|||||||
|
# ws: a Node.js WebSocket library
|
||||||
|
|
||||||
|
[](https://www.npmjs.com/package/ws)
|
||||||
|
[](https://github.com/websockets/ws/actions?query=workflow%3ACI+branch%3Amaster)
|
||||||
|
[](https://coveralls.io/github/websockets/ws)
|
||||||
|
|
||||||
|
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 backend 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](#opt-in-for-performance)
|
||||||
|
- [Legacy opt-in for performance](#legacy-opt-in-for-performance)
|
||||||
|
- [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)
|
||||||
|
- [Client authentication](#client-authentication)
|
||||||
|
- [Server broadcast](#server-broadcast)
|
||||||
|
- [Round-trip time](#round-trip-time)
|
||||||
|
- [Use the Node.js streams API](#use-the-nodejs-streams-api)
|
||||||
|
- [Other examples](#other-examples)
|
||||||
|
- [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
|
||||||
|
|
||||||
|
- **HyBi drafts 07-12** (Use the option `protocolVersion: 8`)
|
||||||
|
- **HyBi drafts 13-17** (Current default, alternatively option
|
||||||
|
`protocolVersion: 13`)
|
||||||
|
|
||||||
|
## Installing
|
||||||
|
|
||||||
|
```
|
||||||
|
npm install ws
|
||||||
|
```
|
||||||
|
|
||||||
|
### Opt-in for performance
|
||||||
|
|
||||||
|
[bufferutil][] is an optional module that can be installed alongside the ws
|
||||||
|
module:
|
||||||
|
|
||||||
|
```
|
||||||
|
npm install --save-optional bufferutil
|
||||||
|
```
|
||||||
|
|
||||||
|
This is a binary addon that improves the performance of certain operations such
|
||||||
|
as masking and unmasking the data payload of the WebSocket frames. Prebuilt
|
||||||
|
binaries are available for the most popular platforms, so you don't necessarily
|
||||||
|
need to have a C++ compiler installed on your machine.
|
||||||
|
|
||||||
|
To force ws to not use bufferutil, use the
|
||||||
|
[`WS_NO_BUFFER_UTIL`](./doc/ws.md#ws_no_buffer_util) environment variable. This
|
||||||
|
can be useful to enhance security in systems where a user can put a package in
|
||||||
|
the package search path of an application of another user, due to how the
|
||||||
|
Node.js resolver algorithm works.
|
||||||
|
|
||||||
|
#### Legacy opt-in for performance
|
||||||
|
|
||||||
|
If you are running on an old version of Node.js (prior to v18.14.0), ws also
|
||||||
|
supports the [utf-8-validate][] module:
|
||||||
|
|
||||||
|
```
|
||||||
|
npm install --save-optional utf-8-validate
|
||||||
|
```
|
||||||
|
|
||||||
|
This contains a binary polyfill for [`buffer.isUtf8()`][].
|
||||||
|
|
||||||
|
To force ws not to use utf-8-validate, use the
|
||||||
|
[`WS_NO_UTF_8_VALIDATE`](./doc/ws.md#ws_no_utf_8_validate) environment variable.
|
||||||
|
|
||||||
|
## API docs
|
||||||
|
|
||||||
|
See [`/doc/ws.md`](./doc/ws.md) for Node.js-like documentation of ws classes and
|
||||||
|
utility functions.
|
||||||
|
|
||||||
|
## 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
|
||||||
|
import WebSocket, { WebSocketServer } from 'ws';
|
||||||
|
|
||||||
|
const wss = new WebSocketServer({
|
||||||
|
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.
|
||||||
|
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 if context takeover is disabled.
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
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`.
|
||||||
|
|
||||||
|
```js
|
||||||
|
import WebSocket from 'ws';
|
||||||
|
|
||||||
|
const ws = new WebSocket('ws://www.host.com/path', {
|
||||||
|
perMessageDeflate: false
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage examples
|
||||||
|
|
||||||
|
### Sending and receiving text data
|
||||||
|
|
||||||
|
```js
|
||||||
|
import WebSocket from 'ws';
|
||||||
|
|
||||||
|
const ws = new WebSocket('ws://www.host.com/path');
|
||||||
|
|
||||||
|
ws.on('error', console.error);
|
||||||
|
|
||||||
|
ws.on('open', function open() {
|
||||||
|
ws.send('something');
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.on('message', function message(data) {
|
||||||
|
console.log('received: %s', data);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sending binary data
|
||||||
|
|
||||||
|
```js
|
||||||
|
import WebSocket from 'ws';
|
||||||
|
|
||||||
|
const ws = new WebSocket('ws://www.host.com/path');
|
||||||
|
|
||||||
|
ws.on('error', console.error);
|
||||||
|
|
||||||
|
ws.on('open', function open() {
|
||||||
|
const array = new Float32Array(5);
|
||||||
|
|
||||||
|
for (var i = 0; i < array.length; ++i) {
|
||||||
|
array[i] = i / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.send(array);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Simple server
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { WebSocketServer } from 'ws';
|
||||||
|
|
||||||
|
const wss = new WebSocketServer({ port: 8080 });
|
||||||
|
|
||||||
|
wss.on('connection', function connection(ws) {
|
||||||
|
ws.on('error', console.error);
|
||||||
|
|
||||||
|
ws.on('message', function message(data) {
|
||||||
|
console.log('received: %s', data);
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.send('something');
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### External HTTP/S server
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { createServer } from 'https';
|
||||||
|
import { readFileSync } from 'fs';
|
||||||
|
import { WebSocketServer } from 'ws';
|
||||||
|
|
||||||
|
const server = createServer({
|
||||||
|
cert: readFileSync('/path/to/cert.pem'),
|
||||||
|
key: readFileSync('/path/to/key.pem')
|
||||||
|
});
|
||||||
|
const wss = new WebSocketServer({ server });
|
||||||
|
|
||||||
|
wss.on('connection', function connection(ws) {
|
||||||
|
ws.on('error', console.error);
|
||||||
|
|
||||||
|
ws.on('message', function message(data) {
|
||||||
|
console.log('received: %s', data);
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.send('something');
|
||||||
|
});
|
||||||
|
|
||||||
|
server.listen(8080);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Multiple servers sharing a single HTTP/S server
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { createServer } from 'http';
|
||||||
|
import { WebSocketServer } from 'ws';
|
||||||
|
|
||||||
|
const server = createServer();
|
||||||
|
const wss1 = new WebSocketServer({ noServer: true });
|
||||||
|
const wss2 = new WebSocketServer({ noServer: true });
|
||||||
|
|
||||||
|
wss1.on('connection', function connection(ws) {
|
||||||
|
ws.on('error', console.error);
|
||||||
|
|
||||||
|
// ...
|
||||||
|
});
|
||||||
|
|
||||||
|
wss2.on('connection', function connection(ws) {
|
||||||
|
ws.on('error', console.error);
|
||||||
|
|
||||||
|
// ...
|
||||||
|
});
|
||||||
|
|
||||||
|
server.on('upgrade', function upgrade(request, socket, head) {
|
||||||
|
const { pathname } = new URL(request.url, 'wss://base.url');
|
||||||
|
|
||||||
|
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);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Client authentication
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { createServer } from 'http';
|
||||||
|
import { WebSocketServer } from 'ws';
|
||||||
|
|
||||||
|
function onSocketError(err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
const server = createServer();
|
||||||
|
const wss = new WebSocketServer({ noServer: true });
|
||||||
|
|
||||||
|
wss.on('connection', function connection(ws, request, client) {
|
||||||
|
ws.on('error', console.error);
|
||||||
|
|
||||||
|
ws.on('message', function message(data) {
|
||||||
|
console.log(`Received message ${data} from user ${client}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
server.on('upgrade', function upgrade(request, socket, head) {
|
||||||
|
socket.on('error', onSocketError);
|
||||||
|
|
||||||
|
// This function is not defined on purpose. Implement it with your own logic.
|
||||||
|
authenticate(request, function next(err, client) {
|
||||||
|
if (err || !client) {
|
||||||
|
socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
|
||||||
|
socket.destroy();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
socket.removeListener('error', onSocketError);
|
||||||
|
|
||||||
|
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
|
||||||
|
import WebSocket, { WebSocketServer } from 'ws';
|
||||||
|
|
||||||
|
const wss = new WebSocketServer({ port: 8080 });
|
||||||
|
|
||||||
|
wss.on('connection', function connection(ws) {
|
||||||
|
ws.on('error', console.error);
|
||||||
|
|
||||||
|
ws.on('message', function message(data, isBinary) {
|
||||||
|
wss.clients.forEach(function each(client) {
|
||||||
|
if (client.readyState === WebSocket.OPEN) {
|
||||||
|
client.send(data, { binary: isBinary });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
A client WebSocket broadcasting to every other connected WebSocket clients,
|
||||||
|
excluding itself.
|
||||||
|
|
||||||
|
```js
|
||||||
|
import WebSocket, { WebSocketServer } from 'ws';
|
||||||
|
|
||||||
|
const wss = new WebSocketServer({ port: 8080 });
|
||||||
|
|
||||||
|
wss.on('connection', function connection(ws) {
|
||||||
|
ws.on('error', console.error);
|
||||||
|
|
||||||
|
ws.on('message', function message(data, isBinary) {
|
||||||
|
wss.clients.forEach(function each(client) {
|
||||||
|
if (client !== ws && client.readyState === WebSocket.OPEN) {
|
||||||
|
client.send(data, { binary: isBinary });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Round-trip time
|
||||||
|
|
||||||
|
```js
|
||||||
|
import WebSocket from 'ws';
|
||||||
|
|
||||||
|
const ws = new WebSocket('wss://websocket-echo.com/');
|
||||||
|
|
||||||
|
ws.on('error', console.error);
|
||||||
|
|
||||||
|
ws.on('open', function open() {
|
||||||
|
console.log('connected');
|
||||||
|
ws.send(Date.now());
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.on('close', function close() {
|
||||||
|
console.log('disconnected');
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.on('message', function message(data) {
|
||||||
|
console.log(`Round-trip time: ${Date.now() - data} ms`);
|
||||||
|
|
||||||
|
setTimeout(function timeout() {
|
||||||
|
ws.send(Date.now());
|
||||||
|
}, 500);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Use the Node.js streams API
|
||||||
|
|
||||||
|
```js
|
||||||
|
import WebSocket, { createWebSocketStream } from 'ws';
|
||||||
|
|
||||||
|
const ws = new WebSocket('wss://websocket-echo.com/');
|
||||||
|
|
||||||
|
const duplex = createWebSocketStream(ws, { encoding: 'utf8' });
|
||||||
|
|
||||||
|
duplex.on('error', console.error);
|
||||||
|
|
||||||
|
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
|
||||||
|
examples folder.
|
||||||
|
|
||||||
|
Otherwise, see the test cases.
|
||||||
|
|
||||||
|
## FAQ
|
||||||
|
|
||||||
|
### How to get the IP address of the client?
|
||||||
|
|
||||||
|
The remote IP address can be obtained from the raw socket.
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { WebSocketServer } from 'ws';
|
||||||
|
|
||||||
|
const wss = new WebSocketServer({ port: 8080 });
|
||||||
|
|
||||||
|
wss.on('connection', function connection(ws, req) {
|
||||||
|
const ip = req.socket.remoteAddress;
|
||||||
|
|
||||||
|
ws.on('error', console.error);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
When the server runs behind a proxy like NGINX, the de-facto standard is to use
|
||||||
|
the `X-Forwarded-For` header.
|
||||||
|
|
||||||
|
```js
|
||||||
|
wss.on('connection', function connection(ws, req) {
|
||||||
|
const ip = req.headers['x-forwarded-for'].split(',')[0].trim();
|
||||||
|
|
||||||
|
ws.on('error', console.error);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### How to detect and close broken connections?
|
||||||
|
|
||||||
|
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).
|
||||||
|
|
||||||
|
In these cases, ping messages can be used as a means to verify that the remote
|
||||||
|
endpoint is still responsive.
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { WebSocketServer } from 'ws';
|
||||||
|
|
||||||
|
function heartbeat() {
|
||||||
|
this.isAlive = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const wss = new WebSocketServer({ port: 8080 });
|
||||||
|
|
||||||
|
wss.on('connection', function connection(ws) {
|
||||||
|
ws.isAlive = true;
|
||||||
|
ws.on('error', console.error);
|
||||||
|
ws.on('pong', heartbeat);
|
||||||
|
});
|
||||||
|
|
||||||
|
const interval = setInterval(function ping() {
|
||||||
|
wss.clients.forEach(function each(ws) {
|
||||||
|
if (ws.isAlive === false) return ws.terminate();
|
||||||
|
|
||||||
|
ws.isAlive = false;
|
||||||
|
ws.ping();
|
||||||
|
});
|
||||||
|
}, 30000);
|
||||||
|
|
||||||
|
wss.on('close', function close() {
|
||||||
|
clearInterval(interval);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Pong messages are automatically sent in response to ping messages as required by
|
||||||
|
the spec.
|
||||||
|
|
||||||
|
Just like the server example above, your clients might as well lose connection
|
||||||
|
without knowing it. You might want to add a ping listener on your clients to
|
||||||
|
prevent that. A simple implementation would be:
|
||||||
|
|
||||||
|
```js
|
||||||
|
import WebSocket from 'ws';
|
||||||
|
|
||||||
|
function heartbeat() {
|
||||||
|
clearTimeout(this.pingTimeout);
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = new WebSocket('wss://websocket-echo.com/');
|
||||||
|
|
||||||
|
client.on('error', console.error);
|
||||||
|
client.on('open', heartbeat);
|
||||||
|
client.on('ping', heartbeat);
|
||||||
|
client.on('close', function clear() {
|
||||||
|
clearTimeout(this.pingTimeout);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### How to connect via a proxy?
|
||||||
|
|
||||||
|
Use a custom `http.Agent` implementation like [https-proxy-agent][] or
|
||||||
|
[socks-proxy-agent][].
|
||||||
|
|
||||||
|
## Changelog
|
||||||
|
|
||||||
|
We're using the GitHub [releases][changelog] for changelog entries.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
[MIT](LICENSE)
|
||||||
|
|
||||||
|
[`buffer.isutf8()`]: https://nodejs.org/api/buffer.html#bufferisutf8input
|
||||||
|
[bufferutil]: https://github.com/websockets/bufferutil
|
||||||
|
[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
|
||||||
|
[utf-8-validate]: https://github.com/websockets/utf-8-validate
|
||||||
|
[ws-server-options]: ./doc/ws.md#new-websocketserveroptions-callback
|
||||||
8
node_modules/ws/browser.js
generated
vendored
Normal file
8
node_modules/ws/browser.js
generated
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
module.exports = function () {
|
||||||
|
throw new Error(
|
||||||
|
'ws does not work in the browser. Browser clients must use the native ' +
|
||||||
|
'WebSocket object'
|
||||||
|
);
|
||||||
|
};
|
||||||
22
node_modules/ws/index.js
generated
vendored
Normal file
22
node_modules/ws/index.js
generated
vendored
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const createWebSocketStream = require('./lib/stream');
|
||||||
|
const extension = require('./lib/extension');
|
||||||
|
const PerMessageDeflate = require('./lib/permessage-deflate');
|
||||||
|
const Receiver = require('./lib/receiver');
|
||||||
|
const Sender = require('./lib/sender');
|
||||||
|
const subprotocol = require('./lib/subprotocol');
|
||||||
|
const WebSocket = require('./lib/websocket');
|
||||||
|
const WebSocketServer = require('./lib/websocket-server');
|
||||||
|
|
||||||
|
WebSocket.createWebSocketStream = createWebSocketStream;
|
||||||
|
WebSocket.extension = extension;
|
||||||
|
WebSocket.PerMessageDeflate = PerMessageDeflate;
|
||||||
|
WebSocket.Receiver = Receiver;
|
||||||
|
WebSocket.Sender = Sender;
|
||||||
|
WebSocket.Server = WebSocketServer;
|
||||||
|
WebSocket.subprotocol = subprotocol;
|
||||||
|
WebSocket.WebSocket = WebSocket;
|
||||||
|
WebSocket.WebSocketServer = WebSocketServer;
|
||||||
|
|
||||||
|
module.exports = WebSocket;
|
||||||
131
node_modules/ws/lib/buffer-util.js
generated
vendored
Normal file
131
node_modules/ws/lib/buffer-util.js
generated
vendored
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const { EMPTY_BUFFER } = require('./constants');
|
||||||
|
|
||||||
|
const FastBuffer = Buffer[Symbol.species];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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) {
|
||||||
|
if (list.length === 0) return EMPTY_BUFFER;
|
||||||
|
if (list.length === 1) return list[0];
|
||||||
|
|
||||||
|
const target = Buffer.allocUnsafe(totalLength);
|
||||||
|
let offset = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < list.length; i++) {
|
||||||
|
const buf = list[i];
|
||||||
|
target.set(buf, offset);
|
||||||
|
offset += buf.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (offset < totalLength) {
|
||||||
|
return new FastBuffer(target.buffer, target.byteOffset, offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (let 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) {
|
||||||
|
for (let i = 0; i < buffer.length; i++) {
|
||||||
|
buffer[i] ^= mask[i & 3];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a buffer to an `ArrayBuffer`.
|
||||||
|
*
|
||||||
|
* @param {Buffer} buf The buffer to convert
|
||||||
|
* @return {ArrayBuffer} Converted buffer
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
function toArrayBuffer(buf) {
|
||||||
|
if (buf.length === buf.buffer.byteLength) {
|
||||||
|
return buf.buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts `data` to a `Buffer`.
|
||||||
|
*
|
||||||
|
* @param {*} data The data to convert
|
||||||
|
* @return {Buffer} The buffer
|
||||||
|
* @throws {TypeError}
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
function toBuffer(data) {
|
||||||
|
toBuffer.readOnly = true;
|
||||||
|
|
||||||
|
if (Buffer.isBuffer(data)) return data;
|
||||||
|
|
||||||
|
let buf;
|
||||||
|
|
||||||
|
if (data instanceof ArrayBuffer) {
|
||||||
|
buf = new FastBuffer(data);
|
||||||
|
} else if (ArrayBuffer.isView(data)) {
|
||||||
|
buf = new FastBuffer(data.buffer, data.byteOffset, data.byteLength);
|
||||||
|
} else {
|
||||||
|
buf = Buffer.from(data);
|
||||||
|
toBuffer.readOnly = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
concat,
|
||||||
|
mask: _mask,
|
||||||
|
toArrayBuffer,
|
||||||
|
toBuffer,
|
||||||
|
unmask: _unmask
|
||||||
|
};
|
||||||
|
|
||||||
|
/* istanbul ignore else */
|
||||||
|
if (!process.env.WS_NO_BUFFER_UTIL) {
|
||||||
|
try {
|
||||||
|
const bufferUtil = require('bufferutil');
|
||||||
|
|
||||||
|
module.exports.mask = function (source, mask, output, offset, length) {
|
||||||
|
if (length < 48) _mask(source, mask, output, offset, length);
|
||||||
|
else bufferUtil.mask(source, mask, output, offset, length);
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports.unmask = function (buffer, mask) {
|
||||||
|
if (buffer.length < 32) _unmask(buffer, mask);
|
||||||
|
else bufferUtil.unmask(buffer, mask);
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
// Continue regardless of the error.
|
||||||
|
}
|
||||||
|
}
|
||||||
19
node_modules/ws/lib/constants.js
generated
vendored
Normal file
19
node_modules/ws/lib/constants.js
generated
vendored
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const BINARY_TYPES = ['nodebuffer', 'arraybuffer', 'fragments'];
|
||||||
|
const hasBlob = typeof Blob !== 'undefined';
|
||||||
|
|
||||||
|
if (hasBlob) BINARY_TYPES.push('blob');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
BINARY_TYPES,
|
||||||
|
CLOSE_TIMEOUT: 30000,
|
||||||
|
EMPTY_BUFFER: Buffer.alloc(0),
|
||||||
|
GUID: '258EAFA5-E914-47DA-95CA-C5AB0DC85B11',
|
||||||
|
hasBlob,
|
||||||
|
kForOnEventAttribute: Symbol('kIsForOnEventAttribute'),
|
||||||
|
kListener: Symbol('kListener'),
|
||||||
|
kStatusCode: Symbol('status-code'),
|
||||||
|
kWebSocket: Symbol('websocket'),
|
||||||
|
NOOP: () => {}
|
||||||
|
};
|
||||||
292
node_modules/ws/lib/event-target.js
generated
vendored
Normal file
292
node_modules/ws/lib/event-target.js
generated
vendored
Normal file
@@ -0,0 +1,292 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const { kForOnEventAttribute, kListener } = require('./constants');
|
||||||
|
|
||||||
|
const kCode = Symbol('kCode');
|
||||||
|
const kData = Symbol('kData');
|
||||||
|
const kError = Symbol('kError');
|
||||||
|
const kMessage = Symbol('kMessage');
|
||||||
|
const kReason = Symbol('kReason');
|
||||||
|
const kTarget = Symbol('kTarget');
|
||||||
|
const kType = Symbol('kType');
|
||||||
|
const kWasClean = Symbol('kWasClean');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class representing an event.
|
||||||
|
*/
|
||||||
|
class Event {
|
||||||
|
/**
|
||||||
|
* Create a new `Event`.
|
||||||
|
*
|
||||||
|
* @param {String} type The name of the event
|
||||||
|
* @throws {TypeError} If the `type` argument is not specified
|
||||||
|
*/
|
||||||
|
constructor(type) {
|
||||||
|
this[kTarget] = null;
|
||||||
|
this[kType] = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {*}
|
||||||
|
*/
|
||||||
|
get target() {
|
||||||
|
return this[kTarget];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {String}
|
||||||
|
*/
|
||||||
|
get type() {
|
||||||
|
return this[kType];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.defineProperty(Event.prototype, 'target', { enumerable: true });
|
||||||
|
Object.defineProperty(Event.prototype, 'type', { enumerable: true });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class representing a close event.
|
||||||
|
*
|
||||||
|
* @extends Event
|
||||||
|
*/
|
||||||
|
class CloseEvent extends Event {
|
||||||
|
/**
|
||||||
|
* Create a new `CloseEvent`.
|
||||||
|
*
|
||||||
|
* @param {String} type The name of the event
|
||||||
|
* @param {Object} [options] A dictionary object that allows for setting
|
||||||
|
* attributes via object members of the same name
|
||||||
|
* @param {Number} [options.code=0] The status code explaining why the
|
||||||
|
* connection was closed
|
||||||
|
* @param {String} [options.reason=''] A human-readable string explaining why
|
||||||
|
* the connection was closed
|
||||||
|
* @param {Boolean} [options.wasClean=false] Indicates whether or not the
|
||||||
|
* connection was cleanly closed
|
||||||
|
*/
|
||||||
|
constructor(type, options = {}) {
|
||||||
|
super(type);
|
||||||
|
|
||||||
|
this[kCode] = options.code === undefined ? 0 : options.code;
|
||||||
|
this[kReason] = options.reason === undefined ? '' : options.reason;
|
||||||
|
this[kWasClean] = options.wasClean === undefined ? false : options.wasClean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {Number}
|
||||||
|
*/
|
||||||
|
get code() {
|
||||||
|
return this[kCode];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {String}
|
||||||
|
*/
|
||||||
|
get reason() {
|
||||||
|
return this[kReason];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {Boolean}
|
||||||
|
*/
|
||||||
|
get wasClean() {
|
||||||
|
return this[kWasClean];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.defineProperty(CloseEvent.prototype, 'code', { enumerable: true });
|
||||||
|
Object.defineProperty(CloseEvent.prototype, 'reason', { enumerable: true });
|
||||||
|
Object.defineProperty(CloseEvent.prototype, 'wasClean', { enumerable: true });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class representing an error event.
|
||||||
|
*
|
||||||
|
* @extends Event
|
||||||
|
*/
|
||||||
|
class ErrorEvent extends Event {
|
||||||
|
/**
|
||||||
|
* Create a new `ErrorEvent`.
|
||||||
|
*
|
||||||
|
* @param {String} type The name of the event
|
||||||
|
* @param {Object} [options] A dictionary object that allows for setting
|
||||||
|
* attributes via object members of the same name
|
||||||
|
* @param {*} [options.error=null] The error that generated this event
|
||||||
|
* @param {String} [options.message=''] The error message
|
||||||
|
*/
|
||||||
|
constructor(type, options = {}) {
|
||||||
|
super(type);
|
||||||
|
|
||||||
|
this[kError] = options.error === undefined ? null : options.error;
|
||||||
|
this[kMessage] = options.message === undefined ? '' : options.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {*}
|
||||||
|
*/
|
||||||
|
get error() {
|
||||||
|
return this[kError];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {String}
|
||||||
|
*/
|
||||||
|
get message() {
|
||||||
|
return this[kMessage];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.defineProperty(ErrorEvent.prototype, 'error', { enumerable: true });
|
||||||
|
Object.defineProperty(ErrorEvent.prototype, 'message', { enumerable: true });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class representing a message event.
|
||||||
|
*
|
||||||
|
* @extends Event
|
||||||
|
*/
|
||||||
|
class MessageEvent extends Event {
|
||||||
|
/**
|
||||||
|
* Create a new `MessageEvent`.
|
||||||
|
*
|
||||||
|
* @param {String} type The name of the event
|
||||||
|
* @param {Object} [options] A dictionary object that allows for setting
|
||||||
|
* attributes via object members of the same name
|
||||||
|
* @param {*} [options.data=null] The message content
|
||||||
|
*/
|
||||||
|
constructor(type, options = {}) {
|
||||||
|
super(type);
|
||||||
|
|
||||||
|
this[kData] = options.data === undefined ? null : options.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {*}
|
||||||
|
*/
|
||||||
|
get data() {
|
||||||
|
return this[kData];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.defineProperty(MessageEvent.prototype, 'data', { enumerable: true });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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} type A string representing the event type to listen for
|
||||||
|
* @param {(Function|Object)} handler The listener to add
|
||||||
|
* @param {Object} [options] An options object specifies characteristics about
|
||||||
|
* the event listener
|
||||||
|
* @param {Boolean} [options.once=false] 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.
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
addEventListener(type, handler, options = {}) {
|
||||||
|
for (const listener of this.listeners(type)) {
|
||||||
|
if (
|
||||||
|
!options[kForOnEventAttribute] &&
|
||||||
|
listener[kListener] === handler &&
|
||||||
|
!listener[kForOnEventAttribute]
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let wrapper;
|
||||||
|
|
||||||
|
if (type === 'message') {
|
||||||
|
wrapper = function onMessage(data, isBinary) {
|
||||||
|
const event = new MessageEvent('message', {
|
||||||
|
data: isBinary ? data : data.toString()
|
||||||
|
});
|
||||||
|
|
||||||
|
event[kTarget] = this;
|
||||||
|
callListener(handler, this, event);
|
||||||
|
};
|
||||||
|
} else if (type === 'close') {
|
||||||
|
wrapper = function onClose(code, message) {
|
||||||
|
const event = new CloseEvent('close', {
|
||||||
|
code,
|
||||||
|
reason: message.toString(),
|
||||||
|
wasClean: this._closeFrameReceived && this._closeFrameSent
|
||||||
|
});
|
||||||
|
|
||||||
|
event[kTarget] = this;
|
||||||
|
callListener(handler, this, event);
|
||||||
|
};
|
||||||
|
} else if (type === 'error') {
|
||||||
|
wrapper = function onError(error) {
|
||||||
|
const event = new ErrorEvent('error', {
|
||||||
|
error,
|
||||||
|
message: error.message
|
||||||
|
});
|
||||||
|
|
||||||
|
event[kTarget] = this;
|
||||||
|
callListener(handler, this, event);
|
||||||
|
};
|
||||||
|
} else if (type === 'open') {
|
||||||
|
wrapper = function onOpen() {
|
||||||
|
const event = new Event('open');
|
||||||
|
|
||||||
|
event[kTarget] = this;
|
||||||
|
callListener(handler, this, event);
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
wrapper[kForOnEventAttribute] = !!options[kForOnEventAttribute];
|
||||||
|
wrapper[kListener] = handler;
|
||||||
|
|
||||||
|
if (options.once) {
|
||||||
|
this.once(type, wrapper);
|
||||||
|
} else {
|
||||||
|
this.on(type, wrapper);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove an event listener.
|
||||||
|
*
|
||||||
|
* @param {String} type A string representing the event type to remove
|
||||||
|
* @param {(Function|Object)} handler The listener to remove
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
removeEventListener(type, handler) {
|
||||||
|
for (const listener of this.listeners(type)) {
|
||||||
|
if (listener[kListener] === handler && !listener[kForOnEventAttribute]) {
|
||||||
|
this.removeListener(type, listener);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
CloseEvent,
|
||||||
|
ErrorEvent,
|
||||||
|
Event,
|
||||||
|
EventTarget,
|
||||||
|
MessageEvent
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call an event listener
|
||||||
|
*
|
||||||
|
* @param {(Function|Object)} listener The listener to call
|
||||||
|
* @param {*} thisArg The value to use as `this`` when calling the listener
|
||||||
|
* @param {Event} event The event to pass to the listener
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
function callListener(listener, thisArg, event) {
|
||||||
|
if (typeof listener === 'object' && listener.handleEvent) {
|
||||||
|
listener.handleEvent.call(listener, event);
|
||||||
|
} else {
|
||||||
|
listener.call(thisArg, event);
|
||||||
|
}
|
||||||
|
}
|
||||||
203
node_modules/ws/lib/extension.js
generated
vendored
Normal file
203
node_modules/ws/lib/extension.js
generated
vendored
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const { tokenChars } = require('./validation');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 (dest[name] === undefined) dest[name] = [elem];
|
||||||
|
else dest[name].push(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 = Object.create(null);
|
||||||
|
let params = Object.create(null);
|
||||||
|
let mustUnescape = false;
|
||||||
|
let isEscaping = false;
|
||||||
|
let inQuotes = false;
|
||||||
|
let extensionName;
|
||||||
|
let paramName;
|
||||||
|
let start = -1;
|
||||||
|
let code = -1;
|
||||||
|
let end = -1;
|
||||||
|
let i = 0;
|
||||||
|
|
||||||
|
for (; i < header.length; i++) {
|
||||||
|
code = header.charCodeAt(i);
|
||||||
|
|
||||||
|
if (extensionName === undefined) {
|
||||||
|
if (end === -1 && tokenChars[code] === 1) {
|
||||||
|
if (start === -1) start = i;
|
||||||
|
} else if (
|
||||||
|
i !== 0 &&
|
||||||
|
(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 = Object.create(null);
|
||||||
|
} 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 = Object.create(null);
|
||||||
|
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;
|
||||||
|
let 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 = Object.create(null);
|
||||||
|
extensionName = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
paramName = undefined;
|
||||||
|
start = end = -1;
|
||||||
|
} else {
|
||||||
|
throw new SyntaxError(`Unexpected character at index ${i}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (start === -1 || inQuotes || code === 0x20 || code === 0x09) {
|
||||||
|
throw new SyntaxError('Unexpected end of input');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (end === -1) end = i;
|
||||||
|
const token = header.slice(start, end);
|
||||||
|
if (extensionName === undefined) {
|
||||||
|
push(offers, token, params);
|
||||||
|
} 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) => {
|
||||||
|
let configurations = extensions[extension];
|
||||||
|
if (!Array.isArray(configurations)) configurations = [configurations];
|
||||||
|
return configurations
|
||||||
|
.map((params) => {
|
||||||
|
return [extension]
|
||||||
|
.concat(
|
||||||
|
Object.keys(params).map((k) => {
|
||||||
|
let 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 };
|
||||||
55
node_modules/ws/lib/limiter.js
generated
vendored
Normal file
55
node_modules/ws/lib/limiter.js
generated
vendored
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const kDone = Symbol('kDone');
|
||||||
|
const kRun = Symbol('kRun');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A very simple job queue with adjustable concurrency. Adapted from
|
||||||
|
* https://github.com/STRML/async-limiter
|
||||||
|
*/
|
||||||
|
class Limiter {
|
||||||
|
/**
|
||||||
|
* Creates a new `Limiter`.
|
||||||
|
*
|
||||||
|
* @param {Number} [concurrency=Infinity] The maximum number of jobs allowed
|
||||||
|
* to run concurrently
|
||||||
|
*/
|
||||||
|
constructor(concurrency) {
|
||||||
|
this[kDone] = () => {
|
||||||
|
this.pending--;
|
||||||
|
this[kRun]();
|
||||||
|
};
|
||||||
|
this.concurrency = concurrency || Infinity;
|
||||||
|
this.jobs = [];
|
||||||
|
this.pending = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a job to the queue.
|
||||||
|
*
|
||||||
|
* @param {Function} job The job to run
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
add(job) {
|
||||||
|
this.jobs.push(job);
|
||||||
|
this[kRun]();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes a job from the queue and runs it if possible.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
[kRun]() {
|
||||||
|
if (this.pending === this.concurrency) return;
|
||||||
|
|
||||||
|
if (this.jobs.length) {
|
||||||
|
const job = this.jobs.shift();
|
||||||
|
|
||||||
|
this.pending++;
|
||||||
|
job(this[kDone]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Limiter;
|
||||||
528
node_modules/ws/lib/permessage-deflate.js
generated
vendored
Normal file
528
node_modules/ws/lib/permessage-deflate.js
generated
vendored
Normal file
@@ -0,0 +1,528 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const zlib = require('zlib');
|
||||||
|
|
||||||
|
const bufferUtil = require('./buffer-util');
|
||||||
|
const Limiter = require('./limiter');
|
||||||
|
const { kStatusCode } = require('./constants');
|
||||||
|
|
||||||
|
const FastBuffer = Buffer[Symbol.species];
|
||||||
|
const TRAILER = Buffer.from([0x00, 0x00, 0xff, 0xff]);
|
||||||
|
const kPerMessageDeflate = Symbol('permessage-deflate');
|
||||||
|
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|Number)} [options.clientMaxWindowBits] Advertise support
|
||||||
|
* for, or request, a custom client window size
|
||||||
|
* @param {Boolean} [options.clientNoContextTakeover=false] Advertise/
|
||||||
|
* acknowledge disabling of client context takeover
|
||||||
|
* @param {Number} [options.concurrencyLimit=10] The number of concurrent
|
||||||
|
* calls to zlib
|
||||||
|
* @param {Boolean} [options.isServer=false] Create the instance in either
|
||||||
|
* server or client mode
|
||||||
|
* @param {Number} [options.maxPayload=0] The maximum allowed message length
|
||||||
|
* @param {(Boolean|Number)} [options.serverMaxWindowBits] Request/confirm the
|
||||||
|
* use of a custom server window size
|
||||||
|
* @param {Boolean} [options.serverNoContextTakeover=false] Request/accept
|
||||||
|
* disabling of server context takeover
|
||||||
|
* @param {Number} [options.threshold=1024] Size (in bytes) below which
|
||||||
|
* messages should not be compressed if context takeover is disabled
|
||||||
|
* @param {Object} [options.zlibDeflateOptions] Options to pass to zlib on
|
||||||
|
* deflate
|
||||||
|
* @param {Object} [options.zlibInflateOptions] Options to pass to zlib on
|
||||||
|
* inflate
|
||||||
|
*/
|
||||||
|
constructor(options) {
|
||||||
|
this._options = options || {};
|
||||||
|
this._threshold =
|
||||||
|
this._options.threshold !== undefined ? this._options.threshold : 1024;
|
||||||
|
this._maxPayload = this._options.maxPayload | 0;
|
||||||
|
this._isServer = !!this._options.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) {
|
||||||
|
this._inflate.close();
|
||||||
|
this._inflate = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._deflate) {
|
||||||
|
const callback = this._deflate[kCallback];
|
||||||
|
|
||||||
|
this._deflate.close();
|
||||||
|
this._deflate = null;
|
||||||
|
|
||||||
|
if (callback) {
|
||||||
|
callback(
|
||||||
|
new Error(
|
||||||
|
'The deflate stream was closed while data was being processed'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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) => {
|
||||||
|
let 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.
|
||||||
|
*
|
||||||
|
* @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.add((done) => {
|
||||||
|
this._decompress(data, fin, (err, result) => {
|
||||||
|
done();
|
||||||
|
callback(err, result);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compress data. Concurrency limited.
|
||||||
|
*
|
||||||
|
* @param {(Buffer|String)} 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.add((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({
|
||||||
|
...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.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 (this._inflate._readableState.endEmitted) {
|
||||||
|
this._inflate.close();
|
||||||
|
this._inflate = null;
|
||||||
|
} else {
|
||||||
|
this._inflate[kTotalLength] = 0;
|
||||||
|
this._inflate[kBuffers] = [];
|
||||||
|
|
||||||
|
if (fin && this.params[`${endpoint}_no_context_takeover`]) {
|
||||||
|
this._inflate.reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
callback(null, data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compress data.
|
||||||
|
*
|
||||||
|
* @param {(Buffer|String)} 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) {
|
||||||
|
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({
|
||||||
|
...this._options.zlibDeflateOptions,
|
||||||
|
windowBits
|
||||||
|
});
|
||||||
|
|
||||||
|
this._deflate[kTotalLength] = 0;
|
||||||
|
this._deflate[kBuffers] = [];
|
||||||
|
|
||||||
|
this._deflate.on('data', deflateOnData);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._deflate[kCallback] = callback;
|
||||||
|
|
||||||
|
this._deflate.write(data);
|
||||||
|
this._deflate.flush(zlib.Z_SYNC_FLUSH, () => {
|
||||||
|
if (!this._deflate) {
|
||||||
|
//
|
||||||
|
// The deflate stream was closed while data was being processed.
|
||||||
|
//
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let data = bufferUtil.concat(
|
||||||
|
this._deflate[kBuffers],
|
||||||
|
this._deflate[kTotalLength]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (fin) {
|
||||||
|
data = new FastBuffer(data.buffer, data.byteOffset, data.length - 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Ensure that the callback will not be called again in
|
||||||
|
// `PerMessageDeflate#cleanup()`.
|
||||||
|
//
|
||||||
|
this._deflate[kCallback] = null;
|
||||||
|
|
||||||
|
this._deflate[kTotalLength] = 0;
|
||||||
|
this._deflate[kBuffers] = [];
|
||||||
|
|
||||||
|
if (fin && this.params[`${endpoint}_no_context_takeover`]) {
|
||||||
|
this._deflate.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
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].code = 'WS_ERR_UNSUPPORTED_MESSAGE_LENGTH';
|
||||||
|
this[kError][kStatusCode] = 1009;
|
||||||
|
this.removeListener('data', inflateOnData);
|
||||||
|
|
||||||
|
//
|
||||||
|
// The choice to employ `zlib.reset()` over `zlib.close()` is dictated by the
|
||||||
|
// fact that in Node.js versions prior to 13.10.0, the callback for
|
||||||
|
// `zlib.flush()` is not called if `zlib.close()` is used. Utilizing
|
||||||
|
// `zlib.reset()` ensures that either the callback is invoked or an error is
|
||||||
|
// emitted.
|
||||||
|
//
|
||||||
|
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;
|
||||||
|
|
||||||
|
if (this[kError]) {
|
||||||
|
this[kCallback](this[kError]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
err[kStatusCode] = 1007;
|
||||||
|
this[kCallback](err);
|
||||||
|
}
|
||||||
706
node_modules/ws/lib/receiver.js
generated
vendored
Normal file
706
node_modules/ws/lib/receiver.js
generated
vendored
Normal file
@@ -0,0 +1,706 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const { Writable } = require('stream');
|
||||||
|
|
||||||
|
const PerMessageDeflate = require('./permessage-deflate');
|
||||||
|
const {
|
||||||
|
BINARY_TYPES,
|
||||||
|
EMPTY_BUFFER,
|
||||||
|
kStatusCode,
|
||||||
|
kWebSocket
|
||||||
|
} = require('./constants');
|
||||||
|
const { concat, toArrayBuffer, unmask } = require('./buffer-util');
|
||||||
|
const { isValidStatusCode, isValidUTF8 } = require('./validation');
|
||||||
|
|
||||||
|
const FastBuffer = Buffer[Symbol.species];
|
||||||
|
|
||||||
|
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;
|
||||||
|
const DEFER_EVENT = 6;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HyBi Receiver implementation.
|
||||||
|
*
|
||||||
|
* @extends Writable
|
||||||
|
*/
|
||||||
|
class Receiver extends Writable {
|
||||||
|
/**
|
||||||
|
* Creates a Receiver instance.
|
||||||
|
*
|
||||||
|
* @param {Object} [options] Options object
|
||||||
|
* @param {Boolean} [options.allowSynchronousEvents=true] Specifies whether
|
||||||
|
* any of the `'message'`, `'ping'`, and `'pong'` events can be emitted
|
||||||
|
* multiple times in the same tick
|
||||||
|
* @param {String} [options.binaryType=nodebuffer] The type for binary data
|
||||||
|
* @param {Object} [options.extensions] An object containing the negotiated
|
||||||
|
* extensions
|
||||||
|
* @param {Boolean} [options.isServer=false] Specifies whether to operate in
|
||||||
|
* client or server mode
|
||||||
|
* @param {Number} [options.maxPayload=0] The maximum allowed message length
|
||||||
|
* @param {Boolean} [options.skipUTF8Validation=false] Specifies whether or
|
||||||
|
* not to skip UTF-8 validation for text and close messages
|
||||||
|
*/
|
||||||
|
constructor(options = {}) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this._allowSynchronousEvents =
|
||||||
|
options.allowSynchronousEvents !== undefined
|
||||||
|
? options.allowSynchronousEvents
|
||||||
|
: true;
|
||||||
|
this._binaryType = options.binaryType || BINARY_TYPES[0];
|
||||||
|
this._extensions = options.extensions || {};
|
||||||
|
this._isServer = !!options.isServer;
|
||||||
|
this._maxPayload = options.maxPayload | 0;
|
||||||
|
this._skipUTF8Validation = !!options.skipUTF8Validation;
|
||||||
|
this[kWebSocket] = undefined;
|
||||||
|
|
||||||
|
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._errored = false;
|
||||||
|
this._loop = false;
|
||||||
|
this._state = GET_INFO;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_write(chunk, encoding, cb) {
|
||||||
|
if (this._opcode === 0x08 && this._state == GET_INFO) 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] = new FastBuffer(
|
||||||
|
buf.buffer,
|
||||||
|
buf.byteOffset + n,
|
||||||
|
buf.length - n
|
||||||
|
);
|
||||||
|
|
||||||
|
return new FastBuffer(buf.buffer, buf.byteOffset, n);
|
||||||
|
}
|
||||||
|
|
||||||
|
const dst = Buffer.allocUnsafe(n);
|
||||||
|
|
||||||
|
do {
|
||||||
|
const buf = this._buffers[0];
|
||||||
|
const offset = dst.length - n;
|
||||||
|
|
||||||
|
if (n >= buf.length) {
|
||||||
|
dst.set(this._buffers.shift(), offset);
|
||||||
|
} else {
|
||||||
|
dst.set(new Uint8Array(buf.buffer, buf.byteOffset, n), offset);
|
||||||
|
this._buffers[0] = new FastBuffer(
|
||||||
|
buf.buffer,
|
||||||
|
buf.byteOffset + n,
|
||||||
|
buf.length - n
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
n -= buf.length;
|
||||||
|
} while (n > 0);
|
||||||
|
|
||||||
|
return dst;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts the parsing loop.
|
||||||
|
*
|
||||||
|
* @param {Function} cb Callback
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
startLoop(cb) {
|
||||||
|
this._loop = true;
|
||||||
|
|
||||||
|
do {
|
||||||
|
switch (this._state) {
|
||||||
|
case GET_INFO:
|
||||||
|
this.getInfo(cb);
|
||||||
|
break;
|
||||||
|
case GET_PAYLOAD_LENGTH_16:
|
||||||
|
this.getPayloadLength16(cb);
|
||||||
|
break;
|
||||||
|
case GET_PAYLOAD_LENGTH_64:
|
||||||
|
this.getPayloadLength64(cb);
|
||||||
|
break;
|
||||||
|
case GET_MASK:
|
||||||
|
this.getMask();
|
||||||
|
break;
|
||||||
|
case GET_DATA:
|
||||||
|
this.getData(cb);
|
||||||
|
break;
|
||||||
|
case INFLATING:
|
||||||
|
case DEFER_EVENT:
|
||||||
|
this._loop = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} while (this._loop);
|
||||||
|
|
||||||
|
if (!this._errored) cb();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads the first two bytes of a frame.
|
||||||
|
*
|
||||||
|
* @param {Function} cb Callback
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
getInfo(cb) {
|
||||||
|
if (this._bufferedBytes < 2) {
|
||||||
|
this._loop = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const buf = this.consume(2);
|
||||||
|
|
||||||
|
if ((buf[0] & 0x30) !== 0x00) {
|
||||||
|
const error = this.createError(
|
||||||
|
RangeError,
|
||||||
|
'RSV2 and RSV3 must be clear',
|
||||||
|
true,
|
||||||
|
1002,
|
||||||
|
'WS_ERR_UNEXPECTED_RSV_2_3'
|
||||||
|
);
|
||||||
|
|
||||||
|
cb(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const compressed = (buf[0] & 0x40) === 0x40;
|
||||||
|
|
||||||
|
if (compressed && !this._extensions[PerMessageDeflate.extensionName]) {
|
||||||
|
const error = this.createError(
|
||||||
|
RangeError,
|
||||||
|
'RSV1 must be clear',
|
||||||
|
true,
|
||||||
|
1002,
|
||||||
|
'WS_ERR_UNEXPECTED_RSV_1'
|
||||||
|
);
|
||||||
|
|
||||||
|
cb(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._fin = (buf[0] & 0x80) === 0x80;
|
||||||
|
this._opcode = buf[0] & 0x0f;
|
||||||
|
this._payloadLength = buf[1] & 0x7f;
|
||||||
|
|
||||||
|
if (this._opcode === 0x00) {
|
||||||
|
if (compressed) {
|
||||||
|
const error = this.createError(
|
||||||
|
RangeError,
|
||||||
|
'RSV1 must be clear',
|
||||||
|
true,
|
||||||
|
1002,
|
||||||
|
'WS_ERR_UNEXPECTED_RSV_1'
|
||||||
|
);
|
||||||
|
|
||||||
|
cb(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this._fragmented) {
|
||||||
|
const error = this.createError(
|
||||||
|
RangeError,
|
||||||
|
'invalid opcode 0',
|
||||||
|
true,
|
||||||
|
1002,
|
||||||
|
'WS_ERR_INVALID_OPCODE'
|
||||||
|
);
|
||||||
|
|
||||||
|
cb(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._opcode = this._fragmented;
|
||||||
|
} else if (this._opcode === 0x01 || this._opcode === 0x02) {
|
||||||
|
if (this._fragmented) {
|
||||||
|
const error = this.createError(
|
||||||
|
RangeError,
|
||||||
|
`invalid opcode ${this._opcode}`,
|
||||||
|
true,
|
||||||
|
1002,
|
||||||
|
'WS_ERR_INVALID_OPCODE'
|
||||||
|
);
|
||||||
|
|
||||||
|
cb(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._compressed = compressed;
|
||||||
|
} else if (this._opcode > 0x07 && this._opcode < 0x0b) {
|
||||||
|
if (!this._fin) {
|
||||||
|
const error = this.createError(
|
||||||
|
RangeError,
|
||||||
|
'FIN must be set',
|
||||||
|
true,
|
||||||
|
1002,
|
||||||
|
'WS_ERR_EXPECTED_FIN'
|
||||||
|
);
|
||||||
|
|
||||||
|
cb(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (compressed) {
|
||||||
|
const error = this.createError(
|
||||||
|
RangeError,
|
||||||
|
'RSV1 must be clear',
|
||||||
|
true,
|
||||||
|
1002,
|
||||||
|
'WS_ERR_UNEXPECTED_RSV_1'
|
||||||
|
);
|
||||||
|
|
||||||
|
cb(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
this._payloadLength > 0x7d ||
|
||||||
|
(this._opcode === 0x08 && this._payloadLength === 1)
|
||||||
|
) {
|
||||||
|
const error = this.createError(
|
||||||
|
RangeError,
|
||||||
|
`invalid payload length ${this._payloadLength}`,
|
||||||
|
true,
|
||||||
|
1002,
|
||||||
|
'WS_ERR_INVALID_CONTROL_PAYLOAD_LENGTH'
|
||||||
|
);
|
||||||
|
|
||||||
|
cb(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const error = this.createError(
|
||||||
|
RangeError,
|
||||||
|
`invalid opcode ${this._opcode}`,
|
||||||
|
true,
|
||||||
|
1002,
|
||||||
|
'WS_ERR_INVALID_OPCODE'
|
||||||
|
);
|
||||||
|
|
||||||
|
cb(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this._fin && !this._fragmented) this._fragmented = this._opcode;
|
||||||
|
this._masked = (buf[1] & 0x80) === 0x80;
|
||||||
|
|
||||||
|
if (this._isServer) {
|
||||||
|
if (!this._masked) {
|
||||||
|
const error = this.createError(
|
||||||
|
RangeError,
|
||||||
|
'MASK must be set',
|
||||||
|
true,
|
||||||
|
1002,
|
||||||
|
'WS_ERR_EXPECTED_MASK'
|
||||||
|
);
|
||||||
|
|
||||||
|
cb(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else if (this._masked) {
|
||||||
|
const error = this.createError(
|
||||||
|
RangeError,
|
||||||
|
'MASK must be clear',
|
||||||
|
true,
|
||||||
|
1002,
|
||||||
|
'WS_ERR_UNEXPECTED_MASK'
|
||||||
|
);
|
||||||
|
|
||||||
|
cb(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._payloadLength === 126) this._state = GET_PAYLOAD_LENGTH_16;
|
||||||
|
else if (this._payloadLength === 127) this._state = GET_PAYLOAD_LENGTH_64;
|
||||||
|
else this.haveLength(cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets extended payload length (7+16).
|
||||||
|
*
|
||||||
|
* @param {Function} cb Callback
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
getPayloadLength16(cb) {
|
||||||
|
if (this._bufferedBytes < 2) {
|
||||||
|
this._loop = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._payloadLength = this.consume(2).readUInt16BE(0);
|
||||||
|
this.haveLength(cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets extended payload length (7+64).
|
||||||
|
*
|
||||||
|
* @param {Function} cb Callback
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
getPayloadLength64(cb) {
|
||||||
|
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) {
|
||||||
|
const error = this.createError(
|
||||||
|
RangeError,
|
||||||
|
'Unsupported WebSocket frame: payload length > 2^53 - 1',
|
||||||
|
false,
|
||||||
|
1009,
|
||||||
|
'WS_ERR_UNSUPPORTED_DATA_PAYLOAD_LENGTH'
|
||||||
|
);
|
||||||
|
|
||||||
|
cb(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._payloadLength = num * Math.pow(2, 32) + buf.readUInt32BE(4);
|
||||||
|
this.haveLength(cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Payload length has been read.
|
||||||
|
*
|
||||||
|
* @param {Function} cb Callback
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
haveLength(cb) {
|
||||||
|
if (this._payloadLength && this._opcode < 0x08) {
|
||||||
|
this._totalPayloadLength += this._payloadLength;
|
||||||
|
if (this._totalPayloadLength > this._maxPayload && this._maxPayload > 0) {
|
||||||
|
const error = this.createError(
|
||||||
|
RangeError,
|
||||||
|
'Max payload size exceeded',
|
||||||
|
false,
|
||||||
|
1009,
|
||||||
|
'WS_ERR_UNSUPPORTED_MESSAGE_LENGTH'
|
||||||
|
);
|
||||||
|
|
||||||
|
cb(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
getData(cb) {
|
||||||
|
let data = EMPTY_BUFFER;
|
||||||
|
|
||||||
|
if (this._payloadLength) {
|
||||||
|
if (this._bufferedBytes < this._payloadLength) {
|
||||||
|
this._loop = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
data = this.consume(this._payloadLength);
|
||||||
|
|
||||||
|
if (
|
||||||
|
this._masked &&
|
||||||
|
(this._mask[0] | this._mask[1] | this._mask[2] | this._mask[3]) !== 0
|
||||||
|
) {
|
||||||
|
unmask(data, this._mask);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._opcode > 0x07) {
|
||||||
|
this.controlMessage(data, cb);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._compressed) {
|
||||||
|
this._state = INFLATING;
|
||||||
|
this.decompress(data, cb);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.length) {
|
||||||
|
//
|
||||||
|
// This message is not compressed so its length is the sum of the payload
|
||||||
|
// length of all fragments.
|
||||||
|
//
|
||||||
|
this._messageLength = this._totalPayloadLength;
|
||||||
|
this._fragments.push(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dataMessage(cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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) {
|
||||||
|
const error = this.createError(
|
||||||
|
RangeError,
|
||||||
|
'Max payload size exceeded',
|
||||||
|
false,
|
||||||
|
1009,
|
||||||
|
'WS_ERR_UNSUPPORTED_MESSAGE_LENGTH'
|
||||||
|
);
|
||||||
|
|
||||||
|
cb(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._fragments.push(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dataMessage(cb);
|
||||||
|
if (this._state === GET_INFO) this.startLoop(cb);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles a data message.
|
||||||
|
*
|
||||||
|
* @param {Function} cb Callback
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
dataMessage(cb) {
|
||||||
|
if (!this._fin) {
|
||||||
|
this._state = GET_INFO;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const messageLength = this._messageLength;
|
||||||
|
const fragments = this._fragments;
|
||||||
|
|
||||||
|
this._totalPayloadLength = 0;
|
||||||
|
this._messageLength = 0;
|
||||||
|
this._fragmented = 0;
|
||||||
|
this._fragments = [];
|
||||||
|
|
||||||
|
if (this._opcode === 2) {
|
||||||
|
let data;
|
||||||
|
|
||||||
|
if (this._binaryType === 'nodebuffer') {
|
||||||
|
data = concat(fragments, messageLength);
|
||||||
|
} else if (this._binaryType === 'arraybuffer') {
|
||||||
|
data = toArrayBuffer(concat(fragments, messageLength));
|
||||||
|
} else if (this._binaryType === 'blob') {
|
||||||
|
data = new Blob(fragments);
|
||||||
|
} else {
|
||||||
|
data = fragments;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._allowSynchronousEvents) {
|
||||||
|
this.emit('message', data, true);
|
||||||
|
this._state = GET_INFO;
|
||||||
|
} else {
|
||||||
|
this._state = DEFER_EVENT;
|
||||||
|
setImmediate(() => {
|
||||||
|
this.emit('message', data, true);
|
||||||
|
this._state = GET_INFO;
|
||||||
|
this.startLoop(cb);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const buf = concat(fragments, messageLength);
|
||||||
|
|
||||||
|
if (!this._skipUTF8Validation && !isValidUTF8(buf)) {
|
||||||
|
const error = this.createError(
|
||||||
|
Error,
|
||||||
|
'invalid UTF-8 sequence',
|
||||||
|
true,
|
||||||
|
1007,
|
||||||
|
'WS_ERR_INVALID_UTF8'
|
||||||
|
);
|
||||||
|
|
||||||
|
cb(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._state === INFLATING || this._allowSynchronousEvents) {
|
||||||
|
this.emit('message', buf, false);
|
||||||
|
this._state = GET_INFO;
|
||||||
|
} else {
|
||||||
|
this._state = DEFER_EVENT;
|
||||||
|
setImmediate(() => {
|
||||||
|
this.emit('message', buf, false);
|
||||||
|
this._state = GET_INFO;
|
||||||
|
this.startLoop(cb);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles a control message.
|
||||||
|
*
|
||||||
|
* @param {Buffer} data Data to handle
|
||||||
|
* @return {(Error|RangeError|undefined)} A possible error
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
controlMessage(data, cb) {
|
||||||
|
if (this._opcode === 0x08) {
|
||||||
|
if (data.length === 0) {
|
||||||
|
this._loop = false;
|
||||||
|
this.emit('conclude', 1005, EMPTY_BUFFER);
|
||||||
|
this.end();
|
||||||
|
} else {
|
||||||
|
const code = data.readUInt16BE(0);
|
||||||
|
|
||||||
|
if (!isValidStatusCode(code)) {
|
||||||
|
const error = this.createError(
|
||||||
|
RangeError,
|
||||||
|
`invalid status code ${code}`,
|
||||||
|
true,
|
||||||
|
1002,
|
||||||
|
'WS_ERR_INVALID_CLOSE_CODE'
|
||||||
|
);
|
||||||
|
|
||||||
|
cb(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const buf = new FastBuffer(
|
||||||
|
data.buffer,
|
||||||
|
data.byteOffset + 2,
|
||||||
|
data.length - 2
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!this._skipUTF8Validation && !isValidUTF8(buf)) {
|
||||||
|
const error = this.createError(
|
||||||
|
Error,
|
||||||
|
'invalid UTF-8 sequence',
|
||||||
|
true,
|
||||||
|
1007,
|
||||||
|
'WS_ERR_INVALID_UTF8'
|
||||||
|
);
|
||||||
|
|
||||||
|
cb(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._loop = false;
|
||||||
|
this.emit('conclude', code, buf);
|
||||||
|
this.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
this._state = GET_INFO;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._allowSynchronousEvents) {
|
||||||
|
this.emit(this._opcode === 0x09 ? 'ping' : 'pong', data);
|
||||||
|
this._state = GET_INFO;
|
||||||
|
} else {
|
||||||
|
this._state = DEFER_EVENT;
|
||||||
|
setImmediate(() => {
|
||||||
|
this.emit(this._opcode === 0x09 ? 'ping' : 'pong', data);
|
||||||
|
this._state = GET_INFO;
|
||||||
|
this.startLoop(cb);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds an error object.
|
||||||
|
*
|
||||||
|
* @param {function(new: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
|
||||||
|
* @param {String} errorCode The exposed error code
|
||||||
|
* @return {(Error|RangeError)} The error
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
createError(ErrorCtor, message, prefix, statusCode, errorCode) {
|
||||||
|
this._loop = false;
|
||||||
|
this._errored = true;
|
||||||
|
|
||||||
|
const err = new ErrorCtor(
|
||||||
|
prefix ? `Invalid WebSocket frame: ${message}` : message
|
||||||
|
);
|
||||||
|
|
||||||
|
Error.captureStackTrace(err, this.createError);
|
||||||
|
err.code = errorCode;
|
||||||
|
err[kStatusCode] = statusCode;
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Receiver;
|
||||||
602
node_modules/ws/lib/sender.js
generated
vendored
Normal file
602
node_modules/ws/lib/sender.js
generated
vendored
Normal file
@@ -0,0 +1,602 @@
|
|||||||
|
/* eslint no-unused-vars: ["error", { "varsIgnorePattern": "^Duplex" }] */
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const { Duplex } = require('stream');
|
||||||
|
const { randomFillSync } = require('crypto');
|
||||||
|
|
||||||
|
const PerMessageDeflate = require('./permessage-deflate');
|
||||||
|
const { EMPTY_BUFFER, kWebSocket, NOOP } = require('./constants');
|
||||||
|
const { isBlob, isValidStatusCode } = require('./validation');
|
||||||
|
const { mask: applyMask, toBuffer } = require('./buffer-util');
|
||||||
|
|
||||||
|
const kByteLength = Symbol('kByteLength');
|
||||||
|
const maskBuffer = Buffer.alloc(4);
|
||||||
|
const RANDOM_POOL_SIZE = 8 * 1024;
|
||||||
|
let randomPool;
|
||||||
|
let randomPoolPointer = RANDOM_POOL_SIZE;
|
||||||
|
|
||||||
|
const DEFAULT = 0;
|
||||||
|
const DEFLATING = 1;
|
||||||
|
const GET_BLOB_DATA = 2;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HyBi Sender implementation.
|
||||||
|
*/
|
||||||
|
class Sender {
|
||||||
|
/**
|
||||||
|
* Creates a Sender instance.
|
||||||
|
*
|
||||||
|
* @param {Duplex} socket The connection socket
|
||||||
|
* @param {Object} [extensions] An object containing the negotiated extensions
|
||||||
|
* @param {Function} [generateMask] The function used to generate the masking
|
||||||
|
* key
|
||||||
|
*/
|
||||||
|
constructor(socket, extensions, generateMask) {
|
||||||
|
this._extensions = extensions || {};
|
||||||
|
|
||||||
|
if (generateMask) {
|
||||||
|
this._generateMask = generateMask;
|
||||||
|
this._maskBuffer = Buffer.alloc(4);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._socket = socket;
|
||||||
|
|
||||||
|
this._firstFragment = true;
|
||||||
|
this._compress = false;
|
||||||
|
|
||||||
|
this._bufferedBytes = 0;
|
||||||
|
this._queue = [];
|
||||||
|
this._state = DEFAULT;
|
||||||
|
this.onerror = NOOP;
|
||||||
|
this[kWebSocket] = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Frames a piece of data according to the HyBi WebSocket protocol.
|
||||||
|
*
|
||||||
|
* @param {(Buffer|String)} data The data to frame
|
||||||
|
* @param {Object} options Options object
|
||||||
|
* @param {Boolean} [options.fin=false] Specifies whether or not to set the
|
||||||
|
* FIN bit
|
||||||
|
* @param {Function} [options.generateMask] The function used to generate the
|
||||||
|
* masking key
|
||||||
|
* @param {Boolean} [options.mask=false] Specifies whether or not to mask
|
||||||
|
* `data`
|
||||||
|
* @param {Buffer} [options.maskBuffer] The buffer used to store the masking
|
||||||
|
* key
|
||||||
|
* @param {Number} options.opcode The opcode
|
||||||
|
* @param {Boolean} [options.readOnly=false] Specifies whether `data` can be
|
||||||
|
* modified
|
||||||
|
* @param {Boolean} [options.rsv1=false] Specifies whether or not to set the
|
||||||
|
* RSV1 bit
|
||||||
|
* @return {(Buffer|String)[]} The framed data
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
static frame(data, options) {
|
||||||
|
let mask;
|
||||||
|
let merge = false;
|
||||||
|
let offset = 2;
|
||||||
|
let skipMasking = false;
|
||||||
|
|
||||||
|
if (options.mask) {
|
||||||
|
mask = options.maskBuffer || maskBuffer;
|
||||||
|
|
||||||
|
if (options.generateMask) {
|
||||||
|
options.generateMask(mask);
|
||||||
|
} else {
|
||||||
|
if (randomPoolPointer === RANDOM_POOL_SIZE) {
|
||||||
|
/* istanbul ignore else */
|
||||||
|
if (randomPool === undefined) {
|
||||||
|
//
|
||||||
|
// This is lazily initialized because server-sent frames must not
|
||||||
|
// be masked so it may never be used.
|
||||||
|
//
|
||||||
|
randomPool = Buffer.alloc(RANDOM_POOL_SIZE);
|
||||||
|
}
|
||||||
|
|
||||||
|
randomFillSync(randomPool, 0, RANDOM_POOL_SIZE);
|
||||||
|
randomPoolPointer = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
mask[0] = randomPool[randomPoolPointer++];
|
||||||
|
mask[1] = randomPool[randomPoolPointer++];
|
||||||
|
mask[2] = randomPool[randomPoolPointer++];
|
||||||
|
mask[3] = randomPool[randomPoolPointer++];
|
||||||
|
}
|
||||||
|
|
||||||
|
skipMasking = (mask[0] | mask[1] | mask[2] | mask[3]) === 0;
|
||||||
|
offset = 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
let dataLength;
|
||||||
|
|
||||||
|
if (typeof data === 'string') {
|
||||||
|
if (
|
||||||
|
(!options.mask || skipMasking) &&
|
||||||
|
options[kByteLength] !== undefined
|
||||||
|
) {
|
||||||
|
dataLength = options[kByteLength];
|
||||||
|
} else {
|
||||||
|
data = Buffer.from(data);
|
||||||
|
dataLength = data.length;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dataLength = data.length;
|
||||||
|
merge = options.mask && options.readOnly && !skipMasking;
|
||||||
|
}
|
||||||
|
|
||||||
|
let payloadLength = dataLength;
|
||||||
|
|
||||||
|
if (dataLength >= 65536) {
|
||||||
|
offset += 8;
|
||||||
|
payloadLength = 127;
|
||||||
|
} else if (dataLength > 125) {
|
||||||
|
offset += 2;
|
||||||
|
payloadLength = 126;
|
||||||
|
}
|
||||||
|
|
||||||
|
const target = Buffer.allocUnsafe(merge ? dataLength + offset : offset);
|
||||||
|
|
||||||
|
target[0] = options.fin ? options.opcode | 0x80 : options.opcode;
|
||||||
|
if (options.rsv1) target[0] |= 0x40;
|
||||||
|
|
||||||
|
target[1] = payloadLength;
|
||||||
|
|
||||||
|
if (payloadLength === 126) {
|
||||||
|
target.writeUInt16BE(dataLength, 2);
|
||||||
|
} else if (payloadLength === 127) {
|
||||||
|
target[2] = target[3] = 0;
|
||||||
|
target.writeUIntBE(dataLength, 4, 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!options.mask) return [target, data];
|
||||||
|
|
||||||
|
target[1] |= 0x80;
|
||||||
|
target[offset - 4] = mask[0];
|
||||||
|
target[offset - 3] = mask[1];
|
||||||
|
target[offset - 2] = mask[2];
|
||||||
|
target[offset - 1] = mask[3];
|
||||||
|
|
||||||
|
if (skipMasking) return [target, data];
|
||||||
|
|
||||||
|
if (merge) {
|
||||||
|
applyMask(data, mask, target, offset, dataLength);
|
||||||
|
return [target];
|
||||||
|
}
|
||||||
|
|
||||||
|
applyMask(data, mask, data, 0, dataLength);
|
||||||
|
return [target, data];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a close message to the other peer.
|
||||||
|
*
|
||||||
|
* @param {Number} [code] The status code component of the body
|
||||||
|
* @param {(String|Buffer)} [data] The message component of the body
|
||||||
|
* @param {Boolean} [mask=false] Specifies whether or not to mask the message
|
||||||
|
* @param {Function} [cb] Callback
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
close(code, data, mask, cb) {
|
||||||
|
let buf;
|
||||||
|
|
||||||
|
if (code === undefined) {
|
||||||
|
buf = EMPTY_BUFFER;
|
||||||
|
} else if (typeof code !== 'number' || !isValidStatusCode(code)) {
|
||||||
|
throw new TypeError('First argument must be a valid error code number');
|
||||||
|
} else if (data === undefined || !data.length) {
|
||||||
|
buf = Buffer.allocUnsafe(2);
|
||||||
|
buf.writeUInt16BE(code, 0);
|
||||||
|
} else {
|
||||||
|
const length = Buffer.byteLength(data);
|
||||||
|
|
||||||
|
if (length > 123) {
|
||||||
|
throw new RangeError('The message must not be greater than 123 bytes');
|
||||||
|
}
|
||||||
|
|
||||||
|
buf = Buffer.allocUnsafe(2 + length);
|
||||||
|
buf.writeUInt16BE(code, 0);
|
||||||
|
|
||||||
|
if (typeof data === 'string') {
|
||||||
|
buf.write(data, 2);
|
||||||
|
} else {
|
||||||
|
buf.set(data, 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
[kByteLength]: buf.length,
|
||||||
|
fin: true,
|
||||||
|
generateMask: this._generateMask,
|
||||||
|
mask,
|
||||||
|
maskBuffer: this._maskBuffer,
|
||||||
|
opcode: 0x08,
|
||||||
|
readOnly: false,
|
||||||
|
rsv1: false
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this._state !== DEFAULT) {
|
||||||
|
this.enqueue([this.dispatch, buf, false, options, cb]);
|
||||||
|
} else {
|
||||||
|
this.sendFrame(Sender.frame(buf, options), cb);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a ping message to the other peer.
|
||||||
|
*
|
||||||
|
* @param {*} data The message to send
|
||||||
|
* @param {Boolean} [mask=false] Specifies whether or not to mask `data`
|
||||||
|
* @param {Function} [cb] Callback
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
ping(data, mask, cb) {
|
||||||
|
let byteLength;
|
||||||
|
let readOnly;
|
||||||
|
|
||||||
|
if (typeof data === 'string') {
|
||||||
|
byteLength = Buffer.byteLength(data);
|
||||||
|
readOnly = false;
|
||||||
|
} else if (isBlob(data)) {
|
||||||
|
byteLength = data.size;
|
||||||
|
readOnly = false;
|
||||||
|
} else {
|
||||||
|
data = toBuffer(data);
|
||||||
|
byteLength = data.length;
|
||||||
|
readOnly = toBuffer.readOnly;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (byteLength > 125) {
|
||||||
|
throw new RangeError('The data size must not be greater than 125 bytes');
|
||||||
|
}
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
[kByteLength]: byteLength,
|
||||||
|
fin: true,
|
||||||
|
generateMask: this._generateMask,
|
||||||
|
mask,
|
||||||
|
maskBuffer: this._maskBuffer,
|
||||||
|
opcode: 0x09,
|
||||||
|
readOnly,
|
||||||
|
rsv1: false
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isBlob(data)) {
|
||||||
|
if (this._state !== DEFAULT) {
|
||||||
|
this.enqueue([this.getBlobData, data, false, options, cb]);
|
||||||
|
} else {
|
||||||
|
this.getBlobData(data, false, options, cb);
|
||||||
|
}
|
||||||
|
} else if (this._state !== DEFAULT) {
|
||||||
|
this.enqueue([this.dispatch, data, false, options, cb]);
|
||||||
|
} else {
|
||||||
|
this.sendFrame(Sender.frame(data, options), cb);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a pong message to the other peer.
|
||||||
|
*
|
||||||
|
* @param {*} data The message to send
|
||||||
|
* @param {Boolean} [mask=false] Specifies whether or not to mask `data`
|
||||||
|
* @param {Function} [cb] Callback
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
pong(data, mask, cb) {
|
||||||
|
let byteLength;
|
||||||
|
let readOnly;
|
||||||
|
|
||||||
|
if (typeof data === 'string') {
|
||||||
|
byteLength = Buffer.byteLength(data);
|
||||||
|
readOnly = false;
|
||||||
|
} else if (isBlob(data)) {
|
||||||
|
byteLength = data.size;
|
||||||
|
readOnly = false;
|
||||||
|
} else {
|
||||||
|
data = toBuffer(data);
|
||||||
|
byteLength = data.length;
|
||||||
|
readOnly = toBuffer.readOnly;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (byteLength > 125) {
|
||||||
|
throw new RangeError('The data size must not be greater than 125 bytes');
|
||||||
|
}
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
[kByteLength]: byteLength,
|
||||||
|
fin: true,
|
||||||
|
generateMask: this._generateMask,
|
||||||
|
mask,
|
||||||
|
maskBuffer: this._maskBuffer,
|
||||||
|
opcode: 0x0a,
|
||||||
|
readOnly,
|
||||||
|
rsv1: false
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isBlob(data)) {
|
||||||
|
if (this._state !== DEFAULT) {
|
||||||
|
this.enqueue([this.getBlobData, data, false, options, cb]);
|
||||||
|
} else {
|
||||||
|
this.getBlobData(data, false, options, cb);
|
||||||
|
}
|
||||||
|
} else if (this._state !== DEFAULT) {
|
||||||
|
this.enqueue([this.dispatch, data, false, options, cb]);
|
||||||
|
} else {
|
||||||
|
this.sendFrame(Sender.frame(data, options), cb);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a data message to the other peer.
|
||||||
|
*
|
||||||
|
* @param {*} data The message to send
|
||||||
|
* @param {Object} options Options object
|
||||||
|
* @param {Boolean} [options.binary=false] Specifies whether `data` is binary
|
||||||
|
* or text
|
||||||
|
* @param {Boolean} [options.compress=false] Specifies whether or not to
|
||||||
|
* compress `data`
|
||||||
|
* @param {Boolean} [options.fin=false] Specifies whether the fragment is the
|
||||||
|
* last one
|
||||||
|
* @param {Boolean} [options.mask=false] Specifies whether or not to mask
|
||||||
|
* `data`
|
||||||
|
* @param {Function} [cb] Callback
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
send(data, options, cb) {
|
||||||
|
const perMessageDeflate = this._extensions[PerMessageDeflate.extensionName];
|
||||||
|
let opcode = options.binary ? 2 : 1;
|
||||||
|
let rsv1 = options.compress;
|
||||||
|
|
||||||
|
let byteLength;
|
||||||
|
let readOnly;
|
||||||
|
|
||||||
|
if (typeof data === 'string') {
|
||||||
|
byteLength = Buffer.byteLength(data);
|
||||||
|
readOnly = false;
|
||||||
|
} else if (isBlob(data)) {
|
||||||
|
byteLength = data.size;
|
||||||
|
readOnly = false;
|
||||||
|
} else {
|
||||||
|
data = toBuffer(data);
|
||||||
|
byteLength = data.length;
|
||||||
|
readOnly = toBuffer.readOnly;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._firstFragment) {
|
||||||
|
this._firstFragment = false;
|
||||||
|
if (
|
||||||
|
rsv1 &&
|
||||||
|
perMessageDeflate &&
|
||||||
|
perMessageDeflate.params[
|
||||||
|
perMessageDeflate._isServer
|
||||||
|
? 'server_no_context_takeover'
|
||||||
|
: 'client_no_context_takeover'
|
||||||
|
]
|
||||||
|
) {
|
||||||
|
rsv1 = byteLength >= perMessageDeflate._threshold;
|
||||||
|
}
|
||||||
|
this._compress = rsv1;
|
||||||
|
} else {
|
||||||
|
rsv1 = false;
|
||||||
|
opcode = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.fin) this._firstFragment = true;
|
||||||
|
|
||||||
|
const opts = {
|
||||||
|
[kByteLength]: byteLength,
|
||||||
|
fin: options.fin,
|
||||||
|
generateMask: this._generateMask,
|
||||||
|
mask: options.mask,
|
||||||
|
maskBuffer: this._maskBuffer,
|
||||||
|
opcode,
|
||||||
|
readOnly,
|
||||||
|
rsv1
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isBlob(data)) {
|
||||||
|
if (this._state !== DEFAULT) {
|
||||||
|
this.enqueue([this.getBlobData, data, this._compress, opts, cb]);
|
||||||
|
} else {
|
||||||
|
this.getBlobData(data, this._compress, opts, cb);
|
||||||
|
}
|
||||||
|
} else if (this._state !== DEFAULT) {
|
||||||
|
this.enqueue([this.dispatch, data, this._compress, opts, cb]);
|
||||||
|
} else {
|
||||||
|
this.dispatch(data, this._compress, opts, cb);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the contents of a blob as binary data.
|
||||||
|
*
|
||||||
|
* @param {Blob} blob The blob
|
||||||
|
* @param {Boolean} [compress=false] Specifies whether or not to compress
|
||||||
|
* the data
|
||||||
|
* @param {Object} options Options object
|
||||||
|
* @param {Boolean} [options.fin=false] Specifies whether or not to set the
|
||||||
|
* FIN bit
|
||||||
|
* @param {Function} [options.generateMask] The function used to generate the
|
||||||
|
* masking key
|
||||||
|
* @param {Boolean} [options.mask=false] Specifies whether or not to mask
|
||||||
|
* `data`
|
||||||
|
* @param {Buffer} [options.maskBuffer] The buffer used to store the masking
|
||||||
|
* key
|
||||||
|
* @param {Number} options.opcode The opcode
|
||||||
|
* @param {Boolean} [options.readOnly=false] Specifies whether `data` can be
|
||||||
|
* modified
|
||||||
|
* @param {Boolean} [options.rsv1=false] Specifies whether or not to set the
|
||||||
|
* RSV1 bit
|
||||||
|
* @param {Function} [cb] Callback
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
getBlobData(blob, compress, options, cb) {
|
||||||
|
this._bufferedBytes += options[kByteLength];
|
||||||
|
this._state = GET_BLOB_DATA;
|
||||||
|
|
||||||
|
blob
|
||||||
|
.arrayBuffer()
|
||||||
|
.then((arrayBuffer) => {
|
||||||
|
if (this._socket.destroyed) {
|
||||||
|
const err = new Error(
|
||||||
|
'The socket was closed while the blob was being read'
|
||||||
|
);
|
||||||
|
|
||||||
|
//
|
||||||
|
// `callCallbacks` is called in the next tick to ensure that errors
|
||||||
|
// that might be thrown in the callbacks behave like errors thrown
|
||||||
|
// outside the promise chain.
|
||||||
|
//
|
||||||
|
process.nextTick(callCallbacks, this, err, cb);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._bufferedBytes -= options[kByteLength];
|
||||||
|
const data = toBuffer(arrayBuffer);
|
||||||
|
|
||||||
|
if (!compress) {
|
||||||
|
this._state = DEFAULT;
|
||||||
|
this.sendFrame(Sender.frame(data, options), cb);
|
||||||
|
this.dequeue();
|
||||||
|
} else {
|
||||||
|
this.dispatch(data, compress, options, cb);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
//
|
||||||
|
// `onError` is called in the next tick for the same reason that
|
||||||
|
// `callCallbacks` above is.
|
||||||
|
//
|
||||||
|
process.nextTick(onError, this, err, cb);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dispatches a message.
|
||||||
|
*
|
||||||
|
* @param {(Buffer|String)} data The message to send
|
||||||
|
* @param {Boolean} [compress=false] Specifies whether or not to compress
|
||||||
|
* `data`
|
||||||
|
* @param {Object} options Options object
|
||||||
|
* @param {Boolean} [options.fin=false] Specifies whether or not to set the
|
||||||
|
* FIN bit
|
||||||
|
* @param {Function} [options.generateMask] The function used to generate the
|
||||||
|
* masking key
|
||||||
|
* @param {Boolean} [options.mask=false] Specifies whether or not to mask
|
||||||
|
* `data`
|
||||||
|
* @param {Buffer} [options.maskBuffer] The buffer used to store the masking
|
||||||
|
* key
|
||||||
|
* @param {Number} options.opcode The opcode
|
||||||
|
* @param {Boolean} [options.readOnly=false] Specifies whether `data` can be
|
||||||
|
* modified
|
||||||
|
* @param {Boolean} [options.rsv1=false] 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._bufferedBytes += options[kByteLength];
|
||||||
|
this._state = DEFLATING;
|
||||||
|
perMessageDeflate.compress(data, options.fin, (_, buf) => {
|
||||||
|
if (this._socket.destroyed) {
|
||||||
|
const err = new Error(
|
||||||
|
'The socket was closed while data was being compressed'
|
||||||
|
);
|
||||||
|
|
||||||
|
callCallbacks(this, err, cb);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._bufferedBytes -= options[kByteLength];
|
||||||
|
this._state = DEFAULT;
|
||||||
|
options.readOnly = false;
|
||||||
|
this.sendFrame(Sender.frame(buf, options), cb);
|
||||||
|
this.dequeue();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes queued send operations.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
dequeue() {
|
||||||
|
while (this._state === DEFAULT && this._queue.length) {
|
||||||
|
const params = this._queue.shift();
|
||||||
|
|
||||||
|
this._bufferedBytes -= params[3][kByteLength];
|
||||||
|
Reflect.apply(params[0], this, params.slice(1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enqueues a send operation.
|
||||||
|
*
|
||||||
|
* @param {Array} params Send operation parameters.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
enqueue(params) {
|
||||||
|
this._bufferedBytes += params[3][kByteLength];
|
||||||
|
this._queue.push(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a frame.
|
||||||
|
*
|
||||||
|
* @param {(Buffer | String)[]} list The frame to send
|
||||||
|
* @param {Function} [cb] Callback
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
sendFrame(list, cb) {
|
||||||
|
if (list.length === 2) {
|
||||||
|
this._socket.cork();
|
||||||
|
this._socket.write(list[0]);
|
||||||
|
this._socket.write(list[1], cb);
|
||||||
|
this._socket.uncork();
|
||||||
|
} else {
|
||||||
|
this._socket.write(list[0], cb);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Sender;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calls queued callbacks with an error.
|
||||||
|
*
|
||||||
|
* @param {Sender} sender The `Sender` instance
|
||||||
|
* @param {Error} err The error to call the callbacks with
|
||||||
|
* @param {Function} [cb] The first callback
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
function callCallbacks(sender, err, cb) {
|
||||||
|
if (typeof cb === 'function') cb(err);
|
||||||
|
|
||||||
|
for (let i = 0; i < sender._queue.length; i++) {
|
||||||
|
const params = sender._queue[i];
|
||||||
|
const callback = params[params.length - 1];
|
||||||
|
|
||||||
|
if (typeof callback === 'function') callback(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles a `Sender` error.
|
||||||
|
*
|
||||||
|
* @param {Sender} sender The `Sender` instance
|
||||||
|
* @param {Error} err The error
|
||||||
|
* @param {Function} [cb] The first pending callback
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
function onError(sender, err, cb) {
|
||||||
|
callCallbacks(sender, err, cb);
|
||||||
|
sender.onerror(err);
|
||||||
|
}
|
||||||
161
node_modules/ws/lib/stream.js
generated
vendored
Normal file
161
node_modules/ws/lib/stream.js
generated
vendored
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
/* eslint no-unused-vars: ["error", { "varsIgnorePattern": "^WebSocket$" }] */
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const WebSocket = require('./websocket');
|
||||||
|
const { Duplex } = require('stream');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emits the `'close'` event on a stream.
|
||||||
|
*
|
||||||
|
* @param {Duplex} stream The stream.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
function emitClose(stream) {
|
||||||
|
stream.emit('close');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The listener of the `'end'` event.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
function duplexOnEnd() {
|
||||||
|
if (!this.destroyed && this._writableState.finished) {
|
||||||
|
this.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The listener of the `'error'` event.
|
||||||
|
*
|
||||||
|
* @param {Error} err The error
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
function duplexOnError(err) {
|
||||||
|
this.removeListener('error', duplexOnError);
|
||||||
|
this.destroy();
|
||||||
|
if (this.listenerCount('error') === 0) {
|
||||||
|
// Do not suppress the throwing behavior.
|
||||||
|
this.emit('error', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wraps a `WebSocket` in a duplex stream.
|
||||||
|
*
|
||||||
|
* @param {WebSocket} ws The `WebSocket` to wrap
|
||||||
|
* @param {Object} [options] The options for the `Duplex` constructor
|
||||||
|
* @return {Duplex} The duplex stream
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
function createWebSocketStream(ws, options) {
|
||||||
|
let terminateOnDestroy = true;
|
||||||
|
|
||||||
|
const duplex = new Duplex({
|
||||||
|
...options,
|
||||||
|
autoDestroy: false,
|
||||||
|
emitClose: false,
|
||||||
|
objectMode: false,
|
||||||
|
writableObjectMode: false
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.on('message', function message(msg, isBinary) {
|
||||||
|
const data =
|
||||||
|
!isBinary && duplex._readableState.objectMode ? msg.toString() : msg;
|
||||||
|
|
||||||
|
if (!duplex.push(data)) ws.pause();
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.once('error', function error(err) {
|
||||||
|
if (duplex.destroyed) return;
|
||||||
|
|
||||||
|
// Prevent `ws.terminate()` from being called by `duplex._destroy()`.
|
||||||
|
//
|
||||||
|
// - If the `'error'` event is emitted before the `'open'` event, then
|
||||||
|
// `ws.terminate()` is a noop as no socket is assigned.
|
||||||
|
// - Otherwise, the error is re-emitted by the listener of the `'error'`
|
||||||
|
// event of the `Receiver` object. The listener already closes the
|
||||||
|
// connection by calling `ws.close()`. This allows a close frame to be
|
||||||
|
// sent to the other peer. If `ws.terminate()` is called right after this,
|
||||||
|
// then the close frame might not be sent.
|
||||||
|
terminateOnDestroy = false;
|
||||||
|
duplex.destroy(err);
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.once('close', function close() {
|
||||||
|
if (duplex.destroyed) return;
|
||||||
|
|
||||||
|
duplex.push(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
duplex._destroy = function (err, callback) {
|
||||||
|
if (ws.readyState === ws.CLOSED) {
|
||||||
|
callback(err);
|
||||||
|
process.nextTick(emitClose, duplex);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let called = false;
|
||||||
|
|
||||||
|
ws.once('error', function error(err) {
|
||||||
|
called = true;
|
||||||
|
callback(err);
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.once('close', function close() {
|
||||||
|
if (!called) callback(err);
|
||||||
|
process.nextTick(emitClose, duplex);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (terminateOnDestroy) ws.terminate();
|
||||||
|
};
|
||||||
|
|
||||||
|
duplex._final = function (callback) {
|
||||||
|
if (ws.readyState === ws.CONNECTING) {
|
||||||
|
ws.once('open', function open() {
|
||||||
|
duplex._final(callback);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the value of the `_socket` property is `null` it means that `ws` is a
|
||||||
|
// client websocket and the handshake failed. In fact, when this happens, a
|
||||||
|
// socket is never assigned to the websocket. Wait for the `'error'` event
|
||||||
|
// that will be emitted by the websocket.
|
||||||
|
if (ws._socket === null) return;
|
||||||
|
|
||||||
|
if (ws._socket._writableState.finished) {
|
||||||
|
callback();
|
||||||
|
if (duplex._readableState.endEmitted) duplex.destroy();
|
||||||
|
} else {
|
||||||
|
ws._socket.once('finish', function finish() {
|
||||||
|
// `duplex` is not destroyed here because the `'end'` event will be
|
||||||
|
// emitted on `duplex` after this `'finish'` event. The EOF signaling
|
||||||
|
// `null` chunk is, in fact, pushed when the websocket emits `'close'`.
|
||||||
|
callback();
|
||||||
|
});
|
||||||
|
ws.close();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
duplex._read = function () {
|
||||||
|
if (ws.isPaused) ws.resume();
|
||||||
|
};
|
||||||
|
|
||||||
|
duplex._write = function (chunk, encoding, callback) {
|
||||||
|
if (ws.readyState === ws.CONNECTING) {
|
||||||
|
ws.once('open', function open() {
|
||||||
|
duplex._write(chunk, encoding, callback);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.send(chunk, callback);
|
||||||
|
};
|
||||||
|
|
||||||
|
duplex.on('end', duplexOnEnd);
|
||||||
|
duplex.on('error', duplexOnError);
|
||||||
|
return duplex;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = createWebSocketStream;
|
||||||
62
node_modules/ws/lib/subprotocol.js
generated
vendored
Normal file
62
node_modules/ws/lib/subprotocol.js
generated
vendored
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const { tokenChars } = require('./validation');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses the `Sec-WebSocket-Protocol` header into a set of subprotocol names.
|
||||||
|
*
|
||||||
|
* @param {String} header The field value of the header
|
||||||
|
* @return {Set} The subprotocol names
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
function parse(header) {
|
||||||
|
const protocols = new Set();
|
||||||
|
let start = -1;
|
||||||
|
let end = -1;
|
||||||
|
let i = 0;
|
||||||
|
|
||||||
|
for (i; i < header.length; i++) {
|
||||||
|
const code = header.charCodeAt(i);
|
||||||
|
|
||||||
|
if (end === -1 && tokenChars[code] === 1) {
|
||||||
|
if (start === -1) start = i;
|
||||||
|
} else if (
|
||||||
|
i !== 0 &&
|
||||||
|
(code === 0x20 /* ' ' */ || code === 0x09) /* '\t' */
|
||||||
|
) {
|
||||||
|
if (end === -1 && start !== -1) end = i;
|
||||||
|
} else if (code === 0x2c /* ',' */) {
|
||||||
|
if (start === -1) {
|
||||||
|
throw new SyntaxError(`Unexpected character at index ${i}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (end === -1) end = i;
|
||||||
|
|
||||||
|
const protocol = header.slice(start, end);
|
||||||
|
|
||||||
|
if (protocols.has(protocol)) {
|
||||||
|
throw new SyntaxError(`The "${protocol}" subprotocol is duplicated`);
|
||||||
|
}
|
||||||
|
|
||||||
|
protocols.add(protocol);
|
||||||
|
start = end = -1;
|
||||||
|
} else {
|
||||||
|
throw new SyntaxError(`Unexpected character at index ${i}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (start === -1 || end !== -1) {
|
||||||
|
throw new SyntaxError('Unexpected end of input');
|
||||||
|
}
|
||||||
|
|
||||||
|
const protocol = header.slice(start, i);
|
||||||
|
|
||||||
|
if (protocols.has(protocol)) {
|
||||||
|
throw new SyntaxError(`The "${protocol}" subprotocol is duplicated`);
|
||||||
|
}
|
||||||
|
|
||||||
|
protocols.add(protocol);
|
||||||
|
return protocols;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { parse };
|
||||||
152
node_modules/ws/lib/validation.js
generated
vendored
Normal file
152
node_modules/ws/lib/validation.js
generated
vendored
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const { isUtf8 } = require('buffer');
|
||||||
|
|
||||||
|
const { hasBlob } = require('./constants');
|
||||||
|
|
||||||
|
//
|
||||||
|
// Allowed token characters:
|
||||||
|
//
|
||||||
|
// '!', '#', '$', '%', '&', ''', '*', '+', '-',
|
||||||
|
// '.', 0-9, A-Z, '^', '_', '`', a-z, '|', '~'
|
||||||
|
//
|
||||||
|
// tokenChars[32] === 0 // ' '
|
||||||
|
// tokenChars[33] === 1 // '!'
|
||||||
|
// tokenChars[34] === 0 // '"'
|
||||||
|
// ...
|
||||||
|
//
|
||||||
|
// prettier-ignore
|
||||||
|
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
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
*/
|
||||||
|
function isValidStatusCode(code) {
|
||||||
|
return (
|
||||||
|
(code >= 1000 &&
|
||||||
|
code <= 1014 &&
|
||||||
|
code !== 1004 &&
|
||||||
|
code !== 1005 &&
|
||||||
|
code !== 1006) ||
|
||||||
|
(code >= 3000 && code <= 4999)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a given buffer contains only correct UTF-8.
|
||||||
|
* Ported from https://www.cl.cam.ac.uk/%7Emgk25/ucs/utf8_check.c by
|
||||||
|
* Markus Kuhn.
|
||||||
|
*
|
||||||
|
* @param {Buffer} buf The buffer to check
|
||||||
|
* @return {Boolean} `true` if `buf` contains only correct UTF-8, else `false`
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
function _isValidUTF8(buf) {
|
||||||
|
const len = buf.length;
|
||||||
|
let i = 0;
|
||||||
|
|
||||||
|
while (i < len) {
|
||||||
|
if ((buf[i] & 0x80) === 0) {
|
||||||
|
// 0xxxxxxx
|
||||||
|
i++;
|
||||||
|
} else if ((buf[i] & 0xe0) === 0xc0) {
|
||||||
|
// 110xxxxx 10xxxxxx
|
||||||
|
if (
|
||||||
|
i + 1 === len ||
|
||||||
|
(buf[i + 1] & 0xc0) !== 0x80 ||
|
||||||
|
(buf[i] & 0xfe) === 0xc0 // Overlong
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
i += 2;
|
||||||
|
} else if ((buf[i] & 0xf0) === 0xe0) {
|
||||||
|
// 1110xxxx 10xxxxxx 10xxxxxx
|
||||||
|
if (
|
||||||
|
i + 2 >= len ||
|
||||||
|
(buf[i + 1] & 0xc0) !== 0x80 ||
|
||||||
|
(buf[i + 2] & 0xc0) !== 0x80 ||
|
||||||
|
(buf[i] === 0xe0 && (buf[i + 1] & 0xe0) === 0x80) || // Overlong
|
||||||
|
(buf[i] === 0xed && (buf[i + 1] & 0xe0) === 0xa0) // Surrogate (U+D800 - U+DFFF)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
i += 3;
|
||||||
|
} else if ((buf[i] & 0xf8) === 0xf0) {
|
||||||
|
// 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
|
||||||
|
if (
|
||||||
|
i + 3 >= len ||
|
||||||
|
(buf[i + 1] & 0xc0) !== 0x80 ||
|
||||||
|
(buf[i + 2] & 0xc0) !== 0x80 ||
|
||||||
|
(buf[i + 3] & 0xc0) !== 0x80 ||
|
||||||
|
(buf[i] === 0xf0 && (buf[i + 1] & 0xf0) === 0x80) || // Overlong
|
||||||
|
(buf[i] === 0xf4 && buf[i + 1] > 0x8f) ||
|
||||||
|
buf[i] > 0xf4 // > U+10FFFF
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
i += 4;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines whether a value is a `Blob`.
|
||||||
|
*
|
||||||
|
* @param {*} value The value to be tested
|
||||||
|
* @return {Boolean} `true` if `value` is a `Blob`, else `false`
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
function isBlob(value) {
|
||||||
|
return (
|
||||||
|
hasBlob &&
|
||||||
|
typeof value === 'object' &&
|
||||||
|
typeof value.arrayBuffer === 'function' &&
|
||||||
|
typeof value.type === 'string' &&
|
||||||
|
typeof value.stream === 'function' &&
|
||||||
|
(value[Symbol.toStringTag] === 'Blob' ||
|
||||||
|
value[Symbol.toStringTag] === 'File')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
isBlob,
|
||||||
|
isValidStatusCode,
|
||||||
|
isValidUTF8: _isValidUTF8,
|
||||||
|
tokenChars
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isUtf8) {
|
||||||
|
module.exports.isValidUTF8 = function (buf) {
|
||||||
|
return buf.length < 24 ? _isValidUTF8(buf) : isUtf8(buf);
|
||||||
|
};
|
||||||
|
} /* istanbul ignore else */ else if (!process.env.WS_NO_UTF_8_VALIDATE) {
|
||||||
|
try {
|
||||||
|
const isValidUTF8 = require('utf-8-validate');
|
||||||
|
|
||||||
|
module.exports.isValidUTF8 = function (buf) {
|
||||||
|
return buf.length < 32 ? _isValidUTF8(buf) : isValidUTF8(buf);
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
// Continue regardless of the error.
|
||||||
|
}
|
||||||
|
}
|
||||||
554
node_modules/ws/lib/websocket-server.js
generated
vendored
Normal file
554
node_modules/ws/lib/websocket-server.js
generated
vendored
Normal file
@@ -0,0 +1,554 @@
|
|||||||
|
/* eslint no-unused-vars: ["error", { "varsIgnorePattern": "^Duplex$", "caughtErrors": "none" }] */
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const EventEmitter = require('events');
|
||||||
|
const http = require('http');
|
||||||
|
const { Duplex } = require('stream');
|
||||||
|
const { createHash } = require('crypto');
|
||||||
|
|
||||||
|
const extension = require('./extension');
|
||||||
|
const PerMessageDeflate = require('./permessage-deflate');
|
||||||
|
const subprotocol = require('./subprotocol');
|
||||||
|
const WebSocket = require('./websocket');
|
||||||
|
const { CLOSE_TIMEOUT, GUID, kWebSocket } = require('./constants');
|
||||||
|
|
||||||
|
const keyRegex = /^[+/0-9A-Za-z]{22}==$/;
|
||||||
|
|
||||||
|
const RUNNING = 0;
|
||||||
|
const CLOSING = 1;
|
||||||
|
const CLOSED = 2;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class representing a WebSocket server.
|
||||||
|
*
|
||||||
|
* @extends EventEmitter
|
||||||
|
*/
|
||||||
|
class WebSocketServer extends EventEmitter {
|
||||||
|
/**
|
||||||
|
* Create a `WebSocketServer` instance.
|
||||||
|
*
|
||||||
|
* @param {Object} options Configuration options
|
||||||
|
* @param {Boolean} [options.allowSynchronousEvents=true] Specifies whether
|
||||||
|
* any of the `'message'`, `'ping'`, and `'pong'` events can be emitted
|
||||||
|
* multiple times in the same tick
|
||||||
|
* @param {Boolean} [options.autoPong=true] Specifies whether or not to
|
||||||
|
* automatically send a pong in response to a ping
|
||||||
|
* @param {Number} [options.backlog=511] The maximum length of the queue of
|
||||||
|
* pending connections
|
||||||
|
* @param {Boolean} [options.clientTracking=true] Specifies whether or not to
|
||||||
|
* track clients
|
||||||
|
* @param {Number} [options.closeTimeout=30000] Duration in milliseconds to
|
||||||
|
* wait for the closing handshake to finish after `websocket.close()` is
|
||||||
|
* called
|
||||||
|
* @param {Function} [options.handleProtocols] A hook to handle protocols
|
||||||
|
* @param {String} [options.host] The hostname where to bind the server
|
||||||
|
* @param {Number} [options.maxPayload=104857600] The maximum allowed message
|
||||||
|
* size
|
||||||
|
* @param {Boolean} [options.noServer=false] Enable no server mode
|
||||||
|
* @param {String} [options.path] Accept only connections matching this path
|
||||||
|
* @param {(Boolean|Object)} [options.perMessageDeflate=false] Enable/disable
|
||||||
|
* permessage-deflate
|
||||||
|
* @param {Number} [options.port] The port where to bind the server
|
||||||
|
* @param {(http.Server|https.Server)} [options.server] A pre-created HTTP/S
|
||||||
|
* server to use
|
||||||
|
* @param {Boolean} [options.skipUTF8Validation=false] Specifies whether or
|
||||||
|
* not to skip UTF-8 validation for text and close messages
|
||||||
|
* @param {Function} [options.verifyClient] A hook to reject connections
|
||||||
|
* @param {Function} [options.WebSocket=WebSocket] Specifies the `WebSocket`
|
||||||
|
* class to use. It must be the `WebSocket` class or class that extends it
|
||||||
|
* @param {Function} [callback] A listener for the `listening` event
|
||||||
|
*/
|
||||||
|
constructor(options, callback) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
options = {
|
||||||
|
allowSynchronousEvents: true,
|
||||||
|
autoPong: true,
|
||||||
|
maxPayload: 100 * 1024 * 1024,
|
||||||
|
skipUTF8Validation: false,
|
||||||
|
perMessageDeflate: false,
|
||||||
|
handleProtocols: null,
|
||||||
|
clientTracking: true,
|
||||||
|
closeTimeout: CLOSE_TIMEOUT,
|
||||||
|
verifyClient: null,
|
||||||
|
noServer: false,
|
||||||
|
backlog: null, // use default (511 as implemented in net.js)
|
||||||
|
server: null,
|
||||||
|
host: null,
|
||||||
|
path: null,
|
||||||
|
port: null,
|
||||||
|
WebSocket,
|
||||||
|
...options
|
||||||
|
};
|
||||||
|
|
||||||
|
if (
|
||||||
|
(options.port == null && !options.server && !options.noServer) ||
|
||||||
|
(options.port != null && (options.server || options.noServer)) ||
|
||||||
|
(options.server && options.noServer)
|
||||||
|
) {
|
||||||
|
throw new TypeError(
|
||||||
|
'One and only 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) {
|
||||||
|
const emitConnection = this.emit.bind(this, 'connection');
|
||||||
|
|
||||||
|
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, emitConnection);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.perMessageDeflate === true) options.perMessageDeflate = {};
|
||||||
|
if (options.clientTracking) {
|
||||||
|
this.clients = new Set();
|
||||||
|
this._shouldEmitClose = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.options = options;
|
||||||
|
this._state = RUNNING;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop the server from accepting new connections and emit the `'close'` event
|
||||||
|
* when all existing connections are closed.
|
||||||
|
*
|
||||||
|
* @param {Function} [cb] A one-time listener for the `'close'` event
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
close(cb) {
|
||||||
|
if (this._state === CLOSED) {
|
||||||
|
if (cb) {
|
||||||
|
this.once('close', () => {
|
||||||
|
cb(new Error('The server is not running'));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
process.nextTick(emitClose, this);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cb) this.once('close', cb);
|
||||||
|
|
||||||
|
if (this._state === CLOSING) return;
|
||||||
|
this._state = CLOSING;
|
||||||
|
|
||||||
|
if (this.options.noServer || this.options.server) {
|
||||||
|
if (this._server) {
|
||||||
|
this._removeListeners();
|
||||||
|
this._removeListeners = this._server = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.clients) {
|
||||||
|
if (!this.clients.size) {
|
||||||
|
process.nextTick(emitClose, this);
|
||||||
|
} else {
|
||||||
|
this._shouldEmitClose = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
process.nextTick(emitClose, this);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const server = this._server;
|
||||||
|
|
||||||
|
this._removeListeners();
|
||||||
|
this._removeListeners = this._server = null;
|
||||||
|
|
||||||
|
//
|
||||||
|
// The HTTP/S server was created internally. Close it, and rely on its
|
||||||
|
// `'close'` event.
|
||||||
|
//
|
||||||
|
server.close(() => {
|
||||||
|
emitClose(this);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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) {
|
||||||
|
const index = req.url.indexOf('?');
|
||||||
|
const pathname = index !== -1 ? req.url.slice(0, index) : req.url;
|
||||||
|
|
||||||
|
if (pathname !== this.options.path) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle a HTTP Upgrade request.
|
||||||
|
*
|
||||||
|
* @param {http.IncomingMessage} req The request object
|
||||||
|
* @param {Duplex} 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 key = req.headers['sec-websocket-key'];
|
||||||
|
const upgrade = req.headers.upgrade;
|
||||||
|
const version = +req.headers['sec-websocket-version'];
|
||||||
|
|
||||||
|
if (req.method !== 'GET') {
|
||||||
|
const message = 'Invalid HTTP method';
|
||||||
|
abortHandshakeOrEmitwsClientError(this, req, socket, 405, message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (upgrade === undefined || upgrade.toLowerCase() !== 'websocket') {
|
||||||
|
const message = 'Invalid Upgrade header';
|
||||||
|
abortHandshakeOrEmitwsClientError(this, req, socket, 400, message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key === undefined || !keyRegex.test(key)) {
|
||||||
|
const message = 'Missing or invalid Sec-WebSocket-Key header';
|
||||||
|
abortHandshakeOrEmitwsClientError(this, req, socket, 400, message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (version !== 13 && version !== 8) {
|
||||||
|
const message = 'Missing or invalid Sec-WebSocket-Version header';
|
||||||
|
abortHandshakeOrEmitwsClientError(this, req, socket, 400, message, {
|
||||||
|
'Sec-WebSocket-Version': '13, 8'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.shouldHandle(req)) {
|
||||||
|
abortHandshake(socket, 400);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const secWebSocketProtocol = req.headers['sec-websocket-protocol'];
|
||||||
|
let protocols = new Set();
|
||||||
|
|
||||||
|
if (secWebSocketProtocol !== undefined) {
|
||||||
|
try {
|
||||||
|
protocols = subprotocol.parse(secWebSocketProtocol);
|
||||||
|
} catch (err) {
|
||||||
|
const message = 'Invalid Sec-WebSocket-Protocol header';
|
||||||
|
abortHandshakeOrEmitwsClientError(this, req, socket, 400, message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const secWebSocketExtensions = req.headers['sec-websocket-extensions'];
|
||||||
|
const extensions = {};
|
||||||
|
|
||||||
|
if (
|
||||||
|
this.options.perMessageDeflate &&
|
||||||
|
secWebSocketExtensions !== undefined
|
||||||
|
) {
|
||||||
|
const perMessageDeflate = new PerMessageDeflate({
|
||||||
|
...this.options.perMessageDeflate,
|
||||||
|
isServer: true,
|
||||||
|
maxPayload: this.options.maxPayload
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const offers = extension.parse(secWebSocketExtensions);
|
||||||
|
|
||||||
|
if (offers[PerMessageDeflate.extensionName]) {
|
||||||
|
perMessageDeflate.accept(offers[PerMessageDeflate.extensionName]);
|
||||||
|
extensions[PerMessageDeflate.extensionName] = perMessageDeflate;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
const message =
|
||||||
|
'Invalid or unacceptable Sec-WebSocket-Extensions header';
|
||||||
|
abortHandshakeOrEmitwsClientError(this, req, socket, 400, message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Optionally call external client verification handler.
|
||||||
|
//
|
||||||
|
if (this.options.verifyClient) {
|
||||||
|
const info = {
|
||||||
|
origin:
|
||||||
|
req.headers[`${version === 8 ? 'sec-websocket-origin' : 'origin'}`],
|
||||||
|
secure: !!(req.socket.authorized || req.socket.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,
|
||||||
|
key,
|
||||||
|
protocols,
|
||||||
|
req,
|
||||||
|
socket,
|
||||||
|
head,
|
||||||
|
cb
|
||||||
|
);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.options.verifyClient(info)) return abortHandshake(socket, 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.completeUpgrade(extensions, key, protocols, req, socket, head, cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Upgrade the connection to WebSocket.
|
||||||
|
*
|
||||||
|
* @param {Object} extensions The accepted extensions
|
||||||
|
* @param {String} key The value of the `Sec-WebSocket-Key` header
|
||||||
|
* @param {Set} protocols The subprotocols
|
||||||
|
* @param {http.IncomingMessage} req The request object
|
||||||
|
* @param {Duplex} socket The network socket between the server and client
|
||||||
|
* @param {Buffer} head The first packet of the upgraded stream
|
||||||
|
* @param {Function} cb Callback
|
||||||
|
* @throws {Error} If called more than once with the same socket
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
completeUpgrade(extensions, key, protocols, req, socket, head, cb) {
|
||||||
|
//
|
||||||
|
// Destroy the socket if the client has already sent a FIN packet.
|
||||||
|
//
|
||||||
|
if (!socket.readable || !socket.writable) return socket.destroy();
|
||||||
|
|
||||||
|
if (socket[kWebSocket]) {
|
||||||
|
throw new Error(
|
||||||
|
'server.handleUpgrade() was called more than once with the same ' +
|
||||||
|
'socket, possibly due to a misconfiguration'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._state > RUNNING) return abortHandshake(socket, 503);
|
||||||
|
|
||||||
|
const digest = createHash('sha1')
|
||||||
|
.update(key + GUID)
|
||||||
|
.digest('base64');
|
||||||
|
|
||||||
|
const headers = [
|
||||||
|
'HTTP/1.1 101 Switching Protocols',
|
||||||
|
'Upgrade: websocket',
|
||||||
|
'Connection: Upgrade',
|
||||||
|
`Sec-WebSocket-Accept: ${digest}`
|
||||||
|
];
|
||||||
|
|
||||||
|
const ws = new this.options.WebSocket(null, undefined, this.options);
|
||||||
|
|
||||||
|
if (protocols.size) {
|
||||||
|
//
|
||||||
|
// Optionally call external protocol selection handler.
|
||||||
|
//
|
||||||
|
const protocol = this.options.handleProtocols
|
||||||
|
? this.options.handleProtocols(protocols, req)
|
||||||
|
: protocols.values().next().value;
|
||||||
|
|
||||||
|
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, {
|
||||||
|
allowSynchronousEvents: this.options.allowSynchronousEvents,
|
||||||
|
maxPayload: this.options.maxPayload,
|
||||||
|
skipUTF8Validation: this.options.skipUTF8Validation
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.clients) {
|
||||||
|
this.clients.add(ws);
|
||||||
|
ws.on('close', () => {
|
||||||
|
this.clients.delete(ws);
|
||||||
|
|
||||||
|
if (this._shouldEmitClose && !this.clients.size) {
|
||||||
|
process.nextTick(emitClose, this);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
cb(ws, req);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = WebSocketServer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add event listeners on an `EventEmitter` using a map of <event, listener>
|
||||||
|
* pairs.
|
||||||
|
*
|
||||||
|
* @param {EventEmitter} server The event emitter
|
||||||
|
* @param {Object.<String, Function>} 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]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emit a `'close'` event on an `EventEmitter`.
|
||||||
|
*
|
||||||
|
* @param {EventEmitter} server The event emitter
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
function emitClose(server) {
|
||||||
|
server._state = CLOSED;
|
||||||
|
server.emit('close');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle socket errors.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
function socketOnError() {
|
||||||
|
this.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close the connection when preconditions are not fulfilled.
|
||||||
|
*
|
||||||
|
* @param {Duplex} 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) {
|
||||||
|
//
|
||||||
|
// The socket is writable unless the user destroyed or ended it before calling
|
||||||
|
// `server.handleUpgrade()` or in the `verifyClient` function, which is a user
|
||||||
|
// error. Handling this does not make much sense as the worst that can happen
|
||||||
|
// is that some of the data written by the user might be discarded due to the
|
||||||
|
// call to `socket.end()` below, which triggers an `'error'` event that in
|
||||||
|
// turn causes the socket to be destroyed.
|
||||||
|
//
|
||||||
|
message = message || http.STATUS_CODES[code];
|
||||||
|
headers = {
|
||||||
|
Connection: 'close',
|
||||||
|
'Content-Type': 'text/html',
|
||||||
|
'Content-Length': Buffer.byteLength(message),
|
||||||
|
...headers
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.once('finish', socket.destroy);
|
||||||
|
|
||||||
|
socket.end(
|
||||||
|
`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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emit a `'wsClientError'` event on a `WebSocketServer` if there is at least
|
||||||
|
* one listener for it, otherwise call `abortHandshake()`.
|
||||||
|
*
|
||||||
|
* @param {WebSocketServer} server The WebSocket server
|
||||||
|
* @param {http.IncomingMessage} req The request object
|
||||||
|
* @param {Duplex} 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] The HTTP response headers
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
function abortHandshakeOrEmitwsClientError(
|
||||||
|
server,
|
||||||
|
req,
|
||||||
|
socket,
|
||||||
|
code,
|
||||||
|
message,
|
||||||
|
headers
|
||||||
|
) {
|
||||||
|
if (server.listenerCount('wsClientError')) {
|
||||||
|
const err = new Error(message);
|
||||||
|
Error.captureStackTrace(err, abortHandshakeOrEmitwsClientError);
|
||||||
|
|
||||||
|
server.emit('wsClientError', err, socket, req);
|
||||||
|
} else {
|
||||||
|
abortHandshake(socket, code, message, headers);
|
||||||
|
}
|
||||||
|
}
|
||||||
1393
node_modules/ws/lib/websocket.js
generated
vendored
Normal file
1393
node_modules/ws/lib/websocket.js
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
70
node_modules/ws/package.json
generated
vendored
Normal file
70
node_modules/ws/package.json
generated
vendored
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
{
|
||||||
|
"name": "ws",
|
||||||
|
"version": "8.20.0",
|
||||||
|
"description": "Simple to use, blazing fast and thoroughly tested websocket client and server for Node.js",
|
||||||
|
"keywords": [
|
||||||
|
"HyBi",
|
||||||
|
"Push",
|
||||||
|
"RFC-6455",
|
||||||
|
"WebSocket",
|
||||||
|
"WebSockets",
|
||||||
|
"real-time"
|
||||||
|
],
|
||||||
|
"homepage": "https://github.com/websockets/ws",
|
||||||
|
"bugs": "https://github.com/websockets/ws/issues",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/websockets/ws.git"
|
||||||
|
},
|
||||||
|
"author": "Einar Otto Stangvik <einaros@gmail.com> (http://2x.io)",
|
||||||
|
"license": "MIT",
|
||||||
|
"main": "index.js",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"browser": "./browser.js",
|
||||||
|
"import": "./wrapper.mjs",
|
||||||
|
"require": "./index.js"
|
||||||
|
},
|
||||||
|
"./package.json": "./package.json"
|
||||||
|
},
|
||||||
|
"browser": "browser.js",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"browser.js",
|
||||||
|
"index.js",
|
||||||
|
"lib/*.js",
|
||||||
|
"wrapper.mjs"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"test": "nyc --reporter=lcov --reporter=text mocha --throw-deprecation test/*.test.js",
|
||||||
|
"integration": "mocha --throw-deprecation test/*.integration.js",
|
||||||
|
"lint": "eslint . && prettier --check --ignore-path .gitignore \"**/*.{json,md,yaml,yml}\""
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"bufferutil": "^4.0.1",
|
||||||
|
"utf-8-validate": ">=5.0.2"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"bufferutil": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"utf-8-validate": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^10.0.1",
|
||||||
|
"benchmark": "^2.1.4",
|
||||||
|
"bufferutil": "^4.0.1",
|
||||||
|
"eslint": "^10.0.1",
|
||||||
|
"eslint-config-prettier": "^10.0.1",
|
||||||
|
"eslint-plugin-prettier": "^5.0.0",
|
||||||
|
"globals": "^17.0.0",
|
||||||
|
"mocha": "^8.4.0",
|
||||||
|
"nyc": "^15.0.0",
|
||||||
|
"prettier": "^3.0.0",
|
||||||
|
"utf-8-validate": "^6.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
21
node_modules/ws/wrapper.mjs
generated
vendored
Normal file
21
node_modules/ws/wrapper.mjs
generated
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import createWebSocketStream from './lib/stream.js';
|
||||||
|
import extension from './lib/extension.js';
|
||||||
|
import PerMessageDeflate from './lib/permessage-deflate.js';
|
||||||
|
import Receiver from './lib/receiver.js';
|
||||||
|
import Sender from './lib/sender.js';
|
||||||
|
import subprotocol from './lib/subprotocol.js';
|
||||||
|
import WebSocket from './lib/websocket.js';
|
||||||
|
import WebSocketServer from './lib/websocket-server.js';
|
||||||
|
|
||||||
|
export {
|
||||||
|
createWebSocketStream,
|
||||||
|
extension,
|
||||||
|
PerMessageDeflate,
|
||||||
|
Receiver,
|
||||||
|
Sender,
|
||||||
|
subprotocol,
|
||||||
|
WebSocket,
|
||||||
|
WebSocketServer
|
||||||
|
};
|
||||||
|
|
||||||
|
export default WebSocket;
|
||||||
24
package-lock.json
generated
24
package-lock.json
generated
@@ -11,7 +11,8 @@
|
|||||||
"dotenv": "^17.3.1",
|
"dotenv": "^17.3.1",
|
||||||
"express": "^4.21.2",
|
"express": "^4.21.2",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"pg": "^8.13.1"
|
"pg": "^8.13.1",
|
||||||
|
"ws": "^8.20.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/accepts": {
|
"node_modules/accepts": {
|
||||||
@@ -1125,6 +1126,27 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ws": {
|
||||||
|
"version": "8.20.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz",
|
||||||
|
"integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"bufferutil": "^4.0.1",
|
||||||
|
"utf-8-validate": ">=5.0.2"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"bufferutil": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"utf-8-validate": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/xtend": {
|
"node_modules/xtend": {
|
||||||
"version": "4.0.2",
|
"version": "4.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
"dotenv": "^17.3.1",
|
"dotenv": "^17.3.1",
|
||||||
"express": "^4.21.2",
|
"express": "^4.21.2",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"pg": "^8.13.1"
|
"pg": "^8.13.1",
|
||||||
|
"ws": "^8.20.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,10 @@
|
|||||||
export async function apiFetchConfig(team) {
|
export async function apiFetchConfig(team) {
|
||||||
const token = localStorage.getItem("authToken");
|
const token = localStorage.getItem("authToken");
|
||||||
const headers = token ? { Authorization: `Bearer ${token}` } : {};
|
const headers = token ? { Authorization: `Bearer ${token}` } : {};
|
||||||
const res = await fetch(`/api/config?team=${encodeURIComponent(team)}`, { headers });
|
const res = await fetch(`/api/config?team=${encodeURIComponent(team)}`, {
|
||||||
|
headers,
|
||||||
|
cache: "no-store",
|
||||||
|
});
|
||||||
if (!res.ok) throw new Error("config_fetch_failed");
|
if (!res.ok) throw new Error("config_fetch_failed");
|
||||||
return res.json();
|
return res.json();
|
||||||
}
|
}
|
||||||
@@ -14,7 +17,10 @@ export async function apiFetchConfig(team) {
|
|||||||
export async function apiFetchGrid(seed) {
|
export async function apiFetchGrid(seed) {
|
||||||
const token = localStorage.getItem("authToken");
|
const token = localStorage.getItem("authToken");
|
||||||
const headers = token ? { Authorization: `Bearer ${token}` } : {};
|
const headers = token ? { Authorization: `Bearer ${token}` } : {};
|
||||||
return fetch(`/api/grid/${encodeURIComponent(seed)}`, { headers });
|
return fetch(`/api/grid/${encodeURIComponent(seed)}`, {
|
||||||
|
headers,
|
||||||
|
cache: "no-store",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Returns the raw Response so the caller can inspect status codes (409, 410, etc.). */
|
/** Returns the raw Response so the caller can inspect status codes (409, 410, etc.). */
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ export function applyUser(user, token) {
|
|||||||
const teamColor = user.team === "blue" ? "rgba(90,200,255,0.9)" : "rgba(220,75,85,0.9)";
|
const teamColor = user.team === "blue" ? "rgba(90,200,255,0.9)" : "rgba(220,75,85,0.9)";
|
||||||
userDisplayEl.innerHTML = `<span style="color:${teamColor}">${user.username}</span>`;
|
userDisplayEl.innerHTML = `<span style="color:${teamColor}">${user.username}</span>`;
|
||||||
logoutBtn.classList.remove("hidden");
|
logoutBtn.classList.remove("hidden");
|
||||||
|
window.dispatchEvent(new CustomEvent("auth:changed"));
|
||||||
}
|
}
|
||||||
|
|
||||||
function logout() {
|
function logout() {
|
||||||
@@ -51,6 +52,7 @@ function logout() {
|
|||||||
userDisplayEl.textContent = "—";
|
userDisplayEl.textContent = "—";
|
||||||
logoutBtn.classList.add("hidden");
|
logoutBtn.classList.add("hidden");
|
||||||
showAuthOverlay();
|
showAuthOverlay();
|
||||||
|
window.dispatchEvent(new CustomEvent("auth:changed"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Session restore ───────────────────────────────────────────────────────────
|
// ── Session restore ───────────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -314,20 +314,28 @@ export function updateResetCountdown() {
|
|||||||
export async function loadVictoryPoints() {
|
export async function loadVictoryPoints() {
|
||||||
try {
|
try {
|
||||||
const { blue, red } = await apiFetchVictoryPoints();
|
const { blue, red } = await apiFetchVictoryPoints();
|
||||||
if (vpBlueEl) vpBlueEl.textContent = String(blue ?? 0);
|
setVictoryPointsDisplay(blue ?? 0, red ?? 0);
|
||||||
if (vpRedEl) vpRedEl.textContent = String(red ?? 0);
|
|
||||||
} catch { /* ignore */ }
|
} catch { /* ignore */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchAndApplyActivePlayers() {
|
export async function fetchAndApplyActivePlayers() {
|
||||||
try {
|
try {
|
||||||
const { blue, red } = await apiFetchActivePlayers();
|
const { blue, red } = await apiFetchActivePlayers();
|
||||||
const fmt = (n) => `${n} joueur${n > 1 ? "s" : ""}`;
|
setActivePlayersDisplay(blue ?? 0, red ?? 0);
|
||||||
if (activeCountBlueEl) activeCountBlueEl.textContent = fmt(blue ?? 0);
|
|
||||||
if (activeCountRedEl) activeCountRedEl.textContent = fmt(red ?? 0);
|
|
||||||
} catch { /* ignore */ }
|
} catch { /* ignore */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setVictoryPointsDisplay(blue, red) {
|
||||||
|
if (vpBlueEl) vpBlueEl.textContent = String(blue ?? 0);
|
||||||
|
if (vpRedEl) vpRedEl.textContent = String(red ?? 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setActivePlayersDisplay(blue, red) {
|
||||||
|
const fmt = (n) => `${n} joueur${n > 1 ? "s" : ""}`;
|
||||||
|
if (activeCountBlueEl) activeCountBlueEl.textContent = fmt(blue ?? 0);
|
||||||
|
if (activeCountRedEl) activeCountRedEl.textContent = fmt(red ?? 0);
|
||||||
|
}
|
||||||
|
|
||||||
// ── Player list popup (click on joueur count) ─────────────────────────────────
|
// ── Player list popup (click on joueur count) ─────────────────────────────────
|
||||||
|
|
||||||
function closePlayerListPopup() {
|
function closePlayerListPopup() {
|
||||||
@@ -400,9 +408,7 @@ let milDeductRed = 0;
|
|||||||
export async function loadMilitaryDeductions() {
|
export async function loadMilitaryDeductions() {
|
||||||
try {
|
try {
|
||||||
const { blue, red } = await apiFetchMilitaryDeductions();
|
const { blue, red } = await apiFetchMilitaryDeductions();
|
||||||
milDeductBlue = blue ?? 0;
|
applyMilitaryDeductionsUpdate({ blue, red });
|
||||||
milDeductRed = red ?? 0;
|
|
||||||
updateEconomyDisplay();
|
|
||||||
} catch { /* ignore */ }
|
} catch { /* ignore */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -505,6 +511,85 @@ export function updateEconomyDisplay() {
|
|||||||
let econScoreBlue = 0;
|
let econScoreBlue = 0;
|
||||||
let econScoreRed = 0;
|
let econScoreRed = 0;
|
||||||
|
|
||||||
|
export function applyMilitaryDeductionsUpdate(deductions) {
|
||||||
|
if (!deductions || typeof deductions !== "object") return;
|
||||||
|
if (deductions.blue !== undefined && deductions.blue !== null) {
|
||||||
|
milDeductBlue = Number(deductions.blue);
|
||||||
|
}
|
||||||
|
if (deductions.red !== undefined && deductions.red !== null) {
|
||||||
|
milDeductRed = Number(deductions.red);
|
||||||
|
}
|
||||||
|
updateEconomyDisplay();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function applyTeamQuotaUpdate(team, actionsRemainingValue) {
|
||||||
|
if (team !== currentTeam) return;
|
||||||
|
if (actionsRemainingValue === null || actionsRemainingValue === undefined) return;
|
||||||
|
teamActionsRemaining = Number(actionsRemainingValue);
|
||||||
|
updateTeamQuotaDisplay();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function applyCellUpdate(cell) {
|
||||||
|
if (!cell || typeof cell !== "object") return;
|
||||||
|
const x = Number(cell.x);
|
||||||
|
const y = Number(cell.y);
|
||||||
|
if (!Number.isInteger(x) || !Number.isInteger(y)) return;
|
||||||
|
|
||||||
|
const key = cellKey(x, y);
|
||||||
|
const hadCell = cells.has(key);
|
||||||
|
cells.set(key, {
|
||||||
|
controlledBy: cell.discoveredBy ?? cell.discovered_by ?? null,
|
||||||
|
hasPlanet: Boolean(cell.hasPlanet ?? cell.has_planet),
|
||||||
|
planet: cell.planet ?? cell.planet_json ?? null,
|
||||||
|
capturedBy: cell.capturedBy ?? cell.captured_by ?? null,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!hadCell) {
|
||||||
|
markTileReveal(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateEconomyDisplay();
|
||||||
|
draw();
|
||||||
|
refreshCursorFromLast();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function applyRealtimeSnapshot(snapshot) {
|
||||||
|
if (!snapshot || typeof snapshot !== "object") return;
|
||||||
|
let shouldUpdateEconomy = false;
|
||||||
|
|
||||||
|
if (snapshot.scores && typeof snapshot.scores === "object") {
|
||||||
|
econScoreBlue = Number(snapshot.scores.blue ?? econScoreBlue);
|
||||||
|
econScoreRed = Number(snapshot.scores.red ?? econScoreRed);
|
||||||
|
if (econScoreBlueEl) econScoreBlueEl.textContent = econScoreBlue.toFixed(3);
|
||||||
|
if (econScoreRedEl) econScoreRedEl.textContent = econScoreRed.toFixed(3);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (snapshot.elementBonus && typeof snapshot.elementBonus === "object") {
|
||||||
|
elemBonusBlue = Number(snapshot.elementBonus.blue ?? elemBonusBlue);
|
||||||
|
elemBonusRed = Number(snapshot.elementBonus.red ?? elemBonusRed);
|
||||||
|
updateEffectiveCooldownDisplay();
|
||||||
|
shouldUpdateEconomy = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (snapshot.militaryDeductions && typeof snapshot.militaryDeductions === "object") {
|
||||||
|
milDeductBlue = Number(snapshot.militaryDeductions.blue ?? milDeductBlue);
|
||||||
|
milDeductRed = Number(snapshot.militaryDeductions.red ?? milDeductRed);
|
||||||
|
shouldUpdateEconomy = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (snapshot.activePlayers && typeof snapshot.activePlayers === "object") {
|
||||||
|
setActivePlayersDisplay(snapshot.activePlayers.blue ?? 0, snapshot.activePlayers.red ?? 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (snapshot.victoryPoints && typeof snapshot.victoryPoints === "object") {
|
||||||
|
setVictoryPointsDisplay(snapshot.victoryPoints.blue ?? 0, snapshot.victoryPoints.red ?? 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldUpdateEconomy) {
|
||||||
|
updateEconomyDisplay();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Trigger the delta fade animation on an element. */
|
/** Trigger the delta fade animation on an element. */
|
||||||
function showEconDelta(el, delta) {
|
function showEconDelta(el, delta) {
|
||||||
if (!el || delta <= 0) return;
|
if (!el || delta <= 0) return;
|
||||||
@@ -580,24 +665,56 @@ export async function fetchConfig() {
|
|||||||
|
|
||||||
// ── Grid fetch ────────────────────────────────────────────────────────────────
|
// ── Grid fetch ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export async function fetchGridForSeed(seed, depth = 0) {
|
export async function fetchGridForSeed(seed) {
|
||||||
if (depth > 4) throw new Error("grid_stale");
|
let currentSeed = seed;
|
||||||
const res = await apiFetchGrid(seed);
|
const seenSeeds = new Set();
|
||||||
if (res.status === 410) {
|
|
||||||
await fetchConfig();
|
for (let attempt = 0; attempt < 5; attempt++) {
|
||||||
return fetchGridForSeed(seedStr, depth + 1);
|
const res = await apiFetchGrid(currentSeed);
|
||||||
}
|
|
||||||
if (!res.ok) throw new Error("grid");
|
if (res.status === 410) {
|
||||||
const data = await res.json();
|
let nextSeed = "";
|
||||||
cells.clear();
|
try {
|
||||||
for (const row of data.cells || []) {
|
const stale = await res.json();
|
||||||
cells.set(cellKey(row.x, row.y), {
|
if (typeof stale?.worldSeed === "string" && stale.worldSeed) {
|
||||||
controlledBy: row.discovered_by ?? row.discoveredBy ?? null,
|
nextSeed = stale.worldSeed;
|
||||||
hasPlanet: Boolean(row.has_planet),
|
}
|
||||||
planet: row.planet_json ?? null,
|
} catch {
|
||||||
capturedBy: row.captured_by ?? row.capturedBy ?? null,
|
// ignore parse errors, fallback to /api/config
|
||||||
});
|
}
|
||||||
|
|
||||||
|
seenSeeds.add(currentSeed);
|
||||||
|
|
||||||
|
if (nextSeed) {
|
||||||
|
if (seenSeeds.has(nextSeed)) break;
|
||||||
|
GAME_CONFIG.worldSeed = nextSeed;
|
||||||
|
applySeed(nextSeed);
|
||||||
|
currentSeed = nextSeed;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
await fetchConfig();
|
||||||
|
currentSeed = seedStr;
|
||||||
|
if (seenSeeds.has(currentSeed)) break;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!res.ok) throw new Error("grid");
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
cells.clear();
|
||||||
|
for (const row of data.cells || []) {
|
||||||
|
cells.set(cellKey(row.x, row.y), {
|
||||||
|
controlledBy: row.discovered_by ?? row.discoveredBy ?? null,
|
||||||
|
hasPlanet: Boolean(row.has_planet),
|
||||||
|
planet: row.planet_json ?? null,
|
||||||
|
capturedBy: row.captured_by ?? row.capturedBy ?? null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
throw new Error("grid_stale");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Action quota ──────────────────────────────────────────────────────────────
|
// ── Action quota ──────────────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -11,6 +11,10 @@ import {
|
|||||||
loadDbInfo,
|
loadDbInfo,
|
||||||
loadElementBonus,
|
loadElementBonus,
|
||||||
loadMilitaryDeductions,
|
loadMilitaryDeductions,
|
||||||
|
applyRealtimeSnapshot,
|
||||||
|
applyCellUpdate,
|
||||||
|
applyTeamQuotaUpdate,
|
||||||
|
applyMilitaryDeductionsUpdate,
|
||||||
refreshFromServer,
|
refreshFromServer,
|
||||||
refreshGridDisplay,
|
refreshGridDisplay,
|
||||||
loadPlayfieldMask,
|
loadPlayfieldMask,
|
||||||
@@ -23,6 +27,11 @@ import {
|
|||||||
hideAuthOverlay,
|
hideAuthOverlay,
|
||||||
} from "./auth.js";
|
} from "./auth.js";
|
||||||
|
|
||||||
|
import {
|
||||||
|
startRealtime,
|
||||||
|
restartRealtime,
|
||||||
|
} from "./realtime.js";
|
||||||
|
|
||||||
// ── DOM refs ──────────────────────────────────────────────────────────────────
|
// ── DOM refs ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
const hint = document.getElementById("hint");
|
const hint = document.getElementById("hint");
|
||||||
@@ -36,8 +45,26 @@ const infoColumn = document.getElementById("infoColumn");
|
|||||||
let configPollTimer = 0;
|
let configPollTimer = 0;
|
||||||
let scorePollTimer = 0;
|
let scorePollTimer = 0;
|
||||||
let resetTimer = null;
|
let resetTimer = null;
|
||||||
|
let victoryPollTimer = 0;
|
||||||
|
let gridPollTimer = 0;
|
||||||
|
let fallbackPollingEnabled = false;
|
||||||
|
let syncInFlight = false;
|
||||||
|
|
||||||
|
function clearFallbackTimers() {
|
||||||
|
clearTimeout(configPollTimer);
|
||||||
|
clearTimeout(scorePollTimer);
|
||||||
|
if (victoryPollTimer) {
|
||||||
|
clearInterval(victoryPollTimer);
|
||||||
|
victoryPollTimer = 0;
|
||||||
|
}
|
||||||
|
if (gridPollTimer) {
|
||||||
|
clearInterval(gridPollTimer);
|
||||||
|
gridPollTimer = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function scheduleConfigPoll() {
|
function scheduleConfigPoll() {
|
||||||
|
if (!fallbackPollingEnabled) return;
|
||||||
clearTimeout(configPollTimer);
|
clearTimeout(configPollTimer);
|
||||||
const ms = Math.max(5_000, GAME_CONFIG.configReloadIntervalSeconds * 1_000);
|
const ms = Math.max(5_000, GAME_CONFIG.configReloadIntervalSeconds * 1_000);
|
||||||
configPollTimer = window.setTimeout(async () => {
|
configPollTimer = window.setTimeout(async () => {
|
||||||
@@ -52,6 +79,7 @@ function scheduleConfigPoll() {
|
|||||||
const ECON_TICK_SECONDS = 5;
|
const ECON_TICK_SECONDS = 5;
|
||||||
|
|
||||||
function scheduleScorePoll() {
|
function scheduleScorePoll() {
|
||||||
|
if (!fallbackPollingEnabled) return;
|
||||||
clearTimeout(scorePollTimer);
|
clearTimeout(scorePollTimer);
|
||||||
scorePollTimer = window.setTimeout(async () => {
|
scorePollTimer = window.setTimeout(async () => {
|
||||||
await fetchAndApplyActivePlayers();
|
await fetchAndApplyActivePlayers();
|
||||||
@@ -62,6 +90,74 @@ function scheduleScorePoll() {
|
|||||||
}, ECON_TICK_SECONDS * 1_000);
|
}, ECON_TICK_SECONDS * 1_000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function startFallbackPolling() {
|
||||||
|
if (fallbackPollingEnabled) return;
|
||||||
|
fallbackPollingEnabled = true;
|
||||||
|
scheduleConfigPoll();
|
||||||
|
scheduleScorePoll();
|
||||||
|
victoryPollTimer = setInterval(loadVictoryPoints, 30_000);
|
||||||
|
gridPollTimer = setInterval(refreshGridDisplay, 1_000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopFallbackPolling() {
|
||||||
|
fallbackPollingEnabled = false;
|
||||||
|
clearFallbackTimers();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function syncFromServer() {
|
||||||
|
if (syncInFlight) return;
|
||||||
|
syncInFlight = true;
|
||||||
|
try {
|
||||||
|
await refreshFromServer();
|
||||||
|
await loadVictoryPoints();
|
||||||
|
} finally {
|
||||||
|
syncInFlight = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleRealtimeMessage(message) {
|
||||||
|
if (!message || typeof message !== "object") return;
|
||||||
|
const worldSeed = typeof message.worldSeed === "string" ? message.worldSeed : null;
|
||||||
|
|
||||||
|
if (worldSeed && seedStr && worldSeed !== seedStr) {
|
||||||
|
syncFromServer();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (message.type) {
|
||||||
|
case "snapshot":
|
||||||
|
applyRealtimeSnapshot(message);
|
||||||
|
break;
|
||||||
|
case "cell-updated":
|
||||||
|
if (message.cell) applyCellUpdate(message.cell);
|
||||||
|
break;
|
||||||
|
case "team-quota-updated":
|
||||||
|
applyTeamQuotaUpdate(message.team, message.actionsRemaining);
|
||||||
|
break;
|
||||||
|
case "military-deductions-updated":
|
||||||
|
applyMilitaryDeductionsUpdate(message.deductions);
|
||||||
|
break;
|
||||||
|
case "config-updated":
|
||||||
|
case "seed-changed":
|
||||||
|
syncFromServer();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function startRealtimeFlow() {
|
||||||
|
startRealtime({
|
||||||
|
onOpen: () => {
|
||||||
|
stopFallbackPolling();
|
||||||
|
},
|
||||||
|
onClose: () => {
|
||||||
|
startFallbackPolling();
|
||||||
|
},
|
||||||
|
onMessage: handleRealtimeMessage,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// ── Burger / mobile menu ──────────────────────────────────────────────────────
|
// ── Burger / mobile menu ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
function openMenu() {
|
function openMenu() {
|
||||||
@@ -101,7 +197,13 @@ async function boot() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await fetchConfig();
|
await fetchConfig();
|
||||||
await fetchGridForSeed(seedStr);
|
try {
|
||||||
|
await fetchGridForSeed(seedStr);
|
||||||
|
} catch (e) {
|
||||||
|
if (e?.message !== "grid_stale") throw e;
|
||||||
|
await fetchConfig();
|
||||||
|
await fetchGridForSeed(seedStr);
|
||||||
|
}
|
||||||
await fetchAndApplyActivePlayers();
|
await fetchAndApplyActivePlayers();
|
||||||
await loadEconScores();
|
await loadEconScores();
|
||||||
await loadVictoryPoints();
|
await loadVictoryPoints();
|
||||||
@@ -120,14 +222,11 @@ async function boot() {
|
|||||||
resetTimer = setInterval(updateResetCountdown, 1_000);
|
resetTimer = setInterval(updateResetCountdown, 1_000);
|
||||||
updateResetCountdown();
|
updateResetCountdown();
|
||||||
|
|
||||||
scheduleConfigPoll();
|
startRealtimeFlow();
|
||||||
scheduleScorePoll();
|
|
||||||
|
|
||||||
// Refresh VP every 30 s so new awards are reflected promptly
|
window.addEventListener("auth:changed", () => {
|
||||||
setInterval(loadVictoryPoints, 30_000);
|
restartRealtime();
|
||||||
|
});
|
||||||
// Refresh grid every second so all clients see new tiles promptly
|
|
||||||
setInterval(refreshGridDisplay, 1_000);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Start ─────────────────────────────────────────────────────────────────────
|
// ── Start ─────────────────────────────────────────────────────────────────────
|
||||||
|
|||||||
106
public/src/realtime.js
Normal file
106
public/src/realtime.js
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
let socket = null;
|
||||||
|
let reconnectTimer = 0;
|
||||||
|
let reconnectAttempt = 0;
|
||||||
|
let stopped = false;
|
||||||
|
let handlers = null;
|
||||||
|
|
||||||
|
function clearReconnectTimer() {
|
||||||
|
if (reconnectTimer) {
|
||||||
|
clearTimeout(reconnectTimer);
|
||||||
|
reconnectTimer = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildSocketUrl() {
|
||||||
|
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
||||||
|
return `${protocol}//${window.location.host}/ws`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendAuthHandshake() {
|
||||||
|
if (!socket || socket.readyState !== WebSocket.OPEN) return;
|
||||||
|
const token = localStorage.getItem("authToken");
|
||||||
|
if (!token) return;
|
||||||
|
socket.send(JSON.stringify({
|
||||||
|
type: "auth",
|
||||||
|
token,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleReconnect() {
|
||||||
|
if (stopped) return;
|
||||||
|
clearReconnectTimer();
|
||||||
|
const delay = Math.min(10_000, 1_000 * 2 ** reconnectAttempt);
|
||||||
|
reconnectAttempt += 1;
|
||||||
|
reconnectTimer = window.setTimeout(() => {
|
||||||
|
connect();
|
||||||
|
}, delay);
|
||||||
|
}
|
||||||
|
|
||||||
|
function connect() {
|
||||||
|
if (stopped) return;
|
||||||
|
clearReconnectTimer();
|
||||||
|
|
||||||
|
if (socket && (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
socket = new WebSocket(buildSocketUrl());
|
||||||
|
|
||||||
|
socket.addEventListener("open", () => {
|
||||||
|
reconnectAttempt = 0;
|
||||||
|
sendAuthHandshake();
|
||||||
|
handlers?.onOpen?.();
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.addEventListener("message", (event) => {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
handlers?.onMessage?.(data);
|
||||||
|
} catch {
|
||||||
|
// ignore invalid payloads
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.addEventListener("close", () => {
|
||||||
|
handlers?.onClose?.();
|
||||||
|
scheduleReconnect();
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.addEventListener("error", () => {
|
||||||
|
// Let close handler trigger reconnect.
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function startRealtime(nextHandlers = {}) {
|
||||||
|
handlers = nextHandlers;
|
||||||
|
stopped = false;
|
||||||
|
connect();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function restartRealtime() {
|
||||||
|
if (stopped) return;
|
||||||
|
if (socket) {
|
||||||
|
try {
|
||||||
|
socket.close();
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
connect();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function stopRealtime() {
|
||||||
|
stopped = true;
|
||||||
|
clearReconnectTimer();
|
||||||
|
if (!socket) return;
|
||||||
|
try {
|
||||||
|
socket.close();
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
socket = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isRealtimeConnected() {
|
||||||
|
return Boolean(socket && socket.readyState === WebSocket.OPEN);
|
||||||
|
}
|
||||||
@@ -11,7 +11,18 @@ const publicDir = path.join(__dirname, "..", "public");
|
|||||||
const app = express();
|
const app = express();
|
||||||
app.use(cors({ origin: process.env.CORS_ORIGIN ?? "*" }));
|
app.use(cors({ origin: process.env.CORS_ORIGIN ?? "*" }));
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
app.use(express.static(publicDir));
|
app.use(express.static(publicDir, {
|
||||||
|
etag: false,
|
||||||
|
lastModified: false,
|
||||||
|
setHeaders: (res) => {
|
||||||
|
res.set("Cache-Control", "no-store");
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
app.use("/api", (_req, res, next) => {
|
||||||
|
res.set("Cache-Control", "no-store");
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
app.use("/api/auth", authRouter);
|
app.use("/api/auth", authRouter);
|
||||||
app.use("/api", gameRouter);
|
app.use("/api", gameRouter);
|
||||||
|
|||||||
@@ -6,8 +6,11 @@ import {
|
|||||||
setElementBonus,
|
setElementBonus,
|
||||||
} from "./db/gameDb.js";
|
} from "./db/gameDb.js";
|
||||||
import { computeTeamIncome, computeTeamElementBonus } from "./helpers/economy.js";
|
import { computeTeamIncome, computeTeamElementBonus } from "./helpers/economy.js";
|
||||||
|
import { buildRealtimeSnapshot } from "./realtimeSnapshot.js";
|
||||||
|
import { broadcast } from "./ws/hub.js";
|
||||||
|
|
||||||
const TICK_SECONDS = 5;
|
const TICK_SECONDS = 5;
|
||||||
|
let lastTickSeed = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Starts the server-side economy tick loop.
|
* Starts the server-side economy tick loop.
|
||||||
@@ -19,6 +22,11 @@ export function startEconTick() {
|
|||||||
setInterval(async () => {
|
setInterval(async () => {
|
||||||
try {
|
try {
|
||||||
const worldSeed = await ensureSeedEpoch();
|
const worldSeed = await ensureSeedEpoch();
|
||||||
|
if (lastTickSeed && lastTickSeed !== worldSeed) {
|
||||||
|
broadcast("seed-changed", { worldSeed });
|
||||||
|
}
|
||||||
|
lastTickSeed = worldSeed;
|
||||||
|
|
||||||
const rows = await getGridCells(worldSeed);
|
const rows = await getGridCells(worldSeed);
|
||||||
const cfg = getConfig();
|
const cfg = getConfig();
|
||||||
|
|
||||||
@@ -41,6 +49,9 @@ export function startEconTick() {
|
|||||||
|
|
||||||
await setElementBonus(worldSeed, "blue", blueBonus);
|
await setElementBonus(worldSeed, "blue", blueBonus);
|
||||||
await setElementBonus(worldSeed, "red", redBonus);
|
await setElementBonus(worldSeed, "red", redBonus);
|
||||||
|
|
||||||
|
const snapshot = await buildRealtimeSnapshot(worldSeed);
|
||||||
|
broadcast("snapshot", snapshot);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("[econ tick]", e.message);
|
console.error("[econ tick]", e.message);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,27 @@
|
|||||||
import "dotenv/config";
|
import "dotenv/config";
|
||||||
|
import { createServer } from "http";
|
||||||
import { loadConfigFile, getConfig } from "./configLoader.js";
|
import { loadConfigFile, getConfig } from "./configLoader.js";
|
||||||
import { initGameSchema, ensureSeedEpoch } from "./db/gameDb.js";
|
import { initGameSchema, ensureSeedEpoch } from "./db/gameDb.js";
|
||||||
import { initUsersSchema } from "./db/usersDb.js";
|
import { initUsersSchema } from "./db/usersDb.js";
|
||||||
import app from "./app.js";
|
import app from "./app.js";
|
||||||
import { startEconTick } from "./econTick.js";
|
import { startEconTick } from "./econTick.js";
|
||||||
|
import { initWebSocketHub, broadcast } from "./ws/hub.js";
|
||||||
|
|
||||||
const PORT = Number(process.env.PORT ?? 8080);
|
const PORT = Number(process.env.PORT ?? 8080);
|
||||||
|
let lastConfigSignature = "";
|
||||||
|
|
||||||
|
function makeConfigSignature(cfg) {
|
||||||
|
return JSON.stringify({
|
||||||
|
dailyActionQuota: cfg.dailyActionQuota,
|
||||||
|
teamActionQuota: cfg.teamActionQuota,
|
||||||
|
databaseWipeoutIntervalSeconds: cfg.databaseWipeoutIntervalSeconds,
|
||||||
|
debugModeForTeams: cfg.debugModeForTeams,
|
||||||
|
configReloadIntervalSeconds: cfg.configReloadIntervalSeconds,
|
||||||
|
elementWorth: cfg.elementWorth ?? {},
|
||||||
|
resourceWorth: cfg.resourceWorth ?? { common: {}, rare: {} },
|
||||||
|
militaryPower: cfg.militaryPower ?? {},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// ── Config-file poll ──────────────────────────────────────────────────────────
|
// ── Config-file poll ──────────────────────────────────────────────────────────
|
||||||
// Periodically re-reads game.settings.json and checks for a seed-epoch change.
|
// Periodically re-reads game.settings.json and checks for a seed-epoch change.
|
||||||
@@ -15,8 +31,27 @@ function scheduleConfigPoll() {
|
|||||||
const ms = Math.max(5_000, getConfig().configReloadIntervalSeconds * 1_000);
|
const ms = Math.max(5_000, getConfig().configReloadIntervalSeconds * 1_000);
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
try {
|
try {
|
||||||
|
const beforeSig = lastConfigSignature;
|
||||||
loadConfigFile();
|
loadConfigFile();
|
||||||
await ensureSeedEpoch();
|
const worldSeed = await ensureSeedEpoch();
|
||||||
|
const cfg = getConfig();
|
||||||
|
const nextSig = makeConfigSignature(cfg);
|
||||||
|
if (beforeSig && nextSig !== beforeSig) {
|
||||||
|
broadcast("config-updated", {
|
||||||
|
worldSeed,
|
||||||
|
config: {
|
||||||
|
dailyActionQuota: cfg.dailyActionQuota,
|
||||||
|
teamActionQuota: cfg.teamActionQuota,
|
||||||
|
databaseWipeoutIntervalSeconds: cfg.databaseWipeoutIntervalSeconds,
|
||||||
|
debugModeForTeams: cfg.debugModeForTeams,
|
||||||
|
configReloadIntervalSeconds: cfg.configReloadIntervalSeconds,
|
||||||
|
elementWorth: cfg.elementWorth ?? {},
|
||||||
|
resourceWorth: cfg.resourceWorth ?? { common: {}, rare: {} },
|
||||||
|
militaryPower: cfg.militaryPower ?? {},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
lastConfigSignature = nextSig;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("[config poll]", e.message);
|
console.error("[config poll]", e.message);
|
||||||
}
|
}
|
||||||
@@ -31,8 +66,12 @@ async function main() {
|
|||||||
await initGameSchema();
|
await initGameSchema();
|
||||||
await initUsersSchema();
|
await initUsersSchema();
|
||||||
await ensureSeedEpoch();
|
await ensureSeedEpoch();
|
||||||
|
lastConfigSignature = makeConfigSignature(getConfig());
|
||||||
|
|
||||||
app.listen(PORT, () => {
|
const httpServer = createServer(app);
|
||||||
|
initWebSocketHub(httpServer);
|
||||||
|
|
||||||
|
httpServer.listen(PORT, () => {
|
||||||
const cfg = getConfig();
|
const cfg = getConfig();
|
||||||
console.log(
|
console.log(
|
||||||
`[server] Listening on :${PORT} dailyQuota=${cfg.dailyActionQuota} wipe=${cfg.databaseWipeoutIntervalSeconds}s`
|
`[server] Listening on :${PORT} dailyQuota=${cfg.dailyActionQuota} wipe=${cfg.databaseWipeoutIntervalSeconds}s`
|
||||||
|
|||||||
26
server/realtimeSnapshot.js
Normal file
26
server/realtimeSnapshot.js
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import {
|
||||||
|
getEconScores,
|
||||||
|
getElementBonus,
|
||||||
|
getMilitaryDeductions,
|
||||||
|
getActivePlayerCounts,
|
||||||
|
getVictoryPoints,
|
||||||
|
} from "./db/gameDb.js";
|
||||||
|
|
||||||
|
export async function buildRealtimeSnapshot(worldSeed) {
|
||||||
|
const [scores, elementBonus, militaryDeductions, activePlayers, victoryPoints] = await Promise.all([
|
||||||
|
getEconScores(worldSeed),
|
||||||
|
getElementBonus(worldSeed),
|
||||||
|
getMilitaryDeductions(worldSeed),
|
||||||
|
getActivePlayerCounts(worldSeed),
|
||||||
|
getVictoryPoints(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
worldSeed,
|
||||||
|
scores,
|
||||||
|
elementBonus,
|
||||||
|
militaryDeductions,
|
||||||
|
activePlayers,
|
||||||
|
victoryPoints,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -42,6 +42,7 @@ import {
|
|||||||
} from "../db/usersDb.js";
|
} from "../db/usersDb.js";
|
||||||
import { computeCell, rowToCellPayload } from "../helpers/cell.js";
|
import { computeCell, rowToCellPayload } from "../helpers/cell.js";
|
||||||
import { computeTeamMilitaryPower } from "../helpers/economy.js";
|
import { computeTeamMilitaryPower } from "../helpers/economy.js";
|
||||||
|
import { broadcast, broadcastToTeam } from "../ws/hub.js";
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
@@ -95,7 +96,7 @@ router.get("/config", async (req, res) => {
|
|||||||
databaseWipeoutIntervalSeconds: rot,
|
databaseWipeoutIntervalSeconds: rot,
|
||||||
debugModeForTeams: cfg.debugModeForTeams,
|
debugModeForTeams: cfg.debugModeForTeams,
|
||||||
configReloadIntervalSeconds: cfg.configReloadIntervalSeconds,
|
configReloadIntervalSeconds: cfg.configReloadIntervalSeconds,
|
||||||
worldSeed: ws.worldSeed,
|
worldSeed,
|
||||||
seedPeriodEndsAtUtc: ws.seedPeriodEndsAtUtc,
|
seedPeriodEndsAtUtc: ws.seedPeriodEndsAtUtc,
|
||||||
seedPeriodStartsAtUtc: ws.seedPeriodStartsAtUtc,
|
seedPeriodStartsAtUtc: ws.seedPeriodStartsAtUtc,
|
||||||
actionsRemaining,
|
actionsRemaining,
|
||||||
@@ -193,6 +194,11 @@ router.post("/cell/reveal", authMiddleware, async (req, res) => {
|
|||||||
const existing = await getExistingCell(seed, x, y);
|
const existing = await getExistingCell(seed, x, y);
|
||||||
if (!existing) return res.status(500).json({ error: "insert_race" });
|
if (!existing) return res.status(500).json({ error: "insert_race" });
|
||||||
|
|
||||||
|
broadcastToTeam(team, "cell-updated", {
|
||||||
|
worldSeed,
|
||||||
|
cell: rowToCellPayload(existing),
|
||||||
|
});
|
||||||
|
|
||||||
return res.json(rowToCellPayload(existing));
|
return res.json(rowToCellPayload(existing));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
@@ -258,10 +264,32 @@ router.post("/cell/capture", authMiddleware, async (req, res) => {
|
|||||||
|
|
||||||
const updatedCell = await getExistingCell(worldSeed, x, y);
|
const updatedCell = await getExistingCell(worldSeed, x, y);
|
||||||
const updatedTeamRow = await getTeamActionsRow(team);
|
const updatedTeamRow = await getTeamActionsRow(team);
|
||||||
|
const updatedCellPayload = rowToCellPayload(updatedCell);
|
||||||
|
|
||||||
|
// Team that made the capture always gets the update.
|
||||||
|
broadcastToTeam(team, "cell-updated", {
|
||||||
|
worldSeed,
|
||||||
|
cell: updatedCellPayload,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Opponent receives the update only if that team had visibility on this cell.
|
||||||
|
const opposingTeam = team === "blue" ? "red" : "blue";
|
||||||
|
const opposingVisible = await checkTeamVisibility(worldSeed, opposingTeam, x, y);
|
||||||
|
if (opposingVisible) {
|
||||||
|
broadcastToTeam(opposingTeam, "cell-updated", {
|
||||||
|
worldSeed,
|
||||||
|
cell: updatedCellPayload,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
broadcastToTeam(team, "team-quota-updated", {
|
||||||
|
team,
|
||||||
|
actionsRemaining: updatedTeamRow?.actions_remaining ?? null,
|
||||||
|
});
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
cell: rowToCellPayload(updatedCell),
|
cell: updatedCellPayload,
|
||||||
teamActionsRemaining: updatedTeamRow?.actions_remaining ?? null,
|
teamActionsRemaining: updatedTeamRow?.actions_remaining ?? null,
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -476,10 +504,30 @@ router.post("/military/attack", authMiddleware, async (req, res) => {
|
|||||||
|
|
||||||
const deductions = await getMilitaryDeductions(worldSeed);
|
const deductions = await getMilitaryDeductions(worldSeed);
|
||||||
const updatedCell = await getExistingCell(worldSeed, x, y);
|
const updatedCell = await getExistingCell(worldSeed, x, y);
|
||||||
|
const updatedCellPayload = rowToCellPayload(updatedCell);
|
||||||
|
|
||||||
|
broadcast("military-deductions-updated", {
|
||||||
|
worldSeed,
|
||||||
|
deductions,
|
||||||
|
});
|
||||||
|
|
||||||
|
broadcastToTeam(attackingTeam, "cell-updated", {
|
||||||
|
worldSeed,
|
||||||
|
cell: updatedCellPayload,
|
||||||
|
});
|
||||||
|
|
||||||
|
const opposingTeam = attackingTeam === "blue" ? "red" : "blue";
|
||||||
|
const opposingVisible = await checkTeamVisibility(worldSeed, opposingTeam, x, y);
|
||||||
|
if (opposingVisible) {
|
||||||
|
broadcastToTeam(opposingTeam, "cell-updated", {
|
||||||
|
worldSeed,
|
||||||
|
cell: updatedCellPayload,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
cell: rowToCellPayload(updatedCell),
|
cell: updatedCellPayload,
|
||||||
deductions,
|
deductions,
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
147
server/ws/hub.js
Normal file
147
server/ws/hub.js
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
import { WebSocketServer, WebSocket } from "ws";
|
||||||
|
import jwt from "jsonwebtoken";
|
||||||
|
import { JWT_SECRET } from "../middleware/auth.js";
|
||||||
|
|
||||||
|
let wss = null;
|
||||||
|
let heartbeatTimer = null;
|
||||||
|
|
||||||
|
const clients = new Set();
|
||||||
|
const HEARTBEAT_MS = 30_000;
|
||||||
|
|
||||||
|
function parseAuthFromToken(token) {
|
||||||
|
try {
|
||||||
|
if (!token) return null;
|
||||||
|
const payload = jwt.verify(token, JWT_SECRET);
|
||||||
|
return {
|
||||||
|
userId: payload.userId,
|
||||||
|
username: payload.username,
|
||||||
|
team: payload.team,
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendRaw(client, text) {
|
||||||
|
if (client.ws.readyState !== WebSocket.OPEN) return;
|
||||||
|
client.ws.send(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toWireMessage(type, payload = {}) {
|
||||||
|
return JSON.stringify({
|
||||||
|
type,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
...payload,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanupClient(client) {
|
||||||
|
clients.delete(client);
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseClientMessage(data) {
|
||||||
|
try {
|
||||||
|
const text = typeof data === "string" ? data : data.toString("utf8");
|
||||||
|
return JSON.parse(text);
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function startHeartbeat() {
|
||||||
|
if (heartbeatTimer) return;
|
||||||
|
heartbeatTimer = setInterval(() => {
|
||||||
|
for (const client of clients) {
|
||||||
|
if (!client.isAlive) {
|
||||||
|
try {
|
||||||
|
client.ws.terminate();
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
cleanupClient(client);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
client.isAlive = false;
|
||||||
|
try {
|
||||||
|
client.ws.ping();
|
||||||
|
} catch {
|
||||||
|
cleanupClient(client);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, HEARTBEAT_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function initWebSocketHub(httpServer) {
|
||||||
|
if (wss) return wss;
|
||||||
|
|
||||||
|
wss = new WebSocketServer({
|
||||||
|
server: httpServer,
|
||||||
|
path: "/ws",
|
||||||
|
});
|
||||||
|
|
||||||
|
wss.on("connection", (ws) => {
|
||||||
|
const client = {
|
||||||
|
ws,
|
||||||
|
auth: null,
|
||||||
|
isAlive: true,
|
||||||
|
};
|
||||||
|
clients.add(client);
|
||||||
|
|
||||||
|
ws.on("pong", () => {
|
||||||
|
client.isAlive = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.on("close", () => {
|
||||||
|
cleanupClient(client);
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.on("error", () => {
|
||||||
|
cleanupClient(client);
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.on("message", (data, isBinary) => {
|
||||||
|
if (isBinary) return;
|
||||||
|
const message = parseClientMessage(data);
|
||||||
|
if (!message || message.type !== "auth") return;
|
||||||
|
|
||||||
|
client.auth = parseAuthFromToken(message.token);
|
||||||
|
sendRaw(
|
||||||
|
client,
|
||||||
|
toWireMessage("auth-state", {
|
||||||
|
authenticated: Boolean(client.auth),
|
||||||
|
team: client.auth?.team ?? null,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
sendRaw(
|
||||||
|
client,
|
||||||
|
toWireMessage("welcome", {
|
||||||
|
authenticated: false,
|
||||||
|
team: null,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
startHeartbeat();
|
||||||
|
return wss;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function broadcast(type, payload = {}) {
|
||||||
|
const text = toWireMessage(type, payload);
|
||||||
|
for (const client of clients) {
|
||||||
|
sendRaw(client, text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function broadcastToTeam(team, type, payload = {}) {
|
||||||
|
const text = toWireMessage(type, payload);
|
||||||
|
for (const client of clients) {
|
||||||
|
if (client.auth?.team !== team) continue;
|
||||||
|
sendRaw(client, text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getConnectedClientCount() {
|
||||||
|
return clients.size;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user