Configuration API (#12301)

This implements a solution that preemptively exits the tick loop if we have already marked the connection as disconnecting. This avoids changing the result of Connection#isConnected in order to avoid other possibly unintentional changes. Fundamentally it should be investigated if closing the connection async is really still needed.

This also additionally removes the login disconnecting logic for server stopping, as this was also a possible issue in the config stage but also shouldn't be an issue as connections are closed on server stop very early.

Additionally, do not check for isConnecting() on VERIFYING, as that seemed to be an old diff originally trying to resolve this code, however isConnected is not updated at this point so it pretty much was useless.
This commit is contained in:
Owen
2025-06-26 15:55:03 -04:00
committed by Nassim Jahnke
parent dda39a0f05
commit 7f60924390
48 changed files with 1808 additions and 639 deletions

View File

@@ -0,0 +1,45 @@
package io.papermc.paper.connection;
import java.util.Map;
import com.destroystokyo.paper.ClientOption;
import org.bukkit.ServerLinks;
/**
* Represents a connection that has properties shared between the GAME and CONFIG stage.
*/
public interface PlayerCommonConnection extends WritablePlayerCookieConnection, ReadablePlayerCookieConnection {
/**
* Sends data to appear in this connection's report logs.
* This is useful for debugging server state that may be causing
* player disconnects.
* <p>
* These are formatted as key - value, where keys are limited to a length of 128 characters,
* values are limited to 4096, and 32 maximum entries can be sent.
*
* @param details report details
*/
void sendReportDetails(Map<String, String> details);
/**
* Sends the given server links to this connection.
*
* @param links links to send
*/
void sendLinks(ServerLinks links);
/**
* Transfers this connection to another server.
*
* @param host host
* @param port port
*/
void transfer(String host, int port);
/**
* @param type client option
* @return the client option value of the player
*/
<T> T getClientOption(ClientOption<T> type);
}

View File

@@ -0,0 +1,37 @@
package io.papermc.paper.connection;
import com.destroystokyo.paper.ClientOption;
import com.destroystokyo.paper.profile.PlayerProfile;
import net.kyori.adventure.audience.Audience;
public interface PlayerConfigurationConnection extends PlayerCommonConnection {
/**
* Returns the audience representing the player in configuration mode.
* This can be used to interact with the Adventure API during the configuration stage.
* This is guaranteed to be an instance of {@link PlayerConfigurationConnection}
*
* @return the configuring player audience
*/
Audience getAudience();
/**
* Gets the profile for this connection.
*
* @return profile
*/
PlayerProfile getProfile();
/**
* Clears the players chat history and their local chat.
*/
void clearChat();
/**
* Completes the configuration for this player, which will cause this player to reenter the game.
* <p>
* Note, this should be only be called if you are reconfiguring the player.
*/
void completeReconfiguration();
}

View File

@@ -0,0 +1,60 @@
package io.papermc.paper.connection;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import net.kyori.adventure.text.Component;
import org.jspecify.annotations.Nullable;
public interface PlayerConnection {
/**
* Disconnects the player connection.
* <p>
* Note that calling this during connection related events may caused undefined behavior.
*
* @param component disconnect reason
*/
void disconnect(Component component);
/**
* Gets if this connection originated from a transferred connection.
* <p>
* Do note that this is sent and stored on the client.
*
* @return is transferred
*/
boolean isTransferred();
/**
* Gets the raw remote address of the connection. This may be a proxy address
* or a Unix domain socket address, depending on how the channel was established.
*
* @return the remote {@link SocketAddress} of the channel
*/
SocketAddress getAddress();
/**
* Gets the real client address of the player. If the connection is behind a proxy,
* this will be the actual players IP address extracted from the proxy handshake.
*
* @return the client {@link InetSocketAddress}
*/
InetSocketAddress getClientAddress();
/**
* Returns the virtual host the client is connected to.
*
* <p>The virtual host refers to the hostname/port the client used to
* connect to the server.</p>
*
* @return The client's virtual host, or {@code null} if unknown
*/
@Nullable InetSocketAddress getVirtualHost();
/**
* Gets the socket address of this player's proxy
*
* @return the player's proxy address, null if the server doesn't have Proxy Protocol enabled, or the player didn't connect to an HAProxy instance
*/
@Nullable InetSocketAddress getHAProxyAddress();
}

View File

@@ -0,0 +1,21 @@
package io.papermc.paper.connection;
import org.bukkit.entity.Player;
public interface PlayerGameConnection extends PlayerCommonConnection {
/**
* Bumps the player to the configuration stage.
* <p>
* This will, by default, cause the player to stay until their connection is released by
* {@link PlayerConfigurationConnection#completeReconfiguration()}
*/
void reenterConfiguration();
/**
* Gets the player that is associated with this game connection.
*
* @return player
*/
Player getPlayer();
}

View File

@@ -0,0 +1,23 @@
package io.papermc.paper.connection;
import com.destroystokyo.paper.profile.PlayerProfile;
import org.jspecify.annotations.Nullable;
public interface PlayerLoginConnection extends ReadablePlayerCookieConnection {
/**
* Gets the authenticated profile for this connection.
* This may return null depending on what stage this connection is at.
*
* @return authenticated profile, or null if not present
*/
@Nullable PlayerProfile getAuthenticatedProfile();
/**
* Gets the player profile that this connection is requesting to authenticate as.
*
* @return the unsafe unauthenticated profile, or null if not sent
*/
@Nullable
PlayerProfile getUnsafeProfile();
}

View File

@@ -0,0 +1,18 @@
package io.papermc.paper.connection;
import java.util.concurrent.CompletableFuture;
import org.bukkit.NamespacedKey;
public interface ReadablePlayerCookieConnection extends PlayerConnection {
/**
* Retrieves a cookie from this connection.
*
* @param key the key identifying the cookie
* @return a {@link CompletableFuture} that will be completed when the
* Cookie response is received or otherwise available. If the cookie is not
* set in the client, the {@link CompletableFuture} will complete with a
* null value.
*/
CompletableFuture<byte[]> retrieveCookie(NamespacedKey key);
}

View File

@@ -0,0 +1,15 @@
package io.papermc.paper.connection;
import org.bukkit.NamespacedKey;
public interface WritablePlayerCookieConnection extends PlayerConnection {
/**
* Stores a cookie in this player's client.
*
* @param key the key identifying the cookie
* @param value the data to store in the cookie
* @throws IllegalStateException if a cookie cannot be stored at this time
*/
void storeCookie(NamespacedKey key, byte[] value);
}

View File

@@ -0,0 +1,9 @@
/**
* This package contains events related to player connections, such as joining and leaving the server.
*/
@ApiStatus.Experimental
@NullMarked
package io.papermc.paper.connection;
import org.jetbrains.annotations.ApiStatus;
import org.jspecify.annotations.NullMarked;

View File

@@ -0,0 +1,86 @@
package io.papermc.paper.event.connection;
import io.papermc.paper.connection.PlayerConnection;
import net.kyori.adventure.text.Component;
import org.bukkit.event.Event;
import org.bukkit.event.HandlerList;
import org.jetbrains.annotations.ApiStatus;
import org.jspecify.annotations.Nullable;
/**
* Validates whether a player connection is able to log in.
* <p>
* Called when is attempting to log in for the first time, or is finishing up
* being configured.
*/
public class PlayerConnectionValidateLoginEvent extends Event {
private static final HandlerList HANDLER_LIST = new HandlerList();
private final PlayerConnection connection;
private @Nullable Component kickMessage;
@ApiStatus.Internal
public PlayerConnectionValidateLoginEvent(final PlayerConnection connection, final @Nullable Component kickMessage) {
super(false);
this.connection = connection;
this.kickMessage = kickMessage;
}
/**
* Gets the connection of the player in this event.
* Note, the type of this connection is not guaranteed to be stable across versions.
* Additionally, disconnecting the player through this connection / using any methods that may send packets
* is not supported.
*
* @return connection
*/
public PlayerConnection getConnection() {
return this.connection;
}
/**
* Allows the player to log in.
* This skips any login validation checks.
*/
public void allow() {
this.kickMessage = null;
}
/**
* Disallows the player from logging in, with the given reason
*
* @param message Kick message to display to the user
*/
public void kickMessage(final Component message) {
this.kickMessage = message;
}
/**
* Gets the reason for why a player is not allowed to join the server.
* This will be null in the case that the player is allowed to log in.
*
* @return disallow reason
*/
public @Nullable Component getKickMessage() {
return this.kickMessage;
}
/**
* Gets if the player is allowed to enter the next stage.
*
* @return if allowed
*/
public boolean isAllowed() {
return this.kickMessage == null;
}
@Override
public HandlerList getHandlers() {
return HANDLER_LIST;
}
public static HandlerList getHandlerList() {
return HANDLER_LIST;
}
}

View File

@@ -0,0 +1,39 @@
package io.papermc.paper.event.connection.configuration;
import io.papermc.paper.connection.PlayerConfigurationConnection;
import org.bukkit.event.Event;
import org.bukkit.event.HandlerList;
import org.jetbrains.annotations.ApiStatus;
/**
* An event that allows you to configure the player.
* This is async and allows you to run configuration code on the player.
* Once this event has finished execution, the player connection will continue.
* <p>
* This occurs after configuration, but before the player has entered the world.
*/
public class AsyncPlayerConnectionConfigureEvent extends Event {
private static final HandlerList HANDLER_LIST = new HandlerList();
private final PlayerConfigurationConnection connection;
@ApiStatus.Internal
public AsyncPlayerConnectionConfigureEvent(final PlayerConfigurationConnection connection) {
super(true);
this.connection = connection;
}
public PlayerConfigurationConnection getConnection() {
return this.connection;
}
@Override
public HandlerList getHandlers() {
return HANDLER_LIST;
}
public static HandlerList getHandlerList() {
return HANDLER_LIST;
}
}

View File

@@ -0,0 +1,36 @@
package io.papermc.paper.event.connection.configuration;
import io.papermc.paper.connection.PlayerConfigurationConnection;
import org.bukkit.Bukkit;
import org.bukkit.event.Event;
import org.bukkit.event.HandlerList;
import org.jetbrains.annotations.ApiStatus;
/**
* Indicates that this player is being configured for the first time, meaning that the connection will start being configured automatically
*/
public class PlayerConnectionInitialConfigureEvent extends Event {
private static final HandlerList HANDLER_LIST = new HandlerList();
private final PlayerConfigurationConnection connection;
@ApiStatus.Internal
public PlayerConnectionInitialConfigureEvent(final PlayerConfigurationConnection connection) {
super(!Bukkit.isPrimaryThread());
this.connection = connection;
}
public PlayerConfigurationConnection getConnection() {
return this.connection;
}
@Override
public HandlerList getHandlers() {
return HANDLER_LIST;
}
public static HandlerList getHandlerList() {
return HANDLER_LIST;
}
}

View File

@@ -0,0 +1,37 @@
package io.papermc.paper.event.connection.configuration;
import io.papermc.paper.connection.PlayerConfigurationConnection;
import org.bukkit.Bukkit;
import org.bukkit.event.Event;
import org.bukkit.event.HandlerList;
import org.jetbrains.annotations.ApiStatus;
/**
* Indicates that this player is being reconfigured, meaning that this connection will be held in the configuration
* stage unless kicked out through {@link PlayerConfigurationConnection#completeReconfiguration()}
*/
public class PlayerConnectionReconfigureEvent extends Event {
private static final HandlerList HANDLER_LIST = new HandlerList();
private final PlayerConfigurationConnection connection;
@ApiStatus.Internal
public PlayerConnectionReconfigureEvent(final PlayerConfigurationConnection connection) {
super(!Bukkit.isPrimaryThread());
this.connection = connection;
}
public PlayerConfigurationConnection getConnection() {
return this.connection;
}
@Override
public HandlerList getHandlers() {
return HANDLER_LIST;
}
public static HandlerList getHandlerList() {
return HANDLER_LIST;
}
}

View File

@@ -0,0 +1,9 @@
/**
* Configuration connection events.
*/
@ApiStatus.Experimental
@NullMarked
package io.papermc.paper.event.connection.configuration;
import org.jetbrains.annotations.ApiStatus;
import org.jspecify.annotations.NullMarked;

View File

@@ -0,0 +1,9 @@
/**
* Common connection events.
*/
@NullMarked
@ApiStatus.Experimental
package io.papermc.paper.event.connection;
import org.jetbrains.annotations.ApiStatus;
import org.jspecify.annotations.NullMarked;

View File

@@ -0,0 +1,100 @@
/*
* Copyright (c) 2017 - Daniel Ennis (Aikar) - MIT License
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package io.papermc.paper.event.player;
import com.destroystokyo.paper.profile.PlayerProfile;
import net.kyori.adventure.text.Component;
import org.bukkit.event.Event;
import org.bukkit.event.HandlerList;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.Contract;
import org.jspecify.annotations.NullMarked;
/**
* Fires when computing if a server is currently considered full for a player.
*/
@NullMarked
public class PlayerServerFullCheckEvent extends Event {
private static final HandlerList HANDLER_LIST = new HandlerList();
private final PlayerProfile profile;
private Component kickMessage;
private boolean allow;
@ApiStatus.Internal
public PlayerServerFullCheckEvent(final PlayerProfile profile, final Component kickMessage, final boolean shouldKick) {
this.profile = profile;
this.kickMessage = kickMessage;
this.allow = !shouldKick;
}
/**
* @return the currently planned message to send to the user if they are unable to join the server
*/
@Contract(pure = true)
public Component kickMessage() {
return this.kickMessage;
}
/**
* @param kickMessage The message to send to the player on kick if not able to join.
*/
public void deny(final Component kickMessage) {
this.kickMessage = kickMessage;
}
/**
* @return The profile of the player trying to connect
*/
public PlayerProfile getPlayerProfile() {
return this.profile;
}
/**
* Sets whether the player is able to join this server.
* @param allow can join the server
*/
public void allow(final boolean allow) {
this.allow = allow;
}
/**
* Gets if the player is currently able to join the server.
*
* @return can join the server, or false if the server should be considered full
*/
public boolean isAllowed() {
return this.allow;
}
@Override
public HandlerList getHandlers() {
return HANDLER_LIST;
}
public static HandlerList getHandlerList() {
return HANDLER_LIST;
}
}

View File

@@ -10,6 +10,7 @@ import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import io.papermc.paper.connection.PlayerGameConnection;
import io.papermc.paper.entity.LookAnchor;
import io.papermc.paper.entity.PlayerGiveResult;
import io.papermc.paper.math.Position;
@@ -3925,4 +3926,12 @@ public interface Player extends HumanEntity, Conversable, OfflinePlayer, PluginM
* @param score New death screen score of player
*/
void setDeathScreenScore(int score);
/**
* Gets the game connection for this player.
*
* @return the game connection
*/
@ApiStatus.Experimental
PlayerGameConnection getConnection();
}

View File

@@ -5,6 +5,7 @@ import java.util.UUID;
import com.destroystokyo.paper.profile.PlayerProfile;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
import io.papermc.paper.connection.PlayerLoginConnection;
import org.bukkit.event.Event;
import org.bukkit.event.HandlerList;
import org.jetbrains.annotations.ApiStatus;
@@ -33,6 +34,7 @@ public class AsyncPlayerPreLoginEvent extends Event {
private Result result;
private Component message;
private PlayerProfile profile;
private final PlayerLoginConnection playerLoginConnection;
@ApiStatus.Internal
@Deprecated(since = "1.7.5", forRemoval = true)
@@ -60,11 +62,11 @@ public class AsyncPlayerPreLoginEvent extends Event {
@ApiStatus.Internal
@Deprecated(forRemoval = true)
public AsyncPlayerPreLoginEvent(@NotNull final String name, @NotNull final InetAddress ipAddress, @NotNull final InetAddress rawAddress, @NotNull final UUID uniqueId, boolean transferred, @NotNull com.destroystokyo.paper.profile.PlayerProfile profile) {
this(name, ipAddress, rawAddress, uniqueId, transferred, profile, "");
this(name, ipAddress, rawAddress, uniqueId, transferred, profile, "", null);
}
@ApiStatus.Internal
public AsyncPlayerPreLoginEvent(@NotNull final String name, @NotNull final InetAddress ipAddress, @NotNull final InetAddress rawAddress, @NotNull final UUID uniqueId, boolean transferred, @NotNull com.destroystokyo.paper.profile.PlayerProfile profile, @NotNull String hostname) {
public AsyncPlayerPreLoginEvent(@NotNull final String name, @NotNull final InetAddress ipAddress, @NotNull final InetAddress rawAddress, @NotNull final UUID uniqueId, boolean transferred, @NotNull com.destroystokyo.paper.profile.PlayerProfile profile, @NotNull String hostname, final PlayerLoginConnection playerLoginConnection) {
super(true);
this.result = Result.ALLOWED;
this.message = Component.empty();
@@ -73,6 +75,7 @@ public class AsyncPlayerPreLoginEvent extends Event {
this.rawAddress = rawAddress;
this.hostname = hostname;
this.transferred = transferred;
this.playerLoginConnection = playerLoginConnection;
}
/**
@@ -301,6 +304,16 @@ public class AsyncPlayerPreLoginEvent extends Event {
return this.transferred;
}
/**
* Gets the connection for the player logging in.
* @return connection
*/
@ApiStatus.Experimental
@NotNull
public PlayerLoginConnection getConnection() {
return playerLoginConnection;
}
@NotNull
@Override
public HandlerList getHandlers() {

View File

@@ -1,7 +1,9 @@
package org.bukkit.event.player;
import io.papermc.paper.connection.PlayerCommonConnection;
import io.papermc.paper.connection.PlayerConfigurationConnection;
import org.bukkit.ServerLinks;
import org.bukkit.entity.Player;
import org.bukkit.event.Event;
import org.bukkit.event.HandlerList;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
@@ -10,18 +12,28 @@ import org.jetbrains.annotations.NotNull;
* This event is called when the list of links is sent to the player.
*/
@ApiStatus.Experimental
public class PlayerLinksSendEvent extends PlayerEvent {
public class PlayerLinksSendEvent extends Event {
private static final HandlerList HANDLER_LIST = new HandlerList();
private final ServerLinks links;
private final PlayerCommonConnection connection;
@ApiStatus.Internal
public PlayerLinksSendEvent(@NotNull final Player player, @NotNull final ServerLinks links) {
super(player);
public PlayerLinksSendEvent(@NotNull final PlayerConfigurationConnection connection, @NotNull final ServerLinks links) {
this.connection = connection;
this.links = links;
}
/**
* Gets the connection that received the links.
* @return connection
*/
@NotNull
public PlayerCommonConnection getConnection() {
return connection;
}
/**
* Gets the links to be sent, for modification.
*

View File

@@ -1,6 +1,8 @@
package org.bukkit.event.player;
import java.net.InetAddress;
import io.papermc.paper.event.connection.PlayerConnectionValidateLoginEvent;
import org.bukkit.Warning;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
import org.bukkit.entity.Player;
@@ -14,7 +16,15 @@ import org.jetbrains.annotations.NotNull;
* Note that this event is called <i>early</i> in the player initialization
* process. It is recommended that most options involving the Player
* <i>entity</i> be postponed to the {@link PlayerJoinEvent} instead.
* @deprecated Use {@link PlayerConnectionValidateLoginEvent} to handle pre-login logic
* (e.g. authentication or ban checks), or {@link io.papermc.paper.event.player.PlayerServerFullCheckEvent} to allow
* players to bypass the server's maximum player limit.
* Minecraft triggers this twice internally, using this event skips one of the validation checks done by the server.
* Additionally, this event causes the full player entity to be created much earlier than it would be in Vanilla,
* leaving it with mostly disfunctional methods and state.
*/
@Warning(reason = "Listening to this event causes the player to be created early.")
@Deprecated(since = "1.21.6")
public class PlayerLoginEvent extends PlayerEvent {
private static final HandlerList HANDLER_LIST = new HandlerList();

View File

@@ -1,5 +1,6 @@
package org.bukkit.plugin.messaging;
import io.papermc.paper.connection.PlayerCommonConnection;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
@@ -18,4 +19,16 @@ public interface PluginMessageListener {
* @param message The raw message that was sent.
*/
public void onPluginMessageReceived(@NotNull String channel, @NotNull Player player, byte @NotNull [] message);
/**
* A method that will be invoked when a PluginMessageSource sends a plugin
* message on a registered channel.
*
* @param channel Channel that the message was sent through.
* @param connection Source of the message.
* @param message The raw message that was sent.
*/
default void onPluginMessageReceived(@NotNull String channel, @NotNull PlayerCommonConnection connection, byte @NotNull [] message) {
}
}