| 
									
										
										
										
											2017-05-26 12:11:39 -06:00
										 |  |  | 'use strict'; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | module.exports.create = function (deps, config) { | 
					
						
							|  |  |  |   var stunnel = require('stunnel'); | 
					
						
							| 
									
										
										
										
											2017-10-19 17:45:05 -06:00
										 |  |  |   var jwt = require('jsonwebtoken'); | 
					
						
							| 
									
										
										
										
											2017-05-26 12:11:39 -06:00
										 |  |  |   var activeTunnels = {}; | 
					
						
							| 
									
										
										
										
											2017-10-19 17:45:05 -06:00
										 |  |  |   var activeDomains = {}; | 
					
						
							| 
									
										
										
										
											2017-05-26 12:11:39 -06:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-10-31 18:10:46 -06:00
										 |  |  |   var customNet = { | 
					
						
							|  |  |  |     createConnection: function (opts, cb) { | 
					
						
							|  |  |  |       console.log('[gl.tunnel] creating connection'); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       // here "reader" means the socket that looks like the connection being accepted
 | 
					
						
							|  |  |  |       // here "writer" means the remote-looking part of the socket that driving the connection
 | 
					
						
							|  |  |  |       var writer; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       function usePair(err, reader) { | 
					
						
							|  |  |  |         if (err) { | 
					
						
							|  |  |  |           process.nextTick(function () { | 
					
						
							|  |  |  |             writer.emit('error', err); | 
					
						
							|  |  |  |           }); | 
					
						
							|  |  |  |           return; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         var wrapOpts = Object.assign({localAddress: '127.0.0.2', localPort: 'tunnel-0'}, opts); | 
					
						
							|  |  |  |         wrapOpts.firstChunk = opts.data; | 
					
						
							|  |  |  |         wrapOpts.hyperPeek = !!opts.data; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         // Also override the remote and local address info. We use `defineProperty` because
 | 
					
						
							|  |  |  |         // otherwise we run into problems of setting properties with only getters defined.
 | 
					
						
							|  |  |  |         Object.defineProperty(reader, 'remoteAddress', { value: wrapOpts.remoteAddress }); | 
					
						
							|  |  |  |         Object.defineProperty(reader, 'remotePort',    { value: wrapOpts.remotePort }); | 
					
						
							|  |  |  |         Object.defineProperty(reader, 'remoteFamiliy', { value: wrapOpts.remoteFamiliy }); | 
					
						
							|  |  |  |         Object.defineProperty(reader, 'localAddress',  { value: wrapOpts.localAddress }); | 
					
						
							|  |  |  |         Object.defineProperty(reader, 'localPort',     { value: wrapOpts.localPort }); | 
					
						
							|  |  |  |         Object.defineProperty(reader, 'localFamiliy',  { value: wrapOpts.localFamiliy }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         deps.tcp.handler(reader, wrapOpts); | 
					
						
							|  |  |  |         process.nextTick(function () { | 
					
						
							|  |  |  |           // this cb will cause the stream to emit its (actually) first data event
 | 
					
						
							|  |  |  |           // (even though it already gave a peek into that first data chunk)
 | 
					
						
							|  |  |  |           console.log('[tunnel] callback, data should begin to flow'); | 
					
						
							|  |  |  |           cb(); | 
					
						
							|  |  |  |         }); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       // We used to use `stream-pair` for non-tls connections, but there are places
 | 
					
						
							|  |  |  |       // that require properties/functions to be present on the socket that aren't
 | 
					
						
							|  |  |  |       // present on a JSStream so it caused problems.
 | 
					
						
							|  |  |  |       writer = require('socket-pair').create(usePair); | 
					
						
							|  |  |  |       return writer; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   }; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-10-19 17:45:05 -06:00
										 |  |  |   function fillData(data) { | 
					
						
							| 
									
										
										
										
											2017-09-28 11:18:44 -06:00
										 |  |  |     if (typeof data === 'string') { | 
					
						
							|  |  |  |       data = { jwt: data }; | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2017-10-19 17:45:05 -06:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-07-26 11:44:08 -06:00
										 |  |  |     if (!data.jwt) { | 
					
						
							| 
									
										
										
										
											2017-10-19 17:45:05 -06:00
										 |  |  |       throw new Error("missing 'jwt' from tunnel data"); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     var decoded = jwt.decode(data.jwt); | 
					
						
							|  |  |  |     if (!decoded) { | 
					
						
							|  |  |  |       throw new Error('invalid JWT'); | 
					
						
							| 
									
										
										
										
											2017-07-26 11:44:08 -06:00
										 |  |  |     } | 
					
						
							| 
									
										
										
										
											2017-10-19 17:45:05 -06:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-05-26 12:11:39 -06:00
										 |  |  |     if (!data.tunnelUrl) { | 
					
						
							|  |  |  |       if (!decoded.aud) { | 
					
						
							| 
									
										
										
										
											2017-10-19 17:45:05 -06:00
										 |  |  |         throw new Error('missing tunnelUrl and audience'); | 
					
						
							| 
									
										
										
										
											2017-05-26 12:11:39 -06:00
										 |  |  |       } | 
					
						
							|  |  |  |       data.tunnelUrl = 'wss://' + decoded.aud + '/'; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-10-19 17:45:05 -06:00
										 |  |  |     data.domains = (decoded.domains || []).slice().sort().join(','); | 
					
						
							|  |  |  |     if (!data.domains) { | 
					
						
							|  |  |  |       throw new Error('JWT contains no domains to be forwarded'); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     return data; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   async function removeToken(data) { | 
					
						
							|  |  |  |     data = fillData(data); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     // Not sure if we might want to throw an error indicating 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; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     console.log('removing token from tunnel at', data.tunnelUrl); | 
					
						
							|  |  |  |     return activeTunnels[data.tunnelUrl].clear(data.jwt).then(function () { | 
					
						
							|  |  |  |       delete activeDomains[data.domains]; | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   async function addToken(data) { | 
					
						
							|  |  |  |     data = fillData(data); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if (activeDomains[data.domains]) { | 
					
						
							|  |  |  |       // If already have a token with the exact same domains and to the same tunnel
 | 
					
						
							|  |  |  |       // server there isn't really a need to add a new one
 | 
					
						
							|  |  |  |       if (activeDomains[data.domains].tunnelUrl === data.tunnelUrl) { | 
					
						
							|  |  |  |         return; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |       // Otherwise we want to detach from the other tunnel server in favor of the new one
 | 
					
						
							|  |  |  |       console.warn('added token with the exact same domains as another'); | 
					
						
							|  |  |  |       await removeToken(activeDomains[data.domains]); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-05-26 12:11:39 -06:00
										 |  |  |     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 | 
					
						
							| 
									
										
										
										
											2017-10-31 18:10:46 -06:00
										 |  |  |       , net: customNet | 
					
						
							| 
									
										
										
										
											2017-05-26 12:11:39 -06:00
										 |  |  |         // 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 } | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |       }); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-10-19 17:45:05 -06:00
										 |  |  |     console.log('appending token to tunnel at', data.tunnelUrl, 'for domains', data.domains); | 
					
						
							|  |  |  |     await activeTunnels[data.tunnelUrl].append(data.jwt); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     // Now that we know the tunnel server accepted our token we can save it
 | 
					
						
							|  |  |  |     // to keep record of what domains we are handling and what tunnel server
 | 
					
						
							|  |  |  |     // those domains should go to.
 | 
					
						
							|  |  |  |     activeDomains[data.domains] = data; | 
					
						
							| 
									
										
										
										
											2017-10-20 18:02:55 -06:00
										 |  |  | 
 | 
					
						
							|  |  |  |     // This is mostly for the start, but return the host for the tunnel server
 | 
					
						
							|  |  |  |     // we've connected to (after stripping the protocol and path away).
 | 
					
						
							|  |  |  |     return data.tunnelUrl.replace(/^[a-z]*:\/\//i, '').replace(/\/.*/, ''); | 
					
						
							| 
									
										
										
										
											2017-05-26 12:11:39 -06:00
										 |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-10-19 17:45:05 -06:00
										 |  |  |   async function acquireToken(session, domains) { | 
					
						
							| 
									
										
										
										
											2017-09-28 11:18:44 -06:00
										 |  |  |     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); | 
					
						
							| 
									
										
										
										
											2017-10-19 17:45:05 -06:00
										 |  |  |     var opts = { | 
					
						
							|  |  |  |       api: 'tunnel.token' | 
					
						
							|  |  |  |     , session: session | 
					
						
							|  |  |  |     , data: { | 
					
						
							|  |  |  |         domains: domains | 
					
						
							|  |  |  |       , device: { | 
					
						
							|  |  |  |           hostname: config.device.hostname | 
					
						
							|  |  |  |         , id: config.device.uid || config.device.id | 
					
						
							| 
									
										
										
										
											2017-09-28 11:18:44 -06:00
										 |  |  |         } | 
					
						
							| 
									
										
										
										
											2017-05-29 15:14:37 -06:00
										 |  |  |       } | 
					
						
							| 
									
										
										
										
											2017-10-19 17:45:05 -06:00
										 |  |  |     }; | 
					
						
							| 
									
										
										
										
											2017-05-29 15:14:37 -06:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-10-19 17:45:05 -06:00
										 |  |  |     var directives = await OAUTH3.discover(session.token.aud); | 
					
						
							|  |  |  |     var tokenData = await OAUTH3.api(directives.api, opts); | 
					
						
							| 
									
										
										
										
											2017-10-20 18:02:55 -06:00
										 |  |  |     return addToken(tokenData); | 
					
						
							| 
									
										
										
										
											2017-05-29 15:14:37 -06:00
										 |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-09-28 11:18:44 -06:00
										 |  |  |   function disconnectAll() { | 
					
						
							|  |  |  |     Object.keys(activeTunnels).forEach(function (key) { | 
					
						
							|  |  |  |       activeTunnels[key].end(); | 
					
						
							| 
									
										
										
										
											2017-07-31 18:35:49 -06:00
										 |  |  |     }); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-10-19 17:45:05 -06:00
										 |  |  |   function currentTokens() { | 
					
						
							|  |  |  |     return JSON.parse(JSON.stringify(activeDomains)); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-05-26 12:11:39 -06:00
										 |  |  |   return { | 
					
						
							| 
									
										
										
										
											2017-09-28 11:18:44 -06:00
										 |  |  |     start:       acquireToken | 
					
						
							|  |  |  |   , startDirect: addToken | 
					
						
							|  |  |  |   , remove:      removeToken | 
					
						
							|  |  |  |   , disconnect:  disconnectAll | 
					
						
							| 
									
										
										
										
											2017-10-19 17:45:05 -06:00
										 |  |  |   , current:     currentTokens | 
					
						
							| 
									
										
										
										
											2017-05-26 12:11:39 -06:00
										 |  |  |   }; | 
					
						
							|  |  |  | }; |