'use strict'; module.exports.create = function (deps, config) { var stunnel = require('stunnel'); var activeTunnels = {}; function addToken(data) { if (typeof data === 'string') { data = { jwt: data }; } if (!data.jwt) { return deps.PromiseA.reject(new Error("missing 'jwt' from tunnel data")); } if (!data.tunnelUrl) { var decoded; try { decoded = JSON.parse(new Buffer(data.jwt.split('.')[1], 'base64').toString('ascii')); } catch (err) { console.warn('invalid web token given to tunnel manager', err); return deps.PromiseA.reject(err); } if (!decoded.aud) { console.warn('tunnel manager given token with no tunnelUrl or audience'); var err = new Error('missing tunnelUrl and audience'); return deps.PromiseA.reject(err); } data.tunnelUrl = 'wss://' + decoded.aud + '/'; } if (!activeTunnels[data.tunnelUrl]) { console.log('creating new tunnel client for', data.tunnelUrl); // We create the tunnel without an initial token so we can append the token and // get the promise that should tell us more about if it worked or not. activeTunnels[data.tunnelUrl] = stunnel.connect({ stunneld: data.tunnelUrl , net: deps.tunnel.net // NOTE: the ports here aren't that important since we are providing a custom // `net.createConnection` that doesn't actually use the port. What is important // is that any services we are interested in are listed in this object and have // a '*' sub-property. , services: { https: { '*': 443 } , http: { '*': 80 } , smtp: { '*': 25 } , smtps: { '*': 587 /*also 465/starttls*/ } , ssh: { '*': 22 } } }); } console.log('appending token to tunnel at', data.tunnelUrl); return activeTunnels[data.tunnelUrl].append(data.jwt); } function acquireToken(session, domains) { var OAUTH3 = deps.OAUTH3; // The OAUTH3 library stores some things on the root session object that we usually // just leave inside the token, but we need to pull those out before we use it here session.provider_uri = session.provider_uri || session.token.provider_uri || session.token.iss; session.client_uri = session.client_uri || session.token.azp; session.scope = session.scope || session.token.scp; console.log('asking for tunnel token from', session.token.aud); return OAUTH3.discover(session.token.aud).then(function (directives) { var opts = { api: 'tunnel.token' , session: session , data: { domains: domains , device: { hostname: config.device.hostname , id: config.device.uid || config.device.id } } }; return OAUTH3.api(directives.api, opts).then(addToken); }); } function removeToken(data) { if (typeof data === 'string') { data = { jwt: data }; } if (!data.tunnelUrl) { var decoded; try { decoded = JSON.parse(new Buffer(data.jwt.split('.')[1], 'base64').toString('ascii')); } catch (err) { console.warn('invalid web token given to tunnel manager', err); return deps.PromiseA.reject(err); } if (!decoded.aud) { console.warn('tunnel manager given token with no tunnelUrl or audience'); var err = new Error('missing tunnelUrl and audience'); return deps.PromiseA.reject(err); } data.tunnelUrl = 'wss://' + decoded.aud + '/'; } // Not sure if we actually want to return an error that the token didn't even belong to a // server that existed, but since it never existed we can consider it as "removed". if (!activeTunnels[data.tunnelUrl]) { return deps.PromiseA.resolve(); } console.log('removing token from tunnel at', data.tunnelUrl); return activeTunnels[data.tunnelUrl].clear(data.jwt); } function disconnectAll() { Object.keys(activeTunnels).forEach(function (key) { activeTunnels[key].end(); }); } return { start: acquireToken , startDirect: addToken , remove: removeToken , disconnect: disconnectAll }; };