347 lines
12 KiB
JavaScript
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;
|
|
});
|
|
});
|
|
});
|
|
});
|
|
};
|