Merge branch '2.x'

This commit is contained in:
AJ ONeal 2016-10-20 22:26:35 -06:00
commit 411d7adc20
8 changed files with 495 additions and 11 deletions

View File

@ -23,12 +23,25 @@ Serving /Users/foo/ at https://localhost.daplie.com:8443
Usage
-----
Examples:
```
node serve.js --servername jane.daplie.me --agree-tos --email jane@example.com --tunnel
```
Options:
* `-p <port>` - i.e. `sudo serve-https -p 443` (defaults to 80+443 or 8443)
* `-d <dirpath>` - i.e. `serve-https -d /tmp/` (defaults to `pwd`)
* `-c <content>` - i.e. `server-https -c 'Hello, World! '` (defaults to directory index)
* `--express-app` - path to a file the exports an express-style app (`function (req, res, next) { ... }`)
* `--express-app <path>` - path to a file the exports an express-style app (`function (req, res, next) { ... }`)
* `--livereload` - inject livereload into all html pages (see also: [fswatch](http://stackoverflow.com/a/13807906/151312)), but be careful if `<dirpath>` has thousands of files it will spike your CPU usage to 100%
* `--email <email>` - email to use for Let's Encrypt, Daplie DNS, Daplie Tunnel
* `--agree-tos` - agree to terms for Let's Encrypt, Daplie DNS
* `--servername <servername>` - use `<servername>` instead of `localhost.daplie.com`
* `--tunnel` - make world-visible (must use `--servername`)
Specifying a custom HTTPS certificate:
* `--key /path/to/privkey.pem` specifies the server private key

View File

88
lib/ddns.js Normal file
View File

@ -0,0 +1,88 @@
'use strict';
module.exports.create = function (opts/*, servers*/) {
var PromiseA = opts.PromiseA;
var dns = PromiseA.promisifyAll(require('dns'));
return PromiseA.all([
dns.resolve4Async(opts.servername).then(function (results) {
return results;
}, function () {})
, dns.resolve6Async(opts.servername).then(function (results) {
return results;
}, function () {})
]).then(function (results) {
var ipv4 = results[0] || [];
var ipv6 = results[1] || [];
var record;
opts.dnsRecords = {
A: ipv4
, AAAA: ipv6
};
Object.keys(opts.ifaces).some(function (ifacename) {
var iface = opts.ifaces[ifacename];
return iface.ipv4.some(function (localIp) {
return ipv4.some(function (remoteIp) {
if (localIp.address === remoteIp) {
record = localIp;
return record;
}
});
}) || iface.ipv6.some(function (localIp) {
return ipv6.forEach(function (remoteIp) {
if (localIp.address === remoteIp) {
record = localIp;
return record;
}
});
});
});
if (!record) {
console.info("DNS Record '" + ipv4.concat(ipv6).join(',') + "' does not match any local IP address.");
console.info("Use --ddns to allow the people of the Internet to access your server.");
}
opts.externalIps.ipv4.some(function (localIp) {
return ipv4.some(function (remoteIp) {
if (localIp.address === remoteIp) {
record = localIp;
return record;
}
});
});
opts.externalIps.ipv6.some(function (localIp) {
return ipv6.some(function (remoteIp) {
if (localIp.address === remoteIp) {
record = localIp;
return record;
}
});
});
if (!record) {
console.info("DNS Record '" + ipv4.concat(ipv6).join(',') + "' does not match any local IP address.");
console.info("Use --ddns to allow the people of the Internet to access your server.");
}
});
};
if (require.main === module) {
var opts = {
servername: 'aj.daplie.me'
, PromiseA: require('bluebird')
};
// ifaces
opts.ifaces = require('./local-ip.js').find();
console.log('opts.ifaces');
console.log(opts.ifaces);
require('./match-ips.js').match(opts.servername, opts).then(function (ips) {
opts.matchingIps = ips.matchingIps || [];
opts.externalIps = ips.externalIps;
module.exports.create(opts);
});
}

117
lib/match-ips.js Normal file
View File

@ -0,0 +1,117 @@
'use strict';
var PromiseA = require('bluebird');
module.exports.match = function (servername, opts) {
return PromiseA.promisify(require('ipify'))().then(function (externalIp) {
var dns = PromiseA.promisifyAll(require('dns'));
opts.externalIps = [ { address: externalIp, family: 'IPv4' } ];
opts.ifaces = require('./local-ip.js').find({ externals: opts.externalIps });
opts.externalIfaces = Object.keys(opts.ifaces).reduce(function (all, iname) {
var iface = opts.ifaces[iname];
iface.ipv4.forEach(function (addr) {
if (addr.external) {
addr.iface = iname;
all.push(addr);
}
});
iface.ipv6.forEach(function (addr) {
if (addr.external) {
addr.iface = iname;
all.push(addr);
}
});
return all;
}, []).filter(Boolean);
function resolveIps(hostname) {
var allIps = [];
return PromiseA.all([
dns.resolve4Async(hostname).then(function (records) {
records.forEach(function (ip) {
allIps.push({
address: ip
, family: 'IPv4'
});
});
}, function () {})
, dns.resolve6Async(hostname).then(function (records) {
records.forEach(function (ip) {
allIps.push({
address: ip
, family: 'IPv6'
});
});
}, function () {})
]).then(function () {
return allIps;
});
}
function resolveIpsAndCnames(hostname) {
return PromiseA.all([
resolveIps(hostname)
, dns.resolveCnameAsync(hostname).then(function (records) {
return PromiseA.all(records.map(function (hostname) {
return resolveIps(hostname);
})).then(function (allIps) {
return allIps.reduce(function (all, ips) {
return all.concat(ips);
}, []);
});
}, function () {
return [];
})
]).then(function (ips) {
return ips.reduce(function (all, set) {
return all.concat(set);
}, []);
});
}
return resolveIpsAndCnames(servername).then(function (allIps) {
var matchingIps = [];
if (!allIps.length) {
console.warn("Could not resolve '" + servername + "'");
}
// { address, family }
allIps.some(function (ip) {
function match(addr) {
if (ip.address === addr.address) {
matchingIps.push(addr);
}
}
opts.externalIps.forEach(match);
// opts.externalIfaces.forEach(match);
Object.keys(opts.ifaces).forEach(function (iname) {
var iface = opts.ifaces[iname];
iface.ipv4.forEach(match);
iface.ipv6.forEach(match);
});
return matchingIps.length;
});
matchingIps.externalIps = {
ipv4: [
{ address: externalIp
, family: 'IPv4'
}
]
, ipv6: [
]
};
matchingIps.matchingIps = matchingIps;
return matchingIps;
});
});
};

137
lib/tunnel.js Normal file
View File

@ -0,0 +1,137 @@
'use strict';
module.exports.create = function (opts, servers) {
// servers = { plainserver, server }
var Oauth3 = require('oauth3-cli');
var Tunnel = require('daplie-tunnel').create({
Oauth3: Oauth3
, PromiseA: opts.PromiseA
, CLI: {
init: function (/*rs, ws, state, options*/) {
// noop
}
}
}).Tunnel;
var stunnel = require('stunnel');
var killcount = 0;
/*
var Dup = {
write: function (chunk, encoding, cb) {
this.__my_socket.push(chunk, encoding);
cb();
}
, read: function (size) {
var x = this.__my_socket.read(size);
if (x) { this.push(x); }
}
, setTimeout: function () {
console.log('TODO implement setTimeout on Duplex');
}
};
var httpServer = require('http').createServer(function (req, res) {
console.log('req.socket.encrypted', req.socket.encrypted);
res.end('Hello, tunneled World!');
});
var tlsServer = require('tls').createServer(opts.httpsOptions, function (tlsSocket) {
console.log('tls connection');
// things get a little messed up here
httpServer.emit('connection', tlsSocket);
// try again
//servers.server.emit('connection', tlsSocket);
});
*/
process.on('SIGINT', function () {
killcount += 1;
console.log('[quit] closing http and https servers');
if (killcount >= 3) {
process.exit(1);
}
if (servers.server) {
servers.server.close();
}
if (servers.insecureServer) {
servers.insecureServer.close();
}
});
return Tunnel.token({
refreshToken: opts.refreshToken
, email: opts.email
, domains: [ opts.servername ]
, device: { hostname: opts.devicename || opts.device }
}).then(function (result) {
// { jwt, tunnelUrl }
return stunnel.connect({
token: result.jwt
, stunneld: result.tunnelUrl
// XXX TODO BUG // this is just for testing
, insecure: /*opts.insecure*/ true
, locals: [
{ protocol: 'https'
, hostname: opts.servername
, port: opts.port
}
, { protocol: 'http'
, hostname: opts.servername
, port: opts.insecurePort || opts.port
}
]
// a simple passthru is proving to not be so simple
, net: require('net') /*
{
createConnection: function (info, cb) {
// data is the hello packet / first chunk
// info = { data, servername, port, host, remoteAddress: { family, address, port } }
var myDuplex = new (require('stream').Duplex)();
var myDuplex2 = new (require('stream').Duplex)();
// duplex = { write, push, end, events: [ 'readable', 'data', 'error', 'end' ] };
myDuplex2.__my_socket = myDuplex;
myDuplex.__my_socket = myDuplex2;
myDuplex2._write = Dup.write;
myDuplex2._read = Dup.read;
myDuplex._write = Dup.write;
myDuplex._read = Dup.read;
myDuplex.remoteFamily = info.remoteFamily;
myDuplex.remoteAddress = info.remoteAddress;
myDuplex.remotePort = info.remotePort;
// socket.local{Family,Address,Port}
myDuplex.localFamily = 'IPv4';
myDuplex.localAddress = '127.0.01';
myDuplex.localPort = info.port;
myDuplex.setTimeout = Dup.setTimeout;
// this doesn't seem to work so well
//servers.server.emit('connection', myDuplex);
// try a little more manual wrapping / unwrapping
var firstByte = info.data[0];
if (firstByte < 32 || firstByte >= 127) {
tlsServer.emit('connection', myDuplex);
}
else {
httpServer.emit('connection', myDuplex);
}
if (cb) {
process.nextTick(cb);
}
return myDuplex2;
}
}
//*/
});
});
};

View File

@ -1,6 +1,6 @@
{
"name": "serve-https",
"version": "1.6.1",
"version": "2.0.2",
"description": "Serves HTTPS using TLS (SSL) certs for localhost.daplie.com - great for testing and development.",
"main": "serve.js",
"scripts": {
@ -38,14 +38,24 @@
},
"homepage": "https://github.com/Daplie/serve-https#readme",
"dependencies": {
"bluebird": "^3.4.6",
"daplie-tunnel": "git+https://github.com/Daplie/daplie-cli-tunnel.git#master",
"ddns-cli": "git+https://github.com/Daplie/node-ddns-client.git#master",
"finalhandler": "^0.4.0",
"httpolyglot": "^0.1.1",
"ipify": "^1.1.0",
"livereload": "^0.5.0",
"le-challenge-ddns": "git+https://github.com/Daplie/le-challenge-ddns.git#master",
"le-challenge-fs": "git+https://github.com/Daplie/le-challenge-fs.git#master",
"le-challenge-sni": "^2.0.1",
"letsencrypt-express": "git+https://github.com/Daplie/letsencrypt-express.git#master",
"letsencrypt": "git+https://github.com/Daplie/node-letsencrypt.git#master",
"livereload": "^0.6.0",
"localhost.daplie.com-certificates": "^1.2.0",
"minimist": "^1.1.1",
"oauth3-cli": "git+https://github.com/OAuth3/oauth3-cli.git#master",
"redirect-https": "^1.1.0",
"serve-index": "^1.7.0",
"serve-static": "^1.10.0"
"serve-static": "^1.10.0",
"stunnel": "git+https://github.com/Daplie/node-tunnel-client.git#master"
}
}

133
serve.js
View File

@ -8,6 +8,7 @@ var https = require('httpolyglot');
var http = require('http');
var fs = require('fs');
var path = require('path');
var DDNS = require('ddns-cli');
var httpPort = 80;
var httpsPort = 443;
var lrPort = 35729;
@ -56,9 +57,42 @@ function createInsecureServer(port, pubdir, opts) {
}
function createServer(port, pubdir, content, opts) {
function approveDomains(params, certs, cb) {
// This is where you check your database and associated
// email addresses with domains and agreements and such
var domains = params.domains;
//var p;
console.log('approveDomains');
console.log(domains);
// The domains being approved for the first time are listed in opts.domains
// Certs being renewed are listed in certs.altnames
if (certs) {
params.domains = certs.altnames;
//p = PromiseA.resolve();
}
else {
//params.email = opts.email;
if (!opts.agreeTos) {
console.error("You have not previously registered '" + domains + "' so you must specify --agree-tos to agree to both the Let's Encrypt and Daplie DNS terms of service.");
process.exit(1);
return;
}
params.agreeTos = opts.agreeTos;
}
// ddns.token(params.email, domains[0])
params.email = opts.email;
params.refreshToken = opts.refreshToken;
params.challengeType = 'dns-01';
params.cli = opts.argv;
cb(null, { options: params, certs: certs });
}
return new PromiseA(function (realResolve) {
var server = https.createServer(opts.httpsOptions);
var app = require('./app');
var app = require('./lib/app.js');
var directive = { public: pubdir, content: content, livereload: opts.livereload
, servername: opts.servername, expressApp: opts.expressApp };
@ -71,6 +105,48 @@ function createServer(port, pubdir, content, opts) {
});
}
// returns an instance of node-letsencrypt with additional helper methods
var webrootPath = require('os').tmpdir();
var leChallengeFs = require('le-challenge-fs').create({ webrootPath: webrootPath });
//var leChallengeSni = require('le-challenge-sni').create({ webrootPath: webrootPath });
var leChallengeDdns = require('le-challenge-ddns').create({ ttl: 1 });
var lex = require('letsencrypt-express').create({
// set to https://acme-v01.api.letsencrypt.org/directory in production
server: opts.debug ? 'staging' : 'https://acme-v01.api.letsencrypt.org/directory'
// If you wish to replace the default plugins, you may do so here
//
, challenges: {
'http-01': leChallengeFs
, 'tls-sni-01': leChallengeFs // leChallengeSni
, 'dns-01': leChallengeDdns
}
, challengeType: (opts.tunnel ? 'http-01' : 'dns-01')
, store: require('le-store-certbot').create({ webrootPath: webrootPath })
, webrootPath: webrootPath
// You probably wouldn't need to replace the default sni handler
// See https://github.com/Daplie/le-sni-auto if you think you do
//, sni: require('le-sni-auto').create({})
, approveDomains: approveDomains
});
var secureContext;
opts.httpsOptions.SNICallback = function (servername, cb ) {
console.log('[https] servername', servername);
if ('localhost.daplie.com' === servername) {
if (!secureContext) {
secureContext = tls.createSecureContext(opts.httpsOptions);
}
cb(null, secureContext);
return;
}
lex.httpsOptions.SNICallback(servername, cb);
};
var server = https.createServer(opts.httpsOptions);
server.on('error', function (err) {
if (opts.errorPort || opts.manualPort) {
showError(err, port);
@ -93,7 +169,7 @@ function createServer(port, pubdir, content, opts) {
var server2 = livereload.createServer({
https: opts.httpsOptions
, port: opts.lrPort
, exclusions: [ '.hg', '.git', '.svn', 'node_modules' ]
, exclusions: [ 'node_modules' ]
});
console.info("[livereload] watching " + pubdir);
@ -119,7 +195,8 @@ function createServer(port, pubdir, content, opts) {
}
server.on('request', function (req, res) {
if (!req.socket.encrypted) {
console.log('[' + req.method + '] ' + req.url);
if (!req.socket.encrypted && !/\/\.well-known\/acme-challenge\//.test(req.url)) {
opts.redirectApp(req, res);
return;
}
@ -165,6 +242,7 @@ function run() {
var opts = {
agreeTos: argv.agreeTos || argv['agree-tos']
, debug: argv.debug
, device: argv.device
, email: argv.email
, httpsOptions: {
key: httpsOptions.key
@ -174,7 +252,9 @@ function run() {
, argv: argv
};
var peerCa;
var p;
opts.PromiseA = PromiseA;
opts.httpsOptions.SNICallback = function (servername, cb) {
if (!secureContext) {
secureContext = tls.createSecureContext(opts.httpsOptions);
@ -244,6 +324,9 @@ function run() {
if (argv.p || argv.port || argv._[0]) {
opts.manualPort = true;
}
if (argv.t || argv.tunnel) {
opts.tunnel = true;
}
if (argv.i || argv['insecure-port']) {
opts.manualInsecurePort = true;
}
@ -257,13 +340,37 @@ function run() {
opts.expressApp = require(path.resolve(process.cwd(), argv['express-app']));
}
if (opts.email || opts.servername) {
if (!opts.agreeTos) {
console.warn("You may need to specify --agree-tos to agree to both the Let's Encrypt and Daplie DNS terms of service.");
}
if (!opts.email) {
// TODO store email in .ddnsrc.json
console.warn("You may need to specify --email to register with both the Let's Encrypt and Daplie DNS.");
}
p = DDNS.refreshToken({
email: opts.email
, silent: true
}, {
debug: false
, email: opts.argv.email
}).then(function (refreshToken) {
opts.refreshToken = refreshToken;
});
}
else {
p = PromiseA.resolve();
}
return p.then(function () {
// can be changed to tunnel external port
opts.redirectOptions = {
port: opts.port
};
opts.redirectApp = require('redirect-https')(opts.redirectOptions);
return createServer(port, pubdir, content, opts).then(function () {
return createServer(port, pubdir, content, opts).then(function (servers) {
var msg;
var p;
var httpsUrl;
@ -292,12 +399,12 @@ function run() {
if (!(argv.servername && defaultServername !== argv.servername && !(argv.key && argv.cert))) {
// ifaces
opts.ifaces = require('./local-ip.js').find();
opts.ifaces = require('./lib/local-ip.js').find();
promise = PromiseA.resolve();
} else {
console.info("Attempting to resolve external connection for '" + argv.servername + "'");
try {
promise = require('./match-ips.js').match(argv.servername, opts);
promise = require('./lib/match-ips.js').match(argv.servername, opts);
} catch(e) {
console.warn("Upgrade to version 2.x to use automatic certificate issuance for '" + argv.servername + "'");
promise = PromiseA.resolve();
@ -333,6 +440,17 @@ function run() {
}
});
}
else if (!opts.tunnel) {
console.info("External IP address does not match local IP address.");
console.info("Use --tunnel to allow the people of the Internet to access your server.");
}
if (opts.tunnel) {
require('./lib/tunnel.js').create(opts, servers);
}
else if (opts.ddns) {
require('./lib/ddns.js').create(opts, servers);
}
Object.keys(opts.ifaces).forEach(function (iname) {
var iface = opts.ifaces[iname];
@ -360,6 +478,7 @@ function run() {
console.info('');
});
});
});
}
if (require.main === module) {