Compare commits

...

2 Commits

Author SHA1 Message Date
q13x
375a275442 Online mode fix & npm package updates 2024-03-06 16:41:57 -08:00
q13x
67c3e0bbda Add EagProxyAAS password protection + direct connect 2024-03-06 15:25:02 -08:00
11 changed files with 1405 additions and 2965 deletions

2666
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -9,22 +9,23 @@
"author": "WorldEditAxe, q13x",
"license": "MIT",
"dependencies": {
"@thi.ng/leb128": "^3.0.5",
"@types/node": "^18.11.18",
"@types/semver": "^7.3.13",
"@types/sharp": "^0.31.1",
"@types/ws": "^8.5.4",
"chalk": "^5.2.0",
"dotenv": "^16.0.3",
"minecraft-protocol": "^1.40.2",
"node-fetch": "^3.3.1",
"parse-domain": "^7.0.1",
"prismarine-block": "^1.16.3",
"prismarine-chunk": "^1.33.0",
"prismarine-registry": "^1.6.0",
"semver": "^7.3.8",
"sharp": "^0.31.3",
"ws": "^8.12.0"
"@thi.ng/leb128": "3.0.5",
"@types/node": "18.11.18",
"@types/semver": "7.3.13",
"@types/sharp": "0.31.1",
"@types/ws": "8.5.4",
"chalk": "5.2.0",
"dotenv": "16.0.3",
"minecraft-protocol": "^1.26.5",
"node-fetch": "3.3.1",
"parse-domain": "7.0.1",
"prismarine-auth": "^2.4.1",
"prismarine-block": "1.16.3",
"prismarine-chunk": "1.33.0",
"prismarine-registry": "1.6.0",
"semver": "^7.6.0",
"sharp": "^0.33.2",
"ws": "8.12.0"
},
"type": "module"
}

View File

@ -12,41 +12,23 @@ import XboxTokenManager from "prismarine-auth/src/TokenManagers/XboxTokenManager
import MsaTokenManager from "prismarine-auth/src/TokenManagers/MsaTokenManager.js";
import BedrockTokenManager from "prismarine-auth/src/TokenManagers/MinecraftBedrockTokenManager.js";
/*
MIT License
Copyright (c) 2020 PrismarineJS
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
async function retry(methodFn, beforeRetry, times) {
for (let attempts = 0; attempts < times; attempts++) {
try {
return await methodFn();
} catch (err) {
if (err instanceof URIError) {
throw err;
while (times--) {
if (times !== 0) {
try {
return await methodFn();
} catch (e) {
if (e instanceof URIError) {
throw e;
} else {
// debug(e);
}
}
await new Promise((resolve) => setTimeout(resolve, 2000));
await beforeRetry();
} else {
return await methodFn();
}
await new Promise((resolve) => setTimeout(resolve, 2000));
await beforeRetry();
}
}
@ -63,9 +45,7 @@ export class CustomAuthflow {
constructor(username = "", cache, options, codeCallback) {
this.username = username;
if (options && !options.flow) {
throw new Error(
"Missing 'flow' argument in options. See docs for more information."
);
throw new Error("Missing 'flow' argument in options. See docs for more information.");
}
this.options = options || { flow: "msal" };
this.initTokenManagers(username, cache);
@ -74,48 +54,32 @@ export class CustomAuthflow {
initTokenManagers(username, cache) {
if (this.options.flow === "live" || this.options.flow === "sisu") {
if (!this.options.authTitle)
throw new Error(
`Please specify an "authTitle" in Authflow constructor when using ${this.options.flow} flow`
);
this.msa = new LiveTokenManager(
this.options.authTitle,
["service::user.auth.xboxlive.com::MBI_SSL"],
cache({ cacheName: this.options.flow, username })
);
if (!this.options.authTitle) throw new Error(`Please specify an "authTitle" in Authflow constructor when using ${this.options.flow} flow`);
this.msa = new LiveTokenManager(this.options.authTitle, ["service::user.auth.xboxlive.com::MBI_SSL"], cache({ cacheName: this.options.flow, username }));
this.doTitleAuth = true;
} else if (this.options.flow === "msal") {
const config = Object.assign(
{ ...msalConfig },
this.options.authTitle
? { auth: { ...msalConfig.auth, clientId: this.options.authTitle } }
: {}
);
this.msa = new MsaTokenManager(
config,
["XboxLive.signin", "offline_access"],
cache({ cacheName: "msal", username })
);
const config = Object.assign({ ...msalConfig }, this.options.authTitle ? { auth: { ...msalConfig.auth, clientId: this.options.authTitle } } : {});
this.msa = new MsaTokenManager(config, ["XboxLive.signin", "offline_access"], cache({ cacheName: "msal", username }));
} else {
throw new Error(
`Unknown flow: ${this.options.flow} (expected "live", "sisu", or "msal")`
);
throw new Error(`Unknown flow: ${this.options.flow} (expected "live", "sisu", or "msal")`);
}
const keyPair = crypto.generateKeyPairSync("ec", { namedCurve: "P-256" });
this.xbl = new XboxTokenManager(
keyPair,
cache({ cacheName: "xbl", username })
);
this.xbl = new XboxTokenManager(keyPair, cache({ cacheName: "xbl", username }));
this.mba = new BedrockTokenManager(cache({ cacheName: "bed", username }));
this.mca = new JavaTokenManager(cache({ cacheName: "mca", username }));
}
static resetTokenCaches(cache) {
if (!cache) throw new Error("You must provide a cache directory to reset.");
if (fs.existsSync(cache)) {
fs.rmSync(cache, { recursive: true });
return true;
try {
if (fs.existsSync(cache)) {
fs.rmSync(cache, { recursive: true });
return true;
}
} catch (e) {
console.log("Failed to clear cache dir", e);
return false;
}
}
@ -130,70 +94,58 @@ export class CustomAuthflow {
console.info(response.message);
});
if (ret.account) {
console.info(`[msa] Signed in as ${ret.account.username}`);
} else {
// We don't get extra account data here per scope
console.info("[msa] Signed in with Microsoft");
}
return ret.accessToken;
}
}
async getXboxToken(
relyingParty = this.options.relyingParty || Endpoints.XboxRelyingParty
) {
async getXboxToken(relyingParty = this.options.relyingParty || Endpoints.XboxRelyingParty, forceRefresh = false) {
const options = { ...this.options, relyingParty };
if (await this.xbl.verifyTokens(relyingParty)) {
const { data } = await this.xbl.getCachedXstsToken(relyingParty);
return data;
} else if (options.password) {
const xsts = await this.xbl.doReplayAuth(
this.username,
options.password,
options
);
return xsts;
} else {
return await retry(
async () => {
const msaToken = await this.getMsaToken();
if (options.flow === "sisu") {
const deviceToken = await this.xbl.getDeviceToken(options);
const sisu = await this.xbl.doSisuAuth(
msaToken,
deviceToken,
options
);
return sisu;
}
const { xstsToken, userToken, deviceToken, titleToken } = await this.xbl.getCachedTokens(relyingParty);
const userToken = await this.xbl.getUserToken(
msaToken,
options.flow === "msal"
);
if (this.doTitleAuth) {
const deviceToken = await this.xbl.getDeviceToken(options);
const titleToken = await this.xbl.getTitleToken(
msaToken,
deviceToken
);
const xsts = await this.xbl.getXSTSToken(
{ userToken, deviceToken, titleToken },
options
);
return xsts;
} else {
const xsts = await this.xbl.getXSTSToken({ userToken }, options);
return xsts;
}
},
() => {
this.msa.forceRefresh = true;
},
2
);
if (xstsToken.valid && !forceRefresh) {
return xstsToken.data;
}
if (options.password) {
const xsts = await this.xbl.doReplayAuth(this.username, options.password, options);
return xsts;
}
return await retry(
async () => {
const msaToken = await this.getMsaToken();
// sisu flow generates user and title tokens differently to other flows and should also be used to refresh them if they are invalid
if (options.flow === "sisu" && (!userToken.valid || !deviceToken.valid || !titleToken.valid)) {
const dt = await this.xbl.getDeviceToken(options);
const sisu = await this.xbl.doSisuAuth(msaToken, dt, options);
return sisu;
}
const ut = userToken.token ?? (await this.xbl.getUserToken(msaToken, options.flow === "msal"));
const dt = deviceToken.token ?? (await this.xbl.getDeviceToken(options));
const tt = titleToken.token ?? (this.doTitleAuth ? await this.xbl.getTitleToken(msaToken, dt) : undefined);
const xsts = await this.xbl.getXSTSToken({ userToken: ut, deviceToken: dt, titleToken: tt }, options);
return xsts;
},
() => {
this.msa.forceRefresh = true;
},
2
);
}
async getMinecraftJavaToken(options: any = {}) {
const response: any = { token: "", entitlements: {}, profile: {} };
const response: any = { token: "", entitlements: {} as any, profile: {} as any };
if (await this.mca.verifyTokens()) {
const { token } = await this.mca.getCachedAccessToken();
response.token = token;
@ -211,15 +163,43 @@ export class CustomAuthflow {
}
if (options.fetchEntitlements) {
response.entitlements = await this.mca.fetchEntitlements(response.token);
response.entitlements = await this.mca.fetchEntitlements(response.token).catch((e) => {});
}
if (options.fetchProfile) {
response.profile = await this.mca.fetchProfile(response.token);
response.profile = await this.mca.fetchProfile(response.token).catch((e) => {});
}
if (options.fetchCertificates) {
response.certificates = await this.mca.fetchCertificates(response.token);
response.certificates = await this.mca.fetchCertificates(response.token).catch((e) => []);
}
return response;
}
async getMinecraftBedrockToken(publicKey) {
// TODO: Fix cache, in order to do cache we also need to cache the ECDH keys so disable it
// is this even a good idea to cache?
if ((await this.mba.verifyTokens()) && false) {
// eslint-disable-line
const { chain } = this.mba.getCachedAccessToken();
return chain;
} else {
if (!publicKey) throw new Error("Need to specifiy a ECDH x509 URL encoded public key");
return await retry(
async () => {
const xsts = await this.getXboxToken(Endpoints.BedrockXSTSRelyingParty);
const token = await this.mba.getAccessToken(publicKey, xsts);
// If we want to auth with a title ID, make sure there's a TitleID in the response
const body = JSON.parse(Buffer.from(token.chain[1].split(".")[1], "base64").toString());
if (!body.extraData.titleId && this.doTitleAuth) {
throw Error("missing titleId in response");
}
return token.chain;
},
() => {
this.xbl.forceRefresh = true;
},
2
);
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -1,10 +1,5 @@
import EventEmitter from "events";
import pkg, {
Client,
ClientOptions,
createClient,
states,
} from "minecraft-protocol";
import pkg, { Client, ClientOptions, createClient, states } from "minecraft-protocol";
import { WebSocket } from "ws";
import { Logger } from "../logger.js";
import { Chat } from "./Chat.js";
@ -15,11 +10,12 @@ import { MineProtocol } from "./Protocol.js";
import { EaglerSkins } from "./skins/EaglerSkins.js";
import { Util } from "./Util.js";
import { BungeeUtil } from "./BungeeUtil.js";
import { IncomingMessage } from "http";
const { createSerializer, createDeserializer } = pkg;
export class Player extends EventEmitter {
public ws: WebSocket;
public ws: WebSocket & { httpRequest: IncomingMessage };
public username?: string;
public skin?: EaglerSkins.EaglerSkin;
public uuid?: string;
@ -37,14 +33,13 @@ export class Player extends EventEmitter {
public clientDeserializer: any;
private _kickMessage: string;
constructor(ws: WebSocket, playerName?: string, serverConnection?: Client) {
constructor(ws: WebSocket & { httpRequest: IncomingMessage }, playerName?: string, serverConnection?: Client) {
super();
this._logger = new Logger(`PlayerHandler-${playerName}`);
this.ws = ws;
this.username = playerName;
this.serverConnection = serverConnection;
if (this.username != null)
this.uuid = Util.generateUUIDFromPlayer(this.username);
if (this.username != null) this.uuid = Util.generateUUIDFromPlayer(this.username);
this.serverSerializer = createSerializer({
state: states.PLAY,
isServer: true,
@ -81,28 +76,15 @@ export class Player extends EventEmitter {
if (msg instanceof Buffer == false) return;
const decoder = PACKET_REGISTRY.get(msg[0]);
if (decoder && decoder.sentAfterHandshake) {
if (
!decoder &&
this.state != Enums.ClientState.POST_HANDSHAKE &&
msg.length >= 1
) {
this._logger.warn(
`Packet with ID 0x${Buffer.from([msg[0]]).toString(
"hex"
)} is missing a corresponding packet handler! Processing for this packet will be skipped.`
);
if (!decoder && this.state != Enums.ClientState.POST_HANDSHAKE && msg.length >= 1) {
this._logger.warn(`Packet with ID 0x${Buffer.from([msg[0]]).toString("hex")} is missing a corresponding packet handler! Processing for this packet will be skipped.`);
} else {
let parsed: Packet, err: boolean;
try {
parsed = new decoder.class();
parsed.deserialize(msg);
} catch (err) {
if (this.state != Enums.ClientState.POST_HANDSHAKE)
this._logger.warn(
`Packet ID 0x${Buffer.from([msg[0]]).toString(
"hex"
)} failed to parse! The packet will be skipped.`
);
if (this.state != Enums.ClientState.POST_HANDSHAKE) this._logger.warn(`Packet ID 0x${Buffer.from([msg[0]]).toString("hex")} failed to parse! The packet will be skipped.`);
err = true;
}
if (!err) {
@ -113,10 +95,7 @@ export class Player extends EventEmitter {
} else {
try {
const parsed = this.serverDeserializer.parsePacketBuffer(msg)?.data,
translated = this.translator.translatePacketClient(
parsed.params,
parsed
),
translated = this.translator.translatePacketClient(parsed.params, parsed),
packetData = {
name: translated[0],
params: translated[1],
@ -132,12 +111,7 @@ export class Player extends EventEmitter {
);
}
} catch (err) {
this._logger.debug(
`Client ${this
.username!} sent an unrecognized packet that could not be parsed!\n${
err.stack ?? err
}`
);
this._logger.debug(`Client ${this.username!} sent an unrecognized packet that could not be parsed!\n${err.stack ?? err}`);
}
}
});
@ -147,21 +121,12 @@ export class Player extends EventEmitter {
this.ws.send(packet.serialize());
}
public async read(
packetId?: Enums.PacketId,
filter?: (packet: Packet) => boolean
): Promise<Packet> {
public async read(packetId?: Enums.PacketId, filter?: (packet: Packet) => boolean): Promise<Packet> {
let res;
await Util.awaitPacket(this.ws, (packet) => {
if ((packetId != null && packetId == packet[0]) || packetId == null) {
const decoder = PACKET_REGISTRY.get(packet[0]);
if (
decoder != null &&
decoder.packetId == packet[0] &&
(this.state == Enums.ClientState.PRE_HANDSHAKE ||
decoder.sentAfterHandshake) &&
decoder.boundTo == Enums.PacketBounds.S
) {
if (decoder != null && decoder.packetId == packet[0] && (this.state == Enums.ClientState.PRE_HANDSHAKE || decoder.sentAfterHandshake) && decoder.boundTo == Enums.PacketBounds.S) {
let parsed: Packet,
err = false;
try {
@ -188,16 +153,7 @@ export class Player extends EventEmitter {
public disconnect(message: Chat.Chat | string) {
if (this.state == Enums.ClientState.POST_HANDSHAKE) {
this.ws.send(
Buffer.concat(
[
[0x40],
MineProtocol.writeString(
typeof message == "string" ? message : JSON.stringify(message)
),
].map((arr) => (arr instanceof Uint8Array ? arr : Buffer.from(arr)))
)
);
this.ws.send(Buffer.concat([[0x40], MineProtocol.writeString(typeof message == "string" ? message : JSON.stringify(message))].map((arr) => (arr instanceof Uint8Array ? arr : Buffer.from(arr)))));
this.ws.close();
} else {
const packet = new SCDisconnectPacket();
@ -208,10 +164,7 @@ export class Player extends EventEmitter {
}
public async connect(options: ClientOptions) {
if (this._alreadyConnected)
throw new Error(
`Invalid state: Player has already been connected to a server, and .connect() was just called. Please use switchServers() instead.`
);
if (this._alreadyConnected) throw new Error(`Invalid state: Player has already been connected to a server, and .connect() was just called. Please use switchServers() instead.`);
this._alreadyConnected = true;
this.serverConnection = createClient(
Object.assign(
@ -269,9 +222,7 @@ export class Player extends EventEmitter {
)
);
await this._bindListenersMineClient(this.serverConnection, true, () =>
oldConnection.end()
)
await this._bindListenersMineClient(this.serverConnection, true, () => oldConnection.end())
.then(() => {
this.emit("switchServer", this.serverConnection, this);
res();
@ -283,11 +234,7 @@ export class Player extends EventEmitter {
});
}
private async _bindListenersMineClient(
client: Client,
switchingServers?: boolean,
onSwitch?: Function
) {
private async _bindListenersMineClient(client: Client, switchingServers?: boolean, onSwitch?: Function) {
return new Promise((res, rej) => {
let stream = false,
uuid;
@ -300,17 +247,13 @@ export class Player extends EventEmitter {
if (!stream) {
rej(err);
} else {
this.disconnect(
`${Enums.ChatColor.RED}Something went wrong: ${err.stack ?? err}`
);
this.disconnect(`${Enums.ChatColor.RED}Something went wrong: ${err.stack ?? err}`);
}
};
setTimeout(() => {
if (!stream && this.state != Enums.ClientState.DISCONNECTED) {
client.end("Timed out waiting for server connection.");
this.disconnect(
Enums.ChatColor.RED + "Timed out waiting for server connection!"
);
this.disconnect(Enums.ChatColor.RED + "Timed out waiting for server connection!");
throw new Error("Timed out waiting for server connection!");
}
}, 30000);
@ -349,14 +292,8 @@ export class Player extends EventEmitter {
if (!stream) {
if (switchingServers) {
if (meta.name == "login" && meta.state == states.PLAY && uuid) {
this.translator = new BungeeUtil.PacketUUIDTranslator(
client.uuid,
this.uuid
);
const pckSeq = BungeeUtil.getRespawnSequence(
packet,
this.serverSerializer
);
this.translator = new BungeeUtil.PacketUUIDTranslator(client.uuid, this.uuid);
const pckSeq = BungeeUtil.getRespawnSequence(packet, this.serverSerializer);
this.ws.send(
this.serverSerializer.createPacketBuffer({
name: "login",
@ -367,19 +304,12 @@ export class Player extends EventEmitter {
stream = true;
if (onSwitch) onSwitch();
res(null);
} else if (
meta.name == "success" &&
meta.state == states.LOGIN &&
!uuid
) {
} else if (meta.name == "success" && meta.state == states.LOGIN && !uuid) {
uuid = packet.uuid;
}
} else {
if (meta.name == "login" && meta.state == states.PLAY && uuid) {
this.translator = new BungeeUtil.PacketUUIDTranslator(
client.uuid,
this.uuid
);
this.translator = new BungeeUtil.PacketUUIDTranslator(client.uuid, this.uuid);
this.ws.send(
this.serverSerializer.createPacketBuffer({
name: "login",
@ -389,19 +319,12 @@ export class Player extends EventEmitter {
stream = true;
if (onSwitch) onSwitch();
res(null);
} else if (
meta.name == "success" &&
meta.state == states.LOGIN &&
!uuid
) {
} else if (meta.name == "success" && meta.state == states.LOGIN && !uuid) {
uuid = packet.uuid;
}
}
} else {
const translated = this.translator!.translatePacketServer(
packet,
meta
),
const translated = this.translator!.translatePacketServer(packet, meta),
eventData = {
name: translated[0],
params: translated[1],
@ -444,8 +367,5 @@ export declare interface Player {
on<U extends keyof PlayerEvents>(event: U, listener: PlayerEvents[U]): this;
once<U extends keyof PlayerEvents>(event: U, listener: PlayerEvents[U]): this;
emit<U extends keyof PlayerEvents>(
event: U,
...args: Parameters<PlayerEvents[U]>
): boolean;
emit<U extends keyof PlayerEvents>(event: U, ...args: Parameters<PlayerEvents[U]>): boolean;
}

View File

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

View File

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

View File

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