From 8a154e833feb7258658d44564365861d390ddb48 Mon Sep 17 00:00:00 2001 From: ayunami2000 Date: Tue, 26 Sep 2023 14:14:49 -0400 Subject: [PATCH] Add optional online-mode skin support --- .../EaglerSkinHandler.java | 56 ++++++++- .../EaglerVoiceHandler.java | 24 ++-- .../EaglerXLoginHandler.java | 3 + .../EaglerXSkinHandler.java | 5 +- .../EaglercraftHandler.java | 4 +- .../EaglercraftInitialHandler.java | 2 + .../ayunViaProxyEagUtils/ExpiringSet.java | 50 ++++---- .../ayunViaProxyEagUtils/FunnyConfig.java | 39 ++++++ .../ayunViaProxyEagUtils/Main.java | 6 + .../ayunViaProxyEagUtils/SkinPackets.java | 62 +++++++++- .../ayunViaProxyEagUtils/SkinService.java | 114 +++++++++++++++++- .../WebSocketActiveNotifier.java | 3 + src/main/resources/eaglerskins.yml | 2 + 13 files changed, 320 insertions(+), 50 deletions(-) create mode 100644 src/main/java/me/ayunami2000/ayunViaProxyEagUtils/FunnyConfig.java create mode 100644 src/main/resources/eaglerskins.yml diff --git a/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/EaglerSkinHandler.java b/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/EaglerSkinHandler.java index b791291..07c0489 100644 --- a/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/EaglerSkinHandler.java +++ b/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/EaglerSkinHandler.java @@ -1,17 +1,26 @@ package me.ayunami2000.ayunViaProxyEagUtils; +import com.google.gson.JsonObject; +import com.viaversion.viaversion.util.GsonUtil; 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.vialoader.util.VersionEnum; +import net.raphimc.viaproxy.ViaProxy; +import net.raphimc.viaproxy.cli.options.Options; import net.raphimc.viaproxy.proxy.util.ExceptionUtil; +import javax.imageio.ImageIO; +import java.awt.image.DataBufferByte; import java.io.ByteArrayOutputStream; import java.io.DataOutputStream; import java.io.IOException; +import java.io.InputStreamReader; +import java.net.URL; +import java.net.URLConnection; import java.nio.charset.StandardCharsets; -import java.util.HashMap; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; @@ -41,10 +50,12 @@ public class EaglerSkinHandler extends ChannelInboundHandlerAdapter { this.user = username; } + @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { ExceptionUtil.handleNettyException(ctx, cause, null); } + @Override 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()) { @@ -102,6 +113,48 @@ public class EaglerSkinHandler extends ChannelInboundHandlerAdapter { conc = conc2; } sendData(ctx, "EAG|UserSkin", conc); + } else if (EaglerXSkinHandler.skinService.loadPremiumSkins) { + try { + URL url = new URL("https://playerdb.co/api/player/minecraft/" + fetch); + URLConnection urlConnection = url.openConnection(); + urlConnection.setRequestProperty("user-agent", "Mozilla/5.0 ViaProxy/" + ViaProxy.VERSION); + JsonObject json = GsonUtil.getGson().fromJson(new InputStreamReader(urlConnection.getInputStream()), JsonObject.class); + if (json.get("success").getAsBoolean()) { + String premiumUUID = json.getAsJsonObject("data").getAsJsonObject("player").getAsJsonObject("meta").get("id").getAsString(); + byte[] tmp = EaglerXSkinHandler.skinService.fetchSkinPacket(uuidFetch, "https://crafatar.com/skins/" + premiumUUID); + if (tmp != null) { + EaglerXSkinHandler.skinService.registerEaglercraftPlayer(uuidFetch, tmp); + 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; + } else { + try { + tmp = ((DataBufferByte) ImageIO.read(new URL("https://crafatar.com/capes/" + premiumUUID)).getRaster().getDataBuffer()).getData(); + data = new byte[4098]; + data[0] = data[1] = 0; + // todo: figure out if we need to shuffle around colors + System.arraycopy(tmp, 0, data, 2, tmp.length); + EaglerSkinHandler.capeCollection.put(uuid, data); + 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; + } catch (Exception ignored) { + } + } + sendData(ctx, "EAG|UserSkin", conc); + } + } + } + } catch (Exception ignored) { + } } } bb.release(); @@ -143,6 +196,7 @@ public class EaglerSkinHandler extends ChannelInboundHandlerAdapter { super.channelRead(ctx, obj); } + @Override public void channelInactive(final ChannelHandlerContext ctx) throws Exception { super.channelInactive(ctx); final UUID uuid = UUID.nameUUIDFromBytes(("OfflinePlayer:" + this.user).getBytes(StandardCharsets.UTF_8)); diff --git a/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/EaglerVoiceHandler.java b/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/EaglerVoiceHandler.java index e63af27..46da498 100644 --- a/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/EaglerVoiceHandler.java +++ b/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/EaglerVoiceHandler.java @@ -38,12 +38,14 @@ public class EaglerVoiceHandler extends ChannelInboundHandlerAdapter { this.user = username; } + @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { ExceptionUtil.handleNettyException(ctx, cause, null); } + @Override 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)) { + if (((EaglercraftHandler) ctx.pipeline().get("eaglercraft-handler")).state == EaglercraftHandler.State.LOGIN_COMPLETE && !ctx.channel().hasAttr(EaglerVoiceHandler.VOICE_ENABLED)) { final ByteArrayOutputStream baos = new ByteArrayOutputStream(); final DataOutputStream dos = new DataOutputStream(baos); dos.write(0); @@ -54,7 +56,7 @@ public class EaglerVoiceHandler extends ChannelInboundHandlerAdapter { } sendData(ctx, baos.toByteArray()); this.sendVoicePlayers(this.user); - ctx.channel().attr((AttributeKey) EaglerVoiceHandler.VOICE_ENABLED).set(true); + ctx.channel().attr(EaglerVoiceHandler.VOICE_ENABLED).set(true); } if (obj instanceof BinaryWebSocketFrame) { final ByteBuf bb = ((BinaryWebSocketFrame) obj).content(); @@ -222,6 +224,7 @@ public class EaglerVoiceHandler extends ChannelInboundHandlerAdapter { super.channelRead(ctx, obj); } + @Override public void channelInactive(final ChannelHandlerContext ctx) throws Exception { super.channelInactive(ctx); this.removeUser(this.user); @@ -233,13 +236,13 @@ public class EaglerVoiceHandler extends ChannelInboundHandlerAdapter { final ByteArrayOutputStream baos = new ByteArrayOutputStream(); final DataOutputStream dos = new DataOutputStream(baos); dos.write(5); - final Set mostlyGlobalPlayers = new HashSet<>(); + final Set mostlyGlobalPlayers = ConcurrentHashMap.newKeySet(); 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) { + if (!mostlyGlobalPlayers.isEmpty()) { dos.writeInt(mostlyGlobalPlayers.size()); for (final String username : mostlyGlobalPlayers) { dos.writeUTF(username); @@ -293,8 +296,8 @@ public class EaglerVoiceHandler extends ChannelInboundHandlerAdapter { 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<>(); + 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"); @@ -309,12 +312,9 @@ public class EaglerVoiceHandler extends ChannelInboundHandlerAdapter { 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")); - } + for (final Map.Entry> trn : turnServerList.entrySet()) { + final Map o = trn.getValue(); + EaglerVoiceHandler.iceServers.add(o.get("url") + ";" + o.get("username") + ";" + o.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 index 2191495..eded26b 100644 --- a/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/EaglerXLoginHandler.java +++ b/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/EaglerXLoginHandler.java @@ -15,10 +15,13 @@ public class EaglerXLoginHandler extends ChannelOutboundHandlerAdapter { this.counter = 0; } + @Override + @SuppressWarnings("deprecation") public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { ExceptionUtil.handleNettyException(ctx, cause, null); } + @Override public void write(final ChannelHandlerContext ctx, final Object msg, final ChannelPromise promise) throws Exception { if (msg instanceof BinaryWebSocketFrame) { final ByteBuf bb = ((BinaryWebSocketFrame) msg).content(); diff --git a/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/EaglerXSkinHandler.java b/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/EaglerXSkinHandler.java index b206803..6a35966 100644 --- a/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/EaglerXSkinHandler.java +++ b/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/EaglerXSkinHandler.java @@ -8,8 +8,6 @@ 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; @@ -24,10 +22,12 @@ public class EaglerXSkinHandler extends ChannelInboundHandlerAdapter { this.pluginMessageId = -1; } + @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { ExceptionUtil.handleNettyException(ctx, cause, null); } + @Override 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) { @@ -94,6 +94,7 @@ public class EaglerXSkinHandler extends ChannelInboundHandlerAdapter { super.channelRead(ctx, obj); } + @Override public void channelInactive(final ChannelHandlerContext ctx) throws Exception { super.channelInactive(ctx); if (this.user != null) { diff --git a/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/EaglercraftHandler.java b/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/EaglercraftHandler.java index c988858..c077804 100644 --- a/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/EaglercraftHandler.java +++ b/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/EaglercraftHandler.java @@ -206,7 +206,7 @@ public class EaglercraftHandler extends MessageToMessageCodec out) { if (!ctx.channel().isOpen()) { return; @@ -64,6 +65,7 @@ public class EaglercraftInitialHandler extends ByteToMessageDecoder { } } + @Override 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 index a8875b7..86cc13f 100644 --- a/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/ExpiringSet.java +++ b/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/ExpiringSet.java @@ -1,25 +1,20 @@ package me.ayunami2000.ayunViaProxyEagUtils; -import java.util.HashMap; -import java.util.HashSet; +import java.util.AbstractSet; import java.util.Iterator; import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; -public class ExpiringSet extends HashSet { +public class ExpiringSet extends AbstractSet { + private final Set realSet; private final long expiration; - private final ExpiringEvent event; private final Map timestamps; public ExpiringSet(final long expiration) { - this.timestamps = new HashMap<>(); + this.realSet = ConcurrentHashMap.newKeySet(); + this.timestamps = new ConcurrentHashMap<>(); 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() { @@ -27,23 +22,20 @@ public class ExpiringSet extends HashSet { final long now = System.currentTimeMillis(); while (iterator.hasNext()) { final T element = iterator.next(); - if (super.contains(element)) { + if (this.realSet.contains(element)) { if (this.timestamps.get(element) + this.expiration >= now) { continue; } - if (this.event != null) { - this.event.onExpiration(element); - } } iterator.remove(); - super.remove(element); + this.realSet.remove(element); } } @Override public boolean add(final T o) { this.checkForExpirations(); - final boolean success = super.add(o); + final boolean success = this.realSet.add(o); if (success) { this.timestamps.put(o, System.currentTimeMillis()); } @@ -53,7 +45,7 @@ public class ExpiringSet extends HashSet { @Override public boolean remove(final Object o) { this.checkForExpirations(); - final boolean success = super.remove(o); + final boolean success = this.realSet.remove(o); if (success) { this.timestamps.remove(o); } @@ -63,16 +55,24 @@ public class ExpiringSet extends HashSet { @Override public void clear() { this.timestamps.clear(); - super.clear(); + this.realSet.clear(); + } + + @Override + public Iterator iterator() { + this.checkForExpirations(); + return this.realSet.iterator(); + } + + @Override + public int size() { + this.checkForExpirations(); + return this.realSet.size(); } @Override public boolean contains(final Object o) { this.checkForExpirations(); - return super.contains(o); - } - - public interface ExpiringEvent { - void onExpiration(final T p0); + return this.realSet.contains(o); } } diff --git a/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/FunnyConfig.java b/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/FunnyConfig.java new file mode 100644 index 0000000..d8cd89d --- /dev/null +++ b/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/FunnyConfig.java @@ -0,0 +1,39 @@ +package me.ayunami2000.ayunViaProxyEagUtils; + +import com.viaversion.viaversion.util.Config; + +import java.io.File; +import java.net.URL; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +public class FunnyConfig extends Config { + private boolean premiumSkins = false; + + protected FunnyConfig(File configFile) { + super(configFile); + } + + @Override + public URL getDefaultConfigURL() { + return Main.class.getResource("/eaglerskins.yml"); + } + + @Override + protected void handleConfig(Map map) { + Object item = map.get("premium-skins"); + if (item instanceof Boolean) { + this.premiumSkins = (Boolean) item; + } + } + + @Override + public List getUnsupportedOptions() { + return Collections.emptyList(); + } + + public boolean getPremiumSkins() { + return this.premiumSkins; + } +} diff --git a/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/Main.java b/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/Main.java index 4ad9681..7ad0610 100644 --- a/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/Main.java +++ b/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/Main.java @@ -28,6 +28,7 @@ public class Main extends ViaProxyPlugin { } static class EaglerConnectionHandler extends ChannelInboundHandlerAdapter { + @Override public void userEventTriggered(final ChannelHandlerContext ctx, final Object evt) throws Exception { super.userEventTriggered(ctx, evt); if (evt instanceof EaglercraftInitialHandler.EaglercraftClientConnected) { @@ -35,12 +36,15 @@ public class Main extends ViaProxyPlugin { ctx.pipeline().addBefore("eaglercraft-handler", "ayun-eag-utils-init", new EaglerUtilsInitHandler()); } } + + @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { ExceptionUtil.handleNettyException(ctx, cause, null); } } static class EaglerUtilsInitHandler extends ChannelInboundHandlerAdapter { + @Override public void channelRead(final ChannelHandlerContext ctx, final Object msg) throws Exception { if (msg instanceof BinaryWebSocketFrame) { final ByteBuf bb = ((BinaryWebSocketFrame) msg).content(); @@ -60,6 +64,8 @@ public class Main extends ViaProxyPlugin { ctx.pipeline().remove("ayun-eag-utils-init"); super.channelRead(ctx, msg); } + + @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { ExceptionUtil.handleNettyException(ctx, cause, null); } diff --git a/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/SkinPackets.java b/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/SkinPackets.java index c67899b..14d9ce5 100644 --- a/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/SkinPackets.java +++ b/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/SkinPackets.java @@ -1,8 +1,11 @@ package me.ayunami2000.ayunViaProxyEagUtils; import io.netty.channel.ChannelHandlerContext; +import net.raphimc.viaproxy.cli.options.Options; import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; import java.util.UUID; public class SkinPackets { @@ -18,7 +21,9 @@ public class SkinPackets { break; } case 6: { - processGetOtherSkinByURL(); + if (EaglerXSkinHandler.skinService.loadPremiumSkins) { + processGetOtherSkinByURL(data, sender, skinService); + } break; } default: { @@ -40,15 +45,42 @@ public class SkinPackets { skinService.processGetOtherSkin(searchUUID, sender); } - private static void processGetOtherSkinByURL() throws IOException { - throw new IOException("Skin URLs not implemented"); + private static void processGetOtherSkinByURL(byte[] data, ChannelHandlerContext sender, SkinService skinService) throws IOException { + if(data.length < 20) { + throw new IOException("Invalid length " + data.length + " for skin request packet"); + } + UUID searchUUID = bytesToUUID(data, 1); + int urlLength = (data[17] << 8) | data[18]; + if(data.length < 19 + urlLength) { + throw new IOException("Invalid length " + data.length + " for skin request packet with " + urlLength + " length URL"); + } + String urlStr = bytesToAscii(data, 19, urlLength); + urlStr = SkinService.sanitizeTextureURL(urlStr); + if(urlStr == null) { + throw new IOException("Invalid URL for skin request packet"); + } + URL url; + try { + url = new URL(urlStr); + }catch(MalformedURLException t) { + throw new IOException("Invalid URL for skin request packet", t); + } + String host = url.getHost(); + if(host.equalsIgnoreCase("textures.minecraft.net")) { + UUID validUUID = createEaglerURLSkinUUID(urlStr); + if(!searchUUID.equals(validUUID)) { + throw new IOException("Invalid generated UUID from skin URL"); + } + skinService.processGetOtherSkin(searchUUID, urlStr, sender); + }else { + throw new IOException("Invalid host in skin packet: " + host); + } } 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) { @@ -66,7 +98,7 @@ public class SkinPackets { } setAlphaForChest(pixels, (byte) (-1)); System.arraycopy(bs, 2, pixels, 0, pixels.length); - generatedPacket = makeCustomResponse(clientUUID, skinModel = (bs[1] & 0xFF), pixels); + generatedPacket = makeCustomResponse(clientUUID, bs[1] & 0xFF, pixels); break; } default: { @@ -76,6 +108,18 @@ public class SkinPackets { skinService.registerEaglercraftPlayer(clientUUID, generatedPacket); } + public static byte[] asciiString(String string) { + byte[] str = new byte[string.length()]; + for(int i = 0; i < str.length; ++i) { + str[i] = (byte)string.charAt(i); + } + return str; + } + + public static UUID createEaglerURLSkinUUID(String skinUrl) { + return UUID.nameUUIDFromBytes(asciiString("EaglercraftSkinURL:" + skinUrl)); + } + 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); @@ -117,6 +161,14 @@ public class SkinPackets { return ret; } + public static String bytesToAscii(byte[] bytes, int off, int len) { + char[] ret = new char[len]; + for(int i = 0; i < len; ++i) { + ret[i] = (char)((int)bytes[off + i] & 0xFF); + } + return new String(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); diff --git a/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/SkinService.java b/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/SkinService.java index 91c3508..4a2b95f 100644 --- a/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/SkinService.java +++ b/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/SkinService.java @@ -6,21 +6,32 @@ 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.util.HashMap; -import java.util.Map; +import java.net.URI; +import java.net.URL; 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) { @@ -60,6 +71,51 @@ public class SkinService { } } sendData(sender, SkinPackets.makeCustomResponse(searchUUID, 0, res)); + } else { + processGetOtherSkin(searchUUID, "https://crafatar.com/skins/" + searchUUID.toString(), sender); + } + } + + public byte[] fetchSkinPacket(final UUID searchUUID, final String skinURL) { + // no rate-limit or size limit. it is assumed that this feature is used privately anyway. + final CachedSkin cached = this.skinCache.get(searchUUID); + if (cached != null) { + return cached.packet; + } else { + try { + byte[] res = ((DataBufferByte) ImageIO.read(new URL(skinURL)).getRaster().getDataBuffer()).getData(); + 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); + } + for (int j = 0; j < res.length; j += 4) { + final byte tmp3 = res[j]; + res[j] = res[j + 1]; + res[j + 1] = res[j + 2]; + res[j + 2] = res[j + 3]; + res[j + 3] = tmp3; + } + } + byte[] pkt = SkinPackets.makeCustomResponse(searchUUID, 0, res); + registerEaglercraftPlayer(searchUUID, pkt); + return pkt; + } catch (IOException ignored) { + return null; + } + } + } + + public void processGetOtherSkin(final UUID searchUUID, final String skinURL, final ChannelHandlerContext sender) { + final byte[] skin = fetchSkinPacket(searchUUID, skinURL); + if (skin != null) { + sendData(sender, skin); } else { sendData(sender, SkinPackets.makePresetResponse(searchUUID)); } @@ -70,7 +126,7 @@ public class SkinService { EaglerSkinHandler.skinCollection.put(clientUUID, newToOldSkin(generatedPacket)); } - private static byte[] newToOldSkin(final byte[] packet) throws IOException { + public static byte[] newToOldSkin(final byte[] packet) throws IOException { final byte type = packet[0]; byte[] res; switch (type) { @@ -126,4 +182,56 @@ public class SkinService { this.packet = packet; } } + + public static String sanitizeTextureURL(String url) { + try { + URI uri = URI.create(url); + StringBuilder builder = new StringBuilder(); + String scheme = uri.getScheme(); + if(scheme == null) { + return null; + } + String host = uri.getHost(); + if(host == null) { + return null; + } + scheme = scheme.toLowerCase(); + builder.append(scheme).append("://"); + builder.append(host); + int port = uri.getPort(); + if(port != -1) { + switch(scheme) { + case "http": + if(port == 80) { + port = -1; + } + break; + case "https": + if(port == 443) { + port = -1; + } + break; + default: + return null; + } + if(port != -1) { + builder.append(":").append(port); + } + } + String path = uri.getRawPath(); + if(path != null) { + if(path.contains("//")) { + path = String.join("/", path.split("[\\/]+")); + } + int len = path.length(); + if(len > 1 && path.charAt(len - 1) == '/') { + path = path.substring(0, len - 1); + } + builder.append(path); + } + return builder.toString(); + }catch(Throwable t) { + return null; + } + } } diff --git a/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/WebSocketActiveNotifier.java b/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/WebSocketActiveNotifier.java index ac50aa3..2090da2 100644 --- a/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/WebSocketActiveNotifier.java +++ b/src/main/java/me/ayunami2000/ayunViaProxyEagUtils/WebSocketActiveNotifier.java @@ -6,9 +6,11 @@ import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler; import net.raphimc.viaproxy.proxy.util.ExceptionUtil; public class WebSocketActiveNotifier extends ChannelInboundHandlerAdapter { + @Override public void channelActive(final ChannelHandlerContext ctx) { } + @Override public void userEventTriggered(final ChannelHandlerContext ctx, final Object evt) throws Exception { if (evt instanceof WebSocketServerProtocolHandler.HandshakeComplete) { ctx.fireChannelActive(); @@ -17,6 +19,7 @@ public class WebSocketActiveNotifier extends ChannelInboundHandlerAdapter { super.userEventTriggered(ctx, evt); } + @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { ExceptionUtil.handleNettyException(ctx, cause, null); } diff --git a/src/main/resources/eaglerskins.yml b/src/main/resources/eaglerskins.yml new file mode 100644 index 0000000..e81f64d --- /dev/null +++ b/src/main/resources/eaglerskins.yml @@ -0,0 +1,2 @@ +# Use premium skins +premium-skins: false \ No newline at end of file