Merge pull request #15 from WorldEditAxe/skin-fix

uh crap wrong name
This commit is contained in:
q13x 2023-07-03 00:25:13 -07:00 committed by GitHub
commit 549c854021
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 999 additions and 558 deletions

View File

@ -36,9 +36,7 @@ Remove all the folders in `src/plugins`.
#### IMPORTANT: READ ME BEFORE USING #### IMPORTANT: READ ME BEFORE USING
It is highly suggested that you use [Resent Client](https://reslauncher.vercel.app/) if you aren't already. It provides better performance, FPS, and will allow for a more stable connection to servers. **IMPORTANT:** Although the vanilla Eaglercraft client is a safe, modified copy of Minecraft AOT-compiled to JavaScript, I cannot guarantee that you **will not get flagged by all anticheats.** While gameplay and testing has shown to be relatively stable and free of anticheat flags, more testing is needed to derive a conclusion on whether or not using EaglerProxy with EagProxyAAS is safe.
**IMPORTANT:** Although both Resent Client and the vanilla Eaglercraft client are safe modified copies of Minecraft AOT-compiled to JavaScript, I cannot guarantee that you **will not get flagged by all anticheats.** While gameplay and testing has shown to be relatively stable and free of anticheat flags, more testing is needed to derive a conclusion on whether or not using EaglerProxy with EagProxyAAS is safe.
EaglerProxy and EagProxyAAS: EaglerProxy and EagProxyAAS:
@ -50,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,290 +1,634 @@
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";
import vec3 from "vec3" 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;
const Util = PLUGIN_MANAGER.Util const Util = PLUGIN_MANAGER.Util;
const MAX_LIFETIME_CONNECTED = 10 * 60 * 1000, MAX_LIFETIME_AUTH = 5 * 60 * 1000, MAX_LIFETIME_LOGIN = 1 * 60 * 1000 const MAX_LIFETIME_CONNECTED = 10 * 60 * 1000,
const REGISTRY = Registry.default('1.8.8'), McBlock = (Block as any).default('1.8.8'), LOGIN_CHUNK = generateSpawnChunk().dump() MAX_LIFETIME_AUTH = 5 * 60 * 1000,
const logger = new PLUGIN_MANAGER.Logger("PlayerHandler") MAX_LIFETIME_LOGIN = 1 * 60 * 1000;
const REGISTRY = Registry.default("1.8.8"),
McBlock = (Block as any).default("1.8.8"),
LOGIN_CHUNK = generateSpawnChunk().dump();
const logger = new PLUGIN_MANAGER.Logger("PlayerHandler");
const FORK_URL = "https://repl.it/github/WorldEditAxe/eaglerproxy" let SERVER: ServerGlobals = null;
let SERVER: ServerGlobals = null
export function setSG(svr: ServerGlobals) { export function setSG(svr: ServerGlobals) {
SERVER = svr SERVER = svr;
} }
export function disconectIdle() { export function disconectIdle() {
SERVER.players.forEach(client => { SERVER.players.forEach((client) => {
if (client.state == ConnectionState.AUTH && (Date.now() - client.lastStatusUpdate) > MAX_LIFETIME_AUTH) { if (
client.gameClient.end("Timed out waiting for user to login via Microsoft") client.state == ConnectionState.AUTH &&
} else if (client.state == ConnectionState.SUCCESS && (Date.now() - client.lastStatusUpdate) > MAX_LIFETIME_CONNECTED) { Date.now() - client.lastStatusUpdate > MAX_LIFETIME_AUTH
client.gameClient.end(Enums.ChatColor.RED + "Please enter the IP of the server you'd like to connect to in chat.") ) {
} client.gameClient.end(
}) "Timed out waiting for user to login via Microsoft"
);
} else if (
client.state == ConnectionState.SUCCESS &&
Date.now() - client.lastStatusUpdate > MAX_LIFETIME_CONNECTED
) {
client.gameClient.end(
Enums.ChatColor.RED +
"Please enter the IP of the server you'd like to connect to in chat."
);
}
});
} }
export function handleConnect(client: ClientState) { export function handleConnect(client: ClientState) {
client.gameClient.write('login', { client.gameClient.write("login", {
entityId: 1, entityId: 1,
gameMode: 2, gameMode: 2,
dimension: 0, dimension: 0,
difficulty: 1, difficulty: 1,
maxPlayers: 1, maxPlayers: 1,
levelType: 'flat', levelType: "flat",
reducedDebugInfo: false reducedDebugInfo: false,
}) });
client.gameClient.write('map_chunk', { client.gameClient.write("map_chunk", {
x: 0, x: 0,
z: 0, z: 0,
groundUp: true, groundUp: true,
bitMap: 0xFFFF, bitMap: 0xffff,
chunkData: LOGIN_CHUNK chunkData: LOGIN_CHUNK,
}) });
client.gameClient.write('position', { client.gameClient.write("position", {
x: 0, x: 0,
y: 65, y: 65,
z: 8.5, z: 8.5,
yaw: -90, yaw: -90,
pitch: 0, pitch: 0,
flags: 0x01 flags: 0x01,
}) });
client.gameClient.write('playerlist_header', { client.gameClient.write("playerlist_header", {
header: JSON.stringify({ header: JSON.stringify({
text: ` ${Enums.ChatColor.GOLD}EaglerProxy Authentication Server ` text: ` ${Enums.ChatColor.GOLD}EaglerProxy Authentication Server `,
}), }),
footer: JSON.stringify({ footer: JSON.stringify({
text: `${Enums.ChatColor.GOLD}Please wait for instructions.` text: `${Enums.ChatColor.GOLD}Please wait for instructions.`,
}) }),
}) });
onConnect(client) onConnect(client);
} }
export function awaitCommand(client: Client, filter: (msg: string) => boolean): Promise<string> { export function awaitCommand(
return new Promise<string>((res, rej) => { client: Client,
const onMsg = packet => { filter: (msg: string) => boolean
if (filter(packet.message)) { ): Promise<string> {
client.removeListener('chat', onMsg) return new Promise<string>((res, rej) => {
client.removeListener('end', onEnd) const onMsg = (packet) => {
res(packet.message) if (filter(packet.message)) {
} client.removeListener("chat", onMsg);
} client.removeListener("end", onEnd);
const onEnd = () => rej("Client disconnected before promise could be resolved") res(packet.message);
client.on('chat', onMsg) }
client.on('end', onEnd) };
}) const onEnd = () =>
rej("Client disconnected before promise could be resolved");
client.on("chat", onMsg);
client.on("end", onEnd);
});
} }
export function sendMessage(client: Client, msg: string, json?: any) { export function sendMessage(client: Client, msg: string) {
client.write('chat', { client.write("chat", {
message: JSON.stringify({ text: msg, ...json }), message: JSON.stringify({ text: msg }),
position: 1 position: 1,
}) });
}
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({
text: msg, text: msg,
color: 'yellow' color: "yellow",
}), }),
position: 1 position: 1,
}) });
} }
export function sendMessageLogin(client: Client, url: string, token: string) { export function sendMessageLogin(client: Client, url: string, token: string) {
client.write('chat', { client.write("chat", {
message: JSON.stringify({ message: JSON.stringify({
text: "Please go to ", text: "Please go to ",
color: Enums.ChatColor.RESET, color: Enums.ChatColor.RESET,
extra: [ extra: [
{ {
text: url, text: url,
color: 'gold', color: "gold",
clickEvent: { clickEvent: {
action: "open_url", action: "open_url",
value: url value: url,
}, },
hoverEvent: { hoverEvent: {
action: "show_text", action: "show_text",
value: Enums.ChatColor.GOLD + "Click to open me in a new window!" value: Enums.ChatColor.GOLD + "Click to open me in a new window!",
} },
}, },
{ {
text: " and login via the code " text: " and login via the code ",
}, },
{ {
text: token, text: token,
color: 'gold', color: "gold",
hoverEvent: { hoverEvent: {
action: "show_text", action: "show_text",
value: Enums.ChatColor.GOLD + "Click me to copy to chat!" value: Enums.ChatColor.GOLD + "Click me to copy to chat!",
}, },
clickEvent: { clickEvent: {
action: "suggest_command", action: "suggest_command",
value: token value: token,
} },
}, },
{ {
text: "." text: ".",
} },
] ],
}),
position: 1,
});
}
export function updateState(
client: Client,
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 `,
}), }),
position: 1 footer: JSON.stringify({
}) text: `${Enums.ChatColor.RED}Choose the connection type: 1 = online, 2 = offline.`,
} }),
});
export function sendMessageFork(client: Client, url: string) { break;
client.write('chat', { case "AUTH":
message: JSON.stringify({ if (code == null || uri == null)
text: "Fork me here: ", throw new Error(
color: 'aqua', "Missing code/uri required for title message type AUTH"
extra: [{ );
text: "<click me to open>", client.write("playerlist_header", {
color: 'green', header: JSON.stringify({
clickEvent: { text: ` ${Enums.ChatColor.GOLD}EaglerProxy Authentication Server `,
action: "open_url", }),
value: url footer: JSON.stringify({
}, text: `${Enums.ChatColor.RED}${uri}${Enums.ChatColor.GOLD} | Code: ${Enums.ChatColor.RED}${code}`,
hoverEvent: { }),
action: "show_text", });
value: Enums.ChatColor.GOLD + "Click me to open in a new window!" break;
} case "SERVER":
}] client.write("playerlist_header", {
}) header: JSON.stringify({
}) text: ` ${Enums.ChatColor.GOLD}EaglerProxy Authentication Server `,
} }),
footer: JSON.stringify({
export function updateState(client: Client, newState: 'AUTH' | 'SERVER', uri?: string, code?: string) { text: `${Enums.ChatColor.RED}/join <ip>${
switch(newState) { config.allowCustomPorts ? " [port]" : ""
case 'AUTH': }`,
if (code == null || uri == null) throw new Error("Missing code/uri required for title message type AUTH") }),
client.write('playerlist_header', { });
header: JSON.stringify({ break;
text: ` ${Enums.ChatColor.GOLD}EaglerProxy Authentication Server ` }
}),
footer: JSON.stringify({
text: `${Enums.ChatColor.RED}${uri}${Enums.ChatColor.GOLD} | Code: ${Enums.ChatColor.RED}${code}`
})
})
break
case 'SERVER':
client.write('playerlist_header', {
header: JSON.stringify({
text: ` ${Enums.ChatColor.GOLD}EaglerProxy Authentication Server `
}),
footer: JSON.stringify({
text: `${Enums.ChatColor.RED}/join <ip> [port]`
})
})
break
}
} }
export async function onConnect(client: ClientState) { export async function onConnect(client: ClientState) {
try { try {
client.state = ConnectionState.AUTH client.state = ConnectionState.AUTH;
client.lastStatusUpdate = Date.now() client.lastStatusUpdate = Date.now();
sendMessageWarning(client.gameClient, `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.`) sendMessageWarning(
await new Promise(res => setTimeout(res, 2000)) client.gameClient,
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. Using any untrusted proxies may put your account at risk.`) `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.`
sendMessageFork(client.gameClient, FORK_URL) );
await new Promise(res => setTimeout(res, 2000)) await new Promise((res) => setTimeout(res, 2000));
client.lastStatusUpdate = Date.now() sendMessageWarning(
let errored = false, savedAuth client.gameClient,
const authHandler = auth(), codeCallback = (code: ServerDeviceCodeResponse) => { `WARNING: It is highly suggested that you turn down settings, as gameplay tends to be very laggy and unplayable on low powered devices.`
updateState(client.gameClient, 'AUTH', code.verification_uri, code.user_code) );
sendMessageLogin(client.gameClient, code.verification_uri, code.user_code) await new Promise((res) => setTimeout(res, 2000));
}
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 sendCustomMessage(client.gameClient, "What would you like to do?", "gray");
client.lastStatusUpdate = Date.now() sendChatComponent(client.gameClient, {
updateState(client.gameClient, 'SERVER') text: "1) ",
sendMessage(client.gameClient, `Provide a server to join. ${Enums.ChatColor.GOLD}/join <ip> [port]${Enums.ChatColor.RESET}.`) color: "gold",
sendMessage(client.gameClient, "(providing a custom port to a SRV record server domain may potentially cause issues)", { color: "gray" }) extra: [
let host: string, port: number {
while (true) { text: "Connect to an online server (Minecraft account needed)",
const msg = await awaitCommand(client.gameClient, msg => msg.startsWith("/join")), parsed = msg.split(/ /gi, 3) color: "white",
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 { hoverEvent: {
host = parsed[1] action: "show_text",
if (parsed.length >= 3) port = parseInt(parsed[2]) value: Enums.ChatColor.GOLD + "Click me to select!",
port = port ?? 25565 },
clickEvent: {
action: "run_command",
value: "1",
},
});
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,
"Select an option from the above (1 = online, 2 = offline), either by clicking or manually typing out the option.",
"green"
);
try { let chosenOption: ConnectType | null = null;
await PLUGIN_MANAGER.proxy.players.get(client.gameClient.username).switchServers({ while (true) {
host: host, const option = await awaitCommand(client.gameClient, (msg) => true);
port: port, switch (option) {
version: "1.8.8", default:
username: savedAuth.selectedProfile.name, sendCustomMessage(
auth: 'mojang', client.gameClient,
keepAlive: false, `I don't understand what you meant by "${option}", please reply with a valid option!`,
session: { "red"
accessToken: savedAuth.accessToken, );
clientToken: savedAuth.selectedProfile.id, case "1":
selectedProfile: { chosenOption = ConnectType.ONLINE;
id: savedAuth.selectedProfile.id, break;
name: savedAuth.selectedProfile.name case "2":
} chosenOption = ConnectType.OFFLINE;
}, break;
skipValidation: true, }
hideErrors: true if (chosenOption != null) break;
})
break
} catch (err) {
if (!client.gameClient.ended) {
sendMessage(client.gameClient, `Something went wrong whilst switching servers: ${err.message}.`, { color: "red" })
if (err.code == "ENOTFOUND") {
sendMessage(client.gameClient, `Please check that the provided IP/hostname is correct, and try again.`, { color: "red" })
}
}
}
}
}
} catch (err) {
if (!client.gameClient.ended) {
logger.error(`Error whilst processing user ${client.gameClient.username}: ${err.stack || err}`)
client.gameClient.end(Enums.ChatColor.YELLOW + "Something went wrong whilst processing your request. Please reconnect.")
}
} }
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}${
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) {
if (!client.gameClient.ended) {
logger.error(
`Error whilst processing user ${client.gameClient.username}: ${
err.stack || err
}`
);
client.gameClient.end(
Enums.ChatColor.YELLOW +
"Something went wrong whilst processing your request. Please reconnect."
);
}
}
} }
export function generateSpawnChunk(): Chunk.PCChunk { export function generateSpawnChunk(): Chunk.PCChunk {
const chunk = new (Chunk.default(REGISTRY))(null) as Chunk.PCChunk const chunk = new (Chunk.default(REGISTRY))(null) as Chunk.PCChunk;
chunk.initialize(() => new McBlock(REGISTRY.blocksByName.air.id, REGISTRY.biomesByName.plains.id, 0)) chunk.initialize(
chunk.setBlock(new Vec3(8, 64, 8), new McBlock(REGISTRY.blocksByName.barrier.id, REGISTRY.biomesByName.plains.id, 0)) () =>
chunk.setBlock(new Vec3(8, 67, 8), new McBlock(REGISTRY.blocksByName.barrier.id, REGISTRY.biomesByName.plains.id, 0)) new McBlock(
chunk.setBlock(new Vec3(7, 65, 8), new McBlock(REGISTRY.blocksByName.barrier.id, REGISTRY.biomesByName.plains.id, 0)) REGISTRY.blocksByName.air.id,
chunk.setBlock(new Vec3(7, 66, 8), new McBlock(REGISTRY.blocksByName.barrier.id, REGISTRY.biomesByName.plains.id, 0)) REGISTRY.biomesByName.plains.id,
chunk.setBlock(new Vec3(9, 65, 8), new McBlock(REGISTRY.blocksByName.barrier.id, REGISTRY.biomesByName.plains.id, 0)) 0
chunk.setBlock(new Vec3(9, 66, 8), new McBlock(REGISTRY.blocksByName.barrier.id, REGISTRY.biomesByName.plains.id, 0)) )
chunk.setBlock(new Vec3(8, 65, 7), new McBlock(REGISTRY.blocksByName.barrier.id, REGISTRY.biomesByName.plains.id, 0)) );
chunk.setBlock(new Vec3(8, 66, 7), new McBlock(REGISTRY.blocksByName.barrier.id, REGISTRY.biomesByName.plains.id, 0)) chunk.setBlock(
chunk.setBlock(new Vec3(8, 65, 9), new McBlock(REGISTRY.blocksByName.barrier.id, REGISTRY.biomesByName.plains.id, 0)) new Vec3(8, 64, 8),
chunk.setBlock(new Vec3(8, 66, 9), new McBlock(REGISTRY.blocksByName.barrier.id, REGISTRY.biomesByName.plains.id, 0)) new McBlock(
chunk.setSkyLight(new Vec3(8, 66, 8), 15) REGISTRY.blocksByName.barrier.id,
return chunk REGISTRY.biomesByName.plains.id,
0
)
);
chunk.setBlock(
new Vec3(8, 67, 8),
new McBlock(
REGISTRY.blocksByName.barrier.id,
REGISTRY.biomesByName.plains.id,
0
)
);
chunk.setBlock(
new Vec3(7, 65, 8),
new McBlock(
REGISTRY.blocksByName.barrier.id,
REGISTRY.biomesByName.plains.id,
0
)
);
chunk.setBlock(
new Vec3(7, 66, 8),
new McBlock(
REGISTRY.blocksByName.barrier.id,
REGISTRY.biomesByName.plains.id,
0
)
);
chunk.setBlock(
new Vec3(9, 65, 8),
new McBlock(
REGISTRY.blocksByName.barrier.id,
REGISTRY.biomesByName.plains.id,
0
)
);
chunk.setBlock(
new Vec3(9, 66, 8),
new McBlock(
REGISTRY.blocksByName.barrier.id,
REGISTRY.biomesByName.plains.id,
0
)
);
chunk.setBlock(
new Vec3(8, 65, 7),
new McBlock(
REGISTRY.blocksByName.barrier.id,
REGISTRY.biomesByName.plains.id,
0
)
);
chunk.setBlock(
new Vec3(8, 66, 7),
new McBlock(
REGISTRY.blocksByName.barrier.id,
REGISTRY.biomesByName.plains.id,
0
)
);
chunk.setBlock(
new Vec3(8, 65, 9),
new McBlock(
REGISTRY.blocksByName.barrier.id,
REGISTRY.biomesByName.plains.id,
0
)
);
chunk.setBlock(
new Vec3(8, 66, 9),
new McBlock(
REGISTRY.blocksByName.barrier.id,
REGISTRY.biomesByName.plains.id,
0
)
);
chunk.setSkyLight(new Vec3(8, 66, 8), 15);
return chunk;
} }

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;
} }