/** * @module net */ import { EventEmitter } from 'events'; import { Socket } from 'net'; import { Packets } from './packets'; import { RC4, OUTGOING_KEY, INCOMING_KEY } from './crypto'; import { Packet } from './packet'; import { Mapper } from './mapper'; import { Logger, LogLevel } from '@n2/common'; import { Writer } from './writer'; import { Reader } from './reader'; /** * The configuration for the RC4 ciphers used by this PacketIO. */ export interface RC4Config { incomingKey: string; outgoingKey: string; } const DEFAULT_RC4: RC4Config = { incomingKey: INCOMING_KEY, outgoingKey: OUTGOING_KEY, }; /** * A utility class which implements the RotMG messaging protocol on top of a `Socket`. */ export class PacketIO extends EventEmitter { /** * The socket this packet interface is attached to. */ socket: Socket; /** * The last packet which was received. */ get lastIncomingPacket(): Packet { return this._lastIncomingPacket; } /** * The last packet which was sent. */ get lastOutgoingPacket(): Packet { return this._lastOutgoingPacket; } private logName: string; private sendRC4: RC4; private receiveRC4: RC4; private outgoingQueue: Packet[]; private writer: Writer; private reader: Reader; private eventHandlers: Map void>; // tslint:disable:variable-name private _lastIncomingPacket: Packet; private _lastOutgoingPacket: Packet; // tslint:enable:variable-name /** * Creates a new `PacketIO` on top of the given `socket`. * @param socket The socket to implement the protocol on top of. */ constructor(socket?: Socket, config: RC4Config = DEFAULT_RC4) { super(); this.writer = new Writer(); this.reader = new Reader(); this.outgoingQueue = []; this.sendRC4 = new RC4(Buffer.from(config.outgoingKey, 'hex')); this.receiveRC4 = new RC4(Buffer.from(config.incomingKey, 'hex')); let crypto = require('crypto'); this.logName = `PacketIO ${crypto.randomBytes(2).toString('hex')}`; crypto = null; this.eventHandlers = new Map([ ['data', this.onData.bind(this)], ['connect', this.onConnect.bind(this)] ]); if (socket) { this.attach(socket); } } /** * Attaches this Packet IO to the `socket`. * @param socket The socket to attach to. */ attach(socket: Socket): void { if (!(socket instanceof Socket)) { throw new TypeError(`Parameter "socket" should be a Socket, not ${typeof socket}`); } if (this.socket) { this.detach(); } this.socket = socket; for (const kvp of this.eventHandlers) { this.socket.on(kvp[0], kvp[1]); } this.onConnect(); } /** * Detaches this Packet IO from its `Socket`. */ detach(): void { for (const kvp of this.eventHandlers) { this.socket.removeListener(kvp[0], kvp[1]); } this.socket = undefined; } /** * Sends a packet. * @param packet The packet to send. */ send(packet: Packet) { if (!this.socket || this.socket.destroyed) { this.emitError(new Error('Not attached to a socket.')); } const type = Mapper.reverseMap.get(packet.type); if (!type) { this.emitError(new Error(`Mapper is missing an id for the packet type ${packet.type}`)); } if (this.outgoingQueue.length === 0) { this.outgoingQueue.push(packet); this.drainQueue(); } else { this.outgoingQueue.push(packet); } } private drainQueue() { this._lastOutgoingPacket = this.outgoingQueue[0]; this.writer.index = 5; const type = Mapper.reverseMap.get(this.outgoingQueue[0].type); this.outgoingQueue[0].write(this.writer); this.writer.writeHeader(type); this.sendRC4.cipher(this.writer.buffer.slice(5, this.writer.index)); new Promise((resolve) => { if (!this.socket.write(this.writer.buffer.slice(0, this.writer.index))) { this.socket.once('drain', resolve); } else { process.nextTick(resolve); } }).then(() => { Logger.log(this.logName, `WRITE: ${this.outgoingQueue[0].type}`, LogLevel.Debug); this.outgoingQueue.shift(); if (this.outgoingQueue.length > 0) { this.drainQueue(); } }); } /** * Emits a packet from this PacketIO instance. This will only * emit the packet to the clients subscribed to this particular PacketIO. * @param packet The packet to emit. */ emitPacket(packet: Packet): void { if (packet && typeof packet.type === 'string') { this._lastIncomingPacket = packet; this.emit(packet.type, packet); this.emit('packet', packet); } else { throw new TypeError(`Parameter "packet" must be a Packet, not ${typeof packet}`); } } private onConnect(): void { this.resetBuffer(); this.sendRC4.reset(); this.receiveRC4.reset(); } private onData(data: Buffer): void { for (const byte of data) { // reconnecting to the nexus causes a 'buffer' byte to be sent // which should be skipped. if (this.reader.index === 0 && byte === 255) { continue; } this.checkBuffer(); this.reader.buffer[this.reader.index++] = byte; } this.checkBuffer(); } private constructPacket(): Packet { this.receiveRC4.cipher(this.reader.buffer.slice(5, this.reader.length)); let packet: Packet; try { const id = this.reader.buffer.readInt8(4); const type = Mapper.map.get(id); if (!type) { throw new Error(`Mapper is missing a packet type for the id ${id}`); } if (this.listenerCount(type) !== 0) { packet = Packets.create(type) as Packet; } else { Logger.log(this.logName, `No listeners for packet type ${type}. Not emitting.`, LogLevel.Debug); } } catch (error) { Logger.log(this.logName, error.message, LogLevel.Error); this.emitError(error); } if (packet) { try { this.reader.index = 5; packet.read(this.reader); } catch (error) { Logger.log(this.logName, `Error while reading ${packet.type}`, LogLevel.Error); Logger.log(this.logName, error.message, LogLevel.Error); this.emitError(new Error('Invalid packet structure.')); this.resetBuffer(); return null; } Logger.log(this.logName, `READ: ${packet.type}, size: ${this.reader.length}`, LogLevel.Debug); } this.resetBuffer(); return packet; } private checkBuffer() { if (this.reader.remaining === 0) { this.reader.index = 0; if (this.reader.length === 5) { const size = this.reader.readInt32(); this.reader.index++; // skip the id. this.reader.resizeBuffer(size); } else { const packet = this.constructPacket(); if (packet) { this.emitPacket(packet); } } } } private resetBuffer(): void { this.reader.resizeBuffer(5); this.reader.index = 0; } private emitError(error: Error): void { if (this.listenerCount('error') === 0) { throw error; } else { this.emit('error', error); } } }