mirror of
https://github.com/ayunami2000/ayunViaProxyEagUtils.git
synced 2024-12-22 06:44:11 -08:00
Add optional online-mode skin support
This commit is contained in:
parent
0dba9a05e6
commit
8a154e833f
|
@ -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));
|
||||
|
|
|
@ -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<String> mostlyGlobalPlayers = new HashSet<>();
|
||||
final Set<String> 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<String, Object> turnServerList = new HashMap<>();
|
||||
HashMap<String, Object> n = new HashMap<>();
|
||||
final Map<String, Map<String, String>> turnServerList = new HashMap<>();
|
||||
HashMap<String, String> 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<String, Object> 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<String, Map<String, String>> trn : turnServerList.entrySet()) {
|
||||
final Map<String, String> o = trn.getValue();
|
||||
EaglerVoiceHandler.iceServers.add(o.get("url") + ";" + o.get("username") + ";" + o.get("password"));
|
||||
}
|
||||
VOICE_ENABLED = AttributeKey.valueOf("ayun-voice-enabled");
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -206,7 +206,7 @@ public class EaglercraftHandler extends MessageToMessageCodec<WebSocketFrame, By
|
|||
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.readCharSequence(data.readUnsignedByte(), StandardCharsets.US_ASCII);
|
||||
data.skipBytes(data.readUnsignedByte());
|
||||
if (data.isReadable()) {
|
||||
throw new IllegalArgumentException("Too much data in packet: " + data.readableBytes() + " bytes");
|
||||
|
@ -224,7 +224,7 @@ public class EaglercraftHandler extends MessageToMessageCodec<WebSocketFrame, By
|
|||
case LOGIN: {
|
||||
final int packetId = data.readUnsignedByte();
|
||||
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()];
|
||||
data.readBytes(dataBytes);
|
||||
if (data.isReadable()) {
|
||||
|
|
|
@ -18,6 +18,7 @@ import java.util.List;
|
|||
public class EaglercraftInitialHandler extends ByteToMessageDecoder {
|
||||
private static SslContext sslContext;
|
||||
|
||||
@Override
|
||||
protected void decode(final ChannelHandlerContext ctx, final ByteBuf in, final List<Object> 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);
|
||||
}
|
||||
|
|
|
@ -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<T> extends HashSet<T> {
|
||||
public class ExpiringSet<T> extends AbstractSet<T> {
|
||||
private final Set<T> realSet;
|
||||
private final long expiration;
|
||||
private final ExpiringEvent<T> event;
|
||||
private final Map<T, Long> 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<T> event) {
|
||||
this.timestamps = new HashMap<>();
|
||||
this.expiration = expiration;
|
||||
this.event = event;
|
||||
}
|
||||
|
||||
public void checkForExpirations() {
|
||||
|
@ -27,23 +22,20 @@ public class ExpiringSet<T> extends HashSet<T> {
|
|||
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<T> extends HashSet<T> {
|
|||
@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<T> extends HashSet<T> {
|
|||
@Override
|
||||
public void 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
|
||||
public boolean contains(final Object o) {
|
||||
this.checkForExpirations();
|
||||
return super.contains(o);
|
||||
}
|
||||
|
||||
public interface ExpiringEvent<T> {
|
||||
void onExpiration(final T p0);
|
||||
return this.realSet.contains(o);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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<UUID, CachedSkin> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
2
src/main/resources/eaglerskins.yml
Normal file
2
src/main/resources/eaglerskins.yml
Normal file
|
@ -0,0 +1,2 @@
|
|||
# Use premium skins
|
||||
premium-skins: false
|
Loading…
Reference in New Issue
Block a user