walnut.js/lib/main.js

229 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 = 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 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();
};