Complete private authentication, server integrations and activity reporting

Add Fabric/Forge version builds and Paper integration, preserve permanent player identities across renames, harden session authorization, and surface privacy-conscious launcher/server activity in the panel.
This commit is contained in:
SCOPEDD committed 2026-09-28 13:31:17 -04:00
commit a1f19e86c6
74 files changed
+2108 -95

No files matched your search

@@ -0,0 +1,16 @@
package net.scopenet.minecraft.mixin;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.server.network.ServerGamePacketListenerImpl;
import net.minecraft.network.protocol.game.*;
import net.scopenet.minecraft.Bridge;
import org.spongepowered.asm.mixin.*;
import org.spongepowered.asm.mixin.injection.*;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(ServerGamePacketListenerImpl.class)
public abstract class CommandMixin {
@Shadow public ServerPlayer player;
@Inject(method = "handleChatCommand", at = @At("RETURN"))
private void scopenet$command(ServerboundChatCommandPacket packet, CallbackInfo ci) {
Bridge.command(player, packet.command());
}
}
@@ -0,0 +1,28 @@
package net.scopenet.minecraft.mixin;
import com.mojang.authlib.GameProfile;
import net.minecraft.network.Connection;
import net.minecraft.network.chat.Component;
import net.minecraft.server.network.ServerLoginPacketListenerImpl;
import net.scopenet.minecraft.Bridge;
import java.util.concurrent.CompletableFuture;
import org.spongepowered.asm.mixin.*;
import org.spongepowered.asm.mixin.injection.*;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(ServerLoginPacketListenerImpl.class)
public abstract class LoginMixin {
@Shadow private GameProfile gameProfile;
@Shadow @Final public Connection connection;
@Shadow public abstract void disconnect(Component reason);
@Unique private CompletableFuture<String> scopenet$verdict;
@Inject(method = "handleAcceptedLogin", at = @At("HEAD"), cancellable = true)
private void scopenet$gate(CallbackInfo ci) {
if (scopenet$verdict == null)
scopenet$verdict = Bridge.login(gameProfile.getId(), gameProfile.getName(), connection.getRemoteAddress());
if (!scopenet$verdict.isDone()) { ci.cancel(); return; }
String denial = scopenet$verdict.join();
if (denial != null) { disconnect(Component.literal(denial)); ci.cancel(); }
}
}
@@ -0,0 +1,21 @@
package net.scopenet.minecraft.mixin;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.server.network.ServerGamePacketListenerImpl;
import net.minecraft.network.protocol.game.*;
import net.scopenet.minecraft.Bridge;
import org.spongepowered.asm.mixin.*;
import org.spongepowered.asm.mixin.injection.*;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(ServerGamePacketListenerImpl.class)
public abstract class CommandMixin {
@Shadow public ServerPlayer player;
@Inject(method = "handleChatCommand", at = @At("RETURN"))
private void scopenet$command(ServerboundChatCommandPacket packet, CallbackInfo ci) {
Bridge.command(player, packet.command());
}
@Inject(method = "handleSignedChatCommand", at = @At("RETURN"))
private void scopenet$signed(ServerboundChatCommandSignedPacket packet, CallbackInfo ci) {
Bridge.command(player, packet.command());
}
}
@@ -0,0 +1,27 @@
package net.scopenet.minecraft.mixin;
import com.mojang.authlib.GameProfile;
import net.minecraft.network.Connection;
import net.minecraft.network.chat.Component;
import net.minecraft.server.network.ServerLoginPacketListenerImpl;
import net.scopenet.minecraft.Bridge;
import java.util.concurrent.CompletableFuture;
import org.spongepowered.asm.mixin.*;
import org.spongepowered.asm.mixin.injection.*;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(ServerLoginPacketListenerImpl.class)
public abstract class LoginMixin {
@Shadow @Final private Connection connection;
@Shadow public abstract void disconnect(Component reason);
@Unique private CompletableFuture<String> scopenet$verdict;
@Inject(method = "verifyLoginAndFinishConnectionSetup", at = @At("HEAD"), cancellable = true)
private void scopenet$gate(GameProfile profile, CallbackInfo ci) {
if (scopenet$verdict == null)
scopenet$verdict = Bridge.login(profile.getId(), profile.getName(), connection.getRemoteAddress());
if (!scopenet$verdict.isDone()) { ci.cancel(); return; }
String denial = scopenet$verdict.join();
if (denial != null) { disconnect(Component.literal(denial)); ci.cancel(); }
}
}
@@ -0,0 +1,21 @@
package net.scopenet.minecraft.mixin;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.server.network.ServerGamePacketListenerImpl;
import net.minecraft.network.protocol.game.*;
import net.scopenet.minecraft.Bridge;
import org.spongepowered.asm.mixin.*;
import org.spongepowered.asm.mixin.injection.*;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(ServerGamePacketListenerImpl.class)
public abstract class CommandMixin {
@Shadow public ServerPlayer player;
@Inject(method = "handleChatCommand", at = @At("RETURN"))
private void scopenet$command(ServerboundChatCommandPacket packet, CallbackInfo ci) {
Bridge.command(player, packet.command());
}
@Inject(method = "handleSignedChatCommand", at = @At("RETURN"))
private void scopenet$signed(ServerboundChatCommandSignedPacket packet, CallbackInfo ci) {
Bridge.command(player, packet.command());
}
}
@@ -0,0 +1,27 @@
package net.scopenet.minecraft.mixin;
import com.mojang.authlib.GameProfile;
import net.minecraft.network.Connection;
import net.minecraft.network.chat.Component;
import net.minecraft.server.network.ServerLoginPacketListenerImpl;
import net.scopenet.minecraft.Bridge;
import java.util.concurrent.CompletableFuture;
import org.spongepowered.asm.mixin.*;
import org.spongepowered.asm.mixin.injection.*;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(ServerLoginPacketListenerImpl.class)
public abstract class LoginMixin {
@Shadow @Final private Connection connection;
@Shadow public abstract void disconnect(Component reason);
@Unique private CompletableFuture<String> scopenet$verdict;
@Inject(method = "verifyLoginAndFinishConnectionSetup", at = @At("HEAD"), cancellable = true)
private void scopenet$gate(GameProfile profile, CallbackInfo ci) {
if (scopenet$verdict == null)
scopenet$verdict = Bridge.login(profile.id(), profile.name(), connection.getRemoteAddress());
if (!scopenet$verdict.isDone()) { ci.cancel(); return; }
String denial = scopenet$verdict.join();
if (denial != null) { disconnect(Component.literal(denial)); ci.cancel(); }
}
}
@@ -0,0 +1,85 @@
package net.scopenet.minecraft;
import net.scopenet.integration.*;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.network.chat.Component;
import java.net.*;
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.CompletableFuture;
public final class Bridge {
private static volatile Integration integration;
private static final Map<UUID, String> online = new HashMap<>();
private Bridge() {}
public static void start(MinecraftServer server) {
try {
integration = new Integration(Settings.load(Path.of("config/scopenet.properties")),
BuildInfo.LOADER, server.getServerVersion(), BuildInfo.VERSION, server.usesAuthentication(),
server.getMaxPlayers(), System.err::println,
(id, message) -> server.execute(() -> {
ServerPlayer player = server.getPlayerList().getPlayer(id);
if (player != null) player.connection.disconnect(Component.literal(message));
}));
} catch (Exception e) {
// Throwing aborts startup; silently disabling an auth mod is unsafe.
throw new IllegalStateException("SCOPENET cannot start: " + e.getMessage(), e);
}
}
public static CompletableFuture<String> login(UUID uuid, String name, SocketAddress address) {
Integration current = integration;
if (current == null) return CompletableFuture.completedFuture(Integration.UNAVAILABLE);
String ip = address instanceof InetSocketAddress socket && socket.getAddress() != null
? socket.getAddress().getHostAddress() : null;
return current.login(uuid, name, ip);
}
public static void tick(MinecraftServer server) {
Integration current = integration;
if (current == null) return;
Set<UUID> present = new HashSet<>();
for (ServerPlayer player : server.getPlayerList().getPlayers()) {
UUID id = player.getUUID();
present.add(id);
if (!online.containsKey(id)) {
String name = player.getName().getString();
online.put(id, name);
current.activity.join(id, name);
}
}
online.entrySet().removeIf(entry -> {
if (present.contains(entry.getKey())) return false;
current.activity.leave(entry.getKey(), entry.getValue());
return true;
});
current.tick();
}
public static void stat(ServerPlayer player, String key, int amount) {
Integration current = integration;
if (current == null) return;
current.activity.add(player.getUUID(), player.getName().getString(), key, amount);
if (key.equals("deaths")) current.activity.event(player.getUUID(), player.getName().getString(), "death", null);
}
public static void action(ServerPlayer player, String action, int amount) {
Integration current = integration;
if (current != null) current.activity.action(player.getUUID(), player.getName().getString(), action, amount);
}
public static void command(ServerPlayer player, String command) {
String name = command.strip().split("\\s+", 2)[0];
Integration current = integration;
if (current != null) current.activity.event(player.getUUID(), player.getName().getString(), "command", "/" + name);
}
public static void stop() {
Integration current = integration;
integration = null;
if (current != null) current.close();
online.clear();
}
}
@@ -0,0 +1,17 @@
package net.scopenet.minecraft.mixin;
import net.minecraft.server.PlayerAdvancements;
import net.minecraft.server.level.ServerPlayer;
import net.scopenet.minecraft.Bridge;
import org.spongepowered.asm.mixin.*;
import org.spongepowered.asm.mixin.injection.*;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
@Mixin(PlayerAdvancements.class)
public abstract class AdvancementMixin {
@Shadow private ServerPlayer player;
@Inject(method = "award", at = @At("RETURN"))
private void scopenet$advancement(CallbackInfoReturnable<Boolean> cir) {
if (cir.getReturnValue()) Bridge.action(player, "advancement_criterion", 1);
}
}
@@ -0,0 +1,21 @@
package net.scopenet.minecraft.mixin;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.item.BlockItem;
import net.minecraft.world.item.context.BlockPlaceContext;
import net.scopenet.minecraft.Bridge;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.*;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
@Mixin(BlockItem.class)
public abstract class BlockItemMixin {
@Inject(method = "place(Lnet/minecraft/world/item/context/BlockPlaceContext;)Lnet/minecraft/world/InteractionResult;", at = @At("RETURN"))
private void scopenet$placed(BlockPlaceContext context, CallbackInfoReturnable<InteractionResult> cir) {
if (cir.getReturnValue().consumesAction() && context.getPlayer() instanceof ServerPlayer player) {
Bridge.stat(player, "blocks_placed", 1);
Bridge.action(player, "block_placed:" + context.getClickedPos().toShortString(), 1);
}
}
}
@@ -0,0 +1,18 @@
package net.scopenet.minecraft.mixin;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.server.network.ServerGamePacketListenerImpl;
import net.scopenet.minecraft.Bridge;
import org.spongepowered.asm.mixin.*;
import org.spongepowered.asm.mixin.injection.*;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(ServerGamePacketListenerImpl.class)
public abstract class ChatMixin {
@Shadow public ServerPlayer player;
@Inject(method = "broadcastChatMessage", at = @At("HEAD"))
private void scopenet$chat(CallbackInfo ci) { Bridge.stat(player, "messages", 1); }
@Inject(method = "handleContainerClick", at = @At("RETURN"))
private void scopenet$inventory(CallbackInfo ci) { Bridge.action(player, "inventory_click", 1); }
}
@@ -0,0 +1,16 @@
package net.scopenet.minecraft.mixin;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.dedicated.DedicatedServer;
import net.scopenet.minecraft.Bridge;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.*;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
@Mixin(DedicatedServer.class)
public abstract class DedicatedMixin {
@Inject(method = "initServer", at = @At("RETURN"))
private void scopenet$start(CallbackInfoReturnable<Boolean> cir) {
if (cir.getReturnValue()) Bridge.start((MinecraftServer) (Object) this);
}
}
@@ -0,0 +1,27 @@
package net.scopenet.minecraft.mixin;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.stats.*;
import net.scopenet.minecraft.Bridge;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.*;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(ServerPlayer.class)
public abstract class PlayerMixin {
@Inject(method = "awardStat", at = @At("TAIL"))
private void scopenet$stat(Stat<?> stat, int amount, CallbackInfo ci) {
Bridge.action((ServerPlayer) (Object) this, stat.getName(), amount);
String key = null;
if (stat.getType() == Stats.BLOCK_MINED) key = "blocks_broken";
else if (stat.getType() == Stats.CUSTOM) {
key = switch (stat.getValue().toString()) {
case "minecraft:deaths" -> "deaths";
case "minecraft:player_kills" -> "player_kills";
case "minecraft:mob_kills" -> "mob_kills";
default -> null;
};
}
if (key != null) Bridge.stat((ServerPlayer) (Object) this, key, amount);
}
}
@@ -0,0 +1,16 @@
package net.scopenet.minecraft.mixin;
import net.minecraft.server.MinecraftServer;
import net.scopenet.minecraft.Bridge;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.*;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(MinecraftServer.class)
public abstract class ServerMixin {
@Inject(method = "tickServer", at = @At("TAIL"))
private void scopenet$tick(CallbackInfo ci) { Bridge.tick((MinecraftServer) (Object) this); }
@Inject(method = "stopServer", at = @At("HEAD"))
private void scopenet$stop(CallbackInfo ci) { Bridge.stop(); }
}
@@ -0,0 +1,9 @@
{
"required": true,
"minVersion": "0.8",
"package": "net.scopenet.minecraft.mixin",
"compatibilityLevel": "JAVA_17",
"refmap": "scopenet.refmap.json",
"mixins": ["DedicatedMixin", "ServerMixin", "LoginMixin", "PlayerMixin", "BlockItemMixin", "ChatMixin", "CommandMixin", "AdvancementMixin"],
"injectors": { "defaultRequire": 1 }
}