diff --git a/bin/walnut b/bin/walnut deleted file mode 100755 index 03ea920..0000000 --- a/bin/walnut +++ /dev/null @@ -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); diff --git a/bin/walnut b/bin/walnut new file mode 120000 index 0000000..b8f03e9 --- /dev/null +++ b/bin/walnut @@ -0,0 +1 @@ +walnut.js \ No newline at end of file diff --git a/bin/walnut.js b/bin/walnut.js deleted file mode 120000 index f26d3ff..0000000 --- a/bin/walnut.js +++ /dev/null @@ -1 +0,0 @@ -walnut \ No newline at end of file diff --git a/bin/walnut.js b/bin/walnut.js new file mode 100755 index 0000000..03ea920 --- /dev/null +++ b/bin/walnut.js @@ -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); diff --git a/boot/master.js b/boot/master.js index 0f058da..b8e23c8 100644 --- a/boot/master.js +++ b/boot/master.js @@ -9,12 +9,46 @@ console.info('arch:', process.arch); console.info('platform:', process.platform); 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 cluster = require('cluster'); //var minWorkers = 2; var numCores = 2; // Math.max(minWorkers, require('os').cpus().length); var workers = []; 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() { if (workers.length < numCores) { @@ -26,53 +60,15 @@ cluster.on('online', function (worker) { console.info('[MASTER] Worker ' + worker.process.pid + ' is online'); 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) { state.firstRun = false; if (useCaddy) { - caddy = require('../lib/spawn-caddy').create(config); + caddy = require('../lib/spawn-caddy').create(caddy); // 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) { if ('walnut.webserver.listening' !== msg.type) { 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 state.caddy = caddy; 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 sqlstore = results.sqlstore; info.type = 'walnut.webserver.onrequest'; // TODO let this load after server is listening - info.conf['org.oauth3.consumer'] = config['org.oauth3.consumer']; - info.conf['org.oauth3.provider'] = config['org.oauth3.provider']; - info.conf.keys = config.keys; - info.conf.memstoreSock = config.memstoreSock; - info.conf.sqlite3Sock = config.sqlite3Sock; + 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.privkey = config.privkey; + //info.conf.pubkey = config.pubkey; info.conf.redirects = [ { "ip": false, "id": "*", "value": false } // default no-www @@ -116,10 +112,10 @@ cluster.on('online', function (worker) { // 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 + // keys + // letsencrypt + // com.example.provider + // com.example.consumer worker.send(info); }); } diff --git a/boot/worker.js b/boot/worker.js index 1e039fd..d5d58c9 100644 --- a/boot/worker.js +++ b/boot/worker.js @@ -4,9 +4,9 @@ module.exports.create = function (opts) { var id = '0'; var promiseApp; - function createAndBindInsecure(message, cb) { + function createAndBindInsecure(lex, message, cb) { // 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'); // 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) { + var lex; + + if (message.conf.letsencrypt) { + lex = createLe(message.conf); + } + // 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) { console.error('[ERROR] worker.js'); 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 process.nextTick(function () { - createAndBindInsecure(message, cb); + createAndBindInsecure(lex, message, cb); }); // we are returning the promise result to the caller @@ -67,7 +106,7 @@ module.exports.create = function (opts) { 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' }); diff --git a/lib/insecure-server.js b/lib/insecure-server.js index 1dc5837..2ac96a5 100644 --- a/lib/insecure-server.js +++ b/lib/insecure-server.js @@ -1,6 +1,6 @@ '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 appPromise; //var app; @@ -101,7 +101,13 @@ module.exports.create = function (securePort, insecurePort, info, serverCallback 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); }; diff --git a/lib/local-server.js b/lib/local-server.js index 37d776d..e088213 100644 --- a/lib/local-server.js +++ b/lib/local-server.js @@ -2,7 +2,7 @@ // 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 (certPaths, port, info, serverCallback) { +module.exports.create = function (lex, certPaths, port, info, serverCallback) { function initServer(err, server) { var app; var promiseApp; @@ -29,7 +29,7 @@ module.exports.create = function (certPaths, port, info, serverCallback) { */ // 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 if (app) { app(req, res); @@ -41,11 +41,18 @@ module.exports.create = function (certPaths, port, info, serverCallback) { 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('./sni-server').create(certPaths, initServer); + require('./sni-server').create(lex, certPaths, initServer); } else { initServer(null, require('http').createServer()); } diff --git a/lib/oauth3-auth.js b/lib/oauth3-auth.js deleted file mode 100644 index 024b28f..0000000 --- a/lib/oauth3-auth.js +++ /dev/null @@ -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); -}; diff --git a/lib/package-server.js b/lib/package-server.js index ea2be47..d1451c0 100644 --- a/lib/package-server.js +++ b/lib/package-server.js @@ -110,7 +110,7 @@ function loadPages(pkgConf, packagedPage, req, res, next) { } function getApi(conf, pkgConf, pkgDeps, packagedApi) { - var PromiseA = require('bluebird'); + var PromiseA = pkgDeps.Promise; var path = require('path'); 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; //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 // @@ -251,16 +254,18 @@ function layerItUp(pkgConf, router, req, res, next) { } 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); - var pathname = router.pathname; + pathname = router.pathname; if ('/' === 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 // (i.e. a company name change. maybe auto vs manual register - just like oauth3?) // 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', { enumerable: true diff --git a/lib/sni-server.js b/lib/sni-server.js index 8e30e5e..e1c48e4 100644 --- a/lib/sni-server.js +++ b/lib/sni-server.js @@ -5,9 +5,10 @@ // require everything as lazily as possible until our server // is actually listening on the socket. Bluebird is heavy. // 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 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() { 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://nodejs.org/api/tls.html // 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 }; @@ -43,5 +44,15 @@ module.exports.create = function (certPaths, serverCallback) { 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(); + } }; diff --git a/lib/spawn-caddy.js b/lib/spawn-caddy.js index f454672..195f9d4 100644 --- a/lib/spawn-caddy.js +++ b/lib/spawn-caddy.js @@ -1,28 +1,28 @@ 'use strict'; -function tplCaddyfile(conf) { +function tplCaddyfile(caddyConf) { var contents = []; - conf.caddy.domains.forEach(function (hostname) { + caddyConf.domains.forEach(function (hostname) { var content = ""; var pagesname = hostname; // TODO prefix - content+= "https://" + hostname + " {\n" + content += "https://" + hostname + " {\n" + " gzip\n" + " tls " + "/srv/walnut/certs/live/" + hostname + "/fullchain.pem " + "/srv/walnut/certs/live/" + hostname + "/privkey.pem\n" ; - if (conf.locked) { + if (caddyConf.locked) { content += " root /srv/walnut/init.public/\n"; } else { - content += " root " + conf.caddy.sitespath + "/" + pagesname + "/\n"; + content += " root " + caddyConf.sitespath + "/" + pagesname + "/\n"; } content += - " proxy /api http://localhost:" + conf.localPort.toString() + " {\n" + " proxy /api http://localhost:" + caddyConf.localPort.toString() + " {\n" + " proxy_header Host {host}\n" + " proxy_header X-Forwarded-Host {host}\n" + " proxy_header X-Forwarded-Proto {scheme}\n" @@ -37,10 +37,10 @@ function tplCaddyfile(conf) { } module.exports.tplCaddyfile = tplCaddyfile; -module.exports.create = function (config) { +module.exports.create = function (caddyConf) { var spawn = require('child_process').spawn; - var caddyBin = config.caddy.bin; - var caddyConf = config.caddy.conf; + var caddyBin = caddyConf.bin; + var caddyfilePath = caddyConf.conf; // TODO put up a booting / lock screen on boot // and wait for all to be grabbed from db // NOTE caddy cannot yet support multiple roots @@ -49,8 +49,8 @@ module.exports.create = function (config) { var fs = require('fs'); // TODO this should be expanded to include proxies a la proxydyn - function writeCaddyfile(conf, cb) { - fs.readdir(config.caddy.sitespath, function (err, nodes) { + function writeCaddyfile(caddyConf, cb) { + fs.readdir(caddyConf.sitespath, function (err, nodes) { if (err) { if (cb) { cb(err); @@ -61,12 +61,12 @@ module.exports.create = function (config) { throw err; } - conf.caddy.domains = nodes.filter(function (node) { + caddyConf.domains = nodes.filter(function (node) { return /\./.test(node) && !/(^\.)|([\/\:\\])/.test(node); }); - var contents = tplCaddyfile(conf); - fs.writeFile(caddyConf, contents, 'utf8', function (err) { + var contents = tplCaddyfile(caddyConf); + fs.writeFile(caddyfilePath, contents, 'utf8', function (err) { if (err) { if (cb) { cb(err); @@ -82,9 +82,9 @@ module.exports.create = function (config) { }); } - function spawnCaddy(conf, cb) { + function spawnCaddy(caddyConf, cb) { console.log('[CADDY] start'); - writeCaddyfile(conf, function (err) { + writeCaddyfile(caddyfilePath, function (err) { if (err) { console.error('[writeCaddyfile]'); console.error(err.stack); @@ -108,7 +108,7 @@ module.exports.create = function (config) { // Command failed: killall caddy // 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) { console.error('[Caddy]', str.toString('utf8')); }); @@ -123,7 +123,7 @@ module.exports.create = function (config) { console.log(code, signal); caddy = null; setTimeout(function () { - spawnCaddy(conf); + spawnCaddy(caddyConf); }, 1 * 1000); }); @@ -151,8 +151,8 @@ module.exports.create = function (config) { return { spawn: spawnCaddy - , update: function (conf) { - return writeCaddyfile(conf, sighup); + , update: function (caddyConf) { + return writeCaddyfile(caddyConf, sighup); } , sighup: sighup }; diff --git a/lib/worker.js b/lib/worker.js index c18e0e4..dea2221 100644 --- a/lib/worker.js +++ b/lib/worker.js @@ -1,6 +1,6 @@ 'use strict'; -module.exports.create = function (webserver, info, state) { +module.exports.create = function (webserver, conf, state) { if (!state) { state = {}; } @@ -14,31 +14,31 @@ module.exports.create = function (webserver, info, state) { var sqlstores = {}; var models = {}; 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.' //, dbname: 'config' , suffix: '' , ext: '.sqlite3' - , sock: info.conf.sqlite3Sock - , ipcKey: info.conf.ipcKey + , sock: conf.sqlite3Sock + , ipcKey: conf.ipcKey }); var clientFactory = require('sqlite3-cluster/client').createClientFactory({ algorithm: 'aes' , bits: 128 , mode: 'cbc' - , dirname: path.join(__dirname, '..', '..', 'var') // TODO info.conf + , dirname: path.join(__dirname, '..', '..', 'var') // TODO conf , prefix: 'com.example.' //, dbname: 'cluster' , suffix: '' , ext: '.sqlcipher' - , sock: info.conf.sqlite3Sock - , ipcKey: info.conf.ipcKey + , sock: conf.sqlite3Sock + , ipcKey: conf.ipcKey }); var cstore = require('cluster-store'); var redirectives; app.disable('x-powered-by'); - if (info.conf.trustProxy) { + if (conf.trustProxy) { console.info('[Trust Proxy]'); app.set('trust proxy', ['loopback']); //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(); // TODO this should be hot loadable / changeable - if (!redirectives && info.conf.redirects) { - redirectives = require('./hostname-redirects').compile(info.conf.redirects); + if (!redirectives && conf.redirects) { + redirectives = require('./hostname-redirects').compile(conf.redirects); } if (!/^www\./.test(host) && !redirectives) { @@ -126,10 +126,10 @@ module.exports.create = function (webserver, info, state) { // TODO security on memstore // TODO memstoreFactory.create cstore.create({ - sock: info.conf.memstoreSock - , connect: info.conf.memstoreSock + sock: conf.memstoreSock + , connect: conf.memstoreSock // TODO implement - , key: info.conf.ipcKey + , key: conf.ipcKey }).then(function (_memstore) { memstore = _memstore; return memstore; @@ -171,14 +171,14 @@ module.exports.create = function (webserver, info, state) { , vhostsMap: vhostsMap , vhostPatterns: null , server: webserver - , externalPort: info.conf.externalPort - , privkey: info.conf.privkey - , pubkey: info.conf.pubkey - , redirects: info.conf.redirects + , externalPort: conf.externalPort + , privkey: conf.privkey + , pubkey: conf.pubkey + , redirects: conf.redirects , apiPrefix: '/api' - , 'org.oauth3.consumer': info.conf['org.oauth3.consumer'] - , 'org.oauth3.provider': info.conf['org.oauth3.provider'] - , keys: info.conf.keys + , 'org.oauth3.consumer': conf['org.oauth3.consumer'] + , 'org.oauth3.provider': conf['org.oauth3.provider'] + , keys: conf.keys }; var pkgDeps = { memstore: memstore @@ -187,10 +187,11 @@ module.exports.create = function (webserver, info, state) { , systemSqlFactory: systemFactory //, handlePromise: require('./lib/common').promisableRequest; //, handleRejection: require('./lib/common').rejectableRequest; - //, localPort: info.conf.localPort + //, localPort: conf.localPort , Promise: PromiseA , express: express , app: app + //, oauthmodels: require('oauthcommon/example-oauthmodels').create(conf) }; var Services = require('./services-loader').create(pkgConf, { memstore: memstore @@ -223,7 +224,7 @@ module.exports.create = function (webserver, info, state) { config: pkgConf , deps: pkgDeps , services: Services - , conf: info.conf + , conf: conf }, req, res, next); } diff --git a/package.json b/package.json index c1e6cfa..d29ba9e 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,7 @@ "cookie-session": "1.x", "cookie-signature": "^1.0.6", "crc": "^3.2.1", + "ddns-cli": "^1.2.1", "debug": "^2.1.3", "depd": "^1.0.0", "destroy": "^1.0.3", @@ -81,6 +82,7 @@ "json-storage": "2.x", "jsonwebtoken": "^5.4.0", "lodash": "2.x", + "letsencrypt-express": "1.1.x", "masterquest-sqlite3": "git://github.com/coolaj86/node-masterquest-sqlite3.git", "media-typer": "^0.3.0", "methods": "^1.1.1", @@ -105,7 +107,7 @@ "request": "2.44.0", "request-ip": "^1.1.1", "scmp": "1.x", - "secret-utils": "1.x", + "secret-utils": "^2.0.0", "semver": "^4.3.1", "send": "^0.12.2", "serve-favicon": "2.x",