diff --git a/bin/goldilocks.js b/bin/goldilocks.js index ab680d0..8522f81 100755 --- a/bin/goldilocks.js +++ b/bin/goldilocks.js @@ -154,6 +154,9 @@ function fillConfig(config, args) { if (!config.dns) { config.dns = { bind: [ 53 ], modules: [{ name: 'proxy', port: 3053 }] }; } + if (!config.ddns) { + config.ddns = { enabled: false }; + } // Use Object.assign to add any properties needed but not defined in the mdns config. // It will first copy the defaults into an empty object, then copy any real config over that. var mdnsDefaults = { port: 5353, broadcast: '224.0.0.251', ttl: 300 }; diff --git a/lib/ddns.js b/lib/ddns.js index 2ed1cca..2c44f12 100644 --- a/lib/ddns.js +++ b/lib/ddns.js @@ -1,88 +1,116 @@ 'use strict'; -module.exports.create = function (opts/*, servers*/) { - var PromiseA = opts.PromiseA; - var dns = PromiseA.promisifyAll(require('dns')); +module.exports.create = function (deps, conf) { + var PromiseA = deps.PromiseA; + var request = PromiseA.promisify(require('request')); + var OAUTH3 = require('../packages/assets/org.oauth3'); + require('../packages/assets/org.oauth3/oauth3.dns.js'); - return PromiseA.all([ - dns.resolve4Async(opts._old_server_name).then(function (results) { - return results; - }, function () {}) - , dns.resolve6Async(opts._old_server_name).then(function (results) { - return results; - }, function () {}) - ]).then(function (results) { - var ipv4 = results[0] || []; - var ipv6 = results[1] || []; - var record; + function dnsType(addr) { + if (/^\d+\.\d+\.\d+\.\d+$/.test(addr)) { + return 'A'; + } + if (-1 !== addr.indexOf(':') && /^[a-f:\.\d]+$/i.test(addr)) { + return 'AAAA'; + } + } - opts.dnsRecords = { - A: ipv4 - , AAAA: ipv6 - }; + function setDeviceAddress(addr) { + return deps.storage.owners.all().then(function (sessions) { + return sessions.filter(function (sess) { + return sess.token.scp.indexOf('dns') >= 0; + })[0]; + }).then(function (session) { + if (!session) { + return PromiseA.reject(new Error('no sessions with DNS grants')); + } - Object.keys(opts.ifaces).some(function (ifacename) { - var iface = opts.ifaces[ifacename]; - - return iface.ipv4.some(function (localIp) { - return ipv4.some(function (remoteIp) { - if (localIp.address === remoteIp) { - record = localIp; - return record; + return OAUTH3.discover(session.aud).then(function (directives) { + return request({ + url: 'https://'+directives.api+'/api/org.oauth3.dns/acl/devices/' + conf.device.hostname + , method: 'POST' + , headers: { + 'Authorization': 'Bearer ' + session.refresh_token + , 'Accept': 'application/json; charset=utf-8' } - }); - }) || iface.ipv6.some(function (localIp) { - return ipv6.forEach(function (remoteIp) { - if (localIp.address === remoteIp) { - record = localIp; - return record; + , json: { + addresses: [ + { value: addr, type: dnsType(addr) } + ] } }); }); }); + } - if (!record) { - console.info("DNS Record '" + ipv4.concat(ipv6).join(',') + "' does not match any local IP address."); - console.info("Use --ddns to allow the people of the Internet to access your server."); + function getDeviceAddresses() { + return deps.storage.owners.all().then(function (sessions) { + return sessions.filter(function (sess) { + return sess.token.scp.indexOf('dns') >= 0; + })[0]; + }).then(function (session) { + if (!session) { + return PromiseA.reject(new Error('no sessions with DNS grants')); + } + + return OAUTH3.discover(session.aud).then(function (directives) { + return request({ + url: 'https://'+directives.api+'/api/org.oauth3.dns/acl/devices' + , method: 'GET' + , headers: { + 'Authorization': 'Bearer ' + session.refresh_token + , 'Accept': 'application/json; charset=utf-8' + } + , json: true + }); + }).then(function (result) { + if (!result.body) { + return PromiseA.reject(new Error('No response body in request for device addresses')); + } + if (result.body.error) { + var err = new Error(result.body.error.message); + return PromiseA.reject(Object.assign(err, result.body.error)); + } + return result.body.devices.filter(function (dev) { + return dev.name === conf.device.hostname; + })[0]; + }).then(function (dev) { + return (dev || {}).addresses || []; + }); + }); + } + + var publicAddress; + function recheckPubAddr() { + if (!conf.ddns.enabled) { + return; } - opts.externalIps.ipv4.some(function (localIp) { - return ipv4.some(function (remoteIp) { - if (localIp.address === remoteIp) { - record = localIp; - return record; + deps.storage.owners.all().then(function (sessions) { + return sessions.filter(function (sess) { + return sess.token.scp.indexOf('dns') >= 0; + })[0]; + }).then(function (session) { + if (!session) { + return; + } + + OAUTH3.discover(session.aud).then(function (directives) { + return deps.loopback.checkPublicAddr(directives.api); + }).then(function (addr) { + if (publicAddress !== addr) { + publicAddress = addr; + setDeviceAddress(addr); } }); }); + } - opts.externalIps.ipv6.some(function (localIp) { - return ipv6.some(function (remoteIp) { - if (localIp.address === remoteIp) { - record = localIp; - return record; - } - }); - }); + setInterval(recheckPubAddr, 5*60*1000); - if (!record) { - console.info("DNS Record '" + ipv4.concat(ipv6).join(',') + "' does not match any local IP address."); - console.info("Use --ddns to allow the people of the Internet to access your server."); - } - }); -}; - -if (require.main === module) { - var opts = { - _old_server_name: 'aj.daplie.me' - , PromiseA: require('bluebird') + return { + setDeviceAddress: setDeviceAddress + , getDeviceAddresses: getDeviceAddresses + , recheckPubAddr: recheckPubAddr }; - // ifaces - opts.ifaces = require('./local-ip.js').find(); - console.log('opts.ifaces'); - console.log(opts.ifaces); - require('./match-ips.js').match(opts._old_server_name, opts).then(function (ips) { - opts.matchingIps = ips.matchingIps || []; - opts.externalIps = ips.externalIps; - module.exports.create(opts); - }); -} +}; diff --git a/lib/loopback.js b/lib/loopback.js index fd827cf..906aae7 100644 --- a/lib/loopback.js +++ b/lib/loopback.js @@ -1,6 +1,6 @@ 'use strict'; -module.exports.create = function (deps) { +module.exports.create = function (deps, conf) { var PromiseA = require('bluebird'); var request = PromiseA.promisify(require('request')); var pending = {}; @@ -49,7 +49,7 @@ module.exports.create = function (deps) { // If the loopback requests don't go to us then there are all kinds of ways it could // error, but none of them really provide much extra information so we don't do // anything that will break the PromiseA.all out and mask the other results. - if (result.body.error) { + if (conf.debug && result.body.error) { console.log('error on remote side of port '+port+' loopback', result.body.error); } return !!result.body.success; @@ -68,7 +68,10 @@ module.exports.create = function (deps) { return checkSinglePort(directives.api, address, port); })) .then(function (values) { - console.log(pending); + if (conf.debug) { + console.log('remaining loopback tokens', pending); + } + var result = {error: null, address: address}; ports.forEach(function (port, ind) { result[port] = values[ind]; @@ -79,6 +82,7 @@ module.exports.create = function (deps) { }); } + loopback.checkPublicAddr = checkPublicAddr; loopback.server = require('http').createServer(function (req, res) { var parsed = require('url').parse(req.url); var token = parsed.pathname.replace('/.well-known/cloud-challenge/', ''); diff --git a/lib/match-ips.js b/lib/match-ips.js deleted file mode 100644 index dbb3ff1..0000000 --- a/lib/match-ips.js +++ /dev/null @@ -1,117 +0,0 @@ -'use strict'; - -var PromiseA = require('bluebird'); - -module.exports.match = function (servername, opts) { - return PromiseA.promisify(require('ipify'))().then(function (externalIp) { - var dns = PromiseA.promisifyAll(require('dns')); - - opts.externalIps = [ { address: externalIp, family: 'IPv4' } ]; - opts.ifaces = require('./local-ip.js').find({ externals: opts.externalIps }); - opts.externalIfaces = Object.keys(opts.ifaces).reduce(function (all, iname) { - var iface = opts.ifaces[iname]; - - iface.ipv4.forEach(function (addr) { - if (addr.external) { - addr.iface = iname; - all.push(addr); - } - }); - iface.ipv6.forEach(function (addr) { - if (addr.external) { - addr.iface = iname; - all.push(addr); - } - }); - - return all; - }, []).filter(Boolean); - - function resolveIps(hostname) { - var allIps = []; - - return PromiseA.all([ - dns.resolve4Async(hostname).then(function (records) { - records.forEach(function (ip) { - allIps.push({ - address: ip - , family: 'IPv4' - }); - }); - }, function () {}) - , dns.resolve6Async(hostname).then(function (records) { - records.forEach(function (ip) { - allIps.push({ - address: ip - , family: 'IPv6' - }); - }); - }, function () {}) - ]).then(function () { - return allIps; - }); - } - - function resolveIpsAndCnames(hostname) { - return PromiseA.all([ - resolveIps(hostname) - , dns.resolveCnameAsync(hostname).then(function (records) { - return PromiseA.all(records.map(function (hostname) { - return resolveIps(hostname); - })).then(function (allIps) { - return allIps.reduce(function (all, ips) { - return all.concat(ips); - }, []); - }); - }, function () { - return []; - }) - ]).then(function (ips) { - return ips.reduce(function (all, set) { - return all.concat(set); - }, []); - }); - } - - return resolveIpsAndCnames(servername).then(function (allIps) { - var matchingIps = []; - - if (!allIps.length) { - console.warn("Could not resolve '" + servername + "'"); - } - - // { address, family } - allIps.some(function (ip) { - function match(addr) { - if (ip.address === addr.address) { - matchingIps.push(addr); - } - } - - opts.externalIps.forEach(match); - // opts.externalIfaces.forEach(match); - - Object.keys(opts.ifaces).forEach(function (iname) { - var iface = opts.ifaces[iname]; - - iface.ipv4.forEach(match); - iface.ipv6.forEach(match); - }); - - return matchingIps.length; - }); - - matchingIps.externalIps = { - ipv4: [ - { address: externalIp - , family: 'IPv4' - } - ] - , ipv6: [ - ] - }; - matchingIps.matchingIps = matchingIps; - return matchingIps; - }); - }); -}; diff --git a/lib/worker.js b/lib/worker.js index 38816e1..cb953e1 100644 --- a/lib/worker.js +++ b/lib/worker.js @@ -31,6 +31,7 @@ function create(conf) { deps.proxy = require('./proxy-conn').create(deps, conf); deps.socks5 = require('./socks5-server').create(deps, conf); deps.loopback = require('./loopback').create(deps, conf); + deps.ddns = require('./ddns').create(deps, conf); require('./goldilocks.js').create(deps, conf); process.removeListener('message', create); diff --git a/package.json b/package.json index 8d7c537..c5e56ee 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,6 @@ "http-proxy": "^1.16.2", "human-readable-ids": "git+https://git.daplie.com/Daplie/human-readable-ids-js#master", "ipaddr.js": "git+https://github.com/whitequark/ipaddr.js.git#v1.3.0", - "ipify": "^1.1.0", "js-yaml": "^3.8.3", "jsonwebtoken": "^7.4.0", "le-challenge-ddns": "git+https://git.daplie.com/Daplie/le-challenge-ddns.git#master",