Fixed skin bug

This commit is contained in:
q13x 2023-06-15 02:12:41 -07:00
parent a11e030e0d
commit 36d650d948
3 changed files with 613 additions and 259 deletions

View File

@ -1,13 +1,17 @@
# EaglerProxy # EaglerProxy
<a href="https://repl.it/github/WorldEditAxe/eaglerproxy"><img height="30px" src="https://raw.githubusercontent.com/FogNetwork/Tsunami/main/deploy/replit2.svg"><img></a> <a href="https://repl.it/github/WorldEditAxe/eaglerproxy"><img height="30px" src="https://raw.githubusercontent.com/FogNetwork/Tsunami/main/deploy/replit2.svg"><img></a>
A standalone reimplementation of EaglercraftX's bungee plugin written in TypeScript, with plugin support. A standalone reimplementation of EaglercraftX's bungee plugin written in TypeScript, with plugin support.
*Working for latest EaglercraftX client version as of `6/6/2023`* _Working for latest EaglercraftX client version as of `6/15/2023`_
## Known Issues ## Known Issues
* Skins may not render/display properly at all (only known to affect non-Eaglercraft players' skins).
* [EagProxyAAS] Player is missing skin when connected to server - [EagProxyAAS] Player is missing skin when connected to server
* Due to Eaglercraft's skin system and how it works, forcing skins onto the client is impossible (from what I know so far). This is only a client-sided bug/glitch - others will only see your Mojang/Minecraft account skin and cape. - Due to Eaglercraft's skin system and how it works, forcing skins onto the client is impossible (from what I know so far). This is only a client-sided bug/glitch - others will only see your Mojang/Minecraft account skin and cape.
## Installing and Running ## Installing and Running
This assumes that you have [Node.js](https://nodejs.org/en) LTS or higher installed on your computer, and that you have basic Git and CLI (command line) knowledge. This assumes that you have [Node.js](https://nodejs.org/en) LTS or higher installed on your computer, and that you have basic Git and CLI (command line) knowledge.
1. Clone/download this repository. 1. Clone/download this repository.
@ -15,55 +19,71 @@ This assumes that you have [Node.js](https://nodejs.org/en) LTS or higher instal
3. Install TypeScript and required dependencies (`npm i -g typescript` and `npm i`). 3. Install TypeScript and required dependencies (`npm i -g typescript` and `npm i`).
4. Compile the TypeScript code into normal JavaScript code (`tsc`). 4. Compile the TypeScript code into normal JavaScript code (`tsc`).
5. Go into the `build` directory, and run `node launcher.js`. 5. Go into the `build` directory, and run `node launcher.js`.
## Plugins ## Plugins
As of right now, there only exists one plugin: EagProxyAAS (read below for more information). As of right now, there only exists one plugin: EagProxyAAS (read below for more information).
### EagProxyAAS ### EagProxyAAS
EagProxyAAS aims to allow any Eaglercraft client to connect to a normal 1.8.9 Minecraft server (includes Hypixel), provided that players own a legitimate Minecraft Java copy. EagProxyAAS aims to allow any Eaglercraft client to connect to a normal 1.8.9 Minecraft server (includes Hypixel), provided that players own a legitimate Minecraft Java copy.
*Demo server: `wss://eaglerproxy.q13x.com/`* _Demo server: `wss://eaglerproxy.q13x.com/`_
#### I don't want to use this plugin! #### I don't want to use this plugin!
Remove all the folders in `src/plugins`. Remove all the folders in `src/plugins`.
#### IMPORTANT: READ ME BEFORE USING #### IMPORTANT: READ ME BEFORE USING
It is highly suggested that you use [Resent Client](https://reslauncher.vercel.app/) if you aren't already. It provides better performance, FPS, and will allow for a more stable connection to servers. It is highly suggested that you use [Resent Client](https://reslauncher.vercel.app/) if you aren't already. It provides better performance, FPS, and will allow for a more stable connection to servers.
**IMPORTANT:** Although both Resent Client and the vanilla Eaglercraft client are safe modified copies of Minecraft AOT-compiled to JavaScript, I cannot guarantee that you **will not get flagged by all anticheats.** While gameplay and testing has shown to be relatively stable and free of anticheat flags, more testing is needed to derive a conclusion on whether or not using EaglerProxy with EagProxyAAS is safe. **IMPORTANT:** Although both Resent Client and the vanilla Eaglercraft client are safe modified copies of Minecraft AOT-compiled to JavaScript, I cannot guarantee that you **will not get flagged by all anticheats.** While gameplay and testing has shown to be relatively stable and free of anticheat flags, more testing is needed to derive a conclusion on whether or not using EaglerProxy with EagProxyAAS is safe.
EaglerProxy and EagProxyAAS: EaglerProxy and EagProxyAAS:
* is compatible with EaglercraftX and uses its handshake system,
* intercepts and reads Minecraft packet traffic between you and the server on the other end (necessary for functionality), - is compatible with EaglercraftX and uses its handshake system,
* only uses login data to authenticate with vanilla Minecraft servers, - intercepts and reads Minecraft packet traffic between you and the server on the other end (necessary for functionality),
* and is open source and safe to use. - only uses login data to authenticate with vanilla Minecraft servers,
- and is open source and safe to use.
EaglerProxy and EagProxyAAS does NOT: EaglerProxy and EagProxyAAS does NOT:
* include any Microsoft/Mojang code,
* ship with offline (cracked) support by default, - include any Microsoft/Mojang code,
* store or otherwise use authentication data for any other purpose as listed on the README, - ship with offline (cracked) support by default,
* Unmodified versions will not maliciously handle your login data, although a modified version has the ability to do so. Only use trusted and unmodified versions of both this plugin and proxy. - store or otherwise use authentication data for any other purpose as listed on the README,
* and intentionally put your account at risk. - Unmodified versions will not maliciously handle your login data, although a modified version has the ability to do so. Only use trusted and unmodified versions of both this plugin and proxy.
- and intentionally put your account at risk.
Remember, open source software is never 100% safe. Read what you run on your computer. Remember, open source software is never 100% safe. Read what you run on your computer.
##### Expectations ##### Expectations
The built-in plugin serves as a demonstration of what can be done with plugins. Below is a list of what to expect from this demo. The built-in plugin serves as a demonstration of what can be done with plugins. Below is a list of what to expect from this demo.
* Expect server and world switching to take anywhere from 5 seconds to over 20.
* Not much can be done to resolve this issue. Issues related to this will likely be closed and marked as invalid. - Expect server and world switching to take anywhere from 5 seconds to over 20.
* It is important that you refrain from moving your mouse and typing on your keyboard during this period. Doing so will increase your chance of timing out, or being unexpectedly kicked with the "End of stream" error. - Not much can be done to resolve this issue. Issues related to this will likely be closed and marked as invalid.
* Expect the game to be unplayable (1-2 FPS at worst, maybe 30 FPS at best). - It is important that you refrain from moving your mouse and typing on your keyboard during this period. Doing so will increase your chance of timing out, or being unexpectedly kicked with the "End of stream" error.
* This is not something fixable on my behalf, as Eaglercraft itself has a history of being slow and laggy. Despite improvments made to the game in attempt to increase performance, Eaglercraft still remains slow and barely playable. - Expect the game to be unplayable (1-2 FPS at worst, maybe 30 FPS at best).
* Try turning down your video settings to off/the lowest setting allowed. Unfullscreening and making your browser window smaller may result in higher FPS. - This is not something fixable on my behalf, as Eaglercraft itself has a history of being slow and laggy. Despite improvments made to the game in attempt to increase performance, Eaglercraft still remains slow and barely playable.
* Expect to be flagged by anticheats. - Try turning down your video settings to off/the lowest setting allowed. Unfullscreening and making your browser window smaller may result in higher FPS.
* While testing has shown the proxy and plugin to be relatively safe to play on, it is not guaranteed that you will not get flagged and banned on every single server out there. - Expect to be flagged by anticheats.
* Tested servers: Hypixel - While testing has shown the proxy and plugin to be relatively safe to play on, it is not guaranteed that you will not get flagged and banned on every single server out there.
- Tested servers: Hypixel
### Plugin Development ### Plugin Development
### Disclaimer ### Disclaimer
The proxy's software utilizes its own plugin API written in JavaScript, rather than BungeeCord's plugin API. For this reason, plugins written for the official BungeeCord plugin will **not** work on this proxy. Below are some instructions for making your very own EaglerProxy plugin. The proxy's software utilizes its own plugin API written in JavaScript, rather than BungeeCord's plugin API. For this reason, plugins written for the official BungeeCord plugin will **not** work on this proxy. Below are some instructions for making your very own EaglerProxy plugin.
*Refer to `src/plugins/EagProxyAAS` for an example plugin.* _Refer to `src/plugins/EagProxyAAS` for an example plugin._
Each and every EaglerProxy plugin consists of two parts: Each and every EaglerProxy plugin consists of two parts:
* an entry point JavaScript file (this file is ran when the plugin is loaded) - an entry point JavaScript file (this file is ran when the plugin is loaded)
* a `metadata.json` metadata file - a `metadata.json` metadata file
Below is a breakdown of everything inside of `metadata.json`: Below is a breakdown of everything inside of `metadata.json`:
``` ```
{ {
"name": "Example Plugin", "name": "Example Plugin",
@ -75,10 +95,13 @@ Below is a breakdown of everything inside of `metadata.json`:
"load_after": ["otherPlugin"] "load_after": ["otherPlugin"]
} }
``` ```
As of right now, there exists no API reference. Please refer to the preinstalled plugin for details regarding API usage. As of right now, there exists no API reference. Please refer to the preinstalled plugin for details regarding API usage.
## Reporting Issues ## Reporting Issues
* Security-related bugs/issues: Directly contact me on Discord (check my profile).
* Non-security-related bugs/issues: Open a new issue, with the following: - Security-related bugs/issues: Directly contact me on Discord (check my profile).
* Bug description - Non-security-related bugs/issues: Open a new issue, with the following:
* Affected versions - Bug description
* Reproduction steps (optional if you can't find) - Affected versions
- Reproduction steps (optional if you can't find)

View File

@ -122,7 +122,15 @@ export function sendMessageLogin(client: Client, url: string, token: string) {
}, },
{ {
text: token, text: token,
color: 'gold' color: 'gold',
hoverEvent: {
action: "show_text",
value: Enums.ChatColor.GOLD + "Click me to copy to chat to copy from there!"
},
clickEvent: {
action: "suggest_command",
value: token
}
}, },
{ {
text: "." text: "."

View File

@ -1,236 +1,559 @@
import { Constants } from "./Constants.js" import { Constants } from "./Constants.js";
import { Enums } from "./Enums.js" import { Enums } from "./Enums.js";
import { MineProtocol } from "./Protocol.js" import { MineProtocol } from "./Protocol.js";
import { Util } from "./Util.js" import { Util } from "./Util.js";
import sharp from "sharp" import sharp from "sharp";
import { Proxy } from "./Proxy.js" import { Proxy } from "./Proxy.js";
import { Player } from "./Player.js" import { Player } from "./Player.js";
import { CSChannelMessagePacket } from "./packets/channel/CSChannelMessage.js" 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";
// TODO: convert all functions to use MineProtocol's UUID manipulation functions // TODO: convert all functions to use MineProtocol's UUID manipulation functions
export namespace EaglerSkins { export namespace EaglerSkins {
export type ClientFetchEaglerSkin = { export type ClientFetchEaglerSkin = {
id: Enums.EaglerSkinPacketId.CFetchSkinEaglerPlayerReq, id: Enums.EaglerSkinPacketId.CFetchSkinEaglerPlayerReq;
uuid: string uuid: string;
} };
export type ServerFetchSkinResultBuiltIn = { export type ServerFetchSkinResultBuiltIn = {
id: Enums.EaglerSkinPacketId.SFetchSkinBuiltInRes, id: Enums.EaglerSkinPacketId.SFetchSkinBuiltInRes;
uuid: string, uuid: string;
skinId: number skinId: number;
} };
export type ServerFetchSkinResultCustom = { export type ServerFetchSkinResultCustom = {
id: Enums.EaglerSkinPacketId.SFetchSkinRes, id: Enums.EaglerSkinPacketId.SFetchSkinRes;
uuid: string, uuid: string;
skin: Util.BoundedBuffer<typeof Constants.EAGLERCRAFT_SKIN_CUSTOM_LENGTH> skin: Util.BoundedBuffer<typeof Constants.EAGLERCRAFT_SKIN_CUSTOM_LENGTH>;
} };
export type ClientDownloadSkinRequest = { export type ClientDownloadSkinRequest = {
id: Enums.EaglerSkinPacketId.CFetchSkinReq, id: Enums.EaglerSkinPacketId.CFetchSkinReq;
uuid: string, uuid: string;
url: string url: string;
} };
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) {
rej(`Tried to fetch ${skinUrl}, got HTTP ${skin.status} instead!`) rej(`Tried to fetch ${skinUrl}, got HTTP ${skin.status} instead!`);
return return;
} else { } else {
res(Buffer.from(await skin.arrayBuffer())) res(Buffer.from(await skin.arrayBuffer()));
} }
}) });
} }
export function readClientDownloadSkinRequestPacket(message: Buffer): ClientDownloadSkinRequest { export function readClientDownloadSkinRequestPacket(
message: Buffer
): ClientDownloadSkinRequest {
const ret: ClientDownloadSkinRequest = { const ret: ClientDownloadSkinRequest = {
id: null, id: null,
uuid: null, uuid: null,
url: null url: null,
} };
const id = MineProtocol.readVarInt(message), const id = MineProtocol.readVarInt(message),
uuid = MineProtocol.readUUID(id.newBuffer), uuid = MineProtocol.readUUID(id.newBuffer),
url = MineProtocol.readString(uuid.newBuffer, 1) url = MineProtocol.readString(uuid.newBuffer, 1);
ret.id = id.value ret.id = id.value;
ret.uuid = uuid.value ret.uuid = uuid.value;
ret.url = url.value ret.url = url.value;
return ret return ret;
} }
export function writeClientDownloadSkinRequestPacket(uuid: string | Buffer, url: string): Buffer { export function writeClientDownloadSkinRequestPacket(
return Buffer.concat([ uuid: string | Buffer,
url: string
): Buffer {
return Buffer.concat(
[
[Enums.EaglerSkinPacketId.CFetchSkinReq], [Enums.EaglerSkinPacketId.CFetchSkinReq],
MineProtocol.writeUUID(uuid), MineProtocol.writeUUID(uuid),
[0x0], [0x0],
MineProtocol.writeString(url) MineProtocol.writeString(url),
].map(arr => arr instanceof Uint8Array ? arr : Buffer.from(arr))) ].map((arr) => (arr instanceof Uint8Array ? arr : Buffer.from(arr)))
);
} }
export function readServerFetchSkinResultBuiltInPacket(message: Buffer): ServerFetchSkinResultBuiltIn { export function readServerFetchSkinResultBuiltInPacket(
message: Buffer
): ServerFetchSkinResultBuiltIn {
const ret: ServerFetchSkinResultBuiltIn = { const ret: ServerFetchSkinResultBuiltIn = {
id: null, id: null,
uuid: null, uuid: null,
skinId: null skinId: null,
} };
const id = MineProtocol.readVarInt(message), const id = MineProtocol.readVarInt(message),
uuid = MineProtocol.readUUID(id.newBuffer), uuid = MineProtocol.readUUID(id.newBuffer),
skinId = MineProtocol.readVarInt(id.newBuffer.subarray(id.newBuffer.length)) skinId = MineProtocol.readVarInt(
ret.id = id.value id.newBuffer.subarray(id.newBuffer.length)
ret.uuid = uuid.value );
ret.skinId = skinId.value ret.id = id.value;
return this ret.uuid = uuid.value;
ret.skinId = skinId.value;
return this;
} }
export function writeServerFetchSkinResultBuiltInPacket(uuid: string | Buffer, skinId: number): Buffer { export function writeServerFetchSkinResultBuiltInPacket(
uuid = typeof uuid == 'string' ? Util.uuidStringToBuffer(uuid) : uuid uuid: string | Buffer,
console.log(1) skinId: number
): Buffer {
uuid = typeof uuid == "string" ? Util.uuidStringToBuffer(uuid) : uuid;
console.log(1);
return Buffer.concat([ return Buffer.concat([
Buffer.from([Enums.EaglerSkinPacketId.SFetchSkinBuiltInRes]), Buffer.from([Enums.EaglerSkinPacketId.SFetchSkinBuiltInRes]),
uuid as Buffer, uuid as Buffer,
Buffer.from([ Buffer.from([skinId >> 24, skinId >> 16, skinId >> 8, skinId & 0xff]),
skinId >> 24, ]);
skinId >> 16,
skinId >> 8,
skinId & 0xFF
])
])
} }
export function readServerFetchSkinResultCustomPacket(message: Buffer): ServerFetchSkinResultCustom { export function readServerFetchSkinResultCustomPacket(
message: Buffer
): ServerFetchSkinResultCustom {
const ret: ServerFetchSkinResultCustom = { const ret: ServerFetchSkinResultCustom = {
id: null, id: null,
uuid: null, uuid: null,
skin: null skin: null,
} };
const id = MineProtocol.readVarInt(message), const id = MineProtocol.readVarInt(message),
uuid = MineProtocol.readUUID(id.newBuffer), uuid = MineProtocol.readUUID(id.newBuffer),
skin = uuid.newBuffer.subarray(0, Constants.EAGLERCRAFT_SKIN_CUSTOM_LENGTH) skin = uuid.newBuffer.subarray(
ret.id = id.value 0,
ret.uuid = uuid.value Constants.EAGLERCRAFT_SKIN_CUSTOM_LENGTH
ret.skin = skin );
return this ret.id = id.value;
ret.uuid = uuid.value;
ret.skin = skin;
return this;
} }
// TODO: fix bug where some people are missing left arm and leg // 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 = typeof uuid == 'string' ? Util.uuidStringToBuffer(uuid) : uuid uuid: string | Buffer,
return Buffer.concat([ skin: Buffer,
downloaded: boolean
): Buffer {
uuid = typeof uuid == "string" ? Util.uuidStringToBuffer(uuid) : uuid;
return Buffer.concat(
[
[Enums.EaglerSkinPacketId.SFetchSkinRes], [Enums.EaglerSkinPacketId.SFetchSkinRes],
uuid, uuid,
!downloaded ? [0x01] : [0x01], // TODO: if buggy, use 0xff instead [-1], // TODO: if buggy, use 0xff instead
skin.subarray(0, Constants.EAGLERCRAFT_SKIN_CUSTOM_LENGTH) skin.subarray(0, Constants.EAGLERCRAFT_SKIN_CUSTOM_LENGTH),
].map(arr => arr instanceof Uint8Array ? arr : Buffer.from(arr))) ].map((arr) => (arr instanceof Uint8Array ? arr : Buffer.from(arr)))
);
} }
export function readClientFetchEaglerSkinPacket(buff: Buffer): ClientFetchEaglerSkin { export function readClientFetchEaglerSkinPacket(
buff: Buffer
): ClientFetchEaglerSkin {
const ret: ClientFetchEaglerSkin = { const ret: ClientFetchEaglerSkin = {
id: null, id: null,
uuid: null uuid: null,
} };
const id = MineProtocol.readVarInt(buff), const id = MineProtocol.readVarInt(buff),
uuid = MineProtocol.readUUID(id.newBuffer) uuid = MineProtocol.readUUID(id.newBuffer);
ret.id = id.value ret.id = id.value;
ret.uuid = uuid.value ret.uuid = uuid.value;
return ret return ret;
} }
export function writeClientFetchEaglerSkin(uuid: string | Buffer, url: string): Buffer { export function writeClientFetchEaglerSkin(
uuid = typeof uuid == 'string' ? Util.uuidStringToBuffer(uuid) : uuid uuid: string | Buffer,
return Buffer.concat([ url: string
): Buffer {
uuid = typeof uuid == "string" ? Util.uuidStringToBuffer(uuid) : uuid;
return Buffer.concat(
[
[Enums.EaglerSkinPacketId.CFetchSkinEaglerPlayerReq], [Enums.EaglerSkinPacketId.CFetchSkinEaglerPlayerReq],
uuid, uuid,
[0x00], [0x00],
MineProtocol.writeString(url) MineProtocol.writeString(url),
].map(arr => arr instanceof Uint8Array ? arr : Buffer.from(arr))) ].map((arr) => (arr instanceof Uint8Array ? arr : Buffer.from(arr)))
);
} }
export async function toEaglerSkin(image: Buffer): Promise<Util.BoundedBuffer<typeof Constants.EAGLERCRAFT_SKIN_CUSTOM_LENGTH>> { export async function copyRawPixels(
const r = await sharp(image).extractChannel('red').raw({ depth: 'uchar' }).toBuffer() imageIn: sharp.Sharp,
const g = await sharp(image).extractChannel('green').raw({ depth: 'uchar' }).toBuffer() imageOut: sharp.Sharp,
const b = await sharp(image).extractChannel('blue').raw({ depth: 'uchar' }).toBuffer() dx1: number,
const a = await sharp(image).ensureAlpha().extractChannel(3).toColorspace('b-w').raw({ depth: 'uchar' }).toBuffer() dy1: number,
const newBuff = Buffer.alloc(Constants.EAGLERCRAFT_SKIN_CUSTOM_LENGTH) dx2: number,
for (let i = 1; i < 64 ** 2; i++) { dy2: number,
const bytePos = i * 4 sx1: number,
newBuff[bytePos] = a[i] sy1: number,
newBuff[bytePos + 1] = b[i] sx2: number,
newBuff[bytePos + 2] = g[i] sy2: number
newBuff[bytePos + 3] = r[i] ): 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
);
} }
return newBuff }
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;
private _logger: Logger private _logger: Logger;
constructor(proxy: Proxy, allowedSkinDomains?: string[]) { constructor(proxy: Proxy, allowedSkinDomains?: string[]) {
this.allowedSkinDomains = allowedSkinDomains ?? ['textures.minecraft.net'] this.allowedSkinDomains = allowedSkinDomains ?? [
this.proxy = proxy ?? PROXY "textures.minecraft.net",
this._logger = new Logger("SkinServer") ];
this._logger.info("Started EaglercraftX skin server.") this.proxy = proxy ?? PROXY;
this._logger = new Logger("SkinServer");
this._logger.info("Started EaglercraftX skin server.");
} }
public async handleRequest(packet: CSChannelMessagePacket, caller: Player) { public async handleRequest(packet: CSChannelMessagePacket, caller: Player) {
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) else if (packet.channel != Constants.EAGLERCRAFT_SKIN_CHANNEL_NAME)
throw new Error("Cannot handle non-EaglerX skin channel messages!") throw new Error("Cannot handle non-EaglerX skin channel messages!");
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(packet.data) const parsedPacket_0 = EaglerSkins.readClientFetchEaglerSkinPacket(
const player = this.proxy.fetchUserByUUID(parsedPacket_0.uuid) packet.data
);
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 = EaglerSkins.writeServerFetchSkinResultBuiltInPacket(player.uuid, player.skin.builtInSkin) response.data =
caller.write(response) EaglerSkins.writeServerFetchSkinResultBuiltInPacket(
player.uuid,
player.skin.builtInSkin
);
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 = EaglerSkins.writeServerFetchSkinResultCustomPacket(player.uuid, player.skin.skin, false) response.data =
caller.write(response) EaglerSkins.writeServerFetchSkinResultCustomPacket(
} else this._logger.warn(`Player ${caller.username} attempted to fetch player ${player.uuid}'s skin, but their skin hasn't loaded yet!`) 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 break;
case Enums.EaglerSkinPacketId.CFetchSkinReq: case Enums.EaglerSkinPacketId.CFetchSkinReq:
const parsedPacket_1 = EaglerSkins.readClientDownloadSkinRequestPacket(packet.data), url = new URL(parsedPacket_1.url).hostname const parsedPacket_1 =
if (!this.allowedSkinDomains.some(domain => Util.areDomainsEqual(domain, url))) { EaglerSkins.readClientDownloadSkinRequestPacket(packet.data),
this._logger.warn(`Player ${caller.username} tried to download a skin with a disallowed domain name(${url})!`) url = new URL(parsedPacket_1.url).hostname;
break 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 { try {
const fetched = await EaglerSkins.downloadSkin(parsedPacket_1.url), const fetched = await EaglerSkins.downloadSkin(parsedPacket_1.url),
processed = await EaglerSkins.toEaglerSkin(fetched), processed = await EaglerSkins.toEaglerSkin(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(parsedPacket_1.uuid, processed, true) response.data = EaglerSkins.writeServerFetchSkinResultCustomPacket(
caller.write(response) parsedPacket_1.uuid,
processed,
true
);
caller.write(response);
} catch (err) { } catch (err) {
this._logger.warn(`Failed to fetch skin URL ${parsedPacket_1.url} for player ${caller.username}: ${err.stack ?? err}`) this._logger.warn(
`Failed to fetch skin URL ${parsedPacket_1.url} for player ${
caller.username
}: ${err.stack ?? err}`
);
} }
} }
} }
} }
export class EaglerSkin { export class EaglerSkin {
owner: Player owner: Player;
type: Enums.SkinType type: Enums.SkinType;
// update this over time // update this over time
builtInSkin?: Util.Range<0, 23> builtInSkin?: Util.Range<0, 23>;
skin?: Util.BoundedBuffer<typeof Constants.EAGLERCRAFT_SKIN_CUSTOM_LENGTH> skin?: Util.BoundedBuffer<typeof Constants.EAGLERCRAFT_SKIN_CUSTOM_LENGTH>;
} }
} }