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 |
19 |
20 |
21 |
22 |
23 |
Not initialized
24 |
25 |
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.
4 |
5 | # Azure Relay Hybrid Connections Clients for Node.JS
6 |
7 | 
8 |
9 | |Package|Status|
10 | |------|-------------|
11 | |hyco-ws|[](https://badge.fury.io/js/hyco-ws)|
12 | |hyco-websocket|[](https://badge.fury.io/js/hyco-websocket)|
13 | |hyco-https|[](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 |
--------------------------------------------------------------------------------