From 49b24a5cb4eb9c716915dcfa300838fce3b74e31 Mon Sep 17 00:00:00 2001 From: Tankred Hase Date: Thu, 17 Aug 2017 15:34:47 +0800 Subject: [PATCH] Migrate to koa 2 Refactor rest api to async/await --- src/app.js | 77 ++++++++++++++++++------------------ src/route/rest.js | 25 ++++++------ test/integration/app-test.js | 32 ++++++++------- 3 files changed, 68 insertions(+), 66 deletions(-) diff --git a/src/app.js b/src/app.js index 61bbf94..1121ac6 100644 --- a/src/app.js +++ b/src/app.js @@ -17,8 +17,8 @@ 'use strict'; -const co = require('co'); -const app = require('koa')(); +const Koa = require('koa'); +const koaBody = require('koa-body'); const log = require('npmlog'); const config = require('config'); const serve = require('koa-static'); @@ -31,6 +31,8 @@ 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; @@ -43,55 +45,50 @@ let rest; // // HKP routes -router.post('/pks/add', function *() { - yield hkp.add(this); -}); -router.get('/pks/lookup', function *() { - yield hkp.lookup(this); -}); +router.post('/pks/add', ctx => hkp.add(ctx)); +router.get('/pks/lookup', ctx => hkp.lookup(ctx)); // REST api routes -router.post('/api/v1/key', function *() { - yield rest.create(this); -}); -router.get('/api/v1/key', function *() { - yield rest.query(this); -}); -router.del('/api/v1/key', function *() { - yield rest.remove(this); -}); +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(function *(next) { - if (util.isTrue(config.server.httpsUpgrade) && util.checkHTTP(this)) { - this.redirect(`https://${this.hostname}${this.url}`); +app.use(async(ctx, next) => { + if (util.isTrue(config.server.httpsUpgrade) && util.checkHTTP(ctx)) { + ctx.redirect(`https://${ctx.hostname}${ctx.url}`); } else { - yield next; + await next(); } }); // Set HTTP response headers -app.use(function *(next) { +app.use(async(ctx, next) => { // HSTS if (util.isTrue(config.server.httpsUpgrade)) { - this.set('Strict-Transport-Security', 'max-age=16070400'); + ctx.set('Strict-Transport-Security', 'max-age=16070400'); } // HPKP if (config.server.httpsKeyPin && config.server.httpsKeyPinBackup) { - this.set('Public-Key-Pins', `pin-sha256="${config.server.httpsKeyPin}"; pin-sha256="${config.server.httpsKeyPinBackup}"; max-age=16070400`); + ctx.set('Public-Key-Pins', `pin-sha256="${config.server.httpsKeyPin}"; pin-sha256="${config.server.httpsKeyPinBackup}"; max-age=16070400`); } // CSP - this.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"); + 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) - this.set('X-Frame-Options', 'DENY'); + ctx.set('X-Frame-Options', 'DENY'); // CORS - this.set('Access-Control-Allow-Origin', '*'); - this.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS'); - this.set('Access-Control-Allow-Headers', 'Content-Type'); - this.set('Connection', 'keep-alive'); - yield next; + 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()); @@ -124,19 +121,23 @@ function injectDependencies() { // if (!global.testing) { // don't automatically start server in tests - co(function *() { - const app = yield 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() => { + 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); + } + })(); } -function *init() { +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 ...'); - yield mongo.init(config.mongo); + await mongo.init(config.mongo); return app; } diff --git a/src/route/rest.js b/src/route/rest.js index ae47f29..af386cf 100644 --- a/src/route/rest.js +++ b/src/route/rest.js @@ -17,7 +17,6 @@ 'use strict'; -const parse = require('co-body'); const util = require('../service/util'); /** @@ -37,13 +36,13 @@ class REST { * Public key upload via http POST * @param {Object} ctx The koa request/response context */ - *create(ctx) { - const {publicKeyArmored, primaryEmail} = yield parse.json(ctx, {limit: '1mb'}); + async create(ctx) { + const {publicKeyArmored, primaryEmail} = ctx.request.body; if (!publicKeyArmored || (primaryEmail && !util.isEmail(primaryEmail))) { ctx.throw(400, 'Invalid request!'); } const origin = util.origin(ctx); - yield this._publicKey.put({publicKeyArmored, primaryEmail, origin}); + await this._publicKey.put({publicKeyArmored, primaryEmail, origin}); ctx.body = 'Upload successful. Check your inbox to verify your email address.'; ctx.status = 201; } @@ -52,29 +51,29 @@ class REST { * Public key query via http GET * @param {Object} ctx The koa request/response context */ - *query(ctx) { + async query(ctx) { const op = ctx.query.op; if (op === 'verify' || op === 'verifyRemove') { - return yield this[op](ctx); // delegate operation + return this[op](ctx); // delegate operation } // do READ if no 'op' provided const q = {keyId: ctx.query.keyId, fingerprint: ctx.query.fingerprint, email: ctx.query.email}; if (!util.isKeyId(q.keyId) && !util.isFingerPrint(q.fingerprint) && !util.isEmail(q.email)) { ctx.throw(400, 'Invalid request!'); } - ctx.body = yield this._publicKey.get(q); + ctx.body = await this._publicKey.get(q); } /** * Verify a public key's user id via http GET * @param {Object} ctx The koa request/response context */ - *verify(ctx) { + async verify(ctx) { const q = {keyId: ctx.query.keyId, nonce: ctx.query.nonce}; if (!util.isKeyId(q.keyId) || !util.isString(q.nonce)) { ctx.throw(400, 'Invalid request!'); } - yield this._publicKey.verify(q); + await this._publicKey.verify(q); // create link for sharing const link = util.url(util.origin(ctx), `/pks/lookup?op=get&search=0x${q.keyId.toUpperCase()}`); ctx.body = `

Email address successfully verified!

Link to share your key: ${link}

`; @@ -85,12 +84,12 @@ class REST { * Request public key removal via http DELETE * @param {Object} ctx The koa request/response context */ - *remove(ctx) { + async remove(ctx) { const q = {keyId: ctx.query.keyId, email: ctx.query.email, origin: util.origin(ctx)}; if (!util.isKeyId(q.keyId) && !util.isEmail(q.email)) { ctx.throw(400, 'Invalid request!'); } - yield this._publicKey.requestRemove(q); + await this._publicKey.requestRemove(q); ctx.body = 'Check your inbox to verify the removal of your key.'; ctx.status = 202; } @@ -99,12 +98,12 @@ class REST { * Verify public key removal via http GET * @param {Object} ctx The koa request/response context */ - *verifyRemove(ctx) { + async verifyRemove(ctx) { const q = {keyId: ctx.query.keyId, nonce: ctx.query.nonce}; if (!util.isKeyId(q.keyId) || !util.isString(q.nonce)) { ctx.throw(400, 'Invalid request!'); } - yield this._publicKey.verifyRemove(q); + await this._publicKey.verifyRemove(q); ctx.body = 'Key successfully removed!'; } } diff --git a/test/integration/app-test.js b/test/integration/app-test.js index 756e85c..3271646 100644 --- a/test/integration/app-test.js +++ b/test/integration/app-test.js @@ -10,6 +10,7 @@ const log = require('npmlog'); describe('Koa App (HTTP Server) Integration Tests', function() { this.timeout(20000); + let sandbox; let app; let mongo; let sendEmailStub; @@ -21,40 +22,41 @@ describe('Koa App (HTTP Server) Integration Tests', function() { const primaryEmail = 'safewithme.testuser@gmail.com'; const fingerprint = '4277257930867231CE393FB8DBC0B3D92B1B86E9'; - before(function *() { + before(async() => { + sandbox = sinon.sandbox.create(); + publicKeyArmored = fs.readFileSync(`${__dirname}/../key1.asc`, 'utf8'); mongo = new Mongo(); - yield mongo.init(config.mongo); + await mongo.init(config.mongo); - sendEmailStub = sinon.stub().returns(Promise.resolve({response: '250'})); + sendEmailStub = sandbox.stub().returns(Promise.resolve({response: '250'})); sendEmailStub.withArgs(sinon.match(recipient => recipient.to.address === primaryEmail), sinon.match(params => { emailParams = params; return Boolean(params.nonce); })); - sinon.stub(nodemailer, 'createTransport').returns({ + sandbox.stub(nodemailer, 'createTransport').returns({ templateSender: () => sendEmailStub, use() {} }); - sinon.stub(log); + sandbox.stub(log); global.testing = true; const init = require('../../src/app'); - app = yield init(); + app = await init(); }); - beforeEach(function *() { - yield mongo.clear(DB_TYPE_PUB_KEY); - yield mongo.clear(DB_TYPE_USER_ID); + beforeEach(async() => { + await mongo.clear(DB_TYPE_PUB_KEY); + await mongo.clear(DB_TYPE_USER_ID); emailParams = null; }); - after(function *() { - sinon.restore(log); - nodemailer.createTransport.restore(); - yield mongo.clear(DB_TYPE_PUB_KEY); - yield mongo.clear(DB_TYPE_USER_ID); - yield mongo.disconnect(); + after(async() => { + sandbox.restore(); + await mongo.clear(DB_TYPE_PUB_KEY); + await mongo.clear(DB_TYPE_USER_ID); + await mongo.disconnect(); }); describe('REST api', () => {