lazier load, faster https listen

This commit is contained in:
AJ ONeal 2015-02-20 21:21:37 +00:00
parent be2c73ef08
commit 90640ee8e7
3 changed files with 260 additions and 214 deletions

View File

@ -1,15 +1,51 @@
'use strict';
var https = require('https');
module.exports.create = function (securePort, certsPath, vhostsdir) {
var PromiseA = require('bluebird').Promise;
var forEachAsync = require('foreachasync').forEachAsync.create(PromiseA);
var https = require('https');
var fs = require('fs');
var path = require('path');
var crypto = require('crypto');
var dummyCerts;
var secureContexts = {};
function loadDummyCerts() {
if (dummyCerts) {
return dummyCerts;
}
dummyCerts = {
key: fs.readFileSync(path.join(certsPath, 'server', 'dummy-server.key.pem'))
, cert: fs.readFileSync(path.join(certsPath, 'server', 'dummy-server.crt.pem'))
, ca: fs.readdirSync(path.join(certsPath, 'ca')).filter(function (node) {
return /crt\.pem$/.test(node);
}).map(function (node) {
console.log('[log dummy ca]', node);
return fs.readFileSync(path.join(certsPath, 'ca', node));
})
};
return dummyCerts;
}
function createSecureContext(certs) {
// workaround for v0.12 / v1.2 backwards compat
try {
return require('tls').createSecureContext(certs);
} catch(e) {
return require('crypto').createCredentials(certs).context;
}
}
function createPromiseApps(secureServer) {
return new PromiseA(function (resolve) {
var forEachAsync = require('foreachasync').forEachAsync.create(PromiseA);
var connect = require('connect');
var app = connect();
var vhost = require('vhost');
module.exports.create = function (securePort, certsPath, vhostsdir) {
var domainMergeMap = {};
var domainMerged = [];
function getDomainInfo(apppath) {
var parts = apppath.split(/[#%]+/);
var hostname = parts.shift();
@ -61,7 +97,7 @@ module.exports.create = function (securePort, certsPath, vhostsdir) {
} catch(e) {
console.error('[ERROR] ' + domaininfo.hostname + ':' + securePort + '/' + domaininfo.pathname);
console.error(e);
res.send('{ "error": { "message": "[ERROR] could not load '
res.end('{ "error": { "message": "[ERROR] could not load '
+ domaininfo.hostname + ':' + securePort + '/' + domaininfo.pathname
+ 'or default error app." } }');
}
@ -75,14 +111,6 @@ module.exports.create = function (securePort, certsPath, vhostsdir) {
return PromiseA.resolve();
}
function loadDomainVhosts() {
domainMerged.forEach(function (domainApp) {
console.log('[log] merged ' + domainApp.hostname);
app.use(vhost(domainApp.hostname, domainApp.apps));
app.use(vhost('www.' + domainApp.hostname, domainApp.apps));
});
}
function readNewVhosts() {
return fs.readdirSync(vhostsdir).filter(function (node) {
// not a hidden or private file
@ -104,32 +132,6 @@ module.exports.create = function (securePort, certsPath, vhostsdir) {
});
}
// connect / express app
var app = connect();
// SSL Server
var secureContexts = {};
var dummyCerts;
var secureOpts;
var secureServer;
/*
var rootDomains = domains.filter(function (domaininfo) {
return domaininfo.isRoot;
});
*/
var domainMergeMap = {};
var domainMerged = [];
function createSecureContext(certs) {
// workaround for v0.12 / v1.2 backwards compat
try {
return require('tls').createSecureContext(certs);
} catch(e) {
return require('crypto').createCredentials(certs).context;
}
}
function getDummyAppContext(err, msg) {
console.error('[ERROR] getDummyAppContext');
console.error(err);
@ -170,22 +172,70 @@ module.exports.create = function (securePort, certsPath, vhostsdir) {
return localApp;
}
function loadDummyCerts() {
var certs = {
key: fs.readFileSync(path.join(certsPath, 'server', 'dummy-server.key.pem'))
, cert: fs.readFileSync(path.join(certsPath, 'server', 'dummy-server.crt.pem'))
, ca: fs.readdirSync(path.join(certsPath, 'ca')).filter(function (node) {
return /crt\.pem$/.test(node);
}).map(function (node) {
console.log('[log dummy ca]', node);
return fs.readFileSync(path.join(certsPath, 'ca', node));
})
};
secureContexts.dummy = createSecureContext(dummyCerts);
dummyCerts = certs;
return certs
function loadDomainVhosts() {
domainMerged.forEach(function (domainApp) {
console.log('[log] loaded mounts for domain ' + domainApp.hostname);
app.use(vhost(domainApp.hostname, domainApp.apps));
app.use(vhost('www.' + domainApp.hostname, domainApp.apps));
});
}
function hotloadApp(req, res, next) {
var forEachAsync = require('foreachasync').forEachAsync.create(PromiseA);
var vhost = (req.headers.host || '').split(':')[0];
// the matching domain didn't catch it
if (domainMergeMap[vhost]) {
next();
return;
}
return forEachAsync(readNewVhosts(), loadDomainMounts).then(loadDomainVhosts).then(function () {
// no matching domain was added
if (!domainMergeMap[vhost]) {
next();
return;
}
return forEachAsync(domainMergeMap[vhost].apps, function (fn) {
return new PromiseA(function (resolve) {
try {
fn(req, res, function (err) {
if (err) {
reject(err);
}
resolve();
});
} catch(e) {
reject(e);
}
});
}).catch(function (e) {
next(e);
});
});
/*
// TODO loop through mounts and see if any fit
domainMergeMap[vhost].mountsMap['/' + domaininfo.dirpathname]
if (!domainMergeMap[domaininfo.hostname]) {
// TODO reread directories
}
*/
}
// TODO pre-cache these once the server has started?
// return forEachAsync(rootDomains, loadCerts);
// TODO load these even more lazily
return forEachAsync(readNewVhosts(), loadDomainMounts).then(loadDomainVhosts).then(function () {
app.use(hotloadApp);
resolve(app);
return;
});
});
};
function loadCerts(domainname) {
// TODO make async
// WARNING: This must be SYNC until we KNOW we're not going to be running on v0.10
@ -219,7 +269,7 @@ module.exports.create = function (securePort, certsPath, vhostsdir) {
}
try {
secureContexts[domainname] = crypto.createCredentials(secOpts).context;
secureContexts[domainname] = createSecureContext(secOpts).context;
} catch(err) {
console.error("[ERROR] Certificates in '" + certsPath + "' could not be used:");
console.error(err);
@ -229,36 +279,9 @@ module.exports.create = function (securePort, certsPath, vhostsdir) {
return secureContexts[domainname];
}
// TODO pre-cache these once the server has started?
// return forEachAsync(rootDomains, loadCerts);
// TODO load these even more lazily
return forEachAsync(readNewVhosts(), loadDomainMounts).then(loadDomainVhosts).then(runServer);
function hotloadApp(req, res, next) {
var vhost = (req.headers.host || '').split(':')[0];
if (!domainMergeMap[vhost]) {
// TODO reread directories
}
/*
// TODO loop through mounts and see if any fit
domainMergeMap[vhost].mountsMap['/' + domaininfo.dirpathname]
if (!domainMergeMap[domaininfo.hostname]) {
// TODO reread directories
}
*/
// TODO hot load all-the-things
next();
}
app.use(hotloadApp);
function runServer() {
//provide a SNICallback when you create the options for the https server
loadDummyCerts();
secureOpts = {
function createSecureServer() {
var dummyCerts = loadDummyCerts();
var secureOpts = {
// fallback / default dummy certs
key: dummyCerts.key
, cert: dummyCerts.cert
@ -270,10 +293,14 @@ module.exports.create = function (securePort, certsPath, vhostsdir) {
secureOpts.SNICallback = function (domainname, cb) {
console.log('[log] SNI:', domainname);
if (!secureContexts.dummy) {
secureContexts.dummy = createSecureContext(dummyCerts);
}
var secureContext = secureContexts[domainname]
|| loadCerts(domainname)
|| secureContexts.dummy
|| createSecureContext(dummyCerts)
//|| createSecureContext(dummyCerts)
//|| createSecureContext(loadDummyCerts())
;
@ -286,28 +313,47 @@ module.exports.create = function (securePort, certsPath, vhostsdir) {
secureContext = createSecureContext(loadDummyCerts());
}
console.log('[log]', secureContext);
// workaround for v0.12 / v1.2 backwards compat bug
if ('function' === typeof cb) {
console.log('using sni callback callback');
cb(null, secureContext);
} else {
console.log('NOT using sni callback callback');
return secureContext;
}
};
}
addSniWorkaroundCallback();
secureServer = https.createServer(secureOpts);
addSniWorkaroundCallback();
return https.createServer(secureOpts);
}
function runServer() {
return new PromiseA(function (resolve) {
var secureServer = createSecureServer();
var promiseApps;
function loadPromise() {
if (!promiseApps) {
promiseApps = createPromiseApps(secureServer);
}
return promiseApps;
}
secureServer.listen(securePort, function () {
resolve(secureServer);
console.log("Listening on https://localhost:" + secureServer.address().port, '\n');
loadPromise();
});
// Get up and listening as absolutely quickly as possible
secureServer.on('request', function (req, res) {
loadPromise().then(function (app) {
app(req, res);
});
secureServer.listen(securePort, function () {
console.log("Listening on https://localhost:" + secureServer.address().port);
});
return PromiseA.resolve();
return secureServer;
});
}
return runServer();
}

View File

@ -51,6 +51,7 @@
"express": "^4.11.2",
"express-session": "^1.10.3",
"foreachasync": "^5.0.5",
"http-proxy": "^1.8.1",
"human-readable-ids": "^1.0.1",
"nat-pmp": "0.0.3",
"node-acme": "0.0.1",

View File

@ -37,8 +37,6 @@ function phoneHome() {
}
];
//
/*
// TODO return a middleware
holepunch.run(require('./redirects.json').reduce(function (all, redirect) {
if (!all[redirect.from.hostname]) {
@ -54,7 +52,8 @@ function phoneHome() {
}, []), ports).catch(function () {
console.error("Couldn't phone home. Oh well");
});
//*/
}
require('./lib/insecure-server').create(securePort, insecurePort, redirects);
require('./lib/vhost-sni-server.js').create(securePort, certsPath, vhostsdir).then(phoneHome);
require('./lib/vhost-sni-server.js').create(securePort, certsPath, vhostsdir)
//.then(phoneHome)
;