can now vhost mounted subapps
This commit is contained in:
		
							parent
							
								
									b28a45581f
								
							
						
					
					
						commit
						e4a13a8b80
					
				| @ -1,48 +1,77 @@ | ||||
| 'use strict'; | ||||
|   | ||||
| var https           = require('https') | ||||
|   , http            = require('http') | ||||
|   , PromiseA        = require('bluebird').Promise | ||||
|   , forEachAsync    = require('foreachasync').forEachAsync.create(PromiseA) | ||||
|   , fs              = require('fs') | ||||
|   , path            = require('path') | ||||
|   , crypto          = require('crypto') | ||||
|   , connect         = require('connect') | ||||
|   , vhost           = require('vhost') | ||||
|   , escapeRe        = require('escape-string-regexp') | ||||
| var https           = require('https'); | ||||
| var http            = require('http'); | ||||
| var PromiseA        = require('bluebird').Promise; | ||||
| var forEachAsync    = require('foreachasync').forEachAsync.create(PromiseA); | ||||
| var fs              = require('fs'); | ||||
| var path            = require('path'); | ||||
| var crypto          = require('crypto'); | ||||
| var connect         = require('connect'); | ||||
| var vhost           = require('vhost'); | ||||
| var escapeRe        = require('escape-string-regexp'); | ||||
| 
 | ||||
|   // connect / express app
 | ||||
|   , app             = connect() | ||||
| var app             = connect(); | ||||
| 
 | ||||
|   // SSL Server
 | ||||
|   , secureContexts  = {} | ||||
|   , secureOpts | ||||
|   , secureServer | ||||
|   , securePort      = /*process.argv[2] ||*/ 443 | ||||
| var secureContexts  = {}; | ||||
| var secureOpts; | ||||
| var secureServer; | ||||
| var securePort      = process.argv[2] || 443; | ||||
| 
 | ||||
|   // force SSL upgrade server
 | ||||
|   , insecureServer | ||||
|   , insecurePort    = /*process.argv[3] ||*/ 80 | ||||
| var insecureServer; | ||||
| var insecurePort    = process.argv[3] || 80; | ||||
| 
 | ||||
|   // the ssl domains I have
 | ||||
|   // 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
 | ||||
|                         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') | ||||
|   .inject() | ||||
|   ; | ||||
| 
 | ||||
| function getAppContext(domain) { | ||||
|   var localApp | ||||
|     ; | ||||
| function getAppContext(domaininfo) { | ||||
|   var localApp; | ||||
| 
 | ||||
|   localApp = require(path.join(__dirname, 'vhosts', domain, 'app.js')); | ||||
|   localApp = require(path.join(__dirname, 'vhosts', domaininfo.dirname, 'app.js')); | ||||
|   if (localApp.create) { | ||||
|     // TODO read local config.yml and pass it in
 | ||||
|     // TODO pass in websocket
 | ||||
|     localApp = localApp.create(/*config*/); | ||||
|   } | ||||
|   if (!localApp.then) { | ||||
| @ -52,20 +81,44 @@ function getAppContext(domain) { | ||||
|   return localApp; | ||||
| } | ||||
| 
 | ||||
| forEachAsync(domains, function (domain) { | ||||
|   secureContexts[domain] = crypto.createCredentials({ | ||||
|     key:  fs.readFileSync(path.join(__dirname, 'vhosts', domain, 'certs/server/my-server.key.pem')) | ||||
|   , cert: fs.readFileSync(path.join(__dirname, 'vhosts', domain, 'certs/server/my-server.crt.pem')) | ||||
|   , ca:   fs.readdirSync(path.join(__dirname, 'vhosts', domain, 'certs/ca')).map(function (node) { | ||||
|             return fs.readFileSync(path.join(__dirname, 'vhosts', domain, 'certs/ca', node)); | ||||
|           }) | ||||
|   }).context; | ||||
| function loadDummyCerts() { | ||||
|   var certsPath = path.join(__dirname, 'certs'); | ||||
|   var certs = { | ||||
|     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')).map(function (node) { | ||||
|                     return fs.readFileSync(path.join(certsPath, 'ca', node)); | ||||
|                   }) | ||||
|   }; | ||||
|   secureContexts.dummy = crypto.createCredentials(certs).context; | ||||
|   secureContexts.dummy.certs = certs; | ||||
| } | ||||
| loadDummyCerts(); | ||||
| 
 | ||||
|   return getAppContext(domain).then(function (localApp) { | ||||
|     app.use(vhost('www.' + domain, localApp)); | ||||
|     app.use(vhost(domain, localApp)); | ||||
|   }); | ||||
| }).then(function () { | ||||
| 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; | ||||
|   } 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; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| forEachAsync(rootDomains, loadCerts).then(function () { | ||||
|   // fallback / default domain
 | ||||
|   /* | ||||
|   app.use('/', function (req, res) { | ||||
| @ -73,104 +126,125 @@ forEachAsync(domains, function (domain) { | ||||
|     res.end("<html><body><h1>Hello, World... This isn't the domain you're looking for.</h1></body></html>"); | ||||
|   }); | ||||
|   */ | ||||
| }); | ||||
| 
 | ||||
| //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 (domain) { | ||||
|                   //console.log('SNI:', domain);
 | ||||
|                   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); | ||||
| secureServer.on('request', app); | ||||
| secureServer.listen(securePort, function () { | ||||
|   console.log("Listening on https://localhost:" + secureServer.address().port); | ||||
| }); | ||||
| 
 | ||||
| // TODO localhost-only server shutdown mechanism
 | ||||
| // that closes all sockets, waits for them to finish,
 | ||||
| // and then hands control over completely to respawned server
 | ||||
| 
 | ||||
| //
 | ||||
| // Redirect HTTP ot HTTPS
 | ||||
| //
 | ||||
| // This simply redirects from the current insecure location to the encrypted location
 | ||||
| //
 | ||||
| insecureServer = http.createServer(); | ||||
| insecureServer.on('request', function (req, res) { | ||||
|   var insecureRedirects; | ||||
|   var host = req.headers.host || ''; | ||||
|   var url = req.url; | ||||
| 
 | ||||
|   // because I have domains for which I don't want to pay for SSL certs
 | ||||
|   insecureRedirects = require('./redirects.json').sort(function (a, b) { | ||||
|     var hlen = b.from.hostname.length - a.from.hostname.length; | ||||
|     var plen; | ||||
|     if (!hlen) { | ||||
|       plen = b.from.path.length - a.from.path.length; | ||||
|       return plen; | ||||
|   return forEachAsync(domains, function (domaininfo) { | ||||
|     // 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() }; | ||||
|       domainMerged.push(domainMergeMap[domaininfo.hostname]); | ||||
|     } | ||||
|     return hlen; | ||||
|   }).forEach(function (redirect) { | ||||
|     var origHost = host; | ||||
|     // TODO if '*' === hostname[0], omit '^'
 | ||||
|     host = host.replace( | ||||
|       new RegExp('^' + escapeRe(redirect.from.hostname)) | ||||
|     , redirect.to.hostname | ||||
|     ); | ||||
|     if (host === origHost) { | ||||
|       return; | ||||
|     } | ||||
|     url = url.replace( | ||||
|       new RegExp('^' + escapeRe(redirect.from.path)) | ||||
|     , redirect.to.path | ||||
|     ); | ||||
| 
 | ||||
|     return getAppContext(domaininfo).then(function (localApp) { | ||||
|       // Note: pathname should NEVER have a leading '/' on its own
 | ||||
|       // 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); | ||||
|   }); | ||||
| 
 | ||||
|   var newLocation = 'https://' | ||||
|     + host.replace(/:\d+/, ':' + securePort) + url | ||||
|     ; | ||||
|   // TODO localhost-only server shutdown mechanism
 | ||||
|   // that closes all sockets, waits for them to finish,
 | ||||
|   // and then hands control over completely to respawned server
 | ||||
| 
 | ||||
|   var metaRedirect = '' | ||||
|     + '<html>\n' | ||||
|     + '<head>\n' | ||||
|     + '  <style>* { background-color: white; color: white; text-decoration: none; }</style>\n' | ||||
|     + '  <META http-equiv="refresh" content="0;URL=' + newLocation + '">\n' | ||||
|     + '</head>\n' | ||||
|     + '<body style="display: none;">\n' | ||||
|     + '  <p>You requested an insecure resource. Please use this instead: \n' | ||||
|     + '    <a href="' + newLocation + '">' + newLocation + '</a></p>\n' | ||||
|     + '</body>\n' | ||||
|     + '</html>\n' | ||||
|     ; | ||||
| 
 | ||||
|   // DO NOT HTTP REDIRECT
 | ||||
|   /* | ||||
|   res.setHeader('Location', newLocation); | ||||
|   res.statusCode = 302; | ||||
|   */ | ||||
| 
 | ||||
|   // BAD NEWS BEARS
 | ||||
|   //
 | ||||
|   // When people are experimenting with the API and posting tutorials
 | ||||
|   // they'll use cURL and they'll forget to prefix with https://
 | ||||
|   // If we allow that, then many users will be sending private tokens
 | ||||
|   // and such with POSTs in clear text and, worse, it will work!
 | ||||
|   // To minimize this, we give browser users a mostly optimal experience,
 | ||||
|   // but people experimenting with the API get a message letting them know
 | ||||
|   // that they're doing it wrong and thus forces them to ensure they encrypt.
 | ||||
|   res.setHeader('Content-Type', 'text/html'); | ||||
|   res.end(metaRedirect); | ||||
| }); | ||||
| insecureServer.listen(insecurePort, function(){ | ||||
|   console.log("\nRedirecting all http traffic to https\n"); | ||||
| }); | ||||
|   // Redirect HTTP ot HTTPS
 | ||||
|   //
 | ||||
|   // This simply redirects from the current insecure location to the encrypted location
 | ||||
|   //
 | ||||
|   insecureServer = http.createServer(); | ||||
|   insecureServer.on('request', function (req, res) { | ||||
|     var insecureRedirects; | ||||
|     var host = req.headers.host || ''; | ||||
|     var url = req.url; | ||||
| 
 | ||||
|     // because I have domains for which I don't want to pay for SSL certs
 | ||||
|     insecureRedirects = require('./redirects.json').sort(function (a, b) { | ||||
|       var hlen = b.from.hostname.length - a.from.hostname.length; | ||||
|       var plen; | ||||
|       if (!hlen) { | ||||
|         plen = b.from.path.length - a.from.path.length; | ||||
|         return plen; | ||||
|       } | ||||
|       return hlen; | ||||
|     }).forEach(function (redirect) { | ||||
|       var origHost = host; | ||||
|       // TODO if '*' === hostname[0], omit '^'
 | ||||
|       host = host.replace( | ||||
|         new RegExp('^' + escapeRe(redirect.from.hostname)) | ||||
|       , redirect.to.hostname | ||||
|       ); | ||||
|       if (host === origHost) { | ||||
|         return; | ||||
|       } | ||||
|       url = url.replace( | ||||
|         new RegExp('^' + escapeRe(redirect.from.path)) | ||||
|       , redirect.to.path | ||||
|       ); | ||||
|     }); | ||||
| 
 | ||||
|     var newLocation = 'https://' | ||||
|       + host.replace(/:\d+/, ':' + securePort) + url | ||||
|       ; | ||||
| 
 | ||||
|     var metaRedirect = '' | ||||
|       + '<html>\n' | ||||
|       + '<head>\n' | ||||
|       + '  <style>* { background-color: white; color: white; text-decoration: none; }</style>\n' | ||||
|       + '  <META http-equiv="refresh" content="0;URL=' + newLocation + '">\n' | ||||
|       + '</head>\n' | ||||
|       + '<body style="display: none;">\n' | ||||
|       + '  <p>You requested an insecure resource. Please use this instead: \n' | ||||
|       + '    <a href="' + newLocation + '">' + newLocation + '</a></p>\n' | ||||
|       + '</body>\n' | ||||
|       + '</html>\n' | ||||
|       ; | ||||
| 
 | ||||
|     // DO NOT HTTP REDIRECT
 | ||||
|     /* | ||||
|     res.setHeader('Location', newLocation); | ||||
|     res.statusCode = 302; | ||||
|     */ | ||||
| 
 | ||||
|     // BAD NEWS BEARS
 | ||||
|     //
 | ||||
|     // When people are experimenting with the API and posting tutorials
 | ||||
|     // they'll use cURL and they'll forget to prefix with https://
 | ||||
|     // If we allow that, then many users will be sending private tokens
 | ||||
|     // and such with POSTs in clear text and, worse, it will work!
 | ||||
|     // To minimize this, we give browser users a mostly optimal experience,
 | ||||
|     // but people experimenting with the API get a message letting them know
 | ||||
|     // that they're doing it wrong and thus forces them to ensure they encrypt.
 | ||||
|     res.setHeader('Content-Type', 'text/html'); | ||||
|     res.end(metaRedirect); | ||||
|   }); | ||||
|   insecureServer.listen(insecurePort, function(){ | ||||
|     console.log("\nRedirecting all http traffic to https\n"); | ||||
|   }); | ||||
| } | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user