import { ConnectionPriority, InetAddress, Protocol, RakNetSession } from '@jsprismarine/raknet'; import Dgram, { Socket } from 'dgram'; import { Protocol as JSPProtocol, Logger } from '@jsprismarine/prismarine'; import { clearIntervalAsync, setIntervalAsync } from 'set-interval-async/dynamic'; import Crypto from 'crypto'; import { EventEmitter } from 'events'; import { RakNetPriority } from '@jsprismarine/raknet/src/Session'; // https://stackoverflow.com/a/1527820/3142553 const getRandomInt = (min: number, max: number) => { min = Math.ceil(min); max = Math.floor(max); return Math.floor(Math.random() * (max - min + 1)) + min; }; // Minecraft related protocol const PROTOCOL = 10; // Max net transfer unit const DEF_MTU_SIZE = 1455; // Raknet ticks const RAKNET_TPS = 100; const RAKNET_TICK_LENGTH = 1 / RAKNET_TPS; export default class Client extends EventEmitter { private clientGUID = Crypto.randomBytes(8).readBigInt64BE(); private readonly logger = new Logger(); private readonly address: InetAddress; private targetAddress!: InetAddress; private connection: RakNetSession | null = null; private readonly socket = Dgram.createSocket({ type: 'udp4' }); private get closed() { return false; } private connecting = false; private connected = false; private offlineHandled = false; private loginHandled = false; public constructor() { super(); this.address = new InetAddress('0.0.0.0', getRandomInt(46000, 49999)); this.socket.bind(this.address.getPort(), this.address.getAddress()); } /** * Creates a packet listener on given address and port. */ public async connect(address = '0.0.0.0', port = 19132) { this.targetAddress = new InetAddress(address, port); this.socket.on('message', (buffer: Buffer) => { void this.handle(buffer); }); if (this.connection) throw new Error('Already connected/connecting to server.'); this.logger.info('JSPrismarine client is now attempting to connect...'); const timer = setIntervalAsync(async () => { if (this.closed) { await clearIntervalAsync(timer); return; } // Send a client packet to the server // so the server goes in target mode // and the login process starts if (!this.connecting) { const pk = new Protocol.UnconnectedPing(); pk.timestamp = BigInt(Date.now()); // TODO: can be omitted... pk. = this.clientGUID; pk.encode(); await this.sendBuffer(pk.getBuffer()); } if (this.connected && !this.loginHandled) { const pk = new JSPProtocol.Packets.LoginPacket(); pk.encode(); const sendPk = new Protocol.Frame(); sendPk.reliability = 0; sendPk.content = pk.getBuffer(); this.connection!.sendFrame(sendPk, ConnectionPriority.NORMAL); // Packet needs to be splitted this.loginHandled = true; } this.connection?.update(Date.now()); }, RAKNET_TICK_LENGTH * 1000); return this; } private async handle(buffer: Buffer) { const header = buffer.readUInt8(); // Read packet header if (this.connection && this.offlineHandled) { return this.connection.handle(buffer); } let buf; switch (header) { case Protocol.MessageHeaders.UNCONNECTED_PONG: buf = this.handleUnconnectedPong(buffer); await this.sendBuffer(buf); break; case Protocol.MessageHeaders.OPEN_CONNECTION_REPLY_1: buf = this.handleOpenConnectionReply1(buffer); await this.sendBuffer(buf); break; case Protocol.MessageHeaders.OPEN_CONNECTION_REPLY_2: this.handleOpenConnectionReply2(buffer); break; default: this.logger.warn(`Unhandled offline packet ID: ${header}`, 'Client/handle'); } } public handleUnconnectedPong(buffer: Buffer) { // Decode server packet const decodedPacket = new Protocol.UnconnectedPong(buffer); decodedPacket.decode(); // Check packet validity // To refactor if (!decodedPacket.isValid()) { throw new Error('Received an invalid offline message'); } // Encode response const packet = new Protocol.OpenConnectionRequest1(); packet.protocol = PROTOCOL; packet.mtuSize = DEF_MTU_SIZE; packet.encode(); // Update session status // this.status = ConnectionStatus.Targetted; return packet.getBuffer(); } public handleOpenConnectionReply1(buffer: Buffer) { // Decode server packet const decodedPacket = new Protocol.OpenConnectionReply1(buffer); decodedPacket.decode(); // Check packet validity // To refactor if (!decodedPacket.isValid()) { throw new Error('Received an invalid offline message'); } // Encode response const packet = new Protocol.OpenConnectionRequest2(); packet.serverAddress = this.targetAddress; packet.mtuSize = DEF_MTU_SIZE; packet.clientGUID = this.clientGUID; packet.encode(); // Update session status this.connecting = true; // This.status = ConnectionStatus.Connected; this.connection = new RakNetSession(this as any, DEF_MTU_SIZE, { address: this.address.getAddress(), port: this.address.getPort(), family: 'IPv4', size: 0 }); return packet.getBuffer(); } public handleOpenConnectionReply2(buffer: Buffer) { // Decode server packet const decodedPacket = new Protocol.OpenConnectionReply2(buffer); decodedPacket.decode(); // Check packet validity // To refactor if (!decodedPacket.isValid()) { throw new Error('Received an invalid offline message'); } // Encode response (encapsulated) const packet = new Protocol.ConnectionRequest(); packet.clientGUID = this.clientGUID; packet.requestTimestamp = BigInt(Date.now()); packet.encode(); const sendPacket = new Protocol.Frame(); sendPacket.reliability = 0; sendPacket.content = packet.getBuffer(); this.connection?.sendFrame(sendPacket, RakNetPriority.NORMAL); this.offlineHandled = true; this.connected = true; // Should be... we can't rely on it } /** * Sends the buffer to the server * * @param buffer * @param address * @param port */ public async sendBuffer(buffer: Buffer): Promise<void> { this.socket.send(buffer, 0, buffer.byteLength, this.targetAddress.getPort(), this.targetAddress.getAddress()); } public getSocket(): Socket { return this.socket; } public getAddress(): InetAddress { return this.address; } public async removeConnection(connection: RakNetSession, reason?: string): Promise<void> { throw new Error('Method not implemented.'); } }