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
|
|
};
|
|
};
|