package io.gitlab.jfronny.betterwhitelist.server; import com.mojang.authlib.GameProfile; import com.mojang.brigadier.context.CommandContext; import io.gitlab.jfronny.betterwhitelist.BetterWhitelist; import io.gitlab.jfronny.betterwhitelist.server.mixin.ServerLoginNetworkHandlerAccessor; import io.gitlab.jfronny.commons.StringFormatter; import io.gitlab.jfronny.muscript.compiler.Parser; import io.gitlab.jfronny.muscript.data.Script; import io.gitlab.jfronny.muscript.data.dynamic.Dynamic; import io.gitlab.jfronny.muscript.error.LocationalException; import net.fabricmc.api.DedicatedServerModInitializer; import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback; import net.fabricmc.fabric.api.networking.v1.*; import net.fabricmc.loader.api.FabricLoader; import net.minecraft.network.PacketByteBuf; import net.minecraft.server.command.ServerCommandSource; import net.minecraft.server.network.ServerLoginNetworkHandler; import net.minecraft.text.Text; import net.minecraft.util.Util; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.*; import java.util.concurrent.*; import static net.minecraft.server.command.CommandManager.literal; public class BetterWhitelistServer implements DedicatedServerModInitializer { private String scriptSource; private Script script; private final Map challenges = new HashMap<>(); @Override public void onInitializeServer() { CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, environment) -> { dispatcher.register( literal(BetterWhitelist.MOD_ID) .requires(source -> source.hasPermissionLevel(4)) .executes(this::printVersion) .then(literal("reload").executes(this::reloadScript)) ); }); ServerLoginConnectionEvents.QUERY_START.register((handler, server, sender, synchronizer) -> { GameProfile gp = profile(handler); Challenge challenge = new Challenge(gp, sender); challenges.put(gp, challenge); PacketByteBuf handshake = PacketByteBufs.create(); handshake.writeInt(BetterWhitelist.PROTOCOL_VERSION); challenge.sender.sendPacket(BetterWhitelist.HANDSHAKE_CHANNEL, handshake); synchronizer.waitFor(challenge.challengeCompleted); }); ServerLoginNetworking.registerGlobalReceiver(BetterWhitelist.HANDSHAKE_CHANNEL, (server, handler, understood, buf, synchronizer, responseSender) -> { if (!understood) { handler.disconnect(Text.literal("This server requires better-whitelist to be installed")); return; } try { if (buf.readInt() != BetterWhitelist.PROTOCOL_VERSION) { handler.disconnect(Text.literal("This server requires a version of better-whitelist supporting the protocol version " + BetterWhitelist.PROTOCOL_VERSION)); return; } Challenge challenge = challenges.get(profile(handler)); challenge.sender = responseSender; Util.getMainWorkerExecutor().execute(new FutureTask<>(() -> { try { ServerScope.run(script, challenge); challenge.challengeCompleted.complete(null); BetterWhitelist.LOG.info("Completed challenge for " + challenge.profile.getName()); } catch (ServerScope.AssertFail fail) { BetterWhitelist.LOG.warn("Failed challenge for " + challenge.profile.getName() + ": " + fail.getMessage()); if (handler.isConnectionOpen()) handler.disconnect(Text.literal(fail.getMessage())); challenge.challengeCompleted.cancel(); } catch (Throwable t) { BetterWhitelist.LOG.error("Something went wrong while trying to execute a challenge\n" + StringFormatter.toString(t, e -> e instanceof LocationalException le ? le.asPrintable(scriptSource).toString() : e.toString() )); if (handler.isConnectionOpen()) handler.disconnect(Text.literal("Something went wrong")); challenge.challengeCompleted.cancel(); } challenges.remove(challenge.profile); return null; })); } catch (Throwable t) { handler.disconnect(Text.literal("Handshake failed")); } }); ServerLoginNetworking.registerGlobalReceiver(BetterWhitelist.CHALLENGE_CHANNEL, (server, handler, understood, buf, synchronizer, responseSender) -> { if (!understood) { handler.disconnect(Text.literal("This server requires better-whitelist to be installed")); return; } try { Challenge ch = challenges.get(profile(handler)); ch.sender = responseSender; String response = buf.readString(); BetterWhitelist.LOG.info("Got response from " + ch.profile.getName() + ": " + response); ch.response.complete(Dynamic.deserialize(response)); } catch (Throwable t) { BetterWhitelist.LOG.error("Failed login", t); handler.disconnect(Text.literal("Invalid dynamic")); } }); try { reloadScript(); } catch (IOException e) { throw new RuntimeException("Could not load whitelist script", e); } } private GameProfile profile(ServerLoginNetworkHandler handler) { GameProfile gp = ((ServerLoginNetworkHandlerAccessor) handler).getProfile(); if (gp == null) throw new NullPointerException("Missing GameProfile"); return gp; } private int printVersion(CommandContext context) { context.getSource().sendMessage(Text.literal("Loaded " + BetterWhitelist.MOD_METADATA.getName() + " " + BetterWhitelist.MOD_METADATA.getVersion())); return 1; } private int reloadScript(CommandContext context) { try { reloadScript(); context.getSource().sendMessage(Text.literal("Successfully reloaded script")); } catch (Throwable t) { BetterWhitelist.LOG.error("Could not reload script", t); context.getSource().sendError(Text.literal("Could not reload script, check server log for details")); } return 1; } private void reloadScript() throws IOException { Path scriptPath = FabricLoader.getInstance() .getConfigDir() .resolve(BetterWhitelist.MOD_ID + ".mu"); if (!Files.exists(scriptPath)) Files.writeString(scriptPath, """ // Use this method to execute a closure on the client and get back the result clientVersion = challenge({ -> // Note that closures sent to the client do not have access to things you declare elsewhere mod('better-whitelist').version }) println("You can, of course, use println-debugging") // You have access to the same methods on the server as you do on the client // You may use the assert method to short-circuit if you encounter a case where the client should not be allowed access // Assert can also have a second argument for the message to send if the assertion fails assert(mod('better-whitelist').version == clientVersion, 'You have the wrong mod version') // you can also send server-evaluated parameters with your challenge assert(challenge({ arg -> arg::allMatch({ v -> mods::values()::anyMatch({ m -> v.id == m.id & v.version == m.version }) }) }, mods::values()::filter({ v -> v.environment != 'server' })::map({ v -> { id = v.id, version = v.version } }))) """); String s = Files.readString(scriptPath); this.script = Parser.parseScript(s); this.scriptSource = s; } }