goldilocks.js/lib/tunnel-client-manager.js

125 lines
4.1 KiB
JavaScript

'use strict';
module.exports.create = function (deps, config) {
var stunnel = require('stunnel');
var activeTunnels = {};
function addToken(data) {
if (typeof data === 'string') {
data = { jwt: data };
}
if (!data.jwt) {
return deps.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 deps.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 deps.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 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);
return OAUTH3.discover(session.token.aud).then(function (directives) {
var opts = {
api: 'tunnel.token'
, session: session
, data: {
domains: domains
, device: {
hostname: config.device.hostname
, id: config.device.uid || config.device.id
}
}
};
return OAUTH3.api(directives.api, opts).then(addToken);
});
}
function removeToken(data) {
if (typeof data === 'string') {
data = { jwt: 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 deps.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 deps.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 deps.PromiseA.resolve();
}
console.log('removing token from tunnel at', data.tunnelUrl);
return activeTunnels[data.tunnelUrl].clear(data.jwt);
}
function disconnectAll() {
Object.keys(activeTunnels).forEach(function (key) {
activeTunnels[key].end();
});
}
return {
start: acquireToken
, startDirect: addToken
, remove: removeToken
, disconnect: disconnectAll
};
};