forked from coolaj86/goldilocks.js
238 lines
7.6 KiB
JavaScript
238 lines
7.6 KiB
JavaScript
'use strict';
|
|
|
|
module.exports.create = function (deps, config) {
|
|
var PromiseA = require('bluebird');
|
|
var fs = PromiseA.promisifyAll(require('fs'));
|
|
var stunnel = require('stunnel');
|
|
var activeTunnels = {};
|
|
|
|
var path = require('path');
|
|
var tokensPath = path.join(__dirname, '..', 'var', 'tokens.json');
|
|
var storage = {
|
|
_read: function () {
|
|
var tokens;
|
|
try {
|
|
tokens = require(tokensPath);
|
|
} catch (err) {
|
|
tokens = {};
|
|
}
|
|
return tokens;
|
|
}
|
|
, _write: function (tokens) {
|
|
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');
|
|
});
|
|
}
|
|
, _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');
|
|
}
|
|
|
|
, all: function () {
|
|
var tokens = storage._read();
|
|
return PromiseA.resolve(Object.keys(tokens).map(function (key) {
|
|
return tokens[key];
|
|
}));
|
|
}
|
|
, save: function (token) {
|
|
return PromiseA.resolve().then(function () {
|
|
var curTokens = storage._read();
|
|
curTokens[storage._makeKey(token.jwt)] = token;
|
|
return storage._write(curTokens);
|
|
});
|
|
}
|
|
, del: function (token) {
|
|
return PromiseA.resolve().then(function () {
|
|
var curTokens = storage._read();
|
|
delete curTokens[storage._makeKey(token.jwt)];
|
|
return storage._write(curTokens);
|
|
});
|
|
}
|
|
};
|
|
|
|
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;
|
|
});
|
|
});
|
|
}
|
|
|
|
function addToken(data) {
|
|
if (!data.jwt) {
|
|
return 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 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 + '/';
|
|
}
|
|
|
|
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 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);
|
|
}
|
|
|
|
if (config.tunnel) {
|
|
var confTokens = config.tunnel;
|
|
if (typeof confTokens === 'string') {
|
|
confTokens = confTokens.split(',');
|
|
}
|
|
confTokens.forEach(function (jwt) {
|
|
if (typeof jwt === 'object') {
|
|
jwt.owner = 'config';
|
|
addToken(jwt);
|
|
} else {
|
|
addToken({ jwt: jwt, owner: 'config' });
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
storage.all().then(function (stored) {
|
|
stored.forEach(function (result) {
|
|
addToken(result);
|
|
});
|
|
});
|
|
|
|
return {
|
|
start: function (session) {
|
|
return acquireToken(session).then(function (token) {
|
|
return addToken(token).then(function () {
|
|
return storage.save(token);
|
|
});
|
|
});
|
|
}
|
|
, add: function (data) {
|
|
return addToken(data).then(function () {
|
|
return storage.save(data);
|
|
});
|
|
}
|
|
, remove: function (data) {
|
|
return storage.del(data.jwt).then(function () {
|
|
return removeToken(data);
|
|
});
|
|
}
|
|
, 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;
|
|
});
|
|
}
|
|
};
|
|
};
|