refactoring to use fs config
This commit is contained in:
parent
28db03ae23
commit
d792404d67
|
@ -62,4 +62,7 @@ function eagerLoad() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
setTimeout(eagerLoad, 100);
|
// this isn't relevant to do in the master process, duh
|
||||||
|
if (false) {
|
||||||
|
setTimeout(eagerLoad, 100);
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,59 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// 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 (lex, certPaths, port, conf, serverCallback) {
|
||||||
|
function initServer(err, server) {
|
||||||
|
var app;
|
||||||
|
var promiseApp;
|
||||||
|
|
||||||
|
if (err) {
|
||||||
|
serverCallback(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
server.on('error', serverCallback);
|
||||||
|
server.listen(port, function () {
|
||||||
|
// is it even theoritically possible for
|
||||||
|
// a request to come in before this callback has fired?
|
||||||
|
// I'm assuming this event must fire before any request event
|
||||||
|
promiseApp = serverCallback(null, server);
|
||||||
|
});
|
||||||
|
/*
|
||||||
|
server.listen(port, '::::', function () {
|
||||||
|
// is it even theoritically possible for
|
||||||
|
// a request to come in before this callback has fired?
|
||||||
|
// I'm assuming this event must fire before any request event
|
||||||
|
promiseApp = serverCallback(null, server);
|
||||||
|
});
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Get up and listening as absolutely quickly as possible
|
||||||
|
function onRequest(req, res) {
|
||||||
|
// this is a hot piece of code, so we cache the result
|
||||||
|
if (app) {
|
||||||
|
app(req, res);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
promiseApp.then(function (_app) {
|
||||||
|
console.log('[Server]', req.method, req.host || req.headers['x-forwarded-host'] || req.headers.host, req.url);
|
||||||
|
app = _app;
|
||||||
|
app(req, res);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lex) {
|
||||||
|
var LEX = require('letsencrypt-express');
|
||||||
|
server.on('request', LEX.createAcmeResponder(lex, onRequest));
|
||||||
|
} else {
|
||||||
|
server.on('request', onRequest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (certPaths) {
|
||||||
|
require('../lib/sni-server').create(lex, certPaths, initServer);
|
||||||
|
} else {
|
||||||
|
initServer(null, require('http').createServer());
|
||||||
|
}
|
||||||
|
};
|
|
@ -25,28 +25,44 @@ var workers = [];
|
||||||
var state = { firstRun: true };
|
var state = { firstRun: true };
|
||||||
// TODO Should these be configurable? If so, where?
|
// TODO Should these be configurable? If so, where?
|
||||||
// TODO communicate config with environment vars?
|
// TODO communicate config with environment vars?
|
||||||
|
var walnut = tryConf(
|
||||||
|
path.join('..', '..', 'config.walnut')
|
||||||
|
, { externalPort: 443
|
||||||
|
, externalInsecurePort: 80
|
||||||
|
, certspath: path.join(__dirname, '..', '..', 'certs', 'live')
|
||||||
|
}
|
||||||
|
);
|
||||||
var caddy = tryConf(
|
var caddy = tryConf(
|
||||||
path.join('..', '..', 'config.caddy.json')
|
path.join('..', '..', 'config.caddy')
|
||||||
, { conf: null // __dirname + '/Caddyfile'
|
, { conf: path.join(__dirname, '..', '..', 'Caddyfile')
|
||||||
, bin: null // '/usr/local/bin/caddy'
|
, bin: null // '/usr/local/bin/caddy'
|
||||||
, sitespath: null // path.join(__dirname, 'sites-enabled')
|
, sitespath: null // path.join(__dirname, 'sites-enabled')
|
||||||
, locked: false // true
|
, locked: false // true
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
var useCaddy = require('fs').existsSync(caddy.bin);
|
var letsencrypt = tryConf(
|
||||||
|
path.join('..', '..', 'config.letsencrypt')
|
||||||
|
, { configDir: path.join(__dirname, '..', '..', 'letsencrypt')
|
||||||
|
, email: null
|
||||||
|
, agreeTos: false
|
||||||
|
}
|
||||||
|
);
|
||||||
|
var useCaddy = caddy.bin && require('fs').existsSync(caddy.bin);
|
||||||
var info = {
|
var info = {
|
||||||
type: 'walnut.init'
|
type: 'walnut.init'
|
||||||
, conf: {
|
, conf: {
|
||||||
protocol: useCaddy ? 'http' : 'https'
|
protocol: useCaddy ? 'http' : 'https'
|
||||||
, externalPort: 443
|
, externalPort: walnut.externalPort
|
||||||
, externalPortInsecure: 80 // TODO externalInsecurePort
|
, externalPortInsecure: walnut.externalInsecurePort // TODO externalInsecurePort
|
||||||
, localPort: process.argv[2] || (useCaddy ? 4080 : 443) // system / local network
|
, localPort: walnut.localPort || (useCaddy ? 4080 : 443) // system / local network
|
||||||
, insecurePort: process.argv[3] || (useCaddy ? 80 : 80) // meh
|
, insecurePort: walnut.insecurePort || (useCaddy ? 80 : 80) // meh
|
||||||
, certPaths: useCaddy ? null : [
|
, certPaths: useCaddy ? null : [
|
||||||
path.join(__dirname, '..', '..', 'certs', 'live')
|
walnut.certspath
|
||||||
, path.join(__dirname, '..', '..', 'letsencrypt', 'live')
|
, path.join(letsencrypt.configDir, 'live')
|
||||||
]
|
]
|
||||||
, trustProxy: useCaddy ? true : false
|
, trustProxy: useCaddy ? true : false
|
||||||
|
, lexConf: letsencrypt
|
||||||
|
, varpath: path.join(__dirname, '..', '..', 'var')
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -67,6 +83,7 @@ cluster.on('online', function (worker) {
|
||||||
// relies on { localPort, locked }
|
// relies on { localPort, locked }
|
||||||
caddy.spawn(caddy);
|
caddy.spawn(caddy);
|
||||||
}
|
}
|
||||||
|
// TODO dyndns in master?
|
||||||
}
|
}
|
||||||
|
|
||||||
function touchMaster(msg) {
|
function touchMaster(msg) {
|
||||||
|
@ -76,47 +93,11 @@ cluster.on('online', function (worker) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// calls init if init has not been called
|
|
||||||
state.caddy = caddy;
|
state.caddy = caddy;
|
||||||
state.workers = workers;
|
state.workers = workers;
|
||||||
require('../lib/master').touch(info.conf, state).then(function (results) {
|
// calls init if init has not been called
|
||||||
//var memstore = results.memstore;
|
require('../lib/master').touch(info.conf, state).then(function (newConf) {
|
||||||
var sqlstore = results.sqlstore;
|
worker.send({ type: 'walnut.webserver.onrequest', conf: newConf });
|
||||||
info.type = 'walnut.webserver.onrequest';
|
|
||||||
// TODO let this load after server is listening
|
|
||||||
info.conf['org.oauth3.consumer'] = results['org.oauth3.consumer'];
|
|
||||||
info.conf['org.oauth3.provider'] = results['org.oauth3.provider'];
|
|
||||||
info.conf.keys = results.keys;
|
|
||||||
//info.conf.memstoreSock = config.memstoreSock;
|
|
||||||
//info.conf.sqlite3Sock = config.sqlite3Sock;
|
|
||||||
// TODO get this from db config instead
|
|
||||||
//info.conf.privkey = config.privkey;
|
|
||||||
//info.conf.pubkey = config.pubkey;
|
|
||||||
info.conf.redirects = [
|
|
||||||
{ "ip": false, "id": "*", "value": false } // default no-www
|
|
||||||
|
|
||||||
, { "ip": false, "id": "daplie.domains", "value": null }
|
|
||||||
, { "ip": false, "id": "*.daplie.domains", "value": false }
|
|
||||||
, { "ip": false, "id": "no.daplie.domains", "value": false }
|
|
||||||
, { "ip": false, "id": "*.no.daplie.domains", "value": false }
|
|
||||||
, { "ip": false, "id": "ns2.daplie.domains", "value": false }
|
|
||||||
|
|
||||||
, { "ip": true, "id": "maybe.daplie.domains", "value": null }
|
|
||||||
, { "ip": true, "id": "*.maybe.daplie.domains", "value": null }
|
|
||||||
|
|
||||||
, { "ip": true, "id": "www.daplie.domains", "value": null }
|
|
||||||
, { "ip": true, "id": "yes.daplie.domains", "value": true }
|
|
||||||
, { "ip": true, "id": "*.yes.daplie.domains", "value": true }
|
|
||||||
, { "ip": true, "id": "ns1.daplie.domains", "value": false }
|
|
||||||
];
|
|
||||||
// TODO use sqlite3 or autogenerate ?
|
|
||||||
info.conf.privkey = require('fs').readFileSync(__dirname + '/../../' + '/nsx.redirect-www.org.key.pem', 'ascii');
|
|
||||||
info.conf.pubkey = require('fs').readFileSync(__dirname + '/../../' + '/nsx.redirect-www.org.key.pem.pub', 'ascii');
|
|
||||||
// keys
|
|
||||||
// letsencrypt
|
|
||||||
// com.example.provider
|
|
||||||
// com.example.consumer
|
|
||||||
worker.send(info);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
146
boot/worker.js
146
boot/worker.js
|
@ -4,25 +4,93 @@ module.exports.create = function (opts) {
|
||||||
var id = '0';
|
var id = '0';
|
||||||
var promiseApp;
|
var promiseApp;
|
||||||
|
|
||||||
function createAndBindInsecure(lex, message, cb) {
|
function createAndBindInsecure(lex, conf, getOrCreateHttpApp) {
|
||||||
// TODO conditional if 80 is being served by caddy
|
// TODO conditional if 80 is being served by caddy
|
||||||
require('../lib/insecure-server').create(lex, 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
|
var appPromise = null;
|
||||||
return cb(null, webserver, null, message);
|
var app = null;
|
||||||
|
var http = require('http');
|
||||||
|
var insecureServer = http.createServer();
|
||||||
|
|
||||||
|
function onRequest(req, res) {
|
||||||
|
if (app) {
|
||||||
|
app(req, res);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!appPromise) {
|
||||||
|
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
||||||
|
res.end('{ "error": { "code": "E_SANITY_FAIL", "message": "should have an express app, but didn\'t" } }');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
appPromise.then(function (_app) {
|
||||||
|
appPromise = null;
|
||||||
|
app = _app;
|
||||||
|
app(req, res);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function createLe(conf) {
|
insecureServer.listen(conf.insecurePort, function () {
|
||||||
|
console.info("#" + id + " Listening on http://"
|
||||||
|
+ insecureServer.address().address + ":" + insecureServer.address().port, '\n');
|
||||||
|
appPromise = getOrCreateHttpApp(null, insecureServer);
|
||||||
|
|
||||||
|
if (!appPromise) {
|
||||||
|
throw new Error('appPromise returned nothing');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
insecureServer.on('request', onRequest);
|
||||||
|
}
|
||||||
|
|
||||||
|
function walkLe(domainname) {
|
||||||
|
var PromiseA = require('bluebird');
|
||||||
|
var fs = PromiseA.promisifyAll(require('fs'));
|
||||||
|
var path = require('path');
|
||||||
|
var parts = domainname.split('.'); //.replace(/^www\./, '').split('.');
|
||||||
|
var configname = parts.join('.') + '.json';
|
||||||
|
var configpath = path.join(__dirname, '..', '..', 'config', configname);
|
||||||
|
|
||||||
|
if (parts.length < 2) {
|
||||||
|
return PromiseA.resolve(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO configpath a la varpath
|
||||||
|
return fs.readFileAsync(configpath, 'utf8').then(function (text) {
|
||||||
|
var data = JSON.parse(text);
|
||||||
|
data.name = configname;
|
||||||
|
return data;
|
||||||
|
}, function (/*err*/) {
|
||||||
|
parts.shift();
|
||||||
|
return walkLe(parts.join('.'));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function createLe(lexConf, conf) {
|
||||||
var LEX = require('letsencrypt-express');
|
var LEX = require('letsencrypt-express');
|
||||||
var lex = LEX.create({
|
var lex = LEX.create({
|
||||||
configDir: conf.letsencrypt.configDir // i.e. __dirname + '/letsencrypt.config'
|
configDir: lexConf.configDir // i.e. __dirname + '/letsencrypt.config'
|
||||||
, approveRegistration: function (hostname, cb) {
|
, approveRegistration: function (hostname, cb) {
|
||||||
|
// TODO cache/report unauthorized
|
||||||
|
if (!hostname) {
|
||||||
|
cb(new Error("[lex.approveRegistration] undefined hostname"), null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
walkLe(hostname).then(function (leAuth) {
|
||||||
|
// TODO should still check dns for hostname (and mx for email)
|
||||||
|
if (leAuth && leAuth.email && leAuth.agreeTos) {
|
||||||
cb(null, {
|
cb(null, {
|
||||||
domains: [hostname] // TODO handle www and bare on the same cert
|
domains: [hostname] // TODO handle www and bare on the same cert
|
||||||
, email: conf.letsencrypt.email
|
, email: leAuth.email
|
||||||
, agreeTos: conf.letsencrypt.agreeTos
|
, agreeTos: leAuth.agreeTos
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// TODO report unauthorized
|
||||||
|
cb(new Error("Valid LetsEncrypt config with email and agreeTos not found for '" + hostname + "'"), null);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
/*
|
/*
|
||||||
letsencrypt.getConfig({ domains: [domain] }, function (err, config) {
|
letsencrypt.getConfig({ domains: [domain] }, function (err, config) {
|
||||||
|
@ -42,81 +110,92 @@ module.exports.create = function (opts) {
|
||||||
*/
|
*/
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
//var letsencrypt = lex.letsencrypt;
|
conf.letsencrypt = lex.letsencrypt;
|
||||||
|
conf.lex = lex;
|
||||||
|
conf.walkLe = walkLe;
|
||||||
|
|
||||||
return lex;
|
return lex;
|
||||||
}
|
}
|
||||||
|
|
||||||
function createAndBindServers(message, cb) {
|
function createAndBindServers(conf, getOrCreateHttpApp) {
|
||||||
var lex;
|
var lex;
|
||||||
|
|
||||||
if (message.conf.letsencrypt) {
|
if (conf.lexConf) {
|
||||||
lex = createLe(message.conf);
|
lex = createLe(conf.lexConf, conf);
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTE that message.conf[x] will be overwritten when the next message comes in
|
// NOTE that message.conf[x] will be overwritten when the next message comes in
|
||||||
require('../lib/local-server').create(lex, message.conf.certPaths, message.conf.localPort, message, function (err, webserver) {
|
require('./local-server').create(lex, conf.certPaths, conf.localPort, conf, function (err, webserver) {
|
||||||
if (err) {
|
if (err) {
|
||||||
console.error('[ERROR] worker.js');
|
console.error('[ERROR] worker.js');
|
||||||
console.error(err.stack);
|
console.error(err.stack);
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.info("#" + id + " Listening on " + message.conf.protocol + "://" + webserver.address().address + ":" + webserver.address().port, '\n');
|
console.info("#" + id + " Listening on " + conf.protocol + "://" + webserver.address().address + ":" + webserver.address().port, '\n');
|
||||||
|
|
||||||
// we don't need time to pass, just to be able to return
|
// we don't need time to pass, just to be able to return
|
||||||
process.nextTick(function () {
|
process.nextTick(function () {
|
||||||
createAndBindInsecure(lex, message, cb);
|
createAndBindInsecure(lex, conf, getOrCreateHttpApp);
|
||||||
});
|
});
|
||||||
|
|
||||||
// we are returning the promise result to the caller
|
// we are returning the promise result to the caller
|
||||||
return cb(null, null, webserver, message);
|
return getOrCreateHttpApp(null, null, webserver, conf);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// Worker Mode
|
// Worker Mode
|
||||||
//
|
//
|
||||||
function waitForConfig(message) {
|
function waitForConfig(realMessage) {
|
||||||
if ('walnut.init' !== message.type) {
|
if ('walnut.init' !== realMessage.type) {
|
||||||
console.warn('[Worker] 0 got unexpected message:');
|
console.warn('[Worker] 0 got unexpected message:');
|
||||||
console.warn(message);
|
console.warn(realMessage);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var conf = realMessage.conf;
|
||||||
process.removeListener('message', waitForConfig);
|
process.removeListener('message', waitForConfig);
|
||||||
|
|
||||||
// NOTE: this callback must return a promise for an express app
|
// NOTE: this callback must return a promise for an express app
|
||||||
createAndBindServers(message, function (err, insecserver, webserver, oldMessage) {
|
|
||||||
// TODO deep merge new message into old message
|
function getExpressApp(err, insecserver, webserver/*, newMessage*/) {
|
||||||
Object.keys(message.conf).forEach(function (key) {
|
|
||||||
oldMessage.conf[key] = message.conf[key];
|
|
||||||
});
|
|
||||||
var PromiseA = require('bluebird');
|
var PromiseA = require('bluebird');
|
||||||
|
|
||||||
if (promiseApp) {
|
if (promiseApp) {
|
||||||
return promiseApp;
|
return promiseApp;
|
||||||
}
|
}
|
||||||
|
|
||||||
promiseApp = new PromiseA(function (resolve) {
|
promiseApp = new PromiseA(function (resolve) {
|
||||||
function initWebServer(srvmsg) {
|
function initHttpApp(srvmsg) {
|
||||||
if ('walnut.webserver.onrequest' !== srvmsg.type) {
|
if ('walnut.webserver.onrequest' !== srvmsg.type) {
|
||||||
console.warn('[Worker] 1 got unexpected message:');
|
console.warn('[Worker] [onrequest] unexpected message:');
|
||||||
console.warn(srvmsg);
|
console.warn(srvmsg);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
process.removeListener('message', initWebServer);
|
process.removeListener('message', initHttpApp);
|
||||||
|
|
||||||
resolve(require('../lib/worker').create(webserver, srvmsg.conf));
|
if (srvmsg.conf) {
|
||||||
|
Object.keys(srvmsg.conf).forEach(function (key) {
|
||||||
|
conf[key] = srvmsg.conf[key];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(require('../lib/worker').create(webserver, conf));
|
||||||
}
|
}
|
||||||
|
|
||||||
process.send({ type: 'walnut.webserver.listening' });
|
process.send({ type: 'walnut.webserver.listening' });
|
||||||
process.on('message', initWebServer);
|
process.on('message', initHttpApp);
|
||||||
}).then(function (app) {
|
}).then(function (app) {
|
||||||
console.info('[Worker Ready]');
|
console.info('[Worker Ready]');
|
||||||
return app;
|
return app;
|
||||||
});
|
});
|
||||||
|
|
||||||
return promiseApp;
|
return promiseApp;
|
||||||
});
|
}
|
||||||
|
|
||||||
|
createAndBindServers(conf, getExpressApp);
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
|
@ -124,11 +203,13 @@ module.exports.create = function (opts) {
|
||||||
//
|
//
|
||||||
if (opts) {
|
if (opts) {
|
||||||
// NOTE: this callback must return a promise for an express app
|
// NOTE: this callback must return a promise for an express app
|
||||||
createAndBindServers(opts, function (err, insecserver, webserver/*, message*/) {
|
createAndBindServers(opts, function (err, insecserver, webserver/*, conf*/) {
|
||||||
var PromiseA = require('bluebird');
|
var PromiseA = require('bluebird');
|
||||||
|
|
||||||
if (promiseApp) {
|
if (promiseApp) {
|
||||||
return promiseApp;
|
return promiseApp;
|
||||||
}
|
}
|
||||||
|
|
||||||
promiseApp = new PromiseA(function (resolve) {
|
promiseApp = new PromiseA(function (resolve) {
|
||||||
opts.getConfig(function (srvmsg) {
|
opts.getConfig(function (srvmsg) {
|
||||||
resolve(require('../lib/worker').create(webserver, srvmsg));
|
resolve(require('../lib/worker').create(webserver, srvmsg));
|
||||||
|
@ -137,6 +218,7 @@ module.exports.create = function (opts) {
|
||||||
console.info('[Standalone Ready]');
|
console.info('[Standalone Ready]');
|
||||||
return app;
|
return app;
|
||||||
});
|
});
|
||||||
|
|
||||||
return promiseApp;
|
return promiseApp;
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
|
20
install.sh
20
install.sh
|
@ -1,6 +1,6 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
sudo mkdir -p /srv/walnut/{certs,core,letsencrypt,lib}
|
sudo mkdir -p /srv/walnut/{certs,core,letsencrypt,lib,config}
|
||||||
sudo mkdir -p /srv/walnut/packages/{api,pages,services}
|
sudo mkdir -p /srv/walnut/packages/{api,pages,services}
|
||||||
sudo chown -R $(whoami):$(whoami) /srv/walnut
|
sudo chown -R $(whoami):$(whoami) /srv/walnut
|
||||||
|
|
||||||
|
@ -9,11 +9,23 @@ git clone https://github.com/Daplie/walnut.git /srv/walnut/core
|
||||||
|
|
||||||
pushd /srv/walnut/core
|
pushd /srv/walnut/core
|
||||||
npm install
|
npm install
|
||||||
sudo rsync -av /srv/walnut/core/etc/init/walnut.conf /etc/init/walnut.conf
|
|
||||||
rsync -av /srv/walnut/core/etc/letsencrypt/ /srv/walnut/certs/
|
|
||||||
|
|
||||||
popd
|
popd
|
||||||
|
|
||||||
|
sudo rsync -a /srv/walnut/core/etc/init/walnut.conf /etc/init/walnut.conf
|
||||||
|
rsync -a /srv/walnut/core/etc/letsencrypt/ /srv/walnut/certs/
|
||||||
mv /srv/walnut/core/node_modules /srv/walnut
|
mv /srv/walnut/core/node_modules /srv/walnut
|
||||||
|
|
||||||
|
echo -n "Enter an email address to use for LetsEncrypt and press [ENTER]: "
|
||||||
|
read LE_EMAIL
|
||||||
|
node -e "
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
require('fs').writeFileSync('/srv/walnut/config.letsencrypt.json', JSON.stringify({
|
||||||
|
configDir: '/srv/walnut/letsencrypt'
|
||||||
|
, email: '$LE_EMAIL'
|
||||||
|
, agreeTos: true
|
||||||
|
}, null, ' '));
|
||||||
|
"
|
||||||
|
|
||||||
sudo service walnut stop
|
sudo service walnut stop
|
||||||
sudo service walnut start
|
sudo service walnut start
|
||||||
|
|
|
@ -0,0 +1,250 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
module.exports.create = function (xconfx, apiFactories, apiDeps) {
|
||||||
|
var PromiseA = apiDeps.Promise;
|
||||||
|
var express = require('express');
|
||||||
|
var fs = PromiseA.promisifyAll(require('fs'));
|
||||||
|
var path = require('path');
|
||||||
|
var localCache = { apis: {}, pkgs: {} };
|
||||||
|
|
||||||
|
// TODO xconfx.apispath
|
||||||
|
xconfx.apispath = path.join(__dirname, '..', '..', 'packages', 'apis');
|
||||||
|
|
||||||
|
function notConfigured(req, res) {
|
||||||
|
res.send({ error: { message: "api '" + req.apiId + "' not configured for domain '" + req.experienceId + "'" } });
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadApi(conf, pkgConf, pkgDeps, packagedApi) {
|
||||||
|
function handlePromise(p) {
|
||||||
|
return p.then(function (api) {
|
||||||
|
packagedApi._api = api;
|
||||||
|
return api;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!packagedApi._promise_api) {
|
||||||
|
packagedApi._promise_api = getApi(conf, pkgConf, pkgDeps, packagedApi);
|
||||||
|
}
|
||||||
|
|
||||||
|
return handlePromise(packagedApi._promise_api);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getApi(conf, pkgConf, pkgDeps, packagedApi) {
|
||||||
|
var PromiseA = pkgDeps.Promise;
|
||||||
|
var path = require('path');
|
||||||
|
var pkgpath = path.join(pkgConf.apipath, packagedApi.id/*, (packagedApi.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 {
|
||||||
|
packagedApi._apipkg = require(path.join(pkgpath, 'package.json'));
|
||||||
|
packagedApi._apiname = packagedApi._apipkg.name;
|
||||||
|
if (packagedApi._apipkg.walnut) {
|
||||||
|
pkgpath += '/' + packagedApi._apipkg.walnut;
|
||||||
|
}
|
||||||
|
promise = PromiseA.resolve(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
|
||||||
|
// packagedApi._api = require(pkgpath).create(pkgConf, pkgDeps, myApp);
|
||||||
|
packagedApi._api = require('express-lazy')();
|
||||||
|
packagedApi._api_app = myApp;
|
||||||
|
|
||||||
|
//require('./oauth3-auth').inject(conf, packagedApi._api, pkgConf, pkgDeps);
|
||||||
|
pkgDeps.getOauth3Controllers =
|
||||||
|
packagedApi._getOauth3Controllers = require('oauthcommon/example-oauthmodels').create(conf).getControllers;
|
||||||
|
require('oauthcommon').inject(packagedApi._getOauth3Controllers, packagedApi._api, pkgConf, pkgDeps);
|
||||||
|
|
||||||
|
// DEBUG
|
||||||
|
//
|
||||||
|
/*
|
||||||
|
packagedApi._api.use('/', function (req, res, next) {
|
||||||
|
console.log('[DEBUG pkgApiApp]', req.method, req.hostname, req.url);
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
//*/
|
||||||
|
|
||||||
|
// TODO fix backwards compat
|
||||||
|
|
||||||
|
// /api/com.example.foo (no change)
|
||||||
|
packagedApi._api.use('/', packagedApi._api_app);
|
||||||
|
|
||||||
|
// /api/com.example.foo => /api
|
||||||
|
packagedApi._api.use('/', function (req, res, next) {
|
||||||
|
var priorUrl = req.url;
|
||||||
|
req.url = '/api' + req.url.slice(('/api/' + packagedApi.id).length);
|
||||||
|
// console.log('api mangle 3:', req.url);
|
||||||
|
packagedApi._api_app(req, res, function (err) {
|
||||||
|
req.url = priorUrl;
|
||||||
|
next(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// /api/com.example.foo => /
|
||||||
|
packagedApi._api.use('/api/' + packagedApi.id, function (req, res, next) {
|
||||||
|
// console.log('api mangle 2:', '/api/' + packagedApi.id, req.url);
|
||||||
|
// console.log(packagedApi._api_app.toString());
|
||||||
|
packagedApi._api_app(req, res, next);
|
||||||
|
});
|
||||||
|
|
||||||
|
resolve(packagedApi._api);
|
||||||
|
}, reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read packages/apis/sub.sld.tld (forward dns) to find list of apis as tld.sld.sub (reverse dns)
|
||||||
|
// TODO packages/allowed_apis/sub.sld.tld (?)
|
||||||
|
// TODO auto-register org.oauth3.consumer for primaryDomain (and all sites?)
|
||||||
|
function loadApiHandler() {
|
||||||
|
return function handler(req, res, next) {
|
||||||
|
var name = req.experienceId;
|
||||||
|
var apiId = req.apiId;
|
||||||
|
var packagepath = path.join(xconfx.apispath, name);
|
||||||
|
|
||||||
|
return fs.readFileAsync(packagepath, 'utf8').then(function (text) {
|
||||||
|
return text.trim().split(/\n/);
|
||||||
|
}, function () {
|
||||||
|
return [];
|
||||||
|
}).then(function (apis) {
|
||||||
|
return function (req, res, next) {
|
||||||
|
var apipath;
|
||||||
|
|
||||||
|
if (!apis.some(function (api) {
|
||||||
|
if (api === apiId) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
})) {
|
||||||
|
if (req.experienceId === ('api.' + xconfx.setupDomain) && 'org.oauth3.consumer' === apiId) {
|
||||||
|
// fallthrough
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
apipath = path.join(xconfx.apispath, apiId);
|
||||||
|
|
||||||
|
if (!localCache.pkgs[apiId]) {
|
||||||
|
return fs.readFileAsync(path.join(apipath, 'package.json'), 'utf8').then(function (text) {
|
||||||
|
var pkg = JSON.parse(text);
|
||||||
|
var deps = {};
|
||||||
|
var myApp;
|
||||||
|
|
||||||
|
if (pkg.walnut) {
|
||||||
|
apipath = path.join(apipath, pkg.walnut);
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.keys(apiDeps).forEach(function (key) {
|
||||||
|
deps[key] = apiDeps[key];
|
||||||
|
});
|
||||||
|
Object.keys(apiFactories).forEach(function (key) {
|
||||||
|
deps[key] = apiFactories[key];
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO pull db stuff from package.json somehow and pass allowed data models as deps
|
||||||
|
//
|
||||||
|
// how can we tell which of these would be correct?
|
||||||
|
// deps.memstore = apiFactories.memstoreFactory.create(apiId);
|
||||||
|
// deps.memstore = apiFactories.memstoreFactory.create(req.experienceId);
|
||||||
|
// deps.memstore = apiFactories.memstoreFactory.create(req.experienceId + apiId);
|
||||||
|
|
||||||
|
// let's go with this one for now and the api can choose to scope or not to scope
|
||||||
|
deps.memstore = apiFactories.memstoreFactory.create(apiId);
|
||||||
|
|
||||||
|
console.log('DEBUG apipath', apipath);
|
||||||
|
myApp = express();
|
||||||
|
//
|
||||||
|
// TODO handle /accounts/:accountId
|
||||||
|
//
|
||||||
|
return PromiseA.resolve(require(apipath).create({}/*pkgConf*/, deps/*pkgDeps*/, myApp/*myApp*/)).then(function (handler) {
|
||||||
|
localCache.pkgs[apiId] = { pkg: pkg, handler: handler || myApp, createdAt: Date.now() };
|
||||||
|
localCache.pkgs[apiId].handler(req, res, next);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
localCache.pkgs[apiId].handler(req, res, next);
|
||||||
|
// TODO expire require cache
|
||||||
|
/*
|
||||||
|
if (Date.now() - localCache.pkgs[apiId].createdAt < (5 * 60 * 1000)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, function (/*err*/) {
|
||||||
|
return null;
|
||||||
|
}).then(function (handler) {
|
||||||
|
|
||||||
|
// keep object reference intact
|
||||||
|
// DO NOT cache non-existant api
|
||||||
|
if (handler) {
|
||||||
|
localCache.apis[name].handler = handler;
|
||||||
|
} else {
|
||||||
|
handler = notConfigured;
|
||||||
|
}
|
||||||
|
handler(req, res, next);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return function (req, res, next) {
|
||||||
|
var experienceId = req.hostname + req.url.replace(/\/api\/.*/, '/').replace(/\/+/g, '#').replace(/#$/, '');
|
||||||
|
var apiId = req.url.replace(/.*\/api\//, '').replace(/\/.*/, '');
|
||||||
|
|
||||||
|
Object.defineProperty(req, 'experienceId', {
|
||||||
|
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?)
|
||||||
|
// NOTE: probably best to alias the name logically
|
||||||
|
, value: experienceId
|
||||||
|
});
|
||||||
|
Object.defineProperty(req, 'apiId', {
|
||||||
|
enumerable: true
|
||||||
|
, configurable: false
|
||||||
|
, writable: false
|
||||||
|
, value: apiId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!localCache.apis[experienceId]) {
|
||||||
|
localCache.apis[experienceId] = { handler: loadApiHandler(experienceId), createdAt: Date.now() };
|
||||||
|
}
|
||||||
|
|
||||||
|
localCache.apis[experienceId].handler(req, res, next);
|
||||||
|
if (Date.now() - localCache.apis[experienceId].createdAt > (5 * 60 * 1000)) {
|
||||||
|
localCache.apis[experienceId] = { handler: loadApiHandler(experienceId), createdAt: Date.now() };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
|
@ -0,0 +1,306 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
//
|
||||||
|
// IMPORTANT !!!
|
||||||
|
//
|
||||||
|
// None of this is authenticated or encrypted
|
||||||
|
//
|
||||||
|
|
||||||
|
module.exports.create = function (app, xconfx, models) {
|
||||||
|
var PromiseA = require('bluebird');
|
||||||
|
var path = require('path');
|
||||||
|
var fs = PromiseA.promisifyAll(require('fs'));
|
||||||
|
var dns = PromiseA.promisifyAll(require('dns'));
|
||||||
|
|
||||||
|
function isInitialized() {
|
||||||
|
// TODO read from file only, not db
|
||||||
|
return models.ComDaplieWalnutConfig.get('config').then(function (conf) {
|
||||||
|
if (!conf || !conf.primaryDomain || !conf.primaryEmail) {
|
||||||
|
console.log('DEBUG incomplete conf', conf);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
xconfx.primaryDomain = xconfx.primaryDomain || conf.primaryDomain;
|
||||||
|
|
||||||
|
var configname = conf.primaryDomain + '.json';
|
||||||
|
var configpath = path.join(__dirname, '..', '..', 'config', configname);
|
||||||
|
|
||||||
|
return fs.readFileAsync(configpath, 'utf8').then(function (text) {
|
||||||
|
return JSON.parse(text);
|
||||||
|
}, function (/*err*/) {
|
||||||
|
console.log('DEBUG not exists leconf', configpath);
|
||||||
|
return false;
|
||||||
|
}).then(function (data) {
|
||||||
|
if (!data || !data.email || !data.agreeTos) {
|
||||||
|
console.log('DEBUG incomplete leconf', data);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function initialize() {
|
||||||
|
var express = require('express');
|
||||||
|
var getIpAddresses = require('./ip-checker').getExternalAddresses;
|
||||||
|
var resolve;
|
||||||
|
|
||||||
|
function errorIfNotApi(req, res, next) {
|
||||||
|
// if it's not an ip address
|
||||||
|
if (/[a-z]+/.test(req.headers.host)) {
|
||||||
|
if (!/^api\./.test(req.headers.host)) {
|
||||||
|
console.log('req.headers.host');
|
||||||
|
console.log(req.headers.host);
|
||||||
|
res.send({ error: { message: "no api. subdomain prefix" } });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
|
||||||
|
function errorIfApi(req, res, next) {
|
||||||
|
if (!/^api\./.test(req.headers.host)) {
|
||||||
|
next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// has api. hostname prefix
|
||||||
|
|
||||||
|
// doesn't have /api url prefix
|
||||||
|
if (!/^\/api\//.test(req.url)) {
|
||||||
|
res.send({ error: { message: "missing /api/ url prefix" } });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.send({ error: { code: 'E_NO_IMPL', message: "not implemented" } });
|
||||||
|
}
|
||||||
|
|
||||||
|
function getConfig(req, res) {
|
||||||
|
getIpAddresses().then(function (inets) {
|
||||||
|
var results = {
|
||||||
|
hostname: require('os').hostname()
|
||||||
|
, inets: inets.addresses.map(function (a) {
|
||||||
|
a.time = undefined;
|
||||||
|
return a;
|
||||||
|
})
|
||||||
|
};
|
||||||
|
//res.send({ inets: require('os').networkInterfaces() });
|
||||||
|
res.send(results);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function verifyIps(inets, hostname) {
|
||||||
|
var map = {};
|
||||||
|
var arr = [];
|
||||||
|
|
||||||
|
inets.forEach(function (addr) {
|
||||||
|
if (!map[addr.family]) {
|
||||||
|
map[addr.family] = true;
|
||||||
|
if (4 === addr.family) {
|
||||||
|
arr.push(dns.resolve4Async(hostname).then(function (arr) {
|
||||||
|
return arr;
|
||||||
|
}, function (/*err*/) {
|
||||||
|
return [];
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
if (6 === addr.family) {
|
||||||
|
arr.push(dns.resolve6Async(hostname).then(function (arr) {
|
||||||
|
return arr;
|
||||||
|
}, function (/*err*/) {
|
||||||
|
return [];
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return PromiseA.all(arr).then(function (fams) {
|
||||||
|
console.log('DEBUG hostname', hostname);
|
||||||
|
var ips = [];
|
||||||
|
|
||||||
|
fams.forEach(function (addrs) {
|
||||||
|
console.log('DEBUG ipv46');
|
||||||
|
console.log(addrs);
|
||||||
|
addrs.forEach(function (addr) {
|
||||||
|
inets.forEach(function (a) {
|
||||||
|
if (a.address === addr) {
|
||||||
|
a.time = undefined;
|
||||||
|
ips.push(a);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
console.log('');
|
||||||
|
});
|
||||||
|
|
||||||
|
return ips;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function setConfig(req, res) {
|
||||||
|
var config = req.body;
|
||||||
|
var results = {};
|
||||||
|
|
||||||
|
return PromiseA.resolve().then(function () {
|
||||||
|
if (!config.agreeTos && !config.tls) {
|
||||||
|
return PromiseA.reject(new Error("To enable encryption you must agree to the LetsEncrypt terms of service"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!config.domain) {
|
||||||
|
return PromiseA.reject(new Error("You must specify a valid domain name"));
|
||||||
|
}
|
||||||
|
config.domain = config.domain.replace(/^www\./, '');
|
||||||
|
|
||||||
|
return getIpAddresses().then(function (inet) {
|
||||||
|
if (!inet.addresses.length) {
|
||||||
|
return PromiseA.reject(new Error("no ip addresses"));
|
||||||
|
}
|
||||||
|
|
||||||
|
results.inets = inet.addresses.map(function (a) {
|
||||||
|
a.time = undefined;
|
||||||
|
return a;
|
||||||
|
});
|
||||||
|
|
||||||
|
results.resolutions = [];
|
||||||
|
return PromiseA.all([
|
||||||
|
// for static content
|
||||||
|
verifyIps(inet.addresses, config.domain).then(function (ips) {
|
||||||
|
results.resolutions.push({ hostname: config.domain, ips: ips });
|
||||||
|
})
|
||||||
|
// for redirects
|
||||||
|
, verifyIps(inet.addresses, 'www.' + config.domain).then(function (ips) {
|
||||||
|
results.resolutions.push({ hostname: 'www.' + config.domain, ips: ips });
|
||||||
|
})
|
||||||
|
// for api
|
||||||
|
, verifyIps(inet.addresses, 'api.' + config.domain).then(function (ips) {
|
||||||
|
results.resolutions.push({ hostname: 'api.' + config.domain, ips: ips });
|
||||||
|
})
|
||||||
|
// for protected assets
|
||||||
|
, verifyIps(inet.addresses, 'assets.' + config.domain).then(function (ips) {
|
||||||
|
results.resolutions.push({ hostname: 'assets.' + config.domain, ips: ips });
|
||||||
|
})
|
||||||
|
// for the cloud management
|
||||||
|
, verifyIps(inet.addresses, 'cloud.' + config.domain).then(function (ips) {
|
||||||
|
results.resolutions.push({ hostname: 'cloud.' + config.domain, ips: ips });
|
||||||
|
})
|
||||||
|
, verifyIps(inet.addresses, 'api.cloud.' + config.domain).then(function (ips) {
|
||||||
|
results.resolutions.push({ hostname: 'api.cloud.' + config.domain, ips: ips });
|
||||||
|
})
|
||||||
|
]).then(function () {
|
||||||
|
if (!results.resolutions[0].ips.length) {
|
||||||
|
results.error = { message: "bare domain could not be resolved to this device" };
|
||||||
|
}
|
||||||
|
else if (!results.resolutions[2].ips.length) {
|
||||||
|
results.error = { message: "api subdomain could not be resolved to this device" };
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
else if (!results.resolutions[1].ips.length) {
|
||||||
|
results.error = { message: "" }
|
||||||
|
}
|
||||||
|
else if (!results.resolutions[3].ips.length) {
|
||||||
|
results.error = { message: "" }
|
||||||
|
}
|
||||||
|
else if (!results.resolutions[4].ips.length || !results.resolutions[4].ips.length) {
|
||||||
|
results.error = { message: "cloud and api.cloud subdomains should be set up" };
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}).then(function () {
|
||||||
|
if (results.error) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var configname = config.domain + '.json';
|
||||||
|
var configpath = path.join(__dirname, '..', '..', 'config', configname);
|
||||||
|
var leAuth = {
|
||||||
|
agreeTos: true
|
||||||
|
, email: config.email // TODO check email
|
||||||
|
, domain: config.domain
|
||||||
|
, createdAt: Date.now()
|
||||||
|
};
|
||||||
|
|
||||||
|
return dns.resolveMxAsync(config.email.replace(/.*@/, '')).then(function (/*addrs*/) {
|
||||||
|
// TODO allow private key to be uploaded
|
||||||
|
return fs.writeFileAsync(configpath, JSON.stringify(leAuth, null, ' '), 'utf8').then(function () {
|
||||||
|
return models.ComDaplieWalnutConfig.upsert('config', {
|
||||||
|
letsencrypt: leAuth
|
||||||
|
, primaryDomain: config.domain
|
||||||
|
, primaryEmail: config.email
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}, function () {
|
||||||
|
return PromiseA.reject(new Error("invalid email address (MX record lookup failed)"));
|
||||||
|
});
|
||||||
|
}).then(function () {
|
||||||
|
if (!results.error && results.inets && resolve) {
|
||||||
|
resolve();
|
||||||
|
resolve = null;
|
||||||
|
}
|
||||||
|
res.send(results);
|
||||||
|
}, function (err) {
|
||||||
|
console.error('Error lib/bootstrap.js');
|
||||||
|
console.error(err.stack || err);
|
||||||
|
res.send({ error: { message: err.message || err.toString() } });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var CORS = require('connect-cors');
|
||||||
|
var cors = CORS({ credentials: true, headers: [
|
||||||
|
'X-Requested-With'
|
||||||
|
, 'X-HTTP-Method-Override'
|
||||||
|
, 'Content-Type'
|
||||||
|
, 'Accept'
|
||||||
|
, 'Authorization'
|
||||||
|
], methods: [ "GET", "POST", "PATCH", "PUT", "DELETE" ] });
|
||||||
|
|
||||||
|
app.use('/', function (req, res, next) {
|
||||||
|
return isInitialized().then(function (initialized) {
|
||||||
|
if (!initialized) {
|
||||||
|
next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(true);
|
||||||
|
|
||||||
|
// force page refresh
|
||||||
|
// TODO goto top of routes?
|
||||||
|
res.statusCode = 302;
|
||||||
|
res.setHeader('Location', req.url);
|
||||||
|
res.end();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
app.use('/api', errorIfNotApi);
|
||||||
|
// NOTE Allows CORS access to API with ?access_token=
|
||||||
|
// TODO Access-Control-Max-Age: 600
|
||||||
|
// TODO How can we help apps handle this? token?
|
||||||
|
// TODO allow apps to configure trustedDomains, auth, etc
|
||||||
|
app.use('/api', cors);
|
||||||
|
app.get('/api/com.daplie.walnut.init', getConfig);
|
||||||
|
app.post('/api/com.daplie.walnut.init', setConfig);
|
||||||
|
app.use('/', errorIfApi);
|
||||||
|
app.use('/', express.static(path.join(__dirname, '..', '..', 'packages', 'pages', 'com.daplie.walnut.init')));
|
||||||
|
|
||||||
|
return new PromiseA(function (_resolve) {
|
||||||
|
resolve = _resolve;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return isInitialized().then(function (initialized) {
|
||||||
|
if (initialized) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return initialize();
|
||||||
|
}, function (err) {
|
||||||
|
console.error('FATAL ERROR:');
|
||||||
|
console.error(err.stack || err);
|
||||||
|
app.use('/', function (req, res) {
|
||||||
|
res.send({
|
||||||
|
error: {
|
||||||
|
message: "Unrecoverable Error Requires manual server update: " + (err.message || err.toString())
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
|
@ -1,113 +0,0 @@
|
||||||
'use strict';
|
|
||||||
|
|
||||||
module.exports.create = function (lex, securePort, insecurePort, info, serverCallback) {
|
|
||||||
var PromiseA = require('bluebird').Promise;
|
|
||||||
var appPromise;
|
|
||||||
//var app;
|
|
||||||
var http = require('http');
|
|
||||||
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) {
|
|
||||||
if (req.headers.host && /^\/.well-known\/acme-challenge/.test(req.url) && useAppInsecurely(req, res)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
// 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.redirects || info.conf.redirects) {
|
|
||||||
redirectives = require('./hostname-redirects').compile(info.redirects || info.conf.redirects);
|
|
||||||
}
|
|
||||||
if (require('./no-www').scrubTheDub(req, res, redirectives)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Let it do this once they visit the https site
|
|
||||||
// res.setHeader('Strict-Transport-Security', 'max-age=10886400; includeSubDomains; preload');
|
|
||||||
|
|
||||||
var host = req.headers.host || '';
|
|
||||||
var url = req.url;
|
|
||||||
|
|
||||||
// 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?
|
|
||||||
|
|
||||||
var escapeHtml = require('escape-html');
|
|
||||||
var newLocation = 'https://'
|
|
||||||
+ host.replace(/:\d+/, ':' + securePort) + url
|
|
||||||
;
|
|
||||||
var safeLocation = escapeHtml(newLocation);
|
|
||||||
|
|
||||||
var metaRedirect = ''
|
|
||||||
+ '<html>\n'
|
|
||||||
+ '<head>\n'
|
|
||||||
+ ' <style>* { background-color: white; color: white; text-decoration: none; }</style>\n'
|
|
||||||
+ ' <META http-equiv="refresh" content="0;URL=' + safeLocation + '">\n'
|
|
||||||
+ '</head>\n'
|
|
||||||
+ '<body style="display: none;">\n'
|
|
||||||
+ ' <p>You requested an insecure resource. Please use this instead: \n'
|
|
||||||
+ ' <a href="' + safeLocation + '">' + safeLocation + '</a></p>\n'
|
|
||||||
+ '</body>\n'
|
|
||||||
+ '</html>\n'
|
|
||||||
;
|
|
||||||
|
|
||||||
// DO NOT HTTP REDIRECT
|
|
||||||
/*
|
|
||||||
res.setHeader('Location', newLocation);
|
|
||||||
res.statusCode = 302;
|
|
||||||
*/
|
|
||||||
|
|
||||||
// BAD NEWS BEARS
|
|
||||||
//
|
|
||||||
// When people are experimenting with the API and posting tutorials
|
|
||||||
// they'll use cURL and they'll forget to prefix with https://
|
|
||||||
// If we allow that, then many users will be sending private tokens
|
|
||||||
// and such with POSTs in clear text and, worse, it will work!
|
|
||||||
// To minimize this, we give browser users a mostly optimal experience,
|
|
||||||
// but people experimenting with the API get a message letting them know
|
|
||||||
// that they're doing it wrong and thus forces them to ensure they encrypt.
|
|
||||||
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
||||||
res.end(metaRedirect);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO localhost-only server shutdown mechanism
|
|
||||||
// that closes all sockets, waits for them to finish,
|
|
||||||
// and then hands control over completely to respawned server
|
|
||||||
|
|
||||||
//
|
|
||||||
// Redirect HTTP to HTTPS
|
|
||||||
//
|
|
||||||
// This simply redirects from the current insecure location to the encrypted location
|
|
||||||
//
|
|
||||||
var insecureServer;
|
|
||||||
insecureServer = http.createServer();
|
|
||||||
insecureServer.listen(insecurePort, function () {
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (lex) {
|
|
||||||
var LEX = require('letsencrypt-express');
|
|
||||||
insecureServer.on('request', LEX.createAcmeResponder(lex, redirectHttps));
|
|
||||||
} else {
|
|
||||||
insecureServer.on('request', redirectHttps);
|
|
||||||
}
|
|
||||||
|
|
||||||
return PromiseA.resolve(insecureServer);
|
|
||||||
};
|
|
|
@ -0,0 +1,138 @@
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
var PromiseA = require('bluebird').Promise;
|
||||||
|
var ifaces = require('os').networkInterfaces();
|
||||||
|
var dns = PromiseA.promisifyAll(require('dns'));
|
||||||
|
var https = require('https');
|
||||||
|
|
||||||
|
function getExternalAddresses() {
|
||||||
|
var iftypes = {};
|
||||||
|
var ipv4check = 'api.ipify.org';
|
||||||
|
var ipv6check = 'myexternalip.com';
|
||||||
|
|
||||||
|
Object.keys(ifaces).forEach(function (ifname) {
|
||||||
|
ifaces[ifname].forEach(function (iface) {
|
||||||
|
// local addresses
|
||||||
|
if (iface.internal) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// auto address space
|
||||||
|
if (/^(fe80:|169\.)/.test(iface.address)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
if (/^(fe80:|10\.|192\.168|172\.1[6-9]|172\.2[0-9]|172\.3[0-1])/.test(iface.address)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
iftypes[iface.family] = true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(iftypes);
|
||||||
|
|
||||||
|
var now = Date.now();
|
||||||
|
|
||||||
|
return PromiseA.all([
|
||||||
|
dns.lookupAsync(ipv4check, { family: 4/*, all: true*/ }).then(function (ans) {
|
||||||
|
iftypes.IPv4 = { address: ans[0], family: ans[1], time: Date.now() - now };
|
||||||
|
}).error(function () {
|
||||||
|
//console.log('no ipv4', Date.now() - now);
|
||||||
|
iftypes.IPv4 = false;
|
||||||
|
})
|
||||||
|
// curl -6 https://myexternalip.com/raw
|
||||||
|
, dns.lookupAsync(ipv6check, { family: 6/*, all: true*/ }).then(function (ans) {
|
||||||
|
iftypes.IPv6 = { address: ans[0], family: ans[1], time: Date.now() - now };
|
||||||
|
}).error(function (err) {
|
||||||
|
console.error('Error ip-checker.js');
|
||||||
|
console.error(err.stack || err);
|
||||||
|
//console.log('no ipv6', Date.now() - now);
|
||||||
|
iftypes.IPv6 = false;
|
||||||
|
})
|
||||||
|
]).then(function () {
|
||||||
|
var requests = [];
|
||||||
|
|
||||||
|
if (iftypes.IPv4) {
|
||||||
|
requests.push(new PromiseA(function (resolve) {
|
||||||
|
var req = https.request({
|
||||||
|
method: 'GET'
|
||||||
|
, hostname: iftypes.IPv4.address
|
||||||
|
, port: 443
|
||||||
|
, headers: {
|
||||||
|
Host: ipv4check
|
||||||
|
}
|
||||||
|
, path: '/'
|
||||||
|
//, family: 4
|
||||||
|
// TODO , localAddress: <<external_ipv4>>
|
||||||
|
}, function (res) {
|
||||||
|
var result = '';
|
||||||
|
|
||||||
|
res.on('error', function (/*err*/) {
|
||||||
|
resolve(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
res.on('data', function (chunk) {
|
||||||
|
result += chunk.toString('utf8');
|
||||||
|
});
|
||||||
|
|
||||||
|
res.on('end', function () {
|
||||||
|
resolve({ address: result, family: 4/*, wan: result === iftypes.IPv4.localAddress*/, time: iftypes.IPv4.time });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
req.on('error', function () {
|
||||||
|
resolve(null);
|
||||||
|
});
|
||||||
|
req.end();
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (iftypes.IPv6) {
|
||||||
|
requests.push(new PromiseA(function (resolve) {
|
||||||
|
var req = https.request({
|
||||||
|
method: 'GET'
|
||||||
|
, hostname: iftypes.IPv6.address
|
||||||
|
, port: 443
|
||||||
|
, headers: {
|
||||||
|
Host: ipv6check
|
||||||
|
}
|
||||||
|
, path: '/raw'
|
||||||
|
//, family: 6
|
||||||
|
// TODO , localAddress: <<external_ipv6>>
|
||||||
|
}, function (res) {
|
||||||
|
var result = '';
|
||||||
|
|
||||||
|
res.on('error', function (/*err*/) {
|
||||||
|
resolve(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
res.on('data', function (chunk) {
|
||||||
|
result += chunk.toString('utf8').trim();
|
||||||
|
});
|
||||||
|
res.on('end', function () {
|
||||||
|
resolve({ address: result, family: 6/*, wan: result === iftypes.IPv6.localAaddress*/, time: iftypes.IPv4.time });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
req.on('error', function () {
|
||||||
|
resolve(null);
|
||||||
|
});
|
||||||
|
req.end();
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
return PromiseA.all(requests).then(function (ips) {
|
||||||
|
ips = ips.filter(function (ip) {
|
||||||
|
return ip;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
addresses: ips
|
||||||
|
, time: Date.now() - now
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.getExternalAddresses = getExternalAddresses;
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
// Note the odd use of callbacks (instead of promises) here
|
// Note the odd use of callbacks (instead of promises) here
|
||||||
// It's to avoid loading bluebird yet (see sni-server.js for explanation)
|
// It's to avoid loading bluebird yet (see sni-server.js for explanation)
|
||||||
module.exports.create = function (lex, certPaths, port, info, serverCallback) {
|
module.exports.create = function (certPaths, port, info, serverCallback) {
|
||||||
function initServer(err, server) {
|
function initServer(err, server) {
|
||||||
var app;
|
var app;
|
||||||
var promiseApp;
|
var promiseApp;
|
||||||
|
@ -29,7 +29,7 @@ module.exports.create = function (lex, certPaths, port, info, serverCallback) {
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Get up and listening as absolutely quickly as possible
|
// Get up and listening as absolutely quickly as possible
|
||||||
function onRequest(req, res) {
|
server.on('request', function (req, res) {
|
||||||
// this is a hot piece of code, so we cache the result
|
// this is a hot piece of code, so we cache the result
|
||||||
if (app) {
|
if (app) {
|
||||||
app(req, res);
|
app(req, res);
|
||||||
|
@ -41,18 +41,11 @@ module.exports.create = function (lex, certPaths, port, info, serverCallback) {
|
||||||
app = _app;
|
app = _app;
|
||||||
app(req, res);
|
app(req, res);
|
||||||
});
|
});
|
||||||
}
|
});
|
||||||
|
|
||||||
if (lex) {
|
|
||||||
var LEX = require('letsencrypt-express');
|
|
||||||
server.on('request', LEX.createAcmeResponder(lex, onRequest));
|
|
||||||
} else {
|
|
||||||
server.on('request', onRequest);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (certPaths) {
|
if (certPaths) {
|
||||||
require('./sni-server').create(lex, certPaths, initServer);
|
require('./sni-server').create(certPaths, initServer);
|
||||||
} else {
|
} else {
|
||||||
initServer(null, require('http').createServer());
|
initServer(null, require('http').createServer());
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,297 @@
|
||||||
|
'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;
|
||||||
|
|
||||||
|
function redirectHttpsHelper(req, res) {
|
||||||
|
var host = req.hostname || req.headers.host || '';
|
||||||
|
var url = req.url;
|
||||||
|
|
||||||
|
// 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?
|
||||||
|
|
||||||
|
var escapeHtml = require('escape-html');
|
||||||
|
var newLocation = 'https://'
|
||||||
|
+ host.replace(/:\d+/, ':' + xconfx.externalPort) + url
|
||||||
|
;
|
||||||
|
var safeLocation = escapeHtml(newLocation);
|
||||||
|
|
||||||
|
var metaRedirect = ''
|
||||||
|
+ '<html>\n'
|
||||||
|
+ '<head>\n'
|
||||||
|
+ ' <style>* { background-color: white; color: white; text-decoration: none; }</style>\n'
|
||||||
|
+ ' <META http-equiv="refresh" content="0;URL=' + safeLocation + '">\n'
|
||||||
|
+ '</head>\n'
|
||||||
|
+ '<body style="display: none;">\n'
|
||||||
|
+ ' <p>You requested an insecure resource. Please use this instead: \n'
|
||||||
|
+ ' <a href="' + safeLocation + '">' + safeLocation + '</a></p>\n'
|
||||||
|
+ '</body>\n'
|
||||||
|
+ '</html>\n'
|
||||||
|
;
|
||||||
|
|
||||||
|
// DO NOT HTTP REDIRECT
|
||||||
|
/*
|
||||||
|
res.setHeader('Location', newLocation);
|
||||||
|
res.statusCode = 302;
|
||||||
|
*/
|
||||||
|
|
||||||
|
// BAD NEWS BEARS
|
||||||
|
//
|
||||||
|
// When people are experimenting with the API and posting tutorials
|
||||||
|
// they'll use cURL and they'll forget to prefix with https://
|
||||||
|
// If we allow that, then many users will be sending private tokens
|
||||||
|
// and such with POSTs in clear text and, worse, it will work!
|
||||||
|
// To minimize this, we give browser users a mostly optimal experience,
|
||||||
|
// but people experimenting with the API get a message letting them know
|
||||||
|
// that they're doing it wrong and thus forces them to ensure they encrypt.
|
||||||
|
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
||||||
|
res.end(metaRedirect);
|
||||||
|
}
|
||||||
|
|
||||||
|
function redirectHttps(req, res) {
|
||||||
|
if (localCache.le[req.hostname]) {
|
||||||
|
if (localCache.le[req.hostname].conf) {
|
||||||
|
redirectHttpsHelper(req, res);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// TODO needs IPC to expire cache
|
||||||
|
redirectSetup(req.hostname, req, res);
|
||||||
|
return;
|
||||||
|
/*
|
||||||
|
if (Date.now() - localCache.le[req.hostname].createdAt < (5 * 60 * 1000)) {
|
||||||
|
// TODO link to dbconf.primaryDomain
|
||||||
|
res.send({ error: { message: "Security Error: Encryption for '" + req.hostname + "' has not been configured."
|
||||||
|
+ " Please use the management interface to set up ACME / Let's Encrypt (or another solution)." } });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return xconfx.walkLe(req.hostname).then(function (leAuth) {
|
||||||
|
if (!leAuth) {
|
||||||
|
redirectSetup(req.hostname, req, res);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
localCache.le[req.hostname] = { conf: leAuth, createdAt: Date.now() };
|
||||||
|
redirectHttps(req, res);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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(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 redirectSetup(reason, req, res/*, next*/) {
|
||||||
|
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 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;
|
||||||
|
|
||||||
|
if (!req.secure) {
|
||||||
|
// did not come from https
|
||||||
|
if (/\.(appcache|manifest)\b/.test(req.url)) {
|
||||||
|
require('./unbrick-appcache').unbrick(req, res);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return redirectHttps(req, res);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
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();
|
||||||
|
};
|
|
@ -2,18 +2,17 @@
|
||||||
|
|
||||||
var cluster = require('cluster');
|
var cluster = require('cluster');
|
||||||
var PromiseA = require('bluebird');
|
var PromiseA = require('bluebird');
|
||||||
// TODO
|
|
||||||
// var rootMasterKey;
|
|
||||||
|
|
||||||
function init(conf, state) {
|
function init(conf, state) {
|
||||||
|
var newConf = {};
|
||||||
if (!conf.ipcKey) {
|
if (!conf.ipcKey) {
|
||||||
conf.ipcKey = require('crypto').randomBytes(16).toString('base64');
|
conf.ipcKey = newConf.ipcKey = require('crypto').randomBytes(16).toString('base64');
|
||||||
}
|
}
|
||||||
if (!conf.sqlite3Sock) {
|
if (!conf.sqlite3Sock) {
|
||||||
conf.sqlite3Sock = '/tmp/sqlite3.' + require('crypto').randomBytes(4).toString('hex') + '.sock';
|
conf.sqlite3Sock = newConf.sqlite3Sock = '/tmp/sqlite3.' + require('crypto').randomBytes(4).toString('hex') + '.sock';
|
||||||
}
|
}
|
||||||
if (!conf.memstoreSock) {
|
if (!conf.memstoreSock) {
|
||||||
conf.memstoreSock = '/tmp/memstore.' + require('crypto').randomBytes(4).toString('hex') + '.sock';
|
conf.memstoreSock = newConf.memstoreSock = '/tmp/memstore.' + require('crypto').randomBytes(4).toString('hex') + '.sock';
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -49,15 +48,14 @@ function init(conf, state) {
|
||||||
verbose: null
|
verbose: null
|
||||||
, sock: conf.sqlite3Sock
|
, sock: conf.sqlite3Sock
|
||||||
, ipcKey: conf.ipcKey
|
, ipcKey: conf.ipcKey
|
||||||
})
|
})/*.then(function () {
|
||||||
|
var sqlite3 = require('sqlite3-cluster/client');
|
||||||
|
return sqliet3.createClientFactory(...);
|
||||||
|
})*/
|
||||||
]).then(function (args) {
|
]).then(function (args) {
|
||||||
state.memstore = args[0];
|
state.memstore = args[0];
|
||||||
state.sqlstore = args[1];
|
//state.sqlstore = args[1];
|
||||||
return {
|
return newConf;
|
||||||
conf: conf
|
|
||||||
, memstore: args[0]
|
|
||||||
, sqlstore: args[1]
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return promise;
|
return promise;
|
||||||
|
@ -69,10 +67,10 @@ function touch(conf, state) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO if no xyz worker, start on xyz worker (unlock, for example)
|
// TODO if no xyz worker, start on xyz worker (unlock, for example)
|
||||||
return state.initialize.then(function () {
|
return state.initialize.then(function (newConf) {
|
||||||
// TODO conf.locked = true|false;
|
// TODO conf.locked = true|false;
|
||||||
conf.initialized = true;
|
conf.initialized = true;
|
||||||
return conf;
|
return newConf;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,215 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var escapeStringRegexp = require('escape-string-regexp');
|
||||||
|
//var apiHandlers = {};
|
||||||
|
|
||||||
|
function getApi(conf, pkgConf, pkgDeps, packagedApi) {
|
||||||
|
var PromiseA = pkgDeps.Promise;
|
||||||
|
var path = require('path');
|
||||||
|
var pkgpath = path.join(pkgConf.apipath, packagedApi.id/*, (packagedApi.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 {
|
||||||
|
packagedApi._apipkg = require(path.join(pkgpath, 'package.json'));
|
||||||
|
packagedApi._apiname = packagedApi._apipkg.name;
|
||||||
|
if (packagedApi._apipkg.walnut) {
|
||||||
|
pkgpath += '/' + packagedApi._apipkg.walnut;
|
||||||
|
}
|
||||||
|
promise = PromiseA.resolve(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
|
||||||
|
// packagedApi._api = require(pkgpath).create(pkgConf, pkgDeps, myApp);
|
||||||
|
packagedApi._api = require('express-lazy')();
|
||||||
|
packagedApi._api_app = myApp;
|
||||||
|
|
||||||
|
//require('./oauth3-auth').inject(conf, packagedApi._api, pkgConf, pkgDeps);
|
||||||
|
pkgDeps.getOauth3Controllers =
|
||||||
|
packagedApi._getOauth3Controllers = require('oauthcommon/example-oauthmodels').create(conf).getControllers;
|
||||||
|
require('oauthcommon').inject(packagedApi._getOauth3Controllers, packagedApi._api, pkgConf, pkgDeps);
|
||||||
|
|
||||||
|
// DEBUG
|
||||||
|
//
|
||||||
|
/*
|
||||||
|
packagedApi._api.use('/', function (req, res, next) {
|
||||||
|
console.log('[DEBUG pkgApiApp]', req.method, req.hostname, req.url);
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
//*/
|
||||||
|
|
||||||
|
// TODO fix backwards compat
|
||||||
|
|
||||||
|
// /api/com.example.foo (no change)
|
||||||
|
packagedApi._api.use('/', packagedApi._api_app);
|
||||||
|
|
||||||
|
// /api/com.example.foo => /api
|
||||||
|
packagedApi._api.use('/', function (req, res, next) {
|
||||||
|
var priorUrl = req.url;
|
||||||
|
req.url = '/api' + req.url.slice(('/api/' + packagedApi.id).length);
|
||||||
|
// console.log('api mangle 3:', req.url);
|
||||||
|
packagedApi._api_app(req, res, function (err) {
|
||||||
|
req.url = priorUrl;
|
||||||
|
next(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// /api/com.example.foo => /
|
||||||
|
packagedApi._api.use('/api/' + packagedApi.id, function (req, res, next) {
|
||||||
|
// console.log('api mangle 2:', '/api/' + packagedApi.id, req.url);
|
||||||
|
// console.log(packagedApi._api_app.toString());
|
||||||
|
packagedApi._api_app(req, res, next);
|
||||||
|
});
|
||||||
|
|
||||||
|
resolve(packagedApi._api);
|
||||||
|
}, reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadApi(conf, pkgConf, pkgDeps, packagedApi) {
|
||||||
|
function handlePromise(p) {
|
||||||
|
return p.then(function (api) {
|
||||||
|
packagedApi._api = api;
|
||||||
|
return api;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!packagedApi._promise_api) {
|
||||||
|
packagedApi._promise_api = getApi(conf, pkgConf, pkgDeps, packagedApi);
|
||||||
|
}
|
||||||
|
|
||||||
|
return handlePromise(packagedApi._promise_api);
|
||||||
|
}
|
||||||
|
|
||||||
|
function runApi(opts, router, req, res, next) {
|
||||||
|
var path = require('path');
|
||||||
|
var pkgConf = opts.config;
|
||||||
|
var pkgDeps = opts.deps;
|
||||||
|
//var Services = opts.Services;
|
||||||
|
var packagedApi;
|
||||||
|
var pathname;
|
||||||
|
|
||||||
|
// TODO compile packagesMap
|
||||||
|
// TODO people may want to use the framework in a non-framework way (i.e. to conceal the module name)
|
||||||
|
router.packagedApis.some(function (_packagedApi) {
|
||||||
|
// console.log('[DEBUG _packagedApi.id]', _packagedApi.id);
|
||||||
|
pathname = router.pathname;
|
||||||
|
if ('/' === pathname) {
|
||||||
|
pathname = '';
|
||||||
|
}
|
||||||
|
// TODO allow for special apis that do not follow convention (.well_known, webfinger, oauth3.html, etc)
|
||||||
|
if (!_packagedApi._api_re) {
|
||||||
|
_packagedApi._api_re = new RegExp(escapeStringRegexp(pathname + '/api/' + _packagedApi.id) + '\/([\\w\\.\\-]+)(\\/|\\?|$)');
|
||||||
|
//console.log('[api re 2]', _packagedApi._api_re);
|
||||||
|
}
|
||||||
|
if (_packagedApi._api_re.test(req.url)) {
|
||||||
|
packagedApi = _packagedApi;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!packagedApi) {
|
||||||
|
console.log("[ODD] no api for '" + req.url + "'");
|
||||||
|
next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reaching this point means that there are APIs for this pathname
|
||||||
|
// it is important to identify this host + pathname (example.com/foo) as the app
|
||||||
|
Object.defineProperty(req, 'experienceId', {
|
||||||
|
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?)
|
||||||
|
// NOTE: probably best to alias the name logically
|
||||||
|
, value: (path.join(req.hostname, pathname || '')).replace(/\/$/, '')
|
||||||
|
});
|
||||||
|
Object.defineProperty(req, 'escapedExperienceId', {
|
||||||
|
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?)
|
||||||
|
// NOTE: probably best to alias the name logically
|
||||||
|
, value: req.experienceId.replace(/\//g, ':')
|
||||||
|
});
|
||||||
|
// packageId should mean hash(api.id + host + path) - also called "api"
|
||||||
|
Object.defineProperty(req, 'packageId', {
|
||||||
|
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?)
|
||||||
|
// NOTE: probably best to alias the name logically
|
||||||
|
, value: packagedApi.domain.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 (packagedApi._api) {
|
||||||
|
packagedApi._api(req, res, next);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// console.log("[DEBUG pkgpath]", pkgConf.apipath, packagedApi.id);
|
||||||
|
loadApi(opts.conf, pkgConf, pkgDeps, packagedApi).then(function (api) {
|
||||||
|
api(req, res, next);
|
||||||
|
}, function (err) {
|
||||||
|
console.error('[App Promise Error]');
|
||||||
|
next(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports.runApi = runApi;
|
|
@ -0,0 +1,87 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var staticHandlers = {};
|
||||||
|
|
||||||
|
function loadPages(pkgConf, packagedPage, req, res, next) {
|
||||||
|
var PromiseA = require('bluebird');
|
||||||
|
var fs = require('fs');
|
||||||
|
var path = require('path');
|
||||||
|
var pkgpath = path.join(pkgConf.pagespath, (packagedPage.package || packagedPage.id), (packagedPage.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);
|
||||||
|
packagedPage._page = app;
|
||||||
|
}, function (err) {
|
||||||
|
console.error('[App Promise Error]');
|
||||||
|
next(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (staticHandlers[pkgpath]) {
|
||||||
|
packagedPage._page = staticHandlers[pkgpath];
|
||||||
|
packagedPage._page(req, res, next);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!packagedPage._promise_page) {
|
||||||
|
packagedPage._promise_page = new PromiseA(function (resolve, reject) {
|
||||||
|
fs.exists(pkgpath, function (exists) {
|
||||||
|
var staticServer;
|
||||||
|
|
||||||
|
if (!exists) {
|
||||||
|
reject(new Error("package '" + pkgpath + "' is registered but does not exist"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
//console.log('[static mount]', pkgpath);
|
||||||
|
// https://github.com/expressjs/serve-static/issues/54
|
||||||
|
// https://github.com/pillarjs/send/issues/91
|
||||||
|
// https://example.com/.well-known/acme-challenge/xxxxxxxxxxxxxxx
|
||||||
|
staticServer = require('serve-static')(pkgpath, { dotfiles: undefined });
|
||||||
|
resolve(staticServer);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handlePromise(packagedPage._promise_page);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 packagedPage;
|
||||||
|
nexti += 1;
|
||||||
|
|
||||||
|
if (err) {
|
||||||
|
next(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// shortest to longest
|
||||||
|
//route = packages.pop();
|
||||||
|
// longest to shortest
|
||||||
|
packagedPage = router.packagedPages[nexti];
|
||||||
|
if (!packagedPage) {
|
||||||
|
next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (packagedPage._page) {
|
||||||
|
packagedPage._page(req, res, nextify);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// could attach to req.{ pkgConf, pkgDeps, Services}
|
||||||
|
loadPages(pkgConf, packagedPage, req, res, next);
|
||||||
|
}
|
||||||
|
|
||||||
|
nextify();
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports.layerItUp = layerItUp;
|
|
@ -1,8 +1,8 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
var escapeStringRegexp = require('escape-string-regexp');
|
var escapeStringRegexp = require('escape-string-regexp');
|
||||||
var staticHandlers = {};
|
var runApi = require('./package-server-apis').runApi;
|
||||||
//var apiHandlers = {};
|
var layerItUp = require('./package-server-static').layerItUp;
|
||||||
|
|
||||||
function compileVhosts(vhostsMap) {
|
function compileVhosts(vhostsMap) {
|
||||||
var results = {
|
var results = {
|
||||||
|
@ -62,297 +62,6 @@ function compileVhosts(vhostsMap) {
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadPages(pkgConf, packagedPage, req, res, next) {
|
|
||||||
var PromiseA = require('bluebird');
|
|
||||||
var fs = require('fs');
|
|
||||||
var path = require('path');
|
|
||||||
var pkgpath = path.join(pkgConf.pagespath, (packagedPage.package || packagedPage.id), (packagedPage.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);
|
|
||||||
packagedPage._page = app;
|
|
||||||
}, function (err) {
|
|
||||||
console.error('[App Promise Error]');
|
|
||||||
next(err);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (staticHandlers[pkgpath]) {
|
|
||||||
packagedPage._page = staticHandlers[pkgpath];
|
|
||||||
packagedPage._page(req, res, next);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!packagedPage._promise_page) {
|
|
||||||
packagedPage._promise_page = new PromiseA(function (resolve, reject) {
|
|
||||||
fs.exists(pkgpath, function (exists) {
|
|
||||||
var staticServer;
|
|
||||||
|
|
||||||
if (!exists) {
|
|
||||||
reject(new Error("package '" + pkgpath + "' is registered but does not exist"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
//console.log('[static mount]', pkgpath);
|
|
||||||
// https://github.com/expressjs/serve-static/issues/54
|
|
||||||
// https://github.com/pillarjs/send/issues/91
|
|
||||||
// https://example.com/.well-known/acme-challenge/xxxxxxxxxxxxxxx
|
|
||||||
staticServer = require('serve-static')(pkgpath, { dotfiles: undefined });
|
|
||||||
resolve(staticServer);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
handlePromise(packagedPage._promise_page);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getApi(conf, pkgConf, pkgDeps, packagedApi) {
|
|
||||||
var PromiseA = pkgDeps.Promise;
|
|
||||||
var path = require('path');
|
|
||||||
var pkgpath = path.join(pkgConf.apipath, packagedApi.id/*, (packagedApi.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 {
|
|
||||||
packagedApi._apipkg = require(path.join(pkgpath, 'package.json'));
|
|
||||||
packagedApi._apiname = packagedApi._apipkg.name;
|
|
||||||
if (packagedApi._apipkg.walnut) {
|
|
||||||
pkgpath += '/' + packagedApi._apipkg.walnut;
|
|
||||||
}
|
|
||||||
promise = PromiseA.resolve(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
|
|
||||||
// packagedApi._api = require(pkgpath).create(pkgConf, pkgDeps, myApp);
|
|
||||||
packagedApi._api = require('express-lazy')();
|
|
||||||
packagedApi._api_app = myApp;
|
|
||||||
|
|
||||||
//require('./oauth3-auth').inject(conf, packagedApi._api, pkgConf, pkgDeps);
|
|
||||||
pkgDeps.getOauth3Controllers =
|
|
||||||
packagedApi._getOauth3Controllers = require('oauthcommon/example-oauthmodels').create(conf).getControllers;
|
|
||||||
require('oauthcommon').inject(packagedApi._getOauth3Controllers, packagedApi._api, pkgConf, pkgDeps);
|
|
||||||
|
|
||||||
// DEBUG
|
|
||||||
//
|
|
||||||
/*
|
|
||||||
packagedApi._api.use('/', function (req, res, next) {
|
|
||||||
console.log('[DEBUG pkgApiApp]', req.method, req.hostname, req.url);
|
|
||||||
next();
|
|
||||||
});
|
|
||||||
//*/
|
|
||||||
|
|
||||||
// TODO fix backwards compat
|
|
||||||
|
|
||||||
// /api/com.example.foo (no change)
|
|
||||||
packagedApi._api.use('/', packagedApi._api_app);
|
|
||||||
|
|
||||||
// /api/com.example.foo => /api
|
|
||||||
packagedApi._api.use('/', function (req, res, next) {
|
|
||||||
var priorUrl = req.url;
|
|
||||||
req.url = '/api' + req.url.slice(('/api/' + packagedApi.id).length);
|
|
||||||
// console.log('api mangle 3:', req.url);
|
|
||||||
packagedApi._api_app(req, res, function (err) {
|
|
||||||
req.url = priorUrl;
|
|
||||||
next(err);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// /api/com.example.foo => /
|
|
||||||
packagedApi._api.use('/api/' + packagedApi.id, function (req, res, next) {
|
|
||||||
// console.log('api mangle 2:', '/api/' + packagedApi.id, req.url);
|
|
||||||
// console.log(packagedApi._api_app.toString());
|
|
||||||
packagedApi._api_app(req, res, next);
|
|
||||||
});
|
|
||||||
|
|
||||||
resolve(packagedApi._api);
|
|
||||||
}, reject);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadApi(conf, pkgConf, pkgDeps, packagedApi) {
|
|
||||||
function handlePromise(p) {
|
|
||||||
return p.then(function (api) {
|
|
||||||
packagedApi._api = api;
|
|
||||||
return api;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!packagedApi._promise_api) {
|
|
||||||
packagedApi._promise_api = getApi(conf, pkgConf, pkgDeps, packagedApi);
|
|
||||||
}
|
|
||||||
|
|
||||||
return handlePromise(packagedApi._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 packagedPage;
|
|
||||||
nexti += 1;
|
|
||||||
|
|
||||||
if (err) {
|
|
||||||
next(err);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// shortest to longest
|
|
||||||
//route = packages.pop();
|
|
||||||
// longest to shortest
|
|
||||||
packagedPage = router.packagedPages[nexti];
|
|
||||||
if (!packagedPage) {
|
|
||||||
next();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (packagedPage._page) {
|
|
||||||
packagedPage._page(req, res, nextify);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// could attach to req.{ pkgConf, pkgDeps, Services}
|
|
||||||
loadPages(pkgConf, packagedPage, req, res, next);
|
|
||||||
}
|
|
||||||
|
|
||||||
nextify();
|
|
||||||
}
|
|
||||||
|
|
||||||
function runApi(opts, router, req, res, next) {
|
|
||||||
var path = require('path');
|
|
||||||
var pkgConf = opts.config;
|
|
||||||
var pkgDeps = opts.deps;
|
|
||||||
//var Services = opts.Services;
|
|
||||||
var packagedApi;
|
|
||||||
var pathname;
|
|
||||||
|
|
||||||
// TODO compile packagesMap
|
|
||||||
// TODO people may want to use the framework in a non-framework way (i.e. to conceal the module name)
|
|
||||||
router.packagedApis.some(function (_packagedApi) {
|
|
||||||
// console.log('[DEBUG _packagedApi.id]', _packagedApi.id);
|
|
||||||
pathname = router.pathname;
|
|
||||||
if ('/' === pathname) {
|
|
||||||
pathname = '';
|
|
||||||
}
|
|
||||||
// TODO allow for special apis that do not follow convention (.well_known, webfinger, oauth3.html, etc)
|
|
||||||
if (!_packagedApi._api_re) {
|
|
||||||
_packagedApi._api_re = new RegExp(escapeStringRegexp(pathname + '/api/' + _packagedApi.id) + '\/([\\w\\.\\-]+)(\\/|\\?|$)');
|
|
||||||
//console.log('[api re 2]', _packagedApi._api_re);
|
|
||||||
}
|
|
||||||
if (_packagedApi._api_re.test(req.url)) {
|
|
||||||
packagedApi = _packagedApi;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!packagedApi) {
|
|
||||||
console.log("[ODD] no api for '" + req.url + "'");
|
|
||||||
next();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reaching this point means that there are APIs for this pathname
|
|
||||||
// it is important to identify this host + pathname (example.com/foo) as the app
|
|
||||||
Object.defineProperty(req, 'experienceId', {
|
|
||||||
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?)
|
|
||||||
// NOTE: probably best to alias the name logically
|
|
||||||
, value: (path.join(req.hostname, pathname || '')).replace(/\/$/, '')
|
|
||||||
});
|
|
||||||
Object.defineProperty(req, 'escapedExperienceId', {
|
|
||||||
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?)
|
|
||||||
// NOTE: probably best to alias the name logically
|
|
||||||
, value: req.experienceId.replace(/\//g, ':')
|
|
||||||
});
|
|
||||||
// packageId should mean hash(api.id + host + path) - also called "api"
|
|
||||||
Object.defineProperty(req, 'packageId', {
|
|
||||||
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?)
|
|
||||||
// NOTE: probably best to alias the name logically
|
|
||||||
, value: packagedApi.domain.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 (packagedApi._api) {
|
|
||||||
packagedApi._api(req, res, next);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// console.log("[DEBUG pkgpath]", pkgConf.apipath, packagedApi.id);
|
|
||||||
loadApi(opts.conf, pkgConf, pkgDeps, packagedApi).then(function (api) {
|
|
||||||
api(req, res, next);
|
|
||||||
}, function (err) {
|
|
||||||
console.error('[App Promise Error]');
|
|
||||||
next(err);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function mapToApp(opts, req, res, next) {
|
function mapToApp(opts, req, res, next) {
|
||||||
// opts = { config, deps, services }
|
// opts = { config, deps, services }
|
||||||
var vhost;
|
var vhost;
|
||||||
|
@ -450,6 +159,5 @@ function mapToApp(opts, req, res, next) {
|
||||||
return runApi(opts, router, req, res, next);
|
return runApi(opts, router, req, res, next);
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports.runApi = runApi;
|
|
||||||
module.exports.compileVhosts = compileVhosts;
|
module.exports.compileVhosts = compileVhosts;
|
||||||
module.exports.mapToApp = mapToApp;
|
module.exports.mapToApp = mapToApp;
|
||||||
|
|
345
lib/worker.js
345
lib/worker.js
|
@ -1,137 +1,50 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
module.exports.create = function (webserver, conf, state) {
|
module.exports.create = function (webserver, xconfx, state) {
|
||||||
|
console.log('DEBUG create worker');
|
||||||
|
|
||||||
if (!state) {
|
if (!state) {
|
||||||
state = {};
|
state = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
var PromiseA = state.Promise || require('bluebird');
|
var PromiseA = state.Promise || require('bluebird');
|
||||||
var path = require('path');
|
|
||||||
//var vhostsdir = path.join(__dirname, 'vhosts');
|
|
||||||
var express = require('express-lazy');
|
|
||||||
var app = express();
|
|
||||||
var memstore;
|
var memstore;
|
||||||
var sqlstores = {};
|
var sqlstores = {};
|
||||||
var models = {};
|
|
||||||
var systemFactory = require('sqlite3-cluster/client').createClientFactory({
|
var systemFactory = require('sqlite3-cluster/client').createClientFactory({
|
||||||
dirname: path.join(__dirname, '..', '..', 'var') // TODO conf
|
dirname: xconfx.varpath
|
||||||
, prefix: 'com.example.'
|
, prefix: 'com.daplie.walnut.'
|
||||||
//, dbname: 'config'
|
//, dbname: 'config'
|
||||||
, suffix: ''
|
, suffix: ''
|
||||||
, ext: '.sqlite3'
|
, ext: '.sqlite3'
|
||||||
, sock: conf.sqlite3Sock
|
, sock: xconfx.sqlite3Sock
|
||||||
, ipcKey: conf.ipcKey
|
, ipcKey: xconfx.ipcKey
|
||||||
});
|
});
|
||||||
|
/*
|
||||||
var clientFactory = require('sqlite3-cluster/client').createClientFactory({
|
var clientFactory = require('sqlite3-cluster/client').createClientFactory({
|
||||||
algorithm: 'aes'
|
algorithm: 'aes'
|
||||||
, bits: 128
|
, bits: 128
|
||||||
, mode: 'cbc'
|
, mode: 'cbc'
|
||||||
, dirname: path.join(__dirname, '..', '..', 'var') // TODO conf
|
, dirname: xconfx.varpath // TODO conf
|
||||||
, prefix: 'com.example.'
|
, prefix: 'com.daplie.walnut.'
|
||||||
//, dbname: 'cluster'
|
//, dbname: 'cluster'
|
||||||
, suffix: ''
|
, suffix: ''
|
||||||
, ext: '.sqlcipher'
|
, ext: '.sqlcipher'
|
||||||
, sock: conf.sqlite3Sock
|
, sock: xconfx.sqlite3Sock
|
||||||
, ipcKey: conf.ipcKey
|
, ipcKey: xconfx.ipcKey
|
||||||
});
|
});
|
||||||
var cstore = require('cluster-store');
|
|
||||||
var redirectives;
|
|
||||||
|
|
||||||
app.disable('x-powered-by');
|
|
||||||
if (conf.trustProxy) {
|
|
||||||
console.info('[Trust Proxy]');
|
|
||||||
app.set('trust proxy', ['loopback']);
|
|
||||||
//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())
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
function unlockDevice(conf, state) {
|
|
||||||
return require('./lib/unlock-device').create().then(function (result) {
|
|
||||||
result.promise.then(function (_rootMasterKey) {
|
|
||||||
process.send({
|
|
||||||
type: 'walnut.keys.root'
|
|
||||||
conf: {
|
|
||||||
rootMasterKey: _rootMasterkey
|
|
||||||
}
|
|
||||||
});
|
|
||||||
conf.locked = false;
|
|
||||||
if (state.caddy) {
|
|
||||||
state.caddy.update(conf);
|
|
||||||
}
|
|
||||||
conf.rootMasterKey = _rootMasterKey;
|
|
||||||
});
|
|
||||||
|
|
||||||
return result.app;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
*/
|
*/
|
||||||
|
var cstore = require('cluster-store');
|
||||||
// TODO handle insecure to actual redirect
|
|
||||||
// blog.coolaj86.com -> coolaj86.com/blog
|
|
||||||
// hmm... that won't really matter with hsts
|
|
||||||
// I guess I just needs letsencrypt
|
|
||||||
|
|
||||||
function scrubTheDub(req, res, next) {
|
|
||||||
var host = req.hostname;
|
|
||||||
|
|
||||||
if (!host || 'string' !== typeof host) {
|
|
||||||
next();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO test if this is even necessary
|
|
||||||
host = host.toLowerCase();
|
|
||||||
|
|
||||||
// TODO this should be hot loadable / changeable
|
|
||||||
if (!redirectives && conf.redirects) {
|
|
||||||
redirectives = require('./hostname-redirects').compile(conf.redirects);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!/^www\./.test(host) && !redirectives) {
|
|
||||||
next();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO misnomer, handles all exact redirects
|
|
||||||
if (!require('./no-www').scrubTheDub(req, res, redirectives)) {
|
|
||||||
next();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function caddyBugfix(req, res, next) {
|
|
||||||
// workaround for Caddy
|
|
||||||
// https://github.com/mholt/caddy/issues/341
|
|
||||||
if (app.get('trust proxy')) {
|
|
||||||
if (req.headers['x-forwarded-proto']) {
|
|
||||||
req.headers['x-forwarded-proto'] = (req.headers['x-forwarded-proto'] || '').split(/,\s+/g)[0] || undefined;
|
|
||||||
}
|
|
||||||
if (req.headers['x-forwarded-host']) {
|
|
||||||
req.headers['x-forwarded-host'] = (req.headers['x-forwarded-host'] || '').split(/,\s+/g)[0] || undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
next();
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO misnomer, this can handle nowww, yeswww, and exact hostname redirects
|
|
||||||
app.use('/', scrubTheDub);
|
|
||||||
app.use('/', caddyBugfix);
|
|
||||||
|
|
||||||
return PromiseA.all([
|
return PromiseA.all([
|
||||||
// TODO security on memstore
|
// TODO security on memstore
|
||||||
// TODO memstoreFactory.create
|
// TODO memstoreFactory.create
|
||||||
cstore.create({
|
cstore.create({
|
||||||
sock: conf.memstoreSock
|
sock: xconfx.memstoreSock
|
||||||
, connect: conf.memstoreSock
|
, connect: xconfx.memstoreSock
|
||||||
// TODO implement
|
// TODO implement
|
||||||
, key: conf.ipcKey
|
, key: xconfx.ipcKey
|
||||||
}).then(function (_memstore) {
|
}).then(function (_memstore) {
|
||||||
memstore = _memstore;
|
memstore = PromiseA.promisifyAll(_memstore);
|
||||||
return memstore;
|
return memstore;
|
||||||
})
|
})
|
||||||
// TODO mark a device as lost, stolen, missing in DNS records
|
// TODO mark a device as lost, stolen, missing in DNS records
|
||||||
|
@ -140,66 +53,76 @@ module.exports.create = function (webserver, conf, state) {
|
||||||
init: true
|
init: true
|
||||||
, dbname: 'config'
|
, dbname: 'config'
|
||||||
})
|
})
|
||||||
, clientFactory.create({
|
|
||||||
init: true
|
|
||||||
, key: '00000000000000000000000000000000'
|
|
||||||
// TODO only complain if the values are different
|
|
||||||
//, algo: 'aes'
|
|
||||||
, dbname: 'auth'
|
|
||||||
})
|
|
||||||
, clientFactory.create({
|
|
||||||
init: false
|
|
||||||
, dbname: 'system'
|
|
||||||
})
|
|
||||||
]).then(function (args) {
|
]).then(function (args) {
|
||||||
memstore = args[0];
|
memstore = args[0];
|
||||||
sqlstores.config = args[1];
|
sqlstores.config = args[1];
|
||||||
sqlstores.auth = args[2];
|
|
||||||
sqlstores.system = args[3];
|
|
||||||
sqlstores.create = clientFactory.create;
|
|
||||||
|
|
||||||
return require('../lib/schemes-config').create(sqlstores.config).then(function (tables) {
|
var wrap = require('masterquest-sqlite3');
|
||||||
models.Config = tables;
|
var dir = [
|
||||||
return models.Config.Config.get().then(function (vhostsMap) {
|
{ tablename: 'com_daplie_walnut_config'
|
||||||
// TODO the core needs to be replacable in one shot
|
, idname: 'id'
|
||||||
// rm -rf /tmp/walnut/; tar xvf -C /tmp/walnut/; mv /srv/walnut /srv/walnut.{{version}}; mv /tmp/walnut /srv/
|
, unique: [ 'id' ]
|
||||||
// this means that any packages must be outside, perhaps /srv/walnut/{boot,core,packages}
|
, indices: [ 'createdAt', 'updatedAt' ]
|
||||||
var pkgConf = {
|
}
|
||||||
pagespath: path.join(__dirname, '..', '..', 'packages', 'pages') + path.sep
|
, { tablename: 'com_daplie_walnut_redirects'
|
||||||
, apipath: path.join(__dirname, '..', '..', 'packages', 'apis') + path.sep
|
, idname: 'id' // blog.example.com:sample.net/blog
|
||||||
, servicespath: path.join(__dirname, '..', '..', 'packages', 'services')
|
, unique: [ 'id' ]
|
||||||
, vhostsMap: vhostsMap
|
, indices: [ 'createdAt', 'updatedAt' ]
|
||||||
, vhostPatterns: null
|
}
|
||||||
, server: webserver
|
];
|
||||||
, externalPort: conf.externalPort
|
|
||||||
, privkey: conf.privkey
|
function scopeMemstore(expId) {
|
||||||
, pubkey: conf.pubkey
|
var scope = expId + '|';
|
||||||
, redirects: conf.redirects
|
return {
|
||||||
, apiPrefix: '/api'
|
getAsync: function (id) {
|
||||||
, 'org.oauth3.consumer': conf['org.oauth3.consumer']
|
return memstore.getAsync(scope + id);
|
||||||
, 'org.oauth3.provider': conf['org.oauth3.provider']
|
}
|
||||||
, keys: conf.keys
|
, setAsync: function (id, data) {
|
||||||
};
|
return memstore.setAsync(scope + id, data);
|
||||||
var pkgDeps = {
|
}
|
||||||
memstore: memstore
|
, touchAsync: function (id, data) {
|
||||||
, sqlstores: sqlstores
|
return memstore.touchAsync(scope + id, data);
|
||||||
, clientSqlFactory: clientFactory
|
}
|
||||||
, systemSqlFactory: systemFactory
|
, destroyAsync: function (id) {
|
||||||
//, handlePromise: require('./lib/common').promisableRequest;
|
return memstore.destroyAsync(scope + id);
|
||||||
//, handleRejection: require('./lib/common').rejectableRequest;
|
}
|
||||||
//, localPort: conf.localPort
|
|
||||||
, Promise: PromiseA
|
// helpers
|
||||||
, express: express
|
, allAsync: function () {
|
||||||
, app: app
|
return memstore.allASync().then(function (db) {
|
||||||
//, oauthmodels: require('oauthcommon/example-oauthmodels').create(conf)
|
return Object.keys(db).filter(function (key) {
|
||||||
};
|
return 0 === key.indexOf(scope);
|
||||||
var Services = require('./services-loader').create(pkgConf, {
|
}).map(function (key) {
|
||||||
memstore: memstore
|
return db[key];
|
||||||
, sqlstores: sqlstores
|
|
||||||
, clientSqlFactory: clientFactory
|
|
||||||
, systemSqlFactory: systemFactory
|
|
||||||
, Promise: PromiseA
|
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
, lengthAsync: function () {
|
||||||
|
return memstore.allASync().then(function (db) {
|
||||||
|
return Object.keys(db).filter(function (key) {
|
||||||
|
return 0 === key.indexOf(scope);
|
||||||
|
}).length;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
, clearAsync: function () {
|
||||||
|
return memstore.allASync().then(function (db) {
|
||||||
|
return Object.keys(db).filter(function (key) {
|
||||||
|
return 0 === key.indexOf(scope);
|
||||||
|
}).map(function (key) {
|
||||||
|
return memstore.destroyAsync(key);
|
||||||
|
});
|
||||||
|
}).then(function () {
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return wrap.wrap(sqlstores.config, dir).then(function (models) {
|
||||||
|
return models.ComDaplieWalnutConfig.find(null, { limit: 100 }).then(function (results) {
|
||||||
|
return models.ComDaplieWalnutConfig.find(null, { limit: 10000 }).then(function (redirects) {
|
||||||
|
var express = require('express-lazy');
|
||||||
|
var app = express();
|
||||||
var recase = require('connect-recase')({
|
var recase = require('connect-recase')({
|
||||||
// TODO allow explicit and or default flag
|
// TODO allow explicit and or default flag
|
||||||
explicit: false
|
explicit: false
|
||||||
|
@ -210,31 +133,47 @@ module.exports.create = function (webserver, conf, state) {
|
||||||
, exceptions: {}
|
, exceptions: {}
|
||||||
//, cancelParam: 'camel'
|
//, cancelParam: 'camel'
|
||||||
});
|
});
|
||||||
|
var bootstrapApp;
|
||||||
|
var mainApp;
|
||||||
|
var apiDeps = {
|
||||||
|
models: models
|
||||||
|
// TODO don't let packages use this directly
|
||||||
|
, Promise: PromiseA
|
||||||
|
};
|
||||||
|
var apiFactories = {
|
||||||
|
memstoreFactory: { create: scopeMemstore }
|
||||||
|
, systemSqlFactory: systemFactory
|
||||||
|
};
|
||||||
|
|
||||||
function handlePackages(req, res, next) {
|
function log(req, res, next) {
|
||||||
// TODO move to caddy parser?
|
console.log('[worker/log]', req.method, req.headers.host, req.url);
|
||||||
if (/(^|\.)proxyable\./.test(req.hostname)) {
|
next();
|
||||||
// device-id-12345678.proxyable.myapp.mydomain.com => myapp.mydomain.com
|
|
||||||
// proxyable.myapp.mydomain.com => myapp.mydomain.com
|
|
||||||
// TODO myapp.mydomain.com.example.proxyable.com => myapp.mydomain.com
|
|
||||||
req.hostname = req.hostname.replace(/.*\.?proxyable\./, '');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
require('./package-server').mapToApp({
|
function setupMain() {
|
||||||
config: pkgConf
|
mainApp = express();
|
||||||
, deps: pkgDeps
|
require('./main').create(mainApp, xconfx, apiFactories, apiDeps).then(function () {
|
||||||
, services: Services
|
// TODO process.send({});
|
||||||
, conf: conf
|
});
|
||||||
}, req, res, next);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO recase
|
if (!bootstrapApp) {
|
||||||
|
bootstrapApp = express();
|
||||||
|
require('./bootstrap').create(bootstrapApp, xconfx, models).then(function () {
|
||||||
|
// TODO process.send({});
|
||||||
|
setupMain();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
//
|
process.on('message', function (data) {
|
||||||
// Generic Template API
|
if ('com.daplie.walnut.bootstrap' === data.type) {
|
||||||
//
|
setupMain();
|
||||||
app
|
}
|
||||||
.use('/api', require('body-parser').json({
|
});
|
||||||
|
|
||||||
|
app.disable('x-powered-by');
|
||||||
|
app.use('/', log);
|
||||||
|
app.use('/api', require('body-parser').json({
|
||||||
strict: true // only objects and arrays
|
strict: true // only objects and arrays
|
||||||
, inflate: true
|
, inflate: true
|
||||||
// limited to due performance issues with JSON.parse and JSON.stringify
|
// limited to due performance issues with JSON.parse and JSON.stringify
|
||||||
|
@ -244,34 +183,35 @@ module.exports.create = function (webserver, conf, state) {
|
||||||
, reviver: undefined
|
, reviver: undefined
|
||||||
, type: 'json'
|
, type: 'json'
|
||||||
, verify: undefined
|
, verify: undefined
|
||||||
}))
|
}));
|
||||||
// DO NOT allow urlencoded at any point, it is expressly forbidden
|
|
||||||
//.use(require('body-parser').urlencoded({
|
|
||||||
// extended: true
|
|
||||||
//, inflate: true
|
|
||||||
//, limit: 100 * 1024
|
|
||||||
//, type: 'urlencoded'
|
|
||||||
//, verify: undefined
|
|
||||||
//}))
|
|
||||||
.use(require('connect-send-error').error())
|
|
||||||
;
|
|
||||||
|
|
||||||
app.use('/api', recase);
|
app.use('/api', recase);
|
||||||
|
|
||||||
app.use('/', handlePackages);
|
app.use('/', function (req, res) {
|
||||||
app.use('/', function (err, req, res, next) {
|
if (!req.secure) {
|
||||||
console.error('[Error Handler]');
|
// did not come from https
|
||||||
console.error(err.stack);
|
if (/\.(appcache|manifest)\b/.test(req.url)) {
|
||||||
if (req.xhr) {
|
require('./unbrick-appcache').unbrick(req, res);
|
||||||
res.send({ error: { message: "kinda unknownish error" } });
|
return;
|
||||||
} else {
|
}
|
||||||
res.send('<html><head><title>ERROR</title></head><body>Error</body></html>');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// sadly express uses arity checking
|
if (xconfx.lex && /\.well-known\/acme-challenge\//.test(req.url)) {
|
||||||
// so the fourth parameter must exist
|
var LEX = require('letsencrypt-express');
|
||||||
if (false) {
|
xconfx.lex.debug = true;
|
||||||
next();
|
xconfx.acmeResponder = xconfx.acmeResponder || LEX.createAcmeResponder(xconfx.lex/*, next*/);
|
||||||
|
xconfx.acmeResponder(req, res);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO check https://letsencrypt.status.io to see if https certification is not available
|
||||||
|
|
||||||
|
if (mainApp) {
|
||||||
|
mainApp(req, res);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
bootstrapApp(req, res);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -279,4 +219,5 @@ module.exports.create = function (webserver, conf, state) {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
pushd node_modules/authentication-microservice/ || git clone git@github.com:coolaj86/node-authentication-microservice node_modules/authentication-microservice
|
||||||
|
git pull
|
||||||
|
popd
|
||||||
|
|
||||||
|
pushd node_modules/oauthclient-microservice/ || git clone git@github.com:OAuth3/node-oauth3clients.git node_modules/oauthclient-microservice
|
||||||
|
git pull
|
||||||
|
popd
|
||||||
|
|
||||||
|
pushd node_modules/oauthcommon/ || git clone git@github.com:coolaj86/node-oauthcommon.git node_modules/oauthcommon
|
||||||
|
git pull
|
||||||
|
popd
|
Loading…
Reference in New Issue