Browse Source

looking at active tunnel session on DDNS config update

ddns
tigerbot 7 years ago
parent
commit
acf2fd7764
  1. 1
      .jshintrc
  2. 1
      lib/admin/config.js
  3. 98
      lib/ddns/index.js
  4. 5
      lib/storage.js
  5. 131
      lib/tunnel-client-manager.js
  6. 5
      package-lock.json
  7. 1
      package.json

1
.jshintrc

@ -13,4 +13,5 @@
, "latedef": true
, "curly": true
, "trailing": true
, "esversion": 6
}

1
lib/admin/config.js

@ -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);

98
lib/ddns/index.js

@ -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

5
lib/storage.js

@ -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) {

131
lib/tunnel-client-manager.js

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

5
package-lock.json

@ -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",

1
package.json

@ -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…
Cancel
Save