add offline server support to EagProxyAAS

This commit is contained in:
q13x 2023-07-03 00:24:45 -07:00
parent 4915b182a8
commit aaca19cedd
5 changed files with 705 additions and 392 deletions

View File

@ -48,7 +48,6 @@ EaglerProxy and EagProxyAAS:
EaglerProxy and EagProxyAAS does NOT: EaglerProxy and EagProxyAAS does NOT:
- include any Microsoft/Mojang code, - 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, - 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. - 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. - and intentionally put your account at risk.

View File

@ -1,4 +1,5 @@
export const config = { export const config = {
bindInternalServerPort: 25569, bindInternalServerPort: 25569,
bindInternalServerIp: "127.0.0.1" bindInternalServerIp: "127.0.0.1",
} allowCustomPorts: false,
};

View File

@ -1,45 +1,50 @@
import { Client, Server } from "minecraft-protocol" import { Client, Server } from "minecraft-protocol";
export type ServerGlobals = { export type ServerGlobals = {
server: Server, server: Server;
players: Map<string, ClientState> players: Map<string, ClientState>;
} };
export type ClientState = { export type ClientState = {
state: ConnectionState, state: ConnectionState;
gameClient: Client, gameClient: Client;
token?: string, token?: string;
lastStatusUpdate: number lastStatusUpdate: number;
} };
export enum ConnectionState { export enum ConnectionState {
AUTH, AUTH,
SUCCESS, SUCCESS,
DISCONNECTED DISCONNECTED,
} }
export enum ChatColor { export enum ChatColor {
BLACK = "§0", BLACK = "§0",
DARK_BLUE = "§1", DARK_BLUE = "§1",
DARK_GREEN = "§2", DARK_GREEN = "§2",
DARK_CYAN = "§3", DARK_CYAN = "§3",
DARK_RED = "§4", DARK_RED = "§4",
PURPLE = "§5", PURPLE = "§5",
GOLD = "§6", GOLD = "§6",
GRAY = "§7", GRAY = "§7",
DARK_GRAY = "§8", DARK_GRAY = "§8",
BLUE = "§9", BLUE = "§9",
BRIGHT_GREEN = "§a", BRIGHT_GREEN = "§a",
CYAN = "§b", CYAN = "§b",
RED = "§c", RED = "§c",
PINK = "§d", PINK = "§d",
YELLOW = "§e", YELLOW = "§e",
WHITE = "§f", WHITE = "§f",
// text styling // text styling
OBFUSCATED = '§k', OBFUSCATED = "§k",
BOLD = '§l', BOLD = "§l",
STRIKETHROUGH = '§m', STRIKETHROUGH = "§m",
UNDERLINED = '§n', UNDERLINED = "§n",
ITALIC = '§o', ITALIC = "§o",
RESET = '§r' RESET = "§r",
}
export enum ConnectType {
ONLINE,
OFFLINE,
} }

View File

@ -1,4 +1,4 @@
import { ServerGlobals } from "./types.js"; import { ConnectType, ServerGlobals } from "./types.js";
import * as Chunk from "prismarine-chunk"; import * as Chunk from "prismarine-chunk";
import * as Block from "prismarine-block"; import * as Block from "prismarine-block";
import * as Registry from "prismarine-registry"; import * as Registry from "prismarine-registry";
@ -6,6 +6,7 @@ import vec3 from "vec3";
import { Client } from "minecraft-protocol"; import { Client } from "minecraft-protocol";
import { ClientState, ConnectionState } from "./types.js"; import { ClientState, ConnectionState } from "./types.js";
import { auth, ServerDeviceCodeResponse } from "./auth.js"; import { auth, ServerDeviceCodeResponse } from "./auth.js";
import { config } from "./config.js";
const { Vec3 } = vec3 as any; const { Vec3 } = vec3 as any;
const Enums = PLUGIN_MANAGER.Enums; 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) { export function sendMessageWarning(client: Client, msg: string) {
client.write("chat", { client.write("chat", {
message: JSON.stringify({ message: JSON.stringify({
@ -144,9 +172,7 @@ export function sendMessageLogin(client: Client, url: string, token: string) {
color: "gold", color: "gold",
hoverEvent: { hoverEvent: {
action: "show_text", action: "show_text",
value: value: Enums.ChatColor.GOLD + "Click me to copy to chat!",
Enums.ChatColor.GOLD +
"Click me to copy to chat to copy from there!",
}, },
clickEvent: { clickEvent: {
action: "suggest_command", action: "suggest_command",
@ -164,11 +190,21 @@ export function sendMessageLogin(client: Client, url: string, token: string) {
export function updateState( export function updateState(
client: Client, client: Client,
newState: "AUTH" | "SERVER", newState: "CONNECTION_TYPE" | "AUTH" | "SERVER",
uri?: string, uri?: string,
code?: string code?: string
) { ) {
switch (newState) { 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": case "AUTH":
if (code == null || uri == null) if (code == null || uri == null)
throw new Error( throw new Error(
@ -189,7 +225,9 @@ export function updateState(
text: ` ${Enums.ChatColor.GOLD}EaglerProxy Authentication Server `, text: ` ${Enums.ChatColor.GOLD}EaglerProxy Authentication Server `,
}), }),
footer: JSON.stringify({ footer: JSON.stringify({
text: `${Enums.ChatColor.RED}/join <ip> [port]`, text: `${Enums.ChatColor.RED}/join <ip>${
config.allowCustomPorts ? " [port]" : ""
}`,
}), }),
}); });
break; 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.` `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)); await new Promise((res) => setTimeout(res, 2000));
sendMessageWarning( sendMessageWarning(
client.gameClient, 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.` `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)); 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(); sendCustomMessage(client.gameClient, "What would you like to do?", "gray");
let errored = false, sendChatComponent(client.gameClient, {
savedAuth; text: "1) ",
const authHandler = auth(), color: "gold",
codeCallback = (code: ServerDeviceCodeResponse) => { extra: [
updateState( {
client.gameClient, text: "Connect to an online server (Minecraft account needed)",
"AUTH", color: "white",
code.verification_uri, },
code.user_code ],
); hoverEvent: {
sendMessageLogin( action: "show_text",
client.gameClient, value: Enums.ChatColor.GOLD + "Click me to select!",
code.verification_uri, },
code.user_code clickEvent: {
); action: "run_command",
}; value: "1",
authHandler.once("error", (err) => { },
if (!client.gameClient.ended) client.gameClient.end(err.message);
errored = true;
}); });
if (errored) return; sendChatComponent(client.gameClient, {
authHandler.on("code", codeCallback); text: "2) ",
await new Promise((res) => color: "gold",
authHandler.once("done", (result) => { extra: [
savedAuth = result; {
res(result); text: "Connect to an offline server (no Minecraft account needed)",
}) color: "white",
); },
sendMessage( ],
hoverEvent: {
action: "show_text",
value: Enums.ChatColor.GOLD + "Click me to select!",
},
clickEvent: {
action: "run_command",
value: "2",
},
});
sendCustomMessage(
client.gameClient, 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; let chosenOption: ConnectType | null = null;
client.lastStatusUpdate = Date.now();
updateState(client.gameClient, "SERVER");
sendMessage(
client.gameClient,
`Provide a server to join. ${Enums.ChatColor.GOLD}/join <ip> [port]${Enums.ChatColor.RESET}.`
);
let host: string, port: number;
while (true) { while (true) {
const msg = await awaitCommand(client.gameClient, (msg) => const option = await awaitCommand(client.gameClient, (msg) => true);
msg.startsWith("/join") switch (option) {
), default:
parsed = msg.split(/ /gi, 3); sendCustomMessage(
if (parsed.length < 2) client.gameClient,
sendMessage( `I don't understand what you meant by "${option}", please reply with a valid option!`,
client.gameClient, "red"
`Please provide a server to connect to. ${Enums.ChatColor.GOLD}/join <ip> [port]${Enums.ChatColor.RESET}.` );
); case "1":
else if (parsed.length > 3 && isNaN(parseInt(parsed[2]))) chosenOption = ConnectType.ONLINE;
sendMessage( break;
client.gameClient, case "2":
`A valid port number has to be passed! ${Enums.ChatColor.GOLD}/join <ip> [port]${Enums.ChatColor.RESET}.` chosenOption = ConnectType.OFFLINE;
); break;
else {
host = parsed[1];
if (parsed.length > 3) port = parseInt(parsed[2]);
port = port ?? 25565;
break;
} }
if (chosenOption != null) break;
} }
try {
await PLUGIN_MANAGER.proxy.players if (chosenOption == ConnectType.ONLINE) {
.get(client.gameClient.username) sendMessageWarning(
.switchServers({ client.gameClient,
host: host, `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.`
port: port, );
version: "1.8.8", await new Promise((res) => setTimeout(res, 2000));
username: savedAuth.selectedProfile.name,
auth: "mojang", client.lastStatusUpdate = Date.now();
keepAlive: false, let errored = false,
session: { savedAuth;
accessToken: savedAuth.accessToken, const authHandler = auth(),
clientToken: savedAuth.selectedProfile.id, codeCallback = (code: ServerDeviceCodeResponse) => {
selectedProfile: { updateState(
id: savedAuth.selectedProfile.id, client.gameClient,
name: savedAuth.selectedProfile.name, "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 <ip>${
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 <ip>${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 <ip>${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 <ip>",
"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,
skipValidation: true, hideErrors: true,
hideErrors: true, });
}); } catch (err) {
} catch (err) { if (!client.gameClient.ended) {
if (!client.gameClient.ended) { client.gameClient.end(
client.gameClient.end( Enums.ChatColor.RED +
Enums.ChatColor.RED + `Something went wrong whilst switching servers: ${err.message}${
`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 <ip>${
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 <ip>${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 <ip>${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 <ip>",
"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) { } catch (err) {

View File

@ -1,276 +1,370 @@
import EventEmitter from "events" import EventEmitter from "events";
import pkg, { Client, ClientOptions, createClient, states } from "minecraft-protocol" import pkg, {
import { WebSocket } from "ws" Client,
import { Logger } from "../logger.js" ClientOptions,
import { Chat } from "./Chat.js" createClient,
import { Constants } from "./Constants.js" states,
import { Enums } from "./Enums.js" } from "minecraft-protocol";
import Packet from "./Packet.js" import { WebSocket } from "ws";
import SCDisconnectPacket from "./packets/SCDisconnectPacket.js" import { Logger } from "../logger.js";
import { MineProtocol } from "./Protocol.js" import { Chat } from "./Chat.js";
import { EaglerSkins } from "./skins/EaglerSkins.js" import { Constants } from "./Constants.js";
import { Util } from "./Util.js" import { Enums } from "./Enums.js";
import { BungeeUtil } from "./BungeeUtil.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 { export class Player extends EventEmitter {
public ws: WebSocket public ws: WebSocket;
public username?: string public username?: string;
public skin?: EaglerSkins.EaglerSkin public skin?: EaglerSkins.EaglerSkin;
public uuid?: string public uuid?: string;
public state?: Enums.ClientState = Enums.ClientState.PRE_HANDSHAKE public state?: Enums.ClientState = Enums.ClientState.PRE_HANDSHAKE;
public serverConnection?: Client public serverConnection?: Client;
private _switchingServers: boolean = false private _switchingServers: boolean = false;
private _logger: Logger private _logger: Logger;
private _alreadyConnected: boolean = false private _alreadyConnected: boolean = false;
private _serializer: any private _serializer: any;
private _deserializer: any private _deserializer: any;
private _kickMessage: string private _kickMessage: string;
constructor(ws: WebSocket, playerName?: string, serverConnection?: Client) { constructor(ws: WebSocket, playerName?: string, serverConnection?: Client) {
super() super();
this._logger = new Logger(`PlayerHandler-${playerName}`) this._logger = new Logger(`PlayerHandler-${playerName}`);
this.ws = ws this.ws = ws;
this.username = playerName this.username = playerName;
this.serverConnection = serverConnection this.serverConnection = serverConnection;
if (this.username != null) this.uuid = Util.generateUUIDFromPlayer(this.username) if (this.username != null)
this._serializer = createSerializer({ this.uuid = Util.generateUUIDFromPlayer(this.username);
state: states.PLAY, this._serializer = createSerializer({
isServer: true, state: states.PLAY,
version: "1.8.9", isServer: true,
customPackets: null version: "1.8.9",
}) customPackets: null,
this._deserializer = createDeserializer({ });
state: states.PLAY, this._deserializer = createDeserializer({
isServer: false, state: states.PLAY,
version: "1.8.9", isServer: false,
customPackets: null version: "1.8.9",
}) customPackets: null,
// this._serializer.pipe(this.ws) });
} // this._serializer.pipe(this.ws)
}
public initListeners() { public initListeners() {
this.ws.on('close', () => { this.ws.on("close", () => {
this.state = Enums.ClientState.DISCONNECTED this.state = Enums.ClientState.DISCONNECTED;
if (this.serverConnection) this.serverConnection.end() if (this.serverConnection) this.serverConnection.end();
this.emit('disconnect', this) this.emit("disconnect", this);
}) });
this.ws.on('message', (msg: Buffer) => { this.ws.on("message", (msg: Buffer) => {
if (msg instanceof Buffer == false) return if (msg instanceof Buffer == false) return;
const decoder = PACKET_REGISTRY.get(msg[0]) const decoder = PACKET_REGISTRY.get(msg[0]);
if (decoder && decoder.sentAfterHandshake) { if (decoder && decoder.sentAfterHandshake) {
if (!decoder && this.state != Enums.ClientState.POST_HANDSHAKE && msg.length >= 1) { if (
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.`) !decoder &&
} else { this.state != Enums.ClientState.POST_HANDSHAKE &&
let parsed: Packet, err: boolean msg.length >= 1
try { ) {
parsed = new decoder.class() this._logger.warn(
parsed.deserialize(msg) `Packet with ID 0x${Buffer.from([msg[0]]).toString(
} catch (err) { "hex"
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.`) )} is missing a corresponding packet handler! Processing for this packet will be skipped.`
err = true );
}
if (!err) {
this.emit('proxyPacket', parsed, this)
return
}
}
}
})
}
public write(packet: Packet) {
this.ws.send(packet.serialize())
}
public async read(packetId?: Enums.PacketId, filter?: (packet: Packet) => boolean): Promise<Packet> {
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()
} else { } else {
const packet = new SCDisconnectPacket() let parsed: Packet, err: boolean;
packet.reason = message try {
this.ws.send(packet.serialize()) parsed = new decoder.class();
this.ws.close() 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) { public write(packet: Packet) {
if (this._alreadyConnected) this.ws.send(packet.serialize());
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) { public async read(
if (!this._alreadyConnected) packetId?: Enums.PacketId,
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.`) filter?: (packet: Packet) => boolean
return new Promise<void | never>(async (res, rej) => { ): Promise<Packet> {
const oldConnection = this.serverConnection let res;
this._switchingServers = true await Util.awaitPacket(this.ws, (packet) => {
if ((packetId != null && packetId == packet[0]) || packetId == null) {
this.ws.send(this._serializer.createPacketBuffer({ const decoder = PACKET_REGISTRY.get(packet[0]);
name: 'chat', if (
params: { decoder != null &&
message: `${Enums.ChatColor.GRAY}Switching servers...`, decoder.packetId == packet[0] &&
position: 1 (this.state == Enums.ClientState.PRE_HANDSHAKE ||
} decoder.sentAfterHandshake) &&
})) decoder.boundTo == Enums.PacketBounds.S
this.ws.send(this._serializer.createPacketBuffer({ ) {
name: 'playerlist_header', let parsed: Packet,
params: { err = false;
header: JSON.stringify({ try {
text: "" parsed = new decoder.class();
}), parsed.deserialize(packet);
footer: JSON.stringify({ } catch (_err) {
text: "" err = true;
}) }
} if (!err) {
})) if (filter && filter(parsed)) {
res = parsed;
this.serverConnection = createClient(Object.assign({ return true;
version: '1.8.9', } else if (filter == null) {
keepAlive: false, res = parsed;
hideErrors: false return true;
}, 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}`)
}
} }
client.on('error', errListener) }
client.on('end', reason => { }
if (!this._switchingServers) this.disconnect(this._kickMessage ?? reason) }
this.ws.removeListener('message', listener) return false;
}) });
client.once('connect', () => { return res;
this.emit('joinServer', client, this) }
})
client.on('packet', (packet, meta) => { public disconnect(message: Chat.Chat | string) {
if (meta.name == 'kick_disconnect') { if (this.state == Enums.ClientState.POST_HANDSHAKE) {
let json this.ws.send(
try { json = JSON.parse(packet.reason) } Buffer.concat(
catch {} [
if (json != null) { [0x40],
this._kickMessage = Chat.chatToPlainString(json) MineProtocol.writeString(
} else this._kickMessage = packet.reason typeof message == "string" ? message : JSON.stringify(message)
} ),
if (!stream) { ].map((arr) => (arr instanceof Uint8Array ? arr : Buffer.from(arr)))
if (switchingServers) { )
if (meta.name == 'login' && meta.state == states.PLAY && uuid) { );
const pckSeq = BungeeUtil.getRespawnSequence(packet, this._serializer) this.ws.close();
this.ws.send(this._serializer.createPacketBuffer({ } else {
name: "login", const packet = new SCDisconnectPacket();
params: packet packet.reason = message;
})) this.ws.send(packet.serialize());
pckSeq.forEach(p => this.ws.send(p)) this.ws.close();
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)
})
} }
}
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<void | never>(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 { interface PlayerEvents {
'switchServer': (connection: Client, player: Player) => void, switchServer: (connection: Client, player: Player) => void;
'joinServer': (connection: Client, player: Player) => void, joinServer: (connection: Client, player: Player) => void;
// for vanilla game packets, bind to connection object instead // for vanilla game packets, bind to connection object instead
'proxyPacket': (packet: Packet, player: Player) => void, proxyPacket: (packet: Packet, player: Player) => void;
'vanillaPacket': (packet: Packet & { cancel: boolean }, origin: 'CLIENT' | 'SERVER', player: Player) => Packet & { cancel: boolean }, vanillaPacket: (
'disconnect': (player: Player) => void packet: Packet & { cancel: boolean },
origin: "CLIENT" | "SERVER",
player: Player
) => Packet & { cancel: boolean };
disconnect: (player: Player) => void;
} }
export declare interface Player { export declare interface Player {
on<U extends keyof PlayerEvents>( on<U extends keyof PlayerEvents>(event: U, listener: PlayerEvents[U]): this;
event: U, listener: PlayerEvents[U]
): this;
emit<U extends keyof PlayerEvents>( emit<U extends keyof PlayerEvents>(
event: U, ...args: Parameters<PlayerEvents[U]> event: U,
): boolean; ...args: Parameters<PlayerEvents[U]>
): boolean;
} }