mirror of
https://github.com/WorldEditAxe/eaglerproxy.git
synced 2024-11-14 09:36:04 -08:00
124 lines
6.4 KiB
JavaScript
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,
|
||
|
};
|
||
|
}
|