Upload via GUI (138 Dateien)
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
package de.aeromc.capes;
|
||||
|
||||
import java.lang.instrument.Instrumentation;
|
||||
|
||||
/**
|
||||
* AeroMC-Umhang-Agent.
|
||||
*
|
||||
* Wird vom Launcher per -javaagent an Minecraft angehängt (jede Version, jeder
|
||||
* Loader). Er klinkt sich in Mojangs authlib ein – über sie lädt Minecraft für
|
||||
* jeden Spieler Skin und Umhang – und setzt dort den AeroMC-Umhang des Spielers
|
||||
* ein, falls er einen hat. Alles Weitere (Herunterladen, Zwischenspeichern,
|
||||
* Anzeigen, auch HD) erledigt Minecraft selbst wie bei einem offiziellen Umhang.
|
||||
*
|
||||
* Argument: Basisadresse des Umhang-Servers (…/wp-json/aeromc/v1).
|
||||
*/
|
||||
public final class Agent {
|
||||
/** Unter diesem Schlüssel liegt der Nachschlage-Dienst in den System-Properties. */
|
||||
public static final String SCHLUESSEL = "aeromc.capes.hook";
|
||||
|
||||
private Agent() {}
|
||||
|
||||
public static void premain(String argument, Instrumentation inst) {
|
||||
starte(argument, inst);
|
||||
}
|
||||
|
||||
public static void agentmain(String argument, Instrumentation inst) {
|
||||
starte(argument, inst);
|
||||
}
|
||||
|
||||
private static void starte(String argument, Instrumentation inst) {
|
||||
try {
|
||||
String basis = argument != null && argument.startsWith("http") ? argument : Nachschlagen.STANDARD_BASIS;
|
||||
// Die eingefügten Befehle in authlib holen sich den Dienst von hier – nur
|
||||
// über JDK-Typen, damit es mit jedem Classloader und Modulsystem klappt.
|
||||
System.getProperties().put(SCHLUESSEL, new Nachschlagen(basis));
|
||||
inst.addTransformer(new Umbau());
|
||||
System.out.println("[AeroMC] Umhang-Agent aktiv");
|
||||
} catch (Throwable t) {
|
||||
System.out.println("[AeroMC] Umhang-Agent konnte nicht starten: " + t);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
package de.aeromc.capes;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.InputStream;
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Method;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Base64;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Wird aus authlib heraus mit (ergebnis, argument) aufgerufen und gibt das
|
||||
* Ergebnis zurück – mit eingesetztem AeroMC-Umhang, falls der Spieler einen hat.
|
||||
* Arbeitet nur über Reflexion, damit es zu jeder authlib-Fassung passt, und
|
||||
* fängt jeden Fehler ab: im schlimmsten Fall bleibt alles wie ohne Agent.
|
||||
*/
|
||||
public final class Nachschlagen implements BiFunction<Object, Object, Object> {
|
||||
public static final String STANDARD_BASIS = "https://viper-network.de/wp-json/aeromc/v1";
|
||||
private static final long GEFUNDEN_MS = 5 * 60 * 1000L;
|
||||
private static final long KEINER_MS = 2 * 60 * 1000L;
|
||||
private static final Pattern URL_MUSTER = Pattern.compile("\"url\"\\s*:\\s*\"((?:[^\"\\\\]|\\\\.)*)\"");
|
||||
private static final Pattern ID_MUSTER = Pattern.compile("\"profileId\"\\s*:\\s*\"([0-9a-fA-F-]{32,36})\"");
|
||||
|
||||
private static final class Eintrag {
|
||||
final String url; // null = kein Umhang
|
||||
final long zeit;
|
||||
Eintrag(String url) { this.url = url; this.zeit = System.currentTimeMillis(); }
|
||||
boolean frisch() { return System.currentTimeMillis() - zeit < (url != null ? GEFUNDEN_MS : KEINER_MS); }
|
||||
}
|
||||
|
||||
private final String basis;
|
||||
private final Map<String, Eintrag> speicher = new ConcurrentHashMap<>();
|
||||
private final Map<String, Boolean> unterwegs = new ConcurrentHashMap<>();
|
||||
private final Map<String, Boolean> gemeldet = new ConcurrentHashMap<>();
|
||||
|
||||
public Nachschlagen(String basis) {
|
||||
this.basis = basis.endsWith("/") ? basis.substring(0, basis.length() - 1) : basis;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object apply(Object ergebnis, Object argument) {
|
||||
try {
|
||||
String id = spielerId(argument);
|
||||
if (id == null) return ergebnis;
|
||||
String url = umhangUrl(id);
|
||||
if (url == null) return ergebnis;
|
||||
Object neu = mitUmhang(ergebnis, url, argument);
|
||||
if (neu != ergebnis && gemeldet.putIfAbsent(id, Boolean.TRUE) == null) {
|
||||
System.out.println("[AeroMC] Umhang fuer " + id + " eingesetzt");
|
||||
}
|
||||
return neu;
|
||||
} catch (Throwable t) {
|
||||
return ergebnis;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Spieler erkennen ----------
|
||||
private static String spielerId(Object argument) throws Exception {
|
||||
if (argument == null) return null;
|
||||
String klasse = argument.getClass().getName();
|
||||
if (klasse.endsWith("GameProfile")) {
|
||||
Object id = rufe(argument, "getId", "id");
|
||||
return id instanceof UUID ? hex((UUID) id) : null;
|
||||
}
|
||||
if (klasse.endsWith("Property")) {
|
||||
Object wert = rufe(argument, "getValue", "value");
|
||||
if (!(wert instanceof String)) return null;
|
||||
String json = new String(Base64.getDecoder().decode(((String) wert).trim()), StandardCharsets.UTF_8);
|
||||
Matcher m = ID_MUSTER.matcher(json);
|
||||
return m.find() ? m.group(1).replace("-", "").toLowerCase() : null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static String hex(UUID u) {
|
||||
return u.toString().replace("-", "").toLowerCase();
|
||||
}
|
||||
|
||||
private static Object rufe(Object ziel, String... namen) throws Exception {
|
||||
for (String n : namen) {
|
||||
try {
|
||||
Method m = ziel.getClass().getMethod(n);
|
||||
return m.invoke(ziel);
|
||||
} catch (NoSuchMethodException ignoriert) {
|
||||
// nächster Name
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ---------- Umhang-Server ----------
|
||||
private String umhangUrl(String id) {
|
||||
Eintrag e = speicher.get(id);
|
||||
if (e != null && e.frisch()) return e.url;
|
||||
// Auf dem Haupt-/Render-Thread nie warten: dort nur im Hintergrund holen;
|
||||
// Minecraft fragt beim nächsten Laden des Spielers erneut
|
||||
String thread = Thread.currentThread().getName();
|
||||
boolean hauptThread = thread.equals("Render thread") || thread.equals("Client thread") || thread.equals("main");
|
||||
if (hauptThread) {
|
||||
if (unterwegs.putIfAbsent(id, Boolean.TRUE) == null) {
|
||||
Thread t = new Thread(() -> { try { hole(id); } finally { unterwegs.remove(id); } }, "AeroMC-Umhang");
|
||||
t.setDaemon(true);
|
||||
t.start();
|
||||
}
|
||||
return e != null ? e.url : null;
|
||||
}
|
||||
return hole(id);
|
||||
}
|
||||
|
||||
private String hole(String id) {
|
||||
try {
|
||||
HttpURLConnection c = (HttpURLConnection) new URL(basis + "/capes?uuids=" + id).openConnection();
|
||||
c.setConnectTimeout(2000);
|
||||
c.setReadTimeout(3000);
|
||||
c.setRequestProperty("User-Agent", "AeroMC-Umhang/1.0");
|
||||
c.setRequestProperty("Accept", "application/json");
|
||||
if (c.getResponseCode() != 200) {
|
||||
speicher.put(id, new Eintrag(null));
|
||||
return null;
|
||||
}
|
||||
String json;
|
||||
try (InputStream in = c.getInputStream()) {
|
||||
ByteArrayOutputStream puffer = new ByteArrayOutputStream();
|
||||
byte[] b = new byte[4096];
|
||||
int n;
|
||||
while ((n = in.read(b)) > 0 && puffer.size() < 65536) puffer.write(b, 0, n);
|
||||
json = new String(puffer.toByteArray(), StandardCharsets.UTF_8);
|
||||
}
|
||||
String url = null;
|
||||
Matcher m = URL_MUSTER.matcher(json);
|
||||
if (json.contains("\"" + id + "\"") && m.find()) {
|
||||
url = m.group(1).replace("\\/", "/");
|
||||
if (!url.startsWith("https://")) url = null;
|
||||
}
|
||||
speicher.put(id, new Eintrag(url));
|
||||
return url;
|
||||
} catch (Throwable t) {
|
||||
// Server nicht erreichbar: kurz merken, damit nicht jeder Spieler hängt
|
||||
speicher.put(id, new Eintrag(null));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Umhang einsetzen ----------
|
||||
private static Object textur(ClassLoader cl, String url) throws Exception {
|
||||
Class<?> k = Class.forName("com.mojang.authlib.minecraft.MinecraftProfileTexture", false, cl);
|
||||
Constructor<?> ctor = k.getConstructor(String.class, Map.class);
|
||||
return ctor.newInstance(url, null);
|
||||
}
|
||||
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
private static Object mitUmhang(Object ergebnis, String url, Object argument) throws Exception {
|
||||
ClassLoader cl = argument.getClass().getClassLoader();
|
||||
if (ergebnis instanceof Map) {
|
||||
// authlib 1–4: Map<MinecraftProfileTexture.Type, MinecraftProfileTexture>
|
||||
Class<?> typ = Class.forName("com.mojang.authlib.minecraft.MinecraftProfileTexture$Type", false, cl);
|
||||
Object cape = Enum.valueOf((Class<Enum>) typ, "CAPE");
|
||||
Map neu = new HashMap((Map) ergebnis);
|
||||
neu.put(cape, textur(cl, url));
|
||||
return neu;
|
||||
}
|
||||
if (ergebnis == null) return null;
|
||||
// authlib 5+: record MinecraftProfileTextures(skin, cape, elytra, signatureState)
|
||||
Class<?> k = ergebnis.getClass();
|
||||
Object skin = k.getMethod("skin").invoke(ergebnis);
|
||||
Object elytra = k.getMethod("elytra").invoke(ergebnis);
|
||||
Object signatur = k.getMethod("signatureState").invoke(ergebnis);
|
||||
for (Constructor<?> ctor : k.getConstructors()) {
|
||||
if (ctor.getParameterCount() == 4) {
|
||||
return ctor.newInstance(skin, textur(cl, url), elytra, signatur);
|
||||
}
|
||||
}
|
||||
return ergebnis;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package de.aeromc.capes;
|
||||
|
||||
import java.lang.instrument.ClassFileTransformer;
|
||||
import java.security.ProtectionDomain;
|
||||
|
||||
import org.objectweb.asm.ClassReader;
|
||||
import org.objectweb.asm.ClassVisitor;
|
||||
import org.objectweb.asm.ClassWriter;
|
||||
import org.objectweb.asm.MethodVisitor;
|
||||
import org.objectweb.asm.Opcodes;
|
||||
|
||||
/**
|
||||
* Baut YggdrasilMinecraftSessionService beim Laden um: vor jedem "return" der
|
||||
* Textur-Methode wird das Ergebnis an den Nachschlage-Dienst gereicht, der den
|
||||
* AeroMC-Umhang einsetzt.
|
||||
*
|
||||
* authlib 5+ (Minecraft 1.20.2 … 26.x):
|
||||
* MinecraftProfileTextures unpackTextures(Property)
|
||||
* authlib 1–4 (Minecraft 1.8 … 1.20.1):
|
||||
* Map<Type, MinecraftProfileTexture> getTextures(GameProfile, boolean)
|
||||
*
|
||||
* Eingefügt wird nur geradliniger Code ohne Sprünge – die vorhandenen
|
||||
* Stack-Map-Frames bleiben gültig, es muss nichts neu berechnet werden.
|
||||
*/
|
||||
final class Umbau implements ClassFileTransformer {
|
||||
static final String ZIEL = "com/mojang/authlib/yggdrasil/YggdrasilMinecraftSessionService";
|
||||
|
||||
@Override
|
||||
public byte[] transform(ClassLoader loader, String name, Class<?> umdefiniert, ProtectionDomain pd, byte[] klasse) {
|
||||
if (!ZIEL.equals(name) || umdefiniert != null) return null;
|
||||
try {
|
||||
ClassReader leser = new ClassReader(klasse);
|
||||
ClassWriter schreiber = new ClassWriter(leser, ClassWriter.COMPUTE_MAXS);
|
||||
final int[] umgebaut = {0};
|
||||
leser.accept(new ClassVisitor(Opcodes.ASM9, schreiber) {
|
||||
@Override
|
||||
public MethodVisitor visitMethod(int zugriff, String mname, String desc, String sig, String[] ausn) {
|
||||
MethodVisitor mv = super.visitMethod(zugriff, mname, desc, sig, ausn);
|
||||
if ((zugriff & Opcodes.ACC_STATIC) != 0) return mv;
|
||||
String rueckgabe = null;
|
||||
if ("unpackTextures".equals(mname) && desc.startsWith("(Lcom/mojang/authlib/properties/Property;)L")) {
|
||||
rueckgabe = desc.substring(desc.indexOf(")L") + 2, desc.length() - 1);
|
||||
} else if ("getTextures".equals(mname) && desc.equals("(Lcom/mojang/authlib/GameProfile;Z)Ljava/util/Map;")) {
|
||||
rueckgabe = "java/util/Map";
|
||||
}
|
||||
if (rueckgabe == null) return mv;
|
||||
umgebaut[0]++;
|
||||
final String typ = rueckgabe;
|
||||
return new MethodVisitor(Opcodes.ASM9, mv) {
|
||||
@Override
|
||||
public void visitInsn(int op) {
|
||||
if (op == Opcodes.ARETURN) {
|
||||
// Stapel: [ergebnis] -> [dienst, ergebnis, argument] -> apply -> [neu]
|
||||
super.visitVarInsn(Opcodes.ALOAD, 1);
|
||||
super.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System", "getProperties", "()Ljava/util/Properties;", false);
|
||||
super.visitLdcInsn(Agent.SCHLUESSEL);
|
||||
super.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/util/Properties", "get", "(Ljava/lang/Object;)Ljava/lang/Object;", false);
|
||||
super.visitTypeInsn(Opcodes.CHECKCAST, "java/util/function/BiFunction");
|
||||
super.visitInsn(Opcodes.DUP_X2);
|
||||
super.visitInsn(Opcodes.POP);
|
||||
super.visitMethodInsn(Opcodes.INVOKEINTERFACE, "java/util/function/BiFunction", "apply",
|
||||
"(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;", true);
|
||||
super.visitTypeInsn(Opcodes.CHECKCAST, typ);
|
||||
}
|
||||
super.visitInsn(op);
|
||||
}
|
||||
};
|
||||
}
|
||||
}, 0);
|
||||
if (umgebaut[0] == 0) return null;
|
||||
System.out.println("[AeroMC] authlib fuer Umhaenge vorbereitet (" + umgebaut[0] + " Methode)");
|
||||
return schreiber.toByteArray();
|
||||
} catch (Throwable t) {
|
||||
System.out.println("[AeroMC] authlib konnte nicht angepasst werden: " + t);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.util.Enumeration;
|
||||
import java.util.jar.Attributes;
|
||||
import java.util.jar.JarEntry;
|
||||
import java.util.jar.JarFile;
|
||||
import java.util.jar.JarOutputStream;
|
||||
import java.util.jar.Manifest;
|
||||
|
||||
import org.objectweb.asm.ClassReader;
|
||||
import org.objectweb.asm.ClassWriter;
|
||||
import org.objectweb.asm.commons.ClassRemapper;
|
||||
import org.objectweb.asm.commons.Remapper;
|
||||
|
||||
/**
|
||||
* Baut die Agent-Datei: eigene Klassen + ASM, dabei org/objectweb/asm nach
|
||||
* de/aeromc/capes/asm umbenannt. So kann das ASM von Fabric/Forge (das auf
|
||||
* demselben Klassenpfad liegt) nie mit unserem verwechselt werden.
|
||||
*
|
||||
* Aufruf: java Shade <ziel.jar> <klassenordner> <asm.jar>
|
||||
*/
|
||||
public class Shade {
|
||||
static final Remapper UMBENENNEN = new Remapper() {
|
||||
@Override
|
||||
public String map(String name) {
|
||||
return name.startsWith("org/objectweb/asm/") ? "de/aeromc/capes/asm/" + name.substring(18) : name;
|
||||
}
|
||||
};
|
||||
|
||||
static byte[] umbenennen(byte[] klasse) {
|
||||
ClassReader r = new ClassReader(klasse);
|
||||
ClassWriter w = new ClassWriter(0);
|
||||
r.accept(new ClassRemapper(w, UMBENENNEN), 0);
|
||||
return w.toByteArray();
|
||||
}
|
||||
|
||||
static byte[] lies(InputStream in) throws Exception {
|
||||
ByteArrayOutputStream b = new ByteArrayOutputStream();
|
||||
byte[] p = new byte[8192];
|
||||
int n;
|
||||
while ((n = in.read(p)) > 0) b.write(p, 0, n);
|
||||
return b.toByteArray();
|
||||
}
|
||||
|
||||
public static void main(String[] a) throws Exception {
|
||||
Manifest mf = new Manifest();
|
||||
Attributes at = mf.getMainAttributes();
|
||||
at.put(Attributes.Name.MANIFEST_VERSION, "1.0");
|
||||
at.putValue("Premain-Class", "de.aeromc.capes.Agent");
|
||||
at.putValue("Agent-Class", "de.aeromc.capes.Agent");
|
||||
at.putValue("Can-Retransform-Classes", "false");
|
||||
at.putValue("Implementation-Title", "AeroMC Umhang-Agent");
|
||||
try (JarOutputStream out = new JarOutputStream(new FileOutputStream(a[0]), mf)) {
|
||||
// eigene Klassen
|
||||
File basis = new File(a[1]);
|
||||
for (File f : Files.walk(basis.toPath()).map(p -> p.toFile()).filter(f -> f.getName().endsWith(".class")).toArray(File[]::new)) {
|
||||
String name = basis.toPath().relativize(f.toPath()).toString().replace('\\', '/');
|
||||
out.putNextEntry(new JarEntry(name));
|
||||
out.write(umbenennen(Files.readAllBytes(f.toPath())));
|
||||
out.closeEntry();
|
||||
}
|
||||
// ASM (umbenannt), ohne Modul-Infos und Signaturen
|
||||
try (JarFile asm = new JarFile(a[2])) {
|
||||
Enumeration<JarEntry> e = asm.entries();
|
||||
while (e.hasMoreElements()) {
|
||||
JarEntry je = e.nextElement();
|
||||
String n = je.getName();
|
||||
if (!n.startsWith("org/objectweb/asm/") || !n.endsWith(".class")) continue;
|
||||
byte[] neu = umbenennen(lies(asm.getInputStream(je)));
|
||||
out.putNextEntry(new JarEntry("de/aeromc/capes/asm/" + n.substring(18)));
|
||||
out.write(neu);
|
||||
out.closeEntry();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user