391 lines
11 KiB
JavaScript
391 lines
11 KiB
JavaScript
'use strict';
|
|
|
|
var escapeStringRegexp = require('escape-string-regexp');
|
|
var staticHandlers = {};
|
|
//var apiHandlers = {};
|
|
|
|
function compileVhosts(vhostsMap) {
|
|
var results = {
|
|
patterns: []
|
|
, conflictsMap: {}
|
|
, matchesMap: {}
|
|
};
|
|
|
|
// compli
|
|
Object.keys(vhostsMap).forEach(function (key) {
|
|
var vhost = vhostsMap[key];
|
|
var bare;
|
|
var www;
|
|
|
|
if ('.' === vhost.hostname[0]) {
|
|
// for consistency
|
|
// TODO this should happen at the database level
|
|
vhost.hostname = '*' + vhost.hostname;
|
|
}
|
|
|
|
if ('*' === vhost.hostname[0]) {
|
|
// TODO check that we are not trying to redirect a tld (.com, .co.uk, .org, etc)
|
|
// tlds should follow the global policy
|
|
if (vhost.hostname[1] && '.' !== vhost.hostname[1]) {
|
|
// this is not a good place to throw as the consequences of a bug would be
|
|
// very bad, but errors should never be silent, so we'll compromise
|
|
console.warn("[NON-FATAL ERROR]: ignoring pattern '" + vhost.hostname + "'");
|
|
results.conflictsMap[vhost.hostname] = vhost;
|
|
}
|
|
|
|
// nix the '*' for easier matching
|
|
vhost.hostname = vhost.hostname.slice(1);
|
|
// except the default
|
|
if (!vhost.hostname) {
|
|
vhost.hostname = '*';
|
|
}
|
|
if (results.conflictsMap[vhost.hostname]) {
|
|
console.warn("[NON-FATAL ERROR]: duplicate entry for pattern '" + vhost.hostname + "'");
|
|
}
|
|
|
|
results.conflictsMap[vhost.hostname] = vhost;
|
|
results.patterns.push(vhost);
|
|
return;
|
|
}
|
|
|
|
bare = vhost.hostname.replace(/^www\./i, '');
|
|
www = vhost.hostname.replace(/^(www\.)?/i, 'www.');
|
|
|
|
results.matchesMap[bare] = vhost;
|
|
results.matchesMap[www] = vhost;
|
|
});
|
|
|
|
results.patterns.sort(function (a, b) {
|
|
return b.id.length - a.id.length;
|
|
});
|
|
|
|
return results;
|
|
}
|
|
|
|
function loadPages(pkgConf, route, req, res, next) {
|
|
var PromiseA = require('bluebird');
|
|
var fs = require('fs');
|
|
var path = require('path');
|
|
var pkgpath = path.join(pkgConf.pagespath, (route.app.package || route.app.id), (route.app.version || ''));
|
|
|
|
// TODO special cases for /.well_known/ and similar (oauth3.html, oauth3.json, webfinger, etc)
|
|
|
|
function handlePromise(p) {
|
|
p.then(function (app) {
|
|
app(req, res, next);
|
|
route._app = app;
|
|
}, function (err) {
|
|
console.error('[App Promise Error]');
|
|
next(err);
|
|
});
|
|
}
|
|
|
|
if (staticHandlers[pkgpath]) {
|
|
route._app = staticHandlers[pkgpath];
|
|
route._app(req, res, next);
|
|
return;
|
|
}
|
|
|
|
if (!route._promise_app) {
|
|
route._promise_app = new PromiseA(function (resolve, reject) {
|
|
fs.exists(pkgpath, function (exists) {
|
|
if (!exists) {
|
|
reject(new Error("package is registered but does not exist"));
|
|
return;
|
|
}
|
|
|
|
//console.log('[static mount]', pkgpath);
|
|
resolve(require('serve-static')(pkgpath));
|
|
});
|
|
});
|
|
}
|
|
|
|
handlePromise(route._promise_app);
|
|
}
|
|
|
|
function getApi(pkgConf, pkgDeps, route) {
|
|
var PromiseA = require('bluebird');
|
|
var path = require('path');
|
|
var pkgpath = path.join(pkgConf.apipath, route.api.id/*, (route.api.version || '')*/);
|
|
|
|
// TODO needs some version stuff (which would also allow hot-loading of updates)
|
|
// TODO version could be tied to sha256sum
|
|
|
|
return new PromiseA(function (resolve, reject) {
|
|
var myApp;
|
|
var ursa;
|
|
var promise;
|
|
|
|
// TODO dynamic requires are a no-no
|
|
// can we statically generate a require-er? on each install?
|
|
// module.exports = { {{pkgpath}}: function () { return require({{pkgpath}}) } }
|
|
// requirer[pkgpath]()
|
|
myApp = pkgDeps.express();
|
|
myApp.disable('x-powered-by');
|
|
if (pkgDeps.app.get('trust proxy')) {
|
|
myApp.set('trust proxy', pkgDeps.app.get('trust proxy'));
|
|
}
|
|
if (!pkgConf.pubkey) {
|
|
/*
|
|
return ursa.createPrivateKey(pem, password, encoding);
|
|
var pem = myKey.toPrivatePem();
|
|
return jwt.verifyAsync(token, myKey.toPublicPem(), { ignoreExpiration: false && true }).then(function (decoded) {
|
|
});
|
|
*/
|
|
ursa = require('ursa');
|
|
pkgConf.keypair = ursa.createPrivateKey(pkgConf.privkey, 'ascii');
|
|
pkgConf.pubkey = ursa.createPublicKey(pkgConf.pubkey, 'ascii'); //conf.keypair.toPublicKey();
|
|
}
|
|
|
|
try {
|
|
route._apipkg = require(path.join(pkgpath, 'package.json'));
|
|
route._apiname = route._apipkg.name;
|
|
if (route._apipkg.walnut) {
|
|
pkgpath += '/' + route.apipkg.walnut;
|
|
}
|
|
promise = require(pkgpath).create(pkgConf, pkgDeps, myApp);
|
|
} catch(e) {
|
|
reject(e);
|
|
return;
|
|
}
|
|
|
|
promise.then(function () {
|
|
// TODO give pub/priv pair for app and all public keys
|
|
// route._api = require(pkgpath).create(pkgConf, pkgDeps, myApp);
|
|
route._api = require('express')();
|
|
route._api_app = myApp;
|
|
// TODO fix backwards compat
|
|
// /api/com.example.foo (no change)
|
|
route._api.use('/', route._api_app);
|
|
// /api/com.example.foo => /
|
|
route._api.use('/api/' + route.api.id, function (req, res, next) {
|
|
//console.log('api mangle 2:', '/api/' + route.api.id, req.url);
|
|
route._api_app(req, res, next);
|
|
});
|
|
// /api/com.example.foo => /api
|
|
route._api.use('/', function (req, res, next) {
|
|
req.url = '/api' + req.url.slice(('/api/' + route.api.id).length);
|
|
//console.log('api mangle 3:', req.url);
|
|
route._api_app(req, res, next);
|
|
});
|
|
resolve(route._api);
|
|
}, reject);
|
|
});
|
|
}
|
|
|
|
function loadApi(pkgConf, pkgDeps, route) {
|
|
function handlePromise(p) {
|
|
return p.then(function (api) {
|
|
route._api = api;
|
|
return api;
|
|
});
|
|
}
|
|
|
|
if (!route._promise_api) {
|
|
route._promise_api = getApi(pkgConf, pkgDeps, route);
|
|
}
|
|
|
|
return handlePromise(route._promise_api);
|
|
}
|
|
|
|
function layerItUp(pkgConf, router, req, res, next) {
|
|
var nexti = -1;
|
|
// Layers exist so that static apps can use them like a virtual filesystem
|
|
// i.e. oauth3.html isn't in *your* app but you may use it and want it mounted at /.well-known/oauth3.html
|
|
// or perhaps some dynamic content (like application cache)
|
|
function nextify(err) {
|
|
var route;
|
|
nexti += 1;
|
|
|
|
if (err) {
|
|
next(err);
|
|
return;
|
|
}
|
|
|
|
// shortest to longest
|
|
//route = packages.pop();
|
|
// longest to shortest
|
|
route = router.packages[nexti];
|
|
if (!route) {
|
|
next();
|
|
return;
|
|
}
|
|
|
|
if (!route.app) {
|
|
// new Error("no Static App is registered for the specified path")
|
|
nextify();
|
|
return;
|
|
}
|
|
if (route._app) {
|
|
route._app(req, res, nextify);
|
|
return;
|
|
}
|
|
|
|
// could attach to req.{ pkgConf, pkgDeps, Services}
|
|
loadPages(pkgConf, route, req, res, next);
|
|
}
|
|
|
|
nextify();
|
|
}
|
|
|
|
function runApi(opts, router, req, res, next) {
|
|
var pkgConf = opts.config;
|
|
var pkgDeps = opts.deps;
|
|
//var Services = opts.Services;
|
|
var route;
|
|
|
|
// TODO compile packagesMap
|
|
// TODO people may want to use the framework in a non-framework way (i.e. to conceal the module name)
|
|
router.packages.some(function (_route) {
|
|
if (!_route.api) {
|
|
return;
|
|
}
|
|
|
|
var pathname = router.pathname;
|
|
if ('/' === pathname) {
|
|
pathname = '';
|
|
}
|
|
// TODO allow for special apis that do not follow convention (.well_known, webfinger, oauth3.html, etc)
|
|
if (!_route._api_re) {
|
|
_route._api_re = new RegExp(escapeStringRegexp(pathname + '/api/' + _route.api.id) + '\/([\\w\\.\\-]+)(\\/|\\?|$)');
|
|
//console.log('[api re 2]', _route._api_re);
|
|
}
|
|
if (_route._api_re.test(req.url)) {
|
|
route = _route;
|
|
return true;
|
|
}
|
|
});
|
|
|
|
if (!route) {
|
|
//console.log('[no api route]');
|
|
next();
|
|
return;
|
|
}
|
|
Object.defineProperty(req, 'appId', {
|
|
enumerable: true
|
|
, configurable: false
|
|
, writable: false
|
|
// TODO this identifier may need to be non-deterministic as to transfer if a domain name changes but is still the "same" app
|
|
// (i.e. a company name change. maybe auto vs manual register - just like oauth3?)
|
|
, value: route.id
|
|
});
|
|
Object.defineProperty(req, 'appConfig', {
|
|
enumerable: true
|
|
, configurable: false
|
|
, writable: false
|
|
, value: {} // TODO just the app-scoped config
|
|
});
|
|
Object.defineProperty(req, 'appDeps', {
|
|
enumerable: true
|
|
, configurable: false
|
|
, writable: false
|
|
, value: {} // TODO app-scoped deps
|
|
// i.e. when we need to use things such as stripe id
|
|
// without exposing them to the app
|
|
});
|
|
|
|
//
|
|
// TODO user authentication should go right about here
|
|
//
|
|
|
|
//
|
|
// TODO freeze objects for passing them into app
|
|
//
|
|
|
|
if (route._api) {
|
|
route._api(req, res, next);
|
|
return;
|
|
}
|
|
|
|
loadApi(pkgConf, pkgDeps, route).then(function (api) {
|
|
api(req, res, next);
|
|
}, function (err) {
|
|
console.error('[App Promise Error]');
|
|
next(err);
|
|
});
|
|
}
|
|
|
|
function mapToApp(opts, req, res, next) {
|
|
// opts = { config, deps, services }
|
|
var vhost;
|
|
var router;
|
|
var pkgConf = opts.config;
|
|
|
|
if (!pkgConf.vhostConf) {
|
|
pkgConf.vhostConf = compileVhosts(pkgConf.vhostsMap);
|
|
}
|
|
|
|
//console.log('req.hostname');
|
|
//console.log(req.hostname);
|
|
|
|
//console.log(Object.keys(pkgConf.vhostConf.matchesMap));
|
|
|
|
// TODO www vs no-www?
|
|
vhost = pkgConf.vhostConf.matchesMap[req.hostname];
|
|
|
|
if (!vhost) {
|
|
pkgConf.vhostConf.patterns.some(function (pkg) {
|
|
// TODO this should be done in the compile phase
|
|
if ('*' === pkg.id[0] && '.' === pkg.id[1]) {
|
|
pkg.id = pkg.id.slice(1);
|
|
}
|
|
if (pkg.id === req.hostname.slice(req.hostname.length - pkg.id.length)) {
|
|
vhost = pkg;
|
|
return true;
|
|
}
|
|
});
|
|
}
|
|
|
|
if (!vhost) {
|
|
next();
|
|
return;
|
|
}
|
|
|
|
// TODO don't modify route here (or in subloaders), modify some other variable instead
|
|
// TODO precompile RegExps and pre-sort app vs api
|
|
vhost.pathnames.some(function (routes) {
|
|
var pathname = routes.pathname;
|
|
if ('/' === pathname) {
|
|
pathname = '';
|
|
}
|
|
|
|
if (!routes._re_app) {
|
|
routes._re_app = new RegExp(escapeStringRegexp(pathname) + '(#|\\/|\\?|$)');
|
|
//console.log('[static re]', routes._re_app);
|
|
}
|
|
|
|
if (!routes._re_api) {
|
|
// TODO allow for special apis that do not follow convention (.well_known, webfinger, oauth3.html, etc)
|
|
routes._re_api = new RegExp(escapeStringRegexp(pathname + '/api/') + '([\\w\\.\\-]+)(\\/|\\?|$)');
|
|
//console.log('[api re]', routes._re_api);
|
|
}
|
|
|
|
if (routes._re_app.test(req.url)) {
|
|
router = routes;
|
|
return true;
|
|
}
|
|
|
|
// no need to test for api yet as it is a postfix
|
|
});
|
|
|
|
if (!router) {
|
|
//console.log('[no router for]', req.url);
|
|
next();
|
|
return;
|
|
}
|
|
|
|
if (!router._re_api.test(req.url)) {
|
|
//console.log('[static router]');
|
|
//console.log(router._re_api, req.url);
|
|
layerItUp(pkgConf, router, req, res, next);
|
|
return;
|
|
}
|
|
|
|
//console.log('[api router]', req.url);
|
|
return runApi(opts, router, req, res, next);
|
|
}
|
|
|
|
module.exports.runApi = runApi;
|
|
module.exports.compileVhosts = compileVhosts;
|
|
module.exports.mapToApp = mapToApp;
|