copay/js/models/Async.js

433 lines
11 KiB
JavaScript
Raw Normal View History

2014-08-07 10:49:29 -07:00
'use strict';
2014-08-18 07:33:00 -07:00
var EventEmitter = require('events').EventEmitter;
2014-08-07 10:49:29 -07:00
var bitcore = require('bitcore');
2014-12-02 06:17:03 -08:00
var log = require('../util/log');
2014-08-07 10:49:29 -07:00
var AuthMessage = bitcore.AuthMessage;
var util = bitcore.util;
2014-08-18 07:33:00 -07:00
var nodeUtil = require('util');
var extend = nodeUtil._extend;
2014-08-07 15:34:48 -07:00
var io = require('socket.io-client');
2014-08-07 11:23:17 -07:00
var preconditions = require('preconditions').singleton();
2014-08-07 10:49:29 -07:00
function Network(opts) {
2014-09-11 10:15:00 -07:00
preconditions.checkArgument(opts);
preconditions.checkArgument(opts.url);
2014-08-07 10:49:29 -07:00
opts = opts || {};
2014-08-18 11:24:58 -07:00
this.maxPeers = opts.maxPeers || 12;
2014-09-11 10:15:00 -07:00
this.url = opts.url;
2014-09-03 12:51:02 -07:00
this.secretNumber = opts.secretNumber;
2014-08-07 10:49:29 -07:00
this.cleanUp();
2014-10-07 14:33:55 -07:00
this.socketOptions = {
reconnection: true,
'force new connection': true,
'secure': this.url.indexOf('https') === 0,
};
2014-10-07 14:33:55 -07:00
if (opts.transports) {
this.socketOptions['transports'] = opts.transports;
}
2014-08-07 10:49:29 -07:00
}
2014-08-18 07:33:00 -07:00
nodeUtil.inherits(Network, EventEmitter);
2014-08-07 10:49:29 -07:00
Network.prototype.cleanUp = function() {
this.started = false;
this.connectedPeers = [];
this.peerId = null;
this.privkey = null;
this.key = null;
this.copayerId = null;
this.allowedCopayerIds = null;
this.isInboundPeerAuth = [];
this.copayerForPeer = {};
this.criticalErr = '';
2014-08-22 08:51:40 -07:00
if (this.socket) {
2014-09-09 18:28:23 -07:00
log.info('Async DISCONNECT');
2014-08-22 08:51:40 -07:00
this.socket.disconnect();
this.socket.removeAllListeners();
2014-08-22 08:51:40 -07:00
this.socket = null;
}
this.removeAllListeners();
2014-08-07 10:49:29 -07:00
};
Network.parent = EventEmitter;
// Array helpers
Network._inArray = function(el, array) {
return array.indexOf(el) > -1;
};
Network._arrayPushOnce = function(el, array) {
var ret = false;
if (!Network._inArray(el, array)) {
array.push(el);
ret = true;
}
return ret;
};
Network._arrayRemove = function(el, array) {
var pos = array.indexOf(el);
if (pos >= 0) array.splice(pos, 1);
return array;
};
Network.prototype.connectedCopayers = function() {
var ret = [];
for (var i in this.connectedPeers) {
var copayerId = this.copayerForPeer[this.connectedPeers[i]];
if (copayerId) ret.push(copayerId);
}
return ret;
};
2014-09-10 13:09:57 -07:00
Network.prototype._sendHello = function(copayerId, secretNumber) {
2014-08-27 13:50:28 -07:00
2014-08-07 10:49:29 -07:00
this.send(copayerId, {
type: 'hello',
copayerId: this.copayerId,
2014-09-10 13:09:57 -07:00
secretNumber: secretNumber
2014-08-07 10:49:29 -07:00
});
};
2014-09-09 11:07:36 -07:00
Network.prototype._sendRejectConnection = function(copayerId) {
this.send(copayerId, {
type: 'rejectConnection',
copayerId: this.copayerId,
});
};
2014-08-07 10:49:29 -07:00
Network.prototype._deletePeer = function(peerId) {
delete this.isInboundPeerAuth[peerId];
delete this.copayerForPeer[peerId];
this.connectedPeers = Network._arrayRemove(peerId, this.connectedPeers);
};
Network.prototype._addCopayer = function(copayerId) {
2014-08-07 10:49:29 -07:00
var peerId = this.peerFromCopayer(copayerId);
this._addCopayerMap(peerId, copayerId);
Network._arrayPushOnce(peerId, this.connectedPeers);
};
Network.prototype._addConnectedCopayer = function(copayerId) {
this._addCopayer(copayerId);
2014-08-07 10:49:29 -07:00
this.emit('connect', copayerId);
};
Network.prototype.getKey = function() {
preconditions.checkState(this.privkey || this.key);
2014-08-07 10:49:29 -07:00
if (!this.key) {
var key = new bitcore.Key();
key.private = new Buffer(this.privkey, 'hex');
key.regenerateSync();
this.key = key;
}
return this.key;
};
//hex version of one's own nonce
Network.prototype.setHexNonce = function(networkNonce) {
if (networkNonce) {
if (networkNonce.length !== 16)
throw new Error('incorrect length of hex nonce');
this.networkNonce = new Buffer(networkNonce, 'hex');
} else
this.iterateNonce();
};
//hex version of copayers' nonces
Network.prototype.setHexNonces = function(networkNonces) {
for (var i in networkNonces) {
if (!this.networkNonces)
this.networkNonces = {};
if (networkNonces[i].length === 16)
this.networkNonces[i] = new Buffer(networkNonces[i], 'hex');
}
};
//for oneself
Network.prototype.getHexNonce = function() {
return this.networkNonce.toString('hex');
};
//for copayers
Network.prototype.getHexNonces = function() {
var networkNoncesHex = [];
for (var i in this.networkNonces) {
networkNoncesHex[i] = this.networkNonces[i].toString('hex');
}
return networkNoncesHex;
};
Network.prototype.iterateNonce = function() {
if (!this.networkNonce || this.networkNonce.length !== 8) {
this.networkNonce = new Buffer(8);
this.networkNonce.fill(0);
}
//the first 4 bytes of a nonce is a unix timestamp in seconds
//the second 4 bytes is just an iterated "sub" nonce
//the whole thing is interpreted as one big endian number
var noncep1 = this.networkNonce.slice(0, 4);
noncep1.writeUInt32BE(Math.floor(Date.now() / 1000), 0);
var noncep2uint = this.networkNonce.slice(4, 8).readUInt32BE(0);
var noncep2 = this.networkNonce.slice(4, 8);
noncep2.writeUInt32BE(noncep2uint + 1, 0);
2014-08-26 11:33:54 -07:00
this.networkNonce = Buffer.concat([noncep1, noncep2], 8);
2014-08-07 10:49:29 -07:00
return this.networkNonce;
};
2014-08-19 11:22:13 -07:00
Network.prototype.decode = function(enc) {
var sender = enc.pubkey;
2014-08-07 10:49:29 -07:00
var key = this.getKey();
2014-08-19 11:22:13 -07:00
var prevnonce = this.networkNonces ? this.networkNonces[sender] : undefined;
var opts = {
prevnonce: prevnonce
};
var decoded = AuthMessage.decode(key, enc, opts);
//if no error thrown in the last step, we can set the copayer's nonce
if (!this.networkNonces)
this.networkNonces = {};
this.networkNonces[sender] = decoded.nonce;
var payload = decoded.payload;
return payload;
};
Network.prototype._onMessage = function(enc) {
2014-08-12 13:41:45 -07:00
var sender = enc.pubkey;
2014-08-07 10:49:29 -07:00
try {
2014-08-19 11:22:13 -07:00
var payload = this.decode(enc);
2014-08-07 10:49:29 -07:00
} catch (e) {
2014-08-12 13:41:45 -07:00
this._deletePeer(sender);
2014-08-07 10:49:29 -07:00
return;
}
2014-08-13 14:27:33 -07:00
2014-10-31 08:27:22 -07:00
if (this.ignoreMessageFromTs && this.ignoreMessageFromTs === enc.ts) {
2014-10-31 14:14:14 -07:00
log.debug('Ignoring trailing message. Ts:', enc.ts);
2014-10-31 08:27:22 -07:00
return;
}
2014-11-23 10:52:39 -08:00
2014-08-07 10:49:29 -07:00
var self = this;
switch (payload.type) {
2014-08-12 13:41:45 -07:00
case 'hello':
2014-09-10 13:09:57 -07:00
if (typeof payload.secretNumber === 'undefined' || payload.secretNumber !== this.secretNumber) {
2014-09-09 11:07:36 -07:00
this._sendRejectConnection(sender);
2014-09-03 12:51:02 -07:00
this._deletePeer(enc.pubkey, 'incorrect secret number');
2014-09-10 13:09:57 -07:00
return;
2014-09-03 12:51:02 -07:00
}
2014-08-19 12:08:10 -07:00
// if we locked allowed copayers, check if it belongs
2014-08-18 12:32:49 -07:00
if (this.allowedCopayerIds && !this.allowedCopayerIds[payload.copayerId]) {
2014-09-09 11:07:36 -07:00
this._sendRejectConnection(sender);
2014-08-18 12:32:49 -07:00
this._deletePeer(sender);
return;
}
2014-08-19 12:08:10 -07:00
//ensure claimed public key is actually the public key of the peer
//e.g., their public key should hash to be their peerId
2014-08-19 12:35:14 -07:00
if (sender !== payload.copayerId) {
2014-09-09 11:07:36 -07:00
this._sendRejectConnection(sender);
2014-08-19 12:08:10 -07:00
this._deletePeer(enc.pubkey, 'incorrect pubkey for peerId');
return;
}
2014-08-12 13:41:45 -07:00
this._addConnectedCopayer(payload.copayerId);
2014-08-07 10:49:29 -07:00
break;
default:
2014-08-26 09:58:06 -07:00
this.emit('data', sender, payload, enc.ts);
2014-08-07 10:49:29 -07:00
}
};
2014-11-29 19:31:17 -08:00
Network.prototype._setupSocketHandlers = function(opts, cb) {
2014-08-07 10:49:29 -07:00
preconditions.checkState(this.socket);
2014-11-23 10:52:39 -08:00
log.debug('setting up connection', opts);
2014-08-07 10:49:29 -07:00
var self = this;
self.socket.on('connect_error', function(m) {
// If socket is not started, destroy it and emit and error
// If it is started, socket.io will try to reconnect.
if (!self.started) {
self.emit('connect_error');
self.cleanUp();
}
});
self.socket.on('subscribed', function(m) {
2014-10-31 08:27:22 -07:00
var fromTs = opts.syncedTimestamp || 0;
// We ask for this message, and then ignore it, only to see if the
// server has erased our old messages.
2014-10-31 08:27:22 -07:00
if (fromTs) {
self.ignoreMessageFromTs = fromTs;
}
log.info('Async: synchronizing from: ', fromTs);
self.socket.emit('sync', fromTs);
self.started = true;
});
self.socket.on('message', function(m) {
2014-08-18 15:19:54 -07:00
// delay execution, to improve error handling
setTimeout(function() {
self._onMessage(m);
}, 1);
});
2014-08-12 13:41:45 -07:00
self.socket.on('error', self._onError.bind(self));
2014-11-29 19:31:17 -08:00
self.socket.on('no_messages', self.emit.bind(self, 'no_messages'));
self.socket.on('no messages', self.emit.bind(self, 'no_messages'));
2014-08-27 13:50:28 -07:00
self.socket.on('connect', function() {
var pubkey = self.getKey().public.toString('hex');
2014-10-07 14:33:55 -07:00
log.debug('Async subscribing to pubkey:', pubkey);
self.socket.emit('subscribe', pubkey);
2014-08-27 14:57:16 -07:00
2014-08-27 13:50:28 -07:00
self.socket.on('disconnect', function() {
self.socket.emit('subscribe', pubkey);
2014-08-27 13:50:28 -07:00
});
if (typeof cb === 'function') cb();
});
2014-08-07 10:49:29 -07:00
};
2014-08-12 13:41:45 -07:00
Network.prototype._onError = function(err) {
2014-09-01 08:04:10 -07:00
log.debug('RECV ERROR: ', err);
log.debug(err.stack);
2014-08-07 15:02:01 -07:00
this.criticalError = err.message;
2014-08-07 10:49:29 -07:00
};
2014-09-10 13:09:57 -07:00
Network.prototype.greet = function(copayerId, secretNumber) {
this._sendHello(copayerId, secretNumber);
2014-08-12 13:41:45 -07:00
var peerId = this.peerFromCopayer(copayerId);
this._addCopayerMap(peerId, copayerId);
};
2014-08-07 10:49:29 -07:00
Network.prototype._addCopayerMap = function(peerId, copayerId) {
if (!this.copayerForPeer[peerId]) {
if (Object.keys(this.copayerForPeer).length < this.maxPeers) {
this.copayerForPeer[peerId] = copayerId;
2014-08-18 11:24:58 -07:00
}
2014-08-07 10:49:29 -07:00
}
};
2014-08-12 13:41:45 -07:00
Network.prototype._setInboundPeerAuth = function(peerId) {
this.isInboundPeerAuth[peerId] = true;
2014-08-07 10:49:29 -07:00
};
Network.prototype.setCopayerId = function(copayerId) {
2014-08-18 15:19:54 -07:00
preconditions.checkState(!this.started, 'network already started: can not change peerId');
2014-08-07 10:49:29 -07:00
this.copayerId = copayerId;
this.copayerIdBuf = new Buffer(copayerId, 'hex');
this.peerId = this.peerFromCopayer(this.copayerId);
this._addCopayerMap(this.peerId, copayerId);
};
// TODO cache this.
Network.prototype.peerFromCopayer = function(hex) {
var SIN = bitcore.SIN;
return new SIN(new Buffer(hex, 'hex')).toString();
};
Network.prototype.start = function(opts, openCallback) {
preconditions.checkArgument(opts);
preconditions.checkArgument(opts.privkey);
preconditions.checkArgument(opts.copayerId);
2014-08-07 10:49:29 -07:00
if (this.started) {
log.debug('Async: Networing already started for this wallet.')
return openCallback();
}
2014-08-07 10:49:29 -07:00
this.privkey = opts.privkey;
this.setCopayerId(opts.copayerId);
2014-08-07 10:49:29 -07:00
this.maxPeers = opts.maxPeers || this.maxPeers;
2014-11-29 19:31:17 -08:00
this.socket = this.createSocket();
this._setupSocketHandlers(opts, openCallback);
2014-08-07 10:49:29 -07:00
};
2014-08-25 14:26:26 -07:00
Network.prototype.createSocket = function() {
2014-10-31 14:14:14 -07:00
log.debug('Async: Connecting to socket:', this.url);
return io.connect(this.url, this.socketOptions);
};
2014-08-07 10:49:29 -07:00
Network.prototype.getOnlinePeerIDs = function() {
return this.connectedPeers;
};
2014-08-18 11:24:58 -07:00
Network.prototype.getCopayerIds = function() {
if (this.allowedCopayerIds) {
return Object.keys(this.allowedCopayerIds);
} else {
var copayerIds = [];
for (var peerId in this.copayerForPeer) {
copayerIds.push(this.copayerForPeer[peerId]);
}
return copayerIds;
}
};
2014-08-19 12:15:06 -07:00
Network.prototype.send = function(dest, payload, cb) {
2014-09-04 13:47:24 -07:00
preconditions.checkState(this.socket);
2014-08-12 13:41:45 -07:00
preconditions.checkArgument(payload);
2014-08-07 10:49:29 -07:00
var self = this;
2014-08-19 12:15:06 -07:00
if (!dest) {
2014-08-19 14:31:40 -07:00
dest = this.getCopayerIds();
2014-08-07 10:49:29 -07:00
payload.isBroadcast = 1;
}
2014-08-28 12:37:34 -07:00
2014-08-19 12:15:06 -07:00
if (typeof dest === 'string')
dest = [dest];
2014-08-07 10:49:29 -07:00
2014-08-19 12:15:06 -07:00
var l = dest.length;
2014-08-07 10:49:29 -07:00
var i = 0;
2014-08-28 15:29:37 -07:00
for (var ii in dest) {
var to = dest[ii];
2014-08-28 15:29:37 -07:00
if (to == this.copayerId)
2014-08-28 14:43:20 -07:00
continue;
2014-09-04 13:47:24 -07:00
var message = this.encode(to, payload);
this.socket.emit('message', message);
}
2014-08-27 13:50:28 -07:00
2014-08-12 13:41:45 -07:00
if (typeof cb === 'function') cb();
2014-08-07 10:49:29 -07:00
};
2014-08-19 12:48:09 -07:00
Network.prototype.encode = function(copayerId, payload, nonce) {
2014-08-19 11:22:13 -07:00
this.iterateNonce();
var opts = {
2014-08-19 12:48:09 -07:00
nonce: nonce || this.networkNonce
2014-08-19 11:22:13 -07:00
};
var copayerIdBuf = new Buffer(copayerId, 'hex');
var message = AuthMessage.encode(copayerIdBuf, this.getKey(), payload, opts);
2014-08-19 12:08:10 -07:00
return message;
2014-08-19 11:22:13 -07:00
};
2014-08-07 10:49:29 -07:00
Network.prototype.isOnline = function() {
return !!this.socket;
};
Network.prototype.lockIncommingConnections = function(allowedCopayerIdsArray) {
this.allowedCopayerIds = {};
for (var i in allowedCopayerIdsArray) {
this.allowedCopayerIds[allowedCopayerIdsArray[i]] = true;
}
};
Network.prototype.setCopayers = function(copayersIdsArray) {
for (var i in copayersIdsArray) {
this._addCopayer(copayersIdsArray[i]);
}
};
2014-08-18 07:33:00 -07:00
module.exports = Network;