can now vhost mounted subapps
This commit is contained in:
parent
b28a45581f
commit
e4a13a8b80
|
@ -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");
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
|
@ -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');
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue