'use strict'; module.exports.create = function (deps, config) { var PromiseA = require('bluebird'); var fs = PromiseA.promisifyAll(require('fs')); var stunnel = require('stunnel'); var activeTunnels = {}; var path = require('path'); var tokensPath = path.join(__dirname, '..', 'var', 'tokens.json'); var storage = { _read: function () { var tokens; try { tokens = require(tokensPath); } catch (err) { tokens = {}; } return tokens; } , _write: function (tokens) { return fs.mkdirAsync(path.dirname(tokensPath)).catch(function (err) { if (err.code !== 'EEXIST') { console.error('failed to mkdir', path.dirname(tokensPath), err.toString()); } }).then(function () { return fs.writeFileAsync(tokensPath, JSON.stringify(tokens), 'utf8'); }); } , all: function () { var tokens = storage._read(); return PromiseA.resolve(Object.keys(tokens).map(function (key) { return tokens[key]; })); } , save: function (result) { var tokens = storage._read(); tokens[result.jwt] = result; storage._write(tokens); } , del: function (id) { var tokens = storage._read(); delete tokens[id]; storage._write(tokens); } }; function addToken(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 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 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 removeToken(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 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 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 PromiseA.resolve(); } console.log('removing token from tunnel at', data.tunnelUrl); return activeTunnels[data.tunnelUrl].clear(data.jwt); } if (typeof config.tunnel === 'string') { config.tunnel.split(',').forEach(function (jwt) { addToken({ jwt: jwt, owner: 'config' }); }); } storage.all().then(function (stored) { stored.forEach(function (result) { addToken(result); }); }); return { add: function (data) { return addToken(data).then(function () { return storage.save(data); }); } , remove: function (data) { return storage.del(data.jwt).then(function () { return removeToken(data); }); } , get: function (owner) { return storage.all().then(function (tokens) { var result = {}; tokens.forEach(function (data) { if (!result[data.owner]) { result[data.owner] = {}; } if (!result[data.owner][data.tunnelUrl]) { result[data.owner][data.tunnelUrl] = []; } data.decoded = JSON.parse(new Buffer(data.jwt.split('.')[0], 'base64')); result[data.owner][data.tunnelUrl].push(data); }); if (owner) { return result[owner] || {}; } return result; }); } }; };