@savid/rlpx-pest
Version:
Get the [status](https://github.com/ethereum/devp2p/blob/master/caps/eth.md#status-0x00) of a [RLPx](https://github.com/ethereum/devp2p/blob/master/rlpx.md) peer via the [eth/66](https://eips.ethereum.org/EIPS/eip-2481) protocol.
321 lines • 13.1 kB
JavaScript
import { randomBytes } from 'crypto';
import EventEmitter from 'events';
import net from 'net';
import * as os from 'os';
import { pk2id, ECIES, buffer2int, int2buffer, BASE_PROTOCOL_VERSION, PREFIXES, DISCONNECT_REASONS, BASE_PROTOCOL_LENGTH, } from '@ethereumjs/devp2p';
import { bufArrToArr, arrToBufArr } from '@ethereumjs/util';
import BufferList from 'bl';
import { getPublicKey } from 'ethereum-cryptography/secp256k1.js';
import RLP from 'rlp';
import * as snappy from 'snappyjs';
import ETH66 from './eth66.js';
const CLIENT = Buffer.from(`RLPxPest/v1.0.0/${os.platform()}-${os.arch()}/nodejs`, 'utf8');
export class PeerError extends Error {
constructor(message, code) {
super(message);
this.name = 'PeerError';
this.code = code;
}
}
export var ErrorCode;
(function (ErrorCode) {
ErrorCode["INITIAL_SOCKET_CONNECT_FAILED"] = "INITIAL_SOCKET_CONNECT_FAILED";
ErrorCode["SOCKET_ERROR"] = "SOCKET_ERROR";
ErrorCode["SOCKET_CLOSED"] = "SOCKET_CLOSED";
ErrorCode["INCOMING_DATA_STATE"] = "INCOMING_DATA_STATE";
ErrorCode["INVALID_HEADER_SIZE"] = "INVALID_HEADER_SIZE";
ErrorCode["SEND_HELLO_MESSAGE_FAILED"] = "SEND_HELLO_MESSAGE_FAILED";
ErrorCode["EMPTY_BODY_MESSAGE"] = "EMPTY_BODY_MESSAGE";
ErrorCode["BODY_PARSE_FAILED"] = "BODY_PARSE_FAILED";
ErrorCode["PEER_DISCONNECTED"] = "PEER_DISCONNECTED";
})(ErrorCode = ErrorCode || (ErrorCode = {}));
const wrapError = (error, code) => {
if (error instanceof PeerError) {
return error;
}
const wrappedError = new PeerError(error.message, code);
wrappedError.stack = error.stack;
return wrappedError;
};
class Peer extends EventEmitter {
constructor({ remoteId, host, port }) {
super();
this.capabilities = [
ETH66((error, response) => {
if (error)
this.emit('err', error);
if (response)
this.emit('status', response);
}),
];
this.remoteId = remoteId;
this.host = host;
this.port = port;
this.privateKey = randomBytes(32);
this.id = pk2id(Buffer.from(getPublicKey(this.privateKey, false)));
this.eciesSession = new ECIES(this.privateKey, this.id, this.remoteId);
this.socket = new net.Socket();
this.socketNextPacketSize = 307;
this.socketData = new BufferList();
this.state = 'Auth';
this.protocols = [];
}
destroy() {
this.socket.destroy();
}
init() {
this.socket.on('error', (error) => this.emit('err', wrapError(error, ErrorCode.INITIAL_SOCKET_CONNECT_FAILED)));
this.socket.once('connect', () => {
this.socket.on('error', (error) => this.emit('err', wrapError(error, ErrorCode.SOCKET_ERROR)));
this.socket.on('data', (data) => this.handleData(data));
this.socket.on('close', () => this.emit('err', new PeerError('socket closed', ErrorCode.SOCKET_CLOSED)));
this.sendAuth();
});
this.socket.connect(this.port, this.host);
}
handleData(data) {
if (this.socket.closed)
return;
this.socketData.append(data);
try {
while (this.socketData.length >= this.socketNextPacketSize) {
switch (this.state) {
case 'Auth':
throw new Error('not implemented');
case 'Ack':
this.handleAck();
break;
case 'Header':
this.handleHeader();
break;
case 'Body':
this.handleBody();
break;
default:
throw new Error(`Unknown state: ${this.state}`);
}
}
}
catch (error) {
this.emit('err', error instanceof Error
? wrapError(error, ErrorCode.INCOMING_DATA_STATE)
: new PeerError('unknown', ErrorCode.INCOMING_DATA_STATE));
this.sendDisconnect(DISCONNECT_REASONS.SUBPROTOCOL_ERROR);
}
}
sendAuth() {
const auth = this.eciesSession.createAuthEIP8();
if (auth && !this.socket?.closed)
this.socket.write(auth);
this.state = 'Ack';
this.socketNextPacketSize = 210;
}
handleAck() {
const bytesCount = this.socketNextPacketSize;
const parseData = this.socketData.slice(0, bytesCount);
if (!this.eciesSession._gotEIP8Ack) {
if (parseData.subarray(0, 1) === Buffer.from('04', 'hex')) {
this.eciesSession.parseAckPlain(parseData);
}
else {
this.eciesSession._gotEIP8Ack = true;
this.socketNextPacketSize = buffer2int(this.socketData.slice(0, 2)) + 2;
return;
}
}
else {
this.eciesSession.parseAckEIP8(parseData);
}
this.state = 'Header';
this.socketNextPacketSize = 32;
process.nextTick(() => this.sendHello());
this.socketData.consume(bytesCount);
}
handleHeader() {
const bytesCount = this.socketNextPacketSize;
const parseData = this.socketData.slice(0, bytesCount);
const size = this.eciesSession.parseHeader(parseData);
if (!size) {
this.emit('err', new PeerError('invalid header size', ErrorCode.INVALID_HEADER_SIZE));
this.sendDisconnect(DISCONNECT_REASONS.NETWORK_ERROR);
return;
}
this.state = 'Body';
this.socketNextPacketSize = size + 16;
if (size % 16 > 0)
this.socketNextPacketSize += 16 - (size % 16);
this.socketData.consume(bytesCount);
}
handleMessage(code, msg) {
if (code === PREFIXES.HELLO)
this.handleHello(msg);
}
sendHello() {
const payload = [
int2buffer(BASE_PROTOCOL_VERSION),
CLIENT,
this.capabilities.map((obj) => [Buffer.from(obj.name), int2buffer(obj.version)]),
Buffer.allocUnsafe(0),
this.id,
];
if (!this.socket.closed) {
if (!this.sendMessage(PREFIXES.HELLO, Buffer.from(RLP.encode(bufArrToArr(payload))))) {
this.emit('err', new PeerError('Failed to send hello message', ErrorCode.SEND_HELLO_MESSAGE_FAILED));
this.sendDisconnect(DISCONNECT_REASONS.NETWORK_ERROR);
}
}
}
sendMessage(code, data) {
if (this.socket.closed)
return false;
const msg = Buffer.concat([Buffer.from(RLP.encode(code)), data]);
const header = this.eciesSession.createHeader(msg.length);
if (!header || this.socket.destroyed)
return false;
this.socket.write(header);
const body = this.eciesSession.createBody(msg);
if (!body || this.socket.destroyed)
return false;
this.socket.write(body);
return true;
}
sendDisconnect(reason) {
const data = Buffer.from(RLP.encode(reason));
if (this.sendMessage(PREFIXES.DISCONNECT, data) !== true)
return;
setTimeout(() => this.socket.end(), 2000);
}
getProtocol(code) {
if (code < BASE_PROTOCOL_LENGTH)
return { protocol: this, offset: 0 };
return this.protocols.find((obj) => code >= obj.offset && code < obj.offset + (obj?.length ?? 0));
}
handleBody() {
const bytesCount = this.socketNextPacketSize;
const parseData = this.socketData.slice(0, bytesCount);
const body = this.eciesSession.parseBody(parseData);
if (!body) {
this.emit('err', new PeerError('empty body payload', ErrorCode.EMPTY_BODY_MESSAGE));
this.sendDisconnect(DISCONNECT_REASONS.NETWORK_ERROR);
return;
}
this.state = 'Header';
this.socketNextPacketSize = 32;
let code = body[0];
if (code === 0x80)
code = 0;
if (code !== PREFIXES.HELLO && code !== PREFIXES.DISCONNECT && this.hello === null) {
this.sendDisconnect(DISCONNECT_REASONS.PROTOCOL_ERROR);
return;
}
const protocolObj = this.getProtocol(code);
if (protocolObj === undefined) {
this.sendDisconnect(DISCONNECT_REASONS.PROTOCOL_ERROR);
return;
}
const msgCode = code - protocolObj.offset;
const protocolName = protocolObj.protocol.constructor.name;
try {
let payload = body.subarray(1);
let compressed = false;
const origPayload = payload;
if (Boolean(this.hello) &&
Boolean(this.hello?.protocolVersion) &&
(this.hello?.protocolVersion ?? 0) >= 5) {
payload = snappy.uncompress(payload);
compressed = true;
}
if (protocolName === 'Peer') {
try {
payload = arrToBufArr(RLP.decode(Uint8Array.from(payload)));
if (msgCode === PREFIXES.DISCONNECT) {
const reason = Buffer.isBuffer(payload)
? buffer2int(payload)
: buffer2int(payload[0] ?? Buffer.from([0]));
this.emit('err', new PeerError(`remote disconnected: ${DISCONNECT_REASONS[reason]}`, ErrorCode.PEER_DISCONNECTED));
return;
}
this.emit('client', payload[1].toString());
}
catch (error) {
if (msgCode === PREFIXES.DISCONNECT) {
try {
if (compressed) {
payload = arrToBufArr(RLP.decode(Uint8Array.from(origPayload)));
}
else {
payload = arrToBufArr(RLP.decode(Uint8Array.from(snappy.uncompress(payload))));
}
}
catch (err) {
throw err instanceof Error ? err : new Error('unknown disconnect error');
}
}
else {
throw error instanceof Error ? error : new Error('unknown error');
}
}
}
protocolObj.protocol.handleMessage(msgCode, payload);
if (protocolName !== 'Peer')
this.sendDisconnect(DISCONNECT_REASONS.SUBPROTOCOL_ERROR);
}
catch (error) {
this.emit('err', error instanceof Error
? wrapError(error, ErrorCode.BODY_PARSE_FAILED)
: new PeerError('failed to parse body payload', ErrorCode.BODY_PARSE_FAILED));
this.sendDisconnect(DISCONNECT_REASONS.SUBPROTOCOL_ERROR);
return;
}
this.socketData.consume(bytesCount);
}
handleHello(payload) {
this.hello = {
protocolVersion: buffer2int(payload[0]),
clientId: payload[1].toString(),
capabilities: payload[2].map((item) => ({
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)) {
this.sendDisconnect(DISCONNECT_REASONS.INVALID_IDENTITY);
return;
}
const shared = {};
this.hello.capabilities.forEach((item) => {
this.capabilities.forEach((obj) => {
if (obj.name !== item.name || obj.version !== item.version)
return;
if (Boolean(shared[obj.name]) && shared[obj.name].version > obj.version)
return;
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 origOffset = offset;
offset += obj.length;
const sendMethod = (code, data) => {
if (code > obj.length)
throw new Error('Code out of range');
this.sendMessage(origOffset + code, data);
};
const SubProtocol = obj.constructor;
const protocol = new SubProtocol(obj.version, this, sendMethod);
return { protocol, offset: origOffset, length: obj.length };
});
if (this.protocols.length === 0)
this.sendDisconnect(DISCONNECT_REASONS.USELESS_PEER);
}
}
export default Peer;
//# sourceMappingURL=peer.js.map