merge with letsencrypt branch

This commit is contained in:
AJ ONeal 2016-03-31 12:30:04 -04:00
commit 28db03ae23
12 changed files with 245 additions and 585 deletions

View File

@ -1,65 +0,0 @@
#!/usr/bin/env node
'use strict';
require('../walnut.js');
/*
var c = require('console-plus');
console.log = c.log;
console.error = c.error;
*/
function eagerLoad() {
var PromiseA = require('bluebird').Promise;
var promise = PromiseA.resolve();
[ 'express'
, 'request'
, 'sqlite3'
, 'body-parser'
, 'urlrouter'
, 'express-lazy'
, 'connect-send-error'
, 'underscore.string'
, 'secret-utils'
, 'connect-cors'
, 'uuid'
, 'connect-recase'
, 'escape-string-regexp'
, 'connect-query'
, 'recase'
].forEach(function (name/*, i*/) {
promise = promise.then(function () {
return new PromiseA(function (resolve/*, reject*/) {
setTimeout(function () {
require(name);
resolve();
}, 4);
});
});
});
[ function () {
require('body-parser').json();
}
/*
// do not use urlencoded as it enables csrf
, function () {
require('body-parser').urlencoded();
}
*/
].forEach(function (fn) {
promise = promise.then(function (thing) {
return new PromiseA(function (resolve) {
setTimeout(function () {
resolve(fn(thing));
}, 4);
});
});
});
promise.then(function () {
console.log('Eager Loading Complete');
});
}
setTimeout(eagerLoad, 100);

1
bin/walnut Symbolic link
View File

@ -0,0 +1 @@
walnut.js

View File

@ -1 +0,0 @@
walnut

65
bin/walnut.js Executable file
View File

@ -0,0 +1,65 @@
#!/usr/bin/env node
'use strict';
require('../walnut.js');
/*
var c = require('console-plus');
console.log = c.log;
console.error = c.error;
*/
function eagerLoad() {
var PromiseA = require('bluebird').Promise;
var promise = PromiseA.resolve();
[ 'express'
, 'request'
, 'sqlite3'
, 'body-parser'
, 'urlrouter'
, 'express-lazy'
, 'connect-send-error'
, 'underscore.string'
, 'secret-utils'
, 'connect-cors'
, 'uuid'
, 'connect-recase'
, 'escape-string-regexp'
, 'connect-query'
, 'recase'
].forEach(function (name/*, i*/) {
promise = promise.then(function () {
return new PromiseA(function (resolve/*, reject*/) {
setTimeout(function () {
require(name);
resolve();
}, 4);
});
});
});
[ function () {
require('body-parser').json();
}
/*
// do not use urlencoded as it enables csrf
, function () {
require('body-parser').urlencoded();
}
*/
].forEach(function (fn) {
promise = promise.then(function (thing) {
return new PromiseA(function (resolve) {
setTimeout(function () {
resolve(fn(thing));
}, 4);
});
});
});
promise.then(function () {
console.log('Eager Loading Complete');
});
}
setTimeout(eagerLoad, 100);

View File

@ -9,12 +9,46 @@ console.info('arch:', process.arch);
console.info('platform:', process.platform); console.info('platform:', process.platform);
console.info('\n\n\n[MASTER] Welcome to WALNUT!'); console.info('\n\n\n[MASTER] Welcome to WALNUT!');
function tryConf(pathname, def) {
try {
return require(pathname);
} catch(e) {
return def;
}
}
var path = require('path'); var path = require('path');
var cluster = require('cluster'); var cluster = require('cluster');
//var minWorkers = 2; //var minWorkers = 2;
var numCores = 2; // Math.max(minWorkers, require('os').cpus().length); var numCores = 2; // Math.max(minWorkers, require('os').cpus().length);
var workers = []; var workers = [];
var state = { firstRun: true }; var state = { firstRun: true };
// TODO Should these be configurable? If so, where?
// TODO communicate config with environment vars?
var caddy = tryConf(
path.join('..', '..', 'config.caddy.json')
, { conf: null // __dirname + '/Caddyfile'
, bin: null // '/usr/local/bin/caddy'
, sitespath: null // path.join(__dirname, 'sites-enabled')
, locked: false // true
}
);
var useCaddy = require('fs').existsSync(caddy.bin);
var info = {
type: 'walnut.init'
, conf: {
protocol: useCaddy ? 'http' : 'https'
, externalPort: 443
, externalPortInsecure: 80 // TODO externalInsecurePort
, localPort: process.argv[2] || (useCaddy ? 4080 : 443) // system / local network
, insecurePort: process.argv[3] || (useCaddy ? 80 : 80) // meh
, certPaths: useCaddy ? null : [
path.join(__dirname, '..', '..', 'certs', 'live')
, path.join(__dirname, '..', '..', 'letsencrypt', 'live')
]
, trustProxy: useCaddy ? true : false
}
};
function fork() { function fork() {
if (workers.length < numCores) { if (workers.length < numCores) {
@ -26,53 +60,15 @@ cluster.on('online', function (worker) {
console.info('[MASTER] Worker ' + worker.process.pid + ' is online'); console.info('[MASTER] Worker ' + worker.process.pid + ' is online');
fork(); fork();
var config = {
externalPort: 443 // world accessible
, externalPortInsecure: 80 // world accessible
// TODO externalInsecurePort?
, locked: false // TODO XXX
// XXX
// TODO needs mappings from db
// TODO autoconfig Caddy caddy
// XXX
, caddy: {
conf: __dirname + '/Caddyfile'
, bin: '/usr/local/bin/caddy'
, sitespath: path.join(__dirname, 'sites-enabled')
}
};
var useCaddy = require('fs').existsSync(config.caddy.bin);
var caddy;
config.localPort = process.argv[2] || (useCaddy ? 4080 : 443); // system / local network
config.insecurePort = process.argv[3] || (useCaddy ? 80 : 80); // meh
if (state.firstRun) { if (state.firstRun) {
state.firstRun = false; state.firstRun = false;
if (useCaddy) { if (useCaddy) {
caddy = require('../lib/spawn-caddy').create(config); caddy = require('../lib/spawn-caddy').create(caddy);
// relies on { localPort, locked } // relies on { localPort, locked }
caddy.spawn(config); caddy.spawn(caddy);
} }
} }
// TODO XXX Should these be configurable? If so, where?
var certPaths = [
path.join(__dirname, '..', '..', 'certs', 'live')
, path.join(__dirname, '..', '..', 'letsencrypt', 'live')
];
// TODO communicate config with environment vars?
var info = {
type: 'walnut.init'
, conf: {
protocol: useCaddy ? 'http' : 'https'
, externalPort: config.externalPort
, localPort: config.localPort
, insecurePort: config.insecurePort
, certPaths: useCaddy ? null : certPaths
, trustProxy: useCaddy ? true : false
}
};
function touchMaster(msg) { function touchMaster(msg) {
if ('walnut.webserver.listening' !== msg.type) { if ('walnut.webserver.listening' !== msg.type) {
console.warn('[MASTER] received unexpected message from worker'); console.warn('[MASTER] received unexpected message from worker');
@ -83,19 +79,19 @@ cluster.on('online', function (worker) {
// calls init if init has not been called // calls init if init has not been called
state.caddy = caddy; state.caddy = caddy;
state.workers = workers; state.workers = workers;
require('../lib/master').touch(config, state).then(function (results) { require('../lib/master').touch(info.conf, state).then(function (results) {
//var memstore = results.memstore; //var memstore = results.memstore;
var sqlstore = results.sqlstore; var sqlstore = results.sqlstore;
info.type = 'walnut.webserver.onrequest'; info.type = 'walnut.webserver.onrequest';
// TODO let this load after server is listening // TODO let this load after server is listening
info.conf['org.oauth3.consumer'] = config['org.oauth3.consumer']; info.conf['org.oauth3.consumer'] = results['org.oauth3.consumer'];
info.conf['org.oauth3.provider'] = config['org.oauth3.provider']; info.conf['org.oauth3.provider'] = results['org.oauth3.provider'];
info.conf.keys = config.keys; info.conf.keys = results.keys;
info.conf.memstoreSock = config.memstoreSock; //info.conf.memstoreSock = config.memstoreSock;
info.conf.sqlite3Sock = config.sqlite3Sock; //info.conf.sqlite3Sock = config.sqlite3Sock;
// TODO get this from db config instead // TODO get this from db config instead
info.conf.privkey = config.privkey; //info.conf.privkey = config.privkey;
info.conf.pubkey = config.pubkey; //info.conf.pubkey = config.pubkey;
info.conf.redirects = [ info.conf.redirects = [
{ "ip": false, "id": "*", "value": false } // default no-www { "ip": false, "id": "*", "value": false } // default no-www
@ -116,10 +112,10 @@ cluster.on('online', function (worker) {
// TODO use sqlite3 or autogenerate ? // TODO use sqlite3 or autogenerate ?
info.conf.privkey = require('fs').readFileSync(__dirname + '/../../' + '/nsx.redirect-www.org.key.pem', 'ascii'); 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'); info.conf.pubkey = require('fs').readFileSync(__dirname + '/../../' + '/nsx.redirect-www.org.key.pem.pub', 'ascii');
// keys // keys
// letsencrypt // letsencrypt
// com.example.provider // com.example.provider
// com.example.consumer // com.example.consumer
worker.send(info); worker.send(info);
}); });
} }

View File

@ -4,9 +4,9 @@ module.exports.create = function (opts) {
var id = '0'; var id = '0';
var promiseApp; var promiseApp;
function createAndBindInsecure(message, cb) { function createAndBindInsecure(lex, message, cb) {
// TODO conditional if 80 is being served by caddy // TODO conditional if 80 is being served by caddy
require('../lib/insecure-server').create(message.conf.externalPort, message.conf.insecurePort, message, function (err, webserver) { 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'); console.info("#" + id + " Listening on http://" + webserver.address().address + ":" + webserver.address().port, '\n');
// we are returning the promise result to the caller // we are returning the promise result to the caller
@ -14,9 +14,48 @@ module.exports.create = function (opts) {
}); });
} }
function createLe(conf) {
var LEX = require('letsencrypt-express');
var lex = LEX.create({
configDir: conf.letsencrypt.configDir // i.e. __dirname + '/letsencrypt.config'
, approveRegistration: function (hostname, cb) {
cb(null, {
domains: [hostname] // TODO handle www and bare on the same cert
, email: conf.letsencrypt.email
, agreeTos: conf.letsencrypt.agreeTos
});
/*
letsencrypt.getConfig({ domains: [domain] }, function (err, config) {
if (!(config && config.checkpoints >= 0)) {
cb(err, null);
return;
}
cb(null, {
email: config.email
// can't remember which it is, but the pyconf is different that the regular variable
, agreeTos: config.tos || config.agree || config.agreeTos
, server: config.server || LE.productionServerUrl
, domains: config.domains || [domain]
});
});
*/
}
});
//var letsencrypt = lex.letsencrypt;
return lex;
}
function createAndBindServers(message, cb) { function createAndBindServers(message, cb) {
var lex;
if (message.conf.letsencrypt) {
lex = createLe(message.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(message.conf.certPaths, message.conf.localPort, message, function (err, webserver) { require('../lib/local-server').create(lex, message.conf.certPaths, message.conf.localPort, message, 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);
@ -27,7 +66,7 @@ module.exports.create = function (opts) {
// 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(message, cb); createAndBindInsecure(lex, message, cb);
}); });
// we are returning the promise result to the caller // we are returning the promise result to the caller
@ -67,7 +106,7 @@ module.exports.create = function (opts) {
process.removeListener('message', initWebServer); process.removeListener('message', initWebServer);
resolve(require('../lib/worker').create(webserver, srvmsg)); resolve(require('../lib/worker').create(webserver, srvmsg.conf));
} }
process.send({ type: 'walnut.webserver.listening' }); process.send({ type: 'walnut.webserver.listening' });

View File

@ -1,6 +1,6 @@
'use strict'; 'use strict';
module.exports.create = function (securePort, insecurePort, info, serverCallback) { module.exports.create = function (lex, securePort, insecurePort, info, serverCallback) {
var PromiseA = require('bluebird').Promise; var PromiseA = require('bluebird').Promise;
var appPromise; var appPromise;
//var app; //var app;
@ -101,7 +101,13 @@ module.exports.create = function (securePort, insecurePort, info, serverCallback
appPromise = serverCallback(null, insecureServer); appPromise = serverCallback(null, insecureServer);
} }
}); });
insecureServer.on('request', redirectHttps);
if (lex) {
var LEX = require('letsencrypt-express');
insecureServer.on('request', LEX.createAcmeResponder(lex, redirectHttps));
} else {
insecureServer.on('request', redirectHttps);
}
return PromiseA.resolve(insecureServer); return PromiseA.resolve(insecureServer);
}; };

View File

@ -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 (certPaths, port, info, serverCallback) { module.exports.create = function (lex, 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 (certPaths, port, info, serverCallback) {
*/ */
// Get up and listening as absolutely quickly as possible // Get up and listening as absolutely quickly as possible
server.on('request', function (req, res) { function onRequest(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,11 +41,18 @@ module.exports.create = function (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(certPaths, initServer); require('./sni-server').create(lex, certPaths, initServer);
} else { } else {
initServer(null, require('http').createServer()); initServer(null, require('http').createServer());
} }

View File

@ -1,407 +0,0 @@
'use strict';
var PromiseA = require('bluebird');
module.exports.inject = function (conf, app, pkgConf, pkgDeps) {
var scoper = require('app-scoped-ids');
var inProcessCache = {};
var createClientFactory = require('sqlite3-cluster/client').createClientFactory;
var dir = [
{ tablename: 'codes'
, idname: 'uuid'
, indices: ['createdAt']
}
, { tablename: 'logins' // coolaj86, coolaj86@gmail.com, +1-317-426-6525
, idname: 'hashId'
//, relations: [{ tablename: 'secrets', id: 'hashid', fk: 'loginId' }]
, indices: ['createdAt', 'type', 'node']
//, immutable: false
}
, { tablename: 'verifications'
, idname: 'hashId' // hash(date + node)
//, relations: [{ tablename: 'secrets', id: 'hashid', fk: 'loginId' }]
, indices: ['createdAt', 'nodeId']
//, immutable: true
}
, { tablename: 'secrets'
, idname: 'hashId' // hash(node + secret)
, indices: ['createdAt']
//, immutable: true
}
, { tablename: 'recoveryNodes' // just for 1st-party logins
, idname: 'hashId' //
// TODO how transmit that something should be deleted / disabled?
, indices: ['createdAt', 'updatedAt', 'loginHash', 'recoveryNode', 'deleted']
}
//
// Accounts
//
, { tablename: 'accounts_logins'
, idname: 'id' // hash(accountId + loginId)
, indices: ['createdAt', 'revokedAt', 'loginId', 'accountId']
}
, { tablename: 'accounts'
, idname: 'id' // crypto random id? or hash(name) ?
, unique: ['name']
, indices: ['createdAt', 'updatedAt', 'deletedAt', 'name', 'displayName']
}
//
// OAuth3
//
, { tablename: 'private_key'
, idname: 'id'
, indices: ['createdAt']
}
, { tablename: 'oauth_clients'
, idname: 'id'
, indices: ['createdAt', 'updatedAt', 'accountId']
, hasMany: ['apiKeys'] // TODO
, belongsTo: ['account']
, schema: function () {
return {
test: true
, insecure: true
};
}
}
, { tablename: 'api_keys'
, idname: 'id'
, indices: ['createdAt', 'updatedAt', 'oauthClientId']
, belongsTo: ['oauthClient'] // TODO pluralization
, schema: function () {
return {
test: true
, insecure: true
};
}
}
, { tablename: 'tokens' // note that a token functions as a session
, idname: 'id'
, indices: ['createdAt', 'updatedAt', 'expiresAt', 'revokedAt', 'oauthClientId', 'loginId', 'accountId']
}
, { tablename: 'grants'
, idname: 'id' // sha256(scope + oauthClientId + (accountId || loginId))
, indices: ['createdAt', 'updatedAt', 'oauthClientId', 'loginId', 'accountId']
}
];
function getAppScopedControllers(experienceId) {
if (inProcessCache[experienceId]) {
return PromiseA.resolve(inProcessCache[experienceId]);
}
var mq = require('masterquest');
var path = require('path');
// TODO how can we encrypt this?
var systemFactory = createClientFactory({
// TODO only complain if the values are different
algorithm: 'aes'
, bits: 128
, mode: 'cbc'
, dirname: path.join(__dirname, '..', '..', 'var') // TODO info.conf
//, prefix: appname.replace(/\//g, ':') // 'com.example.'
//, dbname: 'cluster'
, suffix: ''
, ext: '.sqlcipher'
, sock: conf.sqlite3Sock
, ipcKey: conf.ipcKey
});
var clientFactory = createClientFactory({
// TODO only complain if the values are different
dirname: path.join(__dirname, '..', '..', 'var') // TODO info.conf
, prefix: 'com.oauth3' // 'com.example.'
//, dbname: 'config'
, suffix: ''
, ext: '.sqlite3'
, sock: conf.sqlite3Sock
, ipcKey: conf.ipcKey
});
inProcessCache[experienceId] = systemFactory.create({
init: true
//, key: '00000000000000000000000000000000'
, dbname: experienceId // 'com.example.'
}).then(function (sqlStore) {
//var db = factory.
return mq.wrap(sqlStore, dir).then(function (models) {
return require('./oauthclient-microservice/lib/sign-token').create(models.PrivateKey).init().then(function (signer) {
var CodesCtrl = require('authcodes').create(models.Codes);
/* models = { Logins, Verifications } */
var LoginsCtrl = require('./authentication-microservice/lib/logins').create({}, CodesCtrl, models);
/* models = { ApiKeys, OauthClients } */
var ClientsCtrl = require('./oauthclient-microservice/lib/oauthclients').createController({}, models, signer);
return {
Codes: CodesCtrl
, Logins: LoginsCtrl
, Clients: ClientsCtrl
, SqlFactory: clientFactory
, models: models
};
});
});
}).then(function (ctrls) {
inProcessCache[experienceId] = ctrls;
return ctrls;
});
return inProcessCache[experienceId];
}
//var jwsUtils = require('./lib/jws-utils').create(signer);
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" ] });
// 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
//function weakDecipher(secret, val) { return require('./weak-crypt').weakDecipher(val, secret); }
//
// Generic Session / Login / Account Routes
//
function parseAccessToken(req, opts) {
var token;
var parts;
var scheme;
var credentials;
if (req.headers && req.headers.authorization) {
parts = req.headers.authorization.split(' ');
if (parts.length !== 2) {
return PromiseA.reject(new Error("malformed Authorization header"));
}
scheme = parts[0];
credentials = parts[1];
if (-1 !== (opts && opts.schemes || ['token', 'bearer']).indexOf(scheme.toLowerCase())) {
token = credentials;
}
}
if (req.body && req.body.access_token) {
if (token) { PromiseA.reject(new Error("token exists in header and body")); }
token = req.body.access_token;
}
// TODO disallow query with req.method === 'GET'
// (cookies should be used for protected static assets)
if (req.query && req.query.access_token) {
if (token) { PromiseA.reject(new Error("token already exists in either header or body and also in query")); }
token = req.query.access_token;
}
/*
err = new Error(challenge());
err.code = 'E_BEARER_REALM';
if (!token) { return PromiseA.reject(err); }
*/
return PromiseA.resolve(token);
}
function getClient(req, token, priv, Controllers) {
if (!token) {
token = req.oauth3.token;
}
var cacheId = '_' + token.k + 'Client';
if (priv[cacheId]) {
return PromiseA.resolve(priv[cacheId]);
}
// TODO could get client directly by token.app (id of client)
priv[cacheId] = Controllers.Clients.login(null, token.k).then(function (apiKey) {
if (!apiKey) {
return PromiseA.reject(new Error("Client no longer valid"));
}
priv[cacheId + 'Key'] = apiKey;
priv[cacheId] = apiKey.oauthClient;
return apiKey.oauthClient;
});
return priv[cacheId];
}
function getAccountsByLogin(req, token, priv, Controllers, loginId, decrypt) {
return getClient(req, req.oauth.token, priv).then(function (oauthClient) {
if (decrypt) {
loginId = scoper.unscope(loginId, oauthClient.secret);
}
return Controllers.models.AccountsLogins.find({ loginId: loginId }).then(function (accounts) {
return PromiseA.all(accounts.map(function (obj) {
return Controllers.models.Accounts.get(obj.accountId)/*.then(function (account) {
account.appScopedId = weakCipher(oauthClient.secret, account.id);
return account;
})*/;
}));
});
});
}
function getAccountsByArray(req, Controllers, arr) {
return PromiseA.all(arr.map(function (accountId) {
return Controllers.models.Accounts.get(accountId.id || accountId);
}));
}
function getAccounts(req, token, priv, Controllers) {
if (!token) {
token = req.oauth3.token;
}
var err;
if (priv._accounts) {
return PromiseA.resolve(priv._accounts);
}
if ((req.oauth3.token.idx || req.oauth3.token.usr) && ('password' === req.oauth3.token.grt || 'login' === req.oauth3.token.as)) {
priv._accounts = getAccountsByLogin(req, req.oauth3.token, priv, Controllers, (req.oauth3.token.idx || req.oauth3.token.usr), !!req.oauth3.token.idx);
} else if (req.oauth3.token.axs && req.oauth3.token.axs.length || req.oauth3.token.acx) {
req.oauth3._accounts = getAccountsByArray(req, Controllers, req.oauth3.token.axs && req.oauth3.token.axs.length && req.oauth3.token.axs || [req.oauth3.token.acx]);
} else {
err = new Error("neither login nor accounts were specified");
err.code = "E_NO_AUTHZ";
req.oauth3._accounts = PromiseA.reject(err);
}
req.oauth3._accounts.then(function (accounts) {
req.oauth3._accounts = accounts;
return accounts;
});
return req.oauth3._accounts;
}
function getLoginId(req, token, priv/*, Controllers*/) {
if (!token) {
token = req.oauth3.token;
}
var cacheId = '_' + token.idx + 'LoginId';
if (priv[cacheId]) {
return PromiseA.resolve(priv[cacheId]);
}
// TODO
// this ends up defeating part of the purpose of JWT (few database calls)
// perhaps the oauthClient secret should be sent, encrypted with a master key,
// with the request? Or just mash the oauthClient secret with the loginId
// and encrypt with the master key?
priv._loginId = getClient(req, token, priv).then(function (oauthClient) {
var loginId;
if (token.idx) {
loginId = scoper.unscope(token.idx, oauthClient.secret);
} else {
loginId = token.usr;
}
priv[cacheId] = loginId;
return loginId;
});
return priv[cacheId];
}
function getLogin(req, token, priv, Controllers) {
if (!token) {
token = req.oauth3.token;
}
var cacheId = '_' + token.idx + 'Login';
if (priv[cacheId]) {
return PromiseA.resolve(priv[cacheId]);
}
priv[cacheId] = getLoginId(req, token, priv).then(function (loginId) {
// DB.Logins.get(hashId)
return Controllers.Logins.rawGet(loginId).then(function (login) {
priv[cacheId] = login;
return login;
});
});
return priv[cacheId];
}
function attachOauth3(req, res, next) {
var privs = {};
req.oauth3 = {};
getAppScopedControllers(req.experienceId).then(function (Controllers) {
return parseAccessToken(req).then(function (token) {
if (!token) {
next();
return;
}
var jwt = require('jsonwebtoken');
var data = jwt.decode(token);
var err;
if (!data) {
err = new Error('not a json web token');
err.code = 'E_NOT_JWT';
res.send({
error: err.code
, error_description: err.message
, error_url: 'https://oauth3.org/docs/errors#' + (err.code || 'E_UNKNOWN_EXCEPTION')
});
// PromiseA.reject(err);
return;
}
req.oauth3.token = token;
req.oauth3.getLoginId = function (token) {
getLoginId(req, token || req.oauth3.token, privs, Controllers);
};
req.oauth3.getLogin = function (token) {
getLogin(req, token || req.oauth3.token, privs, Controllers);
};
// TODO modify prototypes?
req.oauth3.getClient = function (token) {
getClient(req, token || req.oauth3.token, privs, Controllers);
};
// TODO req.oauth3.getAccountIds
req.oauth3.getAccounts = function (token) {
getAccounts(req, token || req.oauth3.token, privs, Controllers);
};
next();
});
});
}
app.use('/', cors);
app.use('/', attachOauth3);
};

View File

@ -110,7 +110,7 @@ function loadPages(pkgConf, packagedPage, req, res, next) {
} }
function getApi(conf, pkgConf, pkgDeps, packagedApi) { function getApi(conf, pkgConf, pkgDeps, packagedApi) {
var PromiseA = require('bluebird'); var PromiseA = pkgDeps.Promise;
var path = require('path'); var path = require('path');
var pkgpath = path.join(pkgConf.apipath, packagedApi.id/*, (packagedApi.api.version || '')*/); var pkgpath = path.join(pkgConf.apipath, packagedApi.id/*, (packagedApi.api.version || '')*/);
@ -162,6 +162,9 @@ function getApi(conf, pkgConf, pkgDeps, packagedApi) {
packagedApi._api_app = myApp; packagedApi._api_app = myApp;
//require('./oauth3-auth').inject(conf, packagedApi._api, pkgConf, pkgDeps); //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 // DEBUG
// //
@ -251,16 +254,18 @@ function layerItUp(pkgConf, router, req, res, next) {
} }
function runApi(opts, router, req, res, next) { function runApi(opts, router, req, res, next) {
var path = require('path');
var pkgConf = opts.config; var pkgConf = opts.config;
var pkgDeps = opts.deps; var pkgDeps = opts.deps;
//var Services = opts.Services; //var Services = opts.Services;
var packagedApi; var packagedApi;
var pathname;
// TODO compile packagesMap // TODO compile packagesMap
// TODO people may want to use the framework in a non-framework way (i.e. to conceal the module name) // 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) { router.packagedApis.some(function (_packagedApi) {
// console.log('[DEBUG _packagedApi.id]', _packagedApi.id); // console.log('[DEBUG _packagedApi.id]', _packagedApi.id);
var pathname = router.pathname; pathname = router.pathname;
if ('/' === pathname) { if ('/' === pathname) {
pathname = ''; pathname = '';
} }
@ -290,7 +295,7 @@ function runApi(opts, router, req, res, next) {
// TODO this identifier may need to be non-deterministic as to transfer if a domain name changes but is still the "same" app // 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?) // (i.e. a company name change. maybe auto vs manual register - just like oauth3?)
// NOTE: probably best to alias the name logically // NOTE: probably best to alias the name logically
, value: (req.hostname + req.pathname).replace(/\/$/, '') , value: (path.join(req.hostname, pathname || '')).replace(/\/$/, '')
}); });
Object.defineProperty(req, 'escapedExperienceId', { Object.defineProperty(req, 'escapedExperienceId', {
enumerable: true enumerable: true

View File

@ -5,9 +5,10 @@
// require everything as lazily as possible until our server // require everything as lazily as possible until our server
// is actually listening on the socket. Bluebird is heavy. // is actually listening on the socket. Bluebird is heavy.
// Even the built-in modules can take dozens of milliseconds to require // Even the built-in modules can take dozens of milliseconds to require
module.exports.create = function (certPaths, serverCallback) { module.exports.create = function (lex, certPaths, serverCallback) {
// Recognize that this secureContexts cache is local to this CPU core // Recognize that this secureContexts cache is local to this CPU core
var secureContexts = {}; var secureContexts = {};
var ciphers = 'ECDH+AESGCM:DH+AESGCM:ECDH+AES128:DH+AES:ECDH+3DES:DH+3DES:RSA+AESGCM:RSA+AES:RSA+3DES:!aNULL:!MD5:!DSS:!AES256';
function createSecureServer() { function createSecureServer() {
var domainname = 'www.example.com'; var domainname = 'www.example.com';
@ -21,7 +22,7 @@ module.exports.create = function (certPaths, serverCallback) {
// https://hynek.me/articles/hardening-your-web-servers-ssl-ciphers/ // https://hynek.me/articles/hardening-your-web-servers-ssl-ciphers/
// https://nodejs.org/api/tls.html // https://nodejs.org/api/tls.html
// removed :ECDH+AES256:DH+AES256 and added :!AES256 because AES-256 wastes CPU // removed :ECDH+AES256:DH+AES256 and added :!AES256 because AES-256 wastes CPU
, ciphers: 'ECDH+AESGCM:DH+AESGCM:ECDH+AES128:DH+AES:ECDH+3DES:DH+3DES:RSA+AESGCM:RSA+AES:RSA+3DES:!aNULL:!MD5:!DSS:!AES256' , ciphers: ciphers
, honorCipherOrder: true , honorCipherOrder: true
}; };
@ -43,5 +44,15 @@ module.exports.create = function (certPaths, serverCallback) {
serverCallback(null, require('https').createServer(secureOpts)); serverCallback(null, require('https').createServer(secureOpts));
} }
createSecureServer(); function createLeServer() {
lex.httpsOptions.ciphers = ciphers;
lex.httpsOptions.honorCipherOrder = true;
serverCallback(null, require('https').createServer(lex.httpsOptions));
}
if (lex) {
createLeServer();
} else {
createSecureServer();
}
}; };

View File

@ -1,28 +1,28 @@
'use strict'; 'use strict';
function tplCaddyfile(conf) { function tplCaddyfile(caddyConf) {
var contents = []; var contents = [];
conf.caddy.domains.forEach(function (hostname) { caddyConf.domains.forEach(function (hostname) {
var content = ""; var content = "";
var pagesname = hostname; var pagesname = hostname;
// TODO prefix // TODO prefix
content+= "https://" + hostname + " {\n" content += "https://" + hostname + " {\n"
+ " gzip\n" + " gzip\n"
+ " tls " + " tls "
+ "/srv/walnut/certs/live/" + hostname + "/fullchain.pem " + "/srv/walnut/certs/live/" + hostname + "/fullchain.pem "
+ "/srv/walnut/certs/live/" + hostname + "/privkey.pem\n" + "/srv/walnut/certs/live/" + hostname + "/privkey.pem\n"
; ;
if (conf.locked) { if (caddyConf.locked) {
content += " root /srv/walnut/init.public/\n"; content += " root /srv/walnut/init.public/\n";
} else { } else {
content += " root " + conf.caddy.sitespath + "/" + pagesname + "/\n"; content += " root " + caddyConf.sitespath + "/" + pagesname + "/\n";
} }
content += content +=
" proxy /api http://localhost:" + conf.localPort.toString() + " {\n" " proxy /api http://localhost:" + caddyConf.localPort.toString() + " {\n"
+ " proxy_header Host {host}\n" + " proxy_header Host {host}\n"
+ " proxy_header X-Forwarded-Host {host}\n" + " proxy_header X-Forwarded-Host {host}\n"
+ " proxy_header X-Forwarded-Proto {scheme}\n" + " proxy_header X-Forwarded-Proto {scheme}\n"
@ -37,10 +37,10 @@ function tplCaddyfile(conf) {
} }
module.exports.tplCaddyfile = tplCaddyfile; module.exports.tplCaddyfile = tplCaddyfile;
module.exports.create = function (config) { module.exports.create = function (caddyConf) {
var spawn = require('child_process').spawn; var spawn = require('child_process').spawn;
var caddyBin = config.caddy.bin; var caddyBin = caddyConf.bin;
var caddyConf = config.caddy.conf; var caddyfilePath = caddyConf.conf;
// TODO put up a booting / lock screen on boot // TODO put up a booting / lock screen on boot
// and wait for all to be grabbed from db // and wait for all to be grabbed from db
// NOTE caddy cannot yet support multiple roots // NOTE caddy cannot yet support multiple roots
@ -49,8 +49,8 @@ module.exports.create = function (config) {
var fs = require('fs'); var fs = require('fs');
// TODO this should be expanded to include proxies a la proxydyn // TODO this should be expanded to include proxies a la proxydyn
function writeCaddyfile(conf, cb) { function writeCaddyfile(caddyConf, cb) {
fs.readdir(config.caddy.sitespath, function (err, nodes) { fs.readdir(caddyConf.sitespath, function (err, nodes) {
if (err) { if (err) {
if (cb) { if (cb) {
cb(err); cb(err);
@ -61,12 +61,12 @@ module.exports.create = function (config) {
throw err; throw err;
} }
conf.caddy.domains = nodes.filter(function (node) { caddyConf.domains = nodes.filter(function (node) {
return /\./.test(node) && !/(^\.)|([\/\:\\])/.test(node); return /\./.test(node) && !/(^\.)|([\/\:\\])/.test(node);
}); });
var contents = tplCaddyfile(conf); var contents = tplCaddyfile(caddyConf);
fs.writeFile(caddyConf, contents, 'utf8', function (err) { fs.writeFile(caddyfilePath, contents, 'utf8', function (err) {
if (err) { if (err) {
if (cb) { if (cb) {
cb(err); cb(err);
@ -82,9 +82,9 @@ module.exports.create = function (config) {
}); });
} }
function spawnCaddy(conf, cb) { function spawnCaddy(caddyConf, cb) {
console.log('[CADDY] start'); console.log('[CADDY] start');
writeCaddyfile(conf, function (err) { writeCaddyfile(caddyfilePath, function (err) {
if (err) { if (err) {
console.error('[writeCaddyfile]'); console.error('[writeCaddyfile]');
console.error(err.stack); console.error(err.stack);
@ -108,7 +108,7 @@ module.exports.create = function (config) {
// Command failed: killall caddy // Command failed: killall caddy
// caddy: no process found // caddy: no process found
} }
caddy = spawn(caddyBin, ['-conf', caddyConf], { stdio: ['ignore', 'pipe', 'pipe'] }); caddy = spawn(caddyBin, ['-conf', caddyfilePath], { stdio: ['ignore', 'pipe', 'pipe'] });
caddy.stdout.on('data', function (str) { caddy.stdout.on('data', function (str) {
console.error('[Caddy]', str.toString('utf8')); console.error('[Caddy]', str.toString('utf8'));
}); });
@ -123,7 +123,7 @@ module.exports.create = function (config) {
console.log(code, signal); console.log(code, signal);
caddy = null; caddy = null;
setTimeout(function () { setTimeout(function () {
spawnCaddy(conf); spawnCaddy(caddyConf);
}, 1 * 1000); }, 1 * 1000);
}); });
@ -151,8 +151,8 @@ module.exports.create = function (config) {
return { return {
spawn: spawnCaddy spawn: spawnCaddy
, update: function (conf) { , update: function (caddyConf) {
return writeCaddyfile(conf, sighup); return writeCaddyfile(caddyConf, sighup);
} }
, sighup: sighup , sighup: sighup
}; };

View File

@ -1,6 +1,6 @@
'use strict'; 'use strict';
module.exports.create = function (webserver, info, state) { module.exports.create = function (webserver, conf, state) {
if (!state) { if (!state) {
state = {}; state = {};
} }
@ -14,31 +14,31 @@ module.exports.create = function (webserver, info, state) {
var sqlstores = {}; var sqlstores = {};
var models = {}; var models = {};
var systemFactory = require('sqlite3-cluster/client').createClientFactory({ var systemFactory = require('sqlite3-cluster/client').createClientFactory({
dirname: path.join(__dirname, '..', '..', 'var') // TODO info.conf dirname: path.join(__dirname, '..', '..', 'var') // TODO conf
, prefix: 'com.example.' , prefix: 'com.example.'
//, dbname: 'config' //, dbname: 'config'
, suffix: '' , suffix: ''
, ext: '.sqlite3' , ext: '.sqlite3'
, sock: info.conf.sqlite3Sock , sock: conf.sqlite3Sock
, ipcKey: info.conf.ipcKey , ipcKey: conf.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 info.conf , dirname: path.join(__dirname, '..', '..', 'var') // TODO conf
, prefix: 'com.example.' , prefix: 'com.example.'
//, dbname: 'cluster' //, dbname: 'cluster'
, suffix: '' , suffix: ''
, ext: '.sqlcipher' , ext: '.sqlcipher'
, sock: info.conf.sqlite3Sock , sock: conf.sqlite3Sock
, ipcKey: info.conf.ipcKey , ipcKey: conf.ipcKey
}); });
var cstore = require('cluster-store'); var cstore = require('cluster-store');
var redirectives; var redirectives;
app.disable('x-powered-by'); app.disable('x-powered-by');
if (info.conf.trustProxy) { if (conf.trustProxy) {
console.info('[Trust Proxy]'); console.info('[Trust Proxy]');
app.set('trust proxy', ['loopback']); app.set('trust proxy', ['loopback']);
//app.set('trust proxy', function (ip) { console.log('[ip]', ip); return true; }); //app.set('trust proxy', function (ip) { console.log('[ip]', ip); return true; });
@ -87,8 +87,8 @@ module.exports.create = function (webserver, info, state) {
host = host.toLowerCase(); host = host.toLowerCase();
// TODO this should be hot loadable / changeable // TODO this should be hot loadable / changeable
if (!redirectives && info.conf.redirects) { if (!redirectives && conf.redirects) {
redirectives = require('./hostname-redirects').compile(info.conf.redirects); redirectives = require('./hostname-redirects').compile(conf.redirects);
} }
if (!/^www\./.test(host) && !redirectives) { if (!/^www\./.test(host) && !redirectives) {
@ -126,10 +126,10 @@ module.exports.create = function (webserver, info, state) {
// TODO security on memstore // TODO security on memstore
// TODO memstoreFactory.create // TODO memstoreFactory.create
cstore.create({ cstore.create({
sock: info.conf.memstoreSock sock: conf.memstoreSock
, connect: info.conf.memstoreSock , connect: conf.memstoreSock
// TODO implement // TODO implement
, key: info.conf.ipcKey , key: conf.ipcKey
}).then(function (_memstore) { }).then(function (_memstore) {
memstore = _memstore; memstore = _memstore;
return memstore; return memstore;
@ -171,14 +171,14 @@ module.exports.create = function (webserver, info, state) {
, vhostsMap: vhostsMap , vhostsMap: vhostsMap
, vhostPatterns: null , vhostPatterns: null
, server: webserver , server: webserver
, externalPort: info.conf.externalPort , externalPort: conf.externalPort
, privkey: info.conf.privkey , privkey: conf.privkey
, pubkey: info.conf.pubkey , pubkey: conf.pubkey
, redirects: info.conf.redirects , redirects: conf.redirects
, apiPrefix: '/api' , apiPrefix: '/api'
, 'org.oauth3.consumer': info.conf['org.oauth3.consumer'] , 'org.oauth3.consumer': conf['org.oauth3.consumer']
, 'org.oauth3.provider': info.conf['org.oauth3.provider'] , 'org.oauth3.provider': conf['org.oauth3.provider']
, keys: info.conf.keys , keys: conf.keys
}; };
var pkgDeps = { var pkgDeps = {
memstore: memstore memstore: memstore
@ -187,10 +187,11 @@ module.exports.create = function (webserver, info, state) {
, systemSqlFactory: systemFactory , systemSqlFactory: systemFactory
//, handlePromise: require('./lib/common').promisableRequest; //, handlePromise: require('./lib/common').promisableRequest;
//, handleRejection: require('./lib/common').rejectableRequest; //, handleRejection: require('./lib/common').rejectableRequest;
//, localPort: info.conf.localPort //, localPort: conf.localPort
, Promise: PromiseA , Promise: PromiseA
, express: express , express: express
, app: app , app: app
//, oauthmodels: require('oauthcommon/example-oauthmodels').create(conf)
}; };
var Services = require('./services-loader').create(pkgConf, { var Services = require('./services-loader').create(pkgConf, {
memstore: memstore memstore: memstore
@ -223,7 +224,7 @@ module.exports.create = function (webserver, info, state) {
config: pkgConf config: pkgConf
, deps: pkgDeps , deps: pkgDeps
, services: Services , services: Services
, conf: info.conf , conf: conf
}, req, res, next); }, req, res, next);
} }

View File

@ -58,6 +58,7 @@
"cookie-session": "1.x", "cookie-session": "1.x",
"cookie-signature": "^1.0.6", "cookie-signature": "^1.0.6",
"crc": "^3.2.1", "crc": "^3.2.1",
"ddns-cli": "^1.2.1",
"debug": "^2.1.3", "debug": "^2.1.3",
"depd": "^1.0.0", "depd": "^1.0.0",
"destroy": "^1.0.3", "destroy": "^1.0.3",
@ -81,6 +82,7 @@
"json-storage": "2.x", "json-storage": "2.x",
"jsonwebtoken": "^5.4.0", "jsonwebtoken": "^5.4.0",
"lodash": "2.x", "lodash": "2.x",
"letsencrypt-express": "1.1.x",
"masterquest-sqlite3": "git://github.com/coolaj86/node-masterquest-sqlite3.git", "masterquest-sqlite3": "git://github.com/coolaj86/node-masterquest-sqlite3.git",
"media-typer": "^0.3.0", "media-typer": "^0.3.0",
"methods": "^1.1.1", "methods": "^1.1.1",
@ -105,7 +107,7 @@
"request": "2.44.0", "request": "2.44.0",
"request-ip": "^1.1.1", "request-ip": "^1.1.1",
"scmp": "1.x", "scmp": "1.x",
"secret-utils": "1.x", "secret-utils": "^2.0.0",
"semver": "^4.3.1", "semver": "^4.3.1",
"send": "^0.12.2", "send": "^0.12.2",
"serve-favicon": "2.x", "serve-favicon": "2.x",