| 
									
										
										
										
											2015-02-18 23:06:56 -07:00
										 |  |  | 'use strict'; | 
					
						
							| 
									
										
										
										
											2015-03-06 03:04:51 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-02-18 23:06:56 -07:00
										 |  |  | module.exports.create = function (securePort, certsPath, vhostsdir) { | 
					
						
							| 
									
										
										
										
											2015-02-20 21:21:37 +00:00
										 |  |  |   var PromiseA = require('bluebird').Promise; | 
					
						
							| 
									
										
										
										
											2015-03-06 03:04:51 +00:00
										 |  |  |   var serveStatic; | 
					
						
							| 
									
										
										
										
											2015-02-20 21:21:37 +00:00
										 |  |  |   var https = require('https'); | 
					
						
							|  |  |  |   var fs = require('fs'); | 
					
						
							|  |  |  |   var path = require('path'); | 
					
						
							|  |  |  |   var dummyCerts; | 
					
						
							| 
									
										
										
										
											2015-02-23 20:35:32 +00:00
										 |  |  |   var serveFavicon; | 
					
						
							| 
									
										
										
										
											2015-02-20 21:21:37 +00:00
										 |  |  |   var secureContexts  = {}; | 
					
						
							| 
									
										
										
										
											2015-02-19 22:08:33 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-02-20 21:21:37 +00:00
										 |  |  |   function loadDummyCerts() { | 
					
						
							|  |  |  |     if (dummyCerts) { | 
					
						
							|  |  |  |       return dummyCerts; | 
					
						
							| 
									
										
										
										
											2015-02-19 22:08:33 +00:00
										 |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-02-20 21:21:37 +00:00
										 |  |  |     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)); | 
					
						
							|  |  |  |                     }) | 
					
						
							| 
									
										
										
										
											2015-02-19 22:08:33 +00:00
										 |  |  |     }; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-02-20 21:21:37 +00:00
										 |  |  |     return dummyCerts; | 
					
						
							| 
									
										
										
										
											2015-02-19 22:08:33 +00:00
										 |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-03-06 03:04:51 +00:00
										 |  |  |   function handleAppScopedError(domaininfo, req, res, fn) { | 
					
						
							| 
									
										
										
										
											2015-03-02 22:42:14 +00:00
										 |  |  |     function next(err) { | 
					
						
							|  |  |  |       if (!err) { | 
					
						
							|  |  |  |         fn(req, res); | 
					
						
							|  |  |  |         return; | 
					
						
							|  |  |  |       } | 
					
						
							| 
									
										
										
										
											2015-03-02 21:45:16 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-03-02 22:42:14 +00:00
										 |  |  |       console.error(err); | 
					
						
							|  |  |  |       res.writeHead(500); | 
					
						
							|  |  |  |       res.end( | 
					
						
							|  |  |  |           "<html>" | 
					
						
							|  |  |  |         + "<head>" | 
					
						
							|  |  |  |         + '<link rel="icon" href="favicon.ico" />' | 
					
						
							|  |  |  |         + "</head>" | 
					
						
							|  |  |  |         + "<body>" | 
					
						
							|  |  |  |         + "<pre>" | 
					
						
							|  |  |  |         + "<code>" | 
					
						
							| 
									
										
										
										
											2015-03-05 22:07:45 +00:00
										 |  |  |         + "Method: " + encodeURI(req.method) | 
					
						
							| 
									
										
										
										
											2015-03-02 22:42:14 +00:00
										 |  |  |         + '\n' | 
					
						
							| 
									
										
										
										
											2015-03-05 22:07:45 +00:00
										 |  |  |         + "Hostname: " + encodeURI(domaininfo.hostname) | 
					
						
							| 
									
										
										
										
											2015-03-02 22:42:14 +00:00
										 |  |  |         + '\n' | 
					
						
							| 
									
										
										
										
											2015-03-05 22:07:45 +00:00
										 |  |  |         + "App: " + encodeURI(domaininfo.pathname ? (domaininfo.pathname + '/') : '') | 
					
						
							| 
									
										
										
										
											2015-03-02 22:42:14 +00:00
										 |  |  |         + '\n' | 
					
						
							| 
									
										
										
										
											2015-03-05 22:07:45 +00:00
										 |  |  |         + "Route: " + encodeURI(req.url)//.replace(/^\//, '')
 | 
					
						
							| 
									
										
										
										
											2015-03-02 22:42:14 +00:00
										 |  |  |         + '\n' | 
					
						
							|  |  |  |           // TODO better sanatization
 | 
					
						
							|  |  |  |         + 'Error: '  + (err.message || err.toString()).replace(/</g, '<') | 
					
						
							|  |  |  |         + "</code>" | 
					
						
							|  |  |  |         + "</pre>" | 
					
						
							|  |  |  |         + "</body>" | 
					
						
							|  |  |  |         + "</html>" | 
					
						
							|  |  |  |       ); | 
					
						
							| 
									
										
										
										
											2015-03-02 21:45:16 +00:00
										 |  |  |     } | 
					
						
							| 
									
										
										
										
											2015-03-02 22:42:14 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  |     return next; | 
					
						
							| 
									
										
										
										
											2015-03-02 21:45:16 +00:00
										 |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-02-18 23:06:56 -07:00
										 |  |  |   function createSecureContext(certs) { | 
					
						
							|  |  |  |     // workaround for v0.12 / v1.2 backwards compat
 | 
					
						
							|  |  |  |     try { | 
					
						
							|  |  |  |       return require('tls').createSecureContext(certs); | 
					
						
							| 
									
										
										
										
											2015-03-06 03:04:51 +00:00
										 |  |  |     } catch(e) { | 
					
						
							| 
									
										
										
										
											2015-02-18 23:06:56 -07:00
										 |  |  |       return require('crypto').createCredentials(certs).context; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   } | 
					
						
							| 
									
										
										
										
											2015-02-19 20:51:17 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-02-20 21:21:37 +00:00
										 |  |  |   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'); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       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 | 
					
						
							|  |  |  |         }; | 
					
						
							|  |  |  |       } | 
					
						
							| 
									
										
										
										
											2015-02-18 23:06:56 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-02-20 21:21:37 +00:00
										 |  |  |       function loadDomainMounts(domaininfo) { | 
					
						
							| 
									
										
										
										
											2015-03-06 03:04:51 +00:00
										 |  |  |         var connectContext = {}; | 
					
						
							| 
									
										
										
										
											2015-02-20 21:21:37 +00:00
										 |  |  |         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]); | 
					
						
							|  |  |  |         } | 
					
						
							| 
									
										
										
										
											2015-02-18 23:06:56 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-02-20 21:21:37 +00:00
										 |  |  |         if (domainMergeMap[domaininfo.hostname].mountsMap['/' + domaininfo.dirpathname]) { | 
					
						
							|  |  |  |           return; | 
					
						
							| 
									
										
										
										
											2015-02-18 23:06:56 -07:00
										 |  |  |         } | 
					
						
							| 
									
										
										
										
											2015-02-20 21:21:37 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-02-21 18:00:39 +00:00
										 |  |  |         console.log('[log] [once] Preparing mount for', domaininfo.hostname + '/' + domaininfo.dirpathname); | 
					
						
							| 
									
										
										
										
											2015-02-20 21:21:37 +00:00
										 |  |  |         domainMergeMap[domaininfo.hostname].mountsMap['/' + domaininfo.dirpathname] = function (req, res, next) { | 
					
						
							| 
									
										
										
										
											2015-03-06 03:04:51 +00:00
										 |  |  |           function nextify() { | 
					
						
							|  |  |  |             if (appContext) { | 
					
						
							|  |  |  |               appContext(req, res, next); | 
					
						
							|  |  |  |               return; | 
					
						
							| 
									
										
										
										
											2015-02-21 04:43:10 +00:00
										 |  |  |             } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-03-06 03:04:51 +00:00
										 |  |  |             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); | 
					
						
							|  |  |  |               } | 
					
						
							| 
									
										
										
										
											2015-02-23 20:35:32 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-03-06 03:04:51 +00:00
										 |  |  |               // 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( | 
					
						
							|  |  |  |                         "<html>" | 
					
						
							|  |  |  |                       + "<head>" | 
					
						
							|  |  |  |                       + '<link rel="icon" href="favicon.ico" />' | 
					
						
							|  |  |  |                       + "</head>" | 
					
						
							|  |  |  |                       + "<body>" | 
					
						
							|  |  |  |                       + "Cannot " | 
					
						
							|  |  |  |                       + encodeURI(req.method) | 
					
						
							|  |  |  |                       + " 'https://" | 
					
						
							|  |  |  |                       + encodeURI(domaininfo.hostname) | 
					
						
							|  |  |  |                       + '/' | 
					
						
							|  |  |  |                       + encodeURI(domaininfo.pathname ? (domaininfo.pathname + '/') : '') | 
					
						
							|  |  |  |                       + encodeURI(req.url.replace(/^\//, '')) | 
					
						
							|  |  |  |                       + "'" | 
					
						
							|  |  |  |                       + "<br/>" | 
					
						
							|  |  |  |                       + "<br/>" | 
					
						
							|  |  |  |                       + "Domain: " + encodeURI(domaininfo.hostname) | 
					
						
							|  |  |  |                       + "<br/>" | 
					
						
							|  |  |  |                       + "App: " + encodeURI(domaininfo.pathname) | 
					
						
							|  |  |  |                       + "<br/>" | 
					
						
							|  |  |  |                       + "Route : " + encodeURI(req.url) | 
					
						
							|  |  |  |                       + "</body>" | 
					
						
							|  |  |  |                       + "</html>" | 
					
						
							|  |  |  |                     ); | 
					
						
							|  |  |  |                     /* | 
					
						
							|  |  |  |                     res.end('{ "error": { "messages": "Route matched ' | 
					
						
							|  |  |  |                       + domaininfo.hostname + '/' + domaininfo.pathname | 
					
						
							|  |  |  |                       + ', but was not handled. Forcing hard stop to prevent fallthru." } }'); | 
					
						
							|  |  |  |                     */ | 
					
						
							|  |  |  |                   })); | 
					
						
							| 
									
										
										
										
											2015-03-02 21:45:16 +00:00
										 |  |  |                 })); | 
					
						
							| 
									
										
										
										
											2015-03-06 03:04:51 +00:00
										 |  |  |               } | 
					
						
							|  |  |  |               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; | 
					
						
							|  |  |  |           } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |           connectContext.static(req, res, nextify); | 
					
						
							| 
									
										
										
										
											2015-02-20 21:21:37 +00:00
										 |  |  |         }; | 
					
						
							|  |  |  |         domainMergeMap[domaininfo.hostname].apps.use( | 
					
						
							|  |  |  |           '/' + domaininfo.pathname | 
					
						
							| 
									
										
										
										
											2015-03-06 03:04:51 +00:00
										 |  |  |         , domainMergeMap[domaininfo.hostname].mountsMap['/' + domaininfo.dirpathname] | 
					
						
							| 
									
										
										
										
											2015-02-20 21:21:37 +00:00
										 |  |  |         ); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         return PromiseA.resolve(); | 
					
						
							| 
									
										
										
										
											2015-02-18 23:06:56 -07:00
										 |  |  |       } | 
					
						
							| 
									
										
										
										
											2015-02-20 21:21:37 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  |       function readNewVhosts() { | 
					
						
							|  |  |  |         return fs.readdirSync(vhostsdir).filter(function (node) { | 
					
						
							|  |  |  |           // not a hidden or private file
 | 
					
						
							|  |  |  |           return '.' !== node[0] && '_' !== node[0]; | 
					
						
							|  |  |  |         }).map(getDomainInfo).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; | 
					
						
							| 
									
										
										
										
											2015-02-18 23:06:56 -07:00
										 |  |  |         }); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-02-20 21:21:37 +00:00
										 |  |  |       function getDummyAppContext(err, msg) { | 
					
						
							|  |  |  |         console.error('[ERROR] getDummyAppContext'); | 
					
						
							|  |  |  |         console.error(err); | 
					
						
							|  |  |  |         console.error(msg); | 
					
						
							|  |  |  |         return function (req, res) { | 
					
						
							| 
									
										
										
										
											2015-03-05 22:07:45 +00:00
										 |  |  |           res.writeHead(500); | 
					
						
							| 
									
										
										
										
											2015-02-20 21:21:37 +00:00
										 |  |  |           res.end('{ "error": { "message": "' + msg + '" } }'); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |       } | 
					
						
							| 
									
										
										
										
											2015-02-18 23:06:56 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-02-20 21:21:37 +00:00
										 |  |  |       function getAppContext(domaininfo) { | 
					
						
							|  |  |  |         var localApp; | 
					
						
							| 
									
										
										
										
											2015-02-18 23:06:56 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-02-20 21:21:37 +00:00
										 |  |  |         try { | 
					
						
							|  |  |  |           // TODO live reload required modules
 | 
					
						
							|  |  |  |           localApp = require(path.join(vhostsdir, domaininfo.dirname, 'app.js')); | 
					
						
							|  |  |  |           if (localApp.create) { | 
					
						
							|  |  |  |             // TODO read local config.yml and pass it in
 | 
					
						
							|  |  |  |             // TODO pass in websocket
 | 
					
						
							| 
									
										
										
										
											2015-02-21 01:16:38 +00:00
										 |  |  |             localApp = localApp.create(secureServer, { | 
					
						
							|  |  |  |               dummyCerts: dummyCerts | 
					
						
							|  |  |  |             , hostname: domaininfo.hostname | 
					
						
							|  |  |  |             , port: securePort | 
					
						
							|  |  |  |             , url: domaininfo.pathname | 
					
						
							|  |  |  |             }); | 
					
						
							| 
									
										
										
										
											2015-02-21 04:43:10 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-02-20 21:21:37 +00:00
										 |  |  |             if (!localApp) { | 
					
						
							| 
									
										
										
										
											2015-02-21 04:43:10 +00:00
										 |  |  |               localApp = getDummyAppContext(null, "[ERROR] no app was returned by app.js for " + domaininfo.dirname); | 
					
						
							| 
									
										
										
										
											2015-02-20 21:21:37 +00:00
										 |  |  |             } | 
					
						
							|  |  |  |           } | 
					
						
							| 
									
										
										
										
											2015-02-21 04:43:10 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-02-20 21:21:37 +00:00
										 |  |  |           if (!localApp.then) { | 
					
						
							|  |  |  |             localApp = PromiseA.resolve(localApp); | 
					
						
							|  |  |  |           } else { | 
					
						
							|  |  |  |             return localApp.catch(function (e) { | 
					
						
							|  |  |  |               return getDummyAppContext(e, "[ERROR] initialization failed during create() for " + domaininfo.dirname); | 
					
						
							|  |  |  |             }); | 
					
						
							|  |  |  |           } | 
					
						
							|  |  |  |         } catch(e) { | 
					
						
							|  |  |  |           localApp = getDummyAppContext(e, "[ERROR] could not load app.js for " + domaininfo.dirname); | 
					
						
							|  |  |  |           localApp = PromiseA.resolve(localApp); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         return localApp; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       function loadDomainVhosts() { | 
					
						
							|  |  |  |         domainMerged.forEach(function (domainApp) { | 
					
						
							| 
									
										
										
										
											2015-02-21 01:16:38 +00:00
										 |  |  |           if (domainApp._loaded) { | 
					
						
							|  |  |  |             return; | 
					
						
							|  |  |  |           } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-02-21 18:00:39 +00:00
										 |  |  |           console.log('[log] [once] Loading all mounts for ' + domainApp.hostname); | 
					
						
							| 
									
										
										
										
											2015-02-21 01:16:38 +00:00
										 |  |  |           domainApp._loaded = true; | 
					
						
							| 
									
										
										
										
											2015-02-20 21:21:37 +00:00
										 |  |  |           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
 | 
					
						
							| 
									
										
										
										
											2015-02-21 01:16:38 +00:00
										 |  |  |         console.log('[log] vhost:', vhost); | 
					
						
							| 
									
										
										
										
											2015-02-20 21:21:37 +00:00
										 |  |  |         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) { | 
					
						
							| 
									
										
										
										
											2015-03-06 03:04:51 +00:00
										 |  |  |             return new PromiseA(function (resolve, reject) { | 
					
						
							| 
									
										
										
										
											2015-02-21 01:16:38 +00:00
										 |  |  |               function next(err) { | 
					
						
							|  |  |  |                 if (err) { | 
					
						
							|  |  |  |                   reject(err); | 
					
						
							|  |  |  |                 } | 
					
						
							|  |  |  |                 resolve(); | 
					
						
							|  |  |  |               } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-02-20 21:21:37 +00:00
										 |  |  |               try { | 
					
						
							| 
									
										
										
										
											2015-02-21 01:16:38 +00:00
										 |  |  |                 fn(req, res, next); | 
					
						
							| 
									
										
										
										
											2015-02-20 21:21:37 +00:00
										 |  |  |               } 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 () { | 
					
						
							| 
									
										
										
										
											2015-02-21 01:16:38 +00:00
										 |  |  |         console.log('[log] TODO fix and use hotload'); | 
					
						
							|  |  |  |         //app.use(hotloadApp);
 | 
					
						
							| 
									
										
										
										
											2015-02-20 21:21:37 +00:00
										 |  |  |         resolve(app); | 
					
						
							|  |  |  |         return; | 
					
						
							|  |  |  |       }); | 
					
						
							|  |  |  |     }); | 
					
						
							| 
									
										
										
										
											2015-03-06 03:04:51 +00:00
										 |  |  |   } | 
					
						
							| 
									
										
										
										
											2015-02-18 23:06:56 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  |   function loadCerts(domainname) { | 
					
						
							|  |  |  |     // TODO make async
 | 
					
						
							|  |  |  |     // WARNING: This must be SYNC until we KNOW we're not going to be running on v0.10
 | 
					
						
							|  |  |  |     // Also, once we load Let's Encrypt, it's lights out for v0.10
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     var certsPath = path.join(vhostsdir, domainname, 'certs'); | 
					
						
							| 
									
										
										
										
											2015-03-06 03:04:51 +00:00
										 |  |  |     var secOpts; | 
					
						
							| 
									
										
										
										
											2015-02-18 23:06:56 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  |     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]; | 
					
						
							| 
									
										
										
										
											2015-03-06 03:04:51 +00:00
										 |  |  |       secOpts = { | 
					
						
							| 
									
										
										
										
											2015-02-18 23:06:56 -07:00
										 |  |  |         key:  fs.readFileSync(path.join(certsPath, 'server', keyNode)) | 
					
						
							|  |  |  |       , cert: fs.readFileSync(path.join(certsPath, 'server', crtNode)) | 
					
						
							| 
									
										
										
										
											2015-03-06 03:04:51 +00:00
										 |  |  |       }; | 
					
						
							| 
									
										
										
										
											2015-02-18 23:06:56 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  |       if (fs.existsSync(path.join(certsPath, 'ca'))) { | 
					
						
							|  |  |  |         secOpts.ca = fs.readdirSync(path.join(certsPath, 'ca')).filter(function (node) { | 
					
						
							|  |  |  |           console.log('[log ca]', node); | 
					
						
							|  |  |  |           return /crt\.pem$/.test(node); | 
					
						
							|  |  |  |         }).map(function (node) { | 
					
						
							|  |  |  |           return fs.readFileSync(path.join(certsPath, 'ca', node)); | 
					
						
							|  |  |  |         }); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |     } catch(err) { | 
					
						
							|  |  |  |       // TODO Let's Encrypt / ACME HTTPS
 | 
					
						
							|  |  |  |       console.error("[ERROR] Couldn't READ HTTPS certs from '" + certsPath + "':"); | 
					
						
							|  |  |  |       // this will be a simple file-read error
 | 
					
						
							|  |  |  |       console.error(err.message); | 
					
						
							|  |  |  |       return null; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     try { | 
					
						
							| 
									
										
										
										
											2015-02-21 01:16:38 +00:00
										 |  |  |       secureContexts[domainname] = createSecureContext(secOpts); | 
					
						
							| 
									
										
										
										
											2015-02-18 23:06:56 -07:00
										 |  |  |     } catch(err) { | 
					
						
							|  |  |  |       console.error("[ERROR] Certificates in '" + certsPath + "' could not be used:"); | 
					
						
							|  |  |  |       console.error(err); | 
					
						
							|  |  |  |       return null; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-02-21 01:16:38 +00:00
										 |  |  |     if (!secureContexts[domainname]) { | 
					
						
							|  |  |  |       console.error("[ERROR] Sanity check fail, no cert for '" + domainname + "'"); | 
					
						
							|  |  |  |       return null; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-02-18 23:06:56 -07:00
										 |  |  |     return secureContexts[domainname]; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-02-20 21:21:37 +00:00
										 |  |  |   function createSecureServer() { | 
					
						
							| 
									
										
										
										
											2015-02-21 01:16:38 +00:00
										 |  |  |     var localDummyCerts = loadDummyCerts(); | 
					
						
							| 
									
										
										
										
											2015-02-20 21:21:37 +00:00
										 |  |  |     var secureOpts = { | 
					
						
							| 
									
										
										
										
											2015-02-18 23:06:56 -07:00
										 |  |  |                     // fallback / default dummy certs
 | 
					
						
							| 
									
										
										
										
											2015-02-21 01:16:38 +00:00
										 |  |  |       key:          localDummyCerts.key | 
					
						
							|  |  |  |     , cert:         localDummyCerts.cert | 
					
						
							|  |  |  |     , ca:           localDummyCerts.ca | 
					
						
							| 
									
										
										
										
											2015-03-05 22:07:45 +00:00
										 |  |  |                     // changes from default: disallow RC4
 | 
					
						
							|  |  |  |     , ciphers:      "ECDHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA256:AES128-GCM-SHA256:!RC4:HIGH:!MD5:!aNULL" | 
					
						
							| 
									
										
										
										
											2015-02-18 23:06:56 -07:00
										 |  |  |     }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     function addSniWorkaroundCallback() { | 
					
						
							|  |  |  |       //SNICallback is passed the domain name, see NodeJS docs on TLS
 | 
					
						
							|  |  |  |       secureOpts.SNICallback = function (domainname, cb) { | 
					
						
							| 
									
										
										
										
											2015-02-22 10:09:18 +00:00
										 |  |  |         if (/(^|\.)proxyable\./.test(domainname)) { | 
					
						
							|  |  |  |           // device-id-12345678.proxyable.myapp.mydomain.com => myapp.mydomain.com
 | 
					
						
							|  |  |  |           // proxyable.myapp.mydomain.com => myapp.mydomain.com
 | 
					
						
							| 
									
										
										
										
											2015-02-21 18:05:26 +00:00
										 |  |  |           // TODO myapp.mydomain.com.proxyable.com => myapp.mydomain.com
 | 
					
						
							| 
									
										
										
										
											2015-02-22 10:09:18 +00:00
										 |  |  |           domainname = domainname.replace(/.*\.?proxyable\./, ''); | 
					
						
							| 
									
										
										
										
											2015-02-21 18:05:26 +00:00
										 |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-02-20 21:21:37 +00:00
										 |  |  |         if (!secureContexts.dummy) { | 
					
						
							| 
									
										
										
										
											2015-02-21 01:16:38 +00:00
										 |  |  |           console.log('[log] Loading dummy certs'); | 
					
						
							|  |  |  |           secureContexts.dummy = createSecureContext(localDummyCerts); | 
					
						
							| 
									
										
										
										
											2015-02-20 21:21:37 +00:00
										 |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-02-21 01:16:38 +00:00
										 |  |  |         if (!secureContexts[domainname]) { | 
					
						
							|  |  |  |           console.log('[log] Loading certs for', domainname); | 
					
						
							| 
									
										
										
										
											2015-03-06 03:04:51 +00:00
										 |  |  |           // TODO keep trying to find the cert in case it's uploaded late?
 | 
					
						
							|  |  |  |           secureContexts[domainname] = loadCerts(domainname) || secureContexts.dummy; | 
					
						
							| 
									
										
										
										
											2015-02-18 23:06:56 -07:00
										 |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         // workaround for v0.12 / v1.2 backwards compat bug
 | 
					
						
							|  |  |  |         if ('function' === typeof cb) { | 
					
						
							| 
									
										
										
										
											2015-02-21 01:16:38 +00:00
										 |  |  |           cb(null, secureContexts[domainname] || secureContexts.dummy); | 
					
						
							| 
									
										
										
										
											2015-02-18 23:06:56 -07:00
										 |  |  |         } else { | 
					
						
							| 
									
										
										
										
											2015-02-21 01:16:38 +00:00
										 |  |  |           return secureContexts[domainname] || secureContexts.dummy; | 
					
						
							| 
									
										
										
										
											2015-02-18 23:06:56 -07:00
										 |  |  |         } | 
					
						
							|  |  |  |       }; | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2015-02-20 21:21:37 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-02-18 23:06:56 -07:00
										 |  |  |     addSniWorkaroundCallback(); | 
					
						
							| 
									
										
										
										
											2015-02-20 21:21:37 +00:00
										 |  |  |     return https.createServer(secureOpts); | 
					
						
							|  |  |  |   } | 
					
						
							| 
									
										
										
										
											2015-02-18 23:06:56 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-02-20 21:21:37 +00:00
										 |  |  |   function runServer() { | 
					
						
							|  |  |  |     return new PromiseA(function (resolve) { | 
					
						
							|  |  |  |       var secureServer = createSecureServer(); | 
					
						
							|  |  |  |       var promiseApps; | 
					
						
							| 
									
										
										
										
											2015-02-18 23:06:56 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-02-20 21:21:37 +00:00
										 |  |  |       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) { | 
					
						
							| 
									
										
										
										
											2015-02-22 10:09:18 +00:00
										 |  |  |         if (/(^|\.)proxyable\./.test(req.headers.host)) { | 
					
						
							|  |  |  |           // device-id-12345678.proxyable.myapp.mydomain.com => myapp.mydomain.com
 | 
					
						
							|  |  |  |           // proxyable.myapp.mydomain.com => myapp.mydomain.com
 | 
					
						
							| 
									
										
										
										
											2015-02-21 01:16:38 +00:00
										 |  |  |           // TODO myapp.mydomain.com.proxyable.com => myapp.mydomain.com
 | 
					
						
							| 
									
										
										
										
											2015-02-22 10:09:18 +00:00
										 |  |  |           req.headers.host = req.headers.host.replace(/.*\.?proxyable\./, ''); | 
					
						
							| 
									
										
										
										
											2015-02-21 01:16:38 +00:00
										 |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-02-20 21:21:37 +00:00
										 |  |  |         loadPromise().then(function (app) { | 
					
						
							|  |  |  |           app(req, res); | 
					
						
							|  |  |  |         }); | 
					
						
							|  |  |  |       }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       return secureServer; | 
					
						
							|  |  |  |     }); | 
					
						
							| 
									
										
										
										
											2015-02-18 23:06:56 -07:00
										 |  |  |   } | 
					
						
							| 
									
										
										
										
											2015-02-20 21:21:37 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  |   return runServer(); | 
					
						
							| 
									
										
										
										
											2015-03-06 03:04:51 +00:00
										 |  |  | }; |