Add EagProxyAAS password protection + direct connect

This commit is contained in:
q13x 2024-03-06 15:25:02 -08:00
parent 61cbcfca3e
commit 67c3e0bbda
9 changed files with 865 additions and 2542 deletions

1958
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -2,4 +2,9 @@ export const config = {
bindInternalServerPort: 25569,
bindInternalServerIp: "127.0.0.1",
allowCustomPorts: true,
disallowHypixel: false,
authentication: {
enabled: true,
password: "nope",
},
};

View File

@ -1,25 +1,15 @@
import { config } from "./config.js";
import { createServer } from "minecraft-protocol";
import { ClientState, ConnectionState, ServerGlobals } from "./types.js";
import { handleConnect, hushConsole, setSG } from "./utils.js";
import { handleConnect, hushConsole, sendChatComponent, setSG } from "./utils.js";
import path from "path";
import { readFileSync } from "fs";
import { handleCommand } from "./commands.js";
import { registerEndpoints } from "./service/endpoints.js";
const PluginManager = PLUGIN_MANAGER;
const metadata = JSON.parse(
readFileSync(
process.platform == "win32"
? path
.join(
path.dirname(new URL(import.meta.url).pathname),
"metadata.json"
)
.slice(1)
: path.join(
path.dirname(new URL(import.meta.url).pathname),
"metadata.json"
)
).toString()
readFileSync(process.platform == "win32" ? path.join(path.dirname(new URL(import.meta.url).pathname), "metadata.json").slice(1) : path.join(path.dirname(new URL(import.meta.url).pathname), "metadata.json")).toString()
);
const Logger = PluginManager.Logger;
@ -31,14 +21,11 @@ const Player = PluginManager.Player;
const MineProtocol = PluginManager.MineProtocol;
const EaglerSkins = PluginManager.EaglerSkins;
const Util = PluginManager.Util;
hushConsole();
const logger = new Logger("EaglerProxyAAS");
logger.info(`Starting ${metadata.name} v${metadata.version}...`);
logger.info(
`(internal server port: ${config.bindInternalServerPort}, internal server IP: ${config.bindInternalServerPort})`
);
logger.info(`(internal server port: ${config.bindInternalServerPort}, internal server IP: ${config.bindInternalServerPort})`);
logger.info("Starting internal server...");
let server = createServer({
@ -55,14 +42,161 @@ let server = createServer({
setSG(sGlobals);
server.on("login", (client) => {
logger.info(
`Client ${client.username} has connected to the authentication server.`
const proxyPlayer = PluginManager.proxy.players.get(client.username);
if (proxyPlayer != null) {
const url = new URL(proxyPlayer.ws.httpRequest.url, `http${PluginManager.proxy.config.tls?.enabled ? "s" : ""}://${proxyPlayer.ws.httpRequest.headers.host}`);
if (url.pathname == "/connect-vanilla") {
const host = url.searchParams.get("ip"),
port = url.searchParams.get("port"),
type: "OFFLINE" | "ONLINE" = url.searchParams.get("authType") as any;
if (isNaN(Number(port))) return proxyPlayer.disconnect(Enums.ChatColor.RED + "Bad port number");
if (
!/^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$|^(([a-zA-Z]|[a-zA-Z][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z]|[A-Za-z][A-Za-z0-9\-]*[A-Za-z0-9])$|^\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?\s*$/.test(
host
)
) {
return proxyPlayer.disconnect(Enums.ChatColor.RED + "Bad host provided");
}
if (type == "ONLINE") {
const _profile = proxyPlayer.ws.httpRequest.headers["Minecraft-Profile"];
if (!_profile) proxyPlayer.disconnect(Enums.ChatColor.RED + "Missing Minecraft-Profile header");
let profile;
try {
profile = JSON.parse(_profile as string);
} catch (err) {
proxyPlayer.disconnect(Enums.ChatColor.RED + "Could not read Minecraft-Profile header");
}
logger.info(`Direct OFFLINE proxy forward connection from Eaglercraft player (${client.username}) received.`);
proxyPlayer.on("vanillaPacket", (packet, origin) => {
if (origin == "CLIENT" && packet.name == "chat" && (packet.params.message as string).toLowerCase().startsWith("/eag-") && !packet.cancel) {
packet.cancel = true;
handleCommand(proxyPlayer, packet.params.message as string);
}
});
sendChatComponent(client, {
text: `Joining server under ${profile.selectedProfile.name}/your Minecraft account's username! Run `,
color: "aqua",
extra: [
{
text: "/eag-help",
color: "gold",
hoverEvent: {
action: "show_text",
value: Enums.ChatColor.GOLD + "Click me to run this command!",
},
clickEvent: {
action: "run_command",
value: "/eag-help",
},
},
{
text: " for a list of proxy commands.",
color: "aqua",
},
],
});
(proxyPlayer as any)._onlineSession = {
auth: "mojang",
username: profile.selectedProfile.name,
session: {
accessToken: profile.accessToken,
clientToken: profile.selectedProfile.id,
selectedProfile: {
id: profile.selectedProfile.id,
name: profile.selectedProfile.name,
},
},
};
proxyPlayer
.switchServers({
host: host,
port: Number(port),
version: "1.8.8",
username: profile.selectedProfile.name,
auth: "mojang",
keepAlive: false,
session: {
accessToken: profile.accessToken,
clientToken: profile.selectedProfile.id,
selectedProfile: {
id: profile.selectedProfile.id,
name: profile.selectedProfile.name,
},
},
skipValidation: true,
hideErrors: true,
})
.catch((err) => {
if (!client.ended) {
proxyPlayer.disconnect(
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 if (type == "OFFLINE") {
logger.info(`Direct ONLINE proxy forward connection from Eaglercraft player (${client.username}) received.`);
logger.info(`Player ${client.username} is attempting to connect to ${host}:${port} under their Eaglercraft username (${client.username}) using offline mode!`);
proxyPlayer.on("vanillaPacket", (packet, origin) => {
if (origin == "CLIENT" && packet.name == "chat" && (packet.params.message as string).toLowerCase().startsWith("/eag-") && !packet.cancel) {
packet.cancel = true;
handleCommand(proxyPlayer, packet.params.message as string);
}
});
sendChatComponent(client, {
text: `Joining server under ${client.username}/your Eaglercraft account's username! Run `,
color: "aqua",
extra: [
{
text: "/eag-help",
color: "gold",
hoverEvent: {
action: "show_text",
value: Enums.ChatColor.GOLD + "Click me to run this command!",
},
clickEvent: {
action: "run_command",
value: "/eag-help",
},
},
{
text: " for a list of proxy commands.",
color: "aqua",
},
],
});
proxyPlayer
.switchServers({
host: host,
port: Number(port),
auth: "offline",
username: client.username,
version: "1.8.8",
keepAlive: false,
skipValidation: true,
hideErrors: true,
})
.catch((err) => {
if (!client.ended) {
proxyPlayer.disconnect(
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 {
proxyPlayer.disconnect(Enums.ChatColor.RED + "Missing authentication type");
}
} else {
logger.info(`Client ${client.username} has connected to the authentication server.`);
client.on("end", () => {
sGlobals.players.delete(client.username);
logger.info(
`Client ${client.username} has disconnected from the authentication server.`
);
logger.info(`Client ${client.username} has disconnected from the authentication server.`);
});
const cs: ClientState = {
state: ConnectionState.AUTH,
@ -72,11 +206,14 @@ server.on("login", (client) => {
};
sGlobals.players.set(client.username, cs);
handleConnect(cs);
}
} else {
logger.warn(`Proxy player object is null for ${client.username}?!`);
client.end("Indirect connection to internal authentication server detected!");
}
});
logger.info(
"Redirecting backend server IP... (this is required for the plugin to function)"
);
logger.info("Redirecting backend server IP... (this is required for the plugin to function)");
CONFIG.adapter.server = {
host: config.bindInternalServerIp,
port: config.bindInternalServerPort,
@ -84,3 +221,7 @@ CONFIG.adapter.server = {
CONFIG.adapter.motd = {
l1: Enums.ChatColor.GOLD + "EaglerProxy as a Service",
};
PLUGIN_MANAGER.addListener("proxyFinishLoading", () => {
registerEndpoints();
});

View File

@ -0,0 +1,33 @@
import { config } from "../config.js";
export async function registerEndpoints() {
const proxy = PLUGIN_MANAGER.proxy;
proxy.on("httpConnection", (req, res, ctx) => {
if (req.url.startsWith("/eagpaas/metadata")) {
ctx.handled = true;
res.writeHead(200).end(
JSON.stringify({
branding: "EagProxyAAS",
version: "1",
})
);
} else if (req.url.startsWith("/eagpaas/validate")) {
ctx.handled = true;
if (config.authentication.enabled) {
if (req.headers["authorization"] !== `Basic ${config.authentication.password}`) {
return res.writeHead(403).end(
JSON.stringify({
success: false,
reason: "Access Denied",
})
);
}
}
res.writeHead(200).end(
JSON.stringify({
success: true,
})
);
}
});
}

View File

@ -37,21 +37,10 @@ export function setSG(svr: ServerGlobals) {
export function disconectIdle() {
SERVER.players.forEach((client) => {
if (
client.state == ConnectionState.AUTH &&
Date.now() - client.lastStatusUpdate > MAX_LIFETIME_AUTH
) {
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."
);
if (client.state == ConnectionState.AUTH && Date.now() - client.lastStatusUpdate > MAX_LIFETIME_AUTH) {
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.");
}
});
}
@ -93,10 +82,7 @@ export function handleConnect(client: ClientState) {
onConnect(client);
}
export function awaitCommand(
client: Client,
filter: (msg: string) => boolean
): Promise<string> {
export function awaitCommand(client: Client, filter: (msg: string) => boolean): Promise<string> {
return new Promise<string>((res, rej) => {
const onMsg = (packet) => {
if (filter(packet.message)) {
@ -105,8 +91,7 @@ export function awaitCommand(
res(packet.message);
}
};
const onEnd = () =>
rej("Client disconnected before promise could be resolved");
const onEnd = () => rej("Client disconnected before promise could be resolved");
client.on("chat", onMsg);
client.on("end", onEnd);
});
@ -119,12 +104,7 @@ export function sendMessage(client: Client, msg: string) {
});
}
export function sendCustomMessage(
client: Client,
msg: string,
color: string,
...components: { text: string; color: string }[]
) {
export function sendCustomMessage(client: Client, msg: string, color: string, ...components: { text: string; color: string }[]) {
client.write("chat", {
message: JSON.stringify(
components.length > 0
@ -198,12 +178,7 @@ export function sendMessageLogin(client: Client, url: string, token: string) {
});
}
export function updateState(
client: Client,
newState: "CONNECTION_TYPE" | "AUTH_EASYMC" | "AUTH" | "SERVER",
uri?: string,
code?: string
) {
export function updateState(client: Client, newState: "CONNECTION_TYPE" | "AUTH_EASYMC" | "AUTH" | "SERVER", uri?: string, code?: string) {
switch (newState) {
case "CONNECTION_TYPE":
client.write("playerlist_header", {
@ -226,10 +201,7 @@ export function updateState(
});
break;
case "AUTH":
if (code == null || uri == null)
throw new Error(
"Missing code/uri required for title message type 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({
text: ` ${Enums.ChatColor.GOLD}EaglerProxy Authentication Server `,
@ -245,9 +217,7 @@ export function updateState(
text: ` ${Enums.ChatColor.GOLD}EaglerProxy Authentication Server `,
}),
footer: JSON.stringify({
text: `${Enums.ChatColor.RED}/join <ip>${
config.allowCustomPorts ? " [port]" : ""
}`,
text: `${Enums.ChatColor.RED}/join <ip>${config.allowCustomPorts ? " [port]" : ""}`,
}),
});
break;
@ -271,10 +241,7 @@ export async function onConnect(client: ClientState) {
client.state = ConnectionState.AUTH;
client.lastStatusUpdate = Date.now();
sendMessageWarning(
client.gameClient,
`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.`
);
sendMessageWarning(client.gameClient, `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(
@ -283,12 +250,20 @@ export async function onConnect(client: ClientState) {
);
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.`
);
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));
if (config.authentication.enabled) {
sendCustomMessage(client.gameClient, "This instance is password-protected. Sign in with /password <password>", "gold");
const password = await awaitCommand(client.gameClient, (msg) => msg.startsWith("/password "));
if (password === `/password ${config.authentication.password}`) {
sendCustomMessage(client.gameClient, "Successfully signed into instance!", "green");
} else {
client.gameClient.end(Enums.ChatColor.RED + "Bad password!");
return;
}
}
sendCustomMessage(client.gameClient, "What would you like to do?", "gray");
sendChatComponent(client.gameClient, {
text: "1) ",
@ -344,11 +319,7 @@ export async function onConnect(client: ClientState) {
value: "$3",
},
});
sendCustomMessage(
client.gameClient,
"Select an option from the above (1 = online, 2 = offline, 3 = EasyMC), either by clicking or manually typing out the option's number on the list.",
"green"
);
sendCustomMessage(client.gameClient, "Select an option from the above (1 = online, 2 = offline, 3 = EasyMC), either by clicking or manually typing out the option's number on the list.", "green");
updateState(client.gameClient, "CONNECTION_TYPE");
let chosenOption: ConnectType | null = null;
@ -356,11 +327,7 @@ export async function onConnect(client: ClientState) {
const option = await awaitCommand(client.gameClient, (msg) => true);
switch (option.replace(/\$/gim, "")) {
default:
sendCustomMessage(
client.gameClient,
`I don't understand what you meant by "${option}", please reply with a valid option!`,
"red"
);
sendCustomMessage(client.gameClient, `I don't understand what you meant by "${option}", please reply with a valid option!`, "red");
break;
case "1":
chosenOption = ConnectType.ONLINE;
@ -390,17 +357,8 @@ export async function onConnect(client: ClientState) {
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
);
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);
@ -415,64 +373,30 @@ export async function onConnect(client: ClientState) {
res(result);
})
);
sendMessage(
client.gameClient,
Enums.ChatColor.BRIGHT_GREEN + "Successfully logged into Minecraft!"
);
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}.`
);
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")
),
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
}.`
);
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>" +
(config.allowCustomPorts ? " [port]" : ""),
"red"
);
sendCustomMessage(client.gameClient, "You are not allowed to use custom server ports! /join <ip>" + (config.allowCustomPorts ? " [port]" : ""), "red");
host = null;
port = null;
} else {
if (
host.match(/^(?:\*\.)?((?!hypixel\.net$)[^.]+\.)*hypixel\.net$/)
) {
if (host.match(/^(?:\*\.)?((?!hypixel\.net$)[^.]+\.)*hypixel\.net$/) && config.disallowHypixel) {
sendCustomMessage(
client.gameClient,
"Disallowed server, refusing to connect! Hypixel has been known to falsely flag Eaglercraft clients, and thus we do not allow connecting to their server. /join <ip>" +
(config.allowCustomPorts ? " [port]" : ""),
"Disallowed server, refusing to connect! Hypixel has been known to falsely flag Eaglercraft clients, and thus we do not allow connecting to their server. /join <ip>" + (config.allowCustomPorts ? " [port]" : ""),
"red"
);
} else {
@ -505,21 +429,10 @@ export async function onConnect(client: ClientState) {
},
],
});
logger.info(
`Player ${client.gameClient.username} is attempting to connect to ${host}:${port} under their Minecraft account's username (${savedAuth.selectedProfile.name}) using online mode!`
);
const player = PLUGIN_MANAGER.proxy.players.get(
client.gameClient.username
);
logger.info(`Player ${client.gameClient.username} is attempting to connect to ${host}:${port} under their Minecraft account's username (${savedAuth.selectedProfile.name}) using online mode!`);
const player = PLUGIN_MANAGER.proxy.players.get(client.gameClient.username);
player.on("vanillaPacket", (packet, origin) => {
if (
origin == "CLIENT" &&
packet.name == "chat" &&
(packet.params.message as string)
.toLowerCase()
.startsWith("/eag-") &&
!packet.cancel
) {
if (origin == "CLIENT" && packet.name == "chat" && (packet.params.message as string).toLowerCase().startsWith("/eag-") && !packet.cancel) {
packet.cancel = true;
handleCommand(player, packet.params.message as string);
}
@ -560,13 +473,7 @@ export async function onConnect(client: ClientState) {
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?"
: ""
}`
`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?") : ""}`
);
}
}
@ -576,10 +483,7 @@ export async function onConnect(client: ClientState) {
client.lastStatusUpdate = Date.now();
updateState(client.gameClient, "AUTH_EASYMC");
sendMessageWarning(
client.gameClient,
`WARNING: You've chosen to use an account from EasyMC's account pool. Please note that accounts and shared, and may be banned from whatever server you are attempting to join.`
);
sendMessageWarning(client.gameClient, `WARNING: You've chosen to use an account from EasyMC's account pool. Please note that accounts and shared, and may be banned from whatever server you are attempting to join.`);
sendChatComponent(client.gameClient, {
text: "Please generate an alt token at ",
color: "white",
@ -621,9 +525,7 @@ export async function onConnect(client: ClientState) {
let appendOptions: any;
while (true) {
const tokenResponse = await awaitCommand(client.gameClient, (msg) =>
msg.toLowerCase().startsWith("/login")
),
const tokenResponse = await awaitCommand(client.gameClient, (msg) => msg.toLowerCase().startsWith("/login")),
splitResponse = tokenResponse.split(/ /gim, 2).slice(1);
if (splitResponse.length != 1) {
sendChatComponent(client.gameClient, {
@ -660,9 +562,7 @@ export async function onConnect(client: ClientState) {
color: "white",
hoverEvent: {
action: "show_text",
value:
Enums.ChatColor.GOLD +
"Click me to open in a new window!",
value: Enums.ChatColor.GOLD + "Click me to open in a new window!",
},
clickEvent: {
action: "open_url",
@ -692,18 +592,10 @@ export async function onConnect(client: ClientState) {
],
});
} else {
sendCustomMessage(
client.gameClient,
"Validating alt token...",
"gray"
);
sendCustomMessage(client.gameClient, "Validating alt token...", "gray");
try {
appendOptions = await getTokenProfileEasyMc(token);
sendCustomMessage(
client.gameClient,
`Successfully validated your alt token and retrieved your session profile! You'll be joining to your preferred server as ${appendOptions.username}.`,
"green"
);
sendCustomMessage(client.gameClient, `Successfully validated your alt token and retrieved your session profile! You'll be joining to your preferred server as ${appendOptions.username}.`, "green");
break;
} catch (err) {
sendChatComponent(client.gameClient, {
@ -736,45 +628,18 @@ export async function onConnect(client: ClientState) {
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}.`
);
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")
),
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
}.`
);
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"
);
sendCustomMessage(client.gameClient, "You are not allowed to use custom server ports! /join <ip>", "red");
host = null;
port = null;
} else {
@ -806,21 +671,10 @@ export async function onConnect(client: ClientState) {
},
],
});
logger.info(
`Player ${client.gameClient.username} is attempting to connect to ${host}:${port} under their EasyMC alt token's username (${appendOptions.username}) using EasyMC mode!`
);
const player = PLUGIN_MANAGER.proxy.players.get(
client.gameClient.username
);
logger.info(`Player ${client.gameClient.username} is attempting to connect to ${host}:${port} under their EasyMC alt token's username (${appendOptions.username}) using EasyMC mode!`);
const player = PLUGIN_MANAGER.proxy.players.get(client.gameClient.username);
player.on("vanillaPacket", (packet, origin) => {
if (
origin == "CLIENT" &&
packet.name == "chat" &&
(packet.params.message as string)
.toLowerCase()
.startsWith("/eag-") &&
!packet.cancel
) {
if (origin == "CLIENT" && packet.name == "chat" && (packet.params.message as string).toLowerCase().startsWith("/eag-") && !packet.cancel) {
packet.cancel = true;
handleCommand(player, packet.params.message as string);
}
@ -843,13 +697,7 @@ export async function onConnect(client: ClientState) {
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?"
: ""
}`
`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?") : ""}`
);
}
}
@ -857,45 +705,18 @@ export async function onConnect(client: ClientState) {
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}.`
);
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")
),
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
}.`
);
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"
);
sendCustomMessage(client.gameClient, "You are not allowed to use custom server ports! /join <ip>", "red");
host = null;
port = null;
} else {
@ -927,21 +748,10 @@ export async function onConnect(client: ClientState) {
},
],
});
logger.info(
`Player ${client.gameClient.username} is attempting to connect to ${host}:${port} under their Eaglercraft username (${client.gameClient.username}) using offline mode!`
);
const player = PLUGIN_MANAGER.proxy.players.get(
client.gameClient.username
);
logger.info(`Player ${client.gameClient.username} is attempting to connect to ${host}:${port} under their Eaglercraft username (${client.gameClient.username}) using offline mode!`);
const player = PLUGIN_MANAGER.proxy.players.get(client.gameClient.username);
player.on("vanillaPacket", (packet, origin) => {
if (
origin == "CLIENT" &&
packet.name == "chat" &&
(packet.params.message as string)
.toLowerCase()
.startsWith("/eag-") &&
!packet.cancel
) {
if (origin == "CLIENT" && packet.name == "chat" && (packet.params.message as string).toLowerCase().startsWith("/eag-") && !packet.cancel) {
packet.cancel = true;
handleCommand(player, packet.params.message as string);
}
@ -961,122 +771,32 @@ export async function onConnect(client: ClientState) {
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?"
: ""
}`
`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."
);
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 {
const chunk = new (Chunk.default(REGISTRY))(null) as Chunk.PCChunk;
chunk.initialize(
() =>
new McBlock(
REGISTRY.blocksByName.air.id,
REGISTRY.biomesByName.the_end.id,
0
)
);
chunk.setBlock(
new Vec3(8, 64, 8),
new McBlock(
REGISTRY.blocksByName.sea_lantern.id,
REGISTRY.biomesByName.the_end.id,
0
)
);
chunk.setBlock(
new Vec3(8, 67, 8),
new McBlock(
REGISTRY.blocksByName.barrier.id,
REGISTRY.biomesByName.the_end.id,
0
)
);
chunk.setBlock(
new Vec3(7, 65, 8),
new McBlock(
REGISTRY.blocksByName.barrier.id,
REGISTRY.biomesByName.the_end.id,
0
)
);
chunk.setBlock(
new Vec3(7, 66, 8),
new McBlock(
REGISTRY.blocksByName.barrier.id,
REGISTRY.biomesByName.the_end.id,
0
)
);
chunk.setBlock(
new Vec3(9, 65, 8),
new McBlock(
REGISTRY.blocksByName.barrier.id,
REGISTRY.biomesByName.the_end.id,
0
)
);
chunk.setBlock(
new Vec3(9, 66, 8),
new McBlock(
REGISTRY.blocksByName.barrier.id,
REGISTRY.biomesByName.the_end.id,
0
)
);
chunk.setBlock(
new Vec3(8, 65, 7),
new McBlock(
REGISTRY.blocksByName.barrier.id,
REGISTRY.biomesByName.the_end.id,
0
)
);
chunk.setBlock(
new Vec3(8, 66, 7),
new McBlock(
REGISTRY.blocksByName.barrier.id,
REGISTRY.biomesByName.the_end.id,
0
)
);
chunk.setBlock(
new Vec3(8, 65, 9),
new McBlock(
REGISTRY.blocksByName.barrier.id,
REGISTRY.biomesByName.the_end.id,
0
)
);
chunk.setBlock(
new Vec3(8, 66, 9),
new McBlock(
REGISTRY.blocksByName.barrier.id,
REGISTRY.biomesByName.the_end.id,
0
)
);
chunk.initialize(() => new McBlock(REGISTRY.blocksByName.air.id, REGISTRY.biomesByName.the_end.id, 0));
chunk.setBlock(new Vec3(8, 64, 8), new McBlock(REGISTRY.blocksByName.sea_lantern.id, REGISTRY.biomesByName.the_end.id, 0));
chunk.setBlock(new Vec3(8, 67, 8), new McBlock(REGISTRY.blocksByName.barrier.id, REGISTRY.biomesByName.the_end.id, 0));
chunk.setBlock(new Vec3(7, 65, 8), new McBlock(REGISTRY.blocksByName.barrier.id, REGISTRY.biomesByName.the_end.id, 0));
chunk.setBlock(new Vec3(7, 66, 8), new McBlock(REGISTRY.blocksByName.barrier.id, REGISTRY.biomesByName.the_end.id, 0));
chunk.setBlock(new Vec3(9, 65, 8), new McBlock(REGISTRY.blocksByName.barrier.id, REGISTRY.biomesByName.the_end.id, 0));
chunk.setBlock(new Vec3(9, 66, 8), new McBlock(REGISTRY.blocksByName.barrier.id, REGISTRY.biomesByName.the_end.id, 0));
chunk.setBlock(new Vec3(8, 65, 7), new McBlock(REGISTRY.blocksByName.barrier.id, REGISTRY.biomesByName.the_end.id, 0));
chunk.setBlock(new Vec3(8, 66, 7), new McBlock(REGISTRY.blocksByName.barrier.id, REGISTRY.biomesByName.the_end.id, 0));
chunk.setBlock(new Vec3(8, 65, 9), new McBlock(REGISTRY.blocksByName.barrier.id, REGISTRY.biomesByName.the_end.id, 0));
chunk.setBlock(new Vec3(8, 66, 9), new McBlock(REGISTRY.blocksByName.barrier.id, REGISTRY.biomesByName.the_end.id, 0));
// chunk.setBlockLight(new Vec3(8, 65, 8), 15);
chunk.setBlockLight(new Vec3(8, 66, 8), 15);
return chunk;

View File

@ -1,10 +1,5 @@
import EventEmitter from "events";
import pkg, {
Client,
ClientOptions,
createClient,
states,
} from "minecraft-protocol";
import pkg, { Client, ClientOptions, createClient, states } from "minecraft-protocol";
import { WebSocket } from "ws";
import { Logger } from "../logger.js";
import { Chat } from "./Chat.js";
@ -15,11 +10,12 @@ import { MineProtocol } from "./Protocol.js";
import { EaglerSkins } from "./skins/EaglerSkins.js";
import { Util } from "./Util.js";
import { BungeeUtil } from "./BungeeUtil.js";
import { IncomingMessage } from "http";
const { createSerializer, createDeserializer } = pkg;
export class Player extends EventEmitter {
public ws: WebSocket;
public ws: WebSocket & { httpRequest: IncomingMessage };
public username?: string;
public skin?: EaglerSkins.EaglerSkin;
public uuid?: string;
@ -37,14 +33,13 @@ export class Player extends EventEmitter {
public clientDeserializer: any;
private _kickMessage: string;
constructor(ws: WebSocket, playerName?: string, serverConnection?: Client) {
constructor(ws: WebSocket & { httpRequest: IncomingMessage }, 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);
if (this.username != null) this.uuid = Util.generateUUIDFromPlayer(this.username);
this.serverSerializer = createSerializer({
state: states.PLAY,
isServer: true,
@ -81,28 +76,15 @@ export class Player extends EventEmitter {
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.`
);
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.`
);
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) {
@ -113,10 +95,7 @@ export class Player extends EventEmitter {
} else {
try {
const parsed = this.serverDeserializer.parsePacketBuffer(msg)?.data,
translated = this.translator.translatePacketClient(
parsed.params,
parsed
),
translated = this.translator.translatePacketClient(parsed.params, parsed),
packetData = {
name: translated[0],
params: translated[1],
@ -132,12 +111,7 @@ export class Player extends EventEmitter {
);
}
} catch (err) {
this._logger.debug(
`Client ${this
.username!} sent an unrecognized packet that could not be parsed!\n${
err.stack ?? err
}`
);
this._logger.debug(`Client ${this.username!} sent an unrecognized packet that could not be parsed!\n${err.stack ?? err}`);
}
}
});
@ -147,21 +121,12 @@ export class Player extends EventEmitter {
this.ws.send(packet.serialize());
}
public async read(
packetId?: Enums.PacketId,
filter?: (packet: Packet) => boolean
): Promise<Packet> {
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
) {
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 {
@ -188,16 +153,7 @@ export class Player extends EventEmitter {
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.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();
@ -208,10 +164,7 @@ export class Player extends EventEmitter {
}
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.`
);
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(
@ -269,9 +222,7 @@ export class Player extends EventEmitter {
)
);
await this._bindListenersMineClient(this.serverConnection, true, () =>
oldConnection.end()
)
await this._bindListenersMineClient(this.serverConnection, true, () => oldConnection.end())
.then(() => {
this.emit("switchServer", this.serverConnection, this);
res();
@ -283,11 +234,7 @@ export class Player extends EventEmitter {
});
}
private async _bindListenersMineClient(
client: Client,
switchingServers?: boolean,
onSwitch?: Function
) {
private async _bindListenersMineClient(client: Client, switchingServers?: boolean, onSwitch?: Function) {
return new Promise((res, rej) => {
let stream = false,
uuid;
@ -300,17 +247,13 @@ export class Player extends EventEmitter {
if (!stream) {
rej(err);
} else {
this.disconnect(
`${Enums.ChatColor.RED}Something went wrong: ${err.stack ?? err}`
);
this.disconnect(`${Enums.ChatColor.RED}Something went wrong: ${err.stack ?? err}`);
}
};
setTimeout(() => {
if (!stream && this.state != Enums.ClientState.DISCONNECTED) {
client.end("Timed out waiting for server connection.");
this.disconnect(
Enums.ChatColor.RED + "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);
@ -349,14 +292,8 @@ export class Player extends EventEmitter {
if (!stream) {
if (switchingServers) {
if (meta.name == "login" && meta.state == states.PLAY && uuid) {
this.translator = new BungeeUtil.PacketUUIDTranslator(
client.uuid,
this.uuid
);
const pckSeq = BungeeUtil.getRespawnSequence(
packet,
this.serverSerializer
);
this.translator = new BungeeUtil.PacketUUIDTranslator(client.uuid, this.uuid);
const pckSeq = BungeeUtil.getRespawnSequence(packet, this.serverSerializer);
this.ws.send(
this.serverSerializer.createPacketBuffer({
name: "login",
@ -367,19 +304,12 @@ export class Player extends EventEmitter {
stream = true;
if (onSwitch) onSwitch();
res(null);
} else if (
meta.name == "success" &&
meta.state == states.LOGIN &&
!uuid
) {
} else if (meta.name == "success" && meta.state == states.LOGIN && !uuid) {
uuid = packet.uuid;
}
} else {
if (meta.name == "login" && meta.state == states.PLAY && uuid) {
this.translator = new BungeeUtil.PacketUUIDTranslator(
client.uuid,
this.uuid
);
this.translator = new BungeeUtil.PacketUUIDTranslator(client.uuid, this.uuid);
this.ws.send(
this.serverSerializer.createPacketBuffer({
name: "login",
@ -389,19 +319,12 @@ export class Player extends EventEmitter {
stream = true;
if (onSwitch) onSwitch();
res(null);
} else if (
meta.name == "success" &&
meta.state == states.LOGIN &&
!uuid
) {
} else if (meta.name == "success" && meta.state == states.LOGIN && !uuid) {
uuid = packet.uuid;
}
}
} else {
const translated = this.translator!.translatePacketServer(
packet,
meta
),
const translated = this.translator!.translatePacketServer(packet, meta),
eventData = {
name: translated[0],
params: translated[1],
@ -444,8 +367,5 @@ export declare interface Player {
on<U extends keyof PlayerEvents>(event: U, listener: PlayerEvents[U]): this;
once<U extends keyof PlayerEvents>(event: U, listener: PlayerEvents[U]): this;
emit<U extends keyof PlayerEvents>(
event: U,
...args: Parameters<PlayerEvents[U]>
): boolean;
emit<U extends keyof PlayerEvents>(event: U, ...args: Parameters<PlayerEvents[U]>): boolean;
}

View File

@ -13,12 +13,7 @@ import SCIdentifyPacket from "./packets/SCIdentifyPacket.js";
import { Motd } from "./Motd.js";
import { Player } from "./Player.js";
import { Enums } from "./Enums.js";
import {
NETWORK_VERSION,
PROXY_BRANDING,
PROXY_VERSION,
VANILLA_PROTOCOL_VERSION,
} from "../meta.js";
import { NETWORK_VERSION, PROXY_BRANDING, PROXY_VERSION, VANILLA_PROTOCOL_VERSION } from "../meta.js";
import { CSUsernamePacket } from "./packets/CSUsernamePacket.js";
import { SCSyncUuidPacket } from "./packets/SCSyncUuidPacket.js";
import { SCReadyPacket } from "./packets/SCReadyPacket.js";
@ -61,42 +56,30 @@ export class Proxy extends EventEmitter {
// hijack the initial handler logger to append [InitialHandler] to the beginning
(this.initalHandlerLogger as any)._info = this.initalHandlerLogger.info;
this.initalHandlerLogger.info = (msg: string) => {
(this.initalHandlerLogger as any)._info(
`${chalk.blue("[InitialHandler]")} ${msg}`
);
(this.initalHandlerLogger as any)._info(`${chalk.blue("[InitialHandler]")} ${msg}`);
};
(this.initalHandlerLogger as any)._warn = this.initalHandlerLogger.warn;
this.initalHandlerLogger.warn = (msg: string) => {
(this.initalHandlerLogger as any)._warn(
`${chalk.blue("[InitialHandler]")} ${msg}`
);
(this.initalHandlerLogger as any)._warn(`${chalk.blue("[InitialHandler]")} ${msg}`);
};
(this.initalHandlerLogger as any)._error = this.initalHandlerLogger.error;
this.initalHandlerLogger.error = (msg: string) => {
(this.initalHandlerLogger as any)._error(
`${chalk.blue("[InitialHandler]")} ${msg}`
);
(this.initalHandlerLogger as any)._error(`${chalk.blue("[InitialHandler]")} ${msg}`);
};
(this.initalHandlerLogger as any)._fatal = this.initalHandlerLogger.fatal;
this.initalHandlerLogger.fatal = (msg: string) => {
(this.initalHandlerLogger as any)._fatal(
`${chalk.blue("[InitialHandler]")} ${msg}`
);
(this.initalHandlerLogger as any)._fatal(`${chalk.blue("[InitialHandler]")} ${msg}`);
};
(this.initalHandlerLogger as any)._debug = this.initalHandlerLogger.debug;
this.initalHandlerLogger.debug = (msg: string) => {
(this.initalHandlerLogger as any)._debug(
`${chalk.blue("[InitialHandler]")} ${msg}`
);
(this.initalHandlerLogger as any)._debug(`${chalk.blue("[InitialHandler]")} ${msg}`);
};
this.config = config;
this.pluginManager = pluginManager;
instanceCount++;
process.on("uncaughtException", (err) => {
this._logger.warn(
`An uncaught exception was caught! Error: ${err.stack}`
);
this._logger.warn(`An uncaught exception was caught! Error: ${err.stack}`);
});
process.on("unhandledRejection", (err) => {
@ -107,16 +90,10 @@ export class Proxy extends EventEmitter {
public async init() {
this._logger.info(`Starting ${PROXY_BRANDING} v${PROXY_VERSION}...`);
global.PROXY = this;
if (this.loaded)
throw new Error(
"Can't initiate if proxy instance is already initialized or is being initialized!"
);
if (this.loaded) throw new Error("Can't initiate if proxy instance is already initialized or is being initialized!");
this.loaded = true;
this.packetRegistry = await loadPackets();
this.skinServer = new EaglerSkins.SkinServer(
this,
this.config.skinUrlWhitelist
);
this.skinServer = new EaglerSkins.SkinServer(this, this.config.skinUrlWhitelist);
global.PACKET_REGISTRY = this.packetRegistry;
if (this.config.motd == "FORWARD") {
this._pollServer(this.config.server.host, this.config.server.port);
@ -136,22 +113,12 @@ export class Proxy extends EventEmitter {
},
(req, res) => this._handleNonWSRequest(req, res, this.config)
)
.listen(
this.config.bindPort || 8080,
this.config.bindHost || "127.0.0.1"
);
.listen(this.config.bindPort || 8080, this.config.bindHost || "127.0.0.1");
this.wsServer = new WebSocketServer({
noServer: true,
});
} else {
this.httpServer = http
.createServer((req, res) =>
this._handleNonWSRequest(req, res, this.config)
)
.listen(
this.config.bindPort || 8080,
this.config.bindHost || "127.0.0.1"
);
this.httpServer = http.createServer((req, res) => this._handleNonWSRequest(req, res, this.config)).listen(this.config.bindPort || 8080, this.config.bindHost || "127.0.0.1");
this.wsServer = new WebSocketServer({
noServer: true,
});
@ -166,73 +133,49 @@ export class Proxy extends EventEmitter {
try {
await this._handleWSConnectionReq(r, s, h);
} catch (err) {
this._logger.error(
`Error was caught whilst trying to handle WebSocket upgrade! Error: ${
err.stack ?? err
}`
);
this._logger.error(`Error was caught whilst trying to handle WebSocket upgrade! Error: ${err.stack ?? err}`);
}
});
this.pluginManager.emit("proxyFinishLoading", this, this.pluginManager);
this._logger.info(
`Started WebSocket server and binded to ${this.config.bindHost} on port ${this.config.bindPort}.`
);
this._logger.info(`Started WebSocket server and binded to ${this.config.bindHost} on port ${this.config.bindPort}.`);
}
private _handleNonWSRequest(
req: http.IncomingMessage,
res: http.ServerResponse,
config: Config["adapter"]
) {
res
.setHeader("Content-Type", "text/html")
.writeHead(426)
.end(UPGRADE_REQUIRED_RESPONSE);
private _handleNonWSRequest(req: http.IncomingMessage, res: http.ServerResponse, config: Config["adapter"]) {
const ctx: Util.Handlable = { handled: false };
this.emit("httpConnection", req, res, ctx);
if (!ctx.handled) res.setHeader("Content-Type", "text/html").writeHead(426).end(UPGRADE_REQUIRED_RESPONSE);
}
readonly LOGIN_TIMEOUT = 30000;
private async _handleWSConnection(ws: WebSocket) {
private async _handleWSConnection(ws: WebSocket, req: http.IncomingMessage) {
const firstPacket = await Util.awaitPacket(ws);
let player: Player, handled: boolean;
setTimeout(() => {
if (!handled) {
this.initalHandlerLogger.warn(
`Disconnecting client ${
player
? player.username ??
`[/${(ws as any)._socket.remoteAddress}:${
(ws as any)._socket.remotePort
}`
: `[/${(ws as any)._socket.remoteAddress}:${
(ws as any)._socket.remotePort
}`
player ? player.username ?? `[/${(ws as any)._socket.remoteAddress}:${(ws as any)._socket.remotePort}` : `[/${(ws as any)._socket.remoteAddress}:${(ws as any)._socket.remotePort}`
} due to connection timing out.`
);
if (player)
player.disconnect(
`${Enums.ChatColor.YELLOW} Your connection timed out whilst processing handshake, please try again.`
);
if (player) player.disconnect(`${Enums.ChatColor.YELLOW} Your connection timed out whilst processing handshake, please try again.`);
else ws.close();
}
}, this.LOGIN_TIMEOUT);
try {
const ctx: Util.Handlable = { handled: false };
await this.emit("wsConnection", ws, req, ctx);
if (ctx.handled) return;
if (firstPacket.toString() === "Accept: MOTD") {
if (this.broadcastMotd) {
if ((this.broadcastMotd as any)._static) {
this.broadcastMotd.jsonMotd.data.online = this.players.size;
// sample for players
this.broadcastMotd.jsonMotd.data.players = [];
const playerSample = [...this.players.keys()]
.filter((sample) => !sample.startsWith("!phs_"))
.slice(0, 5);
const playerSample = [...this.players.keys()].filter((sample) => !sample.startsWith("!phs_")).slice(0, 5);
this.broadcastMotd.jsonMotd.data.players = playerSample;
if (this.players.size - playerSample.length > 0)
this.broadcastMotd.jsonMotd.data.players.push(
`${Enums.ChatColor.GRAY}${Enums.ChatColor.ITALIC}(and ${
this.players.size - playerSample.length
} more)`
);
if (this.players.size - playerSample.length > 0) this.broadcastMotd.jsonMotd.data.players.push(`${Enums.ChatColor.GRAY}${Enums.ChatColor.ITALIC}(and ${this.players.size - playerSample.length} more)`);
const bufferized = this.broadcastMotd.toBuffer();
ws.send(bufferized[0]);
@ -246,24 +189,15 @@ export class Proxy extends EventEmitter {
handled = true;
ws.close();
} else {
player = new Player(ws);
(ws as any).httpRequest = req;
player = new Player(ws as any);
const loginPacket = new CSLoginPacket().deserialize(firstPacket);
player.state = Enums.ClientState.PRE_HANDSHAKE;
if (loginPacket.gameVersion != VANILLA_PROTOCOL_VERSION) {
player.disconnect(
`${Enums.ChatColor.RED}Please connect to this proxy on EaglercraftX 1.8.9.`
);
player.disconnect(`${Enums.ChatColor.RED}Please connect to this proxy on EaglercraftX 1.8.9.`);
return;
} else if (loginPacket.networkVersion != NETWORK_VERSION) {
player.disconnect(
`${Enums.ChatColor.RED}Your EaglercraftX version is too ${
loginPacket.networkVersion > NETWORK_VERSION ? "new" : "old"
}! Please ${
loginPacket.networkVersion > NETWORK_VERSION
? "downgrade"
: "update"
}.`
);
player.disconnect(`${Enums.ChatColor.RED}Your EaglercraftX version is too ${loginPacket.networkVersion > NETWORK_VERSION ? "new" : "old"}! Please ${loginPacket.networkVersion > NETWORK_VERSION ? "downgrade" : "update"}.`);
return;
}
try {
@ -275,35 +209,22 @@ export class Proxy extends EventEmitter {
player.username = loginPacket.username;
player.uuid = Util.generateUUIDFromPlayer(player.username);
if (this.players.size > this.config.maxConcurrentClients) {
player.disconnect(
`${Enums.ChatColor.YELLOW}Proxy is full! Please try again later.`
);
player.disconnect(`${Enums.ChatColor.YELLOW}Proxy is full! Please try again later.`);
return;
} else if (
this.players.get(player.username) != null ||
this.players.get(`!phs.${player.uuid}`) != null
) {
player.disconnect(
`${Enums.ChatColor.YELLOW}Someone under your username (${player.username}) is already connected to the proxy!`
);
} else if (this.players.get(player.username) != null || this.players.get(`!phs.${player.uuid}`) != null) {
player.disconnect(`${Enums.ChatColor.YELLOW}Someone under your username (${player.username}) is already connected to the proxy!`);
return;
}
this.players.set(`!phs.${player.uuid}`, player);
this._logger.info(
`Player ${loginPacket.username} (${Util.generateUUIDFromPlayer(
loginPacket.username
)}) running ${loginPacket.brand}/${loginPacket.version} (net ver: ${
loginPacket.networkVersion
}, game ver: ${loginPacket.gameVersion}) is attempting to connect!`
`Player ${loginPacket.username} (${Util.generateUUIDFromPlayer(loginPacket.username)}) running ${loginPacket.brand}/${loginPacket.version} (net ver: ${loginPacket.networkVersion}, game ver: ${
loginPacket.gameVersion
}) is attempting to connect!`
);
player.write(new SCIdentifyPacket());
const usernamePacket: CSUsernamePacket = (await player.read(
Enums.PacketId.CSUsernamePacket
)) as any;
const usernamePacket: CSUsernamePacket = (await player.read(Enums.PacketId.CSUsernamePacket)) as any;
if (usernamePacket.username !== player.username) {
player.disconnect(
`${Enums.ChatColor.YELLOW}Failed to complete handshake. Your game version may be too old or too new.`
);
player.disconnect(`${Enums.ChatColor.YELLOW}Failed to complete handshake. Your game version may be too old or too new.`);
return;
}
const syncUuid = new SCSyncUuidPacket();
@ -311,12 +232,7 @@ export class Proxy extends EventEmitter {
syncUuid.uuid = player.uuid;
player.write(syncUuid);
const prom = await Promise.all([
player.read(Enums.PacketId.CSReadyPacket),
(await player.read(
Enums.PacketId.CSSetSkinPacket
)) as CSSetSkinPacket,
]),
const prom = await Promise.all([player.read(Enums.PacketId.CSReadyPacket), (await player.read(Enums.PacketId.CSSetSkinPacket)) as CSSetSkinPacket]),
skin = prom[1],
obj = new EaglerSkins.EaglerSkin();
obj.owner = player;
@ -331,45 +247,31 @@ export class Proxy extends EventEmitter {
player.initListeners();
this._bindListenersToPlayer(player);
player.state = Enums.ClientState.POST_HANDSHAKE;
this._logger.info(
`Handshake Success! Connecting player ${player.username} to server...`
);
this._logger.info(`Handshake Success! Connecting player ${player.username} to server...`);
handled = true;
await player.connect({
host: this.config.server.host,
port: this.config.server.port,
username: player.username,
});
this._logger.info(
`Player ${player.username} successfully connected to server.`
);
this._logger.info(`Player ${player.username} successfully connected to server.`);
this.emit("playerConnect", player);
}
} catch (err) {
this.initalHandlerLogger.warn(
`Error occurred whilst handling handshake: ${err.stack ?? err}`
);
this.initalHandlerLogger.warn(`Error occurred whilst handling handshake: ${err.stack ?? err}`);
handled = true;
ws.close();
if (player && player.uuid && this.players.has(`!phs.${player.uuid}`))
this.players.delete(`!phs.${player.uuid}`);
if (player && player.uuid && this.players.has(player.username))
this.players.delete(player.username);
if (player && player.uuid && this.players.has(`!phs.${player.uuid}`)) this.players.delete(`!phs.${player.uuid}`);
if (player && player.uuid && this.players.has(player.username)) this.players.delete(player.username);
}
}
private _bindListenersToPlayer(player: Player) {
let sentDisconnectMsg = false;
player.on("disconnect", () => {
if (this.players.has(player.username))
this.players.delete(player.username);
this.initalHandlerLogger.info(
`DISCONNECT ${player.username} <=> DISCONNECTED`
);
if (!sentDisconnectMsg)
this._logger.info(
`Player ${player.username} (${player.uuid}) disconnected from the proxy server.`
);
if (this.players.has(player.username)) this.players.delete(player.username);
this.initalHandlerLogger.info(`DISCONNECT ${player.username} <=> DISCONNECTED`);
if (!sentDisconnectMsg) this._logger.info(`Player ${player.username} (${player.uuid}) disconnected from the proxy server.`);
});
player.on("proxyPacket", async (packet) => {
if (packet.packetId == Enums.PacketId.CSChannelMessagePacket) {
@ -379,23 +281,15 @@ export class Proxy extends EventEmitter {
await this.skinServer.handleRequest(msg, player);
}
} catch (err) {
this._logger.error(
`Failed to process channel message packet! Error: ${
err.stack || err
}`
);
this._logger.error(`Failed to process channel message packet! Error: ${err.stack || err}`);
}
}
});
player.on("switchServer", (client) => {
this.initalHandlerLogger.info(
`SWITCH_SERVER ${player.username} <=> ${client.socket.remoteAddress}:${client.socket.remotePort}`
);
this.initalHandlerLogger.info(`SWITCH_SERVER ${player.username} <=> ${client.socket.remoteAddress}:${client.socket.remotePort}`);
});
player.on("joinServer", (client) => {
this.initalHandlerLogger.info(
`SERVER_CONNECTED ${player.username} <=> ${client.socket.remoteAddress}:${client.socket.remotePort}`
);
this.initalHandlerLogger.info(`SERVER_CONNECTED ${player.username} <=> ${client.socket.remoteAddress}:${client.socket.remotePort}`);
});
}
@ -404,62 +298,33 @@ export class Proxy extends EventEmitter {
private _pollServer(host: string, port: number, interval?: number) {
(async () => {
while (true) {
const motd = await Motd.MOTD.generateMOTDFromPing(host, port).catch(
(err) => {
this._logger.warn(
`Error polling ${host}:${port} for MOTD: ${err.stack ?? err}`
);
}
);
const motd = await Motd.MOTD.generateMOTDFromPing(host, port).catch((err) => {
this._logger.warn(`Error polling ${host}:${port} for MOTD: ${err.stack ?? err}`);
});
if (motd) this.broadcastMotd = motd;
await new Promise((res) =>
setTimeout(res, interval ?? Proxy.POLL_INTERVAL)
);
await new Promise((res) => setTimeout(res, interval ?? Proxy.POLL_INTERVAL));
}
})();
}
private async _handleWSConnectionReq(
req: http.IncomingMessage,
socket: Duplex,
head: Buffer
) {
const origin =
req.headers.origin == null || req.headers.origin == "null"
? null
: req.headers.origin;
private async _handleWSConnectionReq(req: http.IncomingMessage, socket: Duplex, head: Buffer) {
const origin = req.headers.origin == null || req.headers.origin == "null" ? null : req.headers.origin;
if (!this.config.origins.allowOfflineDownloads && origin == null) {
socket.destroy();
return;
}
if (
this.config.origins.originBlacklist != null &&
this.config.origins.originBlacklist.some((host) =>
Util.areDomainsEqual(host, origin)
)
) {
if (this.config.origins.originBlacklist != null && this.config.origins.originBlacklist.some((host) => Util.areDomainsEqual(host, origin))) {
socket.destroy();
return;
}
if (
this.config.origins.originWhitelist != null &&
!this.config.origins.originWhitelist.some((host) =>
Util.areDomainsEqual(host, origin)
)
) {
if (this.config.origins.originWhitelist != null && !this.config.origins.originWhitelist.some((host) => Util.areDomainsEqual(host, origin))) {
socket.destroy();
return;
}
try {
await this.wsServer.handleUpgrade(req, socket, head, (ws) =>
this._handleWSConnection(ws)
);
await this.wsServer.handleUpgrade(req, socket, head, (ws) => this._handleWSConnection(ws, req));
} catch (err) {
this._logger.error(
`Error was caught whilst trying to handle WebSocket connection request! Error: ${
err.stack ?? err
}`
);
this._logger.error(`Error was caught whilst trying to handle WebSocket connection request! Error: ${err.stack ?? err}`);
socket.destroy();
}
}
@ -475,13 +340,12 @@ export class Proxy extends EventEmitter {
interface ProxyEvents {
playerConnect: (player: Player) => void;
playerDisconnect: (player: Player) => void;
httpConnection: (req: http.IncomingMessage, res: http.ServerResponse, ctx: Util.Handlable) => void;
wsConnection: (ws: WebSocket, req: http.IncomingMessage, ctx: Util.Handlable) => void;
}
export declare interface Proxy {
on<U extends keyof ProxyEvents>(event: U, listener: ProxyEvents[U]): this;
emit<U extends keyof ProxyEvents>(
event: U,
...args: Parameters<ProxyEvents[U]>
): boolean;
emit<U extends keyof ProxyEvents>(event: U, ...args: Parameters<ProxyEvents[U]>): boolean;
}

View File

@ -37,26 +37,18 @@ export namespace Util {
export function uuidStringToBuffer(uuid: string): Buffer {
if (!uuid) return Buffer.alloc(16); // Return empty buffer
const hexStr = uuid.replace(/-/g, "");
if (uuid.length != 36 || hexStr.length != 32)
throw new Error(`Invalid UUID string: ${uuid}`);
if (uuid.length != 36 || hexStr.length != 32) throw new Error(`Invalid UUID string: ${uuid}`);
return Buffer.from(hexStr, "hex");
}
export function uuidBufferToString(buffer: Buffer): string {
if (buffer.length != 16)
throw new Error(`Invalid buffer length for uuid: ${buffer.length}`);
if (buffer.length != 16) throw new Error(`Invalid buffer length for uuid: ${buffer.length}`);
if (buffer.equals(Buffer.alloc(16))) return null; // If buffer is all zeros, return null
const str = buffer.toString("hex");
return `${str.slice(0, 8)}-${str.slice(8, 12)}-${str.slice(
12,
16
)}-${str.slice(16, 20)}-${str.slice(20)}`;
return `${str.slice(0, 8)}-${str.slice(8, 12)}-${str.slice(12, 16)}-${str.slice(16, 20)}-${str.slice(20)}`;
}
export function awaitPacket(
ws: WebSocket,
filter?: (msg: Buffer) => boolean
): Promise<Buffer> {
export function awaitPacket(ws: WebSocket, filter?: (msg: Buffer) => boolean): Promise<Buffer> {
return new Promise<Buffer>((res, rej) => {
let resolved = false;
const msgCb = (msg: any) => {
@ -64,17 +56,13 @@ export namespace Util {
resolved = true;
ws.removeListener("message", msgCb);
ws.removeListener("close", discon);
ws.setMaxListeners(
ws.getMaxListeners() - 2 < 0 ? 5 : ws.getMaxListeners() - 2
);
ws.setMaxListeners(ws.getMaxListeners() - 2 < 0 ? 5 : ws.getMaxListeners() - 2);
res(msg);
} else if (filter == null) {
resolved = true;
ws.removeListener("message", msgCb);
ws.removeListener("close", discon);
ws.setMaxListeners(
ws.getMaxListeners() - 2 < 0 ? 5 : ws.getMaxListeners() - 2
);
ws.setMaxListeners(ws.getMaxListeners() - 2 < 0 ? 5 : ws.getMaxListeners() - 2);
res(msg);
}
};
@ -82,9 +70,7 @@ export namespace Util {
resolved = true;
ws.removeListener("message", msgCb);
ws.removeListener("close", discon);
ws.setMaxListeners(
ws.getMaxListeners() - 2 < 0 ? 5 : ws.getMaxListeners() - 2
);
ws.setMaxListeners(ws.getMaxListeners() - 2 < 0 ? 5 : ws.getMaxListeners() - 2);
rej("Connection closed");
};
ws.setMaxListeners(ws.getMaxListeners() + 2);
@ -93,9 +79,7 @@ export namespace Util {
setTimeout(() => {
ws.removeListener("message", msgCb);
ws.removeListener("close", discon);
ws.setMaxListeners(
ws.getMaxListeners() - 2 < 0 ? 5 : ws.getMaxListeners() - 2
);
ws.setMaxListeners(ws.getMaxListeners() - 2 < 0 ? 5 : ws.getMaxListeners() - 2);
rej("Timed out");
}, 10000);
});
@ -104,57 +88,28 @@ export namespace Util {
export function validateUsername(user: string): void | never {
if (user.length > 20) throw new Error("Username is too long!");
if (user.length < 3) throw new Error("Username is too short!");
if (!!user.match(USERNAME_REGEX))
throw new Error(
"Invalid username. Username can only contain alphanumeric characters, and the underscore (_) character."
);
if (!!user.match(USERNAME_REGEX)) throw new Error("Invalid username. Username can only contain alphanumeric characters, and the underscore (_) character.");
}
export function areDomainsEqual(d1: string, d2: string): boolean {
if (d1.endsWith("*."))
d1 = d1.replace(
"*.",
"WILDCARD-LOL-EXTRA-LONG-SUBDOMAIN-TO-LOWER-CHANCES-OF-COLLISION."
);
if (d1.endsWith("*.")) d1 = d1.replace("*.", "WILDCARD-LOL-EXTRA-LONG-SUBDOMAIN-TO-LOWER-CHANCES-OF-COLLISION.");
const parseResult1 = parseDomain(d1),
parseResult2 = parseDomain(d2);
if (
parseResult1.type != ParseResultType.Invalid &&
parseResult2.type != ParseResultType.Invalid
) {
if (
parseResult1.type == ParseResultType.Ip &&
parseResult2.type == ParseResultType.Ip
) {
if (parseResult1.type != ParseResultType.Invalid && parseResult2.type != ParseResultType.Invalid) {
if (parseResult1.type == ParseResultType.Ip && parseResult2.type == ParseResultType.Ip) {
return parseResult1.hostname == parseResult2.hostname ? true : false;
} else if (
parseResult1.type == ParseResultType.Listed &&
parseResult2.type == ParseResultType.Listed
) {
if (
parseResult1.subDomains[0] ==
"WILDCARD-LOL-EXTRA-LONG-SUBDOMAIN-TO-LOWER-CHANCES-OF-COLLISION"
) {
} else if (parseResult1.type == ParseResultType.Listed && parseResult2.type == ParseResultType.Listed) {
if (parseResult1.subDomains[0] == "WILDCARD-LOL-EXTRA-LONG-SUBDOMAIN-TO-LOWER-CHANCES-OF-COLLISION") {
// wildcard
const domainPlusTld1 =
parseResult1.domain +
("." + parseResult1.topLevelDomains.join("."));
const domainPlusTld2 =
parseResult2.domain +
("." + parseResult2.topLevelDomains.join("."));
const domainPlusTld1 = parseResult1.domain + ("." + parseResult1.topLevelDomains.join("."));
const domainPlusTld2 = parseResult2.domain + ("." + parseResult2.topLevelDomains.join("."));
return domainPlusTld1 == domainPlusTld2 ? true : false;
} else {
// no wildcard
return d1 == d2 ? true : false;
}
} else if (
parseResult1.type == ParseResultType.NotListed &&
parseResult2.type == ParseResultType.NotListed
) {
if (
parseResult1.labels[0] ==
"WILDCARD-LOL-EXTRA-LONG-SUBDOMAIN-TO-LOWER-CHANCES-OF-COLLISION"
) {
} else if (parseResult1.type == ParseResultType.NotListed && parseResult2.type == ParseResultType.NotListed) {
if (parseResult1.labels[0] == "WILDCARD-LOL-EXTRA-LONG-SUBDOMAIN-TO-LOWER-CHANCES-OF-COLLISION") {
// wildcard
const domainPlusTld1 = parseResult1.labels.slice(2).join(".");
const domainPlusTld2 = parseResult1.labels.slice(2).join(".");
@ -163,15 +118,8 @@ export namespace Util {
// no wildcard
return d1 == d2 ? true : false;
}
} else if (
parseResult1.type == ParseResultType.Reserved &&
parseResult2.type == ParseResultType.Reserved
) {
if (
parseResult1.hostname == "" &&
parseResult1.hostname === parseResult2.hostname
)
return true;
} else if (parseResult1.type == ParseResultType.Reserved && parseResult2.type == ParseResultType.Reserved) {
if (parseResult1.hostname == "" && parseResult1.hostname === parseResult2.hostname) return true;
else {
// uncertain, fallback to exact hostname matching
return d1 == d2 ? true : false;
@ -229,10 +177,7 @@ export namespace Util {
flags: number;
};
export function generatePositionPacket(
currentPos: PlayerPosition,
newPos: PositionPacket
): PositionPacket {
export function generatePositionPacket(currentPos: PlayerPosition, newPos: PositionPacket): PositionPacket {
const DEFAULT_RELATIVITY = 0x01; // relative to X-axis
const newPosPacket = {
x: newPos.x - currentPos.x * 2,
@ -244,4 +189,8 @@ export namespace Util {
};
return newPosPacket;
}
export type Handlable = {
handled: boolean;
};
}

View File

@ -19,10 +19,7 @@ import { EaglerSkins } from "../skins/EaglerSkins.js";
import { BungeeUtil } from "../BungeeUtil.js";
export class PluginManager extends EventEmitter {
public plugins: Map<
string,
{ exports: any; metadata: PluginLoaderTypes.PluginMetadataPathed }
>;
public plugins: Map<string, { exports: any; metadata: PluginLoaderTypes.PluginMetadataPathed }>;
public proxy: Proxy;
public Logger: typeof Logger = Logger;
@ -59,179 +56,67 @@ export class PluginManager extends EventEmitter {
}
pluginsString = pluginsString.substring(0, pluginsString.length - 1);
this._logger.info(`Found ${pluginMeta.size} plugin(s): ${pluginsString}`);
if(pluginMeta.size !== 0){
if (pluginMeta.size !== 0) {
this._logger.info(`Loading ${pluginMeta.size} plugin(s)...`);
const successLoadCount = await this._loadPlugins(
pluginMeta,
this._getLoadOrder(pluginMeta)
);
const successLoadCount = await this._loadPlugins(pluginMeta, this._getLoadOrder(pluginMeta));
this._logger.info(`Successfully loaded ${successLoadCount} plugin(s).`);
}
this.emit("pluginsFinishLoading", this);
}
private async _findPlugins(
dir: string
): Promise<Map<string, PluginLoaderTypes.PluginMetadataPathed>> {
private async _findPlugins(dir: string): Promise<Map<string, PluginLoaderTypes.PluginMetadataPathed>> {
const ret: Map<string, PluginLoaderTypes.PluginMetadataPathed> = new Map();
const lsRes = (await Promise.all(
(await fs.readdir(dir))
.filter((ent) => !ent.endsWith(".disabled"))
.map(async (res) => [
pathUtil.join(dir, res),
await fs.stat(pathUtil.join(dir, res)),
])
)) as [string, Stats][];
const lsRes = (await Promise.all((await fs.readdir(dir)).filter((ent) => !ent.endsWith(".disabled")).map(async (res) => [pathUtil.join(dir, res), await fs.stat(pathUtil.join(dir, res))]))) as [string, Stats][];
for (const [path, details] of lsRes) {
if (details.isFile()) {
if (path.endsWith(".jar")) {
this._logger.warn(`Non-EaglerProxy plugin found! (${path})`);
this._logger.warn(
`BungeeCord plugins are NOT supported! Only custom EaglerProxy plugins are allowed.`
);
this._logger.warn(`BungeeCord plugins are NOT supported! Only custom EaglerProxy plugins are allowed.`);
} else if (path.endsWith(".zip")) {
this._logger.warn(`.zip file found in plugin directory! (${path})`);
this._logger.warn(
`A .zip file was found in the plugins directory! Perhaps you forgot to unzip it?`
);
} else
this._logger.debug(`Skipping file found in plugin folder: ${path}`);
this._logger.warn(`A .zip file was found in the plugins directory! Perhaps you forgot to unzip it?`);
} else this._logger.debug(`Skipping file found in plugin folder: ${path}`);
} else {
const metadataPath = pathUtil.resolve(
pathUtil.join(path, "metadata.json")
);
const metadataPath = pathUtil.resolve(pathUtil.join(path, "metadata.json"));
let metadata: PluginLoaderTypes.PluginMetadata;
try {
const file = await fs.readFile(metadataPath);
metadata = JSON.parse(file.toString());
// do some type checking
if (typeof metadata.name != "string")
throw new TypeError(
"<metadata>.name is either null or not of a string type!"
);
if (typeof metadata.id != "string")
throw new TypeError(
"<metadata>.id is either null or not of a string type!"
);
if (/ /gm.test(metadata.id))
throw new Error(`<metadata>.id contains whitespace!`);
if (!semver.valid(metadata.version))
throw new Error(
"<metadata>.version is either null, not a string, or is not a valid SemVer!"
);
if (typeof metadata.entry_point != "string")
throw new TypeError(
"<metadata>.entry_point is either null or not a string!"
);
if (!metadata.entry_point.endsWith(".js"))
throw new Error(
`<metadata>.entry_point (${metadata.entry_point}) references a non-JavaScript file!`
);
if (
!(await Util.fsExists(pathUtil.resolve(path, metadata.entry_point)))
)
throw new Error(
`<metadata>.entry_point (${metadata.entry_point}) references a non-existent file!`
);
if (metadata.requirements instanceof Array == false)
throw new TypeError(
"<metadata>.requirements is either null or not an array!"
);
if (typeof metadata.name != "string") throw new TypeError("<metadata>.name is either null or not of a string type!");
if (typeof metadata.id != "string") throw new TypeError("<metadata>.id is either null or not of a string type!");
if (/ /gm.test(metadata.id)) throw new Error(`<metadata>.id contains whitespace!`);
if (!semver.valid(metadata.version)) throw new Error("<metadata>.version is either null, not a string, or is not a valid SemVer!");
if (typeof metadata.entry_point != "string") throw new TypeError("<metadata>.entry_point is either null or not a string!");
if (!metadata.entry_point.endsWith(".js")) throw new Error(`<metadata>.entry_point (${metadata.entry_point}) references a non-JavaScript file!`);
if (!(await Util.fsExists(pathUtil.resolve(path, metadata.entry_point)))) throw new Error(`<metadata>.entry_point (${metadata.entry_point}) references a non-existent file!`);
if (metadata.requirements instanceof Array == false) throw new TypeError("<metadata>.requirements is either null or not an array!");
for (const requirement of metadata.requirements as PluginLoaderTypes.PluginMetadata["requirements"]) {
if (typeof requirement != "object" || requirement == null)
throw new TypeError(
`<metadata>.requirements[${(
metadata.requirements as any
).indexOf(requirement)}] is either null or not an object!`
);
if (typeof requirement.id != "string")
throw new TypeError(
`<metadata>.requirements[${(
metadata.requirements as any
).indexOf(requirement)}].id is either null or not a string!`
);
if (/ /gm.test(requirement.id))
throw new TypeError(
`<metadata>.requirements[${(
metadata.requirements as any
).indexOf(requirement)}].id contains whitespace!`
);
if (
semver.validRange(requirement.version) == null &&
requirement.version != "any"
)
throw new TypeError(
`<metadata>.requirements[${(
metadata.requirements as any
).indexOf(
requirement
)}].version is either null or not a valid SemVer!`
);
if (typeof requirement != "object" || requirement == null) throw new TypeError(`<metadata>.requirements[${(metadata.requirements as any).indexOf(requirement)}] is either null or not an object!`);
if (typeof requirement.id != "string") throw new TypeError(`<metadata>.requirements[${(metadata.requirements as any).indexOf(requirement)}].id is either null or not a string!`);
if (/ /gm.test(requirement.id)) throw new TypeError(`<metadata>.requirements[${(metadata.requirements as any).indexOf(requirement)}].id contains whitespace!`);
if (semver.validRange(requirement.version) == null && requirement.version != "any")
throw new TypeError(`<metadata>.requirements[${(metadata.requirements as any).indexOf(requirement)}].version is either null or not a valid SemVer!`);
}
if (metadata.load_after instanceof Array == false)
throw new TypeError(
"<metadata>.load_after is either null or not an array!"
);
if (metadata.load_after instanceof Array == false) throw new TypeError("<metadata>.load_after is either null or not an array!");
for (const loadReq of metadata.load_after as string[]) {
if (typeof loadReq != "string")
throw new TypeError(
`<metadata>.load_after[${(metadata.load_after as any).indexOf(
loadReq
)}] is either null, or not a valid ID!`
);
if (/ /gm.test(loadReq))
throw new TypeError(
`<metadata>.load_after[${(metadata.load_after as any).indexOf(
loadReq
)}] contains whitespace!`
);
if (typeof loadReq != "string") throw new TypeError(`<metadata>.load_after[${(metadata.load_after as any).indexOf(loadReq)}] is either null, or not a valid ID!`);
if (/ /gm.test(loadReq)) throw new TypeError(`<metadata>.load_after[${(metadata.load_after as any).indexOf(loadReq)}] contains whitespace!`);
}
if (metadata.incompatibilities instanceof Array == false)
throw new TypeError(
"<metadata>.incompatibilities is either null or not an array!"
);
if (metadata.incompatibilities instanceof Array == false) throw new TypeError("<metadata>.incompatibilities is either null or not an array!");
for (const incompatibility of metadata.incompatibilities as PluginLoaderTypes.PluginMetadata["requirements"]) {
if (typeof incompatibility != "object" || incompatibility == null)
throw new TypeError(
`<metadata>.incompatibilities[${(
metadata.load_after as any
).indexOf(incompatibility)}] is either null or not an object!`
);
if (typeof incompatibility.id != "string")
throw new TypeError(
`<metadata>.incompatibilities[${(
metadata.load_after as any
).indexOf(incompatibility)}].id is either null or not a string!`
);
if (/ /gm.test(incompatibility.id))
throw new TypeError(
`<metadata>.incompatibilities[${(
metadata.load_after as any
).indexOf(incompatibility)}].id contains whitespace!`
);
if (semver.validRange(incompatibility.version) == null)
throw new TypeError(
`<metadata>.incompatibilities[${(
metadata.load_after as any
).indexOf(
incompatibility
)}].version is either null or not a valid SemVer!`
);
if (typeof incompatibility != "object" || incompatibility == null) throw new TypeError(`<metadata>.incompatibilities[${(metadata.load_after as any).indexOf(incompatibility)}] is either null or not an object!`);
if (typeof incompatibility.id != "string") throw new TypeError(`<metadata>.incompatibilities[${(metadata.load_after as any).indexOf(incompatibility)}].id is either null or not a string!`);
if (/ /gm.test(incompatibility.id)) throw new TypeError(`<metadata>.incompatibilities[${(metadata.load_after as any).indexOf(incompatibility)}].id contains whitespace!`);
if (semver.validRange(incompatibility.version) == null) throw new TypeError(`<metadata>.incompatibilities[${(metadata.load_after as any).indexOf(incompatibility)}].version is either null or not a valid SemVer!`);
}
if (ret.has(metadata.id))
throw new Error(
`Duplicate plugin ID detected: ${metadata.id}. Are there duplicate plugins in the plugin folder?`
);
if (ret.has(metadata.id)) throw new Error(`Duplicate plugin ID detected: ${metadata.id}. Are there duplicate plugins in the plugin folder?`);
ret.set(metadata.id, {
path: pathUtil.resolve(path),
...metadata,
});
} catch (err) {
this._logger.warn(
`Failed to load plugin metadata file at ${metadataPath}: ${
err.stack ?? err
}`
);
this._logger.warn(`Failed to load plugin metadata file at ${metadataPath}: ${err.stack ?? err}`);
this._logger.warn("This plugin will skip loading due to an error.");
}
}
@ -239,27 +124,16 @@ export class PluginManager extends EventEmitter {
return ret;
}
private async _validatePluginList(
plugins: Map<string, PluginLoaderTypes.PluginMetadataPathed>
) {
private async _validatePluginList(plugins: Map<string, PluginLoaderTypes.PluginMetadataPathed>) {
for (const [id, plugin] of plugins) {
for (const req of plugin.requirements) {
if (
!plugins.has(req.id) &&
req.id != "eaglerproxy" &&
!req.id.startsWith("module:")
) {
this._logger.fatal(
`Error whilst loading plugins: Plugin ${plugin.name}@${plugin.version} requires plugin ${req.id}@${req.version}, but it is not found!`
);
if (!plugins.has(req.id) && req.id != "eaglerproxy" && !req.id.startsWith("module:")) {
this._logger.fatal(`Error whilst loading plugins: Plugin ${plugin.name}@${plugin.version} requires plugin ${req.id}@${req.version}, but it is not found!`);
this._logger.fatal("Loading has halted due to missing dependencies.");
process.exit(1);
}
if (req.id == "eaglerproxy") {
if (
!semver.satisfies(PROXY_VERSION, req.version) &&
req.version != "any"
) {
if (!semver.satisfies(PROXY_VERSION, req.version) && req.version != "any") {
this._logger.fatal(
`Error whilst loading plugins: Plugin ${plugin.name}@${plugin.version} requires a proxy version that satisfies the SemVer requirement ${req.version}, but the proxy version is ${PROXY_VERSION} and does not satisfy the SemVer requirement!`
);
@ -272,32 +146,19 @@ export class PluginManager extends EventEmitter {
await import(moduleName);
} catch (err) {
if (err.code == "ERR_MODULE_NOT_FOUND") {
this._logger.fatal(`Plugin ${plugin.name}@${plugin.version} requires NPM module ${moduleName}${req.version == "any" ? "" : `@${req.version}`} to be installed, but it is not found!`);
this._logger.fatal(
`Plugin ${plugin.name}@${
plugin.version
} requires NPM module ${moduleName}${
req.version == "any" ? "" : `@${req.version}`
} to be installed, but it is not found!`
);
this._logger.fatal(
`Please install this missing package by running "npm install ${moduleName}${
req.version == "any" ? "" : `@${req.version}`
}". If you're using yarn, run "yarn add ${moduleName}${
`Please install this missing package by running "npm install ${moduleName}${req.version == "any" ? "" : `@${req.version}`}". If you're using yarn, run "yarn add ${moduleName}${
req.version == "any" ? "" : `@${req.version}`
}" instead.`
);
this._logger.fatal(
"Loading has halted due to dependency issues."
);
this._logger.fatal("Loading has halted due to dependency issues.");
process.exit(1);
}
}
} else {
let dep = plugins.get(req.id);
if (
!semver.satisfies(dep.version, req.version) &&
req.version != "any"
) {
if (!semver.satisfies(dep.version, req.version) && req.version != "any") {
this._logger.fatal(
`Error whilst loading plugins: Plugin ${plugin.name}@${plugin.version} requires a version of plugin ${dep.name} that satisfies the SemVer requirement ${req.version}, but the plugin ${dep.name}'s version is ${dep.version} and does not satisfy the SemVer requirement!`
);
@ -313,19 +174,13 @@ export class PluginManager extends EventEmitter {
this._logger.fatal(
`Error whilst loading plugins: Plugin incompatibility found! Plugin ${plugin.name}@${plugin.version} is incompatible with ${plugin_incomp.name}@${plugin_incomp.version} as it satisfies the SemVer requirement of ${incomp.version}!`
);
this._logger.fatal(
"Loading has halted due to plugin incompatibility issues."
);
this._logger.fatal("Loading has halted due to plugin incompatibility issues.");
process.exit(1);
}
} else if (incomp.id == "eaglerproxy") {
if (semver.satisfies(PROXY_VERSION, incomp.version)) {
this._logger.fatal(
`Error whilst loading plugins: Plugin ${plugin.name}@${plugin.version} is incompatible with proxy version ${PROXY_VERSION} as it satisfies the SemVer requirement of ${incomp.version}!`
);
this._logger.fatal(
"Loading has halted due to plugin incompatibility issues."
);
this._logger.fatal(`Error whilst loading plugins: Plugin ${plugin.name}@${plugin.version} is incompatible with proxy version ${PROXY_VERSION} as it satisfies the SemVer requirement of ${incomp.version}!`);
this._logger.fatal("Loading has halted due to plugin incompatibility issues.");
process.exit(1);
}
}
@ -333,9 +188,7 @@ export class PluginManager extends EventEmitter {
}
}
private _getLoadOrder(
plugins: Map<string, PluginLoaderTypes.PluginMetadataPathed>
): PluginLoaderTypes.PluginLoadOrder {
private _getLoadOrder(plugins: Map<string, PluginLoaderTypes.PluginMetadataPathed>): PluginLoaderTypes.PluginLoadOrder {
let order = [],
lastPlugin: any;
plugins.forEach((v) => order.push(v.id));
@ -362,21 +215,12 @@ export class PluginManager extends EventEmitter {
return order;
}
private async _loadPlugins(
plugins: Map<string, PluginLoaderTypes.PluginMetadataPathed>,
order: PluginLoaderTypes.PluginLoadOrder
): Promise<number> {
private async _loadPlugins(plugins: Map<string, PluginLoaderTypes.PluginMetadataPathed>, order: PluginLoaderTypes.PluginLoadOrder): Promise<number> {
let successCount = 0;
for (const id of order) {
let pluginMeta = plugins.get(id);
try {
const imp = await import(
process.platform == "win32"
? pathToFileURL(
pathUtil.join(pluginMeta.path, pluginMeta.entry_point)
).toString()
: pathUtil.join(pluginMeta.path, pluginMeta.entry_point)
);
const imp = await import(process.platform == "win32" ? pathToFileURL(pathUtil.join(pluginMeta.path, pluginMeta.entry_point)).toString() : pathUtil.join(pluginMeta.path, pluginMeta.entry_point));
this.plugins.set(pluginMeta.id, {
exports: imp,
metadata: pluginMeta,
@ -384,11 +228,7 @@ export class PluginManager extends EventEmitter {
successCount++;
this.emit("pluginLoad", pluginMeta.id, imp);
} catch (err) {
this._logger.warn(
`Failed to load plugin entry point for plugin (${
pluginMeta.name
}) at ${pluginMeta.path}: ${err.stack ?? err}`
);
this._logger.warn(`Failed to load plugin entry point for plugin (${pluginMeta.name}) at ${pluginMeta.path}: ${err.stack ?? err}`);
this._logger.warn("This plugin will skip loading due to an error.");
}
return successCount;
@ -403,18 +243,9 @@ interface PluginManagerEvents {
}
export declare interface PluginManager {
on<U extends keyof PluginManagerEvents>(
event: U,
listener: PluginManagerEvents[U]
): this;
on<U extends keyof PluginManagerEvents>(event: U, listener: PluginManagerEvents[U]): this;
emit<U extends keyof PluginManagerEvents>(
event: U,
...args: Parameters<PluginManagerEvents[U]>
): boolean;
emit<U extends keyof PluginManagerEvents>(event: U, ...args: Parameters<PluginManagerEvents[U]>): boolean;
once<U extends keyof PluginManagerEvents>(
event: U,
listener: PluginManagerEvents[U]
): this;
once<U extends keyof PluginManagerEvents>(event: U, listener: PluginManagerEvents[U]): this;
}