Migrate to koa 2

Refactor rest api to async/await
This commit is contained in:
Tankred Hase 2017-08-17 15:34:47 +08:00
parent 3dfa447fcf
commit 49b24a5cb4
3 changed files with 68 additions and 66 deletions

View File

@ -17,8 +17,8 @@
'use strict'; 'use strict';
const co = require('co'); const Koa = require('koa');
const app = require('koa')(); const koaBody = require('koa-body');
const log = require('npmlog'); const log = require('npmlog');
const config = require('config'); const config = require('config');
const serve = require('koa-static'); const serve = require('koa-static');
@ -31,6 +31,8 @@ const PublicKey = require('./service/public-key');
const HKP = require('./route/hkp'); const HKP = require('./route/hkp');
const REST = require('./route/rest'); const REST = require('./route/rest');
const app = new Koa();
let mongo; let mongo;
let email; let email;
let pgp; let pgp;
@ -43,55 +45,50 @@ let rest;
// //
// HKP routes // HKP routes
router.post('/pks/add', function *() { router.post('/pks/add', ctx => hkp.add(ctx));
yield hkp.add(this); router.get('/pks/lookup', ctx => hkp.lookup(ctx));
});
router.get('/pks/lookup', function *() {
yield hkp.lookup(this);
});
// REST api routes // REST api routes
router.post('/api/v1/key', function *() { router.post('/api/v1/key', ctx => rest.create(ctx));
yield rest.create(this); router.get('/api/v1/key', ctx => rest.query(ctx));
}); router.del('/api/v1/key', ctx => rest.remove(ctx));
router.get('/api/v1/key', function *() {
yield rest.query(this);
});
router.del('/api/v1/key', function *() {
yield rest.remove(this);
});
// Redirect all http traffic to https // Redirect all http traffic to https
app.use(function *(next) { app.use(async(ctx, next) => {
if (util.isTrue(config.server.httpsUpgrade) && util.checkHTTP(this)) { if (util.isTrue(config.server.httpsUpgrade) && util.checkHTTP(ctx)) {
this.redirect(`https://${this.hostname}${this.url}`); ctx.redirect(`https://${ctx.hostname}${ctx.url}`);
} else { } else {
yield next; await next();
} }
}); });
// Set HTTP response headers // Set HTTP response headers
app.use(function *(next) { app.use(async(ctx, next) => {
// HSTS // HSTS
if (util.isTrue(config.server.httpsUpgrade)) { if (util.isTrue(config.server.httpsUpgrade)) {
this.set('Strict-Transport-Security', 'max-age=16070400'); ctx.set('Strict-Transport-Security', 'max-age=16070400');
} }
// HPKP // HPKP
if (config.server.httpsKeyPin && config.server.httpsKeyPinBackup) { 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 // 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) // Prevent rendering website in foreign iframe (Clickjacking)
this.set('X-Frame-Options', 'DENY'); ctx.set('X-Frame-Options', 'DENY');
// CORS // CORS
this.set('Access-Control-Allow-Origin', '*'); ctx.set('Access-Control-Allow-Origin', '*');
this.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS'); ctx.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
this.set('Access-Control-Allow-Headers', 'Content-Type'); ctx.set('Access-Control-Allow-Headers', 'Content-Type');
this.set('Connection', 'keep-alive'); ctx.set('Connection', 'keep-alive');
yield next; await next();
}); });
app.use(koaBody({
multipart: true,
formLimit: '1mb'
}));
app.use(router.routes()); app.use(router.routes());
app.use(router.allowedMethods()); app.use(router.allowedMethods());
@ -124,19 +121,23 @@ function injectDependencies() {
// //
if (!global.testing) { // don't automatically start server in tests if (!global.testing) { // don't automatically start server in tests
co(function *() { (async() => {
const app = yield init(); try {
const app = await init();
app.listen(config.server.port); app.listen(config.server.port);
log.info('app', `Ready to rock! Listening on http://localhost:${config.server.port}`); log.info('app', `Ready to rock! Listening on http://localhost:${config.server.port}`);
}).catch(err => log.error('app', 'Initialization failed!', err)); } 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 log.level = config.log.level; // set log level depending on process.env.NODE_ENV
injectDependencies(); injectDependencies();
email.init(config.email); email.init(config.email);
log.info('app', 'Connecting to MongoDB ...'); log.info('app', 'Connecting to MongoDB ...');
yield mongo.init(config.mongo); await mongo.init(config.mongo);
return app; return app;
} }

View File

@ -17,7 +17,6 @@
'use strict'; 'use strict';
const parse = require('co-body');
const util = require('../service/util'); const util = require('../service/util');
/** /**
@ -37,13 +36,13 @@ class REST {
* Public key upload via http POST * Public key upload via http POST
* @param {Object} ctx The koa request/response context * @param {Object} ctx The koa request/response context
*/ */
*create(ctx) { async create(ctx) {
const {publicKeyArmored, primaryEmail} = yield parse.json(ctx, {limit: '1mb'}); const {publicKeyArmored, primaryEmail} = ctx.request.body;
if (!publicKeyArmored || (primaryEmail && !util.isEmail(primaryEmail))) { if (!publicKeyArmored || (primaryEmail && !util.isEmail(primaryEmail))) {
ctx.throw(400, 'Invalid request!'); ctx.throw(400, 'Invalid request!');
} }
const origin = util.origin(ctx); 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.body = 'Upload successful. Check your inbox to verify your email address.';
ctx.status = 201; ctx.status = 201;
} }
@ -52,29 +51,29 @@ class REST {
* Public key query via http GET * Public key query via http GET
* @param {Object} ctx The koa request/response context * @param {Object} ctx The koa request/response context
*/ */
*query(ctx) { async query(ctx) {
const op = ctx.query.op; const op = ctx.query.op;
if (op === 'verify' || op === 'verifyRemove') { if (op === 'verify' || op === 'verifyRemove') {
return yield this[op](ctx); // delegate operation return this[op](ctx); // delegate operation
} }
// do READ if no 'op' provided // do READ if no 'op' provided
const q = {keyId: ctx.query.keyId, fingerprint: ctx.query.fingerprint, email: ctx.query.email}; 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)) { if (!util.isKeyId(q.keyId) && !util.isFingerPrint(q.fingerprint) && !util.isEmail(q.email)) {
ctx.throw(400, 'Invalid request!'); 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 * Verify a public key's user id via http GET
* @param {Object} ctx The koa request/response context * @param {Object} ctx The koa request/response context
*/ */
*verify(ctx) { async verify(ctx) {
const q = {keyId: ctx.query.keyId, nonce: ctx.query.nonce}; const q = {keyId: ctx.query.keyId, nonce: ctx.query.nonce};
if (!util.isKeyId(q.keyId) || !util.isString(q.nonce)) { if (!util.isKeyId(q.keyId) || !util.isString(q.nonce)) {
ctx.throw(400, 'Invalid request!'); ctx.throw(400, 'Invalid request!');
} }
yield this._publicKey.verify(q); await this._publicKey.verify(q);
// create link for sharing // create link for sharing
const link = util.url(util.origin(ctx), `/pks/lookup?op=get&search=0x${q.keyId.toUpperCase()}`); const link = util.url(util.origin(ctx), `/pks/lookup?op=get&search=0x${q.keyId.toUpperCase()}`);
ctx.body = `<p>Email address successfully verified!</p><p>Link to share your key: <a href="${link}" target="_blank">${link}</a></p>`; ctx.body = `<p>Email address successfully verified!</p><p>Link to share your key: <a href="${link}" target="_blank">${link}</a></p>`;
@ -85,12 +84,12 @@ class REST {
* Request public key removal via http DELETE * Request public key removal via http DELETE
* @param {Object} ctx The koa request/response context * @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)}; const q = {keyId: ctx.query.keyId, email: ctx.query.email, origin: util.origin(ctx)};
if (!util.isKeyId(q.keyId) && !util.isEmail(q.email)) { if (!util.isKeyId(q.keyId) && !util.isEmail(q.email)) {
ctx.throw(400, 'Invalid request!'); 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.body = 'Check your inbox to verify the removal of your key.';
ctx.status = 202; ctx.status = 202;
} }
@ -99,12 +98,12 @@ class REST {
* Verify public key removal via http GET * Verify public key removal via http GET
* @param {Object} ctx The koa request/response context * @param {Object} ctx The koa request/response context
*/ */
*verifyRemove(ctx) { async verifyRemove(ctx) {
const q = {keyId: ctx.query.keyId, nonce: ctx.query.nonce}; const q = {keyId: ctx.query.keyId, nonce: ctx.query.nonce};
if (!util.isKeyId(q.keyId) || !util.isString(q.nonce)) { if (!util.isKeyId(q.keyId) || !util.isString(q.nonce)) {
ctx.throw(400, 'Invalid request!'); ctx.throw(400, 'Invalid request!');
} }
yield this._publicKey.verifyRemove(q); await this._publicKey.verifyRemove(q);
ctx.body = 'Key successfully removed!'; ctx.body = 'Key successfully removed!';
} }
} }

View File

@ -10,6 +10,7 @@ const log = require('npmlog');
describe('Koa App (HTTP Server) Integration Tests', function() { describe('Koa App (HTTP Server) Integration Tests', function() {
this.timeout(20000); this.timeout(20000);
let sandbox;
let app; let app;
let mongo; let mongo;
let sendEmailStub; let sendEmailStub;
@ -21,40 +22,41 @@ describe('Koa App (HTTP Server) Integration Tests', function() {
const primaryEmail = 'safewithme.testuser@gmail.com'; const primaryEmail = 'safewithme.testuser@gmail.com';
const fingerprint = '4277257930867231CE393FB8DBC0B3D92B1B86E9'; const fingerprint = '4277257930867231CE393FB8DBC0B3D92B1B86E9';
before(function *() { before(async() => {
sandbox = sinon.sandbox.create();
publicKeyArmored = fs.readFileSync(`${__dirname}/../key1.asc`, 'utf8'); publicKeyArmored = fs.readFileSync(`${__dirname}/../key1.asc`, 'utf8');
mongo = new Mongo(); 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 => { sendEmailStub.withArgs(sinon.match(recipient => recipient.to.address === primaryEmail), sinon.match(params => {
emailParams = params; emailParams = params;
return Boolean(params.nonce); return Boolean(params.nonce);
})); }));
sinon.stub(nodemailer, 'createTransport').returns({ sandbox.stub(nodemailer, 'createTransport').returns({
templateSender: () => sendEmailStub, templateSender: () => sendEmailStub,
use() {} use() {}
}); });
sinon.stub(log); sandbox.stub(log);
global.testing = true; global.testing = true;
const init = require('../../src/app'); const init = require('../../src/app');
app = yield init(); app = await init();
}); });
beforeEach(function *() { beforeEach(async() => {
yield mongo.clear(DB_TYPE_PUB_KEY); await mongo.clear(DB_TYPE_PUB_KEY);
yield mongo.clear(DB_TYPE_USER_ID); await mongo.clear(DB_TYPE_USER_ID);
emailParams = null; emailParams = null;
}); });
after(function *() { after(async() => {
sinon.restore(log); sandbox.restore();
nodemailer.createTransport.restore(); await mongo.clear(DB_TYPE_PUB_KEY);
yield mongo.clear(DB_TYPE_PUB_KEY); await mongo.clear(DB_TYPE_USER_ID);
yield mongo.clear(DB_TYPE_USER_ID); await mongo.disconnect();
yield mongo.disconnect();
}); });
describe('REST api', () => { describe('REST api', () => {