feature: Public Profile information for a SIN
This commit is contained in:
parent
9764a1d423
commit
f06e1020d9
|
@ -24,11 +24,10 @@ module.exports = function(grunt) {
|
||||||
livereload: true,
|
livereload: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// we monitor only app/models/* because we have test for models only now
|
test: {
|
||||||
// test: {
|
files: ['test/**/*.js', 'test/*.js','app/**/*.js'],
|
||||||
// files: ['test/**/*.js', 'test/*.js','app/models/*.js'],
|
tasks: ['test'],
|
||||||
// tasks: ['test'],
|
}
|
||||||
// }
|
|
||||||
},
|
},
|
||||||
jshint: {
|
jshint: {
|
||||||
all: {
|
all: {
|
||||||
|
|
|
@ -85,6 +85,7 @@ var enableMailbox = process.env.ENABLE_MAILBOX === 'true';
|
||||||
var enableRatelimiter = process.env.ENABLE_RATELIMITER === 'true';
|
var enableRatelimiter = process.env.ENABLE_RATELIMITER === 'true';
|
||||||
var enableCredentialstore = process.env.ENABLE_CREDSTORE === 'true';
|
var enableCredentialstore = process.env.ENABLE_CREDSTORE === 'true';
|
||||||
var enableEmailstore = process.env.ENABLE_EMAILSTORE === 'true';
|
var enableEmailstore = process.env.ENABLE_EMAILSTORE === 'true';
|
||||||
|
var enablePublicInfo = process.env.ENABLE_PUBLICINFO === 'true';
|
||||||
var loggerLevel = process.env.LOGGER_LEVEL || 'info';
|
var loggerLevel = process.env.LOGGER_LEVEL || 'info';
|
||||||
var enableHTTPS = process.env.ENABLE_HTTPS === 'true';
|
var enableHTTPS = process.env.ENABLE_HTTPS === 'true';
|
||||||
|
|
||||||
|
@ -105,6 +106,8 @@ module.exports = {
|
||||||
credentialstore: require('../plugins/config-credentialstore'),
|
credentialstore: require('../plugins/config-credentialstore'),
|
||||||
enableEmailstore: enableEmailstore,
|
enableEmailstore: enableEmailstore,
|
||||||
emailstore: require('../plugins/config-emailstore'),
|
emailstore: require('../plugins/config-emailstore'),
|
||||||
|
enablePublicInfo: enablePublicInfo,
|
||||||
|
publicInfo: require('../plugins/publicInfo/config'),
|
||||||
loggerLevel: loggerLevel,
|
loggerLevel: loggerLevel,
|
||||||
enableHTTPS: enableHTTPS,
|
enableHTTPS: enableHTTPS,
|
||||||
version: version,
|
version: version,
|
||||||
|
|
|
@ -151,6 +151,10 @@ if (config.enableEmailstore) {
|
||||||
require('./plugins/emailstore').init(expressApp, config.emailstore);
|
require('./plugins/emailstore').init(expressApp, config.emailstore);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (config.enablePublicInfo) {
|
||||||
|
require('./plugins/publicInfo/publicInfo').init(expressApp, config.emailstore);
|
||||||
|
}
|
||||||
|
|
||||||
// express settings
|
// express settings
|
||||||
require('./config/express')(expressApp, historicSync, peerSync);
|
require('./config/express')(expressApp, historicSync, peerSync);
|
||||||
require('./config/routes')(expressApp);
|
require('./config/routes')(expressApp);
|
||||||
|
|
|
@ -54,6 +54,7 @@
|
||||||
"async": "*",
|
"async": "*",
|
||||||
"base58-native": "0.1.2",
|
"base58-native": "0.1.2",
|
||||||
"bignum": "*",
|
"bignum": "*",
|
||||||
|
"bitauth": "^0.1.1",
|
||||||
"bitcore": "git://github.com/bitpay/bitcore.git#aa41c70cff2583d810664c073a324376c39c8b36",
|
"bitcore": "git://github.com/bitpay/bitcore.git#aa41c70cff2583d810664c073a324376c39c8b36",
|
||||||
"bufferput": "git://github.com/bitpay/node-bufferput.git",
|
"bufferput": "git://github.com/bitpay/node-bufferput.git",
|
||||||
"buffertools": "*",
|
"buffertools": "*",
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
module.exports = {
|
||||||
|
};
|
|
@ -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 <tt>error.message</tt>, 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;
|
||||||
|
|
||||||
|
})();
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in New Issue