forked from coolaj86/walnut.js
		
	
		
			
				
	
	
		
			305 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			305 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| 'use strict';
 | |
| 
 | |
| module.exports.create = function (app, xconfx, apiFactories, apiDeps, errorIfApi) {
 | |
|   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 setupDomain = xconfx.setupDomain = ('cloud.' + xconfx.primaryDomain);
 | |
|   var apiApp;
 | |
|   var setupApp;
 | |
|   var CORS;
 | |
|   var cors;
 | |
| 
 | |
|   function redirectSetup(reason, req, res) {
 | |
|     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("The static pages for '" + reason + "' are not listed in '" + path.join(xconfx.sitespath, reason) + "'");
 | |
|   }
 | |
| 
 | |
|   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 = express.static(path.join('lib', 'com.daplie.walnut'));
 | |
|     }
 | |
|     setupApp(req, res, function () {
 | |
|       if ('/' === req.url) {
 | |
|         res.end('Sanity Fail: Configurator not found');
 | |
|         return;
 | |
|       }
 | |
|       next();
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   function loadSiteHandler(name) {
 | |
|     return function handler(req, res, next) {
 | |
|       // path.join('packages/pages', 'com.daplie.hello') // package name (used as file-link)
 | |
|       // path.join('packages/pages', 'domain.tld#hello') // dynamic exact url match
 | |
|       var sitepath = path.join(xconfx.sitespath, name);
 | |
| 
 | |
|       return fs.lstatAsync(sitepath).then(function (stat) {
 | |
|         if (stat.isSymbolicLink()) {
 | |
|           return disallowSymLinks;
 | |
|         }
 | |
| 
 | |
|         if (stat.isDirectory()) {
 | |
|           return express.static(sitepath);
 | |
|         }
 | |
| 
 | |
|         if (!stat.isFile()) {
 | |
|           return disallowNonFiles;
 | |
|         }
 | |
| 
 | |
|         // path.join('packages/pages', 'domain.tld#hello') // a file (not folder) which contains a list of roots
 | |
|         // may look like this:
 | |
|         //
 | |
|         //   com.daplie.hello
 | |
|         //   tld.domain.app
 | |
|         //
 | |
|         // this is basically a 'recursive mount' to signify that 'com.daplie.hello' should be tried first
 | |
|         // and if no file matches that 'tld.domain.app' may be tried next, and so on
 | |
|         //
 | |
|         // this may well become a .htaccess type of situation allowing for redirects and such
 | |
|         return fs.readFileAsync(sitepath, 'utf8').then(function (text) {
 | |
|           // TODO allow cascading multiple lines
 | |
|           text = text.trim().split(/\n/)[0];
 | |
| 
 | |
|           // TODO rerun the above, disallowing link-style (or count or memoize to prevent infinite loop)
 | |
|           // TODO make safe
 | |
|           var packagepath = path.resolve(xconfx.staticpath, text);
 | |
|           if (0 !== packagepath.indexOf(xconfx.staticpath)) {
 | |
|             return securityError;
 | |
|           }
 | |
| 
 | |
|           // instead of actually creating new instances of express.static
 | |
|           // this same effect could be managed by internally re-writing the url (and restoring it)
 | |
|           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) {
 | |
|     console.log('[staticHelper]', appId);
 | |
|     // TODO inter-process cache expirey
 | |
|     // TODO add to xconfx.staticpath
 | |
|     xconfx.staticpath = path.join(__dirname, '..', '..', 'packages', 'pages');
 | |
|     xconfx.sitespath = path.join(__dirname, '..', '..', 'packages', 'sites');
 | |
| 
 | |
|     // Reads in each of the sites directives as 'nodes'
 | |
|     return fs.readdirAsync(xconfx.sitespath).then(function (nodes) {
 | |
|       if (opts && opts.clear) {
 | |
|         localCache.statics = {};
 | |
|       }
 | |
| 
 | |
|       // Order from longest (index length - 1) to shortest (index 0)
 | |
|       function shortToLong(a, b) {
 | |
|         return b.length - a.length;
 | |
|       }
 | |
|       nodes.sort(shortToLong);
 | |
| 
 | |
|       nodes.forEach(function (name) {
 | |
|         console.log('[all apps]', name);
 | |
|         if (!localCache.statics[name]) {
 | |
|           console.log('[load this app]', name);
 | |
|           localCache.statics[name] = { handler: loadSiteHandler(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 serveStaticHelper(appId, opts, req, res, next) {
 | |
|     var appIdParts = appId.split('#');
 | |
|     var appIdPart;
 | |
| 
 | |
|     // TODO for <domain.tld>/<path>/apps/<package> the Uri should be <domain.tld>/<path>
 | |
|     res.setHeader('X-Walnut-Uri', appId.replace(/#/g, '/'));
 | |
| 
 | |
|     // 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 (/^assets\./.test(req.hostname) && /\/assets(\/|$)/.test(req.url)) {
 | |
|       ...
 | |
|     }
 | |
|     */
 | |
| 
 | |
|     // There may be some app folders named 'apple.com', 'apple.com#foo', and 'apple.com#foo#bar'
 | |
|     // Here we're sorting an appId broken into parts like [ 'apple.com', 'foo', 'bar' ]
 | |
|     // and wer're checking to see if this is perhaps '/' of 'apple.com/foo/bar' or '/foo/bar' of 'apple.com', etc
 | |
|     while (appIdParts.length) {
 | |
|       // TODO needs IPC to expire cache when an API says the app mounts have been updated
 | |
|       appIdPart = appIdParts.join('#');
 | |
|       if (localCache.statics[appIdPart]) {
 | |
|         break;
 | |
|       }
 | |
|       // TODO test via staticsKeys
 | |
| 
 | |
|       appIdParts.pop();
 | |
|     }
 | |
| 
 | |
|     if (!appIdPart || !localCache.statics[appIdPart]) {
 | |
|       console.log('[serveStaticHelper] appId', appId);
 | |
|       return staticHelper(appId).then(function (webapp) {
 | |
|         //localCache.statics[appId].handler(req, res, next);
 | |
|         webapp.handler(req, res, next);
 | |
|       });
 | |
|     }
 | |
| 
 | |
|     console.log('[serveStaticHelper] appIdPart', appIdPart);
 | |
|     if (opts && opts.rewrite && -1 !== req.url.indexOf(appIdPart.replace(/#/g, '/').replace(/\/$/, ''))) {
 | |
|       req.url = req.url.slice(req.url.indexOf(appIdPart.replace(/#/g, '/').replace(/\/$/, '')) + appIdPart.replace(/(\/|#)$/, '').length);
 | |
|       if (0 !== req.url.indexOf('/')) {
 | |
|         req.url = '/' + req.url;
 | |
|       }
 | |
|     }
 | |
|     localCache.statics[appIdPart].handler(req, res, next);
 | |
|     if (Date.now() - localCache.statics[appIdPart].createdAt > (5 * 60 * 1000)) {
 | |
|       staticHelper(appId, { clear: true });
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   function serveStatic(req, res, next) {
 | |
|     // We convert the URL that was sent in the browser bar from
 | |
|     // 'https://domain.tld/foo/bar' to 'domain.tld#foo#bar'
 | |
|     var appId = req.hostname + req.url.replace(/\/+/g, '#').replace(/#$/, '');
 | |
|     serveStaticHelper(appId, null, req, res, next);
 | |
|   }
 | |
| 
 | |
|   function serveApps(req, res, next) {
 | |
|     var appId = req.url.slice(1).replace(/\/+/g, '#').replace(/#$/, '');
 | |
| 
 | |
|     if (/^apps\./.test(req.hostname)) {
 | |
|       appId = appId.replace(/^apps#/, '');
 | |
|     } else if (/\bapps#/.test(appId)) {
 | |
|       appId = appId.replace(/.*\bapps#/, '');
 | |
|     } else {
 | |
|       next();
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     console.log('[serveApps] appId', appId);
 | |
|     serveStaticHelper(appId, { rewrite: true }, req, res, next);
 | |
|   }
 | |
| 
 | |
|   // TODO set header 'X-ExperienceId: domain.tld/sub/path'
 | |
|   // This would let an app know whether its app is 'domain.tld' with a path of '/sub/path'
 | |
|   // or if its app is 'domain.tld/sub' with a path of '/path'
 | |
| 
 | |
|   // TODO handle assets.example.com/sub/assets/com.example.xyz/
 | |
| 
 | |
|   app.use('/', function (req, res, next) {
 | |
|     // If this doesn't look like an API we can move along
 | |
|     if (!/\/api(\/|$)/.test(req.url)) {
 | |
|       // /^api\./.test(req.hostname) &&
 | |
|       next();
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     // 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;
 | |
|   });
 | |
|   app.use('/', errorIfApi);
 | |
|   app.use('/', serveStatic);
 | |
|   app.use('/', serveApps);
 | |
| 
 | |
|   return PromiseA.resolve();
 | |
| };
 |