now does redirects, static apps, and api mounting
This commit is contained in:
parent
59dd087084
commit
4f23dae53e
|
@ -17,8 +17,8 @@ var workers = [];
|
|||
var caddypath = '/usr/local/bin/caddy';
|
||||
var useCaddy = require('fs').existsSync(caddypath);
|
||||
var conf = {
|
||||
localPort: process.argv[2] || (useCaddy ? 4080 : 443) // system / local network
|
||||
, insecurePort: process.argv[3] || (useCaddy ? 80 : 80) // meh
|
||||
localPort: process.argv[2] || (useCaddy ? 4080 : 6443) // system / local network
|
||||
, insecurePort: process.argv[3] || (useCaddy ? 80 : 65080) // meh
|
||||
, externalPort: 443 // world accessible
|
||||
// TODO externalInsecurePort?
|
||||
, locked: false // TODO XXX
|
||||
|
@ -42,8 +42,9 @@ function fork() {
|
|||
cluster.on('online', function (worker) {
|
||||
var path = require('path');
|
||||
// TODO XXX Should these be configurable? If so, where?
|
||||
var certPaths = [path.join(__dirname, '..', 'certs', 'live')];
|
||||
var certPaths = [path.join(__dirname, '..', '..', 'certs', 'live')];
|
||||
var info;
|
||||
var config = require('../../config');
|
||||
|
||||
console.info('[MASTER] Worker ' + worker.process.pid + ' is online');
|
||||
fork();
|
||||
|
@ -59,6 +60,8 @@ cluster.on('online', function (worker) {
|
|||
, trustProxy: useCaddy ? true : false
|
||||
, certPaths: useCaddy ? null : certPaths
|
||||
, ipcKey: null
|
||||
// TODO let this load after server is listening
|
||||
, redirects: config.redirects
|
||||
}
|
||||
};
|
||||
worker.send(info);
|
||||
|
@ -85,6 +88,7 @@ cluster.on('online', function (worker) {
|
|||
// TODO get this from db config instead
|
||||
info.conf.privkey = config.privkey;
|
||||
info.conf.pubkey = config.pubkey;
|
||||
info.conf.redirects = config.redirects;
|
||||
worker.send(info);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -2,24 +2,37 @@
|
|||
|
||||
module.exports.create = function (opts) {
|
||||
var id = '0';
|
||||
var promiseApp;
|
||||
|
||||
function createAndBindInsecure(message, cb) {
|
||||
// TODO conditional if 80 is being served by caddy
|
||||
require('../lib/insecure-server').create(message.conf.externalPort, message.conf.insecurePort, message, function (err, webserver) {
|
||||
console.info("#" + id + " Listening on http://" + webserver.address().address + ":" + webserver.address().port, '\n');
|
||||
|
||||
// we are returning the promise result to the caller
|
||||
return cb(null, webserver, null, message);
|
||||
});
|
||||
}
|
||||
|
||||
function createAndBindServers(message, cb) {
|
||||
var msg = message.conf;
|
||||
|
||||
require('../lib/local-server').create(msg.certPaths, msg.localPort, function (err, webserver) {
|
||||
// NOTE that message.conf[x] will be overwritten when the next message comes in
|
||||
require('../lib/local-server').create(message.conf.certPaths, message.conf.localPort, message, function (err, webserver) {
|
||||
if (err) {
|
||||
console.error('[ERROR] worker.js');
|
||||
console.error(err.stack);
|
||||
throw err;
|
||||
}
|
||||
|
||||
console.info("#" + id + " Listening on " + msg.protocol + "://" + webserver.address().address + ":" + webserver.address().port, '\n');
|
||||
console.info("#" + id + " Listening on " + message.conf.protocol + "://" + webserver.address().address + ":" + webserver.address().port, '\n');
|
||||
|
||||
return cb(webserver);
|
||||
// we don't need time to pass, just to be able to return
|
||||
process.nextTick(function () {
|
||||
createAndBindInsecure(message, cb);
|
||||
});
|
||||
|
||||
// we are returning the promise result to the caller
|
||||
return cb(null, null, webserver, message);
|
||||
});
|
||||
|
||||
// TODO conditional if 80 is being served by caddy
|
||||
require('../lib/insecure-server').create(msg.externalPort, msg.insecurePort);
|
||||
}
|
||||
|
||||
//
|
||||
|
@ -35,9 +48,16 @@ module.exports.create = function (opts) {
|
|||
process.removeListener('message', waitForConfig);
|
||||
|
||||
// NOTE: this callback must return a promise for an express app
|
||||
createAndBindServers(message, function (webserver) {
|
||||
createAndBindServers(message, function (err, insecserver, webserver, oldMessage) {
|
||||
// TODO deep merge new message into old message
|
||||
Object.keys(message.conf).forEach(function (key) {
|
||||
oldMessage.conf[key] = message.conf[key];
|
||||
});
|
||||
var PromiseA = require('bluebird');
|
||||
return new PromiseA(function (resolve) {
|
||||
if (promiseApp) {
|
||||
return promiseApp;
|
||||
}
|
||||
promiseApp = new PromiseA(function (resolve) {
|
||||
function initWebServer(srvmsg) {
|
||||
if ('com.daplie.walnut.webserver.onrequest' !== srvmsg.type) {
|
||||
console.warn('[Worker] 1 got unexpected message:');
|
||||
|
@ -56,6 +76,7 @@ module.exports.create = function (opts) {
|
|||
console.info('[Worker Ready]');
|
||||
return app;
|
||||
});
|
||||
return promiseApp;
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -64,9 +85,12 @@ module.exports.create = function (opts) {
|
|||
//
|
||||
if (opts) {
|
||||
// NOTE: this callback must return a promise for an express app
|
||||
createAndBindServers(opts, function (webserver) {
|
||||
createAndBindServers(opts, function (err, insecserver, webserver/*, message*/) {
|
||||
var PromiseA = require('bluebird');
|
||||
return new PromiseA(function (resolve) {
|
||||
if (promiseApp) {
|
||||
return promiseApp;
|
||||
}
|
||||
promiseApp = new PromiseA(function (resolve) {
|
||||
opts.getConfig(function (srvmsg) {
|
||||
resolve(require('../lib/worker').create(webserver, srvmsg));
|
||||
});
|
||||
|
@ -74,6 +98,7 @@ module.exports.create = function (opts) {
|
|||
console.info('[Standalone Ready]');
|
||||
return app;
|
||||
});
|
||||
return promiseApp;
|
||||
});
|
||||
} else {
|
||||
// we are in cluster mode, as opposed to standalone mode
|
||||
|
|
|
@ -1,159 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
// TODO handle static app urls?
|
||||
// NOTE rejecting non-api urls should happen before this
|
||||
module.exports.create = function (conf, deps/*, Services*/) {
|
||||
var PromiseA = deps.Promise;
|
||||
var app = deps.app;
|
||||
var express = deps.express;
|
||||
var escapeStringRegexp = require('escape-string-regexp');
|
||||
var vhostsMap = conf.vhostsMap;
|
||||
|
||||
function getApi(route) {
|
||||
// TODO don't modify route, modify some other variable instead
|
||||
|
||||
var path = require('path');
|
||||
// TODO needs some version stuff (which would also allow hot-loading of updates)
|
||||
// TODO version could be tied to sha256sum
|
||||
var pkgpath = path.join(conf.apipath, (route.api.package || route.api.id), (route.api.version || ''));
|
||||
|
||||
return new PromiseA(function (resolve, reject) {
|
||||
var myApp;
|
||||
var ursa;
|
||||
|
||||
try {
|
||||
// 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 = express();
|
||||
myApp.disable('x-powered-by');
|
||||
if (app.get('trust proxy')) {
|
||||
myApp.set('trust proxy', app.get('trust proxy'));
|
||||
}
|
||||
if (!conf.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');
|
||||
conf.keypair = ursa.createPrivateKey(conf.privkey, 'ascii');
|
||||
conf.pubkey = ursa.createPublicKey(conf.pubkey, 'ascii'); //conf.keypair.toPublicKey();
|
||||
}
|
||||
// TODO give pub/priv pair for app and all public keys
|
||||
route.route = require(pkgpath).create(conf, deps, myApp);
|
||||
} catch(e) {
|
||||
reject(e);
|
||||
return;
|
||||
}
|
||||
|
||||
resolve(route.route);
|
||||
});
|
||||
}
|
||||
|
||||
function api(req, res, next) {
|
||||
var apps;
|
||||
|
||||
if (!vhostsMap[req.hostname]) {
|
||||
// TODO keep track of match-only vhosts, such as '*.example.com',
|
||||
// separate from exact matches
|
||||
next(new Error("this domain is not registered"));
|
||||
return;
|
||||
}
|
||||
|
||||
vhostsMap[req.hostname].pathnames.some(function (route) {
|
||||
var pathname = route.pathname;
|
||||
if ('/' === pathname) {
|
||||
pathname = '/api';
|
||||
}
|
||||
if (-1 === pathname.indexOf('/api')) {
|
||||
// TODO needs namespace for current api
|
||||
pathname = '/api' + pathname;
|
||||
}
|
||||
// pathname += '.local';
|
||||
|
||||
if (!route.re) {
|
||||
route.re = new RegExp(escapeStringRegexp(pathname) + '(#|\\/|\\?|$)');
|
||||
}
|
||||
// re.test("/api")
|
||||
// re.test("/api?")
|
||||
// re.test("/api/")
|
||||
// re.test("/api/foo")
|
||||
// re.test("/apifoo") // false
|
||||
if (route.re.test(req.url)) {
|
||||
// make a copy
|
||||
apps = route.apps.slice(0);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
if (!apps) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
function nextify(err) {
|
||||
var route;
|
||||
|
||||
if (err) {
|
||||
next(err);
|
||||
return;
|
||||
}
|
||||
|
||||
// shortest to longest
|
||||
//route = apps.pop();
|
||||
// longest to shortest
|
||||
route = apps.shift();
|
||||
if (!route) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
if (route.route) {
|
||||
if (route.route.then) {
|
||||
route.route.then(function (expressApp) {
|
||||
expressApp(req, res, nextify);
|
||||
});
|
||||
return;
|
||||
}
|
||||
route.route(req, res, nextify);
|
||||
return;
|
||||
}
|
||||
|
||||
if (route._errored) {
|
||||
nextify(new Error("couldn't load api"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!route.api) {
|
||||
console.error('missing route:', req.url);
|
||||
nextify(new Error("no api available for this route"));
|
||||
return;
|
||||
}
|
||||
|
||||
return getApi(route).then(function (expressApp) {
|
||||
try {
|
||||
expressApp(req, res, nextify);
|
||||
route.route = expressApp;
|
||||
} catch(e) {
|
||||
route._errored = true;
|
||||
console.error('[App Load Error]');
|
||||
nextify(new Error("couldn't load api"));
|
||||
}
|
||||
|
||||
return expressApp;
|
||||
}, function (err) {
|
||||
console.error('[App Promise Error]');
|
||||
nextify(err);
|
||||
});
|
||||
}
|
||||
|
||||
nextify();
|
||||
}
|
||||
|
||||
return {
|
||||
api: api
|
||||
};
|
||||
};
|
|
@ -2,8 +2,7 @@
|
|||
|
||||
// TODO detect infinite redirects
|
||||
|
||||
module.exports.compile = module.exports.sortOpts = function (opts) {
|
||||
var redirects = opts.redirects;
|
||||
module.exports.compile = module.exports.sortOpts = function (redirects) {
|
||||
var dups = {};
|
||||
var results = {
|
||||
conflicts: {}
|
||||
|
|
|
@ -1,50 +1,51 @@
|
|||
'use strict';
|
||||
|
||||
module.exports.create = function (securePort, insecurePort, redirects) {
|
||||
module.exports.create = function (securePort, insecurePort, info, serverCallback) {
|
||||
var PromiseA = require('bluebird').Promise;
|
||||
var appPromise;
|
||||
//var app;
|
||||
var http = require('http');
|
||||
var escapeRe;
|
||||
var redirectives;
|
||||
|
||||
function useAppInsecurely(req, res) {
|
||||
if (!appPromise) {
|
||||
return false;
|
||||
}
|
||||
|
||||
appPromise.then(function (app) {
|
||||
req._WALNUT_SECURITY_EXCEPTION = true;
|
||||
app(req, res);
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function redirectHttps(req, res) {
|
||||
res.setHeader('Strict-Transport-Security', 'max-age=10886400; includeSubDomains; preload');
|
||||
// Let it do this once they visit the https site
|
||||
// res.setHeader('Strict-Transport-Security', 'max-age=10886400; includeSubDomains; preload');
|
||||
|
||||
var insecureRedirects;
|
||||
var host = req.headers.host || '';
|
||||
var url = req.url;
|
||||
|
||||
if (require('./unbrick-appcache').unbrick(req, res)) {
|
||||
return;
|
||||
// TODO
|
||||
// XXX NOTE: info.conf.redirects may or may not be loaded at first
|
||||
// the object will be modified when the config is loaded
|
||||
if (!redirectives && info.conf.redirects) {
|
||||
redirectives = require('./hostname-redirects').compile(info.conf.redirects);
|
||||
}
|
||||
if (require('./no-www').scrubTheDub(req, res, redirectives)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// because I have domains for which I don't want to pay for SSL certs
|
||||
insecureRedirects = (redirects||[]).sort(function (a, b) {
|
||||
var hlen = b.from.hostname.length - a.from.hostname.length;
|
||||
var plen;
|
||||
if (!hlen) {
|
||||
plen = b.from.path.length - a.from.path.length;
|
||||
return plen;
|
||||
}
|
||||
return hlen;
|
||||
}).forEach(function (redirect) {
|
||||
var origHost = host;
|
||||
// TODO
|
||||
// allow exceptions for the case of arduino and whatnot that cannot handle https?
|
||||
// http://evothings.com/is-it-possible-to-secure-micro-controllers-used-within-iot/
|
||||
// needs ECDSA?
|
||||
|
||||
if (!escapeRe) {
|
||||
escapeRe = require('escape-string-regexp');
|
||||
}
|
||||
|
||||
// TODO if '*' === hostname[0], omit '^'
|
||||
host = host.replace(
|
||||
new RegExp('^' + escapeRe(redirect.from.hostname))
|
||||
, redirect.to.hostname
|
||||
);
|
||||
if (host === origHost) {
|
||||
return;
|
||||
}
|
||||
url = url.replace(
|
||||
new RegExp('^' + escapeRe(redirect.from.path))
|
||||
, redirect.to.path
|
||||
);
|
||||
});
|
||||
console.warn('HARD-CODED HTTPS EXCEPTION in insecure-server.js');
|
||||
if (/redirect-www.org/.test(host) && useAppInsecurely(req, res)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
var escapeHtml = require('escape-html');
|
||||
var newLocation = 'https://'
|
||||
|
@ -95,11 +96,14 @@ module.exports.create = function (securePort, insecurePort, redirects) {
|
|||
//
|
||||
var insecureServer;
|
||||
insecureServer = http.createServer();
|
||||
insecureServer.on('request', redirectHttps);
|
||||
insecureServer.listen(insecurePort, function () {
|
||||
console.log("\nListening on https://localhost:" + insecureServer.address().port);
|
||||
console.log("(redirecting all traffic to https)\n");
|
||||
console.log("\nListening on http://localhost:" + insecureServer.address().port);
|
||||
console.log("(handling any explicit redirects and redirecting all other traffic to https)\n");
|
||||
if (serverCallback) {
|
||||
appPromise = serverCallback(null, insecureServer);
|
||||
}
|
||||
});
|
||||
insecureServer.on('request', redirectHttps);
|
||||
|
||||
return PromiseA.resolve(insecureServer);
|
||||
};
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
// Note the odd use of callbacks (instead of promises) here
|
||||
// It's to avoid loading bluebird yet (see sni-server.js for explanation)
|
||||
module.exports.create = function (certPaths, port, serverCallback) {
|
||||
module.exports.create = function (certPaths, port, info, serverCallback) {
|
||||
function initServer(err, server) {
|
||||
var app;
|
||||
var promiseApp;
|
||||
|
@ -45,7 +45,7 @@ module.exports.create = function (certPaths, port, serverCallback) {
|
|||
}
|
||||
|
||||
if (certPaths) {
|
||||
require('./sni-server').create(certPaths, port, initServer);
|
||||
require('./sni-server').create(certPaths, initServer);
|
||||
} else {
|
||||
initServer(null, require('http').createServer());
|
||||
}
|
||||
|
|
|
@ -11,14 +11,16 @@ module.exports.scrubTheDub = function (req, res, redirectives) {
|
|||
var escapeHtml = require('escape-html');
|
||||
var newLocation;
|
||||
var safeLocation;
|
||||
// TODO req.hostname
|
||||
var hostname = (req.headers.host||'').split(':')[0];
|
||||
|
||||
if (redirectives) {
|
||||
newLocation = require('./hostname-redirects').redirectTo(req.hostname, redirectives);
|
||||
newLocation = require('./hostname-redirects').redirectTo(hostname, redirectives);
|
||||
if (!newLocation) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
newLocation = 'https://' + req.hostname.replace(/^www\./, '') + req.url;
|
||||
newLocation = 'https://' + hostname.replace(/^www\./, '') + req.url;
|
||||
}
|
||||
safeLocation = escapeHtml(newLocation);
|
||||
|
||||
|
|
|
@ -0,0 +1,385 @@
|
|||
'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;
|
||||
}
|
||||
|
||||
//console.log('[vhost]');
|
||||
//console.log(vhost);
|
||||
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.apppath, (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;
|
||||
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) {
|
||||
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) {
|
||||
if ('*' === pkg.id || pkg.id === req.hostname.slice(req.hostname.length - pkg.id.length)) {
|
||||
vhost = pkg;
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!vhost) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
//console.log('vhost');
|
||||
//console.log(vhost);
|
||||
|
||||
// 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;
|
|
@ -72,29 +72,29 @@ function deserialize(results) {
|
|||
return config;
|
||||
}
|
||||
|
||||
function sortApps(a, b) {
|
||||
// hlen isn't important in this current use of the sorter,
|
||||
// but is important for an alternate version
|
||||
var hlen = b.hostname.length - a.hostname.length;
|
||||
var plen = b.pathname.length - a.pathname.length;
|
||||
|
||||
// A directory could be named example.com, example.com# example.com##
|
||||
// to indicate order of preference (for API addons, for example)
|
||||
var dlen = (b.priority || b.dirname.length) - (a.priority || a.dirname.length);
|
||||
|
||||
if (!hlen) {
|
||||
if (!plen) {
|
||||
return dlen;
|
||||
}
|
||||
return plen;
|
||||
}
|
||||
return hlen;
|
||||
}
|
||||
|
||||
function getVhostsMap(config) {
|
||||
var vhosts = [];
|
||||
var vhostsMap = {};
|
||||
|
||||
function sortApps(a, b) {
|
||||
// hlen isn't important in this current use of the sorter,
|
||||
// but is important for an alternate version
|
||||
var hlen = b.hostname.length - a.hostname.length;
|
||||
var plen = b.pathname.length - a.pathname.length;
|
||||
|
||||
// A directory could be named example.com, example.com# example.com##
|
||||
// to indicate order of preference (for API addons, for example)
|
||||
var dlen = (b.priority || b.dirname.length) - (a.priority || a.dirname.length);
|
||||
|
||||
if (!hlen) {
|
||||
if (!plen) {
|
||||
return dlen;
|
||||
}
|
||||
return plen;
|
||||
}
|
||||
return hlen;
|
||||
}
|
||||
|
||||
Object.keys(config.domains).forEach(function (domainname) {
|
||||
var domain = config.domains[domainname];
|
||||
var info = getDomainInfo(domainname);
|
||||
|
@ -110,15 +110,15 @@ function getVhostsMap(config) {
|
|||
|
||||
vhosts.forEach(function (domain) {
|
||||
if (!vhostsMap[domain.hostname]) {
|
||||
vhostsMap[domain.hostname] = { pathnamesMap: {}, pathnames: [] };
|
||||
vhostsMap[domain.hostname] = { hostname: domain.hostname, id: domain.id, pathnamesMap: {}, pathnames: [] };
|
||||
}
|
||||
|
||||
if (!vhostsMap[domain.hostname].pathnamesMap[domain.pathname]) {
|
||||
vhostsMap[domain.hostname].pathnamesMap[domain.pathname] = { pathname: domain.pathname, apps: [] };
|
||||
vhostsMap[domain.hostname].pathnamesMap[domain.pathname] = { pathname: domain.pathname, packages: [] };
|
||||
vhostsMap[domain.hostname].pathnames.push(vhostsMap[domain.hostname].pathnamesMap[domain.pathname]);
|
||||
}
|
||||
|
||||
vhostsMap[domain.hostname].pathnamesMap[domain.pathname].apps.push(domain);
|
||||
vhostsMap[domain.hostname].pathnamesMap[domain.pathname].packages.push(domain);
|
||||
});
|
||||
|
||||
return vhostsMap;
|
||||
|
@ -135,7 +135,7 @@ module.exports.create = function (db) {
|
|||
//
|
||||
{ tablename: 'apis'
|
||||
, idname: 'id' // io.lds.auth, com.daplie.radio
|
||||
, unique: ['id']
|
||||
, unique: ['id']
|
||||
// name // LDS Account, Radio
|
||||
, indices: ['createdAt', 'updatedAt', 'deletedAt', 'revokedAt', 'name']
|
||||
}
|
||||
|
|
|
@ -10,8 +10,6 @@ module.exports.create = function (webserver, info, state) {
|
|||
//var vhostsdir = path.join(__dirname, 'vhosts');
|
||||
var express = require('express-lazy');
|
||||
var app = express();
|
||||
var apiHandler;
|
||||
var Services;
|
||||
var memstore;
|
||||
var sqlstores = {};
|
||||
var models = {};
|
||||
|
@ -37,6 +35,7 @@ module.exports.create = function (webserver, info, state) {
|
|||
, ipcKey: info.conf.ipcKey
|
||||
});
|
||||
var cstore = require('cluster-store');
|
||||
var redirectives;
|
||||
|
||||
app.disable('x-powered-by');
|
||||
if (info.conf.trustProxy) {
|
||||
|
@ -45,6 +44,8 @@ module.exports.create = function (webserver, info, state) {
|
|||
//app.set('trust proxy', function (ip) { console.log('[ip]', ip); return true; });
|
||||
} else {
|
||||
console.info('[DO NOT trust proxy]');
|
||||
// TODO make sure the gzip module loads if there isn't a proxy gzip-ing for us
|
||||
// app.use(compression())
|
||||
}
|
||||
|
||||
/*
|
||||
|
@ -85,12 +86,21 @@ module.exports.create = function (webserver, info, state) {
|
|||
// TODO test if this is even necessary
|
||||
host = host.toLowerCase();
|
||||
|
||||
if (!/^www\./.test(host)) {
|
||||
// TODO this should be hot loadable / changeable
|
||||
if (!redirectives && info.conf.redirects) {
|
||||
redirectives = require('./hostname-redirects').compile(info.conf.redirects);
|
||||
}
|
||||
|
||||
if (!/^www\./.test(host) && !redirectives) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
require('./no-www').scrubTheDub(req, res);
|
||||
// TODO misnomer, handles all exact redirects
|
||||
if (!require('./no-www').scrubTheDub(req, res, redirectives)) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
function caddyBugfix(req, res, next) {
|
||||
|
@ -108,6 +118,7 @@ module.exports.create = function (webserver, info, state) {
|
|||
next();
|
||||
}
|
||||
|
||||
// TODO misnomer, this can handle nowww, yeswww, and exact hostname redirects
|
||||
app.use('/', scrubTheDub);
|
||||
app.use('/', caddyBugfix);
|
||||
|
||||
|
@ -153,21 +164,34 @@ module.exports.create = function (webserver, info, state) {
|
|||
// TODO the core needs to be replacable in one shot
|
||||
// rm -rf /tmp/walnut/; tar xvf -C /tmp/walnut/; mv /srv/walnut /srv/walnut.{{version}}; mv /tmp/walnut /srv/
|
||||
// this means that any packages must be outside, perhaps /srv/walnut/{boot,core,packages}
|
||||
var apiConf = {
|
||||
var pkgConf = {
|
||||
apppath: path.join(__dirname, '..', '..', 'packages', 'apps') + path.sep
|
||||
, apipath: path.join(__dirname, '..', '..', 'packages', 'apis') + path.sep
|
||||
, servicespath: path.join(__dirname, '..', '..', 'packages', 'services')
|
||||
, vhostsMap: vhostsMap
|
||||
, vhostPatterns: null
|
||||
, server: webserver
|
||||
, externalPort: info.conf.externalPort
|
||||
, primaryNameserver: info.conf.primaryNameserver
|
||||
, nameservers: info.conf.nameservers
|
||||
, privkey: info.conf.privkey
|
||||
, pubkey: info.conf.pubkey
|
||||
, redirects: info.conf.redirects
|
||||
, apiPrefix: '/api'
|
||||
};
|
||||
|
||||
Services = require('./services-loader').create(apiConf, {
|
||||
var pkgDeps = {
|
||||
memstore: memstore
|
||||
, sqlstores: sqlstores
|
||||
, clientSqlFactory: clientFactory
|
||||
, systemSqlFactory: systemFactory
|
||||
//, handlePromise: require('./lib/common').promisableRequest;
|
||||
//, handleRejection: require('./lib/common').rejectableRequest;
|
||||
//, localPort: info.conf.localPort
|
||||
, Promise: PromiseA
|
||||
, express: express
|
||||
, app: app
|
||||
};
|
||||
var Services = require('./services-loader').create(pkgConf, {
|
||||
memstore: memstore
|
||||
, sqlstores: sqlstores
|
||||
, clientSqlFactory: clientFactory
|
||||
|
@ -175,12 +199,7 @@ module.exports.create = function (webserver, info, state) {
|
|||
, Promise: PromiseA
|
||||
});
|
||||
|
||||
function handleApi(req, res, next) {
|
||||
if (!/^\/api/.test(req.url)) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
function handlePackages(req, res, next) {
|
||||
// TODO move to caddy parser?
|
||||
if (/(^|\.)proxyable\./.test(req.hostname)) {
|
||||
// device-id-12345678.proxyable.myapp.mydomain.com => myapp.mydomain.com
|
||||
|
@ -189,34 +208,11 @@ module.exports.create = function (webserver, info, state) {
|
|||
req.hostname = req.hostname.replace(/.*\.?proxyable\./, '');
|
||||
}
|
||||
|
||||
if (apiHandler) {
|
||||
/*
|
||||
if (apiHandler.then) {
|
||||
apiHandler.then(function (myApp) {
|
||||
myApp(req, res, next);
|
||||
});
|
||||
return;
|
||||
}
|
||||
*/
|
||||
|
||||
apiHandler(req, res, next);
|
||||
return;
|
||||
}
|
||||
|
||||
apiHandler = require('./api-server').create(apiConf, {
|
||||
memstore: memstore
|
||||
, sqlstores: sqlstores
|
||||
, clientSqlFactory: clientFactory
|
||||
, systemSqlFactory: systemFactory
|
||||
//, handlePromise: require('./lib/common').promisableRequest;
|
||||
//, handleRejection: require('./lib/common').rejectableRequest;
|
||||
//, localPort: info.conf.localPort
|
||||
, Promise: PromiseA
|
||||
, express: express
|
||||
, app: app
|
||||
}, Services).api;
|
||||
|
||||
apiHandler(req, res, next);
|
||||
require('./package-server').mapToApp({
|
||||
config: pkgConf
|
||||
, deps: pkgDeps
|
||||
, services: Services
|
||||
}, req, res, next);
|
||||
}
|
||||
|
||||
// TODO recase
|
||||
|
@ -246,7 +242,8 @@ module.exports.create = function (webserver, info, state) {
|
|||
//}))
|
||||
.use(require('connect-send-error').error())
|
||||
;
|
||||
app.use('/', handleApi);
|
||||
|
||||
app.use('/', handlePackages);
|
||||
app.use('/', function (err, req, res, next) {
|
||||
console.error('[Error Handler]');
|
||||
console.error(err.stack);
|
||||
|
|
|
@ -55,9 +55,9 @@ var domains = {
|
|||
, 'www.example.com': false
|
||||
};
|
||||
|
||||
var redirects = sortOpts(opts);
|
||||
var redirects = sortOpts(opts.redirects);
|
||||
|
||||
console.log(redirects);
|
||||
//console.log(redirects);
|
||||
|
||||
Object.keys(domains).forEach(function (domain, i) {
|
||||
var redir = domains[domain];
|
||||
|
@ -68,5 +68,8 @@ Object.keys(domains).forEach(function (domain, i) {
|
|||
}
|
||||
});
|
||||
|
||||
console.log("TODO: we do not yet detect infinite loop redirects");
|
||||
console.log("");
|
||||
console.log("");
|
||||
console.log("Didn't throw any errors. Must have worked, eh?");
|
||||
console.log("TODO: detect and report infinite redirects");
|
||||
console.log("");
|
||||
|
|
Loading…
Reference in New Issue