238 lines
		
	
	
		
			7.6 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			238 lines
		
	
	
		
			7.6 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| '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');
 | |
|       });
 | |
|     }
 | |
|   , _makeKey: function (token) {
 | |
|       // We use a stripped down version of the token contents so that if the token is
 | |
|       // re-issued the nonce and the iat and any other less important things are different
 | |
|       // we don't save essentially duplicate tokens multiple times.
 | |
|       var parsed = JSON.parse((new Buffer(token.split('.')[1], 'base64')).toString());
 | |
|       var stripped = {};
 | |
|       ['aud', 'iss', 'domains'].forEach(function (key) {
 | |
|         if (parsed[key]) {
 | |
|           stripped[key] = parsed[key];
 | |
|         }
 | |
|       });
 | |
|       stripped.domains.sort();
 | |
| 
 | |
|       var hash = require('crypto').createHash('sha256');
 | |
|       return hash.update(JSON.stringify(stripped)).digest('hex');
 | |
|     }
 | |
| 
 | |
|   , all: function () {
 | |
|       var tokens = storage._read();
 | |
|       return PromiseA.resolve(Object.keys(tokens).map(function (key) {
 | |
|         return tokens[key];
 | |
|       }));
 | |
|     }
 | |
|   , save: function (token) {
 | |
|       return PromiseA.resolve().then(function () {
 | |
|         var curTokens = storage._read();
 | |
|         curTokens[storage._makeKey(token.jwt)] = token;
 | |
|         return storage._write(curTokens);
 | |
|       });
 | |
|     }
 | |
|   , del: function (token) {
 | |
|       return PromiseA.resolve().then(function () {
 | |
|         var curTokens = storage._read();
 | |
|         delete curTokens[storage._makeKey(token.jwt)];
 | |
|         return storage._write(curTokens);
 | |
|       });
 | |
|     }
 | |
|   };
 | |
| 
 | |
|   function acquireToken(session) {
 | |
|     var OAUTH3 = deps.OAUTH3;
 | |
|     // session seems to be changed by the API call for some reason, so save the
 | |
|     // owner before that happens.
 | |
|     var owner = session.id;
 | |
| 
 | |
|     // 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: {
 | |
|           // filter to all domains that are on this device
 | |
|           //domains: Object.keys(domainsMap)
 | |
|           device: {
 | |
|             hostname: config.device.hostname
 | |
|           , id: config.device.uid || config.device.id
 | |
|           }
 | |
|         }
 | |
|       };
 | |
| 
 | |
|       return OAUTH3.api(directives.api, opts).then(function (result) {
 | |
|         console.log('got a token from the tunnel server?');
 | |
|         result.owner = owner;
 | |
|         return result;
 | |
|       });
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   function addToken(data) {
 | |
|     if (!data.jwt) {
 | |
|       return 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 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 (config.tunnel) {
 | |
|     var confTokens = config.tunnel;
 | |
|     if (typeof confTokens === 'string') {
 | |
|       confTokens = confTokens.split(',');
 | |
|     }
 | |
|     confTokens.forEach(function (jwt) {
 | |
|       if (typeof jwt === 'object') {
 | |
|         jwt.owner = 'config';
 | |
|         addToken(jwt);
 | |
|       } else {
 | |
|         addToken({ jwt: jwt, owner: 'config' });
 | |
|       }
 | |
|     });
 | |
|   }
 | |
| 
 | |
| 
 | |
|   storage.all().then(function (stored) {
 | |
|     stored.forEach(function (result) {
 | |
|       addToken(result);
 | |
|     });
 | |
|   });
 | |
| 
 | |
|   return {
 | |
|     start: function (session) {
 | |
|       return acquireToken(session).then(function (token) {
 | |
|         return addToken(token).then(function () {
 | |
|           return storage.save(token);
 | |
|         });
 | |
|       });
 | |
|     }
 | |
|   , 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;
 | |
|       });
 | |
|     }
 | |
|   };
 | |
| };
 |