'use strict'; 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 iterateAllModules(action) { var promises = conf.ddns.modules.map(function (mod) { return action(mod, mod.domains); }); conf.domains.forEach(function (dom) { if (!dom.modules || !Array.isArray(dom.modules.ddns) || !dom.modules.ddns.length) { return null; } // For the time being all of our things should only be tried once (regardless if it succeeded) // TODO: revisit this behavior when we support multiple ways of setting records, and/or // if we want to allow later modules to run if early modules fail. promises.push(dom.modules.ddns.reduce(function (prom, mod) { if (prom) { return prom; } return action(mod, dom.names); }, null)); }); return deps.PromiseA.all(promises.filter(Boolean)); } async function getSession(id) { var session = await deps.storage.tokens.get(id); if (!session) { throw new Error('no user token with ID "'+id+'"'); } return session; } var tunnelActive = false; async function connectTunnel() { 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 iterateAllModules(function startTunnel(mod, domainList) { if (mod.type !== 'dns@oauth3.org') { return null; } 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); }); }); tunnelActive = true; } 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; async function checkNetworkEnv() { // Since we can't detect the OS level events when a user plugs in an ethernet cable to recheck // what network environment we are in we check our local network address and the gateway to // determine if we need to run the loopback check and router configuration again. var gw = await network.getGatewayIpAsync(); var addr = await network.getPrivateIpAsync(); if (localAddr === addr && gateway === gw) { return; } localAddr = addr; gateway = gw; var loopResult = await loopback(loopbackDomain); var notLooped = Object.keys(loopResult.ports).filter(function (port) { return !loopResult.ports[port]; }); // if (notLooped.length) { // // TODO: try to automatically configure router to forward ports to us. // } // If we are on a public address or all ports we are listening on are forwarded to us then // we don't need the tunnel and we can set the DNS records for all our domains to our public // address. Otherwise we need to use the tunnel to accept traffic. if (!notLooped.length) { if (tunnelActive) { await disconnectTunnel(); } } else { if (!tunnelActive) { await connectTunnel(); } } } var publicAddress; async function recheckPubAddr() { await checkNetworkEnv(); if (tunnelActive) { return; } var addr = await loopback.checkPublicAddr(loopbackDomain); if (publicAddress === addr) { return; } if (conf.debug) { console.log('previous public address',publicAddress, 'does not match current public address', addr); } publicAddress = addr; await iterateAllModules(function setModuleDNS(mod, domainList) { if (mod.type !== 'dns@oauth3.org' || mod.disabled) { return null; } return getSession(mod.tokenId).then(function (session) { return dnsCtrl.setDeviceAddress(session, addr, domainList); }).catch(function (err) { console.log('error setting DNS records for', domainList.join(', ')); console.log(err); }); }); } 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 , getDeviceAddresses: dnsCtrl.getDeviceAddresses , recheckPubAddr: recheckPubAddr , updateConf: updateConf }; };