331 lines
11 KiB
JavaScript
331 lines
11 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) {
|
|
console.log('[notConfigured] req.hostname', req.hostname);
|
|
if (/\.html\b/.test(req.url)) {
|
|
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);
|
|
|
|
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('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('/', 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();
|
|
};
|