diff --git a/beacon.js b/beacon.js new file mode 100644 index 0000000..c5124bc --- /dev/null +++ b/beacon.js @@ -0,0 +1,83 @@ +'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; diff --git a/helpers/pmp-forward.js b/helpers/pmp-forward.js new file mode 100644 index 0000000..c105bf3 --- /dev/null +++ b/helpers/pmp-forward.js @@ -0,0 +1,81 @@ +'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(); +} diff --git a/helpers/upnp-forward.js b/helpers/upnp-forward.js new file mode 100644 index 0000000..f42dfe0 --- /dev/null +++ b/helpers/upnp-forward.js @@ -0,0 +1,81 @@ +'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; +} diff --git a/loopback-https.js b/loopback-https.js new file mode 100644 index 0000000..b5a098f --- /dev/null +++ b/loopback-https.js @@ -0,0 +1,124 @@ +'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'); + }); + }); +}; diff --git a/scrap.js b/scrap.js new file mode 100644 index 0000000..771e10b --- /dev/null +++ b/scrap.js @@ -0,0 +1,49 @@ + 'use strict'; + + //var config = require('./device.json'); + + // require('ssl-root-cas').inject(); + // TODO try SNI loopback.example.com as result of api.ipify.com with loopback token + + function phoneHome() { + var holepunch = require('./holepunch/beacon'); + var ports; + + ports = [ + { private: 65022 + , public: 65022 + , protocol: 'tcp' + , ttl: 0 + , test: { service: 'ssh' } + , testable: false + } + , { private: 650443 + , public: 650443 + , protocol: 'tcp' + , ttl: 0 + , test: { service: 'https' } + } + , { private: 65080 + , public: 65080 + , protocol: 'tcp' + , ttl: 0 + , test: { service: 'http' } + } + ]; + + // TODO return a middleware + holepunch.run(require('./redirects.json').reduce(function (all, redirect) { + if (!all[redirect.from.hostname]) { + all[redirect.from.hostname] = true; + all.push(redirect.from.hostname); + } + if (!all[redirect.to.hostname]) { + all[redirect.to.hostname] = true; + all.push(redirect.to.hostname); + } + + return all; + }, []), ports).catch(function () { + console.error("Couldn't phone home. Oh well"); + }); + }