[CHORE] merge upstream changes

This commit is contained in:
Matthew Fettig
2014-05-30 14:35:01 -07:00
12 changed files with 2502 additions and 1454 deletions

3
.gitignore vendored
View File

@@ -43,3 +43,6 @@ test/config.js
/src-cov /src-cov
/coverage.html /coverage.html
/coverage /coverage
# Ignore IntelliJ files
.idea

View File

@@ -593,8 +593,23 @@ Amount.prototype.invert = function() {
* 25.2 XRP => 25200000/XRP * 25.2 XRP => 25200000/XRP
* USD 100.40 => 100.4/USD/? * USD 100.40 => 100.4/USD/?
* 100 => 100000000/XRP * 100 => 100000000/XRP
*
*
* The regular expression below matches above cases, broken down for better understanding:
*
* ^\s* // start with any amount of whitespace
* ([a-z]{3})? // optional any 3 letters
* \s* // any amount of whitespace
* (-)? // optional dash
* (\d+) // 1 or more digits
* (\.(\d*))? // optional . character with any amount of digits
* \s* // any amount of whitespace
* ([a-f0-9]{40}|[a-z0-9]{3})? // optional 40 character hex string OR 3 letters
* \s* // any amount of whitespace
* $ // end of string
*
*/ */
Amount.human_RE = /^\s*([a-z]{3})?\s*(-)?(\d+)(?:\.(\d*))?\s*([a-f0-9]{40}|[a-z0-9]{3})?\s*$/i; Amount.human_RE = /^\s*([a-z]{3})?\s*(-)?(\d+)(\.(\d*))?\s*([a-f0-9]{40}|[a-z0-9]{3})?\s*$/i;
Amount.prototype.parse_human = function(j, opts) { Amount.prototype.parse_human = function(j, opts) {
opts = opts || {}; opts = opts || {};

View File

@@ -1,36 +1,60 @@
var async = require('async');
var superagent = require('superagent');
var RippleTxt = require('./rippletxt').RippleTxt; var RippleTxt = require('./rippletxt').RippleTxt;
var request = require('superagent');
function AuthInfo() { function AuthInfo() {
this.rippleTxt = new RippleTxt(); this.rippleTxt = new RippleTxt();
} };
AuthInfo.prototype._getRippleTxt = function(domain, callback) {
this.rippleTxt.get(domain, callback);
};
AuthInfo.prototype._getUser = function(url, callback) {
superagent.get(url, callback);
};
/** /**
* Get auth info for a given username * Get auth info for a given username
*
* @param {string} domain - Domain which hosts the user's info * @param {string} domain - Domain which hosts the user's info
* @param {string} username - Username who's info we are retreiving * @param {string} username - Username who's info we are retreiving
* @param {function} fn - Callback function * @param {function} fn - Callback function
*/ */
AuthInfo.prototype.get = function (domain, username, fn) {
AuthInfo.prototype.get = function(domain, username, callback) {
var self = this; var self = this;
self.rippleTxt.get(domain, function(err, txt){ function getRippleTxt(callback) {
if (err) return fn(err); self._getRippleTxt(domain, function(err, txt) {
if (err) {
processTxt(txt); return callback(err);
});
function processTxt(txt) {
if (!txt.authinfo_url) return fn(new Error("Authentication is not supported on "+domain));
var url = Array.isArray(txt.authinfo_url) ? txt.authinfo_url[0] : txt.authinfo_url;
url += "?domain="+domain+"&username="+username;
request.get(url, function(err, resp){
if (err || resp.error) return fn(new Error("Authentication info server unreachable"));
fn(null, resp.body);
});
} }
if (!txt.authinfo_url) {
return callback(new Error('Authentication is not supported on ' + domain));
}
var url = Array.isArray(txt.authinfo_url) ? txt.authinfo_url[0] : txt.authinfo_url;
url += '?domain=' + domain + '&username=' + username;
callback(null, url);
});
}; };
module.exports.AuthInfo = AuthInfo; function getUser(url, callback) {
self._getUser(url, function(err, res) {
if (err || res.error) {
callback(new Error('Authentication info server unreachable'));
} else {
callback(null, res.body);
}
});
};
async.waterfall([ getRippleTxt, getUser ], callback);
};
exports.AuthInfo = AuthInfo;

View File

@@ -1,45 +1,44 @@
var crypt = require('./crypt').Crypt; var crypt = require('./crypt').Crypt;
var request = require('superagent'); var request = require('superagent');
var async = require('async');
var extend = require("extend"); var extend = require("extend");
var BlobClient = {}; var BlobClient = {};
//Blob object class //Blob object class
var BlobObj = function (url, id, key) { function BlobObj(url, id, key) {
this.url = url; this.url = url;
this.id = id; this.id = id;
this.key = key; this.key = key;
this.data = {};
this.identity = new Identity(this); this.identity = new Identity(this);
this.data = { };
}; };
// Blob operations // Blob operations
// Do NOT change the mapping of existing ops // Do NOT change the mapping of existing ops
BlobObj.ops = { BlobObj.ops = {
// Special // Special
"noop" : 0, noop: 0,
// Simple ops // Simple ops
"set" : 16, set: 16,
"unset" : 17, unset: 17,
"extend" : 18, extend: 18,
// Meta ops // Meta ops
"push" : 32, push: 32,
"pop" : 33, pop: 33,
"shift" : 34, shift: 34,
"unshift" : 35, unshift: 35,
"filter" : 36 filter: 36
}; };
BlobObj.opsReverseMap = [ ]; BlobObj.opsReverseMap = [ ];
for (var name in BlobObj.ops) { for (var name in BlobObj.ops) {
BlobObj.opsReverseMap[BlobObj.ops[name]] = name; BlobObj.opsReverseMap[BlobObj.ops[name]] = name;
} }
//Identity fields //Identity fields
var identityRoot = 'identityVault'; var identityRoot = 'identityVault';
var identityFields = [ var identityFields = [
@@ -83,26 +82,31 @@ var idTypeFields = [
'other' 'other'
]; ];
/* /**
* Initialize a new blob object * Initialize a new blob object
*
* @param {function} fn - Callback function * @param {function} fn - Callback function
*/ */
BlobObj.prototype.init = function(fn) { BlobObj.prototype.init = function(fn) {
var self = this, url; var self = this, url;
if (self.url.indexOf("://") === -1) self.url = "http://" + url;
if (self.url.indexOf('://') === -1) {
self.url = 'http://' + url;
}
url = self.url + '/v1/blob/' + self.id; url = self.url + '/v1/blob/' + self.id;
request.get(url, function(err, resp) { request.get(url, function(err, resp) {
if (err || !resp.body || resp.body.result !== 'success') {
if (err || !resp.body || resp.body.result !== 'success') return fn(new Error('Could not retrieve blob'));
return fn(new Error("Could not retrieve blob")); }
self.revision = resp.body.revision; self.revision = resp.body.revision;
self.encrypted_secret = resp.body.encrypted_secret; self.encrypted_secret = resp.body.encrypted_secret;
if (!self.decrypt(resp.body.blob)) { if (!self.decrypt(resp.body.blob)) {
return fn(new Error("Error while decrypting blob")); return fn(new Error('Error while decrypting blob'));
} }
//Apply patches //Apply patches
@@ -112,26 +116,30 @@ BlobObj.prototype.init = function (fn) {
successful = successful && self.applyEncryptedPatch(patch); successful = successful && self.applyEncryptedPatch(patch);
}); });
if (successful) self.consolidate(); if (successful) {
self.consolidate();
}
} }
fn(null, self);//return with newly decrypted blob //return with newly decrypted blob
fn(null, self);
}).timeout(8000); }).timeout(8000);
}; };
/**
/*
* Consolidate - * Consolidate -
* Consolidate patches as a new revision * Consolidate patches as a new revision
*
* @param {function} fn - Callback function * @param {function} fn - Callback function
*/ */
BlobObj.prototype.consolidate = function(fn) { BlobObj.prototype.consolidate = function(fn) {
// Callback is optional // Callback is optional
if ("function" !== typeof fn) fn = function(){}; if (typeof fn !== 'function') {
fn = function(){};
}
console.log("client: blob: consolidation at revision", this.revision); //console.log('client: blob: consolidation at revision', this.revision);
var encrypted = this.encrypt(); var encrypted = this.encrypt();
var config = { var config = {
@@ -150,80 +158,83 @@ BlobObj.prototype.consolidate = function (fn) {
request.post(signed.url) request.post(signed.url)
.send(signed.data) .send(signed.data)
.end(function(err, resp) { .end(function(err, resp) {
// XXX Add better error information to exception // XXX Add better error information to exception
if (err) return fn(new Error("Failed to consolidate blob - XHR error")); if (err) {
else if (resp.body && resp.body.result === 'success') return fn(null, resp.body); fn(new Error('Failed to consolidate blob - XHR error'));
else return fn(new Error("Failed to consolidate blob")); } else if (resp.body && resp.body.result === 'success') {
fn(null, resp.body);
} else {
fn(new Error('Failed to consolidate blob'));
}
}); });
}; };
/**
/*
* ApplyEncryptedPatch - * ApplyEncryptedPatch -
* save changes from a downloaded patch to the blob * save changes from a downloaded patch to the blob
*
* @param {string} patch - encrypted patch string * @param {string} patch - encrypted patch string
*/ */
BlobObj.prototype.applyEncryptedPatch = function (patch)
{
try {
var params = JSON.parse(crypt.decrypt(this.key, patch));
var op = params.shift();
var path = params.shift();
this.applyUpdate(op, path, params); BlobObj.prototype.applyEncryptedPatch = function(patch) {
try {
var args = JSON.parse(crypt.decrypt(this.key, patch));
var op = args.shift();
var path = args.shift();
this.applyUpdate(op, path, args);
this.revision++; this.revision++;
return true; return true;
} catch (err) { } catch (err) {
console.log("client: blob: failed to apply patch:", err.toString()); //console.log('client: blob: failed to apply patch:', err.toString());
console.log(err.stack); //console.log(err.stack);
return false; return false;
} }
}; };
/** /**
* Encrypt secret with unlock key * Encrypt secret with unlock key
*
* @param {string} secretUnlockkey * @param {string} secretUnlockkey
*/ */
BlobObj.prototype.encryptSecret = function(secretUnlockKey, secret) { BlobObj.prototype.encryptSecret = function(secretUnlockKey, secret) {
return crypt.encrypt(secretUnlockKey, secret); return crypt.encrypt(secretUnlockKey, secret);
}; };
/** /**
* Decrypt secret with unlock key * Decrypt secret with unlock key
*
* @param {string} secretUnlockkey * @param {string} secretUnlockkey
*/ */
BlobObj.prototype.decryptSecret = function(secretUnlockKey) { BlobObj.prototype.decryptSecret = function(secretUnlockKey) {
return crypt.decrypt(secretUnlockKey, this.encrypted_secret); return crypt.decrypt(secretUnlockKey, this.encrypted_secret);
}; };
/** /**
* Decrypt blob with crypt key * Decrypt blob with crypt key
*
* @param {string} data - encrypted blob data * @param {string} data - encrypted blob data
*/ */
BlobObj.prototype.decrypt = function (data) {
BlobObj.prototype.decrypt = function(data) {
try { try {
this.data = JSON.parse(crypt.decrypt(this.key, data)); this.data = JSON.parse(crypt.decrypt(this.key, data));
return this; return this;
} catch (e) { } catch (e) {
console.log("client: blob: decryption failed", e.toString()); //console.log('client: blob: decryption failed', e.toString());
console.log(e.stack); //console.log(e.stack);
return false; return false;
} }
}; };
/** /**
* Encrypt blob with crypt key * Encrypt blob with crypt key
*/ */
BlobObj.prototype.encrypt = function()
{
BlobObj.prototype.encrypt = function() {
// Filter Angular metadata before encryption // Filter Angular metadata before encryption
// if ('object' === typeof this.data && // if ('object' === typeof this.data &&
// 'object' === typeof this.data.contacts) // 'object' === typeof this.data.contacts)
@@ -232,12 +243,13 @@ BlobObj.prototype.encrypt = function()
return crypt.encrypt(this.key, JSON.stringify(this.data)); return crypt.encrypt(this.key, JSON.stringify(this.data));
}; };
/** /**
* Encrypt recovery key * Encrypt recovery key
*
* @param {string} secret * @param {string} secret
* @param {string} blobDecryptKey * @param {string} blobDecryptKey
*/ */
BlobObj.prototype.encryptBlobCrypt = function(secret, blobDecryptKey) { BlobObj.prototype.encryptBlobCrypt = function(secret, blobDecryptKey) {
var recoveryEncryptionKey = crypt.deriveRecoveryEncryptionKeyFromSecret(secret); var recoveryEncryptionKey = crypt.deriveRecoveryEncryptionKeyFromSecret(secret);
return crypt.encrypt(recoveryEncryptionKey, blobDecryptKey); return crypt.encrypt(recoveryEncryptionKey, blobDecryptKey);
@@ -245,55 +257,53 @@ BlobObj.prototype.encryptBlobCrypt = function (secret, blobDecryptKey) {
/** /**
* Decrypt recovery key * Decrypt recovery key
*
* @param {string} secret * @param {string} secret
*/ */
BlobObj.prototype.decryptBlobCrypt = function(secret) { BlobObj.prototype.decryptBlobCrypt = function(secret) {
var recoveryEncryptionKey = crypt.deriveRecoveryEncryptionKeyFromSecret(secret); var recoveryEncryptionKey = crypt.deriveRecoveryEncryptionKeyFromSecret(secret);
return crypt.decrypt(recoveryEncryptionKey, this.encrypted_blobdecrypt_key); return crypt.decrypt(recoveryEncryptionKey, this.encrypted_blobdecrypt_key);
}; };
/**** Blob updating functions ****/ /**** Blob updating functions ****/
/** /**
* Set blob element * Set blob element
*/ */
BlobObj.prototype.set = function(pointer, value, fn) { BlobObj.prototype.set = function(pointer, value, fn) {
this.applyUpdate('set', pointer, [value]); this.applyUpdate('set', pointer, [value]);
this.postUpdate('set', pointer, [value], fn); this.postUpdate('set', pointer, [value], fn);
}; };
/** /**
* Remove blob element * Remove blob element
*/ */
BlobObj.prototype.unset = function(pointer, fn) { BlobObj.prototype.unset = function(pointer, fn) {
this.applyUpdate('unset', pointer, []); this.applyUpdate('unset', pointer, []);
this.postUpdate('unset', pointer, [], fn); this.postUpdate('unset', pointer, [], fn);
}; };
/** /**
* Extend blob object * Extend blob object
*/ */
BlobObj.prototype.extend = function(pointer, value, fn) { BlobObj.prototype.extend = function(pointer, value, fn) {
this.applyUpdate('extend', pointer, [value]); this.applyUpdate('extend', pointer, [value]);
this.postUpdate('extend', pointer, [value], fn); this.postUpdate('extend', pointer, [value], fn);
}; };
/** /**
* Prepend blob array * Prepend blob array
*/ */
BlobObj.prototype.unshift = function(pointer, value, fn) { BlobObj.prototype.unshift = function(pointer, value, fn) {
this.applyUpdate('unshift', pointer, [value]); this.applyUpdate('unshift', pointer, [value]);
this.postUpdate('unshift', pointer, [value], fn); this.postUpdate('unshift', pointer, [value], fn);
}; };
/** /**
* Filter the row(s) from an array. * Filter the row(s) from an array.
* *
@@ -302,49 +312,50 @@ BlobObj.prototype.unshift = function (pointer, value, fn) {
* *
* The subcommands can be any commands with the pointer parameter left out. * The subcommands can be any commands with the pointer parameter left out.
*/ */
BlobObj.prototype.filter = function(pointer, field, value, subcommands, callback) { BlobObj.prototype.filter = function(pointer, field, value, subcommands, callback) {
var params = Array.prototype.slice.apply(arguments); var args = Array.prototype.slice.apply(arguments);
if ("function" === typeof params[params.length-1]) {
callback = params.pop(); if (typeof args[args.length - 1] === 'function') {
callback = args.pop();
} }
params.shift();
args.shift();
// Normalize subcommands to minimize the patch size // Normalize subcommands to minimize the patch size
params = params.slice(0, 2).concat(normalizeSubcommands(params.slice(2), true)); args = args.slice(0, 2).concat(normalizeSubcommands(args.slice(2), true));
this.applyUpdate('filter', pointer, params); this.applyUpdate('filter', pointer, args);
this.postUpdate('filter', pointer, params, callback); this.postUpdate('filter', pointer, args, callback);
}; };
/** /**
* Apply udpdate to the blob * Apply udpdate to the blob
*/ */
BlobObj.prototype.applyUpdate = function (op, path, params) {
BlobObj.prototype.applyUpdate = function(op, path, params) {
// Exchange from numeric op code to string // Exchange from numeric op code to string
if ("number" === typeof op) { if (typeof op === 'number') {
op = BlobObj.opsReverseMap[op]; op = BlobObj.opsReverseMap[op];
} }
if ("string" !== typeof op) {
throw new Error("Blob update op code must be a number or a valid op id string"); if (typeof op !== 'string') {
throw new Error('Blob update op code must be a number or a valid op id string');
} }
// Separate each step in the "pointer" // Separate each step in the 'pointer'
var pointer = path.split("/"); var pointer = path.split('/');
var first = pointer.shift(); var first = pointer.shift();
if (first !== "") {
throw new Error("Invalid JSON pointer: "+path); if (first !== '') {
throw new Error('Invalid JSON pointer: '+path);
} }
this._traverse(this.data, pointer, path, op, params); this._traverse(this.data, pointer, path, op, params);
}; };
//for applyUpdate function //for applyUpdate function
BlobObj.prototype._traverse = function (context, pointer, BlobObj.prototype._traverse = function(context, pointer, originalPointer, op, params) {
originalPointer, op, params) {
var _this = this; var _this = this;
var part = _this.unescapeToken(pointer.shift()); var part = _this.unescapeToken(pointer.shift());
@@ -352,16 +363,15 @@ BlobObj.prototype._traverse = function (context, pointer,
if (part === '-') { if (part === '-') {
part = context.length; part = context.length;
} else if (part % 1 !== 0 && part >= 0) { } else if (part % 1 !== 0 && part >= 0) {
throw new Error("Invalid pointer, array element segments must be " + throw new Error('Invalid pointer, array element segments must be a positive integer, zero or '-'');
"a positive integer, zero or '-'");
} }
} else if ("object" !== typeof context) { } else if (typeof context !== 'object') {
return null; return null;
} else if (!context.hasOwnProperty(part)) { } else if (!context.hasOwnProperty(part)) {
// Some opcodes create the path as they're going along // Some opcodes create the path as they're going along
if (op === "set") { if (op === 'set') {
context[part] = {}; context[part] = {};
} else if (op === "unshift") { } else if (op === 'unshift') {
context[part] = []; context[part] = [];
} else { } else {
return null; return null;
@@ -369,42 +379,39 @@ BlobObj.prototype._traverse = function (context, pointer,
} }
if (pointer.length !== 0) { if (pointer.length !== 0) {
return this._traverse(context[part], pointer, return this._traverse(context[part], pointer, originalPointer, op, params);
originalPointer, op, params);
} }
switch (op) { switch (op) {
case "set": case 'set':
context[part] = params[0]; context[part] = params[0];
break; break;
case "unset": case 'unset':
if (Array.isArray(context)) { if (Array.isArray(context)) {
context.splice(part, 1); context.splice(part, 1);
} else { } else {
delete context[part]; delete context[part];
} }
break; break;
case "extend": case 'extend':
if ("object" !== typeof context[part]) { if (typeof context[part] !== 'object') {
throw new Error("Tried to extend a non-object"); throw new Error('Tried to extend a non-object');
} }
extend(true, context[part], params[0]); extend(true, context[part], params[0]);
break; break;
case "unshift": case 'unshift':
if ("undefined" === typeof context[part]) { if (typeof context[part] === 'undefined') {
context[part] = [ ]; context[part] = [ ];
} else if (!Array.isArray(context[part])) { } else if (!Array.isArray(context[part])) {
throw new Error("Operator 'unshift' must be applied to an array."); throw new Error('Operator "unshift" must be applied to an array.');
} }
context[part].unshift(params[0]); context[part].unshift(params[0]);
break; break;
case "filter": case 'filter':
if (Array.isArray(context[part])) { if (Array.isArray(context[part])) {
context[part].forEach(function(element, i) { context[part].forEach(function(element, i) {
if ("object" === typeof element && if (typeof element === 'object' && element.hasOwnProperty(params[0]) && element[params[0]] === params[1]) {
element.hasOwnProperty(params[0]) && var subpointer = originalPointer + '/' + i;
element[params[0]] === params[1]) {
var subpointer = originalPointer+"/"+i;
var subcommands = normalizeSubcommands(params.slice(2)); var subcommands = normalizeSubcommands(params.slice(2));
subcommands.forEach(function(subcommand) { subcommands.forEach(function(subcommand) {
@@ -417,47 +424,51 @@ BlobObj.prototype._traverse = function (context, pointer,
} }
break; break;
default: default:
throw new Error("Unsupported op "+op); throw new Error('Unsupported op '+op);
} }
}; };
BlobObj.prototype.escapeToken = function(token) { BlobObj.prototype.escapeToken = function(token) {
return token.replace(/[~\/]/g, function (key) { return key === "~" ? "~0" : "~1"; }); return token.replace(/[~\/]/g, function(key) {
return key === '~' ? '~0' : '~1';
});
}; };
BlobObj.prototype.unescapeToken = function(str) { BlobObj.prototype.unescapeToken = function(str) {
return str.replace(/~./g, function(m) { return str.replace(/~./g, function(m) {
switch (m) { switch (m) {
case "~0": case '~0':
return "~"; return '~';
case "~1": case '~1':
return "/"; return '/';
} }
throw("Invalid tilde escape: " + m); throw new Error('Invalid tilde escape: ' + m);
}); });
}; };
/** /**
* Sumbit update to blob vault * Sumbit update to blob vault
*/ */
BlobObj.prototype.postUpdate = function(op, pointer, params, fn) { BlobObj.prototype.postUpdate = function(op, pointer, params, fn) {
// Callback is optional // Callback is optional
if ("function" !== typeof fn) fn = function(){}; if (typeof fn !== 'function') {
fn = function(){};
}
if ("string" === typeof op) { if (typeof op === 'string') {
op = BlobObj.ops[op]; op = BlobObj.ops[op];
} }
if ("number" !== typeof op) {
throw new Error("Blob update op code must be a number or a valid op id string"); if (typeof op !== 'number') {
} throw new Error('Blob update op code must be a number or a valid op id string');
if (op < 0 || op > 255) {
throw new Error("Blob update op code out of bounds");
} }
console.log("client: blob: submitting update", BlobObj.opsReverseMap[op], pointer, params); if (op < 0 || op > 255) {
throw new Error('Blob update op code out of bounds');
}
//console.log('client: blob: submitting update', BlobObj.opsReverseMap[op], pointer, params);
params.unshift(pointer); params.unshift(pointer);
params.unshift(op); params.unshift(op);
@@ -472,36 +483,29 @@ BlobObj.prototype.postUpdate = function (op, pointer, params, fn) {
} }
}; };
var signed = crypt.signRequestHmac(config, this.data.auth_secret, this.id); var signed = crypt.signRequestHmac(config, this.data.auth_secret, this.id);
request.post(signed.url) request.post(signed.url)
.send(signed.data) .send(signed.data)
.end(function(err, resp) { .end(function(err, resp) {
if (err) if (err) {
return fn(new Error("Patch could not be saved - XHR error")); fn(new Error('Patch could not be saved - XHR error'));
else if (!resp.body || resp.body.result !== 'success') } else if (!resp.body || resp.body.result !== 'success') {
return fn(new Error("Patch could not be saved - bad result")); fn(new Error('Patch could not be saved - bad result'));
} else {
return fn(null, resp.body); fn(null, resp.body);
}
}); });
}; };
/***** helper functions *****/ /***** helper functions *****/
function normalizeSubcommands(subcommands, compress) { function normalizeSubcommands(subcommands, compress) {
// Normalize parameter structure // Normalize parameter structure
if ("number" === typeof subcommands[0] || if (/(number|string)/.test(typeof subcommands[0])) {
"string" === typeof subcommands[0]) {
// Case 1: Single subcommand inline // Case 1: Single subcommand inline
subcommands = [subcommands]; subcommands = [subcommands];
} else if (subcommands.length === 1 && } else if (subcommands.length === 1 && Array.isArray(subcommands[0]) && /(number|string)/.test(typeof subcommands[0][0])) {
Array.isArray(subcommands[0]) &&
("number" === typeof subcommands[0][0] ||
"string" === typeof subcommands[0][0])) {
// Case 2: Single subcommand as array // Case 2: Single subcommand as array
// (nothing to do) // (nothing to do)
} else if (Array.isArray(subcommands[0])) { } else if (Array.isArray(subcommands[0])) {
@@ -511,15 +515,18 @@ function normalizeSubcommands(subcommands, compress) {
// Normalize op name and convert strings to numeric codes // Normalize op name and convert strings to numeric codes
subcommands = subcommands.map(function(subcommand) { subcommands = subcommands.map(function(subcommand) {
if ("string" === typeof subcommand[0]) { if (typeof subcommand[0] === 'string') {
subcommand[0] = BlobObj.ops[subcommand[0]]; subcommand[0] = BlobObj.ops[subcommand[0]];
} }
if ("number" !== typeof subcommand[0]) {
throw new Error("Invalid op in subcommand"); if (typeof subcommand[0] !== 'number') {
throw new Error('Invalid op in subcommand');
} }
if ("string" !== typeof subcommand[1]) {
throw new Error("Invalid path in subcommand"); if (typeof subcommand[1] !== 'string') {
throw new Error('Invalid path in subcommand');
} }
return subcommand; return subcommand;
}); });
@@ -542,6 +549,7 @@ function normalizeSubcommands(subcommands, compress) {
* Identity class * Identity class
* *
*/ */
var Identity = function (blob) { var Identity = function (blob) {
var self = this; var self = this;
self.blob = blob; self.blob = blob;
@@ -559,12 +567,12 @@ var Identity = function (blob) {
}; };
}; };
/** /**
* getFullAddress * getFullAddress
* returns the address formed into a text string * returns the address formed into a text string
* @param {string} key - Encryption key * @param {string} key - Encryption key
*/ */
Identity.prototype.getFullAddress = function (key) { Identity.prototype.getFullAddress = function (key) {
if (!this.blob || if (!this.blob ||
!this.blob.data || !this.blob.data ||
@@ -592,6 +600,7 @@ Identity.prototype.getFullAddress = function (key) {
* @param {string} key - Encryption key * @param {string} key - Encryption key
* @param {function} fn - Callback function * @param {function} fn - Callback function
*/ */
Identity.prototype.getAll = function (key) { Identity.prototype.getAll = function (key) {
if (!this.blob || !this.blob.data || !this.blob.data[identityRoot]) { if (!this.blob || !this.blob.data || !this.blob.data[identityRoot]) {
@@ -606,13 +615,13 @@ Identity.prototype.getAll = function (key) {
return result; return result;
}; };
/** /**
* get * get
* get and decrypt a single identity field * get and decrypt a single identity field
* @param {string} pointer - Field to retrieve * @param {string} pointer - Field to retrieve
* @param {string} key - Encryption key * @param {string} key - Encryption key
*/ */
Identity.prototype.get = function (pointer, key) { Identity.prototype.get = function (pointer, key) {
if (!this.blob || !this.blob.data || !this.blob.data[identityRoot]) { if (!this.blob || !this.blob.data || !this.blob.data[identityRoot]) {
return null; return null;
@@ -651,7 +660,6 @@ Identity.prototype.get = function (pointer, key) {
} }
}; };
/** /**
* set * set
* set and encrypt a single identity field. * set and encrypt a single identity field.
@@ -660,6 +668,7 @@ Identity.prototype.get = function (pointer, key) {
* @param {string} value - Unencrypted data * @param {string} value - Unencrypted data
* @param {function} fn - Callback function * @param {function} fn - Callback function
*/ */
Identity.prototype.set = function (pointer, key, value, fn) { Identity.prototype.set = function (pointer, key, value, fn) {
var self = this; var self = this;
@@ -732,7 +741,6 @@ Identity.prototype.set = function (pointer, key, value, fn) {
} }
}; };
/** /**
* unset * unset
* remove a single identity field - will only be removed * remove a single identity field - will only be removed
@@ -741,6 +749,7 @@ Identity.prototype.set = function (pointer, key, value, fn) {
* @param {string} key - Encryption key * @param {string} key - Encryption key
* @param {function} fn - Callback function * @param {function} fn - Callback function
*/ */
Identity.prototype.unset = function (pointer, key, fn) { Identity.prototype.unset = function (pointer, key, fn) {
//NOTE: this is rather useless since you can overwrite //NOTE: this is rather useless since you can overwrite
@@ -753,52 +762,62 @@ Identity.prototype.unset = function (pointer, key, fn) {
this.blob.unset("/" + identityRoot+"/" + pointer, fn); this.blob.unset("/" + identityRoot+"/" + pointer, fn);
}; };
/***** blob client methods ****/ /***** blob client methods ****/
/** /**
* Blob object class * Blob object class
*/ */
BlobClient.Blob = BlobObj;
exports.Blob = BlobObj;
/** /**
* Get ripple name for a given address * Get ripple name for a given address
*/ */
module.exports.getRippleName = function (url, address, fn) {
if (!crypt.isValidAddress(address)) return fn (new Error("Invalid ripple address")); exports.getRippleName = function(url, address, fn) {
if (!crypt.isValidAddress(address)) {
return fn (new Error('Invalid ripple address'));
}
request.get(url + '/v1/user/' + address, function(err, resp){ request.get(url + '/v1/user/' + address, function(err, resp){
if (err) return fn(new Error("Unable to access vault sever")); if (err) {
else if (resp.body && resp.body.username) return fn(null, resp.body.username); fn(new Error('Unable to access vault sever'));
else if (resp.body && resp.body.exists === false) return fn (new Error("No ripple name for this address")); } else if (resp.body && resp.body.username) {
else return fn(new Error("Unable to determine if ripple name exists")); fn(null, resp.body.username);
} else if (resp.body && resp.body.exists === false) {
fn (new Error('No ripple name for this address'));
} else {
fn(new Error('Unable to determine if ripple name exists'));
}
}); });
}; };
/**
/*
* Retrive a blob with url, id and key * Retrive a blob with url, id and key
*/ */
BlobClient.get = function (url, id, crypt, fn) { BlobClient.get = function (url, id, crypt, fn) {
var blob = new BlobObj(url, id, crypt); var blob = new BlobObj(url, id, crypt);
blob.init(fn); blob.init(fn);
}; };
/**
/*
* Verify email address * Verify email address
*/ */
BlobClient.verify = function(url, username, token, fn) { BlobClient.verify = function(url, username, token, fn) {
url += '/v1/user/' + username + '/verify/' + token; url += '/v1/user/' + username + '/verify/' + token;
request.get(url, function(err, resp){ request.get(url, function(err, resp){
if (err) return fn(err); if (err) {
else if (resp.body && resp.body.result === 'success') return fn(null, data); fn(err);
else return fn(new Error("Failed to verify the account")); } else if (resp.body && resp.body.result === 'success') {
fn(null, data);
} else {
fn(new Error('Failed to verify the account'));
}
}); });
}; };
/** /**
* Create a blob object * Create a blob object
* *
@@ -812,11 +831,12 @@ BlobClient.verify = function (url, username, token, fn) {
* @param {object} options.oldUserBlob * @param {object} options.oldUserBlob
* @param {function} fn * @param {function} fn
*/ */
BlobClient.create = function (options, fn) {
BlobClient.create = function(options, fn) {
var blob = new BlobObj(options.url, options.id, options.crypt); var blob = new BlobObj(options.url, options.id, options.crypt);
blob.revision = 0; blob.revision = 0;
blob.data = { blob.data = {
auth_secret: crypt.createSecret(8), auth_secret: crypt.createSecret(8),
account_id: crypt.getAddress(options.masterkey), account_id: crypt.getAddress(options.masterkey),
@@ -834,7 +854,7 @@ BlobClient.create = function (options, fn) {
//post to the blob vault to create //post to the blob vault to create
var config = { var config = {
method : "POST", method: 'POST',
url: options.url + '/v1/user', url: options.url + '/v1/user',
data: { data: {
blob_id: options.id, blob_id: options.id,
@@ -854,10 +874,14 @@ BlobClient.create = function (options, fn) {
request.post(signed) request.post(signed)
.send(signed.data) .send(signed.data)
.end(function(err, resp) { .end(function(err, resp) {
if (err) return fn(err); if (err) {
else if (resp.body && resp.body.result === 'success') return fn(null, blob,resp.body); fn(err);
else return fn(new Error("Could not create blob")); } else if (resp.body && resp.body.result === 'success') {
fn(null, blob,resp.body);
} else {
fn(new Error('Could not create blob'));
}
}); });
}; };
module.exports.BlobClient = BlobClient; exports.BlobClient = BlobClient;

View File

@@ -3,32 +3,33 @@ var base = require('./base').Base;
var UInt160 = require('./uint160').UInt160; var UInt160 = require('./uint160').UInt160;
var message = require('./message'); var message = require('./message');
var request = require('superagent'); var request = require('superagent');
var querystring = require('querystring');
var extend = require("extend"); var extend = require("extend");
var parser = require("url"); var parser = require("url");
var Crypt = { }; var Crypt = { };
var cryptConfig = { var cryptConfig = {
cipher : "aes", cipher : 'aes',
mode : "ccm", mode : 'ccm',
ts : 64, // tag length ts : 64, // tag length
ks : 256, // key size ks : 256, // key size
iter : 1000 // iterations (key derivation) iter : 1000 // iterations (key derivation)
}; };
/** /**
* Full domain hash based on SHA512 * Full domain hash based on SHA512
*/ */
function fdh(data, bytelen)
{ function fdh(data, bytelen) {
var bitlen = bytelen << 3; var bitlen = bytelen << 3;
if (typeof data === "string") { if (typeof data === 'string') {
data = sjcl.codec.utf8String.toBits(data); data = sjcl.codec.utf8String.toBits(data);
} }
// Add hashing rounds until we exceed desired length in bits // Add hashing rounds until we exceed desired length in bits
var counter = 0, output = []; var counter = 0, output = [];
while (sjcl.bitArray.bitLength(output) < bitlen) { while (sjcl.bitArray.bitLength(output) < bitlen) {
var hash = sjcl.hash.sha512.hash(sjcl.bitArray.concat([counter], data)); var hash = sjcl.hash.sha512.hash(sjcl.bitArray.concat([counter], data));
output = sjcl.bitArray.concat(output, hash); output = sjcl.bitArray.concat(output, hash);
@@ -39,86 +40,90 @@ function fdh(data, bytelen)
output = sjcl.bitArray.clamp(output, bitlen); output = sjcl.bitArray.clamp(output, bitlen);
return output; return output;
} };
/** /**
* This is a function to derive different hashes from the same key. * This is a function to derive different hashes from the same key.
* Each hash is derived as HMAC-SHA512HALF(key, token). * Each hash is derived as HMAC-SHA512HALF(key, token).
*
* @param {string} key * @param {string} key
* @param {string} hash * @param {string} hash
*/ */
function keyHash(key, token) { function keyHash(key, token) {
var hmac = new sjcl.misc.hmac(key, sjcl.hash.sha512); var hmac = new sjcl.misc.hmac(key, sjcl.hash.sha512);
return sjcl.codec.hex.fromBits(sjcl.bitArray.bitSlice(hmac.encrypt(token), 0, 256)); return sjcl.codec.hex.fromBits(sjcl.bitArray.bitSlice(hmac.encrypt(token), 0, 256));
} };
/****** exposed functions ******/ /****** exposed functions ******/
/** /**
* KEY DERIVATION FUNCTION * KEY DERIVATION FUNCTION
* *
* This service takes care of the key derivation, i.e. converting low-entropy * This service takes care of the key derivation, i.e. converting low-entropy
* secret into higher entropy secret via either computationally expensive * secret into higher entropy secret via either computationally expensive
* processes or peer-assisted key derivation (PAKDF). * processes or peer-assisted key derivation (PAKDF).
*
* @param {object} opts * @param {object} opts
* @param {string} purpose - Key type/purpose * @param {string} purpose - Key type/purpose
* @param {string} username * @param {string} username
* @param {string} secret - Also known as passphrase/password * @param {string} secret - Also known as passphrase/password
* @param {function} fn * @param {function} fn
*/ */
Crypt.derive = function(opts, purpose, username, secret, fn) { Crypt.derive = function(opts, purpose, username, secret, fn) {
var tokens; var tokens;
if (purpose=='login') tokens = ['id', 'crypt'];
else tokens = ['unlock'];
var iExponent = new sjcl.bn(String(opts.exponent)), if (purpose === 'login') {
iModulus = new sjcl.bn(String(opts.modulus)), tokens = ['id', 'crypt'];
iAlpha = new sjcl.bn(String(opts.alpha)); } else {
tokens = ['unlock'];
}
var publicInfo = "PAKDF_1_0_0:"+opts.host.length+":"+opts.host+ var iExponent = new sjcl.bn(String(opts.exponent));
":"+username.length+":"+username+ var iModulus = new sjcl.bn(String(opts.modulus));
":"+purpose.length+":"+purpose+ var iAlpha = new sjcl.bn(String(opts.alpha));
":",
publicSize = Math.ceil(Math.min((7+iModulus.bitLength()) >>> 3, 256)/8), var publicInfo = [ 'PAKDF_1_0_0', opts.host.length, opts.host, username.length, username, purpose.length, purpose ].join(':') + ':';
publicHash = fdh(publicInfo, publicSize), var publicSize = Math.ceil(Math.min((7 + iModulus.bitLength()) >>> 3, 256) / 8);
publicHex = sjcl.codec.hex.fromBits(publicHash), var publicHash = fdh(publicInfo, publicSize);
iPublic = new sjcl.bn(String(publicHex)).setBitM(0), var publicHex = sjcl.codec.hex.fromBits(publicHash);
secretInfo = publicInfo+":"+secret.length+":"+secret+":", var iPublic = new sjcl.bn(String(publicHex)).setBitM(0);
secretSize = (7+iModulus.bitLength()) >>> 3, var secretInfo = [ publicInfo, secret.length, secret ].join(':') + ':';
secretHash = fdh(secretInfo, secretSize), var secretSize = (7 + iModulus.bitLength()) >>> 3;
secretHex = sjcl.codec.hex.fromBits(secretHash), var secretHash = fdh(secretInfo, secretSize);
iSecret = new sjcl.bn(String(secretHex)).mod(iModulus); var secretHex = sjcl.codec.hex.fromBits(secretHash);
var iSecret = new sjcl.bn(String(secretHex)).mod(iModulus);
if (iSecret.jacobi(iModulus) !== 1) { if (iSecret.jacobi(iModulus) !== 1) {
iSecret = iSecret.mul(iAlpha).mod(iModulus); iSecret = iSecret.mul(iAlpha).mod(iModulus);
} }
var iRandom; var iRandom;
for (;;) { for (;;) {
iRandom = sjcl.bn.random(iModulus, 0); iRandom = sjcl.bn.random(iModulus, 0);
if (iRandom.jacobi(iModulus) === 1) if (iRandom.jacobi(iModulus) === 1) {
break; break;
} }
}
var iBlind = iRandom.powermodMontgomery(iPublic.mul(iExponent), iModulus); var iBlind = iRandom.powermodMontgomery(iPublic.mul(iExponent), iModulus);
var iSignreq = iSecret.mulmod(iBlind, iModulus); var iSignreq = iSecret.mulmod(iBlind, iModulus);
var signreq = sjcl.codec.hex.fromBits(iSignreq.toBits()); var signreq = sjcl.codec.hex.fromBits(iSignreq.toBits());
request.post(opts.url) request.post(opts.url)
.send({ .send({ info: publicInfo, signreq: signreq })
info : publicInfo, .end(function(err, resp) {
signreq : signreq if (err || !resp) {
}).end(function(err, resp) { return fn(new Error('Could not query PAKDF server ' + opts.host));
}
if (err || !resp) return fn(new Error("Could not query PAKDF server "+opts.host));
var data = resp.body || resp.text ? JSON.parse(resp.text) : {}; var data = resp.body || resp.text ? JSON.parse(resp.text) : {};
if (data.result !== 'success') return fn(new Error("Could not query PAKDF server "+opts.host)); if (data.result !== 'success') {
return fn(new Error('Could not query PAKDF server '+opts.host));
}
var iSignres = new sjcl.bn(String(data.signres)); var iSignres = new sjcl.bn(String(data.signres));
var iRandomInv = iRandom.inverseMod(iModulus); var iRandomInv = iRandom.inverseMod(iModulus);
@@ -136,9 +141,10 @@ Crypt.derive = function (opts, purpose, username, secret, fn) {
/** /**
* Imported from ripple-client * Imported from ripple-client
*
*/ */
Crypt.RippleAddress = (function() { Crypt.RippleAddress = (function() {
function append_int(a, i) { function append_int(a, i) {
return [].concat(a, i >> 24, (i >> 16) & 0xff, (i >> 8) & 0xff, i & 0xff); return [].concat(a, i >> 24, (i >> 16) & 0xff, (i >> 8) & 0xff, i & 0xff);
} }
@@ -148,23 +154,24 @@ Crypt.RippleAddress = (function () {
sjcl.hash.sha512.hash(sjcl.codec.bytes.toBits(bytes)), sjcl.hash.sha512.hash(sjcl.codec.bytes.toBits(bytes)),
0, 256 0, 256
); );
} };
function SHA256_RIPEMD160(bits) { function SHA256_RIPEMD160(bits) {
return sjcl.hash.ripemd160.hash(sjcl.hash.sha256.hash(bits)); return sjcl.hash.ripemd160.hash(sjcl.hash.sha256.hash(bits));
} };
return function(seed) { return function(seed) {
this.seed = base.decode_check(33, seed); this.seed = base.decode_check(33, seed);
if (!this.seed) { if (!this.seed) {
throw "Invalid seed."; throw new Error('Invalid seed.');
} }
this.getAddress = function(seq) { this.getAddress = function(seq) {
seq = seq || 0; seq = seq || 0;
var private_gen, public_gen, i = 0; var private_gen, public_gen, i = 0;
do { do {
private_gen = sjcl.bn.fromBits(firstHalfOfSHA512(append_int(this.seed, i))); private_gen = sjcl.bn.fromBits(firstHalfOfSHA512(append_int(this.seed, i)));
i++; i++;
@@ -174,6 +181,7 @@ Crypt.RippleAddress = (function () {
var sec; var sec;
i = 0; i = 0;
do { do {
sec = sjcl.bn.fromBits(firstHalfOfSHA512(append_int(append_int(public_gen.toBytesCompressed(), seq), i))); sec = sjcl.bn.fromBits(firstHalfOfSHA512(append_int(append_int(public_gen.toBytesCompressed(), seq), i)));
i++; i++;
@@ -188,11 +196,12 @@ Crypt.RippleAddress = (function () {
/** /**
* Encrypt data * Encrypt data
* @params {string} key *
* @params {string} data * @param {string} key
* @param {string} data
*/ */
Crypt.encrypt = function(key, data)
{ Crypt.encrypt = function(key, data) {
key = sjcl.codec.hex.toBits(key); key = sjcl.codec.hex.toBits(key);
var opts = extend(true, {}, cryptConfig); var opts = extend(true, {}, cryptConfig);
@@ -208,12 +217,13 @@ Crypt.encrypt = function(key, data)
return sjcl.codec.base64.fromBits(encryptedBits); return sjcl.codec.base64.fromBits(encryptedBits);
}; };
/** /**
* Decrypt data * Decrypt data
* @params {string} key *
* @params {string} data * @param {string} key
* @param {string} data
*/ */
Crypt.decrypt = function (key, data) { Crypt.decrypt = function (key, data) {
key = sjcl.codec.hex.toBits(key); key = sjcl.codec.hex.toBits(key);
@@ -222,7 +232,7 @@ Crypt.decrypt = function (key, data) {
var version = sjcl.bitArray.extract(encryptedBits, 0, 8); var version = sjcl.bitArray.extract(encryptedBits, 0, 8);
if (version !== 0) { if (version !== 0) {
throw new Error("Unsupported encryption version: "+version); throw new Error('Unsupported encryption version: '+version);
} }
var encrypted = extend(true, {}, cryptConfig, { var encrypted = extend(true, {}, cryptConfig, {
@@ -236,25 +246,28 @@ Crypt.decrypt = function (key, data) {
/** /**
* Validate a ripple address * Validate a ripple address
*
* @param {string} address * @param {string} address
*/ */
Crypt.isValidAddress = function (address) { Crypt.isValidAddress = function (address) {
return UInt160.is_valid(address); return UInt160.is_valid(address);
}; };
/** /**
* Validate a ripple address * Validate a ripple address
*
* @param {integer} nWords - number of words * @param {integer} nWords - number of words
*/ */
Crypt.createSecret = function (nWords) { Crypt.createSecret = function (nWords) {
return sjcl.codec.hex.fromBits(sjcl.random.randomWords(nWords)); return sjcl.codec.hex.fromBits(sjcl.random.randomWords(nWords));
}; };
/** /**
* Create a new master key * Create a new master key
*/ */
Crypt.createMaster = function () { Crypt.createMaster = function () {
return base.encode_check(33, sjcl.codec.bytes.fromBits(sjcl.random.randomWords(4))); return base.encode_check(33, sjcl.codec.bytes.fromBits(sjcl.random.randomWords(4)));
}; };
@@ -262,78 +275,89 @@ Crypt.createMaster = function () {
/** /**
* Create a ripple address from a master key * Create a ripple address from a master key
*
* @param {string} masterkey * @param {string} masterkey
*/ */
Crypt.getAddress = function (masterkey) { Crypt.getAddress = function (masterkey) {
return new Crypt.RippleAddress(masterkey).getAddress(); return new Crypt.RippleAddress(masterkey).getAddress();
}; };
/** /**
* Hash data * Hash data
*
* @param {string} data * @param {string} data
*/ */
Crypt.hashSha512 = function (data) { Crypt.hashSha512 = function (data) {
return sjcl.codec.hex.fromBits(sjcl.hash.sha512.hash(data)); return sjcl.codec.hex.fromBits(sjcl.hash.sha512.hash(data));
}; };
/** /**
* Sign a data string with a secret key * Sign a data string with a secret key
*
* @param {string} secret * @param {string} secret
* @param {string} data * @param {string} data
*/ */
Crypt.signString = function(secret, data) { Crypt.signString = function(secret, data) {
var hmac = new sjcl.misc.hmac(sjcl.codec.hex.toBits(secret), sjcl.hash.sha512); var hmac = new sjcl.misc.hmac(sjcl.codec.hex.toBits(secret), sjcl.hash.sha512);
return sjcl.codec.hex.fromBits(hmac.mac(data)); return sjcl.codec.hex.fromBits(hmac.mac(data));
}; };
/** /**
* Create an an accout recovery key * Create an an accout recovery key
*
* @param {string} secret * @param {string} secret
*/ */
Crypt.deriveRecoveryEncryptionKeyFromSecret = function(secret) { Crypt.deriveRecoveryEncryptionKeyFromSecret = function(secret) {
var seed = ripple.Seed.from_json(secret).to_bits(); var seed = ripple.Seed.from_json(secret).to_bits();
var hmac = new sjcl.misc.hmac(seed, sjcl.hash.sha512); var hmac = new sjcl.misc.hmac(seed, sjcl.hash.sha512);
var key = hmac.mac("ripple/hmac/recovery_encryption_key/v1"); var key = hmac.mac('ripple/hmac/recovery_encryption_key/v1');
key = sjcl.bitArray.bitSlice(key, 0, 256); key = sjcl.bitArray.bitSlice(key, 0, 256);
return sjcl.codec.hex.fromBits(key); return sjcl.codec.hex.fromBits(key);
}; };
/** /**
* Convert base64 encoded data into base64url encoded data. * Convert base64 encoded data into base64url encoded data.
*
* @param {String} base64 Data * @param {String} base64 Data
*/ */
Crypt.base64ToBase64Url = function(encodedData) { Crypt.base64ToBase64Url = function(encodedData) {
return encodedData.replace(/\+/g, '-').replace(/\//g, '_').replace(/[=]+$/, ''); return encodedData.replace(/\+/g, '-').replace(/\//g, '_').replace(/[=]+$/, '');
}; };
/** /**
* Convert base64url encoded data into base64 encoded data. * Convert base64url encoded data into base64 encoded data.
*
* @param {String} base64 Data * @param {String} base64 Data
*/ */
Crypt.base64UrlToBase64 = function(encodedData) { Crypt.base64UrlToBase64 = function(encodedData) {
encodedData = encodedData.replace(/-/g, '+').replace(/_/g, '/'); encodedData = encodedData.replace(/-/g, '+').replace(/_/g, '/');
while (encodedData.length % 4) { while (encodedData.length % 4) {
encodedData += '='; encodedData += '=';
} }
return encodedData; return encodedData;
}; };
/** /**
* Create a string from request parameters that * Create a string from request parameters that
* will be used to sign a request * will be used to sign a request
*
* @param {Object} config - request params * @param {Object} config - request params
* @param {Object} parsed - parsed url * @param {Object} parsed - parsed url
* @param {Object} date * @param {Object} date
* @param {Object} mechanism - type of signing * @param {Object} mechanism - type of signing
*/ */
Crypt.getStringToSign = function(config, parsed, date, mechanism) { Crypt.getStringToSign = function(config, parsed, date, mechanism) {
// XXX This method doesn't handle signing GET requests correctly. The data // XXX This method doesn't handle signing GET requests correctly. The data
// field will be merged into the search string, not the request body. // field will be merged into the search string, not the request body.
// Sort the properties of the JSON object into canonical form // Sort the properties of the JSON object into canonical form
var canonicalData = JSON.stringify(copyObjectWithSortedKeys(config.data)); var canonicalData = JSON.stringify(copyObjectWithSortedKeys(config.data));
@@ -362,13 +386,14 @@ Crypt.getStringToSign = function (config, parsed, date, mechanism) {
].join('\n'); ].join('\n');
}; };
/** /**
* HMAC signed request * HMAC signed request
*
* @param {Object} config * @param {Object} config
* @param {Object} auth_secret * @param {Object} auth_secret
* @param {Object} blob_id * @param {Object} blob_id
*/ */
Crypt.signRequestHmac = function(config, auth_secret, blob_id) { Crypt.signRequestHmac = function(config, auth_secret, blob_id) {
config = extend(true, {}, config); config = extend(true, {}, config);
@@ -379,22 +404,26 @@ Crypt.signRequestHmac = function (config, auth_secret, blob_id) {
var stringToSign = Crypt.getStringToSign(config, parsed, date, signatureType); var stringToSign = Crypt.getStringToSign(config, parsed, date, signatureType);
var signature = Crypt.signString(auth_secret, stringToSign); var signature = Crypt.signString(auth_secret, stringToSign);
config.url += (parsed.search ? "&" : "?") + var query = querystring.stringify({
'signature='+Crypt.base64ToBase64Url(signature)+ signature: Crypt.base64ToBase64Url(signature),
'&signature_date='+date+ signature_date: date,
'&signature_blob_id='+blob_id+ signature_blob_id: blob_id,
'&signature_type='+signatureType; signature_type: signatureType
});
config.url += (parsed.search ? '&' : '?') + query;
return config; return config;
}; };
/** /**
* Asymmetric signed request * Asymmetric signed request
*
* @param {Object} config * @param {Object} config
* @param {Object} secretKey * @param {Object} secretKey
* @param {Object} account * @param {Object} account
* @param {Object} blob_id * @param {Object} blob_id
*/ */
Crypt.signRequestAsymmetric = function(config, secretKey, account, blob_id) { Crypt.signRequestAsymmetric = function(config, secretKey, account, blob_id) {
config = extend(true, {}, config); config = extend(true, {}, config);
@@ -405,65 +434,71 @@ Crypt.signRequestAsymmetric = function (config, secretKey, account, blob_id) {
var stringToSign = Crypt.getStringToSign(config, parsed, date, signatureType); var stringToSign = Crypt.getStringToSign(config, parsed, date, signatureType);
var signature = message.signMessage(stringToSign, secretKey); var signature = message.signMessage(stringToSign, secretKey);
config.url += (parsed.search ? "&" : "?") + var query = querystring.stringify({
'signature='+Crypt.base64ToBase64Url(signature)+ signature: Crypt.base64ToBase64Url(signature),
'&signature_date='+date+ signature_date: date,
'&signature_blob_id='+blob_id+ signature_blob_id: blob_id,
'&signature_account='+account+ signature_account: account,
'&signature_type='+signatureType; signature_type: signatureType
})
config.url += (parsed.search ? '&' : '?') + query;
return config; return config;
}; };
//prepare for signing //prepare for signing
function copyObjectWithSortedKeys(object) { function copyObjectWithSortedKeys(object) {
if (isPlainObject(object)) { if (isPlainObject(object)) {
var newObj = {}; var newObj = {};
var keysSorted = Object.keys(object).sort(); var keysSorted = Object.keys(object).sort();
var key; var key;
for (var i in keysSorted) { for (var i in keysSorted) {
key = keysSorted[i]; key = keysSorted[i];
if (Object.prototype.hasOwnProperty.call(object, key)) { if (Object.prototype.hasOwnProperty.call(object, key)) {
newObj[key] = copyObjectWithSortedKeys(object[key]); newObj[key] = copyObjectWithSortedKeys(object[key]);
} }
} }
return newObj; return newObj;
} else if (Array.isArray(object)) { } else if (Array.isArray(object)) {
return object.map(copyObjectWithSortedKeys); return object.map(copyObjectWithSortedKeys);
} else { } else {
return object; return object;
} }
} };
//from npm extend //from npm extend
function isPlainObject(obj) { function isPlainObject(obj) {
var hasOwn = Object.prototype.hasOwnProperty; var hasOwn = Object.prototype.hasOwnProperty;
var toString = Object.prototype.toString; var toString = Object.prototype.toString;
if (!obj || toString.call(obj) !== '[object Object]' || obj.nodeType || obj.setInterval) if (!obj || toString.call(obj) !== '[object Object]' || obj.nodeType || obj.setInterval) {
return false; return false;
}
var has_own_constructor = hasOwn.call(obj, 'constructor'); var has_own_constructor = hasOwn.call(obj, 'constructor');
var has_is_property_of_method = hasOwn.call(obj.constructor.prototype, 'isPrototypeOf'); var has_is_property_of_method = hasOwn.call(obj.constructor.prototype, 'isPrototypeOf');
// Not own constructor property must be Object // Not own constructor property must be Object
if (obj.constructor && !has_own_constructor && !has_is_property_of_method) if (obj.constructor && !has_own_constructor && !has_is_property_of_method) {
return false; return false;
}
// Own properties are enumerated firstly, so to speed up, // Own properties are enumerated firstly, so to speed up,
// if last one is own, then all properties are own. // if last one is own, then all properties are own.
var key; var key;
for ( key in obj ) {} for ( key in obj ) {}
return key === undefined || hasOwn.call( obj, key ); return key === void(0) || hasOwn.call( obj, key );
} }
var dateAsIso8601 = (function() { var dateAsIso8601 = (function() {
function pad(n) { function pad(n) {
return (n < 0 || n > 9 ? "" : "0") + n; return (n < 0 || n > 9 ? '' : '0') + n;
} };
return function dateAsIso8601() { return function dateAsIso8601() {
var date = new Date(); var date = new Date();
@@ -476,5 +511,5 @@ var dateAsIso8601 = (function () {
}; };
})(); })();
exports.Crypt = Crypt;
module.exports.Crypt = Crypt;

File diff suppressed because it is too large Load Diff

View File

@@ -1,16 +1,33 @@
var request = require('superagent'); var superagent = require('superagent');
function RippleTxt() { function RippleTxt() {
this.txts = { }; this.txts = { };
} };
RippleTxt.urlTemplates = [
'https://ripple.{{domain}}/ripple.txt',
'https://www.{{domain}}/ripple.txt',
'https://{{domain}}/ripple.txt',
'http://ripple.{{domain}}/ripple.txt',
'http://www.{{domain}}/ripple.txt',
'http://{{domain}}/ripple.txt'
];
RippleTxt.request = function() {
return request;
};
RippleTxt.prototype.request = function(url, callback) {
return superagent.get(url, callback);
};
/** /**
* Gets the ripple.txt file for the given domain * Gets the ripple.txt file for the given domain
*
* @param {string} domain - Domain to retrieve file from * @param {string} domain - Domain to retrieve file from
* @param {function} fn - Callback function * @param {function} fn - Callback function
*/ */
RippleTxt.prototype.get = function(domain, fn) { RippleTxt.prototype.get = function(domain, fn) {
var self = this; var self = this;
@@ -18,47 +35,47 @@ RippleTxt.prototype.get = function (domain, fn) {
return fn(null, self.txts[domain]); return fn(null, self.txts[domain]);
} }
var urls = [ ;(function nextUrl(i) {
'https://ripple.'+domain+'/ripple.txt', var url = RippleTxt.urlTemplates[i];
'https://www.'+domain+'/ripple.txt',
'https://'+domain+'/ripple.txt',
'http://ripple.'+domain+'/ripple.txt',
'http://www.'+domain+'/ripple.txt',
'http://'+domain+'/ripple.txt'
].reverse();
next(); if (!url) {
function next () { return fn(new Error('No ripple.txt found'));
if (!urls.length) return fn(new Error("No ripple.txt found")); }
var url = urls.pop();
request.get(url, function(err, resp) { url = url.replace('{{domain}}', domain);
if (err || !resp.text) return next();
self.request(url, function(err, resp) {
if (err || !resp.text) {
return nextUrl(++i);
}
var sections = self.parse(resp.text); var sections = self.parse(resp.text);
self.txts[domain] = sections; self.txts[domain] = sections;
fn(null, sections); fn(null, sections);
}); });
} })(0);
}; };
/** /**
* Parse a ripple.txt file * Parse a ripple.txt file
*
* @param {string} txt - Unparsed ripple.txt data * @param {string} txt - Unparsed ripple.txt data
*/ */
RippleTxt.prototype.parse = function(txt) { RippleTxt.prototype.parse = function(txt) {
var txt = txt.replace(/\r?\n/g, '\n').split('\n')
var currentSection = '';
var sections = { };
txt = txt.replace('\r\n', '\n');
txt = txt.replace('\r', '\n');
txt = txt.split('\n');
var currentSection = "", sections = {};
for (var i = 0, l = txt.length; i < l; i++) { for (var i = 0, l = txt.length; i < l; i++) {
var line = txt[i]; var line = txt[i];
if (!line.length || line[0] === '#') { if (!line.length || line[0] === '#') {
continue; continue;
} else if (line[0] === '[' && line[line.length-1] === ']') { }
if (line[0] === '[' && line[line.length - 1] === ']') {
currentSection = line.slice(1, line.length - 1); currentSection = line.slice(1, line.length - 1);
sections[currentSection] = []; sections[currentSection] = [];
} else { } else {
@@ -72,4 +89,4 @@ RippleTxt.prototype.parse = function (txt) {
return sections; return sections;
}; };
module.exports.RippleTxt = RippleTxt; exports.RippleTxt = RippleTxt;

View File

@@ -7,17 +7,19 @@ var log = require('./log').internal.sub('server');
/** /**
* @constructor Server * @constructor Server
*
* @param {Remote} Reference to a Remote object * @param {Remote} Reference to a Remote object
* @param {Object} Options * @param {Object} Options
* * @param {String} host
* host: String * @param {Number|String} port
* port: String or Number * @param [Boolean] securec
* secure: Boolean
*/ */
function Server(remote, opts) { function Server(remote, opts) {
EventEmitter.call(this); EventEmitter.call(this);
var self = this;
if (typeof opts === 'string') { if (typeof opts === 'string') {
var parsedUrl = url.parse(opts); var parsedUrl = url.parse(opts);
opts = { opts = {
@@ -31,16 +33,6 @@ function Server(remote, opts) {
throw new TypeError('Server configuration is not an Object'); throw new TypeError('Server configuration is not an Object');
} }
if (!opts.host) {
opts.host = opts.websocket_ip;
}
if (!opts.port) {
opts.port = opts.websocket_port;
}
if (!opts.secure) {
opts.secure = opts.websocket_ssl;
}
if (isNaN(opts.port)) { if (isNaN(opts.port)) {
throw new TypeError('Server port must be a number'); throw new TypeError('Server port must be a number');
} }
@@ -50,7 +42,7 @@ function Server(remote, opts) {
} }
if (typeof opts.secure !== 'boolean') { if (typeof opts.secure !== 'boolean') {
opts.secure = false; opts.secure = true;
} }
// We want to allow integer strings as valid port numbers for backward compatibility // We want to allow integer strings as valid port numbers for backward compatibility
@@ -66,52 +58,56 @@ function Server(remote, opts) {
throw new TypeError('Server "secure" configuration is not a Boolean'); throw new TypeError('Server "secure" configuration is not a Boolean');
} }
var self = this;
this._remote = remote; this._remote = remote;
this._opts = opts; this._opts = opts;
this._host = opts.host;
this._port = opts.port;
this._secure = opts.secure;
this._ws = void(0); this._ws = void(0);
this._connected = false; this._connected = false;
this._shouldConnect = false; this._shouldConnect = false;
this._state = 'offline'; this._state = 'offline';
this._id = 0; this._id = 0;
this._retry = 0; this._retry = 0;
this._requests = { }; this._requests = { };
this._load_base = 256; this._load_base = 256;
this._load_factor = 256; this._load_factor = 256;
this._fee = 10;
this._fee_ref = 10; this._fee_ref = 10;
this._fee_base = 10; this._fee_base = 10;
this._reserve_base = void(0); this._reserve_base = void(0);
this._reserve_inc = void(0); this._reserve_inc = void(0);
this._fee_cushion = this._remote.fee_cushion; this._fee_cushion = this._remote.fee_cushion;
this._opts.url = (opts.secure ? 'wss://' : 'ws://') + opts.host + ':' + opts.port; this._lastLedgerIndex = NaN;
this._lastLedgerClose = NaN;
this.on('message', function(message) { this._score = 0;
this._scoreWeights = {
ledgerclose: 5,
response: 1
};
this._url = this._opts.url = (this._opts.secure ? 'wss://' : 'ws://')
+ this._opts.host + ':' + this._opts.port;
this.on('message', onMessage);
function onMessage(message) {
self._handleMessage(message); self._handleMessage(message);
}); };
this.on('response_subscribe', function(message) { this.on('response_subscribe', onSubscribeResponse);
function onSubscribeResponse(message) {
self._handleResponseSubscribe(message); self._handleResponseSubscribe(message);
});
function checkServerActivity() {
if (isNaN(self._lastLedgerClose)) {
return;
}
var delta = (Date.now() - self._lastLedgerClose);
if (delta > (1000 * 20)) {
self.reconnect();
}
}; };
function setActivityInterval() { function setActivityInterval() {
self._activityInterval = setInterval(checkServerActivity, 1000); var interval = self._checkActivity.bind(self);
self._activityInterval = setInterval(interval, 1000);
}; };
this.on('disconnect', function onDisconnect() { this.on('disconnect', function onDisconnect() {
@@ -119,8 +115,18 @@ function Server(remote, opts) {
//self.once('ledger_closed', setActivityInterval); //self.once('ledger_closed', setActivityInterval);
}); });
this.once('ledger_closed', function() { //this.once('ledger_closed', setActivityInterval);
//setActiviyInterval();
this._remote.on('ledger_closed', function(ledger) {
self._updateScore('ledgerclose', ledger);
});
this.on('response_ping', function(message, request) {
self._updateScore('response', request);
});
this.on('load_changed', function(load) {
self._updateScore('loadchange', load);
}); });
}; };
@@ -143,6 +149,23 @@ Server.onlineStates = [
'full' 'full'
]; ];
/**
* This is the final interface between client code and a socket connection to a
* `rippled` server. As such, this is a decent hook point to allow a WebSocket
* interface conforming object to be used as a basis to mock rippled. This
* avoids the need to bind a websocket server to a port and allows a more
* synchronous style of code to represent a client <-> server message sequence.
* We can also use this to log a message sequence to a buffer.
*
* @api private
*/
Server.websocketConstructor = function() {
// We require this late, because websocket shims may be loaded after
// ripple-lib in the browser
return require('ws');
};
/** /**
* Set server state * Set server state
* *
@@ -172,6 +195,76 @@ Server.prototype._setState = function(state) {
} }
}; };
/**
* Check that server is still active.
*
* Server activity is determined by ledger_closed events.
* Maximum delay to receive a ledger_closed event is 20s.
*
* If server is inactive, reconnect
*
* @api private
*/
Server.prototype._checkActivity = function() {
if (!this._connected) {
return;
}
if (isNaN(this._lastLedgerClose)) {
return;
}
var delta = (Date.now() - this._lastLedgerClose);
if (delta > (1000 * 25)) {
//this.reconnect();
}
};
/**
* Server maintains a score for request prioritization.
*
* The score is determined by various data including
* this server's lag to receive ledger_closed events,
* ping response time, and load(fee) change
*
* @param {String} type
* @param {Object} data
* @api private
*/
Server.prototype._updateScore = function(type, data) {
if (!this._connected) {
return;
}
var weight = this._scoreWeights[type] || 1;
switch (type) {
case 'ledgerclose':
// Ledger lag
var delta = data.ledger_index - this._lastLedgerIndex;
if (delta > 0) {
this._score += weight * delta;
}
break;
case 'response':
// Ping lag
var delta = Math.floor((Date.now() - data.time) / 200);
this._score += weight * delta;
break;
case 'loadchange':
// Load/fee change
this._fee = Number(this._computeFee(10));
break;
}
if (this._score > 1e3) {
//this.reconnect();
}
};
/** /**
* Get the remote address for a server. * Get the remote address for a server.
* Incompatible with ripple-lib client build * Incompatible with ripple-lib client build
@@ -186,22 +279,6 @@ Server.prototype._remoteAddress = function() {
return address; return address;
}; };
/** This is the final interface between client code and a socket connection to a
* `rippled` server. As such, this is a decent hook point to allow a WebSocket
* interface conforming object to be used as a basis to mock rippled. This
* avoids the need to bind a websocket server to a port and allows a more
* synchronous style of code to represent a client <-> server message sequence.
* We can also use this to log a message sequence to a buffer.
*
* @api private
*/
Server.websocketConstructor = function() {
// We require this late, because websocket shims may be loaded after
// ripple-lib in the browser
return require('ws');
};
/** /**
* Disconnect from rippled WebSocket server * Disconnect from rippled WebSocket server
* *
@@ -274,7 +351,6 @@ Server.prototype.connect = function() {
}; };
ws.onopen = function onOpen() { ws.onopen = function onOpen() {
// If we are no longer the active socket, simply ignore any event
if (ws === self._ws) { if (ws === self._ws) {
self.emit('socket_open'); self.emit('socket_open');
// Subscribe to events // Subscribe to events
@@ -283,7 +359,6 @@ Server.prototype.connect = function() {
}; };
ws.onerror = function onError(e) { ws.onerror = function onError(e) {
// If we are no longer the active socket, simply ignore any event
if (ws === self._ws) { if (ws === self._ws) {
self.emit('socket_error'); self.emit('socket_error');
@@ -309,9 +384,7 @@ Server.prototype.connect = function() {
} }
}; };
// Failure to open.
ws.onclose = function onClose() { ws.onclose = function onClose() {
// If we are no longer the active socket, simply ignore any event
if (ws === self._ws) { if (ws === self._ws) {
if (self._remote.trace) { if (self._remote.trace) {
log.info('onclose:', self._opts.url, ws.readyState); log.info('onclose:', self._opts.url, ws.readyState);
@@ -333,12 +406,16 @@ Server.prototype._retryConnect = function() {
this._retry += 1; this._retry += 1;
var retryTimeout = (this._retry < 40) var retryTimeout = (this._retry < 40)
? (1000 / 20) // First, for 2 seconds: 20 times per second // First, for 2 seconds: 20 times per second
? (1000 / 20)
: (this._retry < 40 + 60) : (this._retry < 40 + 60)
? (1000) // Then, for 1 minute: once per second // Then, for 1 minute: once per second
? (1000)
: (this._retry < 40 + 60 + 60) : (this._retry < 40 + 60 + 60)
? (10 * 1000) // Then, for 10 minutes: once every 10 seconds // Then, for 10 minutes: once every 10 seconds
: (30 * 1000); // Then: once every 30 seconds ? (10 * 1000)
// Then: once every 30 seconds
: (30 * 1000);
function connectionRetry() { function connectionRetry() {
if (self._shouldConnect) { if (self._shouldConnect) {
@@ -365,8 +442,10 @@ Server.prototype._handleClose = function() {
this.emit('socket_close'); this.emit('socket_close');
this._setState('offline'); this._setState('offline');
function noOp() {};
// Prevent additional events from this socket // Prevent additional events from this socket
ws.onopen = ws.onerror = ws.onclose = ws.onmessage = function noOp() {}; ws.onopen = ws.onerror = ws.onclose = ws.onmessage = noOp;
if (self._shouldConnect) { if (self._shouldConnect) {
this._retryConnect(); this._retryConnect();
@@ -389,75 +468,122 @@ Server.prototype._handleMessage = function(message) {
} }
if (!Server.isValidMessage(message)) { if (!Server.isValidMessage(message)) {
this.emit('unexpected', message);
return; return;
} }
switch (message.type) { switch (message.type) {
case 'ledgerClosed': case 'ledgerClosed':
this._handleLedgerClosed(message);
break;
case 'serverStatus':
this._handleServerStatus(message);
break;
case 'response':
this._handleResponse(message);
break;
case 'path_find':
this._handlePathFind(message);
break;
}
};
Server.prototype._handleLedgerClosed = function(message) {
this._lastLedgerIndex = message.ledger_index;
this._lastLedgerClose = Date.now(); this._lastLedgerClose = Date.now();
this.emit('ledger_closed', message); this.emit('ledger_closed', message);
break; };
case 'serverStatus': Server.prototype._handleServerStatus = function(message) {
// This message is only received when online. // This message is only received when online.
// As we are connected, it is the definitive final state. // As we are connected, it is the definitive final state.
var isOnline = ~Server.onlineStates.indexOf(message.server_status);
this._setState(isOnline ? 'online' : 'offline');
this._setState(~(Server.onlineStates.indexOf(message.server_status)) ? 'online' : 'offline'); if (!Server.isLoadStatus(message)) {
return;
if (Server.isLoadStatus(message)) {
self.emit('load', message, self);
self._remote.emit('load', message, self);
if (message.load_base !== self._load_base || message.load_factor !== self._load_factor) {
// Load changed
self._load_base = message.load_base;
self._load_factor = message.load_factor;
self.emit('load_changed', message, self);
self._remote.emit('load_changed', message, self);
} }
}
break;
case 'response': this.emit('load', message, this);
this._remote.emit('load', message, this);
var loadChanged = message.load_base !== this._load_base
|| message.load_factor !== this._load_factor
if (loadChanged) {
this._load_base = message.load_base;
this._load_factor = message.load_factor;
this.emit('load_changed', message, this);
this._remote.emit('load_changed', message, this);
}
};
Server.prototype._handleResponse = function(message) {
// A response to a request. // A response to a request.
var request = self._requests[message.id]; var request = this._requests[message.id];
delete self._requests[message.id];
delete this._requests[message.id];
if (!request) { if (!request) {
if (this._remote.trace) { if (this._remote.trace) {
log.info('UNEXPECTED:', self._opts.url, message); log.info('UNEXPECTED:', this._opts.url, message);
} }
return; return;
} }
if (message.status === 'success') { if (message.status === 'success') {
if (this._remote.trace) { if (this._remote.trace) {
log.info('response:', self._opts.url, message); log.info('response:', this._opts.url, message);
} }
request.emit('success', message.result); var command = request.message.command;
var result = message.result;
var responseEvent = 'response_' + command;
[ self, self._remote ].forEach(function(emitter) { request.emit('success', result);
emitter.emit('response_' + request.message.command, message.result, request, message);
[ this, this._remote ].forEach(function(emitter) {
emitter.emit(responseEvent, result, request, message);
}); });
} else if (message.error) { } else if (message.error) {
if (this._remote.trace) { if (this._remote.trace) {
log.info('error:', self._opts.url, message); log.info('error:', this._opts.url, message);
} }
request.emit('error', { var error = {
error: 'remoteError', error: 'remoteError',
error_message: 'Remote reported an error.', error_message: 'Remote reported an error.',
remote: message remote: message
}); };
}
break;
case 'path_find': request.emit('error', error);
if (this._remote.trace) {
log.info('path_find:', self._opts.url, message);
} }
break; };
Server.prototype._handlePathFind = function(message) {
if (this._remote.trace) {
log.info('path_find:', this._opts.url, message);
}
};
/**
* Handle subscription response messages. Subscription response
* messages indicate that a connection to the server is ready
*
* @api private
*/
Server.prototype._handleResponseSubscribe = function(message) {
if (~(Server.onlineStates.indexOf(message.server_status))) {
this._setState('online');
}
if (Server.isLoadStatus(message)) {
this._load_base = message.load_base || 256;
this._load_factor = message.load_factor || 256;
this._fee_ref = message.fee_ref;
this._fee_base = message.fee_base;
this._reserve_base = message.reserve_base;
this._reserve_inc = message.reserve_inc;
} }
}; };
@@ -484,27 +610,6 @@ Server.isLoadStatus = function(message) {
&& (typeof message.load_factor === 'number'); && (typeof message.load_factor === 'number');
}; };
/**
* Handle subscription response messages. Subscription response
* messages indicate that a connection to the server is ready
*
* @api private
*/
Server.prototype._handleResponseSubscribe = function(message) {
if (~(Server.onlineStates.indexOf(message.server_status))) {
this._setState('online');
}
if (Server.isLoadStatus(message)) {
this._load_base = message.load_base || 256;
this._load_factor = message.load_factor || 256;
this._fee_ref = message.fee_ref;
this._fee_base = message.fee_base;
this._reserve_base = message.reserve_base;
this._reserve_inc = message.reserve_inc;
}
};
/** /**
* Send JSON message to rippled WebSocket server * Send JSON message to rippled WebSocket server
* *
@@ -544,6 +649,7 @@ Server.prototype._request = function(request) {
request.server = this; request.server = this;
request.message.id = this._id; request.message.id = this._id;
request.time = Date.now();
this._requests[request.message.id] = request; this._requests[request.message.id] = request;

View File

@@ -1,227 +1,269 @@
var AuthInfo = require('./authinfo').AuthInfo; var async = require('async');
var blobClient = require('./blob').BlobClient; var blobClient = require('./blob').BlobClient;
var AuthInfo = require('./authinfo').AuthInfo;
var crypt = require('./crypt').Crypt; var crypt = require('./crypt').Crypt;
function VaultClient(opts) { function VaultClient(opts) {
if (!opts) opts = {}; if (!opts) {
else if (typeof opts === "string") opts = {domain:opts}; opts = { };
}
if (typeof opts === 'string') {
opts = { domain: opts };
}
this.domain = opts.domain || 'ripple.com'; this.domain = opts.domain || 'ripple.com';
this.authInfo = new AuthInfo(); this.authInfo = new AuthInfo();
this.infos = { }; this.infos = { };
}
/**
* Reduce username to standardized form.
* Strips whitespace at beginning and end.
* @param {string} username - Username to normalize
*/
VaultClient.prototype.normalizeUsername = function (username) {
username = ""+username;
username = username.trim();
return username;
}; };
/**
* Reduce password to standardized form.
* Strips whitespace at beginning and end.
* @param {string} password - password to normalize
*/
VaultClient.prototype.normalizePassword = function (password) {
password = ""+password;
password = password.trim();
return password;
};
/** /**
* Get a ripple name from a given account address, if it has one * Get a ripple name from a given account address, if it has one
* @param {string} address - Account address to query * @param {string} address - Account address to query
* @param {string} url - Url of blob vault * @param {string} url - Url of blob vault
*/ */
VaultClient.prototype.getRippleName = function(address, url, fn) {
VaultClient.prototype.getRippleName = function(address, url, callback) {
//use the url from previously retrieved authInfo, if necessary //use the url from previously retrieved authInfo, if necessary
if (!url) return fn(new Error("Blob vault URL is required")); if (!url) {
blobClient.getRippleName(url, address, fn); callback(new Error('Blob vault URL is required'));
} else {
blobClient.getRippleName(url, address, callback);
}
}; };
/** /**
* Authenticate and retrieve a decrypted blob using a ripple name and password * Authenticate and retrieve a decrypted blob using a ripple name and password
*
* @param {string} username * @param {string} username
* @param {string} password * @param {string} password
* @param {function} fn - Callback function * @param {function} fn - Callback function
*/ */
VaultClient.prototype.login = function(username, password, fn) {
VaultClient.prototype.login = function(username, password, callback) {
var self = this; var self = this;
function getAuthInfo(callback) {
self.authInfo.get(self.domain, username, function(err, authInfo) { self.authInfo.get(self.domain, username, function(err, authInfo) {
if (err) return fn(err); if (err) {
return callback(err);
}
if (authInfo.version !== 3) { if (authInfo.version !== 3) {
return fn(new Error("This wallet is incompatible with this version of the vault-client.")); return callback(new Error('This wallet is incompatible with this version of the vault-client.'));
} }
if (!authInfo.pakdf) { if (!authInfo.pakdf) {
return fn(new Error("No settings for PAKDF in auth packet.")); return callback(new Error('No settings for PAKDF in auth packet.'));
} }
if (!authInfo.exists) { if (!authInfo.exists) {
return fn(new Error("User does not exist.")); return callback(new Error('User does not exist.'));
} }
if ("string" !== typeof authInfo.blobvault) { if (typeof authInfo.blobvault !== 'string') {
return fn(new Error("No blobvault specified in the authinfo.")); return callback(new Error('No blobvault specified in the authinfo.'));
} }
callback(null, authInfo);
});
};
function deriveLoginKeys(authInfo, callback) {
//derive login keys //derive login keys
crypt.derive(authInfo.pakdf, 'login', username.toLowerCase(), password, function(err, keys) { crypt.derive(authInfo.pakdf, 'login', username.toLowerCase(), password, function(err, keys) {
if (err) return fn(err); if (err) {
callback(err);
} else {
callback(null, authInfo, keys);
}
});
};
function getBlob(authInfo, keys, callback) {
blobClient.get(authInfo.blobvault, keys.id, keys.crypt, function(err, blob) { blobClient.get(authInfo.blobvault, keys.id, keys.crypt, function(err, blob) {
if (err) return fn(err); if (err) {
return callback(err);
}
self.infos[keys.id] = authInfo; //save for relogin //save for relogin
self.infos[keys.id] = authInfo;
fn (null, { callback(null, {
blob: blob, blob: blob,
username: authInfo.username, username: authInfo.username,
verified: authInfo.emailVerified verified: authInfo.emailVerified
}); });
}); });
});
});
}; };
var steps = [
getAuthInfo,
deriveLoginKeys,
getBlob
];
async.waterfall(steps, callback);
};
/** /**
* Retreive and decrypt blob using a blob url, id and crypt derived previously. * Retreive and decrypt blob using a blob url, id and crypt derived previously.
*
* @param {string} url - Blob vault url * @param {string} url - Blob vault url
* @param {string} id - Blob id from previously retreived blob * @param {string} id - Blob id from previously retreived blob
* @param {string} key - Blob decryption key * @param {string} key - Blob decryption key
* @param {function} fn - Callback function * @param {function} fn - Callback function
*/ */
VaultClient.prototype.relogin = function(url, id, key, fn) {
VaultClient.prototype.relogin = function(url, id, key, callback) {
//use the url from previously retrieved authInfo, if necessary //use the url from previously retrieved authInfo, if necessary
if (!url && this.infos[id]) url = this.infos[id].blobvault; if (!url && this.infos[id]) {
url = this.infos[id].blobvault;
}
if (!url) return fn(new Error("Blob vault URL is required")); if (!url) {
return callback(new Error('Blob vault URL is required'));
}
blobClient.get(url, id, key, function(err, blob) { blobClient.get(url, id, key, function(err, blob) {
if (err) return fn(err); if (err) {
callback(err);
fn (null, { } else {
blob : blob, callback (null, { blob: blob });
}); }
}); });
}; };
/** /**
* Decrypt the secret key using a username and password * Decrypt the secret key using a username and password
*
* @param {string} username * @param {string} username
* @param {string} password * @param {string} password
* @param {string} encryptSecret * @param {string} encryptSecret
* @param {function} fn - Callback function * @param {function} fn - Callback function
*/ */
VaultClient.prototype.unlock = function(username, password, encryptSecret, fn) {
VaultClient.prototype.unlock = function(username, password, encryptSecret, callback) {
var self = this; var self = this;
self.authInfo.get(self.domain, username, function(err, authInfo){ function deriveUnlockKey(authInfo, callback) {
if (err) return fn(err);
//derive unlock key //derive unlock key
crypt.derive(authInfo.pakdf, 'unlock', username.toLowerCase(), password, function(err, keys) { crypt.derive(authInfo.pakdf, 'unlock', username.toLowerCase(), password, function(err, keys) {
if (err) return fn(err); if (err) {
return callback(err);
}
fn(null, { callback(null, {
keys: keys, keys: keys,
secret: crypt.decrypt(keys.unlock, encryptSecret) secret: crypt.decrypt(keys.unlock, encryptSecret)
}); });
}); });
});
}; };
self.authInfo.get(self.domain, username, function(err, authInfo) {
if (err) {
callback(err);
} else {
deriveUnlockKey(authInfo, callback);
}
});
};
/** /**
* Retrieve the decrypted blob and secret key in one step using * Retrieve the decrypted blob and secret key in one step using
* the username and password * the username and password
*
* @param {string} username * @param {string} username
* @param {string} password * @param {string} password
* @param {function} fn - Callback function * @param {function} fn - Callback function
*/ */
VaultClient.prototype.loginAndUnlock = function(username, password, fn) {
VaultClient.prototype.loginAndUnlock = function(username, password, callback) {
var self = this; var self = this;
this.login(username, password, function(err, resp){ function deriveUnlockKey(authInfo, blob, callback) {
if (err) return fn(err);
if (!resp.blob || !resp.blob.encrypted_secret)
return fn(new Error("Unable to retrieve blob and secret."));
if (!resp.blob.id || !resp.blob.key)
return fn(new Error("Unable to retrieve keys."));
//get authInfo via id - would have been saved from login
var authInfo = self.infos[resp.blob.id];
if (!authInfo) return fn(new Error("Unable to find authInfo"));
//derive unlock key //derive unlock key
crypt.derive(authInfo.pakdf, 'unlock', username.toLowerCase(), password, function(err, keys) { crypt.derive(authInfo.pakdf, 'unlock', username.toLowerCase(), password, function(err, keys) {
if (err) return fn(err); if (err) {
return callback(err);
}
fn(null, { callback(null, {
blob : resp.blob, blob: blob,
unlock: keys.unlock, unlock: keys.unlock,
secret : crypt.decrypt(keys.unlock, resp.blob.encrypted_secret), secret: crypt.decrypt(keys.unlock, blob.encrypted_secret),
username: authInfo.username, username: authInfo.username,
verified: authInfo.emailVerified verified: authInfo.emailVerified
}); });
}); });
});
}; };
this.login(username, password, function(err, resp) {
if (err) {
return callback(err);
}
if (!resp.blob || !resp.blob.encrypted_secret) {
return callback(new Error('Unable to retrieve blob and secret.'));
}
if (!resp.blob.id || !resp.blob.key) {
return callback(new Error('Unable to retrieve keys.'));
}
//get authInfo via id - would have been saved from login
var authInfo = self.infos[resp.blob.id];
if (!authInfo) {
return callback(new Error('Unable to find authInfo'));
}
deriveUnlockKey(authInfo, resp.blob, callback);
});
};
/** /**
* Check blobvault for existance of username * Check blobvault for existance of username
*
* @param {string} username * @param {string} username
* @param {function} fn - Callback function * @param {function} fn - Callback function
*/ */
VaultClient.prototype.exists = function (username, fn) {
VaultClient.prototype.exists = function(username, callback) {
this.authInfo.get(this.domain, username.toLowerCase(), function(err, authInfo) { this.authInfo.get(this.domain, username.toLowerCase(), function(err, authInfo) {
if (err) return fn(err); if (err) {
return fn(null, !!authInfo.exists); callback(err);
} else {
callback(null, !!authInfo.exists);
}
}); });
}; };
/**
/*
* Verify an email address for an existing user * Verify an email address for an existing user
*
* @param {string} username * @param {string} username
* @param {string} token - Verification token * @param {string} token - Verification token
* @param {function} fn - Callback function * @param {function} fn - Callback function
*/ */
VaultClient.prototype.verify = function (username, token, fn) {
VaultClient.prototype.verify = function(username, token, callback) {
var self = this;
this.authInfo.get(this.domain, username.toLowerCase(), function(err, authInfo) { this.authInfo.get(this.domain, username.toLowerCase(), function(err, authInfo) {
if (err) return fn(err); if (err) {
return callback(err);
if ("string" !== typeof authInfo.blobvault) {
return fn(new Error("No blobvault specified in the authinfo."));
} }
blobClient.verify(authInfo.blobvault, username.toLowerCase(), token, fn); if (typeof authInfo.blobvault !== 'string') {
return callback(new Error('No blobvault specified in the authinfo.'));
}
blobClient.verify(authInfo.blobvault, username.toLowerCase(), token, callback);
}); });
}; };
/**
/*
* Register a new user and save to the blob vault * Register a new user and save to the blob vault
* *
* @param {object} options * @param {object} options
@@ -233,53 +275,77 @@ VaultClient.prototype.verify = function (username, token, fn) {
* @param {object} options.oldUserBlob //optional * @param {object} options.oldUserBlob //optional
* @param {function} fn * @param {function} fn
*/ */
VaultClient.prototype.register = function(options, fn) { VaultClient.prototype.register = function(options, fn) {
var self = this, var self = this;
username = this.normalizeUsername(options.username), var username = String(options.username).trim();
password = this.normalizePassword(options.password); var password = String(options.password).trim();
function getAuthInfo(callback) {
self.authInfo.get(self.domain, username, function(err, authInfo) { self.authInfo.get(self.domain, username, function(err, authInfo) {
if (err) {
if (err) return fn(err); return callback(err);
if ("string" !== typeof authInfo.blobvault) {
return fn(new Error("No blobvault specified in the authinfo."));
} }
if (typeof authInfo.blobvault !== 'string') {
return callback(new Error('No blobvault specified in the authinfo.'));
}
if (!authInfo.pakdf) { if (!authInfo.pakdf) {
return fn(new Error("No settings for PAKDF in auth packet.")); return callback(new Error('No settings for PAKDF in auth packet.'));
} }
//derive login keys callback(null, authInfo);
crypt.derive(authInfo.pakdf, 'login', username.toLowerCase(), password, function(err, loginKeys){ });
if (err) return fn(err); };
//derive unlock key function deriveKeys(authInfo, callback) {
crypt.derive(authInfo.pakdf, 'unlock', username.toLowerCase(), password, function(err, unlockKeys){ // derive unlock and login keys
if (err) return fn(err); var keys = { };
function deriveKey(keyType, callback) {
crypt.derive(authInfo.pakdf, keyType, username.toLowerCase(), password, function(err, key) {
if (err) {
callback(err);
} else {
keys[keyType] = key;
callback();
}
});
};
async.eachSeries([ 'login', 'unlock' ], deriveKey, function(err) {
if (err) {
callback(err);
} else {
callback(null, authInfo, keys);
}
});
};
function create(authInfo, keys) {
var params = { var params = {
'url' : authInfo.blobvault, url: authInfo.blobvault,
'id' : loginKeys.id, id: keys.loginKeys.id,
'crypt' : loginKeys.crypt, crypt: keys.loginKeys.crypt,
'unlock' : unlockKeys.unlock, unlock: keys.unlockKeys.unlock,
'username' : username, username: username,
'email' : options.email, email: options.email,
'masterkey' : options.masterkey || crypt.createMaster(), masterkey: options.masterkey || crypt.createMaster(),
'activateLink' : options.activateLink, activateLink: options.activateLink,
'oldUserBlob' : options.oldUserBlob oldUserBlob: options.oldUserBlob
}; };
blobClient.create(params, function(err, blob) { blobClient.create(params, function(err, blob) {
if (err) return fn(err); if (err) {
fn(null, blob, loginKeys, authInfo.username); callback(err);
}); } else {
}); callback(null, blob, loginKeys, authInfo.username);
}); }
}); });
}; };
async.waterfall([ getAuthInfo, deriveKeys ], create);
};
module.exports.VaultClient = VaultClient; exports.VaultClient = VaultClient;

View File

@@ -184,14 +184,12 @@ describe('Message', function(){
account: 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz' account: 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz'
}; };
Remote.prototype.addServer = function(){}; //Remote.prototype.addServer = function(){};
var test_remote = new Remote({}); var test_remote = new Remote();
assert.throws(function(){ assert.throws(function(){
Message.verifyHashSignature(data); Message.verifyHashSignature(data);
}, /(?=.*callback\ function).*/); }, /(?=.*callback\ function).*/);
}); });
it('should respond with an error if the hash is missing or invalid', function(done){ it('should respond with an error if the hash is missing or invalid', function(done){
@@ -202,8 +200,8 @@ describe('Message', function(){
account: 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz' account: 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz'
}; };
Remote.prototype.addServer = function(){}; //Remote.prototype.addServer = function(){};
var test_remote = new Remote({}); var test_remote = new Remote();
test_remote.state = 'online'; test_remote.state = 'online';
Message.verifyHashSignature(data, test_remote, function(err, valid){ Message.verifyHashSignature(data, test_remote, function(err, valid){
@@ -221,8 +219,8 @@ describe('Message', function(){
signature: 'AAAAHOUJQzG/7BO82fGNt1TNE+GGVXKuQQ0N2nTO+iJETE69PiHnaAkkOzovM177OosxbKjpt3KvwuJflgUB2YGvgjk=' signature: 'AAAAHOUJQzG/7BO82fGNt1TNE+GGVXKuQQ0N2nTO+iJETE69PiHnaAkkOzovM177OosxbKjpt3KvwuJflgUB2YGvgjk='
}; };
Remote.prototype.addServer = function(){}; //Remote.prototype.addServer = function(){};
var test_remote = new Remote({}); var test_remote = new Remote();
test_remote.state = 'online'; test_remote.state = 'online';
Message.verifyHashSignature(data, test_remote, function(err, valid){ Message.verifyHashSignature(data, test_remote, function(err, valid){
@@ -240,8 +238,8 @@ describe('Message', function(){
account: 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz' account: 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz'
}; };
Remote.prototype.addServer = function(){}; //Remote.prototype.addServer = function(){};
var test_remote = new Remote({}); var test_remote = new Remote();
test_remote.state = 'online'; test_remote.state = 'online';
Message.verifyHashSignature(data, test_remote, function(err, valid){ Message.verifyHashSignature(data, test_remote, function(err, valid){
@@ -260,8 +258,8 @@ describe('Message', function(){
signature: 'AAAAHMIPCQGLgdnpX1Ccv1wHb56H4NggxIM6U08Qkb9mUjN2Vn9pZ3CHvq1yWLBi6NqpW+7kedLnmfu4VG2+y43p4Xs=' signature: 'AAAAHMIPCQGLgdnpX1Ccv1wHb56H4NggxIM6U08Qkb9mUjN2Vn9pZ3CHvq1yWLBi6NqpW+7kedLnmfu4VG2+y43p4Xs='
}; };
Remote.prototype.addServer = function(){}; //Remote.prototype.addServer = function(){};
var test_remote = new Remote({}); var test_remote = new Remote();
test_remote.state = 'online'; test_remote.state = 'online';
test_remote.request_account_info = function(account, callback) { test_remote.request_account_info = function(account, callback) {
if (account === data.account) { if (account === data.account) {
@@ -296,8 +294,8 @@ describe('Message', function(){
signature: 'AAAAG+dB/rAjZ5m8eQ/opcqQOJsFbKxOu9jq9KrOAlNO4OdcBDXyCBlkZqS9Xr8oZI2uh0boVsgYOS3pOLJz+Dh3Otk=' signature: 'AAAAG+dB/rAjZ5m8eQ/opcqQOJsFbKxOu9jq9KrOAlNO4OdcBDXyCBlkZqS9Xr8oZI2uh0boVsgYOS3pOLJz+Dh3Otk='
}; };
Remote.prototype.addServer = function(){}; //Remote.prototype.addServer = function(){};
var test_remote = new Remote({}); var test_remote = new Remote();
test_remote.state = 'online'; test_remote.state = 'online';
test_remote.request_account_info = function(account, callback) { test_remote.request_account_info = function(account, callback) {
if (account === data.account) { if (account === data.account) {

View File

@@ -5,15 +5,15 @@ var Remote = utils.load_module('remote').Remote;
var Server = utils.load_module('server').Server; var Server = utils.load_module('server').Server;
var Request = utils.load_module('request').Request; var Request = utils.load_module('request').Request;
var options, spy, mock, stub, remote, callback; var options, spy, mock, stub, remote, callback, database, tx;
describe('Remote', function () { describe('Remote', function () {
describe('initialing a remote with options', function () {
beforeEach(function () { beforeEach(function () {
options = { options = {
trace : true, trace : true,
trusted: true, trusted: true,
local_signing: true, local_signing: true,
servers: [ servers: [
{ host: 's-west.ripple.com', port: 443, secure: true }, { host: 's-west.ripple.com', port: 443, secure: true },
{ host: 's-east.ripple.com', port: 443, secure: true } { host: 's-east.ripple.com', port: 443, secure: true }
@@ -32,13 +32,155 @@ describe('Remote', function () {
}; };
}) })
it('should add a server for each specified', function (done) {
var remote = new Remote(options); describe('remote server initialization - url object', function() {
it('should construct url', function (done) {
var remote = new Remote({
servers: [ { host: 's-west.ripple.com', port: 443, secure: true } ],
});
assert(Array.isArray(remote._servers));
assert(remote._servers[0] instanceof Server);
assert.strictEqual(remote._servers[0]._url, 'wss://s-west.ripple.com:443');
done(); done();
}) })
}) });
describe('functions that return request objects', function () { describe('remote server initialization - url object - no secure property', function() {
it('should construct url', function (done) {
var remote = new Remote({
servers: [ { host: 's-west.ripple.com', port: 443 } ]
});
assert(Array.isArray(remote._servers));
assert(remote._servers[0] instanceof Server);
assert.strictEqual(remote._servers[0]._url, 'wss://s-west.ripple.com:443');
done();
})
});
describe('remote server initialization - url object - secure: false', function() {
it('should construct url', function (done) {
var remote = new Remote({
servers: [ { host: 's-west.ripple.com', port: 443, secure: false } ]
});
assert(Array.isArray(remote._servers));
assert(remote._servers[0] instanceof Server);
assert.strictEqual(remote._servers[0]._url, 'ws://s-west.ripple.com:443');
done();
})
});
describe('remote server initialization - url object - string port', function() {
it('should construct url', function (done) {
var remote = new Remote({
servers: [ { host: 's-west.ripple.com', port: '443', secure: true } ]
});
assert(Array.isArray(remote._servers));
assert(remote._servers[0] instanceof Server);
assert.strictEqual(remote._servers[0]._url, 'wss://s-west.ripple.com:443');
done();
})
});
describe('remote server initialization - url object - invalid host', function() {
it('should construct url', function (done) {
assert.throws(
function() {
var remote = new Remote({
servers: [ { host: '+', port: 443, secure: true } ]
});
}, Error);
done();
})
});
describe('remote server initialization - url object - invalid port', function() {
it('should construct url', function (done) {
assert.throws(
function() {
var remote = new Remote({
servers: [ { host: 's-west.ripple.com', port: null, secure: true } ]
});
}, TypeError);
done();
})
});
describe('remote server initialization - url object - port out of range', function() {
it('should construct url', function (done) {
assert.throws(
function() {
var remote = new Remote({
servers: [ { host: 's-west.ripple.com', port: 65537, secure: true } ]
});
}, Error);
done();
})
});
describe('remote server initialization - url string', function() {
it('should construct url', function (done) {
var remote = new Remote({
servers: [ 'wss://s-west.ripple.com:443' ]
});
assert(Array.isArray(remote._servers));
assert(remote._servers[0] instanceof Server);
assert.strictEqual(remote._servers[0]._url, 'wss://s-west.ripple.com:443');
done();
})
});
describe('remote server initialization - url string - ws://', function() {
it('should construct url', function (done) {
var remote = new Remote({
servers: [ 'ws://s-west.ripple.com:443' ]
});
assert(Array.isArray(remote._servers));
assert(remote._servers[0] instanceof Server);
assert.strictEqual(remote._servers[0]._url, 'ws://s-west.ripple.com:443');
done();
})
});
describe('remote server initialization - url string - invalid host', function() {
it('should construct url', function (done) {
assert.throws(
function() {
var remote = new Remote({
servers: [ 'ws://+:443' ]
});
}, Error
);
done();
})
});
describe('remote server initialization - url string - invalid port', function() {
it('should construct url', function (done) {
assert.throws(
function() {
var remote = new Remote({
servers: [ 'ws://s-west.ripple.com:null' ]
});
}, Error
);
done();
})
});
describe('remote server initialization - url string - port out of range', function() {
it('should construct url', function (done) {
assert.throws(
function() {
var remote = new Remote({
servers: [ 'ws://s-west.ripple.com:65537:' ]
});
}, Error
);
done();
})
});
describe('request constructors', function () {
beforeEach(function () { beforeEach(function () {
callback = function () {} callback = function () {}
remote = new Remote(options); remote = new Remote(options);
@@ -100,4 +242,91 @@ describe('Remote', function () {
}); });
}); });
}) })
describe('create remote and get pending transactions', function() {
before(function() {
tx = [{
tx_json: {
Account : "r4qLSAzv4LZ9TLsR7diphGwKnSEAMQTSjS",
Amount : {
currency : "LTC",
issuer : "r4qLSAzv4LZ9TLsR7diphGwKnSEAMQTSjS",
value : "9.985"
},
Destination : "r4qLSAzv4LZ9TLsR7diphGwKnSEAMQTSjS",
Fee : "15",
Flags : 0,
Paths : [
[
{
account : "rMwjYedjc7qqtKYVLiAccJSmCwih4LnE2q",
currency : "USD",
issuer : "rMwjYedjc7qqtKYVLiAccJSmCwih4LnE2q",
type : 49,
type_hex : "0000000000000031"
},
{
currency : "LTC",
issuer : "rfYv1TXnwgDDK4WQNbFALykYuEBnrR4pDX",
type : 48,
type_hex : "0000000000000030"
},
{
account : "rfYv1TXnwgDDK4WQNbFALykYuEBnrR4pDX",
currency : "LTC",
issuer : "rfYv1TXnwgDDK4WQNbFALykYuEBnrR4pDX",
type : 49,
type_hex : "0000000000000031"
}
]
],
SendMax : {
currency : "USD",
issuer : "r4qLSAzv4LZ9TLsR7diphGwKnSEAMQTSjS",
value : "30.30993068"
},
Sequence : 415,
SigningPubKey : "02854B06CE8F3E65323F89260E9E19B33DA3E01B30EA4CA172612DE77973FAC58A",
TransactionType : "Payment",
TxnSignature : "304602210096C2F385530587DE573936CA51CB86B801A28F777C944E268212BE7341440B7F022100EBF0508A9145A56CDA7FAF314DF3BBE51C6EE450BA7E74D88516891A3608644E"
},
clientID: '48631',
state: 'pending',
submitIndex: 1,
submittedIDs: ["304602210096C2F385530587DE573936CA51CB86B801A28F777C944E268212BE7341440B7F022100EBF0508A9145A56CDA7FAF314DF3BBE51C6EE450BA7E74D88516891A3608644E"],
secret: 'mysecret'
}];
database = {
getPendingTransactions: function(callback) {
callback(null, tx);
}
}
})
it('should set transaction members correct ', function(done) {
remote = new Remote(options);
remote.storage = database;
remote.transaction = function() {
return {
clientID: function(id) {
if (typeof id === 'string') {
this._clientID = id;
}
return this;
},
submit: function() {
assert.deepEqual(this._clientID, tx[0].clientID);
assert.deepEqual(this.submittedIDs,[tx[0].tx_json.TxnSignature]);
assert.equal(this.submitIndex, tx[0].submitIndex);
assert.equal(this.secret, tx[0].secret);
done();
},
parseJson: function(json) {}
}
}
remote.getPendingTransactions();
})
})
}) })

View File

@@ -1,44 +1,106 @@
var assert = require('assert'), var assert = require('assert');
RippleTxt = require('../src/js/ripple/rippletxt').RippleTxt, var RippleTxt = require('../src/js/ripple/rippletxt').RippleTxt;
AuthInfo = require('../src/js/ripple/authinfo').AuthInfo, var AuthInfo = require('../src/js/ripple/authinfo').AuthInfo;
VaultClient = require('../src/js/ripple/vaultclient').VaultClient, var VaultClient = require('../src/js/ripple/vaultclient').VaultClient;
Blob = require('../src/js/ripple/blob').BlobClient.Blob, var Blob = require('../src/js/ripple/blob').Blob;
UInt256 = require('../src/js/ripple/uint256').UInt256; var UInt256 = require('../src/js/ripple/uint256').UInt256;
var exampleData = { var exampleData = {
id : "ef203d3e76552c0592384f909e6f61f1d1f02f61f07643ce015d8b0c9710dd2f", id: 'ef203d3e76552c0592384f909e6f61f1d1f02f61f07643ce015d8b0c9710dd2f',
crypt : "f0cc91a7c1091682c245cd8e13c246cc150b2cf98b17dd6ef092019c99dc9d82", crypt: 'f0cc91a7c1091682c245cd8e13c246cc150b2cf98b17dd6ef092019c99dc9d82',
unlock : "3e15fe3218a9c664835a6f585582e14480112110ddbe50e5028d05fc5bd9b5f4", unlock: '3e15fe3218a9c664835a6f585582e14480112110ddbe50e5028d05fc5bd9b5f4',
blobURL : "https://id.staging.ripple.com", blobURL: 'https://id.staging.ripple.com',
username : "exampleUser", username: 'exampleUser',
password : "pass word", password: 'pass word',
domain : "staging.ripple.com", domain: 'staging.ripple.com',
encrypted_secret : "APYqtqvjJk/J324rx2BGGzUiQ3mtmMMhMsbrUmgxb00W2aFVQzCC2mqd58Z17gzeUUcjtjAm" encrypted_secret: 'APYqtqvjJk/J324rx2BGGzUiQ3mtmMMhMsbrUmgxb00W2aFVQzCC2mqd58Z17gzeUUcjtjAm'
}; };
var rippleTxtRes = {
address: [ 'r3kmLJN5D28dHuH8vZNUZpMC43pEHpaocV' ],
validation_public_key: [ 'n9KPnVLn7ewVzHvn218DcEYsnWLzKerTDwhpofhk4Ym1RUq4TeGw' ],
domain: [ 'ripple.com' ],
ips: [
'23.21.167.100 51235',
'23.23.201.55 51235',
'107.21.116.214 51235'
],
validators: [
'n9KPnVLn7ewVzHvn218DcEYsnWLzKerTDwhpofhk4Ym1RUq4TeGw',
'n9LFzWuhKNvXStHAuemfRKFVECLApowncMAM5chSCL9R5ECHGN4V',
'n94rSdgTyBNGvYg8pZXGuNt59Y5bGAZGxbxyvjDaqD9ceRAgD85P',
'redstem.com'
],
authinfo_url: [
'https://id.staging.ripple.com/v1/authinfo'
]
};
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"; //must be set for self signed certs var authInfoRes = {
body: {
version: 3,
blobvault: 'https://id.staging.ripple.com',
pakdf: {
modulus: 'c7f1bc1dfb1be82d244aef01228c1409c1988943ca9e21431f1669b4aa3864c9f37f3d51b2b4ba1ab9e80f59d267fda1521e88b05117993175e004543c6e3611242f24432ce8efa3b81f0ff660b4f91c5d52f2511a6f38181a7bf9abeef72db056508bbb4eeb5f65f161dd2d5b439655d2ae7081fcc62fdcb281520911d96700c85cdaf12e7d1f15b55ade867240722425198d4ce39019550c4c8a921fc231d3e94297688c2d77cd68ee8fdeda38b7f9a274701fef23b4eaa6c1a9c15b2d77f37634930386fc20ec291be95aed9956801e1c76601b09c413ad915ff03bfdc0b6b233686ae59e8caf11750b509ab4e57ee09202239baee3d6e392d1640185e1cd',
alpha: '7283d19e784f48a96062271a5fa6e2c3addf14e6ezf78a4bb61364856d580f13552008d7b9e3b60ebd9555e9f6c7778ec69f976757d206134e54d61ba9d588a7e37a77cf48060522478352d76db000366ef669a1b1ca93c5e3e05bc344afa1e8ccb15d3343da94180dccf590c2c32408c3f3f176c8885e95d988f1565ee9b80c12f72503ab49917792f907bbb9037487b0afed967fefc9ab090164597fcd391c43fab33029b38e66ff4af96cbf6d90a01b891f856ddd3d94e9c9b307fe01e1353a8c30edd5a94a0ebba5fe7161569000ad3b0d3568872d52b6fbdfce987a687e4b346ea702e8986b03b6b1b85536c813e46052a31ed64ec490d3ba38029544aa',
url: 'https://auth1.ripple.com/api/sign',
exponent: '010001',
host: 'auth1.ripple.com'
},
exists: true,
username: 'exampleUser',
address: 'raVUps4RghLYkVBcpMaRbVKRTTzhesPXd',
emailVerified: true,
reserved: false
},
};
//must be set for self signed certs
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
describe('Ripple Txt', function() { describe('Ripple Txt', function() {
it('should get the context of a ripple.txt file from a given domain', function (done){ it('should get the content of a ripple.txt file from a given domain', function(done) {
var rt = new RippleTxt(); var rt = new RippleTxt();
rt.get(exampleData.domain, function (err, resp){
assert.ifError(err); var requestedUrls = [ ];
assert.equal(typeof resp, 'object');
var testUrls = RippleTxt.urlTemplates.map(function(template) {
return template.replace('{{domain}}', 'localhost');
});
rt.request = function(url, callback) {
requestedUrls.push(url);
callback(new Error());
};
rt.get('localhost', function(err, resp) {
assert(err instanceof Error);
assert.strictEqual(resp, void(0));
assert.deepEqual(requestedUrls, testUrls);
done(); done();
}); });
}); });
}); });
describe('AuthInfo', function() { describe('AuthInfo', function() {
it('should get auth info', function(done) {
var auth = new AuthInfo(); var auth = new AuthInfo();
it ('should get authinfo given a domain and username', function (done){ auth._getRippleTxt = function(domain, callback) {
auth.get(exampleData.domain, exampleData.user, function (err, resp){ assert.strictEqual(domain, 'staging.ripple.com');
callback(null, rippleTxtRes);
};
auth._getUser = function(url, callback) {
assert.strictEqual(url, 'https://id.staging.ripple.com/v1/authinfo?domain=staging.ripple.com&username=exampleUser');
callback(null, authInfoRes);
};
auth.get(exampleData.domain, exampleData.username, function(err, resp) {
assert.ifError(err); assert.ifError(err);
assert.equal(typeof resp, 'object'); Object.keys(authInfoRes.body).forEach(function(prop) {
assert(resp.hasOwnProperty(prop));
});
done(); done();
}); });
}); });
@@ -63,9 +125,8 @@ describe('VaultClient', function () {
it('should determine if a username exists on the domain', function(done) { it('should determine if a username exists on the domain', function(done) {
this.timeout(10000); this.timeout(10000);
client.exists(exampleData.username, function(err, resp) { client.exists(exampleData.username, function(err, resp) {
assert.ifError(err); assert.ifError(err);
assert.equal(typeof resp, 'boolean'); assert.strictEqual(typeof resp, 'boolean');
done(); done();
}); });
}); });
@@ -75,7 +136,6 @@ describe('VaultClient', function () {
it('with username and password should retrive the blob, crypt key, and id', function(done) { it('with username and password should retrive the blob, crypt key, and id', function(done) {
this.timeout(10000); this.timeout(10000);
client.login(exampleData.username, exampleData.password, function(err, resp) { client.login(exampleData.username, exampleData.password, function(err, resp) {
assert.ifError(err); assert.ifError(err);
assert.equal(typeof resp, 'object'); assert.equal(typeof resp, 'object');
assert(resp.blob instanceof Blob); assert(resp.blob instanceof Blob);
@@ -90,12 +150,10 @@ describe('VaultClient', function () {
}); });
}); });
describe('#relogin', function() { describe('#relogin', function() {
it('should retrieve the decrypted blob with blob vault url, id, and crypt key', function(done) { it('should retrieve the decrypted blob with blob vault url, id, and crypt key', function(done) {
this.timeout(10000); this.timeout(10000);
client.relogin(exampleData.blobURL, exampleData.id, exampleData.crypt, function(err, resp) { client.relogin(exampleData.blobURL, exampleData.id, exampleData.crypt, function(err, resp) {
assert.ifError(err); assert.ifError(err);
assert.equal(typeof resp, 'object'); assert.equal(typeof resp, 'object');
assert(resp.blob instanceof Blob); assert(resp.blob instanceof Blob);
@@ -108,7 +166,6 @@ describe('VaultClient', function () {
it('should access the wallet secret using encryption secret, username and password', function (done) { it('should access the wallet secret using encryption secret, username and password', function (done) {
this.timeout(10000); this.timeout(10000);
client.unlock(exampleData.username, exampleData.password, exampleData.encrypted_secret, function(err, resp) { client.unlock(exampleData.username, exampleData.password, exampleData.encrypted_secret, function(err, resp) {
assert.ifError(err); assert.ifError(err);
assert.equal(typeof resp, 'object'); assert.equal(typeof resp, 'object');
assert.equal(typeof resp.keys, 'object'); assert.equal(typeof resp.keys, 'object');
@@ -123,7 +180,6 @@ describe('VaultClient', function () {
it('should get the decrypted blob and decrypted secret given name and password', function (done) { it('should get the decrypted blob and decrypted secret given name and password', function (done) {
this.timeout(10000); this.timeout(10000);
client.loginAndUnlock(exampleData.username, exampleData.password, function(err, resp) { client.loginAndUnlock(exampleData.username, exampleData.password, function(err, resp) {
assert.ifError(err); assert.ifError(err);
assert.equal(typeof resp, 'object'); assert.equal(typeof resp, 'object');
assert(resp.blob instanceof Blob); assert(resp.blob instanceof Blob);
@@ -147,16 +203,14 @@ describe('Blob', function () {
var vaultClient; var vaultClient;
vaultClient = new VaultClient({ domain: exampleData.domain }); vaultClient = new VaultClient({ domain: exampleData.domain });
vaultClient.login(exampleData.username, exampleData.password, function(err,resp) { vaultClient.login(exampleData.username, exampleData.password, function(err,resp) {
assert.ifError(err); assert.ifError(err);
var blob = resp.blob; var blob = resp.blob;
describe('#set', function() { describe('#set', function() {
it('should set a new property in the blob', function(done) { it('should set a new property in the blob', function(done) {
this.timeout(10000); this.timeout(10000)
blob.extend('/testObject', {
blob.extend("/testObject", {
foo: [], foo: [],
}, function(err, resp){ }, function(err, resp){
assert.ifError(err); assert.ifError(err);
@@ -168,10 +222,9 @@ describe('Blob', function () {
describe('#extend', function() { describe('#extend', function() {
it('should extend an object in the blob', function(done) { it('should extend an object in the blob', function(done) {
this.timeout(10000); this.timeout(10000)
blob.extend('/testObject', {
blob.extend("/testObject", { foobar: 'baz',
foobar : "baz",
}, function(err, resp){ }, function(err, resp){
assert.ifError(err); assert.ifError(err);
assert.equal(resp.result, 'success'); assert.equal(resp.result, 'success');
@@ -182,9 +235,8 @@ describe('Blob', function () {
describe('#unset', function() { describe('#unset', function() {
it('should remove a property from the blob', function(done) { it('should remove a property from the blob', function(done) {
this.timeout(10000); this.timeout(10000)
blob.unset('/testObject', function(err, resp){
blob.unset("/testObject", function(err, resp){
assert.ifError(err); assert.ifError(err);
assert.equal(resp.result, 'success'); assert.equal(resp.result, 'success');
done(); done();
@@ -194,11 +246,10 @@ describe('Blob', function () {
describe('#unshift', function() { describe('#unshift', function() {
it('should prepend an item to an array in the blob', function(done) { it('should prepend an item to an array in the blob', function(done) {
this.timeout(10000); this.timeout(10000)
blob.unshift('/testArray', {
blob.unshift("/testArray", { name: 'bob',
name : "bob", address: '1234'
address : "1234"
}, function(err, resp){ }, function(err, resp){
assert.ifError(err); assert.ifError(err);
assert.equal(resp.result, 'success'); assert.equal(resp.result, 'success');
@@ -209,9 +260,9 @@ describe('Blob', function () {
describe('#filter', function() { describe('#filter', function() {
it('should find a specific entity in an array and apply subcommands to it', function(done) { it('should find a specific entity in an array and apply subcommands to it', function(done) {
this.timeout(10000); this.timeout(10000)
blob.filter('/testArray', 'name', 'bob', 'extend', '', {description:"Alice"}, function(err, resp){ blob.filter('/testArray', 'name', 'bob', 'extend', '', {description:'Alice'}, function(err, resp){
assert.ifError(err); assert.ifError(err);
assert.equal(resp.result, 'success'); assert.equal(resp.result, 'success');
done(); done();
@@ -221,12 +272,10 @@ describe('Blob', function () {
describe('#consolidate', function() { describe('#consolidate', function() {
it('should consolidate and save changes to the blob', function(done) { it('should consolidate and save changes to the blob', function(done) {
this.timeout(10000); this.timeout(10000)
blob.unset('/testArray', function(err, resp){ blob.unset('/testArray', function(err, resp){
assert.ifError(err); assert.ifError(err);
assert.equal(resp.result, 'success'); assert.equal(resp.result, 'success');
blob.consolidate(function(err, resp){ blob.consolidate(function(err, resp){
assert.ifError(err); assert.ifError(err);
assert.equal(resp.result, 'success'); assert.equal(resp.result, 'success');
@@ -236,7 +285,6 @@ describe('Blob', function () {
}); });
}); });
/********* Identity tests ***********/ /********* Identity tests ***********/
describe('#identity_set', function () { describe('#identity_set', function () {
it('should set an identity property', function (done) { it('should set an identity property', function (done) {
@@ -289,4 +337,3 @@ describe('Blob', function () {
}); });
}); });
}); });