diff --git a/README.md b/README.md index 48bb6f0ed7..232099ceca 100644 --- a/README.md +++ b/README.md @@ -9,11 +9,18 @@ A list of small tweaks I made: - Implement player UUID rewrite, like what bungeecord does. Make setup with online velocity + offline Minecraft server work correctly (`online-mode=true` on velocity + `online-mode=false` on backend mc servers + `player-info-forwarding-mode=none`) - - TabList packets rewrite - - Affects `LegacyPlayerListItemPacket`, `UpsertPlayerInfoPacket`, `RemovePlayerInfoPacket` packets - - Rewrites player UUIDs inside those packets to their UUIDs in the velocity server - - Entity packets rewrite - - Rewrites player UUIDs inside player creation packets and spectator teleport packets, to their UUIDs in the velocity server + - Packets to rewrite: + - TabList packets + - Affects `LegacyPlayerListItemPacket`, `UpsertPlayerInfoPacket`, `RemovePlayerInfoPacket` packets + - Rewrites player UUIDs inside those packets to their UUIDs in the velocity server + - Entity packets + - Rewrites player UUIDs inside player creation packets and spectator teleport packets, to their UUIDs in the velocity server + - All related configs are under section `uuid-rewrite` in `velocity.toml` + - Optional external uuid mapping sqlite database support + - Enabled with `databaseEnabled = true`, database path configurable with `databasePath` + - Mapping between online / offline uuid will be updated on player connected + - The sqlite database file can be shared between multiple velocity instances + - UUID rewrite can be disabled by setting `enabled = false` # Velocity diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a3d0523dcb..7ca1ad841d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -57,6 +57,7 @@ nightconfig = "com.electronwill.night-config:toml:3.6.7" slf4j = "org.slf4j:slf4j-api:2.0.12" snakeyaml = "org.yaml:snakeyaml:1.33" spotbugs-annotations = "com.github.spotbugs:spotbugs-annotations:4.7.3" +sqlite-jdbc = "org.xerial:sqlite-jdbc:3.46.0.1" # [fallen's fork] player uuid rewrite - uuid database terminalconsoleappender = "net.minecrell:terminalconsoleappender:1.3.0" [bundles] diff --git a/proxy/build.gradle.kts b/proxy/build.gradle.kts index 5e1387b06c..d60773862e 100644 --- a/proxy/build.gradle.kts +++ b/proxy/build.gradle.kts @@ -126,6 +126,7 @@ dependencies { implementation(libs.lmbda) implementation(libs.asm) implementation(libs.bundles.flare) + implementation(libs.sqlite.jdbc) // [fallen's fork] player uuid rewrite - uuid database compileOnly(libs.spotbugs.annotations) compileOnly(libs.auto.service.annotations) testImplementation(libs.mockito) diff --git a/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java b/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java index 192153a085..6de4cfb19d 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java @@ -53,7 +53,6 @@ import com.velocitypowered.proxy.network.ConnectionManager; import com.velocitypowered.proxy.plugin.VelocityPluginManager; import com.velocitypowered.proxy.protocol.ProtocolUtils; -import com.velocitypowered.proxy.protocol.packet.uuidrewrite.TabListUuidRewriter; import com.velocitypowered.proxy.protocol.util.FaviconSerializer; import com.velocitypowered.proxy.protocol.util.GameProfileSerializer; import com.velocitypowered.proxy.scheduler.VelocityScheduler; @@ -64,6 +63,7 @@ import com.velocitypowered.proxy.util.VelocityChannelRegistrar; import com.velocitypowered.proxy.util.ratelimit.Ratelimiter; import com.velocitypowered.proxy.util.ratelimit.Ratelimiters; +import com.velocitypowered.proxy.uuidrewrite.UuidRewriteHooks; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import io.netty.bootstrap.Bootstrap; import io.netty.channel.Channel; @@ -264,6 +264,9 @@ void start() { this.cm.queryBind(configuration.getBind().getHostString(), configuration.getQueryPort()); } + // [fallen's fork] player uuid rewrite - lifecycle hook + UuidRewriteHooks.onServerStart(this); + Metrics.VelocityMetrics.startMetrics(this, configuration.getMetrics()); } @@ -513,6 +516,9 @@ public void shutdown(boolean explicitExit, Component reason) { player.disconnect(reason); } + // [fallen's fork] player uuid rewrite - lifecycle hook + UuidRewriteHooks.onServerStop(this); + try { boolean timedOut = false; @@ -643,6 +649,10 @@ public boolean registerConnection(ConnectedPlayer connection) { connectionsByName.put(lowerName, connection); connectionsByUuid.put(connection.getUniqueId(), connection); } + + // [fallen's fork] player uuid rewrite - lifecycle hook + UuidRewriteHooks.onPlayerConnect(this, connection); + return true; } @@ -656,8 +666,8 @@ public void unregisterConnection(ConnectedPlayer connection) { connectionsByUuid.remove(connection.getUniqueId(), connection); connection.disconnected(); - // [fallen's fork] player uuid rewrite - - TabListUuidRewriter.onPlayerDisconnect(this, connection); + // [fallen's fork] player uuid rewrite - lifecycle hook + UuidRewriteHooks.onPlayerDisconnect(this, connection); } @Override diff --git a/proxy/src/main/java/com/velocitypowered/proxy/config/VelocityConfiguration.java b/proxy/src/main/java/com/velocitypowered/proxy/config/VelocityConfiguration.java index 14cb8caa10..dc34165a80 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/config/VelocityConfiguration.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/config/VelocityConfiguration.java @@ -86,6 +86,9 @@ public class VelocityConfiguration implements ProxyConfig { // [fallen's fork] mojang auth proxy @Expose private final AuthProxy authProxy; + // [fallen's fork] player uuid rewrite + @Expose + private final UuidRewrite uuidRewrite; @Expose private final Query query; @@ -99,11 +102,13 @@ public class VelocityConfiguration implements ProxyConfig { private VelocityConfiguration(Servers servers, ForcedHosts forcedHosts, Advanced advanced, AuthProxy authProxy, // [fallen's fork] mojang auth proxy + UuidRewrite uuidRewrite, // [fallen's fork] player uuid rewrite Query query, Metrics metrics) { this.servers = servers; this.forcedHosts = forcedHosts; this.advanced = advanced; this.authProxy = authProxy; // [fallen's fork] mojang auth proxy + this.uuidRewrite = uuidRewrite; // [fallen's fork] player uuid rewrite this.query = query; this.metrics = metrics; } @@ -115,6 +120,7 @@ private VelocityConfiguration(String bind, String motd, int showMaxPlayers, bool boolean enablePlayerAddressLogging, Servers servers, ForcedHosts forcedHosts, Advanced advanced, AuthProxy authProxy, // [fallen's fork] mojang auth proxy + UuidRewrite uuidRewrite, // [fallen's fork] player uuid rewrite Query query, Metrics metrics, boolean forceKeyAuthentication) { this.bind = bind; this.motd = motd; @@ -131,6 +137,7 @@ private VelocityConfiguration(String bind, String motd, int showMaxPlayers, bool this.forcedHosts = forcedHosts; this.advanced = advanced; this.authProxy = authProxy; // [fallen's fork] mojang auth proxy + this.uuidRewrite = uuidRewrite; // [fallen's fork] player uuid rewrite this.query = query; this.metrics = metrics; this.forceKeyAuthentication = forceKeyAuthentication; @@ -431,6 +438,24 @@ public int getAuthProxyPort() { } // [fallen's fork] mojang auth proxy ends + // [fallen's fork] player uuid rewrite starts + public boolean isUuidRewriteEnabled() { + return uuidRewrite.isEnabled(); + } + + public boolean isUuidRewriteDatabaseEnabled() { + return uuidRewrite.isDatabaseEnabled(); + } + + public void setUuidRewriteDatabaseEnabled(boolean b) { + uuidRewrite.setDatabaseEnabled(b); + } + + public String getUuidRewriteDatabasePath() { + return uuidRewrite.getDatabasePath(); + } + // [fallen's fork] player uuid rewrite ends + public boolean isForceKeyAuthentication() { return forceKeyAuthentication; } @@ -524,7 +549,8 @@ public static VelocityConfiguration read(Path path) throws IOException { final CommentedConfig serversConfig = config.get("servers"); final CommentedConfig forcedHostsConfig = config.get("forced-hosts"); final CommentedConfig advancedConfig = config.get("advanced"); - final CommentedConfig autoProxy = config.get("auth-proxy"); + final CommentedConfig autoProxy = config.get("auth-proxy"); // [fallen's fork] mojang auth proxy + final CommentedConfig uuidRewrite = config.get("uuid-rewrite"); // [fallen's fork] player uuid rewrite final CommentedConfig queryConfig = config.get("query"); final CommentedConfig metricsConfig = config.get("metrics"); final PlayerInfoForwarding forwardingMode = config.getEnumOrElse( @@ -566,7 +592,8 @@ public static VelocityConfiguration read(Path path) throws IOException { new Servers(serversConfig), new ForcedHosts(forcedHostsConfig), new Advanced(advancedConfig), - new AuthProxy(autoProxy), + new AuthProxy(autoProxy), // [fallen's fork] mojang auth proxy + new UuidRewrite(uuidRewrite), // [fallen's fork] player uuid rewrite new Query(queryConfig), new Metrics(metricsConfig), forceKeyAuthentication @@ -894,6 +921,42 @@ public int getPort() { } } + /** + * [fallen's fork] player uuid rewrite - config. + */ + private static class UuidRewrite { + @Expose + private boolean enabled = true; + @Expose + private boolean databaseEnabled = false; + @Expose + private String databasePath = "uuid_mapping.db"; + + public UuidRewrite(CommentedConfig config) { + if (config != null) { + this.enabled = config.getOrElse("enabled", true); + this.databaseEnabled = config.getOrElse("databaseEnabled", false); + this.databasePath = config.getOrElse("databasePath", "uuid_mapping.db"); + } + } + + public boolean isEnabled() { + return enabled; + } + + public boolean isDatabaseEnabled() { + return databaseEnabled; + } + + public void setDatabaseEnabled(boolean databaseEnabled) { + this.databaseEnabled = databaseEnabled; + } + + public String getDatabasePath() { + return databasePath; + } + } + private static class Query { @Expose diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BackendPlaySessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BackendPlaySessionHandler.java index e2bd4601d4..030f26333a 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BackendPlaySessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BackendPlaySessionHandler.java @@ -66,11 +66,11 @@ import com.velocitypowered.proxy.protocol.packet.UpsertPlayerInfoPacket; import com.velocitypowered.proxy.protocol.packet.chat.ComponentHolder; import com.velocitypowered.proxy.protocol.packet.config.StartUpdatePacket; -import com.velocitypowered.proxy.protocol.packet.uuidrewrite.EntityPacketUuidRewriter; -import com.velocitypowered.proxy.protocol.packet.uuidrewrite.TabListUuidRewriter; import com.velocitypowered.proxy.protocol.packet.uuidrewrite.UrSpawnEntityS2CPacket; import com.velocitypowered.proxy.protocol.packet.uuidrewrite.UrSpawnPlayerS2CPacket; import com.velocitypowered.proxy.protocol.util.PluginMessageUtil; +import com.velocitypowered.proxy.uuidrewrite.EntityPacketUuidRewriter; +import com.velocitypowered.proxy.uuidrewrite.TabListUuidRewriter; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufUtil; import io.netty.buffer.Unpooled; diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java index c259a8a10c..1b78fc3998 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java @@ -72,10 +72,10 @@ import com.velocitypowered.proxy.protocol.packet.chat.session.SessionPlayerCommandPacket; import com.velocitypowered.proxy.protocol.packet.config.FinishedUpdatePacket; import com.velocitypowered.proxy.protocol.packet.title.GenericTitlePacket; -import com.velocitypowered.proxy.protocol.packet.uuidrewrite.EntityPacketUuidRewriter; import com.velocitypowered.proxy.protocol.packet.uuidrewrite.UrSpectatorTeleportC2SPacket; import com.velocitypowered.proxy.protocol.util.PluginMessageUtil; import com.velocitypowered.proxy.util.CharacterUtil; +import com.velocitypowered.proxy.uuidrewrite.EntityPacketUuidRewriter; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufUtil; import io.netty.buffer.Unpooled; diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/uuidrewrite/EntityPacketUuidRewriter.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/uuidrewrite/EntityPacketUuidRewriter.java deleted file mode 100644 index 7122884558..0000000000 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/uuidrewrite/EntityPacketUuidRewriter.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright (C) 2024 Velocity Contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.velocitypowered.proxy.protocol.packet.uuidrewrite; - -import com.velocitypowered.api.proxy.Player; -import com.velocitypowered.proxy.VelocityServer; -import com.velocitypowered.proxy.config.PlayerInfoForwarding; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import java.util.UUID; -import java.util.function.Function; - -public class EntityPacketUuidRewriter { - - private static final boolean DEBUG = false; - private static final Logger logger = LogManager.getLogger(EntityPacketUuidRewriter.class); - - public static void rewriteS2C(VelocityServer server, Player connectionPlayer, PacketToRewriteEntityUuid packet) { - rewrite(server, connectionPlayer, packet, Player::getOfflineUuid, Player::getUniqueId); - } - - public static void rewriteC2S(VelocityServer server, Player connectionPlayer, PacketToRewriteEntityUuid packet) { - rewrite(server, connectionPlayer, packet, Player::getUniqueId, Player::getOfflineUuid); - } - - private static void rewrite(VelocityServer server, Player connectionPlayer, PacketToRewriteEntityUuid packet, - Function uuidFrom, Function uuidTo) { - if (DEBUG) { - logger.info("EPUR for {} start, packet {} ({} {})", connectionPlayer.getUsername(), packet.getClass().getSimpleName(), packet.isPlayer(), packet.getEntityUuid()); - } - - var config = server.getConfiguration(); - if (!(config.isOnlineMode() && config.getPlayerInfoForwardingMode() == PlayerInfoForwarding.NONE)) { - return; // early return for performance optimization - } - if (!packet.isPlayer()) { - return; - } - UUID uuid = packet.getEntityUuid(); - if (uuid == null) { - return; - } - - if (DEBUG) { - logger.info("EPUR for {} check pass, uuid to rewrite: {}", connectionPlayer.getUsername(), uuid); - } - - // FIXME: inefficient implementation using for loop. Replace it with map lookup? - for (Player player : server.getAllPlayers()) { - UUID serverUuid = uuidFrom.apply(player); - if (DEBUG) { - logger.info("EPUR for {} checking {} {}", connectionPlayer.getUsername(), player.getUsername(), serverUuid); - } - if (serverUuid.equals(uuid)) { - if (DEBUG) { - logger.info("EPUR for {} match {}", connectionPlayer.getUsername(), player.getUsername()); - } - - UUID newUuid = uuidTo.apply(player); - if (!newUuid.equals(uuid)) { - packet.setEntityUuid(newUuid); - } - break; - } - } - - if (DEBUG) { - logger.info("EPUR for {} check end", connectionPlayer.getUsername()); - } - } -} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/uuidrewrite/EntityPacketUuidRewriter.java b/proxy/src/main/java/com/velocitypowered/proxy/uuidrewrite/EntityPacketUuidRewriter.java new file mode 100644 index 0000000000..f522858d40 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/uuidrewrite/EntityPacketUuidRewriter.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2024 Velocity Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.velocitypowered.proxy.uuidrewrite; + +import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.proxy.VelocityServer; +import com.velocitypowered.proxy.protocol.packet.uuidrewrite.PacketToRewriteEntityUuid; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/** + * [fallen's fork] player uuid rewrite - entity packets: rewrite logic. + */ +public class EntityPacketUuidRewriter { + + private static final boolean DEBUG = false; + private static final Logger logger = LogManager.getLogger(EntityPacketUuidRewriter.class); + + public static void rewriteS2C(VelocityServer server, Player connectionPlayer, PacketToRewriteEntityUuid packet) { + rewrite(server, connectionPlayer, packet, RewriteDirection.S2C); + } + + public static void rewriteC2S(VelocityServer server, Player connectionPlayer, PacketToRewriteEntityUuid packet) { + rewrite(server, connectionPlayer, packet, RewriteDirection.C2S); + } + + private static void rewrite(VelocityServer server, Player connectionPlayer, PacketToRewriteEntityUuid packet, + RewriteDirection direction) { + if (DEBUG) { + logger.info("EPUR for {} start, packet {} ({} {})", connectionPlayer.getUsername(), packet.getClass().getSimpleName(), + packet.isPlayer(), packet.getEntityUuid()); + } + + if (!UuidRewriteUtils.isUuidRewriteEnabled(server.getConfiguration())) { + return; + } + if (!packet.isPlayer()) { + return; + } + + var oldUuid = packet.getEntityUuid(); + if (oldUuid == null) { + return; + } + + var rewriter = UuidRewriter.create(server); + var newUuid = rewriter.rewrite(oldUuid, direction); + if (newUuid != null && !newUuid.equals(oldUuid)) { + packet.setEntityUuid(newUuid); + } + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/uuidrewrite/RewriteDirection.java b/proxy/src/main/java/com/velocitypowered/proxy/uuidrewrite/RewriteDirection.java new file mode 100644 index 0000000000..e80547f68e --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/uuidrewrite/RewriteDirection.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2024 Velocity Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.velocitypowered.proxy.uuidrewrite; + +import com.velocitypowered.api.proxy.Player; +import java.util.UUID; +import java.util.function.Function; + +/** + * [fallen's fork] player uuid rewrite - implementation. + */ +public enum RewriteDirection { + OFFLINE_TO_ONLINE(Player::getOfflineUuid), + ONLINE_TO_OFFLINE(Player::getUniqueId); + + public static final RewriteDirection S2C = OFFLINE_TO_ONLINE; + public static final RewriteDirection C2S = ONLINE_TO_OFFLINE; + + private final Function uuidExtractor; + + RewriteDirection(Function uuidExtractor) { + this.uuidExtractor = uuidExtractor; + } + + UUID getSourceUuid(Player player) { + return this.uuidExtractor.apply(player); + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/uuidrewrite/TabListUuidRewriter.java b/proxy/src/main/java/com/velocitypowered/proxy/uuidrewrite/TabListUuidRewriter.java similarity index 76% rename from proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/uuidrewrite/TabListUuidRewriter.java rename to proxy/src/main/java/com/velocitypowered/proxy/uuidrewrite/TabListUuidRewriter.java index 97e3a21de6..24b0b9441f 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/uuidrewrite/TabListUuidRewriter.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/uuidrewrite/TabListUuidRewriter.java @@ -15,12 +15,11 @@ * along with this program. If not, see . */ -package com.velocitypowered.proxy.protocol.packet.uuidrewrite; +package com.velocitypowered.proxy.uuidrewrite; import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.api.proxy.Player; import com.velocitypowered.proxy.VelocityServer; -import com.velocitypowered.proxy.config.PlayerInfoForwarding; import com.velocitypowered.proxy.connection.backend.VelocityServerConnection; import com.velocitypowered.proxy.connection.client.ConnectedPlayer; import com.velocitypowered.proxy.protocol.MinecraftPacket; @@ -28,10 +27,7 @@ import com.velocitypowered.proxy.protocol.packet.RemovePlayerInfoPacket; import com.velocitypowered.proxy.protocol.packet.UpsertPlayerInfoPacket; import java.util.Collections; -import java.util.HashMap; -import java.util.Map; import java.util.Optional; -import java.util.UUID; import java.util.stream.Collectors; /** @@ -41,26 +37,23 @@ public class TabListUuidRewriter { @SuppressWarnings("BooleanMethodIsAlwaysInverted") private static boolean shouldRewrite(VelocityServer server) { - var config = server.getConfiguration(); - return config.isOnlineMode() && config.getPlayerInfoForwardingMode() == PlayerInfoForwarding.NONE; + return UuidRewriteUtils.isUuidRewriteEnabled(server.getConfiguration()); } - // offline / server uuid -> online / client uuid - private static Map makeUuidMappingView(VelocityServer server) { - Map view = new HashMap<>(); - for (Player player : server.getAllPlayers()) { - view.put(player.getOfflineUuid(), player.getUniqueId()); - } - return view; - } - - // [fallen's fork] player uuid rewrite - // send the missing player tab-list removal packets to other players in the mc server - // see bungeecord net.md_5.bungee.connection.UpstreamBridge#disconnected - public static void onPlayerDisconnect(VelocityServer server, ConnectedPlayer player) { + /** + * [fallen's fork] player uuid rewrite + * send the missing player tab-list removal packets to other players in the mc server + * see bungeecord net.md_5.bungee.connection.UpstreamBridge#disconnected + */ + public static void sendRewrittenTabListRemovalPackets(VelocityServer server, ConnectedPlayer player) { if (!shouldRewrite(server)) { return; } + if (server.getConfiguration().isUuidRewriteDatabaseEnabled()) { + // if the database is enabled, then no need for this bungee hack + // cuz the mapping is always available + return; + } VelocityServerConnection connectedServer = player.getConnectedServer(); if (connectedServer == null) { @@ -77,7 +70,7 @@ public static void onPlayerDisconnect(VelocityServer server, ConnectedPlayer pla for (Player otherPlayer : connectedServer.getServer().getPlayersConnected()) { if (otherPlayer != player && otherPlayer instanceof ConnectedPlayer) { - var connection = ((ConnectedPlayer)otherPlayer).getConnection(); + var connection = ((ConnectedPlayer) otherPlayer).getConnection(); MinecraftPacket packet; if (connection.getProtocolVersion().noLessThan(ProtocolVersion.MINECRAFT_1_19_3)) { packet = newPacket; @@ -97,9 +90,9 @@ public static void rewrite(VelocityServer server, LegacyPlayerListItemPacket pac return; } - var uuidMapping = makeUuidMappingView(server); + var rewriter = UuidRewriter.create(server); packet.getItems().replaceAll(item -> { - var clientSideUuid = uuidMapping.get(item.getUuid()); + var clientSideUuid = rewriter.toClient(item.getUuid()); if (clientSideUuid != null && !clientSideUuid.equals(item.getUuid())) { var newItem = new LegacyPlayerListItemPacket.Item(clientSideUuid); @@ -125,9 +118,9 @@ public static void rewrite(VelocityServer server, UpsertPlayerInfoPacket packet) return; } - var uuidMapping = makeUuidMappingView(server); + var rewriter = UuidRewriter.create(server); packet.getEntries().replaceAll(entry -> { - var clientSideUuid = uuidMapping.get(entry.getProfileId()); + var clientSideUuid = rewriter.toClient(entry.getProfileId()); if (clientSideUuid != null && !clientSideUuid.equals(entry.getProfileId())) { var newEntry = new UpsertPlayerInfoPacket.Entry(clientSideUuid); @@ -153,9 +146,9 @@ public static void rewrite(VelocityServer server, RemovePlayerInfoPacket packet) return; } - var uuidMapping = makeUuidMappingView(server); + var rewriter = UuidRewriter.create(server); var newProfiles = packet.getProfilesToRemove().stream() - .map(serverUuid -> Optional.ofNullable(uuidMapping.get(serverUuid)).orElse(serverUuid)) + .map(serverUuid -> Optional.ofNullable(rewriter.toClient(serverUuid)).orElse(serverUuid)) .collect(Collectors.toList()); packet.setProfilesToRemove(newProfiles); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/uuidrewrite/UuidMappingDatabase.java b/proxy/src/main/java/com/velocitypowered/proxy/uuidrewrite/UuidMappingDatabase.java new file mode 100644 index 0000000000..1d8f90b132 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/uuidrewrite/UuidMappingDatabase.java @@ -0,0 +1,203 @@ +/* + * Copyright (C) 2024 Velocity Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.velocitypowered.proxy.uuidrewrite; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Objects; +import java.util.UUID; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.sqlite.SQLiteConfig; +import org.sqlite.SQLiteDataSource; + +/** + * [fallen's fork] player uuid rewrite - uuid database. + */ +@SuppressWarnings({"MissingJavadocMethod", "MissingJavadocType"}) +public class UuidMappingDatabase { + + private static final Logger logger = LogManager.getLogger(UuidMappingDatabase.class); + private static final UuidMappingDatabase INSTANCE = new UuidMappingDatabase(); + private final SQLiteDataSource dataSource; + private Connection connection; + private boolean enabled = false; + + private UuidMappingDatabase() { + SQLiteConfig config = new SQLiteConfig(); + config.enforceForeignKeys(true); + config.setBusyTimeout(1000); + + this.dataSource = new SQLiteDataSource(config); + } + + public static UuidMappingDatabase getInstance() { + return INSTANCE; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + private Connection getConnection() throws SQLException { + if (this.connection == null || this.connection.isClosed()) { + this.connection = this.dataSource.getConnection(); + this.connection.setAutoCommit(false); + } + return this.connection; + } + + public void init(String dbPath) throws SQLException { + String url = "jdbc:sqlite:" + dbPath; + this.dataSource.setUrl(url); + + try (var stmt = this.getConnection().createStatement()) { + stmt.executeUpdate( + "CREATE TABLE IF NOT EXISTS uuid_mapping (" + + "online_uuid TEXT PRIMARY KEY, " + + "offline_uuid TEXT, " + + "player_name TEXT, " + + "updated_at INTEGER)" + ); + stmt.executeUpdate("CREATE INDEX IF NOT EXISTS idx_online_uuid ON uuid_mapping (online_uuid)"); + stmt.executeUpdate("CREATE INDEX IF NOT EXISTS idx_offline_uuid ON uuid_mapping (offline_uuid)"); + stmt.executeUpdate("CREATE INDEX IF NOT EXISTS idx_last_used ON uuid_mapping (updated_at)"); + stmt.getConnection().commit(); + } + } + + public void close() { + try { + if (this.connection != null && !this.connection.isClosed()) { + this.connection.close(); + } + } catch (SQLException sqlException) { + logger.error("close failed", sqlException); + } + } + + @Nullable + public UUID queryOnlineUuid(UUID offlineUuid) { + if (!this.enabled) { + return null; + } + + String query = "SELECT online_uuid FROM uuid_mapping WHERE offline_uuid = ? ORDER BY updated_at DESC LIMIT 1"; + try (var stmt = this.getConnection().prepareStatement(query)) { + stmt.setString(1, offlineUuid.toString()); + ResultSet resultSet = stmt.executeQuery(); + if (resultSet.next()) { + return UUID.fromString(resultSet.getString("online_uuid")); + } + } catch (SQLException sqlException) { + logger.error("queryOnlineUuid failed", sqlException); + } + return null; + } + + @Nullable + public UUID queryOfflineUuid(UUID onlineUuid) { + if (!this.enabled) { + return null; + } + + String sql = "SELECT offline_uuid FROM uuid_mapping WHERE online_uuid = ? ORDER BY updated_at DESC LIMIT 1"; + try (var stmt = this.getConnection().prepareStatement(sql)) { + stmt.setString(1, onlineUuid.toString()); + ResultSet resultSet = stmt.executeQuery(); + if (resultSet.next()) { + return UUID.fromString(resultSet.getString("offline_uuid")); + } + } catch (SQLException sqlException) { + logger.error("queryOfflineUuid failed", sqlException); + } + return null; + } + + private long lastVacuumMilli = 0; + private final Object vacuumLock = new Object(); + + public void createNewEntry(UUID onlineUuid, UUID offlineUuid, String playerName) { + if (!this.enabled) { + return; + } + + long now = System.currentTimeMillis(); + String sqlQuery = "SELECT * FROM uuid_mapping WHERE online_uuid = ?"; + try (var stmt = this.getConnection().prepareStatement(sqlQuery)) { + stmt.setString(1, onlineUuid.toString()); + ResultSet resultSet = stmt.executeQuery(); + if ( + resultSet.next() + && Objects.equals(resultSet.getString("player_name"), playerName) + && Objects.equals(resultSet.getString("offline_uuid"), offlineUuid.toString()) + && Objects.equals(resultSet.getString("online_uuid"), onlineUuid.toString()) + ) { + // no changes to this player + if (now / 1000 - resultSet.getBigDecimal("updated_at").longValue() < 60 * 60) { // 1h cooldown + // has been updated recently + // skip the update + return; + } + } + } catch (SQLException sqlException) { + logger.error("createNewEntry existence check failed", sqlException); + } + + synchronized (this.vacuumLock) { + if (now - this.lastVacuumMilli >= 24 * 60 * 60) { // 1day + this.vacuumSqlite(); + this.lastVacuumMilli = now; + } + } + + logger.debug("Create or update uuid mapping entry {} {} {}", onlineUuid, offlineUuid, playerName); + try { + String sqlDelete = "DELETE FROM uuid_mapping WHERE offline_uuid = ?"; + String sqlInsert = + "INSERT OR REPLACE INTO uuid_mapping (online_uuid, offline_uuid, player_name, updated_at) " + + "VALUES (?, ?, ?, strftime('%s','now'))"; + + var conn = this.getConnection(); + try (var stmt = conn.prepareStatement(sqlDelete)) { + stmt.setString(1, offlineUuid.toString()); + int cnt = stmt.executeUpdate(); + logger.debug("Deleted {} existed entries with offline_uuid = {}", cnt, offlineUuid); + } + try (var stmt = conn.prepareStatement(sqlInsert)) { + stmt.setString(1, onlineUuid.toString()); + stmt.setString(2, offlineUuid.toString()); + stmt.setString(3, playerName); + stmt.executeUpdate(); + } + conn.commit(); + } catch (SQLException sqlException) { + logger.error("createRow update failed", sqlException); + } + } + + private void vacuumSqlite() { + try (var conn = this.dataSource.getConnection(); var stmt = conn.prepareStatement("VACUUM")) { + stmt.executeUpdate(); + } catch (SQLException sqlException) { + logger.error("vacuumSqlite failed", sqlException); + } + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/uuidrewrite/UuidRewriteHooks.java b/proxy/src/main/java/com/velocitypowered/proxy/uuidrewrite/UuidRewriteHooks.java new file mode 100644 index 0000000000..923bfe9dd8 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/uuidrewrite/UuidRewriteHooks.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2024 Velocity Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.velocitypowered.proxy.uuidrewrite; + +import com.velocitypowered.proxy.VelocityServer; +import com.velocitypowered.proxy.connection.client.ConnectedPlayer; +import java.sql.SQLException; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/** + * [fallen's fork] player uuid rewrite - lifecycle hooks. + */ +@SuppressWarnings("MissingJavadocMethod") +public class UuidRewriteHooks { + + private static final Logger logger = LogManager.getLogger(UuidRewriteHooks.class); + private static final UuidMappingDatabase db = UuidMappingDatabase.getInstance(); + + public static void onServerStart(VelocityServer server) { + var config = server.getConfiguration(); + if (config.isUuidRewriteDatabaseEnabled()) { + var dbPath = config.getUuidRewriteDatabasePath(); + try { + db.init(dbPath); + logger.info("UUID-Rewrite mapping database connect ok, path '{}'", dbPath); + } catch (SQLException e) { + logger.error("UUID-Rewrite mapping database initialization failed, disabling database, path '{}'", dbPath, e); + config.setUuidRewriteDatabaseEnabled(false); + } + } + db.setEnabled(config.isUuidRewriteDatabaseEnabled()); + } + + public static void onServerStop(VelocityServer server) { + var config = server.getConfiguration(); + if (config.isUuidRewriteDatabaseEnabled()) { + db.close(); + } + } + + public static void onPlayerConnect(VelocityServer server, ConnectedPlayer player) { + var config = server.getConfiguration(); + if (UuidRewriteUtils.isUuidRewriteEnabled(config)) { + if (config.isUuidRewriteDatabaseEnabled()) { + db.createNewEntry(player.getUniqueId(), player.getOfflineUuid(), player.getUsername()); + } + } + } + + public static void onPlayerDisconnect(VelocityServer server, ConnectedPlayer player) { + TabListUuidRewriter.sendRewrittenTabListRemovalPackets(server, player); + } + +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/uuidrewrite/UuidRewriteUtils.java b/proxy/src/main/java/com/velocitypowered/proxy/uuidrewrite/UuidRewriteUtils.java new file mode 100644 index 0000000000..51236e3b56 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/uuidrewrite/UuidRewriteUtils.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2024 Velocity Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.velocitypowered.proxy.uuidrewrite; + +import com.velocitypowered.proxy.config.PlayerInfoForwarding; +import com.velocitypowered.proxy.config.VelocityConfiguration; + +/** + * [fallen's fork] player uuid rewrite - implementation. + */ +@SuppressWarnings("MissingJavadocMethod") +public class UuidRewriteUtils { + + public static boolean isUuidRewriteEnabled(VelocityConfiguration config) { + if (config.isUuidRewriteEnabled()) { + return config.isOnlineMode() && config.getPlayerInfoForwardingMode() == PlayerInfoForwarding.NONE; + } + return false; + } + +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/uuidrewrite/UuidRewriter.java b/proxy/src/main/java/com/velocitypowered/proxy/uuidrewrite/UuidRewriter.java new file mode 100644 index 0000000000..6ba15d9772 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/uuidrewrite/UuidRewriter.java @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2024 Velocity Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.velocitypowered.proxy.uuidrewrite; + +import com.google.common.collect.BiMap; +import com.google.common.collect.HashBiMap; +import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.proxy.VelocityServer; +import java.util.UUID; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * offline / server UUID <-> online / client UUID. + */ +@SuppressWarnings({"MissingJavadocMethod", "MissingJavadocType"}) +public interface UuidRewriter { + + // -------------------- Interfaces -------------------- + + @Nullable UUID toOnline(UUID offlineUuid); + + @Nullable UUID toOffline(UUID onlineUuid); + + default @Nullable UUID toClient(UUID serverUuid) { + return this.toOnline(serverUuid); + } + + default @Nullable UUID toServer(UUID clientUuid) { + return this.toOffline(clientUuid); + } + + default @Nullable UUID rewrite(UUID uuid, RewriteDirection direction) { + return switch (direction) { + case ONLINE_TO_OFFLINE -> this.toOffline(uuid); + case OFFLINE_TO_ONLINE -> this.toOnline(uuid); + }; + } + + default @Nullable UUID rewrite(Player player, RewriteDirection direction) { + return this.rewrite(direction.getSourceUuid(player), direction); + } + + // -------------------- Utilities -------------------- + + static UuidRewriter create(VelocityServer server) { + return new ChainedRewriter(new MapRewriter(server), new DatabaseRewriter()); + } + + // -------------------- Implementations -------------------- + + class MapRewriter implements UuidRewriter { + private final BiMap offlineToOnline = HashBiMap.create(); + + private MapRewriter(VelocityServer server) { + for (Player player : server.getAllPlayers()) { + this.offlineToOnline.put(player.getOfflineUuid(), player.getUniqueId()); + } + } + + @Override + public @Nullable UUID toOnline(UUID offlineUuid) { + return this.offlineToOnline.get(offlineUuid); + } + + @Override + public @Nullable UUID toOffline(UUID onlineUuid) { + return this.offlineToOnline.inverse().get(onlineUuid); + } + } + + class DatabaseRewriter implements UuidRewriter { + private DatabaseRewriter() {} + + @Override + public @Nullable UUID toOnline(UUID offlineUuid) { + var db = UuidMappingDatabase.getInstance(); + return db.queryOnlineUuid(offlineUuid); + } + + @Override + public @Nullable UUID toOffline(UUID onlineUuid) { + var db = UuidMappingDatabase.getInstance(); + return db.queryOfflineUuid(onlineUuid); + } + } + + class ChainedRewriter implements UuidRewriter { + private final UuidRewriter[] rewriters; + + private ChainedRewriter(UuidRewriter... rewriters) { + this.rewriters = rewriters; + } + + @Override + public @Nullable UUID toOnline(UUID offlineUuid) { + for (UuidRewriter rewriter : this.rewriters) { + var result = rewriter.toOnline(offlineUuid); + if (result != null) { + return result; + } + } + return null; + } + + @Override + public @Nullable UUID toOffline(UUID onlineUuid) { + for (UuidRewriter rewriter : this.rewriters) { + var result = rewriter.toOffline(onlineUuid); + if (result != null) { + return result; + } + } + return null; + } + } +} diff --git a/proxy/src/main/resources/default-velocity.toml b/proxy/src/main/resources/default-velocity.toml index 7dd1868ba5..79a96ce620 100644 --- a/proxy/src/main/resources/default-velocity.toml +++ b/proxy/src/main/resources/default-velocity.toml @@ -153,6 +153,13 @@ type = "http" hostname = "127.0.0.1" port = 1081 +# [fallen's fork] player uuid rewrite +# See readme for more information +[uuid-rewrite] +enabled = true +databaseEnabled = false +databasePath = "uuid_mapping.db" + [query] # Whether to enable responding to GameSpy 4 query responses or not. enabled = false