ran prettier

This commit is contained in:
q13x 2023-07-04 19:01:02 -07:00
parent 2059fa8a44
commit d7e29c5c00
40 changed files with 2827 additions and 1976 deletions

View File

@ -1,2 +1,2 @@
const logger = new PLUGIN_MANAGER.Logger("ExamplePlugin") const logger = new PLUGIN_MANAGER.Logger("ExamplePlugin");
logger.info("Hi!") logger.info("Hi!");

View File

@ -1,6 +1,9 @@
const path = require("path") const path = require("path");
const os = require("os") const os = require("os");
module.exports = { module.exports = {
sourceDir: path.resolve(os.homedir(), path.join(process.env.REPL_SLUG, "src")) sourceDir: path.resolve(
} os.homedir(),
path.join(process.env.REPL_SLUG, "src")
),
};

View File

@ -1,135 +1,147 @@
// libraries // libraries
const fs = require("fs/promises") const fs = require("fs/promises");
const path = require("path") const path = require("path");
const crypto = require("crypto") const crypto = require("crypto");
const { const { sourceDir } = require("./config.js");
sourceDir
} = require("./config.js")
class Logger { class Logger {
constructor({ name, logDebug }) { constructor({ name, logDebug }) {
this.name = name this.name = name;
this.debug = logDebug this.debug = logDebug;
} }
_log(logType, data, method) { _log(logType, data, method) {
console[method](`[${this.name}] [${logType}] ${typeof data == "string" ? data : data.toString()}`) console[method](
`[${this.name}] [${logType}] ${
typeof data == "string" ? data : data.toString()
}`
);
} }
info(data) { info(data) {
this._log("info", data, "log") this._log("info", data, "log");
} }
warn(data) { warn(data) {
this._log("warn", data, "error") this._log("warn", data, "error");
} }
error(data) { error(data) {
this._log("error", data, "error") this._log("error", data, "error");
} }
debug(data) { debug(data) {
if (this.debug) { if (this.debug) {
this._log("debug", data, "error") this._log("debug", data, "error");
} }
} }
} }
async function recursiveFileSearch(dir) { async function recursiveFileSearch(dir) {
const fileList = [] const fileList = [];
for (const file of await fs.readdir(dir, { withFileTypes: true })) { for (const file of await fs.readdir(dir, { withFileTypes: true })) {
let pathDir = path.resolve(dir, file.name) let pathDir = path.resolve(dir, file.name);
if (file.isFile()) { if (file.isFile()) {
fileList.push(pathDir) fileList.push(pathDir);
} else if (file.isDirectory()) { } else if (file.isDirectory()) {
fileList.push(...(await recursiveFileSearch(pathDir))) fileList.push(...(await recursiveFileSearch(pathDir)));
} else { } else {
logger.warn(`Found directory entry that is neither a file or directory (${pathDir}), ignoring!`) logger.warn(
`Found directory entry that is neither a file or directory (${pathDir}), ignoring!`
);
} }
} }
return fileList return fileList;
} }
const logger = new Logger({ const logger = new Logger({
name: "launcher", name: "launcher",
logDebug: process.env.DEBUG == "true" logDebug: process.env.DEBUG == "true",
}), }),
LINE_SEPERATOR = "-----------------------------------" LINE_SEPERATOR = "-----------------------------------";
if (!process.env.REPL_SLUG) { if (!process.env.REPL_SLUG) {
logger.error(LINE_SEPERATOR) logger.error(LINE_SEPERATOR);
logger.error("Repl not detected!") logger.error("Repl not detected!");
logger.error("") logger.error("");
logger.error("This file is meant to be ran in a Repl") logger.error("This file is meant to be ran in a Repl");
logger.error(LINE_SEPERATOR) logger.error(LINE_SEPERATOR);
} }
logger.info(LINE_SEPERATOR) logger.info(LINE_SEPERATOR);
logger.info("Checking if the proxy needs to be recompiled...") logger.info("Checking if the proxy needs to be recompiled...");
logger.info(LINE_SEPERATOR) logger.info(LINE_SEPERATOR);
fs.readFile(path.join(__dirname, ".sourcehash")) fs.readFile(path.join(__dirname, ".sourcehash"))
.then(data => { .then((data) => {
let oldHash = data.toString() let oldHash = data.toString();
logger.info("Found old hash, calculating hash of source files...") logger.info("Found old hash, calculating hash of source files...");
recursiveFileSearch(sourceDir) recursiveFileSearch(sourceDir)
.then(files => { .then((files) => {
Promise.all(files.map(f => fs.readFile(f))) Promise.all(files.map((f) => fs.readFile(f))).then((data) => {
.then(data => { const hash = crypto.createHash("sha256");
const hash = crypto.createHash("sha256") data.forEach((d) => hash.update(d));
data.forEach(d => hash.update(d)) let sourceHash = hash.digest().toString();
let sourceHash = hash.digest().toString()
if (sourceHash === oldHash) { if (sourceHash === oldHash) {
logger.info("Source hasn't been changed, skipping compilation...") logger.info("Source hasn't been changed, skipping compilation...");
process.exit(0) process.exit(0);
} else { } else {
logger.info("Source has been changed, recompiling...") logger.info("Source has been changed, recompiling...");
fs.writeFile(path.join(__dirname, ".sourcehash"), sourceHash) fs.writeFile(path.join(__dirname, ".sourcehash"), sourceHash)
.then(() => { .then(() => {
process.exit(2) process.exit(2);
})
.catch(err => {
logger.error(`Could not write new hash to disk!\n${err.stack}`)
process.exit(1)
}) })
.catch((err) => {
logger.error(`Could not write new hash to disk!\n${err.stack}`);
process.exit(1);
});
} }
});
}) })
.catch((err) => {
logger.error(
`Could not calculate file hashes for files in directory ${sourceDir}!\n${err.stack}`
);
process.exit(1);
});
}) })
.catch(err => { .catch((err) => {
logger.error(`Could not calculate file hashes for files in directory ${sourceDir}!\n${err.stack}`)
process.exit(1)
})
})
.catch(err => {
if (err.code == "ENOENT") { if (err.code == "ENOENT") {
logger.warn("Previous source hash not found! Assuming a clean install is being used.") logger.warn(
logger.info("Calculating hash...") "Previous source hash not found! Assuming a clean install is being used."
);
logger.info("Calculating hash...");
recursiveFileSearch(sourceDir) recursiveFileSearch(sourceDir)
.then(files => { .then((files) => {
Promise.all(files.map(f => fs.readFile(f))) Promise.all(files.map((f) => fs.readFile(f))).then((data) => {
.then(data => { const hash = crypto.createHash("sha256");
const hash = crypto.createHash("sha256") data.forEach((d) => hash.update(d));
data.forEach(d => hash.update(d)) let sourceHash = hash.digest().toString();
let sourceHash = hash.digest().toString()
fs.writeFile(path.join(__dirname, ".sourcehash"), sourceHash) fs.writeFile(path.join(__dirname, ".sourcehash"), sourceHash)
.then(() => { .then(() => {
logger.info("Saved hash to disk.") logger.info("Saved hash to disk.");
process.exit(2) process.exit(2);
}) })
.catch(err => { .catch((err) => {
logger.error(`Could not write new hash to disk!\n${err.stack}`) logger.error(`Could not write new hash to disk!\n${err.stack}`);
process.exit(1) process.exit(1);
}) });
}) });
})
.catch(err => {
logger.error(`Could not calculate file hashes for files in directory ${sourceDir}!\n${err.stack}`)
process.exit(1)
}) })
.catch((err) => {
logger.error(
`Could not calculate file hashes for files in directory ${sourceDir}!\n${err.stack}`
);
process.exit(1);
});
} else { } else {
logger.error(`Could not read .sourcehash file in ${path.join(__dirname, ".sourcehash")} due to an unknown error! Try again with a clean repl?\n${err.stack}`) logger.error(
process.exit(1) `Could not read .sourcehash file in ${path.join(
__dirname,
".sourcehash"
)} due to an unknown error! Try again with a clean repl?\n${err.stack}`
);
process.exit(1);
} }
}) });

View File

@ -6,7 +6,7 @@ import { Config } from "./launcher_types.js";
export const config: Config = { export const config: Config = {
bridge: { bridge: {
enabled: false, enabled: false,
motd: null motd: null,
}, },
adapter: { adapter: {
name: "EaglerProxy", name: "EaglerProxy",
@ -14,20 +14,22 @@ export const config: Config = {
bindPort: 8080, bindPort: 8080,
maxConcurrentClients: 20, maxConcurrentClients: 20,
skinUrlWhitelist: undefined, skinUrlWhitelist: undefined,
motd: true ? "FORWARD" : { motd: true
? "FORWARD"
: {
iconURL: "logo.png", iconURL: "logo.png",
l1: "yes", l1: "yes",
l2: "no" l2: "no",
}, },
origins: { origins: {
allowOfflineDownloads: true, allowOfflineDownloads: true,
originWhitelist: null, originWhitelist: null,
originBlacklist: null originBlacklist: null,
}, },
server: { server: {
host: "no", host: "no",
port: 46625 port: 46625,
}, },
tls: undefined tls: undefined,
} },
} };

15
src/globals.d.ts vendored
View File

@ -4,10 +4,13 @@ import { Config } from "./launcher_types.js";
import { PluginManager } from "./proxy/pluginLoader/PluginManager.js"; import { PluginManager } from "./proxy/pluginLoader/PluginManager.js";
declare global { declare global {
var CONFIG: Config var CONFIG: Config;
var PROXY: Proxy var PROXY: Proxy;
var PLUGIN_MANAGER: PluginManager var PLUGIN_MANAGER: PluginManager;
var PACKET_REGISTRY: Map<number, Packet & { var PACKET_REGISTRY: Map<
class: any number,
}> Packet & {
class: any;
}
>;
} }

View File

@ -2,26 +2,28 @@ import * as dotenv from "dotenv";
import process from "process"; import process from "process";
import { Proxy } from "./proxy/Proxy.js"; import { Proxy } from "./proxy/Proxy.js";
import { config } from "./config.js"; import { config } from "./config.js";
dotenv.config() dotenv.config();
import { Logger } from "./logger.js"; import { Logger } from "./logger.js";
import { PROXY_BRANDING } from "./meta.js"; import { PROXY_BRANDING } from "./meta.js";
import { PluginManager } from "./proxy/pluginLoader/PluginManager.js"; import { PluginManager } from "./proxy/pluginLoader/PluginManager.js";
import { dirname, join } from "path"; import { dirname, join } from "path";
import { fileURLToPath } from "url"; import { fileURLToPath } from "url";
const logger = new Logger("Launcher") const logger = new Logger("Launcher");
let proxy: Proxy let proxy: Proxy;
global.CONFIG = config global.CONFIG = config;
logger.info("Loading plugins...") logger.info("Loading plugins...");
const pluginManager = new PluginManager(join(dirname(fileURLToPath(import.meta.url)), "plugins")) const pluginManager = new PluginManager(
global.PLUGIN_MANAGER = pluginManager join(dirname(fileURLToPath(import.meta.url)), "plugins")
await pluginManager.loadPlugins() );
global.PLUGIN_MANAGER = pluginManager;
await pluginManager.loadPlugins();
proxy = new Proxy(config.adapter, pluginManager) proxy = new Proxy(config.adapter, pluginManager);
pluginManager.proxy = proxy pluginManager.proxy = proxy;
logger.info(`Launching ${PROXY_BRANDING}...`) logger.info(`Launching ${PROXY_BRANDING}...`);
await proxy.init() await proxy.init();
global.PROXY = proxy global.PROXY = proxy;

View File

@ -1,40 +1,44 @@
export type Config = { export type Config = {
bridge: BridgeOptions, bridge: BridgeOptions;
adapter: AdapterOptions adapter: AdapterOptions;
} };
export type BridgeOptions = { export type BridgeOptions = {
enabled: boolean, enabled: boolean;
motd: 'FORWARD' | { motd:
iconURL?: string, | "FORWARD"
l1: string, | {
l2?: string iconURL?: string;
} l1: string;
} l2?: string;
};
};
export type AdapterOptions = { export type AdapterOptions = {
name: "EaglerProxy", name: "EaglerProxy";
bindHost: string, bindHost: string;
bindPort: number, bindPort: number;
maxConcurrentClients: 20, maxConcurrentClients: 20;
skinUrlWhitelist?: string[], skinUrlWhitelist?: string[];
origins: { origins: {
allowOfflineDownloads: boolean, allowOfflineDownloads: boolean;
originWhitelist: string[], originWhitelist: string[];
originBlacklist: string[] originBlacklist: string[];
}, };
motd: 'FORWARD' | { motd:
iconURL?: string, | "FORWARD"
l1: string, | {
l2?: string iconURL?: string;
}, l1: string;
l2?: string;
};
server: { server: {
host: string, host: string;
port: number port: number;
}, };
tls?: { tls?: {
enabled: boolean, enabled: boolean;
key: null, key: null;
cert: null cert: null;
} };
} };

View File

@ -1,63 +1,95 @@
import { Chalk } from "chalk" import { Chalk } from "chalk";
const color = new Chalk({ level: 2 }) const color = new Chalk({ level: 2 });
let global_verbose: boolean = false let global_verbose: boolean = false;
type JsonLogType = "info" | "warn" | "error" | "fatal" | "debug" type JsonLogType = "info" | "warn" | "error" | "fatal" | "debug";
type JsonOutput = { type JsonOutput = {
type: JsonLogType, type: JsonLogType;
message: string message: string;
} };
export function verboseLogging(newVal?: boolean) { export function verboseLogging(newVal?: boolean) {
global_verbose = newVal ?? global_verbose ? false : true global_verbose = newVal ?? global_verbose ? false : true;
} }
function jsonLog(type: JsonLogType, message: string): string { function jsonLog(type: JsonLogType, message: string): string {
return JSON.stringify({ return (
JSON.stringify({
type: type, type: type,
message: message message: message,
}) + "\n" }) + "\n"
);
} }
export class Logger { export class Logger {
loggerName: string loggerName: string;
verbose: boolean verbose: boolean;
private jsonLog: boolean = process.argv.includes("--json") || process.argv.includes("-j") private jsonLog: boolean =
process.argv.includes("--json") || process.argv.includes("-j");
constructor(name: string, verbose?: boolean) { constructor(name: string, verbose?: boolean) {
this.loggerName = name this.loggerName = name;
if (verbose) this.verbose = verbose if (verbose) this.verbose = verbose;
else this.verbose = global_verbose else this.verbose = global_verbose;
} }
info(s: string) { info(s: string) {
if (!this.jsonLog) process.stdout.write(`${color.green("I")} ${color.gray(new Date().toISOString())} ${color.reset(`${color.yellow(`${this.loggerName}:`)} ${s}`)}\n`) if (!this.jsonLog)
else process.stdout.write(jsonLog("info", s)) process.stdout.write(
`${color.green("I")} ${color.gray(
new Date().toISOString()
)} ${color.reset(`${color.yellow(`${this.loggerName}:`)} ${s}`)}\n`
);
else process.stdout.write(jsonLog("info", s));
} }
warn(s: string) { warn(s: string) {
if (!this.jsonLog) process.stdout.write(`${color.yellow("W")} ${color.gray(new Date().toISOString())} ${color.yellow(`${color.yellow(`${this.loggerName}:`)} ${s}`)}\n`) if (!this.jsonLog)
else process.stderr.write(jsonLog("warn", s)) process.stdout.write(
`${color.yellow("W")} ${color.gray(
new Date().toISOString()
)} ${color.yellow(`${color.yellow(`${this.loggerName}:`)} ${s}`)}\n`
);
else process.stderr.write(jsonLog("warn", s));
} }
error(s: string) { error(s: string) {
if (!this.jsonLog) process.stderr.write(`* ${color.red("E")} ${color.gray(new Date().toISOString())} ${color.redBright(`${color.red(`${this.loggerName}:`)} ${s}`)}\n`) if (!this.jsonLog)
else process.stderr.write(jsonLog("error", s)) process.stderr.write(
`* ${color.red("E")} ${color.gray(
new Date().toISOString()
)} ${color.redBright(`${color.red(`${this.loggerName}:`)} ${s}`)}\n`
);
else process.stderr.write(jsonLog("error", s));
} }
fatal(s: string) { fatal(s: string) {
if (!this.jsonLog) process.stderr.write(`** ${color.red("F!")} ${color.gray(new Date().toISOString())} ${color.bgRedBright(color.redBright(`${color.red(`${this.loggerName}:`)} ${s}`))}\n`) if (!this.jsonLog)
else process.stderr.write(jsonLog("fatal", s)) process.stderr.write(
`** ${color.red("F!")} ${color.gray(
new Date().toISOString()
)} ${color.bgRedBright(
color.redBright(`${color.red(`${this.loggerName}:`)} ${s}`)
)}\n`
);
else process.stderr.write(jsonLog("fatal", s));
} }
debug(s: string) { debug(s: string) {
if (this.verbose || global_verbose) { if (this.verbose || global_verbose) {
if (!this.jsonLog) process.stderr.write(`${color.gray("D")} ${color.gray(new Date().toISOString())} ${color.gray(`${color.gray(`${this.loggerName}:`)} ${s}`)}\n`) if (!this.jsonLog)
else process.stderr.write(jsonLog("debug", s)) process.stderr.write(
`${color.gray("D")} ${color.gray(
new Date().toISOString()
)} ${color.gray(`${color.gray(`${this.loggerName}:`)} ${s}`)}\n`
);
else process.stderr.write(jsonLog("debug", s));
} }
} }
} }
verboseLogging(process.env.DEBUG != null && process.env.DEBUG != "false" ? true : false) verboseLogging(
process.env.DEBUG != null && process.env.DEBUG != "false" ? true : false
);

View File

@ -1,11 +1,11 @@
const f = Object.freeze const f = Object.freeze;
// bridge meta // bridge meta
export const BRIDGE_VERSION: Readonly<number> = f(1) export const BRIDGE_VERSION: Readonly<number> = f(1);
// adapter meta // adapter meta
export const PROXY_BRANDING: Readonly<string> = f("EaglerProxy") export const PROXY_BRANDING: Readonly<string> = f("EaglerProxy");
export const PROXY_VERSION: Readonly<string> = f("1.0.7") export const PROXY_VERSION: Readonly<string> = f("1.0.7");
export const NETWORK_VERSION: Readonly<number> = f(0x03) export const NETWORK_VERSION: Readonly<number> = f(0x03);
export const VANILLA_PROTOCOL_VERSION: Readonly<number> = f(47) export const VANILLA_PROTOCOL_VERSION: Readonly<number> = f(47);

View File

@ -1,64 +1,85 @@
import { randomUUID } from "crypto" import { randomUUID } from "crypto";
import EventEmitter from "events" import EventEmitter from "events";
import pauth from "prismarine-auth" import pauth from "prismarine-auth";
import debug from "debug" import debug from "debug";
const { Authflow, Titles } = pauth; const { Authflow, Titles } = pauth;
const Enums = PLUGIN_MANAGER.Enums const Enums = PLUGIN_MANAGER.Enums;
export type ServerDeviceCodeResponse = { export type ServerDeviceCodeResponse = {
user_code: string user_code: string;
device_code: string device_code: string;
verification_uri: string verification_uri: string;
expires_in: number expires_in: number;
interval: number interval: number;
message: string message: string;
} };
class InMemoryCache { class InMemoryCache {
private cache = {} private cache = {};
async getCached () { async getCached() {
return this.cache return this.cache;
} }
async setCached (value) { async setCached(value) {
this.cache = value this.cache = value;
} }
async setCachedPartial (value) { async setCachedPartial(value) {
this.cache = { this.cache = {
...this.cache, ...this.cache,
...value ...value,
} };
} }
} }
export function auth(): EventEmitter { export function auth(): EventEmitter {
const emitter = new EventEmitter() const emitter = new EventEmitter();
const userIdentifier = randomUUID() const userIdentifier = randomUUID();
const flow = new Authflow(userIdentifier, ({ username, cacheName }) => new InMemoryCache(), { const flow = new Authflow(
userIdentifier,
({ username, cacheName }) => new InMemoryCache(),
{
authTitle: Titles.MinecraftJava, authTitle: Titles.MinecraftJava,
flow: 'sisu', flow: "sisu",
deviceType: "Win32" deviceType: "Win32",
}, code => { },
console.log = () => {} (code) => {
emitter.emit('code', code) console.log = () => {};
}) emitter.emit("code", code);
flow.getMinecraftJavaToken({ fetchProfile: true }) }
.then(async data => { );
const _data = (await (flow as any).mca.cache.getCached()).mca flow
.getMinecraftJavaToken({ fetchProfile: true })
.then(async (data) => {
const _data = (await (flow as any).mca.cache.getCached()).mca;
if (data.profile == null || (data.profile as any).error) if (data.profile == null || (data.profile as any).error)
return emitter.emit('error', new Error(Enums.ChatColor.RED + "Couldn't fetch profile data, does the account own Minecraft: Java Edition?")) return emitter.emit(
emitter.emit('done', { "error",
new Error(
Enums.ChatColor.RED +
"Couldn't fetch profile data, does the account own Minecraft: Java Edition?"
)
);
emitter.emit("done", {
accessToken: data.token, accessToken: data.token,
expiresOn: _data.obtainedOn + _data.expires_in * 1000, expiresOn: _data.obtainedOn + _data.expires_in * 1000,
selectedProfile: data.profile, selectedProfile: data.profile,
availableProfiles: [data.profile] availableProfiles: [data.profile],
});
}) })
}) .catch((err) => {
.catch(err => {
if (err.toString().includes("Not Found")) if (err.toString().includes("Not Found"))
emitter.emit('error', new Error(Enums.ChatColor.RED + "The provided account doesn't own Minecraft: Java Edition!")) emitter.emit(
"error",
new Error(
Enums.ChatColor.RED +
"The provided account doesn't own Minecraft: Java Edition!"
)
);
else else
emitter.emit('error', new Error(Enums.ChatColor.YELLOW + err.toString())) emitter.emit(
}) "error",
return emitter new Error(Enums.ChatColor.YELLOW + err.toString())
);
});
return emitter;
} }

View File

@ -1,5 +1,5 @@
export const config = { export const config = {
bindInternalServerPort: 25569, bindInternalServerPort: 25569,
bindInternalServerIp: "127.0.0.1", bindInternalServerIp: "127.0.0.1",
allowCustomPorts: false, allowCustomPorts: true,
}; };

View File

@ -1,60 +1,68 @@
import metadata from "./metadata.json" assert { type: "json" } import metadata from "./metadata.json" assert { type: "json" };
import { config } from "./config.js" import { config } from "./config.js";
import { createServer } from "minecraft-protocol" import { createServer } from "minecraft-protocol";
import { ClientState, ConnectionState, ServerGlobals } from "./types.js" import { ClientState, ConnectionState, ServerGlobals } from "./types.js";
import { handleConnect, setSG } from "./utils.js" import { handleConnect, setSG } from "./utils.js";
const PluginManager = PLUGIN_MANAGER const PluginManager = PLUGIN_MANAGER;
const Logger = PluginManager.Logger const Logger = PluginManager.Logger;
const Enums = PluginManager.Enums const Enums = PluginManager.Enums;
const Chat = PluginManager.Chat const Chat = PluginManager.Chat;
const Constants = PluginManager.Constants const Constants = PluginManager.Constants;
const Motd = PluginManager.Motd const Motd = PluginManager.Motd;
const Player = PluginManager.Player const Player = PluginManager.Player;
const MineProtocol = PluginManager.MineProtocol const MineProtocol = PluginManager.MineProtocol;
const EaglerSkins = PluginManager.EaglerSkins const EaglerSkins = PluginManager.EaglerSkins;
const Util = PluginManager.Util const Util = PluginManager.Util;
const logger = new Logger("EaglerProxyAAS") const logger = new Logger("EaglerProxyAAS");
logger.info(`Starting ${metadata.name} v${metadata.version}...`) logger.info(`Starting ${metadata.name} v${metadata.version}...`);
logger.info(`(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...") logger.info("Starting internal server...");
let server = createServer({ let server = createServer({
host: config.bindInternalServerIp, host: config.bindInternalServerIp,
port: config.bindInternalServerPort, port: config.bindInternalServerPort,
motdMsg: `${Enums.ChatColor.GOLD}EaglerProxy as a Service`, motdMsg: `${Enums.ChatColor.GOLD}EaglerProxy as a Service`,
"online-mode": false, "online-mode": false,
version: '1.8.9' version: "1.8.9",
}), sGlobals: ServerGlobals = { }),
sGlobals: ServerGlobals = {
server: server, server: server,
players: new Map() players: new Map(),
} };
setSG(sGlobals) setSG(sGlobals);
server.on('login', client => { server.on("login", (client) => {
logger.info(`Client ${client.username} has connected to the authentication server.`) logger.info(
client.on('end', () => { `Client ${client.username} has connected to the authentication server.`
sGlobals.players.delete(client.username) );
logger.info(`Client ${client.username} has disconnected from 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 = { const cs: ClientState = {
state: ConnectionState.AUTH, state: ConnectionState.AUTH,
gameClient: client, gameClient: client,
token: null, token: null,
lastStatusUpdate: null lastStatusUpdate: null,
} };
sGlobals.players.set(client.username, cs) sGlobals.players.set(client.username, cs);
handleConnect(cs) handleConnect(cs);
}) });
logger.info(
logger.info("Redirecting backend server IP... (this is required for the plugin to function)") "Redirecting backend server IP... (this is required for the plugin to function)"
);
CONFIG.adapter.server = { CONFIG.adapter.server = {
host: config.bindInternalServerIp, host: config.bindInternalServerIp,
port: config.bindInternalServerPort port: config.bindInternalServerPort,
} };
CONFIG.adapter.motd = { CONFIG.adapter.motd = {
l1: Enums.ChatColor.GOLD + "EaglerProxy as a Service" l1: Enums.ChatColor.GOLD + "EaglerProxy as a Service",
} };

View File

@ -3,25 +3,32 @@
"id": "eagpaas", "id": "eagpaas",
"version": "1.1.0", "version": "1.1.0",
"entry_point": "index.js", "entry_point": "index.js",
"requirements": [{ "requirements": [
{
"id": "eaglerproxy", "id": "eaglerproxy",
"version": "any" "version": "any"
}, { },
{
"id": "module:vec3", "id": "module:vec3",
"version": "^0.1.0" "version": "^0.1.0"
}, { },
{
"id": "module:prismarine-chunk", "id": "module:prismarine-chunk",
"version": "^1.33.0" "version": "^1.33.0"
}, { },
{
"id": "module:prismarine-block", "id": "module:prismarine-block",
"version": "^1.16.0" "version": "^1.16.0"
}, { },
{
"id": "module:prismarine-registry", "id": "module:prismarine-registry",
"version": "^1.6.0" "version": "^1.6.0"
}, { },
{
"id": "module:minecraft-protocol", "id": "module:minecraft-protocol",
"version": "^1.40.0" "version": "^1.40.0"
}], }
],
"load_after": [], "load_after": [],
"incompatibilities": [] "incompatibilities": []
} }

View File

@ -45,6 +45,7 @@ export enum ChatColor {
} }
export enum ConnectType { export enum ConnectType {
ONLINE, ONLINE = "ONLINE",
OFFLINE, OFFLINE = "OFFLINE",
EASYMC = "EASYMC",
} }

View File

@ -7,6 +7,8 @@ import { Client } from "minecraft-protocol";
import { ClientState, ConnectionState } from "./types.js"; import { ClientState, ConnectionState } from "./types.js";
import { auth, ServerDeviceCodeResponse } from "./auth.js"; import { auth, ServerDeviceCodeResponse } from "./auth.js";
import { config } from "./config.js"; import { config } from "./config.js";
import { handleCommand } from "./commands.js";
import { getTokenProfileEasyMc } from "./auth_easymc.js";
const { Vec3 } = vec3 as any; const { Vec3 } = vec3 as any;
const Enums = PLUGIN_MANAGER.Enums; const Enums = PLUGIN_MANAGER.Enums;
@ -50,7 +52,7 @@ export function handleConnect(client: ClientState) {
client.gameClient.write("login", { client.gameClient.write("login", {
entityId: 1, entityId: 1,
gameMode: 2, gameMode: 2,
dimension: 0, dimension: 1,
difficulty: 1, difficulty: 1,
maxPlayers: 1, maxPlayers: 1,
levelType: "flat", levelType: "flat",
@ -190,7 +192,7 @@ export function sendMessageLogin(client: Client, url: string, token: string) {
export function updateState( export function updateState(
client: Client, client: Client,
newState: "CONNECTION_TYPE" | "AUTH" | "SERVER", newState: "CONNECTION_TYPE" | "AUTH_EASYMC" | "AUTH" | "SERVER",
uri?: string, uri?: string,
code?: string code?: string
) { ) {
@ -201,7 +203,17 @@ export function updateState(
text: ` ${Enums.ChatColor.GOLD}EaglerProxy Authentication Server `, text: ` ${Enums.ChatColor.GOLD}EaglerProxy Authentication Server `,
}), }),
footer: JSON.stringify({ footer: JSON.stringify({
text: `${Enums.ChatColor.RED}Choose the connection type: 1 = online, 2 = offline.`, text: `${Enums.ChatColor.RED}Choose the connection type: 1 = online, 2 = offline, 3 = EasyMC.`,
}),
});
break;
case "AUTH_EASYMC":
client.write("playerlist_header", {
header: JSON.stringify({
text: ` ${Enums.ChatColor.GOLD}EaglerProxy Authentication Server `,
}),
footer: JSON.stringify({
text: `${Enums.ChatColor.RED}easymc.io/get${Enums.ChatColor.GOLD} | ${Enums.ChatColor.RED}/login <alt_token>`,
}), }),
}); });
break; break;
@ -234,6 +246,18 @@ export function updateState(
} }
} }
// assuming that the player will always stay at the same pos
export function playSelectSound(client: Client) {
client.write("named_sound_effect", {
soundName: "note.hat",
x: 8.5,
y: 65,
z: 8.5,
volume: 100,
pitch: 63,
});
}
export async function onConnect(client: ClientState) { export async function onConnect(client: ClientState) {
try { try {
client.state = ConnectionState.AUTH; client.state = ConnectionState.AUTH;
@ -267,7 +291,7 @@ export async function onConnect(client: ClientState) {
}, },
clickEvent: { clickEvent: {
action: "run_command", action: "run_command",
value: "1", value: "$1",
}, },
}); });
sendChatComponent(client.gameClient, { sendChatComponent(client.gameClient, {
@ -285,12 +309,30 @@ export async function onConnect(client: ClientState) {
}, },
clickEvent: { clickEvent: {
action: "run_command", action: "run_command",
value: "2", value: "$2",
},
});
sendChatComponent(client.gameClient, {
text: "3) ",
color: "gold",
extra: [
{
text: "Connect to an online server via EasyMC account pool (no Minecraft account needed)",
color: "white",
},
],
hoverEvent: {
action: "show_text",
value: Enums.ChatColor.GOLD + "Click me to select!",
},
clickEvent: {
action: "run_command",
value: "$3",
}, },
}); });
sendCustomMessage( sendCustomMessage(
client.gameClient, client.gameClient,
"Select an option from the above (1 = online, 2 = offline), either by clicking or manually typing out the option.", "Select an option from the above (1 = online, 2 = offline, 3 = EasyMC), either by clicking or manually typing out the option.",
"green" "green"
); );
updateState(client.gameClient, "CONNECTION_TYPE"); updateState(client.gameClient, "CONNECTION_TYPE");
@ -298,21 +340,28 @@ export async function onConnect(client: ClientState) {
let chosenOption: ConnectType | null = null; let chosenOption: ConnectType | null = null;
while (true) { while (true) {
const option = await awaitCommand(client.gameClient, (msg) => true); const option = await awaitCommand(client.gameClient, (msg) => true);
switch (option) { switch (option.replace(/\$/gim, "")) {
default: default:
sendCustomMessage( sendCustomMessage(
client.gameClient, client.gameClient,
`I don't understand what you meant by "${option}", please reply with a valid option!`, `I don't understand what you meant by "${option}", please reply with a valid option!`,
"red" "red"
); );
break;
case "1": case "1":
chosenOption = ConnectType.ONLINE; chosenOption = ConnectType.ONLINE;
break; break;
case "2": case "2":
chosenOption = ConnectType.OFFLINE; chosenOption = ConnectType.OFFLINE;
break; break;
case "3":
chosenOption = ConnectType.EASYMC;
break;
}
if (chosenOption != null) {
if (option.startsWith("$")) playSelectSound(client.gameClient);
break;
} }
if (chosenOption != null) break;
} }
if (chosenOption == ConnectType.ONLINE) { if (chosenOption == ConnectType.ONLINE) {
@ -347,6 +396,7 @@ export async function onConnect(client: ClientState) {
authHandler.on("code", codeCallback); authHandler.on("code", codeCallback);
await new Promise((res) => await new Promise((res) =>
authHandler.once("done", (result) => { authHandler.once("done", (result) => {
console.log(result);
savedAuth = result; savedAuth = result;
res(result); res(result);
}) })
@ -407,9 +457,64 @@ export async function onConnect(client: ClientState) {
} }
} }
try { try {
await PLUGIN_MANAGER.proxy.players sendChatComponent(client.gameClient, {
.get(client.gameClient.username) text: `Joining server under ${savedAuth.selectedProfile.name}/your Minecraft account's username! Run `,
.switchServers({ 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",
},
],
});
sendCustomMessage(
client.gameClient,
"Attempting to switch servers, please wait... (if you don't get connected to the target server after a while, the server might not be a Minecraft server at all)",
"gray"
);
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
) {
packet.cancel = true;
handleCommand(player, packet.params.message as string);
}
});
(player as any)._onlineSession = {
auth: "mojang",
username: savedAuth.selectedProfile.name,
session: {
accessToken: savedAuth.accessToken,
clientToken: savedAuth.selectedProfile.id,
selectedProfile: {
id: savedAuth.selectedProfile.id,
name: savedAuth.selectedProfile.name,
},
},
};
await player.switchServers({
host: host, host: host,
port: port, port: port,
version: "1.8.8", version: "1.8.8",
@ -441,7 +546,169 @@ export async function onConnect(client: ClientState) {
); );
} }
} }
} else if (chosenOption == ConnectType.EASYMC) {
const EASYMC_GET_TOKEN_URL = "easymc.io/get";
client.state = ConnectionState.AUTH;
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.`
);
sendChatComponent(client.gameClient, {
text: "Please generate an alt token at ",
color: "white",
extra: [
{
text: EASYMC_GET_TOKEN_URL,
color: "gold",
hoverEvent: {
action: "show_text",
value: Enums.ChatColor.GOLD + "Click me to open in a new window!",
},
clickEvent: {
action: "open_url",
value: `https://${EASYMC_GET_TOKEN_URL}`,
},
},
{
text: ", and then run ",
color: "white",
},
{
text: "/login <alt_token>",
color: "gold",
hoverEvent: {
action: "show_text",
value: Enums.ChatColor.GOLD + "Copy me to chat!",
},
clickEvent: {
action: "suggest_command",
value: `/login <alt_token>`,
},
},
{
text: " to log in.",
color: "white",
},
],
});
let appendOptions: any;
while (true) {
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, {
text: "Invalid usage! Please use the command as follows: ",
color: "red",
extra: [
{
text: "/login <alt_token>",
color: "gold",
hoverEvent: {
action: "show_text",
value: Enums.ChatColor.GOLD + "Copy me to chat!",
},
clickEvent: {
action: "suggest_command",
value: `/login <alt_token>`,
},
},
{
text: ".",
color: "red",
},
],
});
} else { } else {
const token = splitResponse[0];
if (token.length != 20) {
sendChatComponent(client.gameClient, {
text: "Please provide a valid token (you can get one ",
color: "red",
extra: [
{
text: "here",
color: "white",
hoverEvent: {
action: "show_text",
value:
Enums.ChatColor.GOLD +
"Click me to open in a new window!",
},
clickEvent: {
action: "open_url",
value: `https://${EASYMC_GET_TOKEN_URL}`,
},
},
{
text: "). ",
color: "red",
},
{
text: "/login <alt_token>",
color: "gold",
hoverEvent: {
action: "show_text",
value: Enums.ChatColor.GOLD + "Copy me to chat!",
},
clickEvent: {
action: "suggest_command",
value: `/login <alt_token>`,
},
},
{
text: ".",
color: "red",
},
],
});
} else {
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"
);
break;
} catch (err) {
sendChatComponent(client.gameClient, {
text: `EasyMC's servers replied with an error (${err.message}), please try again! `,
color: "red",
extra: [
{
text: "/login <alt_token>",
color: "gold",
hoverEvent: {
action: "show_text",
value: Enums.ChatColor.GOLD + "Copy me to chat!",
},
clickEvent: {
action: "suggest_command",
value: `/login <alt_token>`,
},
},
{
text: ".",
color: "red",
},
],
});
}
}
}
}
client.state = ConnectionState.SUCCESS; client.state = ConnectionState.SUCCESS;
client.lastStatusUpdate = Date.now(); client.lastStatusUpdate = Date.now();
updateState(client.gameClient, "SERVER"); updateState(client.gameClient, "SERVER");
@ -493,22 +760,62 @@ export async function onConnect(client: ClientState) {
} }
} }
try { try {
sendChatComponent(client.gameClient, {
text: `Joining server under ${appendOptions.username}/EasyMC account 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",
},
],
});
sendCustomMessage( sendCustomMessage(
client.gameClient, client.gameClient,
"Attempting to switch servers, please wait... (if you don't get connected to the target server after a while, the server might be online only)", "Attempting to switch servers, please wait... (if you don't get connected to the target server for a while, the server might be online only)",
"gray" "gray"
); );
await PLUGIN_MANAGER.proxy.players const player = PLUGIN_MANAGER.proxy.players.get(
.get(client.gameClient.username) client.gameClient.username
.switchServers({ );
player.on("vanillaPacket", (packet, origin) => {
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);
}
});
(player as any)._onlineSession = {
...appendOptions,
isEasyMC: true,
};
await player.switchServers({
host: host, host: host,
port: port, port: port,
version: "1.8.8", version: "1.8.8",
username: client.gameClient.username,
auth: "offline",
keepAlive: false, keepAlive: false,
skipValidation: true, skipValidation: true,
hideErrors: true, hideErrors: true,
...appendOptions,
}); });
} catch (err) { } catch (err) {
if (!client.gameClient.ended) { if (!client.gameClient.ended) {
@ -546,15 +853,15 @@ export function generateSpawnChunk(): Chunk.PCChunk {
() => () =>
new McBlock( new McBlock(
REGISTRY.blocksByName.air.id, REGISTRY.blocksByName.air.id,
REGISTRY.biomesByName.plains.id, REGISTRY.biomesByName.the_end.id,
0 0
) )
); );
chunk.setBlock( chunk.setBlock(
new Vec3(8, 64, 8), new Vec3(8, 64, 8),
new McBlock( new McBlock(
REGISTRY.blocksByName.barrier.id, REGISTRY.blocksByName.sea_lantern.id,
REGISTRY.biomesByName.plains.id, REGISTRY.biomesByName.the_end.id,
0 0
) )
); );
@ -562,7 +869,7 @@ export function generateSpawnChunk(): Chunk.PCChunk {
new Vec3(8, 67, 8), new Vec3(8, 67, 8),
new McBlock( new McBlock(
REGISTRY.blocksByName.barrier.id, REGISTRY.blocksByName.barrier.id,
REGISTRY.biomesByName.plains.id, REGISTRY.biomesByName.the_end.id,
0 0
) )
); );
@ -570,7 +877,7 @@ export function generateSpawnChunk(): Chunk.PCChunk {
new Vec3(7, 65, 8), new Vec3(7, 65, 8),
new McBlock( new McBlock(
REGISTRY.blocksByName.barrier.id, REGISTRY.blocksByName.barrier.id,
REGISTRY.biomesByName.plains.id, REGISTRY.biomesByName.the_end.id,
0 0
) )
); );
@ -578,7 +885,7 @@ export function generateSpawnChunk(): Chunk.PCChunk {
new Vec3(7, 66, 8), new Vec3(7, 66, 8),
new McBlock( new McBlock(
REGISTRY.blocksByName.barrier.id, REGISTRY.blocksByName.barrier.id,
REGISTRY.biomesByName.plains.id, REGISTRY.biomesByName.the_end.id,
0 0
) )
); );
@ -586,7 +893,7 @@ export function generateSpawnChunk(): Chunk.PCChunk {
new Vec3(9, 65, 8), new Vec3(9, 65, 8),
new McBlock( new McBlock(
REGISTRY.blocksByName.barrier.id, REGISTRY.blocksByName.barrier.id,
REGISTRY.biomesByName.plains.id, REGISTRY.biomesByName.the_end.id,
0 0
) )
); );
@ -594,7 +901,7 @@ export function generateSpawnChunk(): Chunk.PCChunk {
new Vec3(9, 66, 8), new Vec3(9, 66, 8),
new McBlock( new McBlock(
REGISTRY.blocksByName.barrier.id, REGISTRY.blocksByName.barrier.id,
REGISTRY.biomesByName.plains.id, REGISTRY.biomesByName.the_end.id,
0 0
) )
); );
@ -602,7 +909,7 @@ export function generateSpawnChunk(): Chunk.PCChunk {
new Vec3(8, 65, 7), new Vec3(8, 65, 7),
new McBlock( new McBlock(
REGISTRY.blocksByName.barrier.id, REGISTRY.blocksByName.barrier.id,
REGISTRY.biomesByName.plains.id, REGISTRY.biomesByName.the_end.id,
0 0
) )
); );
@ -610,7 +917,7 @@ export function generateSpawnChunk(): Chunk.PCChunk {
new Vec3(8, 66, 7), new Vec3(8, 66, 7),
new McBlock( new McBlock(
REGISTRY.blocksByName.barrier.id, REGISTRY.blocksByName.barrier.id,
REGISTRY.biomesByName.plains.id, REGISTRY.biomesByName.the_end.id,
0 0
) )
); );
@ -618,7 +925,7 @@ export function generateSpawnChunk(): Chunk.PCChunk {
new Vec3(8, 65, 9), new Vec3(8, 65, 9),
new McBlock( new McBlock(
REGISTRY.blocksByName.barrier.id, REGISTRY.blocksByName.barrier.id,
REGISTRY.biomesByName.plains.id, REGISTRY.biomesByName.the_end.id,
0 0
) )
); );
@ -626,10 +933,11 @@ export function generateSpawnChunk(): Chunk.PCChunk {
new Vec3(8, 66, 9), new Vec3(8, 66, 9),
new McBlock( new McBlock(
REGISTRY.blocksByName.barrier.id, REGISTRY.blocksByName.barrier.id,
REGISTRY.biomesByName.plains.id, REGISTRY.biomesByName.the_end.id,
0 0
) )
); );
chunk.setSkyLight(new Vec3(8, 66, 8), 15); // chunk.setBlockLight(new Vec3(8, 65, 8), 15);
chunk.setBlockLight(new Vec3(8, 66, 8), 15);
return chunk; return chunk;
} }

View File

@ -1,147 +1,153 @@
import { Logger } from "../logger.js" import { Logger } from "../logger.js";
import mcp, { states } from "minecraft-protocol" import mcp, { states } from "minecraft-protocol";
const { createSerializer, createDeserializer } = mcp const { createSerializer, createDeserializer } = mcp;
export namespace BungeeUtil { export namespace BungeeUtil {
export class PacketUUIDTranslator { export class PacketUUIDTranslator {
public serverSidePlayerUUID: string public serverSidePlayerUUID: string;
public clientSidePlayerUUID: string public clientSidePlayerUUID: string;
static readonly CAST_UUID_SERVER: string[] = [ static readonly CAST_UUID_SERVER: string[] = [
'update_attributes', "update_attributes",
'named_entity_spawn', "named_entity_spawn",
// drop this packet (twitch.tv integration not available anymore) // drop this packet (twitch.tv integration not available anymore)
'player_info' "player_info",
] ];
static readonly CAST_UUID_CLIENT: string[] = [ static readonly CAST_UUID_CLIENT: string[] = ["spectate"];
'spectate'
]
private _logger: Logger private _logger: Logger;
private _serverSerializer: any private _serverSerializer: any;
private _clientSerializer: any private _clientSerializer: any;
private _clientDeserializer: any private _clientDeserializer: any;
private _serverDeserializer: any private _serverDeserializer: any;
constructor(ssPlayerUUID: string, csPlayerUUID: string) { constructor(ssPlayerUUID: string, csPlayerUUID: string) {
this.serverSidePlayerUUID = ssPlayerUUID this.serverSidePlayerUUID = ssPlayerUUID;
this.clientSidePlayerUUID = csPlayerUUID this.clientSidePlayerUUID = csPlayerUUID;
this._logger = new Logger("PacketTranslator") this._logger = new Logger("PacketTranslator");
this._serverSerializer = createSerializer({ this._serverSerializer = createSerializer({
state: states.PLAY, state: states.PLAY,
isServer: true, isServer: true,
version: "1.8.8", version: "1.8.8",
customPackets: null customPackets: null,
}) });
this._clientSerializer = createSerializer({ this._clientSerializer = createSerializer({
state: states.PLAY, state: states.PLAY,
isServer: false, isServer: false,
version: "1.8.8", version: "1.8.8",
customPackets: null customPackets: null,
}) });
this._clientDeserializer = createDeserializer({ this._clientDeserializer = createDeserializer({
state: states.PLAY, state: states.PLAY,
isServer: false, isServer: false,
version: "1.8.8", version: "1.8.8",
customPackets: null customPackets: null,
}) });
this._serverDeserializer = createDeserializer({ this._serverDeserializer = createDeserializer({
state: states.PLAY, state: states.PLAY,
isServer: true, isServer: true,
version: "1.8.8", version: "1.8.8",
customPackets: null customPackets: null,
}) });
} }
public onClientWrite(packet: Buffer): Buffer /* write to server */ { public onClientWrite(packet: Buffer): Buffer /* write to server */ {
const { name, params } = this._serverDeserializer.parsePacketBuffer(packet).data const { name, params } =
return this._clientSerializer.createPacketBuffer(this._translatePacketClient(params, { name })) this._serverDeserializer.parsePacketBuffer(packet).data;
return this._clientSerializer.createPacketBuffer(
this._translatePacketClient(params, { name })
);
} }
public onServerWrite(packet: any, meta: any): Buffer /* write to client */ { public onServerWrite(packet: any, meta: any): Buffer /* write to client */ {
return this._serverSerializer.createPacketBuffer(this._translatePacketServer(packet, meta)) return this._serverSerializer.createPacketBuffer(
this._translatePacketServer(packet, meta)
);
} }
private _translatePacketClient(packet: any, meta: any): any | null { private _translatePacketClient(packet: any, meta: any): any | null {
if (PacketUUIDTranslator.CAST_UUID_CLIENT.some(id => id == meta.name)) { if (PacketUUIDTranslator.CAST_UUID_CLIENT.some((id) => id == meta.name)) {
if (meta.name == 'spectate') { if (meta.name == "spectate") {
if (packet.target == this.clientSidePlayerUUID) { if (packet.target == this.clientSidePlayerUUID) {
packet.target = this.serverSidePlayerUUID packet.target = this.serverSidePlayerUUID;
} else if (packet.target == this.serverSidePlayerUUID) { } else if (packet.target == this.serverSidePlayerUUID) {
packet.target = this.clientSidePlayerUUID packet.target = this.clientSidePlayerUUID;
} }
} }
} }
return { return {
name: meta.name, name: meta.name,
params: packet params: packet,
} };
} }
private _translatePacketServer(packet: any, meta: any): any | null { private _translatePacketServer(packet: any, meta: any): any | null {
if (PacketUUIDTranslator.CAST_UUID_SERVER.some(id => id == meta.name)) { if (PacketUUIDTranslator.CAST_UUID_SERVER.some((id) => id == meta.name)) {
if (meta.name == 'update_attributes') { if (meta.name == "update_attributes") {
for (const prop of packet.properties) { for (const prop of packet.properties) {
for (const modifier of prop.modifiers) { for (const modifier of prop.modifiers) {
if (modifier.uuid == this.serverSidePlayerUUID) { if (modifier.uuid == this.serverSidePlayerUUID) {
modifier.uuid = this.clientSidePlayerUUID modifier.uuid = this.clientSidePlayerUUID;
} else if (modifier.uuid == this.clientSidePlayerUUID) { } else if (modifier.uuid == this.clientSidePlayerUUID) {
modifier.uuid = this.serverSidePlayerUUID modifier.uuid = this.serverSidePlayerUUID;
} }
} }
} }
} else if (meta.name == 'named_entity_spawn') { } else if (meta.name == "named_entity_spawn") {
if (packet.playerUUID == this.serverSidePlayerUUID) { if (packet.playerUUID == this.serverSidePlayerUUID) {
packet.playerUUID = this.clientSidePlayerUUID packet.playerUUID = this.clientSidePlayerUUID;
} else if (packet.playerUUID == this.clientSidePlayerUUID) { } else if (packet.playerUUID == this.clientSidePlayerUUID) {
packet.playerUUID = this.serverSidePlayerUUID packet.playerUUID = this.serverSidePlayerUUID;
} }
} else if (meta.name == 'player_info') { } else if (meta.name == "player_info") {
for (const player of packet.data) { for (const player of packet.data) {
if (player.UUID == this.serverSidePlayerUUID) { if (player.UUID == this.serverSidePlayerUUID) {
player.UUID = this.clientSidePlayerUUID player.UUID = this.clientSidePlayerUUID;
} else if (player.UUID == this.clientSidePlayerUUID) { } else if (player.UUID == this.clientSidePlayerUUID) {
player.UUID = this.serverSidePlayerUUID player.UUID = this.serverSidePlayerUUID;
} }
} }
} }
} }
return { return {
name: meta.name, name: meta.name,
params: packet params: packet,
} };
} }
} }
export function getRespawnSequence(login: any, serializer: any): [Buffer, Buffer] { export function getRespawnSequence(
const dimset = getDimSets(login.dimension) login: any,
serializer: any
): [Buffer, Buffer] {
const dimset = getDimSets(login.dimension);
return [ return [
serializer.createPacketBuffer({ serializer.createPacketBuffer({
name: 'respawn', name: "respawn",
params: { params: {
dimension: dimset[0], dimension: dimset[0],
difficulty: login.difficulty, difficulty: login.difficulty,
gamemode: login.gameMode, gamemode: login.gameMode,
levelType: login.levelType levelType: login.levelType,
} },
}), }),
serializer.createPacketBuffer({ serializer.createPacketBuffer({
name: 'respawn', name: "respawn",
params: { params: {
dimension: dimset[1], dimension: dimset[1],
difficulty: login.difficulty, difficulty: login.difficulty,
gamemode: login.gameMode, gamemode: login.gameMode,
levelType: login.levelType levelType: login.levelType,
} },
}) }),
] ];
} }
function getDimSets(loginDim: number): [number, number] { function getDimSets(loginDim: number): [number, number] {
return [ return [
loginDim == -1 ? 0 : loginDim == 0 ? -1 : loginDim == 1 ? 0 : 0, loginDim == -1 ? 0 : loginDim == 0 ? -1 : loginDim == 1 ? 0 : 0,
loginDim loginDim,
] ];
} }
} }

View File

@ -1,50 +1,57 @@
import { Enums } from "./Enums.js" import { Enums } from "./Enums.js";
export namespace Chat { export namespace Chat {
export type ChatExtra = { export type ChatExtra = {
text: string, text: string;
bold?: boolean, bold?: boolean;
italic?: boolean, italic?: boolean;
underlined?: boolean, underlined?: boolean;
strikethrough?: boolean, strikethrough?: boolean;
obfuscated?: boolean, obfuscated?: boolean;
color?: Enums.ChatColor | 'reset' color?: Enums.ChatColor | "reset";
} };
export type Chat = { export type Chat = {
text?: string, text?: string;
bold?: boolean, bold?: boolean;
italic?: boolean, italic?: boolean;
underlined?: boolean, underlined?: boolean;
strikethrough?: boolean, strikethrough?: boolean;
obfuscated?: boolean, obfuscated?: boolean;
color?: Enums.ChatColor | 'reset', color?: Enums.ChatColor | "reset";
extra?: ChatExtra[] extra?: ChatExtra[];
} };
export function chatToPlainString(chat: Chat): string { export function chatToPlainString(chat: Chat): string {
let ret = '' let ret = "";
if (chat.text != null) ret += chat.text if (chat.text != null) ret += chat.text;
if (chat.extra != null) { if (chat.extra != null) {
chat.extra.forEach(extra => { chat.extra.forEach((extra) => {
let append = "" let append = "";
if (extra.bold) append += Enums.ChatColor.BOLD if (extra.bold) append += Enums.ChatColor.BOLD;
if (extra.italic) append += Enums.ChatColor.ITALIC if (extra.italic) append += Enums.ChatColor.ITALIC;
if (extra.underlined) append += Enums.ChatColor.UNDERLINED if (extra.underlined) append += Enums.ChatColor.UNDERLINED;
if (extra.strikethrough) append += Enums.ChatColor.STRIKETHROUGH if (extra.strikethrough) append += Enums.ChatColor.STRIKETHROUGH;
if (extra.obfuscated) append += Enums.ChatColor.OBFUSCATED if (extra.obfuscated) append += Enums.ChatColor.OBFUSCATED;
if (extra.color) append += extra.color == 'reset' ? Enums.ChatColor.RESET : resolveColor(extra.color) if (extra.color)
append += extra.text append +=
ret += append extra.color == "reset"
}) ? Enums.ChatColor.RESET
: resolveColor(extra.color);
append += extra.text;
ret += append;
});
} }
return ret return ret;
} }
const ccValues = Object.values(Enums.ChatColor) const ccValues = Object.values(Enums.ChatColor);
const ccKeys = Object.keys(Enums.ChatColor).map(str => str.toLowerCase()) const ccKeys = Object.keys(Enums.ChatColor).map((str) => str.toLowerCase());
function resolveColor(colorStr: string) { function resolveColor(colorStr: string) {
return Object.values(Enums.ChatColor)[ccKeys.indexOf(colorStr.toLowerCase())] ?? colorStr return (
Object.values(Enums.ChatColor)[ccKeys.indexOf(colorStr.toLowerCase())] ??
colorStr
);
} }
} }

View File

@ -1,13 +1,17 @@
import * as meta from "../meta.js" import * as meta from "../meta.js";
export namespace Constants { export namespace Constants {
export const EAGLERCRAFT_SKIN_CHANNEL_NAME: string = "EAG|Skins-1.8" export const EAGLERCRAFT_SKIN_CHANNEL_NAME: string = "EAG|Skins-1.8";
export const MAGIC_ENDING_SERVER_SKIN_DOWNLOAD_BUILTIN: number[] = [0x00, 0x00, 0x00] export const MAGIC_ENDING_SERVER_SKIN_DOWNLOAD_BUILTIN: number[] = [
export const MAGIC_ENDING_CLIENT_UPLOAD_SKIN_BUILTIN: number[] = [0x00, 0x05, 0x01, 0x00, 0x00, 0x00] 0x00, 0x00, 0x00,
export const EAGLERCRAFT_SKIN_CUSTOM_LENGTH = 64 ** 2 * 4 ];
export const MAGIC_ENDING_CLIENT_UPLOAD_SKIN_BUILTIN: number[] = [
0x00, 0x05, 0x01, 0x00, 0x00, 0x00,
];
export const EAGLERCRAFT_SKIN_CUSTOM_LENGTH = 64 ** 2 * 4;
export const JOIN_SERVER_PACKET = 0x01 export const JOIN_SERVER_PACKET = 0x01;
export const PLAYER_LOOK_PACKET = 0x08 export const PLAYER_LOOK_PACKET = 0x08;
} }
export const UPGRADE_REQUIRED_RESPONSE = `<!DOCTYPE html><!-- Served by ${meta.PROXY_BRANDING} (version: ${meta.PROXY_VERSION}) --><html> <head> <title>EaglerProxy landing page</title> <style> :root { font-family: "Arial" } code { padding: 3px 10px 3px 10px; border-radius: 5px; font-family: monospace; background-color: #1a1a1a; color: white; } </style> <script type="text/javascript"> window.addEventListener('load', () => { document.getElementById("connect-url").innerHTML = window.location.href.replace(window.location.protocol, window.location.protocol == "https:" ? "wss:" : "ws:"); }); </script> </head> <body> <h1>426 - Upgrade Required</h1> <p>Hello there! It appears as if you've reached the landing page for this EaglerProxy instance. Unfortunately, you cannot connect to the proxy server from here. To connect, use this server IP/URL: <code id="connect-url">loading...</code> (connect from any recent EaglercraftX client via Multiplayer > Direct Connect)</p> </body></html>` export const UPGRADE_REQUIRED_RESPONSE = `<!DOCTYPE html><!-- Served by ${meta.PROXY_BRANDING} (version: ${meta.PROXY_VERSION}) --><html> <head> <title>EaglerProxy landing page</title> <style> :root { font-family: "Arial" } code { padding: 3px 10px 3px 10px; border-radius: 5px; font-family: monospace; background-color: #1a1a1a; color: white; } </style> <script type="text/javascript"> window.addEventListener('load', () => { document.getElementById("connect-url").innerHTML = window.location.href.replace(window.location.protocol, window.location.protocol == "https:" ? "wss:" : "ws:"); }); </script> </head> <body> <h1>426 - Upgrade Required</h1> <p>Hello there! It appears as if you've reached the landing page for this EaglerProxy instance. Unfortunately, you cannot connect to the proxy server from here. To connect, use this server IP/URL: <code id="connect-url">loading...</code> (connect from any recent EaglercraftX client via Multiplayer > Direct Connect)</p> </body></html>`;

View File

@ -9,35 +9,35 @@ export namespace Enums {
SCSyncUuidPacket = 0x05, SCSyncUuidPacket = 0x05,
CSSetSkinPacket = 0x07, CSSetSkinPacket = 0x07,
CSReadyPacket = 0x08, CSReadyPacket = 0x08,
SCReadyPacket = 0x09 SCReadyPacket = 0x09,
} }
export enum ChannelMessageType { export enum ChannelMessageType {
CLIENT = 0x17, CLIENT = 0x17,
SERVER = 0x3f SERVER = 0x3f,
} }
export enum EaglerSkinPacketId { export enum EaglerSkinPacketId {
CFetchSkinEaglerPlayerReq = 0x03, CFetchSkinEaglerPlayerReq = 0x03,
SFetchSkinBuiltInRes = 0x04, SFetchSkinBuiltInRes = 0x04,
SFetchSkinRes = 0x05, SFetchSkinRes = 0x05,
CFetchSkinReq = 0x06 CFetchSkinReq = 0x06,
} }
export enum ClientState { export enum ClientState {
PRE_HANDSHAKE = "PRE_HANDSHAKE", PRE_HANDSHAKE = "PRE_HANDSHAKE",
POST_HANDSHAKE = "POST_HANDSHAKE", POST_HANDSHAKE = "POST_HANDSHAKE",
DISCONNECTED = "DISCONNECTED" DISCONNECTED = "DISCONNECTED",
} }
export enum PacketBounds { export enum PacketBounds {
C = "C", C = "C",
S = "S" S = "S",
} }
export enum SkinType { export enum SkinType {
BUILTIN, BUILTIN,
CUSTOM CUSTOM,
} }
export enum ChatColor { export enum ChatColor {
@ -61,11 +61,11 @@ export namespace Enums {
YELLOW = "§e", YELLOW = "§e",
WHITE = "§f", WHITE = "§f",
// text styling // text styling
OBFUSCATED = '§k', OBFUSCATED = "§k",
BOLD = '§l', BOLD = "§l",
STRIKETHROUGH = '§m', STRIKETHROUGH = "§m",
UNDERLINED = '§n', UNDERLINED = "§n",
ITALIC = '§o', ITALIC = "§o",
RESET = '§r' RESET = "§r",
} }
} }

View File

@ -1,60 +1,82 @@
import { randomUUID } from 'crypto'; import { randomUUID } from "crypto";
import pkg, { NewPingResult } from 'minecraft-protocol'; import pkg, { NewPingResult } from "minecraft-protocol";
import sharp from 'sharp'; import sharp from "sharp";
import { PROXY_BRANDING, PROXY_VERSION } from '../meta.js'; import { PROXY_BRANDING, PROXY_VERSION } from "../meta.js";
import { Config } from '../launcher_types.js' import { Config } from "../launcher_types.js";
import { Chat } from './Chat.js'; import { Chat } from "./Chat.js";
const { ping } = pkg const { ping } = pkg;
export namespace Motd { export namespace Motd {
const ICON_SQRT = 64 const ICON_SQRT = 64;
const IMAGE_DATA_PREPEND = "data:image/png;base64," const IMAGE_DATA_PREPEND = "data:image/png;base64,";
export class MOTD { export class MOTD {
public jsonMotd: JSONMotd public jsonMotd: JSONMotd;
public image?: Buffer public image?: Buffer;
constructor(motd: JSONMotd, image?: Buffer) { constructor(motd: JSONMotd, image?: Buffer) {
this.jsonMotd = motd this.jsonMotd = motd;
this.image = image this.image = image;
} }
public static async generateMOTDFromPing(host: string, port: number): Promise<MOTD> { public static async generateMOTDFromPing(
const pingRes = await ping({ host: host, port: port }) host: string,
if (typeof pingRes.version == 'string') port: number
throw new Error("Non-1.8 server detected!") ): Promise<MOTD> {
const pingRes = await ping({ host: host, port: port });
if (typeof pingRes.version == "string")
throw new Error("Non-1.8 server detected!");
else { else {
const newPingRes = pingRes as NewPingResult const newPingRes = pingRes as NewPingResult;
let image: Buffer let image: Buffer;
if (newPingRes.favicon != null) { if (newPingRes.favicon != null) {
if (!newPingRes.favicon.startsWith(IMAGE_DATA_PREPEND)) throw new Error("Invalid MOTD image!") if (!newPingRes.favicon.startsWith(IMAGE_DATA_PREPEND))
image = await this.generateEaglerMOTDImage(Buffer.from(newPingRes.favicon.substring(IMAGE_DATA_PREPEND.length), 'base64')) throw new Error("Invalid MOTD image!");
image = await this.generateEaglerMOTDImage(
Buffer.from(
newPingRes.favicon.substring(IMAGE_DATA_PREPEND.length),
"base64"
)
);
} }
return new MOTD({ return new MOTD(
{
brand: PROXY_BRANDING, brand: PROXY_BRANDING,
cracked: true, cracked: true,
data: { data: {
cache: true, cache: true,
icon: newPingRes.favicon != null ? true : false, icon: newPingRes.favicon != null ? true : false,
max: newPingRes.players.max, max: newPingRes.players.max,
motd: [typeof newPingRes.description == 'string' ? newPingRes.description : Chat.chatToPlainString(newPingRes.description), ""], motd: [
typeof newPingRes.description == "string"
? newPingRes.description
: Chat.chatToPlainString(newPingRes.description),
"",
],
online: newPingRes.players.online, online: newPingRes.players.online,
players: newPingRes.players.sample != null ? newPingRes.players.sample.map(v => v.name) : [], players:
newPingRes.players.sample != null
? newPingRes.players.sample.map((v) => v.name)
: [],
}, },
name: "placeholder name", name: "placeholder name",
secure: false, secure: false,
time: Date.now(), time: Date.now(),
type: "motd", type: "motd",
uuid: randomUUID(), // replace placeholder with global. cached UUID uuid: randomUUID(), // replace placeholder with global. cached UUID
vers: `${PROXY_BRANDING}/${PROXY_VERSION}` vers: `${PROXY_BRANDING}/${PROXY_VERSION}`,
}, image) },
image
);
} }
} }
public static async generateMOTDFromConfig(config: Config['adapter']): Promise<MOTD> { public static async generateMOTDFromConfig(
if (typeof config.motd != 'string') { config: Config["adapter"]
): Promise<MOTD> {
if (typeof config.motd != "string") {
const motd = new MOTD({ const motd = new MOTD({
brand: PROXY_BRANDING, brand: PROXY_BRANDING,
cracked: true, cracked: true,
@ -64,69 +86,68 @@ export namespace Motd {
max: config.maxConcurrentClients, max: config.maxConcurrentClients,
motd: [config.motd.l1, config.motd.l2 ?? ""], motd: [config.motd.l1, config.motd.l2 ?? ""],
online: 0, online: 0,
players: [] players: [],
}, },
name: config.name, name: config.name,
secure: false, secure: false,
time: Date.now(), time: Date.now(),
type: 'motd', type: "motd",
uuid: randomUUID(), uuid: randomUUID(),
vers: `${PROXY_BRANDING}/${PROXY_VERSION}` vers: `${PROXY_BRANDING}/${PROXY_VERSION}`,
}) });
if (config.motd.iconURL != null) { if (config.motd.iconURL != null) {
motd.image = await this.generateEaglerMOTDImage(config.motd.iconURL) motd.image = await this.generateEaglerMOTDImage(config.motd.iconURL);
} }
return motd return motd;
} else throw new Error("MOTD is set to be forwarded in the config!") } else throw new Error("MOTD is set to be forwarded in the config!");
} }
// TODO: fix not working // TODO: fix not working
public static generateEaglerMOTDImage(file: string | Buffer): Promise<Buffer> { public static generateEaglerMOTDImage(
file: string | Buffer
): Promise<Buffer> {
return new Promise<Buffer>((res, rej) => { return new Promise<Buffer>((res, rej) => {
sharp(file) sharp(file)
.resize(ICON_SQRT, ICON_SQRT, { .resize(ICON_SQRT, ICON_SQRT, {
kernel: 'nearest' kernel: "nearest",
}) })
.raw({ .raw({
depth: 'uchar' depth: "uchar",
}) })
.toBuffer() .toBuffer()
.then(buff => { .then((buff) => {
for (const pixel of buff) { for (const pixel of buff) {
if ((pixel & 0xFFFFFF) == 0) { if ((pixel & 0xffffff) == 0) {
buff[buff.indexOf(pixel)] = 0 buff[buff.indexOf(pixel)] = 0;
} }
} }
res(buff) res(buff);
})
.catch(rej)
}) })
.catch(rej);
});
} }
public toBuffer(): [string, Buffer] { public toBuffer(): [string, Buffer] {
return [ return [JSON.stringify(this.jsonMotd), this.image];
JSON.stringify(this.jsonMotd),
this.image
]
} }
} }
export type JSONMotd = { export type JSONMotd = {
brand: string, brand: string;
cracked: true, cracked: true;
data: { data: {
cache: true, cache: true;
icon: boolean, icon: boolean;
max: number, max: number;
motd: [string, string], motd: [string, string];
online: number, online: number;
players: string[], players: string[];
}, };
name: string, name: string;
secure: false, secure: false;
time: ReturnType<typeof Date.now>, time: ReturnType<typeof Date.now>;
type: "motd", type: "motd";
uuid: ReturnType<typeof randomUUID>, uuid: ReturnType<typeof randomUUID>;
vers: string vers: string;
} };
} }

View File

@ -1,35 +1,44 @@
import { readdir } from "fs/promises" import { readdir } from "fs/promises";
import { dirname, join, resolve } from "path" import { dirname, join, resolve } from "path";
import { fileURLToPath, pathToFileURL } from "url" import { fileURLToPath, pathToFileURL } from "url";
import { Enums } from "./Enums.js" import { Enums } from "./Enums.js";
import { Util } from "./Util.js" import { Util } from "./Util.js";
export default interface Packet { export default interface Packet {
packetId: Enums.PacketId packetId: Enums.PacketId;
type: "packet" type: "packet";
boundTo: Enums.PacketBounds boundTo: Enums.PacketBounds;
sentAfterHandshake: boolean sentAfterHandshake: boolean;
serialize: () => Buffer serialize: () => Buffer;
deserialize: (packet: Buffer) => this deserialize: (packet: Buffer) => this;
} }
export async function loadPackets(dir?: string): Promise<Map<Enums.PacketId, Packet & { class: any }>> { export async function loadPackets(
const files = (await Util.recursiveFileSearch(dir ?? join(dirname(fileURLToPath(import.meta.url)), "packets"))).filter(f => f.endsWith(".js") && !f.endsWith(".disabled.js")) dir?: string
const packetRegistry = new Map() ): Promise<Map<Enums.PacketId, Packet & { class: any }>> {
const files = (
await Util.recursiveFileSearch(
dir ?? join(dirname(fileURLToPath(import.meta.url)), "packets")
)
).filter((f) => f.endsWith(".js") && !f.endsWith(".disabled.js"));
const packetRegistry = new Map();
for (const file of files) { for (const file of files) {
const imp = await import(process.platform == 'win32' ? pathToFileURL(file).toString() : file) const imp = await import(
process.platform == "win32" ? pathToFileURL(file).toString() : file
);
for (const val of Object.values(imp)) { for (const val of Object.values(imp)) {
if (val != null) { if (val != null) {
let e: Packet let e: Packet;
try { e = new (val as any)() } try {
catch {} e = new (val as any)();
if (e != null && e.type == 'packet') { } catch {}
;(e as any).class = val if (e != null && e.type == "packet") {
packetRegistry.set(e.packetId, e) (e as any).class = val;
packetRegistry.set(e.packetId, e);
} }
} }
} }
} }
return packetRegistry return packetRegistry;
} }

View File

@ -1,9 +1,9 @@
import { import {
encodeULEB128 as _encodeVarInt, encodeULEB128 as _encodeVarInt,
decodeULEB128 as _decodeVarInt decodeULEB128 as _decodeVarInt,
} from "@thi.ng/leb128" } from "@thi.ng/leb128";
import { Enums } from "./Enums.js" import { Enums } from "./Enums.js";
import { Util } from "./Util.js" import { Util } from "./Util.js";
// reference: https://wiki.vg/index.php?title=Protocol&oldid=7368 (id: 73) // reference: https://wiki.vg/index.php?title=Protocol&oldid=7368 (id: 73)
// use https://hexed.it/ for hex analysis, dumps.ts for example dumps // use https://hexed.it/ for hex analysis, dumps.ts for example dumps
@ -12,67 +12,76 @@ import { Util } from "./Util.js"
export namespace MineProtocol { export namespace MineProtocol {
export type ReadResult<T> = { export type ReadResult<T> = {
value: T, value: T;
// the new buffer, but with the bytes being read being completely removed // the new buffer, but with the bytes being read being completely removed
// very useful when it comes to chaining // very useful when it comes to chaining
newBuffer: Buffer newBuffer: Buffer;
} };
export type UUID = string export type UUID = string;
export function writeVarInt(int: number): Buffer { export function writeVarInt(int: number): Buffer {
return Buffer.from(_encodeVarInt(int)) return Buffer.from(_encodeVarInt(int));
} }
export function readVarInt(buff: Buffer, offset?: number): ReadResult<number> { export function readVarInt(
buff = offset ? buff.subarray(offset) : buff buff: Buffer,
const read = _decodeVarInt(buff), len = read[1] offset?: number
): ReadResult<number> {
buff = offset ? buff.subarray(offset) : buff;
const read = _decodeVarInt(buff),
len = read[1];
return { return {
// potential oversight? // potential oversight?
value: Number(read[0]), value: Number(read[0]),
newBuffer: buff.subarray(len) newBuffer: buff.subarray(len),
} };
} }
export function writeString(str: string): Buffer { export function writeString(str: string): Buffer {
const bufferized = Buffer.from(str, 'utf8'), len = writeVarInt(bufferized.length) const bufferized = Buffer.from(str, "utf8"),
return Buffer.concat([len, bufferized]) len = writeVarInt(bufferized.length);
return Buffer.concat([len, bufferized]);
} }
export function readString(buff: Buffer, offset?: number): ReadResult<string> { export function readString(
buff = offset ? buff.subarray(offset) : buff buff: Buffer,
const len = readVarInt(buff), str = len.newBuffer.subarray(0, len.value).toString('utf8') offset?: number
): ReadResult<string> {
buff = offset ? buff.subarray(offset) : buff;
const len = readVarInt(buff),
str = len.newBuffer.subarray(0, len.value).toString("utf8");
return { return {
value: str, value: str,
newBuffer: len.newBuffer.subarray(len.value) newBuffer: len.newBuffer.subarray(len.value),
} };
} }
const _readShort = (a: number, b: number) => a << 8 | b << 0 const _readShort = (a: number, b: number) => (a << 8) | (b << 0);
export function readShort(buff: Buffer, offset?: number): ReadResult<number> { export function readShort(buff: Buffer, offset?: number): ReadResult<number> {
buff = offset ? buff.subarray(offset) : buff buff = offset ? buff.subarray(offset) : buff;
return { return {
value: _readShort(buff[0], buff[1]), value: _readShort(buff[0], buff[1]),
newBuffer: buff.subarray(2) newBuffer: buff.subarray(2),
} };
} }
export function writeShort(num: number): Buffer { export function writeShort(num: number): Buffer {
const alloc = Buffer.alloc(2) const alloc = Buffer.alloc(2);
alloc.writeInt16BE(num) alloc.writeInt16BE(num);
return alloc return alloc;
} }
export function readUUID(buff: Buffer, offset?: number): ReadResult<string> { export function readUUID(buff: Buffer, offset?: number): ReadResult<string> {
buff = offset ? buff.subarray(offset) : buff buff = offset ? buff.subarray(offset) : buff;
return { return {
value: Util.uuidBufferToString(buff.subarray(0, 16)), value: Util.uuidBufferToString(buff.subarray(0, 16)),
newBuffer: buff.subarray(16) newBuffer: buff.subarray(16),
} };
} }
export function writeUUID(uuid: string | Buffer): Buffer { export function writeUUID(uuid: string | Buffer): Buffer {
return typeof uuid == 'string' ? Util.uuidStringToBuffer(uuid) : uuid return typeof uuid == "string" ? Util.uuidStringToBuffer(uuid) : uuid;
} }
} }

View File

@ -2,18 +2,23 @@ import { WebSocket, WebSocketServer } from "ws";
import { Config } from "../launcher_types.js"; import { Config } from "../launcher_types.js";
import { Logger } from "../logger.js"; import { Logger } from "../logger.js";
import Packet, { loadPackets } from "./Packet.js"; import Packet, { loadPackets } from "./Packet.js";
import * as http from "http" import * as http from "http";
import * as https from "https" import * as https from "https";
import { readFile } from "fs/promises"; import { readFile } from "fs/promises";
import { Duplex } from "stream"; import { Duplex } from "stream";
import { parseDomain, ParseResultType } from "parse-domain" import { parseDomain, ParseResultType } from "parse-domain";
import { Util } from "./Util.js"; import { Util } from "./Util.js";
import CSLoginPacket from "./packets/CSLoginPacket.js"; import CSLoginPacket from "./packets/CSLoginPacket.js";
import SCIdentifyPacket from "./packets/SCIdentifyPacket.js"; import SCIdentifyPacket from "./packets/SCIdentifyPacket.js";
import { Motd } from "./Motd.js"; import { Motd } from "./Motd.js";
import { Player } from "./Player.js"; import { Player } from "./Player.js";
import { Enums } from "./Enums.js"; import { Enums } from "./Enums.js";
import { 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 { CSUsernamePacket } from "./packets/CSUsernamePacket.js";
import { SCSyncUuidPacket } from "./packets/SCSyncUuidPacket.js"; import { SCSyncUuidPacket } from "./packets/SCSyncUuidPacket.js";
import { SCReadyPacket } from "./packets/SCReadyPacket.js"; import { SCReadyPacket } from "./packets/SCReadyPacket.js";
@ -26,312 +31,457 @@ import { CSChannelMessagePacket } from "./packets/channel/CSChannelMessage.js";
import { Constants, UPGRADE_REQUIRED_RESPONSE } from "./Constants.js"; import { Constants, UPGRADE_REQUIRED_RESPONSE } from "./Constants.js";
import { PluginManager } from "./pluginLoader/PluginManager.js"; import { PluginManager } from "./pluginLoader/PluginManager.js";
let instanceCount = 0 let instanceCount = 0;
const chalk = new Chalk({ level: 2 }) const chalk = new Chalk({ level: 2 });
export class Proxy extends EventEmitter { export class Proxy extends EventEmitter {
public packetRegistry: Map<number, Packet & { public packetRegistry: Map<
class: any number,
}> Packet & {
public players = new Map<string, Player>() class: any;
public pluginManager: PluginManager }
public config: Config['adapter'] >;
public wsServer: WebSocketServer public players = new Map<string, Player>();
public httpServer: http.Server public pluginManager: PluginManager;
public skinServer: EaglerSkins.SkinServer public config: Config["adapter"];
public broadcastMotd?: Motd.MOTD public wsServer: WebSocketServer;
public httpServer: http.Server;
public skinServer: EaglerSkins.SkinServer;
public broadcastMotd?: Motd.MOTD;
private _logger: Logger private _logger: Logger;
private initalHandlerLogger: Logger private initalHandlerLogger: Logger;
private loaded: boolean private loaded: boolean;
constructor(config: Config['adapter'], pluginManager: PluginManager) { constructor(config: Config["adapter"], pluginManager: PluginManager) {
super() super();
this._logger = new Logger(`EaglerProxy-${instanceCount}`) this._logger = new Logger(`EaglerProxy-${instanceCount}`);
this.initalHandlerLogger = new Logger(`EaglerProxy-InitialHandler`) this.initalHandlerLogger = new Logger(`EaglerProxy-InitialHandler`);
// hijack the initial handler logger to append [InitialHandler] to the beginning // hijack the initial handler logger to append [InitialHandler] to the beginning
;(this.initalHandlerLogger as any)._info = this.initalHandlerLogger.info (this.initalHandlerLogger as any)._info = this.initalHandlerLogger.info;
this.initalHandlerLogger.info = (msg: string) => { this.initalHandlerLogger.info = (msg: string) => {
;(this.initalHandlerLogger as any)._info(`${chalk.blue("[InitialHandler]")} ${msg}`) (this.initalHandlerLogger as any)._info(
} `${chalk.blue("[InitialHandler]")} ${msg}`
;(this.initalHandlerLogger as any)._warn = this.initalHandlerLogger.warn );
};
(this.initalHandlerLogger as any)._warn = this.initalHandlerLogger.warn;
this.initalHandlerLogger.warn = (msg: string) => { this.initalHandlerLogger.warn = (msg: string) => {
;(this.initalHandlerLogger as any)._warn(`${chalk.blue("[InitialHandler]")} ${msg}`) (this.initalHandlerLogger as any)._warn(
} `${chalk.blue("[InitialHandler]")} ${msg}`
;(this.initalHandlerLogger as any)._error = this.initalHandlerLogger.error );
};
(this.initalHandlerLogger as any)._error = this.initalHandlerLogger.error;
this.initalHandlerLogger.error = (msg: string) => { this.initalHandlerLogger.error = (msg: string) => {
;(this.initalHandlerLogger as any)._error(`${chalk.blue("[InitialHandler]")} ${msg}`) (this.initalHandlerLogger as any)._error(
} `${chalk.blue("[InitialHandler]")} ${msg}`
;(this.initalHandlerLogger as any)._fatal = this.initalHandlerLogger.fatal );
};
(this.initalHandlerLogger as any)._fatal = this.initalHandlerLogger.fatal;
this.initalHandlerLogger.fatal = (msg: string) => { this.initalHandlerLogger.fatal = (msg: string) => {
;(this.initalHandlerLogger as any)._fatal(`${chalk.blue("[InitialHandler]")} ${msg}`) (this.initalHandlerLogger as any)._fatal(
} `${chalk.blue("[InitialHandler]")} ${msg}`
;(this.initalHandlerLogger as any)._debug = this.initalHandlerLogger.debug );
};
(this.initalHandlerLogger as any)._debug = this.initalHandlerLogger.debug;
this.initalHandlerLogger.debug = (msg: string) => { this.initalHandlerLogger.debug = (msg: string) => {
;(this.initalHandlerLogger as any)._debug(`${chalk.blue("[InitialHandler]")} ${msg}`) (this.initalHandlerLogger as any)._debug(
} `${chalk.blue("[InitialHandler]")} ${msg}`
this.config = config );
this.pluginManager = pluginManager };
instanceCount++ this.config = config;
this.pluginManager = pluginManager;
instanceCount++;
process.on('uncaughtException', err => { 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 => { process.on("unhandledRejection", (err) => {
this._logger.warn(`An unhandled rejection was caught! Rejection: ${err}`) this._logger.warn(`An unhandled rejection was caught! Rejection: ${err}`);
}) });
} }
public async init() { public async init() {
this._logger.info(`Starting ${PROXY_BRANDING} v${PROXY_VERSION}...`) this._logger.info(`Starting ${PROXY_BRANDING} v${PROXY_VERSION}...`);
global.PROXY = this global.PROXY = this;
if (this.loaded) throw new Error("Can't initiate if proxy instance is already initialized or is being initialized!") if (this.loaded)
this.loaded = true throw new Error(
this.packetRegistry = await loadPackets() "Can't initiate if proxy instance is already initialized or is being initialized!"
this.skinServer = new EaglerSkins.SkinServer(this, this.config.skinUrlWhitelist) );
global.PACKET_REGISTRY = this.packetRegistry this.loaded = true;
if (this.config.motd == 'FORWARD') { this.packetRegistry = await loadPackets();
this._pollServer(this.config.server.host, this.config.server.port) 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);
} else { } else {
// TODO: motd // TODO: motd
const broadcastMOTD = await Motd.MOTD.generateMOTDFromConfig(this.config) const broadcastMOTD = await Motd.MOTD.generateMOTDFromConfig(this.config);
;(broadcastMOTD as any)._static = true (broadcastMOTD as any)._static = true;
this.broadcastMotd = broadcastMOTD this.broadcastMotd = broadcastMOTD;
// playercount will be dynamically updated // playercount will be dynamically updated
} }
if (this.config.tls && this.config.tls.enabled) { if (this.config.tls && this.config.tls.enabled) {
this.httpServer = https.createServer({ this.httpServer = https
.createServer(
{
key: await readFile(this.config.tls.key), key: await readFile(this.config.tls.key),
cert: await readFile(this.config.tls.cert) cert: await readFile(this.config.tls.cert),
}, (req, res) => this._handleNonWSRequest(req, res, this.config)).listen(this.config.bindPort || 8080, this.config.bindHost || '127.0.0.1') },
(req, res) => this._handleNonWSRequest(req, res, this.config)
)
.listen(
this.config.bindPort || 8080,
this.config.bindHost || "127.0.0.1"
);
this.wsServer = new WebSocketServer({ this.wsServer = new WebSocketServer({
noServer: true noServer: true,
}) });
} else { } 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({ this.wsServer = new WebSocketServer({
noServer: true noServer: true,
}) });
} }
this.httpServer.on('error', err => { this.httpServer.on("error", (err) => {
this._logger.warn(`HTTP server threw an error: ${err.stack}`) this._logger.warn(`HTTP server threw an error: ${err.stack}`);
}) });
this.wsServer.on('error', err => { this.wsServer.on("error", (err) => {
this._logger.warn(`WebSocket server threw an error: ${err.stack}`) this._logger.warn(`WebSocket server threw an error: ${err.stack}`);
}) });
this.httpServer.on('upgrade', async (r, s, h) => { this.httpServer.on("upgrade", async (r, s, h) => {
try { try {
await this._handleWSConnectionReq(r, s, h) await this._handleWSConnectionReq(r, s, h);
} catch (err) { } 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.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']) { private _handleNonWSRequest(
res.setHeader("Content-Type", "text/html") req: http.IncomingMessage,
res: http.ServerResponse,
config: Config["adapter"]
) {
res
.setHeader("Content-Type", "text/html")
.writeHead(426) .writeHead(426)
.end(UPGRADE_REQUIRED_RESPONSE) .end(UPGRADE_REQUIRED_RESPONSE);
} }
readonly LOGIN_TIMEOUT = 30000 readonly LOGIN_TIMEOUT = 30000;
private async _handleWSConnection(ws: WebSocket) { private async _handleWSConnection(ws: WebSocket) {
const firstPacket = await Util.awaitPacket(ws) const firstPacket = await Util.awaitPacket(ws);
let player: Player, handled: boolean let player: Player, handled: boolean;
setTimeout(() => { setTimeout(() => {
if (!handled) { if (!handled) {
this.initalHandlerLogger.warn(`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}`} due to connection timing out.`) this.initalHandlerLogger.warn(
if (player) player.disconnect(`${Enums.ChatColor.YELLOW} Your connection timed out whilst processing handshake, please try again.`) `Disconnecting client ${
else ws.close() 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.`
);
else ws.close();
} }
}, this.LOGIN_TIMEOUT) }, this.LOGIN_TIMEOUT);
try { try {
if (firstPacket.toString() === "Accept: MOTD") { if (firstPacket.toString() === "Accept: MOTD") {
if (this.broadcastMotd) { if (this.broadcastMotd) {
if ((this.broadcastMotd as any)._static) { if ((this.broadcastMotd as any)._static) {
this.broadcastMotd.jsonMotd.data.online = this.players.size this.broadcastMotd.jsonMotd.data.online = this.players.size;
// sample for players // sample for players
this.broadcastMotd.jsonMotd.data.players = [] this.broadcastMotd.jsonMotd.data.players = [];
const playerSample = [...this.players.keys()] const playerSample = [...this.players.keys()]
.filter(sample => !sample.startsWith("!phs_")) .filter((sample) => !sample.startsWith("!phs_"))
.slice(0, 5) .slice(0, 5);
this.broadcastMotd.jsonMotd.data.players = playerSample 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() const bufferized = this.broadcastMotd.toBuffer();
ws.send(bufferized[0]) ws.send(bufferized[0]);
if (bufferized[1] != null) ws.send(bufferized[1]) if (bufferized[1] != null) ws.send(bufferized[1]);
} else { } else {
const motd = this.broadcastMotd.toBuffer() const motd = this.broadcastMotd.toBuffer();
ws.send(motd[0]) ws.send(motd[0]);
if (motd[1] != null) ws.send(motd[1]) if (motd[1] != null) ws.send(motd[1]);
} }
} }
handled = true handled = true;
ws.close() ws.close();
} else { } else {
player = new Player(ws) player = new Player(ws);
const loginPacket = new CSLoginPacket().deserialize(firstPacket) const loginPacket = new CSLoginPacket().deserialize(firstPacket);
player.state = Enums.ClientState.PRE_HANDSHAKE player.state = Enums.ClientState.PRE_HANDSHAKE;
if (loginPacket.gameVersion != VANILLA_PROTOCOL_VERSION) { if (loginPacket.gameVersion != VANILLA_PROTOCOL_VERSION) {
player.disconnect(`${Enums.ChatColor.RED}Please connect to this proxy on EaglercraftX 1.8.9.`) player.disconnect(
return `${Enums.ChatColor.RED}Please connect to this proxy on EaglercraftX 1.8.9.`
);
return;
} else if (loginPacket.networkVersion != NETWORK_VERSION) { } 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(
return `${Enums.ChatColor.RED}Your EaglercraftX version is too ${
loginPacket.networkVersion > NETWORK_VERSION ? "new" : "old"
}! Please ${
loginPacket.networkVersion > NETWORK_VERSION
? "downgrade"
: "update"
}.`
);
return;
} }
try { Util.validateUsername(loginPacket.username) } try {
catch (err) { Util.validateUsername(loginPacket.username);
player.disconnect(`${Enums.ChatColor.RED}${err.reason || err}`) } catch (err) {
return player.disconnect(`${Enums.ChatColor.RED}${err.reason || err}`);
return;
} }
player.username = loginPacket.username player.username = loginPacket.username;
player.uuid = Util.generateUUIDFromPlayer(player.username) player.uuid = Util.generateUUIDFromPlayer(player.username);
if (this.players.size > this.config.maxConcurrentClients) { if (this.players.size > this.config.maxConcurrentClients) {
player.disconnect(`${Enums.ChatColor.YELLOW}Proxy is full! Please try again later.`) player.disconnect(
return `${Enums.ChatColor.YELLOW}Proxy is full! Please try again later.`
} 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;
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!`
);
return;
} }
this.players.set(`!phs.${player.uuid}`, player) 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!`) this._logger.info(
player.write(new SCIdentifyPacket()) `Player ${loginPacket.username} (${Util.generateUUIDFromPlayer(
const usernamePacket: CSUsernamePacket = await player.read(Enums.PacketId.CSUsernamePacket) as any 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;
if (usernamePacket.username !== player.username) { 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(
return `${Enums.ChatColor.YELLOW}Failed to complete handshake. Your game version may be too old or too new.`
);
return;
} }
const syncUuid = new SCSyncUuidPacket() const syncUuid = new SCSyncUuidPacket();
syncUuid.username = player.username syncUuid.username = player.username;
syncUuid.uuid = player.uuid syncUuid.uuid = player.uuid;
player.write(syncUuid) 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], skin = prom[1],
obj = new EaglerSkins.EaglerSkin() obj = new EaglerSkins.EaglerSkin();
obj.owner = player obj.owner = player;
obj.type = skin.skinType as any obj.type = skin.skinType as any;
if (skin.skinType == Enums.SkinType.CUSTOM) obj.skin = skin.skin if (skin.skinType == Enums.SkinType.CUSTOM) obj.skin = skin.skin;
else obj.builtInSkin = skin.skinId else obj.builtInSkin = skin.skinId;
player.skin = obj player.skin = obj;
player.write(new SCReadyPacket()) player.write(new SCReadyPacket());
this.players.delete(`!phs.${player.uuid}`) this.players.delete(`!phs.${player.uuid}`);
this.players.set(player.username, player) this.players.set(player.username, player);
player.initListeners() player.initListeners();
this._bindListenersToPlayer(player) this._bindListenersToPlayer(player);
player.state = Enums.ClientState.POST_HANDSHAKE player.state = Enums.ClientState.POST_HANDSHAKE;
this._logger.info(`Handshake Success! Connecting player ${player.username} to server...`) this._logger.info(
handled = true `Handshake Success! Connecting player ${player.username} to server...`
);
handled = true;
await player.connect({ await player.connect({
host: this.config.server.host, host: this.config.server.host,
port: this.config.server.port, port: this.config.server.port,
username: player.username username: player.username,
}) });
this._logger.info(`Player ${player.username} successfully connected to server.`) this._logger.info(
this.emit('playerConnect', player) `Player ${player.username} successfully connected to server.`
);
this.emit("playerConnect", player);
} }
} catch (err) { } catch (err) {
this.initalHandlerLogger.warn(`Error occurred whilst handling handshake: ${err.stack ?? err}`) this.initalHandlerLogger.warn(
handled = true `Error occurred whilst handling handshake: ${err.stack ?? err}`
ws.close() );
handled = true;
ws.close();
if (player && player.uuid && this.players.has(`!phs.${player.uuid}`)) if (player && player.uuid && this.players.has(`!phs.${player.uuid}`))
this.players.delete(`!phs.${player.uuid}`) this.players.delete(`!phs.${player.uuid}`);
if (player && player.uuid && this.players.has(player.username)) if (player && player.uuid && this.players.has(player.username))
this.players.delete(player.username) this.players.delete(player.username);
} }
} }
private _bindListenersToPlayer(player: Player) { private _bindListenersToPlayer(player: Player) {
let sentDisconnectMsg = false let sentDisconnectMsg = false;
player.on('disconnect', () => { player.on("disconnect", () => {
if (this.players.has(player.username)) if (this.players.has(player.username))
this.players.delete(player.username) this.players.delete(player.username);
this.initalHandlerLogger.info(`DISCONNECT ${player.username} <=> DISCONNECTED`) this.initalHandlerLogger.info(
if (!sentDisconnectMsg) this._logger.info(`Player ${player.username} (${player.uuid}) disconnected from the proxy server.`) `DISCONNECT ${player.username} <=> DISCONNECTED`
}) );
player.on('proxyPacket', async packet => { 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) { if (packet.packetId == Enums.PacketId.CSChannelMessagePacket) {
try { try {
const msg: CSChannelMessagePacket = packet as any const msg: CSChannelMessagePacket = packet as any;
if (msg.channel == Constants.EAGLERCRAFT_SKIN_CHANNEL_NAME) { if (msg.channel == Constants.EAGLERCRAFT_SKIN_CHANNEL_NAME) {
await this.skinServer.handleRequest(msg, player) await this.skinServer.handleRequest(msg, player);
} }
} catch (err) { } 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 => { 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}`) });
}) player.on("joinServer", (client) => {
this.initalHandlerLogger.info(
`SERVER_CONNECTED ${player.username} <=> ${client.socket.remoteAddress}:${client.socket.remotePort}`
);
});
} }
static readonly POLL_INTERVAL: number = 10000 static readonly POLL_INTERVAL: number = 10000;
private _pollServer(host: string, port: number, interval?: number) { private _pollServer(host: string, port: number, interval?: number) {
;(async () => { (async () => {
while (true) { while (true) {
const motd = await Motd.MOTD.generateMOTDFromPing(host, port) const motd = await Motd.MOTD.generateMOTDFromPing(host, port).catch(
.catch(err => { (err) => {
this._logger.warn(`Error polling ${host}:${port} for MOTD: ${err.stack ?? err}`) this._logger.warn(
}) `Error polling ${host}:${port} for MOTD: ${err.stack ?? err}`
if (motd) this.broadcastMotd = motd );
await new Promise(res => setTimeout(res, interval ?? Proxy.POLL_INTERVAL))
} }
})() );
if (motd) this.broadcastMotd = motd;
await new Promise((res) =>
setTimeout(res, interval ?? Proxy.POLL_INTERVAL)
);
}
})();
} }
private async _handleWSConnectionReq(req: http.IncomingMessage, socket: Duplex, head: Buffer) { private async _handleWSConnectionReq(
const origin = req.headers.origin == null || req.headers.origin == 'null' ? null : req.headers.origin 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) { if (!this.config.origins.allowOfflineDownloads && origin == null) {
socket.destroy() socket.destroy();
return return;
} }
if (this.config.origins.originBlacklist != null && this.config.origins.originBlacklist.some(host => Util.areDomainsEqual(host, origin))) { if (
socket.destroy() this.config.origins.originBlacklist != null &&
return 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 (
socket.destroy() this.config.origins.originWhitelist != null &&
return !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)) } try {
catch (err) { await this.wsServer.handleUpgrade(req, socket, head, (ws) =>
this._logger.error(`Error was caught whilst trying to handle WebSocket connection request! Error: ${err.stack ?? err}`) this._handleWSConnection(ws)
socket.destroy() );
} catch (err) {
this._logger.error(
`Error was caught whilst trying to handle WebSocket connection request! Error: ${
err.stack ?? err
}`
);
socket.destroy();
} }
} }
public fetchUserByUUID(uuid: MineProtocol.UUID): Player | null { public fetchUserByUUID(uuid: MineProtocol.UUID): Player | null {
for (const [username, player] of this.players) { for (const [username, player] of this.players) {
if (player.uuid == uuid) if (player.uuid == uuid) return player;
return player
} }
return null return null;
} }
} }
interface ProxyEvents { interface ProxyEvents {
'playerConnect': (player: Player) => void, playerConnect: (player: Player) => void;
'playerDisconnect': (player: Player) => void playerDisconnect: (player: Player) => void;
} }
export declare interface Proxy { export declare interface Proxy {
on<U extends keyof ProxyEvents>( on<U extends keyof ProxyEvents>(event: U, listener: ProxyEvents[U]): this;
event: U, listener: ProxyEvents[U]
): this;
emit<U extends keyof ProxyEvents>( emit<U extends keyof ProxyEvents>(
event: U, ...args: Parameters<ProxyEvents[U]> event: U,
...args: Parameters<ProxyEvents[U]>
): boolean; ): boolean;
} }

View File

@ -1,138 +1,184 @@
import { createHash } from "crypto"; import { createHash } from "crypto";
import { import { encodeULEB128, decodeULEB128 } from "@thi.ng/leb128";
encodeULEB128,
decodeULEB128,
} from "@thi.ng/leb128"
import { Chat } from "./Chat.js"; import { Chat } from "./Chat.js";
import { WebSocket } from "ws"; import { WebSocket } from "ws";
import { Enums } from "./Enums.js"; import { Enums } from "./Enums.js";
import { Player } from "./Player.js"; import { Player } from "./Player.js";
import * as http from "http" import * as http from "http";
import { Config } from "../launcher_types.js"; import { Config } from "../launcher_types.js";
import { parseDomain, ParseResultType } from "parse-domain"; import { parseDomain, ParseResultType } from "parse-domain";
import { access, readdir } from "fs/promises"; import { access, readdir } from "fs/promises";
import { resolve } from "path"; import { resolve } from "path";
export namespace Util { export namespace Util {
export const encodeVarInt: typeof encodeULEB128 = encodeULEB128 export const encodeVarInt: typeof encodeULEB128 = encodeULEB128;
export const decodeVarInt: typeof decodeULEB128 = decodeULEB128 export const decodeVarInt: typeof decodeULEB128 = decodeULEB128;
// annotation for range // annotation for range
// b = beginning, e = end // b = beginning, e = end
export type Range<B, E> = number export type Range<B, E> = number;
export type BoundedBuffer<S extends number> = Buffer export type BoundedBuffer<S extends number> = Buffer;
const USERNAME_REGEX = /[^0-9^a-z^A-Z^_]/gi const USERNAME_REGEX = /[^0-9^a-z^A-Z^_]/gi;
export function generateUUIDFromPlayer(user: string): string { export function generateUUIDFromPlayer(user: string): string {
const str = `OfflinePlayer:${user}` const str = `OfflinePlayer:${user}`;
let md5Bytes = createHash('md5').update(str).digest() let md5Bytes = createHash("md5").update(str).digest();
md5Bytes[6] &= 0x0f; /* clear version */ md5Bytes[6] &= 0x0f; /* clear version */
md5Bytes[6] |= 0x30; /* set to version 3 */ md5Bytes[6] |= 0x30; /* set to version 3 */
md5Bytes[8] &= 0x3f; /* clear variant */ md5Bytes[8] &= 0x3f; /* clear variant */
md5Bytes[8] |= 0x80; /* set to IETF variant */ md5Bytes[8] |= 0x80; /* set to IETF variant */
return uuidBufferToString(md5Bytes) return uuidBufferToString(md5Bytes);
} }
// excerpt from uuid-buffer // excerpt from uuid-buffer
export function uuidStringToBuffer(uuid: string): Buffer { export function uuidStringToBuffer(uuid: string): Buffer {
if (!uuid) return Buffer.alloc(16); // Return empty buffer if (!uuid) return Buffer.alloc(16); // Return empty buffer
const hexStr = uuid.replace(/-/g, ''); const hexStr = uuid.replace(/-/g, "");
if (uuid.length != 36 || hexStr.length != 32) throw new Error(`Invalid UUID string: ${uuid}`); if (uuid.length != 36 || hexStr.length != 32)
return Buffer.from(hexStr, 'hex'); throw new Error(`Invalid UUID string: ${uuid}`);
return Buffer.from(hexStr, "hex");
} }
export function uuidBufferToString(buffer: Buffer): string { 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 if (buffer.equals(Buffer.alloc(16))) return null; // If buffer is all zeros, return null
const str = buffer.toString('hex'); const str = buffer.toString("hex");
return `${str.slice(0, 8)}-${str.slice(8, 12)}-${str.slice(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) => { return new Promise<Buffer>((res, rej) => {
let resolved = false let resolved = false;
const msgCb = (msg: any) => { const msgCb = (msg: any) => {
if (filter != null && filter(msg)) { if (filter != null && filter(msg)) {
resolved = true resolved = true;
ws.removeListener('message', msgCb) ws.removeListener("message", msgCb);
ws.removeListener('close', discon) ws.removeListener("close", discon);
ws.setMaxListeners(ws.getMaxListeners() - 2 < 0 ? 5 : ws.getMaxListeners() - 2) ws.setMaxListeners(
res(msg) ws.getMaxListeners() - 2 < 0 ? 5 : ws.getMaxListeners() - 2
);
res(msg);
} else if (filter == null) { } else if (filter == null) {
resolved = true resolved = true;
ws.removeListener('message', msgCb) ws.removeListener("message", msgCb);
ws.removeListener('close', discon) ws.removeListener("close", discon);
ws.setMaxListeners(ws.getMaxListeners() - 2 < 0 ? 5 : ws.getMaxListeners() - 2) ws.setMaxListeners(
res(msg) ws.getMaxListeners() - 2 < 0 ? 5 : ws.getMaxListeners() - 2
} );
res(msg);
} }
};
const discon = () => { const discon = () => {
resolved = true resolved = true;
ws.removeListener('message', msgCb) ws.removeListener("message", msgCb);
ws.removeListener('close', discon) ws.removeListener("close", discon);
ws.setMaxListeners(ws.getMaxListeners() - 2 < 0 ? 5 : ws.getMaxListeners() - 2) ws.setMaxListeners(
rej("Connection closed") ws.getMaxListeners() - 2 < 0 ? 5 : ws.getMaxListeners() - 2
} );
ws.setMaxListeners(ws.getMaxListeners() + 2) rej("Connection closed");
ws.on('message', msgCb) };
ws.on('close', discon) ws.setMaxListeners(ws.getMaxListeners() + 2);
ws.on("message", msgCb);
ws.on("close", discon);
setTimeout(() => { setTimeout(() => {
ws.removeListener('message', msgCb) ws.removeListener("message", msgCb);
ws.removeListener('close', discon) ws.removeListener("close", discon);
ws.setMaxListeners(ws.getMaxListeners() - 2 < 0 ? 5 : ws.getMaxListeners() - 2) ws.setMaxListeners(
rej("Timed out") ws.getMaxListeners() - 2 < 0 ? 5 : ws.getMaxListeners() - 2
}, 10000) );
}) rej("Timed out");
}, 10000);
});
} }
export function validateUsername(user: string): void | never { export function validateUsername(user: string): void | never {
if (user.length > 20) if (user.length > 20) throw new Error("Username is too long!");
throw new Error("Username is too long!") if (user.length < 3) throw new Error("Username is too short!");
if (user.length < 3)
throw new Error("Username is too short!")
if (!!user.match(USERNAME_REGEX)) if (!!user.match(USERNAME_REGEX))
throw new Error("Invalid username. Username can only contain alphanumeric characters, and the underscore (_) character.") throw new Error(
"Invalid username. Username can only contain alphanumeric characters, and the underscore (_) character."
);
} }
export function areDomainsEqual(d1: string, d2: string): boolean { export function areDomainsEqual(d1: string, d2: string): boolean {
if (d1.endsWith("*.")) d1 = d1.replace("*.", "WILDCARD-LOL-EXTRA-LONG-SUBDOMAIN-TO-LOWER-CHANCES-OF-COLLISION.") if (d1.endsWith("*."))
const parseResult1 = parseDomain(d1), parseResult2 = parseDomain(d2) d1 = d1.replace(
if (parseResult1.type != ParseResultType.Invalid && parseResult2.type != ParseResultType.Invalid) { "*.",
if (parseResult1.type == ParseResultType.Ip && parseResult2.type == ParseResultType.Ip) { "WILDCARD-LOL-EXTRA-LONG-SUBDOMAIN-TO-LOWER-CHANCES-OF-COLLISION."
return parseResult1.hostname == parseResult2.hostname ? true : false );
} else if (parseResult1.type == ParseResultType.Listed && parseResult2.type == ParseResultType.Listed) { const parseResult1 = parseDomain(d1),
if (parseResult1.subDomains[0] == "WILDCARD-LOL-EXTRA-LONG-SUBDOMAIN-TO-LOWER-CHANCES-OF-COLLISION") { parseResult2 = parseDomain(d2);
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"
) {
// wildcard // wildcard
const domainPlusTld1 = parseResult1.domain + ("." + parseResult1.topLevelDomains.join(".")) const domainPlusTld1 =
const domainPlusTld2 = parseResult2.domain + ("." + parseResult2.topLevelDomains.join(".")) parseResult1.domain +
return domainPlusTld1 == domainPlusTld2 ? true : false ("." + parseResult1.topLevelDomains.join("."));
const domainPlusTld2 =
parseResult2.domain +
("." + parseResult2.topLevelDomains.join("."));
return domainPlusTld1 == domainPlusTld2 ? true : false;
} else { } else {
// no wildcard // no wildcard
return d1 == d2 ? true : false return d1 == d2 ? true : false;
} }
} else if (parseResult1.type == ParseResultType.NotListed && parseResult2.type == ParseResultType.NotListed) { } else if (
if (parseResult1.labels[0] == "WILDCARD-LOL-EXTRA-LONG-SUBDOMAIN-TO-LOWER-CHANCES-OF-COLLISION") { parseResult1.type == ParseResultType.NotListed &&
parseResult2.type == ParseResultType.NotListed
) {
if (
parseResult1.labels[0] ==
"WILDCARD-LOL-EXTRA-LONG-SUBDOMAIN-TO-LOWER-CHANCES-OF-COLLISION"
) {
// wildcard // wildcard
const domainPlusTld1 = parseResult1.labels.slice(2).join('.') const domainPlusTld1 = parseResult1.labels.slice(2).join(".");
const domainPlusTld2 = parseResult1.labels.slice(2).join('.') const domainPlusTld2 = parseResult1.labels.slice(2).join(".");
return domainPlusTld1 == domainPlusTld2 ? true : false return domainPlusTld1 == domainPlusTld2 ? true : false;
} else { } else {
// no wildcard // no wildcard
return d1 == d2 ? true : false return d1 == d2 ? true : false;
} }
} else if (parseResult1.type == ParseResultType.Reserved && parseResult2.type == ParseResultType.Reserved) { } else if (
if (parseResult1.hostname == "" && parseResult1.hostname === parseResult2.hostname) parseResult1.type == ParseResultType.Reserved &&
return true parseResult2.type == ParseResultType.Reserved
) {
if (
parseResult1.hostname == "" &&
parseResult1.hostname === parseResult2.hostname
)
return true;
else { else {
// uncertain, fallback to exact hostname matching // uncertain, fallback to exact hostname matching
return d1 == d2 ? true : false return d1 == d2 ? true : false;
} }
} }
} else { } else {
return false return false;
} }
} }
@ -149,50 +195,53 @@ export namespace Util {
} }
export async function recursiveFileSearch(dir: string): Promise<string[]> { export async function recursiveFileSearch(dir: string): Promise<string[]> {
const ents = [] const ents = [];
for await (const f of _getFiles(dir)) { for await (const f of _getFiles(dir)) {
ents.push(f) ents.push(f);
} }
return ents return ents;
} }
export async function fsExists(path: string): Promise<boolean> { export async function fsExists(path: string): Promise<boolean> {
try { await access(path) } try {
catch (err) { await access(path);
if (err.code == 'ENOENT') } catch (err) {
return false if (err.code == "ENOENT") return false;
else return true else return true;
} }
return true return true;
} }
export type PlayerPosition = { export type PlayerPosition = {
x: number, x: number;
y: number, y: number;
z: number, z: number;
yaw: number, yaw: number;
pitch: number pitch: number;
} };
export type PositionPacket = { export type PositionPacket = {
x: number, x: number;
y: number, y: number;
z: number, z: number;
yaw: number, yaw: number;
pitch: number, pitch: number;
flags: number flags: number;
} };
export function generatePositionPacket(currentPos: PlayerPosition, newPos: PositionPacket): PositionPacket { export function generatePositionPacket(
const DEFAULT_RELATIVITY = 0x01 // relative to X-axis currentPos: PlayerPosition,
newPos: PositionPacket
): PositionPacket {
const DEFAULT_RELATIVITY = 0x01; // relative to X-axis
const newPosPacket = { const newPosPacket = {
x: newPos.x - (currentPos.x * 2), x: newPos.x - currentPos.x * 2,
y: newPos.y, y: newPos.y,
z: newPos.z, z: newPos.z,
yaw: newPos.yaw, yaw: newPos.yaw,
pitch: newPos.pitch, pitch: newPos.pitch,
flags: DEFAULT_RELATIVITY flags: DEFAULT_RELATIVITY,
} };
return newPosPacket return newPosPacket;
} }
} }

View File

@ -4,46 +4,50 @@ import Packet from "../Packet.js";
import { MineProtocol } from "../Protocol.js"; import { MineProtocol } from "../Protocol.js";
export default class CSLoginPacket implements Packet { export default class CSLoginPacket implements Packet {
packetId: Enums.PacketId = Enums.PacketId.CSLoginPacket packetId: Enums.PacketId = Enums.PacketId.CSLoginPacket;
type: "packet" = "packet" type: "packet" = "packet";
boundTo = Enums.PacketBounds.S boundTo = Enums.PacketBounds.S;
sentAfterHandshake = false sentAfterHandshake = false;
networkVersion = NETWORK_VERSION networkVersion = NETWORK_VERSION;
gameVersion = VANILLA_PROTOCOL_VERSION gameVersion = VANILLA_PROTOCOL_VERSION;
brand: string brand: string;
version: string version: string;
username: string username: string;
private _getMagicSeq(): Buffer { private _getMagicSeq(): Buffer {
return Buffer.concat([ return Buffer.concat(
[
[0x02, 0x00, 0x02, 0x00, 0x02, 0x00], [0x02, 0x00, 0x02, 0x00, 0x02, 0x00],
[this.networkVersion], [this.networkVersion],
[0x00, 0x01, 0x00], [0x00, 0x01, 0x00],
[this.gameVersion] [this.gameVersion],
].map(arr => Buffer.from(arr))) ].map((arr) => Buffer.from(arr))
);
} }
public serialize() { public serialize() {
return Buffer.concat( return Buffer.concat(
[[Enums.PacketId.CSLoginPacket], [
[Enums.PacketId.CSLoginPacket],
this._getMagicSeq(), this._getMagicSeq(),
MineProtocol.writeString(this.brand), MineProtocol.writeString(this.brand),
MineProtocol.writeString(this.version), MineProtocol.writeString(this.version),
[0x00], [0x00],
MineProtocol.writeString(this.username)] MineProtocol.writeString(this.username),
.map(arr => arr instanceof Uint8Array ? arr : Buffer.from(arr)) ].map((arr) => (arr instanceof Uint8Array ? arr : Buffer.from(arr)))
) );
} }
public deserialize(packet: Buffer) { public deserialize(packet: Buffer) {
if (packet[0] != this.packetId) throw TypeError("Invalid packet ID detected!") if (packet[0] != this.packetId)
packet = packet.subarray(1 + this._getMagicSeq().length) throw TypeError("Invalid packet ID detected!");
packet = packet.subarray(1 + this._getMagicSeq().length);
const brand = MineProtocol.readString(packet), const brand = MineProtocol.readString(packet),
version = MineProtocol.readString(brand.newBuffer), version = MineProtocol.readString(brand.newBuffer),
username = MineProtocol.readString(version.newBuffer, 1) username = MineProtocol.readString(version.newBuffer, 1);
this.brand = brand.value this.brand = brand.value;
this.version = version.value this.version = version.value;
this.username = username.value this.username = username.value;
return this return this;
} }
} }

View File

@ -2,16 +2,16 @@ import { Enums } from "../Enums.js";
import Packet from "../Packet.js"; import Packet from "../Packet.js";
export class CSReadyPacket implements Packet { export class CSReadyPacket implements Packet {
packetId: Enums.PacketId = Enums.PacketId.CSReadyPacket packetId: Enums.PacketId = Enums.PacketId.CSReadyPacket;
type: "packet" = "packet" type: "packet" = "packet";
boundTo = Enums.PacketBounds.S boundTo = Enums.PacketBounds.S;
sentAfterHandshake = false sentAfterHandshake = false;
public serialize() { public serialize() {
return Buffer.from([this.packetId]) return Buffer.from([this.packetId]);
} }
public deserialize(packet: Buffer) { public deserialize(packet: Buffer) {
return this return this;
} }
} }

View File

@ -4,16 +4,16 @@ import Packet from "../Packet.js";
import { MineProtocol } from "../Protocol.js"; import { MineProtocol } from "../Protocol.js";
export class CSSetSkinPacket implements Packet { export class CSSetSkinPacket implements Packet {
packetId: Enums.PacketId = Enums.PacketId.CSSetSkinPacket packetId: Enums.PacketId = Enums.PacketId.CSSetSkinPacket;
type: "packet" = "packet" type: "packet" = "packet";
boundTo: Enums.PacketBounds = Enums.PacketBounds.S boundTo: Enums.PacketBounds = Enums.PacketBounds.S;
sentAfterHandshake: boolean = false sentAfterHandshake: boolean = false;
version: string | 'skin_v1' = 'skin_v1' version: string | "skin_v1" = "skin_v1";
skinType: Omit<Enums.SkinType, 'NOT_LOADED'> skinType: Omit<Enums.SkinType, "NOT_LOADED">;
skinDimensions?: number skinDimensions?: number;
skin?: Buffer skin?: Buffer;
skinId?: number skinId?: number;
public serialize() { public serialize() {
if (this.skinType == Enums.SkinType.BUILTIN) { if (this.skinType == Enums.SkinType.BUILTIN) {
@ -21,40 +21,50 @@ export class CSSetSkinPacket implements Packet {
Buffer.from([this.packetId]), Buffer.from([this.packetId]),
MineProtocol.writeString(this.version), MineProtocol.writeString(this.version),
MineProtocol.writeVarInt(this.skinDimensions), MineProtocol.writeVarInt(this.skinDimensions),
this.skin this.skin,
]) ]);
} else { } else {
return Buffer.concat([ return Buffer.concat(
[
[this.packetId], [this.packetId],
MineProtocol.writeString(this.version), MineProtocol.writeString(this.version),
Constants.MAGIC_ENDING_CLIENT_UPLOAD_SKIN_BUILTIN, Constants.MAGIC_ENDING_CLIENT_UPLOAD_SKIN_BUILTIN,
[this.skinId] [this.skinId],
].map(arr => arr instanceof Uint8Array ? arr : Buffer.from(arr))) ].map((arr) => (arr instanceof Uint8Array ? arr : Buffer.from(arr)))
);
} }
} }
public deserialize(packet: Buffer) { public deserialize(packet: Buffer) {
packet = packet.subarray(1) packet = packet.subarray(1);
const version = MineProtocol.readString(packet) const version = MineProtocol.readString(packet);
let skinType: Enums.SkinType let skinType: Enums.SkinType;
if (!Constants.MAGIC_ENDING_CLIENT_UPLOAD_SKIN_BUILTIN.some((byte, index) => byte !== version.newBuffer[index])) { if (
!Constants.MAGIC_ENDING_CLIENT_UPLOAD_SKIN_BUILTIN.some(
(byte, index) => byte !== version.newBuffer[index]
)
) {
// built in // built in
skinType = Enums.SkinType.BUILTIN skinType = Enums.SkinType.BUILTIN;
const id = MineProtocol.readVarInt(version.newBuffer.subarray(Constants.MAGIC_ENDING_CLIENT_UPLOAD_SKIN_BUILTIN.length)) const id = MineProtocol.readVarInt(
this.version = version.value version.newBuffer.subarray(
this.skinType = skinType Constants.MAGIC_ENDING_CLIENT_UPLOAD_SKIN_BUILTIN.length
this.skinId = id.value )
return this );
this.version = version.value;
this.skinType = skinType;
this.skinId = id.value;
return this;
} else { } else {
// custom // custom
skinType = Enums.SkinType.CUSTOM skinType = Enums.SkinType.CUSTOM;
const dimensions = MineProtocol.readVarInt(version.newBuffer), const dimensions = MineProtocol.readVarInt(version.newBuffer),
skin = dimensions.newBuffer.subarray(3).subarray(0, 16384) skin = dimensions.newBuffer.subarray(3).subarray(0, 16384);
this.version = version.value this.version = version.value;
this.skinType = skinType this.skinType = skinType;
this.skinDimensions = dimensions.value this.skinDimensions = dimensions.value;
this.skin = skin this.skin = skin;
return this return this;
} }
} }
} }

View File

@ -3,27 +3,29 @@ import Packet from "../Packet.js";
import { MineProtocol } from "../Protocol.js"; import { MineProtocol } from "../Protocol.js";
export class CSUsernamePacket implements Packet { export class CSUsernamePacket implements Packet {
packetId: Enums.PacketId = Enums.PacketId.CSUsernamePacket packetId: Enums.PacketId = Enums.PacketId.CSUsernamePacket;
type: "packet" = "packet" type: "packet" = "packet";
boundTo = Enums.PacketBounds.S boundTo = Enums.PacketBounds.S;
sentAfterHandshake = false sentAfterHandshake = false;
username: string username: string;
static readonly DEFAULT = "default" static readonly DEFAULT = "default";
public serialize() { public serialize() {
return Buffer.concat([ return Buffer.concat(
[
[this.packetId], [this.packetId],
MineProtocol.writeString(this.username), MineProtocol.writeString(this.username),
MineProtocol.writeString(CSUsernamePacket.DEFAULT), MineProtocol.writeString(CSUsernamePacket.DEFAULT),
[0x0] [0x0],
].map(arr => arr instanceof Uint8Array ? arr : Buffer.from(arr))) ].map((arr) => (arr instanceof Uint8Array ? arr : Buffer.from(arr)))
);
} }
public deserialize(packet: Buffer) { public deserialize(packet: Buffer) {
packet = packet.subarray(1) packet = packet.subarray(1);
const username = MineProtocol.readString(packet) const username = MineProtocol.readString(packet);
this.username = username.value this.username = username.value;
return this return this;
} }
} }

View File

@ -4,27 +4,34 @@ import Packet from "../Packet.js";
import { MineProtocol } from "../Protocol.js"; import { MineProtocol } from "../Protocol.js";
export default class SCDisconnectPacket implements Packet { export default class SCDisconnectPacket implements Packet {
packetId: Enums.PacketId = Enums.PacketId.SCDisconnectPacket packetId: Enums.PacketId = Enums.PacketId.SCDisconnectPacket;
type: "packet" = "packet" type: "packet" = "packet";
boundTo = Enums.PacketBounds.C boundTo = Enums.PacketBounds.C;
sentAfterHandshake = false sentAfterHandshake = false;
static readonly REASON = 0x8 static readonly REASON = 0x8;
reason: string | Chat.Chat reason: string | Chat.Chat;
public serialize() { public serialize() {
const msg = (typeof this.reason == 'string' ? this.reason : Chat.chatToPlainString(this.reason)) const msg =
return Buffer.concat([ typeof this.reason == "string"
? this.reason
: Chat.chatToPlainString(this.reason);
return Buffer.concat(
[
[0xff], [0xff],
MineProtocol.writeVarInt(SCDisconnectPacket.REASON), MineProtocol.writeVarInt(SCDisconnectPacket.REASON),
MineProtocol.writeString(" " + msg + " ") MineProtocol.writeString(" " + msg + " "),
].map(arr => arr instanceof Uint8Array ? arr : Buffer.from(arr))) ].map((arr) => (arr instanceof Uint8Array ? arr : Buffer.from(arr)))
);
} }
public deserialize(packet: Buffer) { public deserialize(packet: Buffer) {
if (packet[0] != this.packetId) throw new Error("Invalid packet ID!") if (packet[0] != this.packetId) throw new Error("Invalid packet ID!");
packet = packet.subarray(1 + MineProtocol.writeVarInt(SCDisconnectPacket.REASON).length) packet = packet.subarray(
const reason = MineProtocol.readString(packet) 1 + MineProtocol.writeVarInt(SCDisconnectPacket.REASON).length
this.reason = reason.value );
return this const reason = MineProtocol.readString(packet);
this.reason = reason.value;
return this;
} }
} }

View File

@ -1,40 +1,48 @@
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 { Enums } from "../Enums.js"; import { Enums } from "../Enums.js";
import Packet from "../Packet.js"; import Packet from "../Packet.js";
import { MineProtocol } from "../Protocol.js"; import { MineProtocol } from "../Protocol.js";
export default class SCIdentifyPacket implements Packet { export default class SCIdentifyPacket implements Packet {
packetId: Enums.PacketId = Enums.PacketId.SCIdentifyPacket packetId: Enums.PacketId = Enums.PacketId.SCIdentifyPacket;
type: "packet" = "packet" type: "packet" = "packet";
boundTo = Enums.PacketBounds.C boundTo = Enums.PacketBounds.C;
sentAfterHandshake = false sentAfterHandshake = false;
protocolVer = NETWORK_VERSION protocolVer = NETWORK_VERSION;
gameVersion = VANILLA_PROTOCOL_VERSION gameVersion = VANILLA_PROTOCOL_VERSION;
branding = PROXY_BRANDING branding = PROXY_BRANDING;
version = PROXY_VERSION version = PROXY_VERSION;
public serialize() { public serialize() {
return Buffer.concat([ return Buffer.concat(
[
[0x02], [0x02],
MineProtocol.writeShort(this.protocolVer), MineProtocol.writeShort(this.protocolVer),
MineProtocol.writeShort(this.gameVersion), MineProtocol.writeShort(this.gameVersion),
MineProtocol.writeString(this.branding), MineProtocol.writeString(this.branding),
MineProtocol.writeString(this.version), MineProtocol.writeString(this.version),
[0x00, 0x00, 0x00] [0x00, 0x00, 0x00],
].map(arr => arr instanceof Uint8Array ? arr : Buffer.from(arr))) ].map((arr) => (arr instanceof Uint8Array ? arr : Buffer.from(arr)))
);
} }
public deserialize(packet: Buffer) { public deserialize(packet: Buffer) {
if (packet[0] != this.packetId) throw TypeError("Invalid packet ID detected!") if (packet[0] != this.packetId)
packet = packet.subarray(1) throw TypeError("Invalid packet ID detected!");
packet = packet.subarray(1);
const protoVer = MineProtocol.readShort(packet), const protoVer = MineProtocol.readShort(packet),
gameVer = MineProtocol.readShort(protoVer.newBuffer), gameVer = MineProtocol.readShort(protoVer.newBuffer),
branding = MineProtocol.readString(gameVer.newBuffer), branding = MineProtocol.readString(gameVer.newBuffer),
version = MineProtocol.readString(branding.newBuffer) version = MineProtocol.readString(branding.newBuffer);
this.gameVersion = gameVer.value this.gameVersion = gameVer.value;
this.branding = branding.value this.branding = branding.value;
this.version = version.value this.version = version.value;
return this return this;
} }
} }

View File

@ -2,16 +2,16 @@ import { Enums } from "../Enums.js";
import Packet from "../Packet.js"; import Packet from "../Packet.js";
export class SCReadyPacket implements Packet { export class SCReadyPacket implements Packet {
packetId: Enums.PacketId = Enums.PacketId.SCReadyPacket packetId: Enums.PacketId = Enums.PacketId.SCReadyPacket;
type: "packet" = "packet" type: "packet" = "packet";
boundTo = Enums.PacketBounds.C boundTo = Enums.PacketBounds.C;
sentAfterHandshake = false sentAfterHandshake = false;
public serialize() { public serialize() {
return Buffer.from([this.packetId]) return Buffer.from([this.packetId]);
} }
public deserialize(packet: Buffer) { public deserialize(packet: Buffer) {
return this return this;
} }
} }

View File

@ -4,28 +4,30 @@ import { MineProtocol } from "../Protocol.js";
import { Util } from "../Util.js"; import { Util } from "../Util.js";
export class SCSyncUuidPacket implements Packet { export class SCSyncUuidPacket implements Packet {
packetId: Enums.PacketId = Enums.PacketId.SCSyncUuidPacket packetId: Enums.PacketId = Enums.PacketId.SCSyncUuidPacket;
type: "packet" = "packet" type: "packet" = "packet";
boundTo = Enums.PacketBounds.C boundTo = Enums.PacketBounds.C;
sentAfterHandshake = false sentAfterHandshake = false;
username: string username: string;
uuid: string uuid: string;
public serialize() { public serialize() {
return Buffer.concat([ return Buffer.concat(
[
[this.packetId], [this.packetId],
MineProtocol.writeString(this.username), MineProtocol.writeString(this.username),
Util.uuidStringToBuffer(this.uuid) Util.uuidStringToBuffer(this.uuid),
].map(arr => arr instanceof Uint8Array ? arr : Buffer.from(arr))) ].map((arr) => (arr instanceof Uint8Array ? arr : Buffer.from(arr)))
);
} }
public deserialize(packet: Buffer) { public deserialize(packet: Buffer) {
packet = packet.subarray(1) packet = packet.subarray(1);
const username = MineProtocol.readString(packet), const username = MineProtocol.readString(packet),
uuid = username.newBuffer.subarray(0, 15) uuid = username.newBuffer.subarray(0, 15);
this.username = username.value this.username = username.value;
this.uuid = Util.uuidBufferToString(uuid) this.uuid = Util.uuidBufferToString(uuid);
return this return this;
} }
} }

View File

@ -3,29 +3,30 @@ import Packet from "../../Packet.js";
import { MineProtocol } from "../../Protocol.js"; import { MineProtocol } from "../../Protocol.js";
export class CSChannelMessagePacket implements Packet { export class CSChannelMessagePacket implements Packet {
packetId: Enums.PacketId = Enums.PacketId.CSChannelMessagePacket packetId: Enums.PacketId = Enums.PacketId.CSChannelMessagePacket;
type: "packet" = "packet" type: "packet" = "packet";
boundTo = Enums.PacketBounds.S boundTo = Enums.PacketBounds.S;
sentAfterHandshake = true sentAfterHandshake = true;
readonly messageType: Enums.ChannelMessageType = Enums.ChannelMessageType.CLIENT readonly messageType: Enums.ChannelMessageType =
channel: string Enums.ChannelMessageType.CLIENT;
data: Buffer channel: string;
data: Buffer;
public serialize() { public serialize() {
return Buffer.concat([ return Buffer.concat(
[this.packetId], [[this.packetId], MineProtocol.writeString(this.channel), this.data].map(
MineProtocol.writeString(this.channel), (arr) => (arr instanceof Uint8Array ? arr : Buffer.from(arr))
this.data )
].map(arr => arr instanceof Uint8Array ? arr : Buffer.from(arr))) );
} }
public deserialize(packet: Buffer) { public deserialize(packet: Buffer) {
packet = packet.subarray(1) packet = packet.subarray(1);
const channel = MineProtocol.readString(packet), const channel = MineProtocol.readString(packet),
data = channel.newBuffer data = channel.newBuffer;
this.channel = channel.value this.channel = channel.value;
this.data = data this.data = data;
return this return this;
} }
} }

View File

@ -3,29 +3,30 @@ import Packet from "../../Packet.js";
import { MineProtocol } from "../../Protocol.js"; import { MineProtocol } from "../../Protocol.js";
export class SCChannelMessagePacket implements Packet { export class SCChannelMessagePacket implements Packet {
packetId: Enums.PacketId = Enums.PacketId.SCChannelMessagePacket packetId: Enums.PacketId = Enums.PacketId.SCChannelMessagePacket;
type: "packet" = "packet" type: "packet" = "packet";
boundTo = Enums.PacketBounds.C boundTo = Enums.PacketBounds.C;
sentAfterHandshake = true sentAfterHandshake = true;
readonly messageType: Enums.ChannelMessageType = Enums.ChannelMessageType.SERVER readonly messageType: Enums.ChannelMessageType =
channel: string Enums.ChannelMessageType.SERVER;
data: Buffer channel: string;
data: Buffer;
public serialize() { public serialize() {
return Buffer.concat([ return Buffer.concat(
[this.packetId], [[this.packetId], MineProtocol.writeString(this.channel), this.data].map(
MineProtocol.writeString(this.channel), (arr) => (arr instanceof Uint8Array ? arr : Buffer.from(arr))
this.data )
].map(arr => arr instanceof Uint8Array ? arr : Buffer.from(arr))) );
} }
public deserialize(packet: Buffer) { public deserialize(packet: Buffer) {
packet = packet.subarray(1) packet = packet.subarray(1);
const channel = MineProtocol.readString(packet), const channel = MineProtocol.readString(packet),
data = channel.newBuffer data = channel.newBuffer;
this.channel = channel.value this.channel = channel.value;
this.data = data this.data = data;
return this return this;
} }
} }

View File

@ -3,13 +3,13 @@ export namespace PluginLoaderTypes {
* ## SemVer * ## SemVer
* Abstract typing to define a semantic version string. Refer to https://semver.org/ for more details. * Abstract typing to define a semantic version string. Refer to https://semver.org/ for more details.
*/ */
export type SemVer = string export type SemVer = string;
/** /**
* ## SemVerReq * ## SemVerReq
* Abstract typing to define a semantic version requirement. Refer to https://semver.org/ for more details. * Abstract typing to define a semantic version requirement. Refer to https://semver.org/ for more details.
*/ */
export type SemVerReq = string export type SemVerReq = string;
/** /**
* ## PluginMetadata * ## PluginMetadata
@ -28,26 +28,26 @@ export namespace PluginLoaderTypes {
* @property {string[]} load_after - Defines what plugin(s) to be loaded first before this plugin is loaded. * @property {string[]} load_after - Defines what plugin(s) to be loaded first before this plugin is loaded.
*/ */
export type PluginMetadata = { export type PluginMetadata = {
name: string, name: string;
id: string, id: string;
version: SemVer, version: SemVer;
entry_point: string, entry_point: string;
requirements: PluginRequirement[], requirements: PluginRequirement[];
incompatibilities: PluginRequirement[], incompatibilities: PluginRequirement[];
load_after: string[] load_after: string[];
} };
/** /**
* ## PluginMetadataPathed * ## PluginMetadataPathed
* Internal typing. Provides a path to the plugin metadata file. * Internal typing. Provides a path to the plugin metadata file.
*/ */
export type PluginMetadataPathed = PluginMetadata & { path: string } export type PluginMetadataPathed = PluginMetadata & { path: string };
/** /**
* ## PluginLoadOrder * ## PluginLoadOrder
* Internal typing. Provides a loading order for plugin loading. * Internal typing. Provides a loading order for plugin loading.
*/ */
export type PluginLoadOrder = string[] export type PluginLoadOrder = string[];
/** /**
* ## PluginRequirement * ## PluginRequirement
@ -62,7 +62,7 @@ export namespace PluginLoaderTypes {
* @property {PluginLoaderTypes.SemVerReq} version - The SemVer requirement for the requirement. * @property {PluginLoaderTypes.SemVerReq} version - The SemVer requirement for the requirement.
*/ */
export type PluginRequirement = { export type PluginRequirement = {
id: string, id: string;
version: SemVerReq | 'any' version: SemVerReq | "any";
} };
} }

View File

@ -1,7 +1,7 @@
import { Stats } from "fs"; import { Stats } from "fs";
import * as fs from "fs/promises"; import * as fs from "fs/promises";
import * as pathUtil from "path" import * as pathUtil from "path";
import * as semver from "semver" import * as semver from "semver";
import { EventEmitter } from "events"; import { EventEmitter } from "events";
import { pathToFileURL } from "url"; import { pathToFileURL } from "url";
import { Logger } from "../../logger.js"; import { Logger } from "../../logger.js";
@ -10,7 +10,7 @@ import { Proxy } from "../Proxy.js";
import { Util } from "../Util.js"; import { Util } from "../Util.js";
import { PluginLoaderTypes } from "./PluginLoaderTypes.js"; import { PluginLoaderTypes } from "./PluginLoaderTypes.js";
import { Enums } from "../Enums.js"; import { Enums } from "../Enums.js";
import { Chat } from "../Chat.js" import { Chat } from "../Chat.js";
import { Constants } from "../Constants.js"; import { Constants } from "../Constants.js";
import { Motd } from "../Motd.js"; import { Motd } from "../Motd.js";
import { Player } from "../Player.js"; import { Player } from "../Player.js";
@ -19,248 +19,401 @@ import { EaglerSkins } from "../skins/EaglerSkins.js";
import { BungeeUtil } from "../BungeeUtil.js"; import { BungeeUtil } from "../BungeeUtil.js";
export class PluginManager extends EventEmitter { export class PluginManager extends EventEmitter {
public plugins: Map<string, { exports: any, metadata: PluginLoaderTypes.PluginMetadataPathed }> public plugins: Map<
public proxy: Proxy string,
{ exports: any; metadata: PluginLoaderTypes.PluginMetadataPathed }
>;
public proxy: Proxy;
public Logger: typeof Logger = Logger public Logger: typeof Logger = Logger;
public Enums: typeof Enums = Enums public Enums: typeof Enums = Enums;
public Chat: typeof Chat = Chat public Chat: typeof Chat = Chat;
public Constants: typeof Constants = Constants public Constants: typeof Constants = Constants;
public Motd: typeof Motd = Motd public Motd: typeof Motd = Motd;
public Player: typeof Player = Player public Player: typeof Player = Player;
public MineProtocol: typeof MineProtocol = MineProtocol public MineProtocol: typeof MineProtocol = MineProtocol;
public EaglerSkins: typeof EaglerSkins = EaglerSkins public EaglerSkins: typeof EaglerSkins = EaglerSkins;
public Util: typeof Util = Util public Util: typeof Util = Util;
public BungeeUtil: typeof BungeeUtil = BungeeUtil public BungeeUtil: typeof BungeeUtil = BungeeUtil;
private _loadDir: string private _loadDir: string;
private _logger: Logger private _logger: Logger;
constructor(loadDir: string) { constructor(loadDir: string) {
super() super();
this.setMaxListeners(0) this.setMaxListeners(0);
this._loadDir = loadDir this._loadDir = loadDir;
this.plugins = new Map() this.plugins = new Map();
this.Logger = Logger this.Logger = Logger;
this._logger = new this.Logger('PluginManager') this._logger = new this.Logger("PluginManager");
} }
public async loadPlugins() { public async loadPlugins() {
this._logger.info("Loading plugin metadata files...") this._logger.info("Loading plugin metadata files...");
const pluginMeta = await this._findPlugins(this._loadDir) const pluginMeta = await this._findPlugins(this._loadDir);
await this._validatePluginList(pluginMeta) await this._validatePluginList(pluginMeta);
let pluginsString = '' let pluginsString = "";
for (const [id, plugin] of pluginMeta) { for (const [id, plugin] of pluginMeta) {
pluginsString += `${id}@${plugin.version}` pluginsString += `${id}@${plugin.version}`;
} }
pluginsString = pluginsString.substring(0, pluginsString.length - 1) pluginsString = pluginsString.substring(0, pluginsString.length - 1);
this._logger.info(`Found ${pluginMeta.size} plugin(s): ${pluginsString}`) this._logger.info(`Found ${pluginMeta.size} plugin(s): ${pluginsString}`);
this._logger.info(`Loading ${pluginMeta.size} plugin(s)...`) this._logger.info(`Loading ${pluginMeta.size} plugin(s)...`);
const successLoadCount = await this._loadPlugins(pluginMeta, this._getLoadOrder(pluginMeta)) const successLoadCount = await this._loadPlugins(
this._logger.info(`Successfully loaded ${successLoadCount} plugin(s).`) pluginMeta,
this.emit('pluginsFinishLoading', this) 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(
const ret: Map<string, PluginLoaderTypes.PluginMetadataPathed> = new Map() dir: string
const lsRes = await Promise.all((await fs.readdir(dir)) ): Promise<Map<string, PluginLoaderTypes.PluginMetadataPathed>> {
.filter(ent => !ent.endsWith(".disabled")) const ret: Map<string, PluginLoaderTypes.PluginMetadataPathed> = new Map();
.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) { for (const [path, details] of lsRes) {
if (details.isFile()) { if (details.isFile()) {
if (path.endsWith('.jar')) { if (path.endsWith(".jar")) {
this._logger.warn(`Non-EaglerProxy plugin found! (${path})`) this._logger.warn(`Non-EaglerProxy plugin found! (${path})`);
this._logger.warn(`BungeeCord plugins are NOT supported! Only custom EaglerProxy plugins are allowed.`) this._logger.warn(
} else if (path.endsWith('.zip')) { `BungeeCord plugins are NOT supported! Only custom EaglerProxy plugins are allowed.`
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 if (path.endsWith(".zip")) {
} else this._logger.debug(`Skipping file found in plugin folder: ${path}`) 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}`);
} else { } else {
const metadataPath = pathUtil.resolve(pathUtil.join(path, 'metadata.json')) const metadataPath = pathUtil.resolve(
let metadata: PluginLoaderTypes.PluginMetadata pathUtil.join(path, "metadata.json")
);
let metadata: PluginLoaderTypes.PluginMetadata;
try { try {
const file = await fs.readFile(metadataPath) const file = await fs.readFile(metadataPath);
metadata = JSON.parse(file.toString()) metadata = JSON.parse(file.toString());
// do some type checking // do some type checking
if (typeof metadata.name != 'string') if (typeof metadata.name != "string")
throw new TypeError("<metadata>.name is either null or not of a string type!") throw new TypeError(
if (typeof metadata.id != 'string') "<metadata>.name is either null or not of a string type!"
throw new TypeError("<metadata>.id is either null or not of a string type!") );
if ((/ /gm).test(metadata.id)) if (typeof metadata.id != "string")
throw new Error(`<metadata>.id contains whitespace!`) 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)) if (!semver.valid(metadata.version))
throw new Error("<metadata>.version is either null, not a string, or is not a valid SemVer!") throw new Error(
if (typeof metadata.entry_point != 'string') "<metadata>.version is either null, not a string, or is not a valid SemVer!"
throw new TypeError("<metadata>.entry_point is either null or not a string!") );
if (!metadata.entry_point.endsWith('.js')) if (typeof metadata.entry_point != "string")
throw new Error(`<metadata>.entry_point (${metadata.entry_point}) references a non-JavaScript file!`) throw new TypeError(
if (!await Util.fsExists(pathUtil.resolve(path, metadata.entry_point))) "<metadata>.entry_point is either null or not a string!"
throw new Error(`<metadata>.entry_point (${metadata.entry_point}) references a non-existent file!`) );
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) if (metadata.requirements instanceof Array == false)
throw new TypeError("<metadata>.requirements is either null or not an array!") throw new TypeError(
"<metadata>.requirements is either null or not an array!"
);
for (const requirement of metadata.requirements as PluginLoaderTypes.PluginMetadata["requirements"]) { for (const requirement of metadata.requirements as PluginLoaderTypes.PluginMetadata["requirements"]) {
if (typeof requirement != 'object' || requirement == null) if (typeof requirement != "object" || requirement == null)
throw new TypeError(`<metadata>.requirements[${(metadata.requirements as any).indexOf(requirement)}] is either null or not an object!`) throw new TypeError(
if (typeof requirement.id != 'string') `<metadata>.requirements[${(
throw new TypeError(`<metadata>.requirements[${(metadata.requirements as any).indexOf(requirement)}].id is either null or not a string!`) 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)) if (/ /gm.test(requirement.id))
throw new TypeError(`<metadata>.requirements[${(metadata.requirements as any).indexOf(requirement)}].id contains whitespace!`) throw new TypeError(
if (semver.validRange(requirement.version) == null && requirement.version != 'any') `<metadata>.requirements[${(
throw new TypeError(`<metadata>.requirements[${(metadata.requirements as any).indexOf(requirement)}].version is either null or not a valid SemVer!`) metadata.requirements as any
).indexOf(requirement)}].id contains whitespace!`
);
if (
semver.validRange(requirement.version) == null &&
requirement.version != "any"
)
throw new TypeError(
`<metadata>.requirements[${(
metadata.requirements as any
).indexOf(
requirement
)}].version is either null or not a valid SemVer!`
);
} }
if (metadata.load_after instanceof Array == false) if (metadata.load_after instanceof Array == false)
throw new TypeError("<metadata>.load_after is either null or not an array!") throw new TypeError(
"<metadata>.load_after is either null or not an array!"
);
for (const loadReq of metadata.load_after as string[]) { for (const loadReq of metadata.load_after as string[]) {
if (typeof loadReq != 'string') if (typeof loadReq != "string")
throw new TypeError(`<metadata>.load_after[${(metadata.load_after as any).indexOf(loadReq)}] is either null, or not a valid ID!`) throw new TypeError(
`<metadata>.load_after[${(metadata.load_after as any).indexOf(
loadReq
)}] is either null, or not a valid ID!`
);
if (/ /gm.test(loadReq)) if (/ /gm.test(loadReq))
throw new TypeError(`<metadata>.load_after[${(metadata.load_after as any).indexOf(loadReq)}] contains whitespace!`) throw new TypeError(
`<metadata>.load_after[${(metadata.load_after as any).indexOf(
loadReq
)}] contains whitespace!`
);
} }
if (metadata.incompatibilities instanceof Array == false) if (metadata.incompatibilities instanceof Array == false)
throw new TypeError("<metadata>.incompatibilities is either null or not an array!") throw new TypeError(
"<metadata>.incompatibilities is either null or not an array!"
);
for (const incompatibility of metadata.incompatibilities as PluginLoaderTypes.PluginMetadata["requirements"]) { for (const incompatibility of metadata.incompatibilities as PluginLoaderTypes.PluginMetadata["requirements"]) {
if (typeof incompatibility != 'object' || incompatibility == null) if (typeof incompatibility != "object" || incompatibility == null)
throw new TypeError(`<metadata>.incompatibilities[${(metadata.load_after as any).indexOf(incompatibility)}] is either null or not an object!`) throw new TypeError(
if (typeof incompatibility.id != 'string') `<metadata>.incompatibilities[${(
throw new TypeError(`<metadata>.incompatibilities[${(metadata.load_after as any).indexOf(incompatibility)}].id is either null or not a string!`) 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)) if (/ /gm.test(incompatibility.id))
throw new TypeError(`<metadata>.incompatibilities[${(metadata.load_after as any).indexOf(incompatibility)}].id contains whitespace!`) throw new TypeError(
`<metadata>.incompatibilities[${(
metadata.load_after as any
).indexOf(incompatibility)}].id contains whitespace!`
);
if (semver.validRange(incompatibility.version) == null) 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!`) throw new TypeError(
`<metadata>.incompatibilities[${(
metadata.load_after as any
).indexOf(
incompatibility
)}].version is either null or not a valid SemVer!`
);
} }
if (ret.has(metadata.id)) if (ret.has(metadata.id))
throw new Error(`Duplicate plugin ID detected: ${metadata.id}. Are there duplicate plugins in the plugin folder?`) throw new Error(
`Duplicate plugin ID detected: ${metadata.id}. Are there duplicate plugins in the plugin folder?`
);
ret.set(metadata.id, { ret.set(metadata.id, {
path: pathUtil.resolve(path), path: pathUtil.resolve(path),
...metadata ...metadata,
}) });
} catch (err) { } catch (err) {
this._logger.warn(`Failed to load plugin metadata file at ${metadataPath}: ${err.stack ?? err}`) this._logger.warn(
this._logger.warn("This plugin will skip loading due to an error.") `Failed to load plugin metadata file at ${metadataPath}: ${
err.stack ?? err
}`
);
this._logger.warn("This plugin will skip loading due to an error.");
} }
} }
} }
return ret 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 [id, plugin] of plugins) {
for (const req of plugin.requirements) { for (const req of plugin.requirements) {
if (!plugins.has(req.id) && req.id != 'eaglerproxy' && !req.id.startsWith("module:")) { if (
this._logger.fatal(`Error whilst loading plugins: Plugin ${plugin.name}@${plugin.version} requires plugin ${req.id}@${req.version}, but it is not found!`) !plugins.has(req.id) &&
this._logger.fatal("Loading has halted due to missing dependencies.") req.id != "eaglerproxy" &&
process.exit(1) !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 (req.id == "eaglerproxy") {
if (!semver.satisfies(PROXY_VERSION, req.version) && req.version != 'any') { if (
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!`) !semver.satisfies(PROXY_VERSION, req.version) &&
this._logger.fatal("Loading has halted due to dependency issues.") req.version != "any"
process.exit(1) ) {
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!`
);
this._logger.fatal("Loading has halted due to dependency issues.");
process.exit(1);
} }
} else if (req.id.startsWith("module:")) { } else if (req.id.startsWith("module:")) {
const moduleName = req.id.replace("module:", "") const moduleName = req.id.replace("module:", "");
try { await import(moduleName) } try {
catch (err) { await import(moduleName);
if (err.code == 'ERR_MODULE_NOT_FOUND') { } catch (err) {
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!`) if (err.code == "ERR_MODULE_NOT_FOUND") {
this._logger.fatal(`Please install this missing package by running "npm install ${moduleName}${req.version == 'any' ? "" : `@${req.version}`}". If you're using yarn, run "yarn add ${moduleName}${req.version == 'any' ? "" : `@${req.version}`}" instead.`) this._logger.fatal(
this._logger.fatal("Loading has halted due to dependency issues.") `Plugin ${plugin.name}@${
process.exit(1) plugin.version
} requires NPM module ${moduleName}${
req.version == "any" ? "" : `@${req.version}`
} to be installed, but it is not found!`
);
this._logger.fatal(
`Please install this missing package by running "npm install ${moduleName}${
req.version == "any" ? "" : `@${req.version}`
}". If you're using yarn, run "yarn add ${moduleName}${
req.version == "any" ? "" : `@${req.version}`
}" instead.`
);
this._logger.fatal(
"Loading has halted due to dependency issues."
);
process.exit(1);
} }
} }
} else { } else {
let dep = plugins.get(req.id) let dep = plugins.get(req.id);
if (!semver.satisfies(dep.version, req.version) && req.version != 'any') { if (
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!`) !semver.satisfies(dep.version, req.version) &&
this._logger.fatal("Loading has halted due to dependency issues.") req.version != "any"
process.exit(1) ) {
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!`
);
this._logger.fatal("Loading has halted due to dependency issues.");
process.exit(1);
} }
} }
} }
plugin.incompatibilities.forEach(incomp => { plugin.incompatibilities.forEach((incomp) => {
const plugin_incomp = plugins.get(incomp.id) const plugin_incomp = plugins.get(incomp.id);
if (plugin_incomp) { if (plugin_incomp) {
if (semver.satisfies(plugin_incomp.version, incomp.version)) { if (semver.satisfies(plugin_incomp.version, incomp.version)) {
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(
this._logger.fatal("Loading has halted due to plugin incompatibility issues.") `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}!`
process.exit(1) );
this._logger.fatal(
"Loading has halted due to plugin incompatibility issues."
);
process.exit(1);
} }
} else if (incomp.id == 'eaglerproxy') { } else if (incomp.id == "eaglerproxy") {
if (semver.satisfies(PROXY_VERSION, incomp.version)) { if (semver.satisfies(PROXY_VERSION, incomp.version)) {
this._logger.fatal(`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(
this._logger.fatal("Loading has halted due to plugin incompatibility issues.") `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}!`
process.exit(1) );
this._logger.fatal(
"Loading has halted due to plugin incompatibility issues."
);
process.exit(1);
} }
} }
}) });
} }
} }
private _getLoadOrder(plugins: Map<string, PluginLoaderTypes.PluginMetadataPathed>): PluginLoaderTypes.PluginLoadOrder { private _getLoadOrder(
let order = [], lastPlugin: any plugins: Map<string, PluginLoaderTypes.PluginMetadataPathed>
plugins.forEach(v => order.push(v.id)) ): PluginLoaderTypes.PluginLoadOrder {
let order = [],
lastPlugin: any;
plugins.forEach((v) => order.push(v.id));
for (const [id, plugin] of plugins) { for (const [id, plugin] of plugins) {
const load = plugin.load_after.filter(dep => plugins.has(dep)) const load = plugin.load_after.filter((dep) => plugins.has(dep));
if (load.length < 0) { if (load.length < 0) {
order.push(plugin.id) order.push(plugin.id);
} else { } else {
let mostLastIndexFittingDeps = -1 let mostLastIndexFittingDeps = -1;
for (const loadEnt of load) { for (const loadEnt of load) {
if (loadEnt != lastPlugin) { if (loadEnt != lastPlugin) {
if (order.indexOf(loadEnt) + 1 > mostLastIndexFittingDeps) { if (order.indexOf(loadEnt) + 1 > mostLastIndexFittingDeps) {
mostLastIndexFittingDeps = order.indexOf(loadEnt) + 1 mostLastIndexFittingDeps = order.indexOf(loadEnt) + 1;
} }
} }
} }
if (mostLastIndexFittingDeps != -1) { if (mostLastIndexFittingDeps != -1) {
order.splice(order.indexOf(plugin.id), 1) order.splice(order.indexOf(plugin.id), 1);
order.splice(mostLastIndexFittingDeps - 1, 0, plugin.id) order.splice(mostLastIndexFittingDeps - 1, 0, plugin.id);
lastPlugin = plugin lastPlugin = plugin;
} }
} }
} }
return order return order;
} }
private async _loadPlugins(plugins: Map<string, PluginLoaderTypes.PluginMetadataPathed>, order: PluginLoaderTypes.PluginLoadOrder): Promise<number> { private async _loadPlugins(
let successCount = 0 plugins: Map<string, PluginLoaderTypes.PluginMetadataPathed>,
order: PluginLoaderTypes.PluginLoadOrder
): Promise<number> {
let successCount = 0;
for (const id of order) { for (const id of order) {
let pluginMeta = plugins.get(id) let pluginMeta = plugins.get(id);
try { try {
const imp = await import(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, { this.plugins.set(pluginMeta.id, {
exports: imp, exports: imp,
metadata: pluginMeta metadata: pluginMeta,
}) });
successCount++ successCount++;
this.emit('pluginLoad', pluginMeta.id, imp) this.emit("pluginLoad", pluginMeta.id, imp);
} catch (err) { } catch (err) {
this._logger.warn(`Failed to load plugin entry point for plugin (${pluginMeta.name}) at ${pluginMeta.path}: ${err.stack ?? err}`) this._logger.warn(
this._logger.warn("This plugin will skip loading due to an error.") `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 return successCount;
} }
} }
} }
interface PluginManagerEvents { interface PluginManagerEvents {
'pluginLoad': (name: string, plugin: any) => void, pluginLoad: (name: string, plugin: any) => void;
'pluginsFinishLoading': (manager: PluginManager) => void, pluginsFinishLoading: (manager: PluginManager) => void;
'proxyFinishLoading': (proxy: Proxy, manager: PluginManager) => void proxyFinishLoading: (proxy: Proxy, manager: PluginManager) => void;
} }
export declare interface PluginManager { export declare interface PluginManager {
on<U extends keyof PluginManagerEvents>( on<U extends keyof PluginManagerEvents>(
event: U, listener: PluginManagerEvents[U] event: U,
listener: PluginManagerEvents[U]
): this; ): this;
emit<U extends keyof PluginManagerEvents>( emit<U extends keyof PluginManagerEvents>(
event: U, ...args: Parameters<PluginManagerEvents[U]> event: U,
...args: Parameters<PluginManagerEvents[U]>
): boolean; ): boolean;
once<U extends keyof PluginManagerEvents>( once<U extends keyof PluginManagerEvents>(
event: U, listener: PluginManagerEvents[U] event: U,
listener: PluginManagerEvents[U]
): this; ): this;
} }

View File

@ -1,65 +1,75 @@
export default class SimpleRatelimit<T> { export default class SimpleRatelimit<T> {
readonly requestCount: number readonly requestCount: number;
readonly resetInterval: number readonly resetInterval: number;
private entries: Map<T, Ratelimit> private entries: Map<T, Ratelimit>;
constructor(requestCount: number, resetInterval: number) { constructor(requestCount: number, resetInterval: number) {
this.requestCount = requestCount this.requestCount = requestCount;
this.resetInterval = resetInterval this.resetInterval = resetInterval;
this.entries = new Map() this.entries = new Map();
} }
public get(key: T): Ratelimit { public get(key: T): Ratelimit {
return this.entries.get(key) ?? { return (
this.entries.get(key) ?? {
remainingRequests: this.requestCount, remainingRequests: this.requestCount,
resetTime: new Date(0) resetTime: new Date(0),
} }
);
} }
public consume(key: T, count?: number): Ratelimit | never { public consume(key: T, count?: number): Ratelimit | never {
if (this.entries.has(key)) { if (this.entries.has(key)) {
const ratelimit = this.entries.get(key) const ratelimit = this.entries.get(key);
if (ratelimit.remainingRequests - (count ?? 1) < 0) { if (ratelimit.remainingRequests - (count ?? 1) < 0) {
if (this.requestCount - (count ?? 1) < 0) { if (this.requestCount - (count ?? 1) < 0) {
throw new RatelimitExceededError(`Consume request count is higher than default available request count!`) throw new RatelimitExceededError(
`Consume request count is higher than default available request count!`
);
} else { } else {
throw new RatelimitExceededError(`Ratelimit exceeded, try again in ${ratelimit.resetTime.getDate() - Date.now()} ms!`) throw new RatelimitExceededError(
`Ratelimit exceeded, try again in ${
ratelimit.resetTime.getDate() - Date.now()
} ms!`
);
} }
} }
ratelimit.remainingRequests -= count ?? 1 ratelimit.remainingRequests -= count ?? 1;
return ratelimit return ratelimit;
} else { } else {
if (this.requestCount - (count ?? 1) < 0) { if (this.requestCount - (count ?? 1) < 0) {
throw new RatelimitExceededError(`Consume request count is higher than default available request count!`) throw new RatelimitExceededError(
`Consume request count is higher than default available request count!`
);
} }
const ratelimit: Ratelimit = { const ratelimit: Ratelimit = {
remainingRequests: this.requestCount - (count ?? 1), remainingRequests: this.requestCount - (count ?? 1),
resetTime: new Date(Date.now() + this.resetInterval), resetTime: new Date(Date.now() + this.resetInterval),
timer: null timer: null,
} };
this.entries.set(key, ratelimit) this.entries.set(key, ratelimit);
ratelimit.timer = this._onAdd(ratelimit) ratelimit.timer = this._onAdd(ratelimit);
return ratelimit return ratelimit;
} }
} }
private _onAdd(ratelimit: Ratelimit): NodeJS.Timer { private _onAdd(ratelimit: Ratelimit): NodeJS.Timer {
return setInterval(() => { return setInterval(() => {
// TODO: work on // TODO: work on
}, this.resetInterval) }, this.resetInterval);
} }
} }
export type Ratelimit = { export type Ratelimit = {
remainingRequests: number, remainingRequests: number;
resetTime: Date, resetTime: Date;
timer?: NodeJS.Timer timer?: NodeJS.Timer;
} };
export class RatelimitExceededError extends Error { export class RatelimitExceededError extends Error {
constructor(message: { toString: () => string }) { constructor(message: { toString: () => string }) {
super(message.toString()) super(message.toString());
this.name = "RatelimitExceededError" this.name = "RatelimitExceededError";
Object.setPrototypeOf(this, RatelimitExceededError.prototype) Object.setPrototypeOf(this, RatelimitExceededError.prototype);
} }
} }

View File

@ -8,11 +8,6 @@
"outDir": "build", "outDir": "build",
"resolveJsonModule": true "resolveJsonModule": true
}, },
"include": [ "include": ["src/**/*.json", "src/**/*"],
"src/**/*.json", "hooks": ["copy-files"]
"src/**/*"
],
"hooks": [
"copy-files"
]
} }