Compare commits
20 Commits
v1.0.0-alp
...
master
Author | SHA1 | Date | |
---|---|---|---|
|
f6b7c89926 | ||
|
69ea7fa7c1 | ||
|
aaebc5494e | ||
|
bef1fbaba4 | ||
|
d11a51ff90 | ||
|
9de7d807d2 | ||
|
a220461b44 | ||
|
6cc9ec30f7 | ||
|
967a4a99fc | ||
|
6382701c91 | ||
|
62509a4800 | ||
e8e23388d0 | |||
8f6a5f58ac | |||
4f12286b55 | |||
8e5accc6ce | |||
4bc1adf7a2 | |||
5b1191f1be | |||
9bb093fd4f | |||
6ecb868ae1 | |||
0a3b457583 |
138
README.md
138
README.md
@ -1,12 +1,140 @@
|
||||
# holepunch
|
||||
|
||||
A node.js library and cli for using UPnP SSDP
|
||||
and ZeroConf (Bonjour) NAT-PMP
|
||||
to make home and office devices and services Internet-accessible.
|
||||
A commandline tool (cli) and library (node.js api) for making devices
|
||||
in your home and office Internet-accessible.
|
||||
|
||||
## Progress
|
||||
Uses UPnP / SSDP and NAT-PMP / ZeroConf (Bonjour) for port forwarding / port mapping.
|
||||
|
||||
in development
|
||||
Works for IPv4 and IPv6 interfaces.
|
||||
|
||||
## Status
|
||||
|
||||
Published as alpha, but nearing release quality.
|
||||
|
||||
```bash
|
||||
git clone git@github.com:Daplie/holepunch.git
|
||||
|
||||
pushd holepunch
|
||||
|
||||
node bin/holepunch.js --debug
|
||||
```
|
||||
|
||||
## Install
|
||||
|
||||
**Commandline Tool**
|
||||
```bash
|
||||
npm install --global holepunch
|
||||
```
|
||||
|
||||
**node.js Library**
|
||||
```
|
||||
npm install --save holepunch
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
Some examples that work with what's currently published.
|
||||
|
||||
### Commandline (CLI)
|
||||
|
||||
```bash
|
||||
holepunch --help
|
||||
```
|
||||
|
||||
```
|
||||
holepunch --plain-ports 80,65080 --tls-ports 443,65443
|
||||
```
|
||||
|
||||
### API
|
||||
|
||||
This is the current Dec 30th api in master
|
||||
|
||||
```javascript
|
||||
var punch = require('holepunch');
|
||||
|
||||
punch({
|
||||
debug: true
|
||||
, mappings: [{ internal: 443, external: 443, secure: true }]
|
||||
, ipifyUrls: ['api.ipify.org'],
|
||||
, protocols: ['none', 'upnp', 'pmp']
|
||||
, rvpnConfigs: []
|
||||
}).then(function (mappings) {
|
||||
// be sure to check for an `error` attribute on each mapping
|
||||
console.log(mappings);
|
||||
}, function (err) {
|
||||
console.log(err);
|
||||
});
|
||||
```
|
||||
|
||||
## API (v1.0.0 draft)
|
||||
|
||||
TODO: This is the api that I think I'd like to use for the solid v1.0.0
|
||||
|
||||
```javascript
|
||||
punch(opts)
|
||||
|
||||
opts.debug = true | false // print extra debug info
|
||||
|
||||
opts.mappings = [ // these ports will be tested via tcp / http
|
||||
{ internal: 80 // the port which is bound locally
|
||||
, external: 80 // the port as it is exposed on the internet
|
||||
, loopback: true | false // whether or not to attempt an http(s) loopback test
|
||||
, secure: true | false // (default: true) whether to use tls or plaintext
|
||||
}
|
||||
]
|
||||
|
||||
opts.ipifyUrls = [ // ipify urls
|
||||
'api.ipify.org' // default
|
||||
]
|
||||
|
||||
opts.upnp = true | false // attempt mapping via nat-upnp
|
||||
|
||||
opts.pmp = true | false // attempt mapping via nat-pmp
|
||||
|
||||
opts.rvpnConfigs = [
|
||||
'/etc/holepunch/rvpn.json' // TODO (not implemented)
|
||||
]
|
||||
```
|
||||
|
||||
## Commandline
|
||||
|
||||
TODO `--prebound-ports 22`
|
||||
|
||||
```
|
||||
Usage:
|
||||
holepunch.js [OPTIONS] [ARGS]
|
||||
|
||||
Options:
|
||||
--debug BOOLEAN show traces and logs
|
||||
|
||||
--plain-ports STRING Port numbers to test with plaintext loopback.
|
||||
(default: 65080)
|
||||
(formats: <port>,<internal:external>)
|
||||
|
||||
--tls-ports STRING Port numbers to test with tls loopback.
|
||||
(default: null)
|
||||
|
||||
--ipify-urls STRING Comma separated list of URLs to test for external ip.
|
||||
(default: api.ipify.org)
|
||||
|
||||
--protocols STRING Comma separated list of ip mapping protocols.
|
||||
(default: none,upnp,pmp)
|
||||
|
||||
--rvpn-configs STRING Comma separated list of Reverse VPN config files in
|
||||
the order they should be tried. (default: null)
|
||||
|
||||
-h, --help Display help and usage details
|
||||
```
|
||||
|
||||
## Non-Root
|
||||
|
||||
You **do not need root** to map ports, but you may need root to test them.
|
||||
|
||||
If you're cool with allowing all node programs to bind to privileged ports, try this:
|
||||
|
||||
```bash
|
||||
sudo setcap 'cap_net_bind_service=+ep' /usr/local/bin/node
|
||||
```
|
||||
|
||||
# License
|
||||
|
||||
|
83
beacon.js
83
beacon.js
@ -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;
|
@ -8,10 +8,13 @@ var cli = require('cli');
|
||||
// TODO txt records for browser plugin: TXT _http.example.com _https.example.com
|
||||
cli.parse({
|
||||
debug: [ false, " show traces and logs", 'boolean', false ]
|
||||
, 'plain-ports': [ false, " Port numbers to test with plaintext loopback. (default: 65080) (formats: <port>,<internal:external>,<internal:external1|external2>)", 'string' ]
|
||||
, 'plain-ports': [ false, " Port numbers to test with plaintext loopback. (default: 65080) (formats: <port>,<internal:external>)", 'string' ]
|
||||
//, 'plain-ports': [ false, " Port numbers to test with plaintext loopback. (default: 65080) (formats: <port>,<internal:external>,<internal:external1|external2>)", 'string' ]
|
||||
, 'tls-ports': [ false, " Port numbers to test with tls loopback. (default: null)", 'string' ]
|
||||
, 'ipify-urls': [ false, " Comma separated list of URLs to test for external ip. (default: api.ipify.org)", 'string' ]
|
||||
, 'protocols': [ false, " Comma separated list of ip mapping protocols. (default: none,upnp,pmp)", 'string' ]
|
||||
, protocols: [ false, " Comma separated list of ip mapping protocols. (default: none,upnp,pmp)", 'string' ]
|
||||
//, upnp: [ false, " Use nat-upnp. (default: true)", 'boolean' ]
|
||||
//, pmp: [ false, " Use nat-pmp. (default: true)", 'boolean' ]
|
||||
, 'rvpn-configs': [ false, " Comma separated list of Reverse VPN config files in the order they should be tried. (default: null)", 'string' ]
|
||||
// TODO allow standalone, webroot, etc
|
||||
});
|
||||
@ -21,6 +24,11 @@ cli.main(function(_, options) {
|
||||
console.log('');
|
||||
var args = {};
|
||||
var hp = require('../');
|
||||
var loopback = require('../lib/loopback-listener');
|
||||
var plainPorts = options['plain-ports'];
|
||||
var tlsPorts = options['tls-ports'];
|
||||
var pretest;
|
||||
var protocols;
|
||||
|
||||
function parsePorts(portstr) {
|
||||
var parts = portstr.split(':');
|
||||
@ -28,7 +36,7 @@ cli.main(function(_, options) {
|
||||
internal: parseInt(parts[0], 10)
|
||||
, external: (parts[1]||parts[0]).split('|').map(function (port) {
|
||||
return parseInt(port, 10);
|
||||
})
|
||||
})[0]
|
||||
};
|
||||
|
||||
return opts;
|
||||
@ -39,14 +47,12 @@ cli.main(function(_, options) {
|
||||
}
|
||||
|
||||
if (options.debug) {
|
||||
console.log('options');
|
||||
console.log('[HP CLI] options');
|
||||
console.log(options);
|
||||
}
|
||||
|
||||
args.debug = options.debug;
|
||||
args.plainPorts = options['plain-ports'];
|
||||
args.tlsPorts = options['tls-ports'];
|
||||
args.protocols = options.protocols;
|
||||
protocols = options.protocols;
|
||||
args.ipifyUrls = options['ipify-urls'];
|
||||
args.rvpnConfigs = options['rvpn-configs'];
|
||||
|
||||
@ -55,21 +61,46 @@ cli.main(function(_, options) {
|
||||
} else {
|
||||
args.ipifyUrls = (args.ipifyUrls || 'api.ipify.org').split(',');
|
||||
}
|
||||
if ('false' === args.protocols || false === args.protocols) {
|
||||
args.protocols = [];
|
||||
if ('false' === protocols || false === protocols) {
|
||||
protocols = [];
|
||||
} else {
|
||||
args.protocols = (args.protocols || 'none,upnp,pmp').split(',');
|
||||
protocols = (protocols || 'none,upnp,pmp').split(',');
|
||||
}
|
||||
// Coerce to string. cli returns a number although we request a string.
|
||||
args.tlsPorts = (args.tlsPorts || "").toString().split(',').filter(exists).map(parsePorts);
|
||||
tlsPorts = (tlsPorts || "").toString().split(',').filter(exists).map(parsePorts);
|
||||
args.rvpnConfigs = (args.rvpnConfigs || "").toString().split(',').filter(exists);
|
||||
if ('false' === args.plainPorts || false === args.plainPorts) {
|
||||
args.plainPorts = [];
|
||||
if ('false' === plainPorts || false === plainPorts) {
|
||||
plainPorts = [];
|
||||
} else {
|
||||
args.plainPorts = (args.plainPorts || "65080").toString().split(',').map(parsePorts);
|
||||
plainPorts = (plainPorts || "65080").toString().split(',').map(parsePorts);
|
||||
}
|
||||
pretest = (-1 !== protocols.indexOf('none'));
|
||||
args.upnp = options.upnp
|
||||
|| (-1 !== protocols.indexOf('upnp')) || (-1 !== protocols.indexOf('ssdp'));
|
||||
args.pmp = options.pmp
|
||||
|| (-1 !== protocols.indexOf('pmp')) || (-1 !== protocols.indexOf('nat-pmp'));
|
||||
|
||||
args.mappings = plainPorts.map(function (info) {
|
||||
info.secure = false;
|
||||
info.loopback = pretest;
|
||||
return info;
|
||||
}).concat(tlsPorts.map(function (info) {
|
||||
info.secure = true;
|
||||
info.loopback = pretest;
|
||||
return info;
|
||||
}));
|
||||
|
||||
//var servers = loopback.create(args);
|
||||
loopback.create(args);
|
||||
|
||||
if (args.debug) {
|
||||
console.log('[HP] create servers');
|
||||
//console.log(servers);
|
||||
}
|
||||
|
||||
return hp.create(args).then(function () {
|
||||
return hp(args).then(function () {
|
||||
//console.log('[HP] wishing wanting waiting');
|
||||
console.log('complete, exiting');
|
||||
process.exit(0);
|
||||
});
|
||||
});
|
||||
|
57
bin/service.js
Normal file
57
bin/service.js
Normal file
@ -0,0 +1,57 @@
|
||||
'use strict';
|
||||
|
||||
var punch = require('../');
|
||||
var exec = require('child_process').exec;
|
||||
|
||||
function touch() {
|
||||
exec("/usr/local/bin/ddns --hostname sarah.daplie.com --email 'coolaj86@gmail.com' --agree --token /srv/holepunch/bin/token.jwt", function (err, stdout, stderr) {
|
||||
if (err || stderr) {
|
||||
console.error('ddns error:', err || stderr);
|
||||
}
|
||||
//console.log(stdout);
|
||||
});
|
||||
|
||||
punch({
|
||||
mappings: [
|
||||
{ internal: 80
|
||||
, external: 80
|
||||
, secure: false
|
||||
, loopback: false
|
||||
}
|
||||
, { internal: 65080
|
||||
, external: 65080
|
||||
, secure: false
|
||||
, loopback: false
|
||||
}
|
||||
, { internal: 65443
|
||||
, external: 65443
|
||||
, secure: false
|
||||
, loopback: false
|
||||
}
|
||||
, { internal: 443
|
||||
, external: 443
|
||||
, secure: false
|
||||
, loopback: false
|
||||
}
|
||||
, { internal: 65022
|
||||
, external: 65022
|
||||
, secure: false
|
||||
, loopback: false
|
||||
}
|
||||
, { internal: 22
|
||||
, external: 22
|
||||
, secure: false
|
||||
, loopback: false
|
||||
}
|
||||
]
|
||||
, upnp: true
|
||||
, pmp: true
|
||||
, debug: true
|
||||
}).then(function (results) {
|
||||
console.log('map results');
|
||||
console.log(results);
|
||||
});
|
||||
}
|
||||
|
||||
setInterval(touch, 90 * 60 * 1000);
|
||||
touch();
|
1
etc/holepunch.rc-local
Normal file
1
etc/holepunch.rc-local
Normal file
@ -0,0 +1 @@
|
||||
nohup /usr/local/bin/node /srv/holepunch/bin/service.js &
|
17
etc/holepunch.service
Normal file
17
etc/holepunch.service
Normal file
@ -0,0 +1,17 @@
|
||||
# https://www.digitalocean.com/community/tutorials/how-to-deploy-node-js-applications-using-systemd-and-nginx
|
||||
# goes in /etc/systemd/system/multi-user.target.wants/holepunch.service
|
||||
# systemctl enable holepunch
|
||||
# systemctl start holepunch
|
||||
|
||||
[Service]
|
||||
ExecStart=/usr/local/bin/node /srv/holepunch/bin/service.js
|
||||
Restart=always
|
||||
StandardOutput=syslog
|
||||
StandardError=syslog
|
||||
SyslogIdentifier=node-holepunch
|
||||
User=root
|
||||
Group=root
|
||||
Environment=NODE_ENV=production
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
19
etc/holepunch.upstart
Normal file
19
etc/holepunch.upstart
Normal file
@ -0,0 +1,19 @@
|
||||
# holepunch - keep connected
|
||||
#
|
||||
# The Holepunch Service provides nat traversal via upnp / pmp and ddns
|
||||
|
||||
description "Holepunch Service"
|
||||
|
||||
start on runlevel [2345]
|
||||
stop on runlevel [!2345]
|
||||
|
||||
respawn
|
||||
respawn limit 10 5
|
||||
umask 022
|
||||
|
||||
# 'sshd -D' leaks stderr and confuses things in conjunction with 'console log'
|
||||
console none
|
||||
|
||||
# if you used to set SSHD_OPTS in /etc/default/ssh, you can change the
|
||||
# 'exec' line here instead
|
||||
exec /usr/local/bin/node /srv/holepunch/bin/service.js
|
@ -41,7 +41,7 @@ function createSocket() {
|
||||
});
|
||||
|
||||
socket.on('message', function (chunk, info) {
|
||||
var message = chunk.toString();
|
||||
var message = chunk.toString('utf8');
|
||||
|
||||
console.log('[MESSAGE RECEIVED]');
|
||||
console.log(info);
|
||||
|
@ -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();
|
||||
}
|
35
index.js
35
index.js
@ -1,36 +1,3 @@
|
||||
'use strict';
|
||||
|
||||
var PromiseA = require('bluebird');
|
||||
var requestAsync = PromiseA.promisify(require('request'));
|
||||
|
||||
module.exports.create = function (args) {
|
||||
var promises = [];
|
||||
|
||||
if (args.debug) {
|
||||
console.log('[HP] create holepuncher');
|
||||
console.log(args);
|
||||
}
|
||||
|
||||
// TODO determine if we have a AAAA local ip or not
|
||||
// TODO get A and AAAA records
|
||||
if (-1 !== args.protocols.indexOf('none')) {
|
||||
promises.push(PromiseA.some(args.ipifyUrls.map(function (ipifyUrl) {
|
||||
return requestAsync('https://' + ipifyUrl).then(function (resp) {
|
||||
var ip = (resp.body || '').toString('ascii');
|
||||
|
||||
if (!/\d+\.\d+\.\d+\.\d+/.test(ip) && !/\w+\:\w+/.test(ip)) {
|
||||
return PromiseA.reject(new Error("bad response '" + resp.body + "'"));
|
||||
}
|
||||
|
||||
return ip;
|
||||
});
|
||||
}), 1));
|
||||
}
|
||||
|
||||
return PromiseA.all(promises).then(function (results) {
|
||||
if (args.debug) {
|
||||
console.log('[HP] all done');
|
||||
console.log(results);
|
||||
}
|
||||
});
|
||||
};
|
||||
module.exports = require('./lib/index.js');
|
||||
|
89
lib/external-ip.js
Normal file
89
lib/external-ip.js
Normal file
@ -0,0 +1,89 @@
|
||||
'use strict';
|
||||
|
||||
var PromiseA = require('bluebird');
|
||||
//var dns = PromiseA.promisifyAll(require('dns'));
|
||||
var requestAsync = require('./request');
|
||||
|
||||
module.exports = function (opts) {
|
||||
var promises = [];
|
||||
|
||||
/*
|
||||
// TODO how to support servername
|
||||
promises.push(dns.resolve4Async(hostname).then(function (ips) {
|
||||
return ips.map(function (ip) {
|
||||
return {
|
||||
address: ip
|
||||
, family: 'IPv4'
|
||||
};
|
||||
});
|
||||
}));
|
||||
|
||||
promises.push(dns.resolve6Async(hostname).then(function (ips) {
|
||||
return ips.map(function (ip) {
|
||||
return {
|
||||
address: ip
|
||||
, family: 'IPv6'
|
||||
};
|
||||
});
|
||||
}));
|
||||
*/
|
||||
|
||||
function parseIp(ip) {
|
||||
if (!/\d+\.\d+\.\d+\.\d+/.test(ip) && !/\w+\:\w+/.test(ip)) {
|
||||
return PromiseA.reject(new Error("bad response '" + ip + "'"));
|
||||
}
|
||||
|
||||
return ip;
|
||||
}
|
||||
|
||||
function ignoreEinval(err) {
|
||||
if ('EINVAL' === err.code) {
|
||||
if (opts.debug) {
|
||||
console.warn('[HP] tried to bind to invalid address:');
|
||||
console.warn(err.stack);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
return PromiseA.reject(err);
|
||||
}
|
||||
|
||||
if (opts.debug) {
|
||||
console.log('[HP] external ip opts:');
|
||||
console.log(opts);
|
||||
}
|
||||
|
||||
opts.ifaces.forEach(function (iface) {
|
||||
promises.push(requestAsync({
|
||||
family: iface.family
|
||||
, method: 'GET'
|
||||
, headers: {
|
||||
Host: opts.hostname
|
||||
}
|
||||
, localAddress: iface.address
|
||||
, servername: opts.hostname // is this actually sent to tls.connect()?
|
||||
, hostname: opts.hostname // if so we could do the DNS ourselves
|
||||
// and use the ip address here
|
||||
, port: opts.port || 443
|
||||
, pathname: opts.pathname || opts.path || '/'
|
||||
}).then(parseIp, ignoreEinval).then(function (addr) {
|
||||
return {
|
||||
family: iface.family
|
||||
, address: addr
|
||||
, localAddress: iface.address
|
||||
};
|
||||
}));
|
||||
});
|
||||
|
||||
return PromiseA.all(promises).then(function (results) {
|
||||
if (opts.debug) {
|
||||
console.log('[HP] got all ip address types');
|
||||
console.log(results);
|
||||
}
|
||||
|
||||
return results;
|
||||
}, function (err) {
|
||||
console.error('[HP] error');
|
||||
console.error(err);
|
||||
});
|
||||
};
|
227
lib/index.js
Normal file
227
lib/index.js
Normal file
@ -0,0 +1,227 @@
|
||||
'use strict';
|
||||
|
||||
var PromiseA = require('bluebird');
|
||||
var os = require('os');
|
||||
|
||||
module.exports = function (args) {
|
||||
if (args.debug) {
|
||||
console.log('[HP] create holepuncher');
|
||||
console.log(args);
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
function getExternalIps() {
|
||||
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
|
||||
};
|
||||
}));
|
||||
}
|
||||
|
||||
return PromiseA.any(args.ipifyUrls.map(function (ipifyUrl) {
|
||||
var getIp = require('./external-ip');
|
||||
|
||||
return getIp({ hostname: ipifyUrl, debug: args.debug });
|
||||
}));
|
||||
}
|
||||
|
||||
function testOpenPort(ip, portInfo) {
|
||||
var requestAsync = require('./request');
|
||||
|
||||
if (args.debug) {
|
||||
console.log('[HP] hostname', args.loopbackHostname);
|
||||
}
|
||||
|
||||
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) {
|
||||
if (args.debug) {
|
||||
console.log('[HP] loopback test reached', val);
|
||||
}
|
||||
|
||||
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;
|
||||
}, function (err) {
|
||||
if (args.debug) {
|
||||
console.log('[HP] loopback did not complete');
|
||||
console.log(err.stack);
|
||||
}
|
||||
|
||||
return PromiseA.reject(err);
|
||||
});
|
||||
}
|
||||
|
||||
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 = [];
|
||||
|
||||
if (opts.debug) {
|
||||
console.log('[HP] pretest = ', opts.pretest);
|
||||
}
|
||||
|
||||
if (!opts.pretest) {
|
||||
return PromiseA.reject(new Error("[not an error]: skip the loopback test"));
|
||||
}
|
||||
|
||||
return testOpenPort(ip, opts.portInfo);
|
||||
}));
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
return getExternalIps().then(function (ips) {
|
||||
var portInfos = args.mappings;
|
||||
|
||||
return PromiseA.all(portInfos.map(function (mapping) {
|
||||
// TODO clone-merge args
|
||||
return testPort({
|
||||
portInfo: mapping
|
||||
, ips: ips
|
||||
, pretest: mapping.loopback
|
||||
});
|
||||
})).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
|
||||
var upnps = [];
|
||||
var pmps = [];
|
||||
var pu = PromiseA.resolve();
|
||||
var pm = PromiseA.resolve();
|
||||
|
||||
if (args.upnp) {
|
||||
if (args.debug) {
|
||||
console.log('[HP] will try upnp');
|
||||
}
|
||||
upnps.push(require('./upnp'));
|
||||
}
|
||||
|
||||
if (args.pmp) {
|
||||
if (args.debug) {
|
||||
console.log('[HP] will try nat-pmp');
|
||||
}
|
||||
pmps.push(require('./pmp'));
|
||||
}
|
||||
|
||||
return PromiseA.all(portInfos.map(function (portInfo) {
|
||||
/*
|
||||
// TODO create single dgram listeners and serialize upnp requests
|
||||
// because we can't have multiple requests bound to the same port, duh
|
||||
return PromiseA.any(mappers.map(function (fn) {
|
||||
var p = fn(args, ips, portInfo);
|
||||
|
||||
if (portInfo.ips.length) {
|
||||
return portInfo;
|
||||
}
|
||||
|
||||
return p;
|
||||
}));
|
||||
*/
|
||||
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;
|
||||
});
|
||||
})).then(function () {
|
||||
if (args.debug) {
|
||||
console.log("[HP] all ports successfully mapped");
|
||||
console.log(portInfos);
|
||||
}
|
||||
|
||||
return portInfos;
|
||||
});
|
||||
}).then(function () {
|
||||
return portInfos;
|
||||
}, function (err) {
|
||||
console.warn('[HP] RVPN not implemented');
|
||||
console.warn(err.stack);
|
||||
return portInfos;
|
||||
});
|
||||
});
|
||||
};
|
@ -16,18 +16,20 @@ module.exports.create = function (opts) {
|
||||
|
||||
app.use('/', middleware(opts));
|
||||
|
||||
(opts.plainPorts||[]).forEach(function (plainPort) {
|
||||
var plainServer = http.createServer();
|
||||
plainServer.__plainPort = plainPort;
|
||||
http.on('request', app);
|
||||
results.plainServers.push(plainServer);
|
||||
});
|
||||
opts.mappings.forEach(function (mapping) {
|
||||
var server;
|
||||
|
||||
(opts.tlsPorts||[]).forEach(function (tlsPort) {
|
||||
var tlsServer = https.createServer(httpsOptions);
|
||||
tlsServer.__tlsPort = tlsPort;
|
||||
http.on('request', app);
|
||||
results.tlsServers.push(tlsServer);
|
||||
if (false === mapping.secure) {
|
||||
server = http.createServer();
|
||||
server.__plainPort = mapping;
|
||||
server.on('request', app);
|
||||
results.plainServers.push(server);
|
||||
} else {
|
||||
server = https.createServer(httpsOptions);
|
||||
server.__tlsPort = mapping;
|
||||
server.on('request', app);
|
||||
results.tlsServers.push(server);
|
||||
}
|
||||
});
|
||||
|
||||
function onListen() {
|
||||
@ -41,20 +43,47 @@ module.exports.create = function (opts) {
|
||||
|
||||
process.nextTick(function () {
|
||||
results.plainServers.forEach(function (plainServer) {
|
||||
plainServer.listen(
|
||||
plainServer.__plainPort.port
|
||||
, plainServer.__plainPort.address || '0.0.0.0'
|
||||
, onListen
|
||||
);
|
||||
plainServer.on('error', function (err) {
|
||||
plainServer.error = err;
|
||||
console.warn("[HP loop] Error with plain HTTP server:");
|
||||
console.warn(err.stack);
|
||||
});
|
||||
try {
|
||||
plainServer.listen(
|
||||
plainServer.__plainPort.internal || plainServer.__plainPort.port
|
||||
, plainServer.__plainPort.address || '0.0.0.0'
|
||||
, onListen
|
||||
);
|
||||
} catch(e) {
|
||||
plainServer.error = e;
|
||||
console.warn("[HP loop] Could not create plain HTTP listener:");
|
||||
console.warn(e.stack);
|
||||
}
|
||||
});
|
||||
results.tlsServers.forEach(function (tlsServer) {
|
||||
tlsServer.listen(
|
||||
tlsServer.__tlsPort.port
|
||||
, tlsServer.__tlsPort.address || '0.0.0.0'
|
||||
, onListen
|
||||
);
|
||||
tlsServer.on('error', function (err) {
|
||||
tlsServer.error = err;
|
||||
console.warn("[HP loop] Error with HTTPS server:");
|
||||
console.warn(err.stack);
|
||||
});
|
||||
try {
|
||||
tlsServer.listen(
|
||||
tlsServer.__tlsPort.internal || tlsServer.__tlsPort.port
|
||||
, tlsServer.__tlsPort.address || '0.0.0.0'
|
||||
, onListen
|
||||
);
|
||||
} catch(e) {
|
||||
tlsServer.error = e;
|
||||
console.warn("[HP loop] Could not create HTTPS listener:");
|
||||
console.warn(e.stack);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
results.key = opts.key;
|
||||
results.value = opts.value;
|
||||
results.loopbackHostname = opts.loopbackHostname;
|
||||
results.loopbackPrefix = opts.loopbackPrefix;
|
||||
|
||||
return results;
|
||||
};
|
||||
|
@ -6,14 +6,20 @@ function middleware(opts) {
|
||||
var key = opts.key;
|
||||
var val = opts.value;
|
||||
var vhost = opts.vhost;
|
||||
var pathnamePrefix = opts.prefix || '/.well-known/com.daplie.loopback/';
|
||||
var defaultHostname = 'loopback.daplie.invalid';
|
||||
var pathnamePrefix = opts.loopbackPrefix;
|
||||
var defaultHostname = opts.loopbackHostname;
|
||||
|
||||
if (!defaultHostname) {
|
||||
defaultHostname = opts.loopbackHostname = 'loopback.daplie.invalid';
|
||||
}
|
||||
if (!pathnamePrefix) {
|
||||
pathnamePrefix = opts.loopbackPrefix = '/.well-known/com.daplie.loopback/';
|
||||
}
|
||||
if (!key) {
|
||||
opts.key = require('crypto').randomBytes(8).toString('hex');
|
||||
key = opts.key = require('crypto').randomBytes(8).toString('hex');
|
||||
}
|
||||
if (!val) {
|
||||
opts.value = require('crypto').randomBytes(16).toString('hex');
|
||||
val = opts.value = require('crypto').randomBytes(16).toString('hex');
|
||||
}
|
||||
if (!vhost && vhost !== false) {
|
||||
vhost = defaultHostname;
|
||||
@ -37,16 +43,20 @@ function middleware(opts) {
|
||||
if (0 !== urlpath.indexOf(pathnamePrefix)) {
|
||||
if (opts.debug) {
|
||||
console.log("[HP] Pathname '" + urlpath + "'"
|
||||
+ " failed to match '" + pathnamePrefix + "'");
|
||||
+ " failed to match prefix '" + pathnamePrefix + ": "
|
||||
+ urlpath.indexOf(pathnamePrefix)
|
||||
);
|
||||
}
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
if (scmp(key, urlpath.substr(pathnamePrefix.length, key.length))) {
|
||||
if (!scmp(key, urlpath.substr(pathnamePrefix.length))) {
|
||||
if (opts.debug) {
|
||||
console.log("[HP] Pathname '" + urlpath + "'"
|
||||
+ " failed to match '" + pathnamePrefix + key + "'");
|
||||
console.log("[HP] key '" + urlpath.substr(pathnamePrefix.length) + "'"
|
||||
+ " failed to match '" + key + "': "
|
||||
+ scmp(key, urlpath.substr(pathnamePrefix.length))
|
||||
);
|
||||
}
|
||||
next();
|
||||
return;
|
||||
|
130
lib/pmp.js
Normal file
130
lib/pmp.js
Normal file
@ -0,0 +1,130 @@
|
||||
'use strict';
|
||||
|
||||
var PromiseA = require('bluebird');
|
||||
var natpmp = require('holepunch-nat-pmp');
|
||||
|
||||
function getGateway() {
|
||||
var exec = require('child_process').exec;
|
||||
var netroute;
|
||||
var gw;
|
||||
|
||||
try {
|
||||
netroute = require('netroute');
|
||||
gw = netroute.getGateway();
|
||||
} catch(e) {
|
||||
}
|
||||
|
||||
if (gw) {
|
||||
return PromiseA.resolve(gw);
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
return gw;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function pmpForwardHelper(gw, portInfo) {
|
||||
return new PromiseA(function (resolve, reject) {
|
||||
// create a "client" instance connecting to your local gateway
|
||||
var client = natpmp.connect(gw);
|
||||
client.on('error', function (err) {
|
||||
reject(err);
|
||||
});
|
||||
|
||||
function setPortForward() {
|
||||
// setup a new port mapping
|
||||
client.portMapping({
|
||||
private: portInfo.internal || portInfo.private
|
||||
|| portInfo.external || portInfo.public
|
||||
, public: portInfo.external || portInfo.public
|
||||
|| portInfo.internal || portInfo.private
|
||||
, ttl: portInfo.ttl || 7200 // 0 // 600
|
||||
}, function (err, info) {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(info);
|
||||
// {
|
||||
// type: 'tcp',
|
||||
// epoch: 8922109,
|
||||
// private: 22,
|
||||
// public: 2222,
|
||||
// ...
|
||||
// }
|
||||
|
||||
client.close();
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
|
||||
// explicitly ask for the current external IP address
|
||||
// TODO why did I use a setTimeout here? event loop / timing bug?
|
||||
setTimeout(function () {
|
||||
client.externalIp(function (err, info) {
|
||||
if (err) {
|
||||
console.error('[HP] Error: setTimeout client.externalIp:');
|
||||
console.error(err.stack);
|
||||
return PromiseA.reject(err);
|
||||
}
|
||||
console.log('Current external IP address: %s', info.ip.join('.'));
|
||||
setPortForward();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function pmpForward(portInfo) {
|
||||
return getGateway().then(function (gw) {
|
||||
return pmpForwardHelper(gw, portInfo);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = function (args, ips, portInfo) {
|
||||
if (args.debug) {
|
||||
console.log('[HP] [pmp] portInfo');
|
||||
console.log(portInfo);
|
||||
}
|
||||
return pmpForward(portInfo);
|
||||
};
|
||||
|
||||
module.exports.pmpForward = pmpForward;
|
||||
|
||||
/*
|
||||
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();
|
||||
}
|
||||
*/
|
57
lib/request.js
Normal file
57
lib/request.js
Normal file
@ -0,0 +1,57 @@
|
||||
'use strict';
|
||||
|
||||
var PromiseA = require('bluebird');
|
||||
var https = PromiseA.promisifyAll(require('https'));
|
||||
var http = PromiseA.promisifyAll(require('http'));
|
||||
|
||||
function requestAsync(opts) {
|
||||
return new PromiseA(function (resolve, reject) {
|
||||
var httpr = (false === opts.secure) ? http : https;
|
||||
|
||||
if (opts.debug) {
|
||||
console.log('[HP] requestAsync opts');
|
||||
console.log(opts);
|
||||
}
|
||||
|
||||
var req = httpr.request(opts, function (res) {
|
||||
var data = '';
|
||||
|
||||
res.on('error', function (err) {
|
||||
if (opts.debug) {
|
||||
console.error('[Error] HP: bad request:');
|
||||
console.error(err);
|
||||
}
|
||||
reject(err);
|
||||
});
|
||||
res.on('data', function (chunk) {
|
||||
clearTimeout(req.__timtok);
|
||||
|
||||
if (opts.debug > 2) {
|
||||
console.log('HP: request chunk:');
|
||||
console.log(chunk);
|
||||
}
|
||||
|
||||
data += chunk.toString('utf8');
|
||||
});
|
||||
res.on('end', function () {
|
||||
if (opts.debug > 2) {
|
||||
console.log('HP: request complete:');
|
||||
console.log(data);
|
||||
}
|
||||
resolve(data);
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', reject);
|
||||
req.setTimeout(3 * 1000);
|
||||
req.on('socket', function (socket) {
|
||||
req.__timtok = setTimeout(function () {
|
||||
req.abort();
|
||||
}, 3 * 1000);
|
||||
socket.setTimeout(3 * 1000);
|
||||
});
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = requestAsync;
|
@ -1,15 +1,33 @@
|
||||
'use strict';
|
||||
|
||||
var PromiseA = require('bluebird').Promise;
|
||||
var natUpnp = require('nat-upnp');
|
||||
var natUpnp = require('holepunch-upnp');
|
||||
var client;
|
||||
|
||||
function upnpForward(opts) {
|
||||
if (opts.debug) {
|
||||
console.log('[HP] [upnp] opts');
|
||||
console.log(opts);
|
||||
}
|
||||
|
||||
function useClient(client) {
|
||||
if (opts.debug) {
|
||||
console.log('[HP] [upnp] created client');
|
||||
console.log(client);
|
||||
}
|
||||
|
||||
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 () {
|
||||
public: opts.external || opts.public || opts.internal
|
||||
, private: opts.internal || opts.private || opts.public || opts.external
|
||||
, ttl: opts.ttl || 0
|
||||
}).then(function (result) {
|
||||
if (opts.debug) {
|
||||
console.log('[HP] [upnp] result');
|
||||
console.log(result);
|
||||
}
|
||||
return result;
|
||||
});
|
||||
/*.then(function () {
|
||||
var promitter = client.getMappings();
|
||||
|
||||
promitter.on('entry', function (entry, i) {
|
||||
@ -21,10 +39,33 @@ exports.upnpForward = function (port) {
|
||||
});
|
||||
|
||||
return promitter;
|
||||
})*/;
|
||||
});*/
|
||||
}
|
||||
|
||||
if (client) {
|
||||
return useClient(client);
|
||||
} else {
|
||||
return natUpnp.createClient({ timeout: 3 * 1000 }).then(function (_client) {
|
||||
client = _client;
|
||||
useClient(client);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = function (args, ips, portInfo) {
|
||||
// TODO ips.forEach
|
||||
return upnpForward({
|
||||
debug: args.debug
|
||||
, private: portInfo.internal
|
||||
, internal: portInfo.internal
|
||||
, public: portInfo.external
|
||||
, external: portInfo.external
|
||||
// , localAddress: ip.localAddress
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.upnpForward = upnpForward;
|
||||
|
||||
/*
|
||||
client.portUnmapping({
|
||||
public: 80
|
||||
@ -69,9 +110,9 @@ function run() {
|
||||
exports.upnpForward(options).then(function () {
|
||||
console.log('done');
|
||||
}).catch(function (err) {
|
||||
console.error('ERROR');
|
||||
console.error(err);
|
||||
throw err;
|
||||
console.error('[HP] Error: upnpForward:');
|
||||
console.error(err.stack);
|
||||
return PromiseA.reject(err);
|
||||
});
|
||||
}
|
||||
|
@ -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');
|
||||
});
|
||||
});
|
||||
};
|
17
package.json
17
package.json
@ -1,15 +1,26 @@
|
||||
{
|
||||
"name": "holepunch",
|
||||
"version": "1.0.0-alpha.1",
|
||||
"version": "1.0.0-alpha.2",
|
||||
"description": "Get a direct ip connection by any means possible - direct (public ip), upnp (Microsoft), nat-pmp (Apple), or punch a hole through a firewall using a Reverse VPN (Daplie).",
|
||||
"main": "index.js",
|
||||
"bin": {
|
||||
"holepunch": "holepunch.js"
|
||||
"holepunch": "bin/holepunch.js"
|
||||
},
|
||||
"directories": {
|
||||
"example": "examples"
|
||||
},
|
||||
"dependencies": {},
|
||||
"dependencies": {
|
||||
"bluebird": "^3.1.1",
|
||||
"cli": "^0.11.1",
|
||||
"express": "^4.13.3",
|
||||
"holepunch-nat-pmp": "^1.0.0-alpha.2",
|
||||
"holepunch-upnp": "^1.0.0-alpha.2",
|
||||
"localhost.daplie.com-certificates": "^1.1.2",
|
||||
"scmp": "^1.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"netroute": "^1.0.2"
|
||||
},
|
||||
"devDependencies": {},
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
|
17
tests/upnp.js
Normal file
17
tests/upnp.js
Normal file
@ -0,0 +1,17 @@
|
||||
'use strict';
|
||||
|
||||
var upnp = require('../lib/upnp');
|
||||
var opts = {
|
||||
debug: true
|
||||
};
|
||||
var ips = [];
|
||||
var portInfo = {
|
||||
internal: 65080
|
||||
, external: 65080
|
||||
};
|
||||
|
||||
upnp(opts, ips, portInfo).then(function (result) {
|
||||
console.log('results', result);
|
||||
}, function (err) {
|
||||
console.error('error', err.stack);
|
||||
});
|
Loading…
x
Reference in New Issue
Block a user