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")
logger.info("Hi!")
const logger = new PLUGIN_MANAGER.Logger("ExamplePlugin");
logger.info("Hi!");

View File

@ -1,6 +1,9 @@
const path = require("path")
const os = require("os")
const path = require("path");
const os = require("os");
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
const fs = require("fs/promises")
const path = require("path")
const crypto = require("crypto")
const {
sourceDir
} = require("./config.js")
const fs = require("fs/promises");
const path = require("path");
const crypto = require("crypto");
const { sourceDir } = require("./config.js");
class Logger {
constructor({ name, logDebug }) {
this.name = name
this.debug = logDebug
this.name = name;
this.debug = logDebug;
}
_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) {
this._log("info", data, "log")
this._log("info", data, "log");
}
warn(data) {
this._log("warn", data, "error")
this._log("warn", data, "error");
}
error(data) {
this._log("error", data, "error")
this._log("error", data, "error");
}
debug(data) {
if (this.debug) {
this._log("debug", data, "error")
this._log("debug", data, "error");
}
}
}
async function recursiveFileSearch(dir) {
const fileList = []
const fileList = [];
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()) {
fileList.push(pathDir)
fileList.push(pathDir);
} else if (file.isDirectory()) {
fileList.push(...(await recursiveFileSearch(pathDir)))
fileList.push(...(await recursiveFileSearch(pathDir)));
} 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({
name: "launcher",
logDebug: process.env.DEBUG == "true"
}),
LINE_SEPERATOR = "-----------------------------------"
logDebug: process.env.DEBUG == "true",
}),
LINE_SEPERATOR = "-----------------------------------";
if (!process.env.REPL_SLUG) {
logger.error(LINE_SEPERATOR)
logger.error("Repl not detected!")
logger.error("")
logger.error("This file is meant to be ran in a Repl")
logger.error(LINE_SEPERATOR)
logger.error(LINE_SEPERATOR);
logger.error("Repl not detected!");
logger.error("");
logger.error("This file is meant to be ran in a Repl");
logger.error(LINE_SEPERATOR);
}
logger.info(LINE_SEPERATOR)
logger.info("Checking if the proxy needs to be recompiled...")
logger.info(LINE_SEPERATOR)
logger.info(LINE_SEPERATOR);
logger.info("Checking if the proxy needs to be recompiled...");
logger.info(LINE_SEPERATOR);
fs.readFile(path.join(__dirname, ".sourcehash"))
.then(data => {
let oldHash = data.toString()
logger.info("Found old hash, calculating hash of source files...")
.then((data) => {
let oldHash = data.toString();
logger.info("Found old hash, calculating hash of source files...");
recursiveFileSearch(sourceDir)
.then(files => {
Promise.all(files.map(f => fs.readFile(f)))
.then(data => {
const hash = crypto.createHash("sha256")
data.forEach(d => hash.update(d))
let sourceHash = hash.digest().toString()
.then((files) => {
Promise.all(files.map((f) => fs.readFile(f))).then((data) => {
const hash = crypto.createHash("sha256");
data.forEach((d) => hash.update(d));
let sourceHash = hash.digest().toString();
if (sourceHash === oldHash) {
logger.info("Source hasn't been changed, skipping compilation...")
process.exit(0)
logger.info("Source hasn't been changed, skipping compilation...");
process.exit(0);
} else {
logger.info("Source has been changed, recompiling...")
logger.info("Source has been changed, recompiling...");
fs.writeFile(path.join(__dirname, ".sourcehash"), sourceHash)
.then(() => {
process.exit(2)
})
.catch(err => {
logger.error(`Could not write new hash to disk!\n${err.stack}`)
process.exit(1)
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 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)
})
})
.catch(err => {
.catch((err) => {
if (err.code == "ENOENT") {
logger.warn("Previous source hash not found! Assuming a clean install is being used.")
logger.info("Calculating hash...")
logger.warn(
"Previous source hash not found! Assuming a clean install is being used."
);
logger.info("Calculating hash...");
recursiveFileSearch(sourceDir)
.then(files => {
Promise.all(files.map(f => fs.readFile(f)))
.then(data => {
const hash = crypto.createHash("sha256")
data.forEach(d => hash.update(d))
let sourceHash = hash.digest().toString()
.then((files) => {
Promise.all(files.map((f) => fs.readFile(f))).then((data) => {
const hash = crypto.createHash("sha256");
data.forEach((d) => hash.update(d));
let sourceHash = hash.digest().toString();
fs.writeFile(path.join(__dirname, ".sourcehash"), sourceHash)
.then(() => {
logger.info("Saved hash to disk.")
process.exit(2)
logger.info("Saved hash to disk.");
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 calculate file hashes for files in directory ${sourceDir}!\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);
});
} 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}`)
process.exit(1)
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}`
);
process.exit(1);
}
})
});

View File

@ -6,7 +6,7 @@ import { Config } from "./launcher_types.js";
export const config: Config = {
bridge: {
enabled: false,
motd: null
motd: null,
},
adapter: {
name: "EaglerProxy",
@ -14,20 +14,22 @@ export const config: Config = {
bindPort: 8080,
maxConcurrentClients: 20,
skinUrlWhitelist: undefined,
motd: true ? "FORWARD" : {
motd: true
? "FORWARD"
: {
iconURL: "logo.png",
l1: "yes",
l2: "no"
l2: "no",
},
origins: {
allowOfflineDownloads: true,
originWhitelist: null,
originBlacklist: null
originBlacklist: null,
},
server: {
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";
declare global {
var CONFIG: Config
var PROXY: Proxy
var PLUGIN_MANAGER: PluginManager
var PACKET_REGISTRY: Map<number, Packet & {
class: any
}>
var CONFIG: Config;
var PROXY: Proxy;
var PLUGIN_MANAGER: PluginManager;
var PACKET_REGISTRY: Map<
number,
Packet & {
class: any;
}
>;
}

View File

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

View File

@ -1,40 +1,44 @@
export type Config = {
bridge: BridgeOptions,
adapter: AdapterOptions
}
bridge: BridgeOptions;
adapter: AdapterOptions;
};
export type BridgeOptions = {
enabled: boolean,
motd: 'FORWARD' | {
iconURL?: string,
l1: string,
l2?: string
}
}
enabled: boolean;
motd:
| "FORWARD"
| {
iconURL?: string;
l1: string;
l2?: string;
};
};
export type AdapterOptions = {
name: "EaglerProxy",
bindHost: string,
bindPort: number,
maxConcurrentClients: 20,
skinUrlWhitelist?: string[],
name: "EaglerProxy";
bindHost: string;
bindPort: number;
maxConcurrentClients: 20;
skinUrlWhitelist?: string[];
origins: {
allowOfflineDownloads: boolean,
originWhitelist: string[],
originBlacklist: string[]
},
motd: 'FORWARD' | {
iconURL?: string,
l1: string,
l2?: string
},
allowOfflineDownloads: boolean;
originWhitelist: string[];
originBlacklist: string[];
};
motd:
| "FORWARD"
| {
iconURL?: string;
l1: string;
l2?: string;
};
server: {
host: string,
port: number
},
host: string;
port: number;
};
tls?: {
enabled: boolean,
key: null,
cert: null
}
}
enabled: boolean;
key: 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: JsonLogType,
message: string
}
type: JsonLogType;
message: string;
};
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 {
return JSON.stringify({
return (
JSON.stringify({
type: type,
message: message
message: message,
}) + "\n"
);
}
export class Logger {
loggerName: string
verbose: boolean
private jsonLog: boolean = process.argv.includes("--json") || process.argv.includes("-j")
loggerName: string;
verbose: boolean;
private jsonLog: boolean =
process.argv.includes("--json") || process.argv.includes("-j");
constructor(name: string, verbose?: boolean) {
this.loggerName = name
if (verbose) this.verbose = verbose
else this.verbose = global_verbose
this.loggerName = name;
if (verbose) this.verbose = verbose;
else this.verbose = global_verbose;
}
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`)
else process.stdout.write(jsonLog("info", s))
if (!this.jsonLog)
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) {
if (!this.jsonLog) 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))
if (!this.jsonLog)
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) {
if (!this.jsonLog) 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))
if (!this.jsonLog)
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) {
if (!this.jsonLog) 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))
if (!this.jsonLog)
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) {
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`)
else process.stderr.write(jsonLog("debug", s))
if (!this.jsonLog)
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
export const BRIDGE_VERSION: Readonly<number> = f(1)
export const BRIDGE_VERSION: Readonly<number> = f(1);
// adapter meta
export const PROXY_BRANDING: Readonly<string> = f("EaglerProxy")
export const PROXY_VERSION: Readonly<string> = f("1.0.7")
export const PROXY_BRANDING: Readonly<string> = f("EaglerProxy");
export const PROXY_VERSION: Readonly<string> = f("1.0.7");
export const NETWORK_VERSION: Readonly<number> = f(0x03)
export const VANILLA_PROTOCOL_VERSION: Readonly<number> = f(47)
export const NETWORK_VERSION: Readonly<number> = f(0x03);
export const VANILLA_PROTOCOL_VERSION: Readonly<number> = f(47);

View File

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

View File

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

View File

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

View File

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

View File

@ -7,6 +7,8 @@ import { Client } from "minecraft-protocol";
import { ClientState, ConnectionState } from "./types.js";
import { auth, ServerDeviceCodeResponse } from "./auth.js";
import { config } from "./config.js";
import { handleCommand } from "./commands.js";
import { getTokenProfileEasyMc } from "./auth_easymc.js";
const { Vec3 } = vec3 as any;
const Enums = PLUGIN_MANAGER.Enums;
@ -50,7 +52,7 @@ export function handleConnect(client: ClientState) {
client.gameClient.write("login", {
entityId: 1,
gameMode: 2,
dimension: 0,
dimension: 1,
difficulty: 1,
maxPlayers: 1,
levelType: "flat",
@ -190,7 +192,7 @@ export function sendMessageLogin(client: Client, url: string, token: string) {
export function updateState(
client: Client,
newState: "CONNECTION_TYPE" | "AUTH" | "SERVER",
newState: "CONNECTION_TYPE" | "AUTH_EASYMC" | "AUTH" | "SERVER",
uri?: string,
code?: string
) {
@ -201,7 +203,17 @@ export function updateState(
text: ` ${Enums.ChatColor.GOLD}EaglerProxy Authentication Server `,
}),
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;
@ -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) {
try {
client.state = ConnectionState.AUTH;
@ -267,7 +291,7 @@ export async function onConnect(client: ClientState) {
},
clickEvent: {
action: "run_command",
value: "1",
value: "$1",
},
});
sendChatComponent(client.gameClient, {
@ -285,12 +309,30 @@ export async function onConnect(client: ClientState) {
},
clickEvent: {
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(
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"
);
updateState(client.gameClient, "CONNECTION_TYPE");
@ -298,21 +340,28 @@ export async function onConnect(client: ClientState) {
let chosenOption: ConnectType | null = null;
while (true) {
const option = await awaitCommand(client.gameClient, (msg) => true);
switch (option) {
switch (option.replace(/\$/gim, "")) {
default:
sendCustomMessage(
client.gameClient,
`I don't understand what you meant by "${option}", please reply with a valid option!`,
"red"
);
break;
case "1":
chosenOption = ConnectType.ONLINE;
break;
case "2":
chosenOption = ConnectType.OFFLINE;
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) {
@ -347,6 +396,7 @@ export async function onConnect(client: ClientState) {
authHandler.on("code", codeCallback);
await new Promise((res) =>
authHandler.once("done", (result) => {
console.log(result);
savedAuth = result;
res(result);
})
@ -407,9 +457,64 @@ export async function onConnect(client: ClientState) {
}
}
try {
await PLUGIN_MANAGER.proxy.players
.get(client.gameClient.username)
.switchServers({
sendChatComponent(client.gameClient, {
text: `Joining server under ${savedAuth.selectedProfile.name}/your Minecraft account's username! Run `,
color: "aqua",
extra: [
{
text: "/eag-help",
color: "gold",
hoverEvent: {
action: "show_text",
value: Enums.ChatColor.GOLD + "Click me to run this command!",
},
clickEvent: {
action: "run_command",
value: "/eag-help",
},
},
{
text: " for a list of proxy commands.",
color: "aqua",
},
],
});
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,
port: port,
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 {
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.lastStatusUpdate = Date.now();
updateState(client.gameClient, "SERVER");
@ -493,22 +760,62 @@ export async function onConnect(client: ClientState) {
}
}
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(
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"
);
await PLUGIN_MANAGER.proxy.players
.get(client.gameClient.username)
.switchServers({
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 = {
...appendOptions,
isEasyMC: true,
};
await player.switchServers({
host: host,
port: port,
version: "1.8.8",
username: client.gameClient.username,
auth: "offline",
keepAlive: false,
skipValidation: true,
hideErrors: true,
...appendOptions,
});
} catch (err) {
if (!client.gameClient.ended) {
@ -546,15 +853,15 @@ export function generateSpawnChunk(): Chunk.PCChunk {
() =>
new McBlock(
REGISTRY.blocksByName.air.id,
REGISTRY.biomesByName.plains.id,
REGISTRY.biomesByName.the_end.id,
0
)
);
chunk.setBlock(
new Vec3(8, 64, 8),
new McBlock(
REGISTRY.blocksByName.barrier.id,
REGISTRY.biomesByName.plains.id,
REGISTRY.blocksByName.sea_lantern.id,
REGISTRY.biomesByName.the_end.id,
0
)
);
@ -562,7 +869,7 @@ export function generateSpawnChunk(): Chunk.PCChunk {
new Vec3(8, 67, 8),
new McBlock(
REGISTRY.blocksByName.barrier.id,
REGISTRY.biomesByName.plains.id,
REGISTRY.biomesByName.the_end.id,
0
)
);
@ -570,7 +877,7 @@ export function generateSpawnChunk(): Chunk.PCChunk {
new Vec3(7, 65, 8),
new McBlock(
REGISTRY.blocksByName.barrier.id,
REGISTRY.biomesByName.plains.id,
REGISTRY.biomesByName.the_end.id,
0
)
);
@ -578,7 +885,7 @@ export function generateSpawnChunk(): Chunk.PCChunk {
new Vec3(7, 66, 8),
new McBlock(
REGISTRY.blocksByName.barrier.id,
REGISTRY.biomesByName.plains.id,
REGISTRY.biomesByName.the_end.id,
0
)
);
@ -586,7 +893,7 @@ export function generateSpawnChunk(): Chunk.PCChunk {
new Vec3(9, 65, 8),
new McBlock(
REGISTRY.blocksByName.barrier.id,
REGISTRY.biomesByName.plains.id,
REGISTRY.biomesByName.the_end.id,
0
)
);
@ -594,7 +901,7 @@ export function generateSpawnChunk(): Chunk.PCChunk {
new Vec3(9, 66, 8),
new McBlock(
REGISTRY.blocksByName.barrier.id,
REGISTRY.biomesByName.plains.id,
REGISTRY.biomesByName.the_end.id,
0
)
);
@ -602,7 +909,7 @@ export function generateSpawnChunk(): Chunk.PCChunk {
new Vec3(8, 65, 7),
new McBlock(
REGISTRY.blocksByName.barrier.id,
REGISTRY.biomesByName.plains.id,
REGISTRY.biomesByName.the_end.id,
0
)
);
@ -610,7 +917,7 @@ export function generateSpawnChunk(): Chunk.PCChunk {
new Vec3(8, 66, 7),
new McBlock(
REGISTRY.blocksByName.barrier.id,
REGISTRY.biomesByName.plains.id,
REGISTRY.biomesByName.the_end.id,
0
)
);
@ -618,7 +925,7 @@ export function generateSpawnChunk(): Chunk.PCChunk {
new Vec3(8, 65, 9),
new McBlock(
REGISTRY.blocksByName.barrier.id,
REGISTRY.biomesByName.plains.id,
REGISTRY.biomesByName.the_end.id,
0
)
);
@ -626,10 +933,11 @@ export function generateSpawnChunk(): Chunk.PCChunk {
new Vec3(8, 66, 9),
new McBlock(
REGISTRY.blocksByName.barrier.id,
REGISTRY.biomesByName.plains.id,
REGISTRY.biomesByName.the_end.id,
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;
}

View File

@ -1,147 +1,153 @@
import { Logger } from "../logger.js"
import mcp, { states } from "minecraft-protocol"
import { Logger } from "../logger.js";
import mcp, { states } from "minecraft-protocol";
const { createSerializer, createDeserializer } = mcp
const { createSerializer, createDeserializer } = mcp;
export namespace BungeeUtil {
export class PacketUUIDTranslator {
public serverSidePlayerUUID: string
public clientSidePlayerUUID: string
public serverSidePlayerUUID: string;
public clientSidePlayerUUID: string;
static readonly CAST_UUID_SERVER: string[] = [
'update_attributes',
'named_entity_spawn',
"update_attributes",
"named_entity_spawn",
// drop this packet (twitch.tv integration not available anymore)
'player_info'
]
static readonly CAST_UUID_CLIENT: string[] = [
'spectate'
]
"player_info",
];
static readonly CAST_UUID_CLIENT: string[] = ["spectate"];
private _logger: Logger
private _serverSerializer: any
private _clientSerializer: any
private _clientDeserializer: any
private _serverDeserializer: any
private _logger: Logger;
private _serverSerializer: any;
private _clientSerializer: any;
private _clientDeserializer: any;
private _serverDeserializer: any;
constructor(ssPlayerUUID: string, csPlayerUUID: string) {
this.serverSidePlayerUUID = ssPlayerUUID
this.clientSidePlayerUUID = csPlayerUUID
this._logger = new Logger("PacketTranslator")
this.serverSidePlayerUUID = ssPlayerUUID;
this.clientSidePlayerUUID = csPlayerUUID;
this._logger = new Logger("PacketTranslator");
this._serverSerializer = createSerializer({
state: states.PLAY,
isServer: true,
version: "1.8.8",
customPackets: null
})
customPackets: null,
});
this._clientSerializer = createSerializer({
state: states.PLAY,
isServer: false,
version: "1.8.8",
customPackets: null
})
customPackets: null,
});
this._clientDeserializer = createDeserializer({
state: states.PLAY,
isServer: false,
version: "1.8.8",
customPackets: null
})
customPackets: null,
});
this._serverDeserializer = createDeserializer({
state: states.PLAY,
isServer: true,
version: "1.8.8",
customPackets: null
})
customPackets: null,
});
}
public onClientWrite(packet: Buffer): Buffer /* write to server */ {
const { name, params } = this._serverDeserializer.parsePacketBuffer(packet).data
return this._clientSerializer.createPacketBuffer(this._translatePacketClient(params, { name }))
const { name, params } =
this._serverDeserializer.parsePacketBuffer(packet).data;
return this._clientSerializer.createPacketBuffer(
this._translatePacketClient(params, { name })
);
}
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 {
if (PacketUUIDTranslator.CAST_UUID_CLIENT.some(id => id == meta.name)) {
if (meta.name == 'spectate') {
if (PacketUUIDTranslator.CAST_UUID_CLIENT.some((id) => id == meta.name)) {
if (meta.name == "spectate") {
if (packet.target == this.clientSidePlayerUUID) {
packet.target = this.serverSidePlayerUUID
packet.target = this.serverSidePlayerUUID;
} else if (packet.target == this.serverSidePlayerUUID) {
packet.target = this.clientSidePlayerUUID
packet.target = this.clientSidePlayerUUID;
}
}
}
return {
name: meta.name,
params: packet
}
params: packet,
};
}
private _translatePacketServer(packet: any, meta: any): any | null {
if (PacketUUIDTranslator.CAST_UUID_SERVER.some(id => id == meta.name)) {
if (meta.name == 'update_attributes') {
if (PacketUUIDTranslator.CAST_UUID_SERVER.some((id) => id == meta.name)) {
if (meta.name == "update_attributes") {
for (const prop of packet.properties) {
for (const modifier of prop.modifiers) {
if (modifier.uuid == this.serverSidePlayerUUID) {
modifier.uuid = this.clientSidePlayerUUID
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) {
packet.playerUUID = this.clientSidePlayerUUID
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) {
if (player.UUID == this.serverSidePlayerUUID) {
player.UUID = this.clientSidePlayerUUID
player.UUID = this.clientSidePlayerUUID;
} else if (player.UUID == this.clientSidePlayerUUID) {
player.UUID = this.serverSidePlayerUUID
player.UUID = this.serverSidePlayerUUID;
}
}
}
}
return {
name: meta.name,
params: packet
}
params: packet,
};
}
}
export function getRespawnSequence(login: any, serializer: any): [Buffer, Buffer] {
const dimset = getDimSets(login.dimension)
export function getRespawnSequence(
login: any,
serializer: any
): [Buffer, Buffer] {
const dimset = getDimSets(login.dimension);
return [
serializer.createPacketBuffer({
name: 'respawn',
name: "respawn",
params: {
dimension: dimset[0],
difficulty: login.difficulty,
gamemode: login.gameMode,
levelType: login.levelType
}
levelType: login.levelType,
},
}),
serializer.createPacketBuffer({
name: 'respawn',
name: "respawn",
params: {
dimension: dimset[1],
difficulty: login.difficulty,
gamemode: login.gameMode,
levelType: login.levelType
}
})
]
levelType: login.levelType,
},
}),
];
}
function getDimSets(loginDim: number): [number, number] {
return [
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 type ChatExtra = {
text: string,
bold?: boolean,
italic?: boolean,
underlined?: boolean,
strikethrough?: boolean,
obfuscated?: boolean,
color?: Enums.ChatColor | 'reset'
}
text: string;
bold?: boolean;
italic?: boolean;
underlined?: boolean;
strikethrough?: boolean;
obfuscated?: boolean;
color?: Enums.ChatColor | "reset";
};
export type Chat = {
text?: string,
bold?: boolean,
italic?: boolean,
underlined?: boolean,
strikethrough?: boolean,
obfuscated?: boolean,
color?: Enums.ChatColor | 'reset',
extra?: ChatExtra[]
}
text?: string;
bold?: boolean;
italic?: boolean;
underlined?: boolean;
strikethrough?: boolean;
obfuscated?: boolean;
color?: Enums.ChatColor | "reset";
extra?: ChatExtra[];
};
export function chatToPlainString(chat: Chat): string {
let ret = ''
if (chat.text != null) ret += chat.text
let ret = "";
if (chat.text != null) ret += chat.text;
if (chat.extra != null) {
chat.extra.forEach(extra => {
let append = ""
if (extra.bold) append += Enums.ChatColor.BOLD
if (extra.italic) append += Enums.ChatColor.ITALIC
if (extra.underlined) append += Enums.ChatColor.UNDERLINED
if (extra.strikethrough) append += Enums.ChatColor.STRIKETHROUGH
if (extra.obfuscated) append += Enums.ChatColor.OBFUSCATED
if (extra.color) append += extra.color == 'reset' ? Enums.ChatColor.RESET : resolveColor(extra.color)
append += extra.text
ret += append
})
chat.extra.forEach((extra) => {
let append = "";
if (extra.bold) append += Enums.ChatColor.BOLD;
if (extra.italic) append += Enums.ChatColor.ITALIC;
if (extra.underlined) append += Enums.ChatColor.UNDERLINED;
if (extra.strikethrough) append += Enums.ChatColor.STRIKETHROUGH;
if (extra.obfuscated) append += Enums.ChatColor.OBFUSCATED;
if (extra.color)
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 ccKeys = Object.keys(Enums.ChatColor).map(str => str.toLowerCase())
const ccValues = Object.values(Enums.ChatColor);
const ccKeys = Object.keys(Enums.ChatColor).map((str) => str.toLowerCase());
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 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_CLIENT_UPLOAD_SKIN_BUILTIN: number[] = [0x00, 0x05, 0x01, 0x00, 0x00, 0x00]
export const EAGLERCRAFT_SKIN_CUSTOM_LENGTH = 64 ** 2 * 4
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_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 PLAYER_LOOK_PACKET = 0x08
export const JOIN_SERVER_PACKET = 0x01;
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,
CSSetSkinPacket = 0x07,
CSReadyPacket = 0x08,
SCReadyPacket = 0x09
SCReadyPacket = 0x09,
}
export enum ChannelMessageType {
CLIENT = 0x17,
SERVER = 0x3f
SERVER = 0x3f,
}
export enum EaglerSkinPacketId {
CFetchSkinEaglerPlayerReq = 0x03,
SFetchSkinBuiltInRes = 0x04,
SFetchSkinRes = 0x05,
CFetchSkinReq = 0x06
CFetchSkinReq = 0x06,
}
export enum ClientState {
PRE_HANDSHAKE = "PRE_HANDSHAKE",
POST_HANDSHAKE = "POST_HANDSHAKE",
DISCONNECTED = "DISCONNECTED"
DISCONNECTED = "DISCONNECTED",
}
export enum PacketBounds {
C = "C",
S = "S"
S = "S",
}
export enum SkinType {
BUILTIN,
CUSTOM
CUSTOM,
}
export enum ChatColor {
@ -61,11 +61,11 @@ export namespace Enums {
YELLOW = "§e",
WHITE = "§f",
// text styling
OBFUSCATED = '§k',
BOLD = '§l',
STRIKETHROUGH = '§m',
UNDERLINED = '§n',
ITALIC = '§o',
RESET = '§r'
OBFUSCATED = "§k",
BOLD = "§l",
STRIKETHROUGH = "§m",
UNDERLINED = "§n",
ITALIC = "§o",
RESET = "§r",
}
}

View File

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

View File

@ -1,35 +1,44 @@
import { readdir } from "fs/promises"
import { dirname, join, resolve } from "path"
import { fileURLToPath, pathToFileURL } from "url"
import { Enums } from "./Enums.js"
import { Util } from "./Util.js"
import { readdir } from "fs/promises";
import { dirname, join, resolve } from "path";
import { fileURLToPath, pathToFileURL } from "url";
import { Enums } from "./Enums.js";
import { Util } from "./Util.js";
export default interface Packet {
packetId: Enums.PacketId
type: "packet"
boundTo: Enums.PacketBounds
sentAfterHandshake: boolean
packetId: Enums.PacketId;
type: "packet";
boundTo: Enums.PacketBounds;
sentAfterHandshake: boolean;
serialize: () => Buffer
deserialize: (packet: Buffer) => this
serialize: () => Buffer;
deserialize: (packet: Buffer) => this;
}
export async function loadPackets(dir?: string): 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()
export async function loadPackets(
dir?: string
): 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) {
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)) {
if (val != null) {
let e: Packet
try { e = new (val as any)() }
catch {}
if (e != null && e.type == 'packet') {
;(e as any).class = val
packetRegistry.set(e.packetId, e)
let e: Packet;
try {
e = new (val as any)();
} catch {}
if (e != null && e.type == "packet") {
(e as any).class = val;
packetRegistry.set(e.packetId, e);
}
}
}
}
return packetRegistry
return packetRegistry;
}

View File

@ -1,9 +1,9 @@
import {
encodeULEB128 as _encodeVarInt,
decodeULEB128 as _decodeVarInt
} from "@thi.ng/leb128"
import { Enums } from "./Enums.js"
import { Util } from "./Util.js"
decodeULEB128 as _decodeVarInt,
} from "@thi.ng/leb128";
import { Enums } from "./Enums.js";
import { Util } from "./Util.js";
// reference: https://wiki.vg/index.php?title=Protocol&oldid=7368 (id: 73)
// 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 type ReadResult<T> = {
value: T,
value: T;
// the new buffer, but with the bytes being read being completely removed
// 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 {
return Buffer.from(_encodeVarInt(int))
return Buffer.from(_encodeVarInt(int));
}
export function readVarInt(buff: Buffer, offset?: number): ReadResult<number> {
buff = offset ? buff.subarray(offset) : buff
const read = _decodeVarInt(buff), len = read[1]
export function readVarInt(
buff: Buffer,
offset?: number
): ReadResult<number> {
buff = offset ? buff.subarray(offset) : buff;
const read = _decodeVarInt(buff),
len = read[1];
return {
// potential oversight?
value: Number(read[0]),
newBuffer: buff.subarray(len)
}
newBuffer: buff.subarray(len),
};
}
export function writeString(str: string): Buffer {
const bufferized = Buffer.from(str, 'utf8'), len = writeVarInt(bufferized.length)
return Buffer.concat([len, bufferized])
const bufferized = Buffer.from(str, "utf8"),
len = writeVarInt(bufferized.length);
return Buffer.concat([len, bufferized]);
}
export function readString(buff: Buffer, offset?: number): ReadResult<string> {
buff = offset ? buff.subarray(offset) : buff
const len = readVarInt(buff), str = len.newBuffer.subarray(0, len.value).toString('utf8')
export function readString(
buff: Buffer,
offset?: number
): ReadResult<string> {
buff = offset ? buff.subarray(offset) : buff;
const len = readVarInt(buff),
str = len.newBuffer.subarray(0, len.value).toString("utf8");
return {
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> {
buff = offset ? buff.subarray(offset) : buff
buff = offset ? buff.subarray(offset) : buff;
return {
value: _readShort(buff[0], buff[1]),
newBuffer: buff.subarray(2)
}
newBuffer: buff.subarray(2),
};
}
export function writeShort(num: number): Buffer {
const alloc = Buffer.alloc(2)
alloc.writeInt16BE(num)
return alloc
const alloc = Buffer.alloc(2);
alloc.writeInt16BE(num);
return alloc;
}
export function readUUID(buff: Buffer, offset?: number): ReadResult<string> {
buff = offset ? buff.subarray(offset) : buff
buff = offset ? buff.subarray(offset) : buff;
return {
value: Util.uuidBufferToString(buff.subarray(0, 16)),
newBuffer: buff.subarray(16)
}
newBuffer: buff.subarray(16),
};
}
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 { Logger } from "../logger.js";
import Packet, { loadPackets } from "./Packet.js";
import * as http from "http"
import * as https from "https"
import * as http from "http";
import * as https from "https";
import { readFile } from "fs/promises";
import { Duplex } from "stream";
import { parseDomain, ParseResultType } from "parse-domain"
import { parseDomain, ParseResultType } from "parse-domain";
import { Util } from "./Util.js";
import CSLoginPacket from "./packets/CSLoginPacket.js";
import SCIdentifyPacket from "./packets/SCIdentifyPacket.js";
import { Motd } from "./Motd.js";
import { Player } from "./Player.js";
import { Enums } from "./Enums.js";
import { NETWORK_VERSION, PROXY_BRANDING, PROXY_VERSION, VANILLA_PROTOCOL_VERSION } from "../meta.js";
import {
NETWORK_VERSION,
PROXY_BRANDING,
PROXY_VERSION,
VANILLA_PROTOCOL_VERSION,
} from "../meta.js";
import { CSUsernamePacket } from "./packets/CSUsernamePacket.js";
import { SCSyncUuidPacket } from "./packets/SCSyncUuidPacket.js";
import { SCReadyPacket } from "./packets/SCReadyPacket.js";
@ -26,312 +31,457 @@ import { CSChannelMessagePacket } from "./packets/channel/CSChannelMessage.js";
import { Constants, UPGRADE_REQUIRED_RESPONSE } from "./Constants.js";
import { PluginManager } from "./pluginLoader/PluginManager.js";
let instanceCount = 0
const chalk = new Chalk({ level: 2 })
let instanceCount = 0;
const chalk = new Chalk({ level: 2 });
export class Proxy extends EventEmitter {
public packetRegistry: Map<number, Packet & {
class: any
}>
public players = new Map<string, Player>()
public pluginManager: PluginManager
public config: Config['adapter']
public wsServer: WebSocketServer
public httpServer: http.Server
public skinServer: EaglerSkins.SkinServer
public broadcastMotd?: Motd.MOTD
public packetRegistry: Map<
number,
Packet & {
class: any;
}
>;
public players = new Map<string, Player>();
public pluginManager: PluginManager;
public config: Config["adapter"];
public wsServer: WebSocketServer;
public httpServer: http.Server;
public skinServer: EaglerSkins.SkinServer;
public broadcastMotd?: Motd.MOTD;
private _logger: Logger
private initalHandlerLogger: Logger
private _logger: Logger;
private initalHandlerLogger: Logger;
private loaded: boolean
private loaded: boolean;
constructor(config: Config['adapter'], pluginManager: PluginManager) {
super()
this._logger = new Logger(`EaglerProxy-${instanceCount}`)
this.initalHandlerLogger = new Logger(`EaglerProxy-InitialHandler`)
constructor(config: Config["adapter"], pluginManager: PluginManager) {
super();
this._logger = new Logger(`EaglerProxy-${instanceCount}`);
this.initalHandlerLogger = new Logger(`EaglerProxy-InitialHandler`);
// 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 as any)._info(`${chalk.blue("[InitialHandler]")} ${msg}`)
}
;(this.initalHandlerLogger as any)._warn = this.initalHandlerLogger.warn
(this.initalHandlerLogger as any)._info(
`${chalk.blue("[InitialHandler]")} ${msg}`
);
};
(this.initalHandlerLogger as any)._warn = this.initalHandlerLogger.warn;
this.initalHandlerLogger.warn = (msg: string) => {
;(this.initalHandlerLogger as any)._warn(`${chalk.blue("[InitialHandler]")} ${msg}`)
}
;(this.initalHandlerLogger as any)._error = this.initalHandlerLogger.error
(this.initalHandlerLogger as any)._warn(
`${chalk.blue("[InitialHandler]")} ${msg}`
);
};
(this.initalHandlerLogger as any)._error = this.initalHandlerLogger.error;
this.initalHandlerLogger.error = (msg: string) => {
;(this.initalHandlerLogger as any)._error(`${chalk.blue("[InitialHandler]")} ${msg}`)
}
;(this.initalHandlerLogger as any)._fatal = this.initalHandlerLogger.fatal
(this.initalHandlerLogger as any)._error(
`${chalk.blue("[InitialHandler]")} ${msg}`
);
};
(this.initalHandlerLogger as any)._fatal = this.initalHandlerLogger.fatal;
this.initalHandlerLogger.fatal = (msg: string) => {
;(this.initalHandlerLogger as any)._fatal(`${chalk.blue("[InitialHandler]")} ${msg}`)
}
;(this.initalHandlerLogger as any)._debug = this.initalHandlerLogger.debug
(this.initalHandlerLogger as any)._fatal(
`${chalk.blue("[InitialHandler]")} ${msg}`
);
};
(this.initalHandlerLogger as any)._debug = this.initalHandlerLogger.debug;
this.initalHandlerLogger.debug = (msg: string) => {
;(this.initalHandlerLogger as any)._debug(`${chalk.blue("[InitialHandler]")} ${msg}`)
}
this.config = config
this.pluginManager = pluginManager
instanceCount++
(this.initalHandlerLogger as any)._debug(
`${chalk.blue("[InitialHandler]")} ${msg}`
);
};
this.config = config;
this.pluginManager = pluginManager;
instanceCount++;
process.on('uncaughtException', err => {
this._logger.warn(`An uncaught exception was caught! Error: ${err.stack}`)
})
process.on("uncaughtException", (err) => {
this._logger.warn(
`An uncaught exception was caught! Error: ${err.stack}`
);
});
process.on('unhandledRejection', err => {
this._logger.warn(`An unhandled rejection was caught! Rejection: ${err}`)
})
process.on("unhandledRejection", (err) => {
this._logger.warn(`An unhandled rejection was caught! Rejection: ${err}`);
});
}
public async init() {
this._logger.info(`Starting ${PROXY_BRANDING} v${PROXY_VERSION}...`)
global.PROXY = this
if (this.loaded) throw new Error("Can't initiate if proxy instance is already initialized or is being initialized!")
this.loaded = true
this.packetRegistry = await loadPackets()
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)
this._logger.info(`Starting ${PROXY_BRANDING} v${PROXY_VERSION}...`);
global.PROXY = this;
if (this.loaded)
throw new Error(
"Can't initiate if proxy instance is already initialized or is being initialized!"
);
this.loaded = true;
this.packetRegistry = await loadPackets();
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 {
// TODO: motd
const broadcastMOTD = await Motd.MOTD.generateMOTDFromConfig(this.config)
;(broadcastMOTD as any)._static = true
this.broadcastMotd = broadcastMOTD
const broadcastMOTD = await Motd.MOTD.generateMOTDFromConfig(this.config);
(broadcastMOTD as any)._static = true;
this.broadcastMotd = broadcastMOTD;
// playercount will be dynamically updated
}
if (this.config.tls && this.config.tls.enabled) {
this.httpServer = https.createServer({
this.httpServer = https
.createServer(
{
key: await readFile(this.config.tls.key),
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')
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"
);
this.wsServer = new WebSocketServer({
noServer: true
})
noServer: true,
});
} else {
this.httpServer = http.createServer((req, res) => this._handleNonWSRequest(req, res, this.config)).listen(this.config.bindPort || 8080, this.config.bindHost || '127.0.0.1')
this.httpServer = http
.createServer((req, res) =>
this._handleNonWSRequest(req, res, this.config)
)
.listen(
this.config.bindPort || 8080,
this.config.bindHost || "127.0.0.1"
);
this.wsServer = new WebSocketServer({
noServer: true
})
noServer: true,
});
}
this.httpServer.on('error', err => {
this._logger.warn(`HTTP server threw an error: ${err.stack}`)
})
this.wsServer.on('error', err => {
this._logger.warn(`WebSocket server threw an error: ${err.stack}`)
})
this.httpServer.on('upgrade', async (r, s, h) => {
this.httpServer.on("error", (err) => {
this._logger.warn(`HTTP server threw an error: ${err.stack}`);
});
this.wsServer.on("error", (err) => {
this._logger.warn(`WebSocket server threw an error: ${err.stack}`);
});
this.httpServer.on("upgrade", async (r, s, h) => {
try {
await this._handleWSConnectionReq(r, s, h)
await this._handleWSConnectionReq(r, s, h);
} catch (err) {
this._logger.error(`Error was caught whilst trying to handle WebSocket upgrade! Error: ${err.stack ?? err}`)
this._logger.error(
`Error was caught whilst trying to handle WebSocket upgrade! Error: ${
err.stack ?? err
}`
);
}
})
this.pluginManager.emit('proxyFinishLoading', this, this.pluginManager)
this._logger.info(`Started WebSocket server and binded to ${this.config.bindHost} on port ${this.config.bindPort}.`)
});
this.pluginManager.emit("proxyFinishLoading", this, this.pluginManager);
this._logger.info(
`Started WebSocket server and binded to ${this.config.bindHost} on port ${this.config.bindPort}.`
);
}
private _handleNonWSRequest(req: http.IncomingMessage, res: http.ServerResponse, config: Config['adapter']) {
res.setHeader("Content-Type", "text/html")
private _handleNonWSRequest(
req: http.IncomingMessage,
res: http.ServerResponse,
config: Config["adapter"]
) {
res
.setHeader("Content-Type", "text/html")
.writeHead(426)
.end(UPGRADE_REQUIRED_RESPONSE)
.end(UPGRADE_REQUIRED_RESPONSE);
}
readonly LOGIN_TIMEOUT = 30000
readonly LOGIN_TIMEOUT = 30000;
private async _handleWSConnection(ws: WebSocket) {
const firstPacket = await Util.awaitPacket(ws)
let player: Player, handled: boolean
const firstPacket = await Util.awaitPacket(ws);
let player: Player, handled: boolean;
setTimeout(() => {
if (!handled) {
this.initalHandlerLogger.warn(`Disconnecting client ${player ? player.username ?? `[/${(ws as any)._socket.remoteAddress}:${(ws as any)._socket.remotePort}` : `[/${(ws as any)._socket.remoteAddress}:${(ws as any)._socket.remotePort}`} 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.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.`
);
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 {
if (firstPacket.toString() === "Accept: MOTD") {
if (this.broadcastMotd) {
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
this.broadcastMotd.jsonMotd.data.players = []
this.broadcastMotd.jsonMotd.data.players = [];
const playerSample = [...this.players.keys()]
.filter(sample => !sample.startsWith("!phs_"))
.slice(0, 5)
this.broadcastMotd.jsonMotd.data.players = playerSample
if (this.players.size - playerSample.length > 0) this.broadcastMotd.jsonMotd.data.players.push(`${Enums.ChatColor.GRAY}${Enums.ChatColor.ITALIC}(and ${this.players.size - playerSample.length} more)`)
.filter((sample) => !sample.startsWith("!phs_"))
.slice(0, 5);
this.broadcastMotd.jsonMotd.data.players = playerSample;
if (this.players.size - playerSample.length > 0)
this.broadcastMotd.jsonMotd.data.players.push(
`${Enums.ChatColor.GRAY}${Enums.ChatColor.ITALIC}(and ${
this.players.size - playerSample.length
} more)`
);
const bufferized = this.broadcastMotd.toBuffer()
ws.send(bufferized[0])
if (bufferized[1] != null) ws.send(bufferized[1])
const bufferized = this.broadcastMotd.toBuffer();
ws.send(bufferized[0]);
if (bufferized[1] != null) ws.send(bufferized[1]);
} else {
const motd = this.broadcastMotd.toBuffer()
ws.send(motd[0])
if (motd[1] != null) ws.send(motd[1])
const motd = this.broadcastMotd.toBuffer();
ws.send(motd[0]);
if (motd[1] != null) ws.send(motd[1]);
}
}
handled = true
ws.close()
handled = true;
ws.close();
} else {
player = new Player(ws)
const loginPacket = new CSLoginPacket().deserialize(firstPacket)
player.state = Enums.ClientState.PRE_HANDSHAKE
player = new Player(ws);
const loginPacket = new CSLoginPacket().deserialize(firstPacket);
player.state = Enums.ClientState.PRE_HANDSHAKE;
if (loginPacket.gameVersion != VANILLA_PROTOCOL_VERSION) {
player.disconnect(`${Enums.ChatColor.RED}Please connect to this proxy on EaglercraftX 1.8.9.`)
return
player.disconnect(
`${Enums.ChatColor.RED}Please connect to this proxy on EaglercraftX 1.8.9.`
);
return;
} else if (loginPacket.networkVersion != NETWORK_VERSION) {
player.disconnect(`${Enums.ChatColor.RED}Your EaglercraftX version is too ${loginPacket.networkVersion > NETWORK_VERSION ? "new" : "old"}! Please ${loginPacket.networkVersion > NETWORK_VERSION ? "downgrade" : "update"}.`)
return
player.disconnect(
`${Enums.ChatColor.RED}Your EaglercraftX version is too ${
loginPacket.networkVersion > NETWORK_VERSION ? "new" : "old"
}! Please ${
loginPacket.networkVersion > NETWORK_VERSION
? "downgrade"
: "update"
}.`
);
return;
}
try { Util.validateUsername(loginPacket.username) }
catch (err) {
player.disconnect(`${Enums.ChatColor.RED}${err.reason || err}`)
return
try {
Util.validateUsername(loginPacket.username);
} catch (err) {
player.disconnect(`${Enums.ChatColor.RED}${err.reason || err}`);
return;
}
player.username = loginPacket.username
player.uuid = Util.generateUUIDFromPlayer(player.username)
player.username = loginPacket.username;
player.uuid = Util.generateUUIDFromPlayer(player.username);
if (this.players.size > this.config.maxConcurrentClients) {
player.disconnect(`${Enums.ChatColor.YELLOW}Proxy is full! Please try again later.`)
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
player.disconnect(
`${Enums.ChatColor.YELLOW}Proxy is full! Please try again later.`
);
return;
} else if (
this.players.get(player.username) != null ||
this.players.get(`!phs.${player.uuid}`) != null
) {
player.disconnect(
`${Enums.ChatColor.YELLOW}Someone under your username (${player.username}) is already connected to the proxy!`
);
return;
}
this.players.set(`!phs.${player.uuid}`, player)
this._logger.info(`Player ${loginPacket.username} (${Util.generateUUIDFromPlayer(loginPacket.username)}) running ${loginPacket.brand}/${loginPacket.version} (net ver: ${loginPacket.networkVersion}, game ver: ${loginPacket.gameVersion}) is attempting to connect!`)
player.write(new SCIdentifyPacket())
const usernamePacket: CSUsernamePacket = await player.read(Enums.PacketId.CSUsernamePacket) as any
this.players.set(`!phs.${player.uuid}`, player);
this._logger.info(
`Player ${loginPacket.username} (${Util.generateUUIDFromPlayer(
loginPacket.username
)}) running ${loginPacket.brand}/${loginPacket.version} (net ver: ${
loginPacket.networkVersion
}, game ver: ${loginPacket.gameVersion}) is attempting to connect!`
);
player.write(new SCIdentifyPacket());
const usernamePacket: CSUsernamePacket = (await player.read(
Enums.PacketId.CSUsernamePacket
)) as any;
if (usernamePacket.username !== player.username) {
player.disconnect(`${Enums.ChatColor.YELLOW}Failed to complete handshake. Your game version may be too old or too new.`)
return
player.disconnect(
`${Enums.ChatColor.YELLOW}Failed to complete handshake. Your game version may be too old or too new.`
);
return;
}
const syncUuid = new SCSyncUuidPacket()
syncUuid.username = player.username
syncUuid.uuid = player.uuid
player.write(syncUuid)
const syncUuid = new SCSyncUuidPacket();
syncUuid.username = player.username;
syncUuid.uuid = player.uuid;
player.write(syncUuid);
const prom = await Promise.all([player.read(Enums.PacketId.CSReadyPacket), await player.read(Enums.PacketId.CSSetSkinPacket) as CSSetSkinPacket]),
const prom = await Promise.all([
player.read(Enums.PacketId.CSReadyPacket),
(await player.read(
Enums.PacketId.CSSetSkinPacket
)) as CSSetSkinPacket,
]),
skin = prom[1],
obj = new EaglerSkins.EaglerSkin()
obj.owner = player
obj.type = skin.skinType as any
if (skin.skinType == Enums.SkinType.CUSTOM) obj.skin = skin.skin
else obj.builtInSkin = skin.skinId
player.skin = obj
obj = new EaglerSkins.EaglerSkin();
obj.owner = player;
obj.type = skin.skinType as any;
if (skin.skinType == Enums.SkinType.CUSTOM) obj.skin = skin.skin;
else obj.builtInSkin = skin.skinId;
player.skin = obj;
player.write(new SCReadyPacket())
this.players.delete(`!phs.${player.uuid}`)
this.players.set(player.username, player)
player.initListeners()
this._bindListenersToPlayer(player)
player.state = Enums.ClientState.POST_HANDSHAKE
this._logger.info(`Handshake Success! Connecting player ${player.username} to server...`)
handled = true
player.write(new SCReadyPacket());
this.players.delete(`!phs.${player.uuid}`);
this.players.set(player.username, player);
player.initListeners();
this._bindListenersToPlayer(player);
player.state = Enums.ClientState.POST_HANDSHAKE;
this._logger.info(
`Handshake Success! Connecting player ${player.username} to server...`
);
handled = true;
await player.connect({
host: this.config.server.host,
port: this.config.server.port,
username: player.username
})
this._logger.info(`Player ${player.username} successfully connected to server.`)
this.emit('playerConnect', player)
username: player.username,
});
this._logger.info(
`Player ${player.username} successfully connected to server.`
);
this.emit("playerConnect", player);
}
} catch (err) {
this.initalHandlerLogger.warn(`Error occurred whilst handling handshake: ${err.stack ?? err}`)
handled = true
ws.close()
this.initalHandlerLogger.warn(
`Error occurred whilst handling handshake: ${err.stack ?? err}`
);
handled = true;
ws.close();
if (player && player.uuid && this.players.has(`!phs.${player.uuid}`))
this.players.delete(`!phs.${player.uuid}`)
this.players.delete(`!phs.${player.uuid}`);
if (player && player.uuid && this.players.has(player.username))
this.players.delete(player.username)
this.players.delete(player.username);
}
}
private _bindListenersToPlayer(player: Player) {
let sentDisconnectMsg = false
player.on('disconnect', () => {
let sentDisconnectMsg = false;
player.on("disconnect", () => {
if (this.players.has(player.username))
this.players.delete(player.username)
this.initalHandlerLogger.info(`DISCONNECT ${player.username} <=> DISCONNECTED`)
if (!sentDisconnectMsg) this._logger.info(`Player ${player.username} (${player.uuid}) disconnected from the proxy server.`)
})
player.on('proxyPacket', async packet => {
this.players.delete(player.username);
this.initalHandlerLogger.info(
`DISCONNECT ${player.username} <=> DISCONNECTED`
);
if (!sentDisconnectMsg)
this._logger.info(
`Player ${player.username} (${player.uuid}) disconnected from the proxy server.`
);
});
player.on("proxyPacket", async (packet) => {
if (packet.packetId == Enums.PacketId.CSChannelMessagePacket) {
try {
const msg: CSChannelMessagePacket = packet as any
const msg: CSChannelMessagePacket = packet as any;
if (msg.channel == Constants.EAGLERCRAFT_SKIN_CHANNEL_NAME) {
await this.skinServer.handleRequest(msg, player)
await this.skinServer.handleRequest(msg, player);
}
} catch (err) {
this._logger.error(`Failed to process channel message packet! Error: ${err.stack || err}`)
this._logger.error(
`Failed to process channel message packet! Error: ${
err.stack || err
}`
);
}
}
})
player.on('switchServer', client => {
this.initalHandlerLogger.info(`SWITCH_SERVER ${player.username} <=> ${client.socket.remoteAddress}:${client.socket.remotePort}`)
})
player.on('joinServer', client => {
this.initalHandlerLogger.info(`SERVER_CONNECTED ${player.username} <=> ${client.socket.remoteAddress}:${client.socket.remotePort}`)
})
});
player.on("switchServer", (client) => {
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}`
);
});
}
static readonly POLL_INTERVAL: number = 10000
static readonly POLL_INTERVAL: number = 10000;
private _pollServer(host: string, port: number, interval?: number) {
;(async () => {
(async () => {
while (true) {
const motd = await Motd.MOTD.generateMOTDFromPing(host, port)
.catch(err => {
this._logger.warn(`Error polling ${host}:${port} for MOTD: ${err.stack ?? err}`)
})
if (motd) this.broadcastMotd = motd
await new Promise(res => setTimeout(res, interval ?? Proxy.POLL_INTERVAL))
const motd = await Motd.MOTD.generateMOTDFromPing(host, port).catch(
(err) => {
this._logger.warn(
`Error polling ${host}:${port} for MOTD: ${err.stack ?? err}`
);
}
})()
);
if (motd) this.broadcastMotd = motd;
await new Promise((res) =>
setTimeout(res, interval ?? Proxy.POLL_INTERVAL)
);
}
})();
}
private async _handleWSConnectionReq(req: http.IncomingMessage, socket: Duplex, head: Buffer) {
const origin = req.headers.origin == null || req.headers.origin == 'null' ? null : req.headers.origin
private async _handleWSConnectionReq(
req: http.IncomingMessage,
socket: Duplex,
head: Buffer
) {
const origin =
req.headers.origin == null || req.headers.origin == "null"
? null
: req.headers.origin;
if (!this.config.origins.allowOfflineDownloads && origin == null) {
socket.destroy()
return
socket.destroy();
return;
}
if (this.config.origins.originBlacklist != null && this.config.origins.originBlacklist.some(host => Util.areDomainsEqual(host, origin))) {
socket.destroy()
return
if (
this.config.origins.originBlacklist != null &&
this.config.origins.originBlacklist.some((host) =>
Util.areDomainsEqual(host, origin)
)
) {
socket.destroy();
return;
}
if (this.config.origins.originWhitelist != null && !this.config.origins.originWhitelist.some(host => Util.areDomainsEqual(host, origin))) {
socket.destroy()
return
if (
this.config.origins.originWhitelist != null &&
!this.config.origins.originWhitelist.some((host) =>
Util.areDomainsEqual(host, origin)
)
) {
socket.destroy();
return;
}
try { await this.wsServer.handleUpgrade(req, socket, head, ws => this._handleWSConnection(ws)) }
catch (err) {
this._logger.error(`Error was caught whilst trying to handle WebSocket connection request! Error: ${err.stack ?? err}`)
socket.destroy()
try {
await this.wsServer.handleUpgrade(req, socket, head, (ws) =>
this._handleWSConnection(ws)
);
} 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 {
for (const [username, player] of this.players) {
if (player.uuid == uuid)
return player
if (player.uuid == uuid) return player;
}
return null
return null;
}
}
interface ProxyEvents {
'playerConnect': (player: Player) => void,
'playerDisconnect': (player: Player) => void
playerConnect: (player: Player) => void;
playerDisconnect: (player: Player) => void;
}
export declare interface Proxy {
on<U extends keyof ProxyEvents>(
event: U, listener: ProxyEvents[U]
): this;
on<U extends keyof ProxyEvents>(event: U, listener: ProxyEvents[U]): this;
emit<U extends keyof ProxyEvents>(
event: U, ...args: Parameters<ProxyEvents[U]>
event: U,
...args: Parameters<ProxyEvents[U]>
): boolean;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -4,27 +4,34 @@ import Packet from "../Packet.js";
import { MineProtocol } from "../Protocol.js";
export default class SCDisconnectPacket implements Packet {
packetId: Enums.PacketId = Enums.PacketId.SCDisconnectPacket
type: "packet" = "packet"
boundTo = Enums.PacketBounds.C
sentAfterHandshake = false
static readonly REASON = 0x8
packetId: Enums.PacketId = Enums.PacketId.SCDisconnectPacket;
type: "packet" = "packet";
boundTo = Enums.PacketBounds.C;
sentAfterHandshake = false;
static readonly REASON = 0x8;
reason: string | Chat.Chat
reason: string | Chat.Chat;
public serialize() {
const msg = (typeof this.reason == 'string' ? this.reason : Chat.chatToPlainString(this.reason))
return Buffer.concat([
const msg =
typeof this.reason == "string"
? this.reason
: Chat.chatToPlainString(this.reason);
return Buffer.concat(
[
[0xff],
MineProtocol.writeVarInt(SCDisconnectPacket.REASON),
MineProtocol.writeString(" " + msg + " ")
].map(arr => arr instanceof Uint8Array ? arr : Buffer.from(arr)))
MineProtocol.writeString(" " + msg + " "),
].map((arr) => (arr instanceof Uint8Array ? arr : Buffer.from(arr)))
);
}
public deserialize(packet: Buffer) {
if (packet[0] != this.packetId) throw new Error("Invalid packet ID!")
packet = packet.subarray(1 + MineProtocol.writeVarInt(SCDisconnectPacket.REASON).length)
const reason = MineProtocol.readString(packet)
this.reason = reason.value
return this
if (packet[0] != this.packetId) throw new Error("Invalid packet ID!");
packet = packet.subarray(
1 + MineProtocol.writeVarInt(SCDisconnectPacket.REASON).length
);
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 Packet from "../Packet.js";
import { MineProtocol } from "../Protocol.js";
export default class SCIdentifyPacket implements Packet {
packetId: Enums.PacketId = Enums.PacketId.SCIdentifyPacket
type: "packet" = "packet"
boundTo = Enums.PacketBounds.C
sentAfterHandshake = false
packetId: Enums.PacketId = Enums.PacketId.SCIdentifyPacket;
type: "packet" = "packet";
boundTo = Enums.PacketBounds.C;
sentAfterHandshake = false;
protocolVer = NETWORK_VERSION
gameVersion = VANILLA_PROTOCOL_VERSION
branding = PROXY_BRANDING
version = PROXY_VERSION
protocolVer = NETWORK_VERSION;
gameVersion = VANILLA_PROTOCOL_VERSION;
branding = PROXY_BRANDING;
version = PROXY_VERSION;
public serialize() {
return Buffer.concat([
return Buffer.concat(
[
[0x02],
MineProtocol.writeShort(this.protocolVer),
MineProtocol.writeShort(this.gameVersion),
MineProtocol.writeString(this.branding),
MineProtocol.writeString(this.version),
[0x00, 0x00, 0x00]
].map(arr => arr instanceof Uint8Array ? arr : Buffer.from(arr)))
[0x00, 0x00, 0x00],
].map((arr) => (arr instanceof Uint8Array ? arr : Buffer.from(arr)))
);
}
public deserialize(packet: Buffer) {
if (packet[0] != this.packetId) throw TypeError("Invalid packet ID detected!")
packet = packet.subarray(1)
if (packet[0] != this.packetId)
throw TypeError("Invalid packet ID detected!");
packet = packet.subarray(1);
const protoVer = MineProtocol.readShort(packet),
gameVer = MineProtocol.readShort(protoVer.newBuffer),
branding = MineProtocol.readString(gameVer.newBuffer),
version = MineProtocol.readString(branding.newBuffer)
this.gameVersion = gameVer.value
this.branding = branding.value
this.version = version.value
return this
version = MineProtocol.readString(branding.newBuffer);
this.gameVersion = gameVer.value;
this.branding = branding.value;
this.version = version.value;
return this;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,65 +1,75 @@
export default class SimpleRatelimit<T> {
readonly requestCount: number
readonly resetInterval: number
private entries: Map<T, Ratelimit>
readonly requestCount: number;
readonly resetInterval: number;
private entries: Map<T, Ratelimit>;
constructor(requestCount: number, resetInterval: number) {
this.requestCount = requestCount
this.resetInterval = resetInterval
this.entries = new Map()
this.requestCount = requestCount;
this.resetInterval = resetInterval;
this.entries = new Map();
}
public get(key: T): Ratelimit {
return this.entries.get(key) ?? {
return (
this.entries.get(key) ?? {
remainingRequests: this.requestCount,
resetTime: new Date(0)
resetTime: new Date(0),
}
);
}
public consume(key: T, count?: number): Ratelimit | never {
if (this.entries.has(key)) {
const ratelimit = this.entries.get(key)
const ratelimit = this.entries.get(key);
if (ratelimit.remainingRequests - (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 {
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
return ratelimit
ratelimit.remainingRequests -= count ?? 1;
return ratelimit;
} else {
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 = {
remainingRequests: this.requestCount - (count ?? 1),
resetTime: new Date(Date.now() + this.resetInterval),
timer: null
}
this.entries.set(key, ratelimit)
ratelimit.timer = this._onAdd(ratelimit)
return ratelimit
timer: null,
};
this.entries.set(key, ratelimit);
ratelimit.timer = this._onAdd(ratelimit);
return ratelimit;
}
}
private _onAdd(ratelimit: Ratelimit): NodeJS.Timer {
return setInterval(() => {
// TODO: work on
}, this.resetInterval)
}, this.resetInterval);
}
}
export type Ratelimit = {
remainingRequests: number,
resetTime: Date,
timer?: NodeJS.Timer
}
remainingRequests: number;
resetTime: Date;
timer?: NodeJS.Timer;
};
export class RatelimitExceededError extends Error {
constructor(message: { toString: () => string }) {
super(message.toString())
this.name = "RatelimitExceededError"
Object.setPrototypeOf(this, RatelimitExceededError.prototype)
super(message.toString());
this.name = "RatelimitExceededError";
Object.setPrototypeOf(this, RatelimitExceededError.prototype);
}
}

View File

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