From 40c797b729e73aeac8e7568f216e2fc30c2371a5 Mon Sep 17 00:00:00 2001 From: tigerbot Date: Wed, 26 Apr 2017 19:52:30 -0600 Subject: [PATCH] changed token handling to allow multiple per websocket --- wstunneld.js | 225 ++++++++++++++++++++++++++++----------------------- 1 file changed, 125 insertions(+), 100 deletions(-) diff --git a/wstunneld.js b/wstunneld.js index 5ebb954..2e7a507 100644 --- a/wstunneld.js +++ b/wstunneld.js @@ -23,8 +23,7 @@ Devices.remove = function (store, servername, device) { var index = devices.indexOf(device); if (index < 0) { - var id = device.deviceId || device.servername || device.id; - console.warn('attempted to remove non-present device', id, 'from', servername); + console.warn('attempted to remove non-present device', device.deviceId, 'from', servername); return null; } return devices.splice(index, 1)[0]; @@ -55,75 +54,26 @@ module.exports.create = function (copts) { var pongTimeout = copts.pongTimeout || 10*1000; function onWsConnection(ws) { - var location = url.parse(ws.upgradeReq.url, true); - var authn = (ws.upgradeReq.headers.authorization||'').split(/\s+/); - var jwtoken; - var token; + var socketId = packer.socketToId(ws.upgradeReq.socket); + var remotes = {}; - try { - if (authn[0]) { - if ('basic' === authn[0].toLowerCase()) { - authn = new Buffer(authn[1], 'base64').toString('ascii').split(':'); - } - /* - if (-1 !== [ 'bearer', 'jwk' ].indexOf(authn[0].toLowerCase())) { - jwtoken = authn[1]; - } - */ - } - jwtoken = authn[1] || location.query.access_token; - } catch(e) { - jwtoken = null; + function logName() { + var result = Object.keys(remotes).map(function (jwtoken) { + return remotes[jwtoken].deviceId; + }).join(';'); + + return result || socketId; } - 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) { - 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; } @@ -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 = { onmessage: function (opts) { - // opts.data 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) { - remote.ws.send(packer.pack(opts, null, 'error')); - return; + if (browserConn) { + browserConn.write(opts.data); + } + else { + ws.send(packer.pack(opts, null, 'error')); } - - browserConn.write(opts.data); } , onend: function (opts) { var cid = packer.addrToId(opts); @@ -177,14 +217,7 @@ module.exports.create = function (copts) { closeBrowserConn(cid); } }; - remote.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 unpacker = packer.create(handlers); var lastActivity = Date.now(); 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 // and call this function again when the pong will have timed out. else if (silent < activityTimeout + pongTimeout) { - console.log('pinging', remote.deviceId || remote.servername); + console.log('pinging', logName()); try { - remote.ws.ping(); + ws.ping(); } 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); } @@ -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 // need to close the websocket connection. else { - console.log('home cloud', remote.deviceId || remote.servername, 'connection timed out'); - remote.ws.close(1013, 'connection timeout'); + console.log('home cloud', logName(), 'connection timed out'); + ws.close(1013, 'connection timeout'); } } timeoutId = setTimeout(checkTimeout, activityTimeout); @@ -230,22 +263,14 @@ module.exports.create = function (copts) { refreshTimeout(); console.log('message from home cloud to tunneler to browser', chunk.byteLength); //console.log(chunk.toString()); - remote.unpacker.fns.addChunk(chunk); + unpacker.fns.addChunk(chunk); }); function hangup() { clearTimeout(timeoutId); - console.log('home cloud', remote.deviceId || remote.servername, 'connection closing'); - // Prevent any more browser connections being sent to this remote, and any existing - // connections from trying to send more data across the connection. - 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); + console.log('home cloud', logName(), 'connection closing'); + Object.keys(remotes).forEach(function (jwtoken) { + removeToken(jwtoken); }); }