├── .gitignore ├── CHANGELOG.md ├── README.md ├── examples ├── Message.js ├── bitcoin-testnet.js ├── bitcoin.js ├── bitmessage.js └── namecoin.js ├── lib └── Peer.js ├── package.json └── test ├── mocha.opts └── peer.test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | npm-debug.log -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 0.1.1 / 2014-01-22 2 | ------------------ 3 | * Add `stateChange` events when Peer state changes. 4 | 5 | 0.1.0 / 2014-01-10 6 | ------------------ 7 | * Initial release -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # P2P Node 2 | Low-level library to handle peer-to-peer traffic on cryptocurrency networks. A raw `socket` object in Node emits `data` events whenever the stream is updated. This library sits on top of a raw socket connection, and instead of emitting `data` events every time the stream updates, it waits and emits `message` events whenever a complete message has arrived. 3 | 4 | It uses the [Bitcoin protocol structure](https://en.bitcoin.it/wiki/Protocol_specification#Message_structure) to parse incoming messages; any stream that's encoded as follows can be parsed: 5 | 6 | * **4 bytes: Uint32:** Magic Bytes: Flag the beginning of a message 7 | * **12 bytes: Char:** ASCII string identifying the message type, null-padded to 12 characters long 8 | * **4 bytes: Uint32:** Payload length 9 | * **4 bytes: Uint32:** Checksum: First four bytes of `sha256(sha256(payload))` 10 | * **variable bytes:** Payload data 11 | 12 | The default Magic Bytes and default Port to connect to are set to the Bitcoin protocol. 13 | 14 | ## Usage 15 | 16 | ```js 17 | var Peer = require('p2p-node').Peer; 18 | 19 | var p = new Peer('remote.node.com'); 20 | p.on('connect', function(d) { 21 | console.log("I'm connected!"); 22 | }); 23 | p.on('message', function(d) { 24 | console.log("I got message "+d.command); 25 | }); 26 | ``` 27 | 28 | `Peer` is an [EventEmitter](http://nodejs.org/api/events.html) with the following events: 29 | 30 | ## Events 31 | 32 | ### `connect` 33 | When the socket connects 34 | 35 | Data object passed to listeners: 36 | 37 | ``` 38 | { 39 | peer: Peer 40 | } 41 | ``` 42 | 43 | ### `error` 44 | If the socket errors out 45 | 46 | Data object passed to listeners: 47 | 48 | ``` 49 | { 50 | peer: Peer, 51 | error: Error object from Socket 52 | } 53 | ``` 54 | 55 | ### `end` 56 | When the socket disconnects 57 | 58 | Data object passed to listeners: 59 | 60 | ``` 61 | { 62 | peer: Peer 63 | } 64 | ``` 65 | 66 | ### `message` 67 | When a complete message has arrived 68 | 69 | Data object passed to listeners: 70 | 71 | ``` 72 | { 73 | peer: Peer, 74 | command: String, 75 | data: Raw payload as binary data 76 | } 77 | ``` 78 | 79 | ### `commandMessage` 80 | An alternate version of the `peerMessage` event; in these events, the command of the message is used as the event name (i.e. command `'foo'` would cause a `fooMessage` event). 81 | 82 | ``` 83 | { 84 | peer: Peer, 85 | data: Raw payload as binary data 86 | } 87 | ``` 88 | 89 | ### `stateChange` 90 | When the peer changes state, this event is emitted, except for the "new" state, which is set upon creation. 91 | 92 | ``` 93 | { 94 | new: String (new state name), 95 | old: String (old state name) 96 | } 97 | ``` 98 | -------------------------------------------------------------------------------- /examples/Message.js: -------------------------------------------------------------------------------- 1 | var sha256 = require('crypto-hashing').sha256; 2 | 3 | var Message = exports.Message = function Message(magic) { 4 | this.buffer = new Buffer(10000); 5 | this.cursor = 0; 6 | this.magicBytes = magic; 7 | }; 8 | Message.prototype.checksum = function checksum() { 9 | return new Buffer(sha256.x2(this.buffer.slice(0, this.cursor), { asBytes:true })); 10 | }; 11 | Message.prototype.raw = function raw() { 12 | var out = new Buffer(this.cursor); 13 | this.buffer.copy(out, 0, 0, this.cursor); 14 | return out; 15 | }; 16 | Message.prototype.pad = function pad(num) { 17 | var data = new Buffer(num); 18 | data.fill(0); 19 | return this.put(data); 20 | }; 21 | Message.prototype.put = function put(data) { 22 | if (typeof data == 'number' && data <= 255) { 23 | this.buffer[this.cursor] = data; 24 | this.cursor += 1; 25 | return this; 26 | } 27 | 28 | data.copy(this.buffer, this.cursor); 29 | this.cursor += data.length; 30 | return this; 31 | }; 32 | Message.prototype.putInt16 = function putInt16(num) { 33 | var data = new Buffer(2); 34 | data.writeUInt16LE(num, 0); 35 | return this.put(data); 36 | }; 37 | Message.prototype.putInt32 = function putInt32(num) { 38 | var data = new Buffer(4); 39 | data.writeUInt32LE(num, 0); 40 | return this.put(data); 41 | }; 42 | Message.prototype.putInt64 = function putInt64(num) { 43 | var data = new Buffer(8); 44 | data.fill(0); 45 | data.writeUInt32LE(num, 0); 46 | return this.put(data); 47 | }; 48 | Message.prototype.putString = function putString(str) { 49 | var data = new Buffer(str.length); 50 | for(var i = 0; i < str.length; i++) { 51 | data[i] = str.charCodeAt(i); 52 | } 53 | return this.put(data); 54 | }; 55 | Message.prototype.putVarInt = function putVarInt(num) { 56 | if (num < 0xfd) { 57 | return this.put(num); 58 | } else if (num <= 0xffff) { 59 | return this.put(0xfd).putInt16(num); 60 | } else if (num <= 0xffffffff) { 61 | return this.put(0xfe).putInt32(num); 62 | } else { 63 | return this.put(0xff).putInt64(num); 64 | } 65 | }; 66 | Message.prototype.putVarString = function putVarString(str) { 67 | return this.putVarInt(str.length).putString(str); 68 | } -------------------------------------------------------------------------------- /examples/bitcoin-testnet.js: -------------------------------------------------------------------------------- 1 | var Peer = require('../lib/Peer').Peer; 2 | var Message = require('./Message').Message; 3 | 4 | var dns = require('dns'); 5 | 6 | 7 | var shutdown = false; 8 | var p = false; 9 | var findPeer = function findPeer(pool) { 10 | if (pool.length == 0) { 11 | console.log('No more potential peers...'); 12 | return; 13 | } 14 | 15 | console.log('Finding new peer from pool of '+pool.length+' potential peers'); 16 | p = new Peer(pool.shift()); 17 | 18 | var connectTimeout = setTimeout(function() { // Give them a few seconds to respond, otherwise close the connection automatically 19 | console.log('Peer never connected; hanging up'); 20 | p.destroy(); 21 | }, 5*1000); 22 | connectTimeout.unref(); 23 | var verackTimeout = false; 24 | 25 | p.on('connect', function(d) { 26 | console.log('connect'); 27 | clearTimeout(connectTimeout); 28 | 29 | // Send VERSION message 30 | var m = new Message(p.magicBytes, true); 31 | m.putInt32(70000); // version 32 | m.putInt64(1); // services 33 | m.putInt64(Math.round(new Date().getTime()/1000)); // timestamp 34 | m.pad(26); // addr_me 35 | m.pad(26); // addr_you 36 | m.putInt64(0x1234); // nonce 37 | m.putVarString('/Cryptocoinjs:0.1/'); 38 | m.putInt32(10); // start_height 39 | 40 | //console.log(m.raw().toString('hex')); 41 | console.log('Sending VERSION message'); 42 | verackTimeout = setTimeout(function() { 43 | console.log('No VERACK received; disconnect'); 44 | p.destroy(); 45 | }, 10000); 46 | verackTimeout.unref(); 47 | p.once('verackMessage', function() { 48 | console.log('VERACK received; this peer is active now'); 49 | clearTimeout(verackTimeout); 50 | }); 51 | p.send('version', m.raw()); 52 | }); 53 | p.on('end', function(d) { 54 | console.log('end'); 55 | }); 56 | p.on('error', function(d) { 57 | console.log('error', d); 58 | d.peer.destroy(); 59 | }); 60 | p.on('close', function(d) { 61 | console.log('close', d); 62 | if (shutdown === false) { 63 | console.log('Connection closed, trying next...'); 64 | setImmediate(function() { 65 | clearTimeout(connectTimeout); 66 | clearTimeout(verackTimeout); 67 | findPeer(pool); 68 | }); 69 | } 70 | }); 71 | p.on('message', function(d) { 72 | console.log('message', d.command, d.data.toString('hex')); 73 | }); 74 | 75 | console.log('Attempting connection to '+p.getUUID()); 76 | p.connect(18333, 0x0709110B); 77 | }; 78 | 79 | process.once('SIGINT', function() { 80 | shutdown = true; 81 | console.log('Got SIGINT; closing...'); 82 | var watchdog = setTimeout(function() { 83 | console.log('Peer didn\'t close gracefully; force-closing'); 84 | p.destroy(); 85 | }, 10000); 86 | watchdog.unref(); 87 | p.once('close', function() { 88 | clearTimeout(watchdog); 89 | }); 90 | p.disconnect(); 91 | process.once('SIGINT', function() { 92 | console.log('Hard-kill'); 93 | process.exit(0); 94 | }); 95 | }); 96 | 97 | // Find a single IP address 98 | dns.resolve4('testnet-seed.bitcoin.petertodd.org', function(err, addrs) { 99 | if (err) { 100 | console.log(err); 101 | return; 102 | } 103 | findPeer(addrs); 104 | }); -------------------------------------------------------------------------------- /examples/bitcoin.js: -------------------------------------------------------------------------------- 1 | var Peer = require('../lib/Peer').Peer; 2 | var Message = require('./Message').Message; 3 | 4 | var dns = require('dns'); 5 | 6 | 7 | var shutdown = false; 8 | var p = false; 9 | var findPeer = function findPeer(pool) { 10 | if (pool.length == 0) { 11 | console.log('No more potential peers...'); 12 | return; 13 | } 14 | 15 | console.log('Finding new peer from pool of '+pool.length+' potential peers'); 16 | p = new Peer(pool.shift()); 17 | 18 | var connectTimeout = setTimeout(function() { // Give them a few seconds to respond, otherwise close the connection automatically 19 | console.log('Peer never connected; hanging up'); 20 | p.destroy(); 21 | }, 5*1000); 22 | connectTimeout.unref(); 23 | var verackTimeout = false; 24 | 25 | p.on('connect', function(d) { 26 | console.log('connect'); 27 | clearTimeout(connectTimeout); 28 | 29 | // Send VERSION message 30 | var m = new Message(p.magicBytes, true); 31 | m.putInt32(70000); // version 32 | m.putInt64(1); // services 33 | m.putInt64(Math.round(new Date().getTime()/1000)); // timestamp 34 | m.pad(26); // addr_me 35 | m.pad(26); // addr_you 36 | m.putInt64(0x1234); // nonce 37 | m.putVarString('/Cryptocoinjs:0.1/'); 38 | m.putInt32(10); // start_height 39 | 40 | //console.log(m.raw().toString('hex')); 41 | console.log('Sending VERSION message'); 42 | verackTimeout = setTimeout(function() { 43 | console.log('No VERACK received; disconnect'); 44 | p.destroy(); 45 | }, 10000); 46 | verackTimeout.unref(); 47 | p.once('verackMessage', function() { 48 | console.log('VERACK received; this peer is active now'); 49 | clearTimeout(verackTimeout); 50 | }); 51 | p.send('version', m.raw()); 52 | }); 53 | p.on('end', function(d) { 54 | console.log('end'); 55 | }); 56 | p.on('error', function(d) { 57 | console.log('error', d); 58 | d.peer.destroy(); 59 | }); 60 | p.on('close', function(d) { 61 | console.log('close', d); 62 | if (shutdown === false) { 63 | console.log('Connection closed, trying next...'); 64 | setImmediate(function() { 65 | clearTimeout(connectTimeout); 66 | clearTimeout(verackTimeout); 67 | findPeer(pool); 68 | }); 69 | } 70 | }); 71 | p.on('message', function(d) { 72 | console.log('message', d.command, d.data.toString('hex')); 73 | }); 74 | 75 | console.log('Attempting connection to '+p.getUUID()); 76 | p.connect(); 77 | }; 78 | 79 | process.once('SIGINT', function() { 80 | shutdown = true; 81 | console.log('Got SIGINT; closing...'); 82 | var watchdog = setTimeout(function() { 83 | console.log('Peer didn\'t close gracefully; force-closing'); 84 | p.destroy(); 85 | }, 10000); 86 | watchdog.unref(); 87 | p.once('close', function() { 88 | clearTimeout(watchdog); 89 | }); 90 | p.disconnect(); 91 | process.once('SIGINT', function() { 92 | console.log('Hard-kill'); 93 | process.exit(0); 94 | }); 95 | }); 96 | 97 | // Find a single IP address 98 | dns.resolve4('dnsseed.bluematt.me', function(err, addrs) { 99 | if (err) { 100 | console.log(err); 101 | return; 102 | } 103 | findPeer(addrs); 104 | }); -------------------------------------------------------------------------------- /examples/bitmessage.js: -------------------------------------------------------------------------------- 1 | var Peer = require('../lib/Peer').Peer; 2 | var Message = require('./Message').Message; 3 | var dns = require('dns'); 4 | var crypto = require('crypto'); 5 | 6 | // Resolve DNS seeds 7 | var ipSeeds = []; 8 | var waiting = 2; 9 | dns.resolve4('bootstrap8080.bitmessage.org', function(err, addrs) { 10 | if (err) { 11 | console.log(err); 12 | return; 13 | } 14 | for (var i = 0; i < addrs.length; i++) { 15 | ipSeeds.push(addrs[i]+':8080'); 16 | } 17 | if (--waiting <= 0) { 18 | console.log(ipSeeds); 19 | tryLaunch(ipSeeds); 20 | } 21 | }); 22 | dns.resolve4('bootstrap8444.bitmessage.org', function(err, addrs) { 23 | if (err) { 24 | console.log(err); 25 | return; 26 | } 27 | for (var i = 0; i < addrs.length; i++) { 28 | ipSeeds.push(addrs[i]+':8444'); 29 | } 30 | if (--waiting <= 0) { 31 | console.log(ipSeeds); 32 | tryLaunch(ipSeeds); 33 | } 34 | }); 35 | 36 | var p = false; 37 | var shutdown = false; 38 | var hangupTimer = false; 39 | var verackTimer = false; 40 | 41 | function tryLaunch(hosts) { 42 | if (hosts.length == 0) { 43 | console.log('out of potential connections...'); 44 | return; 45 | } 46 | console.log('Finding new peer from pool of '+hosts.length+' potential peers'); 47 | p = new Peer(hosts.pop(), 8444, 0xE9BEB4D9); 48 | console.log('connecting to '+p.getUUID()); 49 | 50 | var hangupTimer = setTimeout(function() { // Give them a few seconds to respond, otherwise close the connection automatically 51 | console.log('Peer never connected; hanging up'); 52 | p.destroy(); 53 | }, 5*1000); 54 | hangupTimer.unref(); 55 | 56 | // Override message checksum to be SHA512 57 | p.messageChecksum = function(msg) { 58 | var sha512 = crypto.createHash('sha512'); 59 | sha512.update(msg); 60 | return sha512.digest().slice(0,4); 61 | } 62 | 63 | p.on('connect', function(d) { 64 | console.log('connect'); 65 | clearTimeout(hangupTimer); 66 | 67 | // Send VERSION message 68 | var m = new Message(p.magicBytes, true); 69 | m.putInt32(2); // version 70 | m.putInt64(1); // services 71 | m.putInt64(Math.round(new Date().getTime()/1000)); // timestamp 72 | m.pad(26); // addr_me 73 | m.pad(26); // addr_you 74 | m.putInt64(0x1234); // nonce 75 | m.putVarString('/Cryptocoinjs:0.1/'); 76 | m.putVarInt(1); // number of streams 77 | m.putVarInt(1); // Stream I care about 78 | 79 | //console.log(m.raw().toString('hex')); 80 | console.log('Sending VERSION message'); 81 | verackTimeout = setTimeout(function() { 82 | console.log('No VERACK received; disconnect'); 83 | p.destroy(); 84 | }, 10000); 85 | verackTimeout.unref(); 86 | p.once('verackMessage', function() { 87 | console.log('VERACK received; this peer is active now'); 88 | clearTimeout(verackTimeout); 89 | }); 90 | p.send('version', m.raw()); 91 | }); 92 | p.on('end', function(d) { 93 | console.log('end'); 94 | }); 95 | p.on('error', function(d) { 96 | console.log('error', d.error); 97 | if (hosts.length > 0) tryLaunch(hosts); 98 | }); 99 | p.on('close', function(d) { 100 | console.log('close', d); 101 | if (shutdown === false) { 102 | console.log('Connection closed, trying next...'); 103 | setImmediate(function() { 104 | clearTimeout(connectTimeout); 105 | clearTimeout(verackTimeout); 106 | findPeer(pool); 107 | }); 108 | } 109 | }); 110 | 111 | p.on('message', function(d) { 112 | console.log('message', d.command, d.data.toString('hex')); 113 | }); 114 | 115 | p.connect(); 116 | } 117 | 118 | process.once('SIGINT', function() { 119 | shutdown = true; 120 | console.log('Got SIGINT; closing...'); 121 | var watchdog = setTimeout(function() { 122 | console.log('Peer didn\'t close gracefully; force-closing'); 123 | p.destroy(); 124 | }, 10000); 125 | watchdog.unref(); 126 | p.once('close', function() { 127 | clearTimeout(watchdog); 128 | }); 129 | p.disconnect(); 130 | process.once('SIGINT', function() { 131 | console.log('Hard-kill'); 132 | process.exit(0); 133 | }); 134 | }); 135 | -------------------------------------------------------------------------------- /examples/namecoin.js: -------------------------------------------------------------------------------- 1 | var Peer = require('../lib/Peer').Peer; 2 | var Message = require('./Message').Message; 3 | 4 | var p = new Peer(0x291f20b2, 8334, 0xFEB4BEF9); 5 | 6 | p.on('connect', function(d) { 7 | console.log('connect'); 8 | 9 | // Send VERSION message 10 | var m = new Message(p.magicBytes, true); 11 | m.putInt32(35000); // version 12 | m.putInt64(1); // services 13 | m.putInt64(Math.round(new Date().getTime()/1000)); // timestamp 14 | m.pad(26); // addr_me 15 | m.pad(26); // addr_you 16 | m.putInt64(0x1234); // nonce 17 | m.putVarString('/Cryptocoinjs:0.1/'); 18 | m.putInt32(10); // start_height 19 | 20 | //console.log(m.raw().toString('hex')); 21 | p.send('version', m.raw()); 22 | }); 23 | p.on('end', function(d) { 24 | console.log('end'); 25 | }); 26 | p.on('error', function(d) { 27 | console.log('error', d); 28 | }); 29 | p.on('message', function(d) { 30 | console.log('message', d.command, d.data.toString('hex')); 31 | }); 32 | 33 | process.on('SIGINT', function() { 34 | console.log('Got SIGINT; closing...'); 35 | p.disconnect(); 36 | process.exit(0); 37 | }); 38 | 39 | p.connect(); 40 | -------------------------------------------------------------------------------- /lib/Peer.js: -------------------------------------------------------------------------------- 1 | var net = require('net'); 2 | var events = require('events'); 3 | var util = require('util'); 4 | var sha256 = require('crypto-hashing').sha256; 5 | 6 | var Host = function Host(host, port) { 7 | var _host = false, // Private variables 8 | _port = false, 9 | _version = false; 10 | 11 | Object.defineProperties(this, { 12 | 'host': { 13 | get: function() { return _host; }, 14 | enumerable: true 15 | }, 16 | 'port': { 17 | get: function() { return _port; }, 18 | enumerable: true 19 | }, 20 | 'version': { 21 | get: function() { return _version; }, 22 | enumerable: true 23 | } 24 | }); 25 | 26 | if (Array.isArray(host)) { 27 | host = new Buffer(host); 28 | } 29 | _port = +port || this.defaultPort; 30 | 31 | if (typeof host === 'undefined') { 32 | _host = 'localhost'; 33 | _version = 4; 34 | return this; 35 | } else if (typeof host === 'number') { 36 | // an IPv4 address, expressed as a little-endian 4-byte (32-bit) number 37 | // style of "pnSeed" array in Bitcoin reference client 38 | var buf = new Buffer(4); 39 | buf.writeInt32LE(host, 0); 40 | _host = Array.prototype.join.apply(buf, ['.']); 41 | _version = 4; 42 | return this; 43 | } else if (typeof host === 'string') { 44 | _host = host; 45 | _version = net.isIP(host); 46 | 47 | if (_version == 0) { 48 | // DNS host name string 49 | if (_host.indexOf(':') !== -1) { 50 | var pieces = _host.split(':'); 51 | _host = pieces[0]; 52 | _port = pieces[1]; // Given "example.com:8080" as host, and "1234" as port, the "8080" takes priority 53 | _version = net.isIP(_host); 54 | if (_version == 0) { 55 | // TODO: Resolve to IP, to ensure unique-ness 56 | } 57 | } 58 | } 59 | return this; 60 | } else if (Buffer.isBuffer(host)) { 61 | if (host.length == 4) { 62 | // IPv4 stored as bytes 63 | _host = Array.prototype.join.apply(host, ['.']); 64 | _version = 4; 65 | return this; 66 | } else if (host.slice(0, 12).toString('hex') != Host.IPV6_IPV4_PADDING.toString('hex')) { 67 | // IPv6 68 | _host = host.toString('hex').match(/(.{1,4})/g).join(':').replace(/\:(0{2,4})/g, ':0').replace(/^(0{2,4})/g, ':0'); 69 | _version = 6; 70 | return this; 71 | } else { 72 | // IPv4 with padding in front 73 | _host = Array.prototype.join.apply(host.slice(12), ['.']); 74 | _version = 4; 75 | return this; 76 | } 77 | } else { 78 | throw new Error('Cound not instantiate peer; invalid parameter type: '+ typeof host); 79 | } 80 | }; 81 | Host.prototype.IPV6_IPV4_PADDING = new Buffer([0,0,0,0,0,0,0,0,0,0,255,255]); 82 | Host.prototype.defaultPort = 8333; 83 | 84 | var Peer = exports.Peer = function Peer(host, port, magic) { 85 | events.EventEmitter.call(this); 86 | this.lastSeen = false; 87 | if (!(host instanceof Host)) { 88 | host = new Host(host, port); 89 | } 90 | Object.defineProperty(this, 'host', { 91 | enumerable: true, 92 | configurable: false, 93 | writable:false, 94 | value: host 95 | }); 96 | if (typeof magic !== 'undefined') this.magicBytes = magic; 97 | 98 | var myState = 'new'; 99 | Object.defineProperty(this, 'state', { 100 | enumerable: true, 101 | get: function() { 102 | return myState; 103 | }, 104 | set: function(newValue) { 105 | var oldState = myState; 106 | this.emit('stateChange', {new: newValue, old: oldState}); 107 | myState = newValue; 108 | } 109 | }); 110 | 111 | return this; 112 | }; 113 | util.inherits(Peer, events.EventEmitter); 114 | 115 | Peer.prototype.MAX_RECEIVE_BUFFER = 1024*1024*10; 116 | Peer.prototype.magicBytes = 0xD9B4BEF9; 117 | 118 | Peer.prototype.connect = function connect(socket) { 119 | this.state = 'connecting'; 120 | this.inbound = new Buffer(this.MAX_RECEIVE_BUFFER); 121 | this.inboundCursor = 0; 122 | 123 | if (typeof socket === 'undefined' || !(socket instanceof net.Socket)) { 124 | socket = net.createConnection(this.host.port, this.host.host, this.handleConnect.bind(this)); 125 | } else { 126 | this.state = 'connected'; // Binding to an already-connected socket; will not fire a 'connect' event, but will still fire a 'stateChange' event 127 | } 128 | Object.defineProperty(this, 'socket', { 129 | enumerable: false, 130 | configurable: false, 131 | writable:false, 132 | value: socket 133 | }); 134 | this.socket.on('error', this.handleError.bind(this)); 135 | this.socket.on('data', this.handleData.bind(this)); 136 | this.socket.on('end', this.handleEnd.bind(this)); 137 | this.socket.on('close', this.handleClose.bind(this)); 138 | 139 | return this.socket; 140 | }; 141 | 142 | Peer.prototype.disconnect = function disconnect() { 143 | this.state = 'disconnecting'; 144 | this.socket.end(); // Inform the other end we're going away 145 | }; 146 | 147 | Peer.prototype.destroy = function destroy() { 148 | this.socket.destroy(); 149 | }; 150 | 151 | Peer.prototype.getUUID = function getUUID() { 152 | return this.host.host+'~'+this.host.port; 153 | } 154 | 155 | Peer.prototype.handleConnect = function handleConnect() { 156 | this.state = 'connected'; 157 | this.emit('connect', { 158 | peer: this, 159 | }); 160 | }; 161 | 162 | Peer.prototype.handleEnd = function handleEnd() { 163 | this.emit('end', { 164 | peer: this, 165 | }); 166 | }; 167 | 168 | Peer.prototype.handleError = function handleError(data) { 169 | this.emit('error', { 170 | peer: this, 171 | error: data 172 | }); 173 | }; 174 | 175 | Peer.prototype.handleClose = function handleClose(had_error) { 176 | this.state = 'closed'; 177 | this.emit('close', { 178 | peer: this, 179 | had_error: had_error 180 | }); 181 | }; 182 | 183 | Peer.prototype.messageChecksum = function(msg) { 184 | return new Buffer(sha256.x2(msg)).slice(0,4); 185 | }; 186 | 187 | Peer.prototype.send = function send(command, data, callback) { 188 | if (typeof data == 'undefined') { 189 | data = new Buffer(0); 190 | } else if (Array.isArray(data)) { 191 | data = new Buffer(data); 192 | } 193 | var out = new Buffer(data.length + 24); 194 | out.writeUInt32LE(this.magicBytes, 0); // magic 195 | for (var i = 0; i < 12; i++) { 196 | var num = (i >= command.length)? 0 : command.charCodeAt(i); 197 | out.writeUInt8(num, 4+i); // command 198 | } 199 | out.writeUInt32LE(data.length, 16); // length 200 | 201 | var checksum = this.messageChecksum(data); 202 | checksum.copy(out, 20); // checksum 203 | data.copy(out, 24); 204 | 205 | this.socket.write(out, null, callback); 206 | }; 207 | 208 | Peer.prototype.handleData = function handleData(data) { 209 | this.lastSeen = new Date(); 210 | 211 | // Add data to incoming buffer 212 | if (data.length + this.inboundCursor > this.inbound.length) { 213 | this.emit('error', 'Peer exceeded max receiving buffer'); 214 | this.inboundCursor = this.inbound.length+1; 215 | return; 216 | } 217 | data.copy(this.inbound, this.inboundCursor); 218 | this.inboundCursor += data.length; 219 | 220 | if (this.inboundCursor < 20) return; // Can't process something less than 20 bytes in size 221 | 222 | // Split on magic bytes into message(s) 223 | var i = 0, endPoint = 0; 224 | //console.log('searching for messages in '+this.inboundCursor+' bytes'); 225 | while (i < this.inboundCursor) { 226 | if (this.inbound.readUInt32LE(i) == this.magicBytes) { 227 | //console.log('found message start at '+i); 228 | var msgStart = i; 229 | if (this.inboundCursor > msgStart + 16) { 230 | var msgLen = this.inbound.readUInt32LE(msgStart + 16); 231 | //console.log('message is '+msgLen+' bytes long'); 232 | if (this.inboundCursor >= msgStart + msgLen + 24) { 233 | // Complete message; parse it 234 | this.handleMessage(this.inbound.slice(msgStart, msgStart + msgLen + 24)); 235 | endPoint = msgStart + msgLen + 24; 236 | } 237 | i += msgLen+24; // Skip to next message 238 | } else { 239 | i = this.inboundCursor; // Skip to end 240 | } 241 | } else { 242 | i++; 243 | } 244 | } 245 | 246 | // Done processing all found messages 247 | if (endPoint > 0) { 248 | //console.log('messaged parsed up to '+endPoint+', but cursor goes out to '+this.inboundCursor); 249 | this.inbound.copy(this.inbound, 0, endPoint, this.inboundCursor); // Copy from later in the buffer to earlier in the buffer 250 | this.inboundCursor -= endPoint; 251 | //console.log('removed '+endPoint+' bytes processed data, putting cursor to '+this.inboundCursor); 252 | } 253 | }; 254 | 255 | Peer.prototype.handleMessage = function handleMessage(msg) { 256 | var msgLen = msg.readUInt32LE(16); 257 | 258 | // Get command 259 | var cmd = []; 260 | for (var j=0; j<12; j++) { 261 | var s = msg[4+j]; 262 | if (s > 0) { 263 | cmd.push(String.fromCharCode(s)); 264 | } 265 | } 266 | cmd = cmd.join(''); 267 | 268 | var checksum = msg.readUInt32BE(20); 269 | if (msgLen > 0) { 270 | var payload = new Buffer(msgLen); 271 | msg.copy(payload, 0, 24); 272 | var checksumCalc = this.messageChecksum(payload); 273 | if (checksum != checksumCalc.readUInt32BE(0)) { 274 | console.log('Supplied checksum of '+checksum.toString('hex')+' does not match calculated checksum of '+checksumCalc.toString('hex')); 275 | } 276 | } else { 277 | var payload = new Buffer(0); 278 | } 279 | 280 | this.emit('message', { 281 | peer: this, 282 | command: cmd, 283 | data: payload 284 | }); 285 | this.emit(cmd+'Message', { 286 | peer: this, 287 | data: payload 288 | }); 289 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "p2p-node", 3 | "version": "0.1.1", 4 | "description": "Create a peer-to-peer relationship", 5 | "keywords": [ 6 | "p2p", 7 | "network", 8 | "peers", 9 | "cryptography" 10 | ], 11 | "main": "./lib/Peer.js", 12 | "scripts": { 13 | "test": "mocha" 14 | }, 15 | 16 | "dependencies": { 17 | "crypto-hashing": "0.1.x" 18 | }, 19 | "devDependencies": { 20 | "mocha": "1.*" 21 | }, 22 | "repository": { 23 | "url": "https://github.com/cryptocoinjs/p2p-node", 24 | "type": "git" 25 | }, 26 | 27 | "author": "Brooks Boyd " 28 | } 29 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --ui bdd 2 | --reporter spec 3 | --timeout 2000 -------------------------------------------------------------------------------- /test/peer.test.js: -------------------------------------------------------------------------------- 1 | var Peer = require('../lib/Peer').Peer; 2 | var assert = require("assert"); 3 | var net = require('net'); 4 | 5 | describe('P2P Peer', function() { 6 | it('should properly connect to indicated host', function(done) { 7 | var localPeer = false; 8 | var server = net.createServer(function(socket) { 9 | server.close(); 10 | localPeer.destroy(); 11 | done(); 12 | }); 13 | server.listen(function() { 14 | localPeer = new Peer(server.address().address, server.address().port); 15 | localPeer.connect(); 16 | }); 17 | }); 18 | describe('Messaging', function() { 19 | var magic = 0x01020304; 20 | var server = false; 21 | var localPeer = false; 22 | var serverPeer = false; 23 | 24 | beforeEach(function(done) { 25 | serverPeer = false; 26 | server = net.createServer(function(socket) { 27 | serverPeer = new Peer(socket.remoteAddress, socket.remotePort, magic); 28 | serverPeer.connect(socket); 29 | }); 30 | localPeer = false; 31 | server.listen(function() { 32 | localPeer = new Peer(server.address().address, server.address().port, magic); 33 | done(); 34 | }); 35 | }); 36 | 37 | afterEach(function() { 38 | if (serverPeer !== false) serverPeer.destroy(); 39 | server.close(); 40 | if (localPeer !== false) localPeer.destroy(); 41 | }); 42 | 43 | it('should properly parse data stream into message events', function(done) { 44 | var timer = false; 45 | localPeer.on('message', function(d) { 46 | assert.equal(d.command, 'hello'); 47 | assert. equal(d.data.toString('utf8'), 'world'); 48 | clearInterval(timer); 49 | done(); 50 | }); 51 | localPeer.connect(); 52 | timer = setInterval(function() { 53 | if (serverPeer !== false) { 54 | serverPeer.send('hello', new Buffer('world', 'utf8')); 55 | } 56 | }, 100); 57 | }); 58 | it('should properly parse data stream into command message events', function(done) { 59 | var timer = false; 60 | localPeer.once('helloMessage', function(d) { 61 | assert.equal(d.data.toString('utf8'), 'world'); 62 | clearInterval(timer); 63 | done(); 64 | }); 65 | localPeer.connect(); 66 | timer = setInterval(function() { 67 | if (serverPeer !== false) { 68 | serverPeer.send('hello', new Buffer('world', 'utf8')); 69 | } 70 | }, 100); 71 | }); 72 | it('should error out if internal buffer is overflown', function(done) { 73 | var timer = false; 74 | localPeer.once('helloMessage', function(d) { 75 | assert.equal(d.data.toString('utf8'), 'world'); 76 | clearInterval(timer); 77 | done(); 78 | }); 79 | localPeer.MAX_RECEIVE_BUFFER = 10; 80 | localPeer.on('error', function(err) { 81 | if (err == 'Peer exceeded max receiving buffer') { 82 | clearInterval(timer); 83 | done(); 84 | } 85 | }); 86 | localPeer.connect(); 87 | timer = setInterval(function() { 88 | if (serverPeer !== false) { 89 | serverPeer.send('hello', new Buffer('world', 'utf8')); 90 | } 91 | }, 100); 92 | }); 93 | it('should not error out if multiple messages fill up the buffer', function(done) { 94 | var timer = false; 95 | localPeer.once('helloMessage', function(d) { 96 | assert.equal(d.data.toString('utf8'), 'world'); 97 | clearInterval(timer); 98 | done(); 99 | }); 100 | localPeer.MAX_RECEIVE_BUFFER = 30; 101 | var count = 0; 102 | localPeer.on('helloMessage', function(d) { 103 | count++; 104 | if (count >= 5) { 105 | clearInterval(timer); 106 | done(); 107 | } 108 | }); 109 | localPeer.connect(); 110 | timer = setInterval(function() { 111 | if (serverPeer !== false) { 112 | serverPeer.send('hello', new Buffer('world', 'utf8')); 113 | } 114 | }, 100); 115 | }); 116 | }); 117 | }); 118 | --------------------------------------------------------------------------------