Fix patch to use "git am" #5
|
@ -0,0 +1,7 @@
|
|||
spec: .gear/vitastor.spec
|
||||
tar: v@version@:.
|
||||
diff: v@version@:. . name=@name@-@version@.patch
|
||||
tar: upstream/cpp-btree:. name=cpp-btree base=
|
||||
tar: upstream/json11:. name=json11 base=
|
||||
copy?: .gear/*.sysconfig
|
||||
copy?: .gear/*.service
|
|
@ -0,0 +1,3 @@
|
|||
5dc108754ad40d3b1d024f9bd7cca0595ef1a1db upstream/cpp-btree
|
||||
97f06cb20c1e136fd37d58fb40f57dd8f8a3a4a7 upstream/json11
|
||||
cb282d25e07b25ac15fc0ac31ee04f983175ff11 v0.6.5
|
|
@ -0,0 +1,3 @@
|
|||
[remote "upstream"]
|
||||
url = http://yourcmc.ru/git/vitalif/vitastor.git
|
||||
fetch = +refs/heads/*:refs/remotes/upstream/*
|
|
@ -0,0 +1,183 @@
|
|||
|
||||
%global _unpackaged_files_terminate_build 1
|
||||
|
||||
Name: vitastor
|
||||
Version: 0.6.5
|
||||
Release: alt1
|
||||
Summary: Vitastor, a fast software-defined clustered block storage
|
||||
Group: System/Base
|
||||
|
||||
License: VNPL-1.1
|
||||
Url: https://vitastor.io/
|
||||
Source0: %name-%version.tar
|
||||
Source2: cpp-btree.tar
|
||||
Source3: json11.tar
|
||||
|
||||
Patch: %name-%version.patch
|
||||
|
||||
BuildRequires(pre): rpm-macros-cmake
|
||||
BuildRequires: cmake gcc-c++
|
||||
|
||||
BuildRequires: pkgconfig(liburing)
|
||||
BuildRequires: libgperftools-devel
|
||||
BuildRequires: node >= 10
|
||||
BuildRequires: libjerasure-devel
|
||||
BuildRequires: libgf-complete-devel
|
||||
BuildRequires: rdma-core-devel
|
||||
|
||||
%description
|
||||
Vitastor is a small, simple and fast clustered block storage (storage for VM drives),
|
||||
architecturally similar to Ceph which means strong consistency, primary-replication,
|
||||
symmetric clustering and automatic data distribution over any number of drives of any
|
||||
size with configurable redundancy (replication or erasure codes/XOR).
|
||||
|
||||
%package common
|
||||
Summary: Vitastor SDS Common
|
||||
Group: System/Base
|
||||
BuildArch: noarch
|
||||
|
||||
%description common
|
||||
Common utilities for Vitastor.
|
||||
|
||||
%package mon
|
||||
Summary: Vitastor SDS monitor service
|
||||
Group: System/Base
|
||||
BuildArch: noarch
|
||||
Requires: node
|
||||
Requires: lp_solve
|
||||
Requires: %name-common = %EVR
|
||||
|
||||
%description mon
|
||||
Vitastor SDS monitor service.
|
||||
Monitor is a separate daemon that watches cluster state and handles failures.
|
||||
|
||||
%package osd
|
||||
Summary: Vitastor SDS Object Storage Daemon
|
||||
Group: System/Base
|
||||
Requires: %name-common = %EVR
|
||||
|
||||
%description osd
|
||||
Vitastor SDS Object Storage Daemon is a process that stores data and serves read/write requests.
|
||||
|
||||
%package nbd
|
||||
Summary: Vitastor SDS NBD proxy
|
||||
Group: System/Base
|
||||
|
||||
%description nbd
|
||||
Vitastor SDS NBD proxy for kernel mounts.
|
||||
|
||||
%package -n lib%name-client
|
||||
Group: System/Libraries
|
||||
Summary: Vitastor SDS user-space client library
|
||||
License: VNPL-1.1 OR GPL-2.0+
|
||||
|
||||
%description -n lib%name-client
|
||||
Vitastor SDS user-space client library.
|
||||
|
||||
%package -n lib%name-blk
|
||||
Group: System/Libraries
|
||||
Summary: Vitastor SDS blk library
|
||||
|
||||
%description -n lib%name-blk
|
||||
Vitastor SDS blk library.
|
||||
|
||||
%package -n lib%name-devel
|
||||
Group: Development/C++
|
||||
Summary: Vitastor SDS headers of client and blk library
|
||||
License: VNPL-1.1 OR GPL-2.0+
|
||||
Requires: lib%name-blk = %EVR lib%name-client = %EVR
|
||||
|
||||
%description -n lib%name-devel
|
||||
This package contains libraries and headers needed to develop programs
|
||||
that use Vitastor SDS library.
|
||||
|
||||
%prep
|
||||
%setup
|
||||
%patch -p1
|
||||
tar -xf %SOURCE2 -C cpp-btree
|
||||
tar -xf %SOURCE3 -C json11
|
||||
|
||||
%build
|
||||
%cmake \
|
||||
-DCMAKE_VERBOSE_MAKEFILE=ON \
|
||||
-DWITH_QEMU=OFF \
|
||||
-DWITH_FIO=OFF
|
||||
%cmake_build
|
||||
|
||||
%install
|
||||
%cmakeinstall_std
|
||||
|
||||
mkdir -p %buildroot{%_datadir,%_localstatedir}/%name
|
||||
cp -r mon %buildroot%_datadir/%name
|
||||
|
||||
%pre common
|
||||
groupadd -r -f %name 2>/dev/null ||:
|
||||
useradd -r -g %name -s /sbin/nologin -c "Vitastor daemons" -M -d %_localstatedir/%name %name 2>/dev/null ||:
|
||||
|
||||
#%post mon
|
||||
#%post_service vitastor-mon
|
||||
|
||||
#%preun mon
|
||||
#%preun_service vitastor-mon
|
||||
|
||||
#%post osd
|
||||
#systemctl daemon-reload ||:
|
||||
#if [ "$1" -eq 1 ]; then
|
||||
# systemctl -q preset vitastor-osd@\*.service vitastor-osd.target ||:
|
||||
#else
|
||||
# systemctl try-restart vitastor-osd.target ||:
|
||||
#fi
|
||||
|
||||
#%preun osd
|
||||
#if [ "$1" -eq 0 ]; then
|
||||
# systemctl --no-reload -q disable vitastor-osd@\*.service vitastor-osd.target ||:
|
||||
# systemctl stop vitastor-osd@\*.service vitastor-osd.target ||:
|
||||
#fi
|
||||
|
||||
%files common
|
||||
%doc README.md README-ru.md VNPL-1.1.txt GPL-2.0.txt
|
||||
%attr(770,root,%name) %dir %_localstatedir/%name
|
||||
|
||||
%files osd
|
||||
%_bindir/%name-osd
|
||||
# ? may be to utils package?
|
||||
%_bindir/%name-dump-journal
|
||||
%_bindir/%name-rm
|
||||
|
||||
%files mon
|
||||
%_datadir/%name
|
||||
|
||||
%files nbd
|
||||
%_bindir/%name-nbd
|
||||
|
||||
%files -n lib%name-blk
|
||||
%_libdir/lib%{name}_blk.so.*
|
||||
|
||||
%files -n lib%name-client
|
||||
%_libdir/lib%{name}_client.so.*
|
||||
|
||||
%files -n lib%name-devel
|
||||
%_libdir/*.so
|
||||
%_includedir/*
|
||||
|
||||
%changelog
|
||||
* Sun Jul 11 2021 Alexey Shabalin <shaba@altlinux.org> 0.6.5-alt1
|
||||
- 0.6.5
|
||||
|
||||
* Fri Jul 02 2021 Alexey Shabalin <shaba@altlinux.org> 0.6.4-alt2
|
||||
- build master snapshot 30bb6026818b66bbe05bde38d70673e4633313e9
|
||||
- package client header to devel package
|
||||
- merge blk and client devel packages
|
||||
|
||||
* Wed May 19 2021 Alexey Shabalin <shaba@altlinux.org> 0.6.4-alt1
|
||||
- 0.6.4
|
||||
|
||||
* Thu May 06 2021 Alexey Shabalin <shaba@altlinux.org> 0.6.3-alt1
|
||||
- 0.6.3
|
||||
|
||||
* Mon Apr 19 2021 Alexey Shabalin <shaba@altlinux.org> 0.6.2-alt1
|
||||
- 0.6.2
|
||||
|
||||
* Fri Mar 19 2021 Alexey Shabalin <shaba@altlinux.org> 0.5.10-alt1
|
||||
- Initial build.
|
||||
|
|
@ -1,6 +1,16 @@
|
|||
cmake_minimum_required(VERSION 2.8)
|
||||
|
||||
set(MAJOR_VERSION "0")
|
||||
set(MINOR_VERSION "6")
|
||||
set(PATCH_VERSION "2")
|
||||
set(VERSION_STRING "${MAJOR_VERSION}.${MINOR_VERSION}.${PATCH_VERSION}")
|
||||
|
||||
if (CMAKE_VERSION VERSION_LESS 3.0)
|
||||
project(vitastor)
|
||||
else()
|
||||
cmake_policy(SET CMP0048 NEW)
|
||||
project(vitastor VERSION "${VERSION_STRING}")
|
||||
endif()
|
||||
|
||||
set(VERSION "0.6.5")
|
||||
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
## 1.1.2 (NOT YET RELEASED)
|
||||
|
||||
* Update Travis config to test on all LTE Node versions (i.e. 6+)
|
||||
* Reorganize README; add instructions re polyfills
|
||||
* Refactor the code
|
||||
* Upgrade dependencies
|
||||
* Fix minifying issue with missing semicolons
|
||||
* Configure .npmignore to reduce package size
|
||||
|
||||
|
||||
## 1.1.1 (2017-05-29)
|
||||
|
||||
* This CHANGELOG
|
||||
* Various optimizations for modern browsers
|
||||
* Fix %g, %o, %x and %X specifiers
|
||||
* Use ESLint instead of JSHint
|
||||
* Add CONTRIBUTORS file
|
|
@ -0,0 +1,25 @@
|
|||
Alexander Rose [@arose](https://github.com/arose)
|
||||
Alexandru Mărășteanu [@alexei](https://github.com/alexei)
|
||||
Andras [@andrasq](https://github.com/andrasq)
|
||||
Benoit Giannangeli [@giann](https://github.com/giann)
|
||||
Branden Visser [@mrvisser](https://github.com/mrvisser)
|
||||
David Baird
|
||||
daurnimator [@daurnimator](https://github.com/daurnimator)
|
||||
Doug Beck [@beck](https://github.com/beck)
|
||||
Dzmitry Litskalau [@litmit](https://github.com/litmit)
|
||||
Fred Ludlow [@fredludlow](https://github.com/fredludlow)
|
||||
Hans Pufal
|
||||
Henry [@alograg](https://github.com/alograg)
|
||||
Johnny Shields [@johnnyshields](https://github.com/johnnyshields)
|
||||
Kamal Abdali
|
||||
Matt Simerson [@msimerson](https://github.com/msimerson)
|
||||
Maxime Robert [@marob](https://github.com/marob)
|
||||
MeriemKhelifi [@MeriemKhelifi](https://github.com/MeriemKhelifi)
|
||||
Michael Schramm [@wodka](https://github.com/wodka)
|
||||
Nazar Mokrynskyi [@nazar-pc](https://github.com/nazar-pc)
|
||||
Oliver Salzburg [@oliversalzburg](https://github.com/oliversalzburg)
|
||||
Pablo [@ppollono](https://github.com/ppollono)
|
||||
Rabehaja Stevens [@RABEHAJA-STEVENS](https://github.com/RABEHAJA-STEVENS)
|
||||
Raphael Pigulla [@pigulla](https://github.com/pigulla)
|
||||
rebeccapeltz [@rebeccapeltz](https://github.com/rebeccapeltz)
|
||||
Stefan Tingström [@stingstrom](https://github.com/stingstrom)
|
|
@ -0,0 +1,24 @@
|
|||
Copyright (c) 2007-present, Alexandru Mărășteanu <hello@alexei.ro>
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
* Neither the name of this software nor the names of its contributors may be
|
||||
used to endorse or promote products derived from this software without
|
||||
specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
|
||||
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@ -0,0 +1,143 @@
|
|||
# sprintf-js
|
||||
|
||||
[![Build Status][travisci-image]][travisci-url] [![NPM Version][npm-image]][npm-url] [![Dependency Status][dependencies-image]][dependencies-url] [![devDependency Status][dev-dependencies-image]][dev-dependencies-url]
|
||||
|
||||
[travisci-image]: https://travis-ci.org/alexei/sprintf.js.svg?branch=master
|
||||
[travisci-url]: https://travis-ci.org/alexei/sprintf.js
|
||||
|
||||
[npm-image]: https://badge.fury.io/js/sprintf-js.svg
|
||||
[npm-url]: https://badge.fury.io/js/sprintf-js
|
||||
|
||||
[dependencies-image]: https://david-dm.org/alexei/sprintf.js.svg
|
||||
[dependencies-url]: https://david-dm.org/alexei/sprintf.js
|
||||
|
||||
[dev-dependencies-image]: https://david-dm.org/alexei/sprintf.js/dev-status.svg
|
||||
[dev-dependencies-url]: https://david-dm.org/alexei/sprintf.js#info=devDependencies
|
||||
|
||||
**sprintf-js** is a complete open source JavaScript `sprintf` implementation for the **browser** and **Node.js**.
|
||||
|
||||
**Note: as of v1.1.1 you might need some polyfills for older environments. See [Support](#support) section below.**
|
||||
|
||||
## Usage
|
||||
|
||||
var sprintf = require('sprintf-js').sprintf,
|
||||
vsprintf = require('sprintf-js').vsprintf
|
||||
|
||||
sprintf('%2$s %3$s a %1$s', 'cracker', 'Polly', 'wants')
|
||||
vsprintf('The first 4 letters of the english alphabet are: %s, %s, %s and %s', ['a', 'b', 'c', 'd'])
|
||||
|
||||
## Installation
|
||||
|
||||
### NPM
|
||||
|
||||
npm install sprintf-js
|
||||
|
||||
### Bower
|
||||
|
||||
bower install sprintf
|
||||
|
||||
## API
|
||||
|
||||
### `sprintf`
|
||||
|
||||
Returns a formatted string:
|
||||
|
||||
string sprintf(string format, mixed arg1?, mixed arg2?, ...)
|
||||
|
||||
### `vsprintf`
|
||||
|
||||
Same as `sprintf` except it takes an array of arguments, rather than a variable number of arguments:
|
||||
|
||||
string vsprintf(string format, array arguments?)
|
||||
|
||||
## Format specification
|
||||
|
||||
The placeholders in the format string are marked by `%` and are followed by one or more of these elements, in this order:
|
||||
|
||||
* An optional number followed by a `$` sign that selects which argument index to use for the value. If not specified, arguments will be placed in the same order as the placeholders in the input string.
|
||||
* An optional `+` sign that forces to preceed the result with a plus or minus sign on numeric values. By default, only the `-` sign is used on negative numbers.
|
||||
* An optional padding specifier that says what character to use for padding (if specified). Possible values are `0` or any other character precedeed by a `'` (single quote). The default is to pad with *spaces*.
|
||||
* An optional `-` sign, that causes `sprintf` to left-align the result of this placeholder. The default is to right-align the result.
|
||||
* An optional number, that says how many characters the result should have. If the value to be returned is shorter than this number, the result will be padded. When used with the `j` (JSON) type specifier, the padding length specifies the tab size used for indentation.
|
||||
* An optional precision modifier, consisting of a `.` (dot) followed by a number, that says how many digits should be displayed for floating point numbers. When used with the `g` type specifier, it specifies the number of significant digits. When used on a string, it causes the result to be truncated.
|
||||
* A type specifier that can be any of:
|
||||
* `%` — yields a literal `%` character
|
||||
* `b` — yields an integer as a binary number
|
||||
* `c` — yields an integer as the character with that ASCII value
|
||||
* `d` or `i` — yields an integer as a signed decimal number
|
||||
* `e` — yields a float using scientific notation
|
||||
* `u` — yields an integer as an unsigned decimal number
|
||||
* `f` — yields a float as is; see notes on precision above
|
||||
* `g` — yields a float as is; see notes on precision above
|
||||
* `o` — yields an integer as an octal number
|
||||
* `s` — yields a string as is
|
||||
* `t` — yields `true` or `false`
|
||||
* `T` — yields the type of the argument<sup><a href="#fn-1" name="fn-ref-1">1</a></sup>
|
||||
* `v` — yields the primitive value of the specified argument
|
||||
* `x` — yields an integer as a hexadecimal number (lower-case)
|
||||
* `X` — yields an integer as a hexadecimal number (upper-case)
|
||||
* `j` — yields a JavaScript object or array as a JSON encoded string
|
||||
|
||||
## Features
|
||||
|
||||
### Argument swapping
|
||||
|
||||
You can also swap the arguments. That is, the order of the placeholders doesn't have to match the order of the arguments. You can do that by simply indicating in the format string which arguments the placeholders refer to:
|
||||
|
||||
sprintf('%2$s %3$s a %1$s', 'cracker', 'Polly', 'wants')
|
||||
|
||||
And, of course, you can repeat the placeholders without having to increase the number of arguments.
|
||||
|
||||
### Named arguments
|
||||
|
||||
Format strings may contain replacement fields rather than positional placeholders. Instead of referring to a certain argument, you can now refer to a certain key within an object. Replacement fields are surrounded by rounded parentheses - `(` and `)` - and begin with a keyword that refers to a key:
|
||||
|
||||
var user = {
|
||||
name: 'Dolly',
|
||||
}
|
||||
sprintf('Hello %(name)s', user) // Hello Dolly
|
||||
|
||||
Keywords in replacement fields can be optionally followed by any number of keywords or indexes:
|
||||
|
||||
var users = [
|
||||
{name: 'Dolly'},
|
||||
{name: 'Molly'},
|
||||
{name: 'Polly'},
|
||||
]
|
||||
sprintf('Hello %(users[0].name)s, %(users[1].name)s and %(users[2].name)s', {users: users}) // Hello Dolly, Molly and Polly
|
||||
|
||||
Note: mixing positional and named placeholders is not (yet) supported
|
||||
|
||||
### Computed values
|
||||
|
||||
You can pass in a function as a dynamic value and it will be invoked (with no arguments) in order to compute the value on the fly.
|
||||
|
||||
sprintf('Current date and time: %s', function() { return new Date().toString() })
|
||||
|
||||
### AngularJS
|
||||
|
||||
You can use `sprintf` and `vsprintf` (also aliased as `fmt` and `vfmt` respectively) in your AngularJS projects. See `demo/`.
|
||||
|
||||
## Support
|
||||
|
||||
### Node.js
|
||||
|
||||
`sprintf-js` runs in all active Node versions (4.x+).
|
||||
|
||||
### Browser
|
||||
|
||||
`sprintf-js` should work in all modern browsers. As of v1.1.1, you might need polyfills for the following:
|
||||
|
||||
- `String.prototype.repeat()` (any IE)
|
||||
- `Array.isArray()` (IE < 9)
|
||||
- `Object.create()` (IE < 9)
|
||||
|
||||
YMMV
|
||||
|
||||
## License
|
||||
|
||||
**sprintf-js** is licensed under the terms of the 3-clause BSD license.
|
||||
|
||||
## Notes
|
||||
|
||||
<small><sup><a href="#fn-ref-1" name="fn-1">1</a></sup> `sprintf` doesn't use the `typeof` operator. As such, the value `null` is a `null`, an array is an `array` (not an `object`), a date value is a `date` etc.</small>
|
|
@ -0,0 +1,4 @@
|
|||
#ignore all generated files from diff
|
||||
#also skip line ending check
|
||||
*.js -diff -text
|
||||
*.map -diff -text
|
|
@ -0,0 +1,3 @@
|
|||
/*! sprintf-js v1.1.2 | Copyright (c) 2007-present, Alexandru Mărășteanu <hello@alexei.ro> | BSD-3-Clause */
|
||||
!function(){"use strict";angular.module("sprintf",[]).filter("sprintf",function(){return function(){return sprintf.apply(null,arguments)}}).filter("fmt",["$filter",function(t){return t("sprintf")}]).filter("vsprintf",function(){return function(t,n){return vsprintf(t,n)}}).filter("vfmt",["$filter",function(t){return t("vsprintf")}])}();
|
||||
//# sourceMappingURL=angular-sprintf.min.js.map
|
|
@ -0,0 +1 @@
|
|||
{"version":3,"sources":["angular-sprintf.js"],"names":["angular","module","filter","sprintf","apply","arguments","$filter","format","argv","vsprintf"],"mappings":";CAEC,WACG,aAEAA,QACIC,OAAO,UAAW,IAClBC,OAAO,UAAW,WACd,OAAO,WACH,OAAOC,QAAQC,MAAM,KAAMC,cAGnCH,OAAO,MAAO,CAAC,UAAW,SAASI,GAC/B,OAAOA,EAAQ,cAEnBJ,OAAO,WAAY,WACf,OAAO,SAASK,EAAQC,GACpB,OAAOC,SAASF,EAAQC,MAGhCN,OAAO,OAAQ,CAAC,UAAW,SAASI,GAChC,OAAOA,EAAQ,eAnB1B","file":"angular-sprintf.min.js","sourcesContent":["/* global angular, sprintf, vsprintf */\n\n!function() {\n 'use strict'\n\n angular.\n module('sprintf', []).\n filter('sprintf', function() {\n return function() {\n return sprintf.apply(null, arguments)\n }\n }).\n filter('fmt', ['$filter', function($filter) {\n return $filter('sprintf')\n }]).\n filter('vsprintf', function() {\n return function(format, argv) {\n return vsprintf(format, argv)\n }\n }).\n filter('vfmt', ['$filter', function($filter) {\n return $filter('vsprintf')\n }])\n}(); // eslint-disable-line\n"]}
|
|
@ -0,0 +1,3 @@
|
|||
/*! sprintf-js v1.1.2 | Copyright (c) 2007-present, Alexandru Mărășteanu <hello@alexei.ro> | BSD-3-Clause */
|
||||
!function(){"use strict";var g={not_string:/[^s]/,not_bool:/[^t]/,not_type:/[^T]/,not_primitive:/[^v]/,number:/[diefg]/,numeric_arg:/[bcdiefguxX]/,json:/[j]/,not_json:/[^j]/,text:/^[^\x25]+/,modulo:/^\x25{2}/,placeholder:/^\x25(?:([1-9]\d*)\$|\(([^)]+)\))?(\+)?(0|'[^$])?(-)?(\d+)?(?:\.(\d+))?([b-gijostTuvxX])/,key:/^([a-z_][a-z_\d]*)/i,key_access:/^\.([a-z_][a-z_\d]*)/i,index_access:/^\[(\d+)\]/,sign:/^[+-]/};function y(e){return function(e,t){var r,n,i,s,a,o,p,c,l,u=1,f=e.length,d="";for(n=0;n<f;n++)if("string"==typeof e[n])d+=e[n];else if("object"==typeof e[n]){if((s=e[n]).keys)for(r=t[u],i=0;i<s.keys.length;i++){if(null==r)throw new Error(y('[sprintf] Cannot access property "%s" of undefined value "%s"',s.keys[i],s.keys[i-1]));r=r[s.keys[i]]}else r=s.param_no?t[s.param_no]:t[u++];if(g.not_type.test(s.type)&&g.not_primitive.test(s.type)&&r instanceof Function&&(r=r()),g.numeric_arg.test(s.type)&&"number"!=typeof r&&isNaN(r))throw new TypeError(y("[sprintf] expecting number but found %T",r));switch(g.number.test(s.type)&&(c=0<=r),s.type){case"b":r=parseInt(r,10).toString(2);break;case"c":r=String.fromCharCode(parseInt(r,10));break;case"d":case"i":r=parseInt(r,10);break;case"j":r=JSON.stringify(r,null,s.width?parseInt(s.width):0);break;case"e":r=s.precision?parseFloat(r).toExponential(s.precision):parseFloat(r).toExponential();break;case"f":r=s.precision?parseFloat(r).toFixed(s.precision):parseFloat(r);break;case"g":r=s.precision?String(Number(r.toPrecision(s.precision))):parseFloat(r);break;case"o":r=(parseInt(r,10)>>>0).toString(8);break;case"s":r=String(r),r=s.precision?r.substring(0,s.precision):r;break;case"t":r=String(!!r),r=s.precision?r.substring(0,s.precision):r;break;case"T":r=Object.prototype.toString.call(r).slice(8,-1).toLowerCase(),r=s.precision?r.substring(0,s.precision):r;break;case"u":r=parseInt(r,10)>>>0;break;case"v":r=r.valueOf(),r=s.precision?r.substring(0,s.precision):r;break;case"x":r=(parseInt(r,10)>>>0).toString(16);break;case"X":r=(parseInt(r,10)>>>0).toString(16).toUpperCase()}g.json.test(s.type)?d+=r:(!g.number.test(s.type)||c&&!s.sign?l="":(l=c?"+":"-",r=r.toString().replace(g.sign,"")),o=s.pad_char?"0"===s.pad_char?"0":s.pad_char.charAt(1):" ",p=s.width-(l+r).length,a=s.width&&0<p?o.repeat(p):"",d+=s.align?l+r+a:"0"===o?l+a+r:a+l+r)}return d}(function(e){if(p[e])return p[e];var t,r=e,n=[],i=0;for(;r;){if(null!==(t=g.text.exec(r)))n.push(t[0]);else if(null!==(t=g.modulo.exec(r)))n.push("%");else{if(null===(t=g.placeholder.exec(r)))throw new SyntaxError("[sprintf] unexpected placeholder");if(t[2]){i|=1;var s=[],a=t[2],o=[];if(null===(o=g.key.exec(a)))throw new SyntaxError("[sprintf] failed to parse named argument key");for(s.push(o[1]);""!==(a=a.substring(o[0].length));)if(null!==(o=g.key_access.exec(a)))s.push(o[1]);else{if(null===(o=g.index_access.exec(a)))throw new SyntaxError("[sprintf] failed to parse named argument key");s.push(o[1])}t[2]=s}else i|=2;if(3===i)throw new Error("[sprintf] mixing positional and named placeholders is not (yet) supported");n.push({placeholder:t[0],param_no:t[1],keys:t[2],sign:t[3],pad_char:t[4],align:t[5],width:t[6],precision:t[7],type:t[8]})}r=r.substring(t[0].length)}return p[e]=n}(e),arguments)}function e(e,t){return y.apply(null,[e].concat(t||[]))}var p=Object.create(null);"undefined"!=typeof exports&&(exports.sprintf=y,exports.vsprintf=e),"undefined"!=typeof window&&(window.sprintf=y,window.vsprintf=e,"function"==typeof define&&define.amd&&define(function(){return{sprintf:y,vsprintf:e}}))}();
|
||||
//# sourceMappingURL=sprintf.min.js.map
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,63 @@
|
|||
{
|
||||
"_from": "sprintf-js@^1.1.2",
|
||||
"_id": "sprintf-js@1.1.2",
|
||||
"_inBundle": false,
|
||||
"_integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==",
|
||||
"_location": "/sprintf-js",
|
||||
"_phantomChildren": {},
|
||||
"_requested": {
|
||||
"type": "range",
|
||||
"registry": true,
|
||||
"raw": "sprintf-js@^1.1.2",
|
||||
"name": "sprintf-js",
|
||||
"escapedName": "sprintf-js",
|
||||
"rawSpec": "^1.1.2",
|
||||
"saveSpec": null,
|
||||
"fetchSpec": "^1.1.2"
|
||||
},
|
||||
"_requiredBy": [
|
||||
"/"
|
||||
],
|
||||
"_resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz",
|
||||
"_shasum": "da1765262bf8c0f571749f2ad6c26300207ae673",
|
||||
"_spec": "sprintf-js@^1.1.2",
|
||||
"_where": "/home/shaba/RPM/git/vitastor/mon",
|
||||
"author": {
|
||||
"name": "Alexandru Mărășteanu",
|
||||
"email": "hello@alexei.ro"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/alexei/sprintf.js/issues"
|
||||
},
|
||||
"bundleDependencies": false,
|
||||
"deprecated": false,
|
||||
"description": "JavaScript sprintf implementation",
|
||||
"devDependencies": {
|
||||
"benchmark": "^2.1.4",
|
||||
"eslint": "^5.10.0",
|
||||
"gulp": "^3.9.1",
|
||||
"gulp-benchmark": "^1.1.1",
|
||||
"gulp-eslint": "^5.0.0",
|
||||
"gulp-header": "^2.0.5",
|
||||
"gulp-mocha": "^6.0.0",
|
||||
"gulp-rename": "^1.4.0",
|
||||
"gulp-sourcemaps": "^2.6.4",
|
||||
"gulp-uglify": "^3.0.1",
|
||||
"mocha": "^5.2.0"
|
||||
},
|
||||
"homepage": "https://github.com/alexei/sprintf.js#readme",
|
||||
"license": "BSD-3-Clause",
|
||||
"main": "src/sprintf.js",
|
||||
"name": "sprintf-js",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/alexei/sprintf.js.git"
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "eslint --fix .",
|
||||
"pretest": "npm run lint",
|
||||
"test": "mocha test/*.js"
|
||||
},
|
||||
"version": "1.1.2"
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
/* global angular, sprintf, vsprintf */
|
||||
|
||||
!function() {
|
||||
'use strict'
|
||||
|
||||
angular.
|
||||
module('sprintf', []).
|
||||
filter('sprintf', function() {
|
||||
return function() {
|
||||
return sprintf.apply(null, arguments)
|
||||
}
|
||||
}).
|
||||
filter('fmt', ['$filter', function($filter) {
|
||||
return $filter('sprintf')
|
||||
}]).
|
||||
filter('vsprintf', function() {
|
||||
return function(format, argv) {
|
||||
return vsprintf(format, argv)
|
||||
}
|
||||
}).
|
||||
filter('vfmt', ['$filter', function($filter) {
|
||||
return $filter('vsprintf')
|
||||
}])
|
||||
}(); // eslint-disable-line
|
|
@ -0,0 +1,231 @@
|
|||
/* global window, exports, define */
|
||||
|
||||
!function() {
|
||||
'use strict'
|
||||
|
||||
var re = {
|
||||
not_string: /[^s]/,
|
||||
not_bool: /[^t]/,
|
||||
not_type: /[^T]/,
|
||||
not_primitive: /[^v]/,
|
||||
number: /[diefg]/,
|
||||
numeric_arg: /[bcdiefguxX]/,
|
||||
json: /[j]/,
|
||||
not_json: /[^j]/,
|
||||
text: /^[^\x25]+/,
|
||||
modulo: /^\x25{2}/,
|
||||
placeholder: /^\x25(?:([1-9]\d*)\$|\(([^)]+)\))?(\+)?(0|'[^$])?(-)?(\d+)?(?:\.(\d+))?([b-gijostTuvxX])/,
|
||||
key: /^([a-z_][a-z_\d]*)/i,
|
||||
key_access: /^\.([a-z_][a-z_\d]*)/i,
|
||||
index_access: /^\[(\d+)\]/,
|
||||
sign: /^[+-]/
|
||||
}
|
||||
|
||||
function sprintf(key) {
|
||||
// `arguments` is not an array, but should be fine for this call
|
||||
return sprintf_format(sprintf_parse(key), arguments)
|
||||
}
|
||||
|
||||
function vsprintf(fmt, argv) {
|
||||
return sprintf.apply(null, [fmt].concat(argv || []))
|
||||
}
|
||||
|
||||
function sprintf_format(parse_tree, argv) {
|
||||
var cursor = 1, tree_length = parse_tree.length, arg, output = '', i, k, ph, pad, pad_character, pad_length, is_positive, sign
|
||||
for (i = 0; i < tree_length; i++) {
|
||||
if (typeof parse_tree[i] === 'string') {
|
||||
output += parse_tree[i]
|
||||
}
|
||||
else if (typeof parse_tree[i] === 'object') {
|
||||
ph = parse_tree[i] // convenience purposes only
|
||||
if (ph.keys) { // keyword argument
|
||||
arg = argv[cursor]
|
||||
for (k = 0; k < ph.keys.length; k++) {
|
||||
if (arg == undefined) {
|
||||
throw new Error(sprintf('[sprintf] Cannot access property "%s" of undefined value "%s"', ph.keys[k], ph.keys[k-1]))
|
||||
}
|
||||
arg = arg[ph.keys[k]]
|
||||
}
|
||||
}
|
||||
else if (ph.param_no) { // positional argument (explicit)
|
||||
arg = argv[ph.param_no]
|
||||
}
|
||||
else { // positional argument (implicit)
|
||||
arg = argv[cursor++]
|
||||
}
|
||||
|
||||
if (re.not_type.test(ph.type) && re.not_primitive.test(ph.type) && arg instanceof Function) {
|
||||
arg = arg()
|
||||
}
|
||||
|
||||
if (re.numeric_arg.test(ph.type) && (typeof arg !== 'number' && isNaN(arg))) {
|
||||
throw new TypeError(sprintf('[sprintf] expecting number but found %T', arg))
|
||||
}
|
||||
|
||||
if (re.number.test(ph.type)) {
|
||||
is_positive = arg >= 0
|
||||
}
|
||||
|
||||
switch (ph.type) {
|
||||
case 'b':
|
||||
arg = parseInt(arg, 10).toString(2)
|
||||
break
|
||||
case 'c':
|
||||
arg = String.fromCharCode(parseInt(arg, 10))
|
||||
break
|
||||
case 'd':
|
||||
case 'i':
|
||||
arg = parseInt(arg, 10)
|
||||
break
|
||||
case 'j':
|
||||
arg = JSON.stringify(arg, null, ph.width ? parseInt(ph.width) : 0)
|
||||
break
|
||||
case 'e':
|
||||
arg = ph.precision ? parseFloat(arg).toExponential(ph.precision) : parseFloat(arg).toExponential()
|
||||
break
|
||||
case 'f':
|
||||
arg = ph.precision ? parseFloat(arg).toFixed(ph.precision) : parseFloat(arg)
|
||||
break
|
||||
case 'g':
|
||||
arg = ph.precision ? String(Number(arg.toPrecision(ph.precision))) : parseFloat(arg)
|
||||
break
|
||||
case 'o':
|
||||
arg = (parseInt(arg, 10) >>> 0).toString(8)
|
||||
break
|
||||
case 's':
|
||||
arg = String(arg)
|
||||
arg = (ph.precision ? arg.substring(0, ph.precision) : arg)
|
||||
break
|
||||
case 't':
|
||||
arg = String(!!arg)
|
||||
arg = (ph.precision ? arg.substring(0, ph.precision) : arg)
|
||||
break
|
||||
case 'T':
|
||||
arg = Object.prototype.toString.call(arg).slice(8, -1).toLowerCase()
|
||||
arg = (ph.precision ? arg.substring(0, ph.precision) : arg)
|
||||
break
|
||||
case 'u':
|
||||
arg = parseInt(arg, 10) >>> 0
|
||||
break
|
||||
case 'v':
|
||||
arg = arg.valueOf()
|
||||
arg = (ph.precision ? arg.substring(0, ph.precision) : arg)
|
||||
break
|
||||
case 'x':
|
||||
arg = (parseInt(arg, 10) >>> 0).toString(16)
|
||||
break
|
||||
case 'X':
|
||||
arg = (parseInt(arg, 10) >>> 0).toString(16).toUpperCase()
|
||||
break
|
||||
}
|
||||
if (re.json.test(ph.type)) {
|
||||
output += arg
|
||||
}
|
||||
else {
|
||||
if (re.number.test(ph.type) && (!is_positive || ph.sign)) {
|
||||
sign = is_positive ? '+' : '-'
|
||||
arg = arg.toString().replace(re.sign, '')
|
||||
}
|
||||
else {
|
||||
sign = ''
|
||||
}
|
||||
pad_character = ph.pad_char ? ph.pad_char === '0' ? '0' : ph.pad_char.charAt(1) : ' '
|
||||
pad_length = ph.width - (sign + arg).length
|
||||
pad = ph.width ? (pad_length > 0 ? pad_character.repeat(pad_length) : '') : ''
|
||||
output += ph.align ? sign + arg + pad : (pad_character === '0' ? sign + pad + arg : pad + sign + arg)
|
||||
}
|
||||
}
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
var sprintf_cache = Object.create(null)
|
||||
|
||||
function sprintf_parse(fmt) {
|
||||
if (sprintf_cache[fmt]) {
|
||||
return sprintf_cache[fmt]
|
||||
}
|
||||
|
||||
var _fmt = fmt, match, parse_tree = [], arg_names = 0
|
||||
while (_fmt) {
|
||||
if ((match = re.text.exec(_fmt)) !== null) {
|
||||
parse_tree.push(match[0])
|
||||
}
|
||||
else if ((match = re.modulo.exec(_fmt)) !== null) {
|
||||
parse_tree.push('%')
|
||||
}
|
||||
else if ((match = re.placeholder.exec(_fmt)) !== null) {
|
||||
if (match[2]) {
|
||||
arg_names |= 1
|
||||
var field_list = [], replacement_field = match[2], field_match = []
|
||||
if ((field_match = re.key.exec(replacement_field)) !== null) {
|
||||
field_list.push(field_match[1])
|
||||
while ((replacement_field = replacement_field.substring(field_match[0].length)) !== '') {
|
||||
if ((field_match = re.key_access.exec(replacement_field)) !== null) {
|
||||
field_list.push(field_match[1])
|
||||
}
|
||||
else if ((field_match = re.index_access.exec(replacement_field)) !== null) {
|
||||
field_list.push(field_match[1])
|
||||
}
|
||||
else {
|
||||
throw new SyntaxError('[sprintf] failed to parse named argument key')
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
throw new SyntaxError('[sprintf] failed to parse named argument key')
|
||||
}
|
||||
match[2] = field_list
|
||||
}
|
||||
else {
|
||||
arg_names |= 2
|
||||
}
|
||||
if (arg_names === 3) {
|
||||
throw new Error('[sprintf] mixing positional and named placeholders is not (yet) supported')
|
||||
}
|
||||
|
||||
parse_tree.push(
|
||||
{
|
||||
placeholder: match[0],
|
||||
param_no: match[1],
|
||||
keys: match[2],
|
||||
sign: match[3],
|
||||
pad_char: match[4],
|
||||
align: match[5],
|
||||
width: match[6],
|
||||
precision: match[7],
|
||||
type: match[8]
|
||||
}
|
||||
)
|
||||
}
|
||||
else {
|
||||
throw new SyntaxError('[sprintf] unexpected placeholder')
|
||||
}
|
||||
_fmt = _fmt.substring(match[0].length)
|
||||
}
|
||||
return sprintf_cache[fmt] = parse_tree
|
||||
}
|
||||
|
||||
/**
|
||||
* export to either browser or node.js
|
||||
*/
|
||||
/* eslint-disable quote-props */
|
||||
if (typeof exports !== 'undefined') {
|
||||
exports['sprintf'] = sprintf
|
||||
exports['vsprintf'] = vsprintf
|
||||
}
|
||||
if (typeof window !== 'undefined') {
|
||||
window['sprintf'] = sprintf
|
||||
window['vsprintf'] = vsprintf
|
||||
|
||||
if (typeof define === 'function' && define['amd']) {
|
||||
define(function() {
|
||||
return {
|
||||
'sprintf': sprintf,
|
||||
'vsprintf': vsprintf
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
/* eslint-enable quote-props */
|
||||
}(); // eslint-disable-line
|
|
@ -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.
|
|
@ -0,0 +1,496 @@
|
|||
# ws: a Node.js WebSocket library
|
||||
|
||||
[![Version npm](https://img.shields.io/npm/v/ws.svg?logo=npm)](https://www.npmjs.com/package/ws)
|
||||
[![Build](https://img.shields.io/github/workflow/status/websockets/ws/CI/master?label=build&logo=github)](https://github.com/websockets/ws/actions?query=workflow%3ACI+branch%3Amaster)
|
||||
[![Windows x86 Build](https://img.shields.io/appveyor/ci/lpinca/ws/master.svg?logo=appveyor)](https://ci.appveyor.com/project/lpinca/ws)
|
||||
[![Coverage Status](https://img.shields.io/coveralls/websockets/ws/master.svg)](https://coveralls.io/github/websockets/ws)
|
||||
|
||||
ws is a simple to use, blazing fast, and thoroughly tested WebSocket client and
|
||||
server implementation.
|
||||
|
||||
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 and spec compliance](#opt-in-for-performance-and-spec-compliance)
|
||||
- [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 and spec compliance
|
||||
|
||||
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 as required by the spec.
|
||||
|
||||
## 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(/\s*,\s*/)[0];
|
||||
});
|
||||
```
|
||||
|
||||
### 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
|
|
@ -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'
|
||||
);
|
||||
};
|
|
@ -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;
|
|
@ -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
|
||||
};
|
||||
}
|
|
@ -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: () => {}
|
||||
};
|
|
@ -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;
|
|
@ -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 };
|
|
@ -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;
|
|
@ -0,0 +1,517 @@
|
|||
'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][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);
|
||||
}
|
|
@ -0,0 +1,507 @@
|
|||
'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 stream.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);
|
||||
}
|
||||
|
||||
const compressed = (buf[0] & 0x40) === 0x40;
|
||||
|
||||
if (compressed && !this._extensions[PerMessageDeflate.extensionName]) {
|
||||
this._loop = false;
|
||||
return error(RangeError, 'RSV1 must be clear', true, 1002);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
if (!this._fragmented) {
|
||||
this._loop = false;
|
||||
return error(RangeError, 'invalid opcode 0', true, 1002);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
if (compressed) {
|
||||
this._loop = false;
|
||||
return error(RangeError, 'RSV1 must be clear', true, 1002);
|
||||
}
|
||||
|
||||
if (this._payloadLength > 0x7d) {
|
||||
this._loop = false;
|
||||
return error(
|
||||
RangeError,
|
||||
`invalid payload length ${this._payloadLength}`,
|
||||
true,
|
||||
1002
|
||||
);
|
||||
}
|
||||
} else {
|
||||
this._loop = false;
|
||||
return error(RangeError, `invalid opcode ${this._opcode}`, true, 1002);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
} else if (this._masked) {
|
||||
this._loop = false;
|
||||
return error(RangeError, 'MASK must be clear', true, 1002);
|
||||
}
|
||||
|
||||
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
|
||||
);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
} else {
|
||||
const code = data.readUInt16BE(0);
|
||||
|
||||
if (!isValidStatusCode(code)) {
|
||||
return error(RangeError, `invalid status code ${code}`, true, 1002);
|
||||
}
|
||||
|
||||
const buf = data.slice(2);
|
||||
|
||||
if (!isValidUTF8(buf)) {
|
||||
return error(Error, 'invalid UTF-8 sequence', true, 1007);
|
||||
}
|
||||
|
||||
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 {(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
|
||||
* @return {(Error|RangeError)} The error
|
||||
* @private
|
||||
*/
|
||||
function error(ErrorCtor, message, prefix, statusCode) {
|
||||
const err = new ErrorCtor(
|
||||
prefix ? `Invalid WebSocket frame: ${message}` : message
|
||||
);
|
||||
|
||||
Error.captureStackTrace(err, error);
|
||||
err[kStatusCode] = statusCode;
|
||||
return err;
|
||||
}
|
|
@ -0,0 +1,405 @@
|
|||
'use strict';
|
||||
|
||||
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} 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;
|
|
@ -0,0 +1,165 @@
|
|||
'use strict';
|
||||
|
||||
const { Duplex } = require('stream');
|
||||
|
||||
/**
|
||||
* Emits the `'close'` event on a stream.
|
||||
*
|
||||
* @param {stream.Duplex} 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 {stream.Duplex} The duplex stream
|
||||
* @public
|
||||
*/
|
||||
function createWebSocketStream(ws, options) {
|
||||
let resumeOnReceiverDrain = 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;
|
||||
|
||||
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);
|
||||
});
|
||||
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 && !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;
|
|
@ -0,0 +1,30 @@
|
|||
'use strict';
|
||||
|
||||
try {
|
||||
const isValidUTF8 = require('utf-8-validate');
|
||||
|
||||
exports.isValidUTF8 =
|
||||
typeof isValidUTF8 === 'object'
|
||||
? isValidUTF8.Validation.isValidUTF8 // utf-8-validate@<3.0.0
|
||||
: isValidUTF8;
|
||||
} catch (e) /* istanbul ignore next */ {
|
||||
exports.isValidUTF8 = () => true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
exports.isValidStatusCode = (code) => {
|
||||
return (
|
||||
(code >= 1000 &&
|
||||
code <= 1014 &&
|
||||
code !== 1004 &&
|
||||
code !== 1005 &&
|
||||
code !== 1006) ||
|
||||
(code >= 3000 && code <= 4999)
|
||||
);
|
||||
};
|
|
@ -0,0 +1,406 @@
|
|||
'use strict';
|
||||
|
||||
const EventEmitter = require('events');
|
||||
const { createHash } = require('crypto');
|
||||
const { createServer, STATUS_CODES } = require('http');
|
||||
|
||||
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}==$/;
|
||||
|
||||
/**
|
||||
* 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} [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) {
|
||||
throw new TypeError(
|
||||
'One of the "port", "server", or "noServer" options must be specified'
|
||||
);
|
||||
}
|
||||
|
||||
if (options.port != null) {
|
||||
this._server = createServer((req, res) => {
|
||||
const body = 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
||||
//
|
||||
// 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(() => this.emit('close'));
|
||||
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} 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 version = +req.headers['sec-websocket-version'];
|
||||
const extensions = {};
|
||||
|
||||
if (
|
||||
req.method !== 'GET' ||
|
||||
req.headers.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} 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'
|
||||
);
|
||||
}
|
||||
|
||||
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.trim().split(/ *, */);
|
||||
|
||||
//
|
||||
// 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.emit('close');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle premature socket errors.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
function socketOnError() {
|
||||
this.destroy();
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the connection when preconditions are not fulfilled.
|
||||
*
|
||||
* @param {net.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 || STATUS_CODES[code];
|
||||
headers = {
|
||||
Connection: 'close',
|
||||
'Content-Type': 'text/html',
|
||||
'Content-Length': Buffer.byteLength(message),
|
||||
...headers
|
||||
};
|
||||
|
||||
socket.write(
|
||||
`HTTP/1.1 ${code} ${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();
|
||||
}
|
|
@ -0,0 +1,933 @@
|
|||
'use strict';
|
||||
|
||||
const EventEmitter = require('events');
|
||||
const https = require('https');
|
||||
const http = require('http');
|
||||
const net = require('net');
|
||||
const tls = require('tls');
|
||||
const { randomBytes, createHash } = require('crypto');
|
||||
const { URL } = require('url');
|
||||
|
||||
const PerMessageDeflate = require('./permessage-deflate');
|
||||
const Receiver = require('./receiver');
|
||||
const Sender = require('./sender');
|
||||
const {
|
||||
BINARY_TYPES,
|
||||
EMPTY_BUFFER,
|
||||
GUID,
|
||||
kStatusCode,
|
||||
kWebSocket,
|
||||
NOOP
|
||||
} = require('./constants');
|
||||
const { addEventListener, removeEventListener } = require('./event-target');
|
||||
const { format, parse } = require('./extension');
|
||||
const { toBuffer } = require('./buffer-util');
|
||||
|
||||
const readyStates = ['CONNECTING', 'OPEN', 'CLOSING', 'CLOSED'];
|
||||
const protocolVersions = [8, 13];
|
||||
const closeTimeout = 30 * 1000;
|
||||
|
||||
/**
|
||||
* Class representing a WebSocket.
|
||||
*
|
||||
* @extends EventEmitter
|
||||
*/
|
||||
class WebSocket extends EventEmitter {
|
||||
/**
|
||||
* Create a new `WebSocket`.
|
||||
*
|
||||
* @param {(String|url.URL)} address The URL to which to connect
|
||||
* @param {(String|String[])} [protocols] The subprotocols
|
||||
* @param {Object} [options] Connection options
|
||||
*/
|
||||
constructor(address, protocols, options) {
|
||||
super();
|
||||
|
||||
this._binaryType = BINARY_TYPES[0];
|
||||
this._closeCode = 1006;
|
||||
this._closeFrameReceived = false;
|
||||
this._closeFrameSent = false;
|
||||
this._closeMessage = '';
|
||||
this._closeTimer = null;
|
||||
this._extensions = {};
|
||||
this._protocol = '';
|
||||
this._readyState = WebSocket.CONNECTING;
|
||||
this._receiver = null;
|
||||
this._sender = null;
|
||||
this._socket = null;
|
||||
|
||||
if (address !== null) {
|
||||
this._bufferedAmount = 0;
|
||||
this._isServer = false;
|
||||
this._redirects = 0;
|
||||
|
||||
if (Array.isArray(protocols)) {
|
||||
protocols = protocols.join(', ');
|
||||
} else if (typeof protocols === 'object' && protocols !== null) {
|
||||
options = protocols;
|
||||
protocols = undefined;
|
||||
}
|
||||
|
||||
initAsClient(this, address, protocols, options);
|
||||
} else {
|
||||
this._isServer = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This deviates from the WHATWG interface since ws doesn't support the
|
||||
* required default "blob" type (instead we define a custom "nodebuffer"
|
||||
* type).
|
||||
*
|
||||
* @type {String}
|
||||
*/
|
||||
get binaryType() {
|
||||
return this._binaryType;
|
||||
}
|
||||
|
||||
set binaryType(type) {
|
||||
if (!BINARY_TYPES.includes(type)) return;
|
||||
|
||||
this._binaryType = type;
|
||||
|
||||
//
|
||||
// Allow to change `binaryType` on the fly.
|
||||
//
|
||||
if (this._receiver) this._receiver._binaryType = type;
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {Number}
|
||||
*/
|
||||
get bufferedAmount() {
|
||||
if (!this._socket) return this._bufferedAmount;
|
||||
|
||||
return this._socket._writableState.length + this._sender._bufferedBytes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {String}
|
||||
*/
|
||||
get extensions() {
|
||||
return Object.keys(this._extensions).join();
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {String}
|
||||
*/
|
||||
get protocol() {
|
||||
return this._protocol;
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {Number}
|
||||
*/
|
||||
get readyState() {
|
||||
return this._readyState;
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {String}
|
||||
*/
|
||||
get url() {
|
||||
return this._url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up the socket and the internal resources.
|
||||
*
|
||||
* @param {net.Socket} socket The network socket between the server and client
|
||||
* @param {Buffer} head The first packet of the upgraded stream
|
||||
* @param {Number} [maxPayload=0] The maximum allowed message size
|
||||
* @private
|
||||
*/
|
||||
setSocket(socket, head, maxPayload) {
|
||||
const receiver = new Receiver(
|
||||
this.binaryType,
|
||||
this._extensions,
|
||||
this._isServer,
|
||||
maxPayload
|
||||
);
|
||||
|
||||
this._sender = new Sender(socket, this._extensions);
|
||||
this._receiver = receiver;
|
||||
this._socket = socket;
|
||||
|
||||
receiver[kWebSocket] = this;
|
||||
socket[kWebSocket] = this;
|
||||
|
||||
receiver.on('conclude', receiverOnConclude);
|
||||
receiver.on('drain', receiverOnDrain);
|
||||
receiver.on('error', receiverOnError);
|
||||
receiver.on('message', receiverOnMessage);
|
||||
receiver.on('ping', receiverOnPing);
|
||||
receiver.on('pong', receiverOnPong);
|
||||
|
||||
socket.setTimeout(0);
|
||||
socket.setNoDelay();
|
||||
|
||||
if (head.length > 0) socket.unshift(head);
|
||||
|
||||
socket.on('close', socketOnClose);
|
||||
socket.on('data', socketOnData);
|
||||
socket.on('end', socketOnEnd);
|
||||
socket.on('error', socketOnError);
|
||||
|
||||
this._readyState = WebSocket.OPEN;
|
||||
this.emit('open');
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit the `'close'` event.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
emitClose() {
|
||||
if (!this._socket) {
|
||||
this._readyState = WebSocket.CLOSED;
|
||||
this.emit('close', this._closeCode, this._closeMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._extensions[PerMessageDeflate.extensionName]) {
|
||||
this._extensions[PerMessageDeflate.extensionName].cleanup();
|
||||
}
|
||||
|
||||
this._receiver.removeAllListeners();
|
||||
this._readyState = WebSocket.CLOSED;
|
||||
this.emit('close', this._closeCode, this._closeMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a closing handshake.
|
||||
*
|
||||
* +----------+ +-----------+ +----------+
|
||||
* - - -|ws.close()|-->|close frame|-->|ws.close()|- - -
|
||||
* | +----------+ +-----------+ +----------+ |
|
||||
* +----------+ +-----------+ |
|
||||
* CLOSING |ws.close()|<--|close frame|<--+-----+ CLOSING
|
||||
* +----------+ +-----------+ |
|
||||
* | | | +---+ |
|
||||
* +------------------------+-->|fin| - - - -
|
||||
* | +---+ | +---+
|
||||
* - - - - -|fin|<---------------------+
|
||||
* +---+
|
||||
*
|
||||
* @param {Number} [code] Status code explaining why the connection is closing
|
||||
* @param {String} [data] A string explaining why the connection is closing
|
||||
* @public
|
||||
*/
|
||||
close(code, data) {
|
||||
if (this.readyState === WebSocket.CLOSED) return;
|
||||
if (this.readyState === WebSocket.CONNECTING) {
|
||||
const msg = 'WebSocket was closed before the connection was established';
|
||||
return abortHandshake(this, this._req, msg);
|
||||
}
|
||||
|
||||
if (this.readyState === WebSocket.CLOSING) {
|
||||
if (this._closeFrameSent && this._closeFrameReceived) this._socket.end();
|
||||
return;
|
||||
}
|
||||
|
||||
this._readyState = WebSocket.CLOSING;
|
||||
this._sender.close(code, data, !this._isServer, (err) => {
|
||||
//
|
||||
// This error is handled by the `'error'` listener on the socket. We only
|
||||
// want to know if the close frame has been sent here.
|
||||
//
|
||||
if (err) return;
|
||||
|
||||
this._closeFrameSent = true;
|
||||
if (this._closeFrameReceived) this._socket.end();
|
||||
});
|
||||
|
||||
//
|
||||
// Specify a timeout for the closing handshake to complete.
|
||||
//
|
||||
this._closeTimer = setTimeout(
|
||||
this._socket.destroy.bind(this._socket),
|
||||
closeTimeout
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a ping.
|
||||
*
|
||||
* @param {*} [data] The data to send
|
||||
* @param {Boolean} [mask] Indicates whether or not to mask `data`
|
||||
* @param {Function} [cb] Callback which is executed when the ping is sent
|
||||
* @public
|
||||
*/
|
||||
ping(data, mask, cb) {
|
||||
if (this.readyState === WebSocket.CONNECTING) {
|
||||
throw new Error('WebSocket is not open: readyState 0 (CONNECTING)');
|
||||
}
|
||||
|
||||
if (typeof data === 'function') {
|
||||
cb = data;
|
||||
data = mask = undefined;
|
||||
} else if (typeof mask === 'function') {
|
||||
cb = mask;
|
||||
mask = undefined;
|
||||
}
|
||||
|
||||
if (typeof data === 'number') data = data.toString();
|
||||
|
||||
if (this.readyState !== WebSocket.OPEN) {
|
||||
sendAfterClose(this, data, cb);
|
||||
return;
|
||||
}
|
||||
|
||||
if (mask === undefined) mask = !this._isServer;
|
||||
this._sender.ping(data || EMPTY_BUFFER, mask, cb);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a pong.
|
||||
*
|
||||
* @param {*} [data] The data to send
|
||||
* @param {Boolean} [mask] Indicates whether or not to mask `data`
|
||||
* @param {Function} [cb] Callback which is executed when the pong is sent
|
||||
* @public
|
||||
*/
|
||||
pong(data, mask, cb) {
|
||||
if (this.readyState === WebSocket.CONNECTING) {
|
||||
throw new Error('WebSocket is not open: readyState 0 (CONNECTING)');
|
||||
}
|
||||
|
||||
if (typeof data === 'function') {
|
||||
cb = data;
|
||||
data = mask = undefined;
|
||||
} else if (typeof mask === 'function') {
|
||||
cb = mask;
|
||||
mask = undefined;
|
||||
}
|
||||
|
||||
if (typeof data === 'number') data = data.toString();
|
||||
|
||||
if (this.readyState !== WebSocket.OPEN) {
|
||||
sendAfterClose(this, data, cb);
|
||||
return;
|
||||
}
|
||||
|
||||
if (mask === undefined) mask = !this._isServer;
|
||||
this._sender.pong(data || EMPTY_BUFFER, mask, cb);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a data message.
|
||||
*
|
||||
* @param {*} data The message to send
|
||||
* @param {Object} [options] Options object
|
||||
* @param {Boolean} [options.compress] Specifies whether or not to compress
|
||||
* `data`
|
||||
* @param {Boolean} [options.binary] Specifies whether `data` is binary or
|
||||
* text
|
||||
* @param {Boolean} [options.fin=true] Specifies whether the fragment is the
|
||||
* last one
|
||||
* @param {Boolean} [options.mask] Specifies whether or not to mask `data`
|
||||
* @param {Function} [cb] Callback which is executed when data is written out
|
||||
* @public
|
||||
*/
|
||||
send(data, options, cb) {
|
||||
if (this.readyState === WebSocket.CONNECTING) {
|
||||
throw new Error('WebSocket is not open: readyState 0 (CONNECTING)');
|
||||
}
|
||||
|
||||
if (typeof options === 'function') {
|
||||
cb = options;
|
||||
options = {};
|
||||
}
|
||||
|
||||
if (typeof data === 'number') data = data.toString();
|
||||
|
||||
if (this.readyState !== WebSocket.OPEN) {
|
||||
sendAfterClose(this, data, cb);
|
||||
return;
|
||||
}
|
||||
|
||||
const opts = {
|
||||
binary: typeof data !== 'string',
|
||||
mask: !this._isServer,
|
||||
compress: true,
|
||||
fin: true,
|
||||
...options
|
||||
};
|
||||
|
||||
if (!this._extensions[PerMessageDeflate.extensionName]) {
|
||||
opts.compress = false;
|
||||
}
|
||||
|
||||
this._sender.send(data || EMPTY_BUFFER, opts, cb);
|
||||
}
|
||||
|
||||
/**
|
||||
* Forcibly close the connection.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
terminate() {
|
||||
if (this.readyState === WebSocket.CLOSED) return;
|
||||
if (this.readyState === WebSocket.CONNECTING) {
|
||||
const msg = 'WebSocket was closed before the connection was established';
|
||||
return abortHandshake(this, this._req, msg);
|
||||
}
|
||||
|
||||
if (this._socket) {
|
||||
this._readyState = WebSocket.CLOSING;
|
||||
this._socket.destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
readyStates.forEach((readyState, i) => {
|
||||
const descriptor = { enumerable: true, value: i };
|
||||
|
||||
Object.defineProperty(WebSocket.prototype, readyState, descriptor);
|
||||
Object.defineProperty(WebSocket, readyState, descriptor);
|
||||
});
|
||||
|
||||
[
|
||||
'binaryType',
|
||||
'bufferedAmount',
|
||||
'extensions',
|
||||
'protocol',
|
||||
'readyState',
|
||||
'url'
|
||||
].forEach((property) => {
|
||||
Object.defineProperty(WebSocket.prototype, property, { enumerable: true });
|
||||
});
|
||||
|
||||
//
|
||||
// Add the `onopen`, `onerror`, `onclose`, and `onmessage` attributes.
|
||||
// See https://html.spec.whatwg.org/multipage/comms.html#the-websocket-interface
|
||||
//
|
||||
['open', 'error', 'close', 'message'].forEach((method) => {
|
||||
Object.defineProperty(WebSocket.prototype, `on${method}`, {
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
/**
|
||||
* Return the listener of the event.
|
||||
*
|
||||
* @return {(Function|undefined)} The event listener or `undefined`
|
||||
* @public
|
||||
*/
|
||||
get() {
|
||||
const listeners = this.listeners(method);
|
||||
for (let i = 0; i < listeners.length; i++) {
|
||||
if (listeners[i]._listener) return listeners[i]._listener;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
},
|
||||
/**
|
||||
* Add a listener for the event.
|
||||
*
|
||||
* @param {Function} listener The listener to add
|
||||
* @public
|
||||
*/
|
||||
set(listener) {
|
||||
const listeners = this.listeners(method);
|
||||
for (let i = 0; i < listeners.length; i++) {
|
||||
//
|
||||
// Remove only the listeners added via `addEventListener`.
|
||||
//
|
||||
if (listeners[i]._listener) this.removeListener(method, listeners[i]);
|
||||
}
|
||||
this.addEventListener(method, listener);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
WebSocket.prototype.addEventListener = addEventListener;
|
||||
WebSocket.prototype.removeEventListener = removeEventListener;
|
||||
|
||||
module.exports = WebSocket;
|
||||
|
||||
/**
|
||||
* Initialize a WebSocket client.
|
||||
*
|
||||
* @param {WebSocket} websocket The client to initialize
|
||||
* @param {(String|url.URL)} address The URL to which to connect
|
||||
* @param {String} [protocols] The subprotocols
|
||||
* @param {Object} [options] Connection options
|
||||
* @param {(Boolean|Object)} [options.perMessageDeflate=true] Enable/disable
|
||||
* permessage-deflate
|
||||
* @param {Number} [options.handshakeTimeout] Timeout in milliseconds for the
|
||||
* handshake request
|
||||
* @param {Number} [options.protocolVersion=13] Value of the
|
||||
* `Sec-WebSocket-Version` header
|
||||
* @param {String} [options.origin] Value of the `Origin` or
|
||||
* `Sec-WebSocket-Origin` header
|
||||
* @param {Number} [options.maxPayload=104857600] The maximum allowed message
|
||||
* size
|
||||
* @param {Boolean} [options.followRedirects=false] Whether or not to follow
|
||||
* redirects
|
||||
* @param {Number} [options.maxRedirects=10] The maximum number of redirects
|
||||
* allowed
|
||||
* @private
|
||||
*/
|
||||
function initAsClient(websocket, address, protocols, options) {
|
||||
const opts = {
|
||||
protocolVersion: protocolVersions[1],
|
||||
maxPayload: 100 * 1024 * 1024,
|
||||
perMessageDeflate: true,
|
||||
followRedirects: false,
|
||||
maxRedirects: 10,
|
||||
...options,
|
||||
createConnection: undefined,
|
||||
socketPath: undefined,
|
||||
hostname: undefined,
|
||||
protocol: undefined,
|
||||
timeout: undefined,
|
||||
method: undefined,
|
||||
host: undefined,
|
||||
path: undefined,
|
||||
port: undefined
|
||||
};
|
||||
|
||||
if (!protocolVersions.includes(opts.protocolVersion)) {
|
||||
throw new RangeError(
|
||||
`Unsupported protocol version: ${opts.protocolVersion} ` +
|
||||
`(supported versions: ${protocolVersions.join(', ')})`
|
||||
);
|
||||
}
|
||||
|
||||
let parsedUrl;
|
||||
|
||||
if (address instanceof URL) {
|
||||
parsedUrl = address;
|
||||
websocket._url = address.href;
|
||||
} else {
|
||||
parsedUrl = new URL(address);
|
||||
websocket._url = address;
|
||||
}
|
||||
|
||||
const isUnixSocket = parsedUrl.protocol === 'ws+unix:';
|
||||
|
||||
if (!parsedUrl.host && (!isUnixSocket || !parsedUrl.pathname)) {
|
||||
throw new Error(`Invalid URL: ${websocket.url}`);
|
||||
}
|
||||
|
||||
const isSecure =
|
||||
parsedUrl.protocol === 'wss:' || parsedUrl.protocol === 'https:';
|
||||
const defaultPort = isSecure ? 443 : 80;
|
||||
const key = randomBytes(16).toString('base64');
|
||||
const get = isSecure ? https.get : http.get;
|
||||
let perMessageDeflate;
|
||||
|
||||
opts.createConnection = isSecure ? tlsConnect : netConnect;
|
||||
opts.defaultPort = opts.defaultPort || defaultPort;
|
||||
opts.port = parsedUrl.port || defaultPort;
|
||||
opts.host = parsedUrl.hostname.startsWith('[')
|
||||
? parsedUrl.hostname.slice(1, -1)
|
||||
: parsedUrl.hostname;
|
||||
opts.headers = {
|
||||
'Sec-WebSocket-Version': opts.protocolVersion,
|
||||
'Sec-WebSocket-Key': key,
|
||||
Connection: 'Upgrade',
|
||||
Upgrade: 'websocket',
|
||||
...opts.headers
|
||||
};
|
||||
opts.path = parsedUrl.pathname + parsedUrl.search;
|
||||
opts.timeout = opts.handshakeTimeout;
|
||||
|
||||
if (opts.perMessageDeflate) {
|
||||
perMessageDeflate = new PerMessageDeflate(
|
||||
opts.perMessageDeflate !== true ? opts.perMessageDeflate : {},
|
||||
false,
|
||||
opts.maxPayload
|
||||
);
|
||||
opts.headers['Sec-WebSocket-Extensions'] = format({
|
||||
[PerMessageDeflate.extensionName]: perMessageDeflate.offer()
|
||||
});
|
||||
}
|
||||
if (protocols) {
|
||||
opts.headers['Sec-WebSocket-Protocol'] = protocols;
|
||||
}
|
||||
if (opts.origin) {
|
||||
if (opts.protocolVersion < 13) {
|
||||
opts.headers['Sec-WebSocket-Origin'] = opts.origin;
|
||||
} else {
|
||||
opts.headers.Origin = opts.origin;
|
||||
}
|
||||
}
|
||||
if (parsedUrl.username || parsedUrl.password) {
|
||||
opts.auth = `${parsedUrl.username}:${parsedUrl.password}`;
|
||||
}
|
||||
|
||||
if (isUnixSocket) {
|
||||
const parts = opts.path.split(':');
|
||||
|
||||
opts.socketPath = parts[0];
|
||||
opts.path = parts[1];
|
||||
}
|
||||
|
||||
let req = (websocket._req = get(opts));
|
||||
|
||||
if (opts.timeout) {
|
||||
req.on('timeout', () => {
|
||||
abortHandshake(websocket, req, 'Opening handshake has timed out');
|
||||
});
|
||||
}
|
||||
|
||||
req.on('error', (err) => {
|
||||
if (req === null || req.aborted) return;
|
||||
|
||||
req = websocket._req = null;
|
||||
websocket._readyState = WebSocket.CLOSING;
|
||||
websocket.emit('error', err);
|
||||
websocket.emitClose();
|
||||
});
|
||||
|
||||
req.on('response', (res) => {
|
||||
const location = res.headers.location;
|
||||
const statusCode = res.statusCode;
|
||||
|
||||
if (
|
||||
location &&
|
||||
opts.followRedirects &&
|
||||
statusCode >= 300 &&
|
||||
statusCode < 400
|
||||
) {
|
||||
if (++websocket._redirects > opts.maxRedirects) {
|
||||
abortHandshake(websocket, req, 'Maximum redirects exceeded');
|
||||
return;
|
||||
}
|
||||
|
||||
req.abort();
|
||||
|
||||
const addr = new URL(location, address);
|
||||
|
||||
initAsClient(websocket, addr, protocols, options);
|
||||
} else if (!websocket.emit('unexpected-response', req, res)) {
|
||||
abortHandshake(
|
||||
websocket,
|
||||
req,
|
||||
`Unexpected server response: ${res.statusCode}`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
req.on('upgrade', (res, socket, head) => {
|
||||
websocket.emit('upgrade', res);
|
||||
|
||||
//
|
||||
// The user may have closed the connection from a listener of the `upgrade`
|
||||
// event.
|
||||
//
|
||||
if (websocket.readyState !== WebSocket.CONNECTING) return;
|
||||
|
||||
req = websocket._req = null;
|
||||
|
||||
const digest = createHash('sha1')
|
||||
.update(key + GUID)
|
||||
.digest('base64');
|
||||
|
||||
if (res.headers['sec-websocket-accept'] !== digest) {
|
||||
abortHandshake(websocket, socket, 'Invalid Sec-WebSocket-Accept header');
|
||||
return;
|
||||
}
|
||||
|
||||
const serverProt = res.headers['sec-websocket-protocol'];
|
||||
const protList = (protocols || '').split(/, */);
|
||||
let protError;
|
||||
|
||||
if (!protocols && serverProt) {
|
||||
protError = 'Server sent a subprotocol but none was requested';
|
||||
} else if (protocols && !serverProt) {
|
||||
protError = 'Server sent no subprotocol';
|
||||
} else if (serverProt && !protList.includes(serverProt)) {
|
||||
protError = 'Server sent an invalid subprotocol';
|
||||
}
|
||||
|
||||
if (protError) {
|
||||
abortHandshake(websocket, socket, protError);
|
||||
return;
|
||||
}
|
||||
|
||||
if (serverProt) websocket._protocol = serverProt;
|
||||
|
||||
if (perMessageDeflate) {
|
||||
try {
|
||||
const extensions = parse(res.headers['sec-websocket-extensions']);
|
||||
|
||||
if (extensions[PerMessageDeflate.extensionName]) {
|
||||
perMessageDeflate.accept(extensions[PerMessageDeflate.extensionName]);
|
||||
websocket._extensions[
|
||||
PerMessageDeflate.extensionName
|
||||
] = perMessageDeflate;
|
||||
}
|
||||
} catch (err) {
|
||||
abortHandshake(
|
||||
websocket,
|
||||
socket,
|
||||
'Invalid Sec-WebSocket-Extensions header'
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
websocket.setSocket(socket, head, opts.maxPayload);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a `net.Socket` and initiate a connection.
|
||||
*
|
||||
* @param {Object} options Connection options
|
||||
* @return {net.Socket} The newly created socket used to start the connection
|
||||
* @private
|
||||
*/
|
||||
function netConnect(options) {
|
||||
options.path = options.socketPath;
|
||||
return net.connect(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a `tls.TLSSocket` and initiate a connection.
|
||||
*
|
||||
* @param {Object} options Connection options
|
||||
* @return {tls.TLSSocket} The newly created socket used to start the connection
|
||||
* @private
|
||||
*/
|
||||
function tlsConnect(options) {
|
||||
options.path = undefined;
|
||||
|
||||
if (!options.servername && options.servername !== '') {
|
||||
options.servername = net.isIP(options.host) ? '' : options.host;
|
||||
}
|
||||
|
||||
return tls.connect(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Abort the handshake and emit an error.
|
||||
*
|
||||
* @param {WebSocket} websocket The WebSocket instance
|
||||
* @param {(http.ClientRequest|net.Socket)} stream The request to abort or the
|
||||
* socket to destroy
|
||||
* @param {String} message The error message
|
||||
* @private
|
||||
*/
|
||||
function abortHandshake(websocket, stream, message) {
|
||||
websocket._readyState = WebSocket.CLOSING;
|
||||
|
||||
const err = new Error(message);
|
||||
Error.captureStackTrace(err, abortHandshake);
|
||||
|
||||
if (stream.setHeader) {
|
||||
stream.abort();
|
||||
stream.once('abort', websocket.emitClose.bind(websocket));
|
||||
websocket.emit('error', err);
|
||||
} else {
|
||||
stream.destroy(err);
|
||||
stream.once('error', websocket.emit.bind(websocket, 'error'));
|
||||
stream.once('close', websocket.emitClose.bind(websocket));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle cases where the `ping()`, `pong()`, or `send()` methods are called
|
||||
* when the `readyState` attribute is `CLOSING` or `CLOSED`.
|
||||
*
|
||||
* @param {WebSocket} websocket The WebSocket instance
|
||||
* @param {*} [data] The data to send
|
||||
* @param {Function} [cb] Callback
|
||||
* @private
|
||||
*/
|
||||
function sendAfterClose(websocket, data, cb) {
|
||||
if (data) {
|
||||
const length = toBuffer(data).length;
|
||||
|
||||
//
|
||||
// The `_bufferedAmount` property is used only when the peer is a client and
|
||||
// the opening handshake fails. Under these circumstances, in fact, the
|
||||
// `setSocket()` method is not called, so the `_socket` and `_sender`
|
||||
// properties are set to `null`.
|
||||
//
|
||||
if (websocket._socket) websocket._sender._bufferedBytes += length;
|
||||
else websocket._bufferedAmount += length;
|
||||
}
|
||||
|
||||
if (cb) {
|
||||
const err = new Error(
|
||||
`WebSocket is not open: readyState ${websocket.readyState} ` +
|
||||
`(${readyStates[websocket.readyState]})`
|
||||
);
|
||||
cb(err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The listener of the `Receiver` `'conclude'` event.
|
||||
*
|
||||
* @param {Number} code The status code
|
||||
* @param {String} reason The reason for closing
|
||||
* @private
|
||||
*/
|
||||
function receiverOnConclude(code, reason) {
|
||||
const websocket = this[kWebSocket];
|
||||
|
||||
websocket._socket.removeListener('data', socketOnData);
|
||||
websocket._socket.resume();
|
||||
|
||||
websocket._closeFrameReceived = true;
|
||||
websocket._closeMessage = reason;
|
||||
websocket._closeCode = code;
|
||||
|
||||
if (code === 1005) websocket.close();
|
||||
else websocket.close(code, reason);
|
||||
}
|
||||
|
||||
/**
|
||||
* The listener of the `Receiver` `'drain'` event.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
function receiverOnDrain() {
|
||||
this[kWebSocket]._socket.resume();
|
||||
}
|
||||
|
||||
/**
|
||||
* The listener of the `Receiver` `'error'` event.
|
||||
*
|
||||
* @param {(RangeError|Error)} err The emitted error
|
||||
* @private
|
||||
*/
|
||||
function receiverOnError(err) {
|
||||
const websocket = this[kWebSocket];
|
||||
|
||||
websocket._socket.removeListener('data', socketOnData);
|
||||
|
||||
websocket._readyState = WebSocket.CLOSING;
|
||||
websocket._closeCode = err[kStatusCode];
|
||||
websocket.emit('error', err);
|
||||
websocket._socket.destroy();
|
||||
}
|
||||
|
||||
/**
|
||||
* The listener of the `Receiver` `'finish'` event.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
function receiverOnFinish() {
|
||||
this[kWebSocket].emitClose();
|
||||
}
|
||||
|
||||
/**
|
||||
* The listener of the `Receiver` `'message'` event.
|
||||
*
|
||||
* @param {(String|Buffer|ArrayBuffer|Buffer[])} data The message
|
||||
* @private
|
||||
*/
|
||||
function receiverOnMessage(data) {
|
||||
this[kWebSocket].emit('message', data);
|
||||
}
|
||||
|
||||
/**
|
||||
* The listener of the `Receiver` `'ping'` event.
|
||||
*
|
||||
* @param {Buffer} data The data included in the ping frame
|
||||
* @private
|
||||
*/
|
||||
function receiverOnPing(data) {
|
||||
const websocket = this[kWebSocket];
|
||||
|
||||
websocket.pong(data, !websocket._isServer, NOOP);
|
||||
websocket.emit('ping', data);
|
||||
}
|
||||
|
||||
/**
|
||||
* The listener of the `Receiver` `'pong'` event.
|
||||
*
|
||||
* @param {Buffer} data The data included in the pong frame
|
||||
* @private
|
||||
*/
|
||||
function receiverOnPong(data) {
|
||||
this[kWebSocket].emit('pong', data);
|
||||
}
|
||||
|
||||
/**
|
||||
* The listener of the `net.Socket` `'close'` event.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
function socketOnClose() {
|
||||
const websocket = this[kWebSocket];
|
||||
|
||||
this.removeListener('close', socketOnClose);
|
||||
this.removeListener('end', socketOnEnd);
|
||||
|
||||
websocket._readyState = WebSocket.CLOSING;
|
||||
|
||||
//
|
||||
// The close frame might not have been received or the `'end'` event emitted,
|
||||
// for example, if the socket was destroyed due to an error. Ensure that the
|
||||
// `receiver` stream is closed after writing any remaining buffered data to
|
||||
// it. If the readable side of the socket is in flowing mode then there is no
|
||||
// buffered data as everything has been already written and `readable.read()`
|
||||
// will return `null`. If instead, the socket is paused, any possible buffered
|
||||
// data will be read as a single chunk and emitted synchronously in a single
|
||||
// `'data'` event.
|
||||
//
|
||||
websocket._socket.read();
|
||||
websocket._receiver.end();
|
||||
|
||||
this.removeListener('data', socketOnData);
|
||||
this[kWebSocket] = undefined;
|
||||
|
||||
clearTimeout(websocket._closeTimer);
|
||||
|
||||
if (
|
||||
websocket._receiver._writableState.finished ||
|
||||
websocket._receiver._writableState.errorEmitted
|
||||
) {
|
||||
websocket.emitClose();
|
||||
} else {
|
||||
websocket._receiver.on('error', receiverOnFinish);
|
||||
websocket._receiver.on('finish', receiverOnFinish);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The listener of the `net.Socket` `'data'` event.
|
||||
*
|
||||
* @param {Buffer} chunk A chunk of data
|
||||
* @private
|
||||
*/
|
||||
function socketOnData(chunk) {
|
||||
if (!this[kWebSocket]._receiver.write(chunk)) {
|
||||
this.pause();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The listener of the `net.Socket` `'end'` event.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
function socketOnEnd() {
|
||||
const websocket = this[kWebSocket];
|
||||
|
||||
websocket._readyState = WebSocket.CLOSING;
|
||||
websocket._receiver.end();
|
||||
this.end();
|
||||
}
|
||||
|
||||
/**
|
||||
* The listener of the `net.Socket` `'error'` event.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
function socketOnError() {
|
||||
const websocket = this[kWebSocket];
|
||||
|
||||
this.removeListener('error', socketOnError);
|
||||
this.on('error', NOOP);
|
||||
|
||||
if (websocket) {
|
||||
websocket._readyState = WebSocket.CLOSING;
|
||||
this.destroy();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,90 @@
|
|||
{
|
||||
"_from": "ws@^7.2.5",
|
||||
"_id": "ws@7.4.4",
|
||||
"_inBundle": false,
|
||||
"_integrity": "sha512-Qm8k8ojNQIMx7S+Zp8u/uHOx7Qazv3Yv4q68MiWWWOJhiwG5W3x7iqmRtJo8xxrciZUY4vRxUTJCKuRnF28ZZw==",
|
||||
"_location": "/ws",
|
||||
"_phantomChildren": {},
|
||||
"_requested": {
|
||||
"type": "range",
|
||||
"registry": true,
|
||||
"raw": "ws@^7.2.5",
|
||||
"name": "ws",
|
||||
"escapedName": "ws",
|
||||
"rawSpec": "^7.2.5",
|
||||
"saveSpec": null,
|
||||
"fetchSpec": "^7.2.5"
|
||||
},
|
||||
"_requiredBy": [
|
||||
"/"
|
||||
],
|
||||
"_resolved": "https://registry.npmjs.org/ws/-/ws-7.4.4.tgz",
|
||||
"_shasum": "383bc9742cb202292c9077ceab6f6047b17f2d59",
|
||||
"_spec": "ws@^7.2.5",
|
||||
"_where": "/home/shaba/RPM/git/vitastor/mon",
|
||||
"author": {
|
||||
"name": "Einar Otto Stangvik",
|
||||
"email": "einaros@gmail.com",
|
||||
"url": "http://2x.io"
|
||||
},
|
||||
"browser": "browser.js",
|
||||
"bugs": {
|
||||
"url": "https://github.com/websockets/ws/issues"
|
||||
},
|
||||
"bundleDependencies": false,
|
||||
"deprecated": false,
|
||||
"description": "Simple to use, blazing fast and thoroughly tested websocket client and server for Node.js",
|
||||
"devDependencies": {
|
||||
"benchmark": "^2.1.4",
|
||||
"bufferutil": "^4.0.1",
|
||||
"eslint": "^7.2.0",
|
||||
"eslint-config-prettier": "^8.1.0",
|
||||
"eslint-plugin-prettier": "^3.0.1",
|
||||
"mocha": "^7.0.0",
|
||||
"nyc": "^15.0.0",
|
||||
"prettier": "^2.0.5",
|
||||
"utf-8-validate": "^5.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.3.0"
|
||||
},
|
||||
"files": [
|
||||
"browser.js",
|
||||
"index.js",
|
||||
"lib/*.js"
|
||||
],
|
||||
"homepage": "https://github.com/websockets/ws",
|
||||
"keywords": [
|
||||
"HyBi",
|
||||
"Push",
|
||||
"RFC-6455",
|
||||
"WebSocket",
|
||||
"WebSockets",
|
||||
"real-time"
|
||||
],
|
||||
"license": "MIT",
|
||||
"main": "index.js",
|
||||
"name": "ws",
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": "^5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/websockets/ws.git"
|
||||
},
|
||||
"scripts": {
|
||||
"integration": "mocha --throw-deprecation test/*.integration.js",
|
||||
"lint": "eslint --ignore-path .gitignore . && prettier --check --ignore-path .gitignore \"**/*.{json,md,yaml,yml}\"",
|
||||
"test": "nyc --reporter=lcov --reporter=text mocha --throw-deprecation test/*.test.js"
|
||||
},
|
||||
"version": "7.4.4"
|
||||
}
|
|
@ -1,8 +1,8 @@
|
|||
commit c6e1958a1b4974828e8e5852beb252ce6594e670
|
||||
From commit c6e1958a1b4974828e8e5852beb252ce6594e670
|
||||
From: Vitaliy Filippov <vitalif@yourcmc.ru>
|
||||
Author: Vitaliy Filippov <vitalif@yourcmc.ru>
|
||||
Date: Mon Jun 28 01:20:19 2021 +0300
|
||||
|
||||
Add Vitastor support
|
||||
Subject: Add Vitastor support
|
||||
|
||||
diff --git a/docs/schemas/domaincommon.rng b/docs/schemas/domaincommon.rng
|
||||
index 5ea14b6..a9df168 100644
|
||||
|
|
|
@ -1,6 +1,3 @@
|
|||
cmake_minimum_required(VERSION 2.8)
|
||||
|
||||
project(vitastor)
|
||||
|
||||
include(GNUInstallDirs)
|
||||
|
||||
|
@ -22,19 +19,10 @@ if (${WITH_ASAN})
|
|||
add_link_options(-fsanitize=address -fno-omit-frame-pointer)
|
||||
endif (${WITH_ASAN})
|
||||
|
||||
set(CMAKE_BUILD_TYPE RelWithDebInfo)
|
||||
string(REGEX REPLACE "([\\/\\-]O)[12]?" "\\13" CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE}")
|
||||
string(REGEX REPLACE "([\\/\\-]O)[12]?" "\\13" CMAKE_CXX_FLAGS_MINSIZEREL "${CMAKE_CXX_FLAGS_MINSIZEREL}")
|
||||
string(REGEX REPLACE "([\\/\\-]O)[12]?" "\\13" CMAKE_CXX_FLAGS_RELWITHDEBINFO "${CMAKE_CXX_FLAGS_RELWITHDEBINFO}")
|
||||
string(REGEX REPLACE "([\\/\\-]D) *NDEBUG" "" CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE}")
|
||||
string(REGEX REPLACE "([\\/\\-]D) *NDEBUG" "" CMAKE_CXX_FLAGS_MINSIZEREL "${CMAKE_CXX_FLAGS_MINSIZEREL}")
|
||||
string(REGEX REPLACE "([\\/\\-]D) *NDEBUG" "" CMAKE_CXX_FLAGS_RELWITHDEBINFO "${CMAKE_CXX_FLAGS_RELWITHDEBINFO}")
|
||||
string(REGEX REPLACE "([\\/\\-]O)[12]?" "\\13" CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE}")
|
||||
string(REGEX REPLACE "([\\/\\-]O)[12]?" "\\13" CMAKE_C_FLAGS_MINSIZEREL "${CMAKE_C_FLAGS_MINSIZEREL}")
|
||||
string(REGEX REPLACE "([\\/\\-]O)[12]?" "\\13" CMAKE_C_FLAGS_RELWITHDEBINFO "${CMAKE_C_FLAGS_RELWITHDEBINFO}")
|
||||
string(REGEX REPLACE "([\\/\\-]D) *NDEBUG" "" CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE}")
|
||||
string(REGEX REPLACE "([\\/\\-]D) *NDEBUG" "" CMAKE_C_FLAGS_MINSIZEREL "${CMAKE_C_FLAGS_MINSIZEREL}")
|
||||
string(REGEX REPLACE "([\\/\\-]D) *NDEBUG" "" CMAKE_C_FLAGS_RELWITHDEBINFO "${CMAKE_C_FLAGS_RELWITHDEBINFO}")
|
||||
# If the build type isn't specified, set to Relwithdebinfo as default.
|
||||
if (NOT CMAKE_BUILD_TYPE)
|
||||
set(CMAKE_BUILD_TYPE "RelWithDebInfo")
|
||||
endif()
|
||||
|
||||
find_package(PkgConfig)
|
||||
pkg_check_modules(LIBURING REQUIRED liburing)
|
||||
|
|
Loading…
Reference in New Issue