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, bindInternalServerPort: 25569,
bindInternalServerIp: "127.0.0.1", bindInternalServerIp: "127.0.0.1",
allowCustomPorts: true, allowCustomPorts: true,
disallowHypixel: false,
authentication: {
enabled: true,
password: "nope",
},
}; };

View File

@ -1,25 +1,15 @@
import { config } from "./config.js"; import { config } from "./config.js";
import { createServer } from "minecraft-protocol"; import { createServer } from "minecraft-protocol";
import { ClientState, ConnectionState, ServerGlobals } from "./types.js"; 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 path from "path";
import { readFileSync } from "fs"; import { readFileSync } from "fs";
import { handleCommand } from "./commands.js";
import { registerEndpoints } from "./service/endpoints.js";
const PluginManager = PLUGIN_MANAGER; const PluginManager = PLUGIN_MANAGER;
const metadata = JSON.parse( const metadata = JSON.parse(
readFileSync( 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()
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; const Logger = PluginManager.Logger;
@ -31,14 +21,11 @@ const Player = PluginManager.Player;
const MineProtocol = PluginManager.MineProtocol; const MineProtocol = PluginManager.MineProtocol;
const EaglerSkins = PluginManager.EaglerSkins; const EaglerSkins = PluginManager.EaglerSkins;
const Util = PluginManager.Util; const Util = PluginManager.Util;
hushConsole(); hushConsole();
const logger = new Logger("EaglerProxyAAS"); const logger = new Logger("EaglerProxyAAS");
logger.info(`Starting ${metadata.name} v${metadata.version}...`); logger.info(`Starting ${metadata.name} v${metadata.version}...`);
logger.info( logger.info(`(internal server port: ${config.bindInternalServerPort}, internal server IP: ${config.bindInternalServerPort})`);
`(internal server port: ${config.bindInternalServerPort}, internal server IP: ${config.bindInternalServerPort})`
);
logger.info("Starting internal server..."); logger.info("Starting internal server...");
let server = createServer({ let server = createServer({
@ -55,14 +42,161 @@ let server = createServer({
setSG(sGlobals); setSG(sGlobals);
server.on("login", (client) => { server.on("login", (client) => {
logger.info( const proxyPlayer = PluginManager.proxy.players.get(client.username);
`Client ${client.username} has connected to the authentication server.` 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", () => { client.on("end", () => {
sGlobals.players.delete(client.username); sGlobals.players.delete(client.username);
logger.info( logger.info(`Client ${client.username} has disconnected from the authentication server.`);
`Client ${client.username} has disconnected from the authentication server.`
);
}); });
const cs: ClientState = { const cs: ClientState = {
state: ConnectionState.AUTH, state: ConnectionState.AUTH,
@ -72,11 +206,14 @@ server.on("login", (client) => {
}; };
sGlobals.players.set(client.username, cs); sGlobals.players.set(client.username, cs);
handleConnect(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( logger.info("Redirecting backend server IP... (this is required for the plugin to function)");
"Redirecting backend server IP... (this is required for the plugin to function)"
);
CONFIG.adapter.server = { CONFIG.adapter.server = {
host: config.bindInternalServerIp, host: config.bindInternalServerIp,
port: config.bindInternalServerPort, port: config.bindInternalServerPort,
@ -84,3 +221,7 @@ CONFIG.adapter.server = {
CONFIG.adapter.motd = { CONFIG.adapter.motd = {
l1: Enums.ChatColor.GOLD + "EaglerProxy as a Service", 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() { export function disconectIdle() {
SERVER.players.forEach((client) => { SERVER.players.forEach((client) => {
if ( if (client.state == ConnectionState.AUTH && Date.now() - client.lastStatusUpdate > MAX_LIFETIME_AUTH) {
client.state == ConnectionState.AUTH && client.gameClient.end("Timed out waiting for user to login via Microsoft");
Date.now() - client.lastStatusUpdate > MAX_LIFETIME_AUTH } 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.");
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); onConnect(client);
} }
export function awaitCommand( export function awaitCommand(client: Client, filter: (msg: string) => boolean): Promise<string> {
client: Client,
filter: (msg: string) => boolean
): Promise<string> {
return new Promise<string>((res, rej) => { return new Promise<string>((res, rej) => {
const onMsg = (packet) => { const onMsg = (packet) => {
if (filter(packet.message)) { if (filter(packet.message)) {
@ -105,8 +91,7 @@ export function awaitCommand(
res(packet.message); res(packet.message);
} }
}; };
const onEnd = () => const onEnd = () => rej("Client disconnected before promise could be resolved");
rej("Client disconnected before promise could be resolved");
client.on("chat", onMsg); client.on("chat", onMsg);
client.on("end", onEnd); client.on("end", onEnd);
}); });
@ -119,12 +104,7 @@ export function sendMessage(client: Client, msg: string) {
}); });
} }
export function sendCustomMessage( export function sendCustomMessage(client: Client, msg: string, color: string, ...components: { text: string; color: string }[]) {
client: Client,
msg: string,
color: string,
...components: { text: string; color: string }[]
) {
client.write("chat", { client.write("chat", {
message: JSON.stringify( message: JSON.stringify(
components.length > 0 components.length > 0
@ -198,12 +178,7 @@ export function sendMessageLogin(client: Client, url: string, token: string) {
}); });
} }
export function updateState( export function updateState(client: Client, newState: "CONNECTION_TYPE" | "AUTH_EASYMC" | "AUTH" | "SERVER", uri?: string, code?: string) {
client: Client,
newState: "CONNECTION_TYPE" | "AUTH_EASYMC" | "AUTH" | "SERVER",
uri?: string,
code?: string
) {
switch (newState) { switch (newState) {
case "CONNECTION_TYPE": case "CONNECTION_TYPE":
client.write("playerlist_header", { client.write("playerlist_header", {
@ -226,10 +201,7 @@ export function updateState(
}); });
break; break;
case "AUTH": case "AUTH":
if (code == null || uri == null) if (code == null || uri == null) throw new Error("Missing code/uri required for title message type AUTH");
throw new Error(
"Missing code/uri required for title message type AUTH"
);
client.write("playerlist_header", { client.write("playerlist_header", {
header: JSON.stringify({ header: JSON.stringify({
text: ` ${Enums.ChatColor.GOLD}EaglerProxy Authentication Server `, text: ` ${Enums.ChatColor.GOLD}EaglerProxy Authentication Server `,
@ -245,9 +217,7 @@ export function updateState(
text: ` ${Enums.ChatColor.GOLD}EaglerProxy Authentication Server `, text: ` ${Enums.ChatColor.GOLD}EaglerProxy Authentication Server `,
}), }),
footer: JSON.stringify({ footer: JSON.stringify({
text: `${Enums.ChatColor.RED}/join <ip>${ text: `${Enums.ChatColor.RED}/join <ip>${config.allowCustomPorts ? " [port]" : ""}`,
config.allowCustomPorts ? " [port]" : ""
}`,
}), }),
}); });
break; break;
@ -271,10 +241,7 @@ export async function onConnect(client: ClientState) {
client.state = ConnectionState.AUTH; client.state = ConnectionState.AUTH;
client.lastStatusUpdate = Date.now(); client.lastStatusUpdate = Date.now();
sendMessageWarning( 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.`);
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)); await new Promise((res) => setTimeout(res, 2000));
sendMessageWarning( sendMessageWarning(
@ -283,12 +250,20 @@ export async function onConnect(client: ClientState) {
); );
await new Promise((res) => setTimeout(res, 2000)); await new Promise((res) => setTimeout(res, 2000));
sendMessageWarning( 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.`);
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)); 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"); sendCustomMessage(client.gameClient, "What would you like to do?", "gray");
sendChatComponent(client.gameClient, { sendChatComponent(client.gameClient, {
text: "1) ", text: "1) ",
@ -344,11 +319,7 @@ export async function onConnect(client: ClientState) {
value: "$3", value: "$3",
}, },
}); });
sendCustomMessage( 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");
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"); updateState(client.gameClient, "CONNECTION_TYPE");
let chosenOption: ConnectType | null = null; let chosenOption: ConnectType | null = null;
@ -356,11 +327,7 @@ export async function onConnect(client: ClientState) {
const option = await awaitCommand(client.gameClient, (msg) => true); const option = await awaitCommand(client.gameClient, (msg) => true);
switch (option.replace(/\$/gim, "")) { switch (option.replace(/\$/gim, "")) {
default: default:
sendCustomMessage( sendCustomMessage(client.gameClient, `I don't understand what you meant by "${option}", please reply with a valid option!`, "red");
client.gameClient,
`I don't understand what you meant by "${option}", please reply with a valid option!`,
"red"
);
break; break;
case "1": case "1":
chosenOption = ConnectType.ONLINE; chosenOption = ConnectType.ONLINE;
@ -390,17 +357,8 @@ export async function onConnect(client: ClientState) {
savedAuth; savedAuth;
const authHandler = auth(), const authHandler = auth(),
codeCallback = (code: ServerDeviceCodeResponse) => { codeCallback = (code: ServerDeviceCodeResponse) => {
updateState( updateState(client.gameClient, "AUTH", code.verification_uri, code.user_code);
client.gameClient, sendMessageLogin(client.gameClient, code.verification_uri, code.user_code);
"AUTH",
code.verification_uri,
code.user_code
);
sendMessageLogin(
client.gameClient,
code.verification_uri,
code.user_code
);
}; };
authHandler.once("error", (err) => { authHandler.once("error", (err) => {
if (!client.gameClient.ended) client.gameClient.end(err.message); if (!client.gameClient.ended) client.gameClient.end(err.message);
@ -415,64 +373,30 @@ export async function onConnect(client: ClientState) {
res(result); res(result);
}) })
); );
sendMessage( sendMessage(client.gameClient, Enums.ChatColor.BRIGHT_GREEN + "Successfully logged into Minecraft!");
client.gameClient,
Enums.ChatColor.BRIGHT_GREEN + "Successfully logged into Minecraft!"
);
client.state = ConnectionState.SUCCESS; client.state = ConnectionState.SUCCESS;
client.lastStatusUpdate = Date.now(); client.lastStatusUpdate = Date.now();
updateState(client.gameClient, "SERVER"); updateState(client.gameClient, "SERVER");
sendMessage( sendMessage(client.gameClient, `Provide a server to join. ${Enums.ChatColor.GOLD}/join <ip>${config.allowCustomPorts ? " [port]" : ""}${Enums.ChatColor.RESET}.`);
client.gameClient,
`Provide a server to join. ${Enums.ChatColor.GOLD}/join <ip>${
config.allowCustomPorts ? " [port]" : ""
}${Enums.ChatColor.RESET}.`
);
let host: string, port: number; let host: string, port: number;
while (true) { while (true) {
const msg = await awaitCommand(client.gameClient, (msg) => const msg = await awaitCommand(client.gameClient, (msg) => msg.startsWith("/join")),
msg.startsWith("/join")
),
parsed = msg.split(/ /gi, 3); parsed = msg.split(/ /gi, 3);
if (parsed.length < 2) 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}.`);
sendMessage( 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}.`);
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 { else {
host = parsed[1]; host = parsed[1];
if (parsed.length > 2) port = parseInt(parsed[2]); if (parsed.length > 2) port = parseInt(parsed[2]);
if (port != null && !config.allowCustomPorts) { if (port != null && !config.allowCustomPorts) {
sendCustomMessage( sendCustomMessage(client.gameClient, "You are not allowed to use custom server ports! /join <ip>" + (config.allowCustomPorts ? " [port]" : ""), "red");
client.gameClient,
"You are not allowed to use custom server ports! /join <ip>" +
(config.allowCustomPorts ? " [port]" : ""),
"red"
);
host = null; host = null;
port = null; port = null;
} else { } else {
if ( if (host.match(/^(?:\*\.)?((?!hypixel\.net$)[^.]+\.)*hypixel\.net$/) && config.disallowHypixel) {
host.match(/^(?:\*\.)?((?!hypixel\.net$)[^.]+\.)*hypixel\.net$/)
) {
sendCustomMessage( sendCustomMessage(
client.gameClient, 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>" + "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]" : ""),
(config.allowCustomPorts ? " [port]" : ""),
"red" "red"
); );
} else { } else {
@ -505,21 +429,10 @@ export async function onConnect(client: ClientState) {
}, },
], ],
}); });
logger.info( 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!`);
`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);
);
const player = PLUGIN_MANAGER.proxy.players.get(
client.gameClient.username
);
player.on("vanillaPacket", (packet, origin) => { player.on("vanillaPacket", (packet, origin) => {
if ( if (origin == "CLIENT" && packet.name == "chat" && (packet.params.message as string).toLowerCase().startsWith("/eag-") && !packet.cancel) {
origin == "CLIENT" &&
packet.name == "chat" &&
(packet.params.message as string)
.toLowerCase()
.startsWith("/eag-") &&
!packet.cancel
) {
packet.cancel = true; packet.cancel = true;
handleCommand(player, packet.params.message as string); handleCommand(player, packet.params.message as string);
} }
@ -560,13 +473,7 @@ export async function onConnect(client: ClientState) {
if (!client.gameClient.ended) { if (!client.gameClient.ended) {
client.gameClient.end( client.gameClient.end(
Enums.ChatColor.RED + Enums.ChatColor.RED +
`Something went wrong whilst switching servers: ${err.message}${ `Something went wrong whilst switching servers: ${err.message}${err.code == "ENOTFOUND" ? (host.includes(":") ? `\n${Enums.ChatColor.GRAY}Suggestion: Replace the : in your IP with a space.` : "\nIs that IP valid?") : ""}`
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(); client.lastStatusUpdate = Date.now();
updateState(client.gameClient, "AUTH_EASYMC"); updateState(client.gameClient, "AUTH_EASYMC");
sendMessageWarning( 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.`);
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, { sendChatComponent(client.gameClient, {
text: "Please generate an alt token at ", text: "Please generate an alt token at ",
color: "white", color: "white",
@ -621,9 +525,7 @@ export async function onConnect(client: ClientState) {
let appendOptions: any; let appendOptions: any;
while (true) { while (true) {
const tokenResponse = await awaitCommand(client.gameClient, (msg) => const tokenResponse = await awaitCommand(client.gameClient, (msg) => msg.toLowerCase().startsWith("/login")),
msg.toLowerCase().startsWith("/login")
),
splitResponse = tokenResponse.split(/ /gim, 2).slice(1); splitResponse = tokenResponse.split(/ /gim, 2).slice(1);
if (splitResponse.length != 1) { if (splitResponse.length != 1) {
sendChatComponent(client.gameClient, { sendChatComponent(client.gameClient, {
@ -660,9 +562,7 @@ export async function onConnect(client: ClientState) {
color: "white", color: "white",
hoverEvent: { hoverEvent: {
action: "show_text", action: "show_text",
value: value: Enums.ChatColor.GOLD + "Click me to open in a new window!",
Enums.ChatColor.GOLD +
"Click me to open in a new window!",
}, },
clickEvent: { clickEvent: {
action: "open_url", action: "open_url",
@ -692,18 +592,10 @@ export async function onConnect(client: ClientState) {
], ],
}); });
} else { } else {
sendCustomMessage( sendCustomMessage(client.gameClient, "Validating alt token...", "gray");
client.gameClient,
"Validating alt token...",
"gray"
);
try { try {
appendOptions = await getTokenProfileEasyMc(token); appendOptions = await getTokenProfileEasyMc(token);
sendCustomMessage( 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");
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; break;
} catch (err) { } catch (err) {
sendChatComponent(client.gameClient, { sendChatComponent(client.gameClient, {
@ -736,45 +628,18 @@ export async function onConnect(client: ClientState) {
client.state = ConnectionState.SUCCESS; client.state = ConnectionState.SUCCESS;
client.lastStatusUpdate = Date.now(); client.lastStatusUpdate = Date.now();
updateState(client.gameClient, "SERVER"); updateState(client.gameClient, "SERVER");
sendMessage( sendMessage(client.gameClient, `Provide a server to join. ${Enums.ChatColor.GOLD}/join <ip>${config.allowCustomPorts ? " [port]" : ""}${Enums.ChatColor.RESET}.`);
client.gameClient,
`Provide a server to join. ${Enums.ChatColor.GOLD}/join <ip>${
config.allowCustomPorts ? " [port]" : ""
}${Enums.ChatColor.RESET}.`
);
let host: string, port: number; let host: string, port: number;
while (true) { while (true) {
const msg = await awaitCommand(client.gameClient, (msg) => const msg = await awaitCommand(client.gameClient, (msg) => msg.startsWith("/join")),
msg.startsWith("/join")
),
parsed = msg.split(/ /gi, 3); parsed = msg.split(/ /gi, 3);
if (parsed.length < 2) 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}.`);
sendMessage( 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}.`);
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 { else {
host = parsed[1]; host = parsed[1];
if (parsed.length > 2) port = parseInt(parsed[2]); if (parsed.length > 2) port = parseInt(parsed[2]);
if (port != null && !config.allowCustomPorts) { if (port != null && !config.allowCustomPorts) {
sendCustomMessage( sendCustomMessage(client.gameClient, "You are not allowed to use custom server ports! /join <ip>", "red");
client.gameClient,
"You are not allowed to use custom server ports! /join <ip>",
"red"
);
host = null; host = null;
port = null; port = null;
} else { } else {
@ -806,21 +671,10 @@ export async function onConnect(client: ClientState) {
}, },
], ],
}); });
logger.info( 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!`);
`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);
);
const player = PLUGIN_MANAGER.proxy.players.get(
client.gameClient.username
);
player.on("vanillaPacket", (packet, origin) => { player.on("vanillaPacket", (packet, origin) => {
if ( if (origin == "CLIENT" && packet.name == "chat" && (packet.params.message as string).toLowerCase().startsWith("/eag-") && !packet.cancel) {
origin == "CLIENT" &&
packet.name == "chat" &&
(packet.params.message as string)
.toLowerCase()
.startsWith("/eag-") &&
!packet.cancel
) {
packet.cancel = true; packet.cancel = true;
handleCommand(player, packet.params.message as string); handleCommand(player, packet.params.message as string);
} }
@ -843,13 +697,7 @@ export async function onConnect(client: ClientState) {
if (!client.gameClient.ended) { if (!client.gameClient.ended) {
client.gameClient.end( client.gameClient.end(
Enums.ChatColor.RED + Enums.ChatColor.RED +
`Something went wrong whilst switching servers: ${err.message}${ `Something went wrong whilst switching servers: ${err.message}${err.code == "ENOTFOUND" ? (host.includes(":") ? `\n${Enums.ChatColor.GRAY}Suggestion: Replace the : in your IP with a space.` : "\nIs that IP valid?") : ""}`
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.state = ConnectionState.SUCCESS;
client.lastStatusUpdate = Date.now(); client.lastStatusUpdate = Date.now();
updateState(client.gameClient, "SERVER"); updateState(client.gameClient, "SERVER");
sendMessage( sendMessage(client.gameClient, `Provide a server to join. ${Enums.ChatColor.GOLD}/join <ip>${config.allowCustomPorts ? " [port]" : ""}${Enums.ChatColor.RESET}.`);
client.gameClient,
`Provide a server to join. ${Enums.ChatColor.GOLD}/join <ip>${
config.allowCustomPorts ? " [port]" : ""
}${Enums.ChatColor.RESET}.`
);
let host: string, port: number; let host: string, port: number;
while (true) { while (true) {
const msg = await awaitCommand(client.gameClient, (msg) => const msg = await awaitCommand(client.gameClient, (msg) => msg.startsWith("/join")),
msg.startsWith("/join")
),
parsed = msg.split(/ /gi, 3); parsed = msg.split(/ /gi, 3);
if (parsed.length < 2) 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}.`);
sendMessage( 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}.`);
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 { else {
host = parsed[1]; host = parsed[1];
if (parsed.length > 2) port = parseInt(parsed[2]); if (parsed.length > 2) port = parseInt(parsed[2]);
if (port != null && !config.allowCustomPorts) { if (port != null && !config.allowCustomPorts) {
sendCustomMessage( sendCustomMessage(client.gameClient, "You are not allowed to use custom server ports! /join <ip>", "red");
client.gameClient,
"You are not allowed to use custom server ports! /join <ip>",
"red"
);
host = null; host = null;
port = null; port = null;
} else { } else {
@ -927,21 +748,10 @@ export async function onConnect(client: ClientState) {
}, },
], ],
}); });
logger.info( logger.info(`Player ${client.gameClient.username} is attempting to connect to ${host}:${port} under their Eaglercraft username (${client.gameClient.username}) using offline mode!`);
`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);
);
const player = PLUGIN_MANAGER.proxy.players.get(
client.gameClient.username
);
player.on("vanillaPacket", (packet, origin) => { player.on("vanillaPacket", (packet, origin) => {
if ( if (origin == "CLIENT" && packet.name == "chat" && (packet.params.message as string).toLowerCase().startsWith("/eag-") && !packet.cancel) {
origin == "CLIENT" &&
packet.name == "chat" &&
(packet.params.message as string)
.toLowerCase()
.startsWith("/eag-") &&
!packet.cancel
) {
packet.cancel = true; packet.cancel = true;
handleCommand(player, packet.params.message as string); handleCommand(player, packet.params.message as string);
} }
@ -961,122 +771,32 @@ export async function onConnect(client: ClientState) {
if (!client.gameClient.ended) { if (!client.gameClient.ended) {
client.gameClient.end( client.gameClient.end(
Enums.ChatColor.RED + Enums.ChatColor.RED +
`Something went wrong whilst switching servers: ${err.message}${ `Something went wrong whilst switching servers: ${err.message}${err.code == "ENOTFOUND" ? (host.includes(":") ? `\n${Enums.ChatColor.GRAY}Suggestion: Replace the : in your IP with a space.` : "\nIs that IP valid?") : ""}`
err.code == "ENOTFOUND"
? host.includes(":")
? `\n${Enums.ChatColor.GRAY}Suggestion: Replace the : in your IP with a space.`
: "\nIs that IP valid?"
: ""
}`
); );
} }
} }
} }
} catch (err) { } catch (err) {
if (!client.gameClient.ended) { if (!client.gameClient.ended) {
logger.error( logger.error(`Error whilst processing user ${client.gameClient.username}: ${err.stack || err}`);
`Error whilst processing user ${client.gameClient.username}: ${ client.gameClient.end(Enums.ChatColor.YELLOW + "Something went wrong whilst processing your request. Please reconnect.");
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( 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));
new McBlock( chunk.setBlock(new Vec3(8, 67, 8), new McBlock(REGISTRY.blocksByName.barrier.id, REGISTRY.biomesByName.the_end.id, 0));
REGISTRY.blocksByName.air.id, chunk.setBlock(new Vec3(7, 65, 8), new McBlock(REGISTRY.blocksByName.barrier.id, REGISTRY.biomesByName.the_end.id, 0));
REGISTRY.biomesByName.the_end.id, chunk.setBlock(new Vec3(7, 66, 8), new McBlock(REGISTRY.blocksByName.barrier.id, REGISTRY.biomesByName.the_end.id, 0));
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( chunk.setBlock(new Vec3(8, 66, 7), new McBlock(REGISTRY.blocksByName.barrier.id, REGISTRY.biomesByName.the_end.id, 0));
new Vec3(8, 64, 8), chunk.setBlock(new Vec3(8, 65, 9), new McBlock(REGISTRY.blocksByName.barrier.id, REGISTRY.biomesByName.the_end.id, 0));
new McBlock( chunk.setBlock(new Vec3(8, 66, 9), new McBlock(REGISTRY.blocksByName.barrier.id, REGISTRY.biomesByName.the_end.id, 0));
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, 65, 8), 15);
chunk.setBlockLight(new Vec3(8, 66, 8), 15); chunk.setBlockLight(new Vec3(8, 66, 8), 15);
return chunk; return chunk;

View File

@ -1,10 +1,5 @@
import EventEmitter from "events"; import EventEmitter from "events";
import pkg, { import pkg, { Client, ClientOptions, createClient, states } from "minecraft-protocol";
Client,
ClientOptions,
createClient,
states,
} from "minecraft-protocol";
import { WebSocket } from "ws"; import { WebSocket } from "ws";
import { Logger } from "../logger.js"; import { Logger } from "../logger.js";
import { Chat } from "./Chat.js"; import { Chat } from "./Chat.js";
@ -15,11 +10,12 @@ import { MineProtocol } from "./Protocol.js";
import { EaglerSkins } from "./skins/EaglerSkins.js"; import { EaglerSkins } from "./skins/EaglerSkins.js";
import { Util } from "./Util.js"; import { Util } from "./Util.js";
import { BungeeUtil } from "./BungeeUtil.js"; import { BungeeUtil } from "./BungeeUtil.js";
import { IncomingMessage } from "http";
const { createSerializer, createDeserializer } = pkg; const { createSerializer, createDeserializer } = pkg;
export class Player extends EventEmitter { export class Player extends EventEmitter {
public ws: WebSocket; public ws: WebSocket & { httpRequest: IncomingMessage };
public username?: string; public username?: string;
public skin?: EaglerSkins.EaglerSkin; public skin?: EaglerSkins.EaglerSkin;
public uuid?: string; public uuid?: string;
@ -37,14 +33,13 @@ export class Player extends EventEmitter {
public clientDeserializer: any; public clientDeserializer: any;
private _kickMessage: string; private _kickMessage: string;
constructor(ws: WebSocket, playerName?: string, serverConnection?: Client) { constructor(ws: WebSocket & { httpRequest: IncomingMessage }, 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) if (this.username != null) this.uuid = Util.generateUUIDFromPlayer(this.username);
this.uuid = Util.generateUUIDFromPlayer(this.username);
this.serverSerializer = createSerializer({ this.serverSerializer = createSerializer({
state: states.PLAY, state: states.PLAY,
isServer: true, isServer: true,
@ -81,28 +76,15 @@ export class Player extends EventEmitter {
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 ( if (!decoder && this.state != Enums.ClientState.POST_HANDSHAKE && msg.length >= 1) {
!decoder && 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.`);
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 { } else {
let parsed: Packet, err: boolean; let parsed: Packet, err: boolean;
try { try {
parsed = new decoder.class(); parsed = new decoder.class();
parsed.deserialize(msg); parsed.deserialize(msg);
} catch (err) { } catch (err) {
if (this.state != Enums.ClientState.POST_HANDSHAKE) 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.`);
this._logger.warn(
`Packet ID 0x${Buffer.from([msg[0]]).toString(
"hex"
)} failed to parse! The packet will be skipped.`
);
err = true; err = true;
} }
if (!err) { if (!err) {
@ -113,10 +95,7 @@ export class Player extends EventEmitter {
} else { } else {
try { try {
const parsed = this.serverDeserializer.parsePacketBuffer(msg)?.data, const parsed = this.serverDeserializer.parsePacketBuffer(msg)?.data,
translated = this.translator.translatePacketClient( translated = this.translator.translatePacketClient(parsed.params, parsed),
parsed.params,
parsed
),
packetData = { packetData = {
name: translated[0], name: translated[0],
params: translated[1], params: translated[1],
@ -132,12 +111,7 @@ export class Player extends EventEmitter {
); );
} }
} catch (err) { } catch (err) {
this._logger.debug( this._logger.debug(`Client ${this.username!} sent an unrecognized packet that could not be parsed!\n${err.stack ?? err}`);
`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()); this.ws.send(packet.serialize());
} }
public async read( public async read(packetId?: Enums.PacketId, filter?: (packet: Packet) => boolean): Promise<Packet> {
packetId?: Enums.PacketId,
filter?: (packet: Packet) => boolean
): Promise<Packet> {
let res; let res;
await Util.awaitPacket(this.ws, (packet) => { await Util.awaitPacket(this.ws, (packet) => {
if ((packetId != null && packetId == packet[0]) || packetId == null) { if ((packetId != null && packetId == packet[0]) || packetId == null) {
const decoder = PACKET_REGISTRY.get(packet[0]); const decoder = PACKET_REGISTRY.get(packet[0]);
if ( if (decoder != null && decoder.packetId == packet[0] && (this.state == Enums.ClientState.PRE_HANDSHAKE || decoder.sentAfterHandshake) && decoder.boundTo == Enums.PacketBounds.S) {
decoder != null &&
decoder.packetId == packet[0] &&
(this.state == Enums.ClientState.PRE_HANDSHAKE ||
decoder.sentAfterHandshake) &&
decoder.boundTo == Enums.PacketBounds.S
) {
let parsed: Packet, let parsed: Packet,
err = false; err = false;
try { try {
@ -188,16 +153,7 @@ export class Player extends EventEmitter {
public disconnect(message: Chat.Chat | string) { public disconnect(message: Chat.Chat | string) {
if (this.state == Enums.ClientState.POST_HANDSHAKE) { if (this.state == Enums.ClientState.POST_HANDSHAKE) {
this.ws.send( this.ws.send(Buffer.concat([[0x40], MineProtocol.writeString(typeof message == "string" ? message : JSON.stringify(message))].map((arr) => (arr instanceof Uint8Array ? arr : Buffer.from(arr)))));
Buffer.concat(
[
[0x40],
MineProtocol.writeString(
typeof message == "string" ? message : JSON.stringify(message)
),
].map((arr) => (arr instanceof Uint8Array ? arr : Buffer.from(arr)))
)
);
this.ws.close(); this.ws.close();
} else { } else {
const packet = new SCDisconnectPacket(); const packet = new SCDisconnectPacket();
@ -208,10 +164,7 @@ export class Player extends EventEmitter {
} }
public async connect(options: ClientOptions) { public async connect(options: ClientOptions) {
if (this._alreadyConnected) 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.`);
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._alreadyConnected = true;
this.serverConnection = createClient( this.serverConnection = createClient(
Object.assign( Object.assign(
@ -269,9 +222,7 @@ export class Player extends EventEmitter {
) )
); );
await this._bindListenersMineClient(this.serverConnection, true, () => await this._bindListenersMineClient(this.serverConnection, true, () => oldConnection.end())
oldConnection.end()
)
.then(() => { .then(() => {
this.emit("switchServer", this.serverConnection, this); this.emit("switchServer", this.serverConnection, this);
res(); res();
@ -283,11 +234,7 @@ export class Player extends EventEmitter {
}); });
} }
private async _bindListenersMineClient( private async _bindListenersMineClient(client: Client, switchingServers?: boolean, onSwitch?: Function) {
client: Client,
switchingServers?: boolean,
onSwitch?: Function
) {
return new Promise((res, rej) => { return new Promise((res, rej) => {
let stream = false, let stream = false,
uuid; uuid;
@ -300,17 +247,13 @@ export class Player extends EventEmitter {
if (!stream) { if (!stream) {
rej(err); rej(err);
} else { } else {
this.disconnect( this.disconnect(`${Enums.ChatColor.RED}Something went wrong: ${err.stack ?? err}`);
`${Enums.ChatColor.RED}Something went wrong: ${err.stack ?? err}`
);
} }
}; };
setTimeout(() => { setTimeout(() => {
if (!stream && this.state != Enums.ClientState.DISCONNECTED) { if (!stream && this.state != Enums.ClientState.DISCONNECTED) {
client.end("Timed out waiting for server connection."); client.end("Timed out waiting for server connection.");
this.disconnect( this.disconnect(Enums.ChatColor.RED + "Timed out waiting for server connection!");
Enums.ChatColor.RED + "Timed out waiting for server connection!"
);
throw new Error("Timed out waiting for server connection!"); throw new Error("Timed out waiting for server connection!");
} }
}, 30000); }, 30000);
@ -349,14 +292,8 @@ export class Player extends EventEmitter {
if (!stream) { if (!stream) {
if (switchingServers) { if (switchingServers) {
if (meta.name == "login" && meta.state == states.PLAY && uuid) { if (meta.name == "login" && meta.state == states.PLAY && uuid) {
this.translator = new BungeeUtil.PacketUUIDTranslator( this.translator = new BungeeUtil.PacketUUIDTranslator(client.uuid, this.uuid);
client.uuid, const pckSeq = BungeeUtil.getRespawnSequence(packet, this.serverSerializer);
this.uuid
);
const pckSeq = BungeeUtil.getRespawnSequence(
packet,
this.serverSerializer
);
this.ws.send( this.ws.send(
this.serverSerializer.createPacketBuffer({ this.serverSerializer.createPacketBuffer({
name: "login", name: "login",
@ -367,19 +304,12 @@ export class Player extends EventEmitter {
stream = true; stream = true;
if (onSwitch) onSwitch(); if (onSwitch) onSwitch();
res(null); res(null);
} else if ( } else if (meta.name == "success" && meta.state == states.LOGIN && !uuid) {
meta.name == "success" &&
meta.state == states.LOGIN &&
!uuid
) {
uuid = packet.uuid; uuid = packet.uuid;
} }
} else { } else {
if (meta.name == "login" && meta.state == states.PLAY && uuid) { if (meta.name == "login" && meta.state == states.PLAY && uuid) {
this.translator = new BungeeUtil.PacketUUIDTranslator( this.translator = new BungeeUtil.PacketUUIDTranslator(client.uuid, this.uuid);
client.uuid,
this.uuid
);
this.ws.send( this.ws.send(
this.serverSerializer.createPacketBuffer({ this.serverSerializer.createPacketBuffer({
name: "login", name: "login",
@ -389,19 +319,12 @@ export class Player extends EventEmitter {
stream = true; stream = true;
if (onSwitch) onSwitch(); if (onSwitch) onSwitch();
res(null); res(null);
} else if ( } else if (meta.name == "success" && meta.state == states.LOGIN && !uuid) {
meta.name == "success" &&
meta.state == states.LOGIN &&
!uuid
) {
uuid = packet.uuid; uuid = packet.uuid;
} }
} }
} else { } else {
const translated = this.translator!.translatePacketServer( const translated = this.translator!.translatePacketServer(packet, meta),
packet,
meta
),
eventData = { eventData = {
name: translated[0], name: translated[0],
params: translated[1], params: translated[1],
@ -444,8 +367,5 @@ export declare interface Player {
on<U extends keyof PlayerEvents>(event: U, listener: PlayerEvents[U]): this; on<U extends keyof PlayerEvents>(event: U, listener: PlayerEvents[U]): this;
once<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>( emit<U extends keyof PlayerEvents>(event: U, ...args: Parameters<PlayerEvents[U]>): boolean;
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 { Motd } from "./Motd.js";
import { Player } from "./Player.js"; import { Player } from "./Player.js";
import { Enums } from "./Enums.js"; import { Enums } from "./Enums.js";
import { import { NETWORK_VERSION, PROXY_BRANDING, PROXY_VERSION, VANILLA_PROTOCOL_VERSION } from "../meta.js";
NETWORK_VERSION,
PROXY_BRANDING,
PROXY_VERSION,
VANILLA_PROTOCOL_VERSION,
} from "../meta.js";
import { CSUsernamePacket } from "./packets/CSUsernamePacket.js"; import { CSUsernamePacket } from "./packets/CSUsernamePacket.js";
import { SCSyncUuidPacket } from "./packets/SCSyncUuidPacket.js"; import { SCSyncUuidPacket } from "./packets/SCSyncUuidPacket.js";
import { SCReadyPacket } from "./packets/SCReadyPacket.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 // hijack the initial handler logger to append [InitialHandler] to the beginning
(this.initalHandlerLogger as any)._info = this.initalHandlerLogger.info; (this.initalHandlerLogger as any)._info = this.initalHandlerLogger.info;
this.initalHandlerLogger.info = (msg: string) => { this.initalHandlerLogger.info = (msg: string) => {
(this.initalHandlerLogger as any)._info( (this.initalHandlerLogger as any)._info(`${chalk.blue("[InitialHandler]")} ${msg}`);
`${chalk.blue("[InitialHandler]")} ${msg}`
);
}; };
(this.initalHandlerLogger as any)._warn = this.initalHandlerLogger.warn; (this.initalHandlerLogger as any)._warn = this.initalHandlerLogger.warn;
this.initalHandlerLogger.warn = (msg: string) => { this.initalHandlerLogger.warn = (msg: string) => {
(this.initalHandlerLogger as any)._warn( (this.initalHandlerLogger as any)._warn(`${chalk.blue("[InitialHandler]")} ${msg}`);
`${chalk.blue("[InitialHandler]")} ${msg}`
);
}; };
(this.initalHandlerLogger as any)._error = this.initalHandlerLogger.error; (this.initalHandlerLogger as any)._error = this.initalHandlerLogger.error;
this.initalHandlerLogger.error = (msg: string) => { this.initalHandlerLogger.error = (msg: string) => {
(this.initalHandlerLogger as any)._error( (this.initalHandlerLogger as any)._error(`${chalk.blue("[InitialHandler]")} ${msg}`);
`${chalk.blue("[InitialHandler]")} ${msg}`
);
}; };
(this.initalHandlerLogger as any)._fatal = this.initalHandlerLogger.fatal; (this.initalHandlerLogger as any)._fatal = this.initalHandlerLogger.fatal;
this.initalHandlerLogger.fatal = (msg: string) => { this.initalHandlerLogger.fatal = (msg: string) => {
(this.initalHandlerLogger as any)._fatal( (this.initalHandlerLogger as any)._fatal(`${chalk.blue("[InitialHandler]")} ${msg}`);
`${chalk.blue("[InitialHandler]")} ${msg}`
);
}; };
(this.initalHandlerLogger as any)._debug = this.initalHandlerLogger.debug; (this.initalHandlerLogger as any)._debug = this.initalHandlerLogger.debug;
this.initalHandlerLogger.debug = (msg: string) => { this.initalHandlerLogger.debug = (msg: string) => {
(this.initalHandlerLogger as any)._debug( (this.initalHandlerLogger as any)._debug(`${chalk.blue("[InitialHandler]")} ${msg}`);
`${chalk.blue("[InitialHandler]")} ${msg}`
);
}; };
this.config = config; this.config = config;
this.pluginManager = pluginManager; this.pluginManager = pluginManager;
instanceCount++; instanceCount++;
process.on("uncaughtException", (err) => { process.on("uncaughtException", (err) => {
this._logger.warn( this._logger.warn(`An uncaught exception was caught! Error: ${err.stack}`);
`An uncaught exception was caught! Error: ${err.stack}`
);
}); });
process.on("unhandledRejection", (err) => { process.on("unhandledRejection", (err) => {
@ -107,16 +90,10 @@ export class Proxy extends EventEmitter {
public async init() { public async init() {
this._logger.info(`Starting ${PROXY_BRANDING} v${PROXY_VERSION}...`); this._logger.info(`Starting ${PROXY_BRANDING} v${PROXY_VERSION}...`);
global.PROXY = this; global.PROXY = this;
if (this.loaded) if (this.loaded) throw new Error("Can't initiate if proxy instance is already initialized or is being initialized!");
throw new Error(
"Can't initiate if proxy instance is already initialized or is being initialized!"
);
this.loaded = true; this.loaded = true;
this.packetRegistry = await loadPackets(); this.packetRegistry = await loadPackets();
this.skinServer = new EaglerSkins.SkinServer( this.skinServer = new EaglerSkins.SkinServer(this, this.config.skinUrlWhitelist);
this,
this.config.skinUrlWhitelist
);
global.PACKET_REGISTRY = this.packetRegistry; global.PACKET_REGISTRY = this.packetRegistry;
if (this.config.motd == "FORWARD") { if (this.config.motd == "FORWARD") {
this._pollServer(this.config.server.host, this.config.server.port); 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) (req, res) => this._handleNonWSRequest(req, res, this.config)
) )
.listen( .listen(this.config.bindPort || 8080, this.config.bindHost || "127.0.0.1");
this.config.bindPort || 8080,
this.config.bindHost || "127.0.0.1"
);
this.wsServer = new WebSocketServer({ this.wsServer = new WebSocketServer({
noServer: true, noServer: true,
}); });
} else { } else {
this.httpServer = http this.httpServer = http.createServer((req, res) => this._handleNonWSRequest(req, res, this.config)).listen(this.config.bindPort || 8080, this.config.bindHost || "127.0.0.1");
.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({ this.wsServer = new WebSocketServer({
noServer: true, noServer: true,
}); });
@ -166,73 +133,49 @@ export class Proxy extends EventEmitter {
try { try {
await this._handleWSConnectionReq(r, s, h); await this._handleWSConnectionReq(r, s, h);
} catch (err) { } catch (err) {
this._logger.error( this._logger.error(`Error was caught whilst trying to handle WebSocket upgrade! Error: ${err.stack ?? err}`);
`Error was caught whilst trying to handle WebSocket upgrade! Error: ${
err.stack ?? err
}`
);
} }
}); });
this.pluginManager.emit("proxyFinishLoading", this, this.pluginManager); this.pluginManager.emit("proxyFinishLoading", this, this.pluginManager);
this._logger.info( this._logger.info(`Started WebSocket server and binded to ${this.config.bindHost} on port ${this.config.bindPort}.`);
`Started WebSocket server and binded to ${this.config.bindHost} on port ${this.config.bindPort}.`
);
} }
private _handleNonWSRequest( private _handleNonWSRequest(req: http.IncomingMessage, res: http.ServerResponse, config: Config["adapter"]) {
req: http.IncomingMessage, const ctx: Util.Handlable = { handled: false };
res: http.ServerResponse, this.emit("httpConnection", req, res, ctx);
config: Config["adapter"] if (!ctx.handled) res.setHeader("Content-Type", "text/html").writeHead(426).end(UPGRADE_REQUIRED_RESPONSE);
) {
res
.setHeader("Content-Type", "text/html")
.writeHead(426)
.end(UPGRADE_REQUIRED_RESPONSE);
} }
readonly LOGIN_TIMEOUT = 30000; readonly LOGIN_TIMEOUT = 30000;
private async _handleWSConnection(ws: WebSocket) { private async _handleWSConnection(ws: WebSocket, req: http.IncomingMessage) {
const firstPacket = await Util.awaitPacket(ws); const firstPacket = await Util.awaitPacket(ws);
let player: Player, handled: boolean; let player: Player, handled: boolean;
setTimeout(() => { setTimeout(() => {
if (!handled) { if (!handled) {
this.initalHandlerLogger.warn( this.initalHandlerLogger.warn(
`Disconnecting client ${ `Disconnecting client ${
player player ? player.username ?? `[/${(ws as any)._socket.remoteAddress}:${(ws as any)._socket.remotePort}` : `[/${(ws as any)._socket.remoteAddress}:${(ws as any)._socket.remotePort}`
? 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.` } due to connection timing out.`
); );
if (player) if (player) player.disconnect(`${Enums.ChatColor.YELLOW} Your connection timed out whilst processing handshake, please try again.`);
player.disconnect(
`${Enums.ChatColor.YELLOW} Your connection timed out whilst processing handshake, please try again.`
);
else ws.close(); else ws.close();
} }
}, this.LOGIN_TIMEOUT); }, this.LOGIN_TIMEOUT);
try { try {
const ctx: Util.Handlable = { handled: false };
await this.emit("wsConnection", ws, req, ctx);
if (ctx.handled) return;
if (firstPacket.toString() === "Accept: MOTD") { if (firstPacket.toString() === "Accept: MOTD") {
if (this.broadcastMotd) { if (this.broadcastMotd) {
if ((this.broadcastMotd as any)._static) { if ((this.broadcastMotd as any)._static) {
this.broadcastMotd.jsonMotd.data.online = this.players.size; this.broadcastMotd.jsonMotd.data.online = this.players.size;
// sample for players // sample for players
this.broadcastMotd.jsonMotd.data.players = []; this.broadcastMotd.jsonMotd.data.players = [];
const playerSample = [...this.players.keys()] const playerSample = [...this.players.keys()].filter((sample) => !sample.startsWith("!phs_")).slice(0, 5);
.filter((sample) => !sample.startsWith("!phs_"))
.slice(0, 5);
this.broadcastMotd.jsonMotd.data.players = playerSample; this.broadcastMotd.jsonMotd.data.players = playerSample;
if (this.players.size - playerSample.length > 0) 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)`);
this.broadcastMotd.jsonMotd.data.players.push(
`${Enums.ChatColor.GRAY}${Enums.ChatColor.ITALIC}(and ${
this.players.size - playerSample.length
} more)`
);
const bufferized = this.broadcastMotd.toBuffer(); const bufferized = this.broadcastMotd.toBuffer();
ws.send(bufferized[0]); ws.send(bufferized[0]);
@ -246,24 +189,15 @@ export class Proxy extends EventEmitter {
handled = true; handled = true;
ws.close(); ws.close();
} else { } else {
player = new Player(ws); (ws as any).httpRequest = req;
player = new Player(ws as any);
const loginPacket = new CSLoginPacket().deserialize(firstPacket); const loginPacket = new CSLoginPacket().deserialize(firstPacket);
player.state = Enums.ClientState.PRE_HANDSHAKE; player.state = Enums.ClientState.PRE_HANDSHAKE;
if (loginPacket.gameVersion != VANILLA_PROTOCOL_VERSION) { if (loginPacket.gameVersion != VANILLA_PROTOCOL_VERSION) {
player.disconnect( player.disconnect(`${Enums.ChatColor.RED}Please connect to this proxy on EaglercraftX 1.8.9.`);
`${Enums.ChatColor.RED}Please connect to this proxy on EaglercraftX 1.8.9.`
);
return; return;
} else if (loginPacket.networkVersion != NETWORK_VERSION) { } else if (loginPacket.networkVersion != NETWORK_VERSION) {
player.disconnect( player.disconnect(`${Enums.ChatColor.RED}Your EaglercraftX version is too ${loginPacket.networkVersion > NETWORK_VERSION ? "new" : "old"}! Please ${loginPacket.networkVersion > NETWORK_VERSION ? "downgrade" : "update"}.`);
`${Enums.ChatColor.RED}Your EaglercraftX version is too ${
loginPacket.networkVersion > NETWORK_VERSION ? "new" : "old"
}! Please ${
loginPacket.networkVersion > NETWORK_VERSION
? "downgrade"
: "update"
}.`
);
return; return;
} }
try { try {
@ -275,35 +209,22 @@ export class Proxy extends EventEmitter {
player.username = loginPacket.username; player.username = loginPacket.username;
player.uuid = Util.generateUUIDFromPlayer(player.username); player.uuid = Util.generateUUIDFromPlayer(player.username);
if (this.players.size > this.config.maxConcurrentClients) { if (this.players.size > this.config.maxConcurrentClients) {
player.disconnect( player.disconnect(`${Enums.ChatColor.YELLOW}Proxy is full! Please try again later.`);
`${Enums.ChatColor.YELLOW}Proxy is full! Please try again later.`
);
return; return;
} else if ( } else if (this.players.get(player.username) != null || this.players.get(`!phs.${player.uuid}`) != null) {
this.players.get(player.username) != null || player.disconnect(`${Enums.ChatColor.YELLOW}Someone under your username (${player.username}) is already connected to the proxy!`);
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; return;
} }
this.players.set(`!phs.${player.uuid}`, player); this.players.set(`!phs.${player.uuid}`, player);
this._logger.info( this._logger.info(
`Player ${loginPacket.username} (${Util.generateUUIDFromPlayer( `Player ${loginPacket.username} (${Util.generateUUIDFromPlayer(loginPacket.username)}) running ${loginPacket.brand}/${loginPacket.version} (net ver: ${loginPacket.networkVersion}, game ver: ${
loginPacket.username loginPacket.gameVersion
)}) running ${loginPacket.brand}/${loginPacket.version} (net ver: ${ }) is attempting to connect!`
loginPacket.networkVersion
}, game ver: ${loginPacket.gameVersion}) is attempting to connect!`
); );
player.write(new SCIdentifyPacket()); player.write(new SCIdentifyPacket());
const usernamePacket: CSUsernamePacket = (await player.read( const usernamePacket: CSUsernamePacket = (await player.read(Enums.PacketId.CSUsernamePacket)) as any;
Enums.PacketId.CSUsernamePacket
)) as any;
if (usernamePacket.username !== player.username) { if (usernamePacket.username !== player.username) {
player.disconnect( player.disconnect(`${Enums.ChatColor.YELLOW}Failed to complete handshake. Your game version may be too old or too new.`);
`${Enums.ChatColor.YELLOW}Failed to complete handshake. Your game version may be too old or too new.`
);
return; return;
} }
const syncUuid = new SCSyncUuidPacket(); const syncUuid = new SCSyncUuidPacket();
@ -311,12 +232,7 @@ export class Proxy extends EventEmitter {
syncUuid.uuid = player.uuid; syncUuid.uuid = player.uuid;
player.write(syncUuid); player.write(syncUuid);
const prom = await Promise.all([ const prom = await Promise.all([player.read(Enums.PacketId.CSReadyPacket), (await player.read(Enums.PacketId.CSSetSkinPacket)) as CSSetSkinPacket]),
player.read(Enums.PacketId.CSReadyPacket),
(await player.read(
Enums.PacketId.CSSetSkinPacket
)) as CSSetSkinPacket,
]),
skin = prom[1], skin = prom[1],
obj = new EaglerSkins.EaglerSkin(); obj = new EaglerSkins.EaglerSkin();
obj.owner = player; obj.owner = player;
@ -331,45 +247,31 @@ export class Proxy extends EventEmitter {
player.initListeners(); player.initListeners();
this._bindListenersToPlayer(player); this._bindListenersToPlayer(player);
player.state = Enums.ClientState.POST_HANDSHAKE; player.state = Enums.ClientState.POST_HANDSHAKE;
this._logger.info( this._logger.info(`Handshake Success! Connecting player ${player.username} to server...`);
`Handshake Success! Connecting player ${player.username} to server...`
);
handled = true; handled = true;
await player.connect({ await player.connect({
host: this.config.server.host, host: this.config.server.host,
port: this.config.server.port, port: this.config.server.port,
username: player.username, username: player.username,
}); });
this._logger.info( this._logger.info(`Player ${player.username} successfully connected to server.`);
`Player ${player.username} successfully connected to server.`
);
this.emit("playerConnect", player); this.emit("playerConnect", player);
} }
} catch (err) { } catch (err) {
this.initalHandlerLogger.warn( this.initalHandlerLogger.warn(`Error occurred whilst handling handshake: ${err.stack ?? err}`);
`Error occurred whilst handling handshake: ${err.stack ?? err}`
);
handled = true; handled = true;
ws.close(); ws.close();
if (player && player.uuid && this.players.has(`!phs.${player.uuid}`)) if (player && player.uuid && this.players.has(`!phs.${player.uuid}`)) this.players.delete(`!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(player.username))
this.players.delete(player.username);
} }
} }
private _bindListenersToPlayer(player: Player) { private _bindListenersToPlayer(player: Player) {
let sentDisconnectMsg = false; let sentDisconnectMsg = false;
player.on("disconnect", () => { player.on("disconnect", () => {
if (this.players.has(player.username)) if (this.players.has(player.username)) this.players.delete(player.username);
this.players.delete(player.username); this.initalHandlerLogger.info(`DISCONNECT ${player.username} <=> DISCONNECTED`);
this.initalHandlerLogger.info( if (!sentDisconnectMsg) this._logger.info(`Player ${player.username} (${player.uuid}) disconnected from the proxy server.`);
`DISCONNECT ${player.username} <=> DISCONNECTED`
);
if (!sentDisconnectMsg)
this._logger.info(
`Player ${player.username} (${player.uuid}) disconnected from the proxy server.`
);
}); });
player.on("proxyPacket", async (packet) => { player.on("proxyPacket", async (packet) => {
if (packet.packetId == Enums.PacketId.CSChannelMessagePacket) { if (packet.packetId == Enums.PacketId.CSChannelMessagePacket) {
@ -379,23 +281,15 @@ export class Proxy extends EventEmitter {
await this.skinServer.handleRequest(msg, player); await this.skinServer.handleRequest(msg, player);
} }
} catch (err) { } catch (err) {
this._logger.error( this._logger.error(`Failed to process channel message packet! Error: ${err.stack || err}`);
`Failed to process channel message packet! Error: ${
err.stack || err
}`
);
} }
} }
}); });
player.on("switchServer", (client) => { player.on("switchServer", (client) => {
this.initalHandlerLogger.info( this.initalHandlerLogger.info(`SWITCH_SERVER ${player.username} <=> ${client.socket.remoteAddress}:${client.socket.remotePort}`);
`SWITCH_SERVER ${player.username} <=> ${client.socket.remoteAddress}:${client.socket.remotePort}`
);
}); });
player.on("joinServer", (client) => { player.on("joinServer", (client) => {
this.initalHandlerLogger.info( this.initalHandlerLogger.info(`SERVER_CONNECTED ${player.username} <=> ${client.socket.remoteAddress}:${client.socket.remotePort}`);
`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) { private _pollServer(host: string, port: number, interval?: number) {
(async () => { (async () => {
while (true) { while (true) {
const motd = await Motd.MOTD.generateMOTDFromPing(host, port).catch( const motd = await Motd.MOTD.generateMOTDFromPing(host, port).catch((err) => {
(err) => { this._logger.warn(`Error polling ${host}:${port} for MOTD: ${err.stack ?? err}`);
this._logger.warn( });
`Error polling ${host}:${port} for MOTD: ${err.stack ?? err}`
);
}
);
if (motd) this.broadcastMotd = motd; if (motd) this.broadcastMotd = motd;
await new Promise((res) => await new Promise((res) => setTimeout(res, interval ?? Proxy.POLL_INTERVAL));
setTimeout(res, interval ?? Proxy.POLL_INTERVAL)
);
} }
})(); })();
} }
private async _handleWSConnectionReq( private async _handleWSConnectionReq(req: http.IncomingMessage, socket: Duplex, head: Buffer) {
req: http.IncomingMessage, const origin = req.headers.origin == null || req.headers.origin == "null" ? null : req.headers.origin;
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) { if (!this.config.origins.allowOfflineDownloads && origin == null) {
socket.destroy(); socket.destroy();
return; return;
} }
if ( if (this.config.origins.originBlacklist != null && this.config.origins.originBlacklist.some((host) => Util.areDomainsEqual(host, origin))) {
this.config.origins.originBlacklist != null &&
this.config.origins.originBlacklist.some((host) =>
Util.areDomainsEqual(host, origin)
)
) {
socket.destroy(); socket.destroy();
return; return;
} }
if ( if (this.config.origins.originWhitelist != null && !this.config.origins.originWhitelist.some((host) => Util.areDomainsEqual(host, origin))) {
this.config.origins.originWhitelist != null &&
!this.config.origins.originWhitelist.some((host) =>
Util.areDomainsEqual(host, origin)
)
) {
socket.destroy(); socket.destroy();
return; return;
} }
try { try {
await this.wsServer.handleUpgrade(req, socket, head, (ws) => await this.wsServer.handleUpgrade(req, socket, head, (ws) => this._handleWSConnection(ws, req));
this._handleWSConnection(ws)
);
} catch (err) { } catch (err) {
this._logger.error( this._logger.error(`Error was caught whilst trying to handle WebSocket connection request! Error: ${err.stack ?? err}`);
`Error was caught whilst trying to handle WebSocket connection request! Error: ${
err.stack ?? err
}`
);
socket.destroy(); socket.destroy();
} }
} }
@ -475,13 +340,12 @@ export class Proxy extends EventEmitter {
interface ProxyEvents { interface ProxyEvents {
playerConnect: (player: Player) => void; playerConnect: (player: Player) => void;
playerDisconnect: (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 { export declare interface Proxy {
on<U extends keyof ProxyEvents>(event: U, listener: ProxyEvents[U]): this; 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 { export function uuidStringToBuffer(uuid: string): Buffer {
if (!uuid) return Buffer.alloc(16); // Return empty buffer if (!uuid) return Buffer.alloc(16); // Return empty buffer
const hexStr = uuid.replace(/-/g, ""); const hexStr = uuid.replace(/-/g, "");
if (uuid.length != 36 || hexStr.length != 32) if (uuid.length != 36 || hexStr.length != 32) throw new Error(`Invalid UUID string: ${uuid}`);
throw new Error(`Invalid UUID string: ${uuid}`);
return Buffer.from(hexStr, "hex"); return Buffer.from(hexStr, "hex");
} }
export function uuidBufferToString(buffer: Buffer): string { export function uuidBufferToString(buffer: Buffer): string {
if (buffer.length != 16) if (buffer.length != 16) throw new Error(`Invalid buffer length for uuid: ${buffer.length}`);
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 if (buffer.equals(Buffer.alloc(16))) return null; // If buffer is all zeros, return null
const str = buffer.toString("hex"); const str = buffer.toString("hex");
return `${str.slice(0, 8)}-${str.slice(8, 12)}-${str.slice( return `${str.slice(0, 8)}-${str.slice(8, 12)}-${str.slice(12, 16)}-${str.slice(16, 20)}-${str.slice(20)}`;
12,
16
)}-${str.slice(16, 20)}-${str.slice(20)}`;
} }
export function awaitPacket( export function awaitPacket(ws: WebSocket, filter?: (msg: Buffer) => boolean): Promise<Buffer> {
ws: WebSocket,
filter?: (msg: Buffer) => boolean
): Promise<Buffer> {
return new Promise<Buffer>((res, rej) => { return new Promise<Buffer>((res, rej) => {
let resolved = false; let resolved = false;
const msgCb = (msg: any) => { const msgCb = (msg: any) => {
@ -64,17 +56,13 @@ export namespace Util {
resolved = true; resolved = true;
ws.removeListener("message", msgCb); ws.removeListener("message", msgCb);
ws.removeListener("close", discon); ws.removeListener("close", discon);
ws.setMaxListeners( ws.setMaxListeners(ws.getMaxListeners() - 2 < 0 ? 5 : ws.getMaxListeners() - 2);
ws.getMaxListeners() - 2 < 0 ? 5 : ws.getMaxListeners() - 2
);
res(msg); res(msg);
} else if (filter == null) { } else if (filter == null) {
resolved = true; resolved = true;
ws.removeListener("message", msgCb); ws.removeListener("message", msgCb);
ws.removeListener("close", discon); ws.removeListener("close", discon);
ws.setMaxListeners( ws.setMaxListeners(ws.getMaxListeners() - 2 < 0 ? 5 : ws.getMaxListeners() - 2);
ws.getMaxListeners() - 2 < 0 ? 5 : ws.getMaxListeners() - 2
);
res(msg); res(msg);
} }
}; };
@ -82,9 +70,7 @@ export namespace Util {
resolved = true; resolved = true;
ws.removeListener("message", msgCb); ws.removeListener("message", msgCb);
ws.removeListener("close", discon); ws.removeListener("close", discon);
ws.setMaxListeners( ws.setMaxListeners(ws.getMaxListeners() - 2 < 0 ? 5 : ws.getMaxListeners() - 2);
ws.getMaxListeners() - 2 < 0 ? 5 : ws.getMaxListeners() - 2
);
rej("Connection closed"); rej("Connection closed");
}; };
ws.setMaxListeners(ws.getMaxListeners() + 2); ws.setMaxListeners(ws.getMaxListeners() + 2);
@ -93,9 +79,7 @@ export namespace Util {
setTimeout(() => { setTimeout(() => {
ws.removeListener("message", msgCb); ws.removeListener("message", msgCb);
ws.removeListener("close", discon); ws.removeListener("close", discon);
ws.setMaxListeners( ws.setMaxListeners(ws.getMaxListeners() - 2 < 0 ? 5 : ws.getMaxListeners() - 2);
ws.getMaxListeners() - 2 < 0 ? 5 : ws.getMaxListeners() - 2
);
rej("Timed out"); rej("Timed out");
}, 10000); }, 10000);
}); });
@ -104,57 +88,28 @@ export namespace Util {
export function validateUsername(user: string): void | never { export function validateUsername(user: string): void | never {
if (user.length > 20) throw new Error("Username is too long!"); if (user.length > 20) throw new Error("Username is too long!");
if (user.length < 3) throw new Error("Username is too short!"); if (user.length < 3) throw new Error("Username is too short!");
if (!!user.match(USERNAME_REGEX)) if (!!user.match(USERNAME_REGEX)) throw new Error("Invalid username. Username can only contain alphanumeric characters, and the underscore (_) character.");
throw new Error(
"Invalid username. Username can only contain alphanumeric characters, and the underscore (_) character."
);
} }
export function areDomainsEqual(d1: string, d2: string): boolean { export function areDomainsEqual(d1: string, d2: string): boolean {
if (d1.endsWith("*.")) if (d1.endsWith("*.")) d1 = d1.replace("*.", "WILDCARD-LOL-EXTRA-LONG-SUBDOMAIN-TO-LOWER-CHANCES-OF-COLLISION.");
d1 = d1.replace(
"*.",
"WILDCARD-LOL-EXTRA-LONG-SUBDOMAIN-TO-LOWER-CHANCES-OF-COLLISION."
);
const parseResult1 = parseDomain(d1), const parseResult1 = parseDomain(d1),
parseResult2 = parseDomain(d2); parseResult2 = parseDomain(d2);
if ( if (parseResult1.type != ParseResultType.Invalid && parseResult2.type != ParseResultType.Invalid) {
parseResult1.type != ParseResultType.Invalid && if (parseResult1.type == ParseResultType.Ip && parseResult2.type == ParseResultType.Ip) {
parseResult2.type != ParseResultType.Invalid
) {
if (
parseResult1.type == ParseResultType.Ip &&
parseResult2.type == ParseResultType.Ip
) {
return parseResult1.hostname == parseResult2.hostname ? true : false; return parseResult1.hostname == parseResult2.hostname ? true : false;
} else if ( } else if (parseResult1.type == ParseResultType.Listed && parseResult2.type == ParseResultType.Listed) {
parseResult1.type == ParseResultType.Listed && if (parseResult1.subDomains[0] == "WILDCARD-LOL-EXTRA-LONG-SUBDOMAIN-TO-LOWER-CHANCES-OF-COLLISION") {
parseResult2.type == ParseResultType.Listed
) {
if (
parseResult1.subDomains[0] ==
"WILDCARD-LOL-EXTRA-LONG-SUBDOMAIN-TO-LOWER-CHANCES-OF-COLLISION"
) {
// wildcard // wildcard
const domainPlusTld1 = const domainPlusTld1 = parseResult1.domain + ("." + parseResult1.topLevelDomains.join("."));
parseResult1.domain + const domainPlusTld2 = parseResult2.domain + ("." + parseResult2.topLevelDomains.join("."));
("." + parseResult1.topLevelDomains.join("."));
const domainPlusTld2 =
parseResult2.domain +
("." + parseResult2.topLevelDomains.join("."));
return domainPlusTld1 == domainPlusTld2 ? true : false; return domainPlusTld1 == domainPlusTld2 ? true : false;
} else { } else {
// no wildcard // no wildcard
return d1 == d2 ? true : false; return d1 == d2 ? true : false;
} }
} else if ( } else if (parseResult1.type == ParseResultType.NotListed && parseResult2.type == ParseResultType.NotListed) {
parseResult1.type == ParseResultType.NotListed && if (parseResult1.labels[0] == "WILDCARD-LOL-EXTRA-LONG-SUBDOMAIN-TO-LOWER-CHANCES-OF-COLLISION") {
parseResult2.type == ParseResultType.NotListed
) {
if (
parseResult1.labels[0] ==
"WILDCARD-LOL-EXTRA-LONG-SUBDOMAIN-TO-LOWER-CHANCES-OF-COLLISION"
) {
// wildcard // wildcard
const domainPlusTld1 = parseResult1.labels.slice(2).join("."); const domainPlusTld1 = parseResult1.labels.slice(2).join(".");
const domainPlusTld2 = parseResult1.labels.slice(2).join("."); const domainPlusTld2 = parseResult1.labels.slice(2).join(".");
@ -163,15 +118,8 @@ export namespace Util {
// no wildcard // no wildcard
return d1 == d2 ? true : false; return d1 == d2 ? true : false;
} }
} else if ( } else if (parseResult1.type == ParseResultType.Reserved && parseResult2.type == ParseResultType.Reserved) {
parseResult1.type == ParseResultType.Reserved && if (parseResult1.hostname == "" && parseResult1.hostname === parseResult2.hostname) return true;
parseResult2.type == ParseResultType.Reserved
) {
if (
parseResult1.hostname == "" &&
parseResult1.hostname === parseResult2.hostname
)
return true;
else { else {
// uncertain, fallback to exact hostname matching // uncertain, fallback to exact hostname matching
return d1 == d2 ? true : false; return d1 == d2 ? true : false;
@ -229,10 +177,7 @@ export namespace Util {
flags: number; flags: number;
}; };
export function generatePositionPacket( export function generatePositionPacket(currentPos: PlayerPosition, newPos: PositionPacket): PositionPacket {
currentPos: PlayerPosition,
newPos: PositionPacket
): PositionPacket {
const DEFAULT_RELATIVITY = 0x01; // relative to X-axis const DEFAULT_RELATIVITY = 0x01; // relative to X-axis
const newPosPacket = { const newPosPacket = {
x: newPos.x - currentPos.x * 2, x: newPos.x - currentPos.x * 2,
@ -244,4 +189,8 @@ export namespace Util {
}; };
return newPosPacket; return newPosPacket;
} }
export type Handlable = {
handled: boolean;
};
} }

View File

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