looking at active tunnel session on DDNS config update
This commit is contained in:
parent
c23f5ae25b
commit
acf2fd7764
|
@ -13,4 +13,5 @@
|
||||||
, "latedef": true
|
, "latedef": true
|
||||||
, "curly": true
|
, "curly": true
|
||||||
, "trailing": true
|
, "trailing": true
|
||||||
|
, "esversion": 6
|
||||||
}
|
}
|
||||||
|
|
|
@ -335,6 +335,7 @@ class ConfigChanger {
|
||||||
constructor(start) {
|
constructor(start) {
|
||||||
Object.assign(this, JSON.parse(JSON.stringify(start)));
|
Object.assign(this, JSON.parse(JSON.stringify(start)));
|
||||||
delete this.device;
|
delete this.device;
|
||||||
|
delete this.debug;
|
||||||
|
|
||||||
this.domains = new DomainList(this.domains);
|
this.domains = new DomainList(this.domains);
|
||||||
this.http.modules = new ModuleList(this.http.modules);
|
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 network = deps.PromiseA.promisifyAll(deps.recase.camelCopy(require('network')));
|
||||||
var loopback = require('./loopback').create(deps, conf);
|
var loopback = require('./loopback').create(deps, conf);
|
||||||
var dnsCtrl = require('./dns-ctrl').create(deps, conf);
|
var dnsCtrl = require('./dns-ctrl').create(deps, conf);
|
||||||
|
var equal = require('deep-equal');
|
||||||
|
|
||||||
var loopbackDomain;
|
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) {
|
function iterateAllModules(action) {
|
||||||
var promises = conf.ddns.modules.map(function (mod) {
|
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);
|
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; }
|
if (mod.type !== 'dns@oauth3.org') { return null; }
|
||||||
|
|
||||||
return getSession(mod.token_id).then(function (dnsSession) {
|
return getSession(mod.tokenId).then(function (dnsSession) {
|
||||||
return deps.tunnelClients.start(tunnelSession || dnsSession, domainsList);
|
return deps.tunnelClients.start(tunnelSession || dnsSession, domainList);
|
||||||
}).catch(function (err) {
|
}).catch(function (err) {
|
||||||
console.log('error starting tunnel for', domainsList.join(', '));
|
console.log('error starting tunnel for', domainList.join(', '));
|
||||||
console.log(err);
|
console.log(err);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -73,6 +63,44 @@ module.exports.create = function (deps, conf) {
|
||||||
async function disconnectTunnel() {
|
async function disconnectTunnel() {
|
||||||
deps.tunnelClients.disconnect();
|
deps.tunnelClients.disconnect();
|
||||||
tunnelActive = false;
|
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;
|
var localAddr, gateway;
|
||||||
|
@ -128,13 +156,13 @@ module.exports.create = function (deps, conf) {
|
||||||
}
|
}
|
||||||
publicAddress = addr;
|
publicAddress = addr;
|
||||||
|
|
||||||
await iterateAllModules(function setModuleDNS(mod, domainsList) {
|
await iterateAllModules(function setModuleDNS(mod, domainList) {
|
||||||
if (mod.type !== 'dns@oauth3.org' || mod.disabled) { return null; }
|
if (mod.type !== 'dns@oauth3.org' || mod.disabled) { return null; }
|
||||||
|
|
||||||
return getSession(mod.token_id).then(function (session) {
|
return getSession(mod.tokenId).then(function (session) {
|
||||||
return dnsCtrl.setDeviceAddress(session, addr, domainsList);
|
return dnsCtrl.setDeviceAddress(session, addr, domainList);
|
||||||
}).catch(function (err) {
|
}).catch(function (err) {
|
||||||
console.log('error setting DNS records for', domainsList.join(', '));
|
console.log('error setting DNS records for', domainList.join(', '));
|
||||||
console.log(err);
|
console.log(err);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -143,6 +171,38 @@ module.exports.create = function (deps, conf) {
|
||||||
recheckPubAddr();
|
recheckPubAddr();
|
||||||
setInterval(recheckPubAddr, 5*60*1000);
|
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 {
|
return {
|
||||||
loopbackServer: loopback.server
|
loopbackServer: loopback.server
|
||||||
, setDeviceAddress: dnsCtrl.setDeviceAddress
|
, setDeviceAddress: dnsCtrl.setDeviceAddress
|
||||||
|
|
|
@ -141,9 +141,8 @@ module.exports.create = function (deps, conf) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return read(self._filename).then(function (tokens) {
|
return read(self._filename).then(function (tokens) {
|
||||||
return self._convertToken(id, tokens[id]);
|
self._cache[id] = self._convertToken(id, tokens[id]);
|
||||||
}).then(function (session) {
|
return self._cache[id];
|
||||||
self._cache[id] = session;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
, save: function saveUserToken(newToken) {
|
, save: function saveUserToken(newToken) {
|
||||||
|
|
|
@ -2,31 +2,68 @@
|
||||||
|
|
||||||
module.exports.create = function (deps, config) {
|
module.exports.create = function (deps, config) {
|
||||||
var stunnel = require('stunnel');
|
var stunnel = require('stunnel');
|
||||||
|
var jwt = require('jsonwebtoken');
|
||||||
var activeTunnels = {};
|
var activeTunnels = {};
|
||||||
|
var activeDomains = {};
|
||||||
|
|
||||||
function addToken(data) {
|
function fillData(data) {
|
||||||
if (typeof data === 'string') {
|
if (typeof data === 'string') {
|
||||||
data = { jwt: data };
|
data = { jwt: data };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!data.jwt) {
|
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) {
|
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) {
|
if (!decoded.aud) {
|
||||||
console.warn('tunnel manager given token with no tunnelUrl or audience');
|
throw new Error('missing tunnelUrl and audience');
|
||||||
var err = new Error('missing tunnelUrl and audience');
|
|
||||||
return deps.PromiseA.reject(err);
|
|
||||||
}
|
}
|
||||||
data.tunnelUrl = 'wss://' + decoded.aud + '/';
|
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]) {
|
if (!activeTunnels[data.tunnelUrl]) {
|
||||||
console.log('creating new tunnel client for', 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
|
// 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);
|
console.log('appending token to tunnel at', data.tunnelUrl, 'for domains', data.domains);
|
||||||
return activeTunnels[data.tunnelUrl].append(data.jwt);
|
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;
|
var OAUTH3 = deps.OAUTH3;
|
||||||
|
|
||||||
// The OAUTH3 library stores some things on the root session object that we usually
|
// 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;
|
session.scope = session.scope || session.token.scp;
|
||||||
|
|
||||||
console.log('asking for tunnel token from', session.token.aud);
|
console.log('asking for tunnel token from', session.token.aud);
|
||||||
return OAUTH3.discover(session.token.aud).then(function (directives) {
|
var opts = {
|
||||||
var opts = {
|
api: 'tunnel.token'
|
||||||
api: 'tunnel.token'
|
, session: session
|
||||||
, session: session
|
, data: {
|
||||||
, data: {
|
domains: domains
|
||||||
domains: domains
|
, device: {
|
||||||
, device: {
|
hostname: config.device.hostname
|
||||||
hostname: config.device.hostname
|
, id: config.device.uid || config.device.id
|
||||||
, 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
|
var directives = await OAUTH3.discover(session.token.aud);
|
||||||
// server that existed, but since it never existed we can consider it as "removed".
|
var tokenData = await OAUTH3.api(directives.api, opts);
|
||||||
if (!activeTunnels[data.tunnelUrl]) {
|
await addToken(tokenData);
|
||||||
return deps.PromiseA.resolve();
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('removing token from tunnel at', data.tunnelUrl);
|
|
||||||
return activeTunnels[data.tunnelUrl].clear(data.jwt);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function disconnectAll() {
|
function disconnectAll() {
|
||||||
|
@ -115,10 +127,15 @@ module.exports.create = function (deps, config) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function currentTokens() {
|
||||||
|
return JSON.parse(JSON.stringify(activeDomains));
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
start: acquireToken
|
start: acquireToken
|
||||||
, startDirect: addToken
|
, startDirect: addToken
|
||||||
, remove: removeToken
|
, remove: removeToken
|
||||||
, disconnect: disconnectAll
|
, disconnect: disconnectAll
|
||||||
|
, current: currentTokens
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -427,6 +427,11 @@
|
||||||
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
|
||||||
"integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA="
|
"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": {
|
"delayed-stream": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||||
|
|
|
@ -41,6 +41,7 @@
|
||||||
"bluebird": "^3.4.6",
|
"bluebird": "^3.4.6",
|
||||||
"body-parser": "git+https://github.com/expressjs/body-parser.git#1.16.1",
|
"body-parser": "git+https://github.com/expressjs/body-parser.git#1.16.1",
|
||||||
"commander": "^2.9.0",
|
"commander": "^2.9.0",
|
||||||
|
"deep-equal": "^1.0.1",
|
||||||
"dns-suite": "git+https://git@git.daplie.com/Daplie/dns-suite#v1",
|
"dns-suite": "git+https://git@git.daplie.com/Daplie/dns-suite#v1",
|
||||||
"express": "git+https://github.com/expressjs/express.git#4.x",
|
"express": "git+https://github.com/expressjs/express.git#4.x",
|
||||||
"finalhandler": "^0.4.0",
|
"finalhandler": "^0.4.0",
|
||||||
|
|
Loading…
Reference in New Issue