can now vhost mounted subapps

This commit is contained in:
AJ ONeal 2015-02-14 18:43:04 +00:00
parent b28a45581f
commit e4a13a8b80
2 changed files with 208 additions and 133 deletions

View File

@ -1,48 +1,77 @@
'use strict'; 'use strict';
var https = require('https') var https = require('https');
, http = require('http') var http = require('http');
, PromiseA = require('bluebird').Promise var PromiseA = require('bluebird').Promise;
, forEachAsync = require('foreachasync').forEachAsync.create(PromiseA) var forEachAsync = require('foreachasync').forEachAsync.create(PromiseA);
, fs = require('fs') var fs = require('fs');
, path = require('path') var path = require('path');
, crypto = require('crypto') var crypto = require('crypto');
, connect = require('connect') var connect = require('connect');
, vhost = require('vhost') var vhost = require('vhost');
, escapeRe = require('escape-string-regexp') var escapeRe = require('escape-string-regexp');
// connect / express app // connect / express app
, app = connect() var app = connect();
// SSL Server // SSL Server
, secureContexts = {} var secureContexts = {};
, secureOpts var secureOpts;
, secureServer var secureServer;
, securePort = /*process.argv[2] ||*/ 443 var securePort = process.argv[2] || 443;
// force SSL upgrade server // force SSL upgrade server
, insecureServer var insecureServer;
, insecurePort = /*process.argv[3] ||*/ 80 var insecurePort = process.argv[3] || 80;
// the ssl domains I have // the ssl domains I have
// TODO read vhosts minus // TODO read vhosts minus
, domains = fs.readdirSync(path.join(__dirname, 'vhosts')).filter(function (node) { var domains = fs.readdirSync(path.join(__dirname, 'vhosts')).filter(function (node) {
// not a hidden or private file // not a hidden or private file
return '.' !== node[0] && '_' !== node[0]; return '.' !== node[0] && '_' !== node[0];
}) }).map(function (apppath) {
; var parts = apppath.split(/[#%]+/);
var hostname = parts.shift();
var pathname = parts.join('/').replace(/\/+/g, '/').replace(/^\//, '');
return {
hostname: hostname
, pathname: pathname
, dirname: apppath
, isRoot: apppath === hostname
};
}).sort(function (a, b) {
var hlen = b.hostname.length - a.hostname.length;
var plen = b.pathname.length - a.pathname.length;
// A directory could be named example.com, example.com# example.com##
// to indicate order of preference (for API addons, for example)
var dlen = b.dirname.length - a.dirname.length;
if (!hlen) {
if (!plen) {
return dlen;
}
return plen;
}
return plen;
});
var rootDomains = domains.filter(function (domaininfo) {
return domaininfo.isRoot;
});
var domainMergeMap = {};
var domainMerged = [];
require('ssl-root-cas') require('ssl-root-cas')
.inject() .inject()
; ;
function getAppContext(domain) { function getAppContext(domaininfo) {
var localApp var localApp;
;
localApp = require(path.join(__dirname, 'vhosts', domain, 'app.js')); localApp = require(path.join(__dirname, 'vhosts', domaininfo.dirname, 'app.js'));
if (localApp.create) { if (localApp.create) {
// TODO read local config.yml and pass it in // TODO read local config.yml and pass it in
// TODO pass in websocket
localApp = localApp.create(/*config*/); localApp = localApp.create(/*config*/);
} }
if (!localApp.then) { if (!localApp.then) {
@ -52,20 +81,44 @@ function getAppContext(domain) {
return localApp; return localApp;
} }
forEachAsync(domains, function (domain) { function loadDummyCerts() {
secureContexts[domain] = crypto.createCredentials({ var certsPath = path.join(__dirname, 'certs');
key: fs.readFileSync(path.join(__dirname, 'vhosts', domain, 'certs/server/my-server.key.pem')) var certs = {
, cert: fs.readFileSync(path.join(__dirname, 'vhosts', domain, 'certs/server/my-server.crt.pem')) key: fs.readFileSync(path.join(certsPath, 'server', 'dummy-server.key.pem'))
, ca: fs.readdirSync(path.join(__dirname, 'vhosts', domain, 'certs/ca')).map(function (node) { , cert: fs.readFileSync(path.join(certsPath, 'server', 'dummy-server.crt.pem'))
return fs.readFileSync(path.join(__dirname, 'vhosts', domain, 'certs/ca', node)); , ca: fs.readdirSync(path.join(certsPath, 'ca')).map(function (node) {
return fs.readFileSync(path.join(certsPath, 'ca', node));
})
};
secureContexts.dummy = crypto.createCredentials(certs).context;
secureContexts.dummy.certs = certs;
}
loadDummyCerts();
function loadCerts(domaininfo) {
var certsPath = path.join(__dirname, 'vhosts', domaininfo.dirname, 'certs');
try {
var nodes = fs.readdirSync(path.join(certsPath, 'server'));
var keyNode = nodes.filter(function (node) { return /\.key\.pem$/.test(node); })[0];
var crtNode = nodes.filter(function (node) { return /\.crt\.pem$/.test(node); })[0];
secureContexts[domaininfo.hostname] = crypto.createCredentials({
key: fs.readFileSync(path.join(certsPath, 'server', keyNode))
, cert: fs.readFileSync(path.join(certsPath, 'server', crtNode))
, ca: fs.readdirSync(path.join(certsPath, 'ca')).map(function (node) {
return fs.readFileSync(path.join(certsPath, 'ca', node));
}) })
}).context; }).context;
} catch(err) {
// TODO Let's Encrypt / ACME HTTPS
console.error("[ERROR] Couldn't load HTTPS certs from '" + certsPath + "':");
console.error(err);
secureContexts[domaininfo.hostname] = secureContexts.dummy;
}
}
return getAppContext(domain).then(function (localApp) { forEachAsync(rootDomains, loadCerts).then(function () {
app.use(vhost('www.' + domain, localApp));
app.use(vhost(domain, localApp));
});
}).then(function () {
// fallback / default domain // fallback / default domain
/* /*
app.use('/', function (req, res) { app.use('/', function (req, res) {
@ -73,40 +126,60 @@ forEachAsync(domains, function (domain) {
res.end("<html><body><h1>Hello, World... This isn't the domain you're looking for.</h1></body></html>"); res.end("<html><body><h1>Hello, World... This isn't the domain you're looking for.</h1></body></html>");
}); });
*/ */
}); return forEachAsync(domains, function (domaininfo) {
// should order and group by longest domain, then longest path
//provide a SNICallback when you create the options for the https server if (!domainMergeMap[domaininfo.hostname]) {
secureOpts = { // create an connect / express app exclusive to this domain
//SNICallback is passed the domain name, see NodeJS docs on TLS // TODO express??
SNICallback: function (domain) { domainMergeMap[domaininfo.hostname] = { hostname: domaininfo.hostname, apps: connect() };
//console.log('SNI:', domain); domainMerged.push(domainMergeMap[domaininfo.hostname]);
return secureContexts[domain];
} }
// fallback / default domain
, key: fs.readFileSync(path.join(__dirname, 'certs/server', 'dummy-server.key.pem'))
, cert: fs.readFileSync(path.join(__dirname, 'certs/server', 'dummy-server.crt.pem'))
, ca: fs.readdirSync(path.join(__dirname, 'certs/ca')).map(function (node) {
return fs.readFileSync(path.join(__dirname, 'certs/ca', node));
})
};
secureServer = https.createServer(secureOpts); return getAppContext(domaininfo).then(function (localApp) {
secureServer.on('request', app); // Note: pathname should NEVER have a leading '/' on its own
secureServer.listen(securePort, function () { // we always add it explicitly
domainMergeMap[domaininfo.hostname].apps.use('/' + domaininfo.pathname, localApp);
console.info('Loaded ' + domaininfo.hostname + ':' + securePort + domaininfo.pathname);
});
}).then(function () {
domainMerged.forEach(function (domainApp) {
app.use(vhost(domainApp.hostname, domainApp.apps));
app.use(vhost('www.' + domainApp.hostname, domainApp.apps));
});
});
}).then(runServer);
function runServer() {
//provide a SNICallback when you create the options for the https server
secureOpts = {
//SNICallback is passed the domain name, see NodeJS docs on TLS
SNICallback: function (domainname) {
//console.log('SNI:', domain);
return secureContexts[domainname] || secureContext.dummy;
}
// fallback / default dummy certs
, key: secureContexts.dummy.certs.key
, cert: secureContexts.dummy.certs.cert
, ca: secureContexts.dummy.certs.ca
};
secureServer = https.createServer(secureOpts);
secureServer.on('request', app);
secureServer.listen(securePort, function () {
console.log("Listening on https://localhost:" + secureServer.address().port); console.log("Listening on https://localhost:" + secureServer.address().port);
}); });
// TODO localhost-only server shutdown mechanism // TODO localhost-only server shutdown mechanism
// that closes all sockets, waits for them to finish, // that closes all sockets, waits for them to finish,
// and then hands control over completely to respawned server // and then hands control over completely to respawned server
// //
// Redirect HTTP ot HTTPS // Redirect HTTP ot HTTPS
// //
// This simply redirects from the current insecure location to the encrypted location // This simply redirects from the current insecure location to the encrypted location
// //
insecureServer = http.createServer(); insecureServer = http.createServer();
insecureServer.on('request', function (req, res) { insecureServer.on('request', function (req, res) {
var insecureRedirects; var insecureRedirects;
var host = req.headers.host || ''; var host = req.headers.host || '';
var url = req.url; var url = req.url;
@ -170,7 +243,8 @@ insecureServer.on('request', function (req, res) {
// that they're doing it wrong and thus forces them to ensure they encrypt. // that they're doing it wrong and thus forces them to ensure they encrypt.
res.setHeader('Content-Type', 'text/html'); res.setHeader('Content-Type', 'text/html');
res.end(metaRedirect); res.end(metaRedirect);
}); });
insecureServer.listen(insecurePort, function(){ insecureServer.listen(insecurePort, function(){
console.log("\nRedirecting all http traffic to https\n"); console.log("\nRedirecting all http traffic to https\n");
}); });
}

View File

@ -30,5 +30,6 @@ holepunch.run([
, 'prod.coolaj86.com' , 'prod.coolaj86.com'
, 'production.coolaj86.com' , 'production.coolaj86.com'
], ports).then(function () { ], ports).then(function () {
// TODO use as module
require('./vhost-sni-server.js'); require('./vhost-sni-server.js');
}); });