From 662fbc4e3cc1ebbd817af6bb01a12cc46f479c2a Mon Sep 17 00:00:00 2001 From: Andrew Steinborn Date: Thu, 23 Jun 2022 23:59:13 -0400 Subject: [PATCH] Implement the ServerData packet by firing ProxyPingEvent (#771) * Implement the ServerData packet by firing ProxyPingEvent Mojang introduced the enable-status server property with Minecraft 1.19, which if enabled causes servers to close the connection when a client tries to ping them. Mojang wants to show the MOTD and favicon on the server select screen for those who manage to log in, so we need to implement this packet as well. The good news is that we can send this packet as many times as needed on the same connection This matches the behavior of pinging the server. This is a minor, but completely backwards-compatible, API breakage: Player inherits from InboundConnection so we do not have to change ProxyPingEvent, however plugins not expecting a Player might get confused. * typo --- .../api/event/proxy/ProxyPingEvent.java | 7 +- .../velocitypowered/proxy/VelocityServer.java | 7 + .../connection/MinecraftSessionHandler.java | 5 + .../backend/BackendPlaySessionHandler.java | 18 +++ .../connection/client/ConnectedPlayer.java | 4 +- .../client/HandshakeSessionHandler.java | 12 +- .../client/InitialInboundConnection.java | 4 +- .../client/StatusSessionHandler.java | 131 +-------------- .../util/ServerListPingHandler.java | 153 ++++++++++++++++++ .../util/VelocityInboundConnection.java | 25 +++ .../proxy/protocol/StateRegistry.java | 3 + .../proxy/protocol/packet/ServerData.java | 94 +++++++++++ 12 files changed, 330 insertions(+), 133 deletions(-) create mode 100644 proxy/src/main/java/com/velocitypowered/proxy/connection/util/ServerListPingHandler.java create mode 100644 proxy/src/main/java/com/velocitypowered/proxy/connection/util/VelocityInboundConnection.java create mode 100644 proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ServerData.java diff --git a/api/src/main/java/com/velocitypowered/api/event/proxy/ProxyPingEvent.java b/api/src/main/java/com/velocitypowered/api/event/proxy/ProxyPingEvent.java index b7e040eef..d8b89b2be 100644 --- a/api/src/main/java/com/velocitypowered/api/event/proxy/ProxyPingEvent.java +++ b/api/src/main/java/com/velocitypowered/api/event/proxy/ProxyPingEvent.java @@ -13,10 +13,11 @@ import com.velocitypowered.api.proxy.InboundConnection; import com.velocitypowered.api.proxy.server.ServerPing; /** - * This event is fired when a server list ping request is sent by a remote client. Velocity will + * This event is fired when a request for server information is sent by a remote client, or when the + * server sends the MOTD and favicon to the client after a successful login. Velocity will * wait on this event to finish firing before delivering the results to the remote client, but - * you are urged to be as parsimonious as possible when handling this event due to the amount of - * ping packets a client can send. + * you are urged to handle this event as quickly as possible when handling this event due to the + * amount of ping packets a client can send. */ @AwaitingEvent public final class ProxyPingEvent { diff --git a/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java b/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java index c3272c85a..2d36cf8ef 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java @@ -44,6 +44,7 @@ import com.velocitypowered.proxy.command.builtin.VelocityCommand; import com.velocitypowered.proxy.config.VelocityConfiguration; import com.velocitypowered.proxy.connection.client.ConnectedPlayer; import com.velocitypowered.proxy.connection.player.VelocityResourcePackInfo; +import com.velocitypowered.proxy.connection.util.ServerListPingHandler; import com.velocitypowered.proxy.console.VelocityConsole; import com.velocitypowered.proxy.crypto.EncryptionUtils; import com.velocitypowered.proxy.event.VelocityEventManager; @@ -142,6 +143,7 @@ public class VelocityServer implements ProxyServer, ForwardingAudience { private final VelocityEventManager eventManager; private final VelocityScheduler scheduler; private final VelocityChannelRegistrar channelRegistrar = new VelocityChannelRegistrar(); + private ServerListPingHandler serverListPingHandler; VelocityServer(final ProxyOptions options) { pluginManager = new VelocityPluginManager(this); @@ -151,6 +153,7 @@ public class VelocityServer implements ProxyServer, ForwardingAudience { console = new VelocityConsole(this); cm = new ConnectionManager(this); servers = new ServerMap(this); + serverListPingHandler = new ServerListPingHandler(this); this.options = options; this.bossBarManager = new AdventureBossBarManager(); } @@ -370,6 +373,10 @@ public class VelocityServer implements ProxyServer, ForwardingAudience { return this.cm.backendChannelInitializer.get(); } + public ServerListPingHandler getServerListPingHandler() { + return serverListPingHandler; + } + public boolean isShutdown() { return shutdown; } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftSessionHandler.java index bdd3b5a0b..63d3c36a7 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftSessionHandler.java @@ -37,6 +37,7 @@ import com.velocitypowered.proxy.protocol.packet.PluginMessage; import com.velocitypowered.proxy.protocol.packet.ResourcePackRequest; import com.velocitypowered.proxy.protocol.packet.ResourcePackResponse; import com.velocitypowered.proxy.protocol.packet.Respawn; +import com.velocitypowered.proxy.protocol.packet.ServerData; import com.velocitypowered.proxy.protocol.packet.ServerLogin; import com.velocitypowered.proxy.protocol.packet.ServerLoginSuccess; import com.velocitypowered.proxy.protocol.packet.SetCompression; @@ -261,4 +262,8 @@ public interface MinecraftSessionHandler { default boolean handle(PlayerCommand packet) { return false; } + + default boolean handle(ServerData serverData) { + return false; + } } 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 c485be158..7d9e9f1c9 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 @@ -27,6 +27,7 @@ import com.velocitypowered.api.event.command.PlayerAvailableCommandsEvent; import com.velocitypowered.api.event.connection.PluginMessageEvent; import com.velocitypowered.api.event.player.PlayerResourcePackStatusEvent; import com.velocitypowered.api.event.player.ServerResourcePackSendEvent; +import com.velocitypowered.api.event.proxy.ProxyPingEvent; import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.api.proxy.messages.ChannelIdentifier; import com.velocitypowered.api.proxy.player.ResourcePackInfo; @@ -46,6 +47,7 @@ import com.velocitypowered.proxy.protocol.packet.PlayerListItem; import com.velocitypowered.proxy.protocol.packet.PluginMessage; import com.velocitypowered.proxy.protocol.packet.ResourcePackRequest; import com.velocitypowered.proxy.protocol.packet.ResourcePackResponse; +import com.velocitypowered.proxy.protocol.packet.ServerData; import com.velocitypowered.proxy.protocol.packet.TabCompleteResponse; import com.velocitypowered.proxy.protocol.util.PluginMessageUtil; import io.netty.buffer.ByteBuf; @@ -268,6 +270,22 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler { return true; } + @Override + public boolean handle(ServerData packet) { + server.getServerListPingHandler().getInitialPing(this.serverConn.getPlayer()) + .thenComposeAsync( + ping -> server.getEventManager().fire(new ProxyPingEvent(this.serverConn.getPlayer(), ping)), + playerConnection.eventLoop() + ) + .thenAcceptAsync(pingEvent -> + this.playerConnection.write( + new ServerData(pingEvent.getPing().getDescriptionComponent(), + pingEvent.getPing().getFavicon().orElse(null), + packet.isPreviewsChat()) + ), playerConnection.eventLoop()); + return true; + } + @Override public void handleGeneric(MinecraftPacket packet) { if (packet instanceof PluginMessage) { diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java index 33cae2f9a..8c63673bd 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java @@ -56,6 +56,7 @@ import com.velocitypowered.proxy.connection.backend.VelocityServerConnection; import com.velocitypowered.proxy.connection.player.VelocityResourcePackInfo; import com.velocitypowered.proxy.connection.util.ConnectionMessages; import com.velocitypowered.proxy.connection.util.ConnectionRequestResults.Impl; +import com.velocitypowered.proxy.connection.util.VelocityInboundConnection; import com.velocitypowered.proxy.protocol.ProtocolUtils; import com.velocitypowered.proxy.protocol.StateRegistry; import com.velocitypowered.proxy.protocol.packet.ClientSettings; @@ -112,7 +113,8 @@ import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.jetbrains.annotations.NotNull; -public class ConnectedPlayer implements MinecraftConnectionAssociation, Player, KeyIdentifiable { +public class ConnectedPlayer implements MinecraftConnectionAssociation, Player, KeyIdentifiable, + VelocityInboundConnection { private static final int MAX_PLUGIN_CHANNELS = 1024; private static final PlainTextComponentSerializer PASS_THRU_TRANSLATE = PlainTextComponentSerializer.builder() diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/HandshakeSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/HandshakeSessionHandler.java index 2870b64bc..0d578a4f1 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/HandshakeSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/HandshakeSessionHandler.java @@ -29,6 +29,7 @@ import com.velocitypowered.proxy.connection.ConnectionTypes; import com.velocitypowered.proxy.connection.MinecraftConnection; import com.velocitypowered.proxy.connection.MinecraftSessionHandler; import com.velocitypowered.proxy.connection.forge.legacy.LegacyForgeConstants; +import com.velocitypowered.proxy.connection.util.VelocityInboundConnection; import com.velocitypowered.proxy.protocol.MinecraftPacket; import com.velocitypowered.proxy.protocol.StateRegistry; import com.velocitypowered.proxy.protocol.packet.Handshake; @@ -60,7 +61,7 @@ public class HandshakeSessionHandler implements MinecraftSessionHandler { @Override public boolean handle(LegacyPing packet) { connection.setProtocolVersion(ProtocolVersion.LEGACY); - StatusSessionHandler handler = new StatusSessionHandler(server, connection, + StatusSessionHandler handler = new StatusSessionHandler(server, new LegacyInboundConnection(connection, packet)); connection.setSessionHandler(handler); handler.handle(packet); @@ -91,7 +92,7 @@ public class HandshakeSessionHandler implements MinecraftSessionHandler { switch (nextState) { case STATUS: - connection.setSessionHandler(new StatusSessionHandler(server, connection, ic)); + connection.setSessionHandler(new StatusSessionHandler(server, ic)); break; case LOGIN: this.handleLogin(handshake, ic); @@ -197,7 +198,7 @@ public class HandshakeSessionHandler implements MinecraftSessionHandler { connection.close(true); } - private static class LegacyInboundConnection implements InboundConnection { + private static class LegacyInboundConnection implements VelocityInboundConnection { private final MinecraftConnection connection; private final LegacyPing ping; @@ -232,5 +233,10 @@ public class HandshakeSessionHandler implements MinecraftSessionHandler { public String toString() { return "[legacy connection] " + this.getRemoteAddress().toString(); } + + @Override + public MinecraftConnection getConnection() { + return connection; + } } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/InitialInboundConnection.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/InitialInboundConnection.java index d243e339e..d350c4841 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/InitialInboundConnection.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/InitialInboundConnection.java @@ -21,6 +21,7 @@ import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.api.proxy.InboundConnection; import com.velocitypowered.proxy.connection.MinecraftConnection; import com.velocitypowered.proxy.connection.MinecraftConnectionAssociation; +import com.velocitypowered.proxy.connection.util.VelocityInboundConnection; import com.velocitypowered.proxy.protocol.packet.Disconnect; import com.velocitypowered.proxy.protocol.packet.Handshake; import com.velocitypowered.proxy.util.ClosestLocaleMatcher; @@ -33,7 +34,7 @@ import net.kyori.adventure.translation.GlobalTranslator; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -public final class InitialInboundConnection implements InboundConnection, +public final class InitialInboundConnection implements VelocityInboundConnection, MinecraftConnectionAssociation { private static final Logger logger = LogManager.getLogger(InitialInboundConnection.class); @@ -74,6 +75,7 @@ public final class InitialInboundConnection implements InboundConnection, return "[initial connection] " + connection.getRemoteAddress().toString(); } + @Override public MinecraftConnection getConnection() { return connection; } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/StatusSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/StatusSessionHandler.java index 792c5b458..328f9492d 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/StatusSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/StatusSessionHandler.java @@ -17,33 +17,18 @@ package com.velocitypowered.proxy.connection.client; -import com.google.common.collect.ImmutableList; -import com.spotify.futures.CompletableFutures; import com.velocitypowered.api.event.proxy.ProxyPingEvent; -import com.velocitypowered.api.network.ProtocolVersion; -import com.velocitypowered.api.proxy.InboundConnection; -import com.velocitypowered.api.proxy.server.RegisteredServer; -import com.velocitypowered.api.proxy.server.ServerPing; -import com.velocitypowered.api.util.ModInfo; import com.velocitypowered.proxy.VelocityServer; -import com.velocitypowered.proxy.config.PingPassthroughMode; -import com.velocitypowered.proxy.config.VelocityConfiguration; import com.velocitypowered.proxy.connection.MinecraftConnection; import com.velocitypowered.proxy.connection.MinecraftSessionHandler; +import com.velocitypowered.proxy.connection.util.VelocityInboundConnection; import com.velocitypowered.proxy.protocol.packet.LegacyDisconnect; import com.velocitypowered.proxy.protocol.packet.LegacyPing; import com.velocitypowered.proxy.protocol.packet.StatusPing; import com.velocitypowered.proxy.protocol.packet.StatusRequest; import com.velocitypowered.proxy.protocol.packet.StatusResponse; -import com.velocitypowered.proxy.server.VelocityRegisteredServer; import com.velocitypowered.proxy.util.except.QuietRuntimeException; import io.netty.buffer.ByteBuf; -import java.net.InetSocketAddress; -import java.util.ArrayList; -import java.util.List; -import java.util.Locale; -import java.util.Optional; -import java.util.concurrent.CompletableFuture; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -55,13 +40,12 @@ public class StatusSessionHandler implements MinecraftSessionHandler { private final VelocityServer server; private final MinecraftConnection connection; - private final InboundConnection inbound; + private final VelocityInboundConnection inbound; private boolean pingReceived = false; - StatusSessionHandler(VelocityServer server, MinecraftConnection connection, - InboundConnection inbound) { + StatusSessionHandler(VelocityServer server, VelocityInboundConnection inbound) { this.server = server; - this.connection = connection; + this.connection = inbound.getConnection(); this.inbound = inbound; } @@ -73,116 +57,13 @@ public class StatusSessionHandler implements MinecraftSessionHandler { } } - private ServerPing constructLocalPing(ProtocolVersion version) { - VelocityConfiguration configuration = server.getConfiguration(); - return new ServerPing( - new ServerPing.Version(version.getProtocol(), - "Velocity " + ProtocolVersion.SUPPORTED_VERSION_STRING), - new ServerPing.Players(server.getPlayerCount(), configuration.getShowMaxPlayers(), - ImmutableList.of()), - configuration.getMotd(), - configuration.getFavicon().orElse(null), - configuration.isAnnounceForge() ? ModInfo.DEFAULT : null - ); - } - - private CompletableFuture attemptPingPassthrough(PingPassthroughMode mode, - List servers, ProtocolVersion pingingVersion) { - ServerPing fallback = constructLocalPing(pingingVersion); - List> pings = new ArrayList<>(); - for (String s : servers) { - Optional rs = server.getServer(s); - if (!rs.isPresent()) { - continue; - } - VelocityRegisteredServer vrs = (VelocityRegisteredServer) rs.get(); - pings.add(vrs.ping(connection.eventLoop(), pingingVersion)); - } - if (pings.isEmpty()) { - return CompletableFuture.completedFuture(fallback); - } - - CompletableFuture> pingResponses = CompletableFutures.successfulAsList(pings, - (ex) -> fallback); - switch (mode) { - case ALL: - return pingResponses.thenApply(responses -> { - // Find the first non-fallback - for (ServerPing response : responses) { - if (response == fallback) { - continue; - } - return response; - } - return fallback; - }); - case MODS: - return pingResponses.thenApply(responses -> { - // Find the first non-fallback that contains a mod list - for (ServerPing response : responses) { - if (response == fallback) { - continue; - } - Optional modInfo = response.getModinfo(); - if (modInfo.isPresent()) { - return fallback.asBuilder().mods(modInfo.get()).build(); - } - } - return fallback; - }); - case DESCRIPTION: - return pingResponses.thenApply(responses -> { - // Find the first non-fallback. If it includes a modlist, add it too. - for (ServerPing response : responses) { - if (response == fallback) { - continue; - } - - if (response.getDescriptionComponent() == null) { - continue; - } - - return new ServerPing( - fallback.getVersion(), - fallback.getPlayers().orElse(null), - response.getDescriptionComponent(), - fallback.getFavicon().orElse(null), - response.getModinfo().orElse(null) - ); - } - return fallback; - }); - default: - // Not possible, but covered for completeness. - return CompletableFuture.completedFuture(fallback); - } - } - - private CompletableFuture getInitialPing() { - VelocityConfiguration configuration = server.getConfiguration(); - ProtocolVersion shownVersion = ProtocolVersion.isSupported(connection.getProtocolVersion()) - ? connection.getProtocolVersion() : ProtocolVersion.MAXIMUM_VERSION; - PingPassthroughMode passthrough = configuration.getPingPassthrough(); - - if (passthrough == PingPassthroughMode.DISABLED) { - return CompletableFuture.completedFuture(constructLocalPing(shownVersion)); - } else { - String virtualHostStr = inbound.getVirtualHost().map(InetSocketAddress::getHostString) - .map(str -> str.toLowerCase(Locale.ROOT)) - .orElse(""); - List serversToTry = server.getConfiguration().getForcedHosts().getOrDefault( - virtualHostStr, server.getConfiguration().getAttemptConnectionOrder()); - return attemptPingPassthrough(configuration.getPingPassthrough(), serversToTry, shownVersion); - } - } - @Override public boolean handle(LegacyPing packet) { if (this.pingReceived) { throw EXPECTED_AWAITING_REQUEST; } this.pingReceived = true; - getInitialPing() + server.getServerListPingHandler().getInitialPing(this.inbound) .thenCompose(ping -> server.getEventManager().fire(new ProxyPingEvent(inbound, ping))) .thenAcceptAsync(event -> connection.closeWith( LegacyDisconnect.fromServerPing(event.getPing(), packet.getVersion())), @@ -207,7 +88,7 @@ public class StatusSessionHandler implements MinecraftSessionHandler { } this.pingReceived = true; - getInitialPing() + this.server.getServerListPingHandler().getInitialPing(inbound) .thenCompose(ping -> server.getEventManager().fire(new ProxyPingEvent(inbound, ping))) .thenAcceptAsync( (event) -> { diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/util/ServerListPingHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/util/ServerListPingHandler.java new file mode 100644 index 000000000..6bc29e5ca --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/util/ServerListPingHandler.java @@ -0,0 +1,153 @@ +/* + * Copyright (C) 2018 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.connection.util; + +import com.google.common.collect.ImmutableList; +import com.spotify.futures.CompletableFutures; +import com.velocitypowered.api.network.ProtocolVersion; +import com.velocitypowered.api.proxy.server.RegisteredServer; +import com.velocitypowered.api.proxy.server.ServerPing; +import com.velocitypowered.api.util.ModInfo; +import com.velocitypowered.proxy.VelocityServer; +import com.velocitypowered.proxy.config.PingPassthroughMode; +import com.velocitypowered.proxy.config.VelocityConfiguration; +import com.velocitypowered.proxy.server.VelocityRegisteredServer; +import java.net.InetSocketAddress; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; + +public class ServerListPingHandler { + + private final VelocityServer server; + + public ServerListPingHandler(VelocityServer server) { + this.server = server; + } + + private ServerPing constructLocalPing(ProtocolVersion version) { + VelocityConfiguration configuration = server.getConfiguration(); + return new ServerPing( + new ServerPing.Version(version.getProtocol(), + "Velocity " + ProtocolVersion.SUPPORTED_VERSION_STRING), + new ServerPing.Players(server.getPlayerCount(), configuration.getShowMaxPlayers(), + ImmutableList.of()), + configuration.getMotd(), + configuration.getFavicon().orElse(null), + configuration.isAnnounceForge() ? ModInfo.DEFAULT : null + ); + } + + private CompletableFuture attemptPingPassthrough(VelocityInboundConnection connection, + PingPassthroughMode mode, List servers, ProtocolVersion responseProtocolVersion) { + ServerPing fallback = constructLocalPing(connection.getProtocolVersion()); + List> pings = new ArrayList<>(); + for (String s : servers) { + Optional rs = server.getServer(s); + if (!rs.isPresent()) { + continue; + } + VelocityRegisteredServer vrs = (VelocityRegisteredServer) rs.get(); + pings.add(vrs.ping(connection.getConnection().eventLoop(), responseProtocolVersion)); + } + if (pings.isEmpty()) { + return CompletableFuture.completedFuture(fallback); + } + + CompletableFuture> pingResponses = CompletableFutures.successfulAsList(pings, + (ex) -> fallback); + switch (mode) { + case ALL: + return pingResponses.thenApply(responses -> { + // Find the first non-fallback + for (ServerPing response : responses) { + if (response == fallback) { + continue; + } + return response; + } + return fallback; + }); + case MODS: + return pingResponses.thenApply(responses -> { + // Find the first non-fallback that contains a mod list + for (ServerPing response : responses) { + if (response == fallback) { + continue; + } + Optional modInfo = response.getModinfo(); + if (modInfo.isPresent()) { + return fallback.asBuilder().mods(modInfo.get()).build(); + } + } + return fallback; + }); + case DESCRIPTION: + return pingResponses.thenApply(responses -> { + // Find the first non-fallback. If it includes a modlist, add it too. + for (ServerPing response : responses) { + if (response == fallback) { + continue; + } + + if (response.getDescriptionComponent() == null) { + continue; + } + + return new ServerPing( + fallback.getVersion(), + fallback.getPlayers().orElse(null), + response.getDescriptionComponent(), + fallback.getFavicon().orElse(null), + response.getModinfo().orElse(null) + ); + } + return fallback; + }); + // Not possible, but covered for completeness. + default: + return CompletableFuture.completedFuture(fallback); + } + } + + /** + * Fetches the "default" server ping for a player. + * + * @param connection the connection + * @return a future with the initial ping result + */ + public CompletableFuture getInitialPing(VelocityInboundConnection connection) { + VelocityConfiguration configuration = server.getConfiguration(); + ProtocolVersion shownVersion = ProtocolVersion.isSupported(connection.getProtocolVersion()) + ? connection.getProtocolVersion() : ProtocolVersion.MAXIMUM_VERSION; + PingPassthroughMode passthroughMode = configuration.getPingPassthrough(); + + if (passthroughMode == PingPassthroughMode.DISABLED) { + return CompletableFuture.completedFuture(constructLocalPing(shownVersion)); + } else { + String virtualHostStr = connection.getVirtualHost().map(InetSocketAddress::getHostString) + .map(str -> str.toLowerCase(Locale.ROOT)) + .orElse(""); + List serversToTry = server.getConfiguration().getForcedHosts().getOrDefault( + virtualHostStr, server.getConfiguration().getAttemptConnectionOrder()); + return attemptPingPassthrough(connection, passthroughMode, serversToTry, shownVersion); + } + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/util/VelocityInboundConnection.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/util/VelocityInboundConnection.java new file mode 100644 index 000000000..8cde57bd1 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/util/VelocityInboundConnection.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2018 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.connection.util; + +import com.velocitypowered.api.proxy.InboundConnection; +import com.velocitypowered.proxy.connection.MinecraftConnection; + +public interface VelocityInboundConnection extends InboundConnection { + MinecraftConnection getConnection(); +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/StateRegistry.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/StateRegistry.java index ccedb7dc8..49ce6c56d 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/StateRegistry.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/StateRegistry.java @@ -56,6 +56,7 @@ import com.velocitypowered.proxy.protocol.packet.PluginMessage; import com.velocitypowered.proxy.protocol.packet.ResourcePackRequest; import com.velocitypowered.proxy.protocol.packet.ResourcePackResponse; import com.velocitypowered.proxy.protocol.packet.Respawn; +import com.velocitypowered.proxy.protocol.packet.ServerData; import com.velocitypowered.proxy.protocol.packet.ServerLogin; import com.velocitypowered.proxy.protocol.packet.ServerLoginSuccess; import com.velocitypowered.proxy.protocol.packet.SetCompression; @@ -310,6 +311,8 @@ public enum StateRegistry { map(0x34, MINECRAFT_1_19, false)); clientbound.register(SystemChat.class, SystemChat::new, map(0x5F, MINECRAFT_1_19, true)); + clientbound.register(ServerData.class, ServerData::new, + map(0x3F, MINECRAFT_1_19, true)); } }, LOGIN { diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ServerData.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ServerData.java new file mode 100644 index 000000000..5f4857bc8 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ServerData.java @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2018 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; + +import com.velocitypowered.api.network.ProtocolVersion; +import com.velocitypowered.api.util.Favicon; +import com.velocitypowered.proxy.connection.MinecraftSessionHandler; +import com.velocitypowered.proxy.protocol.MinecraftPacket; +import com.velocitypowered.proxy.protocol.ProtocolUtils; +import io.netty.buffer.ByteBuf; +import net.kyori.adventure.text.Component; +import org.jetbrains.annotations.Nullable; + +public class ServerData implements MinecraftPacket { + + private @Nullable Component description; + private @Nullable Favicon favicon; + private boolean previewsChat; + + public ServerData() { + } + + public ServerData(@Nullable Component description, @Nullable Favicon favicon, + boolean previewsChat) { + this.description = description; + this.favicon = favicon; + this.previewsChat = previewsChat; + } + + @Override + public void decode(ByteBuf buf, ProtocolUtils.Direction direction, + ProtocolVersion protocolVersion) { + if (buf.readBoolean()) { + this.description = ProtocolUtils.getJsonChatSerializer(protocolVersion) + .deserialize(ProtocolUtils.readString(buf)); + } + if (buf.readBoolean()) { + this.favicon = new Favicon(ProtocolUtils.readString(buf)); + } + this.previewsChat = buf.readBoolean(); + } + + @Override + public void encode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion protocolVersion) { + boolean hasDescription = this.description != null; + buf.writeBoolean(hasDescription); + if (hasDescription) { + ProtocolUtils.writeString( + buf, + ProtocolUtils.getJsonChatSerializer(protocolVersion).serialize(this.description) + ); + } + + boolean hasFavicon = this.favicon != null; + buf.writeBoolean(hasFavicon); + if (hasFavicon) { + ProtocolUtils.writeString(buf, favicon.getBase64Url()); + } + + buf.writeBoolean(this.previewsChat); + } + + @Override + public boolean handle(MinecraftSessionHandler handler) { + return handler.handle(this); + } + + public Component getDescription() { + return description; + } + + public Favicon getFavicon() { + return favicon; + } + + public boolean isPreviewsChat() { + return previewsChat; + } +}