├── relay.png ├── hyco-websocket ├── lib │ ├── version.js │ ├── hybridconnectionswebsocket.js │ ├── utils.js │ ├── HybridConnectionsWebSocketRequest.js │ └── HybridConnectionsWebSocketServer.js ├── .npmignore ├── .gitignore ├── test │ ├── autobahn │ │ ├── fuzzingserver.json │ │ └── fuzzingclient.json │ ├── unit │ │ ├── regressions.js │ │ ├── websocketFrame.js │ │ ├── dropBeforeAccept.js │ │ ├── w3cwebsocket.js │ │ └── request.js │ ├── scripts │ │ ├── make-relay-uri.js │ │ ├── memoryleak-server.js │ │ ├── memoryleak-client.js │ │ ├── fragmentation-test-page.html │ │ ├── echo-server.js │ │ ├── libwebsockets-test-client.js │ │ ├── autobahn-test-client.js │ │ ├── fragmentation-test-client.js │ │ ├── fragmentation-test-server.js │ │ ├── libwebsockets-test-server.js │ │ └── libwebsockets-test.html │ └── shared │ │ ├── test-server.js │ │ └── start-echo-server.js ├── package.json ├── index.js ├── .jshintrc └── README.md ├── hyco-https ├── .npmignore ├── .gitignore ├── examples │ └── simple │ │ ├── package-lock.json │ │ ├── package.json │ │ ├── listener.js │ │ └── sender.js ├── package.json ├── tests │ ├── get.test.js │ ├── pipe.test.js │ ├── chunkedpost.test.js │ └── post.test.js ├── index.js ├── README.md └── lib │ └── _hyco_incoming.js ├── hyco-ws ├── .npmignore ├── .gitignore ├── examples │ └── simple │ │ ├── package.json │ │ ├── listener.js │ │ └── sender.js ├── package.json ├── index.js ├── README.md └── lib │ └── HybridConnectionWebSocketServer.js ├── .gitignore ├── .github ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md └── CONTRIBUTING.md ├── package.json ├── LICENSE ├── appveyor.yml ├── CONTRIBUTE.md ├── THIRD PARTY NOTICES ├── SECURITY.md ├── jest.config.js └── README.md /relay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure/azure-relay-node/HEAD/relay.png -------------------------------------------------------------------------------- /hyco-websocket/lib/version.js: -------------------------------------------------------------------------------- 1 | module.exports = require('../package.json').version; 2 | 3 | -------------------------------------------------------------------------------- /hyco-https/.npmignore: -------------------------------------------------------------------------------- 1 | .npmignore 2 | .gitignore 3 | 4 | example/ 5 | build/ 6 | test/ 7 | node_modules/ 8 | -------------------------------------------------------------------------------- /hyco-ws/.npmignore: -------------------------------------------------------------------------------- 1 | .npmignore 2 | .gitignore 3 | 4 | example/ 5 | build/ 6 | test/ 7 | node_modules/ 8 | -------------------------------------------------------------------------------- /hyco-websocket/.npmignore: -------------------------------------------------------------------------------- 1 | .npmignore 2 | .gitignore 3 | 4 | example/ 5 | build/ 6 | test/ 7 | node_modules/ 8 | -------------------------------------------------------------------------------- /hyco-ws/.gitignore: -------------------------------------------------------------------------------- 1 | npm-debug.log 2 | node_modules 3 | .*.swp 4 | .lock-* 5 | build 6 | coverage 7 | 8 | builderror.log 9 | -------------------------------------------------------------------------------- /hyco-https/.gitignore: -------------------------------------------------------------------------------- 1 | npm-debug.log 2 | node_modules 3 | .*.swp 4 | .lock-* 5 | build 6 | coverage 7 | 8 | builderror.log 9 | -------------------------------------------------------------------------------- /hyco-websocket/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | .lock-* 4 | build 5 | build/* 6 | builderror.log 7 | npm-debug.log 8 | -------------------------------------------------------------------------------- /hyco-https/examples/simple/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simple", 3 | "version": "0.0.0", 4 | "lockfileVersion": 1 5 | } 6 | -------------------------------------------------------------------------------- /hyco-websocket/test/autobahn/fuzzingserver.json: -------------------------------------------------------------------------------- 1 | { 2 | "url": "ws://127.0.0.1:9001", 3 | "outdir": "./reports/clients", 4 | "cases": ["*"], 5 | "exclude-cases": [], 6 | "exclude-agent-cases": {} 7 | } 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | .vscode/* 3 | 4 | # Build and package folders 5 | ################### 6 | build 7 | dist 8 | packages 9 | typings 10 | .tmp 11 | 12 | # Node 13 | ################### 14 | node_modules 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Actual Behavior 2 | 1. 3 | 2. 4 | 5 | ## Expected Behavior 6 | 1. 7 | 2. 8 | 9 | ## Versions 10 | - OS platform and version: 11 | - Node Version: 12 | - NPM package version or commit ID: -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "workspaces": [ 4 | "hyco-https", 5 | "hyco-websocket", 6 | "hyco-ws" 7 | ], 8 | "devDependencies": { 9 | "jest": "^23.6.0" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /hyco-ws/examples/simple/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "", 3 | "name": "simple", 4 | "version": "0.0.0", 5 | "repository": { 6 | "type": "git", 7 | "url": "git://github.com/Azure/azure-relay-node.git" 8 | }, 9 | "devDependencies": {}, 10 | "optionalDependencies": {} 11 | } 12 | -------------------------------------------------------------------------------- /hyco-https/examples/simple/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "", 3 | "name": "simple", 4 | "version": "0.0.0", 5 | "repository": { 6 | "type": "git", 7 | "url": "git://github.com/Azure/azure-relay-node.git" 8 | }, 9 | "devDependencies": {}, 10 | "optionalDependencies": {} 11 | } 12 | -------------------------------------------------------------------------------- /hyco-websocket/test/autobahn/fuzzingclient.json: -------------------------------------------------------------------------------- 1 | { 2 | "options": {"failByDrop": false}, 3 | "outdir": "./reports/servers", 4 | 5 | "servers": [ 6 | { 7 | "agent": "WebSocket-Node 1.0.23", 8 | "url": "ws://127.0.0.1:8080", 9 | "options": {"version": 18} 10 | } 11 | ], 12 | 13 | "cases": ["*"], 14 | "exclude-cases": [], 15 | "exclude-agent-cases": {} 16 | } 17 | -------------------------------------------------------------------------------- /hyco-websocket/lib/hybridconnectionswebsocket.js: -------------------------------------------------------------------------------- 1 | var websocket = require('websocket'); 2 | 3 | module.exports = { 4 | 'server' : websocket.server, 5 | 'relayedServer' : require('./HybridConnectionsWebSocketServer'), 6 | 'client' : websocket.client, 7 | 'router' : websocket.router, 8 | 'frame' : websocket.frame, 9 | 'request' : websocket.request, 10 | 'relayedRequest' : require('./HybridConnectionsWebSocketRequest'), 11 | 'connection' : websocket.connection, 12 | 'w3cwebsocket' : websocket.w3cwebsocket, 13 | 'deprecation' : websocket.deprecation, 14 | 'version' : require('./version') 15 | }; 16 | -------------------------------------------------------------------------------- /hyco-https/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Microsoft Corporation", 3 | "name": "hyco-https", 4 | "description": "HTTP with Azure Relay Hybrid Connections", 5 | "version": "1.4.5", 6 | "license": "MIT", 7 | "main": "index.js", 8 | "homepage": "https://docs.microsoft.com/en-us/azure/service-bus-relay/", 9 | "keywords": [ 10 | "https" 11 | ], 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/Azure/azure-relay-node.git" 15 | }, 16 | "dependencies": { 17 | "crypto": "latest", 18 | "events": "latest", 19 | "https": "latest", 20 | "moment": "^2.22.2", 21 | "util": "latest", 22 | "ws": "^6.0.0" 23 | }, 24 | "gypfile": true, 25 | "devDependencies": { 26 | "jest": "^23.4.2" 27 | }, 28 | "scripts": { 29 | "test": "jest" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /hyco-ws/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Microsoft Corporation", 3 | "name": "hyco-ws", 4 | "description": "Websockets with Azure Relay Hybrid Connections", 5 | "version": "1.0.7", 6 | "license": "MIT", 7 | "main": "index.js", 8 | "homepage": "https://docs.microsoft.com/en-us/azure/service-bus-relay/", 9 | "keywords": [ 10 | "Hixie", 11 | "HyBi", 12 | "Push", 13 | "RFC-6455", 14 | "WebSocket", 15 | "WebSockets", 16 | "real-time" 17 | ], 18 | "repository" : { 19 | "type" : "git", 20 | "url" : "https://github.com/Azure/azure-relay-node.git" 21 | }, 22 | "dependencies": { 23 | "ws": "7.2.x", 24 | "crypto": "latest", 25 | "moment": "latest", 26 | "util" : "latest", 27 | "http" : "latest", 28 | "events" : "latest" 29 | }, 30 | "gypfile": true 31 | } 32 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 7 | 8 | This checklist is used to make sure that common guidelines for a pull request are followed. 9 | 10 | - [ ] **I have read the [contribution guidelines](./CONTRIBUTING.md).** 11 | - [ ] Title of the pull request is clear and informative. 12 | - [ ] There are a small number of commits, each of which have an informative message. This means that previously merged commits do not appear in the history of the PR. 13 | - [ ] The pull request does not introduce breaking changes (unless a major version change occurs in the assembly and module). 14 | - [ ] If applicable, the public code is properly documented. 15 | - [ ] The code builds without any errors. -------------------------------------------------------------------------------- /hyco-websocket/test/unit/regressions.js: -------------------------------------------------------------------------------- 1 | var test = require('tape'); 2 | 3 | var WebSocketClient = require('../../lib/WebSocketClient'); 4 | var startEchoServer = require('../shared/start-echo-server'); 5 | 6 | test('Issue 195 - passing number to connection.send() shouldn\'t throw', function(t) { 7 | startEchoServer(function(err, echoServer) { 8 | if (err) { 9 | return t.fail('Unable to start echo server: ' + err); 10 | } 11 | 12 | var client = new WebSocketClient(); 13 | client.on('connect', function(connection) { 14 | t.pass('connected'); 15 | 16 | t.doesNotThrow(function() { 17 | connection.send(12345); 18 | }); 19 | 20 | connection.close(); 21 | echoServer.kill(); 22 | t.end(); 23 | }); 24 | 25 | client.on('connectFailed', function(errorDescription) { 26 | echoServer.kill(); 27 | t.fail(errorDescription); 28 | t.end(); 29 | }); 30 | 31 | client.connect('ws://localhost:8080', null); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /hyco-websocket/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Microsoft Corporation", 3 | "name": "hyco-websocket", 4 | "description": "Websockets with Azure Relay Hybrid Connections", 5 | "version": "1.0.7", 6 | "license": "MIT", 7 | "main": "index.js", 8 | "homepage": "https://docs.microsoft.com/en-us/azure/service-bus-relay/", 9 | "keywords": [ 10 | "websocket", 11 | "websockets", 12 | "socket", 13 | "networking", 14 | "comet", 15 | "push", 16 | "RFC-6455", 17 | "realtime", 18 | "server", 19 | "client" 20 | ], 21 | "repository": { 22 | "type": "git", 23 | "url": "https://github.com/Azure/azure-relay-node.git" 24 | }, 25 | "engines": { 26 | "node": ">=0.8.0" 27 | }, 28 | "dependencies": { 29 | "websocket": "latest", 30 | "crypto": "latest", 31 | "moment": "latest", 32 | "url": "latest", 33 | "events": "latest", 34 | "querystring": "latest" 35 | }, 36 | "config": { 37 | "verbose": false 38 | }, 39 | "directories": { 40 | "lib": "./lib" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /hyco-websocket/test/unit/websocketFrame.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var test = require('tape'); 4 | var bufferEqual = require('buffer-equal'); 5 | var WebSocketFrame = require('../../lib/WebSocketFrame'); 6 | 7 | test('Serializing a WebSocket Frame with no data', function(t) { 8 | t.plan(2); 9 | 10 | // WebSocketFrame uses a per-connection buffer for the mask bytes 11 | // and the frame header to avoid allocating tons of small chunks of RAM. 12 | var maskBytesBuffer = new Buffer(4); 13 | var frameHeaderBuffer = new Buffer(10); 14 | 15 | var frameBytes; 16 | var frame = new WebSocketFrame(maskBytesBuffer, frameHeaderBuffer, {}); 17 | frame.fin = true; 18 | frame.mask = true; 19 | frame.opcode = 0x09; // WebSocketFrame.PING 20 | t.doesNotThrow( 21 | function() { frameBytes = frame.toBuffer(true); }, 22 | 'should not throw an error' 23 | ); 24 | 25 | t.assert( 26 | bufferEqual 27 | (frameBytes, new Buffer('898000000000', 'hex')), 28 | 'Generated bytes should be correct' 29 | ); 30 | 31 | t.end(); 32 | }); -------------------------------------------------------------------------------- /hyco-websocket/test/scripts/make-relay-uri.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var WebSocket = require('../..'); 4 | var WebSocketServer = require('../../lib/HybridConnectionsWebSocketServer'); 5 | 6 | var args = { /* defaults */ 7 | debug: false, 8 | ns : process.env.RELAY_NAMESPACE, 9 | path : process.env.RELAY_PATH, 10 | keyrule : process.env.RELAY_KEYRULE, 11 | key : process.env.RELAY_KEY 12 | }; 13 | 14 | /* Parse command line options */ 15 | var pattern = /^--(.*?)(?:=(.*))?$/; 16 | process.argv.forEach(function(value) { 17 | var match = pattern.exec(value); 18 | if (match) { 19 | args[match[1]] = match[2] ? match[2] : true; 20 | } 21 | }); 22 | 23 | var ns = args.ns; 24 | var path = args.path; 25 | var keyrule = args.keyrule; 26 | var key = args.key; 27 | var debug = args.debug; 28 | 29 | if (ns == null || path == null || keyrule == null || key == null) { 30 | return; 31 | } 32 | 33 | var uri = WebSocket.createRelayListenUri(ns, path); 34 | uri = WebSocket.appendRelayToken(uri, keyrule, key); 35 | 36 | console.log(uri); 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Microsoft Azure 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. -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | environment: 2 | SB_HC_NAMESPACE: 3 | secure: kHPebM+qsL3Dop5trud7EaAu2m4apSN1ydhwSZblLBwq5xCKV8fkrkW24qCtMmef 4 | SB_HC_KEYRULE: 5 | secure: /q6x7VmbbG2W9plFBk82xg== 6 | SB_HC_KEY: 7 | secure: /uJo816ZsrBGheZQ7TIzpwoNsNDxpVDxMzz+zY5gBHG/4LUELEZqv0r1I3eB2kvr 8 | SB_HC_PATH: 9 | secure: 6w40hsFa1GyAflELGGJaCg== 10 | matrix: 11 | - nodejs_version: "8" 12 | 13 | init: 14 | # debugging Appveyor build. More info: 15 | # https://www.appveyor.com/docs/how-to/rdp-to-build-worker/ 16 | - ps: iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1')) 17 | - git config --global core.symlinks true 18 | 19 | install: 20 | - ps: Install-Product node $env:nodejs_version x64 21 | - node --version 22 | - yarn 23 | 24 | cache: 25 | - node_modules 26 | - .eslintcache 27 | - "%LOCALAPPDATA%\\Yarn" 28 | 29 | test_script: 30 | - yarn jest --color --runInBand 31 | 32 | # Don't actually build. 33 | build: off 34 | 35 | notifications: 36 | - provider: Email 37 | on_build_success: false 38 | on_build_failure: false 39 | on_build_status_changed: false -------------------------------------------------------------------------------- /hyco-websocket/test/shared/test-server.js: -------------------------------------------------------------------------------- 1 | var WebSocket = require('../../'); 2 | var WebSocketServer = require('../../lib/HybridConnectionsWebSocketServer'); 3 | 4 | var wsServer; 5 | 6 | function prepare(callback) { 7 | if (typeof(callback) !== 'function') { callback = function() {}; } 8 | 9 | var ns = process.env.RELAY_NAMESPACE; 10 | var path = process.env.RELAY_PATH; 11 | var keyrule = process.env.RELAY_KEYRULE; 12 | var key = process.env.RELAY_KEY; 13 | 14 | var uri = WebSocket.createRelayListenUri(ns, path); 15 | 16 | wsServer = new WebSocketServer({ 17 | server : uri, 18 | token: WebSocket.createRelayToken(uri, keyrule, key), 19 | autoAcceptConnections: false, 20 | maxReceivedFrameSize: 64*1024*1024, // 64MiB 21 | maxReceivedMessageSize: 64*1024*1024, // 64MiB 22 | fragmentOutgoingMessages: false, 23 | keepalive: false, 24 | disableNagleAlgorithm: false 25 | }); 26 | } 27 | 28 | function stopServer() { 29 | try { 30 | wsServer.shutDown(); 31 | } 32 | catch (e) { 33 | console.warn('stopServer threw', e); 34 | } 35 | } 36 | 37 | module.exports = { 38 | prepare: prepare, 39 | stopServer: stopServer 40 | }; 41 | -------------------------------------------------------------------------------- /hyco-ws/examples/simple/listener.js: -------------------------------------------------------------------------------- 1 | var args = { /* defaults */ 2 | ns : process.env.SB_HC_NAMESPACE, 3 | path : process.env.SB_HC_PATH, 4 | keyrule : process.env.SB_HC_KEYRULE, 5 | key : process.env.SB_HC_KEY 6 | }; 7 | 8 | /* Parse command line options */ 9 | var pattern = /^--(.*?)(?:=(.*))?$/; 10 | process.argv.forEach(function(value) { 11 | var match = pattern.exec(value); 12 | if (match) { 13 | args[match[1]] = match[2] ? match[2] : true; 14 | } 15 | }); 16 | 17 | if (args.ns == null || args.path == null || args.keyrule == null || args.key == null) { 18 | console.log('listener.js --ns=[namespace] --path=[path] --keyrule=[keyrule] --key=[key]'); 19 | } else { 20 | 21 | var WebSocket = require('../../') 22 | var uri = WebSocket.createRelayListenUri(args.ns, args.path); 23 | var wss = WebSocket.createRelayedServer( 24 | { 25 | server : uri, 26 | keyName: args.keyrule, 27 | key: args.key 28 | }, 29 | function(ws) { 30 | console.log('connection accepted'); 31 | ws.onmessage = function(event) { 32 | console.log(JSON.parse(event.data)); 33 | }; 34 | ws.on('close', function() { 35 | console.log('connection closed'); 36 | }); 37 | }); 38 | 39 | wss.on('error', function(err) { 40 | console.log('error: ' + err); 41 | }); 42 | } -------------------------------------------------------------------------------- /hyco-websocket/test/shared/start-echo-server.js: -------------------------------------------------------------------------------- 1 | module.exports = startEchoServer; 2 | 3 | function startEchoServer(outputStream, callback) { 4 | if ('function' === typeof outputStream) { 5 | callback = outputStream; 6 | outputStream = null; 7 | } 8 | if ('function' !== typeof callback) { 9 | callback = function() {}; 10 | } 11 | 12 | var path = require('path').join(__dirname + '/../scripts/echo-server.js'); 13 | 14 | console.log(path); 15 | 16 | var echoServer = require('child_process').spawn('node', [ path ]); 17 | 18 | var state = 'starting'; 19 | 20 | var processProxy = { 21 | kill: function(signal) { 22 | state = 'exiting'; 23 | echoServer.kill(signal); 24 | } 25 | }; 26 | 27 | if (outputStream) { 28 | echoServer.stdout.pipe(outputStream); 29 | echoServer.stderr.pipe(outputStream); 30 | } 31 | 32 | echoServer.stdout.on('data', function(chunk) { 33 | chunk = chunk.toString(); 34 | if (/Server is listening/.test(chunk)) { 35 | if (state === 'starting') { 36 | state = 'ready'; 37 | callback(null, processProxy); 38 | } 39 | } 40 | }); 41 | 42 | echoServer.on('exit', function(code, signal) { 43 | echoServer = null; 44 | if (state !== 'exiting') { 45 | state = 'exited'; 46 | callback(new Error('Echo Server exited unexpectedly with code ' + code)); 47 | process.exit(1); 48 | } 49 | }); 50 | 51 | process.on('exit', function() { 52 | if (echoServer && state === 'ready') { 53 | echoServer.kill(); 54 | } 55 | }); 56 | } 57 | -------------------------------------------------------------------------------- /hyco-https/examples/simple/listener.js: -------------------------------------------------------------------------------- 1 | const https = require('../../') 2 | 3 | var args = { /* defaults */ 4 | ns : process.env.SB_HC_NAMESPACE, 5 | path : process.env.SB_HC_PATH, 6 | keyrule : process.env.SB_HC_KEYRULE, 7 | key : process.env.SB_HC_KEY 8 | }; 9 | 10 | /* Parse command line options */ 11 | var pattern = /^--(.*?)(?:=(.*))?$/; 12 | process.argv.forEach(function(value) { 13 | var match = pattern.exec(value); 14 | if (match) { 15 | args[match[1]] = match[2] ? match[2] : true; 16 | } 17 | }); 18 | 19 | if (args.ns == null || args.path == null || args.keyrule == null || args.key == null) { 20 | console.log('listener.js --ns=[namespace] --path=[path] --keyrule=[keyrule] --key=[key]'); 21 | } else { 22 | var uri = https.createRelayListenUri(args.ns, args.path); 23 | var server = https.createRelayedServer( 24 | { 25 | server : uri, 26 | token : () => https.createRelayToken(uri, args.keyrule, args.key) 27 | }, 28 | (req, res) => { 29 | console.log('request accepted: ' + req.method + ' on ' + req.url); 30 | res.setHeader('Content-Type', 'text/html'); 31 | res.end('Hey!Relayed Node.js Server!'); 32 | }); 33 | 34 | server.listen( (err) => { 35 | if (err) { 36 | return console.log('something bad happened', err) 37 | } 38 | console.log(`server is listening on ${port}`) 39 | }); 40 | 41 | server.on('error', (err) => { 42 | console.log('error: ' + err); 43 | }); 44 | } -------------------------------------------------------------------------------- /hyco-ws/examples/simple/sender.js: -------------------------------------------------------------------------------- 1 | var args = { /* defaults */ 2 | ns : process.env.SB_HC_NAMESPACE, 3 | path : process.env.SB_HC_PATH, 4 | keyrule : process.env.SB_HC_KEYRULE, 5 | key : process.env.SB_HC_KEY 6 | }; 7 | 8 | /* Parse command line options */ 9 | var pattern = /^--(.*?)(?:=(.*))?$/; 10 | process.argv.forEach(function(value) { 11 | var match = pattern.exec(value); 12 | if (match) { 13 | args[match[1]] = match[2] ? match[2] : true; 14 | } 15 | }); 16 | 17 | if (args.ns == null || args.path == null || args.keyrule == null || args.key == null) { 18 | console.log('sender.js --ns=[namespace] --path=[path] --keyrule=[keyrule] --key=[key]'); 19 | } else { 20 | 21 | var WebSocket = require('../..') 22 | var uri = WebSocket.createRelaySendUri(args.ns, args.path); 23 | WebSocket.relayedConnect( 24 | uri, 25 | WebSocket.createRelayToken(uri, args.keyrule, args.key), 26 | function(wss) { 27 | var id = setInterval(function() { 28 | wss.send(JSON.stringify(process.memoryUsage()), function() { /* ignore errors */ }); 29 | }, 100); 30 | 31 | console.log('Started client interval. Press any key to stop.'); 32 | wss.on('close', function() { 33 | console.log('stopping client interval'); 34 | clearInterval(id); 35 | process.exit(); 36 | }); 37 | 38 | process.stdin.setRawMode(true); 39 | process.stdin.resume(); 40 | process.stdin.on('data', function() { 41 | wss.close(); 42 | }); 43 | } 44 | ); 45 | } -------------------------------------------------------------------------------- /CONTRIBUTE.md: -------------------------------------------------------------------------------- 1 | ## What to contribute 2 | There are many ways that you can contribute to the Azure Relay client project: 3 | 4 | * Submit a bug 5 | * Submit a code fix for a bug 6 | * Submit code to add a new platform/language support to the project, or modify existing code 7 | * Submit additions or modifications to the documentation 8 | * Submit a feature request 9 | 10 | ## Contributing Code 11 | To contribute code you need to issue a Pull Request against the develop branch. All code submissions will be reviewed and tested by the team, and those that meet a high bar for both quality and design/roadmap appropriateness will be merged into the source. Be sure to follow the existing file/folder structure when adding new boards or sensors. 12 | 13 | You must sign a [Contribution License Agreement](https://cla.microsoft.com/) ([CLA](https://cla.microsoft.com/)) before submitting a Pull Request. To complete the CLA, you will need to submit the request via the form and then electronically sign the CLA when you receive the email containing the link to the document. 14 | 15 | ## Big contributions 16 | If your contribution is significantly big it is better to first check with the project developers in order to make sure the change aligns with the long term plans. This can be done simply by submitting a question via the GitHub Issues section. 17 | 18 | ## Things to keep in mind when contributing 19 | Some guidance for when you make a contribution: 20 | 21 | * Add/update unit tests and code as required by your change 22 | * Make sure you run all the unit tests on the affected platform(s)/languages. If the change is in common code, generally running on one platform would be acceptable. 23 | * Run end-to-end tests or simple sample code to make sure the lib works in an end-to-end scenario. 24 | -------------------------------------------------------------------------------- /hyco-https/examples/simple/sender.js: -------------------------------------------------------------------------------- 1 | var https = require('../..') 2 | 3 | var args = { /* defaults */ 4 | ns: process.env.SB_HC_NAMESPACE, 5 | path: process.env.SB_HC_PATH, 6 | keyrule: process.env.SB_HC_KEYRULE, 7 | key: process.env.SB_HC_KEY 8 | }; 9 | 10 | /* Parse command line options */ 11 | var pattern = /^--(.*?)(?:=(.*))?$/; 12 | process.argv.forEach(function (value) { 13 | var match = pattern.exec(value); 14 | if (match) { 15 | args[match[1]] = match[2] ? match[2] : true; 16 | } 17 | }); 18 | 19 | if (args.ns == null || args.path == null || args.keyrule == null || args.key == null) { 20 | console.log('sender.js --ns=[namespace] --path=[path] --keyrule=[keyrule] --key=[key]'); 21 | } else { 22 | 23 | var ns = args.ns; 24 | var path = args.path; 25 | var keyrule = args.keyrule; 26 | var key = args.key; 27 | 28 | https.get({ 29 | hostname : ns, 30 | path : ((!path || path.length == 0 || path[0] !== '/')?'/':'') + path, 31 | port : 443, 32 | headers : { 33 | 'ServiceBusAuthorization' : 34 | https.createRelayToken(https.createRelayHttpsUri(ns, path), keyrule, key) 35 | } 36 | }, (res) => { 37 | let error; 38 | if (res.statusCode !== 200) { 39 | console.error('Request Failed.\n Status Code:' + res.statusCode); 40 | res.resume(); 41 | } 42 | else { 43 | res.setEncoding('utf8'); 44 | res.on('data', (chunk) => { 45 | console.log(`BODY: ${chunk}`); 46 | }); 47 | res.on('end', () => { 48 | console.log('No more data in response.'); 49 | }); 50 | }; 51 | }).on('error', (e) => { 52 | console.error(`Got error: ${e.message}`); 53 | }); 54 | } -------------------------------------------------------------------------------- /hyco-websocket/lib/utils.js: -------------------------------------------------------------------------------- 1 | var noop = exports.noop = function() {}; 2 | 3 | exports.extend = function extend(dest, source) { 4 | for (var prop in source) { 5 | dest[prop] = source[prop]; 6 | } 7 | }; 8 | 9 | exports.eventEmitterListenerCount = 10 | require('events').EventEmitter.listenerCount || 11 | function(emitter, type) { return emitter.listeners(type).length; }; 12 | 13 | exports.BufferingLogger = function createBufferingLogger(identifier, uniqueID) { 14 | var logFunction = require('debug')(identifier); 15 | if (logFunction.enabled) { 16 | var logger = new BufferingLogger(identifier, uniqueID, logFunction); 17 | var debug = logger.log.bind(logger); 18 | debug.printOutput = logger.printOutput.bind(logger); 19 | debug.enabled = logFunction.enabled; 20 | return debug; 21 | } 22 | logFunction.printOutput = noop; 23 | return logFunction; 24 | }; 25 | 26 | function BufferingLogger(identifier, uniqueID, logFunction) { 27 | this.logFunction = logFunction; 28 | this.identifier = identifier; 29 | this.uniqueID = uniqueID; 30 | this.buffer = []; 31 | } 32 | 33 | BufferingLogger.prototype.log = function() { 34 | this.buffer.push([ new Date(), Array.prototype.slice.call(arguments) ]); 35 | return this; 36 | }; 37 | 38 | BufferingLogger.prototype.clear = function() { 39 | this.buffer = []; 40 | return this; 41 | }; 42 | 43 | BufferingLogger.prototype.printOutput = function(logFunction) { 44 | if (!logFunction) { logFunction = this.logFunction; } 45 | var uniqueID = this.uniqueID; 46 | this.buffer.forEach(function(entry) { 47 | var date = entry[0].toLocaleString(); 48 | var args = entry[1].slice(); 49 | var formatString = args[0]; 50 | if (formatString !== (void 0) && formatString !== null) { 51 | formatString = '%s - %s - ' + formatString.toString(); 52 | args.splice(0, 1, formatString, date, uniqueID); 53 | logFunction.apply(global, args); 54 | } 55 | }); 56 | }; 57 | -------------------------------------------------------------------------------- /hyco-websocket/test/unit/dropBeforeAccept.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var test = require('tape'); 4 | 5 | var WebSocketClient = require('../../lib/WebSocketClient'); 6 | var server = require('../shared/test-server'); 7 | var stopServer = server.stopServer; 8 | 9 | test('Drop TCP Connection Before server accepts the request', function(t) { 10 | t.plan(5); 11 | 12 | server.prepare(function(err, wsServer) { 13 | if (err) { 14 | t.fail('Unable to start test server'); 15 | return t.end(); 16 | } 17 | 18 | wsServer.on('connect', function(connection) { 19 | t.pass('Server should emit connect event'); 20 | }); 21 | 22 | wsServer.on('request', function(request) { 23 | t.pass('Request received'); 24 | 25 | // Wait 500 ms before accepting connection 26 | setTimeout(function() { 27 | var connection = request.accept(request.requestedProtocols[0], request.origin); 28 | 29 | connection.on('close', function(reasonCode, description) { 30 | t.pass('Connection should emit close event'); 31 | t.equal(reasonCode, 1006, 'Close reason code should be 1006'); 32 | t.equal(description, 33 | 'TCP connection lost before handshake completed.', 34 | 'Description should be correct'); 35 | t.end(); 36 | stopServer(); 37 | }); 38 | 39 | connection.on('error', function(error) { 40 | t.fail('No error events should be received on the connection'); 41 | stopServer(); 42 | }); 43 | }, 500); 44 | }); 45 | 46 | var client = new WebSocketClient(); 47 | client.on('connect', function(connection) { 48 | t.fail('Client should never connect.'); 49 | connection.drop(); 50 | stopServer(); 51 | t.end(); 52 | }); 53 | 54 | client.connect('ws://localhost:64321/', ['test']); 55 | 56 | setTimeout(function() { 57 | // Bail on the connection before we hear back from the server. 58 | client.abort(); 59 | }, 250); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /hyco-websocket/test/scripts/memoryleak-server.js: -------------------------------------------------------------------------------- 1 | process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; 2 | 3 | // var heapdump = require('heapdump'); 4 | // var memwatch = require('memwatch'); 5 | var fs = require('fs'); 6 | var WebSocketServer = require('../../lib/websocket').server; // Is this module even present? 7 | var https = require('https'); 8 | 9 | var activeCount = 0; 10 | 11 | var config = { 12 | key: fs.readFileSync('privatekey.pem'), 13 | cert: fs.readFileSync('certificate.pem') 14 | }; 15 | 16 | var server = https.createServer(config); 17 | 18 | server.listen(8080, function() { 19 | console.log((new Date()) + ' Server is listening on port 8080 (wss)'); 20 | }); 21 | 22 | var wsServer = new WebSocketServer({ 23 | httpServer: server, 24 | autoAcceptConnections: false 25 | }); 26 | 27 | wsServer.on('request', function(request) { 28 | activeCount++; 29 | console.log('Opened from: %j\n---activeCount---: %d', request.remoteAddresses, activeCount); 30 | var connection = request.accept(null, request.origin); 31 | console.log((new Date()) + ' Connection accepted.'); 32 | connection.on('message', function(message) { 33 | if (message.type === 'utf8') { 34 | console.log('Received Message: ' + message.utf8Data); 35 | setTimeout(function() { 36 | if (connection.connected) { 37 | connection.sendUTF(message.utf8Data); 38 | } 39 | }, 1000); 40 | } 41 | }); 42 | connection.on('close', function(reasonCode, description) { 43 | activeCount--; 44 | console.log('Closed. (' + reasonCode + ') ' + description + 45 | '\n---activeCount---: ' + activeCount); 46 | // connection._debug.printOutput(); 47 | }); 48 | connection.on('error', function(error) { 49 | console.log('Connection error: ' + error); 50 | }); 51 | }); 52 | 53 | // setInterval( function() { 54 | // // global.gc(); 55 | // var filename = './heapdump/' + new Date().getTime() + '_' + activeCount + '.heapsnapshot'; 56 | // console.log('Triggering heapdump to write to %s', filename); 57 | // heapdump.writeSnapshot(filename); 58 | // }, 10000 ); 59 | // memwatch.on('leak', function(info) { console.log(info); }); 60 | -------------------------------------------------------------------------------- /THIRD PARTY NOTICES: -------------------------------------------------------------------------------- 1 | Third Party Notices for Azure Relay Node Client Libraries 2 | 3 | This project incorporates components from the projects listed below. The original 4 | copyright notices and the licenses under which Microsoft received such components 5 | are set forth below. Microsoft reserves all rights not expressly granted herein, 6 | whether by implication, estoppel or otherwise. 7 | 8 | theturtle32/WebSocket-Node - https://github.com/theturtle32/WebSocket-Node 9 | 10 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use 11 | these files except in compliance with the License. You may obtain a copy of the 12 | License at 13 | 14 | http://www.apache.org/licenses/LICENSE-2.0 15 | 16 | Unless required by applicable law or agreed to in writing, software distributed 17 | under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 18 | CONDITIONS OF ANY KIND, either express or implied. See the License for the 19 | specific language governing permissions and limitations under the License. 20 | 21 | websockets/ws - https://github.com/websockets/ws 22 | 23 | Copyright (c) 2011 Einar Otto Stangvik 24 | 25 | Permission is hereby granted, free of charge, to any person obtaining a copy 26 | of this software and associated documentation files (the "Software"), to deal 27 | in the Software without restriction, including without limitation the rights 28 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 29 | copies of the Software, and to permit persons to whom the Software is 30 | furnished to do so, subject to the following conditions: 31 | 32 | The above copyright notice and this permission notice shall be included in all 33 | copies or substantial portions of the Software. 34 | 35 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 36 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 37 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 38 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 39 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 40 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 41 | SOFTWARE. 42 | 43 | -------------------------------------------------------------------------------- /hyco-websocket/test/unit/w3cwebsocket.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var test = require('tape'); 4 | var WebSocket = require('../../lib/W3CWebSocket'); 5 | var startEchoServer = require('../shared/start-echo-server'); 6 | 7 | test('W3CWebSockets adding event listeners with ws.onxxxxx', function(t) { 8 | var counter = 0; 9 | var message = 'This is a test message.'; 10 | 11 | startEchoServer(function(err, echoServer) { 12 | if (err) { 13 | return t.fail('Unable to start echo server: ' + err); 14 | } 15 | 16 | var ws = new WebSocket('ws://localhost:8080/'); 17 | 18 | ws.onopen = function() { 19 | t.equal(++counter, 1, 'onopen should be called first'); 20 | 21 | ws.send(message); 22 | }; 23 | ws.onerror = function(event) { 24 | t.fail('No errors are expected: ' + event); 25 | }; 26 | ws.onmessage = function(event) { 27 | t.equal(++counter, 2, 'onmessage should be called second'); 28 | 29 | t.equal(event.data, message, 'Received message data should match sent message data.'); 30 | 31 | ws.close(); 32 | }; 33 | ws.onclose = function(event) { 34 | t.equal(++counter, 3, 'onclose should be called last'); 35 | 36 | echoServer.kill(); 37 | 38 | t.end(); 39 | }; 40 | }); 41 | }); 42 | 43 | test('W3CWebSockets adding event listeners with ws.addEventListener', function(t) { 44 | var counter = 0; 45 | var message = 'This is a test message.'; 46 | 47 | startEchoServer(function(err, echoServer) { 48 | if (err) { return t.fail('Unable to start echo server: ' + err); } 49 | 50 | var ws = new WebSocket('ws://localhost:8080/'); 51 | 52 | ws.addEventListener('open', function() { 53 | t.equal(++counter, 1, '"open" should be fired first'); 54 | 55 | ws.send(message); 56 | }); 57 | ws.addEventListener('error', function(event) { 58 | t.fail('No errors are expected: ' + event); 59 | }); 60 | ws.addEventListener('message', function(event) { 61 | t.equal(++counter, 2, '"message" should be fired second'); 62 | 63 | t.equal(event.data, message, 'Received message data should match sent message data.'); 64 | 65 | ws.close(); 66 | }); 67 | ws.addEventListener('close', function(event) { 68 | t.equal(++counter, 3, '"close" should be fired'); 69 | }); 70 | ws.addEventListener('close', function(event) { 71 | t.equal(++counter, 4, '"close" should be fired one more time'); 72 | 73 | echoServer.kill(); 74 | 75 | t.end(); 76 | }); 77 | }); 78 | }); 79 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Security 4 | 5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). 6 | 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/opensource/security/definition), please report it to us as described below. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/opensource/security/create-report). 14 | 15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/opensource/security/pgpkey). 16 | 17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://aka.ms/opensource/security/msrc). 18 | 19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 20 | 21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | * Full paths of source file(s) related to the manifestation of the issue 23 | * The location of the affected source code (tag/branch/commit or direct URL) 24 | * Any special configuration required to reproduce the issue 25 | * Step-by-step instructions to reproduce the issue 26 | * Proof-of-concept or exploit code (if possible) 27 | * Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/opensource/security/bounty) page for more details about our active programs. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/opensource/security/cvd). 40 | 41 | 42 | -------------------------------------------------------------------------------- /hyco-https/tests/get.test.js: -------------------------------------------------------------------------------- 1 | var https = require('..') 2 | 3 | var totalRequests = 10; // Total requests to send over the test 4 | jest.setTimeout(5000 + (totalRequests * 200)); // Expect 5 seconds + 5 requests per second 5 | 6 | test('HTTPS GET', (done) => { 7 | var ns = process.env.SB_HC_NAMESPACE ? process.env.SB_HC_NAMESPACE.replace(/^"(.*)"$/, '$1') : null; 8 | var path = process.env.SB_HC_PATH ? process.env.SB_HC_PATH : "a2"; 9 | var keyrule = process.env.SB_HC_KEYRULE ? process.env.SB_HC_KEYRULE.replace(/^"(.*)"$/, '$1') : null; 10 | var key = process.env.SB_HC_KEY ? process.env.SB_HC_KEY.replace(/^"(.*)"$/, '$1') : null; 11 | 12 | expect(ns).toBeDefined(); 13 | expect(path).toBeDefined(); 14 | expect(keyrule).toBeDefined(); 15 | expect(key).toBeDefined(); 16 | 17 | var listenerCount = 0; 18 | var senderCount = 0; 19 | 20 | /* set up the listener */ 21 | var uri = https.createRelayListenUri(ns, path); 22 | var server = https.createRelayedServer({ 23 | server: uri, 24 | token: () => https.createRelayToken(uri, keyrule, key) 25 | }, 26 | (req, res) => { 27 | expect(req.method).toBe('GET'); 28 | expect(req.headers.custom).toBe('Hello'); 29 | res.end('Hello'); 30 | listenerCount++; 31 | }); 32 | 33 | // fail we get an error 34 | server.listen((err) => { 35 | expect(err).toBeUndefined(); 36 | }); 37 | // fail if we get an error (we'll always get one if this triggers) 38 | server.on('error', (err) => { 39 | expect(err).toBeUndefined(); 40 | }); 41 | 42 | /* set up the client */ 43 | var clientUri = https.createRelayHttpsUri(ns, path); 44 | var token = https.createRelayToken(clientUri, keyrule, key); 45 | 46 | server.on('listening', () => { 47 | for (var i = 0; i < totalRequests; i++) { 48 | https.get({ 49 | hostname: ns, 50 | path: ((!path || path.length == 0 || path[0] !== '/') ? '/' : '') + path, 51 | port: 443, 52 | headers: { 53 | 'ServiceBusAuthorization': token, 54 | 'Custom': 'Hello' 55 | } 56 | }, (res) => { 57 | expect(res.statusCode).toBe(200); 58 | res.setEncoding('utf8'); 59 | res.on('data', (chunk) => { 60 | expect(chunk).toBe('Hello'); 61 | }); 62 | res.on('end', () => { 63 | senderCount++; 64 | if (listenerCount == totalRequests && senderCount == totalRequests) { 65 | server.close(); 66 | done(); 67 | } 68 | }); 69 | }).on('error', (e) => { 70 | expect(e).toBeUndefined(); 71 | }); 72 | } 73 | }); 74 | }); -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribute or Provide Feedback for Azure Relay 2 | 3 | ## Table of Contents 4 | 5 | - [Code of Conduct](#code-of-conduct) 6 | - [Filing Issues](#filing-issues) 7 | - [Pull Requests](#pull-requests) 8 | - [General guidelines](#general-guidelines) 9 | - [Testing guidelines](#testing-guidelines) 10 | 11 | ## Code of Conduct 12 | 13 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 14 | 15 | ## Filing Issues 16 | 17 | You can find all of the issues that have been filed in the [Issues](https://github.com/Azure/azure-relay-node/issues) section of the repository. 18 | 19 | If you encounter any bugs, please file an issue [here](https://github.com/Azure/azure-relay-node/issues/new) and make sure to fill out the provided template with the requested information. 20 | 21 | To suggest a new feature or changes that could be made, file an issue the same way you would for a bug, but remove the provided template and replace it with information about your suggestion. 22 | 23 | ### Pull Requests 24 | 25 | If you are thinking about making a large change to this library, **break up the change into small, logical, testable chunks, and organize your pull requests accordingly**. 26 | 27 | You can find all of the pull requests that have been opened in the [Pull Request](https://github.com/Azure/azure-relay-node/pulls) section of the repository. 28 | 29 | To open your own pull request, click [here](https://github.com/Azure/azure-relay-node/compare). When creating a pull request, keep the following in mind: 30 | - Make sure you are pointing to the fork and branch that your changes were made in 31 | - The pull request template that is provided **should be filled out**; this is not something that should just be deleted or ignored when the pull request is created 32 | - Deleting or ignoring this template will elongate the time it takes for your pull request to be reviewed 33 | 34 | #### General guidelines 35 | 36 | The following guidelines must be followed in **EVERY** pull request that is opened. 37 | 38 | - Title of the pull request is clear and informative 39 | - There are a small number of commits that each have an informative message 40 | - A description of the changes the pull request makes is included, and a reference to the bug/issue the pull request fixes is included, if applicable 41 | - All files have the Microsoft copyright header 42 | 43 | #### Testing guidelines 44 | 45 | The following guidelines must be followed in **EVERY** pull request that is opened. 46 | 47 | - Pull request includes test coverage for the included changes 48 | - Tests must use xunit 49 | - Test code should not contain hard coded values for resource names or similar values 50 | - Test should not use App.config files for settings 51 | -------------------------------------------------------------------------------- /hyco-websocket/test/scripts/memoryleak-client.js: -------------------------------------------------------------------------------- 1 | var WebSocketClient = require('../../lib/websocket').client; 2 | 3 | var connectionAmount = process.argv[2]; 4 | var activeCount = 0; 5 | var deviceList = []; 6 | 7 | connectDevices(); 8 | 9 | function logActiveCount() { 10 | console.log('---activecount---: ' + activeCount); 11 | } 12 | 13 | setInterval(logActiveCount, 500); 14 | 15 | function connectDevices() { 16 | for (var i = 0; i < connectionAmount; i++) { 17 | connect(i); 18 | } 19 | } 20 | 21 | function connect(i) { 22 | // console.log('--- Connecting: ' + i); 23 | var client = new WebSocketClient(); 24 | client._clientID = i; 25 | deviceList[i] = client; 26 | 27 | client.on('connectFailed', function(error) { 28 | console.log(i + ' - connect Error: ' + error.toString()); 29 | }); 30 | 31 | client.on('connect', function(connection) { 32 | console.log(i + ' - connect'); 33 | activeCount++; 34 | client.connection = connection; 35 | flake(i); 36 | 37 | maybeScheduleSend(i); 38 | 39 | connection.on('error', function(error) { 40 | console.log(i + ' - ' + error.toString()); 41 | }); 42 | 43 | connection.on('close', function(reasonCode, closeDescription) { 44 | console.log(i + ' - close (%d) %s', reasonCode, closeDescription); 45 | activeCount --; 46 | if (client._flakeTimeout) { 47 | clearTimeout(client._flakeTimeout); 48 | client._flakeTimeout = null; 49 | } 50 | connect(i); 51 | }); 52 | 53 | connection.on('message', function(message) { 54 | if (message.type === 'utf8') { 55 | console.log(i + ' received: \'' + message.utf8Data + '\''); 56 | } 57 | }); 58 | }); 59 | client.connect('wss://localhost:8080'); 60 | } 61 | 62 | function disconnect(i) { 63 | var client = deviceList[i]; 64 | if (client._flakeTimeout) { 65 | client._flakeTimeout = null; 66 | } 67 | client.connection.close(); 68 | } 69 | 70 | function maybeScheduleSend(i) { 71 | var client = deviceList[i]; 72 | var random = Math.round(Math.random() * 100); 73 | console.log(i + ' - scheduling send. Random: ' + random); 74 | if (random < 50) { 75 | setTimeout(function() { 76 | console.log(i + ' - send timeout. Connected? ' + client.connection.connected); 77 | if (client && client.connection.connected) { 78 | console.log(i + ' - Sending test data! random: ' + random); 79 | client.connection.send((new Array(random)).join('TestData')); 80 | } 81 | }, random); 82 | } 83 | } 84 | 85 | function flake(i) { 86 | var client = deviceList[i]; 87 | var timeBeforeDisconnect = Math.round(Math.random() * 2000); 88 | client._flakeTimeout = setTimeout( function() { 89 | disconnect(i); 90 | }, timeBeforeDisconnect); 91 | } 92 | -------------------------------------------------------------------------------- /hyco-websocket/test/scripts/fragmentation-test-page.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 18 | 19 | 20 | Firefox Bug 21 | 107 | 108 | 109 |
110 |

Controls

111 | 112 | 113 |

114 |
115 |
116 |

Output

117 |
Ready.
118 |
119 | 120 | 121 | -------------------------------------------------------------------------------- /hyco-websocket/test/unit/request.js: -------------------------------------------------------------------------------- 1 | var test = require('tape'); 2 | 3 | var WebSocketClient = require('../../lib/WebSocketClient'); 4 | var server = require('../shared/test-server'); 5 | var stopServer = server.stopServer; 6 | 7 | test('Request can only be rejected or accepted once.', function(t) { 8 | t.plan(6); 9 | 10 | t.on('end', function() { 11 | stopServer(); 12 | }); 13 | 14 | server.prepare(function(err, wsServer) { 15 | if (err) { 16 | t.fail('Unable to start test server'); 17 | return t.end(); 18 | } 19 | 20 | wsServer.once('request', firstReq); 21 | connect(2); 22 | 23 | function firstReq(request) { 24 | var accept = request.accept.bind(request, request.requestedProtocols[0], request.origin); 25 | var reject = request.reject.bind(request); 26 | 27 | t.doesNotThrow(accept, 'First call to accept() should succeed.'); 28 | t.throws(accept, 'Second call to accept() should throw.'); 29 | t.throws(reject, 'Call to reject() after accept() should throw.'); 30 | 31 | wsServer.once('request', secondReq); 32 | } 33 | 34 | function secondReq(request) { 35 | var accept = request.accept.bind(request, request.requestedProtocols[0], request.origin); 36 | var reject = request.reject.bind(request); 37 | 38 | t.doesNotThrow(reject, 'First call to reject() should succeed.'); 39 | t.throws(reject, 'Second call to reject() should throw.'); 40 | t.throws(accept, 'Call to accept() after reject() should throw.'); 41 | 42 | t.end(); 43 | } 44 | 45 | function connect(numTimes) { 46 | var client; 47 | for (var i = 0; i < numTimes; i++) { 48 | client = new WebSocketClient(); 49 | client.connect('ws://localhost:64321/', 'foo'); 50 | client.on('connect', function(connection) { connection.close(); }); 51 | } 52 | } 53 | }); 54 | }); 55 | 56 | test('Protocol mismatch should be handled gracefully', function(t) { 57 | var wsServer; 58 | 59 | t.test('setup', function(t) { 60 | server.prepare(function(err, result) { 61 | if (err) { 62 | t.fail('Unable to start test server'); 63 | return t.end(); 64 | } 65 | 66 | wsServer = result; 67 | t.end(); 68 | }); 69 | }); 70 | 71 | t.test('mismatched protocol connection', function(t) { 72 | t.plan(2); 73 | wsServer.on('request', handleRequest); 74 | 75 | var client = new WebSocketClient(); 76 | 77 | var timer = setTimeout(function() { 78 | t.fail('Timeout waiting for client event'); 79 | }, 2000); 80 | 81 | client.connect('ws://localhost:64321/', 'some_protocol_here'); 82 | client.on('connect', function(connection) { 83 | clearTimeout(timer); 84 | connection.close(); 85 | t.fail('connect event should not be emitted on client'); 86 | }); 87 | client.on('connectFailed', function() { 88 | clearTimeout(timer); 89 | t.pass('connectFailed event should be emitted on client'); 90 | }); 91 | 92 | 93 | 94 | function handleRequest(request) { 95 | var accept = request.accept.bind(request, 'this_is_the_wrong_protocol', request.origin); 96 | t.throws(accept, 'request.accept() should throw'); 97 | } 98 | }); 99 | 100 | t.test('teardown', function(t) { 101 | stopServer(); 102 | t.end(); 103 | }); 104 | }); 105 | -------------------------------------------------------------------------------- /hyco-websocket/test/scripts/echo-server.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /************************************************************************ 3 | * Copyright 2010-2015 Brian McKelvey. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the 'License'); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an 'AS IS' BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | ***********************************************************************/ 17 | 18 | var WebSocket = require('../..'); 19 | var WebSocketServer = require('../../lib/HybridConnectionsWebSocketServer'); 20 | 21 | var args = { /* defaults */ 22 | debug: false, 23 | ns : process.env.RELAY_NAMESPACE, 24 | path : process.env.RELAY_PATH, 25 | keyrule : process.env.RELAY_KEYRULE, 26 | key : process.env.RELAY_KEY 27 | }; 28 | 29 | /* Parse command line options */ 30 | var pattern = /^--(.*?)(?:=(.*))?$/; 31 | process.argv.forEach(function(value) { 32 | var match = pattern.exec(value); 33 | if (match) { 34 | args[match[1]] = match[2] ? match[2] : true; 35 | } 36 | }); 37 | 38 | var ns = args.ns; 39 | var path = args.path; 40 | var keyrule = args.keyrule; 41 | var key = args.key; 42 | var debug = args.debug; 43 | 44 | console.log('WebSocket-Node: echo-server'); 45 | 46 | if (ns == null || path == null || keyrule == null || key == null) { 47 | console.log('Usage: ./echo-server.js [--ns=ns.servicebus.windows.net] [--path=path] [--keyrule=keyrule] [--key=key] [--debug]'); 48 | return; 49 | } 50 | 51 | var uri = WebSocket.createRelayListenUri(ns, path); 52 | 53 | var wsServer = new WebSocketServer({ 54 | server : uri, 55 | token: WebSocket.createRelayToken(uri, keyrule, key), 56 | autoAcceptConnections: true, 57 | maxReceivedFrameSize: 64*1024*1024, // 64MiB 58 | maxReceivedMessageSize: 64*1024*1024, // 64MiB 59 | fragmentOutgoingMessages: false, 60 | keepalive: false, 61 | disableNagleAlgorithm: false 62 | }); 63 | 64 | wsServer.on('connect', function(connection) { 65 | if (debug) { console.log((new Date()) + ' Connection accepted' + 66 | ' - Protocol Version ' + connection.webSocketVersion); } 67 | function sendCallback(err) { 68 | if (err) { 69 | console.error('send() error: ' + err); 70 | connection.drop(); 71 | setTimeout(function() { 72 | process.exit(100); 73 | }, 100); 74 | } 75 | } 76 | connection.on('message', function(message) { 77 | if (message.type === 'utf8') { 78 | if (debug) { console.log('Received utf-8 message of ' + message.utf8Data.length + ' characters.'); } 79 | connection.sendUTF(message.utf8Data, sendCallback); 80 | } 81 | else if (message.type === 'binary') { 82 | if (debug) { console.log('Received Binary Message of ' + message.binaryData.length + ' bytes'); } 83 | connection.sendBytes(message.binaryData, sendCallback); 84 | } 85 | }); 86 | connection.on('close', function(reasonCode, description) { 87 | if (debug) { console.log((new Date()) + ' Peer ' + connection.remoteAddress + ' disconnected.'); } 88 | connection._debug.printOutput(); 89 | }); 90 | }); 91 | -------------------------------------------------------------------------------- /hyco-websocket/test/scripts/libwebsockets-test-client.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /************************************************************************ 3 | * Copyright 2010-2015 Brian McKelvey. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the 'License'); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an 'AS IS' BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | ***********************************************************************/ 17 | 18 | var WebSocketClient = require('../../lib/WebSocketClient'); 19 | 20 | var args = { /* defaults */ 21 | secure: false, 22 | version: 13 23 | }; 24 | 25 | /* Parse command line options */ 26 | var pattern = /^--(.*?)(?:=(.*))?$/; 27 | process.argv.forEach(function(value) { 28 | var match = pattern.exec(value); 29 | if (match) { 30 | args[match[1]] = match[2] ? match[2] : true; 31 | } 32 | }); 33 | 34 | args.protocol = args.secure ? 'wss:' : 'ws:'; 35 | args.version = parseInt(args.version, 10); 36 | 37 | if (!args.host || !args.port) { 38 | console.log('WebSocket-Node: Test client for Andy Green\'s libwebsockets-test-server'); 39 | console.log('Usage: ./libwebsockets-test-client.js --host=127.0.0.1 --port=8080 [--version=8|13] [--secure]'); 40 | console.log(''); 41 | return; 42 | } 43 | 44 | var mirrorClient = new WebSocketClient({ 45 | webSocketVersion: args.version 46 | }); 47 | 48 | mirrorClient.on('connectFailed', function(error) { 49 | console.log('Connect Error: ' + error.toString()); 50 | }); 51 | 52 | mirrorClient.on('connect', function(connection) { 53 | console.log('lws-mirror-protocol connected'); 54 | connection.on('error', function(error) { 55 | console.log('Connection Error: ' + error.toString()); 56 | }); 57 | connection.on('close', function() { 58 | console.log('lws-mirror-protocol Connection Closed'); 59 | }); 60 | function sendCallback(err) { 61 | if (err) { console.error('send() error: ' + err); } 62 | } 63 | function spamCircles() { 64 | if (connection.connected) { 65 | // c #7A9237 487 181 14; 66 | var color = 0x800000 + Math.round(Math.random() * 0x7FFFFF); 67 | var x = Math.round(Math.random() * 502); 68 | var y = Math.round(Math.random() * 306); 69 | var radius = Math.round(Math.random() * 30); 70 | connection.send('c #' + color.toString(16) + ' ' + x + ' ' + y + ' ' + radius + ';', sendCallback); 71 | setTimeout(spamCircles, 10); 72 | } 73 | } 74 | spamCircles(); 75 | }); 76 | 77 | mirrorClient.connect(args.protocol + '//' + args.host + ':' + args.port + '/', 'lws-mirror-protocol'); 78 | 79 | var incrementClient = new WebSocketClient({ 80 | webSocketVersion: args.version 81 | }); 82 | 83 | incrementClient.on('connectFailed', function(error) { 84 | console.log('Connect Error: ' + error.toString()); 85 | }); 86 | 87 | incrementClient.on('connect', function(connection) { 88 | console.log('dumb-increment-protocol connected'); 89 | connection.on('error', function(error) { 90 | console.log('Connection Error: ' + error.toString()); 91 | }); 92 | connection.on('close', function() { 93 | console.log('dumb-increment-protocol Connection Closed'); 94 | }); 95 | connection.on('message', function(message) { 96 | console.log('Number: \'' + message.utf8Data + '\''); 97 | }); 98 | }); 99 | 100 | incrementClient.connect(args.protocol + '//' + args.host + ':' + args.port + '/', 'dumb-increment-protocol'); 101 | -------------------------------------------------------------------------------- /hyco-websocket/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var crypto = require('crypto') 4 | var moment = require('moment') 5 | var url = require('url'); 6 | 7 | var WS = module.exports = require('./lib/hybridconnectionswebsocket'); 8 | 9 | /** 10 | * Create a Relay Token 11 | * 12 | * @param {String} uri The URL/address to connect to. 13 | * @param {String} keyName The SharedAccessSignature key name. 14 | * @param {String} key The SharedAccessSignature key value. 15 | * @param {number} expirationSeconds Optional number of seconds until the generated token should expire. Default is 1 hour (3600) if not specified. 16 | * @api public 17 | */ 18 | WS.createRelayToken = function createRelayToken(uri, keyName, key, expirationSeconds) { 19 | var parsedUrl = url.parse(uri); 20 | parsedUrl.protocol = 'http'; 21 | parsedUrl.search = parsedUrl.hash = parsedUrl.port = null; 22 | parsedUrl.pathname = parsedUrl.pathname.replace('$hc/',''); 23 | uri = url.format(parsedUrl); 24 | 25 | if (!expirationSeconds) { 26 | // Token expires in one hour (3600 seconds) 27 | expirationSeconds = 3600; 28 | } 29 | 30 | var unixSeconds = moment().add(expirationSeconds, 'seconds').unix(); 31 | var string_to_sign = encodeURIComponent(uri) + '\n' + unixSeconds; 32 | var hmac = crypto.createHmac('sha256', key); 33 | hmac.update(string_to_sign); 34 | var signature = hmac.digest('base64'); 35 | var token = 'SharedAccessSignature sr=' + encodeURIComponent(uri) + '&sig=' + encodeURIComponent(signature) + '&se=' + unixSeconds + '&skn=' + keyName; 36 | return token; 37 | }; 38 | 39 | /** 40 | * Create a Relay Token and append it to an existing Uri 41 | * 42 | * @param {String} uri The URL/address to connect to. 43 | * @param {String} keyName The SharedAccessSignature key name. 44 | * @param {String} key The SharedAccessSignature key value. 45 | * @param {number} expirationSeconds Optional number of seconds until the generated token should expire. Default is 1 hour (3600) if not specified. 46 | * @api public 47 | */ 48 | WS.appendRelayToken = function appendRelayToken(uri, keyName, key, expirationSeconds) { 49 | var token = WS.createRelayToken(uri, keyName, key, expirationSeconds); 50 | 51 | var parsedUrl = url.parse(uri); 52 | parsedUrl.search = parsedUrl.search + '&sb-hc-token=' + encodeURIComponent(token); 53 | return url.format(parsedUrl); 54 | } 55 | 56 | /** 57 | * Create a Uri for using with Relay Hybrid Connections 58 | * 59 | * @param {String} serviceBusNamespace The ServiceBus namespace, e.g. 'contoso.servicebus.windows.net'. 60 | * @param {String} path The endpoint path. 61 | * @api public 62 | */ 63 | WS.createRelayBaseUri = function createRelayBaseUri(serviceBusNamespace, path) { 64 | return 'wss://' + serviceBusNamespace + ':443/$hc/' + path; 65 | } 66 | 67 | /** 68 | * Create a Uri for sending to a Relay Hybrid Connection endpoint 69 | * 70 | * @param {String} serviceBusNamespace The ServiceBus namespace, e.g. 'contoso.servicebus.windows.net'. 71 | * @param {String} path The endpoint path. 72 | * @param {String} token Optional SharedAccessSignature token for authenticating the sender. 73 | * @param {String} id Optional A Guid string for end to end correlation. 74 | * @api public 75 | */ 76 | WS.createRelaySendUri = function createRelaySendUri(serviceBusNamespace, path, token, id) { 77 | var uri = WS.createRelayBaseUri(serviceBusNamespace, path); 78 | uri = uri + (uri.indexOf('?') == -1 ? '?' : '&') + 'sb-hc-action=connect'; 79 | if (token != null) { 80 | uri = uri + '&sb-hc-token=' + encodeURIComponent(token); 81 | } 82 | if (id != null) { 83 | uri = uri + '&sb-hc-id=' + encodeURIComponent(id); 84 | } 85 | return uri; 86 | } 87 | 88 | /** 89 | * Create a Uri for listening on a Relay Hybrid Connection endpoint 90 | * 91 | * @param {String} serviceBusNamespace The ServiceBus namespace, e.g. 'contoso.servicebus.windows.net'. 92 | * @param {String} path The endpoint path. 93 | * @param {String} token Optional SharedAccessSignature token for authenticating the listener. 94 | * @param {String} id Optional A Guid string for end to end correlation. 95 | * @api public 96 | */ 97 | WS.createRelayListenUri = function createRelayListenUri(serviceBusNamespace, path, token, id) { 98 | var uri = WS.createRelayBaseUri(serviceBusNamespace, path); 99 | uri = uri + (uri.indexOf('?') == -1 ? '?' : '&') + 'sb-hc-action=listen'; 100 | if (token != null) { 101 | uri = uri + '&sb-hc-token=' + encodeURIComponent(token); 102 | } 103 | if (id != null) { 104 | uri = uri + '&sb-hc-id=' + encodeURIComponent(id); 105 | } 106 | return uri; 107 | } -------------------------------------------------------------------------------- /hyco-https/tests/pipe.test.js: -------------------------------------------------------------------------------- 1 | var https = require('..'); 2 | const Stream = require('stream'); 3 | 4 | var ns = process.env.SB_HC_NAMESPACE ? process.env.SB_HC_NAMESPACE.replace(/^"(.*)"$/, '$1') : null; 5 | var path = process.env.SB_HC_PATH ? process.env.SB_HC_PATH : "a2"; 6 | var keyrule = process.env.SB_HC_KEYRULE ? process.env.SB_HC_KEYRULE.replace(/^"(.*)"$/, '$1') : null; 7 | var key = process.env.SB_HC_KEY ? process.env.SB_HC_KEY.replace(/^"(.*)"$/, '$1') : null; 8 | 9 | expect(ns).toBeDefined(); 10 | expect(path).toBeDefined(); 11 | expect(keyrule).toBeDefined(); 12 | expect(key).toBeDefined(); 13 | 14 | var smallMessage = "SmallMessage"; 15 | var kb = ""; 16 | for (var i = 1024; i > 0; i--) { 17 | kb += String.fromCharCode(i % 128); 18 | } 19 | // let stream push 1kb at a time 20 | var over64kbMessage = []; 21 | for (var j = 64; j >= 0; j--) { 22 | over64kbMessage.push(kb); 23 | } 24 | 25 | jest.setTimeout(10000); 26 | 27 | function streamResponse(preStreamMessage, streamMessage, postStreamMessage, done) { 28 | var responseExpected = ""; 29 | var uri = https.createRelayListenUri(ns, path); 30 | var server = https.createRelayedServer( 31 | { 32 | server: uri, 33 | token: () => https.createRelayToken(uri, keyrule, key) 34 | }, 35 | function (req, res) { 36 | var readStream = new Stream.Readable(); 37 | 38 | readStream.on('error', function(err) { 39 | expect(err).toBeUndefined(); 40 | }); 41 | readStream.on('end', function() { 42 | if (postStreamMessage) { 43 | res.write(postStreamMessage); 44 | responseExpected += postStreamMessage; 45 | } 46 | res.end(); 47 | }); 48 | 49 | res.setHeader('Content-type', 'text/plain'); 50 | readStream.pipe(res); 51 | 52 | if (preStreamMessage) { 53 | res.write(preStreamMessage); 54 | responseExpected += preStreamMessage; 55 | } 56 | 57 | for (var i = 0; i < streamMessage.length; i++) { 58 | readStream.push(streamMessage[i]); 59 | responseExpected += streamMessage[i]; 60 | } 61 | readStream.push(null); // signal the end of stream 62 | } 63 | ); 64 | 65 | // fail we get an error 66 | server.listen((err) => { 67 | expect(err).toBeUndefined(); 68 | }); 69 | 70 | // fail if we get an error (we'll always get one if this triggers) 71 | server.on('error', (err) => { 72 | expect(err).toBeUndefined(); 73 | }); 74 | 75 | server.on('listening', () => { 76 | https.get({ 77 | hostname: ns, 78 | path: ((!path || path.length == 0 || path[0] !== '/') ? '/' : '') + path, 79 | port: 443, 80 | method : "GET", 81 | headers: { 82 | 'ServiceBusAuthorization': https.createRelayToken(uri, keyrule, key), 83 | 'Custom' : 'Hello', 84 | } 85 | }, (res) => { 86 | var chunks = ''; 87 | expect(res.statusCode).toBe(200); 88 | res.setEncoding('utf8'); 89 | res.on('data', (chunk) => { 90 | chunks += chunk; 91 | }); 92 | res.on('end', () => { 93 | expect(chunks.length).toBe(Buffer.byteLength(responseExpected)); 94 | expect(chunks).toBe(responseExpected); 95 | server.close(); 96 | jest.clearAllTimers(); 97 | done(); 98 | }); 99 | }).on('error', (e) => { 100 | expect(e).toBeUndefined(); 101 | }); 102 | }); 103 | } 104 | 105 | test('PipeSmallStreamOnly', (done) => { 106 | streamResponse(null, smallMessage, null, done); 107 | }); 108 | 109 | test('PipeLargeStreamOnly', (done) => { 110 | streamResponse(null, over64kbMessage.join(""), null, done); 111 | }); 112 | 113 | test('SmallMessageThenPipeSmallStream', (done) => { 114 | streamResponse(smallMessage, smallMessage, null, done); 115 | }); 116 | 117 | test('LargeMessageThenPipeSmallStream', (done) => { 118 | streamResponse(over64kbMessage.join(""), smallMessage, null, done); 119 | }); 120 | 121 | test('SmallMessageThenPipeLargeStream', (done) => { 122 | streamResponse(smallMessage, over64kbMessage.join(""), null, done); 123 | }); 124 | 125 | test('PipeSmallStreamThenSmallMessage', (done) => { 126 | streamResponse(null, smallMessage, smallMessage, done); 127 | }); 128 | 129 | test('PipeSmallStreamThenLargeMessage', (done) => { 130 | streamResponse(null, smallMessage, over64kbMessage.join(""), done); 131 | }); 132 | 133 | test('PipeLargeStreamThenSmallMessage', (done) => { 134 | streamResponse(null, over64kbMessage.join(""), smallMessage, done); 135 | }); 136 | 137 | 138 | 139 | -------------------------------------------------------------------------------- /hyco-websocket/test/scripts/autobahn-test-client.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /************************************************************************ 3 | * Copyright 2010-2015 Brian McKelvey. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the 'License'); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an 'AS IS' BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | ***********************************************************************/ 17 | 18 | var WebSocketClient = require('../../lib/WebSocketClient'); 19 | var wsVersion = require('../../lib/websocket').version; 20 | var querystring = require('querystring'); 21 | 22 | var args = { /* defaults */ 23 | secure: false, 24 | port: '9000', 25 | host: 'localhost' 26 | }; 27 | 28 | /* Parse command line options */ 29 | var pattern = /^--(.*?)(?:=(.*))?$/; 30 | process.argv.forEach(function(value) { 31 | var match = pattern.exec(value); 32 | if (match) { 33 | args[match[1]] = match[2] ? match[2] : true; 34 | } 35 | }); 36 | 37 | args.protocol = args.secure ? 'wss:' : 'ws:'; 38 | 39 | console.log('WebSocket-Node: Echo test client for running against the Autobahn test suite'); 40 | console.log('Usage: ./autobahn-test-client.js --host=127.0.0.1 --port=9000 [--secure]'); 41 | console.log(''); 42 | 43 | console.log('Starting test run.'); 44 | 45 | getCaseCount(function(caseCount) { 46 | var currentCase = 1; 47 | runNextTestCase(); 48 | 49 | function runNextTestCase() { 50 | runTestCase(currentCase++, caseCount, function() { 51 | if (currentCase <= caseCount) { 52 | process.nextTick(runNextTestCase); 53 | } 54 | else { 55 | process.nextTick(function() { 56 | console.log('Test suite complete, generating report.'); 57 | updateReport(function() { 58 | console.log('Report generated.'); 59 | }); 60 | }); 61 | } 62 | }); 63 | } 64 | }); 65 | 66 | 67 | function runTestCase(caseIndex, caseCount, callback) { 68 | console.log('Running test ' + caseIndex + ' of ' + caseCount); 69 | var echoClient = new WebSocketClient({ 70 | maxReceivedFrameSize: 64*1024*1024, // 64MiB 71 | maxReceivedMessageSize: 64*1024*1024, // 64MiB 72 | fragmentOutgoingMessages: false, 73 | keepalive: false, 74 | disableNagleAlgorithm: false 75 | }); 76 | 77 | echoClient.on('connectFailed', function(error) { 78 | console.log('Connect Error: ' + error.toString()); 79 | }); 80 | 81 | echoClient.on('connect', function(connection) { 82 | connection.on('error', function(error) { 83 | console.log('Connection Error: ' + error.toString()); 84 | }); 85 | connection.on('close', function() { 86 | callback(); 87 | }); 88 | connection.on('message', function(message) { 89 | if (message.type === 'utf8') { 90 | connection.sendUTF(message.utf8Data); 91 | } 92 | else if (message.type === 'binary') { 93 | connection.sendBytes(message.binaryData); 94 | } 95 | }); 96 | }); 97 | 98 | var qs = querystring.stringify({ 99 | case: caseIndex, 100 | agent: 'WebSocket-Node Client v' + wsVersion 101 | }); 102 | echoClient.connect('ws://' + args.host + ':' + args.port + '/runCase?' + qs, []); 103 | } 104 | 105 | function getCaseCount(callback) { 106 | var client = new WebSocketClient(); 107 | var caseCount = NaN; 108 | client.on('connect', function(connection) { 109 | connection.on('close', function() { 110 | callback(caseCount); 111 | }); 112 | connection.on('message', function(message) { 113 | if (message.type === 'utf8') { 114 | console.log('Got case count: ' + message.utf8Data); 115 | caseCount = parseInt(message.utf8Data, 10); 116 | } 117 | else if (message.type === 'binary') { 118 | throw new Error('Unexpected binary message when retrieving case count'); 119 | } 120 | }); 121 | }); 122 | client.connect('ws://' + args.host + ':' + args.port + '/getCaseCount', []); 123 | } 124 | 125 | function updateReport(callback) { 126 | var client = new WebSocketClient(); 127 | var qs = querystring.stringify({ 128 | agent: 'WebSocket-Node Client v' + wsVersion 129 | }); 130 | client.on('connect', function(connection) { 131 | connection.on('close', callback); 132 | }); 133 | client.connect('ws://localhost:9000/updateReports?' + qs); 134 | } 135 | -------------------------------------------------------------------------------- /hyco-https/tests/chunkedpost.test.js: -------------------------------------------------------------------------------- 1 | 2 | var https = require('..') 3 | 4 | var ns = process.env.SB_HC_NAMESPACE ? process.env.SB_HC_NAMESPACE.replace(/^"(.*)"$/, '$1') : null; 5 | var path = process.env.SB_HC_PATH ? process.env.SB_HC_PATH : "a2"; 6 | var keyrule = process.env.SB_HC_KEYRULE ? process.env.SB_HC_KEYRULE.replace(/^"(.*)"$/, '$1') : null; 7 | var key = process.env.SB_HC_KEY ? process.env.SB_HC_KEY.replace(/^"(.*)"$/, '$1') : null; 8 | 9 | expect(ns).toBeDefined(); 10 | expect(path).toBeDefined(); 11 | expect(keyrule).toBeDefined(); 12 | expect(key).toBeDefined(); 13 | 14 | // create a large message over 64kb to force a rendezvous connection 15 | var largeMessage = ""; 16 | for (var i = 1024 * 64; i >= 0; i--) { 17 | largeMessage += String.fromCharCode(i % 128); 18 | } 19 | 20 | function sendAndReceive(requestMsg, responseMsg, done) { 21 | var totalRequests = 10; // Total requests to send over the test 22 | jest.setTimeout(5000 + (totalRequests * 200)); // Expect 5 seconds + 5 requests per second 23 | 24 | var requestLength = requestMsg.length; 25 | var responseLength = responseMsg.length; 26 | var listenerCount = 0; 27 | var senderCount = 0; 28 | 29 | /* set up the listener */ 30 | var uri = https.createRelayListenUri(ns, path); 31 | var server = https.createRelayedServer({ 32 | server: uri, 33 | token: () => https.createRelayToken(uri, keyrule, key) 34 | }, 35 | (req, res) => { 36 | var chunks = ''; 37 | expect(req.method).toBe("POST"); 38 | expect(req.headers.custom).toBe("Hello"); 39 | req.setEncoding('utf-8'); 40 | req.on('data', (chunk) => { 41 | chunks += chunk; 42 | }); 43 | req.on('end', () => { 44 | expect(chunks.length).toBe(requestLength); 45 | expect(chunks).toBe(requestMsg); 46 | res.write(responseMsg.substring(0, responseLength / 4)); 47 | res.write(responseMsg.substring(responseLength / 4, responseLength / 2)); 48 | res.write(responseMsg.substring(responseLength / 2, responseLength)); 49 | res.end(); 50 | listenerCount++; 51 | }); 52 | }); 53 | 54 | // fail we get an error 55 | server.listen((err) => { 56 | expect(err).toBeUndefined(); 57 | }); 58 | // fail if we get an error (we'll always get one if this triggers) 59 | server.on('error', (err) => { 60 | expect(err).toBeUndefined(); 61 | }); 62 | 63 | /* set up the client */ 64 | var clientUri = https.createRelayHttpsUri(ns, path); 65 | var token = https.createRelayToken(clientUri, keyrule, key); 66 | 67 | server.on('listening', () => { 68 | for (var i = 0; i < totalRequests; i++) { 69 | var req = https.request({ 70 | hostname: ns, 71 | path: ((!path || path.length == 0 || path[0] !== '/') ? '/' : '') + path, 72 | port: 443, 73 | method : "POST", 74 | headers: { 75 | 'ServiceBusAuthorization': token, 76 | 'Custom' : 'Hello', 77 | 'Content-Type': 'text/plain', 78 | // 'Content-Length': Buffer.byteLength(requestMsg) 79 | } 80 | }, (res) => { 81 | var chunks = ''; 82 | expect(res.statusCode).toBe(200); 83 | res.setEncoding('utf8'); 84 | res.on('data', (chunk) => { 85 | chunks += chunk; 86 | }); 87 | res.on('end', () => { 88 | expect(chunks.length).toBe(responseLength); 89 | expect(chunks).toBe(responseMsg); 90 | senderCount++; 91 | if (listenerCount == totalRequests && senderCount == totalRequests) { 92 | server.close(); 93 | jest.clearAllTimers(); 94 | done(); 95 | } 96 | }); 97 | }).on('error', (e) => { 98 | expect(e).toBeUndefined(); 99 | }); 100 | 101 | req.write(requestMsg.substring(0, requestLength / 4)); 102 | req.write(requestMsg.substring(requestLength / 4, requestLength / 2)); 103 | req.write(requestMsg.substring(requestLength / 2, requestLength)); 104 | req.end(); 105 | } 106 | }); 107 | } 108 | 109 | test('HttpChunkedPostEmptyReqEmptyRes', (done) => { 110 | sendAndReceive("", "", done); 111 | }) 112 | 113 | test('HttpChunkedPostSmallReqSmallRes', (done) => { 114 | sendAndReceive("Hello", "Goodbye", done); 115 | }); 116 | 117 | test('HttpChunkedPostSmallReqLargeRes', (done) => { 118 | sendAndReceive("Hello", largeMessage, done); 119 | }); 120 | 121 | test('HttpChunkedPostLargeReqSmallRes', (done) => { 122 | sendAndReceive(largeMessage, "Goodbye", done); 123 | }); 124 | 125 | test('HttpChunkedPostLargeReqLargeRes', (done) => { 126 | sendAndReceive(largeMessage, largeMessage, done); 127 | }); -------------------------------------------------------------------------------- /hyco-websocket/test/scripts/fragmentation-test-client.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /************************************************************************ 3 | * Copyright 2010-2015 Brian McKelvey. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the 'License'); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an 'AS IS' BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | ***********************************************************************/ 17 | 18 | var WebSocketClient = require('../../lib/WebSocketClient'); 19 | 20 | console.log('WebSocket-Node: Test client for parsing fragmented messages.'); 21 | 22 | var args = { /* defaults */ 23 | secure: false, 24 | port: '8080', 25 | host: '127.0.0.1', 26 | 'no-defragment': false, 27 | binary: false 28 | }; 29 | 30 | /* Parse command line options */ 31 | var pattern = /^--(.*?)(?:=(.*))?$/; 32 | process.argv.forEach(function(value) { 33 | var match = pattern.exec(value); 34 | if (match) { 35 | args[match[1]] = match[2] ? match[2] : true; 36 | } 37 | }); 38 | 39 | args.protocol = args.secure ? 'wss:' : 'ws:'; 40 | 41 | if (args.help) { 42 | console.log('Usage: ./fragmentation-test-client.js [--host=127.0.0.1] [--port=8080] [--no-defragment] [--binary]'); 43 | console.log(''); 44 | return; 45 | } 46 | else { 47 | console.log('Use --help for usage information.'); 48 | } 49 | 50 | 51 | var client = new WebSocketClient({ 52 | maxReceivedMessageSize: 128*1024*1024, // 128 MiB 53 | maxReceivedFrameSize: 1*1024*1024, // 1 MiB 54 | assembleFragments: !args['no-defragment'] 55 | }); 56 | 57 | client.on('connectFailed', function(error) { 58 | console.log('Client Error: ' + error.toString()); 59 | }); 60 | 61 | 62 | var requestedLength = 100; 63 | var messageSize = 0; 64 | var startTime; 65 | var byteCounter; 66 | 67 | client.on('connect', function(connection) { 68 | console.log('Connected'); 69 | startTime = new Date(); 70 | byteCounter = 0; 71 | 72 | connection.on('error', function(error) { 73 | console.log('Connection Error: ' + error.toString()); 74 | }); 75 | 76 | connection.on('close', function() { 77 | console.log('Connection Closed'); 78 | }); 79 | 80 | connection.on('message', function(message) { 81 | if (message.type === 'utf8') { 82 | console.log('Received utf-8 message of ' + message.utf8Data.length + ' characters.'); 83 | logThroughput(message.utf8Data.length); 84 | requestData(); 85 | } 86 | else { 87 | console.log('Received binary message of ' + message.binaryData.length + ' bytes.'); 88 | logThroughput(message.binaryData.length); 89 | requestData(); 90 | } 91 | }); 92 | 93 | connection.on('frame', function(frame) { 94 | console.log('Frame: 0x' + frame.opcode.toString(16) + '; ' + frame.length + ' bytes; Flags: ' + renderFlags(frame)); 95 | messageSize += frame.length; 96 | if (frame.fin) { 97 | console.log('Total message size: ' + messageSize + ' bytes.'); 98 | logThroughput(messageSize); 99 | messageSize = 0; 100 | requestData(); 101 | } 102 | }); 103 | 104 | function logThroughput(numBytes) { 105 | byteCounter += numBytes; 106 | var duration = (new Date()).valueOf() - startTime.valueOf(); 107 | if (duration > 1000) { 108 | var kiloBytesPerSecond = Math.round((byteCounter / 1024) / (duration/1000)); 109 | console.log(' Throughput: ' + kiloBytesPerSecond + ' KBps'); 110 | startTime = new Date(); 111 | byteCounter = 0; 112 | } 113 | } 114 | 115 | function sendUTFCallback(err) { 116 | if (err) { console.error('sendUTF() error: ' + err); } 117 | } 118 | 119 | function requestData() { 120 | if (args.binary) { 121 | connection.sendUTF('sendBinaryMessage|' + requestedLength, sendUTFCallback); 122 | } 123 | else { 124 | connection.sendUTF('sendMessage|' + requestedLength, sendUTFCallback); 125 | } 126 | requestedLength += Math.ceil(Math.random() * 1024); 127 | } 128 | 129 | function renderFlags(frame) { 130 | var flags = []; 131 | if (frame.fin) { 132 | flags.push('[FIN]'); 133 | } 134 | if (frame.rsv1) { 135 | flags.push('[RSV1]'); 136 | } 137 | if (frame.rsv2) { 138 | flags.push('[RSV2]'); 139 | } 140 | if (frame.rsv3) { 141 | flags.push('[RSV3]'); 142 | } 143 | if (frame.mask) { 144 | flags.push('[MASK]'); 145 | } 146 | if (flags.length === 0) { 147 | return '---'; 148 | } 149 | return flags.join(' '); 150 | } 151 | 152 | requestData(); 153 | }); 154 | 155 | if (args['no-defragment']) { 156 | console.log('Not automatically re-assembling fragmented messages.'); 157 | } 158 | else { 159 | console.log('Maximum aggregate message size: ' + client.config.maxReceivedMessageSize + ' bytes.'); 160 | } 161 | console.log('Connecting'); 162 | 163 | client.connect(args.protocol + '//' + args.host + ':' + args.port + '/', 'fragmentation-test'); 164 | -------------------------------------------------------------------------------- /hyco-websocket/test/scripts/fragmentation-test-server.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /************************************************************************ 3 | * Copyright 2010-2015 Brian McKelvey. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the 'License'); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an 'AS IS' BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | ***********************************************************************/ 17 | 18 | var WebSocketServer = require('../../lib/WebSocketServer'); 19 | var WebSocketRouter = require('../../lib/WebSocketRouter'); 20 | var http = require('http'); 21 | var fs = require('fs'); 22 | 23 | console.log('WebSocket-Node: Test server to spit out fragmented messages.'); 24 | 25 | var args = { 26 | 'no-fragmentation': false, 27 | 'fragment': '16384', 28 | 'port': '8080' 29 | }; 30 | 31 | /* Parse command line options */ 32 | var pattern = /^--(.*?)(?:=(.*))?$/; 33 | process.argv.forEach(function(value) { 34 | var match = pattern.exec(value); 35 | if (match) { 36 | args[match[1]] = match[2] ? match[2] : true; 37 | } 38 | }); 39 | 40 | args.protocol = 'ws:'; 41 | 42 | if (args.help) { 43 | console.log('Usage: ./fragmentation-test-server.js [--port=8080] [--fragment=n] [--no-fragmentation]'); 44 | console.log(''); 45 | return; 46 | } 47 | else { 48 | console.log('Use --help for usage information.'); 49 | } 50 | 51 | var server = http.createServer(function(request, response) { 52 | console.log((new Date()) + ' Received request for ' + request.url); 53 | if (request.url === '/') { 54 | fs.readFile('fragmentation-test-page.html', 'utf8', function(err, data) { 55 | if (err) { 56 | response.writeHead(404); 57 | response.end(); 58 | } 59 | else { 60 | response.writeHead(200, { 61 | 'Content-Type': 'text/html' 62 | }); 63 | response.end(data); 64 | } 65 | }); 66 | } 67 | else { 68 | response.writeHead(404); 69 | response.end(); 70 | } 71 | }); 72 | server.listen(args.port, function() { 73 | console.log((new Date()) + ' Server is listening on port ' + args.port); 74 | }); 75 | 76 | var wsServer = new WebSocketServer({ 77 | httpServer: server, 78 | fragmentOutgoingMessages: !args['no-fragmentation'], 79 | fragmentationThreshold: parseInt(args['fragment'], 10) 80 | }); 81 | 82 | var router = new WebSocketRouter(); 83 | router.attachServer(wsServer); 84 | 85 | var lorem = 'Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat.'; 86 | 87 | router.mount('*', 'fragmentation-test', function(request) { 88 | var connection = request.accept(request.origin); 89 | console.log((new Date()) + ' connection accepted from ' + connection.remoteAddress); 90 | 91 | 92 | connection.on('message', function(message) { 93 | function sendCallback(err) { 94 | if (err) { console.error('send() error: ' + err); } 95 | } 96 | if (message.type === 'utf8') { 97 | var length = 0; 98 | var match = /sendMessage\|(\d+)/.exec(message.utf8Data); 99 | var requestedLength; 100 | if (match) { 101 | requestedLength = parseInt(match[1], 10); 102 | var longLorem = ''; 103 | while (length < requestedLength) { 104 | longLorem += (' ' + lorem); 105 | length = Buffer.byteLength(longLorem); 106 | } 107 | longLorem = longLorem.slice(0,requestedLength); 108 | length = Buffer.byteLength(longLorem); 109 | if (length > 0) { 110 | connection.sendUTF(longLorem, sendCallback); 111 | console.log((new Date()) + ' sent ' + length + ' byte utf-8 message to ' + connection.remoteAddress); 112 | } 113 | return; 114 | } 115 | 116 | match = /sendBinaryMessage\|(\d+)/.exec(message.utf8Data); 117 | if (match) { 118 | requestedLength = parseInt(match[1], 10); 119 | 120 | // Generate random binary data. 121 | var buffer = new Buffer(requestedLength); 122 | for (var i = 0; i < requestedLength; i++) { 123 | buffer[i] = Math.ceil(Math.random()*255); 124 | } 125 | 126 | connection.sendBytes(buffer, sendCallback); 127 | console.log((new Date()) + ' sent ' + buffer.length + ' byte binary message to ' + connection.remoteAddress); 128 | return; 129 | } 130 | } 131 | }); 132 | 133 | connection.on('close', function(reasonCode, description) { 134 | console.log((new Date()) + ' peer ' + connection.remoteAddress + ' disconnected.'); 135 | }); 136 | 137 | connection.on('error', function(error) { 138 | console.log('Connection error for peer ' + connection.remoteAddress + ': ' + error); 139 | }); 140 | }); 141 | 142 | console.log('Point your WebSocket Protocol Version 8 compliant browser at http://localhost:' + args.port + '/'); 143 | if (args['no-fragmentation']) { 144 | console.log('Fragmentation disabled.'); 145 | } 146 | else { 147 | console.log('Fragmenting messages at ' + wsServer.config.fragmentationThreshold + ' bytes'); 148 | } 149 | -------------------------------------------------------------------------------- /hyco-websocket/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | // JSHint Default Configuration File (as on JSHint website) 3 | // See http://jshint.com/docs/ for more details 4 | 5 | "maxerr" : 50, // {int} Maximum error before stopping 6 | 7 | // Enforcing 8 | "bitwise" : false, // true: Prohibit bitwise operators (&, |, ^, etc.) 9 | "camelcase" : false, // true: Identifiers must be in camelCase 10 | "curly" : true, // true: Require {} for every new block or scope 11 | "eqeqeq" : true, // true: Require triple equals (===) for comparison 12 | "freeze" : true, // true: prohibits overwriting prototypes of native objects such as Array, Date etc. 13 | "forin" : false, // true: Require filtering for..in loops with obj.hasOwnProperty() 14 | "immed" : true, // true: Require immediate invocations to be wrapped in parens e.g. `(function () { } ());` 15 | "latedef" : "nofunc", // true: Require variables/functions to be defined before being used 16 | "newcap" : true, // true: Require capitalization of all constructor functions e.g. `new F()` 17 | "noarg" : true, // true: Prohibit use of `arguments.caller` and `arguments.callee` 18 | "noempty" : true, // true: Prohibit use of empty blocks 19 | "nonbsp" : true, // true: Prohibit "non-breaking whitespace" characters. 20 | "nonew" : true, // true: Prohibit use of constructors for side-effects (without assignment) 21 | "plusplus" : false, // true: Prohibit use of `++` & `--` 22 | "quotmark" : "single", // Quotation mark consistency: 23 | // false : do nothing (default) 24 | // true : ensure whatever is used is consistent 25 | // "single" : require single quotes 26 | // "double" : require double quotes 27 | "undef" : true, // true: Require all non-global variables to be declared (prevents global leaks) 28 | "unused" : "vars", // vars: Require all defined variables be used, ignore function params 29 | "strict" : false, // true: Requires all functions run in ES5 Strict Mode 30 | "maxparams" : false, // {int} Max number of formal params allowed per function 31 | "maxdepth" : false, // {int} Max depth of nested blocks (within functions) 32 | "maxstatements" : false, // {int} Max number statements per function 33 | "maxcomplexity" : false, // {int} Max cyclomatic complexity per function 34 | "maxlen" : false, // {int} Max number of characters per line 35 | 36 | // Relaxing 37 | "asi" : false, // true: Tolerate Automatic Semicolon Insertion (no semicolons) 38 | "boss" : false, // true: Tolerate assignments where comparisons would be expected 39 | "debug" : false, // true: Allow debugger statements e.g. browser breakpoints. 40 | "eqnull" : false, // true: Tolerate use of `== null` 41 | "es5" : false, // true: Allow ES5 syntax (ex: getters and setters) 42 | "esnext" : true, // true: Allow ES.next (ES6) syntax (ex: `const`) 43 | "moz" : false, // true: Allow Mozilla specific syntax (extends and overrides esnext features) 44 | // (ex: `for each`, multiple try/catch, function expression…) 45 | "evil" : false, // true: Tolerate use of `eval` and `new Function()` 46 | "expr" : false, // true: Tolerate `ExpressionStatement` as Programs 47 | "funcscope" : false, // true: Tolerate defining variables inside control statements 48 | "globalstrict" : false, // true: Allow global "use strict" (also enables 'strict') 49 | "iterator" : false, // true: Tolerate using the `__iterator__` property 50 | "lastsemic" : false, // true: Tolerate omitting a semicolon for the last statement of a 1-line block 51 | "laxbreak" : false, // true: Tolerate possibly unsafe line breakings 52 | "laxcomma" : false, // true: Tolerate comma-first style coding 53 | "loopfunc" : false, // true: Tolerate functions being defined in loops 54 | "multistr" : false, // true: Tolerate multi-line strings 55 | "noyield" : false, // true: Tolerate generator functions with no yield statement in them. 56 | "notypeof" : false, // true: Tolerate invalid typeof operator values 57 | "proto" : false, // true: Tolerate using the `__proto__` property 58 | "scripturl" : false, // true: Tolerate script-targeted URLs 59 | "shadow" : false, // true: Allows re-define variables later in code e.g. `var x=1; x=2;` 60 | "sub" : true, // true: Tolerate using `[]` notation when it can still be expressed in dot notation 61 | "supernew" : false, // true: Tolerate `new function () { ... };` and `new Object;` 62 | "validthis" : false, // true: Tolerate using this in a non-constructor function 63 | 64 | // Environments 65 | "browser" : true, // Web Browser (window, document, etc) 66 | "browserify" : true, // Browserify (node.js code in the browser) 67 | "couch" : false, // CouchDB 68 | "devel" : true, // Development/debugging (alert, confirm, etc) 69 | "dojo" : false, // Dojo Toolkit 70 | "jasmine" : false, // Jasmine 71 | "jquery" : false, // jQuery 72 | "mocha" : false, // Mocha 73 | "mootools" : false, // MooTools 74 | "node" : true, // Node.js 75 | "nonstandard" : false, // Widely adopted globals (escape, unescape, etc) 76 | "prototypejs" : false, // Prototype and Scriptaculous 77 | "qunit" : false, // QUnit 78 | "rhino" : false, // Rhino 79 | "shelljs" : false, // ShellJS 80 | "worker" : false, // Web Workers 81 | "wsh" : false, // Windows Scripting Host 82 | "yui" : false, // Yahoo User Interface 83 | 84 | // Custom Globals 85 | "globals" : { // additional predefined global variables 86 | "WebSocket": true 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /hyco-ws/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var crypto = require('crypto') 4 | var moment = require('moment') 5 | var url = require('url'); 6 | 7 | /** 8 | * Adapted from 9 | * ws: a node.js websocket client 10 | * Copyright(c) 2011 Einar Otto Stangvik 11 | * MIT Licensed 12 | * 13 | */ 14 | 15 | var WS = module.exports = require('ws'); 16 | 17 | WS.RelayedServer = require('./lib/HybridConnectionWebSocketServer'); 18 | 19 | /** 20 | * Create a new HybridConnectionsWebSocketServer. 21 | * 22 | * @param {Object} options Server options 23 | * @param {Function} fn Optional connection listener. 24 | * @returns {WS.RelayedServer} 25 | * @api public 26 | */ 27 | WS.createRelayedServer = function createRelayedServer(options, fn) { 28 | var server = new WS.RelayedServer(options); 29 | 30 | if (typeof fn === 'function') { 31 | server.on('connection', fn); 32 | } 33 | 34 | return server; 35 | }; 36 | 37 | /** 38 | * Create a new WebSocket connection. 39 | * 40 | * @param {String} address The URL/address we need to connect to. 41 | * @param {String} token Optional relay access token for sending. 42 | * @param {Function} fn Open listener. 43 | * @returns {WS} 44 | * @api public 45 | */ 46 | WS.relayedConnect = function relayedConnect(address, token, fn) { 47 | var opt = null; 48 | if (token != null) { 49 | opt = { headers : { 'ServiceBusAuthorization' : token}}; 50 | }; 51 | var client = new WS(address, null, opt); 52 | 53 | if (typeof fn === 'function') { 54 | client.on('open', function() { fn(client) }); 55 | } 56 | 57 | return client; 58 | }; 59 | 60 | /** 61 | * Create a Relay Token 62 | * 63 | * @param {String} uri The URL/address to connect to. 64 | * @param {String} keyName The SharedAccessSignature key name. 65 | * @param {String} key The SharedAccessSignature key value. 66 | * @param {number} expirationSeconds Optional number of seconds until the generated token should expire. Default is 1 hour (3600) if not specified. 67 | * @api public 68 | */ 69 | WS.createRelayToken = function createRelayToken(uri, keyName, key, expirationSeconds) { 70 | var parsedUrl = url.parse(uri); 71 | parsedUrl.protocol = 'http'; 72 | parsedUrl.search = parsedUrl.hash = parsedUrl.port = null; 73 | parsedUrl.pathname = parsedUrl.pathname.replace('$hc/',''); 74 | uri = url.format(parsedUrl); 75 | 76 | if (!expirationSeconds) { 77 | // Token expires in one hour (3600 seconds) 78 | expirationSeconds = 3600; 79 | } 80 | 81 | var unixSeconds = moment().add(expirationSeconds, 'seconds').unix(); 82 | var string_to_sign = encodeURIComponent(uri) + '\n' + unixSeconds; 83 | var hmac = crypto.createHmac('sha256', key); 84 | hmac.update(string_to_sign); 85 | var signature = hmac.digest('base64'); 86 | var token = 'SharedAccessSignature sr=' + encodeURIComponent(uri) + '&sig=' + encodeURIComponent(signature) + '&se=' + unixSeconds + '&skn=' + keyName; 87 | return token; 88 | }; 89 | 90 | /** 91 | * Create a Relay Token and append it to an existing Uri 92 | * 93 | * @param {String} uri The URL/address to connect to. 94 | * @param {String} keyName The SharedAccessSignature key name. 95 | * @param {String} key The SharedAccessSignature key value. 96 | * @param {number} expirationSeconds Optional number of seconds until the generated token should expire. Default is 1 hour (3600) if not specified. 97 | * @api public 98 | */ 99 | WS.appendRelayToken = function appendRelayToken(uri, keyName, key, expirationSeconds) { 100 | var token = WS.createRelayToken(uri, keyName, key, expirationSeconds); 101 | 102 | var parsedUrl = url.parse(uri); 103 | parsedUrl.search = parsedUrl.search + '&sb-hc-token=' + encodeURIComponent(token); 104 | return url.format(parsedUrl); 105 | } 106 | 107 | /** 108 | * Create a Uri for using with Relay Hybrid Connections 109 | * 110 | * @param {String} serviceBusNamespace The ServiceBus namespace, e.g. 'contoso.servicebus.windows.net'. 111 | * @param {String} path The endpoint path. 112 | * @api public 113 | */ 114 | WS.createRelayBaseUri = function createRelayBaseUri(serviceBusNamespace, path) { 115 | return 'wss://' + serviceBusNamespace + ':443/$hc/' + path; 116 | } 117 | 118 | /** 119 | * Create a Uri for sending to a Relay Hybrid Connection endpoint 120 | * 121 | * @param {String} serviceBusNamespace The ServiceBus namespace, e.g. 'contoso.servicebus.windows.net'. 122 | * @param {String} path The endpoint path. 123 | * @param {String} token Optional SharedAccessSignature token for authenticating the sender. 124 | * @param {String} id Optional A Guid string for end to end correlation. 125 | * @api public 126 | */ 127 | WS.createRelaySendUri = function createRelaySendUri(serviceBusNamespace, path, token, id) { 128 | var uri = WS.createRelayBaseUri(serviceBusNamespace, path); 129 | uri = uri + (uri.indexOf('?') == -1 ? '?' : '&') + 'sb-hc-action=connect'; 130 | if (token != null) { 131 | uri = uri + '&sb-hc-token=' + encodeURIComponent(token); 132 | } 133 | if (id != null) { 134 | uri = uri + '&sb-hc-id=' + encodeURIComponent(id); 135 | } 136 | return uri; 137 | } 138 | 139 | /** 140 | * Create a Uri for listening on a Relay Hybrid Connection endpoint 141 | * 142 | * @param {String} serviceBusNamespace The ServiceBus namespace, e.g. 'contoso.servicebus.windows.net'. 143 | * @param {String} path The endpoint path. 144 | * @param {String} token Optional SharedAccessSignature token for authenticating the listener. 145 | * @param {String} id Optional A Guid string for end to end correlation. 146 | * @api public 147 | */ 148 | WS.createRelayListenUri = function createRelayListenUri(serviceBusNamespace, path, token, id) { 149 | var uri = WS.createRelayBaseUri(serviceBusNamespace, path); 150 | uri = uri + (uri.indexOf('?') == -1 ? '?' : '&') + 'sb-hc-action=listen'; 151 | if (token != null) { 152 | uri = uri + '&sb-hc-token=' + encodeURIComponent(token); 153 | } 154 | if (id != null) { 155 | uri = uri + '&sb-hc-id=' + encodeURIComponent(id); 156 | } 157 | return uri; 158 | } -------------------------------------------------------------------------------- /hyco-https/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var crypto = require('crypto') 4 | var moment = require('moment') 5 | var url = require('url'); 6 | 7 | 8 | var https = module.exports = require('https'); 9 | var relay = require('./lib/HybridConnectionHttpsServer'); 10 | 11 | https.Server = relay.Server; 12 | https.ServerResponse = relay.ServerResponse; 13 | 14 | /** 15 | * Create a new HybridConnectionsHttpsServer. 16 | * 17 | * @param {Object} options Server options 18 | * @param {Function} fn Optional connection listener. 19 | * @returns {https.RelayedServer} 20 | * @api public 21 | */ 22 | https.createRelayedServer = function createServer(options, fn) { 23 | var server = new https.Server(options, fn); 24 | return server; 25 | }; 26 | 27 | /** 28 | * Create a Relay Token 29 | * 30 | * @param {String} uri The URL/address to connect to. 31 | * @param {String} keyName The SharedAccessSignature key name. 32 | * @param {String} key The SharedAccessSignature key value. 33 | * @param {number} expirationSeconds Optional number of seconds until the generated token should expire. Default is 1 hour (3600) if not specified. 34 | * @api public 35 | */ 36 | https.createRelayToken = function createRelayToken(uri, keyName, key, expirationSeconds) { 37 | var parsedUrl = url.parse(uri); 38 | parsedUrl.protocol = 'http'; 39 | parsedUrl.search = parsedUrl.hash = parsedUrl.port = null; 40 | parsedUrl.pathname = parsedUrl.pathname.replace('$hc/',''); 41 | uri = url.format(parsedUrl); 42 | 43 | if (!expirationSeconds) { 44 | // Token expires in one hour (3600 seconds) 45 | expirationSeconds = 3600; 46 | } 47 | 48 | var unixSeconds = moment().add(expirationSeconds, 'seconds').unix(); 49 | var string_to_sign = encodeURIComponent(uri) + '\n' + unixSeconds; 50 | var hmac = crypto.createHmac('sha256', key); 51 | hmac.update(string_to_sign); 52 | var signature = hmac.digest('base64'); 53 | var token = 'SharedAccessSignature sr=' + encodeURIComponent(uri) + '&sig=' + encodeURIComponent(signature) + '&se=' + unixSeconds + '&skn=' + keyName; 54 | return token; 55 | }; 56 | 57 | /** 58 | * Create a Relay Token and append it to an existing Uri 59 | * 60 | * @param {String} uri The URL/address to connect to. 61 | * @param {String} keyName The SharedAccessSignature key name. 62 | * @param {String} key The SharedAccessSignature key value. 63 | * @param {number} expirationSeconds Optional number of seconds until the generated token should expire. Default is 1 hour (3600) if not specified. 64 | * @api public 65 | */ 66 | https.appendRelayToken = function appendRelayToken(uri, keyName, key, expirationSeconds) { 67 | var token = https.createRelayToken(uri, keyName, key, expirationSeconds); 68 | 69 | var parsedUrl = url.parse(uri); 70 | parsedUrl.search = parsedUrl.search + (uri.indexOf('?') == -1 ? '?' : '&') + 'sb-hc-token=' + encodeURIComponent(token); 71 | return url.format(parsedUrl); 72 | } 73 | 74 | /** 75 | * Create a Uri for using with Relay Hybrid Connections 76 | * 77 | * @param {String} serviceBusNamespace The ServiceBus namespace, e.g. 'contoso.servicebus.windows.net'. 78 | * @param {String} path The endpoint path. 79 | * @api public 80 | */ 81 | https.createRelayBaseUri = function createRelayBaseUri(serviceBusNamespace, path) { 82 | return 'wss://' + serviceBusNamespace + ':443/$hc/' + path; 83 | } 84 | 85 | /** 86 | * Create a Uri for requesting from a Relay Hybrid Connection endpoint 87 | * 88 | * @param {String} serviceBusNamespace The ServiceBus namespace, e.g. 'contoso.servicebus.windows.net'. 89 | * @param {String} path The endpoint path. 90 | * @param {String} token Optional SharedAccessSignature token for authenticating the sender. 91 | * @param {String} id Optional A Guid string for end to end correlation. 92 | * @api public 93 | */ 94 | https.createRelayHttpsUri = function createRelayHttpsUri(serviceBusNamespace, path, token, id) { 95 | var uri = 'https://' + serviceBusNamespace + '/' + path; 96 | if (token != null) { 97 | uri = uri + (uri.indexOf('?') == -1 ? '?' : '&') + 'sb-hc-token=' + encodeURIComponent(token); 98 | } 99 | if (id != null) { 100 | uri = uri + (uri.indexOf('?') == -1 ? '?' : '&') + 'sb-hc-id=' + encodeURIComponent(id); 101 | } 102 | return uri; 103 | } 104 | 105 | /** 106 | * Create a Uri for sending to a Relay Hybrid Connection Websocket endpoint 107 | * 108 | * @param {String} serviceBusNamespace The ServiceBus namespace, e.g. 'contoso.servicebus.windows.net'. 109 | * @param {String} path The endpoint path. 110 | * @param {String} token Optional SharedAccessSignature token for authenticating the sender. 111 | * @param {String} id Optional A Guid string for end to end correlation. 112 | * @api public 113 | */ 114 | 115 | https.createRelaySendUri = function createRelaySendUri(serviceBusNamespace, path, token, id) { 116 | var uri = https.createRelayBaseUri(serviceBusNamespace, path); 117 | uri = uri + (uri.indexOf('?') == -1 ? '?' : '&') + 'sb-hc-action=connect'; 118 | if (token != null) { 119 | uri = uri + '&sb-hc-token=' + encodeURIComponent(token); 120 | } 121 | if (id != null) { 122 | uri = uri + '&sb-hc-id=' + encodeURIComponent(id); 123 | } 124 | return uri; 125 | } 126 | 127 | /** 128 | * Create a Uri for listening on a Relay Hybrid Connection endpoint 129 | * 130 | * @param {String} serviceBusNamespace The ServiceBus namespace, e.g. 'contoso.servicebus.windows.net'. 131 | * @param {String} path The endpoint path. 132 | * @param {String} token Optional SharedAccessSignature token for authenticating the listener. 133 | * @param {String} id Optional A Guid string for end to end correlation. 134 | * @api public 135 | */ 136 | https.createRelayListenUri = function createRelayListenUri(serviceBusNamespace, path, token, id) { 137 | var uri = https.createRelayBaseUri(serviceBusNamespace, path); 138 | uri = uri + (uri.indexOf('?') == -1 ? '?' : '&') + 'sb-hc-action=listen'; 139 | if (token != null) { 140 | uri = uri + '&sb-hc-token=' + encodeURIComponent(token); 141 | } 142 | if (id != null) { 143 | uri = uri + '&sb-hc-id=' + encodeURIComponent(id); 144 | } 145 | return uri; 146 | } -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | // For a detailed explanation regarding each configuration property, visit: 2 | // https://jestjs.io/docs/en/configuration.html 3 | 4 | module.exports = { 5 | // All imported modules in your tests should be mocked automatically 6 | // automock: false, 7 | 8 | // Stop running tests after the first failure 9 | // bail: false, 10 | 11 | // Respect "browser" field in package.json when resolving modules 12 | browser: false, 13 | 14 | // The directory where Jest should store its cached dependency information 15 | // cacheDirectory: "C:\\Users\\clemensv\\AppData\\Local\\Temp\\jest", 16 | 17 | // Automatically clear mock calls and instances between every test 18 | // clearMocks: false, 19 | 20 | // Indicates whether the coverage information should be collected while executing the test 21 | // collectCoverage: false, 22 | 23 | // An array of glob patterns indicating a set of files for which coverage information should be collected 24 | // collectCoverageFrom: null, 25 | 26 | // The directory where Jest should output its coverage files 27 | // coverageDirectory: "coverage", 28 | 29 | // An array of regexp pattern strings used to skip coverage collection 30 | // coveragePathIgnorePatterns: [ 31 | // "\\\\node_modules\\\\" 32 | // ], 33 | 34 | // A list of reporter names that Jest uses when writing coverage reports 35 | // coverageReporters: [ 36 | // "json", 37 | // "text", 38 | // "lcov", 39 | // "clover" 40 | // ], 41 | 42 | // An object that configures minimum threshold enforcement for coverage results 43 | // coverageThreshold: null, 44 | 45 | // Make calling deprecated APIs throw helpful error messages 46 | // errorOnDeprecated: false, 47 | 48 | // Force coverage collection from ignored files usin a array of glob patterns 49 | // forceCoverageMatch: [], 50 | 51 | // A path to a module which exports an async function that is triggered once before all test suites 52 | // globalSetup: null, 53 | 54 | // A path to a module which exports an async function that is triggered once after all test suites 55 | // globalTeardown: null, 56 | 57 | // A set of global variables that need to be available in all test environments 58 | // globals: {}, 59 | 60 | // An array of directory names to be searched recursively up from the requiring module's location 61 | // moduleDirectories: [ 62 | // "node_modules" 63 | // ], 64 | 65 | // An array of file extensions your modules use 66 | // moduleFileExtensions: [ 67 | // "js", 68 | // "json", 69 | // "jsx", 70 | // "node" 71 | // ], 72 | 73 | // A map from regular expressions to module names that allow to stub out resources with a single module 74 | // moduleNameMapper: {}, 75 | 76 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader 77 | // modulePathIgnorePatterns: [], 78 | 79 | // Activates notifications for test results 80 | // notify: false, 81 | 82 | // An enum that specifies notification mode. Requires { notify: true } 83 | // notifyMode: "always", 84 | 85 | // A preset that is used as a base for Jest's configuration 86 | // preset: null, 87 | 88 | // Run tests from one or more projects 89 | // projects: null, 90 | 91 | // Use this configuration option to add custom reporters to Jest 92 | // reporters: undefined, 93 | 94 | // Automatically reset mock state between every test 95 | // resetMocks: false, 96 | 97 | // Reset the module registry before running each individual test 98 | // resetModules: false, 99 | 100 | // A path to a custom resolver 101 | // resolver: null, 102 | 103 | // Automatically restore mock state between every test 104 | // restoreMocks: false, 105 | 106 | // The root directory that Jest should scan for tests and modules within 107 | // rootDir: null, 108 | 109 | // A list of paths to directories that Jest should use to search for files in 110 | // roots: [ 111 | // "" 112 | // ], 113 | 114 | // Allows you to use a custom runner instead of Jest's default test runner 115 | // runner: "jest-runner", 116 | 117 | // The paths to modules that run some code to configure or set up the testing environment before each test 118 | // setupFiles: [], 119 | 120 | // The path to a module that runs some code to configure or set up the testing framework before each test 121 | // setupTestFrameworkScriptFile: null, 122 | 123 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing 124 | // snapshotSerializers: [], 125 | 126 | // The test environment that will be used for testing 127 | testEnvironment: "node", 128 | 129 | // Options that will be passed to the testEnvironment 130 | // testEnvironmentOptions: {}, 131 | 132 | // Adds a location field to test results 133 | // testLocationInResults: false, 134 | 135 | // The glob patterns Jest uses to detect test files 136 | // testMatch: [ 137 | // "**/__tests__/**/*.js?(x)", 138 | // "**/?(*.)+(spec|test).js?(x)" 139 | // ], 140 | 141 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped 142 | // testPathIgnorePatterns: [ 143 | // "\\\\node_modules\\\\" 144 | // ], 145 | 146 | // The regexp pattern Jest uses to detect test files 147 | // testRegex: "", 148 | 149 | // This option allows the use of a custom results processor 150 | // testResultsProcessor: null, 151 | 152 | // This option allows use of a custom test runner 153 | // testRunner: "jasmine2", 154 | 155 | // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href 156 | // testURL: "about:blank", 157 | 158 | // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout" 159 | // timers: "real", 160 | 161 | // A map from regular expressions to paths to transformers 162 | // transform: null, 163 | 164 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation 165 | // transformIgnorePatterns: [ 166 | // "\\\\node_modules\\\\" 167 | // ], 168 | 169 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them 170 | // unmockedModulePathPatterns: undefined, 171 | 172 | // Indicates whether each individual test should be reported during the run 173 | // verbose: null, 174 | 175 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode 176 | // watchPathIgnorePatterns: [], 177 | 178 | // Whether to use watchman for file crawling 179 | watchman: false, 180 | }; 181 | -------------------------------------------------------------------------------- /hyco-websocket/test/scripts/libwebsockets-test-server.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /************************************************************************ 3 | * Copyright 2010-2015 Brian McKelvey. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the 'License'); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an 'AS IS' BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | ***********************************************************************/ 17 | 18 | var WebSocketServer = require('../../lib/WebSocketServer'); 19 | var WebSocketRouter = require('../../lib/WebSocketRouter'); 20 | var http = require('http'); 21 | var fs = require('fs'); 22 | 23 | var args = { /* defaults */ 24 | secure: false 25 | }; 26 | 27 | /* Parse command line options */ 28 | var pattern = /^--(.*?)(?:=(.*))?$/; 29 | process.argv.forEach(function(value) { 30 | var match = pattern.exec(value); 31 | if (match) { 32 | args[match[1]] = match[2] ? match[2] : true; 33 | } 34 | }); 35 | 36 | args.protocol = args.secure ? 'wss:' : 'ws:'; 37 | 38 | if (!args.port) { 39 | console.log('WebSocket-Node: Test Server implementing Andy Green\'s'); 40 | console.log('libwebsockets-test-server protocols.'); 41 | console.log('Usage: ./libwebsockets-test-server.js --port=8080 [--secure]'); 42 | console.log(''); 43 | return; 44 | } 45 | 46 | if (args.secure) { 47 | console.log('WebSocket-Node: Test Server implementing Andy Green\'s'); 48 | console.log('libwebsockets-test-server protocols.'); 49 | console.log('ERROR: TLS is not yet supported.'); 50 | console.log(''); 51 | return; 52 | } 53 | 54 | var server = http.createServer(function(request, response) { 55 | console.log((new Date()) + ' Received request for ' + request.url); 56 | if (request.url === '/') { 57 | fs.readFile('libwebsockets-test.html', 'utf8', function(err, data) { 58 | if (err) { 59 | response.writeHead(404); 60 | response.end(); 61 | } 62 | else { 63 | response.writeHead(200, { 64 | 'Content-Type': 'text/html' 65 | }); 66 | response.end(data); 67 | } 68 | }); 69 | } 70 | else { 71 | response.writeHead(404); 72 | response.end(); 73 | } 74 | }); 75 | server.listen(args.port, function() { 76 | console.log((new Date()) + ' Server is listening on port ' + args.port); 77 | }); 78 | 79 | var wsServer = new WebSocketServer({ 80 | httpServer: server 81 | }); 82 | 83 | var router = new WebSocketRouter(); 84 | router.attachServer(wsServer); 85 | 86 | var mirrorConnections = []; 87 | 88 | var mirrorHistory = []; 89 | 90 | function sendCallback(err) { 91 | if (err) { console.error('send() error: ' + err); } 92 | } 93 | 94 | router.mount('*', 'lws-mirror-protocol', function(request) { 95 | var cookies = [ 96 | { 97 | name: 'TestCookie', 98 | value: 'CookieValue' + Math.floor(Math.random()*1000), 99 | path: '/', 100 | secure: false, 101 | maxage: 5000, 102 | httponly: true 103 | } 104 | ]; 105 | 106 | // Should do origin verification here. You have to pass the accepted 107 | // origin into the accept method of the request. 108 | var connection = request.accept(request.origin, cookies); 109 | console.log((new Date()) + ' lws-mirror-protocol connection accepted from ' + connection.remoteAddress + 110 | ' - Protocol Version ' + connection.webSocketVersion); 111 | 112 | if (mirrorHistory.length > 0) { 113 | var historyString = mirrorHistory.join(''); 114 | console.log((new Date()) + ' sending mirror protocol history to client; ' + connection.remoteAddress + ' : ' + Buffer.byteLength(historyString) + ' bytes'); 115 | 116 | connection.send(historyString, sendCallback); 117 | } 118 | 119 | mirrorConnections.push(connection); 120 | 121 | connection.on('message', function(message) { 122 | // We only care about text messages 123 | if (message.type === 'utf8') { 124 | // Clear canvas command received 125 | if (message.utf8Data === 'clear;') { 126 | mirrorHistory = []; 127 | } 128 | else { 129 | // Record all other commands in the history 130 | mirrorHistory.push(message.utf8Data); 131 | } 132 | 133 | // Re-broadcast the command to all connected clients 134 | mirrorConnections.forEach(function(outputConnection) { 135 | outputConnection.send(message.utf8Data, sendCallback); 136 | }); 137 | } 138 | }); 139 | 140 | connection.on('close', function(closeReason, description) { 141 | var index = mirrorConnections.indexOf(connection); 142 | if (index !== -1) { 143 | console.log((new Date()) + ' lws-mirror-protocol peer ' + connection.remoteAddress + ' disconnected, code: ' + closeReason + '.'); 144 | mirrorConnections.splice(index, 1); 145 | } 146 | }); 147 | 148 | connection.on('error', function(error) { 149 | console.log('Connection error for peer ' + connection.remoteAddress + ': ' + error); 150 | }); 151 | }); 152 | 153 | router.mount('*', 'dumb-increment-protocol', function(request) { 154 | // Should do origin verification here. You have to pass the accepted 155 | // origin into the accept method of the request. 156 | var connection = request.accept(request.origin); 157 | console.log((new Date()) + ' dumb-increment-protocol connection accepted from ' + connection.remoteAddress + 158 | ' - Protocol Version ' + connection.webSocketVersion); 159 | 160 | var number = 0; 161 | connection.timerInterval = setInterval(function() { 162 | connection.send((number++).toString(10), sendCallback); 163 | }, 50); 164 | connection.on('close', function() { 165 | clearInterval(connection.timerInterval); 166 | }); 167 | connection.on('message', function(message) { 168 | if (message.type === 'utf8') { 169 | if (message.utf8Data === 'reset\n') { 170 | console.log((new Date()) + ' increment reset received'); 171 | number = 0; 172 | } 173 | } 174 | }); 175 | connection.on('close', function(closeReason, description) { 176 | console.log((new Date()) + ' dumb-increment-protocol peer ' + connection.remoteAddress + ' disconnected, code: ' + closeReason + '.'); 177 | }); 178 | }); 179 | 180 | console.log('WebSocket-Node: Test Server implementing Andy Green\'s'); 181 | console.log('libwebsockets-test-server protocols.'); 182 | console.log('Point your WebSocket Protocol Version 8 complant browser to http://localhost:' + args.port + '/'); 183 | -------------------------------------------------------------------------------- /hyco-websocket/test/scripts/libwebsockets-test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Minimal Websocket test app 7 | 8 | 9 | 10 |

libwebsockets "dumb-increment-protocol" test applet

11 | The incrementing number is coming from the server and is individual for 12 | each connection to the server... try opening a second browser window. 13 | Click the button to send the server a websocket message to 14 | reset the number.

15 | 16 | 17 | 18 | 22 | 23 | 24 | 25 |
19 | 20 | 21 |
Not initialized
26 | 27 |

libwebsockets "lws-mirror-protocol" test applet

28 | Use the mouse to draw on the canvas below -- all other browser windows open 29 | on this page see your drawing in realtime and you can see any of theirs as 30 | well. 31 |

32 | The lws-mirror protocol doesn't interpret what is being sent to it, it just 33 | re-sends it to every other websocket it has a connection with using that 34 | protocol, including the guy who sent the packet. 35 |

libwebsockets-test-client spams circles on to this shared canvas when 36 | run.

37 |

38 | 39 | 40 | 41 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 |
Drawing color: 42 | 48 |
Not initialized
58 | 59 | 252 | 253 | 254 | 255 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Microsoft Azure Relay 3 |

4 | 5 | # Azure Relay Hybrid Connections Clients for Node.JS 6 | 7 | ![build status](https://ci.appveyor.com/api/projects/status/github/Azure/azure-relay-node?branch=master&svg=true) 8 | 9 | |Package|Status| 10 | |------|-------------| 11 | |hyco-ws|[![npm version](https://badge.fury.io/js/hyco-ws.svg)](https://badge.fury.io/js/hyco-ws)| 12 | |hyco-websocket|[![npm version](https://badge.fury.io/js/hyco-websocket.svg)](https://badge.fury.io/js/hyco-websocket)| 13 | |hyco-https|[![npm version](https://badge.fury.io/js/hyco-https.svg)](https://badge.fury.io/js/hyco-https)| 14 | 15 | This repository contains Node packages and samples for the Hybrid Connections feature of the 16 | Microsoft Azure Relay, a capability pillar of the Azure Service Bus platform. 17 | 18 | Hybrid Connections is a secure, open-protocol evolution of the existing Service Bus Relay 19 | service that has been available in Azure since the beginning and handles millions of connections 20 | daily. 21 | 22 | Hybrid Connections allows establishing bi-directional, binary stream communication between 23 | two networked applications, whereby either or both of the parties can reside behind NATs or 24 | Firewalls. Hybrid Connections is based on HTTP(S) and WebSockets. 25 | 26 | ## How to provide feedback 27 | 28 | See our [Contribution Guidelines](./.github/CONTRIBUTING.md). 29 | 30 | ## Samples 31 | 32 | For Relay Hybrid Connections samples, see the [azure/azure-relay](https://github.com/Azure/azure-relay/tree/master/samples/Hybrid%20Connections) service repository. 33 | 34 | ## How it works 35 | 36 | For Node, the code in the repository allows a **publicly discoverable and reachable** WebSocket 37 | server to be hosted on any machine that has outbound access to the Internet, and 38 | specifically to the Microsoft Azure Relay service in the chosen region, via HTTPS port 443. 39 | 40 | The WebSocket server code will look instantly familiar as it is directly based on and integrated 41 | with two of the most popular existing WebSocket packages in the Node universe: "ws" and "websocket". 42 | 43 | ``` JS 44 | require('ws') ==> require('hyco-ws') 45 | require('websocket') ==> require('hyco-websocket') 46 | ``` 47 | 48 | As you create a WebSocket server using either of the alternate "hyco-ws" and "hyco-websocket" 49 | packages from this repository, the server will not listen on a TCP port on the local network, 50 | but rather delegate listening to a configured Hybrid Connection path the Azure Relay service 51 | in Service Bus. The delegation happens by ways of opening and maintaining a "control connection" 52 | WebSocket that remains opened and reconnects automatically when dropped inadvertently. This 53 | listener connection is automatically TLS/SSL protected without you having to juggle any certificates. 54 | 55 | ### Servers 56 | 57 | The snippet below shows the "ws"/"hyco-ws" variant of creating a server. The API usage is 58 | completely "normal" except for using the "hyco-ws" package and creating an instance of the 59 | *RelayedServer* instead of *Server*. The default underlying *Server* class remains fully available 60 | when using "hyco-ws" instead of "ws", meaning you can host a relayed and a local WebSocket 61 | server side-by-side from within the same application. The "websocket"/"hyco-websocket" 62 | experience is analogous and explained in the package's README. 63 | 64 | ``` JS 65 | var WebSocket = require('hyco-ws'); 66 | 67 | var uri = WebSocket.createRelayListenUri(ns, path); 68 | var wss = WebSocket.RelayedServer( 69 | { 70 | server : uri, 71 | token: function() { return WebSocket.createRelayToken(uri, keyrule, key); } 72 | }); 73 | 74 | wss.on('connection', function (ws) { 75 | console.log('connection accepted'); 76 | ws.onmessage = function (event) { 77 | console.log(JSON.parse(event.data)); 78 | }; 79 | ws.on('close', function () { 80 | console.log('connection closed'); 81 | }); 82 | }); 83 | 84 | wss.on('error', function(err) { 85 | console.log('error: ' + err); 86 | }); 87 | ``` 88 | 89 | Up to 25 WebSocket listeners can listen concurrently on the same Hybrid Connection path on the 90 | Relay; if two or more listeners are connected, the service will automatically balance incoming 91 | connection requests across the connected listeners which also provides an easy failover capability. 92 | You don't have to do anything to enable this, just have multiple listeners share the same path. 93 | 94 | Clients connect to the server through the Relay service on the same path the listener is listening 95 | on. The client uses the regular WebSocket protocol. WebSocket subprotocols and extensions can 96 | be negotiated between client and the Web Socket server end-to-end as you would without the Relay. 97 | 98 | What happens under the covers, as you can find if you poke around in the code of the two packages, is 99 | that any connection that is from a client to the Relay service will be announced to the Listener 100 | with a control message over the open control channel. The control message contains information about 101 | a "rendezvous endpoint" that is valid for a brief period. The server framework will decide whether 102 | to accept the incoming connection, potentially including calling some extensibility hooks, and 103 | then open an outbound WebSocket to the rendezvous endpoint. The client WebSocket and this "data" 104 | WebSocket are then bound into a single end-to-end connection by the Relay service, behaving like 105 | a single WebSocket. 106 | 107 | ### Clients 108 | 109 | If the Relay requires a sender token (which is the default), that token can be included either 110 | as a query parameter ('sb-hc-token') or with the 'ServiceBusAuthorization' HTTP header. The latter is 111 | preferred; mostly since URLs end up in many logs. 112 | 113 | ``` JS 114 | var WebSocket = require('hyco-ws'); 115 | 116 | var opt = { headers : { 'ServiceBusAuthorization' : token}}; 117 | var address = WebSocket.createRelaySendUri(ns, path), 118 | 119 | var client = new WebSocket(address, null, opt); 120 | client.on('open', function() { 121 | client.send("Hi!"); 122 | }); 123 | 124 | ``` 125 | 126 | The standard WebSocket client that is built into current browsers doesn't support setting 127 | the headers for the HTTP handshake, so you'll have to use the query string parameter. The 128 | snippet below is from the modified "serverstats" sample included in this repo that leans 129 | on the similar sample from the "ws" package. The placeholders in the WebSocket URI are 130 | replaced with the correct values for namespace, path, and token using a template engine. 131 | 132 | ``` HTML 133 | 147 | ``` 148 | 149 | ## packages 150 | 151 | The README documents for the two includes packages discuss the particular additions made 152 | to accomodate support for Hybrid Connections. What's common for both libraries is that 153 | you can use the 'hyco-ws' and the 'hyco-websocket' packages instead of the 'ws' and 'websocket' 154 | without losing any existing functionality. Both packages contain and expose the full and unaltered 155 | functionality of their respective base packages. 156 | 157 | * [README for hyco-ws](./hyco-ws/README.md) 158 | * [README for hyco-websocket](./hyco-websocket/README.md) 159 | 160 | ## Examples 161 | 162 | The repo contains local examples at the package level and a few global examples. 163 | [The global samples in /examples](/examples/README.md) use the latest, public, npm-published versions 164 | of the packages and require that you install all dependencies with "npm install". 165 | 166 | The local examples under [hyco-ws](./hyco-ws) and [hyco-websocket](./hyco-websocket) reference the 167 | code in your checked out repo. 168 | 169 | -------------------------------------------------------------------------------- /hyco-https/README.md: -------------------------------------------------------------------------------- 1 | # The 'hyco-https' Package for Azure Relay Hybrid Connections 2 | 3 | ## Overview 4 | 5 | This Node package for Azure Relay Hybrid Connections is built on and extends the core ['https'](https://nodejs.org/api/https.html) Node module. This module re-exports all exports of that base module and adds new exports that enable integration with the Azure Relay service's Hybrid Connections HTTP request feature. 6 | 7 | Existing applications that `require('https')` can use this package instead with `require('hyco-https')`. This allows an application residing anywhere to accept HTTPS requests via a public endpoint. 8 | 9 | ## Documentation 10 | 11 | The API follows the exact patterns of the Node 'http' and ['https'](https://nodejs.org/api/https.html) modules, and this document describes how this package differs from that baseline. 12 | 13 | The module completely overrides the server behavior of the 'https' package, meaning that the same Node application instance cannot concurrently use the regular 'https' module functionality to listen locally for HTTP requests. 14 | 15 | The client functionality of the 'https' package is untouched. 16 | 17 | For application frameworks, such as ExpressJS, that internally override the [`https.ServerResponse`](https://nodejs.org/api/http.html#http_class_http_serverresponse) class, the application should explicitly include the 'http' and 'hyco-https' modules in the following way, and *before* loading the framework, even if the framework commonly does not require a prior explicit reference of 'http' or 'https': 18 | 19 | ```js 20 | var http = require('http'); 21 | var https = require('hyco-https'); 22 | http.ServerResponse = https.ServerResponse; 23 | 24 | var express = require('express'); 25 | ``` 26 | 27 | 28 | ### Package Helper methods 29 | 30 | There are several utility methods available on the package export that can be 31 | referenced like this: 32 | 33 | ``` JavaScript 34 | const https = require('hyco-https'); 35 | 36 | var listenUri = https.createRelayListenUri('namespace.servicebus.windows.net', 'path'); 37 | listenUri = https.appendRelayToken(listenUri, 'ruleName', '...key...') 38 | ... 39 | 40 | ``` 41 | 42 | The helper methods are for use with this package, but might be also be used by a Node server 43 | for enabling web or device clients to create listeners or senders by handing them URIs that 44 | already embed short-lived tokens and that can be used with common WebSocket stacks that do 45 | not support setting HTTP headers for the WebSocket handshake. Embedding authorization tokens 46 | into the URI is primarily supported for those library-external usage scenarios. 47 | 48 | 49 | #### createRelayListenUri 50 | ``` JavaScript 51 | var uri = createRelayListenUri([namespaceName], [path], [[token]], [[id]]) 52 | ``` 53 | 54 | Creates a valid Azure Relay Hybrid Connection listener URI for the given namespace and path. This 55 | URI can then be used with the createRelayedServer function. 56 | 57 | - **namespaceName** (required) - the domain-qualified name of the Azure Relay namespace to use 58 | - **path** (required) - the name of an existing Azure Relay Hybrid Connection in that namespace 59 | - **token** (optional) - a previously issued Relay access token that shall be embedded in 60 | the listener URI (see below) 61 | - **id** (optional) - a tracking identifier that allows end-to-end diagnostics tracking of requests 62 | 63 | The **token** value is optional and should only be used when it is not possible to send HTTP 64 | headers along with the WebSocket handshake as it is the case with the W3C WebSocket stack. 65 | 66 | 67 | #### createRelayHttpsUri 68 | ``` JavaScript 69 | var uri = createRelayHttpsUri([namespaceName], [path], [[token]], [[id]]) 70 | ``` 71 | 72 | Creates a valid Azure Relay Hybrid Connection HTTPS URI for the given namespace and path. This 73 | URI can be used with any HTTPS client. 74 | 75 | - **namespaceName** (required) - the domain-qualified name of the Azure Relay namespace to use 76 | - **path** (required) - the name of an existing Azure Relay Hybrid Connection in that namespace 77 | - **token** (optional) - a previously issued Relay access token that shall be embedded in 78 | the send URI (see below) 79 | - **id** (optional) - a tracking identifier that allows end-to-end diagnostics tracking of requests 80 | 81 | The **token** value is optional and should only be used when it is not possible to send HTTP 82 | headers along with the WebSocket handshake as it is the case with the W3C WebSocket stack. 83 | 84 | 85 | #### createRelayToken 86 | ``` JavaScript 87 | var token = createRelayToken([uri], [ruleName], [key], [[expirationSeconds]]) 88 | ``` 89 | 90 | Creates an Azure Relay Shared Access Signature (SAS) token for the given target URI, SAS rule, 91 | and SAS rule key that is valid for the given number of seconds or for an hour from the current 92 | instant if the expiry argunent is omitted. 93 | 94 | - **uri** (required) - the URI for which the token is to be issued. The URI will be normalized to 95 | using the http scheme and query string information will be stripped. 96 | - **ruleName** (required) - SAS rule name either for the entity represented by the given URI or 97 | for the namespace represented by teh URI host-portion. 98 | - **key** (required) - valid key for the SAS rule. 99 | - **expirationSeconds** (optional) - the number of seconds until the generated token should expire. 100 | The default is 1 hour (3600) if not specified. 101 | 102 | The issued token will confer the rights associated with the chosen SAS rule for the chosen duration. 103 | 104 | #### appendRelayToken 105 | ``` JavaScript 106 | var uri = appendRelayToken([uri], [ruleName], [key], [[expirationSeconds]]) 107 | ``` 108 | 109 | This method is functionally equivalent to the **createRelayToken** method above, but 110 | returns the token correctly appended to the input URI. 111 | 112 | ### createRelayedServer 113 | 114 | The `createRelayedServer()` method creates a server that does not listen on the local network, but delegates listening to the Azure Relay. Except for the options, it behaves just like the regular [`createServer()`](https://nodejs.org/api/https.html#https_https_createserver_options_requestlistener) function. 115 | 116 | 117 | #### Example 118 | 119 | The [sample](./examples/simple/listener.js) included in this repo illustrates the use. For information on how to create an Hybrid Connection and obtain keys, please read through the [Getting Started](https://docs.microsoft.com/azure/service-bus-relay/relay-hybrid-connections-node-get-started) document. 120 | 121 | If you are familiar with the regular 'https' module, you will find the code below just as familiar. Request and response and error handling is identical. 122 | 123 | ``` js 124 | 125 | const https = require('hyco-https'); 126 | 127 | var args = { 128 | ns : process.env.SB_HC_NAMESPACE, // fully qualified relay namespace 129 | path : process.env.SB_HC_PATH, // path of the Hybrid Connection 130 | keyrule : process.env.SB_HC_KEYRULE, // name of a SAS rule 131 | key : process.env.SB_HC_KEY // key of the SAS rule 132 | }; 133 | 134 | var uri = https.createRelayListenUri(args.ns, args.path); 135 | var server = https.createRelayedServer( 136 | { 137 | server : uri, 138 | token : () => https.createRelayToken(uri, args.keyrule, args.key) 139 | }, 140 | (req, res) => { 141 | console.log('request accepted: ' + req.method + ' on ' + req.url); 142 | res.setHeader('Content-Type', 'text/html'); 143 | res.end('Hey!Relayed Node.js Server!'); 144 | }); 145 | 146 | server.listen( (err) => { 147 | if (err) { 148 | return console.log('something bad happened', err) 149 | } 150 | console.log(`server is listening`) 151 | }); 152 | 153 | server.on('error', (err) => { 154 | console.log('error: ' + err); 155 | }); 156 | ``` 157 | 158 | The `options` element supports a different set of arguments than the 159 | `createServer()` since it is neither a standalone listener nor embeddable into an existing HTTP 160 | listener framework. There are also fewer options available since the listener management is 161 | largely delegated to the Relay service. 162 | 163 | Constructor arguments: 164 | 165 | - **server** (required) - the fully qualified URI for a Hybrid Connection name on which to listen, usually 166 | constructed with the https.createRelayListenUri() helper. 167 | - **token** (required) - this argument *either* holds a previously issued token string *or* a callback 168 | function that can be called to obtain such a token string. The callback option 169 | is preferred as it allows token renewal. 170 | -------------------------------------------------------------------------------- /hyco-https/tests/post.test.js: -------------------------------------------------------------------------------- 1 | var https = require('..') 2 | 3 | var ns = process.env.SB_HC_NAMESPACE ? process.env.SB_HC_NAMESPACE.replace(/^"(.*)"$/, '$1') : null; 4 | var path = process.env.SB_HC_PATH ? process.env.SB_HC_PATH : "a2"; 5 | var keyrule = process.env.SB_HC_KEYRULE ? process.env.SB_HC_KEYRULE.replace(/^"(.*)"$/, '$1') : null; 6 | var key = process.env.SB_HC_KEY ? process.env.SB_HC_KEY.replace(/^"(.*)"$/, '$1') : null; 7 | 8 | expect(ns).toBeDefined(); 9 | expect(path).toBeDefined(); 10 | expect(keyrule).toBeDefined(); 11 | expect(key).toBeDefined(); 12 | 13 | var smallMessage = "SmallMessage"; 14 | var exactly64kbMessage = ""; 15 | for (var i = 1024 * 64; i > 0; i--) { 16 | exactly64kbMessage += String.fromCharCode(i % 128); 17 | } 18 | var over64kbMessage = exactly64kbMessage + String.fromCharCode(0); 19 | 20 | // reqWriteMsgs, reqEndMsg, resWriteMsgs, resEndMsg should be string[], string, or null 21 | function sendAndReceive(reqWriteMsgs, reqEndMsg, resWriteMsgs, resEndMsg, done) { 22 | var reqExpected = ""; 23 | var resExpected = ""; 24 | 25 | if (reqWriteMsgs) { 26 | reqExpected = Array.isArray(reqWriteMsgs) ? reqWriteMsgs.reduce((total, current) => { return total + current; }) : reqWriteMsgs; 27 | } 28 | reqExpected += (reqEndMsg) ? reqEndMsg : ""; 29 | 30 | if (resWriteMsgs) { 31 | resExpected = Array.isArray(resWriteMsgs) ? resWriteMsgs.reduce((total, current) => { return total + current; }) : resWriteMsgs; 32 | } 33 | resExpected += (resEndMsg) ? resEndMsg : ""; 34 | 35 | jest.setTimeout(5000); // 5 seconds timeout per test 36 | 37 | /* set up the listener */ 38 | var uri = https.createRelayListenUri(ns, path); 39 | var server = https.createRelayedServer({ 40 | server: uri, 41 | token: () => https.createRelayToken(uri, keyrule, key) 42 | }, 43 | (req, res) => { 44 | expect(req.method).toBe("POST"); 45 | expect(req.headers.custom).toBe("Hello"); 46 | req.setEncoding('utf-8'); 47 | req.on('data', (chunk) => { 48 | expect(chunk.length).toBe(Buffer.byteLength(reqExpected)); 49 | expect(chunk).toBe(reqExpected); 50 | }); 51 | req.on('end', () => { 52 | if (resWriteMsgs) { 53 | if (Array.isArray(resWriteMsgs)) { 54 | resWriteMsgs.forEach((msg) => { 55 | res.write(msg); 56 | }); 57 | } else { 58 | res.write(resWriteMsgs); 59 | } 60 | } 61 | 62 | if (resEndMsg && resEndMsg.length) { 63 | res.end(resEndMsg); 64 | } else { 65 | res.end(); 66 | } 67 | }); 68 | }); 69 | 70 | // fail we get an error 71 | server.listen((err) => { 72 | expect(err).toBeUndefined(); 73 | }); 74 | // fail if we get an error (we'll always get one if this triggers) 75 | server.on('error', (err) => { 76 | expect(err).toBeUndefined(); 77 | }); 78 | 79 | /* set up the client */ 80 | var clientUri = https.createRelayHttpsUri(ns, path); 81 | var token = https.createRelayToken(clientUri, keyrule, key); 82 | 83 | server.on('listening', () => { 84 | var req = https.request({ 85 | hostname: ns, 86 | path: ((!path || path.length == 0 || path[0] !== '/') ? '/' : '') + path, 87 | port: 443, 88 | method : "POST", 89 | headers: { 90 | 'ServiceBusAuthorization': token, 91 | 'Custom' : 'Hello', 92 | 'Content-Type': 'text/plain', 93 | 'Content-Length': Buffer.byteLength(reqExpected) 94 | } 95 | }, (res) => { 96 | var chunks = ''; 97 | expect(res.statusCode).toBe(200); 98 | res.setEncoding('utf8'); 99 | res.on('data', (chunk) => { 100 | chunks += chunk; 101 | }); 102 | res.on('end', () => { 103 | expect(chunks.length).toBe(Buffer.byteLength(resExpected)); 104 | expect(chunks).toBe(resExpected); 105 | server.close(); 106 | jest.clearAllTimers(); 107 | done(); 108 | }); 109 | }).on('error', (e) => { 110 | expect(e).toBeUndefined(); 111 | }); 112 | 113 | if (reqWriteMsgs) { 114 | if (Array.isArray(reqWriteMsgs)) { 115 | reqWriteMsgs.forEach((msg) => { 116 | req.write(msg); 117 | }); 118 | } else { 119 | req.write(reqWriteMsgs); 120 | } 121 | } 122 | 123 | if (reqEndMsg && reqEndMsg.length) { 124 | req.end(reqEndMsg); 125 | } else { 126 | req.end(); 127 | } 128 | }); 129 | } 130 | 131 | var testMessages = { 132 | // testName : testMessage 133 | "SmallMessage" : smallMessage, 134 | "Exactly64kbMessage" : exactly64kbMessage, 135 | "Over64kbMessage" : over64kbMessage 136 | } 137 | 138 | describe('HttpPostEmptyReqEmptyResTest', () => { 139 | test('HttpPostEmptyReqEmptyRes', (done) => { 140 | sendAndReceive(null, "", null, "", done); 141 | }); 142 | }); 143 | 144 | describe('HttpPostRequestTests', () => { 145 | describe('EndOnly', () => { 146 | Object.keys(testMessages).forEach((testName) => { 147 | test('HttpPostRequestEndOnly' + testName, (done) => { 148 | sendAndReceive(null, testMessages[testName], null, "ResponseMessage", done); 149 | }); 150 | }); 151 | }); 152 | describe('WriteOnly', () => { 153 | Object.keys(testMessages).forEach((testName) => { 154 | test('HttpPostRequestWriteOnly' + testName, (done) => { 155 | sendAndReceive(testMessages[testName], null, "ResponseMessage", null, done); 156 | }); 157 | }); 158 | }); 159 | describe('WriteAndEnd', () => { 160 | test('HttpPostRequestSmallWriteSmallEnd', (done) => { 161 | sendAndReceive(smallMessage, smallMessage, "ResponseMessage", null, done); 162 | }); 163 | test('HttpPostRequestSmallWrite64kbEnd', (done) => { 164 | sendAndReceive(smallMessage, exactly64kbMessage, "ResponseMessage", null, done); 165 | }); 166 | test('HttpPostRequestLargeWriteSmallEnd', (done) => { 167 | sendAndReceive(over64kbMessage, smallMessage, "ResponseMessage", null, done); 168 | }); 169 | }); 170 | describe('MultipleWrites', () => { 171 | test('HttpPostRequestSmallWrites', (done) => { 172 | sendAndReceive([smallMessage, smallMessage], null, "ResponseMessage", null, done); 173 | }); 174 | test('HttpPostRequestExceed64kbWrites', (done) => { 175 | sendAndReceive([smallMessage, exactly64kbMessage], null, "ResponseMessage", null, done); 176 | }); 177 | test('HttpPostRequestLargeThenSmallWrites', (done) => { 178 | sendAndReceive([over64kbMessage, smallMessage], null, "ResponseMessage", null, done); 179 | }); 180 | }); 181 | }); 182 | 183 | describe('HttpPostResponseTests', () => { 184 | describe('EndOnly', () => { 185 | Object.keys(testMessages).forEach((testName) => { 186 | test('HttpPostResponseEndOnly' + testName, (done) => { 187 | sendAndReceive(null, "RequestMessage", null, testMessages[testName], done); 188 | }); 189 | }); 190 | }); 191 | describe('WriteOnly', () => { 192 | Object.keys(testMessages).forEach((testName) => { 193 | test('HttpPostResponseWriteOnly' + testName, (done) => { 194 | sendAndReceive("RequestMessage", null, testMessages[testName], null, done); 195 | }); 196 | }); 197 | }); 198 | describe('WriteAndEnd', () => { 199 | test('HttpPostResponseSmallWriteSmallEnd', (done) => { 200 | sendAndReceive("RequestMessage", null, smallMessage, smallMessage, done); 201 | }); 202 | test('HttpPostResponseSmallWrite64kbEnd', (done) => { 203 | sendAndReceive("RequestMessage", null, smallMessage, exactly64kbMessage, done); 204 | }); 205 | test('HttpPostResponseLargeWriteSmallEnd', (done) => { 206 | sendAndReceive("RequestMessage", null, over64kbMessage, smallMessage, done); 207 | }); 208 | }); 209 | describe('MultipleWrites', () => { 210 | test('HttpPostResponseSmallWrites', (done) => { 211 | sendAndReceive("RequestMessage", null, [smallMessage, smallMessage], null, done); 212 | }); 213 | test('HttpPostResponseExceed64kbWrites', (done) => { 214 | sendAndReceive("RequestMessage", null, [smallMessage, exactly64kbMessage], null, done); 215 | }); 216 | test('HttpPostResponseLargeThenSmallWrites', (done) => { 217 | sendAndReceive("RequestMessage", null, [over64kbMessage, smallMessage], null, done); 218 | }); 219 | }); 220 | }); -------------------------------------------------------------------------------- /hyco-ws/README.md: -------------------------------------------------------------------------------- 1 | # The 'hyco-ws' Package for Azure Relay Hybrid Connections 2 | 3 | ## Overview 4 | 5 | This Node package for Azure Relay Hybrid Connections is built on and extends the 6 | ['ws'](https://www.npmjs.com/package/ws) NPM package. This package 7 | re-exports all exports of that base package and adds new exports that enable 8 | integration with the Azure Relay service's Hybrid Connections feature. 9 | 10 | Existing applications that `require('ws')` can use this package instead 11 | with `require('hyco-ws')` , which also enables hybrid scenarios where an 12 | application can listen for WebSocket connections locally from "inside the firewall" 13 | and via Relay Hybrid Connections all at the same time. 14 | 15 | ## Documentation 16 | 17 | The API is [generally documented in the main 'ws' package](https://github.com/websockets/ws/blob/master/doc/ws.md) 18 | and this document describes how this package differs from that baseline. 19 | 20 | The key differences between the base package and this 'hyco-ws' is that it adds 21 | a new server class, that is exported via `require('hyco-ws').RelayedServer`, 22 | and a few helper methods. 23 | 24 | ### Package Helper methods 25 | 26 | There are several utility methods available on the package export that can be 27 | referenced like this: 28 | 29 | ``` JavaScript 30 | const WebSocket = require('hyco-ws'); 31 | 32 | var listenUri = WebSocket.createRelayListenUri('namespace.servicebus.windows.net', 'path'); 33 | listenUri = WebSocket.appendRelayToken(listenUri, 'ruleName', '...key...') 34 | ... 35 | 36 | ``` 37 | 38 | The helper methods are for use with this package, but might be also be used by a Node server 39 | for enabling web or device clients to create listeners or senders by handing them URIs that 40 | already embed short-lived tokens and that can be used with common WebSocket stacks that do 41 | not support setting HTTP headers for the WebSocket handshake. Embedding authorization tokens 42 | into the URI is primarily supported for those library-external usage scenarios. 43 | 44 | #### createRelayListenUri 45 | ``` JavaScript 46 | var uri = createRelayListenUri([namespaceName], [path], [[token]], [[id]]) 47 | ``` 48 | 49 | Creates a valid Azure Relay Hybrid Connection listener URI for the given namespace and path. This 50 | URI can then be used with the relayed version of the WebSocketServer class. 51 | 52 | - **namespaceName** (required) - the domain-qualified name of the Azure Relay namespace to use 53 | - **path** (required) - the name of an existing Azure Relay Hybrid Connection in that namespace 54 | - **token** (optional) - a previously issued Relay access token that shall be embedded in 55 | the listener URI (see below) 56 | - **id** (optional) - a tracking identifier that allows end-to-end diagnostics tracking of requests 57 | 58 | The **token** value is optional and should only be used when it is not possible to send HTTP 59 | headers along with the WebSocket handshake as it is the case with the W3C WebSocket stack. 60 | 61 | 62 | #### createRelaySendUri 63 | ``` JavaScript 64 | var uri = createRelaySendUri([namespaceName], [path], [[token]], [[id]]) 65 | ``` 66 | 67 | Creates a valid Azure Relay Hybrid Connection send URI for the given namespace and path. This 68 | URI can be used with any WebSocket client. 69 | 70 | - **namespaceName** (required) - the domain-qualified name of the Azure Relay namespace to use 71 | - **path** (required) - the name of an existing Azure Relay Hybrid Connection in that namespace 72 | - **token** (optional) - a previously issued Relay access token that shall be embedded in 73 | the send URI (see below) 74 | - **id** (optional) - a tracking identifier that allows end-to-end diagnostics tracking of requests 75 | 76 | The **token** value is optional and should only be used when it is not possible to send HTTP 77 | headers along with the WebSocket handshake as it is the case with the W3C WebSocket stack. 78 | 79 | 80 | #### createRelayToken 81 | ``` JavaScript 82 | var token = createRelayToken([uri], [ruleName], [key], [[expirationSeconds]]) 83 | ``` 84 | 85 | Creates an Azure Relay Shared Access Signature (SAS) token for the given target URI, SAS rule, 86 | and SAS rule key that is valid for the given number of seconds or for an hour from the current 87 | instant if the expiry argunent is omitted. 88 | 89 | - **uri** (required) - the URI for which the token is to be issued. The URI will be normalized to 90 | using the http scheme and query string information will be stripped. 91 | - **ruleName** (required) - SAS rule name either for the entity represented by the given URI or 92 | for the namespace represented by teh URI host-portion. 93 | - **key** (required) - valid key for the SAS rule. 94 | - **expirationSeconds** (optional) - the number of seconds until the generated token should expire. 95 | The default is 1 hour (3600) if not specified. 96 | 97 | The issued token will confer the rights associated with the chosen SAS rule for the chosen duration. 98 | 99 | #### appendRelayToken 100 | ``` JavaScript 101 | var uri = appendRelayToken([uri], [ruleName], [key], [[expirationSeconds]]) 102 | ``` 103 | 104 | This method is functionally equivalent to the **createRelayToken** method above, but 105 | returns the token correctly appended to the input URI. 106 | 107 | ### Class ws.RelayedServer 108 | 109 | The `hycows.RelayedServer` class is an alternative to the `ws.Server` 110 | class that does not listen on the local network, but delegates listening to the Azure Relay. 111 | 112 | The two classes are largely contract compatible, meaning that an existing application using 113 | the `ws.Server` class can be changed to use the relayed version quite easily. The 114 | main differences in the constructor and the available options. 115 | 116 | #### Constructor 117 | 118 | ``` JavaScript 119 | var ws = require('hyco-ws'); 120 | var server = ws.RelayedServer; 121 | 122 | var wss = new server( 123 | { 124 | server : ws.createRelayListenUri(ns, path), 125 | token: function() { return ws.createRelayToken('http://' + ns, keyrule, key); } 126 | }); 127 | ``` 128 | 129 | The `RelayedServer` constructor supports a different set of arguments than the 130 | `Server` since it is neither a standalone listener nor embeddable into an existing HTTP 131 | listener framework. There are also fewer options available since the WebSocket management is 132 | largely delegated to the Relay service. 133 | 134 | Constructor arguments: 135 | 136 | - **server** (required) - the fully qualified URI for a Hybrid Connection name on which to listen, usually 137 | constructed with the WebSocket.createRelayListenUri() helper. 138 | - **token** (required) - this argument *either* holds a previously issued token string *or* a callback 139 | function that can be called to obtain such a token string. The callback option 140 | is preferred as it allows token renewal. 141 | 142 | #### Events 143 | 144 | `RelayedServer` instances emit three Events that allow you to handle incoming requests, establish 145 | connections, and detect error conditions. You must subscribe to the 'connect' event to handle 146 | messages. 147 | 148 | ##### headers 149 | ``` JavaScript 150 | function(headers) 151 | ``` 152 | 153 | The 'headers' event is raised just before an incoming connection is accepted, allowing 154 | for modification of the headers to send to the client. 155 | 156 | ##### connection 157 | ``` JavaScript 158 | function(socket) 159 | ``` 160 | 161 | Emitted whenever a new WebSocket connection is accepted. The object is of type ws.WebSocket 162 | just as with the base package. 163 | 164 | 165 | ##### error 166 | ``` JavaScript 167 | function(error) 168 | ``` 169 | 170 | If the underlying server emits an error, it will be forwarded here. 171 | 172 | #### Helpers 173 | 174 | To simplify starting a relayed server and immediately subscribing to incoming connections, 175 | the package exposes a simple helper function, which is also used in the samples: 176 | 177 | ##### createRelayedListener 178 | 179 | ``` JavaScript 180 | var WebSocket = require('hyco-ws'); 181 | 182 | var wss = WebSocket.createRelayedServer( 183 | { 184 | server : WebSocket.createRelayListenUri(ns, path), 185 | token: function() { return WebSocket.createRelayToken('http://' + ns, keyrule, key); } 186 | }, 187 | function (ws) { 188 | console.log('connection accepted'); 189 | ws.onmessage = function (event) { 190 | console.log(JSON.parse(event.data)); 191 | }; 192 | ws.on('close', function () { 193 | console.log('connection closed'); 194 | }); 195 | }); 196 | ``` 197 | 198 | var server = createRelayedServer([options], [connectCallback] ) 199 | 200 | This method is simple syntactic sugar that calls the constructor to create a new 201 | instance of the RelayedServer and then subscribes the provided callback 202 | to the 'connection' event. 203 | 204 | ##### relayedConnect 205 | 206 | Simply mirroring the `createRelayedServer` helper in function, `relayedConnect` 207 | creates a client connection and subscribes to the 'open' event on the 208 | resulting socket. 209 | 210 | ``` JavaScript 211 | var uri = WebSocket.createRelaySendUri(ns, path); 212 | WebSocket.relayedConnect( 213 | uri, 214 | WebSocket.createRelayToken(uri, keyrule, key), 215 | function (socket) { 216 | ... 217 | } 218 | ); 219 | ``` -------------------------------------------------------------------------------- /hyco-ws/lib/HybridConnectionWebSocketServer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const util = require('util'); 4 | const EventEmitter = require('events'); 5 | const http = require('http'); 6 | const crypto = require('crypto'); 7 | const WebSocket = require('ws'); 8 | const url = require('url'); 9 | const moment = require('moment'); 10 | 11 | // slightly awful workaround to pull submodules 12 | var wsc = require.cache[require.resolve('ws')] 13 | const Extensions = wsc.require('./lib/Extensions'); 14 | const PerMessageDeflate = wsc.require('./lib/PerMessageDeflate'); 15 | 16 | var isDefinedAndNonNull = function(options, key) { 17 | return typeof options[key] != 'undefined' && options[key] !== null; 18 | }; 19 | 20 | /** 21 | * WebSocket Server implementation 22 | */ 23 | function HybridConnectionsWebSocketServer(options, callback) { 24 | if (this instanceof HybridConnectionsWebSocketServer === false) { 25 | return new HybridConnectionsWebSocketServer(options, callback); 26 | } 27 | 28 | EventEmitter.call(this); 29 | 30 | options = Object.assign({ 31 | server: null, 32 | token: null, 33 | id: null, 34 | verifyClient: null, 35 | handleProtocols: null, 36 | disableHixie: false, 37 | clientTracking: true, 38 | perMessageDeflate: true, 39 | maxPayload: 100 * 1024 * 1024, 40 | backlog: null // use default (511 as implemented in net.js) 41 | }, options); 42 | 43 | if (!isDefinedAndNonNull(options, 'server')) { 44 | throw new TypeError('\'server\' must be provided'); 45 | } 46 | 47 | if (!isDefinedAndNonNull(options, 'token')) { 48 | throw new TypeError('A \'token\' string or function must be provided'); 49 | } 50 | 51 | var self = this; 52 | 53 | this.listenUri = options.server; 54 | if (isDefinedAndNonNull(options, 'id')) { 55 | this.listenUri = listenUri + '&id=' + options.id; 56 | } 57 | 58 | this.closeRequested = false; 59 | this.options = options; 60 | this.path = options.path; 61 | this.clients = []; 62 | 63 | connectControlChannel(this); 64 | } 65 | 66 | /** 67 | * Inherits from EventEmitter. 68 | */ 69 | 70 | util.inherits(HybridConnectionsWebSocketServer, EventEmitter); 71 | 72 | /** 73 | * Immediately shuts down the connection. 74 | * 75 | * @api public 76 | */ 77 | HybridConnectionsWebSocketServer.prototype.close = function(callback) { 78 | this.closeRequested = true; 79 | // terminate all associated clients 80 | var error = null; 81 | try { 82 | for (var i = 0, l = this.clients.length; i < l; ++i) { 83 | this.clients[i].close(); 84 | } 85 | this.controlChannel.close(); 86 | } 87 | catch (e) { 88 | error = e; 89 | } 90 | 91 | if (callback) { 92 | callback(error); 93 | } else if (error) { 94 | throw error; 95 | } 96 | } 97 | 98 | function connectControlChannel(server) { 99 | /* create the control connection */ 100 | 101 | var opt = null; 102 | var token = null; 103 | var tokenRenewDuration = null; 104 | if (typeof server.options.token === 'function') { 105 | // server.options.token is a function, call it periodically to renew the token 106 | tokenRenewDuration = new moment.duration(1, 'hours'); 107 | token = server.options.token(); 108 | } else { 109 | // server.options.token is a string, the token cannot be renewed automatically 110 | token = server.options.token; 111 | } 112 | 113 | if (token) { 114 | opt = { headers: { 'ServiceBusAuthorization': token } }; 115 | } 116 | 117 | server.controlChannel = new WebSocket(server.listenUri, null, opt); 118 | 119 | // This represents the token renew timer/interval, keep a reference in order to cancel it. 120 | var tokenRenewTimer = null; 121 | 122 | server.controlChannel.onerror = function(event) { 123 | server.emit('error', event); 124 | clearInterval(tokenRenewTimer); 125 | if (!server.closeRequested) { 126 | connectControlChannel(server); 127 | } 128 | } 129 | 130 | server.controlChannel.onopen = function(event) { 131 | server.emit('listening'); 132 | } 133 | 134 | server.controlChannel.onclose = function(event) { 135 | clearInterval(tokenRenewTimer); 136 | 137 | if (!server.closeRequested) { 138 | // reconnect 139 | connectControlChannel(server); 140 | } else { 141 | server.emit('close', server); 142 | } 143 | } 144 | 145 | server.controlChannel.onmessage = function(event) { 146 | var message = JSON.parse(event.data); 147 | if (isDefinedAndNonNull(message, 'accept')) { 148 | accept(server, message); 149 | } 150 | }; 151 | 152 | if (tokenRenewDuration) { 153 | // tokenRenewDuration having a value means server.options.token is a function, renew the token periodically 154 | tokenRenewTimer = setInterval(function() { 155 | if (!server.closeRequested) { 156 | var newToken = server.options.token(); 157 | var renewToken = { 'renewToken' : { 'token' : newToken } }; 158 | server.controlChannel.send( 159 | JSON.stringify(renewToken), 160 | function(error) { 161 | if (error) { 162 | console.log('renewToken error: ' + error); 163 | } 164 | } 165 | ); 166 | } 167 | }, 168 | tokenRenewDuration.asMilliseconds()); 169 | } 170 | } 171 | 172 | function accept(server, message) { 173 | var address = message.accept.address; 174 | var req = { headers: {} }; 175 | var headers = []; 176 | 177 | for (var keys = Object.keys(message.accept.connectHeaders), l = keys.length; l; --l) { 178 | req.headers[keys[l - 1].toLowerCase()] = message.accept.connectHeaders[keys[l - 1]]; 179 | } 180 | // verify key presence 181 | if (!req.headers['sec-websocket-key']) { 182 | abortConnection(message, 400, 'Bad Request'); 183 | return; 184 | } 185 | 186 | // verify version 187 | var version = parseInt(req.headers['sec-websocket-version']); 188 | // verify protocol 189 | var protocols = req.headers['sec-websocket-protocol']; 190 | 191 | // verify client 192 | var origin = version < 13 ? 193 | req.headers['sec-websocket-origin'] : 194 | req.headers['origin']; 195 | 196 | // handle extensions offer 197 | var extensionsOffer = Extensions.parse(req.headers['sec-websocket-extensions']); 198 | 199 | // handler to call when the connection sequence completes 200 | var self = server; 201 | var completeHybiUpgrade2 = function(protocol) { 202 | 203 | var extensions = {}; 204 | try { 205 | extensions = acceptExtensions.call(self, extensionsOffer); 206 | } catch (err) { 207 | abortConnection(message, 400, 'Bad Request'); 208 | return; 209 | } 210 | 211 | if (Object.keys(extensions).length) { 212 | var serverExtensions = {}; 213 | Object.keys(extensions).forEach(function(token) { 214 | serverExtensions[token] = [extensions[token].params] 215 | }); 216 | headers.push('Sec-WebSocket-Extensions: ' + Extensions.format(serverExtensions)); 217 | } 218 | 219 | // allows external modification/inspection of handshake headers 220 | self.emit('headers', headers); 221 | 222 | try { 223 | var client = new WebSocket(address, protocol, { 224 | headers: headers, 225 | perMessageDeflate: false 226 | }); 227 | 228 | client.on('error', function(event) { 229 | var index = server.clients.indexOf(client); 230 | if (index != -1) { 231 | server.clients.splice(index, 1); 232 | } 233 | }); 234 | 235 | server.emit('connection', client); 236 | if (self.options.clientTracking) { 237 | self.clients.push(client); 238 | client.on('close', function() { 239 | var index = self.clients.indexOf(client); 240 | if (index != -1) { 241 | self.clients.splice(index, 1); 242 | } 243 | }); 244 | } 245 | } catch (err) { 246 | console.log(err); 247 | } 248 | } 249 | 250 | // optionally call external protocol selection handler before 251 | // calling completeHybiUpgrade2 252 | var completeHybiUpgrade1 = function() { 253 | // choose from the sub-protocols 254 | if (typeof self.options.handleProtocols == 'function') { 255 | var protList = (protocols || '').split(/, */); 256 | var callbackCalled = false; 257 | self.options.handleProtocols(protList, function(result, protocol) { 258 | callbackCalled = true; 259 | if (!result) abortConnection(socket, 401, 'Unauthorized'); 260 | else completeHybiUpgrade2(protocol); 261 | }); 262 | if (!callbackCalled) { 263 | // the handleProtocols handler never called our callback 264 | abortConnection(socket, 501, 'Could not process protocols'); 265 | } 266 | return; 267 | } else { 268 | if (typeof protocols !== 'undefined') { 269 | completeHybiUpgrade2(protocols.split(/, */)[0]); 270 | } 271 | else { 272 | completeHybiUpgrade2(); 273 | } 274 | } 275 | } 276 | 277 | completeHybiUpgrade1(); 278 | } 279 | 280 | function acceptExtensions(offer) { 281 | var extensions = {}; 282 | var options = this.options.perMessageDeflate; 283 | var maxPayload = this.options.maxPayload; 284 | if (options && offer[PerMessageDeflate.extensionName]) { 285 | var perMessageDeflate = new PerMessageDeflate(options !== true ? options : {}, true, maxPayload); 286 | perMessageDeflate.accept(offer[PerMessageDeflate.extensionName]); 287 | extensions[PerMessageDeflate.extensionName] = perMessageDeflate; 288 | } 289 | return extensions; 290 | } 291 | 292 | function abortConnection(message, status, reason) { 293 | 294 | var client = new WebSocketClient(); 295 | var rejectUri = message.address + '&statusCode=' + status + '&statusDescription=' + encodeURIComponent(reason); 296 | 297 | client.connect(rejectUri, null, null); 298 | client.on('error', function(connection) { 299 | this.emit('requestRejected', this); 300 | }); 301 | } 302 | 303 | module.exports = HybridConnectionsWebSocketServer; -------------------------------------------------------------------------------- /hyco-https/lib/_hyco_incoming.js: -------------------------------------------------------------------------------- 1 | // Copyright Joyent, Inc. and other Node contributors. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a 4 | // copy of this software and associated documentation files (the 5 | // "Software"), to deal in the Software without restriction, including 6 | // without limitation the rights to use, copy, modify, merge, publish, 7 | // distribute, sublicense, and/or sell copies of the Software, and to permit 8 | // persons to whom the Software is furnished to do so, subject to the 9 | // following conditions: 10 | // 11 | // The above copyright notice and this permission notice shall be included 12 | // in all copies or substantial portions of the Software. 13 | // 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 15 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN 17 | // NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 18 | // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 19 | // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE 20 | // USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | 'use strict'; 23 | 24 | const util = require('util'); 25 | const Stream = require('stream'); 26 | 27 | /* Abstract base class for ServerRequest and ClientResponse. */ 28 | function IncomingMessage(relayRequestMessage, relayWebSocket) { 29 | Stream.Readable.call(this); 30 | this.socket = relayWebSocket; 31 | this.connection = relayWebSocket; 32 | 33 | this.httpVersionMajor = 1; 34 | this.httpVersionMinor = 1; 35 | this.httpVersion = "1.1"; 36 | this.complete = false; 37 | this.headers = {}; 38 | this.rawHeaders = []; 39 | this.trailers = {}; 40 | this.rawTrailers = []; 41 | 42 | this.readable = true; 43 | 44 | this.aborted = false; 45 | 46 | this.upgrade = null; 47 | 48 | // request (server) only 49 | this.url = relayRequestMessage.request.requestTarget; 50 | this.method = relayRequestMessage.request.method; 51 | 52 | 53 | this._consuming = false; 54 | // flag for when we decide that this message cannot possibly be 55 | // read by the user, so there's no point continuing to handle it. 56 | this._dumped = false; 57 | 58 | for (var header in relayRequestMessage.request.requestHeaders) { 59 | this._addHeaderLine(header, relayRequestMessage.request.requestHeaders[header], this.headers); 60 | } 61 | } 62 | util.inherits(IncomingMessage, Stream.Readable); 63 | 64 | 65 | IncomingMessage.prototype.setTimeout = function setTimeout(msecs, callback) { 66 | if (callback) 67 | this.on('timeout', callback); 68 | this.socket.setTimeout(msecs); 69 | return this; 70 | }; 71 | 72 | 73 | IncomingMessage.prototype._read = function _read(n) { 74 | 75 | }; 76 | 77 | IncomingMessage.prototype.handleBody = function handleBody(buf) { 78 | this.push(buf); 79 | this.push(null); 80 | }; 81 | 82 | 83 | // It's possible that the socket will be destroyed, and removed from 84 | // any messages, before ever calling this. In that case, just skip 85 | // it, since something else is destroying this connection anyway. 86 | IncomingMessage.prototype.destroy = function destroy(error) { 87 | }; 88 | 89 | 90 | // This function is used to help avoid the lowercasing of a field name if it 91 | // matches a 'traditional cased' version of a field name. It then returns the 92 | // lowercased name to both avoid calling toLowerCase() a second time and to 93 | // indicate whether the field was a 'no duplicates' field. If a field is not a 94 | // 'no duplicates' field, a `0` byte is prepended as a flag. The one exception 95 | // to this is the Set-Cookie header which is indicated by a `1` byte flag, since 96 | // it is an 'array' field and thus is treated differently in _addHeaderLines(). 97 | // TODO: perhaps http_parser could be returning both raw and lowercased versions 98 | // of known header names to avoid us having to call toLowerCase() for those 99 | // headers. 100 | 101 | // 'array' header list is taken from: 102 | // https://mxr.mozilla.org/mozilla/source/netwerk/protocol/http/src/nsHttpHeaderArray.cpp 103 | function matchKnownFields(field) { 104 | var low = false; 105 | while (true) { 106 | switch (field) { 107 | case 'Content-Type': 108 | case 'content-type': 109 | return 'content-type'; 110 | case 'Content-Length': 111 | case 'content-length': 112 | return 'content-length'; 113 | case 'User-Agent': 114 | case 'user-agent': 115 | return 'user-agent'; 116 | case 'Referer': 117 | case 'referer': 118 | return 'referer'; 119 | case 'Host': 120 | case 'host': 121 | return 'host'; 122 | case 'Authorization': 123 | case 'authorization': 124 | return 'authorization'; 125 | case 'Proxy-Authorization': 126 | case 'proxy-authorization': 127 | return 'proxy-authorization'; 128 | case 'If-Modified-Since': 129 | case 'if-modified-since': 130 | return 'if-modified-since'; 131 | case 'If-Unmodified-Since': 132 | case 'if-unmodified-since': 133 | return 'if-unmodified-since'; 134 | case 'From': 135 | case 'from': 136 | return 'from'; 137 | case 'Location': 138 | case 'location': 139 | return 'location'; 140 | case 'Max-Forwards': 141 | case 'max-forwards': 142 | return 'max-forwards'; 143 | case 'Retry-After': 144 | case 'retry-after': 145 | return 'retry-after'; 146 | case 'ETag': 147 | case 'etag': 148 | return 'etag'; 149 | case 'Last-Modified': 150 | case 'last-modified': 151 | return 'last-modified'; 152 | case 'Server': 153 | case 'server': 154 | return 'server'; 155 | case 'Age': 156 | case 'age': 157 | return 'age'; 158 | case 'Expires': 159 | case 'expires': 160 | return 'expires'; 161 | case 'Set-Cookie': 162 | case 'set-cookie': 163 | return '\u0001'; 164 | case 'Cookie': 165 | case 'cookie': 166 | return '\u0002cookie'; 167 | // The fields below are not used in _addHeaderLine(), but they are common 168 | // headers where we can avoid toLowerCase() if the mixed or lower case 169 | // versions match the first time through. 170 | case 'Transfer-Encoding': 171 | case 'transfer-encoding': 172 | return '\u0000transfer-encoding'; 173 | case 'Date': 174 | case 'date': 175 | return '\u0000date'; 176 | case 'Connection': 177 | case 'connection': 178 | return '\u0000connection'; 179 | case 'Cache-Control': 180 | case 'cache-control': 181 | return '\u0000cache-control'; 182 | case 'Vary': 183 | case 'vary': 184 | return '\u0000vary'; 185 | case 'Content-Encoding': 186 | case 'content-encoding': 187 | return '\u0000content-encoding'; 188 | case 'Origin': 189 | case 'origin': 190 | return '\u0000origin'; 191 | case 'Upgrade': 192 | case 'upgrade': 193 | return '\u0000upgrade'; 194 | case 'Expect': 195 | case 'expect': 196 | return '\u0000expect'; 197 | case 'If-Match': 198 | case 'if-match': 199 | return '\u0000if-match'; 200 | case 'If-None-Match': 201 | case 'if-none-match': 202 | return '\u0000if-none-match'; 203 | case 'Accept': 204 | case 'accept': 205 | return '\u0000accept'; 206 | case 'Accept-Encoding': 207 | case 'accept-encoding': 208 | return '\u0000accept-encoding'; 209 | case 'Accept-Language': 210 | case 'accept-language': 211 | return '\u0000accept-language'; 212 | case 'X-Forwarded-For': 213 | case 'x-forwarded-for': 214 | return '\u0000x-forwarded-for'; 215 | case 'X-Forwarded-Host': 216 | case 'x-forwarded-host': 217 | return '\u0000x-forwarded-host'; 218 | case 'X-Forwarded-Proto': 219 | case 'x-forwarded-proto': 220 | return '\u0000x-forwarded-proto'; 221 | default: 222 | if (low) 223 | return '\u0000' + field; 224 | field = field.toLowerCase(); 225 | low = true; 226 | } 227 | } 228 | } 229 | // Add the given (field, value) pair to the message 230 | // 231 | // Per RFC2616, section 4.2 it is acceptable to join multiple instances of the 232 | // same header with a ', ' if the header in question supports specification of 233 | // multiple values this way. The one exception to this is the Cookie header, 234 | // which has multiple values joined with a '; ' instead. If a header's values 235 | // cannot be joined in either of these ways, we declare the first instance the 236 | // winner and drop the second. Extended header fields (those beginning with 237 | // 'x-') are always joined. 238 | IncomingMessage.prototype._addHeaderLine = _addHeaderLine; 239 | function _addHeaderLine(field, value, dest) { 240 | field = matchKnownFields(field); 241 | var flag = field.charCodeAt(0); 242 | if (flag === 0 || flag === 2) { 243 | field = field.slice(1); 244 | // Make a delimited list 245 | if (typeof dest[field] === 'string') { 246 | dest[field] += (flag === 0 ? ', ' : '; ') + value; 247 | } else { 248 | dest[field] = value; 249 | } 250 | } else if (flag === 1) { 251 | // Array header -- only Set-Cookie at the moment 252 | if (dest['set-cookie'] !== undefined) { 253 | dest['set-cookie'].push(value); 254 | } else { 255 | dest['set-cookie'] = [value]; 256 | } 257 | } else if (dest[field] === undefined) { 258 | // Drop duplicates 259 | dest[field] = value; 260 | } 261 | } 262 | 263 | 264 | // Call this instead of resume() if we want to just 265 | // dump all the data to /dev/null 266 | IncomingMessage.prototype._dump = function _dump() { 267 | if (!this._dumped) { 268 | this._dumped = true; 269 | // If there is buffered data, it may trigger 'data' events. 270 | // Remove 'data' event listeners explicitly. 271 | this.removeAllListeners('data'); 272 | this.resume(); 273 | } 274 | }; 275 | 276 | module.exports = { 277 | IncomingMessage 278 | }; 279 | -------------------------------------------------------------------------------- /hyco-websocket/lib/HybridConnectionsWebSocketRequest.js: -------------------------------------------------------------------------------- 1 | /************************************************************************ 2 | * Copyright 2010-2015 Brian McKelvey. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | ***********************************************************************/ 16 | 17 | var crypto = require('crypto'); 18 | var util = require('util'); 19 | var url = require('url'); 20 | var EventEmitter = require('events').EventEmitter; 21 | var WebSocketClient = require('websocket').client; 22 | var WebSocketConnection = require('websocket').connection; 23 | 24 | var headerValueSplitRegExp = /,\s*/; 25 | var headerParamSplitRegExp = /;\s*/; 26 | var headerSanitizeRegExp = /[\r\n]/g; 27 | var xForwardedForSeparatorRegExp = /,\s*/; 28 | var separators = [ 29 | '(', ')', '<', '>', '@', 30 | ',', ';', ':', '\\', '\"', 31 | '/', '[', ']', '?', '=', 32 | '{', '}', ' ', String.fromCharCode(9) 33 | ]; 34 | 35 | var cookieSeparatorRegEx = /[;,] */; 36 | 37 | function HybridConnectionsWebSocketRequest(address, id, httpHeaders, serverConfig) { 38 | // Superclass Constructor 39 | EventEmitter.call(this); 40 | 41 | this.resource = address; 42 | this.address = address; 43 | this.id = id; 44 | this.httpRequest = { 45 | headers : {} 46 | }; 47 | 48 | for (var keys = Object.keys(httpHeaders), l = keys.length; l; --l) { 49 | this.httpRequest.headers[ keys[l - 1].toLowerCase() ] = httpHeaders[ keys[l - 1] ]; 50 | } 51 | 52 | this.serverConfig = serverConfig; 53 | this._resolved = false; 54 | } 55 | 56 | util.inherits(HybridConnectionsWebSocketRequest, EventEmitter); 57 | 58 | HybridConnectionsWebSocketRequest.prototype.readHandshake = function() { 59 | var self = this; 60 | var request = this.httpRequest; 61 | 62 | // Decode URL 63 | this.resourceURL = url.parse(this.resource, true); 64 | 65 | this.host = request.headers['host']; 66 | if (!this.host) { 67 | throw new Error('Client must provide a Host header.'); 68 | } 69 | 70 | this.key = request.headers['sec-websocket-key']; 71 | if (!this.key) { 72 | throw new Error('Client must provide a value for Sec-WebSocket-Key.'); 73 | } 74 | 75 | this.webSocketVersion = parseInt(request.headers['sec-websocket-version'], 10); 76 | 77 | if (!this.webSocketVersion || isNaN(this.webSocketVersion)) { 78 | throw new Error('Client must provide a value for Sec-WebSocket-Version.'); 79 | } 80 | 81 | // Protocol is optional. 82 | var protocolString = request.headers['sec-websocket-protocol']; 83 | this.protocolFullCaseMap = {}; 84 | this.requestedProtocols = []; 85 | if (protocolString) { 86 | var requestedProtocolsFullCase = protocolString.split(headerValueSplitRegExp); 87 | requestedProtocolsFullCase.forEach(function(protocol) { 88 | var lcProtocol = protocol.toLocaleLowerCase(); 89 | self.requestedProtocols.push(lcProtocol); 90 | self.protocolFullCaseMap[lcProtocol] = protocol; 91 | }); 92 | } 93 | 94 | if (!this.serverConfig.ignoreXForwardedFor && 95 | request.headers['x-forwarded-for']) { 96 | var immediatePeerIP = this.remoteAddress; 97 | this.remoteAddresses = request.headers['x-forwarded-for'] 98 | .split(xForwardedForSeparatorRegExp); 99 | this.remoteAddresses.push(immediatePeerIP); 100 | this.remoteAddress = this.remoteAddresses[0]; 101 | } 102 | 103 | // Extensions are optional. 104 | var extensionsString = request.headers['sec-websocket-extensions']; 105 | this.requestedExtensions = this.parseExtensions(extensionsString); 106 | 107 | // Cookies are optional 108 | var cookieString = request.headers['cookie']; 109 | this.cookies = this.parseCookies(cookieString); 110 | }; 111 | 112 | HybridConnectionsWebSocketRequest.prototype.parseExtensions = function(extensionsString) { 113 | if (!extensionsString || extensionsString.length === 0) { 114 | return []; 115 | } 116 | var extensions = extensionsString.toLocaleLowerCase().split(headerValueSplitRegExp); 117 | extensions.forEach(function(extension, index, array) { 118 | var params = extension.split(headerParamSplitRegExp); 119 | var extensionName = params[0]; 120 | var extensionParams = params.slice(1); 121 | extensionParams.forEach(function(rawParam, index, array) { 122 | var arr = rawParam.split('='); 123 | var obj = { 124 | name: arr[0], 125 | value: arr[1] 126 | }; 127 | array.splice(index, 1, obj); 128 | }); 129 | var obj = { 130 | name: extensionName, 131 | params: extensionParams 132 | }; 133 | array.splice(index, 1, obj); 134 | }); 135 | return extensions; 136 | }; 137 | 138 | // This function adapted from node-cookie 139 | // https://github.com/shtylman/node-cookie 140 | HybridConnectionsWebSocketRequest.prototype.parseCookies = function(str) { 141 | // Sanity Check 142 | if (!str || typeof(str) !== 'string') { 143 | return []; 144 | } 145 | 146 | var cookies = []; 147 | var pairs = str.split(cookieSeparatorRegEx); 148 | 149 | pairs.forEach(function(pair) { 150 | var eq_idx = pair.indexOf('='); 151 | if (eq_idx === -1) { 152 | cookies.push({ 153 | name: pair, 154 | value: null 155 | }); 156 | return; 157 | } 158 | 159 | var key = pair.substr(0, eq_idx).trim(); 160 | var val = pair.substr(++eq_idx, pair.length).trim(); 161 | 162 | // quoted values 163 | if ('"' === val[0]) { 164 | val = val.slice(1, -1); 165 | } 166 | 167 | cookies.push({ 168 | name: key, 169 | value: decodeURIComponent(val) 170 | }); 171 | }); 172 | 173 | return cookies; 174 | }; 175 | 176 | HybridConnectionsWebSocketRequest.prototype.accept = function(acceptedProtocol, allowedOrigin, cookies, callback) { 177 | var req = this; 178 | var protocolFullCase = null; 179 | var extraHeaders = {}; 180 | 181 | if (acceptedProtocol) { 182 | protocolFullCase = this.protocolFullCaseMap[acceptedProtocol.toLocaleLowerCase()]; 183 | if (typeof(protocolFullCase) === 'undefined') { 184 | protocolFullCase = acceptedProtocol; 185 | } 186 | } 187 | else { 188 | protocolFullCase = acceptedProtocol; 189 | } 190 | this.protocolFullCaseMap = null; 191 | 192 | if (protocolFullCase) { 193 | // validate protocol 194 | for (var i = 0; i < protocolFullCase.length; i++) { 195 | var charCode = protocolFullCase.charCodeAt(i); 196 | var character = protocolFullCase.charAt(i); 197 | if (charCode < 0x21 || charCode > 0x7E || separators.indexOf(character) !== -1) { 198 | this.reject(500); 199 | throw new Error('Illegal character "' + String.fromCharCode(character) + '" in subprotocol.'); 200 | } 201 | } 202 | if (this.requestedProtocols.indexOf(acceptedProtocol) === -1) { 203 | this.reject(500); 204 | throw new Error('Specified protocol was not requested by the client.'); 205 | } 206 | 207 | protocolFullCase = protocolFullCase.replace(headerSanitizeRegExp, ''); 208 | } 209 | this.requestedProtocols = null; 210 | 211 | // Mark the request resolved now so that the user can't call accept or 212 | // reject a second time. 213 | this._resolved = true; 214 | this.emit('requestResolved', this); 215 | 216 | var client = new WebSocketClient(); 217 | client.connect(this.address, protocolFullCase); 218 | client.on('connect', function(connection) { 219 | req.emit('requestAccepted', connection); 220 | if (callback) { 221 | callback(connection); 222 | } 223 | }); 224 | client.on('error', function(event) { 225 | req.emit('requestRejected', event); 226 | if (callback) { 227 | callback(event); 228 | } 229 | }); 230 | }; 231 | 232 | HybridConnectionsWebSocketRequest.prototype.reject = function(status, reason, extraHeaders, callback) { 233 | var req = this; 234 | var client = new WebSocketClient(); 235 | var rejectUri = this.address + '&statusCode=' + status + '&statusDescription=' + encodeURIComponent(reason); 236 | 237 | client.connect(rejectUri, null, null, extraHeaders); 238 | // we expect this to complete with a 410 Gone 239 | client.on('error', function(event) { 240 | this.emit('requestRejected', event); 241 | callback(event); 242 | }); 243 | }; 244 | 245 | HybridConnectionsWebSocketRequest.prototype._handleSocketCloseBeforeAccept = function() { 246 | this._socketIsClosing = true; 247 | this._removeSocketCloseListeners(); 248 | }; 249 | 250 | HybridConnectionsWebSocketRequest.prototype._removeSocketCloseListeners = function() { 251 | this.socket.removeListener('end', this._socketCloseHandler); 252 | this.socket.removeListener('close', this._socketCloseHandler); 253 | }; 254 | 255 | HybridConnectionsWebSocketRequest.prototype._verifyResolution = function() { 256 | if (this._resolved) { 257 | throw new Error('HybridConnectionsWebSocketRequest may only be accepted or rejected one time.'); 258 | } 259 | }; 260 | 261 | function cleanupFailedConnection(connection) { 262 | // Since we have to return a connection object even if the socket is 263 | // already dead in order not to break the API, we schedule a 'close' 264 | // event on the connection object to occur immediately. 265 | process.nextTick(function() { 266 | // WebSocketConnection.CLOSE_REASON_ABNORMAL = 1006 267 | // Third param: Skip sending the close frame to a dead socket 268 | connection.drop(1006, 'TCP connection lost before handshake completed.', true); 269 | }); 270 | } 271 | 272 | module.exports = HybridConnectionsWebSocketRequest; 273 | -------------------------------------------------------------------------------- /hyco-websocket/README.md: -------------------------------------------------------------------------------- 1 | # The 'hyco-websocket' Package for Azure Relay Hybrid Connections 2 | 3 | ## Overview 4 | 5 | This Node package for Azure Relay Hybrid Connections is built on and extends the 6 | ['websocket'](https://www.npmjs.com/package/websocket) NPM package. This package 7 | re-exports all exports of that base package and adds new exports that enable 8 | integration with the Azure Relay service's Hybrid Connections feature. 9 | 10 | Existing applications that `require('websocket')` can use this package instead 11 | with `require('hyco-websocket')` , which also enables hybrid scenarios where an 12 | application can listen for WebSocket connections locally from "inside the firewall" 13 | and via Relay Hybrid Connections all at the same time. 14 | 15 | ## Documentation 16 | 17 | The API is [generally documented in the main 'websocket' package](https://github.com/theturtle32/WebSocket-Node/blob/master/docs/index.md) 18 | and this document describes how this package differs from that baseline. 19 | 20 | The key differences between the base package and this 'hyco-websocket' is that it adds 21 | a new server class, that is exported via `require('hyco-websocket').relayedServer`, 22 | and a few helper methods. 23 | 24 | ### Package Helper methods 25 | 26 | There are three new utility methods available on the package export that can be 27 | referenced like this: 28 | 29 | ``` JavaScript 30 | const WebSocket = require('hyco-websocket'); 31 | 32 | var listenUri = WebSocket.createRelayListenUri('namespace.servicebus.windows.net', 'path'); 33 | listenUri = WebSocket.appendRelayToken(listenUri, 'ruleName', '...key...') 34 | ... 35 | 36 | ``` 37 | 38 | The helper methods are for use with this package, but might be also be used by a Node server 39 | for enabling web or device clients to create listeners or senders by handing them URIs that 40 | already embed short-lived tokens and that can be used with common WebSocket stacks that do 41 | not support setting HTTP headers for the WebSocket handshake. Embedding authorization tokens 42 | into the URI is primarily supported for those library-external usage scenarios. 43 | 44 | #### createRelayListenUri 45 | 46 | ``` JavaScript 47 | var uri = WebSocket.createRelayListenUri([namespaceName], [path], [[token]], [[id]]) 48 | ``` 49 | 50 | Creates a valid Azure Relay Hybrid Connection listener URI for the given namespace and path. This 51 | URI can then be used with the relayed version of the WebSocketServer class. 52 | 53 | - **namespaceName** (required) - the domain-qualified name of the Azure Relay namespace to use 54 | - **path** (required) - the name of an existing Azure Relay Hybrid Connection in that namespace 55 | - **token** (optional) - a previously issued Relay access token that shall be embedded in 56 | the listener URI (see below) 57 | - **id** (optional) - a tracking identifier that allows end-to-end diagnostics tracking of requests 58 | 59 | The **token** value is optional and should only be used when it is not possible to send HTTP 60 | headers along with the WebSocket handshake as it is the case with the W3C WebSocket stack. 61 | 62 | 63 | #### createRelaySendUri 64 | ``` JavaScript 65 | var uri = WebSocket.createRelaySendUri([namespaceName], [path], [[token]], [[id]]) 66 | ``` 67 | 68 | Creates a valid Azure Relay Hybrid Connection send URI for the given namespace and path. This 69 | URI can be used with any WebSocket client. 70 | 71 | - **namespaceName** (required) - the domain-qualified name of the Azure Relay namespace to use 72 | - **path** (required) - the name of an existing Azure Relay Hybrid Connection in that namespace 73 | - **token** (optional) - a previously issued Relay access token that shall be embedded in 74 | the send URI (see below) 75 | - **id** (optional) - a tracking identifier that allows end-to-end diagnostics tracking of requests 76 | 77 | The **token** value is optional and should only be used when it is not possible to send HTTP 78 | headers along with the WebSocket handshake as it is the case with the W3C WebSocket stack. 79 | 80 | 81 | #### createRelayToken 82 | ``` JavaScript 83 | var token = WebSocket.createRelayToken([uri], [ruleName], [key], [[expirationSeconds]]) 84 | ``` 85 | 86 | Creates an Azure Relay Shared Access Signature (SAS) token for the given target URI, SAS rule, 87 | and SAS rule key that is valid until the given expiration instant (UNIX epoch) or for an 88 | hour from the current instant if the expiry argunent is omitted. 89 | 90 | - **uri** (required) - the URI for which the token is to be issued. The URI will be normalized to 91 | using the http scheme and query string information will be stripped. 92 | - **ruleName** (required) - SAS rule name either for the entity represented by the given URI or 93 | for the namespace represented by the URI host-portion. 94 | - **key** (required) - valid key for the SAS rule. 95 | - **expirationSeconds** (optional) - the number of seconds until the generated token should expire. 96 | The default is 1 hour (3600) if not specified. 97 | 98 | The issued token will confer the rights associated with the chosen SAS rule for the chosen duration. 99 | 100 | #### appendRelayToken 101 | ``` JavaScript 102 | var uri = WebSocket.appendRelayToken([uri], [ruleName], [key], [[expirationSeconds]]) 103 | ``` 104 | 105 | This method is functionally equivalent to the **createRelayToken** method above, but 106 | returns the token correctly appended to the input URI. 107 | 108 | ### HybridConnectionsWebSocketServer 109 | 110 | The `HybridConnectionsWebSocketServer` class is an alternative to the `WebSocketServer` 111 | class that does not listen on the local network, but delegates listening to the Azure Relay. 112 | 113 | The two classes are largely contract compatible, meaning that an existing application using 114 | the `WebSocketServer` class can be changed to use the relayed version quite easily. The 115 | main differences are the constructor and an unfortunately required behavioral change for when 116 | explicit control of accepting incoming WebSockets is required. 117 | 118 | The `HybridConnectionsWebSocketServer` does not support the `mount()` and `unmount()` methods. 119 | The server starts automatically after construction. 120 | 121 | #### Constructor 122 | 123 | ``` JavaScript 124 | var WebSocket = require('hyco-websocket'); 125 | var HybridConnectionsWebSocketServer = WebSocket.relayedServer; 126 | 127 | var wss = new HybridConnectionsWebSocketServer( 128 | { 129 | server : WebSocket.createRelayListenUri(ns, path), 130 | token: function() { return WebSocket.createRelayToken('http://' + ns, keyrule, key); }, 131 | autoAcceptConnections : true 132 | }); 133 | ``` 134 | 135 | The `HybridConnectionsWebSocketServer` constructor supports a different set of arguments than the 136 | `WebSocketServer` since it is neither a standalone listener nor embeddable into an existing HTTP 137 | listener framework. There are also fewer options available since the WebSocket management is 138 | largely delegated to the Relay service. 139 | 140 | Constructor arguments: 141 | 142 | - **server** (required) - the fully qualified URI for a Hybrid Connection name on which to listen, usually 143 | constructed with the WebSocket.createRelayListenUri() helper. 144 | - **token** (required) - this argument *either* holds a previously issued token string *or* a callback 145 | function that can be called to obtain such a token string. The callback option 146 | is preferred as it allows token renewal. 147 | - **autoAcceptConnections** (optional, defaults to *false*) - determines whether connections should be 148 | automatically accepted, independent of the sub-protocol and extensions. 149 | 150 | #### Events 151 | 152 | Just as with the stock WebSocketServer, HybridConnectionsWebSocketServer instances emit three Events 153 | that allow you to handle incoming requests, establish connections, and detect when a connection 154 | has been closed. 155 | 156 | ##### request 157 | ``` JavaScript 158 | function(webSocketRequest) 159 | ``` 160 | 161 | If autoAcceptConnections is set to false, a request event will be emitted by the server whenever 162 | a new WebSocket request is made. You should inspect the requested protocols and the user's origin 163 | to verify the connection, and then accept or reject it by calling `webSocketRequest.accept('chosen-protocol', 'accepted-origin', cb)` or `webSocketRequest.reject(cb)`. 164 | 165 | > **ATTENTION! CHANGE IN BEHAVIOR.** 166 | > The accept() and reject() methods of [WebSocketRequest](https://github.com/theturtle32/WebSocket-Node/blob/master/docs/WebSocketRequest.md) 167 | > in the base library are synchronous. The method accept() immediately returns the `WebSocketConnection`. 168 | > With the Relay, accepting the connection requires a network activity, which means the operation must 169 | > be carried out asynchronously. See details below in `HybridConnectionsWebSocketRequest`. 170 | 171 | ##### connect 172 | ``` JavaScript 173 | function(webSocketConnection) 174 | ``` 175 | 176 | Emitted whenever a new WebSocket connection is accepted. 177 | 178 | ##### close 179 | ``` JavaScript 180 | function(webSocketConnection, closeReason, description) 181 | ``` 182 | 183 | Whenever a connection is closed for any reason, the HybridConnectionsWebSocketServer instance will emit a close event, 184 | passing a reference to the WebSocketConnection instance that was closed. closeReason is the numeric 185 | reason status code for the connection closure, and description is a textual description of the close 186 | reason, if available. 187 | 188 | ### HybridConnectionsWebSocketRequest 189 | 190 | The request object is a variation of the [WebSocketRequest](https://github.com/theturtle32/WebSocket-Node/blob/master/docs/WebSocketRequest.md) 191 | object that is made available through the request event callback on the server object when `autoAcceptConnections` is set to false. 192 | 193 | The object is functionally equivalent and provides the same information properties as the 194 | base object. The signatures of the `accept` and `reject` methods differ: 195 | 196 | #### Methods 197 | 198 | The following two methods differ from the stock request object in being asynchronous: 199 | 200 | ##### accept 201 | ``` JavaScript 202 | accept(acceptedProtocol, allowedOrigin, cookies, callback) 203 | ``` 204 | 205 | Returns: nothing 206 | 207 | After inspecting the HybridConnectionsWebSocketRequest's properties, call this function on the request object to 208 | accept the connection. If you don't have a particular subprotocol you wish to speak, you may 209 | pass null for the acceptedProtocol parameter. Note that the acceptedProtocol parameter is 210 | case-insensitive, and you must either pass a value that was originally requested by the client or 211 | null. For browser clients (in which the origin property would be non-null) you must pass that 212 | user's origin as the allowedOrigin parameter to confirm that you wish to accept connections 213 | from the given origin. 214 | 215 | The callback is invoked with the established WebSocketConnection instance that can be used 216 | to communicate with the connected client. 217 | 218 | ##### reject 219 | ``` JavaScript 220 | reject([httpStatus], [reason], cb) 221 | ``` 222 | 223 | If you decide to reject the connection, you must call reject. You may optionally pass in an 224 | HTTP Status code (such as 404) and a textual description that will be sent to the client. 225 | The connection will then be closed. 226 | 227 | The callback is invoked, without arguments, when the rejection is complete. 228 | -------------------------------------------------------------------------------- /hyco-websocket/lib/HybridConnectionsWebSocketServer.js: -------------------------------------------------------------------------------- 1 | /************************************************************************ 2 | * Copyright 2010-2015 Brian McKelvey. 3 | * Derivative Copyright Microsoft Corporation 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | ***********************************************************************/ 17 | 18 | const extend = require('./utils').extend; 19 | const utils = require('./utils'); 20 | const util = require('util'); 21 | const EventEmitter = require('events').EventEmitter; 22 | const WebSocketClient = require('websocket').client; 23 | const WebSocketRequest = require('./HybridConnectionsWebSocketRequest'); 24 | const querystring = require('querystring'); 25 | const moment = require('moment'); 26 | 27 | var isDefinedAndNonNull = function(options, key) { 28 | return typeof options[key] != 'undefined' && options[key] !== null; 29 | }; 30 | 31 | var HybridConnectionsWebSocketServer = function HybridConnectionsWebSocketServer(config) { 32 | // Superclass Constructor 33 | EventEmitter.call(this); 34 | 35 | this.closeRequested = false; 36 | this._handlers = { 37 | requestAccepted: this.handleRequestAccepted.bind(this), 38 | requestResolved: this.handleRequestResolved.bind(this) 39 | }; 40 | this.pendingRequests = []; 41 | this.connections = []; 42 | if (config) { 43 | this.open(config); 44 | } 45 | }; 46 | 47 | util.inherits(HybridConnectionsWebSocketServer, EventEmitter); 48 | 49 | HybridConnectionsWebSocketServer.prototype.open = function(config) { 50 | this.config = { 51 | // hybrid connection endpoint address 52 | server: null, 53 | // listen token string or callback to generate one 54 | token: null, 55 | // identifier 56 | id: null, 57 | 58 | // If true, the server will automatically send a ping to all 59 | // connections every 'keepaliveInterval' milliseconds. The timer is 60 | // reset on any received data from the client. 61 | keepalive: true, 62 | 63 | // The interval to send keepalive pings to connected clients if the 64 | // connection is idle. Any received data will reset the counter. 65 | keepaliveInterval: 20000, 66 | 67 | // If true, the server will consider any connection that has not 68 | // received any data within the amount of time specified by 69 | // 'keepaliveGracePeriod' after a keepalive ping has been sent to 70 | // be dead, and will drop the connection. 71 | // Ignored if keepalive is false. 72 | dropConnectionOnKeepaliveTimeout: true, 73 | 74 | // The amount of time to wait after sending a keepalive ping before 75 | // closing the connection if the connected peer does not respond. 76 | // Ignored if keepalive is false. 77 | keepaliveGracePeriod: 10000, 78 | 79 | // Whether to use native TCP keep-alive instead of WebSockets ping 80 | // and pong packets. Native TCP keep-alive sends smaller packets 81 | // on the wire and so uses bandwidth more efficiently. This may 82 | // be more important when talking to mobile devices. 83 | // If this value is set to true, then these values will be ignored: 84 | // keepaliveGracePeriod 85 | // dropConnectionOnKeepaliveTimeout 86 | useNativeKeepalive: false, 87 | 88 | // If true, fragmented messages will be automatically assembled 89 | // and the full message will be emitted via a 'message' event. 90 | // If false, each frame will be emitted via a 'frame' event and 91 | // the application will be responsible for aggregating multiple 92 | // fragmented frames. Single-frame messages will emit a 'message' 93 | // event in addition to the 'frame' event. 94 | // Most users will want to leave this set to 'true' 95 | assembleFragments: true, 96 | 97 | // If this is true, websocket connections will be accepted 98 | // regardless of the path and protocol specified by the client. 99 | // The protocol accepted will be the first that was requested 100 | // by the client. Clients from any origin will be accepted. 101 | // This should only be used in the simplest of cases. You should 102 | // probably leave this set to 'false' and inspect the request 103 | // object to make sure it's acceptable before accepting it. 104 | autoAcceptConnections: false, 105 | 106 | // Whether or not the X-Forwarded-For header should be respected. 107 | // It's important to set this to 'true' when accepting connections 108 | // from untrusted connections, as a malicious client could spoof its 109 | // IP address by simply setting this header. It's meant to be added 110 | // by a trusted proxy or other intermediary within your own 111 | // infrastructure. 112 | // See: http://en.wikipedia.org/wiki/X-Forwarded-For 113 | ignoreXForwardedFor: false, 114 | 115 | // The Nagle Algorithm makes more efficient use of network resources 116 | // by introducing a small delay before sending small packets so that 117 | // multiple messages can be batched together before going onto the 118 | // wire. This however comes at the cost of latency, so the default 119 | // is to disable it. If you don't need low latency and are streaming 120 | // lots of small messages, you can change this to 'false' 121 | disableNagleAlgorithm: true, 122 | 123 | // The number of milliseconds to wait after sending a close frame 124 | // for an acknowledgement to come back before giving up and just 125 | // closing the socket. 126 | closeTimeout: 5000 127 | }; 128 | extend(this.config, config); 129 | 130 | if (this.config.server) { 131 | // connect 132 | this.listenUri = config.server; 133 | if (isDefinedAndNonNull(config, 'id')) { 134 | this.listenUri = listenUri + '&id=' + config.id; 135 | } 136 | 137 | connectControlChannel(this); 138 | } 139 | else { 140 | throw new Error('You must specify a hybrid connections server address on which to open the WebSocket server.'); 141 | } 142 | }; 143 | 144 | HybridConnectionsWebSocketServer.prototype.close = function() { 145 | this.closeRequested = true; 146 | if (this.controlChannel) { 147 | this.controlChannel.close(); 148 | } 149 | this.closeAllConnections(); 150 | }; 151 | 152 | HybridConnectionsWebSocketServer.prototype.closeAllConnections = function() { 153 | this.connections.forEach(function(connection) { 154 | connection.close(); 155 | }); 156 | }; 157 | 158 | HybridConnectionsWebSocketServer.prototype.broadcast = function(data) { 159 | if (Buffer.isBuffer(data)) { 160 | this.broadcastBytes(data); 161 | } 162 | else if (typeof (data.toString) === 'function') { 163 | this.broadcastUTF(data); 164 | } 165 | }; 166 | 167 | HybridConnectionsWebSocketServer.prototype.broadcastUTF = function(utfData) { 168 | this.connections.forEach(function(connection) { 169 | connection.sendUTF(utfData); 170 | }); 171 | }; 172 | 173 | HybridConnectionsWebSocketServer.prototype.broadcastBytes = function(binaryData) { 174 | this.connections.forEach(function(connection) { 175 | connection.sendBytes(binaryData); 176 | }); 177 | }; 178 | 179 | HybridConnectionsWebSocketServer.prototype.shutDown = function() { 180 | this.closeAllConnections(); 181 | }; 182 | 183 | HybridConnectionsWebSocketServer.prototype.handleRequestAccepted = function(connection) { 184 | var self = this; 185 | connection.once('close', function(closeReason, description) { 186 | self.handleConnectionClose(connection, closeReason, description); 187 | }); 188 | this.connections.push(connection); 189 | this.emit('connect', connection); 190 | }; 191 | 192 | HybridConnectionsWebSocketServer.prototype.handleRequestResolved = function(request) { 193 | var index = this.pendingRequests.indexOf(request); 194 | if (index !== -1) { this.pendingRequests.splice(index, 1); } 195 | }; 196 | 197 | HybridConnectionsWebSocketServer.prototype.handleConnectionClose = function(closeReason, description) { 198 | console.log(description); 199 | } 200 | 201 | function connectControlChannel(server) { 202 | /* create the control connection */ 203 | 204 | var headers = null; 205 | var tokenRenewDuration = null; 206 | if (server.config.token != null) { 207 | var token = null; 208 | if (typeof server.config.token === 'function') { 209 | // server.config.token is a function, call it periodically to renew the token 210 | tokenRenewDuration = new moment.duration(1, 'hours'); 211 | token = server.config.token(); 212 | } else { 213 | // server.config.token is a string, the token cannot be renewed automatically 214 | token = server.config.token; 215 | } 216 | 217 | headers = { 'ServiceBusAuthorization': token }; 218 | }; 219 | 220 | // This represents the token renew timer/interval, keep a reference in order to cancel it. 221 | var tokenRenewTimer = null; 222 | 223 | var client = new WebSocketClient(); 224 | client.connect(server.listenUri, null, null, headers); 225 | client.on('connect', function(connection) { 226 | server.controlChannel = connection; 227 | server.controlChannel.on('error', function(event) { 228 | server.emit('error', event); 229 | clearInterval(tokenRenewTimer); 230 | if (!closeRequested) { 231 | connectControlChannel(server); 232 | } 233 | }); 234 | 235 | server.controlChannel.on('close', function(event) { 236 | clearInterval(tokenRenewTimer); 237 | if (!closeRequested) { 238 | // reconnect 239 | connectControlChannel(server); 240 | } else { 241 | server.controlChannel = null; 242 | server.emit('close', server); 243 | } 244 | }); 245 | 246 | server.controlChannel.on('message', function(message) { 247 | if (message.type === 'utf8') { 248 | try { 249 | handleControl(server, JSON.parse(message.utf8Data)); 250 | } 251 | catch (e) { 252 | // do nothing if there's an error. 253 | } 254 | } 255 | }); 256 | }); 257 | 258 | client.on('connectFailed', function(event) { 259 | console.log(event); 260 | server.emit('error', event); 261 | }); 262 | 263 | if (tokenRenewDuration) { 264 | // tokenRenewDuration having a value means server.config.token is a function, renew the token periodically 265 | tokenRenewTimer = setInterval(function() { 266 | if (!server.closeRequested) { 267 | var newToken = server.config.token(); 268 | console.log('Renewing Token: ' + newToken); 269 | var renewToken = { 'renewToken' : { 'token' : newToken } }; 270 | server.controlChannel.send( 271 | JSON.stringify(renewToken), 272 | function(error) { 273 | if (error) { 274 | console.log('renewToken error: ' + error); 275 | } 276 | } 277 | ); 278 | } 279 | }, 280 | tokenRenewDuration.asMilliseconds()); 281 | } 282 | } 283 | 284 | function handleControl(server, message) { 285 | if (isDefinedAndNonNull(message, 'accept')) { 286 | handleAccept(server, message); 287 | } 288 | } 289 | 290 | function handleAccept(server, message) { 291 | var wsRequest = new WebSocketRequest( 292 | message.accept.address, 293 | message.accept.id, 294 | message.accept.connectHeaders, 295 | server.config); 296 | try { 297 | wsRequest.readHandshake(); 298 | } 299 | catch (e) { 300 | wsRequest.reject( 301 | e.httpCode ? e.httpCode : 400, 302 | e.message, 303 | e.headers 304 | ); 305 | debug('Invalid handshake: %s', e.message); 306 | return; 307 | } 308 | 309 | server.pendingRequests.push(wsRequest); 310 | 311 | wsRequest.once('requestAccepted', server._handlers.requestAccepted); 312 | wsRequest.once('requestResolved', server._handlers.requestResolved); 313 | 314 | if (!server.config.autoAcceptConnections && utils.eventEmitterListenerCount(server, 'request') > 0) { 315 | server.emit('request', wsRequest); 316 | } 317 | else if (server.config.autoAcceptConnections) { 318 | wsRequest.accept(wsRequest.requestedProtocols[0], wsRequest.origin); 319 | } 320 | else { 321 | wsRequest.reject(404, 'No handler is configured to accept the connection.'); 322 | } 323 | } 324 | 325 | module.exports = HybridConnectionsWebSocketServer; 326 | --------------------------------------------------------------------------------