Mirror von
https://github.com/PaperMC/Velocity.git
synchronisiert 2024-11-06 00:00:47 +01:00
Ursprung
377e6b6626
Commit
d97ed956a7
@ -57,7 +57,8 @@ public enum ProtocolVersion {
|
||||
MINECRAFT_1_17(755, "1.17"),
|
||||
MINECRAFT_1_17_1(756, "1.17.1"),
|
||||
MINECRAFT_1_18(757, "1.18", "1.18.1"),
|
||||
MINECRAFT_1_18_2(758, "1.18.2");
|
||||
MINECRAFT_1_18_2(758, "1.18.2"),
|
||||
MINECRAFT_1_19(759, "1.19");
|
||||
|
||||
private static final int SNAPSHOT_BIT = 30;
|
||||
|
||||
|
@ -7,6 +7,7 @@
|
||||
|
||||
package com.velocitypowered.api.proxy;
|
||||
|
||||
import com.velocitypowered.api.proxy.crypto.KeyIdentifiable;
|
||||
import com.velocitypowered.api.proxy.messages.ChannelIdentifier;
|
||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||
|
||||
@ -14,7 +15,7 @@ import org.checkerframework.checker.nullness.qual.Nullable;
|
||||
* Allows the server to communicate with a client logging into the proxy using login plugin
|
||||
* messages.
|
||||
*/
|
||||
public interface LoginPhaseConnection extends InboundConnection {
|
||||
public interface LoginPhaseConnection extends InboundConnection, KeyIdentifiable {
|
||||
void sendLoginPluginMessage(ChannelIdentifier identifier, byte[] contents,
|
||||
MessageConsumer consumer);
|
||||
|
||||
|
@ -9,6 +9,7 @@ package com.velocitypowered.api.proxy;
|
||||
|
||||
import com.velocitypowered.api.command.CommandSource;
|
||||
import com.velocitypowered.api.event.player.PlayerResourcePackStatusEvent;
|
||||
import com.velocitypowered.api.proxy.crypto.KeyIdentifiable;
|
||||
import com.velocitypowered.api.proxy.messages.ChannelIdentifier;
|
||||
import com.velocitypowered.api.proxy.messages.ChannelMessageSink;
|
||||
import com.velocitypowered.api.proxy.messages.ChannelMessageSource;
|
||||
@ -37,7 +38,7 @@ import org.jetbrains.annotations.NotNull;
|
||||
* Represents a player who is connected to the proxy.
|
||||
*/
|
||||
public interface Player extends CommandSource, Identified, InboundConnection,
|
||||
ChannelMessageSource, ChannelMessageSink, HoverEventSource<HoverEvent.ShowEntity>, Keyed {
|
||||
ChannelMessageSource, ChannelMessageSink, HoverEventSource<HoverEvent.ShowEntity>, Keyed, KeyIdentifiable {
|
||||
|
||||
/**
|
||||
* Returns the player's current username.
|
||||
|
@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright (C) 2018 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.proxy.crypto;
|
||||
|
||||
import java.security.PublicKey;
|
||||
|
||||
/**
|
||||
* Represents session-server cross-signed dated RSA public key.
|
||||
*/
|
||||
public interface IdentifiedKey extends KeySigned {
|
||||
|
||||
/**
|
||||
* Returns RSA public key.
|
||||
* Note: this key is at least 2048 bits but may be larger.
|
||||
*
|
||||
* @return the RSA public key in question
|
||||
*/
|
||||
PublicKey getSignedPublicKey();
|
||||
|
||||
|
||||
/**
|
||||
* Validates a signature against this public key.
|
||||
* @param signature the signature data
|
||||
* @param toVerify the signed data
|
||||
*
|
||||
* @return validity of the signature
|
||||
*/
|
||||
boolean verifyDataSignature(byte[] signature, byte[]... toVerify);
|
||||
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
/*
|
||||
* Copyright (C) 2018 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.proxy.crypto;
|
||||
|
||||
/**
|
||||
* Identifies a type with a public RSA signature.
|
||||
*/
|
||||
public interface KeyIdentifiable {
|
||||
|
||||
/**
|
||||
* Returns the timed identified key of the object context.
|
||||
* <p>Only available in 1.19 and newer</p>
|
||||
* @return the key or null if not available
|
||||
*/
|
||||
IdentifiedKey getIdentifiedKey();
|
||||
}
|
@ -0,0 +1,76 @@
|
||||
/*
|
||||
* Copyright (C) 2018 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.proxy.crypto;
|
||||
|
||||
import com.google.common.annotations.Beta;
|
||||
|
||||
import java.security.PublicKey;
|
||||
import java.time.Instant;
|
||||
|
||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||
|
||||
public interface KeySigned {
|
||||
|
||||
/**
|
||||
* Returns the key used to sign the object.
|
||||
*
|
||||
* @return the key
|
||||
*/
|
||||
PublicKey getSigner();
|
||||
|
||||
/**
|
||||
* Returns the expiry time point of the key.
|
||||
* Note: this limit is arbitrary. RSA keys don't expire,
|
||||
* but the signature of this key as provided by the session
|
||||
* server will expire.
|
||||
*
|
||||
* @return the expiry time point
|
||||
*/
|
||||
Instant getExpiryTemporal();
|
||||
|
||||
|
||||
/**
|
||||
* Check if the signature has expired.
|
||||
*
|
||||
* @return true if proxy time is after expiry time
|
||||
*/
|
||||
default boolean hasExpired() {
|
||||
return Instant.now().isAfter(getExpiryTemporal());
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the signature of the signed object.
|
||||
*
|
||||
* @return an RSA signature
|
||||
*/
|
||||
@Nullable
|
||||
byte[] getSignature();
|
||||
|
||||
/**
|
||||
* Validates the signature, expiry temporal and key against the
|
||||
* signer public key. Note: This will **not** check for
|
||||
* expiry. You can check for expiry with {@link KeySigned#hasExpired()}.
|
||||
* <p>DOES NOT WORK YET FOR MESSAGES AND COMMANDS!</p>
|
||||
*
|
||||
* @return validity of the signature
|
||||
*/
|
||||
@Beta
|
||||
default boolean isSignatureValid() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the signature salt or null if not salted.
|
||||
*
|
||||
* @return signature salt or null
|
||||
*/
|
||||
default byte[] getSalt() {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright (C) 2018 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.proxy.crypto;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public interface SignedMessage extends KeySigned {
|
||||
|
||||
/**
|
||||
* Returns the signed message.
|
||||
*
|
||||
* @return the message
|
||||
*/
|
||||
String getMessage();
|
||||
|
||||
/**
|
||||
* Returns the signers UUID.
|
||||
*
|
||||
* @return the uuid
|
||||
*/
|
||||
UUID getSignerUuid();
|
||||
|
||||
/**
|
||||
* If true the signature of this message applies to a stylized component instead.
|
||||
*
|
||||
* @return signature signs preview
|
||||
*/
|
||||
boolean isPreviewSigned();
|
||||
|
||||
}
|
@ -8,6 +8,7 @@
|
||||
package com.velocitypowered.api.proxy.player;
|
||||
|
||||
import com.velocitypowered.api.proxy.Player;
|
||||
import com.velocitypowered.api.proxy.crypto.IdentifiedKey;
|
||||
import com.velocitypowered.api.util.GameProfile;
|
||||
import java.util.Collection;
|
||||
import java.util.Optional;
|
||||
@ -80,4 +81,19 @@ public interface TabList {
|
||||
@Deprecated
|
||||
TabListEntry buildEntry(GameProfile profile, @Nullable Component displayName, int latency,
|
||||
int gameMode);
|
||||
|
||||
/**
|
||||
* Builds a tab list entry.
|
||||
*
|
||||
* @deprecated Internal usage. Use {@link TabListEntry.Builder} instead.
|
||||
* @param profile profile
|
||||
* @param displayName display name
|
||||
* @param latency latency
|
||||
* @param gameMode game mode
|
||||
* @param key the player key
|
||||
* @return entry
|
||||
*/
|
||||
@Deprecated
|
||||
TabListEntry buildEntry(GameProfile profile, @Nullable Component displayName, int latency,
|
||||
int gameMode, @Nullable IdentifiedKey key);
|
||||
}
|
||||
|
@ -7,6 +7,8 @@
|
||||
|
||||
package com.velocitypowered.api.proxy.player;
|
||||
|
||||
import com.velocitypowered.api.proxy.crypto.IdentifiedKey;
|
||||
import com.velocitypowered.api.proxy.crypto.KeyIdentifiable;
|
||||
import com.velocitypowered.api.util.GameProfile;
|
||||
import java.util.Optional;
|
||||
import net.kyori.adventure.text.Component;
|
||||
@ -15,7 +17,7 @@ import org.checkerframework.checker.nullness.qual.Nullable;
|
||||
/**
|
||||
* Represents a single entry in a {@link TabList}.
|
||||
*/
|
||||
public interface TabListEntry {
|
||||
public interface TabListEntry extends KeyIdentifiable {
|
||||
|
||||
/**
|
||||
* Returns the parent {@link TabList} of this {@code this} {@link TabListEntry}.
|
||||
@ -125,6 +127,8 @@ public interface TabListEntry {
|
||||
private int latency = 0;
|
||||
private int gameMode = 0;
|
||||
|
||||
private @Nullable IdentifiedKey playerKey;
|
||||
|
||||
private Builder() {
|
||||
}
|
||||
|
||||
@ -152,6 +156,18 @@ public interface TabListEntry {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link IdentifiedKey} of the {@link TabListEntry}.
|
||||
*
|
||||
* @param playerKey key to set
|
||||
* @return {@code this}, for chaining
|
||||
* @see TabListEntry#getIdentifiedKey()
|
||||
*/
|
||||
public Builder playerKey(IdentifiedKey playerKey) {
|
||||
this.playerKey = playerKey;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the displayed name of the {@link TabListEntry}.
|
||||
*
|
||||
@ -200,7 +216,7 @@ public interface TabListEntry {
|
||||
if (profile == null) {
|
||||
throw new IllegalStateException("The GameProfile must be set when building a TabListEntry");
|
||||
}
|
||||
return tabList.buildEntry(profile, displayName, latency, gameMode);
|
||||
return tabList.buildEntry(profile, displayName, latency, gameMode, playerKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -45,6 +45,7 @@ import com.velocitypowered.proxy.config.VelocityConfiguration;
|
||||
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
|
||||
import com.velocitypowered.proxy.connection.player.VelocityResourcePackInfo;
|
||||
import com.velocitypowered.proxy.console.VelocityConsole;
|
||||
import com.velocitypowered.proxy.crypto.EncryptionUtils;
|
||||
import com.velocitypowered.proxy.event.VelocityEventManager;
|
||||
import com.velocitypowered.proxy.network.ConnectionManager;
|
||||
import com.velocitypowered.proxy.plugin.VelocityPluginManager;
|
||||
@ -55,7 +56,6 @@ import com.velocitypowered.proxy.scheduler.VelocityScheduler;
|
||||
import com.velocitypowered.proxy.server.ServerMap;
|
||||
import com.velocitypowered.proxy.util.AddressUtil;
|
||||
import com.velocitypowered.proxy.util.ClosestLocaleMatcher;
|
||||
import com.velocitypowered.proxy.util.EncryptionUtils;
|
||||
import com.velocitypowered.proxy.util.FileSystemUtils;
|
||||
import com.velocitypowered.proxy.util.VelocityChannelRegistrar;
|
||||
import com.velocitypowered.proxy.util.bossbar.AdventureBossBarManager;
|
||||
@ -80,7 +80,6 @@ import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.ResourceBundle;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
@ -97,7 +96,6 @@ import net.kyori.adventure.key.Key;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.translation.GlobalTranslator;
|
||||
import net.kyori.adventure.translation.TranslationRegistry;
|
||||
import net.kyori.adventure.util.UTF8ResourceBundleControl;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.asynchttpclient.AsyncHttpClient;
|
||||
|
@ -75,6 +75,7 @@ public class VelocityConfiguration implements ProxyConfig {
|
||||
@Expose private boolean enablePlayerAddressLogging = true;
|
||||
private net.kyori.adventure.text.@MonotonicNonNull Component motdAsComponent;
|
||||
private @Nullable Favicon favicon;
|
||||
@Expose private boolean forceKeyAuthentication = true; // Added in 1.19
|
||||
|
||||
private VelocityConfiguration(Servers servers, ForcedHosts forcedHosts, Advanced advanced,
|
||||
Query query, Metrics metrics) {
|
||||
@ -90,7 +91,7 @@ public class VelocityConfiguration implements ProxyConfig {
|
||||
PlayerInfoForwarding playerInfoForwardingMode, byte[] forwardingSecret,
|
||||
boolean onlineModeKickExistingPlayers, PingPassthroughMode pingPassthrough,
|
||||
boolean enablePlayerAddressLogging, Servers servers,ForcedHosts forcedHosts,
|
||||
Advanced advanced, Query query, Metrics metrics) {
|
||||
Advanced advanced, Query query, Metrics metrics, boolean forceKeyAuthentication) {
|
||||
this.bind = bind;
|
||||
this.motd = motd;
|
||||
this.showMaxPlayers = showMaxPlayers;
|
||||
@ -107,6 +108,7 @@ public class VelocityConfiguration implements ProxyConfig {
|
||||
this.advanced = advanced;
|
||||
this.query = query;
|
||||
this.metrics = metrics;
|
||||
this.forceKeyAuthentication = forceKeyAuthentication;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -381,6 +383,10 @@ public class VelocityConfiguration implements ProxyConfig {
|
||||
return advanced.isLogPlayerConnections();
|
||||
}
|
||||
|
||||
public boolean isForceKeyAuthentication() {
|
||||
return forceKeyAuthentication;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return MoreObjects.toStringHelper(this)
|
||||
@ -397,6 +403,7 @@ public class VelocityConfiguration implements ProxyConfig {
|
||||
.add("query", query)
|
||||
.add("favicon", favicon)
|
||||
.add("enablePlayerAddressLogging", enablePlayerAddressLogging)
|
||||
.add("forceKeyAuthentication", forceKeyAuthentication)
|
||||
.toString();
|
||||
}
|
||||
|
||||
@ -466,6 +473,7 @@ public class VelocityConfiguration implements ProxyConfig {
|
||||
String motd = config.getOrElse("motd", "	add3A Velocity Server");
|
||||
int maxPlayers = config.getIntOrElse("show-max-players", 500);
|
||||
Boolean onlineMode = config.getOrElse("online-mode", true);
|
||||
Boolean forceKeyAuthentication = config.getOrElse("force-key-authentication", true);
|
||||
Boolean announceForge = config.getOrElse("announce-forge", true);
|
||||
Boolean preventClientProxyConnections = config.getOrElse("prevent-client-proxy-connections",
|
||||
true);
|
||||
@ -488,7 +496,8 @@ public class VelocityConfiguration implements ProxyConfig {
|
||||
new ForcedHosts(forcedHostsConfig),
|
||||
new Advanced(advancedConfig),
|
||||
new Query(queryConfig),
|
||||
new Metrics(metricsConfig)
|
||||
new Metrics(metricsConfig),
|
||||
forceKeyAuthentication
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -20,7 +20,6 @@ package com.velocitypowered.proxy.connection;
|
||||
import com.velocitypowered.proxy.protocol.MinecraftPacket;
|
||||
import com.velocitypowered.proxy.protocol.packet.AvailableCommands;
|
||||
import com.velocitypowered.proxy.protocol.packet.BossBar;
|
||||
import com.velocitypowered.proxy.protocol.packet.Chat;
|
||||
import com.velocitypowered.proxy.protocol.packet.ClientSettings;
|
||||
import com.velocitypowered.proxy.protocol.packet.Disconnect;
|
||||
import com.velocitypowered.proxy.protocol.packet.EncryptionRequest;
|
||||
@ -46,6 +45,13 @@ import com.velocitypowered.proxy.protocol.packet.StatusRequest;
|
||||
import com.velocitypowered.proxy.protocol.packet.StatusResponse;
|
||||
import com.velocitypowered.proxy.protocol.packet.TabCompleteRequest;
|
||||
import com.velocitypowered.proxy.protocol.packet.TabCompleteResponse;
|
||||
import com.velocitypowered.proxy.protocol.packet.chat.LegacyChat;
|
||||
import com.velocitypowered.proxy.protocol.packet.chat.PlayerChat;
|
||||
import com.velocitypowered.proxy.protocol.packet.chat.PlayerChatPreview;
|
||||
import com.velocitypowered.proxy.protocol.packet.chat.PlayerCommand;
|
||||
import com.velocitypowered.proxy.protocol.packet.chat.ServerChatPreview;
|
||||
import com.velocitypowered.proxy.protocol.packet.chat.ServerPlayerChat;
|
||||
import com.velocitypowered.proxy.protocol.packet.chat.SystemChat;
|
||||
import com.velocitypowered.proxy.protocol.packet.title.LegacyTitlePacket;
|
||||
import com.velocitypowered.proxy.protocol.packet.title.TitleActionbarPacket;
|
||||
import com.velocitypowered.proxy.protocol.packet.title.TitleClearPacket;
|
||||
@ -104,7 +110,7 @@ public interface MinecraftSessionHandler {
|
||||
return false;
|
||||
}
|
||||
|
||||
default boolean handle(Chat packet) {
|
||||
default boolean handle(LegacyChat packet) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -231,4 +237,28 @@ public interface MinecraftSessionHandler {
|
||||
default boolean handle(ResourcePackResponse packet) {
|
||||
return false;
|
||||
}
|
||||
|
||||
default boolean handle(PlayerChat packet) {
|
||||
return false;
|
||||
}
|
||||
|
||||
default boolean handle(SystemChat packet) {
|
||||
return false;
|
||||
}
|
||||
|
||||
default boolean handle(ServerPlayerChat packet) {
|
||||
return false;
|
||||
}
|
||||
|
||||
default boolean handle(PlayerChatPreview packet) {
|
||||
return false;
|
||||
}
|
||||
|
||||
default boolean handle(ServerChatPreview packet) {
|
||||
return false;
|
||||
}
|
||||
|
||||
default boolean handle(PlayerCommand packet) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -24,7 +24,8 @@ public class VelocityConstants {
|
||||
}
|
||||
|
||||
public static final String VELOCITY_IP_FORWARDING_CHANNEL = "velocity:player_info";
|
||||
public static final int FORWARDING_VERSION = 1;
|
||||
public static final int MODERN_FORWARDING_DEFAULT = 1;
|
||||
public static final int MODERN_FORWARDING_WITH_KEY = 2;
|
||||
|
||||
public static final byte[] EMPTY_BYTE_ARRAY = new byte[0];
|
||||
}
|
||||
|
@ -18,6 +18,8 @@
|
||||
package com.velocitypowered.proxy.connection.backend;
|
||||
|
||||
import com.velocitypowered.api.event.player.ServerLoginPluginMessageEvent;
|
||||
import com.velocitypowered.api.network.ProtocolVersion;
|
||||
import com.velocitypowered.api.proxy.crypto.IdentifiedKey;
|
||||
import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier;
|
||||
import com.velocitypowered.api.util.GameProfile;
|
||||
import com.velocitypowered.proxy.VelocityServer;
|
||||
@ -47,6 +49,7 @@ import javax.crypto.Mac;
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||
|
||||
public class LoginSessionHandler implements MinecraftSessionHandler {
|
||||
|
||||
@ -78,7 +81,7 @@ public class LoginSessionHandler implements MinecraftSessionHandler {
|
||||
&& packet.getChannel().equals(VelocityConstants.VELOCITY_IP_FORWARDING_CHANNEL)) {
|
||||
ByteBuf forwardingData = createForwardingData(configuration.getForwardingSecret(),
|
||||
serverConn.getPlayerRemoteAddressAsString(),
|
||||
serverConn.getPlayer().getGameProfile());
|
||||
serverConn.getPlayer().getGameProfile(), mc.getProtocolVersion(), serverConn.getPlayer().getIdentifiedKey());
|
||||
LoginPluginResponse response = new LoginPluginResponse(packet.getId(), true, forwardingData);
|
||||
mc.write(response);
|
||||
informationForwarded = true;
|
||||
@ -163,15 +166,23 @@ public class LoginSessionHandler implements MinecraftSessionHandler {
|
||||
}
|
||||
|
||||
private static ByteBuf createForwardingData(byte[] hmacSecret, String address,
|
||||
GameProfile profile) {
|
||||
GameProfile profile, ProtocolVersion version,
|
||||
@Nullable IdentifiedKey playerKey) {
|
||||
ByteBuf forwarded = Unpooled.buffer(2048);
|
||||
try {
|
||||
ProtocolUtils.writeVarInt(forwarded, VelocityConstants.FORWARDING_VERSION);
|
||||
int forwardingVersion = version.compareTo(ProtocolVersion.MINECRAFT_1_19) >= 0 && playerKey != null
|
||||
? VelocityConstants.MODERN_FORWARDING_WITH_KEY : VelocityConstants.MODERN_FORWARDING_DEFAULT;
|
||||
|
||||
ProtocolUtils.writeVarInt(forwarded, forwardingVersion);
|
||||
ProtocolUtils.writeString(forwarded, address);
|
||||
ProtocolUtils.writeUuid(forwarded, profile.getId());
|
||||
ProtocolUtils.writeString(forwarded, profile.getName());
|
||||
ProtocolUtils.writeProperties(forwarded, profile.getProperties());
|
||||
|
||||
if (forwardingVersion >= VelocityConstants.MODERN_FORWARDING_WITH_KEY) {
|
||||
ProtocolUtils.writePlayerKey(forwarded, playerKey);
|
||||
}
|
||||
|
||||
SecretKey key = new SecretKeySpec(hmacSecret, "HmacSHA256");
|
||||
Mac mac = Mac.getInstance("HmacSHA256");
|
||||
mac.init(key);
|
||||
|
@ -190,7 +190,7 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation,
|
||||
|
||||
mc.setProtocolVersion(protocolVersion);
|
||||
mc.setState(StateRegistry.LOGIN);
|
||||
mc.delayedWrite(new ServerLogin(proxyPlayer.getUsername()));
|
||||
mc.delayedWrite(new ServerLogin(proxyPlayer.getUsername(), proxyPlayer.getIdentifiedKey()));
|
||||
mc.flush();
|
||||
}
|
||||
|
||||
|
@ -85,7 +85,7 @@ public class AuthSessionHandler implements MinecraftSessionHandler {
|
||||
|
||||
// Initiate a regular connection and move over to it.
|
||||
ConnectedPlayer player = new ConnectedPlayer(server, profileEvent.getGameProfile(),
|
||||
mcConnection, inbound.getVirtualHost().orElse(null), onlineMode);
|
||||
mcConnection, inbound.getVirtualHost().orElse(null), onlineMode, inbound.getIdentifiedKey());
|
||||
this.connectedPlayer = player;
|
||||
if (!server.canRegisterConnection(player)) {
|
||||
player.disconnect0(Component.translatable("velocity.error.already-connected-proxy",
|
||||
@ -133,6 +133,7 @@ public class AuthSessionHandler implements MinecraftSessionHandler {
|
||||
}
|
||||
ServerLoginSuccess success = new ServerLoginSuccess();
|
||||
success.setUsername(player.getUsername());
|
||||
success.setProperties(player.getGameProfileProperties());
|
||||
success.setUuid(playerUniqueId);
|
||||
mcConnection.write(success);
|
||||
|
||||
|
@ -42,10 +42,11 @@ import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
|
||||
import com.velocitypowered.proxy.connection.backend.BackendConnectionPhases;
|
||||
import com.velocitypowered.proxy.connection.backend.BungeeCordMessageResponder;
|
||||
import com.velocitypowered.proxy.connection.backend.VelocityServerConnection;
|
||||
import com.velocitypowered.proxy.crypto.SignedChatCommand;
|
||||
import com.velocitypowered.proxy.crypto.SignedChatMessage;
|
||||
import com.velocitypowered.proxy.protocol.MinecraftPacket;
|
||||
import com.velocitypowered.proxy.protocol.StateRegistry;
|
||||
import com.velocitypowered.proxy.protocol.packet.BossBar;
|
||||
import com.velocitypowered.proxy.protocol.packet.Chat;
|
||||
import com.velocitypowered.proxy.protocol.packet.ClientSettings;
|
||||
import com.velocitypowered.proxy.protocol.packet.JoinGame;
|
||||
import com.velocitypowered.proxy.protocol.packet.KeepAlive;
|
||||
@ -55,6 +56,10 @@ import com.velocitypowered.proxy.protocol.packet.Respawn;
|
||||
import com.velocitypowered.proxy.protocol.packet.TabCompleteRequest;
|
||||
import com.velocitypowered.proxy.protocol.packet.TabCompleteResponse;
|
||||
import com.velocitypowered.proxy.protocol.packet.TabCompleteResponse.Offer;
|
||||
import com.velocitypowered.proxy.protocol.packet.chat.ChatBuilder;
|
||||
import com.velocitypowered.proxy.protocol.packet.chat.LegacyChat;
|
||||
import com.velocitypowered.proxy.protocol.packet.chat.PlayerChat;
|
||||
import com.velocitypowered.proxy.protocol.packet.chat.PlayerCommand;
|
||||
import com.velocitypowered.proxy.protocol.packet.title.GenericTitlePacket;
|
||||
import com.velocitypowered.proxy.protocol.util.PluginMessageUtil;
|
||||
import com.velocitypowered.proxy.util.CharacterUtil;
|
||||
@ -62,6 +67,8 @@ import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.ByteBufUtil;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import io.netty.util.ReferenceCountUtil;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
@ -90,9 +97,11 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
|
||||
private final Queue<PluginMessage> loginPluginMessages = new ConcurrentLinkedQueue<>();
|
||||
private final VelocityServer server;
|
||||
private @Nullable TabCompleteRequest outstandingTabComplete;
|
||||
private @Nullable Instant lastChatMessage; // Added in 1.19
|
||||
|
||||
/**
|
||||
* Constructs a client play session handler.
|
||||
*
|
||||
* @param server the Velocity server instance
|
||||
* @param player the player
|
||||
*/
|
||||
@ -101,6 +110,83 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
|
||||
this.server = server;
|
||||
}
|
||||
|
||||
// I will not allow hacks to bypass this;
|
||||
private boolean tickLastMessage(SignedChatMessage nextMessage) {
|
||||
if (lastChatMessage != null && lastChatMessage.isAfter(nextMessage.getExpiryTemporal())) {
|
||||
player.disconnect(Component.translatable("multiplayer.disconnect.out_of_order_chat"));
|
||||
return false;
|
||||
}
|
||||
|
||||
lastChatMessage = nextMessage.getExpiryTemporal();
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean validateChat(String message) {
|
||||
if (CharacterUtil.containsIllegalCharacters(message)) {
|
||||
player.disconnect(Component.translatable("velocity.error.illegal-chat-characters",
|
||||
NamedTextColor.RED));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private MinecraftConnection retrieveServerConnection() {
|
||||
VelocityServerConnection serverConnection = player.getConnectedServer();
|
||||
if (serverConnection == null) {
|
||||
return null;
|
||||
}
|
||||
return serverConnection.getConnection();
|
||||
}
|
||||
|
||||
private void processCommandMessage(String message, @Nullable SignedChatCommand signedCommand,
|
||||
MinecraftPacket original) {
|
||||
server.getCommandManager().callCommandEvent(player, message)
|
||||
.thenComposeAsync(event -> processCommandExecuteResult(message,
|
||||
event.getResult(), signedCommand))
|
||||
.whenComplete((ignored, throwable) -> {
|
||||
if (server.getConfiguration().isLogCommandExecutions()) {
|
||||
logger.info("{} -> executed command /{}", player, message);
|
||||
}
|
||||
})
|
||||
.exceptionally(e -> {
|
||||
logger.info("Exception occurred while running command for {}",
|
||||
player.getUsername(), e);
|
||||
player.sendMessage(Component.translatable("velocity.command.generic-error",
|
||||
NamedTextColor.RED));
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
private void processPlayerChat(String message, @Nullable SignedChatMessage signedMessage,
|
||||
MinecraftPacket original) {
|
||||
MinecraftConnection smc = retrieveServerConnection();
|
||||
if (smc == null) {
|
||||
return;
|
||||
}
|
||||
PlayerChatEvent event = new PlayerChatEvent(player, message);
|
||||
server.getEventManager().fire(event)
|
||||
.thenAcceptAsync(pme -> {
|
||||
PlayerChatEvent.ChatResult chatResult = pme.getResult();
|
||||
if (chatResult.isAllowed()) {
|
||||
Optional<String> eventMsg = pme.getResult().getMessage();
|
||||
if (eventMsg.isPresent()) {
|
||||
if (player.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_19) >= 0
|
||||
&& player.getIdentifiedKey() != null) {
|
||||
logger.warn("A plugin changed a signed chat message. The server may not accept it.");
|
||||
}
|
||||
smc.write(ChatBuilder.builder(player.getProtocolVersion())
|
||||
.message(event.getMessage()).toServer());
|
||||
} else {
|
||||
smc.write(original);
|
||||
}
|
||||
}
|
||||
}, smc.eventLoop())
|
||||
.exceptionally((ex) -> {
|
||||
logger.error("Exception while handling player chat for {}", player, ex);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void activated() {
|
||||
Collection<String> channels = server.getChannelRegistrar().getChannelsForProtocol(player
|
||||
@ -142,58 +228,57 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean handle(Chat packet) {
|
||||
VelocityServerConnection serverConnection = player.getConnectedServer();
|
||||
if (serverConnection == null) {
|
||||
public boolean handle(PlayerCommand packet) {
|
||||
if (!validateChat(packet.getCommand())) {
|
||||
return true;
|
||||
}
|
||||
MinecraftConnection smc = serverConnection.getConnection();
|
||||
if (smc == null) {
|
||||
if (!packet.isUnsigned()) {
|
||||
SignedChatCommand signedCommand = packet.signedContainer(player.getIdentifiedKey(), player.getUniqueId(), false);
|
||||
if (signedCommand != null) {
|
||||
processCommandMessage(packet.getCommand(), signedCommand, packet);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
processCommandMessage(packet.getCommand(), null, packet);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean handle(PlayerChat packet) {
|
||||
if (!validateChat(packet.getMessage())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!packet.isUnsigned()) {
|
||||
// Bad if spoofed
|
||||
SignedChatMessage signedChat = packet.signedContainer(player.getIdentifiedKey(), player.getUniqueId(), false);
|
||||
if (signedChat != null) {
|
||||
// Server doesn't care for expiry as long as order is correct
|
||||
if (!tickLastMessage(signedChat)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
processPlayerChat(packet.getMessage(), signedChat, packet);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
processPlayerChat(packet.getMessage(), null, packet);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean handle(LegacyChat packet) {
|
||||
String msg = packet.getMessage();
|
||||
if (CharacterUtil.containsIllegalCharacters(msg)) {
|
||||
player.disconnect(Component.translatable("velocity.error.illegal-chat-characters",
|
||||
NamedTextColor.RED));
|
||||
if (!validateChat(msg)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (msg.startsWith("/")) {
|
||||
String originalCommand = msg.substring(1);
|
||||
server.getCommandManager().callCommandEvent(player, msg.substring(1))
|
||||
.thenComposeAsync(event -> processCommandExecuteResult(originalCommand,
|
||||
event.getResult()))
|
||||
.whenComplete((ignored, throwable) -> {
|
||||
if (server.getConfiguration().isLogCommandExecutions()) {
|
||||
logger.info("{} -> executed command /{}", player, originalCommand);
|
||||
}
|
||||
})
|
||||
.exceptionally(e -> {
|
||||
logger.info("Exception occurred while running command for {}",
|
||||
player.getUsername(), e);
|
||||
player.sendMessage(Component.translatable("velocity.command.generic-error",
|
||||
NamedTextColor.RED));
|
||||
return null;
|
||||
});
|
||||
processCommandMessage(msg.substring(1), null, packet);
|
||||
} else {
|
||||
PlayerChatEvent event = new PlayerChatEvent(player, msg);
|
||||
server.getEventManager().fire(event)
|
||||
.thenAcceptAsync(pme -> {
|
||||
PlayerChatEvent.ChatResult chatResult = pme.getResult();
|
||||
if (chatResult.isAllowed()) {
|
||||
Optional<String> eventMsg = pme.getResult().getMessage();
|
||||
if (eventMsg.isPresent()) {
|
||||
smc.write(Chat.createServerbound(eventMsg.get()));
|
||||
} else {
|
||||
smc.write(packet);
|
||||
}
|
||||
}
|
||||
}, smc.eventLoop())
|
||||
.exceptionally((ex) -> {
|
||||
logger.error("Exception while handling player chat for {}", player, ex);
|
||||
return null;
|
||||
});
|
||||
processPlayerChat(msg, null, packet);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@ -229,7 +314,7 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
|
||||
}
|
||||
}
|
||||
server.getEventManager().fireAndForget(new PlayerChannelRegisterEvent(player,
|
||||
ImmutableList.copyOf(channelIdentifiers)));
|
||||
ImmutableList.copyOf(channelIdentifiers)));
|
||||
backendConn.write(packet.retain());
|
||||
} else if (PluginMessageUtil.isUnregister(packet)) {
|
||||
player.getKnownChannels().removeAll(PluginMessageUtil.getChannels(packet));
|
||||
@ -368,7 +453,8 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
|
||||
/**
|
||||
* Handles the {@code JoinGame} packet. This function is responsible for handling the client-side
|
||||
* switching servers in Velocity.
|
||||
* @param joinGame the join game packet
|
||||
*
|
||||
* @param joinGame the join game packet
|
||||
* @param destination the new server we are connecting to
|
||||
*/
|
||||
public void handleBackendJoinGame(JoinGame joinGame, VelocityServerConnection destination) {
|
||||
@ -452,7 +538,7 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
|
||||
new Respawn(sentOldDim, joinGame.getPartialHashedSeed(),
|
||||
joinGame.getDifficulty(), joinGame.getGamemode(), joinGame.getLevelType(),
|
||||
false, joinGame.getDimensionInfo(), joinGame.getPreviousGamemode(),
|
||||
joinGame.getCurrentDimensionData()));
|
||||
joinGame.getCurrentDimensionData(), joinGame.getLastDeathPosition()));
|
||||
}
|
||||
|
||||
private void doSafeClientServerSwitch(JoinGame joinGame) {
|
||||
@ -469,14 +555,14 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
|
||||
new Respawn(tempDim, joinGame.getPartialHashedSeed(), joinGame.getDifficulty(),
|
||||
joinGame.getGamemode(), joinGame.getLevelType(),
|
||||
false, joinGame.getDimensionInfo(), joinGame.getPreviousGamemode(),
|
||||
joinGame.getCurrentDimensionData()));
|
||||
joinGame.getCurrentDimensionData(), joinGame.getLastDeathPosition()));
|
||||
|
||||
// Now send a respawn packet in the correct dimension.
|
||||
player.getConnection().delayedWrite(
|
||||
new Respawn(joinGame.getDimension(), joinGame.getPartialHashedSeed(),
|
||||
joinGame.getDifficulty(), joinGame.getGamemode(), joinGame.getLevelType(),
|
||||
false, joinGame.getDimensionInfo(), joinGame.getPreviousGamemode(),
|
||||
joinGame.getCurrentDimensionData()));
|
||||
joinGame.getCurrentDimensionData(), joinGame.getLastDeathPosition()));
|
||||
}
|
||||
|
||||
public List<UUID> getServerBossBars() {
|
||||
@ -619,8 +705,10 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
private CompletableFuture<Void> processCommandExecuteResult(String originalCommand,
|
||||
CommandResult result) {
|
||||
CommandResult result,
|
||||
@Nullable SignedChatCommand signedCommand) {
|
||||
if (result == CommandResult.denied()) {
|
||||
return CompletableFuture.completedFuture(null);
|
||||
}
|
||||
@ -628,13 +716,30 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
|
||||
MinecraftConnection smc = player.ensureAndGetCurrentServer().ensureConnected();
|
||||
String commandToRun = result.getCommand().orElse(originalCommand);
|
||||
if (result.isForwardToServer()) {
|
||||
return CompletableFuture.runAsync(() -> smc.write(Chat.createServerbound("/"
|
||||
+ commandToRun)), smc.eventLoop());
|
||||
ChatBuilder write = ChatBuilder
|
||||
.builder(player.getProtocolVersion())
|
||||
.asPlayer(player);
|
||||
|
||||
if (signedCommand != null && commandToRun.equals(signedCommand.getBaseCommand())) {
|
||||
write.message(signedCommand);
|
||||
} else {
|
||||
write.message("/" + commandToRun);
|
||||
}
|
||||
return CompletableFuture.runAsync(() -> smc.write(write.toServer()), smc.eventLoop());
|
||||
} else {
|
||||
return server.getCommandManager().executeImmediatelyAsync(player, commandToRun)
|
||||
.thenAcceptAsync(hasRun -> {
|
||||
if (!hasRun) {
|
||||
smc.write(Chat.createServerbound("/" + commandToRun));
|
||||
ChatBuilder write = ChatBuilder
|
||||
.builder(player.getProtocolVersion())
|
||||
.asPlayer(player);
|
||||
|
||||
if (signedCommand != null && commandToRun.equals(signedCommand.getBaseCommand())) {
|
||||
write.message(signedCommand);
|
||||
} else {
|
||||
write.message("/" + commandToRun);
|
||||
}
|
||||
smc.write(write.toServer());
|
||||
}
|
||||
}, smc.eventLoop());
|
||||
}
|
||||
|
@ -41,6 +41,8 @@ import com.velocitypowered.api.permission.Tristate;
|
||||
import com.velocitypowered.api.proxy.ConnectionRequestBuilder;
|
||||
import com.velocitypowered.api.proxy.Player;
|
||||
import com.velocitypowered.api.proxy.ServerConnection;
|
||||
import com.velocitypowered.api.proxy.crypto.IdentifiedKey;
|
||||
import com.velocitypowered.api.proxy.crypto.KeyIdentifiable;
|
||||
import com.velocitypowered.api.proxy.messages.ChannelIdentifier;
|
||||
import com.velocitypowered.api.proxy.player.PlayerSettings;
|
||||
import com.velocitypowered.api.proxy.player.ResourcePackInfo;
|
||||
@ -56,13 +58,14 @@ import com.velocitypowered.proxy.connection.util.ConnectionMessages;
|
||||
import com.velocitypowered.proxy.connection.util.ConnectionRequestResults.Impl;
|
||||
import com.velocitypowered.proxy.protocol.ProtocolUtils;
|
||||
import com.velocitypowered.proxy.protocol.StateRegistry;
|
||||
import com.velocitypowered.proxy.protocol.packet.Chat;
|
||||
import com.velocitypowered.proxy.protocol.packet.ClientSettings;
|
||||
import com.velocitypowered.proxy.protocol.packet.Disconnect;
|
||||
import com.velocitypowered.proxy.protocol.packet.HeaderAndFooter;
|
||||
import com.velocitypowered.proxy.protocol.packet.KeepAlive;
|
||||
import com.velocitypowered.proxy.protocol.packet.PluginMessage;
|
||||
import com.velocitypowered.proxy.protocol.packet.ResourcePackRequest;
|
||||
import com.velocitypowered.proxy.protocol.packet.chat.ChatBuilder;
|
||||
import com.velocitypowered.proxy.protocol.packet.chat.LegacyChat;
|
||||
import com.velocitypowered.proxy.protocol.packet.title.GenericTitlePacket;
|
||||
import com.velocitypowered.proxy.server.VelocityRegisteredServer;
|
||||
import com.velocitypowered.proxy.tablist.VelocityTabList;
|
||||
@ -109,7 +112,7 @@ 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 {
|
||||
public class ConnectedPlayer implements MinecraftConnectionAssociation, Player, KeyIdentifiable {
|
||||
|
||||
private static final int MAX_PLUGIN_CHANNELS = 1024;
|
||||
private static final PlainTextComponentSerializer PASS_THRU_TRANSLATE = PlainTextComponentSerializer.builder()
|
||||
@ -159,9 +162,10 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
|
||||
.build();
|
||||
private @Nullable String clientBrand;
|
||||
private @Nullable Locale effectiveLocale;
|
||||
private @Nullable IdentifiedKey playerKey;
|
||||
|
||||
ConnectedPlayer(VelocityServer server, GameProfile profile, MinecraftConnection connection,
|
||||
@Nullable InetSocketAddress virtualHost, boolean onlineMode) {
|
||||
@Nullable InetSocketAddress virtualHost, boolean onlineMode, @Nullable IdentifiedKey playerKey) {
|
||||
this.server = server;
|
||||
this.profile = profile;
|
||||
this.connection = connection;
|
||||
@ -176,6 +180,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
|
||||
} else {
|
||||
this.tabList = new VelocityTabListLegacy(this);
|
||||
}
|
||||
this.playerKey = playerKey;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -311,7 +316,9 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
|
||||
@Override
|
||||
public void sendMessage(@NonNull Identity identity, @NonNull Component message) {
|
||||
Component translated = translateMessage(message);
|
||||
connection.write(Chat.createClientbound(identity, translated, this.getProtocolVersion()));
|
||||
|
||||
connection.write(ChatBuilder.builder(this.getProtocolVersion())
|
||||
.component(translated).forIdentity(identity).toClient());
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -321,9 +328,11 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
|
||||
Preconditions.checkNotNull(type, "type");
|
||||
|
||||
Component translated = translateMessage(message);
|
||||
Chat packet = Chat.createClientbound(identity, translated, this.getProtocolVersion());
|
||||
packet.setType(type == MessageType.CHAT ? Chat.CHAT_TYPE : Chat.SYSTEM_TYPE);
|
||||
connection.write(packet);
|
||||
|
||||
connection.write(ChatBuilder.builder(this.getProtocolVersion())
|
||||
.component(translated).forIdentity(identity)
|
||||
.setType(type == MessageType.CHAT ? ChatBuilder.ChatType.CHAT : ChatBuilder.ChatType.SYSTEM)
|
||||
.toClient());
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -344,10 +353,10 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
|
||||
JsonObject object = new JsonObject();
|
||||
object.addProperty("text", LegacyComponentSerializer.legacySection()
|
||||
.serialize(translated));
|
||||
Chat chat = new Chat();
|
||||
chat.setMessage(object.toString());
|
||||
chat.setType(Chat.GAME_INFO_TYPE);
|
||||
connection.write(chat);
|
||||
LegacyChat legacyChat = new LegacyChat();
|
||||
legacyChat.setMessage(object.toString());
|
||||
legacyChat.setType(LegacyChat.GAME_INFO_TYPE);
|
||||
connection.write(legacyChat);
|
||||
}
|
||||
}
|
||||
|
||||
@ -883,10 +892,11 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
|
||||
|
||||
@Override
|
||||
public void spoofChatInput(String input) {
|
||||
Preconditions.checkArgument(input.length() <= Chat.MAX_SERVERBOUND_MESSAGE_LENGTH,
|
||||
"input cannot be greater than " + Chat.MAX_SERVERBOUND_MESSAGE_LENGTH
|
||||
Preconditions.checkArgument(input.length() <= LegacyChat.MAX_SERVERBOUND_MESSAGE_LENGTH,
|
||||
"input cannot be greater than " + LegacyChat.MAX_SERVERBOUND_MESSAGE_LENGTH
|
||||
+ " characters in length");
|
||||
ensureBackendConnection().write(Chat.createServerbound(input));
|
||||
ensureBackendConnection().write(ChatBuilder.builder(getProtocolVersion())
|
||||
.asPlayer(this).message(input).toServer());
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -1055,6 +1065,11 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
|
||||
return knownChannels;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IdentifiedKey getIdentifiedKey() {
|
||||
return playerKey;
|
||||
}
|
||||
|
||||
private class IdentityImpl implements Identity {
|
||||
@Override
|
||||
public @NonNull UUID uuid() {
|
||||
|
@ -20,12 +20,15 @@ package com.velocitypowered.proxy.connection.client;
|
||||
import static com.google.common.net.UrlEscapers.urlFormParameterEscaper;
|
||||
import static com.velocitypowered.proxy.VelocityServer.GENERAL_GSON;
|
||||
import static com.velocitypowered.proxy.connection.VelocityConstants.EMPTY_BYTE_ARRAY;
|
||||
import static com.velocitypowered.proxy.util.EncryptionUtils.decryptRsa;
|
||||
import static com.velocitypowered.proxy.util.EncryptionUtils.generateServerId;
|
||||
import static com.velocitypowered.proxy.crypto.EncryptionUtils.decryptRsa;
|
||||
import static com.velocitypowered.proxy.crypto.EncryptionUtils.generateServerId;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.primitives.Longs;
|
||||
import com.velocitypowered.api.event.connection.PreLoginEvent;
|
||||
import com.velocitypowered.api.event.connection.PreLoginEvent.PreLoginComponentResult;
|
||||
import com.velocitypowered.api.network.ProtocolVersion;
|
||||
import com.velocitypowered.api.proxy.crypto.IdentifiedKey;
|
||||
import com.velocitypowered.api.util.GameProfile;
|
||||
import com.velocitypowered.proxy.VelocityServer;
|
||||
import com.velocitypowered.proxy.connection.MinecraftConnection;
|
||||
@ -77,6 +80,23 @@ public class InitialLoginSessionHandler implements MinecraftSessionHandler {
|
||||
public boolean handle(ServerLogin packet) {
|
||||
assertState(LoginState.LOGIN_PACKET_EXPECTED);
|
||||
this.currentState = LoginState.LOGIN_PACKET_RECEIVED;
|
||||
IdentifiedKey playerKey = packet.getPlayerKey();
|
||||
if (playerKey != null) {
|
||||
if (playerKey.hasExpired()) {
|
||||
inbound.disconnect(Component.translatable("multiplayer.disconnect.invalid_public_key_signature"));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!playerKey.isSignatureValid()) {
|
||||
inbound.disconnect(Component.translatable("multiplayer.disconnect.invalid_public_key"));
|
||||
return true;
|
||||
}
|
||||
} else if (mcConnection.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_19) >= 0
|
||||
&& server.getConfiguration().isForceKeyAuthentication()) {
|
||||
inbound.disconnect(Component.translatable("multiplayer.disconnect.missing_public_key"));
|
||||
return true;
|
||||
}
|
||||
inbound.setPlayerKey(playerKey);
|
||||
this.login = packet;
|
||||
|
||||
PreLoginEvent event = new PreLoginEvent(inbound, login.getUsername());
|
||||
@ -135,7 +155,6 @@ public class InitialLoginSessionHandler implements MinecraftSessionHandler {
|
||||
public boolean handle(EncryptionResponse packet) {
|
||||
assertState(LoginState.ENCRYPTION_REQUEST_SENT);
|
||||
this.currentState = LoginState.ENCRYPTION_RESPONSE_RECEIVED;
|
||||
|
||||
ServerLogin login = this.login;
|
||||
if (login == null) {
|
||||
throw new IllegalStateException("No ServerLogin packet received yet.");
|
||||
@ -147,9 +166,16 @@ public class InitialLoginSessionHandler implements MinecraftSessionHandler {
|
||||
|
||||
try {
|
||||
KeyPair serverKeyPair = server.getServerKeyPair();
|
||||
byte[] decryptedVerifyToken = decryptRsa(serverKeyPair, packet.getVerifyToken());
|
||||
if (!MessageDigest.isEqual(verify, decryptedVerifyToken)) {
|
||||
throw new IllegalStateException("Unable to successfully decrypt the verification token.");
|
||||
if (inbound.getIdentifiedKey() != null) {
|
||||
IdentifiedKey playerKey = inbound.getIdentifiedKey();
|
||||
if (!playerKey.verifyDataSignature(packet.getVerifyToken(), verify, Longs.toByteArray(packet.getSalt()))) {
|
||||
throw new IllegalStateException("Invalid client public signature.");
|
||||
}
|
||||
} else {
|
||||
byte[] decryptedVerifyToken = decryptRsa(serverKeyPair, packet.getVerifyToken());
|
||||
if (!MessageDigest.isEqual(verify, decryptedVerifyToken)) {
|
||||
throw new IllegalStateException("Unable to successfully decrypt the verification token.");
|
||||
}
|
||||
}
|
||||
|
||||
byte[] decryptedSharedSecret = decryptRsa(serverKeyPair, packet.getSharedSecret());
|
||||
|
@ -19,6 +19,8 @@ package com.velocitypowered.proxy.connection.client;
|
||||
|
||||
import com.velocitypowered.api.network.ProtocolVersion;
|
||||
import com.velocitypowered.api.proxy.LoginPhaseConnection;
|
||||
import com.velocitypowered.api.proxy.crypto.IdentifiedKey;
|
||||
import com.velocitypowered.api.proxy.crypto.KeyIdentifiable;
|
||||
import com.velocitypowered.api.proxy.messages.ChannelIdentifier;
|
||||
import com.velocitypowered.proxy.connection.MinecraftConnection;
|
||||
import com.velocitypowered.proxy.protocol.packet.LoginPluginMessage;
|
||||
@ -32,9 +34,10 @@ import java.util.Optional;
|
||||
import java.util.Queue;
|
||||
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
import space.vectrix.flare.fastutil.Int2ObjectSyncMap;
|
||||
|
||||
public class LoginInboundConnection implements LoginPhaseConnection {
|
||||
public class LoginInboundConnection implements LoginPhaseConnection, KeyIdentifiable {
|
||||
|
||||
private static final AtomicIntegerFieldUpdater<LoginInboundConnection> SEQUENCE_UPDATER =
|
||||
AtomicIntegerFieldUpdater.newUpdater(LoginInboundConnection.class, "sequenceCounter");
|
||||
@ -45,6 +48,7 @@ public class LoginInboundConnection implements LoginPhaseConnection {
|
||||
private final Queue<LoginPluginMessage> loginMessagesToSend;
|
||||
private volatile Runnable onAllMessagesHandled;
|
||||
private volatile boolean loginEventFired;
|
||||
private @MonotonicNonNull IdentifiedKey playerKey;
|
||||
|
||||
LoginInboundConnection(
|
||||
InitialInboundConnection delegate) {
|
||||
@ -149,4 +153,13 @@ public class LoginInboundConnection implements LoginPhaseConnection {
|
||||
MinecraftConnection delegatedConnection() {
|
||||
return delegate.getConnection();
|
||||
}
|
||||
|
||||
public void setPlayerKey(IdentifiedKey playerKey) {
|
||||
this.playerKey = playerKey;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IdentifiedKey getIdentifiedKey() {
|
||||
return playerKey;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,109 @@
|
||||
/*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.velocitypowered.proxy.connection.registry;
|
||||
|
||||
/*
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import net.kyori.adventure.nbt.*;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import net.kyori.adventure.text.format.TextDecoration;
|
||||
import net.kyori.adventure.text.format.TextFormat;
|
||||
import net.kyori.adventure.translation.Translatable;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
*/
|
||||
|
||||
// TODO Implement
|
||||
public class ChatData {
|
||||
/*
|
||||
private static final ListBinaryTag EMPTY_LIST_TAG = ListBinaryTag.empty();
|
||||
|
||||
|
||||
private final String identifier;
|
||||
private final int id;
|
||||
private final Map<>
|
||||
|
||||
|
||||
public static class Decoration implements Translatable {
|
||||
|
||||
private final List<String> parameters;
|
||||
private final List<TextFormat> style;
|
||||
private final String translationKey;
|
||||
|
||||
public List<String> getParameters() {
|
||||
return parameters;
|
||||
}
|
||||
|
||||
public List<TextFormat> getStyle() {
|
||||
return style;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull String translationKey() {
|
||||
return translationKey;
|
||||
}
|
||||
|
||||
public Decoration(List<String> parameters, List<TextFormat> style, String translationKey) {
|
||||
this.parameters = Preconditions.checkNotNull(parameters);
|
||||
this.style = Preconditions.checkNotNull(style);
|
||||
this.translationKey = Preconditions.checkNotNull(translationKey);
|
||||
Preconditions.checkArgument(translationKey.length() > 0);
|
||||
}
|
||||
|
||||
public static Decoration decodeRegistryEntry(CompoundBinaryTag toDecode) {
|
||||
ImmutableList.Builder<String> parameters = ImmutableList.builder();
|
||||
ListBinaryTag paramList = toDecode.getList("parameters", EMPTY_LIST_TAG);
|
||||
if (paramList != EMPTY_LIST_TAG) {
|
||||
paramList.forEach(binaryTag -> parameters.add(binaryTag.toString()));
|
||||
}
|
||||
|
||||
ImmutableList.Builder<TextFormat> style = ImmutableList.builder();
|
||||
CompoundBinaryTag styleList = toDecode.getCompound("style");
|
||||
for (String key : styleList.keySet()) {
|
||||
if ("color".equals(key)) {
|
||||
NamedTextColor color = Preconditions.checkNotNull(
|
||||
NamedTextColor.NAMES.value(styleList.getString(key)));
|
||||
style.add(color);
|
||||
} else {
|
||||
// Key is a Style option instead
|
||||
TextDecoration deco = TextDecoration.NAMES.value(key);
|
||||
// This wouldn't be here if it wasn't applied, but here it goes anyway:
|
||||
byte val = styleList.getByte(key);
|
||||
if (val != 0) {
|
||||
style.add(deco);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String translationKey = toDecode.getString("translation_key");
|
||||
|
||||
return new Decoration(parameters.build(), style.build(), translationKey);
|
||||
}
|
||||
|
||||
public void encodeRegistryEntry(CompoundBinaryTag )
|
||||
|
||||
}
|
||||
|
||||
public static enum Priority {
|
||||
SYSTEM,
|
||||
CHAT
|
||||
}
|
||||
*/
|
||||
}
|
@ -45,42 +45,51 @@ public final class DimensionData {
|
||||
private final @Nullable String effects;
|
||||
private final @Nullable Integer minY; // Required and added by 1.17
|
||||
private final @Nullable Integer height; // Required and added by 1.17
|
||||
private final @Nullable Integer monsterSpawnBlockLightLimit; // Required and added by 1.19
|
||||
private final @Nullable Integer monsterSpawnLightLevel; // Required and added by 1.19
|
||||
|
||||
/**
|
||||
* Initializes a new {@link DimensionData} instance.
|
||||
* @param registryIdentifier the identifier for the dimension from the registry.
|
||||
* @param dimensionId the dimension ID contained in the registry (the "id" tag)
|
||||
* @param isNatural indicates if the dimension use natural world generation (e.g. overworld)
|
||||
* @param ambientLight the light level the client sees without external lighting
|
||||
* @param isShrunk indicates if the world is shrunk, aka not the full 256 blocks (e.g. nether)
|
||||
* @param isUltrawarm internal dimension warmth flag
|
||||
* @param hasCeiling indicates if the dimension has a ceiling layer
|
||||
* @param hasSkylight indicates if the dimension should display the sun
|
||||
* @param isPiglinSafe indicates if piglins should naturally zombify in this dimension
|
||||
* @param doBedsWork indicates if players should be able to sleep in beds in this dimension
|
||||
* @param doRespawnAnchorsWork indicates if player respawn points can be used in this dimension
|
||||
* @param hasRaids indicates if raids can be spawned in the dimension
|
||||
* @param logicalHeight the natural max height for the given dimension
|
||||
* @param burningBehaviourIdentifier the identifier for how burning blocks work in the dimension
|
||||
* @param fixedTime optional. If set to any game daytime value will deactivate time cycle
|
||||
* @param createDragonFight optional. Internal flag used in the end dimension
|
||||
* @param coordinateScale optional, unknown purpose
|
||||
* @param effects optional, unknown purpose
|
||||
* @param minY the world effective lowest build-level
|
||||
* @param height the world height above zero
|
||||
*
|
||||
* @param registryIdentifier the identifier for the dimension from the registry.
|
||||
* @param dimensionId the dimension ID contained in the registry (the "id" tag)
|
||||
* @param isNatural indicates if the dimension use natural world generation (e.g. overworld)
|
||||
* @param ambientLight the light level the client sees without external lighting
|
||||
* @param isShrunk indicates if the world is shrunk, aka not the full 256 blocks (e.g. nether)
|
||||
* @param isUltrawarm internal dimension warmth flag
|
||||
* @param hasCeiling indicates if the dimension has a ceiling layer
|
||||
* @param hasSkylight indicates if the dimension should display the sun
|
||||
* @param isPiglinSafe indicates if piglins should naturally zombify in this dimension
|
||||
* @param doBedsWork indicates if players should be able to sleep in beds in this dimension
|
||||
* @param doRespawnAnchorsWork indicates if player respawn points can be used in this dimension
|
||||
* @param hasRaids indicates if raids can be spawned in the dimension
|
||||
* @param logicalHeight the natural max height for the given dimension
|
||||
* @param burningBehaviourIdentifier the identifier for how burning blocks work in the dimension
|
||||
* @param fixedTime optional. If set to any game daytime value will deactivate time cycle
|
||||
* @param createDragonFight optional. Internal flag used in the end dimension
|
||||
* @param coordinateScale optional, unknown purpose
|
||||
* @param effects optional, unknown purpose
|
||||
* @param minY the world effective lowest build-level
|
||||
* @param height the world height above zero
|
||||
* @param monsterSpawnBlockLightLimit an integer controlling the block light needed to prevent monster spawns.
|
||||
* @param monsterSpawnLightLevel an int provider which is evaluated to find a value to compare the current
|
||||
* overall brightness with to determine if a monster should be allowed to spawn.
|
||||
*/
|
||||
public DimensionData(String registryIdentifier,
|
||||
@Nullable Integer dimensionId,
|
||||
boolean isNatural,
|
||||
float ambientLight, boolean isShrunk, boolean isUltrawarm,
|
||||
boolean hasCeiling, boolean hasSkylight,
|
||||
boolean isPiglinSafe, boolean doBedsWork,
|
||||
boolean doRespawnAnchorsWork, boolean hasRaids,
|
||||
int logicalHeight, String burningBehaviourIdentifier,
|
||||
@Nullable Long fixedTime, @Nullable Boolean createDragonFight,
|
||||
@Nullable Double coordinateScale,
|
||||
@Nullable String effects,
|
||||
@Nullable Integer minY, @Nullable Integer height) {
|
||||
@Nullable Integer dimensionId,
|
||||
boolean isNatural,
|
||||
float ambientLight, boolean isShrunk, boolean isUltrawarm,
|
||||
boolean hasCeiling, boolean hasSkylight,
|
||||
boolean isPiglinSafe, boolean doBedsWork,
|
||||
boolean doRespawnAnchorsWork, boolean hasRaids,
|
||||
int logicalHeight, String burningBehaviourIdentifier,
|
||||
@Nullable Long fixedTime, @Nullable Boolean createDragonFight,
|
||||
@Nullable Double coordinateScale,
|
||||
@Nullable String effects,
|
||||
@Nullable Integer minY, @Nullable Integer height, @Nullable Integer monsterSpawnBlockLightLimit,
|
||||
@Nullable Integer monsterSpawnLightLevel) {
|
||||
this.monsterSpawnBlockLightLimit = monsterSpawnBlockLightLimit;
|
||||
this.monsterSpawnLightLevel = monsterSpawnLightLevel;
|
||||
Preconditions.checkNotNull(
|
||||
registryIdentifier, "registryIdentifier cannot be null");
|
||||
Preconditions.checkArgument(registryIdentifier.length() > 0,
|
||||
@ -193,15 +202,15 @@ public final class DimensionData {
|
||||
* and {@code dimensionId}.
|
||||
*
|
||||
* @param registryIdentifier the identifier for the dimension from the registry
|
||||
* @param dimensionId optional, dimension ID
|
||||
* @param dimensionId optional, dimension ID
|
||||
* @return a new {@link DimensionData}
|
||||
*/
|
||||
public DimensionData annotateWith(String registryIdentifier,
|
||||
@Nullable Integer dimensionId) {
|
||||
@Nullable Integer dimensionId) {
|
||||
return new DimensionData(registryIdentifier, dimensionId, isNatural, ambientLight, isShrunk,
|
||||
isUltrawarm, hasCeiling, hasSkylight, isPiglinSafe, doBedsWork, doRespawnAnchorsWork,
|
||||
hasRaids, logicalHeight, burningBehaviourIdentifier, fixedTime, createDragonFight,
|
||||
coordinateScale, effects, minY, height);
|
||||
coordinateScale, effects, minY, height, monsterSpawnBlockLightLimit, monsterSpawnLightLevel);
|
||||
}
|
||||
|
||||
public boolean isUnannotated() {
|
||||
@ -217,7 +226,7 @@ public final class DimensionData {
|
||||
* @return game dimension data
|
||||
*/
|
||||
public static DimensionData decodeBaseCompoundTag(CompoundBinaryTag details,
|
||||
ProtocolVersion version) {
|
||||
ProtocolVersion version) {
|
||||
boolean isNatural = details.getBoolean("natural");
|
||||
float ambientLight = details.getFloat("ambient_light");
|
||||
boolean isShrunk = details.getBoolean("shrunk");
|
||||
@ -240,28 +249,40 @@ public final class DimensionData {
|
||||
: null;
|
||||
Integer minY = details.keySet().contains("min_y") ? details.getInt("min_y") : null;
|
||||
Integer height = details.keySet().contains("height") ? details.getInt("height") : null;
|
||||
Integer monsterSpawnBlockLightLimit = details.keySet().contains("monster_spawn_block_light_limit")
|
||||
? details.getInt("monster_spawn_block_light_limit") : null;
|
||||
Integer monsterSpawnLightLevel =
|
||||
details.keySet().contains("monster_spawn_light_level") ? details.getInt("monster_spawn_block_light_limit") :
|
||||
null;
|
||||
if (version.compareTo(ProtocolVersion.MINECRAFT_1_17) >= 0) {
|
||||
Preconditions.checkNotNull(height,
|
||||
"DimensionData requires 'height' to be present for this version");
|
||||
"DimensionData requires 'height' to be present for this version");
|
||||
Preconditions.checkNotNull(minY,
|
||||
"DimensionData requires 'minY' to be present for this version");
|
||||
"DimensionData requires 'minY' to be present for this version");
|
||||
}
|
||||
if (version.compareTo(ProtocolVersion.MINECRAFT_1_19) >= 0) {
|
||||
Preconditions.checkNotNull(monsterSpawnBlockLightLimit,
|
||||
"DimensionData requires 'monster_spawn_block_light_limit' to be present for this version.");
|
||||
Preconditions.checkNotNull(monsterSpawnLightLevel,
|
||||
"DimensionData requires 'monster_spawn_light_level' to be present for this version.");
|
||||
}
|
||||
return new DimensionData(
|
||||
UNKNOWN_DIMENSION_ID, null, isNatural, ambientLight, isShrunk,
|
||||
isUltrawarm, hasCeiling, hasSkylight, isPiglinSafe, doBedsWork, doRespawnAnchorsWork,
|
||||
hasRaids, logicalHeight, burningBehaviourIdentifier, fixedTime, hasEnderdragonFight,
|
||||
coordinateScale, effects, minY, height);
|
||||
coordinateScale, effects, minY, height, monsterSpawnBlockLightLimit, monsterSpawnLightLevel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a given CompoundTag to a DimensionData instance. Assumes the data is part of a
|
||||
* dimension registry.
|
||||
* @param dimTag the compound from the registry to read
|
||||
*
|
||||
* @param dimTag the compound from the registry to read
|
||||
* @param version the protocol version
|
||||
* @return game dimension data
|
||||
*/
|
||||
public static DimensionData decodeRegistryEntry(CompoundBinaryTag dimTag,
|
||||
ProtocolVersion version) {
|
||||
ProtocolVersion version) {
|
||||
String registryIdentifier = dimTag.getString("name");
|
||||
CompoundBinaryTag details;
|
||||
Integer dimensionId = null;
|
||||
@ -278,6 +299,7 @@ public final class DimensionData {
|
||||
|
||||
/**
|
||||
* Encodes the Dimension data as CompoundTag.
|
||||
*
|
||||
* @param version the version to serialize as
|
||||
* @return compound containing the dimension data
|
||||
*/
|
||||
@ -301,6 +323,7 @@ public final class DimensionData {
|
||||
|
||||
/**
|
||||
* Serializes details of this dimension.
|
||||
*
|
||||
* @return serialized details of this dimension
|
||||
*/
|
||||
public CompoundBinaryTag serializeDimensionDetails() {
|
||||
@ -335,6 +358,12 @@ public final class DimensionData {
|
||||
if (height != null) {
|
||||
ret.putInt("height", height);
|
||||
}
|
||||
if (monsterSpawnBlockLightLimit != null) {
|
||||
ret.putInt("monster_spawn_block_light_limit", monsterSpawnBlockLightLimit);
|
||||
}
|
||||
if (monsterSpawnLightLevel != null) {
|
||||
ret.putInt("monster_spawn_light_level", monsterSpawnLightLevel);
|
||||
}
|
||||
return ret.build();
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,263 @@
|
||||
/*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.velocitypowered.proxy.crypto;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.io.ByteStreams;
|
||||
import com.velocitypowered.proxy.util.except.QuietDecoderException;
|
||||
import it.unimi.dsi.fastutil.Pair;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.math.BigInteger;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.Key;
|
||||
import java.security.KeyFactory;
|
||||
import java.security.KeyPair;
|
||||
import java.security.KeyPairGenerator;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.security.Signature;
|
||||
import java.security.spec.InvalidKeySpecException;
|
||||
import java.security.spec.X509EncodedKeySpec;
|
||||
import java.util.Base64;
|
||||
import javax.crypto.Cipher;
|
||||
|
||||
public enum EncryptionUtils {
|
||||
;
|
||||
|
||||
public static final Pair<String, String> PEM_RSA_PUBLIC_KEY_DESCRIPTOR =
|
||||
Pair.of("-----BEGIN RSA PUBLIC KEY-----", "-----END RSA PUBLIC KEY-----");
|
||||
public static final Pair<String, String> PEM_RSA_PRIVATE_KEY_DESCRIPTOR =
|
||||
Pair.of("-----BEGIN RSA PRIVATE KEY-----", "-----END RSA PRIVATE KEY-----");
|
||||
|
||||
public static final String SHA1_WITH_RSA = "SHA1withRSA";
|
||||
public static final String SHA256_WITH_RSA = "SHA256withRSA";
|
||||
|
||||
public static final QuietDecoderException INVALID_SIGNATURE
|
||||
= new QuietDecoderException("Incorrectly signed chat message");
|
||||
public static final QuietDecoderException PREVIEW_SIGNATURE_MISSING
|
||||
= new QuietDecoderException("Unsigned chat message requested signed preview");
|
||||
public static final byte[] EMPTY = new byte[0];
|
||||
private static PublicKey YGGDRASIL_SESSION_KEY;
|
||||
private static KeyFactory RSA_KEY_FACTORY;
|
||||
|
||||
private static final Base64.Encoder MIME_SPECIAL_ENCODER
|
||||
= Base64.getMimeEncoder(76, "\n".getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
static {
|
||||
try {
|
||||
RSA_KEY_FACTORY = KeyFactory.getInstance("RSA");
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
try {
|
||||
byte[] bytes = ByteStreams.toByteArray(
|
||||
EncryptionUtils.class.getClassLoader().getResourceAsStream("yggdrasil_session_pubkey.der"));
|
||||
YGGDRASIL_SESSION_KEY = parseRsaPublicKey(bytes);
|
||||
} catch (IOException | NullPointerException err) {
|
||||
throw new RuntimeException(err);
|
||||
}
|
||||
}
|
||||
|
||||
public static PublicKey getYggdrasilSessionKey() {
|
||||
return YGGDRASIL_SESSION_KEY;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies a key signature.
|
||||
*
|
||||
* @param algorithm the signature algorithm
|
||||
* @param base the public key to verify with
|
||||
* @param signature the signature to verify against
|
||||
* @param toVerify the byte array(s) of data to verify
|
||||
* @return validity of the signature
|
||||
*/
|
||||
public static boolean verifySignature(String algorithm, PublicKey base, byte[] signature, byte[]... toVerify) {
|
||||
Preconditions.checkArgument(toVerify.length > 0);
|
||||
try {
|
||||
Signature construct = Signature.getInstance(algorithm);
|
||||
construct.initVerify(base);
|
||||
for (byte[] bytes : toVerify) {
|
||||
construct.update(bytes);
|
||||
}
|
||||
return construct.verify(signature);
|
||||
} catch (GeneralSecurityException e) {
|
||||
throw new IllegalArgumentException("Invalid signature parameters");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a signature for input data.
|
||||
*
|
||||
* @param algorithm the signature algorithm
|
||||
* @param base the private key to sign with
|
||||
* @param toSign the byte array(s) of data to sign
|
||||
* @return the generated signature
|
||||
*/
|
||||
public static byte[] generateSignature(String algorithm, PrivateKey base, byte[]... toSign) {
|
||||
Preconditions.checkArgument(toSign.length > 0);
|
||||
try {
|
||||
Signature construct = Signature.getInstance(algorithm);
|
||||
construct.initSign(base);
|
||||
for (byte[] bytes : toSign) {
|
||||
construct.update(bytes);
|
||||
}
|
||||
return construct.sign();
|
||||
} catch (GeneralSecurityException e) {
|
||||
throw new IllegalArgumentException("Invalid signature parameters");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes a long array as Big-endian byte array.
|
||||
*
|
||||
* @param bits the long (array) of numbers to encode
|
||||
* @return the encoded bytes
|
||||
*/
|
||||
public static byte[] longToBigEndianByteArray(long... bits) {
|
||||
ByteBuffer ret = ByteBuffer.allocate(8 * bits.length).order(ByteOrder.BIG_ENDIAN);
|
||||
for (long put : bits) {
|
||||
ret.putLong(put);
|
||||
}
|
||||
return ret.array();
|
||||
}
|
||||
|
||||
public static String encodeUrlEncoded(byte[] data) {
|
||||
return MIME_SPECIAL_ENCODER.encodeToString(data);
|
||||
}
|
||||
|
||||
public static byte[] decodeUrlEncoded(String toParse) {
|
||||
return Base64.getMimeDecoder().decode(toParse);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a cer-encoded RSA key into its key bytes.
|
||||
*
|
||||
* @param toParse the cer-encoded key String
|
||||
* @param descriptors the type of key
|
||||
* @return the parsed key bytes
|
||||
*/
|
||||
public static byte[] parsePemEncoded(String toParse, Pair<String, String> descriptors) {
|
||||
int startIdx = toParse.indexOf(descriptors.first());
|
||||
Preconditions.checkArgument(startIdx >= 0);
|
||||
int firstLen = descriptors.first().length();
|
||||
int endIdx = toParse.indexOf(descriptors.second(), firstLen + startIdx) + 1;
|
||||
Preconditions.checkArgument(endIdx > 0);
|
||||
return decodeUrlEncoded(toParse.substring(startIdx + firstLen, endIdx));
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes an RSA key as String cer format.
|
||||
*
|
||||
* @param toEncode the private or public RSA key
|
||||
* @return the encoded key cer
|
||||
*/
|
||||
public static String pemEncodeRsaKey(Key toEncode) {
|
||||
Preconditions.checkNotNull(toEncode);
|
||||
Pair<String, String> encoder;
|
||||
if (toEncode instanceof PublicKey) {
|
||||
encoder = PEM_RSA_PUBLIC_KEY_DESCRIPTOR;
|
||||
} else if (toEncode instanceof PrivateKey) {
|
||||
encoder = PEM_RSA_PRIVATE_KEY_DESCRIPTOR;
|
||||
} else {
|
||||
throw new IllegalArgumentException("Invalid key type");
|
||||
}
|
||||
|
||||
return encoder.first() + "\n"
|
||||
+ encodeUrlEncoded(toEncode.getEncoded()) + "\n"
|
||||
+ encoder.second() + "\n";
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse an RSA public key from key bytes.
|
||||
*
|
||||
* @param keyValue the key bytes
|
||||
* @return the generated key
|
||||
*/
|
||||
public static PublicKey parseRsaPublicKey(byte[] keyValue) {
|
||||
try {
|
||||
return RSA_KEY_FACTORY.generatePublic(new X509EncodedKeySpec(keyValue));
|
||||
} catch (InvalidKeySpecException e) {
|
||||
throw new IllegalArgumentException("Invalid key bytes");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an RSA key pair.
|
||||
*
|
||||
* @param keysize the key size (in bits) for the RSA key pair
|
||||
* @return the generated key pair
|
||||
*/
|
||||
public static KeyPair createRsaKeyPair(final int keysize) {
|
||||
try {
|
||||
final KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
|
||||
generator.initialize(keysize);
|
||||
return generator.generateKeyPair();
|
||||
} catch (final NoSuchAlgorithmException e) {
|
||||
throw new RuntimeException("Unable to generate RSA keypair", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a hex digest in two's complement form for use with the Mojang joinedServer endpoint.
|
||||
*
|
||||
* @param digest the bytes to digest
|
||||
* @return the hex digest
|
||||
*/
|
||||
public static String twosComplementHexdigest(byte[] digest) {
|
||||
return new BigInteger(digest).toString(16);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypts an RSA message.
|
||||
*
|
||||
* @param keyPair the key pair to use
|
||||
* @param bytes the bytes of the encrypted message
|
||||
* @return the decrypted message
|
||||
* @throws GeneralSecurityException if the message couldn't be decoded
|
||||
*/
|
||||
public static byte[] decryptRsa(KeyPair keyPair, byte[] bytes) throws GeneralSecurityException {
|
||||
Cipher cipher = Cipher.getInstance("RSA");
|
||||
cipher.init(Cipher.DECRYPT_MODE, keyPair.getPrivate());
|
||||
return cipher.doFinal(bytes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the server ID for the hasJoined endpoint.
|
||||
*
|
||||
* @param sharedSecret the shared secret between the client and the proxy
|
||||
* @param key the RSA public key
|
||||
* @return the server ID
|
||||
*/
|
||||
public static String generateServerId(byte[] sharedSecret, PublicKey key) {
|
||||
try {
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-1");
|
||||
digest.update(sharedSecret);
|
||||
digest.update(key.getEncoded());
|
||||
return twosComplementHexdigest(digest.digest());
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,98 @@
|
||||
/*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.velocitypowered.proxy.crypto;
|
||||
|
||||
import com.velocitypowered.api.proxy.crypto.IdentifiedKey;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.PublicKey;
|
||||
import java.time.Instant;
|
||||
import java.util.Arrays;
|
||||
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
|
||||
public class IdentifiedKeyImpl implements IdentifiedKey {
|
||||
|
||||
private final PublicKey publicKey;
|
||||
private final byte[] signature;
|
||||
private final Instant expiryTemporal;
|
||||
private @MonotonicNonNull Boolean isSignatureValid;
|
||||
|
||||
public IdentifiedKeyImpl(byte[] keyBits, long expiry,
|
||||
byte[] signature) {
|
||||
this(EncryptionUtils.parseRsaPublicKey(keyBits), Instant.ofEpochMilli(expiry), signature);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an Identified key from data.
|
||||
*/
|
||||
public IdentifiedKeyImpl(PublicKey publicKey, Instant expiryTemporal, byte[] signature) {
|
||||
this.publicKey = publicKey;
|
||||
this.expiryTemporal = expiryTemporal;
|
||||
this.signature = signature;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PublicKey getSignedPublicKey() {
|
||||
return publicKey;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PublicKey getSigner() {
|
||||
return EncryptionUtils.getYggdrasilSessionKey();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Instant getExpiryTemporal() {
|
||||
return expiryTemporal;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getSignature() {
|
||||
return signature;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSignatureValid() {
|
||||
if (isSignatureValid == null) {
|
||||
String pemKey = EncryptionUtils.pemEncodeRsaKey(publicKey);
|
||||
long expires = expiryTemporal.toEpochMilli();
|
||||
byte[] toVerify = ("" + expires + pemKey).getBytes(StandardCharsets.US_ASCII);
|
||||
isSignatureValid = EncryptionUtils.verifySignature(
|
||||
EncryptionUtils.SHA1_WITH_RSA, EncryptionUtils.getYggdrasilSessionKey(), signature, toVerify);
|
||||
}
|
||||
return isSignatureValid;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean verifyDataSignature(byte[] signature, byte[]... toVerify) {
|
||||
try {
|
||||
return EncryptionUtils.verifySignature(EncryptionUtils.SHA256_WITH_RSA, publicKey, signature, toVerify);
|
||||
} catch (IllegalArgumentException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "IdentifiedKeyImpl{"
|
||||
+ "publicKey=" + publicKey
|
||||
+ ", signature=" + Arrays.toString(signature)
|
||||
+ ", expiryTemporal=" + expiryTemporal
|
||||
+ ", isSignatureValid=" + isSignatureValid
|
||||
+ '}';
|
||||
}
|
||||
}
|
@ -0,0 +1,86 @@
|
||||
/*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.velocitypowered.proxy.crypto;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.velocitypowered.api.proxy.crypto.KeySigned;
|
||||
import java.security.PublicKey;
|
||||
import java.time.Instant;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||
|
||||
public class SignedChatCommand implements KeySigned {
|
||||
private final String command;
|
||||
private final PublicKey signer;
|
||||
private final Instant expiry;
|
||||
private final byte[] salt;
|
||||
private final UUID sender;
|
||||
//private final boolean isValid;
|
||||
private final boolean isPreviewSigned;
|
||||
|
||||
private final Map<String, byte[]> signatures;
|
||||
|
||||
/**
|
||||
* Create a signed command from data.
|
||||
*/
|
||||
public SignedChatCommand(String command, PublicKey signer, UUID sender,
|
||||
Instant expiry, Map<String, byte[]> signature, byte[] salt, boolean isPreviewSigned) {
|
||||
this.command = Preconditions.checkNotNull(command);
|
||||
this.signer = Preconditions.checkNotNull(signer);
|
||||
this.sender = Preconditions.checkNotNull(sender);
|
||||
this.signatures = Preconditions.checkNotNull(signature);
|
||||
this.expiry = Preconditions.checkNotNull(expiry);
|
||||
this.salt = Preconditions.checkNotNull(salt);
|
||||
this.isPreviewSigned = isPreviewSigned;
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public PublicKey getSigner() {
|
||||
return signer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Instant getExpiryTemporal() {
|
||||
return expiry;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable byte[] getSignature() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getSalt() {
|
||||
return salt;
|
||||
}
|
||||
|
||||
public String getBaseCommand() {
|
||||
return command;
|
||||
}
|
||||
|
||||
|
||||
public Map<String, byte[]> getSignatures() {
|
||||
return signatures;
|
||||
}
|
||||
|
||||
public boolean isPreviewSigned() {
|
||||
return isPreviewSigned;
|
||||
}
|
||||
}
|
@ -0,0 +1,111 @@
|
||||
/*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.velocitypowered.proxy.crypto;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.primitives.Longs;
|
||||
import com.velocitypowered.api.proxy.crypto.SignedMessage;
|
||||
import com.velocitypowered.proxy.util.except.QuietDecoderException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.KeyPair;
|
||||
import java.security.PublicKey;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.TemporalAmount;
|
||||
import java.util.UUID;
|
||||
|
||||
import net.kyori.adventure.text.Component;
|
||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||
|
||||
public class SignedChatMessage implements SignedMessage {
|
||||
|
||||
private static final QuietDecoderException INVALID_SIGNED_CHAT =
|
||||
new QuietDecoderException("Couldn't parse chat message");
|
||||
|
||||
public static final TemporalAmount EXPIRY_TIME = Duration.ofMinutes(2L);
|
||||
|
||||
private final String message;
|
||||
private final PublicKey signer;
|
||||
private final byte[] signature;
|
||||
private final Instant expiry;
|
||||
private final byte[] salt;
|
||||
private final UUID sender;
|
||||
//private final boolean isValid;
|
||||
private final boolean isPreviewSigned;
|
||||
|
||||
/**
|
||||
* Create a signed message from data.
|
||||
*/
|
||||
public SignedChatMessage(String message, PublicKey signer, UUID sender,
|
||||
Instant expiry, byte[] signature, byte[] salt, boolean isPreviewSigned) {
|
||||
this.message = Preconditions.checkNotNull(message);
|
||||
this.signer = Preconditions.checkNotNull(signer);
|
||||
this.sender = Preconditions.checkNotNull(sender);
|
||||
this.signature = Preconditions.checkNotNull(signature);
|
||||
this.expiry = Preconditions.checkNotNull(expiry);
|
||||
this.salt = Preconditions.checkNotNull(salt);
|
||||
this.isPreviewSigned = isPreviewSigned;
|
||||
|
||||
|
||||
//this.isValid = EncryptionUtils.verifySignature(EncryptionUtils.SHA1_WITH_RSA, signer,
|
||||
// signature, salt, EncryptionUtils.longToBigEndianByteArray(
|
||||
// sender.getMostSignificantBits(), sender.getLeastSignificantBits()
|
||||
// ), Longs.toByteArray(expiry.getEpochSecond()), message.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
@Override
|
||||
public PublicKey getSigner() {
|
||||
return signer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Instant getExpiryTemporal() {
|
||||
return expiry;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable byte[] getSignature() {
|
||||
return signature;
|
||||
}
|
||||
|
||||
//@Override
|
||||
//public boolean isSignatureValid() {
|
||||
// return isValid;
|
||||
//}
|
||||
|
||||
@Override
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
@Override
|
||||
public UUID getSignerUuid() {
|
||||
return sender;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPreviewSigned() {
|
||||
return isPreviewSigned;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getSalt() {
|
||||
return salt;
|
||||
}
|
||||
|
||||
}
|
@ -21,7 +21,9 @@ import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static com.velocitypowered.proxy.protocol.util.NettyPreconditions.checkFrame;
|
||||
|
||||
import com.velocitypowered.api.network.ProtocolVersion;
|
||||
import com.velocitypowered.api.proxy.crypto.IdentifiedKey;
|
||||
import com.velocitypowered.api.util.GameProfile;
|
||||
import com.velocitypowered.proxy.crypto.IdentifiedKeyImpl;
|
||||
import com.velocitypowered.proxy.protocol.netty.MinecraftDecoder;
|
||||
import com.velocitypowered.proxy.protocol.util.VelocityLegacyHoverEventSerializer;
|
||||
import com.velocitypowered.proxy.util.except.QuietDecoderException;
|
||||
@ -59,7 +61,7 @@ public enum ProtocolUtils {
|
||||
.legacyHoverEventSerializer(VelocityLegacyHoverEventSerializer.INSTANCE)
|
||||
.build();
|
||||
|
||||
private static final int DEFAULT_MAX_STRING_SIZE = 65536; // 64KiB
|
||||
public static final int DEFAULT_MAX_STRING_SIZE = 65536; // 64KiB
|
||||
private static final QuietDecoderException BAD_VARINT_CACHED =
|
||||
new QuietDecoderException("Bad VarInt decoded");
|
||||
private static final int[] VARINT_EXACT_BYTE_LENGTHS = new int[33];
|
||||
@ -537,6 +539,31 @@ public enum ProtocolUtils {
|
||||
return PRE_1_16_SERIALIZER;
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a players {@link IdentifiedKey} to the buffer.
|
||||
*
|
||||
* @param buf the buffer
|
||||
* @param playerKey the key to write
|
||||
*/
|
||||
public static void writePlayerKey(ByteBuf buf, IdentifiedKey playerKey) {
|
||||
buf.writeLong(playerKey.getExpiryTemporal().toEpochMilli());
|
||||
ProtocolUtils.writeByteArray(buf, playerKey.getSignedPublicKey().getEncoded());
|
||||
ProtocolUtils.writeByteArray(buf, playerKey.getSignature());
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a players {@link IdentifiedKey} from the buffer.
|
||||
*
|
||||
* @param buf the buffer
|
||||
* @return the key
|
||||
*/
|
||||
public static IdentifiedKey readPlayerKey(ByteBuf buf) {
|
||||
long expiry = buf.readLong();
|
||||
byte[] key = ProtocolUtils.readByteArray(buf);
|
||||
byte[] signature = ProtocolUtils.readByteArray(buf, 4096);
|
||||
return new IdentifiedKeyImpl(key, expiry, signature);
|
||||
}
|
||||
|
||||
public enum Direction {
|
||||
SERVERBOUND,
|
||||
CLIENTBOUND;
|
||||
|
@ -28,6 +28,8 @@ import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_16_2;
|
||||
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_16_4;
|
||||
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_17;
|
||||
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_18;
|
||||
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_18_2;
|
||||
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_19;
|
||||
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_7_2;
|
||||
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_8;
|
||||
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_9;
|
||||
@ -39,7 +41,6 @@ import static com.velocitypowered.proxy.protocol.ProtocolUtils.Direction;
|
||||
import com.velocitypowered.api.network.ProtocolVersion;
|
||||
import com.velocitypowered.proxy.protocol.packet.AvailableCommands;
|
||||
import com.velocitypowered.proxy.protocol.packet.BossBar;
|
||||
import com.velocitypowered.proxy.protocol.packet.Chat;
|
||||
import com.velocitypowered.proxy.protocol.packet.ClientSettings;
|
||||
import com.velocitypowered.proxy.protocol.packet.Disconnect;
|
||||
import com.velocitypowered.proxy.protocol.packet.EncryptionRequest;
|
||||
@ -63,6 +64,10 @@ import com.velocitypowered.proxy.protocol.packet.StatusRequest;
|
||||
import com.velocitypowered.proxy.protocol.packet.StatusResponse;
|
||||
import com.velocitypowered.proxy.protocol.packet.TabCompleteRequest;
|
||||
import com.velocitypowered.proxy.protocol.packet.TabCompleteResponse;
|
||||
import com.velocitypowered.proxy.protocol.packet.chat.LegacyChat;
|
||||
import com.velocitypowered.proxy.protocol.packet.chat.PlayerChat;
|
||||
import com.velocitypowered.proxy.protocol.packet.chat.PlayerCommand;
|
||||
import com.velocitypowered.proxy.protocol.packet.chat.SystemChat;
|
||||
import com.velocitypowered.proxy.protocol.packet.title.LegacyTitlePacket;
|
||||
import com.velocitypowered.proxy.protocol.packet.title.TitleActionbarPacket;
|
||||
import com.velocitypowered.proxy.protocol.packet.title.TitleClearPacket;
|
||||
@ -114,19 +119,25 @@ public enum StateRegistry {
|
||||
map(0x02, MINECRAFT_1_12, false),
|
||||
map(0x01, MINECRAFT_1_12_1, false),
|
||||
map(0x05, MINECRAFT_1_13, false),
|
||||
map(0x06, MINECRAFT_1_14, false));
|
||||
serverbound.register(Chat.class, Chat::new,
|
||||
map(0x06, MINECRAFT_1_14, false),
|
||||
map(0x08, MINECRAFT_1_19, false));
|
||||
serverbound.register(LegacyChat.class, LegacyChat::new,
|
||||
map(0x01, MINECRAFT_1_7_2, false),
|
||||
map(0x02, MINECRAFT_1_9, false),
|
||||
map(0x03, MINECRAFT_1_12, false),
|
||||
map(0x02, MINECRAFT_1_12_1, false),
|
||||
map(0x03, MINECRAFT_1_14, false));
|
||||
map(0x03, MINECRAFT_1_14, MINECRAFT_1_18_2, false));
|
||||
serverbound.register(PlayerCommand.class, PlayerCommand::new,
|
||||
map(0x03, MINECRAFT_1_19, false));
|
||||
serverbound.register(PlayerChat.class, PlayerChat::new,
|
||||
map(0x04, MINECRAFT_1_19, false));
|
||||
serverbound.register(ClientSettings.class, ClientSettings::new,
|
||||
map(0x15, MINECRAFT_1_7_2, false),
|
||||
map(0x04, MINECRAFT_1_9, false),
|
||||
map(0x05, MINECRAFT_1_12, false),
|
||||
map(0x04, MINECRAFT_1_12_1, false),
|
||||
map(0x05, MINECRAFT_1_14, false));
|
||||
map(0x05, MINECRAFT_1_14, false),
|
||||
map(0x07, MINECRAFT_1_19, false));
|
||||
serverbound.register(PluginMessage.class, PluginMessage::new,
|
||||
map(0x17, MINECRAFT_1_7_2, false),
|
||||
map(0x09, MINECRAFT_1_9, false),
|
||||
@ -134,7 +145,8 @@ public enum StateRegistry {
|
||||
map(0x09, MINECRAFT_1_12_1, false),
|
||||
map(0x0A, MINECRAFT_1_13, false),
|
||||
map(0x0B, MINECRAFT_1_14, false),
|
||||
map(0x0A, MINECRAFT_1_17, false));
|
||||
map(0x0A, MINECRAFT_1_17, false),
|
||||
map(0x0C, MINECRAFT_1_19, false));
|
||||
serverbound.register(KeepAlive.class, KeepAlive::new,
|
||||
map(0x00, MINECRAFT_1_7_2, false),
|
||||
map(0x0B, MINECRAFT_1_9, false),
|
||||
@ -143,7 +155,8 @@ public enum StateRegistry {
|
||||
map(0x0E, MINECRAFT_1_13, false),
|
||||
map(0x0F, MINECRAFT_1_14, false),
|
||||
map(0x10, MINECRAFT_1_16, false),
|
||||
map(0x0F, MINECRAFT_1_17, false));
|
||||
map(0x0F, MINECRAFT_1_17, false),
|
||||
map(0x11, MINECRAFT_1_19, false));
|
||||
serverbound.register(ResourcePackResponse.class, ResourcePackResponse::new,
|
||||
map(0x19, MINECRAFT_1_8, false),
|
||||
map(0x16, MINECRAFT_1_9, false),
|
||||
@ -151,20 +164,22 @@ public enum StateRegistry {
|
||||
map(0x1D, MINECRAFT_1_13, false),
|
||||
map(0x1F, MINECRAFT_1_14, false),
|
||||
map(0x20, MINECRAFT_1_16, false),
|
||||
map(0x21, MINECRAFT_1_16_2, false));
|
||||
map(0x21, MINECRAFT_1_16_2, false),
|
||||
map(0x23, MINECRAFT_1_19, false));
|
||||
|
||||
clientbound.register(BossBar.class, BossBar::new,
|
||||
map(0x0C, MINECRAFT_1_9, false),
|
||||
map(0x0D, MINECRAFT_1_15, false),
|
||||
map(0x0C, MINECRAFT_1_16, false),
|
||||
map(0x0D, MINECRAFT_1_17, false));
|
||||
clientbound.register(Chat.class, Chat::new,
|
||||
map(0x0D, MINECRAFT_1_17, false),
|
||||
map(0x0A, MINECRAFT_1_19, false));
|
||||
clientbound.register(LegacyChat.class, LegacyChat::new,
|
||||
map(0x02, MINECRAFT_1_7_2, true),
|
||||
map(0x0F, MINECRAFT_1_9, true),
|
||||
map(0x0E, MINECRAFT_1_13, true),
|
||||
map(0x0F, MINECRAFT_1_15, true),
|
||||
map(0x0E, MINECRAFT_1_16, true),
|
||||
map(0x0F, MINECRAFT_1_17, true));
|
||||
map(0x0F, MINECRAFT_1_17, MINECRAFT_1_18_2, true));
|
||||
clientbound.register(TabCompleteResponse.class, TabCompleteResponse::new,
|
||||
map(0x3A, MINECRAFT_1_7_2, false),
|
||||
map(0x0E, MINECRAFT_1_9, false),
|
||||
@ -172,13 +187,15 @@ public enum StateRegistry {
|
||||
map(0x11, MINECRAFT_1_15, false),
|
||||
map(0x10, MINECRAFT_1_16, false),
|
||||
map(0x0F, MINECRAFT_1_16_2, false),
|
||||
map(0x11, MINECRAFT_1_17, false));
|
||||
map(0x11, MINECRAFT_1_17, false),
|
||||
map(0x0E, MINECRAFT_1_19, false));
|
||||
clientbound.register(AvailableCommands.class, AvailableCommands::new,
|
||||
map(0x11, MINECRAFT_1_13, false),
|
||||
map(0x12, MINECRAFT_1_15, false),
|
||||
map(0x11, MINECRAFT_1_16, false),
|
||||
map(0x10, MINECRAFT_1_16_2, false),
|
||||
map(0x12, MINECRAFT_1_17, false));
|
||||
map(0x12, MINECRAFT_1_17, false),
|
||||
map(0x0F, MINECRAFT_1_19, false));
|
||||
clientbound.register(PluginMessage.class, PluginMessage::new,
|
||||
map(0x3F, MINECRAFT_1_7_2, false),
|
||||
map(0x18, MINECRAFT_1_9, false),
|
||||
@ -187,7 +204,8 @@ public enum StateRegistry {
|
||||
map(0x19, MINECRAFT_1_15, false),
|
||||
map(0x18, MINECRAFT_1_16, false),
|
||||
map(0x17, MINECRAFT_1_16_2, false),
|
||||
map(0x18, MINECRAFT_1_17, false));
|
||||
map(0x18, MINECRAFT_1_17, false),
|
||||
map(0x15, MINECRAFT_1_19, false));
|
||||
clientbound.register(Disconnect.class, Disconnect::new,
|
||||
map(0x40, MINECRAFT_1_7_2, false),
|
||||
map(0x1A, MINECRAFT_1_9, false),
|
||||
@ -196,7 +214,8 @@ public enum StateRegistry {
|
||||
map(0x1B, MINECRAFT_1_15, false),
|
||||
map(0x1A, MINECRAFT_1_16, false),
|
||||
map(0x19, MINECRAFT_1_16_2, false),
|
||||
map(0x1A, MINECRAFT_1_17, false));
|
||||
map(0x1A, MINECRAFT_1_17, false),
|
||||
map(0x17, MINECRAFT_1_19, false));
|
||||
clientbound.register(KeepAlive.class, KeepAlive::new,
|
||||
map(0x00, MINECRAFT_1_7_2, false),
|
||||
map(0x1F, MINECRAFT_1_9, false),
|
||||
@ -205,7 +224,8 @@ public enum StateRegistry {
|
||||
map(0x21, MINECRAFT_1_15, false),
|
||||
map(0x20, MINECRAFT_1_16, false),
|
||||
map(0x1F, MINECRAFT_1_16_2, false),
|
||||
map(0x21, MINECRAFT_1_17, false));
|
||||
map(0x21, MINECRAFT_1_17, false),
|
||||
map(0x1E, MINECRAFT_1_19, false));
|
||||
clientbound.register(JoinGame.class, JoinGame::new,
|
||||
map(0x01, MINECRAFT_1_7_2, false),
|
||||
map(0x23, MINECRAFT_1_9, false),
|
||||
@ -214,7 +234,8 @@ public enum StateRegistry {
|
||||
map(0x26, MINECRAFT_1_15, false),
|
||||
map(0x25, MINECRAFT_1_16, false),
|
||||
map(0x24, MINECRAFT_1_16_2, false),
|
||||
map(0x26, MINECRAFT_1_17, false));
|
||||
map(0x26, MINECRAFT_1_17, false),
|
||||
map(0x23, MINECRAFT_1_19, false));
|
||||
clientbound.register(Respawn.class, Respawn::new,
|
||||
map(0x07, MINECRAFT_1_7_2, true),
|
||||
map(0x33, MINECRAFT_1_9, true),
|
||||
@ -225,7 +246,8 @@ public enum StateRegistry {
|
||||
map(0x3B, MINECRAFT_1_15, true),
|
||||
map(0x3A, MINECRAFT_1_16, true),
|
||||
map(0x39, MINECRAFT_1_16_2, true),
|
||||
map(0x3D, MINECRAFT_1_17, true));
|
||||
map(0x3D, MINECRAFT_1_17, true),
|
||||
map(0x3B, MINECRAFT_1_19, true));
|
||||
clientbound.register(ResourcePackRequest.class, ResourcePackRequest::new,
|
||||
map(0x48, MINECRAFT_1_8, false),
|
||||
map(0x32, MINECRAFT_1_9, false),
|
||||
@ -236,7 +258,8 @@ public enum StateRegistry {
|
||||
map(0x3A, MINECRAFT_1_15, false),
|
||||
map(0x39, MINECRAFT_1_16, false),
|
||||
map(0x38, MINECRAFT_1_16_2, false),
|
||||
map(0x3C, MINECRAFT_1_17, false));
|
||||
map(0x3C, MINECRAFT_1_17, false),
|
||||
map(0x3A, MINECRAFT_1_19, false));
|
||||
clientbound.register(HeaderAndFooter.class, HeaderAndFooter::new,
|
||||
map(0x47, MINECRAFT_1_8, true),
|
||||
map(0x48, MINECRAFT_1_9, true),
|
||||
@ -248,7 +271,8 @@ public enum StateRegistry {
|
||||
map(0x54, MINECRAFT_1_15, true),
|
||||
map(0x53, MINECRAFT_1_16, true),
|
||||
map(0x5E, MINECRAFT_1_17, true),
|
||||
map(0x5F, MINECRAFT_1_18, true));
|
||||
map(0x5F, MINECRAFT_1_18, true),
|
||||
map(0x60, MINECRAFT_1_19, true));
|
||||
clientbound.register(LegacyTitlePacket.class, LegacyTitlePacket::new,
|
||||
map(0x45, MINECRAFT_1_8, true),
|
||||
map(0x45, MINECRAFT_1_9, true),
|
||||
@ -265,12 +289,14 @@ public enum StateRegistry {
|
||||
map(0x59, MINECRAFT_1_17, true),
|
||||
map(0x5A, MINECRAFT_1_18, true));
|
||||
clientbound.register(TitleActionbarPacket.class, TitleActionbarPacket::new,
|
||||
map(0x41, MINECRAFT_1_17, true));
|
||||
map(0x41, MINECRAFT_1_17, true),
|
||||
map(0x40, MINECRAFT_1_19, true));
|
||||
clientbound.register(TitleTimesPacket.class, TitleTimesPacket::new,
|
||||
map(0x5A, MINECRAFT_1_17, true),
|
||||
map(0x5B, MINECRAFT_1_18, true));
|
||||
clientbound.register(TitleClearPacket.class, TitleClearPacket::new,
|
||||
map(0x10, MINECRAFT_1_17, true));
|
||||
map(0x10, MINECRAFT_1_17, true),
|
||||
map(0x0D, MINECRAFT_1_19, true));
|
||||
clientbound.register(PlayerListItem.class, PlayerListItem::new,
|
||||
map(0x38, MINECRAFT_1_7_2, false),
|
||||
map(0x2D, MINECRAFT_1_9, false),
|
||||
@ -280,7 +306,10 @@ public enum StateRegistry {
|
||||
map(0x34, MINECRAFT_1_15, false),
|
||||
map(0x33, MINECRAFT_1_16, false),
|
||||
map(0x32, MINECRAFT_1_16_2, false),
|
||||
map(0x36, MINECRAFT_1_17, false));
|
||||
map(0x36, MINECRAFT_1_17, false),
|
||||
map(0x34, MINECRAFT_1_19, false));
|
||||
clientbound.register(SystemChat.class, SystemChat::new,
|
||||
map(0x5F, MINECRAFT_1_19, true));
|
||||
}
|
||||
},
|
||||
LOGIN {
|
||||
|
@ -83,7 +83,7 @@ public class AvailableCommands implements MinecraftPacket {
|
||||
int commands = ProtocolUtils.readVarInt(buf);
|
||||
WireNode[] wireNodes = new WireNode[commands];
|
||||
for (int i = 0; i < commands; i++) {
|
||||
wireNodes[i] = deserializeNode(buf, i);
|
||||
wireNodes[i] = deserializeNode(buf, i, protocolVersion);
|
||||
}
|
||||
|
||||
// Iterate over the deserialized nodes and attempt to form a graph. We also resolve any cycles
|
||||
@ -130,13 +130,13 @@ public class AvailableCommands implements MinecraftPacket {
|
||||
// Now serialize the children.
|
||||
ProtocolUtils.writeVarInt(buf, idMappings.size());
|
||||
for (CommandNode<CommandSource> child : idMappings.keySet()) {
|
||||
serializeNode(child, buf, idMappings);
|
||||
serializeNode(child, buf, idMappings, protocolVersion);
|
||||
}
|
||||
ProtocolUtils.writeVarInt(buf, idMappings.getInt(rootNode));
|
||||
}
|
||||
|
||||
private static void serializeNode(CommandNode<CommandSource> node, ByteBuf buf,
|
||||
Object2IntMap<CommandNode<CommandSource>> idMappings) {
|
||||
Object2IntMap<CommandNode<CommandSource>> idMappings, ProtocolVersion protocolVersion) {
|
||||
byte flags = 0;
|
||||
if (node.getRedirect() != null) {
|
||||
flags |= FLAG_IS_REDIRECT;
|
||||
@ -168,7 +168,7 @@ public class AvailableCommands implements MinecraftPacket {
|
||||
if (node instanceof ArgumentCommandNode<?, ?>) {
|
||||
ProtocolUtils.writeString(buf, node.getName());
|
||||
ArgumentPropertyRegistry.serialize(buf,
|
||||
((ArgumentCommandNode<CommandSource, ?>) node).getType());
|
||||
((ArgumentCommandNode<CommandSource, ?>) node).getType(), protocolVersion);
|
||||
|
||||
if (((ArgumentCommandNode<CommandSource, ?>) node).getCustomSuggestions() != null) {
|
||||
SuggestionProvider<CommandSource> provider = ((ArgumentCommandNode<CommandSource, ?>) node)
|
||||
@ -189,7 +189,7 @@ public class AvailableCommands implements MinecraftPacket {
|
||||
return handler.handle(this);
|
||||
}
|
||||
|
||||
private static WireNode deserializeNode(ByteBuf buf, int idx) {
|
||||
private static WireNode deserializeNode(ByteBuf buf, int idx, ProtocolVersion version) {
|
||||
byte flags = buf.readByte();
|
||||
int[] children = ProtocolUtils.readIntegerArray(buf);
|
||||
int redirectTo = -1;
|
||||
@ -205,14 +205,13 @@ public class AvailableCommands implements MinecraftPacket {
|
||||
.literal(ProtocolUtils.readString(buf)));
|
||||
case NODE_TYPE_ARGUMENT:
|
||||
String name = ProtocolUtils.readString(buf);
|
||||
ArgumentType<?> argumentType = ArgumentPropertyRegistry.deserialize(buf);
|
||||
ArgumentType<?> argumentType = ArgumentPropertyRegistry.deserialize(buf, version);
|
||||
|
||||
RequiredArgumentBuilder<CommandSource, ?> argumentBuilder = RequiredArgumentBuilder
|
||||
.argument(name, argumentType);
|
||||
if ((flags & FLAG_HAS_SUGGESTIONS) != 0) {
|
||||
argumentBuilder.suggests(new ProtocolSuggestionProvider(ProtocolUtils.readString(buf)));
|
||||
}
|
||||
|
||||
return new WireNode(idx, flags, children, redirectTo, argumentBuilder);
|
||||
default:
|
||||
throw new IllegalArgumentException("Unknown node type " + (flags & FLAG_NODE_TYPE));
|
||||
|
@ -24,13 +24,19 @@ 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 com.velocitypowered.proxy.util.except.QuietDecoderException;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
public class EncryptionResponse implements MinecraftPacket {
|
||||
|
||||
private final static QuietDecoderException NO_SALT = new QuietDecoderException("Encryption response didn't contain salt");
|
||||
|
||||
private byte[] sharedSecret = EMPTY_BYTE_ARRAY;
|
||||
private byte[] verifyToken = EMPTY_BYTE_ARRAY;
|
||||
private @Nullable Long salt;
|
||||
|
||||
public byte[] getSharedSecret() {
|
||||
return sharedSecret.clone();
|
||||
@ -40,6 +46,13 @@ public class EncryptionResponse implements MinecraftPacket {
|
||||
return verifyToken.clone();
|
||||
}
|
||||
|
||||
public long getSalt() {
|
||||
if (salt == null) {
|
||||
throw NO_SALT;
|
||||
}
|
||||
return salt;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "EncryptionResponse{"
|
||||
@ -52,7 +65,14 @@ public class EncryptionResponse implements MinecraftPacket {
|
||||
public void decode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) {
|
||||
if (version.compareTo(ProtocolVersion.MINECRAFT_1_8) >= 0) {
|
||||
this.sharedSecret = ProtocolUtils.readByteArray(buf, 128);
|
||||
this.verifyToken = ProtocolUtils.readByteArray(buf, 128);
|
||||
|
||||
if (version.compareTo(ProtocolVersion.MINECRAFT_1_19) >= 0
|
||||
&& !buf.readBoolean()) {
|
||||
salt = buf.readLong();
|
||||
}
|
||||
|
||||
this.verifyToken = ProtocolUtils.readByteArray(buf,
|
||||
version.compareTo(ProtocolVersion.MINECRAFT_1_19) >= 0 ? 256 : 128);
|
||||
} else {
|
||||
this.sharedSecret = ProtocolUtils.readByteArray17(buf);
|
||||
this.verifyToken = ProtocolUtils.readByteArray17(buf);
|
||||
@ -63,6 +83,14 @@ public class EncryptionResponse implements MinecraftPacket {
|
||||
public void encode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) {
|
||||
if (version.compareTo(ProtocolVersion.MINECRAFT_1_8) >= 0) {
|
||||
ProtocolUtils.writeByteArray(buf, sharedSecret);
|
||||
if (version.compareTo(ProtocolVersion.MINECRAFT_1_19) >= 0) {
|
||||
if (salt != null) {
|
||||
buf.writeBoolean(false);
|
||||
buf.writeLong(salt);
|
||||
} else {
|
||||
buf.writeBoolean(true);
|
||||
}
|
||||
}
|
||||
ProtocolUtils.writeByteArray(buf, verifyToken);
|
||||
} else {
|
||||
ProtocolUtils.writeByteArray17(sharedSecret, buf, false);
|
||||
@ -79,11 +107,22 @@ public class EncryptionResponse implements MinecraftPacket {
|
||||
public int expectedMaxLength(ByteBuf buf, Direction direction, ProtocolVersion version) {
|
||||
// It turns out these come out to the same length, whether we're talking >=1.8 or not.
|
||||
// The length prefix always winds up being 2 bytes.
|
||||
return 260;
|
||||
int base = 256 + 2 + 2;
|
||||
if (version.compareTo(ProtocolVersion.MINECRAFT_1_19) >= 0) {
|
||||
// Verify token is twice as long on 1.19+
|
||||
// Additional 1 byte for left <> right and 8 bytes for salt
|
||||
base += 128 + 8 + 1;
|
||||
}
|
||||
return base;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int expectedMinLength(ByteBuf buf, Direction direction, ProtocolVersion version) {
|
||||
return expectedMaxLength(buf, direction, version);
|
||||
int base = expectedMaxLength(buf, direction, version);
|
||||
if (version.compareTo(ProtocolVersion.MINECRAFT_1_19) >= 0) {
|
||||
// These are "optional"
|
||||
base -= 128 + 8;
|
||||
}
|
||||
return base;
|
||||
}
|
||||
}
|
||||
|
@ -25,6 +25,7 @@ import com.velocitypowered.proxy.connection.registry.DimensionInfo;
|
||||
import com.velocitypowered.proxy.connection.registry.DimensionRegistry;
|
||||
import com.velocitypowered.proxy.protocol.*;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import it.unimi.dsi.fastutil.Pair;
|
||||
import net.kyori.adventure.nbt.BinaryTagIO;
|
||||
import net.kyori.adventure.nbt.BinaryTagTypes;
|
||||
import net.kyori.adventure.nbt.CompoundBinaryTag;
|
||||
@ -33,7 +34,7 @@ import org.checkerframework.checker.nullness.qual.Nullable;
|
||||
|
||||
public class JoinGame implements MinecraftPacket {
|
||||
|
||||
private static final BinaryTagIO.Reader JOINGAME_READER = BinaryTagIO.reader(2 * 1024 * 1024);
|
||||
private static final BinaryTagIO.Reader JOINGAME_READER = BinaryTagIO.reader(4 * 1024 * 1024);
|
||||
private int entityId;
|
||||
private short gamemode;
|
||||
private int dimension;
|
||||
@ -51,6 +52,8 @@ public class JoinGame implements MinecraftPacket {
|
||||
private short previousGamemode; // 1.16+
|
||||
private CompoundBinaryTag biomeRegistry; // 1.16.2+
|
||||
private int simulationDistance; // 1.18+
|
||||
private @Nullable Pair<String, Long> lastDeathPosition;
|
||||
private CompoundBinaryTag chatTypeRegistry; // placeholder, 1.19+
|
||||
|
||||
public int getEntityId() {
|
||||
return entityId;
|
||||
@ -172,6 +175,22 @@ public class JoinGame implements MinecraftPacket {
|
||||
this.simulationDistance = simulationDistance;
|
||||
}
|
||||
|
||||
public Pair<String, Long> getLastDeathPosition() {
|
||||
return lastDeathPosition;
|
||||
}
|
||||
|
||||
public void setLastDeathPosition(Pair<String, Long> lastDeathPosition) {
|
||||
this.lastDeathPosition = lastDeathPosition;
|
||||
}
|
||||
|
||||
public CompoundBinaryTag getChatTypeRegistry() {
|
||||
return chatTypeRegistry;
|
||||
}
|
||||
|
||||
public void setChatTypeRegistry(CompoundBinaryTag chatTypeRegistry) {
|
||||
this.chatTypeRegistry = chatTypeRegistry;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "JoinGame{"
|
||||
@ -188,6 +207,7 @@ public class JoinGame implements MinecraftPacket {
|
||||
+ ", dimensionInfo='" + dimensionInfo + '\''
|
||||
+ ", previousGamemode=" + previousGamemode
|
||||
+ ", simulationDistance=" + simulationDistance
|
||||
+ ", lastDeathPosition='" + lastDeathPosition + '\''
|
||||
+ '}';
|
||||
}
|
||||
|
||||
@ -253,6 +273,7 @@ public class JoinGame implements MinecraftPacket {
|
||||
dimensionRegistryContainer = registryContainer.getCompound("minecraft:dimension_type")
|
||||
.getList("value", BinaryTagTypes.COMPOUND);
|
||||
this.biomeRegistry = registryContainer.getCompound("minecraft:worldgen/biome");
|
||||
this.chatTypeRegistry = registryContainer.getCompound("minecraft:chat_type");
|
||||
} else {
|
||||
dimensionRegistryContainer = registryContainer.getList("dimension",
|
||||
BinaryTagTypes.COMPOUND);
|
||||
@ -263,7 +284,8 @@ public class JoinGame implements MinecraftPacket {
|
||||
|
||||
String dimensionIdentifier;
|
||||
String levelName = null;
|
||||
if (version.compareTo(ProtocolVersion.MINECRAFT_1_16_2) >= 0) {
|
||||
if (version.compareTo(ProtocolVersion.MINECRAFT_1_16_2) >= 0
|
||||
&& version.compareTo(ProtocolVersion.MINECRAFT_1_19) < 0) {
|
||||
CompoundBinaryTag currentDimDataTag = ProtocolUtils.readCompoundTag(buf, JOINGAME_READER);
|
||||
dimensionIdentifier = ProtocolUtils.readString(buf);
|
||||
this.currentDimensionData = DimensionData.decodeBaseCompoundTag(currentDimDataTag, version)
|
||||
@ -290,6 +312,10 @@ public class JoinGame implements MinecraftPacket {
|
||||
boolean isDebug = buf.readBoolean();
|
||||
boolean isFlat = buf.readBoolean();
|
||||
this.dimensionInfo = new DimensionInfo(dimensionIdentifier, levelName, isFlat, isDebug);
|
||||
// optional death location
|
||||
if (version.compareTo(ProtocolVersion.MINECRAFT_1_19) >= 0 && buf.readBoolean()) {
|
||||
this.lastDeathPosition = Pair.of(ProtocolUtils.readString(buf), buf.readLong());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -356,11 +382,13 @@ public class JoinGame implements MinecraftPacket {
|
||||
dimensionRegistryEntry.put("value", encodedDimensionRegistry);
|
||||
registryContainer.put("minecraft:dimension_type", dimensionRegistryEntry.build());
|
||||
registryContainer.put("minecraft:worldgen/biome", biomeRegistry);
|
||||
registryContainer.put("minecraft:chat_type", chatTypeRegistry);
|
||||
} else {
|
||||
registryContainer.put("dimension", encodedDimensionRegistry);
|
||||
}
|
||||
ProtocolUtils.writeCompoundTag(buf, registryContainer.build());
|
||||
if (version.compareTo(ProtocolVersion.MINECRAFT_1_16_2) >= 0) {
|
||||
if (version.compareTo(ProtocolVersion.MINECRAFT_1_16_2) >= 0
|
||||
&& version.compareTo(ProtocolVersion.MINECRAFT_1_19) < 0) {
|
||||
ProtocolUtils.writeCompoundTag(buf, currentDimensionData.serializeDimensionDetails());
|
||||
ProtocolUtils.writeString(buf, dimensionInfo.getRegistryIdentifier());
|
||||
} else {
|
||||
@ -382,6 +410,17 @@ public class JoinGame implements MinecraftPacket {
|
||||
buf.writeBoolean(showRespawnScreen);
|
||||
buf.writeBoolean(dimensionInfo.isDebugType());
|
||||
buf.writeBoolean(dimensionInfo.isFlat());
|
||||
|
||||
// optional death location
|
||||
if (version.compareTo(ProtocolVersion.MINECRAFT_1_19) >= 0) {
|
||||
if (lastDeathPosition != null) {
|
||||
buf.writeBoolean(true);
|
||||
ProtocolUtils.writeString(buf, lastDeathPosition.key());
|
||||
buf.writeLong(lastDeathPosition.value());
|
||||
} else {
|
||||
buf.writeBoolean(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -19,6 +19,7 @@ package com.velocitypowered.proxy.protocol.packet;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.velocitypowered.api.network.ProtocolVersion;
|
||||
import com.velocitypowered.api.proxy.crypto.IdentifiedKey;
|
||||
import com.velocitypowered.api.proxy.player.TabListEntry;
|
||||
import com.velocitypowered.api.util.GameProfile;
|
||||
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
|
||||
@ -74,6 +75,12 @@ public class PlayerListItem implements MinecraftPacket {
|
||||
item.setGameMode(ProtocolUtils.readVarInt(buf));
|
||||
item.setLatency(ProtocolUtils.readVarInt(buf));
|
||||
item.setDisplayName(readOptionalComponent(buf, version));
|
||||
|
||||
if (version.compareTo(ProtocolVersion.MINECRAFT_1_19) >= 0) {
|
||||
if (buf.readBoolean()) {
|
||||
item.setPlayerKey(ProtocolUtils.readPlayerKey(buf));
|
||||
}
|
||||
}
|
||||
break;
|
||||
case UPDATE_GAMEMODE:
|
||||
item.setGameMode(ProtocolUtils.readVarInt(buf));
|
||||
@ -124,8 +131,15 @@ public class PlayerListItem implements MinecraftPacket {
|
||||
ProtocolUtils.writeProperties(buf, item.getProperties());
|
||||
ProtocolUtils.writeVarInt(buf, item.getGameMode());
|
||||
ProtocolUtils.writeVarInt(buf, item.getLatency());
|
||||
|
||||
writeDisplayName(buf, item.getDisplayName(), version);
|
||||
if (version.compareTo(ProtocolVersion.MINECRAFT_1_19) >= 0) {
|
||||
if (item.getPlayerKey() != null) {
|
||||
buf.writeBoolean(true);
|
||||
ProtocolUtils.writePlayerKey(buf, item.getPlayerKey());
|
||||
} else {
|
||||
buf.writeBoolean(false);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case UPDATE_GAMEMODE:
|
||||
ProtocolUtils.writeVarInt(buf, item.getGameMode());
|
||||
@ -181,6 +195,7 @@ public class PlayerListItem implements MinecraftPacket {
|
||||
private int gameMode;
|
||||
private int latency;
|
||||
private @Nullable Component displayName;
|
||||
private @Nullable IdentifiedKey playerKey;
|
||||
|
||||
public Item() {
|
||||
uuid = null;
|
||||
@ -196,6 +211,7 @@ public class PlayerListItem implements MinecraftPacket {
|
||||
.setProperties(entry.getProfile().getProperties())
|
||||
.setLatency(entry.getLatency())
|
||||
.setGameMode(entry.getGameMode())
|
||||
.setPlayerKey(entry.getIdentifiedKey())
|
||||
.setDisplayName(entry.getDisplayNameComponent().orElse(null));
|
||||
}
|
||||
|
||||
@ -247,5 +263,14 @@ public class PlayerListItem implements MinecraftPacket {
|
||||
this.displayName = displayName;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Item setPlayerKey(IdentifiedKey playerKey) {
|
||||
this.playerKey = playerKey;
|
||||
return this;
|
||||
}
|
||||
|
||||
public IdentifiedKey getPlayerKey() {
|
||||
return playerKey;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -24,8 +24,10 @@ import com.velocitypowered.proxy.connection.registry.DimensionInfo;
|
||||
import com.velocitypowered.proxy.protocol.MinecraftPacket;
|
||||
import com.velocitypowered.proxy.protocol.ProtocolUtils;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import it.unimi.dsi.fastutil.Pair;
|
||||
import net.kyori.adventure.nbt.BinaryTagIO;
|
||||
import net.kyori.adventure.nbt.CompoundBinaryTag;
|
||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||
|
||||
public class Respawn implements MinecraftPacket {
|
||||
|
||||
@ -38,13 +40,14 @@ public class Respawn implements MinecraftPacket {
|
||||
private DimensionInfo dimensionInfo; // 1.16-1.16.1
|
||||
private short previousGamemode; // 1.16+
|
||||
private DimensionData currentDimensionData; // 1.16.2+
|
||||
private @Nullable Pair<String, Long> lastDeathPosition; // 1.19+
|
||||
|
||||
public Respawn() {
|
||||
}
|
||||
|
||||
public Respawn(int dimension, long partialHashedSeed, short difficulty, short gamemode,
|
||||
String levelType, boolean shouldKeepPlayerData, DimensionInfo dimensionInfo,
|
||||
short previousGamemode, DimensionData currentDimensionData) {
|
||||
short previousGamemode, DimensionData currentDimensionData, @Nullable Pair<String, Long> lastDeathPosition) {
|
||||
this.dimension = dimension;
|
||||
this.partialHashedSeed = partialHashedSeed;
|
||||
this.difficulty = difficulty;
|
||||
@ -54,6 +57,7 @@ public class Respawn implements MinecraftPacket {
|
||||
this.dimensionInfo = dimensionInfo;
|
||||
this.previousGamemode = previousGamemode;
|
||||
this.currentDimensionData = currentDimensionData;
|
||||
this.lastDeathPosition = lastDeathPosition;
|
||||
}
|
||||
|
||||
public int getDimension() {
|
||||
@ -112,6 +116,14 @@ public class Respawn implements MinecraftPacket {
|
||||
this.previousGamemode = previousGamemode;
|
||||
}
|
||||
|
||||
public void setLastDeathPosition(Pair<String, Long> lastDeathPosition) {
|
||||
this.lastDeathPosition = lastDeathPosition;
|
||||
}
|
||||
|
||||
public Pair<String, Long> getLastDeathPosition() {
|
||||
return lastDeathPosition;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Respawn{"
|
||||
@ -133,7 +145,8 @@ public class Respawn implements MinecraftPacket {
|
||||
String dimensionIdentifier = null;
|
||||
String levelName = null;
|
||||
if (version.compareTo(ProtocolVersion.MINECRAFT_1_16) >= 0) {
|
||||
if (version.compareTo(ProtocolVersion.MINECRAFT_1_16_2) >= 0) {
|
||||
if (version.compareTo(ProtocolVersion.MINECRAFT_1_16_2) >= 0
|
||||
&& version.compareTo(ProtocolVersion.MINECRAFT_1_19) < 0) {
|
||||
CompoundBinaryTag dimDataTag = ProtocolUtils.readCompoundTag(buf, BinaryTagIO.reader());
|
||||
dimensionIdentifier = ProtocolUtils.readString(buf);
|
||||
this.currentDimensionData = DimensionData.decodeBaseCompoundTag(dimDataTag, version)
|
||||
@ -161,12 +174,16 @@ public class Respawn implements MinecraftPacket {
|
||||
} else {
|
||||
this.levelType = ProtocolUtils.readString(buf, 16);
|
||||
}
|
||||
if (version.compareTo(ProtocolVersion.MINECRAFT_1_19) >= 0 && buf.readBoolean()) {
|
||||
this.lastDeathPosition = Pair.of(ProtocolUtils.readString(buf), buf.readLong());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void encode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) {
|
||||
if (version.compareTo(ProtocolVersion.MINECRAFT_1_16) >= 0) {
|
||||
if (version.compareTo(ProtocolVersion.MINECRAFT_1_16_2) >= 0) {
|
||||
if (version.compareTo(ProtocolVersion.MINECRAFT_1_16_2) >= 0
|
||||
&& version.compareTo(ProtocolVersion.MINECRAFT_1_19) < 0) {
|
||||
ProtocolUtils.writeCompoundTag(buf, currentDimensionData.serializeDimensionDetails());
|
||||
ProtocolUtils.writeString(buf, dimensionInfo.getRegistryIdentifier());
|
||||
} else {
|
||||
@ -191,6 +208,17 @@ public class Respawn implements MinecraftPacket {
|
||||
} else {
|
||||
ProtocolUtils.writeString(buf, levelType);
|
||||
}
|
||||
|
||||
// optional death location
|
||||
if (version.compareTo(ProtocolVersion.MINECRAFT_1_19) >= 0) {
|
||||
if (lastDeathPosition != null) {
|
||||
buf.writeBoolean(true);
|
||||
ProtocolUtils.writeString(buf, lastDeathPosition.key());
|
||||
buf.writeLong(lastDeathPosition.value());
|
||||
} else {
|
||||
buf.writeBoolean(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -19,6 +19,7 @@ package com.velocitypowered.proxy.protocol.packet;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.velocitypowered.api.network.ProtocolVersion;
|
||||
import com.velocitypowered.api.proxy.crypto.IdentifiedKey;
|
||||
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
|
||||
import com.velocitypowered.proxy.protocol.MinecraftPacket;
|
||||
import com.velocitypowered.proxy.protocol.ProtocolUtils;
|
||||
@ -32,11 +33,12 @@ public class ServerLogin implements MinecraftPacket {
|
||||
private static final QuietDecoderException EMPTY_USERNAME = new QuietDecoderException("Empty username!");
|
||||
|
||||
private @Nullable String username;
|
||||
private @Nullable IdentifiedKey playerKey; // Introduced in 1.19
|
||||
|
||||
public ServerLogin() {
|
||||
}
|
||||
|
||||
public ServerLogin(String username) {
|
||||
public ServerLogin(String username, @Nullable IdentifiedKey playerKey) {
|
||||
this.username = Preconditions.checkNotNull(username, "username");
|
||||
}
|
||||
|
||||
@ -47,10 +49,19 @@ public class ServerLogin implements MinecraftPacket {
|
||||
return username;
|
||||
}
|
||||
|
||||
public IdentifiedKey getPlayerKey() {
|
||||
return playerKey;
|
||||
}
|
||||
|
||||
public void setPlayerKey(IdentifiedKey playerKey) {
|
||||
this.playerKey = playerKey;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ServerLogin{"
|
||||
+ "username='" + username + '\''
|
||||
+ "playerKey='" + playerKey + '\''
|
||||
+ '}';
|
||||
}
|
||||
|
||||
@ -60,6 +71,12 @@ public class ServerLogin implements MinecraftPacket {
|
||||
if (username.isEmpty()) {
|
||||
throw EMPTY_USERNAME;
|
||||
}
|
||||
|
||||
if (version.compareTo(ProtocolVersion.MINECRAFT_1_19) >= 0) {
|
||||
if (buf.readBoolean()) {
|
||||
playerKey = ProtocolUtils.readPlayerKey(buf);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -68,13 +85,27 @@ public class ServerLogin implements MinecraftPacket {
|
||||
throw new IllegalStateException("No username found!");
|
||||
}
|
||||
ProtocolUtils.writeString(buf, username);
|
||||
|
||||
if (version.compareTo(ProtocolVersion.MINECRAFT_1_19) >= 0) {
|
||||
if (playerKey != null) {
|
||||
buf.writeBoolean(true);
|
||||
ProtocolUtils.writePlayerKey(buf, playerKey);
|
||||
} else {
|
||||
buf.writeBoolean(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int expectedMaxLength(ByteBuf buf, Direction direction, ProtocolVersion version) {
|
||||
// Accommodate the rare (but likely malicious) use of UTF-8 usernames, since it is technically
|
||||
// legal on the protocol level.
|
||||
return 1 + (16 * 4);
|
||||
int base = 1 + (16 * 4);
|
||||
if (version.compareTo(ProtocolVersion.MINECRAFT_1_19) >= 0) {
|
||||
return -1;
|
||||
//TODO: ## 19
|
||||
}
|
||||
return base;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -18,11 +18,14 @@
|
||||
package com.velocitypowered.proxy.protocol.packet;
|
||||
|
||||
import com.velocitypowered.api.network.ProtocolVersion;
|
||||
import com.velocitypowered.api.util.GameProfile;
|
||||
import com.velocitypowered.api.util.UuidUtils;
|
||||
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
|
||||
import com.velocitypowered.proxy.protocol.MinecraftPacket;
|
||||
import com.velocitypowered.proxy.protocol.ProtocolUtils;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||
|
||||
@ -30,6 +33,7 @@ public class ServerLoginSuccess implements MinecraftPacket {
|
||||
|
||||
private @Nullable UUID uuid;
|
||||
private @Nullable String username;
|
||||
private @Nullable List<GameProfile.Property> properties;
|
||||
|
||||
public UUID getUuid() {
|
||||
if (uuid == null) {
|
||||
@ -53,17 +57,28 @@ public class ServerLoginSuccess implements MinecraftPacket {
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
public List<GameProfile.Property> getProperties() {
|
||||
return properties;
|
||||
}
|
||||
|
||||
public void setProperties(List<GameProfile.Property> properties) {
|
||||
this.properties = properties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ServerLoginSuccess{"
|
||||
+ "uuid=" + uuid
|
||||
+ ", username='" + username + '\''
|
||||
+ ", properties='" + properties + '\''
|
||||
+ '}';
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) {
|
||||
if (version.compareTo(ProtocolVersion.MINECRAFT_1_16) >= 0) {
|
||||
if (version.compareTo(ProtocolVersion.MINECRAFT_1_19) >= 0) {
|
||||
uuid = ProtocolUtils.readUuid(buf);
|
||||
} else if (version.compareTo(ProtocolVersion.MINECRAFT_1_16) >= 0) {
|
||||
uuid = ProtocolUtils.readUuidIntArray(buf);
|
||||
} else if (version.compareTo(ProtocolVersion.MINECRAFT_1_7_6) >= 0) {
|
||||
uuid = UUID.fromString(ProtocolUtils.readString(buf, 36));
|
||||
@ -71,6 +86,10 @@ public class ServerLoginSuccess implements MinecraftPacket {
|
||||
uuid = UuidUtils.fromUndashed(ProtocolUtils.readString(buf, 32));
|
||||
}
|
||||
username = ProtocolUtils.readString(buf, 16);
|
||||
|
||||
if (version.compareTo(ProtocolVersion.MINECRAFT_1_19) >= 0) {
|
||||
properties = ProtocolUtils.readProperties(buf);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -78,7 +97,9 @@ public class ServerLoginSuccess implements MinecraftPacket {
|
||||
if (uuid == null) {
|
||||
throw new IllegalStateException("No UUID specified!");
|
||||
}
|
||||
if (version.compareTo(ProtocolVersion.MINECRAFT_1_16) >= 0) {
|
||||
if (version.compareTo(ProtocolVersion.MINECRAFT_1_19) >= 0) {
|
||||
ProtocolUtils.writeUuid(buf, uuid);
|
||||
} else if (version.compareTo(ProtocolVersion.MINECRAFT_1_16) >= 0) {
|
||||
ProtocolUtils.writeUuidIntArray(buf, uuid);
|
||||
} else if (version.compareTo(ProtocolVersion.MINECRAFT_1_7_6) >= 0) {
|
||||
ProtocolUtils.writeString(buf, uuid.toString());
|
||||
@ -89,6 +110,14 @@ public class ServerLoginSuccess implements MinecraftPacket {
|
||||
throw new IllegalStateException("No username specified!");
|
||||
}
|
||||
ProtocolUtils.writeString(buf, username);
|
||||
|
||||
if (version.compareTo(ProtocolVersion.MINECRAFT_1_19) >= 0) {
|
||||
if (properties == null) {
|
||||
ProtocolUtils.writeVarInt(buf, 0);
|
||||
} else {
|
||||
ProtocolUtils.writeProperties(buf, properties);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -0,0 +1,99 @@
|
||||
/*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.velocitypowered.proxy.protocol.packet.brigadier;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.velocitypowered.api.network.ProtocolVersion;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||
|
||||
public class ArgumentIdentifier {
|
||||
|
||||
private final String identifier;
|
||||
private final Map<ProtocolVersion, Integer> versionById;
|
||||
|
||||
private ArgumentIdentifier(String identifier, VersionSet... versions) {
|
||||
this.identifier = Preconditions.checkNotNull(identifier);
|
||||
|
||||
Preconditions.checkNotNull(versions);
|
||||
|
||||
Map<ProtocolVersion, Integer> temp = new HashMap<>();
|
||||
|
||||
ProtocolVersion previous = null;
|
||||
for (int i = 0; i < versions.length; i++) {
|
||||
VersionSet current = Preconditions.checkNotNull(versions[i]);
|
||||
|
||||
Preconditions.checkArgument(current.getVersion().compareTo(ProtocolVersion.MINECRAFT_1_19) >= 0,
|
||||
"Version too old for ID index");
|
||||
Preconditions.checkArgument(previous == null || previous.compareTo(current.getVersion()) > 0,
|
||||
"Invalid protocol version order");
|
||||
|
||||
for (ProtocolVersion v : ProtocolVersion.values()) {
|
||||
if (v.compareTo(current.getVersion()) >= 0) {
|
||||
temp.put(v, current.getId());
|
||||
}
|
||||
}
|
||||
previous = current.getVersion();
|
||||
|
||||
}
|
||||
|
||||
this.versionById = ImmutableMap.copyOf(temp);
|
||||
}
|
||||
|
||||
public String getIdentifier() {
|
||||
return identifier;
|
||||
}
|
||||
|
||||
public @Nullable Integer getIdByProtocolVersion(ProtocolVersion version) {
|
||||
return versionById.get(Preconditions.checkNotNull(version));
|
||||
}
|
||||
|
||||
public static VersionSet mapSet(ProtocolVersion version, int id) {
|
||||
return new VersionSet(version, id);
|
||||
}
|
||||
|
||||
public static ArgumentIdentifier id(String identifier, VersionSet... versions) {
|
||||
return new ArgumentIdentifier(identifier, versions);
|
||||
}
|
||||
|
||||
/**
|
||||
* This class is purely for convenience.
|
||||
*/
|
||||
public static class VersionSet {
|
||||
private final ProtocolVersion version;
|
||||
private final int id;
|
||||
|
||||
private VersionSet(ProtocolVersion version, int id) {
|
||||
this.version = Preconditions.checkNotNull(version);
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public int getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public ProtocolVersion getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -17,6 +17,9 @@
|
||||
|
||||
package com.velocitypowered.proxy.protocol.packet.brigadier;
|
||||
|
||||
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_19;
|
||||
import static com.velocitypowered.proxy.protocol.packet.brigadier.ArgumentIdentifier.id;
|
||||
import static com.velocitypowered.proxy.protocol.packet.brigadier.ArgumentIdentifier.mapSet;
|
||||
import static com.velocitypowered.proxy.protocol.packet.brigadier.DoubleArgumentPropertySerializer.DOUBLE;
|
||||
import static com.velocitypowered.proxy.protocol.packet.brigadier.EmptyArgumentPropertySerializer.EMPTY;
|
||||
import static com.velocitypowered.proxy.protocol.packet.brigadier.FloatArgumentPropertySerializer.FLOAT;
|
||||
@ -25,6 +28,7 @@ import static com.velocitypowered.proxy.protocol.packet.brigadier.LongArgumentPr
|
||||
import static com.velocitypowered.proxy.protocol.packet.brigadier.ModArgumentPropertySerializer.MOD;
|
||||
import static com.velocitypowered.proxy.protocol.packet.brigadier.StringArgumentPropertySerializer.STRING;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.mojang.brigadier.arguments.ArgumentType;
|
||||
import com.mojang.brigadier.arguments.BoolArgumentType;
|
||||
import com.mojang.brigadier.arguments.DoubleArgumentType;
|
||||
@ -32,35 +36,38 @@ import com.mojang.brigadier.arguments.FloatArgumentType;
|
||||
import com.mojang.brigadier.arguments.IntegerArgumentType;
|
||||
import com.mojang.brigadier.arguments.LongArgumentType;
|
||||
import com.mojang.brigadier.arguments.StringArgumentType;
|
||||
import com.velocitypowered.api.network.ProtocolVersion;
|
||||
import com.velocitypowered.proxy.protocol.ProtocolUtils;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||
|
||||
public class ArgumentPropertyRegistry {
|
||||
private ArgumentPropertyRegistry() {
|
||||
throw new AssertionError();
|
||||
}
|
||||
|
||||
private static final Map<String, ArgumentPropertySerializer<?>> byId = new HashMap<>();
|
||||
private static final Map<ArgumentIdentifier, ArgumentPropertySerializer<?>> byIdentifier =
|
||||
new HashMap<>();
|
||||
private static final Map<Class<? extends ArgumentType>,
|
||||
ArgumentPropertySerializer<?>> byClass = new HashMap<>();
|
||||
private static final Map<Class<? extends ArgumentType>, String> classToId = new HashMap<>();
|
||||
private static final Map<Class<? extends ArgumentType>, ArgumentIdentifier> classToId =
|
||||
new HashMap<>();
|
||||
|
||||
private static <T extends ArgumentType<?>> void register(String identifier, Class<T> klazz,
|
||||
ArgumentPropertySerializer<T> serializer) {
|
||||
byId.put(identifier, serializer);
|
||||
private static <T extends ArgumentType<?>> void register(ArgumentIdentifier identifier,
|
||||
Class<T> klazz, ArgumentPropertySerializer<T> serializer) {
|
||||
byIdentifier.put(identifier, serializer);
|
||||
byClass.put(klazz, serializer);
|
||||
classToId.put(klazz, identifier);
|
||||
}
|
||||
|
||||
private static <T> void empty(String identifier) {
|
||||
private static <T> void empty(ArgumentIdentifier identifier) {
|
||||
empty(identifier, EMPTY);
|
||||
}
|
||||
|
||||
private static <T> void empty(String identifier, ArgumentPropertySerializer<T> serializer) {
|
||||
byId.put(identifier, serializer);
|
||||
private static <T> void empty(ArgumentIdentifier identifier,
|
||||
ArgumentPropertySerializer<T> serializer) {
|
||||
byIdentifier.put(identifier, serializer);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -68,13 +75,14 @@ public class ArgumentPropertyRegistry {
|
||||
* @param buf the buffer to deserialize
|
||||
* @return the deserialized {@link ArgumentType}
|
||||
*/
|
||||
public static ArgumentType<?> deserialize(ByteBuf buf) {
|
||||
String identifier = ProtocolUtils.readString(buf);
|
||||
ArgumentPropertySerializer<?> serializer = byId.get(identifier);
|
||||
public static ArgumentType<?> deserialize(ByteBuf buf, ProtocolVersion protocolVersion) {
|
||||
ArgumentIdentifier identifier = readIdentifier(buf, protocolVersion);
|
||||
|
||||
ArgumentPropertySerializer<?> serializer = byIdentifier.get(identifier);
|
||||
if (serializer == null) {
|
||||
throw new IllegalArgumentException("Argument type identifier " + identifier + " unknown.");
|
||||
}
|
||||
Object result = serializer.deserialize(buf);
|
||||
Object result = serializer.deserialize(buf, protocolVersion);
|
||||
|
||||
if (result instanceof ArgumentType) {
|
||||
return (ArgumentType<?>) result;
|
||||
@ -88,95 +96,144 @@ public class ArgumentPropertyRegistry {
|
||||
* @param buf the buffer to serialize into
|
||||
* @param type the type to serialize
|
||||
*/
|
||||
public static void serialize(ByteBuf buf, ArgumentType<?> type) {
|
||||
public static void serialize(ByteBuf buf, ArgumentType<?> type,
|
||||
ProtocolVersion protocolVersion) {
|
||||
if (type instanceof PassthroughProperty) {
|
||||
PassthroughProperty property = (PassthroughProperty) type;
|
||||
ProtocolUtils.writeString(buf, property.getIdentifier());
|
||||
writeIdentifier(buf, property.getIdentifier(), protocolVersion);
|
||||
if (property.getResult() != null) {
|
||||
property.getSerializer().serialize(property.getResult(), buf);
|
||||
property.getSerializer().serialize(property.getResult(), buf, protocolVersion);
|
||||
}
|
||||
} else if (type instanceof ModArgumentProperty) {
|
||||
ModArgumentProperty property = (ModArgumentProperty) type;
|
||||
ProtocolUtils.writeString(buf, property.getIdentifier());
|
||||
writeIdentifier(buf, property.getIdentifier(), protocolVersion);
|
||||
buf.writeBytes(property.getData());
|
||||
} else {
|
||||
ArgumentPropertySerializer serializer = byClass.get(type.getClass());
|
||||
String id = classToId.get(type.getClass());
|
||||
ArgumentIdentifier id = classToId.get(type.getClass());
|
||||
if (serializer == null || id == null) {
|
||||
throw new IllegalArgumentException("Don't know how to serialize "
|
||||
+ type.getClass().getName());
|
||||
}
|
||||
ProtocolUtils.writeString(buf, id);
|
||||
serializer.serialize(type, buf);
|
||||
writeIdentifier(buf, id, protocolVersion);
|
||||
serializer.serialize(type, buf, protocolVersion);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the {@link ArgumentIdentifier} to a version-specific buffer.
|
||||
* @param buf the buffer to write to
|
||||
* @param identifier the identifier to write
|
||||
* @param protocolVersion the protocol version to use
|
||||
*/
|
||||
public static void writeIdentifier(ByteBuf buf, ArgumentIdentifier identifier,
|
||||
ProtocolVersion protocolVersion) {
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_19) >= 0) {
|
||||
Integer id = identifier.getIdByProtocolVersion(protocolVersion);
|
||||
Preconditions.checkNotNull(id, "Don't know how to serialize type " + identifier);
|
||||
|
||||
ProtocolUtils.writeVarInt(buf, id);
|
||||
} else {
|
||||
ProtocolUtils.writeString(buf, identifier.getIdentifier());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the {@link ArgumentIdentifier} from a version-specific buffer.
|
||||
* @param buf the buffer to write to
|
||||
* @param protocolVersion the protocol version to use
|
||||
* @return the identifier read from the buffer
|
||||
*/
|
||||
public static ArgumentIdentifier readIdentifier(ByteBuf buf, ProtocolVersion protocolVersion) {
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_19) >= 0) {
|
||||
int id = ProtocolUtils.readVarInt(buf);
|
||||
for (ArgumentIdentifier i : byIdentifier.keySet()) {
|
||||
Integer v = i.getIdByProtocolVersion(protocolVersion);
|
||||
if (v != null && v == id) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
String identifier = ProtocolUtils.readString(buf);
|
||||
for (ArgumentIdentifier i : byIdentifier.keySet()) {
|
||||
if (i.getIdentifier().equals(identifier)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static {
|
||||
// Base Brigadier argument types
|
||||
register("brigadier:string", StringArgumentType.class, STRING);
|
||||
register("brigadier:integer", IntegerArgumentType.class, INTEGER);
|
||||
register("brigadier:float", FloatArgumentType.class, FLOAT);
|
||||
register("brigadier:double", DoubleArgumentType.class, DOUBLE);
|
||||
register("brigadier:bool", BoolArgumentType.class,
|
||||
register(id("brigadier:bool", mapSet(MINECRAFT_1_19, 0)), BoolArgumentType.class,
|
||||
new ArgumentPropertySerializer<>() {
|
||||
@Override
|
||||
public BoolArgumentType deserialize(ByteBuf buf) {
|
||||
public BoolArgumentType deserialize(ByteBuf buf, ProtocolVersion protocolVersion) {
|
||||
return BoolArgumentType.bool();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serialize(BoolArgumentType object, ByteBuf buf) {
|
||||
public void serialize(BoolArgumentType object, ByteBuf buf,
|
||||
ProtocolVersion protocolVersion) {
|
||||
|
||||
}
|
||||
});
|
||||
register("brigadier:long", LongArgumentType.class, LONG);
|
||||
register("minecraft:resource", RegistryKeyArgument.class, RegistryKeyArgumentSerializer.REGISTRY);
|
||||
register("minecraft:resource_or_tag", RegistryKeyArgument.class, RegistryKeyArgumentSerializer.REGISTRY);
|
||||
register(id("brigadier:float", mapSet(MINECRAFT_1_19, 1)), FloatArgumentType.class, FLOAT);
|
||||
register(id("brigadier:double", mapSet(MINECRAFT_1_19, 2)), DoubleArgumentType.class, DOUBLE);
|
||||
register(id("brigadier:integer", mapSet(MINECRAFT_1_19, 3)), IntegerArgumentType.class, INTEGER);
|
||||
register(id("brigadier:long", mapSet(MINECRAFT_1_19, 4)), LongArgumentType.class, LONG);
|
||||
register(id("brigadier:string", mapSet(MINECRAFT_1_19, 5)), StringArgumentType.class, STRING);
|
||||
|
||||
empty(id("minecraft:entity", mapSet(MINECRAFT_1_19, 6)), ByteArgumentPropertySerializer.BYTE);
|
||||
empty(id("minecraft:game_profile", mapSet(MINECRAFT_1_19, 7)));
|
||||
empty(id("minecraft:block_pos", mapSet(MINECRAFT_1_19, 8)));
|
||||
empty(id("minecraft:column_pos", mapSet(MINECRAFT_1_19, 9)));
|
||||
empty(id("minecraft:vec3", mapSet(MINECRAFT_1_19, 10)));
|
||||
empty(id("minecraft:vec2", mapSet(MINECRAFT_1_19, 11)));
|
||||
empty(id("minecraft:block_state", mapSet(MINECRAFT_1_19, 12)));
|
||||
empty(id("minecraft:block_predicate", mapSet(MINECRAFT_1_19, 13)));
|
||||
empty(id("minecraft:item_stack", mapSet(MINECRAFT_1_19, 14)));
|
||||
empty(id("minecraft:item_predicate", mapSet(MINECRAFT_1_19, 15)));
|
||||
empty(id("minecraft:color", mapSet(MINECRAFT_1_19, 16)));
|
||||
empty(id("minecraft:component", mapSet(MINECRAFT_1_19, 17)));
|
||||
empty(id("minecraft:message", mapSet(MINECRAFT_1_19, 18)));
|
||||
empty(id("minecraft:nbt_compound_tag", mapSet(MINECRAFT_1_19, 19))); // added in 1.14
|
||||
empty(id("minecraft:nbt_tag", mapSet(MINECRAFT_1_19, 20))); // added in 1.14
|
||||
empty(id("minecraft:nbt_path", mapSet(MINECRAFT_1_19, 21)));
|
||||
empty(id("minecraft:objective", mapSet(MINECRAFT_1_19, 22)));
|
||||
empty(id("minecraft:objective_criteria", mapSet(MINECRAFT_1_19, 23)));
|
||||
empty(id("minecraft:operation", mapSet(MINECRAFT_1_19, 24)));
|
||||
empty(id("minecraft:particle", mapSet(MINECRAFT_1_19, 25)));
|
||||
empty(id("minecraft:angle", mapSet(MINECRAFT_1_19, 26))); // added in 1.16.2
|
||||
empty(id("minecraft:rotation", mapSet(MINECRAFT_1_19, 27)));
|
||||
empty(id("minecraft:scoreboard_slot", mapSet(MINECRAFT_1_19, 28)));
|
||||
empty(id("minecraft:score_holder", mapSet(MINECRAFT_1_19, 29)), ByteArgumentPropertySerializer.BYTE);
|
||||
empty(id("minecraft:swizzle", mapSet(MINECRAFT_1_19, 30)));
|
||||
empty(id("minecraft:team", mapSet(MINECRAFT_1_19, 31)));
|
||||
empty(id("minecraft:item_slot", mapSet(MINECRAFT_1_19, 32)));
|
||||
empty(id("minecraft:resource_location", mapSet(MINECRAFT_1_19, 33)));
|
||||
empty(id("minecraft:mob_effect", mapSet(MINECRAFT_1_19, 34)));
|
||||
empty(id("minecraft:function", mapSet(MINECRAFT_1_19, 35)));
|
||||
empty(id("minecraft:entity_anchor", mapSet(MINECRAFT_1_19, 36)));
|
||||
empty(id("minecraft:int_range", mapSet(MINECRAFT_1_19, 37)));
|
||||
empty(id("minecraft:float_range", mapSet(MINECRAFT_1_19, 38)));
|
||||
empty(id("minecraft:item_enchantment", mapSet(MINECRAFT_1_19, 39)));
|
||||
empty(id("minecraft:entity_summon", mapSet(MINECRAFT_1_19, 40)));
|
||||
empty(id("minecraft:dimension", mapSet(MINECRAFT_1_19, 41)));
|
||||
empty(id("minecraft:time", mapSet(MINECRAFT_1_19, 42))); // added in 1.14
|
||||
|
||||
register(id("minecraft:resource_or_tag", mapSet(MINECRAFT_1_19, 43)),
|
||||
RegistryKeyArgument.class, RegistryKeyArgumentSerializer.REGISTRY);
|
||||
register(id("minecraft:resource", mapSet(MINECRAFT_1_19, 44)),
|
||||
RegistryKeyArgument.class, RegistryKeyArgumentSerializer.REGISTRY);
|
||||
|
||||
empty(id("minecraft:uuid", mapSet(MINECRAFT_1_19, 45))); // added in 1.16
|
||||
|
||||
// Crossstitch support
|
||||
register("crossstitch:mod_argument", ModArgumentProperty.class, MOD);
|
||||
register(id("crossstitch:mod_argument", mapSet(MINECRAFT_1_19, -256)), ModArgumentProperty.class, MOD);
|
||||
|
||||
// Minecraft argument types with extra properties
|
||||
empty("minecraft:entity", ByteArgumentPropertySerializer.BYTE);
|
||||
empty("minecraft:score_holder", ByteArgumentPropertySerializer.BYTE);
|
||||
|
||||
// Minecraft argument types
|
||||
empty("minecraft:game_profile");
|
||||
empty("minecraft:block_pos");
|
||||
empty("minecraft:column_pos");
|
||||
empty("minecraft:vec3");
|
||||
empty("minecraft:vec2");
|
||||
empty("minecraft:block_state");
|
||||
empty("minecraft:block_predicate");
|
||||
empty("minecraft:item_stack");
|
||||
empty("minecraft:item_predicate");
|
||||
empty("minecraft:color");
|
||||
empty("minecraft:component");
|
||||
empty("minecraft:message");
|
||||
empty("minecraft:nbt");
|
||||
empty("minecraft:nbt_compound_tag"); // added in 1.14
|
||||
empty("minecraft:nbt_tag"); // added in 1.14
|
||||
empty("minecraft:nbt_path");
|
||||
empty("minecraft:objective");
|
||||
empty("minecraft:objective_criteria");
|
||||
empty("minecraft:operation");
|
||||
empty("minecraft:particle");
|
||||
empty("minecraft:rotation");
|
||||
empty("minecraft:scoreboard_slot");
|
||||
empty("minecraft:swizzle");
|
||||
empty("minecraft:team");
|
||||
empty("minecraft:item_slot");
|
||||
empty("minecraft:resource_location");
|
||||
empty("minecraft:mob_effect");
|
||||
empty("minecraft:function");
|
||||
empty("minecraft:entity_anchor");
|
||||
empty("minecraft:item_enchantment");
|
||||
empty("minecraft:entity_summon");
|
||||
empty("minecraft:dimension");
|
||||
empty("minecraft:int_range");
|
||||
empty("minecraft:float_range");
|
||||
empty("minecraft:time"); // added in 1.14
|
||||
empty("minecraft:uuid"); // added in 1.16
|
||||
empty("minecraft:angle"); // added in 1.16.2
|
||||
empty(id("minecraft:nbt")); // No longer in 1.19+
|
||||
}
|
||||
}
|
||||
|
@ -17,11 +17,12 @@
|
||||
|
||||
package com.velocitypowered.proxy.protocol.packet.brigadier;
|
||||
|
||||
import com.velocitypowered.api.network.ProtocolVersion;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||
|
||||
public interface ArgumentPropertySerializer<T> {
|
||||
@Nullable T deserialize(ByteBuf buf);
|
||||
@Nullable T deserialize(ByteBuf buf, ProtocolVersion protocolVersion);
|
||||
|
||||
void serialize(T object, ByteBuf buf);
|
||||
void serialize(T object, ByteBuf buf, ProtocolVersion protocolVersion);
|
||||
}
|
||||
|
@ -17,6 +17,7 @@
|
||||
|
||||
package com.velocitypowered.proxy.protocol.packet.brigadier;
|
||||
|
||||
import com.velocitypowered.api.network.ProtocolVersion;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
|
||||
class ByteArgumentPropertySerializer implements ArgumentPropertySerializer<Byte> {
|
||||
@ -28,12 +29,12 @@ class ByteArgumentPropertySerializer implements ArgumentPropertySerializer<Byte>
|
||||
}
|
||||
|
||||
@Override
|
||||
public Byte deserialize(ByteBuf buf) {
|
||||
public Byte deserialize(ByteBuf buf, ProtocolVersion protocolVersion) {
|
||||
return buf.readByte();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serialize(Byte object, ByteBuf buf) {
|
||||
public void serialize(Byte object, ByteBuf buf, ProtocolVersion protocolVersion) {
|
||||
buf.writeByte(object);
|
||||
}
|
||||
}
|
||||
|
@ -22,6 +22,7 @@ import static com.velocitypowered.proxy.protocol.packet.brigadier.IntegerArgumen
|
||||
import static com.velocitypowered.proxy.protocol.packet.brigadier.IntegerArgumentPropertySerializer.getFlags;
|
||||
|
||||
import com.mojang.brigadier.arguments.DoubleArgumentType;
|
||||
import com.velocitypowered.api.network.ProtocolVersion;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
|
||||
class DoubleArgumentPropertySerializer implements ArgumentPropertySerializer<DoubleArgumentType> {
|
||||
@ -32,7 +33,7 @@ class DoubleArgumentPropertySerializer implements ArgumentPropertySerializer<Dou
|
||||
}
|
||||
|
||||
@Override
|
||||
public DoubleArgumentType deserialize(ByteBuf buf) {
|
||||
public DoubleArgumentType deserialize(ByteBuf buf, ProtocolVersion protocolVersion) {
|
||||
byte flags = buf.readByte();
|
||||
double minimum = (flags & HAS_MINIMUM) != 0 ? buf.readDouble() : Double.MIN_VALUE;
|
||||
double maximum = (flags & HAS_MAXIMUM) != 0 ? buf.readDouble() : Double.MAX_VALUE;
|
||||
@ -40,7 +41,7 @@ class DoubleArgumentPropertySerializer implements ArgumentPropertySerializer<Dou
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serialize(DoubleArgumentType object, ByteBuf buf) {
|
||||
public void serialize(DoubleArgumentType object, ByteBuf buf, ProtocolVersion protocolVersion) {
|
||||
boolean hasMinimum = Double.compare(object.getMinimum(), Double.MIN_VALUE) != 0;
|
||||
boolean hasMaximum = Double.compare(object.getMaximum(), Double.MAX_VALUE) != 0;
|
||||
byte flag = getFlags(hasMinimum, hasMaximum);
|
||||
|
@ -17,6 +17,7 @@
|
||||
|
||||
package com.velocitypowered.proxy.protocol.packet.brigadier;
|
||||
|
||||
import com.velocitypowered.api.network.ProtocolVersion;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||
|
||||
@ -32,12 +33,12 @@ class EmptyArgumentPropertySerializer implements ArgumentPropertySerializer<Void
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Void deserialize(ByteBuf buf) {
|
||||
public @Nullable Void deserialize(ByteBuf buf, ProtocolVersion protocolVersion) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serialize(Void object, ByteBuf buf) {
|
||||
public void serialize(Void object, ByteBuf buf, ProtocolVersion protocolVersion) {
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -22,6 +22,7 @@ import static com.velocitypowered.proxy.protocol.packet.brigadier.IntegerArgumen
|
||||
import static com.velocitypowered.proxy.protocol.packet.brigadier.IntegerArgumentPropertySerializer.getFlags;
|
||||
|
||||
import com.mojang.brigadier.arguments.FloatArgumentType;
|
||||
import com.velocitypowered.api.network.ProtocolVersion;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
|
||||
class FloatArgumentPropertySerializer implements ArgumentPropertySerializer<FloatArgumentType> {
|
||||
@ -33,7 +34,7 @@ class FloatArgumentPropertySerializer implements ArgumentPropertySerializer<Floa
|
||||
}
|
||||
|
||||
@Override
|
||||
public FloatArgumentType deserialize(ByteBuf buf) {
|
||||
public FloatArgumentType deserialize(ByteBuf buf, ProtocolVersion protocolVersion) {
|
||||
byte flags = buf.readByte();
|
||||
float minimum = (flags & HAS_MINIMUM) != 0 ? buf.readFloat() : Float.MIN_VALUE;
|
||||
float maximum = (flags & HAS_MAXIMUM) != 0 ? buf.readFloat() : Float.MAX_VALUE;
|
||||
@ -41,7 +42,7 @@ class FloatArgumentPropertySerializer implements ArgumentPropertySerializer<Floa
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serialize(FloatArgumentType object, ByteBuf buf) {
|
||||
public void serialize(FloatArgumentType object, ByteBuf buf, ProtocolVersion protocolVersion) {
|
||||
boolean hasMinimum = Float.compare(object.getMinimum(), Float.MIN_VALUE) != 0;
|
||||
boolean hasMaximum = Float.compare(object.getMaximum(), Float.MAX_VALUE) != 0;
|
||||
byte flag = getFlags(hasMinimum, hasMaximum);
|
||||
|
@ -18,6 +18,7 @@
|
||||
package com.velocitypowered.proxy.protocol.packet.brigadier;
|
||||
|
||||
import com.mojang.brigadier.arguments.IntegerArgumentType;
|
||||
import com.velocitypowered.api.network.ProtocolVersion;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
|
||||
class IntegerArgumentPropertySerializer implements ArgumentPropertySerializer<IntegerArgumentType> {
|
||||
@ -32,7 +33,7 @@ class IntegerArgumentPropertySerializer implements ArgumentPropertySerializer<In
|
||||
}
|
||||
|
||||
@Override
|
||||
public IntegerArgumentType deserialize(ByteBuf buf) {
|
||||
public IntegerArgumentType deserialize(ByteBuf buf, ProtocolVersion protocolVersion) {
|
||||
byte flags = buf.readByte();
|
||||
int minimum = (flags & HAS_MINIMUM) != 0 ? buf.readInt() : Integer.MIN_VALUE;
|
||||
int maximum = (flags & HAS_MAXIMUM) != 0 ? buf.readInt() : Integer.MAX_VALUE;
|
||||
@ -40,7 +41,7 @@ class IntegerArgumentPropertySerializer implements ArgumentPropertySerializer<In
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serialize(IntegerArgumentType object, ByteBuf buf) {
|
||||
public void serialize(IntegerArgumentType object, ByteBuf buf, ProtocolVersion protocolVersion) {
|
||||
boolean hasMinimum = object.getMinimum() != Integer.MIN_VALUE;
|
||||
boolean hasMaximum = object.getMaximum() != Integer.MAX_VALUE;
|
||||
byte flag = getFlags(hasMinimum, hasMaximum);
|
||||
|
@ -22,6 +22,7 @@ import static com.velocitypowered.proxy.protocol.packet.brigadier.IntegerArgumen
|
||||
import static com.velocitypowered.proxy.protocol.packet.brigadier.IntegerArgumentPropertySerializer.getFlags;
|
||||
|
||||
import com.mojang.brigadier.arguments.LongArgumentType;
|
||||
import com.velocitypowered.api.network.ProtocolVersion;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
|
||||
class LongArgumentPropertySerializer implements ArgumentPropertySerializer<LongArgumentType> {
|
||||
@ -33,7 +34,7 @@ class LongArgumentPropertySerializer implements ArgumentPropertySerializer<LongA
|
||||
}
|
||||
|
||||
@Override
|
||||
public LongArgumentType deserialize(ByteBuf buf) {
|
||||
public LongArgumentType deserialize(ByteBuf buf, ProtocolVersion protocolVersion) {
|
||||
byte flags = buf.readByte();
|
||||
long minimum = (flags & HAS_MINIMUM) != 0 ? buf.readLong() : Long.MIN_VALUE;
|
||||
long maximum = (flags & HAS_MAXIMUM) != 0 ? buf.readLong() : Long.MAX_VALUE;
|
||||
@ -41,7 +42,7 @@ class LongArgumentPropertySerializer implements ArgumentPropertySerializer<LongA
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serialize(LongArgumentType object, ByteBuf buf) {
|
||||
public void serialize(LongArgumentType object, ByteBuf buf, ProtocolVersion protocolVersion) {
|
||||
boolean hasMinimum = object.getMinimum() != Long.MIN_VALUE;
|
||||
boolean hasMaximum = object.getMaximum() != Long.MAX_VALUE;
|
||||
byte flag = getFlags(hasMinimum, hasMaximum);
|
||||
|
@ -30,15 +30,15 @@ import java.util.concurrent.CompletableFuture;
|
||||
|
||||
public class ModArgumentProperty implements ArgumentType<ByteBuf> {
|
||||
|
||||
private final String identifier;
|
||||
private final ArgumentIdentifier identifier;
|
||||
private final ByteBuf data;
|
||||
|
||||
public ModArgumentProperty(String identifier, ByteBuf data) {
|
||||
public ModArgumentProperty(ArgumentIdentifier identifier, ByteBuf data) {
|
||||
this.identifier = identifier;
|
||||
this.data = Unpooled.unreleasableBuffer(data.asReadOnly());
|
||||
}
|
||||
|
||||
public String getIdentifier() {
|
||||
public ArgumentIdentifier getIdentifier() {
|
||||
return identifier;
|
||||
}
|
||||
|
||||
|
@ -17,6 +17,7 @@
|
||||
|
||||
package com.velocitypowered.proxy.protocol.packet.brigadier;
|
||||
|
||||
import com.velocitypowered.api.network.ProtocolVersion;
|
||||
import com.velocitypowered.proxy.protocol.ProtocolUtils;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.Unpooled;
|
||||
@ -30,14 +31,22 @@ class ModArgumentPropertySerializer implements ArgumentPropertySerializer<ModArg
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable ModArgumentProperty deserialize(ByteBuf buf) {
|
||||
String identifier = ProtocolUtils.readString(buf);
|
||||
public @Nullable ModArgumentProperty deserialize(ByteBuf buf, ProtocolVersion version) {
|
||||
ArgumentIdentifier identifier;
|
||||
if (version.compareTo(ProtocolVersion.MINECRAFT_1_19) >= 0) {
|
||||
int idx = ProtocolUtils.readVarInt(buf);
|
||||
identifier = ArgumentIdentifier.id("crossstitch:identified_" + (idx < 0 ? "n" + (-idx) : idx),
|
||||
ArgumentIdentifier.mapSet(version, idx));
|
||||
} else {
|
||||
identifier = ArgumentIdentifier.id(ProtocolUtils.readString(buf));
|
||||
}
|
||||
|
||||
byte[] extraData = ProtocolUtils.readByteArray(buf);
|
||||
return new ModArgumentProperty(identifier, Unpooled.wrappedBuffer(extraData));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serialize(ModArgumentProperty object, ByteBuf buf) {
|
||||
public void serialize(ModArgumentProperty object, ByteBuf buf, ProtocolVersion version) {
|
||||
// This is special-cased by ArgumentPropertyRegistry
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
@ -23,18 +23,18 @@ import org.checkerframework.checker.nullness.qual.Nullable;
|
||||
|
||||
class PassthroughProperty<T> implements ArgumentType<T> {
|
||||
|
||||
private final String identifier;
|
||||
private final ArgumentIdentifier identifier;
|
||||
private final ArgumentPropertySerializer<T> serializer;
|
||||
private final @Nullable T result;
|
||||
|
||||
PassthroughProperty(String identifier, ArgumentPropertySerializer<T> serializer,
|
||||
PassthroughProperty(ArgumentIdentifier identifier, ArgumentPropertySerializer<T> serializer,
|
||||
@Nullable T result) {
|
||||
this.identifier = identifier;
|
||||
this.serializer = serializer;
|
||||
this.result = result;
|
||||
}
|
||||
|
||||
public String getIdentifier() {
|
||||
public ArgumentIdentifier getIdentifier() {
|
||||
return identifier;
|
||||
}
|
||||
|
||||
|
@ -17,6 +17,7 @@
|
||||
|
||||
package com.velocitypowered.proxy.protocol.packet.brigadier;
|
||||
|
||||
import com.velocitypowered.api.network.ProtocolVersion;
|
||||
import com.velocitypowered.proxy.protocol.ProtocolUtils;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
@ -25,12 +26,12 @@ public class RegistryKeyArgumentSerializer implements ArgumentPropertySerializer
|
||||
static final RegistryKeyArgumentSerializer REGISTRY = new RegistryKeyArgumentSerializer();
|
||||
|
||||
@Override
|
||||
public RegistryKeyArgument deserialize(ByteBuf buf) {
|
||||
public RegistryKeyArgument deserialize(ByteBuf buf, ProtocolVersion protocolVersion) {
|
||||
return new RegistryKeyArgument(ProtocolUtils.readString(buf));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serialize(RegistryKeyArgument object, ByteBuf buf) {
|
||||
public void serialize(RegistryKeyArgument object, ByteBuf buf, ProtocolVersion protocolVersion) {
|
||||
ProtocolUtils.writeString(buf, object.getIdentifier());
|
||||
}
|
||||
}
|
||||
|
@ -18,6 +18,7 @@
|
||||
package com.velocitypowered.proxy.protocol.packet.brigadier;
|
||||
|
||||
import com.mojang.brigadier.arguments.StringArgumentType;
|
||||
import com.velocitypowered.api.network.ProtocolVersion;
|
||||
import com.velocitypowered.proxy.protocol.ProtocolUtils;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
|
||||
@ -34,7 +35,7 @@ class StringArgumentPropertySerializer implements ArgumentPropertySerializer<Str
|
||||
}
|
||||
|
||||
@Override
|
||||
public StringArgumentType deserialize(ByteBuf buf) {
|
||||
public StringArgumentType deserialize(ByteBuf buf, ProtocolVersion protocolVersion) {
|
||||
int type = ProtocolUtils.readVarInt(buf);
|
||||
switch (type) {
|
||||
case 0:
|
||||
@ -49,7 +50,7 @@ class StringArgumentPropertySerializer implements ArgumentPropertySerializer<Str
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serialize(StringArgumentType object, ByteBuf buf) {
|
||||
public void serialize(StringArgumentType object, ByteBuf buf, ProtocolVersion protocolVersion) {
|
||||
switch (object.getType()) {
|
||||
case SINGLE_WORD:
|
||||
ProtocolUtils.writeVarInt(buf, 0);
|
||||
|
@ -0,0 +1,182 @@
|
||||
/*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.velocitypowered.proxy.protocol.packet.chat;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.velocitypowered.api.network.ProtocolVersion;
|
||||
import com.velocitypowered.api.proxy.Player;
|
||||
import com.velocitypowered.proxy.crypto.SignedChatCommand;
|
||||
import com.velocitypowered.proxy.crypto.SignedChatMessage;
|
||||
import com.velocitypowered.proxy.protocol.MinecraftPacket;
|
||||
import com.velocitypowered.proxy.protocol.ProtocolUtils;
|
||||
import java.time.Instant;
|
||||
import java.util.UUID;
|
||||
import net.kyori.adventure.identity.Identity;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||
|
||||
public class ChatBuilder {
|
||||
|
||||
private final ProtocolVersion version;
|
||||
|
||||
private @MonotonicNonNull Component component;
|
||||
private @MonotonicNonNull String message;
|
||||
private @MonotonicNonNull SignedChatMessage signedChatMessage;
|
||||
private @MonotonicNonNull SignedChatCommand signedCommand;
|
||||
|
||||
private @Nullable Player sender;
|
||||
private @Nullable Identity senderIdentity;
|
||||
|
||||
private ChatType type = ChatType.CHAT;
|
||||
|
||||
private ChatBuilder(ProtocolVersion version) {
|
||||
this.version = version;
|
||||
}
|
||||
|
||||
public static ChatBuilder builder(ProtocolVersion version) {
|
||||
return new ChatBuilder(Preconditions.checkNotNull(version));
|
||||
}
|
||||
|
||||
public ChatBuilder component(Component message) {
|
||||
this.component = Preconditions.checkNotNull(message);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the message to the provided message.
|
||||
*
|
||||
* @param message The message for the chat.
|
||||
* @return {@code this}
|
||||
*/
|
||||
public ChatBuilder message(String message) {
|
||||
Preconditions.checkArgument(this.message == null);
|
||||
this.message = Preconditions.checkNotNull(message);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the signed message to the provided message.
|
||||
*
|
||||
* @param message The signed message for the chat.
|
||||
* @return {@code this}
|
||||
*/
|
||||
public ChatBuilder message(SignedChatMessage message) {
|
||||
Preconditions.checkNotNull(message);
|
||||
Preconditions.checkArgument(this.message == null);
|
||||
this.message = message.getMessage();
|
||||
this.signedChatMessage = message;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the signed command to the provided command.
|
||||
*
|
||||
* @param command The signed command for the chat.
|
||||
* @return {@code this}
|
||||
*/
|
||||
public ChatBuilder message(SignedChatCommand command) {
|
||||
Preconditions.checkNotNull(command);
|
||||
Preconditions.checkArgument(this.message == null);
|
||||
this.message = command.getBaseCommand();
|
||||
this.signedCommand = command;
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
public ChatBuilder setType(ChatType type) {
|
||||
this.type = type;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ChatBuilder asPlayer(@Nullable Player player) {
|
||||
this.sender = player;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ChatBuilder forIdentity(@Nullable Identity identity) {
|
||||
this.senderIdentity = identity;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ChatBuilder asServer() {
|
||||
this.sender = null;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link MinecraftPacket} which can be sent to the client; using the provided information in the builder.
|
||||
*
|
||||
* @return The {@link MinecraftPacket} to send to the client.
|
||||
*/
|
||||
public MinecraftPacket toClient() {
|
||||
// This is temporary
|
||||
UUID identity = sender == null ? (senderIdentity == null ? Identity.nil().uuid()
|
||||
: senderIdentity.uuid()) : sender.getUniqueId();
|
||||
Component msg = component == null ? Component.text(message) : component;
|
||||
|
||||
if (version.compareTo(ProtocolVersion.MINECRAFT_1_19) >= 0) {
|
||||
return new SystemChat(msg, type.getId());
|
||||
} else {
|
||||
return new LegacyChat(ProtocolUtils.getJsonChatSerializer(version).serialize(msg), type.getId(), identity);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link MinecraftPacket} which can be sent to the server; using the provided information in the builder.
|
||||
*
|
||||
* @return The {@link MinecraftPacket} to send to the server.
|
||||
*/
|
||||
public MinecraftPacket toServer() {
|
||||
if (version.compareTo(ProtocolVersion.MINECRAFT_1_19) >= 0) {
|
||||
if (signedChatMessage != null) {
|
||||
return new PlayerChat(signedChatMessage);
|
||||
} else if (signedCommand != null) {
|
||||
return new PlayerCommand(signedCommand);
|
||||
} else {
|
||||
// Well crap
|
||||
if (message.startsWith("/")) {
|
||||
return new PlayerCommand(message.substring(1), ImmutableList.of(), Instant.now());
|
||||
} else {
|
||||
// This will produce an error on the server, but needs to be here.
|
||||
return new PlayerChat(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
LegacyChat chat = new LegacyChat();
|
||||
chat.setMessage(message);
|
||||
return chat;
|
||||
}
|
||||
|
||||
public static enum ChatType {
|
||||
CHAT((byte) 0),
|
||||
SYSTEM((byte) 1),
|
||||
GAME_INFO((byte) 2);
|
||||
|
||||
private final byte raw;
|
||||
|
||||
ChatType(byte raw) {
|
||||
this.raw = raw;
|
||||
}
|
||||
|
||||
public byte getId() {
|
||||
return raw;
|
||||
}
|
||||
}
|
||||
}
|
@ -15,7 +15,7 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.velocitypowered.proxy.protocol.packet;
|
||||
package com.velocitypowered.proxy.protocol.packet.chat;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.velocitypowered.api.network.ProtocolVersion;
|
||||
@ -23,12 +23,11 @@ import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
|
||||
import com.velocitypowered.proxy.protocol.MinecraftPacket;
|
||||
import com.velocitypowered.proxy.protocol.ProtocolUtils;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import java.util.UUID;
|
||||
import net.kyori.adventure.identity.Identity;
|
||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public class Chat implements MinecraftPacket {
|
||||
public class LegacyChat implements MinecraftPacket {
|
||||
|
||||
public static final byte CHAT_TYPE = (byte) 0;
|
||||
public static final byte SYSTEM_TYPE = (byte) 1;
|
||||
@ -41,15 +40,21 @@ public class Chat implements MinecraftPacket {
|
||||
private byte type;
|
||||
private @Nullable UUID sender;
|
||||
|
||||
public Chat() {
|
||||
public LegacyChat() {
|
||||
}
|
||||
|
||||
public Chat(String message, byte type, UUID sender) {
|
||||
/**
|
||||
* Creates a Chat packet.
|
||||
*/
|
||||
public LegacyChat(String message, byte type, UUID sender) {
|
||||
this.message = message;
|
||||
this.type = type;
|
||||
this.sender = sender;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the Chat message.
|
||||
*/
|
||||
public String getMessage() {
|
||||
if (message == null) {
|
||||
throw new IllegalStateException("Message is not specified");
|
||||
@ -115,20 +120,4 @@ public class Chat implements MinecraftPacket {
|
||||
public boolean handle(MinecraftSessionHandler handler) {
|
||||
return handler.handle(this);
|
||||
}
|
||||
|
||||
public static Chat createClientbound(Identity identity,
|
||||
net.kyori.adventure.text.Component component, ProtocolVersion version) {
|
||||
return createClientbound(component, CHAT_TYPE, identity.uuid(), version);
|
||||
}
|
||||
|
||||
public static Chat createClientbound(net.kyori.adventure.text.Component component, byte type,
|
||||
UUID sender, ProtocolVersion version) {
|
||||
Preconditions.checkNotNull(component, "component");
|
||||
return new Chat(ProtocolUtils.getJsonChatSerializer(version).serialize(component), type,
|
||||
sender);
|
||||
}
|
||||
|
||||
public static Chat createServerbound(String message) {
|
||||
return new Chat(message, CHAT_TYPE, EMPTY_SENDER);
|
||||
}
|
||||
}
|
@ -0,0 +1,139 @@
|
||||
/*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.velocitypowered.proxy.protocol.packet.chat;
|
||||
|
||||
import com.google.common.primitives.Longs;
|
||||
import com.velocitypowered.api.network.ProtocolVersion;
|
||||
import com.velocitypowered.api.proxy.crypto.IdentifiedKey;
|
||||
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
|
||||
import com.velocitypowered.proxy.crypto.EncryptionUtils;
|
||||
import com.velocitypowered.proxy.crypto.SignedChatMessage;
|
||||
import com.velocitypowered.proxy.protocol.MinecraftPacket;
|
||||
import com.velocitypowered.proxy.protocol.ProtocolUtils;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import java.time.Instant;
|
||||
import java.util.UUID;
|
||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||
|
||||
public class PlayerChat implements MinecraftPacket {
|
||||
|
||||
private String message;
|
||||
private boolean signedPreview;
|
||||
private boolean unsigned = false;
|
||||
private @Nullable Instant expiry;
|
||||
private @Nullable byte[] signature;
|
||||
private @Nullable byte[] salt;
|
||||
|
||||
public PlayerChat() {
|
||||
}
|
||||
|
||||
public PlayerChat(String message) {
|
||||
this.message = message;
|
||||
this.unsigned = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create new {@link PlayerChat} based on a previously {@link SignedChatMessage}.
|
||||
*
|
||||
* @param message The {@link SignedChatMessage} to turn into {@link PlayerChat}.
|
||||
*/
|
||||
public PlayerChat(SignedChatMessage message) {
|
||||
this.message = message.getMessage();
|
||||
this.expiry = message.getExpiryTemporal();
|
||||
this.salt = message.getSalt();
|
||||
this.signature = message.getSignature();
|
||||
this.signedPreview = message.isPreviewSigned();
|
||||
}
|
||||
|
||||
public Instant getExpiry() {
|
||||
return expiry;
|
||||
}
|
||||
|
||||
public boolean isUnsigned() {
|
||||
return unsigned;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
public boolean isSignedPreview() {
|
||||
return signedPreview;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion protocolVersion) {
|
||||
message = ProtocolUtils.readString(buf, 256);
|
||||
|
||||
long expiresAt = buf.readLong();
|
||||
long saltLong = buf.readLong();
|
||||
byte[] signatureBytes = ProtocolUtils.readByteArray(buf);
|
||||
|
||||
if (saltLong != 0L && signatureBytes.length > 0) {
|
||||
salt = Longs.toByteArray(saltLong);
|
||||
signature = signatureBytes;
|
||||
expiry = Instant.ofEpochMilli(expiresAt);
|
||||
} else if (saltLong == 0L && signature.length == 0) {
|
||||
unsigned = true;
|
||||
} else {
|
||||
throw EncryptionUtils.INVALID_SIGNATURE;
|
||||
}
|
||||
|
||||
signedPreview = buf.readBoolean();
|
||||
if (signedPreview && unsigned) {
|
||||
throw EncryptionUtils.PREVIEW_SIGNATURE_MISSING;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void encode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion protocolVersion) {
|
||||
ProtocolUtils.writeString(buf, message);
|
||||
|
||||
buf.writeLong(unsigned ? Instant.now().toEpochMilli() : expiry.toEpochMilli());
|
||||
buf.writeLong(unsigned ? 0L : Longs.fromByteArray(salt));
|
||||
ProtocolUtils.writeByteArray(buf, unsigned ? EncryptionUtils.EMPTY : signature);
|
||||
|
||||
buf.writeBoolean(signedPreview);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a signature and creates a {@link SignedChatMessage} from the given signature.
|
||||
*
|
||||
* @param signer the signer's information
|
||||
* @param sender the sender of the message
|
||||
* @param mustSign instructs the function to throw if the signature is invalid.
|
||||
* @return The {@link SignedChatMessage} or null if the signature couldn't be verified.
|
||||
* @throws com.velocitypowered.proxy.util.except.QuietDecoderException when mustSign is {@code true} and the signature
|
||||
* is invalid.
|
||||
*/
|
||||
public SignedChatMessage signedContainer(IdentifiedKey signer, UUID sender, boolean mustSign) {
|
||||
if (unsigned) {
|
||||
if (mustSign) {
|
||||
throw EncryptionUtils.INVALID_SIGNATURE;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
return new SignedChatMessage(message, signer.getSignedPublicKey(), sender, expiry, signature, salt, signedPreview);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean handle(MinecraftSessionHandler handler) {
|
||||
return handler.handle(this);
|
||||
}
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
/*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.velocitypowered.proxy.protocol.packet.chat;
|
||||
|
||||
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 io.netty.buffer.ByteBuf;
|
||||
|
||||
public class PlayerChatPreview implements MinecraftPacket {
|
||||
|
||||
private int id;
|
||||
private String query;
|
||||
|
||||
public int getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getQuery() {
|
||||
return query;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion protocolVersion) {
|
||||
id = buf.readInt();
|
||||
query = ProtocolUtils.readString(buf, 256);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void encode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion protocolVersion) {
|
||||
buf.writeInt(id);
|
||||
ProtocolUtils.writeString(buf, query);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean handle(MinecraftSessionHandler handler) {
|
||||
return handler.handle(this);
|
||||
}
|
||||
}
|
@ -0,0 +1,177 @@
|
||||
/*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.velocitypowered.proxy.protocol.packet.chat;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.primitives.Longs;
|
||||
import com.velocitypowered.api.network.ProtocolVersion;
|
||||
import com.velocitypowered.api.proxy.crypto.IdentifiedKey;
|
||||
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
|
||||
import com.velocitypowered.proxy.crypto.EncryptionUtils;
|
||||
import com.velocitypowered.proxy.crypto.SignedChatCommand;
|
||||
import com.velocitypowered.proxy.crypto.SignedChatMessage;
|
||||
import com.velocitypowered.proxy.protocol.MinecraftPacket;
|
||||
import com.velocitypowered.proxy.protocol.ProtocolUtils;
|
||||
import com.velocitypowered.proxy.util.except.QuietDecoderException;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
public class PlayerCommand implements MinecraftPacket {
|
||||
|
||||
private static final int MAX_NUM_ARGUMENTS = 8;
|
||||
private static final int MAX_LENGTH_ARGUMENTS = 16;
|
||||
private static final QuietDecoderException LIMITS_VIOLATION =
|
||||
new QuietDecoderException("Command arguments incorrect size");
|
||||
|
||||
private boolean unsigned = false;
|
||||
private String command;
|
||||
private Instant timestamp;
|
||||
private long salt;
|
||||
private boolean signedPreview; // Good god. Please no.
|
||||
private Map<String, byte[]> arguments = ImmutableMap.of();
|
||||
|
||||
public boolean isSignedPreview() {
|
||||
return signedPreview;
|
||||
}
|
||||
|
||||
public Instant getTimestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
public boolean isUnsigned() {
|
||||
return unsigned;
|
||||
}
|
||||
|
||||
public String getCommand() {
|
||||
return command;
|
||||
}
|
||||
|
||||
public PlayerCommand() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an {@link PlayerCommand} packet based on a command and list of arguments.
|
||||
*
|
||||
* @param command the command to run
|
||||
* @param arguments the arguments of the command
|
||||
* @param timestamp the timestamp of the command execution
|
||||
*/
|
||||
public PlayerCommand(String command, List<String> arguments, Instant timestamp) {
|
||||
this.unsigned = true;
|
||||
ImmutableMap.Builder<String, byte[]> builder = ImmutableMap.builder();
|
||||
arguments.forEach(entry -> builder.put(entry, EncryptionUtils.EMPTY));
|
||||
this.arguments = builder.build();
|
||||
this.timestamp = timestamp;
|
||||
this.command = command;
|
||||
this.signedPreview = false;
|
||||
this.salt = 0L;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create new {@link PlayerCommand} based on a previously {@link SignedChatCommand}.
|
||||
*
|
||||
* @param signedCommand The {@link SignedChatCommand} to turn into {@link PlayerCommand}.
|
||||
*/
|
||||
public PlayerCommand(SignedChatCommand signedCommand) {
|
||||
this.command = signedCommand.getBaseCommand();
|
||||
this.arguments = ImmutableMap.copyOf(signedCommand.getSignatures());
|
||||
this.timestamp = signedCommand.getExpiryTemporal();
|
||||
this.salt = Longs.fromByteArray(signedCommand.getSalt());
|
||||
this.signedPreview = signedCommand.isPreviewSigned();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion protocolVersion) {
|
||||
command = ProtocolUtils.readString(buf, 256);
|
||||
timestamp = Instant.ofEpochMilli(buf.readLong());
|
||||
|
||||
salt = buf.readLong();
|
||||
if (salt == 0L) {
|
||||
unsigned = true;
|
||||
}
|
||||
|
||||
int mapSize = ProtocolUtils.readVarInt(buf);
|
||||
if (mapSize > MAX_NUM_ARGUMENTS) {
|
||||
throw LIMITS_VIOLATION;
|
||||
}
|
||||
// Mapped as Argument : signature
|
||||
ImmutableMap.Builder<String, byte[]> entries = ImmutableMap.builderWithExpectedSize(mapSize);
|
||||
for (int i = 0; i < mapSize; i++) {
|
||||
entries.put(ProtocolUtils.readString(buf, MAX_LENGTH_ARGUMENTS),
|
||||
ProtocolUtils.readByteArray(buf, unsigned ? 0 : ProtocolUtils.DEFAULT_MAX_STRING_SIZE));
|
||||
}
|
||||
arguments = entries.build();
|
||||
|
||||
signedPreview = buf.readBoolean();
|
||||
if (unsigned && signedPreview) {
|
||||
throw EncryptionUtils.PREVIEW_SIGNATURE_MISSING;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void encode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion protocolVersion) {
|
||||
ProtocolUtils.writeString(buf, command);
|
||||
buf.writeLong(timestamp.toEpochMilli());
|
||||
|
||||
buf.writeLong(unsigned ? 0L : salt);
|
||||
|
||||
int size = arguments.size();
|
||||
if (size > MAX_NUM_ARGUMENTS) {
|
||||
throw LIMITS_VIOLATION;
|
||||
}
|
||||
ProtocolUtils.writeVarInt(buf, size);
|
||||
for (Map.Entry<String, byte[]> entry : arguments.entrySet()) {
|
||||
// What annoys me is that this isn't "sorted"
|
||||
ProtocolUtils.writeString(buf, entry.getKey());
|
||||
ProtocolUtils.writeByteArray(buf, unsigned ? EncryptionUtils.EMPTY : entry.getValue());
|
||||
}
|
||||
|
||||
buf.writeBoolean(signedPreview);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a signature and creates a {@link SignedChatCommand} from the given signature.
|
||||
*
|
||||
* @param signer the signer's information
|
||||
* @param sender the sender of the message
|
||||
* @param mustSign instructs the function to throw if the signature is invalid.
|
||||
* @return The {@link SignedChatCommand} or null if the signature couldn't be verified.
|
||||
* @throws com.velocitypowered.proxy.util.except.QuietDecoderException when mustSign is {@code true} and the signature
|
||||
* is invalid.
|
||||
*/
|
||||
public SignedChatCommand signedContainer(IdentifiedKey signer, UUID sender, boolean mustSign) {
|
||||
if (unsigned) {
|
||||
if (mustSign) {
|
||||
throw EncryptionUtils.INVALID_SIGNATURE;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
return new SignedChatCommand(command, signer.getSignedPublicKey(), sender, timestamp,
|
||||
arguments, Longs.toByteArray(salt), signedPreview);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean handle(MinecraftSessionHandler handler) {
|
||||
return handler.handle(this);
|
||||
}
|
||||
}
|
@ -0,0 +1,65 @@
|
||||
/*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.velocitypowered.proxy.protocol.packet.chat;
|
||||
|
||||
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 io.netty.buffer.ByteBuf;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer;
|
||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||
|
||||
public class ServerChatPreview implements MinecraftPacket {
|
||||
|
||||
private int id;
|
||||
private @Nullable Component preview;
|
||||
|
||||
public Component getPreview() {
|
||||
return preview;
|
||||
}
|
||||
|
||||
public int getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion protocolVersion) {
|
||||
id = buf.readInt();
|
||||
if (buf.readBoolean()) {
|
||||
preview = GsonComponentSerializer.gson().deserialize(ProtocolUtils.readString(buf));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void encode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion protocolVersion) {
|
||||
buf.writeInt(id);
|
||||
if (preview != null) {
|
||||
buf.writeBoolean(true);
|
||||
ProtocolUtils.writeString(buf, GsonComponentSerializer.gson().serialize(preview));
|
||||
} else {
|
||||
buf.writeBoolean(false);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean handle(MinecraftSessionHandler handler) {
|
||||
return handler.handle(this);
|
||||
}
|
||||
}
|
@ -0,0 +1,89 @@
|
||||
/*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.velocitypowered.proxy.protocol.packet.chat;
|
||||
|
||||
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 io.netty.buffer.ByteBuf;
|
||||
import java.time.Instant;
|
||||
import java.util.UUID;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||
|
||||
public class ServerPlayerChat implements MinecraftPacket {
|
||||
|
||||
private Component component;
|
||||
private @Nullable Component unsignedComponent;
|
||||
private int type;
|
||||
|
||||
private UUID sender;
|
||||
private Component senderName;
|
||||
private @Nullable Component teamName;
|
||||
|
||||
private Instant expiry;
|
||||
|
||||
public void setType(int type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public void setComponent(Component component) {
|
||||
this.component = component;
|
||||
}
|
||||
|
||||
public int getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public Component getComponent() {
|
||||
return component;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion protocolVersion) {
|
||||
component = ProtocolUtils.getJsonChatSerializer(protocolVersion).deserialize(ProtocolUtils.readString(buf));
|
||||
if (buf.readBoolean()) {
|
||||
unsignedComponent = component = ProtocolUtils.getJsonChatSerializer(protocolVersion)
|
||||
.deserialize(ProtocolUtils.readString(buf));
|
||||
}
|
||||
|
||||
type = ProtocolUtils.readVarInt(buf);
|
||||
|
||||
sender = ProtocolUtils.readUuid(buf);
|
||||
senderName = ProtocolUtils.getJsonChatSerializer(protocolVersion).deserialize(ProtocolUtils.readString(buf));
|
||||
if (buf.readBoolean()) {
|
||||
teamName = ProtocolUtils.getJsonChatSerializer(protocolVersion).deserialize(ProtocolUtils.readString(buf));
|
||||
}
|
||||
|
||||
expiry = Instant.ofEpochMilli(buf.readLong());
|
||||
|
||||
long salt = buf.readLong();
|
||||
byte[] signature = ProtocolUtils.readByteArray(buf);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void encode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion protocolVersion) {
|
||||
// TBD
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean handle(MinecraftSessionHandler handler) {
|
||||
return handler.handle(this);
|
||||
}
|
||||
}
|
@ -0,0 +1,63 @@
|
||||
/*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.velocitypowered.proxy.protocol.packet.chat;
|
||||
|
||||
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 io.netty.buffer.ByteBuf;
|
||||
import net.kyori.adventure.text.Component;
|
||||
|
||||
public class SystemChat implements MinecraftPacket {
|
||||
|
||||
public SystemChat() {}
|
||||
|
||||
public SystemChat(Component component, int type) {
|
||||
this.component = component;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
private Component component;
|
||||
private int type;
|
||||
|
||||
public int getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public Component getComponent() {
|
||||
return component;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion protocolVersion) {
|
||||
component = ProtocolUtils.getJsonChatSerializer(protocolVersion).deserialize(ProtocolUtils.readString(buf));
|
||||
type = ProtocolUtils.readVarInt(buf);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void encode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion protocolVersion) {
|
||||
ProtocolUtils.writeString(buf, ProtocolUtils.getJsonChatSerializer(protocolVersion).serialize(component));
|
||||
ProtocolUtils.writeVarInt(buf, type);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean handle(MinecraftSessionHandler handler) {
|
||||
return handler.handle(this);
|
||||
}
|
||||
}
|
@ -18,6 +18,7 @@
|
||||
package com.velocitypowered.proxy.tablist;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.velocitypowered.api.proxy.crypto.IdentifiedKey;
|
||||
import com.velocitypowered.api.proxy.player.TabList;
|
||||
import com.velocitypowered.api.proxy.player.TabListEntry;
|
||||
import com.velocitypowered.api.util.GameProfile;
|
||||
@ -119,10 +120,16 @@ public class VelocityTabList implements TabList {
|
||||
return Collections.unmodifiableCollection(this.entries.values());
|
||||
}
|
||||
|
||||
@Override
|
||||
public TabListEntry buildEntry(GameProfile profile, @Nullable Component displayName, int latency, int gameMode) {
|
||||
return buildEntry(profile, displayName, latency, gameMode, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TabListEntry buildEntry(GameProfile profile,
|
||||
net.kyori.adventure.text.@Nullable Component displayName, int latency, int gameMode) {
|
||||
return new VelocityTabListEntry(this, profile, displayName, latency, gameMode);
|
||||
net.kyori.adventure.text.@Nullable Component displayName,
|
||||
int latency, int gameMode, @Nullable IdentifiedKey key) {
|
||||
return new VelocityTabListEntry(this, profile, displayName, latency, gameMode, key);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -17,6 +17,7 @@
|
||||
|
||||
package com.velocitypowered.proxy.tablist;
|
||||
|
||||
import com.velocitypowered.api.proxy.crypto.IdentifiedKey;
|
||||
import com.velocitypowered.api.proxy.player.TabList;
|
||||
import com.velocitypowered.api.proxy.player.TabListEntry;
|
||||
import com.velocitypowered.api.util.GameProfile;
|
||||
@ -31,14 +32,17 @@ public class VelocityTabListEntry implements TabListEntry {
|
||||
private net.kyori.adventure.text.Component displayName;
|
||||
private int latency;
|
||||
private int gameMode;
|
||||
private @Nullable IdentifiedKey playerKey;
|
||||
|
||||
VelocityTabListEntry(VelocityTabList tabList, GameProfile profile,
|
||||
net.kyori.adventure.text.@Nullable Component displayName, int latency, int gameMode) {
|
||||
net.kyori.adventure.text.@Nullable Component displayName, int latency, int gameMode,
|
||||
@Nullable IdentifiedKey playerKey) {
|
||||
this.tabList = tabList;
|
||||
this.profile = profile;
|
||||
this.displayName = displayName;
|
||||
this.latency = latency;
|
||||
this.gameMode = gameMode;
|
||||
this.playerKey = playerKey;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -98,4 +102,13 @@ public class VelocityTabListEntry implements TabListEntry {
|
||||
void setGameModeInternal(int gameMode) {
|
||||
this.gameMode = gameMode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IdentifiedKey getIdentifiedKey() {
|
||||
return playerKey;
|
||||
}
|
||||
|
||||
void setPlayerKeyInternal(IdentifiedKey playerKey) {
|
||||
this.playerKey = playerKey;
|
||||
}
|
||||
}
|
||||
|
@ -26,7 +26,7 @@ public class VelocityTabListEntryLegacy extends VelocityTabListEntry {
|
||||
|
||||
VelocityTabListEntryLegacy(VelocityTabListLegacy tabList, GameProfile profile,
|
||||
@Nullable Component displayName, int latency, int gameMode) {
|
||||
super(tabList, profile, displayName, latency, gameMode);
|
||||
super(tabList, profile, displayName, latency, gameMode, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -1,89 +0,0 @@
|
||||
/*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.velocitypowered.proxy.util;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.KeyPair;
|
||||
import java.security.KeyPairGenerator;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.PublicKey;
|
||||
import javax.crypto.Cipher;
|
||||
|
||||
public enum EncryptionUtils {
|
||||
;
|
||||
|
||||
/**
|
||||
* Generates an RSA key pair.
|
||||
*
|
||||
* @param keysize the key size (in bits) for the RSA key pair
|
||||
* @return the generated key pair
|
||||
*/
|
||||
public static KeyPair createRsaKeyPair(final int keysize) {
|
||||
try {
|
||||
final KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
|
||||
generator.initialize(keysize);
|
||||
return generator.generateKeyPair();
|
||||
} catch (final NoSuchAlgorithmException e) {
|
||||
throw new RuntimeException("Unable to generate RSA keypair", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a hex digest in two's complement form for use with the Mojang joinedServer endpoint.
|
||||
*
|
||||
* @param digest the bytes to digest
|
||||
* @return the hex digest
|
||||
*/
|
||||
public static String twosComplementHexdigest(byte[] digest) {
|
||||
return new BigInteger(digest).toString(16);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypts an RSA message.
|
||||
*
|
||||
* @param keyPair the key pair to use
|
||||
* @param bytes the bytes of the encrypted message
|
||||
* @return the decrypted message
|
||||
* @throws GeneralSecurityException if the message couldn't be decoded
|
||||
*/
|
||||
public static byte[] decryptRsa(KeyPair keyPair, byte[] bytes) throws GeneralSecurityException {
|
||||
Cipher cipher = Cipher.getInstance("RSA");
|
||||
cipher.init(Cipher.DECRYPT_MODE, keyPair.getPrivate());
|
||||
return cipher.doFinal(bytes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the server ID for the hasJoined endpoint.
|
||||
*
|
||||
* @param sharedSecret the shared secret between the client and the proxy
|
||||
* @param key the RSA public key
|
||||
* @return the server ID
|
||||
*/
|
||||
public static String generateServerId(byte[] sharedSecret, PublicKey key) {
|
||||
try {
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-1");
|
||||
digest.update(sharedSecret);
|
||||
digest.update(key.getEncoded());
|
||||
return twosComplementHexdigest(digest.digest());
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
}
|
@ -29,7 +29,7 @@ velocity.error.modern-forwarding-needs-new-client=This server is only compatible
|
||||
velocity.error.modern-forwarding-failed=Your server did not send a forwarding request to the proxy. Make sure the server is configured for Velocity forwarding.
|
||||
velocity.error.moved-to-new-server=You were kicked from {0}: {1}
|
||||
velocity.error.no-available-servers=There are no available servers to connect you to. Try again later or contact an admin.
|
||||
velocity.error.illegal-chat-characters=Illegal characters in chat
|
||||
velocity.error.illegal-legacyChat-characters=Illegal characters in legacyChat
|
||||
|
||||
# Commands
|
||||
velocity.command.generic-error=An error occurred while running this command.
|
||||
|
@ -15,6 +15,9 @@ show-max-players = 500
|
||||
# Should we authenticate players with Mojang? By default, this is on.
|
||||
online-mode = true
|
||||
|
||||
# Should the proxy enforce the new public key security standard? By default, this is on.
|
||||
force-key-authentication = true
|
||||
|
||||
# If client's ISP/AS sent from this proxy is different from the one from Mojang's
|
||||
# authentication server, the player is kicked. This disallows some VPN and proxy
|
||||
# connections but is a weak form of protection.
|
||||
|
BIN
proxy/src/main/resources/yggdrasil_session_pubkey.der
Normale Datei
BIN
proxy/src/main/resources/yggdrasil_session_pubkey.der
Normale Datei
Binäre Datei nicht angezeigt.
@ -19,8 +19,10 @@ package com.velocitypowered.proxy.util;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
import com.velocitypowered.proxy.crypto.EncryptionUtils;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class EncryptionUtilsTest {
|
||||
|
Laden…
In neuem Issue referenzieren
Einen Benutzer sperren