diff --git a/Gruntfile.js b/Gruntfile.js index aa20e9bf..80c6464e 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -24,11 +24,10 @@ module.exports = function(grunt) { livereload: true, }, }, - // we monitor only app/models/* because we have test for models only now -// test: { -// files: ['test/**/*.js', 'test/*.js','app/models/*.js'], -// tasks: ['test'], -// } + test: { + files: ['test/**/*.js', 'test/*.js','app/**/*.js'], + tasks: ['test'], + } }, jshint: { all: { diff --git a/config/config.js b/config/config.js index f0da3f9d..ee0a01de 100644 --- a/config/config.js +++ b/config/config.js @@ -85,6 +85,7 @@ var enableMailbox = process.env.ENABLE_MAILBOX === 'true'; var enableRatelimiter = process.env.ENABLE_RATELIMITER === 'true'; var enableCredentialstore = process.env.ENABLE_CREDSTORE === 'true'; var enableEmailstore = process.env.ENABLE_EMAILSTORE === 'true'; +var enablePublicInfo = process.env.ENABLE_PUBLICINFO === 'true'; var loggerLevel = process.env.LOGGER_LEVEL || 'info'; var enableHTTPS = process.env.ENABLE_HTTPS === 'true'; @@ -105,6 +106,8 @@ module.exports = { credentialstore: require('../plugins/config-credentialstore'), enableEmailstore: enableEmailstore, emailstore: require('../plugins/config-emailstore'), + enablePublicInfo: enablePublicInfo, + publicInfo: require('../plugins/publicInfo/config'), loggerLevel: loggerLevel, enableHTTPS: enableHTTPS, version: version, diff --git a/insight.js b/insight.js index 8ca809b1..22d670c1 100755 --- a/insight.js +++ b/insight.js @@ -151,6 +151,10 @@ if (config.enableEmailstore) { require('./plugins/emailstore').init(expressApp, config.emailstore); } +if (config.enablePublicInfo) { + require('./plugins/publicInfo/publicInfo').init(expressApp, config.emailstore); +} + // express settings require('./config/express')(expressApp, historicSync, peerSync); require('./config/routes')(expressApp); diff --git a/package.json b/package.json index 6189684b..756dc6c7 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "async": "*", "base58-native": "0.1.2", "bignum": "*", + "bitauth": "^0.1.1", "bitcore": "git://github.com/bitpay/bitcore.git#aa41c70cff2583d810664c073a324376c39c8b36", "bufferput": "git://github.com/bitpay/node-bufferput.git", "buffertools": "*", diff --git a/plugins/publicInfo/config.js b/plugins/publicInfo/config.js new file mode 100644 index 00000000..7be35b6b --- /dev/null +++ b/plugins/publicInfo/config.js @@ -0,0 +1,2 @@ +module.exports = { +}; diff --git a/plugins/publicInfo/publicInfo.js b/plugins/publicInfo/publicInfo.js new file mode 100644 index 00000000..ddafb034 --- /dev/null +++ b/plugins/publicInfo/publicInfo.js @@ -0,0 +1,144 @@ +/** + * Module to allow Copay users to publish public information about themselves + * + * It uses BitAuth to verify the authenticity of the request. + * + */ +(function() { + +'use strict'; + +var logger = require('../../lib/logger').logger, + levelup = require('levelup'), + bitauth = require('bitauth'), + globalConfig = require('../../config/config'), + querystring = require('querystring'); + +var publicInfo = {}; + +/** + * Constant enum with the errors that the application may return + */ +var errors = { + MISSING_PARAMETER: { + code: 400, + message: 'Missing required parameter' + }, + UNAUTHENTICATED: { + code: 401, + message: 'SIN validation error' + }, + NOT_FOUND: { + code: 404, + message: 'There\'s no record of public information for the public key requested' + } +}; + +var NAMESPACE = 'public-info-'; +var MAX_ALLOWED_STORAGE = 64 * 1024 /* no more than 64 kb of data is allowed to be stored */; + +/** + * Initializes the plugin + * + * @param {Express} expressApp + * @param {Object} config + */ +publicInfo.init = function(expressApp, config) { + logger.info('Using publicInfo plugin'); + + var path = globalConfig.leveldb + '/publicinfo' + (globalConfig.name ? ('-' + globalConfig.name) : ''); + publicInfo.db = config.db || globalConfig.db || levelup(path); + + expressApp.post(globalConfig.apiPrefix + '/public', publicInfo.post); + expressApp.get(globalConfig.apiPrefix + '/public/:sin', publicInfo.get); +}; + +/** + * Helper function that ends a requests showing the user an error. The response body will be a JSON + * encoded object with only one property with key "error" and value error.message, one of + * the parameters of the function + * + * @param {Object} error - The error that caused the request to be terminated + * @param {number} error.code - the HTTP code to return + * @param {string} error.message - the message to send in the body + * @param {Express.Response} response - the express.js response. the methods status, json, and end + * will be called, terminating the request. + */ +var returnError = function(error, response) { + response.status(error.code).json({error: error.message}).end(); +}; + +/** + * Store a record in the database. The underlying database is merely a levelup instance (a key + * value store) that uses the SIN to store the body of the message. + * + * @param {Express.Request} request + * @param {Express.Response} response + */ +publicInfo.post = function(request, response) { + + var record = ''; + request.on('data', function(data) { + record += data; + if (record.length > MAX_ALLOWED_STORAGE) { + record = ''; + response.writeHead(413, {'Content-Type': 'text/plain'}).end(); + request.connection.destroy(); + } + }).on('end', function() { + var fullUrl = request.protocol + '://' + request.get('host') + request.url; + var data = fullUrl + record; + + bitauth.verifySignature(data, request.headers['x-identity'], request.headers['x-signature'], + function(err, result) { + if(err || !result) { + return returnError(errors.UNAUTHENTICATED, response); + } + + // Get the SIN from the public key + var sin = bitauth.getSinFromPublicKey(request.headers['x-identity']); + if (!sin) { + return returnError(errors.UNAUTHENTICATED, response); + } + publicInfo.db.put(NAMESPACE + sin, record, function (err) { + if (err) { + return returnError({code: 500, message: err}, response); + } + response.json({success: true}).end(); + if (request.testCallback) { + request.testCallback(); + } + }); + } + ); + }); +}; + +/** + * Retrieve a record from the database. + * + * The request is expected to contain the parameter "sin" + * + * @param {Express.Request} request + * @param {Express.Response} response + */ +publicInfo.get = function(request, response) { + var sin = request.param('sin'); + if (!sin) { + return returnError(errors.MISSING_PARAMETER, response); + } + + publicInfo.db.get(NAMESPACE + sin, function (err, value) { + if (err) { + if (err.notFound) { + return returnError(errors.NOT_FOUND, response); + } + return returnError({code: 500, message: err}, response); + } + response.send(value).end(); + }); +}; + +module.exports = publicInfo; + +})(); diff --git a/test/test.EmailStore.js b/test/test.EmailStore.js index 32a4dfe0..67029687 100644 --- a/test/test.EmailStore.js +++ b/test/test.EmailStore.js @@ -85,7 +85,7 @@ describe('emailstore test', function() { it('should allow the user to retrieve credentials', function() { request.param.onFirstCall().returns('secret'); leveldb_stub.get.reset(); - + var returnValue = '!@#$%'; leveldb_stub.get.onFirstCall().callsArgWith(1, null, returnValue); response.send.returnsThis(); diff --git a/test/test.PublicProfile.js b/test/test.PublicProfile.js new file mode 100644 index 00000000..cfe9fc68 --- /dev/null +++ b/test/test.PublicProfile.js @@ -0,0 +1,113 @@ +'use strict'; + +var chai = require('chai'); +var assert = require('assert'); +var sinon = require('sinon'); +var should = chai.should; +var expect = chai.expect; +var bitauth = require('bitauth'); + +describe('public profile test', function() { + + var globalConfig = require('../config/config'); + var leveldb_stub = sinon.stub(); + leveldb_stub.put = sinon.stub(); + leveldb_stub.get = sinon.stub(); + var plugin = require('../plugins/publicInfo/publicInfo.js'); + var express_mock = null; + var request = null; + var response = null; + + beforeEach(function() { + + express_mock = sinon.stub(); + express_mock.post = sinon.stub(); + express_mock.get = sinon.stub(); + + plugin.init(express_mock, {db: leveldb_stub}); + + request = sinon.stub(); + request.on = sinon.stub(); + request.param = sinon.stub(); + response = sinon.stub(); + response.send = sinon.stub(); + response.status = sinon.stub(); + response.json = sinon.stub(); + response.end = sinon.stub(); + }); + + it('initializes correctly', function() { + assert(plugin.db === leveldb_stub); + assert(express_mock.post.calledWith( + globalConfig.apiPrefix + '/public', plugin.post + )); + assert(express_mock.get.calledWith( + globalConfig.apiPrefix + '/public/:sin', plugin.get + )); + }); + + it.only('writes a message correctly', function(done) { + + var privateKey = bitauth.generateSin(); + var protocol = 'https'; + var dataToSign = protocol + '://hosturlSTUFF'; + var signature = bitauth.sign(dataToSign, privateKey.priv); + request.get = function() { return 'host'; }; + request.protocol = protocol; + request.url = 'url'; + request.headers = { + 'x-identity': privateKey.pub, + 'x-signature': signature + }; + request.on.onFirstCall().callsArgWith(1, 'STUFF'); + request.on.onFirstCall().returnsThis(); + request.on.onSecondCall().callsArg(1); + + leveldb_stub.put.onFirstCall().callsArg(2); + response.status.returns(response); + response.json.returns(response); + + request.testCallback = function() { + assert(leveldb_stub.put.firstCall.args[0] === 'public-info-' + privateKey.sin); + assert(leveldb_stub.put.firstCall.args[1] === 'STUFF'); + assert(response.json.calledOnce); + assert(response.end.calledOnce); + done(); + }; + + plugin.post(request, response); + }); + + it('fails if the signature is invalid', function() { + var data = 'uecord3'; + request.get = function() { return ''; }; + request.headers = {}; + request.on.onFirstCall().callsArgWith(1, data); + request.on.onFirstCall().returnsThis(); + request.on.onSecondCall().callsArg(1); + leveldb_stub.put = sinon.stub(); + + leveldb_stub.put.onFirstCall().callsArg(2); + response.json.returnsThis(); + response.status.returnsThis(); + + plugin.post(request, response); + + assert(response.end.calledOnce); + }); + + it('retrieves a message correctly', function() { + + request.param.onFirstCall().returns('SIN'); + + var returnValue = '!@#$%'; + leveldb_stub.get.onFirstCall().callsArgWith(1, null, returnValue); + response.send.returnsThis(); + + plugin.get(request, response); + + assert(leveldb_stub.get.firstCall.args[0] === 'public-info-SIN'); + assert(response.send.calledWith(returnValue)); + assert(response.end.calledOnce); + }); +});