mirror of
https://github.com/WorldEditAxe/eaglerproxy.git
synced 2024-12-03 05:54:12 -08:00
ran prettier
This commit is contained in:
parent
2059fa8a44
commit
d7e29c5c00
|
@ -1,2 +1,2 @@
|
||||||
const logger = new PLUGIN_MANAGER.Logger("ExamplePlugin")
|
const logger = new PLUGIN_MANAGER.Logger("ExamplePlugin");
|
||||||
logger.info("Hi!")
|
logger.info("Hi!");
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
{
|
{
|
||||||
"name": "Example Plugin",
|
"name": "Example Plugin",
|
||||||
"id": "examplePlugin",
|
"id": "examplePlugin",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"entry_point": "index.js",
|
"entry_point": "index.js",
|
||||||
"requirements": [],
|
"requirements": [],
|
||||||
"incompatibilities": [],
|
"incompatibilities": [],
|
||||||
"load_after": []
|
"load_after": []
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
const path = require("path")
|
const path = require("path");
|
||||||
const os = require("os")
|
const os = require("os");
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
sourceDir: path.resolve(os.homedir(), path.join(process.env.REPL_SLUG, "src"))
|
sourceDir: path.resolve(
|
||||||
}
|
os.homedir(),
|
||||||
|
path.join(process.env.REPL_SLUG, "src")
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
|
@ -1,135 +1,147 @@
|
||||||
// libraries
|
// libraries
|
||||||
const fs = require("fs/promises")
|
const fs = require("fs/promises");
|
||||||
const path = require("path")
|
const path = require("path");
|
||||||
const crypto = require("crypto")
|
const crypto = require("crypto");
|
||||||
const {
|
const { sourceDir } = require("./config.js");
|
||||||
sourceDir
|
|
||||||
} = require("./config.js")
|
|
||||||
|
|
||||||
|
|
||||||
class Logger {
|
class Logger {
|
||||||
constructor({ name, logDebug }) {
|
constructor({ name, logDebug }) {
|
||||||
this.name = name
|
this.name = name;
|
||||||
this.debug = logDebug
|
this.debug = logDebug;
|
||||||
}
|
}
|
||||||
|
|
||||||
_log(logType, data, method) {
|
_log(logType, data, method) {
|
||||||
console[method](`[${this.name}] [${logType}] ${typeof data == "string" ? data : data.toString()}`)
|
console[method](
|
||||||
}
|
`[${this.name}] [${logType}] ${
|
||||||
|
typeof data == "string" ? data : data.toString()
|
||||||
|
}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
info(data) {
|
info(data) {
|
||||||
this._log("info", data, "log")
|
this._log("info", data, "log");
|
||||||
}
|
}
|
||||||
|
|
||||||
warn(data) {
|
warn(data) {
|
||||||
this._log("warn", data, "error")
|
this._log("warn", data, "error");
|
||||||
}
|
}
|
||||||
|
|
||||||
error(data) {
|
error(data) {
|
||||||
this._log("error", data, "error")
|
this._log("error", data, "error");
|
||||||
}
|
}
|
||||||
|
|
||||||
debug(data) {
|
debug(data) {
|
||||||
if (this.debug) {
|
if (this.debug) {
|
||||||
this._log("debug", data, "error")
|
this._log("debug", data, "error");
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function recursiveFileSearch(dir) {
|
async function recursiveFileSearch(dir) {
|
||||||
const fileList = []
|
const fileList = [];
|
||||||
for (const file of await fs.readdir(dir, { withFileTypes: true })) {
|
for (const file of await fs.readdir(dir, { withFileTypes: true })) {
|
||||||
let pathDir = path.resolve(dir, file.name)
|
let pathDir = path.resolve(dir, file.name);
|
||||||
if (file.isFile()) {
|
if (file.isFile()) {
|
||||||
fileList.push(pathDir)
|
fileList.push(pathDir);
|
||||||
} else if (file.isDirectory()) {
|
} else if (file.isDirectory()) {
|
||||||
fileList.push(...(await recursiveFileSearch(pathDir)))
|
fileList.push(...(await recursiveFileSearch(pathDir)));
|
||||||
} else {
|
} else {
|
||||||
logger.warn(`Found directory entry that is neither a file or directory (${pathDir}), ignoring!`)
|
logger.warn(
|
||||||
}
|
`Found directory entry that is neither a file or directory (${pathDir}), ignoring!`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return fileList
|
}
|
||||||
|
return fileList;
|
||||||
}
|
}
|
||||||
|
|
||||||
const logger = new Logger({
|
const logger = new Logger({
|
||||||
name: "launcher",
|
name: "launcher",
|
||||||
logDebug: process.env.DEBUG == "true"
|
logDebug: process.env.DEBUG == "true",
|
||||||
}),
|
}),
|
||||||
LINE_SEPERATOR = "-----------------------------------"
|
LINE_SEPERATOR = "-----------------------------------";
|
||||||
|
|
||||||
if (!process.env.REPL_SLUG) {
|
if (!process.env.REPL_SLUG) {
|
||||||
logger.error(LINE_SEPERATOR)
|
logger.error(LINE_SEPERATOR);
|
||||||
logger.error("Repl not detected!")
|
logger.error("Repl not detected!");
|
||||||
logger.error("")
|
logger.error("");
|
||||||
logger.error("This file is meant to be ran in a Repl")
|
logger.error("This file is meant to be ran in a Repl");
|
||||||
logger.error(LINE_SEPERATOR)
|
logger.error(LINE_SEPERATOR);
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(LINE_SEPERATOR)
|
logger.info(LINE_SEPERATOR);
|
||||||
logger.info("Checking if the proxy needs to be recompiled...")
|
logger.info("Checking if the proxy needs to be recompiled...");
|
||||||
logger.info(LINE_SEPERATOR)
|
logger.info(LINE_SEPERATOR);
|
||||||
|
|
||||||
fs.readFile(path.join(__dirname, ".sourcehash"))
|
fs.readFile(path.join(__dirname, ".sourcehash"))
|
||||||
.then(data => {
|
.then((data) => {
|
||||||
let oldHash = data.toString()
|
let oldHash = data.toString();
|
||||||
logger.info("Found old hash, calculating hash of source files...")
|
logger.info("Found old hash, calculating hash of source files...");
|
||||||
recursiveFileSearch(sourceDir)
|
recursiveFileSearch(sourceDir)
|
||||||
.then(files => {
|
.then((files) => {
|
||||||
Promise.all(files.map(f => fs.readFile(f)))
|
Promise.all(files.map((f) => fs.readFile(f))).then((data) => {
|
||||||
.then(data => {
|
const hash = crypto.createHash("sha256");
|
||||||
const hash = crypto.createHash("sha256")
|
data.forEach((d) => hash.update(d));
|
||||||
data.forEach(d => hash.update(d))
|
let sourceHash = hash.digest().toString();
|
||||||
let sourceHash = hash.digest().toString()
|
|
||||||
|
|
||||||
if (sourceHash === oldHash) {
|
if (sourceHash === oldHash) {
|
||||||
logger.info("Source hasn't been changed, skipping compilation...")
|
logger.info("Source hasn't been changed, skipping compilation...");
|
||||||
process.exit(0)
|
process.exit(0);
|
||||||
} else {
|
} else {
|
||||||
logger.info("Source has been changed, recompiling...")
|
logger.info("Source has been changed, recompiling...");
|
||||||
fs.writeFile(path.join(__dirname, ".sourcehash"), sourceHash)
|
fs.writeFile(path.join(__dirname, ".sourcehash"), sourceHash)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
process.exit(2)
|
process.exit(2);
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch((err) => {
|
||||||
logger.error(`Could not write new hash to disk!\n${err.stack}`)
|
logger.error(`Could not write new hash to disk!\n${err.stack}`);
|
||||||
process.exit(1)
|
process.exit(1);
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch((err) => {
|
||||||
logger.error(`Could not calculate file hashes for files in directory ${sourceDir}!\n${err.stack}`)
|
logger.error(
|
||||||
process.exit(1)
|
`Could not calculate file hashes for files in directory ${sourceDir}!\n${err.stack}`
|
||||||
})
|
);
|
||||||
})
|
process.exit(1);
|
||||||
.catch(err => {
|
});
|
||||||
if (err.code == "ENOENT") {
|
})
|
||||||
logger.warn("Previous source hash not found! Assuming a clean install is being used.")
|
.catch((err) => {
|
||||||
logger.info("Calculating hash...")
|
if (err.code == "ENOENT") {
|
||||||
recursiveFileSearch(sourceDir)
|
logger.warn(
|
||||||
.then(files => {
|
"Previous source hash not found! Assuming a clean install is being used."
|
||||||
Promise.all(files.map(f => fs.readFile(f)))
|
);
|
||||||
.then(data => {
|
logger.info("Calculating hash...");
|
||||||
const hash = crypto.createHash("sha256")
|
recursiveFileSearch(sourceDir)
|
||||||
data.forEach(d => hash.update(d))
|
.then((files) => {
|
||||||
let sourceHash = hash.digest().toString()
|
Promise.all(files.map((f) => fs.readFile(f))).then((data) => {
|
||||||
fs.writeFile(path.join(__dirname, ".sourcehash"), sourceHash)
|
const hash = crypto.createHash("sha256");
|
||||||
.then(() => {
|
data.forEach((d) => hash.update(d));
|
||||||
logger.info("Saved hash to disk.")
|
let sourceHash = hash.digest().toString();
|
||||||
process.exit(2)
|
fs.writeFile(path.join(__dirname, ".sourcehash"), sourceHash)
|
||||||
})
|
.then(() => {
|
||||||
.catch(err => {
|
logger.info("Saved hash to disk.");
|
||||||
logger.error(`Could not write new hash to disk!\n${err.stack}`)
|
process.exit(2);
|
||||||
process.exit(1)
|
})
|
||||||
})
|
.catch((err) => {
|
||||||
})
|
logger.error(`Could not write new hash to disk!\n${err.stack}`);
|
||||||
})
|
process.exit(1);
|
||||||
.catch(err => {
|
});
|
||||||
logger.error(`Could not calculate file hashes for files in directory ${sourceDir}!\n${err.stack}`)
|
});
|
||||||
process.exit(1)
|
})
|
||||||
})
|
.catch((err) => {
|
||||||
} else {
|
logger.error(
|
||||||
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}`)
|
`Could not calculate file hashes for files in directory ${sourceDir}!\n${err.stack}`
|
||||||
process.exit(1)
|
);
|
||||||
}
|
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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"name": "replit_runtime",
|
"name": "replit_runtime",
|
||||||
"description": "Runtime preparation utility for Repls. Determines whether or not a recompilation is necessary.",
|
"description": "Runtime preparation utility for Repls. Determines whether or not a recompilation is necessary.",
|
||||||
"type": "commonjs"
|
"type": "commonjs"
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,30 +4,32 @@
|
||||||
import { Config } from "./launcher_types.js";
|
import { Config } from "./launcher_types.js";
|
||||||
|
|
||||||
export const config: Config = {
|
export const config: Config = {
|
||||||
bridge: {
|
bridge: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
motd: null
|
motd: null,
|
||||||
|
},
|
||||||
|
adapter: {
|
||||||
|
name: "EaglerProxy",
|
||||||
|
bindHost: "0.0.0.0",
|
||||||
|
bindPort: 8080,
|
||||||
|
maxConcurrentClients: 20,
|
||||||
|
skinUrlWhitelist: undefined,
|
||||||
|
motd: true
|
||||||
|
? "FORWARD"
|
||||||
|
: {
|
||||||
|
iconURL: "logo.png",
|
||||||
|
l1: "yes",
|
||||||
|
l2: "no",
|
||||||
|
},
|
||||||
|
origins: {
|
||||||
|
allowOfflineDownloads: true,
|
||||||
|
originWhitelist: null,
|
||||||
|
originBlacklist: null,
|
||||||
},
|
},
|
||||||
adapter: {
|
server: {
|
||||||
name: "EaglerProxy",
|
host: "no",
|
||||||
bindHost: "0.0.0.0",
|
port: 46625,
|
||||||
bindPort: 8080,
|
},
|
||||||
maxConcurrentClients: 20,
|
tls: undefined,
|
||||||
skinUrlWhitelist: undefined,
|
},
|
||||||
motd: true ? "FORWARD" : {
|
};
|
||||||
iconURL: "logo.png",
|
|
||||||
l1: "yes",
|
|
||||||
l2: "no"
|
|
||||||
},
|
|
||||||
origins: {
|
|
||||||
allowOfflineDownloads: true,
|
|
||||||
originWhitelist: null,
|
|
||||||
originBlacklist: null
|
|
||||||
},
|
|
||||||
server: {
|
|
||||||
host: "no",
|
|
||||||
port: 46625
|
|
||||||
},
|
|
||||||
tls: undefined
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
17
src/globals.d.ts
vendored
17
src/globals.d.ts
vendored
|
@ -4,10 +4,13 @@ import { Config } from "./launcher_types.js";
|
||||||
import { PluginManager } from "./proxy/pluginLoader/PluginManager.js";
|
import { PluginManager } from "./proxy/pluginLoader/PluginManager.js";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
var CONFIG: Config
|
var CONFIG: Config;
|
||||||
var PROXY: Proxy
|
var PROXY: Proxy;
|
||||||
var PLUGIN_MANAGER: PluginManager
|
var PLUGIN_MANAGER: PluginManager;
|
||||||
var PACKET_REGISTRY: Map<number, Packet & {
|
var PACKET_REGISTRY: Map<
|
||||||
class: any
|
number,
|
||||||
}>
|
Packet & {
|
||||||
}
|
class: any;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
}
|
||||||
|
|
28
src/index.ts
28
src/index.ts
|
@ -2,26 +2,28 @@ import * as dotenv from "dotenv";
|
||||||
import process from "process";
|
import process from "process";
|
||||||
import { Proxy } from "./proxy/Proxy.js";
|
import { Proxy } from "./proxy/Proxy.js";
|
||||||
import { config } from "./config.js";
|
import { config } from "./config.js";
|
||||||
dotenv.config()
|
dotenv.config();
|
||||||
import { Logger } from "./logger.js";
|
import { Logger } from "./logger.js";
|
||||||
import { PROXY_BRANDING } from "./meta.js";
|
import { PROXY_BRANDING } from "./meta.js";
|
||||||
import { PluginManager } from "./proxy/pluginLoader/PluginManager.js";
|
import { PluginManager } from "./proxy/pluginLoader/PluginManager.js";
|
||||||
import { dirname, join } from "path";
|
import { dirname, join } from "path";
|
||||||
import { fileURLToPath } from "url";
|
import { fileURLToPath } from "url";
|
||||||
|
|
||||||
const logger = new Logger("Launcher")
|
const logger = new Logger("Launcher");
|
||||||
let proxy: Proxy
|
let proxy: Proxy;
|
||||||
|
|
||||||
global.CONFIG = config
|
global.CONFIG = config;
|
||||||
|
|
||||||
logger.info("Loading plugins...")
|
logger.info("Loading plugins...");
|
||||||
const pluginManager = new PluginManager(join(dirname(fileURLToPath(import.meta.url)), "plugins"))
|
const pluginManager = new PluginManager(
|
||||||
global.PLUGIN_MANAGER = pluginManager
|
join(dirname(fileURLToPath(import.meta.url)), "plugins")
|
||||||
await pluginManager.loadPlugins()
|
);
|
||||||
|
global.PLUGIN_MANAGER = pluginManager;
|
||||||
|
await pluginManager.loadPlugins();
|
||||||
|
|
||||||
proxy = new Proxy(config.adapter, pluginManager)
|
proxy = new Proxy(config.adapter, pluginManager);
|
||||||
pluginManager.proxy = proxy
|
pluginManager.proxy = proxy;
|
||||||
|
|
||||||
logger.info(`Launching ${PROXY_BRANDING}...`)
|
logger.info(`Launching ${PROXY_BRANDING}...`);
|
||||||
await proxy.init()
|
await proxy.init();
|
||||||
global.PROXY = proxy
|
global.PROXY = proxy;
|
||||||
|
|
|
@ -1,40 +1,44 @@
|
||||||
export type Config = {
|
export type Config = {
|
||||||
bridge: BridgeOptions,
|
bridge: BridgeOptions;
|
||||||
adapter: AdapterOptions
|
adapter: AdapterOptions;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type BridgeOptions = {
|
export type BridgeOptions = {
|
||||||
enabled: boolean,
|
enabled: boolean;
|
||||||
motd: 'FORWARD' | {
|
motd:
|
||||||
iconURL?: string,
|
| "FORWARD"
|
||||||
l1: string,
|
| {
|
||||||
l2?: string
|
iconURL?: string;
|
||||||
}
|
l1: string;
|
||||||
}
|
l2?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export type AdapterOptions = {
|
export type AdapterOptions = {
|
||||||
name: "EaglerProxy",
|
name: "EaglerProxy";
|
||||||
bindHost: string,
|
bindHost: string;
|
||||||
bindPort: number,
|
bindPort: number;
|
||||||
maxConcurrentClients: 20,
|
maxConcurrentClients: 20;
|
||||||
skinUrlWhitelist?: string[],
|
skinUrlWhitelist?: string[];
|
||||||
origins: {
|
origins: {
|
||||||
allowOfflineDownloads: boolean,
|
allowOfflineDownloads: boolean;
|
||||||
originWhitelist: string[],
|
originWhitelist: string[];
|
||||||
originBlacklist: string[]
|
originBlacklist: string[];
|
||||||
},
|
};
|
||||||
motd: 'FORWARD' | {
|
motd:
|
||||||
iconURL?: string,
|
| "FORWARD"
|
||||||
l1: string,
|
| {
|
||||||
l2?: string
|
iconURL?: string;
|
||||||
},
|
l1: string;
|
||||||
server: {
|
l2?: string;
|
||||||
host: string,
|
};
|
||||||
port: number
|
server: {
|
||||||
},
|
host: string;
|
||||||
tls?: {
|
port: number;
|
||||||
enabled: boolean,
|
};
|
||||||
key: null,
|
tls?: {
|
||||||
cert: null
|
enabled: boolean;
|
||||||
}
|
key: null;
|
||||||
}
|
cert: null;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
114
src/logger.ts
114
src/logger.ts
|
@ -1,63 +1,95 @@
|
||||||
import { Chalk } from "chalk"
|
import { Chalk } from "chalk";
|
||||||
|
|
||||||
const color = new Chalk({ level: 2 })
|
const color = new Chalk({ level: 2 });
|
||||||
|
|
||||||
let global_verbose: boolean = false
|
let global_verbose: boolean = false;
|
||||||
|
|
||||||
type JsonLogType = "info" | "warn" | "error" | "fatal" | "debug"
|
type JsonLogType = "info" | "warn" | "error" | "fatal" | "debug";
|
||||||
type JsonOutput = {
|
type JsonOutput = {
|
||||||
type: JsonLogType,
|
type: JsonLogType;
|
||||||
message: string
|
message: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
export function verboseLogging(newVal?: boolean) {
|
export function verboseLogging(newVal?: boolean) {
|
||||||
global_verbose = newVal ?? global_verbose ? false : true
|
global_verbose = newVal ?? global_verbose ? false : true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function jsonLog(type: JsonLogType, message: string): string {
|
function jsonLog(type: JsonLogType, message: string): string {
|
||||||
return JSON.stringify({
|
return (
|
||||||
type: type,
|
JSON.stringify({
|
||||||
message: message
|
type: type,
|
||||||
|
message: message,
|
||||||
}) + "\n"
|
}) + "\n"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Logger {
|
export class Logger {
|
||||||
loggerName: string
|
loggerName: string;
|
||||||
verbose: boolean
|
verbose: boolean;
|
||||||
private jsonLog: boolean = process.argv.includes("--json") || process.argv.includes("-j")
|
private jsonLog: boolean =
|
||||||
|
process.argv.includes("--json") || process.argv.includes("-j");
|
||||||
|
|
||||||
constructor(name: string, verbose?: boolean) {
|
constructor(name: string, verbose?: boolean) {
|
||||||
this.loggerName = name
|
this.loggerName = name;
|
||||||
if (verbose) this.verbose = verbose
|
if (verbose) this.verbose = verbose;
|
||||||
else this.verbose = global_verbose
|
else this.verbose = global_verbose;
|
||||||
}
|
}
|
||||||
|
|
||||||
info(s: string) {
|
info(s: string) {
|
||||||
if (!this.jsonLog) process.stdout.write(`${color.green("I")} ${color.gray(new Date().toISOString())} ${color.reset(`${color.yellow(`${this.loggerName}:`)} ${s}`)}\n`)
|
if (!this.jsonLog)
|
||||||
else process.stdout.write(jsonLog("info", s))
|
process.stdout.write(
|
||||||
}
|
`${color.green("I")} ${color.gray(
|
||||||
|
new Date().toISOString()
|
||||||
|
)} ${color.reset(`${color.yellow(`${this.loggerName}:`)} ${s}`)}\n`
|
||||||
|
);
|
||||||
|
else process.stdout.write(jsonLog("info", s));
|
||||||
|
}
|
||||||
|
|
||||||
warn(s: string) {
|
warn(s: string) {
|
||||||
if (!this.jsonLog) process.stdout.write(`${color.yellow("W")} ${color.gray(new Date().toISOString())} ${color.yellow(`${color.yellow(`${this.loggerName}:`)} ${s}`)}\n`)
|
if (!this.jsonLog)
|
||||||
else process.stderr.write(jsonLog("warn", s))
|
process.stdout.write(
|
||||||
}
|
`${color.yellow("W")} ${color.gray(
|
||||||
|
new Date().toISOString()
|
||||||
|
)} ${color.yellow(`${color.yellow(`${this.loggerName}:`)} ${s}`)}\n`
|
||||||
|
);
|
||||||
|
else process.stderr.write(jsonLog("warn", s));
|
||||||
|
}
|
||||||
|
|
||||||
error(s: string) {
|
error(s: string) {
|
||||||
if (!this.jsonLog) process.stderr.write(`* ${color.red("E")} ${color.gray(new Date().toISOString())} ${color.redBright(`${color.red(`${this.loggerName}:`)} ${s}`)}\n`)
|
if (!this.jsonLog)
|
||||||
else process.stderr.write(jsonLog("error", s))
|
process.stderr.write(
|
||||||
}
|
`* ${color.red("E")} ${color.gray(
|
||||||
|
new Date().toISOString()
|
||||||
|
)} ${color.redBright(`${color.red(`${this.loggerName}:`)} ${s}`)}\n`
|
||||||
|
);
|
||||||
|
else process.stderr.write(jsonLog("error", s));
|
||||||
|
}
|
||||||
|
|
||||||
fatal(s: string) {
|
fatal(s: string) {
|
||||||
if (!this.jsonLog) process.stderr.write(`** ${color.red("F!")} ${color.gray(new Date().toISOString())} ${color.bgRedBright(color.redBright(`${color.red(`${this.loggerName}:`)} ${s}`))}\n`)
|
if (!this.jsonLog)
|
||||||
else process.stderr.write(jsonLog("fatal", s))
|
process.stderr.write(
|
||||||
}
|
`** ${color.red("F!")} ${color.gray(
|
||||||
|
new Date().toISOString()
|
||||||
|
)} ${color.bgRedBright(
|
||||||
|
color.redBright(`${color.red(`${this.loggerName}:`)} ${s}`)
|
||||||
|
)}\n`
|
||||||
|
);
|
||||||
|
else process.stderr.write(jsonLog("fatal", s));
|
||||||
|
}
|
||||||
|
|
||||||
debug(s: string) {
|
debug(s: string) {
|
||||||
if (this.verbose || global_verbose) {
|
if (this.verbose || global_verbose) {
|
||||||
if (!this.jsonLog) process.stderr.write(`${color.gray("D")} ${color.gray(new Date().toISOString())} ${color.gray(`${color.gray(`${this.loggerName}:`)} ${s}`)}\n`)
|
if (!this.jsonLog)
|
||||||
else process.stderr.write(jsonLog("debug", s))
|
process.stderr.write(
|
||||||
}
|
`${color.gray("D")} ${color.gray(
|
||||||
|
new Date().toISOString()
|
||||||
|
)} ${color.gray(`${color.gray(`${this.loggerName}:`)} ${s}`)}\n`
|
||||||
|
);
|
||||||
|
else process.stderr.write(jsonLog("debug", s));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
verboseLogging(process.env.DEBUG != null && process.env.DEBUG != "false" ? true : false)
|
verboseLogging(
|
||||||
|
process.env.DEBUG != null && process.env.DEBUG != "false" ? true : false
|
||||||
|
);
|
||||||
|
|
12
src/meta.ts
12
src/meta.ts
|
@ -1,11 +1,11 @@
|
||||||
const f = Object.freeze
|
const f = Object.freeze;
|
||||||
|
|
||||||
// bridge meta
|
// bridge meta
|
||||||
export const BRIDGE_VERSION: Readonly<number> = f(1)
|
export const BRIDGE_VERSION: Readonly<number> = f(1);
|
||||||
|
|
||||||
// adapter meta
|
// adapter meta
|
||||||
export const PROXY_BRANDING: Readonly<string> = f("EaglerProxy")
|
export const PROXY_BRANDING: Readonly<string> = f("EaglerProxy");
|
||||||
export const PROXY_VERSION: Readonly<string> = f("1.0.7")
|
export const PROXY_VERSION: Readonly<string> = f("1.0.7");
|
||||||
|
|
||||||
export const NETWORK_VERSION: Readonly<number> = f(0x03)
|
export const NETWORK_VERSION: Readonly<number> = f(0x03);
|
||||||
export const VANILLA_PROTOCOL_VERSION: Readonly<number> = f(47)
|
export const VANILLA_PROTOCOL_VERSION: Readonly<number> = f(47);
|
||||||
|
|
|
@ -1,64 +1,85 @@
|
||||||
import { randomUUID } from "crypto"
|
import { randomUUID } from "crypto";
|
||||||
import EventEmitter from "events"
|
import EventEmitter from "events";
|
||||||
import pauth from "prismarine-auth"
|
import pauth from "prismarine-auth";
|
||||||
import debug from "debug"
|
import debug from "debug";
|
||||||
|
|
||||||
const { Authflow, Titles } = pauth;
|
const { Authflow, Titles } = pauth;
|
||||||
const Enums = PLUGIN_MANAGER.Enums
|
const Enums = PLUGIN_MANAGER.Enums;
|
||||||
|
|
||||||
export type ServerDeviceCodeResponse = {
|
export type ServerDeviceCodeResponse = {
|
||||||
user_code: string
|
user_code: string;
|
||||||
device_code: string
|
device_code: string;
|
||||||
verification_uri: string
|
verification_uri: string;
|
||||||
expires_in: number
|
expires_in: number;
|
||||||
interval: number
|
interval: number;
|
||||||
message: string
|
message: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
class InMemoryCache {
|
class InMemoryCache {
|
||||||
private cache = {}
|
private cache = {};
|
||||||
async getCached () {
|
async getCached() {
|
||||||
return this.cache
|
return this.cache;
|
||||||
}
|
}
|
||||||
async setCached (value) {
|
async setCached(value) {
|
||||||
this.cache = value
|
this.cache = value;
|
||||||
}
|
}
|
||||||
async setCachedPartial (value) {
|
async setCachedPartial(value) {
|
||||||
this.cache = {
|
this.cache = {
|
||||||
...this.cache,
|
...this.cache,
|
||||||
...value
|
...value,
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function auth(): EventEmitter {
|
export function auth(): EventEmitter {
|
||||||
const emitter = new EventEmitter()
|
const emitter = new EventEmitter();
|
||||||
const userIdentifier = randomUUID()
|
const userIdentifier = randomUUID();
|
||||||
const flow = new Authflow(userIdentifier, ({ username, cacheName }) => new InMemoryCache(), {
|
const flow = new Authflow(
|
||||||
authTitle: Titles.MinecraftJava,
|
userIdentifier,
|
||||||
flow: 'sisu',
|
({ username, cacheName }) => new InMemoryCache(),
|
||||||
deviceType: "Win32"
|
{
|
||||||
}, code => {
|
authTitle: Titles.MinecraftJava,
|
||||||
console.log = () => {}
|
flow: "sisu",
|
||||||
emitter.emit('code', code)
|
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", {
|
||||||
|
accessToken: data.token,
|
||||||
|
expiresOn: _data.obtainedOn + _data.expires_in * 1000,
|
||||||
|
selectedProfile: data.profile,
|
||||||
|
availableProfiles: [data.profile],
|
||||||
|
});
|
||||||
})
|
})
|
||||||
flow.getMinecraftJavaToken({ fetchProfile: true })
|
.catch((err) => {
|
||||||
.then(async data => {
|
if (err.toString().includes("Not Found"))
|
||||||
const _data = (await (flow as any).mca.cache.getCached()).mca
|
emitter.emit(
|
||||||
if (data.profile == null || (data.profile as any).error)
|
"error",
|
||||||
return emitter.emit('error', new Error(Enums.ChatColor.RED + "Couldn't fetch profile data, does the account own Minecraft: Java Edition?"))
|
new Error(
|
||||||
emitter.emit('done', {
|
Enums.ChatColor.RED +
|
||||||
accessToken: data.token,
|
"The provided account doesn't own Minecraft: Java Edition!"
|
||||||
expiresOn: _data.obtainedOn + _data.expires_in * 1000,
|
)
|
||||||
selectedProfile: data.profile,
|
);
|
||||||
availableProfiles: [data.profile]
|
else
|
||||||
})
|
emitter.emit(
|
||||||
})
|
"error",
|
||||||
.catch(err => {
|
new Error(Enums.ChatColor.YELLOW + err.toString())
|
||||||
if (err.toString().includes("Not Found"))
|
);
|
||||||
emitter.emit('error', new Error(Enums.ChatColor.RED + "The provided account doesn't own Minecraft: Java Edition!"))
|
});
|
||||||
else
|
return emitter;
|
||||||
emitter.emit('error', new Error(Enums.ChatColor.YELLOW + err.toString()))
|
}
|
||||||
})
|
|
||||||
return emitter
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
export const config = {
|
export const config = {
|
||||||
bindInternalServerPort: 25569,
|
bindInternalServerPort: 25569,
|
||||||
bindInternalServerIp: "127.0.0.1",
|
bindInternalServerIp: "127.0.0.1",
|
||||||
allowCustomPorts: false,
|
allowCustomPorts: true,
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,60 +1,68 @@
|
||||||
import metadata from "./metadata.json" assert { type: "json" }
|
import metadata from "./metadata.json" assert { type: "json" };
|
||||||
import { config } from "./config.js"
|
import { config } from "./config.js";
|
||||||
import { createServer } from "minecraft-protocol"
|
import { createServer } from "minecraft-protocol";
|
||||||
import { ClientState, ConnectionState, ServerGlobals } from "./types.js"
|
import { ClientState, ConnectionState, ServerGlobals } from "./types.js";
|
||||||
import { handleConnect, setSG } from "./utils.js"
|
import { handleConnect, setSG } from "./utils.js";
|
||||||
|
|
||||||
const PluginManager = PLUGIN_MANAGER
|
const PluginManager = PLUGIN_MANAGER;
|
||||||
|
|
||||||
const Logger = PluginManager.Logger
|
const Logger = PluginManager.Logger;
|
||||||
const Enums = PluginManager.Enums
|
const Enums = PluginManager.Enums;
|
||||||
const Chat = PluginManager.Chat
|
const Chat = PluginManager.Chat;
|
||||||
const Constants = PluginManager.Constants
|
const Constants = PluginManager.Constants;
|
||||||
const Motd = PluginManager.Motd
|
const Motd = PluginManager.Motd;
|
||||||
const Player = PluginManager.Player
|
const Player = PluginManager.Player;
|
||||||
const MineProtocol = PluginManager.MineProtocol
|
const MineProtocol = PluginManager.MineProtocol;
|
||||||
const EaglerSkins = PluginManager.EaglerSkins
|
const EaglerSkins = PluginManager.EaglerSkins;
|
||||||
const Util = PluginManager.Util
|
const Util = PluginManager.Util;
|
||||||
|
|
||||||
const logger = new Logger("EaglerProxyAAS")
|
const logger = new Logger("EaglerProxyAAS");
|
||||||
logger.info(`Starting ${metadata.name} v${metadata.version}...`)
|
logger.info(`Starting ${metadata.name} v${metadata.version}...`);
|
||||||
logger.info(`(internal server port: ${config.bindInternalServerPort}, internal server IP: ${config.bindInternalServerPort})`)
|
logger.info(
|
||||||
|
`(internal server port: ${config.bindInternalServerPort}, internal server IP: ${config.bindInternalServerPort})`
|
||||||
|
);
|
||||||
|
|
||||||
logger.info("Starting internal server...")
|
logger.info("Starting internal server...");
|
||||||
let server = createServer({
|
let server = createServer({
|
||||||
host: config.bindInternalServerIp,
|
host: config.bindInternalServerIp,
|
||||||
port: config.bindInternalServerPort,
|
port: config.bindInternalServerPort,
|
||||||
motdMsg: `${Enums.ChatColor.GOLD}EaglerProxy as a Service`,
|
motdMsg: `${Enums.ChatColor.GOLD}EaglerProxy as a Service`,
|
||||||
"online-mode": false,
|
"online-mode": false,
|
||||||
version: '1.8.9'
|
version: "1.8.9",
|
||||||
}), sGlobals: ServerGlobals = {
|
}),
|
||||||
|
sGlobals: ServerGlobals = {
|
||||||
server: server,
|
server: server,
|
||||||
players: new Map()
|
players: new Map(),
|
||||||
}
|
};
|
||||||
setSG(sGlobals)
|
setSG(sGlobals);
|
||||||
|
|
||||||
server.on('login', client => {
|
server.on("login", (client) => {
|
||||||
logger.info(`Client ${client.username} has connected to the authentication server.`)
|
logger.info(
|
||||||
client.on('end', () => {
|
`Client ${client.username} has connected to the authentication server.`
|
||||||
sGlobals.players.delete(client.username)
|
);
|
||||||
logger.info(`Client ${client.username} has disconnected from the authentication server.`)
|
client.on("end", () => {
|
||||||
})
|
sGlobals.players.delete(client.username);
|
||||||
const cs: ClientState = {
|
logger.info(
|
||||||
state: ConnectionState.AUTH,
|
`Client ${client.username} has disconnected from the authentication server.`
|
||||||
gameClient: client,
|
);
|
||||||
token: null,
|
});
|
||||||
lastStatusUpdate: null
|
const cs: ClientState = {
|
||||||
}
|
state: ConnectionState.AUTH,
|
||||||
sGlobals.players.set(client.username, cs)
|
gameClient: client,
|
||||||
handleConnect(cs)
|
token: null,
|
||||||
})
|
lastStatusUpdate: null,
|
||||||
|
};
|
||||||
|
sGlobals.players.set(client.username, cs);
|
||||||
|
handleConnect(cs);
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.info(
|
||||||
logger.info("Redirecting backend server IP... (this is required for the plugin to function)")
|
"Redirecting backend server IP... (this is required for the plugin to function)"
|
||||||
|
);
|
||||||
CONFIG.adapter.server = {
|
CONFIG.adapter.server = {
|
||||||
host: config.bindInternalServerIp,
|
host: config.bindInternalServerIp,
|
||||||
port: config.bindInternalServerPort
|
port: config.bindInternalServerPort,
|
||||||
}
|
};
|
||||||
CONFIG.adapter.motd = {
|
CONFIG.adapter.motd = {
|
||||||
l1: Enums.ChatColor.GOLD + "EaglerProxy as a Service"
|
l1: Enums.ChatColor.GOLD + "EaglerProxy as a Service",
|
||||||
}
|
};
|
||||||
|
|
|
@ -1,27 +1,34 @@
|
||||||
{
|
{
|
||||||
"name": "EaglerProxy as a Service",
|
"name": "EaglerProxy as a Service",
|
||||||
"id": "eagpaas",
|
"id": "eagpaas",
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"entry_point": "index.js",
|
"entry_point": "index.js",
|
||||||
"requirements": [{
|
"requirements": [
|
||||||
"id": "eaglerproxy",
|
{
|
||||||
"version": "any"
|
"id": "eaglerproxy",
|
||||||
}, {
|
"version": "any"
|
||||||
"id": "module:vec3",
|
},
|
||||||
"version": "^0.1.0"
|
{
|
||||||
}, {
|
"id": "module:vec3",
|
||||||
"id": "module:prismarine-chunk",
|
"version": "^0.1.0"
|
||||||
"version": "^1.33.0"
|
},
|
||||||
}, {
|
{
|
||||||
"id": "module:prismarine-block",
|
"id": "module:prismarine-chunk",
|
||||||
"version": "^1.16.0"
|
"version": "^1.33.0"
|
||||||
}, {
|
},
|
||||||
"id": "module:prismarine-registry",
|
{
|
||||||
"version": "^1.6.0"
|
"id": "module:prismarine-block",
|
||||||
}, {
|
"version": "^1.16.0"
|
||||||
"id": "module:minecraft-protocol",
|
},
|
||||||
"version": "^1.40.0"
|
{
|
||||||
}],
|
"id": "module:prismarine-registry",
|
||||||
"load_after": [],
|
"version": "^1.6.0"
|
||||||
"incompatibilities": []
|
},
|
||||||
|
{
|
||||||
|
"id": "module:minecraft-protocol",
|
||||||
|
"version": "^1.40.0"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"load_after": [],
|
||||||
|
"incompatibilities": []
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,6 +45,7 @@ export enum ChatColor {
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ConnectType {
|
export enum ConnectType {
|
||||||
ONLINE,
|
ONLINE = "ONLINE",
|
||||||
OFFLINE,
|
OFFLINE = "OFFLINE",
|
||||||
|
EASYMC = "EASYMC",
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,8 @@ import { Client } from "minecraft-protocol";
|
||||||
import { ClientState, ConnectionState } from "./types.js";
|
import { ClientState, ConnectionState } from "./types.js";
|
||||||
import { auth, ServerDeviceCodeResponse } from "./auth.js";
|
import { auth, ServerDeviceCodeResponse } from "./auth.js";
|
||||||
import { config } from "./config.js";
|
import { config } from "./config.js";
|
||||||
|
import { handleCommand } from "./commands.js";
|
||||||
|
import { getTokenProfileEasyMc } from "./auth_easymc.js";
|
||||||
|
|
||||||
const { Vec3 } = vec3 as any;
|
const { Vec3 } = vec3 as any;
|
||||||
const Enums = PLUGIN_MANAGER.Enums;
|
const Enums = PLUGIN_MANAGER.Enums;
|
||||||
|
@ -50,7 +52,7 @@ export function handleConnect(client: ClientState) {
|
||||||
client.gameClient.write("login", {
|
client.gameClient.write("login", {
|
||||||
entityId: 1,
|
entityId: 1,
|
||||||
gameMode: 2,
|
gameMode: 2,
|
||||||
dimension: 0,
|
dimension: 1,
|
||||||
difficulty: 1,
|
difficulty: 1,
|
||||||
maxPlayers: 1,
|
maxPlayers: 1,
|
||||||
levelType: "flat",
|
levelType: "flat",
|
||||||
|
@ -190,7 +192,7 @@ export function sendMessageLogin(client: Client, url: string, token: string) {
|
||||||
|
|
||||||
export function updateState(
|
export function updateState(
|
||||||
client: Client,
|
client: Client,
|
||||||
newState: "CONNECTION_TYPE" | "AUTH" | "SERVER",
|
newState: "CONNECTION_TYPE" | "AUTH_EASYMC" | "AUTH" | "SERVER",
|
||||||
uri?: string,
|
uri?: string,
|
||||||
code?: string
|
code?: string
|
||||||
) {
|
) {
|
||||||
|
@ -201,7 +203,17 @@ export function updateState(
|
||||||
text: ` ${Enums.ChatColor.GOLD}EaglerProxy Authentication Server `,
|
text: ` ${Enums.ChatColor.GOLD}EaglerProxy Authentication Server `,
|
||||||
}),
|
}),
|
||||||
footer: JSON.stringify({
|
footer: JSON.stringify({
|
||||||
text: `${Enums.ChatColor.RED}Choose the connection type: 1 = online, 2 = offline.`,
|
text: `${Enums.ChatColor.RED}Choose the connection type: 1 = online, 2 = offline, 3 = EasyMC.`,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case "AUTH_EASYMC":
|
||||||
|
client.write("playerlist_header", {
|
||||||
|
header: JSON.stringify({
|
||||||
|
text: ` ${Enums.ChatColor.GOLD}EaglerProxy Authentication Server `,
|
||||||
|
}),
|
||||||
|
footer: JSON.stringify({
|
||||||
|
text: `${Enums.ChatColor.RED}easymc.io/get${Enums.ChatColor.GOLD} | ${Enums.ChatColor.RED}/login <alt_token>`,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
@ -234,6 +246,18 @@ export function updateState(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// assuming that the player will always stay at the same pos
|
||||||
|
export function playSelectSound(client: Client) {
|
||||||
|
client.write("named_sound_effect", {
|
||||||
|
soundName: "note.hat",
|
||||||
|
x: 8.5,
|
||||||
|
y: 65,
|
||||||
|
z: 8.5,
|
||||||
|
volume: 100,
|
||||||
|
pitch: 63,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export async function onConnect(client: ClientState) {
|
export async function onConnect(client: ClientState) {
|
||||||
try {
|
try {
|
||||||
client.state = ConnectionState.AUTH;
|
client.state = ConnectionState.AUTH;
|
||||||
|
@ -267,7 +291,7 @@ export async function onConnect(client: ClientState) {
|
||||||
},
|
},
|
||||||
clickEvent: {
|
clickEvent: {
|
||||||
action: "run_command",
|
action: "run_command",
|
||||||
value: "1",
|
value: "$1",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
sendChatComponent(client.gameClient, {
|
sendChatComponent(client.gameClient, {
|
||||||
|
@ -285,12 +309,30 @@ export async function onConnect(client: ClientState) {
|
||||||
},
|
},
|
||||||
clickEvent: {
|
clickEvent: {
|
||||||
action: "run_command",
|
action: "run_command",
|
||||||
value: "2",
|
value: "$2",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
sendChatComponent(client.gameClient, {
|
||||||
|
text: "3) ",
|
||||||
|
color: "gold",
|
||||||
|
extra: [
|
||||||
|
{
|
||||||
|
text: "Connect to an online server via EasyMC account pool (no Minecraft account needed)",
|
||||||
|
color: "white",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
hoverEvent: {
|
||||||
|
action: "show_text",
|
||||||
|
value: Enums.ChatColor.GOLD + "Click me to select!",
|
||||||
|
},
|
||||||
|
clickEvent: {
|
||||||
|
action: "run_command",
|
||||||
|
value: "$3",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
sendCustomMessage(
|
sendCustomMessage(
|
||||||
client.gameClient,
|
client.gameClient,
|
||||||
"Select an option from the above (1 = online, 2 = offline), either by clicking or manually typing out the option.",
|
"Select an option from the above (1 = online, 2 = offline, 3 = EasyMC), either by clicking or manually typing out the option.",
|
||||||
"green"
|
"green"
|
||||||
);
|
);
|
||||||
updateState(client.gameClient, "CONNECTION_TYPE");
|
updateState(client.gameClient, "CONNECTION_TYPE");
|
||||||
|
@ -298,21 +340,28 @@ export async function onConnect(client: ClientState) {
|
||||||
let chosenOption: ConnectType | null = null;
|
let chosenOption: ConnectType | null = null;
|
||||||
while (true) {
|
while (true) {
|
||||||
const option = await awaitCommand(client.gameClient, (msg) => true);
|
const option = await awaitCommand(client.gameClient, (msg) => true);
|
||||||
switch (option) {
|
switch (option.replace(/\$/gim, "")) {
|
||||||
default:
|
default:
|
||||||
sendCustomMessage(
|
sendCustomMessage(
|
||||||
client.gameClient,
|
client.gameClient,
|
||||||
`I don't understand what you meant by "${option}", please reply with a valid option!`,
|
`I don't understand what you meant by "${option}", please reply with a valid option!`,
|
||||||
"red"
|
"red"
|
||||||
);
|
);
|
||||||
|
break;
|
||||||
case "1":
|
case "1":
|
||||||
chosenOption = ConnectType.ONLINE;
|
chosenOption = ConnectType.ONLINE;
|
||||||
break;
|
break;
|
||||||
case "2":
|
case "2":
|
||||||
chosenOption = ConnectType.OFFLINE;
|
chosenOption = ConnectType.OFFLINE;
|
||||||
break;
|
break;
|
||||||
|
case "3":
|
||||||
|
chosenOption = ConnectType.EASYMC;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (chosenOption != null) {
|
||||||
|
if (option.startsWith("$")) playSelectSound(client.gameClient);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
if (chosenOption != null) break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (chosenOption == ConnectType.ONLINE) {
|
if (chosenOption == ConnectType.ONLINE) {
|
||||||
|
@ -347,6 +396,7 @@ export async function onConnect(client: ClientState) {
|
||||||
authHandler.on("code", codeCallback);
|
authHandler.on("code", codeCallback);
|
||||||
await new Promise((res) =>
|
await new Promise((res) =>
|
||||||
authHandler.once("done", (result) => {
|
authHandler.once("done", (result) => {
|
||||||
|
console.log(result);
|
||||||
savedAuth = result;
|
savedAuth = result;
|
||||||
res(result);
|
res(result);
|
||||||
})
|
})
|
||||||
|
@ -407,26 +457,81 @@ export async function onConnect(client: ClientState) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await PLUGIN_MANAGER.proxy.players
|
sendChatComponent(client.gameClient, {
|
||||||
.get(client.gameClient.username)
|
text: `Joining server under ${savedAuth.selectedProfile.name}/your Minecraft account's username! Run `,
|
||||||
.switchServers({
|
color: "aqua",
|
||||||
host: host,
|
extra: [
|
||||||
port: port,
|
{
|
||||||
version: "1.8.8",
|
text: "/eag-help",
|
||||||
username: savedAuth.selectedProfile.name,
|
color: "gold",
|
||||||
auth: "mojang",
|
hoverEvent: {
|
||||||
keepAlive: false,
|
action: "show_text",
|
||||||
session: {
|
value: Enums.ChatColor.GOLD + "Click me to run this command!",
|
||||||
accessToken: savedAuth.accessToken,
|
},
|
||||||
clientToken: savedAuth.selectedProfile.id,
|
clickEvent: {
|
||||||
selectedProfile: {
|
action: "run_command",
|
||||||
id: savedAuth.selectedProfile.id,
|
value: "/eag-help",
|
||||||
name: savedAuth.selectedProfile.name,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
skipValidation: true,
|
{
|
||||||
hideErrors: true,
|
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",
|
||||||
|
username: savedAuth.selectedProfile.name,
|
||||||
|
auth: "mojang",
|
||||||
|
keepAlive: false,
|
||||||
|
session: {
|
||||||
|
accessToken: savedAuth.accessToken,
|
||||||
|
clientToken: savedAuth.selectedProfile.id,
|
||||||
|
selectedProfile: {
|
||||||
|
id: savedAuth.selectedProfile.id,
|
||||||
|
name: savedAuth.selectedProfile.name,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
skipValidation: true,
|
||||||
|
hideErrors: true,
|
||||||
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!client.gameClient.ended) {
|
if (!client.gameClient.ended) {
|
||||||
client.gameClient.end(
|
client.gameClient.end(
|
||||||
|
@ -441,7 +546,169 @@ export async function onConnect(client: ClientState) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} 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.state = ConnectionState.SUCCESS;
|
||||||
client.lastStatusUpdate = Date.now();
|
client.lastStatusUpdate = Date.now();
|
||||||
updateState(client.gameClient, "SERVER");
|
updateState(client.gameClient, "SERVER");
|
||||||
|
@ -493,23 +760,63 @@ export async function onConnect(client: ClientState) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
sendChatComponent(client.gameClient, {
|
||||||
|
text: `Joining server under ${appendOptions.username}/EasyMC account username! Run `,
|
||||||
|
color: "aqua",
|
||||||
|
extra: [
|
||||||
|
{
|
||||||
|
text: "/eag-help",
|
||||||
|
color: "gold",
|
||||||
|
hoverEvent: {
|
||||||
|
action: "show_text",
|
||||||
|
value: Enums.ChatColor.GOLD + "Click me to run this command!",
|
||||||
|
},
|
||||||
|
clickEvent: {
|
||||||
|
action: "run_command",
|
||||||
|
value: "/eag-help",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: " for a list of proxy commands.",
|
||||||
|
color: "aqua",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
sendCustomMessage(
|
sendCustomMessage(
|
||||||
client.gameClient,
|
client.gameClient,
|
||||||
"Attempting to switch servers, please wait... (if you don't get connected to the target server after a while, the server might be online only)",
|
"Attempting to switch servers, please wait... (if you don't get connected to the target server for a while, the server might be online only)",
|
||||||
"gray"
|
"gray"
|
||||||
);
|
);
|
||||||
await PLUGIN_MANAGER.proxy.players
|
const player = PLUGIN_MANAGER.proxy.players.get(
|
||||||
.get(client.gameClient.username)
|
client.gameClient.username
|
||||||
.switchServers({
|
);
|
||||||
host: host,
|
player.on("vanillaPacket", (packet, origin) => {
|
||||||
port: port,
|
if (
|
||||||
version: "1.8.8",
|
origin == "CLIENT" &&
|
||||||
username: client.gameClient.username,
|
packet.name == "chat" &&
|
||||||
auth: "offline",
|
(packet.params.message as string)
|
||||||
keepAlive: false,
|
.toLowerCase()
|
||||||
skipValidation: true,
|
.startsWith("/eag-") &&
|
||||||
hideErrors: true,
|
!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",
|
||||||
|
keepAlive: false,
|
||||||
|
skipValidation: true,
|
||||||
|
hideErrors: true,
|
||||||
|
...appendOptions,
|
||||||
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!client.gameClient.ended) {
|
if (!client.gameClient.ended) {
|
||||||
client.gameClient.end(
|
client.gameClient.end(
|
||||||
|
@ -546,15 +853,15 @@ export function generateSpawnChunk(): Chunk.PCChunk {
|
||||||
() =>
|
() =>
|
||||||
new McBlock(
|
new McBlock(
|
||||||
REGISTRY.blocksByName.air.id,
|
REGISTRY.blocksByName.air.id,
|
||||||
REGISTRY.biomesByName.plains.id,
|
REGISTRY.biomesByName.the_end.id,
|
||||||
0
|
0
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
chunk.setBlock(
|
chunk.setBlock(
|
||||||
new Vec3(8, 64, 8),
|
new Vec3(8, 64, 8),
|
||||||
new McBlock(
|
new McBlock(
|
||||||
REGISTRY.blocksByName.barrier.id,
|
REGISTRY.blocksByName.sea_lantern.id,
|
||||||
REGISTRY.biomesByName.plains.id,
|
REGISTRY.biomesByName.the_end.id,
|
||||||
0
|
0
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
@ -562,7 +869,7 @@ export function generateSpawnChunk(): Chunk.PCChunk {
|
||||||
new Vec3(8, 67, 8),
|
new Vec3(8, 67, 8),
|
||||||
new McBlock(
|
new McBlock(
|
||||||
REGISTRY.blocksByName.barrier.id,
|
REGISTRY.blocksByName.barrier.id,
|
||||||
REGISTRY.biomesByName.plains.id,
|
REGISTRY.biomesByName.the_end.id,
|
||||||
0
|
0
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
@ -570,7 +877,7 @@ export function generateSpawnChunk(): Chunk.PCChunk {
|
||||||
new Vec3(7, 65, 8),
|
new Vec3(7, 65, 8),
|
||||||
new McBlock(
|
new McBlock(
|
||||||
REGISTRY.blocksByName.barrier.id,
|
REGISTRY.blocksByName.barrier.id,
|
||||||
REGISTRY.biomesByName.plains.id,
|
REGISTRY.biomesByName.the_end.id,
|
||||||
0
|
0
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
@ -578,7 +885,7 @@ export function generateSpawnChunk(): Chunk.PCChunk {
|
||||||
new Vec3(7, 66, 8),
|
new Vec3(7, 66, 8),
|
||||||
new McBlock(
|
new McBlock(
|
||||||
REGISTRY.blocksByName.barrier.id,
|
REGISTRY.blocksByName.barrier.id,
|
||||||
REGISTRY.biomesByName.plains.id,
|
REGISTRY.biomesByName.the_end.id,
|
||||||
0
|
0
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
@ -586,7 +893,7 @@ export function generateSpawnChunk(): Chunk.PCChunk {
|
||||||
new Vec3(9, 65, 8),
|
new Vec3(9, 65, 8),
|
||||||
new McBlock(
|
new McBlock(
|
||||||
REGISTRY.blocksByName.barrier.id,
|
REGISTRY.blocksByName.barrier.id,
|
||||||
REGISTRY.biomesByName.plains.id,
|
REGISTRY.biomesByName.the_end.id,
|
||||||
0
|
0
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
@ -594,7 +901,7 @@ export function generateSpawnChunk(): Chunk.PCChunk {
|
||||||
new Vec3(9, 66, 8),
|
new Vec3(9, 66, 8),
|
||||||
new McBlock(
|
new McBlock(
|
||||||
REGISTRY.blocksByName.barrier.id,
|
REGISTRY.blocksByName.barrier.id,
|
||||||
REGISTRY.biomesByName.plains.id,
|
REGISTRY.biomesByName.the_end.id,
|
||||||
0
|
0
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
@ -602,7 +909,7 @@ export function generateSpawnChunk(): Chunk.PCChunk {
|
||||||
new Vec3(8, 65, 7),
|
new Vec3(8, 65, 7),
|
||||||
new McBlock(
|
new McBlock(
|
||||||
REGISTRY.blocksByName.barrier.id,
|
REGISTRY.blocksByName.barrier.id,
|
||||||
REGISTRY.biomesByName.plains.id,
|
REGISTRY.biomesByName.the_end.id,
|
||||||
0
|
0
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
@ -610,7 +917,7 @@ export function generateSpawnChunk(): Chunk.PCChunk {
|
||||||
new Vec3(8, 66, 7),
|
new Vec3(8, 66, 7),
|
||||||
new McBlock(
|
new McBlock(
|
||||||
REGISTRY.blocksByName.barrier.id,
|
REGISTRY.blocksByName.barrier.id,
|
||||||
REGISTRY.biomesByName.plains.id,
|
REGISTRY.biomesByName.the_end.id,
|
||||||
0
|
0
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
@ -618,7 +925,7 @@ export function generateSpawnChunk(): Chunk.PCChunk {
|
||||||
new Vec3(8, 65, 9),
|
new Vec3(8, 65, 9),
|
||||||
new McBlock(
|
new McBlock(
|
||||||
REGISTRY.blocksByName.barrier.id,
|
REGISTRY.blocksByName.barrier.id,
|
||||||
REGISTRY.biomesByName.plains.id,
|
REGISTRY.biomesByName.the_end.id,
|
||||||
0
|
0
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
@ -626,10 +933,11 @@ export function generateSpawnChunk(): Chunk.PCChunk {
|
||||||
new Vec3(8, 66, 9),
|
new Vec3(8, 66, 9),
|
||||||
new McBlock(
|
new McBlock(
|
||||||
REGISTRY.blocksByName.barrier.id,
|
REGISTRY.blocksByName.barrier.id,
|
||||||
REGISTRY.biomesByName.plains.id,
|
REGISTRY.biomesByName.the_end.id,
|
||||||
0
|
0
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
chunk.setSkyLight(new Vec3(8, 66, 8), 15);
|
// chunk.setBlockLight(new Vec3(8, 65, 8), 15);
|
||||||
|
chunk.setBlockLight(new Vec3(8, 66, 8), 15);
|
||||||
return chunk;
|
return chunk;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,147 +1,153 @@
|
||||||
import { Logger } from "../logger.js"
|
import { Logger } from "../logger.js";
|
||||||
import mcp, { states } from "minecraft-protocol"
|
import mcp, { states } from "minecraft-protocol";
|
||||||
|
|
||||||
const { createSerializer, createDeserializer } = mcp
|
const { createSerializer, createDeserializer } = mcp;
|
||||||
|
|
||||||
export namespace BungeeUtil {
|
export namespace BungeeUtil {
|
||||||
export class PacketUUIDTranslator {
|
export class PacketUUIDTranslator {
|
||||||
public serverSidePlayerUUID: string
|
public serverSidePlayerUUID: string;
|
||||||
public clientSidePlayerUUID: string
|
public clientSidePlayerUUID: string;
|
||||||
|
|
||||||
static readonly CAST_UUID_SERVER: string[] = [
|
static readonly CAST_UUID_SERVER: string[] = [
|
||||||
'update_attributes',
|
"update_attributes",
|
||||||
'named_entity_spawn',
|
"named_entity_spawn",
|
||||||
// drop this packet (twitch.tv integration not available anymore)
|
// drop this packet (twitch.tv integration not available anymore)
|
||||||
'player_info'
|
"player_info",
|
||||||
]
|
];
|
||||||
static readonly CAST_UUID_CLIENT: string[] = [
|
static readonly CAST_UUID_CLIENT: string[] = ["spectate"];
|
||||||
'spectate'
|
|
||||||
]
|
private _logger: Logger;
|
||||||
|
private _serverSerializer: any;
|
||||||
private _logger: Logger
|
private _clientSerializer: any;
|
||||||
private _serverSerializer: any
|
private _clientDeserializer: any;
|
||||||
private _clientSerializer: any
|
private _serverDeserializer: any;
|
||||||
private _clientDeserializer: any
|
|
||||||
private _serverDeserializer: any
|
constructor(ssPlayerUUID: string, csPlayerUUID: string) {
|
||||||
|
this.serverSidePlayerUUID = ssPlayerUUID;
|
||||||
constructor(ssPlayerUUID: string, csPlayerUUID: string) {
|
this.clientSidePlayerUUID = csPlayerUUID;
|
||||||
this.serverSidePlayerUUID = ssPlayerUUID
|
this._logger = new Logger("PacketTranslator");
|
||||||
this.clientSidePlayerUUID = csPlayerUUID
|
this._serverSerializer = createSerializer({
|
||||||
this._logger = new Logger("PacketTranslator")
|
state: states.PLAY,
|
||||||
this._serverSerializer = createSerializer({
|
isServer: true,
|
||||||
state: states.PLAY,
|
version: "1.8.8",
|
||||||
isServer: true,
|
customPackets: null,
|
||||||
version: "1.8.8",
|
});
|
||||||
customPackets: null
|
this._clientSerializer = createSerializer({
|
||||||
})
|
state: states.PLAY,
|
||||||
this._clientSerializer = createSerializer({
|
isServer: false,
|
||||||
state: states.PLAY,
|
version: "1.8.8",
|
||||||
isServer: false,
|
customPackets: null,
|
||||||
version: "1.8.8",
|
});
|
||||||
customPackets: null
|
this._clientDeserializer = createDeserializer({
|
||||||
})
|
state: states.PLAY,
|
||||||
this._clientDeserializer = createDeserializer({
|
isServer: false,
|
||||||
state: states.PLAY,
|
version: "1.8.8",
|
||||||
isServer: false,
|
customPackets: null,
|
||||||
version: "1.8.8",
|
});
|
||||||
customPackets: null
|
this._serverDeserializer = createDeserializer({
|
||||||
})
|
state: states.PLAY,
|
||||||
this._serverDeserializer = createDeserializer({
|
isServer: true,
|
||||||
state: states.PLAY,
|
version: "1.8.8",
|
||||||
isServer: true,
|
customPackets: null,
|
||||||
version: "1.8.8",
|
});
|
||||||
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 }))
|
|
||||||
}
|
|
||||||
|
|
||||||
public onServerWrite(packet: any, meta: any): Buffer /* write to client */ {
|
|
||||||
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 (packet.target == this.clientSidePlayerUUID) {
|
|
||||||
packet.target = this.serverSidePlayerUUID
|
|
||||||
} else if (packet.target == this.serverSidePlayerUUID) {
|
|
||||||
packet.target = this.clientSidePlayerUUID
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
name: meta.name,
|
|
||||||
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') {
|
|
||||||
for (const prop of packet.properties) {
|
|
||||||
for (const modifier of prop.modifiers) {
|
|
||||||
if (modifier.uuid == this.serverSidePlayerUUID) {
|
|
||||||
modifier.uuid = this.clientSidePlayerUUID
|
|
||||||
} else if (modifier.uuid == this.clientSidePlayerUUID) {
|
|
||||||
modifier.uuid = this.serverSidePlayerUUID
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (meta.name == 'named_entity_spawn') {
|
|
||||||
if (packet.playerUUID == this.serverSidePlayerUUID) {
|
|
||||||
packet.playerUUID = this.clientSidePlayerUUID
|
|
||||||
} else if (packet.playerUUID == this.clientSidePlayerUUID) {
|
|
||||||
packet.playerUUID = this.serverSidePlayerUUID
|
|
||||||
}
|
|
||||||
} else if (meta.name == 'player_info') {
|
|
||||||
for (const player of packet.data) {
|
|
||||||
if (player.UUID == this.serverSidePlayerUUID) {
|
|
||||||
player.UUID = this.clientSidePlayerUUID
|
|
||||||
} else if (player.UUID == this.clientSidePlayerUUID) {
|
|
||||||
player.UUID = this.serverSidePlayerUUID
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
name: meta.name,
|
|
||||||
params: packet
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getRespawnSequence(login: any, serializer: any): [Buffer, Buffer] {
|
public onClientWrite(packet: Buffer): Buffer /* write to server */ {
|
||||||
const dimset = getDimSets(login.dimension)
|
const { name, params } =
|
||||||
return [
|
this._serverDeserializer.parsePacketBuffer(packet).data;
|
||||||
serializer.createPacketBuffer({
|
return this._clientSerializer.createPacketBuffer(
|
||||||
name: 'respawn',
|
this._translatePacketClient(params, { name })
|
||||||
params: {
|
);
|
||||||
dimension: dimset[0],
|
|
||||||
difficulty: login.difficulty,
|
|
||||||
gamemode: login.gameMode,
|
|
||||||
levelType: login.levelType
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
serializer.createPacketBuffer({
|
|
||||||
name: 'respawn',
|
|
||||||
params: {
|
|
||||||
dimension: dimset[1],
|
|
||||||
difficulty: login.difficulty,
|
|
||||||
gamemode: login.gameMode,
|
|
||||||
levelType: login.levelType
|
|
||||||
}
|
|
||||||
})
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDimSets(loginDim: number): [number, number] {
|
public onServerWrite(packet: any, meta: any): Buffer /* write to client */ {
|
||||||
return [
|
return this._serverSerializer.createPacketBuffer(
|
||||||
loginDim == -1 ? 0 : loginDim == 0 ? -1 : loginDim == 1 ? 0 : 0,
|
this._translatePacketServer(packet, meta)
|
||||||
loginDim
|
);
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
private _translatePacketClient(packet: any, meta: any): any | null {
|
||||||
|
if (PacketUUIDTranslator.CAST_UUID_CLIENT.some((id) => id == meta.name)) {
|
||||||
|
if (meta.name == "spectate") {
|
||||||
|
if (packet.target == this.clientSidePlayerUUID) {
|
||||||
|
packet.target = this.serverSidePlayerUUID;
|
||||||
|
} else if (packet.target == this.serverSidePlayerUUID) {
|
||||||
|
packet.target = this.clientSidePlayerUUID;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
name: meta.name,
|
||||||
|
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") {
|
||||||
|
for (const prop of packet.properties) {
|
||||||
|
for (const modifier of prop.modifiers) {
|
||||||
|
if (modifier.uuid == this.serverSidePlayerUUID) {
|
||||||
|
modifier.uuid = this.clientSidePlayerUUID;
|
||||||
|
} else if (modifier.uuid == this.clientSidePlayerUUID) {
|
||||||
|
modifier.uuid = this.serverSidePlayerUUID;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (meta.name == "named_entity_spawn") {
|
||||||
|
if (packet.playerUUID == this.serverSidePlayerUUID) {
|
||||||
|
packet.playerUUID = this.clientSidePlayerUUID;
|
||||||
|
} else if (packet.playerUUID == this.clientSidePlayerUUID) {
|
||||||
|
packet.playerUUID = this.serverSidePlayerUUID;
|
||||||
|
}
|
||||||
|
} else if (meta.name == "player_info") {
|
||||||
|
for (const player of packet.data) {
|
||||||
|
if (player.UUID == this.serverSidePlayerUUID) {
|
||||||
|
player.UUID = this.clientSidePlayerUUID;
|
||||||
|
} else if (player.UUID == this.clientSidePlayerUUID) {
|
||||||
|
player.UUID = this.serverSidePlayerUUID;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
name: meta.name,
|
||||||
|
params: packet,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getRespawnSequence(
|
||||||
|
login: any,
|
||||||
|
serializer: any
|
||||||
|
): [Buffer, Buffer] {
|
||||||
|
const dimset = getDimSets(login.dimension);
|
||||||
|
return [
|
||||||
|
serializer.createPacketBuffer({
|
||||||
|
name: "respawn",
|
||||||
|
params: {
|
||||||
|
dimension: dimset[0],
|
||||||
|
difficulty: login.difficulty,
|
||||||
|
gamemode: login.gameMode,
|
||||||
|
levelType: login.levelType,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
serializer.createPacketBuffer({
|
||||||
|
name: "respawn",
|
||||||
|
params: {
|
||||||
|
dimension: dimset[1],
|
||||||
|
difficulty: login.difficulty,
|
||||||
|
gamemode: login.gameMode,
|
||||||
|
levelType: login.levelType,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDimSets(loginDim: number): [number, number] {
|
||||||
|
return [
|
||||||
|
loginDim == -1 ? 0 : loginDim == 0 ? -1 : loginDim == 1 ? 0 : 0,
|
||||||
|
loginDim,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,50 +1,57 @@
|
||||||
import { Enums } from "./Enums.js"
|
import { Enums } from "./Enums.js";
|
||||||
|
|
||||||
export namespace Chat {
|
export namespace Chat {
|
||||||
export type ChatExtra = {
|
export type ChatExtra = {
|
||||||
text: string,
|
text: string;
|
||||||
bold?: boolean,
|
bold?: boolean;
|
||||||
italic?: boolean,
|
italic?: boolean;
|
||||||
underlined?: boolean,
|
underlined?: boolean;
|
||||||
strikethrough?: boolean,
|
strikethrough?: boolean;
|
||||||
obfuscated?: boolean,
|
obfuscated?: boolean;
|
||||||
color?: Enums.ChatColor | 'reset'
|
color?: Enums.ChatColor | "reset";
|
||||||
}
|
};
|
||||||
|
|
||||||
export type Chat = {
|
|
||||||
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
|
|
||||||
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
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
||||||
const ccValues = Object.values(Enums.ChatColor)
|
export type Chat = {
|
||||||
const ccKeys = Object.keys(Enums.ChatColor).map(str => str.toLowerCase())
|
text?: string;
|
||||||
|
bold?: boolean;
|
||||||
|
italic?: boolean;
|
||||||
|
underlined?: boolean;
|
||||||
|
strikethrough?: boolean;
|
||||||
|
obfuscated?: boolean;
|
||||||
|
color?: Enums.ChatColor | "reset";
|
||||||
|
extra?: ChatExtra[];
|
||||||
|
};
|
||||||
|
|
||||||
function resolveColor(colorStr: string) {
|
export function chatToPlainString(chat: Chat): string {
|
||||||
return Object.values(Enums.ChatColor)[ccKeys.indexOf(colorStr.toLowerCase())] ?? colorStr
|
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;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,13 +1,17 @@
|
||||||
import * as meta from "../meta.js"
|
import * as meta from "../meta.js";
|
||||||
|
|
||||||
export namespace Constants {
|
export namespace Constants {
|
||||||
export const EAGLERCRAFT_SKIN_CHANNEL_NAME: string = "EAG|Skins-1.8"
|
export const EAGLERCRAFT_SKIN_CHANNEL_NAME: string = "EAG|Skins-1.8";
|
||||||
export const MAGIC_ENDING_SERVER_SKIN_DOWNLOAD_BUILTIN: number[] = [0x00, 0x00, 0x00]
|
export const MAGIC_ENDING_SERVER_SKIN_DOWNLOAD_BUILTIN: number[] = [
|
||||||
export const MAGIC_ENDING_CLIENT_UPLOAD_SKIN_BUILTIN: number[] = [0x00, 0x05, 0x01, 0x00, 0x00, 0x00]
|
0x00, 0x00, 0x00,
|
||||||
export const EAGLERCRAFT_SKIN_CUSTOM_LENGTH = 64 ** 2 * 4
|
];
|
||||||
|
export const MAGIC_ENDING_CLIENT_UPLOAD_SKIN_BUILTIN: number[] = [
|
||||||
export const JOIN_SERVER_PACKET = 0x01
|
0x00, 0x05, 0x01, 0x00, 0x00, 0x00,
|
||||||
export const PLAYER_LOOK_PACKET = 0x08
|
];
|
||||||
|
export const EAGLERCRAFT_SKIN_CUSTOM_LENGTH = 64 ** 2 * 4;
|
||||||
|
|
||||||
|
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>`;
|
||||||
|
|
|
@ -1,71 +1,71 @@
|
||||||
export namespace Enums {
|
export namespace Enums {
|
||||||
export enum PacketId {
|
export enum PacketId {
|
||||||
CSLoginPacket = 0x01,
|
CSLoginPacket = 0x01,
|
||||||
SCIdentifyPacket = 0x02,
|
SCIdentifyPacket = 0x02,
|
||||||
SCDisconnectPacket = 0xff,
|
SCDisconnectPacket = 0xff,
|
||||||
SCChannelMessagePacket = 0x3f,
|
SCChannelMessagePacket = 0x3f,
|
||||||
CSChannelMessagePacket = 0x17,
|
CSChannelMessagePacket = 0x17,
|
||||||
CSUsernamePacket = 0x04,
|
CSUsernamePacket = 0x04,
|
||||||
SCSyncUuidPacket = 0x05,
|
SCSyncUuidPacket = 0x05,
|
||||||
CSSetSkinPacket = 0x07,
|
CSSetSkinPacket = 0x07,
|
||||||
CSReadyPacket = 0x08,
|
CSReadyPacket = 0x08,
|
||||||
SCReadyPacket = 0x09
|
SCReadyPacket = 0x09,
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ChannelMessageType {
|
export enum ChannelMessageType {
|
||||||
CLIENT = 0x17,
|
CLIENT = 0x17,
|
||||||
SERVER = 0x3f
|
SERVER = 0x3f,
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum EaglerSkinPacketId {
|
export enum EaglerSkinPacketId {
|
||||||
CFetchSkinEaglerPlayerReq = 0x03,
|
CFetchSkinEaglerPlayerReq = 0x03,
|
||||||
SFetchSkinBuiltInRes = 0x04,
|
SFetchSkinBuiltInRes = 0x04,
|
||||||
SFetchSkinRes = 0x05,
|
SFetchSkinRes = 0x05,
|
||||||
CFetchSkinReq = 0x06
|
CFetchSkinReq = 0x06,
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ClientState {
|
|
||||||
PRE_HANDSHAKE = "PRE_HANDSHAKE",
|
|
||||||
POST_HANDSHAKE = "POST_HANDSHAKE",
|
|
||||||
DISCONNECTED = "DISCONNECTED"
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum PacketBounds {
|
export enum ClientState {
|
||||||
C = "C",
|
PRE_HANDSHAKE = "PRE_HANDSHAKE",
|
||||||
S = "S"
|
POST_HANDSHAKE = "POST_HANDSHAKE",
|
||||||
}
|
DISCONNECTED = "DISCONNECTED",
|
||||||
|
}
|
||||||
|
|
||||||
export enum SkinType {
|
export enum PacketBounds {
|
||||||
BUILTIN,
|
C = "C",
|
||||||
CUSTOM
|
S = "S",
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ChatColor {
|
export enum SkinType {
|
||||||
AQUA = "§b",
|
BUILTIN,
|
||||||
BLACK = "§0",
|
CUSTOM,
|
||||||
DARK_BLUE = "§1",
|
}
|
||||||
DARK_GREEN = "§2",
|
|
||||||
DARK_CYAN = "§3",
|
export enum ChatColor {
|
||||||
DARK_RED = "§4",
|
AQUA = "§b",
|
||||||
PURPLE = "§5",
|
BLACK = "§0",
|
||||||
GOLD = "§6",
|
DARK_BLUE = "§1",
|
||||||
GRAY = "§7",
|
DARK_GREEN = "§2",
|
||||||
GREEN = "§a",
|
DARK_CYAN = "§3",
|
||||||
DARK_GRAY = "§8",
|
DARK_RED = "§4",
|
||||||
BLUE = "§9",
|
PURPLE = "§5",
|
||||||
BRIGHT_GREEN = "§a",
|
GOLD = "§6",
|
||||||
LIGHT_PURPLE = "§d",
|
GRAY = "§7",
|
||||||
CYAN = "§b",
|
GREEN = "§a",
|
||||||
RED = "§c",
|
DARK_GRAY = "§8",
|
||||||
PINK = "§d",
|
BLUE = "§9",
|
||||||
YELLOW = "§e",
|
BRIGHT_GREEN = "§a",
|
||||||
WHITE = "§f",
|
LIGHT_PURPLE = "§d",
|
||||||
// text styling
|
CYAN = "§b",
|
||||||
OBFUSCATED = '§k',
|
RED = "§c",
|
||||||
BOLD = '§l',
|
PINK = "§d",
|
||||||
STRIKETHROUGH = '§m',
|
YELLOW = "§e",
|
||||||
UNDERLINED = '§n',
|
WHITE = "§f",
|
||||||
ITALIC = '§o',
|
// text styling
|
||||||
RESET = '§r'
|
OBFUSCATED = "§k",
|
||||||
}
|
BOLD = "§l",
|
||||||
}
|
STRIKETHROUGH = "§m",
|
||||||
|
UNDERLINED = "§n",
|
||||||
|
ITALIC = "§o",
|
||||||
|
RESET = "§r",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,132 +1,153 @@
|
||||||
import { randomUUID } from 'crypto';
|
import { randomUUID } from "crypto";
|
||||||
import pkg, { NewPingResult } from 'minecraft-protocol';
|
import pkg, { NewPingResult } from "minecraft-protocol";
|
||||||
import sharp from 'sharp';
|
import sharp from "sharp";
|
||||||
import { PROXY_BRANDING, PROXY_VERSION } from '../meta.js';
|
import { PROXY_BRANDING, PROXY_VERSION } from "../meta.js";
|
||||||
import { Config } from '../launcher_types.js'
|
import { Config } from "../launcher_types.js";
|
||||||
import { Chat } from './Chat.js';
|
import { Chat } from "./Chat.js";
|
||||||
const { ping } = pkg
|
const { ping } = pkg;
|
||||||
|
|
||||||
export namespace Motd {
|
export namespace Motd {
|
||||||
const ICON_SQRT = 64
|
const ICON_SQRT = 64;
|
||||||
const IMAGE_DATA_PREPEND = "data:image/png;base64,"
|
const IMAGE_DATA_PREPEND = "data:image/png;base64,";
|
||||||
|
|
||||||
export class MOTD {
|
export class MOTD {
|
||||||
public jsonMotd: JSONMotd
|
public jsonMotd: JSONMotd;
|
||||||
public image?: Buffer
|
public image?: Buffer;
|
||||||
|
|
||||||
constructor(motd: JSONMotd, image?: Buffer) {
|
constructor(motd: JSONMotd, image?: Buffer) {
|
||||||
this.jsonMotd = motd
|
this.jsonMotd = motd;
|
||||||
this.image = image
|
this.image = image;
|
||||||
}
|
|
||||||
|
|
||||||
public static async generateMOTDFromPing(host: string, port: number): Promise<MOTD> {
|
|
||||||
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
|
|
||||||
|
|
||||||
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'))
|
|
||||||
}
|
|
||||||
|
|
||||||
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), ""],
|
|
||||||
online: newPingRes.players.online,
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static async generateMOTDFromConfig(config: Config['adapter']): Promise<MOTD> {
|
|
||||||
if (typeof config.motd != 'string') {
|
|
||||||
const motd = new MOTD({
|
|
||||||
brand: PROXY_BRANDING,
|
|
||||||
cracked: true,
|
|
||||||
data: {
|
|
||||||
cache: true,
|
|
||||||
icon: config.motd.iconURL != null ? true : false,
|
|
||||||
max: config.maxConcurrentClients,
|
|
||||||
motd: [config.motd.l1, config.motd.l2 ?? ""],
|
|
||||||
online: 0,
|
|
||||||
players: []
|
|
||||||
},
|
|
||||||
name: config.name,
|
|
||||||
secure: false,
|
|
||||||
time: Date.now(),
|
|
||||||
type: 'motd',
|
|
||||||
uuid: randomUUID(),
|
|
||||||
vers: `${PROXY_BRANDING}/${PROXY_VERSION}`
|
|
||||||
})
|
|
||||||
if (config.motd.iconURL != null) {
|
|
||||||
motd.image = await this.generateEaglerMOTDImage(config.motd.iconURL)
|
|
||||||
}
|
|
||||||
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> {
|
|
||||||
return new Promise<Buffer>((res, rej) => {
|
|
||||||
sharp(file)
|
|
||||||
.resize(ICON_SQRT, ICON_SQRT, {
|
|
||||||
kernel: 'nearest'
|
|
||||||
})
|
|
||||||
.raw({
|
|
||||||
depth: 'uchar'
|
|
||||||
})
|
|
||||||
.toBuffer()
|
|
||||||
.then(buff => {
|
|
||||||
for (const pixel of buff) {
|
|
||||||
if ((pixel & 0xFFFFFF) == 0) {
|
|
||||||
buff[buff.indexOf(pixel)] = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
res(buff)
|
|
||||||
})
|
|
||||||
.catch(rej)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
public toBuffer(): [string, Buffer] {
|
|
||||||
return [
|
|
||||||
JSON.stringify(this.jsonMotd),
|
|
||||||
this.image
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type JSONMotd = {
|
public static async generateMOTDFromPing(
|
||||||
brand: string,
|
host: string,
|
||||||
cracked: true,
|
port: number
|
||||||
data: {
|
): 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;
|
||||||
|
|
||||||
|
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"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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),
|
||||||
|
"",
|
||||||
|
],
|
||||||
|
online: newPingRes.players.online,
|
||||||
|
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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async generateMOTDFromConfig(
|
||||||
|
config: Config["adapter"]
|
||||||
|
): Promise<MOTD> {
|
||||||
|
if (typeof config.motd != "string") {
|
||||||
|
const motd = new MOTD({
|
||||||
|
brand: PROXY_BRANDING,
|
||||||
|
cracked: true,
|
||||||
|
data: {
|
||||||
cache: true,
|
cache: true,
|
||||||
icon: boolean,
|
icon: config.motd.iconURL != null ? true : false,
|
||||||
max: number,
|
max: config.maxConcurrentClients,
|
||||||
motd: [string, string],
|
motd: [config.motd.l1, config.motd.l2 ?? ""],
|
||||||
online: number,
|
online: 0,
|
||||||
players: string[],
|
players: [],
|
||||||
},
|
},
|
||||||
name: string,
|
name: config.name,
|
||||||
secure: false,
|
secure: false,
|
||||||
time: ReturnType<typeof Date.now>,
|
time: Date.now(),
|
||||||
type: "motd",
|
type: "motd",
|
||||||
uuid: ReturnType<typeof randomUUID>,
|
uuid: randomUUID(),
|
||||||
vers: string
|
vers: `${PROXY_BRANDING}/${PROXY_VERSION}`,
|
||||||
|
});
|
||||||
|
if (config.motd.iconURL != null) {
|
||||||
|
motd.image = await this.generateEaglerMOTDImage(config.motd.iconURL);
|
||||||
|
}
|
||||||
|
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> {
|
||||||
|
return new Promise<Buffer>((res, rej) => {
|
||||||
|
sharp(file)
|
||||||
|
.resize(ICON_SQRT, ICON_SQRT, {
|
||||||
|
kernel: "nearest",
|
||||||
|
})
|
||||||
|
.raw({
|
||||||
|
depth: "uchar",
|
||||||
|
})
|
||||||
|
.toBuffer()
|
||||||
|
.then((buff) => {
|
||||||
|
for (const pixel of buff) {
|
||||||
|
if ((pixel & 0xffffff) == 0) {
|
||||||
|
buff[buff.indexOf(pixel)] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
res(buff);
|
||||||
|
})
|
||||||
|
.catch(rej);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public toBuffer(): [string, Buffer] {
|
||||||
|
return [JSON.stringify(this.jsonMotd), this.image];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type JSONMotd = {
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
@ -1,35 +1,44 @@
|
||||||
import { readdir } from "fs/promises"
|
import { readdir } from "fs/promises";
|
||||||
import { dirname, join, resolve } from "path"
|
import { dirname, join, resolve } from "path";
|
||||||
import { fileURLToPath, pathToFileURL } from "url"
|
import { fileURLToPath, pathToFileURL } from "url";
|
||||||
import { Enums } from "./Enums.js"
|
import { Enums } from "./Enums.js";
|
||||||
import { Util } from "./Util.js"
|
import { Util } from "./Util.js";
|
||||||
|
|
||||||
export default interface Packet {
|
export default interface Packet {
|
||||||
packetId: Enums.PacketId
|
packetId: Enums.PacketId;
|
||||||
type: "packet"
|
type: "packet";
|
||||||
boundTo: Enums.PacketBounds
|
boundTo: Enums.PacketBounds;
|
||||||
sentAfterHandshake: boolean
|
sentAfterHandshake: boolean;
|
||||||
|
|
||||||
serialize: () => Buffer
|
serialize: () => Buffer;
|
||||||
deserialize: (packet: Buffer) => this
|
deserialize: (packet: Buffer) => this;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function loadPackets(dir?: string): Promise<Map<Enums.PacketId, Packet & { class: any }>> {
|
export async function loadPackets(
|
||||||
const files = (await Util.recursiveFileSearch(dir ?? join(dirname(fileURLToPath(import.meta.url)), "packets"))).filter(f => f.endsWith(".js") && !f.endsWith(".disabled.js"))
|
dir?: string
|
||||||
const packetRegistry = new Map()
|
): Promise<Map<Enums.PacketId, Packet & { class: any }>> {
|
||||||
for (const file of files) {
|
const files = (
|
||||||
const imp = await import(process.platform == 'win32' ? pathToFileURL(file).toString() : file)
|
await Util.recursiveFileSearch(
|
||||||
for (const val of Object.values(imp)) {
|
dir ?? join(dirname(fileURLToPath(import.meta.url)), "packets")
|
||||||
if (val != null) {
|
)
|
||||||
let e: Packet
|
).filter((f) => f.endsWith(".js") && !f.endsWith(".disabled.js"));
|
||||||
try { e = new (val as any)() }
|
const packetRegistry = new Map();
|
||||||
catch {}
|
for (const file of files) {
|
||||||
if (e != null && e.type == 'packet') {
|
const imp = await import(
|
||||||
;(e as any).class = val
|
process.platform == "win32" ? pathToFileURL(file).toString() : file
|
||||||
packetRegistry.set(e.packetId, e)
|
);
|
||||||
}
|
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);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return packetRegistry
|
}
|
||||||
}
|
return packetRegistry;
|
||||||
|
}
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import {
|
import {
|
||||||
encodeULEB128 as _encodeVarInt,
|
encodeULEB128 as _encodeVarInt,
|
||||||
decodeULEB128 as _decodeVarInt
|
decodeULEB128 as _decodeVarInt,
|
||||||
} from "@thi.ng/leb128"
|
} from "@thi.ng/leb128";
|
||||||
import { Enums } from "./Enums.js"
|
import { Enums } from "./Enums.js";
|
||||||
import { Util } from "./Util.js"
|
import { Util } from "./Util.js";
|
||||||
|
|
||||||
// reference: https://wiki.vg/index.php?title=Protocol&oldid=7368 (id: 73)
|
// reference: https://wiki.vg/index.php?title=Protocol&oldid=7368 (id: 73)
|
||||||
// use https://hexed.it/ for hex analysis, dumps.ts for example dumps
|
// use https://hexed.it/ for hex analysis, dumps.ts for example dumps
|
||||||
|
@ -11,68 +11,77 @@ import { Util } from "./Util.js"
|
||||||
// datatypes being used thus far. There may be more, but however, they will be added here as needed.
|
// datatypes being used thus far. There may be more, but however, they will be added here as needed.
|
||||||
|
|
||||||
export namespace MineProtocol {
|
export namespace MineProtocol {
|
||||||
export type ReadResult<T> = {
|
export type ReadResult<T> = {
|
||||||
value: T,
|
value: T;
|
||||||
// the new buffer, but with the bytes being read being completely removed
|
// the new buffer, but with the bytes being read being completely removed
|
||||||
// very useful when it comes to chaining
|
// very useful when it comes to chaining
|
||||||
newBuffer: Buffer
|
newBuffer: Buffer;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type UUID = string
|
export type UUID = string;
|
||||||
|
|
||||||
export function writeVarInt(int: number): Buffer {
|
export function writeVarInt(int: number): Buffer {
|
||||||
return Buffer.from(_encodeVarInt(int))
|
return Buffer.from(_encodeVarInt(int));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function readVarInt(buff: Buffer, offset?: number): ReadResult<number> {
|
|
||||||
buff = offset ? buff.subarray(offset) : buff
|
|
||||||
const read = _decodeVarInt(buff), len = read[1]
|
|
||||||
return {
|
|
||||||
// potential oversight?
|
|
||||||
value: Number(read[0]),
|
|
||||||
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])
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
return {
|
|
||||||
value: _readShort(buff[0], buff[1]),
|
|
||||||
newBuffer: buff.subarray(2)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function writeShort(num: number): Buffer {
|
|
||||||
const alloc = Buffer.alloc(2)
|
|
||||||
alloc.writeInt16BE(num)
|
|
||||||
return alloc
|
|
||||||
}
|
|
||||||
|
|
||||||
export function readUUID(buff: Buffer, offset?: number): ReadResult<string> {
|
export function readVarInt(
|
||||||
buff = offset ? buff.subarray(offset) : buff
|
buff: Buffer,
|
||||||
return {
|
offset?: number
|
||||||
value: Util.uuidBufferToString(buff.subarray(0, 16)),
|
): ReadResult<number> {
|
||||||
newBuffer: buff.subarray(16)
|
buff = offset ? buff.subarray(offset) : buff;
|
||||||
}
|
const read = _decodeVarInt(buff),
|
||||||
}
|
len = read[1];
|
||||||
|
return {
|
||||||
|
// potential oversight?
|
||||||
|
value: Number(read[0]),
|
||||||
|
newBuffer: buff.subarray(len),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function writeUUID(uuid: string | Buffer): Buffer {
|
export function writeString(str: string): Buffer {
|
||||||
return typeof uuid == 'string' ? Util.uuidStringToBuffer(uuid) : uuid
|
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");
|
||||||
|
return {
|
||||||
|
value: str,
|
||||||
|
newBuffer: len.newBuffer.subarray(len.value),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
return {
|
||||||
|
value: _readShort(buff[0], buff[1]),
|
||||||
|
newBuffer: buff.subarray(2),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function writeShort(num: number): Buffer {
|
||||||
|
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;
|
||||||
|
return {
|
||||||
|
value: Util.uuidBufferToString(buff.subarray(0, 16)),
|
||||||
|
newBuffer: buff.subarray(16),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function writeUUID(uuid: string | Buffer): Buffer {
|
||||||
|
return typeof uuid == "string" ? Util.uuidStringToBuffer(uuid) : uuid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -2,18 +2,23 @@ import { WebSocket, WebSocketServer } from "ws";
|
||||||
import { Config } from "../launcher_types.js";
|
import { Config } from "../launcher_types.js";
|
||||||
import { Logger } from "../logger.js";
|
import { Logger } from "../logger.js";
|
||||||
import Packet, { loadPackets } from "./Packet.js";
|
import Packet, { loadPackets } from "./Packet.js";
|
||||||
import * as http from "http"
|
import * as http from "http";
|
||||||
import * as https from "https"
|
import * as https from "https";
|
||||||
import { readFile } from "fs/promises";
|
import { readFile } from "fs/promises";
|
||||||
import { Duplex } from "stream";
|
import { Duplex } from "stream";
|
||||||
import { parseDomain, ParseResultType } from "parse-domain"
|
import { parseDomain, ParseResultType } from "parse-domain";
|
||||||
import { Util } from "./Util.js";
|
import { Util } from "./Util.js";
|
||||||
import CSLoginPacket from "./packets/CSLoginPacket.js";
|
import CSLoginPacket from "./packets/CSLoginPacket.js";
|
||||||
import SCIdentifyPacket from "./packets/SCIdentifyPacket.js";
|
import SCIdentifyPacket from "./packets/SCIdentifyPacket.js";
|
||||||
import { Motd } from "./Motd.js";
|
import { Motd } from "./Motd.js";
|
||||||
import { Player } from "./Player.js";
|
import { Player } from "./Player.js";
|
||||||
import { Enums } from "./Enums.js";
|
import { Enums } from "./Enums.js";
|
||||||
import { NETWORK_VERSION, PROXY_BRANDING, PROXY_VERSION, VANILLA_PROTOCOL_VERSION } from "../meta.js";
|
import {
|
||||||
|
NETWORK_VERSION,
|
||||||
|
PROXY_BRANDING,
|
||||||
|
PROXY_VERSION,
|
||||||
|
VANILLA_PROTOCOL_VERSION,
|
||||||
|
} from "../meta.js";
|
||||||
import { CSUsernamePacket } from "./packets/CSUsernamePacket.js";
|
import { CSUsernamePacket } from "./packets/CSUsernamePacket.js";
|
||||||
import { SCSyncUuidPacket } from "./packets/SCSyncUuidPacket.js";
|
import { SCSyncUuidPacket } from "./packets/SCSyncUuidPacket.js";
|
||||||
import { SCReadyPacket } from "./packets/SCReadyPacket.js";
|
import { SCReadyPacket } from "./packets/SCReadyPacket.js";
|
||||||
|
@ -26,312 +31,457 @@ import { CSChannelMessagePacket } from "./packets/channel/CSChannelMessage.js";
|
||||||
import { Constants, UPGRADE_REQUIRED_RESPONSE } from "./Constants.js";
|
import { Constants, UPGRADE_REQUIRED_RESPONSE } from "./Constants.js";
|
||||||
import { PluginManager } from "./pluginLoader/PluginManager.js";
|
import { PluginManager } from "./pluginLoader/PluginManager.js";
|
||||||
|
|
||||||
let instanceCount = 0
|
let instanceCount = 0;
|
||||||
const chalk = new Chalk({ level: 2 })
|
const chalk = new Chalk({ level: 2 });
|
||||||
|
|
||||||
export class Proxy extends EventEmitter {
|
export class Proxy extends EventEmitter {
|
||||||
public packetRegistry: Map<number, Packet & {
|
public packetRegistry: Map<
|
||||||
class: any
|
number,
|
||||||
}>
|
Packet & {
|
||||||
public players = new Map<string, Player>()
|
class: any;
|
||||||
public pluginManager: PluginManager
|
|
||||||
public config: Config['adapter']
|
|
||||||
public wsServer: WebSocketServer
|
|
||||||
public httpServer: http.Server
|
|
||||||
public skinServer: EaglerSkins.SkinServer
|
|
||||||
public broadcastMotd?: Motd.MOTD
|
|
||||||
|
|
||||||
private _logger: Logger
|
|
||||||
private initalHandlerLogger: Logger
|
|
||||||
|
|
||||||
private loaded: boolean
|
|
||||||
|
|
||||||
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.info = (msg: string) => {
|
|
||||||
;(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.error = (msg: string) => {
|
|
||||||
;(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.debug = (msg: string) => {
|
|
||||||
;(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('unhandledRejection', err => {
|
|
||||||
this._logger.warn(`An unhandled rejection was caught! Rejection: ${err}`)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
>;
|
||||||
|
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 async init() {
|
private _logger: Logger;
|
||||||
this._logger.info(`Starting ${PROXY_BRANDING} v${PROXY_VERSION}...`)
|
private initalHandlerLogger: Logger;
|
||||||
global.PROXY = this
|
|
||||||
if (this.loaded) throw new Error("Can't initiate if proxy instance is already initialized or is being initialized!")
|
private loaded: boolean;
|
||||||
this.loaded = true
|
|
||||||
this.packetRegistry = await loadPackets()
|
constructor(config: Config["adapter"], pluginManager: PluginManager) {
|
||||||
this.skinServer = new EaglerSkins.SkinServer(this, this.config.skinUrlWhitelist)
|
super();
|
||||||
global.PACKET_REGISTRY = this.packetRegistry
|
this._logger = new Logger(`EaglerProxy-${instanceCount}`);
|
||||||
if (this.config.motd == 'FORWARD') {
|
this.initalHandlerLogger = new Logger(`EaglerProxy-InitialHandler`);
|
||||||
this._pollServer(this.config.server.host, this.config.server.port)
|
// hijack the initial handler logger to append [InitialHandler] to the beginning
|
||||||
} else {
|
(this.initalHandlerLogger as any)._info = this.initalHandlerLogger.info;
|
||||||
// TODO: motd
|
this.initalHandlerLogger.info = (msg: string) => {
|
||||||
const broadcastMOTD = await Motd.MOTD.generateMOTDFromConfig(this.config)
|
(this.initalHandlerLogger as any)._info(
|
||||||
;(broadcastMOTD as any)._static = true
|
`${chalk.blue("[InitialHandler]")} ${msg}`
|
||||||
this.broadcastMotd = broadcastMOTD
|
);
|
||||||
// playercount will be dynamically updated
|
};
|
||||||
}
|
(this.initalHandlerLogger as any)._warn = this.initalHandlerLogger.warn;
|
||||||
if (this.config.tls && this.config.tls.enabled) {
|
this.initalHandlerLogger.warn = (msg: string) => {
|
||||||
this.httpServer = https.createServer({
|
(this.initalHandlerLogger as any)._warn(
|
||||||
key: await readFile(this.config.tls.key),
|
`${chalk.blue("[InitialHandler]")} ${msg}`
|
||||||
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({
|
(this.initalHandlerLogger as any)._error = this.initalHandlerLogger.error;
|
||||||
noServer: true
|
this.initalHandlerLogger.error = (msg: string) => {
|
||||||
})
|
(this.initalHandlerLogger as any)._error(
|
||||||
} else {
|
`${chalk.blue("[InitialHandler]")} ${msg}`
|
||||||
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
|
(this.initalHandlerLogger as any)._fatal = this.initalHandlerLogger.fatal;
|
||||||
})
|
this.initalHandlerLogger.fatal = (msg: string) => {
|
||||||
}
|
(this.initalHandlerLogger as any)._fatal(
|
||||||
this.httpServer.on('error', err => {
|
`${chalk.blue("[InitialHandler]")} ${msg}`
|
||||||
this._logger.warn(`HTTP server threw an error: ${err.stack}`)
|
);
|
||||||
})
|
};
|
||||||
this.wsServer.on('error', err => {
|
(this.initalHandlerLogger as any)._debug = this.initalHandlerLogger.debug;
|
||||||
this._logger.warn(`WebSocket server threw an error: ${err.stack}`)
|
this.initalHandlerLogger.debug = (msg: string) => {
|
||||||
})
|
(this.initalHandlerLogger as any)._debug(
|
||||||
this.httpServer.on('upgrade', async (r, s, h) => {
|
`${chalk.blue("[InitialHandler]")} ${msg}`
|
||||||
try {
|
);
|
||||||
await this._handleWSConnectionReq(r, s, h)
|
};
|
||||||
} catch (err) {
|
this.config = config;
|
||||||
this._logger.error(`Error was caught whilst trying to handle WebSocket upgrade! Error: ${err.stack ?? err}`)
|
this.pluginManager = pluginManager;
|
||||||
}
|
instanceCount++;
|
||||||
})
|
|
||||||
this.pluginManager.emit('proxyFinishLoading', this, this.pluginManager)
|
process.on("uncaughtException", (err) => {
|
||||||
this._logger.info(`Started WebSocket server and binded to ${this.config.bindHost} on port ${this.config.bindPort}.`)
|
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}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
} else {
|
||||||
|
// TODO: motd
|
||||||
|
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(
|
||||||
|
{
|
||||||
|
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"
|
||||||
|
);
|
||||||
|
this.wsServer = new WebSocketServer({
|
||||||
|
noServer: true,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.httpServer = http
|
||||||
|
.createServer((req, res) =>
|
||||||
|
this._handleNonWSRequest(req, res, this.config)
|
||||||
|
)
|
||||||
|
.listen(
|
||||||
|
this.config.bindPort || 8080,
|
||||||
|
this.config.bindHost || "127.0.0.1"
|
||||||
|
);
|
||||||
|
this.wsServer = new WebSocketServer({
|
||||||
|
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) => {
|
||||||
|
try {
|
||||||
|
await this._handleWSConnectionReq(r, s, h);
|
||||||
|
} catch (err) {
|
||||||
|
this._logger.error(
|
||||||
|
`Error was caught whilst trying to handle WebSocket upgrade! Error: ${
|
||||||
|
err.stack ?? err
|
||||||
|
}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.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']) {
|
private _handleNonWSRequest(
|
||||||
res.setHeader("Content-Type", "text/html")
|
req: http.IncomingMessage,
|
||||||
.writeHead(426)
|
res: http.ServerResponse,
|
||||||
.end(UPGRADE_REQUIRED_RESPONSE)
|
config: Config["adapter"]
|
||||||
}
|
) {
|
||||||
|
res
|
||||||
|
.setHeader("Content-Type", "text/html")
|
||||||
|
.writeHead(426)
|
||||||
|
.end(UPGRADE_REQUIRED_RESPONSE);
|
||||||
|
}
|
||||||
|
|
||||||
readonly LOGIN_TIMEOUT = 30000
|
readonly LOGIN_TIMEOUT = 30000;
|
||||||
|
|
||||||
private async _handleWSConnection(ws: WebSocket) {
|
private async _handleWSConnection(ws: WebSocket) {
|
||||||
const firstPacket = await Util.awaitPacket(ws)
|
const firstPacket = await Util.awaitPacket(ws);
|
||||||
let player: Player, handled: boolean
|
let player: Player, handled: boolean;
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (!handled) {
|
if (!handled) {
|
||||||
this.initalHandlerLogger.warn(`Disconnecting client ${player ? player.username ?? `[/${(ws as any)._socket.remoteAddress}:${(ws as any)._socket.remotePort}` : `[/${(ws as any)._socket.remoteAddress}:${(ws as any)._socket.remotePort}`} due to connection timing out.`)
|
this.initalHandlerLogger.warn(
|
||||||
if (player) player.disconnect(`${Enums.ChatColor.YELLOW} Your connection timed out whilst processing handshake, please try again.`)
|
`Disconnecting client ${
|
||||||
else ws.close()
|
player
|
||||||
}
|
? player.username ??
|
||||||
}, this.LOGIN_TIMEOUT)
|
`[/${(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);
|
||||||
|
try {
|
||||||
|
if (firstPacket.toString() === "Accept: MOTD") {
|
||||||
|
if (this.broadcastMotd) {
|
||||||
|
if ((this.broadcastMotd as any)._static) {
|
||||||
|
this.broadcastMotd.jsonMotd.data.online = this.players.size;
|
||||||
|
// sample for players
|
||||||
|
this.broadcastMotd.jsonMotd.data.players = [];
|
||||||
|
const playerSample = [...this.players.keys()]
|
||||||
|
.filter((sample) => !sample.startsWith("!phs_"))
|
||||||
|
.slice(0, 5);
|
||||||
|
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]);
|
||||||
|
} else {
|
||||||
|
const motd = this.broadcastMotd.toBuffer();
|
||||||
|
ws.send(motd[0]);
|
||||||
|
if (motd[1] != null) ws.send(motd[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
handled = true;
|
||||||
|
ws.close();
|
||||||
|
} else {
|
||||||
|
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;
|
||||||
|
} 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;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
if (firstPacket.toString() === "Accept: MOTD") {
|
Util.validateUsername(loginPacket.username);
|
||||||
if (this.broadcastMotd) {
|
|
||||||
if ((this.broadcastMotd as any)._static) {
|
|
||||||
this.broadcastMotd.jsonMotd.data.online = this.players.size
|
|
||||||
// sample for players
|
|
||||||
this.broadcastMotd.jsonMotd.data.players = []
|
|
||||||
const playerSample = [...this.players.keys()]
|
|
||||||
.filter(sample => !sample.startsWith("!phs_"))
|
|
||||||
.slice(0, 5)
|
|
||||||
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])
|
|
||||||
} else {
|
|
||||||
const motd = this.broadcastMotd.toBuffer()
|
|
||||||
ws.send(motd[0])
|
|
||||||
if (motd[1] != null) ws.send(motd[1])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
handled = true
|
|
||||||
ws.close()
|
|
||||||
} else {
|
|
||||||
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
|
|
||||||
} 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
|
|
||||||
}
|
|
||||||
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)
|
|
||||||
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
|
|
||||||
}
|
|
||||||
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
|
|
||||||
}
|
|
||||||
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]),
|
|
||||||
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
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.initalHandlerLogger.warn(`Error occurred whilst handling handshake: ${err.stack ?? err}`)
|
player.disconnect(`${Enums.ChatColor.RED}${err.reason || err}`);
|
||||||
handled = true
|
return;
|
||||||
ws.close()
|
|
||||||
if (player && player.uuid && this.players.has(`!phs.${player.uuid}`))
|
|
||||||
this.players.delete(`!phs.${player.uuid}`)
|
|
||||||
if (player && player.uuid && this.players.has(player.username))
|
|
||||||
this.players.delete(player.username)
|
|
||||||
}
|
}
|
||||||
}
|
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;
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
const syncUuid = new SCSyncUuidPacket();
|
||||||
|
syncUuid.username = player.username;
|
||||||
|
syncUuid.uuid = player.uuid;
|
||||||
|
player.write(syncUuid);
|
||||||
|
|
||||||
private _bindListenersToPlayer(player: Player) {
|
const prom = await Promise.all([
|
||||||
let sentDisconnectMsg = false
|
player.read(Enums.PacketId.CSReadyPacket),
|
||||||
player.on('disconnect', () => {
|
(await player.read(
|
||||||
if (this.players.has(player.username))
|
Enums.PacketId.CSSetSkinPacket
|
||||||
this.players.delete(player.username)
|
)) as CSSetSkinPacket,
|
||||||
this.initalHandlerLogger.info(`DISCONNECT ${player.username} <=> DISCONNECTED`)
|
]),
|
||||||
if (!sentDisconnectMsg) this._logger.info(`Player ${player.username} (${player.uuid}) disconnected from the proxy server.`)
|
skin = prom[1],
|
||||||
})
|
obj = new EaglerSkins.EaglerSkin();
|
||||||
player.on('proxyPacket', async packet => {
|
obj.owner = player;
|
||||||
if (packet.packetId == Enums.PacketId.CSChannelMessagePacket) {
|
obj.type = skin.skinType as any;
|
||||||
try {
|
if (skin.skinType == Enums.SkinType.CUSTOM) obj.skin = skin.skin;
|
||||||
const msg: CSChannelMessagePacket = packet as any
|
else obj.builtInSkin = skin.skinId;
|
||||||
if (msg.channel == Constants.EAGLERCRAFT_SKIN_CHANNEL_NAME) {
|
player.skin = obj;
|
||||||
await this.skinServer.handleRequest(msg, player)
|
|
||||||
}
|
|
||||||
} catch (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}`)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
static readonly POLL_INTERVAL: number = 10000
|
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);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
this.initalHandlerLogger.warn(
|
||||||
|
`Error occurred whilst handling handshake: ${err.stack ?? err}`
|
||||||
|
);
|
||||||
|
handled = true;
|
||||||
|
ws.close();
|
||||||
|
if (player && player.uuid && this.players.has(`!phs.${player.uuid}`))
|
||||||
|
this.players.delete(`!phs.${player.uuid}`);
|
||||||
|
if (player && player.uuid && this.players.has(player.username))
|
||||||
|
this.players.delete(player.username);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private _pollServer(host: string, port: number, interval?: number) {
|
private _bindListenersToPlayer(player: Player) {
|
||||||
;(async () => {
|
let sentDisconnectMsg = false;
|
||||||
while (true) {
|
player.on("disconnect", () => {
|
||||||
const motd = await Motd.MOTD.generateMOTDFromPing(host, port)
|
if (this.players.has(player.username))
|
||||||
.catch(err => {
|
this.players.delete(player.username);
|
||||||
this._logger.warn(`Error polling ${host}:${port} for MOTD: ${err.stack ?? err}`)
|
this.initalHandlerLogger.info(
|
||||||
})
|
`DISCONNECT ${player.username} <=> DISCONNECTED`
|
||||||
if (motd) this.broadcastMotd = motd
|
);
|
||||||
await new Promise(res => setTimeout(res, interval ?? Proxy.POLL_INTERVAL))
|
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;
|
||||||
|
if (msg.channel == Constants.EAGLERCRAFT_SKIN_CHANNEL_NAME) {
|
||||||
|
await this.skinServer.handleRequest(msg, player);
|
||||||
|
}
|
||||||
|
} catch (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}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private async _handleWSConnectionReq(req: http.IncomingMessage, socket: Duplex, head: Buffer) {
|
static readonly POLL_INTERVAL: number = 10000;
|
||||||
const origin = req.headers.origin == null || req.headers.origin == 'null' ? null : req.headers.origin
|
|
||||||
if (!this.config.origins.allowOfflineDownloads && origin == null) {
|
|
||||||
socket.destroy()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (this.config.origins.originBlacklist != null && this.config.origins.originBlacklist.some(host => Util.areDomainsEqual(host, origin))) {
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public fetchUserByUUID(uuid: MineProtocol.UUID): Player | null {
|
private _pollServer(host: string, port: number, interval?: number) {
|
||||||
for (const [username, player] of this.players) {
|
(async () => {
|
||||||
if (player.uuid == uuid)
|
while (true) {
|
||||||
return player
|
const motd = await Motd.MOTD.generateMOTDFromPing(host, port).catch(
|
||||||
}
|
(err) => {
|
||||||
return null
|
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;
|
||||||
|
if (!this.config.origins.allowOfflineDownloads && origin == null) {
|
||||||
|
socket.destroy();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
this.config.origins.originBlacklist != null &&
|
||||||
|
this.config.origins.originBlacklist.some((host) =>
|
||||||
|
Util.areDomainsEqual(host, origin)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public fetchUserByUUID(uuid: MineProtocol.UUID): Player | null {
|
||||||
|
for (const [username, player] of this.players) {
|
||||||
|
if (player.uuid == uuid) return player;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ProxyEvents {
|
interface ProxyEvents {
|
||||||
'playerConnect': (player: Player) => void,
|
playerConnect: (player: Player) => void;
|
||||||
'playerDisconnect': (player: Player) => void
|
playerDisconnect: (player: Player) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export declare interface Proxy {
|
export declare interface Proxy {
|
||||||
on<U extends keyof ProxyEvents>(
|
on<U extends keyof ProxyEvents>(event: U, listener: ProxyEvents[U]): this;
|
||||||
event: U, listener: ProxyEvents[U]
|
|
||||||
): this;
|
emit<U extends keyof ProxyEvents>(
|
||||||
|
event: U,
|
||||||
emit<U extends keyof ProxyEvents>(
|
...args: Parameters<ProxyEvents[U]>
|
||||||
event: U, ...args: Parameters<ProxyEvents[U]>
|
): boolean;
|
||||||
): boolean;
|
}
|
||||||
}
|
|
||||||
|
|
|
@ -1,198 +1,247 @@
|
||||||
import { createHash } from "crypto";
|
import { createHash } from "crypto";
|
||||||
import {
|
import { encodeULEB128, decodeULEB128 } from "@thi.ng/leb128";
|
||||||
encodeULEB128,
|
|
||||||
decodeULEB128,
|
|
||||||
} from "@thi.ng/leb128"
|
|
||||||
import { Chat } from "./Chat.js";
|
import { Chat } from "./Chat.js";
|
||||||
import { WebSocket } from "ws";
|
import { WebSocket } from "ws";
|
||||||
import { Enums } from "./Enums.js";
|
import { Enums } from "./Enums.js";
|
||||||
import { Player } from "./Player.js";
|
import { Player } from "./Player.js";
|
||||||
import * as http from "http"
|
import * as http from "http";
|
||||||
import { Config } from "../launcher_types.js";
|
import { Config } from "../launcher_types.js";
|
||||||
import { parseDomain, ParseResultType } from "parse-domain";
|
import { parseDomain, ParseResultType } from "parse-domain";
|
||||||
import { access, readdir } from "fs/promises";
|
import { access, readdir } from "fs/promises";
|
||||||
import { resolve } from "path";
|
import { resolve } from "path";
|
||||||
|
|
||||||
export namespace Util {
|
export namespace Util {
|
||||||
export const encodeVarInt: typeof encodeULEB128 = encodeULEB128
|
export const encodeVarInt: typeof encodeULEB128 = encodeULEB128;
|
||||||
export const decodeVarInt: typeof decodeULEB128 = decodeULEB128
|
export const decodeVarInt: typeof decodeULEB128 = decodeULEB128;
|
||||||
|
|
||||||
// annotation for range
|
// annotation for range
|
||||||
// b = beginning, e = end
|
// b = beginning, e = end
|
||||||
export type Range<B, E> = number
|
export type Range<B, E> = number;
|
||||||
|
|
||||||
export type BoundedBuffer<S extends number> = Buffer
|
export type BoundedBuffer<S extends number> = Buffer;
|
||||||
|
|
||||||
const USERNAME_REGEX = /[^0-9^a-z^A-Z^_]/gi
|
const USERNAME_REGEX = /[^0-9^a-z^A-Z^_]/gi;
|
||||||
|
|
||||||
export function generateUUIDFromPlayer(user: string): string {
|
export function generateUUIDFromPlayer(user: string): string {
|
||||||
const str = `OfflinePlayer:${user}`
|
const str = `OfflinePlayer:${user}`;
|
||||||
let md5Bytes = createHash('md5').update(str).digest()
|
let md5Bytes = createHash("md5").update(str).digest();
|
||||||
md5Bytes[6] &= 0x0f; /* clear version */
|
md5Bytes[6] &= 0x0f; /* clear version */
|
||||||
md5Bytes[6] |= 0x30; /* set to version 3 */
|
md5Bytes[6] |= 0x30; /* set to version 3 */
|
||||||
md5Bytes[8] &= 0x3f; /* clear variant */
|
md5Bytes[8] &= 0x3f; /* clear variant */
|
||||||
md5Bytes[8] |= 0x80; /* set to IETF variant */
|
md5Bytes[8] |= 0x80; /* set to IETF variant */
|
||||||
return uuidBufferToString(md5Bytes)
|
return uuidBufferToString(md5Bytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
// excerpt from uuid-buffer
|
// excerpt from uuid-buffer
|
||||||
|
|
||||||
export function uuidStringToBuffer(uuid: string): Buffer {
|
export function uuidStringToBuffer(uuid: string): Buffer {
|
||||||
if (!uuid) return Buffer.alloc(16); // Return empty buffer
|
if (!uuid) return Buffer.alloc(16); // Return empty buffer
|
||||||
const hexStr = uuid.replace(/-/g, '');
|
const hexStr = uuid.replace(/-/g, "");
|
||||||
if (uuid.length != 36 || hexStr.length != 32) throw new Error(`Invalid UUID string: ${uuid}`);
|
if (uuid.length != 36 || hexStr.length != 32)
|
||||||
return Buffer.from(hexStr, 'hex');
|
throw new Error(`Invalid UUID string: ${uuid}`);
|
||||||
}
|
return Buffer.from(hexStr, "hex");
|
||||||
|
}
|
||||||
|
|
||||||
export function uuidBufferToString(buffer: Buffer): string {
|
export function uuidBufferToString(buffer: Buffer): string {
|
||||||
if (buffer.length != 16) throw new Error(`Invalid buffer length for uuid: ${buffer.length}`);
|
if (buffer.length != 16)
|
||||||
if (buffer.equals(Buffer.alloc(16))) return null; // If buffer is all zeros, return null
|
throw new Error(`Invalid buffer length for uuid: ${buffer.length}`);
|
||||||
const str = buffer.toString('hex');
|
if (buffer.equals(Buffer.alloc(16))) return null; // If buffer is all zeros, return null
|
||||||
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(
|
||||||
return new Promise<Buffer>((res, rej) => {
|
ws: WebSocket,
|
||||||
let resolved = false
|
filter?: (msg: Buffer) => boolean
|
||||||
const msgCb = (msg: any) => {
|
): Promise<Buffer> {
|
||||||
if (filter != null && filter(msg)) {
|
return new Promise<Buffer>((res, rej) => {
|
||||||
resolved = true
|
let resolved = false;
|
||||||
ws.removeListener('message', msgCb)
|
const msgCb = (msg: any) => {
|
||||||
ws.removeListener('close', discon)
|
if (filter != null && filter(msg)) {
|
||||||
ws.setMaxListeners(ws.getMaxListeners() - 2 < 0 ? 5 : ws.getMaxListeners() - 2)
|
resolved = true;
|
||||||
res(msg)
|
ws.removeListener("message", msgCb);
|
||||||
} else if (filter == null) {
|
ws.removeListener("close", discon);
|
||||||
resolved = true
|
ws.setMaxListeners(
|
||||||
ws.removeListener('message', msgCb)
|
ws.getMaxListeners() - 2 < 0 ? 5 : ws.getMaxListeners() - 2
|
||||||
ws.removeListener('close', discon)
|
);
|
||||||
ws.setMaxListeners(ws.getMaxListeners() - 2 < 0 ? 5 : ws.getMaxListeners() - 2)
|
res(msg);
|
||||||
res(msg)
|
} else if (filter == null) {
|
||||||
}
|
resolved = true;
|
||||||
}
|
ws.removeListener("message", msgCb);
|
||||||
const discon = () => {
|
ws.removeListener("close", discon);
|
||||||
resolved = true
|
ws.setMaxListeners(
|
||||||
ws.removeListener('message', msgCb)
|
ws.getMaxListeners() - 2 < 0 ? 5 : ws.getMaxListeners() - 2
|
||||||
ws.removeListener('close', discon)
|
);
|
||||||
ws.setMaxListeners(ws.getMaxListeners() - 2 < 0 ? 5 : ws.getMaxListeners() - 2)
|
res(msg);
|
||||||
rej("Connection closed")
|
}
|
||||||
}
|
};
|
||||||
ws.setMaxListeners(ws.getMaxListeners() + 2)
|
const discon = () => {
|
||||||
ws.on('message', msgCb)
|
resolved = true;
|
||||||
ws.on('close', discon)
|
ws.removeListener("message", msgCb);
|
||||||
setTimeout(() => {
|
ws.removeListener("close", discon);
|
||||||
ws.removeListener('message', msgCb)
|
ws.setMaxListeners(
|
||||||
ws.removeListener('close', discon)
|
ws.getMaxListeners() - 2 < 0 ? 5 : ws.getMaxListeners() - 2
|
||||||
ws.setMaxListeners(ws.getMaxListeners() - 2 < 0 ? 5 : ws.getMaxListeners() - 2)
|
);
|
||||||
rej("Timed out")
|
rej("Connection closed");
|
||||||
}, 10000)
|
};
|
||||||
})
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function validateUsername(user: string): void | never {
|
export function validateUsername(user: string): void | never {
|
||||||
if (user.length > 20)
|
if (user.length > 20) throw new Error("Username is too long!");
|
||||||
throw new Error("Username is too long!")
|
if (user.length < 3) throw new Error("Username is too short!");
|
||||||
if (user.length < 3)
|
if (!!user.match(USERNAME_REGEX))
|
||||||
throw new Error("Username is too short!")
|
throw new Error(
|
||||||
if (!!user.match(USERNAME_REGEX))
|
"Invalid username. Username can only contain alphanumeric characters, and the underscore (_) character."
|
||||||
throw new Error("Invalid username. Username can only contain alphanumeric characters, and the underscore (_) character.")
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function areDomainsEqual(d1: string, d2: string): boolean {
|
export function areDomainsEqual(d1: string, d2: string): boolean {
|
||||||
if (d1.endsWith("*.")) d1 = d1.replace("*.", "WILDCARD-LOL-EXTRA-LONG-SUBDOMAIN-TO-LOWER-CHANCES-OF-COLLISION.")
|
if (d1.endsWith("*."))
|
||||||
const parseResult1 = parseDomain(d1), parseResult2 = parseDomain(d2)
|
d1 = d1.replace(
|
||||||
if (parseResult1.type != ParseResultType.Invalid && parseResult2.type != ParseResultType.Invalid) {
|
"*.",
|
||||||
if (parseResult1.type == ParseResultType.Ip && parseResult2.type == ParseResultType.Ip) {
|
"WILDCARD-LOL-EXTRA-LONG-SUBDOMAIN-TO-LOWER-CHANCES-OF-COLLISION."
|
||||||
return parseResult1.hostname == parseResult2.hostname ? true : false
|
);
|
||||||
} else if (parseResult1.type == ParseResultType.Listed && parseResult2.type == ParseResultType.Listed) {
|
const parseResult1 = parseDomain(d1),
|
||||||
if (parseResult1.subDomains[0] == "WILDCARD-LOL-EXTRA-LONG-SUBDOMAIN-TO-LOWER-CHANCES-OF-COLLISION") {
|
parseResult2 = parseDomain(d2);
|
||||||
// wildcard
|
if (
|
||||||
const domainPlusTld1 = parseResult1.domain + ("." + parseResult1.topLevelDomains.join("."))
|
parseResult1.type != ParseResultType.Invalid &&
|
||||||
const domainPlusTld2 = parseResult2.domain + ("." + parseResult2.topLevelDomains.join("."))
|
parseResult2.type != ParseResultType.Invalid
|
||||||
return domainPlusTld1 == domainPlusTld2 ? true : false
|
) {
|
||||||
} else {
|
if (
|
||||||
// no wildcard
|
parseResult1.type == ParseResultType.Ip &&
|
||||||
return d1 == d2 ? true : false
|
parseResult2.type == ParseResultType.Ip
|
||||||
}
|
) {
|
||||||
} else if (parseResult1.type == ParseResultType.NotListed && parseResult2.type == ParseResultType.NotListed) {
|
return parseResult1.hostname == parseResult2.hostname ? true : false;
|
||||||
if (parseResult1.labels[0] == "WILDCARD-LOL-EXTRA-LONG-SUBDOMAIN-TO-LOWER-CHANCES-OF-COLLISION") {
|
} else if (
|
||||||
// wildcard
|
parseResult1.type == ParseResultType.Listed &&
|
||||||
const domainPlusTld1 = parseResult1.labels.slice(2).join('.')
|
parseResult2.type == ParseResultType.Listed
|
||||||
const domainPlusTld2 = parseResult1.labels.slice(2).join('.')
|
) {
|
||||||
return domainPlusTld1 == domainPlusTld2 ? true : false
|
if (
|
||||||
} else {
|
parseResult1.subDomains[0] ==
|
||||||
// no wildcard
|
"WILDCARD-LOL-EXTRA-LONG-SUBDOMAIN-TO-LOWER-CHANCES-OF-COLLISION"
|
||||||
return d1 == d2 ? true : false
|
) {
|
||||||
}
|
// wildcard
|
||||||
} else if (parseResult1.type == ParseResultType.Reserved && parseResult2.type == ParseResultType.Reserved) {
|
const domainPlusTld1 =
|
||||||
if (parseResult1.hostname == "" && parseResult1.hostname === parseResult2.hostname)
|
parseResult1.domain +
|
||||||
return true
|
("." + parseResult1.topLevelDomains.join("."));
|
||||||
else {
|
const domainPlusTld2 =
|
||||||
// uncertain, fallback to exact hostname matching
|
parseResult2.domain +
|
||||||
return d1 == d2 ? true : false
|
("." + parseResult2.topLevelDomains.join("."));
|
||||||
}
|
return domainPlusTld1 == domainPlusTld2 ? true : false;
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
return false
|
// no wildcard
|
||||||
|
return d1 == d2 ? true : false;
|
||||||
}
|
}
|
||||||
}
|
} else if (
|
||||||
|
parseResult1.type == ParseResultType.NotListed &&
|
||||||
async function* _getFiles(dir: string) {
|
parseResult2.type == ParseResultType.NotListed
|
||||||
const dirents = await readdir(dir, { withFileTypes: true });
|
) {
|
||||||
for (const dirent of dirents) {
|
if (
|
||||||
const res = resolve(dir, dirent.name);
|
parseResult1.labels[0] ==
|
||||||
if (dirent.isDirectory()) {
|
"WILDCARD-LOL-EXTRA-LONG-SUBDOMAIN-TO-LOWER-CHANCES-OF-COLLISION"
|
||||||
yield* _getFiles(res);
|
) {
|
||||||
} else {
|
// wildcard
|
||||||
yield res;
|
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;
|
||||||
}
|
}
|
||||||
}
|
} else if (
|
||||||
|
parseResult1.type == ParseResultType.Reserved &&
|
||||||
export async function recursiveFileSearch(dir: string): Promise<string[]> {
|
parseResult2.type == ParseResultType.Reserved
|
||||||
const ents = []
|
) {
|
||||||
for await (const f of _getFiles(dir)) {
|
if (
|
||||||
ents.push(f)
|
parseResult1.hostname == "" &&
|
||||||
|
parseResult1.hostname === parseResult2.hostname
|
||||||
|
)
|
||||||
|
return true;
|
||||||
|
else {
|
||||||
|
// uncertain, fallback to exact hostname matching
|
||||||
|
return d1 == d2 ? true : false;
|
||||||
}
|
}
|
||||||
return ents
|
}
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function fsExists(path: string): Promise<boolean> {
|
async function* _getFiles(dir: string) {
|
||||||
try { await access(path) }
|
const dirents = await readdir(dir, { withFileTypes: true });
|
||||||
catch (err) {
|
for (const dirent of dirents) {
|
||||||
if (err.code == 'ENOENT')
|
const res = resolve(dir, dirent.name);
|
||||||
return false
|
if (dirent.isDirectory()) {
|
||||||
else return true
|
yield* _getFiles(res);
|
||||||
}
|
} else {
|
||||||
return true
|
yield res;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export type PlayerPosition = {
|
export async function recursiveFileSearch(dir: string): Promise<string[]> {
|
||||||
x: number,
|
const ents = [];
|
||||||
y: number,
|
for await (const f of _getFiles(dir)) {
|
||||||
z: number,
|
ents.push(f);
|
||||||
yaw: number,
|
|
||||||
pitch: number
|
|
||||||
}
|
}
|
||||||
|
return ents;
|
||||||
|
}
|
||||||
|
|
||||||
export type PositionPacket = {
|
export async function fsExists(path: string): Promise<boolean> {
|
||||||
x: number,
|
try {
|
||||||
y: number,
|
await access(path);
|
||||||
z: number,
|
} catch (err) {
|
||||||
yaw: number,
|
if (err.code == "ENOENT") return false;
|
||||||
pitch: number,
|
else return true;
|
||||||
flags: number
|
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
export function generatePositionPacket(currentPos: PlayerPosition, newPos: PositionPacket): PositionPacket {
|
export type PlayerPosition = {
|
||||||
const DEFAULT_RELATIVITY = 0x01 // relative to X-axis
|
x: number;
|
||||||
const newPosPacket = {
|
y: number;
|
||||||
x: newPos.x - (currentPos.x * 2),
|
z: number;
|
||||||
y: newPos.y,
|
yaw: number;
|
||||||
z: newPos.z,
|
pitch: number;
|
||||||
yaw: newPos.yaw,
|
};
|
||||||
pitch: newPos.pitch,
|
|
||||||
flags: DEFAULT_RELATIVITY
|
export type PositionPacket = {
|
||||||
}
|
x: number;
|
||||||
return newPosPacket
|
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
|
||||||
|
const newPosPacket = {
|
||||||
|
x: newPos.x - currentPos.x * 2,
|
||||||
|
y: newPos.y,
|
||||||
|
z: newPos.z,
|
||||||
|
yaw: newPos.yaw,
|
||||||
|
pitch: newPos.pitch,
|
||||||
|
flags: DEFAULT_RELATIVITY,
|
||||||
|
};
|
||||||
|
return newPosPacket;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -4,46 +4,50 @@ import Packet from "../Packet.js";
|
||||||
import { MineProtocol } from "../Protocol.js";
|
import { MineProtocol } from "../Protocol.js";
|
||||||
|
|
||||||
export default class CSLoginPacket implements Packet {
|
export default class CSLoginPacket implements Packet {
|
||||||
packetId: Enums.PacketId = Enums.PacketId.CSLoginPacket
|
packetId: Enums.PacketId = Enums.PacketId.CSLoginPacket;
|
||||||
type: "packet" = "packet"
|
type: "packet" = "packet";
|
||||||
boundTo = Enums.PacketBounds.S
|
boundTo = Enums.PacketBounds.S;
|
||||||
sentAfterHandshake = false
|
sentAfterHandshake = false;
|
||||||
|
|
||||||
networkVersion = NETWORK_VERSION
|
|
||||||
gameVersion = VANILLA_PROTOCOL_VERSION
|
|
||||||
brand: string
|
|
||||||
version: string
|
|
||||||
username: string
|
|
||||||
|
|
||||||
private _getMagicSeq(): Buffer {
|
|
||||||
return Buffer.concat([
|
|
||||||
[0x02, 0x00, 0x02, 0x00, 0x02, 0x00],
|
|
||||||
[this.networkVersion],
|
|
||||||
[0x00, 0x01, 0x00],
|
|
||||||
[this.gameVersion]
|
|
||||||
].map(arr => Buffer.from(arr)))
|
|
||||||
}
|
|
||||||
|
|
||||||
public serialize() {
|
networkVersion = NETWORK_VERSION;
|
||||||
return Buffer.concat(
|
gameVersion = VANILLA_PROTOCOL_VERSION;
|
||||||
[[Enums.PacketId.CSLoginPacket],
|
brand: string;
|
||||||
this._getMagicSeq(),
|
version: string;
|
||||||
MineProtocol.writeString(this.brand),
|
username: string;
|
||||||
MineProtocol.writeString(this.version),
|
|
||||||
[0x00],
|
private _getMagicSeq(): Buffer {
|
||||||
MineProtocol.writeString(this.username)]
|
return Buffer.concat(
|
||||||
.map(arr => arr instanceof Uint8Array ? arr : Buffer.from(arr))
|
[
|
||||||
)
|
[0x02, 0x00, 0x02, 0x00, 0x02, 0x00],
|
||||||
}
|
[this.networkVersion],
|
||||||
public deserialize(packet: Buffer) {
|
[0x00, 0x01, 0x00],
|
||||||
if (packet[0] != this.packetId) throw TypeError("Invalid packet ID detected!")
|
[this.gameVersion],
|
||||||
packet = packet.subarray(1 + this._getMagicSeq().length)
|
].map((arr) => Buffer.from(arr))
|
||||||
const brand = MineProtocol.readString(packet),
|
);
|
||||||
version = MineProtocol.readString(brand.newBuffer),
|
}
|
||||||
username = MineProtocol.readString(version.newBuffer, 1)
|
|
||||||
this.brand = brand.value
|
public serialize() {
|
||||||
this.version = version.value
|
return Buffer.concat(
|
||||||
this.username = username.value
|
[
|
||||||
return this
|
[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)))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
public deserialize(packet: Buffer) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -2,16 +2,16 @@ import { Enums } from "../Enums.js";
|
||||||
import Packet from "../Packet.js";
|
import Packet from "../Packet.js";
|
||||||
|
|
||||||
export class CSReadyPacket implements Packet {
|
export class CSReadyPacket implements Packet {
|
||||||
packetId: Enums.PacketId = Enums.PacketId.CSReadyPacket
|
packetId: Enums.PacketId = Enums.PacketId.CSReadyPacket;
|
||||||
type: "packet" = "packet"
|
type: "packet" = "packet";
|
||||||
boundTo = Enums.PacketBounds.S
|
boundTo = Enums.PacketBounds.S;
|
||||||
sentAfterHandshake = false
|
sentAfterHandshake = false;
|
||||||
|
|
||||||
public serialize() {
|
public serialize() {
|
||||||
return Buffer.from([this.packetId])
|
return Buffer.from([this.packetId]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public deserialize(packet: Buffer) {
|
public deserialize(packet: Buffer) {
|
||||||
return this
|
return this;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,57 +4,67 @@ import Packet from "../Packet.js";
|
||||||
import { MineProtocol } from "../Protocol.js";
|
import { MineProtocol } from "../Protocol.js";
|
||||||
|
|
||||||
export class CSSetSkinPacket implements Packet {
|
export class CSSetSkinPacket implements Packet {
|
||||||
packetId: Enums.PacketId = Enums.PacketId.CSSetSkinPacket
|
packetId: Enums.PacketId = Enums.PacketId.CSSetSkinPacket;
|
||||||
type: "packet" = "packet"
|
type: "packet" = "packet";
|
||||||
boundTo: Enums.PacketBounds = Enums.PacketBounds.S
|
boundTo: Enums.PacketBounds = Enums.PacketBounds.S;
|
||||||
sentAfterHandshake: boolean = false
|
sentAfterHandshake: boolean = false;
|
||||||
|
|
||||||
version: string | 'skin_v1' = 'skin_v1'
|
version: string | "skin_v1" = "skin_v1";
|
||||||
skinType: Omit<Enums.SkinType, 'NOT_LOADED'>
|
skinType: Omit<Enums.SkinType, "NOT_LOADED">;
|
||||||
skinDimensions?: number
|
skinDimensions?: number;
|
||||||
skin?: Buffer
|
skin?: Buffer;
|
||||||
skinId?: number
|
skinId?: number;
|
||||||
|
|
||||||
public serialize() {
|
public serialize() {
|
||||||
if (this.skinType == Enums.SkinType.BUILTIN) {
|
if (this.skinType == Enums.SkinType.BUILTIN) {
|
||||||
return Buffer.concat([
|
return Buffer.concat([
|
||||||
Buffer.from([this.packetId]),
|
Buffer.from([this.packetId]),
|
||||||
MineProtocol.writeString(this.version),
|
MineProtocol.writeString(this.version),
|
||||||
MineProtocol.writeVarInt(this.skinDimensions),
|
MineProtocol.writeVarInt(this.skinDimensions),
|
||||||
this.skin
|
this.skin,
|
||||||
])
|
]);
|
||||||
} else {
|
} else {
|
||||||
return Buffer.concat([
|
return Buffer.concat(
|
||||||
[this.packetId],
|
[
|
||||||
MineProtocol.writeString(this.version),
|
[this.packetId],
|
||||||
Constants.MAGIC_ENDING_CLIENT_UPLOAD_SKIN_BUILTIN,
|
MineProtocol.writeString(this.version),
|
||||||
[this.skinId]
|
Constants.MAGIC_ENDING_CLIENT_UPLOAD_SKIN_BUILTIN,
|
||||||
].map(arr => arr instanceof Uint8Array ? arr : Buffer.from(arr)))
|
[this.skinId],
|
||||||
}
|
].map((arr) => (arr instanceof Uint8Array ? arr : Buffer.from(arr)))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public deserialize(packet: Buffer) {
|
public deserialize(packet: Buffer) {
|
||||||
packet = packet.subarray(1)
|
packet = packet.subarray(1);
|
||||||
const version = MineProtocol.readString(packet)
|
const version = MineProtocol.readString(packet);
|
||||||
let skinType: Enums.SkinType
|
let skinType: Enums.SkinType;
|
||||||
if (!Constants.MAGIC_ENDING_CLIENT_UPLOAD_SKIN_BUILTIN.some((byte, index) => byte !== version.newBuffer[index])) {
|
if (
|
||||||
// built in
|
!Constants.MAGIC_ENDING_CLIENT_UPLOAD_SKIN_BUILTIN.some(
|
||||||
skinType = Enums.SkinType.BUILTIN
|
(byte, index) => byte !== version.newBuffer[index]
|
||||||
const id = MineProtocol.readVarInt(version.newBuffer.subarray(Constants.MAGIC_ENDING_CLIENT_UPLOAD_SKIN_BUILTIN.length))
|
)
|
||||||
this.version = version.value
|
) {
|
||||||
this.skinType = skinType
|
// built in
|
||||||
this.skinId = id.value
|
skinType = Enums.SkinType.BUILTIN;
|
||||||
return this
|
const id = MineProtocol.readVarInt(
|
||||||
} else {
|
version.newBuffer.subarray(
|
||||||
// custom
|
Constants.MAGIC_ENDING_CLIENT_UPLOAD_SKIN_BUILTIN.length
|
||||||
skinType = Enums.SkinType.CUSTOM
|
)
|
||||||
const dimensions = MineProtocol.readVarInt(version.newBuffer),
|
);
|
||||||
skin = dimensions.newBuffer.subarray(3).subarray(0, 16384)
|
this.version = version.value;
|
||||||
this.version = version.value
|
this.skinType = skinType;
|
||||||
this.skinType = skinType
|
this.skinId = id.value;
|
||||||
this.skinDimensions = dimensions.value
|
return this;
|
||||||
this.skin = skin
|
} else {
|
||||||
return this
|
// 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -3,27 +3,29 @@ import Packet from "../Packet.js";
|
||||||
import { MineProtocol } from "../Protocol.js";
|
import { MineProtocol } from "../Protocol.js";
|
||||||
|
|
||||||
export class CSUsernamePacket implements Packet {
|
export class CSUsernamePacket implements Packet {
|
||||||
packetId: Enums.PacketId = Enums.PacketId.CSUsernamePacket
|
packetId: Enums.PacketId = Enums.PacketId.CSUsernamePacket;
|
||||||
type: "packet" = "packet"
|
type: "packet" = "packet";
|
||||||
boundTo = Enums.PacketBounds.S
|
boundTo = Enums.PacketBounds.S;
|
||||||
sentAfterHandshake = false
|
sentAfterHandshake = false;
|
||||||
|
|
||||||
username: string
|
username: string;
|
||||||
static readonly DEFAULT = "default"
|
static readonly DEFAULT = "default";
|
||||||
|
|
||||||
public serialize() {
|
public serialize() {
|
||||||
return Buffer.concat([
|
return Buffer.concat(
|
||||||
[this.packetId],
|
[
|
||||||
MineProtocol.writeString(this.username),
|
[this.packetId],
|
||||||
MineProtocol.writeString(CSUsernamePacket.DEFAULT),
|
MineProtocol.writeString(this.username),
|
||||||
[0x0]
|
MineProtocol.writeString(CSUsernamePacket.DEFAULT),
|
||||||
].map(arr => arr instanceof Uint8Array ? arr : Buffer.from(arr)))
|
[0x0],
|
||||||
}
|
].map((arr) => (arr instanceof Uint8Array ? arr : Buffer.from(arr)))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public deserialize(packet: Buffer) {
|
public deserialize(packet: Buffer) {
|
||||||
packet = packet.subarray(1)
|
packet = packet.subarray(1);
|
||||||
const username = MineProtocol.readString(packet)
|
const username = MineProtocol.readString(packet);
|
||||||
this.username = username.value
|
this.username = username.value;
|
||||||
return this
|
return this;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,27 +4,34 @@ import Packet from "../Packet.js";
|
||||||
import { MineProtocol } from "../Protocol.js";
|
import { MineProtocol } from "../Protocol.js";
|
||||||
|
|
||||||
export default class SCDisconnectPacket implements Packet {
|
export default class SCDisconnectPacket implements Packet {
|
||||||
packetId: Enums.PacketId = Enums.PacketId.SCDisconnectPacket
|
packetId: Enums.PacketId = Enums.PacketId.SCDisconnectPacket;
|
||||||
type: "packet" = "packet"
|
type: "packet" = "packet";
|
||||||
boundTo = Enums.PacketBounds.C
|
boundTo = Enums.PacketBounds.C;
|
||||||
sentAfterHandshake = false
|
sentAfterHandshake = false;
|
||||||
static readonly REASON = 0x8
|
static readonly REASON = 0x8;
|
||||||
|
|
||||||
reason: string | Chat.Chat
|
reason: string | Chat.Chat;
|
||||||
|
|
||||||
public serialize() {
|
public serialize() {
|
||||||
const msg = (typeof this.reason == 'string' ? this.reason : Chat.chatToPlainString(this.reason))
|
const msg =
|
||||||
return Buffer.concat([
|
typeof this.reason == "string"
|
||||||
[0xff],
|
? this.reason
|
||||||
MineProtocol.writeVarInt(SCDisconnectPacket.REASON),
|
: Chat.chatToPlainString(this.reason);
|
||||||
MineProtocol.writeString(" " + msg + " ")
|
return Buffer.concat(
|
||||||
].map(arr => arr instanceof Uint8Array ? arr : Buffer.from(arr)))
|
[
|
||||||
}
|
[0xff],
|
||||||
public deserialize(packet: Buffer) {
|
MineProtocol.writeVarInt(SCDisconnectPacket.REASON),
|
||||||
if (packet[0] != this.packetId) throw new Error("Invalid packet ID!")
|
MineProtocol.writeString(" " + msg + " "),
|
||||||
packet = packet.subarray(1 + MineProtocol.writeVarInt(SCDisconnectPacket.REASON).length)
|
].map((arr) => (arr instanceof Uint8Array ? arr : Buffer.from(arr)))
|
||||||
const reason = MineProtocol.readString(packet)
|
);
|
||||||
this.reason = reason.value
|
}
|
||||||
return this
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,40 +1,48 @@
|
||||||
import { NETWORK_VERSION, PROXY_BRANDING, PROXY_VERSION, VANILLA_PROTOCOL_VERSION } from "../../meta.js";
|
import {
|
||||||
|
NETWORK_VERSION,
|
||||||
|
PROXY_BRANDING,
|
||||||
|
PROXY_VERSION,
|
||||||
|
VANILLA_PROTOCOL_VERSION,
|
||||||
|
} from "../../meta.js";
|
||||||
import { Enums } from "../Enums.js";
|
import { Enums } from "../Enums.js";
|
||||||
import Packet from "../Packet.js";
|
import Packet from "../Packet.js";
|
||||||
import { MineProtocol } from "../Protocol.js";
|
import { MineProtocol } from "../Protocol.js";
|
||||||
|
|
||||||
export default class SCIdentifyPacket implements Packet {
|
export default class SCIdentifyPacket implements Packet {
|
||||||
packetId: Enums.PacketId = Enums.PacketId.SCIdentifyPacket
|
packetId: Enums.PacketId = Enums.PacketId.SCIdentifyPacket;
|
||||||
type: "packet" = "packet"
|
type: "packet" = "packet";
|
||||||
boundTo = Enums.PacketBounds.C
|
boundTo = Enums.PacketBounds.C;
|
||||||
sentAfterHandshake = false
|
sentAfterHandshake = false;
|
||||||
|
|
||||||
protocolVer = NETWORK_VERSION
|
protocolVer = NETWORK_VERSION;
|
||||||
gameVersion = VANILLA_PROTOCOL_VERSION
|
gameVersion = VANILLA_PROTOCOL_VERSION;
|
||||||
branding = PROXY_BRANDING
|
branding = PROXY_BRANDING;
|
||||||
version = PROXY_VERSION
|
version = PROXY_VERSION;
|
||||||
|
|
||||||
public serialize() {
|
public serialize() {
|
||||||
return Buffer.concat([
|
return Buffer.concat(
|
||||||
[0x02],
|
[
|
||||||
MineProtocol.writeShort(this.protocolVer),
|
[0x02],
|
||||||
MineProtocol.writeShort(this.gameVersion),
|
MineProtocol.writeShort(this.protocolVer),
|
||||||
MineProtocol.writeString(this.branding),
|
MineProtocol.writeShort(this.gameVersion),
|
||||||
MineProtocol.writeString(this.version),
|
MineProtocol.writeString(this.branding),
|
||||||
[0x00, 0x00, 0x00]
|
MineProtocol.writeString(this.version),
|
||||||
].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) {
|
public deserialize(packet: Buffer) {
|
||||||
if (packet[0] != this.packetId) throw TypeError("Invalid packet ID detected!")
|
if (packet[0] != this.packetId)
|
||||||
packet = packet.subarray(1)
|
throw TypeError("Invalid packet ID detected!");
|
||||||
const protoVer = MineProtocol.readShort(packet),
|
packet = packet.subarray(1);
|
||||||
gameVer = MineProtocol.readShort(protoVer.newBuffer),
|
const protoVer = MineProtocol.readShort(packet),
|
||||||
branding = MineProtocol.readString(gameVer.newBuffer),
|
gameVer = MineProtocol.readShort(protoVer.newBuffer),
|
||||||
version = MineProtocol.readString(branding.newBuffer)
|
branding = MineProtocol.readString(gameVer.newBuffer),
|
||||||
this.gameVersion = gameVer.value
|
version = MineProtocol.readString(branding.newBuffer);
|
||||||
this.branding = branding.value
|
this.gameVersion = gameVer.value;
|
||||||
this.version = version.value
|
this.branding = branding.value;
|
||||||
return this
|
this.version = version.value;
|
||||||
}
|
return this;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -2,16 +2,16 @@ import { Enums } from "../Enums.js";
|
||||||
import Packet from "../Packet.js";
|
import Packet from "../Packet.js";
|
||||||
|
|
||||||
export class SCReadyPacket implements Packet {
|
export class SCReadyPacket implements Packet {
|
||||||
packetId: Enums.PacketId = Enums.PacketId.SCReadyPacket
|
packetId: Enums.PacketId = Enums.PacketId.SCReadyPacket;
|
||||||
type: "packet" = "packet"
|
type: "packet" = "packet";
|
||||||
boundTo = Enums.PacketBounds.C
|
boundTo = Enums.PacketBounds.C;
|
||||||
sentAfterHandshake = false
|
sentAfterHandshake = false;
|
||||||
|
|
||||||
public serialize() {
|
public serialize() {
|
||||||
return Buffer.from([this.packetId])
|
return Buffer.from([this.packetId]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public deserialize(packet: Buffer) {
|
public deserialize(packet: Buffer) {
|
||||||
return this
|
return this;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,28 +4,30 @@ import { MineProtocol } from "../Protocol.js";
|
||||||
import { Util } from "../Util.js";
|
import { Util } from "../Util.js";
|
||||||
|
|
||||||
export class SCSyncUuidPacket implements Packet {
|
export class SCSyncUuidPacket implements Packet {
|
||||||
packetId: Enums.PacketId = Enums.PacketId.SCSyncUuidPacket
|
packetId: Enums.PacketId = Enums.PacketId.SCSyncUuidPacket;
|
||||||
type: "packet" = "packet"
|
type: "packet" = "packet";
|
||||||
boundTo = Enums.PacketBounds.C
|
boundTo = Enums.PacketBounds.C;
|
||||||
sentAfterHandshake = false
|
sentAfterHandshake = false;
|
||||||
|
|
||||||
username: string
|
username: string;
|
||||||
uuid: string
|
uuid: string;
|
||||||
|
|
||||||
public serialize() {
|
public serialize() {
|
||||||
return Buffer.concat([
|
return Buffer.concat(
|
||||||
[this.packetId],
|
[
|
||||||
MineProtocol.writeString(this.username),
|
[this.packetId],
|
||||||
Util.uuidStringToBuffer(this.uuid)
|
MineProtocol.writeString(this.username),
|
||||||
].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)
|
|
||||||
const username = MineProtocol.readString(packet),
|
public deserialize(packet: Buffer) {
|
||||||
uuid = username.newBuffer.subarray(0, 15)
|
packet = packet.subarray(1);
|
||||||
this.username = username.value
|
const username = MineProtocol.readString(packet),
|
||||||
this.uuid = Util.uuidBufferToString(uuid)
|
uuid = username.newBuffer.subarray(0, 15);
|
||||||
return this
|
this.username = username.value;
|
||||||
}
|
this.uuid = Util.uuidBufferToString(uuid);
|
||||||
}
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -3,29 +3,30 @@ import Packet from "../../Packet.js";
|
||||||
import { MineProtocol } from "../../Protocol.js";
|
import { MineProtocol } from "../../Protocol.js";
|
||||||
|
|
||||||
export class CSChannelMessagePacket implements Packet {
|
export class CSChannelMessagePacket implements Packet {
|
||||||
packetId: Enums.PacketId = Enums.PacketId.CSChannelMessagePacket
|
packetId: Enums.PacketId = Enums.PacketId.CSChannelMessagePacket;
|
||||||
type: "packet" = "packet"
|
type: "packet" = "packet";
|
||||||
boundTo = Enums.PacketBounds.S
|
boundTo = Enums.PacketBounds.S;
|
||||||
sentAfterHandshake = true
|
sentAfterHandshake = true;
|
||||||
|
|
||||||
readonly messageType: Enums.ChannelMessageType = Enums.ChannelMessageType.CLIENT
|
readonly messageType: Enums.ChannelMessageType =
|
||||||
channel: string
|
Enums.ChannelMessageType.CLIENT;
|
||||||
data: Buffer
|
channel: string;
|
||||||
|
data: Buffer;
|
||||||
|
|
||||||
public serialize() {
|
public serialize() {
|
||||||
return Buffer.concat([
|
return Buffer.concat(
|
||||||
[this.packetId],
|
[[this.packetId], MineProtocol.writeString(this.channel), this.data].map(
|
||||||
MineProtocol.writeString(this.channel),
|
(arr) => (arr instanceof Uint8Array ? arr : Buffer.from(arr))
|
||||||
this.data
|
)
|
||||||
].map(arr => arr instanceof Uint8Array ? arr : Buffer.from(arr)))
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public deserialize(packet: Buffer) {
|
public deserialize(packet: Buffer) {
|
||||||
packet = packet.subarray(1)
|
packet = packet.subarray(1);
|
||||||
const channel = MineProtocol.readString(packet),
|
const channel = MineProtocol.readString(packet),
|
||||||
data = channel.newBuffer
|
data = channel.newBuffer;
|
||||||
this.channel = channel.value
|
this.channel = channel.value;
|
||||||
this.data = data
|
this.data = data;
|
||||||
return this
|
return this;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,29 +3,30 @@ import Packet from "../../Packet.js";
|
||||||
import { MineProtocol } from "../../Protocol.js";
|
import { MineProtocol } from "../../Protocol.js";
|
||||||
|
|
||||||
export class SCChannelMessagePacket implements Packet {
|
export class SCChannelMessagePacket implements Packet {
|
||||||
packetId: Enums.PacketId = Enums.PacketId.SCChannelMessagePacket
|
packetId: Enums.PacketId = Enums.PacketId.SCChannelMessagePacket;
|
||||||
type: "packet" = "packet"
|
type: "packet" = "packet";
|
||||||
boundTo = Enums.PacketBounds.C
|
boundTo = Enums.PacketBounds.C;
|
||||||
sentAfterHandshake = true
|
sentAfterHandshake = true;
|
||||||
|
|
||||||
readonly messageType: Enums.ChannelMessageType = Enums.ChannelMessageType.SERVER
|
|
||||||
channel: string
|
|
||||||
data: Buffer
|
|
||||||
|
|
||||||
public serialize() {
|
readonly messageType: Enums.ChannelMessageType =
|
||||||
return Buffer.concat([
|
Enums.ChannelMessageType.SERVER;
|
||||||
[this.packetId],
|
channel: string;
|
||||||
MineProtocol.writeString(this.channel),
|
data: Buffer;
|
||||||
this.data
|
|
||||||
].map(arr => arr instanceof Uint8Array ? arr : Buffer.from(arr)))
|
|
||||||
}
|
|
||||||
|
|
||||||
public deserialize(packet: Buffer) {
|
public serialize() {
|
||||||
packet = packet.subarray(1)
|
return Buffer.concat(
|
||||||
const channel = MineProtocol.readString(packet),
|
[[this.packetId], MineProtocol.writeString(this.channel), this.data].map(
|
||||||
data = channel.newBuffer
|
(arr) => (arr instanceof Uint8Array ? arr : Buffer.from(arr))
|
||||||
this.channel = channel.value
|
)
|
||||||
this.data = data
|
);
|
||||||
return this
|
}
|
||||||
}
|
|
||||||
}
|
public deserialize(packet: Buffer) {
|
||||||
|
packet = packet.subarray(1);
|
||||||
|
const channel = MineProtocol.readString(packet),
|
||||||
|
data = channel.newBuffer;
|
||||||
|
this.channel = channel.value;
|
||||||
|
this.data = data;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,68 +1,68 @@
|
||||||
export namespace PluginLoaderTypes {
|
export namespace PluginLoaderTypes {
|
||||||
/**
|
/**
|
||||||
* ## SemVer
|
* ## SemVer
|
||||||
* Abstract typing to define a semantic version string. Refer to https://semver.org/ for more details.
|
* Abstract typing to define a semantic version string. Refer to https://semver.org/ for more details.
|
||||||
*/
|
*/
|
||||||
export type SemVer = string
|
export type SemVer = string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ## SemVerReq
|
* ## SemVerReq
|
||||||
* Abstract typing to define a semantic version requirement. Refer to https://semver.org/ for more details.
|
* Abstract typing to define a semantic version requirement. Refer to https://semver.org/ for more details.
|
||||||
*/
|
*/
|
||||||
export type SemVerReq = string
|
export type SemVerReq = string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ## PluginMetadata
|
* ## PluginMetadata
|
||||||
* Data structure of the JSON plugin metadata stored in `metadata.json`. Below is an example plugin metadata object.
|
* Data structure of the JSON plugin metadata stored in `metadata.json`. Below is an example plugin metadata object.
|
||||||
* @example
|
* @example
|
||||||
* {
|
* {
|
||||||
* name: "ExamplePlugin",
|
* name: "ExamplePlugin",
|
||||||
* version: "1.0.0"
|
* version: "1.0.0"
|
||||||
* }
|
* }
|
||||||
*
|
*
|
||||||
* @property {string} name - The name of the plugin. Spaces are allowed, and this will be shown to the end user.
|
* @property {string} name - The name of the plugin. Spaces are allowed, and this will be shown to the end user.
|
||||||
* @property {string} id - The internal ID of the plugin. Spaces are not allowed, and any ID conflicts will cause the proxy to not load.
|
* @property {string} id - The internal ID of the plugin. Spaces are not allowed, and any ID conflicts will cause the proxy to not load.
|
||||||
* @property {PluginLoaderTypes.SemVer} version - The version of the plugin. Must follow SemVer guidelines.
|
* @property {PluginLoaderTypes.SemVer} version - The version of the plugin. Must follow SemVer guidelines.
|
||||||
* @property {string} entry_point - Reference to the entry point JS file of the plugin. Is relative to the file of the `metadata.json`.
|
* @property {string} entry_point - Reference to the entry point JS file of the plugin. Is relative to the file of the `metadata.json`.
|
||||||
* @property {PluginLoaderTypes.PluginRequirement[]} requirements - The plugin requirement(s) of the plugin. Proxy will not load if any requirement cannot be satisfied.
|
* @property {PluginLoaderTypes.PluginRequirement[]} requirements - The plugin requirement(s) of the plugin. Proxy will not load if any requirement cannot be satisfied.
|
||||||
* @property {string[]} load_after - Defines what plugin(s) to be loaded first before this plugin is loaded.
|
* @property {string[]} load_after - Defines what plugin(s) to be loaded first before this plugin is loaded.
|
||||||
*/
|
*/
|
||||||
export type PluginMetadata = {
|
export type PluginMetadata = {
|
||||||
name: string,
|
name: string;
|
||||||
id: string,
|
id: string;
|
||||||
version: SemVer,
|
version: SemVer;
|
||||||
entry_point: string,
|
entry_point: string;
|
||||||
requirements: PluginRequirement[],
|
requirements: PluginRequirement[];
|
||||||
incompatibilities: PluginRequirement[],
|
incompatibilities: PluginRequirement[];
|
||||||
load_after: string[]
|
load_after: string[];
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ## PluginMetadataPathed
|
* ## PluginMetadataPathed
|
||||||
* Internal typing. Provides a path to the plugin metadata file.
|
* Internal typing. Provides a path to the plugin metadata file.
|
||||||
*/
|
*/
|
||||||
export type PluginMetadataPathed = PluginMetadata & { path: string }
|
export type PluginMetadataPathed = PluginMetadata & { path: string };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ## PluginLoadOrder
|
* ## PluginLoadOrder
|
||||||
* Internal typing. Provides a loading order for plugin loading.
|
* Internal typing. Provides a loading order for plugin loading.
|
||||||
*/
|
*/
|
||||||
export type PluginLoadOrder = string[]
|
export type PluginLoadOrder = string[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ## PluginRequirement
|
* ## PluginRequirement
|
||||||
* A plugin requirement used to define dependencies for a specific plugin.
|
* A plugin requirement used to define dependencies for a specific plugin.
|
||||||
* Semantic versions may be used for the attribute `version`, and you can
|
* Semantic versions may be used for the attribute `version`, and you can
|
||||||
* use `eaglerproxy` to define a requirement for the proxy version.
|
* use `eaglerproxy` to define a requirement for the proxy version.
|
||||||
* @example
|
* @example
|
||||||
* {
|
* {
|
||||||
* id: "eaglerproxy"
|
* id: "eaglerproxy"
|
||||||
* }
|
* }
|
||||||
* @property {string} id - The ID of the plugin to be used as a requirement.
|
* @property {string} id - The ID of the plugin to be used as a requirement.
|
||||||
* @property {PluginLoaderTypes.SemVerReq} version - The SemVer requirement for the requirement.
|
* @property {PluginLoaderTypes.SemVerReq} version - The SemVer requirement for the requirement.
|
||||||
*/
|
*/
|
||||||
export type PluginRequirement = {
|
export type PluginRequirement = {
|
||||||
id: string,
|
id: string;
|
||||||
version: SemVerReq | 'any'
|
version: SemVerReq | "any";
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { Stats } from "fs";
|
import { Stats } from "fs";
|
||||||
import * as fs from "fs/promises";
|
import * as fs from "fs/promises";
|
||||||
import * as pathUtil from "path"
|
import * as pathUtil from "path";
|
||||||
import * as semver from "semver"
|
import * as semver from "semver";
|
||||||
import { EventEmitter } from "events";
|
import { EventEmitter } from "events";
|
||||||
import { pathToFileURL } from "url";
|
import { pathToFileURL } from "url";
|
||||||
import { Logger } from "../../logger.js";
|
import { Logger } from "../../logger.js";
|
||||||
|
@ -10,7 +10,7 @@ import { Proxy } from "../Proxy.js";
|
||||||
import { Util } from "../Util.js";
|
import { Util } from "../Util.js";
|
||||||
import { PluginLoaderTypes } from "./PluginLoaderTypes.js";
|
import { PluginLoaderTypes } from "./PluginLoaderTypes.js";
|
||||||
import { Enums } from "../Enums.js";
|
import { Enums } from "../Enums.js";
|
||||||
import { Chat } from "../Chat.js"
|
import { Chat } from "../Chat.js";
|
||||||
import { Constants } from "../Constants.js";
|
import { Constants } from "../Constants.js";
|
||||||
import { Motd } from "../Motd.js";
|
import { Motd } from "../Motd.js";
|
||||||
import { Player } from "../Player.js";
|
import { Player } from "../Player.js";
|
||||||
|
@ -19,248 +19,401 @@ import { EaglerSkins } from "../skins/EaglerSkins.js";
|
||||||
import { BungeeUtil } from "../BungeeUtil.js";
|
import { BungeeUtil } from "../BungeeUtil.js";
|
||||||
|
|
||||||
export class PluginManager extends EventEmitter {
|
export class PluginManager extends EventEmitter {
|
||||||
public plugins: Map<string, { exports: any, metadata: PluginLoaderTypes.PluginMetadataPathed }>
|
public plugins: Map<
|
||||||
public proxy: Proxy
|
string,
|
||||||
|
{ exports: any; metadata: PluginLoaderTypes.PluginMetadataPathed }
|
||||||
|
>;
|
||||||
|
public proxy: Proxy;
|
||||||
|
|
||||||
public Logger: typeof Logger = Logger
|
public Logger: typeof Logger = Logger;
|
||||||
public Enums: typeof Enums = Enums
|
public Enums: typeof Enums = Enums;
|
||||||
public Chat: typeof Chat = Chat
|
public Chat: typeof Chat = Chat;
|
||||||
public Constants: typeof Constants = Constants
|
public Constants: typeof Constants = Constants;
|
||||||
public Motd: typeof Motd = Motd
|
public Motd: typeof Motd = Motd;
|
||||||
public Player: typeof Player = Player
|
public Player: typeof Player = Player;
|
||||||
public MineProtocol: typeof MineProtocol = MineProtocol
|
public MineProtocol: typeof MineProtocol = MineProtocol;
|
||||||
public EaglerSkins: typeof EaglerSkins = EaglerSkins
|
public EaglerSkins: typeof EaglerSkins = EaglerSkins;
|
||||||
public Util: typeof Util = Util
|
public Util: typeof Util = Util;
|
||||||
public BungeeUtil: typeof BungeeUtil = BungeeUtil
|
public BungeeUtil: typeof BungeeUtil = BungeeUtil;
|
||||||
|
|
||||||
private _loadDir: string
|
private _loadDir: string;
|
||||||
private _logger: Logger
|
private _logger: Logger;
|
||||||
|
|
||||||
constructor(loadDir: string) {
|
constructor(loadDir: string) {
|
||||||
super()
|
super();
|
||||||
this.setMaxListeners(0)
|
this.setMaxListeners(0);
|
||||||
this._loadDir = loadDir
|
this._loadDir = loadDir;
|
||||||
this.plugins = new Map()
|
this.plugins = new Map();
|
||||||
this.Logger = Logger
|
this.Logger = Logger;
|
||||||
this._logger = new this.Logger('PluginManager')
|
this._logger = new this.Logger("PluginManager");
|
||||||
|
}
|
||||||
|
|
||||||
|
public async loadPlugins() {
|
||||||
|
this._logger.info("Loading plugin metadata files...");
|
||||||
|
const pluginMeta = await this._findPlugins(this._loadDir);
|
||||||
|
await this._validatePluginList(pluginMeta);
|
||||||
|
|
||||||
|
let pluginsString = "";
|
||||||
|
for (const [id, plugin] of pluginMeta) {
|
||||||
|
pluginsString += `${id}@${plugin.version}`;
|
||||||
}
|
}
|
||||||
|
pluginsString = pluginsString.substring(0, pluginsString.length - 1);
|
||||||
|
this._logger.info(`Found ${pluginMeta.size} plugin(s): ${pluginsString}`);
|
||||||
|
|
||||||
public async loadPlugins() {
|
this._logger.info(`Loading ${pluginMeta.size} plugin(s)...`);
|
||||||
this._logger.info("Loading plugin metadata files...")
|
const successLoadCount = await this._loadPlugins(
|
||||||
const pluginMeta = await this._findPlugins(this._loadDir)
|
pluginMeta,
|
||||||
await this._validatePluginList(pluginMeta)
|
this._getLoadOrder(pluginMeta)
|
||||||
|
);
|
||||||
|
this._logger.info(`Successfully loaded ${successLoadCount} plugin(s).`);
|
||||||
|
this.emit("pluginsFinishLoading", this);
|
||||||
|
}
|
||||||
|
|
||||||
let pluginsString = ''
|
private async _findPlugins(
|
||||||
for (const [id, plugin] of pluginMeta) {
|
dir: string
|
||||||
pluginsString += `${id}@${plugin.version}`
|
): 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}`);
|
||||||
|
} else {
|
||||||
|
const metadataPath = pathUtil.resolve(
|
||||||
|
pathUtil.join(path, "metadata.json")
|
||||||
|
);
|
||||||
|
let metadata: PluginLoaderTypes.PluginMetadata;
|
||||||
|
try {
|
||||||
|
const file = await fs.readFile(metadataPath);
|
||||||
|
metadata = JSON.parse(file.toString());
|
||||||
|
// do some type checking
|
||||||
|
if (typeof metadata.name != "string")
|
||||||
|
throw new TypeError(
|
||||||
|
"<metadata>.name is either null or not of a string type!"
|
||||||
|
);
|
||||||
|
if (typeof metadata.id != "string")
|
||||||
|
throw new TypeError(
|
||||||
|
"<metadata>.id is either null or not of a string type!"
|
||||||
|
);
|
||||||
|
if (/ /gm.test(metadata.id))
|
||||||
|
throw new Error(`<metadata>.id contains whitespace!`);
|
||||||
|
if (!semver.valid(metadata.version))
|
||||||
|
throw new Error(
|
||||||
|
"<metadata>.version is either null, not a string, or is not a valid SemVer!"
|
||||||
|
);
|
||||||
|
if (typeof metadata.entry_point != "string")
|
||||||
|
throw new TypeError(
|
||||||
|
"<metadata>.entry_point is either null or not a string!"
|
||||||
|
);
|
||||||
|
if (!metadata.entry_point.endsWith(".js"))
|
||||||
|
throw new Error(
|
||||||
|
`<metadata>.entry_point (${metadata.entry_point}) references a non-JavaScript file!`
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
!(await Util.fsExists(pathUtil.resolve(path, metadata.entry_point)))
|
||||||
|
)
|
||||||
|
throw new Error(
|
||||||
|
`<metadata>.entry_point (${metadata.entry_point}) references a non-existent file!`
|
||||||
|
);
|
||||||
|
if (metadata.requirements instanceof Array == false)
|
||||||
|
throw new TypeError(
|
||||||
|
"<metadata>.requirements is either null or not an array!"
|
||||||
|
);
|
||||||
|
for (const requirement of metadata.requirements as PluginLoaderTypes.PluginMetadata["requirements"]) {
|
||||||
|
if (typeof requirement != "object" || requirement == null)
|
||||||
|
throw new TypeError(
|
||||||
|
`<metadata>.requirements[${(
|
||||||
|
metadata.requirements as any
|
||||||
|
).indexOf(requirement)}] is either null or not an object!`
|
||||||
|
);
|
||||||
|
if (typeof requirement.id != "string")
|
||||||
|
throw new TypeError(
|
||||||
|
`<metadata>.requirements[${(
|
||||||
|
metadata.requirements as any
|
||||||
|
).indexOf(requirement)}].id is either null or not a string!`
|
||||||
|
);
|
||||||
|
if (/ /gm.test(requirement.id))
|
||||||
|
throw new TypeError(
|
||||||
|
`<metadata>.requirements[${(
|
||||||
|
metadata.requirements as any
|
||||||
|
).indexOf(requirement)}].id contains whitespace!`
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
semver.validRange(requirement.version) == null &&
|
||||||
|
requirement.version != "any"
|
||||||
|
)
|
||||||
|
throw new TypeError(
|
||||||
|
`<metadata>.requirements[${(
|
||||||
|
metadata.requirements as any
|
||||||
|
).indexOf(
|
||||||
|
requirement
|
||||||
|
)}].version is either null or not a valid SemVer!`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (metadata.load_after instanceof Array == false)
|
||||||
|
throw new TypeError(
|
||||||
|
"<metadata>.load_after is either null or not an array!"
|
||||||
|
);
|
||||||
|
for (const loadReq of metadata.load_after as string[]) {
|
||||||
|
if (typeof loadReq != "string")
|
||||||
|
throw new TypeError(
|
||||||
|
`<metadata>.load_after[${(metadata.load_after as any).indexOf(
|
||||||
|
loadReq
|
||||||
|
)}] is either null, or not a valid ID!`
|
||||||
|
);
|
||||||
|
if (/ /gm.test(loadReq))
|
||||||
|
throw new TypeError(
|
||||||
|
`<metadata>.load_after[${(metadata.load_after as any).indexOf(
|
||||||
|
loadReq
|
||||||
|
)}] contains whitespace!`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (metadata.incompatibilities instanceof Array == false)
|
||||||
|
throw new TypeError(
|
||||||
|
"<metadata>.incompatibilities is either null or not an array!"
|
||||||
|
);
|
||||||
|
for (const incompatibility of metadata.incompatibilities as PluginLoaderTypes.PluginMetadata["requirements"]) {
|
||||||
|
if (typeof incompatibility != "object" || incompatibility == null)
|
||||||
|
throw new TypeError(
|
||||||
|
`<metadata>.incompatibilities[${(
|
||||||
|
metadata.load_after as any
|
||||||
|
).indexOf(incompatibility)}] is either null or not an object!`
|
||||||
|
);
|
||||||
|
if (typeof incompatibility.id != "string")
|
||||||
|
throw new TypeError(
|
||||||
|
`<metadata>.incompatibilities[${(
|
||||||
|
metadata.load_after as any
|
||||||
|
).indexOf(incompatibility)}].id is either null or not a string!`
|
||||||
|
);
|
||||||
|
if (/ /gm.test(incompatibility.id))
|
||||||
|
throw new TypeError(
|
||||||
|
`<metadata>.incompatibilities[${(
|
||||||
|
metadata.load_after as any
|
||||||
|
).indexOf(incompatibility)}].id contains whitespace!`
|
||||||
|
);
|
||||||
|
if (semver.validRange(incompatibility.version) == null)
|
||||||
|
throw new TypeError(
|
||||||
|
`<metadata>.incompatibilities[${(
|
||||||
|
metadata.load_after as any
|
||||||
|
).indexOf(
|
||||||
|
incompatibility
|
||||||
|
)}].version is either null or not a valid SemVer!`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (ret.has(metadata.id))
|
||||||
|
throw new Error(
|
||||||
|
`Duplicate plugin ID detected: ${metadata.id}. Are there duplicate plugins in the plugin folder?`
|
||||||
|
);
|
||||||
|
ret.set(metadata.id, {
|
||||||
|
path: pathUtil.resolve(path),
|
||||||
|
...metadata,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
this._logger.warn(
|
||||||
|
`Failed to load plugin metadata file at ${metadataPath}: ${
|
||||||
|
err.stack ?? err
|
||||||
|
}`
|
||||||
|
);
|
||||||
|
this._logger.warn("This plugin will skip loading due to an error.");
|
||||||
}
|
}
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
private async _findPlugins(dir: string): Promise<Map<string, PluginLoaderTypes.PluginMetadataPathed>> {
|
private async _validatePluginList(
|
||||||
const ret: Map<string, PluginLoaderTypes.PluginMetadataPathed> = new Map()
|
plugins: Map<string, PluginLoaderTypes.PluginMetadataPathed>
|
||||||
const lsRes = await Promise.all((await fs.readdir(dir))
|
) {
|
||||||
.filter(ent => !ent.endsWith(".disabled"))
|
for (const [id, plugin] of plugins) {
|
||||||
.map(async res => [pathUtil.join(dir, res), await fs.stat(pathUtil.join(dir, res))])) as [string, Stats][]
|
for (const req of plugin.requirements) {
|
||||||
for (const [path, details] of lsRes) {
|
if (
|
||||||
if (details.isFile()) {
|
!plugins.has(req.id) &&
|
||||||
if (path.endsWith('.jar')) {
|
req.id != "eaglerproxy" &&
|
||||||
this._logger.warn(`Non-EaglerProxy plugin found! (${path})`)
|
!req.id.startsWith("module:")
|
||||||
this._logger.warn(`BungeeCord plugins are NOT supported! Only custom EaglerProxy plugins are allowed.`)
|
) {
|
||||||
} else if (path.endsWith('.zip')) {
|
this._logger.fatal(
|
||||||
this._logger.warn(`.zip file found in plugin directory! (${path})`)
|
`Error whilst loading plugins: Plugin ${plugin.name}@${plugin.version} requires plugin ${req.id}@${req.version}, but it is not found!`
|
||||||
this._logger.warn(`A .zip file was found in the plugins directory! Perhaps you forgot to unzip it?`)
|
);
|
||||||
} else this._logger.debug(`Skipping file found in plugin folder: ${path}`)
|
this._logger.fatal("Loading has halted due to missing dependencies.");
|
||||||
} else {
|
process.exit(1);
|
||||||
const metadataPath = pathUtil.resolve(pathUtil.join(path, 'metadata.json'))
|
}
|
||||||
let metadata: PluginLoaderTypes.PluginMetadata
|
if (req.id == "eaglerproxy") {
|
||||||
try {
|
if (
|
||||||
const file = await fs.readFile(metadataPath)
|
!semver.satisfies(PROXY_VERSION, req.version) &&
|
||||||
metadata = JSON.parse(file.toString())
|
req.version != "any"
|
||||||
// do some type checking
|
) {
|
||||||
if (typeof metadata.name != 'string')
|
this._logger.fatal(
|
||||||
throw new TypeError("<metadata>.name is either null or not of a string type!")
|
`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!`
|
||||||
if (typeof metadata.id != 'string')
|
);
|
||||||
throw new TypeError("<metadata>.id is either null or not of a string type!")
|
this._logger.fatal("Loading has halted due to dependency issues.");
|
||||||
if ((/ /gm).test(metadata.id))
|
process.exit(1);
|
||||||
throw new Error(`<metadata>.id contains whitespace!`)
|
}
|
||||||
if (!semver.valid(metadata.version))
|
} else if (req.id.startsWith("module:")) {
|
||||||
throw new Error("<metadata>.version is either null, not a string, or is not a valid SemVer!")
|
const moduleName = req.id.replace("module:", "");
|
||||||
if (typeof metadata.entry_point != 'string')
|
try {
|
||||||
throw new TypeError("<metadata>.entry_point is either null or not a string!")
|
await import(moduleName);
|
||||||
if (!metadata.entry_point.endsWith('.js'))
|
} catch (err) {
|
||||||
throw new Error(`<metadata>.entry_point (${metadata.entry_point}) references a non-JavaScript file!`)
|
if (err.code == "ERR_MODULE_NOT_FOUND") {
|
||||||
if (!await Util.fsExists(pathUtil.resolve(path, metadata.entry_point)))
|
this._logger.fatal(
|
||||||
throw new Error(`<metadata>.entry_point (${metadata.entry_point}) references a non-existent file!`)
|
`Plugin ${plugin.name}@${
|
||||||
if (metadata.requirements instanceof Array == false)
|
plugin.version
|
||||||
throw new TypeError("<metadata>.requirements is either null or not an array!")
|
} requires NPM module ${moduleName}${
|
||||||
for (const requirement of metadata.requirements as PluginLoaderTypes.PluginMetadata["requirements"]) {
|
req.version == "any" ? "" : `@${req.version}`
|
||||||
if (typeof requirement != 'object' || requirement == null)
|
} to be installed, but it is not found!`
|
||||||
throw new TypeError(`<metadata>.requirements[${(metadata.requirements as any).indexOf(requirement)}] is either null or not an object!`)
|
);
|
||||||
if (typeof requirement.id != 'string')
|
this._logger.fatal(
|
||||||
throw new TypeError(`<metadata>.requirements[${(metadata.requirements as any).indexOf(requirement)}].id is either null or not a string!`)
|
`Please install this missing package by running "npm install ${moduleName}${
|
||||||
if (/ /gm.test(requirement.id))
|
req.version == "any" ? "" : `@${req.version}`
|
||||||
throw new TypeError(`<metadata>.requirements[${(metadata.requirements as any).indexOf(requirement)}].id contains whitespace!`)
|
}". If you're using yarn, run "yarn add ${moduleName}${
|
||||||
if (semver.validRange(requirement.version) == null && requirement.version != 'any')
|
req.version == "any" ? "" : `@${req.version}`
|
||||||
throw new TypeError(`<metadata>.requirements[${(metadata.requirements as any).indexOf(requirement)}].version is either null or not a valid SemVer!`)
|
}" instead.`
|
||||||
}
|
);
|
||||||
if (metadata.load_after instanceof Array == false)
|
this._logger.fatal(
|
||||||
throw new TypeError("<metadata>.load_after is either null or not an array!")
|
"Loading has halted due to dependency issues."
|
||||||
for (const loadReq of metadata.load_after as string[]) {
|
);
|
||||||
if (typeof loadReq != 'string')
|
process.exit(1);
|
||||||
throw new TypeError(`<metadata>.load_after[${(metadata.load_after as any).indexOf(loadReq)}] is either null, or not a valid ID!`)
|
|
||||||
if (/ /gm.test(loadReq))
|
|
||||||
throw new TypeError(`<metadata>.load_after[${(metadata.load_after as any).indexOf(loadReq)}] contains whitespace!`)
|
|
||||||
}
|
|
||||||
if (metadata.incompatibilities instanceof Array == false)
|
|
||||||
throw new TypeError("<metadata>.incompatibilities is either null or not an array!")
|
|
||||||
for (const incompatibility of metadata.incompatibilities as PluginLoaderTypes.PluginMetadata["requirements"]) {
|
|
||||||
if (typeof incompatibility != 'object' || incompatibility == null)
|
|
||||||
throw new TypeError(`<metadata>.incompatibilities[${(metadata.load_after as any).indexOf(incompatibility)}] is either null or not an object!`)
|
|
||||||
if (typeof incompatibility.id != 'string')
|
|
||||||
throw new TypeError(`<metadata>.incompatibilities[${(metadata.load_after as any).indexOf(incompatibility)}].id is either null or not a string!`)
|
|
||||||
if (/ /gm.test(incompatibility.id))
|
|
||||||
throw new TypeError(`<metadata>.incompatibilities[${(metadata.load_after as any).indexOf(incompatibility)}].id contains whitespace!`)
|
|
||||||
if (semver.validRange(incompatibility.version) == null)
|
|
||||||
throw new TypeError(`<metadata>.incompatibilities[${(metadata.load_after as any).indexOf(incompatibility)}].version is either null or not a valid SemVer!`)
|
|
||||||
}
|
|
||||||
if (ret.has(metadata.id))
|
|
||||||
throw new Error(`Duplicate plugin ID detected: ${metadata.id}. Are there duplicate plugins in the plugin folder?`)
|
|
||||||
ret.set(metadata.id, {
|
|
||||||
path: pathUtil.resolve(path),
|
|
||||||
...metadata
|
|
||||||
})
|
|
||||||
} catch (err) {
|
|
||||||
this._logger.warn(`Failed to load plugin metadata file at ${metadataPath}: ${err.stack ?? err}`)
|
|
||||||
this._logger.warn("This plugin will skip loading due to an error.")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
} 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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return ret
|
}
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
} 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async _validatePluginList(plugins: Map<string, PluginLoaderTypes.PluginMetadataPathed>) {
|
private _getLoadOrder(
|
||||||
for (const [id, plugin] of plugins) {
|
plugins: Map<string, PluginLoaderTypes.PluginMetadataPathed>
|
||||||
for (const req of plugin.requirements) {
|
): PluginLoaderTypes.PluginLoadOrder {
|
||||||
if (!plugins.has(req.id) && req.id != 'eaglerproxy' && !req.id.startsWith("module:")) {
|
let order = [],
|
||||||
this._logger.fatal(`Error whilst loading plugins: Plugin ${plugin.name}@${plugin.version} requires plugin ${req.id}@${req.version}, but it is not found!`)
|
lastPlugin: any;
|
||||||
this._logger.fatal("Loading has halted due to missing dependencies.")
|
plugins.forEach((v) => order.push(v.id));
|
||||||
process.exit(1)
|
for (const [id, plugin] of plugins) {
|
||||||
}
|
const load = plugin.load_after.filter((dep) => plugins.has(dep));
|
||||||
if (req.id == 'eaglerproxy') {
|
if (load.length < 0) {
|
||||||
if (!semver.satisfies(PROXY_VERSION, req.version) && req.version != 'any') {
|
order.push(plugin.id);
|
||||||
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!`)
|
} else {
|
||||||
this._logger.fatal("Loading has halted due to dependency issues.")
|
let mostLastIndexFittingDeps = -1;
|
||||||
process.exit(1)
|
for (const loadEnt of load) {
|
||||||
}
|
if (loadEnt != lastPlugin) {
|
||||||
} else if (req.id.startsWith("module:")) {
|
if (order.indexOf(loadEnt) + 1 > mostLastIndexFittingDeps) {
|
||||||
const moduleName = req.id.replace("module:", "")
|
mostLastIndexFittingDeps = order.indexOf(loadEnt) + 1;
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
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)
|
|
||||||
}
|
|
||||||
} 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
if (mostLastIndexFittingDeps != -1) {
|
||||||
|
order.splice(order.indexOf(plugin.id), 1);
|
||||||
|
order.splice(mostLastIndexFittingDeps - 1, 0, plugin.id);
|
||||||
|
lastPlugin = plugin;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
return order;
|
||||||
|
}
|
||||||
|
|
||||||
private _getLoadOrder(plugins: Map<string, PluginLoaderTypes.PluginMetadataPathed>): PluginLoaderTypes.PluginLoadOrder {
|
private async _loadPlugins(
|
||||||
let order = [], lastPlugin: any
|
plugins: Map<string, PluginLoaderTypes.PluginMetadataPathed>,
|
||||||
plugins.forEach(v => order.push(v.id))
|
order: PluginLoaderTypes.PluginLoadOrder
|
||||||
for (const [id, plugin] of plugins) {
|
): Promise<number> {
|
||||||
const load = plugin.load_after.filter(dep => plugins.has(dep))
|
let successCount = 0;
|
||||||
if (load.length < 0) {
|
for (const id of order) {
|
||||||
order.push(plugin.id)
|
let pluginMeta = plugins.get(id);
|
||||||
} else {
|
try {
|
||||||
let mostLastIndexFittingDeps = -1
|
const imp = await import(
|
||||||
for (const loadEnt of load) {
|
process.platform == "win32"
|
||||||
if (loadEnt != lastPlugin) {
|
? pathToFileURL(
|
||||||
if (order.indexOf(loadEnt) + 1 > mostLastIndexFittingDeps) {
|
pathUtil.join(pluginMeta.path, pluginMeta.entry_point)
|
||||||
mostLastIndexFittingDeps = order.indexOf(loadEnt) + 1
|
).toString()
|
||||||
}
|
: pathUtil.join(pluginMeta.path, pluginMeta.entry_point)
|
||||||
}
|
);
|
||||||
}
|
this.plugins.set(pluginMeta.id, {
|
||||||
if (mostLastIndexFittingDeps != -1) {
|
exports: imp,
|
||||||
order.splice(order.indexOf(plugin.id), 1)
|
metadata: pluginMeta,
|
||||||
order.splice(mostLastIndexFittingDeps - 1, 0, plugin.id)
|
});
|
||||||
lastPlugin = plugin
|
successCount++;
|
||||||
}
|
this.emit("pluginLoad", pluginMeta.id, imp);
|
||||||
}
|
} catch (err) {
|
||||||
}
|
this._logger.warn(
|
||||||
return order
|
`Failed to load plugin entry point for plugin (${
|
||||||
}
|
pluginMeta.name
|
||||||
|
}) at ${pluginMeta.path}: ${err.stack ?? err}`
|
||||||
private async _loadPlugins(plugins: Map<string, PluginLoaderTypes.PluginMetadataPathed>, order: PluginLoaderTypes.PluginLoadOrder): Promise<number> {
|
);
|
||||||
let successCount = 0
|
this._logger.warn("This plugin will skip loading due to an error.");
|
||||||
for (const id of order) {
|
}
|
||||||
let pluginMeta = plugins.get(id)
|
return successCount;
|
||||||
try {
|
|
||||||
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)
|
|
||||||
} 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.")
|
|
||||||
}
|
|
||||||
return successCount
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PluginManagerEvents {
|
interface PluginManagerEvents {
|
||||||
'pluginLoad': (name: string, plugin: any) => void,
|
pluginLoad: (name: string, plugin: any) => void;
|
||||||
'pluginsFinishLoading': (manager: PluginManager) => void,
|
pluginsFinishLoading: (manager: PluginManager) => void;
|
||||||
'proxyFinishLoading': (proxy: Proxy, manager: PluginManager) => void
|
proxyFinishLoading: (proxy: Proxy, manager: PluginManager) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export declare interface PluginManager {
|
export declare interface PluginManager {
|
||||||
on<U extends keyof PluginManagerEvents>(
|
on<U extends keyof PluginManagerEvents>(
|
||||||
event: U, listener: PluginManagerEvents[U]
|
event: U,
|
||||||
): this;
|
listener: PluginManagerEvents[U]
|
||||||
|
): this;
|
||||||
emit<U extends keyof PluginManagerEvents>(
|
|
||||||
event: U, ...args: Parameters<PluginManagerEvents[U]>
|
|
||||||
): boolean;
|
|
||||||
|
|
||||||
once<U extends keyof PluginManagerEvents>(
|
emit<U extends keyof PluginManagerEvents>(
|
||||||
event: U, listener: PluginManagerEvents[U]
|
event: U,
|
||||||
): this;
|
...args: Parameters<PluginManagerEvents[U]>
|
||||||
}
|
): boolean;
|
||||||
|
|
||||||
|
once<U extends keyof PluginManagerEvents>(
|
||||||
|
event: U,
|
||||||
|
listener: PluginManagerEvents[U]
|
||||||
|
): this;
|
||||||
|
}
|
||||||
|
|
|
@ -1,65 +1,75 @@
|
||||||
export default class SimpleRatelimit<T> {
|
export default class SimpleRatelimit<T> {
|
||||||
readonly requestCount: number
|
readonly requestCount: number;
|
||||||
readonly resetInterval: number
|
readonly resetInterval: number;
|
||||||
private entries: Map<T, Ratelimit>
|
private entries: Map<T, Ratelimit>;
|
||||||
|
|
||||||
constructor(requestCount: number, resetInterval: number) {
|
constructor(requestCount: number, resetInterval: number) {
|
||||||
this.requestCount = requestCount
|
this.requestCount = requestCount;
|
||||||
this.resetInterval = resetInterval
|
this.resetInterval = resetInterval;
|
||||||
this.entries = new Map()
|
this.entries = new Map();
|
||||||
}
|
}
|
||||||
|
|
||||||
public get(key: T): Ratelimit {
|
public get(key: T): Ratelimit {
|
||||||
return this.entries.get(key) ?? {
|
return (
|
||||||
remainingRequests: this.requestCount,
|
this.entries.get(key) ?? {
|
||||||
resetTime: new Date(0)
|
remainingRequests: this.requestCount,
|
||||||
}
|
resetTime: new Date(0),
|
||||||
}
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public consume(key: T, count?: number): Ratelimit | never {
|
public consume(key: T, count?: number): Ratelimit | never {
|
||||||
if (this.entries.has(key)) {
|
if (this.entries.has(key)) {
|
||||||
const ratelimit = this.entries.get(key)
|
const ratelimit = this.entries.get(key);
|
||||||
if (ratelimit.remainingRequests - (count ?? 1) < 0) {
|
if (ratelimit.remainingRequests - (count ?? 1) < 0) {
|
||||||
if (this.requestCount - (count ?? 1) < 0) {
|
if (this.requestCount - (count ?? 1) < 0) {
|
||||||
throw new RatelimitExceededError(`Consume request count is higher than default available request count!`)
|
throw new RatelimitExceededError(
|
||||||
} else {
|
`Consume request count is higher than default available request count!`
|
||||||
throw new RatelimitExceededError(`Ratelimit exceeded, try again in ${ratelimit.resetTime.getDate() - Date.now()} ms!`)
|
);
|
||||||
}
|
|
||||||
}
|
|
||||||
ratelimit.remainingRequests -= count ?? 1
|
|
||||||
return ratelimit
|
|
||||||
} else {
|
} else {
|
||||||
if (this.requestCount - (count ?? 1) < 0) {
|
throw new RatelimitExceededError(
|
||||||
throw new RatelimitExceededError(`Consume request count is higher than default available request count!`)
|
`Ratelimit exceeded, try again in ${
|
||||||
}
|
ratelimit.resetTime.getDate() - Date.now()
|
||||||
const ratelimit: Ratelimit = {
|
} ms!`
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
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!`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private _onAdd(ratelimit: Ratelimit): NodeJS.Timer {
|
private _onAdd(ratelimit: Ratelimit): NodeJS.Timer {
|
||||||
return setInterval(() => {
|
return setInterval(() => {
|
||||||
// TODO: work on
|
// TODO: work on
|
||||||
}, this.resetInterval)
|
}, this.resetInterval);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Ratelimit = {
|
export type Ratelimit = {
|
||||||
remainingRequests: number,
|
remainingRequests: number;
|
||||||
resetTime: Date,
|
resetTime: Date;
|
||||||
timer?: NodeJS.Timer
|
timer?: NodeJS.Timer;
|
||||||
}
|
};
|
||||||
|
|
||||||
export class RatelimitExceededError extends Error {
|
export class RatelimitExceededError extends Error {
|
||||||
constructor(message: { toString: () => string }) {
|
constructor(message: { toString: () => string }) {
|
||||||
super(message.toString())
|
super(message.toString());
|
||||||
this.name = "RatelimitExceededError"
|
this.name = "RatelimitExceededError";
|
||||||
Object.setPrototypeOf(this, RatelimitExceededError.prototype)
|
Object.setPrototypeOf(this, RatelimitExceededError.prototype);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,11 +8,6 @@
|
||||||
"outDir": "build",
|
"outDir": "build",
|
||||||
"resolveJsonModule": true
|
"resolveJsonModule": true
|
||||||
},
|
},
|
||||||
"include": [
|
"include": ["src/**/*.json", "src/**/*"],
|
||||||
"src/**/*.json",
|
"hooks": ["copy-files"]
|
||||||
"src/**/*"
|
}
|
||||||
],
|
|
||||||
"hooks": [
|
|
||||||
"copy-files"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user