From a5eac20833cee47fb7aa3c051997603157810d69 Mon Sep 17 00:00:00 2001 From: ayunami2000 Date: Mon, 2 Oct 2023 14:48:42 -0400 Subject: [PATCH] Add nearly full Eaglercraft server support TODO: finish making Eaglercraft 1.5 skins show up on Eaglercraft 1.8 --- build.gradle | 2 +- .../ConnectionHandshake.java | 357 ++++++++++++++++++ .../EaglerServerHandler.java | 291 ++++++++++++++ .../EaglerSkinHandler.java | 10 +- .../EaglerVoiceHandler.java | 4 + .../EaglerXSkinHandler.java | 22 +- .../EaglercraftHandler.java | 36 +- .../EaglercraftInitialHandler.java | 68 +++- .../ayunViaProxyEagUtils/FunnyConfig.java | 25 +- .../ayunViaProxyEagUtils/GeneralDigest.java | 108 ++++++ .../HandshakePacketTypes.java | 63 ++++ .../ayunViaProxyEagUtils/Main.java | 129 ++++++- .../ayunViaProxyEagUtils/MsgPromise.java | 13 + .../ayunViaProxyEagUtils/SHA256Digest.java | 233 ++++++++++++ .../ayunViaProxyEagUtils/SkinPackets.java | 2 +- .../ayunViaProxyEagUtils/SkinService.java | 11 - .../WebSocketConnectedNotifier.java | 61 +++ src/main/resources/eaglerskins.yml | 2 - src/main/resources/vpeagutils.yml | 8 + 19 files changed, 1389 insertions(+), 56 deletions(-) create mode 100644 src/main/java/me/ayunami2000/ayunViaProxyEagUtils/ConnectionHandshake.java create mode 100644 src/main/java/me/ayunami2000/ayunViaProxyEagUtils/EaglerServerHandler.java create mode 100644 src/main/java/me/ayunami2000/ayunViaProxyEagUtils/GeneralDigest.java create mode 100644 src/main/java/me/ayunami2000/ayunViaProxyEagUtils/HandshakePacketTypes.java create mode 100644 src/main/java/me/ayunami2000/ayunViaProxyEagUtils/MsgPromise.java create mode 100644 src/main/java/me/ayunami2000/ayunViaProxyEagUtils/SHA256Digest.java create mode 100644 src/main/java/me/ayunami2000/ayunViaProxyEagUtils/WebSocketConnectedNotifier.java delete mode 100644 src/main/resources/eaglerskins.yml create mode 100644 src/main/resources/vpeagutils.yml diff --git a/build.gradle b/build.gradle index 4cc11ff..8de1a72 100644 --- a/build.gradle +++ b/build.gradle @@ -10,5 +10,5 @@ repositories { } dependencies { - implementation files("libs/ViaProxy-3.0.21-SNAPSHOT+java8_PATCHED.jar") + implementation files("libs/ViaProxy-3.0.22-SNAPSHOT+java8.jar") } \ No newline at end of file diff --git a/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/ConnectionHandshake.java b/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/ConnectionHandshake.java new file mode 100644 index 0000000..060f5a4 --- /dev/null +++ b/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/ConnectionHandshake.java @@ -0,0 +1,357 @@ +package me.ayunami2000.ayunViaProxyEagUtils; + +import com.google.gson.JsonObject; +import com.mojang.authlib.GameProfile; +import com.viaversion.viaversion.util.ChatColorUtil; +import io.netty.channel.Channel; +import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame; +import io.netty.util.AttributeKey; +import net.lenni0451.mcstructs.text.serializer.TextComponentSerializer; +import net.raphimc.viaproxy.ViaProxy; +import net.raphimc.viaproxy.proxy.session.ProxyConnection; +import net.raphimc.viaproxy.util.logging.Logger; + +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.UUID; + +/** + * Copyright (c) 2022-2023 LAX1DUDE. All Rights Reserved. + * + * WITH THE EXCEPTION OF PATCH FILES, MINIFIED JAVASCRIPT, AND ALL FILES + * NORMALLY FOUND IN AN UNMODIFIED MINECRAFT RESOURCE PACK, YOU ARE NOT ALLOWED + * TO SHARE, DISTRIBUTE, OR REPURPOSE ANY FILE USED BY OR PRODUCED BY THE + * SOFTWARE IN THIS REPOSITORY WITHOUT PRIOR PERMISSION FROM THE PROJECT AUTHOR. + * + * NOT FOR COMMERCIAL OR MALICIOUS USE + * + * (please read the 'LICENSE' file this repo's root directory for more info) + * + */ +public class ConnectionHandshake { + private static final AttributeKey serverVersKey = AttributeKey.newInstance("eag-server-vers"); + private static final int protocolV2 = 2; + private static final int protocolV3 = 3; + + public static void attemptHandshake(List out, Channel ch, ProxyConnection proxyConnection, String password) { + try { + ByteArrayOutputStream bao = new ByteArrayOutputStream(); + DataOutputStream d = new DataOutputStream(bao); + + d.writeByte(HandshakePacketTypes.PROTOCOL_CLIENT_VERSION); + + d.writeByte(2); // legacy protocol version + + d.writeShort(2); // supported eagler protocols count + d.writeShort(protocolV2); // client supports v2 + d.writeShort(protocolV3); // client supports v3 + + d.writeShort(1); // supported game protocols count + d.writeShort(proxyConnection.getServerVersion().getVersion()); // client supports this protocol + + String clientBrand = "ViaProxy"; + d.writeByte(clientBrand.length()); + d.writeBytes(clientBrand); + + String clientVers = ViaProxy.VERSION; + d.writeByte(clientVers.length()); + d.writeBytes(clientVers); + + d.writeBoolean(password != null); + + String username = proxyConnection.getGameProfile().getName(); + d.writeByte(username.length()); + d.writeBytes(username); + + out.add(new BinaryWebSocketFrame(ch.alloc().buffer(bao.size()).writeBytes(bao.toByteArray()))); + } catch (Throwable t) { + Logger.LOGGER.error("Exception in handshake"); + Logger.LOGGER.error(t); + } + } + + public static void attemptHandshake2(Channel ch, byte[] read, ProxyConnection proxyConnection, String password) { + try { + ByteArrayOutputStream bao = new ByteArrayOutputStream(); + DataOutputStream d = new DataOutputStream(bao); + + String username = proxyConnection.getGameProfile().getName(); + + DataInputStream di = new DataInputStream(new ByteArrayInputStream(read)); + + int type = di.read(); + if (type == HandshakePacketTypes.PROTOCOL_VERSION_MISMATCH) { + + StringBuilder protocols = new StringBuilder(); + int c = di.readShort(); + for (int i = 0; i < c; ++i) { + if (i > 0) { + protocols.append(", "); + } + protocols.append("v").append(di.readShort()); + } + + StringBuilder games = new StringBuilder(); + c = di.readShort(); + for (int i = 0; i < c; ++i) { + if (i > 0) { + games.append(", "); + } + games.append("mc").append(di.readShort()); + } + + Logger.LOGGER.info("Incompatible client: v2 & mc" + proxyConnection.getServerVersion().getVersion()); + Logger.LOGGER.info("Server supports: {}", protocols); + Logger.LOGGER.info("Server supports: {}", games); + + int msgLen = di.read(); + byte[] dat = new byte[msgLen]; + di.read(dat); + String msg = new String(dat, StandardCharsets.UTF_8); + + proxyConnection.kickClient(msg); + } else if (type == HandshakePacketTypes.PROTOCOL_SERVER_VERSION) { + int serverVers = di.readShort(); + + if (serverVers != protocolV2 && serverVers != protocolV3) { + Logger.LOGGER.info("Incompatible server version: {}", serverVers); + proxyConnection.kickClient(serverVers < protocolV2 ? "Outdated Server" : "Outdated Client"); + return; + } + + ch.attr(serverVersKey).set(serverVers); + + int gameVers = di.readShort(); + if (gameVers != proxyConnection.getServerVersion().getVersion()) { + Logger.LOGGER.info("Incompatible minecraft protocol version: {}", gameVers); + proxyConnection.kickClient("This server does not support " + proxyConnection.getServerVersion().getName() + "!"); + return; + } + + Logger.LOGGER.info("Server protocol: {}", serverVers); + + int msgLen = di.read(); + byte[] dat = new byte[msgLen]; + di.read(dat); + String brandStr = asciiString(dat); + + msgLen = di.read(); + dat = new byte[msgLen]; + di.read(dat); + String versionStr = asciiString(dat); + + Logger.LOGGER.info("Server version: {}", versionStr); + Logger.LOGGER.info("Server brand: {}", brandStr); + + int authType = di.read(); + int saltLength = (int) di.readShort() & 0xFFFF; + + byte[] salt = new byte[saltLength]; + di.read(salt); + + bao.reset(); + d.writeByte(HandshakePacketTypes.PROTOCOL_CLIENT_REQUEST_LOGIN); + + d.writeByte(username.length()); + d.writeBytes(username); + + String requestedServer = "default"; + d.writeByte(requestedServer.length()); + d.writeBytes(requestedServer); + + if (authType != 0 && password != null && !password.isEmpty()) { + if (authType == HandshakePacketTypes.AUTH_METHOD_PLAINTEXT) { + Logger.LOGGER.warn("Server is using insecure plaintext authentication"); + d.writeByte(password.length() << 1); + d.writeChars(password); + } else if (authType == HandshakePacketTypes.AUTH_METHOD_EAGLER_SHA256) { + SHA256Digest digest = new SHA256Digest(); + + int passLen = password.length(); + + digest.update((byte) ((passLen >> 8) & 0xFF)); + digest.update((byte) (passLen & 0xFF)); + + for (int i = 0; i < passLen; ++i) { + char codePoint = password.charAt(i); + digest.update((byte) ((codePoint >> 8) & 0xFF)); + digest.update((byte) (codePoint & 0xFF)); + } + + digest.update(HandshakePacketTypes.EAGLER_SHA256_SALT_SAVE, 0, 32); + + byte[] hashed = new byte[32]; + digest.doFinal(hashed, 0); + + digest.reset(); + + digest.update(hashed, 0, 32); + digest.update(salt, 0, 32); + digest.update(HandshakePacketTypes.EAGLER_SHA256_SALT_BASE, 0, 32); + + digest.doFinal(hashed, 0); + + digest.reset(); + + digest.update(hashed, 0, 32); + digest.update(salt, 32, 32); + digest.update(HandshakePacketTypes.EAGLER_SHA256_SALT_BASE, 0, 32); + + digest.doFinal(hashed, 0); + + d.writeByte(32); + d.write(hashed); + } else if (authType == HandshakePacketTypes.AUTH_METHOD_AUTHME_SHA256) { + SHA256Digest digest = new SHA256Digest(); + + byte[] passwd = password.getBytes(StandardCharsets.UTF_8); + digest.update(passwd, 0, passwd.length); + + byte[] hashed = new byte[32]; + digest.doFinal(hashed, 0); + + byte[] toHexAndSalt = new byte[64]; + for (int i = 0; i < 32; ++i) { + toHexAndSalt[i << 1] = HEX[(hashed[i] >> 4) & 0xF]; + toHexAndSalt[(i << 1) + 1] = HEX[hashed[i] & 0xF]; + } + + digest.reset(); + digest.update(toHexAndSalt, 0, 64); + digest.update(salt, 0, salt.length); + + digest.doFinal(hashed, 0); + + for (int i = 0; i < 32; ++i) { + toHexAndSalt[i << 1] = HEX[(hashed[i] >> 4) & 0xF]; + toHexAndSalt[(i << 1) + 1] = HEX[hashed[i] & 0xF]; + } + + d.writeByte(64); + d.write(toHexAndSalt); + } else { + Logger.LOGGER.error("Unsupported authentication type: {}", authType); + proxyConnection.kickClient(ChatColorUtil.COLOR_CHAR + "cUnsupported authentication type: " + authType + "\n\n" + ChatColorUtil.COLOR_CHAR + "7(Use a newer version of the client)"); + return; + } + } else { + d.writeByte(0); + } + + ch.writeAndFlush(new BinaryWebSocketFrame(ch.alloc().buffer(bao.size()).writeBytes(bao.toByteArray()))); + } else if (type == HandshakePacketTypes.PROTOCOL_SERVER_ERROR) { + showError(proxyConnection, di, true); + } + } catch (Throwable t) { + Logger.LOGGER.error("Exception in handshake"); + Logger.LOGGER.error(t); + } + } + + public static void attemptHandshake3(Channel ch, byte[] read, ProxyConnection proxyConnection) { + try { + int serverVers = ch.attr(serverVersKey).get(); + + ByteArrayOutputStream bao = new ByteArrayOutputStream(); + DataOutputStream d = new DataOutputStream(bao); + + int msgLen; + byte[] dat; + + DataInputStream di = new DataInputStream(new ByteArrayInputStream(read)); + int type = di.read(); + if (type == HandshakePacketTypes.PROTOCOL_SERVER_ALLOW_LOGIN) { + msgLen = di.read(); + dat = new byte[msgLen]; + di.read(dat); + + String serverUsername = asciiString(dat); + + JsonObject json = new JsonObject(); + json.addProperty("name", serverUsername); + UUID uuid = new UUID(di.readLong(), di.readLong()); + json.addProperty("uuid", uuid.toString()); + proxyConnection.setGameProfile(new GameProfile(uuid, serverUsername)); + + if (proxyConnection.getC2P().hasAttr(EaglercraftHandler.profileDataKey)) { + EaglercraftHandler.ProfileData profileData = proxyConnection.getC2P().attr(EaglercraftHandler.profileDataKey).get(); + bao.reset(); + d.writeByte(HandshakePacketTypes.PROTOCOL_CLIENT_PROFILE_DATA); + d.writeByte(profileData.type.length()); + d.writeBytes(profileData.type); + d.writeShort(profileData.data.length); + d.write(profileData.data); + ch.writeAndFlush(new BinaryWebSocketFrame(ch.alloc().buffer(bao.size()).writeBytes(bao.toByteArray()))); + } + + bao.reset(); + d.writeByte(HandshakePacketTypes.PROTOCOL_CLIENT_FINISH_LOGIN); + ch.writeAndFlush(new BinaryWebSocketFrame(ch.alloc().buffer(bao.size()).writeBytes(bao.toByteArray()))); + } else if (type == HandshakePacketTypes.PROTOCOL_SERVER_DENY_LOGIN) { + if (serverVers == protocolV2) { + msgLen = di.read(); + } else { + msgLen = di.readUnsignedShort(); + } + dat = new byte[msgLen]; + di.read(dat); + String errStr = new String(dat, StandardCharsets.UTF_8); + proxyConnection.kickClient(TextComponentSerializer.V1_8.deserialize(errStr).asLegacyFormatString()); + } else if (type == HandshakePacketTypes.PROTOCOL_SERVER_ERROR) { + showError(proxyConnection, di, serverVers == protocolV2); + } + } catch (Throwable t) { + Logger.LOGGER.error("Exception in handshake"); + Logger.LOGGER.error(t); + } + } + + public static void attemptHandshake4(Channel ch, byte[] read, ProxyConnection proxyConnection) { + try { + int serverVers = ch.attr(serverVersKey).get(); + + DataInputStream di = new DataInputStream(new ByteArrayInputStream(read)); + int type = di.read(); + if (type == HandshakePacketTypes.PROTOCOL_SERVER_ERROR) { + showError(proxyConnection, di, serverVers == protocolV2); + } + } catch (Throwable t) { + Logger.LOGGER.error("Exception in handshake"); + Logger.LOGGER.error(t); + } + } + + private static void showError(ProxyConnection proxyConnection, DataInputStream err, boolean v2) throws IOException { + int errorCode = err.read(); + int msgLen = v2 ? err.read() : err.readUnsignedShort(); + byte[] dat = new byte[msgLen]; + err.read(dat); + String errStr = new String(dat, StandardCharsets.UTF_8); + Logger.LOGGER.info("Server Error Code {}: {}", errorCode, errStr); + if(errorCode == HandshakePacketTypes.SERVER_ERROR_RATELIMIT_BLOCKED) { + proxyConnection.kickClient("Server Error Ratelimited (blocked)"); + }else if(errorCode == HandshakePacketTypes.SERVER_ERROR_RATELIMIT_LOCKED) { + proxyConnection.kickClient("Server Error Ratelimited (locked)"); + }else if(errorCode == HandshakePacketTypes.SERVER_ERROR_CUSTOM_MESSAGE) { + proxyConnection.kickClient("Server Error Message " + TextComponentSerializer.V1_8.deserialize(errStr).asLegacyFormatString()); + }else if(errorCode == HandshakePacketTypes.SERVER_ERROR_AUTHENTICATION_REQUIRED) { + proxyConnection.kickClient("Server Error Authentication required " + TextComponentSerializer.V1_8.deserialize(errStr).asLegacyFormatString()); + }else { + proxyConnection.kickClient("Server Error Code " + errorCode + "\n" + TextComponentSerializer.V1_8.deserialize(errStr).asLegacyFormatString()); + } + } + + private static final byte[] HEX = new byte[]{ + (byte) '0', (byte) '1', (byte) '2', (byte) '3', (byte) '4', (byte) '5', (byte) '6', (byte) '7', + (byte) '8', (byte) '9', (byte) 'a', (byte) 'b', (byte) 'c', (byte) 'd', (byte) 'e', (byte) 'f' + }; + + public static String asciiString(byte[] bytes) { + char[] str = new char[bytes.length]; + for(int i = 0; i < bytes.length; ++i) { + str[i] = (char)((int) bytes[i] & 0xFF); + } + return new String(str); + } +} diff --git a/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/EaglerServerHandler.java b/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/EaglerServerHandler.java new file mode 100644 index 0000000..1dbbd0e --- /dev/null +++ b/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/EaglerServerHandler.java @@ -0,0 +1,291 @@ +package me.ayunami2000.ayunViaProxyEagUtils; + +import com.google.common.primitives.Ints; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; +import io.netty.buffer.Unpooled; +import io.netty.channel.*; +import io.netty.handler.codec.MessageToMessageCodec; +import io.netty.handler.codec.http.websocketx.*; +import net.raphimc.netminecraft.constants.MCPackets; +import net.raphimc.netminecraft.netty.connection.NetClient; +import net.raphimc.netminecraft.packet.PacketTypes; +import net.raphimc.vialegacy.protocols.release.protocol1_6_1to1_5_2.ClientboundPackets1_5_2; +import net.raphimc.vialegacy.protocols.release.protocol1_6_1to1_5_2.ServerboundPackets1_5_2; +import net.raphimc.vialegacy.protocols.release.protocol1_7_2_5to1_6_4.types.Types1_6_4; +import net.raphimc.vialoader.util.VersionEnum; +import net.raphimc.viaproxy.proxy.session.ProxyConnection; +import net.raphimc.viaproxy.proxy.util.ExceptionUtil; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.*; + +public class EaglerServerHandler extends MessageToMessageCodec { + private final VersionEnum version; + private final String password; + private final NetClient proxyConnection; + private final Map uuidStringMap = new HashMap<>(); + private final List skinsBeingFetched = new ArrayList<>(); + private ByteBuf serverBoundPartialPacket = Unpooled.EMPTY_BUFFER; + private ByteBuf clientBoundPartialPacket = Unpooled.EMPTY_BUFFER; + public EaglerServerHandler(NetClient proxyConnection, String password) { + this.version = proxyConnection instanceof ProxyConnection ? ((ProxyConnection) proxyConnection).getServerVersion() : VersionEnum.r1_5_2; + this.password = password; + this.proxyConnection = proxyConnection; + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + ExceptionUtil.handleNettyException(ctx, cause, null); + } + + private int handshakeState = 0; + + @Override + public void encode(ChannelHandlerContext ctx, ByteBuf in, List out) { + if (version.isNewerThan(VersionEnum.r1_6_4)) { + if (handshakeState == 0) { + handshakeState = 1; + if (((ProxyConnection) proxyConnection).getGameProfile() == null) { + out.add(Unpooled.EMPTY_BUFFER); + ctx.close(); + return; + } + ConnectionHandshake.attemptHandshake(out, ctx.channel(), (ProxyConnection) proxyConnection, password); + if (out.isEmpty()) { + out.add(Unpooled.EMPTY_BUFFER); + } + } else if (handshakeState < 4) { + out.add(Unpooled.EMPTY_BUFFER); + } else { + out.add(new BinaryWebSocketFrame(in.retain())); + } + } else { + ByteBuf bb = ctx.alloc().buffer(serverBoundPartialPacket.readableBytes() + in.readableBytes()); + bb.writeBytes(serverBoundPartialPacket); + serverBoundPartialPacket.release(); + serverBoundPartialPacket = Unpooled.EMPTY_BUFFER; + bb.writeBytes(in); + int readerIndex = 0; + try { + while (bb.isReadable()) { + readerIndex = bb.readerIndex(); + ServerboundPackets1_5_2 pkt = ServerboundPackets1_5_2.getPacket(bb.readUnsignedByte()); + pkt.getPacketReader().accept(null, bb); + int len = bb.readerIndex() - readerIndex; + ByteBuf packet = ctx.alloc().buffer(len); + bb.readerIndex(readerIndex); + bb.readBytes(packet, len); + encodeOld(ctx, packet, out); + } + } catch (Exception e) { + bb.readerIndex(readerIndex); + if (bb.readableBytes() > 65535) { + ctx.close(); + out.add(Unpooled.EMPTY_BUFFER); + return; + } + serverBoundPartialPacket = ctx.alloc().buffer(bb.readableBytes()); + serverBoundPartialPacket.writeBytes(bb); + } + } + if (out.isEmpty()) { + out.add(Unpooled.EMPTY_BUFFER); + } + } + + public void encodeOld(ChannelHandlerContext ctx, ByteBuf in, List out) { + if (handshakeState == 0) { + handshakeState = 1; + if (in.readableBytes() >= 2 && in.getUnsignedByte(0) == 2) { + in.setByte(1, in.getUnsignedByte(1) + 8); + } + } + if (in.readableBytes() >= 1 && in.getUnsignedByte(0) == 0xFD) { + return; + } + if (in.readableBytes() >= 3 && in.getUnsignedByte(0) == 250) { + in.skipBytes(1); + String tag; + byte[] msg; + try { + tag = Types1_6_4.STRING.read(in); + if (tag.equals("EAG|Skins-1.8")) { + msg = new byte[in.readShort()]; + in.readBytes(msg); + if (msg.length == 0) { + throw new IOException("Zero-length packet recieved"); + } + final int packetId = msg[0] & 0xFF; + switch (packetId) { + case 3: { + if (msg.length != 17) { + throw new IOException("Invalid length " + msg.length + " for skin request packet"); + } + final UUID searchUUID = SkinPackets.bytesToUUID(msg, 1); + if (uuidStringMap.containsKey(searchUUID)) { + // skinsBeingFetched.add(searchUUID); + String name = uuidStringMap.get(searchUUID); + ByteBuf bb = ctx.alloc().buffer(); + bb.writeByte((byte) 250); + Types1_6_4.STRING.write(bb, "EAG|FetchSkin"); // todo: get to work + bb.writeByte((byte) 0); + bb.writeByte((byte) 0); + bb.writeBytes(name.getBytes(StandardCharsets.UTF_8)); + out.add(new BinaryWebSocketFrame(bb)); + } + break; + } + case 6: { + break; + } + default: { + throw new IOException("Unknown packet type " + packetId); + } + } + return; + } + } catch (Exception ignored) { + } + in.resetReaderIndex(); + } + out.add(new BinaryWebSocketFrame(in.retain())); + } + + @Override + public void decode(ChannelHandlerContext ctx, BinaryWebSocketFrame in, List out) { + if (version.isNewerThan(VersionEnum.r1_6_4)) { + if (handshakeState == 0) { + out.add(Unpooled.EMPTY_BUFFER); + } else if (handshakeState == 1) { + handshakeState = 2; + ConnectionHandshake.attemptHandshake2(ctx.channel(), ByteBufUtil.getBytes(in.content()), (ProxyConnection) proxyConnection, password); + out.add(Unpooled.EMPTY_BUFFER); + } else if (handshakeState == 2) { + handshakeState = 3; + ConnectionHandshake.attemptHandshake3(ctx.channel(), ByteBufUtil.getBytes(in.content()), (ProxyConnection) proxyConnection); + ByteBuf bb = ctx.alloc().buffer(); + PacketTypes.writeVarInt(bb, MCPackets.S2C_LOGIN_SUCCESS.getId(version.getVersion())); + PacketTypes.writeString(bb, ((ProxyConnection) proxyConnection).getGameProfile().getId().toString()); + PacketTypes.writeString(bb, ((ProxyConnection) proxyConnection).getGameProfile().getName()); + out.add(bb); + } else if (handshakeState == 3) { + handshakeState = 4; + ConnectionHandshake.attemptHandshake4(ctx.channel(), ByteBufUtil.getBytes(in.content()), (ProxyConnection) proxyConnection); + out.add(Unpooled.EMPTY_BUFFER); + } else { + if (in.content().getByte(0) == MCPackets.S2C_LOGIN_SUCCESS.getId(version.getVersion()) && in.content().getByte(1) == 0 && in.content().getByte(2) == 2) { + out.add(Unpooled.EMPTY_BUFFER); + return; + } + if (in.content().getByte(0) == 0) { + in.content().skipBytes(1); + PacketTypes.readVarInt(in.content()); + if (in.content().readableBytes() > 0) { + in.content().setByte(0, 0x40); + } + in.content().resetReaderIndex(); + } + out.add(in.content().retain()); + } + } else { + ByteBuf bb = ctx.alloc().buffer(clientBoundPartialPacket.readableBytes() + in.content().readableBytes()); + bb.writeBytes(clientBoundPartialPacket); + clientBoundPartialPacket.release(); + clientBoundPartialPacket = Unpooled.EMPTY_BUFFER; + bb.writeBytes(in.content()); + int readerIndex = 0; + try { + while (bb.isReadable()) { + readerIndex = bb.readerIndex(); + ClientboundPackets1_5_2 pkt = ClientboundPackets1_5_2.getPacket(bb.readUnsignedByte()); + pkt.getPacketReader().accept(null, bb); + int len = bb.readerIndex() - readerIndex; + ByteBuf packet = ctx.alloc().buffer(len); + bb.readerIndex(readerIndex); + bb.readBytes(packet, len); + decodeOld(ctx, packet, out); + } + } catch (Exception e) { + bb.readerIndex(readerIndex); + if (bb.readableBytes() > 65535) { + ctx.close(); + out.add(Unpooled.EMPTY_BUFFER); + return; + } + clientBoundPartialPacket = ctx.alloc().buffer(bb.readableBytes()); + clientBoundPartialPacket.writeBytes(bb); + } + } + if (out.isEmpty()) { + out.add(Unpooled.EMPTY_BUFFER); + } + } + public void decodeOld(ChannelHandlerContext ctx, ByteBuf in, List out) { + if (in.getUnsignedByte(0) == 0x14) { + in.skipBytes(5); + try { + String name = Types1_6_4.STRING.read(in); + UUID uuid = UUID.nameUUIDFromBytes(("OfflinePlayer:" + name).getBytes(StandardCharsets.UTF_8)); + uuidStringMap.put(uuid, name); + } catch (Exception ignored) { + } + in.resetReaderIndex(); + } + if (in.getUnsignedByte(0) == 0xFD) { + in.writerIndex(0); + in.writeByte((byte) 0xCD); + in.writeByte((byte) 0x00); + ctx.writeAndFlush(new BinaryWebSocketFrame(in.retain())); + return; + } + if (!skinsBeingFetched.isEmpty() && in.readableBytes() >= 3 && in.getUnsignedByte(0) == 250) { + in.skipBytes(1); + String tag; + byte[] msg; + try { + tag = Types1_6_4.STRING.read(in); + // System.out.println(tag); + if (tag.equals("EAG|UserSkin")) { + msg = new byte[in.readShort()]; + in.readBytes(msg); + System.out.println(msg.length); + byte[] res = new byte[msg.length - 1]; + System.arraycopy(msg, 1, res, 0, res.length); + if (res.length == 8192) { + final int[] tmp1 = new int[2048]; + final int[] tmp2 = new int[4096]; + for (int i = 0; i < tmp1.length; ++i) { + tmp1[i] = Ints.fromBytes(res[i * 4 + 3], res[i * 4], res[i * 4 + 1], res[i * 4 + 2]); + } + SkinConverter.convert64x32to64x64(tmp1, tmp2); + res = new byte[16384]; + for (int i = 0; i < tmp2.length; ++i) { + System.arraycopy(Ints.toByteArray(tmp2[i]), 0, res, i * 4, 4); + } + } else { + for (int j = 0; j < res.length; j += 4) { + final byte tmp3 = res[j + 3]; + res[j + 3] = res[j + 2]; + res[j + 2] = res[j + 1]; + res[j + 1] = res[j]; + res[j] = tmp3; + } + } + in.writerIndex(1); + Types1_6_4.STRING.write(in, "EAG|Skins-1.8"); + byte[] data = SkinPackets.makeCustomResponse(skinsBeingFetched.remove(0), 0, res); + in.writeShort(data.length); + in.writeBytes(data); + } + } catch (Exception ignored) { + } + in.resetReaderIndex(); + } + if (in.getByte(0) == (byte) 0x83 && in.getShort(1) != 358) { + return; + } + out.add(in.retain()); + } +} \ No newline at end of file diff --git a/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/EaglerSkinHandler.java b/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/EaglerSkinHandler.java index 07c0489..7530232 100644 --- a/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/EaglerSkinHandler.java +++ b/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/EaglerSkinHandler.java @@ -77,6 +77,10 @@ public class EaglerSkinHandler extends ChannelInboundHandlerAdapter { } try { if ("EAG|MySkin".equals(tag)) { + if (!FunnyConfig.eaglerSkins) { + bb.release(); + return; + } if (!EaglerSkinHandler.skinCollection.containsKey(uuid)) { final int t = msg[0] & 0xFF; if (t < EaglerSkinHandler.SKIN_DATA_SIZE.length && msg.length == EaglerSkinHandler.SKIN_DATA_SIZE[t] + 1) { @@ -87,6 +91,10 @@ public class EaglerSkinHandler extends ChannelInboundHandlerAdapter { return; } if ("EAG|MyCape".equals(tag)) { + if (!FunnyConfig.eaglerSkins) { + bb.release(); + return; + } if (!EaglerSkinHandler.capeCollection.containsKey(uuid)) { final int t = msg[0] & 0xFF; if (t < EaglerSkinHandler.CAPE_DATA_SIZE.length && msg.length == EaglerSkinHandler.CAPE_DATA_SIZE[t] + 2) { @@ -113,7 +121,7 @@ public class EaglerSkinHandler extends ChannelInboundHandlerAdapter { conc = conc2; } sendData(ctx, "EAG|UserSkin", conc); - } else if (EaglerXSkinHandler.skinService.loadPremiumSkins) { + } else if (FunnyConfig.premiumSkins) { try { URL url = new URL("https://playerdb.co/api/player/minecraft/" + fetch); URLConnection urlConnection = url.openConnection(); diff --git a/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/EaglerVoiceHandler.java b/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/EaglerVoiceHandler.java index 46da498..91eafba 100644 --- a/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/EaglerVoiceHandler.java +++ b/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/EaglerVoiceHandler.java @@ -78,6 +78,10 @@ public class EaglerVoiceHandler extends ChannelInboundHandlerAdapter { super.channelRead(ctx, obj); return; } + if (!FunnyConfig.eaglerVoice) { + bb.release(); + return; + } final DataInputStream streamIn = new DataInputStream(new ByteArrayInputStream(msg)); final int sig = streamIn.read(); switch (sig) { diff --git a/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/EaglerXSkinHandler.java b/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/EaglerXSkinHandler.java index 6a35966..7ff6bcf 100644 --- a/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/EaglerXSkinHandler.java +++ b/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/EaglerXSkinHandler.java @@ -13,7 +13,7 @@ import java.util.concurrent.ConcurrentHashMap; public class EaglerXSkinHandler extends ChannelInboundHandlerAdapter { private final ConcurrentHashMap profileData; - public static final SkinService skinService; + public static SkinService skinService; private String user; private int pluginMessageId; @@ -63,15 +63,17 @@ public class EaglerXSkinHandler extends ChannelInboundHandlerAdapter { } if (this.user == null) { this.user = ((EaglercraftHandler) ctx.pipeline().get("eaglercraft-handler")).username; - final UUID clientUUID = UUID.nameUUIDFromBytes(("OfflinePlayer:" + this.user).getBytes(StandardCharsets.UTF_8)); - if (this.profileData.containsKey("skin_v1")) { - try { - SkinPackets.registerEaglerPlayer(clientUUID, this.profileData.get("skin_v1"), EaglerXSkinHandler.skinService); - } catch (Throwable ex) { + if (FunnyConfig.eaglerSkins) { + final UUID clientUUID = UUID.nameUUIDFromBytes(("OfflinePlayer:" + this.user).getBytes(StandardCharsets.UTF_8)); + if (this.profileData.containsKey("skin_v1")) { + try { + SkinPackets.registerEaglerPlayer(clientUUID, this.profileData.get("skin_v1"), EaglerXSkinHandler.skinService); + } catch (Throwable ex) { + SkinPackets.registerEaglerPlayerFallback(clientUUID, EaglerXSkinHandler.skinService); + } + } else { SkinPackets.registerEaglerPlayerFallback(clientUUID, EaglerXSkinHandler.skinService); } - } else { - SkinPackets.registerEaglerPlayerFallback(clientUUID, EaglerXSkinHandler.skinService); } } if (this.pluginMessageId <= 0) { @@ -101,8 +103,4 @@ public class EaglerXSkinHandler extends ChannelInboundHandlerAdapter { EaglerXSkinHandler.skinService.unregisterPlayer(UUID.nameUUIDFromBytes(("OfflinePlayer:" + this.user).getBytes(StandardCharsets.UTF_8))); } } - - static { - skinService = new SkinService(); - } } diff --git a/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/EaglercraftHandler.java b/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/EaglercraftHandler.java index c077804..9bc6007 100644 --- a/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/EaglercraftHandler.java +++ b/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/EaglercraftHandler.java @@ -15,6 +15,7 @@ import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame; import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; import io.netty.handler.codec.http.websocketx.WebSocketFrame; import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler; +import io.netty.util.AttributeKey; import net.lenni0451.mcstructs.text.serializer.TextComponentSerializer; import net.raphimc.netminecraft.constants.ConnectionState; import net.raphimc.netminecraft.constants.MCPackets; @@ -24,6 +25,7 @@ import net.raphimc.vialegacy.protocols.release.protocol1_6_1to1_5_2.ServerboundP import net.raphimc.vialegacy.protocols.release.protocol1_7_2_5to1_6_4.types.Types1_6_4; import net.raphimc.vialoader.util.VersionEnum; import net.raphimc.viaproxy.ViaProxy; +import net.raphimc.viaproxy.proxy.client2proxy.Client2ProxyChannelInitializer; import net.raphimc.viaproxy.proxy.util.ExceptionUtil; import net.raphimc.viaproxy.util.logging.Logger; @@ -38,6 +40,15 @@ import java.util.List; import java.util.UUID; public class EaglercraftHandler extends MessageToMessageCodec { + public static class ProfileData { + public final String type; + public final byte[] data; + public ProfileData(String type, byte[] data) { + this.type = type; + this.data = data; + } + } + public static final AttributeKey profileDataKey = AttributeKey.newInstance("eagx-profile-data"); private HostAndPort host; public State state; public VersionEnum version; @@ -50,7 +61,9 @@ public class EaglercraftHandler extends MessageToMessageCodec out) { @@ -27,7 +42,7 @@ public class EaglercraftInitialHandler extends ByteToMessageDecoder { return; } if (in.readableBytes() >= 3 || in.getByte(0) != 71) { - if (in.readableBytes() >= 3 && in.getCharSequence(0, 3, StandardCharsets.UTF_8).equals("GET")) { + if ((in.readableBytes() >= 3 && in.getCharSequence(0, 3, StandardCharsets.UTF_8).equals("GET")) || (in.readableBytes() >= 4 && in.getCharSequence(0, 4, StandardCharsets.UTF_8).equals("POST"))) { if (EaglercraftInitialHandler.sslContext != null) { ctx.pipeline().addBefore("eaglercraft-initial-handler", "ws-ssl-handler", EaglercraftInitialHandler.sslContext.newHandler(ctx.alloc())); } @@ -37,6 +52,43 @@ public class EaglercraftInitialHandler extends ByteToMessageDecoder { ctx.pipeline().addBefore("eaglercraft-initial-handler", "ws-handler", new WebSocketServerProtocolHandler("/", null, true)); ctx.pipeline().addBefore("eaglercraft-initial-handler", "ws-active-notifier", new WebSocketActiveNotifier()); ctx.pipeline().addBefore("eaglercraft-initial-handler", "eaglercraft-handler", new EaglercraftHandler()); + ctx.pipeline().replace(Client2ProxyChannelInitializer.LEGACY_PASSTHROUGH_INITIAL_HANDLER_NAME, Client2ProxyChannelInitializer.LEGACY_PASSTHROUGH_INITIAL_HANDLER_NAME, new PassthroughInitialHandler() { + @Override + protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) { + if (ctx.channel().isOpen()) { + if (msg.isReadable()) { + int lengthOrPacketId = msg.getUnsignedByte(0); + if (lengthOrPacketId == 0 || lengthOrPacketId == 1 || lengthOrPacketId == 2 || lengthOrPacketId == 254) { + boolean fard = false; + for (Map.Entry entry : ctx.pipeline()) { + if (entry.getKey().equals(Client2ProxyChannelInitializer.LEGACY_PASSTHROUGH_INITIAL_HANDLER_NAME)) { + fard = true; + } + if (fard) { + ctx.pipeline().remove(entry.getValue()); + } + } + + Supplier handlerSupplier = () -> PluginManager.EVENT_MANAGER.call(new Client2ProxyHandlerCreationEvent(new PassthroughClient2ProxyHandler(), true)).getHandler(); + PassthroughClient2ProxyChannelInitializer channelInitializer = new PassthroughClient2ProxyChannelInitializer(handlerSupplier); + try { + initChannelMethod.invoke(channelInitializer, ctx.channel()); + } catch (IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(e); + } + try { + ((ChannelInboundHandler) ctx.pipeline().get(MCPipeline.HANDLER_HANDLER_NAME)).channelRead(ctx, msg.retain()); + } catch (Exception e) { + throw new RuntimeException(e); + } + } else { + ctx.pipeline().remove(this); + ctx.pipeline().fireChannelRead(msg.retain()); + } + } + } + } + }); ctx.fireUserEventTriggered(EaglercraftClientConnected.INSTANCE); ctx.pipeline().fireChannelRead(in.readBytes(in.readableBytes())); } else { @@ -55,14 +107,16 @@ public class EaglercraftInitialHandler extends ByteToMessageDecoder { throw new RuntimeException("Failed to load SSL context", e); } } + try { + initChannelMethod = PassthroughClient2ProxyChannelInitializer.class.getDeclaredMethod("initChannel", Channel.class); + initChannelMethod.setAccessible(true); + } catch (NoSuchMethodException e) { + throw new RuntimeException(e); + } } public static final class EaglercraftClientConnected { - public static final EaglercraftClientConnected INSTANCE; - - static { - INSTANCE = new EaglercraftClientConnected(); - } + public static final EaglercraftClientConnected INSTANCE = new EaglercraftClientConnected(); } @Override diff --git a/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/FunnyConfig.java b/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/FunnyConfig.java index d8cd89d..54a0aa0 100644 --- a/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/FunnyConfig.java +++ b/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/FunnyConfig.java @@ -9,7 +9,10 @@ import java.util.List; import java.util.Map; public class FunnyConfig extends Config { - private boolean premiumSkins = false; + public static boolean premiumSkins = false; + public static boolean eaglerUtils = true; + public static boolean eaglerSkins = true; + public static boolean eaglerVoice = true; protected FunnyConfig(File configFile) { super(configFile); @@ -17,14 +20,26 @@ public class FunnyConfig extends Config { @Override public URL getDefaultConfigURL() { - return Main.class.getResource("/eaglerskins.yml"); + return Main.class.getResource("/vpeagutils.yml"); } @Override protected void handleConfig(Map map) { Object item = map.get("premium-skins"); if (item instanceof Boolean) { - this.premiumSkins = (Boolean) item; + premiumSkins = (Boolean) item; + } + item = map.get("eagler-utils"); + if (item instanceof Boolean) { + eaglerUtils = (Boolean) item; + } + item = map.get("eagler-skins"); + if (item instanceof Boolean) { + eaglerSkins = (Boolean) item; + } + item = map.get("eagler-voice"); + if (item instanceof Boolean) { + eaglerVoice = (Boolean) item; } } @@ -32,8 +47,4 @@ public class FunnyConfig extends Config { public List getUnsupportedOptions() { return Collections.emptyList(); } - - public boolean getPremiumSkins() { - return this.premiumSkins; - } } diff --git a/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/GeneralDigest.java b/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/GeneralDigest.java new file mode 100644 index 0000000..a55d777 --- /dev/null +++ b/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/GeneralDigest.java @@ -0,0 +1,108 @@ +package me.ayunami2000.ayunViaProxyEagUtils; + +/** + * base implementation of MD4 family style digest as outlined in "Handbook of + * Applied Cryptography", pages 344 - 347. + */ +public abstract class GeneralDigest { + private byte[] xBuf; + private int xBufOff; + + private long byteCount; + + /** + * Standard constructor + */ + protected GeneralDigest() { + xBuf = new byte[4]; + xBufOff = 0; + } + + /** + * Copy constructor. We are using copy constructors in place of the + * Object.clone() interface as this interface is not supported by J2ME. + */ + protected GeneralDigest(GeneralDigest t) { + xBuf = new byte[t.xBuf.length]; + System.arraycopy(t.xBuf, 0, xBuf, 0, t.xBuf.length); + + xBufOff = t.xBufOff; + byteCount = t.byteCount; + } + + public void update(byte in) { + xBuf[xBufOff++] = in; + + if (xBufOff == xBuf.length) { + processWord(xBuf, 0); + xBufOff = 0; + } + + byteCount++; + } + + public void update(byte[] in, int inOff, int len) { + // + // fill the current word + // + while ((xBufOff != 0) && (len > 0)) { + update(in[inOff]); + + inOff++; + len--; + } + + // + // process whole words. + // + while (len > xBuf.length) { + processWord(in, inOff); + + inOff += xBuf.length; + len -= xBuf.length; + byteCount += xBuf.length; + } + + // + // load in the remainder. + // + while (len > 0) { + update(in[inOff]); + + inOff++; + len--; + } + } + + public void finish() { + long bitLength = (byteCount << 3); + + // + // add the pad bytes. + // + update((byte) 128); + + while (xBufOff != 0) { + update((byte) 0); + } + + processLength(bitLength); + + processBlock(); + } + + public void reset() { + byteCount = 0; + + xBufOff = 0; + for (int i = 0; i < xBuf.length; i++) { + xBuf[i] = 0; + } + } + + protected abstract void processWord(byte[] in, int inOff); + + protected abstract void processLength(long bitLength); + + protected abstract void processBlock(); +} diff --git a/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/HandshakePacketTypes.java b/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/HandshakePacketTypes.java new file mode 100644 index 0000000..bf6d6d3 --- /dev/null +++ b/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/HandshakePacketTypes.java @@ -0,0 +1,63 @@ +package me.ayunami2000.ayunViaProxyEagUtils; + +/** + * Copyright (c) 2022-2023 LAX1DUDE. All Rights Reserved. + * + * WITH THE EXCEPTION OF PATCH FILES, MINIFIED JAVASCRIPT, AND ALL FILES + * NORMALLY FOUND IN AN UNMODIFIED MINECRAFT RESOURCE PACK, YOU ARE NOT ALLOWED + * TO SHARE, DISTRIBUTE, OR REPURPOSE ANY FILE USED BY OR PRODUCED BY THE + * SOFTWARE IN THIS REPOSITORY WITHOUT PRIOR PERMISSION FROM THE PROJECT AUTHOR. + * + * NOT FOR COMMERCIAL OR MALICIOUS USE + * + * (please read the 'LICENSE' file this repo's root directory for more info) + * + */ +public class HandshakePacketTypes { + + public static final String AUTHENTICATION_REQUIRED = "Authentication Required:"; + + public static final int PROTOCOL_CLIENT_VERSION = 0x01; + public static final int PROTOCOL_SERVER_VERSION = 0x02; + public static final int PROTOCOL_VERSION_MISMATCH = 0x03; + public static final int PROTOCOL_CLIENT_REQUEST_LOGIN = 0x04; + public static final int PROTOCOL_SERVER_ALLOW_LOGIN = 0x05; + public static final int PROTOCOL_SERVER_DENY_LOGIN = 0x06; + public static final int PROTOCOL_CLIENT_PROFILE_DATA = 0x07; + public static final int PROTOCOL_CLIENT_FINISH_LOGIN = 0x08; + public static final int PROTOCOL_SERVER_FINISH_LOGIN = 0x09; + public static final int PROTOCOL_SERVER_ERROR = 0xFF; + + public static final int STATE_OPENED = 0x00; + public static final int STATE_CLIENT_VERSION = 0x01; + public static final int STATE_CLIENT_LOGIN = 0x02; + public static final int STATE_CLIENT_COMPLETE = 0x03; + + public static final int SERVER_ERROR_UNKNOWN_PACKET = 0x01; + public static final int SERVER_ERROR_INVALID_PACKET = 0x02; + public static final int SERVER_ERROR_WRONG_PACKET = 0x03; + public static final int SERVER_ERROR_EXCESSIVE_PROFILE_DATA = 0x04; + public static final int SERVER_ERROR_DUPLICATE_PROFILE_DATA = 0x05; + public static final int SERVER_ERROR_RATELIMIT_BLOCKED = 0x06; + public static final int SERVER_ERROR_RATELIMIT_LOCKED = 0x07; + public static final int SERVER_ERROR_CUSTOM_MESSAGE = 0x08; + public static final int SERVER_ERROR_AUTHENTICATION_REQUIRED = 0x09; + + public static final int AUTH_METHOD_NONE = 0x0; + public static final int AUTH_METHOD_EAGLER_SHA256 = 0x01; + public static final int AUTH_METHOD_AUTHME_SHA256 = 0x02; + public static final int AUTH_METHOD_PLAINTEXT = 0xFF; + + public static final byte[] EAGLER_SHA256_SALT_BASE = new byte[] { (byte) 117, (byte) 43, (byte) 1, (byte) 112, + (byte) 75, (byte) 3, (byte) 188, (byte) 61, (byte) 121, (byte) 31, (byte) 34, (byte) 181, (byte) 234, + (byte) 31, (byte) 247, (byte) 72, (byte) 12, (byte) 168, (byte) 138, (byte) 45, (byte) 143, (byte) 77, + (byte) 118, (byte) 245, (byte) 187, (byte) 242, (byte) 188, (byte) 219, (byte) 160, (byte) 235, (byte) 235, + (byte) 68 }; + + public static final byte[] EAGLER_SHA256_SALT_SAVE = new byte[] { (byte) 49, (byte) 25, (byte) 39, (byte) 38, + (byte) 253, (byte) 85, (byte) 70, (byte) 245, (byte) 71, (byte) 150, (byte) 253, (byte) 206, (byte) 4, + (byte) 26, (byte) 198, (byte) 249, (byte) 145, (byte) 251, (byte) 232, (byte) 174, (byte) 186, (byte) 98, + (byte) 27, (byte) 232, (byte) 55, (byte) 144, (byte) 83, (byte) 21, (byte) 36, (byte) 55, (byte) 170, + (byte) 118 }; + +} diff --git a/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/Main.java b/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/Main.java index 7ad0610..9044851 100644 --- a/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/Main.java +++ b/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/Main.java @@ -1,28 +1,149 @@ package me.ayunami2000.ayunViaProxyEagUtils; import io.netty.buffer.ByteBuf; -import io.netty.channel.ChannelHandlerContext; -import io.netty.channel.ChannelInboundHandlerAdapter; -import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame; +import io.netty.channel.*; +import io.netty.handler.codec.http.*; +import io.netty.handler.codec.http.websocketx.*; +import io.netty.handler.codec.http.websocketx.extensions.compression.WebSocketClientCompressionHandler; +import io.netty.handler.ssl.SslHandler; +import io.netty.util.AttributeKey; import net.lenni0451.lambdaevents.EventHandler; +import net.raphimc.netminecraft.constants.MCPipeline; +import net.raphimc.netminecraft.netty.connection.NetClient; +import net.raphimc.netminecraft.util.ServerAddress; import net.raphimc.vialegacy.protocols.release.protocol1_7_2_5to1_6_4.types.Types1_6_4; +import net.raphimc.vialoader.util.VersionEnum; import net.raphimc.viaproxy.plugins.PluginManager; import net.raphimc.viaproxy.plugins.ViaProxyPlugin; import net.raphimc.viaproxy.plugins.events.Client2ProxyChannelInitializeEvent; +import net.raphimc.viaproxy.plugins.events.Proxy2ServerChannelInitializeEvent; import net.raphimc.viaproxy.plugins.events.types.ITyped; +import net.raphimc.viaproxy.proxy.session.LegacyProxyConnection; +import net.raphimc.viaproxy.proxy.session.ProxyConnection; import net.raphimc.viaproxy.proxy.util.ExceptionUtil; +import javax.net.ssl.*; +import java.io.File; +import java.net.URI; +import java.net.URISyntaxException; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.security.cert.X509Certificate; + public class Main extends ViaProxyPlugin { + public static final AttributeKey secureWs = AttributeKey.newInstance("eag-secure-ws"); + public static final AttributeKey wsPath = AttributeKey.newInstance("eag-ws-path"); + public static final AttributeKey eagxPass = AttributeKey.newInstance("eag-x-pass"); + private static final SSLContext sc; + + static { + try { + TrustManager[] trustAllCerts = new TrustManager[]{ + new X509TrustManager() { + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[0]; + } + + public void checkClientTrusted(X509Certificate[] certs, String authType) { + } + + public void checkServerTrusted(X509Certificate[] certs, String authType) { + } + }}; + + // Ignore differences between given hostname and certificate hostname + sc = SSLContext.getInstance("SSL"); + sc.init(null, trustAllCerts, new SecureRandom()); + HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory()); + HttpsURLConnection.setDefaultHostnameVerifier((hostname, session) -> true); + } catch (KeyManagementException | NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + } + public void onEnable() { PluginManager.EVENT_MANAGER.register(this); + (new FunnyConfig(new File("ViaLoader", "vpeagutils.yml"))).reloadConfig(); + EaglerXSkinHandler.skinService = new SkinService(); + } + + @EventHandler + public void onEvent(final Proxy2ServerChannelInitializeEvent event) throws URISyntaxException { + if (event.getType() == ITyped.Type.POST) { + Channel ch = event.getChannel(); + + NetClient proxyConnection; + Channel c2p; + ServerAddress addr; + if (event.isLegacyPassthrough()) { + proxyConnection = LegacyProxyConnection.fromChannel(ch); + c2p = ((LegacyProxyConnection) proxyConnection).getC2P(); + addr = ((LegacyProxyConnection) proxyConnection).getServerAddress(); + } else { + proxyConnection = ProxyConnection.fromChannel(ch); + c2p = ((ProxyConnection) proxyConnection).getC2P(); + addr = ((ProxyConnection) proxyConnection).getServerAddress(); + } + + + + + + c2p.attr(secureWs).set(true); + + + + + + if (c2p.hasAttr(secureWs)) { + ch.attr(MCPipeline.COMPRESSION_THRESHOLD_ATTRIBUTE_KEY).set(-2); + if (proxyConnection instanceof ProxyConnection && ((ProxyConnection) proxyConnection).getServerVersion().isNewerThan(VersionEnum.r1_6_4)) { + ch.pipeline().remove(MCPipeline.SIZER_HANDLER_NAME); + } else if (ch.pipeline().get(MCPipeline.ENCRYPTION_HANDLER_NAME) != null) { + ch.pipeline().remove(MCPipeline.ENCRYPTION_HANDLER_NAME); + } + StringBuilder url = new StringBuilder("ws"); + boolean secure = c2p.attr(secureWs).get(); + if (secure) { + final SSLEngine sslEngine = sc.createSSLEngine(addr.getAddress(), addr.getPort()); + sslEngine.setUseClientMode(true); + sslEngine.setNeedClientAuth(false); + ch.pipeline().addFirst("eag-server-ssl", new SslHandler(sslEngine)); + url.append("s"); + ch.pipeline().addAfter("eag-server-ssl", "eag-server-http-codec", new HttpClientCodec()); + } else { + ch.pipeline().addFirst("eag-server-http-codec", new HttpClientCodec()); + } + url.append("://").append(addr.getAddress()); + boolean addPort = (secure && addr.getPort() != 443) || (!secure && addr.getPort() != 80); + if (addPort) { + url.append(":").append(addr.getPort()); + } + String path = c2p.attr(wsPath).get(); + if (path != null) { + url.append("/").append(path); + } + URI uri = new URI(url.toString()); + HttpHeaders headers = new DefaultHttpHeaders(); + headers.set(HttpHeaderNames.HOST, uri.getHost() + (addPort ? ":" + uri.getPort() : "")); + headers.set(HttpHeaderNames.ORIGIN, "via.shhnowisnottheti.me"); + ch.pipeline().addAfter("eag-server-http-codec", "eag-server-http-aggregator", new HttpObjectAggregator(2097152, true)); + ch.pipeline().addAfter("eag-server-http-aggregator", "eag-server-ws-compression", WebSocketClientCompressionHandler.INSTANCE); + ch.pipeline().addAfter("eag-server-ws-compression", "eag-server-ws-handshaker", new WebSocketClientProtocolHandler(WebSocketClientHandshakerFactory.newHandshaker(uri, WebSocketVersion.V13, null, true, headers, 2097152))); + ch.pipeline().addAfter("eag-server-ws-handshaker", "eag-server-ws-ready", new WebSocketConnectedNotifier()); + ch.pipeline().addAfter("eag-server-ws-ready", "eag-server-handler", new EaglerServerHandler(proxyConnection, c2p.attr(eagxPass).get())); + } + } } @EventHandler public void onEvent(final Client2ProxyChannelInitializeEvent event) { + if (event.isLegacyPassthrough()) return; if (event.getType() == ITyped.Type.PRE) { event.getChannel().pipeline().addLast("eaglercraft-initial-handler", new EaglercraftInitialHandler()); } - if (event.getType() == ITyped.Type.POST) { + if (event.getType() == ITyped.Type.POST && FunnyConfig.eaglerUtils) { event.getChannel().pipeline().addAfter("eaglercraft-initial-handler", "ayun-eag-detector", new EaglerConnectionHandler()); } } diff --git a/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/MsgPromise.java b/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/MsgPromise.java new file mode 100644 index 0000000..7298343 --- /dev/null +++ b/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/MsgPromise.java @@ -0,0 +1,13 @@ +package me.ayunami2000.ayunViaProxyEagUtils; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelPromise; + +public class MsgPromise { + public Object msg; + public ChannelPromise promise; + public MsgPromise(Object msg, ChannelPromise promise) { + this.msg = msg; + this.promise = promise; + } +} diff --git a/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/SHA256Digest.java b/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/SHA256Digest.java new file mode 100644 index 0000000..a917f99 --- /dev/null +++ b/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/SHA256Digest.java @@ -0,0 +1,233 @@ +package me.ayunami2000.ayunViaProxyEagUtils; + +public class SHA256Digest extends GeneralDigest { + + private static final int DIGEST_LENGTH = 32; + + private int H1, H2, H3, H4, H5, H6, H7, H8; + + private int[] X = new int[64]; + private int xOff; + + public SHA256Digest() { + reset(); + } + + public static int bigEndianToInt(byte[] bs, int off) { + int n = bs[off] << 24; + n |= (bs[++off] & 0xff) << 16; + n |= (bs[++off] & 0xff) << 8; + n |= (bs[++off] & 0xff); + return n; + } + + public static void bigEndianToInt(byte[] bs, int off, int[] ns) { + for (int i = 0; i < ns.length; ++i) { + ns[i] = bigEndianToInt(bs, off); + off += 4; + } + } + + public static byte[] intToBigEndian(int n) { + byte[] bs = new byte[4]; + intToBigEndian(n, bs, 0); + return bs; + } + + public static void intToBigEndian(int n, byte[] bs, int off) { + bs[off] = (byte) (n >>> 24); + bs[++off] = (byte) (n >>> 16); + bs[++off] = (byte) (n >>> 8); + bs[++off] = (byte) (n); + } + + protected void processWord(byte[] in, int inOff) { + X[xOff] = bigEndianToInt(in, inOff); + + if (++xOff == 16) { + processBlock(); + } + } + + protected void processLength(long bitLength) { + if (xOff > 14) { + processBlock(); + } + + X[14] = (int) (bitLength >>> 32); + X[15] = (int) (bitLength & 0xffffffff); + } + + public int doFinal(byte[] out, int outOff) { + finish(); + + intToBigEndian(H1, out, outOff); + intToBigEndian(H2, out, outOff + 4); + intToBigEndian(H3, out, outOff + 8); + intToBigEndian(H4, out, outOff + 12); + intToBigEndian(H5, out, outOff + 16); + intToBigEndian(H6, out, outOff + 20); + intToBigEndian(H7, out, outOff + 24); + intToBigEndian(H8, out, outOff + 28); + + reset(); + + return DIGEST_LENGTH; + } + + /** + * reset the chaining variables + */ + public void reset() { + super.reset(); + + /* + * SHA-256 initial hash value The first 32 bits of the fractional parts of the + * square roots of the first eight prime numbers + */ + + H1 = 0x6a09e667; + H2 = 0xbb67ae85; + H3 = 0x3c6ef372; + H4 = 0xa54ff53a; + H5 = 0x510e527f; + H6 = 0x9b05688c; + H7 = 0x1f83d9ab; + H8 = 0x5be0cd19; + + xOff = 0; + for (int i = 0; i != X.length; i++) { + X[i] = 0; + } + } + + protected void processBlock() { + // + // expand 16 word block into 64 word blocks. + // + for (int t = 16; t <= 63; t++) { + X[t] = Theta1(X[t - 2]) + X[t - 7] + Theta0(X[t - 15]) + X[t - 16]; + } + + // + // set up working variables. + // + int a = H1; + int b = H2; + int c = H3; + int d = H4; + int e = H5; + int f = H6; + int g = H7; + int h = H8; + + int t = 0; + for (int i = 0; i < 8; i++) { + // t = 8 * i + h += Sum1(e) + Ch(e, f, g) + K[t] + X[t]; + d += h; + h += Sum0(a) + Maj(a, b, c); + ++t; + + // t = 8 * i + 1 + g += Sum1(d) + Ch(d, e, f) + K[t] + X[t]; + c += g; + g += Sum0(h) + Maj(h, a, b); + ++t; + + // t = 8 * i + 2 + f += Sum1(c) + Ch(c, d, e) + K[t] + X[t]; + b += f; + f += Sum0(g) + Maj(g, h, a); + ++t; + + // t = 8 * i + 3 + e += Sum1(b) + Ch(b, c, d) + K[t] + X[t]; + a += e; + e += Sum0(f) + Maj(f, g, h); + ++t; + + // t = 8 * i + 4 + d += Sum1(a) + Ch(a, b, c) + K[t] + X[t]; + h += d; + d += Sum0(e) + Maj(e, f, g); + ++t; + + // t = 8 * i + 5 + c += Sum1(h) + Ch(h, a, b) + K[t] + X[t]; + g += c; + c += Sum0(d) + Maj(d, e, f); + ++t; + + // t = 8 * i + 6 + b += Sum1(g) + Ch(g, h, a) + K[t] + X[t]; + f += b; + b += Sum0(c) + Maj(c, d, e); + ++t; + + // t = 8 * i + 7 + a += Sum1(f) + Ch(f, g, h) + K[t] + X[t]; + e += a; + a += Sum0(b) + Maj(b, c, d); + ++t; + } + + H1 += a; + H2 += b; + H3 += c; + H4 += d; + H5 += e; + H6 += f; + H7 += g; + H8 += h; + + // + // reset the offset and clean out the word buffer. + // + xOff = 0; + for (int i = 0; i < 16; i++) { + X[i] = 0; + } + } + + /* SHA-256 functions */ + private static int Ch(int x, int y, int z) { + return (x & y) ^ ((~x) & z); +// return z ^ (x & (y ^ z)); + } + + private static int Maj(int x, int y, int z) { +// return (x & y) ^ (x & z) ^ (y & z); + return (x & y) | (z & (x ^ y)); + } + + private static int Sum0(int x) { + return ((x >>> 2) | (x << 30)) ^ ((x >>> 13) | (x << 19)) ^ ((x >>> 22) | (x << 10)); + } + + private static int Sum1(int x) { + return ((x >>> 6) | (x << 26)) ^ ((x >>> 11) | (x << 21)) ^ ((x >>> 25) | (x << 7)); + } + + private static int Theta0(int x) { + return ((x >>> 7) | (x << 25)) ^ ((x >>> 18) | (x << 14)) ^ (x >>> 3); + } + + private static int Theta1(int x) { + return ((x >>> 17) | (x << 15)) ^ ((x >>> 19) | (x << 13)) ^ (x >>> 10); + } + + /* + * SHA-256 Constants (represent the first 32 bits of the fractional parts of the + * cube roots of the first sixty-four prime numbers) + */ + static final int K[] = { 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, + 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, + 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152, + 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, + 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, + 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, + 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, + 0xa4506ceb, 0xbef9a3f7, 0xc67178f2 }; + +} diff --git a/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/SkinPackets.java b/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/SkinPackets.java index 14d9ce5..30b22cd 100644 --- a/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/SkinPackets.java +++ b/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/SkinPackets.java @@ -21,7 +21,7 @@ public class SkinPackets { break; } case 6: { - if (EaglerXSkinHandler.skinService.loadPremiumSkins) { + if (FunnyConfig.premiumSkins) { processGetOtherSkinByURL(data, sender, skinService); } break; diff --git a/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/SkinService.java b/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/SkinService.java index 4a2b95f..597e994 100644 --- a/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/SkinService.java +++ b/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/SkinService.java @@ -6,15 +6,9 @@ import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame; import net.raphimc.netminecraft.constants.MCPackets; import net.raphimc.netminecraft.packet.PacketTypes; -import net.raphimc.vialegacy.ViaLegacy; -import net.raphimc.vialegacy.ViaLegacyConfig; -import net.raphimc.vialoader.util.VersionEnum; -import net.raphimc.viaproxy.ViaProxy; -import net.raphimc.viaproxy.cli.options.Options; import javax.imageio.ImageIO; import java.awt.image.DataBufferByte; -import java.io.File; import java.io.IOException; import java.io.InputStream; import java.net.URI; @@ -23,15 +17,10 @@ import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; public class SkinService { - public final boolean loadPremiumSkins; private final ConcurrentHashMap skinCache; public SkinService() { this.skinCache = new ConcurrentHashMap<>(); - File funnyFile = new File("ViaLoader", "eaglerskins.yml"); - FunnyConfig funnyConfig = new FunnyConfig(funnyFile); - funnyConfig.reloadConfig(); - this.loadPremiumSkins = funnyConfig.getPremiumSkins(); } private static void sendData(final ChannelHandlerContext ctx, final byte[] data) { diff --git a/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/WebSocketConnectedNotifier.java b/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/WebSocketConnectedNotifier.java new file mode 100644 index 0000000..2c56f88 --- /dev/null +++ b/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/WebSocketConnectedNotifier.java @@ -0,0 +1,61 @@ +package me.ayunami2000.ayunViaProxyEagUtils; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelDuplexHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelPromise; +import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame; +import io.netty.handler.codec.http.websocketx.WebSocketClientProtocolHandler; +import io.netty.handler.ssl.SslCompletionEvent; + +import java.util.ArrayList; +import java.util.List; + +public class WebSocketConnectedNotifier extends ChannelDuplexHandler { + private final List msgsRead = new ArrayList<>(); + private final List msgsWrite = new ArrayList<>(); + @Override + public void userEventTriggered(final ChannelHandlerContext ctx, final Object evt) { + if (evt == WebSocketClientProtocolHandler.ClientHandshakeStateEvent.HANDSHAKE_COMPLETE) { + ctx.fireChannelActive(); + ctx.fireUserEventTriggered(evt); + ctx.pipeline().remove(this); + for (final Object msg : msgsRead) + ctx.fireChannelRead(msg); + msgsRead.clear(); + for (final MsgPromise msg : msgsWrite) + ctx.write(msg.msg, msg.promise); + ctx.flush(); + msgsWrite.clear(); + } else if (evt == WebSocketClientProtocolHandler.ClientHandshakeStateEvent.HANDSHAKE_TIMEOUT) { + ctx.fireUserEventTriggered(evt); + ctx.close(); + } else { + if (evt instanceof SslCompletionEvent && !((SslCompletionEvent) evt).isSuccess()) { + ((SslCompletionEvent) evt).cause().printStackTrace(); + } + ctx.fireUserEventTriggered(evt); + } + } + @Override + public void channelActive(final ChannelHandlerContext ctx) { + // + } + @Override + public void channelRead(final ChannelHandlerContext ctx, final Object msg) throws Exception { + if (msg instanceof ByteBuf) { + msgsRead.add(msg); + } else { + ctx.fireChannelRead(msg); + } + } + + @Override + public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { + if (msg instanceof BinaryWebSocketFrame || msg instanceof ByteBuf) { + msgsWrite.add(new MsgPromise(msg, promise)); + } else { + ctx.write(msg, promise); + } + } +} \ No newline at end of file diff --git a/src/main/resources/eaglerskins.yml b/src/main/resources/eaglerskins.yml deleted file mode 100644 index e81f64d..0000000 --- a/src/main/resources/eaglerskins.yml +++ /dev/null @@ -1,2 +0,0 @@ -# Use premium skins -premium-skins: false \ No newline at end of file diff --git a/src/main/resources/vpeagutils.yml b/src/main/resources/vpeagutils.yml new file mode 100644 index 0000000..44c74e0 --- /dev/null +++ b/src/main/resources/vpeagutils.yml @@ -0,0 +1,8 @@ +# Use premium skins +premium-skins: false +# Handle Eagler skins and voice on the proxy side +eagler-utils: true +# Sync Eagler skins +eagler-skins: true +# Enable Eagler voice chat +eagler-voice: true \ No newline at end of file