commit 0dba9a05e6b9b7fdac579f68107d87263ef6b168 Author: ayunami2000 Date: Mon Sep 25 12:46:10 2023 -0400 open sauce diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..dfe0770 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c3f36c4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +.gradle +.idea +build +gradle +libs +gradlew +gradlew.bat +src/main/resources/*.png \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..6a4bcde --- /dev/null +++ b/LICENSE @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2023, ayunami2000 +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..345142c --- /dev/null +++ b/README.md @@ -0,0 +1,4 @@ +# ayunViaProxyEagUtils +eaglercraft support for viaproxy. ws:// on same port as java. supports both 1.5.2 and 1.8.8 eaglercraft depending on the configuration (e.g. use legacy passthrough & protocolsupport if the backend server doesn't support 1.5.2) + +note: skin files from 1.5.2 are excluded \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..4cc11ff --- /dev/null +++ b/build.gradle @@ -0,0 +1,14 @@ +plugins { + id 'java' +} + +group 'me.ayunami2000.ayunViaProxyEagUtils' +version '1.0-SNAPSHOT' + +repositories { + mavenCentral() +} + +dependencies { + implementation files("libs/ViaProxy-3.0.21-SNAPSHOT+java8_PATCHED.jar") +} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..2f44a9a --- /dev/null +++ b/settings.gradle @@ -0,0 +1,2 @@ +rootProject.name = 'ayunViaProxyEagUtils' + diff --git a/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/EaglerSkinHandler.java b/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/EaglerSkinHandler.java new file mode 100644 index 0000000..b791291 --- /dev/null +++ b/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/EaglerSkinHandler.java @@ -0,0 +1,163 @@ +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 net.raphimc.vialegacy.protocols.release.protocol1_7_2_5to1_6_4.types.Types1_6_4; +import net.raphimc.viaproxy.proxy.util.ExceptionUtil; + +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +public class EaglerSkinHandler extends ChannelInboundHandlerAdapter { + public static final ConcurrentHashMap skinCollection; + private static final ConcurrentHashMap capeCollection; + private static final ConcurrentHashMap lastSkinLayerUpdate; + private static final int[] SKIN_DATA_SIZE; + private static final int[] CAPE_DATA_SIZE; + private static final ConcurrentHashMap users; + private final String user; + + private static void sendData(final ChannelHandlerContext ctx, final String channel, final byte[] data) throws IOException { + final ByteBuf bb = ctx.alloc().buffer(); + bb.writeByte(250); + try { + Types1_6_4.STRING.write(bb, channel); + } catch (Exception e) { + throw new IOException(e); + } + bb.writeShort(data.length); + bb.writeBytes(data); + ctx.writeAndFlush(new BinaryWebSocketFrame(bb)); + } + + public EaglerSkinHandler(final String username) { + this.user = username; + } + + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + ExceptionUtil.handleNettyException(ctx, cause, null); + } + + public void channelRead(final ChannelHandlerContext ctx, final Object obj) throws Exception { + final UUID uuid = UUID.nameUUIDFromBytes(("OfflinePlayer:" + this.user).getBytes(StandardCharsets.UTF_8)); + if (!EaglerSkinHandler.users.containsKey(uuid) && ctx.channel().isActive()) { + EaglerSkinHandler.users.put(uuid, ctx); + } + if (obj instanceof BinaryWebSocketFrame) { + final ByteBuf bb = ((BinaryWebSocketFrame) obj).content(); + if (bb.readableBytes() >= 3 && bb.readByte() == -6) { + String tag; + byte[] msg; + try { + tag = Types1_6_4.STRING.read(bb); + msg = new byte[bb.readShort()]; + bb.readBytes(msg); + } catch (Exception e) { + bb.resetReaderIndex(); + super.channelRead(ctx, obj); + return; + } + try { + if ("EAG|MySkin".equals(tag)) { + 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) { + EaglerSkinHandler.skinCollection.put(uuid, msg); + } + } + bb.release(); + return; + } + if ("EAG|MyCape".equals(tag)) { + 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) { + EaglerSkinHandler.capeCollection.put(uuid, msg); + } + } + bb.release(); + return; + } + if ("EAG|FetchSkin".equals(tag)) { + if (msg.length > 2) { + final String fetch = new String(msg, 2, msg.length - 2, StandardCharsets.UTF_8); + final UUID uuidFetch = UUID.nameUUIDFromBytes(("OfflinePlayer:" + fetch).getBytes(StandardCharsets.UTF_8)); + byte[] data; + if ((data = EaglerSkinHandler.skinCollection.get(uuidFetch)) != null) { + byte[] conc = new byte[data.length + 2]; + conc[0] = msg[0]; + conc[1] = msg[1]; + System.arraycopy(data, 0, conc, 2, data.length); + if ((data = EaglerSkinHandler.capeCollection.get(uuidFetch)) != null) { + final byte[] conc2 = new byte[conc.length + data.length]; + System.arraycopy(conc, 0, conc2, 0, conc.length); + System.arraycopy(data, 0, conc2, conc.length, data.length); + conc = conc2; + } + sendData(ctx, "EAG|UserSkin", conc); + } + } + bb.release(); + return; + } + if ("EAG|SkinLayers".equals(tag)) { + final long millis = System.currentTimeMillis(); + final Long lsu = EaglerSkinHandler.lastSkinLayerUpdate.get(uuid); + if (lsu != null && millis - lsu < 700L) { + return; + } + EaglerSkinHandler.lastSkinLayerUpdate.put(uuid, millis); + byte[] conc2; + if ((conc2 = EaglerSkinHandler.capeCollection.get(uuid)) != null) { + conc2[1] = msg[0]; + } else { + conc2 = new byte[]{2, msg[0], 0}; + EaglerSkinHandler.capeCollection.put(uuid, conc2); + } + final ByteArrayOutputStream bao = new ByteArrayOutputStream(); + final DataOutputStream dd = new DataOutputStream(bao); + dd.write(msg[0]); + dd.writeUTF(this.user); + final byte[] bpacket = bao.toByteArray(); + for (final UUID pl : EaglerSkinHandler.users.keySet()) { + if (!pl.equals(uuid)) { + sendData(EaglerSkinHandler.users.get(pl), "EAG|SkinLayers", bpacket); + } + } + bb.release(); + return; + } + } catch (Throwable var18) { + var18.printStackTrace(); + } + } + bb.resetReaderIndex(); + } + super.channelRead(ctx, obj); + } + + public void channelInactive(final ChannelHandlerContext ctx) throws Exception { + super.channelInactive(ctx); + final UUID uuid = UUID.nameUUIDFromBytes(("OfflinePlayer:" + this.user).getBytes(StandardCharsets.UTF_8)); + EaglerSkinHandler.users.remove(uuid); + EaglerSkinHandler.skinCollection.remove(uuid); + EaglerSkinHandler.capeCollection.remove(uuid); + EaglerSkinHandler.lastSkinLayerUpdate.remove(uuid); + } + + static { + skinCollection = new ConcurrentHashMap<>(); + capeCollection = new ConcurrentHashMap<>(); + lastSkinLayerUpdate = new ConcurrentHashMap<>(); + SKIN_DATA_SIZE = new int[]{8192, 16384, -9, -9, 1, 16384, -9}; + CAPE_DATA_SIZE = new int[]{4096, -9, 1}; + users = new ConcurrentHashMap<>(); + } +} diff --git a/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/EaglerVoiceHandler.java b/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/EaglerVoiceHandler.java new file mode 100644 index 0000000..e63af27 --- /dev/null +++ b/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/EaglerVoiceHandler.java @@ -0,0 +1,321 @@ +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.util.AttributeKey; +import net.raphimc.vialegacy.protocols.release.protocol1_7_2_5to1_6_4.types.Types1_6_4; +import net.raphimc.viaproxy.proxy.util.ExceptionUtil; + +import java.io.*; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArraySet; + +public class EaglerVoiceHandler extends ChannelInboundHandlerAdapter { + private static final ConcurrentHashMap voicePlayers; + private static final ConcurrentHashMap> voiceRequests; + private static final CopyOnWriteArraySet voicePairs; + private final String user; + private static final Collection iceServers; + private static final AttributeKey VOICE_ENABLED; + + private static void sendData(final ChannelHandlerContext ctx, final byte[] data) throws IOException { + final ByteBuf bb = ctx.alloc().buffer(); + bb.writeByte(250); + try { + Types1_6_4.STRING.write(bb, "EAG|Voice"); + } catch (Exception e) { + throw new IOException(e); + } + bb.writeShort(data.length); + bb.writeBytes(data); + ctx.writeAndFlush(new BinaryWebSocketFrame(bb)); + } + + public EaglerVoiceHandler(final String username) { + this.user = username; + } + + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + ExceptionUtil.handleNettyException(ctx, cause, null); + } + + public void channelRead(final ChannelHandlerContext ctx, final Object obj) throws Exception { + if (((EaglercraftHandler) ctx.pipeline().get("eaglercraft-handler")).state == EaglercraftHandler.State.LOGIN_COMPLETE && !ctx.channel().hasAttr((AttributeKey) EaglerVoiceHandler.VOICE_ENABLED)) { + final ByteArrayOutputStream baos = new ByteArrayOutputStream(); + final DataOutputStream dos = new DataOutputStream(baos); + dos.write(0); + dos.writeBoolean(true); + dos.write(EaglerVoiceHandler.iceServers.size()); + for (final String str : EaglerVoiceHandler.iceServers) { + dos.writeUTF(str); + } + sendData(ctx, baos.toByteArray()); + this.sendVoicePlayers(this.user); + ctx.channel().attr((AttributeKey) EaglerVoiceHandler.VOICE_ENABLED).set(true); + } + if (obj instanceof BinaryWebSocketFrame) { + final ByteBuf bb = ((BinaryWebSocketFrame) obj).content(); + if (bb.readableBytes() >= 3 && bb.readByte() == -6) { + String tag; + byte[] msg; + try { + tag = Types1_6_4.STRING.read(bb); + msg = new byte[bb.readShort()]; + bb.readBytes(msg); + } catch (Exception e) { + bb.resetReaderIndex(); + super.channelRead(ctx, obj); + return; + } + try { + if (!tag.equals("EAG|Voice")) { + bb.resetReaderIndex(); + super.channelRead(ctx, obj); + return; + } + final DataInputStream streamIn = new DataInputStream(new ByteArrayInputStream(msg)); + final int sig = streamIn.read(); + switch (sig) { + case 0: { + if (!EaglerVoiceHandler.voicePlayers.containsKey(this.user)) { + bb.release(); + return; + } + final String targetUser = streamIn.readUTF(); + if (this.user.equals(targetUser)) { + bb.release(); + return; + } + if (this.checkVoicePair(this.user, targetUser)) { + bb.release(); + return; + } + if (!EaglerVoiceHandler.voicePlayers.containsKey(targetUser)) { + bb.release(); + return; + } + if (!EaglerVoiceHandler.voiceRequests.containsKey(this.user)) { + EaglerVoiceHandler.voiceRequests.put(this.user, new ExpiringSet<>(2000L)); + } + if (!EaglerVoiceHandler.voiceRequests.get(this.user).contains(targetUser)) { + EaglerVoiceHandler.voiceRequests.get(this.user).add(targetUser); + if (EaglerVoiceHandler.voiceRequests.containsKey(targetUser) && EaglerVoiceHandler.voiceRequests.get(targetUser).contains(this.user)) { + if (EaglerVoiceHandler.voiceRequests.containsKey(targetUser)) { + EaglerVoiceHandler.voiceRequests.get(targetUser).remove(this.user); + if (EaglerVoiceHandler.voiceRequests.get(targetUser).isEmpty()) { + EaglerVoiceHandler.voiceRequests.remove(targetUser); + } + } + if (EaglerVoiceHandler.voiceRequests.containsKey(this.user)) { + EaglerVoiceHandler.voiceRequests.get(this.user).remove(targetUser); + if (EaglerVoiceHandler.voiceRequests.get(this.user).isEmpty()) { + EaglerVoiceHandler.voiceRequests.remove(this.user); + } + } + EaglerVoiceHandler.voicePairs.add(new String[]{this.user, targetUser}); + ByteArrayOutputStream baos2 = new ByteArrayOutputStream(); + DataOutputStream dos2 = new DataOutputStream(baos2); + dos2.write(1); + dos2.writeUTF(this.user); + dos2.writeBoolean(false); + sendData(EaglerVoiceHandler.voicePlayers.get(targetUser), baos2.toByteArray()); + baos2 = new ByteArrayOutputStream(); + dos2 = new DataOutputStream(baos2); + dos2.write(1); + dos2.writeUTF(targetUser); + dos2.writeBoolean(true); + sendData(ctx, baos2.toByteArray()); + } + bb.release(); + return; + } + bb.release(); + return; + } + case 1: { + if (!EaglerVoiceHandler.voicePlayers.containsKey(this.user)) { + final ByteArrayOutputStream baos3 = new ByteArrayOutputStream(); + final DataOutputStream dos3 = new DataOutputStream(baos3); + dos3.write(1); + dos3.writeUTF(this.user); + final byte[] out = baos3.toByteArray(); + for (final ChannelHandlerContext conn : EaglerVoiceHandler.voicePlayers.values()) { + sendData(conn, out); + } + EaglerVoiceHandler.voicePlayers.put(this.user, ctx); + for (final String username : EaglerVoiceHandler.voicePlayers.keySet()) { + this.sendVoicePlayers(username); + } + bb.release(); + return; + } + bb.release(); + return; + } + case 2: { + if (!EaglerVoiceHandler.voicePlayers.containsKey(this.user)) { + bb.release(); + return; + } + try { + final String targetUser = streamIn.readUTF(); + if (!EaglerVoiceHandler.voicePlayers.containsKey(targetUser)) { + bb.release(); + return; + } + if (EaglerVoiceHandler.voicePairs.removeIf(pair -> (pair[0].equals(this.user) && pair[1].equals(targetUser)) || (pair[0].equals(targetUser) && pair[1].equals(this.user)))) { + ByteArrayOutputStream baos2 = new ByteArrayOutputStream(); + DataOutputStream dos2 = new DataOutputStream(baos2); + dos2.write(2); + dos2.writeUTF(this.user); + sendData(EaglerVoiceHandler.voicePlayers.get(targetUser), baos2.toByteArray()); + baos2 = new ByteArrayOutputStream(); + dos2 = new DataOutputStream(baos2); + dos2.write(2); + dos2.writeUTF(targetUser); + sendData(ctx, baos2.toByteArray()); + break; + } + } catch (EOFException var7) { + this.removeUser(this.user); + } + bb.release(); + return; + } + case 3: + case 4: { + if (EaglerVoiceHandler.voicePlayers.containsKey(this.user)) { + final String username = streamIn.readUTF(); + if (this.checkVoicePair(this.user, username)) { + final String data = streamIn.readUTF(); + final ByteArrayOutputStream baos4 = new ByteArrayOutputStream(); + final DataOutputStream dos4 = new DataOutputStream(baos4); + dos4.write(sig); + dos4.writeUTF(this.user); + dos4.writeUTF(data); + sendData(EaglerVoiceHandler.voicePlayers.get(username), baos4.toByteArray()); + } + bb.release(); + return; + } + bb.release(); + return; + } + default: { + bb.release(); + return; + } + } + } catch (Throwable var8) { + this.removeUser(this.user); + bb.release(); + return; + } + bb.release(); + return; + } + bb.resetReaderIndex(); + } + super.channelRead(ctx, obj); + } + + public void channelInactive(final ChannelHandlerContext ctx) throws Exception { + super.channelInactive(ctx); + this.removeUser(this.user); + } + + public void sendVoicePlayers(final String name) { + if (EaglerVoiceHandler.voicePlayers.containsKey(name)) { + try { + final ByteArrayOutputStream baos = new ByteArrayOutputStream(); + final DataOutputStream dos = new DataOutputStream(baos); + dos.write(5); + final Set mostlyGlobalPlayers = new HashSet<>(); + for (final String username : EaglerVoiceHandler.voicePlayers.keySet()) { + if (!username.equals(name) && EaglerVoiceHandler.voicePairs.stream().noneMatch(pair -> (pair[0].equals(name) && pair[1].equals(username)) || (pair[0].equals(username) && pair[1].equals(name)))) { + mostlyGlobalPlayers.add(username); + } + } + if (mostlyGlobalPlayers.size() > 0) { + dos.writeInt(mostlyGlobalPlayers.size()); + for (final String username : mostlyGlobalPlayers) { + dos.writeUTF(username); + } + sendData(EaglerVoiceHandler.voicePlayers.get(name), baos.toByteArray()); + } + } catch (IOException ignored) { + } + } + } + + public void removeUser(final String name) { + EaglerVoiceHandler.voicePlayers.remove(name); + for (final String username : EaglerVoiceHandler.voicePlayers.keySet()) { + if (!name.equals(username)) { + this.sendVoicePlayers(username); + } + } + for (final String[] voicePair : EaglerVoiceHandler.voicePairs) { + String target = null; + if (voicePair[0].equals(name)) { + target = voicePair[1]; + } else if (voicePair[1].equals(name)) { + target = voicePair[0]; + } + if (target != null && EaglerVoiceHandler.voicePlayers.containsKey(target)) { + try { + final ByteArrayOutputStream baos = new ByteArrayOutputStream(); + final DataOutputStream dos = new DataOutputStream(baos); + dos.write(2); + dos.writeUTF(name); + sendData(EaglerVoiceHandler.voicePlayers.get(target), baos.toByteArray()); + } catch (IOException ignored) { + } + } + } + EaglerVoiceHandler.voicePairs.removeIf(pair -> pair[0].equals(name) || pair[1].equals(name)); + } + + private boolean checkVoicePair(final String user1, final String user2) { + return EaglerVoiceHandler.voicePairs.stream().anyMatch(pair -> (pair[0].equals(user1) && pair[1].equals(user2)) || (pair[0].equals(user2) && pair[1].equals(user1))); + } + + static { + voicePlayers = new ConcurrentHashMap<>(); + voiceRequests = new ConcurrentHashMap<>(); + voicePairs = new CopyOnWriteArraySet<>(); + (iceServers = new ArrayList<>()).add("stun:stun.l.google.com:19302"); + EaglerVoiceHandler.iceServers.add("stun:stun1.l.google.com:19302"); + EaglerVoiceHandler.iceServers.add("stun:stun2.l.google.com:19302"); + EaglerVoiceHandler.iceServers.add("stun:stun3.l.google.com:19302"); + EaglerVoiceHandler.iceServers.add("stun:stun4.l.google.com:19302"); + EaglerVoiceHandler.iceServers.add("stun:openrelay.metered.ca:80"); + final Map turnServerList = new HashMap<>(); + HashMap n = new HashMap<>(); + n.put("url", "turn:openrelay.metered.ca:80"); + n.put("username", "openrelayproject"); + n.put("password", "openrelayproject"); + turnServerList.put("openrelay1", n); + n = new HashMap<>(); + n.put("url", "turn:openrelay.metered.ca:443"); + n.put("username", "openrelayproject"); + n.put("password", "openrelayproject"); + turnServerList.put("openrelay2", n); + n = new HashMap<>(); + n.put("url", "turn:openrelay.metered.ca:443?transport=tcp"); + n.put("username", "openrelayproject"); + n.put("password", "openrelayproject"); + turnServerList.put("openrelay3", n); + for (final Map.Entry trn : turnServerList.entrySet()) { + final Object o = trn.getValue(); + if (o instanceof Map) { + final Map o2 = (Map) o; + EaglerVoiceHandler.iceServers.add("" + o2.get("url") + ";" + o2.get("username") + ";" + o2.get("password")); + } + } + VOICE_ENABLED = AttributeKey.valueOf("ayun-voice-enabled"); + } +} diff --git a/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/EaglerXLoginHandler.java b/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/EaglerXLoginHandler.java new file mode 100644 index 0000000..2191495 --- /dev/null +++ b/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/EaglerXLoginHandler.java @@ -0,0 +1,35 @@ +package me.ayunami2000.ayunViaProxyEagUtils; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelOutboundHandlerAdapter; +import io.netty.channel.ChannelPromise; +import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame; +import net.raphimc.netminecraft.packet.PacketTypes; +import net.raphimc.viaproxy.proxy.util.ExceptionUtil; + +public class EaglerXLoginHandler extends ChannelOutboundHandlerAdapter { + private int counter; + + public EaglerXLoginHandler() { + this.counter = 0; + } + + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + ExceptionUtil.handleNettyException(ctx, cause, null); + } + + public void write(final ChannelHandlerContext ctx, final Object msg, final ChannelPromise promise) throws Exception { + if (msg instanceof BinaryWebSocketFrame) { + final ByteBuf bb = ((BinaryWebSocketFrame) msg).content(); + if (PacketTypes.readVarInt(bb) == 2 && ++this.counter == 2) { + ctx.pipeline().remove("ayun-eag-x-login"); + bb.writerIndex(bb.readerIndex()); + PacketTypes.writeString(bb, ""); + bb.writeByte(2); + } + bb.resetReaderIndex(); + } + super.write(ctx, msg, promise); + } +} diff --git a/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/EaglerXSkinHandler.java b/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/EaglerXSkinHandler.java new file mode 100644 index 0000000..b206803 --- /dev/null +++ b/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/EaglerXSkinHandler.java @@ -0,0 +1,107 @@ +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 net.raphimc.netminecraft.packet.PacketTypes; +import net.raphimc.viaproxy.proxy.util.ExceptionUtil; + +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +public class EaglerXSkinHandler extends ChannelInboundHandlerAdapter { + private final ConcurrentHashMap profileData; + public static final SkinService skinService; + private String user; + private int pluginMessageId; + + public EaglerXSkinHandler() { + this.profileData = new ConcurrentHashMap<>(); + this.pluginMessageId = -1; + } + + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + ExceptionUtil.handleNettyException(ctx, cause, null); + } + + public void channelRead(final ChannelHandlerContext ctx, final Object obj) throws Exception { + final EaglercraftHandler.State state = ((EaglercraftHandler) ctx.pipeline().get("eaglercraft-handler")).state; + if (state == EaglercraftHandler.State.LOGIN && obj instanceof BinaryWebSocketFrame) { + final ByteBuf bb = ((BinaryWebSocketFrame) obj).content(); + if (bb.readUnsignedByte() == 7) { + if (this.profileData.size() > 12) { + ctx.close(); + bb.release(); + return; + } + int strlen = bb.readUnsignedByte(); + final String dataType = bb.readCharSequence(strlen, StandardCharsets.US_ASCII).toString(); + strlen = bb.readUnsignedShort(); + final byte[] readData = new byte[strlen]; + bb.readBytes(readData); + if (bb.isReadable()) { + ctx.close(); + bb.release(); + return; + } + if (this.profileData.containsKey(dataType)) { + ctx.close(); + bb.release(); + return; + } + this.profileData.put(dataType, readData); + } + bb.resetReaderIndex(); + } + if (state != EaglercraftHandler.State.LOGIN_COMPLETE) { + super.channelRead(ctx, obj); + return; + } + 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) { + SkinPackets.registerEaglerPlayerFallback(clientUUID, EaglerXSkinHandler.skinService); + } + } else { + SkinPackets.registerEaglerPlayerFallback(clientUUID, EaglerXSkinHandler.skinService); + } + } + if (this.pluginMessageId <= 0) { + this.pluginMessageId = ((EaglercraftHandler) ctx.pipeline().get("eaglercraft-handler")).pluginMessageId; + } + if (obj instanceof BinaryWebSocketFrame) { + final ByteBuf bb = ((BinaryWebSocketFrame) obj).content(); + try { + if (PacketTypes.readVarInt(bb) == this.pluginMessageId && PacketTypes.readString(bb, 32767).equals("EAG|Skins-1.8")) { + final byte[] data = new byte[bb.readableBytes()]; + bb.readBytes(data); + SkinPackets.processPacket(data, ctx, EaglerXSkinHandler.skinService); + bb.release(); + return; + } + } catch (Exception ignored) { + } + bb.resetReaderIndex(); + } + super.channelRead(ctx, obj); + } + + public void channelInactive(final ChannelHandlerContext ctx) throws Exception { + super.channelInactive(ctx); + if (this.user != null) { + 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 new file mode 100644 index 0000000..c988858 --- /dev/null +++ b/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/EaglercraftHandler.java @@ -0,0 +1,355 @@ +package me.ayunami2000.ayunViaProxyEagUtils; + +import com.google.common.net.HostAndPort; +import com.viaversion.viaversion.libs.gson.JsonArray; +import com.viaversion.viaversion.libs.gson.JsonObject; +import com.viaversion.viaversion.libs.gson.JsonParser; +import com.viaversion.viaversion.protocols.base.ClientboundStatusPackets; +import com.viaversion.viaversion.protocols.base.ServerboundHandshakePackets; +import com.viaversion.viaversion.protocols.base.ServerboundLoginPackets; +import com.viaversion.viaversion.protocols.base.ServerboundStatusPackets; +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.MessageToMessageCodec; +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 net.lenni0451.mcstructs.text.serializer.TextComponentSerializer; +import net.raphimc.netminecraft.constants.ConnectionState; +import net.raphimc.netminecraft.constants.MCPackets; +import net.raphimc.netminecraft.constants.MCPipeline; +import net.raphimc.netminecraft.packet.PacketTypes; +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.ViaProxy; +import net.raphimc.viaproxy.proxy.util.ExceptionUtil; +import net.raphimc.viaproxy.util.logging.Logger; + +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Base64; +import java.util.List; +import java.util.UUID; + +public class EaglercraftHandler extends MessageToMessageCodec { + private HostAndPort host; + public State state; + public VersionEnum version; + public int pluginMessageId; + public String username; + + public EaglercraftHandler() { + this.state = State.PRE_HANDSHAKE; + } + + public void channelActive(final ChannelHandlerContext ctx) throws Exception { + ctx.channel().attr(MCPipeline.COMPRESSION_THRESHOLD_ATTRIBUTE_KEY).set(-2); + ctx.pipeline().remove("sizer"); + super.channelActive(ctx); + } + + protected void encode(final ChannelHandlerContext ctx, final ByteBuf in, final List out) throws IOException { + if (this.state == State.STATUS) { + final int packetId = PacketTypes.readVarInt(in); + if (packetId != ClientboundStatusPackets.STATUS_RESPONSE.getId()) { + throw new IllegalStateException("Unexpected packet id " + packetId); + } + final JsonObject root = JsonParser.parseString(PacketTypes.readString(in, 32767)).getAsJsonObject(); + final JsonObject response = new JsonObject(); + response.addProperty("name", "ViaProxy"); + response.addProperty("brand", "ViaProxy"); + if (root.has("version")) { + response.add("vers", root.getAsJsonObject("version").get("name")); + } else { + response.addProperty("vers", "Unknown"); + } + response.addProperty("cracked", Boolean.TRUE); + response.addProperty("secure", Boolean.FALSE); + response.addProperty("time", System.currentTimeMillis()); + response.addProperty("uuid", UUID.randomUUID().toString()); + response.addProperty("type", "motd"); + final JsonObject data = new JsonObject(); + data.addProperty("cache", Boolean.FALSE); + final JsonArray motd = new JsonArray(); + if (root.has("description")) { + final String[] split = TextComponentSerializer.V1_8.deserialize(root.get("description").toString()).asLegacyFormatString().split("\n"); + for (final String motdLine : split) { + motd.add(motdLine); + } + } + data.add("motd", motd); + data.addProperty("icon", root.has("favicon")); + if (root.has("players")) { + final JsonObject javaPlayers = root.getAsJsonObject("players"); + data.add("online", javaPlayers.get("online")); + data.add("max", javaPlayers.get("max")); + final JsonArray players = new JsonArray(); + if (javaPlayers.has("sample")) { + javaPlayers.getAsJsonArray("sample").forEach(player -> players.add(TextComponentSerializer.V1_8.deserialize(player.getAsJsonObject().get("name").getAsString()).asLegacyFormatString())); + } + data.add("players", players); + } + response.add("data", data); + out.add(new TextWebSocketFrame(response.toString())); + if (root.has("favicon")) { + final BufferedImage icon = ImageIO.read(new ByteArrayInputStream(Base64.getDecoder().decode(root.get("favicon").getAsString().substring(22).replace("\n", "").getBytes(StandardCharsets.UTF_8)))); + final int[] pixels = icon.getRGB(0, 0, 64, 64, null, 0, 64); + final byte[] iconPixels = new byte[16384]; + for (int i = 0; i < 4096; ++i) { + iconPixels[i * 4] = (byte) (pixels[i] >> 16 & 0xFF); + iconPixels[i * 4 + 1] = (byte) (pixels[i] >> 8 & 0xFF); + iconPixels[i * 4 + 2] = (byte) (pixels[i] & 0xFF); + iconPixels[i * 4 + 3] = (byte) (pixels[i] >> 24 & 0xFF); + } + out.add(new BinaryWebSocketFrame(ctx.alloc().buffer().writeBytes(iconPixels))); + } + } else { + if (this.state != State.LOGIN_COMPLETE) { + throw new IllegalStateException("Cannot send packets before login is completed"); + } + out.add(new BinaryWebSocketFrame(in.retain())); + } + } + + protected void decode(final ChannelHandlerContext ctx, final WebSocketFrame in, final List out) throws Exception { + if (in instanceof BinaryWebSocketFrame) { + final ByteBuf data = in.content(); + switch (this.state) { + case PRE_HANDSHAKE: { + if (data.readableBytes() >= 2 && data.getByte(0) == 2 && data.getByte(1) == 69) { + data.setByte(1, 61); + this.state = State.LOGIN_COMPLETE; + this.version = VersionEnum.r1_5_2; + out.add(data.retain()); + break; + } + this.state = State.HANDSHAKE; + } + case HANDSHAKE: { + final int packetId = data.readUnsignedByte(); + if (packetId != 1) { + throw new IllegalStateException("Unexpected packet id " + packetId + " in state " + this.state); + } + int eaglercraftVersion = data.readUnsignedByte(); + int minecraftVersion; + if (eaglercraftVersion == 1) { + minecraftVersion = data.readUnsignedByte(); + } else { + if (eaglercraftVersion != 2) { + throw new IllegalArgumentException("Unknown Eaglercraft version: " + eaglercraftVersion); + } + int count = data.readUnsignedShort(); + final List eaglercraftVersions = new ArrayList<>(count); + for (int i = 0; i < count; ++i) { + eaglercraftVersions.add(data.readUnsignedShort()); + } + if (!eaglercraftVersions.contains(2) && !eaglercraftVersions.contains(3)) { + Logger.LOGGER.error("No supported eaglercraft versions found"); + ctx.close(); + return; + } + if (eaglercraftVersions.contains(3)) { + eaglercraftVersion = 3; + } + count = data.readUnsignedShort(); + final List minecraftVersions = new ArrayList<>(count); + for (int j = 0; j < count; ++j) { + minecraftVersions.add(data.readUnsignedShort()); + } + if (minecraftVersions.size() != 1) { + Logger.LOGGER.error("No supported minecraft versions found"); + ctx.close(); + } + minecraftVersion = minecraftVersions.get(0); + } + final String clientBrand = data.readCharSequence(data.readUnsignedByte(), StandardCharsets.US_ASCII).toString(); + final String clientVersionString = data.readCharSequence(data.readUnsignedByte(), StandardCharsets.US_ASCII).toString(); + if (eaglercraftVersion >= 2) { + data.skipBytes(1); + data.skipBytes(data.readUnsignedByte()); + } + if (data.isReadable()) { + throw new IllegalArgumentException("Too much data in packet: " + data.readableBytes() + " bytes"); + } + Logger.LOGGER.info("Eaglercraft client connected: " + clientBrand + " " + clientVersionString); + this.state = State.HANDSHAKE_COMPLETE; + this.version = VersionEnum.fromProtocolId(minecraftVersion); + if (this.version.equals(VersionEnum.UNKNOWN)) { + Logger.LOGGER.error("Unsupported protocol version: " + minecraftVersion); + ctx.close(); + return; + } + final ByteBuf response = ctx.alloc().buffer(); + response.writeByte(2); + if (eaglercraftVersion == 1) { + response.writeByte(1); + } else { + response.writeShort(eaglercraftVersion); + response.writeShort(minecraftVersion); + } + response.writeByte("ViaProxy".length()).writeCharSequence("ViaProxy", StandardCharsets.US_ASCII); + response.writeByte(ViaProxy.VERSION.length()).writeCharSequence(ViaProxy.VERSION, StandardCharsets.US_ASCII); + response.writeByte(0); + response.writeShort(0); + ctx.writeAndFlush(new BinaryWebSocketFrame(response)); + break; + } + case HANDSHAKE_COMPLETE: { + final int packetId = data.readUnsignedByte(); + if (packetId != 4) { + throw new IllegalStateException("Unexpected packet id " + packetId + " in state " + this.state); + } + final String username = data.readCharSequence(data.readUnsignedByte(), StandardCharsets.US_ASCII).toString(); + data.readCharSequence(data.readUnsignedByte(), StandardCharsets.US_ASCII).toString(); + data.skipBytes(data.readUnsignedByte()); + if (data.isReadable()) { + throw new IllegalArgumentException("Too much data in packet: " + data.readableBytes() + " bytes"); + } + this.state = State.LOGIN; + this.username = username; + final UUID uuid = UUID.nameUUIDFromBytes(("OfflinePlayer:" + username).getBytes(StandardCharsets.UTF_8)); + final ByteBuf response2 = ctx.alloc().buffer(); + response2.writeByte(5); + response2.writeByte(username.length()).writeCharSequence(username, StandardCharsets.US_ASCII); + response2.writeLong(uuid.getMostSignificantBits()).writeLong(uuid.getLeastSignificantBits()); + ctx.writeAndFlush(new BinaryWebSocketFrame(response2)); + break; + } + case LOGIN: { + final int packetId = data.readUnsignedByte(); + if (packetId == 7) { + data.readCharSequence(data.readUnsignedByte(), StandardCharsets.US_ASCII).toString(); + final byte[] dataBytes = new byte[data.readUnsignedShort()]; + data.readBytes(dataBytes); + if (data.isReadable()) { + throw new IllegalArgumentException("Too much data in packet: " + data.readableBytes() + " bytes"); + } + } else { + if (packetId != 8) { + throw new IllegalStateException("Unexpected packet id " + packetId + " in state " + this.state); + } + if (data.isReadable()) { + throw new IllegalArgumentException("Too much data in packet: " + data.readableBytes() + " bytes"); + } + this.state = State.LOGIN_COMPLETE; + this.pluginMessageId = MCPackets.C2S_PLUGIN_MESSAGE.getId(this.version.getVersion()); + if (this.pluginMessageId == -1) { + Logger.LOGGER.error("Unsupported protocol version: " + this.version.getVersion()); + ctx.close(); + return; + } + if (ctx.pipeline().get("legacy-passthrough-handler") != null) { + ctx.pipeline().remove("legacy-passthrough-handler"); + } + out.add(this.writeHandshake(ctx.alloc().buffer(), ConnectionState.LOGIN)); + final ByteBuf loginHello = ctx.alloc().buffer(); + PacketTypes.writeVarInt(loginHello, ServerboundLoginPackets.HELLO.getId()); + PacketTypes.writeString(loginHello, this.username); + out.add(loginHello); + final ByteBuf response3 = ctx.alloc().buffer(); + response3.writeByte(9); + ctx.writeAndFlush(new BinaryWebSocketFrame(response3)); + } + break; + } + case LOGIN_COMPLETE: { + if (this.version.equals(VersionEnum.r1_5_2)) { + final int packetId = data.readUnsignedByte(); + if (packetId == ServerboundPackets1_5_2.SHARED_KEY.getId()) { + ctx.channel().writeAndFlush(new BinaryWebSocketFrame(data.readerIndex(0).retain())); + break; + } + if (packetId == ServerboundPackets1_5_2.PLUGIN_MESSAGE.getId() && Types1_6_4.STRING.read(data).startsWith("EAG|")) { + break; + } + } else if (this.version.isNewerThanOrEqualTo(VersionEnum.r1_7_2tor1_7_5)) { + final int packetId = PacketTypes.readVarInt(data); + if (packetId == this.pluginMessageId && PacketTypes.readString(data, 32767).startsWith("EAG|")) { + break; + } + } + out.add(data.readerIndex(0).retain()); + break; + } + default: { + throw new IllegalStateException("Unexpected binary frame in state " + this.state); + } + } + } else { + if (!(in instanceof TextWebSocketFrame)) { + throw new UnsupportedOperationException("Unsupported frame type: " + in.getClass().getName()); + } + final String text = ((TextWebSocketFrame) in).text(); + if (this.state != State.PRE_HANDSHAKE) { + throw new IllegalStateException("Unexpected text frame in state " + this.state); + } + if (!text.equalsIgnoreCase("accept: motd")) { + ctx.close(); + return; + } + this.state = State.STATUS; + this.version = VersionEnum.r1_8; + if (ctx.pipeline().get("legacy-passthrough-handler") != null) { + ctx.pipeline().remove("legacy-passthrough-handler"); + } + out.add(this.writeHandshake(ctx.alloc().buffer(), ConnectionState.STATUS)); + final ByteBuf statusRequest = ctx.alloc().buffer(); + PacketTypes.writeVarInt(statusRequest, ServerboundStatusPackets.STATUS_REQUEST.getId()); + out.add(statusRequest); + } + } + + public void userEventTriggered(final ChannelHandlerContext ctx, final Object evt) throws Exception { + if (evt instanceof WebSocketServerProtocolHandler.HandshakeComplete) { + final WebSocketServerProtocolHandler.HandshakeComplete handshake = (WebSocketServerProtocolHandler.HandshakeComplete) evt; + if (!handshake.requestHeaders().contains("Host")) { + ctx.close(); + return; + } + this.host = HostAndPort.fromString(handshake.requestHeaders().get("Host")).withDefaultPort(80); + } + super.userEventTriggered(ctx, evt); + } + + private ByteBuf writeHandshake(final ByteBuf byteBuf, final ConnectionState state) { + PacketTypes.writeVarInt(byteBuf, ServerboundHandshakePackets.CLIENT_INTENTION.getId()); + PacketTypes.writeVarInt(byteBuf, this.version.getVersion()); + PacketTypes.writeString(byteBuf, this.host.getHost()); + byteBuf.writeShort(this.host.getPort()); + int i; + switch (state) { + case PLAY: + i = 0; + break; + case STATUS: + i = 1; + break; + case LOGIN: + i = 2; + break; + default: + i = -1; + } + PacketTypes.writeVarInt(byteBuf, i); + return byteBuf; + } + + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + ExceptionUtil.handleNettyException(ctx, cause, null); + } + + public enum State { + STATUS, + PRE_HANDSHAKE, + HANDSHAKE, + HANDSHAKE_COMPLETE, + LOGIN, + LOGIN_COMPLETE + } +} diff --git a/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/EaglercraftInitialHandler.java b/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/EaglercraftInitialHandler.java new file mode 100644 index 0000000..038404b --- /dev/null +++ b/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/EaglercraftInitialHandler.java @@ -0,0 +1,70 @@ +package me.ayunami2000.ayunViaProxyEagUtils; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.ByteToMessageDecoder; +import io.netty.handler.codec.http.HttpObjectAggregator; +import io.netty.handler.codec.http.HttpServerCodec; +import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler; +import io.netty.handler.codec.http.websocketx.extensions.compression.WebSocketServerCompressionHandler; +import io.netty.handler.ssl.SslContext; +import io.netty.handler.ssl.SslContextBuilder; +import net.raphimc.viaproxy.proxy.util.ExceptionUtil; + +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.util.List; + +public class EaglercraftInitialHandler extends ByteToMessageDecoder { + private static SslContext sslContext; + + protected void decode(final ChannelHandlerContext ctx, final ByteBuf in, final List out) { + if (!ctx.channel().isOpen()) { + return; + } + if (!in.isReadable()) { + return; + } + if (in.readableBytes() >= 3 || in.getByte(0) != 71) { + if (in.readableBytes() >= 3 && in.getCharSequence(0, 3, StandardCharsets.UTF_8).equals("GET")) { + if (EaglercraftInitialHandler.sslContext != null) { + ctx.pipeline().addBefore("eaglercraft-initial-handler", "ws-ssl-handler", EaglercraftInitialHandler.sslContext.newHandler(ctx.alloc())); + } + ctx.pipeline().addBefore("eaglercraft-initial-handler", "ws-http-codec", new HttpServerCodec()); + ctx.pipeline().addBefore("eaglercraft-initial-handler", "ws-http-aggregator", new HttpObjectAggregator(65535, true)); + ctx.pipeline().addBefore("eaglercraft-initial-handler", "ws-compression", new WebSocketServerCompressionHandler()); + 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.fireUserEventTriggered(EaglercraftClientConnected.INSTANCE); + ctx.pipeline().fireChannelRead(in.readBytes(in.readableBytes())); + } else { + out.add(in.readBytes(in.readableBytes())); + } + ctx.pipeline().remove(this); + } + } + + static { + final File certFolder = new File("certs"); + if (certFolder.exists()) { + try { + EaglercraftInitialHandler.sslContext = SslContextBuilder.forServer(new File(certFolder, "fullchain.pem"), new File(certFolder, "privkey.pem")).build(); + } catch (Throwable e) { + throw new RuntimeException("Failed to load SSL context", e); + } + } + } + + public static final class EaglercraftClientConnected { + public static final EaglercraftClientConnected INSTANCE; + + static { + INSTANCE = new EaglercraftClientConnected(); + } + } + + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + ExceptionUtil.handleNettyException(ctx, cause, null); + } +} diff --git a/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/ExpiringSet.java b/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/ExpiringSet.java new file mode 100644 index 0000000..a8875b7 --- /dev/null +++ b/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/ExpiringSet.java @@ -0,0 +1,78 @@ +package me.ayunami2000.ayunViaProxyEagUtils; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; + +public class ExpiringSet extends HashSet { + private final long expiration; + private final ExpiringEvent event; + private final Map timestamps; + + public ExpiringSet(final long expiration) { + this.timestamps = new HashMap<>(); + this.expiration = expiration; + this.event = null; + } + + public ExpiringSet(final long expiration, final ExpiringEvent event) { + this.timestamps = new HashMap<>(); + this.expiration = expiration; + this.event = event; + } + + public void checkForExpirations() { + final Iterator iterator = this.timestamps.keySet().iterator(); + final long now = System.currentTimeMillis(); + while (iterator.hasNext()) { + final T element = iterator.next(); + if (super.contains(element)) { + if (this.timestamps.get(element) + this.expiration >= now) { + continue; + } + if (this.event != null) { + this.event.onExpiration(element); + } + } + iterator.remove(); + super.remove(element); + } + } + + @Override + public boolean add(final T o) { + this.checkForExpirations(); + final boolean success = super.add(o); + if (success) { + this.timestamps.put(o, System.currentTimeMillis()); + } + return success; + } + + @Override + public boolean remove(final Object o) { + this.checkForExpirations(); + final boolean success = super.remove(o); + if (success) { + this.timestamps.remove(o); + } + return success; + } + + @Override + public void clear() { + this.timestamps.clear(); + super.clear(); + } + + @Override + public boolean contains(final Object o) { + this.checkForExpirations(); + return super.contains(o); + } + + public interface ExpiringEvent { + void onExpiration(final T p0); + } +} diff --git a/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/Main.java b/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/Main.java new file mode 100644 index 0000000..4ad9681 --- /dev/null +++ b/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/Main.java @@ -0,0 +1,67 @@ +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 net.lenni0451.lambdaevents.EventHandler; +import net.raphimc.vialegacy.protocols.release.protocol1_7_2_5to1_6_4.types.Types1_6_4; +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.types.ITyped; +import net.raphimc.viaproxy.proxy.util.ExceptionUtil; + +public class Main extends ViaProxyPlugin { + public void onEnable() { + PluginManager.EVENT_MANAGER.register(this); + } + + @EventHandler + public void onEvent(final Client2ProxyChannelInitializeEvent event) { + if (event.getType() == ITyped.Type.PRE) { + event.getChannel().pipeline().addLast("eaglercraft-initial-handler", new EaglercraftInitialHandler()); + } + if (event.getType() == ITyped.Type.POST) { + event.getChannel().pipeline().addAfter("eaglercraft-initial-handler", "ayun-eag-detector", new EaglerConnectionHandler()); + } + } + + static class EaglerConnectionHandler extends ChannelInboundHandlerAdapter { + public void userEventTriggered(final ChannelHandlerContext ctx, final Object evt) throws Exception { + super.userEventTriggered(ctx, evt); + if (evt instanceof EaglercraftInitialHandler.EaglercraftClientConnected) { + ctx.pipeline().remove("ayun-eag-detector"); + ctx.pipeline().addBefore("eaglercraft-handler", "ayun-eag-utils-init", new EaglerUtilsInitHandler()); + } + } + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + ExceptionUtil.handleNettyException(ctx, cause, null); + } + } + + static class EaglerUtilsInitHandler extends ChannelInboundHandlerAdapter { + public void channelRead(final ChannelHandlerContext ctx, final Object msg) throws Exception { + if (msg instanceof BinaryWebSocketFrame) { + final ByteBuf bb = ((BinaryWebSocketFrame) msg).content(); + try { + if (bb.readByte() == 2 && bb.readByte() == 69) { + final String username = Types1_6_4.STRING.read(bb); + ctx.pipeline().addBefore("eaglercraft-handler", "ayun-eag-voice", new EaglerVoiceHandler(username)); + ctx.pipeline().addBefore("eaglercraft-handler", "ayun-eag-skin", new EaglerSkinHandler(username)); + } else { + ctx.pipeline().addBefore("eaglercraft-handler", "ayun-eag-x-login", new EaglerXLoginHandler()); + ctx.pipeline().addBefore("eaglercraft-handler", "ayun-eag-skin-x", new EaglerXSkinHandler()); + } + } catch (Exception ignored) { + } + bb.resetReaderIndex(); + } + ctx.pipeline().remove("ayun-eag-utils-init"); + super.channelRead(ctx, msg); + } + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + ExceptionUtil.handleNettyException(ctx, cause, null); + } + } +} diff --git a/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/SkinConverter.java b/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/SkinConverter.java new file mode 100644 index 0000000..3612538 --- /dev/null +++ b/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/SkinConverter.java @@ -0,0 +1,42 @@ +package me.ayunami2000.ayunViaProxyEagUtils; + +public class SkinConverter { + public static void convert64x32to64x64(final int[] skinIn, final int[] skinOut) { + copyRawPixels(skinIn, skinOut, 0, 0, 0, 0, 64, 32, false); + copyRawPixels(skinIn, skinOut, 24, 48, 20, 4, 16, 8, 20); + copyRawPixels(skinIn, skinOut, 28, 48, 24, 8, 16, 12, 20); + copyRawPixels(skinIn, skinOut, 20, 52, 16, 8, 20, 12, 32); + copyRawPixels(skinIn, skinOut, 24, 52, 20, 4, 20, 8, 32); + copyRawPixels(skinIn, skinOut, 28, 52, 24, 0, 20, 4, 32); + copyRawPixels(skinIn, skinOut, 32, 52, 28, 12, 20, 16, 32); + copyRawPixels(skinIn, skinOut, 40, 48, 36, 44, 16, 48, 20); + copyRawPixels(skinIn, skinOut, 44, 48, 40, 48, 16, 52, 20); + copyRawPixels(skinIn, skinOut, 36, 52, 32, 48, 20, 52, 32); + copyRawPixels(skinIn, skinOut, 40, 52, 36, 44, 20, 48, 32); + copyRawPixels(skinIn, skinOut, 44, 52, 40, 40, 20, 44, 32); + copyRawPixels(skinIn, skinOut, 48, 52, 44, 52, 20, 56, 32); + } + + private static void copyRawPixels(final int[] imageIn, final int[] imageOut, final int dx1, final int dy1, final int dx2, final int sx1, final int sy1, final int sx2, final int sy2) { + if (dx1 > dx2) { + copyRawPixels(imageIn, imageOut, sx1, sy1, dx2, dy1, sx2 - sx1, sy2 - sy1, true); + } else { + copyRawPixels(imageIn, imageOut, sx1, sy1, dx1, dy1, sx2 - sx1, sy2 - sy1, false); + } + } + + private static void copyRawPixels(final int[] imageIn, final int[] imageOut, final int srcX, final int srcY, final int dstX, final int dstY, final int width, final int height, final boolean flip) { + for (int y = 0; y < height; ++y) { + for (int x = 0; x < width; ++x) { + final int i = imageIn[(srcY + y) * 64 + srcX + x]; + int j; + if (flip) { + j = (dstY + y) * 64 + dstX + width - x - 1; + } else { + j = (dstY + y) * 64 + dstX + x; + } + imageOut[j] = i; + } + } + } +} diff --git a/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/SkinPackets.java b/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/SkinPackets.java new file mode 100644 index 0000000..c67899b --- /dev/null +++ b/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/SkinPackets.java @@ -0,0 +1,146 @@ +package me.ayunami2000.ayunViaProxyEagUtils; + +import io.netty.channel.ChannelHandlerContext; + +import java.io.IOException; +import java.util.UUID; + +public class SkinPackets { + public static void processPacket(final byte[] data, final ChannelHandlerContext sender, final SkinService skinService) throws IOException { + if (data.length == 0) { + throw new IOException("Zero-length packet recieved"); + } + final int packetId = data[0] & 0xFF; + try { + switch (packetId) { + case 3: { + processGetOtherSkin(data, sender, skinService); + break; + } + case 6: { + processGetOtherSkinByURL(); + break; + } + default: { + throw new IOException("Unknown packet type " + packetId); + } + } + } catch (IOException ex) { + throw ex; + } catch (Throwable t) { + throw new IOException("Unhandled exception handling packet type " + packetId, t); + } + } + + private static void processGetOtherSkin(final byte[] data, final ChannelHandlerContext sender, final SkinService skinService) throws IOException { + if (data.length != 17) { + throw new IOException("Invalid length " + data.length + " for skin request packet"); + } + final UUID searchUUID = bytesToUUID(data, 1); + skinService.processGetOtherSkin(searchUUID, sender); + } + + private static void processGetOtherSkinByURL() throws IOException { + throw new IOException("Skin URLs not implemented"); + } + + public static void registerEaglerPlayer(final UUID clientUUID, final byte[] bs, final SkinService skinService) throws IOException { + if (bs.length == 0) { + throw new IOException("Zero-length packet recieved"); + } + int skinModel = -1; + final int packetType = bs[0] & 0xFF; + byte[] generatedPacket; + switch (packetType) { + case 1: { + if (bs.length != 5) { + throw new IOException("Invalid length " + bs.length + " for preset skin packet"); + } + generatedPacket = makePresetResponse(clientUUID, bs[1] << 24 | bs[2] << 16 | bs[3] << 8 | (bs[4] & 0xFF)); + break; + } + case 2: { + final byte[] pixels = new byte[16384]; + if (bs.length != 2 + pixels.length) { + throw new IOException("Invalid length " + bs.length + " for custom skin packet"); + } + setAlphaForChest(pixels, (byte) (-1)); + System.arraycopy(bs, 2, pixels, 0, pixels.length); + generatedPacket = makeCustomResponse(clientUUID, skinModel = (bs[1] & 0xFF), pixels); + break; + } + default: { + throw new IOException("Unknown skin packet type: " + packetType); + } + } + skinService.registerEaglercraftPlayer(clientUUID, generatedPacket); + } + + public static void registerEaglerPlayerFallback(final UUID clientUUID, final SkinService skinService) throws IOException { + final int skinModel = ((clientUUID.hashCode() & 0x1) != 0x0) ? 1 : 0; + final byte[] generatedPacket = makePresetResponse(clientUUID, skinModel); + skinService.registerEaglercraftPlayer(clientUUID, generatedPacket); + } + + public static void setAlphaForChest(final byte[] skin64x64, final byte alpha) { + if (skin64x64.length != 16384) { + throw new IllegalArgumentException("Skin is not 64x64!"); + } + for (int y = 20; y < 32; ++y) { + for (int x = 16; x < 40; ++x) { + skin64x64[y << 8 | x << 2] = alpha; + } + } + } + + public static byte[] makePresetResponse(final UUID uuid) { + return makePresetResponse(uuid, ((uuid.hashCode() & 0x1) != 0x0) ? 1 : 0); + } + + public static byte[] makePresetResponse(final UUID uuid, final int presetId) { + final byte[] ret = new byte[21]; + ret[0] = 4; + UUIDToBytes(uuid, ret, 1); + ret[17] = (byte) (presetId >> 24); + ret[18] = (byte) (presetId >> 16); + ret[19] = (byte) (presetId >> 8); + ret[20] = (byte) (presetId & 0xFF); + return ret; + } + + public static byte[] makeCustomResponse(final UUID uuid, final int model, final byte[] pixels) { + final byte[] ret = new byte[18 + pixels.length]; + ret[0] = 5; + UUIDToBytes(uuid, ret, 1); + ret[17] = (byte) model; + System.arraycopy(pixels, 0, ret, 18, pixels.length); + return ret; + } + + public static UUID bytesToUUID(final byte[] bytes, final int off) { + final long msb = ((long) bytes[off] & 0xFFL) << 56 | ((long) bytes[off + 1] & 0xFFL) << 48 | ((long) bytes[off + 2] & 0xFFL) << 40 | ((long) bytes[off + 3] & 0xFFL) << 32 | ((long) bytes[off + 4] & 0xFFL) << 24 | ((long) bytes[off + 5] & 0xFFL) << 16 | ((long) bytes[off + 6] & 0xFFL) << 8 | ((long) bytes[off + 7] & 0xFFL); + final long lsb = ((long) bytes[off + 8] & 0xFFL) << 56 | ((long) bytes[off + 9] & 0xFFL) << 48 | ((long) bytes[off + 10] & 0xFFL) << 40 | ((long) bytes[off + 11] & 0xFFL) << 32 | ((long) bytes[off + 12] & 0xFFL) << 24 | ((long) bytes[off + 13] & 0xFFL) << 16 | ((long) bytes[off + 14] & 0xFFL) << 8 | ((long) bytes[off + 15] & 0xFFL); + return new UUID(msb, lsb); + } + + public static void UUIDToBytes(final UUID uuid, final byte[] bytes, final int off) { + final long msb = uuid.getMostSignificantBits(); + final long lsb = uuid.getLeastSignificantBits(); + bytes[off] = (byte) (msb >> 56); + bytes[off + 1] = (byte) (msb >> 48); + bytes[off + 2] = (byte) (msb >> 40); + bytes[off + 3] = (byte) (msb >> 32); + bytes[off + 4] = (byte) (msb >> 24); + bytes[off + 5] = (byte) (msb >> 16); + bytes[off + 6] = (byte) (msb >> 8); + bytes[off + 7] = (byte) (msb & 0xFFL); + bytes[off + 8] = (byte) (lsb >> 56); + bytes[off + 9] = (byte) (lsb >> 48); + bytes[off + 10] = (byte) (lsb >> 40); + bytes[off + 11] = (byte) (lsb >> 32); + bytes[off + 12] = (byte) (lsb >> 24); + bytes[off + 13] = (byte) (lsb >> 16); + bytes[off + 14] = (byte) (lsb >> 8); + bytes[off + 15] = (byte) (lsb & 0xFFL); + } +} diff --git a/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/SkinService.java b/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/SkinService.java new file mode 100644 index 0000000..91c3508 --- /dev/null +++ b/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/SkinService.java @@ -0,0 +1,129 @@ +package me.ayunami2000.ayunViaProxyEagUtils; + +import com.google.common.primitives.Ints; +import io.netty.buffer.ByteBuf; +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 javax.imageio.ImageIO; +import java.awt.image.DataBufferByte; +import java.io.IOException; +import java.io.InputStream; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +public class SkinService { + private final ConcurrentHashMap skinCache; + + public SkinService() { + this.skinCache = new ConcurrentHashMap<>(); + } + + private static void sendData(final ChannelHandlerContext ctx, final byte[] data) { + final ByteBuf bb = ctx.alloc().buffer(); + PacketTypes.writeVarInt(bb, MCPackets.S2C_PLUGIN_MESSAGE.getId((((EaglercraftHandler) ctx.pipeline().get("eaglercraft-handler")).version).getVersion())); + PacketTypes.writeString(bb, "EAG|Skins-1.8"); + bb.writeBytes(data); + ctx.writeAndFlush(new BinaryWebSocketFrame(bb)); + } + + public void processGetOtherSkin(final UUID searchUUID, final ChannelHandlerContext sender) { + final CachedSkin cached = this.skinCache.get(searchUUID); + if (cached != null) { + sendData(sender, cached.packet); + } else if (EaglerSkinHandler.skinCollection.containsKey(searchUUID)) { + final byte[] src = EaglerSkinHandler.skinCollection.get(searchUUID); + byte[] res = new byte[src.length - 1]; + System.arraycopy(src, 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; + } + } + sendData(sender, SkinPackets.makeCustomResponse(searchUUID, 0, res)); + } else { + sendData(sender, SkinPackets.makePresetResponse(searchUUID)); + } + } + + public void registerEaglercraftPlayer(final UUID clientUUID, final byte[] generatedPacket) throws IOException { + this.skinCache.put(clientUUID, new CachedSkin(clientUUID, generatedPacket)); + EaglerSkinHandler.skinCollection.put(clientUUID, newToOldSkin(generatedPacket)); + } + + private static byte[] newToOldSkin(final byte[] packet) throws IOException { + final byte type = packet[0]; + byte[] res; + switch (type) { + case 4: { + res = new byte[16385]; + res[0] = 1; + final int presetId = packet[17] << 24 | packet[18] << 16 | packet[19] << 8 | packet[20]; + final InputStream stream = Main.class.getResourceAsStream("/" + presetId + ".png"); + if (stream == null) { + throw new IOException("Invalid skin preset: " + presetId); + } + System.arraycopy(((DataBufferByte) ImageIO.read(stream).getRaster().getDataBuffer()).getData(), 0, res, 1, 16384); + for (int i = 1; i < 16385; i += 4) { + final byte tmp = res[i]; + res[i] = res[i + 1]; + res[i + 1] = res[i + 2]; + res[i + 2] = res[i + 3]; + res[i + 3] = tmp; + } + break; + } + case 5: { + res = new byte[16385]; + res[0] = 1; + System.arraycopy(packet, 18, res, 1, 16384); + for (int i = 1; i < 16385; i += 4) { + final byte tmp = res[i]; + res[i] = res[i + 1]; + res[i + 1] = res[i + 2]; + res[i + 2] = res[i + 3]; + res[i + 3] = tmp; + } + break; + } + default: { + throw new IOException("Invalid skin packet type: " + type); + } + } + return res; + } + + public void unregisterPlayer(final UUID clientUUID) { + this.skinCache.remove(clientUUID); + EaglerSkinHandler.skinCollection.remove(clientUUID); + } + + private static class CachedSkin { + protected final UUID uuid; + protected final byte[] packet; + + protected CachedSkin(final UUID uuid, final byte[] packet) { + this.uuid = uuid; + this.packet = packet; + } + } +} diff --git a/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/WebSocketActiveNotifier.java b/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/WebSocketActiveNotifier.java new file mode 100644 index 0000000..ac50aa3 --- /dev/null +++ b/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/WebSocketActiveNotifier.java @@ -0,0 +1,23 @@ +package me.ayunami2000.ayunViaProxyEagUtils; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler; +import net.raphimc.viaproxy.proxy.util.ExceptionUtil; + +public class WebSocketActiveNotifier extends ChannelInboundHandlerAdapter { + public void channelActive(final ChannelHandlerContext ctx) { + } + + public void userEventTriggered(final ChannelHandlerContext ctx, final Object evt) throws Exception { + if (evt instanceof WebSocketServerProtocolHandler.HandshakeComplete) { + ctx.fireChannelActive(); + ctx.pipeline().remove(this); + } + super.userEventTriggered(ctx, evt); + } + + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + ExceptionUtil.handleNettyException(ctx, cause, null); + } +} diff --git a/src/main/resources/viaproxy.yml b/src/main/resources/viaproxy.yml new file mode 100644 index 0000000..ebc7bbe --- /dev/null +++ b/src/main/resources/viaproxy.yml @@ -0,0 +1,4 @@ +name: ViaProxyEagUtils +version: 1.0 +author: ayunami2000 +main: me.ayunami2000.ayunViaProxyEagUtils.Main