diff --git a/package.json b/package.json index f4887f0..88648de 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,11 @@ "test": "grunt test" }, "dependencies": { - "mongodb": "^2.1.20" + "co": "^4.6.0", + "koa": "^1.2.0", + "koa-router": "^5.4.0", + "mongodb": "^2.1.20", + "npmlog": "^2.0.4" }, "devDependencies": { "chai": "^3.5.0", diff --git a/server.js b/server.js new file mode 100644 index 0000000..530af93 --- /dev/null +++ b/server.js @@ -0,0 +1,64 @@ +/** + * 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 cluster = require('cluster'); +const numCPUs = require('os').cpus().length; +const log = require('npmlog'); + +// +// Error handling +// + +process.on('SIGTERM', () => { + log.warn('exit', 'Exited on SIGTERM'); + process.exit(0); +}); + +process.on('SIGINT', () => { + log.warn('exit', 'Exited on SIGINT'); + process.exit(0); +}); + +process.on('uncaughtException', err => { + log.error('server', 'Uncaught exception ', err); + process.exit(1); +}); + +// +// Start worker cluster depending on number of CPUs +// + +if (cluster.isMaster) { + for (var 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('exit', worker => { + log.warn('cluster', 'Worker #%s [pid:%s] died', worker.id, worker.process.pid); + setTimeout(() => { + cluster.fork(); + }, 5000); + }); +} else { + require('./src/worker'); +} \ No newline at end of file diff --git a/src/ctrl/public-key.js b/src/ctrl/public-key.js new file mode 100644 index 0000000..704f025 --- /dev/null +++ b/src/ctrl/public-key.js @@ -0,0 +1,35 @@ +/** + * 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'; + +/** + * A controller that handlers PGP public keys queries to the database + */ +class PublicKey { + + /** + * Create an instance of the controller + * @param {Object} mongo An instance of the MongoDB client + */ + constructor(mongo) { + this._mongo = mongo; + } + +} + +module.exports = PublicKey; \ No newline at end of file diff --git a/src/routes/hkp.js b/src/routes/hkp.js new file mode 100644 index 0000000..cef2084 --- /dev/null +++ b/src/routes/hkp.js @@ -0,0 +1,129 @@ +/** + * 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'; + +/** + * An implementation of the OpenPGP HTTP Keyserver Protocol (HKP) + * See https://tools.ietf.org/html/draft-shaw-openpgp-hkp-00 + */ +class HKP { + + /** + * Create an instance of the HKP server + * @param {Object} publicKey An instance of the public key controller + */ + constructor(publicKey) { + this._publicKey = publicKey; + } + + /** + * Public key lookup via http GET + * @param {Object} ctx The koa request/response context + */ + *lookup(ctx) { + var params = this.parseQueryString(ctx); + if (!params) { + return; // invalid request + } + + this.setHeaders(ctx); + if (params.mr) { + this.setGetMRHEaders(ctx); + } + ctx.body = yield Promise.resolve('----- BEGIN PUBLIC PGP KEY -----'); + } + + /** + * Public key upload via http POST + * @param {Object} ctx The koa request/response context + */ + *add(ctx) { + ctx.throw(501, 'Not implemented!'); + return yield Promise.resolve(); + } + + /** + * Parse the query string for a lookup request and set a corresponding + * error code if the requests is not supported or invalid. + * @param {Object} ctx The koa request/response context + * @return {Object} The query parameters or undefined for an invalid request + */ + parseQueryString(ctx) { + let q = ctx.query; + let params = { + op: q.op, // operation ... only 'get' is supported + mr: q.options === 'mr', // machine readable + keyid: this.checkId(q.search) ? q.search.replace('0x', '') : null, + email: this.checkEmail(q.search) ? q.search : null, + }; + + if (params.op !== 'get') { + ctx.status = 501; + ctx.body = 'Not implemented!'; + return; + } else if (!params.keyid && !params.email) { + ctx.status = 404; + ctx.body = 'Invalid request!'; + return; + } + + return params; + } + + /** + * Checks for a valid email address. + * @param {String} address The email address + * @return {Boolean} If the email address if valid + */ + checkEmail(address) { + return /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,63}$/.test(address); + } + + /** + * Checks for a valid key id in the query string. A key must be prepended + * with '0x' and can be between 8 and 40 characters long. + * @param {String} keyid The key id + * @return {Boolean} If the key id is valid + */ + checkId(keyid) { + return /^0x[a-fA-Z0-9]{8,40}/.test(keyid); + } + + /** + * Set HTTP headers for the HKP requests. + * @param {Object} ctx The koa request/response context + */ + setHeaders(ctx) { + ctx.set('Access-Control-Allow-Origin', '*'); + ctx.set('Cache-Control', 'no-cache'); + ctx.set('Pragma', 'no-cache'); + ctx.set('Connection', 'keep-alive'); + } + + /** + * Set HTTP headers for a GET requests with 'mr' (machine readable) options. + * @param {Object} ctx The koa request/response context + */ + setGetMRHEaders(ctx) { + ctx.set('Content-Type', 'application/pgp-keys; charset=UTF-8'); + ctx.set('Content-Disposition', 'attachment; filename=openpgpkey.asc'); + } + +} + +module.exports = HKP; \ No newline at end of file diff --git a/src/worker.js b/src/worker.js new file mode 100644 index 0000000..45fe4e1 --- /dev/null +++ b/src/worker.js @@ -0,0 +1,79 @@ +/** + * 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 co = require('co'); +const fs = require('fs'); +const app = require('koa')(); +const log = require('npmlog'); +const router = require('koa-router')(); +const Mongo = require('./dao/mongo'); +const PublicKey = require('./ctrl/public-key'); +const HKP = require('./routes/hkp'); + +let mongo, publicKey, hkp; + +// +// Configure koa router +// + +router.get('/pks/lookup', function *() { + yield hkp.lookup(this); +}); +router.post('/pks/add', function *() { + yield hkp.add(this); +}); + +app.use(router.routes()); +app.use(router.allowedMethods()); +app.on('error', (err, ctx) => log.error('worker', 'Unknown server error', err, ctx)); + +// +// Module initialization +// + +function injectDependencies() { + let credentials = readCredentials(); + mongo = new Mongo({ + uri: process.env.MONGO_URI || credentials.mongoUri, + user: process.env.MONGO_USER || credentials.mongoUser, + password: process.env.MONGO_PASS || credentials.mongoPass + }); + publicKey = new PublicKey(mongo); + hkp = new HKP(publicKey); +} + +function readCredentials() { + try { + return JSON.parse(fs.readFileSync(__dirname + '/../credentials.json')); + } catch(e) { + log.info('worker', 'No credentials.json found ... using environment vars.'); + } +} + +// +// Start app ... connect to the database and start listening +// + +co(function *() { + + injectDependencies(); + yield mongo.connect(); + app.listen(process.env.PORT || 8888); + +}).catch(err => log.error('worker', 'Initialization failed!', err)); \ No newline at end of file