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:
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();
|
||||
}
|
||||
}
|
||||
+17
@@ -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 }
|
||||
}
|
||||
Reference in new issue
Block a user