forked from coolaj86/goldilocks.js
		
	
		
			
				
	
	
		
			192 lines
		
	
	
		
			6.7 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			192 lines
		
	
	
		
			6.7 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| 'use strict';
 | |
| 
 | |
| module.exports.create = function (deps, config) {
 | |
|   var stunnel = require('stunnel');
 | |
|   var jwt = require('jsonwebtoken');
 | |
|   var activeTunnels = {};
 | |
|   var activeDomains = {};
 | |
| 
 | |
|   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;
 | |
|     }
 | |
|   };
 | |
| 
 | |
|   function fillData(data) {
 | |
|     if (typeof data === 'string') {
 | |
|       data = { jwt: data };
 | |
|     }
 | |
| 
 | |
|     if (!data.jwt) {
 | |
|       throw new Error("missing 'jwt' from tunnel data");
 | |
|     }
 | |
|     var decoded = jwt.decode(data.jwt);
 | |
|     if (!decoded) {
 | |
|       throw new Error('invalid JWT');
 | |
|     }
 | |
| 
 | |
|     if (!data.tunnelUrl) {
 | |
|       if (!decoded.aud) {
 | |
|         throw new Error('missing tunnelUrl and audience');
 | |
|       }
 | |
|       data.tunnelUrl = 'wss://' + decoded.aud + '/';
 | |
|     }
 | |
| 
 | |
|     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]);
 | |
|     }
 | |
| 
 | |
|     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: customNet
 | |
|         // 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, '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;
 | |
| 
 | |
|     // 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(/\/.*/, '');
 | |
|   }
 | |
| 
 | |
|   async 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);
 | |
|     var opts = {
 | |
|       api: 'tunnel.token'
 | |
|     , session: session
 | |
|     , data: {
 | |
|         domains: domains
 | |
|       , device: {
 | |
|           hostname: config.device.hostname
 | |
|         , id: config.device.uid || config.device.id
 | |
|         }
 | |
|       }
 | |
|     };
 | |
| 
 | |
|     var directives = await OAUTH3.discover(session.token.aud);
 | |
|     var tokenData = await OAUTH3.api(directives.api, opts);
 | |
|     return addToken(tokenData);
 | |
|   }
 | |
| 
 | |
|   function disconnectAll() {
 | |
|     Object.keys(activeTunnels).forEach(function (key) {
 | |
|       activeTunnels[key].end();
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   function currentTokens() {
 | |
|     return JSON.parse(JSON.stringify(activeDomains));
 | |
|   }
 | |
| 
 | |
|   return {
 | |
|     start:       acquireToken
 | |
|   , startDirect: addToken
 | |
|   , remove:      removeToken
 | |
|   , disconnect:  disconnectAll
 | |
|   , current:     currentTokens
 | |
|   };
 | |
| };
 |