baseline
This commit is contained in:
Коммит
157fea3a5f
|
@ -0,0 +1,14 @@
|
|||
.vscode
|
||||
.vscode/*
|
||||
|
||||
# Build and package folders
|
||||
###################
|
||||
build
|
||||
dist
|
||||
packages
|
||||
typings
|
||||
.tmp
|
||||
|
||||
# Node
|
||||
###################
|
||||
node_modules
|
|
@ -0,0 +1,90 @@
|
|||
# Azure Relay Hybrid Connections for Node.JS
|
||||
|
||||
This repository contains Node modules and samples for the Hybrid Connections feature of the
|
||||
Microsoft Azure Relay, a capability pilar of the Azure Service Bus platform.
|
||||
|
||||
Azure Relay is one of the key capability pillars of the Azure Service Bus platform. Hybrid
|
||||
Connections is a secure, open-protocol evolution of the existing Relay service that has been
|
||||
available in Azure since the beginning. Hybrid Connections is based on HTTP and WebSockets.
|
||||
|
||||
Hybrid Connections allows establishing bi-directional, binary stream communication between
|
||||
two networked applications, whereby either or both of the parties can reside behind NATs or
|
||||
Firewalls.
|
||||
|
||||
## Functional Principles
|
||||
|
||||
For Node, the code in the repository allows a **publicly discoverable and reachable** WebSocket
|
||||
server to be hosted on any machine that has outbound access to the Internet, and
|
||||
specifically to the Microsoft Azure Relay service in the chosen region, via HTTPS port 443.
|
||||
|
||||
The WebSocket server code will look instantly familiar as it is directly based on and integrated
|
||||
with the most most popular existing WebSocket modules in the Node universe: "ws" and "websocket".
|
||||
|
||||
``` JS
|
||||
|
||||
require('ws') ==> require('hyco-ws')
|
||||
require('websocket') ==> require('hyco-websocket')
|
||||
|
||||
```
|
||||
|
||||
As you create a WebSocket server using either of the alternate "hyco-ws" and "hyco-websocket"
|
||||
modules from this repository, the server will not listen on a TCP port on the local network,
|
||||
but rather delegate listening to a configured Hybrid Connection path the Azure Relay service
|
||||
in Service Bus. That listener connection is automatically TLS/SSL protected without you having
|
||||
to juggle any certificates.
|
||||
|
||||
The example below shows the "ws"/"hyco-ws" variant of creating a server. The API is usage is
|
||||
completely "normal" except for using the "hyco-ws" module and creating an instance of the
|
||||
*RelayedServer* instead of *Server*. The default underlying *Server* class remains fully available
|
||||
when using "hyco-ws" instead of "ws", meaning you can host a relayed and a local WebSocket
|
||||
server side-by-side from within the same application. The "websocket"/"hyco-websocket"
|
||||
experience is analogous and explained in the module's README.
|
||||
|
||||
``` JS
|
||||
var WebSocket = require('hyco-ws')
|
||||
|
||||
var wss = WebSocket.RelayedServer(
|
||||
{
|
||||
// create the
|
||||
server : WebSocket.createRelayListenUri(ns, path),
|
||||
token: WebSocket.createRelayToken('http://'+ns, keyrule, key)
|
||||
});
|
||||
|
||||
wss.on('connection', function (ws) {
|
||||
console.log('connection accepted');
|
||||
ws.onmessage = function (event) {
|
||||
console.log(JSON.parse(event.data));
|
||||
};
|
||||
ws.on('close', function () {
|
||||
console.log('connection closed');
|
||||
});
|
||||
});
|
||||
|
||||
wss.on('error', function(err) {
|
||||
console.log('error' + err);
|
||||
});
|
||||
```
|
||||
|
||||
Up to 25 WebSocket listeners can listen concurrently on the same Hybrid Connection path on the
|
||||
Relay; if two or more listeners are connected, the service will automatically balance incoming
|
||||
connection requests across the connected listeners. which also provides an easy failover capability.
|
||||
You don't have to do anything to enable this, just have multiple listeners share the same path.
|
||||
|
||||
Clients connect to the server through the Relay service on the same path the listener is listening
|
||||
on. The client uses the regular WebSocket protocol.
|
||||
three
|
||||
|
||||
## Modules
|
||||
|
||||
This repository hosts two different modules for Node that integrate with the Hybrid Connections
|
||||
feature. The modules are designed to act, as much as possible, as contract-compatible drop-in
|
||||
replacements for two of the most popular existing WebSocket modules in the Node universe:
|
||||
"ws" and "websocket". "Contract-compatible" means that you can take nearly any existing module or
|
||||
app that uses either library and convert it to work through the Hybrid Connections relay with
|
||||
minimal changes.
|
||||
|
||||
### Functional principles
|
||||
|
||||
The functional principle
|
||||
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
node_modules/
|
||||
node_modules/*
|
|
@ -0,0 +1,74 @@
|
|||
var https = require('https');
|
||||
var tunnel = require('./lib/tunnel');
|
||||
var WebSocket = require('../hyco-websocket');
|
||||
|
||||
var argv = require('optimist').argv;
|
||||
if ( argv._.length < 4) {
|
||||
console.log("connect.js [namespace] [path] [key-rule] [key]")
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
var ns = argv._[0];
|
||||
var path = argv._[1];
|
||||
var keyrule = argv._[2];
|
||||
var key = argv._[3];
|
||||
var server = WebSocket.createRelaySendUri(ns, path);
|
||||
var token = WebSocket.createRelayToken('http://'+ns, keyrule, key);
|
||||
|
||||
var credentials, tunnels = [];
|
||||
|
||||
var shell = global.shell = require('./lib/shell');
|
||||
|
||||
shell.on('command', function(cmd, args) {
|
||||
if (cmd == 'help') {
|
||||
shell.echo('Commands:');
|
||||
shell.echo('tunnel [localhost:]port [remotehost:]port');
|
||||
shell.echo('close [tunnel-id]');
|
||||
shell.echo('exit');
|
||||
shell.prompt();
|
||||
} else
|
||||
if (cmd == 'tunnel') {
|
||||
tunnel.createTunnel(server, token, credentials, args[0], args[1], function(err, server) {
|
||||
if (err) {
|
||||
shell.echo(String(err));
|
||||
} else {
|
||||
var id = tunnels.push(server);
|
||||
shell.echo('Tunnel created with id: ' + id);
|
||||
}
|
||||
shell.prompt();
|
||||
});
|
||||
} else
|
||||
if (cmd == 'close') {
|
||||
var id = parseInt(args[0], 10) - 1;
|
||||
if (tunnels[id]) {
|
||||
tunnels[id].close();
|
||||
tunnels[id] = null;
|
||||
shell.echo('Tunnel ' + (id + 1) + ' closed.');
|
||||
} else {
|
||||
shell.echo('Invalid tunnel id.');
|
||||
}
|
||||
shell.prompt();
|
||||
} else
|
||||
if (cmd == 'exit') {
|
||||
shell.exit();
|
||||
} else {
|
||||
shell.echo('Invalid command. Type `help` for more information.');
|
||||
shell.prompt();
|
||||
}
|
||||
});
|
||||
|
||||
shell.echo('WebSocket Tunnel Console v0.1');
|
||||
shell.echo('Remote Host: ' + ns);
|
||||
|
||||
authenticate(function() {
|
||||
shell.prompt();
|
||||
});
|
||||
|
||||
function authenticate(callback) {
|
||||
shell.prompt('Username: ', function(user) {
|
||||
shell.prompt('Password: ', function(pw) {
|
||||
credentials = user + ':' + pw;
|
||||
callback();
|
||||
}, {passwordMode: true});
|
||||
});
|
||||
}
|
|
@ -0,0 +1,106 @@
|
|||
var readline = require('readline');
|
||||
var EventEmitter = require('events').EventEmitter;
|
||||
|
||||
var rl = readline.createInterface(process.stdin, process.stdout);
|
||||
|
||||
var shell = module.exports = new EventEmitter();
|
||||
var paused, passwordMode;
|
||||
|
||||
//Hack to hide mask characters from output
|
||||
var normalWrite = process.stdout.write;
|
||||
var dummyWrite = function (data) {
|
||||
var args = Array.prototype.slice.call(arguments);
|
||||
if (typeof data == 'string') {
|
||||
//todo: deal with control characters like backspace
|
||||
args[0] = new Array(data.length + 1).join('*');
|
||||
}
|
||||
normalWrite.apply(process.stdout, args);
|
||||
};
|
||||
|
||||
rl.on('SIGINT', function () {
|
||||
shell.exit();
|
||||
});
|
||||
|
||||
shell.pause = function (s) {
|
||||
process.stdin.pause();
|
||||
rl.pause();
|
||||
paused = true;
|
||||
};
|
||||
|
||||
shell.resume = function (s) {
|
||||
process.stdin.resume();
|
||||
rl.resume();
|
||||
paused = false;
|
||||
};
|
||||
|
||||
shell.echo = function (s) {
|
||||
var args = Array.prototype.slice.call(arguments);
|
||||
for (var i = 0; i < args.length; i++) {
|
||||
shell.writeLine(args[i]);
|
||||
}
|
||||
};
|
||||
|
||||
shell.setPasswordMode = function (enabled) {
|
||||
passwordMode = !!enabled;
|
||||
process.stdout.write = (enabled) ? dummyWrite : normalWrite;
|
||||
};
|
||||
|
||||
shell.writeLine = function (s) {
|
||||
if (paused) {
|
||||
rl.output.write(s);
|
||||
rl.output.write('\n');
|
||||
} else {
|
||||
try {
|
||||
rl.output.cursorTo(0);
|
||||
} catch (e) {
|
||||
// nop
|
||||
}
|
||||
rl.output.write(s);
|
||||
try {
|
||||
rl.output.clearLine(1);
|
||||
} catch (e) {
|
||||
// nop
|
||||
}
|
||||
rl.output.write('\n');
|
||||
try {
|
||||
rl._refreshLine();
|
||||
} catch (e) {
|
||||
// nop
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var lineHandler;
|
||||
rl.on('line', function (line) {
|
||||
shell.pause();
|
||||
if (passwordMode) {
|
||||
shell.setPasswordMode(false);
|
||||
process.stdout.write('\n');
|
||||
}
|
||||
if (lineHandler) {
|
||||
lineHandler(line);
|
||||
} else {
|
||||
var parts = line.trim().split(/\s+/);
|
||||
shell.emit('command', parts[0], parts.slice(1));
|
||||
}
|
||||
});
|
||||
|
||||
shell.prompt = function (query, callback, opts) {
|
||||
opts = opts || {};
|
||||
lineHandler = callback;
|
||||
rl.setPrompt(query || '> ');
|
||||
shell.resume();
|
||||
rl.prompt();
|
||||
if (opts.passwordMode) {
|
||||
shell.setPasswordMode(true);
|
||||
}
|
||||
};
|
||||
|
||||
shell.exit = function () {
|
||||
rl.close();
|
||||
process.stdin.destroy();
|
||||
process.exit();
|
||||
};
|
||||
|
||||
//start initially paused
|
||||
rl.pause();
|
|
@ -0,0 +1,124 @@
|
|||
var net = require('net');
|
||||
var WebSocketClient = require('../../hyco-websocket').client;
|
||||
|
||||
exports.createTunnel = function(wsServerAddr, token, credentials, listen, forward, callback) {
|
||||
listen = parseAddr(listen, {host: '0.0.0.0', port: '8080'});
|
||||
forward = parseAddr(forward, {host: '127.0.0.1', port: listen.port});
|
||||
|
||||
var server = net.createServer(function (tcpSock) {
|
||||
|
||||
var wsClient = new WebSocketClient();
|
||||
|
||||
var webSock, buffer = [];
|
||||
|
||||
tcpSock.on('data', function(data) {
|
||||
if (!webSock || buffer.length) {
|
||||
buffer.push(data);
|
||||
} else {
|
||||
webSock.send(data);
|
||||
}
|
||||
});
|
||||
|
||||
tcpSock.on('close', function() {
|
||||
log('TCP socket closed');
|
||||
if (webSock) {
|
||||
webSock.close();
|
||||
} else {
|
||||
webSock = null;
|
||||
}
|
||||
});
|
||||
|
||||
tcpSock.on('error', function(err) {
|
||||
log('TCP socket closed');
|
||||
if (webSock) {
|
||||
webSock.close();
|
||||
} else {
|
||||
webSock = null;
|
||||
}
|
||||
});
|
||||
|
||||
wsClient.on('connect', function(connection) {
|
||||
log('WebSocket connected');
|
||||
|
||||
//flush buffer
|
||||
while (buffer.length) {
|
||||
connection.send(buffer.shift());
|
||||
}
|
||||
|
||||
//check if tcpSock is already closed
|
||||
if (webSock === null) {
|
||||
connection.close();
|
||||
return;
|
||||
}
|
||||
|
||||
webSock = connection;
|
||||
webSock.on('message', function (msg) {
|
||||
if (msg.type == 'utf8') {
|
||||
//log('Received UTF8 message');
|
||||
var data = JSON.parse(msg.utf8Data);
|
||||
if (data.status == 'error') {
|
||||
log(data.details);
|
||||
webSock.close();
|
||||
}
|
||||
} else {
|
||||
//log('Received binary message');
|
||||
tcpSock.write(msg.binaryData);
|
||||
}
|
||||
});
|
||||
|
||||
webSock.on('close', function (reasonCode, description) {
|
||||
log('WebSocket closed; ' + reasonCode + '; ' + description);
|
||||
tcpSock.destroy();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
wsClient.on('connectFailed', function(err) {
|
||||
log('WebSocket connection failed: ' + err);
|
||||
tcpSock.destroy();
|
||||
});
|
||||
|
||||
var url = wsServerAddr; //+ '&port=' + forward.port + '&host=' + forward.host;
|
||||
wsClient.connect(url, null, null,
|
||||
{ 'ServiceBusAuthorization' : token,
|
||||
'Authorization': 'Basic ' + new Buffer(credentials).toString('base64')});
|
||||
|
||||
});
|
||||
|
||||
server.on('error', function (err) {
|
||||
callback(err);
|
||||
});
|
||||
|
||||
server.listen(listen.port, listen.host, function() {
|
||||
var addr = server.address();
|
||||
log('listening on ' + addr.address + ':' + addr.port);
|
||||
if (callback) callback(null, server);
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
function parseAddr(str, addr) {
|
||||
if (str) {
|
||||
var parts = str.split(':');
|
||||
if (parts.length == 1) {
|
||||
if (parts[0] == parseInt(parts[0], 10).toString()) {
|
||||
addr.port = parts[0];
|
||||
} else {
|
||||
addr.host = parts[0];
|
||||
}
|
||||
} else
|
||||
if (parts.length == 2) {
|
||||
addr = {host: parts[0], port: parts[1]};
|
||||
}
|
||||
}
|
||||
return addr;
|
||||
}
|
||||
|
||||
|
||||
function log(s) {
|
||||
if (global.shell) {
|
||||
global.shell.echo(s);
|
||||
} else {
|
||||
console.log(s);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
BSD 2-Clause License
|
||||
|
||||
Copyright (c) 2016, Simon Sturmer <sstur@me.com>
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"name": "node-websocket-tunnel",
|
||||
"version": "1.0.0",
|
||||
"repository" : {
|
||||
"type" : "git",
|
||||
"url" : "git://github.com/clemensv/node-websocket-tunnel.git"
|
||||
},
|
||||
"dependencies": {
|
||||
"optimist": "^0.3.1",
|
||||
"websocket": "^1.0.4",
|
||||
"wordwrap": "^0.0.2"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
#TCP over WebSocket
|
||||
|
||||
This tool allows you to tunnel TCP connections over WebSocket protocol using SSL. It consists of a server agent and
|
||||
a client console. If the server agent is running on a remote machine you can use it as a middle-man to route connections
|
||||
securely to any network host even through firewalls and proxies.
|
||||
|
||||
##Example use cases:
|
||||
- You are on a restricted network that only allows traffic on ports 80 and 443
|
||||
- You wish to connect securely to a service from a public access point, and cannot use SSH
|
||||
|
||||
Due to WebSocket connections starting out as normal HTTPS, this can be used to tunnel connections through certain
|
||||
restrictive firewalls that do not even allow SSH or OpenVPN over port 443.
|
||||
|
||||
##Usage
|
||||
|
||||
On a server, run `server.js` specifying optional port and address to bind to (defaults to 0.0.0.0:443):
|
||||
|
||||
`node server.js 74.125.227.148:443`
|
||||
|
||||
On a client, run `connect.js` specifying remote host and optional port (defaults to 443):
|
||||
|
||||
`node connect.js 74.125.227.148`
|
||||
|
||||
You will be prompted for username/password which the server will verify against users.txt and then you are presented
|
||||
with a command shell where you can create and destroy tunnels.
|
||||
|
||||
`> tunnel 3306 8.12.44.238:3306`
|
||||
|
||||
This will listen on port 3306 on the client (localhost) and forward connections to remote host 8.12.44.238 via the
|
||||
WebSocket server. Destination port, if omitted, will default to source port.
|
||||
|
||||
The server uses SSL key files present in `keys/` and users listed in `users.txt` (in which passwords are md5 hashed).
|
||||
|
|
@ -0,0 +1,149 @@
|
|||
var fs = require('fs');
|
||||
var net = require('net');
|
||||
var crypto = require('crypto');
|
||||
var urlParse = require('url').parse;
|
||||
var WebSocket = require('../hyco-websocket');
|
||||
var WebSocketServer = require('../hyco-websocket').server;
|
||||
|
||||
process.chdir(__dirname);
|
||||
|
||||
var argv = require('optimist').argv;
|
||||
var pidfile;
|
||||
|
||||
//kill an already running instance
|
||||
if (argv.kill) {
|
||||
pidfile = argv.kill;
|
||||
if (!pidfile.match(/\.pid$/i))
|
||||
pidfile += '.pid';
|
||||
try {
|
||||
var pid = fs.readFileSync(pidfile, 'utf8');
|
||||
fs.unlinkSync(pidfile);
|
||||
process.kill(parseInt(pid, 10));
|
||||
console.log('Killed process ' + pid);
|
||||
} catch (e) {
|
||||
console.log('Error killing process ' + (pid || argv.kill));
|
||||
}
|
||||
process.exit();
|
||||
}
|
||||
|
||||
//write pid to file so it can be killed with --kill
|
||||
if (argv.pidfile) {
|
||||
pidfile = argv.pidfile;
|
||||
if (!pidfile.match(/\.pid$/i))
|
||||
pidfile += '.pid';
|
||||
fs.writeFileSync(pidfile, process.pid);
|
||||
}
|
||||
|
||||
var users = loadUsers();
|
||||
|
||||
if (argv._.length < 4) {
|
||||
console.log("server.js [namespace] [path] [key-rule] [key]")
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
var ns = argv._[0];
|
||||
var path = argv._[1];
|
||||
var keyrule = argv._[2];
|
||||
var key = argv._[3];
|
||||
|
||||
|
||||
|
||||
var wsServer = new WebSocketServer(
|
||||
{
|
||||
server: WebSocket.createRelayListenUri(ns, path),
|
||||
token: WebSocket.createRelayToken('http://' + ns, keyrule, key),
|
||||
|
||||
});
|
||||
|
||||
wsServer.on('request', function (request) {
|
||||
createTunnel(request, 3389);
|
||||
});
|
||||
|
||||
|
||||
function authenticate(request) {
|
||||
var encoded = request.headers['authorization'] || '', credentials;
|
||||
encoded = encoded.replace(/Basic /i, '');
|
||||
try {
|
||||
credentials = new Buffer(encoded, 'base64').toString('utf8').split(':');
|
||||
} catch (e) {
|
||||
credentials = [];
|
||||
}
|
||||
var user = credentials[0], hash = md5(credentials[1]);
|
||||
return (users[user] == hash);
|
||||
}
|
||||
|
||||
function createTunnel(request, port, host) {
|
||||
if (!authenticate(request.httpRequest)) {
|
||||
request.reject(403);
|
||||
return;
|
||||
}
|
||||
request.accept(null, null, null, function (webSock) {
|
||||
console.log(webSock.remoteAddress + ' connected - Protocol Version ' + webSock.webSocketVersion);
|
||||
|
||||
var tcpSock = new net.Socket();
|
||||
|
||||
tcpSock.on('error', function (err) {
|
||||
webSock.send(JSON.stringify({ status: 'error', details: 'Upstream socket error; ' + err }));
|
||||
});
|
||||
|
||||
tcpSock.on('data', function (data) {
|
||||
webSock.send(data);
|
||||
});
|
||||
|
||||
tcpSock.on('close', function () {
|
||||
webSock.close();
|
||||
});
|
||||
|
||||
tcpSock.connect(port, host || '127.0.0.1', function () {
|
||||
webSock.on('message', function (msg) {
|
||||
if (msg.type === 'utf8') {
|
||||
//console.log('received utf message: ' + msg.utf8Data);
|
||||
} else {
|
||||
//console.log('received binary message of length ' + msg.binaryData.length);
|
||||
tcpSock.write(msg.binaryData);
|
||||
}
|
||||
});
|
||||
webSock.send(JSON.stringify({ status: 'ready', details: 'Upstream socket connected' }));
|
||||
});
|
||||
|
||||
webSock.on('close', function () {
|
||||
tcpSock.destroy();
|
||||
console.log(webSock.remoteAddress + ' disconnected');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function loadUsers() {
|
||||
var lines = fs.readFileSync('./users.txt', 'utf8');
|
||||
var users = {};
|
||||
lines.split(/[\r\n]+/g).forEach(function (line) {
|
||||
var parts = line.split(':');
|
||||
if (parts.length == 2) {
|
||||
users[parts[0]] = parts[1];
|
||||
}
|
||||
});
|
||||
return users;
|
||||
}
|
||||
|
||||
function md5(s) {
|
||||
var hash = crypto.createHash('md5');
|
||||
hash.update(new Buffer(String(s)));
|
||||
return hash.digest('hex');
|
||||
}
|
||||
|
||||
function parseAddr(str, addr) {
|
||||
if (str) {
|
||||
var parts = str.split(':');
|
||||
if (parts.length == 1) {
|
||||
if (parts[0] == parseInt(parts[0], 10).toString()) {
|
||||
addr.port = parts[0];
|
||||
} else {
|
||||
addr.host = parts[0];
|
||||
}
|
||||
} else
|
||||
if (parts.length == 2) {
|
||||
addr = { host: parts[0], port: parts[1] };
|
||||
}
|
||||
}
|
||||
return addr;
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
simon:bf787577ff656cde5b5d1f8236a75d2a
|
||||
foo:37b51d194a7513e45b56f6524f2d51f2
|
|
@ -0,0 +1,7 @@
|
|||
node_modules
|
||||
.DS_Store
|
||||
.lock-*
|
||||
build
|
||||
build/*
|
||||
builderror.log
|
||||
npm-debug.log
|
|
@ -0,0 +1,88 @@
|
|||
{
|
||||
// JSHint Default Configuration File (as on JSHint website)
|
||||
// See http://jshint.com/docs/ for more details
|
||||
|
||||
"maxerr" : 50, // {int} Maximum error before stopping
|
||||
|
||||
// Enforcing
|
||||
"bitwise" : false, // true: Prohibit bitwise operators (&, |, ^, etc.)
|
||||
"camelcase" : false, // true: Identifiers must be in camelCase
|
||||
"curly" : true, // true: Require {} for every new block or scope
|
||||
"eqeqeq" : true, // true: Require triple equals (===) for comparison
|
||||
"freeze" : true, // true: prohibits overwriting prototypes of native objects such as Array, Date etc.
|
||||
"forin" : false, // true: Require filtering for..in loops with obj.hasOwnProperty()
|
||||
"immed" : true, // true: Require immediate invocations to be wrapped in parens e.g. `(function () { } ());`
|
||||
"latedef" : "nofunc", // true: Require variables/functions to be defined before being used
|
||||
"newcap" : true, // true: Require capitalization of all constructor functions e.g. `new F()`
|
||||
"noarg" : true, // true: Prohibit use of `arguments.caller` and `arguments.callee`
|
||||
"noempty" : true, // true: Prohibit use of empty blocks
|
||||
"nonbsp" : true, // true: Prohibit "non-breaking whitespace" characters.
|
||||
"nonew" : true, // true: Prohibit use of constructors for side-effects (without assignment)
|
||||
"plusplus" : false, // true: Prohibit use of `++` & `--`
|
||||
"quotmark" : "single", // Quotation mark consistency:
|
||||
// false : do nothing (default)
|
||||
// true : ensure whatever is used is consistent
|
||||
// "single" : require single quotes
|
||||
// "double" : require double quotes
|
||||
"undef" : true, // true: Require all non-global variables to be declared (prevents global leaks)
|
||||
"unused" : "vars", // vars: Require all defined variables be used, ignore function params
|
||||
"strict" : false, // true: Requires all functions run in ES5 Strict Mode
|
||||
"maxparams" : false, // {int} Max number of formal params allowed per function
|
||||
"maxdepth" : false, // {int} Max depth of nested blocks (within functions)
|
||||
"maxstatements" : false, // {int} Max number statements per function
|
||||
"maxcomplexity" : false, // {int} Max cyclomatic complexity per function
|
||||
"maxlen" : false, // {int} Max number of characters per line
|
||||
|
||||
// Relaxing
|
||||
"asi" : false, // true: Tolerate Automatic Semicolon Insertion (no semicolons)
|
||||
"boss" : false, // true: Tolerate assignments where comparisons would be expected
|
||||
"debug" : false, // true: Allow debugger statements e.g. browser breakpoints.
|
||||
"eqnull" : false, // true: Tolerate use of `== null`
|
||||
"es5" : false, // true: Allow ES5 syntax (ex: getters and setters)
|
||||
"esnext" : true, // true: Allow ES.next (ES6) syntax (ex: `const`)
|
||||
"moz" : false, // true: Allow Mozilla specific syntax (extends and overrides esnext features)
|
||||
// (ex: `for each`, multiple try/catch, function expression…)
|
||||
"evil" : false, // true: Tolerate use of `eval` and `new Function()`
|
||||
"expr" : false, // true: Tolerate `ExpressionStatement` as Programs
|
||||
"funcscope" : false, // true: Tolerate defining variables inside control statements
|
||||
"globalstrict" : false, // true: Allow global "use strict" (also enables 'strict')
|
||||
"iterator" : false, // true: Tolerate using the `__iterator__` property
|
||||
"lastsemic" : false, // true: Tolerate omitting a semicolon for the last statement of a 1-line block
|
||||
"laxbreak" : false, // true: Tolerate possibly unsafe line breakings
|
||||
"laxcomma" : false, // true: Tolerate comma-first style coding
|
||||
"loopfunc" : false, // true: Tolerate functions being defined in loops
|
||||
"multistr" : false, // true: Tolerate multi-line strings
|
||||
"noyield" : false, // true: Tolerate generator functions with no yield statement in them.
|
||||
"notypeof" : false, // true: Tolerate invalid typeof operator values
|
||||
"proto" : false, // true: Tolerate using the `__proto__` property
|
||||
"scripturl" : false, // true: Tolerate script-targeted URLs
|
||||
"shadow" : false, // true: Allows re-define variables later in code e.g. `var x=1; x=2;`
|
||||
"sub" : true, // true: Tolerate using `[]` notation when it can still be expressed in dot notation
|
||||
"supernew" : false, // true: Tolerate `new function () { ... };` and `new Object;`
|
||||
"validthis" : false, // true: Tolerate using this in a non-constructor function
|
||||
|
||||
// Environments
|
||||
"browser" : true, // Web Browser (window, document, etc)
|
||||
"browserify" : true, // Browserify (node.js code in the browser)
|
||||
"couch" : false, // CouchDB
|
||||
"devel" : true, // Development/debugging (alert, confirm, etc)
|
||||
"dojo" : false, // Dojo Toolkit
|
||||
"jasmine" : false, // Jasmine
|
||||
"jquery" : false, // jQuery
|
||||
"mocha" : false, // Mocha
|
||||
"mootools" : false, // MooTools
|
||||
"node" : true, // Node.js
|
||||
"nonstandard" : false, // Widely adopted globals (escape, unescape, etc)
|
||||
"prototypejs" : false, // Prototype and Scriptaculous
|
||||
"qunit" : false, // QUnit
|
||||
"rhino" : false, // Rhino
|
||||
"shelljs" : false, // ShellJS
|
||||
"worker" : false, // Web Workers
|
||||
"wsh" : false, // Windows Scripting Host
|
||||
"yui" : false, // Yahoo User Interface
|
||||
|
||||
// Custom Globals
|
||||
"globals" : { // additional predefined global variables
|
||||
"WebSocket": true
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
.npmignore
|
||||
.gitignore
|
||||
|
||||
example/
|
||||
build/
|
||||
test/
|
||||
node_modules/
|
|
@ -0,0 +1,3 @@
|
|||
Changelog
|
||||
=========
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
WebSocket Client & Server Implementation for Node
|
||||
=================================================
|
||||
|
||||
Overview
|
||||
--------
|
||||
|
||||
|
||||
Documentation
|
||||
=============
|
||||
|
||||
|
||||
|
||||
Changelog
|
||||
---------
|
|
@ -0,0 +1,41 @@
|
|||
|
||||
if ( process.argv.length < 6) {
|
||||
console.log("listener.js [namespace] [path] [key-rule] [key]");
|
||||
} else {
|
||||
|
||||
var ns = process.argv[2];
|
||||
var path = process.argv[3];
|
||||
var keyrule = process.argv[4];
|
||||
var key = process.argv[5];
|
||||
|
||||
var WebSocket = require('../../');
|
||||
var WebSocketServer = require('../../').server;
|
||||
|
||||
var wss = new WebSocketServer(
|
||||
{
|
||||
server : WebSocket.createRelayListenUri(ns, path),
|
||||
token: WebSocket.createRelayToken('http://'+ns, keyrule, key),
|
||||
autoAcceptConnections : true
|
||||
});
|
||||
wss.on('connect',
|
||||
function (ws) {
|
||||
console.log('connection accepted');
|
||||
ws.on('message', function (message) {
|
||||
if (message.type === 'utf8') {
|
||||
try {
|
||||
console.log(JSON.parse(message.utf8Data));
|
||||
}
|
||||
catch(e) {
|
||||
// do nothing if there's an error.
|
||||
}
|
||||
}
|
||||
});
|
||||
ws.on('close', function () {
|
||||
console.log('connection closed');
|
||||
});
|
||||
});
|
||||
|
||||
wss.on('error', function(err) {
|
||||
console.log('error' + err);
|
||||
});
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"author": "",
|
||||
"name": "simple",
|
||||
"version": "0.0.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/clemensv/azure-hybridconnections.git"
|
||||
},
|
||||
"devDependencies": {},
|
||||
"optionalDependencies": {}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
if (process.argv.length < 6) {
|
||||
console.log("listener.js [namespace] [path] [key-rule] [key]");
|
||||
} else {
|
||||
|
||||
var ns = process.argv[2];
|
||||
var path = process.argv[3];
|
||||
var keyrule = process.argv[4];
|
||||
var key = process.argv[5];
|
||||
|
||||
var WebSocket = require('../../')
|
||||
var WebSocketClient = WebSocket.client
|
||||
|
||||
|
||||
var address = WebSocket.createRelaySendUri(ns, path);
|
||||
var token = WebSocket.createRelayToken('http://'+ns, keyrule, key);
|
||||
|
||||
var client = new WebSocketClient({tlsOptions: { rejectUnauthorized: false }});
|
||||
client.connect(address, null, null, { 'ServiceBusAuthorization' : token});
|
||||
|
||||
client.on('connect', function(connection){
|
||||
var id = setInterval(function () {
|
||||
connection.send(JSON.stringify(process.memoryUsage()), function () { /* ignore errors */ });
|
||||
}, 100);
|
||||
|
||||
console.log('Started client interval. Press any key to stop.');
|
||||
connection.on('close', function () {
|
||||
console.log('stopping client interval');
|
||||
clearInterval(id);
|
||||
process.exit();
|
||||
});
|
||||
|
||||
process.stdin.setRawMode(true);
|
||||
process.stdin.resume();
|
||||
process.stdin.on('data', function () {
|
||||
connection.close();
|
||||
});
|
||||
});
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
/**
|
||||
* Dependencies.
|
||||
*/
|
||||
var gulp = require('gulp');
|
||||
var jshint = require('gulp-jshint');
|
||||
|
||||
gulp.task('lint', function() {
|
||||
return gulp.src(['gulpfile.js', 'lib/**/*.js', 'test/**/*.js'])
|
||||
.pipe(jshint('.jshintrc'))
|
||||
.pipe(jshint.reporter('jshint-stylish', {verbose: true}))
|
||||
.pipe(jshint.reporter('fail'));
|
||||
});
|
||||
|
||||
gulp.task('default', gulp.series('lint'));
|
|
@ -0,0 +1,38 @@
|
|||
'use strict';
|
||||
|
||||
var crypto = require('crypto')
|
||||
var moment = require('moment')
|
||||
|
||||
var WS = module.exports = require('./lib/hybridconnectionswebsocket');
|
||||
|
||||
WS.createRelayToken = function createRelayToken(uri, key_name, key) {
|
||||
|
||||
// Token expires in one hour
|
||||
var expiry = moment().add(1, 'hours').unix();
|
||||
|
||||
var string_to_sign = encodeURIComponent(uri) + '\n' + expiry;
|
||||
var hmac = crypto.createHmac('sha256', key);
|
||||
hmac.update(string_to_sign);
|
||||
var signature = hmac.digest('base64');
|
||||
var token = 'SharedAccessSignature sr=' + encodeURIComponent(uri) + '&sig=' + encodeURIComponent(signature) + '&se=' + expiry + '&skn=' + key_name;
|
||||
|
||||
return token;
|
||||
};
|
||||
|
||||
WS.createRelaySendUri = function createRelaySendUri(serviceBusNamespace, path, token)
|
||||
{
|
||||
var uri = 'wss://' + serviceBusNamespace + ':443/$servicebus/hybridconnection?action=connect&path=' + path;
|
||||
if ( token != null ) {
|
||||
uri = uri + '&token=' + encodeURIComponent(token);
|
||||
}
|
||||
return uri;
|
||||
}
|
||||
|
||||
WS.createRelayListenUri = function createRelayListenUri(serviceBusNamespace, path, token)
|
||||
{
|
||||
var uri = 'wss://' + serviceBusNamespace + ':443/$servicebus/hybridconnection?action=listen&path=' + path;
|
||||
if ( token != null ) {
|
||||
uri = uri + '&token=' + encodeURIComponent(token);
|
||||
}
|
||||
return uri;
|
||||
}
|
|
@ -0,0 +1,240 @@
|
|||
/************************************************************************
|
||||
* Copyright 2010-2015 Brian McKelvey.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
***********************************************************************/
|
||||
|
||||
var crypto = require('crypto');
|
||||
var util = require('util');
|
||||
var url = require('url');
|
||||
var EventEmitter = require('events').EventEmitter;
|
||||
var WebSocketClient = require('websocket').client;
|
||||
var WebSocketConnection = require('websocket').connection;
|
||||
var wait = require('wait.for');
|
||||
|
||||
var headerValueSplitRegExp = /,\s*/;
|
||||
var headerParamSplitRegExp = /;\s*/;
|
||||
var headerSanitizeRegExp = /[\r\n]/g;
|
||||
var xForwardedForSeparatorRegExp = /,\s*/;
|
||||
var separators = [
|
||||
'(', ')', '<', '>', '@',
|
||||
',', ';', ':', '\\', '\"',
|
||||
'/', '[', ']', '?', '=',
|
||||
'{', '}', ' ', String.fromCharCode(9)
|
||||
];
|
||||
var controlChars = [String.fromCharCode(127) /* DEL */];
|
||||
for (var i=0; i < 31; i ++) {
|
||||
/* US-ASCII Control Characters */
|
||||
controlChars.push(String.fromCharCode(i));
|
||||
}
|
||||
|
||||
var cookieNameValidateRegEx = /([\x00-\x20\x22\x28\x29\x2c\x2f\x3a-\x3f\x40\x5b-\x5e\x7b\x7d\x7f])/;
|
||||
var cookieValueValidateRegEx = /[^\x21\x23-\x2b\x2d-\x3a\x3c-\x5b\x5d-\x7e]/;
|
||||
var cookieValueDQuoteValidateRegEx = /^"[^"]*"$/;
|
||||
var controlCharsAndSemicolonRegEx = /[\x00-\x20\x3b]/g;
|
||||
|
||||
var cookieSeparatorRegEx = /[;,] */;
|
||||
|
||||
|
||||
|
||||
function HybridConnectionsWebSocketRequest(resource, address, id, httpHeaders, serverConfig) {
|
||||
// Superclass Constructor
|
||||
EventEmitter.call(this);
|
||||
|
||||
this.resource = resource;
|
||||
this.address = address;
|
||||
this.id = id;
|
||||
this.httpRequest = {
|
||||
headers : {}
|
||||
};
|
||||
|
||||
for(var keys = Object.keys(httpHeaders), l = keys.length; l; --l)
|
||||
{
|
||||
this.httpRequest.headers[ keys[l-1].toLowerCase() ] = httpHeaders[ keys[l-1] ];
|
||||
}
|
||||
|
||||
this.serverConfig = serverConfig;
|
||||
this._resolved = false;
|
||||
}
|
||||
|
||||
util.inherits(HybridConnectionsWebSocketRequest, EventEmitter);
|
||||
|
||||
HybridConnectionsWebSocketRequest.prototype.readHandshake = function() {
|
||||
var self = this;
|
||||
var request = this.httpRequest;
|
||||
|
||||
// Decode URL
|
||||
this.resourceURL = url.parse(this.resource, true);
|
||||
|
||||
this.host = request.headers['host'];
|
||||
if (!this.host) {
|
||||
throw new Error('Client must provide a Host header.');
|
||||
}
|
||||
|
||||
this.key = request.headers['sec-websocket-key'];
|
||||
if (!this.key) {
|
||||
throw new Error('Client must provide a value for Sec-WebSocket-Key.');
|
||||
}
|
||||
|
||||
this.webSocketVersion = parseInt(request.headers['sec-websocket-version'], 10);
|
||||
|
||||
if (!this.webSocketVersion || isNaN(this.webSocketVersion)) {
|
||||
throw new Error('Client must provide a value for Sec-WebSocket-Version.');
|
||||
}
|
||||
|
||||
|
||||
// Protocol is optional.
|
||||
var protocolString = request.headers['sec-websocket-protocol'];
|
||||
this.protocolFullCaseMap = {};
|
||||
this.requestedProtocols = [];
|
||||
if (protocolString) {
|
||||
var requestedProtocolsFullCase = protocolString.split(headerValueSplitRegExp);
|
||||
requestedProtocolsFullCase.forEach(function(protocol) {
|
||||
var lcProtocol = protocol.toLocaleLowerCase();
|
||||
self.requestedProtocols.push(lcProtocol);
|
||||
self.protocolFullCaseMap[lcProtocol] = protocol;
|
||||
});
|
||||
}
|
||||
|
||||
if (!this.serverConfig.ignoreXForwardedFor &&
|
||||
request.headers['x-forwarded-for']) {
|
||||
var immediatePeerIP = this.remoteAddress;
|
||||
this.remoteAddresses = request.headers['x-forwarded-for']
|
||||
.split(xForwardedForSeparatorRegExp);
|
||||
this.remoteAddresses.push(immediatePeerIP);
|
||||
this.remoteAddress = this.remoteAddresses[0];
|
||||
}
|
||||
|
||||
// Extensions are optional.
|
||||
var extensionsString = request.headers['sec-websocket-extensions'];
|
||||
this.requestedExtensions = this.parseExtensions(extensionsString);
|
||||
|
||||
// Cookies are optional
|
||||
var cookieString = request.headers['cookie'];
|
||||
this.cookies = this.parseCookies(cookieString);
|
||||
};
|
||||
|
||||
HybridConnectionsWebSocketRequest.prototype.parseExtensions = function(extensionsString) {
|
||||
if (!extensionsString || extensionsString.length === 0) {
|
||||
return [];
|
||||
}
|
||||
var extensions = extensionsString.toLocaleLowerCase().split(headerValueSplitRegExp);
|
||||
extensions.forEach(function(extension, index, array) {
|
||||
var params = extension.split(headerParamSplitRegExp);
|
||||
var extensionName = params[0];
|
||||
var extensionParams = params.slice(1);
|
||||
extensionParams.forEach(function(rawParam, index, array) {
|
||||
var arr = rawParam.split('=');
|
||||
var obj = {
|
||||
name: arr[0],
|
||||
value: arr[1]
|
||||
};
|
||||
array.splice(index, 1, obj);
|
||||
});
|
||||
var obj = {
|
||||
name: extensionName,
|
||||
params: extensionParams
|
||||
};
|
||||
array.splice(index, 1, obj);
|
||||
});
|
||||
return extensions;
|
||||
};
|
||||
|
||||
// This function adapted from node-cookie
|
||||
// https://github.com/shtylman/node-cookie
|
||||
HybridConnectionsWebSocketRequest.prototype.parseCookies = function(str) {
|
||||
// Sanity Check
|
||||
if (!str || typeof(str) !== 'string') {
|
||||
return [];
|
||||
}
|
||||
|
||||
var cookies = [];
|
||||
var pairs = str.split(cookieSeparatorRegEx);
|
||||
|
||||
pairs.forEach(function(pair) {
|
||||
var eq_idx = pair.indexOf('=');
|
||||
if (eq_idx === -1) {
|
||||
cookies.push({
|
||||
name: pair,
|
||||
value: null
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
var key = pair.substr(0, eq_idx).trim();
|
||||
var val = pair.substr(++eq_idx, pair.length).trim();
|
||||
|
||||
// quoted values
|
||||
if ('"' === val[0]) {
|
||||
val = val.slice(1, -1);
|
||||
}
|
||||
|
||||
cookies.push({
|
||||
name: key,
|
||||
value: decodeURIComponent(val)
|
||||
});
|
||||
});
|
||||
|
||||
return cookies;
|
||||
};
|
||||
|
||||
HybridConnectionsWebSocketRequest.prototype.accept = function(acceptedProtocol, allowedOrigin, cookies, callback) {
|
||||
var req = this;
|
||||
var client = new WebSocketClient({tlsOptions: { rejectUnauthorized: false }});
|
||||
client.connect(this.address, null, null);
|
||||
client.on('connect', function(connection) {
|
||||
req.emit('requestAccepted', connection);
|
||||
callback(connection);
|
||||
});
|
||||
};
|
||||
|
||||
HybridConnectionsWebSocketRequest.prototype.reject = function(status, reason, extraHeaders, callback) {
|
||||
var req = this;
|
||||
var client = new WebSocketClient({tlsOptions: { rejectUnauthorized: false }});
|
||||
var rejectUri = this.address + "&statusCode=" + status + "&statusDescription" + encodeURIComponent(reason);
|
||||
|
||||
client.connect(rejectUri, null, null);
|
||||
client.on('error', function(event){
|
||||
this.emit('requestRejected', this);
|
||||
callback(event);
|
||||
});
|
||||
};
|
||||
|
||||
HybridConnectionsWebSocketRequest.prototype._handleSocketCloseBeforeAccept = function() {
|
||||
this._socketIsClosing = true;
|
||||
this._removeSocketCloseListeners();
|
||||
};
|
||||
|
||||
HybridConnectionsWebSocketRequest.prototype._removeSocketCloseListeners = function() {
|
||||
this.socket.removeListener('end', this._socketCloseHandler);
|
||||
this.socket.removeListener('close', this._socketCloseHandler);
|
||||
};
|
||||
|
||||
HybridConnectionsWebSocketRequest.prototype._verifyResolution = function() {
|
||||
if (this._resolved) {
|
||||
throw new Error('HybridConnectionsWebSocketRequest may only be accepted or rejected one time.');
|
||||
}
|
||||
};
|
||||
|
||||
function cleanupFailedConnection(connection) {
|
||||
// Since we have to return a connection object even if the socket is
|
||||
// already dead in order not to break the API, we schedule a 'close'
|
||||
// event on the connection object to occur immediately.
|
||||
process.nextTick(function() {
|
||||
// WebSocketConnection.CLOSE_REASON_ABNORMAL = 1006
|
||||
// Third param: Skip sending the close frame to a dead socket
|
||||
connection.drop(1006, 'TCP connection lost before handshake completed.', true);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = HybridConnectionsWebSocketRequest;
|
|
@ -0,0 +1,269 @@
|
|||
/************************************************************************
|
||||
* Copyright 2010-2015 Brian McKelvey.
|
||||
* Derivative Copyright Microsoft Corporation
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
***********************************************************************/
|
||||
|
||||
var extend = require('./utils').extend;
|
||||
var utils = require('./utils');
|
||||
var util = require('util');
|
||||
var EventEmitter = require('events').EventEmitter;
|
||||
var WebSocketClient = require('websocket').client;
|
||||
var WebSocketRequest = require('./HybridConnectionsWebSocketRequest');
|
||||
|
||||
var isDefinedAndNonNull = function (options, key) {
|
||||
return typeof options[key] != 'undefined' && options[key] !== null;
|
||||
};
|
||||
|
||||
var HybridConnectionsWebSocketServer = function HybridConnectionsWebSocketServer(config) {
|
||||
// Superclass Constructor
|
||||
EventEmitter.call(this);
|
||||
|
||||
this._handlers = {
|
||||
requestAccepted: this.handleRequestAccepted.bind(this),
|
||||
requestResolved: this.handleRequestResolved.bind(this)
|
||||
};
|
||||
this.pendingRequests = [];
|
||||
this.connections = [];
|
||||
if (config) {
|
||||
this.open(config);
|
||||
}
|
||||
};
|
||||
|
||||
util.inherits(HybridConnectionsWebSocketServer, EventEmitter);
|
||||
|
||||
HybridConnectionsWebSocketServer.prototype.open = function(config) {
|
||||
this.config = {
|
||||
// hybrid connection endpoint address
|
||||
server: null,
|
||||
// listen token
|
||||
token: null,
|
||||
// identifier
|
||||
id: null,
|
||||
|
||||
// If true, the server will automatically send a ping to all
|
||||
// connections every 'keepaliveInterval' milliseconds. The timer is
|
||||
// reset on any received data from the client.
|
||||
keepalive: true,
|
||||
|
||||
// The interval to send keepalive pings to connected clients if the
|
||||
// connection is idle. Any received data will reset the counter.
|
||||
keepaliveInterval: 20000,
|
||||
|
||||
// If true, the server will consider any connection that has not
|
||||
// received any data within the amount of time specified by
|
||||
// 'keepaliveGracePeriod' after a keepalive ping has been sent to
|
||||
// be dead, and will drop the connection.
|
||||
// Ignored if keepalive is false.
|
||||
dropConnectionOnKeepaliveTimeout: true,
|
||||
|
||||
// The amount of time to wait after sending a keepalive ping before
|
||||
// closing the connection if the connected peer does not respond.
|
||||
// Ignored if keepalive is false.
|
||||
keepaliveGracePeriod: 10000,
|
||||
|
||||
// Whether to use native TCP keep-alive instead of WebSockets ping
|
||||
// and pong packets. Native TCP keep-alive sends smaller packets
|
||||
// on the wire and so uses bandwidth more efficiently. This may
|
||||
// be more important when talking to mobile devices.
|
||||
// If this value is set to true, then these values will be ignored:
|
||||
// keepaliveGracePeriod
|
||||
// dropConnectionOnKeepaliveTimeout
|
||||
useNativeKeepalive: false,
|
||||
|
||||
// If true, fragmented messages will be automatically assembled
|
||||
// and the full message will be emitted via a 'message' event.
|
||||
// If false, each frame will be emitted via a 'frame' event and
|
||||
// the application will be responsible for aggregating multiple
|
||||
// fragmented frames. Single-frame messages will emit a 'message'
|
||||
// event in addition to the 'frame' event.
|
||||
// Most users will want to leave this set to 'true'
|
||||
assembleFragments: true,
|
||||
|
||||
// If this is true, websocket connections will be accepted
|
||||
// regardless of the path and protocol specified by the client.
|
||||
// The protocol accepted will be the first that was requested
|
||||
// by the client. Clients from any origin will be accepted.
|
||||
// This should only be used in the simplest of cases. You should
|
||||
// probably leave this set to 'false' and inspect the request
|
||||
// object to make sure it's acceptable before accepting it.
|
||||
autoAcceptConnections: false,
|
||||
|
||||
// Whether or not the X-Forwarded-For header should be respected.
|
||||
// It's important to set this to 'true' when accepting connections
|
||||
// from untrusted connections, as a malicious client could spoof its
|
||||
// IP address by simply setting this header. It's meant to be added
|
||||
// by a trusted proxy or other intermediary within your own
|
||||
// infrastructure.
|
||||
// See: http://en.wikipedia.org/wiki/X-Forwarded-For
|
||||
ignoreXForwardedFor: false,
|
||||
|
||||
// The Nagle Algorithm makes more efficient use of network resources
|
||||
// by introducing a small delay before sending small packets so that
|
||||
// multiple messages can be batched together before going onto the
|
||||
// wire. This however comes at the cost of latency, so the default
|
||||
// is to disable it. If you don't need low latency and are streaming
|
||||
// lots of small messages, you can change this to 'false'
|
||||
disableNagleAlgorithm: true,
|
||||
|
||||
// The number of milliseconds to wait after sending a close frame
|
||||
// for an acknowledgement to come back before giving up and just
|
||||
// closing the socket.
|
||||
closeTimeout: 5000
|
||||
};
|
||||
extend(this.config, config);
|
||||
|
||||
if (this.config.server) {
|
||||
// connect
|
||||
this.listenUri = config.server;
|
||||
if (isDefinedAndNonNull(config, 'id')) {
|
||||
this.listenUri = listenUri + '&id=' + config.id;
|
||||
}
|
||||
|
||||
connectControlChannel(this);
|
||||
}
|
||||
else {
|
||||
throw new Error('You must specify an hybrid connections server address on which to open the WebSocket server.');
|
||||
}
|
||||
};
|
||||
|
||||
HybridConnectionsWebSocketServer.prototype.closeAllConnections = function() {
|
||||
this.connections.forEach(function(connection) {
|
||||
connection.close();
|
||||
});
|
||||
};
|
||||
|
||||
HybridConnectionsWebSocketServer.prototype.broadcast = function(data) {
|
||||
if (Buffer.isBuffer(data)) {
|
||||
this.broadcastBytes(data);
|
||||
}
|
||||
else if (typeof(data.toString) === 'function') {
|
||||
this.broadcastUTF(data);
|
||||
}
|
||||
};
|
||||
|
||||
HybridConnectionsWebSocketServer.prototype.broadcastUTF = function(utfData) {
|
||||
this.connections.forEach(function(connection) {
|
||||
connection.sendUTF(utfData);
|
||||
});
|
||||
};
|
||||
|
||||
HybridConnectionsWebSocketServer.prototype.broadcastBytes = function(binaryData) {
|
||||
this.connections.forEach(function(connection) {
|
||||
connection.sendBytes(binaryData);
|
||||
});
|
||||
};
|
||||
|
||||
HybridConnectionsWebSocketServer.prototype.shutDown = function() {
|
||||
this.closeAllConnections();
|
||||
};
|
||||
|
||||
HybridConnectionsWebSocketServer.prototype.handleRequestAccepted = function(connection) {
|
||||
var self = this;
|
||||
connection.once('close', function(closeReason, description) {
|
||||
self.handleConnectionClose(connection, closeReason, description);
|
||||
});
|
||||
this.connections.push(connection);
|
||||
this.emit('connect', connection);
|
||||
};
|
||||
|
||||
HybridConnectionsWebSocketServer.prototype.handleRequestResolved = function(request) {
|
||||
var index = this.pendingRequests.indexOf(request);
|
||||
if (index !== -1) { this.pendingRequests.splice(index, 1); }
|
||||
};
|
||||
|
||||
HybridConnectionsWebSocketServer.prototype.handleConnectionClose = function(closeReason, description) {
|
||||
console.log(description);
|
||||
}
|
||||
|
||||
function connectControlChannel(server) {
|
||||
/* create the control connection */
|
||||
|
||||
var headers = null;
|
||||
if ( server.config.token != null) {
|
||||
headers = { 'ServiceBusAuthorization' : server.config.token};
|
||||
};
|
||||
|
||||
|
||||
var client = new WebSocketClient();
|
||||
client.connect(server.listenUri, null, null, headers);
|
||||
client.on('connect', function(connection) {
|
||||
server.controlChannel = connection;
|
||||
server.controlChannel.on('error', function (event) {
|
||||
server.emit('error', event);
|
||||
if (!closeRequested) {
|
||||
connectControlChannel(server);
|
||||
}
|
||||
});
|
||||
|
||||
server.controlChannel.on('close', function(event) {
|
||||
// reconnect
|
||||
if (!closeRequested)
|
||||
{
|
||||
connectControlChannel(server);
|
||||
} else {
|
||||
server.emit('close', server);
|
||||
}
|
||||
|
||||
});
|
||||
server.controlChannel.on('message', function (message) {
|
||||
if (message.type === 'utf8') {
|
||||
try {
|
||||
handleControl(server, JSON.parse(message.utf8Data));
|
||||
}
|
||||
catch(e) {
|
||||
// do nothing if there's an error.
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
client.on('connectFailed', function(event){});
|
||||
}
|
||||
|
||||
function handleControl(server,message) {
|
||||
if ( isDefinedAndNonNull(message, 'accept') ) {
|
||||
var address = message.accept.address;
|
||||
|
||||
var wsRequest = new WebSocketRequest(server.listenUri, message.accept.address, message.accept.id, message.accept.connectHeaders, server.config);
|
||||
try {
|
||||
wsRequest.readHandshake();
|
||||
}
|
||||
catch(e) {
|
||||
wsRequest.reject(
|
||||
e.httpCode ? e.httpCode : 400,
|
||||
e.message,
|
||||
e.headers
|
||||
);
|
||||
debug('Invalid handshake: %s', e.message);
|
||||
return;
|
||||
}
|
||||
|
||||
server.pendingRequests.push(wsRequest);
|
||||
|
||||
wsRequest.once('requestAccepted', server._handlers.requestAccepted);
|
||||
wsRequest.once('requestResolved', server._handlers.requestResolved);
|
||||
|
||||
if (!server.config.autoAcceptConnections && utils.eventEmitterListenerCount(server, 'request') > 0) {
|
||||
server.emit('request', wsRequest);
|
||||
}
|
||||
else if (server.config.autoAcceptConnections) {
|
||||
wsRequest.accept(wsRequest.requestedProtocols[0], wsRequest.origin);
|
||||
}
|
||||
else {
|
||||
wsRequest.reject(404, 'No handler is configured to accept the connection.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = HybridConnectionsWebSocketServer;
|
|
@ -0,0 +1,13 @@
|
|||
var websocket = require('websocket')
|
||||
|
||||
module.exports = {
|
||||
'server' : require('./HybridConnectionsWebSocketServer'),
|
||||
'client' : websocket.client,
|
||||
'router' : websocket.router,
|
||||
'frame' : websocket.frame,
|
||||
'request' : require('./HybridConnectionsWebSocketRequest'),
|
||||
'connection' : websocket.connection,
|
||||
'w3cwebsocket' : websocket.w3cwebsocket,
|
||||
'deprecation' : websocket.deprecation,
|
||||
'version' : require('./version')
|
||||
};
|
|
@ -0,0 +1,60 @@
|
|||
var noop = exports.noop = function(){};
|
||||
|
||||
exports.extend = function extend(dest, source) {
|
||||
for (var prop in source) {
|
||||
dest[prop] = source[prop];
|
||||
}
|
||||
};
|
||||
|
||||
exports.eventEmitterListenerCount =
|
||||
require('events').EventEmitter.listenerCount ||
|
||||
function(emitter, type) { return emitter.listeners(type).length; };
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
exports.BufferingLogger = function createBufferingLogger(identifier, uniqueID) {
|
||||
var logFunction = require('debug')(identifier);
|
||||
if (logFunction.enabled) {
|
||||
var logger = new BufferingLogger(identifier, uniqueID, logFunction);
|
||||
var debug = logger.log.bind(logger);
|
||||
debug.printOutput = logger.printOutput.bind(logger);
|
||||
debug.enabled = logFunction.enabled;
|
||||
return debug;
|
||||
}
|
||||
logFunction.printOutput = noop;
|
||||
return logFunction;
|
||||
};
|
||||
|
||||
function BufferingLogger(identifier, uniqueID, logFunction) {
|
||||
this.logFunction = logFunction;
|
||||
this.identifier = identifier;
|
||||
this.uniqueID = uniqueID;
|
||||
this.buffer = [];
|
||||
}
|
||||
|
||||
BufferingLogger.prototype.log = function() {
|
||||
this.buffer.push([ new Date(), Array.prototype.slice.call(arguments) ]);
|
||||
return this;
|
||||
};
|
||||
|
||||
BufferingLogger.prototype.clear = function() {
|
||||
this.buffer = [];
|
||||
return this;
|
||||
};
|
||||
|
||||
BufferingLogger.prototype.printOutput = function(logFunction) {
|
||||
if (!logFunction) { logFunction = this.logFunction; }
|
||||
var uniqueID = this.uniqueID;
|
||||
this.buffer.forEach(function(entry) {
|
||||
var date = entry[0].toLocaleString();
|
||||
var args = entry[1].slice();
|
||||
var formatString = args[0];
|
||||
if (formatString !== (void 0) && formatString !== null) {
|
||||
formatString = '%s - %s - ' + formatString.toString();
|
||||
args.splice(0, 1, formatString, date, uniqueID);
|
||||
logFunction.apply(global, args);
|
||||
}
|
||||
});
|
||||
};
|
|
@ -0,0 +1 @@
|
|||
module.exports = require('../package.json').version;
|
|
@ -0,0 +1,45 @@
|
|||
{
|
||||
"name": "hyco-websocket",
|
||||
"description": "",
|
||||
"keywords": [
|
||||
"websocket",
|
||||
"websockets",
|
||||
"socket",
|
||||
"networking",
|
||||
"comet",
|
||||
"push",
|
||||
"RFC-6455",
|
||||
"realtime",
|
||||
"server",
|
||||
"client"
|
||||
],
|
||||
"author": "",
|
||||
"version": "1.0.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/clemensv/azure-relay-hybridconnections.git"
|
||||
},
|
||||
"homepage": "https://azure.com",
|
||||
"engines": {
|
||||
"node": ">=0.8.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"wait.for" : "latest",
|
||||
"websocket": "latest",
|
||||
"crypto": "latest",
|
||||
"moment": "latest"
|
||||
},
|
||||
"devDependencies": {
|
||||
},
|
||||
"config": {
|
||||
"verbose": false
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
},
|
||||
"main": "index",
|
||||
"directories": {
|
||||
"lib": "./lib"
|
||||
},
|
||||
"license": "Apache-2.0"
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
npm-debug.log
|
||||
node_modules
|
||||
.*.swp
|
||||
.lock-*
|
||||
build
|
||||
coverage
|
||||
|
||||
builderror.log
|
|
@ -0,0 +1,7 @@
|
|||
.npmignore
|
||||
.gitignore
|
||||
|
||||
example/
|
||||
build/
|
||||
test/
|
||||
node_modules/
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"author": "",
|
||||
"name": "serverstats",
|
||||
"version": "0.0.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/clemensv/azure-hybridconnections.git"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">0.4.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"hyco-ws": "latest",
|
||||
"express": "4.x",
|
||||
"mustache-express" : "latest"
|
||||
},
|
||||
"devDependencies": {},
|
||||
"optionalDependencies": {}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
body {
|
||||
font-family: Tahoma, Geneva, sans-serif;
|
||||
}
|
||||
|
||||
div {
|
||||
display: inline;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
function updateStats(memuse) {
|
||||
document.getElementById('rss').innerHTML = memuse.rss;
|
||||
document.getElementById('heapTotal').innerHTML = memuse.heapTotal;
|
||||
document.getElementById('heapUsed').innerHTML = memuse.heapUsed;
|
||||
}
|
||||
|
||||
var host = window.document.location.host.replace(/:.*/, '');
|
||||
var ws = new WebSocket('wss://{{ns}}:443/$servicebus/hybridconnection?action=connect&path={{path}}&token={{token}}');
|
||||
ws.onmessage = function (event) {
|
||||
updateStats(JSON.parse(event.data));
|
||||
};
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<strong>Server Stats</strong><br>
|
||||
RSS: <div id='rss'></div><br>
|
||||
Heap total: <div id='heapTotal'></div><br>
|
||||
Heap used: <div id='heapUsed'></div><br>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,37 @@
|
|||
if ( process.argv.length < 6) {
|
||||
console.log("listener.js [namespace] [path] [key-rule] [key]");
|
||||
} else {
|
||||
var WebSocket = require('hyco-ws')
|
||||
, http = require('http')
|
||||
, express = require('express')
|
||||
, mustache = require('mustache-express')
|
||||
, app = express();
|
||||
|
||||
var _ns = process.argv[2];
|
||||
var _path = process.argv[3];
|
||||
var _keyrule = process.argv[4];
|
||||
var _key = process.argv[5];
|
||||
|
||||
app.engine('html', mustache());
|
||||
app.set('view engine', 'mustache');
|
||||
app.set('views', __dirname + '/public');
|
||||
app.get('/', function (req, res) {
|
||||
res.render('index.html', { ns : _ns, path : _path, token : encodeURIComponent(WebSocket.createRelayToken('http://'+_ns, _keyrule, _key)) });
|
||||
});
|
||||
app.listen(8080);
|
||||
|
||||
WebSocket.createRelayedServer(
|
||||
{
|
||||
server : WebSocket.createRelayListenUri(_ns, _path),
|
||||
token: WebSocket.createRelayToken('http://'+_ns, _keyrule, _key)
|
||||
}, function(ws) {
|
||||
var id = setInterval(function() {
|
||||
ws.send(JSON.stringify(process.memoryUsage()), function() { /* ignore errors */ });
|
||||
}, 100);
|
||||
console.log('started client interval');
|
||||
ws.on('close', function() {
|
||||
console.log('stopping client interval');
|
||||
clearInterval(id);
|
||||
});
|
||||
});
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
|
||||
if ( process.argv.length < 6) {
|
||||
console.log("listener.js [namespace] [path] [key-rule] [key]");
|
||||
} else {
|
||||
|
||||
var ns = process.argv[2];
|
||||
var path = process.argv[3];
|
||||
var keyrule = process.argv[4];
|
||||
var key = process.argv[5];
|
||||
|
||||
var WebSocket = require('hyco-ws')
|
||||
|
||||
var wss = WebSocket.createRelayedServer(
|
||||
{
|
||||
server : WebSocket.createRelayListenUri(ns, path),
|
||||
token: WebSocket.createRelayToken('http://'+ns, keyrule, key)
|
||||
},
|
||||
function (ws) {
|
||||
console.log('connection accepted');
|
||||
ws.onmessage = function (event) {
|
||||
console.log(JSON.parse(event.data));
|
||||
};
|
||||
ws.on('close', function () {
|
||||
console.log('connection closed');
|
||||
});
|
||||
});
|
||||
|
||||
wss.on('error', function(err) {
|
||||
console.log('error' + err);
|
||||
});
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"author": "",
|
||||
"name": "simple-externalpkg",
|
||||
"version": "0.0.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/clemensv/azure-hybridconnections.git"
|
||||
},
|
||||
"dependencies": {
|
||||
"hyco-ws": "latest"
|
||||
},
|
||||
"devDependencies": {},
|
||||
"optionalDependencies": {}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
if (process.argv.length < 6) {
|
||||
console.log("listener.js [namespace] [path] [key-rule] [key]");
|
||||
} else {
|
||||
|
||||
var ns = process.argv[2];
|
||||
var path = process.argv[3];
|
||||
var keyrule = process.argv[4];
|
||||
var key = process.argv[5];
|
||||
|
||||
var WebSocket = require('hyco-ws')
|
||||
|
||||
WebSocket.relayedConnect(
|
||||
WebSocket.createRelaySendUri(ns, path),
|
||||
WebSocket.createRelayToken('http://'+ns, keyrule, key),
|
||||
function (wss) {
|
||||
var id = setInterval(function () {
|
||||
wss.send(JSON.stringify(process.memoryUsage()), function () { /* ignore errors */ });
|
||||
}, 100);
|
||||
|
||||
console.log('Started client interval. Press any key to stop.');
|
||||
wss.on('close', function () {
|
||||
console.log('stopping client interval');
|
||||
clearInterval(id);
|
||||
process.exit();
|
||||
});
|
||||
|
||||
process.stdin.setRawMode(true);
|
||||
process.stdin.resume();
|
||||
process.stdin.on('data', function () {
|
||||
wss.close();
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
|
||||
if ( process.argv.length < 6) {
|
||||
console.log("listener.js [namespace] [path] [key-rule] [key]");
|
||||
} else {
|
||||
|
||||
var ns = process.argv[2];
|
||||
var path = process.argv[3];
|
||||
var keyrule = process.argv[4];
|
||||
var key = process.argv[5];
|
||||
|
||||
var WebSocket = require('../../')
|
||||
|
||||
var wss = WebSocket.createRelayedServer(
|
||||
{
|
||||
server : WebSocket.createRelayListenUri(ns, path),
|
||||
token: WebSocket.createRelayToken('http://'+ns, keyrule, key)
|
||||
},
|
||||
function (ws) {
|
||||
console.log('connection accepted');
|
||||
ws.onmessage = function (event) {
|
||||
console.log(JSON.parse(event.data));
|
||||
};
|
||||
ws.on('close', function () {
|
||||
console.log('connection closed');
|
||||
});
|
||||
});
|
||||
|
||||
wss.on('error', function(err) {
|
||||
console.log('error' + err);
|
||||
});
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"author": "",
|
||||
"name": "simple",
|
||||
"version": "0.0.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/clemensv/azure-hybridconnections.git"
|
||||
},
|
||||
"devDependencies": {},
|
||||
"optionalDependencies": {}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
if (process.argv.length < 6) {
|
||||
console.log("listener.js [namespace] [path] [key-rule] [key]");
|
||||
} else {
|
||||
|
||||
var ns = process.argv[2];
|
||||
var path = process.argv[3];
|
||||
var keyrule = process.argv[4];
|
||||
var key = process.argv[5];
|
||||
|
||||
var WebSocket = require('../../')
|
||||
|
||||
WebSocket.relayedConnect(
|
||||
WebSocket.createRelaySendUri(ns, path),
|
||||
WebSocket.createRelayToken('http://'+ns, keyrule, key),
|
||||
function (wss) {
|
||||
var id = setInterval(function () {
|
||||
wss.send(JSON.stringify(process.memoryUsage()), function () { /* ignore errors */ });
|
||||
}, 100);
|
||||
|
||||
console.log('Started client interval. Press any key to stop.');
|
||||
wss.on('close', function () {
|
||||
console.log('stopping client interval');
|
||||
clearInterval(id);
|
||||
process.exit();
|
||||
});
|
||||
|
||||
process.stdin.setRawMode(true);
|
||||
process.stdin.resume();
|
||||
process.stdin.on('data', function () {
|
||||
wss.close();
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
|
@ -0,0 +1,88 @@
|
|||
'use strict';
|
||||
|
||||
var crypto = require('crypto')
|
||||
var moment = require('moment')
|
||||
|
||||
/*!
|
||||
* Adapted from
|
||||
* ws: a node.js websocket client
|
||||
* Copyright(c) 2011 Einar Otto Stangvik <einaros@gmail.com>
|
||||
* MIT Licensed
|
||||
*
|
||||
*/
|
||||
|
||||
var WS = module.exports = require('ws');
|
||||
|
||||
WS.RelayedServer = require('./lib/HybridConnectionWebSocketServer');
|
||||
|
||||
/**
|
||||
* Create a new WebSocket server.
|
||||
*
|
||||
* @param {Object} options Server options
|
||||
* @param {Function} fn Optional connection listener.
|
||||
* @returns {WS.Server}
|
||||
* @api public
|
||||
*/
|
||||
WS.createRelayedServer = function createRelayedServer(options, fn) {
|
||||
var server = new WS.RelayedServer(options);
|
||||
|
||||
if (typeof fn === 'function') {
|
||||
server.on('connection', fn);
|
||||
}
|
||||
|
||||
return server;
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a new WebSocket connection.
|
||||
*
|
||||
* @param {String} address The URL/address we need to connect to.
|
||||
* @param {Function} fn Open listener.
|
||||
* @returns {WS}
|
||||
* @api public
|
||||
*/
|
||||
WS.relayedConnect = WS.createRelayedConnection = function relayedConnect(address, token, fn) {
|
||||
var opt = null;
|
||||
if ( token != null) {
|
||||
opt = { headers : { 'ServiceBusAuthorization' : token}};
|
||||
};
|
||||
var client = new WS(address, null, opt);
|
||||
|
||||
if (typeof fn === 'function') {
|
||||
client.on('open', function() { fn(client) });
|
||||
}
|
||||
|
||||
return client;
|
||||
};
|
||||
|
||||
WS.createRelayToken = function createRelayToken(uri, key_name, key) {
|
||||
|
||||
// Token expires in one hour
|
||||
var expiry = moment().add(1, 'hours').unix();
|
||||
|
||||
var string_to_sign = encodeURIComponent(uri) + '\n' + expiry;
|
||||
var hmac = crypto.createHmac('sha256', key);
|
||||
hmac.update(string_to_sign);
|
||||
var signature = hmac.digest('base64');
|
||||
var token = 'SharedAccessSignature sr=' + encodeURIComponent(uri) + '&sig=' + encodeURIComponent(signature) + '&se=' + expiry + '&skn=' + key_name;
|
||||
|
||||
return token;
|
||||
};
|
||||
|
||||
WS.createRelaySendUri = function createRelaySendUri(serviceBusNamespace, path, token)
|
||||
{
|
||||
var uri = 'wss://' + serviceBusNamespace + ':443/$servicebus/hybridconnection?action=connect&path=' + path;
|
||||
if ( token != null ) {
|
||||
uri = uri + '&token=' + encodeURIComponent(token);
|
||||
}
|
||||
return uri;
|
||||
}
|
||||
|
||||
WS.createRelayListenUri = function createRelayListenUri(serviceBusNamespace, path, token)
|
||||
{
|
||||
var uri = 'wss://' + serviceBusNamespace + ':443/$servicebus/hybridconnection?action=listen&path=' + path;
|
||||
if ( token != null ) {
|
||||
uri = uri + '&token=' + encodeURIComponent(token);
|
||||
}
|
||||
return uri;
|
||||
}
|
|
@ -0,0 +1,267 @@
|
|||
'use strict';
|
||||
|
||||
const util = require('util');
|
||||
const EventEmitter = require('events');
|
||||
const http = require('http');
|
||||
const crypto = require('crypto');
|
||||
const WebSocket = require('ws');
|
||||
const url = require('url');
|
||||
|
||||
// slightly awful workaround to pull submodules
|
||||
var wsc = require.cache[require.resolve('ws')]
|
||||
const Extensions = wsc.require('./lib/Extensions');
|
||||
const PerMessageDeflate = wsc.require('./lib/PerMessageDeflate');
|
||||
|
||||
|
||||
var isDefinedAndNonNull = function (options, key) {
|
||||
return typeof options[key] != 'undefined' && options[key] !== null;
|
||||
};
|
||||
|
||||
/**
|
||||
* WebSocket Server implementation
|
||||
*/
|
||||
|
||||
function HybridConnectionsWebSocketServer(options, callback) {
|
||||
if (this instanceof HybridConnectionsWebSocketServer === false) {
|
||||
return new HybridConnectionsWebSocketServer(options, callback);
|
||||
}
|
||||
|
||||
EventEmitter.call(this);
|
||||
|
||||
options = Object.assign({
|
||||
server: null,
|
||||
token: null,
|
||||
id: null,
|
||||
verifyClient: null,
|
||||
handleProtocols: null,
|
||||
disableHixie: false,
|
||||
clientTracking: true,
|
||||
perMessageDeflate: true,
|
||||
maxPayload: 100 * 1024 * 1024,
|
||||
backlog: null // use default (511 as implemented in net.js)
|
||||
}, options);
|
||||
|
||||
if (!isDefinedAndNonNull(options, 'server')) {
|
||||
throw new TypeError('\'server\' must be provided');
|
||||
}
|
||||
|
||||
var self = this;
|
||||
|
||||
this.listenUri = options.server;
|
||||
if (isDefinedAndNonNull(options, 'id')) {
|
||||
this.listenUri = listenUri + '&id=' + options.id;
|
||||
}
|
||||
|
||||
this.closeRequested = false;
|
||||
this.options = options;
|
||||
this.path = options.path;
|
||||
this.clients = [];
|
||||
|
||||
connectControlChannel(this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Inherits from EventEmitter.
|
||||
*/
|
||||
|
||||
util.inherits(HybridConnectionsWebSocketServer, EventEmitter);
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Immediately shuts down the connection.
|
||||
*
|
||||
* @api public
|
||||
*/
|
||||
|
||||
HybridConnectionsWebSocketServer.prototype.close = function (callback) {
|
||||
this.closeRequested = true;
|
||||
// terminate all associated clients
|
||||
var error = null;
|
||||
try {
|
||||
for (var i = 0, l = this.clients.length; i < l; ++i) {
|
||||
this.clients[i].close();
|
||||
}
|
||||
this.controlChannel.close();
|
||||
}
|
||||
catch (e) {
|
||||
error = e;
|
||||
}
|
||||
|
||||
if (callback)
|
||||
callback(error);
|
||||
else if (error)
|
||||
throw error;
|
||||
}
|
||||
|
||||
|
||||
function connectControlChannel(server) {
|
||||
/* create the control connection */
|
||||
|
||||
var opt = null;
|
||||
if (server.options.token != null) {
|
||||
opt = { headers: { 'ServiceBusAuthorization': server.options.token } };
|
||||
};
|
||||
server.controlChannel = new WebSocket(server.listenUri, null, opt);
|
||||
|
||||
server.controlChannel.onerror = function (event) {
|
||||
server.emit('error', event);
|
||||
if (!server.closeRequested) {
|
||||
connectControlChannel(server);
|
||||
}
|
||||
}
|
||||
|
||||
server.controlChannel.onopen = function (event) {
|
||||
server.emit('listening');
|
||||
}
|
||||
|
||||
server.controlChannel.onclose = function (event) {
|
||||
// reconnect
|
||||
if (!server.closeRequested) {
|
||||
connectControlChannel(server);
|
||||
} else {
|
||||
server.emit('close', server);
|
||||
}
|
||||
|
||||
}
|
||||
server.controlChannel.onmessage = function (event) {
|
||||
var message = JSON.parse(event.data);
|
||||
if (isDefinedAndNonNull(message, 'accept')) {
|
||||
accept(server, message);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function accept(server, message) {
|
||||
var address = message.accept.address;
|
||||
var req = { headers: {} };
|
||||
var headers = [];
|
||||
|
||||
for (var keys = Object.keys(message.accept.connectHeaders), l = keys.length; l; --l) {
|
||||
req.headers[keys[l - 1].toLowerCase()] = message.accept.connectHeaders[keys[l - 1]];
|
||||
}
|
||||
// verify key presence
|
||||
if (!req.headers['sec-websocket-key']) {
|
||||
abortConnection(message, 400, 'Bad Request');
|
||||
return;
|
||||
}
|
||||
|
||||
// verify version
|
||||
var version = parseInt(req.headers['sec-websocket-version']);
|
||||
// verify protocol
|
||||
var protocols = req.headers['sec-websocket-protocol'];
|
||||
|
||||
// verify client
|
||||
var origin = version < 13 ?
|
||||
req.headers['sec-websocket-origin'] :
|
||||
req.headers['origin'];
|
||||
|
||||
// handle extensions offer
|
||||
var extensionsOffer = Extensions.parse(req.headers['sec-websocket-extensions']);
|
||||
|
||||
// handler to call when the connection sequence completes
|
||||
var self = server;
|
||||
var completeHybiUpgrade2 = function (protocol) {
|
||||
|
||||
if (typeof protocol != 'undefined') {
|
||||
headers.push('Sec-WebSocket-Protocol: ' + protocol);
|
||||
}
|
||||
|
||||
var extensions = {};
|
||||
try {
|
||||
extensions = acceptExtensions.call(self, extensionsOffer);
|
||||
} catch (err) {
|
||||
abortConnection(message, 400, 'Bad Request');
|
||||
return;
|
||||
}
|
||||
|
||||
if (Object.keys(extensions).length) {
|
||||
var serverExtensions = {};
|
||||
Object.keys(extensions).forEach(function (token) {
|
||||
serverExtensions[token] = [extensions[token].params]
|
||||
});
|
||||
headers.push('Sec-WebSocket-Extensions: ' + Extensions.format(serverExtensions));
|
||||
}
|
||||
|
||||
// allows external modification/inspection of handshake headers
|
||||
self.emit('headers', headers);
|
||||
|
||||
var client = new WebSocket(address, null, { rejectUnauthorized: false, headers: headers });
|
||||
server.clients.push(client);
|
||||
|
||||
client.on('close', function () {
|
||||
var index = server.clients.indexOf(client);
|
||||
if (index != -1) {
|
||||
server.clients.splice(index, 1);
|
||||
}
|
||||
});
|
||||
|
||||
server.emit('connection', client);
|
||||
if (self.options.clientTracking) {
|
||||
self.clients.push(client);
|
||||
client.on('close', function () {
|
||||
var index = self.clients.indexOf(client);
|
||||
if (index != -1) {
|
||||
self.clients.splice(index, 1);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// optionally call external protocol selection handler before
|
||||
// calling completeHybiUpgrade2
|
||||
var completeHybiUpgrade1 = function () {
|
||||
// choose from the sub-protocols
|
||||
if (typeof self.options.handleProtocols == 'function') {
|
||||
var protList = (protocols || '').split(/, */);
|
||||
var callbackCalled = false;
|
||||
self.options.handleProtocols(protList, function (result, protocol) {
|
||||
callbackCalled = true;
|
||||
if (!result) abortConnection(socket, 401, 'Unauthorized');
|
||||
else completeHybiUpgrade2(protocol);
|
||||
});
|
||||
if (!callbackCalled) {
|
||||
// the handleProtocols handler never called our callback
|
||||
abortConnection(socket, 501, 'Could not process protocols');
|
||||
}
|
||||
return;
|
||||
} else {
|
||||
if (typeof protocols !== 'undefined') {
|
||||
completeHybiUpgrade2(protocols.split(/, */)[0]);
|
||||
}
|
||||
else {
|
||||
completeHybiUpgrade2();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
completeHybiUpgrade1();
|
||||
}
|
||||
|
||||
function acceptExtensions(offer) {
|
||||
var extensions = {};
|
||||
var options = this.options.perMessageDeflate;
|
||||
var maxPayload = this.options.maxPayload;
|
||||
if (options && offer[PerMessageDeflate.extensionName]) {
|
||||
var perMessageDeflate = new PerMessageDeflate(options !== true ? options : {}, true, maxPayload);
|
||||
perMessageDeflate.accept(offer[PerMessageDeflate.extensionName]);
|
||||
extensions[PerMessageDeflate.extensionName] = perMessageDeflate;
|
||||
}
|
||||
return extensions;
|
||||
}
|
||||
|
||||
function abortConnection(message, status, reason) {
|
||||
|
||||
var client = new WebSocketClient({ tlsOptions: { rejectUnauthorized: false } });
|
||||
var rejectUri = message.address + "&statusCode=" + status + "&statusDescription" + encodeURIComponent(reason);
|
||||
|
||||
client.connect(rejectUri, null, null);
|
||||
client.on('error', function (connection) {
|
||||
this.emit('requestRejected', this);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
module.exports = HybridConnectionsWebSocketServer
|
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"author": "xyz",
|
||||
"name": "hyco-ws",
|
||||
"description": "xyz",
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"main": "index.js",
|
||||
"keywords": [
|
||||
"Hixie",
|
||||
"HyBi",
|
||||
"Push",
|
||||
"RFC-6455",
|
||||
"WebSocket",
|
||||
"WebSockets",
|
||||
"real-time"
|
||||
],
|
||||
"repository" :
|
||||
{
|
||||
"type" : "git",
|
||||
"url" : "https://github.com/clemensv/azure-relay-hybridconnections.git"
|
||||
},
|
||||
"dependencies": {
|
||||
"ws": "1.1.x",
|
||||
"crypto": "latest",
|
||||
"moment": "latest"
|
||||
},
|
||||
"gypfile": true,
|
||||
"private" : false
|
||||
}
|
Загрузка…
Ссылка в новой задаче