351 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			351 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
'use strict';
 | 
						|
 | 
						|
module.exports.create = function (app, xconfx, apiFactories, apiDeps, errorIfApi, errorIfAssets) {
 | 
						|
  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) {
 | 
						|
      console.log('[notConfigured] req.hostname', req.hostname);
 | 
						|
      if ('/' === req.url[req.url.length - 1] || /\.html\b/.test(req.url)) {
 | 
						|
        redirectSetup(req.hostname, req, res);
 | 
						|
        return;
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    if (!setupApp) {
 | 
						|
      //setupApp = express.static(path.join(xconfx.staticpath, 'walnut@daplie.com'));
 | 
						|
      setupApp = express.static(path.join(__dirname, 'walnut@daplie.com', 'setup'));
 | 
						|
    }
 | 
						|
    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);
 | 
						|
 | 
						|
      console.log('sitepath', sitepath);
 | 
						|
      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 = nodes.filter(function (pkgName) {
 | 
						|
        console.log('[all apps]', pkgName);
 | 
						|
        // load the apps that match this id's domain and could match the path
 | 
						|
        // domain.daplie.me matches domain.daplie.me
 | 
						|
        // daplie.me#path#to#thing matches daplie.me
 | 
						|
        // daplie.me does NOT match daplie.me#path#to#thing
 | 
						|
        var reqParts = appId.split('#');
 | 
						|
        var pkgParts = pkgName.split('#');
 | 
						|
        var reqDomain = reqParts.shift();
 | 
						|
        var pkgDomain = pkgParts.shift();
 | 
						|
        var reqPath = reqParts.join('#');
 | 
						|
        var pkgPath = pkgParts.join('#');
 | 
						|
        if (reqPath.length) {
 | 
						|
          reqPath += '#';
 | 
						|
        }
 | 
						|
        if (pkgPath.length) {
 | 
						|
          pkgPath += '#';
 | 
						|
        }
 | 
						|
        if (!(reqDomain === pkgDomain && 0 === reqPath.indexOf(pkgPath))) {
 | 
						|
          return false;
 | 
						|
        }
 | 
						|
        if (!localCache.statics[pkgName]) {
 | 
						|
          console.log('[load this app]', pkgName);
 | 
						|
          localCache.statics[pkgName] = { handler: loadSiteHandler(pkgName), createdAt: Date.now() };
 | 
						|
        }
 | 
						|
        return true;
 | 
						|
      });
 | 
						|
 | 
						|
      // Secure Matching
 | 
						|
      // apple.com#blah#  apple.com#blah#
 | 
						|
      // apple.com.us#    apple.com#foo#
 | 
						|
      // apple.com#       apple.com#foo#
 | 
						|
      console.log('[lib/main.js] nodes', nodes);
 | 
						|
      nodes.some(function (pkgName) {
 | 
						|
        console.log('pkgName, appId', pkgName, appId);
 | 
						|
        if (0 === (appId + '#').indexOf(pkgName + '#')) {
 | 
						|
          if (appId !== pkgName) {
 | 
						|
            localCache.statics[appId] = localCache.statics[pkgName];
 | 
						|
          }
 | 
						|
          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('/api', require('connect-send-error').error());
 | 
						|
  app.use('/assets', require('connect-send-error').error());
 | 
						|
  app.use('/', function (req, res, next) {
 | 
						|
    // If this doesn't look like an API or assets we can move along
 | 
						|
 | 
						|
    /*
 | 
						|
    console.log('.');
 | 
						|
    console.log('[main.js] req.url, req.hostname');
 | 
						|
    console.log(req.url);
 | 
						|
    console.log(req.hostname);
 | 
						|
    console.log('.');
 | 
						|
    */
 | 
						|
 | 
						|
    if (!/\/(api|assets)(\/|$)/.test(req.url)) {
 | 
						|
      //console.log('[main.js] api|assets');
 | 
						|
      next();
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    // keep https://assets.example.com/assets but skip https://example.com/assets
 | 
						|
    if (/\/assets(\/|$)/.test(req.url) && !/(^|\.)(api|assets)(\.)/.test(req.hostname) && !/^[0-9\.]+$/.test(req.hostname)) {
 | 
						|
      //console.log('[main.js] skip');
 | 
						|
      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);
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    apiApp(req, res, next);
 | 
						|
    return;
 | 
						|
  });
 | 
						|
  app.use('/', errorIfApi);
 | 
						|
  app.use('/', errorIfAssets);
 | 
						|
  app.use('/', serveStatic);
 | 
						|
  app.use('/', serveApps);
 | 
						|
 | 
						|
  return PromiseA.resolve();
 | 
						|
};
 |