[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
/coverage.html
/coverage
# Ignore IntelliJ files
.idea

View File

@@ -593,8 +593,23 @@ Amount.prototype.invert = function() {
* 25.2 XRP => 25200000/XRP
* USD 100.40 => 100.4/USD/?
* 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) {
opts = opts || {};

View File

@@ -1,36 +1,60 @@
var RippleTxt = require('./rippletxt').RippleTxt;
var request = require('superagent');
var async = require('async');
var superagent = require('superagent');
var RippleTxt = require('./rippletxt').RippleTxt;
function AuthInfo () {
function AuthInfo() {
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
*
* @param {string} domain - Domain which hosts the user's info
* @param {string} username - Username who's info we are retreiving
* @param {function} fn - Callback function
*/
AuthInfo.prototype.get = function (domain, username, fn) {
AuthInfo.prototype.get = function(domain, username, callback) {
var self = this;
self.rippleTxt.get(domain, function(err, txt){
if (err) return fn(err);
function getRippleTxt(callback) {
self._getRippleTxt(domain, function(err, txt) {
if (err) {
return callback(err);
}
processTxt(txt);
});
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;
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;
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);
callback(null, url);
});
}
};
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);
};
module.exports.AuthInfo = AuthInfo;
exports.AuthInfo = AuthInfo;

View File

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

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

View File

@@ -7,17 +7,19 @@ var log = require('./log').internal.sub('server');
/**
* @constructor Server
*
* @param {Remote} Reference to a Remote object
* @param {Object} Options
*
* host: String
* port: String or Number
* secure: Boolean
* @param {String} host
* @param {Number|String} port
* @param [Boolean] securec
*/
function Server(remote, opts) {
EventEmitter.call(this);
var self = this;
if (typeof opts === 'string') {
var parsedUrl = url.parse(opts);
opts = {
@@ -31,16 +33,6 @@ function Server(remote, opts) {
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)) {
throw new TypeError('Server port must be a number');
}
@@ -50,7 +42,7 @@ function Server(remote, opts) {
}
if (typeof opts.secure !== 'boolean') {
opts.secure = false;
opts.secure = true;
}
// 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');
}
var self = this;
this._remote = remote;
this._opts = opts;
this._ws = void(0);
this._remote = remote;
this._opts = opts;
this._host = opts.host;
this._port = opts.port;
this._secure = opts.secure;
this._ws = void(0);
this._connected = false;
this._shouldConnect = false;
this._state = 'offline';
this._id = 0;
this._retry = 0;
this._requests = { };
this._load_base = 256;
this._load_factor = 256;
this._id = 0;
this._retry = 0;
this._requests = { };
this._load_base = 256;
this._load_factor = 256;
this._fee = 10;
this._fee_ref = 10;
this._fee_base = 10;
this._reserve_base = void(0);
this._reserve_inc = void(0);
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);
});
};
this.on('response_subscribe', function(message) {
this.on('response_subscribe', onSubscribeResponse);
function onSubscribeResponse(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() {
self._activityInterval = setInterval(checkServerActivity, 1000);
var interval = self._checkActivity.bind(self);
self._activityInterval = setInterval(interval, 1000);
};
this.on('disconnect', function onDisconnect() {
@@ -119,8 +115,18 @@ function Server(remote, opts) {
//self.once('ledger_closed', setActivityInterval);
});
this.once('ledger_closed', function() {
//setActiviyInterval();
//this.once('ledger_closed', setActivityInterval);
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'
];
/**
* 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
*
@@ -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.
* Incompatible with ripple-lib client build
@@ -186,22 +279,6 @@ Server.prototype._remoteAddress = function() {
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
*
@@ -274,7 +351,6 @@ Server.prototype.connect = function() {
};
ws.onopen = function onOpen() {
// If we are no longer the active socket, simply ignore any event
if (ws === self._ws) {
self.emit('socket_open');
// Subscribe to events
@@ -283,7 +359,6 @@ Server.prototype.connect = function() {
};
ws.onerror = function onError(e) {
// If we are no longer the active socket, simply ignore any event
if (ws === self._ws) {
self.emit('socket_error');
@@ -309,9 +384,7 @@ Server.prototype.connect = function() {
}
};
// Failure to open.
ws.onclose = function onClose() {
// If we are no longer the active socket, simply ignore any event
if (ws === self._ws) {
if (self._remote.trace) {
log.info('onclose:', self._opts.url, ws.readyState);
@@ -333,12 +406,16 @@ Server.prototype._retryConnect = function() {
this._retry += 1;
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)
? (1000) // Then, for 1 minute: once per second
// Then, for 1 minute: once per second
? (1000)
: (this._retry < 40 + 60 + 60)
? (10 * 1000) // Then, for 10 minutes: once every 10 seconds
: (30 * 1000); // Then: once every 30 seconds
// Then, for 10 minutes: once every 10 seconds
? (10 * 1000)
// Then: once every 30 seconds
: (30 * 1000);
function connectionRetry() {
if (self._shouldConnect) {
@@ -365,8 +442,10 @@ Server.prototype._handleClose = function() {
this.emit('socket_close');
this._setState('offline');
function noOp() {};
// 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) {
this._retryConnect();
@@ -389,78 +468,125 @@ Server.prototype._handleMessage = function(message) {
}
if (!Server.isValidMessage(message)) {
this.emit('unexpected', message);
return;
}
switch (message.type) {
case 'ledgerClosed':
this._lastLedgerClose = Date.now();
this.emit('ledger_closed', message);
this._handleLedgerClosed(message);
break;
case 'serverStatus':
// This message is only received when online.
// As we are connected, it is the definitive final state.
this._setState(~(Server.onlineStates.indexOf(message.server_status)) ? 'online' : 'offline');
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);
}
}
this._handleServerStatus(message);
break;
case 'response':
// A response to a request.
var request = self._requests[message.id];
delete self._requests[message.id];
if (!request) {
if (this._remote.trace) {
log.info('UNEXPECTED:', self._opts.url, message);
}
return;
}
if (message.status === 'success') {
if (this._remote.trace) {
log.info('response:', self._opts.url, message);
}
request.emit('success', message.result);
[ self, self._remote ].forEach(function(emitter) {
emitter.emit('response_' + request.message.command, message.result, request, message);
});
} else if (message.error) {
if (this._remote.trace) {
log.info('error:', self._opts.url, message);
}
request.emit('error', {
error: 'remoteError',
error_message: 'Remote reported an error.',
remote: message
});
}
this._handleResponse(message);
break;
case 'path_find':
if (this._remote.trace) {
log.info('path_find:', self._opts.url, message);
}
this._handlePathFind(message);
break;
}
};
Server.prototype._handleLedgerClosed = function(message) {
this._lastLedgerIndex = message.ledger_index;
this._lastLedgerClose = Date.now();
this.emit('ledger_closed', message);
};
Server.prototype._handleServerStatus = function(message) {
// This message is only received when online.
// As we are connected, it is the definitive final state.
var isOnline = ~Server.onlineStates.indexOf(message.server_status);
this._setState(isOnline ? 'online' : 'offline');
if (!Server.isLoadStatus(message)) {
return;
}
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.
var request = this._requests[message.id];
delete this._requests[message.id];
if (!request) {
if (this._remote.trace) {
log.info('UNEXPECTED:', this._opts.url, message);
}
return;
}
if (message.status === 'success') {
if (this._remote.trace) {
log.info('response:', this._opts.url, message);
}
var command = request.message.command;
var result = message.result;
var responseEvent = 'response_' + command;
request.emit('success', result);
[ this, this._remote ].forEach(function(emitter) {
emitter.emit(responseEvent, result, request, message);
});
} else if (message.error) {
if (this._remote.trace) {
log.info('error:', this._opts.url, message);
}
var error = {
error: 'remoteError',
error_message: 'Remote reported an error.',
remote: message
};
request.emit('error', error);
}
};
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;
}
};
/**
* Check that received message from rippled is valid
*
@@ -484,27 +610,6 @@ Server.isLoadStatus = function(message) {
&& (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
*
@@ -544,6 +649,7 @@ Server.prototype._request = function(request) {
request.server = this;
request.message.id = this._id;
request.time = Date.now();
this._requests[request.message.id] = request;
@@ -564,8 +670,8 @@ Server.prototype._request = function(request) {
Server.prototype._isConnected = function(request) {
var isSubscribeRequest = request
&& request.message.command === 'subscribe'
&& this._ws.readyState === 1;
&& request.message.command === 'subscribe'
&& this._ws.readyState === 1;
return this._connected || (this._ws && isSubscribeRequest);
};

View File

@@ -1,227 +1,269 @@
var AuthInfo = require('./authinfo').AuthInfo;
var async = require('async');
var blobClient = require('./blob').BlobClient;
var AuthInfo = require('./authinfo').AuthInfo;
var crypt = require('./crypt').Crypt;
function VaultClient(opts) {
if (!opts) opts = {};
else if (typeof opts === "string") opts = {domain:opts};
if (!opts) {
opts = { };
}
this.domain = opts.domain || 'ripple.com';
this.authInfo = new AuthInfo();
this.infos = {};
}
if (typeof opts === 'string') {
opts = { domain: opts };
}
/**
* 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;
this.domain = opts.domain || 'ripple.com';
this.authInfo = new AuthInfo();
this.infos = { };
};
/**
* 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
* @param {string} address - Account address to query
* @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
if (!url) return fn(new Error("Blob vault URL is required"));
blobClient.getRippleName(url, address, fn);
if (!url) {
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
*
* @param {string} username
* @param {string} password
* @param {function} fn - Callback function
*/
VaultClient.prototype.login = function(username, password, fn) {
VaultClient.prototype.login = function(username, password, callback) {
var self = this;
self.authInfo.get(self.domain, username, function(err, authInfo){
if (err) return fn(err);
function getAuthInfo(callback) {
self.authInfo.get(self.domain, username, function(err, authInfo) {
if (err) {
return callback(err);
}
if (authInfo.version !== 3) {
return fn(new Error("This wallet is incompatible with this version of the vault-client."));
}
if (authInfo.version !== 3) {
return callback(new Error('This wallet is incompatible with this version of the vault-client.'));
}
if (!authInfo.pakdf) {
return fn(new Error("No settings for PAKDF in auth packet."));
}
if (!authInfo.pakdf) {
return callback(new Error('No settings for PAKDF in auth packet.'));
}
if (!authInfo.exists) {
return fn(new Error("User does not exist."));
}
if (!authInfo.exists) {
return callback(new Error('User does not exist.'));
}
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.'));
}
callback(null, authInfo);
});
};
function deriveLoginKeys(authInfo, callback) {
//derive login keys
crypt.derive(authInfo.pakdf, 'login', username.toLowerCase(), password, function(err, keys){
if (err) return fn(err);
crypt.derive(authInfo.pakdf, 'login', username.toLowerCase(), password, function(err, keys) {
if (err) {
callback(err);
} else {
callback(null, authInfo, keys);
}
});
};
blobClient.get(authInfo.blobvault, keys.id, keys.crypt, function (err, blob) {
if (err) return fn(err);
function getBlob(authInfo, keys, callback) {
blobClient.get(authInfo.blobvault, keys.id, keys.crypt, function(err, blob) {
if (err) {
return callback(err);
}
self.infos[keys.id] = authInfo; //save for relogin
//save for relogin
self.infos[keys.id] = authInfo;
fn (null, {
blob : blob,
username : authInfo.username,
verified : authInfo.emailVerified
});
callback(null, {
blob: blob,
username: authInfo.username,
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.
*
* @param {string} url - Blob vault url
* @param {string} id - Blob id from previously retreived blob
* @param {string} key - Blob decryption key
* @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
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) {
if (err) return fn(err);
fn (null, {
blob : blob,
});
blobClient.get(url, id, key, function(err, blob) {
if (err) {
callback(err);
} else {
callback (null, { blob: blob });
}
});
};
/**
* Decrypt the secret key using a username and password
*
* @param {string} username
* @param {string} password
* @param {string} encryptSecret
* @param {function} fn - Callback function
*/
VaultClient.prototype.unlock = function(username, password, encryptSecret, fn) {
VaultClient.prototype.unlock = function(username, password, encryptSecret, callback) {
var self = this;
self.authInfo.get(self.domain, username, function(err, authInfo){
if (err) return fn(err);
function deriveUnlockKey(authInfo, callback) {
//derive unlock key
crypt.derive(authInfo.pakdf, 'unlock', username.toLowerCase(), password, function(err, keys){
if (err) return fn(err);
crypt.derive(authInfo.pakdf, 'unlock', username.toLowerCase(), password, function(err, keys) {
if (err) {
return callback(err);
}
fn(null, {
keys : keys,
secret : crypt.decrypt(keys.unlock, encryptSecret)
callback(null, {
keys: keys,
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
* the username and password
*
* @param {string} username
* @param {string} password
* @param {function} fn - Callback function
*/
VaultClient.prototype.loginAndUnlock = function(username, password, fn) {
VaultClient.prototype.loginAndUnlock = function(username, password, callback) {
var self = this;
this.login(username, password, function(err, resp){
if (err) return fn(err);
function deriveUnlockKey(authInfo, blob, callback) {
//derive unlock key
crypt.derive(authInfo.pakdf, 'unlock', username.toLowerCase(), password, function(err, keys) {
if (err) {
return callback(err);
}
if (!resp.blob || !resp.blob.encrypted_secret)
return fn(new Error("Unable to retrieve blob and secret."));
callback(null, {
blob: blob,
unlock: keys.unlock,
secret: crypt.decrypt(keys.unlock, blob.encrypted_secret),
username: authInfo.username,
verified: authInfo.emailVerified
});
});
};
if (!resp.blob.id || !resp.blob.key)
return fn(new Error("Unable to retrieve keys."));
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 fn(new Error("Unable to find authInfo"));
//derive unlock key
crypt.derive(authInfo.pakdf, 'unlock', username.toLowerCase(), password, function(err, keys){
if (err) return fn(err);
if (!authInfo) {
return callback(new Error('Unable to find authInfo'));
}
fn(null, {
blob : resp.blob,
unlock : keys.unlock,
secret : crypt.decrypt(keys.unlock, resp.blob.encrypted_secret),
username : authInfo.username,
verified : authInfo.emailVerified
});
});
deriveUnlockKey(authInfo, resp.blob, callback);
});
};
/**
* Check blobvault for existance of username
*
* @param {string} username
* @param {function} fn - Callback function
*/
VaultClient.prototype.exists = function (username, fn) {
this.authInfo.get(this.domain, username.toLowerCase(), function(err, authInfo){
if (err) return fn(err);
return fn(null, !!authInfo.exists);
VaultClient.prototype.exists = function(username, callback) {
this.authInfo.get(this.domain, username.toLowerCase(), function(err, authInfo) {
if (err) {
callback(err);
} else {
callback(null, !!authInfo.exists);
}
});
};
/*
/**
* Verify an email address for an existing user
*
* @param {string} username
* @param {string} token - Verification token
* @param {function} fn - Callback function
*/
VaultClient.prototype.verify = function (username, token, fn) {
this.authInfo.get(this.domain, username.toLowerCase(), function (err, authInfo) {
if (err) return fn(err);
VaultClient.prototype.verify = function(username, token, callback) {
var self = this;
if ("string" !== typeof authInfo.blobvault) {
return fn(new Error("No blobvault specified in the authinfo."));
this.authInfo.get(this.domain, username.toLowerCase(), function(err, authInfo) {
if (err) {
return callback(err);
}
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
*
* @param {object} options
@@ -233,53 +275,77 @@ VaultClient.prototype.verify = function (username, token, fn) {
* @param {object} options.oldUserBlob //optional
* @param {function} fn
*/
VaultClient.prototype.register = function (options, fn) {
var self = this,
username = this.normalizeUsername(options.username),
password = this.normalizePassword(options.password);
VaultClient.prototype.register = function(options, fn) {
var self = this;
var username = String(options.username).trim();
var password = String(options.password).trim();
self.authInfo.get(self.domain, username, function(err, authInfo){
function getAuthInfo(callback) {
self.authInfo.get(self.domain, username, function(err, authInfo) {
if (err) {
return callback(err);
}
if (err) return fn(err);
if (typeof authInfo.blobvault !== 'string') {
return callback(new Error('No blobvault specified in the authinfo.'));
}
if ("string" !== typeof authInfo.blobvault) {
return fn(new Error("No blobvault specified in the authinfo."));
}
if (!authInfo.pakdf) {
return callback(new Error('No settings for PAKDF in auth packet.'));
}
if (!authInfo.pakdf) {
return fn(new Error("No settings for PAKDF in auth packet."));
}
//derive login keys
crypt.derive(authInfo.pakdf, 'login', username.toLowerCase(), password, function(err, loginKeys){
if (err) return fn(err);
//derive unlock key
crypt.derive(authInfo.pakdf, 'unlock', username.toLowerCase(), password, function(err, unlockKeys){
if (err) return fn(err);
var params = {
'url' : authInfo.blobvault,
'id' : loginKeys.id,
'crypt' : loginKeys.crypt,
'unlock' : unlockKeys.unlock,
'username' : username,
'email' : options.email,
'masterkey' : options.masterkey || crypt.createMaster(),
'activateLink' : options.activateLink,
'oldUserBlob' : options.oldUserBlob
};
blobClient.create(params, function(err, blob){
if (err) return fn(err);
fn(null, blob, loginKeys, authInfo.username);
});
});
callback(null, authInfo);
});
});
};
function deriveKeys(authInfo, callback) {
// derive unlock and login keys
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 = {
url: authInfo.blobvault,
id: keys.loginKeys.id,
crypt: keys.loginKeys.crypt,
unlock: keys.unlockKeys.unlock,
username: username,
email: options.email,
masterkey: options.masterkey || crypt.createMaster(),
activateLink: options.activateLink,
oldUserBlob: options.oldUserBlob
};
blobClient.create(params, function(err, blob) {
if (err) {
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'
};
Remote.prototype.addServer = function(){};
var test_remote = new Remote({});
//Remote.prototype.addServer = function(){};
var test_remote = new Remote();
assert.throws(function(){
Message.verifyHashSignature(data);
}, /(?=.*callback\ function).*/);
});
it('should respond with an error if the hash is missing or invalid', function(done){
@@ -202,8 +200,8 @@ describe('Message', function(){
account: 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz'
};
Remote.prototype.addServer = function(){};
var test_remote = new Remote({});
//Remote.prototype.addServer = function(){};
var test_remote = new Remote();
test_remote.state = 'online';
Message.verifyHashSignature(data, test_remote, function(err, valid){
@@ -221,8 +219,8 @@ describe('Message', function(){
signature: 'AAAAHOUJQzG/7BO82fGNt1TNE+GGVXKuQQ0N2nTO+iJETE69PiHnaAkkOzovM177OosxbKjpt3KvwuJflgUB2YGvgjk='
};
Remote.prototype.addServer = function(){};
var test_remote = new Remote({});
//Remote.prototype.addServer = function(){};
var test_remote = new Remote();
test_remote.state = 'online';
Message.verifyHashSignature(data, test_remote, function(err, valid){
@@ -240,8 +238,8 @@ describe('Message', function(){
account: 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz'
};
Remote.prototype.addServer = function(){};
var test_remote = new Remote({});
//Remote.prototype.addServer = function(){};
var test_remote = new Remote();
test_remote.state = 'online';
Message.verifyHashSignature(data, test_remote, function(err, valid){
@@ -260,8 +258,8 @@ describe('Message', function(){
signature: 'AAAAHMIPCQGLgdnpX1Ccv1wHb56H4NggxIM6U08Qkb9mUjN2Vn9pZ3CHvq1yWLBi6NqpW+7kedLnmfu4VG2+y43p4Xs='
};
Remote.prototype.addServer = function(){};
var test_remote = new Remote({});
//Remote.prototype.addServer = function(){};
var test_remote = new Remote();
test_remote.state = 'online';
test_remote.request_account_info = function(account, callback) {
if (account === data.account) {
@@ -296,8 +294,8 @@ describe('Message', function(){
signature: 'AAAAG+dB/rAjZ5m8eQ/opcqQOJsFbKxOu9jq9KrOAlNO4OdcBDXyCBlkZqS9Xr8oZI2uh0boVsgYOS3pOLJz+Dh3Otk='
};
Remote.prototype.addServer = function(){};
var test_remote = new Remote({});
//Remote.prototype.addServer = function(){};
var test_remote = new Remote();
test_remote.state = 'online';
test_remote.request_account_info = function(account, callback) {
if (account === data.account) {

View File

@@ -5,40 +5,182 @@ var Remote = utils.load_module('remote').Remote;
var Server = utils.load_module('server').Server;
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('initialing a remote with options', function () {
beforeEach(function () {
options = {
trace : true,
trusted: true,
local_signing: true,
servers: [
{ host: 's-west.ripple.com', port: 443, secure: true },
{ host: 's-east.ripple.com', port: 443, secure: true }
],
beforeEach(function () {
options = {
trace : true,
trusted: true,
local_signing: true,
blobvault : 'https://blobvault.payward.com',
persistent_auth : false,
transactions_per_page: 50,
servers: [
{ host: 's-west.ripple.com', port: 443, secure: true },
{ host: 's-east.ripple.com', port: 443, secure: true }
],
bridge: {
out: {
// 'bitcoin': 'localhost:3000'
// 'bitcoin': 'https://www.bitstamp.net/ripple/bridge/out/bitcoin/'
}
},
blobvault : 'https://blobvault.payward.com',
persistent_auth : false,
transactions_per_page: 50,
};
})
it('should add a server for each specified', function (done) {
var remote = new Remote(options);
done();
})
bridge: {
out: {
// 'bitcoin': 'localhost:3000'
// 'bitcoin': 'https://www.bitstamp.net/ripple/bridge/out/bitcoin/'
}
},
};
})
describe('functions that return request objects', function () {
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();
})
});
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 () {
callback = function () {}
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'),
RippleTxt = require('../src/js/ripple/rippletxt').RippleTxt,
AuthInfo = require('../src/js/ripple/authinfo').AuthInfo,
VaultClient = require('../src/js/ripple/vaultclient').VaultClient,
Blob = require('../src/js/ripple/blob').BlobClient.Blob,
UInt256 = require('../src/js/ripple/uint256').UInt256;
var assert = require('assert');
var RippleTxt = require('../src/js/ripple/rippletxt').RippleTxt;
var AuthInfo = require('../src/js/ripple/authinfo').AuthInfo;
var VaultClient = require('../src/js/ripple/vaultclient').VaultClient;
var Blob = require('../src/js/ripple/blob').Blob;
var UInt256 = require('../src/js/ripple/uint256').UInt256;
var exampleData = {
id : "ef203d3e76552c0592384f909e6f61f1d1f02f61f07643ce015d8b0c9710dd2f",
crypt : "f0cc91a7c1091682c245cd8e13c246cc150b2cf98b17dd6ef092019c99dc9d82",
unlock : "3e15fe3218a9c664835a6f585582e14480112110ddbe50e5028d05fc5bd9b5f4",
blobURL : "https://id.staging.ripple.com",
username : "exampleUser",
password : "pass word",
domain : "staging.ripple.com",
encrypted_secret : "APYqtqvjJk/J324rx2BGGzUiQ3mtmMMhMsbrUmgxb00W2aFVQzCC2mqd58Z17gzeUUcjtjAm"
id: 'ef203d3e76552c0592384f909e6f61f1d1f02f61f07643ce015d8b0c9710dd2f',
crypt: 'f0cc91a7c1091682c245cd8e13c246cc150b2cf98b17dd6ef092019c99dc9d82',
unlock: '3e15fe3218a9c664835a6f585582e14480112110ddbe50e5028d05fc5bd9b5f4',
blobURL: 'https://id.staging.ripple.com',
username: 'exampleUser',
password: 'pass word',
domain: 'staging.ripple.com',
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 () {
it('should get the context of a ripple.txt file from a given domain', function (done){
describe('Ripple Txt', function() {
it('should get the content of a ripple.txt file from a given domain', function(done) {
var rt = new RippleTxt();
rt.get(exampleData.domain, function (err, resp){
assert.ifError(err);
assert.equal(typeof resp, 'object');
var requestedUrls = [ ];
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();
});
});
});
describe('AuthInfo', function() {
it('should get auth info', function(done) {
var auth = new AuthInfo();
describe('AuthInfo', function () {
var auth = new AuthInfo();
auth._getRippleTxt = function(domain, callback) {
assert.strictEqual(domain, 'staging.ripple.com');
callback(null, rippleTxtRes);
};
it ('should get authinfo given a domain and username', function (done){
auth.get(exampleData.domain, exampleData.user, function (err, resp){
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.equal(typeof resp, 'object');
Object.keys(authInfoRes.body).forEach(function(prop) {
assert(resp.hasOwnProperty(prop));
});
done();
});
});
@@ -47,8 +109,8 @@ describe('AuthInfo', function () {
describe('VaultClient', function () {
var client = new VaultClient(exampleData.domain);
describe('#initialization', function () {
it('should be initialized with a domain', function () {
describe('#initialization', function() {
it('should be initialized with a domain', function() {
var client = new VaultClient({ domain: exampleData.domain });
assert.strictEqual(client.domain, exampleData.domain);
});
@@ -59,23 +121,21 @@ describe('VaultClient', function () {
});
});
describe('#exists', function () {
it('should determine if a username exists on the domain', function (done) {
describe('#exists', function() {
it('should determine if a username exists on the domain', function(done) {
this.timeout(10000);
client.exists(exampleData.username, function(err, resp) {
assert.ifError(err);
assert.equal(typeof resp, 'boolean');
assert.strictEqual(typeof resp, 'boolean');
done();
});
});
});
describe('#login', function () {
it('with username and password should retrive the blob, crypt key, and id', function (done) {
describe('#login', function() {
it('with username and password should retrive the blob, crypt key, and id', function(done) {
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.equal(typeof resp, 'object');
assert(resp.blob instanceof Blob);
@@ -90,12 +150,10 @@ describe('VaultClient', function () {
});
});
describe('#relogin', function () {
it('should retrieve the decrypted blob with blob vault url, id, and crypt key', function (done) {
describe('#relogin', function() {
it('should retrieve the decrypted blob with blob vault url, id, and crypt key', function(done) {
this.timeout(10000);
client.relogin(exampleData.blobURL, exampleData.id, exampleData.crypt, function(err, resp) {
assert.ifError(err);
assert.equal(typeof resp, 'object');
assert(resp.blob instanceof Blob);
@@ -107,8 +165,7 @@ describe('VaultClient', function () {
describe('#unlock', function() {
it('should access the wallet secret using encryption secret, username and password', function (done) {
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.equal(typeof resp, 'object');
assert.equal(typeof resp.keys, 'object');
@@ -122,8 +179,7 @@ describe('VaultClient', function () {
describe('#loginAndUnlock', function () {
it('should get the decrypted blob and decrypted secret given name and password', function (done) {
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.equal(typeof resp, 'object');
assert(resp.blob instanceof Blob);
@@ -147,17 +203,15 @@ describe('Blob', function () {
var vaultClient;
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);
var blob = resp.blob;
describe('#set', function () {
it('should set a new property in the blob', function (done) {
this.timeout(10000);
blob.extend("/testObject", {
foo : [],
describe('#set', function() {
it('should set a new property in the blob', function(done) {
this.timeout(10000)
blob.extend('/testObject', {
foo: [],
}, function(err, resp){
assert.ifError(err);
assert.equal(resp.result, 'success');
@@ -166,12 +220,11 @@ describe('Blob', function () {
});
});
describe('#extend', function () {
it('should extend an object in the blob', function (done) {
this.timeout(10000);
blob.extend("/testObject", {
foobar : "baz",
describe('#extend', function() {
it('should extend an object in the blob', function(done) {
this.timeout(10000)
blob.extend('/testObject', {
foobar: 'baz',
}, function(err, resp){
assert.ifError(err);
assert.equal(resp.result, 'success');
@@ -180,11 +233,10 @@ describe('Blob', function () {
});
});
describe('#unset', function () {
it('should remove a property from the blob', function (done) {
this.timeout(10000);
blob.unset("/testObject", function(err, resp){
describe('#unset', function() {
it('should remove a property from the blob', function(done) {
this.timeout(10000)
blob.unset('/testObject', function(err, resp){
assert.ifError(err);
assert.equal(resp.result, 'success');
done();
@@ -192,13 +244,12 @@ describe('Blob', function () {
});
});
describe('#unshift', function () {
it('should prepend an item to an array in the blob', function (done) {
this.timeout(10000);
blob.unshift("/testArray", {
name : "bob",
address : "1234"
describe('#unshift', function() {
it('should prepend an item to an array in the blob', function(done) {
this.timeout(10000)
blob.unshift('/testArray', {
name: 'bob',
address: '1234'
}, function(err, resp){
assert.ifError(err);
assert.equal(resp.result, 'success');
@@ -207,11 +258,11 @@ 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) {
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.equal(resp.result, 'success');
done();
@@ -219,14 +270,12 @@ describe('Blob', function () {
});
});
describe('#consolidate', function () {
it('should consolidate and save changes to the blob', function (done) {
this.timeout(10000);
describe('#consolidate', function() {
it('should consolidate and save changes to the blob', function(done) {
this.timeout(10000)
blob.unset('/testArray', function(err, resp){
assert.ifError(err);
assert.equal(resp.result, 'success');
blob.consolidate(function(err, resp){
assert.ifError(err);
assert.equal(resp.result, 'success');
@@ -236,7 +285,6 @@ describe('Blob', function () {
});
});
/********* Identity tests ***********/
describe('#identity_set', function () {
it('should set an identity property', function (done) {
@@ -289,4 +337,3 @@ describe('Blob', function () {
});
});
});