├── Documentation~ ├── index.md └── images │ ├── node-app-console.png │ ├── package-manager-add.png │ ├── package-manager-menu.png │ ├── server-startup-console.png │ └── package-manager-selection-dialog.png ├── ChannelClientNodeApp~ ├── node_modules │ ├── async-limiter │ │ ├── .eslintignore │ │ ├── .travis.yml │ │ ├── .nycrc │ │ ├── LICENSE │ │ ├── index.js │ │ ├── package.json │ │ └── readme.md │ └── ws │ │ ├── index.js │ │ ├── lib │ │ ├── constants.js │ │ ├── validation.js │ │ ├── buffer-util.js │ │ ├── event-target.js │ │ ├── extension.js │ │ ├── websocket-server.js │ │ ├── sender.js │ │ ├── receiver.js │ │ ├── permessage-deflate.js │ │ └── websocket.js │ │ ├── LICENSE │ │ ├── package.json │ │ └── README.md ├── package.json ├── index.html ├── package-lock.json ├── main.js ├── index.js └── event-service.js ├── .yamato └── has_moved.md ├── CHANGELOG.md.meta ├── LICENSE.md.meta ├── README.md.meta ├── package.json.meta ├── Editor.meta ├── .gitignore ├── CHANGELOG.md ├── Editor ├── Unity.ChannelClientExamples.Editor.asmdef ├── Unity.ChannelClientExamples.Editor.asmdef.meta ├── ChannelServiceAPI.cs.meta ├── ChannelServiceAPIExample.cs.meta ├── EventServiceDocExample.cs.meta ├── EventServiceExampleWindow.cs.meta ├── ChannelCommunicationDocExample.cs.meta ├── EventServiceExampleWindow.cs ├── EventServiceDocExample.cs ├── ChannelServiceAPIExample.cs ├── ChannelCommunicationDocExample.cs └── ChannelServiceAPI.cs ├── package.json ├── .npmignore ├── LICENSE.md └── README.md /Documentation~/index.md: -------------------------------------------------------------------------------- 1 | # Package documentation guides 2 | 3 | -------------------------------------------------------------------------------- /ChannelClientNodeApp~/node_modules/async-limiter/.eslintignore: -------------------------------------------------------------------------------- 1 | coverage 2 | .nyc_output -------------------------------------------------------------------------------- /.yamato/has_moved.md: -------------------------------------------------------------------------------- 1 | Please refer to https://github.cds.internal.unity3d.com/unity/upm-ci-yamato-templates -------------------------------------------------------------------------------- /Documentation~/images/node-app-console.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unity-Technologies/com.unity.channel-client-examples/master/Documentation~/images/node-app-console.png -------------------------------------------------------------------------------- /Documentation~/images/package-manager-add.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unity-Technologies/com.unity.channel-client-examples/master/Documentation~/images/package-manager-add.png -------------------------------------------------------------------------------- /Documentation~/images/package-manager-menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unity-Technologies/com.unity.channel-client-examples/master/Documentation~/images/package-manager-menu.png -------------------------------------------------------------------------------- /Documentation~/images/server-startup-console.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unity-Technologies/com.unity.channel-client-examples/master/Documentation~/images/server-startup-console.png -------------------------------------------------------------------------------- /CHANGELOG.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: ece22e426d63f784e82c0d084bb94467 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /LICENSE.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: f7b70a4f1c1072f46b8a5adb5282cfa1 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /README.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 29fd65ce760125347b4a1ca81192df80 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Documentation~/images/package-manager-selection-dialog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unity-Technologies/com.unity.channel-client-examples/master/Documentation~/images/package-manager-selection-dialog.png -------------------------------------------------------------------------------- /package.json.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 26685444539b96f44a54356a052eb8fd 3 | PackageManifestImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /ChannelClientNodeApp~/node_modules/async-limiter/.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "6" 4 | - "8" 5 | - "10" 6 | - "node" 7 | script: npm run travis 8 | cache: 9 | yarn: true 10 | -------------------------------------------------------------------------------- /Editor.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 05c58485d50fa1e4ebad4a67ee32177d 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | artifacts/** 2 | build/** 3 | .build_script/** 4 | node_modules/** 5 | .DS_Store 6 | .npmrc 7 | !Documentation~ 8 | !.Documentation 9 | npm-debug.log 10 | build.sh.meta 11 | build.bat.meta 12 | .idea/ 13 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [0.1.0] - 2020-03-05 4 | ### This is the first release of com.unity.channel-client-examples 5 | Initial release of the package with a node application example and a Unity Handlers. 6 | 7 | -------------------------------------------------------------------------------- /ChannelClientNodeApp~/node_modules/async-limiter/.nycrc: -------------------------------------------------------------------------------- 1 | { 2 | "check-coverage": false, 3 | "lines": 99, 4 | "statements": 99, 5 | "functions": 99, 6 | "branches": 99, 7 | "include": [ 8 | "index.js" 9 | ] 10 | } -------------------------------------------------------------------------------- /Editor/Unity.ChannelClientExamples.Editor.asmdef: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Unity.ChannelService.Editor", 3 | "references": [ 4 | ], 5 | "includePlatforms": [ 6 | "Editor" 7 | ], 8 | "excludePlatforms": [] 9 | } 10 | -------------------------------------------------------------------------------- /Editor/Unity.ChannelClientExamples.Editor.asmdef.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: bac489cecddbe2d498ac45970e85400a 3 | AssemblyDefinitionImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /ChannelClientNodeApp~/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "unity-connect-example", 3 | "version": "0.0.1", 4 | "description": "An example of connecting to the ", 5 | "main": "index.js", 6 | "dependencies": { 7 | "ws": "^5.2.0" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /ChannelClientNodeApp~/node_modules/ws/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const WebSocket = require('./lib/websocket'); 4 | 5 | WebSocket.Server = require('./lib/websocket-server'); 6 | WebSocket.Receiver = require('./lib/receiver'); 7 | WebSocket.Sender = require('./lib/sender'); 8 | 9 | module.exports = WebSocket; 10 | -------------------------------------------------------------------------------- /Editor/ChannelServiceAPI.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: fa789809f10b5e54f820de0f68ed2d52 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "com.unity.channel-client-examples", 3 | "displayName":"Unity Channel Communication", 4 | "version": "0.1.0-preview", 5 | "unity": "2020.1", 6 | "description": "Provide a public API and examples of using the Unity ChannelService and ChannelClient.", 7 | "dependencies": { 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Editor/ChannelServiceAPIExample.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: fc6863c930a77fe41ab55782c535d96d 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/EventServiceDocExample.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 1f8e14691b0516543a3614c54f867c9c 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/EventServiceExampleWindow.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: eb682b1cfc8798c41b1b347d2d7a2e06 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/ChannelCommunicationDocExample.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: f559317f37ae4b144bfc0bcb36ab2723 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /ChannelClientNodeApp~/node_modules/ws/lib/constants.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | BINARY_TYPES: ['nodebuffer', 'arraybuffer', 'fragments'], 5 | GUID: '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', 6 | kStatusCode: Symbol('status-code'), 7 | kWebSocket: Symbol('websocket'), 8 | EMPTY_BUFFER: Buffer.alloc(0), 9 | NOOP: () => {} 10 | }; 11 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | artifacts/** 2 | build/** 3 | .build_script/** 4 | node_modules/** 5 | Documentation/ApiDocs/** 6 | Documentation~/ApiDocs/** 7 | .DS_Store 8 | .npmrc 9 | .npmignore 10 | .gitignore 11 | CONTRIBUTING.md 12 | CONTRIBUTING.md.meta 13 | QAReport.md 14 | QAReport.md.meta 15 | .gitlab-ci.yml 16 | build.sh 17 | build.sh.meta 18 | build.bat 19 | build.bat.meta 20 | -------------------------------------------------------------------------------- /ChannelClientNodeApp~/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |

Test!

7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | [com.unity.channel-client-examples] copyright © [2020] Unity Technologies ApS 2 | 3 | Licensed under the Unity Companion License for Unity-dependent projects--see [Unity Companion License](http://www.unity3d.com/legal/licenses/Unity_Companion_License). 4 | 5 | Unless expressly provided otherwise, the Software under this license is made available strictly on an “AS IS” BASIS WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED. Please review the license for details on these and other terms and conditions. 6 | -------------------------------------------------------------------------------- /ChannelClientNodeApp~/node_modules/ws/lib/validation.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | try { 4 | const isValidUTF8 = require('utf-8-validate'); 5 | 6 | exports.isValidUTF8 = typeof isValidUTF8 === 'object' 7 | ? isValidUTF8.Validation.isValidUTF8 // utf-8-validate@<3.0.0 8 | : isValidUTF8; 9 | } catch (e) /* istanbul ignore next */ { 10 | exports.isValidUTF8 = () => true; 11 | } 12 | 13 | /** 14 | * Checks if a status code is allowed in a close frame. 15 | * 16 | * @param {Number} code The status code 17 | * @return {Boolean} `true` if the status code is valid, else `false` 18 | * @public 19 | */ 20 | exports.isValidStatusCode = (code) => { 21 | return ( 22 | (code >= 1000 && 23 | code <= 1013 && 24 | code !== 1004 && 25 | code !== 1005 && 26 | code !== 1006) || 27 | (code >= 3000 && code <= 4999) 28 | ); 29 | }; 30 | -------------------------------------------------------------------------------- /ChannelClientNodeApp~/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "unity-connect-example", 3 | "version": "0.0.1", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "async-limiter": { 8 | "version": "1.0.1", 9 | "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", 10 | "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==" 11 | }, 12 | "ws": { 13 | "version": "5.2.2", 14 | "resolved": "https://registry.npmjs.org/ws/-/ws-5.2.2.tgz", 15 | "integrity": "sha512-jaHFD6PFv6UgoIVda6qZllptQsMlDEJkTQcybzzXDYM1XO9Y8em691FGMPmM46WGyLU4z9KMgQN+qrux/nhlHA==", 16 | "requires": { 17 | "async-limiter": "~1.0.0" 18 | } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /ChannelClientNodeApp~/node_modules/async-limiter/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2017 Samuel Reed 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 9 | -------------------------------------------------------------------------------- /ChannelClientNodeApp~/node_modules/ws/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2011 Einar Otto Stangvik 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /ChannelClientNodeApp~/node_modules/async-limiter/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function Queue(options) { 4 | if (!(this instanceof Queue)) { 5 | return new Queue(options); 6 | } 7 | 8 | options = options || {}; 9 | this.concurrency = options.concurrency || Infinity; 10 | this.pending = 0; 11 | this.jobs = []; 12 | this.cbs = []; 13 | this._done = done.bind(this); 14 | } 15 | 16 | var arrayAddMethods = [ 17 | 'push', 18 | 'unshift', 19 | 'splice' 20 | ]; 21 | 22 | arrayAddMethods.forEach(function(method) { 23 | Queue.prototype[method] = function() { 24 | var methodResult = Array.prototype[method].apply(this.jobs, arguments); 25 | this._run(); 26 | return methodResult; 27 | }; 28 | }); 29 | 30 | Object.defineProperty(Queue.prototype, 'length', { 31 | get: function() { 32 | return this.pending + this.jobs.length; 33 | } 34 | }); 35 | 36 | Queue.prototype._run = function() { 37 | if (this.pending === this.concurrency) { 38 | return; 39 | } 40 | if (this.jobs.length) { 41 | var job = this.jobs.shift(); 42 | this.pending++; 43 | job(this._done); 44 | this._run(); 45 | } 46 | 47 | if (this.pending === 0) { 48 | while (this.cbs.length !== 0) { 49 | var cb = this.cbs.pop(); 50 | process.nextTick(cb); 51 | } 52 | } 53 | }; 54 | 55 | Queue.prototype.onDone = function(cb) { 56 | if (typeof cb === 'function') { 57 | this.cbs.push(cb); 58 | this._run(); 59 | } 60 | }; 61 | 62 | function done() { 63 | this.pending--; 64 | this._run(); 65 | } 66 | 67 | module.exports = Queue; 68 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Channel Client Examples 2 | 3 | ## Code Walkthrough 4 | - Make a public API for the ChannelService (currently internal). See `Editor/ChannelServiceAPI.cs` 5 | - Currently in trunk all ChannelService API are internals. 6 | - Make an example implementing 2 new routes, one binary and the other using string. See See `Editor/ChannelServiceExample.cs` 7 | - This adds a new menu item: `Tools/Register Custom Ping Pong Channels` that can be use to restart the server and register all listeners. 8 | - `ChannelClientNodeApp~` contains a node application that will connect to Unity on the specified routes and send a bit of data. 9 | 10 | ## How to connect? 11 | 12 | - By default, if you use this package the `ChannelService` will be started (basically the Websocket server will be listening for connection) automatically and some handlers will be registered each domain reload. 13 | - The connection address in is : 127.0.0.1:XXXX. Connecting port is dynamic (!). *This should be streamlined...* 14 | - To bypass the fact that the port is dynamic, the StartChannelService function of `ChannelServiceAPI` creates a file `/Local/Unity/Editor/ChannelService.info` and writes the address and port in that file. 15 | 16 | ## The Demo 17 | 18 | 1) Start Unity 19 | 2) Ensure you add the `com.unity.channelservice` package. 20 | 21 | ![pm](Documentation~/images/package-manager-menu.png) 22 | ![pm](Documentation~/images/package-manager-add.png) 23 | ![pm](Documentation~/images/package-manager-selection-dialog.png) 24 | 25 | 3) See that the console prints these messages: 26 | 27 | ![pm](Documentation~/images/server-startup-console.png) 28 | 29 | 4) Install [nodejs](https://nodejs.org/) 30 | 31 | 5) Open a command console and be sure to `cd` to the `ChannelClientNodeApp~`. Type `node index.js`. 32 | 33 | 6) You should see the following: 34 | 35 | ![pm](Documentation~/images/node-app-console.png) -------------------------------------------------------------------------------- /ChannelClientNodeApp~/main.js: -------------------------------------------------------------------------------- 1 | import * as eventService from "./event-service.js" 2 | 3 | const kWebEmit1 = "webEmit1"; 4 | const kWebRequest1 = "webRequest1"; 5 | const kWebRequest2 = "webRequest2"; 6 | const kUnityEmit1 = "unityEmit1"; 7 | const kUnityRequest1 = "unityRequest1"; 8 | 9 | // Setup UI 10 | document.getElementById("emit1").addEventListener("click", OnEmit1); 11 | document.getElementById("request1").addEventListener("click", OnRequest1); 12 | document.getElementById("request2").addEventListener("click", OnRequest2); 13 | document.getElementById("log1").addEventListener("click", OnLog1); 14 | 15 | // Setup EventService 16 | eventService.Start(54209); 17 | let unityEmit1OffHandler = eventService.On(kUnityEmit1, (eventType, data) => { 18 | console.log(`On ${kUnityEmit1}: [${eventType}] ${data.join(",")}`) 19 | unityEmit1OffHandler(); 20 | }); 21 | let unityRequest1OffHandler = eventService.On(kUnityRequest1, (eventType, data) => { 22 | console.log(`On ${kUnityRequest1}: [${eventType}] ${data.join(",")}`) 23 | return [1, "2", 3.0]; 24 | }); 25 | 26 | function OnEmit1() 27 | { 28 | eventService.Emit(kWebEmit1, [1, "2", 3.0]); 29 | } 30 | 31 | function OnRequest1() 32 | { 33 | eventService.Request(kWebRequest1, (err, data) => { 34 | if (err) { 35 | console.error(err); 36 | return; 37 | } 38 | console.log(`On receiving ${kWebRequest1}: ${data.join(",")}`); 39 | }, [1, "2", 3.0]); 40 | } 41 | 42 | function OnRequest2() 43 | { 44 | eventService.Request(kWebRequest2, (err, data) => { 45 | if (!err) { 46 | console.error(`On ${kWebRequest2}: there should have been an error.`); 47 | return; 48 | } 49 | console.log(err); 50 | }, ["123"], 100) 51 | } 52 | 53 | function OnLog1() 54 | { 55 | eventService.Log("This tests the EventService.Log() function."); 56 | } 57 | -------------------------------------------------------------------------------- /ChannelClientNodeApp~/index.js: -------------------------------------------------------------------------------- 1 | const WebSocket = require('ws'); 2 | const fs = require('fs'); 3 | const path = require('path'); 4 | 5 | const channelServiceInfoFile = path.join(process.env.LOCALAPPDATA, "Unity", "Editor", "ChannelService.info"); 6 | 7 | function unityChannelConnect(name, isBinary, readyForConnectionCallback) 8 | { 9 | if (!fs.existsSync(channelServiceInfoFile)) 10 | throw `${channelServiceInfoFile} doesn't exists. ChannelService is not started.` 11 | 12 | const addr = fs.readFileSync(channelServiceInfoFile); 13 | const connectTo = `ws://${addr}/${name}`; 14 | console.log("Trying to connect to " + connectTo); 15 | 16 | // Note that the port is dynamic... 17 | const socket = new WebSocket(connectTo); 18 | if (isBinary) 19 | socket.binaryType = 'arraybuffer'; 20 | 21 | const binStatus = isBinary ? "binary" : ""; 22 | 23 | var isReady = false; 24 | 25 | // Connection opened 26 | socket.addEventListener('open', function (event) { 27 | console.log(`[${name}] Connected ${binStatus}`); 28 | }); 29 | 30 | socket.addEventListener('close', function (event) { 31 | console.log(`[${name}] Closed ${binStatus}`); 32 | }); 33 | 34 | // Listen for messages 35 | socket.addEventListener('message', function (event) { 36 | if (!isReady) 37 | { 38 | isReady = true; 39 | readyForConnectionCallback(); 40 | } 41 | 42 | if (isBinary) 43 | console.log(`[${name} - binary] ${event.data}`); 44 | else 45 | console.log(`[${name}] ${event.data}`); 46 | }); 47 | 48 | return socket; 49 | } 50 | 51 | var binaryConnection = unityChannelConnect("custom_binary_ping_pong", true, () => { 52 | const array = new Int32Array(5); 53 | for (var i = 0; i < array.length; ++i) { 54 | array[i] = i; 55 | } 56 | binaryConnection.send(array); 57 | }); 58 | 59 | var stringConnection = unityChannelConnect("custom_ascii_ping_pong", false, () => { 60 | stringConnection.send("hello ascii world!!!"); 61 | }); -------------------------------------------------------------------------------- /Editor/EventServiceExampleWindow.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Unity.MPE; 3 | using UnityEditor; 4 | using UnityEngine; 5 | using UnityEngine.UIElements; 6 | 7 | public class EventServiceExampleWindow : EditorWindow 8 | { 9 | const string k_WebEmit1 = "webEmit1"; 10 | const string k_WebRequest1 = "webRequest1"; 11 | const string k_UnityEmit1 = "unityEmit1"; 12 | const string k_UnityRequest1 = "unityRequest1"; 13 | 14 | Action m_WebEmit1Off = () => { }; 15 | Action m_WebRequest1Off = () => { }; 16 | 17 | [MenuItem("Tools/Open EventServiceExample Window")] 18 | static void Init() 19 | { 20 | GetWindow(); 21 | } 22 | 23 | void OnEnable() 24 | { 25 | m_WebEmit1Off = EventService.On(k_WebEmit1, (type, data) => 26 | { 27 | Debug.Log($"On {k_WebEmit1}: [{type}] {string.Join(",", data)}"); 28 | }); 29 | 30 | m_WebRequest1Off = EventService.On(k_WebRequest1, (type, data) => 31 | { 32 | Debug.Log($"On {k_WebRequest1}: [{type}] {string.Join(",", data)}"); 33 | return new object[] { "test", 42, 123.4f }; 34 | }); 35 | 36 | var emit1Button = new Button(EmitUnity1); 37 | emit1Button.text = "Test Emit 1"; 38 | rootVisualElement.Add(emit1Button); 39 | 40 | var request1Button = new Button(RequestUnity1); 41 | request1Button.text = "Test Request 1"; 42 | rootVisualElement.Add(request1Button); 43 | } 44 | 45 | void OnDisable() 46 | { 47 | m_WebEmit1Off(); 48 | m_WebRequest1Off(); 49 | } 50 | 51 | static void EmitUnity1() 52 | { 53 | EventService.Emit(k_UnityEmit1, new object[] {"test", 42}); 54 | } 55 | 56 | static void RequestUnity1() 57 | { 58 | EventService.Request(k_UnityRequest1, (err, data) => 59 | { 60 | if (err != null) 61 | { 62 | Debug.LogException(err); 63 | return; 64 | } 65 | Debug.Log($"On receiving {k_UnityRequest1}: {string.Join(",", data)}"); 66 | }, new object[] { "test", 42 }); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /ChannelClientNodeApp~/node_modules/ws/lib/buffer-util.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Merges an array of buffers into a new buffer. 5 | * 6 | * @param {Buffer[]} list The array of buffers to concat 7 | * @param {Number} totalLength The total length of buffers in the list 8 | * @return {Buffer} The resulting buffer 9 | * @public 10 | */ 11 | function concat (list, totalLength) { 12 | const target = Buffer.allocUnsafe(totalLength); 13 | var offset = 0; 14 | 15 | for (var i = 0; i < list.length; i++) { 16 | const buf = list[i]; 17 | buf.copy(target, offset); 18 | offset += buf.length; 19 | } 20 | 21 | return target; 22 | } 23 | 24 | /** 25 | * Masks a buffer using the given mask. 26 | * 27 | * @param {Buffer} source The buffer to mask 28 | * @param {Buffer} mask The mask to use 29 | * @param {Buffer} output The buffer where to store the result 30 | * @param {Number} offset The offset at which to start writing 31 | * @param {Number} length The number of bytes to mask. 32 | * @public 33 | */ 34 | function _mask (source, mask, output, offset, length) { 35 | for (var i = 0; i < length; i++) { 36 | output[offset + i] = source[i] ^ mask[i & 3]; 37 | } 38 | } 39 | 40 | /** 41 | * Unmasks a buffer using the given mask. 42 | * 43 | * @param {Buffer} buffer The buffer to unmask 44 | * @param {Buffer} mask The mask to use 45 | * @public 46 | */ 47 | function _unmask (buffer, mask) { 48 | // Required until https://github.com/nodejs/node/issues/9006 is resolved. 49 | const length = buffer.length; 50 | for (var i = 0; i < length; i++) { 51 | buffer[i] ^= mask[i & 3]; 52 | } 53 | } 54 | 55 | try { 56 | const bufferUtil = require('bufferutil'); 57 | const bu = bufferUtil.BufferUtil || bufferUtil; 58 | 59 | module.exports = { 60 | mask (source, mask, output, offset, length) { 61 | if (length < 48) _mask(source, mask, output, offset, length); 62 | else bu.mask(source, mask, output, offset, length); 63 | }, 64 | unmask (buffer, mask) { 65 | if (buffer.length < 32) _unmask(buffer, mask); 66 | else bu.unmask(buffer, mask); 67 | }, 68 | concat 69 | }; 70 | } catch (e) /* istanbul ignore next */ { 71 | module.exports = { concat, mask: _mask, unmask: _unmask }; 72 | } 73 | -------------------------------------------------------------------------------- /ChannelClientNodeApp~/node_modules/async-limiter/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "_args": [ 3 | [ 4 | "async-limiter@1.0.1", 5 | "D:\\work\\code\\com.unity.channelservice\\ChannelClientNodeApp~" 6 | ] 7 | ], 8 | "_from": "async-limiter@1.0.1", 9 | "_id": "async-limiter@1.0.1", 10 | "_inBundle": false, 11 | "_integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==", 12 | "_location": "/async-limiter", 13 | "_phantomChildren": {}, 14 | "_requested": { 15 | "type": "version", 16 | "registry": true, 17 | "raw": "async-limiter@1.0.1", 18 | "name": "async-limiter", 19 | "escapedName": "async-limiter", 20 | "rawSpec": "1.0.1", 21 | "saveSpec": null, 22 | "fetchSpec": "1.0.1" 23 | }, 24 | "_requiredBy": [ 25 | "/ws" 26 | ], 27 | "_resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", 28 | "_spec": "1.0.1", 29 | "_where": "D:\\work\\code\\com.unity.channelservice\\ChannelClientNodeApp~", 30 | "author": { 31 | "name": "Samuel Reed" 32 | }, 33 | "bugs": { 34 | "url": "https://github.com/strml/async-limiter/issues" 35 | }, 36 | "dependencies": {}, 37 | "description": "asynchronous function queue with adjustable concurrency", 38 | "devDependencies": { 39 | "coveralls": "^3.0.3", 40 | "eslint": "^5.16.0", 41 | "eslint-plugin-mocha": "^5.3.0", 42 | "intelli-espower-loader": "^1.0.1", 43 | "mocha": "^6.1.4", 44 | "nyc": "^14.1.1", 45 | "power-assert": "^1.6.1" 46 | }, 47 | "homepage": "https://github.com/strml/async-limiter#readme", 48 | "keywords": [ 49 | "throttle", 50 | "async", 51 | "limiter", 52 | "asynchronous", 53 | "job", 54 | "task", 55 | "concurrency", 56 | "concurrent" 57 | ], 58 | "license": "MIT", 59 | "name": "async-limiter", 60 | "repository": { 61 | "type": "git", 62 | "url": "git+https://github.com/strml/async-limiter.git" 63 | }, 64 | "scripts": { 65 | "coverage": "nyc npm test && nyc report --reporter=text-lcov | coveralls", 66 | "example": "node example", 67 | "lint": "eslint .", 68 | "test": "mocha --require intelli-espower-loader test/", 69 | "travis": "npm run lint && npm run test" 70 | }, 71 | "version": "1.0.1" 72 | } 73 | -------------------------------------------------------------------------------- /ChannelClientNodeApp~/node_modules/ws/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "_args": [ 3 | [ 4 | "ws@5.2.2", 5 | "D:\\work\\code\\com.unity.channelservice\\ChannelClientNodeApp~" 6 | ] 7 | ], 8 | "_from": "ws@5.2.2", 9 | "_id": "ws@5.2.2", 10 | "_inBundle": false, 11 | "_integrity": "sha512-jaHFD6PFv6UgoIVda6qZllptQsMlDEJkTQcybzzXDYM1XO9Y8em691FGMPmM46WGyLU4z9KMgQN+qrux/nhlHA==", 12 | "_location": "/ws", 13 | "_phantomChildren": {}, 14 | "_requested": { 15 | "type": "version", 16 | "registry": true, 17 | "raw": "ws@5.2.2", 18 | "name": "ws", 19 | "escapedName": "ws", 20 | "rawSpec": "5.2.2", 21 | "saveSpec": null, 22 | "fetchSpec": "5.2.2" 23 | }, 24 | "_requiredBy": [ 25 | "/" 26 | ], 27 | "_resolved": "https://registry.npmjs.org/ws/-/ws-5.2.2.tgz", 28 | "_spec": "5.2.2", 29 | "_where": "D:\\work\\code\\com.unity.channelservice\\ChannelClientNodeApp~", 30 | "author": { 31 | "name": "Einar Otto Stangvik", 32 | "email": "einaros@gmail.com", 33 | "url": "http://2x.io" 34 | }, 35 | "bugs": { 36 | "url": "https://github.com/websockets/ws/issues" 37 | }, 38 | "dependencies": { 39 | "async-limiter": "~1.0.0" 40 | }, 41 | "description": "Simple to use, blazing fast and thoroughly tested websocket client and server for Node.js", 42 | "devDependencies": { 43 | "benchmark": "~2.1.2", 44 | "bufferutil": "~3.0.0", 45 | "eslint": "~4.19.0", 46 | "eslint-config-standard": "~11.0.0", 47 | "eslint-plugin-import": "~2.12.0", 48 | "eslint-plugin-node": "~6.0.0", 49 | "eslint-plugin-promise": "~3.8.0", 50 | "eslint-plugin-standard": "~3.0.0", 51 | "mocha": "~5.2.0", 52 | "nyc": "~12.0.2", 53 | "utf-8-validate": "~4.0.0" 54 | }, 55 | "files": [ 56 | "index.js", 57 | "lib" 58 | ], 59 | "homepage": "https://github.com/websockets/ws", 60 | "keywords": [ 61 | "HyBi", 62 | "Push", 63 | "RFC-6455", 64 | "WebSocket", 65 | "WebSockets", 66 | "real-time" 67 | ], 68 | "license": "MIT", 69 | "main": "index.js", 70 | "name": "ws", 71 | "repository": { 72 | "type": "git", 73 | "url": "git+https://github.com/websockets/ws.git" 74 | }, 75 | "scripts": { 76 | "integration": "eslint . && mocha test/*.integration.js", 77 | "lint": "eslint .", 78 | "test": "eslint . && nyc --reporter=html --reporter=text mocha test/*.test.js" 79 | }, 80 | "version": "5.2.2" 81 | } 82 | -------------------------------------------------------------------------------- /Editor/EventServiceDocExample.cs: -------------------------------------------------------------------------------- 1 | // #define COMMUNICATION_PUBLIC_API 2 | #if COMMUNICATION_PUBLIC_API 3 | using UnityEditor; 4 | using UnityEngine; 5 | using Unity.MPE; 6 | using System; 7 | 8 | public static class EventServiceDocExample 9 | { 10 | static Action s_CustomLogEventDisconnect; 11 | static Action s_PingPongEventDisconnect; 12 | 13 | [MenuItem("EventServiceDoc/Step 0")] 14 | static void StartChannelService() 15 | { 16 | if (!ChannelService.IsRunning()) 17 | { 18 | ChannelService.Start(); 19 | } 20 | Debug.Log($"[Step 0] ChannelService Running: {ChannelService.GetAddress()}:{ChannelService.GetPort()}"); 21 | } 22 | 23 | [MenuItem("EventServiceDoc/Step 1")] 24 | static void SetupEventServiceHandlers() 25 | { 26 | Debug.Log("[Step 1] Setup handlers"); 27 | s_CustomLogEventDisconnect = EventService.On("custom_log", (eventType, args) => { 28 | Debug.Log($"Log a {eventType} {args[0]}"); 29 | }); 30 | 31 | s_PingPongEventDisconnect = EventService.On("pingpong", (eventType, args) => 32 | { 33 | Debug.Log($"Receive a {eventType} {args[0]}"); 34 | return "pong!"; 35 | }); 36 | } 37 | 38 | [MenuItem("EventServiceDoc/Step 2")] 39 | static void EmitMessage() 40 | { 41 | Debug.Log("[Step 2] Emitting a custom log"); 42 | EventService.Emit("custom_log", "Hello world!", -1, EventDataSerialization.JsonUtility); 43 | } 44 | 45 | [MenuItem("EventServiceDoc/Step 3")] 46 | static void SendRequest() 47 | { 48 | Debug.Log("[Step 3] Sending a request"); 49 | EventService.Request("pingpong", (err, data) => 50 | { 51 | Debug.Log($"Request fulfilled: {data[0]}"); 52 | }, 53 | "ping", -1, EventDataSerialization.JsonUtility); 54 | } 55 | 56 | [MenuItem("EventServiceDoc/Step 4")] 57 | static void CloseHandlers() 58 | { 59 | Debug.Log("[Step 4] Closing all Event handlers"); 60 | s_CustomLogEventDisconnect(); 61 | s_PingPongEventDisconnect(); 62 | } 63 | } 64 | 65 | /* 66 | 67 | If you execute the 5 menu item one after the other, this will print the following 68 | in the console: 69 | 70 | [Step 0] ChannelService Running: 127.0.0.1:65000 71 | 72 | [Step 1] Setup handlers 73 | 74 | [Step 2] Emitting a custom log 75 | 76 | Log a custom_log Hello world! 77 | 78 | [Step 3] Sending a request 79 | 80 | Receive a pingpong ping 81 | 82 | Request fulfilled: pong! 83 | 84 | [Step 4] Closing all Event handlers 85 | 86 | */ 87 | #endif -------------------------------------------------------------------------------- /Editor/ChannelServiceAPIExample.cs: -------------------------------------------------------------------------------- 1 | // #define REGISTER_CHANNEL_ON_STARTUP 2 | using System; 3 | using System.Collections; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Reflection; 7 | using System.Text; 8 | using Unity.MPE; 9 | using UnityEditor; 10 | using UnityEngine; 11 | 12 | static class ChannelAPIExample 13 | { 14 | static int s_BinaryChannelId; 15 | static int s_StringChannelId; 16 | static Action s_DisconnectBinaryChannel; 17 | static Action s_DisconnectStringChannel; 18 | 19 | #if REGISTER_CHANNEL_ON_STARTUP 20 | // This function is called each domain reload (i.e each time a script recompiles). 21 | [InitializeOnLoadMethod] 22 | static void RegisterChannelOnLoad() 23 | { 24 | RegisterChannelService(); 25 | } 26 | #endif 27 | 28 | static void RegisterChannelService() 29 | { 30 | if (!ChannelServiceAPI.IsRunning()) 31 | ChannelServiceAPI.StartChannelService(); 32 | 33 | Debug.Log($"ChannelService Running: {ChannelServiceAPI.GetAddress()}:{ChannelServiceAPI.GetPort()}"); 34 | 35 | if (s_DisconnectBinaryChannel == null) 36 | { 37 | s_DisconnectBinaryChannel = ChannelServiceAPI.GetOrCreateChannel("custom_binary_ping_pong", HandleChannelBinaryMessage); 38 | s_BinaryChannelId = ChannelServiceAPI.GetChannelId("custom_binary_ping_pong"); 39 | Debug.Log($"channel_custom_binary id: {s_BinaryChannelId}"); 40 | } 41 | 42 | if (s_DisconnectStringChannel == null) 43 | { 44 | s_DisconnectStringChannel = ChannelServiceAPI.GetOrCreateChannel("custom_ascii_ping_pong", HandleChannelStringMessage); 45 | s_StringChannelId = ChannelServiceAPI.GetChannelId("custom_ascii_ping_pong"); 46 | Debug.Log($"channel_custom_ascii id: {s_StringChannelId}"); 47 | } 48 | } 49 | 50 | [MenuItem("ChannelServiceAPI/Register new channels")] 51 | static void RegisterMenu() 52 | { 53 | RegisterChannelService(); 54 | } 55 | 56 | static void HandleChannelBinaryMessage(int connectionId, byte[] data) 57 | { 58 | var msg = ""; 59 | for (var i = 0; i < Math.Min(10, data.Length); ++i) 60 | { 61 | msg += data[i].ToString(); 62 | } 63 | Debug.Log($"receiving binary from connection {connectionId} - {data.Length} bytes - {msg}"); 64 | 65 | // Let's pong it back: 66 | ChannelServiceAPI.SendBinary(connectionId, data); 67 | } 68 | 69 | static void HandleChannelStringMessage(int connectionId, byte[] data) 70 | { 71 | var msgStr = Encoding.UTF8.GetString(data); 72 | Debug.Log($"receiving string from connection {connectionId} - {msgStr}"); 73 | 74 | // Let's pong it back: 75 | ChannelServiceAPI.Send(connectionId, msgStr); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /ChannelClientNodeApp~/node_modules/async-limiter/readme.md: -------------------------------------------------------------------------------- 1 | # Async-Limiter 2 | 3 | A module for limiting concurrent asynchronous actions in flight. Forked from [queue](https://github.com/jessetane/queue). 4 | 5 | [![npm](http://img.shields.io/npm/v/async-limiter.svg?style=flat-square)](http://www.npmjs.org/async-limiter) 6 | [![tests](https://img.shields.io/travis/STRML/async-limiter.svg?style=flat-square&branch=master)](https://travis-ci.org/STRML/async-limiter) 7 | [![coverage](https://img.shields.io/coveralls/STRML/async-limiter.svg?style=flat-square&branch=master)](https://coveralls.io/r/STRML/async-limiter) 8 | 9 | This module exports a class `Limiter` that implements some of the `Array` API. 10 | Pass async functions (ones that accept a callback or return a promise) to an instance's additive array methods. 11 | 12 | ## Motivation 13 | 14 | Certain functions, like `zlib`, have [undesirable behavior](https://github.com/nodejs/node/issues/8871#issuecomment-250915913) when 15 | run at infinite concurrency. 16 | 17 | In this case, it is actually faster, and takes far less memory, to limit concurrency. 18 | 19 | This module should do the absolute minimum work necessary to queue up functions. PRs are welcome that would 20 | make this module faster or lighter, but new functionality is not desired. 21 | 22 | Style should confirm to nodejs/node style. 23 | 24 | ## Example 25 | 26 | ``` javascript 27 | var Limiter = require('async-limiter') 28 | 29 | var t = new Limiter({concurrency: 2}); 30 | var results = [] 31 | 32 | // add jobs using the familiar Array API 33 | t.push(function (cb) { 34 | results.push('two') 35 | cb() 36 | }) 37 | 38 | t.push( 39 | function (cb) { 40 | results.push('four') 41 | cb() 42 | }, 43 | function (cb) { 44 | results.push('five') 45 | cb() 46 | } 47 | ) 48 | 49 | t.unshift(function (cb) { 50 | results.push('one') 51 | cb() 52 | }) 53 | 54 | t.splice(2, 0, function (cb) { 55 | results.push('three') 56 | cb() 57 | }) 58 | 59 | // Jobs run automatically. If you want a callback when all are done, 60 | // call 'onDone()'. 61 | t.onDone(function () { 62 | console.log('all done:', results) 63 | }) 64 | ``` 65 | 66 | ## Zlib Example 67 | 68 | ```js 69 | const zlib = require('zlib'); 70 | const Limiter = require('async-limiter'); 71 | 72 | const message = {some: "data"}; 73 | const payload = new Buffer(JSON.stringify(message)); 74 | 75 | // Try with different concurrency values to see how this actually 76 | // slows significantly with higher concurrency! 77 | // 78 | // 5: 1398.607ms 79 | // 10: 1375.668ms 80 | // Infinity: 4423.300ms 81 | // 82 | const t = new Limiter({concurrency: 5}); 83 | function deflate(payload, cb) { 84 | t.push(function(done) { 85 | zlib.deflate(payload, function(err, buffer) { 86 | done(); 87 | cb(err, buffer); 88 | }); 89 | }); 90 | } 91 | 92 | console.time('deflate'); 93 | for(let i = 0; i < 30000; ++i) { 94 | deflate(payload, function (err, buffer) {}); 95 | } 96 | t.onDone(function() { 97 | console.timeEnd('deflate'); 98 | }); 99 | ``` 100 | 101 | ## Install 102 | 103 | `npm install async-limiter` 104 | 105 | ## Test 106 | 107 | `npm test` 108 | 109 | ## API 110 | 111 | ### `var t = new Limiter([opts])` 112 | Constructor. `opts` may contain inital values for: 113 | * `t.concurrency` 114 | 115 | ## Instance methods 116 | 117 | ### `t.onDone(fn)` 118 | `fn` will be called once and only once, when the queue is empty. 119 | 120 | ## Instance methods mixed in from `Array` 121 | Mozilla has docs on how these methods work [here](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array). 122 | ### `t.push(element1, ..., elementN)` 123 | ### `t.unshift(element1, ..., elementN)` 124 | ### `t.splice(index , howMany[, element1[, ...[, elementN]]])` 125 | 126 | ## Properties 127 | ### `t.concurrency` 128 | Max number of jobs the queue should process concurrently, defaults to `Infinity`. 129 | 130 | ### `t.length` 131 | Jobs pending + jobs to process (readonly). 132 | 133 | -------------------------------------------------------------------------------- /ChannelClientNodeApp~/node_modules/ws/lib/event-target.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Class representing an event. 5 | * 6 | * @private 7 | */ 8 | class Event { 9 | /** 10 | * Create a new `Event`. 11 | * 12 | * @param {String} type The name of the event 13 | * @param {Object} target A reference to the target to which the event was dispatched 14 | */ 15 | constructor (type, target) { 16 | this.target = target; 17 | this.type = type; 18 | } 19 | } 20 | 21 | /** 22 | * Class representing a message event. 23 | * 24 | * @extends Event 25 | * @private 26 | */ 27 | class MessageEvent extends Event { 28 | /** 29 | * Create a new `MessageEvent`. 30 | * 31 | * @param {(String|Buffer|ArrayBuffer|Buffer[])} data The received data 32 | * @param {WebSocket} target A reference to the target to which the event was dispatched 33 | */ 34 | constructor (data, target) { 35 | super('message', target); 36 | 37 | this.data = data; 38 | } 39 | } 40 | 41 | /** 42 | * Class representing a close event. 43 | * 44 | * @extends Event 45 | * @private 46 | */ 47 | class CloseEvent extends Event { 48 | /** 49 | * Create a new `CloseEvent`. 50 | * 51 | * @param {Number} code The status code explaining why the connection is being closed 52 | * @param {String} reason A human-readable string explaining why the connection is closing 53 | * @param {WebSocket} target A reference to the target to which the event was dispatched 54 | */ 55 | constructor (code, reason, target) { 56 | super('close', target); 57 | 58 | this.wasClean = target._closeFrameReceived && target._closeFrameSent; 59 | this.reason = reason; 60 | this.code = code; 61 | } 62 | } 63 | 64 | /** 65 | * Class representing an open event. 66 | * 67 | * @extends Event 68 | * @private 69 | */ 70 | class OpenEvent extends Event { 71 | /** 72 | * Create a new `OpenEvent`. 73 | * 74 | * @param {WebSocket} target A reference to the target to which the event was dispatched 75 | */ 76 | constructor (target) { 77 | super('open', target); 78 | } 79 | } 80 | 81 | /** 82 | * Class representing an error event. 83 | * 84 | * @extends Event 85 | * @private 86 | */ 87 | class ErrorEvent extends Event { 88 | /** 89 | * Create a new `ErrorEvent`. 90 | * 91 | * @param {Object} error The error that generated this event 92 | * @param {WebSocket} target A reference to the target to which the event was dispatched 93 | */ 94 | constructor (error, target) { 95 | super('error', target); 96 | 97 | this.message = error.message; 98 | this.error = error; 99 | } 100 | } 101 | 102 | /** 103 | * This provides methods for emulating the `EventTarget` interface. It's not 104 | * meant to be used directly. 105 | * 106 | * @mixin 107 | */ 108 | const EventTarget = { 109 | /** 110 | * Register an event listener. 111 | * 112 | * @param {String} method A string representing the event type to listen for 113 | * @param {Function} listener The listener to add 114 | * @public 115 | */ 116 | addEventListener (method, listener) { 117 | if (typeof listener !== 'function') return; 118 | 119 | function onMessage (data) { 120 | listener.call(this, new MessageEvent(data, this)); 121 | } 122 | 123 | function onClose (code, message) { 124 | listener.call(this, new CloseEvent(code, message, this)); 125 | } 126 | 127 | function onError (error) { 128 | listener.call(this, new ErrorEvent(error, this)); 129 | } 130 | 131 | function onOpen () { 132 | listener.call(this, new OpenEvent(this)); 133 | } 134 | 135 | if (method === 'message') { 136 | onMessage._listener = listener; 137 | this.on(method, onMessage); 138 | } else if (method === 'close') { 139 | onClose._listener = listener; 140 | this.on(method, onClose); 141 | } else if (method === 'error') { 142 | onError._listener = listener; 143 | this.on(method, onError); 144 | } else if (method === 'open') { 145 | onOpen._listener = listener; 146 | this.on(method, onOpen); 147 | } else { 148 | this.on(method, listener); 149 | } 150 | }, 151 | 152 | /** 153 | * Remove an event listener. 154 | * 155 | * @param {String} method A string representing the event type to remove 156 | * @param {Function} listener The listener to remove 157 | * @public 158 | */ 159 | removeEventListener (method, listener) { 160 | const listeners = this.listeners(method); 161 | 162 | for (var i = 0; i < listeners.length; i++) { 163 | if (listeners[i] === listener || listeners[i]._listener === listener) { 164 | this.removeListener(method, listeners[i]); 165 | } 166 | } 167 | } 168 | }; 169 | 170 | module.exports = EventTarget; 171 | -------------------------------------------------------------------------------- /Editor/ChannelCommunicationDocExample.cs: -------------------------------------------------------------------------------- 1 | // #define COMMUNICATION_PUBLIC_API 2 | #if COMMUNICATION_PUBLIC_API 3 | using System; 4 | using System.Text; 5 | using Unity.MPE; 6 | using UnityEditor; 7 | using UnityEngine; 8 | 9 | public static class ChannelCommunicationDocExample 10 | { 11 | [MenuItem("ChannelDoc/Step 1")] 12 | static void StartChannelService() 13 | { 14 | if (!ChannelService.IsRunning()) 15 | { 16 | ChannelService.Start(); 17 | } 18 | Debug.Log($"[Step1] ChannelService Running: {ChannelService.GetAddress()}:{ChannelService.GetPort()}"); 19 | } 20 | 21 | static int s_BinaryChannelId; 22 | static int s_StringChannelId; 23 | static Action s_DisconnectBinaryChannel; 24 | static Action s_DisconnectStringChannel; 25 | 26 | [MenuItem("ChannelDoc/Step 2")] 27 | static void SetupChannelService() 28 | { 29 | if (s_DisconnectBinaryChannel == null) 30 | { 31 | s_DisconnectBinaryChannel = ChannelService.GetOrCreateChannel("custom_binary_ping_pong", HandleChannelBinaryMessage); 32 | s_BinaryChannelId = ChannelService.ChannelNameToId("custom_binary_ping_pong"); 33 | } 34 | Debug.Log($"[Step2] Setup channel_custom_binary id: {s_BinaryChannelId}"); 35 | 36 | if (s_DisconnectStringChannel == null) 37 | { 38 | s_DisconnectStringChannel = ChannelService.GetOrCreateChannel("custom_ascii_ping_pong", HandleChannelStringMessage); 39 | s_StringChannelId = ChannelService.ChannelNameToId("custom_ascii_ping_pong"); 40 | } 41 | Debug.Log($"[Step2] Setup channel_custom_ascii id: {s_StringChannelId}"); 42 | } 43 | 44 | static void HandleChannelBinaryMessage(int connectionId, byte[] data) 45 | { 46 | var msg = ""; 47 | for (var i = 0; i < Math.Min(10, data.Length); ++i) 48 | { 49 | msg += data[i].ToString(); 50 | } 51 | Debug.Log($"Channel Handling binary from connection {connectionId} - {data.Length} bytes - {msg}"); 52 | 53 | // Let's pong it back: 54 | ChannelService.Send(connectionId, data); 55 | } 56 | 57 | static void HandleChannelStringMessage(int connectionId, byte[] data) 58 | { 59 | // We are receiving a new message. All message are always handled as bytes in a ChannelHandler. 60 | // Since our clients are expecting string data, encode the data and send it back as a string: 61 | 62 | var msgStr = Encoding.UTF8.GetString(data); 63 | Debug.Log($"Channel Handling string from connection {connectionId} - {msgStr}"); 64 | 65 | // Let's pong it back: 66 | ChannelService.Send(connectionId, msgStr); 67 | } 68 | 69 | static ChannelClient s_BinaryClient; 70 | static Action s_DisconnectBinaryClient; 71 | static ChannelClient s_StringClient; 72 | static Action s_DisconnectStringClient; 73 | [MenuItem("ChannelDoc/Step 3")] 74 | static void SetupChannelClient() 75 | { 76 | const bool autoTick = true; 77 | 78 | if (s_BinaryClient == null) 79 | { 80 | s_BinaryClient = ChannelClient.GetOrCreateClient("custom_binary_ping_pong"); 81 | s_BinaryClient.Start(autoTick); 82 | s_DisconnectBinaryClient = s_BinaryClient.On(HandleClientBinaryMessage); 83 | } 84 | Debug.Log($"[Step3] Setup client for channel custom_binary_ping_pong. ClientId: {s_BinaryClient.clientId}"); 85 | 86 | if (s_StringClient == null) 87 | { 88 | s_StringClient = ChannelClient.GetOrCreateClient("custom_ascii_ping_pong"); 89 | s_StringClient.Start(autoTick); 90 | s_DisconnectStringClient = s_StringClient.On(HandleClientStringMessage); 91 | } 92 | Debug.Log($"[Step3] Setup client for channel custom_ascii_ping_pong. ClientId: {s_StringClient.clientId}"); 93 | } 94 | 95 | static void HandleClientBinaryMessage(byte[] data) 96 | { 97 | Debug.Log($"Receiving pong binary data: {data} for clientId: {s_BinaryClient.clientId} with channelName: {s_BinaryClient.channelName}"); 98 | } 99 | 100 | static void HandleClientStringMessage(string data) 101 | { 102 | Debug.Log($"Receiving pong data: {data} for clientId: {s_StringClient.clientId} with channelName: {s_StringClient.channelName}"); 103 | } 104 | 105 | [MenuItem("ChannelDoc/Step 4")] 106 | static void ClientSendMessageToServer() 107 | { 108 | Debug.Log("[Step 4]: Clients are sending data!"); 109 | s_BinaryClient.Send(new byte[] { 0, 1, 2, 3, 4, 5, 6, 7 }); 110 | s_StringClient.Send("Hello world!"); 111 | } 112 | 113 | [MenuItem("ChannelDoc/Step 5")] 114 | static void CloseClients() 115 | { 116 | Debug.Log("[Step 5]: Closing clients"); 117 | s_DisconnectBinaryClient(); 118 | s_BinaryClient.Close(); 119 | 120 | s_DisconnectStringClient(); 121 | s_StringClient.Close(); 122 | } 123 | 124 | [MenuItem("ChannelDoc/Step 6")] 125 | static void CloseService() 126 | { 127 | Debug.Log("[Step 6]: Closing clients"); 128 | 129 | s_DisconnectBinaryChannel(); 130 | s_DisconnectStringChannel(); 131 | 132 | ChannelService.Stop(); 133 | } 134 | } 135 | 136 | /* 137 | If you execute the 6 menu item one after the other, this will print the following 138 | in the console: 139 | 140 | [Step1] ChannelService Running: 127.0.0.1:64647 141 | 142 | [Step2] Setup channel_custom_binary id: -1698345965 143 | 144 | [Step2] Setup channel_custom_ascii id: -930064725 145 | 146 | [Step3] Setup client for channel custom_binary_ping_pong. ClientId: -1698345965 147 | 148 | [Step3] Setup client for channel custom_ascii_ping_pong. ClientId: -930064725 149 | 150 | [Step 4]: Clients are sending data! 151 | 152 | Channel Handling binary from connection 1 - 8 bytes - 01234567 153 | 154 | Channel Handling string from connection 2 - Hello world! 155 | 156 | Receiving pong binary data: System.Byte[] for clientId: -1698345965 with channelName: custom_binary_ping_pong 157 | 158 | Receiving pong data: Hello world! for clientId: -930064725 with channelName: custom_ascii_ping_pong 159 | 160 | [Step 5]: Closing clients 161 | 162 | [Step 6]: Closing clients 163 | 164 | */ 165 | #endif -------------------------------------------------------------------------------- /Editor/ChannelServiceAPI.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Reflection; 7 | using System.Text; 8 | using System.Xml.Schema; 9 | using Unity.MPE; 10 | using UnityEditor; 11 | using UnityEngine; 12 | 13 | public static class ChannelServiceAPI 14 | { 15 | static Type[] s_AllTypes; 16 | static Type s_ChannelServiceType; 17 | static Type s_ChannelInfoType; 18 | static Type s_ChannelHandlerType; 19 | 20 | public delegate void ChannelHandler(int clientId, byte[] binaryData); 21 | 22 | static ChannelServiceAPI() 23 | { 24 | var assembly = typeof(EventService).Assembly; 25 | s_AllTypes = assembly.GetTypes().ToArray(); 26 | s_ChannelServiceType = s_AllTypes.FirstOrDefault(t => t.Name == "ChannelService"); 27 | s_ChannelInfoType = s_AllTypes.FirstOrDefault(t => t.Name == "ChannelInfo"); 28 | s_ChannelHandlerType = s_AllTypes.FirstOrDefault(t => t.Name == "ChannelHandler"); 29 | 30 | EditorApplication.quitting += () => 31 | { 32 | var infoFile = GetIsConnectedFile(); 33 | if (File.Exists(infoFile)) 34 | { 35 | File.Delete(infoFile); 36 | } 37 | }; 38 | } 39 | 40 | public static void StartChannelService() 41 | { 42 | Debug.Log("Starting ChannelService"); 43 | var startFunction = s_ChannelServiceType.GetMethod("Start", BindingFlags.Public | BindingFlags.Static); 44 | startFunction.Invoke(null, new object[0]); 45 | 46 | if (IsRunning()) 47 | { 48 | var infoFile = GetIsConnectedFile(); 49 | if (File.Exists(infoFile)) 50 | { 51 | File.Delete(infoFile); 52 | } 53 | File.WriteAllText(infoFile, $"{GetAddress()}:{GetPort()}"); 54 | } 55 | } 56 | 57 | public static void CloseChannel(string channelName) 58 | { 59 | Debug.Log($"Closing Channel {channelName}"); 60 | var startFunction = s_ChannelServiceType.GetMethod("Start", BindingFlags.Public | BindingFlags.Static); 61 | startFunction.Invoke(null, new object[0]); 62 | } 63 | 64 | public static void StopChannelService() 65 | { 66 | Debug.Log("Stopping ChannelService"); 67 | var stopFunction = s_ChannelServiceType.GetMethod("Stop", BindingFlags.Public | BindingFlags.Static); 68 | stopFunction.Invoke(null, new object[0]); 69 | } 70 | 71 | public static int GetChannelId(string channelName) 72 | { 73 | var getChannelInfoFunction = s_ChannelServiceType.GetMethod("GetChannelFromName", BindingFlags.NonPublic | BindingFlags.Static); 74 | var channelInfo = getChannelInfoFunction.Invoke(null, new [] { channelName }); 75 | var channelIdProperty = s_ChannelInfoType.GetProperty("channelId", BindingFlags.Public | BindingFlags.Instance); 76 | return (int)channelIdProperty.GetValue(channelInfo); 77 | } 78 | 79 | public static string GetAddress() 80 | { 81 | var getAddressFunction = s_ChannelServiceType.GetMethod("GetAddress", BindingFlags.Public | BindingFlags.Static); 82 | return getAddressFunction.Invoke(null, new object[0]) as string; 83 | } 84 | 85 | public static bool IsRunning() 86 | { 87 | var isRunningFunction = s_ChannelServiceType.GetMethod("IsRunning", BindingFlags.Public | BindingFlags.Static); 88 | return (bool)isRunningFunction.Invoke(null, new object[0]); 89 | } 90 | 91 | public static int GetPort() 92 | { 93 | var getPortFunction = s_ChannelServiceType.GetMethod("GetPort", BindingFlags.Public | BindingFlags.Static); 94 | return (int)getPortFunction.Invoke(null, new object[0]); 95 | } 96 | 97 | internal static Action GetOrCreateChannel(string channelName, ChannelHandler handler) 98 | { 99 | Debug.Log($"Channel {channelName} ready"); 100 | var getOrCreateChannelFunction = s_ChannelServiceType.GetMethod("GetOrCreateChannel", BindingFlags.Public | BindingFlags.Static); 101 | var properTypeHandler = ConvertDelegate(handler, s_ChannelHandlerType); 102 | return getOrCreateChannelFunction.Invoke(null, new object[] { channelName, properTypeHandler }) as Action; 103 | } 104 | 105 | // Broadcast to all connections on the same channel 106 | public static void BroadcastBinary(int channelId, byte[] data) 107 | { 108 | var broadcastFunction = s_ChannelServiceType.GetMethod("BroadcastBinary", BindingFlags.NonPublic | BindingFlags.Static); 109 | broadcastFunction.Invoke(null, new object[] { channelId, data }); 110 | } 111 | 112 | // Send direct message to specific connection 113 | public static void SendBinary(int connectionId, byte[] data) 114 | { 115 | var sendFunction = s_ChannelServiceType.GetMethod("SendBinary", BindingFlags.NonPublic | BindingFlags.Static); 116 | sendFunction.Invoke(null, new object[] { connectionId, data }); 117 | } 118 | 119 | public static void Broadcast(int channelId, byte[] data) 120 | { 121 | var broadcastFunction = s_ChannelServiceType.GetMethods(BindingFlags.Public | BindingFlags.Static).First(mi => mi.Name == "Broadcast" && mi.GetParameters()[1].ParameterType == typeof(string)); 122 | broadcastFunction.Invoke(null, new object[] { channelId, data }); 123 | } 124 | 125 | // Send direct message to specific connection 126 | public static void Send(int connectionId, string data) 127 | { 128 | var sendFunction = s_ChannelServiceType.GetMethods(BindingFlags.Public | BindingFlags.Static).First(mi => mi.Name == "Send" && mi.GetParameters()[1].ParameterType == typeof(string)); 129 | sendFunction.Invoke(null, new object[] { connectionId, data }); 130 | } 131 | 132 | static Delegate ConvertDelegate(Delegate sourceDelegate, Type targetType) 133 | { 134 | return Delegate.CreateDelegate( 135 | targetType, 136 | sourceDelegate.Target, 137 | sourceDelegate.Method); 138 | } 139 | 140 | static string GetIsConnectedFile() 141 | { 142 | return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Unity", "Editor", "ChannelService.info").Replace("\\", "/"); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /ChannelClientNodeApp~/node_modules/ws/lib/extension.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // 4 | // Allowed token characters: 5 | // 6 | // '!', '#', '$', '%', '&', ''', '*', '+', '-', 7 | // '.', 0-9, A-Z, '^', '_', '`', a-z, '|', '~' 8 | // 9 | // tokenChars[32] === 0 // ' ' 10 | // tokenChars[33] === 1 // '!' 11 | // tokenChars[34] === 0 // '"' 12 | // ... 13 | // 14 | const tokenChars = [ 15 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0 - 15 16 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 16 - 31 17 | 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, // 32 - 47 18 | 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, // 48 - 63 19 | 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 64 - 79 20 | 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, // 80 - 95 21 | 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 96 - 111 22 | 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0 // 112 - 127 23 | ]; 24 | 25 | /** 26 | * Adds an offer to the map of extension offers or a parameter to the map of 27 | * parameters. 28 | * 29 | * @param {Object} dest The map of extension offers or parameters 30 | * @param {String} name The extension or parameter name 31 | * @param {(Object|Boolean|String)} elem The extension parameters or the 32 | * parameter value 33 | * @private 34 | */ 35 | function push (dest, name, elem) { 36 | if (Object.prototype.hasOwnProperty.call(dest, name)) dest[name].push(elem); 37 | else dest[name] = [elem]; 38 | } 39 | 40 | /** 41 | * Parses the `Sec-WebSocket-Extensions` header into an object. 42 | * 43 | * @param {String} header The field value of the header 44 | * @return {Object} The parsed object 45 | * @public 46 | */ 47 | function parse (header) { 48 | const offers = {}; 49 | 50 | if (header === undefined || header === '') return offers; 51 | 52 | var params = {}; 53 | var mustUnescape = false; 54 | var isEscaping = false; 55 | var inQuotes = false; 56 | var extensionName; 57 | var paramName; 58 | var start = -1; 59 | var end = -1; 60 | 61 | for (var i = 0; i < header.length; i++) { 62 | const code = header.charCodeAt(i); 63 | 64 | if (extensionName === undefined) { 65 | if (end === -1 && tokenChars[code] === 1) { 66 | if (start === -1) start = i; 67 | } else if (code === 0x20/* ' ' */|| code === 0x09/* '\t' */) { 68 | if (end === -1 && start !== -1) end = i; 69 | } else if (code === 0x3b/* ';' */ || code === 0x2c/* ',' */) { 70 | if (start === -1) { 71 | throw new SyntaxError(`Unexpected character at index ${i}`); 72 | } 73 | 74 | if (end === -1) end = i; 75 | const name = header.slice(start, end); 76 | if (code === 0x2c) { 77 | push(offers, name, params); 78 | params = {}; 79 | } else { 80 | extensionName = name; 81 | } 82 | 83 | start = end = -1; 84 | } else { 85 | throw new SyntaxError(`Unexpected character at index ${i}`); 86 | } 87 | } else if (paramName === undefined) { 88 | if (end === -1 && tokenChars[code] === 1) { 89 | if (start === -1) start = i; 90 | } else if (code === 0x20 || code === 0x09) { 91 | if (end === -1 && start !== -1) end = i; 92 | } else if (code === 0x3b || code === 0x2c) { 93 | if (start === -1) { 94 | throw new SyntaxError(`Unexpected character at index ${i}`); 95 | } 96 | 97 | if (end === -1) end = i; 98 | push(params, header.slice(start, end), true); 99 | if (code === 0x2c) { 100 | push(offers, extensionName, params); 101 | params = {}; 102 | extensionName = undefined; 103 | } 104 | 105 | start = end = -1; 106 | } else if (code === 0x3d/* '=' */&& start !== -1 && end === -1) { 107 | paramName = header.slice(start, i); 108 | start = end = -1; 109 | } else { 110 | throw new SyntaxError(`Unexpected character at index ${i}`); 111 | } 112 | } else { 113 | // 114 | // The value of a quoted-string after unescaping must conform to the 115 | // token ABNF, so only token characters are valid. 116 | // Ref: https://tools.ietf.org/html/rfc6455#section-9.1 117 | // 118 | if (isEscaping) { 119 | if (tokenChars[code] !== 1) { 120 | throw new SyntaxError(`Unexpected character at index ${i}`); 121 | } 122 | if (start === -1) start = i; 123 | else if (!mustUnescape) mustUnescape = true; 124 | isEscaping = false; 125 | } else if (inQuotes) { 126 | if (tokenChars[code] === 1) { 127 | if (start === -1) start = i; 128 | } else if (code === 0x22/* '"' */ && start !== -1) { 129 | inQuotes = false; 130 | end = i; 131 | } else if (code === 0x5c/* '\' */) { 132 | isEscaping = true; 133 | } else { 134 | throw new SyntaxError(`Unexpected character at index ${i}`); 135 | } 136 | } else if (code === 0x22 && header.charCodeAt(i - 1) === 0x3d) { 137 | inQuotes = true; 138 | } else if (end === -1 && tokenChars[code] === 1) { 139 | if (start === -1) start = i; 140 | } else if (start !== -1 && (code === 0x20 || code === 0x09)) { 141 | if (end === -1) end = i; 142 | } else if (code === 0x3b || code === 0x2c) { 143 | if (start === -1) { 144 | throw new SyntaxError(`Unexpected character at index ${i}`); 145 | } 146 | 147 | if (end === -1) end = i; 148 | var value = header.slice(start, end); 149 | if (mustUnescape) { 150 | value = value.replace(/\\/g, ''); 151 | mustUnescape = false; 152 | } 153 | push(params, paramName, value); 154 | if (code === 0x2c) { 155 | push(offers, extensionName, params); 156 | params = {}; 157 | extensionName = undefined; 158 | } 159 | 160 | paramName = undefined; 161 | start = end = -1; 162 | } else { 163 | throw new SyntaxError(`Unexpected character at index ${i}`); 164 | } 165 | } 166 | } 167 | 168 | if (start === -1 || inQuotes) { 169 | throw new SyntaxError('Unexpected end of input'); 170 | } 171 | 172 | if (end === -1) end = i; 173 | const token = header.slice(start, end); 174 | if (extensionName === undefined) { 175 | push(offers, token, {}); 176 | } else { 177 | if (paramName === undefined) { 178 | push(params, token, true); 179 | } else if (mustUnescape) { 180 | push(params, paramName, token.replace(/\\/g, '')); 181 | } else { 182 | push(params, paramName, token); 183 | } 184 | push(offers, extensionName, params); 185 | } 186 | 187 | return offers; 188 | } 189 | 190 | /** 191 | * Builds the `Sec-WebSocket-Extensions` header field value. 192 | * 193 | * @param {Object} extensions The map of extensions and parameters to format 194 | * @return {String} A string representing the given object 195 | * @public 196 | */ 197 | function format (extensions) { 198 | return Object.keys(extensions).map((extension) => { 199 | var configurations = extensions[extension]; 200 | if (!Array.isArray(configurations)) configurations = [configurations]; 201 | return configurations.map((params) => { 202 | return [extension].concat(Object.keys(params).map((k) => { 203 | var values = params[k]; 204 | if (!Array.isArray(values)) values = [values]; 205 | return values.map((v) => v === true ? k : `${k}=${v}`).join('; '); 206 | })).join('; '); 207 | }).join(', '); 208 | }).join(', '); 209 | } 210 | 211 | module.exports = { format, parse }; 212 | -------------------------------------------------------------------------------- /ChannelClientNodeApp~/node_modules/ws/lib/websocket-server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const EventEmitter = require('events'); 4 | const crypto = require('crypto'); 5 | const http = require('http'); 6 | const url = require('url'); 7 | 8 | const PerMessageDeflate = require('./permessage-deflate'); 9 | const extension = require('./extension'); 10 | const constants = require('./constants'); 11 | const WebSocket = require('./websocket'); 12 | 13 | /** 14 | * Class representing a WebSocket server. 15 | * 16 | * @extends EventEmitter 17 | */ 18 | class WebSocketServer extends EventEmitter { 19 | /** 20 | * Create a `WebSocketServer` instance. 21 | * 22 | * @param {Object} options Configuration options 23 | * @param {String} options.host The hostname where to bind the server 24 | * @param {Number} options.port The port where to bind the server 25 | * @param {http.Server} options.server A pre-created HTTP/S server to use 26 | * @param {Function} options.verifyClient An hook to reject connections 27 | * @param {Function} options.handleProtocols An hook to handle protocols 28 | * @param {String} options.path Accept only connections matching this path 29 | * @param {Boolean} options.noServer Enable no server mode 30 | * @param {Boolean} options.clientTracking Specifies whether or not to track clients 31 | * @param {(Boolean|Object)} options.perMessageDeflate Enable/disable permessage-deflate 32 | * @param {Number} options.maxPayload The maximum allowed message size 33 | * @param {Function} callback A listener for the `listening` event 34 | */ 35 | constructor (options, callback) { 36 | super(); 37 | 38 | options = Object.assign({ 39 | maxPayload: 100 * 1024 * 1024, 40 | perMessageDeflate: false, 41 | handleProtocols: null, 42 | clientTracking: true, 43 | verifyClient: null, 44 | noServer: false, 45 | backlog: null, // use default (511 as implemented in net.js) 46 | server: null, 47 | host: null, 48 | path: null, 49 | port: null 50 | }, options); 51 | 52 | if (options.port == null && !options.server && !options.noServer) { 53 | throw new TypeError( 54 | 'One of the "port", "server", or "noServer" options must be specified' 55 | ); 56 | } 57 | 58 | if (options.port != null) { 59 | this._server = http.createServer((req, res) => { 60 | const body = http.STATUS_CODES[426]; 61 | 62 | res.writeHead(426, { 63 | 'Content-Length': body.length, 64 | 'Content-Type': 'text/plain' 65 | }); 66 | res.end(body); 67 | }); 68 | this._server.listen(options.port, options.host, options.backlog, callback); 69 | } else if (options.server) { 70 | this._server = options.server; 71 | } 72 | 73 | if (this._server) { 74 | this._removeListeners = addListeners(this._server, { 75 | listening: this.emit.bind(this, 'listening'), 76 | error: this.emit.bind(this, 'error'), 77 | upgrade: (req, socket, head) => { 78 | this.handleUpgrade(req, socket, head, (ws) => { 79 | this.emit('connection', ws, req); 80 | }); 81 | } 82 | }); 83 | } 84 | 85 | if (options.perMessageDeflate === true) options.perMessageDeflate = {}; 86 | if (options.clientTracking) this.clients = new Set(); 87 | this.options = options; 88 | } 89 | 90 | /** 91 | * Returns the bound address, the address family name, and port of the server 92 | * as reported by the operating system if listening on an IP socket. 93 | * If the server is listening on a pipe or UNIX domain socket, the name is 94 | * returned as a string. 95 | * 96 | * @return {(Object|String|null)} The address of the server 97 | * @public 98 | */ 99 | address () { 100 | if (this.options.noServer) { 101 | throw new Error('The server is operating in "noServer" mode'); 102 | } 103 | 104 | if (!this._server) return null; 105 | return this._server.address(); 106 | } 107 | 108 | /** 109 | * Close the server. 110 | * 111 | * @param {Function} cb Callback 112 | * @public 113 | */ 114 | close (cb) { 115 | // 116 | // Terminate all associated clients. 117 | // 118 | if (this.clients) { 119 | for (const client of this.clients) client.terminate(); 120 | } 121 | 122 | const server = this._server; 123 | 124 | if (server) { 125 | this._removeListeners(); 126 | this._removeListeners = this._server = null; 127 | 128 | // 129 | // Close the http server if it was internally created. 130 | // 131 | if (this.options.port != null) return server.close(cb); 132 | } 133 | 134 | if (cb) cb(); 135 | } 136 | 137 | /** 138 | * See if a given request should be handled by this server instance. 139 | * 140 | * @param {http.IncomingMessage} req Request object to inspect 141 | * @return {Boolean} `true` if the request is valid, else `false` 142 | * @public 143 | */ 144 | shouldHandle (req) { 145 | if (this.options.path && url.parse(req.url).pathname !== this.options.path) { 146 | return false; 147 | } 148 | 149 | return true; 150 | } 151 | 152 | /** 153 | * Handle a HTTP Upgrade request. 154 | * 155 | * @param {http.IncomingMessage} req The request object 156 | * @param {net.Socket} socket The network socket between the server and client 157 | * @param {Buffer} head The first packet of the upgraded stream 158 | * @param {Function} cb Callback 159 | * @public 160 | */ 161 | handleUpgrade (req, socket, head, cb) { 162 | socket.on('error', socketOnError); 163 | 164 | const version = +req.headers['sec-websocket-version']; 165 | const extensions = {}; 166 | 167 | if ( 168 | req.method !== 'GET' || req.headers.upgrade.toLowerCase() !== 'websocket' || 169 | !req.headers['sec-websocket-key'] || (version !== 8 && version !== 13) || 170 | !this.shouldHandle(req) 171 | ) { 172 | return abortHandshake(socket, 400); 173 | } 174 | 175 | if (this.options.perMessageDeflate) { 176 | const perMessageDeflate = new PerMessageDeflate( 177 | this.options.perMessageDeflate, 178 | true, 179 | this.options.maxPayload 180 | ); 181 | 182 | try { 183 | const offers = extension.parse( 184 | req.headers['sec-websocket-extensions'] 185 | ); 186 | 187 | if (offers[PerMessageDeflate.extensionName]) { 188 | perMessageDeflate.accept(offers[PerMessageDeflate.extensionName]); 189 | extensions[PerMessageDeflate.extensionName] = perMessageDeflate; 190 | } 191 | } catch (err) { 192 | return abortHandshake(socket, 400); 193 | } 194 | } 195 | 196 | // 197 | // Optionally call external client verification handler. 198 | // 199 | if (this.options.verifyClient) { 200 | const info = { 201 | origin: req.headers[`${version === 8 ? 'sec-websocket-origin' : 'origin'}`], 202 | secure: !!(req.connection.authorized || req.connection.encrypted), 203 | req 204 | }; 205 | 206 | if (this.options.verifyClient.length === 2) { 207 | this.options.verifyClient(info, (verified, code, message, headers) => { 208 | if (!verified) { 209 | return abortHandshake(socket, code || 401, message, headers); 210 | } 211 | 212 | this.completeUpgrade(extensions, req, socket, head, cb); 213 | }); 214 | return; 215 | } 216 | 217 | if (!this.options.verifyClient(info)) return abortHandshake(socket, 401); 218 | } 219 | 220 | this.completeUpgrade(extensions, req, socket, head, cb); 221 | } 222 | 223 | /** 224 | * Upgrade the connection to WebSocket. 225 | * 226 | * @param {Object} extensions The accepted extensions 227 | * @param {http.IncomingMessage} req The request object 228 | * @param {net.Socket} socket The network socket between the server and client 229 | * @param {Buffer} head The first packet of the upgraded stream 230 | * @param {Function} cb Callback 231 | * @private 232 | */ 233 | completeUpgrade (extensions, req, socket, head, cb) { 234 | // 235 | // Destroy the socket if the client has already sent a FIN packet. 236 | // 237 | if (!socket.readable || !socket.writable) return socket.destroy(); 238 | 239 | const key = crypto.createHash('sha1') 240 | .update(req.headers['sec-websocket-key'] + constants.GUID, 'binary') 241 | .digest('base64'); 242 | 243 | const headers = [ 244 | 'HTTP/1.1 101 Switching Protocols', 245 | 'Upgrade: websocket', 246 | 'Connection: Upgrade', 247 | `Sec-WebSocket-Accept: ${key}` 248 | ]; 249 | 250 | const ws = new WebSocket(null); 251 | var protocol = req.headers['sec-websocket-protocol']; 252 | 253 | if (protocol) { 254 | protocol = protocol.trim().split(/ *, */); 255 | 256 | // 257 | // Optionally call external protocol selection handler. 258 | // 259 | if (this.options.handleProtocols) { 260 | protocol = this.options.handleProtocols(protocol, req); 261 | } else { 262 | protocol = protocol[0]; 263 | } 264 | 265 | if (protocol) { 266 | headers.push(`Sec-WebSocket-Protocol: ${protocol}`); 267 | ws.protocol = protocol; 268 | } 269 | } 270 | 271 | if (extensions[PerMessageDeflate.extensionName]) { 272 | const params = extensions[PerMessageDeflate.extensionName].params; 273 | const value = extension.format({ 274 | [PerMessageDeflate.extensionName]: [params] 275 | }); 276 | headers.push(`Sec-WebSocket-Extensions: ${value}`); 277 | ws._extensions = extensions; 278 | } 279 | 280 | // 281 | // Allow external modification/inspection of handshake headers. 282 | // 283 | this.emit('headers', headers, req); 284 | 285 | socket.write(headers.concat('\r\n').join('\r\n')); 286 | socket.removeListener('error', socketOnError); 287 | 288 | ws.setSocket(socket, head, this.options.maxPayload); 289 | 290 | if (this.clients) { 291 | this.clients.add(ws); 292 | ws.on('close', () => this.clients.delete(ws)); 293 | } 294 | 295 | cb(ws); 296 | } 297 | } 298 | 299 | module.exports = WebSocketServer; 300 | 301 | /** 302 | * Add event listeners on an `EventEmitter` using a map of 303 | * pairs. 304 | * 305 | * @param {EventEmitter} server The event emitter 306 | * @param {Object.} map The listeners to add 307 | * @return {Function} A function that will remove the added listeners when called 308 | * @private 309 | */ 310 | function addListeners (server, map) { 311 | for (const event of Object.keys(map)) server.on(event, map[event]); 312 | 313 | return function removeListeners () { 314 | for (const event of Object.keys(map)) { 315 | server.removeListener(event, map[event]); 316 | } 317 | }; 318 | } 319 | 320 | /** 321 | * Handle premature socket errors. 322 | * 323 | * @private 324 | */ 325 | function socketOnError () { 326 | this.destroy(); 327 | } 328 | 329 | /** 330 | * Close the connection when preconditions are not fulfilled. 331 | * 332 | * @param {net.Socket} socket The socket of the upgrade request 333 | * @param {Number} code The HTTP response status code 334 | * @param {String} [message] The HTTP response body 335 | * @param {Object} [headers] Additional HTTP response headers 336 | * @private 337 | */ 338 | function abortHandshake (socket, code, message, headers) { 339 | if (socket.writable) { 340 | message = message || http.STATUS_CODES[code]; 341 | headers = Object.assign({ 342 | 'Connection': 'close', 343 | 'Content-type': 'text/html', 344 | 'Content-Length': Buffer.byteLength(message) 345 | }, headers); 346 | 347 | socket.write( 348 | `HTTP/1.1 ${code} ${http.STATUS_CODES[code]}\r\n` + 349 | Object.keys(headers).map(h => `${h}: ${headers[h]}`).join('\r\n') + 350 | '\r\n\r\n' + 351 | message 352 | ); 353 | } 354 | 355 | socket.removeListener('error', socketOnError); 356 | socket.destroy(); 357 | } 358 | -------------------------------------------------------------------------------- /ChannelClientNodeApp~/node_modules/ws/lib/sender.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const crypto = require('crypto'); 4 | 5 | const PerMessageDeflate = require('./permessage-deflate'); 6 | const bufferUtil = require('./buffer-util'); 7 | const validation = require('./validation'); 8 | const constants = require('./constants'); 9 | 10 | /** 11 | * HyBi Sender implementation. 12 | */ 13 | class Sender { 14 | /** 15 | * Creates a Sender instance. 16 | * 17 | * @param {net.Socket} socket The connection socket 18 | * @param {Object} extensions An object containing the negotiated extensions 19 | */ 20 | constructor (socket, extensions) { 21 | this._extensions = extensions || {}; 22 | this._socket = socket; 23 | 24 | this._firstFragment = true; 25 | this._compress = false; 26 | 27 | this._bufferedBytes = 0; 28 | this._deflating = false; 29 | this._queue = []; 30 | } 31 | 32 | /** 33 | * Frames a piece of data according to the HyBi WebSocket protocol. 34 | * 35 | * @param {Buffer} data The data to frame 36 | * @param {Object} options Options object 37 | * @param {Number} options.opcode The opcode 38 | * @param {Boolean} options.readOnly Specifies whether `data` can be modified 39 | * @param {Boolean} options.fin Specifies whether or not to set the FIN bit 40 | * @param {Boolean} options.mask Specifies whether or not to mask `data` 41 | * @param {Boolean} options.rsv1 Specifies whether or not to set the RSV1 bit 42 | * @return {Buffer[]} The framed data as a list of `Buffer` instances 43 | * @public 44 | */ 45 | static frame (data, options) { 46 | const merge = data.length < 1024 || (options.mask && options.readOnly); 47 | var offset = options.mask ? 6 : 2; 48 | var payloadLength = data.length; 49 | 50 | if (data.length >= 65536) { 51 | offset += 8; 52 | payloadLength = 127; 53 | } else if (data.length > 125) { 54 | offset += 2; 55 | payloadLength = 126; 56 | } 57 | 58 | const target = Buffer.allocUnsafe(merge ? data.length + offset : offset); 59 | 60 | target[0] = options.fin ? options.opcode | 0x80 : options.opcode; 61 | if (options.rsv1) target[0] |= 0x40; 62 | 63 | if (payloadLength === 126) { 64 | target.writeUInt16BE(data.length, 2); 65 | } else if (payloadLength === 127) { 66 | target.writeUInt32BE(0, 2); 67 | target.writeUInt32BE(data.length, 6); 68 | } 69 | 70 | if (!options.mask) { 71 | target[1] = payloadLength; 72 | if (merge) { 73 | data.copy(target, offset); 74 | return [target]; 75 | } 76 | 77 | return [target, data]; 78 | } 79 | 80 | const mask = crypto.randomBytes(4); 81 | 82 | target[1] = payloadLength | 0x80; 83 | target[offset - 4] = mask[0]; 84 | target[offset - 3] = mask[1]; 85 | target[offset - 2] = mask[2]; 86 | target[offset - 1] = mask[3]; 87 | 88 | if (merge) { 89 | bufferUtil.mask(data, mask, target, offset, data.length); 90 | return [target]; 91 | } 92 | 93 | bufferUtil.mask(data, mask, data, 0, data.length); 94 | return [target, data]; 95 | } 96 | 97 | /** 98 | * Sends a close message to the other peer. 99 | * 100 | * @param {(Number|undefined)} code The status code component of the body 101 | * @param {String} data The message component of the body 102 | * @param {Boolean} mask Specifies whether or not to mask the message 103 | * @param {Function} cb Callback 104 | * @public 105 | */ 106 | close (code, data, mask, cb) { 107 | var buf; 108 | 109 | if (code === undefined) { 110 | buf = constants.EMPTY_BUFFER; 111 | } else if (typeof code !== 'number' || !validation.isValidStatusCode(code)) { 112 | throw new TypeError('First argument must be a valid error code number'); 113 | } else if (data === undefined || data === '') { 114 | buf = Buffer.allocUnsafe(2); 115 | buf.writeUInt16BE(code, 0); 116 | } else { 117 | buf = Buffer.allocUnsafe(2 + Buffer.byteLength(data)); 118 | buf.writeUInt16BE(code, 0); 119 | buf.write(data, 2); 120 | } 121 | 122 | if (this._deflating) { 123 | this.enqueue([this.doClose, buf, mask, cb]); 124 | } else { 125 | this.doClose(buf, mask, cb); 126 | } 127 | } 128 | 129 | /** 130 | * Frames and sends a close message. 131 | * 132 | * @param {Buffer} data The message to send 133 | * @param {Boolean} mask Specifies whether or not to mask `data` 134 | * @param {Function} cb Callback 135 | * @private 136 | */ 137 | doClose (data, mask, cb) { 138 | this.sendFrame(Sender.frame(data, { 139 | fin: true, 140 | rsv1: false, 141 | opcode: 0x08, 142 | mask, 143 | readOnly: false 144 | }), cb); 145 | } 146 | 147 | /** 148 | * Sends a ping message to the other peer. 149 | * 150 | * @param {*} data The message to send 151 | * @param {Boolean} mask Specifies whether or not to mask `data` 152 | * @param {Function} cb Callback 153 | * @public 154 | */ 155 | ping (data, mask, cb) { 156 | var readOnly = true; 157 | 158 | if (!Buffer.isBuffer(data)) { 159 | if (data instanceof ArrayBuffer) { 160 | data = Buffer.from(data); 161 | } else if (ArrayBuffer.isView(data)) { 162 | data = viewToBuffer(data); 163 | } else { 164 | data = Buffer.from(data); 165 | readOnly = false; 166 | } 167 | } 168 | 169 | if (this._deflating) { 170 | this.enqueue([this.doPing, data, mask, readOnly, cb]); 171 | } else { 172 | this.doPing(data, mask, readOnly, cb); 173 | } 174 | } 175 | 176 | /** 177 | * Frames and sends a ping message. 178 | * 179 | * @param {*} data The message to send 180 | * @param {Boolean} mask Specifies whether or not to mask `data` 181 | * @param {Boolean} readOnly Specifies whether `data` can be modified 182 | * @param {Function} cb Callback 183 | * @private 184 | */ 185 | doPing (data, mask, readOnly, cb) { 186 | this.sendFrame(Sender.frame(data, { 187 | fin: true, 188 | rsv1: false, 189 | opcode: 0x09, 190 | mask, 191 | readOnly 192 | }), cb); 193 | } 194 | 195 | /** 196 | * Sends a pong message to the other peer. 197 | * 198 | * @param {*} data The message to send 199 | * @param {Boolean} mask Specifies whether or not to mask `data` 200 | * @param {Function} cb Callback 201 | * @public 202 | */ 203 | pong (data, mask, cb) { 204 | var readOnly = true; 205 | 206 | if (!Buffer.isBuffer(data)) { 207 | if (data instanceof ArrayBuffer) { 208 | data = Buffer.from(data); 209 | } else if (ArrayBuffer.isView(data)) { 210 | data = viewToBuffer(data); 211 | } else { 212 | data = Buffer.from(data); 213 | readOnly = false; 214 | } 215 | } 216 | 217 | if (this._deflating) { 218 | this.enqueue([this.doPong, data, mask, readOnly, cb]); 219 | } else { 220 | this.doPong(data, mask, readOnly, cb); 221 | } 222 | } 223 | 224 | /** 225 | * Frames and sends a pong message. 226 | * 227 | * @param {*} data The message to send 228 | * @param {Boolean} mask Specifies whether or not to mask `data` 229 | * @param {Boolean} readOnly Specifies whether `data` can be modified 230 | * @param {Function} cb Callback 231 | * @private 232 | */ 233 | doPong (data, mask, readOnly, cb) { 234 | this.sendFrame(Sender.frame(data, { 235 | fin: true, 236 | rsv1: false, 237 | opcode: 0x0a, 238 | mask, 239 | readOnly 240 | }), cb); 241 | } 242 | 243 | /** 244 | * Sends a data message to the other peer. 245 | * 246 | * @param {*} data The message to send 247 | * @param {Object} options Options object 248 | * @param {Boolean} options.compress Specifies whether or not to compress `data` 249 | * @param {Boolean} options.binary Specifies whether `data` is binary or text 250 | * @param {Boolean} options.fin Specifies whether the fragment is the last one 251 | * @param {Boolean} options.mask Specifies whether or not to mask `data` 252 | * @param {Function} cb Callback 253 | * @public 254 | */ 255 | send (data, options, cb) { 256 | var opcode = options.binary ? 2 : 1; 257 | var rsv1 = options.compress; 258 | var readOnly = true; 259 | 260 | if (!Buffer.isBuffer(data)) { 261 | if (data instanceof ArrayBuffer) { 262 | data = Buffer.from(data); 263 | } else if (ArrayBuffer.isView(data)) { 264 | data = viewToBuffer(data); 265 | } else { 266 | data = Buffer.from(data); 267 | readOnly = false; 268 | } 269 | } 270 | 271 | const perMessageDeflate = this._extensions[PerMessageDeflate.extensionName]; 272 | 273 | if (this._firstFragment) { 274 | this._firstFragment = false; 275 | if (rsv1 && perMessageDeflate) { 276 | rsv1 = data.length >= perMessageDeflate._threshold; 277 | } 278 | this._compress = rsv1; 279 | } else { 280 | rsv1 = false; 281 | opcode = 0; 282 | } 283 | 284 | if (options.fin) this._firstFragment = true; 285 | 286 | if (perMessageDeflate) { 287 | const opts = { 288 | fin: options.fin, 289 | rsv1, 290 | opcode, 291 | mask: options.mask, 292 | readOnly 293 | }; 294 | 295 | if (this._deflating) { 296 | this.enqueue([this.dispatch, data, this._compress, opts, cb]); 297 | } else { 298 | this.dispatch(data, this._compress, opts, cb); 299 | } 300 | } else { 301 | this.sendFrame(Sender.frame(data, { 302 | fin: options.fin, 303 | rsv1: false, 304 | opcode, 305 | mask: options.mask, 306 | readOnly 307 | }), cb); 308 | } 309 | } 310 | 311 | /** 312 | * Dispatches a data message. 313 | * 314 | * @param {Buffer} data The message to send 315 | * @param {Boolean} compress Specifies whether or not to compress `data` 316 | * @param {Object} options Options object 317 | * @param {Number} options.opcode The opcode 318 | * @param {Boolean} options.readOnly Specifies whether `data` can be modified 319 | * @param {Boolean} options.fin Specifies whether or not to set the FIN bit 320 | * @param {Boolean} options.mask Specifies whether or not to mask `data` 321 | * @param {Boolean} options.rsv1 Specifies whether or not to set the RSV1 bit 322 | * @param {Function} cb Callback 323 | * @private 324 | */ 325 | dispatch (data, compress, options, cb) { 326 | if (!compress) { 327 | this.sendFrame(Sender.frame(data, options), cb); 328 | return; 329 | } 330 | 331 | const perMessageDeflate = this._extensions[PerMessageDeflate.extensionName]; 332 | 333 | this._deflating = true; 334 | perMessageDeflate.compress(data, options.fin, (_, buf) => { 335 | options.readOnly = false; 336 | this.sendFrame(Sender.frame(buf, options), cb); 337 | this._deflating = false; 338 | this.dequeue(); 339 | }); 340 | } 341 | 342 | /** 343 | * Executes queued send operations. 344 | * 345 | * @private 346 | */ 347 | dequeue () { 348 | while (!this._deflating && this._queue.length) { 349 | const params = this._queue.shift(); 350 | 351 | this._bufferedBytes -= params[1].length; 352 | params[0].apply(this, params.slice(1)); 353 | } 354 | } 355 | 356 | /** 357 | * Enqueues a send operation. 358 | * 359 | * @param {Array} params Send operation parameters. 360 | * @private 361 | */ 362 | enqueue (params) { 363 | this._bufferedBytes += params[1].length; 364 | this._queue.push(params); 365 | } 366 | 367 | /** 368 | * Sends a frame. 369 | * 370 | * @param {Buffer[]} list The frame to send 371 | * @param {Function} cb Callback 372 | * @private 373 | */ 374 | sendFrame (list, cb) { 375 | if (list.length === 2) { 376 | this._socket.write(list[0]); 377 | this._socket.write(list[1], cb); 378 | } else { 379 | this._socket.write(list[0], cb); 380 | } 381 | } 382 | } 383 | 384 | module.exports = Sender; 385 | 386 | /** 387 | * Converts an `ArrayBuffer` view into a buffer. 388 | * 389 | * @param {(DataView|TypedArray)} view The view to convert 390 | * @return {Buffer} Converted view 391 | * @private 392 | */ 393 | function viewToBuffer (view) { 394 | const buf = Buffer.from(view.buffer); 395 | 396 | if (view.byteLength !== view.buffer.byteLength) { 397 | return buf.slice(view.byteOffset, view.byteOffset + view.byteLength); 398 | } 399 | 400 | return buf; 401 | } 402 | -------------------------------------------------------------------------------- /ChannelClientNodeApp~/node_modules/ws/README.md: -------------------------------------------------------------------------------- 1 | # ws: a Node.js WebSocket library 2 | 3 | [![Version npm](https://img.shields.io/npm/v/ws.svg)](https://www.npmjs.com/package/ws) 4 | [![Linux Build](https://img.shields.io/travis/websockets/ws/master.svg)](https://travis-ci.org/websockets/ws) 5 | [![Windows Build](https://ci.appveyor.com/api/projects/status/github/websockets/ws?branch=master&svg=true)](https://ci.appveyor.com/project/lpinca/ws) 6 | [![Coverage Status](https://img.shields.io/coveralls/websockets/ws/master.svg)](https://coveralls.io/r/websockets/ws?branch=master) 7 | 8 | ws is a simple to use, blazing fast, and thoroughly tested WebSocket client 9 | and server implementation. 10 | 11 | Passes the quite extensive Autobahn test suite: [server][server-report], 12 | [client][client-report]. 13 | 14 | **Note**: This module does not work in the browser. The client in the docs is a 15 | reference to a back end with the role of a client in the WebSocket 16 | communication. Browser clients must use the native 17 | [`WebSocket`](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) object. 18 | To make the same code work seamlessly on Node.js and the browser, you can use 19 | one of the many wrappers available on npm, like 20 | [isomorphic-ws](https://github.com/heineiuo/isomorphic-ws). 21 | 22 | ## Table of Contents 23 | 24 | * [Protocol support](#protocol-support) 25 | * [Installing](#installing) 26 | + [Opt-in for performance and spec compliance](#opt-in-for-performance-and-spec-compliance) 27 | * [API docs](#api-docs) 28 | * [WebSocket compression](#websocket-compression) 29 | * [Usage examples](#usage-examples) 30 | + [Sending and receiving text data](#sending-and-receiving-text-data) 31 | + [Sending binary data](#sending-binary-data) 32 | + [Simple server](#simple-server) 33 | + [External HTTP/S server](#external-https-server) 34 | + [Multiple servers sharing a single HTTP/S server](#multiple-servers-sharing-a-single-https-server) 35 | + [Server broadcast](#server-broadcast) 36 | + [echo.websocket.org demo](#echowebsocketorg-demo) 37 | + [Other examples](#other-examples) 38 | * [Error handling best practices](#error-handling-best-practices) 39 | * [FAQ](#faq) 40 | + [How to get the IP address of the client?](#how-to-get-the-ip-address-of-the-client) 41 | + [How to detect and close broken connections?](#how-to-detect-and-close-broken-connections) 42 | + [How to connect via a proxy?](#how-to-connect-via-a-proxy) 43 | * [Changelog](#changelog) 44 | * [License](#license) 45 | 46 | ## Protocol support 47 | 48 | * **HyBi drafts 07-12** (Use the option `protocolVersion: 8`) 49 | * **HyBi drafts 13-17** (Current default, alternatively option `protocolVersion: 13`) 50 | 51 | ## Installing 52 | 53 | ``` 54 | npm install --save ws 55 | ``` 56 | 57 | ### Opt-in for performance and spec compliance 58 | 59 | There are 2 optional modules that can be installed along side with the ws 60 | module. These modules are binary addons which improve certain operations. 61 | Prebuilt binaries are available for the most popular platforms so you don't 62 | necessarily need to have a C++ compiler installed on your machine. 63 | 64 | - `npm install --save-optional bufferutil`: Allows to efficiently perform 65 | operations such as masking and unmasking the data payload of the WebSocket 66 | frames. 67 | - `npm install --save-optional utf-8-validate`: Allows to efficiently check 68 | if a message contains valid UTF-8 as required by the spec. 69 | 70 | ## API docs 71 | 72 | See [`/doc/ws.md`](./doc/ws.md) for Node.js-like docs for the ws classes. 73 | 74 | ## WebSocket compression 75 | 76 | ws supports the [permessage-deflate extension][permessage-deflate] which 77 | enables the client and server to negotiate a compression algorithm and its 78 | parameters, and then selectively apply it to the data payloads of each 79 | WebSocket message. 80 | 81 | The extension is disabled by default on the server and enabled by default on 82 | the client. It adds a significant overhead in terms of performance and memory 83 | consumption so we suggest to enable it only if it is really needed. 84 | 85 | Note that Node.js has a variety of issues with high-performance compression, 86 | where increased concurrency, especially on Linux, can lead to 87 | [catastrophic memory fragmentation][node-zlib-bug] and slow performance. 88 | If you intend to use permessage-deflate in production, it is worthwhile to set 89 | up a test representative of your workload and ensure Node.js/zlib will handle 90 | it with acceptable performance and memory usage. 91 | 92 | Tuning of permessage-deflate can be done via the options defined below. You can 93 | also use `zlibDeflateOptions` and `zlibInflateOptions`, which is passed directly 94 | into the creation of [raw deflate/inflate streams][node-zlib-deflaterawdocs]. 95 | 96 | See [the docs][ws-server-options] for more options. 97 | 98 | ```js 99 | const WebSocket = require('ws'); 100 | 101 | const wss = new WebSocket.Server({ 102 | port: 8080, 103 | perMessageDeflate: { 104 | zlibDeflateOptions: { // See zlib defaults. 105 | chunkSize: 1024, 106 | memLevel: 7, 107 | level: 3, 108 | }, 109 | zlibInflateOptions: { 110 | chunkSize: 10 * 1024 111 | }, 112 | // Other options settable: 113 | clientNoContextTakeover: true, // Defaults to negotiated value. 114 | serverNoContextTakeover: true, // Defaults to negotiated value. 115 | clientMaxWindowBits: 10, // Defaults to negotiated value. 116 | serverMaxWindowBits: 10, // Defaults to negotiated value. 117 | // Below options specified as default values. 118 | concurrencyLimit: 10, // Limits zlib concurrency for perf. 119 | threshold: 1024, // Size (in bytes) below which messages 120 | // should not be compressed. 121 | } 122 | }); 123 | ``` 124 | 125 | The client will only use the extension if it is supported and enabled on the 126 | server. To always disable the extension on the client set the 127 | `perMessageDeflate` option to `false`. 128 | 129 | ```js 130 | const WebSocket = require('ws'); 131 | 132 | const ws = new WebSocket('ws://www.host.com/path', { 133 | perMessageDeflate: false 134 | }); 135 | ``` 136 | 137 | ## Usage examples 138 | 139 | ### Sending and receiving text data 140 | 141 | ```js 142 | const WebSocket = require('ws'); 143 | 144 | const ws = new WebSocket('ws://www.host.com/path'); 145 | 146 | ws.on('open', function open() { 147 | ws.send('something'); 148 | }); 149 | 150 | ws.on('message', function incoming(data) { 151 | console.log(data); 152 | }); 153 | ``` 154 | 155 | ### Sending binary data 156 | 157 | ```js 158 | const WebSocket = require('ws'); 159 | 160 | const ws = new WebSocket('ws://www.host.com/path'); 161 | 162 | ws.on('open', function open() { 163 | const array = new Float32Array(5); 164 | 165 | for (var i = 0; i < array.length; ++i) { 166 | array[i] = i / 2; 167 | } 168 | 169 | ws.send(array); 170 | }); 171 | ``` 172 | 173 | ### Simple server 174 | 175 | ```js 176 | const WebSocket = require('ws'); 177 | 178 | const wss = new WebSocket.Server({ port: 8080 }); 179 | 180 | wss.on('connection', function connection(ws) { 181 | ws.on('message', function incoming(message) { 182 | console.log('received: %s', message); 183 | }); 184 | 185 | ws.send('something'); 186 | }); 187 | ``` 188 | 189 | ### External HTTP/S server 190 | 191 | ```js 192 | const fs = require('fs'); 193 | const https = require('https'); 194 | const WebSocket = require('ws'); 195 | 196 | const server = new https.createServer({ 197 | cert: fs.readFileSync('/path/to/cert.pem'), 198 | key: fs.readFileSync('/path/to/key.pem') 199 | }); 200 | const wss = new WebSocket.Server({ server }); 201 | 202 | wss.on('connection', function connection(ws) { 203 | ws.on('message', function incoming(message) { 204 | console.log('received: %s', message); 205 | }); 206 | 207 | ws.send('something'); 208 | }); 209 | 210 | server.listen(8080); 211 | ``` 212 | 213 | ### Multiple servers sharing a single HTTP/S server 214 | 215 | ```js 216 | const http = require('http'); 217 | const WebSocket = require('ws'); 218 | 219 | const server = http.createServer(); 220 | const wss1 = new WebSocket.Server({ noServer: true }); 221 | const wss2 = new WebSocket.Server({ noServer: true }); 222 | 223 | wss1.on('connection', function connection(ws) { 224 | // ... 225 | }); 226 | 227 | wss2.on('connection', function connection(ws) { 228 | // ... 229 | }); 230 | 231 | server.on('upgrade', function upgrade(request, socket, head) { 232 | const pathname = url.parse(request.url).pathname; 233 | 234 | if (pathname === '/foo') { 235 | wss1.handleUpgrade(request, socket, head, function done(ws) { 236 | wss1.emit('connection', ws, request); 237 | }); 238 | } else if (pathname === '/bar') { 239 | wss2.handleUpgrade(request, socket, head, function done(ws) { 240 | wss2.emit('connection', ws, request); 241 | }); 242 | } else { 243 | socket.destroy(); 244 | } 245 | }); 246 | 247 | server.listen(8080); 248 | ``` 249 | 250 | ### Server broadcast 251 | 252 | ```js 253 | const WebSocket = require('ws'); 254 | 255 | const wss = new WebSocket.Server({ port: 8080 }); 256 | 257 | // Broadcast to all. 258 | wss.broadcast = function broadcast(data) { 259 | wss.clients.forEach(function each(client) { 260 | if (client.readyState === WebSocket.OPEN) { 261 | client.send(data); 262 | } 263 | }); 264 | }; 265 | 266 | wss.on('connection', function connection(ws) { 267 | ws.on('message', function incoming(data) { 268 | // Broadcast to everyone else. 269 | wss.clients.forEach(function each(client) { 270 | if (client !== ws && client.readyState === WebSocket.OPEN) { 271 | client.send(data); 272 | } 273 | }); 274 | }); 275 | }); 276 | ``` 277 | 278 | ### echo.websocket.org demo 279 | 280 | ```js 281 | const WebSocket = require('ws'); 282 | 283 | const ws = new WebSocket('wss://echo.websocket.org/', { 284 | origin: 'https://websocket.org' 285 | }); 286 | 287 | ws.on('open', function open() { 288 | console.log('connected'); 289 | ws.send(Date.now()); 290 | }); 291 | 292 | ws.on('close', function close() { 293 | console.log('disconnected'); 294 | }); 295 | 296 | ws.on('message', function incoming(data) { 297 | console.log(`Roundtrip time: ${Date.now() - data} ms`); 298 | 299 | setTimeout(function timeout() { 300 | ws.send(Date.now()); 301 | }, 500); 302 | }); 303 | ``` 304 | 305 | ### Other examples 306 | 307 | For a full example with a browser client communicating with a ws server, see the 308 | examples folder. 309 | 310 | Otherwise, see the test cases. 311 | 312 | ## Error handling best practices 313 | 314 | ```js 315 | // If the WebSocket is closed before the following send is attempted 316 | ws.send('something'); 317 | 318 | // Errors (both immediate and async write errors) can be detected in an optional 319 | // callback. The callback is also the only way of being notified that data has 320 | // actually been sent. 321 | ws.send('something', function ack(error) { 322 | // If error is not defined, the send has been completed, otherwise the error 323 | // object will indicate what failed. 324 | }); 325 | 326 | // Immediate errors can also be handled with `try...catch`, but **note** that 327 | // since sends are inherently asynchronous, socket write failures will *not* be 328 | // captured when this technique is used. 329 | try { ws.send('something'); } 330 | catch (e) { /* handle error */ } 331 | ``` 332 | 333 | ## FAQ 334 | 335 | ### How to get the IP address of the client? 336 | 337 | The remote IP address can be obtained from the raw socket. 338 | 339 | ```js 340 | const WebSocket = require('ws'); 341 | 342 | const wss = new WebSocket.Server({ port: 8080 }); 343 | 344 | wss.on('connection', function connection(ws, req) { 345 | const ip = req.connection.remoteAddress; 346 | }); 347 | ``` 348 | 349 | When the server runs behind a proxy like NGINX, the de-facto standard is to use 350 | the `X-Forwarded-For` header. 351 | 352 | ```js 353 | wss.on('connection', function connection(ws, req) { 354 | const ip = req.headers['x-forwarded-for'].split(/\s*,\s*/)[0]; 355 | }); 356 | ``` 357 | 358 | ### How to detect and close broken connections? 359 | 360 | Sometimes the link between the server and the client can be interrupted in a 361 | way that keeps both the server and the client unaware of the broken state of the 362 | connection (e.g. when pulling the cord). 363 | 364 | In these cases ping messages can be used as a means to verify that the remote 365 | endpoint is still responsive. 366 | 367 | ```js 368 | const WebSocket = require('ws'); 369 | 370 | const wss = new WebSocket.Server({ port: 8080 }); 371 | 372 | function noop() {} 373 | 374 | function heartbeat() { 375 | this.isAlive = true; 376 | } 377 | 378 | wss.on('connection', function connection(ws) { 379 | ws.isAlive = true; 380 | ws.on('pong', heartbeat); 381 | }); 382 | 383 | const interval = setInterval(function ping() { 384 | wss.clients.forEach(function each(ws) { 385 | if (ws.isAlive === false) return ws.terminate(); 386 | 387 | ws.isAlive = false; 388 | ws.ping(noop); 389 | }); 390 | }, 30000); 391 | ``` 392 | 393 | Pong messages are automatically sent in response to ping messages as required 394 | by the spec. 395 | 396 | ### How to connect via a proxy? 397 | 398 | Use a custom `http.Agent` implementation like [https-proxy-agent][] or 399 | [socks-proxy-agent][]. 400 | 401 | ## Changelog 402 | 403 | We're using the GitHub [releases][changelog] for changelog entries. 404 | 405 | ## License 406 | 407 | [MIT](LICENSE) 408 | 409 | [https-proxy-agent]: https://github.com/TooTallNate/node-https-proxy-agent 410 | [socks-proxy-agent]: https://github.com/TooTallNate/node-socks-proxy-agent 411 | [client-report]: http://websockets.github.io/ws/autobahn/clients/ 412 | [server-report]: http://websockets.github.io/ws/autobahn/servers/ 413 | [permessage-deflate]: https://tools.ietf.org/html/rfc7692 414 | [changelog]: https://github.com/websockets/ws/releases 415 | [node-zlib-bug]: https://github.com/nodejs/node/issues/8871 416 | [node-zlib-deflaterawdocs]: https://nodejs.org/api/zlib.html#zlib_zlib_createdeflateraw_options 417 | [ws-server-options]: https://github.com/websockets/ws/blob/master/doc/ws.md#new-websocketserveroptions-callback 418 | -------------------------------------------------------------------------------- /ChannelClientNodeApp~/node_modules/ws/lib/receiver.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const stream = require('stream'); 4 | 5 | const PerMessageDeflate = require('./permessage-deflate'); 6 | const bufferUtil = require('./buffer-util'); 7 | const validation = require('./validation'); 8 | const constants = require('./constants'); 9 | 10 | const GET_INFO = 0; 11 | const GET_PAYLOAD_LENGTH_16 = 1; 12 | const GET_PAYLOAD_LENGTH_64 = 2; 13 | const GET_MASK = 3; 14 | const GET_DATA = 4; 15 | const INFLATING = 5; 16 | 17 | /** 18 | * HyBi Receiver implementation. 19 | * 20 | * @extends stream.Writable 21 | */ 22 | class Receiver extends stream.Writable { 23 | /** 24 | * Creates a Receiver instance. 25 | * 26 | * @param {String} binaryType The type for binary data 27 | * @param {Object} extensions An object containing the negotiated extensions 28 | * @param {Number} maxPayload The maximum allowed message length 29 | */ 30 | constructor (binaryType, extensions, maxPayload) { 31 | super(); 32 | 33 | this._binaryType = binaryType || constants.BINARY_TYPES[0]; 34 | this[constants.kWebSocket] = undefined; 35 | this._extensions = extensions || {}; 36 | this._maxPayload = maxPayload | 0; 37 | 38 | this._bufferedBytes = 0; 39 | this._buffers = []; 40 | 41 | this._compressed = false; 42 | this._payloadLength = 0; 43 | this._mask = undefined; 44 | this._fragmented = 0; 45 | this._masked = false; 46 | this._fin = false; 47 | this._opcode = 0; 48 | 49 | this._totalPayloadLength = 0; 50 | this._messageLength = 0; 51 | this._fragments = []; 52 | 53 | this._state = GET_INFO; 54 | this._loop = false; 55 | } 56 | 57 | /** 58 | * Implements `Writable.prototype._write()`. 59 | * 60 | * @param {Buffer} chunk The chunk of data to write 61 | * @param {String} encoding The character encoding of `chunk` 62 | * @param {Function} cb Callback 63 | */ 64 | _write (chunk, encoding, cb) { 65 | if (this._opcode === 0x08) return cb(); 66 | 67 | this._bufferedBytes += chunk.length; 68 | this._buffers.push(chunk); 69 | this.startLoop(cb); 70 | } 71 | 72 | /** 73 | * Consumes `n` bytes from the buffered data. 74 | * 75 | * @param {Number} n The number of bytes to consume 76 | * @return {Buffer} The consumed bytes 77 | * @private 78 | */ 79 | consume (n) { 80 | this._bufferedBytes -= n; 81 | 82 | if (n === this._buffers[0].length) return this._buffers.shift(); 83 | 84 | if (n < this._buffers[0].length) { 85 | const buf = this._buffers[0]; 86 | this._buffers[0] = buf.slice(n); 87 | return buf.slice(0, n); 88 | } 89 | 90 | const dst = Buffer.allocUnsafe(n); 91 | 92 | do { 93 | const buf = this._buffers[0]; 94 | 95 | if (n >= buf.length) { 96 | this._buffers.shift().copy(dst, dst.length - n); 97 | } else { 98 | buf.copy(dst, dst.length - n, 0, n); 99 | this._buffers[0] = buf.slice(n); 100 | } 101 | 102 | n -= buf.length; 103 | } while (n > 0); 104 | 105 | return dst; 106 | } 107 | 108 | /** 109 | * Starts the parsing loop. 110 | * 111 | * @param {Function} cb Callback 112 | * @private 113 | */ 114 | startLoop (cb) { 115 | var err; 116 | this._loop = true; 117 | 118 | do { 119 | switch (this._state) { 120 | case GET_INFO: 121 | err = this.getInfo(); 122 | break; 123 | case GET_PAYLOAD_LENGTH_16: 124 | err = this.getPayloadLength16(); 125 | break; 126 | case GET_PAYLOAD_LENGTH_64: 127 | err = this.getPayloadLength64(); 128 | break; 129 | case GET_MASK: 130 | this.getMask(); 131 | break; 132 | case GET_DATA: 133 | err = this.getData(cb); 134 | break; 135 | default: // `INFLATING` 136 | this._loop = false; 137 | return; 138 | } 139 | } while (this._loop); 140 | 141 | cb(err); 142 | } 143 | 144 | /** 145 | * Reads the first two bytes of a frame. 146 | * 147 | * @return {(RangeError|undefined)} A possible error 148 | * @private 149 | */ 150 | getInfo () { 151 | if (this._bufferedBytes < 2) { 152 | this._loop = false; 153 | return; 154 | } 155 | 156 | const buf = this.consume(2); 157 | 158 | if ((buf[0] & 0x30) !== 0x00) { 159 | this._loop = false; 160 | return error(RangeError, 'RSV2 and RSV3 must be clear', true, 1002); 161 | } 162 | 163 | const compressed = (buf[0] & 0x40) === 0x40; 164 | 165 | if (compressed && !this._extensions[PerMessageDeflate.extensionName]) { 166 | this._loop = false; 167 | return error(RangeError, 'RSV1 must be clear', true, 1002); 168 | } 169 | 170 | this._fin = (buf[0] & 0x80) === 0x80; 171 | this._opcode = buf[0] & 0x0f; 172 | this._payloadLength = buf[1] & 0x7f; 173 | 174 | if (this._opcode === 0x00) { 175 | if (compressed) { 176 | this._loop = false; 177 | return error(RangeError, 'RSV1 must be clear', true, 1002); 178 | } 179 | 180 | if (!this._fragmented) { 181 | this._loop = false; 182 | return error(RangeError, 'invalid opcode 0', true, 1002); 183 | } 184 | 185 | this._opcode = this._fragmented; 186 | } else if (this._opcode === 0x01 || this._opcode === 0x02) { 187 | if (this._fragmented) { 188 | this._loop = false; 189 | return error(RangeError, `invalid opcode ${this._opcode}`, true, 1002); 190 | } 191 | 192 | this._compressed = compressed; 193 | } else if (this._opcode > 0x07 && this._opcode < 0x0b) { 194 | if (!this._fin) { 195 | this._loop = false; 196 | return error(RangeError, 'FIN must be set', true, 1002); 197 | } 198 | 199 | if (compressed) { 200 | this._loop = false; 201 | return error(RangeError, 'RSV1 must be clear', true, 1002); 202 | } 203 | 204 | if (this._payloadLength > 0x7d) { 205 | this._loop = false; 206 | return error( 207 | RangeError, 208 | `invalid payload length ${this._payloadLength}`, 209 | true, 210 | 1002 211 | ); 212 | } 213 | } else { 214 | this._loop = false; 215 | return error(RangeError, `invalid opcode ${this._opcode}`, true, 1002); 216 | } 217 | 218 | if (!this._fin && !this._fragmented) this._fragmented = this._opcode; 219 | this._masked = (buf[1] & 0x80) === 0x80; 220 | 221 | if (this._payloadLength === 126) this._state = GET_PAYLOAD_LENGTH_16; 222 | else if (this._payloadLength === 127) this._state = GET_PAYLOAD_LENGTH_64; 223 | else return this.haveLength(); 224 | } 225 | 226 | /** 227 | * Gets extended payload length (7+16). 228 | * 229 | * @return {(RangeError|undefined)} A possible error 230 | * @private 231 | */ 232 | getPayloadLength16 () { 233 | if (this._bufferedBytes < 2) { 234 | this._loop = false; 235 | return; 236 | } 237 | 238 | this._payloadLength = this.consume(2).readUInt16BE(0); 239 | return this.haveLength(); 240 | } 241 | 242 | /** 243 | * Gets extended payload length (7+64). 244 | * 245 | * @return {(RangeError|undefined)} A possible error 246 | * @private 247 | */ 248 | getPayloadLength64 () { 249 | if (this._bufferedBytes < 8) { 250 | this._loop = false; 251 | return; 252 | } 253 | 254 | const buf = this.consume(8); 255 | const num = buf.readUInt32BE(0); 256 | 257 | // 258 | // The maximum safe integer in JavaScript is 2^53 - 1. An error is returned 259 | // if payload length is greater than this number. 260 | // 261 | if (num > Math.pow(2, 53 - 32) - 1) { 262 | this._loop = false; 263 | return error( 264 | RangeError, 265 | 'Unsupported WebSocket frame: payload length > 2^53 - 1', 266 | false, 267 | 1009 268 | ); 269 | } 270 | 271 | this._payloadLength = num * Math.pow(2, 32) + buf.readUInt32BE(4); 272 | return this.haveLength(); 273 | } 274 | 275 | /** 276 | * Payload length has been read. 277 | * 278 | * @return {(RangeError|undefined)} A possible error 279 | * @private 280 | */ 281 | haveLength () { 282 | if (this._payloadLength && this._opcode < 0x08) { 283 | this._totalPayloadLength += this._payloadLength; 284 | if (this._totalPayloadLength > this._maxPayload && this._maxPayload > 0) { 285 | this._loop = false; 286 | return error(RangeError, 'Max payload size exceeded', false, 1009); 287 | } 288 | } 289 | 290 | if (this._masked) this._state = GET_MASK; 291 | else this._state = GET_DATA; 292 | } 293 | 294 | /** 295 | * Reads mask bytes. 296 | * 297 | * @private 298 | */ 299 | getMask () { 300 | if (this._bufferedBytes < 4) { 301 | this._loop = false; 302 | return; 303 | } 304 | 305 | this._mask = this.consume(4); 306 | this._state = GET_DATA; 307 | } 308 | 309 | /** 310 | * Reads data bytes. 311 | * 312 | * @param {Function} cb Callback 313 | * @return {(Error|RangeError|undefined)} A possible error 314 | * @private 315 | */ 316 | getData (cb) { 317 | var data = constants.EMPTY_BUFFER; 318 | 319 | if (this._payloadLength) { 320 | if (this._bufferedBytes < this._payloadLength) { 321 | this._loop = false; 322 | return; 323 | } 324 | 325 | data = this.consume(this._payloadLength); 326 | if (this._masked) bufferUtil.unmask(data, this._mask); 327 | } 328 | 329 | if (this._opcode > 0x07) return this.controlMessage(data); 330 | 331 | if (this._compressed) { 332 | this._state = INFLATING; 333 | this.decompress(data, cb); 334 | return; 335 | } 336 | 337 | if (data.length) { 338 | // 339 | // This message is not compressed so its lenght is the sum of the payload 340 | // length of all fragments. 341 | // 342 | this._messageLength = this._totalPayloadLength; 343 | this._fragments.push(data); 344 | } 345 | 346 | return this.dataMessage(); 347 | } 348 | 349 | /** 350 | * Decompresses data. 351 | * 352 | * @param {Buffer} data Compressed data 353 | * @param {Function} cb Callback 354 | * @private 355 | */ 356 | decompress (data, cb) { 357 | const perMessageDeflate = this._extensions[PerMessageDeflate.extensionName]; 358 | 359 | perMessageDeflate.decompress(data, this._fin, (err, buf) => { 360 | if (err) return cb(err); 361 | 362 | if (buf.length) { 363 | this._messageLength += buf.length; 364 | if (this._messageLength > this._maxPayload && this._maxPayload > 0) { 365 | return cb(error(RangeError, 'Max payload size exceeded', false, 1009)); 366 | } 367 | 368 | this._fragments.push(buf); 369 | } 370 | 371 | const er = this.dataMessage(); 372 | if (er) return cb(er); 373 | 374 | this.startLoop(cb); 375 | }); 376 | } 377 | 378 | /** 379 | * Handles a data message. 380 | * 381 | * @return {(Error|undefined)} A possible error 382 | * @private 383 | */ 384 | dataMessage () { 385 | if (this._fin) { 386 | const messageLength = this._messageLength; 387 | const fragments = this._fragments; 388 | 389 | this._totalPayloadLength = 0; 390 | this._messageLength = 0; 391 | this._fragmented = 0; 392 | this._fragments = []; 393 | 394 | if (this._opcode === 2) { 395 | var data; 396 | 397 | if (this._binaryType === 'nodebuffer') { 398 | data = toBuffer(fragments, messageLength); 399 | } else if (this._binaryType === 'arraybuffer') { 400 | data = toArrayBuffer(toBuffer(fragments, messageLength)); 401 | } else { 402 | data = fragments; 403 | } 404 | 405 | this.emit('message', data); 406 | } else { 407 | const buf = toBuffer(fragments, messageLength); 408 | 409 | if (!validation.isValidUTF8(buf)) { 410 | this._loop = false; 411 | return error(Error, 'invalid UTF-8 sequence', true, 1007); 412 | } 413 | 414 | this.emit('message', buf.toString()); 415 | } 416 | } 417 | 418 | this._state = GET_INFO; 419 | } 420 | 421 | /** 422 | * Handles a control message. 423 | * 424 | * @param {Buffer} data Data to handle 425 | * @return {(Error|RangeError|undefined)} A possible error 426 | * @private 427 | */ 428 | controlMessage (data) { 429 | if (this._opcode === 0x08) { 430 | this._loop = false; 431 | 432 | if (data.length === 0) { 433 | this.emit('conclude', 1005, ''); 434 | this.end(); 435 | } else if (data.length === 1) { 436 | return error(RangeError, 'invalid payload length 1', true, 1002); 437 | } else { 438 | const code = data.readUInt16BE(0); 439 | 440 | if (!validation.isValidStatusCode(code)) { 441 | return error(RangeError, `invalid status code ${code}`, true, 1002); 442 | } 443 | 444 | const buf = data.slice(2); 445 | 446 | if (!validation.isValidUTF8(buf)) { 447 | return error(Error, 'invalid UTF-8 sequence', true, 1007); 448 | } 449 | 450 | this.emit('conclude', code, buf.toString()); 451 | this.end(); 452 | } 453 | 454 | return; 455 | } 456 | 457 | if (this._opcode === 0x09) this.emit('ping', data); 458 | else this.emit('pong', data); 459 | 460 | this._state = GET_INFO; 461 | } 462 | } 463 | 464 | module.exports = Receiver; 465 | 466 | /** 467 | * Builds an error object. 468 | * 469 | * @param {(Error|RangeError)} ErrorCtor The error constructor 470 | * @param {String} message The error message 471 | * @param {Boolean} prefix Specifies whether or not to add a default prefix to 472 | * `message` 473 | * @param {Number} statusCode The status code 474 | * @return {(Error|RangeError)} The error 475 | * @private 476 | */ 477 | function error (ErrorCtor, message, prefix, statusCode) { 478 | const err = new ErrorCtor( 479 | prefix ? `Invalid WebSocket frame: ${message}` : message 480 | ); 481 | 482 | Error.captureStackTrace(err, error); 483 | err[constants.kStatusCode] = statusCode; 484 | return err; 485 | } 486 | 487 | /** 488 | * Makes a buffer from a list of fragments. 489 | * 490 | * @param {Buffer[]} fragments The list of fragments composing the message 491 | * @param {Number} messageLength The length of the message 492 | * @return {Buffer} 493 | * @private 494 | */ 495 | function toBuffer (fragments, messageLength) { 496 | if (fragments.length === 1) return fragments[0]; 497 | if (fragments.length > 1) return bufferUtil.concat(fragments, messageLength); 498 | return constants.EMPTY_BUFFER; 499 | } 500 | 501 | /** 502 | * Converts a buffer to an `ArrayBuffer`. 503 | * 504 | * @param {Buffer} The buffer to convert 505 | * @return {ArrayBuffer} Converted buffer 506 | */ 507 | function toArrayBuffer (buf) { 508 | if (buf.byteOffset === 0 && buf.byteLength === buf.buffer.byteLength) { 509 | return buf.buffer; 510 | } 511 | 512 | return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength); 513 | } 514 | -------------------------------------------------------------------------------- /ChannelClientNodeApp~/node_modules/ws/lib/permessage-deflate.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Limiter = require('async-limiter'); 4 | const zlib = require('zlib'); 5 | 6 | const bufferUtil = require('./buffer-util'); 7 | const constants = require('./constants'); 8 | 9 | const TRAILER = Buffer.from([0x00, 0x00, 0xff, 0xff]); 10 | const EMPTY_BLOCK = Buffer.from([0x00]); 11 | 12 | const kPerMessageDeflate = Symbol('permessage-deflate'); 13 | const kWriteInProgress = Symbol('write-in-progress'); 14 | const kPendingClose = Symbol('pending-close'); 15 | const kTotalLength = Symbol('total-length'); 16 | const kCallback = Symbol('callback'); 17 | const kBuffers = Symbol('buffers'); 18 | const kError = Symbol('error'); 19 | 20 | // 21 | // We limit zlib concurrency, which prevents severe memory fragmentation 22 | // as documented in https://github.com/nodejs/node/issues/8871#issuecomment-250915913 23 | // and https://github.com/websockets/ws/issues/1202 24 | // 25 | // Intentionally global; it's the global thread pool that's an issue. 26 | // 27 | let zlibLimiter; 28 | 29 | /** 30 | * permessage-deflate implementation. 31 | */ 32 | class PerMessageDeflate { 33 | /** 34 | * Creates a PerMessageDeflate instance. 35 | * 36 | * @param {Object} options Configuration options 37 | * @param {Boolean} options.serverNoContextTakeover Request/accept disabling 38 | * of server context takeover 39 | * @param {Boolean} options.clientNoContextTakeover Advertise/acknowledge 40 | * disabling of client context takeover 41 | * @param {(Boolean|Number)} options.serverMaxWindowBits Request/confirm the 42 | * use of a custom server window size 43 | * @param {(Boolean|Number)} options.clientMaxWindowBits Advertise support 44 | * for, or request, a custom client window size 45 | * @param {Object} options.zlibDeflateOptions Options to pass to zlib on deflate 46 | * @param {Object} options.zlibInflateOptions Options to pass to zlib on inflate 47 | * @param {Number} options.threshold Size (in bytes) below which messages 48 | * should not be compressed 49 | * @param {Number} options.concurrencyLimit The number of concurrent calls to 50 | * zlib 51 | * @param {Boolean} isServer Create the instance in either server or client 52 | * mode 53 | * @param {Number} maxPayload The maximum allowed message length 54 | */ 55 | constructor (options, isServer, maxPayload) { 56 | this._maxPayload = maxPayload | 0; 57 | this._options = options || {}; 58 | this._threshold = this._options.threshold !== undefined 59 | ? this._options.threshold 60 | : 1024; 61 | this._isServer = !!isServer; 62 | this._deflate = null; 63 | this._inflate = null; 64 | 65 | this.params = null; 66 | 67 | if (!zlibLimiter) { 68 | const concurrency = this._options.concurrencyLimit !== undefined 69 | ? this._options.concurrencyLimit 70 | : 10; 71 | zlibLimiter = new Limiter({ concurrency }); 72 | } 73 | } 74 | 75 | /** 76 | * @type {String} 77 | */ 78 | static get extensionName () { 79 | return 'permessage-deflate'; 80 | } 81 | 82 | /** 83 | * Create an extension negotiation offer. 84 | * 85 | * @return {Object} Extension parameters 86 | * @public 87 | */ 88 | offer () { 89 | const params = {}; 90 | 91 | if (this._options.serverNoContextTakeover) { 92 | params.server_no_context_takeover = true; 93 | } 94 | if (this._options.clientNoContextTakeover) { 95 | params.client_no_context_takeover = true; 96 | } 97 | if (this._options.serverMaxWindowBits) { 98 | params.server_max_window_bits = this._options.serverMaxWindowBits; 99 | } 100 | if (this._options.clientMaxWindowBits) { 101 | params.client_max_window_bits = this._options.clientMaxWindowBits; 102 | } else if (this._options.clientMaxWindowBits == null) { 103 | params.client_max_window_bits = true; 104 | } 105 | 106 | return params; 107 | } 108 | 109 | /** 110 | * Accept an extension negotiation offer/response. 111 | * 112 | * @param {Array} configurations The extension negotiation offers/reponse 113 | * @return {Object} Accepted configuration 114 | * @public 115 | */ 116 | accept (configurations) { 117 | configurations = this.normalizeParams(configurations); 118 | 119 | this.params = this._isServer 120 | ? this.acceptAsServer(configurations) 121 | : this.acceptAsClient(configurations); 122 | 123 | return this.params; 124 | } 125 | 126 | /** 127 | * Releases all resources used by the extension. 128 | * 129 | * @public 130 | */ 131 | cleanup () { 132 | if (this._inflate) { 133 | if (this._inflate[kWriteInProgress]) { 134 | this._inflate[kPendingClose] = true; 135 | } else { 136 | this._inflate.close(); 137 | this._inflate = null; 138 | } 139 | } 140 | if (this._deflate) { 141 | if (this._deflate[kWriteInProgress]) { 142 | this._deflate[kPendingClose] = true; 143 | } else { 144 | this._deflate.close(); 145 | this._deflate = null; 146 | } 147 | } 148 | } 149 | 150 | /** 151 | * Accept an extension negotiation offer. 152 | * 153 | * @param {Array} offers The extension negotiation offers 154 | * @return {Object} Accepted configuration 155 | * @private 156 | */ 157 | acceptAsServer (offers) { 158 | const opts = this._options; 159 | const accepted = offers.find((params) => { 160 | if ( 161 | (opts.serverNoContextTakeover === false && 162 | params.server_no_context_takeover) || 163 | (params.server_max_window_bits && 164 | (opts.serverMaxWindowBits === false || 165 | (typeof opts.serverMaxWindowBits === 'number' && 166 | opts.serverMaxWindowBits > params.server_max_window_bits))) || 167 | (typeof opts.clientMaxWindowBits === 'number' && 168 | !params.client_max_window_bits) 169 | ) { 170 | return false; 171 | } 172 | 173 | return true; 174 | }); 175 | 176 | if (!accepted) { 177 | throw new Error('None of the extension offers can be accepted'); 178 | } 179 | 180 | if (opts.serverNoContextTakeover) { 181 | accepted.server_no_context_takeover = true; 182 | } 183 | if (opts.clientNoContextTakeover) { 184 | accepted.client_no_context_takeover = true; 185 | } 186 | if (typeof opts.serverMaxWindowBits === 'number') { 187 | accepted.server_max_window_bits = opts.serverMaxWindowBits; 188 | } 189 | if (typeof opts.clientMaxWindowBits === 'number') { 190 | accepted.client_max_window_bits = opts.clientMaxWindowBits; 191 | } else if ( 192 | accepted.client_max_window_bits === true || 193 | opts.clientMaxWindowBits === false 194 | ) { 195 | delete accepted.client_max_window_bits; 196 | } 197 | 198 | return accepted; 199 | } 200 | 201 | /** 202 | * Accept the extension negotiation response. 203 | * 204 | * @param {Array} response The extension negotiation response 205 | * @return {Object} Accepted configuration 206 | * @private 207 | */ 208 | acceptAsClient (response) { 209 | const params = response[0]; 210 | 211 | if ( 212 | this._options.clientNoContextTakeover === false && 213 | params.client_no_context_takeover 214 | ) { 215 | throw new Error('Unexpected parameter "client_no_context_takeover"'); 216 | } 217 | 218 | if (!params.client_max_window_bits) { 219 | if (typeof this._options.clientMaxWindowBits === 'number') { 220 | params.client_max_window_bits = this._options.clientMaxWindowBits; 221 | } 222 | } else if ( 223 | this._options.clientMaxWindowBits === false || 224 | (typeof this._options.clientMaxWindowBits === 'number' && 225 | params.client_max_window_bits > this._options.clientMaxWindowBits) 226 | ) { 227 | throw new Error( 228 | 'Unexpected or invalid parameter "client_max_window_bits"' 229 | ); 230 | } 231 | 232 | return params; 233 | } 234 | 235 | /** 236 | * Normalize parameters. 237 | * 238 | * @param {Array} configurations The extension negotiation offers/reponse 239 | * @return {Array} The offers/response with normalized parameters 240 | * @private 241 | */ 242 | normalizeParams (configurations) { 243 | configurations.forEach((params) => { 244 | Object.keys(params).forEach((key) => { 245 | var value = params[key]; 246 | 247 | if (value.length > 1) { 248 | throw new Error(`Parameter "${key}" must have only a single value`); 249 | } 250 | 251 | value = value[0]; 252 | 253 | if (key === 'client_max_window_bits') { 254 | if (value !== true) { 255 | const num = +value; 256 | if (!Number.isInteger(num) || num < 8 || num > 15) { 257 | throw new TypeError( 258 | `Invalid value for parameter "${key}": ${value}` 259 | ); 260 | } 261 | value = num; 262 | } else if (!this._isServer) { 263 | throw new TypeError( 264 | `Invalid value for parameter "${key}": ${value}` 265 | ); 266 | } 267 | } else if (key === 'server_max_window_bits') { 268 | const num = +value; 269 | if (!Number.isInteger(num) || num < 8 || num > 15) { 270 | throw new TypeError( 271 | `Invalid value for parameter "${key}": ${value}` 272 | ); 273 | } 274 | value = num; 275 | } else if ( 276 | key === 'client_no_context_takeover' || 277 | key === 'server_no_context_takeover' 278 | ) { 279 | if (value !== true) { 280 | throw new TypeError( 281 | `Invalid value for parameter "${key}": ${value}` 282 | ); 283 | } 284 | } else { 285 | throw new Error(`Unknown parameter "${key}"`); 286 | } 287 | 288 | params[key] = value; 289 | }); 290 | }); 291 | 292 | return configurations; 293 | } 294 | 295 | /** 296 | * Decompress data. Concurrency limited by async-limiter. 297 | * 298 | * @param {Buffer} data Compressed data 299 | * @param {Boolean} fin Specifies whether or not this is the last fragment 300 | * @param {Function} callback Callback 301 | * @public 302 | */ 303 | decompress (data, fin, callback) { 304 | zlibLimiter.push((done) => { 305 | this._decompress(data, fin, (err, result) => { 306 | done(); 307 | callback(err, result); 308 | }); 309 | }); 310 | } 311 | 312 | /** 313 | * Compress data. Concurrency limited by async-limiter. 314 | * 315 | * @param {Buffer} data Data to compress 316 | * @param {Boolean} fin Specifies whether or not this is the last fragment 317 | * @param {Function} callback Callback 318 | * @public 319 | */ 320 | compress (data, fin, callback) { 321 | zlibLimiter.push((done) => { 322 | this._compress(data, fin, (err, result) => { 323 | done(); 324 | callback(err, result); 325 | }); 326 | }); 327 | } 328 | 329 | /** 330 | * Decompress data. 331 | * 332 | * @param {Buffer} data Compressed data 333 | * @param {Boolean} fin Specifies whether or not this is the last fragment 334 | * @param {Function} callback Callback 335 | * @private 336 | */ 337 | _decompress (data, fin, callback) { 338 | const endpoint = this._isServer ? 'client' : 'server'; 339 | 340 | if (!this._inflate) { 341 | const key = `${endpoint}_max_window_bits`; 342 | const windowBits = typeof this.params[key] !== 'number' 343 | ? zlib.Z_DEFAULT_WINDOWBITS 344 | : this.params[key]; 345 | 346 | this._inflate = zlib.createInflateRaw( 347 | Object.assign({}, this._options.zlibInflateOptions, { windowBits }) 348 | ); 349 | this._inflate[kPerMessageDeflate] = this; 350 | this._inflate[kTotalLength] = 0; 351 | this._inflate[kBuffers] = []; 352 | this._inflate.on('error', inflateOnError); 353 | this._inflate.on('data', inflateOnData); 354 | } 355 | 356 | this._inflate[kCallback] = callback; 357 | this._inflate[kWriteInProgress] = true; 358 | 359 | this._inflate.write(data); 360 | if (fin) this._inflate.write(TRAILER); 361 | 362 | this._inflate.flush(() => { 363 | const err = this._inflate[kError]; 364 | 365 | if (err) { 366 | this._inflate.close(); 367 | this._inflate = null; 368 | callback(err); 369 | return; 370 | } 371 | 372 | const data = bufferUtil.concat( 373 | this._inflate[kBuffers], 374 | this._inflate[kTotalLength] 375 | ); 376 | 377 | if ( 378 | (fin && this.params[`${endpoint}_no_context_takeover`]) || 379 | this._inflate[kPendingClose] 380 | ) { 381 | this._inflate.close(); 382 | this._inflate = null; 383 | } else { 384 | this._inflate[kWriteInProgress] = false; 385 | this._inflate[kTotalLength] = 0; 386 | this._inflate[kBuffers] = []; 387 | } 388 | 389 | callback(null, data); 390 | }); 391 | } 392 | 393 | /** 394 | * Compress data. 395 | * 396 | * @param {Buffer} data Data to compress 397 | * @param {Boolean} fin Specifies whether or not this is the last fragment 398 | * @param {Function} callback Callback 399 | * @private 400 | */ 401 | _compress (data, fin, callback) { 402 | if (!data || data.length === 0) { 403 | process.nextTick(callback, null, EMPTY_BLOCK); 404 | return; 405 | } 406 | 407 | const endpoint = this._isServer ? 'server' : 'client'; 408 | 409 | if (!this._deflate) { 410 | const key = `${endpoint}_max_window_bits`; 411 | const windowBits = typeof this.params[key] !== 'number' 412 | ? zlib.Z_DEFAULT_WINDOWBITS 413 | : this.params[key]; 414 | 415 | this._deflate = zlib.createDeflateRaw( 416 | Object.assign( 417 | // TODO deprecate memLevel/level and recommend zlibDeflateOptions instead 418 | { 419 | memLevel: this._options.memLevel, 420 | level: this._options.level 421 | }, 422 | this._options.zlibDeflateOptions, 423 | { windowBits } 424 | ) 425 | ); 426 | 427 | this._deflate[kTotalLength] = 0; 428 | this._deflate[kBuffers] = []; 429 | 430 | // 431 | // `zlib.DeflateRaw` emits an `'error'` event only when an attempt to use 432 | // it is made after it has already been closed. This cannot happen here, 433 | // so we only add a listener for the `'data'` event. 434 | // 435 | this._deflate.on('data', deflateOnData); 436 | } 437 | 438 | this._deflate[kWriteInProgress] = true; 439 | 440 | this._deflate.write(data); 441 | this._deflate.flush(zlib.Z_SYNC_FLUSH, () => { 442 | var data = bufferUtil.concat( 443 | this._deflate[kBuffers], 444 | this._deflate[kTotalLength] 445 | ); 446 | 447 | if (fin) data = data.slice(0, data.length - 4); 448 | 449 | if ( 450 | (fin && this.params[`${endpoint}_no_context_takeover`]) || 451 | this._deflate[kPendingClose] 452 | ) { 453 | this._deflate.close(); 454 | this._deflate = null; 455 | } else { 456 | this._deflate[kWriteInProgress] = false; 457 | this._deflate[kTotalLength] = 0; 458 | this._deflate[kBuffers] = []; 459 | } 460 | 461 | callback(null, data); 462 | }); 463 | } 464 | } 465 | 466 | module.exports = PerMessageDeflate; 467 | 468 | /** 469 | * The listener of the `zlib.DeflateRaw` stream `'data'` event. 470 | * 471 | * @param {Buffer} chunk A chunk of data 472 | * @private 473 | */ 474 | function deflateOnData (chunk) { 475 | this[kBuffers].push(chunk); 476 | this[kTotalLength] += chunk.length; 477 | } 478 | 479 | /** 480 | * The listener of the `zlib.InflateRaw` stream `'data'` event. 481 | * 482 | * @param {Buffer} chunk A chunk of data 483 | * @private 484 | */ 485 | function inflateOnData (chunk) { 486 | this[kTotalLength] += chunk.length; 487 | 488 | if ( 489 | this[kPerMessageDeflate]._maxPayload < 1 || 490 | this[kTotalLength] <= this[kPerMessageDeflate]._maxPayload 491 | ) { 492 | this[kBuffers].push(chunk); 493 | return; 494 | } 495 | 496 | this[kError] = new RangeError('Max payload size exceeded'); 497 | this[kError][constants.kStatusCode] = 1009; 498 | this.removeListener('data', inflateOnData); 499 | this.reset(); 500 | } 501 | 502 | /** 503 | * The listener of the `zlib.InflateRaw` stream `'error'` event. 504 | * 505 | * @param {Error} err The emitted error 506 | * @private 507 | */ 508 | function inflateOnError (err) { 509 | // 510 | // There is no need to call `Zlib#close()` as the handle is automatically 511 | // closed when an error is emitted. 512 | // 513 | this[kPerMessageDeflate]._inflate = null; 514 | err[constants.kStatusCode] = 1007; 515 | this[kCallback](err); 516 | } 517 | -------------------------------------------------------------------------------- /ChannelClientNodeApp~/event-service.js: -------------------------------------------------------------------------------- 1 | /** 2 | * EventService module 3 | * @module event-service 4 | */ 5 | 6 | const kRequest = "request"; 7 | const kRequestAcknowledge = "requestAck"; 8 | const kRequestExecute = "requestExecute"; 9 | const kRequestResult = "requestResult"; 10 | 11 | const kEvent = "event"; 12 | const kLog = "log"; 13 | const kRequestDefaultTimeout = 700; 14 | 15 | const kAddress = "127.0.0.1"; 16 | 17 | /** 18 | * Callback used to handle an Emit or Request. 19 | * 20 | * @callback onHandler 21 | * @param {string} eventType The type of the event. Corresponds to the eventType used in Emit and Request. 22 | * @param {object[]} data The data sent by Emit or Request. 23 | * @returns {*} Nothing in the case of a Emit handler, otherwise anything. 24 | */ 25 | 26 | /** 27 | * Callback used to handle the result of a Request. 28 | * 29 | * @callback promiseHandler 30 | * @param {Error} err The error, if there was an error or the request has been cancelled. Null otherwise. 31 | * @param {object[]} data The result of all request handlers. 32 | */ 33 | 34 | /** 35 | * Callback used to unregister an event handler registered with the function On. 36 | * 37 | * @callback offHandler 38 | */ 39 | 40 | /** 41 | * Map of event handlers. 42 | * @type {Map} 43 | */ 44 | let s_Events = new Map(); 45 | 46 | /** 47 | * Map of requests. 48 | * @type {Map} 49 | */ 50 | let s_Requests = new Map(); 51 | 52 | let s_Ws = null; 53 | let s_Connected = false; 54 | let s_ConnectionId = -1; 55 | 56 | let s_RequestId = 0; 57 | 58 | let s_TickTimer = null; 59 | 60 | /** 61 | * Enum for event data serialization. 62 | * @readonly 63 | * @enum {number} 64 | */ 65 | const EventDataSerialization = { 66 | /** @type {number} */ 67 | StandardJson: 0, 68 | /** @type {number} */ 69 | JsonUtility: 1 70 | } 71 | 72 | class RequestData { 73 | /** @type {string} */ 74 | eventType; 75 | /** @type {number} */ 76 | id; 77 | /** @type {promiseHandler[]} */ 78 | promises; 79 | /** @type {number} */ 80 | offerStartTime; 81 | /** @type {boolean} */ 82 | isAcknowledged; 83 | /** @type {object} */ 84 | data; 85 | /** @type {number} */ 86 | timeoutInMs; 87 | /** @type {object} */ 88 | dataInfos; 89 | } 90 | 91 | class RequestMessage { 92 | /** @type {string} */ 93 | reqType; 94 | /** @type {string} */ 95 | eventType; 96 | /** @type {number} */ 97 | senderId; 98 | /** @type {number} */ 99 | requestId; 100 | /** @type {object} */ 101 | data; 102 | /** @type {object} */ 103 | dataInfos; 104 | /** @type {EventDataSerialization} */ 105 | eventDataSerialization; 106 | } 107 | 108 | // API functions 109 | 110 | export class OperationCanceledException extends Error { 111 | /** 112 | * Construct a new OperationCanceledException. 113 | * @param {string} message The error message. 114 | */ 115 | constructor(message) { 116 | super(message); 117 | this.name = 'OperationCanceledException'; 118 | } 119 | } 120 | 121 | export class TimeoutException extends Error { 122 | /** 123 | * Construct a new TimeoutException. 124 | * @param {string} message The error message. 125 | */ 126 | constructor(message) { 127 | super(message); 128 | this.name = 'TimeoutException'; 129 | } 130 | } 131 | 132 | /** 133 | * Start the EventService. 134 | * @param {number} port Port on which to connect to. 135 | */ 136 | export function Start(port) { 137 | const connectTo = `ws://${kAddress}:${port}/event`; 138 | s_Ws = new WebSocket(connectTo); 139 | s_Ws.onopen = OnOpen; 140 | s_Ws.onclose = OnClose; 141 | s_Ws.onerror = OnError; 142 | s_Ws.onmessage = OnMessage; 143 | 144 | s_TickTimer = setInterval(Tick, 0); 145 | } 146 | 147 | /** 148 | * Close the EventService. This function is called automatically when the websocket is closed or there is an error. 149 | */ 150 | export function Close() { 151 | s_Connected = false; 152 | s_ConnectionId = -1; 153 | Clear(); 154 | 155 | clearInterval(s_TickTimer); 156 | } 157 | 158 | /** 159 | * Clears all pending requests. 160 | */ 161 | export function Clear() { 162 | s_Requests.clear(); 163 | } 164 | 165 | /** 166 | * Register an event handler for a specific event raised by Emit or Request. 167 | * @param {string} eventType The type of event to handle. 168 | * @param {onHandler} onHandler Callback that handles the event. 169 | * @returns {offHandler} Callback that you can call to unregister the handler. 170 | */ 171 | export function On(eventType, onHandler) { 172 | let handlers = null; 173 | if (!s_Events.has(eventType)) { 174 | handlers = []; 175 | s_Events.set(eventType, handlers); 176 | } 177 | handlers = s_Events.get(eventType); 178 | if (handlers.includes(onHandler)) { 179 | throw "Cannot add existing event handler: " + eventType; 180 | } 181 | handlers.push(onHandler); 182 | 183 | return () => { 184 | Off(eventType, onHandler) 185 | } 186 | } 187 | 188 | /** 189 | * Unregister an event handler. 190 | * @param {string} eventType The type of event to handle. 191 | * @param {onHandler} onHandler The handler to unregister. 192 | */ 193 | export function Off(eventType, onHandler) { 194 | if (s_Events.has(eventType)) { 195 | let handlers = s_Events.get(eventType); 196 | let index = handlers.indexOf(onHandler) 197 | if (index > -1) 198 | handlers.splice(index, 1); 199 | if (handlers.length === 0) { 200 | s_Events.delete(eventType); 201 | } 202 | } 203 | } 204 | 205 | /** 206 | * Check if the EventService is connected to the server. 207 | * @return {boolean} True if the EventService is connected. 208 | */ 209 | export function IsConnected() { 210 | return s_Connected; 211 | } 212 | 213 | /** 214 | * Emit an event with some values. 215 | * @param {string} eventType The type of event to emit. 216 | * @param {object|object[]} args Values to send. Can be null. 217 | * @param {number} [targetId=-1] Target client Id. -1 for any client. 218 | */ 219 | export function Emit(eventType, args, targetId = -1) { 220 | if (!Array.isArray(args) && !!args) { 221 | args = [args]; 222 | } 223 | 224 | let notifyWildcard = true; 225 | let req = CreateRequest(kEvent, eventType, targetId, -1, args); 226 | 227 | NotifyLocalListeners(eventType, args, notifyWildcard); 228 | 229 | SendRequest(req); 230 | } 231 | 232 | /** 233 | * Check if there is any request pending for a specific event. 234 | * @param {string} eventType Type of event. 235 | * @returns {boolean} True if there is any request pending. 236 | */ 237 | export function IsRequestPending(eventType) { 238 | return s_Requests.has(eventType); 239 | } 240 | 241 | /** 242 | * Cancel any pending requests for a specific event. 243 | * @param {string} eventType Type of event. 244 | * @param {string} [message=null] Message to send to the request handler. 245 | * @returns {boolean} True if the request was canceled. 246 | */ 247 | export function CancelRequest(eventType, message = null) { 248 | if (!s_Requests.has(eventType)) 249 | return false; 250 | 251 | let request = s_Requests.get(eventType); 252 | CleanRequest(eventType); 253 | Reject(request, new OperationCanceledException(!!message ? message : `Request ${eventType} canceled`)); 254 | return true; 255 | } 256 | 257 | /** 258 | * Send a request to anyone who is connected to the EventService. 259 | * @param {string} eventType Type of event to request. 260 | * @param {promiseHandler} promiseHandler Callback that handles the result of the request. 261 | * @param {object|object[]} args Values to send with the request. Can be null. 262 | * @param {number} [timeoutInMs=] Number of milliseconds to wait before cancelling the request automatically. 263 | */ 264 | export function Request(eventType, promiseHandler, args, timeoutInMs = kRequestDefaultTimeout) { 265 | if (!Array.isArray(args) && !!args) { 266 | args = [args]; 267 | } 268 | 269 | if (s_Requests.has(eventType)) { 270 | let request = s_Requests.get(eventType); 271 | request.promises.push(promiseHandler); 272 | return; 273 | } 274 | 275 | let request = new RequestData(); 276 | request.eventType = eventType; 277 | request.promises = [promiseHandler]; 278 | request.timeoutInMs = timeoutInMs; 279 | 280 | if (HasHandlers(eventType)) { 281 | let results = NotifyLocalListeners(eventType, args, false); 282 | 283 | let exception = results.find(r => r instanceof Error); 284 | if (exception) { 285 | Reject(request, exception); 286 | } 287 | else { 288 | Resolve(request, results); 289 | } 290 | } 291 | else { 292 | request.offerStartTime = Date.now(); 293 | let requestId = GetNewRequestId(); 294 | request.id = requestId; 295 | 296 | let msg = CreateRequest(kRequest, eventType, -1, requestId, args); 297 | SendRequest(msg); 298 | 299 | s_Requests.set(eventType, request); 300 | request.data = msg.data; 301 | request.dataInfos = msg.dataInfos; 302 | } 303 | } 304 | 305 | /** 306 | * Send a message to the server to be printed in the console. 307 | * @param {string} msg Message to log. 308 | */ 309 | export function Log(msg) { 310 | let req = CreateRequest(kLog, null, -1, -1, msg, null); 311 | SendRequest(req); 312 | } 313 | 314 | // Private functions 315 | function OnOpen(ev) { 316 | } 317 | 318 | function OnClose(ev) { 319 | Close(); 320 | } 321 | 322 | function OnError(ev) { 323 | Close(); 324 | console.error(ev); 325 | } 326 | 327 | function OnMessage(ev) { 328 | if (IsConnected()) { 329 | HandleIncomingEvent(ev) 330 | } else { 331 | // The server sends us our connectionId in plain text 332 | let data = ev["data"]; 333 | s_ConnectionId = parseInt(data); 334 | s_Connected = true; 335 | } 336 | } 337 | 338 | function CreateRequest(msgType, eventType, targetId, requestId, args, dataInfos) { 339 | let req = {}; 340 | req["req"] = msgType; 341 | if (targetId != -1) { 342 | req["targetId"] = targetId; 343 | } 344 | if (!!eventType) 345 | req["type"] = eventType; 346 | req["senderId"] = s_ConnectionId; 347 | if (requestId) 348 | req["requestId"] = requestId; 349 | req["data"] = args; 350 | if (dataInfos) 351 | req["dataInfos"] = dataInfos; 352 | 353 | return req; 354 | } 355 | 356 | function SendRequest(request) { 357 | s_Ws.send(JSON.stringify(request)); 358 | } 359 | 360 | function NotifyLocalListeners(eventType, data, notifyWildcard) { 361 | let result = []; 362 | if (s_Events.has(eventType)) { 363 | let handlers = s_Events.get(eventType); 364 | try { 365 | for (const handler of handlers) { 366 | result.push(handler(eventType, data)); 367 | } 368 | } 369 | catch (ex) { 370 | result = [ex]; 371 | console.error(ex); 372 | } 373 | } 374 | 375 | if (notifyWildcard && s_Events.has("*")) { 376 | let handlers = s_Events.get(eventType); 377 | try { 378 | for (const handler of handlers) { 379 | handler(eventType, data); 380 | } 381 | } 382 | catch (ex) { 383 | console.error(ex); 384 | } 385 | } 386 | 387 | return result; 388 | } 389 | 390 | function HandleIncomingEvent(event) { 391 | let msg = DeserializeEvent(event); 392 | if (!msg) 393 | return; 394 | 395 | switch (msg.reqType) { 396 | case kRequest: // Receiver 397 | // We are able to answer this request. Acknowledge it to the sender: 398 | if (HasHandlers(msg.eventType)) { 399 | let response = CreateRequest(kRequestAcknowledge, msg.eventType, msg.senderId, msg.requestId, null, null); 400 | SendRequest(response); 401 | } 402 | break; 403 | case kRequestAcknowledge: // Request emitter 404 | let pendingRequest = GetPendingRequest(msg.eventType, msg.requestId); 405 | if (pendingRequest != null) { 406 | // A client is able to fulfill the request: proceed with request execution: 407 | pendingRequest.isAcknowledged = true; 408 | pendingRequest.offerStartTime = Date.now(); 409 | 410 | let message = CreateRequest(kRequestExecute, msg.eventType, msg.senderId, msg.requestId, pendingRequest.data, pendingRequest.dataInfos); 411 | SendRequest(message); 412 | } 413 | // else Request might potentially have timed out. 414 | break; 415 | case kRequestExecute: // Request receiver 416 | { 417 | // We are fulfilling the request: send the execution results 418 | let results = NotifyLocalListeners(msg.eventType, msg.data, false); 419 | let response = CreateRequest(kRequestResult, msg.eventType, msg.senderId, msg.requestId, results, msg.eventDataSerialization); 420 | SendRequest(response); 421 | break; 422 | } 423 | case kRequestResult: // Request emitter 424 | let pendingRequestAwaitingResult = GetPendingRequest(msg.eventType, msg.requestId); 425 | if (pendingRequestAwaitingResult != null) { 426 | let timeForSuccess = Date.now() - pendingRequestAwaitingResult.offerStartTime; 427 | console.log(`[UMPE] Request ${msg.eventType} successful in ${timeForSuccess} ms`); 428 | Resolve(pendingRequestAwaitingResult, msg.data); 429 | CleanRequest(msg.eventType); 430 | } 431 | break; 432 | case kEvent: 433 | { 434 | NotifyLocalListeners(msg.eventType, msg.data, true); 435 | break; 436 | } 437 | } 438 | } 439 | 440 | function DeserializeEvent(event) { 441 | let msg = JSON.parse(event.data); 442 | 443 | if (!msg) { 444 | console.error("Invalid message: " + event); 445 | return null; 446 | } 447 | 448 | if (!msg.hasOwnProperty("type")) { 449 | console.error("Message doesn't contain type: " + event); 450 | return null; 451 | } 452 | 453 | if (!msg.hasOwnProperty("req")) { 454 | console.error("Message doesn't contain req: " + event); 455 | return null; 456 | } 457 | 458 | if (!msg.hasOwnProperty("senderId")) { 459 | console.error("Message doesn't contain senderId: " + event); 460 | return null; 461 | } 462 | 463 | // If we are receiving our own messages, bail out. 464 | if (msg.senderId === s_ConnectionId) { 465 | return null; 466 | } 467 | 468 | let deserializedMsg = new RequestMessage(); 469 | 470 | deserializedMsg.reqType = msg.req; 471 | deserializedMsg.eventType = msg.type; 472 | deserializedMsg.senderId = msg.senderId; 473 | deserializedMsg.requestId = -1; 474 | if (msg.hasOwnProperty("requestId")) 475 | deserializedMsg.requestId = msg.requestId; 476 | 477 | if (msg.hasOwnProperty("data")) 478 | deserializedMsg.data = msg.data; 479 | if (msg.hasOwnProperty("dataInfos")) { 480 | deserializedMsg.dataInfos = msg.dataInfos; 481 | deserializedMsg.eventDataSerialization = EventDataSerialization.JsonUtility; 482 | } 483 | 484 | return deserializedMsg; 485 | } 486 | 487 | function CleanRequest(eventType) { 488 | s_Requests.delete(eventType); 489 | } 490 | 491 | function Resolve(offer, results) { 492 | for (let i = 0, end = offer.promises.length; i != end; ++i) { 493 | try { 494 | offer.promises[i](null, results); 495 | } 496 | catch (e) { 497 | console.error(e); 498 | } 499 | } 500 | } 501 | 502 | function Reject(offer, err) { 503 | for (let i = 0, end = offer.promises.length; i != end; ++i) { 504 | try { 505 | offer.promises[i](err, null); 506 | } 507 | catch (e) { 508 | console.error(e); 509 | } 510 | } 511 | } 512 | 513 | function HasHandlers(eventType) { 514 | return s_Events.has(eventType) && s_Events.get(eventType).length > 0; 515 | } 516 | 517 | function GetNewRequestId() { 518 | return ++s_RequestId; 519 | } 520 | 521 | function GetPendingRequest(eventType, requestId) { 522 | if (!s_Requests.has(eventType)) { 523 | return null; 524 | } 525 | let pendingRequest = s_Requests.get(eventType); 526 | if (pendingRequest != null && pendingRequest.id != requestId) { 527 | console.log(`Request Id mismatch: (Pending)${pendingRequest.id} vs (request)${requestId}`); 528 | // Mismatch request: clean it. 529 | CleanRequest(eventType); 530 | pendingRequest = null; 531 | } 532 | return pendingRequest != null && pendingRequest.id == requestId ? pendingRequest : null; 533 | } 534 | 535 | function Tick() { 536 | if (!IsConnected()) 537 | return; 538 | 539 | if (s_Requests.size > 0) { 540 | let now = Date.now(); 541 | for (let request of s_Requests.values()) { 542 | let elapsedTime = now - request.offerStartTime; 543 | if (request.isAcknowledged) 544 | continue; 545 | if (elapsedTime > request.timeoutInMs) { 546 | CleanRequest(request.eventType); 547 | Reject(request, new TimeoutException(`Request timeout for ${request.eventType} (${elapsedTime} > ${request.timeoutInMs})`)); 548 | } 549 | } 550 | } 551 | } 552 | -------------------------------------------------------------------------------- /ChannelClientNodeApp~/node_modules/ws/lib/websocket.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const EventEmitter = require('events'); 4 | const crypto = require('crypto'); 5 | const https = require('https'); 6 | const http = require('http'); 7 | const net = require('net'); 8 | const tls = require('tls'); 9 | const url = require('url'); 10 | 11 | const PerMessageDeflate = require('./permessage-deflate'); 12 | const EventTarget = require('./event-target'); 13 | const extension = require('./extension'); 14 | const constants = require('./constants'); 15 | const Receiver = require('./receiver'); 16 | const Sender = require('./sender'); 17 | 18 | const readyStates = ['CONNECTING', 'OPEN', 'CLOSING', 'CLOSED']; 19 | const kWebSocket = constants.kWebSocket; 20 | const protocolVersions = [8, 13]; 21 | const closeTimeout = 30 * 1000; // Allow 30 seconds to terminate the connection cleanly. 22 | 23 | /** 24 | * Class representing a WebSocket. 25 | * 26 | * @extends EventEmitter 27 | */ 28 | class WebSocket extends EventEmitter { 29 | /** 30 | * Create a new `WebSocket`. 31 | * 32 | * @param {(String|url.Url|url.URL)} address The URL to which to connect 33 | * @param {(String|String[])} protocols The subprotocols 34 | * @param {Object} options Connection options 35 | */ 36 | constructor (address, protocols, options) { 37 | super(); 38 | 39 | this.readyState = WebSocket.CONNECTING; 40 | this.protocol = ''; 41 | 42 | this._binaryType = constants.BINARY_TYPES[0]; 43 | this._closeFrameReceived = false; 44 | this._closeFrameSent = false; 45 | this._closeMessage = ''; 46 | this._closeTimer = null; 47 | this._closeCode = 1006; 48 | this._extensions = {}; 49 | this._isServer = true; 50 | this._receiver = null; 51 | this._sender = null; 52 | this._socket = null; 53 | 54 | if (address !== null) { 55 | if (Array.isArray(protocols)) { 56 | protocols = protocols.join(', '); 57 | } else if (typeof protocols === 'object' && protocols !== null) { 58 | options = protocols; 59 | protocols = undefined; 60 | } 61 | 62 | initAsClient.call(this, address, protocols, options); 63 | } 64 | } 65 | 66 | get CONNECTING () { return WebSocket.CONNECTING; } 67 | get CLOSING () { return WebSocket.CLOSING; } 68 | get CLOSED () { return WebSocket.CLOSED; } 69 | get OPEN () { return WebSocket.OPEN; } 70 | 71 | /** 72 | * This deviates from the WHATWG interface since ws doesn't support the required 73 | * default "blob" type (instead we define a custom "nodebuffer" type). 74 | * 75 | * @type {String} 76 | */ 77 | get binaryType () { 78 | return this._binaryType; 79 | } 80 | 81 | set binaryType (type) { 82 | if (constants.BINARY_TYPES.indexOf(type) < 0) return; 83 | 84 | this._binaryType = type; 85 | 86 | // 87 | // Allow to change `binaryType` on the fly. 88 | // 89 | if (this._receiver) this._receiver._binaryType = type; 90 | } 91 | 92 | /** 93 | * @type {Number} 94 | */ 95 | get bufferedAmount () { 96 | if (!this._socket) return 0; 97 | 98 | // 99 | // `socket.bufferSize` is `undefined` if the socket is closed. 100 | // 101 | return (this._socket.bufferSize || 0) + this._sender._bufferedBytes; 102 | } 103 | 104 | /** 105 | * @type {String} 106 | */ 107 | get extensions () { 108 | return Object.keys(this._extensions).join(); 109 | } 110 | 111 | /** 112 | * Set up the socket and the internal resources. 113 | * 114 | * @param {net.Socket} socket The network socket between the server and client 115 | * @param {Buffer} head The first packet of the upgraded stream 116 | * @param {Number} maxPayload The maximum allowed message size 117 | * @private 118 | */ 119 | setSocket (socket, head, maxPayload) { 120 | const receiver = new Receiver( 121 | this._binaryType, 122 | this._extensions, 123 | maxPayload 124 | ); 125 | 126 | this._sender = new Sender(socket, this._extensions); 127 | this._receiver = receiver; 128 | this._socket = socket; 129 | 130 | receiver[kWebSocket] = this; 131 | socket[kWebSocket] = this; 132 | 133 | receiver.on('conclude', receiverOnConclude); 134 | receiver.on('drain', receiverOnDrain); 135 | receiver.on('error', receiverOnError); 136 | receiver.on('message', receiverOnMessage); 137 | receiver.on('ping', receiverOnPing); 138 | receiver.on('pong', receiverOnPong); 139 | 140 | socket.setTimeout(0); 141 | socket.setNoDelay(); 142 | 143 | if (head.length > 0) socket.unshift(head); 144 | 145 | socket.on('close', socketOnClose); 146 | socket.on('data', socketOnData); 147 | socket.on('end', socketOnEnd); 148 | socket.on('error', socketOnError); 149 | 150 | this.readyState = WebSocket.OPEN; 151 | this.emit('open'); 152 | } 153 | 154 | /** 155 | * Emit the `'close'` event. 156 | * 157 | * @private 158 | */ 159 | emitClose () { 160 | this.readyState = WebSocket.CLOSED; 161 | 162 | if (!this._socket) { 163 | this.emit('close', this._closeCode, this._closeMessage); 164 | return; 165 | } 166 | 167 | if (this._extensions[PerMessageDeflate.extensionName]) { 168 | this._extensions[PerMessageDeflate.extensionName].cleanup(); 169 | } 170 | 171 | this._receiver.removeAllListeners(); 172 | this.emit('close', this._closeCode, this._closeMessage); 173 | } 174 | 175 | /** 176 | * Start a closing handshake. 177 | * 178 | * +----------+ +-----------+ +----------+ 179 | * - - -|ws.close()|-->|close frame|-->|ws.close()|- - - 180 | * | +----------+ +-----------+ +----------+ | 181 | * +----------+ +-----------+ | 182 | * CLOSING |ws.close()|<--|close frame|<--+-----+ CLOSING 183 | * +----------+ +-----------+ | 184 | * | | | +---+ | 185 | * +------------------------+-->|fin| - - - - 186 | * | +---+ | +---+ 187 | * - - - - -|fin|<---------------------+ 188 | * +---+ 189 | * 190 | * @param {Number} code Status code explaining why the connection is closing 191 | * @param {String} data A string explaining why the connection is closing 192 | * @public 193 | */ 194 | close (code, data) { 195 | if (this.readyState === WebSocket.CLOSED) return; 196 | if (this.readyState === WebSocket.CONNECTING) { 197 | const msg = 'WebSocket was closed before the connection was established'; 198 | return abortHandshake(this, this._req, msg); 199 | } 200 | 201 | if (this.readyState === WebSocket.CLOSING) { 202 | if (this._closeFrameSent && this._closeFrameReceived) this._socket.end(); 203 | return; 204 | } 205 | 206 | this.readyState = WebSocket.CLOSING; 207 | this._sender.close(code, data, !this._isServer, (err) => { 208 | // 209 | // This error is handled by the `'error'` listener on the socket. We only 210 | // want to know if the close frame has been sent here. 211 | // 212 | if (err) return; 213 | 214 | this._closeFrameSent = true; 215 | 216 | if (this._socket.writable) { 217 | if (this._closeFrameReceived) this._socket.end(); 218 | 219 | // 220 | // Ensure that the connection is closed even if the closing handshake 221 | // fails. 222 | // 223 | this._closeTimer = setTimeout( 224 | this._socket.destroy.bind(this._socket), 225 | closeTimeout 226 | ); 227 | } 228 | }); 229 | } 230 | 231 | /** 232 | * Send a ping. 233 | * 234 | * @param {*} data The data to send 235 | * @param {Boolean} mask Indicates whether or not to mask `data` 236 | * @param {Function} cb Callback which is executed when the ping is sent 237 | * @public 238 | */ 239 | ping (data, mask, cb) { 240 | if (typeof data === 'function') { 241 | cb = data; 242 | data = mask = undefined; 243 | } else if (typeof mask === 'function') { 244 | cb = mask; 245 | mask = undefined; 246 | } 247 | 248 | if (this.readyState !== WebSocket.OPEN) { 249 | const err = new Error( 250 | `WebSocket is not open: readyState ${this.readyState} ` + 251 | `(${readyStates[this.readyState]})` 252 | ); 253 | 254 | if (cb) return cb(err); 255 | throw err; 256 | } 257 | 258 | if (typeof data === 'number') data = data.toString(); 259 | if (mask === undefined) mask = !this._isServer; 260 | this._sender.ping(data || constants.EMPTY_BUFFER, mask, cb); 261 | } 262 | 263 | /** 264 | * Send a pong. 265 | * 266 | * @param {*} data The data to send 267 | * @param {Boolean} mask Indicates whether or not to mask `data` 268 | * @param {Function} cb Callback which is executed when the pong is sent 269 | * @public 270 | */ 271 | pong (data, mask, cb) { 272 | if (typeof data === 'function') { 273 | cb = data; 274 | data = mask = undefined; 275 | } else if (typeof mask === 'function') { 276 | cb = mask; 277 | mask = undefined; 278 | } 279 | 280 | if (this.readyState !== WebSocket.OPEN) { 281 | const err = new Error( 282 | `WebSocket is not open: readyState ${this.readyState} ` + 283 | `(${readyStates[this.readyState]})` 284 | ); 285 | 286 | if (cb) return cb(err); 287 | throw err; 288 | } 289 | 290 | if (typeof data === 'number') data = data.toString(); 291 | if (mask === undefined) mask = !this._isServer; 292 | this._sender.pong(data || constants.EMPTY_BUFFER, mask, cb); 293 | } 294 | 295 | /** 296 | * Send a data message. 297 | * 298 | * @param {*} data The message to send 299 | * @param {Object} options Options object 300 | * @param {Boolean} options.compress Specifies whether or not to compress `data` 301 | * @param {Boolean} options.binary Specifies whether `data` is binary or text 302 | * @param {Boolean} options.fin Specifies whether the fragment is the last one 303 | * @param {Boolean} options.mask Specifies whether or not to mask `data` 304 | * @param {Function} cb Callback which is executed when data is written out 305 | * @public 306 | */ 307 | send (data, options, cb) { 308 | if (typeof options === 'function') { 309 | cb = options; 310 | options = {}; 311 | } 312 | 313 | if (this.readyState !== WebSocket.OPEN) { 314 | const err = new Error( 315 | `WebSocket is not open: readyState ${this.readyState} ` + 316 | `(${readyStates[this.readyState]})` 317 | ); 318 | 319 | if (cb) return cb(err); 320 | throw err; 321 | } 322 | 323 | if (typeof data === 'number') data = data.toString(); 324 | 325 | const opts = Object.assign({ 326 | binary: typeof data !== 'string', 327 | mask: !this._isServer, 328 | compress: true, 329 | fin: true 330 | }, options); 331 | 332 | if (!this._extensions[PerMessageDeflate.extensionName]) { 333 | opts.compress = false; 334 | } 335 | 336 | this._sender.send(data || constants.EMPTY_BUFFER, opts, cb); 337 | } 338 | 339 | /** 340 | * Forcibly close the connection. 341 | * 342 | * @public 343 | */ 344 | terminate () { 345 | if (this.readyState === WebSocket.CLOSED) return; 346 | if (this.readyState === WebSocket.CONNECTING) { 347 | const msg = 'WebSocket was closed before the connection was established'; 348 | return abortHandshake(this, this._req, msg); 349 | } 350 | 351 | if (this._socket) { 352 | this.readyState = WebSocket.CLOSING; 353 | this._socket.destroy(); 354 | } 355 | } 356 | } 357 | 358 | readyStates.forEach((readyState, i) => { 359 | WebSocket[readyStates[i]] = i; 360 | }); 361 | 362 | // 363 | // Add the `onopen`, `onerror`, `onclose`, and `onmessage` attributes. 364 | // See https://html.spec.whatwg.org/multipage/comms.html#the-websocket-interface 365 | // 366 | ['open', 'error', 'close', 'message'].forEach((method) => { 367 | Object.defineProperty(WebSocket.prototype, `on${method}`, { 368 | /** 369 | * Return the listener of the event. 370 | * 371 | * @return {(Function|undefined)} The event listener or `undefined` 372 | * @public 373 | */ 374 | get () { 375 | const listeners = this.listeners(method); 376 | for (var i = 0; i < listeners.length; i++) { 377 | if (listeners[i]._listener) return listeners[i]._listener; 378 | } 379 | }, 380 | /** 381 | * Add a listener for the event. 382 | * 383 | * @param {Function} listener The listener to add 384 | * @public 385 | */ 386 | set (listener) { 387 | const listeners = this.listeners(method); 388 | for (var i = 0; i < listeners.length; i++) { 389 | // 390 | // Remove only the listeners added via `addEventListener`. 391 | // 392 | if (listeners[i]._listener) this.removeListener(method, listeners[i]); 393 | } 394 | this.addEventListener(method, listener); 395 | } 396 | }); 397 | }); 398 | 399 | WebSocket.prototype.addEventListener = EventTarget.addEventListener; 400 | WebSocket.prototype.removeEventListener = EventTarget.removeEventListener; 401 | 402 | module.exports = WebSocket; 403 | 404 | /** 405 | * Initialize a WebSocket client. 406 | * 407 | * @param {(String|url.Url|url.URL)} address The URL to which to connect 408 | * @param {String} protocols The subprotocols 409 | * @param {Object} options Connection options 410 | * @param {(Boolean|Object)} options.perMessageDeflate Enable/disable permessage-deflate 411 | * @param {Number} options.handshakeTimeout Timeout in milliseconds for the handshake request 412 | * @param {Number} options.protocolVersion Value of the `Sec-WebSocket-Version` header 413 | * @param {String} options.origin Value of the `Origin` or `Sec-WebSocket-Origin` header 414 | * @private 415 | */ 416 | function initAsClient (address, protocols, options) { 417 | options = Object.assign({ 418 | protocolVersion: protocolVersions[1], 419 | perMessageDeflate: true 420 | }, options, { 421 | createConnection: undefined, 422 | socketPath: undefined, 423 | hostname: undefined, 424 | protocol: undefined, 425 | timeout: undefined, 426 | method: undefined, 427 | auth: undefined, 428 | host: undefined, 429 | path: undefined, 430 | port: undefined 431 | }); 432 | 433 | if (protocolVersions.indexOf(options.protocolVersion) === -1) { 434 | throw new RangeError( 435 | `Unsupported protocol version: ${options.protocolVersion} ` + 436 | `(supported versions: ${protocolVersions.join(', ')})` 437 | ); 438 | } 439 | 440 | this._isServer = false; 441 | 442 | var parsedUrl; 443 | 444 | if (typeof address === 'object' && address.href !== undefined) { 445 | parsedUrl = address; 446 | this.url = address.href; 447 | } else { 448 | parsedUrl = url.parse(address); 449 | this.url = address; 450 | } 451 | 452 | const isUnixSocket = parsedUrl.protocol === 'ws+unix:'; 453 | 454 | if (!parsedUrl.host && (!isUnixSocket || !parsedUrl.pathname)) { 455 | throw new Error(`Invalid URL: ${this.url}`); 456 | } 457 | 458 | const isSecure = parsedUrl.protocol === 'wss:' || parsedUrl.protocol === 'https:'; 459 | const key = crypto.randomBytes(16).toString('base64'); 460 | const httpObj = isSecure ? https : http; 461 | const path = parsedUrl.search 462 | ? `${parsedUrl.pathname || '/'}${parsedUrl.search}` 463 | : parsedUrl.pathname || '/'; 464 | var perMessageDeflate; 465 | 466 | options.createConnection = isSecure ? tlsConnect : netConnect; 467 | options.port = parsedUrl.port || (isSecure ? 443 : 80); 468 | options.host = parsedUrl.hostname.startsWith('[') 469 | ? parsedUrl.hostname.slice(1, -1) 470 | : parsedUrl.hostname; 471 | options.headers = Object.assign({ 472 | 'Sec-WebSocket-Version': options.protocolVersion, 473 | 'Sec-WebSocket-Key': key, 474 | 'Connection': 'Upgrade', 475 | 'Upgrade': 'websocket' 476 | }, options.headers); 477 | options.path = path; 478 | 479 | if (options.perMessageDeflate) { 480 | perMessageDeflate = new PerMessageDeflate( 481 | options.perMessageDeflate !== true ? options.perMessageDeflate : {}, 482 | false 483 | ); 484 | options.headers['Sec-WebSocket-Extensions'] = extension.format({ 485 | [PerMessageDeflate.extensionName]: perMessageDeflate.offer() 486 | }); 487 | } 488 | if (protocols) { 489 | options.headers['Sec-WebSocket-Protocol'] = protocols; 490 | } 491 | if (options.origin) { 492 | if (options.protocolVersion < 13) { 493 | options.headers['Sec-WebSocket-Origin'] = options.origin; 494 | } else { 495 | options.headers.Origin = options.origin; 496 | } 497 | } 498 | if (parsedUrl.auth) { 499 | options.auth = parsedUrl.auth; 500 | } else if (parsedUrl.username || parsedUrl.password) { 501 | options.auth = `${parsedUrl.username}:${parsedUrl.password}`; 502 | } 503 | 504 | if (isUnixSocket) { 505 | const parts = path.split(':'); 506 | 507 | if (options.agent == null && process.versions.modules < 57) { 508 | // 509 | // Setting `socketPath` in conjunction with `createConnection` without an 510 | // agent throws an error on Node.js < 8. Work around the issue by using a 511 | // different property. 512 | // 513 | options._socketPath = parts[0]; 514 | } else { 515 | options.socketPath = parts[0]; 516 | } 517 | 518 | options.path = parts[1]; 519 | } 520 | 521 | var req = this._req = httpObj.get(options); 522 | 523 | if (options.handshakeTimeout) { 524 | req.setTimeout( 525 | options.handshakeTimeout, 526 | () => abortHandshake(this, req, 'Opening handshake has timed out') 527 | ); 528 | } 529 | 530 | req.on('error', (err) => { 531 | if (this._req.aborted) return; 532 | 533 | req = this._req = null; 534 | this.readyState = WebSocket.CLOSING; 535 | this.emit('error', err); 536 | this.emitClose(); 537 | }); 538 | 539 | req.on('response', (res) => { 540 | if (this.emit('unexpected-response', req, res)) return; 541 | 542 | abortHandshake(this, req, `Unexpected server response: ${res.statusCode}`); 543 | }); 544 | 545 | req.on('upgrade', (res, socket, head) => { 546 | this.emit('upgrade', res); 547 | 548 | // 549 | // The user may have closed the connection from a listener of the `upgrade` 550 | // event. 551 | // 552 | if (this.readyState !== WebSocket.CONNECTING) return; 553 | 554 | req = this._req = null; 555 | 556 | const digest = crypto.createHash('sha1') 557 | .update(key + constants.GUID, 'binary') 558 | .digest('base64'); 559 | 560 | if (res.headers['sec-websocket-accept'] !== digest) { 561 | abortHandshake(this, socket, 'Invalid Sec-WebSocket-Accept header'); 562 | return; 563 | } 564 | 565 | const serverProt = res.headers['sec-websocket-protocol']; 566 | const protList = (protocols || '').split(/, */); 567 | var protError; 568 | 569 | if (!protocols && serverProt) { 570 | protError = 'Server sent a subprotocol but none was requested'; 571 | } else if (protocols && !serverProt) { 572 | protError = 'Server sent no subprotocol'; 573 | } else if (serverProt && protList.indexOf(serverProt) === -1) { 574 | protError = 'Server sent an invalid subprotocol'; 575 | } 576 | 577 | if (protError) { 578 | abortHandshake(this, socket, protError); 579 | return; 580 | } 581 | 582 | if (serverProt) this.protocol = serverProt; 583 | 584 | if (perMessageDeflate) { 585 | try { 586 | const extensions = extension.parse( 587 | res.headers['sec-websocket-extensions'] 588 | ); 589 | 590 | if (extensions[PerMessageDeflate.extensionName]) { 591 | perMessageDeflate.accept( 592 | extensions[PerMessageDeflate.extensionName] 593 | ); 594 | this._extensions[PerMessageDeflate.extensionName] = perMessageDeflate; 595 | } 596 | } catch (err) { 597 | abortHandshake(this, socket, 'Invalid Sec-WebSocket-Extensions header'); 598 | return; 599 | } 600 | } 601 | 602 | this.setSocket(socket, head, 0); 603 | }); 604 | } 605 | 606 | /** 607 | * Create a `net.Socket` and initiate a connection. 608 | * 609 | * @param {Object} options Connection options 610 | * @return {net.Socket} The newly created socket used to start the connection 611 | * @private 612 | */ 613 | function netConnect (options) { 614 | options.path = options.socketPath || options._socketPath || undefined; 615 | return net.connect(options); 616 | } 617 | 618 | /** 619 | * Create a `tls.TLSSocket` and initiate a connection. 620 | * 621 | * @param {Object} options Connection options 622 | * @return {tls.TLSSocket} The newly created socket used to start the connection 623 | * @private 624 | */ 625 | function tlsConnect (options) { 626 | options.path = options.socketPath || options._socketPath || undefined; 627 | options.servername = options.servername || options.host; 628 | return tls.connect(options); 629 | } 630 | 631 | /** 632 | * Abort the handshake and emit an error. 633 | * 634 | * @param {WebSocket} websocket The WebSocket instance 635 | * @param {(http.ClientRequest|net.Socket)} stream The request to abort or the 636 | * socket to destroy 637 | * @param {String} message The error message 638 | * @private 639 | */ 640 | function abortHandshake (websocket, stream, message) { 641 | websocket.readyState = WebSocket.CLOSING; 642 | 643 | const err = new Error(message); 644 | Error.captureStackTrace(err, abortHandshake); 645 | 646 | if (stream.setHeader) { 647 | stream.abort(); 648 | stream.once('abort', websocket.emitClose.bind(websocket)); 649 | websocket.emit('error', err); 650 | } else { 651 | stream.destroy(err); 652 | stream.once('error', websocket.emit.bind(websocket, 'error')); 653 | stream.once('close', websocket.emitClose.bind(websocket)); 654 | } 655 | } 656 | 657 | /** 658 | * The listener of the `Receiver` `'conclude'` event. 659 | * 660 | * @param {Number} code The status code 661 | * @param {String} reason The reason for closing 662 | * @private 663 | */ 664 | function receiverOnConclude (code, reason) { 665 | const websocket = this[kWebSocket]; 666 | 667 | websocket._socket.removeListener('data', socketOnData); 668 | websocket._socket.resume(); 669 | 670 | websocket._closeFrameReceived = true; 671 | websocket._closeMessage = reason; 672 | websocket._closeCode = code; 673 | 674 | if (code === 1005) websocket.close(); 675 | else websocket.close(code, reason); 676 | } 677 | 678 | /** 679 | * The listener of the `Receiver` `'drain'` event. 680 | * 681 | * @private 682 | */ 683 | function receiverOnDrain () { 684 | this[kWebSocket]._socket.resume(); 685 | } 686 | 687 | /** 688 | * The listener of the `Receiver` `'error'` event. 689 | * 690 | * @param {(RangeError|Error)} err The emitted error 691 | * @private 692 | */ 693 | function receiverOnError (err) { 694 | const websocket = this[kWebSocket]; 695 | 696 | websocket._socket.removeListener('data', socketOnData); 697 | 698 | websocket.readyState = WebSocket.CLOSING; 699 | websocket._closeCode = err[constants.kStatusCode]; 700 | websocket.emit('error', err); 701 | websocket._socket.destroy(); 702 | } 703 | 704 | /** 705 | * The listener of the `Receiver` `'finish'` event. 706 | * 707 | * @private 708 | */ 709 | function receiverOnFinish () { 710 | this[kWebSocket].emitClose(); 711 | } 712 | 713 | /** 714 | * The listener of the `Receiver` `'message'` event. 715 | * 716 | * @param {(String|Buffer|ArrayBuffer|Buffer[])} data The message 717 | * @private 718 | */ 719 | function receiverOnMessage (data) { 720 | this[kWebSocket].emit('message', data); 721 | } 722 | 723 | /** 724 | * The listener of the `Receiver` `'ping'` event. 725 | * 726 | * @param {Buffer} data The data included in the ping frame 727 | * @private 728 | */ 729 | function receiverOnPing (data) { 730 | const websocket = this[kWebSocket]; 731 | 732 | websocket.pong(data, !websocket._isServer, constants.NOOP); 733 | websocket.emit('ping', data); 734 | } 735 | 736 | /** 737 | * The listener of the `Receiver` `'pong'` event. 738 | * 739 | * @param {Buffer} data The data included in the pong frame 740 | * @private 741 | */ 742 | function receiverOnPong (data) { 743 | this[kWebSocket].emit('pong', data); 744 | } 745 | 746 | /** 747 | * The listener of the `net.Socket` `'close'` event. 748 | * 749 | * @private 750 | */ 751 | function socketOnClose () { 752 | const websocket = this[kWebSocket]; 753 | 754 | this.removeListener('close', socketOnClose); 755 | this.removeListener('end', socketOnEnd); 756 | 757 | websocket.readyState = WebSocket.CLOSING; 758 | 759 | // 760 | // The close frame might not have been received or the `'end'` event emitted, 761 | // for example, if the socket was destroyed due to an error. Ensure that the 762 | // `receiver` stream is closed after writing any remaining buffered data to 763 | // it. If the readable side of the socket is in flowing mode then there is no 764 | // buffered data as everything has been already written and `readable.read()` 765 | // will return `null`. If instead, the socket is paused, any possible buffered 766 | // data will be read as a single chunk and emitted synchronously in a single 767 | // `'data'` event. 768 | // 769 | websocket._socket.read(); 770 | websocket._receiver.end(); 771 | 772 | this.removeListener('data', socketOnData); 773 | this[kWebSocket] = undefined; 774 | 775 | clearTimeout(websocket._closeTimer); 776 | 777 | if ( 778 | websocket._receiver._writableState.finished || 779 | websocket._receiver._writableState.errorEmitted 780 | ) { 781 | websocket.emitClose(); 782 | } else { 783 | websocket._receiver.on('error', receiverOnFinish); 784 | websocket._receiver.on('finish', receiverOnFinish); 785 | } 786 | } 787 | 788 | /** 789 | * The listener of the `net.Socket` `'data'` event. 790 | * 791 | * @param {Buffer} chunk A chunk of data 792 | * @private 793 | */ 794 | function socketOnData (chunk) { 795 | if (!this[kWebSocket]._receiver.write(chunk)) { 796 | this.pause(); 797 | } 798 | } 799 | 800 | /** 801 | * The listener of the `net.Socket` `'end'` event. 802 | * 803 | * @private 804 | */ 805 | function socketOnEnd () { 806 | const websocket = this[kWebSocket]; 807 | 808 | websocket.readyState = WebSocket.CLOSING; 809 | websocket._receiver.end(); 810 | this.end(); 811 | } 812 | 813 | /** 814 | * The listener of the `net.Socket` `'error'` event. 815 | * 816 | * @private 817 | */ 818 | function socketOnError () { 819 | const websocket = this[kWebSocket]; 820 | 821 | this.removeListener('error', socketOnError); 822 | this.on('error', constants.NOOP); 823 | 824 | if (websocket) { 825 | websocket.readyState = WebSocket.CLOSING; 826 | this.destroy(); 827 | } 828 | } 829 | --------------------------------------------------------------------------------