q13x-eaglerproxy/server/proxy/skins/SkinServer.js
2024-09-04 12:02:00 +00:00

124 lines
6.4 KiB
JavaScript

import DiskDB from "../databases/DiskDB.js";
import crypto from "crypto";
import { Logger } from "../../logger.js";
import { Constants } from "../Constants.js";
import { Enums } from "../Enums.js";
import { Util } from "../Util.js";
import { SCChannelMessagePacket } from "../packets/channel/SCChannelMessage.js";
import { EaglerSkins } from "./EaglerSkins.js";
import { ImageEditor } from "./ImageEditor.js";
import { MineProtocol } from "../Protocol.js";
import ExponentialBackoffRequestController from "../ratelimit/ExponentialBackoffRequestController.js";
export class SkinServer {
allowedSkinDomains;
cache;
proxy;
backoffController;
usingNative;
usingCache;
_logger;
deleteTask;
lifetime;
constructor(proxy, native, sweepInterval, cacheLifetime, cacheFolder = "./skinCache", useCache = true, allowedSkinDomains) {
this.allowedSkinDomains = allowedSkinDomains ?? ["textures.minecraft.net"];
if (useCache) {
this.cache = new DiskDB(cacheFolder, (v) => exportCachedSkin(v), (b) => readCachedSkin(b), (k) => k.replaceAll("-", ""));
}
this.proxy = proxy ?? PROXY;
this.usingCache = useCache;
this.usingNative = native;
this.lifetime = cacheLifetime;
this.backoffController = new ExponentialBackoffRequestController();
this._logger = new Logger("SkinServer");
this._logger.info("Started EaglercraftX skin server.");
if (useCache)
this.deleteTask = setInterval(async () => await this.cache.filter((ent) => Date.now() < ent.expires), sweepInterval);
}
unload() {
if (this.deleteTask != null)
clearInterval(this.deleteTask);
}
async handleRequest(packet, caller, proxy) {
if (packet.messageType == Enums.ChannelMessageType.SERVER)
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!");
{
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]) {
default:
throw new Error("Unknown operation!");
break;
case Enums.EaglerSkinPacketId.CFetchSkinEaglerPlayerReq:
const parsedPacket_0 = EaglerSkins.readClientFetchEaglerSkinPacket(packet.data);
const player = this.proxy.fetchUserByUUID(parsedPacket_0.uuid);
if (player) {
if (player.skin.type == Enums.SkinType.BUILTIN) {
const response = new SCChannelMessagePacket();
response.channel = Constants.EAGLERCRAFT_SKIN_CHANNEL_NAME;
response.data = EaglerSkins.writeServerFetchSkinResultBuiltInPacket(player.uuid, player.skin.builtInSkin);
caller.write(response);
}
else if (player.skin.type == Enums.SkinType.CUSTOM) {
const response = new SCChannelMessagePacket();
response.channel = Constants.EAGLERCRAFT_SKIN_CHANNEL_NAME;
response.data = EaglerSkins.writeServerFetchSkinResultCustomPacket(player.uuid, player.skin.skin, false);
caller.write(response);
}
else
this._logger.warn(`Player ${caller.username} attempted to fetch player ${player.uuid}'s skin, but their skin hasn't loaded yet!`);
}
break;
case Enums.EaglerSkinPacketId.CFetchSkinReq:
const parsedPacket_1 = EaglerSkins.readClientDownloadSkinRequestPacket(packet.data), url = new URL(parsedPacket_1.url).hostname;
if (!this.allowedSkinDomains.some((domain) => Util.areDomainsEqual(domain, url))) {
this._logger.warn(`Player ${caller.username} tried to download a skin with a disallowed domain name (${url})!`);
break;
}
try {
let cacheHit = null, skin = null;
if (this.usingCache) {
(cacheHit = await this.cache.get(parsedPacket_1.uuid)), (skin = cacheHit != null ? cacheHit.data : null);
if (!skin) {
const fetched = await EaglerSkins.safeDownloadSkin(parsedPacket_1.url, this.backoffController);
skin = fetched;
await this.cache.set(parsedPacket_1.uuid, {
uuid: parsedPacket_1.uuid,
expires: Date.now() + this.lifetime,
data: fetched,
});
}
}
else {
skin = await EaglerSkins.safeDownloadSkin(parsedPacket_1.url, this.backoffController);
}
const processed = this.usingNative ? await ImageEditor.toEaglerSkin(skin) : await ImageEditor.toEaglerSkinJS(skin), response = new SCChannelMessagePacket();
response.channel = Constants.EAGLERCRAFT_SKIN_CHANNEL_NAME;
response.data = EaglerSkins.writeServerFetchSkinResultCustomPacket(parsedPacket_1.uuid, processed, true);
caller.write(response);
}
catch (err) {
this._logger.warn(`Failed to fetch skin URL ${parsedPacket_1.url} for player ${caller.username}: ${err.stack ?? err}`);
}
}
}
}
function digestMd5Hex(data) {
return crypto.createHash("md5").update(data).digest("hex");
}
function exportCachedSkin(skin) {
const endUuid = MineProtocol.writeString(skin.uuid), encExp = MineProtocol.writeVarLong(skin.expires), encSkin = MineProtocol.writeBinary(skin.data);
return Buffer.concat([endUuid, encExp, encSkin]);
}
function readCachedSkin(data) {
const readUuid = MineProtocol.readString(data), readExp = MineProtocol.readVarLong(readUuid.newBuffer), readSkin = MineProtocol.readBinary(readExp.newBuffer);
return {
uuid: readUuid.value,
expires: readExp.value,
data: readSkin.value,
};
}