walnut.js/lib/worker.js

347 lines
12 KiB
JavaScript

'use strict';
module.exports.create = function (webserver, xconfx, state) {
console.log('[worker] create');
xconfx.debug = true;
console.log('DEBUG create worker');
if (!state) {
state = {};
}
var PromiseA = state.Promise || require('bluebird');
var memstore;
var sqlstores = {};
var systemFactory = require('sqlite3-cluster/client').createClientFactory({
dirname: xconfx.varpath
, prefix: 'walnut+'
//, dbname: 'config'
, suffix: '@daplie.com'
, ext: '.sqlite3'
, sock: xconfx.sqlite3Sock
, ipcKey: xconfx.ipcKey
});
/*
var clientFactory = require('sqlite3-cluster/client').createClientFactory({
algorithm: 'aes'
, bits: 128
, mode: 'cbc'
, dirname: xconfx.varpath // TODO conf
, prefix: 'com.daplie.walnut.'
//, dbname: 'cluster'
, suffix: ''
, ext: '.sqlcipher'
, sock: xconfx.sqlite3Sock
, ipcKey: xconfx.ipcKey
});
*/
var cstore = require('cluster-store');
console.log('[worker] creating data stores...');
return PromiseA.all([
// TODO security on memstore
// TODO memstoreFactory.create
cstore.create({
sock: xconfx.memstoreSock
, connect: xconfx.memstoreSock
// TODO implement
, key: xconfx.ipcKey
}).then(function (_memstore) {
console.log('[worker] cstore created');
memstore = PromiseA.promisifyAll(_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'
}).then(function (sysdb) {
console.log('[worker] sysdb created');
return sysdb;
})
]).then(function (args) {
console.log('[worker] database factories created');
memstore = args[0];
sqlstores.config = args[1];
var wrap = require('masterquest-sqlite3');
var dir = [
{ tablename: 'com_daplie_walnut_config'
, idname: 'id'
, unique: [ 'id' ]
, indices: [ 'createdAt', 'updatedAt' ]
}
, { tablename: 'com_daplie_walnut_redirects'
, idname: 'id' // blog.example.com:sample.net/blog
, unique: [ 'id' ]
, indices: [ 'createdAt', 'updatedAt' ]
}
];
function scopeMemstore(expId) {
var scope = expId + '|';
return {
getAsync: function (id) {
return memstore.getAsync(scope + id);
}
, setAsync: function (id, data) {
return memstore.setAsync(scope + id, data);
}
, touchAsync: function (id, data) {
return memstore.touchAsync(scope + id, data);
}
, destroyAsync: function (id) {
return memstore.destroyAsync(scope + id);
}
// helpers
, allAsync: function () {
return memstore.allAsync().then(function (db) {
return Object.keys(db).filter(function (key) {
return 0 === key.indexOf(scope);
}).map(function (key) {
return db[key];
});
});
}
, 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) {
console.log('[worker] database wrapped');
return models.ComDaplieWalnutConfig.find(null, { limit: 100 }).then(function (results) {
console.log('[worker] config query complete');
return models.ComDaplieWalnutConfig.find(null, { limit: 10000 }).then(function (redirects) {
console.log('[worker] configuring express');
var express = require('express-lazy');
var app = express();
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'
});
var bootstrapApp;
var mainApp;
var apiDeps = {
models: models
// TODO don't let packages use this directly
, Promise: PromiseA
, dns: PromiseA.promisifyAll(require('dns'))
, crypto: PromiseA.promisifyAll(require('crypto'))
, fs: PromiseA.promisifyAll(require('fs'))
, path: require('path')
, validate: {
isEmail: function (email) {
return /@/.test(email) && !/\s+/.test(email);
}
, email: function (email) {
if (apiDeps.validate.isEmail(email)) {
return null;
}
return new Error('invalid email address');
}
}
};
var apiFactories = {
memstoreFactory: { create: scopeMemstore }
, systemSqlFactory: systemFactory
};
var hostsmap = {};
function log(req, res, next) {
var hostname = (req.hostname || req.headers.host || '').split(':').shift();
// Printing all incoming requests for debugging
console.log('[worker/log]', req.method, hostname, req.url);
// logging all the invalid hostnames that come here out of curiousity
if (hostname && !hostsmap[hostname]) {
hostsmap[hostname] = true;
require('fs').writeFile(
require('path').join(__dirname, '..', '..', 'var', 'hostnames', hostname)
, hostname
, function () {}
);
}
next();
}
function setupMain() {
if (xconfx.debug) { console.log('[main] setup'); }
mainApp = express();
require('./main').create(mainApp, xconfx, apiFactories, apiDeps, errorIfApi, errorIfAssets).then(function () {
if (xconfx.debug) { console.log('[main] ready'); }
// TODO process.send({});
});
}
if (!bootstrapApp) {
if (xconfx.debug) { console.log('[bootstrap] setup'); }
if (xconfx.primaryDomain) {
bootstrapApp = true;
setupMain();
return;
}
bootstrapApp = express();
require('./bootstrap').create(bootstrapApp, xconfx, models).then(function () {
if (xconfx.debug) { console.log('[bootstrap] ready'); }
// TODO process.send({});
setupMain();
});
}
process.on('message', function (data) {
if ('com.daplie.walnut.bootstrap' === data.type) {
setupMain();
}
});
function errorIfNotApi(req, res, next) {
var hostname = req.hostname || req.headers.host;
if (!/^api\.[a-z0-9\-]+/.test(hostname)) {
res.send({ error:
{ message: "['" + hostname + req.url + "'] API access is restricted to proper 'api'-prefixed lowercase subdomains."
+ " The HTTP 'Host' header must exist and must begin with 'api.' as in 'api.example.com'."
+ " For development you may test with api.localhost.daplie.me (or any domain by modifying your /etc/hosts)"
, code: 'E_NOT_API'
, _hostname: hostname
}
});
return;
}
next();
}
function errorIfNotAssets(req, res, next) {
var hostname = req.hostname || req.headers.host;
if (!/^assets\.[a-z0-9\-]+/.test(hostname)) {
res.send({ error:
{ message: "['" + hostname + req.url + "'] protected asset access is restricted to proper 'asset'-prefixed lowercase subdomains."
+ " The HTTP 'Host' header must exist and must begin with 'assets.' as in 'assets.example.com'."
+ " For development you may test with assets.localhost.daplie.me (or any domain by modifying your /etc/hosts)"
, code: 'E_NOT_API'
, _hostname: hostname
}
});
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)) {
console.log('[walnut/worker api] req.url', req.url);
res.send({ error: { message: "missing /api/ url prefix" } });
return;
}
res.send({ error: { code: 'E_NO_IMPL', message: "API not implemented" } });
}
function errorIfAssets(req, res, next) {
if (!/^assets\./.test(req.headers.host)) {
next();
return;
}
// has api. hostname prefix
// doesn't have /api url prefix
if (!/^\/assets\//.test(req.url)) {
console.log('[walnut/worker assets] req.url', req.url);
res.send({ error: { message: "missing /assets/ url prefix" } });
return;
}
res.send({ error: { code: 'E_NO_IMPL', message: "assets handler not implemented" } });
}
app.disable('x-powered-by');
app.use('/', log);
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
}));
app.use('/api', recase);
var cookieParser = require('cookie-parser'); // signing is done in JWT
app.set('trust proxy', ['loopback', 'linklocal', 'uniquelocal']);
app.use('/api', errorIfNotApi);
app.use('/assets', /*errorIfNotAssets,*/ cookieParser()); // serializer { path: '/assets', httpOnly: true, sameSite: true/*, domain: assets.example.com*/ }
app.use('/', function (req, res) {
if (!(req.encrypted || req.secure)) {
// did not come from https
if (/\.(appcache|manifest)\b/.test(req.url)) {
require('./unbrick-appcache').unbrick(req, res);
return;
}
console.log('[lib/worker] unencrypted:', req.headers);
res.end("Connection is not encrypted. That's no bueno or, as we say in Hungarian, nem szabad!");
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;
}
});
return app;
});
});
});
});
};