move ddns client to own service

This commit is contained in:
AJ ONeal 2016-03-28 22:16:02 -04:00
parent 0869186ced
commit 951c411107
10 changed files with 0 additions and 711 deletions

View File

@ -44,7 +44,6 @@ cluster.on('online', function (worker) {
, path.join(__dirname, 'letsencrypt', 'live')
];
var info;
conf.ddns = config.ddns;
conf.redirects = config.redirects;
console.info('[MASTER] Worker ' + worker.process.pid + ' is online');
@ -63,7 +62,6 @@ cluster.on('online', function (worker) {
, ipcKey: null
// TODO let this load after server is listening
, redirects: config.redirects
, ddns: config.ddns
, 'org.oauth3.consumer': config['org.oauth3.consumer']
, 'org.oauth3.provider': config['org.oauth3.provider']
, keys: config.keys
@ -88,13 +86,10 @@ cluster.on('online', function (worker) {
info.conf.sqlite3Sock = conf.sqlite3Sock;
// TODO get this from db config instead
var config = require('../../config');
info.conf.primaryNameserver = config.ddns.primaryNameserver;
info.conf.nameservers = config.ddns.nameservers;
// TODO get this from db config instead
info.conf.privkey = config.privkey;
info.conf.pubkey = config.pubkey;
info.conf.redirects = config.redirects;
info.conf.ddns = config.ddns;
worker.send(info);
});
}

View File

@ -1,83 +0,0 @@
'use strict';
var PromiseA = require('bluebird').Promise;
var updateIp = require('./helpers/update-ip.js').update;
var request = PromiseA.promisifyAll(require('request'));
var requestAsync = PromiseA.promisify(require('request'));
var upnpForward = require('./helpers/upnp-forward').upnpForward;
var pmpForward = require('./helpers/pmp-forward').pmpForward;
var loopbackHttps = require('./loopback-https');
//var checkip = require('check-ip-address');
function openPort(ip, port) {
if (!/tcp|https|http/.test(port.protocol || 'tcp')) {
throw new Error('not yet supported \'' + port.protocol + '\'');
}
if (false === port.testable) {
return PromiseA.resolve();
}
return loopbackHttps.create(ip, port.private, port.public).then(function () {
console.log('success');
}).catch(function (err) {
// TODO test err
return upnpForward(port).catch(function (err) {
console.error('[ERROR] UPnP Port Forward');
console.error(err);
// TODO test err
return pmpForward(port);
}).then(function () {
return loopbackHttps.create(ip, port.private, port.public);
});
});
}
// 1. update dyndns
// 1.5. check ip every 5 min
// 2. loopback test on ip for http / https / ssh
// 3. if needed: discover gateway, map ports
function beacon(hostnames, ports) {
// test with
// dig -p 53 @redirect-www.org pi.nadal.daplie.com A
return updateIp({
updater: 'redirect-www.org'
, port: 65443
, ddns: hostnames.map(function (hostname) {
return { "name": hostname /*, "value": ipaddress, "type": "A"*/ };
})
}).then(function (data) {
var promises = [];
console.log("Updated DynDNS");
console.log(data);
ports.forEach(function (port) {
promises.push(openPort(JSON.parse(data)[0].answers[0] || hostname, port));
});
return PromiseA.all(promises);
}).then(function () {
console.log('opened ports');
});
/*
request.getAsync('http://checkip.hellabit.com').spread(function (resp, data) {
console.log("External IP is", data);
}).then(function () {
return upnpForward().catch(function (err) {
console.error('ERROR: UPnP failure:');
console.error(err);
});
}).then(function () {
return pmpForward().catch(function (err) {
console.error('TODO: Notify user that their router is not compatible');
});
});
// TODO test roundtrip
*/
}
//setInterval(beacon, 5 * 60 * 1000);
exports.run = beacon;

View File

@ -1,81 +0,0 @@
'use strict';
var PromiseA = require('bluebird').Promise;
var natpmp = require('nat-pmp');
var exec = require('child_process').exec;
exports.pmpForward = function (port) {
return new PromiseA(function (resolve, reject) {
exec('ip route show default', function (err, stdout, stderr) {
var gw;
if (err || stderr) { reject(err || stderr); return; }
// default via 192.168.1.1 dev eth0
gw = stdout.replace(/^default via (\d+\.\d+\.\d+\.\d+) dev[\s\S]+/m, '$1');
console.log('Possible PMP gateway is', gw);
// create a "client" instance connecting to your local gateway
var client = natpmp.connect(gw);
function setPortForward() {
// setup a new port mapping
client.portMapping({
private: port.private || port.public
, public: port.public || port.private
, ttl: port.ttl || 0 // 600
}, function (err, info) {
if (err) {
reject(err);
return;
}
console.log(info);
// {
// type: 'tcp',
// epoch: 8922109,
// private: 22,
// public: 2222,
// ...
// }
resolve();
});
}
// explicitly ask for the current external IP address
setTimeout(function () {
client.externalIp(function (err, info) {
if (err) throw err;
console.log('Current external IP address: %s', info.ip.join('.'));
setPortForward();
});
});
});
});
};
function usage() {
console.warn("");
console.warn("node helpers/pmp-forward [public port] [private port] [ttl]");
console.warn("");
}
function run() {
var pubPort = parseInt(process.argv[2], 10) || 0;
var privPort = parseInt(process.argv[3], 10) || pubPort;
var ttl = parseInt(process.argv[4], 10) || 0;
var options = { public: pubPort, private: privPort, ttl: ttl };
if (!pubPort) {
usage();
return;
}
exports.pmpForward(options).then(function () {
console.log('done');
});
}
if (require.main === module) {
run();
}

View File

@ -1,90 +0,0 @@
#!/usr/bin/env node
'use strict';
var PromiseA = require('bluebird').Promise;
var https = require('https');
var fs = PromiseA.promisifyAll(require('fs'));
module.exports.update = function (opts) {
return new PromiseA(function (resolve, reject) {
var options;
var hostname = opts.hostname || opts.updater;
var port = opts.port;
var pathname = opts.pathname;
var req;
if (!hostname) {
throw new Error('Please specify a DDNS host as opts.hostname');
}
if (!pathname) {
throw new Error('Please specify the api route as opts.pathname');
}
options = {
host: hostname
, port: port
, method: 'POST'
, headers: {
'Content-Type': 'application/json'
}
, path: pathname
//, auth: opts.auth || 'admin:secret'
};
if (opts.cacert) {
if (!Array.isArray(opts.cacert)) {
opts.cacert = [opts.cacert];
}
options.ca = opts.cacert;
}
if (opts.token || opts.jwt) {
options.headers.Authorization = 'Bearer ' + (opts.token || opts.jwt);
}
if (false === opts.cacert) {
options.rejectUnauthorized = false;
}
options.ca = (options.ca||[]).map(function (str) {
if ('string' === typeof str && str.length < 1000) {
str = fs.readFileAsync(str);
}
return str;
});
return PromiseA.all(options.ca).then(function (cas) {
options.ca = cas;
options.agent = new https.Agent(options);
req = https.request(options, function(res) {
var textData = '';
res.on('error', function (err) {
reject(err);
});
res.on('data', function (chunk) {
textData += chunk.toString();
// console.log(chunk.toString());
});
res.on('end', function () {
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);
}
});
});
req.on('error', function (err) {
reject(err);
});
req.end(JSON.stringify(opts.ddns, null, ' '));
}, reject);
});
};

View File

@ -1,81 +0,0 @@
'use strict';
var PromiseA = require('bluebird').Promise;
var natUpnp = require('nat-upnp');
exports.upnpForward = function (port) {
return natUpnp.createClient({ timeout: 1800 }).then(function (client) {
return client.portMapping({
public: port.public,
private: port.private || port.public,
ttl: port.ttl || 0
})/*.then(function () {
var promitter = client.getMappings();
promitter.on('entry', function (entry, i) {
console.log('entry', i);
console.log(entry);
}).then(function (mappings) {
console.log('mappings');
console.log(mappings);
});
return promitter;
})*/;
});
};
/*
client.portUnmapping({
public: 80
});
.findGateway().then(function (stuff) {
console.log('[a] gateway');
console.log(stuff.gateway);
console.log('[a] address');
console.log(stuff.address);
}).then(function () {
return client
*/
/*
client.getMappings({ local: true }, function(err, results) {
console.log('local mappings', results);
});
client.externalIp(function(err, ip) {
console.log('ext-ip', ip);
});
*/
function usage() {
console.warn("");
console.warn("node helpers/upnp-forward [public port] [private port] [ttl]");
console.warn("");
}
function run() {
var pubPort = parseInt(process.argv[2], 10) || 0;
var privPort = parseInt(process.argv[3], 10) || pubPort;
var ttl = parseInt(process.argv[4], 10) || 0;
var options = { public: pubPort, private: privPort, ttl: ttl };
if (!pubPort) {
usage();
return;
}
exports.upnpForward(options).then(function () {
console.log('done');
}).catch(function (err) {
console.error('ERROR');
console.error(err);
throw err;
});
}
if (require.main === module) {
run();
return;
}

View File

@ -1,124 +0,0 @@
'use strict';
var https = require('https');
var path = require('path');
var fs = require('fs');
var PromiseA = global.Promise || require('bluebird').Promise;
exports.create = function (ip, localPort, externalPort) {
return new PromiseA(function (resolve, reject) {
var token = Math.random().toString(16).split('.')[1];
var tokenPath = Math.random().toString(16).split('.')[1];
var options;
var server;
var options;
var certsPath = path.join(__dirname, 'certs', 'server');
var caCertsPath = path.join(__dirname, 'certs', 'ca');
function testConnection() {
var awesome = false;
var timetok;
var webreq;
var options = {
// not hostname because we set headers.host on our own
host: ip
, headers: {
// whatever's on the fake cert
'Host': 'redirect-www.org'
}
, port: externalPort
, path: '/' + tokenPath
, ca: fs.readFileSync(path.join(caCertsPath, 'my-root-ca.crt.pem'))
};
options.agent = new https.Agent(options);
timetok = setTimeout(function () {
reject(new Error("timed out while testing NAT loopback for port " + externalPort));
}, 2000);
function finishHim(err) {
clearTimeout(timetok);
server.close(function () {
if (!err && awesome) {
resolve();
}
});
if (err || !awesome) {
if (err) {
reject(err);
}
else if (!awesome) {
reject(new Error("loopback failed. Why? here's my best guess: "
+ "the ssl cert matched, so you've probably got two boxes and this isn't the right one"));
}
return;
}
}
webreq = https.request(options, function(res) {
res.on('data', function (resToken) {
if (resToken.toString() === token) {
awesome = true;
return;
}
});
res.on('error', function (err) {
console.error('[ERROR] https.request.response');
console.error(err);
finishHim(new Error("loopback failed. Why? here's my best guess: "
+ "the connection was interrupted"));
});
res.on('end', function () {
finishHim();
});
});
webreq.on('error', function (err) {
console.error('[ERROR] https.request');
console.error(err);
if (/ssl|cert|chain/i.test(err.message || err.toString())) {
finishHim(new Error("loopback failed. Why? here's my best guess: "
+ "the ssl cert validation may have failed (might port-forward to the wrong box)"));
} else {
finishHim(new Error("loopback failed. Why? here's my best guess: "
+ "port forwarding isn't configured for " + ip + ":" + externalPort + " to " + localPort));
}
});
webreq.end();
}
//
// SSL Certificates
//
options = {
key: fs.readFileSync(path.join(certsPath, 'my-server.key.pem'))
, ca: [ fs.readFileSync(path.join(caCertsPath, 'my-root-ca.crt.pem')) ]
, cert: fs.readFileSync(path.join(certsPath, 'my-server.crt.pem'))
, requestCert: false
, rejectUnauthorized: false
};
//
// Serve an Express App securely with HTTPS
//
server = https.createServer(options);
function listen(app) {
server.on('request', app);
server.listen(localPort, function () {
localPort = server.address().port;
setTimeout(testConnection, 2000);
});
}
listen(function (req, res) {
if (('/' + tokenPath) === req.url) {
res.end(token);
return;
}
res.end('loopback failure');
});
});
};

View File

@ -1,82 +0,0 @@
'use strict';
var updateIp = require('../holepunch/helpers/update-ip.js').update;
/*
* @param {string[]} hostnames - A list of hostnames
* @param {Object[]} addresses - A list of { address: <ip-address>, family: <4|6> }
*/
function update(services, hostnames, addresses, ddnsToken) {
// TODO use not-yet-built API to get and store tokens
// TODO use API to add and remove nameservers
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 = require('../dyndns-token.js').token;
, "token": ddnsToken
};
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({
hostname: service.hostname
, port: service.port
, pathname: service.pathname
, cacert: service.cacert
, token: ddnsToken
, 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 (services, hostnames, ddnsToken) {
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(services, hostnames, result.addresses, ddnsToken);
});
};
if (require.main === module) {
module.exports.update().then(function (results) {
console.log('results');
console.log(results);
});
}

View File

@ -1,126 +0,0 @@
"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: <<external_ipv4>>
}, 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: <<external_ipv6>>
}, 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;

View File

@ -4,42 +4,10 @@ var cluster = require('cluster');
var PromiseA = require('bluebird');
var memstore;
var sqlstore;
var config;
// TODO
// var rootMasterKey;
function updateIps() {
console.log('[UPDATE IP]');
var allMap = {};
var hostnames = config.redirects.reduce(function (all, redirect) {
if (redirect.ip && !allMap[redirect.id]) {
allMap[redirect.id] = true;
all.push(redirect.id);
}
return all;
}, []);
require('./ddns-updater').update(
config.ddns.services
, hostnames
, config.ddns.token
).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);
});
}
function init(conf/*, state*/) {
config = conf;
if (!conf.ipcKey) {
conf.ipcKey = require('crypto').randomBytes(16).toString('base64');
}
@ -101,11 +69,6 @@ function init(conf/*, state*/) {
*/
});
// TODO check the IP every 5 minutes and update it every hour
setInterval(updateIps, 60 * 60 * 1000);
// we don't want this to load right away (extra procesing time)
setTimeout(updateIps, 1);
return promise;
}

View File

@ -172,8 +172,6 @@ module.exports.create = function (webserver, info, state) {
, vhostPatterns: null
, server: webserver
, externalPort: info.conf.externalPort
, primaryNameserver: info.conf.primaryNameserver
, nameservers: info.conf.nameservers
, privkey: info.conf.privkey
, pubkey: info.conf.pubkey
, redirects: info.conf.redirects