diff --git a/api/src/main/java/com/velocitypowered/api/event/player/CookieReceiveEvent.java b/api/src/main/java/com/velocitypowered/api/event/player/CookieReceiveEvent.java new file mode 100644 index 0000000000..d6ef0d556b --- /dev/null +++ b/api/src/main/java/com/velocitypowered/api/event/player/CookieReceiveEvent.java @@ -0,0 +1,155 @@ +/* + * Copyright (C) 2024 Velocity Contributors + * + * The Velocity API is licensed under the terms of the MIT License. For more details, + * reference the LICENSE file in the api top-level directory. + */ + +package com.velocitypowered.api.event.player; + +import com.google.common.base.Preconditions; +import com.velocitypowered.api.event.ResultedEvent; +import com.velocitypowered.api.event.annotation.AwaitingEvent; +import com.velocitypowered.api.proxy.Player; +import java.util.Arrays; +import net.kyori.adventure.key.Key; +import org.jetbrains.annotations.Nullable; + +/** + * This event is fired when a cookie response from a client is received by the proxy. + * This usually happens after either a proxy plugin or a backend server requested a cookie. + * Velocity will wait on this event to finish firing before discarding the + * received cookie (if handled) or forwarding it to the backend server. + */ +@AwaitingEvent +public final class CookieReceiveEvent implements ResultedEvent { + + private final Player player; + private final Key originalKey; + private final byte @Nullable [] originalData; + private ForwardResult result; + + /** + * Creates a new instance. + * + * @param player the player who sent the cookie response + * @param key the identifier of the cookie + * @param data the data of the cookie + */ + public CookieReceiveEvent(final Player player, final Key key, final byte @Nullable [] data) { + this.player = Preconditions.checkNotNull(player, "player"); + this.originalKey = Preconditions.checkNotNull(key, "key"); + this.originalData = data; + this.result = ForwardResult.forward(); + } + + @Override + public ForwardResult getResult() { + return result; + } + + @Override + public void setResult(ForwardResult result) { + this.result = Preconditions.checkNotNull(result, "result"); + } + + public Player getPlayer() { + return player; + } + + public Key getOriginalKey() { + return originalKey; + } + + public byte @Nullable [] getOriginalData() { + return originalData; + } + + @Override + public String toString() { + return "CookieReceiveEvent{" + + ", originalKey=" + originalKey + + ", originalData=" + Arrays.toString(originalData) + + ", result=" + result + + '}'; + } + + /** + * A result determining whether or not to forward the cookie response on. + */ + public static final class ForwardResult implements ResultedEvent.Result { + + private static final ForwardResult ALLOWED = new ForwardResult(true, null, null); + private static final ForwardResult DENIED = new ForwardResult(false, null, null); + + private final boolean status; + private final Key key; + private final byte[] data; + + private ForwardResult(final boolean status, final Key key, final byte[] data) { + this.status = status; + this.key = key; + this.data = data; + } + + @Override + public boolean isAllowed() { + return status; + } + + public Key getKey() { + return key; + } + + public byte[] getData() { + return data; + } + + @Override + public String toString() { + return status ? "forward to backend server" : "handled by proxy"; + } + + /** + * Allows the cookie response to be forwarded to the backend server. + * + * @return the forward result + */ + public static ForwardResult forward() { + return ALLOWED; + } + + /** + * Prevents the cookie response from being forwarded to the backend server, the cookie response + * is handled by the proxy. + * + * @return the handled result + */ + public static ForwardResult handled() { + return DENIED; + } + + /** + * Allows the cookie response to be forwarded to the backend server, but silently replaces the + * identifier of the cookie with another. + * + * @param key the identifier to use instead + * @return a result with a new key + */ + public static ForwardResult key(final Key key) { + Preconditions.checkNotNull(key, "key"); + return new ForwardResult(true, key, null); + } + + /** + * Allows the cookie response to be forwarded to the backend server, but silently replaces the + * data of the cookie with another. + * + * @param data the data of the cookie to use instead + * @return a result with new data + */ + public static ForwardResult data(final byte[] data) { + return new ForwardResult(true, null, data); + } + } +} diff --git a/api/src/main/java/com/velocitypowered/api/event/player/CookieRequestEvent.java b/api/src/main/java/com/velocitypowered/api/event/player/CookieRequestEvent.java new file mode 100644 index 0000000000..021fd7e632 --- /dev/null +++ b/api/src/main/java/com/velocitypowered/api/event/player/CookieRequestEvent.java @@ -0,0 +1,127 @@ +/* + * Copyright (C) 2024 Velocity Contributors + * + * The Velocity API is licensed under the terms of the MIT License. For more details, + * reference the LICENSE file in the api top-level directory. + */ + +package com.velocitypowered.api.event.player; + +import com.google.common.base.Preconditions; +import com.velocitypowered.api.event.ResultedEvent; +import com.velocitypowered.api.event.annotation.AwaitingEvent; +import com.velocitypowered.api.proxy.Player; +import net.kyori.adventure.key.Key; + +/** + * This event is fired when a cookie is requested from a client either by a proxy plugin or + * by a backend server. Velocity will wait on this event to finish firing before discarding the + * received cookie (if handled) or forwarding it to the backend server. + */ +@AwaitingEvent +public final class CookieRequestEvent implements ResultedEvent { + + private final Player player; + private final Key originalKey; + private ForwardResult result; + + /** + * Creates a new instance. + * + * @param player the player from whom the cookies is requested + * @param key the identifier of the cookie + */ + public CookieRequestEvent(final Player player, final Key key) { + this.player = Preconditions.checkNotNull(player, "player"); + this.originalKey = Preconditions.checkNotNull(key, "key"); + this.result = ForwardResult.forward(); + } + + @Override + public ForwardResult getResult() { + return result; + } + + @Override + public void setResult(ForwardResult result) { + this.result = Preconditions.checkNotNull(result, "result"); + } + + public Player getPlayer() { + return player; + } + + public Key getOriginalKey() { + return originalKey; + } + + @Override + public String toString() { + return "CookieRequestEvent{" + + ", originalKey=" + originalKey + + ", result=" + result + + '}'; + } + + /** + * A result determining whether or not to forward the cookie request on. + */ + public static final class ForwardResult implements Result { + + private static final ForwardResult ALLOWED = new ForwardResult(true, null); + private static final ForwardResult DENIED = new ForwardResult(false, null); + + private final boolean status; + private final Key key; + + private ForwardResult(final boolean status, final Key key) { + this.status = status; + this.key = key; + } + + @Override + public boolean isAllowed() { + return status; + } + + public Key getKey() { + return key; + } + + @Override + public String toString() { + return status ? "forward to client" : "handled by proxy"; + } + + /** + * Allows the cookie request to be forwarded to the client. + * + * @return the forward result + */ + public static ForwardResult forward() { + return ALLOWED; + } + + /** + * Prevents the cookie request from being forwarded to the client, the cookie request is + * handled by the proxy. + * + * @return the handled result + */ + public static ForwardResult handled() { + return DENIED; + } + + /** + * Allows the cookie response to be forwarded to the client, but silently replaces the + * identifier of the cookie with another. + * + * @param key the identifier to use instead + * @return a result with a new key + */ + public static ForwardResult key(final Key key) { + Preconditions.checkNotNull(key, "key"); + return new ForwardResult(true, key); + } + } +} diff --git a/api/src/main/java/com/velocitypowered/api/event/player/CookieStoreEvent.java b/api/src/main/java/com/velocitypowered/api/event/player/CookieStoreEvent.java new file mode 100644 index 0000000000..0bcb4dcd15 --- /dev/null +++ b/api/src/main/java/com/velocitypowered/api/event/player/CookieStoreEvent.java @@ -0,0 +1,154 @@ +/* + * Copyright (C) 2024 Velocity Contributors + * + * The Velocity API is licensed under the terms of the MIT License. For more details, + * reference the LICENSE file in the api top-level directory. + */ + +package com.velocitypowered.api.event.player; + +import com.google.common.base.Preconditions; +import com.velocitypowered.api.event.ResultedEvent; +import com.velocitypowered.api.event.annotation.AwaitingEvent; +import com.velocitypowered.api.proxy.Player; +import java.util.Arrays; +import net.kyori.adventure.key.Key; + +/** + * This event is fired when a cookie should be stored on a player's client. This process can be + * initiated either by a proxy plugin or by a backend server. Velocity will wait on this event + * to finish firing before discarding the cookie (if handled) or forwarding it to the client so + * that it can store the cookie. + */ +@AwaitingEvent +public final class CookieStoreEvent implements ResultedEvent { + + private final Player player; + private final Key originalKey; + private final byte[] originalData; + private ForwardResult result; + + /** + * Creates a new instance. + * + * @param player the player who should store the cookie + * @param key the identifier of the cookie + * @param data the data of the cookie + */ + public CookieStoreEvent(final Player player, final Key key, final byte[] data) { + this.player = Preconditions.checkNotNull(player, "player"); + this.originalKey = Preconditions.checkNotNull(key, "key"); + this.originalData = Preconditions.checkNotNull(data, "data"); + this.result = ForwardResult.forward(); + } + + @Override + public ForwardResult getResult() { + return result; + } + + @Override + public void setResult(ForwardResult result) { + this.result = Preconditions.checkNotNull(result, "result"); + } + + public Player getPlayer() { + return player; + } + + public Key getOriginalKey() { + return originalKey; + } + + public byte[] getOriginalData() { + return originalData; + } + + @Override + public String toString() { + return "CookieStoreEvent{" + + ", originalKey=" + originalKey + + ", originalData=" + Arrays.toString(originalData) + + ", result=" + result + + '}'; + } + + /** + * A result determining whether or not to forward the cookie on. + */ + public static final class ForwardResult implements Result { + + private static final ForwardResult ALLOWED = new ForwardResult(true, null, null); + private static final ForwardResult DENIED = new ForwardResult(false, null, null); + + private final boolean status; + private final Key key; + private final byte[] data; + + private ForwardResult(final boolean status, final Key key, final byte[] data) { + this.status = status; + this.key = key; + this.data = data; + } + + @Override + public boolean isAllowed() { + return status; + } + + public Key getKey() { + return key; + } + + public byte[] getData() { + return data; + } + + @Override + public String toString() { + return status ? "forward to client" : "handled by proxy"; + } + + /** + * Allows the cookie to be forwarded to the client so that it can store it. + * + * @return the forward result + */ + public static ForwardResult forward() { + return ALLOWED; + } + + /** + * Prevents the cookie from being forwarded to the client, the cookie is handled by the proxy. + * + * @return the handled result + */ + public static ForwardResult handled() { + return DENIED; + } + + /** + * Allows the cookie to be forwarded to the client so that it can store it, but silently + * replaces the identifier of the cookie with another. + * + * @param key the identifier to use instead + * @return a result with a new key + */ + public static ForwardResult key(final Key key) { + Preconditions.checkNotNull(key, "key"); + return new ForwardResult(true, key, null); + } + + /** + * Allows the cookie to be forwarded to the client so that it can store it, but silently + * replaces the data of the cookie with another. + * + * @param data the data of the cookie to use instead + * @return a result with new data + */ + public static ForwardResult data(final byte[] data) { + Preconditions.checkNotNull(data, "data"); + return new ForwardResult(true, null, data); + } + } +} diff --git a/api/src/main/java/com/velocitypowered/api/proxy/Player.java b/api/src/main/java/com/velocitypowered/api/proxy/Player.java index 57b3075750..dfe9a2bc72 100644 --- a/api/src/main/java/com/velocitypowered/api/proxy/Player.java +++ b/api/src/main/java/com/velocitypowered/api/proxy/Player.java @@ -8,6 +8,7 @@ package com.velocitypowered.api.proxy; import com.velocitypowered.api.command.CommandSource; +import com.velocitypowered.api.event.player.CookieReceiveEvent; import com.velocitypowered.api.event.player.PlayerResourcePackStatusEvent; import com.velocitypowered.api.proxy.crypto.KeyIdentifiable; import com.velocitypowered.api.proxy.messages.ChannelIdentifier; @@ -436,4 +437,28 @@ default void openBook(@NotNull Book book) { * @since 3.3.0 */ void transferToHost(@NotNull InetSocketAddress address); + + /** + * Stores a cookie with arbitrary data on the player's client. + * + * @param key the identifier of the cookie + * @param data the data of the cookie + * @throws IllegalArgumentException if the player is from a version lower than 1.20.5 + * @since 3.3.0 + * @sinceMinecraft 1.20.5 + */ + void storeCookie(Key key, byte[] data); + + /** + * Requests a previously stored cookie from the player's client. + * Calling this method causes the client to send the cookie to the proxy. + * To retrieve the actual data of the requested cookie, you have to use the + * {@link CookieReceiveEvent}. + * + * @param key the identifier of the cookie + * @throws IllegalArgumentException if the player is from a version lower than 1.20.5 + * @since 3.3.0 + * @sinceMinecraft 1.20.5 + */ + void requestCookie(Key key); } \ No newline at end of file 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 67cd9d11b3..288e667e12 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftSessionHandler.java @@ -22,6 +22,8 @@ import com.velocitypowered.proxy.protocol.packet.BossBarPacket; import com.velocitypowered.proxy.protocol.packet.BundleDelimiterPacket; import com.velocitypowered.proxy.protocol.packet.ClientSettingsPacket; +import com.velocitypowered.proxy.protocol.packet.ClientboundCookieRequestPacket; +import com.velocitypowered.proxy.protocol.packet.ClientboundStoreCookiePacket; import com.velocitypowered.proxy.protocol.packet.DisconnectPacket; import com.velocitypowered.proxy.protocol.packet.EncryptionRequestPacket; import com.velocitypowered.proxy.protocol.packet.EncryptionResponsePacket; @@ -45,6 +47,7 @@ import com.velocitypowered.proxy.protocol.packet.ServerDataPacket; import com.velocitypowered.proxy.protocol.packet.ServerLoginPacket; import com.velocitypowered.proxy.protocol.packet.ServerLoginSuccessPacket; +import com.velocitypowered.proxy.protocol.packet.ServerboundCookieResponsePacket; import com.velocitypowered.proxy.protocol.packet.SetCompressionPacket; import com.velocitypowered.proxy.protocol.packet.StatusPingPacket; import com.velocitypowered.proxy.protocol.packet.StatusRequestPacket; @@ -339,4 +342,16 @@ default boolean handle(TransferPacket transfer) { default boolean handle(KnownPacksPacket packet) { return false; } + + default boolean handle(ClientboundStoreCookiePacket packet) { + return false; + } + + default boolean handle(ClientboundCookieRequestPacket packet) { + return false; + } + + default boolean handle(ServerboundCookieResponsePacket packet) { + 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 c5d1aa108f..5b83e65490 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 @@ -26,6 +26,8 @@ import com.velocitypowered.api.event.command.PlayerAvailableCommandsEvent; import com.velocitypowered.api.event.connection.PluginMessageEvent; import com.velocitypowered.api.event.connection.PreTransferEvent; +import com.velocitypowered.api.event.player.CookieRequestEvent; +import com.velocitypowered.api.event.player.CookieStoreEvent; import com.velocitypowered.api.event.player.PlayerResourcePackStatusEvent; import com.velocitypowered.api.event.player.ServerResourcePackSendEvent; import com.velocitypowered.api.event.proxy.ProxyPingEvent; @@ -48,6 +50,8 @@ import com.velocitypowered.proxy.protocol.packet.BossBarPacket; import com.velocitypowered.proxy.protocol.packet.BundleDelimiterPacket; import com.velocitypowered.proxy.protocol.packet.ClientSettingsPacket; +import com.velocitypowered.proxy.protocol.packet.ClientboundCookieRequestPacket; +import com.velocitypowered.proxy.protocol.packet.ClientboundStoreCookiePacket; import com.velocitypowered.proxy.protocol.packet.DisconnectPacket; import com.velocitypowered.proxy.protocol.packet.KeepAlivePacket; import com.velocitypowered.proxy.protocol.packet.LegacyPlayerListItemPacket; @@ -70,6 +74,7 @@ import io.netty.handler.timeout.ReadTimeoutException; import java.net.InetSocketAddress; import java.util.regex.Pattern; +import net.kyori.adventure.key.Key; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -390,6 +395,39 @@ public boolean handle(TransferPacket packet) { return true; } + @Override + public boolean handle(ClientboundStoreCookiePacket packet) { + server.getEventManager() + .fire(new CookieStoreEvent(serverConn.getPlayer(), packet.getKey(), packet.getPayload())) + .thenAcceptAsync(event -> { + if (event.getResult().isAllowed()) { + final Key resultedKey = event.getResult().getKey() == null + ? event.getOriginalKey() : event.getResult().getKey(); + final byte[] resultedData = event.getResult().getData() == null + ? event.getOriginalData() : event.getResult().getData(); + + playerConnection.write(new ClientboundStoreCookiePacket(resultedKey, resultedData)); + } + }, playerConnection.eventLoop()); + + return true; + } + + @Override + public boolean handle(ClientboundCookieRequestPacket packet) { + server.getEventManager().fire(new CookieRequestEvent(serverConn.getPlayer(), packet.getKey())) + .thenAcceptAsync(event -> { + if (event.getResult().isAllowed()) { + final Key resultedKey = event.getResult().getKey() == null + ? event.getOriginalKey() : event.getResult().getKey(); + + playerConnection.write(new ClientboundCookieRequestPacket(resultedKey)); + } + }, playerConnection.eventLoop()); + + return true; + } + @Override public void handleGeneric(MinecraftPacket packet) { if (packet instanceof PluginMessagePacket) { diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/ConfigSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/ConfigSessionHandler.java index 31d1dedb04..baff6017b9 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/ConfigSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/ConfigSessionHandler.java @@ -18,6 +18,8 @@ package com.velocitypowered.proxy.connection.backend; import com.velocitypowered.api.event.connection.PreTransferEvent; +import com.velocitypowered.api.event.player.CookieRequestEvent; +import com.velocitypowered.api.event.player.CookieStoreEvent; import com.velocitypowered.api.event.player.PlayerResourcePackStatusEvent; import com.velocitypowered.api.event.player.ServerResourcePackSendEvent; import com.velocitypowered.api.network.ProtocolVersion; @@ -34,6 +36,8 @@ import com.velocitypowered.proxy.protocol.MinecraftPacket; import com.velocitypowered.proxy.protocol.StateRegistry; import com.velocitypowered.proxy.protocol.netty.MinecraftDecoder; +import com.velocitypowered.proxy.protocol.packet.ClientboundCookieRequestPacket; +import com.velocitypowered.proxy.protocol.packet.ClientboundStoreCookiePacket; import com.velocitypowered.proxy.protocol.packet.DisconnectPacket; import com.velocitypowered.proxy.protocol.packet.KeepAlivePacket; import com.velocitypowered.proxy.protocol.packet.PluginMessagePacket; @@ -48,6 +52,7 @@ import java.io.IOException; import java.net.InetSocketAddress; import java.util.concurrent.CompletableFuture; +import net.kyori.adventure.key.Key; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -251,6 +256,40 @@ public boolean handle(TransferPacket packet) { return true; } + @Override + public boolean handle(ClientboundStoreCookiePacket packet) { + server.getEventManager() + .fire(new CookieStoreEvent(serverConn.getPlayer(), packet.getKey(), packet.getPayload())) + .thenAcceptAsync(event -> { + if (event.getResult().isAllowed()) { + final Key resultedKey = event.getResult().getKey() == null + ? event.getOriginalKey() : event.getResult().getKey(); + final byte[] resultedData = event.getResult().getData() == null + ? event.getOriginalData() : event.getResult().getData(); + + serverConn.getPlayer().getConnection() + .write(new ClientboundStoreCookiePacket(resultedKey, resultedData)); + } + }, serverConn.ensureConnected().eventLoop()); + + return true; + } + + @Override + public boolean handle(ClientboundCookieRequestPacket packet) { + server.getEventManager().fire(new CookieRequestEvent(serverConn.getPlayer(), packet.getKey())) + .thenAcceptAsync(event -> { + if (event.getResult().isAllowed()) { + final Key resultedKey = event.getResult().getKey() == null + ? event.getOriginalKey() : event.getResult().getKey(); + + serverConn.getPlayer().getConnection().write(new ClientboundCookieRequestPacket(resultedKey)); + } + }, serverConn.ensureConnected().eventLoop()); + + return true; + } + @Override public void disconnected() { resultFuture.completeExceptionally( diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/LoginSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/LoginSessionHandler.java index 0c0e5e553f..a672c9174c 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/LoginSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/LoginSessionHandler.java @@ -17,6 +17,7 @@ package com.velocitypowered.proxy.connection.backend; +import com.velocitypowered.api.event.player.CookieRequestEvent; import com.velocitypowered.api.event.player.ServerLoginPluginMessageEvent; import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier; @@ -31,6 +32,8 @@ import com.velocitypowered.proxy.connection.util.ConnectionRequestResults; import com.velocitypowered.proxy.connection.util.ConnectionRequestResults.Impl; import com.velocitypowered.proxy.protocol.StateRegistry; +import com.velocitypowered.proxy.protocol.packet.ClientboundCookieRequestPacket; +import com.velocitypowered.proxy.protocol.packet.ClientboundStoreCookiePacket; import com.velocitypowered.proxy.protocol.packet.DisconnectPacket; import com.velocitypowered.proxy.protocol.packet.EncryptionRequestPacket; import com.velocitypowered.proxy.protocol.packet.LoginAcknowledgedPacket; @@ -43,6 +46,7 @@ import io.netty.buffer.ByteBufUtil; import io.netty.buffer.Unpooled; import java.util.concurrent.CompletableFuture; +import net.kyori.adventure.key.Key; import net.kyori.adventure.text.Component; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -174,6 +178,26 @@ public boolean handle(ServerLoginSuccessPacket packet) { return true; } + @Override + public boolean handle(ClientboundStoreCookiePacket packet) { + throw new IllegalStateException("Can only store cookie in CONFIGURATION or PLAY protocol"); + } + + @Override + public boolean handle(ClientboundCookieRequestPacket packet) { + server.getEventManager().fire(new CookieRequestEvent(serverConn.getPlayer(), packet.getKey())) + .thenAcceptAsync(event -> { + if (event.getResult().isAllowed()) { + final Key resultedKey = event.getResult().getKey() == null + ? event.getOriginalKey() : event.getResult().getKey(); + + serverConn.getPlayer().getConnection().write(new ClientboundCookieRequestPacket(resultedKey)); + } + }, serverConn.ensureConnected().eventLoop()); + + return true; + } + @Override public void exception(Throwable throwable) { resultFuture.completeExceptionally(throwable); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/AuthSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/AuthSessionHandler.java index 4315edf7a0..77290584d8 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/AuthSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/AuthSessionHandler.java @@ -24,6 +24,7 @@ import com.velocitypowered.api.event.connection.LoginEvent; import com.velocitypowered.api.event.connection.PostLoginEvent; import com.velocitypowered.api.event.permission.PermissionsSetupEvent; +import com.velocitypowered.api.event.player.CookieReceiveEvent; import com.velocitypowered.api.event.player.GameProfileRequestEvent; import com.velocitypowered.api.event.player.PlayerChooseInitialServerEvent; import com.velocitypowered.api.network.ProtocolVersion; @@ -41,6 +42,7 @@ import com.velocitypowered.proxy.protocol.StateRegistry; import com.velocitypowered.proxy.protocol.packet.LoginAcknowledgedPacket; import com.velocitypowered.proxy.protocol.packet.ServerLoginSuccessPacket; +import com.velocitypowered.proxy.protocol.packet.ServerboundCookieResponsePacket; import com.velocitypowered.proxy.protocol.packet.SetCompressionPacket; import io.netty.buffer.ByteBuf; import java.util.Objects; @@ -188,6 +190,24 @@ public boolean handle(LoginAcknowledgedPacket packet) { return true; } + @Override + public boolean handle(ServerboundCookieResponsePacket packet) { + server.getEventManager() + .fire(new CookieReceiveEvent(connectedPlayer, packet.getKey(), packet.getPayload())) + .thenAcceptAsync(event -> { + if (event.getResult().isAllowed()) { + // The received cookie must have been requested by a proxy plugin in login phase, + // because if a backend server requests a cookie in login phase, the client is already + // in config phase. Therefore, the only way, we receive a CookieResponsePacket from a + // client in login phase is when a proxy plugin requested a cookie in login phase. + throw new IllegalStateException( + "A cookie was requested by a proxy plugin in login phase but the response wasn't handled"); + } + }, mcConnection.eventLoop()); + + return true; + } + private void completeLoginProtocolPhaseAndInitialize(ConnectedPlayer player) { mcConnection.setAssociation(player); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientConfigSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientConfigSessionHandler.java index d9c5295db8..52b67f7b58 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientConfigSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientConfigSessionHandler.java @@ -17,6 +17,7 @@ package com.velocitypowered.proxy.connection.client; +import com.velocitypowered.api.event.player.CookieReceiveEvent; import com.velocitypowered.api.event.player.PlayerClientBrandEvent; import com.velocitypowered.proxy.VelocityServer; import com.velocitypowered.proxy.connection.MinecraftConnection; @@ -32,6 +33,7 @@ import com.velocitypowered.proxy.protocol.packet.PingIdentifyPacket; import com.velocitypowered.proxy.protocol.packet.PluginMessagePacket; import com.velocitypowered.proxy.protocol.packet.ResourcePackResponsePacket; +import com.velocitypowered.proxy.protocol.packet.ServerboundCookieResponsePacket; import com.velocitypowered.proxy.protocol.packet.config.FinishedUpdatePacket; import com.velocitypowered.proxy.protocol.packet.config.KnownPacksPacket; import com.velocitypowered.proxy.protocol.util.PluginMessageUtil; @@ -39,6 +41,7 @@ import io.netty.buffer.Unpooled; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; +import net.kyori.adventure.key.Key; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; import org.apache.logging.log4j.LogManager; @@ -144,6 +147,28 @@ public boolean handle(KnownPacksPacket packet) { return false; } + @Override + public boolean handle(ServerboundCookieResponsePacket packet) { + server.getEventManager() + .fire(new CookieReceiveEvent(player, packet.getKey(), packet.getPayload())) + .thenAcceptAsync(event -> { + if (event.getResult().isAllowed()) { + final VelocityServerConnection serverConnection = player.getConnectionInFlight(); + if (serverConnection != null) { + final Key resultedKey = event.getResult().getKey() == null + ? event.getOriginalKey() : event.getResult().getKey(); + final byte[] resultedData = event.getResult().getData() == null + ? event.getOriginalData() : event.getResult().getData(); + + serverConnection.ensureConnected() + .write(new ServerboundCookieResponsePacket(resultedKey, resultedData)); + } + } + }, player.getConnection().eventLoop()); + + return true; + } + @Override public void handleGeneric(MinecraftPacket packet) { VelocityServerConnection serverConnection = player.getConnectedServer(); 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 f5e6e880fe..2a595b1673 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 @@ -23,6 +23,7 @@ import com.mojang.brigadier.suggestion.Suggestion; import com.velocitypowered.api.command.VelocityBrigadierMessage; import com.velocitypowered.api.event.connection.PluginMessageEvent; +import com.velocitypowered.api.event.player.CookieReceiveEvent; import com.velocitypowered.api.event.player.PlayerChannelRegisterEvent; import com.velocitypowered.api.event.player.PlayerClientBrandEvent; import com.velocitypowered.api.event.player.TabCompleteEvent; @@ -48,6 +49,7 @@ import com.velocitypowered.proxy.protocol.packet.PluginMessagePacket; import com.velocitypowered.proxy.protocol.packet.ResourcePackResponsePacket; import com.velocitypowered.proxy.protocol.packet.RespawnPacket; +import com.velocitypowered.proxy.protocol.packet.ServerboundCookieResponsePacket; import com.velocitypowered.proxy.protocol.packet.TabCompleteRequestPacket; import com.velocitypowered.proxy.protocol.packet.TabCompleteResponsePacket; import com.velocitypowered.proxy.protocol.packet.TabCompleteResponsePacket.Offer; @@ -83,6 +85,7 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.TimeUnit; +import net.kyori.adventure.key.Key; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; import org.apache.logging.log4j.LogManager; @@ -418,6 +421,28 @@ public boolean handle(FinishedUpdatePacket packet) { return true; } + @Override + public boolean handle(ServerboundCookieResponsePacket packet) { + server.getEventManager() + .fire(new CookieReceiveEvent(player, packet.getKey(), packet.getPayload())) + .thenAcceptAsync(event -> { + if (event.getResult().isAllowed()) { + final VelocityServerConnection serverConnection = player.getConnectedServer(); + if (serverConnection != null) { + final Key resultedKey = event.getResult().getKey() == null + ? event.getOriginalKey() : event.getResult().getKey(); + final byte[] resultedData = event.getResult().getData() == null + ? event.getOriginalData() : event.getResult().getData(); + + serverConnection.ensureConnected() + .write(new ServerboundCookieResponsePacket(resultedKey, resultedData)); + } + } + }, player.getConnection().eventLoop()); + + return true; + } + @Override public void handleGeneric(MinecraftPacket packet) { VelocityServerConnection serverConnection = player.getConnectedServer(); 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 edefbc2d8d..a471e2b531 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 @@ -27,6 +27,8 @@ import com.velocitypowered.api.event.connection.DisconnectEvent; import com.velocitypowered.api.event.connection.DisconnectEvent.LoginStatus; import com.velocitypowered.api.event.connection.PreTransferEvent; +import com.velocitypowered.api.event.player.CookieRequestEvent; +import com.velocitypowered.api.event.player.CookieStoreEvent; import com.velocitypowered.api.event.player.KickedFromServerEvent; import com.velocitypowered.api.event.player.KickedFromServerEvent.DisconnectPlayer; import com.velocitypowered.api.event.player.KickedFromServerEvent.Notify; @@ -65,6 +67,8 @@ import com.velocitypowered.proxy.protocol.StateRegistry; import com.velocitypowered.proxy.protocol.netty.MinecraftEncoder; import com.velocitypowered.proxy.protocol.packet.ClientSettingsPacket; +import com.velocitypowered.proxy.protocol.packet.ClientboundCookieRequestPacket; +import com.velocitypowered.proxy.protocol.packet.ClientboundStoreCookiePacket; import com.velocitypowered.proxy.protocol.packet.DisconnectPacket; import com.velocitypowered.proxy.protocol.packet.HeaderAndFooterPacket; import com.velocitypowered.proxy.protocol.packet.KeepAlivePacket; @@ -105,6 +109,7 @@ import net.kyori.adventure.audience.MessageType; import net.kyori.adventure.bossbar.BossBar; import net.kyori.adventure.identity.Identity; +import net.kyori.adventure.key.Key; import net.kyori.adventure.permission.PermissionChecker; import net.kyori.adventure.platform.facet.FacetPointers; import net.kyori.adventure.platform.facet.FacetPointers.Type; @@ -1008,6 +1013,50 @@ public void transferToHost(final InetSocketAddress address) { }); } + @Override + public void storeCookie(final Key key, final byte[] data) { + Preconditions.checkNotNull(key); + Preconditions.checkNotNull(data); + Preconditions.checkArgument( + this.getProtocolVersion().noLessThan(ProtocolVersion.MINECRAFT_1_20_5), + "Player version must be at least 1.20.5 to be able to store cookies"); + + if (connection.getState() != StateRegistry.PLAY + && connection.getState() != StateRegistry.CONFIG) { + throw new IllegalStateException("Can only store cookie in CONFIGURATION or PLAY protocol"); + } + + server.getEventManager().fire(new CookieStoreEvent(this, key, data)) + .thenAcceptAsync(event -> { + if (event.getResult().isAllowed()) { + final Key resultedKey = event.getResult().getKey() == null + ? event.getOriginalKey() : event.getResult().getKey(); + final byte[] resultedData = event.getResult().getData() == null + ? event.getOriginalData() : event.getResult().getData(); + + connection.write(new ClientboundStoreCookiePacket(resultedKey, resultedData)); + } + }, connection.eventLoop()); + } + + @Override + public void requestCookie(final Key key) { + Preconditions.checkNotNull(key); + Preconditions.checkArgument( + this.getProtocolVersion().noLessThan(ProtocolVersion.MINECRAFT_1_20_5), + "Player version must be at least 1.20.5 to be able to retrieve cookies"); + + server.getEventManager().fire(new CookieRequestEvent(this, key)) + .thenAcceptAsync(event -> { + if (event.getResult().isAllowed()) { + final Key resultedKey = event.getResult().getKey() == null + ? event.getOriginalKey() : event.getResult().getKey(); + + connection.write(new ClientboundCookieRequestPacket(resultedKey)); + } + }, connection.eventLoop()); + } + @Override public void addCustomChatCompletions(@NotNull Collection completions) { Preconditions.checkNotNull(completions, "completions"); 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 b0ee89f755..07a7dab488 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/StateRegistry.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/StateRegistry.java @@ -52,6 +52,8 @@ import com.velocitypowered.proxy.protocol.packet.BossBarPacket; import com.velocitypowered.proxy.protocol.packet.BundleDelimiterPacket; import com.velocitypowered.proxy.protocol.packet.ClientSettingsPacket; +import com.velocitypowered.proxy.protocol.packet.ClientboundCookieRequestPacket; +import com.velocitypowered.proxy.protocol.packet.ClientboundStoreCookiePacket; import com.velocitypowered.proxy.protocol.packet.DisconnectPacket; import com.velocitypowered.proxy.protocol.packet.EncryptionRequestPacket; import com.velocitypowered.proxy.protocol.packet.EncryptionResponsePacket; @@ -73,6 +75,7 @@ import com.velocitypowered.proxy.protocol.packet.ServerDataPacket; import com.velocitypowered.proxy.protocol.packet.ServerLoginPacket; import com.velocitypowered.proxy.protocol.packet.ServerLoginSuccessPacket; +import com.velocitypowered.proxy.protocol.packet.ServerboundCookieResponsePacket; import com.velocitypowered.proxy.protocol.packet.SetCompressionPacket; import com.velocitypowered.proxy.protocol.packet.StatusPingPacket; import com.velocitypowered.proxy.protocol.packet.StatusRequestPacket; @@ -146,6 +149,9 @@ public enum StateRegistry { serverbound.register( ClientSettingsPacket.class, ClientSettingsPacket::new, map(0x00, MINECRAFT_1_20_2, false)); + serverbound.register( + ServerboundCookieResponsePacket.class, ServerboundCookieResponsePacket::new, + map(0x01, MINECRAFT_1_20_5, false)); serverbound.register( PluginMessagePacket.class, PluginMessagePacket::new, map(0x01, MINECRAFT_1_20_2, false), @@ -171,6 +177,9 @@ public enum StateRegistry { KnownPacksPacket::new, map(0x07, MINECRAFT_1_20_5, false)); + clientbound.register( + ClientboundCookieRequestPacket.class, ClientboundCookieRequestPacket::new, + map(0x00, MINECRAFT_1_20_5, false)); clientbound.register( PluginMessagePacket.class, PluginMessagePacket::new, map(0x00, MINECRAFT_1_20_2, false), @@ -202,6 +211,9 @@ public enum StateRegistry { map(0x06, MINECRAFT_1_20_2, false), map(0x07, MINECRAFT_1_20_3, false), map(0x09, MINECRAFT_1_20_5, false)); + clientbound.register( + ClientboundStoreCookiePacket.class, ClientboundStoreCookiePacket::new, + map(0x0A, MINECRAFT_1_20_5, false)); clientbound.register(TransferPacket.class, TransferPacket::new, map(0x0B, MINECRAFT_1_20_5, false)); clientbound.register(ActiveFeaturesPacket.class, ActiveFeaturesPacket::new, @@ -276,6 +288,9 @@ public enum StateRegistry { map(0x08, MINECRAFT_1_19_4, false), map(0x09, MINECRAFT_1_20_2, false), map(0x0A, MINECRAFT_1_20_5, false)); + serverbound.register( + ServerboundCookieResponsePacket.class, ServerboundCookieResponsePacket::new, + map(0x11, MINECRAFT_1_20_5, false)); serverbound.register( PluginMessagePacket.class, PluginMessagePacket::new, @@ -374,6 +389,9 @@ public enum StateRegistry { map(0x0E, MINECRAFT_1_19_3, false), map(0x10, MINECRAFT_1_19_4, false), map(0x11, MINECRAFT_1_20_2, false)); + clientbound.register( + ClientboundCookieRequestPacket.class, ClientboundCookieRequestPacket::new, + map(0x16, MINECRAFT_1_20_5, false)); clientbound.register( PluginMessagePacket.class, PluginMessagePacket::new, @@ -595,6 +613,9 @@ public enum StateRegistry { map(0x3A, MINECRAFT_1_19_4, false), map(0x3C, MINECRAFT_1_20_2, false), map(0x3E, MINECRAFT_1_20_5, false)); + clientbound.register( + ClientboundStoreCookiePacket.class, ClientboundStoreCookiePacket::new, + map(0x6B, MINECRAFT_1_20_5, false)); clientbound.register( SystemChatPacket.class, SystemChatPacket::new, @@ -654,6 +675,9 @@ public enum StateRegistry { serverbound.register( LoginAcknowledgedPacket.class, LoginAcknowledgedPacket::new, map(0x03, MINECRAFT_1_20_2, false)); + serverbound.register( + ServerboundCookieResponsePacket.class, ServerboundCookieResponsePacket::new, + map(0x04, MINECRAFT_1_20_5, false)); clientbound.register( DisconnectPacket.class, () -> new DisconnectPacket(this), @@ -671,6 +695,9 @@ public enum StateRegistry { LoginPluginMessagePacket.class, LoginPluginMessagePacket::new, map(0x04, MINECRAFT_1_13, false)); + clientbound.register( + ClientboundCookieRequestPacket.class, ClientboundCookieRequestPacket::new, + map(0x05, MINECRAFT_1_20_5, false)); } }; diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ClientboundCookieRequestPacket.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ClientboundCookieRequestPacket.java new file mode 100644 index 0000000000..fd558b29ca --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ClientboundCookieRequestPacket.java @@ -0,0 +1,57 @@ +/* + * 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; + +import com.velocitypowered.api.network.ProtocolVersion; +import com.velocitypowered.proxy.connection.MinecraftSessionHandler; +import com.velocitypowered.proxy.protocol.MinecraftPacket; +import com.velocitypowered.proxy.protocol.ProtocolUtils; +import com.velocitypowered.proxy.protocol.ProtocolUtils.Direction; +import io.netty.buffer.ByteBuf; +import net.kyori.adventure.key.Key; + +public class ClientboundCookieRequestPacket implements MinecraftPacket { + + private Key key; + + public Key getKey() { + return key; + } + + public ClientboundCookieRequestPacket() { + } + + public ClientboundCookieRequestPacket(final Key key) { + this.key = key; + } + + @Override + public void decode(ByteBuf buf, Direction direction, ProtocolVersion protocolVersion) { + this.key = ProtocolUtils.readKey(buf); + } + + @Override + public void encode(ByteBuf buf, Direction direction, ProtocolVersion protocolVersion) { + ProtocolUtils.writeKey(buf, key); + } + + @Override + public boolean handle(MinecraftSessionHandler handler) { + return handler.handle(this); + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ClientboundStoreCookiePacket.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ClientboundStoreCookiePacket.java new file mode 100644 index 0000000000..7823b55841 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ClientboundStoreCookiePacket.java @@ -0,0 +1,65 @@ +/* + * 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; + +import com.velocitypowered.api.network.ProtocolVersion; +import com.velocitypowered.proxy.connection.MinecraftSessionHandler; +import com.velocitypowered.proxy.protocol.MinecraftPacket; +import com.velocitypowered.proxy.protocol.ProtocolUtils; +import com.velocitypowered.proxy.protocol.ProtocolUtils.Direction; +import io.netty.buffer.ByteBuf; +import net.kyori.adventure.key.Key; + +public class ClientboundStoreCookiePacket implements MinecraftPacket { + + private Key key; + private byte[] payload; + + public Key getKey() { + return key; + } + + public byte[] getPayload() { + return payload; + } + + public ClientboundStoreCookiePacket() { + } + + public ClientboundStoreCookiePacket(final Key key, final byte[] payload) { + this.key = key; + this.payload = payload; + } + + @Override + public void decode(ByteBuf buf, Direction direction, ProtocolVersion protocolVersion) { + this.key = ProtocolUtils.readKey(buf); + this.payload = ProtocolUtils.readByteArray(buf, 5120); + } + + @Override + public void encode(ByteBuf buf, Direction direction, ProtocolVersion protocolVersion) { + ProtocolUtils.writeKey(buf, key); + ProtocolUtils.writeByteArray(buf, payload); + } + + @Override + public boolean handle(MinecraftSessionHandler handler) { + return handler.handle(this); + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ServerboundCookieResponsePacket.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ServerboundCookieResponsePacket.java new file mode 100644 index 0000000000..bee12b8024 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ServerboundCookieResponsePacket.java @@ -0,0 +1,72 @@ +/* + * 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; + +import com.velocitypowered.api.network.ProtocolVersion; +import com.velocitypowered.proxy.connection.MinecraftSessionHandler; +import com.velocitypowered.proxy.protocol.MinecraftPacket; +import com.velocitypowered.proxy.protocol.ProtocolUtils; +import com.velocitypowered.proxy.protocol.ProtocolUtils.Direction; +import io.netty.buffer.ByteBuf; +import net.kyori.adventure.key.Key; +import org.checkerframework.checker.nullness.qual.Nullable; + +public class ServerboundCookieResponsePacket implements MinecraftPacket { + + private Key key; + private byte @Nullable [] payload; + + public Key getKey() { + return key; + } + + public byte @Nullable [] getPayload() { + return payload; + } + + public ServerboundCookieResponsePacket() { + } + + public ServerboundCookieResponsePacket(final Key key, final byte @Nullable [] payload) { + this.key = key; + this.payload = payload; + } + + @Override + public void decode(ByteBuf buf, Direction direction, ProtocolVersion protocolVersion) { + this.key = ProtocolUtils.readKey(buf); + if (buf.readBoolean()) { + this.payload = ProtocolUtils.readByteArray(buf, 5120); + } + } + + @Override + public void encode(ByteBuf buf, Direction direction, ProtocolVersion protocolVersion) { + ProtocolUtils.writeKey(buf, key); + final boolean hasPayload = payload != null && payload.length > 0; + buf.writeBoolean(hasPayload); + if (hasPayload) { + ProtocolUtils.writeByteArray(buf, payload); + } + } + + @Override + public boolean handle(MinecraftSessionHandler handler) { + return handler.handle(this); + } +}