From aaca19cedd456853bb72a100ba85e5e6e032d811 Mon Sep 17 00:00:00 2001 From: q13x <84165981+WorldEditAxe@users.noreply.github.com> Date: Mon, 3 Jul 2023 00:24:45 -0700 Subject: [PATCH] add offline server support to EagProxyAAS --- README.md | 3 +- src/plugins/EagProxyAAS/config.ts | 7 +- src/plugins/EagProxyAAS/types.ts | 77 ++-- src/plugins/EagProxyAAS/utils.ts | 400 +++++++++++++++----- src/proxy/Player.ts | 610 +++++++++++++++++------------- 5 files changed, 705 insertions(+), 392 deletions(-) diff --git a/README.md b/README.md index d621d76..61ff420 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ As of right now, there only exists one plugin: EagProxyAAS (read below for more EagProxyAAS aims to allow any Eaglercraft client to connect to a normal 1.8.9 Minecraft server (includes Hypixel), provided that players own a legitimate Minecraft Java copy. -_Demo server: `wss://eaglerproxy.q13x.com/` (not being hosted w/ Replit due to data transfer limits)_ +_Demo server: `wss://eaglerproxy.q13x.com/` (not being hosted w/ Replit due to data transfer limits)_ #### I don't want to use this plugin! @@ -48,7 +48,6 @@ EaglerProxy and EagProxyAAS: EaglerProxy and EagProxyAAS does NOT: - include any Microsoft/Mojang code, -- ship with offline (cracked) support by default, - store or otherwise use authentication data for any other purpose as listed on the README, - Unmodified versions will not maliciously handle your login data, although a modified version has the ability to do so. Only use trusted and unmodified versions of both this plugin and proxy. - and intentionally put your account at risk. diff --git a/src/plugins/EagProxyAAS/config.ts b/src/plugins/EagProxyAAS/config.ts index ad1bdca..c3a2951 100644 --- a/src/plugins/EagProxyAAS/config.ts +++ b/src/plugins/EagProxyAAS/config.ts @@ -1,4 +1,5 @@ export const config = { - bindInternalServerPort: 25569, - bindInternalServerIp: "127.0.0.1" -} \ No newline at end of file + bindInternalServerPort: 25569, + bindInternalServerIp: "127.0.0.1", + allowCustomPorts: false, +}; diff --git a/src/plugins/EagProxyAAS/types.ts b/src/plugins/EagProxyAAS/types.ts index cf1eee2..4bdfd1f 100644 --- a/src/plugins/EagProxyAAS/types.ts +++ b/src/plugins/EagProxyAAS/types.ts @@ -1,45 +1,50 @@ -import { Client, Server } from "minecraft-protocol" +import { Client, Server } from "minecraft-protocol"; export type ServerGlobals = { - server: Server, - players: Map -} + server: Server; + players: Map; +}; export type ClientState = { - state: ConnectionState, - gameClient: Client, - token?: string, - lastStatusUpdate: number -} + state: ConnectionState; + gameClient: Client; + token?: string; + lastStatusUpdate: number; +}; export enum ConnectionState { - AUTH, - SUCCESS, - DISCONNECTED + AUTH, + SUCCESS, + DISCONNECTED, } export enum ChatColor { - BLACK = "§0", - DARK_BLUE = "§1", - DARK_GREEN = "§2", - DARK_CYAN = "§3", - DARK_RED = "§4", - PURPLE = "§5", - GOLD = "§6", - GRAY = "§7", - DARK_GRAY = "§8", - BLUE = "§9", - BRIGHT_GREEN = "§a", - CYAN = "§b", - RED = "§c", - PINK = "§d", - YELLOW = "§e", - WHITE = "§f", - // text styling - OBFUSCATED = '§k', - BOLD = '§l', - STRIKETHROUGH = '§m', - UNDERLINED = '§n', - ITALIC = '§o', - RESET = '§r' -} \ No newline at end of file + BLACK = "§0", + DARK_BLUE = "§1", + DARK_GREEN = "§2", + DARK_CYAN = "§3", + DARK_RED = "§4", + PURPLE = "§5", + GOLD = "§6", + GRAY = "§7", + DARK_GRAY = "§8", + BLUE = "§9", + BRIGHT_GREEN = "§a", + CYAN = "§b", + RED = "§c", + PINK = "§d", + YELLOW = "§e", + WHITE = "§f", + // text styling + OBFUSCATED = "§k", + BOLD = "§l", + STRIKETHROUGH = "§m", + UNDERLINED = "§n", + ITALIC = "§o", + RESET = "§r", +} + +export enum ConnectType { + ONLINE, + OFFLINE, +} diff --git a/src/plugins/EagProxyAAS/utils.ts b/src/plugins/EagProxyAAS/utils.ts index 1e9c959..01a88d9 100644 --- a/src/plugins/EagProxyAAS/utils.ts +++ b/src/plugins/EagProxyAAS/utils.ts @@ -1,4 +1,4 @@ -import { ServerGlobals } from "./types.js"; +import { ConnectType, ServerGlobals } from "./types.js"; import * as Chunk from "prismarine-chunk"; import * as Block from "prismarine-block"; import * as Registry from "prismarine-registry"; @@ -6,6 +6,7 @@ import vec3 from "vec3"; import { Client } from "minecraft-protocol"; import { ClientState, ConnectionState } from "./types.js"; import { auth, ServerDeviceCodeResponse } from "./auth.js"; +import { config } from "./config.js"; const { Vec3 } = vec3 as any; const Enums = PLUGIN_MANAGER.Enums; @@ -108,6 +109,33 @@ export function sendMessage(client: Client, msg: string) { }); } +export function sendCustomMessage( + client: Client, + msg: string, + color: string, + ...components: { text: string; color: string }[] +) { + client.write("chat", { + message: JSON.stringify( + components.length > 0 + ? { + text: msg, + color, + extra: components, + } + : { text: msg, color } + ), + position: 1, + }); +} + +export function sendChatComponent(client: Client, component: any) { + client.write("chat", { + message: JSON.stringify(component), + position: 1, + }); +} + export function sendMessageWarning(client: Client, msg: string) { client.write("chat", { message: JSON.stringify({ @@ -144,9 +172,7 @@ export function sendMessageLogin(client: Client, url: string, token: string) { color: "gold", hoverEvent: { action: "show_text", - value: - Enums.ChatColor.GOLD + - "Click me to copy to chat to copy from there!", + value: Enums.ChatColor.GOLD + "Click me to copy to chat!", }, clickEvent: { action: "suggest_command", @@ -164,11 +190,21 @@ export function sendMessageLogin(client: Client, url: string, token: string) { export function updateState( client: Client, - newState: "AUTH" | "SERVER", + newState: "CONNECTION_TYPE" | "AUTH" | "SERVER", uri?: string, code?: string ) { switch (newState) { + case "CONNECTION_TYPE": + client.write("playerlist_header", { + header: JSON.stringify({ + text: ` ${Enums.ChatColor.GOLD}EaglerProxy Authentication Server `, + }), + footer: JSON.stringify({ + text: `${Enums.ChatColor.RED}Choose the connection type: 1 = online, 2 = offline.`, + }), + }); + break; case "AUTH": if (code == null || uri == null) throw new Error( @@ -189,7 +225,9 @@ export function updateState( text: ` ${Enums.ChatColor.GOLD}EaglerProxy Authentication Server `, }), footer: JSON.stringify({ - text: `${Enums.ChatColor.RED}/join [port]`, + text: `${Enums.ChatColor.RED}/join ${ + config.allowCustomPorts ? " [port]" : "" + }`, }), }); break; @@ -206,108 +244,284 @@ export async function onConnect(client: ClientState) { `WARNING: This proxy allows you to connect to any 1.8.9 server. Gameplay has shown no major issues, but please note that EaglercraftX may flag some anticheats while playing.` ); await new Promise((res) => setTimeout(res, 2000)); + sendMessageWarning( client.gameClient, `WARNING: It is highly suggested that you turn down settings, as gameplay tends to be very laggy and unplayable on low powered devices.` ); await new Promise((res) => setTimeout(res, 2000)); - sendMessageWarning( - client.gameClient, - `WARNING: You will be prompted to log in via Microsoft to obtain a session token necessary to join games. Any data related to your account will not be saved and for transparency reasons this proxy's source code is available on Github.` - ); - await new Promise((res) => setTimeout(res, 2000)); - client.lastStatusUpdate = Date.now(); - let errored = false, - savedAuth; - const authHandler = auth(), - codeCallback = (code: ServerDeviceCodeResponse) => { - updateState( - client.gameClient, - "AUTH", - code.verification_uri, - code.user_code - ); - sendMessageLogin( - client.gameClient, - code.verification_uri, - code.user_code - ); - }; - authHandler.once("error", (err) => { - if (!client.gameClient.ended) client.gameClient.end(err.message); - errored = true; + sendCustomMessage(client.gameClient, "What would you like to do?", "gray"); + sendChatComponent(client.gameClient, { + text: "1) ", + color: "gold", + extra: [ + { + text: "Connect to an online server (Minecraft account needed)", + color: "white", + }, + ], + hoverEvent: { + action: "show_text", + value: Enums.ChatColor.GOLD + "Click me to select!", + }, + clickEvent: { + action: "run_command", + value: "1", + }, }); - if (errored) return; - authHandler.on("code", codeCallback); - await new Promise((res) => - authHandler.once("done", (result) => { - savedAuth = result; - res(result); - }) - ); - sendMessage( + sendChatComponent(client.gameClient, { + text: "2) ", + color: "gold", + extra: [ + { + text: "Connect to an offline server (no Minecraft account needed)", + color: "white", + }, + ], + hoverEvent: { + action: "show_text", + value: Enums.ChatColor.GOLD + "Click me to select!", + }, + clickEvent: { + action: "run_command", + value: "2", + }, + }); + sendCustomMessage( client.gameClient, - Enums.ChatColor.BRIGHT_GREEN + "Successfully logged into Minecraft!" + "Select an option from the above (1 = online, 2 = offline), either by clicking or manually typing out the option.", + "green" ); - client.state = ConnectionState.SUCCESS; - client.lastStatusUpdate = Date.now(); - updateState(client.gameClient, "SERVER"); - sendMessage( - client.gameClient, - `Provide a server to join. ${Enums.ChatColor.GOLD}/join [port]${Enums.ChatColor.RESET}.` - ); - let host: string, port: number; + let chosenOption: ConnectType | null = null; while (true) { - const msg = await awaitCommand(client.gameClient, (msg) => - msg.startsWith("/join") - ), - parsed = msg.split(/ /gi, 3); - if (parsed.length < 2) - sendMessage( - client.gameClient, - `Please provide a server to connect to. ${Enums.ChatColor.GOLD}/join [port]${Enums.ChatColor.RESET}.` - ); - else if (parsed.length > 3 && isNaN(parseInt(parsed[2]))) - sendMessage( - client.gameClient, - `A valid port number has to be passed! ${Enums.ChatColor.GOLD}/join [port]${Enums.ChatColor.RESET}.` - ); - else { - host = parsed[1]; - if (parsed.length > 3) port = parseInt(parsed[2]); - port = port ?? 25565; - break; + const option = await awaitCommand(client.gameClient, (msg) => true); + switch (option) { + default: + sendCustomMessage( + client.gameClient, + `I don't understand what you meant by "${option}", please reply with a valid option!`, + "red" + ); + case "1": + chosenOption = ConnectType.ONLINE; + break; + case "2": + chosenOption = ConnectType.OFFLINE; + break; } + if (chosenOption != null) break; } - try { - await PLUGIN_MANAGER.proxy.players - .get(client.gameClient.username) - .switchServers({ - host: host, - port: port, - version: "1.8.8", - username: savedAuth.selectedProfile.name, - auth: "mojang", - keepAlive: false, - session: { - accessToken: savedAuth.accessToken, - clientToken: savedAuth.selectedProfile.id, - selectedProfile: { - id: savedAuth.selectedProfile.id, - name: savedAuth.selectedProfile.name, + + if (chosenOption == ConnectType.ONLINE) { + sendMessageWarning( + client.gameClient, + `You will be prompted to log in via Microsoft to obtain a session token necessary to join games. Any data related to your account will not be saved and for transparency reasons this proxy's source code is available on Github.` + ); + await new Promise((res) => setTimeout(res, 2000)); + + client.lastStatusUpdate = Date.now(); + let errored = false, + savedAuth; + const authHandler = auth(), + codeCallback = (code: ServerDeviceCodeResponse) => { + updateState( + client.gameClient, + "AUTH", + code.verification_uri, + code.user_code + ); + sendMessageLogin( + client.gameClient, + code.verification_uri, + code.user_code + ); + }; + authHandler.once("error", (err) => { + if (!client.gameClient.ended) client.gameClient.end(err.message); + errored = true; + }); + if (errored) return; + authHandler.on("code", codeCallback); + await new Promise((res) => + authHandler.once("done", (result) => { + savedAuth = result; + res(result); + }) + ); + sendMessage( + client.gameClient, + Enums.ChatColor.BRIGHT_GREEN + "Successfully logged into Minecraft!" + ); + + client.state = ConnectionState.SUCCESS; + client.lastStatusUpdate = Date.now(); + updateState(client.gameClient, "SERVER"); + sendMessage( + client.gameClient, + `Provide a server to join. ${Enums.ChatColor.GOLD}/join ${ + config.allowCustomPorts ? " [port]" : "" + }${Enums.ChatColor.RESET}.` + ); + let host: string, port: number; + while (true) { + const msg = await awaitCommand(client.gameClient, (msg) => + msg.startsWith("/join") + ), + parsed = msg.split(/ /gi, 3); + if (parsed.length < 2) + sendMessage( + client.gameClient, + `Please provide a server to connect to. ${ + Enums.ChatColor.GOLD + }/join ${config.allowCustomPorts ? " [port]" : ""}${ + Enums.ChatColor.RESET + }.` + ); + else if (parsed.length > 2 && isNaN(parseInt(parsed[2]))) + sendMessage( + client.gameClient, + `A valid port number has to be passed! ${ + Enums.ChatColor.GOLD + }/join ${config.allowCustomPorts ? " [port]" : ""}${ + Enums.ChatColor.RESET + }.` + ); + else { + host = parsed[1]; + if (parsed.length > 2) port = parseInt(parsed[2]); + if (port != null && !config.allowCustomPorts) { + sendCustomMessage( + client.gameClient, + "You are not allowed to use custom server ports! /join ", + "red" + ); + host = null; + port = null; + } else { + port = port ?? 25565; + break; + } + } + } + try { + await PLUGIN_MANAGER.proxy.players + .get(client.gameClient.username) + .switchServers({ + host: host, + port: port, + version: "1.8.8", + username: savedAuth.selectedProfile.name, + auth: "mojang", + keepAlive: false, + session: { + accessToken: savedAuth.accessToken, + clientToken: savedAuth.selectedProfile.id, + selectedProfile: { + id: savedAuth.selectedProfile.id, + name: savedAuth.selectedProfile.name, + }, }, - }, - skipValidation: true, - hideErrors: true, - }); - } catch (err) { - if (!client.gameClient.ended) { - client.gameClient.end( - Enums.ChatColor.RED + - `Something went wrong whilst switching servers: ${err.message}` + skipValidation: true, + hideErrors: true, + }); + } catch (err) { + if (!client.gameClient.ended) { + client.gameClient.end( + Enums.ChatColor.RED + + `Something went wrong whilst switching servers: ${err.message}${ + err.code == "ENOTFOUND" + ? host.includes(":") + ? `\n${Enums.ChatColor.GRAY}Suggestion: Replace the : in your IP with a space.` + : "\nIs that IP valid?" + : "" + }` + ); + } + } + } else { + client.state = ConnectionState.SUCCESS; + client.lastStatusUpdate = Date.now(); + updateState(client.gameClient, "SERVER"); + sendMessage( + client.gameClient, + `Provide a server to join. ${Enums.ChatColor.GOLD}/join ${ + config.allowCustomPorts ? " [port]" : "" + }${Enums.ChatColor.RESET}.` + ); + let host: string, port: number; + while (true) { + const msg = await awaitCommand(client.gameClient, (msg) => + msg.startsWith("/join") + ), + parsed = msg.split(/ /gi, 3); + if (parsed.length < 2) + sendMessage( + client.gameClient, + `Please provide a server to connect to. ${ + Enums.ChatColor.GOLD + }/join ${config.allowCustomPorts ? " [port]" : ""}${ + Enums.ChatColor.RESET + }.` + ); + else if (parsed.length > 2 && isNaN(parseInt(parsed[2]))) + sendMessage( + client.gameClient, + `A valid port number has to be passed! ${ + Enums.ChatColor.GOLD + }/join ${config.allowCustomPorts ? " [port]" : ""}${ + Enums.ChatColor.RESET + }.` + ); + else { + host = parsed[1]; + if (parsed.length > 2) port = parseInt(parsed[2]); + if (port != null && !config.allowCustomPorts) { + sendCustomMessage( + client.gameClient, + "You are not allowed to use custom server ports! /join ", + "red" + ); + host = null; + port = null; + } else { + port = port ?? 25565; + break; + } + } + } + try { + sendCustomMessage( + client.gameClient, + "Attempting to switch servers, please wait... (if you don't get connected to the target server for a while, the server might be online only)", + "gray" ); + await PLUGIN_MANAGER.proxy.players + .get(client.gameClient.username) + .switchServers({ + host: host, + port: port, + version: "1.8.8", + username: client.gameClient.username, + auth: "offline", + keepAlive: false, + skipValidation: true, + hideErrors: true, + }); + } catch (err) { + if (!client.gameClient.ended) { + client.gameClient.end( + Enums.ChatColor.RED + + `Something went wrong whilst switching servers: ${err.message}${ + err.code == "ENOTFOUND" + ? host.includes(":") + ? `\n${Enums.ChatColor.GRAY}Suggestion: Replace the : in your IP with a space.` + : "\nIs that IP valid?" + : "" + }` + ); + } } } } catch (err) { diff --git a/src/proxy/Player.ts b/src/proxy/Player.ts index de02cdf..8f849d8 100644 --- a/src/proxy/Player.ts +++ b/src/proxy/Player.ts @@ -1,276 +1,370 @@ -import EventEmitter from "events" -import pkg, { Client, ClientOptions, createClient, states } from "minecraft-protocol" -import { WebSocket } from "ws" -import { Logger } from "../logger.js" -import { Chat } from "./Chat.js" -import { Constants } from "./Constants.js" -import { Enums } from "./Enums.js" -import Packet from "./Packet.js" -import SCDisconnectPacket from "./packets/SCDisconnectPacket.js" -import { MineProtocol } from "./Protocol.js" -import { EaglerSkins } from "./skins/EaglerSkins.js" -import { Util } from "./Util.js" -import { BungeeUtil } from "./BungeeUtil.js" +import EventEmitter from "events"; +import pkg, { + Client, + ClientOptions, + createClient, + states, +} from "minecraft-protocol"; +import { WebSocket } from "ws"; +import { Logger } from "../logger.js"; +import { Chat } from "./Chat.js"; +import { Constants } from "./Constants.js"; +import { Enums } from "./Enums.js"; +import Packet from "./Packet.js"; +import SCDisconnectPacket from "./packets/SCDisconnectPacket.js"; +import { MineProtocol } from "./Protocol.js"; +import { EaglerSkins } from "./skins/EaglerSkins.js"; +import { Util } from "./Util.js"; +import { BungeeUtil } from "./BungeeUtil.js"; -const { createSerializer, createDeserializer } = pkg +const { createSerializer, createDeserializer } = pkg; export class Player extends EventEmitter { - public ws: WebSocket - public username?: string - public skin?: EaglerSkins.EaglerSkin - public uuid?: string - public state?: Enums.ClientState = Enums.ClientState.PRE_HANDSHAKE - public serverConnection?: Client + public ws: WebSocket; + public username?: string; + public skin?: EaglerSkins.EaglerSkin; + public uuid?: string; + public state?: Enums.ClientState = Enums.ClientState.PRE_HANDSHAKE; + public serverConnection?: Client; - private _switchingServers: boolean = false - private _logger: Logger - private _alreadyConnected: boolean = false - private _serializer: any - private _deserializer: any - private _kickMessage: string - - constructor(ws: WebSocket, playerName?: string, serverConnection?: Client) { - super() - this._logger = new Logger(`PlayerHandler-${playerName}`) - this.ws = ws - this.username = playerName - this.serverConnection = serverConnection - if (this.username != null) this.uuid = Util.generateUUIDFromPlayer(this.username) - this._serializer = createSerializer({ - state: states.PLAY, - isServer: true, - version: "1.8.9", - customPackets: null - }) - this._deserializer = createDeserializer({ - state: states.PLAY, - isServer: false, - version: "1.8.9", - customPackets: null - }) - // this._serializer.pipe(this.ws) - } + private _switchingServers: boolean = false; + private _logger: Logger; + private _alreadyConnected: boolean = false; + private _serializer: any; + private _deserializer: any; + private _kickMessage: string; - public initListeners() { - this.ws.on('close', () => { - this.state = Enums.ClientState.DISCONNECTED - if (this.serverConnection) this.serverConnection.end() - this.emit('disconnect', this) - }) - this.ws.on('message', (msg: Buffer) => { - if (msg instanceof Buffer == false) return - const decoder = PACKET_REGISTRY.get(msg[0]) - if (decoder && decoder.sentAfterHandshake) { - if (!decoder && this.state != Enums.ClientState.POST_HANDSHAKE && msg.length >= 1) { - this._logger.warn(`Packet with ID 0x${Buffer.from([msg[0]]).toString('hex')} is missing a corresponding packet handler! Processing for this packet will be skipped.`) - } else { - let parsed: Packet, err: boolean - try { - parsed = new decoder.class() - parsed.deserialize(msg) - } catch (err) { - if (this.state != Enums.ClientState.POST_HANDSHAKE) this._logger.warn(`Packet ID 0x${Buffer.from([msg[0]]).toString('hex')} failed to parse! The packet will be skipped.`) - err = true - } - if (!err) { - this.emit('proxyPacket', parsed, this) - return - } - } - } - }) - } + constructor(ws: WebSocket, playerName?: string, serverConnection?: Client) { + super(); + this._logger = new Logger(`PlayerHandler-${playerName}`); + this.ws = ws; + this.username = playerName; + this.serverConnection = serverConnection; + if (this.username != null) + this.uuid = Util.generateUUIDFromPlayer(this.username); + this._serializer = createSerializer({ + state: states.PLAY, + isServer: true, + version: "1.8.9", + customPackets: null, + }); + this._deserializer = createDeserializer({ + state: states.PLAY, + isServer: false, + version: "1.8.9", + customPackets: null, + }); + // this._serializer.pipe(this.ws) + } - public write(packet: Packet) { - this.ws.send(packet.serialize()) - } - - public async read(packetId?: Enums.PacketId, filter?: (packet: Packet) => boolean): Promise { - let res - await Util.awaitPacket(this.ws, packet => { - if ((packetId != null && packetId == packet[0]) || (packetId == null)) { - const decoder = PACKET_REGISTRY.get(packet[0]) - if (decoder != null && decoder.packetId == packet[0] && (this.state == Enums.ClientState.PRE_HANDSHAKE || decoder.sentAfterHandshake) && decoder.boundTo == Enums.PacketBounds.S) { - let parsed: Packet, err = false - try { - parsed = new decoder.class() - parsed.deserialize(packet) - } catch (_err) { - err = true - } - if (!err) { - if (filter && filter(parsed)) { - res = parsed - return true - } else if (filter == null) { - res = parsed - return true - } - } - } - } - return false - }) - return res - } - - public disconnect(message: Chat.Chat | string) { - if (this.state == Enums.ClientState.POST_HANDSHAKE) { - this.ws.send(Buffer.concat([ - [0x40], - MineProtocol.writeString((typeof message == 'string' ? message : JSON.stringify(message))) - ].map(arr => arr instanceof Uint8Array ? arr : Buffer.from(arr)))) - this.ws.close() + public initListeners() { + this.ws.on("close", () => { + this.state = Enums.ClientState.DISCONNECTED; + if (this.serverConnection) this.serverConnection.end(); + this.emit("disconnect", this); + }); + this.ws.on("message", (msg: Buffer) => { + if (msg instanceof Buffer == false) return; + const decoder = PACKET_REGISTRY.get(msg[0]); + if (decoder && decoder.sentAfterHandshake) { + if ( + !decoder && + this.state != Enums.ClientState.POST_HANDSHAKE && + msg.length >= 1 + ) { + this._logger.warn( + `Packet with ID 0x${Buffer.from([msg[0]]).toString( + "hex" + )} is missing a corresponding packet handler! Processing for this packet will be skipped.` + ); } else { - const packet = new SCDisconnectPacket() - packet.reason = message - this.ws.send(packet.serialize()) - this.ws.close() + let parsed: Packet, err: boolean; + try { + parsed = new decoder.class(); + parsed.deserialize(msg); + } catch (err) { + if (this.state != Enums.ClientState.POST_HANDSHAKE) + this._logger.warn( + `Packet ID 0x${Buffer.from([msg[0]]).toString( + "hex" + )} failed to parse! The packet will be skipped.` + ); + err = true; + } + if (!err) { + this.emit("proxyPacket", parsed, this); + return; + } } - } + } + }); + } - public async connect(options: ClientOptions) { - if (this._alreadyConnected) - throw new Error(`Invalid state: Player has already been connected to a server, and .connect() was just called. Please use switchServers() instead.`) - this._alreadyConnected = true - this.serverConnection = createClient(Object.assign({ - version: '1.8.9', - keepAlive: false, - hideErrors: false - }, options)) - await this._bindListenersMineClient(this.serverConnection) - } + public write(packet: Packet) { + this.ws.send(packet.serialize()); + } - public switchServers(options: ClientOptions) { - if (!this._alreadyConnected) - throw new Error(`Invalid state: Player hasn't already been connected to a server, and .switchServers() has been called. Please use .connect() when initially connecting to a server, and only use .switchServers() if you want to switch servers.`) - return new Promise(async (res, rej) => { - const oldConnection = this.serverConnection - this._switchingServers = true - - this.ws.send(this._serializer.createPacketBuffer({ - name: 'chat', - params: { - message: `${Enums.ChatColor.GRAY}Switching servers...`, - position: 1 - } - })) - this.ws.send(this._serializer.createPacketBuffer({ - name: 'playerlist_header', - params: { - header: JSON.stringify({ - text: "" - }), - footer: JSON.stringify({ - text: "" - }) - } - })) - - this.serverConnection = createClient(Object.assign({ - version: '1.8.9', - keepAlive: false, - hideErrors: false - }, options)) - - await this._bindListenersMineClient(this.serverConnection, true, () => oldConnection.end()) - .then(() => { - this.emit('switchServer', this.serverConnection, this) - res() - }) - .catch(err => { - this.serverConnection = oldConnection - rej(err) - }) - }) - } - - private async _bindListenersMineClient(client: Client, switchingServers?: boolean, onSwitch?: Function) { - return new Promise((res, rej) => { - let stream = false, uuid - const listener = msg => { - if (stream) { - client.writeRaw(msg) - } - }, errListener = err => { - if (!stream) { - rej(err) - } else { - this.disconnect(`${Enums.ChatColor.RED}Something went wrong: ${err.stack ?? err}`) - } + public async read( + packetId?: Enums.PacketId, + filter?: (packet: Packet) => boolean + ): Promise { + let res; + await Util.awaitPacket(this.ws, (packet) => { + if ((packetId != null && packetId == packet[0]) || packetId == null) { + const decoder = PACKET_REGISTRY.get(packet[0]); + if ( + decoder != null && + decoder.packetId == packet[0] && + (this.state == Enums.ClientState.PRE_HANDSHAKE || + decoder.sentAfterHandshake) && + decoder.boundTo == Enums.PacketBounds.S + ) { + let parsed: Packet, + err = false; + try { + parsed = new decoder.class(); + parsed.deserialize(packet); + } catch (_err) { + err = true; + } + if (!err) { + if (filter && filter(parsed)) { + res = parsed; + return true; + } else if (filter == null) { + res = parsed; + return true; } - client.on('error', errListener) - client.on('end', reason => { - if (!this._switchingServers) this.disconnect(this._kickMessage ?? reason) - this.ws.removeListener('message', listener) - }) - client.once('connect', () => { - this.emit('joinServer', client, this) - }) - client.on('packet', (packet, meta) => { - if (meta.name == 'kick_disconnect') { - let json - try { json = JSON.parse(packet.reason) } - catch {} - if (json != null) { - this._kickMessage = Chat.chatToPlainString(json) - } else this._kickMessage = packet.reason - } - if (!stream) { - if (switchingServers) { - if (meta.name == 'login' && meta.state == states.PLAY && uuid) { - const pckSeq = BungeeUtil.getRespawnSequence(packet, this._serializer) - this.ws.send(this._serializer.createPacketBuffer({ - name: "login", - params: packet - })) - pckSeq.forEach(p => this.ws.send(p)) - stream = true - if (onSwitch) onSwitch() - res(null) - } else if (meta.name == 'success' && meta.state == states.LOGIN && !uuid) { - uuid = packet.uuid - } - } else { - if (meta.name == 'login' && meta.state == states.PLAY && uuid) { - this.ws.send(this._serializer.createPacketBuffer({ - name: "login", - params: packet - })) - stream = true - if (onSwitch) onSwitch() - res(null) - } else if (meta.name == 'success' && meta.state == states.LOGIN && !uuid) { - uuid = packet.uuid - } - } - } else { - this.ws.send(this._serializer.createPacketBuffer({ - name: meta.name, - params: packet - })) - } - }) - this.ws.on('message', listener) - }) + } + } + } + return false; + }); + return res; + } + + public disconnect(message: Chat.Chat | string) { + if (this.state == Enums.ClientState.POST_HANDSHAKE) { + this.ws.send( + Buffer.concat( + [ + [0x40], + MineProtocol.writeString( + typeof message == "string" ? message : JSON.stringify(message) + ), + ].map((arr) => (arr instanceof Uint8Array ? arr : Buffer.from(arr))) + ) + ); + this.ws.close(); + } else { + const packet = new SCDisconnectPacket(); + packet.reason = message; + this.ws.send(packet.serialize()); + this.ws.close(); } + } + + public async connect(options: ClientOptions) { + if (this._alreadyConnected) + throw new Error( + `Invalid state: Player has already been connected to a server, and .connect() was just called. Please use switchServers() instead.` + ); + this._alreadyConnected = true; + this.serverConnection = createClient( + Object.assign( + { + version: "1.8.9", + keepAlive: false, + hideErrors: false, + }, + options + ) + ); + await this._bindListenersMineClient(this.serverConnection); + } + + public switchServers(options: ClientOptions) { + if (!this._alreadyConnected) + throw new Error( + `Invalid state: Player hasn't already been connected to a server, and .switchServers() has been called. Please use .connect() when initially connecting to a server, and only use .switchServers() if you want to switch servers.` + ); + return new Promise(async (res, rej) => { + const oldConnection = this.serverConnection; + this._switchingServers = true; + + this.ws.send( + this._serializer.createPacketBuffer({ + name: "chat", + params: { + message: `${Enums.ChatColor.GRAY}Switching servers...`, + position: 1, + }, + }) + ); + this.ws.send( + this._serializer.createPacketBuffer({ + name: "playerlist_header", + params: { + header: JSON.stringify({ + text: "", + }), + footer: JSON.stringify({ + text: "", + }), + }, + }) + ); + + this.serverConnection = createClient( + Object.assign( + { + version: "1.8.9", + keepAlive: false, + hideErrors: false, + }, + options + ) + ); + + await this._bindListenersMineClient(this.serverConnection, true, () => + oldConnection.end() + ) + .then(() => { + this.emit("switchServer", this.serverConnection, this); + res(); + }) + .catch((err) => { + this.serverConnection = oldConnection; + rej(err); + }); + }); + } + + private async _bindListenersMineClient( + client: Client, + switchingServers?: boolean, + onSwitch?: Function + ) { + return new Promise((res, rej) => { + let stream = false, + uuid; + const listener = (msg) => { + if (stream) { + client.writeRaw(msg); + } + }, + errListener = (err) => { + if (!stream) { + rej(err); + } else { + this.disconnect( + `${Enums.ChatColor.RED}Something went wrong: ${err.stack ?? err}` + ); + } + }; + setTimeout(() => { + if (!stream) { + client.end("Timed out waiting for server connection."); + this.disconnect( + Enums.ChatColor.RED + "Timed out waiting for server connection!" + ); + throw new Error("Timed out waiting for server connection!"); + } + }, 30000); + client.on("error", errListener); + client.on("end", (reason) => { + if (!this._switchingServers) + this.disconnect(this._kickMessage ?? reason); + this.ws.removeListener("message", listener); + }); + client.once("connect", () => { + this.emit("joinServer", client, this); + }); + client.on("packet", (packet, meta) => { + if (meta.name == "kick_disconnect") { + let json; + try { + json = JSON.parse(packet.reason); + } catch {} + if (json != null) { + this._kickMessage = Chat.chatToPlainString(json); + } else this._kickMessage = packet.reason; + } + if (!stream) { + if (switchingServers) { + if (meta.name == "login" && meta.state == states.PLAY && uuid) { + const pckSeq = BungeeUtil.getRespawnSequence( + packet, + this._serializer + ); + this.ws.send( + this._serializer.createPacketBuffer({ + name: "login", + params: packet, + }) + ); + pckSeq.forEach((p) => this.ws.send(p)); + stream = true; + if (onSwitch) onSwitch(); + res(null); + } else if ( + meta.name == "success" && + meta.state == states.LOGIN && + !uuid + ) { + uuid = packet.uuid; + } + } else { + if (meta.name == "login" && meta.state == states.PLAY && uuid) { + this.ws.send( + this._serializer.createPacketBuffer({ + name: "login", + params: packet, + }) + ); + stream = true; + if (onSwitch) onSwitch(); + res(null); + } else if ( + meta.name == "success" && + meta.state == states.LOGIN && + !uuid + ) { + uuid = packet.uuid; + } + } + } else { + this.ws.send( + this._serializer.createPacketBuffer({ + name: meta.name, + params: packet, + }) + ); + } + }); + this.ws.on("message", listener); + }); + } } interface PlayerEvents { - 'switchServer': (connection: Client, player: Player) => void, - 'joinServer': (connection: Client, player: Player) => void, - // for vanilla game packets, bind to connection object instead - 'proxyPacket': (packet: Packet, player: Player) => void, - 'vanillaPacket': (packet: Packet & { cancel: boolean }, origin: 'CLIENT' | 'SERVER', player: Player) => Packet & { cancel: boolean }, - 'disconnect': (player: Player) => void + switchServer: (connection: Client, player: Player) => void; + joinServer: (connection: Client, player: Player) => void; + // for vanilla game packets, bind to connection object instead + proxyPacket: (packet: Packet, player: Player) => void; + vanillaPacket: ( + packet: Packet & { cancel: boolean }, + origin: "CLIENT" | "SERVER", + player: Player + ) => Packet & { cancel: boolean }; + disconnect: (player: Player) => void; } export declare interface Player { - on( - event: U, listener: PlayerEvents[U] - ): this; - - emit( - event: U, ...args: Parameters - ): boolean; -} \ No newline at end of file + on(event: U, listener: PlayerEvents[U]): this; + + emit( + event: U, + ...args: Parameters + ): boolean; +}