'use strict'; module.exports.create = function (securePort, vhostsdir) { var PromiseA = require('bluebird').Promise; var serveStatic; var fs = require('fs'); var path = require('path'); var dummyCerts; var serveFavicon; var loopbackToken = require('crypto').randomBytes(32).toString('hex'); function handleAppScopedError(tag, domaininfo, req, res, fn) { function next(err) { if (!err) { fn(req, res); return; } if (res.headersSent) { console.error('[ERROR] handleAppScopedError headersSent'); console.log(err); console.log(err.stack); return; } console.error('[ERROR] handleAppScopedError'); console.log(err); console.log(err.stack); res.writeHead(500); res.end( "" + "
" + '' + "" + "" + ""
+ ""
+ "Method: " + encodeURI(req.method)
+ '\n'
+ "Hostname: " + encodeURI(domaininfo.hostname)
+ '\n'
+ "App: " + encodeURI(domaininfo.pathname ? (domaininfo.pathname + '/') : '')
+ '\n'
+ "Route: " + encodeURI(req.url)//.replace(/^\//, '')
+ '\n'
// TODO better sanatization
+ 'Error: ' + (err.message || err.toString()).replace(/"
+ "
"
+ ""
+ ""
);
}
return next;
}
function createPromiseApps(secureServer) {
return new PromiseA(function (resolve) {
var forEachAsync = require('foreachasync').forEachAsync.create(PromiseA);
var connect = require('connect');
// TODO make lazy
var app = connect().use(require('compression')());
var vhost = require('vhost');
var domainMergeMap = {};
var domainMerged = [];
function getDomainInfo(apppath) {
var parts = apppath.split(/[#%]+/);
var hostname = parts.shift();
var pathname = parts.join('/').replace(/\/+/g, '/').replace(/^\//, '');
return {
hostname: hostname
, pathname: pathname
, dirpathname: parts.join('#')
, dirname: apppath
, isRoot: apppath === hostname
};
}
function loadDomainMounts(domaininfo) {
var connectContext = {};
var appContext;
// should order and group by longest domain, then longest path
if (!domainMergeMap[domaininfo.hostname]) {
// create an connect / express app exclusive to this domain
// TODO express??
domainMergeMap[domaininfo.hostname] = {
hostname: domaininfo.hostname
, apps: connect()
, mountsMap: {}
};
domainMerged.push(domainMergeMap[domaininfo.hostname]);
}
if (domainMergeMap[domaininfo.hostname].mountsMap['/' + domaininfo.dirpathname]) {
return;
}
console.log('[log] [once] Preparing mount for', domaininfo.hostname + '/' + domaininfo.dirpathname);
domainMergeMap[domaininfo.hostname].mountsMap['/' + domaininfo.dirpathname] = function (req, res, next) {
res.setHeader('Strict-Transport-Security', 'max-age=10886400; includeSubDomains; preload');
function loadThatApp() {
var time = Date.now();
console.log('[log] LOADING "' + domaininfo.hostname + '/' + domaininfo.pathname + '"', req.url);
return getAppContext(domaininfo).then(function (localApp) {
console.info((Date.now() - time) + 'ms Loaded ' + domaininfo.hostname + ':' + securePort + '/' + domaininfo.pathname);
//if (localApp.arity >= 2) { /* connect uses .apply(null, arguments)*/ }
if ('function' !== typeof localApp) {
localApp = getDummyAppContext(null, "[ERROR] no connect-style export from " + domaininfo.dirname);
}
// Note: pathname should NEVER have a leading '/' on its own
// we always add it explicitly
function localAppWrapped(req, res) {
console.log('[debug]', domaininfo.hostname + '/' + domaininfo.pathname, req.url);
localApp(req, res, handleAppScopedError('localApp', domaininfo, req, res, function (req, res) {
if (!serveFavicon) {
serveFavicon = require('serve-favicon')(path.join(__dirname, '..', 'public', 'favicon.ico'));
}
// TODO redirect GET /favicon.ico to GET (req.headers.referer||'') + /favicon.ico
// TODO other common root things - robots.txt, app-icon, etc
serveFavicon(req, res, handleAppScopedError('serveFavicon', domaininfo, req, res, function (req, res) {
connectContext.static(req, res, handleAppScopedError('connect.static', domaininfo, req, res, function (req, res) {
res.writeHead(404);
res.end(
""
+ ""
+ ''
+ ""
+ ""
+ "Cannot "
+ encodeURI(req.method)
+ " 'https://"
+ encodeURI(domaininfo.hostname)
+ '/'
+ encodeURI(domaininfo.pathname ? (domaininfo.pathname + '/') : '')
+ encodeURI(req.url.replace(/^\//, ''))
+ "'"
+ "You requested an old resource. Please use this instead: \n' + ' ' + safeLocation + '
\n' + '\n' + '\n' ; // 301 redirects will not work for appcache res.end(metaRedirect); })); }); } /* 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 console.log('[log] vhost:', vhost); 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, reject) { function next(err) { if (err) { reject(err); } resolve(); } try { fn(req, res, next); } 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 () { console.log('[log] TODO fix and use hotload'); //app.use(hotloadApp); resolve(app); return; }); }); } return { create: createPromiseApps }; };