Rename to hkt.sh

This commit is contained in:
mango
2026-03-21 01:10:53 +08:00
parent 76a263d0f9
commit 8f1171fe99
6676 changed files with 1724268 additions and 0 deletions

18
node_modules/chrome-remote-interface/LICENSE generated vendored Normal file
View 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
node_modules/chrome-remote-interface/README.md generated vendored Normal file
View File

@@ -0,0 +1,991 @@
# chrome-remote-interface
[![CI status](https://github.com/cyrus-and/chrome-remote-interface/actions/workflows/ci.yml/badge.svg)](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
node_modules/chrome-remote-interface/bin/client.js generated vendored Executable file
View 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);
}
}

File diff suppressed because one or more lines are too long

46
node_modules/chrome-remote-interface/index.js generated vendored Normal file
View 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
node_modules/chrome-remote-interface/lib/api.js generated vendored Normal file
View 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
node_modules/chrome-remote-interface/lib/chrome.js generated vendored Normal file
View 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
node_modules/chrome-remote-interface/lib/defaults.js generated vendored Normal file
View File

@@ -0,0 +1,4 @@
'use strict';
module.exports.HOST = 'localhost';
module.exports.PORT = 9222;

127
node_modules/chrome-remote-interface/lib/devtools.js generated vendored Normal file
View 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
node_modules/chrome-remote-interface/lib/errors.js generated vendored Normal file
View 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;

View 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
node_modules/chrome-remote-interface/lib/protocol.json generated vendored Normal file

File diff suppressed because it is too large Load Diff

View 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;

View 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.

View File

@@ -0,0 +1,495 @@
# ws: a Node.js WebSocket library
[![Version npm](https://img.shields.io/npm/v/ws.svg?logo=npm)](https://www.npmjs.com/package/ws)
[![CI](https://img.shields.io/github/workflow/status/websockets/ws/CI/master?label=CI&logo=github)](https://github.com/websockets/ws/actions?query=workflow%3ACI+branch%3Amaster)
[![Coverage Status](https://img.shields.io/coveralls/websockets/ws/master.svg?logo=coveralls)](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

View 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'
);
};

View 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;

View 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
};
}

View 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: () => {}
};

View 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;

View 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 };

View 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;

View 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);
}

View 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;
}

View 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;

View 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;

View 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
};
}

View 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();
}

File diff suppressed because it is too large Load Diff

View 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
node_modules/chrome-remote-interface/package.json generated vendored Normal file
View 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
node_modules/chrome-remote-interface/webpack.config.js generated vendored Normal file
View 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'
}
};