Commit 45ab6965 authored by Steve Johnson's avatar Steve Johnson

updated README

parent 6c05f072
......@@ -47,7 +47,7 @@ The bridge (non-SSL) running on your server should ideally be only a backup - th
Usage examples: (configure in your forever script or run from the command line)
# Standard bridge on port 1338
/usr/local/bin/node /.../ws2bridge-ssl.js -config /.../ws2bridge.config
/usr/local/bin/node /.../ws2bridge.js -config /.../ws2bridge.config
# SSL bridge on port 1339
/usr/local/bin/node /.../ws2bridge-ssl.js -ssl -config /.../ws2bridge-html5-ssl.config
/////////////////////////////////////////////////////////////////////////////////
//
// WS2Bridge Server
//
// First cut at WS bridge for Remote and Personal STB prototype.
// This is CableLabs Confidential information and should not be disclosed outside of CableLabs.
//
// 3/15/13 - (SJ) Added NSDIRECTORY code to directly query WebSocketServer for device and application lists, and be notified of changes.
//
/////////////////////////////////////////////////////////////////////////////////
// Common Modules
var fs = require('fs');
// 'hack' to fix fs.exists in different versions of Node's FS module
fs.exists = fs.exists || require('path').exists;
fs.existsSync = fs.existsSync || require('path').existsSync;
var os=require('os');
// TODO: We need a better way to specify the network interface. Could be wired or wireless. For now, just
// pick the first one that is not local (lo). Use config file?
var ipaddr = '';
var ifaces=os.networkInterfaces();
for (var dev in ifaces) {
var alias=0;
ifaces[dev].forEach(function(details){
if (details.family=='IPv4') {
console.log(dev+(alias?':'+alias:''),details.address);
++alias;
dev = dev.substring(0,2);
if (dev != 'lo') {
if (ipaddr == '') {
ipaddr = details.address;
}
}
}
});
}
// Defaults
var configPath = './ws2bridge.config';
var scheme = 'ws';
// Command line args
for (var index = 2; index < process.argv.length; index++) {
var val = process.argv[index];
if ( val == "-config") {
index++;
if (process.argv[index] != undefined) {
configPath = process.argv[index];
}
else {
console.log("missing config file path");
}
}
else if (val == "-scheme") {
index++;
if (process.argv[index] != undefined) {
if (process.argv[index] == 'ws' || process.argv[index] == 'wss') {
scheme = process.argv[index];
}
else {
console.log("Invalid scheme: "+ process.argv[index]);
}
}
else {
console.log("missing scheme name");
}
}
}
var WebSocketServer = require('ws').Server,
http = require('https'),
fs = require('fs'),
url = require('url'),
mime = require('mime'),
querystring = require('querystring'),
//sys = require('sys'),
//spawn = require('child_process').spawn,
logfile = '/var/log/ws2bridge.log',
keepaliveLimit = 8, // Seconds until death if no keepalive.
serviceType = 'urn:cablelabs:html5:service:wsbridge:1';
// Config file
var config = {};
(function(){
fs.exists(configPath, function(exists) {
if (exists) {
fs.readFile( configPath, function(err, data) {
if(err) {
return;
}
config = JSON.parse(data);
// TODO: Validate config parameters, esp required.
if (config.scheme != undefined) {
scheme = config.scheme;
}
// Start the servers
startup();
});
}
else {
console.log("File not found: "+configPath);
exit(1);
}
});
})();
// Proper exit
process.on('SIGTERM', function(){
console.log('process received SIGTERM: terminating');
process.exit(1);
});
var server = http.createServer(function(request, response) {
var path = url.parse(request.url).pathname;
var fileRoot = __dirname + '/wsroot';
var relativePath = fileRoot + path;
switch (path) {
case '/dd.xml':
// Device description
sendDeviceDescription(response);
break;
case '/getshareddevices':
var query = url.parse(request.url).query, appType = querystring.parse(query).type;
response.writeHead(200, {"Content-Type": "text/plain"});
if ( query == null ) {
response.end(JSON.stringify(advertisedApps.getDevNames(request.connection.remoteAddress, appType)));
} else {
response.end(JSON.stringify(advertisedApps.getDevices(request.connection.remoteAddress)));
}
break;
case '/restart':
serveFile( fileRoot + "/monitorRestart.html", response);
setTimeout(function(){
process.kill(process.pid, 'SIGTERM');
},500);
break;
case '/ready':
response.writeHead(200, { 'Content-Type': 'text/html' });
response.end('<html><body>Ready</body></html>', "utf-8");
break;
// TODO: make this a dynamically updating page
case '/status':
response.writeHead(200, { 'Content-Type': 'text/html' });
response.end('<html><body>Bridge Status</body></html>', "utf-8");
break;
case '/xxxxxxlogtail':
response.writeHead(200, {'Content-Type': "text/plain;charset=UTF-8"});
tail.stdout.on("data", function (data) {
if (typeof data != 'string') {
data = data.toString();
}
response.end(data, "utf-8");
});
break;
default:
// First check to see if this is a file
fs.exists(relativePath, function(exists) {
if (exists) {
fs.readFile( relativePath, function(err, file) {
if(err) {
console.log("Error on readfile for " + relativePath);
return;
}
response.writeHead(200, { 'Content-Type': mime.lookup(relativePath) });
response.end(file, "utf-8");
});
}
else {
console.log("Error 404: unknown http request for " + path);
response.writeHead(404, { 'Content-Type': 'text/html' });
response.end('<html><body>File not found</body></html>', "utf-8");
}
});
}
});
// Moved to startup()
//server.listen(webSocketServerPort, function() {
// console.log('Server is running on port ' + webSocketServerPort);
//});
/*
* AdervistedApps contains apps that advertised by opening a WS connection
* Var apps contains the information and is structured as
* { WAN_IP1 :{ app1 :['DeviceName1',...],app2 :...},
* WAN_IP2 :
* ...
* }
* add() adds DeviceName for app @ WAN_IP
* remove() deletes DeviceName for app @ WAN_IP
* getDevNames() returns the array of DeviceNames for app @ WAN_IP
*/
function AdvertisedApps() {
var apps = {};
var devs = {};
this.add = function(remoteAddress, appName, devName) {
if(apps[remoteAddress] == undefined) {
apps[remoteAddress] = {};
}
if(apps[remoteAddress][appName] == undefined) {
apps[remoteAddress][appName] = [];
}
if(devs[remoteAddress] == undefined) {
devs[remoteAddress] = {};
}
if(devs[remoteAddress][devName] == undefined) {
devs[remoteAddress][devName] = [];
}
apps[remoteAddress][appName].push(devName);
devs[remoteAddress][devName].push(appName);
return true;
};
this.remove = function(remoteAddress, name) {
var devName = name.split(':')[0],
appName = name.split(':')[1];
if(typeof(devName) != 'string' || typeof(appName) != 'string'
|| devName == '' || appName == '') {
return false;
}
var appDevices = apps[remoteAddress][appName];
// console.log('remove: ' + devName + 'from ' + appDevices);
appDevices.splice(appDevices.indexOf(devName), 1);
if (appDevices.length == 0) {
delete apps[remoteAddress][appName];
if (Object.keys(apps[remoteAddress]).length == 0 ) {
delete apps[remoteAddress];
}
}
var deviceApps = devs[remoteAddress][devName];
// console.log('remove: ' + appName + 'from ' + deviceApps);
deviceApps.splice(deviceApps.indexOf(appName), 1);
if (deviceApps.length == 0) {
delete devs[remoteAddress][devName];
if (Object.keys(devs[remoteAddress]).length == 0 ) {
delete devs[remoteAddress];
}
}
return true;
};
this.remoteAddresses = function() {
var remoteAddresses = [];
for (var ipAddr in apps) {
remoteAddresses.push(ipAddr);
}
return remoteAddresses;
}
this.getDevNames = function(ipAddr, appName) {
if(apps[ipAddr] != undefined) {
if(apps[ipAddr][appName] != undefined) {
return apps[ipAddr][appName];
}
}
return [];
};
// Return a device or list of devices and the apps they are sharing
// We only return advertised applications that do not have a remote connection
// This needs to be revisited for one to many bridges.
this.getDevices = function(ipAddr, device) {
var devices = {};
if(devs[ipAddr] != undefined) {
for (var dev in devs[ipAddr]) {
devices[dev] = [];
var sharedApps = devs[ipAddr][dev];
if (sharedApps != undefined) {
for (var i=0; i < sharedApps.length; i++) {
var sharedApp = sharedApps[i];
if ( isBridgeConnected(ipAddr, dev, sharedApp) == false) {
devices[dev].push(sharedApp);
}
}
}
}
}
return devices;
}
// Return an app or list of apps and devices sharing the app
// We only return advertised applications that do not have a remote connection
// This needs to be revisited for one to many bridges.
this.getApplications = function(ipAddr) {
var applications = {};
if(apps[ipAddr] != undefined) {
for (var app in apps[ipAddr]) {
var sharedDevices = apps[ipAddr][app];
if (sharedDevices != undefined) {
for (var i=0; i < sharedDevices.length; i++) {
var sharedDevice = sharedDevices[i];
if ( isBridgeConnected(ipAddr, sharedDevice, app) == false) {
if (applications[app] == undefined) {
applications[app] = [];
}
applications[app].push(sharedDevice);
}
}
}
}
}
return applications;
}
}
function isBridgeConnected(ipaddr, devName, appName) {
var bridge = devName + ':' + appName;
if (bridgeClients[ipaddr] == undefined) {
return false;
}
else if (bridgeClients[ipaddr][bridge] == undefined) {
return false;
}
else if (bridgeClients[ipaddr][bridge].c2 == undefined) {
return false;
}
//console.log('bridge connected: '+bridge+' to device: '+bridgeClients[ipaddr][bridge].c2.deviceName);
return true;
}
var wsServer = new WebSocketServer({server: server}),
bridgeClients = {}, // Currently connected bridge clients
advertisedApps = new AdvertisedApps,
directoryClients = {},
monitorClients = [],
messageCounter = 0,
updateCounter = 0;
// This callback function is called every time someone tries to connect to wsServer
// UPDATE: this is actually never called!
//wsServer.on('request', function(request) {
// //console.log('Request received');
// request.accept(null, request.origin);
//});
var KEndPoint_Shared = 0;
var KEndPoint_Remote = 1;
var KEndPoint_Directory = 2;
var KEndPoint_Monitor = 3;
var KConnState_Open = 0; // Socket is open
var KConnState_Connected = 1; // Socket is connected to Bridge
wsServer.on('connection', function(connection) {
// TODO: Need new BridgeObject, referenced by both connection and bridgeObjects array so we have full context after endpoint closes.
var firstMsg = false;
var bridgeName = '';
var otherConnection = '';
var remoteAddress = resolveRemoteAddress(connection._socket.remoteAddress);
// Extend connection to maintain state and guard sends
connection.otherEndpoint = null;
connection.endpointType = -1;
connection.remoteAddress = resolveRemoteAddress(connection._socket.remoteAddress);
connection.realAddress = connection._socket.remoteAddress;
connection.bridgeName = '';
connection.connState = KConnState_Open;
connection.deviceName = '';
connection.send2 = connection.send;
connection.send = function(msg) {
if ( connection.readyState == 1) {
try {
connection.send2(msg);
}
catch(e) {
console.log('Send error: '+e+' Message: '+msg);
connection.close();
}
}
else {
connection.close();
}
}
// user sent some message
connection.on('message', function(message) {
if (firstMsg == false) { // first message is bridge name
firstMsg = true;
bridgeName = message;
connection.connState = KConnState_Connected;
connection.bridgeName = bridgeName;
connection.connectTime = new Date();
if ( bridgeName == 'NSDIRECTORY') { // directory client
connection.endpointType = KEndPoint_Directory;
console.log("DIRECTORY connection from: " + remoteAddress + " ("+connection.realAddress+")" );
if(directoryClients[remoteAddress] == undefined) {
directoryClients[remoteAddress] = new Array();
}
directoryClients[remoteAddress].push(connection);
connection.send(JSON.stringify({message: 'wsDirectoryChanged'}));
notifyMonitorListeners();
}
else if ( bridgeName == 'NSMONITOR') { // directory client
connection.endpointType = KEndPoint_Monitor;
//console.log("MONITOR connection from " + remoteAddress );
if(monitorClients == undefined) {
monitorClients = new Array();
}
monitorClients.push(connection);
generateConnectionMap(connection);
generateLogData(connection);
}
else { // bridge client
console.log("BRIDGE connect request ("+bridgeName+") from: " + remoteAddress + " ("+connection.realAddress+")" );
// Parse the bridge name
var device1 = bridgeName.split(':')[0], // shared device
appName = bridgeName.split(':')[1], // application
device2 = bridgeName.split(':')[2]; // remote device (if this is a c2 connection)
if (appName == undefined) {
console.log(" Invalid bridgeName: " + bridgeName);
connection.send('wsInvalidBridgeName');
connection.close();
return;
}
if (device2 != undefined) {
// This is a remote device attempting to connect to an existing bridge.
bridgeName = device1 + ':' + appName;
connection.bridgeName = bridgeName;
connection.endpointType = KEndPoint_Remote;
if (bridgeClients[remoteAddress] != undefined && bridgeClients[remoteAddress][bridgeName] != undefined) {
if (bridgeClients[remoteAddress][bridgeName].c2 == undefined) { // 1st remote connection
bridgeClients[remoteAddress][bridgeName].c2 = connection;
connection.deviceName = device2;
otherConnection = 'c1';
console.log(' First remote connection from device: '+device2);
// Tell both bridgeClients that bridge is connected
bridgeClients[remoteAddress][bridgeName].c1.send('wsBridgeConnected');
bridgeClients[remoteAddress][bridgeName].c2.send('wsBridgeConnected');
bridgeClients[remoteAddress][bridgeName].activity = new Date();
connection.keepalive = keepaliveLimit;
notifyDirectoryListeners(remoteAddress);
} else { //2nd remote connection - close it
// TODO: Revisit about support for one to many connections.
console.log(' Second remote connection from device: '+device2+ " (refusing connection, closing)");
connection.send('wsBridgeAlreadyConnected');
connection.close();
return;
}
}
else {
connection.send('wsBridgeDoesNotExist');
connection.close();
return;
}
}
else {
if (bridgeClients[remoteAddress] == undefined) {
bridgeClients[remoteAddress] = {};
}
// This is a shared device
if(bridgeClients[remoteAddress][bridgeName] == undefined) { // Create empty bridge
bridgeClients[remoteAddress][bridgeName] = {};
}
connection.endpointType = KEndPoint_Shared;
if(bridgeClients[remoteAddress][bridgeName].c1 != undefined) { // This shared device/application is already connected
connection.send('wsBridgeAlreadyExists');
connection.close();
}
else {
// Establish new bridge connection from shared device
bridgeClients[remoteAddress][bridgeName].c1 = connection;
bridgeClients[remoteAddress][bridgeName].application = appName;
bridgeClients[remoteAddress][bridgeName].sent = 0;
bridgeClients[remoteAddress][bridgeName].activity = new Date();
bridgeClients[remoteAddress][bridgeName].remoteAddress = remoteAddress;
connection.deviceName = device1;
connection.keepalive = keepaliveLimit;
connection.send('wsSharedConnected');
otherConnection = 'c2';
console.log(' First shared connection from: '+device1);
advertisedApps.add(remoteAddress, appName, device1, connection);
notifyDirectoryListeners(remoteAddress);
}
}
notifyMonitorListeners();
}
}
else { // process incoming message
if ( bridgeName == 'NSDIRECTORY') { // directory request for devices/applications
var msgObj = eval("(" + message + ')');
//console.log( "Received directory request: "+JSON.stringify(msgObj));
if ( msgObj.message == 'getApplications') {
//console.log(" - getApplications");
var appList = advertisedApps.getApplications(remoteAddress);
connection.send(JSON.stringify({message: 'wsApplicationList', applications: appList}));
}
else if ( msgObj.message == 'getDevices') {
var deviceList = advertisedApps.getDevices(remoteAddress);
//console.log(" - getDevices " + deviceList);
connection.send(JSON.stringify({message: 'wsDeviceList', devices: deviceList}));
}
else if ( msgObj.message == 'resetSharedDevice') {
var sharedConnection = getConnectionForSharedDevice(remoteAddress, msgObj.device);
if (sharedConnection != null) {
sharedConnection.send("wsReset");
}
}
}
else if ( bridgeName == 'NSMONITOR') { // monitor request
if ( message == 'refresh') {
// TODO: build json structure with topology and full log file.
//var appList = advertisedApps.getApplications(remoteAddress);
generateConnectionMap(connection);
generateLogData(connection);
}
else if (message == 'restart') {
setTimeout(function(){
process.kill(process.pid, 'SIGTERM');
},500);
}
}
else { // relay the message to the other WebSocket connection
if (message == 'wsKeepalive') {
connection.keepalive = keepaliveLimit;
}
else {
var dest = bridgeClients[remoteAddress][bridgeName][otherConnection];
if(dest != undefined) {
dest.send(message);
bridgeClients[remoteAddress][bridgeName].sent++;
bridgeClients[remoteAddress][bridgeName].activity = new Date();
messageCounter++;
}
}
}
}
});
// user disconnected
connection.on('close', function(connection) {
//console.log('\nConnection closed for socket at remote address: ' + remoteAddress + ' on '+(new Date()));
var index = -1;
if (bridgeName == 'NSDIRECTORY') {
if (directoryClients[remoteAddress] != undefined) {
index = directoryClients[remoteAddress].indexOf(this);
}
if (index!=-1) { // directory connection
console.log('Closing NSDIRECTORY client from: ' + remoteAddress);
directoryClients[remoteAddress].splice(index, 1);
if (directoryClients[remoteAddress].length == 0) {
delete directoryClients[remoteAddress];
}
}
}
else if (bridgeName == 'NSMONITOR') {
index = monitorClients.indexOf(this);
if (index!=-1) { // directory connection
//console.log('Closing NSMONITOR client from: ' + remoteAddress);
monitorClients.splice(index, 1);
}
}
else { // bridge connection
switch(connection.endpointType) {
case KEndPoint_Remote:
// 2nd connection closing
// TODO: Need new BridgeObject that is referenced both by the bridgeClients array and the connection.
closeC2(remoteAddress, bridgeName);
break;
case KEndPoint_Shared:
// 1st client closing. Close other side and delete bridge
closeC1(remoteAddress, bridgeName);
break;
case '':
console.log('Closing connection of unknown type: '+otherConnection);
break;
}
notifyMonitorListeners();
}
});
connection.on('error', function(err) {
console.log('Connection error detected for socket at remote address: ' + remoteAddress + ' (removing connection)');
var index = -1;
if (bridgeName == 'NSDIRECTORY') {
if (directoryClients[remoteAddress] != undefined) {
index = directoryClients[remoteAddress].indexOf(this);
}
if (index!=-1) { // directory connection
console.log('Closing NSDIRECTORY client from: ' + remoteAddress);
directoryClients[remoteAddress].splice(index, 1);
if (directoryClients[remoteAddress].length == 0) {
delete directoryClients[remoteAddress];
}
}
}
else if (bridgeName == 'NSMONITOR') {
index = monitorClients.indexOf(this);
if (index!=-1) { // directory connection
//console.log('Closing NSMONITOR client from: ' + remoteAddress);
monitorClients.splice(index, 1);
}
}
else { // bridge connection
switch(connection.endpointType) {
case KEndPoint_Remote:
// 2nd connection closing
// TODO: Need new BridgeObject that is referenced both by the bridgeClients array and the connection.
closeC2(remoteAddress, bridgeName);
break;
case KEndPoint_Shared:
// 1st client closing. Close other side and delete bridge
closeC1(remoteAddress, bridgeName);
break;
case '':
console.log('Closing connection of unknown type: '+otherConnection);
break;
}
notifyMonitorListeners();
}
});
});
function networkAddr(ipAddr) {
var splitAddr = ipAddr.split('.');
if ((splitAddr[0] & 7) == 1) {return splitAddr[0] + '.00.00.00';} //class A
if ((splitAddr[0] & 7) == 2) {return splitAddr[0] + '.' + splitAddr[1] + '.00.00';} //class B
if ((splitAddr[0] & 7) == 6) {return splitAddr[0] + '.' + splitAddr[1] + '.' + splitAddr[2] + '.00'} //class C