add offline server support to EagProxyAAS

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

View File

@ -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.

View File

@ -1,4 +1,5 @@
export const config = {
bindInternalServerPort: 25569,
bindInternalServerIp: "127.0.0.1"
}
bindInternalServerPort: 25569,
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 = {
server: Server,
players: Map<string, ClientState>
}
server: Server;
players: Map<string, ClientState>;
};
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'
}
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,
}

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 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 <ip> [port]`,
text: `${Enums.ChatColor.RED}/join <ip>${
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 <ip> [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 <ip> [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 <ip> [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 <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,
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 <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) {

View File

@ -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<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()
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<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}`)
}
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;
}
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<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 {
'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<U extends keyof PlayerEvents>(
event: U, listener: PlayerEvents[U]
): this;
emit<U extends keyof PlayerEvents>(
event: U, ...args: Parameters<PlayerEvents[U]>
): boolean;
}
on<U extends keyof PlayerEvents>(event: U, listener: PlayerEvents[U]): this;
emit<U extends keyof PlayerEvents>(
event: U,
...args: Parameters<PlayerEvents[U]>
): boolean;
}