walnut.js/lib/worker.js

294 lines
9.5 KiB
JavaScript

'use strict';
module.exports.create = function (webserver, info, state) {
if (!state) {
state = {};
}
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 sqlstores = {};
var models = {};
var systemFactory = require('sqlite3-cluster/client').createClientFactory({
dirname: path.join(__dirname, '..', '..', 'var') // TODO info.conf
, prefix: 'com.example.'
//, dbname: 'config'
, suffix: ''
, ext: '.sqlite3'
, sock: info.conf.sqlite3Sock
, ipcKey: info.conf.ipcKey
});
var clientFactory = require('sqlite3-cluster/client').createClientFactory({
algorithm: 'aes'
, bits: 128
, mode: 'cbc'
, dirname: path.join(__dirname, '..', '..', 'var') // TODO info.conf
, prefix: 'com.example.'
//, dbname: 'cluster'
, suffix: ''
, ext: '.sqlcipher'
, sock: info.conf.sqlite3Sock
, ipcKey: info.conf.ipcKey
});
var cstore = require('cluster-store');
var redirectives;
app.disable('x-powered-by');
if (info.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;
});
}
*/
// 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 && info.conf.redirects) {
redirectives = require('./hostname-redirects').compile(info.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([
// TODO security on memstore
// TODO memstoreFactory.create
cstore.create({
sock: info.conf.memstoreSock
, connect: info.conf.memstoreSock
// TODO implement
, key: info.conf.ipcKey
}).then(function (_memstore) {
memstore = _memstore;
return memstore;
})
// TODO mark a device as lost, stolen, missing in DNS records
// (and in turn allow other devices to lock it, turn on location reporting, etc)
, systemFactory.create({
init: true
, 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) {
memstore = args[0];
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) {
models.Config = tables;
return models.Config.Config.get().then(function (vhostsMap) {
// TODO the core needs to be replacable in one shot
// rm -rf /tmp/walnut/; tar xvf -C /tmp/walnut/; mv /srv/walnut /srv/walnut.{{version}}; mv /tmp/walnut /srv/
// this means that any packages must be outside, perhaps /srv/walnut/{boot,core,packages}
var pkgConf = {
pagespath: path.join(__dirname, '..', '..', 'packages', 'pages') + path.sep
, apipath: path.join(__dirname, '..', '..', 'packages', 'apis') + path.sep
, servicespath: path.join(__dirname, '..', '..', 'packages', 'services')
, vhostsMap: vhostsMap
, vhostPatterns: null
, server: webserver
, externalPort: info.conf.externalPort
, primaryNameserver: info.conf.primaryNameserver
, nameservers: info.conf.nameservers
, privkey: info.conf.privkey
, pubkey: info.conf.pubkey
, redirects: info.conf.redirects
, apiPrefix: '/api'
, 'org.oauth3.consumer': info.conf['org.oauth3.consumer']
, 'org.oauth3.provider': info.conf['org.oauth3.provider']
, keys: info.conf.keys
};
var pkgDeps = {
memstore: memstore
, sqlstores: sqlstores
, clientSqlFactory: clientFactory
, systemSqlFactory: systemFactory
//, handlePromise: require('./lib/common').promisableRequest;
//, handleRejection: require('./lib/common').rejectableRequest;
//, localPort: info.conf.localPort
, Promise: PromiseA
, express: express
, app: app
//, oauthmodels: require('oauthcommon/example-oauthmodels').create(info.conf)
};
var Services = require('./services-loader').create(pkgConf, {
memstore: memstore
, sqlstores: sqlstores
, clientSqlFactory: clientFactory
, systemSqlFactory: systemFactory
, Promise: PromiseA
});
var recase = require('connect-recase')({
// TODO allow explicit and or default flag
explicit: false
, default: 'snake'
, prefixes: ['/api']
// TODO allow exclude
//, exclusions: [config.oauthPrefix]
, exceptions: {}
//, cancelParam: 'camel'
});
function handlePackages(req, res, next) {
// TODO move to caddy parser?
if (/(^|\.)proxyable\./.test(req.hostname)) {
// device-id-12345678.proxyable.myapp.mydomain.com => myapp.mydomain.com
// proxyable.myapp.mydomain.com => myapp.mydomain.com
// TODO myapp.mydomain.com.example.proxyable.com => myapp.mydomain.com
req.hostname = req.hostname.replace(/.*\.?proxyable\./, '');
}
// hacky fix for com.enom.reseller, com.daplie.domains, org.oauth3.dns, org.oauth3.domains
if (req.path) {
req.url = req.url.replace(/com\.daplie\.domains/, 'com.enom.reseller');
req.url = req.url.replace(/org\.oauth3\.domains/, 'com.enom.reseller');
req.url = req.url.replace(/org\.oauth3\.dns/, 'com.enom.reseller');
req.url = req.url.replace(/org\.oauth3\.provider\/credentials/, 'org.oauth3.provider/logins');
//req.path = req.path.replace(/com\.daplie\.domains/, 'com.enom.reseller');
}
require('./package-server').mapToApp({
config: pkgConf
, deps: pkgDeps
, services: Services
, conf: info.conf
}, req, res, next);
}
// TODO recase
//
// Generic Template API
//
app
.use('/api', require('body-parser').json({
strict: true // only objects and arrays
, inflate: true
// limited to due performance issues with JSON.parse and JSON.stringify
// http://josh.zeigler.us/technology/web-development/how-big-is-too-big-for-json/
//, limit: 128 * 1024
, limit: 1.5 * 1024 * 1024
, reviver: undefined
, type: 'json'
, 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('/', handlePackages);
app.use('/', function (err, req, res, next) {
console.error('[Error Handler]');
console.error(err.stack);
if (req.xhr) {
res.send({ error: { message: "kinda unknownish error" } });
} else {
res.send('<html><head><title>ERROR</title></head><body>Error</body></html>');
}
// sadly express uses arity checking
// so the fourth parameter must exist
if (false) {
next();
}
});
return app;
});
});
});
};