Merge pull request #11 from layue13/feat/spigot-profile-adapter

perf: Spigot/CraftBukkit 1.7.10 上读取 GameProfile 也不再用反射
This commit is contained in:
layue13 authored and GitHub committed 2026-10-22 20:23:49 +08:00
commit 0253130760
11 files changed
+166 -9

No files matched your search

+3 -1
View File
@@ -63,7 +63,9 @@ val verifyBackendJar = tasks.register("verifyBackendJar") {
check(names.count { name -> name == it } == 1) { "Backend jar must contain exactly one $it" }
}
check(names.none { it.startsWith("org/bukkit/") }) { "Bukkit API must remain server-provided" }
check(names.none { it.startsWith("com/mojang/") }) { "authlib must remain server-provided" }
check(names.none { it.startsWith("com/mojang/") || it.startsWith("net/minecraft/") }) {
"authlib and Minecraft classes must remain server-provided"
}
entries.filter { it.name.endsWith(".class") }.forEach { entry ->
DataInputStream(archive.getInputStream(entry)).use { input ->
input.readInt()
@@ -5,7 +5,8 @@ import java.util.logging.Logger;
/**
* How the backend reaches a player's GameProfile properties on the running server. Bukkit has no API for them, so
* each server family needs its own access. A compiled adapter is chosen once at startup; servers without one use the
* cached reflective fallback.
* cached reflective fallback. The two server families never share class names (CraftBukkit's package is versioned
* only on Spigot, and only Spigot relocates authlib), so at most one compiled adapter can load.
*/
interface ProfileAccess {
/** True when this access knows the player's class; other players go to the reflective fallback. */
@@ -25,13 +26,17 @@ interface ProfileAccess {
/** A short name for the startup log. */
String description();
/** The compiled Uranium adapter when its classes are present, otherwise the reflective fallback. */
/** The first compiled adapter whose classes are present (Uranium, then Spigot 1.7.10), else the fallback. */
static ProfileAccess select() {
ProfileAccess selected;
try {
selected = UraniumProfileAccess.create();
} catch (LinkageError notUranium) {
selected = ReflectiveProfileAccess.INSTANCE;
try {
selected = SpigotProfileAccess.create();
} catch (LinkageError notSpigot) {
selected = ReflectiveProfileAccess.INSTANCE;
}
}
Logger.getLogger(ProfileAccess.class.getName()).info("GameProfile access: " + selected.description());
return selected;
@@ -6,7 +6,7 @@ import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
/**
* Fallback for servers without a compiled adapter (Spigot/CraftBukkit 1.7.10 and others): finds the GameProfile
* Fallback for servers without a compiled adapter (neither Uranium nor Spigot/CraftBukkit 1.7.10): finds the GameProfile
* methods by name. Lookups are resolved once per receiver class and cached, including misses; a missing method means
* the feature is unavailable on this server, never a guess.
*/
@@ -0,0 +1,44 @@
package dev.moonbridge.bukkit;
import net.minecraft.util.com.mojang.authlib.GameProfile;
import net.minecraft.util.com.mojang.authlib.properties.Property;
import net.minecraft.util.com.mojang.authlib.properties.PropertyMap;
import org.bukkit.craftbukkit.v1_7_R4.entity.CraftPlayer;
/**
* Direct calls into Spigot and CraftBukkit 1.7.10 (v1_7_R4), which relocate authlib and Guava under
* {@code net.minecraft.util}. Compiled against the stubs in src/serverStubs, whose signatures were read with javap from
* spigot-1.7.10-R0.1-SNAPSHOT (build 1649) and craftbukkit-1.7.10-R0.1; both builds declare them identically.
*/
final class SpigotProfileAccess implements ProfileAccess {
private SpigotProfileAccess() { }
/** Throws {@link LinkageError} when the server lacks these classes, so selection happens at startup. */
static SpigotProfileAccess create() {
// Class literals load the classes now rather than on the first join.
Class<?>[] required = {CraftPlayer.class, GameProfile.class, PropertyMap.class, Property.class};
return required.length > 0 ? new SpigotProfileAccess() : null;
}
@Override public boolean handles(Object player) { return player instanceof CraftPlayer; }
@Override public Object properties(Object player) {
if (!(player instanceof CraftPlayer)) return null;
GameProfile profile = ((CraftPlayer) player).getProfile();
return profile == null ? null : profile.getProperties();
}
@Override public Iterable<?> get(Object properties, String name) {
return ((PropertyMap) properties).get(name);
}
@Override public boolean remove(Object properties, String name, Object property) {
return ((PropertyMap) properties).remove(name, property);
}
@Override public Object value(Object property) {
return ((Property) property).getValue();
}
@Override public String description() { return "compiled Spigot/CraftBukkit 1.7.10 adapter"; }
}
+7 -3
View File
@@ -1,4 +1,8 @@
Compile-only copies of the server classes `UraniumProfileAccess` calls. Their names and signatures match Uranium
(`TDLM/Uranium`, `org.bukkit.craftbukkit.entity.CraftPlayer#getProfile`) and authlib 1.5.16 exactly; the bodies only
serve tests. This source set is never packaged: the running server provides the real classes, and
Compile-only copies of the server classes the compiled profile adapters call; the bodies only serve tests.
- `UraniumProfileAccess`: Uranium (`TDLM/Uranium`, `org.bukkit.craftbukkit.entity.CraftPlayer#getProfile`) and
authlib 1.5.16 (`com.mojang.authlib`).
- `SpigotProfileAccess`: Spigot and CraftBukkit 1.7.10 (`org.bukkit.craftbukkit.v1_7_R4.entity.CraftPlayer`, authlib
relocated to `net.minecraft.util.com.mojang.authlib`), read with javap from spigot-1.7.10-R0.1-SNAPSHOT build 1649
and craftbukkit-1.7.10-R0.1. This source set is never packaged: the running server provides the real classes, and
`verifyBackendJar` fails if any of them reaches the backend jar.
@@ -0,0 +1,10 @@
package net.minecraft.util.com.mojang.authlib;
import net.minecraft.util.com.mojang.authlib.properties.PropertyMap;
/** Stub of the GameProfile Spigot/CraftBukkit 1.7.10 relocates under net.minecraft.util. */
public class GameProfile {
private final PropertyMap properties = new PropertyMap();
public PropertyMap getProperties() { return properties; }
}
@@ -0,0 +1,10 @@
package net.minecraft.util.com.mojang.authlib.properties;
/** Stub of the relocated Property. */
public class Property {
private final String value;
public Property(String name, String value) { this.value = value; }
public String getValue() { return value; }
}
@@ -0,0 +1,32 @@
package net.minecraft.util.com.mojang.authlib.properties;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Stub of the relocated PropertyMap. The real class extends the relocated Guava ForwardingMultimap, which declares
* these two methods; calls compiled against this stub resolve to them at run time.
*/
public class PropertyMap {
private final Map<Object, List<Property>> entries = new HashMap<Object, List<Property>>();
public Collection<Property> get(Object key) {
List<Property> values = entries.get(key);
return values == null ? new ArrayList<Property>() : new ArrayList<Property>(values);
}
public boolean remove(Object key, Object value) {
List<Property> values = entries.get(key);
return values != null && values.remove(value);
}
/** Test helper only; the backend never calls it. */
public boolean put(String key, Property value) {
List<Property> values = entries.get(key);
if (values == null) entries.put(key, values = new ArrayList<Property>());
return values.add(value);
}
}
@@ -0,0 +1,12 @@
package org.bukkit.craftbukkit.v1_7_R4.entity;
import net.minecraft.util.com.mojang.authlib.GameProfile;
/** Stub of Spigot/CraftBukkit 1.7.10's CraftPlayer: only the method the backend calls. */
public class CraftPlayer {
private final GameProfile profile;
public CraftPlayer(GameProfile profile) { this.profile = profile; }
public GameProfile getProfile() { return profile; }
}
@@ -0,0 +1,38 @@
package dev.moonbridge.bukkit;
import dev.moonbridge.messaging.session.ForwardedSessionProof;
import net.minecraft.util.com.mojang.authlib.GameProfile;
import net.minecraft.util.com.mojang.authlib.properties.Property;
import org.bukkit.craftbukkit.v1_7_R4.entity.CraftPlayer;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
/** The compiled Spigot/CraftBukkit 1.7.10 adapter against stubs carrying the javap-verified signatures. */
class SpigotProfileAccessTest {
@Test void compiledAdapterRemovesTheProofAndKeepsOtherProperties() {
GameProfile profile = new GameProfile();
Property proof = new Property(ForwardedSessionProof.PROPERTY_NAME, "proof-value");
Property textures = new Property("textures", "texture-value");
profile.getProperties().put(ForwardedSessionProof.PROPERTY_NAME, proof);
profile.getProperties().put("textures", textures);
SpigotProfileAccess access = SpigotProfileAccess.create();
CraftPlayer player = new CraftPlayer(profile);
assertTrue(access.handles(player));
OwnedBukkitSessionService.ProofProperty stripped = OwnedBukkitSessionService.stripForwardedProof(access, player);
assertTrue(stripped.removed);
assertEquals("proof-value", stripped.proof);
assertTrue(profile.getProperties().get(ForwardedSessionProof.PROPERTY_NAME).isEmpty());
assertSame(textures, profile.getProperties().get("textures").iterator().next());
}
@Test void eachAdapterOnlyHandlesItsOwnServersPlayers() {
Object uraniumPlayer = new org.bukkit.craftbukkit.entity.CraftPlayer(new com.mojang.authlib.GameProfile());
Object spigotPlayer = new CraftPlayer(new GameProfile());
assertFalse(SpigotProfileAccess.create().handles(uraniumPlayer));
assertFalse(UraniumProfileAccess.create().handles(spigotPlayer));
assertTrue(SpigotProfileAccess.create().handles(spigotPlayer));
}
}
+1 -1
View File
@@ -104,7 +104,7 @@ Proxy 为每个插件管理独立的 scope,在停用时自动撤销。项目
在 `OFFLINE` 代理模式中,只有显式配置了该 backend 的 session-binding secret 时,代理才会在初始连接或转服时转发由代理派生的 offline UUID 和会话证明;这证明代理连接身份,不表示 Mojang 已认证。没有配置 secret 的 offline backend 仍接收原始握手。静态 backend 或尚未获得正数 registration epoch 的目标不会收到会话证明。
后端宿主在玩家加入时从 GameProfile 属性中取出并移除会话证明。Bukkit API 不提供 GameProfile,所以按服务器选择访问方式,启动日志会写明 `GameProfile access: …`:Uranium(`org.bukkit.craftbukkit.entity.CraftPlayer`、未重定位的 authlib)使用编译进 JAR 的直接调用,没有反射;其他服务器(例如 Spigot/CraftBukkit 1.7.10)使用按类缓存的反射回退。编译调用与服务器实际签名不符时,宿主记一条警告后改用反射回退。两条路径都找不到 GameProfile 时,该玩家没有会话绑定,不会猜测。
后端宿主在玩家加入时从 GameProfile 属性中取出并移除会话证明。Bukkit API 不提供 GameProfile,所以按服务器选择访问方式,启动日志会写明 `GameProfile access: …`:Uranium(`org.bukkit.craftbukkit.entity.CraftPlayer`、未重定位的 authlib)和 Spigot/CraftBukkit 1.7.10(`v1_7_R4`、authlib 重定位到 `net.minecraft.util`)都使用编译进 JAR 的直接调用,没有反射;其他服务器使用按类缓存的反射回退。编译调用与服务器实际签名不符时,宿主记一条警告后改用反射回退。两条路径都找不到 GameProfile 时,该玩家没有会话绑定,不会猜测。
实例的游戏地址必须是代理实际可达的 `tcp://host:port`。Docker 中使用容器服务名,容器间不要使用 `127.0.0.1`。仅将玩家端口公开,控制端口放在私有网络。不要把真实凭据写入镜像或提交到仓库。