Add optional online-mode skin support

This commit is contained in:
ayunami2000 2023-09-26 14:14:49 -04:00
parent 0dba9a05e6
commit 8a154e833f
13 changed files with 320 additions and 50 deletions

View File

@ -1,17 +1,26 @@
package me.ayunami2000.ayunViaProxyEagUtils; package me.ayunami2000.ayunViaProxyEagUtils;
import com.google.gson.JsonObject;
import com.viaversion.viaversion.util.GsonUtil;
import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame; 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.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 net.raphimc.viaproxy.proxy.util.ExceptionUtil;
import javax.imageio.ImageIO;
import java.awt.image.DataBufferByte;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream; import java.io.DataOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URL;
import java.net.URLConnection;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
@ -41,10 +50,12 @@ public class EaglerSkinHandler extends ChannelInboundHandlerAdapter {
this.user = username; this.user = username;
} }
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
ExceptionUtil.handleNettyException(ctx, cause, null); ExceptionUtil.handleNettyException(ctx, cause, null);
} }
@Override
public void channelRead(final ChannelHandlerContext ctx, final Object obj) throws Exception { public void channelRead(final ChannelHandlerContext ctx, final Object obj) throws Exception {
final UUID uuid = UUID.nameUUIDFromBytes(("OfflinePlayer:" + this.user).getBytes(StandardCharsets.UTF_8)); final UUID uuid = UUID.nameUUIDFromBytes(("OfflinePlayer:" + this.user).getBytes(StandardCharsets.UTF_8));
if (!EaglerSkinHandler.users.containsKey(uuid) && ctx.channel().isActive()) { if (!EaglerSkinHandler.users.containsKey(uuid) && ctx.channel().isActive()) {
@ -102,6 +113,48 @@ public class EaglerSkinHandler extends ChannelInboundHandlerAdapter {
conc = conc2; conc = conc2;
} }
sendData(ctx, "EAG|UserSkin", conc); 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(); bb.release();
@ -143,6 +196,7 @@ public class EaglerSkinHandler extends ChannelInboundHandlerAdapter {
super.channelRead(ctx, obj); super.channelRead(ctx, obj);
} }
@Override
public void channelInactive(final ChannelHandlerContext ctx) throws Exception { public void channelInactive(final ChannelHandlerContext ctx) throws Exception {
super.channelInactive(ctx); super.channelInactive(ctx);
final UUID uuid = UUID.nameUUIDFromBytes(("OfflinePlayer:" + this.user).getBytes(StandardCharsets.UTF_8)); final UUID uuid = UUID.nameUUIDFromBytes(("OfflinePlayer:" + this.user).getBytes(StandardCharsets.UTF_8));

View File

@ -38,12 +38,14 @@ public class EaglerVoiceHandler extends ChannelInboundHandlerAdapter {
this.user = username; this.user = username;
} }
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
ExceptionUtil.handleNettyException(ctx, cause, null); ExceptionUtil.handleNettyException(ctx, cause, null);
} }
@Override
public void channelRead(final ChannelHandlerContext ctx, final Object obj) throws Exception { 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 ByteArrayOutputStream baos = new ByteArrayOutputStream();
final DataOutputStream dos = new DataOutputStream(baos); final DataOutputStream dos = new DataOutputStream(baos);
dos.write(0); dos.write(0);
@ -54,7 +56,7 @@ public class EaglerVoiceHandler extends ChannelInboundHandlerAdapter {
} }
sendData(ctx, baos.toByteArray()); sendData(ctx, baos.toByteArray());
this.sendVoicePlayers(this.user); 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) { if (obj instanceof BinaryWebSocketFrame) {
final ByteBuf bb = ((BinaryWebSocketFrame) obj).content(); final ByteBuf bb = ((BinaryWebSocketFrame) obj).content();
@ -222,6 +224,7 @@ public class EaglerVoiceHandler extends ChannelInboundHandlerAdapter {
super.channelRead(ctx, obj); super.channelRead(ctx, obj);
} }
@Override
public void channelInactive(final ChannelHandlerContext ctx) throws Exception { public void channelInactive(final ChannelHandlerContext ctx) throws Exception {
super.channelInactive(ctx); super.channelInactive(ctx);
this.removeUser(this.user); this.removeUser(this.user);
@ -233,13 +236,13 @@ public class EaglerVoiceHandler extends ChannelInboundHandlerAdapter {
final ByteArrayOutputStream baos = new ByteArrayOutputStream(); final ByteArrayOutputStream baos = new ByteArrayOutputStream();
final DataOutputStream dos = new DataOutputStream(baos); final DataOutputStream dos = new DataOutputStream(baos);
dos.write(5); dos.write(5);
final Set<String> mostlyGlobalPlayers = new HashSet<>(); final Set<String> mostlyGlobalPlayers = ConcurrentHashMap.newKeySet();
for (final String username : EaglerVoiceHandler.voicePlayers.keySet()) { 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)))) { 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); mostlyGlobalPlayers.add(username);
} }
} }
if (mostlyGlobalPlayers.size() > 0) { if (!mostlyGlobalPlayers.isEmpty()) {
dos.writeInt(mostlyGlobalPlayers.size()); dos.writeInt(mostlyGlobalPlayers.size());
for (final String username : mostlyGlobalPlayers) { for (final String username : mostlyGlobalPlayers) {
dos.writeUTF(username); 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:stun3.l.google.com:19302");
EaglerVoiceHandler.iceServers.add("stun:stun4.l.google.com:19302"); EaglerVoiceHandler.iceServers.add("stun:stun4.l.google.com:19302");
EaglerVoiceHandler.iceServers.add("stun:openrelay.metered.ca:80"); EaglerVoiceHandler.iceServers.add("stun:openrelay.metered.ca:80");
final Map<String, Object> turnServerList = new HashMap<>(); final Map<String, Map<String, String>> turnServerList = new HashMap<>();
HashMap<String, Object> n = new HashMap<>(); HashMap<String, String> n = new HashMap<>();
n.put("url", "turn:openrelay.metered.ca:80"); n.put("url", "turn:openrelay.metered.ca:80");
n.put("username", "openrelayproject"); n.put("username", "openrelayproject");
n.put("password", "openrelayproject"); n.put("password", "openrelayproject");
@ -309,12 +312,9 @@ public class EaglerVoiceHandler extends ChannelInboundHandlerAdapter {
n.put("username", "openrelayproject"); n.put("username", "openrelayproject");
n.put("password", "openrelayproject"); n.put("password", "openrelayproject");
turnServerList.put("openrelay3", n); turnServerList.put("openrelay3", n);
for (final Map.Entry<String, Object> trn : turnServerList.entrySet()) { for (final Map.Entry<String, Map<String, String>> trn : turnServerList.entrySet()) {
final Object o = trn.getValue(); final Map<String, String> o = trn.getValue();
if (o instanceof Map) { EaglerVoiceHandler.iceServers.add(o.get("url") + ";" + o.get("username") + ";" + o.get("password"));
final Map o2 = (Map) o;
EaglerVoiceHandler.iceServers.add("" + o2.get("url") + ";" + o2.get("username") + ";" + o2.get("password"));
}
} }
VOICE_ENABLED = AttributeKey.valueOf("ayun-voice-enabled"); VOICE_ENABLED = AttributeKey.valueOf("ayun-voice-enabled");
} }

View File

@ -15,10 +15,13 @@ public class EaglerXLoginHandler extends ChannelOutboundHandlerAdapter {
this.counter = 0; this.counter = 0;
} }
@Override
@SuppressWarnings("deprecation")
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
ExceptionUtil.handleNettyException(ctx, cause, null); ExceptionUtil.handleNettyException(ctx, cause, null);
} }
@Override
public void write(final ChannelHandlerContext ctx, final Object msg, final ChannelPromise promise) throws Exception { public void write(final ChannelHandlerContext ctx, final Object msg, final ChannelPromise promise) throws Exception {
if (msg instanceof BinaryWebSocketFrame) { if (msg instanceof BinaryWebSocketFrame) {
final ByteBuf bb = ((BinaryWebSocketFrame) msg).content(); final ByteBuf bb = ((BinaryWebSocketFrame) msg).content();

View File

@ -8,8 +8,6 @@ import net.raphimc.netminecraft.packet.PacketTypes;
import net.raphimc.viaproxy.proxy.util.ExceptionUtil; import net.raphimc.viaproxy.proxy.util.ExceptionUtil;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
@ -24,10 +22,12 @@ public class EaglerXSkinHandler extends ChannelInboundHandlerAdapter {
this.pluginMessageId = -1; this.pluginMessageId = -1;
} }
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
ExceptionUtil.handleNettyException(ctx, cause, null); ExceptionUtil.handleNettyException(ctx, cause, null);
} }
@Override
public void channelRead(final ChannelHandlerContext ctx, final Object obj) throws Exception { public void channelRead(final ChannelHandlerContext ctx, final Object obj) throws Exception {
final EaglercraftHandler.State state = ((EaglercraftHandler) ctx.pipeline().get("eaglercraft-handler")).state; final EaglercraftHandler.State state = ((EaglercraftHandler) ctx.pipeline().get("eaglercraft-handler")).state;
if (state == EaglercraftHandler.State.LOGIN && obj instanceof BinaryWebSocketFrame) { if (state == EaglercraftHandler.State.LOGIN && obj instanceof BinaryWebSocketFrame) {
@ -94,6 +94,7 @@ public class EaglerXSkinHandler extends ChannelInboundHandlerAdapter {
super.channelRead(ctx, obj); super.channelRead(ctx, obj);
} }
@Override
public void channelInactive(final ChannelHandlerContext ctx) throws Exception { public void channelInactive(final ChannelHandlerContext ctx) throws Exception {
super.channelInactive(ctx); super.channelInactive(ctx);
if (this.user != null) { if (this.user != null) {

View File

@ -206,7 +206,7 @@ public class EaglercraftHandler extends MessageToMessageCodec<WebSocketFrame, By
throw new IllegalStateException("Unexpected packet id " + packetId + " in state " + this.state); throw new IllegalStateException("Unexpected packet id " + packetId + " in state " + this.state);
} }
final String username = data.readCharSequence(data.readUnsignedByte(), StandardCharsets.US_ASCII).toString(); final String username = data.readCharSequence(data.readUnsignedByte(), StandardCharsets.US_ASCII).toString();
data.readCharSequence(data.readUnsignedByte(), StandardCharsets.US_ASCII).toString(); data.readCharSequence(data.readUnsignedByte(), StandardCharsets.US_ASCII);
data.skipBytes(data.readUnsignedByte()); data.skipBytes(data.readUnsignedByte());
if (data.isReadable()) { if (data.isReadable()) {
throw new IllegalArgumentException("Too much data in packet: " + data.readableBytes() + " bytes"); throw new IllegalArgumentException("Too much data in packet: " + data.readableBytes() + " bytes");
@ -224,7 +224,7 @@ public class EaglercraftHandler extends MessageToMessageCodec<WebSocketFrame, By
case LOGIN: { case LOGIN: {
final int packetId = data.readUnsignedByte(); final int packetId = data.readUnsignedByte();
if (packetId == 7) { if (packetId == 7) {
data.readCharSequence(data.readUnsignedByte(), StandardCharsets.US_ASCII).toString(); data.readCharSequence(data.readUnsignedByte(), StandardCharsets.US_ASCII);
final byte[] dataBytes = new byte[data.readUnsignedShort()]; final byte[] dataBytes = new byte[data.readUnsignedShort()];
data.readBytes(dataBytes); data.readBytes(dataBytes);
if (data.isReadable()) { if (data.isReadable()) {

View File

@ -18,6 +18,7 @@ import java.util.List;
public class EaglercraftInitialHandler extends ByteToMessageDecoder { public class EaglercraftInitialHandler extends ByteToMessageDecoder {
private static SslContext sslContext; private static SslContext sslContext;
@Override
protected void decode(final ChannelHandlerContext ctx, final ByteBuf in, final List<Object> out) { protected void decode(final ChannelHandlerContext ctx, final ByteBuf in, final List<Object> out) {
if (!ctx.channel().isOpen()) { if (!ctx.channel().isOpen()) {
return; return;
@ -64,6 +65,7 @@ public class EaglercraftInitialHandler extends ByteToMessageDecoder {
} }
} }
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
ExceptionUtil.handleNettyException(ctx, cause, null); ExceptionUtil.handleNettyException(ctx, cause, null);
} }

View File

@ -1,25 +1,20 @@
package me.ayunami2000.ayunViaProxyEagUtils; package me.ayunami2000.ayunViaProxyEagUtils;
import java.util.HashMap; import java.util.AbstractSet;
import java.util.HashSet;
import java.util.Iterator; import java.util.Iterator;
import java.util.Map; import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
public class ExpiringSet<T> extends HashSet<T> { public class ExpiringSet<T> extends AbstractSet<T> {
private final Set<T> realSet;
private final long expiration; private final long expiration;
private final ExpiringEvent<T> event;
private final Map<T, Long> timestamps; private final Map<T, Long> timestamps;
public ExpiringSet(final long expiration) { public ExpiringSet(final long expiration) {
this.timestamps = new HashMap<>(); this.realSet = ConcurrentHashMap.newKeySet();
this.timestamps = new ConcurrentHashMap<>();
this.expiration = expiration; this.expiration = expiration;
this.event = null;
}
public ExpiringSet(final long expiration, final ExpiringEvent<T> event) {
this.timestamps = new HashMap<>();
this.expiration = expiration;
this.event = event;
} }
public void checkForExpirations() { public void checkForExpirations() {
@ -27,23 +22,20 @@ public class ExpiringSet<T> extends HashSet<T> {
final long now = System.currentTimeMillis(); final long now = System.currentTimeMillis();
while (iterator.hasNext()) { while (iterator.hasNext()) {
final T element = iterator.next(); final T element = iterator.next();
if (super.contains(element)) { if (this.realSet.contains(element)) {
if (this.timestamps.get(element) + this.expiration >= now) { if (this.timestamps.get(element) + this.expiration >= now) {
continue; continue;
} }
if (this.event != null) {
this.event.onExpiration(element);
}
} }
iterator.remove(); iterator.remove();
super.remove(element); this.realSet.remove(element);
} }
} }
@Override @Override
public boolean add(final T o) { public boolean add(final T o) {
this.checkForExpirations(); this.checkForExpirations();
final boolean success = super.add(o); final boolean success = this.realSet.add(o);
if (success) { if (success) {
this.timestamps.put(o, System.currentTimeMillis()); this.timestamps.put(o, System.currentTimeMillis());
} }
@ -53,7 +45,7 @@ public class ExpiringSet<T> extends HashSet<T> {
@Override @Override
public boolean remove(final Object o) { public boolean remove(final Object o) {
this.checkForExpirations(); this.checkForExpirations();
final boolean success = super.remove(o); final boolean success = this.realSet.remove(o);
if (success) { if (success) {
this.timestamps.remove(o); this.timestamps.remove(o);
} }
@ -63,16 +55,24 @@ public class ExpiringSet<T> extends HashSet<T> {
@Override @Override
public void clear() { public void clear() {
this.timestamps.clear(); this.timestamps.clear();
super.clear(); this.realSet.clear();
}
@Override
public Iterator<T> iterator() {
this.checkForExpirations();
return this.realSet.iterator();
}
@Override
public int size() {
this.checkForExpirations();
return this.realSet.size();
} }
@Override @Override
public boolean contains(final Object o) { public boolean contains(final Object o) {
this.checkForExpirations(); this.checkForExpirations();
return super.contains(o); return this.realSet.contains(o);
}
public interface ExpiringEvent<T> {
void onExpiration(final T p0);
} }
} }

View File

@ -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<String, Object> map) {
Object item = map.get("premium-skins");
if (item instanceof Boolean) {
this.premiumSkins = (Boolean) item;
}
}
@Override
public List<String> getUnsupportedOptions() {
return Collections.emptyList();
}
public boolean getPremiumSkins() {
return this.premiumSkins;
}
}

View File

@ -28,6 +28,7 @@ public class Main extends ViaProxyPlugin {
} }
static class EaglerConnectionHandler extends ChannelInboundHandlerAdapter { static class EaglerConnectionHandler extends ChannelInboundHandlerAdapter {
@Override
public void userEventTriggered(final ChannelHandlerContext ctx, final Object evt) throws Exception { public void userEventTriggered(final ChannelHandlerContext ctx, final Object evt) throws Exception {
super.userEventTriggered(ctx, evt); super.userEventTriggered(ctx, evt);
if (evt instanceof EaglercraftInitialHandler.EaglercraftClientConnected) { 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()); ctx.pipeline().addBefore("eaglercraft-handler", "ayun-eag-utils-init", new EaglerUtilsInitHandler());
} }
} }
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
ExceptionUtil.handleNettyException(ctx, cause, null); ExceptionUtil.handleNettyException(ctx, cause, null);
} }
} }
static class EaglerUtilsInitHandler extends ChannelInboundHandlerAdapter { static class EaglerUtilsInitHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(final ChannelHandlerContext ctx, final Object msg) throws Exception { public void channelRead(final ChannelHandlerContext ctx, final Object msg) throws Exception {
if (msg instanceof BinaryWebSocketFrame) { if (msg instanceof BinaryWebSocketFrame) {
final ByteBuf bb = ((BinaryWebSocketFrame) msg).content(); final ByteBuf bb = ((BinaryWebSocketFrame) msg).content();
@ -60,6 +64,8 @@ public class Main extends ViaProxyPlugin {
ctx.pipeline().remove("ayun-eag-utils-init"); ctx.pipeline().remove("ayun-eag-utils-init");
super.channelRead(ctx, msg); super.channelRead(ctx, msg);
} }
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
ExceptionUtil.handleNettyException(ctx, cause, null); ExceptionUtil.handleNettyException(ctx, cause, null);
} }

View File

@ -1,8 +1,11 @@
package me.ayunami2000.ayunViaProxyEagUtils; package me.ayunami2000.ayunViaProxyEagUtils;
import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelHandlerContext;
import net.raphimc.viaproxy.cli.options.Options;
import java.io.IOException; import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.UUID; import java.util.UUID;
public class SkinPackets { public class SkinPackets {
@ -18,7 +21,9 @@ public class SkinPackets {
break; break;
} }
case 6: { case 6: {
processGetOtherSkinByURL(); if (EaglerXSkinHandler.skinService.loadPremiumSkins) {
processGetOtherSkinByURL(data, sender, skinService);
}
break; break;
} }
default: { default: {
@ -40,15 +45,42 @@ public class SkinPackets {
skinService.processGetOtherSkin(searchUUID, sender); skinService.processGetOtherSkin(searchUUID, sender);
} }
private static void processGetOtherSkinByURL() throws IOException { private static void processGetOtherSkinByURL(byte[] data, ChannelHandlerContext sender, SkinService skinService) throws IOException {
throw new IOException("Skin URLs not implemented"); 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 { public static void registerEaglerPlayer(final UUID clientUUID, final byte[] bs, final SkinService skinService) throws IOException {
if (bs.length == 0) { if (bs.length == 0) {
throw new IOException("Zero-length packet recieved"); throw new IOException("Zero-length packet recieved");
} }
int skinModel = -1;
final int packetType = bs[0] & 0xFF; final int packetType = bs[0] & 0xFF;
byte[] generatedPacket; byte[] generatedPacket;
switch (packetType) { switch (packetType) {
@ -66,7 +98,7 @@ public class SkinPackets {
} }
setAlphaForChest(pixels, (byte) (-1)); setAlphaForChest(pixels, (byte) (-1));
System.arraycopy(bs, 2, pixels, 0, pixels.length); System.arraycopy(bs, 2, pixels, 0, pixels.length);
generatedPacket = makeCustomResponse(clientUUID, skinModel = (bs[1] & 0xFF), pixels); generatedPacket = makeCustomResponse(clientUUID, bs[1] & 0xFF, pixels);
break; break;
} }
default: { default: {
@ -76,6 +108,18 @@ public class SkinPackets {
skinService.registerEaglercraftPlayer(clientUUID, generatedPacket); 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 { public static void registerEaglerPlayerFallback(final UUID clientUUID, final SkinService skinService) throws IOException {
final int skinModel = ((clientUUID.hashCode() & 0x1) != 0x0) ? 1 : 0; final int skinModel = ((clientUUID.hashCode() & 0x1) != 0x0) ? 1 : 0;
final byte[] generatedPacket = makePresetResponse(clientUUID, skinModel); final byte[] generatedPacket = makePresetResponse(clientUUID, skinModel);
@ -117,6 +161,14 @@ public class SkinPackets {
return ret; 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) { 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 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); 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);

View File

@ -6,21 +6,32 @@ import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame; import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame;
import net.raphimc.netminecraft.constants.MCPackets; import net.raphimc.netminecraft.constants.MCPackets;
import net.raphimc.netminecraft.packet.PacketTypes; 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 javax.imageio.ImageIO;
import java.awt.image.DataBufferByte; import java.awt.image.DataBufferByte;
import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.util.HashMap; import java.net.URI;
import java.util.Map; import java.net.URL;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
public class SkinService { public class SkinService {
public final boolean loadPremiumSkins;
private final ConcurrentHashMap<UUID, CachedSkin> skinCache; private final ConcurrentHashMap<UUID, CachedSkin> skinCache;
public SkinService() { public SkinService() {
this.skinCache = new ConcurrentHashMap<>(); 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) { private static void sendData(final ChannelHandlerContext ctx, final byte[] data) {
@ -60,6 +71,51 @@ public class SkinService {
} }
} }
sendData(sender, SkinPackets.makeCustomResponse(searchUUID, 0, res)); 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 { } else {
sendData(sender, SkinPackets.makePresetResponse(searchUUID)); sendData(sender, SkinPackets.makePresetResponse(searchUUID));
} }
@ -70,7 +126,7 @@ public class SkinService {
EaglerSkinHandler.skinCollection.put(clientUUID, newToOldSkin(generatedPacket)); 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]; final byte type = packet[0];
byte[] res; byte[] res;
switch (type) { switch (type) {
@ -126,4 +182,56 @@ public class SkinService {
this.packet = packet; 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;
}
}
} }

View File

@ -6,9 +6,11 @@ import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
import net.raphimc.viaproxy.proxy.util.ExceptionUtil; import net.raphimc.viaproxy.proxy.util.ExceptionUtil;
public class WebSocketActiveNotifier extends ChannelInboundHandlerAdapter { public class WebSocketActiveNotifier extends ChannelInboundHandlerAdapter {
@Override
public void channelActive(final ChannelHandlerContext ctx) { public void channelActive(final ChannelHandlerContext ctx) {
} }
@Override
public void userEventTriggered(final ChannelHandlerContext ctx, final Object evt) throws Exception { public void userEventTriggered(final ChannelHandlerContext ctx, final Object evt) throws Exception {
if (evt instanceof WebSocketServerProtocolHandler.HandshakeComplete) { if (evt instanceof WebSocketServerProtocolHandler.HandshakeComplete) {
ctx.fireChannelActive(); ctx.fireChannelActive();
@ -17,6 +19,7 @@ public class WebSocketActiveNotifier extends ChannelInboundHandlerAdapter {
super.userEventTriggered(ctx, evt); super.userEventTriggered(ctx, evt);
} }
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
ExceptionUtil.handleNettyException(ctx, cause, null); ExceptionUtil.handleNettyException(ctx, cause, null);
} }

View File

@ -0,0 +1,2 @@
# Use premium skins
premium-skins: false