From 7d17139a4b39d5b5019303e32d224cc12cdebd70 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Mon, 5 Oct 2015 19:51:58 +0000 Subject: [PATCH] hourly update ip address --- ddns-client.js | 2 +- dyndns-token.sample.js | 1 + holepunch/helpers/update-ip.js | 21 +++++- lib/ddns-updater.js | 101 ++++++++++++++++++++++++++ lib/ip-checker.js | 126 +++++++++++++++++++++++++++++++++ lib/vhost-sni-server.js | 33 ++++++++- package.json | 2 +- 7 files changed, 280 insertions(+), 6 deletions(-) create mode 100644 dyndns-token.sample.js create mode 100644 lib/ddns-updater.js create mode 100644 lib/ip-checker.js diff --git a/ddns-client.js b/ddns-client.js index b883077..0b016ae 100755 --- a/ddns-client.js +++ b/ddns-client.js @@ -64,5 +64,5 @@ cli.main(function (args, options) { console.log(JSON.stringify(data, null, ' ')); console.log('Test with'); console.log('dig ' + options.hostname + ' ' + options.type); - }) + }); }); diff --git a/dyndns-token.sample.js b/dyndns-token.sample.js new file mode 100644 index 0000000..b5cefe5 --- /dev/null +++ b/dyndns-token.sample.js @@ -0,0 +1 @@ +module.exports = { token: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy.zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" }; diff --git a/holepunch/helpers/update-ip.js b/holepunch/helpers/update-ip.js index 8a3c7a9..3e3606b 100644 --- a/holepunch/helpers/update-ip.js +++ b/holepunch/helpers/update-ip.js @@ -11,6 +11,7 @@ module.exports.update = function (opts) { var options; var hostname = opts.updater || 'redirect-www.org'; var port = opts.port || 65443; + var req; options = { host: hostname @@ -49,7 +50,7 @@ module.exports.update = function (opts) { options.agent = new https.Agent(options); - https.request(options, function(res) { + req = https.request(options, function(res) { var textData = ''; res.on('error', function (err) { @@ -60,8 +61,22 @@ module.exports.update = function (opts) { // console.log(chunk.toString()); }); res.on('end', function () { - resolve(textData); + var err; + try { + resolve(JSON.parse(textData)); + } catch(e) { + err = new Error("Unparsable Server Response"); + err.code = 'E_INVALID_SERVER_RESPONSE'; + err.data = textData; + reject(err); + } }); - }).end(JSON.stringify(opts.ddns, null, ' ')); + }); + + req.on('error', function () { + reject(err); + }); + + req.end(JSON.stringify(opts.ddns, null, ' ')); }); }; diff --git a/lib/ddns-updater.js b/lib/ddns-updater.js new file mode 100644 index 0000000..cbc0589 --- /dev/null +++ b/lib/ddns-updater.js @@ -0,0 +1,101 @@ +'use strict'; + +var updateIp = require('../holepunch/helpers/update-ip.js').update; +// TODO XXX use API + storage +var token = require('../dyndns-token.js').token; + +/* + * @param {string[]} hostnames - A list of hostnames + * @param {Object[]} addresses - A list of { address: , family: <4|6> } + */ +function update(hostnames, addresses) { + // TODO use not-yet-built API to get and store tokens + // TODO use API to add and remove nameservers + var services = [ + // TODO XXX don't disable cacert checking + { hostname: 'ns1.redirect-www.org', port: 65443, cacert: false } + , { hostname: 'ns2.redirect-www.org', port: 65443, cacert: false } + ]; + var answers = []; + var promises; + var results = []; + var PromiseA; + + hostnames.forEach(function (hostname) { + addresses.forEach(function (address) { + var answer = { + "name": hostname + , "value": address.address + , "type": null + , "priority": null + , "token": token + }; + + if (4 === address.family) { + answer.type = 'A'; + } + else if (6 === address.family) { + answer.type = 'AAAA'; + } + else { + console.error('[ERROR] unspported address:'); + console.error(address); + return; + } + + answers.push(answer); + }); + }); + + promises = services.map(function (service, i) { + return updateIp({ + updater: service.hostname + , port: service.port + , cacert: service.cacert + , token: token + , ddns: answers + }).then(function (data) { + results[i] = { service: service, data: data }; + return data; + }).error(function (err) { + results[i] = { service: service, error: err }; + }); + }); + + PromiseA = require('bluebird').Promise; + return PromiseA.all(promises).then(function () { + return results; + }); +} + +module.exports.update = function () { + var allMap = {}; + var hostnames = require('../redirects.json').reduce(function (all, redirect) { + if (!allMap[redirect.from.hostname]) { + allMap[redirect.from.hostname] = true; + all.push(redirect.from.hostname); + } + if (!all[redirect.to.hostname]) { + allMap[redirect.to.hostname] = true; + all.push(redirect.to.hostname); + } + + return all; + }, []); + + return require('./ip-checker').getExternalAddresses().then(function (result) { + //console.log(Object.keys(allMap), result); + //console.log(hostnames) + //console.log(result.addresses); + console.log('[IP CHECKER] hostnames.length', hostnames.length); + console.log('[IP CHECKER] result.addresses.length', result.addresses.length); + return update(hostnames, result.addresses); + }); +}; + +if (require.main === module) { + module.exports.update().then(function (results) { + console.log('results'); + console.log(results); + }); +} diff --git a/lib/ip-checker.js b/lib/ip-checker.js new file mode 100644 index 0000000..9a05592 --- /dev/null +++ b/lib/ip-checker.js @@ -0,0 +1,126 @@ +"use strict"; + +var PromiseA = require('bluebird').Promise; +var ifaces = require('os').networkInterfaces(); +var dns = PromiseA.promisifyAll(require('dns')); +var https = require('https'); + +function getExternalAddresses() { + var iftypes = {}; + + Object.keys(ifaces).forEach(function (ifname) { + ifaces[ifname].forEach(function (iface) { + if (iface.internal) { + return; + } + /* + if (/^(::|f[cde])/.test(iface.address)) { + console.log('non-public ipv6'); + return; + } + */ + + iftypes[iface.family] = true; + }); + }); + + var now = Date.now(); + + return PromiseA.all([ + dns.lookupAsync('api.ipify.org', { family: 4/*, all: true*/ }).then(function (ans) { + iftypes.IPv4 = { address: ans[0], family: ans[1], time: Date.now() - now }; + }).error(function () { + //console.log('no ipv4', Date.now() - now); + iftypes.IPv4 = false; + }) + , dns.lookupAsync('api.ipify.org', { family: 6/*, all: true*/ }).then(function (ans) { + iftypes.IPv6 = { address: ans[0], family: ans[1], time: Date.now() - now }; + }).error(function () { + //console.log('no ipv6', Date.now() - now); + iftypes.IPv6 = false; + }) + ]).then(function () { + var requests = []; + + if (iftypes.IPv4) { + requests.push(new PromiseA(function (resolve) { + var req = https.request({ + method: 'GET' + , hostname: iftypes.IPv4.address + , port: 443 + , headers: { + Host: 'api.ipify.org' + } + , path: '/' + //, family: 4 + // TODO , localAddress: <> + }, function (res) { + var result = ''; + + res.on('error', function (/*err*/) { + resolve(null); + }); + + res.on('data', function (chunk) { + result += chunk.toString('utf8'); + }); + res.on('end', function () { + resolve({ address: result, family: 4/*, wan: result === iftypes.IPv4.localAddress*/, time: iftypes.IPv4.time }); + }); + }); + + req.on('error', function () { + resolve(null); + }); + req.end(); + })); + } + + if (iftypes.IPv6) { + requests.push(new PromiseA(function (resolve) { + var req = https.request({ + method: 'GET' + , hostname: iftypes.IPv6.address + , port: 443 + , headers: { + Host: 'api.ipify.org' + } + , path: '/' + //, family: 6 + // TODO , localAddress: <> + }, function (res) { + var result = ''; + + res.on('error', function (/*err*/) { + resolve(null); + }); + + res.on('data', function (chunk) { + result += chunk.toString('utf8'); + }); + res.on('end', function () { + resolve({ address: result, family: 6/*, wan: result === iftypes.IPv6.localAaddress*/, time: iftypes.IPv4.time }); + }); + }); + + req.on('error', function () { + resolve(null); + }); + req.end(); + })); + } + + return PromiseA.all(requests).then(function (ips) { + ips = ips.filter(function (ip) { + return ip; + }); + + return { + addresses: ips + , time: Date.now() - now + }; + }); + }); +} + +exports.getExternalAddresses = getExternalAddresses; diff --git a/lib/vhost-sni-server.js b/lib/vhost-sni-server.js index f0510ab..c24705e 100644 --- a/lib/vhost-sni-server.js +++ b/lib/vhost-sni-server.js @@ -8,7 +8,9 @@ module.exports.create = function (securePort, certsPath, vhostsdir) { var path = require('path'); var dummyCerts; var serveFavicon; - var secureContexts = {}; + var secureContexts = {}; + var loopbackApp; + var loopbackToken = require('crypto').randomBytes(32).toString('hex'); function loadDummyCerts() { if (dummyCerts) { @@ -294,9 +296,20 @@ module.exports.create = function (securePort, certsPath, vhostsdir) { }; } + function getLoopbackApp() { + return function (req, res) { + res.setHeader('Content-Type', 'application/json; charset=utf-8'); + res.end(JSON.stringify({ "success": true, "token": loopbackToken })); + }; + } + function getAppContext(domaininfo) { var localApp; + if ('loopback.daplie.invalid' === domaininfo.dirname) { + return getLoopbackApp(); + } + try { // TODO live reload required modules localApp = require(path.join(vhostsdir, domaininfo.dirname, 'app.js')); @@ -589,5 +602,23 @@ module.exports.create = function (securePort, certsPath, vhostsdir) { }); } + function updateIps() { + console.log('[UPDATE IP]'); + require('./ddns-updater').update().then(function (results) { + results.forEach(function (result) { + if (result.error) { + console.error(result); + } else { + console.log('[SUCCESS]', result.service.hostname); + } + }); + }).error(function (err) { + console.error('[UPDATE IP] ERROR'); + console.error(err); + }); + } + // TODO check the IP every 5 minutes and update it every hour + setInterval(updateIps, 60 * 60 * 1000); + updateIps(); return runServer(); }; diff --git a/package.json b/package.json index 19779a5..2f603df 100644 --- a/package.json +++ b/package.json @@ -87,7 +87,7 @@ "negotiator": "^0.5.1", "node-pre-gyp": "^0.6.4", "node-uuid": "1.x", - "nodemailer": "1.x", + "nodemailer": "^1.4.0", "nodemailer-mailgun-transport": "1.x", "oauth": "0.9.x", "oauth2orize": "git://github.com/coolaj86/oauth2orize.git#v1.0.1+scope.1",