changed token handling to allow multiple per websocket

This commit is contained in:
tigerbot 2017-04-26 19:52:30 -06:00
parent 65df12ecb3
commit 40c797b729
1 changed files with 125 additions and 100 deletions

View File

@ -23,8 +23,7 @@ Devices.remove = function (store, servername, device) {
var index = devices.indexOf(device); var index = devices.indexOf(device);
if (index < 0) { if (index < 0) {
var id = device.deviceId || device.servername || device.id; console.warn('attempted to remove non-present device', device.deviceId, 'from', servername);
console.warn('attempted to remove non-present device', id, 'from', servername);
return null; return null;
} }
return devices.splice(index, 1)[0]; return devices.splice(index, 1)[0];
@ -55,75 +54,26 @@ module.exports.create = function (copts) {
var pongTimeout = copts.pongTimeout || 10*1000; var pongTimeout = copts.pongTimeout || 10*1000;
function onWsConnection(ws) { function onWsConnection(ws) {
var location = url.parse(ws.upgradeReq.url, true); var socketId = packer.socketToId(ws.upgradeReq.socket);
var authn = (ws.upgradeReq.headers.authorization||'').split(/\s+/); var remotes = {};
var jwtoken;
var token;
try { function logName() {
if (authn[0]) { var result = Object.keys(remotes).map(function (jwtoken) {
if ('basic' === authn[0].toLowerCase()) { return remotes[jwtoken].deviceId;
authn = new Buffer(authn[1], 'base64').toString('ascii').split(':'); }).join(';');
}
/* return result || socketId;
if (-1 !== [ 'bearer', 'jwk' ].indexOf(authn[0].toLowerCase())) {
jwtoken = authn[1];
}
*/
}
jwtoken = authn[1] || location.query.access_token;
} catch(e) {
jwtoken = null;
} }
try {
token = jwt.verify(jwtoken, copts.secret);
} catch(e) {
token = null;
}
/*
if (!token || !token.name) {
console.log('location, token');
console.log(location.query.access_token);
console.log(token);
}
*/
if (!token) {
ws.send(JSON.stringify({ error: { message: "invalid access token", code: "E_INVALID_TOKEN" } }));
ws.close();
return;
}
//console.log('[wstunneld.js] DEBUG', token);
if (!Array.isArray(token.domains)) {
if ('string' === typeof token.name) {
token.domains = [ token.name ];
}
}
if (!Array.isArray(token.domains)) {
ws.send(JSON.stringify({ error: { message: "invalid server name", code: "E_INVALID_NAME" } }));
ws.close();
return;
}
var remote = {};
remote.ws = ws;
remote.servername = (token.device && token.device.hostname) || token.domains.join(',');
remote.deviceId = (token.device && token.device.id) || null;
remote.id = packer.socketToId(ws.upgradeReq.socket);
console.log("remote.id", remote.id);
remote.domains = token.domains;
remote.clients = {};
// TODO allow tls to be decrypted by server if client is actually a browser
// and we haven't implemented tls in the browser yet
// remote.decrypt = token.decrypt;
function closeBrowserConn(cid) { function closeBrowserConn(cid) {
if (!remote.clients[cid]) { var remote;
Object.keys(remotes).some(function (jwtoken) {
if (remotes[jwtoken].clients[cid]) {
remote = remotes[jwtoken];
return true;
}
});
if (!remote) {
return; return;
} }
@ -151,20 +101,110 @@ module.exports.create = function (copts) {
; ;
} }
function addToken(jwtoken) {
if (remotes[jwtoken]) {
ws.send(JSON.stringify({ error: { message: "token sent multiple times", code: "E_TOKEN_REPEAT" } }));
return false;
}
var token;
try {
token = jwt.verify(jwtoken, copts.secret);
} catch (e) {
token = null;
}
if (!token) {
ws.send(JSON.stringify({ error: { message: "invalid access token", code: "E_INVALID_TOKEN" } }));
return false;
}
if (!Array.isArray(token.domains)) {
if ('string' === typeof token.name) {
token.domains = [ token.name ];
}
}
if (!Array.isArray(token.domains)) {
ws.send(JSON.stringify({ error: { message: "invalid server name", code: "E_INVALID_NAME" } }));
return false;
}
if (token.domains.some(function (name) { return typeof name !== 'string'; })) {
ws.send(JSON.stringify({ error: { message: "invalid server name", code: "E_INVALID_NAME" } }));
return false;
}
// Add the custom properties we need to manage this remote, then add it to all the relevant
// domains and the list of all this websocket's remotes.
token.deviceId = (token.device && (token.device.id || token.device.hostname)) || token.domains.join(',');
token.ws = ws;
token.clients = {};
token.domains.forEach(function (domainname) {
console.log('domainname', domainname);
Devices.add(deviceLists, domainname, token);
});
remotes[jwtoken] = token;
console.log("added token '" + token.deviceId + "' to websocket", socketId);
return true;
}
function removeToken(jwtoken) {
var remote = remotes[jwtoken];
if (!remote) {
return false;
}
// Prevent any more browser connections being sent to this remote, and any existing
// connections from trying to send more data across the connection.
remote.domains.forEach(function (domainname) {
Devices.remove(deviceLists, domainname, remote);
});
remote.ws = null;
// Close all of the existing browser connections associated with this websocket connection.
Object.keys(remote.clients).forEach(function (cid) {
closeBrowserConn(cid);
});
delete remotes[jwtoken];
console.log("removed token '" + remote.deviceId + "' from websocket", socketId);
}
var firstToken;
var authn = (ws.upgradeReq.headers.authorization||'').split(/\s+/);
if (authn[0] && 'basic' === authn[0].toLowerCase()) {
try {
authn = new Buffer(authn[1], 'base64').toString('ascii').split(':');
firstToken = authn[1];
} catch (err) { }
}
if (!firstToken) {
firstToken = url.parse(ws.upgradeReq.url, true).query.access_token;
}
if (firstToken && !addToken(firstToken)) {
ws.close();
return;
}
var handlers = { var handlers = {
onmessage: function (opts) { onmessage: function (opts) {
// opts.data
var cid = packer.addrToId(opts); var cid = packer.addrToId(opts);
var browserConn = remote.clients[cid]; console.log("remote '" + logName() + "' has data for '" + cid + "'", opts.data.byteLength);
console.log("remote '" + remote.servername + " : " + remote.id + "' has data for '" + cid + "'", opts.data.byteLength); var browserConn;
Object.keys(remotes).some(function (jwtoken) {
if (remotes[jwtoken].clients[cid]) {
browserConn = remotes[jwtoken].clients[cid];
return true;
}
});
if (!browserConn) { if (browserConn) {
remote.ws.send(packer.pack(opts, null, 'error')); browserConn.write(opts.data);
return; }
else {
ws.send(packer.pack(opts, null, 'error'));
} }
browserConn.write(opts.data);
} }
, onend: function (opts) { , onend: function (opts) {
var cid = packer.addrToId(opts); var cid = packer.addrToId(opts);
@ -177,14 +217,7 @@ module.exports.create = function (copts) {
closeBrowserConn(cid); closeBrowserConn(cid);
} }
}; };
remote.unpacker = packer.create(handlers); var unpacker = packer.create(handlers);
// Now that we have created our remote object we need to store it in the deviceList for
// each domainname we are supposed to be handling.
token.domains.forEach(function (domainname) {
console.log('domainname', domainname);
Devices.add(deviceLists, domainname, remote);
});
var lastActivity = Date.now(); var lastActivity = Date.now();
var timeoutId; var timeoutId;
@ -204,11 +237,11 @@ module.exports.create = function (copts) {
// Otherwise we check to see if the pong has also timed out, and if not we send a ping // Otherwise we check to see if the pong has also timed out, and if not we send a ping
// and call this function again when the pong will have timed out. // and call this function again when the pong will have timed out.
else if (silent < activityTimeout + pongTimeout) { else if (silent < activityTimeout + pongTimeout) {
console.log('pinging', remote.deviceId || remote.servername); console.log('pinging', logName());
try { try {
remote.ws.ping(); ws.ping();
} catch (err) { } catch (err) {
console.warn('failed to ping home cloud', remote.deviceId || remote.servername); console.warn('failed to ping home cloud', logName());
} }
timeoutId = setTimeout(checkTimeout, pongTimeout); timeoutId = setTimeout(checkTimeout, pongTimeout);
} }
@ -216,8 +249,8 @@ module.exports.create = function (copts) {
// Last case means the ping we sent before didn't get a response soon enough, so we // Last case means the ping we sent before didn't get a response soon enough, so we
// need to close the websocket connection. // need to close the websocket connection.
else { else {
console.log('home cloud', remote.deviceId || remote.servername, 'connection timed out'); console.log('home cloud', logName(), 'connection timed out');
remote.ws.close(1013, 'connection timeout'); ws.close(1013, 'connection timeout');
} }
} }
timeoutId = setTimeout(checkTimeout, activityTimeout); timeoutId = setTimeout(checkTimeout, activityTimeout);
@ -230,22 +263,14 @@ module.exports.create = function (copts) {
refreshTimeout(); refreshTimeout();
console.log('message from home cloud to tunneler to browser', chunk.byteLength); console.log('message from home cloud to tunneler to browser', chunk.byteLength);
//console.log(chunk.toString()); //console.log(chunk.toString());
remote.unpacker.fns.addChunk(chunk); unpacker.fns.addChunk(chunk);
}); });
function hangup() { function hangup() {
clearTimeout(timeoutId); clearTimeout(timeoutId);
console.log('home cloud', remote.deviceId || remote.servername, 'connection closing'); console.log('home cloud', logName(), 'connection closing');
// Prevent any more browser connections being sent to this remote, and any existing Object.keys(remotes).forEach(function (jwtoken) {
// connections from trying to send more data across the connection. removeToken(jwtoken);
token.domains.forEach(function (domainname) {
Devices.remove(deviceLists, domainname, remote);
});
remote.ws = null;
// Close all of the existing browser connections associated with this websocket connection.
Object.keys(remote.clients).forEach(function (cid) {
closeBrowserConn(cid);
}); });
} }