UNPKG

ntrnetwork

Version:

Transledger peer to peer wire communication

399 lines (344 loc) 14.1 kB
//---------------------------------------------------------------------------------------- // Peer class // // Authors: // "Alex Beregszaszi <alex@rtfs.hu>", // "Kirill Fomichev <fanatid@ya.ru> (https://github.com/fanatid)", // "Martin Becze <mb@ethdev.com>", // "Holger Drewes <holger.drewes@gmail.com>", // "Didier PH Martin <didierphmartin@transledger.io>" // // updates: // October 17th 2018 Didier PH Martin // September 2 2020 Didier PH Martin // --------------------------------------------------------------------------------------- const { EventEmitter } = require('events'); const rlp = require('rlp-encoding'); const util = require('../util'); const BufferList = require('bl'); const ms = require('ms'); const Buffer = require('safe-buffer').Buffer; const { int2buffer, buffer2int } = require('../util'); const ECIES = require('./ecies'); const dpt = require('../dpt') const debug = (process.env.PEER) ? process.env.PEER : require('../environment').debug.peer; const BASE_PROTOCOL_VERSION = 4; const BASE_PROTOCOL_LENGTH = 16; const PING_INTERVAL = ms('15s') const PREFIXES = { HELLO: 0x00, DISCONNECT: 0x01, PING: 0x02, PONG: 0x03 } class Peer extends EventEmitter { constructor (options) { super() // hello data this._clientId = options.clientId; this._capabilities = options.capabilities; this._port = options.port; this._id = options.id; this._remoteClientIdFilter = options.remoteClientIdFilter; this.channelID = options.channelID; // ECIES session this._remoteId = options.remoteId; this._EIP8 = options.EIP8 !== undefined ? options.EIP8 : true; this._eciesSession = new ECIES(options.privateKey, this._id, this._remoteId); // Auth, Ack, Header, Body this._state = 'Auth' this._weHello = null this._hello = null this._nextPacketSize = 307 // socket this._socket = options.socket this._socket.on('error', (err) => this.emit('error', err)) this._socket.once('close', () => { clearInterval(this._pingIntervalId) clearTimeout(this._pingTimeoutId) this._closed = true if (this._connected) this.emit('close', this._disconnectReason, this._disconnectWe) }) const bl = new BufferList() this._socket.on('data', (data) => { if (this._closed) return bl.append(data) while (bl.length >= this._nextPacketSize) { const bytesCount = this._nextPacketSize const parseData = bl.slice(0, bytesCount) try { if (this._state === 'Auth') { if (!this._eciesSession._gotEIP8Auth) { try { this._eciesSession.parseAuthPlain(parseData) } catch (err) { this._eciesSession._gotEIP8Auth = true this._nextPacketSize = util.buffer2int(data.slice(0, 2)) + 2 continue } } else { this._eciesSession.parseAuthEIP8(parseData) } this._state = 'Header' this._nextPacketSize = 32 process.nextTick(() => this._sendAck()) } else if (this._state === 'Ack') { if (!this._eciesSession._gotEIP8Ack) { try { this._eciesSession.parseAckPlain(parseData) debug ? console.log(`${Date().toString().substring(0, 24)} rplx/peer._socket.on('data'): Received ack (old format) from ${this._socket.remoteAddress}:${this._socket.remotePort}`) : null; } catch (err) { this._eciesSession._gotEIP8Ack = true this._nextPacketSize = util.buffer2int(data.slice(0, 2)) + 2 continue } } else { this._eciesSession.parseAckEIP8(parseData) debug ? console.log(`${Date().toString().substring(0, 24)} rplx/peer._socket.on('data'): Received ack (EIP8) from ${this._socket.remoteAddress}:${this._socket.remotePort}`) : null; } this._state = 'Header' this._nextPacketSize = 32 process.nextTick(() => this._sendHello()) } else { this._parsePacketContent(parseData) } } catch (err) { this.emit('error', err) } bl.consume(bytesCount) } }) this._connected = false this._closed = false this._disconnectReason = null this._disconnectWe = null this._pingIntervalId = null this._pingTimeout = options.timeout this._pingTimeoutId = null // sub-protocols this._protocols = [] // send AUTH if outgoing connection if (this._remoteId !== null) this._sendAuth() } static get DISCONNECT_REASONS() { return { DISCONNECT_REQUESTED: 0x00, NETWORK_ERROR: 0x01, PROTOCOL_ERROR: 0x02, USELESS_PEER: 0x03, TOO_MANY_PEERS: 0x04, ALREADY_CONNECTED: 0x05, INCOMPATIBLE_VERSION: 0x06, INVALID_IDENTITY: 0x07, CLIENT_QUITTING: 0x08, UNEXPECTED_IDENTITY: 0x09, SAME_IDENTITY: 0x0a, TIMEOUT: 0x0b, SUBPROTOCOL_ERROR: 0x10 } } _parseSocketData (data) { } _parsePacketContent (data) { debug ? console.log(`--------- ParsePacketContent -----------------`) : null; switch (this._state) { case 'Header': debug ? console.log(`${Date().toString().substring(0, 24)} rplx/peer._parsePacketContent (data): Received header ${this._socket.remoteAddress}:${this._socket.remotePort}`) : null; const size = this._eciesSession.parseHeader(data) this._state = 'Body' this._nextPacketSize = size + 16 if (size % 16 > 0) this._nextPacketSize += 16 - size % 16 break case 'Body': const body = this._eciesSession.parseBody(data) this._state = 'Header' this._nextPacketSize = 32 // RLP hack let code = body[0] if (code === 0x80) code = 0 debug ? console.log(`${Date().toString().substring(0, 24)} rplx/peer._parsePacketContent (data): Received body ${this._socket.remoteAddress}:${this._socket.remotePort} code: ${code} data:${body.toString('hex')}`) : null; if (code !== PREFIXES.HELLO && code !== PREFIXES.DISCONNECT && this._hello === null) { return this.disconnect(Peer.DISCONNECT_REASONS.PROTOCOL_ERROR) } const obj = this._getProtocol(code) if (obj === undefined) return this.disconnect(Peer.DISCONNECT_REASONS.PROTOCOL_ERROR) const msgCode = code - obj.offset const prefix = this.getMsgPrefix(msgCode) debug ? console.log(`${Date().toString().substring(0, 24)} rplx./peer._parsePacketContent (data): Received ${prefix} (message code: ${code} - ${obj.offset} = ${msgCode}) ${this._socket.remoteAddress}:${this._socket.remotePort}`) : null; try { obj.protocol._handleMessage(msgCode, body.slice(1)) } catch (err) { this.disconnect(Peer.DISCONNECT_REASONS.SUBPROTOCOL_ERROR) this.emit('error', err) } debug ? console.log(`-------------------------------------`) : null; break } } _getProtocol (code) { if (code < BASE_PROTOCOL_LENGTH) return { protocol: this, offset: 0 } for (let obj of this._protocols) { if (code >= obj.offset && code < obj.offset + obj.length) return obj } } _handleMessage (code, msg) { const payload = rlp.decode(msg) switch (code) { case PREFIXES.HELLO: this._hello = { protocolVersion: buffer2int(payload[0]), clientId: payload[1].toString(), capabilities: payload[2].map((item) => { return { name: item[0].toString(), version: buffer2int(item[1]) } }), port: buffer2int(payload[3]), id: payload[4] } if (this._remoteId === null) { this._remoteId = Buffer.from(this._hello.id) } else if (!this._remoteId.equals(this._hello.id)) { return this.disconnect(Peer.DISCONNECT_REASONS.INVALID_IDENTITY) } if (this._remoteClientIdFilter) { for (let filterStr of this._remoteClientIdFilter) { if (!this._hello.clientId.toLowerCase().includes(filterStr.toLowerCase())) { return this.disconnect(Peer.DISCONNECT_REASONS.USELESS_PEER) } } } const shared = {} for (let item of this._hello.capabilities) { for (let obj of this._capabilities) { if (obj.name !== item.name || obj.version !== item.version) continue if (shared[obj.name] && shared[obj.name].version > obj.version) continue shared[obj.name] = obj } } let offset = BASE_PROTOCOL_LENGTH this._protocols = Object.keys(shared).map((key) => shared[key]) .sort((obj1, obj2) => obj1.name < obj2.name ? -1 : 1) .map((obj) => { const _offset = offset offset += obj.length const SubProtocol = obj.constructor const protocol = new SubProtocol(obj.version, this, (code, data) => { if (code > obj.length) throw new Error('Code out of range') this._sendMessage(_offset + code, data) }) return { protocol, offset: _offset, length: obj.length } }) if (this._protocols.length === 0) { return this.disconnect(Peer.DISCONNECT_REASONS.USELESS_PEER) } this._connected = true this._pingIntervalId = setInterval(() => this._sendPing(), PING_INTERVAL) if (this._weHello) { this.emit('connect') } break case PREFIXES.DISCONNECT: this._closed = true this._disconnectReason = payload[0].length === 0 ? 0 : payload[0][0] this._disconnectWe = false this._socket.end() break case PREFIXES.PING: this._sendPong() break case PREFIXES.PONG: clearTimeout(this._pingTimeoutId) break } } _sendAuth () { if (this._closed) return debug ? console.log(`${Date().toString().substring(0, 24)} rplx/peer._sendAuth(): Send auth (EIP8: ${this._EIP8}) to ${this._socket.remoteAddress}:${this._socket.remotePort}`) : null; if (this._EIP8) { this._socket.write(this._eciesSession.createAuthEIP8()) } else { this._socket.write(this._eciesSession.createAuthNonEIP8()) } this._state = 'Ack' this._nextPacketSize = 210 } _sendAck () { if (this._closed) return debug ? console.log(`${Date().toString().substring(0, 24)} rplx/peer._sendAck(): Send ack (EIP8: ${this._eciesSession._gotEIP8Auth}) to ${this._socket.remoteAddress}:${this._socket.remotePort}`) : null; if (this._eciesSession._gotEIP8Auth) { this._socket.write(this._eciesSession.createAckEIP8()) } else { this._socket.write(this._eciesSession.createAckOld()) } this._state = 'Header' this._nextPacketSize = 32 this._sendHello() } _sendMessage (code, data) { if (this._closed) return false const msg = Buffer.concat([ rlp.encode(code), data ]) this._socket.write(this._eciesSession.createHeader(msg.length)) this._socket.write(this._eciesSession.createBody(msg)) return true; } _sendHello () { debug ? console.log(`${Date().toString().substring(0, 24)} rplx/peer._sendHello: Send HELLO to ${this._socket.remoteAddress}:${this._socket.remotePort}`) : null const payload = [ int2buffer(BASE_PROTOCOL_VERSION), this._clientId, this._capabilities.map((obj) => [ Buffer.from(obj.name), int2buffer(obj.version) ]), this._port === null ? Buffer.allocUnsafe(0) : int2buffer(this._port), this._id ] if (!this._closed) { if (this._sendMessage(PREFIXES.HELLO, rlp.encode(payload))) { this._weHello = payload } if (this._hello) { this.emit('connect') } } } _sendPing () { debug ? console.log(`${Date().toString().substring(0, 24)} rplx/peer._sendPing(): Send PING to ${this._socket.remoteAddress}:${this._socket.remotePort}`) : null; const data = rlp.encode([]) if (!this._sendMessage(PREFIXES.PING, data)) return clearTimeout(this._pingTimeoutId) this._pingTimeoutId = setTimeout(() => { this.disconnect(Peer.DISCONNECT_REASONS.TIMEOUT) }, this._pingTimeout) } _sendPong () { debug ? console.log(`${Date().toString().substring(0, 24)} rplx/peer._sendPong(): Send PONG to ${this._socket.remoteAddress}:${this._socket.remotePort}`) : null; const data = rlp.encode([]) this._sendMessage(PREFIXES.PONG, data) } _sendDisconnect (reason) { debug ? console.log(`${Date().toString().substring(0, 24)} rplx/peer._sendDisconnect(reason): Send DISCONNECT to ${this._socket.remoteAddress}:${this._socket.remotePort} (reason: ${this.getDisconnectPrefix(reason)})`) : null; const data = rlp.encode(reason) if (!this._sendMessage(PREFIXES.DISCONNECT, data)) return this._disconnectReason = reason this._disconnectWe = true this._closed = true setTimeout(() => this._socket.destroy(), ms('2s')) } getId () { if (this._remoteId === null) return null return Buffer.from(this._remoteId) } getHelloMessage () { return this._hello } getProtocols () { return this._protocols.map((obj) => obj.protocol) } getMsgPrefix (code) { return Object.keys(PREFIXES).find(key => PREFIXES[key] === code) } getDisconnectPrefix (code) { return Object.keys(Peer.DISCONNECT_REASONS).find(key => Peer.DISCONNECT_REASONS[key] === code) } disconnect (reason = Peer.DISCONNECT_REASONS.DISCONNECT_REQUESTED) { this._sendDisconnect(reason); } } module.exports = Peer