mirror of
https://github.com/WorldEditAxe/eaglerproxy.git
synced 2024-11-21 04:56:04 -08:00
Added option to swap between native and pure JS image manipulation
This commit is contained in:
parent
d6fa0c082d
commit
36adb113c0
|
@ -13,22 +13,36 @@ export const config: Config = {
|
||||||
bindHost: "0.0.0.0",
|
bindHost: "0.0.0.0",
|
||||||
bindPort: 8080,
|
bindPort: 8080,
|
||||||
maxConcurrentClients: 20,
|
maxConcurrentClients: 20,
|
||||||
|
useNatives: false,
|
||||||
|
skinServer: {
|
||||||
skinUrlWhitelist: undefined,
|
skinUrlWhitelist: undefined,
|
||||||
|
},
|
||||||
motd: true
|
motd: true
|
||||||
? "FORWARD"
|
? "FORWARD"
|
||||||
: {
|
: {
|
||||||
iconURL: "logo.png",
|
iconURL: "motd.png",
|
||||||
l1: "yes",
|
l1: "yes",
|
||||||
l2: "no",
|
l2: "no",
|
||||||
},
|
},
|
||||||
|
ratelimits: {
|
||||||
|
lockout: 10,
|
||||||
|
limits: {
|
||||||
|
http: 100,
|
||||||
|
ws: 100,
|
||||||
|
motd: 100,
|
||||||
|
skins: 1000,
|
||||||
|
skinsIp: 10000,
|
||||||
|
connect: 100,
|
||||||
|
},
|
||||||
|
},
|
||||||
origins: {
|
origins: {
|
||||||
allowOfflineDownloads: true,
|
allowOfflineDownloads: true,
|
||||||
originWhitelist: null,
|
originWhitelist: null,
|
||||||
originBlacklist: null,
|
originBlacklist: null,
|
||||||
},
|
},
|
||||||
server: {
|
server: {
|
||||||
host: "no",
|
host: "0.0.0.0",
|
||||||
port: 46625,
|
port: 25565,
|
||||||
},
|
},
|
||||||
tls: undefined,
|
tls: undefined,
|
||||||
},
|
},
|
||||||
|
|
|
@ -8,16 +8,19 @@ 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";
|
||||||
|
import { ImageEditor } from "./proxy/skins/ImageEditor.js";
|
||||||
|
|
||||||
const logger = new Logger("Launcher");
|
const logger = new Logger("Launcher");
|
||||||
let proxy: Proxy;
|
let proxy: Proxy;
|
||||||
|
|
||||||
global.CONFIG = config;
|
global.CONFIG = config;
|
||||||
|
config.adapter.useNatives = config.adapter.useNatives ?? true;
|
||||||
|
|
||||||
|
logger.info("Loading libraries...");
|
||||||
|
await ImageEditor.loadLibraries(config.adapter.useNatives);
|
||||||
|
|
||||||
logger.info("Loading plugins...");
|
logger.info("Loading plugins...");
|
||||||
const pluginManager = new PluginManager(
|
const pluginManager = new PluginManager(join(dirname(fileURLToPath(import.meta.url)), "plugins"));
|
||||||
join(dirname(fileURLToPath(import.meta.url)), "plugins")
|
|
||||||
);
|
|
||||||
global.PLUGIN_MANAGER = pluginManager;
|
global.PLUGIN_MANAGER = pluginManager;
|
||||||
await pluginManager.loadPlugins();
|
await pluginManager.loadPlugins();
|
||||||
|
|
||||||
|
|
|
@ -19,7 +19,10 @@ export type AdapterOptions = {
|
||||||
bindHost: string;
|
bindHost: string;
|
||||||
bindPort: number;
|
bindPort: number;
|
||||||
maxConcurrentClients: 20;
|
maxConcurrentClients: 20;
|
||||||
|
useNatives?: boolean;
|
||||||
|
skinServer: {
|
||||||
skinUrlWhitelist?: string[];
|
skinUrlWhitelist?: string[];
|
||||||
|
};
|
||||||
origins: {
|
origins: {
|
||||||
allowOfflineDownloads: boolean;
|
allowOfflineDownloads: boolean;
|
||||||
originWhitelist: string[];
|
originWhitelist: string[];
|
||||||
|
@ -32,6 +35,17 @@ export type AdapterOptions = {
|
||||||
l1: string;
|
l1: string;
|
||||||
l2?: string;
|
l2?: string;
|
||||||
};
|
};
|
||||||
|
ratelimits: {
|
||||||
|
lockout: number;
|
||||||
|
limits: {
|
||||||
|
http: number;
|
||||||
|
ws: number;
|
||||||
|
motd: number;
|
||||||
|
connect: number;
|
||||||
|
skins: number;
|
||||||
|
skinsIp: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
server: {
|
server: {
|
||||||
host: string;
|
host: string;
|
||||||
port: number;
|
port: number;
|
||||||
|
|
|
@ -144,7 +144,7 @@ export class CustomAuthflow {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getMinecraftJavaToken(options: any = {}) {
|
async getMinecraftJavaToken(options: any = {}, quit: { quit: boolean }) {
|
||||||
const response: any = { token: "", entitlements: {} as any, profile: {} as any };
|
const response: any = { token: "", entitlements: {} as any, profile: {} as any };
|
||||||
if (await this.mca.verifyTokens()) {
|
if (await this.mca.verifyTokens()) {
|
||||||
const { token } = await this.mca.getCachedAccessToken();
|
const { token } = await this.mca.getCachedAccessToken();
|
||||||
|
@ -154,6 +154,7 @@ export class CustomAuthflow {
|
||||||
async () => {
|
async () => {
|
||||||
const xsts = await this.getXboxToken(Endpoints.PCXSTSRelyingParty);
|
const xsts = await this.getXboxToken(Endpoints.PCXSTSRelyingParty);
|
||||||
response.token = await this.mca.getAccessToken(xsts);
|
response.token = await this.mca.getAccessToken(xsts);
|
||||||
|
if (quit.quit) return;
|
||||||
},
|
},
|
||||||
() => {
|
() => {
|
||||||
this.xbl.forceRefresh = true;
|
this.xbl.forceRefresh = true;
|
||||||
|
@ -161,6 +162,7 @@ export class CustomAuthflow {
|
||||||
2
|
2
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (quit.quit) return;
|
||||||
|
|
||||||
if (options.fetchEntitlements) {
|
if (options.fetchEntitlements) {
|
||||||
response.entitlements = await this.mca.fetchEntitlements(response.token).catch((e) => {});
|
response.entitlements = await this.mca.fetchEntitlements(response.token).catch((e) => {});
|
||||||
|
|
|
@ -31,7 +31,7 @@ class InMemoryCache {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function auth(): EventEmitter {
|
export function auth(quit: { quit: boolean }): EventEmitter {
|
||||||
const emitter = new EventEmitter();
|
const emitter = new EventEmitter();
|
||||||
const userIdentifier = randomUUID();
|
const userIdentifier = randomUUID();
|
||||||
const flow = new CustomAuthflow(
|
const flow = new CustomAuthflow(
|
||||||
|
@ -48,17 +48,12 @@ export function auth(): EventEmitter {
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
flow
|
flow
|
||||||
.getMinecraftJavaToken({ fetchProfile: true })
|
.getMinecraftJavaToken({ fetchProfile: true }, quit)
|
||||||
.then(async (data) => {
|
.then(async (data) => {
|
||||||
|
if (!data || quit.quit) return;
|
||||||
|
|
||||||
const _data = (await (flow as any).mca.cache.getCached()).mca;
|
const _data = (await (flow as any).mca.cache.getCached()).mca;
|
||||||
if (data.profile == null || (data.profile as any).error)
|
if (data.profile == null || (data.profile as any).error) return emitter.emit("error", new Error(Enums.ChatColor.RED + "Couldn't fetch profile data, does the account own Minecraft: Java Edition?"));
|
||||||
return emitter.emit(
|
|
||||||
"error",
|
|
||||||
new Error(
|
|
||||||
Enums.ChatColor.RED +
|
|
||||||
"Couldn't fetch profile data, does the account own Minecraft: Java Edition?"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
emitter.emit("done", {
|
emitter.emit("done", {
|
||||||
accessToken: data.token,
|
accessToken: data.token,
|
||||||
expiresOn: _data.obtainedOn + _data.expires_in * 1000,
|
expiresOn: _data.obtainedOn + _data.expires_in * 1000,
|
||||||
|
@ -67,19 +62,8 @@ export function auth(): EventEmitter {
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
if (err.toString().includes("Not Found"))
|
if (err.toString().includes("Not Found")) emitter.emit("error", new Error(Enums.ChatColor.RED + "The provided account doesn't own Minecraft: Java Edition!"));
|
||||||
emitter.emit(
|
else emitter.emit("error", new Error(Enums.ChatColor.YELLOW + err.toString()));
|
||||||
"error",
|
|
||||||
new Error(
|
|
||||||
Enums.ChatColor.RED +
|
|
||||||
"The provided account doesn't own Minecraft: Java Edition!"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
else
|
|
||||||
emitter.emit(
|
|
||||||
"error",
|
|
||||||
new Error(Enums.ChatColor.YELLOW + err.toString())
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
return emitter;
|
return emitter;
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,25 +9,12 @@ export async function getTokenProfileEasyMc(token: string): Promise<object> {
|
||||||
token,
|
token,
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
const res = await fetch(
|
const res = await fetch("https://api.easymc.io/v1/token/redeem", fetchOptions);
|
||||||
"https://api.easymc.io/v1/token/redeem",
|
|
||||||
fetchOptions
|
|
||||||
);
|
|
||||||
const resJson = await res.json();
|
const resJson = await res.json();
|
||||||
|
|
||||||
if (resJson.error) throw new Error(Enums.ChatColor.RED + `${resJson.error}`);
|
if (resJson.error) throw new Error(Enums.ChatColor.RED + `${resJson.error}`);
|
||||||
if (!resJson)
|
if (!resJson) throw new Error(Enums.ChatColor.RED + "EasyMC replied with an empty response!?");
|
||||||
throw new Error(
|
if (resJson.session?.length !== 43 || resJson.mcName?.length < 3 || resJson.uuid?.length !== 36) throw new Error(Enums.ChatColor.RED + "Invalid response from EasyMC received!");
|
||||||
Enums.ChatColor.RED + "EasyMC replied with an empty response!?"
|
|
||||||
);
|
|
||||||
if (
|
|
||||||
resJson.session?.length !== 43 ||
|
|
||||||
resJson.mcName?.length < 3 ||
|
|
||||||
resJson.uuid?.length !== 36
|
|
||||||
)
|
|
||||||
throw new Error(
|
|
||||||
Enums.ChatColor.RED + "Invalid response from EasyMC received!"
|
|
||||||
);
|
|
||||||
return {
|
return {
|
||||||
auth: "mojang",
|
auth: "mojang",
|
||||||
sessionServer: "https://sessionserver.easymc.io",
|
sessionServer: "https://sessionserver.easymc.io",
|
||||||
|
|
|
@ -2,9 +2,11 @@ export const config = {
|
||||||
bindInternalServerPort: 25569,
|
bindInternalServerPort: 25569,
|
||||||
bindInternalServerIp: "127.0.0.1",
|
bindInternalServerIp: "127.0.0.1",
|
||||||
allowCustomPorts: true,
|
allowCustomPorts: true,
|
||||||
|
allowDirectConnectEndpoints: false,
|
||||||
disallowHypixel: false,
|
disallowHypixel: false,
|
||||||
|
showDisclaimers: false,
|
||||||
authentication: {
|
authentication: {
|
||||||
enabled: true,
|
enabled: false,
|
||||||
password: "nope",
|
password: "nope",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -222,6 +222,8 @@ CONFIG.adapter.motd = {
|
||||||
l1: Enums.ChatColor.GOLD + "EaglerProxy as a Service",
|
l1: Enums.ChatColor.GOLD + "EaglerProxy as a Service",
|
||||||
};
|
};
|
||||||
|
|
||||||
PLUGIN_MANAGER.addListener("proxyFinishLoading", () => {
|
if (config.allowDirectConnectEndpoints) {
|
||||||
|
PLUGIN_MANAGER.addListener("proxyFinishLoading", () => {
|
||||||
registerEndpoints();
|
registerEndpoints();
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { ServerDeviceCodeResponse, auth } from "../auth.js";
|
||||||
import { config } from "../config.js";
|
import { config } from "../config.js";
|
||||||
|
|
||||||
export async function registerEndpoints() {
|
export async function registerEndpoints() {
|
||||||
|
@ -30,4 +31,77 @@ export async function registerEndpoints() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
proxy.on("wsConnection", (ws, req, ctx) => {
|
||||||
|
try {
|
||||||
|
if (req.url.startsWith("/eagpaas/token")) {
|
||||||
|
ctx.handled = true;
|
||||||
|
if (config.authentication.enabled) {
|
||||||
|
if (req.headers.authorization !== `Basic ${config.authentication.password}`) {
|
||||||
|
ws.send(
|
||||||
|
JSON.stringify({
|
||||||
|
type: "ERROR",
|
||||||
|
error: "Access Denied",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
ws.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const quit = { quit: false },
|
||||||
|
authHandler = auth(quit),
|
||||||
|
codeCallback = (code: ServerDeviceCodeResponse) => {
|
||||||
|
ws.send(
|
||||||
|
JSON.stringify({
|
||||||
|
type: "CODE",
|
||||||
|
data: code,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
ws.once("close", () => {
|
||||||
|
quit.quit = true;
|
||||||
|
});
|
||||||
|
authHandler
|
||||||
|
.on("code", codeCallback)
|
||||||
|
.on("error", (err) => {
|
||||||
|
ws.send(
|
||||||
|
JSON.stringify({
|
||||||
|
type: "ERROR",
|
||||||
|
reason: err,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
ws.close();
|
||||||
|
})
|
||||||
|
.on("done", (result) => {
|
||||||
|
ws.send(
|
||||||
|
JSON.stringify({
|
||||||
|
type: "COMPLETE",
|
||||||
|
data: result,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
ws.close();
|
||||||
|
});
|
||||||
|
} else if (req.url.startsWith("/eagpaas/ping")) {
|
||||||
|
ctx.handled = true;
|
||||||
|
if (config.authentication.enabled) {
|
||||||
|
if (req.headers.authorization !== `Basic ${config.authentication.password}`) {
|
||||||
|
ws.send(
|
||||||
|
JSON.stringify({
|
||||||
|
type: "ERROR",
|
||||||
|
error: "Access Denied",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
ws.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.once("message", (_) => {
|
||||||
|
ws.send(_);
|
||||||
|
ws.close();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (err) {}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -241,6 +241,15 @@ export async function onConnect(client: ClientState) {
|
||||||
client.state = ConnectionState.AUTH;
|
client.state = ConnectionState.AUTH;
|
||||||
client.lastStatusUpdate = Date.now();
|
client.lastStatusUpdate = Date.now();
|
||||||
|
|
||||||
|
client.gameClient.on("packet", (packet, meta) => {
|
||||||
|
if (meta.name == "client_command" && packet.payload == 1) {
|
||||||
|
client.gameClient.write("statistics", {
|
||||||
|
entries: [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (config.showDisclaimers) {
|
||||||
sendMessageWarning(client.gameClient, `WARNING: This proxy allows you to connect to any 1.8.9 server. Gameplay has shown no major issues, but please note that EaglercraftX may flag some anticheats while playing.`);
|
sendMessageWarning(client.gameClient, `WARNING: This proxy allows you to connect to any 1.8.9 server. Gameplay has shown no major issues, but please note that EaglercraftX may flag some anticheats while playing.`);
|
||||||
await new Promise((res) => setTimeout(res, 2000));
|
await new Promise((res) => setTimeout(res, 2000));
|
||||||
|
|
||||||
|
@ -252,6 +261,7 @@ export async function onConnect(client: ClientState) {
|
||||||
|
|
||||||
sendMessageWarning(client.gameClient, `WARNING: It is highly suggested that you turn down settings, as gameplay tends to be very laggy and unplayable on low powered devices.`);
|
sendMessageWarning(client.gameClient, `WARNING: It is highly suggested that you turn down settings, as gameplay tends to be very laggy and unplayable on low powered devices.`);
|
||||||
await new Promise((res) => setTimeout(res, 2000));
|
await new Promise((res) => setTimeout(res, 2000));
|
||||||
|
}
|
||||||
|
|
||||||
if (config.authentication.enabled) {
|
if (config.authentication.enabled) {
|
||||||
sendCustomMessage(client.gameClient, "This instance is password-protected. Sign in with /password <password>", "gold");
|
sendCustomMessage(client.gameClient, "This instance is password-protected. Sign in with /password <password>", "gold");
|
||||||
|
@ -346,20 +356,27 @@ export async function onConnect(client: ClientState) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (chosenOption == ConnectType.ONLINE) {
|
if (chosenOption == ConnectType.ONLINE) {
|
||||||
|
if (config.showDisclaimers) {
|
||||||
sendMessageWarning(
|
sendMessageWarning(
|
||||||
client.gameClient,
|
client.gameClient,
|
||||||
`WARNING: You will be prompted to log in via Microsoft to obtain a session token necessary to join games. Any data related to your account will not be saved and for transparency reasons this proxy's source code is available on Github.`
|
`WARNING: You will be prompted to log in via Microsoft to obtain a session token necessary to join games. Any data related to your account will not be saved and for transparency reasons this proxy's source code is available on Github.`
|
||||||
);
|
);
|
||||||
|
}
|
||||||
await new Promise((res) => setTimeout(res, 2000));
|
await new Promise((res) => setTimeout(res, 2000));
|
||||||
|
|
||||||
client.lastStatusUpdate = Date.now();
|
client.lastStatusUpdate = Date.now();
|
||||||
let errored = false,
|
let errored = false,
|
||||||
savedAuth;
|
savedAuth;
|
||||||
const authHandler = auth(),
|
const quit = { quit: false },
|
||||||
|
authHandler = auth(quit),
|
||||||
codeCallback = (code: ServerDeviceCodeResponse) => {
|
codeCallback = (code: ServerDeviceCodeResponse) => {
|
||||||
updateState(client.gameClient, "AUTH", code.verification_uri, code.user_code);
|
updateState(client.gameClient, "AUTH", code.verification_uri, code.user_code);
|
||||||
sendMessageLogin(client.gameClient, code.verification_uri, code.user_code);
|
sendMessageLogin(client.gameClient, code.verification_uri, code.user_code);
|
||||||
};
|
};
|
||||||
|
client.gameClient.once("end", (res) => {
|
||||||
|
quit.quit = true;
|
||||||
|
});
|
||||||
|
|
||||||
authHandler.once("error", (err) => {
|
authHandler.once("error", (err) => {
|
||||||
if (!client.gameClient.ended) client.gameClient.end(err.message);
|
if (!client.gameClient.ended) client.gameClient.end(err.message);
|
||||||
errored = true;
|
errored = true;
|
||||||
|
|
|
@ -2,16 +2,16 @@ 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[] = [
|
export const MAGIC_ENDING_SERVER_SKIN_DOWNLOAD_BUILTIN: number[] = [0x00, 0x00, 0x00];
|
||||||
0x00, 0x00, 0x00,
|
export const MAGIC_ENDING_CLIENT_UPLOAD_SKIN_BUILTIN: number[] = [0x00, 0x05, 0x01, 0x00, 0x00, 0x00];
|
||||||
];
|
|
||||||
export const MAGIC_ENDING_CLIENT_UPLOAD_SKIN_BUILTIN: number[] = [
|
|
||||||
0x00, 0x05, 0x01, 0x00, 0x00, 0x00,
|
|
||||||
];
|
|
||||||
export const EAGLERCRAFT_SKIN_CUSTOM_LENGTH = 64 ** 2 * 4;
|
export const EAGLERCRAFT_SKIN_CUSTOM_LENGTH = 64 ** 2 * 4;
|
||||||
|
|
||||||
export const JOIN_SERVER_PACKET = 0x01;
|
export const JOIN_SERVER_PACKET = 0x01;
|
||||||
export const PLAYER_LOOK_PACKET = 0x08;
|
export const PLAYER_LOOK_PACKET = 0x08;
|
||||||
|
|
||||||
|
export const ICON_SQRT = 64;
|
||||||
|
export const END_BUFFER_LENGTH = ICON_SQRT ** 8;
|
||||||
|
export const IMAGE_DATA_PREPEND = "data:image/png;base64,";
|
||||||
}
|
}
|
||||||
|
|
||||||
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,44 +1,36 @@
|
||||||
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 { 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";
|
||||||
|
import { Constants } from "./Constants.js";
|
||||||
|
import { ImageEditor } from "./skins/ImageEditor.js";
|
||||||
const { ping } = pkg;
|
const { ping } = pkg;
|
||||||
|
|
||||||
export namespace Motd {
|
export namespace Motd {
|
||||||
const ICON_SQRT = 64;
|
|
||||||
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;
|
||||||
|
public usingNatives: boolean;
|
||||||
|
|
||||||
constructor(motd: JSONMotd, image?: Buffer) {
|
constructor(motd: JSONMotd, native: boolean, image?: Buffer) {
|
||||||
this.jsonMotd = motd;
|
this.jsonMotd = motd;
|
||||||
this.image = image;
|
this.image = image;
|
||||||
|
this.usingNatives = native;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async generateMOTDFromPing(
|
public static async generateMOTDFromPing(host: string, port: number, useNatives: boolean): Promise<MOTD> {
|
||||||
host: string,
|
|
||||||
port: number
|
|
||||||
): Promise<MOTD> {
|
|
||||||
const pingRes = await ping({ host: host, port: port });
|
const pingRes = await ping({ host: host, port: port });
|
||||||
if (typeof pingRes.version == "string")
|
if (typeof pingRes.version == "string") throw new Error("Non-1.8 server detected!");
|
||||||
throw new Error("Non-1.8 server detected!");
|
|
||||||
else {
|
else {
|
||||||
const newPingRes = pingRes as NewPingResult;
|
const newPingRes = pingRes as NewPingResult;
|
||||||
let image: Buffer;
|
let image: Buffer;
|
||||||
|
|
||||||
if (newPingRes.favicon != null) {
|
if (newPingRes.favicon != null) {
|
||||||
if (!newPingRes.favicon.startsWith(IMAGE_DATA_PREPEND))
|
if (!newPingRes.favicon.startsWith(Constants.IMAGE_DATA_PREPEND)) throw new Error("Invalid MOTD image!");
|
||||||
throw new Error("Invalid MOTD image!");
|
image = useNatives
|
||||||
image = await this.generateEaglerMOTDImage(
|
? await ImageEditor.generateEaglerMOTDImage(Buffer.from(newPingRes.favicon.substring(Constants.IMAGE_DATA_PREPEND.length), "base64"))
|
||||||
Buffer.from(
|
: await ImageEditor.generateEaglerMOTDImageJS(Buffer.from(newPingRes.favicon.substring(Constants.IMAGE_DATA_PREPEND.length), "base64"));
|
||||||
newPingRes.favicon.substring(IMAGE_DATA_PREPEND.length),
|
|
||||||
"base64"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return new MOTD(
|
return new MOTD(
|
||||||
|
@ -49,17 +41,9 @@ export namespace Motd {
|
||||||
cache: true,
|
cache: true,
|
||||||
icon: newPingRes.favicon != null ? true : false,
|
icon: newPingRes.favicon != null ? true : false,
|
||||||
max: newPingRes.players.max,
|
max: newPingRes.players.max,
|
||||||
motd: [
|
motd: [typeof newPingRes.description == "string" ? newPingRes.description : Chat.chatToPlainString(newPingRes.description), ""],
|
||||||
typeof newPingRes.description == "string"
|
|
||||||
? newPingRes.description
|
|
||||||
: Chat.chatToPlainString(newPingRes.description),
|
|
||||||
"",
|
|
||||||
],
|
|
||||||
online: newPingRes.players.online,
|
online: newPingRes.players.online,
|
||||||
players:
|
players: newPingRes.players.sample != null ? newPingRes.players.sample.map((v) => v.name) : [],
|
||||||
newPingRes.players.sample != null
|
|
||||||
? newPingRes.players.sample.map((v) => v.name)
|
|
||||||
: [],
|
|
||||||
},
|
},
|
||||||
name: "placeholder name",
|
name: "placeholder name",
|
||||||
secure: false,
|
secure: false,
|
||||||
|
@ -68,16 +52,16 @@ export namespace Motd {
|
||||||
uuid: randomUUID(), // replace placeholder with global. cached UUID
|
uuid: randomUUID(), // replace placeholder with global. cached UUID
|
||||||
vers: `${PROXY_BRANDING}/${PROXY_VERSION}`,
|
vers: `${PROXY_BRANDING}/${PROXY_VERSION}`,
|
||||||
},
|
},
|
||||||
|
useNatives,
|
||||||
image
|
image
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async generateMOTDFromConfig(
|
public static async generateMOTDFromConfig(config: Config["adapter"], useNatives: boolean): Promise<MOTD> {
|
||||||
config: Config["adapter"]
|
|
||||||
): Promise<MOTD> {
|
|
||||||
if (typeof config.motd != "string") {
|
if (typeof config.motd != "string") {
|
||||||
const motd = new MOTD({
|
const motd = new MOTD(
|
||||||
|
{
|
||||||
brand: PROXY_BRANDING,
|
brand: PROXY_BRANDING,
|
||||||
cracked: true,
|
cracked: true,
|
||||||
data: {
|
data: {
|
||||||
|
@ -94,39 +78,16 @@ export namespace Motd {
|
||||||
type: "motd",
|
type: "motd",
|
||||||
uuid: randomUUID(),
|
uuid: randomUUID(),
|
||||||
vers: `${PROXY_BRANDING}/${PROXY_VERSION}`,
|
vers: `${PROXY_BRANDING}/${PROXY_VERSION}`,
|
||||||
});
|
},
|
||||||
|
useNatives
|
||||||
|
);
|
||||||
if (config.motd.iconURL != null) {
|
if (config.motd.iconURL != null) {
|
||||||
motd.image = await this.generateEaglerMOTDImage(config.motd.iconURL);
|
motd.image = useNatives ? await ImageEditor.generateEaglerMOTDImage(config.motd.iconURL) : await ImageEditor.generateEaglerMOTDImageJS(config.motd.iconURL); // TODO: swap between native and pure JS
|
||||||
}
|
}
|
||||||
return motd;
|
return motd;
|
||||||
} else throw new Error("MOTD is set to be forwarded in the config!");
|
} else throw new Error("MOTD is set to be forwarded in the config!");
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: fix not working
|
|
||||||
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] {
|
public toBuffer(): [string, Buffer] {
|
||||||
return [JSON.stringify(this.jsonMotd), this.image];
|
return [JSON.stringify(this.jsonMotd), this.image];
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,11 +11,12 @@ import { EaglerSkins } from "./skins/EaglerSkins.js";
|
||||||
import { Util } from "./Util.js";
|
import { Util } from "./Util.js";
|
||||||
import { BungeeUtil } from "./BungeeUtil.js";
|
import { BungeeUtil } from "./BungeeUtil.js";
|
||||||
import { IncomingMessage } from "http";
|
import { IncomingMessage } from "http";
|
||||||
|
import { Socket } from "net";
|
||||||
|
|
||||||
const { createSerializer, createDeserializer } = pkg;
|
const { createSerializer, createDeserializer } = pkg;
|
||||||
|
|
||||||
export class Player extends EventEmitter {
|
export class Player extends EventEmitter {
|
||||||
public ws: WebSocket & { httpRequest: IncomingMessage };
|
public ws: WebSocket & { httpRequest: IncomingMessage; _socket: Socket };
|
||||||
public username?: string;
|
public username?: string;
|
||||||
public skin?: EaglerSkins.EaglerSkin;
|
public skin?: EaglerSkins.EaglerSkin;
|
||||||
public uuid?: string;
|
public uuid?: string;
|
||||||
|
@ -36,7 +37,7 @@ export class Player extends EventEmitter {
|
||||||
constructor(ws: WebSocket & { httpRequest: IncomingMessage }, playerName?: string, serverConnection?: Client) {
|
constructor(ws: WebSocket & { httpRequest: IncomingMessage }, playerName?: string, serverConnection?: Client) {
|
||||||
super();
|
super();
|
||||||
this._logger = new Logger(`PlayerHandler-${playerName}`);
|
this._logger = new Logger(`PlayerHandler-${playerName}`);
|
||||||
this.ws = ws;
|
this.ws = ws as any;
|
||||||
this.username = playerName;
|
this.username = playerName;
|
||||||
this.serverConnection = serverConnection;
|
this.serverConnection = serverConnection;
|
||||||
if (this.username != null) this.uuid = Util.generateUUIDFromPlayer(this.username);
|
if (this.username != null) this.uuid = Util.generateUUIDFromPlayer(this.username);
|
||||||
|
|
|
@ -25,6 +25,7 @@ import { CSSetSkinPacket } from "./packets/CSSetSkinPacket.js";
|
||||||
import { CSChannelMessagePacket } from "./packets/channel/CSChannelMessage.js";
|
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";
|
||||||
|
import ProxyRatelimitManager from "./ratelimit/ProxyRatelimitManager.js";
|
||||||
|
|
||||||
let instanceCount = 0;
|
let instanceCount = 0;
|
||||||
const chalk = new Chalk({ level: 2 });
|
const chalk = new Chalk({ level: 2 });
|
||||||
|
@ -43,6 +44,7 @@ export class Proxy extends EventEmitter {
|
||||||
public httpServer: http.Server;
|
public httpServer: http.Server;
|
||||||
public skinServer: EaglerSkins.SkinServer;
|
public skinServer: EaglerSkins.SkinServer;
|
||||||
public broadcastMotd?: Motd.MOTD;
|
public broadcastMotd?: Motd.MOTD;
|
||||||
|
public ratelimit: ProxyRatelimitManager;
|
||||||
|
|
||||||
private _logger: Logger;
|
private _logger: Logger;
|
||||||
private initalHandlerLogger: Logger;
|
private initalHandlerLogger: Logger;
|
||||||
|
@ -93,13 +95,12 @@ export class Proxy extends EventEmitter {
|
||||||
if (this.loaded) throw new Error("Can't initiate if proxy instance is already initialized or is being initialized!");
|
if (this.loaded) throw new Error("Can't initiate if proxy instance is already initialized or is being initialized!");
|
||||||
this.loaded = true;
|
this.loaded = true;
|
||||||
this.packetRegistry = await loadPackets();
|
this.packetRegistry = await loadPackets();
|
||||||
this.skinServer = new EaglerSkins.SkinServer(this, this.config.skinUrlWhitelist);
|
this.skinServer = new EaglerSkins.SkinServer(this, this.config.useNatives, this.config.skinServer.skinUrlWhitelist);
|
||||||
global.PACKET_REGISTRY = this.packetRegistry;
|
global.PACKET_REGISTRY = this.packetRegistry;
|
||||||
if (this.config.motd == "FORWARD") {
|
if (this.config.motd == "FORWARD") {
|
||||||
this._pollServer(this.config.server.host, this.config.server.port);
|
this._pollServer(this.config.server.host, this.config.server.port);
|
||||||
} else {
|
} else {
|
||||||
// TODO: motd
|
const broadcastMOTD = await Motd.MOTD.generateMOTDFromConfig(this.config, this.config.useNatives);
|
||||||
const broadcastMOTD = await Motd.MOTD.generateMOTDFromConfig(this.config);
|
|
||||||
(broadcastMOTD as any)._static = true;
|
(broadcastMOTD as any)._static = true;
|
||||||
this.broadcastMotd = broadcastMOTD;
|
this.broadcastMotd = broadcastMOTD;
|
||||||
// playercount will be dynamically updated
|
// playercount will be dynamically updated
|
||||||
|
@ -136,19 +137,31 @@ export class Proxy extends EventEmitter {
|
||||||
this._logger.error(`Error was caught whilst trying to handle WebSocket upgrade! Error: ${err.stack ?? err}`);
|
this._logger.error(`Error was caught whilst trying to handle WebSocket upgrade! Error: ${err.stack ?? err}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
this.ratelimit = new ProxyRatelimitManager(this.config.ratelimits);
|
||||||
this.pluginManager.emit("proxyFinishLoading", this, this.pluginManager);
|
this.pluginManager.emit("proxyFinishLoading", this, this.pluginManager);
|
||||||
this._logger.info(`Started WebSocket server and binded to ${this.config.bindHost} on port ${this.config.bindPort}.`);
|
this._logger.info(`Started WebSocket server and binded to ${this.config.bindHost} on port ${this.config.bindPort}.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _handleNonWSRequest(req: http.IncomingMessage, res: http.ServerResponse, config: Config["adapter"]) {
|
private _handleNonWSRequest(req: http.IncomingMessage, res: http.ServerResponse, config: Config["adapter"]) {
|
||||||
|
if (this.ratelimit.http.consume(req.socket.remoteAddress).success) {
|
||||||
const ctx: Util.Handlable = { handled: false };
|
const ctx: Util.Handlable = { handled: false };
|
||||||
this.emit("httpConnection", req, res, ctx);
|
this.emit("httpConnection", req, res, ctx);
|
||||||
if (!ctx.handled) res.setHeader("Content-Type", "text/html").writeHead(426).end(UPGRADE_REQUIRED_RESPONSE);
|
if (!ctx.handled) 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, req: http.IncomingMessage) {
|
private async _handleWSConnection(ws: WebSocket, req: http.IncomingMessage) {
|
||||||
|
const rl = this.ratelimit.ws.consume(req.socket.remoteAddress);
|
||||||
|
if (!rl.success) {
|
||||||
|
return ws.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
const ctx: Util.Handlable = { handled: false };
|
||||||
|
await this.emit("wsConnection", ws, req, ctx);
|
||||||
|
if (ctx.handled) return;
|
||||||
|
|
||||||
const firstPacket = await Util.awaitPacket(ws);
|
const firstPacket = await Util.awaitPacket(ws);
|
||||||
let player: Player, handled: boolean;
|
let player: Player, handled: boolean;
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
@ -163,11 +176,10 @@ export class Proxy extends EventEmitter {
|
||||||
}
|
}
|
||||||
}, this.LOGIN_TIMEOUT);
|
}, this.LOGIN_TIMEOUT);
|
||||||
try {
|
try {
|
||||||
const ctx: Util.Handlable = { handled: false };
|
|
||||||
await this.emit("wsConnection", ws, req, ctx);
|
|
||||||
if (ctx.handled) return;
|
|
||||||
|
|
||||||
if (firstPacket.toString() === "Accept: MOTD") {
|
if (firstPacket.toString() === "Accept: MOTD") {
|
||||||
|
if (!this.ratelimit.motd.consume(req.socket.remoteAddress).success) {
|
||||||
|
return ws.close();
|
||||||
|
}
|
||||||
if (this.broadcastMotd) {
|
if (this.broadcastMotd) {
|
||||||
if ((this.broadcastMotd as any)._static) {
|
if ((this.broadcastMotd as any)._static) {
|
||||||
this.broadcastMotd.jsonMotd.data.online = this.players.size;
|
this.broadcastMotd.jsonMotd.data.online = this.players.size;
|
||||||
|
@ -191,6 +203,13 @@ export class Proxy extends EventEmitter {
|
||||||
} else {
|
} else {
|
||||||
(ws as any).httpRequest = req;
|
(ws as any).httpRequest = req;
|
||||||
player = new Player(ws as any);
|
player = new Player(ws as any);
|
||||||
|
const rl = this.ratelimit.connect.consume(req.socket.remoteAddress);
|
||||||
|
if (!rl.success) {
|
||||||
|
handled = true;
|
||||||
|
player.disconnect(`${Enums.ChatColor.RED}You have been ratelimited!\nTry again in ${Enums.ChatColor.WHITE}${rl.retryIn / 1000}${Enums.ChatColor.RED} seconds`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const loginPacket = new CSLoginPacket().deserialize(firstPacket);
|
const loginPacket = new CSLoginPacket().deserialize(firstPacket);
|
||||||
player.state = Enums.ClientState.PRE_HANDSHAKE;
|
player.state = Enums.ClientState.PRE_HANDSHAKE;
|
||||||
if (loginPacket.gameVersion != VANILLA_PROTOCOL_VERSION) {
|
if (loginPacket.gameVersion != VANILLA_PROTOCOL_VERSION) {
|
||||||
|
@ -249,6 +268,7 @@ export class Proxy extends EventEmitter {
|
||||||
player.state = Enums.ClientState.POST_HANDSHAKE;
|
player.state = Enums.ClientState.POST_HANDSHAKE;
|
||||||
this._logger.info(`Handshake Success! Connecting player ${player.username} to server...`);
|
this._logger.info(`Handshake Success! Connecting player ${player.username} to server...`);
|
||||||
handled = true;
|
handled = true;
|
||||||
|
|
||||||
await player.connect({
|
await player.connect({
|
||||||
host: this.config.server.host,
|
host: this.config.server.host,
|
||||||
port: this.config.server.port,
|
port: this.config.server.port,
|
||||||
|
@ -278,7 +298,7 @@ export class Proxy extends EventEmitter {
|
||||||
try {
|
try {
|
||||||
const msg: CSChannelMessagePacket = packet as any;
|
const msg: CSChannelMessagePacket = packet as any;
|
||||||
if (msg.channel == Constants.EAGLERCRAFT_SKIN_CHANNEL_NAME) {
|
if (msg.channel == Constants.EAGLERCRAFT_SKIN_CHANNEL_NAME) {
|
||||||
await this.skinServer.handleRequest(msg, player);
|
await this.skinServer.handleRequest(msg, player, this);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this._logger.error(`Failed to process channel message packet! Error: ${err.stack || err}`);
|
this._logger.error(`Failed to process channel message packet! Error: ${err.stack || err}`);
|
||||||
|
@ -298,7 +318,7 @@ export class Proxy extends EventEmitter {
|
||||||
private _pollServer(host: string, port: number, interval?: number) {
|
private _pollServer(host: string, port: number, interval?: number) {
|
||||||
(async () => {
|
(async () => {
|
||||||
while (true) {
|
while (true) {
|
||||||
const motd = await Motd.MOTD.generateMOTDFromPing(host, port).catch((err) => {
|
const motd = await Motd.MOTD.generateMOTDFromPing(host, port, this.config.useNatives).catch((err) => {
|
||||||
this._logger.warn(`Error polling ${host}:${port} for MOTD: ${err.stack ?? err}`);
|
this._logger.warn(`Error polling ${host}:${port} for MOTD: ${err.stack ?? err}`);
|
||||||
});
|
});
|
||||||
if (motd) this.broadcastMotd = motd;
|
if (motd) this.broadcastMotd = motd;
|
||||||
|
|
115
src/proxy/ratelimit/BucketRatelimiter.ts
Normal file
115
src/proxy/ratelimit/BucketRatelimiter.ts
Normal file
|
@ -0,0 +1,115 @@
|
||||||
|
export default class BucketRateLimiter {
|
||||||
|
public capacity: number;
|
||||||
|
public refillsPerMin: number;
|
||||||
|
public keyMap: Map<string, KeyData>;
|
||||||
|
public static readonly GC_TOLERANCE: number = 50;
|
||||||
|
|
||||||
|
constructor(capacity: number, refillsPerMin: number) {
|
||||||
|
this.capacity = capacity;
|
||||||
|
this.refillsPerMin = refillsPerMin;
|
||||||
|
this.keyMap = new Map();
|
||||||
|
}
|
||||||
|
|
||||||
|
public consume(key: string, consumeTokens: number = 1): RateLimitData {
|
||||||
|
if (this.keyMap.size > BucketRateLimiter.GC_TOLERANCE) this.removeFull();
|
||||||
|
if (this.keyMap.has(key)) {
|
||||||
|
const bucket = this.keyMap.get(key);
|
||||||
|
|
||||||
|
// refill bucket as needed
|
||||||
|
const now = Date.now();
|
||||||
|
if (now - bucket.lastRefillTime > 60000) {
|
||||||
|
const refillTimes = Math.floor((now - bucket.lastRefillTime) / 60000);
|
||||||
|
bucket.tokens += refillTimes * this.refillsPerMin;
|
||||||
|
bucket.lastRefillTime = now - (refillTimes % 60000);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bucket.tokens >= consumeTokens) {
|
||||||
|
bucket.tokens -= consumeTokens;
|
||||||
|
return { success: true };
|
||||||
|
} else {
|
||||||
|
const difference = consumeTokens - bucket.tokens;
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
missingTokens: difference,
|
||||||
|
retryIn: Math.ceil(difference / this.refillsPerMin) * 60000 - ((now - bucket.lastRefillTime) % 60000),
|
||||||
|
retryAt: Date.now() + Math.ceil(difference / this.refillsPerMin) * 60000 - ((now - bucket.lastRefillTime) % 60000),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const bucket: KeyData = {
|
||||||
|
tokens: this.capacity,
|
||||||
|
lastRefillTime: Date.now(),
|
||||||
|
};
|
||||||
|
if (bucket.tokens >= consumeTokens) {
|
||||||
|
bucket.tokens -= consumeTokens;
|
||||||
|
this.keyMap.set(key, bucket);
|
||||||
|
return { success: true };
|
||||||
|
} else {
|
||||||
|
const difference = consumeTokens - bucket.tokens;
|
||||||
|
const now = Date.now();
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
missingTokens: difference,
|
||||||
|
retryIn: Math.ceil(difference / this.refillsPerMin) * 60000 - ((now - bucket.lastRefillTime) % 60000),
|
||||||
|
retryAt: Date.now() + Math.ceil(difference / this.refillsPerMin) * 60000 - ((now - bucket.lastRefillTime) % 60000),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public addToBucket(key: string, amount: number) {
|
||||||
|
if (this.keyMap.has(key)) {
|
||||||
|
this.keyMap.get(key).tokens += amount;
|
||||||
|
} else {
|
||||||
|
this.keyMap.set(key, {
|
||||||
|
tokens: this.capacity + amount,
|
||||||
|
lastRefillTime: Date.now(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public setBucketSize(key: string, amount: number) {
|
||||||
|
if (this.keyMap.has(key)) {
|
||||||
|
this.keyMap.get(key).tokens = amount;
|
||||||
|
} else {
|
||||||
|
this.keyMap.set(key, {
|
||||||
|
tokens: amount,
|
||||||
|
lastRefillTime: Date.now(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public subtractFromBucket(key: string, amount: number) {
|
||||||
|
if (this.keyMap.has(key)) {
|
||||||
|
const bucket = this.keyMap.get(key);
|
||||||
|
bucket.tokens -= amount;
|
||||||
|
} else {
|
||||||
|
this.keyMap.set(key, {
|
||||||
|
tokens: this.capacity - amount,
|
||||||
|
lastRefillTime: Date.now(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public removeFull() {
|
||||||
|
let remove: string[] = [];
|
||||||
|
this.keyMap.forEach((v, k) => {
|
||||||
|
if (v.tokens == this.capacity) {
|
||||||
|
remove.push(k);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
remove.forEach((v) => this.keyMap.delete(v));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type RateLimitData = {
|
||||||
|
success: boolean;
|
||||||
|
missingTokens?: number;
|
||||||
|
retryIn?: number;
|
||||||
|
retryAt?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type KeyData = {
|
||||||
|
tokens: number;
|
||||||
|
lastRefillTime: number;
|
||||||
|
};
|
21
src/proxy/ratelimit/ProxyRatelimitManager.ts
Normal file
21
src/proxy/ratelimit/ProxyRatelimitManager.ts
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import { Config } from "../../launcher_types.js";
|
||||||
|
import { Player } from "../Player.js";
|
||||||
|
import BucketRateLimiter from "./BucketRatelimiter.js";
|
||||||
|
|
||||||
|
export default class ProxyRatelimitManager {
|
||||||
|
http: BucketRateLimiter;
|
||||||
|
motd: BucketRateLimiter;
|
||||||
|
ws: BucketRateLimiter;
|
||||||
|
connect: BucketRateLimiter;
|
||||||
|
skinsIP: BucketRateLimiter;
|
||||||
|
skinsConnection: BucketRateLimiter;
|
||||||
|
|
||||||
|
constructor(config: Config["adapter"]["ratelimits"]) {
|
||||||
|
this.http = new BucketRateLimiter(config.limits.http, config.limits.http);
|
||||||
|
this.ws = new BucketRateLimiter(config.limits.ws, config.limits.ws);
|
||||||
|
this.motd = new BucketRateLimiter(config.limits.motd, config.limits.motd);
|
||||||
|
this.connect = new BucketRateLimiter(config.limits.connect, config.limits.connect);
|
||||||
|
this.skinsIP = new BucketRateLimiter(config.limits.skinsIp, config.limits.skinsIp);
|
||||||
|
this.skinsConnection = new BucketRateLimiter(config.limits.skins, config.limits.skins);
|
||||||
|
}
|
||||||
|
}
|
|
@ -9,6 +9,8 @@ import { CSChannelMessagePacket } from "../packets/channel/CSChannelMessage.js";
|
||||||
import { SCChannelMessagePacket } from "../packets/channel/SCChannelMessage.js";
|
import { SCChannelMessagePacket } from "../packets/channel/SCChannelMessage.js";
|
||||||
import { Logger } from "../../logger.js";
|
import { Logger } from "../../logger.js";
|
||||||
import fetch from "node-fetch";
|
import fetch from "node-fetch";
|
||||||
|
import Jimp from "jimp";
|
||||||
|
import { ImageEditor } from "./ImageEditor.js";
|
||||||
|
|
||||||
// TODO: convert all functions to use MineProtocol's UUID manipulation functions
|
// TODO: convert all functions to use MineProtocol's UUID manipulation functions
|
||||||
|
|
||||||
|
@ -67,22 +69,15 @@ export namespace EaglerSkins {
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function skinUrlFromUuid(uuid: string): Promise<string> {
|
export async function skinUrlFromUuid(uuid: string): Promise<string> {
|
||||||
const response = (await (
|
const response = (await (await fetch(`https://sessionserver.mojang.com/session/minecraft/profile/${uuid}`)).json()) as unknown as MojangFetchProfileResponse;
|
||||||
await fetch(
|
const parsed = JSON.parse(Buffer.from(response.properties[0].value, "base64").toString()) as unknown as MojangTextureResponse;
|
||||||
`https://sessionserver.mojang.com/session/minecraft/profile/${uuid}`
|
|
||||||
)
|
|
||||||
).json()) as unknown as MojangFetchProfileResponse;
|
|
||||||
const parsed = JSON.parse(
|
|
||||||
Buffer.from(response.properties[0].value, "base64").toString()
|
|
||||||
) as unknown as MojangTextureResponse;
|
|
||||||
console.log(parsed.textures.SKIN.url);
|
console.log(parsed.textures.SKIN.url);
|
||||||
return parsed.textures.SKIN.url;
|
return parsed.textures.SKIN.url;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function downloadSkin(skinUrl: string): Promise<Buffer> {
|
export function downloadSkin(skinUrl: string): Promise<Buffer> {
|
||||||
const url = new URL(skinUrl);
|
const url = new URL(skinUrl);
|
||||||
if (url.protocol != "https:" && url.protocol != "http:")
|
if (url.protocol != "https:" && url.protocol != "http:") throw new Error("Invalid skin URL protocol!");
|
||||||
throw new Error("Invalid skin URL protocol!");
|
|
||||||
return new Promise<Buffer>(async (res, rej) => {
|
return new Promise<Buffer>(async (res, rej) => {
|
||||||
const skin = await fetch(skinUrl);
|
const skin = await fetch(skinUrl);
|
||||||
if (skin.status != 200) {
|
if (skin.status != 200) {
|
||||||
|
@ -94,9 +89,7 @@ export namespace EaglerSkins {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function readClientDownloadSkinRequestPacket(
|
export function readClientDownloadSkinRequestPacket(message: Buffer): ClientDownloadSkinRequest {
|
||||||
message: Buffer
|
|
||||||
): ClientDownloadSkinRequest {
|
|
||||||
const ret: ClientDownloadSkinRequest = {
|
const ret: ClientDownloadSkinRequest = {
|
||||||
id: null,
|
id: null,
|
||||||
uuid: null,
|
uuid: null,
|
||||||
|
@ -111,23 +104,11 @@ export namespace EaglerSkins {
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function writeClientDownloadSkinRequestPacket(
|
export function writeClientDownloadSkinRequestPacket(uuid: string | Buffer, url: string): Buffer {
|
||||||
uuid: string | Buffer,
|
return Buffer.concat([[Enums.EaglerSkinPacketId.CFetchSkinReq], MineProtocol.writeUUID(uuid), [0x0], MineProtocol.writeString(url)].map((arr) => (arr instanceof Uint8Array ? arr : Buffer.from(arr))));
|
||||||
url: string
|
|
||||||
): Buffer {
|
|
||||||
return Buffer.concat(
|
|
||||||
[
|
|
||||||
[Enums.EaglerSkinPacketId.CFetchSkinReq],
|
|
||||||
MineProtocol.writeUUID(uuid),
|
|
||||||
[0x0],
|
|
||||||
MineProtocol.writeString(url),
|
|
||||||
].map((arr) => (arr instanceof Uint8Array ? arr : Buffer.from(arr)))
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function readServerFetchSkinResultBuiltInPacket(
|
export function readServerFetchSkinResultBuiltInPacket(message: Buffer): ServerFetchSkinResultBuiltIn {
|
||||||
message: Buffer
|
|
||||||
): ServerFetchSkinResultBuiltIn {
|
|
||||||
const ret: ServerFetchSkinResultBuiltIn = {
|
const ret: ServerFetchSkinResultBuiltIn = {
|
||||||
id: null,
|
id: null,
|
||||||
uuid: null,
|
uuid: null,
|
||||||
|
@ -135,31 +116,20 @@ export namespace EaglerSkins {
|
||||||
};
|
};
|
||||||
const id = MineProtocol.readVarInt(message),
|
const id = MineProtocol.readVarInt(message),
|
||||||
uuid = MineProtocol.readUUID(id.newBuffer),
|
uuid = MineProtocol.readUUID(id.newBuffer),
|
||||||
skinId = MineProtocol.readVarInt(
|
skinId = MineProtocol.readVarInt(id.newBuffer.subarray(id.newBuffer.length));
|
||||||
id.newBuffer.subarray(id.newBuffer.length)
|
|
||||||
);
|
|
||||||
ret.id = id.value;
|
ret.id = id.value;
|
||||||
ret.uuid = uuid.value;
|
ret.uuid = uuid.value;
|
||||||
ret.skinId = skinId.value;
|
ret.skinId = skinId.value;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function writeServerFetchSkinResultBuiltInPacket(
|
export function writeServerFetchSkinResultBuiltInPacket(uuid: string | Buffer, skinId: number): Buffer {
|
||||||
uuid: string | Buffer,
|
|
||||||
skinId: number
|
|
||||||
): Buffer {
|
|
||||||
uuid = typeof uuid == "string" ? Util.uuidStringToBuffer(uuid) : uuid;
|
uuid = typeof uuid == "string" ? Util.uuidStringToBuffer(uuid) : uuid;
|
||||||
console.log(1);
|
console.log(1);
|
||||||
return Buffer.concat([
|
return Buffer.concat([Buffer.from([Enums.EaglerSkinPacketId.SFetchSkinBuiltInRes]), uuid as Buffer, Buffer.from([skinId >> 24, skinId >> 16, skinId >> 8, skinId & 0xff])]);
|
||||||
Buffer.from([Enums.EaglerSkinPacketId.SFetchSkinBuiltInRes]),
|
|
||||||
uuid as Buffer,
|
|
||||||
Buffer.from([skinId >> 24, skinId >> 16, skinId >> 8, skinId & 0xff]),
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function readServerFetchSkinResultCustomPacket(
|
export function readServerFetchSkinResultCustomPacket(message: Buffer): ServerFetchSkinResultCustom {
|
||||||
message: Buffer
|
|
||||||
): ServerFetchSkinResultCustom {
|
|
||||||
const ret: ServerFetchSkinResultCustom = {
|
const ret: ServerFetchSkinResultCustom = {
|
||||||
id: null,
|
id: null,
|
||||||
uuid: null,
|
uuid: null,
|
||||||
|
@ -167,22 +137,14 @@ export namespace EaglerSkins {
|
||||||
};
|
};
|
||||||
const id = MineProtocol.readVarInt(message),
|
const id = MineProtocol.readVarInt(message),
|
||||||
uuid = MineProtocol.readUUID(id.newBuffer),
|
uuid = MineProtocol.readUUID(id.newBuffer),
|
||||||
skin = uuid.newBuffer.subarray(
|
skin = uuid.newBuffer.subarray(0, Constants.EAGLERCRAFT_SKIN_CUSTOM_LENGTH);
|
||||||
0,
|
|
||||||
Constants.EAGLERCRAFT_SKIN_CUSTOM_LENGTH
|
|
||||||
);
|
|
||||||
ret.id = id.value;
|
ret.id = id.value;
|
||||||
ret.uuid = uuid.value;
|
ret.uuid = uuid.value;
|
||||||
ret.skin = skin;
|
ret.skin = skin;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: fix bug where some people are missing left arm and leg
|
export function writeServerFetchSkinResultCustomPacket(uuid: string | Buffer, skin: Buffer, downloaded: boolean): Buffer {
|
||||||
export function writeServerFetchSkinResultCustomPacket(
|
|
||||||
uuid: string | Buffer,
|
|
||||||
skin: Buffer,
|
|
||||||
downloaded: boolean
|
|
||||||
): Buffer {
|
|
||||||
uuid = typeof uuid == "string" ? Util.uuidStringToBuffer(uuid) : uuid;
|
uuid = typeof uuid == "string" ? Util.uuidStringToBuffer(uuid) : uuid;
|
||||||
return Buffer.concat(
|
return Buffer.concat(
|
||||||
[
|
[
|
||||||
|
@ -194,9 +156,7 @@ export namespace EaglerSkins {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function readClientFetchEaglerSkinPacket(
|
export function readClientFetchEaglerSkinPacket(buff: Buffer): ClientFetchEaglerSkin {
|
||||||
buff: Buffer
|
|
||||||
): ClientFetchEaglerSkin {
|
|
||||||
const ret: ClientFetchEaglerSkin = {
|
const ret: ClientFetchEaglerSkin = {
|
||||||
id: null,
|
id: null,
|
||||||
uuid: null,
|
uuid: null,
|
||||||
|
@ -208,386 +168,67 @@ export namespace EaglerSkins {
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function writeClientFetchEaglerSkin(
|
|
||||||
uuid: string | Buffer,
|
|
||||||
url: string
|
|
||||||
): Buffer {
|
|
||||||
uuid = typeof uuid == "string" ? Util.uuidStringToBuffer(uuid) : uuid;
|
|
||||||
return Buffer.concat(
|
|
||||||
[
|
|
||||||
[Enums.EaglerSkinPacketId.CFetchSkinEaglerPlayerReq],
|
|
||||||
uuid,
|
|
||||||
[0x00],
|
|
||||||
MineProtocol.writeString(url),
|
|
||||||
].map((arr) => (arr instanceof Uint8Array ? arr : Buffer.from(arr)))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function copyRawPixels(
|
|
||||||
imageIn: sharp.Sharp,
|
|
||||||
imageOut: sharp.Sharp,
|
|
||||||
dx1: number,
|
|
||||||
dy1: number,
|
|
||||||
dx2: number,
|
|
||||||
dy2: number,
|
|
||||||
sx1: number,
|
|
||||||
sy1: number,
|
|
||||||
sx2: number,
|
|
||||||
sy2: number
|
|
||||||
): Promise<sharp.Sharp> {
|
|
||||||
const inMeta = await imageIn.metadata(),
|
|
||||||
outMeta = await imageOut.metadata();
|
|
||||||
|
|
||||||
if (dx1 > dx2) {
|
|
||||||
return _copyRawPixels(
|
|
||||||
imageIn,
|
|
||||||
imageOut,
|
|
||||||
sx1,
|
|
||||||
sy1,
|
|
||||||
dx2,
|
|
||||||
dy1,
|
|
||||||
sx2 - sx1,
|
|
||||||
sy2 - sy1,
|
|
||||||
inMeta.width!,
|
|
||||||
outMeta.width!,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return _copyRawPixels(
|
|
||||||
imageIn,
|
|
||||||
imageOut,
|
|
||||||
sx1,
|
|
||||||
sy1,
|
|
||||||
dx1,
|
|
||||||
dy1,
|
|
||||||
sx2 - sx1,
|
|
||||||
sy2 - sy1,
|
|
||||||
inMeta.width!,
|
|
||||||
outMeta.width!,
|
|
||||||
false
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function _copyRawPixels(
|
|
||||||
imageIn: sharp.Sharp,
|
|
||||||
imageOut: sharp.Sharp,
|
|
||||||
srcX: number,
|
|
||||||
srcY: number,
|
|
||||||
dstX: number,
|
|
||||||
dstY: number,
|
|
||||||
width: number,
|
|
||||||
height: number,
|
|
||||||
imgSrcWidth: number,
|
|
||||||
imgDstWidth: number,
|
|
||||||
flip: boolean
|
|
||||||
): Promise<sharp.Sharp> {
|
|
||||||
const inData = await imageIn.raw().toBuffer();
|
|
||||||
const outData = await imageOut.raw().toBuffer();
|
|
||||||
const outMeta = await imageOut.metadata();
|
|
||||||
|
|
||||||
for (let y = 0; y < height; y++) {
|
|
||||||
for (let x = 0; x < width; x++) {
|
|
||||||
let srcIndex = (srcY + y) * imgSrcWidth + srcX + x;
|
|
||||||
let dstIndex = (dstY + y) * imgDstWidth + dstX + x;
|
|
||||||
|
|
||||||
if (flip) {
|
|
||||||
srcIndex = (srcY + y) * imgSrcWidth + srcX + (width - x - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let c = 0; c < 4; c++) {
|
|
||||||
// Assuming RGBA channels
|
|
||||||
outData[dstIndex * 4 + c] = inData[srcIndex * 4 + c];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return sharp(outData, {
|
|
||||||
raw: {
|
|
||||||
width: outMeta.width!,
|
|
||||||
height: outMeta.height!,
|
|
||||||
channels: 4,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function toEaglerSkin(
|
|
||||||
image: Buffer
|
|
||||||
): Promise<
|
|
||||||
Util.BoundedBuffer<typeof Constants.EAGLERCRAFT_SKIN_CUSTOM_LENGTH>
|
|
||||||
> {
|
|
||||||
const meta = await sharp(image).metadata();
|
|
||||||
let sharpImage = sharp(image);
|
|
||||||
if (meta.height != 64) {
|
|
||||||
// assume 32 height skin
|
|
||||||
let imageOut = sharp(
|
|
||||||
await sharpImage
|
|
||||||
.extend({ bottom: 32, background: { r: 0, g: 0, b: 0, alpha: 0 } })
|
|
||||||
.toBuffer()
|
|
||||||
);
|
|
||||||
|
|
||||||
imageOut = await copyRawPixels(
|
|
||||||
sharpImage,
|
|
||||||
imageOut,
|
|
||||||
24,
|
|
||||||
48,
|
|
||||||
20,
|
|
||||||
52,
|
|
||||||
4,
|
|
||||||
16,
|
|
||||||
8,
|
|
||||||
20
|
|
||||||
);
|
|
||||||
imageOut = await copyRawPixels(
|
|
||||||
sharpImage,
|
|
||||||
imageOut,
|
|
||||||
28,
|
|
||||||
48,
|
|
||||||
24,
|
|
||||||
52,
|
|
||||||
8,
|
|
||||||
16,
|
|
||||||
12,
|
|
||||||
20
|
|
||||||
);
|
|
||||||
imageOut = await copyRawPixels(
|
|
||||||
sharpImage,
|
|
||||||
imageOut,
|
|
||||||
20,
|
|
||||||
52,
|
|
||||||
16,
|
|
||||||
64,
|
|
||||||
8,
|
|
||||||
20,
|
|
||||||
12,
|
|
||||||
32
|
|
||||||
);
|
|
||||||
imageOut = await copyRawPixels(
|
|
||||||
sharpImage,
|
|
||||||
imageOut,
|
|
||||||
24,
|
|
||||||
52,
|
|
||||||
20,
|
|
||||||
64,
|
|
||||||
4,
|
|
||||||
20,
|
|
||||||
8,
|
|
||||||
32
|
|
||||||
);
|
|
||||||
imageOut = await copyRawPixels(
|
|
||||||
sharpImage,
|
|
||||||
imageOut,
|
|
||||||
28,
|
|
||||||
52,
|
|
||||||
24,
|
|
||||||
64,
|
|
||||||
0,
|
|
||||||
20,
|
|
||||||
4,
|
|
||||||
32
|
|
||||||
);
|
|
||||||
imageOut = await copyRawPixels(
|
|
||||||
sharpImage,
|
|
||||||
imageOut,
|
|
||||||
32,
|
|
||||||
52,
|
|
||||||
28,
|
|
||||||
64,
|
|
||||||
12,
|
|
||||||
20,
|
|
||||||
16,
|
|
||||||
32
|
|
||||||
);
|
|
||||||
imageOut = await copyRawPixels(
|
|
||||||
sharpImage,
|
|
||||||
imageOut,
|
|
||||||
40,
|
|
||||||
48,
|
|
||||||
36,
|
|
||||||
52,
|
|
||||||
44,
|
|
||||||
16,
|
|
||||||
48,
|
|
||||||
20
|
|
||||||
);
|
|
||||||
imageOut = await copyRawPixels(
|
|
||||||
sharpImage,
|
|
||||||
imageOut,
|
|
||||||
44,
|
|
||||||
48,
|
|
||||||
40,
|
|
||||||
52,
|
|
||||||
48,
|
|
||||||
16,
|
|
||||||
52,
|
|
||||||
20
|
|
||||||
);
|
|
||||||
imageOut = await copyRawPixels(
|
|
||||||
sharpImage,
|
|
||||||
imageOut,
|
|
||||||
36,
|
|
||||||
52,
|
|
||||||
32,
|
|
||||||
64,
|
|
||||||
48,
|
|
||||||
20,
|
|
||||||
52,
|
|
||||||
32
|
|
||||||
);
|
|
||||||
imageOut = await copyRawPixels(
|
|
||||||
sharpImage,
|
|
||||||
imageOut,
|
|
||||||
40,
|
|
||||||
52,
|
|
||||||
36,
|
|
||||||
64,
|
|
||||||
44,
|
|
||||||
20,
|
|
||||||
48,
|
|
||||||
32
|
|
||||||
);
|
|
||||||
imageOut = await copyRawPixels(
|
|
||||||
sharpImage,
|
|
||||||
imageOut,
|
|
||||||
44,
|
|
||||||
52,
|
|
||||||
40,
|
|
||||||
64,
|
|
||||||
40,
|
|
||||||
20,
|
|
||||||
44,
|
|
||||||
32
|
|
||||||
);
|
|
||||||
imageOut = await copyRawPixels(
|
|
||||||
sharpImage,
|
|
||||||
imageOut,
|
|
||||||
48,
|
|
||||||
52,
|
|
||||||
44,
|
|
||||||
64,
|
|
||||||
52,
|
|
||||||
20,
|
|
||||||
56,
|
|
||||||
32
|
|
||||||
);
|
|
||||||
|
|
||||||
sharpImage = imageOut;
|
|
||||||
}
|
|
||||||
|
|
||||||
const r = await sharpImage
|
|
||||||
.extractChannel("red")
|
|
||||||
.raw({ depth: "uchar" })
|
|
||||||
.toBuffer();
|
|
||||||
const g = await sharpImage
|
|
||||||
.extractChannel("green")
|
|
||||||
.raw({ depth: "uchar" })
|
|
||||||
.toBuffer();
|
|
||||||
const b = await sharpImage
|
|
||||||
.extractChannel("blue")
|
|
||||||
.raw({ depth: "uchar" })
|
|
||||||
.toBuffer();
|
|
||||||
const a = await sharpImage
|
|
||||||
.ensureAlpha()
|
|
||||||
.extractChannel(3)
|
|
||||||
.toColorspace("b-w")
|
|
||||||
.raw({ depth: "uchar" })
|
|
||||||
.toBuffer();
|
|
||||||
const newBuff = Buffer.alloc(Constants.EAGLERCRAFT_SKIN_CUSTOM_LENGTH);
|
|
||||||
for (let i = 1; i < 64 ** 2; i++) {
|
|
||||||
const bytePos = i * 4;
|
|
||||||
newBuff[bytePos] = a[i];
|
|
||||||
newBuff[bytePos + 1] = b[i];
|
|
||||||
newBuff[bytePos + 2] = g[i];
|
|
||||||
newBuff[bytePos + 3] = r[i];
|
|
||||||
}
|
|
||||||
return newBuff;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class SkinServer {
|
export class SkinServer {
|
||||||
public allowedSkinDomains: string[];
|
public allowedSkinDomains: string[];
|
||||||
public proxy: Proxy;
|
public proxy: Proxy;
|
||||||
|
public usingNative: boolean;
|
||||||
private _logger: Logger;
|
private _logger: Logger;
|
||||||
|
|
||||||
constructor(proxy: Proxy, allowedSkinDomains?: string[]) {
|
constructor(proxy: Proxy, native: boolean, allowedSkinDomains?: string[]) {
|
||||||
this.allowedSkinDomains = allowedSkinDomains ?? [
|
this.allowedSkinDomains = allowedSkinDomains ?? ["textures.minecraft.net"];
|
||||||
"textures.minecraft.net",
|
|
||||||
];
|
|
||||||
this.proxy = proxy ?? PROXY;
|
this.proxy = proxy ?? PROXY;
|
||||||
|
this.usingNative = native;
|
||||||
this._logger = new Logger("SkinServer");
|
this._logger = new Logger("SkinServer");
|
||||||
this._logger.info("Started EaglercraftX skin server.");
|
this._logger.info("Started EaglercraftX skin server.");
|
||||||
}
|
}
|
||||||
|
|
||||||
public async handleRequest(packet: CSChannelMessagePacket, caller: Player) {
|
public async handleRequest(packet: CSChannelMessagePacket, caller: Player, proxy: Proxy) {
|
||||||
if (packet.messageType == Enums.ChannelMessageType.SERVER)
|
if (packet.messageType == Enums.ChannelMessageType.SERVER) throw new Error("Server message was passed to client message handler!");
|
||||||
throw new Error("Server message was passed to client message handler!");
|
else if (packet.channel != Constants.EAGLERCRAFT_SKIN_CHANNEL_NAME) throw new Error("Cannot handle non-EaglerX skin channel messages!");
|
||||||
else if (packet.channel != Constants.EAGLERCRAFT_SKIN_CHANNEL_NAME)
|
|
||||||
throw new Error("Cannot handle non-EaglerX skin channel messages!");
|
{
|
||||||
|
const rl = proxy.ratelimit.skinsConnection.consume(caller.username),
|
||||||
|
rlip = proxy.ratelimit.skinsIP.consume(caller.ws._socket.remoteAddress);
|
||||||
|
if (!rl.success || !rlip.success) return;
|
||||||
|
}
|
||||||
|
|
||||||
switch (packet.data[0] as Enums.EaglerSkinPacketId) {
|
switch (packet.data[0] as Enums.EaglerSkinPacketId) {
|
||||||
default:
|
default:
|
||||||
throw new Error("Unknown operation!");
|
throw new Error("Unknown operation!");
|
||||||
break;
|
break;
|
||||||
case Enums.EaglerSkinPacketId.CFetchSkinEaglerPlayerReq:
|
case Enums.EaglerSkinPacketId.CFetchSkinEaglerPlayerReq:
|
||||||
const parsedPacket_0 = EaglerSkins.readClientFetchEaglerSkinPacket(
|
const parsedPacket_0 = EaglerSkins.readClientFetchEaglerSkinPacket(packet.data);
|
||||||
packet.data
|
|
||||||
);
|
|
||||||
const player = this.proxy.fetchUserByUUID(parsedPacket_0.uuid);
|
const player = this.proxy.fetchUserByUUID(parsedPacket_0.uuid);
|
||||||
if (player) {
|
if (player) {
|
||||||
if (player.skin.type == Enums.SkinType.BUILTIN) {
|
if (player.skin.type == Enums.SkinType.BUILTIN) {
|
||||||
const response = new SCChannelMessagePacket();
|
const response = new SCChannelMessagePacket();
|
||||||
response.channel = Constants.EAGLERCRAFT_SKIN_CHANNEL_NAME;
|
response.channel = Constants.EAGLERCRAFT_SKIN_CHANNEL_NAME;
|
||||||
response.data =
|
response.data = EaglerSkins.writeServerFetchSkinResultBuiltInPacket(player.uuid, player.skin.builtInSkin);
|
||||||
EaglerSkins.writeServerFetchSkinResultBuiltInPacket(
|
|
||||||
player.uuid,
|
|
||||||
player.skin.builtInSkin
|
|
||||||
);
|
|
||||||
caller.write(response);
|
caller.write(response);
|
||||||
} else if (player.skin.type == Enums.SkinType.CUSTOM) {
|
} else if (player.skin.type == Enums.SkinType.CUSTOM) {
|
||||||
const response = new SCChannelMessagePacket();
|
const response = new SCChannelMessagePacket();
|
||||||
response.channel = Constants.EAGLERCRAFT_SKIN_CHANNEL_NAME;
|
response.channel = Constants.EAGLERCRAFT_SKIN_CHANNEL_NAME;
|
||||||
response.data =
|
response.data = EaglerSkins.writeServerFetchSkinResultCustomPacket(player.uuid, player.skin.skin, false);
|
||||||
EaglerSkins.writeServerFetchSkinResultCustomPacket(
|
|
||||||
player.uuid,
|
|
||||||
player.skin.skin,
|
|
||||||
false
|
|
||||||
);
|
|
||||||
caller.write(response);
|
caller.write(response);
|
||||||
} else
|
} else this._logger.warn(`Player ${caller.username} attempted to fetch player ${player.uuid}'s skin, but their skin hasn't loaded yet!`);
|
||||||
this._logger.warn(
|
|
||||||
`Player ${caller.username} attempted to fetch player ${player.uuid}'s skin, but their skin hasn't loaded yet!`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case Enums.EaglerSkinPacketId.CFetchSkinReq:
|
case Enums.EaglerSkinPacketId.CFetchSkinReq:
|
||||||
const parsedPacket_1 =
|
const parsedPacket_1 = EaglerSkins.readClientDownloadSkinRequestPacket(packet.data),
|
||||||
EaglerSkins.readClientDownloadSkinRequestPacket(packet.data),
|
|
||||||
url = new URL(parsedPacket_1.url).hostname;
|
url = new URL(parsedPacket_1.url).hostname;
|
||||||
if (
|
if (!this.allowedSkinDomains.some((domain) => Util.areDomainsEqual(domain, url))) {
|
||||||
!this.allowedSkinDomains.some((domain) =>
|
this._logger.warn(`Player ${caller.username} tried to download a skin with a disallowed domain name(${url})!`);
|
||||||
Util.areDomainsEqual(domain, url)
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
this._logger.warn(
|
|
||||||
`Player ${caller.username} tried to download a skin with a disallowed domain name(${url})!`
|
|
||||||
);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const fetched = await EaglerSkins.downloadSkin(parsedPacket_1.url),
|
const fetched = await EaglerSkins.downloadSkin(parsedPacket_1.url),
|
||||||
processed = await EaglerSkins.toEaglerSkin(fetched),
|
processed = this.usingNative ? await ImageEditor.toEaglerSkin(fetched) : await ImageEditor.toEaglerSkinJS(fetched),
|
||||||
response = new SCChannelMessagePacket();
|
response = new SCChannelMessagePacket();
|
||||||
response.channel = Constants.EAGLERCRAFT_SKIN_CHANNEL_NAME;
|
response.channel = Constants.EAGLERCRAFT_SKIN_CHANNEL_NAME;
|
||||||
response.data = EaglerSkins.writeServerFetchSkinResultCustomPacket(
|
response.data = EaglerSkins.writeServerFetchSkinResultCustomPacket(parsedPacket_1.uuid, processed, true);
|
||||||
parsedPacket_1.uuid,
|
|
||||||
processed,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
caller.write(response);
|
caller.write(response);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this._logger.warn(
|
this._logger.warn(`Failed to fetch skin URL ${parsedPacket_1.url} for player ${caller.username}: ${err.stack ?? err}`);
|
||||||
`Failed to fetch skin URL ${parsedPacket_1.url} for player ${
|
|
||||||
caller.username
|
|
||||||
}: ${err.stack ?? err}`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
217
src/proxy/skins/ImageEditor.ts
Normal file
217
src/proxy/skins/ImageEditor.ts
Normal file
|
@ -0,0 +1,217 @@
|
||||||
|
import { Constants } from "../Constants.js";
|
||||||
|
import { Enums } from "../Enums.js";
|
||||||
|
import { MineProtocol } from "../Protocol.js";
|
||||||
|
import { Util } from "../Util.js";
|
||||||
|
import fs from "fs/promises";
|
||||||
|
|
||||||
|
let Jimp: Jimp = null;
|
||||||
|
type Jimp = any;
|
||||||
|
|
||||||
|
let sharp: any = null;
|
||||||
|
type Sharp = any;
|
||||||
|
|
||||||
|
export namespace ImageEditor {
|
||||||
|
let loadedLibraries: boolean = false;
|
||||||
|
|
||||||
|
export async function loadLibraries(native: boolean) {
|
||||||
|
if (loadedLibraries) return;
|
||||||
|
if (native) sharp = (await import("sharp")).default;
|
||||||
|
else Jimp = (await import("jimp")).default;
|
||||||
|
loadedLibraries = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function writeClientFetchEaglerSkin(uuid: string | Buffer, url: string): Buffer {
|
||||||
|
uuid = typeof uuid == "string" ? Util.uuidStringToBuffer(uuid) : uuid;
|
||||||
|
return Buffer.concat([[Enums.EaglerSkinPacketId.CFetchSkinEaglerPlayerReq], uuid, [0x00], MineProtocol.writeString(url)].map((arr) => (arr instanceof Uint8Array ? arr : Buffer.from(arr))));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function copyRawPixelsJS(imageIn: Jimp, imageOut: Jimp, dx1: number, dy1: number, dx2: number, dy2: number, sx1: number, sy1: number, sx2: number, sy2: number): Promise<Jimp> {
|
||||||
|
if (dx1 > dx2) {
|
||||||
|
return _copyRawPixelsJS(imageIn, imageOut, sx1, sy1, dx2, dy1, sx2 - sx1, sy2 - sy1, imageIn.getWidth(), imageOut.getWidth(), true);
|
||||||
|
} else {
|
||||||
|
return _copyRawPixelsJS(imageIn, imageOut, sx1, sy1, dx2, dy1, sx2 - sx1, sy2 - sy1, imageIn.getWidth(), imageOut.getWidth(), false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function _copyRawPixelsJS(imageIn: Jimp, imageOut: Jimp, srcX: number, srcY: number, dstX: number, dstY: number, inWidth: number, outWidth: number, imgSrcWidth: number, imgDstWidth: number, flip: boolean): Promise<Jimp> {
|
||||||
|
const inData = imageIn.bitmap.data,
|
||||||
|
outData = imageIn.bitmap.data;
|
||||||
|
|
||||||
|
for (let y = 0; y < outWidth; y++) {
|
||||||
|
for (let x = 0; x < inWidth; x++) {
|
||||||
|
let srcIndex = (srcY + y) * imgSrcWidth + srcX + x;
|
||||||
|
let dstIndex = (dstY + y) * imgDstWidth + dstX + x;
|
||||||
|
|
||||||
|
if (flip) {
|
||||||
|
srcIndex = (srcY + y) * imgSrcWidth + srcX + (inWidth - x - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let c = 0; c < 4; c++) {
|
||||||
|
// Assuming RGBA channels
|
||||||
|
outData[dstIndex * 4 + c] = inData[srcIndex * 4 + c];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return await Jimp.read(outData);
|
||||||
|
|
||||||
|
// return sharp(outData, {
|
||||||
|
// raw: {
|
||||||
|
// width: outMeta.width!,
|
||||||
|
// height: outMeta.height!,
|
||||||
|
// channels: 4,
|
||||||
|
// },
|
||||||
|
// });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function copyRawPixels(imageIn: Sharp, imageOut: Sharp, dx1: number, dy1: number, dx2: number, dy2: number, sx1: number, sy1: number, sx2: number, sy2: number): Promise<Sharp> {
|
||||||
|
const inMeta = await imageIn.metadata(),
|
||||||
|
outMeta = await imageOut.metadata();
|
||||||
|
|
||||||
|
if (dx1 > dx2) {
|
||||||
|
return _copyRawPixels(imageIn, imageOut, sx1, sy1, dx2, dy1, sx2 - sx1, sy2 - sy1, inMeta.width!, outMeta.width!, true);
|
||||||
|
} else {
|
||||||
|
return _copyRawPixels(imageIn, imageOut, sx1, sy1, dx1, dy1, sx2 - sx1, sy2 - sy1, inMeta.width!, outMeta.width!, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function _copyRawPixels(imageIn: Sharp, imageOut: Sharp, srcX: number, srcY: number, dstX: number, dstY: number, width: number, height: number, imgSrcWidth: number, imgDstWidth: number, flip: boolean): Promise<Sharp> {
|
||||||
|
const inData = await imageIn.raw().toBuffer();
|
||||||
|
const outData = await imageOut.raw().toBuffer();
|
||||||
|
const outMeta = await imageOut.metadata();
|
||||||
|
|
||||||
|
for (let y = 0; y < height; y++) {
|
||||||
|
for (let x = 0; x < width; x++) {
|
||||||
|
let srcIndex = (srcY + y) * imgSrcWidth + srcX + x;
|
||||||
|
let dstIndex = (dstY + y) * imgDstWidth + dstX + x;
|
||||||
|
|
||||||
|
if (flip) {
|
||||||
|
srcIndex = (srcY + y) * imgSrcWidth + srcX + (width - x - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let c = 0; c < 4; c++) {
|
||||||
|
// Assuming RGBA channels
|
||||||
|
outData[dstIndex * 4 + c] = inData[srcIndex * 4 + c];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sharp(outData, {
|
||||||
|
raw: {
|
||||||
|
width: outMeta.width!,
|
||||||
|
height: outMeta.height!,
|
||||||
|
channels: 4,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function toEaglerSkinJS(image: Buffer): Promise<Util.BoundedBuffer<typeof Constants.EAGLERCRAFT_SKIN_CUSTOM_LENGTH>> {
|
||||||
|
let jimpImage = await Jimp.read(image),
|
||||||
|
height = jimpImage.getHeight();
|
||||||
|
if (height != 64) {
|
||||||
|
// assume 32 height skin
|
||||||
|
let imageOut = await Jimp.create(64, 64, 0x0);
|
||||||
|
for (let row = 0; row < height; row++) {
|
||||||
|
for (let col = 0; col < 64; col++) {
|
||||||
|
imageOut.setPixelColor(jimpImage.getPixelColor(row, col), row, col);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
imageOut = await copyRawPixelsJS(jimpImage, imageOut, 24, 48, 20, 52, 4, 16, 8, 20);
|
||||||
|
imageOut = await copyRawPixelsJS(jimpImage, imageOut, 28, 48, 24, 52, 8, 16, 12, 20);
|
||||||
|
imageOut = await copyRawPixelsJS(jimpImage, imageOut, 20, 52, 16, 64, 8, 20, 12, 32);
|
||||||
|
imageOut = await copyRawPixelsJS(jimpImage, imageOut, 24, 52, 20, 64, 4, 20, 8, 32);
|
||||||
|
imageOut = await copyRawPixelsJS(jimpImage, imageOut, 28, 52, 24, 64, 0, 20, 4, 32);
|
||||||
|
imageOut = await copyRawPixelsJS(jimpImage, imageOut, 32, 52, 28, 64, 12, 20, 16, 32);
|
||||||
|
imageOut = await copyRawPixelsJS(jimpImage, imageOut, 40, 48, 36, 52, 44, 16, 48, 20);
|
||||||
|
imageOut = await copyRawPixelsJS(jimpImage, imageOut, 44, 48, 40, 52, 48, 16, 52, 20);
|
||||||
|
imageOut = await copyRawPixelsJS(jimpImage, imageOut, 36, 52, 32, 64, 48, 20, 52, 32);
|
||||||
|
imageOut = await copyRawPixelsJS(jimpImage, imageOut, 40, 52, 36, 64, 44, 20, 48, 32);
|
||||||
|
imageOut = await copyRawPixelsJS(jimpImage, imageOut, 44, 52, 40, 64, 40, 20, 44, 32);
|
||||||
|
imageOut = await copyRawPixelsJS(jimpImage, imageOut, 48, 52, 44, 64, 52, 20, 56, 32);
|
||||||
|
|
||||||
|
jimpImage = imageOut;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newBuff = Buffer.alloc(Constants.EAGLERCRAFT_SKIN_CUSTOM_LENGTH);
|
||||||
|
const bitmap = jimpImage.bitmap.data;
|
||||||
|
for (let i = 1; i < 64 ** 2; i++) {
|
||||||
|
const bytePos = i * 4;
|
||||||
|
// red, green, blue, alpha => alpha, blue, green, red
|
||||||
|
newBuff[bytePos] = bitmap[bytePos + 3];
|
||||||
|
newBuff[bytePos + 1] = bitmap[bytePos + 2];
|
||||||
|
newBuff[bytePos + 2] = bitmap[bytePos + 1];
|
||||||
|
newBuff[bytePos + 3] = bitmap[bytePos];
|
||||||
|
}
|
||||||
|
return newBuff;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function toEaglerSkin(image: Buffer): Promise<Util.BoundedBuffer<typeof Constants.EAGLERCRAFT_SKIN_CUSTOM_LENGTH>> {
|
||||||
|
const meta = await sharp(image).metadata();
|
||||||
|
let sharpImage = sharp(image);
|
||||||
|
if (meta.height != 64) {
|
||||||
|
// assume 32 height skin
|
||||||
|
let imageOut = sharp(await sharpImage.extend({ bottom: 32, background: { r: 0, g: 0, b: 0, alpha: 0 } }).toBuffer());
|
||||||
|
|
||||||
|
imageOut = await copyRawPixels(sharpImage, imageOut, 24, 48, 20, 52, 4, 16, 8, 20);
|
||||||
|
imageOut = await copyRawPixels(sharpImage, imageOut, 28, 48, 24, 52, 8, 16, 12, 20);
|
||||||
|
imageOut = await copyRawPixels(sharpImage, imageOut, 20, 52, 16, 64, 8, 20, 12, 32);
|
||||||
|
imageOut = await copyRawPixels(sharpImage, imageOut, 24, 52, 20, 64, 4, 20, 8, 32);
|
||||||
|
imageOut = await copyRawPixels(sharpImage, imageOut, 28, 52, 24, 64, 0, 20, 4, 32);
|
||||||
|
imageOut = await copyRawPixels(sharpImage, imageOut, 32, 52, 28, 64, 12, 20, 16, 32);
|
||||||
|
imageOut = await copyRawPixels(sharpImage, imageOut, 40, 48, 36, 52, 44, 16, 48, 20);
|
||||||
|
imageOut = await copyRawPixels(sharpImage, imageOut, 44, 48, 40, 52, 48, 16, 52, 20);
|
||||||
|
imageOut = await copyRawPixels(sharpImage, imageOut, 36, 52, 32, 64, 48, 20, 52, 32);
|
||||||
|
imageOut = await copyRawPixels(sharpImage, imageOut, 40, 52, 36, 64, 44, 20, 48, 32);
|
||||||
|
imageOut = await copyRawPixels(sharpImage, imageOut, 44, 52, 40, 64, 40, 20, 44, 32);
|
||||||
|
imageOut = await copyRawPixels(sharpImage, imageOut, 48, 52, 44, 64, 52, 20, 56, 32);
|
||||||
|
|
||||||
|
sharpImage = imageOut;
|
||||||
|
}
|
||||||
|
|
||||||
|
const r = await sharpImage.extractChannel("red").raw({ depth: "uchar" }).toBuffer();
|
||||||
|
const g = await sharpImage.extractChannel("green").raw({ depth: "uchar" }).toBuffer();
|
||||||
|
const b = await sharpImage.extractChannel("blue").raw({ depth: "uchar" }).toBuffer();
|
||||||
|
const a = await sharpImage.ensureAlpha().extractChannel(3).toColorspace("b-w").raw({ depth: "uchar" }).toBuffer();
|
||||||
|
const newBuff = Buffer.alloc(Constants.EAGLERCRAFT_SKIN_CUSTOM_LENGTH);
|
||||||
|
for (let i = 1; i < 64 ** 2; i++) {
|
||||||
|
const bytePos = i * 4;
|
||||||
|
newBuff[bytePos] = a[i];
|
||||||
|
newBuff[bytePos + 1] = b[i];
|
||||||
|
newBuff[bytePos + 2] = g[i];
|
||||||
|
newBuff[bytePos + 3] = r[i];
|
||||||
|
}
|
||||||
|
return newBuff;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generateEaglerMOTDImage(file: string | Buffer): Promise<Buffer> {
|
||||||
|
return new Promise<Buffer>((res, rej) => {
|
||||||
|
sharp(file)
|
||||||
|
.resize(Constants.ICON_SQRT, Constants.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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generateEaglerMOTDImageJS(file: string | Buffer): Promise<Buffer> {
|
||||||
|
return new Promise<Buffer>(async (res, rej) => {
|
||||||
|
Jimp.read(typeof file == "string" ? await fs.readFile(file) : file, async (err, image) => {
|
||||||
|
image = image.resize(Constants.ICON_SQRT, Constants.ICON_SQRT, Jimp.RESIZE_NEAREST_NEIGHBOR);
|
||||||
|
res(image.bitmap.data);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user