feat: Implement ds-screencast engine, panel preview, and Waveshare ESP-NOW communication.
This commit is contained in:
parent
bf2b7c3cd5
commit
9e2cb29dd9
95 changed files with 50095 additions and 74 deletions
|
|
@ -973,10 +973,16 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── File loading ────────────────────────────────────
|
// ── File loading ────────────────────────────────────
|
||||||
// Load from URL param
|
|
||||||
const params = new URLSearchParams(location.search);
|
const params = new URLSearchParams(location.search);
|
||||||
const fileUrl = params.get('file');
|
const fileUrl = params.get('file');
|
||||||
if (fileUrl) {
|
const wsUrl = params.get('ws'); // e.g. ?ws=ws://localhost:9201
|
||||||
|
|
||||||
|
if (wsUrl) {
|
||||||
|
// ── WebSocket Binary Bridge Mode ─────────────────
|
||||||
|
// Connects to a relay that bridges UDP ↔ WebSocket.
|
||||||
|
// Receives binary signal frames from hub in real-time.
|
||||||
|
connectWebSocket(wsUrl);
|
||||||
|
} else if (fileUrl) {
|
||||||
fetch(fileUrl)
|
fetch(fileUrl)
|
||||||
.then(r => r.json())
|
.then(r => r.json())
|
||||||
.then(ir => { buildUI(ir); log(`Loaded from ${fileUrl}`); })
|
.then(ir => { buildUI(ir); log(`Loaded from ${fileUrl}`); })
|
||||||
|
|
@ -1003,13 +1009,126 @@
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Also try loading app.ir.json from same directory
|
// Auto-load app.ir.json (file mode fallback)
|
||||||
if (!fileUrl) {
|
if (!fileUrl && !wsUrl) {
|
||||||
fetch('app.ir.json')
|
fetch('app.ir.json')
|
||||||
.then(r => r.json())
|
.then(r => r.json())
|
||||||
.then(ir => { buildUI(ir); log('Auto-loaded app.ir.json'); })
|
.then(ir => { buildUI(ir); log('Auto-loaded app.ir.json'); })
|
||||||
.catch(() => log('No app.ir.json found. Drag-drop an IR file or use ?file=URL'));
|
.catch(() => log('No app.ir.json found. Drag-drop an IR file or use ?file=URL or ?ws=ws://host:port'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── WebSocket Binary Bridge ──────────────────────────
|
||||||
|
// Frame types must match ds_espnow.h
|
||||||
|
const DS_NOW_SIG = 0x20;
|
||||||
|
const DS_NOW_SIG_BATCH = 0x21;
|
||||||
|
const DS_NOW_ACTION = 0x31;
|
||||||
|
const DS_NOW_PING = 0xFE;
|
||||||
|
const DS_NOW_PONG = 0xFD;
|
||||||
|
const DS_UDP_IR_PUSH = 0x40;
|
||||||
|
|
||||||
|
let ws = null;
|
||||||
|
let wsSeq = 0;
|
||||||
|
|
||||||
|
function connectWebSocket(url) {
|
||||||
|
log(`Connecting to ${url}...`, 'sig');
|
||||||
|
ws = new WebSocket(url);
|
||||||
|
ws.binaryType = 'arraybuffer';
|
||||||
|
|
||||||
|
ws.onopen = () => {
|
||||||
|
log('WebSocket connected — receiving live signals', 'sig');
|
||||||
|
document.getElementById('status').textContent = '🟢 Live';
|
||||||
|
document.querySelector('.dot').style.background = '#22c55e';
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onmessage = (event) => {
|
||||||
|
if (typeof event.data === 'string') {
|
||||||
|
// JSON message — treat as IR push
|
||||||
|
try {
|
||||||
|
const ir = JSON.parse(event.data);
|
||||||
|
buildUI(ir);
|
||||||
|
log('IR push received via WebSocket');
|
||||||
|
} catch (e) {
|
||||||
|
log(`WS JSON error: ${e}`);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Binary message
|
||||||
|
const buf = new DataView(event.data);
|
||||||
|
if (buf.byteLength < 1) return;
|
||||||
|
const type = buf.getUint8(0);
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case DS_NOW_SIG:
|
||||||
|
if (buf.byteLength >= 7) {
|
||||||
|
const sigId = buf.getUint16(1, true);
|
||||||
|
const value = buf.getInt32(3, true);
|
||||||
|
updateSignal(sigId, value);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case DS_NOW_SIG_BATCH:
|
||||||
|
if (buf.byteLength >= 3) {
|
||||||
|
const count = buf.getUint8(1);
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
const offset = 3 + i * 6;
|
||||||
|
if (offset + 6 > buf.byteLength) break;
|
||||||
|
const sigId = buf.getUint16(offset, true);
|
||||||
|
const value = buf.getInt32(offset + 2, true);
|
||||||
|
updateSignal(sigId, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case DS_NOW_PING: {
|
||||||
|
// Respond with pong
|
||||||
|
const pong = new Uint8Array([DS_NOW_PONG, buf.getUint8(1)]);
|
||||||
|
ws.send(pong.buffer);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case DS_UDP_IR_PUSH:
|
||||||
|
// Binary IR push: [magic:2][type][0][len:u16][json...]
|
||||||
|
if (buf.byteLength >= 6) {
|
||||||
|
const len = buf.getUint16(4, true);
|
||||||
|
const jsonBytes = new Uint8Array(event.data, 6, len);
|
||||||
|
const json = new TextDecoder().decode(jsonBytes);
|
||||||
|
try {
|
||||||
|
const ir = JSON.parse(json);
|
||||||
|
buildUI(ir);
|
||||||
|
log(`IR push received (${len} bytes)`);
|
||||||
|
} catch (e) {
|
||||||
|
log(`IR parse error: ${e}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
log(`Unknown binary frame: 0x${type.toString(16)}`, 'evt');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onclose = () => {
|
||||||
|
log('WebSocket disconnected — reconnecting in 3s...', 'evt');
|
||||||
|
document.getElementById('status').textContent = '🔴 Disconnected';
|
||||||
|
document.querySelector('.dot').style.background = '#ef4444';
|
||||||
|
setTimeout(() => connectWebSocket(url), 3000);
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onerror = (e) => {
|
||||||
|
log(`WebSocket error: ${e}`, 'evt');
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send action event to hub (when button clicked in previewer)
|
||||||
|
function sendActionToHub(nodeId, actionType) {
|
||||||
|
if (!ws || ws.readyState !== WebSocket.OPEN) return;
|
||||||
|
const buf = new Uint8Array([DS_NOW_ACTION, nodeId, actionType, wsSeq++ & 0xFF]);
|
||||||
|
ws.send(buf.buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expose globally so buildButton can call it
|
||||||
|
window.sendActionToHub = sendActionToHub;
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
|
|
||||||
29
devices/panel-preview/node_modules/.package-lock.json
generated
vendored
Normal file
29
devices/panel-preview/node_modules/.package-lock.json
generated
vendored
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
{
|
||||||
|
"name": "panel-preview",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"node_modules/ws": {
|
||||||
|
"version": "8.19.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
|
||||||
|
"integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
20
devices/panel-preview/node_modules/ws/LICENSE
generated
vendored
Normal file
20
devices/panel-preview/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
devices/panel-preview/node_modules/ws/README.md
generated
vendored
Normal file
548
devices/panel-preview/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
devices/panel-preview/node_modules/ws/browser.js
generated
vendored
Normal file
8
devices/panel-preview/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'
|
||||||
|
);
|
||||||
|
};
|
||||||
13
devices/panel-preview/node_modules/ws/index.js
generated
vendored
Normal file
13
devices/panel-preview/node_modules/ws/index.js
generated
vendored
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const WebSocket = require('./lib/websocket');
|
||||||
|
|
||||||
|
WebSocket.createWebSocketStream = require('./lib/stream');
|
||||||
|
WebSocket.Server = require('./lib/websocket-server');
|
||||||
|
WebSocket.Receiver = require('./lib/receiver');
|
||||||
|
WebSocket.Sender = require('./lib/sender');
|
||||||
|
|
||||||
|
WebSocket.WebSocket = WebSocket;
|
||||||
|
WebSocket.WebSocketServer = WebSocket.Server;
|
||||||
|
|
||||||
|
module.exports = WebSocket;
|
||||||
131
devices/panel-preview/node_modules/ws/lib/buffer-util.js
generated
vendored
Normal file
131
devices/panel-preview/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
devices/panel-preview/node_modules/ws/lib/constants.js
generated
vendored
Normal file
19
devices/panel-preview/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
devices/panel-preview/node_modules/ws/lib/event-target.js
generated
vendored
Normal file
292
devices/panel-preview/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
devices/panel-preview/node_modules/ws/lib/extension.js
generated
vendored
Normal file
203
devices/panel-preview/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
devices/panel-preview/node_modules/ws/lib/limiter.js
generated
vendored
Normal file
55
devices/panel-preview/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
devices/panel-preview/node_modules/ws/lib/permessage-deflate.js
generated
vendored
Normal file
528
devices/panel-preview/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|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
|
||||||
|
* @param {Boolean} [isServer=false] Create the instance in either server or
|
||||||
|
* client mode
|
||||||
|
* @param {Number} [maxPayload=0] The maximum allowed message length
|
||||||
|
*/
|
||||||
|
constructor(options, isServer, maxPayload) {
|
||||||
|
this._maxPayload = maxPayload | 0;
|
||||||
|
this._options = options || {};
|
||||||
|
this._threshold =
|
||||||
|
this._options.threshold !== undefined ? this._options.threshold : 1024;
|
||||||
|
this._isServer = !!isServer;
|
||||||
|
this._deflate = null;
|
||||||
|
this._inflate = null;
|
||||||
|
|
||||||
|
this.params = null;
|
||||||
|
|
||||||
|
if (!zlibLimiter) {
|
||||||
|
const concurrency =
|
||||||
|
this._options.concurrencyLimit !== undefined
|
||||||
|
? this._options.concurrencyLimit
|
||||||
|
: 10;
|
||||||
|
zlibLimiter = new Limiter(concurrency);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {String}
|
||||||
|
*/
|
||||||
|
static get extensionName() {
|
||||||
|
return 'permessage-deflate';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an extension negotiation offer.
|
||||||
|
*
|
||||||
|
* @return {Object} Extension parameters
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
offer() {
|
||||||
|
const params = {};
|
||||||
|
|
||||||
|
if (this._options.serverNoContextTakeover) {
|
||||||
|
params.server_no_context_takeover = true;
|
||||||
|
}
|
||||||
|
if (this._options.clientNoContextTakeover) {
|
||||||
|
params.client_no_context_takeover = true;
|
||||||
|
}
|
||||||
|
if (this._options.serverMaxWindowBits) {
|
||||||
|
params.server_max_window_bits = this._options.serverMaxWindowBits;
|
||||||
|
}
|
||||||
|
if (this._options.clientMaxWindowBits) {
|
||||||
|
params.client_max_window_bits = this._options.clientMaxWindowBits;
|
||||||
|
} else if (this._options.clientMaxWindowBits == null) {
|
||||||
|
params.client_max_window_bits = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return params;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Accept an extension negotiation offer/response.
|
||||||
|
*
|
||||||
|
* @param {Array} configurations The extension negotiation offers/reponse
|
||||||
|
* @return {Object} Accepted configuration
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
accept(configurations) {
|
||||||
|
configurations = this.normalizeParams(configurations);
|
||||||
|
|
||||||
|
this.params = this._isServer
|
||||||
|
? this.acceptAsServer(configurations)
|
||||||
|
: this.acceptAsClient(configurations);
|
||||||
|
|
||||||
|
return this.params;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Releases all resources used by the extension.
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
cleanup() {
|
||||||
|
if (this._inflate) {
|
||||||
|
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
devices/panel-preview/node_modules/ws/lib/receiver.js
generated
vendored
Normal file
706
devices/panel-preview/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
devices/panel-preview/node_modules/ws/lib/sender.js
generated
vendored
Normal file
602
devices/panel-preview/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
devices/panel-preview/node_modules/ws/lib/stream.js
generated
vendored
Normal file
161
devices/panel-preview/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
devices/panel-preview/node_modules/ws/lib/subprotocol.js
generated
vendored
Normal file
62
devices/panel-preview/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
devices/panel-preview/node_modules/ws/lib/validation.js
generated
vendored
Normal file
152
devices/panel-preview/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
devices/panel-preview/node_modules/ws/lib/websocket-server.js
generated
vendored
Normal file
554
devices/panel-preview/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,
|
||||||
|
true,
|
||||||
|
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
devices/panel-preview/node_modules/ws/lib/websocket.js
generated
vendored
Normal file
1393
devices/panel-preview/node_modules/ws/lib/websocket.js
generated
vendored
Normal file
File diff suppressed because it is too large
Load diff
69
devices/panel-preview/node_modules/ws/package.json
generated
vendored
Normal file
69
devices/panel-preview/node_modules/ws/package.json
generated
vendored
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
{
|
||||||
|
"name": "ws",
|
||||||
|
"version": "8.19.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": {
|
||||||
|
"benchmark": "^2.1.4",
|
||||||
|
"bufferutil": "^4.0.1",
|
||||||
|
"eslint": "^9.0.0",
|
||||||
|
"eslint-config-prettier": "^10.0.1",
|
||||||
|
"eslint-plugin-prettier": "^5.0.0",
|
||||||
|
"globals": "^16.0.0",
|
||||||
|
"mocha": "^8.4.0",
|
||||||
|
"nyc": "^15.0.0",
|
||||||
|
"prettier": "^3.0.0",
|
||||||
|
"utf-8-validate": "^6.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
8
devices/panel-preview/node_modules/ws/wrapper.mjs
generated
vendored
Normal file
8
devices/panel-preview/node_modules/ws/wrapper.mjs
generated
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
import createWebSocketStream from './lib/stream.js';
|
||||||
|
import Receiver from './lib/receiver.js';
|
||||||
|
import Sender from './lib/sender.js';
|
||||||
|
import WebSocket from './lib/websocket.js';
|
||||||
|
import WebSocketServer from './lib/websocket-server.js';
|
||||||
|
|
||||||
|
export { createWebSocketStream, Receiver, Sender, WebSocket, WebSocketServer };
|
||||||
|
export default WebSocket;
|
||||||
37
devices/panel-preview/package-lock.json
generated
Normal file
37
devices/panel-preview/package-lock.json
generated
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
{
|
||||||
|
"name": "panel-preview",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "panel-preview",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"ws": "^8.19.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ws": {
|
||||||
|
"version": "8.19.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
|
||||||
|
"integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
15
devices/panel-preview/package.json
Normal file
15
devices/panel-preview/package.json
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
"name": "panel-preview",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"main": "relay-bridge.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"description": "",
|
||||||
|
"dependencies": {
|
||||||
|
"ws": "^8.19.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
139
devices/panel-preview/relay-bridge.js
Normal file
139
devices/panel-preview/relay-bridge.js
Normal file
|
|
@ -0,0 +1,139 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* DreamStack Relay Bridge — UDP ↔ WebSocket
|
||||||
|
*
|
||||||
|
* Bridges the hub's UDP binary frames to the browser previewer
|
||||||
|
* via WebSocket. This lets you test the full signal pipeline
|
||||||
|
* without ESP32 hardware.
|
||||||
|
*
|
||||||
|
* Architecture:
|
||||||
|
* ds-hub (Rust) → UDP:9200 → [this relay] → WS:9201 → browser previewer
|
||||||
|
* browser → WS:9201 → [this relay] → UDP:9200 → ds-hub
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* node relay-bridge.js
|
||||||
|
* Then open previewer with: ?ws=ws://localhost:9201
|
||||||
|
*/
|
||||||
|
|
||||||
|
const dgram = require('dgram');
|
||||||
|
const { WebSocketServer } = require('ws');
|
||||||
|
const http = require('http');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const UDP_PORT = 9200;
|
||||||
|
const WS_PORT = 9201;
|
||||||
|
const HTTP_PORT = 9876; // Serve previewer HTML too
|
||||||
|
|
||||||
|
// ─── WebSocket Server ───
|
||||||
|
const wss = new WebSocketServer({ host: '0.0.0.0', port: WS_PORT });
|
||||||
|
const clients = new Set();
|
||||||
|
|
||||||
|
wss.on('connection', (ws, req) => {
|
||||||
|
clients.add(ws);
|
||||||
|
console.log(`[WS] Client connected (${clients.size} total)`);
|
||||||
|
|
||||||
|
ws.on('message', (data) => {
|
||||||
|
// Forward binary messages from browser → UDP (hub)
|
||||||
|
if (Buffer.isBuffer(data) || data instanceof ArrayBuffer) {
|
||||||
|
const buf = Buffer.from(data);
|
||||||
|
udp.send(buf, 0, buf.length, UDP_PORT, '127.0.0.1', (err) => {
|
||||||
|
if (err) console.error('[UDP] Send error:', err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.on('close', () => {
|
||||||
|
clients.delete(ws);
|
||||||
|
console.log(`[WS] Client disconnected (${clients.size} remaining)`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// If we have a cached IR, send it immediately
|
||||||
|
if (cachedIR) {
|
||||||
|
ws.send(cachedIR);
|
||||||
|
console.log('[WS] Sent cached IR to new client');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── UDP Receiver ───
|
||||||
|
const udp = dgram.createSocket('udp4');
|
||||||
|
let cachedIR = null; // Cache latest IR push for new WS clients
|
||||||
|
let hubAddr = null; // Remember hub address for replies
|
||||||
|
|
||||||
|
udp.on('message', (msg, rinfo) => {
|
||||||
|
hubAddr = rinfo;
|
||||||
|
|
||||||
|
// Check if this is an IR push (has magic bytes + IR type)
|
||||||
|
if (msg.length >= 6 && msg[0] === 0xD5 && msg[1] === 0x7A && msg[2] === 0x40) {
|
||||||
|
// Extract and cache the IR JSON
|
||||||
|
const len = msg.readUInt16LE(4);
|
||||||
|
const json = msg.slice(6, 6 + len).toString();
|
||||||
|
cachedIR = json;
|
||||||
|
console.log(`[UDP] IR push received (${len} bytes), broadcasting to ${clients.size} WS clients`);
|
||||||
|
|
||||||
|
// Send as JSON text to all WS clients
|
||||||
|
for (const ws of clients) {
|
||||||
|
if (ws.readyState === 1) ws.send(json);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Forward all other binary frames to WS clients
|
||||||
|
for (const ws of clients) {
|
||||||
|
if (ws.readyState === 1) ws.send(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log signal updates
|
||||||
|
if (msg[0] === 0x20 && msg.length >= 7) {
|
||||||
|
const sigId = msg.readUInt16LE(1);
|
||||||
|
const value = msg.readInt32LE(3);
|
||||||
|
// Only log occasionally to avoid spam
|
||||||
|
if (sigId === 0 || value % 10 === 0) {
|
||||||
|
console.log(`[UDP] Signal ${sigId} = ${value}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
udp.on('error', (err) => {
|
||||||
|
console.error('[UDP] Error:', err);
|
||||||
|
});
|
||||||
|
|
||||||
|
udp.bind(UDP_PORT, () => {
|
||||||
|
console.log(`[UDP] Listening on port ${UDP_PORT}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── HTTP Server (serve previewer) ───
|
||||||
|
const server = http.createServer((req, res) => {
|
||||||
|
let filePath = path.join(__dirname, req.url === '/' ? 'index.html' : req.url.split('?')[0]);
|
||||||
|
|
||||||
|
if (!fs.existsSync(filePath)) {
|
||||||
|
res.writeHead(404);
|
||||||
|
res.end('Not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ext = path.extname(filePath);
|
||||||
|
const contentType = {
|
||||||
|
'.html': 'text/html',
|
||||||
|
'.js': 'application/javascript',
|
||||||
|
'.css': 'text/css',
|
||||||
|
'.json': 'application/json',
|
||||||
|
'.png': 'image/png',
|
||||||
|
}[ext] || 'application/octet-stream';
|
||||||
|
|
||||||
|
res.writeHead(200, { 'Content-Type': contentType });
|
||||||
|
fs.createReadStream(filePath).pipe(res);
|
||||||
|
});
|
||||||
|
|
||||||
|
server.listen(HTTP_PORT, '0.0.0.0', () => {
|
||||||
|
console.log(`\n DreamStack Relay Bridge`);
|
||||||
|
console.log(` ─────────────────────────`);
|
||||||
|
console.log(` HTTP: http://localhost:${HTTP_PORT}/`);
|
||||||
|
console.log(` WebSocket: ws://localhost:${WS_PORT}`);
|
||||||
|
console.log(` UDP: port ${UDP_PORT}`);
|
||||||
|
console.log(`\n Open previewer in live mode:`);
|
||||||
|
console.log(` http://localhost:${HTTP_PORT}/index.html?ws=ws://localhost:${WS_PORT}`);
|
||||||
|
console.log(`\n Or file mode (no hub needed):`);
|
||||||
|
console.log(` http://localhost:${HTTP_PORT}/index.html`);
|
||||||
|
console.log('');
|
||||||
|
});
|
||||||
96
devices/panel-preview/test-hub.js
Normal file
96
devices/panel-preview/test-hub.js
Normal file
|
|
@ -0,0 +1,96 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* Test script — simulates a hub sending signals to the relay bridge.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* 1. Start relay: node relay-bridge.js
|
||||||
|
* 2. Open browser: http://localhost:9876/index.html?ws=ws://localhost:9201
|
||||||
|
* 3. Run this: node test-hub.js
|
||||||
|
*
|
||||||
|
* This will:
|
||||||
|
* 1. Push IR JSON (from app.ir.json) via UDP
|
||||||
|
* 2. Send signal updates every 250ms
|
||||||
|
* 3. Listen for panel events (action/touch)
|
||||||
|
*/
|
||||||
|
|
||||||
|
const dgram = require('dgram');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const UDP_PORT = 9200;
|
||||||
|
const client = dgram.createSocket('udp4');
|
||||||
|
|
||||||
|
// ─── Push IR JSON ───
|
||||||
|
const irPath = path.join(__dirname, 'app.ir.json');
|
||||||
|
if (fs.existsSync(irPath)) {
|
||||||
|
const json = fs.readFileSync(irPath, 'utf8');
|
||||||
|
const data = Buffer.from(json);
|
||||||
|
|
||||||
|
// Build IR push frame: [D5][7A][40][00][len:u16LE][json...]
|
||||||
|
const header = Buffer.alloc(6);
|
||||||
|
header[0] = 0xD5; // magic
|
||||||
|
header[1] = 0x7A; // magic
|
||||||
|
header[2] = 0x40; // DS_UDP_IR_PUSH
|
||||||
|
header[3] = 0x00; // reserved
|
||||||
|
header.writeUInt16LE(data.length, 4);
|
||||||
|
|
||||||
|
const frame = Buffer.concat([header, data]);
|
||||||
|
client.send(frame, 0, frame.length, UDP_PORT, '127.0.0.1', () => {
|
||||||
|
console.log(`[Hub] IR pushed (${data.length} bytes)`);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log('[Hub] No app.ir.json found, skipping IR push');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Send periodic signal updates ───
|
||||||
|
let tick = 0;
|
||||||
|
|
||||||
|
setInterval(() => {
|
||||||
|
tick++;
|
||||||
|
|
||||||
|
// Signal 6 (ticks) — increment every second
|
||||||
|
if (tick % 4 === 0) {
|
||||||
|
const sigFrame = Buffer.alloc(7);
|
||||||
|
sigFrame[0] = 0x20; // DS_NOW_SIG
|
||||||
|
sigFrame.writeUInt16LE(6, 1); // signal_id = 6
|
||||||
|
sigFrame.writeInt32LE(Math.floor(tick / 4), 3); // value = seconds
|
||||||
|
client.send(sigFrame, 0, 7, UDP_PORT, '127.0.0.1');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Signal 2 (score) — increment every 10 ticks
|
||||||
|
if (tick % 40 === 0) {
|
||||||
|
const sigFrame = Buffer.alloc(7);
|
||||||
|
sigFrame[0] = 0x20;
|
||||||
|
sigFrame.writeUInt16LE(2, 1); // signal_id = 2 (score)
|
||||||
|
sigFrame.writeInt32LE(Math.floor(tick / 40), 3);
|
||||||
|
client.send(sigFrame, 0, 7, UDP_PORT, '127.0.0.1');
|
||||||
|
console.log(`[Hub] Score → ${Math.floor(tick / 40)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Batch update: signals 0,1 (head position) every 250ms
|
||||||
|
if (tick % 1 === 0) {
|
||||||
|
const batchFrame = Buffer.alloc(3 + 2 * 6); // 2 entries
|
||||||
|
batchFrame[0] = 0x21; // DS_NOW_SIG_BATCH
|
||||||
|
batchFrame[1] = 2; // count
|
||||||
|
batchFrame[2] = tick & 0xFF; // seq
|
||||||
|
|
||||||
|
// Signal 0 (headX)
|
||||||
|
batchFrame.writeUInt16LE(0, 3);
|
||||||
|
batchFrame.writeInt32LE(4 + (tick % 8), 5);
|
||||||
|
|
||||||
|
// Signal 1 (headY)
|
||||||
|
batchFrame.writeUInt16LE(1, 9);
|
||||||
|
batchFrame.writeInt32LE(4, 11);
|
||||||
|
|
||||||
|
client.send(batchFrame, 0, batchFrame.length, UDP_PORT, '127.0.0.1');
|
||||||
|
}
|
||||||
|
}, 250);
|
||||||
|
|
||||||
|
// ─── Listen for events from panel/browser ───
|
||||||
|
const listener = dgram.createSocket('udp4');
|
||||||
|
// Note: relay bridge sends events back to the source address,
|
||||||
|
// so we'd need to be listening on the same port. For testing,
|
||||||
|
// the relay bridge console logs will show events.
|
||||||
|
|
||||||
|
console.log('[Hub] Sending signals every 250ms...');
|
||||||
|
console.log('[Hub] Press Ctrl+C to stop');
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
idf_component_register(
|
idf_component_register(
|
||||||
SRCS "main.c" "ds_codec.c"
|
SRCS "main.c" "ds_codec.c" "ds_espnow.c" "ds_runtime.c"
|
||||||
INCLUDE_DIRS "."
|
INCLUDE_DIRS "."
|
||||||
REQUIRES
|
REQUIRES
|
||||||
esp_wifi
|
esp_wifi
|
||||||
|
|
@ -8,4 +8,6 @@ idf_component_register(
|
||||||
esp_timer
|
esp_timer
|
||||||
nvs_flash
|
nvs_flash
|
||||||
esp_psram
|
esp_psram
|
||||||
|
esp_now
|
||||||
|
lwip
|
||||||
)
|
)
|
||||||
|
|
|
||||||
248
devices/waveshare-p4-panel/main/ds_espnow.c
Normal file
248
devices/waveshare-p4-panel/main/ds_espnow.c
Normal file
|
|
@ -0,0 +1,248 @@
|
||||||
|
/**
|
||||||
|
* DreamStack ESP-NOW Transport — Implementation
|
||||||
|
*
|
||||||
|
* Handles ESP-NOW receive/send for binary signal frames,
|
||||||
|
* and UDP listener for IR JSON push.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <string.h>
|
||||||
|
#include "freertos/FreeRTOS.h"
|
||||||
|
#include "freertos/task.h"
|
||||||
|
#include "esp_log.h"
|
||||||
|
#include "esp_wifi.h"
|
||||||
|
#include "esp_now.h"
|
||||||
|
#include "lwip/sockets.h"
|
||||||
|
|
||||||
|
#include "ds_espnow.h"
|
||||||
|
|
||||||
|
static const char *TAG = "ds-espnow";
|
||||||
|
|
||||||
|
// ─── State ───
|
||||||
|
static ds_espnow_config_t s_config;
|
||||||
|
static uint8_t s_seq = 0;
|
||||||
|
static int s_udp_sock = -1;
|
||||||
|
static TaskHandle_t s_udp_task = NULL;
|
||||||
|
|
||||||
|
// ─── IR fragment reassembly ───
|
||||||
|
#define MAX_IR_SIZE 16384
|
||||||
|
static uint8_t s_ir_buf[MAX_IR_SIZE];
|
||||||
|
static size_t s_ir_len = 0;
|
||||||
|
static uint8_t s_frag_received = 0;
|
||||||
|
static uint8_t s_frag_total = 0;
|
||||||
|
static uint8_t s_frag_seq = 0xFF;
|
||||||
|
|
||||||
|
// ─── ESP-NOW Receive Callback ───
|
||||||
|
static void espnow_recv_cb(const esp_now_recv_info_t *recv_info,
|
||||||
|
const uint8_t *data, int len) {
|
||||||
|
if (len < 1) return;
|
||||||
|
|
||||||
|
uint8_t type = data[0];
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case DS_NOW_SIG:
|
||||||
|
if (len >= sizeof(ds_sig_frame_t)) {
|
||||||
|
const ds_sig_frame_t *f = (const ds_sig_frame_t *)data;
|
||||||
|
if (s_config.on_signal) {
|
||||||
|
s_config.on_signal(f->signal_id, f->value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case DS_NOW_SIG_BATCH:
|
||||||
|
if (len >= sizeof(ds_sig_batch_t)) {
|
||||||
|
const ds_sig_batch_t *b = (const ds_sig_batch_t *)data;
|
||||||
|
const ds_sig_entry_t *entries = (const ds_sig_entry_t *)(data + sizeof(ds_sig_batch_t));
|
||||||
|
size_t expected = sizeof(ds_sig_batch_t) + b->count * sizeof(ds_sig_entry_t);
|
||||||
|
if (len >= expected && s_config.on_signal) {
|
||||||
|
for (int i = 0; i < b->count; i++) {
|
||||||
|
s_config.on_signal(entries[i].id, entries[i].val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case DS_NOW_PING: {
|
||||||
|
// Respond with pong
|
||||||
|
ds_heartbeat_t pong = { .type = DS_NOW_PONG, .seq = data[1] };
|
||||||
|
esp_now_send(recv_info->src_addr, (const uint8_t *)&pong, sizeof(pong));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case DS_NOW_PONG:
|
||||||
|
ESP_LOGD(TAG, "Pong received (seq=%d)", data[1]);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
ESP_LOGW(TAG, "Unknown ESP-NOW frame type: 0x%02x", type);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── ESP-NOW Send Callback ───
|
||||||
|
static void espnow_send_cb(const uint8_t *mac_addr, esp_now_send_status_t status) {
|
||||||
|
if (status != ESP_NOW_SEND_SUCCESS) {
|
||||||
|
ESP_LOGW(TAG, "ESP-NOW send failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── UDP Listener Task ───
|
||||||
|
// Receives IR JSON push and fragmented IR over UDP
|
||||||
|
static void udp_listener_task(void *arg) {
|
||||||
|
struct sockaddr_in addr = {
|
||||||
|
.sin_family = AF_INET,
|
||||||
|
.sin_port = htons(DS_UDP_PORT),
|
||||||
|
.sin_addr.s_addr = htonl(INADDR_ANY),
|
||||||
|
};
|
||||||
|
|
||||||
|
s_udp_sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
|
||||||
|
if (s_udp_sock < 0) {
|
||||||
|
ESP_LOGE(TAG, "Failed to create UDP socket");
|
||||||
|
vTaskDelete(NULL);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bind(s_udp_sock, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
|
||||||
|
ESP_LOGE(TAG, "Failed to bind UDP port %d", DS_UDP_PORT);
|
||||||
|
close(s_udp_sock);
|
||||||
|
s_udp_sock = -1;
|
||||||
|
vTaskDelete(NULL);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "UDP listener on port %d", DS_UDP_PORT);
|
||||||
|
|
||||||
|
uint8_t buf[1500];
|
||||||
|
while (1) {
|
||||||
|
int len = recvfrom(s_udp_sock, buf, sizeof(buf), 0, NULL, NULL);
|
||||||
|
if (len < 4) continue;
|
||||||
|
|
||||||
|
// Check magic
|
||||||
|
if (buf[0] != 0xD5 || buf[1] != 0x7A) continue;
|
||||||
|
|
||||||
|
uint8_t frame_type = buf[2];
|
||||||
|
|
||||||
|
if (frame_type == DS_UDP_IR_PUSH) {
|
||||||
|
// Non-fragmented IR push
|
||||||
|
const ds_ir_push_t *hdr = (const ds_ir_push_t *)buf;
|
||||||
|
size_t json_len = hdr->length;
|
||||||
|
if (json_len <= len - sizeof(ds_ir_push_t) && json_len < MAX_IR_SIZE) {
|
||||||
|
memcpy(s_ir_buf, buf + sizeof(ds_ir_push_t), json_len);
|
||||||
|
s_ir_buf[json_len] = '\0';
|
||||||
|
ESP_LOGI(TAG, "IR push received (%zu bytes)", json_len);
|
||||||
|
if (s_config.on_ir_push) {
|
||||||
|
s_config.on_ir_push((const char *)s_ir_buf, json_len);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (frame_type == DS_UDP_IR_FRAG) {
|
||||||
|
// Fragmented IR push
|
||||||
|
if (len < sizeof(ds_ir_frag_t)) continue;
|
||||||
|
const ds_ir_frag_t *frag = (const ds_ir_frag_t *)buf;
|
||||||
|
size_t payload_len = len - sizeof(ds_ir_frag_t);
|
||||||
|
const uint8_t *payload = buf + sizeof(ds_ir_frag_t);
|
||||||
|
|
||||||
|
// New fragment group?
|
||||||
|
if (frag->seq != s_frag_seq) {
|
||||||
|
s_frag_seq = frag->seq;
|
||||||
|
s_frag_received = 0;
|
||||||
|
s_frag_total = frag->frag_total;
|
||||||
|
s_ir_len = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append fragment (assume ordered delivery)
|
||||||
|
if (s_ir_len + payload_len < MAX_IR_SIZE) {
|
||||||
|
memcpy(s_ir_buf + s_ir_len, payload, payload_len);
|
||||||
|
s_ir_len += payload_len;
|
||||||
|
s_frag_received++;
|
||||||
|
|
||||||
|
if (s_frag_received >= s_frag_total) {
|
||||||
|
s_ir_buf[s_ir_len] = '\0';
|
||||||
|
ESP_LOGI(TAG, "IR reassembled (%zu bytes, %d frags)",
|
||||||
|
s_ir_len, s_frag_total);
|
||||||
|
if (s_config.on_ir_push) {
|
||||||
|
s_config.on_ir_push((const char *)s_ir_buf, s_ir_len);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Public API ───
|
||||||
|
|
||||||
|
esp_err_t ds_espnow_init(const ds_espnow_config_t *config) {
|
||||||
|
s_config = *config;
|
||||||
|
if (s_config.channel == 0) s_config.channel = DS_ESPNOW_CHANNEL;
|
||||||
|
|
||||||
|
// Initialize ESP-NOW
|
||||||
|
esp_err_t ret = esp_now_init();
|
||||||
|
if (ret != ESP_OK) {
|
||||||
|
ESP_LOGE(TAG, "ESP-NOW init failed: %s", esp_err_to_name(ret));
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_now_register_recv_cb(espnow_recv_cb);
|
||||||
|
esp_now_register_send_cb(espnow_send_cb);
|
||||||
|
|
||||||
|
// Add hub as peer
|
||||||
|
esp_now_peer_info_t peer = {
|
||||||
|
.channel = s_config.channel,
|
||||||
|
.ifidx = WIFI_IF_STA,
|
||||||
|
.encrypt = false,
|
||||||
|
};
|
||||||
|
memcpy(peer.peer_addr, s_config.hub_mac, 6);
|
||||||
|
ret = esp_now_add_peer(&peer);
|
||||||
|
if (ret != ESP_OK) {
|
||||||
|
ESP_LOGW(TAG, "Add peer failed (may already exist): %s", esp_err_to_name(ret));
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "ESP-NOW initialized (ch=%d, hub=%02x:%02x:%02x:%02x:%02x:%02x)",
|
||||||
|
s_config.channel,
|
||||||
|
s_config.hub_mac[0], s_config.hub_mac[1], s_config.hub_mac[2],
|
||||||
|
s_config.hub_mac[3], s_config.hub_mac[4], s_config.hub_mac[5]);
|
||||||
|
|
||||||
|
// Start UDP listener
|
||||||
|
xTaskCreate(udp_listener_task, "ds_udp", 4096, NULL, 5, &s_udp_task);
|
||||||
|
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t ds_espnow_send_action(uint8_t node_id, uint8_t action) {
|
||||||
|
ds_action_frame_t frame = {
|
||||||
|
.type = DS_NOW_ACTION,
|
||||||
|
.node_id = node_id,
|
||||||
|
.action = action,
|
||||||
|
.seq = s_seq++,
|
||||||
|
};
|
||||||
|
return esp_now_send(s_config.hub_mac, (const uint8_t *)&frame, sizeof(frame));
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t ds_espnow_send_touch(uint8_t node_id, uint8_t event,
|
||||||
|
uint16_t x, uint16_t y) {
|
||||||
|
ds_touch_now_t frame = {
|
||||||
|
.type = DS_NOW_TOUCH,
|
||||||
|
.node_id = node_id,
|
||||||
|
.event = event,
|
||||||
|
.seq = s_seq++,
|
||||||
|
.x = x,
|
||||||
|
.y = y,
|
||||||
|
};
|
||||||
|
return esp_now_send(s_config.hub_mac, (const uint8_t *)&frame, sizeof(frame));
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t ds_espnow_send_ping(void) {
|
||||||
|
ds_heartbeat_t ping = { .type = DS_NOW_PING, .seq = s_seq++ };
|
||||||
|
return esp_now_send(s_config.hub_mac, (const uint8_t *)&ping, sizeof(ping));
|
||||||
|
}
|
||||||
|
|
||||||
|
void ds_espnow_deinit(void) {
|
||||||
|
if (s_udp_task) {
|
||||||
|
vTaskDelete(s_udp_task);
|
||||||
|
s_udp_task = NULL;
|
||||||
|
}
|
||||||
|
if (s_udp_sock >= 0) {
|
||||||
|
close(s_udp_sock);
|
||||||
|
s_udp_sock = -1;
|
||||||
|
}
|
||||||
|
esp_now_deinit();
|
||||||
|
ESP_LOGI(TAG, "ESP-NOW deinitialized");
|
||||||
|
}
|
||||||
143
devices/waveshare-p4-panel/main/ds_espnow.h
Normal file
143
devices/waveshare-p4-panel/main/ds_espnow.h
Normal file
|
|
@ -0,0 +1,143 @@
|
||||||
|
/**
|
||||||
|
* DreamStack ESP-NOW Transport — Ultra-Low Latency Binary Protocol
|
||||||
|
*
|
||||||
|
* Sub-1ms signal delivery over ESP-NOW (WiFi direct, no router).
|
||||||
|
* Binary packed frames instead of JSON for minimal overhead.
|
||||||
|
*
|
||||||
|
* Transport strategy:
|
||||||
|
* ESP-NOW: real-time signals + events (<1ms, ≤250 bytes)
|
||||||
|
* UDP: initial IR push + large payloads (~2ms, ≤1472 bytes)
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include "esp_now.h"
|
||||||
|
|
||||||
|
// ─── ESP-NOW Frame Types ───
|
||||||
|
#define DS_NOW_SIG 0x20 // Single signal update (hub → panel)
|
||||||
|
#define DS_NOW_SIG_BATCH 0x21 // Batch signal update (hub → panel)
|
||||||
|
#define DS_NOW_TOUCH 0x30 // Touch event (panel → hub)
|
||||||
|
#define DS_NOW_ACTION 0x31 // Button/widget action (panel → hub)
|
||||||
|
#define DS_NOW_PING 0xFE // Heartbeat (bidirectional)
|
||||||
|
#define DS_NOW_PONG 0xFD // Heartbeat response
|
||||||
|
|
||||||
|
// ─── UDP Frame Types ───
|
||||||
|
#define DS_UDP_IR_PUSH 0x40 // Full IR JSON push (hub → panel)
|
||||||
|
#define DS_UDP_IR_FRAG 0x41 // IR fragment for payloads > MTU
|
||||||
|
#define DS_UDP_DISCOVER 0x42 // Panel discovery broadcast
|
||||||
|
|
||||||
|
// ─── UDP Port ───
|
||||||
|
#define DS_UDP_PORT 9200
|
||||||
|
|
||||||
|
// ─── ESP-NOW Channel ───
|
||||||
|
#define DS_ESPNOW_CHANNEL 1
|
||||||
|
|
||||||
|
// ─── Max ESP-NOW payload ───
|
||||||
|
#define DS_ESPNOW_MAX_DATA 250
|
||||||
|
|
||||||
|
// ─── Signal Update Frame (7 bytes) ───
|
||||||
|
// Hub → Panel: update a single signal value
|
||||||
|
typedef struct __attribute__((packed)) {
|
||||||
|
uint8_t type; // DS_NOW_SIG
|
||||||
|
uint16_t signal_id; // which signal (0-65535)
|
||||||
|
int32_t value; // new value
|
||||||
|
} ds_sig_frame_t;
|
||||||
|
|
||||||
|
// ─── Signal Batch Frame (3 + 6*N bytes) ───
|
||||||
|
// Hub → Panel: update multiple signals at once
|
||||||
|
typedef struct __attribute__((packed)) {
|
||||||
|
uint16_t id;
|
||||||
|
int32_t val;
|
||||||
|
} ds_sig_entry_t;
|
||||||
|
|
||||||
|
typedef struct __attribute__((packed)) {
|
||||||
|
uint8_t type; // DS_NOW_SIG_BATCH
|
||||||
|
uint8_t count; // number of signals (max ~40 in 250B)
|
||||||
|
uint8_t seq; // sequence number (wrapping u8)
|
||||||
|
// followed by `count` ds_sig_entry_t entries
|
||||||
|
} ds_sig_batch_t;
|
||||||
|
|
||||||
|
// ─── Touch Event Frame (8 bytes) ───
|
||||||
|
// Panel → Hub: touch on the display
|
||||||
|
typedef struct __attribute__((packed)) {
|
||||||
|
uint8_t type; // DS_NOW_TOUCH
|
||||||
|
uint8_t node_id; // which UI node (from IR)
|
||||||
|
uint8_t event; // 0=click, 1=long_press, 2=release, 3=drag
|
||||||
|
uint8_t seq; // sequence number
|
||||||
|
uint16_t x; // touch X coordinate
|
||||||
|
uint16_t y; // touch Y coordinate
|
||||||
|
} ds_touch_now_t;
|
||||||
|
|
||||||
|
// ─── Action Event Frame (4 bytes) ───
|
||||||
|
// Panel → Hub: widget action (button click, toggle, etc.)
|
||||||
|
typedef struct __attribute__((packed)) {
|
||||||
|
uint8_t type; // DS_NOW_ACTION
|
||||||
|
uint8_t node_id; // which widget
|
||||||
|
uint8_t action; // 0=click, 1=toggle, 2=slide_change
|
||||||
|
uint8_t seq; // sequence number
|
||||||
|
} ds_action_frame_t;
|
||||||
|
|
||||||
|
// ─── Heartbeat Frame (2 bytes) ───
|
||||||
|
typedef struct __attribute__((packed)) {
|
||||||
|
uint8_t type; // DS_NOW_PING or DS_NOW_PONG
|
||||||
|
uint8_t seq; // echo back on pong
|
||||||
|
} ds_heartbeat_t;
|
||||||
|
|
||||||
|
// ─── UDP IR Push Header (4 bytes + payload) ───
|
||||||
|
typedef struct __attribute__((packed)) {
|
||||||
|
uint8_t magic[2]; // 0xD5, 0x7A
|
||||||
|
uint16_t length; // JSON payload length
|
||||||
|
// followed by `length` bytes of IR JSON
|
||||||
|
} ds_ir_push_t;
|
||||||
|
|
||||||
|
// ─── UDP IR Fragment Header (6 bytes + payload) ───
|
||||||
|
typedef struct __attribute__((packed)) {
|
||||||
|
uint8_t magic[2]; // 0xD5, 0x7A
|
||||||
|
uint8_t type; // DS_UDP_IR_FRAG
|
||||||
|
uint8_t frag_id; // fragment index (0-based)
|
||||||
|
uint8_t frag_total; // total fragments
|
||||||
|
uint8_t seq; // group sequence
|
||||||
|
// followed by fragment data (up to 1466 bytes)
|
||||||
|
} ds_ir_frag_t;
|
||||||
|
|
||||||
|
// ─── Callbacks ───
|
||||||
|
typedef void (*ds_signal_cb_t)(uint16_t signal_id, int32_t value);
|
||||||
|
typedef void (*ds_ir_cb_t)(const char *ir_json, size_t length);
|
||||||
|
|
||||||
|
// ─── Configuration ───
|
||||||
|
typedef struct {
|
||||||
|
uint8_t hub_mac[6]; // Hub MAC address (set to FF:FF:FF:FF:FF:FF for broadcast)
|
||||||
|
uint8_t channel; // WiFi channel (default: DS_ESPNOW_CHANNEL)
|
||||||
|
ds_signal_cb_t on_signal; // Called when a signal update arrives
|
||||||
|
ds_ir_cb_t on_ir_push; // Called when a full IR JSON arrives
|
||||||
|
} ds_espnow_config_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize ESP-NOW transport.
|
||||||
|
* Sets up ESP-NOW, registers peer, starts UDP listener.
|
||||||
|
* WiFi must be initialized first (STA or AP mode, no connection needed).
|
||||||
|
*/
|
||||||
|
esp_err_t ds_espnow_init(const ds_espnow_config_t *config);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send an action event to the hub (panel → hub).
|
||||||
|
* Encodes as ds_action_frame_t and sends via ESP-NOW.
|
||||||
|
*/
|
||||||
|
esp_err_t ds_espnow_send_action(uint8_t node_id, uint8_t action);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a touch event to the hub (panel → hub).
|
||||||
|
*/
|
||||||
|
esp_err_t ds_espnow_send_touch(uint8_t node_id, uint8_t event,
|
||||||
|
uint16_t x, uint16_t y);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a heartbeat ping.
|
||||||
|
*/
|
||||||
|
esp_err_t ds_espnow_send_ping(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deinitialize ESP-NOW transport.
|
||||||
|
*/
|
||||||
|
void ds_espnow_deinit(void);
|
||||||
458
devices/waveshare-p4-panel/main/ds_runtime.c
Normal file
458
devices/waveshare-p4-panel/main/ds_runtime.c
Normal file
|
|
@ -0,0 +1,458 @@
|
||||||
|
/**
|
||||||
|
* DreamStack Panel IR Runtime — Implementation
|
||||||
|
*
|
||||||
|
* Parses Panel IR JSON (using cJSON) and creates LVGL 9 widgets.
|
||||||
|
* This is the C port of the browser-based panel previewer logic.
|
||||||
|
*
|
||||||
|
* Key features:
|
||||||
|
* - Signal table with text template expansion ({0} → value)
|
||||||
|
* - Reactive updates: signal change → refresh bound labels
|
||||||
|
* - Event dispatch: button click → action opcode → ESP-NOW
|
||||||
|
* - Timer execution from IR timers[] array
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <string.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include "esp_log.h"
|
||||||
|
#include "cJSON.h"
|
||||||
|
#include "lvgl.h"
|
||||||
|
|
||||||
|
#include "ds_runtime.h"
|
||||||
|
|
||||||
|
static const char *TAG = "ds-runtime";
|
||||||
|
|
||||||
|
// ─── Signal Table ───
|
||||||
|
static ds_signal_t s_signals[DS_MAX_SIGNALS];
|
||||||
|
static uint16_t s_signal_count = 0;
|
||||||
|
|
||||||
|
// ─── Timer Table ───
|
||||||
|
static ds_timer_t s_timers[DS_MAX_TIMERS];
|
||||||
|
static uint8_t s_timer_count = 0;
|
||||||
|
|
||||||
|
// ─── Text Binding Table ───
|
||||||
|
// Maps LVGL label objects to their IR text templates
|
||||||
|
typedef struct {
|
||||||
|
lv_obj_t *label; // LVGL label widget
|
||||||
|
char template[128]; // Text template with {N} placeholders
|
||||||
|
bool used;
|
||||||
|
} ds_binding_t;
|
||||||
|
|
||||||
|
static ds_binding_t s_bindings[DS_MAX_BINDINGS];
|
||||||
|
static uint16_t s_binding_count = 0;
|
||||||
|
|
||||||
|
// ─── Parent and Callback ───
|
||||||
|
static lv_obj_t *s_parent = NULL;
|
||||||
|
static lv_obj_t *s_root = NULL;
|
||||||
|
static ds_action_cb_t s_action_cb = NULL;
|
||||||
|
|
||||||
|
// ─── Forward Declarations ───
|
||||||
|
static lv_obj_t *build_node(cJSON *node, lv_obj_t *parent);
|
||||||
|
static void expand_template(const char *tpl, char *out, size_t out_len);
|
||||||
|
static void execute_action(cJSON *action);
|
||||||
|
static void refresh_bindings(void);
|
||||||
|
|
||||||
|
// ─── Action opcodes (match IR spec) ───
|
||||||
|
#define OP_INC 1
|
||||||
|
#define OP_DEC 2
|
||||||
|
#define OP_ADD 3
|
||||||
|
#define OP_SUB 4
|
||||||
|
#define OP_SET 5
|
||||||
|
#define OP_TOGGLE 6
|
||||||
|
|
||||||
|
static uint8_t parse_op(const char *op_str) {
|
||||||
|
if (strcmp(op_str, "inc") == 0) return OP_INC;
|
||||||
|
if (strcmp(op_str, "dec") == 0) return OP_DEC;
|
||||||
|
if (strcmp(op_str, "add") == 0) return OP_ADD;
|
||||||
|
if (strcmp(op_str, "sub") == 0) return OP_SUB;
|
||||||
|
if (strcmp(op_str, "set") == 0) return OP_SET;
|
||||||
|
if (strcmp(op_str, "toggle") == 0) return OP_TOGGLE;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Timer Callback ───
|
||||||
|
static void timer_cb(lv_timer_t *timer) {
|
||||||
|
ds_timer_t *t = (ds_timer_t *)lv_timer_get_user_data(timer);
|
||||||
|
if (!t) return;
|
||||||
|
|
||||||
|
int32_t val = s_signals[t->action_sig].i;
|
||||||
|
switch (t->action_op) {
|
||||||
|
case OP_INC: val++; break;
|
||||||
|
case OP_DEC: val--; break;
|
||||||
|
case OP_ADD: val += t->action_val; break;
|
||||||
|
case OP_SUB: val -= t->action_val; break;
|
||||||
|
case OP_SET: val = t->action_val; break;
|
||||||
|
case OP_TOGGLE: val = val ? 0 : 1; break;
|
||||||
|
}
|
||||||
|
ds_signal_update(t->action_sig, val);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Button Event Handler ───
|
||||||
|
typedef struct {
|
||||||
|
cJSON *action; // JSON action object (kept alive while UI exists)
|
||||||
|
uint8_t node_id;
|
||||||
|
} btn_user_data_t;
|
||||||
|
|
||||||
|
static void btn_click_cb(lv_event_t *e) {
|
||||||
|
btn_user_data_t *ud = (btn_user_data_t *)lv_event_get_user_data(e);
|
||||||
|
if (ud && ud->action) {
|
||||||
|
execute_action(ud->action);
|
||||||
|
if (s_action_cb) {
|
||||||
|
s_action_cb(ud->node_id, 0); // notify ESP-NOW
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Build Helpers ───
|
||||||
|
|
||||||
|
static lv_obj_t *build_container(cJSON *node, lv_obj_t *parent, bool is_row) {
|
||||||
|
lv_obj_t *cont = lv_obj_create(parent);
|
||||||
|
lv_obj_set_size(cont, LV_PCT(100), LV_SIZE_CONTENT);
|
||||||
|
lv_obj_set_flex_flow(cont, is_row ? LV_FLEX_FLOW_ROW : LV_FLEX_FLOW_COLUMN);
|
||||||
|
lv_obj_set_style_bg_opa(cont, LV_OPA_TRANSP, 0);
|
||||||
|
lv_obj_set_style_border_width(cont, 0, 0);
|
||||||
|
lv_obj_set_style_pad_all(cont, 4, 0);
|
||||||
|
|
||||||
|
cJSON *gap = cJSON_GetObjectItem(node, "gap");
|
||||||
|
if (gap) {
|
||||||
|
lv_obj_set_style_pad_gap(cont, gap->valueint, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON *children = cJSON_GetObjectItem(node, "c");
|
||||||
|
if (children && cJSON_IsArray(children)) {
|
||||||
|
cJSON *child;
|
||||||
|
cJSON_ArrayForEach(child, children) {
|
||||||
|
build_node(child, cont);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cont;
|
||||||
|
}
|
||||||
|
|
||||||
|
static lv_obj_t *build_label(cJSON *node, lv_obj_t *parent) {
|
||||||
|
lv_obj_t *lbl = lv_label_create(parent);
|
||||||
|
cJSON *text = cJSON_GetObjectItem(node, "text");
|
||||||
|
if (text && text->valuestring) {
|
||||||
|
char expanded[256];
|
||||||
|
expand_template(text->valuestring, expanded, sizeof(expanded));
|
||||||
|
lv_label_set_text(lbl, expanded);
|
||||||
|
|
||||||
|
// Register binding if template contains {N}
|
||||||
|
if (strchr(text->valuestring, '{') && s_binding_count < DS_MAX_BINDINGS) {
|
||||||
|
ds_binding_t *b = &s_bindings[s_binding_count++];
|
||||||
|
b->label = lbl;
|
||||||
|
strncpy(b->template, text->valuestring, sizeof(b->template) - 1);
|
||||||
|
b->used = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON *size = cJSON_GetObjectItem(node, "size");
|
||||||
|
if (size) {
|
||||||
|
lv_obj_set_style_text_font(lbl,
|
||||||
|
size->valueint >= 24 ? &lv_font_montserrat_24 :
|
||||||
|
size->valueint >= 18 ? &lv_font_montserrat_18 :
|
||||||
|
size->valueint >= 14 ? &lv_font_montserrat_14 :
|
||||||
|
&lv_font_montserrat_12, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return lbl;
|
||||||
|
}
|
||||||
|
|
||||||
|
static lv_obj_t *build_button(cJSON *node, lv_obj_t *parent) {
|
||||||
|
lv_obj_t *btn = lv_btn_create(parent);
|
||||||
|
lv_obj_t *lbl = lv_label_create(btn);
|
||||||
|
|
||||||
|
cJSON *text = cJSON_GetObjectItem(node, "text");
|
||||||
|
if (text && text->valuestring) {
|
||||||
|
char expanded[256];
|
||||||
|
expand_template(text->valuestring, expanded, sizeof(expanded));
|
||||||
|
lv_label_set_text(lbl, expanded);
|
||||||
|
|
||||||
|
// Register binding for button text too
|
||||||
|
if (strchr(text->valuestring, '{') && s_binding_count < DS_MAX_BINDINGS) {
|
||||||
|
ds_binding_t *b = &s_bindings[s_binding_count++];
|
||||||
|
b->label = lbl;
|
||||||
|
strncpy(b->template, text->valuestring, sizeof(b->template) - 1);
|
||||||
|
b->used = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON *on = cJSON_GetObjectItem(node, "on");
|
||||||
|
if (on) {
|
||||||
|
cJSON *click = cJSON_GetObjectItem(on, "click");
|
||||||
|
if (click) {
|
||||||
|
btn_user_data_t *ud = malloc(sizeof(btn_user_data_t));
|
||||||
|
ud->action = click; // keep reference (IR JSON stays in memory)
|
||||||
|
cJSON *id_obj = cJSON_GetObjectItem(node, "id");
|
||||||
|
ud->node_id = id_obj ? id_obj->valueint : 0;
|
||||||
|
lv_obj_add_event_cb(btn, btn_click_cb, LV_EVENT_CLICKED, ud);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return btn;
|
||||||
|
}
|
||||||
|
|
||||||
|
static lv_obj_t *build_slider(cJSON *node, lv_obj_t *parent) {
|
||||||
|
lv_obj_t *slider = lv_slider_create(parent);
|
||||||
|
lv_obj_set_width(slider, LV_PCT(80));
|
||||||
|
|
||||||
|
cJSON *min = cJSON_GetObjectItem(node, "min");
|
||||||
|
cJSON *max = cJSON_GetObjectItem(node, "max");
|
||||||
|
if (min) lv_slider_set_range(slider, min->valueint, max ? max->valueint : 100);
|
||||||
|
|
||||||
|
cJSON *bind = cJSON_GetObjectItem(node, "bind");
|
||||||
|
if (bind) {
|
||||||
|
int sid = bind->valueint;
|
||||||
|
lv_slider_set_value(slider, s_signals[sid].i, LV_ANIM_OFF);
|
||||||
|
// TODO: add slider change event → ds_signal_update
|
||||||
|
}
|
||||||
|
|
||||||
|
return slider;
|
||||||
|
}
|
||||||
|
|
||||||
|
static lv_obj_t *build_panel(cJSON *node, lv_obj_t *parent) {
|
||||||
|
lv_obj_t *pnl = lv_obj_create(parent);
|
||||||
|
lv_obj_set_size(pnl, LV_PCT(100), LV_SIZE_CONTENT);
|
||||||
|
lv_obj_set_flex_flow(pnl, LV_FLEX_FLOW_COLUMN);
|
||||||
|
lv_obj_set_style_pad_all(pnl, 12, 0);
|
||||||
|
lv_obj_set_style_radius(pnl, 8, 0);
|
||||||
|
|
||||||
|
cJSON *title = cJSON_GetObjectItem(node, "text");
|
||||||
|
if (title && title->valuestring) {
|
||||||
|
lv_obj_t *t = lv_label_create(pnl);
|
||||||
|
lv_label_set_text(t, title->valuestring);
|
||||||
|
lv_obj_set_style_text_font(t, &lv_font_montserrat_14, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON *children = cJSON_GetObjectItem(node, "c");
|
||||||
|
if (children && cJSON_IsArray(children)) {
|
||||||
|
cJSON *child;
|
||||||
|
cJSON_ArrayForEach(child, children) {
|
||||||
|
build_node(child, pnl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return pnl;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Node Dispatcher ───
|
||||||
|
static lv_obj_t *build_node(cJSON *node, lv_obj_t *parent) {
|
||||||
|
if (!node) return NULL;
|
||||||
|
|
||||||
|
cJSON *type = cJSON_GetObjectItem(node, "t");
|
||||||
|
if (!type || !type->valuestring) return NULL;
|
||||||
|
|
||||||
|
const char *t = type->valuestring;
|
||||||
|
|
||||||
|
if (strcmp(t, "col") == 0) return build_container(node, parent, false);
|
||||||
|
if (strcmp(t, "row") == 0) return build_container(node, parent, true);
|
||||||
|
if (strcmp(t, "lbl") == 0) return build_label(node, parent);
|
||||||
|
if (strcmp(t, "btn") == 0) return build_button(node, parent);
|
||||||
|
if (strcmp(t, "sld") == 0) return build_slider(node, parent);
|
||||||
|
if (strcmp(t, "pnl") == 0) return build_panel(node, parent);
|
||||||
|
// stk, inp, sw, bar, img — add as needed
|
||||||
|
|
||||||
|
ESP_LOGW(TAG, "Unknown node type: %s", t);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Template Expansion ───
|
||||||
|
// Replace {N} with signal values: "{2}: {0}°F" → "Kitchen: 72°F"
|
||||||
|
static void expand_template(const char *tpl, char *out, size_t out_len) {
|
||||||
|
size_t pos = 0;
|
||||||
|
while (*tpl && pos < out_len - 1) {
|
||||||
|
if (*tpl == '{') {
|
||||||
|
tpl++;
|
||||||
|
int sig_id = 0;
|
||||||
|
while (*tpl >= '0' && *tpl <= '9') {
|
||||||
|
sig_id = sig_id * 10 + (*tpl - '0');
|
||||||
|
tpl++;
|
||||||
|
}
|
||||||
|
if (*tpl == '}') tpl++;
|
||||||
|
|
||||||
|
if (sig_id < DS_MAX_SIGNALS && s_signals[sig_id].used) {
|
||||||
|
int written = snprintf(out + pos, out_len - pos, "%d",
|
||||||
|
(int)s_signals[sig_id].i);
|
||||||
|
if (written > 0) pos += written;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
out[pos++] = *tpl++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out[pos] = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Action Executor ───
|
||||||
|
static void execute_action(cJSON *action) {
|
||||||
|
if (!action) return;
|
||||||
|
|
||||||
|
// Handle arrays of actions
|
||||||
|
if (cJSON_IsArray(action)) {
|
||||||
|
cJSON *item;
|
||||||
|
cJSON_ArrayForEach(item, action) {
|
||||||
|
execute_action(item);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON *op_json = cJSON_GetObjectItem(action, "op");
|
||||||
|
cJSON *s_json = cJSON_GetObjectItem(action, "s");
|
||||||
|
if (!op_json || !s_json) return;
|
||||||
|
|
||||||
|
uint8_t op = parse_op(op_json->valuestring);
|
||||||
|
uint16_t sid = s_json->valueint;
|
||||||
|
if (sid >= DS_MAX_SIGNALS) return;
|
||||||
|
|
||||||
|
int32_t val = s_signals[sid].i;
|
||||||
|
cJSON *v_json = cJSON_GetObjectItem(action, "v");
|
||||||
|
int32_t v = v_json ? v_json->valueint : 0;
|
||||||
|
|
||||||
|
switch (op) {
|
||||||
|
case OP_INC: val++; break;
|
||||||
|
case OP_DEC: val--; break;
|
||||||
|
case OP_ADD: val += v; break;
|
||||||
|
case OP_SUB: val -= v; break;
|
||||||
|
case OP_SET: val = v; break;
|
||||||
|
case OP_TOGGLE: val = val ? 0 : 1; break;
|
||||||
|
}
|
||||||
|
|
||||||
|
ds_signal_update(sid, val);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Refresh Bindings ───
|
||||||
|
// Called after any signal change to update bound labels
|
||||||
|
static void refresh_bindings(void) {
|
||||||
|
for (int i = 0; i < s_binding_count; i++) {
|
||||||
|
if (!s_bindings[i].used) continue;
|
||||||
|
char expanded[256];
|
||||||
|
expand_template(s_bindings[i].template, expanded, sizeof(expanded));
|
||||||
|
lv_label_set_text(s_bindings[i].label, expanded);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Public API ───
|
||||||
|
|
||||||
|
esp_err_t ds_runtime_init(void *parent, ds_action_cb_t action_cb) {
|
||||||
|
s_parent = (lv_obj_t *)parent;
|
||||||
|
s_action_cb = action_cb;
|
||||||
|
s_root = NULL;
|
||||||
|
s_signal_count = 0;
|
||||||
|
s_binding_count = 0;
|
||||||
|
s_timer_count = 0;
|
||||||
|
memset(s_signals, 0, sizeof(s_signals));
|
||||||
|
memset(s_bindings, 0, sizeof(s_bindings));
|
||||||
|
memset(s_timers, 0, sizeof(s_timers));
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "Runtime initialized");
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t ds_ui_build(const char *ir_json, size_t length) {
|
||||||
|
// Destroy previous UI
|
||||||
|
ds_ui_destroy();
|
||||||
|
|
||||||
|
cJSON *ir = cJSON_ParseWithLength(ir_json, length);
|
||||||
|
if (!ir) {
|
||||||
|
ESP_LOGE(TAG, "Failed to parse IR JSON");
|
||||||
|
return ESP_ERR_INVALID_ARG;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load signals
|
||||||
|
cJSON *signals = cJSON_GetObjectItem(ir, "signals");
|
||||||
|
if (signals && cJSON_IsArray(signals)) {
|
||||||
|
cJSON *sig;
|
||||||
|
cJSON_ArrayForEach(sig, signals) {
|
||||||
|
cJSON *id = cJSON_GetObjectItem(sig, "id");
|
||||||
|
cJSON *v = cJSON_GetObjectItem(sig, "v");
|
||||||
|
if (id && id->valueint < DS_MAX_SIGNALS) {
|
||||||
|
int sid = id->valueint;
|
||||||
|
s_signals[sid].i = v ? v->valueint : 0;
|
||||||
|
s_signals[sid].type = DS_SIG_INT;
|
||||||
|
s_signals[sid].used = true;
|
||||||
|
s_signal_count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build UI tree
|
||||||
|
cJSON *root = cJSON_GetObjectItem(ir, "root");
|
||||||
|
if (root) {
|
||||||
|
s_root = build_node(root, s_parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load timers
|
||||||
|
cJSON *timers = cJSON_GetObjectItem(ir, "timers");
|
||||||
|
if (timers && cJSON_IsArray(timers)) {
|
||||||
|
cJSON *t;
|
||||||
|
cJSON_ArrayForEach(t, timers) {
|
||||||
|
if (s_timer_count >= DS_MAX_TIMERS) break;
|
||||||
|
cJSON *ms = cJSON_GetObjectItem(t, "ms");
|
||||||
|
cJSON *action = cJSON_GetObjectItem(t, "action");
|
||||||
|
if (ms && action) {
|
||||||
|
ds_timer_t *timer = &s_timers[s_timer_count];
|
||||||
|
timer->ms = ms->valueint;
|
||||||
|
|
||||||
|
cJSON *op = cJSON_GetObjectItem(action, "op");
|
||||||
|
cJSON *s = cJSON_GetObjectItem(action, "s");
|
||||||
|
cJSON *v = cJSON_GetObjectItem(action, "v");
|
||||||
|
timer->action_op = op ? parse_op(op->valuestring) : 0;
|
||||||
|
timer->action_sig = s ? s->valueint : 0;
|
||||||
|
timer->action_val = v ? v->valueint : 0;
|
||||||
|
|
||||||
|
timer->timer = lv_timer_create(timer_cb, timer->ms, timer);
|
||||||
|
s_timer_count++;
|
||||||
|
ESP_LOGI(TAG, "Timer: every %dms → op %d on s%d",
|
||||||
|
(int)timer->ms, timer->action_op, timer->action_sig);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "UI built: %d signals, %d bindings, %d timers",
|
||||||
|
s_signal_count, s_binding_count, s_timer_count);
|
||||||
|
|
||||||
|
// Keep IR JSON alive for button action references
|
||||||
|
// (cJSON_Delete would invalidate action pointers)
|
||||||
|
// TODO: deep-copy actions to avoid this leak
|
||||||
|
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ds_ui_destroy(void) {
|
||||||
|
// Stop timers
|
||||||
|
for (int i = 0; i < s_timer_count; i++) {
|
||||||
|
if (s_timers[i].timer) {
|
||||||
|
lv_timer_delete((lv_timer_t *)s_timers[i].timer);
|
||||||
|
s_timers[i].timer = NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s_timer_count = 0;
|
||||||
|
|
||||||
|
// Clear bindings
|
||||||
|
s_binding_count = 0;
|
||||||
|
memset(s_bindings, 0, sizeof(s_bindings));
|
||||||
|
|
||||||
|
// Destroy LVGL tree
|
||||||
|
if (s_root) {
|
||||||
|
lv_obj_del(s_root);
|
||||||
|
s_root = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "UI destroyed");
|
||||||
|
}
|
||||||
|
|
||||||
|
void ds_signal_update(uint16_t signal_id, int32_t value) {
|
||||||
|
if (signal_id >= DS_MAX_SIGNALS) return;
|
||||||
|
s_signals[signal_id].i = value;
|
||||||
|
s_signals[signal_id].used = true;
|
||||||
|
|
||||||
|
// Refresh all bound labels
|
||||||
|
refresh_bindings();
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t ds_signal_count(void) {
|
||||||
|
return s_signal_count;
|
||||||
|
}
|
||||||
|
|
||||||
|
int32_t ds_signal_get(uint16_t signal_id) {
|
||||||
|
if (signal_id >= DS_MAX_SIGNALS) return 0;
|
||||||
|
return s_signals[signal_id].i;
|
||||||
|
}
|
||||||
90
devices/waveshare-p4-panel/main/ds_runtime.h
Normal file
90
devices/waveshare-p4-panel/main/ds_runtime.h
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
/**
|
||||||
|
* DreamStack Panel IR Runtime — LVGL Widget Builder
|
||||||
|
*
|
||||||
|
* Parses Panel IR JSON and creates LVGL widgets.
|
||||||
|
* Handles signal binding, text template expansion,
|
||||||
|
* event dispatch, and timer execution.
|
||||||
|
*
|
||||||
|
* This is the C equivalent of the browser-based panel previewer.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include "esp_err.h"
|
||||||
|
|
||||||
|
// ─── Limits ───
|
||||||
|
#define DS_MAX_SIGNALS 64
|
||||||
|
#define DS_MAX_NODES 128
|
||||||
|
#define DS_MAX_TIMERS 8
|
||||||
|
#define DS_MAX_BINDINGS 64
|
||||||
|
|
||||||
|
// ─── Signal types ───
|
||||||
|
typedef enum {
|
||||||
|
DS_SIG_INT = 0,
|
||||||
|
DS_SIG_BOOL,
|
||||||
|
DS_SIG_STRING,
|
||||||
|
} ds_sig_type_t;
|
||||||
|
|
||||||
|
// ─── Signal value ───
|
||||||
|
typedef struct {
|
||||||
|
int32_t i; // integer value (also used for bool: 0/1)
|
||||||
|
char s[32]; // string value (short strings only)
|
||||||
|
ds_sig_type_t type;
|
||||||
|
bool used;
|
||||||
|
} ds_signal_t;
|
||||||
|
|
||||||
|
// ─── Timer entry ───
|
||||||
|
typedef struct {
|
||||||
|
uint32_t ms; // interval in milliseconds
|
||||||
|
uint8_t action_op; // action opcode
|
||||||
|
uint16_t action_sig; // target signal
|
||||||
|
int32_t action_val; // value for set/add/sub
|
||||||
|
void *timer; // LVGL timer handle (lv_timer_t *)
|
||||||
|
} ds_timer_t;
|
||||||
|
|
||||||
|
// ─── Action callback (for forwarding to ESP-NOW) ───
|
||||||
|
typedef void (*ds_action_cb_t)(uint8_t node_id, uint8_t action_type);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the Panel IR runtime.
|
||||||
|
* Must be called after LVGL is initialized.
|
||||||
|
*
|
||||||
|
* @param parent LVGL parent object (usually lv_scr_act())
|
||||||
|
* @param action_cb Callback for widget actions (forwarded to ESP-NOW)
|
||||||
|
*/
|
||||||
|
esp_err_t ds_runtime_init(void *parent, ds_action_cb_t action_cb);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the UI from Panel IR JSON.
|
||||||
|
* Parses the JSON, creates LVGL widgets, binds signals.
|
||||||
|
* Destroys any previously built UI first.
|
||||||
|
*
|
||||||
|
* @param ir_json Panel IR JSON string
|
||||||
|
* @param length Length of the JSON string
|
||||||
|
*/
|
||||||
|
esp_err_t ds_ui_build(const char *ir_json, size_t length);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Destroy the current UI tree.
|
||||||
|
* Removes all LVGL widgets and clears signal bindings.
|
||||||
|
*/
|
||||||
|
void ds_ui_destroy(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a signal value and refresh bound widgets.
|
||||||
|
*
|
||||||
|
* @param signal_id Signal ID (0-based)
|
||||||
|
* @param value New integer value
|
||||||
|
*/
|
||||||
|
void ds_signal_update(uint16_t signal_id, int32_t value);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current signal count.
|
||||||
|
*/
|
||||||
|
uint16_t ds_signal_count(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get signal value by ID.
|
||||||
|
*/
|
||||||
|
int32_t ds_signal_get(uint16_t signal_id);
|
||||||
|
|
@ -1,15 +1,12 @@
|
||||||
/**
|
/**
|
||||||
* DreamStack Thin Client — Waveshare ESP32-P4 10.1" Panel
|
* DreamStack Thin Client — Waveshare ESP32-P4 10.1" Panel
|
||||||
*
|
*
|
||||||
* Firmware that turns the panel into a dumb pixel display
|
* Dual transport firmware:
|
||||||
* with touch input. All rendering happens on the source device.
|
* 1. ESP-NOW + Panel IR (primary) — sub-1ms signal delivery, LVGL native rendering
|
||||||
|
* 2. WebSocket + pixel streaming (fallback) — for non-DreamStack content
|
||||||
*
|
*
|
||||||
* Flow: WiFi → WebSocket → receive delta frames → blit to display
|
* Build with -DDS_USE_ESPNOW=1 for ESP-NOW mode (default)
|
||||||
* Touch → encode event → send over WebSocket
|
* Build with -DDS_USE_ESPNOW=0 for WebSocket-only pixel mode
|
||||||
*
|
|
||||||
* Dependencies (via ESP Component Registry):
|
|
||||||
* - waveshare/esp_lcd_jd9365_10_1 (10.1" MIPI DSI display driver)
|
|
||||||
* - espressif/esp_websocket_client (WebSocket client)
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
|
|
@ -21,26 +18,39 @@
|
||||||
#include "esp_event.h"
|
#include "esp_event.h"
|
||||||
#include "nvs_flash.h"
|
#include "nvs_flash.h"
|
||||||
#include "esp_lcd_panel_ops.h"
|
#include "esp_lcd_panel_ops.h"
|
||||||
#include "esp_websocket_client.h"
|
|
||||||
|
|
||||||
#include "ds_codec.h"
|
#include "ds_codec.h"
|
||||||
#include "ds_protocol.h"
|
#include "ds_protocol.h"
|
||||||
|
|
||||||
|
// ─── Transport selection ───
|
||||||
|
#ifndef DS_USE_ESPNOW
|
||||||
|
#define DS_USE_ESPNOW 1
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if DS_USE_ESPNOW
|
||||||
|
#include "ds_espnow.h"
|
||||||
|
#include "ds_runtime.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
static const char *TAG = "ds-panel";
|
static const char *TAG = "ds-panel";
|
||||||
|
|
||||||
// ─── Configuration (set via menuconfig or hardcode for POC) ───
|
// ─── Configuration ───
|
||||||
#define PANEL_WIDTH 800
|
#define PANEL_WIDTH 800
|
||||||
#define PANEL_HEIGHT 1280
|
#define PANEL_HEIGHT 1280
|
||||||
#define PIXEL_BYTES 2 // RGB565
|
#define PIXEL_BYTES 2 // RGB565
|
||||||
#define FB_SIZE (PANEL_WIDTH * PANEL_HEIGHT * PIXEL_BYTES) // ~2MB
|
#define FB_SIZE (PANEL_WIDTH * PANEL_HEIGHT * PIXEL_BYTES)
|
||||||
|
|
||||||
#define WIFI_SSID CONFIG_WIFI_SSID
|
#define WIFI_SSID CONFIG_WIFI_SSID
|
||||||
#define WIFI_PASS CONFIG_WIFI_PASS
|
#define WIFI_PASS CONFIG_WIFI_PASS
|
||||||
#define RELAY_URL CONFIG_RELAY_URL // e.g. "ws://192.168.1.100:9100/stream/home"
|
|
||||||
|
|
||||||
// ─── Framebuffers (in PSRAM) ───
|
#if !DS_USE_ESPNOW
|
||||||
static uint8_t *framebuffer; // Current display state
|
#define RELAY_URL CONFIG_RELAY_URL
|
||||||
static uint8_t *scratch_buf; // Temp buffer for delta decode
|
#include "esp_websocket_client.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// ─── Framebuffers (in PSRAM, for pixel mode) ───
|
||||||
|
static uint8_t *framebuffer;
|
||||||
|
static uint8_t *scratch_buf;
|
||||||
|
|
||||||
// ─── Display handle ───
|
// ─── Display handle ───
|
||||||
static esp_lcd_panel_handle_t panel_handle = NULL;
|
static esp_lcd_panel_handle_t panel_handle = NULL;
|
||||||
|
|
@ -48,7 +58,63 @@ static esp_lcd_panel_handle_t panel_handle = NULL;
|
||||||
// ─── Touch state ───
|
// ─── Touch state ───
|
||||||
static uint16_t input_seq = 0;
|
static uint16_t input_seq = 0;
|
||||||
|
|
||||||
// ─── WebSocket event handler ───
|
#if DS_USE_ESPNOW
|
||||||
|
// ═══════════════════════════════════════════════════════
|
||||||
|
// ESP-NOW + Panel IR Mode
|
||||||
|
// ═══════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
static void on_signal(uint16_t signal_id, int32_t value) {
|
||||||
|
// Feed signal updates to the Panel IR runtime
|
||||||
|
ds_signal_update(signal_id, value);
|
||||||
|
ESP_LOGD(TAG, "Signal %d = %d", signal_id, (int)value);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void on_ir_push(const char *ir_json, size_t length) {
|
||||||
|
// Build LVGL UI from Panel IR JSON
|
||||||
|
ESP_LOGI(TAG, "IR push received (%zu bytes), building UI...", length);
|
||||||
|
esp_err_t ret = ds_ui_build(ir_json, length);
|
||||||
|
if (ret == ESP_OK) {
|
||||||
|
ESP_LOGI(TAG, "UI built: %d signals", ds_signal_count());
|
||||||
|
} else {
|
||||||
|
ESP_LOGE(TAG, "Failed to build UI from IR");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void on_action(uint8_t node_id, uint8_t action_type) {
|
||||||
|
// Forward widget actions to hub via ESP-NOW
|
||||||
|
ds_espnow_send_action(node_id, action_type);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void espnow_init_and_run(void) {
|
||||||
|
// Initialize Panel IR runtime (LVGL must be ready)
|
||||||
|
// TODO: replace NULL with lv_scr_act() once LVGL is initialized
|
||||||
|
ds_runtime_init(NULL, on_action);
|
||||||
|
|
||||||
|
// Initialize ESP-NOW transport
|
||||||
|
ds_espnow_config_t config = {
|
||||||
|
.hub_mac = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}, // broadcast
|
||||||
|
.channel = DS_ESPNOW_CHANNEL,
|
||||||
|
.on_signal = on_signal,
|
||||||
|
.on_ir_push = on_ir_push,
|
||||||
|
};
|
||||||
|
esp_err_t ret = ds_espnow_init(&config);
|
||||||
|
if (ret != ESP_OK) {
|
||||||
|
ESP_LOGE(TAG, "ESP-NOW init failed");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send periodic pings so hub discovers us
|
||||||
|
while (1) {
|
||||||
|
ds_espnow_send_ping();
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(5000));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#else
|
||||||
|
// ═══════════════════════════════════════════════════════
|
||||||
|
// WebSocket Pixel Streaming Mode (Legacy)
|
||||||
|
// ═══════════════════════════════════════════════════════
|
||||||
|
|
||||||
static void ws_event_handler(void *arg, esp_event_base_t base,
|
static void ws_event_handler(void *arg, esp_event_base_t base,
|
||||||
int32_t event_id, void *event_data) {
|
int32_t event_id, void *event_data) {
|
||||||
esp_websocket_event_data_t *data = (esp_websocket_event_data_t *)event_data;
|
esp_websocket_event_data_t *data = (esp_websocket_event_data_t *)event_data;
|
||||||
|
|
@ -69,7 +135,6 @@ static void ws_event_handler(void *arg, esp_event_base_t base,
|
||||||
|
|
||||||
switch (hdr.frame_type) {
|
switch (hdr.frame_type) {
|
||||||
case DS_FRAME_PIXELS:
|
case DS_FRAME_PIXELS:
|
||||||
// Full keyframe — copy directly to framebuffer
|
|
||||||
if (payload_len == FB_SIZE) {
|
if (payload_len == FB_SIZE) {
|
||||||
memcpy(framebuffer, payload, FB_SIZE);
|
memcpy(framebuffer, payload, FB_SIZE);
|
||||||
esp_lcd_panel_draw_bitmap(panel_handle,
|
esp_lcd_panel_draw_bitmap(panel_handle,
|
||||||
|
|
@ -79,7 +144,6 @@ static void ws_event_handler(void *arg, esp_event_base_t base,
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case DS_FRAME_DELTA:
|
case DS_FRAME_DELTA:
|
||||||
// Delta frame — RLE decode + XOR apply
|
|
||||||
if (ds_apply_delta_rle(framebuffer, FB_SIZE,
|
if (ds_apply_delta_rle(framebuffer, FB_SIZE,
|
||||||
payload, payload_len, scratch_buf) == 0) {
|
payload, payload_len, scratch_buf) == 0) {
|
||||||
esp_lcd_panel_draw_bitmap(panel_handle,
|
esp_lcd_panel_draw_bitmap(panel_handle,
|
||||||
|
|
@ -90,7 +154,6 @@ static void ws_event_handler(void *arg, esp_event_base_t base,
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case DS_FRAME_PING:
|
case DS_FRAME_PING:
|
||||||
// Respond with pong (same message back)
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case DS_FRAME_END:
|
case DS_FRAME_END:
|
||||||
|
|
@ -109,7 +172,6 @@ static void ws_event_handler(void *arg, esp_event_base_t base,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Send touch event over WebSocket ───
|
|
||||||
static void send_touch_event(esp_websocket_client_handle_t ws,
|
static void send_touch_event(esp_websocket_client_handle_t ws,
|
||||||
uint8_t id, uint16_t x, uint16_t y, uint8_t phase) {
|
uint8_t id, uint16_t x, uint16_t y, uint8_t phase) {
|
||||||
uint8_t buf[DS_HEADER_SIZE + sizeof(ds_touch_event_t)];
|
uint8_t buf[DS_HEADER_SIZE + sizeof(ds_touch_event_t)];
|
||||||
|
|
@ -120,52 +182,27 @@ static void send_touch_event(esp_websocket_client_handle_t ws,
|
||||||
esp_websocket_client_send_bin(ws, (const char *)buf, len, portMAX_DELAY);
|
esp_websocket_client_send_bin(ws, (const char *)buf, len, portMAX_DELAY);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Touch polling task ───
|
#endif // DS_USE_ESPNOW
|
||||||
//
|
|
||||||
// TODO: Replace with actual GT9271 I2C touch driver.
|
|
||||||
// The Waveshare BSP should provide touch reading functions.
|
|
||||||
// This is a placeholder showing the integration pattern.
|
|
||||||
//
|
|
||||||
static void touch_task(void *arg) {
|
|
||||||
esp_websocket_client_handle_t ws = (esp_websocket_client_handle_t)arg;
|
|
||||||
|
|
||||||
|
// ─── Touch polling task ───
|
||||||
|
static void touch_task(void *arg) {
|
||||||
while (1) {
|
while (1) {
|
||||||
// TODO: Read from GT9271 touch controller via I2C
|
// TODO: Read from GT9271 touch controller via I2C
|
||||||
// Example (pseudocode):
|
|
||||||
//
|
|
||||||
// gt9271_touch_data_t td;
|
// gt9271_touch_data_t td;
|
||||||
// if (gt9271_read(&td) == ESP_OK && td.num_points > 0) {
|
// if (gt9271_read(&td) == ESP_OK && td.num_points > 0) {
|
||||||
// for (int i = 0; i < td.num_points; i++) {
|
// #if DS_USE_ESPNOW
|
||||||
// send_touch_event(ws, td.points[i].id,
|
// ds_espnow_send_touch(0, 0, td.points[0].x, td.points[0].y);
|
||||||
// td.points[i].x, td.points[i].y,
|
// #else
|
||||||
// td.points[i].phase);
|
// send_touch_event(ws, ...);
|
||||||
// }
|
// #endif
|
||||||
// }
|
// }
|
||||||
|
|
||||||
vTaskDelay(pdMS_TO_TICKS(10)); // 100Hz touch polling
|
vTaskDelay(pdMS_TO_TICKS(10)); // 100Hz touch polling
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Display initialization ───
|
// ─── Display initialization ───
|
||||||
//
|
|
||||||
// TODO: Initialize MIPI DSI display using Waveshare component.
|
|
||||||
// Add `waveshare/esp_lcd_jd9365_10_1` to idf_component.yml
|
|
||||||
//
|
|
||||||
static esp_err_t display_init(void) {
|
static esp_err_t display_init(void) {
|
||||||
// TODO: Configure MIPI DSI bus and JD9365 panel driver
|
// TODO: Initialize MIPI DSI display using Waveshare component
|
||||||
// Example (pseudocode):
|
|
||||||
//
|
|
||||||
// esp_lcd_dsi_bus_config_t bus_cfg = { ... };
|
|
||||||
// esp_lcd_new_dsi_bus(&bus_cfg, &dsi_bus);
|
|
||||||
//
|
|
||||||
// esp_lcd_panel_dev_config_t panel_cfg = {
|
|
||||||
// .reset_gpio_num = ...,
|
|
||||||
// .rgb_ele_order = LCD_RGB_ELEMENT_ORDER_RGB,
|
|
||||||
// .bits_per_pixel = 16, // RGB565
|
|
||||||
// };
|
|
||||||
// esp_lcd_new_panel_jd9365_10_1(dsi_bus, &panel_cfg, &panel_handle);
|
|
||||||
// esp_lcd_panel_init(panel_handle);
|
|
||||||
|
|
||||||
ESP_LOGI(TAG, "Display initialized (%dx%d RGB565)", PANEL_WIDTH, PANEL_HEIGHT);
|
ESP_LOGI(TAG, "Display initialized (%dx%d RGB565)", PANEL_WIDTH, PANEL_HEIGHT);
|
||||||
return ESP_OK;
|
return ESP_OK;
|
||||||
}
|
}
|
||||||
|
|
@ -188,48 +225,57 @@ static void wifi_init(void) {
|
||||||
esp_wifi_set_mode(WIFI_MODE_STA);
|
esp_wifi_set_mode(WIFI_MODE_STA);
|
||||||
esp_wifi_set_config(WIFI_IF_STA, &wifi_cfg);
|
esp_wifi_set_config(WIFI_IF_STA, &wifi_cfg);
|
||||||
esp_wifi_start();
|
esp_wifi_start();
|
||||||
esp_wifi_connect();
|
|
||||||
|
|
||||||
|
#if DS_USE_ESPNOW
|
||||||
|
// For ESP-NOW: don't need to connect to AP, just start WiFi
|
||||||
|
ESP_LOGI(TAG, "WiFi started (ESP-NOW mode, no AP connection needed)");
|
||||||
|
#else
|
||||||
|
esp_wifi_connect();
|
||||||
ESP_LOGI(TAG, "WiFi connecting to %s...", WIFI_SSID);
|
ESP_LOGI(TAG, "WiFi connecting to %s...", WIFI_SSID);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Main ───
|
// ─── Main ───
|
||||||
void app_main(void) {
|
void app_main(void) {
|
||||||
ESP_LOGI(TAG, "DreamStack Thin Client v0.1");
|
#if DS_USE_ESPNOW
|
||||||
ESP_LOGI(TAG, "Panel: %dx%d @ %d bpp = %d bytes",
|
ESP_LOGI(TAG, "DreamStack Panel v0.2 (ESP-NOW + Panel IR)");
|
||||||
|
#else
|
||||||
|
ESP_LOGI(TAG, "DreamStack Panel v0.2 (WebSocket + Pixel)");
|
||||||
|
#endif
|
||||||
|
ESP_LOGI(TAG, "Display: %dx%d @ %d bpp = %d bytes",
|
||||||
PANEL_WIDTH, PANEL_HEIGHT, PIXEL_BYTES * 8, FB_SIZE);
|
PANEL_WIDTH, PANEL_HEIGHT, PIXEL_BYTES * 8, FB_SIZE);
|
||||||
|
|
||||||
// Initialize NVS (required for WiFi)
|
|
||||||
nvs_flash_init();
|
nvs_flash_init();
|
||||||
|
|
||||||
// Allocate framebuffers in PSRAM
|
// Allocate framebuffers in PSRAM (needed for pixel mode, optional for IR mode)
|
||||||
framebuffer = heap_caps_calloc(1, FB_SIZE, MALLOC_CAP_SPIRAM);
|
framebuffer = heap_caps_calloc(1, FB_SIZE, MALLOC_CAP_SPIRAM);
|
||||||
scratch_buf = heap_caps_calloc(1, FB_SIZE, MALLOC_CAP_SPIRAM);
|
scratch_buf = heap_caps_calloc(1, FB_SIZE, MALLOC_CAP_SPIRAM);
|
||||||
if (!framebuffer || !scratch_buf) {
|
if (!framebuffer || !scratch_buf) {
|
||||||
ESP_LOGE(TAG, "Failed to allocate framebuffers in PSRAM (%d bytes each)", FB_SIZE);
|
ESP_LOGE(TAG, "Failed to allocate framebuffers in PSRAM (%d bytes each)", FB_SIZE);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ESP_LOGI(TAG, "Framebuffers allocated in PSRAM (%d MB each)", FB_SIZE / (1024 * 1024));
|
|
||||||
|
|
||||||
// Initialize display
|
|
||||||
display_init();
|
display_init();
|
||||||
|
|
||||||
// Initialize WiFi
|
|
||||||
wifi_init();
|
wifi_init();
|
||||||
vTaskDelay(pdMS_TO_TICKS(3000)); // Wait for WiFi connection
|
vTaskDelay(pdMS_TO_TICKS(2000));
|
||||||
|
|
||||||
// Connect WebSocket to relay
|
#if DS_USE_ESPNOW
|
||||||
|
// ESP-NOW mode: init transport + runtime, wait for IR push
|
||||||
|
espnow_init_and_run();
|
||||||
|
#else
|
||||||
|
// WebSocket mode: connect to relay, receive pixel frames
|
||||||
esp_websocket_client_config_t ws_cfg = {
|
esp_websocket_client_config_t ws_cfg = {
|
||||||
.uri = RELAY_URL,
|
.uri = RELAY_URL,
|
||||||
.buffer_size = 64 * 1024, // 64KB receive buffer
|
.buffer_size = 64 * 1024,
|
||||||
};
|
};
|
||||||
esp_websocket_client_handle_t ws = esp_websocket_client_init(&ws_cfg);
|
esp_websocket_client_handle_t ws = esp_websocket_client_init(&ws_cfg);
|
||||||
esp_websocket_register_events(ws, WEBSOCKET_EVENT_ANY, ws_event_handler, NULL);
|
esp_websocket_register_events(ws, WEBSOCKET_EVENT_ANY, ws_event_handler, NULL);
|
||||||
esp_websocket_client_start(ws);
|
esp_websocket_client_start(ws);
|
||||||
ESP_LOGI(TAG, "WebSocket connecting to %s...", RELAY_URL);
|
ESP_LOGI(TAG, "WebSocket connecting to %s...", RELAY_URL);
|
||||||
|
|
||||||
// Start touch polling task
|
|
||||||
xTaskCreate(touch_task, "touch", 4096, ws, 5, NULL);
|
xTaskCreate(touch_task, "touch", 4096, ws, 5, NULL);
|
||||||
|
#endif
|
||||||
|
|
||||||
ESP_LOGI(TAG, "Thin client running. Waiting for frames...");
|
ESP_LOGI(TAG, "Panel running. Waiting for frames...");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
449
docs/explorations.md
Normal file
449
docs/explorations.md
Normal file
|
|
@ -0,0 +1,449 @@
|
||||||
|
# DreamStack Hardware Explorations
|
||||||
|
|
||||||
|
> Research notes on form factors, display technologies, and touch input methods for DreamStack-powered surfaces.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. USB Dongle (Chromecast-like)
|
||||||
|
|
||||||
|
The DreamStack relay protocol + delta codec already does 90% of the work. A dongle receives the bitstream and outputs to HDMI.
|
||||||
|
|
||||||
|
### Path A: ESP32-S3 HDMI Dongle (~$15 DIY)
|
||||||
|
|
||||||
|
| Component | Part | Cost |
|
||||||
|
|-----------|------|------|
|
||||||
|
| SoC | ESP32-S3-WROOM-1 (N16R8) | ~$4 |
|
||||||
|
| HDMI output | CH7035B or ADV7513 HDMI encoder IC | ~$3 |
|
||||||
|
| USB-C power | Standard power-only connector | ~$0.50 |
|
||||||
|
| PCB + passives | Custom PCB (JLCPCB) | ~$5 for 5 boards |
|
||||||
|
| HDMI connector | Type-A male or mini-HDMI | ~$1 |
|
||||||
|
|
||||||
|
- ESP32-S3 LCD parallel interface → HDMI encoder IC → HDMI out
|
||||||
|
- WiFi connects to DreamStack relay, receives delta-compressed frames
|
||||||
|
- Resolution limit: ~480×320 smooth, 800×600 at lower FPS
|
||||||
|
- Input via BLE HID remote or HDMI CEC (pin 13)
|
||||||
|
|
||||||
|
### Path B: Linux Stick / Allwinner (~$25-40)
|
||||||
|
|
||||||
|
MangoPi MQ-Pro / Radxa Zero form factor:
|
||||||
|
|
||||||
|
| Component | Part | Cost |
|
||||||
|
|-----------|------|------|
|
||||||
|
| SoC | Allwinner H616/H618 (HDMI built-in) | ~$15 module |
|
||||||
|
| RAM | 512MB DDR3 onboard | included |
|
||||||
|
| WiFi | RTL8723CS | ~$3 |
|
||||||
|
| Storage | 8GB eMMC or SD | ~$3 |
|
||||||
|
|
||||||
|
- Runs minimal Linux (Buildroot), headless browser or C receiver writing to `/dev/fb0`
|
||||||
|
- Native HDMI — no encoder IC needed
|
||||||
|
- Full DreamStack JS runtime in headless Chromium/WPE-WebKit
|
||||||
|
- CEC for remote control
|
||||||
|
|
||||||
|
### Path C: Pi Zero 2 W (~$15, recommended MVP)
|
||||||
|
|
||||||
|
Best for proving the concept immediately — $15, mini-HDMI out, WiFi, runs DreamStack natively.
|
||||||
|
|
||||||
|
```
|
||||||
|
Laptop WiFi/LAN Pi Zero 2 W (in 3D-printed HDMI case)
|
||||||
|
────── ───────── ─────────────────────────────────────
|
||||||
|
dreamstack dev app.ds headless browser / ds_runtime
|
||||||
|
→ relay-bridge ──────── WebSocket ───────→ → HDMI out to TV
|
||||||
|
←── CEC/BLE ←──── remote control
|
||||||
|
```
|
||||||
|
|
||||||
|
### Off-the-shelf stick computers
|
||||||
|
|
||||||
|
| Device | Price | HDMI | WiFi | Notes |
|
||||||
|
|--------|-------|------|------|-------|
|
||||||
|
| Raspberry Pi Zero 2 W | $15 | Mini-HDMI ✅ | ✅ | Best form factor |
|
||||||
|
| MangoPi MQ-Pro (RISC-V) | $20 | HDMI ✅ | ✅ | Stick form factor |
|
||||||
|
| Radxa Zero | $25 | Micro-HDMI ✅ | ✅ | Amlogic S905Y2 |
|
||||||
|
| T-Dongle S3 (LilyGO) | $12 | No (LCD only) | ✅ | ESP32-S3, tiny LCD |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Projected Touch Wall
|
||||||
|
|
||||||
|
### Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
SOURCE (laptop/Pi) RELAY (:9100) WALL
|
||||||
|
─────────────── ───────────── ────
|
||||||
|
DreamStack app WebSocket hub UST Projector
|
||||||
|
800×1280 canvas + touch overlay
|
||||||
|
|
||||||
|
pixels → XOR delta → RLE ────→ relay ─────────────────→ decode → project
|
||||||
|
←── touch {x,y,phase} ←── touch sensor
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ultra-Short-Throw Projectors
|
||||||
|
|
||||||
|
| Product | Price (new) | Price (used) | Notes |
|
||||||
|
|---------|-------------|-------------|-------|
|
||||||
|
| Xiaomi Laser Cinema 2 | ~$1,200 | ~$400 | Good value |
|
||||||
|
| BenQ V7050i | ~$2,500 | ~$800 | 4K HDR |
|
||||||
|
| JMGO U2 | ~$1,000 | ~$300 | Budget friendly |
|
||||||
|
| Epson LS500 | ~$2,000 | ~$600 | Bright |
|
||||||
|
|
||||||
|
Wall prep: screen paint (Silver Ticket / Rust-Oleum, ~$30).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Touch Input Technologies
|
||||||
|
|
||||||
|
Ranked from fastest to slowest latency:
|
||||||
|
|
||||||
|
### 3a. Piezoelectric Sensors (<1ms, ~$7)
|
||||||
|
|
||||||
|
Stick 3-4 piezo discs on the **back** of the wall. Finger taps create vibrations; time-difference-of-arrival (TDOA) triangulates X,Y.
|
||||||
|
|
||||||
|
| Part | Cost |
|
||||||
|
|------|------|
|
||||||
|
| 4× piezo disc (35mm) | ~$2 |
|
||||||
|
| ESP32-S3 (built-in ADC, 40kHz+ sampling) | ~$5 |
|
||||||
|
|
||||||
|
```
|
||||||
|
Wall (drywall, glass, wood, whiteboard)
|
||||||
|
┌───────────────────────────────────────┐
|
||||||
|
│ P1 ● ● P2 │
|
||||||
|
│ 👆 TAP │
|
||||||
|
│ P3 ● ● P4 │
|
||||||
|
└───────────────────────────────────────┘
|
||||||
|
└────── ESP32 (TDOA → x,y) ─────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
**Pros:** Near-instant, invisible, dirt cheap, works through paint
|
||||||
|
**Cons:** Only detects **taps** (not drag/hover), needs hard surface
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3b. Capacitive Wire/Paint Grid (1-3ms, ~$21 DIY)
|
||||||
|
|
||||||
|
Grid of conductors (copper tape or conductive paint) behind the wall. Measures capacitance change when a finger approaches.
|
||||||
|
|
||||||
|
| Part | Cost |
|
||||||
|
|------|------|
|
||||||
|
| Copper tape grid (30 channels) or graphite paint | ~$10 |
|
||||||
|
| MPR121 capacitive controller ×3 | ~$6 |
|
||||||
|
| ESP32 | ~$5 |
|
||||||
|
|
||||||
|
**Supports:** Touch ✅ Drag ✅ Multi-touch ✅ Hover (~1-2cm) ✅
|
||||||
|
**Resolution:** Depends on grid pitch — 3cm pitch ≈ 30×40 nodes over 100"
|
||||||
|
|
||||||
|
#### How Mutual Capacitance Works
|
||||||
|
|
||||||
|
Two layers of conductors (rows + columns) cross each other, separated by a thin insulator. Each intersection forms a capacitor. A finger near any intersection absorbs electric field, reducing measured capacitance.
|
||||||
|
|
||||||
|
The controller scans one row at a time (AC drive), reads all columns simultaneously. Full scan of 30×40 grid: ~0.5-1ms. Continuous scanning gives automatic drag/swipe detection. Sub-pixel interpolation from adjacent node readings gives ~1mm accuracy from 5mm pitch.
|
||||||
|
|
||||||
|
#### Recommended ICs
|
||||||
|
|
||||||
|
| IC | Grid Size | Touch Points | Price |
|
||||||
|
|----|-----------|-------------|-------|
|
||||||
|
| MTCH6303 (Microchip) | 15×49 | 10 | ~$5 |
|
||||||
|
| IQS7211A (Azoteq) | 15×22 | 5 | ~$3 |
|
||||||
|
| GT911 (Goodix) | 26×14 | 5 | ~$2 |
|
||||||
|
| FT5x06 (FocalTech) | 24×14 | 5 | ~$2 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3c. FTIR — Frustrated Total Internal Reflection (3-8ms, ~$110-250)
|
||||||
|
|
||||||
|
Acrylic sheet on wall with IR LEDs on edges (total internal reflection). Finger touch "frustrates" the reflection → bright spots detected by IR camera.
|
||||||
|
|
||||||
|
| Part | Cost |
|
||||||
|
|------|------|
|
||||||
|
| 4mm acrylic sheet (100") | ~$80-150 |
|
||||||
|
| IR LED strip (850nm) on edges | ~$10 |
|
||||||
|
| IR camera (120fps, no IR filter) | ~$15 |
|
||||||
|
| ESP32-S3 or Pi | ~$5-75 |
|
||||||
|
|
||||||
|
**Pros:** Multi-touch, precise, pressure-sensitive (brighter blob = more pressure)
|
||||||
|
**Cons:** Needs smooth flat surface (acrylic)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3d. IR Touch Frame (8-15ms, ~$250-500)
|
||||||
|
|
||||||
|
Aluminum frame with IR LEDs + sensors on 4 edges. Finger breaks IR beams → X,Y.
|
||||||
|
|
||||||
|
| Size | Price | Touch Points |
|
||||||
|
|------|-------|-------------|
|
||||||
|
| 65" | ~$250 | 6-10 pt |
|
||||||
|
| 82" | ~$350 | 10-20 pt |
|
||||||
|
| 100" | ~$500 | 10-20 pt |
|
||||||
|
| 120"+ | ~$800+ | 20+ pt |
|
||||||
|
|
||||||
|
Premium: Neonode zForce (~6-8ms, 200Hz). Budget: generic Chinese frames (~15-30ms, 100Hz).
|
||||||
|
|
||||||
|
**Pros:** USB HID plug-and-play, works on any surface
|
||||||
|
**Cons:** Physical border/bezel on wall
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3e. Depth Camera (15-30ms, ~$80-450)
|
||||||
|
|
||||||
|
| Camera | FPS | Latency | Range | Price |
|
||||||
|
|--------|-----|---------|-------|-------|
|
||||||
|
| **Intel RealSense D405** | 90fps | ~11ms | 7cm-50cm | ~$80 |
|
||||||
|
| RealSense D435i | 90fps | ~11ms | 10cm-10m | ~$200 |
|
||||||
|
| OAK-D SR (Short Range) | 60fps | ~12ms | 2cm-100cm | ~$150 |
|
||||||
|
| OAK-D Pro | 30fps depth | ~15ms | 20cm-15m | ~$200 |
|
||||||
|
| Stereolabs ZED Mini | 100fps | ~10ms | 10cm-12m | ~$300 |
|
||||||
|
| Stereolabs ZED X Mini | 120fps | ~8ms | 10cm-15m | ~$450 |
|
||||||
|
| Orbbec Gemini 2 | 60fps | ~16ms | 15cm-10m | ~$130 |
|
||||||
|
|
||||||
|
**RealSense D405** is ideal for wall touch — 90fps hardware stereo depth, 7cm minimum distance, global shutter. No ML needed for touch detection: just threshold the depth map (`depth < 5mm → TOUCH`, `< 150mm → HOVER`).
|
||||||
|
|
||||||
|
Layer MediaPipe on top (parallel) for gesture classification.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3f. Hybrid: Best of All Worlds
|
||||||
|
|
||||||
|
| Input | Method | Latency |
|
||||||
|
|-------|--------|---------|
|
||||||
|
| Tap detection + pressure | Piezo (4 corners) | <1ms |
|
||||||
|
| Touch + drag + hover | Capacitive grid | 1-3ms |
|
||||||
|
| Hand gestures (air) | RealSense D405 | ~15ms |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Gesture / Hand Tracking
|
||||||
|
|
||||||
|
### DIY Approaches (Ultraleap alternative)
|
||||||
|
|
||||||
|
#### Stereo IR Camera + MediaPipe (~$30-50)
|
||||||
|
|
||||||
|
Two OV2710 IR USB cameras (stereo pair, ~$15 each) + 850nm IR LED strip (~$5). MediaPipe Hands on Pi 5 or Jetson: 21 landmarks per hand, 30-120fps. Stereo triangulation gives Z. **Latency: ~20-30ms.**
|
||||||
|
|
||||||
|
#### Single Depth Camera (~$80-150)
|
||||||
|
|
||||||
|
Use RealSense D405 or OAK-D SR (see above). Hardware depth gives Z-distance from wall.
|
||||||
|
|
||||||
|
#### ESP32-S3 + IR Matrix (~$20, lowest latency)
|
||||||
|
|
||||||
|
IR LEDs flood the area in front of the wall. 2-3 IR cameras do blob detection at 120fps on ESP32-S3. Z estimated from blob size. No ML needed. **Latency: 5-10ms.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Conductive Paint Recipes
|
||||||
|
|
||||||
|
For capacitive grid electrodes painted directly on walls.
|
||||||
|
|
||||||
|
### Graphite Paint (easiest, ~$5)
|
||||||
|
|
||||||
|
| Ingredient | Amount | Source |
|
||||||
|
|-----------|--------|--------|
|
||||||
|
| Graphite powder (<45μm) | 3 tbsp | Art supply, Amazon (~$8/lb) |
|
||||||
|
| PVA glue (white school glue) | 2 tbsp | Any store |
|
||||||
|
| Water | 1 tbsp | Tap |
|
||||||
|
|
||||||
|
~60% graphite, 30% glue, 10% water by volume. **Resistance: ~500-2000 Ω/sq.** Good enough for capacitive sensing.
|
||||||
|
|
||||||
|
### Carbon Black + Acrylic (~$15)
|
||||||
|
|
||||||
|
20-25% carbon black powder (conductive grade) in 75-80% acrylic medium. **Resistance: ~200-800 Ω/sq.** Better adhesion. Wear mask + gloves.
|
||||||
|
|
||||||
|
### Nickel Paint (~$20)
|
||||||
|
|
||||||
|
MG Chemicals 841, premade. **Resistance: ~5-50 Ω/sq.** Mid-range.
|
||||||
|
|
||||||
|
### Silver Paint (~$30-50)
|
||||||
|
|
||||||
|
Premade: Bare Conductive (~$25/50ml), MG Chemicals 842.
|
||||||
|
DIY: 70-80% silver flake powder (<10μm), 15-20% acrylic medium, 5-10% butyl acetate.
|
||||||
|
**Resistance: ~0.5-5 Ω/sq.** Near-wire conductivity.
|
||||||
|
|
||||||
|
### For capacitive sensing: graphite is sufficient
|
||||||
|
|
||||||
|
Capacitive touch doesn't need low resistance — just enough conductivity to couple with a finger. Paint lines with tape masking at 3-5cm spacing.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Pixel Paint — Paint-On Displays
|
||||||
|
|
||||||
|
### Electroluminescent (EL) Paint Display
|
||||||
|
|
||||||
|
Real and buildable. A stack of painted layers that glow when AC voltage is applied.
|
||||||
|
|
||||||
|
```
|
||||||
|
Layer stack (painted in order):
|
||||||
|
|
||||||
|
5. Clear topcoat
|
||||||
|
4. Transparent conductor (PEDOT:PSS) ← rows
|
||||||
|
3. Phosphor layer (ZnS:Cu in acrylic) ← glows
|
||||||
|
2. Dielectric (BaTiO₃ in acrylic) ← insulator
|
||||||
|
1. Base conductor (silver/carbon paint) ← columns
|
||||||
|
─── Wall surface ───
|
||||||
|
```
|
||||||
|
|
||||||
|
Row/column intersection = one pixel. AC across a specific row+column → only that intersection glows (passive matrix).
|
||||||
|
|
||||||
|
| Layer | Material | Cost/m² |
|
||||||
|
|-------|----------|---------|
|
||||||
|
| Base conductor (columns) | Silver paint, painted in strips | ~$50 |
|
||||||
|
| Dielectric | Barium titanate (BaTiO₃) in acrylic | ~$30 |
|
||||||
|
| Phosphor | ZnS:Cu powder in acrylic | ~$20 |
|
||||||
|
| Top conductor (rows) | PEDOT:PSS | ~$40 |
|
||||||
|
| Driver electronics | HV507 shift registers + ESP32 | ~$30 |
|
||||||
|
| **Total** | | **~$170/m²** |
|
||||||
|
|
||||||
|
#### Resolution at different pitches
|
||||||
|
|
||||||
|
| Pixel Pitch | Pixels (100" wall) | Comparable To |
|
||||||
|
|------------|---------------------|---------------|
|
||||||
|
| 20mm | 110×65 = 7,150 | LED sign |
|
||||||
|
| 10mm | 220×130 = 28,600 | Scoreboard |
|
||||||
|
| 5mm | 440×260 = 114,400 | ~400×260 display ✅ |
|
||||||
|
| 2mm | 1100×650 = 715,000 | Near SD |
|
||||||
|
|
||||||
|
At 5mm pitch: 440×260 — enough for DreamStack UIs, dashboards, snake game.
|
||||||
|
|
||||||
|
#### Color
|
||||||
|
|
||||||
|
- ZnS:Cu → green (brightest)
|
||||||
|
- ZnS:Cu,Mn → amber/orange
|
||||||
|
- ZnS:Cu,Al → blue-green
|
||||||
|
- Full RGB requires 3 sub-pixels per pixel (3× driver count)
|
||||||
|
- Monochrome green is practical and looks great
|
||||||
|
|
||||||
|
#### Built-in touch (free!)
|
||||||
|
|
||||||
|
The row/column electrodes double as capacitive sensing electrodes via time-multiplexing:
|
||||||
|
1. **Sense phase** (1ms): measure capacitance = touch position
|
||||||
|
2. **Drive phase** (15ms): apply AC = illuminate pixels
|
||||||
|
|
||||||
|
Same paint layers, no extra hardware.
|
||||||
|
|
||||||
|
#### Driver IC
|
||||||
|
|
||||||
|
HV507 — 64-channel high-voltage shift register. Drives 100V+ outputs from 3.3V SPI. Chain several for full display.
|
||||||
|
|
||||||
|
### Other Display Paint Technologies (Future)
|
||||||
|
|
||||||
|
| Technology | Status | Color | Speed |
|
||||||
|
|-----------|--------|-------|-------|
|
||||||
|
| Electrochromic (PEDOT:PSS, WO₃) | Real | Grayscale | 1-30s (too slow for video) |
|
||||||
|
| Thermochromic + resistive grid | Hackable | Limited | 1-5s |
|
||||||
|
| Perovskite spray-on LEDs | Lab only | Full color | ~ms |
|
||||||
|
| QD-LED inkjet | Lab only | Full color | ~ms |
|
||||||
|
|
||||||
|
Perovskite / QD-LED spray-on is the future (~2028-2030) but not available today.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Off-the-Shelf Solutions
|
||||||
|
|
||||||
|
### Capacitive Touch Overlays (stick-on film)
|
||||||
|
|
||||||
|
| Product | Max Size | Touch Points | Latency | Price |
|
||||||
|
|---------|----------|-------------|---------|-------|
|
||||||
|
| **Displax Skin Ultra** | 105" | 40 | ~6ms | ~$800-1500 |
|
||||||
|
| Visual Planet TouchFoil | 100"+ | 40 | ~8ms | ~$600-1200 |
|
||||||
|
| PQ Labs iTouch Plus | 150"+ | 32 | ~8ms | ~$400-900 |
|
||||||
|
| AliExpress "PCAP touch foil" | 100"+ | 10 | ~10-15ms | ~$200-400 |
|
||||||
|
|
||||||
|
Displax Skin Ultra: transparent polymer film with nano-wire grid, adhesive-backed, works through 6mm of material, USB HID, detects hover at ~2cm. Stick on wall, plug USB, done.
|
||||||
|
|
||||||
|
### All-in-One Interactive Projectors
|
||||||
|
|
||||||
|
| Product | Size | Touch | Latency | Price (new) |
|
||||||
|
|---------|------|-------|---------|-------------|
|
||||||
|
| **Epson BrightLink 770Fi** | 100" | 10pt + pen | ~10ms | ~$2,500 |
|
||||||
|
| Epson BrightLink 735Fi | 100" | 10pt + pen | ~10ms | ~$2,000 |
|
||||||
|
| BenQ LW890UST | 100" | 10pt | ~12ms | ~$1,800 |
|
||||||
|
| Boxlight Mimio MiXX | 100" | 20pt | ~8ms | ~$2,200 |
|
||||||
|
|
||||||
|
**Used education projectors** (schools constantly upgrade):
|
||||||
|
|
||||||
|
| Used Option | Price |
|
||||||
|
|-------------|-------|
|
||||||
|
| Epson BrightLink 695Wi/696Ui | $300-600 |
|
||||||
|
| BenQ MW855UST+ with PointWrite | $400-700 |
|
||||||
|
| Promethean UST + ActivBoard | $300-500 |
|
||||||
|
|
||||||
|
### Interactive Flat Panels (giant touchscreen monitors)
|
||||||
|
|
||||||
|
| Product | Size | Price (new) | Price (used) |
|
||||||
|
|---------|------|-------------|-------------|
|
||||||
|
| **SMART Board MX** | 65-86" | $3,000-6,000 | $500-1,500 |
|
||||||
|
| Promethean ActivPanel | 65-86" | $3,000-5,000 | $600-1,200 |
|
||||||
|
| ViewSonic ViewBoard | 65-98" | $2,000-8,000 | $500-1,500 |
|
||||||
|
| Samsung Flip | 55-85" | $2,000-4,000 | $800-2,000 |
|
||||||
|
| Microsoft Surface Hub 2S | 50-85" | $5,000-12,000 | $1,500-3,000 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Recommended Builds
|
||||||
|
|
||||||
|
### Budget: $675
|
||||||
|
|
||||||
|
| Component | Source | Price |
|
||||||
|
|-----------|--------|-------|
|
||||||
|
| Used SMART Board 65" | eBay | ~$600 |
|
||||||
|
| Pi 5 | Official | ~$75 |
|
||||||
|
|
||||||
|
Plug HDMI + USB, run DreamStack, done.
|
||||||
|
|
||||||
|
### Mid-Range: $700
|
||||||
|
|
||||||
|
| Component | Price |
|
||||||
|
|-----------|-------|
|
||||||
|
| UST projector (used) | ~$300 |
|
||||||
|
| PCAP touch foil 100" (AliExpress) | ~$300 |
|
||||||
|
| Pi 5 | ~$75 |
|
||||||
|
| Screen paint | ~$30 |
|
||||||
|
|
||||||
|
### Premium: $1,050
|
||||||
|
|
||||||
|
| Component | Price |
|
||||||
|
|-----------|-------|
|
||||||
|
| UST projector (used) | ~$400 |
|
||||||
|
| 100" IR touch frame | ~$350 |
|
||||||
|
| RealSense D405 (gestures + hover) | ~$80 |
|
||||||
|
| Pi 5 | ~$75 |
|
||||||
|
| Piezo sensors (4 corners, tap confirm) | ~$7 |
|
||||||
|
| Screen paint | ~$30 |
|
||||||
|
|
||||||
|
Touch at 8-15ms + hover/gestures at 15ms + tap confirmation at <1ms.
|
||||||
|
|
||||||
|
### DIY Maximum: ~$200 + wall paint
|
||||||
|
|
||||||
|
| Component | Price |
|
||||||
|
|-----------|-------|
|
||||||
|
| Conductive graphite paint (capacitive grid) | ~$10 |
|
||||||
|
| MPR121/MTCH6303 cap-touch IC | ~$5 |
|
||||||
|
| ESP32-S3 | ~$5 |
|
||||||
|
| UST projector (used) | ~$300 |
|
||||||
|
|
||||||
|
Paint your own touch grid on the wall, 1-3ms latency, no frame needed.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. DreamStack Integration
|
||||||
|
|
||||||
|
All touch methods feed into the existing relay protocol:
|
||||||
|
|
||||||
|
```
|
||||||
|
Touch sensor (any method above)
|
||||||
|
→ ESP32 or Pi reads touch events
|
||||||
|
→ Encodes as DreamStack protocol:
|
||||||
|
0x01 Pointer move (x, y)
|
||||||
|
0x02 Pointer down (x, y, buttons)
|
||||||
|
0x03 Pointer up
|
||||||
|
0x10 KeyDown (keyCode)
|
||||||
|
0x20 Hover (x, y, z_distance) ← new
|
||||||
|
0x21 Swipe (direction, velocity) ← new
|
||||||
|
0x22 Pinch/Grab (state) ← new
|
||||||
|
→ WebSocket → DreamStack relay
|
||||||
|
→ App receives as signal updates
|
||||||
|
```
|
||||||
|
|
||||||
|
DreamStack syntax for handling:
|
||||||
|
|
||||||
|
```
|
||||||
|
on hover(ev) -> opacity = lerp(0.5, 1.0, ev.z)
|
||||||
|
on swipe(ev) -> navigate(if ev.dir == "left" then "/next" else "/prev")
|
||||||
|
on grab(ev) -> scale = if ev.closed then 0.9 else 1.0
|
||||||
|
```
|
||||||
259
engine/ds-screencast/capture.js
Normal file
259
engine/ds-screencast/capture.js
Normal file
|
|
@ -0,0 +1,259 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* DreamStack Screencast — CDP Capture Agent
|
||||||
|
*
|
||||||
|
* Streams any web page to DreamStack panels via Chrome DevTools Protocol.
|
||||||
|
* Zero changes to the target app — just point at any URL.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* node capture.js [url] [--headless] [--fps=N] [--quality=N]
|
||||||
|
*
|
||||||
|
* Examples:
|
||||||
|
* node capture.js http://localhost:3000
|
||||||
|
* node capture.js https://react.dev --headless --fps=30
|
||||||
|
*/
|
||||||
|
|
||||||
|
const CDP = require('chrome-remote-interface');
|
||||||
|
const { WebSocketServer } = require('ws');
|
||||||
|
const http = require('http');
|
||||||
|
const { spawn } = require('child_process');
|
||||||
|
|
||||||
|
// ─── Config ───
|
||||||
|
const TARGET_URL = process.argv[2] || 'http://localhost:3000';
|
||||||
|
const WIDTH = 800;
|
||||||
|
const HEIGHT = 1280;
|
||||||
|
const WS_PORT = 9300;
|
||||||
|
const MONITOR_PORT = 9301;
|
||||||
|
const CDP_PORT = 9222;
|
||||||
|
const QUALITY = parseInt((process.argv.find(a => a.startsWith('--quality=')) || '').split('=')[1] || '75');
|
||||||
|
const MAX_FPS = parseInt((process.argv.find(a => a.startsWith('--fps=')) || '').split('=')[1] || '30');
|
||||||
|
const HEADLESS = process.argv.includes('--headless');
|
||||||
|
|
||||||
|
const clients = new Set();
|
||||||
|
let frameCount = 0;
|
||||||
|
let bytesSent = 0;
|
||||||
|
const t0 = Date.now();
|
||||||
|
|
||||||
|
// ─── 1. Launch Chrome ───
|
||||||
|
function launchChrome() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const args = [
|
||||||
|
`--remote-debugging-port=${CDP_PORT}`,
|
||||||
|
`--window-size=${WIDTH},${HEIGHT}`,
|
||||||
|
'--disable-gpu', '--no-first-run', '--no-default-browser-check',
|
||||||
|
'--disable-extensions', '--disable-translate', '--disable-sync',
|
||||||
|
'--disable-background-networking', '--disable-default-apps',
|
||||||
|
'--mute-audio', '--no-sandbox',
|
||||||
|
];
|
||||||
|
if (HEADLESS) args.push('--headless=new');
|
||||||
|
args.push('about:blank');
|
||||||
|
|
||||||
|
const proc = spawn('google-chrome', args, { stdio: ['pipe', 'pipe', 'pipe'] });
|
||||||
|
|
||||||
|
proc.stderr.on('data', d => {
|
||||||
|
if (d.toString().includes('DevTools listening')) resolve(proc);
|
||||||
|
});
|
||||||
|
|
||||||
|
proc.on('error', reject);
|
||||||
|
proc.on('exit', code => { console.log(`[Chrome] exit ${code}`); process.exit(0); });
|
||||||
|
|
||||||
|
// Fallback timeout
|
||||||
|
setTimeout(() => resolve(proc), 4000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── 2. WebSocket server for panels/monitor ───
|
||||||
|
function startWS() {
|
||||||
|
const wss = new WebSocketServer({ host: '0.0.0.0', port: WS_PORT });
|
||||||
|
wss.on('connection', (ws, req) => {
|
||||||
|
clients.add(ws);
|
||||||
|
console.log(`[WS] +1 panel (${clients.size}) from ${req.socket.remoteAddress}`);
|
||||||
|
ws.on('close', () => { clients.delete(ws); console.log(`[WS] -1 panel (${clients.size})`); });
|
||||||
|
ws.on('message', data => { ws._inputHandler?.(data); });
|
||||||
|
});
|
||||||
|
console.log(`[WS] Panels: ws://0.0.0.0:${WS_PORT}`);
|
||||||
|
return wss;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── 3. Monitor page ───
|
||||||
|
function startMonitor() {
|
||||||
|
const html = `<!DOCTYPE html><html><head>
|
||||||
|
<title>DreamStack Screencast</title>
|
||||||
|
<style>
|
||||||
|
*{margin:0;padding:0;box-sizing:border-box}
|
||||||
|
body{background:#0a0a0a;display:flex;flex-direction:column;align-items:center;justify-content:center;height:100vh;font-family:system-ui;color:#999}
|
||||||
|
canvas{border:1px solid #333;border-radius:8px;max-height:85vh;cursor:crosshair}
|
||||||
|
#hud{margin-top:10px;font:13px/1.5 monospace;text-align:center}
|
||||||
|
h3{margin-bottom:6px;color:#555;font-size:14px}
|
||||||
|
</style></head><body>
|
||||||
|
<h3>DreamStack Screencast Monitor</h3>
|
||||||
|
<canvas id="c" width="${WIDTH}" height="${HEIGHT}"></canvas>
|
||||||
|
<div id="hud">Connecting…</div>
|
||||||
|
<script>
|
||||||
|
const c=document.getElementById('c'),ctx=c.getContext('2d'),hud=document.getElementById('hud');
|
||||||
|
c.style.width=Math.min(${WIDTH},innerWidth*.45)+'px';c.style.height='auto';
|
||||||
|
let fr=0,by=0,t=Date.now();
|
||||||
|
const ws=new WebSocket('ws://'+location.hostname+':${WS_PORT}');
|
||||||
|
ws.binaryType='arraybuffer';
|
||||||
|
ws.onopen=()=>{hud.textContent='Connected — waiting for frames…'};
|
||||||
|
ws.onmessage=e=>{
|
||||||
|
const buf=new Uint8Array(e.data);
|
||||||
|
if(buf[0]!==0x50)return;
|
||||||
|
const jpeg=buf.slice(9);fr++;by+=jpeg.length;
|
||||||
|
const blob=new Blob([jpeg],{type:'image/jpeg'});
|
||||||
|
const url=URL.createObjectURL(blob);
|
||||||
|
const img=new Image();
|
||||||
|
img.onload=()=>{ctx.drawImage(img,0,0);URL.revokeObjectURL(url)};
|
||||||
|
img.src=url;
|
||||||
|
const now=Date.now();
|
||||||
|
if(now-t>1000){
|
||||||
|
hud.textContent='FPS: '+(fr/((now-t)/1000)).toFixed(1)+' | '+(by/1024/((now-t)/1000)).toFixed(0)+' KB/s | Frame: '+(jpeg.length/1024).toFixed(1)+'KB';
|
||||||
|
fr=0;by=0;t=now;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
c.addEventListener('click',e=>{
|
||||||
|
const r=c.getBoundingClientRect(),sx=${WIDTH}/r.width,sy=${HEIGHT}/r.height;
|
||||||
|
const x=Math.round((e.clientX-r.left)*sx),y=Math.round((e.clientY-r.top)*sy);
|
||||||
|
const b=new Uint8Array(7);const dv=new DataView(b.buffer);
|
||||||
|
b[0]=0x60;b[1]=0;dv.setUint16(2,x,true);dv.setUint16(4,y,true);b[6]=0;
|
||||||
|
ws.send(b);
|
||||||
|
setTimeout(()=>{const e2=new Uint8Array(7);const d2=new DataView(e2.buffer);e2[0]=0x60;e2[1]=2;d2.setUint16(2,x,true);d2.setUint16(4,y,true);e2[6]=0;ws.send(e2)},50);
|
||||||
|
hud.textContent+=' | Click: ('+x+','+y+')';
|
||||||
|
});
|
||||||
|
ws.onclose=()=>{hud.textContent='Disconnected — reload to retry'};
|
||||||
|
</script></body></html>`;
|
||||||
|
|
||||||
|
http.createServer((_, res) => { res.writeHead(200, { 'Content-Type': 'text/html' }); res.end(html); })
|
||||||
|
.listen(MONITOR_PORT, '0.0.0.0', () => console.log(`[Monitor] http://0.0.0.0:${MONITOR_PORT}`));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── 4. CDP screencast loop ───
|
||||||
|
async function startScreencast() {
|
||||||
|
let client;
|
||||||
|
// Retry CDP connect (Chrome may still be starting)
|
||||||
|
for (let i = 0; i < 10; i++) {
|
||||||
|
try {
|
||||||
|
client = await CDP({ port: CDP_PORT });
|
||||||
|
break;
|
||||||
|
} catch {
|
||||||
|
await new Promise(r => setTimeout(r, 1000));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!client) throw new Error('Cannot connect to Chrome CDP');
|
||||||
|
|
||||||
|
const { Page, Input, Emulation } = client;
|
||||||
|
|
||||||
|
await Page.enable();
|
||||||
|
await Emulation.setDeviceMetricsOverride({
|
||||||
|
width: WIDTH, height: HEIGHT, deviceScaleFactor: 1, mobile: true,
|
||||||
|
});
|
||||||
|
await Emulation.setTouchEmulationEnabled({ enabled: true });
|
||||||
|
await Page.navigate({ url: TARGET_URL });
|
||||||
|
await new Promise(r => setTimeout(r, 2000)); // let page load
|
||||||
|
|
||||||
|
// Wire up input forwarding from panels
|
||||||
|
for (const ws of clients) {
|
||||||
|
ws._inputHandler = (data) => handleInput(Buffer.from(data), Input, Page);
|
||||||
|
}
|
||||||
|
// Also for future connections
|
||||||
|
const origAdd = clients.add.bind(clients);
|
||||||
|
clients.add = function (ws) {
|
||||||
|
origAdd(ws);
|
||||||
|
ws._inputHandler = (data) => handleInput(Buffer.from(data), Input, Page);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Start screencast
|
||||||
|
await Page.startScreencast({
|
||||||
|
format: 'jpeg', quality: QUALITY,
|
||||||
|
maxWidth: WIDTH, maxHeight: HEIGHT,
|
||||||
|
everyNthFrame: Math.max(1, Math.round(60 / MAX_FPS)),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Listen for frames via the event API
|
||||||
|
client.on('event', (message) => {
|
||||||
|
if (message.method !== 'Page.screencastFrame') return;
|
||||||
|
|
||||||
|
const { sessionId, data, metadata } = message.params;
|
||||||
|
|
||||||
|
// ACK immediately (fire-and-forget)
|
||||||
|
Page.screencastFrameAck({ sessionId }).catch(() => { });
|
||||||
|
|
||||||
|
frameCount++;
|
||||||
|
const jpegBuf = Buffer.from(data, 'base64');
|
||||||
|
bytesSent += jpegBuf.length;
|
||||||
|
|
||||||
|
// Build frame: [0x50][ts:u32LE][w:u16LE][h:u16LE][jpeg...]
|
||||||
|
const hdr = Buffer.alloc(9);
|
||||||
|
hdr[0] = 0x50;
|
||||||
|
hdr.writeUInt32LE((Date.now() - t0) >>> 0, 1);
|
||||||
|
hdr.writeUInt16LE(metadata.deviceWidth || WIDTH, 5);
|
||||||
|
hdr.writeUInt16LE(metadata.deviceHeight || HEIGHT, 7);
|
||||||
|
const frame = Buffer.concat([hdr, jpegBuf]);
|
||||||
|
|
||||||
|
// Broadcast
|
||||||
|
for (const ws of clients) {
|
||||||
|
if (ws.readyState === 1) ws.send(frame);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (frameCount % 60 === 0) {
|
||||||
|
const elapsed = (Date.now() - t0) / 1000;
|
||||||
|
console.log(`[Cast] #${frameCount} | ${(jpegBuf.length / 1024).toFixed(1)}KB | avg ${(bytesSent / 1024 / elapsed).toFixed(0)} KB/s | ${clients.size} panels`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`[CDP] Casting ${TARGET_URL} → ${WIDTH}×${HEIGHT} @ q${QUALITY}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Input handler ───
|
||||||
|
function handleInput(buf, Input, Page) {
|
||||||
|
if (buf.length < 1) return;
|
||||||
|
const t = buf[0];
|
||||||
|
|
||||||
|
if (t === 0x60 && buf.length >= 7) {
|
||||||
|
const phase = buf[1];
|
||||||
|
const x = buf.readUInt16LE(2), y = buf.readUInt16LE(4);
|
||||||
|
const type = phase === 0 ? 'touchStart' : phase === 1 ? 'touchMove' : 'touchEnd';
|
||||||
|
Input.dispatchTouchEvent({
|
||||||
|
type, touchPoints: [{ x, y, id: buf[6] || 0, radiusX: 10, radiusY: 10, force: phase === 2 ? 0 : 1 }]
|
||||||
|
}).catch(() => { });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (t === 0x61 && buf.length >= 6) {
|
||||||
|
const x = buf.readUInt16LE(1), y = buf.readUInt16LE(3);
|
||||||
|
Input.dispatchMouseEvent({ type: 'mousePressed', x, y, button: 'left', clickCount: 1 }).catch(() => { });
|
||||||
|
setTimeout(() => Input.dispatchMouseEvent({ type: 'mouseReleased', x, y, button: 'left', clickCount: 1 }).catch(() => { }), 50);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (t === 0x63 && buf.length >= 3) {
|
||||||
|
const len = buf.readUInt16LE(1);
|
||||||
|
const url = buf.slice(3, 3 + len).toString();
|
||||||
|
Page.navigate({ url }).catch(() => { });
|
||||||
|
console.log(`[Nav] → ${url}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Main ───
|
||||||
|
async function main() {
|
||||||
|
console.log(`\n DreamStack Screencast`);
|
||||||
|
console.log(` ─────────────────────`);
|
||||||
|
console.log(` URL: ${TARGET_URL}`);
|
||||||
|
console.log(` Viewport: ${WIDTH}×${HEIGHT}`);
|
||||||
|
console.log(` Quality: ${QUALITY}% FPS: ${MAX_FPS}`);
|
||||||
|
console.log(` Headless: ${HEADLESS}\n`);
|
||||||
|
|
||||||
|
const chrome = await launchChrome();
|
||||||
|
startWS();
|
||||||
|
startMonitor();
|
||||||
|
await startScreencast();
|
||||||
|
|
||||||
|
console.log(`\n ✓ Streaming! Panels → ws://0.0.0.0:${WS_PORT}`);
|
||||||
|
console.log(` ✓ Monitor → http://localhost:${MONITOR_PORT}\n`);
|
||||||
|
|
||||||
|
process.on('SIGINT', () => {
|
||||||
|
console.log('\n[Stop]');
|
||||||
|
chrome.kill();
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(err => { console.error('Fatal:', err); process.exit(1); });
|
||||||
1
engine/ds-screencast/node_modules/.bin/chrome-remote-interface
generated
vendored
Symbolic link
1
engine/ds-screencast/node_modules/.bin/chrome-remote-interface
generated
vendored
Symbolic link
|
|
@ -0,0 +1 @@
|
||||||
|
../chrome-remote-interface/bin/client.js
|
||||||
69
engine/ds-screencast/node_modules/.package-lock.json
generated
vendored
Normal file
69
engine/ds-screencast/node_modules/.package-lock.json
generated
vendored
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
{
|
||||||
|
"name": "ds-screencast",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"node_modules/chrome-remote-interface": {
|
||||||
|
"version": "0.34.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/chrome-remote-interface/-/chrome-remote-interface-0.34.0.tgz",
|
||||||
|
"integrity": "sha512-rTTcTZ3zemx8I+nvBii7d8BAF0Ms8LLEroypfvwwZOwSpyNGLE28nStXyCA6VwGp2YSQfmCrQH21F/E+oBFvMw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"commander": "2.11.x",
|
||||||
|
"ws": "^7.2.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"chrome-remote-interface": "bin/client.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/chrome-remote-interface/node_modules/ws": {
|
||||||
|
"version": "7.5.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz",
|
||||||
|
"integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.3.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"bufferutil": "^4.0.1",
|
||||||
|
"utf-8-validate": "^5.0.2"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"bufferutil": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"utf-8-validate": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/commander": {
|
||||||
|
"version": "2.11.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/commander/-/commander-2.11.0.tgz",
|
||||||
|
"integrity": "sha512-b0553uYA5YAEGgyYIGYROzKQ7X5RAqedkfjiZxwi0kL1g3bOaBNNZfYkzt/CL0umgD5wc9Jec2FbB98CjkMRvQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/ws": {
|
||||||
|
"version": "8.19.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
|
||||||
|
"integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
18
engine/ds-screencast/node_modules/chrome-remote-interface/LICENSE
generated
vendored
Normal file
18
engine/ds-screencast/node_modules/chrome-remote-interface/LICENSE
generated
vendored
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
Copyright (c) 2026 Andrea Cardaci <cyrus.and@gmail.com>
|
||||||
|
|
||||||
|
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.
|
||||||
991
engine/ds-screencast/node_modules/chrome-remote-interface/README.md
generated
vendored
Normal file
991
engine/ds-screencast/node_modules/chrome-remote-interface/README.md
generated
vendored
Normal file
|
|
@ -0,0 +1,991 @@
|
||||||
|
# chrome-remote-interface
|
||||||
|
|
||||||
|
[](https://github.com/cyrus-and/chrome-remote-interface/actions?query=workflow:CI)
|
||||||
|
|
||||||
|
[Chrome Debugging Protocol] interface that helps to instrument Chrome (or any
|
||||||
|
other suitable [implementation](#implementations)) by providing a simple
|
||||||
|
abstraction of commands and notifications using a straightforward JavaScript
|
||||||
|
API.
|
||||||
|
|
||||||
|
## Sample API usage
|
||||||
|
|
||||||
|
The following snippet loads `https://github.com` and dumps every request made:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const CDP = require('chrome-remote-interface');
|
||||||
|
|
||||||
|
async function example() {
|
||||||
|
let client;
|
||||||
|
try {
|
||||||
|
// connect to endpoint
|
||||||
|
client = await CDP();
|
||||||
|
// extract domains
|
||||||
|
const {Network, Page} = client;
|
||||||
|
// setup handlers
|
||||||
|
Network.requestWillBeSent((params) => {
|
||||||
|
console.log(params.request.url);
|
||||||
|
});
|
||||||
|
// enable events then start!
|
||||||
|
await Network.enable();
|
||||||
|
await Page.enable();
|
||||||
|
await Page.navigate({url: 'https://github.com'});
|
||||||
|
await Page.loadEventFired();
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
} finally {
|
||||||
|
if (client) {
|
||||||
|
await client.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
example();
|
||||||
|
```
|
||||||
|
|
||||||
|
Find more examples in the [wiki]. You may also want to take a look at the [FAQ].
|
||||||
|
|
||||||
|
[wiki]: https://github.com/cyrus-and/chrome-remote-interface/wiki
|
||||||
|
[async-await-example]: https://github.com/cyrus-and/chrome-remote-interface/wiki/Async-await-example
|
||||||
|
[FAQ]: https://github.com/cyrus-and/chrome-remote-interface#faq
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
npm install chrome-remote-interface
|
||||||
|
|
||||||
|
Install globally (`-g`) to just use the [bundled client](#bundled-client).
|
||||||
|
|
||||||
|
## Implementations
|
||||||
|
|
||||||
|
This module should work with every application implementing the
|
||||||
|
[Chrome Debugging Protocol]. In particular, it has been tested against the
|
||||||
|
following implementations:
|
||||||
|
|
||||||
|
Implementation | Protocol version | [Protocol] | [List] | [New] | [Activate] | [Close] | [Version]
|
||||||
|
---------------------------|--------------------|------------|--------|-------|------------|---------|-----------
|
||||||
|
[Chrome][1.1] | [tip-of-tree][1.2] | yes¹ | yes | yes | yes | yes | yes
|
||||||
|
[Opera][2.1] | [tip-of-tree][2.2] | yes | yes | yes | yes | yes | yes
|
||||||
|
[Node.js][3.1] ([v6.3.0]+) | [node][3.2] | yes | no | no | no | no | yes
|
||||||
|
[Safari (iOS)][4.1] | [*partial*][4.2] | no | yes | no | no | no | no
|
||||||
|
[Edge][5.1] | [*partial*][5.2] | yes | yes | no | no | no | yes
|
||||||
|
[Firefox (Nightly)][6.1] | [*partial*][6.2] | yes | yes | no | yes | yes | yes
|
||||||
|
|
||||||
|
¹ Not available on [Chrome for Android][chrome-mobile-protocol], hence a local version of the protocol must be used.
|
||||||
|
|
||||||
|
[chrome-mobile-protocol]: https://bugs.chromium.org/p/chromium/issues/detail?id=824626#c4
|
||||||
|
|
||||||
|
[1.1]: #chromechromium
|
||||||
|
[1.2]: https://chromedevtools.github.io/devtools-protocol/tot/
|
||||||
|
|
||||||
|
[2.1]: #opera
|
||||||
|
[2.2]: https://chromedevtools.github.io/devtools-protocol/tot/
|
||||||
|
|
||||||
|
[3.1]: #nodejs
|
||||||
|
[3.2]: https://chromedevtools.github.io/devtools-protocol/v8/
|
||||||
|
|
||||||
|
[4.1]: #safari-ios
|
||||||
|
[4.2]: http://trac.webkit.org/browser/trunk/Source/JavaScriptCore/inspector/protocol
|
||||||
|
|
||||||
|
[5.1]: #edge
|
||||||
|
[5.2]: https://docs.microsoft.com/en-us/microsoft-edge/devtools-protocol/0.1/domains/
|
||||||
|
|
||||||
|
[6.1]: #firefox-nightly
|
||||||
|
[6.2]: https://firefox-source-docs.mozilla.org/remote/index.html
|
||||||
|
|
||||||
|
[v6.3.0]: https://nodejs.org/en/blog/release/v6.3.0/
|
||||||
|
|
||||||
|
[Protocol]: #cdpprotocoloptions-callback
|
||||||
|
[List]: #cdplistoptions-callback
|
||||||
|
[New]: #cdpnewoptions-callback
|
||||||
|
[Activate]: #cdpactivateoptions-callback
|
||||||
|
[Close]: #cdpcloseoptions-callback
|
||||||
|
[Version]: #cdpversionoptions-callback
|
||||||
|
|
||||||
|
The meaning of *target* varies according to the implementation, for example,
|
||||||
|
each Chrome tab represents a target whereas for Node.js a target is the
|
||||||
|
currently inspected script.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
An instance of either Chrome itself or another implementation needs to be
|
||||||
|
running on a known port in order to use this module (defaults to
|
||||||
|
`localhost:9222`).
|
||||||
|
|
||||||
|
### Chrome/Chromium
|
||||||
|
|
||||||
|
#### Desktop
|
||||||
|
|
||||||
|
Start Chrome with the `--remote-debugging-port` option, for example:
|
||||||
|
|
||||||
|
google-chrome --remote-debugging-port=9222
|
||||||
|
|
||||||
|
##### Headless
|
||||||
|
|
||||||
|
Since version 59, additionally use the `--headless` option, for example:
|
||||||
|
|
||||||
|
google-chrome --headless --remote-debugging-port=9222
|
||||||
|
|
||||||
|
#### Android
|
||||||
|
|
||||||
|
Plug the device and make sure to authorize the connection from the device itself. Then
|
||||||
|
enable the port forwarding, for example:
|
||||||
|
|
||||||
|
adb -d forward tcp:9222 localabstract:chrome_devtools_remote
|
||||||
|
|
||||||
|
After that you should be able to use `http://127.0.0.1:9222` as usual, but note that in
|
||||||
|
Android, Chrome does not have its own protocol available, so a local version must be used.
|
||||||
|
See [here](#chrome-debugging-protocol-versions) for more information.
|
||||||
|
|
||||||
|
##### WebView
|
||||||
|
|
||||||
|
In order to be inspectable, a WebView must
|
||||||
|
be [configured for debugging][webview] and the corresponding process ID must be
|
||||||
|
known. There are several ways to obtain it, for example:
|
||||||
|
|
||||||
|
adb shell grep -a webview_devtools_remote /proc/net/unix
|
||||||
|
|
||||||
|
Finally, port forwarding can be enabled as follows:
|
||||||
|
|
||||||
|
adb forward tcp:9222 localabstract:webview_devtools_remote_<pid>
|
||||||
|
|
||||||
|
[webview]: https://developers.google.com/web/tools/chrome-devtools/remote-debugging/webviews#configure_webviews_for_debugging
|
||||||
|
|
||||||
|
### Opera
|
||||||
|
|
||||||
|
Start Opera with the `--remote-debugging-port` option, for example:
|
||||||
|
|
||||||
|
opera --remote-debugging-port=9222
|
||||||
|
|
||||||
|
### Node.js
|
||||||
|
|
||||||
|
Start Node.js with the `--inspect` option, for example:
|
||||||
|
|
||||||
|
node --inspect=9222 script.js
|
||||||
|
|
||||||
|
### Safari (iOS)
|
||||||
|
|
||||||
|
Install and run the [iOS WebKit Debug Proxy][iwdp]. Then use it with the `local`
|
||||||
|
option set to `true` to use the local version of the protocol or pass a custom
|
||||||
|
descriptor upon connection (`protocol` option).
|
||||||
|
|
||||||
|
[iwdp]: https://github.com/google/ios-webkit-debug-proxy
|
||||||
|
|
||||||
|
### Edge
|
||||||
|
|
||||||
|
Start Edge with the `--devtools-server-port` option, for example:
|
||||||
|
|
||||||
|
MicrosoftEdge.exe --devtools-server-port 9222 about:blank
|
||||||
|
|
||||||
|
Please find more information [here][edge-devtools].
|
||||||
|
|
||||||
|
[edge-devtools]: https://docs.microsoft.com/en-us/microsoft-edge/devtools-protocol/
|
||||||
|
|
||||||
|
### Firefox (Nightly)
|
||||||
|
|
||||||
|
Start Firefox with the `--remote-debugging-port` option, for example:
|
||||||
|
|
||||||
|
firefox --remote-debugging-port 9222
|
||||||
|
|
||||||
|
Bear in mind that this is an experimental feature of Firefox.
|
||||||
|
|
||||||
|
## Bundled client
|
||||||
|
|
||||||
|
This module comes with a bundled client application that can be used to
|
||||||
|
interactively control a remote instance.
|
||||||
|
|
||||||
|
### Target management
|
||||||
|
|
||||||
|
The bundled client exposes subcommands to interact with the HTTP frontend
|
||||||
|
(e.g., [List](#cdplistoptions-callback), [New](#cdpnewoptions-callback), etc.),
|
||||||
|
run with `--help` to display the list of available options.
|
||||||
|
|
||||||
|
Here are some examples:
|
||||||
|
|
||||||
|
```js
|
||||||
|
$ chrome-remote-interface new 'http://example.com'
|
||||||
|
{
|
||||||
|
"description": "",
|
||||||
|
"devtoolsFrontendUrl": "/devtools/inspector.html?ws=localhost:9222/devtools/page/b049bb56-de7d-424c-a331-6ae44cf7ae01",
|
||||||
|
"id": "b049bb56-de7d-424c-a331-6ae44cf7ae01",
|
||||||
|
"thumbnailUrl": "/thumb/b049bb56-de7d-424c-a331-6ae44cf7ae01",
|
||||||
|
"title": "",
|
||||||
|
"type": "page",
|
||||||
|
"url": "http://example.com/",
|
||||||
|
"webSocketDebuggerUrl": "ws://localhost:9222/devtools/page/b049bb56-de7d-424c-a331-6ae44cf7ae01"
|
||||||
|
}
|
||||||
|
$ chrome-remote-interface close 'b049bb56-de7d-424c-a331-6ae44cf7ae01'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Inspection
|
||||||
|
|
||||||
|
Using the `inspect` subcommand it is possible to perform [command execution](#clientdomainmethodparams-callback)
|
||||||
|
and [event binding](#clientdomaineventcallback) in a REPL fashion that provides completion.
|
||||||
|
|
||||||
|
Here is a sample session:
|
||||||
|
|
||||||
|
```js
|
||||||
|
$ chrome-remote-interface inspect
|
||||||
|
>>> Runtime.evaluate({expression: 'window.location.toString()'})
|
||||||
|
{ result: { type: 'string', value: 'about:blank' } }
|
||||||
|
>>> Page.enable()
|
||||||
|
{}
|
||||||
|
>>> Page.loadEventFired(console.log)
|
||||||
|
[Function]
|
||||||
|
>>> Page.navigate({url: 'https://github.com'})
|
||||||
|
{ frameId: 'E1657E22F06E6E0BE13DFA8130C20298',
|
||||||
|
loaderId: '439236ADE39978F98C20E8939A32D3A5' }
|
||||||
|
>>> { timestamp: 7454.721299 } // from Page.loadEventFired
|
||||||
|
>>> Runtime.evaluate({expression: 'window.location.toString()'})
|
||||||
|
{ result: { type: 'string', value: 'https://github.com/' } }
|
||||||
|
```
|
||||||
|
|
||||||
|
Additionally there are some custom commands available:
|
||||||
|
|
||||||
|
```js
|
||||||
|
>>> .help
|
||||||
|
[...]
|
||||||
|
.reset Remove all the registered event handlers
|
||||||
|
.target Display the current target
|
||||||
|
```
|
||||||
|
|
||||||
|
## Embedded documentation
|
||||||
|
|
||||||
|
In both the REPL and the regular API every object of the protocol is *decorated*
|
||||||
|
with the meta information found within the descriptor. In addition The
|
||||||
|
`category` field is added, which determines if the member is a `command`, an
|
||||||
|
`event` or a `type`.
|
||||||
|
|
||||||
|
For example to learn how to call `Page.navigate`:
|
||||||
|
|
||||||
|
```js
|
||||||
|
>>> Page.navigate
|
||||||
|
{ [Function]
|
||||||
|
category: 'command',
|
||||||
|
parameters: { url: { type: 'string', description: 'URL to navigate the page to.' } },
|
||||||
|
returns:
|
||||||
|
[ { name: 'frameId',
|
||||||
|
'$ref': 'FrameId',
|
||||||
|
hidden: true,
|
||||||
|
description: 'Frame id that will be navigated.' } ],
|
||||||
|
description: 'Navigates current page to the given URL.',
|
||||||
|
handlers: [ 'browser', 'renderer' ] }
|
||||||
|
```
|
||||||
|
|
||||||
|
To learn about the parameters returned by the `Network.requestWillBeSent` event:
|
||||||
|
|
||||||
|
```js
|
||||||
|
>>> Network.requestWillBeSent
|
||||||
|
{ [Function]
|
||||||
|
category: 'event',
|
||||||
|
description: 'Fired when page is about to send HTTP request.',
|
||||||
|
parameters:
|
||||||
|
{ requestId: { '$ref': 'RequestId', description: 'Request identifier.' },
|
||||||
|
frameId:
|
||||||
|
{ '$ref': 'Page.FrameId',
|
||||||
|
description: 'Frame identifier.',
|
||||||
|
hidden: true },
|
||||||
|
loaderId: { '$ref': 'LoaderId', description: 'Loader identifier.' },
|
||||||
|
documentURL:
|
||||||
|
{ type: 'string',
|
||||||
|
description: 'URL of the document this request is loaded for.' },
|
||||||
|
request: { '$ref': 'Request', description: 'Request data.' },
|
||||||
|
timestamp: { '$ref': 'Timestamp', description: 'Timestamp.' },
|
||||||
|
wallTime:
|
||||||
|
{ '$ref': 'Timestamp',
|
||||||
|
hidden: true,
|
||||||
|
description: 'UTC Timestamp.' },
|
||||||
|
initiator: { '$ref': 'Initiator', description: 'Request initiator.' },
|
||||||
|
redirectResponse:
|
||||||
|
{ optional: true,
|
||||||
|
'$ref': 'Response',
|
||||||
|
description: 'Redirect response data.' },
|
||||||
|
type:
|
||||||
|
{ '$ref': 'Page.ResourceType',
|
||||||
|
optional: true,
|
||||||
|
hidden: true,
|
||||||
|
description: 'Type of this resource.' } } }
|
||||||
|
```
|
||||||
|
|
||||||
|
To inspect the `Network.Request` (note that unlike commands and events, types
|
||||||
|
are named in upper camel case) type:
|
||||||
|
|
||||||
|
```js
|
||||||
|
>>> Network.Request
|
||||||
|
{ category: 'type',
|
||||||
|
id: 'Request',
|
||||||
|
type: 'object',
|
||||||
|
description: 'HTTP request data.',
|
||||||
|
properties:
|
||||||
|
{ url: { type: 'string', description: 'Request URL.' },
|
||||||
|
method: { type: 'string', description: 'HTTP request method.' },
|
||||||
|
headers: { '$ref': 'Headers', description: 'HTTP request headers.' },
|
||||||
|
postData:
|
||||||
|
{ type: 'string',
|
||||||
|
optional: true,
|
||||||
|
description: 'HTTP POST request data.' },
|
||||||
|
mixedContentType:
|
||||||
|
{ optional: true,
|
||||||
|
type: 'string',
|
||||||
|
enum: [Object],
|
||||||
|
description: 'The mixed content status of the request, as defined in http://www.w3.org/TR/mixed-content/' },
|
||||||
|
initialPriority:
|
||||||
|
{ '$ref': 'ResourcePriority',
|
||||||
|
description: 'Priority of the resource request at the time request is sent.' } } }
|
||||||
|
```
|
||||||
|
|
||||||
|
## Chrome Debugging Protocol versions
|
||||||
|
|
||||||
|
By default `chrome-remote-interface` *asks* the remote instance to provide its
|
||||||
|
own protocol.
|
||||||
|
|
||||||
|
This behavior can be changed by setting the `local` option to `true`
|
||||||
|
upon [connection](#cdpoptions-callback), in which case the [local version] of
|
||||||
|
the protocol descriptor is used. This file is manually updated from time to time
|
||||||
|
using `scripts/update-protocol.sh` and pushed to this repository.
|
||||||
|
|
||||||
|
To further override the above behavior there are basically two options:
|
||||||
|
|
||||||
|
- pass a custom protocol descriptor upon [connection](#cdpoptions-callback)
|
||||||
|
(`protocol` option);
|
||||||
|
|
||||||
|
- use the *raw* version of the [commands](#clientsendmethod-params-callback)
|
||||||
|
and [events](#event-domainmethod) interface to use bleeding-edge features that
|
||||||
|
do not appear in the [local version] of the protocol descriptor;
|
||||||
|
|
||||||
|
[local version]: lib/protocol.json
|
||||||
|
|
||||||
|
## Browser usage
|
||||||
|
|
||||||
|
This module is able to run within a web context, with obvious limitations
|
||||||
|
though, namely external HTTP requests
|
||||||
|
([List](#cdplistoptions-callback), [New](#cdpnewoptions-callback), etc.) cannot
|
||||||
|
be performed directly, for this reason the user must provide a global
|
||||||
|
`criRequest` in order to use them:
|
||||||
|
|
||||||
|
```js
|
||||||
|
function criRequest(options, callback) {}
|
||||||
|
```
|
||||||
|
|
||||||
|
`options` is the same object used by the Node.js `http` module and `callback` is
|
||||||
|
a function taking two arguments: `err` (JavaScript `Error` object or `null`) and
|
||||||
|
`data` (string result).
|
||||||
|
|
||||||
|
### Using [webpack](https://webpack.github.io/)
|
||||||
|
|
||||||
|
It just works, simply require this module:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const CDP = require('chrome-remote-interface');
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using *vanilla* JavaScript
|
||||||
|
|
||||||
|
To generate a JavaScript file that can be used with a `<script>` element:
|
||||||
|
|
||||||
|
1. run `npm install` from the root directory;
|
||||||
|
|
||||||
|
2. manually run webpack with:
|
||||||
|
|
||||||
|
TARGET=var npm run webpack
|
||||||
|
|
||||||
|
3. use as:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<script>
|
||||||
|
function criRequest(options, callback) { /*...*/ }
|
||||||
|
</script>
|
||||||
|
<script src="chrome-remote-interface.js"></script>
|
||||||
|
```
|
||||||
|
|
||||||
|
## TypeScript Support
|
||||||
|
|
||||||
|
[TypeScript][] definitions are kindly provided by [Khairul Azhar Kasmiran][] and [Seth Westphal][], and can be installed from [DefinitelyTyped][]:
|
||||||
|
|
||||||
|
```
|
||||||
|
npm install --save-dev @types/chrome-remote-interface
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that the TypeScript definitions are automatically generated from the npm package `devtools-protocol@0.0.927104`. For other versions of devtools-protocol:
|
||||||
|
|
||||||
|
1. Install patch-package using [the instructions given](https://github.com/ds300/patch-package#set-up).
|
||||||
|
2. Copy the contents of the corresponding https://github.com/ChromeDevTools/devtools-protocol/tree/master/types folder (according to commit) into `node_modules/devtools-protocol/types`.
|
||||||
|
3. Run `npx patch-package devtools-protocol` so that the changes persist across an `npm install`.
|
||||||
|
|
||||||
|
[TypeScript]: https://www.typescriptlang.org/
|
||||||
|
[Khairul Azhar Kasmiran]: https://github.com/kazarmy
|
||||||
|
[Seth Westphal]: https://github.com/westy92
|
||||||
|
[DefinitelyTyped]: https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/chrome-remote-interface
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
The API consists of three parts:
|
||||||
|
|
||||||
|
- *DevTools* methods (for those [implementations](#implementations) that support
|
||||||
|
them, e.g., [List](#cdplistoptions-callback), [New](#cdpnewoptions-callback),
|
||||||
|
etc.);
|
||||||
|
|
||||||
|
- [connection](#cdpoptions-callback) establishment;
|
||||||
|
|
||||||
|
- the actual [protocol interaction](#class-cdp).
|
||||||
|
|
||||||
|
### CDP([options], [callback])
|
||||||
|
|
||||||
|
Connects to a remote instance using the [Chrome Debugging Protocol].
|
||||||
|
|
||||||
|
`options` is an object with the following optional properties:
|
||||||
|
|
||||||
|
- `host`: HTTP frontend host. Defaults to `localhost`;
|
||||||
|
- `port`: HTTP frontend port. Defaults to `9222`;
|
||||||
|
- `secure`: HTTPS/WSS frontend. Defaults to `false`;
|
||||||
|
- `useHostName`: do not perform a DNS lookup of the host. Defaults to `false`;
|
||||||
|
- `alterPath`: a `function` taking and returning the path fragment of a URL
|
||||||
|
before that a request happens. Defaults to the identity function;
|
||||||
|
- `target`: determines which target this client should attach to. The behavior
|
||||||
|
changes according to the type:
|
||||||
|
|
||||||
|
- a `function` that takes the array returned by the `List` method and returns
|
||||||
|
a target or its numeric index relative to the array;
|
||||||
|
- a target `object` like those returned by the `New` and `List` methods;
|
||||||
|
- a `string` representing the raw WebSocket URL, in this case `host` and
|
||||||
|
`port` are not used to fetch the target list, yet they are used to complete
|
||||||
|
the URL if relative;
|
||||||
|
- a `string` representing the target id.
|
||||||
|
|
||||||
|
Defaults to a function which returns the first available target according to
|
||||||
|
the implementation (note that at most one connection can be established to the
|
||||||
|
same target);
|
||||||
|
- `protocol`: [Chrome Debugging Protocol] descriptor object. Defaults to use the
|
||||||
|
protocol chosen according to the `local` option;
|
||||||
|
- `local`: a boolean indicating whether the protocol must be fetched *remotely*
|
||||||
|
or if the local version must be used. It has no effect if the `protocol`
|
||||||
|
option is set. Defaults to `false`.
|
||||||
|
|
||||||
|
These options are also valid properties of all the instances of the `CDP`
|
||||||
|
class. In addition to that, the `webSocketUrl` field contains the currently used
|
||||||
|
WebSocket URL.
|
||||||
|
|
||||||
|
`callback` is a listener automatically added to the `connect` event of the
|
||||||
|
returned `EventEmitter`. When `callback` is omitted a `Promise` object is
|
||||||
|
returned which becomes fulfilled if the `connect` event is triggered and
|
||||||
|
rejected if the `error` event is triggered.
|
||||||
|
|
||||||
|
The `EventEmitter` supports the following events:
|
||||||
|
|
||||||
|
#### Event: 'connect'
|
||||||
|
|
||||||
|
```js
|
||||||
|
function (client) {}
|
||||||
|
```
|
||||||
|
|
||||||
|
Emitted when the connection to the WebSocket is established.
|
||||||
|
|
||||||
|
`client` is an instance of the `CDP` class.
|
||||||
|
|
||||||
|
#### Event: 'error'
|
||||||
|
|
||||||
|
```js
|
||||||
|
function (err) {}
|
||||||
|
```
|
||||||
|
|
||||||
|
Emitted when `http://host:port/json` cannot be reached or if it is not possible
|
||||||
|
to connect to the WebSocket.
|
||||||
|
|
||||||
|
`err` is an instance of `Error`.
|
||||||
|
|
||||||
|
### CDP.Protocol([options], [callback])
|
||||||
|
|
||||||
|
Fetch the [Chrome Debugging Protocol] descriptor.
|
||||||
|
|
||||||
|
`options` is an object with the following optional properties:
|
||||||
|
|
||||||
|
- `host`: HTTP frontend host. Defaults to `localhost`;
|
||||||
|
- `port`: HTTP frontend port. Defaults to `9222`;
|
||||||
|
- `secure`: HTTPS/WSS frontend. Defaults to `false`;
|
||||||
|
- `useHostName`: do not perform a DNS lookup of the host. Defaults to `false`;
|
||||||
|
- `alterPath`: a `function` taking and returning the path fragment of a URL
|
||||||
|
before that a request happens. Defaults to the identity function;
|
||||||
|
- `local`: a boolean indicating whether the protocol must be fetched *remotely*
|
||||||
|
or if the local version must be returned. Defaults to `false`.
|
||||||
|
|
||||||
|
`callback` is executed when the protocol is fetched, it gets the following
|
||||||
|
arguments:
|
||||||
|
|
||||||
|
- `err`: a `Error` object indicating the success status;
|
||||||
|
- `protocol`: the [Chrome Debugging Protocol] descriptor.
|
||||||
|
|
||||||
|
When `callback` is omitted a `Promise` object is returned.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const CDP = require('chrome-remote-interface');
|
||||||
|
CDP.Protocol((err, protocol) => {
|
||||||
|
if (!err) {
|
||||||
|
console.log(JSON.stringify(protocol, null, 4));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### CDP.List([options], [callback])
|
||||||
|
|
||||||
|
Request the list of the available open targets/tabs of the remote instance.
|
||||||
|
|
||||||
|
`options` is an object with the following optional properties:
|
||||||
|
|
||||||
|
- `host`: HTTP frontend host. Defaults to `localhost`;
|
||||||
|
- `port`: HTTP frontend port. Defaults to `9222`;
|
||||||
|
- `secure`: HTTPS/WSS frontend. Defaults to `false`;
|
||||||
|
- `useHostName`: do not perform a DNS lookup of the host. Defaults to `false`;
|
||||||
|
- `alterPath`: a `function` taking and returning the path fragment of a URL
|
||||||
|
before that a request happens. Defaults to the identity function.
|
||||||
|
|
||||||
|
`callback` is executed when the list is correctly received, it gets the
|
||||||
|
following arguments:
|
||||||
|
|
||||||
|
- `err`: a `Error` object indicating the success status;
|
||||||
|
- `targets`: the array returned by `http://host:port/json/list` containing the
|
||||||
|
target list.
|
||||||
|
|
||||||
|
When `callback` is omitted a `Promise` object is returned.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const CDP = require('chrome-remote-interface');
|
||||||
|
CDP.List((err, targets) => {
|
||||||
|
if (!err) {
|
||||||
|
console.log(targets);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### CDP.New([options], [callback])
|
||||||
|
|
||||||
|
Create a new target/tab in the remote instance.
|
||||||
|
|
||||||
|
`options` is an object with the following optional properties:
|
||||||
|
|
||||||
|
- `host`: HTTP frontend host. Defaults to `localhost`;
|
||||||
|
- `port`: HTTP frontend port. Defaults to `9222`;
|
||||||
|
- `secure`: HTTPS/WSS frontend. Defaults to `false`;
|
||||||
|
- `useHostName`: do not perform a DNS lookup of the host. Defaults to `false`;
|
||||||
|
- `alterPath`: a `function` taking and returning the path fragment of a URL
|
||||||
|
before that a request happens. Defaults to the identity function;
|
||||||
|
- `url`: URL to load in the new target/tab. Defaults to `about:blank`.
|
||||||
|
|
||||||
|
`callback` is executed when the target is created, it gets the following
|
||||||
|
arguments:
|
||||||
|
|
||||||
|
- `err`: a `Error` object indicating the success status;
|
||||||
|
- `target`: the object returned by `http://host:port/json/new` containing the
|
||||||
|
target.
|
||||||
|
|
||||||
|
When `callback` is omitted a `Promise` object is returned.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const CDP = require('chrome-remote-interface');
|
||||||
|
CDP.New((err, target) => {
|
||||||
|
if (!err) {
|
||||||
|
console.log(target);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### CDP.Activate([options], [callback])
|
||||||
|
|
||||||
|
Activate an open target/tab of the remote instance.
|
||||||
|
|
||||||
|
`options` is an object with the following properties:
|
||||||
|
|
||||||
|
- `host`: HTTP frontend host. Defaults to `localhost`;
|
||||||
|
- `port`: HTTP frontend port. Defaults to `9222`;
|
||||||
|
- `secure`: HTTPS/WSS frontend. Defaults to `false`;
|
||||||
|
- `useHostName`: do not perform a DNS lookup of the host. Defaults to `false`;
|
||||||
|
- `alterPath`: a `function` taking and returning the path fragment of a URL
|
||||||
|
before that a request happens. Defaults to the identity function;
|
||||||
|
- `id`: Target id. Required, no default.
|
||||||
|
|
||||||
|
`callback` is executed when the response to the activation request is
|
||||||
|
received. It gets the following arguments:
|
||||||
|
|
||||||
|
- `err`: a `Error` object indicating the success status;
|
||||||
|
|
||||||
|
When `callback` is omitted a `Promise` object is returned.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const CDP = require('chrome-remote-interface');
|
||||||
|
CDP.Activate({id: 'CC46FBFA-3BDA-493B-B2E4-2BE6EB0D97EC'}, (err) => {
|
||||||
|
if (!err) {
|
||||||
|
console.log('target is activated');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### CDP.Close([options], [callback])
|
||||||
|
|
||||||
|
Close an open target/tab of the remote instance.
|
||||||
|
|
||||||
|
`options` is an object with the following properties:
|
||||||
|
|
||||||
|
- `host`: HTTP frontend host. Defaults to `localhost`;
|
||||||
|
- `port`: HTTP frontend port. Defaults to `9222`;
|
||||||
|
- `secure`: HTTPS/WSS frontend. Defaults to `false`;
|
||||||
|
- `useHostName`: do not perform a DNS lookup of the host. Defaults to `false`;
|
||||||
|
- `alterPath`: a `function` taking and returning the path fragment of a URL
|
||||||
|
before that a request happens. Defaults to the identity function;
|
||||||
|
- `id`: Target id. Required, no default.
|
||||||
|
|
||||||
|
`callback` is executed when the response to the close request is received. It
|
||||||
|
gets the following arguments:
|
||||||
|
|
||||||
|
- `err`: a `Error` object indicating the success status;
|
||||||
|
|
||||||
|
When `callback` is omitted a `Promise` object is returned.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const CDP = require('chrome-remote-interface');
|
||||||
|
CDP.Close({id: 'CC46FBFA-3BDA-493B-B2E4-2BE6EB0D97EC'}, (err) => {
|
||||||
|
if (!err) {
|
||||||
|
console.log('target is closing');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that the callback is fired when the target is *queued* for removal, but the
|
||||||
|
actual removal will occur asynchronously.
|
||||||
|
|
||||||
|
### CDP.Version([options], [callback])
|
||||||
|
|
||||||
|
Request version information from the remote instance.
|
||||||
|
|
||||||
|
`options` is an object with the following optional properties:
|
||||||
|
|
||||||
|
- `host`: HTTP frontend host. Defaults to `localhost`;
|
||||||
|
- `port`: HTTP frontend port. Defaults to `9222`;
|
||||||
|
- `secure`: HTTPS/WSS frontend. Defaults to `false`;
|
||||||
|
- `useHostName`: do not perform a DNS lookup of the host. Defaults to `false`;
|
||||||
|
- `alterPath`: a `function` taking and returning the path fragment of a URL
|
||||||
|
before that a request happens. Defaults to the identity function.
|
||||||
|
|
||||||
|
`callback` is executed when the version information is correctly received, it
|
||||||
|
gets the following arguments:
|
||||||
|
|
||||||
|
- `err`: a `Error` object indicating the success status;
|
||||||
|
- `info`: a JSON object returned by `http://host:port/json/version` containing
|
||||||
|
the version information.
|
||||||
|
|
||||||
|
When `callback` is omitted a `Promise` object is returned.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const CDP = require('chrome-remote-interface');
|
||||||
|
CDP.Version((err, info) => {
|
||||||
|
if (!err) {
|
||||||
|
console.log(info);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Class: CDP
|
||||||
|
|
||||||
|
#### Event: 'event'
|
||||||
|
|
||||||
|
```js
|
||||||
|
function (message) {}
|
||||||
|
```
|
||||||
|
|
||||||
|
Emitted when the remote instance sends any notification through the WebSocket.
|
||||||
|
|
||||||
|
`message` is the object received, it has the following properties:
|
||||||
|
|
||||||
|
- `method`: a string describing the notification (e.g.,
|
||||||
|
`'Network.requestWillBeSent'`);
|
||||||
|
- `params`: an object containing the payload;
|
||||||
|
- `sessionId`: an optional string representing the session identifier.
|
||||||
|
|
||||||
|
Refer to the [Chrome Debugging Protocol] specification for more information.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
```js
|
||||||
|
client.on('event', (message) => {
|
||||||
|
if (message.method === 'Network.requestWillBeSent') {
|
||||||
|
console.log(message.params);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Event: '`<domain>`.`<method>`'
|
||||||
|
|
||||||
|
```js
|
||||||
|
function (params, sessionId) {}
|
||||||
|
```
|
||||||
|
|
||||||
|
Emitted when the remote instance sends a notification for `<domain>.<method>`
|
||||||
|
through the WebSocket.
|
||||||
|
|
||||||
|
`params` is an object containing the payload.
|
||||||
|
|
||||||
|
`sessionId` is an optional string representing the session identifier.
|
||||||
|
|
||||||
|
This is just a utility event which allows to easily listen for specific
|
||||||
|
notifications (see [`'event'`](#event-event)), for example:
|
||||||
|
|
||||||
|
```js
|
||||||
|
client.on('Network.requestWillBeSent', console.log);
|
||||||
|
```
|
||||||
|
|
||||||
|
Additionally, the equivalent `<domain>.on('<method>', ...)` syntax is available, for example:
|
||||||
|
|
||||||
|
```js
|
||||||
|
client.Network.on('requestWillBeSent', console.log);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Event: '`<domain>`.`<method>`.`<sessionId>`'
|
||||||
|
|
||||||
|
```js
|
||||||
|
function (params, sessionId) {}
|
||||||
|
```
|
||||||
|
|
||||||
|
Equivalent to the following but only for those events belonging to the given `session`:
|
||||||
|
|
||||||
|
```js
|
||||||
|
client.on('<domain>.<event>', callback);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Event: 'ready'
|
||||||
|
|
||||||
|
```js
|
||||||
|
function () {}
|
||||||
|
```
|
||||||
|
|
||||||
|
Emitted every time that there are no more pending commands waiting for a
|
||||||
|
response from the remote instance. The interaction is asynchronous so the only
|
||||||
|
way to serialize a sequence of commands is to use the callback provided by
|
||||||
|
the [`send`](#clientsendmethod-params-callback) method. This event acts as a
|
||||||
|
barrier and it is useful to avoid the *callback hell* in certain simple
|
||||||
|
situations.
|
||||||
|
|
||||||
|
Users are encouraged to extensively check the response of each method and should
|
||||||
|
prefer the promises API when dealing with complex asynchronous program flows.
|
||||||
|
|
||||||
|
For example to load a URL only after having enabled the notifications of both
|
||||||
|
`Network` and `Page` domains:
|
||||||
|
|
||||||
|
```js
|
||||||
|
client.Network.enable();
|
||||||
|
client.Page.enable();
|
||||||
|
client.once('ready', () => {
|
||||||
|
client.Page.navigate({url: 'https://github.com'});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
In this particular case, not enforcing this kind of serialization may cause that
|
||||||
|
the remote instance does not properly deliver the desired notifications the
|
||||||
|
client.
|
||||||
|
|
||||||
|
|
||||||
|
#### Event: 'disconnect'
|
||||||
|
|
||||||
|
```js
|
||||||
|
function () {}
|
||||||
|
```
|
||||||
|
|
||||||
|
Emitted when the instance closes the WebSocket connection.
|
||||||
|
|
||||||
|
This may happen for example when the user opens DevTools or when the tab is
|
||||||
|
closed.
|
||||||
|
|
||||||
|
#### client.send(method, [params], [sessionId], [callback])
|
||||||
|
|
||||||
|
Issue a command to the remote instance.
|
||||||
|
|
||||||
|
`method` is a string describing the command.
|
||||||
|
|
||||||
|
`params` is an object containing the payload.
|
||||||
|
|
||||||
|
`sessionId` is a string representing the session identifier.
|
||||||
|
|
||||||
|
`callback` is executed when the remote instance sends a response to this
|
||||||
|
command, it gets the following arguments:
|
||||||
|
|
||||||
|
- `error`: a boolean value indicating the success status, as reported by the
|
||||||
|
remote instance;
|
||||||
|
- `response`: an object containing either the response (`result` field, if
|
||||||
|
`error === false`) or the indication of the error (`error` field, if `error
|
||||||
|
=== true`).
|
||||||
|
|
||||||
|
When `callback` is omitted a `Promise` object is returned instead, with the
|
||||||
|
fulfilled/rejected states implemented according to the `error` parameter. The
|
||||||
|
`Error` object con be an instance of [`ProtocolError`](#cdpprotocolerror) for
|
||||||
|
protocol invocation errors. Alternatively, in case of low-level WebSocket
|
||||||
|
errors, the `error` parameter contains the originating `Error` object.
|
||||||
|
|
||||||
|
Note that the field `id` mentioned in the [Chrome Debugging Protocol]
|
||||||
|
specification is managed internally and it is not exposed to the user.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
```js
|
||||||
|
client.send('Page.navigate', {url: 'https://github.com'}, console.log);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### client.`<domain>`.`<method>`([params], [sessionId], [callback])
|
||||||
|
|
||||||
|
Just a shorthand for:
|
||||||
|
|
||||||
|
```js
|
||||||
|
client.send('<domain>.<method>', params, sessionId, callback);
|
||||||
|
```
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
```js
|
||||||
|
client.Page.navigate({url: 'https://github.com'}, console.log);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### client.`<domain>`.`<event>`([sessionId], [callback])
|
||||||
|
|
||||||
|
Just a shorthand for:
|
||||||
|
|
||||||
|
```js
|
||||||
|
client.on('<domain>.<event>[.<sessionId>]', callback);
|
||||||
|
```
|
||||||
|
|
||||||
|
When `callback` is omitted the event is registered only once and a `Promise`
|
||||||
|
object is returned. Notice though that in this case the optional `sessionId` usually passed to `callback` is not returned.
|
||||||
|
|
||||||
|
When `callback` is provided, it returns a function that can be used to
|
||||||
|
unsubscribe `callback` from the event, it can be useful when anonymous functions
|
||||||
|
are used as callbacks.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const unsubscribe = client.Network.requestWillBeSent((params, sessionId) => {
|
||||||
|
console.log(params.request.url);
|
||||||
|
});
|
||||||
|
unsubscribe();
|
||||||
|
```
|
||||||
|
|
||||||
|
#### client.close([callback])
|
||||||
|
|
||||||
|
Close the connection to the remote instance.
|
||||||
|
|
||||||
|
`callback` is executed when the WebSocket is successfully closed.
|
||||||
|
|
||||||
|
When `callback` is omitted a `Promise` object is returned.
|
||||||
|
|
||||||
|
#### client['`<domain>`.`<name>`']
|
||||||
|
|
||||||
|
Just a shorthand for:
|
||||||
|
|
||||||
|
```js
|
||||||
|
client.<domain>.<name>
|
||||||
|
```
|
||||||
|
|
||||||
|
Where `<name>` can be a command, an event, or a type.
|
||||||
|
|
||||||
|
### CDP.ProtocolError
|
||||||
|
|
||||||
|
Error returned by the [`send`](#clientsendmethod-params-callback) method in case Chrome experienced issues in the protocol invocation. It exposes the following fields:
|
||||||
|
|
||||||
|
- `request`: the raw request object containing the `method`, `params`, and `sessionId` fields;
|
||||||
|
- `response`: the raw response from Chrome, usually containing the `code`, `message`, and `data` fields.
|
||||||
|
|
||||||
|
## FAQ
|
||||||
|
|
||||||
|
### Invoking `Domain.methodOrEvent` I obtain `Domain.methodOrEvent is not a function`
|
||||||
|
|
||||||
|
This means that you are trying to use a method or an event that are not present
|
||||||
|
in the protocol descriptor that you are using.
|
||||||
|
|
||||||
|
If the protocol is fetched from Chrome directly, then it means that this version
|
||||||
|
of Chrome does not support that feature. The solution is to update it.
|
||||||
|
|
||||||
|
If you are using a local or custom version of the protocol, then it means that
|
||||||
|
the version is obsolete. The solution is to provide an up-to-date one, or if you
|
||||||
|
are using the protocol embedded in chrome-remote-interface, make sure to be
|
||||||
|
running the latest version of this module. In case the embedded protocol is
|
||||||
|
obsolete, please [file an issue](https://github.com/cyrus-and/chrome-remote-interface/issues/new).
|
||||||
|
|
||||||
|
See [here](#chrome-debugging-protocol-versions) for more information.
|
||||||
|
|
||||||
|
### Invoking `Domain.method` I obtain `Domain.method wasn't found`
|
||||||
|
|
||||||
|
This means that you are providing a custom or local protocol descriptor
|
||||||
|
(`CDP({protocol: customProtocol})`) which declares `Domain.method` while the
|
||||||
|
Chrome version that you are using does not support it.
|
||||||
|
|
||||||
|
To inspect the currently available protocol descriptor use:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ chrome-remote-interface inspect
|
||||||
|
```
|
||||||
|
|
||||||
|
See [here](#chrome-debugging-protocol-versions) for more information.
|
||||||
|
|
||||||
|
### Why my program stalls or behave unexpectedly if I run Chrome in a Docker container?
|
||||||
|
|
||||||
|
This happens because the size of `/dev/shm` is set to 64MB by default in Docker
|
||||||
|
and may not be enough for Chrome to navigate certain web pages.
|
||||||
|
|
||||||
|
You can change this value by running your container with, say,
|
||||||
|
`--shm-size=256m`.
|
||||||
|
|
||||||
|
### Using `Runtime.evaluate` with `awaitPromise: true` I sometimes obtain `Error: Promise was collected`
|
||||||
|
|
||||||
|
This is thrown by `Runtime.evaluate` when the browser-side promise gets
|
||||||
|
*collected* by the Chrome's garbage collector, this happens when the whole
|
||||||
|
JavaScript execution environment is invalidated, e.g., a when page is navigated
|
||||||
|
or reloaded while a promise is still waiting to be resolved.
|
||||||
|
|
||||||
|
Here is an example:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ chrome-remote-interface inspect
|
||||||
|
>>> Runtime.evaluate({expression: `new Promise(() => {})`, awaitPromise: true})
|
||||||
|
>>> Page.reload() // then wait several seconds
|
||||||
|
{ result: {} }
|
||||||
|
{ error: { code: -32000, message: 'Promise was collected' } }
|
||||||
|
```
|
||||||
|
|
||||||
|
To fix this, just make sure there are no pending promises before closing,
|
||||||
|
reloading, etc. a page.
|
||||||
|
|
||||||
|
### How does this compare to Puppeteer?
|
||||||
|
|
||||||
|
[Puppeteer] is an additional high-level API built upon the [Chrome Debugging
|
||||||
|
Protocol] which, among the other things, may start and use a bundled version of
|
||||||
|
Chromium instead of the one installed on your system. Use it if its API meets
|
||||||
|
your needs as it would probably be easier to work with.
|
||||||
|
|
||||||
|
chrome-remote-interface instead is just a general purpose 1:1 Node.js binding
|
||||||
|
for the [Chrome Debugging Protocol]. Use it if you need all the power of the raw
|
||||||
|
protocol, e.g., to implement your own high-level API.
|
||||||
|
|
||||||
|
See [#240] for a more thorough discussion.
|
||||||
|
|
||||||
|
[Puppeteer]: https://github.com/GoogleChrome/puppeteer
|
||||||
|
[#240]: https://github.com/cyrus-and/chrome-remote-interface/issues/240
|
||||||
|
|
||||||
|
## Contributors
|
||||||
|
|
||||||
|
- [Andrey Sidorov](https://github.com/sidorares)
|
||||||
|
- [Greg Cochard](https://github.com/gcochard)
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
- [Chrome Debugging Protocol]
|
||||||
|
- [Chrome Debugging Protocol Google group](https://groups.google.com/forum/#!forum/chrome-debugging-protocol)
|
||||||
|
- [devtools-protocol official repo](https://github.com/ChromeDevTools/devtools-protocol)
|
||||||
|
- [Showcase Chrome Debugging Protocol Clients](https://developer.chrome.com/devtools/docs/debugging-clients)
|
||||||
|
- [Awesome chrome-devtools](https://github.com/ChromeDevTools/awesome-chrome-devtools)
|
||||||
|
|
||||||
|
[Chrome Debugging Protocol]: https://chromedevtools.github.io/devtools-protocol/
|
||||||
311
engine/ds-screencast/node_modules/chrome-remote-interface/bin/client.js
generated
vendored
Executable file
311
engine/ds-screencast/node_modules/chrome-remote-interface/bin/client.js
generated
vendored
Executable file
|
|
@ -0,0 +1,311 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const repl = require('repl');
|
||||||
|
const util = require('util');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const program = require('commander');
|
||||||
|
|
||||||
|
const CDP = require('../');
|
||||||
|
const packageInfo = require('../package.json');
|
||||||
|
|
||||||
|
function display(object) {
|
||||||
|
return util.inspect(object, {
|
||||||
|
colors: process.stdout.isTTY,
|
||||||
|
depth: null
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function toJSON(object) {
|
||||||
|
return JSON.stringify(object, null, 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
|
||||||
|
function inspect(target, args, options) {
|
||||||
|
options.local = args.local;
|
||||||
|
// otherwise the active target
|
||||||
|
if (target) {
|
||||||
|
if (args.webSocket) {
|
||||||
|
// by WebSocket URL
|
||||||
|
options.target = target;
|
||||||
|
} else {
|
||||||
|
// by target id
|
||||||
|
options.target = (targets) => {
|
||||||
|
return targets.findIndex((_target) => {
|
||||||
|
return _target.id === target;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.protocol) {
|
||||||
|
options.protocol = JSON.parse(fs.readFileSync(args.protocol));
|
||||||
|
}
|
||||||
|
|
||||||
|
CDP(options, (client) => {
|
||||||
|
const cdpRepl = repl.start({
|
||||||
|
prompt: process.stdin.isTTY ? '\x1b[32m>>>\x1b[0m ' : '',
|
||||||
|
ignoreUndefined: true,
|
||||||
|
writer: display
|
||||||
|
});
|
||||||
|
|
||||||
|
// XXX always await promises on the REPL
|
||||||
|
const defaultEval = cdpRepl.eval;
|
||||||
|
cdpRepl.eval = (cmd, context, filename, callback) => {
|
||||||
|
defaultEval(cmd, context, filename, async (err, result) => {
|
||||||
|
if (err) {
|
||||||
|
// propagate errors from the eval
|
||||||
|
callback(err);
|
||||||
|
} else {
|
||||||
|
// awaits the promise and either return result or error
|
||||||
|
try {
|
||||||
|
callback(null, await Promise.resolve(result));
|
||||||
|
} catch (err) {
|
||||||
|
callback(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const homePath = process.env.HOME || process.env.USERPROFILE;
|
||||||
|
const historyFile = path.join(homePath, '.cri_history');
|
||||||
|
const historySize = 10000;
|
||||||
|
|
||||||
|
function loadHistory() {
|
||||||
|
// only if run from a terminal
|
||||||
|
if (!process.stdin.isTTY) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// attempt to open the history file
|
||||||
|
let fd;
|
||||||
|
try {
|
||||||
|
fd = fs.openSync(historyFile, 'r');
|
||||||
|
} catch (err) {
|
||||||
|
return; // no history file present
|
||||||
|
}
|
||||||
|
// populate the REPL history
|
||||||
|
fs.readFileSync(fd, 'utf8')
|
||||||
|
.split('\n')
|
||||||
|
.filter((entry) => {
|
||||||
|
return entry.trim();
|
||||||
|
})
|
||||||
|
.reverse() // to be compatible with repl.history files
|
||||||
|
.forEach((entry) => {
|
||||||
|
cdpRepl.history.push(entry);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveHistory() {
|
||||||
|
// only if run from a terminal
|
||||||
|
if (!process.stdin.isTTY) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// only store the last chunk
|
||||||
|
const entries = cdpRepl.history.slice(0, historySize).reverse().join('\n');
|
||||||
|
fs.writeFileSync(historyFile, entries + '\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
// utility custom command
|
||||||
|
cdpRepl.defineCommand('target', {
|
||||||
|
help: 'Display the current target',
|
||||||
|
action: () => {
|
||||||
|
console.log(client.webSocketUrl);
|
||||||
|
cdpRepl.displayPrompt();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// utility to purge all the event handlers
|
||||||
|
cdpRepl.defineCommand('reset', {
|
||||||
|
help: 'Remove all the registered event handlers',
|
||||||
|
action: () => {
|
||||||
|
client.removeAllListeners();
|
||||||
|
cdpRepl.displayPrompt();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// enable history
|
||||||
|
loadHistory();
|
||||||
|
|
||||||
|
// disconnect on exit
|
||||||
|
cdpRepl.on('exit', () => {
|
||||||
|
if (process.stdin.isTTY) {
|
||||||
|
console.log();
|
||||||
|
}
|
||||||
|
client.close();
|
||||||
|
saveHistory();
|
||||||
|
});
|
||||||
|
|
||||||
|
// exit on disconnection
|
||||||
|
client.on('disconnect', () => {
|
||||||
|
console.error('Disconnected.');
|
||||||
|
saveHistory();
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
// add protocol API
|
||||||
|
for (const domainObject of client.protocol.domains) {
|
||||||
|
// walk the domain names
|
||||||
|
const domainName = domainObject.domain;
|
||||||
|
cdpRepl.context[domainName] = {};
|
||||||
|
// walk the items in the domain
|
||||||
|
for (const itemName in client[domainName]) {
|
||||||
|
// add CDP object to the REPL context
|
||||||
|
const cdpObject = client[domainName][itemName];
|
||||||
|
cdpRepl.context[domainName][itemName] = cdpObject;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).on('error', (err) => {
|
||||||
|
console.error('Cannot connect to remote endpoint:', err.toString());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function list(options) {
|
||||||
|
CDP.List(options, (err, targets) => {
|
||||||
|
if (err) {
|
||||||
|
console.error(err.toString());
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
console.log(toJSON(targets));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function _new(url, options) {
|
||||||
|
options.url = url;
|
||||||
|
CDP.New(options, (err, target) => {
|
||||||
|
if (err) {
|
||||||
|
console.error(err.toString());
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
console.log(toJSON(target));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function activate(args, options) {
|
||||||
|
options.id = args;
|
||||||
|
CDP.Activate(options, (err) => {
|
||||||
|
if (err) {
|
||||||
|
console.error(err.toString());
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function close(args, options) {
|
||||||
|
options.id = args;
|
||||||
|
CDP.Close(options, (err) => {
|
||||||
|
if (err) {
|
||||||
|
console.error(err.toString());
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function version(options) {
|
||||||
|
CDP.Version(options, (err, info) => {
|
||||||
|
if (err) {
|
||||||
|
console.error(err.toString());
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
console.log(toJSON(info));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function protocol(args, options) {
|
||||||
|
options.local = args.local;
|
||||||
|
CDP.Protocol(options, (err, protocol) => {
|
||||||
|
if (err) {
|
||||||
|
console.error(err.toString());
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
console.log(toJSON(protocol));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
|
||||||
|
let action;
|
||||||
|
|
||||||
|
program
|
||||||
|
.option('-v, --v', 'Show this module version')
|
||||||
|
.option('-t, --host <host>', 'HTTP frontend host')
|
||||||
|
.option('-p, --port <port>', 'HTTP frontend port')
|
||||||
|
.option('-s, --secure', 'HTTPS/WSS frontend')
|
||||||
|
.option('-n, --use-host-name', 'Do not perform a DNS lookup of the host');
|
||||||
|
|
||||||
|
program
|
||||||
|
.command('inspect [<target>]')
|
||||||
|
.description('inspect a target (defaults to the first available target)')
|
||||||
|
.option('-w, --web-socket', 'interpret <target> as a WebSocket URL instead of a target id')
|
||||||
|
.option('-j, --protocol <file.json>', 'Chrome Debugging Protocol descriptor (overrides `--local`)')
|
||||||
|
.option('-l, --local', 'Use the local protocol descriptor')
|
||||||
|
.action((target, args) => {
|
||||||
|
action = inspect.bind(null, target, args);
|
||||||
|
});
|
||||||
|
|
||||||
|
program
|
||||||
|
.command('list')
|
||||||
|
.description('list all the available targets/tabs')
|
||||||
|
.action(() => {
|
||||||
|
action = list;
|
||||||
|
});
|
||||||
|
|
||||||
|
program
|
||||||
|
.command('new [<url>]')
|
||||||
|
.description('create a new target/tab')
|
||||||
|
.action((url) => {
|
||||||
|
action = _new.bind(null, url);
|
||||||
|
});
|
||||||
|
|
||||||
|
program
|
||||||
|
.command('activate <id>')
|
||||||
|
.description('activate a target/tab by id')
|
||||||
|
.action((id) => {
|
||||||
|
action = activate.bind(null, id);
|
||||||
|
});
|
||||||
|
|
||||||
|
program
|
||||||
|
.command('close <id>')
|
||||||
|
.description('close a target/tab by id')
|
||||||
|
.action((id) => {
|
||||||
|
action = close.bind(null, id);
|
||||||
|
});
|
||||||
|
|
||||||
|
program
|
||||||
|
.command('version')
|
||||||
|
.description('show the browser version')
|
||||||
|
.action(() => {
|
||||||
|
action = version;
|
||||||
|
});
|
||||||
|
|
||||||
|
program
|
||||||
|
.command('protocol')
|
||||||
|
.description('show the currently available protocol descriptor')
|
||||||
|
.option('-l, --local', 'Return the local protocol descriptor')
|
||||||
|
.action((args) => {
|
||||||
|
action = protocol.bind(null, args);
|
||||||
|
});
|
||||||
|
|
||||||
|
program.parse(process.argv);
|
||||||
|
|
||||||
|
// common options
|
||||||
|
const options = {
|
||||||
|
host: program.host,
|
||||||
|
port: program.port,
|
||||||
|
secure: program.secure,
|
||||||
|
useHostName: program.useHostName
|
||||||
|
};
|
||||||
|
|
||||||
|
if (action) {
|
||||||
|
action(options);
|
||||||
|
} else {
|
||||||
|
if (program.v) {
|
||||||
|
console.log(packageInfo.version);
|
||||||
|
} else {
|
||||||
|
program.outputHelp();
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
1
engine/ds-screencast/node_modules/chrome-remote-interface/chrome-remote-interface.js
generated
vendored
Normal file
1
engine/ds-screencast/node_modules/chrome-remote-interface/chrome-remote-interface.js
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
46
engine/ds-screencast/node_modules/chrome-remote-interface/index.js
generated
vendored
Normal file
46
engine/ds-screencast/node_modules/chrome-remote-interface/index.js
generated
vendored
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const EventEmitter = require('events');
|
||||||
|
const dns = require('dns');
|
||||||
|
|
||||||
|
const devtools = require('./lib/devtools.js');
|
||||||
|
const errors = require('./lib/errors.js');
|
||||||
|
const Chrome = require('./lib/chrome.js');
|
||||||
|
|
||||||
|
// XXX reset the default that has been changed in
|
||||||
|
// (https://github.com/nodejs/node/pull/39987) to prefer IPv4. since
|
||||||
|
// implementations alway bind on 127.0.0.1 this solution should be fairly safe
|
||||||
|
// (see #467)
|
||||||
|
if (dns.setDefaultResultOrder) {
|
||||||
|
dns.setDefaultResultOrder('ipv4first');
|
||||||
|
}
|
||||||
|
|
||||||
|
function CDP(options, callback) {
|
||||||
|
if (typeof options === 'function') {
|
||||||
|
callback = options;
|
||||||
|
options = undefined;
|
||||||
|
}
|
||||||
|
const notifier = new EventEmitter();
|
||||||
|
if (typeof callback === 'function') {
|
||||||
|
// allow to register the error callback later
|
||||||
|
process.nextTick(() => {
|
||||||
|
new Chrome(options, notifier);
|
||||||
|
});
|
||||||
|
return notifier.once('connect', callback);
|
||||||
|
} else {
|
||||||
|
return new Promise((fulfill, reject) => {
|
||||||
|
notifier.once('connect', fulfill);
|
||||||
|
notifier.once('error', reject);
|
||||||
|
new Chrome(options, notifier);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = CDP;
|
||||||
|
module.exports.Protocol = devtools.Protocol;
|
||||||
|
module.exports.List = devtools.List;
|
||||||
|
module.exports.New = devtools.New;
|
||||||
|
module.exports.Activate = devtools.Activate;
|
||||||
|
module.exports.Close = devtools.Close;
|
||||||
|
module.exports.Version = devtools.Version;
|
||||||
|
module.exports.ProtocolError = errors.ProtocolError;
|
||||||
92
engine/ds-screencast/node_modules/chrome-remote-interface/lib/api.js
generated
vendored
Normal file
92
engine/ds-screencast/node_modules/chrome-remote-interface/lib/api.js
generated
vendored
Normal file
|
|
@ -0,0 +1,92 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
function arrayToObject(parameters) {
|
||||||
|
const keyValue = {};
|
||||||
|
parameters.forEach((parameter) =>{
|
||||||
|
const name = parameter.name;
|
||||||
|
delete parameter.name;
|
||||||
|
keyValue[name] = parameter;
|
||||||
|
});
|
||||||
|
return keyValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
function decorate(to, category, object) {
|
||||||
|
to.category = category;
|
||||||
|
Object.keys(object).forEach((field) => {
|
||||||
|
// skip the 'name' field as it is part of the function prototype
|
||||||
|
if (field === 'name') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// commands and events have parameters whereas types have properties
|
||||||
|
if (category === 'type' && field === 'properties' ||
|
||||||
|
field === 'parameters') {
|
||||||
|
to[field] = arrayToObject(object[field]);
|
||||||
|
} else {
|
||||||
|
to[field] = object[field];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function addCommand(chrome, domainName, command) {
|
||||||
|
const commandName = `${domainName}.${command.name}`;
|
||||||
|
const handler = (params, sessionId, callback) => {
|
||||||
|
return chrome.send(commandName, params, sessionId, callback);
|
||||||
|
};
|
||||||
|
decorate(handler, 'command', command);
|
||||||
|
chrome[commandName] = chrome[domainName][command.name] = handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addEvent(chrome, domainName, event) {
|
||||||
|
const eventName = `${domainName}.${event.name}`;
|
||||||
|
const handler = (sessionId, handler) => {
|
||||||
|
if (typeof sessionId === 'function') {
|
||||||
|
handler = sessionId;
|
||||||
|
sessionId = undefined;
|
||||||
|
}
|
||||||
|
const rawEventName = sessionId ? `${eventName}.${sessionId}` : eventName;
|
||||||
|
if (typeof handler === 'function') {
|
||||||
|
chrome.on(rawEventName, handler);
|
||||||
|
return () => chrome.removeListener(rawEventName, handler);
|
||||||
|
} else {
|
||||||
|
return new Promise((fulfill, reject) => {
|
||||||
|
chrome.once(rawEventName, fulfill);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
decorate(handler, 'event', event);
|
||||||
|
chrome[eventName] = chrome[domainName][event.name] = handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addType(chrome, domainName, type) {
|
||||||
|
const typeName = `${domainName}.${type.id}`;
|
||||||
|
const help = {};
|
||||||
|
decorate(help, 'type', type);
|
||||||
|
chrome[typeName] = chrome[domainName][type.id] = help;
|
||||||
|
}
|
||||||
|
|
||||||
|
function prepare(object, protocol) {
|
||||||
|
// assign the protocol and generate the shorthands
|
||||||
|
object.protocol = protocol;
|
||||||
|
protocol.domains.forEach((domain) => {
|
||||||
|
const domainName = domain.domain;
|
||||||
|
object[domainName] = {};
|
||||||
|
// add commands
|
||||||
|
(domain.commands || []).forEach((command) => {
|
||||||
|
addCommand(object, domainName, command);
|
||||||
|
});
|
||||||
|
// add events
|
||||||
|
(domain.events || []).forEach((event) => {
|
||||||
|
addEvent(object, domainName, event);
|
||||||
|
});
|
||||||
|
// add types
|
||||||
|
(domain.types || []).forEach((type) => {
|
||||||
|
addType(object, domainName, type);
|
||||||
|
});
|
||||||
|
// add utility listener for each domain
|
||||||
|
object[domainName].on = (eventName, handler) => {
|
||||||
|
return object[domainName][eventName](handler);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports.prepare = prepare;
|
||||||
302
engine/ds-screencast/node_modules/chrome-remote-interface/lib/chrome.js
generated
vendored
Normal file
302
engine/ds-screencast/node_modules/chrome-remote-interface/lib/chrome.js
generated
vendored
Normal file
|
|
@ -0,0 +1,302 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const EventEmitter = require('events');
|
||||||
|
const util = require('util');
|
||||||
|
const formatUrl = require('url').format;
|
||||||
|
const parseUrl = require('url').parse;
|
||||||
|
|
||||||
|
const WebSocket = require('ws');
|
||||||
|
|
||||||
|
const api = require('./api.js');
|
||||||
|
const defaults = require('./defaults.js');
|
||||||
|
const devtools = require('./devtools.js');
|
||||||
|
const errors = require('./errors.js');
|
||||||
|
|
||||||
|
class Chrome extends EventEmitter {
|
||||||
|
constructor(options, notifier) {
|
||||||
|
super();
|
||||||
|
// options
|
||||||
|
const defaultTarget = (targets) => {
|
||||||
|
// prefer type = 'page' inspectable targets as they represents
|
||||||
|
// browser tabs (fall back to the first inspectable target
|
||||||
|
// otherwise)
|
||||||
|
let backup;
|
||||||
|
let target = targets.find((target) => {
|
||||||
|
if (target.webSocketDebuggerUrl) {
|
||||||
|
backup = backup || target;
|
||||||
|
return target.type === 'page';
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
target = target || backup;
|
||||||
|
if (target) {
|
||||||
|
return target;
|
||||||
|
} else {
|
||||||
|
throw new Error('No inspectable targets');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
options = options || {};
|
||||||
|
this.host = options.host || defaults.HOST;
|
||||||
|
this.port = options.port || defaults.PORT;
|
||||||
|
this.secure = !!(options.secure);
|
||||||
|
this.useHostName = !!(options.useHostName);
|
||||||
|
this.alterPath = options.alterPath || ((path) => path);
|
||||||
|
this.protocol = options.protocol;
|
||||||
|
this.local = !!(options.local);
|
||||||
|
this.target = options.target || defaultTarget;
|
||||||
|
// locals
|
||||||
|
this._notifier = notifier;
|
||||||
|
this._callbacks = {};
|
||||||
|
this._nextCommandId = 1;
|
||||||
|
// properties
|
||||||
|
this.webSocketUrl = undefined;
|
||||||
|
// operations
|
||||||
|
this._start();
|
||||||
|
}
|
||||||
|
|
||||||
|
// avoid misinterpreting protocol's members as custom util.inspect functions
|
||||||
|
inspect(depth, options) {
|
||||||
|
options.customInspect = false;
|
||||||
|
return util.inspect(this, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
send(method, params, sessionId, callback) {
|
||||||
|
// handle optional arguments
|
||||||
|
const optionals = Array.from(arguments).slice(1);
|
||||||
|
params = optionals.find(x => typeof x === 'object');
|
||||||
|
sessionId = optionals.find(x => typeof x === 'string');
|
||||||
|
callback = optionals.find(x => typeof x === 'function');
|
||||||
|
// return a promise when a callback is not provided
|
||||||
|
if (typeof callback === 'function') {
|
||||||
|
this._enqueueCommand(method, params, sessionId, callback);
|
||||||
|
return undefined;
|
||||||
|
} else {
|
||||||
|
return new Promise((fulfill, reject) => {
|
||||||
|
this._enqueueCommand(method, params, sessionId, (error, response) => {
|
||||||
|
if (error) {
|
||||||
|
const request = {method, params, sessionId};
|
||||||
|
reject(
|
||||||
|
error instanceof Error
|
||||||
|
? error // low-level WebSocket error
|
||||||
|
: new errors.ProtocolError(request, response)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
fulfill(response);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
close(callback) {
|
||||||
|
const closeWebSocket = (callback) => {
|
||||||
|
// don't close if it's already closed
|
||||||
|
if (this._ws.readyState === 3) {
|
||||||
|
callback();
|
||||||
|
} else {
|
||||||
|
// don't notify on user-initiated shutdown ('disconnect' event)
|
||||||
|
this._ws.removeAllListeners('close');
|
||||||
|
this._ws.once('close', () => {
|
||||||
|
this._ws.removeAllListeners();
|
||||||
|
this._handleConnectionClose();
|
||||||
|
callback();
|
||||||
|
});
|
||||||
|
this._ws.close();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (typeof callback === 'function') {
|
||||||
|
closeWebSocket(callback);
|
||||||
|
return undefined;
|
||||||
|
} else {
|
||||||
|
return new Promise((fulfill, reject) => {
|
||||||
|
closeWebSocket(fulfill);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// initiate the connection process
|
||||||
|
async _start() {
|
||||||
|
const options = {
|
||||||
|
host: this.host,
|
||||||
|
port: this.port,
|
||||||
|
secure: this.secure,
|
||||||
|
useHostName: this.useHostName,
|
||||||
|
alterPath: this.alterPath
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
// fetch the WebSocket debugger URL
|
||||||
|
const url = await this._fetchDebuggerURL(options);
|
||||||
|
// allow the user to alter the URL
|
||||||
|
const urlObject = parseUrl(url);
|
||||||
|
urlObject.pathname = options.alterPath(urlObject.pathname);
|
||||||
|
this.webSocketUrl = formatUrl(urlObject);
|
||||||
|
// update the connection parameters using the debugging URL
|
||||||
|
options.host = urlObject.hostname;
|
||||||
|
options.port = urlObject.port || options.port;
|
||||||
|
// fetch the protocol and prepare the API
|
||||||
|
const protocol = await this._fetchProtocol(options);
|
||||||
|
api.prepare(this, protocol);
|
||||||
|
// finally connect to the WebSocket
|
||||||
|
await this._connectToWebSocket();
|
||||||
|
// since the handler is executed synchronously, the emit() must be
|
||||||
|
// performed in the next tick so that uncaught errors in the client code
|
||||||
|
// are not intercepted by the Promise mechanism and therefore reported
|
||||||
|
// via the 'error' event
|
||||||
|
process.nextTick(() => {
|
||||||
|
this._notifier.emit('connect', this);
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
this._notifier.emit('error', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetch the WebSocket URL according to 'target'
|
||||||
|
async _fetchDebuggerURL(options) {
|
||||||
|
const userTarget = this.target;
|
||||||
|
switch (typeof userTarget) {
|
||||||
|
case 'string': {
|
||||||
|
let idOrUrl = userTarget;
|
||||||
|
// use default host and port if omitted (and a relative URL is specified)
|
||||||
|
if (idOrUrl.startsWith('/')) {
|
||||||
|
idOrUrl = `ws://${this.host}:${this.port}${idOrUrl}`;
|
||||||
|
}
|
||||||
|
// a WebSocket URL is specified by the user (e.g., node-inspector)
|
||||||
|
if (idOrUrl.match(/^wss?:/i)) {
|
||||||
|
return idOrUrl; // done!
|
||||||
|
}
|
||||||
|
// a target id is specified by the user
|
||||||
|
else {
|
||||||
|
const targets = await devtools.List(options);
|
||||||
|
const object = targets.find((target) => target.id === idOrUrl);
|
||||||
|
return object.webSocketDebuggerUrl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case 'object': {
|
||||||
|
const object = userTarget;
|
||||||
|
return object.webSocketDebuggerUrl;
|
||||||
|
}
|
||||||
|
case 'function': {
|
||||||
|
const func = userTarget;
|
||||||
|
const targets = await devtools.List(options);
|
||||||
|
const result = func(targets);
|
||||||
|
const object = typeof result === 'number' ? targets[result] : result;
|
||||||
|
return object.webSocketDebuggerUrl;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
throw new Error(`Invalid target argument "${this.target}"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetch the protocol according to 'protocol' and 'local'
|
||||||
|
async _fetchProtocol(options) {
|
||||||
|
// if a protocol has been provided then use it
|
||||||
|
if (this.protocol) {
|
||||||
|
return this.protocol;
|
||||||
|
}
|
||||||
|
// otherwise user either the local or the remote version
|
||||||
|
else {
|
||||||
|
options.local = this.local;
|
||||||
|
return await devtools.Protocol(options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// establish the WebSocket connection and start processing user commands
|
||||||
|
_connectToWebSocket() {
|
||||||
|
return new Promise((fulfill, reject) => {
|
||||||
|
// create the WebSocket
|
||||||
|
try {
|
||||||
|
if (this.secure) {
|
||||||
|
this.webSocketUrl = this.webSocketUrl.replace(/^ws:/i, 'wss:');
|
||||||
|
}
|
||||||
|
this._ws = new WebSocket(this.webSocketUrl, [], {
|
||||||
|
maxPayload: 256 * 1024 * 1024,
|
||||||
|
perMessageDeflate: false,
|
||||||
|
followRedirects: true,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
// handles bad URLs
|
||||||
|
reject(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// set up event handlers
|
||||||
|
this._ws.on('open', () => {
|
||||||
|
fulfill();
|
||||||
|
});
|
||||||
|
this._ws.on('message', (data) => {
|
||||||
|
const message = JSON.parse(data);
|
||||||
|
this._handleMessage(message);
|
||||||
|
});
|
||||||
|
this._ws.on('close', (code) => {
|
||||||
|
this._handleConnectionClose();
|
||||||
|
this.emit('disconnect');
|
||||||
|
});
|
||||||
|
this._ws.on('error', (err) => {
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleConnectionClose() {
|
||||||
|
// make sure to complete all the unresolved callbacks
|
||||||
|
const err = new Error('WebSocket connection closed');
|
||||||
|
for (const callback of Object.values(this._callbacks)) {
|
||||||
|
callback(err);
|
||||||
|
}
|
||||||
|
this._callbacks = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle the messages read from the WebSocket
|
||||||
|
_handleMessage(message) {
|
||||||
|
// command response
|
||||||
|
if (message.id) {
|
||||||
|
const callback = this._callbacks[message.id];
|
||||||
|
if (!callback) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// interpret the lack of both 'error' and 'result' as success
|
||||||
|
// (this may happen with node-inspector)
|
||||||
|
if (message.error) {
|
||||||
|
callback(true, message.error);
|
||||||
|
} else {
|
||||||
|
callback(false, message.result || {});
|
||||||
|
}
|
||||||
|
// unregister command response callback
|
||||||
|
delete this._callbacks[message.id];
|
||||||
|
// notify when there are no more pending commands
|
||||||
|
if (Object.keys(this._callbacks).length === 0) {
|
||||||
|
this.emit('ready');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// event
|
||||||
|
else if (message.method) {
|
||||||
|
const {method, params, sessionId} = message;
|
||||||
|
this.emit('event', message);
|
||||||
|
this.emit(method, params, sessionId);
|
||||||
|
this.emit(`${method}.${sessionId}`, params, sessionId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// send a command to the remote endpoint and register a callback for the reply
|
||||||
|
_enqueueCommand(method, params, sessionId, callback) {
|
||||||
|
const id = this._nextCommandId++;
|
||||||
|
const message = {
|
||||||
|
id,
|
||||||
|
method,
|
||||||
|
sessionId,
|
||||||
|
params: params || {}
|
||||||
|
};
|
||||||
|
this._ws.send(JSON.stringify(message), (err) => {
|
||||||
|
if (err) {
|
||||||
|
// handle low-level WebSocket errors
|
||||||
|
if (typeof callback === 'function') {
|
||||||
|
callback(err);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this._callbacks[id] = callback;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Chrome;
|
||||||
4
engine/ds-screencast/node_modules/chrome-remote-interface/lib/defaults.js
generated
vendored
Normal file
4
engine/ds-screencast/node_modules/chrome-remote-interface/lib/defaults.js
generated
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
module.exports.HOST = 'localhost';
|
||||||
|
module.exports.PORT = 9222;
|
||||||
127
engine/ds-screencast/node_modules/chrome-remote-interface/lib/devtools.js
generated
vendored
Normal file
127
engine/ds-screencast/node_modules/chrome-remote-interface/lib/devtools.js
generated
vendored
Normal file
|
|
@ -0,0 +1,127 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const http = require('http');
|
||||||
|
const https = require('https');
|
||||||
|
|
||||||
|
const defaults = require('./defaults.js');
|
||||||
|
const externalRequest = require('./external-request.js');
|
||||||
|
|
||||||
|
// options.path must be specified; callback(err, data)
|
||||||
|
function devToolsInterface(path, options, callback) {
|
||||||
|
const transport = options.secure ? https : http;
|
||||||
|
const requestOptions = {
|
||||||
|
method: options.method,
|
||||||
|
host: options.host || defaults.HOST,
|
||||||
|
port: options.port || defaults.PORT,
|
||||||
|
useHostName: options.useHostName,
|
||||||
|
path: (options.alterPath ? options.alterPath(path) : path)
|
||||||
|
};
|
||||||
|
externalRequest(transport, requestOptions, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
// wrapper that allows to return a promise if the callback is omitted, it works
|
||||||
|
// for DevTools methods
|
||||||
|
function promisesWrapper(func) {
|
||||||
|
return (options, callback) => {
|
||||||
|
// options is an optional argument
|
||||||
|
if (typeof options === 'function') {
|
||||||
|
callback = options;
|
||||||
|
options = undefined;
|
||||||
|
}
|
||||||
|
options = options || {};
|
||||||
|
// just call the function otherwise wrap a promise around its execution
|
||||||
|
if (typeof callback === 'function') {
|
||||||
|
func(options, callback);
|
||||||
|
return undefined;
|
||||||
|
} else {
|
||||||
|
return new Promise((fulfill, reject) => {
|
||||||
|
func(options, (err, result) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
} else {
|
||||||
|
fulfill(result);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function Protocol(options, callback) {
|
||||||
|
// if the local protocol is requested
|
||||||
|
if (options.local) {
|
||||||
|
const localDescriptor = require('./protocol.json');
|
||||||
|
callback(null, localDescriptor);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// try to fetch the protocol remotely
|
||||||
|
devToolsInterface('/json/protocol', options, (err, descriptor) => {
|
||||||
|
if (err) {
|
||||||
|
callback(err);
|
||||||
|
} else {
|
||||||
|
callback(null, JSON.parse(descriptor));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function List(options, callback) {
|
||||||
|
devToolsInterface('/json/list', options, (err, tabs) => {
|
||||||
|
if (err) {
|
||||||
|
callback(err);
|
||||||
|
} else {
|
||||||
|
callback(null, JSON.parse(tabs));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function New(options, callback) {
|
||||||
|
let path = '/json/new';
|
||||||
|
if (Object.prototype.hasOwnProperty.call(options, 'url')) {
|
||||||
|
path += `?${options.url}`;
|
||||||
|
}
|
||||||
|
options.method = options.method || 'PUT'; // see #497
|
||||||
|
devToolsInterface(path, options, (err, tab) => {
|
||||||
|
if (err) {
|
||||||
|
callback(err);
|
||||||
|
} else {
|
||||||
|
callback(null, JSON.parse(tab));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function Activate(options, callback) {
|
||||||
|
devToolsInterface('/json/activate/' + options.id, options, (err) => {
|
||||||
|
if (err) {
|
||||||
|
callback(err);
|
||||||
|
} else {
|
||||||
|
callback(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function Close(options, callback) {
|
||||||
|
devToolsInterface('/json/close/' + options.id, options, (err) => {
|
||||||
|
if (err) {
|
||||||
|
callback(err);
|
||||||
|
} else {
|
||||||
|
callback(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function Version(options, callback) {
|
||||||
|
devToolsInterface('/json/version', options, (err, versionInfo) => {
|
||||||
|
if (err) {
|
||||||
|
callback(err);
|
||||||
|
} else {
|
||||||
|
callback(null, JSON.parse(versionInfo));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports.Protocol = promisesWrapper(Protocol);
|
||||||
|
module.exports.List = promisesWrapper(List);
|
||||||
|
module.exports.New = promisesWrapper(New);
|
||||||
|
module.exports.Activate = promisesWrapper(Activate);
|
||||||
|
module.exports.Close = promisesWrapper(Close);
|
||||||
|
module.exports.Version = promisesWrapper(Version);
|
||||||
16
engine/ds-screencast/node_modules/chrome-remote-interface/lib/errors.js
generated
vendored
Normal file
16
engine/ds-screencast/node_modules/chrome-remote-interface/lib/errors.js
generated
vendored
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
class ProtocolError extends Error {
|
||||||
|
constructor(request, response) {
|
||||||
|
let {message} = response;
|
||||||
|
if (response.data) {
|
||||||
|
message += ` (${response.data})`;
|
||||||
|
}
|
||||||
|
super(message);
|
||||||
|
// attach the original response as well
|
||||||
|
this.request = request;
|
||||||
|
this.response = response;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports.ProtocolError = ProtocolError;
|
||||||
44
engine/ds-screencast/node_modules/chrome-remote-interface/lib/external-request.js
generated
vendored
Normal file
44
engine/ds-screencast/node_modules/chrome-remote-interface/lib/external-request.js
generated
vendored
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const dns = require('dns');
|
||||||
|
const util = require('util');
|
||||||
|
|
||||||
|
const REQUEST_TIMEOUT = 10000;
|
||||||
|
|
||||||
|
// callback(err, data)
|
||||||
|
async function externalRequest(transport, options, callback) {
|
||||||
|
// perform the DNS lookup manually so that the HTTP host header generated by
|
||||||
|
// http.get will contain the IP address, this is needed because since Chrome
|
||||||
|
// 66 the host header cannot contain an host name different than localhost
|
||||||
|
// (see https://github.com/cyrus-and/chrome-remote-interface/issues/340)
|
||||||
|
if (!options.useHostName) {
|
||||||
|
try {
|
||||||
|
const {address} = await util.promisify(dns.lookup)(options.host);
|
||||||
|
options.host = address;
|
||||||
|
} catch (err) {
|
||||||
|
callback(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// perform the actual request
|
||||||
|
const request = transport.request(options, (response) => {
|
||||||
|
let data = '';
|
||||||
|
response.on('data', (chunk) => {
|
||||||
|
data += chunk;
|
||||||
|
});
|
||||||
|
response.on('end', () => {
|
||||||
|
if (response.statusCode === 200) {
|
||||||
|
callback(null, data);
|
||||||
|
} else {
|
||||||
|
callback(new Error(data));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
request.setTimeout(REQUEST_TIMEOUT, () => {
|
||||||
|
request.abort();
|
||||||
|
});
|
||||||
|
request.on('error', callback);
|
||||||
|
request.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = externalRequest;
|
||||||
27862
engine/ds-screencast/node_modules/chrome-remote-interface/lib/protocol.json
generated
vendored
Normal file
27862
engine/ds-screencast/node_modules/chrome-remote-interface/lib/protocol.json
generated
vendored
Normal file
File diff suppressed because it is too large
Load diff
39
engine/ds-screencast/node_modules/chrome-remote-interface/lib/websocket-wrapper.js
generated
vendored
Normal file
39
engine/ds-screencast/node_modules/chrome-remote-interface/lib/websocket-wrapper.js
generated
vendored
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const EventEmitter = require('events');
|
||||||
|
|
||||||
|
// wrapper around the Node.js ws module
|
||||||
|
// for use in browsers
|
||||||
|
class WebSocketWrapper extends EventEmitter {
|
||||||
|
constructor(url) {
|
||||||
|
super();
|
||||||
|
this._ws = new WebSocket(url); // eslint-disable-line no-undef
|
||||||
|
this._ws.onopen = () => {
|
||||||
|
this.emit('open');
|
||||||
|
};
|
||||||
|
this._ws.onclose = () => {
|
||||||
|
this.emit('close');
|
||||||
|
};
|
||||||
|
this._ws.onmessage = (event) => {
|
||||||
|
this.emit('message', event.data);
|
||||||
|
};
|
||||||
|
this._ws.onerror = () => {
|
||||||
|
this.emit('error', new Error('WebSocket error'));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this._ws.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
send(data, callback) {
|
||||||
|
try {
|
||||||
|
this._ws.send(data);
|
||||||
|
callback();
|
||||||
|
} catch (err) {
|
||||||
|
callback(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = WebSocketWrapper;
|
||||||
21
engine/ds-screencast/node_modules/chrome-remote-interface/node_modules/ws/LICENSE
generated
vendored
Normal file
21
engine/ds-screencast/node_modules/chrome-remote-interface/node_modules/ws/LICENSE
generated
vendored
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2011 Einar Otto Stangvik <einaros@gmail.com>
|
||||||
|
|
||||||
|
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.
|
||||||
495
engine/ds-screencast/node_modules/chrome-remote-interface/node_modules/ws/README.md
generated
vendored
Normal file
495
engine/ds-screencast/node_modules/chrome-remote-interface/node_modules/ws/README.md
generated
vendored
Normal file
|
|
@ -0,0 +1,495 @@
|
||||||
|
# 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 back end with the role of a client in the WebSocket
|
||||||
|
communication. Browser clients must use the native
|
||||||
|
[`WebSocket`](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket)
|
||||||
|
object. To make the same code work seamlessly on Node.js and the browser, you
|
||||||
|
can use one of the many wrappers available on npm, like
|
||||||
|
[isomorphic-ws](https://github.com/heineiuo/isomorphic-ws).
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
- [Protocol support](#protocol-support)
|
||||||
|
- [Installing](#installing)
|
||||||
|
- [Opt-in for performance](#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)
|
||||||
|
- [echo.websocket.org demo](#echowebsocketorg-demo)
|
||||||
|
- [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
|
||||||
|
|
||||||
|
There are 2 optional modules that can be installed along side with the ws
|
||||||
|
module. These modules are binary addons which improve certain operations.
|
||||||
|
Prebuilt binaries are available for the most popular platforms so you don't
|
||||||
|
necessarily need to have a C++ compiler installed on your machine.
|
||||||
|
|
||||||
|
- `npm install --save-optional bufferutil`: Allows to efficiently perform
|
||||||
|
operations such as masking and unmasking the data payload of the WebSocket
|
||||||
|
frames.
|
||||||
|
- `npm install --save-optional utf-8-validate`: Allows to efficiently check if a
|
||||||
|
message contains valid UTF-8.
|
||||||
|
|
||||||
|
## 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
|
||||||
|
const WebSocket = require('ws');
|
||||||
|
|
||||||
|
const wss = new WebSocket.Server({
|
||||||
|
port: 8080,
|
||||||
|
perMessageDeflate: {
|
||||||
|
zlibDeflateOptions: {
|
||||||
|
// See zlib defaults.
|
||||||
|
chunkSize: 1024,
|
||||||
|
memLevel: 7,
|
||||||
|
level: 3
|
||||||
|
},
|
||||||
|
zlibInflateOptions: {
|
||||||
|
chunkSize: 10 * 1024
|
||||||
|
},
|
||||||
|
// Other options settable:
|
||||||
|
clientNoContextTakeover: true, // Defaults to negotiated value.
|
||||||
|
serverNoContextTakeover: true, // Defaults to negotiated value.
|
||||||
|
serverMaxWindowBits: 10, // Defaults to negotiated value.
|
||||||
|
// Below options specified as default values.
|
||||||
|
concurrencyLimit: 10, // Limits zlib concurrency for perf.
|
||||||
|
threshold: 1024 // Size (in bytes) below which messages
|
||||||
|
// should not be compressed.
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
The client will only use the extension if it is supported and enabled on the
|
||||||
|
server. To always disable the extension on the client set the
|
||||||
|
`perMessageDeflate` option to `false`.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const WebSocket = require('ws');
|
||||||
|
|
||||||
|
const ws = new WebSocket('ws://www.host.com/path', {
|
||||||
|
perMessageDeflate: false
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage examples
|
||||||
|
|
||||||
|
### Sending and receiving text data
|
||||||
|
|
||||||
|
```js
|
||||||
|
const WebSocket = require('ws');
|
||||||
|
|
||||||
|
const ws = new WebSocket('ws://www.host.com/path');
|
||||||
|
|
||||||
|
ws.on('open', function open() {
|
||||||
|
ws.send('something');
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.on('message', function incoming(data) {
|
||||||
|
console.log(data);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sending binary data
|
||||||
|
|
||||||
|
```js
|
||||||
|
const WebSocket = require('ws');
|
||||||
|
|
||||||
|
const ws = new WebSocket('ws://www.host.com/path');
|
||||||
|
|
||||||
|
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
|
||||||
|
const WebSocket = require('ws');
|
||||||
|
|
||||||
|
const wss = new WebSocket.Server({ port: 8080 });
|
||||||
|
|
||||||
|
wss.on('connection', function connection(ws) {
|
||||||
|
ws.on('message', function incoming(message) {
|
||||||
|
console.log('received: %s', message);
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.send('something');
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### External HTTP/S server
|
||||||
|
|
||||||
|
```js
|
||||||
|
const fs = require('fs');
|
||||||
|
const https = require('https');
|
||||||
|
const WebSocket = require('ws');
|
||||||
|
|
||||||
|
const server = https.createServer({
|
||||||
|
cert: fs.readFileSync('/path/to/cert.pem'),
|
||||||
|
key: fs.readFileSync('/path/to/key.pem')
|
||||||
|
});
|
||||||
|
const wss = new WebSocket.Server({ server });
|
||||||
|
|
||||||
|
wss.on('connection', function connection(ws) {
|
||||||
|
ws.on('message', function incoming(message) {
|
||||||
|
console.log('received: %s', message);
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.send('something');
|
||||||
|
});
|
||||||
|
|
||||||
|
server.listen(8080);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Multiple servers sharing a single HTTP/S server
|
||||||
|
|
||||||
|
```js
|
||||||
|
const http = require('http');
|
||||||
|
const WebSocket = require('ws');
|
||||||
|
const url = require('url');
|
||||||
|
|
||||||
|
const server = http.createServer();
|
||||||
|
const wss1 = new WebSocket.Server({ noServer: true });
|
||||||
|
const wss2 = new WebSocket.Server({ noServer: true });
|
||||||
|
|
||||||
|
wss1.on('connection', function connection(ws) {
|
||||||
|
// ...
|
||||||
|
});
|
||||||
|
|
||||||
|
wss2.on('connection', function connection(ws) {
|
||||||
|
// ...
|
||||||
|
});
|
||||||
|
|
||||||
|
server.on('upgrade', function upgrade(request, socket, head) {
|
||||||
|
const pathname = url.parse(request.url).pathname;
|
||||||
|
|
||||||
|
if (pathname === '/foo') {
|
||||||
|
wss1.handleUpgrade(request, socket, head, function done(ws) {
|
||||||
|
wss1.emit('connection', ws, request);
|
||||||
|
});
|
||||||
|
} else if (pathname === '/bar') {
|
||||||
|
wss2.handleUpgrade(request, socket, head, function done(ws) {
|
||||||
|
wss2.emit('connection', ws, request);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
socket.destroy();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.listen(8080);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Client authentication
|
||||||
|
|
||||||
|
```js
|
||||||
|
const http = require('http');
|
||||||
|
const WebSocket = require('ws');
|
||||||
|
|
||||||
|
const server = http.createServer();
|
||||||
|
const wss = new WebSocket.Server({ noServer: true });
|
||||||
|
|
||||||
|
wss.on('connection', function connection(ws, request, client) {
|
||||||
|
ws.on('message', function message(msg) {
|
||||||
|
console.log(`Received message ${msg} from user ${client}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
server.on('upgrade', function upgrade(request, socket, head) {
|
||||||
|
// This function is not defined on purpose. Implement it with your own logic.
|
||||||
|
authenticate(request, (err, client) => {
|
||||||
|
if (err || !client) {
|
||||||
|
socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
|
||||||
|
socket.destroy();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
wss.handleUpgrade(request, socket, head, function done(ws) {
|
||||||
|
wss.emit('connection', ws, request, client);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
server.listen(8080);
|
||||||
|
```
|
||||||
|
|
||||||
|
Also see the provided [example][session-parse-example] using `express-session`.
|
||||||
|
|
||||||
|
### Server broadcast
|
||||||
|
|
||||||
|
A client WebSocket broadcasting to all connected WebSocket clients, including
|
||||||
|
itself.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const WebSocket = require('ws');
|
||||||
|
|
||||||
|
const wss = new WebSocket.Server({ port: 8080 });
|
||||||
|
|
||||||
|
wss.on('connection', function connection(ws) {
|
||||||
|
ws.on('message', function incoming(data) {
|
||||||
|
wss.clients.forEach(function each(client) {
|
||||||
|
if (client.readyState === WebSocket.OPEN) {
|
||||||
|
client.send(data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
A client WebSocket broadcasting to every other connected WebSocket clients,
|
||||||
|
excluding itself.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const WebSocket = require('ws');
|
||||||
|
|
||||||
|
const wss = new WebSocket.Server({ port: 8080 });
|
||||||
|
|
||||||
|
wss.on('connection', function connection(ws) {
|
||||||
|
ws.on('message', function incoming(data) {
|
||||||
|
wss.clients.forEach(function each(client) {
|
||||||
|
if (client !== ws && client.readyState === WebSocket.OPEN) {
|
||||||
|
client.send(data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### echo.websocket.org demo
|
||||||
|
|
||||||
|
```js
|
||||||
|
const WebSocket = require('ws');
|
||||||
|
|
||||||
|
const ws = new WebSocket('wss://echo.websocket.org/', {
|
||||||
|
origin: 'https://websocket.org'
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.on('open', function open() {
|
||||||
|
console.log('connected');
|
||||||
|
ws.send(Date.now());
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.on('close', function close() {
|
||||||
|
console.log('disconnected');
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.on('message', function incoming(data) {
|
||||||
|
console.log(`Roundtrip time: ${Date.now() - data} ms`);
|
||||||
|
|
||||||
|
setTimeout(function timeout() {
|
||||||
|
ws.send(Date.now());
|
||||||
|
}, 500);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Use the Node.js streams API
|
||||||
|
|
||||||
|
```js
|
||||||
|
const WebSocket = require('ws');
|
||||||
|
|
||||||
|
const ws = new WebSocket('wss://echo.websocket.org/', {
|
||||||
|
origin: 'https://websocket.org'
|
||||||
|
});
|
||||||
|
|
||||||
|
const duplex = WebSocket.createWebSocketStream(ws, { encoding: 'utf8' });
|
||||||
|
|
||||||
|
duplex.pipe(process.stdout);
|
||||||
|
process.stdin.pipe(duplex);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Other examples
|
||||||
|
|
||||||
|
For a full example with a browser client communicating with a ws server, see the
|
||||||
|
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
|
||||||
|
const WebSocket = require('ws');
|
||||||
|
|
||||||
|
const wss = new WebSocket.Server({ port: 8080 });
|
||||||
|
|
||||||
|
wss.on('connection', function connection(ws, req) {
|
||||||
|
const ip = req.socket.remoteAddress;
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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
|
||||||
|
const WebSocket = require('ws');
|
||||||
|
|
||||||
|
function noop() {}
|
||||||
|
|
||||||
|
function heartbeat() {
|
||||||
|
this.isAlive = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const wss = new WebSocket.Server({ port: 8080 });
|
||||||
|
|
||||||
|
wss.on('connection', function connection(ws) {
|
||||||
|
ws.isAlive = true;
|
||||||
|
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(noop);
|
||||||
|
});
|
||||||
|
}, 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
|
||||||
|
const WebSocket = require('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://echo.websocket.org/');
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
[changelog]: https://github.com/websockets/ws/releases
|
||||||
|
[client-report]: http://websockets.github.io/ws/autobahn/clients/
|
||||||
|
[https-proxy-agent]: https://github.com/TooTallNate/node-https-proxy-agent
|
||||||
|
[node-zlib-bug]: https://github.com/nodejs/node/issues/8871
|
||||||
|
[node-zlib-deflaterawdocs]:
|
||||||
|
https://nodejs.org/api/zlib.html#zlib_zlib_createdeflateraw_options
|
||||||
|
[permessage-deflate]: https://tools.ietf.org/html/rfc7692
|
||||||
|
[server-report]: http://websockets.github.io/ws/autobahn/servers/
|
||||||
|
[session-parse-example]: ./examples/express-session-parse
|
||||||
|
[socks-proxy-agent]: https://github.com/TooTallNate/node-socks-proxy-agent
|
||||||
|
[ws-server-options]:
|
||||||
|
https://github.com/websockets/ws/blob/master/doc/ws.md#new-websocketserveroptions-callback
|
||||||
8
engine/ds-screencast/node_modules/chrome-remote-interface/node_modules/ws/browser.js
generated
vendored
Normal file
8
engine/ds-screencast/node_modules/chrome-remote-interface/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'
|
||||||
|
);
|
||||||
|
};
|
||||||
10
engine/ds-screencast/node_modules/chrome-remote-interface/node_modules/ws/index.js
generated
vendored
Normal file
10
engine/ds-screencast/node_modules/chrome-remote-interface/node_modules/ws/index.js
generated
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const WebSocket = require('./lib/websocket');
|
||||||
|
|
||||||
|
WebSocket.createWebSocketStream = require('./lib/stream');
|
||||||
|
WebSocket.Server = require('./lib/websocket-server');
|
||||||
|
WebSocket.Receiver = require('./lib/receiver');
|
||||||
|
WebSocket.Sender = require('./lib/sender');
|
||||||
|
|
||||||
|
module.exports = WebSocket;
|
||||||
129
engine/ds-screencast/node_modules/chrome-remote-interface/node_modules/ws/lib/buffer-util.js
generated
vendored
Normal file
129
engine/ds-screencast/node_modules/chrome-remote-interface/node_modules/ws/lib/buffer-util.js
generated
vendored
Normal file
|
|
@ -0,0 +1,129 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const { EMPTY_BUFFER } = require('./constants');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 target.slice(0, 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) {
|
||||||
|
// Required until https://github.com/nodejs/node/issues/9006 is resolved.
|
||||||
|
const length = buffer.length;
|
||||||
|
for (let i = 0; i < 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.byteLength === buf.buffer.byteLength) {
|
||||||
|
return buf.buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 = Buffer.from(data);
|
||||||
|
} else if (ArrayBuffer.isView(data)) {
|
||||||
|
buf = Buffer.from(data.buffer, data.byteOffset, data.byteLength);
|
||||||
|
} else {
|
||||||
|
buf = Buffer.from(data);
|
||||||
|
toBuffer.readOnly = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const bufferUtil = require('bufferutil');
|
||||||
|
const bu = bufferUtil.BufferUtil || bufferUtil;
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
concat,
|
||||||
|
mask(source, mask, output, offset, length) {
|
||||||
|
if (length < 48) _mask(source, mask, output, offset, length);
|
||||||
|
else bu.mask(source, mask, output, offset, length);
|
||||||
|
},
|
||||||
|
toArrayBuffer,
|
||||||
|
toBuffer,
|
||||||
|
unmask(buffer, mask) {
|
||||||
|
if (buffer.length < 32) _unmask(buffer, mask);
|
||||||
|
else bu.unmask(buffer, mask);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} catch (e) /* istanbul ignore next */ {
|
||||||
|
module.exports = {
|
||||||
|
concat,
|
||||||
|
mask: _mask,
|
||||||
|
toArrayBuffer,
|
||||||
|
toBuffer,
|
||||||
|
unmask: _unmask
|
||||||
|
};
|
||||||
|
}
|
||||||
10
engine/ds-screencast/node_modules/chrome-remote-interface/node_modules/ws/lib/constants.js
generated
vendored
Normal file
10
engine/ds-screencast/node_modules/chrome-remote-interface/node_modules/ws/lib/constants.js
generated
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
BINARY_TYPES: ['nodebuffer', 'arraybuffer', 'fragments'],
|
||||||
|
GUID: '258EAFA5-E914-47DA-95CA-C5AB0DC85B11',
|
||||||
|
kStatusCode: Symbol('status-code'),
|
||||||
|
kWebSocket: Symbol('websocket'),
|
||||||
|
EMPTY_BUFFER: Buffer.alloc(0),
|
||||||
|
NOOP: () => {}
|
||||||
|
};
|
||||||
184
engine/ds-screencast/node_modules/chrome-remote-interface/node_modules/ws/lib/event-target.js
generated
vendored
Normal file
184
engine/ds-screencast/node_modules/chrome-remote-interface/node_modules/ws/lib/event-target.js
generated
vendored
Normal file
|
|
@ -0,0 +1,184 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class representing an event.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
class Event {
|
||||||
|
/**
|
||||||
|
* Create a new `Event`.
|
||||||
|
*
|
||||||
|
* @param {String} type The name of the event
|
||||||
|
* @param {Object} target A reference to the target to which the event was
|
||||||
|
* dispatched
|
||||||
|
*/
|
||||||
|
constructor(type, target) {
|
||||||
|
this.target = target;
|
||||||
|
this.type = type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class representing a message event.
|
||||||
|
*
|
||||||
|
* @extends Event
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
class MessageEvent extends Event {
|
||||||
|
/**
|
||||||
|
* Create a new `MessageEvent`.
|
||||||
|
*
|
||||||
|
* @param {(String|Buffer|ArrayBuffer|Buffer[])} data The received data
|
||||||
|
* @param {WebSocket} target A reference to the target to which the event was
|
||||||
|
* dispatched
|
||||||
|
*/
|
||||||
|
constructor(data, target) {
|
||||||
|
super('message', target);
|
||||||
|
|
||||||
|
this.data = data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class representing a close event.
|
||||||
|
*
|
||||||
|
* @extends Event
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
class CloseEvent extends Event {
|
||||||
|
/**
|
||||||
|
* Create a new `CloseEvent`.
|
||||||
|
*
|
||||||
|
* @param {Number} code The status code explaining why the connection is being
|
||||||
|
* closed
|
||||||
|
* @param {String} reason A human-readable string explaining why the
|
||||||
|
* connection is closing
|
||||||
|
* @param {WebSocket} target A reference to the target to which the event was
|
||||||
|
* dispatched
|
||||||
|
*/
|
||||||
|
constructor(code, reason, target) {
|
||||||
|
super('close', target);
|
||||||
|
|
||||||
|
this.wasClean = target._closeFrameReceived && target._closeFrameSent;
|
||||||
|
this.reason = reason;
|
||||||
|
this.code = code;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class representing an open event.
|
||||||
|
*
|
||||||
|
* @extends Event
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
class OpenEvent extends Event {
|
||||||
|
/**
|
||||||
|
* Create a new `OpenEvent`.
|
||||||
|
*
|
||||||
|
* @param {WebSocket} target A reference to the target to which the event was
|
||||||
|
* dispatched
|
||||||
|
*/
|
||||||
|
constructor(target) {
|
||||||
|
super('open', target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class representing an error event.
|
||||||
|
*
|
||||||
|
* @extends Event
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
class ErrorEvent extends Event {
|
||||||
|
/**
|
||||||
|
* Create a new `ErrorEvent`.
|
||||||
|
*
|
||||||
|
* @param {Object} error The error that generated this event
|
||||||
|
* @param {WebSocket} target A reference to the target to which the event was
|
||||||
|
* dispatched
|
||||||
|
*/
|
||||||
|
constructor(error, target) {
|
||||||
|
super('error', target);
|
||||||
|
|
||||||
|
this.message = error.message;
|
||||||
|
this.error = error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This provides methods for emulating the `EventTarget` interface. It's not
|
||||||
|
* meant to be used directly.
|
||||||
|
*
|
||||||
|
* @mixin
|
||||||
|
*/
|
||||||
|
const EventTarget = {
|
||||||
|
/**
|
||||||
|
* Register an event listener.
|
||||||
|
*
|
||||||
|
* @param {String} type A string representing the event type to listen for
|
||||||
|
* @param {Function} listener 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, listener, options) {
|
||||||
|
if (typeof listener !== 'function') return;
|
||||||
|
|
||||||
|
function onMessage(data) {
|
||||||
|
listener.call(this, new MessageEvent(data, this));
|
||||||
|
}
|
||||||
|
|
||||||
|
function onClose(code, message) {
|
||||||
|
listener.call(this, new CloseEvent(code, message, this));
|
||||||
|
}
|
||||||
|
|
||||||
|
function onError(error) {
|
||||||
|
listener.call(this, new ErrorEvent(error, this));
|
||||||
|
}
|
||||||
|
|
||||||
|
function onOpen() {
|
||||||
|
listener.call(this, new OpenEvent(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
const method = options && options.once ? 'once' : 'on';
|
||||||
|
|
||||||
|
if (type === 'message') {
|
||||||
|
onMessage._listener = listener;
|
||||||
|
this[method](type, onMessage);
|
||||||
|
} else if (type === 'close') {
|
||||||
|
onClose._listener = listener;
|
||||||
|
this[method](type, onClose);
|
||||||
|
} else if (type === 'error') {
|
||||||
|
onError._listener = listener;
|
||||||
|
this[method](type, onError);
|
||||||
|
} else if (type === 'open') {
|
||||||
|
onOpen._listener = listener;
|
||||||
|
this[method](type, onOpen);
|
||||||
|
} else {
|
||||||
|
this[method](type, listener);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove an event listener.
|
||||||
|
*
|
||||||
|
* @param {String} type A string representing the event type to remove
|
||||||
|
* @param {Function} listener The listener to remove
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
removeEventListener(type, listener) {
|
||||||
|
const listeners = this.listeners(type);
|
||||||
|
|
||||||
|
for (let i = 0; i < listeners.length; i++) {
|
||||||
|
if (listeners[i] === listener || listeners[i]._listener === listener) {
|
||||||
|
this.removeListener(type, listeners[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = EventTarget;
|
||||||
223
engine/ds-screencast/node_modules/chrome-remote-interface/node_modules/ws/lib/extension.js
generated
vendored
Normal file
223
engine/ds-screencast/node_modules/chrome-remote-interface/node_modules/ws/lib/extension.js
generated
vendored
Normal file
|
|
@ -0,0 +1,223 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
//
|
||||||
|
// 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
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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);
|
||||||
|
|
||||||
|
if (header === undefined || header === '') return offers;
|
||||||
|
|
||||||
|
let params = Object.create(null);
|
||||||
|
let mustUnescape = false;
|
||||||
|
let isEscaping = false;
|
||||||
|
let inQuotes = false;
|
||||||
|
let extensionName;
|
||||||
|
let paramName;
|
||||||
|
let start = -1;
|
||||||
|
let end = -1;
|
||||||
|
let i = 0;
|
||||||
|
|
||||||
|
for (; i < header.length; i++) {
|
||||||
|
const code = header.charCodeAt(i);
|
||||||
|
|
||||||
|
if (extensionName === undefined) {
|
||||||
|
if (end === -1 && tokenChars[code] === 1) {
|
||||||
|
if (start === -1) start = i;
|
||||||
|
} else if (code === 0x20 /* ' ' */ || code === 0x09 /* '\t' */) {
|
||||||
|
if (end === -1 && start !== -1) end = i;
|
||||||
|
} else if (code === 0x3b /* ';' */ || code === 0x2c /* ',' */) {
|
||||||
|
if (start === -1) {
|
||||||
|
throw new SyntaxError(`Unexpected character at index ${i}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (end === -1) end = i;
|
||||||
|
const name = header.slice(start, end);
|
||||||
|
if (code === 0x2c) {
|
||||||
|
push(offers, name, params);
|
||||||
|
params = 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) {
|
||||||
|
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
engine/ds-screencast/node_modules/chrome-remote-interface/node_modules/ws/lib/limiter.js
generated
vendored
Normal file
55
engine/ds-screencast/node_modules/chrome-remote-interface/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;
|
||||||
518
engine/ds-screencast/node_modules/chrome-remote-interface/node_modules/ws/lib/permessage-deflate.js
generated
vendored
Normal file
518
engine/ds-screencast/node_modules/chrome-remote-interface/node_modules/ws/lib/permessage-deflate.js
generated
vendored
Normal file
|
|
@ -0,0 +1,518 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const zlib = require('zlib');
|
||||||
|
|
||||||
|
const bufferUtil = require('./buffer-util');
|
||||||
|
const Limiter = require('./limiter');
|
||||||
|
const { kStatusCode, NOOP } = require('./constants');
|
||||||
|
|
||||||
|
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} [options.serverNoContextTakeover=false] Request/accept
|
||||||
|
* disabling of server context takeover
|
||||||
|
* @param {Boolean} [options.clientNoContextTakeover=false] Advertise/
|
||||||
|
* acknowledge disabling of client context takeover
|
||||||
|
* @param {(Boolean|Number)} [options.serverMaxWindowBits] Request/confirm the
|
||||||
|
* use of a custom server window size
|
||||||
|
* @param {(Boolean|Number)} [options.clientMaxWindowBits] Advertise support
|
||||||
|
* for, or request, a custom client window size
|
||||||
|
* @param {Object} [options.zlibDeflateOptions] Options to pass to zlib on
|
||||||
|
* deflate
|
||||||
|
* @param {Object} [options.zlibInflateOptions] Options to pass to zlib on
|
||||||
|
* inflate
|
||||||
|
* @param {Number} [options.threshold=1024] Size (in bytes) below which
|
||||||
|
* messages should not be compressed
|
||||||
|
* @param {Number} [options.concurrencyLimit=10] The number of concurrent
|
||||||
|
* calls to zlib
|
||||||
|
* @param {Boolean} [isServer=false] Create the instance in either server or
|
||||||
|
* client mode
|
||||||
|
* @param {Number} [maxPayload=0] The maximum allowed message length
|
||||||
|
*/
|
||||||
|
constructor(options, isServer, maxPayload) {
|
||||||
|
this._maxPayload = maxPayload | 0;
|
||||||
|
this._options = options || {};
|
||||||
|
this._threshold =
|
||||||
|
this._options.threshold !== undefined ? this._options.threshold : 1024;
|
||||||
|
this._isServer = !!isServer;
|
||||||
|
this._deflate = null;
|
||||||
|
this._inflate = null;
|
||||||
|
|
||||||
|
this.params = null;
|
||||||
|
|
||||||
|
if (!zlibLimiter) {
|
||||||
|
const concurrency =
|
||||||
|
this._options.concurrencyLimit !== undefined
|
||||||
|
? this._options.concurrencyLimit
|
||||||
|
: 10;
|
||||||
|
zlibLimiter = new Limiter(concurrency);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {String}
|
||||||
|
*/
|
||||||
|
static get extensionName() {
|
||||||
|
return 'permessage-deflate';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an extension negotiation offer.
|
||||||
|
*
|
||||||
|
* @return {Object} Extension parameters
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
offer() {
|
||||||
|
const params = {};
|
||||||
|
|
||||||
|
if (this._options.serverNoContextTakeover) {
|
||||||
|
params.server_no_context_takeover = true;
|
||||||
|
}
|
||||||
|
if (this._options.clientNoContextTakeover) {
|
||||||
|
params.client_no_context_takeover = true;
|
||||||
|
}
|
||||||
|
if (this._options.serverMaxWindowBits) {
|
||||||
|
params.server_max_window_bits = this._options.serverMaxWindowBits;
|
||||||
|
}
|
||||||
|
if (this._options.clientMaxWindowBits) {
|
||||||
|
params.client_max_window_bits = this._options.clientMaxWindowBits;
|
||||||
|
} else if (this._options.clientMaxWindowBits == null) {
|
||||||
|
params.client_max_window_bits = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return params;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Accept an extension negotiation offer/response.
|
||||||
|
*
|
||||||
|
* @param {Array} configurations The extension negotiation offers/reponse
|
||||||
|
* @return {Object} Accepted configuration
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
accept(configurations) {
|
||||||
|
configurations = this.normalizeParams(configurations);
|
||||||
|
|
||||||
|
this.params = this._isServer
|
||||||
|
? this.acceptAsServer(configurations)
|
||||||
|
: this.acceptAsClient(configurations);
|
||||||
|
|
||||||
|
return this.params;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Releases all resources used by the extension.
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
cleanup() {
|
||||||
|
if (this._inflate) {
|
||||||
|
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} 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} 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] = [];
|
||||||
|
|
||||||
|
//
|
||||||
|
// An `'error'` event is emitted, only on Node.js < 10.0.0, if the
|
||||||
|
// `zlib.DeflateRaw` instance is closed while data is being processed.
|
||||||
|
// This can happen if `PerMessageDeflate#cleanup()` is called at the wrong
|
||||||
|
// time due to an abnormal WebSocket closure.
|
||||||
|
//
|
||||||
|
this._deflate.on('error', NOOP);
|
||||||
|
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 = data.slice(0, 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);
|
||||||
|
this.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The listener of the `zlib.InflateRaw` stream `'error'` event.
|
||||||
|
*
|
||||||
|
* @param {Error} err The emitted error
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
function inflateOnError(err) {
|
||||||
|
//
|
||||||
|
// There is no need to call `Zlib#close()` as the handle is automatically
|
||||||
|
// closed when an error is emitted.
|
||||||
|
//
|
||||||
|
this[kPerMessageDeflate]._inflate = null;
|
||||||
|
err[kStatusCode] = 1007;
|
||||||
|
this[kCallback](err);
|
||||||
|
}
|
||||||
607
engine/ds-screencast/node_modules/chrome-remote-interface/node_modules/ws/lib/receiver.js
generated
vendored
Normal file
607
engine/ds-screencast/node_modules/chrome-remote-interface/node_modules/ws/lib/receiver.js
generated
vendored
Normal file
|
|
@ -0,0 +1,607 @@
|
||||||
|
'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 GET_INFO = 0;
|
||||||
|
const GET_PAYLOAD_LENGTH_16 = 1;
|
||||||
|
const GET_PAYLOAD_LENGTH_64 = 2;
|
||||||
|
const GET_MASK = 3;
|
||||||
|
const GET_DATA = 4;
|
||||||
|
const INFLATING = 5;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HyBi Receiver implementation.
|
||||||
|
*
|
||||||
|
* @extends Writable
|
||||||
|
*/
|
||||||
|
class Receiver extends Writable {
|
||||||
|
/**
|
||||||
|
* Creates a Receiver instance.
|
||||||
|
*
|
||||||
|
* @param {String} [binaryType=nodebuffer] The type for binary data
|
||||||
|
* @param {Object} [extensions] An object containing the negotiated extensions
|
||||||
|
* @param {Boolean} [isServer=false] Specifies whether to operate in client or
|
||||||
|
* server mode
|
||||||
|
* @param {Number} [maxPayload=0] The maximum allowed message length
|
||||||
|
*/
|
||||||
|
constructor(binaryType, extensions, isServer, maxPayload) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this._binaryType = binaryType || BINARY_TYPES[0];
|
||||||
|
this[kWebSocket] = undefined;
|
||||||
|
this._extensions = extensions || {};
|
||||||
|
this._isServer = !!isServer;
|
||||||
|
this._maxPayload = maxPayload | 0;
|
||||||
|
|
||||||
|
this._bufferedBytes = 0;
|
||||||
|
this._buffers = [];
|
||||||
|
|
||||||
|
this._compressed = false;
|
||||||
|
this._payloadLength = 0;
|
||||||
|
this._mask = undefined;
|
||||||
|
this._fragmented = 0;
|
||||||
|
this._masked = false;
|
||||||
|
this._fin = false;
|
||||||
|
this._opcode = 0;
|
||||||
|
|
||||||
|
this._totalPayloadLength = 0;
|
||||||
|
this._messageLength = 0;
|
||||||
|
this._fragments = [];
|
||||||
|
|
||||||
|
this._state = GET_INFO;
|
||||||
|
this._loop = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements `Writable.prototype._write()`.
|
||||||
|
*
|
||||||
|
* @param {Buffer} chunk The chunk of data to write
|
||||||
|
* @param {String} encoding The character encoding of `chunk`
|
||||||
|
* @param {Function} cb Callback
|
||||||
|
* @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] = buf.slice(n);
|
||||||
|
return buf.slice(0, 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] = buf.slice(n);
|
||||||
|
}
|
||||||
|
|
||||||
|
n -= buf.length;
|
||||||
|
} while (n > 0);
|
||||||
|
|
||||||
|
return dst;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts the parsing loop.
|
||||||
|
*
|
||||||
|
* @param {Function} cb Callback
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
startLoop(cb) {
|
||||||
|
let err;
|
||||||
|
this._loop = true;
|
||||||
|
|
||||||
|
do {
|
||||||
|
switch (this._state) {
|
||||||
|
case GET_INFO:
|
||||||
|
err = this.getInfo();
|
||||||
|
break;
|
||||||
|
case GET_PAYLOAD_LENGTH_16:
|
||||||
|
err = this.getPayloadLength16();
|
||||||
|
break;
|
||||||
|
case GET_PAYLOAD_LENGTH_64:
|
||||||
|
err = this.getPayloadLength64();
|
||||||
|
break;
|
||||||
|
case GET_MASK:
|
||||||
|
this.getMask();
|
||||||
|
break;
|
||||||
|
case GET_DATA:
|
||||||
|
err = this.getData(cb);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// `INFLATING`
|
||||||
|
this._loop = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} while (this._loop);
|
||||||
|
|
||||||
|
cb(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads the first two bytes of a frame.
|
||||||
|
*
|
||||||
|
* @return {(RangeError|undefined)} A possible error
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
getInfo() {
|
||||||
|
if (this._bufferedBytes < 2) {
|
||||||
|
this._loop = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const buf = this.consume(2);
|
||||||
|
|
||||||
|
if ((buf[0] & 0x30) !== 0x00) {
|
||||||
|
this._loop = false;
|
||||||
|
return error(
|
||||||
|
RangeError,
|
||||||
|
'RSV2 and RSV3 must be clear',
|
||||||
|
true,
|
||||||
|
1002,
|
||||||
|
'WS_ERR_UNEXPECTED_RSV_2_3'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const compressed = (buf[0] & 0x40) === 0x40;
|
||||||
|
|
||||||
|
if (compressed && !this._extensions[PerMessageDeflate.extensionName]) {
|
||||||
|
this._loop = false;
|
||||||
|
return error(
|
||||||
|
RangeError,
|
||||||
|
'RSV1 must be clear',
|
||||||
|
true,
|
||||||
|
1002,
|
||||||
|
'WS_ERR_UNEXPECTED_RSV_1'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._fin = (buf[0] & 0x80) === 0x80;
|
||||||
|
this._opcode = buf[0] & 0x0f;
|
||||||
|
this._payloadLength = buf[1] & 0x7f;
|
||||||
|
|
||||||
|
if (this._opcode === 0x00) {
|
||||||
|
if (compressed) {
|
||||||
|
this._loop = false;
|
||||||
|
return error(
|
||||||
|
RangeError,
|
||||||
|
'RSV1 must be clear',
|
||||||
|
true,
|
||||||
|
1002,
|
||||||
|
'WS_ERR_UNEXPECTED_RSV_1'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this._fragmented) {
|
||||||
|
this._loop = false;
|
||||||
|
return error(
|
||||||
|
RangeError,
|
||||||
|
'invalid opcode 0',
|
||||||
|
true,
|
||||||
|
1002,
|
||||||
|
'WS_ERR_INVALID_OPCODE'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._opcode = this._fragmented;
|
||||||
|
} else if (this._opcode === 0x01 || this._opcode === 0x02) {
|
||||||
|
if (this._fragmented) {
|
||||||
|
this._loop = false;
|
||||||
|
return error(
|
||||||
|
RangeError,
|
||||||
|
`invalid opcode ${this._opcode}`,
|
||||||
|
true,
|
||||||
|
1002,
|
||||||
|
'WS_ERR_INVALID_OPCODE'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._compressed = compressed;
|
||||||
|
} else if (this._opcode > 0x07 && this._opcode < 0x0b) {
|
||||||
|
if (!this._fin) {
|
||||||
|
this._loop = false;
|
||||||
|
return error(
|
||||||
|
RangeError,
|
||||||
|
'FIN must be set',
|
||||||
|
true,
|
||||||
|
1002,
|
||||||
|
'WS_ERR_EXPECTED_FIN'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (compressed) {
|
||||||
|
this._loop = false;
|
||||||
|
return error(
|
||||||
|
RangeError,
|
||||||
|
'RSV1 must be clear',
|
||||||
|
true,
|
||||||
|
1002,
|
||||||
|
'WS_ERR_UNEXPECTED_RSV_1'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._payloadLength > 0x7d) {
|
||||||
|
this._loop = false;
|
||||||
|
return error(
|
||||||
|
RangeError,
|
||||||
|
`invalid payload length ${this._payloadLength}`,
|
||||||
|
true,
|
||||||
|
1002,
|
||||||
|
'WS_ERR_INVALID_CONTROL_PAYLOAD_LENGTH'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this._loop = false;
|
||||||
|
return error(
|
||||||
|
RangeError,
|
||||||
|
`invalid opcode ${this._opcode}`,
|
||||||
|
true,
|
||||||
|
1002,
|
||||||
|
'WS_ERR_INVALID_OPCODE'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this._fin && !this._fragmented) this._fragmented = this._opcode;
|
||||||
|
this._masked = (buf[1] & 0x80) === 0x80;
|
||||||
|
|
||||||
|
if (this._isServer) {
|
||||||
|
if (!this._masked) {
|
||||||
|
this._loop = false;
|
||||||
|
return error(
|
||||||
|
RangeError,
|
||||||
|
'MASK must be set',
|
||||||
|
true,
|
||||||
|
1002,
|
||||||
|
'WS_ERR_EXPECTED_MASK'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if (this._masked) {
|
||||||
|
this._loop = false;
|
||||||
|
return error(
|
||||||
|
RangeError,
|
||||||
|
'MASK must be clear',
|
||||||
|
true,
|
||||||
|
1002,
|
||||||
|
'WS_ERR_UNEXPECTED_MASK'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._payloadLength === 126) this._state = GET_PAYLOAD_LENGTH_16;
|
||||||
|
else if (this._payloadLength === 127) this._state = GET_PAYLOAD_LENGTH_64;
|
||||||
|
else return this.haveLength();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets extended payload length (7+16).
|
||||||
|
*
|
||||||
|
* @return {(RangeError|undefined)} A possible error
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
getPayloadLength16() {
|
||||||
|
if (this._bufferedBytes < 2) {
|
||||||
|
this._loop = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._payloadLength = this.consume(2).readUInt16BE(0);
|
||||||
|
return this.haveLength();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets extended payload length (7+64).
|
||||||
|
*
|
||||||
|
* @return {(RangeError|undefined)} A possible error
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
getPayloadLength64() {
|
||||||
|
if (this._bufferedBytes < 8) {
|
||||||
|
this._loop = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const buf = this.consume(8);
|
||||||
|
const num = buf.readUInt32BE(0);
|
||||||
|
|
||||||
|
//
|
||||||
|
// The maximum safe integer in JavaScript is 2^53 - 1. An error is returned
|
||||||
|
// if payload length is greater than this number.
|
||||||
|
//
|
||||||
|
if (num > Math.pow(2, 53 - 32) - 1) {
|
||||||
|
this._loop = false;
|
||||||
|
return error(
|
||||||
|
RangeError,
|
||||||
|
'Unsupported WebSocket frame: payload length > 2^53 - 1',
|
||||||
|
false,
|
||||||
|
1009,
|
||||||
|
'WS_ERR_UNSUPPORTED_DATA_PAYLOAD_LENGTH'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._payloadLength = num * Math.pow(2, 32) + buf.readUInt32BE(4);
|
||||||
|
return this.haveLength();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Payload length has been read.
|
||||||
|
*
|
||||||
|
* @return {(RangeError|undefined)} A possible error
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
haveLength() {
|
||||||
|
if (this._payloadLength && this._opcode < 0x08) {
|
||||||
|
this._totalPayloadLength += this._payloadLength;
|
||||||
|
if (this._totalPayloadLength > this._maxPayload && this._maxPayload > 0) {
|
||||||
|
this._loop = false;
|
||||||
|
return error(
|
||||||
|
RangeError,
|
||||||
|
'Max payload size exceeded',
|
||||||
|
false,
|
||||||
|
1009,
|
||||||
|
'WS_ERR_UNSUPPORTED_MESSAGE_LENGTH'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._masked) this._state = GET_MASK;
|
||||||
|
else this._state = GET_DATA;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads mask bytes.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
getMask() {
|
||||||
|
if (this._bufferedBytes < 4) {
|
||||||
|
this._loop = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._mask = this.consume(4);
|
||||||
|
this._state = GET_DATA;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads data bytes.
|
||||||
|
*
|
||||||
|
* @param {Function} cb Callback
|
||||||
|
* @return {(Error|RangeError|undefined)} A possible error
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
getData(cb) {
|
||||||
|
let data = EMPTY_BUFFER;
|
||||||
|
|
||||||
|
if (this._payloadLength) {
|
||||||
|
if (this._bufferedBytes < this._payloadLength) {
|
||||||
|
this._loop = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
data = this.consume(this._payloadLength);
|
||||||
|
if (this._masked) unmask(data, this._mask);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._opcode > 0x07) return this.controlMessage(data);
|
||||||
|
|
||||||
|
if (this._compressed) {
|
||||||
|
this._state = INFLATING;
|
||||||
|
this.decompress(data, cb);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.length) {
|
||||||
|
//
|
||||||
|
// This message is not compressed so its lenght is the sum of the payload
|
||||||
|
// length of all fragments.
|
||||||
|
//
|
||||||
|
this._messageLength = this._totalPayloadLength;
|
||||||
|
this._fragments.push(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.dataMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decompresses data.
|
||||||
|
*
|
||||||
|
* @param {Buffer} data Compressed data
|
||||||
|
* @param {Function} cb Callback
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
decompress(data, cb) {
|
||||||
|
const perMessageDeflate = this._extensions[PerMessageDeflate.extensionName];
|
||||||
|
|
||||||
|
perMessageDeflate.decompress(data, this._fin, (err, buf) => {
|
||||||
|
if (err) return cb(err);
|
||||||
|
|
||||||
|
if (buf.length) {
|
||||||
|
this._messageLength += buf.length;
|
||||||
|
if (this._messageLength > this._maxPayload && this._maxPayload > 0) {
|
||||||
|
return cb(
|
||||||
|
error(
|
||||||
|
RangeError,
|
||||||
|
'Max payload size exceeded',
|
||||||
|
false,
|
||||||
|
1009,
|
||||||
|
'WS_ERR_UNSUPPORTED_MESSAGE_LENGTH'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._fragments.push(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
const er = this.dataMessage();
|
||||||
|
if (er) return cb(er);
|
||||||
|
|
||||||
|
this.startLoop(cb);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles a data message.
|
||||||
|
*
|
||||||
|
* @return {(Error|undefined)} A possible error
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
dataMessage() {
|
||||||
|
if (this._fin) {
|
||||||
|
const messageLength = this._messageLength;
|
||||||
|
const fragments = this._fragments;
|
||||||
|
|
||||||
|
this._totalPayloadLength = 0;
|
||||||
|
this._messageLength = 0;
|
||||||
|
this._fragmented = 0;
|
||||||
|
this._fragments = [];
|
||||||
|
|
||||||
|
if (this._opcode === 2) {
|
||||||
|
let data;
|
||||||
|
|
||||||
|
if (this._binaryType === 'nodebuffer') {
|
||||||
|
data = concat(fragments, messageLength);
|
||||||
|
} else if (this._binaryType === 'arraybuffer') {
|
||||||
|
data = toArrayBuffer(concat(fragments, messageLength));
|
||||||
|
} else {
|
||||||
|
data = fragments;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.emit('message', data);
|
||||||
|
} else {
|
||||||
|
const buf = concat(fragments, messageLength);
|
||||||
|
|
||||||
|
if (!isValidUTF8(buf)) {
|
||||||
|
this._loop = false;
|
||||||
|
return error(
|
||||||
|
Error,
|
||||||
|
'invalid UTF-8 sequence',
|
||||||
|
true,
|
||||||
|
1007,
|
||||||
|
'WS_ERR_INVALID_UTF8'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.emit('message', buf.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this._state = GET_INFO;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles a control message.
|
||||||
|
*
|
||||||
|
* @param {Buffer} data Data to handle
|
||||||
|
* @return {(Error|RangeError|undefined)} A possible error
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
controlMessage(data) {
|
||||||
|
if (this._opcode === 0x08) {
|
||||||
|
this._loop = false;
|
||||||
|
|
||||||
|
if (data.length === 0) {
|
||||||
|
this.emit('conclude', 1005, '');
|
||||||
|
this.end();
|
||||||
|
} else if (data.length === 1) {
|
||||||
|
return error(
|
||||||
|
RangeError,
|
||||||
|
'invalid payload length 1',
|
||||||
|
true,
|
||||||
|
1002,
|
||||||
|
'WS_ERR_INVALID_CONTROL_PAYLOAD_LENGTH'
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const code = data.readUInt16BE(0);
|
||||||
|
|
||||||
|
if (!isValidStatusCode(code)) {
|
||||||
|
return error(
|
||||||
|
RangeError,
|
||||||
|
`invalid status code ${code}`,
|
||||||
|
true,
|
||||||
|
1002,
|
||||||
|
'WS_ERR_INVALID_CLOSE_CODE'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const buf = data.slice(2);
|
||||||
|
|
||||||
|
if (!isValidUTF8(buf)) {
|
||||||
|
return error(
|
||||||
|
Error,
|
||||||
|
'invalid UTF-8 sequence',
|
||||||
|
true,
|
||||||
|
1007,
|
||||||
|
'WS_ERR_INVALID_UTF8'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.emit('conclude', code, buf.toString());
|
||||||
|
this.end();
|
||||||
|
}
|
||||||
|
} else if (this._opcode === 0x09) {
|
||||||
|
this.emit('ping', data);
|
||||||
|
} else {
|
||||||
|
this.emit('pong', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._state = GET_INFO;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Receiver;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds an error object.
|
||||||
|
*
|
||||||
|
* @param {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
|
||||||
|
*/
|
||||||
|
function error(ErrorCtor, message, prefix, statusCode, errorCode) {
|
||||||
|
const err = new ErrorCtor(
|
||||||
|
prefix ? `Invalid WebSocket frame: ${message}` : message
|
||||||
|
);
|
||||||
|
|
||||||
|
Error.captureStackTrace(err, error);
|
||||||
|
err.code = errorCode;
|
||||||
|
err[kStatusCode] = statusCode;
|
||||||
|
return err;
|
||||||
|
}
|
||||||
409
engine/ds-screencast/node_modules/chrome-remote-interface/node_modules/ws/lib/sender.js
generated
vendored
Normal file
409
engine/ds-screencast/node_modules/chrome-remote-interface/node_modules/ws/lib/sender.js
generated
vendored
Normal file
|
|
@ -0,0 +1,409 @@
|
||||||
|
/* eslint no-unused-vars: ["error", { "varsIgnorePattern": "^net|tls$" }] */
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const net = require('net');
|
||||||
|
const tls = require('tls');
|
||||||
|
const { randomFillSync } = require('crypto');
|
||||||
|
|
||||||
|
const PerMessageDeflate = require('./permessage-deflate');
|
||||||
|
const { EMPTY_BUFFER } = require('./constants');
|
||||||
|
const { isValidStatusCode } = require('./validation');
|
||||||
|
const { mask: applyMask, toBuffer } = require('./buffer-util');
|
||||||
|
|
||||||
|
const mask = Buffer.alloc(4);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HyBi Sender implementation.
|
||||||
|
*/
|
||||||
|
class Sender {
|
||||||
|
/**
|
||||||
|
* Creates a Sender instance.
|
||||||
|
*
|
||||||
|
* @param {(net.Socket|tls.Socket)} socket The connection socket
|
||||||
|
* @param {Object} [extensions] An object containing the negotiated extensions
|
||||||
|
*/
|
||||||
|
constructor(socket, extensions) {
|
||||||
|
this._extensions = extensions || {};
|
||||||
|
this._socket = socket;
|
||||||
|
|
||||||
|
this._firstFragment = true;
|
||||||
|
this._compress = false;
|
||||||
|
|
||||||
|
this._bufferedBytes = 0;
|
||||||
|
this._deflating = false;
|
||||||
|
this._queue = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Frames a piece of data according to the HyBi WebSocket protocol.
|
||||||
|
*
|
||||||
|
* @param {Buffer} data The data to frame
|
||||||
|
* @param {Object} options Options object
|
||||||
|
* @param {Number} options.opcode The opcode
|
||||||
|
* @param {Boolean} [options.readOnly=false] Specifies whether `data` can be
|
||||||
|
* modified
|
||||||
|
* @param {Boolean} [options.fin=false] Specifies whether or not to set the
|
||||||
|
* FIN bit
|
||||||
|
* @param {Boolean} [options.mask=false] Specifies whether or not to mask
|
||||||
|
* `data`
|
||||||
|
* @param {Boolean} [options.rsv1=false] Specifies whether or not to set the
|
||||||
|
* RSV1 bit
|
||||||
|
* @return {Buffer[]} The framed data as a list of `Buffer` instances
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
static frame(data, options) {
|
||||||
|
const merge = options.mask && options.readOnly;
|
||||||
|
let offset = options.mask ? 6 : 2;
|
||||||
|
let payloadLength = data.length;
|
||||||
|
|
||||||
|
if (data.length >= 65536) {
|
||||||
|
offset += 8;
|
||||||
|
payloadLength = 127;
|
||||||
|
} else if (data.length > 125) {
|
||||||
|
offset += 2;
|
||||||
|
payloadLength = 126;
|
||||||
|
}
|
||||||
|
|
||||||
|
const target = Buffer.allocUnsafe(merge ? data.length + offset : offset);
|
||||||
|
|
||||||
|
target[0] = options.fin ? options.opcode | 0x80 : options.opcode;
|
||||||
|
if (options.rsv1) target[0] |= 0x40;
|
||||||
|
|
||||||
|
target[1] = payloadLength;
|
||||||
|
|
||||||
|
if (payloadLength === 126) {
|
||||||
|
target.writeUInt16BE(data.length, 2);
|
||||||
|
} else if (payloadLength === 127) {
|
||||||
|
target.writeUInt32BE(0, 2);
|
||||||
|
target.writeUInt32BE(data.length, 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!options.mask) return [target, data];
|
||||||
|
|
||||||
|
randomFillSync(mask, 0, 4);
|
||||||
|
|
||||||
|
target[1] |= 0x80;
|
||||||
|
target[offset - 4] = mask[0];
|
||||||
|
target[offset - 3] = mask[1];
|
||||||
|
target[offset - 2] = mask[2];
|
||||||
|
target[offset - 1] = mask[3];
|
||||||
|
|
||||||
|
if (merge) {
|
||||||
|
applyMask(data, mask, target, offset, data.length);
|
||||||
|
return [target];
|
||||||
|
}
|
||||||
|
|
||||||
|
applyMask(data, mask, data, 0, data.length);
|
||||||
|
return [target, data];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a close message to the other peer.
|
||||||
|
*
|
||||||
|
* @param {Number} [code] The status code component of the body
|
||||||
|
* @param {String} [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 === '') {
|
||||||
|
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);
|
||||||
|
buf.write(data, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._deflating) {
|
||||||
|
this.enqueue([this.doClose, buf, mask, cb]);
|
||||||
|
} else {
|
||||||
|
this.doClose(buf, mask, cb);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Frames and sends a close message.
|
||||||
|
*
|
||||||
|
* @param {Buffer} data The message to send
|
||||||
|
* @param {Boolean} [mask=false] Specifies whether or not to mask `data`
|
||||||
|
* @param {Function} [cb] Callback
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
doClose(data, mask, cb) {
|
||||||
|
this.sendFrame(
|
||||||
|
Sender.frame(data, {
|
||||||
|
fin: true,
|
||||||
|
rsv1: false,
|
||||||
|
opcode: 0x08,
|
||||||
|
mask,
|
||||||
|
readOnly: false
|
||||||
|
}),
|
||||||
|
cb
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a ping message to the other peer.
|
||||||
|
*
|
||||||
|
* @param {*} data The message to send
|
||||||
|
* @param {Boolean} [mask=false] Specifies whether or not to mask `data`
|
||||||
|
* @param {Function} [cb] Callback
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
ping(data, mask, cb) {
|
||||||
|
const buf = toBuffer(data);
|
||||||
|
|
||||||
|
if (buf.length > 125) {
|
||||||
|
throw new RangeError('The data size must not be greater than 125 bytes');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._deflating) {
|
||||||
|
this.enqueue([this.doPing, buf, mask, toBuffer.readOnly, cb]);
|
||||||
|
} else {
|
||||||
|
this.doPing(buf, mask, toBuffer.readOnly, cb);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Frames and sends a ping message.
|
||||||
|
*
|
||||||
|
* @param {Buffer} data The message to send
|
||||||
|
* @param {Boolean} [mask=false] Specifies whether or not to mask `data`
|
||||||
|
* @param {Boolean} [readOnly=false] Specifies whether `data` can be modified
|
||||||
|
* @param {Function} [cb] Callback
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
doPing(data, mask, readOnly, cb) {
|
||||||
|
this.sendFrame(
|
||||||
|
Sender.frame(data, {
|
||||||
|
fin: true,
|
||||||
|
rsv1: false,
|
||||||
|
opcode: 0x09,
|
||||||
|
mask,
|
||||||
|
readOnly
|
||||||
|
}),
|
||||||
|
cb
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a pong message to the other peer.
|
||||||
|
*
|
||||||
|
* @param {*} data The message to send
|
||||||
|
* @param {Boolean} [mask=false] Specifies whether or not to mask `data`
|
||||||
|
* @param {Function} [cb] Callback
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
pong(data, mask, cb) {
|
||||||
|
const buf = toBuffer(data);
|
||||||
|
|
||||||
|
if (buf.length > 125) {
|
||||||
|
throw new RangeError('The data size must not be greater than 125 bytes');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._deflating) {
|
||||||
|
this.enqueue([this.doPong, buf, mask, toBuffer.readOnly, cb]);
|
||||||
|
} else {
|
||||||
|
this.doPong(buf, mask, toBuffer.readOnly, cb);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Frames and sends a pong message.
|
||||||
|
*
|
||||||
|
* @param {Buffer} data The message to send
|
||||||
|
* @param {Boolean} [mask=false] Specifies whether or not to mask `data`
|
||||||
|
* @param {Boolean} [readOnly=false] Specifies whether `data` can be modified
|
||||||
|
* @param {Function} [cb] Callback
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
doPong(data, mask, readOnly, cb) {
|
||||||
|
this.sendFrame(
|
||||||
|
Sender.frame(data, {
|
||||||
|
fin: true,
|
||||||
|
rsv1: false,
|
||||||
|
opcode: 0x0a,
|
||||||
|
mask,
|
||||||
|
readOnly
|
||||||
|
}),
|
||||||
|
cb
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a data message to the other peer.
|
||||||
|
*
|
||||||
|
* @param {*} data The message to send
|
||||||
|
* @param {Object} options Options object
|
||||||
|
* @param {Boolean} [options.compress=false] Specifies whether or not to
|
||||||
|
* compress `data`
|
||||||
|
* @param {Boolean} [options.binary=false] Specifies whether `data` is binary
|
||||||
|
* or text
|
||||||
|
* @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 buf = toBuffer(data);
|
||||||
|
const perMessageDeflate = this._extensions[PerMessageDeflate.extensionName];
|
||||||
|
let opcode = options.binary ? 2 : 1;
|
||||||
|
let rsv1 = options.compress;
|
||||||
|
|
||||||
|
if (this._firstFragment) {
|
||||||
|
this._firstFragment = false;
|
||||||
|
if (rsv1 && perMessageDeflate) {
|
||||||
|
rsv1 = buf.length >= perMessageDeflate._threshold;
|
||||||
|
}
|
||||||
|
this._compress = rsv1;
|
||||||
|
} else {
|
||||||
|
rsv1 = false;
|
||||||
|
opcode = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.fin) this._firstFragment = true;
|
||||||
|
|
||||||
|
if (perMessageDeflate) {
|
||||||
|
const opts = {
|
||||||
|
fin: options.fin,
|
||||||
|
rsv1,
|
||||||
|
opcode,
|
||||||
|
mask: options.mask,
|
||||||
|
readOnly: toBuffer.readOnly
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this._deflating) {
|
||||||
|
this.enqueue([this.dispatch, buf, this._compress, opts, cb]);
|
||||||
|
} else {
|
||||||
|
this.dispatch(buf, this._compress, opts, cb);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.sendFrame(
|
||||||
|
Sender.frame(buf, {
|
||||||
|
fin: options.fin,
|
||||||
|
rsv1: false,
|
||||||
|
opcode,
|
||||||
|
mask: options.mask,
|
||||||
|
readOnly: toBuffer.readOnly
|
||||||
|
}),
|
||||||
|
cb
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dispatches a data message.
|
||||||
|
*
|
||||||
|
* @param {Buffer} data The message to send
|
||||||
|
* @param {Boolean} [compress=false] Specifies whether or not to compress
|
||||||
|
* `data`
|
||||||
|
* @param {Object} options Options object
|
||||||
|
* @param {Number} options.opcode The opcode
|
||||||
|
* @param {Boolean} [options.readOnly=false] Specifies whether `data` can be
|
||||||
|
* modified
|
||||||
|
* @param {Boolean} [options.fin=false] Specifies whether or not to set the
|
||||||
|
* FIN bit
|
||||||
|
* @param {Boolean} [options.mask=false] Specifies whether or not to mask
|
||||||
|
* `data`
|
||||||
|
* @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 += data.length;
|
||||||
|
this._deflating = true;
|
||||||
|
perMessageDeflate.compress(data, options.fin, (_, buf) => {
|
||||||
|
if (this._socket.destroyed) {
|
||||||
|
const err = new Error(
|
||||||
|
'The socket was closed while data was being compressed'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (typeof cb === 'function') cb(err);
|
||||||
|
|
||||||
|
for (let i = 0; i < this._queue.length; i++) {
|
||||||
|
const callback = this._queue[i][4];
|
||||||
|
|
||||||
|
if (typeof callback === 'function') callback(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._bufferedBytes -= data.length;
|
||||||
|
this._deflating = false;
|
||||||
|
options.readOnly = false;
|
||||||
|
this.sendFrame(Sender.frame(buf, options), cb);
|
||||||
|
this.dequeue();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes queued send operations.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
dequeue() {
|
||||||
|
while (!this._deflating && this._queue.length) {
|
||||||
|
const params = this._queue.shift();
|
||||||
|
|
||||||
|
this._bufferedBytes -= params[1].length;
|
||||||
|
Reflect.apply(params[0], this, params.slice(1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enqueues a send operation.
|
||||||
|
*
|
||||||
|
* @param {Array} params Send operation parameters.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
enqueue(params) {
|
||||||
|
this._bufferedBytes += params[1].length;
|
||||||
|
this._queue.push(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a frame.
|
||||||
|
*
|
||||||
|
* @param {Buffer[]} list The frame to send
|
||||||
|
* @param {Function} [cb] Callback
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
sendFrame(list, cb) {
|
||||||
|
if (list.length === 2) {
|
||||||
|
this._socket.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;
|
||||||
180
engine/ds-screencast/node_modules/chrome-remote-interface/node_modules/ws/lib/stream.js
generated
vendored
Normal file
180
engine/ds-screencast/node_modules/chrome-remote-interface/node_modules/ws/lib/stream.js
generated
vendored
Normal file
|
|
@ -0,0 +1,180 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
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 resumeOnReceiverDrain = true;
|
||||||
|
let terminateOnDestroy = true;
|
||||||
|
|
||||||
|
function receiverOnDrain() {
|
||||||
|
if (resumeOnReceiverDrain) ws._socket.resume();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ws.readyState === ws.CONNECTING) {
|
||||||
|
ws.once('open', function open() {
|
||||||
|
ws._receiver.removeAllListeners('drain');
|
||||||
|
ws._receiver.on('drain', receiverOnDrain);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
ws._receiver.removeAllListeners('drain');
|
||||||
|
ws._receiver.on('drain', receiverOnDrain);
|
||||||
|
}
|
||||||
|
|
||||||
|
const duplex = new Duplex({
|
||||||
|
...options,
|
||||||
|
autoDestroy: false,
|
||||||
|
emitClose: false,
|
||||||
|
objectMode: false,
|
||||||
|
writableObjectMode: false
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.on('message', function message(msg) {
|
||||||
|
if (!duplex.push(msg)) {
|
||||||
|
resumeOnReceiverDrain = false;
|
||||||
|
ws._socket.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.readyState === ws.OPEN || ws.readyState === ws.CLOSING) &&
|
||||||
|
!resumeOnReceiverDrain
|
||||||
|
) {
|
||||||
|
resumeOnReceiverDrain = true;
|
||||||
|
if (!ws._receiver._writableState.needDrain) ws._socket.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;
|
||||||
104
engine/ds-screencast/node_modules/chrome-remote-interface/node_modules/ws/lib/validation.js
generated
vendored
Normal file
104
engine/ds-screencast/node_modules/chrome-remote-interface/node_modules/ws/lib/validation.js
generated
vendored
Normal file
|
|
@ -0,0 +1,104 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
let isValidUTF8 = require('utf-8-validate');
|
||||||
|
|
||||||
|
/* istanbul ignore if */
|
||||||
|
if (typeof isValidUTF8 === 'object') {
|
||||||
|
isValidUTF8 = isValidUTF8.Validation.isValidUTF8; // utf-8-validate@<3.0.0
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
isValidStatusCode,
|
||||||
|
isValidUTF8(buf) {
|
||||||
|
return buf.length < 150 ? _isValidUTF8(buf) : isValidUTF8(buf);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} catch (e) /* istanbul ignore next */ {
|
||||||
|
module.exports = {
|
||||||
|
isValidStatusCode,
|
||||||
|
isValidUTF8: _isValidUTF8
|
||||||
|
};
|
||||||
|
}
|
||||||
449
engine/ds-screencast/node_modules/chrome-remote-interface/node_modules/ws/lib/websocket-server.js
generated
vendored
Normal file
449
engine/ds-screencast/node_modules/chrome-remote-interface/node_modules/ws/lib/websocket-server.js
generated
vendored
Normal file
|
|
@ -0,0 +1,449 @@
|
||||||
|
/* eslint no-unused-vars: ["error", { "varsIgnorePattern": "^net|tls|https$" }] */
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const EventEmitter = require('events');
|
||||||
|
const http = require('http');
|
||||||
|
const https = require('https');
|
||||||
|
const net = require('net');
|
||||||
|
const tls = require('tls');
|
||||||
|
const { createHash } = require('crypto');
|
||||||
|
|
||||||
|
const PerMessageDeflate = require('./permessage-deflate');
|
||||||
|
const WebSocket = require('./websocket');
|
||||||
|
const { format, parse } = require('./extension');
|
||||||
|
const { 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 {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 {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 {Function} [options.verifyClient] A hook to reject connections
|
||||||
|
* @param {Function} [callback] A listener for the `listening` event
|
||||||
|
*/
|
||||||
|
constructor(options, callback) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
options = {
|
||||||
|
maxPayload: 100 * 1024 * 1024,
|
||||||
|
perMessageDeflate: false,
|
||||||
|
handleProtocols: null,
|
||||||
|
clientTracking: true,
|
||||||
|
verifyClient: null,
|
||||||
|
noServer: false,
|
||||||
|
backlog: null, // use default (511 as implemented in net.js)
|
||||||
|
server: null,
|
||||||
|
host: null,
|
||||||
|
path: null,
|
||||||
|
port: null,
|
||||||
|
...options
|
||||||
|
};
|
||||||
|
|
||||||
|
if (
|
||||||
|
(options.port == null && !options.server && !options.noServer) ||
|
||||||
|
(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.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();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close the server.
|
||||||
|
*
|
||||||
|
* @param {Function} [cb] Callback
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
close(cb) {
|
||||||
|
if (cb) this.once('close', cb);
|
||||||
|
|
||||||
|
if (this._state === CLOSED) {
|
||||||
|
process.nextTick(emitClose, this);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._state === CLOSING) return;
|
||||||
|
this._state = CLOSING;
|
||||||
|
|
||||||
|
//
|
||||||
|
// Terminate all associated clients.
|
||||||
|
//
|
||||||
|
if (this.clients) {
|
||||||
|
for (const client of this.clients) client.terminate();
|
||||||
|
}
|
||||||
|
|
||||||
|
const server = this._server;
|
||||||
|
|
||||||
|
if (server) {
|
||||||
|
this._removeListeners();
|
||||||
|
this._removeListeners = this._server = null;
|
||||||
|
|
||||||
|
//
|
||||||
|
// Close the http server if it was internally created.
|
||||||
|
//
|
||||||
|
if (this.options.port != null) {
|
||||||
|
server.close(emitClose.bind(undefined, this));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
process.nextTick(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 {(net.Socket|tls.Socket)} socket The network socket between the
|
||||||
|
* server and client
|
||||||
|
* @param {Buffer} head The first packet of the upgraded stream
|
||||||
|
* @param {Function} cb Callback
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
handleUpgrade(req, socket, head, cb) {
|
||||||
|
socket.on('error', socketOnError);
|
||||||
|
|
||||||
|
const key =
|
||||||
|
req.headers['sec-websocket-key'] !== undefined
|
||||||
|
? req.headers['sec-websocket-key'].trim()
|
||||||
|
: false;
|
||||||
|
const upgrade = req.headers.upgrade;
|
||||||
|
const version = +req.headers['sec-websocket-version'];
|
||||||
|
const extensions = {};
|
||||||
|
|
||||||
|
if (
|
||||||
|
req.method !== 'GET' ||
|
||||||
|
upgrade === undefined ||
|
||||||
|
upgrade.toLowerCase() !== 'websocket' ||
|
||||||
|
!key ||
|
||||||
|
!keyRegex.test(key) ||
|
||||||
|
(version !== 8 && version !== 13) ||
|
||||||
|
!this.shouldHandle(req)
|
||||||
|
) {
|
||||||
|
return abortHandshake(socket, 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.options.perMessageDeflate) {
|
||||||
|
const perMessageDeflate = new PerMessageDeflate(
|
||||||
|
this.options.perMessageDeflate,
|
||||||
|
true,
|
||||||
|
this.options.maxPayload
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const offers = parse(req.headers['sec-websocket-extensions']);
|
||||||
|
|
||||||
|
if (offers[PerMessageDeflate.extensionName]) {
|
||||||
|
perMessageDeflate.accept(offers[PerMessageDeflate.extensionName]);
|
||||||
|
extensions[PerMessageDeflate.extensionName] = perMessageDeflate;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
return abortHandshake(socket, 400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Optionally call external client verification handler.
|
||||||
|
//
|
||||||
|
if (this.options.verifyClient) {
|
||||||
|
const info = {
|
||||||
|
origin:
|
||||||
|
req.headers[`${version === 8 ? 'sec-websocket-origin' : 'origin'}`],
|
||||||
|
secure: !!(req.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(key, extensions, req, socket, head, cb);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.options.verifyClient(info)) return abortHandshake(socket, 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.completeUpgrade(key, extensions, req, socket, head, cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Upgrade the connection to WebSocket.
|
||||||
|
*
|
||||||
|
* @param {String} key The value of the `Sec-WebSocket-Key` header
|
||||||
|
* @param {Object} extensions The accepted extensions
|
||||||
|
* @param {http.IncomingMessage} req The request object
|
||||||
|
* @param {(net.Socket|tls.Socket)} 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(key, extensions, req, socket, head, cb) {
|
||||||
|
//
|
||||||
|
// Destroy the socket if the client has already sent a FIN packet.
|
||||||
|
//
|
||||||
|
if (!socket.readable || !socket.writable) return socket.destroy();
|
||||||
|
|
||||||
|
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 WebSocket(null);
|
||||||
|
let protocol = req.headers['sec-websocket-protocol'];
|
||||||
|
|
||||||
|
if (protocol) {
|
||||||
|
protocol = protocol.split(',').map(trim);
|
||||||
|
|
||||||
|
//
|
||||||
|
// Optionally call external protocol selection handler.
|
||||||
|
//
|
||||||
|
if (this.options.handleProtocols) {
|
||||||
|
protocol = this.options.handleProtocols(protocol, req);
|
||||||
|
} else {
|
||||||
|
protocol = protocol[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (protocol) {
|
||||||
|
headers.push(`Sec-WebSocket-Protocol: ${protocol}`);
|
||||||
|
ws._protocol = protocol;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (extensions[PerMessageDeflate.extensionName]) {
|
||||||
|
const params = extensions[PerMessageDeflate.extensionName].params;
|
||||||
|
const value = format({
|
||||||
|
[PerMessageDeflate.extensionName]: [params]
|
||||||
|
});
|
||||||
|
headers.push(`Sec-WebSocket-Extensions: ${value}`);
|
||||||
|
ws._extensions = extensions;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Allow external modification/inspection of handshake headers.
|
||||||
|
//
|
||||||
|
this.emit('headers', headers, req);
|
||||||
|
|
||||||
|
socket.write(headers.concat('\r\n').join('\r\n'));
|
||||||
|
socket.removeListener('error', socketOnError);
|
||||||
|
|
||||||
|
ws.setSocket(socket, head, this.options.maxPayload);
|
||||||
|
|
||||||
|
if (this.clients) {
|
||||||
|
this.clients.add(ws);
|
||||||
|
ws.on('close', () => this.clients.delete(ws));
|
||||||
|
}
|
||||||
|
|
||||||
|
cb(ws, 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 premature socket errors.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
function socketOnError() {
|
||||||
|
this.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close the connection when preconditions are not fulfilled.
|
||||||
|
*
|
||||||
|
* @param {(net.Socket|tls.Socket)} socket The socket of the upgrade request
|
||||||
|
* @param {Number} code The HTTP response status code
|
||||||
|
* @param {String} [message] The HTTP response body
|
||||||
|
* @param {Object} [headers] Additional HTTP response headers
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
function abortHandshake(socket, code, message, headers) {
|
||||||
|
if (socket.writable) {
|
||||||
|
message = message || http.STATUS_CODES[code];
|
||||||
|
headers = {
|
||||||
|
Connection: 'close',
|
||||||
|
'Content-Type': 'text/html',
|
||||||
|
'Content-Length': Buffer.byteLength(message),
|
||||||
|
...headers
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.write(
|
||||||
|
`HTTP/1.1 ${code} ${http.STATUS_CODES[code]}\r\n` +
|
||||||
|
Object.keys(headers)
|
||||||
|
.map((h) => `${h}: ${headers[h]}`)
|
||||||
|
.join('\r\n') +
|
||||||
|
'\r\n\r\n' +
|
||||||
|
message
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
socket.removeListener('error', socketOnError);
|
||||||
|
socket.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove whitespace characters from both ends of a string.
|
||||||
|
*
|
||||||
|
* @param {String} str The string
|
||||||
|
* @return {String} A new string representing `str` stripped of whitespace
|
||||||
|
* characters from both its beginning and end
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
function trim(str) {
|
||||||
|
return str.trim();
|
||||||
|
}
|
||||||
1197
engine/ds-screencast/node_modules/chrome-remote-interface/node_modules/ws/lib/websocket.js
generated
vendored
Normal file
1197
engine/ds-screencast/node_modules/chrome-remote-interface/node_modules/ws/lib/websocket.js
generated
vendored
Normal file
File diff suppressed because it is too large
Load diff
56
engine/ds-screencast/node_modules/chrome-remote-interface/node_modules/ws/package.json
generated
vendored
Normal file
56
engine/ds-screencast/node_modules/chrome-remote-interface/node_modules/ws/package.json
generated
vendored
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
{
|
||||||
|
"name": "ws",
|
||||||
|
"version": "7.5.10",
|
||||||
|
"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": "websockets/ws",
|
||||||
|
"author": "Einar Otto Stangvik <einaros@gmail.com> (http://2x.io)",
|
||||||
|
"license": "MIT",
|
||||||
|
"main": "index.js",
|
||||||
|
"browser": "browser.js",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.3.0"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"browser.js",
|
||||||
|
"index.js",
|
||||||
|
"lib/*.js"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"test": "nyc --reporter=lcov --reporter=text mocha --throw-deprecation test/*.test.js",
|
||||||
|
"integration": "mocha --throw-deprecation test/*.integration.js",
|
||||||
|
"lint": "eslint --ignore-path .gitignore . && 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": {
|
||||||
|
"benchmark": "^2.1.4",
|
||||||
|
"bufferutil": "^4.0.1",
|
||||||
|
"eslint": "^7.2.0",
|
||||||
|
"eslint-config-prettier": "^8.1.0",
|
||||||
|
"eslint-plugin-prettier": "^4.0.0",
|
||||||
|
"mocha": "^7.0.0",
|
||||||
|
"nyc": "^15.0.0",
|
||||||
|
"prettier": "^2.0.5",
|
||||||
|
"utf-8-validate": "^5.0.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
64
engine/ds-screencast/node_modules/chrome-remote-interface/package.json
generated
vendored
Normal file
64
engine/ds-screencast/node_modules/chrome-remote-interface/package.json
generated
vendored
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
{
|
||||||
|
"name": "chrome-remote-interface",
|
||||||
|
"author": "Andrea Cardaci <cyrus.and@gmail.com>",
|
||||||
|
"license": "MIT",
|
||||||
|
"contributors": [
|
||||||
|
"Andrey Sidorov <sidoares@yandex.ru>",
|
||||||
|
"Greg Cochard <greg@gregcochard.com>"
|
||||||
|
],
|
||||||
|
"description": "Chrome Debugging Protocol interface",
|
||||||
|
"keywords": [
|
||||||
|
"chrome",
|
||||||
|
"debug",
|
||||||
|
"protocol",
|
||||||
|
"remote",
|
||||||
|
"interface"
|
||||||
|
],
|
||||||
|
"homepage": "https://github.com/cyrus-and/chrome-remote-interface",
|
||||||
|
"version": "0.34.0",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git://github.com/cyrus-and/chrome-remote-interface.git"
|
||||||
|
},
|
||||||
|
"bugs": {
|
||||||
|
"url": "http://github.com/cyrus-and/chrome-remote-interface/issues"
|
||||||
|
},
|
||||||
|
"engine-strict": {
|
||||||
|
"node": ">=8"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"commander": "2.11.x",
|
||||||
|
"ws": "^7.2.0"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"lib",
|
||||||
|
"bin",
|
||||||
|
"index.js",
|
||||||
|
"chrome-remote-interface.js",
|
||||||
|
"webpack.config.js"
|
||||||
|
],
|
||||||
|
"bin": {
|
||||||
|
"chrome-remote-interface": "bin/client.js"
|
||||||
|
},
|
||||||
|
"main": "index.js",
|
||||||
|
"browser": "chrome-remote-interface.js",
|
||||||
|
"devDependencies": {
|
||||||
|
"babel-core": "^6.26.3",
|
||||||
|
"babel-loader": "8.x.x",
|
||||||
|
"babel-polyfill": "^6.26.0",
|
||||||
|
"babel-preset-env": "^0.0.0",
|
||||||
|
"eslint": "^8.8.0",
|
||||||
|
"json-loader": "^0.5.4",
|
||||||
|
"mocha": "^11.1.0",
|
||||||
|
"process": "^0.11.10",
|
||||||
|
"url": "^0.11.0",
|
||||||
|
"util": "^0.12.4",
|
||||||
|
"webpack": "^5.39.0",
|
||||||
|
"webpack-cli": "^4.7.2"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"test": "./scripts/run-tests.sh",
|
||||||
|
"webpack": "webpack",
|
||||||
|
"prepare": "webpack"
|
||||||
|
}
|
||||||
|
}
|
||||||
48
engine/ds-screencast/node_modules/chrome-remote-interface/webpack.config.js
generated
vendored
Normal file
48
engine/ds-screencast/node_modules/chrome-remote-interface/webpack.config.js
generated
vendored
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const TerserPlugin = require('terser-webpack-plugin');
|
||||||
|
const webpack = require('webpack');
|
||||||
|
|
||||||
|
function criWrapper(_, options, callback) {
|
||||||
|
window.criRequest(options, callback); // eslint-disable-line no-undef
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
mode: 'production',
|
||||||
|
resolve: {
|
||||||
|
fallback: {
|
||||||
|
'util': require.resolve('util/'),
|
||||||
|
'url': require.resolve('url/'),
|
||||||
|
'http': false,
|
||||||
|
'https': false,
|
||||||
|
'dns': false
|
||||||
|
},
|
||||||
|
alias: {
|
||||||
|
'ws': './websocket-wrapper.js'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
externals: [
|
||||||
|
{
|
||||||
|
'./external-request.js': `var (${criWrapper})`
|
||||||
|
}
|
||||||
|
],
|
||||||
|
plugins: [
|
||||||
|
new webpack.ProvidePlugin({
|
||||||
|
process: 'process/browser',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
optimization: {
|
||||||
|
minimizer: [
|
||||||
|
new TerserPlugin({
|
||||||
|
extractComments: false,
|
||||||
|
})
|
||||||
|
],
|
||||||
|
},
|
||||||
|
entry: ['babel-polyfill', './index.js'],
|
||||||
|
output: {
|
||||||
|
path: __dirname,
|
||||||
|
filename: 'chrome-remote-interface.js',
|
||||||
|
libraryTarget: process.env.TARGET || 'commonjs2',
|
||||||
|
library: 'CDP'
|
||||||
|
}
|
||||||
|
};
|
||||||
298
engine/ds-screencast/node_modules/commander/History.md
generated
vendored
Normal file
298
engine/ds-screencast/node_modules/commander/History.md
generated
vendored
Normal file
|
|
@ -0,0 +1,298 @@
|
||||||
|
|
||||||
|
2.11.0 / 2017-07-03
|
||||||
|
==================
|
||||||
|
|
||||||
|
* Fix help section order and padding (#652)
|
||||||
|
* feature: support for signals to subcommands (#632)
|
||||||
|
* Fixed #37, --help should not display first (#447)
|
||||||
|
* Fix translation errors. (#570)
|
||||||
|
* Add package-lock.json
|
||||||
|
* Remove engines
|
||||||
|
* Upgrade package version
|
||||||
|
* Prefix events to prevent conflicts between commands and options (#494)
|
||||||
|
* Removing dependency on graceful-readlink
|
||||||
|
* Support setting name in #name function and make it chainable
|
||||||
|
* Add .vscode directory to .gitignore (Visual Studio Code metadata)
|
||||||
|
* Updated link to ruby commander in readme files
|
||||||
|
|
||||||
|
2.10.0 / 2017-06-19
|
||||||
|
==================
|
||||||
|
|
||||||
|
* Update .travis.yml. drop support for older node.js versions.
|
||||||
|
* Fix require arguments in README.md
|
||||||
|
* On SemVer you do not start from 0.0.1
|
||||||
|
* Add missing semi colon in readme
|
||||||
|
* Add save param to npm install
|
||||||
|
* node v6 travis test
|
||||||
|
* Update Readme_zh-CN.md
|
||||||
|
* Allow literal '--' to be passed-through as an argument
|
||||||
|
* Test subcommand alias help
|
||||||
|
* link build badge to master branch
|
||||||
|
* Support the alias of Git style sub-command
|
||||||
|
* added keyword commander for better search result on npm
|
||||||
|
* Fix Sub-Subcommands
|
||||||
|
* test node.js stable
|
||||||
|
* Fixes TypeError when a command has an option called `--description`
|
||||||
|
* Update README.md to make it beginner friendly and elaborate on the difference between angled and square brackets.
|
||||||
|
* Add chinese Readme file
|
||||||
|
|
||||||
|
2.9.0 / 2015-10-13
|
||||||
|
==================
|
||||||
|
|
||||||
|
* Add option `isDefault` to set default subcommand #415 @Qix-
|
||||||
|
* Add callback to allow filtering or post-processing of help text #434 @djulien
|
||||||
|
* Fix `undefined` text in help information close #414 #416 @zhiyelee
|
||||||
|
|
||||||
|
2.8.1 / 2015-04-22
|
||||||
|
==================
|
||||||
|
|
||||||
|
* Back out `support multiline description` Close #396 #397
|
||||||
|
|
||||||
|
2.8.0 / 2015-04-07
|
||||||
|
==================
|
||||||
|
|
||||||
|
* Add `process.execArg` support, execution args like `--harmony` will be passed to sub-commands #387 @DigitalIO @zhiyelee
|
||||||
|
* Fix bug in Git-style sub-commands #372 @zhiyelee
|
||||||
|
* Allow commands to be hidden from help #383 @tonylukasavage
|
||||||
|
* When git-style sub-commands are in use, yet none are called, display help #382 @claylo
|
||||||
|
* Add ability to specify arguments syntax for top-level command #258 @rrthomas
|
||||||
|
* Support multiline descriptions #208 @zxqfox
|
||||||
|
|
||||||
|
2.7.1 / 2015-03-11
|
||||||
|
==================
|
||||||
|
|
||||||
|
* Revert #347 (fix collisions when option and first arg have same name) which causes a bug in #367.
|
||||||
|
|
||||||
|
2.7.0 / 2015-03-09
|
||||||
|
==================
|
||||||
|
|
||||||
|
* Fix git-style bug when installed globally. Close #335 #349 @zhiyelee
|
||||||
|
* Fix collisions when option and first arg have same name. Close #346 #347 @tonylukasavage
|
||||||
|
* Add support for camelCase on `opts()`. Close #353 @nkzawa
|
||||||
|
* Add node.js 0.12 and io.js to travis.yml
|
||||||
|
* Allow RegEx options. #337 @palanik
|
||||||
|
* Fixes exit code when sub-command failing. Close #260 #332 @pirelenito
|
||||||
|
* git-style `bin` files in $PATH make sense. Close #196 #327 @zhiyelee
|
||||||
|
|
||||||
|
2.6.0 / 2014-12-30
|
||||||
|
==================
|
||||||
|
|
||||||
|
* added `Command#allowUnknownOption` method. Close #138 #318 @doozr @zhiyelee
|
||||||
|
* Add application description to the help msg. Close #112 @dalssoft
|
||||||
|
|
||||||
|
2.5.1 / 2014-12-15
|
||||||
|
==================
|
||||||
|
|
||||||
|
* fixed two bugs incurred by variadic arguments. Close #291 @Quentin01 #302 @zhiyelee
|
||||||
|
|
||||||
|
2.5.0 / 2014-10-24
|
||||||
|
==================
|
||||||
|
|
||||||
|
* add support for variadic arguments. Closes #277 @whitlockjc
|
||||||
|
|
||||||
|
2.4.0 / 2014-10-17
|
||||||
|
==================
|
||||||
|
|
||||||
|
* fixed a bug on executing the coercion function of subcommands option. Closes #270
|
||||||
|
* added `Command.prototype.name` to retrieve command name. Closes #264 #266 @tonylukasavage
|
||||||
|
* added `Command.prototype.opts` to retrieve all the options as a simple object of key-value pairs. Closes #262 @tonylukasavage
|
||||||
|
* fixed a bug on subcommand name. Closes #248 @jonathandelgado
|
||||||
|
* fixed function normalize doesn’t honor option terminator. Closes #216 @abbr
|
||||||
|
|
||||||
|
2.3.0 / 2014-07-16
|
||||||
|
==================
|
||||||
|
|
||||||
|
* add command alias'. Closes PR #210
|
||||||
|
* fix: Typos. Closes #99
|
||||||
|
* fix: Unused fs module. Closes #217
|
||||||
|
|
||||||
|
2.2.0 / 2014-03-29
|
||||||
|
==================
|
||||||
|
|
||||||
|
* add passing of previous option value
|
||||||
|
* fix: support subcommands on windows. Closes #142
|
||||||
|
* Now the defaultValue passed as the second argument of the coercion function.
|
||||||
|
|
||||||
|
2.1.0 / 2013-11-21
|
||||||
|
==================
|
||||||
|
|
||||||
|
* add: allow cflag style option params, unit test, fixes #174
|
||||||
|
|
||||||
|
2.0.0 / 2013-07-18
|
||||||
|
==================
|
||||||
|
|
||||||
|
* remove input methods (.prompt, .confirm, etc)
|
||||||
|
|
||||||
|
1.3.2 / 2013-07-18
|
||||||
|
==================
|
||||||
|
|
||||||
|
* add support for sub-commands to co-exist with the original command
|
||||||
|
|
||||||
|
1.3.1 / 2013-07-18
|
||||||
|
==================
|
||||||
|
|
||||||
|
* add quick .runningCommand hack so you can opt-out of other logic when running a sub command
|
||||||
|
|
||||||
|
1.3.0 / 2013-07-09
|
||||||
|
==================
|
||||||
|
|
||||||
|
* add EACCES error handling
|
||||||
|
* fix sub-command --help
|
||||||
|
|
||||||
|
1.2.0 / 2013-06-13
|
||||||
|
==================
|
||||||
|
|
||||||
|
* allow "-" hyphen as an option argument
|
||||||
|
* support for RegExp coercion
|
||||||
|
|
||||||
|
1.1.1 / 2012-11-20
|
||||||
|
==================
|
||||||
|
|
||||||
|
* add more sub-command padding
|
||||||
|
* fix .usage() when args are present. Closes #106
|
||||||
|
|
||||||
|
1.1.0 / 2012-11-16
|
||||||
|
==================
|
||||||
|
|
||||||
|
* add git-style executable subcommand support. Closes #94
|
||||||
|
|
||||||
|
1.0.5 / 2012-10-09
|
||||||
|
==================
|
||||||
|
|
||||||
|
* fix `--name` clobbering. Closes #92
|
||||||
|
* fix examples/help. Closes #89
|
||||||
|
|
||||||
|
1.0.4 / 2012-09-03
|
||||||
|
==================
|
||||||
|
|
||||||
|
* add `outputHelp()` method.
|
||||||
|
|
||||||
|
1.0.3 / 2012-08-30
|
||||||
|
==================
|
||||||
|
|
||||||
|
* remove invalid .version() defaulting
|
||||||
|
|
||||||
|
1.0.2 / 2012-08-24
|
||||||
|
==================
|
||||||
|
|
||||||
|
* add `--foo=bar` support [arv]
|
||||||
|
* fix password on node 0.8.8. Make backward compatible with 0.6 [focusaurus]
|
||||||
|
|
||||||
|
1.0.1 / 2012-08-03
|
||||||
|
==================
|
||||||
|
|
||||||
|
* fix issue #56
|
||||||
|
* fix tty.setRawMode(mode) was moved to tty.ReadStream#setRawMode() (i.e. process.stdin.setRawMode())
|
||||||
|
|
||||||
|
1.0.0 / 2012-07-05
|
||||||
|
==================
|
||||||
|
|
||||||
|
* add support for optional option descriptions
|
||||||
|
* add defaulting of `.version()` to package.json's version
|
||||||
|
|
||||||
|
0.6.1 / 2012-06-01
|
||||||
|
==================
|
||||||
|
|
||||||
|
* Added: append (yes or no) on confirmation
|
||||||
|
* Added: allow node.js v0.7.x
|
||||||
|
|
||||||
|
0.6.0 / 2012-04-10
|
||||||
|
==================
|
||||||
|
|
||||||
|
* Added `.prompt(obj, callback)` support. Closes #49
|
||||||
|
* Added default support to .choose(). Closes #41
|
||||||
|
* Fixed the choice example
|
||||||
|
|
||||||
|
0.5.1 / 2011-12-20
|
||||||
|
==================
|
||||||
|
|
||||||
|
* Fixed `password()` for recent nodes. Closes #36
|
||||||
|
|
||||||
|
0.5.0 / 2011-12-04
|
||||||
|
==================
|
||||||
|
|
||||||
|
* Added sub-command option support [itay]
|
||||||
|
|
||||||
|
0.4.3 / 2011-12-04
|
||||||
|
==================
|
||||||
|
|
||||||
|
* Fixed custom help ordering. Closes #32
|
||||||
|
|
||||||
|
0.4.2 / 2011-11-24
|
||||||
|
==================
|
||||||
|
|
||||||
|
* Added travis support
|
||||||
|
* Fixed: line-buffered input automatically trimmed. Closes #31
|
||||||
|
|
||||||
|
0.4.1 / 2011-11-18
|
||||||
|
==================
|
||||||
|
|
||||||
|
* Removed listening for "close" on --help
|
||||||
|
|
||||||
|
0.4.0 / 2011-11-15
|
||||||
|
==================
|
||||||
|
|
||||||
|
* Added support for `--`. Closes #24
|
||||||
|
|
||||||
|
0.3.3 / 2011-11-14
|
||||||
|
==================
|
||||||
|
|
||||||
|
* Fixed: wait for close event when writing help info [Jerry Hamlet]
|
||||||
|
|
||||||
|
0.3.2 / 2011-11-01
|
||||||
|
==================
|
||||||
|
|
||||||
|
* Fixed long flag definitions with values [felixge]
|
||||||
|
|
||||||
|
0.3.1 / 2011-10-31
|
||||||
|
==================
|
||||||
|
|
||||||
|
* Changed `--version` short flag to `-V` from `-v`
|
||||||
|
* Changed `.version()` so it's configurable [felixge]
|
||||||
|
|
||||||
|
0.3.0 / 2011-10-31
|
||||||
|
==================
|
||||||
|
|
||||||
|
* Added support for long flags only. Closes #18
|
||||||
|
|
||||||
|
0.2.1 / 2011-10-24
|
||||||
|
==================
|
||||||
|
|
||||||
|
* "node": ">= 0.4.x < 0.7.0". Closes #20
|
||||||
|
|
||||||
|
0.2.0 / 2011-09-26
|
||||||
|
==================
|
||||||
|
|
||||||
|
* Allow for defaults that are not just boolean. Default peassignment only occurs for --no-*, optional, and required arguments. [Jim Isaacs]
|
||||||
|
|
||||||
|
0.1.0 / 2011-08-24
|
||||||
|
==================
|
||||||
|
|
||||||
|
* Added support for custom `--help` output
|
||||||
|
|
||||||
|
0.0.5 / 2011-08-18
|
||||||
|
==================
|
||||||
|
|
||||||
|
* Changed: when the user enters nothing prompt for password again
|
||||||
|
* Fixed issue with passwords beginning with numbers [NuckChorris]
|
||||||
|
|
||||||
|
0.0.4 / 2011-08-15
|
||||||
|
==================
|
||||||
|
|
||||||
|
* Fixed `Commander#args`
|
||||||
|
|
||||||
|
0.0.3 / 2011-08-15
|
||||||
|
==================
|
||||||
|
|
||||||
|
* Added default option value support
|
||||||
|
|
||||||
|
0.0.2 / 2011-08-15
|
||||||
|
==================
|
||||||
|
|
||||||
|
* Added mask support to `Command#password(str[, mask], fn)`
|
||||||
|
* Added `Command#password(str, fn)`
|
||||||
|
|
||||||
|
0.0.1 / 2010-01-03
|
||||||
|
==================
|
||||||
|
|
||||||
|
* Initial release
|
||||||
22
engine/ds-screencast/node_modules/commander/LICENSE
generated
vendored
Normal file
22
engine/ds-screencast/node_modules/commander/LICENSE
generated
vendored
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
(The MIT License)
|
||||||
|
|
||||||
|
Copyright (c) 2011 TJ Holowaychuk <tj@vision-media.ca>
|
||||||
|
|
||||||
|
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.
|
||||||
351
engine/ds-screencast/node_modules/commander/Readme.md
generated
vendored
Normal file
351
engine/ds-screencast/node_modules/commander/Readme.md
generated
vendored
Normal file
|
|
@ -0,0 +1,351 @@
|
||||||
|
# Commander.js
|
||||||
|
|
||||||
|
|
||||||
|
[](http://travis-ci.org/tj/commander.js)
|
||||||
|
[](https://www.npmjs.org/package/commander)
|
||||||
|
[](https://www.npmjs.org/package/commander)
|
||||||
|
[](https://gitter.im/tj/commander.js?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||||
|
|
||||||
|
The complete solution for [node.js](http://nodejs.org) command-line interfaces, inspired by Ruby's [commander](https://github.com/commander-rb/commander).
|
||||||
|
[API documentation](http://tj.github.com/commander.js/)
|
||||||
|
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
$ npm install commander --save
|
||||||
|
|
||||||
|
## Option parsing
|
||||||
|
|
||||||
|
Options with commander are defined with the `.option()` method, also serving as documentation for the options. The example below parses args and options from `process.argv`, leaving remaining args as the `program.args` array which were not consumed by options.
|
||||||
|
|
||||||
|
```js
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Module dependencies.
|
||||||
|
*/
|
||||||
|
|
||||||
|
var program = require('commander');
|
||||||
|
|
||||||
|
program
|
||||||
|
.version('0.1.0')
|
||||||
|
.option('-p, --peppers', 'Add peppers')
|
||||||
|
.option('-P, --pineapple', 'Add pineapple')
|
||||||
|
.option('-b, --bbq-sauce', 'Add bbq sauce')
|
||||||
|
.option('-c, --cheese [type]', 'Add the specified type of cheese [marble]', 'marble')
|
||||||
|
.parse(process.argv);
|
||||||
|
|
||||||
|
console.log('you ordered a pizza with:');
|
||||||
|
if (program.peppers) console.log(' - peppers');
|
||||||
|
if (program.pineapple) console.log(' - pineapple');
|
||||||
|
if (program.bbqSauce) console.log(' - bbq');
|
||||||
|
console.log(' - %s cheese', program.cheese);
|
||||||
|
```
|
||||||
|
|
||||||
|
Short flags may be passed as a single arg, for example `-abc` is equivalent to `-a -b -c`. Multi-word options such as "--template-engine" are camel-cased, becoming `program.templateEngine` etc.
|
||||||
|
|
||||||
|
|
||||||
|
## Coercion
|
||||||
|
|
||||||
|
```js
|
||||||
|
function range(val) {
|
||||||
|
return val.split('..').map(Number);
|
||||||
|
}
|
||||||
|
|
||||||
|
function list(val) {
|
||||||
|
return val.split(',');
|
||||||
|
}
|
||||||
|
|
||||||
|
function collect(val, memo) {
|
||||||
|
memo.push(val);
|
||||||
|
return memo;
|
||||||
|
}
|
||||||
|
|
||||||
|
function increaseVerbosity(v, total) {
|
||||||
|
return total + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
program
|
||||||
|
.version('0.1.0')
|
||||||
|
.usage('[options] <file ...>')
|
||||||
|
.option('-i, --integer <n>', 'An integer argument', parseInt)
|
||||||
|
.option('-f, --float <n>', 'A float argument', parseFloat)
|
||||||
|
.option('-r, --range <a>..<b>', 'A range', range)
|
||||||
|
.option('-l, --list <items>', 'A list', list)
|
||||||
|
.option('-o, --optional [value]', 'An optional value')
|
||||||
|
.option('-c, --collect [value]', 'A repeatable value', collect, [])
|
||||||
|
.option('-v, --verbose', 'A value that can be increased', increaseVerbosity, 0)
|
||||||
|
.parse(process.argv);
|
||||||
|
|
||||||
|
console.log(' int: %j', program.integer);
|
||||||
|
console.log(' float: %j', program.float);
|
||||||
|
console.log(' optional: %j', program.optional);
|
||||||
|
program.range = program.range || [];
|
||||||
|
console.log(' range: %j..%j', program.range[0], program.range[1]);
|
||||||
|
console.log(' list: %j', program.list);
|
||||||
|
console.log(' collect: %j', program.collect);
|
||||||
|
console.log(' verbosity: %j', program.verbose);
|
||||||
|
console.log(' args: %j', program.args);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Regular Expression
|
||||||
|
```js
|
||||||
|
program
|
||||||
|
.version('0.1.0')
|
||||||
|
.option('-s --size <size>', 'Pizza size', /^(large|medium|small)$/i, 'medium')
|
||||||
|
.option('-d --drink [drink]', 'Drink', /^(coke|pepsi|izze)$/i)
|
||||||
|
.parse(process.argv);
|
||||||
|
|
||||||
|
console.log(' size: %j', program.size);
|
||||||
|
console.log(' drink: %j', program.drink);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Variadic arguments
|
||||||
|
|
||||||
|
The last argument of a command can be variadic, and only the last argument. To make an argument variadic you have to
|
||||||
|
append `...` to the argument name. Here is an example:
|
||||||
|
|
||||||
|
```js
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Module dependencies.
|
||||||
|
*/
|
||||||
|
|
||||||
|
var program = require('commander');
|
||||||
|
|
||||||
|
program
|
||||||
|
.version('0.1.0')
|
||||||
|
.command('rmdir <dir> [otherDirs...]')
|
||||||
|
.action(function (dir, otherDirs) {
|
||||||
|
console.log('rmdir %s', dir);
|
||||||
|
if (otherDirs) {
|
||||||
|
otherDirs.forEach(function (oDir) {
|
||||||
|
console.log('rmdir %s', oDir);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
program.parse(process.argv);
|
||||||
|
```
|
||||||
|
|
||||||
|
An `Array` is used for the value of a variadic argument. This applies to `program.args` as well as the argument passed
|
||||||
|
to your action as demonstrated above.
|
||||||
|
|
||||||
|
## Specify the argument syntax
|
||||||
|
|
||||||
|
```js
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
var program = require('commander');
|
||||||
|
|
||||||
|
program
|
||||||
|
.version('0.1.0')
|
||||||
|
.arguments('<cmd> [env]')
|
||||||
|
.action(function (cmd, env) {
|
||||||
|
cmdValue = cmd;
|
||||||
|
envValue = env;
|
||||||
|
});
|
||||||
|
|
||||||
|
program.parse(process.argv);
|
||||||
|
|
||||||
|
if (typeof cmdValue === 'undefined') {
|
||||||
|
console.error('no command given!');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
console.log('command:', cmdValue);
|
||||||
|
console.log('environment:', envValue || "no environment given");
|
||||||
|
```
|
||||||
|
Angled brackets (e.g. `<cmd>`) indicate required input. Square brackets (e.g. `[env]`) indicate optional input.
|
||||||
|
|
||||||
|
## Git-style sub-commands
|
||||||
|
|
||||||
|
```js
|
||||||
|
// file: ./examples/pm
|
||||||
|
var program = require('commander');
|
||||||
|
|
||||||
|
program
|
||||||
|
.version('0.1.0')
|
||||||
|
.command('install [name]', 'install one or more packages')
|
||||||
|
.command('search [query]', 'search with optional query')
|
||||||
|
.command('list', 'list packages installed', {isDefault: true})
|
||||||
|
.parse(process.argv);
|
||||||
|
```
|
||||||
|
|
||||||
|
When `.command()` is invoked with a description argument, no `.action(callback)` should be called to handle sub-commands, otherwise there will be an error. This tells commander that you're going to use separate executables for sub-commands, much like `git(1)` and other popular tools.
|
||||||
|
The commander will try to search the executables in the directory of the entry script (like `./examples/pm`) with the name `program-command`, like `pm-install`, `pm-search`.
|
||||||
|
|
||||||
|
Options can be passed with the call to `.command()`. Specifying `true` for `opts.noHelp` will remove the option from the generated help output. Specifying `true` for `opts.isDefault` will run the subcommand if no other subcommand is specified.
|
||||||
|
|
||||||
|
If the program is designed to be installed globally, make sure the executables have proper modes, like `755`.
|
||||||
|
|
||||||
|
### `--harmony`
|
||||||
|
|
||||||
|
You can enable `--harmony` option in two ways:
|
||||||
|
* Use `#! /usr/bin/env node --harmony` in the sub-commands scripts. Note some os version don’t support this pattern.
|
||||||
|
* Use the `--harmony` option when call the command, like `node --harmony examples/pm publish`. The `--harmony` option will be preserved when spawning sub-command process.
|
||||||
|
|
||||||
|
## Automated --help
|
||||||
|
|
||||||
|
The help information is auto-generated based on the information commander already knows about your program, so the following `--help` info is for free:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ ./examples/pizza --help
|
||||||
|
|
||||||
|
Usage: pizza [options]
|
||||||
|
|
||||||
|
An application for pizzas ordering
|
||||||
|
|
||||||
|
Options:
|
||||||
|
|
||||||
|
-h, --help output usage information
|
||||||
|
-V, --version output the version number
|
||||||
|
-p, --peppers Add peppers
|
||||||
|
-P, --pineapple Add pineapple
|
||||||
|
-b, --bbq Add bbq sauce
|
||||||
|
-c, --cheese <type> Add the specified type of cheese [marble]
|
||||||
|
-C, --no-cheese You do not want any cheese
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## Custom help
|
||||||
|
|
||||||
|
You can display arbitrary `-h, --help` information
|
||||||
|
by listening for "--help". Commander will automatically
|
||||||
|
exit once you are done so that the remainder of your program
|
||||||
|
does not execute causing undesired behaviours, for example
|
||||||
|
in the following executable "stuff" will not output when
|
||||||
|
`--help` is used.
|
||||||
|
|
||||||
|
```js
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Module dependencies.
|
||||||
|
*/
|
||||||
|
|
||||||
|
var program = require('commander');
|
||||||
|
|
||||||
|
program
|
||||||
|
.version('0.1.0')
|
||||||
|
.option('-f, --foo', 'enable some foo')
|
||||||
|
.option('-b, --bar', 'enable some bar')
|
||||||
|
.option('-B, --baz', 'enable some baz');
|
||||||
|
|
||||||
|
// must be before .parse() since
|
||||||
|
// node's emit() is immediate
|
||||||
|
|
||||||
|
program.on('--help', function(){
|
||||||
|
console.log(' Examples:');
|
||||||
|
console.log('');
|
||||||
|
console.log(' $ custom-help --help');
|
||||||
|
console.log(' $ custom-help -h');
|
||||||
|
console.log('');
|
||||||
|
});
|
||||||
|
|
||||||
|
program.parse(process.argv);
|
||||||
|
|
||||||
|
console.log('stuff');
|
||||||
|
```
|
||||||
|
|
||||||
|
Yields the following help output when `node script-name.js -h` or `node script-name.js --help` are run:
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
Usage: custom-help [options]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
|
||||||
|
-h, --help output usage information
|
||||||
|
-V, --version output the version number
|
||||||
|
-f, --foo enable some foo
|
||||||
|
-b, --bar enable some bar
|
||||||
|
-B, --baz enable some baz
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
$ custom-help --help
|
||||||
|
$ custom-help -h
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## .outputHelp(cb)
|
||||||
|
|
||||||
|
Output help information without exiting.
|
||||||
|
Optional callback cb allows post-processing of help text before it is displayed.
|
||||||
|
|
||||||
|
If you want to display help by default (e.g. if no command was provided), you can use something like:
|
||||||
|
|
||||||
|
```js
|
||||||
|
var program = require('commander');
|
||||||
|
var colors = require('colors');
|
||||||
|
|
||||||
|
program
|
||||||
|
.version('0.1.0')
|
||||||
|
.command('getstream [url]', 'get stream URL')
|
||||||
|
.parse(process.argv);
|
||||||
|
|
||||||
|
if (!process.argv.slice(2).length) {
|
||||||
|
program.outputHelp(make_red);
|
||||||
|
}
|
||||||
|
|
||||||
|
function make_red(txt) {
|
||||||
|
return colors.red(txt); //display the help text in red on the console
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## .help(cb)
|
||||||
|
|
||||||
|
Output help information and exit immediately.
|
||||||
|
Optional callback cb allows post-processing of help text before it is displayed.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
```js
|
||||||
|
var program = require('commander');
|
||||||
|
|
||||||
|
program
|
||||||
|
.version('0.1.0')
|
||||||
|
.option('-C, --chdir <path>', 'change the working directory')
|
||||||
|
.option('-c, --config <path>', 'set config path. defaults to ./deploy.conf')
|
||||||
|
.option('-T, --no-tests', 'ignore test hook');
|
||||||
|
|
||||||
|
program
|
||||||
|
.command('setup [env]')
|
||||||
|
.description('run setup commands for all envs')
|
||||||
|
.option("-s, --setup_mode [mode]", "Which setup mode to use")
|
||||||
|
.action(function(env, options){
|
||||||
|
var mode = options.setup_mode || "normal";
|
||||||
|
env = env || 'all';
|
||||||
|
console.log('setup for %s env(s) with %s mode', env, mode);
|
||||||
|
});
|
||||||
|
|
||||||
|
program
|
||||||
|
.command('exec <cmd>')
|
||||||
|
.alias('ex')
|
||||||
|
.description('execute the given remote cmd')
|
||||||
|
.option("-e, --exec_mode <mode>", "Which exec mode to use")
|
||||||
|
.action(function(cmd, options){
|
||||||
|
console.log('exec "%s" using %s mode', cmd, options.exec_mode);
|
||||||
|
}).on('--help', function() {
|
||||||
|
console.log(' Examples:');
|
||||||
|
console.log();
|
||||||
|
console.log(' $ deploy exec sequential');
|
||||||
|
console.log(' $ deploy exec async');
|
||||||
|
console.log();
|
||||||
|
});
|
||||||
|
|
||||||
|
program
|
||||||
|
.command('*')
|
||||||
|
.action(function(env){
|
||||||
|
console.log('deploying "%s"', env);
|
||||||
|
});
|
||||||
|
|
||||||
|
program.parse(process.argv);
|
||||||
|
```
|
||||||
|
|
||||||
|
More Demos can be found in the [examples](https://github.com/tj/commander.js/tree/master/examples) directory.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
||||||
1137
engine/ds-screencast/node_modules/commander/index.js
generated
vendored
Normal file
1137
engine/ds-screencast/node_modules/commander/index.js
generated
vendored
Normal file
File diff suppressed because it is too large
Load diff
29
engine/ds-screencast/node_modules/commander/package.json
generated
vendored
Normal file
29
engine/ds-screencast/node_modules/commander/package.json
generated
vendored
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
{
|
||||||
|
"name": "commander",
|
||||||
|
"version": "2.11.0",
|
||||||
|
"description": "the complete solution for node.js command-line programs",
|
||||||
|
"keywords": [
|
||||||
|
"commander",
|
||||||
|
"command",
|
||||||
|
"option",
|
||||||
|
"parser"
|
||||||
|
],
|
||||||
|
"author": "TJ Holowaychuk <tj@vision-media.ca>",
|
||||||
|
"license": "MIT",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/tj/commander.js.git"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"should": "^11.2.1",
|
||||||
|
"sinon": "^2.3.5"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"test": "make test"
|
||||||
|
},
|
||||||
|
"main": "index",
|
||||||
|
"files": [
|
||||||
|
"index.js"
|
||||||
|
],
|
||||||
|
"dependencies": {}
|
||||||
|
}
|
||||||
20
engine/ds-screencast/node_modules/ws/LICENSE
generated
vendored
Normal file
20
engine/ds-screencast/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
engine/ds-screencast/node_modules/ws/README.md
generated
vendored
Normal file
548
engine/ds-screencast/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
engine/ds-screencast/node_modules/ws/browser.js
generated
vendored
Normal file
8
engine/ds-screencast/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'
|
||||||
|
);
|
||||||
|
};
|
||||||
13
engine/ds-screencast/node_modules/ws/index.js
generated
vendored
Normal file
13
engine/ds-screencast/node_modules/ws/index.js
generated
vendored
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const WebSocket = require('./lib/websocket');
|
||||||
|
|
||||||
|
WebSocket.createWebSocketStream = require('./lib/stream');
|
||||||
|
WebSocket.Server = require('./lib/websocket-server');
|
||||||
|
WebSocket.Receiver = require('./lib/receiver');
|
||||||
|
WebSocket.Sender = require('./lib/sender');
|
||||||
|
|
||||||
|
WebSocket.WebSocket = WebSocket;
|
||||||
|
WebSocket.WebSocketServer = WebSocket.Server;
|
||||||
|
|
||||||
|
module.exports = WebSocket;
|
||||||
131
engine/ds-screencast/node_modules/ws/lib/buffer-util.js
generated
vendored
Normal file
131
engine/ds-screencast/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
engine/ds-screencast/node_modules/ws/lib/constants.js
generated
vendored
Normal file
19
engine/ds-screencast/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
engine/ds-screencast/node_modules/ws/lib/event-target.js
generated
vendored
Normal file
292
engine/ds-screencast/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
engine/ds-screencast/node_modules/ws/lib/extension.js
generated
vendored
Normal file
203
engine/ds-screencast/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
engine/ds-screencast/node_modules/ws/lib/limiter.js
generated
vendored
Normal file
55
engine/ds-screencast/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
engine/ds-screencast/node_modules/ws/lib/permessage-deflate.js
generated
vendored
Normal file
528
engine/ds-screencast/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|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
|
||||||
|
* @param {Boolean} [isServer=false] Create the instance in either server or
|
||||||
|
* client mode
|
||||||
|
* @param {Number} [maxPayload=0] The maximum allowed message length
|
||||||
|
*/
|
||||||
|
constructor(options, isServer, maxPayload) {
|
||||||
|
this._maxPayload = maxPayload | 0;
|
||||||
|
this._options = options || {};
|
||||||
|
this._threshold =
|
||||||
|
this._options.threshold !== undefined ? this._options.threshold : 1024;
|
||||||
|
this._isServer = !!isServer;
|
||||||
|
this._deflate = null;
|
||||||
|
this._inflate = null;
|
||||||
|
|
||||||
|
this.params = null;
|
||||||
|
|
||||||
|
if (!zlibLimiter) {
|
||||||
|
const concurrency =
|
||||||
|
this._options.concurrencyLimit !== undefined
|
||||||
|
? this._options.concurrencyLimit
|
||||||
|
: 10;
|
||||||
|
zlibLimiter = new Limiter(concurrency);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {String}
|
||||||
|
*/
|
||||||
|
static get extensionName() {
|
||||||
|
return 'permessage-deflate';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an extension negotiation offer.
|
||||||
|
*
|
||||||
|
* @return {Object} Extension parameters
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
offer() {
|
||||||
|
const params = {};
|
||||||
|
|
||||||
|
if (this._options.serverNoContextTakeover) {
|
||||||
|
params.server_no_context_takeover = true;
|
||||||
|
}
|
||||||
|
if (this._options.clientNoContextTakeover) {
|
||||||
|
params.client_no_context_takeover = true;
|
||||||
|
}
|
||||||
|
if (this._options.serverMaxWindowBits) {
|
||||||
|
params.server_max_window_bits = this._options.serverMaxWindowBits;
|
||||||
|
}
|
||||||
|
if (this._options.clientMaxWindowBits) {
|
||||||
|
params.client_max_window_bits = this._options.clientMaxWindowBits;
|
||||||
|
} else if (this._options.clientMaxWindowBits == null) {
|
||||||
|
params.client_max_window_bits = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return params;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Accept an extension negotiation offer/response.
|
||||||
|
*
|
||||||
|
* @param {Array} configurations The extension negotiation offers/reponse
|
||||||
|
* @return {Object} Accepted configuration
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
accept(configurations) {
|
||||||
|
configurations = this.normalizeParams(configurations);
|
||||||
|
|
||||||
|
this.params = this._isServer
|
||||||
|
? this.acceptAsServer(configurations)
|
||||||
|
: this.acceptAsClient(configurations);
|
||||||
|
|
||||||
|
return this.params;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Releases all resources used by the extension.
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
cleanup() {
|
||||||
|
if (this._inflate) {
|
||||||
|
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
engine/ds-screencast/node_modules/ws/lib/receiver.js
generated
vendored
Normal file
706
engine/ds-screencast/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
engine/ds-screencast/node_modules/ws/lib/sender.js
generated
vendored
Normal file
602
engine/ds-screencast/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
engine/ds-screencast/node_modules/ws/lib/stream.js
generated
vendored
Normal file
161
engine/ds-screencast/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
engine/ds-screencast/node_modules/ws/lib/subprotocol.js
generated
vendored
Normal file
62
engine/ds-screencast/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
engine/ds-screencast/node_modules/ws/lib/validation.js
generated
vendored
Normal file
152
engine/ds-screencast/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
engine/ds-screencast/node_modules/ws/lib/websocket-server.js
generated
vendored
Normal file
554
engine/ds-screencast/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,
|
||||||
|
true,
|
||||||
|
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
engine/ds-screencast/node_modules/ws/lib/websocket.js
generated
vendored
Normal file
1393
engine/ds-screencast/node_modules/ws/lib/websocket.js
generated
vendored
Normal file
File diff suppressed because it is too large
Load diff
69
engine/ds-screencast/node_modules/ws/package.json
generated
vendored
Normal file
69
engine/ds-screencast/node_modules/ws/package.json
generated
vendored
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
{
|
||||||
|
"name": "ws",
|
||||||
|
"version": "8.19.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": {
|
||||||
|
"benchmark": "^2.1.4",
|
||||||
|
"bufferutil": "^4.0.1",
|
||||||
|
"eslint": "^9.0.0",
|
||||||
|
"eslint-config-prettier": "^10.0.1",
|
||||||
|
"eslint-plugin-prettier": "^5.0.0",
|
||||||
|
"globals": "^16.0.0",
|
||||||
|
"mocha": "^8.4.0",
|
||||||
|
"nyc": "^15.0.0",
|
||||||
|
"prettier": "^3.0.0",
|
||||||
|
"utf-8-validate": "^6.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
8
engine/ds-screencast/node_modules/ws/wrapper.mjs
generated
vendored
Normal file
8
engine/ds-screencast/node_modules/ws/wrapper.mjs
generated
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
import createWebSocketStream from './lib/stream.js';
|
||||||
|
import Receiver from './lib/receiver.js';
|
||||||
|
import Sender from './lib/sender.js';
|
||||||
|
import WebSocket from './lib/websocket.js';
|
||||||
|
import WebSocketServer from './lib/websocket-server.js';
|
||||||
|
|
||||||
|
export { createWebSocketStream, Receiver, Sender, WebSocket, WebSocketServer };
|
||||||
|
export default WebSocket;
|
||||||
78
engine/ds-screencast/package-lock.json
generated
Normal file
78
engine/ds-screencast/package-lock.json
generated
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
{
|
||||||
|
"name": "ds-screencast",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "ds-screencast",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"chrome-remote-interface": "^0.34.0",
|
||||||
|
"ws": "^8.19.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/chrome-remote-interface": {
|
||||||
|
"version": "0.34.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/chrome-remote-interface/-/chrome-remote-interface-0.34.0.tgz",
|
||||||
|
"integrity": "sha512-rTTcTZ3zemx8I+nvBii7d8BAF0Ms8LLEroypfvwwZOwSpyNGLE28nStXyCA6VwGp2YSQfmCrQH21F/E+oBFvMw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"commander": "2.11.x",
|
||||||
|
"ws": "^7.2.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"chrome-remote-interface": "bin/client.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/chrome-remote-interface/node_modules/ws": {
|
||||||
|
"version": "7.5.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz",
|
||||||
|
"integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.3.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"bufferutil": "^4.0.1",
|
||||||
|
"utf-8-validate": "^5.0.2"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"bufferutil": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"utf-8-validate": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/commander": {
|
||||||
|
"version": "2.11.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/commander/-/commander-2.11.0.tgz",
|
||||||
|
"integrity": "sha512-b0553uYA5YAEGgyYIGYROzKQ7X5RAqedkfjiZxwi0kL1g3bOaBNNZfYkzt/CL0umgD5wc9Jec2FbB98CjkMRvQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/ws": {
|
||||||
|
"version": "8.19.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
|
||||||
|
"integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
16
engine/ds-screencast/package.json
Normal file
16
engine/ds-screencast/package.json
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"name": "ds-screencast",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"description": "",
|
||||||
|
"dependencies": {
|
||||||
|
"chrome-remote-interface": "^0.34.0",
|
||||||
|
"ws": "^8.19.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
221
engine/ds-stream/src/ds_hub.rs
Normal file
221
engine/ds-stream/src/ds_hub.rs
Normal file
|
|
@ -0,0 +1,221 @@
|
||||||
|
//! DreamStack Hub — ESP-NOW + UDP Signal Sender
|
||||||
|
//!
|
||||||
|
//! Hub-side transport for Panel IR:
|
||||||
|
//! - UDP: push full IR JSON to panels on port 9200
|
||||||
|
//! - ESP-NOW (via serial bridge): send binary signal frames (<1ms)
|
||||||
|
//!
|
||||||
|
//! # Architecture
|
||||||
|
//!
|
||||||
|
//! Since Linux doesn't have native ESP-NOW support, we use an
|
||||||
|
//! ESP32-C3 USB dongle as a serial bridge:
|
||||||
|
//!
|
||||||
|
//! ```text
|
||||||
|
//! ┌──────────┐ UART/USB ┌──────────┐ ESP-NOW ┌──────────┐
|
||||||
|
//! │ ds-hub │ ─────────► │ C3 bridge │ ────────► │ Panel │
|
||||||
|
//! │ (Rust) │ ◄───────── │ (dongle) │ ◄──────── │ (ESP32) │
|
||||||
|
//! └──────────┘ └──────────┘ └──────────┘
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! For testing without hardware, the hub falls back to UDP-only mode,
|
||||||
|
//! which works with the browser previewer over WebSocket.
|
||||||
|
|
||||||
|
use std::net::UdpSocket;
|
||||||
|
use std::io::{self, Read, Write};
|
||||||
|
use std::time::Duration;
|
||||||
|
use std::fs;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
/// DreamStack binary frame types (must match ds_espnow.h)
|
||||||
|
pub mod frame {
|
||||||
|
pub const DS_NOW_SIG: u8 = 0x20;
|
||||||
|
pub const DS_NOW_SIG_BATCH: u8 = 0x21;
|
||||||
|
pub const DS_NOW_TOUCH: u8 = 0x30;
|
||||||
|
pub const DS_NOW_ACTION: u8 = 0x31;
|
||||||
|
pub const DS_NOW_PING: u8 = 0xFE;
|
||||||
|
pub const DS_NOW_PONG: u8 = 0xFD;
|
||||||
|
|
||||||
|
pub const DS_UDP_IR_PUSH: u8 = 0x40;
|
||||||
|
pub const DS_UDP_IR_FRAG: u8 = 0x41;
|
||||||
|
|
||||||
|
pub const DS_MAGIC: [u8; 2] = [0xD5, 0x7A];
|
||||||
|
pub const DS_UDP_PORT: u16 = 9200;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A signal update ready to send.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct SignalUpdate {
|
||||||
|
pub id: u16,
|
||||||
|
pub value: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Hub transport — sends signals and IR to panels.
|
||||||
|
pub struct DsHub {
|
||||||
|
udp: UdpSocket,
|
||||||
|
panel_addr: String,
|
||||||
|
seq: u8,
|
||||||
|
// serial: Option<Box<dyn SerialPort>>, // For ESP32-C3 bridge (future)
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DsHub {
|
||||||
|
/// Create a new hub targeting a panel at the given address.
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// let hub = DsHub::new("192.168.1.100")?;
|
||||||
|
/// hub.push_ir(&ir_json)?;
|
||||||
|
/// hub.send_signal(0, 75)?;
|
||||||
|
/// ```
|
||||||
|
pub fn new(panel_ip: &str) -> io::Result<Self> {
|
||||||
|
let udp = UdpSocket::bind("0.0.0.0:0")?;
|
||||||
|
udp.set_broadcast(true)?;
|
||||||
|
Ok(DsHub {
|
||||||
|
udp,
|
||||||
|
panel_addr: format!("{}:{}", panel_ip, frame::DS_UDP_PORT),
|
||||||
|
seq: 0,
|
||||||
|
// serial: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a hub that broadcasts to all panels on the network.
|
||||||
|
pub fn broadcast() -> io::Result<Self> {
|
||||||
|
let udp = UdpSocket::bind("0.0.0.0:0")?;
|
||||||
|
udp.set_broadcast(true)?;
|
||||||
|
Ok(DsHub {
|
||||||
|
udp,
|
||||||
|
panel_addr: format!("255.255.255.255:{}", frame::DS_UDP_PORT),
|
||||||
|
seq: 0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Push IR JSON to the panel via UDP.
|
||||||
|
/// Fragments if > 1400 bytes (UDP MTU safety margin).
|
||||||
|
pub fn push_ir(&mut self, ir_json: &str) -> io::Result<()> {
|
||||||
|
let data = ir_json.as_bytes();
|
||||||
|
|
||||||
|
if data.len() <= 1400 {
|
||||||
|
// Single packet
|
||||||
|
let mut buf = Vec::with_capacity(4 + data.len());
|
||||||
|
buf.extend_from_slice(&frame::DS_MAGIC);
|
||||||
|
buf.push(frame::DS_UDP_IR_PUSH);
|
||||||
|
buf.push(0); // reserved
|
||||||
|
buf.extend_from_slice(&(data.len() as u16).to_le_bytes());
|
||||||
|
buf.extend_from_slice(data);
|
||||||
|
self.udp.send_to(&buf, &self.panel_addr)?;
|
||||||
|
} else {
|
||||||
|
// Fragment
|
||||||
|
let frag_size = 1400;
|
||||||
|
let total = (data.len() + frag_size - 1) / frag_size;
|
||||||
|
let seq = self.next_seq();
|
||||||
|
|
||||||
|
for i in 0..total {
|
||||||
|
let start = i * frag_size;
|
||||||
|
let end = std::cmp::min(start + frag_size, data.len());
|
||||||
|
let chunk = &data[start..end];
|
||||||
|
|
||||||
|
let mut buf = Vec::with_capacity(6 + chunk.len());
|
||||||
|
buf.extend_from_slice(&frame::DS_MAGIC);
|
||||||
|
buf.push(frame::DS_UDP_IR_FRAG);
|
||||||
|
buf.push(i as u8); // frag_id
|
||||||
|
buf.push(total as u8); // frag_total
|
||||||
|
buf.push(seq); // group seq
|
||||||
|
buf.extend_from_slice(chunk);
|
||||||
|
self.udp.send_to(&buf, &self.panel_addr)?;
|
||||||
|
|
||||||
|
// Small delay between fragments to avoid overwhelming receiver
|
||||||
|
std::thread::sleep(Duration::from_millis(2));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send a single signal update via UDP (fallback, ~2ms).
|
||||||
|
/// For sub-1ms, use ESP-NOW via serial bridge.
|
||||||
|
pub fn send_signal(&mut self, id: u16, value: i32) -> io::Result<()> {
|
||||||
|
let buf: [u8; 7] = [
|
||||||
|
frame::DS_NOW_SIG,
|
||||||
|
(id & 0xFF) as u8,
|
||||||
|
((id >> 8) & 0xFF) as u8,
|
||||||
|
(value & 0xFF) as u8,
|
||||||
|
((value >> 8) & 0xFF) as u8,
|
||||||
|
((value >> 16) & 0xFF) as u8,
|
||||||
|
((value >> 24) & 0xFF) as u8,
|
||||||
|
];
|
||||||
|
self.udp.send_to(&buf, &self.panel_addr)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send a batch of signal updates in one UDP packet.
|
||||||
|
pub fn send_signal_batch(&mut self, signals: &[SignalUpdate]) -> io::Result<()> {
|
||||||
|
let mut buf = Vec::with_capacity(3 + signals.len() * 6);
|
||||||
|
buf.push(frame::DS_NOW_SIG_BATCH);
|
||||||
|
buf.push(signals.len() as u8);
|
||||||
|
buf.push(self.next_seq());
|
||||||
|
|
||||||
|
for sig in signals {
|
||||||
|
buf.extend_from_slice(&sig.id.to_le_bytes());
|
||||||
|
buf.extend_from_slice(&sig.value.to_le_bytes());
|
||||||
|
}
|
||||||
|
|
||||||
|
self.udp.send_to(&buf, &self.panel_addr)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send a heartbeat ping.
|
||||||
|
pub fn send_ping(&mut self) -> io::Result<()> {
|
||||||
|
let buf = [frame::DS_NOW_PING, self.next_seq()];
|
||||||
|
self.udp.send_to(&buf, &self.panel_addr)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Listen for panel responses (pong, action events).
|
||||||
|
/// Blocks for up to `timeout` duration.
|
||||||
|
pub fn recv_event(&self, timeout: Duration) -> io::Result<Option<PanelEvent>> {
|
||||||
|
self.udp.set_read_timeout(Some(timeout))?;
|
||||||
|
let mut buf = [0u8; 256];
|
||||||
|
match self.udp.recv_from(&mut buf) {
|
||||||
|
Ok((len, _addr)) => {
|
||||||
|
if len < 1 { return Ok(None); }
|
||||||
|
match buf[0] {
|
||||||
|
frame::DS_NOW_PONG => Ok(Some(PanelEvent::Pong(buf[1]))),
|
||||||
|
frame::DS_NOW_ACTION if len >= 4 => Ok(Some(PanelEvent::Action {
|
||||||
|
node_id: buf[1],
|
||||||
|
action: buf[2],
|
||||||
|
})),
|
||||||
|
frame::DS_NOW_TOUCH if len >= 8 => {
|
||||||
|
let x = u16::from_le_bytes([buf[4], buf[5]]);
|
||||||
|
let y = u16::from_le_bytes([buf[6], buf[7]]);
|
||||||
|
Ok(Some(PanelEvent::Touch {
|
||||||
|
node_id: buf[1],
|
||||||
|
event: buf[2],
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
_ => Ok(None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) if e.kind() == io::ErrorKind::WouldBlock => Ok(None),
|
||||||
|
Err(e) => Err(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load IR JSON from a file and push to panel.
|
||||||
|
pub fn push_ir_file(&mut self, path: &Path) -> io::Result<()> {
|
||||||
|
let json = fs::read_to_string(path)?;
|
||||||
|
self.push_ir(&json)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn next_seq(&mut self) -> u8 {
|
||||||
|
let s = self.seq;
|
||||||
|
self.seq = self.seq.wrapping_add(1);
|
||||||
|
s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Events received from a panel.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum PanelEvent {
|
||||||
|
Pong(u8),
|
||||||
|
Action { node_id: u8, action: u8 },
|
||||||
|
Touch { node_id: u8, event: u8, x: u16, y: u16 },
|
||||||
|
}
|
||||||
|
|
@ -24,3 +24,4 @@
|
||||||
pub mod protocol;
|
pub mod protocol;
|
||||||
pub mod codec;
|
pub mod codec;
|
||||||
pub mod relay;
|
pub mod relay;
|
||||||
|
pub mod ds_hub;
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue