holepunch.js/lib/index.js

228 lines
5.9 KiB
JavaScript
Raw Permalink Normal View History

2015-12-30 03:36:14 +00:00
'use strict';
var PromiseA = require('bluebird');
2015-12-31 02:21:29 +00:00
var os = require('os');
2015-12-30 03:36:14 +00:00
2015-12-31 02:21:29 +00:00
module.exports = function (args) {
2015-12-30 03:36:14 +00:00
if (args.debug) {
console.log('[HP] create holepuncher');
console.log(args);
}
2015-12-31 02:21:29 +00:00
var interfaces = os.networkInterfaces();
var ifacenames = Object.keys(interfaces).filter(function (ifacename) {
// http://www.freedesktop.org/wiki/Software/systemd/PredictableNetworkInterfaceNames/
// https://wiki.archlinux.org/index.php/Network_configuration#Device_names
// we do not include tun and bridge devices because we're trying
// to see if any physical interface is internet-connected first
return /^(en|sl|wl|ww|eth|net|lan|wifi|inet)/.test(ifacename);
});
2015-12-30 03:36:14 +00:00
2015-12-30 06:35:21 +00:00
function getExternalIps() {
2015-12-31 02:21:29 +00:00
if (!args.ipifyUrls || !args.ipifyUrls.length) {
return PromiseA.resolve(args.ifaces.map(function (iface) {
return {
family: iface.family
//, address: addr
, address: iface.address // TODO check where this is used
, localAddress: iface.address
};
}));
}
2015-12-30 06:35:21 +00:00
return PromiseA.any(args.ipifyUrls.map(function (ipifyUrl) {
2015-12-30 03:36:14 +00:00
var getIp = require('./external-ip');
return getIp({ hostname: ipifyUrl, debug: args.debug });
}));
}
2015-12-30 06:35:21 +00:00
function testOpenPort(ip, portInfo) {
var requestAsync = require('./request');
2015-12-30 08:46:22 +00:00
if (args.debug) {
console.log('[HP] hostname', args.loopbackHostname);
}
2015-12-30 06:35:21 +00:00
return requestAsync({
secure: portInfo.secure
, rejectUnauthorized: false
, hostname: ip.address
// '/.well-known/com.daplie.loopback/'
, path: args.loopbackPrefix + args.key
// 'loopback.daplie.invalid'
, servername: args.loopbackHostname
, localAddress: ip.localAddress
, port: portInfo.external || portInfo.internal
, headers: {
// 'loopback.daplie.invalid'
Host: args.loopbackHostname
}
}).then(function (val) {
2015-12-30 08:22:04 +00:00
if (args.debug) {
console.log('[HP] loopback test reached', val);
}
2015-12-30 06:35:21 +00:00
if (val !== args.value) {
return PromiseA.reject(new Error("invalid loopback token value"));
}
ip.validated = true;
ip.ports.push(portInfo);
portInfo.ips.push(ip);
return portInfo;
2015-12-30 08:22:04 +00:00
}, function (err) {
if (args.debug) {
2015-12-30 21:40:52 +00:00
console.log('[HP] loopback did not complete');
console.log(err.stack);
2015-12-30 08:22:04 +00:00
}
return PromiseA.reject(err);
2015-12-30 06:35:21 +00:00
});
}
function testPort(opts) {
// TODO should ip.address === ip.localAddress be treated differently?
// TODO check local firewall?
// TODO would it ever make sense for a public ip to respond to upnp?
// TODO should we pass any or require all?
opts.portInfo.ips = [];
return PromiseA.any(opts.ips.map(function (ip) {
ip.ports = [];
2015-12-30 08:22:04 +00:00
if (opts.debug) {
2015-12-30 21:40:52 +00:00
console.log('[HP] pretest = ', opts.pretest);
2015-12-30 08:22:04 +00:00
}
2015-12-30 06:35:21 +00:00
if (!opts.pretest) {
2015-12-31 02:21:29 +00:00
return PromiseA.reject(new Error("[not an error]: skip the loopback test"));
2015-12-30 06:35:21 +00:00
}
return testOpenPort(ip, opts.portInfo);
}));
}
2015-12-31 02:21:29 +00:00
args.ifaces = ifacenames.reduce(function (all, ifacename) {
var ifs = interfaces[ifacename];
ifs.forEach(function (iface) {
if (!iface.internal && !/^fe80/.test(iface.address)) {
all.push(iface);
}
});
return all;
}, []);
if (args.debug) {
console.log('[HP] external ifaces:');
console.log(args.ifaces);
}
2015-12-30 06:35:21 +00:00
return getExternalIps().then(function (ips) {
2015-12-31 02:21:29 +00:00
var portInfos = args.mappings;
2015-12-30 06:35:21 +00:00
2015-12-31 02:21:29 +00:00
return PromiseA.all(portInfos.map(function (mapping) {
2015-12-30 06:35:21 +00:00
// TODO clone-merge args
return testPort({
2015-12-31 02:21:29 +00:00
portInfo: mapping
2015-12-30 06:35:21 +00:00
, ips: ips
2015-12-31 02:21:29 +00:00
, pretest: mapping.loopback
2015-12-30 06:35:21 +00:00
});
})).then(function (portInfos) {
if (args.debug) {
console.log('[HP] all done on the first try');
console.log(portInfos);
}
return portInfos;
}, function () {
// at least one port could not be mapped
2015-12-31 02:21:29 +00:00
var upnps = [];
var pmps = [];
var pu = PromiseA.resolve();
var pm = PromiseA.resolve();
2015-12-30 06:35:21 +00:00
2015-12-31 02:21:29 +00:00
if (args.upnp) {
2015-12-30 21:40:52 +00:00
if (args.debug) {
console.log('[HP] will try upnp');
}
2015-12-31 02:21:29 +00:00
upnps.push(require('./upnp'));
2015-12-30 06:35:21 +00:00
}
2015-12-31 02:21:29 +00:00
if (args.pmp) {
2015-12-30 21:40:52 +00:00
if (args.debug) {
console.log('[HP] will try nat-pmp');
}
2015-12-31 02:21:29 +00:00
pmps.push(require('./pmp'));
2015-12-30 06:35:21 +00:00
}
return PromiseA.all(portInfos.map(function (portInfo) {
2015-12-31 02:21:29 +00:00
/*
// TODO create single dgram listeners and serialize upnp requests
// because we can't have multiple requests bound to the same port, duh
2015-12-30 06:35:21 +00:00
return PromiseA.any(mappers.map(function (fn) {
var p = fn(args, ips, portInfo);
2015-12-30 08:22:04 +00:00
if (portInfo.ips.length) {
return portInfo;
2015-12-30 06:35:21 +00:00
}
return p;
}));
2015-12-31 02:21:29 +00:00
*/
var good;
function nextu(fn) {
pu = pu.then(function () {
return fn(args, ips, portInfo);
}).then(function (results) {
good = results;
return null;
}, function (/*err*/) {
return null;
});
}
function nextm(fn) {
pm = pm.then(function () {
return fn(args, ips, portInfo);
}).then(function (results) {
good = results;
return null;
}, function (/*err*/) {
return null;
});
}
upnps.forEach(nextu);
pmps.forEach(nextm);
return PromiseA.any([pu, pm]).then(function () {
if (!good) {
return PromiseA.reject(new Error("no port map success"));
}
return null;
});
2015-12-30 06:35:21 +00:00
})).then(function () {
if (args.debug) {
console.log("[HP] all ports successfully mapped");
console.log(portInfos);
}
return portInfos;
});
}).then(function () {
return portInfos;
2015-12-30 08:22:04 +00:00
}, function (err) {
2015-12-30 06:35:21 +00:00
console.warn('[HP] RVPN not implemented');
2015-12-30 08:22:04 +00:00
console.warn(err.stack);
2015-12-30 06:35:21 +00:00
return portInfos;
});
2015-12-30 03:36:14 +00:00
});
};