looking at active tunnel session on DDNS config update
This commit is contained in:
parent
c23f5ae25b
commit
acf2fd7764
|
@ -13,4 +13,5 @@
|
|||
, "latedef": true
|
||||
, "curly": true
|
||||
, "trailing": true
|
||||
, "esversion": 6
|
||||
}
|
||||
|
|
|
@ -335,6 +335,7 @@ class ConfigChanger {
|
|||
constructor(start) {
|
||||
Object.assign(this, JSON.parse(JSON.stringify(start)));
|
||||
delete this.device;
|
||||
delete this.debug;
|
||||
|
||||
this.domains = new DomainList(this.domains);
|
||||
this.http.modules = new ModuleList(this.http.modules);
|
||||
|
|
|
@ -4,19 +4,9 @@ module.exports.create = function (deps, conf) {
|
|||
var network = deps.PromiseA.promisifyAll(deps.recase.camelCopy(require('network')));
|
||||
var loopback = require('./loopback').create(deps, conf);
|
||||
var dnsCtrl = require('./dns-ctrl').create(deps, conf);
|
||||
var equal = require('deep-equal');
|
||||
|
||||
var loopbackDomain;
|
||||
function updateConf() {
|
||||
loopbackDomain = 'oauth3.org';
|
||||
if (conf.ddns && conf.ddns.loopback) {
|
||||
if (conf.ddns.loopback.type === 'tunnel@oauth3.org' && conf.ddns.loopback.domain) {
|
||||
loopbackDomain = conf.ddns.loopback.domain;
|
||||
} else {
|
||||
console.error('invalid loopback configuration: bad type or missing domain');
|
||||
}
|
||||
}
|
||||
}
|
||||
updateConf();
|
||||
|
||||
function iterateAllModules(action) {
|
||||
var promises = conf.ddns.modules.map(function (mod) {
|
||||
|
@ -57,13 +47,13 @@ module.exports.create = function (deps, conf) {
|
|||
tunnelSession = await deps.storage.tokens.get(conf.ddns.tunnel.tokenId);
|
||||
}
|
||||
|
||||
await iterateAllModules(function startTunnel(mod, domainsList) {
|
||||
await iterateAllModules(function startTunnel(mod, domainList) {
|
||||
if (mod.type !== 'dns@oauth3.org') { return null; }
|
||||
|
||||
return getSession(mod.token_id).then(function (dnsSession) {
|
||||
return deps.tunnelClients.start(tunnelSession || dnsSession, domainsList);
|
||||
return getSession(mod.tokenId).then(function (dnsSession) {
|
||||
return deps.tunnelClients.start(tunnelSession || dnsSession, domainList);
|
||||
}).catch(function (err) {
|
||||
console.log('error starting tunnel for', domainsList.join(', '));
|
||||
console.log('error starting tunnel for', domainList.join(', '));
|
||||
console.log(err);
|
||||
});
|
||||
});
|
||||
|
@ -73,6 +63,44 @@ module.exports.create = function (deps, conf) {
|
|||
async function disconnectTunnel() {
|
||||
deps.tunnelClients.disconnect();
|
||||
tunnelActive = false;
|
||||
await Promise.resolve();
|
||||
}
|
||||
async function checkTunnelTokens() {
|
||||
var oldTokens = deps.tunnelClients.current();
|
||||
|
||||
var newTokens = await iterateAllModules(function checkTokens(mod, domainList) {
|
||||
if (mod.type !== 'dns@oauth3.org') { return null; }
|
||||
|
||||
var domainStr = domainList.slice().sort().join(',');
|
||||
// If there is already a token handling exactly the domains this modules
|
||||
// needs handled remove it from the list of tokens to be removed. Otherwise
|
||||
// return the module and domain list so we can get new tokens.
|
||||
if (oldTokens[domainStr]) {
|
||||
delete oldTokens[domainStr];
|
||||
} else {
|
||||
return Promise.resolve({ mod, domainList });
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.all(Object.values(oldTokens).map(deps.tunnelClients.remove));
|
||||
|
||||
if (!newTokens.length) { return; }
|
||||
|
||||
var tunnelSession;
|
||||
if (conf.ddns.tunnel) {
|
||||
// In the case of a non-existant token, I'm not sure if we want to throw here and prevent
|
||||
// any tunnel connections, or if we want to ignore it and fall back to the DNS tokens
|
||||
tunnelSession = await deps.storage.tokens.get(conf.ddns.tunnel.tokenId);
|
||||
}
|
||||
|
||||
await Promise.all(newTokens.map(function startTunnel({mod, domainList}) {
|
||||
return getSession(mod.tokenId).then(function (dnsSession) {
|
||||
return deps.tunnelClients.start(tunnelSession || dnsSession, domainList);
|
||||
}).catch(function (err) {
|
||||
console.log('error starting tunnel for', domainList.join(', '));
|
||||
console.log(err);
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
var localAddr, gateway;
|
||||
|
@ -128,13 +156,13 @@ module.exports.create = function (deps, conf) {
|
|||
}
|
||||
publicAddress = addr;
|
||||
|
||||
await iterateAllModules(function setModuleDNS(mod, domainsList) {
|
||||
await iterateAllModules(function setModuleDNS(mod, domainList) {
|
||||
if (mod.type !== 'dns@oauth3.org' || mod.disabled) { return null; }
|
||||
|
||||
return getSession(mod.token_id).then(function (session) {
|
||||
return dnsCtrl.setDeviceAddress(session, addr, domainsList);
|
||||
return getSession(mod.tokenId).then(function (session) {
|
||||
return dnsCtrl.setDeviceAddress(session, addr, domainList);
|
||||
}).catch(function (err) {
|
||||
console.log('error setting DNS records for', domainsList.join(', '));
|
||||
console.log('error setting DNS records for', domainList.join(', '));
|
||||
console.log(err);
|
||||
});
|
||||
});
|
||||
|
@ -143,6 +171,38 @@ module.exports.create = function (deps, conf) {
|
|||
recheckPubAddr();
|
||||
setInterval(recheckPubAddr, 5*60*1000);
|
||||
|
||||
var curConf;
|
||||
function updateConf() {
|
||||
if (curConf && equal(curConf.ddns, conf.ddns) && equal(curConf.domains, conf.domains)) {
|
||||
// We could update curConf, but since everything we care about is the same...
|
||||
return;
|
||||
}
|
||||
|
||||
if (!curConf || !equal(curConf.ddns.loopback, conf.ddns.loopback)) {
|
||||
loopbackDomain = 'oauth3.org';
|
||||
if (conf.ddns && conf.ddns.loopback) {
|
||||
if (conf.ddns.loopback.type === 'tunnel@oauth3.org' && conf.ddns.loopback.domain) {
|
||||
loopbackDomain = conf.ddns.loopback.domain;
|
||||
} else {
|
||||
console.error('invalid loopback configuration: bad type or missing domain');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (tunnelActive) {
|
||||
if (!curConf || !equal(curConf.ddns.tunnel, conf.ddns.tunnel)) {
|
||||
disconnectTunnel().then(connectTunnel);
|
||||
} else {
|
||||
checkTunnelTokens();
|
||||
}
|
||||
}
|
||||
|
||||
// We need to make a deep copy of the config so we can use it next time to
|
||||
// compare and see what setup/cleanup is needed to adapt to the changes.
|
||||
curConf = JSON.parse(JSON.stringify(conf));
|
||||
}
|
||||
updateConf();
|
||||
|
||||
return {
|
||||
loopbackServer: loopback.server
|
||||
, setDeviceAddress: dnsCtrl.setDeviceAddress
|
||||
|
|
|
@ -141,9 +141,8 @@ module.exports.create = function (deps, conf) {
|
|||
}
|
||||
|
||||
return read(self._filename).then(function (tokens) {
|
||||
return self._convertToken(id, tokens[id]);
|
||||
}).then(function (session) {
|
||||
self._cache[id] = session;
|
||||
self._cache[id] = self._convertToken(id, tokens[id]);
|
||||
return self._cache[id];
|
||||
});
|
||||
}
|
||||
, save: function saveUserToken(newToken) {
|
||||
|
|
|
@ -2,31 +2,68 @@
|
|||
|
||||
module.exports.create = function (deps, config) {
|
||||
var stunnel = require('stunnel');
|
||||
var jwt = require('jsonwebtoken');
|
||||
var activeTunnels = {};
|
||||
var activeDomains = {};
|
||||
|
||||
function addToken(data) {
|
||||
function fillData(data) {
|
||||
if (typeof data === 'string') {
|
||||
data = { jwt: data };
|
||||
}
|
||||
|
||||
if (!data.jwt) {
|
||||
return deps.PromiseA.reject(new Error("missing 'jwt' from tunnel data"));
|
||||
throw new Error("missing 'jwt' from tunnel data");
|
||||
}
|
||||
var decoded = jwt.decode(data.jwt);
|
||||
if (!decoded) {
|
||||
throw new Error('invalid JWT');
|
||||
}
|
||||
|
||||
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);
|
||||
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
|
||||
|
@ -48,11 +85,16 @@ module.exports.create = function (deps, config) {
|
|||
});
|
||||
}
|
||||
|
||||
console.log('appending token to tunnel at', data.tunnelUrl);
|
||||
return activeTunnels[data.tunnelUrl].append(data.jwt);
|
||||
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;
|
||||
}
|
||||
|
||||
function acquireToken(session, domains) {
|
||||
async function acquireToken(session, domains) {
|
||||
var OAUTH3 = deps.OAUTH3;
|
||||
|
||||
// The OAUTH3 library stores some things on the root session object that we usually
|
||||
|
@ -62,51 +104,21 @@ module.exports.create = function (deps, config) {
|
|||
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
|
||||
}
|
||||
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);
|
||||
var directives = await OAUTH3.discover(session.token.aud);
|
||||
var tokenData = await OAUTH3.api(directives.api, opts);
|
||||
await addToken(tokenData);
|
||||
}
|
||||
|
||||
function disconnectAll() {
|
||||
|
@ -115,10 +127,15 @@ module.exports.create = function (deps, config) {
|
|||
});
|
||||
}
|
||||
|
||||
function currentTokens() {
|
||||
return JSON.parse(JSON.stringify(activeDomains));
|
||||
}
|
||||
|
||||
return {
|
||||
start: acquireToken
|
||||
, startDirect: addToken
|
||||
, remove: removeToken
|
||||
, disconnect: disconnectAll
|
||||
, current: currentTokens
|
||||
};
|
||||
};
|
||||
|
|
|
@ -427,6 +427,11 @@
|
|||
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
|
||||
"integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA="
|
||||
},
|
||||
"deep-equal": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz",
|
||||
"integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU="
|
||||
},
|
||||
"delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
|
|
|
@ -41,6 +41,7 @@
|
|||
"bluebird": "^3.4.6",
|
||||
"body-parser": "git+https://github.com/expressjs/body-parser.git#1.16.1",
|
||||
"commander": "^2.9.0",
|
||||
"deep-equal": "^1.0.1",
|
||||
"dns-suite": "git+https://git@git.daplie.com/Daplie/dns-suite#v1",
|
||||
"express": "git+https://github.com/expressjs/express.git#4.x",
|
||||
"finalhandler": "^0.4.0",
|
||||
|
|
Loading…
Reference in New Issue