diff --git a/lib/insecure-server.js b/lib/insecure-server.js index 9be997b..fbda75b 100644 --- a/lib/insecure-server.js +++ b/lib/insecure-server.js @@ -1,9 +1,10 @@ 'use strict'; -var http = require('http'); -var escapeRe = require('escape-string-regexp'); - module.exports.create = function (securePort, insecurePort, redirects) { + var PromiseA = require('bluebird').Promise; + var http = require('http'); + var escapeRe; + function redirectHttps(req, res) { var insecureRedirects; var host = req.headers.host || ''; @@ -20,6 +21,11 @@ module.exports.create = function (securePort, insecurePort, redirects) { return hlen; }).forEach(function (redirect) { var origHost = host; + + if (!escapeRe) { + escapeRe = require('escape-string-regexp'); + } + // TODO if '*' === hostname[0], omit '^' host = host.replace( new RegExp('^' + escapeRe(redirect.from.hostname)) @@ -42,11 +48,11 @@ module.exports.create = function (securePort, insecurePort, redirects) { + '\n' + '\n' + ' \n' - + ' \n' + + ' \n' + '\n' + '\n' + '

You requested an insecure resource. Please use this instead: \n' - + ' ' + encodeURI(newLocation) + '

\n' + + ' ' + newLocation + '

\n' + '\n' + '\n' ; @@ -83,7 +89,9 @@ module.exports.create = function (securePort, insecurePort, redirects) { insecureServer = http.createServer(); insecureServer.on('request', redirectHttps); insecureServer.listen(insecurePort, function () { - console.log("\nListening on https://localhost:" + insecureServer.address().port); - console.log("(redirecting all traffic to https)\n"); + console.log("\nListening on https://localhost:" + insecureServer.address().port); + console.log("(redirecting all traffic to https)\n"); }); + + return PromiseA.resolve(insecureServer); }; diff --git a/lib/vhost-sni-server.js b/lib/vhost-sni-server.js index 2193940..ee03844 100644 --- a/lib/vhost-sni-server.js +++ b/lib/vhost-sni-server.js @@ -1,7 +1,8 @@ 'use strict'; - + module.exports.create = function (securePort, certsPath, vhostsdir) { var PromiseA = require('bluebird').Promise; + var serveStatic; var https = require('https'); var fs = require('fs'); var path = require('path'); @@ -28,7 +29,7 @@ module.exports.create = function (securePort, certsPath, vhostsdir) { return dummyCerts; } - function handleAppScopedError(req, res, fn) { + function handleAppScopedError(domaininfo, req, res, fn) { function next(err) { if (!err) { fn(req, res); @@ -69,7 +70,7 @@ module.exports.create = function (securePort, certsPath, vhostsdir) { // workaround for v0.12 / v1.2 backwards compat try { return require('tls').createSecureContext(certs); - } catch(e) { + } catch(e) { return require('crypto').createCredentials(certs).context; } } @@ -99,6 +100,7 @@ module.exports.create = function (securePort, certsPath, vhostsdir) { } function loadDomainMounts(domaininfo) { + var connectContext = {}; var appContext; // should order and group by longest domain, then longest path @@ -119,85 +121,103 @@ module.exports.create = function (securePort, certsPath, vhostsdir) { console.log('[log] [once] Preparing mount for', domaininfo.hostname + '/' + domaininfo.dirpathname); domainMergeMap[domaininfo.hostname].mountsMap['/' + domaininfo.dirpathname] = function (req, res, next) { - if (appContext) { - appContext(req, res, next); + function nextify() { + if (appContext) { + appContext(req, res, next); + return; + } + + console.log('[log] LOADING "' + domaininfo.hostname + '/' + domaininfo.pathname + '"', req.url); + getAppContext(domaininfo).then(function (localApp) { + //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(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(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(/^\//, '')) + + "'" + + "
" + + "
" + + "Domain: " + encodeURI(domaininfo.hostname) + + "
" + + "App: " + encodeURI(domaininfo.pathname) + + "
" + + "Route : " + encodeURI(req.url) + + "" + + "" + ); + /* + res.end('{ "error": { "messages": "Route matched ' + + domaininfo.hostname + '/' + domaininfo.pathname + + ', but was not handled. Forcing hard stop to prevent fallthru." } }'); + */ + })); + })); + } + try { + domainMergeMap[domaininfo.hostname].apps.use('/' + domaininfo.pathname, localAppWrapped); + console.info('Loaded ' + domaininfo.hostname + ':' + securePort + '/' + domaininfo.pathname); + appContext = localAppWrapped; + appContext(req, res, next); + } catch(e) { + console.error('[ERROR] ' + + domaininfo.hostname + ':' + securePort + + '/' + domaininfo.pathname + ); + console.error(e); + // TODO this may not work in web apps (due to 500), probably okay + res.writeHead(500); + res.end('{ "error": { "message": "[ERROR] could not load ' + + encodeURI(domaininfo.hostname) + ':' + securePort + '/' + encodeURI(domaininfo.pathname) + + 'or default error app." } }'); + } + }); + } + + if (!serveStatic) { + serveStatic = require('serve-static'); + } + + if (!connectContext.static) { + console.log('[static]', path.join(vhostsdir, domaininfo.dirname, 'public')); + connectContext.static = serveStatic(path.join(vhostsdir, domaininfo.dirname, 'public')); + } + + if (/^\/api\//.test(req.url)) { + nextify(); return; } - console.log('[log] LOADING "' + domaininfo.hostname + '/' + domaininfo.pathname + '"', req.url); - getAppContext(domaininfo).then(function (localApp) { - //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(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(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(/^\//, '')) - + "'" - + "
" - + "
" - + "Domain: " + encodeURI(domaininfo.hostname) - + "
" - + "App: " + encodeURI(domaininfo.pathname) - + "
" - + "Route : " + encodeURI(req.url) - + "" - + "" - ); - /* - res.end('{ "error": { "messages": "Route matched ' - + domaininfo.hostname + '/' + domaininfo.pathname - + ', but was not handled. Forcing hard stop to prevent fallthru." } }'); - */ - })); - })); - } - try { - domainMergeMap[domaininfo.hostname].apps.use('/' + domaininfo.pathname, localAppWrapped); - console.info('Loaded ' + domaininfo.hostname + ':' + securePort + '/' + domaininfo.pathname); - appContext = localAppWrapped; - appContext(req, res, next); - } catch(e) { - console.error('[ERROR] ' - + domaininfo.hostname + ':' + securePort - + '/' + domaininfo.pathname - ); - console.error(e); - // TODO this may not work in web apps (due to 500), probably okay - res.writeHead(500); - res.end('{ "error": { "message": "[ERROR] could not load ' - + encodeURI(domaininfo.hostname) + ':' + securePort + '/' + encodeURI(domaininfo.pathname) - + 'or default error app." } }'); - } - }); + connectContext.static(req, res, nextify); }; domainMergeMap[domaininfo.hostname].apps.use( '/' + domaininfo.pathname - , domainMergeMap[domaininfo.hostname].mountsMap['/' + domaininfo.dirpathname] + , domainMergeMap[domaininfo.hostname].mountsMap['/' + domaininfo.dirpathname] ); return PromiseA.resolve(); @@ -302,7 +322,7 @@ module.exports.create = function (securePort, certsPath, vhostsdir) { } return forEachAsync(domainMergeMap[vhost].apps, function (fn) { - return new PromiseA(function (resolve) { + return new PromiseA(function (resolve, reject) { function next(err) { if (err) { reject(err); @@ -341,7 +361,7 @@ module.exports.create = function (securePort, certsPath, vhostsdir) { return; }); }); - }; + } function loadCerts(domainname) { // TODO make async @@ -349,15 +369,16 @@ module.exports.create = function (securePort, certsPath, vhostsdir) { // Also, once we load Let's Encrypt, it's lights out for v0.10 var certsPath = path.join(vhostsdir, domainname, 'certs'); + var secOpts; 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]; - var secOpts = { + secOpts = { key: fs.readFileSync(path.join(certsPath, 'server', keyNode)) , cert: fs.readFileSync(path.join(certsPath, 'server', crtNode)) - } + }; if (fs.existsSync(path.join(certsPath, 'ca'))) { secOpts.ca = fs.readdirSync(path.join(certsPath, 'ca')).filter(function (node) { @@ -419,7 +440,8 @@ module.exports.create = function (securePort, certsPath, vhostsdir) { if (!secureContexts[domainname]) { console.log('[log] Loading certs for', domainname); - secureContexts[domainname] = loadCerts(domainname); + // TODO keep trying to find the cert in case it's uploaded late? + secureContexts[domainname] = loadCerts(domainname) || secureContexts.dummy; } // workaround for v0.12 / v1.2 backwards compat bug @@ -472,4 +494,4 @@ module.exports.create = function (securePort, certsPath, vhostsdir) { } return runServer(); -} +};