From 95ff2d9247c4a297a17cf77607017e9e94cedd4a Mon Sep 17 00:00:00 2001 From: Tankred Hase Date: Thu, 17 Aug 2017 19:16:49 +0800 Subject: [PATCH] Cleanup app/init/koa-middlewares --- index.js | 8 +- src/app.js | 144 --------------------------------- src/app/index.js | 67 +++++++++++++++ src/app/middleware.js | 65 +++++++++++++++ src/dao/mongo.js | 2 + src/index.js | 32 ++++++++ test/integration/app-test.js | 5 +- test/integration/mongo-test.js | 6 ++ 8 files changed, 178 insertions(+), 151 deletions(-) delete mode 100644 src/app.js create mode 100644 src/app/index.js create mode 100644 src/app/middleware.js create mode 100644 src/index.js diff --git a/index.js b/index.js index d42c166..fe6da9c 100644 --- a/index.js +++ b/index.js @@ -22,7 +22,7 @@ const numCPUs = require('os').cpus().length; const config = require('config'); const log = require('npmlog'); -log.level = config.log.level; // set log level depending on process.env.NODE_ENV +log.level = config.log.level; // // Start worker cluster depending on number of CPUs @@ -32,13 +32,13 @@ if (cluster.isMaster) { for (let i = 0; i < numCPUs; i++) { cluster.fork(); } - cluster.on('fork', worker => log.info('cluster', 'Forked worker #%s [pid:%s]', worker.id, worker.process.pid)); + cluster.on('fork', worker => log.info('cluster', `Forked worker #${worker.id} [pid:${worker.process.pid}]`)); cluster.on('exit', worker => { - log.warn('cluster', 'Worker #%s [pid:%s] died', worker.id, worker.process.pid); + log.warn('cluster', `Worker #${worker.id} [pid:${worker.process.pid}] died`); setTimeout(() => cluster.fork(), 5000); }); } else { - require('./src/app'); + require('./src'); } // diff --git a/src/app.js b/src/app.js deleted file mode 100644 index b4f7eb5..0000000 --- a/src/app.js +++ /dev/null @@ -1,144 +0,0 @@ -/** - * Mailvelope - secure email with OpenPGP encryption for Webmail - * Copyright (C) 2016 Mailvelope GmbH - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License version 3 - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -'use strict'; - -const Koa = require('koa'); -const koaBody = require('koa-body'); -const log = require('npmlog'); -const config = require('config'); -const serve = require('koa-static'); -const router = require('koa-router')(); -const util = require('./service/util'); -const Mongo = require('./dao/mongo'); -const Email = require('./email/email'); -const PGP = require('./service/pgp'); -const PublicKey = require('./service/public-key'); -const HKP = require('./route/hkp'); -const REST = require('./route/rest'); - -const app = new Koa(); - -let mongo; -let email; -let pgp; -let publicKey; -let hkp; -let rest; - -// -// Configure koa HTTP server -// - -// HKP routes -router.post('/pks/add', ctx => hkp.add(ctx)); -router.get('/pks/lookup', ctx => hkp.lookup(ctx)); - -// REST api routes -router.post('/api/v1/key', ctx => rest.create(ctx)); -router.get('/api/v1/key', ctx => rest.query(ctx)); -router.del('/api/v1/key', ctx => rest.remove(ctx)); - -// Redirect all http traffic to https -app.use(async (ctx, next) => { - if (util.isTrue(config.server.httpsUpgrade) && util.checkHTTP(ctx)) { - ctx.redirect(`https://${ctx.hostname}${ctx.url}`); - } else { - await next(); - } -}); - -// Set HTTP response headers -app.use(async (ctx, next) => { - // HSTS - if (util.isTrue(config.server.httpsUpgrade)) { - ctx.set('Strict-Transport-Security', 'max-age=16070400'); - } - // HPKP - if (config.server.httpsKeyPin && config.server.httpsKeyPinBackup) { - ctx.set('Public-Key-Pins', `pin-sha256="${config.server.httpsKeyPin}"; pin-sha256="${config.server.httpsKeyPinBackup}"; max-age=16070400`); - } - // CSP - ctx.set('Content-Security-Policy', "default-src 'self'; object-src 'none'; script-src 'self' code.jquery.com; style-src 'self' maxcdn.bootstrapcdn.com; font-src 'self' maxcdn.bootstrapcdn.com"); - // Prevent rendering website in foreign iframe (Clickjacking) - ctx.set('X-Frame-Options', 'DENY'); - // CORS - ctx.set('Access-Control-Allow-Origin', '*'); - ctx.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS'); - ctx.set('Access-Control-Allow-Headers', 'Content-Type'); - ctx.set('Connection', 'keep-alive'); - await next(); -}); - -app.use(koaBody({ - multipart: true, - formLimit: '1mb' -})); - -app.use(router.routes()); -app.use(router.allowedMethods()); - -// serve static files -app.use(serve(`${__dirname}/static`)); - -app.on('error', (error, ctx) => { - if (error.status) { - log.verbose('app', 'Request faild: %s, %s', error.status, error.message); - } else { - log.error('app', 'Unknown error', error, ctx); - } -}); - -// -// Module initialization -// - -function injectDependencies() { - mongo = new Mongo(); - email = new Email(); - pgp = new PGP(); - publicKey = new PublicKey(pgp, mongo, email); - hkp = new HKP(publicKey); - rest = new REST(publicKey); -} - -// -// Start app ... connect to the database and start listening -// - -if (!global.testing) { // don't automatically start server in tests - (async () => { - try { - const app = await init(); - app.listen(config.server.port); - log.info('app', `Ready to rock! Listening on http://localhost:${config.server.port}`); - } catch (err) { - log.error('app', 'Initialization failed!', err); - } - })(); -} - -async function init() { - log.level = config.log.level; // set log level depending on process.env.NODE_ENV - injectDependencies(); - email.init(config.email); - log.info('app', 'Connecting to MongoDB ...'); - await mongo.init(config.mongo); - return app; -} - -module.exports = init; diff --git a/src/app/index.js b/src/app/index.js new file mode 100644 index 0000000..85ab831 --- /dev/null +++ b/src/app/index.js @@ -0,0 +1,67 @@ +/** + * Mailvelope - secure email with OpenPGP encryption for Webmail + * Copyright (C) 2016 Mailvelope GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License version 3 + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +'use strict'; + +const Koa = require('koa'); +const config = require('config'); +const serve = require('koa-static'); +const router = require('koa-router')(); +const middleware = require('./middleware'); +const Mongo = require('../dao/mongo'); +const Email = require('../email/email'); +const PGP = require('../service/pgp'); +const PublicKey = require('../service/public-key'); +const HKP = require('../route/hkp'); +const REST = require('../route/rest'); + +const app = new Koa(); + +let hkp; +let rest; + +// HKP and REST api routes +router.post('/pks/add', ctx => hkp.add(ctx)); +router.get('/pks/lookup', ctx => hkp.lookup(ctx)); +router.post('/api/v1/key', ctx => rest.create(ctx)); +router.get('/api/v1/key', ctx => rest.query(ctx)); +router.del('/api/v1/key', ctx => rest.remove(ctx)); + +// setup koa middlewares +app.on('error', middleware.logUnknownError); +app.use(middleware.upgradeToHTTPS); +app.use(middleware.setHTTPResponseHeaders); +app.use(middleware.parseBody()); +app.use(router.routes()); +app.use(router.allowedMethods()); +app.use(serve(`${__dirname}/../static`)); + +async function init() { + // inject dependencies + const mongo = new Mongo(); + const email = new Email(); + const pgp = new PGP(); + const publicKey = new PublicKey(pgp, mongo, email); + hkp = new HKP(publicKey); + rest = new REST(publicKey); + // init DAOs + email.init(config.email); + await mongo.init(config.mongo); + return app; +} + +module.exports = init; diff --git a/src/app/middleware.js b/src/app/middleware.js new file mode 100644 index 0000000..533ca1b --- /dev/null +++ b/src/app/middleware.js @@ -0,0 +1,65 @@ +/** + * Mailvelope - secure email with OpenPGP encryption for Webmail + * Copyright (C) 2016 Mailvelope GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License version 3 + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +'use strict'; + +const log = require('npmlog'); +const config = require('config'); +const koaBody = require('koa-body'); +const util = require('../service/util'); + +exports.upgradeToHTTPS = async function(ctx, next) { + if (util.isTrue(config.server.httpsUpgrade) && util.checkHTTP(ctx)) { + ctx.redirect(`https://${ctx.hostname}${ctx.url}`); + } else { + await next(); + } +}; + +exports.setHTTPResponseHeaders = async function(ctx, next) { + // HSTS + if (util.isTrue(config.server.httpsUpgrade)) { + ctx.set('Strict-Transport-Security', 'max-age=16070400'); + } + // HPKP + if (config.server.httpsKeyPin && config.server.httpsKeyPinBackup) { + ctx.set('Public-Key-Pins', `pin-sha256="${config.server.httpsKeyPin}"; pin-sha256="${config.server.httpsKeyPinBackup}"; max-age=16070400`); + } + // CSP + ctx.set('Content-Security-Policy', "default-src 'self'; object-src 'none'; script-src 'self' code.jquery.com; style-src 'self' maxcdn.bootstrapcdn.com; font-src 'self' maxcdn.bootstrapcdn.com"); + // Prevent rendering website in foreign iframe (Clickjacking) + ctx.set('X-Frame-Options', 'DENY'); + // CORS + ctx.set('Access-Control-Allow-Origin', '*'); + ctx.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS'); + ctx.set('Access-Control-Allow-Headers', 'Content-Type'); + ctx.set('Connection', 'keep-alive'); + await next(); +}; + +exports.parseBody = () => koaBody({ + multipart: true, + formLimit: '1mb' +}); + +exports.logUnknownError = function(error, ctx) { + if (error.status) { + log.verbose('middleware', 'Request faild: %s, %s', error.status, error.message); + } else { + log.error('middleware', 'Unknown error', error, ctx); + } +}; diff --git a/src/dao/mongo.js b/src/dao/mongo.js index d61d572..12f5170 100644 --- a/src/dao/mongo.js +++ b/src/dao/mongo.js @@ -17,6 +17,7 @@ 'use strict'; +const log = require('npmlog'); const MongoClient = require('mongodb').MongoClient; /** @@ -31,6 +32,7 @@ class Mongo { * @yield {undefined} */ async init({uri, user, pass}) { + log.info('mongo', 'Connecting to MongoDB ...'); const url = `mongodb://${user}:${pass}@${uri}`; this._db = await MongoClient.connect(url); } diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..a5986bd --- /dev/null +++ b/src/index.js @@ -0,0 +1,32 @@ +/** + * Mailvelope - secure email with OpenPGP encryption for Webmail + * Copyright (C) 2016 Mailvelope GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License version 3 + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +'use strict'; + +const log = require('npmlog'); +const config = require('config'); +const init = require('./app'); + +(async () => { + try { + const app = await init(); + app.listen(config.server.port); + log.info('app', `Listening on http://localhost:${config.server.port}`); + } catch (err) { + log.error('app', 'Initialization failed!', err); + } +})(); diff --git a/test/integration/app-test.js b/test/integration/app-test.js index ae5445e..bd8a09e 100644 --- a/test/integration/app-test.js +++ b/test/integration/app-test.js @@ -25,6 +25,8 @@ describe('Koa App (HTTP Server) Integration Tests', function() { before(async () => { sandbox = sinon.sandbox.create(); + sandbox.stub(log); + publicKeyArmored = fs.readFileSync(`${__dirname}/../key1.asc`, 'utf8'); mongo = new Mongo(); await mongo.init(config.mongo); @@ -39,9 +41,6 @@ describe('Koa App (HTTP Server) Integration Tests', function() { use() {} }); - sandbox.stub(log); - - global.testing = true; const init = require('../../src/app'); app = await init(); }); diff --git a/test/integration/mongo-test.js b/test/integration/mongo-test.js index 9a9f9bb..ef20411 100644 --- a/test/integration/mongo-test.js +++ b/test/integration/mongo-test.js @@ -1,5 +1,6 @@ 'use strict'; +const log = require('npmlog'); const config = require('config'); const Mongo = require('../../src/dao/mongo'); @@ -7,9 +8,13 @@ describe('Mongo Integration Tests', function() { this.timeout(20000); const DB_TYPE = 'apple'; + let sandbox; let mongo; before(async () => { + sandbox = sinon.sandbox.create(); + sandbox.stub(log); + mongo = new Mongo(); await mongo.init(config.mongo); }); @@ -19,6 +24,7 @@ describe('Mongo Integration Tests', function() { }); after(async () => { + sandbox.restore(); await mongo.clear(DB_TYPE); await mongo.disconnect(); });