First working prototype of the keyserver
This commit is contained in:
parent
439ab77422
commit
2d07c34060
10
config/default.js
Normal file
10
config/default.js
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
server: {
|
||||||
|
port: process.env.PORT || 8888,
|
||||||
|
},
|
||||||
|
log: {
|
||||||
|
level: "silly"
|
||||||
|
}
|
||||||
|
};
|
7
config/production.js
Normal file
7
config/production.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
log: {
|
||||||
|
level: "error"
|
||||||
|
}
|
||||||
|
};
|
@ -13,11 +13,15 @@
|
|||||||
"test": "grunt test"
|
"test": "grunt test"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"addressparser": "^1.0.1",
|
||||||
"co": "^4.6.0",
|
"co": "^4.6.0",
|
||||||
|
"co-body": "^4.2.0",
|
||||||
"koa": "^1.2.0",
|
"koa": "^1.2.0",
|
||||||
"koa-router": "^5.4.0",
|
"koa-router": "^5.4.0",
|
||||||
"mongodb": "^2.1.20",
|
"mongodb": "^2.1.20",
|
||||||
"npmlog": "^2.0.4"
|
"nodemailer": "^2.4.2",
|
||||||
|
"npmlog": "^2.0.4",
|
||||||
|
"openpgp": "^2.3.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"chai": "^3.5.0",
|
"chai": "^3.5.0",
|
||||||
@ -27,6 +31,7 @@
|
|||||||
"grunt-jscs": "^2.8.0",
|
"grunt-jscs": "^2.8.0",
|
||||||
"grunt-mocha-test": "^0.12.7",
|
"grunt-mocha-test": "^0.12.7",
|
||||||
"mocha": "^2.5.3",
|
"mocha": "^2.5.3",
|
||||||
"sinon": "^1.17.4"
|
"sinon": "^1.17.4",
|
||||||
|
"supertest": "^1.2.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,8 +19,11 @@
|
|||||||
|
|
||||||
const cluster = require('cluster');
|
const cluster = require('cluster');
|
||||||
const numCPUs = require('os').cpus().length;
|
const numCPUs = require('os').cpus().length;
|
||||||
|
const config = require('config');
|
||||||
const log = require('npmlog');
|
const log = require('npmlog');
|
||||||
|
|
||||||
|
log.level = config.log.level; // set log level depending on process.env.NODE_ENV
|
||||||
|
|
||||||
//
|
//
|
||||||
// Start worker cluster depending on number of CPUs
|
// Start worker cluster depending on number of CPUs
|
||||||
//
|
//
|
||||||
|
@ -17,11 +17,13 @@
|
|||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
const log = require('npmlog');
|
||||||
|
const util = require('./util');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Database documents have the format:
|
* Database documents have the format:
|
||||||
* {
|
* {
|
||||||
* _id: "02C134D079701934", // the 16 byte key id
|
* _id: "02C134D079701934", // the 16 byte key id in uppercase hex
|
||||||
* email: "jon@example.com", // the primary and verified email address
|
|
||||||
* publicKeyArmored: "-----BEGIN PGP PUBLIC KEY BLOCK----- ... -----END PGP PUBLIC KEY BLOCK-----"
|
* publicKeyArmored: "-----BEGIN PGP PUBLIC KEY BLOCK----- ... -----END PGP PUBLIC KEY BLOCK-----"
|
||||||
* }
|
* }
|
||||||
*/
|
*/
|
||||||
@ -34,21 +36,73 @@ class PublicKey {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Create an instance of the controller
|
* Create an instance of the controller
|
||||||
* @param {Object} mongo An instance of the MongoDB client
|
* @param {Object} openpgp An instance of OpenPGP.js
|
||||||
|
* @param {Object} mongo An instance of the MongoDB client
|
||||||
|
* @param {Object} email An instance of the Email Sender
|
||||||
|
* @param {Object} userid An instance of the UserId controller
|
||||||
*/
|
*/
|
||||||
constructor(mongo) {
|
constructor(openpgp, mongo, email, userid) {
|
||||||
|
this._openpgp = openpgp;
|
||||||
this._mongo = mongo;
|
this._mongo = mongo;
|
||||||
|
this._email = email;
|
||||||
|
this._userid = userid;
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// Create/Update
|
// Create/Update
|
||||||
//
|
//
|
||||||
|
|
||||||
put(options) {
|
/**
|
||||||
|
* Persist a new public key
|
||||||
|
* @param {String} options.publicKeyArmored The ascii armored pgp key block
|
||||||
|
* @param {String} options.primaryEmail (optional) The key's primary email address
|
||||||
|
* @yield {undefined}
|
||||||
|
*/
|
||||||
|
*put(options) {
|
||||||
|
// parse key block
|
||||||
|
let publicKeyArmored = options.publicKeyArmored;
|
||||||
|
let params = this.parseKey(publicKeyArmored);
|
||||||
|
// check for existing verfied key by id or email addresses
|
||||||
|
let verified = yield this._userid.getVerfied(params);
|
||||||
|
if (verified) {
|
||||||
|
throw util.error(304, 'Key for this user already exists: ' + verified.stringify());
|
||||||
|
}
|
||||||
|
// delete old/unverified key and user ids with the same key id
|
||||||
|
yield this.remove({ keyid:params.keyid });
|
||||||
|
// persist new key
|
||||||
|
let r = yield this._mongo.create({ _id:params.keyid, publicKeyArmored }, DB_TYPE);
|
||||||
|
if (r.insertedCount !== 1) {
|
||||||
|
throw util.error(500, 'Failed to persist key');
|
||||||
|
}
|
||||||
|
// persist new user ids
|
||||||
|
let userIds = yield this._userid.batch(params);
|
||||||
|
yield this._email.sendVerification({ userIds, primaryEmail:options.primaryEmail });
|
||||||
}
|
}
|
||||||
|
|
||||||
verify(options) {
|
/**
|
||||||
|
* Parse an ascii armored pgp key block and get its parameters.
|
||||||
|
* @param {String} publicKeyArmored The ascii armored pgp key block
|
||||||
|
* @return {Object} The key's id and user ids
|
||||||
|
*/
|
||||||
|
parseKey(publicKeyArmored) {
|
||||||
|
let keys, userIds = [];
|
||||||
|
try {
|
||||||
|
keys = this._openpgp.key.readArmored(publicKeyArmored).keys;
|
||||||
|
} catch(e) {
|
||||||
|
log.error('public-key', 'Failed to parse PGP key:\n%s', publicKeyArmored, e);
|
||||||
|
throw util.error(500, 'Failed to parse PGP key');
|
||||||
|
}
|
||||||
|
// get key user ids
|
||||||
|
keys.forEach(key => userIds = userIds.concat(key.getUserIds()));
|
||||||
|
userIds = util.deDup(userIds);
|
||||||
|
// get key id
|
||||||
|
return {
|
||||||
|
keyid: keys[0].primaryKey.getKeyId().toHex().toUpperCase(),
|
||||||
|
userIds: util.parseUserIds(userIds)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
verify() {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -56,22 +110,50 @@ class PublicKey {
|
|||||||
// Read
|
// Read
|
||||||
//
|
//
|
||||||
|
|
||||||
get(options) {
|
/**
|
||||||
|
* Fetch a verified public key from the database. Either the key id or the
|
||||||
|
* email address muss be provided.
|
||||||
|
* @param {String} options.keyid (optional) The public key id
|
||||||
|
* @param {String} options.email (optional) The user's email address
|
||||||
|
* @yield {Object} The public key document
|
||||||
|
*/
|
||||||
|
*get(options) {
|
||||||
|
let keyid = options.keyid, email = options.email;
|
||||||
|
let verified = yield this._userid.getVerfied({
|
||||||
|
keyid: keyid ? keyid.toUpperCase() : undefined,
|
||||||
|
userIds: email ? [{ email:email.toLowerCase() }] : undefined
|
||||||
|
});
|
||||||
|
if (verified) {
|
||||||
|
return yield this._mongo.get({ _id:verified.keyid }, DB_TYPE);
|
||||||
|
} else {
|
||||||
|
throw util.error(404, 'Key not found');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// Delete
|
// Delete
|
||||||
//
|
//
|
||||||
|
|
||||||
remove(options) {
|
flagForRemove() {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
verifyRemove(options) {
|
verifyRemove() {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a public key document and its corresponding user id documents.
|
||||||
|
* @param {String} options.keyid The key id
|
||||||
|
* @yield {undefined}
|
||||||
|
*/
|
||||||
|
*remove(options) {
|
||||||
|
// remove key document
|
||||||
|
yield this._mongo.remove({ _id:options.keyid }, DB_TYPE);
|
||||||
|
// remove matching user id documents
|
||||||
|
yield this._userid.remove({ keyid:options.keyid });
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = PublicKey;
|
module.exports = PublicKey;
|
114
src/ctrl/user-id.js
Normal file
114
src/ctrl/user-id.js
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
/**
|
||||||
|
* 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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Database documents have the format:
|
||||||
|
* {
|
||||||
|
* _id: ObjectID, // randomly generated by MongoDB
|
||||||
|
* email: "jon@example.com", // the email address in lowercase
|
||||||
|
* name: "Jon Smith",
|
||||||
|
* keyid: "02C134D079701934", // id of the public key document in uppercase hex
|
||||||
|
* nonce: "123e4567-e89b-12d3-a456-426655440000", // verifier used to prove ownership
|
||||||
|
* verified: true // if the user ID has been verified
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
const DB_TYPE = 'userid';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A controller that handles User ID queries to the database
|
||||||
|
*/
|
||||||
|
class UserId {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an instance of the controller
|
||||||
|
* @param {Object} mongo An instance of the MongoDB client
|
||||||
|
*/
|
||||||
|
constructor(mongo) {
|
||||||
|
this._mongo = mongo;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Create/Update
|
||||||
|
//
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store a list of user ids. There can only be one verified user ID for
|
||||||
|
* an email address at any given time.
|
||||||
|
* @param {String} options.keyid The public key id
|
||||||
|
* @param {Array} options.userIds The userIds to persist
|
||||||
|
* @yield {Array} A list of user ids with generated nonces
|
||||||
|
*/
|
||||||
|
*batch(options) {
|
||||||
|
options.userIds.forEach(u => u.keyid = options.keyid); // set keyid on docs
|
||||||
|
let r = yield this._mongo.batch(options.userIds, DB_TYPE);
|
||||||
|
if (r.insertedCount !== options.userIds.length) {
|
||||||
|
throw new Error('Failed to persist user ids');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Read
|
||||||
|
//
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a verified user IDs either by key id or email address.
|
||||||
|
* There can only be one verified user ID for an email address
|
||||||
|
* at any given time.
|
||||||
|
* @param {String} options.keyid The public key id
|
||||||
|
* @param {String} options.userIds A list of user ids to check
|
||||||
|
* @yield {Object} The verified user ID document
|
||||||
|
*/
|
||||||
|
*getVerfied(options) {
|
||||||
|
let keyid = options.keyid, userIds = options.userIds;
|
||||||
|
if (keyid) {
|
||||||
|
// try by key id
|
||||||
|
let uids = yield this._mongo.list({ keyid }, DB_TYPE);
|
||||||
|
let verified = uids.find(u => u.verified);
|
||||||
|
if (verified) {
|
||||||
|
return verified;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (userIds) {
|
||||||
|
// try by email addresses
|
||||||
|
for (let uid of userIds) {
|
||||||
|
let uids = yield this._mongo.list({ email:uid.email }, DB_TYPE);
|
||||||
|
let verified = uids.find(u => u.verified);
|
||||||
|
if (verified) {
|
||||||
|
return verified;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Delete
|
||||||
|
//
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove all user ids matching a certain query
|
||||||
|
* @param {String} options.keyid The public key id
|
||||||
|
* @yield {undefined}
|
||||||
|
*/
|
||||||
|
*remove(options) {
|
||||||
|
yield this._mongo.remove({ keyid:options.keyid }, DB_TYPE);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = UserId;
|
109
src/ctrl/util.js
Normal file
109
src/ctrl/util.js
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
/**
|
||||||
|
* 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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const addressparser = require('addressparser');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks for a valid string
|
||||||
|
* @param {} data The input to be checked
|
||||||
|
* @return {boolean} If data is a string
|
||||||
|
*/
|
||||||
|
exports.isString = function(data) {
|
||||||
|
return typeof data === 'string' || String.prototype.isPrototypeOf(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks for a valid key id which is between 8 and 40 hex chars.
|
||||||
|
* @param {string} data The key id
|
||||||
|
* @return {boolean} If the key id if valid
|
||||||
|
*/
|
||||||
|
exports.validateKeyId = function(data) {
|
||||||
|
if (!this.isString(data)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return /^[a-fA-F0-9]{8,40}$/.test(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks for a valid email address.
|
||||||
|
* @param {string} data The email address
|
||||||
|
* @return {boolean} If the email address if valid
|
||||||
|
*/
|
||||||
|
exports.validateAddress = function(data) {
|
||||||
|
if (!this.isString(data)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const re = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
|
||||||
|
return re.test(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate an ascii armored public PGP key block.
|
||||||
|
* @param {string} data The armored key block
|
||||||
|
* @return {boolean} If the key is valid
|
||||||
|
*/
|
||||||
|
exports.validatePublicKey = function(data) {
|
||||||
|
if (!this.isString(data)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const begin = /-----BEGIN PGP PUBLIC KEY BLOCK-----/;
|
||||||
|
const end = /-----END PGP PUBLIC KEY BLOCK-----/;
|
||||||
|
return begin.test(data) && end.test(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse an array of user id string to objects
|
||||||
|
* @param {Array} userIds A list of user ids strings
|
||||||
|
* @return {Array} An array of user id objects
|
||||||
|
*/
|
||||||
|
exports.parseUserIds = function(userIds) {
|
||||||
|
let result = [];
|
||||||
|
userIds.forEach(uid => result = result.concat(addressparser(uid)));
|
||||||
|
return result.map(u => ({
|
||||||
|
email: u.address ? u.address.toLowerCase() : undefined,
|
||||||
|
name: u.name
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deduplicates items in an array
|
||||||
|
* @param {Array} list The list of items with duplicates
|
||||||
|
* @return {Array} The list of items without duplicates
|
||||||
|
*/
|
||||||
|
exports.deDup = function(list) {
|
||||||
|
var result = [];
|
||||||
|
(list || []).forEach(function(i) {
|
||||||
|
if (result.indexOf(i) === -1) {
|
||||||
|
result.push(i);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an error with a custom status attribute e.g. for http codes.
|
||||||
|
* @param {number} status The error's http status code
|
||||||
|
* @param {string} message The error message
|
||||||
|
* @return {Error} The resulting error object
|
||||||
|
*/
|
||||||
|
exports.error = function(status, message) {
|
||||||
|
let err = new Error(message);
|
||||||
|
err.status = status;
|
||||||
|
return err;
|
||||||
|
};
|
@ -22,8 +22,31 @@
|
|||||||
*/
|
*/
|
||||||
class Email {
|
class Email {
|
||||||
|
|
||||||
send(options) {
|
/**
|
||||||
|
* Create an instance of the email object.
|
||||||
|
* @param {Object} mailer An instance of nodemailer
|
||||||
|
*/
|
||||||
|
constructor(mailer) {
|
||||||
|
this._mailer = mailer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send the verification email to the user to verify email address
|
||||||
|
* ownership. If the primary email address is provided, only one email
|
||||||
|
* will be sent out. Otherwise all of the PGP key's user IDs will be
|
||||||
|
* verified, resulting in an email sent per user ID.
|
||||||
|
* @param {Array} options.userIds The user id documents containing the nonces
|
||||||
|
* @param {Array} options.primaryEmail (optional) The user's primary email address
|
||||||
|
* @yield {undefined}
|
||||||
|
*/
|
||||||
|
sendVerification() {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
send() {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
module.exports = Email;
|
@ -26,20 +26,17 @@ class Mongo {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Create an instance of the MongoDB client.
|
* Create an instance of the MongoDB client.
|
||||||
* @param {String} options.uri The mongodb uri
|
* @param {String} options.uri The mongodb uri
|
||||||
* @param {String} options.user The databse user
|
* @param {String} options.user The databse user
|
||||||
* @param {String} options.password The database user's password
|
* @param {String} options.password The database user's password
|
||||||
* @param {String} options.type (optional) The default collection type to use e.g. 'publickey'
|
|
||||||
* @return {undefined}
|
|
||||||
*/
|
*/
|
||||||
constructor(options) {
|
constructor(options) {
|
||||||
this._uri = 'mongodb://' + options.user + ':' + options.password + '@' + options.uri;
|
this._uri = 'mongodb://' + options.user + ':' + options.password + '@' + options.uri;
|
||||||
this._type = options.type;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes the database client by connecting to the MongoDB.
|
* Initializes the database client by connecting to the MongoDB.
|
||||||
* @return {undefined}
|
* @yield {undefined}
|
||||||
*/
|
*/
|
||||||
*connect() {
|
*connect() {
|
||||||
this._db = yield MongoClient.connect(this._uri);
|
this._db = yield MongoClient.connect(this._uri);
|
||||||
@ -47,7 +44,7 @@ class Mongo {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Cleanup by closing the connection to the database.
|
* Cleanup by closing the connection to the database.
|
||||||
* @return {undefined}
|
* @yield {undefined}
|
||||||
*/
|
*/
|
||||||
disconnect() {
|
disconnect() {
|
||||||
return this._db.close();
|
return this._db.close();
|
||||||
@ -55,67 +52,78 @@ class Mongo {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Inserts a single document.
|
* Inserts a single document.
|
||||||
* @param {Object} document Inserts a single documents
|
* @param {Object} document Inserts a single document
|
||||||
* @param {String} type (optional) The collection to use e.g. 'publickey'
|
* @param {String} type The collection to use e.g. 'publickey'
|
||||||
* @return {Object} The operation result
|
* @yield {Object} The operation result
|
||||||
*/
|
*/
|
||||||
create(document, type) {
|
create(document, type) {
|
||||||
let col = this._db.collection(type || this._type);
|
let col = this._db.collection(type);
|
||||||
return col.insertOne(document);
|
return col.insertOne(document);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inserts a list of documents.
|
||||||
|
* @param {Array} documents Inserts a list of documents
|
||||||
|
* @param {String} type The collection to use e.g. 'publickey'
|
||||||
|
* @yield {Object} The operation result
|
||||||
|
*/
|
||||||
|
batch(documents, type) {
|
||||||
|
let col = this._db.collection(type);
|
||||||
|
return col.insertMany(documents);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update a single document.
|
* Update a single document.
|
||||||
* @param {Object} query The query e.g. { _id:'0' }
|
* @param {Object} query The query e.g. { _id:'0' }
|
||||||
* @param {Object} diff The attributes to change/set e.g. { foo:'bar' }
|
* @param {Object} diff The attributes to change/set e.g. { foo:'bar' }
|
||||||
* @param {String} type (optional) The collection to use e.g. 'publickey'
|
* @param {String} type The collection to use e.g. 'publickey'
|
||||||
* @return {Object} The operation result
|
* @yield {Object} The operation result
|
||||||
*/
|
*/
|
||||||
update(query, diff, type) {
|
update(query, diff, type) {
|
||||||
let col = this._db.collection(type || this._type);
|
let col = this._db.collection(type);
|
||||||
return col.updateOne(query, { $set:diff });
|
return col.updateOne(query, { $set:diff });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Read a single document.
|
* Read a single document.
|
||||||
* @param {Object} query The query e.g. { _id:'0' }
|
* @param {Object} query The query e.g. { _id:'0' }
|
||||||
* @param {String} type (optional) The collection to use e.g. 'publickey'
|
* @param {String} type The collection to use e.g. 'publickey'
|
||||||
* @return {Object} The document object
|
* @yield {Object} The document object
|
||||||
*/
|
*/
|
||||||
get(query, type) {
|
get(query, type) {
|
||||||
let col = this._db.collection(type || this._type);
|
let col = this._db.collection(type);
|
||||||
return col.findOne(query);
|
return col.findOne(query);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Read multiple documents at once.
|
* Read multiple documents at once.
|
||||||
* @param {Object} query The query e.g. { foo:'bar' }
|
* @param {Object} query The query e.g. { foo:'bar' }
|
||||||
* @param {String} type (optional) The collection to use e.g. 'publickey'
|
* @param {String} type The collection to use e.g. 'publickey'
|
||||||
* @return {Array} An array of document objects
|
* @yield {Array} An array of document objects
|
||||||
*/
|
*/
|
||||||
list(query, type) {
|
list(query, type) {
|
||||||
let col = this._db.collection(type || this._type);
|
let col = this._db.collection(type);
|
||||||
return col.find(query).toArray();
|
return col.find(query).toArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete a single document.
|
* Delete all documents matching a query.
|
||||||
* @param {Object} query The query e.g. { _id:'0' }
|
* @param {Object} query The query e.g. { _id:'0' }
|
||||||
* @param {String} type (optional) The collection to use e.g. 'publickey'
|
* @param {String} type The collection to use e.g. 'publickey'
|
||||||
* @return {Object} The document object
|
* @yield {Object} The operation result
|
||||||
*/
|
*/
|
||||||
remove(query, type) {
|
remove(query, type) {
|
||||||
let col = this._db.collection(type || this._type);
|
let col = this._db.collection(type);
|
||||||
return col.deleteOne(query);
|
return col.deleteMany(query);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clear all documents of a collection.
|
* Clear all documents of a collection.
|
||||||
* @param {String} type (optional) The collection to use e.g. 'publickey'
|
* @param {String} type The collection to use e.g. 'publickey'
|
||||||
* @return {Object} The operation result
|
* @yield {Object} The operation result
|
||||||
*/
|
*/
|
||||||
clear(type) {
|
clear(type) {
|
||||||
let col = this._db.collection(type || this._type);
|
let col = this._db.collection(type);
|
||||||
return col.deleteMany({});
|
return col.deleteMany({});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,6 +17,9 @@
|
|||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
const parse = require('co-body');
|
||||||
|
const util = require('../ctrl/util');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An implementation of the OpenPGP HTTP Keyserver Protocol (HKP)
|
* An implementation of the OpenPGP HTTP Keyserver Protocol (HKP)
|
||||||
* See https://tools.ietf.org/html/draft-shaw-openpgp-hkp-00
|
* See https://tools.ietf.org/html/draft-shaw-openpgp-hkp-00
|
||||||
@ -36,8 +39,11 @@ class HKP {
|
|||||||
* @param {Object} ctx The koa request/response context
|
* @param {Object} ctx The koa request/response context
|
||||||
*/
|
*/
|
||||||
*add(ctx) {
|
*add(ctx) {
|
||||||
ctx.throw(501, 'Not implemented!');
|
let body = yield parse.form(ctx, { limit: '1mb' });
|
||||||
yield;
|
if (!util.validatePublicKey(body.keytext)) {
|
||||||
|
ctx.throw(400, 'Invalid request!');
|
||||||
|
}
|
||||||
|
yield this._publicKey.put({ publicKeyArmored:body.keytext });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -46,20 +52,9 @@ class HKP {
|
|||||||
*/
|
*/
|
||||||
*lookup(ctx) {
|
*lookup(ctx) {
|
||||||
let params = this.parseQueryString(ctx);
|
let params = this.parseQueryString(ctx);
|
||||||
if (!params) {
|
|
||||||
return; // invalid request
|
|
||||||
}
|
|
||||||
|
|
||||||
let key = yield this._publicKey.get(params);
|
let key = yield this._publicKey.get(params);
|
||||||
if (key) {
|
this.setGetHeaders(ctx, params);
|
||||||
ctx.body = key.publicKeyArmored;
|
ctx.body = key.publicKeyArmored;
|
||||||
if (params.mr) {
|
|
||||||
this.setGetMRHEaders(ctx);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
ctx.status = 404;
|
|
||||||
ctx.body = 'Not found!';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -74,33 +69,20 @@ class HKP {
|
|||||||
mr: ctx.query.options === 'mr' // machine readable
|
mr: ctx.query.options === 'mr' // machine readable
|
||||||
};
|
};
|
||||||
if (this.checkId(ctx.query.search)) {
|
if (this.checkId(ctx.query.search)) {
|
||||||
params._id = ctx.query.search.replace(/^0x/, '');
|
params.keyid = ctx.query.search.replace(/^0x/, '');
|
||||||
} else if(this.checkEmail(ctx.query.search)) {
|
} else if(util.validateAddress(ctx.query.search)) {
|
||||||
params.email = ctx.query.search;
|
params.email = ctx.query.search;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (params.op !== 'get') {
|
if (params.op !== 'get') {
|
||||||
ctx.status = 501;
|
ctx.throw(501, 'Not implemented!');
|
||||||
ctx.body = 'Not implemented!';
|
} else if (!params.keyid && !params.email) {
|
||||||
return;
|
ctx.throw(400, 'Invalid request!');
|
||||||
} else if (!params._id && !params.email) {
|
|
||||||
ctx.status = 400;
|
|
||||||
ctx.body = 'Invalid request!';
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return params;
|
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
|
* Checks for a valid key id in the query string. A key must be prepended
|
||||||
* with '0x' and can be between 8 and 40 hex characters long.
|
* with '0x' and can be between 8 and 40 hex characters long.
|
||||||
@ -108,16 +90,22 @@ class HKP {
|
|||||||
* @return {Boolean} If the key id is valid
|
* @return {Boolean} If the key id is valid
|
||||||
*/
|
*/
|
||||||
checkId(keyid) {
|
checkId(keyid) {
|
||||||
|
if (!util.isString(keyid)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return /^0x[a-fA-F0-9]{8,40}$/.test(keyid);
|
return /^0x[a-fA-F0-9]{8,40}$/.test(keyid);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set HTTP headers for a GET requests with 'mr' (machine readable) options.
|
* Set HTTP headers for a GET requests with 'mr' (machine readable) options.
|
||||||
* @param {Object} ctx The koa request/response context
|
* @param {Object} ctx The koa request/response context
|
||||||
|
* @param {Object} params The parsed query string parameters
|
||||||
*/
|
*/
|
||||||
setGetMRHEaders(ctx) {
|
setGetHeaders(ctx, params) {
|
||||||
ctx.set('Content-Type', 'application/pgp-keys; charset=UTF-8');
|
if (params.mr) {
|
||||||
ctx.set('Content-Disposition', 'attachment; filename=openpgpkey.asc');
|
ctx.set('Content-Type', 'application/pgp-keys; charset=UTF-8');
|
||||||
|
ctx.set('Content-Disposition', 'attachment; filename=openpgpkey.asc');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -17,18 +17,37 @@
|
|||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
const parse = require('co-body');
|
||||||
|
const util = require('../ctrl/util');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The REST api to provide additional functionality on top of HKP
|
* The REST api to provide additional functionality on top of HKP
|
||||||
*/
|
*/
|
||||||
class REST {
|
class REST {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an instance of the REST server
|
||||||
|
* @param {Object} publicKey An instance of the public key controller
|
||||||
|
*/
|
||||||
constructor(publicKey) {
|
constructor(publicKey) {
|
||||||
this._publicKey = publicKey;
|
this._publicKey = publicKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Create/Update
|
||||||
|
//
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Public key upload via http POST
|
||||||
|
* @param {Object} ctx The koa request/response context
|
||||||
|
*/
|
||||||
*create(ctx) {
|
*create(ctx) {
|
||||||
ctx.throw(501, 'Not implemented!');
|
let pk = yield parse.json(ctx, { limit: '1mb' });
|
||||||
yield;
|
if ((pk.primaryEmail && !util.validateAddress(pk.primaryEmail)) ||
|
||||||
|
!util.validatePublicKey(pk.publicKeyArmored)) {
|
||||||
|
ctx.throw(400, 'Invalid request!');
|
||||||
|
}
|
||||||
|
yield this._publicKey(pk);
|
||||||
}
|
}
|
||||||
|
|
||||||
*verify(ctx) {
|
*verify(ctx) {
|
||||||
@ -36,11 +55,38 @@ class REST {
|
|||||||
yield;
|
yield;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Read
|
||||||
|
//
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Public key fetch via http GET
|
||||||
|
* @param {Object} ctx The koa request/response context
|
||||||
|
*/
|
||||||
*read(ctx) {
|
*read(ctx) {
|
||||||
ctx.throw(501, 'Not implemented!');
|
let q = { keyid:ctx.query.keyid, email:ctx.query.email };
|
||||||
yield;
|
if (!util.validateKeyId(q.keyid) && !util.validateAddress(q.email)) {
|
||||||
|
ctx.throw(400, 'Invalid request!');
|
||||||
|
}
|
||||||
|
ctx.body = yield this._publicKey.get(q);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Public key fetch via http GET (shorthand link for sharing)
|
||||||
|
* @param {Object} ctx The koa request/response context
|
||||||
|
*/
|
||||||
|
*share(ctx) {
|
||||||
|
let q = { email:ctx.params.email };
|
||||||
|
if (!util.validateAddress(q.email)) {
|
||||||
|
ctx.throw(400, 'Invalid request!');
|
||||||
|
}
|
||||||
|
ctx.body = (yield this._publicKey.get(q)).publicKeyArmored;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Delete
|
||||||
|
//
|
||||||
|
|
||||||
*remove(ctx) {
|
*remove(ctx) {
|
||||||
ctx.throw(501, 'Not implemented!');
|
ctx.throw(501, 'Not implemented!');
|
||||||
yield;
|
yield;
|
||||||
|
@ -18,16 +18,20 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const co = require('co');
|
const co = require('co');
|
||||||
const fs = require('fs');
|
|
||||||
const app = require('koa')();
|
const app = require('koa')();
|
||||||
const log = require('npmlog');
|
const log = require('npmlog');
|
||||||
|
const config = require('config');
|
||||||
const router = require('koa-router')();
|
const router = require('koa-router')();
|
||||||
|
const openpgp = require('openpgp');
|
||||||
|
const nodemailer = require('nodemailer');
|
||||||
const Mongo = require('./dao/mongo');
|
const Mongo = require('./dao/mongo');
|
||||||
|
const Email = require('./dao/email');
|
||||||
|
const UserId = require('./ctrl/user-id');
|
||||||
const PublicKey = require('./ctrl/public-key');
|
const PublicKey = require('./ctrl/public-key');
|
||||||
const HKP = require('./routes/hkp');
|
const HKP = require('./routes/hkp');
|
||||||
const REST = require('./routes/rest');
|
const REST = require('./routes/rest');
|
||||||
|
|
||||||
let mongo, publicKey, hkp, rest;
|
let mongo, email, userId, publicKey, hkp, rest;
|
||||||
|
|
||||||
//
|
//
|
||||||
// Configure koa HTTP server
|
// Configure koa HTTP server
|
||||||
@ -42,25 +46,25 @@ router.get('/pks/lookup', function *() { // ?op=get&search=0x1234567890123456
|
|||||||
});
|
});
|
||||||
|
|
||||||
// REST api routes
|
// REST api routes
|
||||||
router.post('/api/key', function *() { // no query params
|
router.post('/api/v1/key', function *() { // { publicKeyArmored, primaryEmail } hint the primary email address
|
||||||
yield rest.create(this);
|
yield rest.create(this);
|
||||||
});
|
});
|
||||||
router.get('/api/key', function *() { // ?id=keyid OR ?email=email
|
router.get('/api/v1/key', function *() { // ?id=keyid OR ?email=email
|
||||||
yield rest.read(this);
|
yield rest.read(this);
|
||||||
});
|
});
|
||||||
router.del('/api/key', function *() { // ?id=keyid OR ?email=email
|
router.del('/api/v1/key', function *() { // ?id=keyid OR ?email=email
|
||||||
yield rest.remove(this);
|
yield rest.remove(this);
|
||||||
});
|
});
|
||||||
|
|
||||||
// links for verification and sharing
|
// links for verification and sharing
|
||||||
router.get('/api/verify', function *() { // ?id=keyid&nonce=nonce
|
router.get('/api/v1/verify', function *() { // ?id=keyid&nonce=nonce
|
||||||
yield rest.verify(this);
|
yield rest.verify(this);
|
||||||
});
|
});
|
||||||
router.get('/api/verifyRemove', function *() { // ?id=keyid&nonce=nonce
|
router.get('/api/v1/verifyRemove', function *() { // ?id=keyid&nonce=nonce
|
||||||
yield rest.verifyRemove(this);
|
yield rest.verifyRemove(this);
|
||||||
});
|
});
|
||||||
router.get('/:email', function *() { // shorthand link for sharing
|
router.get('/:email', function *() { // shorthand link for sharing
|
||||||
yield rest.read(this);
|
yield rest.share(this);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Set HTTP response headers
|
// Set HTTP response headers
|
||||||
@ -76,7 +80,16 @@ app.use(function *(next) {
|
|||||||
|
|
||||||
app.use(router.routes());
|
app.use(router.routes());
|
||||||
app.use(router.allowedMethods());
|
app.use(router.allowedMethods());
|
||||||
app.on('error', (err, ctx) => log.error('worker', 'Unknown server error', err, ctx));
|
|
||||||
|
app.on('error', (error, ctx) => {
|
||||||
|
if (error.status) {
|
||||||
|
ctx.status = error.status;
|
||||||
|
ctx.body = error.message;
|
||||||
|
log.verbose('worker', 'Request faild: %s, %s', error.status, error.message);
|
||||||
|
} else {
|
||||||
|
log.error('worker', 'Unknown error', error, ctx);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
//
|
//
|
||||||
// Module initialization
|
// Module initialization
|
||||||
@ -89,14 +102,16 @@ function injectDependencies() {
|
|||||||
user: process.env.MONGO_USER || credentials.mongoUser,
|
user: process.env.MONGO_USER || credentials.mongoUser,
|
||||||
password: process.env.MONGO_PASS || credentials.mongoPass
|
password: process.env.MONGO_PASS || credentials.mongoPass
|
||||||
});
|
});
|
||||||
publicKey = new PublicKey(mongo);
|
email = new Email(nodemailer);
|
||||||
|
userId = new UserId(mongo);
|
||||||
|
publicKey = new PublicKey(openpgp, mongo, email, userId);
|
||||||
hkp = new HKP(publicKey);
|
hkp = new HKP(publicKey);
|
||||||
rest = new REST(publicKey);
|
rest = new REST(publicKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
function readCredentials() {
|
function readCredentials() {
|
||||||
try {
|
try {
|
||||||
return JSON.parse(fs.readFileSync(__dirname + '/../credentials.json'));
|
return require('../credentials.json');
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
log.info('worker', 'No credentials.json found ... using environment vars.');
|
log.info('worker', 'No credentials.json found ... using environment vars.');
|
||||||
}
|
}
|
||||||
@ -106,10 +121,17 @@ function readCredentials() {
|
|||||||
// Start app ... connect to the database and start listening
|
// Start app ... connect to the database and start listening
|
||||||
//
|
//
|
||||||
|
|
||||||
co(function *() {
|
if (!global.testing) { // don't automatically start server in tests
|
||||||
|
co(function *() {
|
||||||
|
let app = yield init();
|
||||||
|
app.listen(config.server.port);
|
||||||
|
}).catch(err => log.error('worker', 'Initialization failed!', err));
|
||||||
|
}
|
||||||
|
|
||||||
|
function *init() {
|
||||||
injectDependencies();
|
injectDependencies();
|
||||||
yield mongo.connect();
|
yield mongo.connect();
|
||||||
app.listen(process.env.PORT || 8888);
|
return app;
|
||||||
|
}
|
||||||
|
|
||||||
}).catch(err => log.error('worker', 'Initialization failed!', err));
|
module.exports = init;
|
@ -2,62 +2,70 @@
|
|||||||
|
|
||||||
require('co-mocha')(require('mocha')); // monkey patch mocha for generators
|
require('co-mocha')(require('mocha')); // monkey patch mocha for generators
|
||||||
|
|
||||||
const Mongo = require('../../src/dao/mongo'),
|
const log = require('npmlog');
|
||||||
expect = require('chai').expect,
|
const Mongo = require('../../src/dao/mongo');
|
||||||
fs = require('fs');
|
const expect = require('chai').expect;
|
||||||
|
|
||||||
describe('Mongo Integration Tests', function() {
|
describe('Mongo Integration Tests', function() {
|
||||||
this.timeout(20000);
|
this.timeout(20000);
|
||||||
|
|
||||||
const defaultType = 'apple';
|
const DB_TYPE = 'apple';
|
||||||
const secondaryType = 'orange';
|
|
||||||
let mongo;
|
let mongo;
|
||||||
|
|
||||||
before(function *() {
|
before(function *() {
|
||||||
let credentials;
|
let credentials;
|
||||||
try {
|
try {
|
||||||
credentials = JSON.parse(fs.readFileSync(__dirname + '/../../credentials.json'));
|
credentials = require('../../credentials.json');
|
||||||
} catch(e) {}
|
} catch(e) {
|
||||||
|
log.info('mongo-test', 'No credentials.json found ... using environment vars.');
|
||||||
|
}
|
||||||
mongo = new Mongo({
|
mongo = new Mongo({
|
||||||
uri: process.env.MONGO_URI || credentials.mongoUri,
|
uri: process.env.MONGO_URI || credentials.mongoUri,
|
||||||
user: process.env.MONGO_USER || credentials.mongoUser,
|
user: process.env.MONGO_USER || credentials.mongoUser,
|
||||||
password: process.env.MONGO_PASS || credentials.mongoPass,
|
password: process.env.MONGO_PASS || credentials.mongoPass
|
||||||
type: defaultType
|
|
||||||
});
|
});
|
||||||
yield mongo.connect();
|
yield mongo.connect();
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(function *() {
|
beforeEach(function *() {
|
||||||
yield mongo.clear();
|
yield mongo.clear(DB_TYPE);
|
||||||
yield mongo.clear(secondaryType);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(function() {});
|
afterEach(function() {});
|
||||||
|
|
||||||
after(function *() {
|
after(function *() {
|
||||||
yield mongo.clear();
|
yield mongo.clear(DB_TYPE);
|
||||||
yield mongo.clear(secondaryType);
|
|
||||||
yield mongo.disconnect();
|
yield mongo.disconnect();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("create", function() {
|
describe("create", function() {
|
||||||
it('should insert a document', function *() {
|
it('should insert a document', function *() {
|
||||||
let r = yield mongo.create({ _id:'0' });
|
let r = yield mongo.create({ _id:'0' }, DB_TYPE);
|
||||||
expect(r.insertedCount).to.equal(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should insert a document with a type', function *() {
|
|
||||||
let r = yield mongo.create({ _id:'0' });
|
|
||||||
expect(r.insertedCount).to.equal(1);
|
|
||||||
r = yield mongo.create({ _id:'0' }, secondaryType);
|
|
||||||
expect(r.insertedCount).to.equal(1);
|
expect(r.insertedCount).to.equal(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should fail if two with the same ID are inserted', function *() {
|
it('should fail if two with the same ID are inserted', function *() {
|
||||||
let r = yield mongo.create({ _id:'0' });
|
let r = yield mongo.create({ _id:'0' }, DB_TYPE);
|
||||||
expect(r.insertedCount).to.equal(1);
|
expect(r.insertedCount).to.equal(1);
|
||||||
try {
|
try {
|
||||||
r = yield mongo.create({ _id:'0' });
|
r = yield mongo.create({ _id:'0' }, DB_TYPE);
|
||||||
|
} catch(e) {
|
||||||
|
expect(e.message).to.match(/duplicate/);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("batch", function() {
|
||||||
|
it('should insert a document', function *() {
|
||||||
|
let r = yield mongo.batch([{ _id:'0' }, { _id:'1' }], DB_TYPE);
|
||||||
|
expect(r.insertedCount).to.equal(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fail if docs with the same ID are inserted', function *() {
|
||||||
|
let r = yield mongo.batch([{ _id:'0' }, { _id:'1' }], DB_TYPE);
|
||||||
|
expect(r.insertedCount).to.equal(2);
|
||||||
|
try {
|
||||||
|
r = yield mongo.batch([{ _id:'0' }, { _id:'1' }], DB_TYPE);
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
expect(e.message).to.match(/duplicate/);
|
expect(e.message).to.match(/duplicate/);
|
||||||
}
|
}
|
||||||
@ -66,64 +74,35 @@ describe('Mongo Integration Tests', function() {
|
|||||||
|
|
||||||
describe("update", function() {
|
describe("update", function() {
|
||||||
it('should update a document', function *() {
|
it('should update a document', function *() {
|
||||||
let r = yield mongo.create({ _id:'0' });
|
let r = yield mongo.create({ _id:'0' }, DB_TYPE);
|
||||||
r = yield mongo.update({ _id:'0' }, { foo:'bar' });
|
r = yield mongo.update({ _id:'0' }, { foo:'bar' }, DB_TYPE);
|
||||||
expect(r.modifiedCount).to.equal(1);
|
expect(r.modifiedCount).to.equal(1);
|
||||||
r = yield mongo.get({ _id:'0' });
|
r = yield mongo.get({ _id:'0' }, DB_TYPE);
|
||||||
expect(r.foo).to.equal('bar');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should update a document with a type', function *() {
|
|
||||||
let r = yield mongo.create({ _id:'0' }, secondaryType);
|
|
||||||
r = yield mongo.update({ _id:'0' }, { foo:'bar' }, secondaryType);
|
|
||||||
expect(r.modifiedCount).to.equal(1);
|
|
||||||
r = yield mongo.get({ _id:'0' }, secondaryType);
|
|
||||||
expect(r.foo).to.equal('bar');
|
expect(r.foo).to.equal('bar');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("get", function() {
|
describe("get", function() {
|
||||||
it('should get a document', function *() {
|
it('should get a document', function *() {
|
||||||
let r = yield mongo.create({ _id:'0' });
|
let r = yield mongo.create({ _id:'0' }, DB_TYPE);
|
||||||
r = yield mongo.get({ _id:'0' });
|
r = yield mongo.get({ _id:'0' }, DB_TYPE);
|
||||||
expect(r).to.exist;
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should get a document with a type', function *() {
|
|
||||||
let r = yield mongo.create({ _id:'0' }, secondaryType);
|
|
||||||
r = yield mongo.get({ _id:'0' }, secondaryType);
|
|
||||||
expect(r).to.exist;
|
expect(r).to.exist;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("list", function() {
|
describe("list", function() {
|
||||||
it('should list documents', function *() {
|
it('should list documents', function *() {
|
||||||
let r = yield mongo.create({ _id:'0', foo:'bar' });
|
let r = yield mongo.batch([{ _id:'0', foo:'bar' }, { _id:'1', foo:'bar' }], DB_TYPE);
|
||||||
r = yield mongo.create({ _id:'1', foo:'bar' });
|
r = yield mongo.list({ foo:'bar' }, DB_TYPE);
|
||||||
r = yield mongo.list({ foo:'bar' });
|
expect(r).to.deep.equal([{ _id:'0', foo:'bar' }, { _id:'1', foo:'bar' }], DB_TYPE);
|
||||||
expect(r).to.deep.equal([{ _id:'0', foo:'bar' }, { _id:'1', foo:'bar' }]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should list documents with a type', function *() {
|
|
||||||
let r = yield mongo.create({ _id:'0', foo:'bar' }, secondaryType);
|
|
||||||
r = yield mongo.create({ _id:'1', foo:'bar' }, secondaryType);
|
|
||||||
r = yield mongo.list({ foo:'bar' }, secondaryType);
|
|
||||||
expect(r).to.deep.equal([{ _id:'0', foo:'bar' }, { _id:'1', foo:'bar' }]);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("remove", function() {
|
describe("remove", function() {
|
||||||
it('should remove a document', function *() {
|
it('should remove a document', function *() {
|
||||||
let r = yield mongo.create({ _id:'0' });
|
let r = yield mongo.create({ _id:'0' }, DB_TYPE);
|
||||||
r = yield mongo.remove({ _id:'0' });
|
r = yield mongo.remove({ _id:'0' }, DB_TYPE);
|
||||||
r = yield mongo.get({ _id:'0' });
|
r = yield mongo.get({ _id:'0' }, DB_TYPE);
|
||||||
expect(r).to.not.exist;
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should remove a document with a type', function *() {
|
|
||||||
let r = yield mongo.create({ _id:'0' }, secondaryType);
|
|
||||||
r = yield mongo.remove({ _id:'0' }, secondaryType);
|
|
||||||
r = yield mongo.get({ _id:'0' }, secondaryType);
|
|
||||||
expect(r).to.not.exist;
|
expect(r).to.not.exist;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
69
test/integration/worker-test.js
Normal file
69
test/integration/worker-test.js
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
require('co-mocha')(require('mocha')); // monkey patch mocha for generators
|
||||||
|
|
||||||
|
const request = require('supertest');
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
describe.skip('Koa HTTP Server (worker) Integration Tests', function() {
|
||||||
|
this.timeout(20000);
|
||||||
|
|
||||||
|
let app, pgpKey1;
|
||||||
|
|
||||||
|
before(function *() {
|
||||||
|
pgpKey1 = fs.readFileSync(__dirname + '/../key1.asc', 'utf8');
|
||||||
|
global.testing = true;
|
||||||
|
let init = require('../../src/worker');
|
||||||
|
app = yield init();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(function () {});
|
||||||
|
|
||||||
|
afterEach(function() {});
|
||||||
|
|
||||||
|
after(function () {});
|
||||||
|
|
||||||
|
describe('REST api', function() {
|
||||||
|
describe('POST /api/v1/key', function() {
|
||||||
|
it('should return 400 for an invalid body', function (done) {
|
||||||
|
request(app.listen())
|
||||||
|
.post('/api/v1/key')
|
||||||
|
.send({ foo: 'bar' })
|
||||||
|
.expect(400)
|
||||||
|
.end(done);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('HKP api', function() {
|
||||||
|
describe('GET /pks/add', function() {
|
||||||
|
it('should return 200 for a valid request', function (done) {
|
||||||
|
request(app.listen())
|
||||||
|
.get('/pks/lookup?op=get&search=0xDBC0B3D92B1B86E9')
|
||||||
|
.expect(200)
|
||||||
|
.end(done);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('POST /pks/add', function() {
|
||||||
|
it('should return 400 for an invalid body', function (done) {
|
||||||
|
request(app.listen())
|
||||||
|
.post('/pks/add')
|
||||||
|
.type('form')
|
||||||
|
.send('keytext=asdf')
|
||||||
|
.expect(400)
|
||||||
|
.end(done);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 200 for a valid PGP key', function (done) {
|
||||||
|
request(app.listen())
|
||||||
|
.post('/pks/add')
|
||||||
|
.type('form')
|
||||||
|
.send('keytext=' + encodeURIComponent(pgpKey1))
|
||||||
|
.expect(200)
|
||||||
|
.end(done);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
32
test/key1.asc
Normal file
32
test/key1.asc
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||||
|
Version: OpenPGP.js v1.4.1
|
||||||
|
|
||||||
|
xsBNBFZ+SREBCADFXPrLlyXIFHmBcRFEs2nwzID8X3+YmUOvY55+TJbyeggt
|
||||||
|
Ccgd21JiBNuL3yilZ2HmQbjzmJvuLFIKCBSmGeg3LU3QhcoSdHZu+NtiimuJ
|
||||||
|
/CruUFD0JJJn0LdV+4R9nlsoUaNtmTfmgS0ErfcICTmzASgjIwk1bKwFN6qh
|
||||||
|
L+LAVNOU7QL5Jk/6MDUVIU/6sijqGAANAy3aDnFx0x0e0A0QUVdXZU4TUV42
|
||||||
|
wu31rN39ZhgfHwvSEDZsljh2UvoYgS9uFfmO2CxXhoSsp3LKbwP9Os9atlOr
|
||||||
|
7vNOO+nL6GXuBFqNNNycpajiIMBLm6XTeY4ci+EaUX54q7DFUJfamBS3ABEB
|
||||||
|
AAHNM3NhZmV3aXRobWUgdGVzdHVzZXIgPHNhZmV3aXRobWUudGVzdHVzZXJA
|
||||||
|
Z21haWwuY29tPsLAcgQQAQgAJgUCVn5JEgYLCQgHAwIJENvAs9krG4bpBBUI
|
||||||
|
AgoDFgIBAhsDAh4BAAAr6Qf/WWCYCBL2Izau5S+H5zZtCk5Rde51pGw1fsMd
|
||||||
|
gMlZK7knMUSlfjIEx6C/y5uQRRJc6f44p7B609mCGwW+f91wpAGq4d6O3+BV
|
||||||
|
25GDj25UvN8dBW42cufLG7tTSXkDXQNhaWEF15yD7aDOdaWy6OpD66xiR2Rs
|
||||||
|
vT9BG5la4vIVwQxcMeTg62axTe/uu7IwcxQr1zT9nNvw5lmrF68YqTVl4ArM
|
||||||
|
axV9yMsbTmjYvS5LmD2vSzRi/OJzfIMYAiTbkSgoF91lFp0rAgq485MEOBjF
|
||||||
|
T4CnwCVHT8BOjeBi7JnKDb3JN7HGYHX1ZwuaiYJDtkbqHKrTLSmtJJUTJBRs
|
||||||
|
p14uqM7ATQRWfkkRAQgA2l9OZ2doMLKNhi6JC1Qd1iBWrmMAflbuOstoRz76
|
||||||
|
C/++VUlVeT7tuOiVJtYgxc1qRgIZEwzZIpM5/p25lX6mrXkgUJd7w8EYbTqa
|
||||||
|
J9h5jeontZaGHciVRAWyUy35PMevXTt4pKXQvzGS3jXK46ICE2/rxa02sE1N
|
||||||
|
S1kHCMQnWh89uMpE7sIG2s8QPOYVBHk88hf4M6jGDT2f2pKFwWuJ51z9IZul
|
||||||
|
wF/my6/rSBkXqhckJCOaq4H1F6iQCV6R0NmEMMe+UYz784mcw9B5JDqYoTux
|
||||||
|
3uCPEUeb7kW3M6IdPo2OSEEuvukdQvDmt3jKjnCk5RPKYZYFl9yqFVAf0gbp
|
||||||
|
EwARAQABwsBfBBgBCAATBQJWfkkTCRDbwLPZKxuG6QIbDAAAJyQH/icIJUhb
|
||||||
|
AuFntIB/nuX52kubnaXLlQc/erIg4y2aN92+g9ULv2myw6lf+kt5IHQtroR1
|
||||||
|
MVFSZgHVwSIhrwZqbaZTvi7VZq5NYDjRL1mda+rodhyNQEVM+8Q4XZh7yR8h
|
||||||
|
TNZn6OsENP1ctxs4J4T/jJL0mdhG/aCkbO4DICAEToViWOmUOpQBJwUI41Wh
|
||||||
|
qSeCLRV510QasWZVe0o86yCB11gxrg/+xd5XN6Za/pTtz+4KeD3m8ssygdNS
|
||||||
|
woY7ieO647qE7GagQdWP+4BIYPeEqnRTqyTMxpSivlal2IcEw/Fi0xM97+ER
|
||||||
|
FtBVBq+eZC88+gSiwcmxsB8s3rMPQhJ6Q0Y=
|
||||||
|
=/POi
|
||||||
|
-----END PGP PUBLIC KEY BLOCK-----
|
Loading…
Reference in New Issue
Block a user