forked from coolaj86/walnut.js
		
	
		
			
				
	
	
		
			228 lines
		
	
	
		
			6.7 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			228 lines
		
	
	
		
			6.7 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| 'use strict';
 | |
| 
 | |
| module.exports.create = function (app, xconfx, apiFactories, apiDeps) {
 | |
|   var PromiseA = require('bluebird');
 | |
|   var path = require('path');
 | |
|   var fs = PromiseA.promisifyAll(require('fs'));
 | |
|   // NOTE: each process has its own cache
 | |
|   var localCache = { le: {}, statics: {} };
 | |
|   var express = require('express');
 | |
|   var apiApp;
 | |
|   var setupDomain = xconfx.setupDomain = ('cloud.' + xconfx.primaryDomain);
 | |
|   var setupApp;
 | |
|   var CORS;
 | |
|   var cors;
 | |
| 
 | |
|   function redirectSetup(reason, req, res/*, next*/) {
 | |
|     console.log('xconfx', xconfx);
 | |
|     var url = 'https://cloud.' + xconfx.primaryDomain;
 | |
| 
 | |
|     if (443 !== xconfx.externalPort) {
 | |
|       url += ':' + xconfx.externalPort;
 | |
|     }
 | |
| 
 | |
|     url += '#referrer=' + reason;
 | |
| 
 | |
|     res.statusCode = 302;
 | |
|     res.setHeader('Location', url);
 | |
|     res.end();
 | |
|   }
 | |
| 
 | |
|   function disallowSymLinks(req, res) {
 | |
|     res.end(
 | |
|       "Symbolic Links are not supported on all platforms and are therefore disallowed."
 | |
|     + " Instead, simply create a file of the same name as the link with a single line of text"
 | |
|     + " which should be the relative or absolute path to the target directory."
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   function disallowNonFiles(req, res) {
 | |
|     res.end(
 | |
|       "Pipes, Blocks, Sockets, FIFOs, and other such nonsense are not permitted."
 | |
|     + " Instead please create a directory from which to read or create a file "
 | |
|     + " with a single line of text which should be the target directory to read from."
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   function securityError(req, res) {
 | |
|     res.end("Security Error: Link points outside of packages/pages");
 | |
|   }
 | |
| 
 | |
|   function notConfigured(req, res, next) {
 | |
|     if (setupDomain !== req.hostname) {
 | |
|       redirectSetup(req.hostname, req, res);
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     if (!setupApp) {
 | |
|       setupApp = express.static(path.join(xconfx.staticpath, 'com.daplie.walnut'));
 | |
|     }
 | |
|     setupApp(req, res, function () {
 | |
|       if ('/' === req.url) {
 | |
|         res.end('Sanity Fail: Configurator not found');
 | |
|         return;
 | |
|       }
 | |
|       next();
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   function loadHandler(name) {
 | |
|     return function handler(req, res, next) {
 | |
|       var packagepath = path.join(xconfx.staticpath, name);
 | |
| 
 | |
|       return fs.lstatAsync(packagepath).then(function (stat) {
 | |
|         if (stat.isSymbolicLink()) {
 | |
|           return disallowSymLinks;
 | |
|         }
 | |
| 
 | |
|         if (stat.isDirectory()) {
 | |
|           return express.static(packagepath);
 | |
|         }
 | |
| 
 | |
|         if (!stat.isFile()) {
 | |
|           return disallowNonFiles;
 | |
|         }
 | |
| 
 | |
|         return fs.readFileAsync(packagepath, 'utf8').then(function (text) {
 | |
|           // TODO allow cascading
 | |
|           text = text.trim().split(/\n/)[0];
 | |
| 
 | |
|           // TODO rerun the above, disallowing link-style (or count or memoize to prevent infinite loop)
 | |
|           // TODO make safe
 | |
|           packagepath = path.resolve(xconfx.staticpath, text);
 | |
|           if (0 !== packagepath.indexOf(xconfx.staticpath)) {
 | |
|             return securityError;
 | |
|           }
 | |
| 
 | |
|           return express.static(packagepath);
 | |
|         });
 | |
|       }, function (/*err*/) {
 | |
|         return notConfigured;
 | |
|       }).then(function (handler) {
 | |
| 
 | |
|         // keep object reference intact
 | |
|         localCache.statics[name].handler = handler;
 | |
|         handler(req, res, next);
 | |
|       });
 | |
|     };
 | |
|   }
 | |
| 
 | |
|   function staticHelper(appId, opts) {
 | |
|     // TODO inter-process cache expirey
 | |
|     // TODO add to xconfx.staticpath
 | |
|     xconfx.staticpath = path.join(__dirname, '..', '..', 'packages', 'pages');
 | |
|     return fs.readdirAsync(xconfx.staticpath).then(function (nodes) {
 | |
|       if (opts && opts.clear) {
 | |
|         localCache.statics = {};
 | |
|       }
 | |
| 
 | |
|       // longest to shortest
 | |
|       function shortToLong(a, b) {
 | |
|         return b.length - a.length;
 | |
|       }
 | |
|       nodes.sort(shortToLong);
 | |
| 
 | |
|       nodes.forEach(function (name) {
 | |
|         if (!localCache.statics[name]) {
 | |
|           localCache.statics[name] = { handler: loadHandler(name), createdAt: Date.now() };
 | |
|         }
 | |
|       });
 | |
| 
 | |
|       // Secure Matching
 | |
|       // apple.com#blah#  apple.com#blah#
 | |
|       // apple.com.us#    apple.com#foo#
 | |
|       // apple.com#       apple.com#foo#
 | |
|       nodes.some(function (name) {
 | |
|         if (0 === (name + '#').indexOf(appId + '#')) {
 | |
|           if (appId !== name) {
 | |
|             localCache.statics[appId] = localCache.statics[name];
 | |
|           }
 | |
|           return true;
 | |
|         }
 | |
|       });
 | |
| 
 | |
|       if (!localCache.statics[appId]) {
 | |
|         localCache.statics[appId] = { handler: notConfigured, createdAt: Date.now() };
 | |
|       }
 | |
| 
 | |
|       localCache.staticsKeys = Object.keys(localCache.statics).sort(shortToLong);
 | |
|       return localCache.statics[appId];
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   function serveStatic(req, res, next) {
 | |
|     // If we get this far we can be pretty confident that
 | |
|     // the domain was already set up because it's encrypted
 | |
|     var appId = req.hostname + req.url.replace(/\/+/g, '#').replace(/#$/, '');
 | |
|     var appIdParts = appId.split('#');
 | |
|     var appIdPart;
 | |
| 
 | |
|     // TODO configuration for allowing www
 | |
|     if (/^www\./.test(req.hostname)) {
 | |
|       // NOTE: acme responder and appcache unbricker must come before scrubTheDub
 | |
|       if (/\.(appcache|manifest)\b/.test(req.url)) {
 | |
|         require('./unbrick-appcache').unbrick(req, res);
 | |
|         return;
 | |
|       }
 | |
|       require('./no-www').scrubTheDub(req, res);
 | |
|       return;
 | |
|     }
 | |
|     /*
 | |
|     if (!redirectives && config.redirects) {
 | |
|       redirectives = require('./hostname-redirects').compile(config.redirects);
 | |
|     }
 | |
|     */
 | |
| 
 | |
|     // TODO assets.example.com/sub/assets/com.example.xyz/
 | |
|     if (/^api\./.test(req.hostname) && /\/api(\/|$)/.test(req.url)) {
 | |
|       // supports api.example.com/sub/app/api/com.example.xyz/
 | |
|       if (!apiApp) {
 | |
|         apiApp = require('./apis').create(xconfx, apiFactories, apiDeps);
 | |
|       }
 | |
| 
 | |
|       if (/^OPTIONS$/i.test(req.method)) {
 | |
|         if (!cors) {
 | |
|           CORS = require('connect-cors');
 | |
|           cors = CORS({ credentials: true, headers: [
 | |
|             'X-Requested-With'
 | |
|           , 'X-HTTP-Method-Override'
 | |
|           , 'Content-Type'
 | |
|           , 'Accept'
 | |
|           , 'Authorization'
 | |
|           ], methods: [ "GET", "POST", "PATCH", "PUT", "DELETE" ] });
 | |
|         }
 | |
|         cors(req, res, apiApp);
 | |
|       }
 | |
| 
 | |
|       apiApp(req, res, next);
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     while (appIdParts.length) {
 | |
|       // TODO needs IPC to expire cache
 | |
|       appIdPart = appIdParts.join('#');
 | |
|       if (localCache.statics[appIdPart]) {
 | |
|         break;
 | |
|       }
 | |
|       // TODO test via staticsKeys
 | |
| 
 | |
|       appIdParts.pop();
 | |
|     }
 | |
| 
 | |
|     if (!appIdPart || !localCache.statics[appIdPart]) {
 | |
|       return staticHelper(appId).then(function () {
 | |
|         localCache.statics[appId].handler(req, res, next);
 | |
|       });
 | |
|     }
 | |
| 
 | |
|     localCache.statics[appIdPart].handler(req, res, next);
 | |
|     if (Date.now() - localCache.statics[appIdPart].createdAt > (5 * 60 * 1000)) {
 | |
|       staticHelper(appId, { clear: true });
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   app.use('/', serveStatic);
 | |
| 
 | |
|   return PromiseA.resolve();
 | |
| };
 |