diff --git a/utils.ts b/utils.ts index 098653e..57cad98 100644 --- a/utils.ts +++ b/utils.ts @@ -1,25 +1,31 @@ -import { v3 } from "uuid" import WebSocket from "ws" -import { ProxiedPlayer } from "./classes.js" import { encodeULEB128 as encodeVarInt, decodeULEB128 as decodeVarInt, encodeSLEB128 as encodeSVarInt, decodeSLEB128 as decodeSVarInt } from "@thi.ng/leb128" -import { DisconnectReason, EaglerPacketId, MAGIC_ENDING_IDENTIFYS_BYTES } from "./eaglerPacketDef.js" +import { DisconnectReason, EAGLERCRAFT_SKIN_CHANNEL_NAME, EaglerPacketId, MAGIC_BUILTIN_SKIN_BYTES, MAGIC_ENDING_IDENTIFY_S_BYTES } from "./eaglerPacketDef.js" import { Logger } from "./logger.js" -import { State } from "./types.js" -import { toBuffer } from "uuid-buffer" +import { ChannelMessageType, Chat, ChatColor, ProxiedPlayer, State, UUID } from "./types.js" +import { toBuffer, toString as uuidToString } from "uuid-buffer" import * as mc from "minecraft-protocol" import { config } from "./config.js" +import sharp from "sharp" +import { createHash, randomUUID } from "crypto" +import { encodeSSkinDl, encodeSSkinDlBuiltin, packChannelMessage, processClientReqPacket } from "./eaglerSkin.js" -const MAGIC_UUID = "a7e774bc-7ea4-11ed-9a58-1f9e14304a59" const logger = new Logger("LoginHandler") const USERNAME_REGEX = /[^0-9^a-z^A-Z^_]/gi export function genUUID(user: string): string { - return v3(user, MAGIC_UUID) + const str = `OfflinePlayer:${user}` + let md5Bytes = createHash('md5').update(str).digest() + md5Bytes[6] &= 0x0f; /* clear version */ + md5Bytes[6] |= 0x30; /* set to version 3 */ + md5Bytes[8] &= 0x3f; /* clear variant */ + md5Bytes[8] |= 0x80; /* set to IETF variant */ + return uuidToString(md5Bytes) } export function bufferizeUUID(uuid: string): Buffer { @@ -33,17 +39,30 @@ export function validateUsername(user: string): void | never { throw new Error("Invalid username. Username can only contain alphanumeric characters, and the underscore (_) character.") } -export function disconnect(player: ProxiedPlayer, message: string, code?: DisconnectReason) { +export function chatToPlainString(chat: Chat): string { + let ret = '' + if (chat.text != null) ret += chat.text + if (chat.extra != null) { + chat.extra.forEach(extra => { + ret += extra.text + }) + } + return ret +} + +export function disconnect(player: ProxiedPlayer, message: Chat | string, code?: DisconnectReason) { if (player.state == State.POST_HANDSHAKE) { - const messageLen = encodeVarInt(message.length) - const d = Buffer.alloc(1 + messageLen.length + message.length) - d.set([0x40, ...messageLen, ...Buffer.from(message)]) + const message_m = (typeof message == 'string' ? JSON.stringify({ text: message }) : JSON.stringify(message)) + 0x0 + const messageLen = encodeVarInt(message_m.length) + const d = Buffer.alloc([0x40, ...messageLen, ...Buffer.from(message_m)].length) + d.set([0x40, ...messageLen, ...Buffer.from(message_m)]) player.ws.send(d) player.ws.close() } else { - const messageLen = encodeVarInt(message.length), codeEnc = encodeVarInt(code ?? DisconnectReason.CUSTOM) - const d = Buffer.alloc(1 + codeEnc.length + messageLen.length + message.length) - d.set([0xff,...codeEnc, ...messageLen, ...Buffer.from(message)]) + const message_m = (typeof message == 'string' ? message : chatToPlainString(message)) + const messageLen = encodeVarInt(message_m.length), codeEnc = encodeVarInt(code ?? DisconnectReason.CUSTOM) + const d = Buffer.alloc([0xff,...codeEnc, ...messageLen, ...Buffer.from(message_m)].length) + d.set([0xff,...codeEnc, ...messageLen, ...Buffer.from(message_m)]) player.ws.send(d) player.ws.close() } @@ -88,7 +107,7 @@ export function awaitPacket(ws: WebSocket, id?: EaglerPacketId): Promise export function loginServer(ip: string, port: number, client: ProxiedPlayer) { return new Promise((res, rej) => { - let receivedCompression = false + let blockedSuccessLogin = false const mcClient = mc.createClient({ host: ip, port: port, @@ -100,28 +119,20 @@ export function loginServer(ip: string, port: number, client: ProxiedPlayer) { mcClient.end() rej(err) }) - mcClient.on('end', () => { - client.ws.close() - }) mcClient.on('connect', () => { client.remoteConnection = mcClient + mcClient.on('end', () => { + client.ws.close() + }) logger.info(`Player ${client.username} has been connected to the server.`) res() }) mcClient.on('raw', p => { - if (p[0] == 0x03 && !receivedCompression) { - receivedCompression = true - const compT = { - id: null, - thres: null - } - const id = decodeVarInt(p) - compT.id = Number(id[0]) - const thres = decodeSVarInt(p.subarray(id[1])) - compT.thres = thres[0] - - client.compressionThreshold = compT.thres + // block the login success packet to fix the bug that prints the UUID in chat on join + if (p[0] == 0x02 && blockedSuccessLogin) { client.ws.send(p) + } else if (p[0] == 0x02) { + blockedSuccessLogin = !blockedSuccessLogin } else { client.ws.send(p) } @@ -136,11 +147,10 @@ export async function doHandshake(client: ProxiedPlayer, initialPacket: Buffer) client.remoteConnection.end() } PROXY.players.delete(client.username) - PROXY.playerStats.onlineCount -= 1 logger.info(`Client [/${client.ip}:${client.remotePort}]${client.username ? ` (${client.username})` : ""} disconnected from the server.`) }) - if (PROXY.players.size + 1 > PROXY.playerStats.max) { - disconnect(client, "The proxy is full!", DisconnectReason.CUSTOM) + if (PROXY.players.size + 1 > PROXY.config.maxPlayers) { + disconnect(client, ChatColor.YELLOW + "The proxy is full!", DisconnectReason.CUSTOM) return } const identifyC = { @@ -164,8 +174,8 @@ export async function doHandshake(client: ProxiedPlayer, initialPacket: Buffer) if (true) { const brandingLen = encodeVarInt(PROXY.brand.length), brand = PROXY.brand const verLen = encodeVarInt(PROXY.version.length), version = PROXY.version - const buff = Buffer.alloc(2 + MAGIC_ENDING_IDENTIFYS_BYTES.length + brandingLen.length + brand.length + verLen.length + version.length) - buff.set([EaglerPacketId.IDENTIFY_SERVER, 0x01, ...brandingLen, ...Buffer.from(brand), ...verLen, ...Buffer.from(version), ...Buffer.from(MAGIC_ENDING_IDENTIFYS_BYTES)]) + const buff = Buffer.alloc(2 + MAGIC_ENDING_IDENTIFY_S_BYTES.length + brandingLen.length + brand.length + verLen.length + version.length) + buff.set([EaglerPacketId.IDENTIFY_SERVER, 0x01, ...brandingLen, ...Buffer.from(brand), ...verLen, ...Buffer.from(version), ...Buffer.from(MAGIC_ENDING_IDENTIFY_S_BYTES)]) client.ws.send(buff) } client.clientBrand = identifyC.branding @@ -181,61 +191,189 @@ export async function doHandshake(client: ProxiedPlayer, initialPacket: Buffer) if (login[0] === EaglerPacketId.LOGIN) { const Iid = decodeVarInt(login) loginP.id = Number(Iid[0]) - const usernameLen = decodeVarInt(login.subarray(loginP[1])) + const usernameLen = decodeVarInt(login.subarray(Iid[1])) loginP.usernameLen = Number(usernameLen[0]) loginP.username = login.subarray(Iid[1] + usernameLen[1], Iid[1] + usernameLen[1] + loginP.usernameLen).toString() const randomStrLen = decodeVarInt(login.subarray(Iid[1] + usernameLen[1] + loginP.usernameLen)) loginP.randomStrLen = Number(randomStrLen[0]) loginP.randomStr = login.subarray(Iid[1] + usernameLen[1] + loginP.usernameLen + randomStrLen[1], Iid[1] + usernameLen[1] + loginP.usernameLen + randomStrLen[1] + loginP.randomStrLen).toString() + client.username = loginP.username client.uuid = genUUID(client.username) try { validateUsername(client.username) } catch (err) { - disconnect(client, err.message, DisconnectReason.BAD_USERNAME) + disconnect(client, ChatColor.RED + err.message, DisconnectReason.CUSTOM) return } if (PROXY.players.has(client.username)) { - disconnect(client, `Duplicate username: ${client.username}. Please connect under a different username.`, DisconnectReason.DUPLICATE_USERNAME) + disconnect(client, `Duplicate username: ${client.username}. Please connect under a different username.`, DisconnectReason.CUSTOM) return } PROXY.players.set(client.username, client) if (true) { const usernameLen = encodeVarInt(client.username.length), username = client.username - const uuidLen = encodeVarInt(client.uuid.length), uuid = client.uuid - const buff = Buffer.alloc(1 + usernameLen.length + username.length + uuidLen.length + uuid.length) - buff.set([EaglerPacketId.LOGIN_ACK, ...usernameLen, ...Buffer.from(username), ...uuidLen, ...Buffer.from(uuid)]) + const uuid = bufferizeUUID(client.uuid) + const buff = Buffer.alloc(1 + usernameLen.length + username.length + uuid.length) + buff.set([EaglerPacketId.LOGIN_ACK, ...usernameLen, ...Buffer.from(username), ...Buffer.from(uuid)]) client.ws.send(buff) - if (true) { const [skin, ready] = await Promise.all([awaitPacket(client.ws, EaglerPacketId.SKIN), awaitPacket(client.ws, EaglerPacketId.C_READY)]) if (ready[0] != 0x08) { logger.error(`Client [/${client.ip}:${client.remotePort}] sent an unexpected packet! Disconnecting.`) - disconnect(client, "Received bad packet", DisconnectReason.UNEXPECTED_PACKET) + disconnect(client, ChatColor.RED + "Received bad packet.", DisconnectReason.CUSTOM) client.ws.close() return } + if (true) { + const skinP = { + id: null, + skinVerLen: null, + skinVer: null, // skin_v1 + type: null, // CUSTOM or BUILTIN + skinId: null, + skinDimens: null, + skin: null + } + const Iid = decodeVarInt(skin) + skinP.id = Number(Iid[0]) + const skinVerLen = decodeVarInt(skin.subarray(Iid[1])) + skinP.skinVerLen = Number(skinVerLen[0]) + skinP.skinVer = skin.subarray(Iid[1] + skinVerLen[1], Iid[1] + skinVerLen[1] + skinP.skinVerLen).toString() + const typebuff = skin.subarray(Iid[1] + skinVerLen[1] + skinP.skinVerLen, Iid[1] + skinVerLen[1] + skinP.skinVerLen + MAGIC_BUILTIN_SKIN_BYTES.length) + if (typebuff.compare(Buffer.from(MAGIC_BUILTIN_SKIN_BYTES)) == 0) { + skinP.type = "BUILTIN" + skinP.skinId = Number(decodeVarInt(skin.subarray(Iid[1] + skinVerLen[1] + skinP.skinVerLen + MAGIC_BUILTIN_SKIN_BYTES.length))[0]) + } else { + skinP.type = "CUSTOM" + const skinSqrt = decodeVarInt(skin.subarray(Iid[1] + skinVerLen[1] + skinP.skinVerLen)), dimensions = Number(skinSqrt[0]) * Number(skinSqrt[0]) * 3 + skinP.skinDimens = dimensions + skinP.skin = skin.subarray(Iid[1] + skinVerLen[1] + skinP.skinVerLen + skinSqrt[1] + 16) + console.log(skinP.skin.length) + if (skinP.skin.length > 16385) { + disconnect(client, ChatColor.RED + "Invalid skin received!") + return + } + console.log(skinP.skin[skinP.skin.length - 1]) + } + client.skin = { + type: skinP.type, + skinId: skinP.skinId, + customSkin: skinP.skin + } + } const buff = Buffer.alloc(1) buff.set([EaglerPacketId.COMPLETE_HANDSHAKE]) client.ws.send(buff) client.state = State.POST_HANDSHAKE - PROXY.playerStats.onlineCount += 1 - logger.info(`Client [/${client.ip}:${client.remotePort}] authenticated as player "${client.username}" and passed handshake. Connecting!`) + logger.info(`Client [/${client.ip}:${client.remotePort}] authenticated as player ${client.username} (${client.uuid}) and passed handshake. Connecting!`) try { await loginServer(config.server.host, config.server.port, client) } catch (err) { logger.error(`Could not connect to remote server at [/${config.server.host}:${config.server.port}]: ${err}`) - disconnect(client, "Failed to connect to server. Please try again later.", DisconnectReason.CUSTOM) + disconnect(client, ChatColor.RED + "Failed to connect to server. Please try again later.", DisconnectReason.CUSTOM) client.state = State.DISCONNECTED client.ws.close() return } + + if (client.queuedEaglerSkinPackets.length > 0) { + for (const packet of client.queuedEaglerSkinPackets) { + processClientReqPacket(packet, client) + } + } } } } else { logger.error(`Client [/${client.ip}:${client.remotePort}] sent an unexpected packet! Disconnecting.`) - disconnect(client, "Received bad packet", DisconnectReason.UNEXPECTED_PACKET) + disconnect(client, ChatColor.RED + "Received bad packet", DisconnectReason.CUSTOM) client.ws.close() } +} + +export type MotdPlayer = { + name: string, + id: UUID +} + +export type MotdJSONRes = { + brand: string, + cracked: true, + data: { + cache: true, + icon: boolean, + max: number, + motd: [string, string], + online: number, + players: string[], + }, + name: string, + secure: false, + time: ReturnType, + type: "motd", + uuid: ReturnType, + vers: string +} + +// a 16384 byte array +export type MotdServerLogo = Buffer + +const ICON_SQRT = 64 + +export function generateMOTDImage(file: Buffer): Promise { + return new Promise((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) + }) +} + +export function handleMotd(player: Partial) { + const names = [] + for (const [username, player] of PROXY.players) { + if (names.length > 0) { + names.push(`${ChatColor.GRAY}${ChatColor.ITALIC}(and ${PROXY.players.size - names.length} more)`) + break + } else { + names.push(username) + } + } + + player.ws.send(JSON.stringify({ + brand: PROXY.brand, + cracked: true, + data: { + cache: true, + icon: PROXY.MOTD.icon ? true : false, + max: PROXY.config.maxPlayers, + motd: PROXY.MOTD.motd, + online: PROXY.players.size, + players: names + }, + name: PROXY.serverName, + secure: false, + time: Date.now(), + type: "motd", + uuid: PROXY.proxyUUID, + vers: PROXY.MOTDVersion + } as MotdJSONRes)) + if (PROXY.MOTD.icon) { + player.ws.send(PROXY.MOTD.icon) + } + player.ws.close() } \ No newline at end of file