| 
									
										
										
										
											2017-05-26 12:11:39 -06:00
										 |  |  | 'use strict'; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | module.exports.create = function (deps, config) { | 
					
						
							| 
									
										
										
										
											2017-05-29 13:41:09 -06:00
										 |  |  |   var PromiseA = require('bluebird'); | 
					
						
							|  |  |  |   var fs = PromiseA.promisifyAll(require('fs')); | 
					
						
							| 
									
										
										
										
											2017-05-26 12:11:39 -06:00
										 |  |  |   var stunnel = require('stunnel'); | 
					
						
							|  |  |  |   var activeTunnels = {}; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-06-01 13:06:52 -06:00
										 |  |  |   var path = require('path'); | 
					
						
							|  |  |  |   var tokensPath = path.join(__dirname, '..', 'var', 'tokens.json'); | 
					
						
							| 
									
										
										
										
											2017-05-29 13:41:09 -06:00
										 |  |  |   var storage = { | 
					
						
							| 
									
										
										
										
											2017-05-29 15:14:37 -06:00
										 |  |  |     _read: function () { | 
					
						
							| 
									
										
										
										
											2017-05-29 13:41:09 -06:00
										 |  |  |       var tokens; | 
					
						
							|  |  |  |       try { | 
					
						
							|  |  |  |         tokens = require(tokensPath); | 
					
						
							|  |  |  |       } catch (err) { | 
					
						
							|  |  |  |         tokens = {}; | 
					
						
							|  |  |  |       } | 
					
						
							| 
									
										
										
										
											2017-05-29 15:14:37 -06:00
										 |  |  |       return tokens; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   , _write: function (tokens) { | 
					
						
							| 
									
										
										
										
											2017-06-01 13:06:52 -06:00
										 |  |  |       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'); | 
					
						
							|  |  |  |       }); | 
					
						
							| 
									
										
										
										
											2017-05-29 15:14:37 -06:00
										 |  |  |     } | 
					
						
							| 
									
										
										
										
											2017-06-15 14:14:14 -06:00
										 |  |  |   , _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'); | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2017-05-29 13:41:09 -06:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-05-29 15:14:37 -06:00
										 |  |  |   , all: function () { | 
					
						
							|  |  |  |       var tokens = storage._read(); | 
					
						
							| 
									
										
										
										
											2017-05-29 13:41:09 -06:00
										 |  |  |       return PromiseA.resolve(Object.keys(tokens).map(function (key) { | 
					
						
							|  |  |  |         return tokens[key]; | 
					
						
							|  |  |  |       })); | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2017-06-15 14:14:14 -06:00
										 |  |  |   , save: function (token) { | 
					
						
							|  |  |  |       return PromiseA.resolve().then(function () { | 
					
						
							|  |  |  |         var curTokens = storage._read(); | 
					
						
							| 
									
										
										
										
											2017-06-21 16:07:48 -06:00
										 |  |  |         curTokens[storage._makeKey(token.jwt)] = token; | 
					
						
							| 
									
										
										
										
											2017-06-15 14:14:14 -06:00
										 |  |  |         return storage._write(curTokens); | 
					
						
							|  |  |  |       }); | 
					
						
							| 
									
										
										
										
											2017-05-29 15:14:37 -06:00
										 |  |  |     } | 
					
						
							| 
									
										
										
										
											2017-06-15 14:14:14 -06:00
										 |  |  |   , del: function (token) { | 
					
						
							|  |  |  |       return PromiseA.resolve().then(function () { | 
					
						
							|  |  |  |         var curTokens = storage._read(); | 
					
						
							| 
									
										
										
										
											2017-06-21 16:07:48 -06:00
										 |  |  |         delete curTokens[storage._makeKey(token.jwt)]; | 
					
						
							| 
									
										
										
										
											2017-06-15 14:14:14 -06:00
										 |  |  |         return storage._write(curTokens); | 
					
						
							|  |  |  |       }); | 
					
						
							| 
									
										
										
										
											2017-05-29 13:41:09 -06:00
										 |  |  |     } | 
					
						
							|  |  |  |   }; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-06-23 17:22:45 -06:00
										 |  |  |   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; | 
					
						
							|  |  |  |       }); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-05-26 12:11:39 -06:00
										 |  |  |   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); | 
					
						
							| 
									
										
										
										
											2017-05-29 13:41:09 -06:00
										 |  |  |         return PromiseA.reject(err); | 
					
						
							| 
									
										
										
										
											2017-05-26 12:11:39 -06:00
										 |  |  |       } | 
					
						
							|  |  |  |       if (!decoded.aud) { | 
					
						
							|  |  |  |         console.warn('tunnel manager given token with no tunnelUrl or audience'); | 
					
						
							| 
									
										
										
										
											2017-05-29 13:41:09 -06:00
										 |  |  |         var err = new Error('missing tunnelUrl and audience'); | 
					
						
							|  |  |  |         return PromiseA.reject(err); | 
					
						
							| 
									
										
										
										
											2017-05-26 12:11:39 -06:00
										 |  |  |       } | 
					
						
							|  |  |  |       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); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-05-29 15:14:37 -06:00
										 |  |  |   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); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-05-26 12:11:39 -06:00
										 |  |  |   if (typeof config.tunnel === 'string') { | 
					
						
							|  |  |  |     config.tunnel.split(',').forEach(function (jwt) { | 
					
						
							|  |  |  |       addToken({ jwt: jwt, owner: 'config' }); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  |   } | 
					
						
							| 
									
										
										
										
											2017-05-29 13:41:09 -06:00
										 |  |  |   storage.all().then(function (stored) { | 
					
						
							|  |  |  |     stored.forEach(function (result) { | 
					
						
							|  |  |  |       addToken(result); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  |   }); | 
					
						
							| 
									
										
										
										
											2017-05-26 12:11:39 -06:00
										 |  |  | 
 | 
					
						
							|  |  |  |   return { | 
					
						
							| 
									
										
										
										
											2017-06-23 17:22:45 -06:00
										 |  |  |     start: function (session) { | 
					
						
							|  |  |  |       return acquireToken(session).then(function (token) { | 
					
						
							|  |  |  |         return addToken(token).then(function () { | 
					
						
							|  |  |  |           return storage.save(token); | 
					
						
							|  |  |  |         }); | 
					
						
							|  |  |  |       }); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   , add: function (data) { | 
					
						
							| 
									
										
										
										
											2017-05-29 13:41:09 -06:00
										 |  |  |       return addToken(data).then(function () { | 
					
						
							|  |  |  |         return storage.save(data); | 
					
						
							|  |  |  |       }); | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2017-05-29 15:14:37 -06:00
										 |  |  |   , remove: function (data) { | 
					
						
							|  |  |  |       return storage.del(data.jwt).then(function () { | 
					
						
							|  |  |  |         return removeToken(data); | 
					
						
							|  |  |  |       }); | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2017-05-30 12:15:19 -06:00
										 |  |  |   , 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; | 
					
						
							|  |  |  |       }); | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2017-05-26 12:11:39 -06:00
										 |  |  |   }; | 
					
						
							|  |  |  | }; |