├── .eslintignore ├── .eslintrc ├── .github ├── FUNDING.yml └── workflows │ └── nodejs.yml ├── .gitignore ├── .npmignore ├── .nvmrc ├── .prettierrc ├── AUTHORS ├── CHANGELOG.md ├── LICENSE ├── README.md ├── examples ├── .eslintrc ├── .gitignore ├── echo │ ├── README.md │ ├── index.html │ ├── package.json │ └── server.js ├── express │ ├── index.html │ ├── package.json │ └── server.js ├── hapi │ ├── html │ │ └── index.html │ ├── package.json │ └── server.js ├── haproxy.cfg ├── koa │ ├── index.html │ ├── package.json │ └── server.js └── multiplex │ ├── README.md │ ├── index.html │ ├── package.json │ └── server.js ├── index.js ├── lib ├── handlers.js ├── iframe.js ├── info.js ├── listener.js ├── middleware.js ├── server.js ├── session.js ├── sockjs-connection.js ├── transport │ ├── base-receiver.js │ ├── eventsource.js │ ├── htmlfile.js │ ├── jsonp-polling.js │ ├── list.js │ ├── response-receiver.js │ ├── transport.js │ ├── websocket-raw.js │ ├── websocket.js │ ├── xhr-polling.js │ ├── xhr-streaming.js │ └── xhr.js ├── utils.js └── webjs.js ├── package-lock.json ├── package.json ├── scripts └── test.sh └── tests ├── .eslintrc └── test_server ├── README.md ├── config.js ├── server.js └── sockjs_app.js /.eslintignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sockjs/sockjs-node/a43ed3bd25c9df16e18c681a678d5bc47b0758a1/.eslintignore -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es6": true, 4 | "node": true 5 | }, 6 | "extends": ["eslint:recommended", "plugin:prettier/recommended"] 7 | } 8 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | tidelift: npm/sockjs 4 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: Node CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | strategy: 11 | matrix: 12 | node-version: [18.x, 20.x, 22.x] 13 | 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: actions/setup-python@v4 17 | with: 18 | python-version: '3.x' 19 | architecture: 'x64' 20 | - name: Install virtualenv 21 | run: | 22 | pip install virtualenv 23 | - name: Use Node.js ${{ matrix.node-version }} 24 | uses: actions/setup-node@v4 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | - name: npm install, build, and test 28 | run: | 29 | npm ci 30 | ./scripts/test.sh 31 | env: 32 | CI: true 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .pidfile.pid 2 | node_modules 3 | *~ 4 | sockjs-protocol 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .gitignore 2 | .eslintrc 3 | .eslintignore 4 | .nvmrc 5 | node_modules 6 | examples 7 | *~ 8 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | lts/erbium 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "singleQuote": true, 4 | "trailingComma": "none" 5 | } 6 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | # This is the list of sockjs authors for copyright purposes. 2 | # 3 | # This does not necessarily list everyone who has contributed code, since in 4 | # some cases, their employer may be the copyright holder. To see the full list 5 | # of contributors, see the revision history in source control. 6 | Bryce Kahle 7 | Marek Majkowski 8 | VMWare 9 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Unreleased 2 | 3 | ## **BREAKING CHANGES** 4 | * `installHandlers(server, options)` renamed to `attach(server)` and only takes a single argument. 5 | This means you cannot use the same SockJS server installed at multiple prefixes. 6 | In practice this was confusing and not common. 7 | * `websocket` option is deprecated, but still respected. Please use the new `transports` option. 8 | * Node.js `>= 6.5.0` is required. 9 | 10 | ## Other Fixes/Changes 11 | * Convert from coffeescript to ES6. 12 | * Update minimum Node.js version to 6.X. 13 | * Update SockJSConnection implementation to be compatible with latest Node.js streams. 14 | * SockJSConnection properties `readable` and `writable` have been removed. These are used internally by Node.js streams. 15 | * Remove `console.log` logging by default. 16 | * Remove usage of exceptions for flow control. 17 | * Add `debug` logs for easier troubleshooting. 18 | * Added `transports` option to allow selection of specific transports. 19 | * Added `detach(server)` function to remove SockJS from a HTTP server instance. 20 | * Update dependencies. 21 | * Examples have been updated to use latest versions of libraries. 22 | 23 | 24 | 0.3.19 25 | ====== 26 | 27 | * Update `node-uuid` version #224 28 | * Add `disable_cors` option to prevent CORS headers from being added to responses #218 29 | * Add `dnt` header to whitelist #212 30 | * Add `x-forwarded-host` and `x-forwarded-port` headers to whitelist #208 31 | * Update `sockjs_url` default to latest 1.x target #223 32 | * Updated hapi.js example #216 33 | 34 | 0.3.18 35 | ====== 36 | 37 | * Change to using `res.statusCode` instead of manual parsing of `res._header` #213 38 | * Update sockjs-protocol filename in README #203 39 | 40 | 0.3.17 41 | ====== 42 | 43 | * Fix usage of undefined `session` in `heartbeat_timeout` #179 44 | 45 | 0.3.16 46 | ====== 47 | 48 | * Fix CORS response for null origin #177 49 | * Add websocket ping-pong and close if no response #129, #162, #169 50 | * Update sockjs-client version in examples #182 51 | * Add koa example #180 52 | * Disable raw websocket endpoint when websocket = false #183 53 | * Upgrade to faye-websocket 0.10.0 and use proper close code 54 | * When connection is aborted, don't delay the teardown 55 | * Forward additional headers #188 56 | * Add `no-transform` to Cache-Control headers #189 57 | * Update documentation about heartbeats #192 58 | 59 | 60 | 0.3.15 61 | ====== 62 | 63 | * Remove usage of naked '@' function params to be compatible with coffeescript 1.9.0 #175 64 | 65 | 0.3.14 66 | ====== 67 | 68 | * Re-publish to npm because of build issue in 0.3.13 69 | 70 | 0.3.13 71 | ====== 72 | 73 | * Upgrade faye-websocket to 0.9.3 to fix #171 74 | 75 | 0.3.12 76 | ====== 77 | 78 | * Allow Faye socket constructor options to be passed with 79 | faye_server_options option to createServer 80 | * Fix websocket bad json tests 81 | * Upgrade Faye to allow 0.9.* 82 | 83 | 0.3.11 84 | ====== 85 | 86 | * #133 - only delay disconnect on non-websocket transports 87 | * Upgrade Faye to 0.8.0 88 | 89 | 0.3.10 90 | ====== 91 | 92 | * #168 - Add CORS headers for eventsource 93 | * #158 - schedule heartbeat timer even if send_buffer is not empty 94 | * #96 - remove rbytes dependency 95 | * #83 - update documentation for prefix 96 | * #163 - add protection to JSON for SWF exploit 97 | * #104 - delete unused parameters in code 98 | * #106 - update CDN urls 99 | * #79 - Don't remove stream listeners until after end so 'close' event is heard 100 | * Get rid of need for _sockjs_onload global variable 101 | * Use Faye for websocket request validation 102 | * Upgrade Faye to 0.7.3 103 | * Upgrade node-uuid to 1.4.1 104 | 105 | 0.3.9 106 | ===== 107 | 108 | * #130 - Set Vary: Origin on CORS requests 109 | * Upgrade Faye to 0.7.2 from 0.7.0 110 | 111 | 112 | 0.3.8 113 | ===== 114 | 115 | * #118 - Allow servers to specify a base URL in /info 116 | * #131 - Don't look up session id undefined 117 | * #124 - Small grammar updates for ReadMe 118 | * Upgrade Faye to 0.7.0 from 0.4.0 119 | 120 | 0.3.7 121 | ===== 122 | 123 | * Expose "protocol" on raw websocket connection instance, correctly 124 | 125 | 0.3.6 126 | ===== 127 | 128 | * When the server closes a connection, make sure the send buffer still 129 | gets flushed. 130 | * Expose "protocol" on raw websocket connection instance 131 | * #105, #109, #113 - expose 'host', 'user-agent', and 'accept-language' 132 | headers 133 | * Serve SockJS over https CDN by default 134 | * Upgrade Faye to 0.4.4 from 0.4.0 135 | 136 | 0.3.5 137 | ===== 138 | 139 | * #103 - connection.protocol might have been empty on some rare 140 | occasions. 141 | * #99 - faye-websocket was leaking sockets in "closed" state 142 | when dealing with rfc websockets 143 | 144 | 145 | 0.3.4 146 | ===== 147 | 148 | * #73 - apparently 'package' is a reserved keyword (use 'pkg' instead) 149 | * #93 - Coffescript can leak a variable when the same name is used 150 | in catch statement. Let's always use 'x' as the variable in catch. 151 | * #76 - decorateConnection could throw an error if remote connection 152 | was closed before setup was complete 153 | * #90 - Fix "TypeError: 'addListener'" exception (via @pl). 154 | * remove 'optionalDependencies' section from package.json, 155 | 'rbytes' was always optional. 156 | * #91 - Fix rare null exception. 157 | 158 | 159 | 0.3.3 160 | ===== 161 | 162 | * sockjs/sockjs-protocol#56, #88 Fix for iOS 6 caching POSTs 163 | 164 | 165 | 0.3.1 166 | ===== 167 | 168 | * #58 - websocket transport emitted an array instead of a string 169 | during onmessage event. 170 | * Running under node.js 0.7 caused infinite recursion (Stephan Kochen) 171 | * #59 - restrict characters allowed in callback parameter 172 | * Updated readme - rbytes package is optional 173 | * Updated readme WRT deployments on heroku 174 | * Add minimalistic license block to every source file. 175 | 176 | 177 | 0.3.0 178 | ===== 179 | 180 | * Sending JSESSIONID cookie is now *disabled* by default. 181 | * sockjs/sockjs-protocol#46 - introduce new service 182 | required for protocol tests "/cookie_needed_echo" 183 | * Initial work towards better integration with 184 | "connect" (Stephan Kochen). See discusion: 185 | https://github.com/senchalabs/connect/pull/506 186 | * More documentation about the Cookie and Origin headers. 187 | * #51 - expose "readyState" on connection instance 188 | * #53 - expose "protocol" on connection instance 189 | * #52 - Some protocols may not emit 'close' event with IE. 190 | * sockjs/sockjs-client#49 - Support 'null' origin - aka: allow SockJS 191 | client to be served from file:// paths. 192 | 193 | 194 | 0.2.1 195 | ===== 196 | 197 | * Bumped "faye-websocket" dependency to 0.4. Updated 198 | code to take advantage of introduced changes. 199 | * Pinned "node-static" and bumped "node-uuid" dependencies. 200 | * Removed "Origin" header list of headers exposed to the user. 201 | This header is not really meaningful in sockjs context. 202 | * Header "Access-Control-Allow-Methods" was misspelled. 203 | 204 | 205 | 0.2.0 206 | ===== 207 | 208 | * #36, #3 - Replace a custom WebSocket server implementation 209 | with faye-websocket-node. 210 | * Multiple changes to support SockJS-protocol 0.2. 211 | * The session is now closed on network errors immediately 212 | (instead of waiting 5 seconds) 213 | * Raw websocket interface available - to make it easier 214 | to write command line SockJS clients. 215 | * Support '/info' url. 216 | * The test server got moved from SockJS-client to SockJS-node. 217 | * Dropped deprecated Server API (use createServer method instead). 218 | * Option `websocket` is now used instead of `disabled_transports`. 219 | 220 | 221 | 0.1.2 222 | ===== 223 | 224 | * #27 - Allow all unicode characters to be send over SockJS. 225 | * #14 - Make it possible to customize JSESSIONID cookie logic. 226 | 227 | 228 | 0.1.1 229 | ===== 230 | 231 | * #32 Expose various request headers on connection. 232 | * #30 Expose request path on connection. 233 | 234 | 235 | 0.1.0 236 | ===== 237 | 238 | * The API changed, there is now an idiomatic API, modelled on node.js 239 | Stream API. The old API is deprecated and there is a dummy wrapper 240 | that emulates it. Please do upgrade to the new idiomatic API. 241 | * #22 Initial support for hybi13 (stephank) 242 | * New options accepted by the `Server` constructor: `log`, 243 | `heartbeat_delay` and `disconnect_delay`. 244 | * SockJS is now not able to send rich data structures - all data 245 | passed to `write` is converted to a string. 246 | * #23 `Connection.remoteAddress` property introduced (Stéphan Kochen) 247 | * Loads of small changes in order to adhere to protocol spec. 248 | 249 | 250 | 0.0.5 251 | ===== 252 | 253 | * #20: `npm submodule sockjs` didn't work due to outdated github 254 | path. 255 | 256 | 257 | 0.0.4 258 | ===== 259 | 260 | * Support for htmlfile transport, used by IE in a deployment 261 | dependent on cookies. 262 | * Added /chunking_test API, used to detect support for HTTP chunking 263 | on client side. 264 | * Unified code logic for all the chunking transports - the same code 265 | is reused for polling versions. 266 | * All the chunking transports are closed by the server after 128K was 267 | send, in order to force client to GC and reconnect. 268 | * Don't distribute source coffeescript with npm. 269 | * Minor fixes in websocket code. 270 | * Dropped jQuery dependency. 271 | * Unicode encoding could been garbled during XHR upload. 272 | * Other minor fixes. 273 | 274 | 275 | 0.0.3 276 | ====== 277 | 278 | * EventSource transport didn't emit 'close' event. 279 | 280 | 281 | 0.0.2 282 | ===== 283 | 284 | * By default set JSESSIONID cookie, useful for load balancing. 285 | 286 | 287 | 0.0.1 288 | ===== 289 | 290 | * Initial release. 291 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2011-2019 The sockjs Authors. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SockJS-node 2 | 3 | [![npm version](https://img.shields.io/npm/v/sockjs.svg?style=flat-square)](https://www.npmjs.com/package/sockjs)[![Dependencies](https://img.shields.io/david/sockjs/sockjs-node.svg?style=flat-square)](https://david-dm.org/sockjs/sockjs-node) 4 | 5 | # SockJS for enterprise 6 | 7 | Available as part of the Tidelift Subscription. 8 | 9 | The maintainers of SockJS and thousands of other packages are working with Tidelift to deliver commercial support and maintenance for the open source dependencies you use to build your applications. Save time, reduce risk, and improve code health, while paying the maintainers of the exact dependencies you use. [Learn more.](https://tidelift.com/subscription/pkg/npm-sockjs?utm_source=npm-sockjs&utm_medium=referral&utm_campaign=enterprise&utm_term=repo) 10 | 11 | # SockJS family 12 | 13 | * [SockJS-client](https://github.com/sockjs/sockjs-client) JavaScript client library 14 | * [SockJS-node](https://github.com/sockjs/sockjs-node) Node.js server 15 | * [SockJS-erlang](https://github.com/sockjs/sockjs-erlang) Erlang server 16 | * [SockJS-tornado](https://github.com/MrJoes/sockjs-tornado) Python/Tornado server 17 | * [vert.x](https://github.com/eclipse/vert.x) Java/vert.x server 18 | 19 | Work in progress: 20 | 21 | * [SockJS-ruby](https://github.com/nyarly/sockjs-ruby) 22 | * [SockJS-netty](https://github.com/cgbystrom/sockjs-netty) 23 | * [SockJS-gevent](https://github.com/sdiehl/sockjs-gevent) ([and a fork](https://github.com/njoyce/sockjs-gevent)) 24 | * [pyramid-SockJS](https://github.com/fafhrd91/pyramid_sockjs) 25 | * [wildcloud-websockets](https://github.com/wildcloud/wildcloud-websockets) 26 | * [SockJS-cyclone](https://github.com/flaviogrossi/sockjs-cyclone) 27 | * [SockJS-twisted](https://github.com/Fugiman/sockjs-twisted/) 28 | * [wai-SockJS](https://github.com/Palmik/wai-sockjs) 29 | * [SockJS-perl](https://github.com/vti/sockjs-perl) 30 | * [SockJS-go](https://github.com/igm/sockjs-go/) 31 | * [actix/sockjs](https://github.com/actix/sockjs) for Rust 32 | 33 | ⚠️️ **ATTENTION** This is pre-release documentation. The documentation for the 34 | latest stable release is at: https://github.com/sockjs/sockjs-node/tree/v0.3.19 ️⚠️ 35 | 36 | # What is SockJS? 37 | 38 | SockJS is a JavaScript library (for browsers) that provides a WebSocket-like 39 | object. SockJS gives you a coherent, cross-browser, Javascript API 40 | which creates a low latency, full duplex, cross-domain communication 41 | channel between the browser and the web server, with WebSockets or without. 42 | This necessitates the use of a server, which this is one version of, for Node.js. 43 | 44 | 45 | # SockJS-node server 46 | 47 | SockJS-node is a Node.js server side counterpart of 48 | [SockJS-client browser library](https://github.com/sockjs/sockjs-client). 49 | 50 | To install `sockjs-node` run: 51 | 52 | npm install sockjs 53 | 54 | A simplified echo SockJS server could look more or less like: 55 | 56 | ```javascript 57 | const http = require('http'); 58 | const sockjs = require('sockjs'); 59 | 60 | const echo = sockjs.createServer({ prefix:'/echo' }); 61 | echo.on('connection', function(conn) { 62 | conn.on('data', function(message) { 63 | conn.write(message); 64 | }); 65 | conn.on('close', function() {}); 66 | }); 67 | 68 | const server = http.createServer(); 69 | echo.attach(server); 70 | server.listen(9999, '0.0.0.0'); 71 | ``` 72 | 73 | (Take look at 74 | [examples](https://github.com/sockjs/sockjs-node/tree/main/examples/echo) 75 | directory for a complete version.) 76 | 77 | Subscribe to 78 | [SockJS mailing list](https://groups.google.com/forum/#!forum/sockjs) for 79 | discussions and support. 80 | 81 | 82 | # SockJS-node API 83 | 84 | The API design is based on common Node APIs like the 85 | [Streams API](https://nodejs.org/api/stream.html) or the 86 | [Http.Server API](https://nodejs.org/api/http.html#http_class_http_server). 87 | 88 | ## Server class 89 | 90 | SockJS module is generating a `Server` class, similar to 91 | [Node.js http.createServer](https://nodejs.org/api/http.html#http_http_createserver_requestlistener) 92 | module. 93 | 94 | ```javascript 95 | const sockjs_server = sockjs.createServer(options); 96 | ``` 97 | 98 | Where `options` is a hash which can contain: 99 | 100 |
101 |
sockjs_url (string, required)
102 |
Transports which don't support cross-domain communication natively 103 | ('eventsource' to name one) use an iframe trick. A simple page is 104 | served from the SockJS server (using its foreign domain) and is 105 | placed in an invisible iframe. Code run from this iframe doesn't 106 | need to worry about cross-domain issues, as it's being run from 107 | domain local to the SockJS server. This iframe also does need to 108 | load SockJS javascript client library, and this option lets you specify 109 | its url (if you're unsure, point it to 110 | 111 | the latest minified SockJS client release, this is the default). 112 | You must explicitly specify this url on the server side for security 113 | reasons - we don't want the possibility of running any foreign 114 | javascript within the SockJS domain (aka cross site scripting attack). 115 | Also, sockjs javascript library is probably already cached by the 116 | browser - it makes sense to reuse the sockjs url you're using in 117 | normally.
118 | 119 |
prefix (string regex)
120 |
A url prefix for the server. All http requests which paths begins 121 | with selected prefix will be handled by SockJS. All other requests 122 | will be passed through, to previously registered handlers.
123 | 124 |
response_limit (integer)
125 |
Most streaming transports save responses on the client side and 126 | don't free memory used by delivered messages. Such transports need 127 | to be garbage-collected once in a while. `response_limit` sets 128 | a minimum number of bytes that can be send over a single http streaming 129 | request before it will be closed. After that client needs to open 130 | new request. Setting this value to one effectively disables 131 | streaming and will make streaming transports to behave like polling 132 | transports. The default value is 128K.
133 | 134 |
transports (Array of strings)
135 |
List of transports to enable. Select from `eventsource`, `htmlfile`, 136 | `jsonp-polling`, `websocket`, `websocket-raw`, `xhr-polling`, 137 | and `xhr-streaming`.
138 | 139 |
jsessionid (boolean or function)
140 |
Some hosting providers enable sticky sessions only to requests that 141 | have JSESSIONID cookie set. This setting controls if the server should 142 | set this cookie to a dummy value. By default setting JSESSIONID cookie 143 | is disabled. More sophisticated behaviour can be achieved by supplying 144 | a function.
145 | 146 |
log (function(severity, message))
147 |
It's quite useful, especially for debugging, to see some messages 148 | printed by a SockJS-node library. This is done using this `log` 149 | function, which is by default set to nothing. If this 150 | behaviour annoys you for some reason, override `log` setting with a 151 | custom handler. The following `severities` are used: `debug` 152 | (miscellaneous logs), `info` (requests logs), `error` (serious 153 | errors, consider filing an issue).
154 | 155 |
heartbeat_delay (milliseconds)
156 |
In order to keep proxies and load balancers from closing long 157 | running http requests we need to pretend that the connection is 158 | active and send a heartbeat packet once in a while. This setting 159 | controls how often this is done. By default a heartbeat packet is 160 | sent every 25 seconds.
161 | 162 |
disconnect_delay (milliseconds)
163 |
The server sends a `close` event when a client receiving 164 | connection have not been seen for a while. This delay is configured 165 | by this setting. By default the `close` event will be emitted when a 166 | receiving connection wasn't seen for 5 seconds.
167 | 168 |
disable_cors (boolean)
169 |
Enabling this option will prevent 170 | CORS 171 | headers from being included in the HTTP response. Can be used when the 172 | sockjs client is known to be connecting from the same origin as the 173 | sockjs server. This also disables the iframe HTML endpoint.
174 |
175 | 176 | 177 | ## Server instance 178 | 179 | Once you have create `Server` instance you can hook it to the 180 | [http.Server instance](https://nodejs.org/api/http.html#http_class_http_server). 181 | 182 | ```javascript 183 | var http_server = http.createServer(); 184 | sockjs_server.attach(http_server); 185 | http_server.listen(...); 186 | ``` 187 | 188 | `Server` instance is an 189 | [EventEmitter](https://nodejs.org/api/events.html#events_class_eventemitter), 190 | and emits following event: 191 | 192 |
193 |
Event: connection (connection)
194 |
A new connection has been successfully opened.
195 |
196 | 197 | All http requests that don't go under the path selected by `prefix` 198 | will remain unanswered and will be passed to previously registered 199 | handlers. You must install your custom http handlers before calling 200 | `attach`. You can remove the SockJS handler later with `detach`. 201 | 202 | ## Connection instance 203 | 204 | A `Connection` instance supports 205 | [Node Stream API](https://nodejs.org/api/stream.html) and 206 | has following methods and properties: 207 | 208 |
209 |
Property: remoteAddress (string)
210 |
Last known IP address of the client.
211 | 212 |
Property: remotePort (number)
213 |
Last known port number of the client.
214 | 215 |
Property: address (object)
216 |
Hash with 'address' and 'port' fields.
217 | 218 |
Property: headers (object)
219 |
Hash containing various headers copied from last receiving request 220 | on that connection. Exposed headers include: `origin`, `referer` 221 | and `x-forwarded-for` (and friends). We explicitly do not grant 222 | access to `cookie` header, as using it may easily lead to security 223 | issues (for details read the section "Authorisation").
224 | 225 |
Property: url (string)
226 |
Url 227 | property copied from last request.
228 | 229 |
Property: pathname (string)
230 |
`pathname` from parsed url, for convenience.
231 | 232 |
Property: prefix (string)
233 |
Prefix of the url on which the request was handled.
234 | 235 |
Property: protocol (string)
236 |
Protocol used by the connection. Keep in mind that some protocols 237 | are indistinguishable - for example "xhr-polling" and "xdr-polling".
238 | 239 |
Property: readyState (integer)
240 |
Current state of the connection: 241 | 0-connecting, 1-open, 2-closing, 3-closed.
242 | 243 |
write(message)
244 |
Sends a message over opened connection. A message must be a 245 | non-empty string. It's illegal to send a message after the connection was 246 | closed (either after 'close' or 'end' method or 'close' event).
247 | 248 |
close([code], [reason])
249 |
Asks the remote client to disconnect. 'code' and 'reason' 250 | parameters are optional and can be used to share the reason of 251 | disconnection.
252 | 253 |
end()
254 |
Asks the remote client to disconnect with default 'code' and 255 | 'reason' values.
256 | 257 |
258 | 259 | A `Connection` instance emits the following events: 260 | 261 |
262 |
Event: data (message)
263 |
A message arrived on the connection. Message is a unicode 264 | string.
265 | 266 |
Event: close ()
267 |
Connection was closed. This event is triggered exactly once for 268 | every connection.
269 |
270 | 271 | For example: 272 | 273 | ```javascript 274 | sockjs_server.on('connection', function(conn) { 275 | console.log('connection' + conn); 276 | conn.on('close', function() { 277 | console.log('close ' + conn); 278 | }); 279 | conn.on('data', function(message) { 280 | console.log('message ' + conn, message); 281 | }); 282 | }); 283 | ``` 284 | 285 | ## Footnote 286 | 287 | A fully working echo server does need a bit more boilerplate (to 288 | handle requests unanswered by SockJS), see the 289 | [`echo` example](https://github.com/sockjs/sockjs-node/tree/main/examples/echo) 290 | for a complete code. 291 | 292 | # Examples 293 | 294 | If you want to see samples of running code, take a look at: 295 | 296 | * [./examples/echo](https://github.com/sockjs/sockjs-node/tree/main/examples/echo) 297 | directory, which contains a full example of a echo server. 298 | * [./tests/test_server](https://github.com/sockjs/sockjs-node/tree/main/tests/test_server) a standard SockJS test server. 299 | 300 | 301 | # Connecting to SockJS-node without the client 302 | 303 | Although the main point of SockJS it to enable browser-to-server 304 | connectivity, it is possible to connect to SockJS from an external 305 | application. Any SockJS server complying with 0.3 protocol does 306 | support a raw WebSocket url. The raw WebSocket url for the test server 307 | looks like: 308 | 309 | * ws://localhost:8081/echo/websocket 310 | 311 | You can connect any WebSocket RFC 6455 compliant WebSocket client to 312 | this url. This can be a command line client, external application, 313 | third party code or even a browser (though I don't know why you would 314 | want to do so). 315 | 316 | Note: This endpoint will *not send any heartbeat packets*. 317 | 318 | 319 | # Deployment and load balancing 320 | 321 | There are two issues that need to be considered when planning a 322 | non-trivial SockJS-node deployment: WebSocket-compatible load balancer 323 | and sticky sessions (aka session affinity). 324 | 325 | ## WebSocket compatible load balancer 326 | 327 | Often WebSockets don't play nicely with proxies and load balancers. 328 | Deploying a SockJS server behind Nginx or Apache could be painful. 329 | 330 | Fortunately recent versions of an excellent load balancer 331 | [HAProxy](http://haproxy.1wt.eu/) are able to proxy WebSocket 332 | connections. We propose to put HAProxy as a front line load balancer 333 | and use it to split SockJS traffic from normal HTTP data. Take a look 334 | at the sample 335 | [SockJS HAProxy configuration](https://github.com/sockjs/sockjs-node/blob/main/examples/haproxy.cfg). 336 | 337 | The config also shows how to use HAproxy balancing to split traffic 338 | between multiple Node.js servers. You can also do balancing using dns 339 | names. 340 | 341 | ## Sticky sessions 342 | 343 | If you plan deploying more than one SockJS server, you must make sure 344 | that all HTTP requests for a single session will hit the same server. 345 | SockJS has two mechanisms that can be useful to achieve that: 346 | 347 | * Urls are prefixed with server and session id numbers, like: 348 | `/resource///transport`. This is 349 | useful for load balancers that support prefix-based affinity 350 | (HAProxy does). 351 | * `JSESSIONID` cookie is being set by SockJS-node. Many load 352 | balancers turn on sticky sessions if that cookie is set. This 353 | technique is derived from Java applications, where sticky sessions 354 | are often necessary. HAProxy does support this method, as well as 355 | some hosting providers, for example CloudFoundry. In order to 356 | enable this method on the client side, please supply a 357 | `cookie:true` option to SockJS constructor. 358 | 359 | 360 | # Development and testing 361 | 362 | If you want to work on SockJS-node source code, you need to clone the 363 | git repo and follow these steps. First you need to install 364 | dependencies: 365 | 366 | cd sockjs-node 367 | npm install 368 | 369 | If compilation succeeds you may want to test if your changes pass all 370 | the tests. Currently, there are two separate test suites. 371 | 372 | ## SockJS-protocol Python tests 373 | 374 | To run it run something like: 375 | 376 | ./scripts/test.sh 377 | 378 | For details see 379 | [SockJS-protocol README](https://github.com/sockjs/sockjs-protocol#readme). 380 | 381 | ## SockJS-client Karma tests 382 | 383 | To run it run something like: 384 | 385 | cd sockjs-client 386 | npm run test:browser_local 387 | 388 | For details see 389 | [SockJS-client README](https://github.com/sockjs/sockjs-client#readme). 390 | 391 | # Various issues and design considerations 392 | 393 | ## Authorisation 394 | 395 | SockJS-node does not expose cookies to the application. This is done 396 | deliberately as using cookie-based authorisation with SockJS simply 397 | doesn't make sense and will lead to security issues. 398 | 399 | Cookies are a contract between a browser and an http server, and are 400 | identified by a domain name. If a browser has a cookie set for 401 | particular domain, it will pass it as a part of all http requests to 402 | the host. But to get various transports working, SockJS uses a middleman 403 | - an iframe hosted from target SockJS domain. That means the server 404 | will receive requests from the iframe, and not from the real 405 | domain. The domain of an iframe is the same as the SockJS domain. The 406 | problem is that any website can embed the iframe and communicate with 407 | it - and request establishing SockJS connection. Using cookies for 408 | authorisation in this scenario will result in granting full access to 409 | SockJS communication with your website from any website. This is a 410 | classic CSRF attack. 411 | 412 | Basically - cookies are not suited for SockJS model. If you want to 413 | authorise a session - provide a unique token on a page, send it as a 414 | first thing over SockJS connection and validate it on the server 415 | side. In essence, this is how cookies work. 416 | 417 | 418 | ## Deploying SockJS on Heroku 419 | 420 | Long polling is known to cause problems on Heroku, but 421 | [workaround for SockJS is available](https://github.com/sockjs/sockjs-node/issues/57#issuecomment-5242187). 422 | -------------------------------------------------------------------------------- /examples/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "no-console": 0 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /examples/.gitignore: -------------------------------------------------------------------------------- 1 | package-lock.json 2 | -------------------------------------------------------------------------------- /examples/echo/README.md: -------------------------------------------------------------------------------- 1 | SockJS-node Echo example 2 | ======================== 3 | 4 | To run this example, first install dependencies: 5 | 6 | npm install 7 | 8 | And run a server: 9 | 10 | node server.js 11 | 12 | 13 | That will spawn an http server at http://127.0.0.1:9999/ which will 14 | serve both html (served from the current directory) and also SockJS 15 | server (under the [/echo](http://127.0.0.1:9999/echo) path). 16 | -------------------------------------------------------------------------------- /examples/echo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 35 | 36 |

SockJS Echo example

37 | 38 |
39 |
40 |
41 |
42 | 43 | 71 | 72 | -------------------------------------------------------------------------------- /examples/echo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sockjs-echo", 3 | "version": "0.0.1", 4 | "dependencies": { 5 | "node-static": "^0.7.11", 6 | "sockjs": "^0.4.0" 7 | }, 8 | "private": true 9 | } 10 | -------------------------------------------------------------------------------- /examples/echo/server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const http = require('http'); 4 | const sockjs = require('sockjs'); 5 | const node_static = require('node-static'); 6 | 7 | // 1. Echo sockjs server 8 | const sockjs_opts = { 9 | prefix: '/echo' 10 | }; 11 | 12 | const sockjs_echo = sockjs.createServer(sockjs_opts); 13 | sockjs_echo.on('connection', function (conn) { 14 | conn.on('data', function (message) { 15 | conn.write(message); 16 | }); 17 | }); 18 | 19 | // 2. Static files server 20 | const static_directory = new node_static.Server(__dirname); 21 | 22 | // 3. Usual http stuff 23 | const server = http.createServer(); 24 | server.addListener('request', function (req, res) { 25 | static_directory.serve(req, res); 26 | }); 27 | server.addListener('upgrade', function (req, res) { 28 | res.end(); 29 | }); 30 | 31 | sockjs_echo.attach(server); 32 | 33 | console.log(' [*] Listening on 0.0.0.0:9999'); 34 | server.listen(9999, '0.0.0.0'); 35 | -------------------------------------------------------------------------------- /examples/express/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 35 | 36 |

SockJS Express example

37 | 38 |
39 |
40 |
41 |
42 | 43 | 71 | 72 | -------------------------------------------------------------------------------- /examples/express/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sockjs-express", 3 | "version": "0.0.1", 4 | "dependencies": { 5 | "express": "^4.16.3", 6 | "sockjs": "^0.4.0" 7 | }, 8 | "private": true 9 | } 10 | -------------------------------------------------------------------------------- /examples/express/server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const http = require('http'); 4 | const express = require('express'); 5 | const sockjs = require('sockjs'); 6 | 7 | const sockjs_opts = { 8 | prefix: '/echo' 9 | }; 10 | 11 | const sockjs_echo = sockjs.createServer(sockjs_opts); 12 | sockjs_echo.on('connection', (conn) => { 13 | conn.on('data', (msg) => conn.write(msg)); 14 | }); 15 | 16 | const app = express(); 17 | app.get('/', (req, res) => res.sendFile(__dirname + '/index.html')); 18 | 19 | const server = http.createServer(app); 20 | sockjs_echo.attach(server); 21 | 22 | server.listen(9999, '0.0.0.0', () => { 23 | console.log(' [*] Listening on 0.0.0.0:9999'); 24 | }); 25 | -------------------------------------------------------------------------------- /examples/hapi/html/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 35 | 36 |

SockJS Echo example

37 | 38 |
39 |
40 |
41 |
42 | 43 | 71 | 72 | -------------------------------------------------------------------------------- /examples/hapi/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sockjs-hapi", 3 | "version": "0.0.1", 4 | "dependencies": { 5 | "hapi": "^17.6.0", 6 | "inert": "^5.1.0", 7 | "sockjs": "^0.4.0" 8 | }, 9 | "private": true 10 | } 11 | -------------------------------------------------------------------------------- /examples/hapi/server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const sockjs = require('sockjs'); 4 | const Hapi = require('hapi'); 5 | const Inert = require('inert'); 6 | 7 | // 1. Echo sockjs server 8 | const sockjs_opts = { 9 | prefix: '/echo' 10 | }; 11 | 12 | const sockjs_echo = sockjs.createServer(sockjs_opts); 13 | sockjs_echo.on('connection', function (conn) { 14 | conn.on('data', function (message) { 15 | conn.write(message); 16 | }); 17 | }); 18 | 19 | // Create a server and set port (default host 0.0.0.0) 20 | const hapi_server = new Hapi.Server({ 21 | port: 9999 22 | }); 23 | 24 | hapi_server.register(Inert).then(() => { 25 | hapi_server.route({ 26 | method: 'GET', 27 | path: '/{path*}', 28 | handler: { 29 | file: './html/index.html' 30 | } 31 | }); 32 | 33 | //hapi_server.listener is the http listener hapi uses 34 | sockjs_echo.attach(hapi_server.listener); 35 | hapi_server.start().then(() => console.log(' [*] Listening on 0.0.0.0:9999')); 36 | }); 37 | -------------------------------------------------------------------------------- /examples/haproxy.cfg: -------------------------------------------------------------------------------- 1 | # Requires recent Haproxy to work with websockets (for example 1.4.16). 2 | defaults 3 | mode http 4 | # Set timeouts to your needs 5 | timeout client 5s 6 | timeout connect 5s 7 | timeout server 5s 8 | 9 | frontend all 0.0.0.0:8888 10 | mode http 11 | timeout client 120s 12 | 13 | option forwardfor 14 | # Fake connection:close, required in this setup. 15 | option http-server-close 16 | option http-pretend-keepalive 17 | 18 | acl is_sockjs path_beg /echo /broadcast /close 19 | acl is_stats path_beg /stats 20 | 21 | use_backend sockjs if is_sockjs 22 | use_backend stats if is_stats 23 | default_backend static 24 | 25 | 26 | backend sockjs 27 | # Load-balance according to hash created from first two 28 | # directories in url path. For example requests going to /1/ 29 | # should be handled by single server (assuming resource prefix is 30 | # one-level deep, like "/echo"). 31 | balance uri depth 2 32 | timeout server 120s 33 | server srv_sockjs1 127.0.0.1:9999 34 | # server srv_sockjs2 127.0.0.1:9998 35 | 36 | backend static 37 | balance roundrobin 38 | server srv_static 127.0.0.1:8000 39 | 40 | backend stats 41 | stats uri /stats 42 | stats enable 43 | -------------------------------------------------------------------------------- /examples/koa/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 35 | 36 |

SockJS Express example

37 | 38 |
39 |
40 |
41 |
42 | 43 | 71 | 72 | -------------------------------------------------------------------------------- /examples/koa/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sockjs-koa", 3 | "version": "0.0.1", 4 | "dependencies": { 5 | "koa": "^2.5.3", 6 | "sockjs": "^0.4.0" 7 | }, 8 | "private": true 9 | } 10 | -------------------------------------------------------------------------------- /examples/koa/server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Koa = require('koa'); 4 | const sockjs = require('sockjs'); 5 | const http = require('http'); 6 | const fs = require('fs'); 7 | const path = require('path'); 8 | 9 | // 1. Echo sockjs server 10 | const sockjs_opts = { 11 | prefix: '/echo' 12 | }; 13 | const sockjs_echo = sockjs.createServer(sockjs_opts); 14 | sockjs_echo.on('connection', function (conn) { 15 | conn.on('data', function (message) { 16 | conn.write(message); 17 | }); 18 | }); 19 | 20 | // 2. koa server 21 | const app = new Koa(); 22 | 23 | app.use(function (ctx, next) { 24 | return next().then(() => { 25 | const filePath = __dirname + '/index.html'; 26 | ctx.response.type = path.extname(filePath); 27 | ctx.response.body = fs.createReadStream(filePath); 28 | }); 29 | }); 30 | 31 | const server = http.createServer(app.callback()); 32 | sockjs_echo.attach(server); 33 | 34 | server.listen(9999, '0.0.0.0'); 35 | console.log(' [*] Listening on 0.0.0.0:9999'); 36 | -------------------------------------------------------------------------------- /examples/multiplex/README.md: -------------------------------------------------------------------------------- 1 | WebSocket-multiplex SockJS example 2 | ================================== 3 | 4 | This example is a copy of example from 5 | [websocket-multiplex](https://github.com/sockjs/websocket-multiplex/) 6 | project: 7 | 8 | * https://github.com/sockjs/websocket-multiplex/ 9 | 10 | 11 | To run this example, first install dependencies: 12 | 13 | npm install 14 | 15 | And run a server: 16 | 17 | node server.js 18 | 19 | 20 | That will spawn an http server at http://127.0.0.1:9999/ which will 21 | serve both html (served from the current directory) and also SockJS 22 | service (under the [/multiplex](http://127.0.0.1:9999/multiplex) 23 | path). 24 | 25 | With that set up, WebSocket-multiplex is able to push three virtual 26 | connections over a single SockJS connection. See the code for details. 27 | -------------------------------------------------------------------------------- /examples/multiplex/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 36 | 37 |

SockJS Multiplex example

38 | 39 |
40 |
41 |
42 |
43 | 44 |
45 |
46 |
47 |
48 | 49 |
50 |
51 |
52 |
53 | 54 | 96 | 97 | -------------------------------------------------------------------------------- /examples/multiplex/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sockjs-multiplex", 3 | "version": "0.0.1", 4 | "dependencies": { 5 | "express": "^4.16.3", 6 | "sockjs": "^0.4.0", 7 | "websocket-multiplex": "^0.1.0" 8 | }, 9 | "private": true 10 | } 11 | -------------------------------------------------------------------------------- /examples/multiplex/server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const http = require('http'); 4 | const express = require('express'); 5 | const sockjs = require('sockjs'); 6 | const websocket_multiplex = require('websocket-multiplex'); 7 | 8 | // 1. Setup SockJS server 9 | const sockjs_opts = { 10 | prefix: '/multiplex' 11 | }; 12 | const service = sockjs.createServer(sockjs_opts); 13 | 14 | // 2. Setup multiplexing 15 | const multiplexer = new websocket_multiplex.MultiplexServer(service); 16 | 17 | const ann = multiplexer.registerChannel('ann'); 18 | ann.on('connection', function (conn) { 19 | conn.write('Ann says hi!'); 20 | conn.on('data', function (data) { 21 | conn.write('Ann nods: ' + data); 22 | }); 23 | }); 24 | 25 | const bob = multiplexer.registerChannel('bob'); 26 | bob.on('connection', function (conn) { 27 | conn.write("Bob doesn't agree."); 28 | conn.on('data', function (data) { 29 | conn.write('Bob says no to: ' + data); 30 | }); 31 | }); 32 | 33 | const carl = multiplexer.registerChannel('carl'); 34 | carl.on('connection', function (conn) { 35 | conn.write('Carl says goodbye!'); 36 | // Explicitly cancel connection 37 | conn.end(); 38 | }); 39 | 40 | // 3. Express server 41 | const app = express(); 42 | app.get('/', function (req, res) { 43 | res.sendFile(__dirname + '/index.html'); 44 | }); 45 | 46 | const server = http.createServer(app); 47 | service.attach(server); 48 | 49 | server.listen(9999, '0.0.0.0', () => { 50 | console.log(' [*] Listening on 0.0.0.0:9999'); 51 | }); 52 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Server = require('./lib/server'); 4 | 5 | module.exports.createServer = function createServer(options) { 6 | return new Server(options); 7 | }; 8 | 9 | module.exports.listen = function listen(http_server, options) { 10 | const srv = exports.createServer(options); 11 | if (http_server) { 12 | srv.attach(http_server); 13 | } 14 | return srv; 15 | }; 16 | -------------------------------------------------------------------------------- /lib/handlers.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | welcome_screen(req, res) { 5 | res.setHeader('Content-Type', 'text/plain; charset=UTF-8'); 6 | res.writeHead(200); 7 | res.end('Welcome to SockJS!\n'); 8 | }, 9 | 10 | handle_404(req, res) { 11 | res.setHeader('Content-Type', 'text/plain; charset=UTF-8'); 12 | res.writeHead(404); 13 | res.end('404 Error: Page not found\n'); 14 | }, 15 | 16 | handle_405(req, res, methods) { 17 | res.writeHead(405, { Allow: methods.join(', ') }); 18 | res.end(); 19 | }, 20 | 21 | handle_error(err, req, res) { 22 | if (res.finished) { 23 | return; 24 | } 25 | if (typeof err === 'object' && 'status' in err) { 26 | res.setHeader('Content-Type', 'text/plain; charset=UTF-8'); 27 | res.writeHead(err.status); 28 | res.end(err.message || ''); 29 | } else { 30 | try { 31 | res.writeHead(500); 32 | res.end('500 - Internal Server Error'); 33 | } catch (ex) { 34 | this.options.log( 35 | 'error', 36 | `Exception on "${req.method} ${req.url}" in filter "${req.last_fun}":\n${ex.stack || ex}` 37 | ); 38 | } 39 | } 40 | } 41 | }; 42 | -------------------------------------------------------------------------------- /lib/iframe.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const utils = require('./utils'); 4 | const middleware = require('./middleware'); 5 | 6 | const iframe_template = ` 7 | 8 | 9 | 10 | 11 | 12 | 16 | 17 | 18 |

Don't panic!

19 |

This is a SockJS hidden iframe. It's used for cross domain magic.

20 | 21 | `; 22 | 23 | module.exports = { 24 | iframe(req, res, _head, next) { 25 | const context = { 26 | '{{ sockjs_url }}': this.options.sockjs_url 27 | }; 28 | 29 | let content = iframe_template; 30 | for (const k in context) { 31 | content = content.replace(k, context[k]); 32 | } 33 | 34 | const quoted_md5 = `"${utils.md5_hex(content)}"`; 35 | 36 | if ('if-none-match' in req.headers && req.headers['if-none-match'] === quoted_md5) { 37 | res.statusCode = 304; 38 | res.end(); 39 | return next(); 40 | } 41 | 42 | middleware.cache_for(res); 43 | res.setHeader('Content-Type', 'text/html; charset=UTF-8'); 44 | res.setHeader('ETag', quoted_md5); 45 | res.setHeader('Content-Length', Buffer.byteLength(content)); 46 | res.end(content); 47 | next(); 48 | } 49 | }; 50 | -------------------------------------------------------------------------------- /lib/info.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const utils = require('./utils'); 4 | const middleware = require('./middleware'); 5 | 6 | module.exports = { 7 | info(req, res) { 8 | const info = { 9 | // deprecated option, but useful for old clients 10 | websocket: this.options.transports.includes('websocket'), 11 | transports: this.options.transports, 12 | origins: this.options.disable_cors ? undefined : ['*:*'], 13 | cookie_needed: !!this.options.jsessionid, 14 | entropy: utils.random32() 15 | }; 16 | // Users can specify a new base URL which further requests will be made 17 | // against. For example, it may contain a randomized domain name to 18 | // avoid browser per-domain connection limits. 19 | if (typeof this.options.base_url === 'function') { 20 | info.base_url = this.options.base_url(); 21 | } else if (this.options.base_url) { 22 | info.base_url = this.options.base_url; 23 | } 24 | res.setHeader('Content-Type', 'application/json; charset=UTF-8'); 25 | res.writeHead(200); 26 | res.end(JSON.stringify(info)); 27 | }, 28 | 29 | info_options(req, res, _head, next) { 30 | res.statusCode = 204; 31 | middleware.cache_for(res); 32 | res.setHeader('Access-Control-Allow-Methods', 'OPTIONS, GET'); 33 | res.setHeader('Access-Control-Max-Age', res.cache_for); 34 | res.end(); 35 | next(); 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /lib/listener.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const debug = require('debug')('sockjs:listener'); 4 | const transportList = require('./transport/list'); 5 | const middleware = require('./middleware'); 6 | const handlers = require('./handlers'); 7 | const info = require('./info'); 8 | const iframe = require('./iframe'); 9 | 10 | module.exports.generateDispatcher = function generateDispatcher(options) { 11 | const p = (s) => new RegExp(`^${options.prefix}${s}[/]?$`); 12 | const t = (s) => [p(`/([^/.]+)/([^/.]+)${s}`), 'server', 'session']; 13 | const prefix_dispatcher = [ 14 | ['GET', p(''), [handlers.welcome_screen]], 15 | ['OPTIONS', p('/info'), [middleware.h_sid, middleware.xhr_cors, info.info_options]], 16 | ['GET', p('/info'), [middleware.xhr_cors, middleware.h_no_cache, info.info]] 17 | ]; 18 | if (!options.disable_cors) { 19 | prefix_dispatcher.push(['GET', p('/iframe[0-9-.a-z_]*.html'), [iframe.iframe]]); 20 | } 21 | 22 | const transport_dispatcher = []; 23 | 24 | for (const name of options.transports) { 25 | const tr = transportList[name]; 26 | if (!tr) { 27 | throw new Error(`unknown transport ${name}`); 28 | } 29 | debug('enabling transport', name); 30 | 31 | for (const route of tr.routes) { 32 | const d = route.transport ? transport_dispatcher : prefix_dispatcher; 33 | const path = route.transport ? t(route.path) : p(route.path); 34 | const fullroute = [route.method, path, route.handlers]; 35 | if (!d.some((x) => x[0] == route.method && x[1].toString() === path.toString())) { 36 | d.push(fullroute); 37 | } 38 | } 39 | } 40 | return prefix_dispatcher.concat(transport_dispatcher); 41 | }; 42 | -------------------------------------------------------------------------------- /lib/middleware.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const FayeWebsocket = require('faye-websocket'); 4 | const utils = require('./utils'); 5 | const querystring = require('querystring'); 6 | 7 | module.exports = { 8 | h_no_cache(req, res, _head, next) { 9 | res.setHeader('Cache-Control', 'no-store, no-cache, no-transform, must-revalidate, max-age=0'); 10 | next(); 11 | }, 12 | 13 | h_sid(req, res, _head, next) { 14 | // Some load balancers do sticky sessions, but only if there is 15 | // a JSESSIONID cookie. If this cookie isn't yet set, we shall 16 | // set it to a dummy value. It doesn't really matter what, as 17 | // session information is usually added by the load balancer. 18 | req.cookies = utils.parseCookie(req.headers.cookie); 19 | if (typeof this.options.jsessionid === 'function') { 20 | // Users can supply a function 21 | this.options.jsessionid(req, res); 22 | } else if (this.options.jsessionid) { 23 | // We need to set it every time, to give the loadbalancer 24 | // opportunity to attach its own cookies. 25 | const jsid = req.cookies['JSESSIONID'] || 'dummy'; 26 | res.setHeader('Set-Cookie', `JSESSIONID=${jsid}; path=/`); 27 | } 28 | next(); 29 | }, 30 | 31 | cache_for(res, duration = 365 * 24 * 60 * 60) { 32 | res.cache_for = duration; 33 | const exp = new Date(Date.now() + duration * 1000); 34 | res.setHeader('Cache-Control', `public, max-age=${duration}`); 35 | res.setHeader('Expires', exp.toGMTString()); 36 | }, 37 | 38 | log_request(req, res, _head, next) { 39 | const td = Date.now() - req.start_date; 40 | this.options.log( 41 | 'info', 42 | `${req.method} ${req.url} ${td}ms ${res.finished ? res.statusCode : '(unfinished)'}` 43 | ); 44 | next(); 45 | }, 46 | 47 | expect_form(req, res, _head, next) { 48 | utils.getBody(req, (err, body) => { 49 | if (err) { 50 | return next(err); 51 | } 52 | 53 | switch ((req.headers['content-type'] || '').split(';')[0]) { 54 | case 'application/x-www-form-urlencoded': 55 | req.body = querystring.parse(body); 56 | break; 57 | case 'text/plain': 58 | case '': 59 | req.body = body; 60 | break; 61 | default: 62 | this.options.log('error', `Unsupported content-type ${req.headers['content-type']}`); 63 | break; 64 | } 65 | next(); 66 | }); 67 | }, 68 | 69 | expect_xhr(req, res, _head, next) { 70 | utils.getBody(req, (err, body) => { 71 | if (err) { 72 | return next(err); 73 | } 74 | 75 | switch ((req.headers['content-type'] || '').split(';')[0]) { 76 | case 'text/plain': 77 | case 'T': 78 | case 'application/json': 79 | case 'application/xml': 80 | case '': 81 | case 'text/xml': 82 | req.body = body; 83 | break; 84 | default: 85 | this.options.log('error', `Unsupported content-type ${req.headers['content-type']}`); 86 | break; 87 | } 88 | next(); 89 | }); 90 | }, 91 | 92 | websocket_check(req, _socket, _head, next) { 93 | if (!FayeWebsocket.isWebSocket(req)) { 94 | return next({ 95 | status: 400, 96 | message: 'Not a valid websocket request' 97 | }); 98 | } 99 | next(); 100 | }, 101 | 102 | xhr_options(req, res, _head, next) { 103 | res.statusCode = 204; // No content 104 | module.exports.cache_for(res); 105 | res.setHeader('Access-Control-Allow-Methods', 'OPTIONS, POST'); 106 | res.setHeader('Access-Control-Max-Age', res.cache_for); 107 | res.end(); 108 | next(); 109 | }, 110 | 111 | xhr_cors(req, res, _head, next) { 112 | if (this.options.disable_cors) { 113 | return next(); 114 | } 115 | 116 | let origin; 117 | if (!req.headers['origin']) { 118 | origin = '*'; 119 | } else { 120 | origin = req.headers['origin']; 121 | res.setHeader('Access-Control-Allow-Credentials', 'true'); 122 | } 123 | res.setHeader('Access-Control-Allow-Origin', origin); 124 | res.setHeader('Vary', 'Origin'); 125 | const headers = req.headers['access-control-request-headers']; 126 | if (headers) { 127 | res.setHeader('Access-Control-Allow-Headers', headers); 128 | } 129 | next(); 130 | } 131 | }; 132 | -------------------------------------------------------------------------------- /lib/server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const events = require('events'); 4 | const url = require('url'); 5 | const debug = require('debug')('sockjs:server'); 6 | const listener = require('./listener'); 7 | const webjs = require('./webjs'); 8 | const pkg = require('../package.json'); 9 | 10 | class Server extends events.EventEmitter { 11 | constructor(user_options) { 12 | super(); 13 | this.options = Object.assign( 14 | { 15 | prefix: '', 16 | transports: [ 17 | 'eventsource', 18 | 'htmlfile', 19 | 'jsonp-polling', 20 | 'websocket', 21 | 'websocket-raw', 22 | 'xhr-polling', 23 | 'xhr-streaming' 24 | ], 25 | response_limit: 128 * 1024, 26 | faye_server_options: null, 27 | jsessionid: false, 28 | heartbeat_delay: 25000, 29 | disconnect_delay: 5000, 30 | log() {}, 31 | sockjs_url: 'https://cdn.jsdelivr.net/npm/sockjs-client@1/dist/sockjs.min.js' 32 | }, 33 | user_options 34 | ); 35 | 36 | // support old options.websocket setting 37 | if (user_options.websocket === false) { 38 | const trs = new Set(this.options.transports); 39 | trs.delete('websocket'); 40 | trs.delete('websocket-raw'); 41 | this.options.transports = Array.from(trs.values()); 42 | } 43 | 44 | this._prefixMatches = () => true; 45 | if (this.options.prefix) { 46 | // remove trailing slash, but not leading 47 | this.options.prefix = this.options.prefix.replace(/\/$/, ''); 48 | this._prefixMatches = (requrl) => url.parse(requrl).pathname.startsWith(this.options.prefix); 49 | } 50 | 51 | this.options.log( 52 | 'debug', 53 | `SockJS v${pkg.version} bound to ${JSON.stringify(this.options.prefix)}` 54 | ); 55 | this.handler = webjs.generateHandler(this, listener.generateDispatcher(this.options)); 56 | } 57 | 58 | attach(server) { 59 | this._rlisteners = this._installListener(server, 'request'); 60 | this._ulisteners = this._installListener(server, 'upgrade'); 61 | } 62 | 63 | detach(server) { 64 | if (this._rlisteners) { 65 | this._removeListener(server, 'request', this._rlisteners); 66 | this._rlisteners = null; 67 | } 68 | if (this._ulisteners) { 69 | this._removeListener(server, 'upgrade', this._ulisteners); 70 | this._ulisteners = null; 71 | } 72 | } 73 | 74 | _removeListener(server, eventName, listeners) { 75 | server.removeListener(eventName, this.handler); 76 | listeners.forEach((l) => server.on(eventName, l)); 77 | } 78 | 79 | _installListener(server, eventName) { 80 | const listeners = server.listeners(eventName).filter((x) => x !== this.handler); 81 | server.removeAllListeners(eventName); 82 | server.on(eventName, (req, res, head) => { 83 | if (this._prefixMatches(req.url)) { 84 | debug('prefix match', eventName, req.url, this.options.prefix); 85 | this.handler(req, res, head); 86 | } else { 87 | listeners.forEach((l) => l.call(server, req, res, head)); 88 | } 89 | }); 90 | return listeners; 91 | } 92 | } 93 | 94 | module.exports = Server; 95 | -------------------------------------------------------------------------------- /lib/session.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const debug = require('debug')('sockjs:session'); 4 | const Transport = require('./transport/transport'); 5 | const SockJSConnection = require('./sockjs-connection'); 6 | 7 | const MAP = new Map(); 8 | function closeFrame(status, reason) { 9 | return `c${JSON.stringify([status, reason])}`; 10 | } 11 | 12 | class Session { 13 | static bySessionId(session_id) { 14 | if (!session_id) { 15 | return null; 16 | } 17 | return MAP.get(session_id) || null; 18 | } 19 | 20 | static _register(req, server, session_id, receiver) { 21 | let session = Session.bySessionId(session_id); 22 | if (!session) { 23 | debug('create new session', session_id); 24 | session = new Session(session_id, server); 25 | } 26 | session.register(req, receiver); 27 | return session; 28 | } 29 | 30 | static register(req, server, receiver) { 31 | debug('static register', req.session); 32 | return Session._register(req, server, req.session, receiver); 33 | } 34 | 35 | static registerNoSession(req, server, receiver) { 36 | debug('static registerNoSession'); 37 | return Session._register(req, server, undefined, receiver); 38 | } 39 | 40 | constructor(session_id, server) { 41 | this.session_id = session_id; 42 | this.heartbeat_delay = server.options.heartbeat_delay; 43 | this.disconnect_delay = server.options.disconnect_delay; 44 | this.prefix = server.options.prefix; 45 | this.send_buffer = []; 46 | this.is_closing = false; 47 | this.readyState = Transport.CONNECTING; 48 | debug('readyState', 'CONNECTING', this.session_id); 49 | if (this.session_id) { 50 | MAP.set(this.session_id, this); 51 | } 52 | this.didTimeout = this.didTimeout.bind(this); 53 | this.to_tref = setTimeout(this.didTimeout, this.disconnect_delay); 54 | this.connection = new SockJSConnection(this); 55 | this.emit_open = () => { 56 | this.emit_open = null; 57 | server.emit('connection', this.connection); 58 | }; 59 | } 60 | 61 | get id() { 62 | return this.session_id; 63 | } 64 | 65 | register(req, recv) { 66 | if (this.recv) { 67 | recv.sendFrame(closeFrame(2010, 'Another connection still open')); 68 | recv.close(); 69 | return; 70 | } 71 | if (this.to_tref) { 72 | clearTimeout(this.to_tref); 73 | this.to_tref = null; 74 | } 75 | if (this.readyState === Transport.CLOSING) { 76 | this.flushToRecv(recv); 77 | recv.sendFrame(this.close_frame); 78 | recv.close(); 79 | this.to_tref = setTimeout(this.didTimeout, this.disconnect_delay); 80 | return; 81 | } 82 | // Registering. From now on 'unregister' is responsible for 83 | // setting the timer. 84 | this.recv = recv; 85 | this.recv.session = this; 86 | 87 | // Save parameters from request 88 | this.decorateConnection(req); 89 | 90 | // first, send the open frame 91 | if (this.readyState === Transport.CONNECTING) { 92 | this.recv.sendFrame('o'); 93 | this.readyState = Transport.OPEN; 94 | debug('readyState', 'OPEN', this.session_id); 95 | // Emit the open event, but not right now 96 | process.nextTick(this.emit_open); 97 | } 98 | 99 | // At this point the transport might have gotten away (jsonp). 100 | if (!this.recv) { 101 | return; 102 | } 103 | this.tryFlush(); 104 | } 105 | 106 | decorateConnection(req) { 107 | Session.decorateConnection(req, this.connection, this.recv); 108 | } 109 | 110 | static decorateConnection(req, connection, recv) { 111 | let socket = recv.socket; 112 | if (!socket) { 113 | socket = recv.response.socket; 114 | } 115 | // Store the last known address. 116 | let remoteAddress, remotePort, address; 117 | try { 118 | remoteAddress = socket.remoteAddress; 119 | remotePort = socket.remotePort; 120 | address = socket.address(); 121 | } catch (x) { 122 | // intentionally empty 123 | } 124 | 125 | if (remoteAddress) { 126 | // All-or-nothing 127 | connection.remoteAddress = remoteAddress; 128 | connection.remotePort = remotePort; 129 | connection.address = address; 130 | } 131 | 132 | connection.url = req.url; 133 | connection.pathname = req.pathname; 134 | connection.protocol = recv.protocol; 135 | 136 | const headers = {}; 137 | const allowedHeaders = [ 138 | 'referer', 139 | 'x-client-ip', 140 | 'x-forwarded-for', 141 | 'x-cluster-client-ip', 142 | 'via', 143 | 'x-real-ip', 144 | 'x-forwarded-proto', 145 | 'x-ssl', 146 | 'dnt', 147 | 'host', 148 | 'user-agent', 149 | 'accept-language' 150 | ]; 151 | for (const key of allowedHeaders) { 152 | if (req.headers[key]) { 153 | headers[key] = req.headers[key]; 154 | } 155 | } 156 | 157 | if (headers) { 158 | connection.headers = headers; 159 | } 160 | } 161 | 162 | unregister() { 163 | debug('unregister', this.session_id); 164 | const delay = this.recv.delay_disconnect; 165 | this.recv.session = null; 166 | this.recv = null; 167 | if (this.to_tref) { 168 | clearTimeout(this.to_tref); 169 | } 170 | 171 | if (delay) { 172 | debug('delay timeout', this.session_id); 173 | this.to_tref = setTimeout(this.didTimeout, this.disconnect_delay); 174 | } else { 175 | debug('immediate timeout', this.session_id); 176 | this.didTimeout(); 177 | } 178 | } 179 | 180 | flushToRecv(recv) { 181 | if (this.send_buffer.length > 0) { 182 | const sb = this.send_buffer; 183 | this.send_buffer = []; 184 | recv.sendBulk(sb); 185 | return true; 186 | } 187 | return false; 188 | } 189 | 190 | tryFlush() { 191 | if (!this.flushToRecv(this.recv) || !this.to_tref) { 192 | if (this.to_tref) { 193 | clearTimeout(this.to_tref); 194 | } 195 | const x = () => { 196 | if (this.recv) { 197 | this.to_tref = setTimeout(x, this.heartbeat_delay); 198 | this.recv.heartbeat(); 199 | } 200 | }; 201 | this.to_tref = setTimeout(x, this.heartbeat_delay); 202 | } 203 | } 204 | 205 | didTimeout() { 206 | if (this.to_tref) { 207 | clearTimeout(this.to_tref); 208 | this.to_tref = null; 209 | } 210 | if ( 211 | this.readyState !== Transport.CONNECTING && 212 | this.readyState !== Transport.OPEN && 213 | this.readyState !== Transport.CLOSING 214 | ) { 215 | throw new Error('INVALID_STATE_ERR'); 216 | } 217 | if (this.recv) { 218 | throw new Error('RECV_STILL_THERE'); 219 | } 220 | debug('readyState', 'CLOSED', this.session_id); 221 | this.readyState = Transport.CLOSED; 222 | this.connection.push(null); 223 | this.connection = null; 224 | if (this.session_id) { 225 | MAP.delete(this.session_id); 226 | debug('delete session', this.session_id, MAP.size); 227 | this.session_id = null; 228 | } 229 | } 230 | 231 | didMessage(payload) { 232 | if (this.readyState === Transport.OPEN) { 233 | this.connection.push(payload); 234 | } 235 | } 236 | 237 | send(payload) { 238 | if (this.readyState !== Transport.OPEN) { 239 | return false; 240 | } 241 | this.send_buffer.push(payload); 242 | if (this.recv) { 243 | this.tryFlush(); 244 | } 245 | return true; 246 | } 247 | 248 | close(status = 1000, reason = 'Normal closure') { 249 | debug('close', status, reason); 250 | if (this.readyState !== Transport.OPEN) { 251 | return false; 252 | } 253 | this.readyState = Transport.CLOSING; 254 | debug('readyState', 'CLOSING', this.session_id); 255 | this.close_frame = closeFrame(status, reason); 256 | if (this.recv) { 257 | // Go away. sendFrame can trigger close which can 258 | // trigger unregister. Make sure this.recv is not null. 259 | this.recv.sendFrame(this.close_frame); 260 | if (this.recv) { 261 | this.recv.close(); 262 | } 263 | if (this.recv) { 264 | this.unregister(); 265 | } 266 | } 267 | return true; 268 | } 269 | } 270 | 271 | module.exports = Session; 272 | -------------------------------------------------------------------------------- /lib/sockjs-connection.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const debug = require('debug')('sockjs:connection'); 4 | const stream = require('stream'); 5 | const { v4: uuid } = require('uuid'); 6 | 7 | class SockJSConnection extends stream.Duplex { 8 | constructor(session) { 9 | super({ decodeStrings: false, encoding: 'utf8', readableObjectMode: true }); 10 | this._session = session; 11 | this.id = uuid(); 12 | this.headers = {}; 13 | this.prefix = this._session.prefix; 14 | debug('new connection', this.id, this.prefix); 15 | } 16 | 17 | toString() { 18 | return ``; 19 | } 20 | 21 | _write(chunk, encoding, callback) { 22 | if (Buffer.isBuffer(chunk)) { 23 | chunk = chunk.toString(); 24 | } 25 | this._session.send(chunk); 26 | callback(); 27 | } 28 | 29 | _read() {} 30 | 31 | end(chunk, encoding, callback) { 32 | super.end(chunk, encoding, callback); 33 | this.close(); 34 | } 35 | 36 | close(code, reason) { 37 | debug('close', code, reason); 38 | return this._session.close(code, reason); 39 | } 40 | 41 | get readyState() { 42 | return this._session.readyState; 43 | } 44 | } 45 | 46 | module.exports = SockJSConnection; 47 | -------------------------------------------------------------------------------- /lib/transport/base-receiver.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const utils = require('../utils'); 4 | const debug = require('debug')('sockjs:base-receiver'); 5 | 6 | class BaseReceiver { 7 | constructor(socket) { 8 | this.abort = this.abort.bind(this); 9 | this.socket = socket; 10 | this.socket.on('close', this.abort); 11 | this.socket.on('end', this.abort); 12 | } 13 | 14 | tearDown() { 15 | if (!this.socket) { 16 | return; 17 | } 18 | debug('tearDown', this.session && this.session.id); 19 | this.socket.removeListener('close', this.abort); 20 | this.socket.removeListener('end', this.abort); 21 | this.socket = null; 22 | } 23 | 24 | abort() { 25 | debug('abort', this.session && this.session.id); 26 | this.delay_disconnect = false; 27 | this.close(); 28 | } 29 | 30 | close() { 31 | debug('close', this.session && this.session.id); 32 | this.tearDown(); 33 | if (this.session) { 34 | this.session.unregister(); 35 | } 36 | } 37 | 38 | sendBulk(messages) { 39 | const q_msgs = messages.map((m) => utils.quote(m)).join(','); 40 | return this.sendFrame(`a[${q_msgs}]`); 41 | } 42 | 43 | heartbeat() { 44 | return this.sendFrame('h'); 45 | } 46 | } 47 | 48 | module.exports = BaseReceiver; 49 | -------------------------------------------------------------------------------- /lib/transport/eventsource.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const utils = require('../utils'); 3 | const ResponseReceiver = require('./response-receiver'); 4 | const Session = require('../session'); 5 | const middleware = require('../middleware'); 6 | 7 | class EventSourceReceiver extends ResponseReceiver { 8 | constructor(req, res, options) { 9 | super(req, res, options); 10 | this.protocol = 'eventsource'; 11 | } 12 | 13 | sendFrame(payload) { 14 | // Beware of leading whitespace 15 | const data = `data: ${utils.escape_selected(payload, '\r\n\x00')}\r\n\r\n`; 16 | return super.sendFrame(data); 17 | } 18 | } 19 | 20 | function eventsource(req, res, _head, next) { 21 | let origin; 22 | if (!req.headers['origin'] || req.headers['origin'] === 'null') { 23 | origin = '*'; 24 | } else { 25 | origin = req.headers['origin']; 26 | res.setHeader('Access-Control-Allow-Credentials', 'true'); 27 | } 28 | res.setHeader('Content-Type', 'text/event-stream'); 29 | res.setHeader('Access-Control-Allow-Origin', origin); 30 | res.setHeader('Vary', 'Origin'); 31 | const headers = req.headers['access-control-request-headers']; 32 | if (headers) { 33 | res.setHeader('Access-Control-Allow-Headers', headers); 34 | } 35 | 36 | res.writeHead(200); 37 | // Opera needs one more new line at the start. 38 | res.write('\r\n'); 39 | 40 | Session.register(req, this, new EventSourceReceiver(req, res, this.options)); 41 | next(); 42 | } 43 | 44 | module.exports = { 45 | routes: [ 46 | { 47 | method: 'GET', 48 | path: '/eventsource', 49 | handlers: [middleware.h_sid, middleware.h_no_cache, eventsource], 50 | transport: true 51 | } 52 | ] 53 | }; 54 | -------------------------------------------------------------------------------- /lib/transport/htmlfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const ResponseReceiver = require('./response-receiver'); 4 | const Session = require('../session'); 5 | const middleware = require('../middleware'); 6 | 7 | // Browsers fail with "Uncaught exception: ReferenceError: Security 8 | // error: attempted to read protected variable: _jp". Set 9 | // document.domain in order to work around that. 10 | let iframe_template = ` 11 | 12 | 13 | 14 | 15 |

Don't panic!

16 | 23 | `; 24 | // Safari needs at least 1024 bytes to parse the website. Relevant: 25 | // http://code.google.com/p/browsersec/wiki/Part2#Survey_of_content_sniffing_behaviors 26 | iframe_template += Array(1024 - iframe_template.length + 14).join(' '); 27 | iframe_template += '\r\n\r\n'; 28 | 29 | class HtmlFileReceiver extends ResponseReceiver { 30 | constructor(req, res, options) { 31 | super(req, res, options); 32 | this.protocol = 'htmlfile'; 33 | } 34 | 35 | sendFrame(payload) { 36 | return super.sendFrame(`\r\n`); 37 | } 38 | } 39 | 40 | function htmlfile(req, res, _head, next) { 41 | if (!('c' in req.query || 'callback' in req.query)) { 42 | return next({ 43 | status: 500, 44 | message: '"callback" parameter required' 45 | }); 46 | } 47 | const callback = 'c' in req.query ? req.query['c'] : req.query['callback']; 48 | if (/[^a-zA-Z0-9-_.]/.test(callback)) { 49 | return next({ 50 | status: 500, 51 | message: 'invalid "callback" parameter' 52 | }); 53 | } 54 | 55 | res.setHeader('Content-Type', 'text/html; charset=UTF-8'); 56 | res.writeHead(200); 57 | res.write(iframe_template.replace(/{{ callback }}/g, callback)); 58 | 59 | Session.register(req, this, new HtmlFileReceiver(req, res, this.options)); 60 | next(); 61 | } 62 | 63 | module.exports = { 64 | routes: [ 65 | { 66 | method: 'GET', 67 | path: '/htmlfile', 68 | handlers: [middleware.h_sid, middleware.h_no_cache, htmlfile], 69 | transport: true 70 | } 71 | ] 72 | }; 73 | -------------------------------------------------------------------------------- /lib/transport/jsonp-polling.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const ResponseReceiver = require('./response-receiver'); 4 | const Session = require('../session'); 5 | const middleware = require('../middleware'); 6 | 7 | class JsonpReceiver extends ResponseReceiver { 8 | constructor(req, res, options, callback) { 9 | super(req, res, options); 10 | this.protocol = 'jsonp-polling'; 11 | this.max_response_size = 1; 12 | this.callback = callback; 13 | } 14 | 15 | sendFrame(payload) { 16 | // Yes, JSONed twice, there isn't a a better way, we must pass 17 | // a string back, and the script, will be evaled() by the 18 | // browser. 19 | // prepend comment to avoid SWF exploit #163 20 | return super.sendFrame(`/**/${this.callback}(${JSON.stringify(payload)});\r\n`); 21 | } 22 | } 23 | 24 | function jsonp(req, res, _head, next) { 25 | if (!('c' in req.query || 'callback' in req.query)) { 26 | return next({ 27 | status: 500, 28 | message: '"callback" parameter required' 29 | }); 30 | } 31 | 32 | const callback = 'c' in req.query ? req.query['c'] : req.query['callback']; 33 | if (/[^a-zA-Z0-9-_.]/.test(callback) || callback.length > 32) { 34 | return next({ 35 | status: 500, 36 | message: 'invalid "callback" parameter' 37 | }); 38 | } 39 | 40 | // protect against SWF JSONP exploit - #163 41 | res.setHeader('X-Content-Type-Options', 'nosniff'); 42 | res.setHeader('Content-Type', 'application/javascript; charset=UTF-8'); 43 | res.writeHead(200); 44 | 45 | Session.register(req, this, new JsonpReceiver(req, res, this.options, callback)); 46 | next(); 47 | } 48 | 49 | function jsonp_send(req, res, _head, next) { 50 | if (!req.body) { 51 | return next({ 52 | status: 500, 53 | message: 'Payload expected.' 54 | }); 55 | } 56 | let d; 57 | if (typeof req.body === 'string') { 58 | try { 59 | d = JSON.parse(req.body); 60 | } catch (x) { 61 | return next({ 62 | status: 500, 63 | message: 'Broken JSON encoding.' 64 | }); 65 | } 66 | } else { 67 | d = req.body.d; 68 | } 69 | if (typeof d === 'string' && d) { 70 | try { 71 | d = JSON.parse(d); 72 | } catch (x) { 73 | return next({ 74 | status: 500, 75 | message: 'Broken JSON encoding.' 76 | }); 77 | } 78 | } 79 | 80 | if (!d || d.__proto__.constructor !== Array) { 81 | return next({ 82 | status: 500, 83 | message: 'Payload expected.' 84 | }); 85 | } 86 | const jsonp = Session.bySessionId(req.session); 87 | if (jsonp === null) { 88 | return next({ status: 404, message: 'session not found' }); 89 | } 90 | for (const message of d) { 91 | jsonp.didMessage(message); 92 | } 93 | 94 | res.setHeader('Content-Length', '2'); 95 | res.setHeader('Content-Type', 'text/plain; charset=UTF-8'); 96 | res.writeHead(200); 97 | res.end('ok'); 98 | next(); 99 | } 100 | 101 | module.exports = { 102 | routes: [ 103 | { 104 | method: 'GET', 105 | path: '/jsonp', 106 | handlers: [middleware.h_sid, middleware.h_no_cache, jsonp], 107 | transport: true 108 | }, 109 | { 110 | method: 'POST', 111 | path: '/jsonp_send', 112 | handlers: [middleware.h_sid, middleware.h_no_cache, middleware.expect_form, jsonp_send], 113 | transport: true 114 | } 115 | ] 116 | }; 117 | -------------------------------------------------------------------------------- /lib/transport/list.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | eventsource: require('./eventsource'), 5 | htmlfile: require('./htmlfile'), 6 | 'jsonp-polling': require('./jsonp-polling'), 7 | websocket: require('./websocket'), 8 | 'websocket-raw': require('./websocket-raw'), 9 | 'xhr-polling': require('./xhr-polling'), 10 | 'xhr-streaming': require('./xhr-streaming') 11 | }; 12 | -------------------------------------------------------------------------------- /lib/transport/response-receiver.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const BaseReceiver = require('./base-receiver'); 4 | const debug = require('debug')('sockjs:response-receiver'); 5 | 6 | // Write stuff to response, using chunked encoding if possible. 7 | class ResponseReceiver extends BaseReceiver { 8 | constructor(request, response, options) { 9 | super(request.socket); 10 | this.max_response_size = options.response_limit; 11 | this.delay_disconnect = true; 12 | this.request = request; 13 | this.response = response; 14 | this.options = options; 15 | this.curr_response_size = 0; 16 | try { 17 | this.request.socket.setKeepAlive(true, 5000); 18 | } catch (x) { 19 | // intentionally empty 20 | } 21 | } 22 | 23 | sendFrame(payload) { 24 | debug('sendFrame'); 25 | this.curr_response_size += payload.length; 26 | let r = false; 27 | try { 28 | this.response.write(payload); 29 | r = true; 30 | } catch (x) { 31 | // intentionally empty 32 | } 33 | if (this.max_response_size && this.curr_response_size >= this.max_response_size) { 34 | this.close(); 35 | } 36 | return r; 37 | } 38 | 39 | close() { 40 | super.close(...arguments); 41 | try { 42 | this.response.end(); 43 | } catch (x) { 44 | // intentionally empty 45 | } 46 | this.request = null; 47 | this.response = null; 48 | } 49 | } 50 | 51 | module.exports = ResponseReceiver; 52 | -------------------------------------------------------------------------------- /lib/transport/transport.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | class Transport {} 4 | 5 | Transport.CONNECTING = 0; 6 | Transport.OPEN = 1; 7 | Transport.CLOSING = 2; 8 | Transport.CLOSED = 3; 9 | 10 | module.exports = Transport; 11 | -------------------------------------------------------------------------------- /lib/transport/websocket-raw.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const FayeWebsocket = require('faye-websocket'); 4 | const Session = require('../session'); 5 | const Transport = require('./transport'); 6 | const SockJSConnection = require('../sockjs-connection'); 7 | const middleware = require('../middleware'); 8 | 9 | class RawWebsocketSessionReceiver { 10 | constructor(req, conn, server, ws) { 11 | this.ws = ws; 12 | this.prefix = server.options.prefix; 13 | this.readyState = Transport.OPEN; 14 | this.recv = { 15 | socket: conn, 16 | protocol: 'websocket-raw' 17 | }; 18 | 19 | this.connection = new SockJSConnection(this); 20 | Session.decorateConnection(req, this.connection, this.recv); 21 | server.emit('connection', this.connection); 22 | 23 | this._close = this._close.bind(this); 24 | this.ws.once('close', this._close); 25 | 26 | this.didMessage = this.didMessage.bind(this); 27 | this.ws.on('message', this.didMessage); 28 | } 29 | 30 | didMessage(m) { 31 | if (this.readyState === Transport.OPEN) { 32 | this.connection.emit('data', m.data); 33 | } 34 | } 35 | 36 | send(payload) { 37 | if (this.readyState !== Transport.OPEN) { 38 | return false; 39 | } 40 | this.ws.send(payload); 41 | return true; 42 | } 43 | 44 | close(status = 1000, reason = 'Normal closure') { 45 | if (this.readyState !== Transport.OPEN) { 46 | return false; 47 | } 48 | this.readyState = Transport.CLOSING; 49 | this.ws.close(status, reason, false); 50 | return true; 51 | } 52 | 53 | _close() { 54 | if (!this.ws) { 55 | return; 56 | } 57 | this.ws.removeEventListener('message', this.didMessage); 58 | this.ws.removeEventListener('close', this._close); 59 | try { 60 | this.ws.close(1000, 'Normal closure', false); 61 | } catch (x) { 62 | // intentionally empty 63 | } 64 | this.ws = null; 65 | 66 | this.readyState = Transport.CLOSED; 67 | this.connection.emit('end'); 68 | this.connection.emit('close'); 69 | this.connection = null; 70 | } 71 | } 72 | 73 | function raw_websocket(req, socket, head, next) { 74 | const ver = req.headers['sec-websocket-version'] || ''; 75 | if (['8', '13'].indexOf(ver) === -1) { 76 | return next({ 77 | status: 400, 78 | message: 'Only supported WebSocket protocol is RFC 6455.' 79 | }); 80 | } 81 | const ws = new FayeWebsocket(req, socket, head, null, this.options.faye_server_options); 82 | ws.onopen = () => { 83 | new RawWebsocketSessionReceiver(req, socket, this, ws); 84 | }; 85 | next(); 86 | } 87 | 88 | module.exports = { 89 | routes: [ 90 | { 91 | method: 'GET', 92 | path: '/websocket', 93 | handlers: [middleware.websocket_check, raw_websocket], 94 | transport: false 95 | } 96 | ] 97 | }; 98 | -------------------------------------------------------------------------------- /lib/transport/websocket.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const debug = require('debug')('sockjs:trans:websocket'); 4 | const FayeWebsocket = require('faye-websocket'); 5 | const BaseReceiver = require('./base-receiver'); 6 | const Session = require('../session'); 7 | const middleware = require('../middleware'); 8 | 9 | class WebSocketReceiver extends BaseReceiver { 10 | constructor(ws, socket) { 11 | super(socket); 12 | debug('new connection'); 13 | this.protocol = 'websocket'; 14 | this.ws = ws; 15 | try { 16 | socket.setKeepAlive(true, 5000); 17 | } catch (x) { 18 | // intentionally empty 19 | } 20 | this.ws.once('close', this.abort); 21 | this.ws.on('message', (m) => this.didMessage(m.data)); 22 | this.heartbeatTimeout = this.heartbeatTimeout.bind(this); 23 | } 24 | 25 | tearDown() { 26 | if (this.ws) { 27 | this.ws.removeEventListener('close', this.abort); 28 | } 29 | super.tearDown(); 30 | } 31 | 32 | didMessage(payload) { 33 | debug('message'); 34 | if (this.ws && this.session && payload.length > 0) { 35 | let message; 36 | try { 37 | message = JSON.parse(payload); 38 | } catch (x) { 39 | return this.close(3000, 'Broken framing.'); 40 | } 41 | if (payload[0] === '[') { 42 | message.forEach((msg) => this.session.didMessage(msg)); 43 | } else { 44 | this.session.didMessage(message); 45 | } 46 | } 47 | } 48 | 49 | sendFrame(payload) { 50 | debug('send'); 51 | if (this.ws) { 52 | try { 53 | this.ws.send(payload); 54 | return true; 55 | } catch (x) { 56 | // intentionally empty 57 | } 58 | } 59 | return false; 60 | } 61 | 62 | close(status = 1000, reason = 'Normal closure') { 63 | super.close(status, reason); 64 | if (this.ws) { 65 | try { 66 | this.ws.close(status, reason, false); 67 | } catch (x) { 68 | // intentionally empty 69 | } 70 | } 71 | this.ws = null; 72 | } 73 | 74 | heartbeat() { 75 | const supportsHeartbeats = this.ws.ping(null, () => clearTimeout(this.hto_ref)); 76 | 77 | if (supportsHeartbeats) { 78 | this.hto_ref = setTimeout(this.heartbeatTimeout, 10000); 79 | } else { 80 | super.heartbeat(); 81 | } 82 | } 83 | 84 | heartbeatTimeout() { 85 | if (this.session) { 86 | this.session.close(3000, 'No response from heartbeat'); 87 | } 88 | } 89 | } 90 | 91 | function sockjs_websocket(req, socket, head, next) { 92 | const ws = new FayeWebsocket(req, socket, head, null, this.options.faye_server_options); 93 | ws.once('open', () => { 94 | // websockets possess no session_id 95 | Session.registerNoSession(req, this, new WebSocketReceiver(ws, socket)); 96 | }); 97 | next(); 98 | } 99 | 100 | module.exports = { 101 | routes: [ 102 | { 103 | method: 'GET', 104 | path: '/websocket', 105 | handlers: [middleware.websocket_check, sockjs_websocket], 106 | transport: true 107 | } 108 | ] 109 | }; 110 | -------------------------------------------------------------------------------- /lib/transport/xhr-polling.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const ResponseReceiver = require('./response-receiver'); 4 | const Session = require('../session'); 5 | const middleware = require('../middleware'); 6 | const xhr = require('./xhr'); 7 | 8 | class XhrPollingReceiver extends ResponseReceiver { 9 | constructor(req, res, options) { 10 | super(req, res, options); 11 | this.protocol = 'xhr-polling'; 12 | this.max_response_size = 1; 13 | } 14 | 15 | sendFrame(payload) { 16 | return super.sendFrame(payload + '\n'); 17 | } 18 | } 19 | 20 | function xhr_poll(req, res, _head, next) { 21 | res.setHeader('Content-Type', 'application/javascript; charset=UTF-8'); 22 | res.writeHead(200); 23 | 24 | Session.register(req, this, new XhrPollingReceiver(req, res, this.options)); 25 | next(); 26 | } 27 | 28 | module.exports = { 29 | routes: [ 30 | { 31 | method: 'POST', 32 | path: '/xhr', 33 | handlers: [middleware.h_sid, middleware.h_no_cache, middleware.xhr_cors, xhr_poll], 34 | transport: true 35 | }, 36 | { 37 | method: 'OPTIONS', 38 | path: '/xhr', 39 | handlers: [middleware.h_sid, middleware.xhr_cors, middleware.xhr_options], 40 | transport: true 41 | }, 42 | { 43 | method: 'POST', 44 | path: '/xhr_send', 45 | handlers: [ 46 | middleware.h_sid, 47 | middleware.h_no_cache, 48 | middleware.xhr_cors, 49 | middleware.expect_xhr, 50 | xhr.xhr_send 51 | ], 52 | transport: true 53 | }, 54 | { 55 | method: 'OPTIONS', 56 | path: '/xhr_send', 57 | handlers: [middleware.h_sid, middleware.xhr_cors, middleware.xhr_options], 58 | transport: true 59 | } 60 | ] 61 | }; 62 | -------------------------------------------------------------------------------- /lib/transport/xhr-streaming.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const ResponseReceiver = require('./response-receiver'); 4 | const Session = require('../session'); 5 | const middleware = require('../middleware'); 6 | const xhr = require('./xhr'); 7 | 8 | class XhrStreamingReceiver extends ResponseReceiver { 9 | constructor(req, res, options) { 10 | super(req, res, options); 11 | this.protocol = 'xhr-streaming'; 12 | } 13 | 14 | sendFrame(payload) { 15 | return super.sendFrame(payload + '\n'); 16 | } 17 | } 18 | 19 | function xhr_streaming(req, res, _head, next) { 20 | res.setHeader('Content-Type', 'application/javascript; charset=UTF-8'); 21 | res.writeHead(200); 22 | 23 | // IE requires 2KB prefix: 24 | // http://blogs.msdn.com/b/ieinternals/archive/2010/04/06/comet-streaming-in-internet-explorer-with-xmlhttprequest-and-xdomainrequest.aspx 25 | res.write(Array(2049).join('h') + '\n'); 26 | 27 | Session.register(req, this, new XhrStreamingReceiver(req, res, this.options)); 28 | next(); 29 | } 30 | 31 | module.exports = { 32 | routes: [ 33 | { 34 | method: 'POST', 35 | path: '/xhr_streaming', 36 | handlers: [middleware.h_sid, middleware.h_no_cache, middleware.xhr_cors, xhr_streaming], 37 | transport: true 38 | }, 39 | { 40 | method: 'OPTIONS', 41 | path: '/xhr_streaming', 42 | handlers: [middleware.h_sid, middleware.xhr_cors, middleware.xhr_options], 43 | transport: true 44 | }, 45 | { 46 | method: 'POST', 47 | path: '/xhr_send', 48 | handlers: [ 49 | middleware.h_sid, 50 | middleware.h_no_cache, 51 | middleware.xhr_cors, 52 | middleware.expect_xhr, 53 | xhr.xhr_send 54 | ], 55 | transport: true 56 | }, 57 | { 58 | method: 'OPTIONS', 59 | path: '/xhr_send', 60 | handlers: [middleware.h_sid, middleware.xhr_cors, middleware.xhr_options], 61 | transport: true 62 | } 63 | ] 64 | }; 65 | -------------------------------------------------------------------------------- /lib/transport/xhr.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Session = require('../session'); 4 | 5 | module.exports = { 6 | xhr_send(req, res, _head, next) { 7 | if (!req.body) { 8 | return next({ 9 | status: 500, 10 | message: 'Payload expected.' 11 | }); 12 | } 13 | let d; 14 | try { 15 | d = JSON.parse(req.body); 16 | } catch (x) { 17 | return next({ 18 | status: 500, 19 | message: 'Broken JSON encoding.' 20 | }); 21 | } 22 | 23 | if (!d || d.__proto__.constructor !== Array) { 24 | return next({ 25 | status: 500, 26 | message: 'Payload expected.' 27 | }); 28 | } 29 | const jsonp = Session.bySessionId(req.session); 30 | if (!jsonp) { 31 | return next({ status: 404 }); 32 | } 33 | for (const message of d) { 34 | jsonp.didMessage(message); 35 | } 36 | 37 | // FF assumes that the response is XML. 38 | res.setHeader('Content-Type', 'text/plain; charset=UTF-8'); 39 | res.writeHead(204); 40 | res.end(); 41 | } 42 | }; 43 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const crypto = require('crypto'); 3 | const http = require('http'); 4 | 5 | // used in case of 'upgrade' requests where res is 6 | // net.Socket instead of http.ServerResponse 7 | module.exports.fake_response = function fake_response(req, res) { 8 | // This is quite simplistic, don't expect much. 9 | const headers = { Connection: 'close' }; 10 | res.writeHead = function (status, user_headers = {}) { 11 | let r = []; 12 | r.push(`HTTP/${req.httpVersion} ${status} ${http.STATUS_CODES[status]}`); 13 | Object.assign(headers, user_headers); 14 | for (const k in headers) { 15 | r.push(`${k}: ${headers[k]}`); 16 | } 17 | r.push(''); 18 | r.push(''); 19 | try { 20 | res.write(r.join('\r\n')); 21 | } catch (x) { 22 | // intentionally empty 23 | } 24 | }; 25 | res.setHeader = (k, v) => (headers[k] = v); 26 | }; 27 | 28 | module.exports.escape_selected = function escape_selected(str, chars) { 29 | const map = {}; 30 | chars = `%${chars}`; 31 | Array.prototype.forEach.call(chars, (c) => (map[c] = escape(c))); 32 | const r = new RegExp(`([${chars}])`); 33 | const parts = str.split(r); 34 | parts.forEach((v, i) => { 35 | if (v.length === 1 && v in map) { 36 | parts[i] = map[v]; 37 | } 38 | }); 39 | return parts.join(''); 40 | }; 41 | 42 | module.exports.md5_hex = function md5_hex(data) { 43 | return crypto.createHash('md5').update(data).digest('hex'); 44 | }; 45 | 46 | // eslint-disable-next-line no-control-regex 47 | const escapable = /[\x00-\x1f\ud800-\udfff\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufff0-\uffff]/g; 48 | 49 | function unroll_lookup(escapable) { 50 | const unrolled = {}; 51 | const c = Array.from(Array(65536).keys()).map((i) => String.fromCharCode(i)); 52 | escapable.lastIndex = 0; 53 | c.join('').replace(escapable, (a) => { 54 | unrolled[a] = `\\u${`0000${a.charCodeAt(0).toString(16)}`.slice(-4)}`; 55 | }); 56 | return unrolled; 57 | } 58 | 59 | const lookup = unroll_lookup(escapable); 60 | 61 | module.exports.quote = function quote(string) { 62 | const quoted = JSON.stringify(string); 63 | 64 | // In most cases normal json encoding fast and enough 65 | escapable.lastIndex = 0; 66 | if (!escapable.test(quoted)) { 67 | return quoted; 68 | } 69 | 70 | return quoted.replace(escapable, (a) => lookup[a]); 71 | }; 72 | 73 | module.exports.parseCookie = function parseCookie(cookie_header) { 74 | const cookies = {}; 75 | if (cookie_header) { 76 | cookie_header.split(';').forEach((cookie) => { 77 | const [name, value] = cookie.split('='); 78 | cookies[name.trim()] = (value || '').trim(); 79 | }); 80 | } 81 | return cookies; 82 | }; 83 | 84 | module.exports.random32 = function random32() { 85 | return crypto.randomBytes(4).readUInt32LE(0); 86 | }; 87 | 88 | module.exports.getBody = function getBody(req, cb) { 89 | let body = []; 90 | req.on('data', (d) => { 91 | body.push(d); 92 | }); 93 | req.once('end', () => { 94 | cb(null, Buffer.concat(body).toString('utf8')); 95 | }); 96 | req.once('error', cb); 97 | req.once('close', () => { 98 | body = null; 99 | }); 100 | }; 101 | -------------------------------------------------------------------------------- /lib/webjs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const debug = require('debug')('sockjs:webjs'); 4 | const url = require('url'); 5 | const utils = require('./utils'); 6 | const handlers = require('./handlers'); 7 | const middleware = require('./middleware'); 8 | 9 | function execute_async_request(server, funs, req, res, head) { 10 | function next(err) { 11 | if (err) { 12 | if (err.status) { 13 | const handlerName = `handle_${err.status}`; 14 | if (handlers[handlerName]) { 15 | return handlers[handlerName].call(server, req, res, err); 16 | } 17 | } 18 | return handlers.handle_error.call(server, err, req, res); 19 | } 20 | if (!funs.length) { 21 | return; 22 | } 23 | const fun = funs.shift(); 24 | debug('call', fun); 25 | fun.call(server, req, res, head, next); 26 | } 27 | next(); 28 | } 29 | 30 | module.exports.generateHandler = function generateHandler(server, dispatcher) { 31 | return function (req, res, head) { 32 | if (res.writeHead === undefined) { 33 | utils.fake_response(req, res); 34 | } 35 | const parsedUrl = url.parse(req.url, true); 36 | req.pathname = parsedUrl.pathname || ''; 37 | req.query = parsedUrl.query; 38 | req.start_date = Date.now(); 39 | 40 | let found = false; 41 | const allowed_methods = []; 42 | for (const row of dispatcher) { 43 | let [method, path, funs] = row; 44 | if (!Array.isArray(path)) { 45 | path = [path]; 46 | } 47 | // path[0] must be a regexp 48 | const m = req.pathname.match(path[0]); 49 | if (!m) { 50 | continue; 51 | } 52 | if (req.method !== method) { 53 | allowed_methods.push(method); 54 | continue; 55 | } 56 | for (let i = 1; i < path.length; i++) { 57 | req[path[i]] = m[i]; 58 | } 59 | funs = funs.slice(0); 60 | funs.push(middleware.log_request); 61 | execute_async_request(server, funs, req, res, head); 62 | found = true; 63 | break; 64 | } 65 | 66 | if (!found) { 67 | if (allowed_methods.length !== 0) { 68 | handlers.handle_405.call(server, req, res, allowed_methods); 69 | } else { 70 | handlers.handle_404.call(server, req, res); 71 | } 72 | middleware.log_request.call(server, req, res, true, () => {}); 73 | } 74 | }; 75 | }; 76 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sockjs", 3 | "version": "0.4.0-rc.1", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@babel/code-frame": { 8 | "version": "7.10.1", 9 | "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.1.tgz", 10 | "integrity": "sha512-IGhtTmpjGbYzcEDOw7DcQtbQSXcG9ftmAXtWTu9V936vDye4xjjekktFAtgZsWpzTj/X01jocB46mTywm/4SZw==", 11 | "dev": true, 12 | "requires": { 13 | "@babel/highlight": "^7.10.1" 14 | } 15 | }, 16 | "@babel/helper-validator-identifier": { 17 | "version": "7.10.1", 18 | "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.1.tgz", 19 | "integrity": "sha512-5vW/JXLALhczRCWP0PnFDMCJAchlBvM7f4uk/jXritBnIa6E1KmqmtrS3yn1LAnxFBypQ3eneLuXjsnfQsgILw==", 20 | "dev": true 21 | }, 22 | "@babel/highlight": { 23 | "version": "7.10.1", 24 | "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.1.tgz", 25 | "integrity": "sha512-8rMof+gVP8mxYZApLF/JgNDAkdKa+aJt3ZYxF8z6+j/hpeXL7iMsKCPHa2jNMHu/qqBwzQF4OHNoYi8dMA/rYg==", 26 | "dev": true, 27 | "requires": { 28 | "@babel/helper-validator-identifier": "^7.10.1", 29 | "chalk": "^2.0.0", 30 | "js-tokens": "^4.0.0" 31 | }, 32 | "dependencies": { 33 | "chalk": { 34 | "version": "2.4.2", 35 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", 36 | "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", 37 | "dev": true, 38 | "requires": { 39 | "ansi-styles": "^3.2.1", 40 | "escape-string-regexp": "^1.0.5", 41 | "supports-color": "^5.3.0" 42 | } 43 | } 44 | } 45 | }, 46 | "@types/color-name": { 47 | "version": "1.1.1", 48 | "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", 49 | "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==", 50 | "dev": true 51 | }, 52 | "acorn": { 53 | "version": "7.2.0", 54 | "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.2.0.tgz", 55 | "integrity": "sha512-apwXVmYVpQ34m/i71vrApRrRKCWQnZZF1+npOD0WV5xZFfwWOmKGQ2RWlfdy9vWITsenisM8M0Qeq8agcFHNiQ==", 56 | "dev": true 57 | }, 58 | "acorn-jsx": { 59 | "version": "5.2.0", 60 | "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.2.0.tgz", 61 | "integrity": "sha512-HiUX/+K2YpkpJ+SzBffkM/AQ2YE03S0U1kjTLVpoJdhZMOWy8qvXVN9JdLqv2QsaQ6MPYQIuNmwD8zOiYUofLQ==", 62 | "dev": true 63 | }, 64 | "ajv": { 65 | "version": "6.12.6", 66 | "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", 67 | "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", 68 | "dev": true, 69 | "requires": { 70 | "fast-deep-equal": "^3.1.1", 71 | "fast-json-stable-stringify": "^2.0.0", 72 | "json-schema-traverse": "^0.4.1", 73 | "uri-js": "^4.2.2" 74 | } 75 | }, 76 | "ansi-escapes": { 77 | "version": "4.3.1", 78 | "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.1.tgz", 79 | "integrity": "sha512-JWF7ocqNrp8u9oqpgV+wH5ftbt+cfvv+PTjOvKLT3AdYly/LmORARfEVT1iyjwN+4MqE5UmVKoAdIBqeoCHgLA==", 80 | "dev": true, 81 | "requires": { 82 | "type-fest": "^0.11.0" 83 | }, 84 | "dependencies": { 85 | "type-fest": { 86 | "version": "0.11.0", 87 | "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.11.0.tgz", 88 | "integrity": "sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ==", 89 | "dev": true 90 | } 91 | } 92 | }, 93 | "ansi-regex": { 94 | "version": "5.0.1", 95 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", 96 | "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", 97 | "dev": true 98 | }, 99 | "ansi-styles": { 100 | "version": "3.2.1", 101 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", 102 | "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", 103 | "dev": true, 104 | "requires": { 105 | "color-convert": "^1.9.0" 106 | } 107 | }, 108 | "argparse": { 109 | "version": "1.0.10", 110 | "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", 111 | "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", 112 | "dev": true, 113 | "requires": { 114 | "sprintf-js": "~1.0.2" 115 | } 116 | }, 117 | "astral-regex": { 118 | "version": "1.0.0", 119 | "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", 120 | "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", 121 | "dev": true 122 | }, 123 | "balanced-match": { 124 | "version": "1.0.0", 125 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", 126 | "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", 127 | "dev": true 128 | }, 129 | "brace-expansion": { 130 | "version": "1.1.11", 131 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 132 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 133 | "dev": true, 134 | "requires": { 135 | "balanced-match": "^1.0.0", 136 | "concat-map": "0.0.1" 137 | } 138 | }, 139 | "callsites": { 140 | "version": "3.1.0", 141 | "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", 142 | "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", 143 | "dev": true 144 | }, 145 | "chalk": { 146 | "version": "4.0.0", 147 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.0.0.tgz", 148 | "integrity": "sha512-N9oWFcegS0sFr9oh1oz2d7Npos6vNoWW9HvtCg5N1KRFpUhaAhvTv5Y58g880fZaEYSNm3qDz8SU1UrGvp+n7A==", 149 | "dev": true, 150 | "requires": { 151 | "ansi-styles": "^4.1.0", 152 | "supports-color": "^7.1.0" 153 | }, 154 | "dependencies": { 155 | "ansi-styles": { 156 | "version": "4.2.1", 157 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", 158 | "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", 159 | "dev": true, 160 | "requires": { 161 | "@types/color-name": "^1.1.1", 162 | "color-convert": "^2.0.1" 163 | } 164 | }, 165 | "color-convert": { 166 | "version": "2.0.1", 167 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", 168 | "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", 169 | "dev": true, 170 | "requires": { 171 | "color-name": "~1.1.4" 172 | } 173 | }, 174 | "color-name": { 175 | "version": "1.1.4", 176 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", 177 | "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", 178 | "dev": true 179 | }, 180 | "has-flag": { 181 | "version": "4.0.0", 182 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", 183 | "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", 184 | "dev": true 185 | }, 186 | "supports-color": { 187 | "version": "7.1.0", 188 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", 189 | "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", 190 | "dev": true, 191 | "requires": { 192 | "has-flag": "^4.0.0" 193 | } 194 | } 195 | } 196 | }, 197 | "chardet": { 198 | "version": "0.7.0", 199 | "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", 200 | "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", 201 | "dev": true 202 | }, 203 | "cli-cursor": { 204 | "version": "3.1.0", 205 | "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", 206 | "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", 207 | "dev": true, 208 | "requires": { 209 | "restore-cursor": "^3.1.0" 210 | } 211 | }, 212 | "cli-width": { 213 | "version": "2.2.1", 214 | "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.1.tgz", 215 | "integrity": "sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw==", 216 | "dev": true 217 | }, 218 | "color-convert": { 219 | "version": "1.9.3", 220 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", 221 | "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", 222 | "dev": true, 223 | "requires": { 224 | "color-name": "1.1.3" 225 | } 226 | }, 227 | "color-name": { 228 | "version": "1.1.3", 229 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", 230 | "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", 231 | "dev": true 232 | }, 233 | "concat-map": { 234 | "version": "0.0.1", 235 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 236 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", 237 | "dev": true 238 | }, 239 | "cross-spawn": { 240 | "version": "7.0.3", 241 | "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", 242 | "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", 243 | "dev": true, 244 | "requires": { 245 | "path-key": "^3.1.0", 246 | "shebang-command": "^2.0.0", 247 | "which": "^2.0.1" 248 | } 249 | }, 250 | "debug": { 251 | "version": "4.3.1", 252 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", 253 | "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", 254 | "requires": { 255 | "ms": "2.1.2" 256 | } 257 | }, 258 | "deep-is": { 259 | "version": "0.1.3", 260 | "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", 261 | "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", 262 | "dev": true 263 | }, 264 | "doctrine": { 265 | "version": "3.0.0", 266 | "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", 267 | "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", 268 | "dev": true, 269 | "requires": { 270 | "esutils": "^2.0.2" 271 | } 272 | }, 273 | "emoji-regex": { 274 | "version": "8.0.0", 275 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", 276 | "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", 277 | "dev": true 278 | }, 279 | "escape-string-regexp": { 280 | "version": "1.0.5", 281 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", 282 | "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", 283 | "dev": true 284 | }, 285 | "eslint": { 286 | "version": "7.2.0", 287 | "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.2.0.tgz", 288 | "integrity": "sha512-B3BtEyaDKC5MlfDa2Ha8/D6DsS4fju95zs0hjS3HdGazw+LNayai38A25qMppK37wWGWNYSPOR6oYzlz5MHsRQ==", 289 | "dev": true, 290 | "requires": { 291 | "@babel/code-frame": "^7.0.0", 292 | "ajv": "^6.10.0", 293 | "chalk": "^4.0.0", 294 | "cross-spawn": "^7.0.2", 295 | "debug": "^4.0.1", 296 | "doctrine": "^3.0.0", 297 | "eslint-scope": "^5.1.0", 298 | "eslint-utils": "^2.0.0", 299 | "eslint-visitor-keys": "^1.2.0", 300 | "espree": "^7.1.0", 301 | "esquery": "^1.2.0", 302 | "esutils": "^2.0.2", 303 | "file-entry-cache": "^5.0.1", 304 | "functional-red-black-tree": "^1.0.1", 305 | "glob-parent": "^5.0.0", 306 | "globals": "^12.1.0", 307 | "ignore": "^4.0.6", 308 | "import-fresh": "^3.0.0", 309 | "imurmurhash": "^0.1.4", 310 | "inquirer": "^7.0.0", 311 | "is-glob": "^4.0.0", 312 | "js-yaml": "^3.13.1", 313 | "json-stable-stringify-without-jsonify": "^1.0.1", 314 | "levn": "^0.4.1", 315 | "lodash": "^4.17.14", 316 | "minimatch": "^3.0.4", 317 | "natural-compare": "^1.4.0", 318 | "optionator": "^0.9.1", 319 | "progress": "^2.0.0", 320 | "regexpp": "^3.1.0", 321 | "semver": "^7.2.1", 322 | "strip-ansi": "^6.0.0", 323 | "strip-json-comments": "^3.1.0", 324 | "table": "^5.2.3", 325 | "text-table": "^0.2.0", 326 | "v8-compile-cache": "^2.0.3" 327 | } 328 | }, 329 | "eslint-config-prettier": { 330 | "version": "6.11.0", 331 | "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-6.11.0.tgz", 332 | "integrity": "sha512-oB8cpLWSAjOVFEJhhyMZh6NOEOtBVziaqdDQ86+qhDHFbZXoRTM7pNSvFRfW/W/L/LrQ38C99J5CGuRBBzBsdA==", 333 | "dev": true, 334 | "requires": { 335 | "get-stdin": "^6.0.0" 336 | } 337 | }, 338 | "eslint-plugin-prettier": { 339 | "version": "3.1.3", 340 | "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-3.1.3.tgz", 341 | "integrity": "sha512-+HG5jmu/dN3ZV3T6eCD7a4BlAySdN7mLIbJYo0z1cFQuI+r2DiTJEFeF68ots93PsnrMxbzIZ2S/ieX+mkrBeQ==", 342 | "dev": true, 343 | "requires": { 344 | "prettier-linter-helpers": "^1.0.0" 345 | } 346 | }, 347 | "eslint-scope": { 348 | "version": "5.1.0", 349 | "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.0.tgz", 350 | "integrity": "sha512-iiGRvtxWqgtx5m8EyQUJihBloE4EnYeGE/bz1wSPwJE6tZuJUtHlhqDM4Xj2ukE8Dyy1+HCZ4hE0fzIVMzb58w==", 351 | "dev": true, 352 | "requires": { 353 | "esrecurse": "^4.1.0", 354 | "estraverse": "^4.1.1" 355 | } 356 | }, 357 | "eslint-utils": { 358 | "version": "2.0.0", 359 | "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.0.0.tgz", 360 | "integrity": "sha512-0HCPuJv+7Wv1bACm8y5/ECVfYdfsAm9xmVb7saeFlxjPYALefjhbYoCkBjPdPzGH8wWyTpAez82Fh3VKYEZ8OA==", 361 | "dev": true, 362 | "requires": { 363 | "eslint-visitor-keys": "^1.1.0" 364 | } 365 | }, 366 | "eslint-visitor-keys": { 367 | "version": "1.2.0", 368 | "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.2.0.tgz", 369 | "integrity": "sha512-WFb4ihckKil6hu3Dp798xdzSfddwKKU3+nGniKF6HfeW6OLd2OUDEPP7TcHtB5+QXOKg2s6B2DaMPE1Nn/kxKQ==", 370 | "dev": true 371 | }, 372 | "espree": { 373 | "version": "7.1.0", 374 | "resolved": "https://registry.npmjs.org/espree/-/espree-7.1.0.tgz", 375 | "integrity": "sha512-dcorZSyfmm4WTuTnE5Y7MEN1DyoPYy1ZR783QW1FJoenn7RailyWFsq/UL6ZAAA7uXurN9FIpYyUs3OfiIW+Qw==", 376 | "dev": true, 377 | "requires": { 378 | "acorn": "^7.2.0", 379 | "acorn-jsx": "^5.2.0", 380 | "eslint-visitor-keys": "^1.2.0" 381 | } 382 | }, 383 | "esprima": { 384 | "version": "4.0.1", 385 | "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", 386 | "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", 387 | "dev": true 388 | }, 389 | "esquery": { 390 | "version": "1.3.1", 391 | "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.3.1.tgz", 392 | "integrity": "sha512-olpvt9QG0vniUBZspVRN6lwB7hOZoTRtT+jzR+tS4ffYx2mzbw+z0XCOk44aaLYKApNX5nMm+E+P6o25ip/DHQ==", 393 | "dev": true, 394 | "requires": { 395 | "estraverse": "^5.1.0" 396 | }, 397 | "dependencies": { 398 | "estraverse": { 399 | "version": "5.1.0", 400 | "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.1.0.tgz", 401 | "integrity": "sha512-FyohXK+R0vE+y1nHLoBM7ZTyqRpqAlhdZHCWIWEviFLiGB8b04H6bQs8G+XTthacvT8VuwvteiP7RJSxMs8UEw==", 402 | "dev": true 403 | } 404 | } 405 | }, 406 | "esrecurse": { 407 | "version": "4.2.1", 408 | "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz", 409 | "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==", 410 | "dev": true, 411 | "requires": { 412 | "estraverse": "^4.1.0" 413 | } 414 | }, 415 | "estraverse": { 416 | "version": "4.3.0", 417 | "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", 418 | "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", 419 | "dev": true 420 | }, 421 | "esutils": { 422 | "version": "2.0.3", 423 | "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", 424 | "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", 425 | "dev": true 426 | }, 427 | "external-editor": { 428 | "version": "3.1.0", 429 | "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", 430 | "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", 431 | "dev": true, 432 | "requires": { 433 | "chardet": "^0.7.0", 434 | "iconv-lite": "^0.4.24", 435 | "tmp": "^0.0.33" 436 | } 437 | }, 438 | "fast-deep-equal": { 439 | "version": "3.1.1", 440 | "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz", 441 | "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==", 442 | "dev": true 443 | }, 444 | "fast-diff": { 445 | "version": "1.2.0", 446 | "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz", 447 | "integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==", 448 | "dev": true 449 | }, 450 | "fast-json-stable-stringify": { 451 | "version": "2.1.0", 452 | "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", 453 | "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", 454 | "dev": true 455 | }, 456 | "fast-levenshtein": { 457 | "version": "2.0.6", 458 | "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", 459 | "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", 460 | "dev": true 461 | }, 462 | "faye-websocket": { 463 | "version": "0.11.3", 464 | "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.3.tgz", 465 | "integrity": "sha512-D2y4bovYpzziGgbHYtGCMjlJM36vAl/y+xUyn1C+FVx8szd1E+86KwVw6XvYSzOP8iMpm1X0I4xJD+QtUb36OA==", 466 | "requires": { 467 | "websocket-driver": ">=0.5.1" 468 | } 469 | }, 470 | "figures": { 471 | "version": "3.2.0", 472 | "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", 473 | "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", 474 | "dev": true, 475 | "requires": { 476 | "escape-string-regexp": "^1.0.5" 477 | } 478 | }, 479 | "file-entry-cache": { 480 | "version": "5.0.1", 481 | "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-5.0.1.tgz", 482 | "integrity": "sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==", 483 | "dev": true, 484 | "requires": { 485 | "flat-cache": "^2.0.1" 486 | } 487 | }, 488 | "flat-cache": { 489 | "version": "2.0.1", 490 | "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz", 491 | "integrity": "sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==", 492 | "dev": true, 493 | "requires": { 494 | "flatted": "^2.0.0", 495 | "rimraf": "2.6.3", 496 | "write": "1.0.3" 497 | } 498 | }, 499 | "flatted": { 500 | "version": "2.0.2", 501 | "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.2.tgz", 502 | "integrity": "sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==", 503 | "dev": true 504 | }, 505 | "fs.realpath": { 506 | "version": "1.0.0", 507 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 508 | "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", 509 | "dev": true 510 | }, 511 | "functional-red-black-tree": { 512 | "version": "1.0.1", 513 | "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", 514 | "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", 515 | "dev": true 516 | }, 517 | "get-stdin": { 518 | "version": "6.0.0", 519 | "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-6.0.0.tgz", 520 | "integrity": "sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g==", 521 | "dev": true 522 | }, 523 | "glob": { 524 | "version": "7.1.6", 525 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", 526 | "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", 527 | "dev": true, 528 | "requires": { 529 | "fs.realpath": "^1.0.0", 530 | "inflight": "^1.0.4", 531 | "inherits": "2", 532 | "minimatch": "^3.0.4", 533 | "once": "^1.3.0", 534 | "path-is-absolute": "^1.0.0" 535 | } 536 | }, 537 | "glob-parent": { 538 | "version": "5.1.2", 539 | "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", 540 | "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", 541 | "dev": true, 542 | "requires": { 543 | "is-glob": "^4.0.1" 544 | } 545 | }, 546 | "globals": { 547 | "version": "12.4.0", 548 | "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", 549 | "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==", 550 | "dev": true, 551 | "requires": { 552 | "type-fest": "^0.8.1" 553 | } 554 | }, 555 | "has-flag": { 556 | "version": "3.0.0", 557 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", 558 | "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", 559 | "dev": true 560 | }, 561 | "http-parser-js": { 562 | "version": "0.5.2", 563 | "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.2.tgz", 564 | "integrity": "sha512-opCO9ASqg5Wy2FNo7A0sxy71yGbbkJJXLdgMK04Tcypw9jr2MgWbyubb0+WdmDmGnFflO7fRbqbaihh/ENDlRQ==" 565 | }, 566 | "iconv-lite": { 567 | "version": "0.4.24", 568 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", 569 | "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", 570 | "dev": true, 571 | "requires": { 572 | "safer-buffer": ">= 2.1.2 < 3" 573 | } 574 | }, 575 | "ignore": { 576 | "version": "4.0.6", 577 | "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", 578 | "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", 579 | "dev": true 580 | }, 581 | "import-fresh": { 582 | "version": "3.2.1", 583 | "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.1.tgz", 584 | "integrity": "sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ==", 585 | "dev": true, 586 | "requires": { 587 | "parent-module": "^1.0.0", 588 | "resolve-from": "^4.0.0" 589 | } 590 | }, 591 | "imurmurhash": { 592 | "version": "0.1.4", 593 | "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", 594 | "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", 595 | "dev": true 596 | }, 597 | "inflight": { 598 | "version": "1.0.6", 599 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 600 | "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", 601 | "dev": true, 602 | "requires": { 603 | "once": "^1.3.0", 604 | "wrappy": "1" 605 | } 606 | }, 607 | "inherits": { 608 | "version": "2.0.4", 609 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 610 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", 611 | "dev": true 612 | }, 613 | "inquirer": { 614 | "version": "7.1.0", 615 | "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.1.0.tgz", 616 | "integrity": "sha512-5fJMWEmikSYu0nv/flMc475MhGbB7TSPd/2IpFV4I4rMklboCH2rQjYY5kKiYGHqUF9gvaambupcJFFG9dvReg==", 617 | "dev": true, 618 | "requires": { 619 | "ansi-escapes": "^4.2.1", 620 | "chalk": "^3.0.0", 621 | "cli-cursor": "^3.1.0", 622 | "cli-width": "^2.0.0", 623 | "external-editor": "^3.0.3", 624 | "figures": "^3.0.0", 625 | "lodash": "^4.17.15", 626 | "mute-stream": "0.0.8", 627 | "run-async": "^2.4.0", 628 | "rxjs": "^6.5.3", 629 | "string-width": "^4.1.0", 630 | "strip-ansi": "^6.0.0", 631 | "through": "^2.3.6" 632 | }, 633 | "dependencies": { 634 | "ansi-styles": { 635 | "version": "4.2.1", 636 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", 637 | "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", 638 | "dev": true, 639 | "requires": { 640 | "@types/color-name": "^1.1.1", 641 | "color-convert": "^2.0.1" 642 | } 643 | }, 644 | "chalk": { 645 | "version": "3.0.0", 646 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", 647 | "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", 648 | "dev": true, 649 | "requires": { 650 | "ansi-styles": "^4.1.0", 651 | "supports-color": "^7.1.0" 652 | } 653 | }, 654 | "color-convert": { 655 | "version": "2.0.1", 656 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", 657 | "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", 658 | "dev": true, 659 | "requires": { 660 | "color-name": "~1.1.4" 661 | } 662 | }, 663 | "color-name": { 664 | "version": "1.1.4", 665 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", 666 | "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", 667 | "dev": true 668 | }, 669 | "has-flag": { 670 | "version": "4.0.0", 671 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", 672 | "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", 673 | "dev": true 674 | }, 675 | "supports-color": { 676 | "version": "7.1.0", 677 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", 678 | "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", 679 | "dev": true, 680 | "requires": { 681 | "has-flag": "^4.0.0" 682 | } 683 | } 684 | } 685 | }, 686 | "is-extglob": { 687 | "version": "2.1.1", 688 | "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", 689 | "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", 690 | "dev": true 691 | }, 692 | "is-fullwidth-code-point": { 693 | "version": "3.0.0", 694 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", 695 | "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", 696 | "dev": true 697 | }, 698 | "is-glob": { 699 | "version": "4.0.1", 700 | "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", 701 | "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", 702 | "dev": true, 703 | "requires": { 704 | "is-extglob": "^2.1.1" 705 | } 706 | }, 707 | "isexe": { 708 | "version": "2.0.0", 709 | "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", 710 | "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", 711 | "dev": true 712 | }, 713 | "js-tokens": { 714 | "version": "4.0.0", 715 | "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", 716 | "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", 717 | "dev": true 718 | }, 719 | "js-yaml": { 720 | "version": "3.14.0", 721 | "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.0.tgz", 722 | "integrity": "sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==", 723 | "dev": true, 724 | "requires": { 725 | "argparse": "^1.0.7", 726 | "esprima": "^4.0.0" 727 | } 728 | }, 729 | "json-schema-traverse": { 730 | "version": "0.4.1", 731 | "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", 732 | "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", 733 | "dev": true 734 | }, 735 | "json-stable-stringify-without-jsonify": { 736 | "version": "1.0.1", 737 | "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", 738 | "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", 739 | "dev": true 740 | }, 741 | "levn": { 742 | "version": "0.4.1", 743 | "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", 744 | "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", 745 | "dev": true, 746 | "requires": { 747 | "prelude-ls": "^1.2.1", 748 | "type-check": "~0.4.0" 749 | } 750 | }, 751 | "lodash": { 752 | "version": "4.17.21", 753 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", 754 | "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", 755 | "dev": true 756 | }, 757 | "mimic-fn": { 758 | "version": "2.1.0", 759 | "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", 760 | "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", 761 | "dev": true 762 | }, 763 | "minimatch": { 764 | "version": "3.0.4", 765 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", 766 | "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", 767 | "dev": true, 768 | "requires": { 769 | "brace-expansion": "^1.1.7" 770 | } 771 | }, 772 | "minimist": { 773 | "version": "1.2.8", 774 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", 775 | "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", 776 | "dev": true 777 | }, 778 | "mkdirp": { 779 | "version": "0.5.5", 780 | "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", 781 | "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", 782 | "dev": true, 783 | "requires": { 784 | "minimist": "^1.2.5" 785 | } 786 | }, 787 | "ms": { 788 | "version": "2.1.2", 789 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", 790 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" 791 | }, 792 | "mute-stream": { 793 | "version": "0.0.8", 794 | "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", 795 | "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", 796 | "dev": true 797 | }, 798 | "natural-compare": { 799 | "version": "1.4.0", 800 | "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", 801 | "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", 802 | "dev": true 803 | }, 804 | "once": { 805 | "version": "1.4.0", 806 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 807 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 808 | "dev": true, 809 | "requires": { 810 | "wrappy": "1" 811 | } 812 | }, 813 | "onetime": { 814 | "version": "5.1.0", 815 | "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.0.tgz", 816 | "integrity": "sha512-5NcSkPHhwTVFIQN+TUqXoS5+dlElHXdpAWu9I0HP20YOtIi+aZ0Ct82jdlILDxjLEAWwvm+qj1m6aEtsDVmm6Q==", 817 | "dev": true, 818 | "requires": { 819 | "mimic-fn": "^2.1.0" 820 | } 821 | }, 822 | "optionator": { 823 | "version": "0.9.1", 824 | "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", 825 | "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", 826 | "dev": true, 827 | "requires": { 828 | "deep-is": "^0.1.3", 829 | "fast-levenshtein": "^2.0.6", 830 | "levn": "^0.4.1", 831 | "prelude-ls": "^1.2.1", 832 | "type-check": "^0.4.0", 833 | "word-wrap": "^1.2.3" 834 | } 835 | }, 836 | "os-tmpdir": { 837 | "version": "1.0.2", 838 | "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", 839 | "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", 840 | "dev": true 841 | }, 842 | "parent-module": { 843 | "version": "1.0.1", 844 | "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", 845 | "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", 846 | "dev": true, 847 | "requires": { 848 | "callsites": "^3.0.0" 849 | } 850 | }, 851 | "path-is-absolute": { 852 | "version": "1.0.1", 853 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 854 | "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", 855 | "dev": true 856 | }, 857 | "path-key": { 858 | "version": "3.1.1", 859 | "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", 860 | "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", 861 | "dev": true 862 | }, 863 | "prelude-ls": { 864 | "version": "1.2.1", 865 | "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", 866 | "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", 867 | "dev": true 868 | }, 869 | "prettier": { 870 | "version": "2.0.5", 871 | "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.0.5.tgz", 872 | "integrity": "sha512-7PtVymN48hGcO4fGjybyBSIWDsLU4H4XlvOHfq91pz9kkGlonzwTfYkaIEwiRg/dAJF9YlbsduBAgtYLi+8cFg==", 873 | "dev": true 874 | }, 875 | "prettier-linter-helpers": { 876 | "version": "1.0.0", 877 | "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", 878 | "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", 879 | "dev": true, 880 | "requires": { 881 | "fast-diff": "^1.1.2" 882 | } 883 | }, 884 | "progress": { 885 | "version": "2.0.3", 886 | "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", 887 | "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", 888 | "dev": true 889 | }, 890 | "punycode": { 891 | "version": "2.1.1", 892 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", 893 | "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", 894 | "dev": true 895 | }, 896 | "regexpp": { 897 | "version": "3.1.0", 898 | "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.1.0.tgz", 899 | "integrity": "sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q==", 900 | "dev": true 901 | }, 902 | "resolve-from": { 903 | "version": "4.0.0", 904 | "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", 905 | "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", 906 | "dev": true 907 | }, 908 | "restore-cursor": { 909 | "version": "3.1.0", 910 | "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", 911 | "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", 912 | "dev": true, 913 | "requires": { 914 | "onetime": "^5.1.0", 915 | "signal-exit": "^3.0.2" 916 | } 917 | }, 918 | "rimraf": { 919 | "version": "2.6.3", 920 | "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", 921 | "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", 922 | "dev": true, 923 | "requires": { 924 | "glob": "^7.1.3" 925 | } 926 | }, 927 | "run-async": { 928 | "version": "2.4.1", 929 | "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", 930 | "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", 931 | "dev": true 932 | }, 933 | "rxjs": { 934 | "version": "6.5.5", 935 | "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.5.tgz", 936 | "integrity": "sha512-WfQI+1gohdf0Dai/Bbmk5L5ItH5tYqm3ki2c5GdWhKjalzjg93N3avFjVStyZZz+A2Em+ZxKH5bNghw9UeylGQ==", 937 | "dev": true, 938 | "requires": { 939 | "tslib": "^1.9.0" 940 | } 941 | }, 942 | "safe-buffer": { 943 | "version": "5.2.1", 944 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 945 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" 946 | }, 947 | "safer-buffer": { 948 | "version": "2.1.2", 949 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 950 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", 951 | "dev": true 952 | }, 953 | "semver": { 954 | "version": "7.3.2", 955 | "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", 956 | "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==", 957 | "dev": true 958 | }, 959 | "shebang-command": { 960 | "version": "2.0.0", 961 | "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", 962 | "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", 963 | "dev": true, 964 | "requires": { 965 | "shebang-regex": "^3.0.0" 966 | } 967 | }, 968 | "shebang-regex": { 969 | "version": "3.0.0", 970 | "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", 971 | "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", 972 | "dev": true 973 | }, 974 | "signal-exit": { 975 | "version": "3.0.3", 976 | "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", 977 | "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==", 978 | "dev": true 979 | }, 980 | "slice-ansi": { 981 | "version": "2.1.0", 982 | "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz", 983 | "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==", 984 | "dev": true, 985 | "requires": { 986 | "ansi-styles": "^3.2.0", 987 | "astral-regex": "^1.0.0", 988 | "is-fullwidth-code-point": "^2.0.0" 989 | }, 990 | "dependencies": { 991 | "is-fullwidth-code-point": { 992 | "version": "2.0.0", 993 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", 994 | "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", 995 | "dev": true 996 | } 997 | } 998 | }, 999 | "sprintf-js": { 1000 | "version": "1.0.3", 1001 | "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", 1002 | "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", 1003 | "dev": true 1004 | }, 1005 | "string-width": { 1006 | "version": "4.2.0", 1007 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", 1008 | "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", 1009 | "dev": true, 1010 | "requires": { 1011 | "emoji-regex": "^8.0.0", 1012 | "is-fullwidth-code-point": "^3.0.0", 1013 | "strip-ansi": "^6.0.0" 1014 | } 1015 | }, 1016 | "strip-ansi": { 1017 | "version": "6.0.0", 1018 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", 1019 | "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", 1020 | "dev": true, 1021 | "requires": { 1022 | "ansi-regex": "^5.0.0" 1023 | } 1024 | }, 1025 | "strip-json-comments": { 1026 | "version": "3.1.0", 1027 | "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.0.tgz", 1028 | "integrity": "sha512-e6/d0eBu7gHtdCqFt0xJr642LdToM5/cN4Qb9DbHjVx1CP5RyeM+zH7pbecEmDv/lBqb0QH+6Uqq75rxFPkM0w==", 1029 | "dev": true 1030 | }, 1031 | "supports-color": { 1032 | "version": "5.5.0", 1033 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", 1034 | "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", 1035 | "dev": true, 1036 | "requires": { 1037 | "has-flag": "^3.0.0" 1038 | } 1039 | }, 1040 | "table": { 1041 | "version": "5.4.6", 1042 | "resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz", 1043 | "integrity": "sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug==", 1044 | "dev": true, 1045 | "requires": { 1046 | "ajv": "^6.10.2", 1047 | "lodash": "^4.17.14", 1048 | "slice-ansi": "^2.1.0", 1049 | "string-width": "^3.0.0" 1050 | }, 1051 | "dependencies": { 1052 | "ansi-regex": { 1053 | "version": "4.1.1", 1054 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", 1055 | "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", 1056 | "dev": true 1057 | }, 1058 | "emoji-regex": { 1059 | "version": "7.0.3", 1060 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", 1061 | "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", 1062 | "dev": true 1063 | }, 1064 | "is-fullwidth-code-point": { 1065 | "version": "2.0.0", 1066 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", 1067 | "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", 1068 | "dev": true 1069 | }, 1070 | "string-width": { 1071 | "version": "3.1.0", 1072 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", 1073 | "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", 1074 | "dev": true, 1075 | "requires": { 1076 | "emoji-regex": "^7.0.1", 1077 | "is-fullwidth-code-point": "^2.0.0", 1078 | "strip-ansi": "^5.1.0" 1079 | } 1080 | }, 1081 | "strip-ansi": { 1082 | "version": "5.2.0", 1083 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", 1084 | "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", 1085 | "dev": true, 1086 | "requires": { 1087 | "ansi-regex": "^4.1.0" 1088 | } 1089 | } 1090 | } 1091 | }, 1092 | "text-table": { 1093 | "version": "0.2.0", 1094 | "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", 1095 | "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", 1096 | "dev": true 1097 | }, 1098 | "through": { 1099 | "version": "2.3.8", 1100 | "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", 1101 | "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", 1102 | "dev": true 1103 | }, 1104 | "tmp": { 1105 | "version": "0.0.33", 1106 | "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", 1107 | "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", 1108 | "dev": true, 1109 | "requires": { 1110 | "os-tmpdir": "~1.0.2" 1111 | } 1112 | }, 1113 | "tslib": { 1114 | "version": "1.13.0", 1115 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz", 1116 | "integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==", 1117 | "dev": true 1118 | }, 1119 | "type-check": { 1120 | "version": "0.4.0", 1121 | "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", 1122 | "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", 1123 | "dev": true, 1124 | "requires": { 1125 | "prelude-ls": "^1.2.1" 1126 | } 1127 | }, 1128 | "type-fest": { 1129 | "version": "0.8.1", 1130 | "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", 1131 | "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", 1132 | "dev": true 1133 | }, 1134 | "uri-js": { 1135 | "version": "4.2.2", 1136 | "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", 1137 | "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", 1138 | "dev": true, 1139 | "requires": { 1140 | "punycode": "^2.1.0" 1141 | } 1142 | }, 1143 | "uuid": { 1144 | "version": "8.1.0", 1145 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.1.0.tgz", 1146 | "integrity": "sha512-CI18flHDznR0lq54xBycOVmphdCYnQLKn8abKn7PXUiKUGdEd+/l9LWNJmugXel4hXq7S+RMNl34ecyC9TntWg==" 1147 | }, 1148 | "v8-compile-cache": { 1149 | "version": "2.1.1", 1150 | "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.1.1.tgz", 1151 | "integrity": "sha512-8OQ9CL+VWyt3JStj7HX7/ciTL2V3Rl1Wf5OL+SNTm0yK1KvtReVulksyeRnCANHHuUxHlQig+JJDlUhBt1NQDQ==", 1152 | "dev": true 1153 | }, 1154 | "websocket-driver": { 1155 | "version": "0.7.4", 1156 | "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", 1157 | "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", 1158 | "requires": { 1159 | "http-parser-js": ">=0.5.1", 1160 | "safe-buffer": ">=5.1.0", 1161 | "websocket-extensions": ">=0.1.1" 1162 | } 1163 | }, 1164 | "websocket-extensions": { 1165 | "version": "0.1.4", 1166 | "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", 1167 | "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==" 1168 | }, 1169 | "which": { 1170 | "version": "2.0.2", 1171 | "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", 1172 | "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", 1173 | "dev": true, 1174 | "requires": { 1175 | "isexe": "^2.0.0" 1176 | } 1177 | }, 1178 | "word-wrap": { 1179 | "version": "1.2.4", 1180 | "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.4.tgz", 1181 | "integrity": "sha512-2V81OA4ugVo5pRo46hAoD2ivUJx8jXmWXfUkY4KFNw0hEptvN0QfH3K4nHiwzGeKl5rFKedV48QVoqYavy4YpA==", 1182 | "dev": true 1183 | }, 1184 | "wrappy": { 1185 | "version": "1.0.2", 1186 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 1187 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", 1188 | "dev": true 1189 | }, 1190 | "write": { 1191 | "version": "1.0.3", 1192 | "resolved": "https://registry.npmjs.org/write/-/write-1.0.3.tgz", 1193 | "integrity": "sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==", 1194 | "dev": true, 1195 | "requires": { 1196 | "mkdirp": "^0.5.1" 1197 | } 1198 | } 1199 | } 1200 | } 1201 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sockjs", 3 | "description": "SockJS-node is a server counterpart of SockJS-client a JavaScript library that provides a WebSocket-like object in the browser. SockJS gives you a coherent, cross-browser, Javascript API which creates a low latency, full duplex, cross-domain communication channel between the browser and the web server.", 4 | "version": "0.4.0-rc.1", 5 | "author": "Bryce Kahle", 6 | "bugs": { 7 | "url": "https://github.com/sockjs/sockjs-node/issues" 8 | }, 9 | "contributors": [ 10 | { 11 | "name": "Bryce Kahle", 12 | "email": "bkahle@gmail.com" 13 | }, 14 | { 15 | "name": "Marek Majkowski", 16 | "email": "deadbeef@popcount.org" 17 | } 18 | ], 19 | "dependencies": { 20 | "debug": "^4.3.1", 21 | "faye-websocket": "^0.11.3", 22 | "uuid": "^8.1.0" 23 | }, 24 | "devDependencies": { 25 | "eslint": "^7.2.0", 26 | "eslint-config-prettier": "^6.11.0", 27 | "eslint-plugin-prettier": "^3.1.3", 28 | "prettier": "^2.0.5" 29 | }, 30 | "homepage": "https://github.com/sockjs/sockjs-node", 31 | "keywords": [ 32 | "websockets", 33 | "websocket" 34 | ], 35 | "license": "MIT", 36 | "main": "index.js", 37 | "repository": { 38 | "type": "git", 39 | "url": "https://github.com/sockjs/sockjs-node.git" 40 | }, 41 | "scripts": { 42 | "lint": "eslint .", 43 | "fmt": "prettier --write '{lib,examples,tests}/**/*.js'", 44 | "version": "git add CHANGELOG.MD", 45 | "postversion": "npm publish", 46 | "postpublish": "git push origin --all && git push origin --tags" 47 | }, 48 | "engines": { 49 | "node": ">=6.5.0" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /scripts/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | rm -rf sockjs-protocol 6 | git clone --depth=1 https://github.com/sockjs/sockjs-protocol.git 7 | cd sockjs-protocol 8 | make test_deps pycco_deps 9 | cd .. 10 | node tests/test_server/server.js & 11 | SRVPID=$! 12 | sleep 1 13 | 14 | set +e 15 | 16 | cd sockjs-protocol 17 | ./venv/bin/python sockjs-protocol.py 18 | PASSED=$? 19 | kill $SRVPID 20 | exit $PASSED 21 | -------------------------------------------------------------------------------- /tests/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "no-console": 0 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /tests/test_server/README.md: -------------------------------------------------------------------------------- 1 | SockJS test server 2 | ================== 3 | 4 | In order to test sockjs server implementation the server needs to 5 | provide a standarized sockjs endpoint that will can used by various 6 | sockjs-related tests. For example by QUnit or 7 | [Sockjs-protocol](https://github.com/sockjs/sockjs-protocol) tests. 8 | 9 | This small code does exactly that - runs a simple server that supports 10 | the following SockJS services: 11 | 12 | * `/echo` 13 | * `/disabled_websocket_echo` 14 | * `/cookie_needed_echo` 15 | * `/close` 16 | * `/ticker` 17 | * `/amplify` 18 | * `/broadcast` 19 | 20 | If you just want to quickly run it: 21 | 22 | node server.js 23 | -------------------------------------------------------------------------------- /tests/test_server/config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const debug = require('debug')('sockjs:test-server:app'); 4 | 5 | exports.config = { 6 | server_opts: { 7 | sockjs_url: 'http://localhost:8080/lib/sockjs.js', 8 | websocket: true, 9 | log: (x, ...rest) => debug(`[${x}]`, ...rest) 10 | }, 11 | 12 | port: 8081 13 | }; 14 | -------------------------------------------------------------------------------- /tests/test_server/server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const http = require('http'); 3 | const config = require('./config').config; 4 | const sockjs_app = require('./sockjs_app'); 5 | 6 | const server = http.createServer(); 7 | server.addListener('request', function (req, res) { 8 | res.setHeader('Content-Type', 'text/plain'); 9 | res.writeHead(404); 10 | res.end('404 - Nothing here (via sockjs-node test_server)'); 11 | }); 12 | server.addListener('upgrade', function (req, res) { 13 | res.end(); 14 | }); 15 | 16 | sockjs_app.install(config.server_opts, server); 17 | 18 | server.listen(config.port, config.host, () => { 19 | const addr = server.address(); 20 | console.log(` [*] Listening on ${addr.address}:${addr.port}`); 21 | }); 22 | -------------------------------------------------------------------------------- /tests/test_server/sockjs_app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const sockjs = require('../../index'); 3 | const debug = require('debug')('sockjs:test-server:app'); 4 | 5 | exports.install = function (opts, server) { 6 | const echoHandler = function (conn) { 7 | debug(` [+] echo open ${conn}`); 8 | conn.on('close', function () { 9 | debug(` [-] echo close ${conn}`); 10 | }); 11 | conn.on('data', function (m) { 12 | const d = JSON.stringify(m); 13 | debug(` [ ] echo message ${conn} ${d.slice(0, 64)}${d.length > 64 ? '...' : ''}`); 14 | conn.write(m); 15 | }); 16 | }; 17 | 18 | const sjs_echo = sockjs.createServer( 19 | Object.assign({}, opts, { prefix: '/echo', response_limit: 4096 }) 20 | ); 21 | sjs_echo.on('connection', echoHandler); 22 | sjs_echo.attach(server); 23 | 24 | const sjs_echo2 = sockjs.createServer( 25 | Object.assign({}, opts, { 26 | prefix: '/disabled_websocket_echo', 27 | websocket: false 28 | }) 29 | ); 30 | sjs_echo2.on('connection', echoHandler); 31 | sjs_echo2.attach(server); 32 | 33 | const sjs_echo3 = sockjs.createServer( 34 | Object.assign({}, opts, { prefix: '/cookie_needed_echo', jsessionid: true }) 35 | ); 36 | sjs_echo3.on('connection', echoHandler); 37 | sjs_echo3.attach(server); 38 | 39 | const sjs_close = sockjs.createServer(Object.assign({}, opts, { prefix: '/close' })); 40 | sjs_close.on('connection', function (conn) { 41 | debug(` [+] close open ${conn}`); 42 | conn.close(3000, 'Go away!'); 43 | conn.on('close', function () { 44 | debug(` [-] close close ${conn}`); 45 | }); 46 | }); 47 | sjs_close.attach(server); 48 | 49 | const sjs_ticker = sockjs.createServer(Object.assign({}, opts, { prefix: '/ticker' })); 50 | sjs_ticker.on('connection', function (conn) { 51 | debug(` [+] ticker open ${conn}`); 52 | let tref; 53 | const schedule = function () { 54 | conn.write('tick!'); 55 | tref = setTimeout(schedule, 1000); 56 | }; 57 | tref = setTimeout(schedule, 1000); 58 | conn.on('close', function () { 59 | clearTimeout(tref); 60 | debug(` [-] ticker close ${conn}`); 61 | }); 62 | }); 63 | sjs_ticker.attach(server); 64 | 65 | const broadcast = {}; 66 | const sjs_broadcast = sockjs.createServer(Object.assign({}, opts, { prefix: '/broadcast' })); 67 | sjs_broadcast.on('connection', function (conn) { 68 | debug(` [+] broadcast open ${conn}`); 69 | broadcast[conn.id] = conn; 70 | conn.on('close', function () { 71 | delete broadcast[conn.id]; 72 | debug(` [-] broadcast close${conn}`); 73 | }); 74 | conn.on('data', function (m) { 75 | debug(` [-] broadcast message ${m}`); 76 | for (const id in broadcast) { 77 | broadcast[id].write(m); 78 | } 79 | }); 80 | }); 81 | sjs_broadcast.attach(server); 82 | 83 | const sjs_amplify = sockjs.createServer(Object.assign({}, opts, { prefix: '/amplify' })); 84 | sjs_amplify.on('connection', function (conn) { 85 | debug(` [+] amp open ${conn}`); 86 | conn.on('close', function () { 87 | debug(` [-] amp close ${conn}`); 88 | }); 89 | conn.on('data', function (m) { 90 | let n = Math.floor(Number(m)); 91 | n = n > 0 && n < 19 ? n : 1; 92 | debug(` [ ] amp message: 2^${n}`); 93 | conn.write(new Array(Math.pow(2, n) + 1).join('x')); 94 | }); 95 | }); 96 | sjs_amplify.attach(server); 97 | }; 98 | --------------------------------------------------------------------------------