Compare commits

...
13 Commits
Author SHA1 Message Date
Jones 7f604dd4ad Release 2.0.5 (#74) 2023-10-20 17:41:16 +02:00
Jones ab96929070 Fix EmptyChunkData packet being broken on 1.17+ (#73) 2023-10-20 17:36:43 +02:00
Michel Elkenwaat fa20826b8e Merge remote-tracking branch 'origin/main' 2023-10-20 17:22:25 +02:00
Michel Elkenwaat c8c212a3ee Fix issues with the plugin description (#69) 2023-10-20 17:22:08 +02:00
Michel Elkenwaat ad7d3e62eb Fix issues with the plugin description https://github.com/jonesdevelopment/sonar/pull/69 2023-10-20 17:21:30 +02:00
Ismael 343d71d18c Fix incompatibilities with Gradle 7.x (#69)
Fix "java.lang.IllegalStateException: Module entity with name: sonar.bukkit should be available"
2023-10-20 16:38:52 +02:00
Jones a2e252aece Use a better conversion technique to improve update checker (#72) 2023-10-20 15:09:31 +02:00
Jones 124bed86dc Fix invalid root tag in chunk data packet (#70)
* fix: use Velocity's JoinGame packet for consistency (https://github.com/jonesdevelopment/sonar/issues/68)

* fix: incorrect motion blocking root tag in EmptyChunkData packet (https://github.com/jonesdevelopment/sonar/issues/68)
2023-10-20 14:19:53 +02:00
Jones 971d43df00 Recode IPv4 regex pattern check (#67) 2023-10-19 17:03:52 +02:00
Jones 1671ccac55 Release 2.0.4 (#66) 2023-10-18 16:38:22 +02:00
dependabot[bot] 8e03bdac5f build(deps): bump io.netty:netty-all from 4.1.99.Final to 4.1.100.Final (#64)
Bumps [io.netty:netty-all](https://github.com/netty/netty) from 4.1.99.Final to 4.1.100.Final.
- [Commits](https://github.com/netty/netty/compare/netty-4.1.99.Final...netty-4.1.100.Final)

---
updated-dependencies:
- dependency-name: io.netty:netty-all
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-18 16:35:22 +02:00
Jones 7c0bdc568e Continuation of the integration of 1.20.2 (#65)
* style(all): general code reformat

* feat: remove ActiveFeatures packet

* refactor: create packages for each state

* feat: fully recode JoinGame and RegistrySync packets

* feat: [wip] implement configuration state support to verification handler

* fix: encoder/decoder state not being set properly

* feat: integrate configuration state with the rest of the verification

* fix: PositionLook packet having an invalid packet id on 1.20.2

* fix: don't create and cache chunk mask for 1.17 clients

* style: remove comment; there's not really any BF code left

* feat: add todo for 1.20.2 barrier block mappings

* feat: exempt 1.20.2 clients from gravity and collision checks (TODO)
2023-10-18 16:34:37 +02:00
Jones 354c7a0143 Prepare 1.20.2 support and fix deserialization issue (#62)
* fix: compatibility with the latest version of Velocity (git-eb594fc7-b274)

* fix: implement invalid protocol disconnect if the version is unknown

* feat: add already-online disconnect message translation

* feat: implement 1.20.2 support for JoinGame packet

* feat: add new packets for 1.20.2 config state

* feat: [wip] add packet mappings for 1.20.2 (config state not included)

* fix: implement backwards compatibility for Velocity b265 and below

* fix: optimize login handling by directly running it in the channel's event loop

* feat: make lockdown bypass permission customizable

* fix(common): make all packets more debugging-friendly

* feat: [wip] implement config protocol state

* style: add comments for documenting FallbackPacketDecoder

* fix(all): deserialization not working for some components (https://github.com/jonesdevelopment/sonar/issues/63)
2023-10-14 15:12:39 +02:00
48 changed files with 850 additions and 639 deletions

No files matched your search

+2 -2
View File
@@ -49,7 +49,7 @@ subprojects {
implementation("com.j256.ormlite:ormlite-jdbc:6.1") // ORM
implementation("xyz.jonesdev:cappuccino:0.1.6-SNAPSHOT") // expiring cache
compileOnly("io.netty:netty-all:4.1.99.Final") // netty
compileOnly("io.netty:netty-all:4.1.100.Final") // netty
}
}
@@ -72,7 +72,7 @@ tasks {
shadowJar {
// Set the file name of the shadowed jar
archiveFileName.set("${rootProject.name}.jar")
archiveFileName.set("Sonar.jar")
// bStats has to be relocated to the Sonar package otherwise it throws an exception
// https://github.com/Bastian/bstats-metrics/blob/master/base/src/main/java/org/bstats/MetricsBase.java#L251
+2 -1
View File
@@ -1,2 +1,3 @@
group=xyz.jonesdev.sonar
version=2.0.3
version=2.0.5
description=Effective Anti-bot plugin for Velocity, BungeeCord, and Bukkit (1.7-latest)
+1 -1
View File
@@ -1,4 +1,4 @@
rootProject.name = "Sonar"
rootProject.name = "sonar"
sequenceOf("api", "bukkit", "bungee", "common", "velocity").forEach {
val path = "sonar-$it"
@@ -88,7 +88,7 @@ public interface SonarCommand {
}
component = component
.clickEvent(ClickEvent.clickEvent(ClickEvent.Action.SUGGEST_COMMAND,
"/sonar " + sub.getInfo().name() + " "))
"/sonar " + sub.getInfo().name() + " "))
.hoverEvent(HoverEvent.hoverEvent(HoverEvent.Action.SHOW_TEXT, hoverComponent));
CACHED_HELP_MESSAGE.add(component);
});
@@ -28,6 +28,7 @@ import xyz.jonesdev.sonar.api.command.subcommand.argument.Argument;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Arrays;
import java.util.Objects;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
@@ -35,10 +36,8 @@ import java.util.stream.Collectors;
public abstract class Subcommand {
private final @NotNull SubcommandInfo info;
private final String permission, aliases, arguments;
protected static final Sonar SONAR = Sonar.get();
protected static final Pattern IP_PATTERN = Pattern.compile(
"^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])([.])){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$"
);
public Subcommand() {
info = getClass().getAnnotation(SubcommandInfo.class);
@@ -52,8 +51,14 @@ public abstract class Subcommand {
.collect(Collectors.joining(", "));
}
protected static @Nullable InetAddress checkIP(final InvocationSource source, final String rawIP) {
if (!IP_PATTERN.matcher(rawIP).matches()) {
// https://stackoverflow.com/questions/5284147/validating-ipv4-addresses-with-regexp
private static final Pattern IP_PATTERN = Pattern.compile("^((25[0-5]|(2[0-4]|1\\d|[1-9]|)\\d)\\.?\\b){4}$");
protected static @Nullable InetAddress getInetAddressIfValid(final InvocationSource source, final String rawIP) {
// We perform a regex check beforehand,
// so we don't have to create a new InetAddress,
// therefore, potentially saving some performance.
if (!IP_PATTERN.matcher(Objects.requireNonNull(rawIP)).matches()) {
source.sendMessage(SONAR.getConfig().getCommands().getIncorrectIpAddress());
return null;
}
@@ -112,6 +112,8 @@ public final class SonarConfiguration {
private Component tooManyPlayers;
private Component tooFastReconnect;
private Component invalidUsername;
private Component invalidProtocol;
private Component alreadyConnected;
private Component verificationSuccess;
private Component verificationFailed;
private Component alreadyVerifying;
@@ -195,6 +197,7 @@ public final class SonarConfiguration {
private boolean notifyAdmins;
private boolean logAttempts;
private Component disconnect;
private String bypassPermission;
private String activated;
private String deactivated;
private String notification;
@@ -268,8 +271,10 @@ public final class SonarConfiguration {
generalConfig.getYaml().setComment("database",
"You can connect Sonar to a database to keep verified players even after restarting your server"
+ LINE_SEPARATOR + "Note: IP addresses are saved in plain text. You are responsible for keeping your database safe!"
+ LINE_SEPARATOR + "However, IP addresses cannot be traced back to players as Sonar uses UUIDs instead of usernames");
+ LINE_SEPARATOR + "Note: IP addresses are saved in plain text. You are responsible for keeping your database" +
" safe!"
+ LINE_SEPARATOR + "However, IP addresses cannot be traced back to players as Sonar uses UUIDs instead of " +
"usernames");
generalConfig.getYaml().setComment("database.url",
"URL for authenticating with the SQL database");
database.url = generalConfig.getString("database.url", "localhost");
@@ -293,7 +298,7 @@ public final class SonarConfiguration {
// Lockdown
generalConfig.getYaml().setComment("lockdown",
"You can lock your server down using '/sonar lockdown' if, for example,"
+ LINE_SEPARATOR + "bots are bypassing the verification or any other reason");
+ LINE_SEPARATOR + "bots are bypassing the verification or any other reason");
generalConfig.getYaml().setComment("lockdown.enabled",
"Should Sonar prevent all players from joining the server?");
lockdown.enabled = generalConfig.getBoolean("lockdown.enabled", false);
@@ -306,10 +311,15 @@ public final class SonarConfiguration {
"Should Sonar notify admins when they join the server during lockdown?");
lockdown.notifyAdmins = generalConfig.getBoolean("lockdown.notify-admins", true);
generalConfig.getYaml().setComment("lockdown.bypass-permission",
"Which permission does a player need in order to bypass the lockdown mode?"
+ LINE_SEPARATOR + "Players with this permission will also receive admin notifications when joining");
lockdown.bypassPermission = generalConfig.getString("lockdown.bypass-permission", "sonar.lockdown.bypass");
// Queue
generalConfig.getYaml().setComment("queue",
"Every new login request will be queued to avoid spam join attacks"
+ LINE_SEPARATOR + "The queue is updated every 500 milliseconds (10 ticks)");
+ LINE_SEPARATOR + "The queue is updated every 500 milliseconds (10 ticks)");
generalConfig.getYaml().setComment("queue.max-polls",
"Maximum number of concurrent queue polls per 500 milliseconds");
queue.maxQueuePolls = clamp(generalConfig.getInt("queue.max-polls", 30), 1, 1000);
@@ -317,29 +327,31 @@ public final class SonarConfiguration {
// Verification
generalConfig.getYaml().setComment("verification",
"Every new player that joins for the first time will be sent to"
+ LINE_SEPARATOR + "a lightweight limbo server where advanced bot checks are performed");
+ LINE_SEPARATOR + "a lightweight limbo server where advanced bot checks are performed");
generalConfig.getYaml().setComment("verification.enabled",
"Should Sonar verify new players? (Recommended)");
verification.enabled = generalConfig.getBoolean("verification.enabled", true);
generalConfig.getYaml().setComment("verification.checks.gravity",
"Checks if the players' falling motion is following Minecraft's gravity formula"
+ LINE_SEPARATOR + "All predicted motions are precalculated in order to save performance");
+ LINE_SEPARATOR + "All predicted motions are precalculated in order to save performance");
generalConfig.getYaml().setComment("verification.checks.gravity.enabled",
"Should Sonar check for valid client gravity? (Recommended)");
verification.checkGravity = generalConfig.getBoolean("verification.checks.gravity.enabled", true);
generalConfig.getYaml().setComment("verification.checks.gravity.max-movement-ticks",
"Maximum number of ticks the player has to fall in order to be allowed to hit the platform");
verification.maxMovementTicks = clamp(generalConfig.getInt("verification.checks.gravity.max-movement-ticks", 8), 2, 100);
verification.maxMovementTicks = clamp(generalConfig.getInt("verification.checks.gravity.max-movement-ticks", 8),
2, 100);
generalConfig.getYaml().setComment("verification.checks.gravity.max-ignored-ticks",
"Maximum number of ignored Y movement changes before a player fails verification");
verification.maxIgnoredTicks = clamp(generalConfig.getInt("verification.checks.gravity.max-ignored-ticks", 5), 1, 128);
verification.maxIgnoredTicks = clamp(generalConfig.getInt("verification.checks.gravity.max-ignored-ticks", 5), 1,
128);
generalConfig.getYaml().setComment("verification.checks.collisions",
"Checks if the players collides with barrier blocks spawned below the player"
+ LINE_SEPARATOR + "Note: The collision check will be skipped if the gravity check is disabled");
+ LINE_SEPARATOR + "Note: The collision check will be skipped if the gravity check is disabled");
generalConfig.getYaml().setComment("verification.checks.collisions.enabled",
"Should Sonar check for valid client collisions? (Recommended)");
verification.checkCollisions = generalConfig.getBoolean("verification.checks.collisions.enabled", true);
@@ -757,6 +769,26 @@ public final class SonarConfiguration {
"%footer%"
))));
messagesConfig.getYaml().setComment("verification.invalid-protocol",
"Disconnect message that is shown when someone joins with a too new or too old version");
verification.invalidProtocol = deserialize(fromList(messagesConfig.getStringList("verification.invalid-protocol",
Arrays.asList(
"%header%",
"<red>Your protocol version is currently unsupported.",
"%footer%"
))));
messagesConfig.getYaml().setComment("verification.already-online",
"Disconnect message that is shown when someone tries verifying with an account that is online");
verification.alreadyConnected = deserialize(fromList(messagesConfig.getStringList("verification.already-online",
Arrays.asList(
"%header%",
"<red>There is someone already online with your account.",
"<gray>Please wait a few seconds before trying to verify again.",
"<gray>If this keeps occurring, try restarting your game or contact support.",
"%footer%"
))));
messagesConfig.getYaml().setComment("verification.success",
"Disconnect message that is shown when someone verifies successfully");
verification.verificationSuccess = deserialize(fromList(messagesConfig.getStringList("verification.success",
@@ -810,10 +842,10 @@ public final class SonarConfiguration {
))));
messagesConfig.getYaml().setComment("verbose.animation",
"Alternative symbols:"
+ LINE_SEPARATOR + "- ▙"
+ LINE_SEPARATOR + "- ▛"
+ LINE_SEPARATOR + "- ▜"
+ LINE_SEPARATOR + "- ▟");
+ LINE_SEPARATOR + "- ▙"
+ LINE_SEPARATOR + "- ▛"
+ LINE_SEPARATOR + "- ▜"
+ LINE_SEPARATOR + "- ▟");
verbose.animation = Collections.unmodifiableList(messagesConfig.getStringList("verbose.animation",
Arrays.asList("◜", "◝", "◞", "◟")
));
@@ -17,6 +17,7 @@
package xyz.jonesdev.sonar.api.fallback.protocol;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.util.EnumSet;
@@ -66,10 +67,10 @@ public enum ProtocolVersion {
MINECRAFT_1_19_3(761),
MINECRAFT_1_19_4(762),
MINECRAFT_1_20(763),
MINECRAFT_1_20_2(-1);
MINECRAFT_1_20_2(764);
@Getter
private final int protocol;
private final int snapshotProtocol = -1;
public static final ProtocolVersion MINIMUM_VERSION = MINECRAFT_1_7_2;
public static final Map<Integer, ProtocolVersion> ID_TO_PROTOCOL_CONSTANT;
@@ -81,7 +82,6 @@ public enum ProtocolVersion {
for (final ProtocolVersion version : values()) {
versions.putIfAbsent(version.protocol, version);
versions.put(version.snapshotProtocol, version);
}
ID_TO_PROTOCOL_CONSTANT = versions;
@@ -104,10 +104,6 @@ public enum ProtocolVersion {
return ID_TO_PROTOCOL_CONSTANT.getOrDefault(protocol, UNKNOWN);
}
public int getProtocol() {
return protocol == -1 ? snapshotProtocol : protocol;
}
public boolean isUnknown() {
return this == UNKNOWN;
}
+1 -2
View File
@@ -7,12 +7,11 @@ plugins {
apply(plugin = "net.minecrell.plugin-yml.bukkit")
bukkit {
name = rootProject.name
name = "Sonar"
version = rootProject.version.toString().split("-")[0]
main = "xyz.jonesdev.sonar.bukkit.SonarBukkitPlugin"
authors = listOf("Jones Development", "Sonar Contributors")
website = "https://jonesdev.xyz/discord/"
description = "Effective Anti-bot plugin for Velocity, BungeeCord, and Bukkit (1.7-latest)"
load = BukkitPluginDescription.PluginLoadOrder.POSTWORLD
commands {
@@ -45,10 +45,13 @@ public final class BukkitSonarCommand implements CommandExecutor, TabExecutor, S
final Command command,
final String label,
final String[] args) {
// Create our own invocation source wrapper to handle messages properly
final InvocationSource invocationSource = new BukkitInvocationSource(sender);
if (!(sender instanceof ConsoleCommandSender)) {
// Check if the player actually has the permission to run the command
if (!sender.hasPermission("sonar.command")) {
sender.sendMessage(Sonar.get().getConfig().getNoPermission());
invocationSource.sendMessage(Sonar.get().getConfig().getNoPermission());
return false;
}
// Checking if it contains will only break more since it can throw
@@ -60,13 +63,13 @@ public final class BukkitSonarCommand implements CommandExecutor, TabExecutor, S
// Spamming should be prevented, especially if some heavy operations are done,
// which is not the case here but let's still stay safe!
if (mapTimestamp > 0L) {
sender.sendMessage(Sonar.get().getConfig().getCommands().getCommandCoolDown());
invocationSource.sendMessage(Sonar.get().getConfig().getCommands().getCommandCoolDown());
// Format delay
final long timestamp = System.currentTimeMillis();
final double left = 0.5D - (timestamp - mapTimestamp) / 1000D;
sender.sendMessage(Sonar.get().getConfig().getCommands().getCommandCoolDownLeft()
invocationSource.sendMessage(Sonar.get().getConfig().getCommands().getCommandCoolDownLeft()
.replace("%time-left%", Sonar.DECIMAL_FORMAT.format(left)));
return false;
}
@@ -76,8 +79,6 @@ public final class BukkitSonarCommand implements CommandExecutor, TabExecutor, S
Optional<Subcommand> subcommand = Optional.empty();
final InvocationSource invocationSource = new BukkitInvocationSource(sender);
if (args.length > 0) {
// Search subcommand if command arguments are present
subcommand = Sonar.get().getSubcommandRegistry().getSubcommands().parallelStream()
+1 -2
View File
@@ -5,11 +5,10 @@ plugins {
apply(plugin = "net.minecrell.plugin-yml.bungee")
bungee {
name = rootProject.name
name = "Sonar"
version = rootProject.version.toString().split("-")[0]
main = "xyz.jonesdev.sonar.bungee.SonarBungeePlugin"
author = "Jones Development, Sonar Contributors"
description = "Effective Anti-bot plugin for Velocity, BungeeCord, and Bukkit (1.7-latest)"
}
dependencies {
@@ -19,7 +19,6 @@ package xyz.jonesdev.sonar.bungee.command;
import net.kyori.adventure.text.Component;
import net.md_5.bungee.api.CommandSender;
import net.md_5.bungee.api.chat.TextComponent;
import net.md_5.bungee.api.connection.ProxiedPlayer;
import net.md_5.bungee.api.plugin.Command;
import net.md_5.bungee.api.plugin.TabExecutor;
@@ -48,10 +47,13 @@ public final class BungeeSonarCommand extends Command implements TabExecutor, So
@Override
@SuppressWarnings({"redundantSuppression"})
public void execute(final @NotNull CommandSender sender, final String[] args) {
// Create our own invocation source wrapper to handle messages properly
final InvocationSource invocationSource = new BungeeInvocationSource(sender);
if (!(sender instanceof ConsoleCommandSender)) {
// Check if the player actually has the permission to run the command
if (!sender.hasPermission("sonar.command")) {
sender.sendMessage(new TextComponent(Sonar.get().getConfig().getNoPermission()));
invocationSource.sendMessage(Sonar.get().getConfig().getNoPermission());
return;
}
// Checking if it contains will only break more since it can throw
@@ -63,14 +65,14 @@ public final class BungeeSonarCommand extends Command implements TabExecutor, So
// Spamming should be prevented, especially if some heavy operations are done,
// which is not the case here but let's still stay safe!
if (mapTimestamp > 0L) {
sender.sendMessage(new TextComponent(Sonar.get().getConfig().getCommands().getCommandCoolDown()));
invocationSource.sendMessage(Sonar.get().getConfig().getCommands().getCommandCoolDown());
// Format delay
final long timestamp = System.currentTimeMillis();
final double left = 0.5D - (timestamp - mapTimestamp) / 1000D;
sender.sendMessage(new TextComponent(Sonar.get().getConfig().getCommands().getCommandCoolDownLeft()
.replace("%time-left%", Sonar.DECIMAL_FORMAT.format(left))));
invocationSource.sendMessage(Sonar.get().getConfig().getCommands().getCommandCoolDownLeft()
.replace("%time-left%", Sonar.DECIMAL_FORMAT.format(left)));
return;
}
@@ -79,8 +81,6 @@ public final class BungeeSonarCommand extends Command implements TabExecutor, So
Optional<Subcommand> subcommand = Optional.empty();
final InvocationSource invocationSource = new BungeeInvocationSource(sender);
if (args.length > 0) {
// Search subcommand if command arguments are present
subcommand = Sonar.get().getSubcommandRegistry().getSubcommands().parallelStream()
@@ -65,7 +65,7 @@ public final class FallbackListener implements Listener {
@SuppressWarnings("deprecation")
public void handle(final @NotNull PostLoginEvent event) throws Throwable {
if (Sonar.get().getConfig().getLockdown().isEnabled()) {
if (!event.getPlayer().hasPermission("sonar.lockdown.bypass")) {
if (!event.getPlayer().hasPermission(Sonar.get().getConfig().getLockdown().getBypassPermission())) {
final PendingConnection pendingConnection = event.getPlayer().getPendingConnection();
// Try to close the channel with a custom serialized disconnect component
if (pendingConnection instanceof FallbackInitialHandler) {
@@ -29,7 +29,7 @@ import xyz.jonesdev.sonar.api.fallback.Fallback;
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.bungee.fallback.handler.FallbackInitialHandler;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.Disconnect;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.Disconnect;
import java.net.InetAddress;
@@ -21,23 +21,26 @@ import io.netty.buffer.ByteBuf;
import io.netty.handler.codec.CorruptedFrameException;
import io.netty.handler.codec.DecoderException;
import lombok.Setter;
import lombok.val;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.event.impl.UserVerifySuccessEvent;
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
import xyz.jonesdev.sonar.api.model.VerifiedPlayer;
import xyz.jonesdev.sonar.api.timer.SystemTimer;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketListener;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.*;
import xyz.jonesdev.sonar.common.fallback.protocol.*;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.config.FinishConfiguration;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.login.LoginAcknowledged;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.*;
import xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil;
import java.util.Random;
import java.util.UUID;
import java.util.regex.Pattern;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_13;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_8;
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_PACKET_DECODER;
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_PACKET_ENCODER;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer.*;
public final class FallbackVerificationHandler implements FallbackPacketListener {
@@ -50,23 +53,24 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
private int tick, totalReceivedPackets;
private int ignoredMovementTicks;
private double posX, posY, posZ, lastY;
private boolean resolvedClientBrand, resolvedClientSettings;
@Setter
private State state;
private @NotNull State state = State.LOGIN_ACK;
private boolean listenForMovements;
private final SystemTimer login = new SystemTimer();
private static final Random RANDOM = new Random();
public enum State {
// PRE-JOIN
// 1.20.2 configuration state
LOGIN_ACK, CONFIGURE,
// pre-JOIN ping check
KEEP_ALIVE,
// JOIN
CLIENT_SETTINGS, PLUGIN_MESSAGE,
// POST-JOIN
TRANSACTION,
// PLAY
// post-JOIN checks
CLIENT_SETTINGS, PLUGIN_MESSAGE, TRANSACTION,
// PLAY checks
TELEPORT, POSITION, COLLISIONS,
// OTHER
// Done
SUCCESS
}
@@ -76,12 +80,47 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
this.user = user;
this.username = username;
this.playerUuid = playerUuid;
this.state = State.KEEP_ALIVE;
if (user.getProtocolVersion().compareTo(MINECRAFT_1_20_2) < 0) {
// Start initializing the actual join process
initialJoinProcess();
}
}
private void configure() {
// Set the state to CONFIGURE to avoid false positives
state = State.CONFIGURE;
// Send necessary configuration packets to the client
user.delayedWrite(synchronizeRegistry);
user.delayedWrite(FINISH_CONFIGURATION);
// Send all packets in one flush
user.getChannel().flush();
// Set decoder state to actually catch all packets
updateEncoderDecoderState(FallbackPacketRegistry.CONFIG);
}
private void updateEncoderDecoderState(final @NotNull FallbackPacketRegistry registry) {
val decoder = (FallbackPacketDecoder) user.getChannel().pipeline().get(FALLBACK_PACKET_DECODER);
val encoder = (FallbackPacketEncoder) user.getChannel().pipeline().get(FALLBACK_PACKET_ENCODER);
if (decoder != null && encoder != null) {
// Update the packet registry state to be able to listen for CONFIG packets
decoder.updateRegistry(registry);
encoder.updateRegistry(registry);
} else {
// Something went wrong - the decoder should not be null
user.getFallback().getLogger().warn("Necessary pipelines for {} not found", username);
// Close the channel to prevent bypasses or other potential exploits
user.getChannel().close();
}
}
private void initialJoinProcess() {
if (user.getProtocolVersion().compareTo(MINECRAFT_1_8) < 0) {
// 1.7 players don't have KeepAlive packets in the login process
sendJoinGamePacket();
} else {
// Set the state to KEEP_ALIVE to avoid false positives
state = State.KEEP_ALIVE;
// Generate a random KeepAlive ID for the pre-join check
expectedKeepAliveId = RANDOM.nextInt();
// Send first KeepAlive to check if the connection is somewhat responsive
@@ -100,11 +139,19 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
}
private void sendJoinGamePacket() {
// Set the state to CLIENT_SETTINGS to avoid false positives
// and go on with the flow of the verification.
state = State.CLIENT_SETTINGS;
final boolean v1_20_2 = user.getProtocolVersion().compareTo(MINECRAFT_1_20_2) >= 0;
if (!v1_20_2) {
// Set the state to CLIENT_SETTINGS to avoid false positives
// and go on with the flow of the verification.
state = State.CLIENT_SETTINGS;
}
// Select the JoinGame packet for the respective protocol version
user.write(getJoinPacketForVersion(user.getProtocolVersion()));
if (v1_20_2) {
// Perform the transaction check since the ClientSettings
// and PluginMessage packets have already been validated.
sendTransaction();
}
}
private void sendAbilitiesAndTeleport() {
@@ -184,6 +231,27 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
return;
}
if (packet instanceof LoginAcknowledged) {
// Check if we are currently expecting a LoginAcknowledged packet
assertState(State.LOGIN_ACK);
// Start the configuration process for 1.20.2 clients
configure();
}
if (packet instanceof FinishConfiguration) {
// Check if we are currently expecting a FinishConfiguration packet
assertState(State.CONFIGURE);
// Check if the client has already sent valid ClientSettings and PluginMessage packets
checkFrame(resolvedClientBrand, "did not resolve client brand");
checkFrame(resolvedClientSettings, "did not resolve client settings");
// Start initializing the actual join process
updateEncoderDecoderState(FallbackPacketRegistry.GAME);
initialJoinProcess();
}
if (packet instanceof KeepAlive) {
final KeepAlive keepAlive = (KeepAlive) packet;
@@ -209,14 +277,20 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
if (state == State.CLIENT_SETTINGS) {
state = State.PLUGIN_MESSAGE;
}
// Make sure we mark the ClientSettings as valid
resolvedClientSettings = true;
}
if (packet instanceof PluginMessage) {
final PluginMessage pluginMessage = (PluginMessage) packet;
// Only the brand channel is important
// Only the brand channel is important, drop the rest
if (pluginMessage.getChannel().equals("MC|Brand")
|| pluginMessage.getChannel().equals("minecraft:brand")) {
// Check if the brand packet was sent twice,
// which is not possible when using a vanilla Minecraft client.
checkFrame(!resolvedClientBrand, "duplicate client brand packet");
// Check if the channel is correct - 1.13 uses the new namespace
// system ('minecraft:' + channel) and anything below 1.13 uses
@@ -232,6 +306,9 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
// Send the transaction packet
sendTransaction();
}
// Make sure we mark the PluginMessage as valid
resolvedClientBrand = true;
}
}
@@ -252,14 +329,20 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
return;
}
// First, send an abilities packet to the client to make
// TODO: implement ChunkData packet for 1.20.2 and fix block state Ids for barriers
if (user.getProtocolVersion().compareTo(MINECRAFT_1_20_2) >= 0) {
finish();
return;
}
// First, send an Abilities packet to the client to make
// sure the player falls even in spectator mode.
// Then, teleport the player to the spawn position.
sendAbilitiesAndTeleport();
// 1.7-1.8 clients do not have a TeleportConfirm packet
if (user.getProtocolVersion().compareTo(MINECRAFT_1_8) <= 0) {
// Immediately send the chunk data as 1.7-1.8 can't confirm teleports
// Immediately send the chunk data as 1.7-1.8 can't confirm teleport packets
sendChunkData();
} else {
// Send all previously sent packets in one flush since we didn't flush
@@ -285,19 +368,21 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
sendChunkData();
}
if (packet instanceof Position) {
final Position position = (Position) packet;
handlePositionUpdate(position.getX(), position.getY(), position.getZ(), position.isOnGround());
}
if (state != State.LOGIN_ACK) {
if (packet instanceof Position) {
final Position position = (Position) packet;
handlePositionUpdate(position.getX(), position.getY(), position.getZ(), position.isOnGround());
}
if (packet instanceof PositionLook) {
final PositionLook position = (PositionLook) packet;
handlePositionUpdate(position.getX(), position.getY(), position.getZ(), position.isOnGround());
}
if (packet instanceof PositionLook) {
final PositionLook position = (PositionLook) packet;
handlePositionUpdate(position.getX(), position.getY(), position.getZ(), position.isOnGround());
}
if (packet instanceof Player) {
final Player player = (Player) packet;
handlePositionUpdate(posX, posY, posZ, player.isOnGround());
if (packet instanceof Player) {
final Player player = (Player) packet;
handlePositionUpdate(posX, posY, posZ, player.isOnGround());
}
}
}
@@ -369,7 +454,7 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
}
// Check if the player is colliding by performing a basic Y offset check.
// The offset cannot greater than 0 since the blocks will not let the player fall through them.
// The offset cannot be greater than 0 since the blocks will not let the player fall through them.
checkFrame(offsetY <= 0, "no collisions: " + offsetY);
if (ground) {
@@ -21,65 +21,86 @@ import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.handler.codec.CorruptedFrameException;
import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_20_2;
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry.Direction.SERVERBOUND;
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry.GAME;
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry.LOGIN;
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.readVarInt;
@RequiredArgsConstructor
public final class FallbackPacketDecoder extends ChannelInboundHandlerAdapter {
private final FallbackUser<?, ?> user;
private final FallbackPacketRegistry.ProtocolRegistry registry;
private final FallbackPacketListener listener;
private @NotNull FallbackPacketRegistry.ProtocolRegistry registry;
public FallbackPacketDecoder(final @NotNull FallbackUser<?, ?> user,
final @NotNull FallbackPacketListener listener) {
this.user = user;
this.registry = FallbackPacketRegistry.GAME.getProtocolRegistry(
FallbackPacketRegistry.Direction.SERVERBOUND, user.getProtocolVersion()
);
this.registry = (user.getProtocolVersion().compareTo(MINECRAFT_1_20_2) >= 0 ? LOGIN : GAME)
.getProtocolRegistry(SERVERBOUND, user.getProtocolVersion());
this.listener = listener;
}
public void updateRegistry(final @NotNull FallbackPacketRegistry registry) {
this.registry = registry.getProtocolRegistry(SERVERBOUND, user.getProtocolVersion());
}
@Override
public void channelRead(final @NotNull ChannelHandlerContext ctx,
final @NotNull Object msg) throws Exception {
if (msg instanceof ByteBuf) {
final ByteBuf byteBuf = (ByteBuf) msg;
// Release the ByteBuf if the connection is not active
// or the ByteBuf doesn't contain any data to avoid
// memory leaks or other potential exploits.
if (!ctx.channel().isActive() || !byteBuf.isReadable()) {
byteBuf.release();
return;
}
final int originalReaderIndex = byteBuf.readerIndex();
// Read the packet ID and then create the packet from it
final int packetId = readVarInt(byteBuf);
final FallbackPacket packet = registry.createPacket(packetId);
// If the packet hasn't been found, skip it
// TODO: Can we fail the verification afterwards?
if (packet == null) {
byteBuf.readerIndex(originalReaderIndex);
} else {
try {
doLengthSanityChecks(byteBuf, packet);
try {
packet.decode(byteBuf, user.getProtocolVersion());
} catch (Throwable throwable) {
user.fail("could not decode packet");
throw new CorruptedFrameException("Failed to decode packet");
}
if (byteBuf.isReadable()) {
user.fail("could not read packet to end (" + byteBuf.readableBytes() + " bytes left)");
throw new CorruptedFrameException("Could not read packet to end");
}
listener.handle(packet);
} finally {
byteBuf.release();
}
return;
}
try {
// Ensure that the packet isn't too large or too small
doLengthSanityChecks(byteBuf, packet);
try {
// Try to decode the packet for the given protocol version
packet.decode(byteBuf, user.getProtocolVersion());
} catch (Throwable throwable) {
user.fail("failed to decode packet (" + byteBuf.readableBytes() + " bytes)");
throw new CorruptedFrameException("Failed to decode packet");
}
// Check if the packet still has bytes left after we decoded it
if (byteBuf.isReadable()) {
user.fail("could not read packet to end (" + byteBuf.readableBytes() + " bytes left)");
throw new CorruptedFrameException("Could not read packet to end");
}
// Let our verification handler process the packet
listener.handle(packet);
} finally {
// Release the ByteBuf to avoid memory leaks
byteBuf.release();
}
} else {
// Packets are always ByteBufs - if we detect an unknown object, fail verification
user.fail("packet is not a ByteBuf");
throw new CorruptedFrameException("Packet is not a ByteBuf?!");
}
}
@@ -87,13 +108,13 @@ public final class FallbackPacketDecoder extends ChannelInboundHandlerAdapter {
final @NotNull FallbackPacket packet) throws Exception {
final int expectedMaxLen = packet.expectedMaxLength(byteBuf, user.getProtocolVersion());
if (expectedMaxLen != -1 && byteBuf.readableBytes() > expectedMaxLen) {
user.fail("packet too large");
user.fail("packet too large (" + byteBuf.readableBytes() + " bytes)");
throw new CorruptedFrameException("Packet too large");
}
final int expectedMinLen = packet.expectedMinLength(byteBuf, user.getProtocolVersion());
if (byteBuf.readableBytes() < expectedMinLen) {
user.fail("packet too small");
user.fail("packet too small (" + byteBuf.readableBytes() + " bytes)");
throw new CorruptedFrameException("Packet too small");
}
}
@@ -20,8 +20,11 @@ package xyz.jonesdev.sonar.common.fallback.protocol;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry.Direction.CLIENTBOUND;
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry.LOGIN;
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.writeVarInt;
public final class FallbackPacketEncoder extends MessageToByteEncoder<FallbackPacket> {
@@ -30,13 +33,11 @@ public final class FallbackPacketEncoder extends MessageToByteEncoder<FallbackPa
public FallbackPacketEncoder(final ProtocolVersion protocolVersion) {
this.protocolVersion = protocolVersion;
this.registry = FallbackPacketRegistry.LOGIN
.getProtocolRegistry(FallbackPacketRegistry.Direction.CLIENTBOUND, protocolVersion);
updateRegistry(LOGIN);
}
public void loginSuccess() {
this.registry = FallbackPacketRegistry.GAME
.getProtocolRegistry(FallbackPacketRegistry.Direction.CLIENTBOUND, protocolVersion);
public void updateRegistry(final @NotNull FallbackPacketRegistry registry) {
this.registry = registry.getProtocolRegistry(CLIENTBOUND, protocolVersion);
}
@Override
@@ -22,7 +22,11 @@ import io.netty.util.collection.IntObjectMap;
import lombok.Data;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.*;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.config.FinishConfiguration;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.config.RegistrySync;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.login.LoginAcknowledged;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.login.LoginSuccess;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.*;
import java.util.*;
import java.util.function.Supplier;
@@ -34,13 +38,34 @@ import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
public enum FallbackPacketRegistry {
LOGIN {
{
clientbound.fallback = false;
serverbound.fallback = false;
serverbound.register(LoginAcknowledged.class, LoginAcknowledged::new,
map(0x03, MINECRAFT_1_20_2, false));
clientbound.register(ServerLoginSuccess.class, ServerLoginSuccess::new,
clientbound.register(LoginSuccess.class, LoginSuccess::new,
map(0x02, MINECRAFT_1_7_2, false));
}
},
CONFIG {
{
clientbound.register(Disconnect.class, Disconnect::new,
map(0x01, MINECRAFT_1_20_2, false));
clientbound.register(FinishConfiguration.class, FinishConfiguration::new,
map(0x02, MINECRAFT_1_20_2, false));
clientbound.register(KeepAlive.class, KeepAlive::new,
map(0x03, MINECRAFT_1_20_2, false));
clientbound.register(RegistrySync.class, RegistrySync::new,
map(0x05, MINECRAFT_1_20_2, false));
serverbound.register(ClientSettings.class, ClientSettings::new,
map(0x00, MINECRAFT_1_20_2, false));
serverbound.register(PluginMessage.class, PluginMessage::new,
map(0x01, MINECRAFT_1_20_2, false));
serverbound.register(FinishConfiguration.class, FinishConfiguration::new,
map(0x02, MINECRAFT_1_20_2, false));
serverbound.register(KeepAlive.class, KeepAlive::new,
map(0x03, MINECRAFT_1_20_2, false));
}
},
GAME {
{
clientbound.register(JoinGame.class, JoinGame::new,
@@ -55,7 +80,8 @@ public enum FallbackPacketRegistry {
map(0x23, MINECRAFT_1_19, true),
map(0x25, MINECRAFT_1_19_1, true),
map(0x24, MINECRAFT_1_19_3, true),
map(0x28, MINECRAFT_1_19_4, true));
map(0x28, MINECRAFT_1_19_4, true),
map(0x29, MINECRAFT_1_20_2, true));
clientbound.register(KeepAlive.class, KeepAlive::new,
map(0x00, MINECRAFT_1_7_2, false),
map(0x1F, MINECRAFT_1_9, false),
@@ -68,7 +94,8 @@ public enum FallbackPacketRegistry {
map(0x1E, MINECRAFT_1_19, false),
map(0x20, MINECRAFT_1_19_1, false),
map(0x1F, MINECRAFT_1_19_3, false),
map(0x23, MINECRAFT_1_19_4, false));
map(0x23, MINECRAFT_1_19_4, false),
map(0x24, MINECRAFT_1_20_2, false));
clientbound.register(Disconnect.class, Disconnect::new,
map(0x40, MINECRAFT_1_7_2, true),
map(0x1A, MINECRAFT_1_9, true),
@@ -81,7 +108,8 @@ public enum FallbackPacketRegistry {
map(0x17, MINECRAFT_1_19, true),
map(0x19, MINECRAFT_1_19_1, true),
map(0x17, MINECRAFT_1_19_3, true),
map(0x1A, MINECRAFT_1_19_4, true));
map(0x1A, MINECRAFT_1_19_4, true),
map(0x1B, MINECRAFT_1_20_2, true));
clientbound.register(PositionLook.class, PositionLook::new,
map(0x08, MINECRAFT_1_7_2, true),
map(0x2E, MINECRAFT_1_9, true),
@@ -95,7 +123,8 @@ public enum FallbackPacketRegistry {
map(0x36, MINECRAFT_1_19, true),
map(0x39, MINECRAFT_1_19_1, true),
map(0x38, MINECRAFT_1_19_3, true),
map(0x3C, MINECRAFT_1_19_4, true));
map(0x3C, MINECRAFT_1_19_4, true),
map(0x3E, MINECRAFT_1_20_2, true));
clientbound.register(Abilities.class, Abilities::new,
map(0x39, MINECRAFT_1_7_2, true),
map(0x2B, MINECRAFT_1_9, true),
@@ -109,7 +138,8 @@ public enum FallbackPacketRegistry {
map(0x2f, MINECRAFT_1_19, true),
map(0x31, MINECRAFT_1_19_1, true),
map(0x30, MINECRAFT_1_19_3, true),
map(0x34, MINECRAFT_1_19_4, true));
map(0x34, MINECRAFT_1_19_4, true),
map(0x36, MINECRAFT_1_20_2, true));
clientbound.register(EmptyChunkData.class, EmptyChunkData::new,
map(0x21, MINECRAFT_1_7_2, true),
map(0x20, MINECRAFT_1_9, true),
@@ -122,33 +152,21 @@ public enum FallbackPacketRegistry {
map(0x1F, MINECRAFT_1_19, true),
map(0x21, MINECRAFT_1_19_1, true),
map(0x20, MINECRAFT_1_19_3, true),
map(0x24, MINECRAFT_1_19_4, true));
map(0x24, MINECRAFT_1_19_4, true),
map(0x25, MINECRAFT_1_20_2, true));
clientbound.register(UpdateSectionBlocks.class, UpdateSectionBlocks::new,
map(0x22, MINECRAFT_1_7_2, true),
map(16, MINECRAFT_1_9, true),
map(15, MINECRAFT_1_13, true),
map(16, MINECRAFT_1_15, true),
map(15, MINECRAFT_1_16, true),
map(59, MINECRAFT_1_16_2, true),
map(63, MINECRAFT_1_17, true),
map(61, MINECRAFT_1_19, true),
map(64, MINECRAFT_1_19_1, true),
map(63, MINECRAFT_1_19_3, true),
map(67, MINECRAFT_1_19_4, true));
clientbound.register(DefaultSpawnPosition.class, DefaultSpawnPosition::new,
map(0x05, MINECRAFT_1_7_2, true),
map(0x43, MINECRAFT_1_9, true),
map(0x45, MINECRAFT_1_12, true),
map(0x46, MINECRAFT_1_12_1, true),
map(0x49, MINECRAFT_1_13, true),
map(0x4D, MINECRAFT_1_14, true),
map(0x4E, MINECRAFT_1_15, true),
map(0x42, MINECRAFT_1_16, true),
map(0x4B, MINECRAFT_1_17, true),
map(0x4A, MINECRAFT_1_19, true),
map(0x4D, MINECRAFT_1_19_1, true),
map(0x4C, MINECRAFT_1_19_3, true),
map(0x50, MINECRAFT_1_19_4, true));
map(0x10, MINECRAFT_1_9, true),
map(0x0F, MINECRAFT_1_13, true),
map(0x10, MINECRAFT_1_15, true),
map(0x0F, MINECRAFT_1_16, true),
map(0x3B, MINECRAFT_1_16_2, true),
map(0x3F, MINECRAFT_1_17, true),
map(0x3D, MINECRAFT_1_19, true),
map(0x40, MINECRAFT_1_19_1, true),
map(0x3F, MINECRAFT_1_19_3, true),
map(0x43, MINECRAFT_1_19_4, true),
map(0x45, MINECRAFT_1_20_2, true));
clientbound.register(Transaction.class, Transaction::new,
map(0x32, MINECRAFT_1_7_2, true),
map(0x11, MINECRAFT_1_9, true),
@@ -160,7 +178,8 @@ public enum FallbackPacketRegistry {
map(0x2D, MINECRAFT_1_19, true),
map(0x2F, MINECRAFT_1_19_1, true),
map(0x2E, MINECRAFT_1_19_3, true),
map(0x32, MINECRAFT_1_19_4, true));
map(0x32, MINECRAFT_1_19_4, true),
map(0x33, MINECRAFT_1_20_2, true));
clientbound.register(Chat.class, Chat::new,
map(0x02, MINECRAFT_1_7_2, true),
map(0x0F, MINECRAFT_1_9, true),
@@ -171,7 +190,8 @@ public enum FallbackPacketRegistry {
map(0x5F, MINECRAFT_1_19, true),
map(0x62, MINECRAFT_1_19_1, true),
map(0x60, MINECRAFT_1_19_3, true),
map(0x64, MINECRAFT_1_19_4, true));
map(0x64, MINECRAFT_1_19_4, true),
map(0x67, MINECRAFT_1_20_2, true));
serverbound.register(KeepAlive.class, KeepAlive::new,
map(0x00, MINECRAFT_1_7_2, false),
@@ -185,7 +205,8 @@ public enum FallbackPacketRegistry {
map(0x11, MINECRAFT_1_19, false),
map(0x12, MINECRAFT_1_19_1, false),
map(0x11, MINECRAFT_1_19_3, false),
map(0x12, MINECRAFT_1_19_4, false));
map(0x12, MINECRAFT_1_19_4, false),
map(0x14, MINECRAFT_1_20_2, false));
serverbound.register(ClientSettings.class, ClientSettings::new,
map(0x15, MINECRAFT_1_7_2, false),
map(0x04, MINECRAFT_1_9, false),
@@ -195,7 +216,8 @@ public enum FallbackPacketRegistry {
map(0x07, MINECRAFT_1_19, false),
map(0x08, MINECRAFT_1_19_1, false),
map(0x07, MINECRAFT_1_19_3, false),
map(0x08, MINECRAFT_1_19_4, false));
map(0x08, MINECRAFT_1_19_4, false),
map(0x09, MINECRAFT_1_20_2, false));
serverbound.register(PluginMessage.class, PluginMessage::new,
map(0x17, MINECRAFT_1_7_2, false),
map(0x09, MINECRAFT_1_9, false),
@@ -207,7 +229,8 @@ public enum FallbackPacketRegistry {
map(0x0C, MINECRAFT_1_19, false),
map(0x0D, MINECRAFT_1_19_1, false),
map(0x0C, MINECRAFT_1_19_3, false),
map(0x0D, MINECRAFT_1_19_4, false));
map(0x0D, MINECRAFT_1_19_4, false),
map(0x0F, MINECRAFT_1_20_2, false));
serverbound.register(PositionLook.class, PositionLook::new,
map(0x06, MINECRAFT_1_7_2, false),
map(0x0D, MINECRAFT_1_9, false),
@@ -220,7 +243,8 @@ public enum FallbackPacketRegistry {
map(0x14, MINECRAFT_1_19, false),
map(0x15, MINECRAFT_1_19_1, false),
map(0x14, MINECRAFT_1_19_3, false),
map(0x15, MINECRAFT_1_19_4, false));
map(0x15, MINECRAFT_1_19_4, false),
map(0x17, MINECRAFT_1_20_2, false));
serverbound.register(TeleportConfirm.class, TeleportConfirm::new,
map(0x00, MINECRAFT_1_9, false));
serverbound.register(Position.class, Position::new,
@@ -235,7 +259,8 @@ public enum FallbackPacketRegistry {
map(0x13, MINECRAFT_1_19, false),
map(0x14, MINECRAFT_1_19_1, false),
map(0x13, MINECRAFT_1_19_3, false),
map(0x14, MINECRAFT_1_19_4, false));
map(0x14, MINECRAFT_1_19_4, false),
map(0x16, MINECRAFT_1_20_2, false));
serverbound.register(Player.class, Player::new,
map(0x03, MINECRAFT_1_7_2, false),
map(0x0F, MINECRAFT_1_9, false),
@@ -248,7 +273,8 @@ public enum FallbackPacketRegistry {
map(0x16, MINECRAFT_1_19, false),
map(0x17, MINECRAFT_1_19_1, false),
map(0x16, MINECRAFT_1_19_3, false),
map(0x17, MINECRAFT_1_19_4, false));
map(0x17, MINECRAFT_1_19_4, false),
map(0x19, MINECRAFT_1_20_2, false));
serverbound.register(Transaction.class, Transaction::new,
map(0x0F, MINECRAFT_1_7_2, false),
map(0x05, MINECRAFT_1_9, false),
@@ -260,7 +286,8 @@ public enum FallbackPacketRegistry {
map(0x1F, MINECRAFT_1_19, false),
map(0x20, MINECRAFT_1_19_1, false),
map(0x1F, MINECRAFT_1_19_3, false),
map(0x20, MINECRAFT_1_19_4, false));
map(0x20, MINECRAFT_1_19_4, false),
map(0x23, MINECRAFT_1_20_2, false));
}
};
@@ -279,7 +306,6 @@ public enum FallbackPacketRegistry {
public static class PacketRegistry {
private final Map<ProtocolVersion, ProtocolRegistry> versions;
private boolean fallback = true;
PacketRegistry() {
Map<ProtocolVersion, ProtocolRegistry> mutableVersions = new EnumMap<>(ProtocolVersion.class);
@@ -295,9 +321,6 @@ public enum FallbackPacketRegistry {
ProtocolRegistry getProtocolRegistry(final ProtocolVersion version) {
final ProtocolRegistry registry = versions.get(version);
if (registry == null) {
if (fallback) {
return getProtocolRegistry(MINIMUM_VERSION);
}
throw new IllegalArgumentException("Could not find data for protocol version " + version);
}
return registry;
@@ -398,7 +421,7 @@ public enum FallbackPacketRegistry {
return supplier.get();
}
public int getPacketId(final FallbackPacket packet) {
public int getPacketId(final @NotNull FallbackPacket packet) {
final int id = packetClassToId.getOrDefault(packet.getClass(), Integer.MIN_VALUE);
if (id == Integer.MIN_VALUE) {
@@ -426,9 +449,9 @@ public enum FallbackPacketRegistry {
}
}
private static PacketMapping map(final int id,
final ProtocolVersion version,
final boolean encodeOnly) {
private static @NotNull PacketMapping map(final int id,
final ProtocolVersion version,
final boolean encodeOnly) {
return new PacketMapping(id, version, null, encodeOnly);
}
}
@@ -30,10 +30,12 @@ import xyz.jonesdev.sonar.common.fallback.protocol.block.BlockPosition;
import xyz.jonesdev.sonar.common.fallback.protocol.block.BlockType;
import xyz.jonesdev.sonar.common.fallback.protocol.block.ChangedBlock;
import xyz.jonesdev.sonar.common.fallback.protocol.dimension.DimensionInfo;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.Abilities;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.EmptyChunkData;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.JoinGame;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.UpdateSectionBlocks;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.config.FinishConfiguration;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.config.RegistrySync;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.Abilities;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.EmptyChunkData;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.JoinGame;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.UpdateSectionBlocks;
import java.io.InputStream;
import java.util.Collections;
@@ -43,10 +45,6 @@ import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
@UtilityClass
public class FallbackPreparer {
private final DimensionInfo DIMENSION = new DimensionInfo(
"minecraft:overworld",
"sonar", false, false
);
// Mappings
private final CompoundBinaryTag CHAT_TYPE_119;
@@ -61,25 +59,32 @@ public class FallbackPreparer {
DAMAGE_TYPE_120 = getMapping("damage_type_1_20.nbt");
}
// Abilities
public final FallbackPacket DEFAULT_ABILITIES = new Abilities((byte) 0, 0f, 0f);
// Chunks
public final FallbackPacket EMPTY_CHUNK_DATA = new EmptyChunkData(0, 0);
// Finish Configuration
public final FallbackPacket FINISH_CONFIGURATION = new FinishConfiguration();
// Registry Sync
public FallbackPacket synchronizeRegistry;
// JoinGame
private JoinGame legacyJoinGame;
private JoinGame joinGame116;
private JoinGame joinGame1162;
private JoinGame joinGame1182;
private JoinGame joinGame1191;
private JoinGame joinGame1194;
private FallbackPacket legacyJoinGame;
private FallbackPacket joinGame116;
private FallbackPacket joinGame1162;
private FallbackPacket joinGame1182;
private FallbackPacket joinGame1191;
private FallbackPacket joinGame1194;
private JoinGame joinGame120;
// Abilities
public final Abilities DEFAULT_ABILITIES = new Abilities((byte) 0, 0f, 0f);
// Chunks
public final EmptyChunkData EMPTY_CHUNK_DATA = new EmptyChunkData(0, 0);
// Dimension
private final DimensionInfo DIMENSION = new DimensionInfo(
"minecraft:overworld", "sonar", false, false);
// Collisions
public final int BLOCKS_PER_ROW = 8; // 8 * 8 = 64 (max)
private final int SPAWN_BUFFER = 10; // player spawns at 255 + 10 (10 blocks above the platform)
public final int DEFAULT_Y_COLLIDE_POSITION = 255; // 255 is max
private final int SPAWN_BUFFER = 5; // player spawns at 255 + 5 (5 blocks above the platform)
public final int DEFAULT_Y_COLLIDE_POSITION = 255; // 255 is the maximum Y position allowed
public UpdateSectionBlocks UPDATE_SECTION_BLOCKS;
private final ChangedBlock[] CHANGED_BLOCKS = new ChangedBlock[BLOCKS_PER_ROW * BLOCKS_PER_ROW];
@@ -98,6 +103,7 @@ public class FallbackPreparer {
joinGame1191 = createJoinGamePacket(MINECRAFT_1_19_1);
joinGame1194 = createJoinGamePacket(MINECRAFT_1_19_4);
joinGame120 = createJoinGamePacket(MINECRAFT_1_20);
synchronizeRegistry = new RegistrySync(joinGame120.getRegistry());
maxMovementTick = Sonar.get().getConfig().getVerification().getMaxMovementTicks();
maxPredictionTick = maxMovementTick + 10;
@@ -145,9 +151,9 @@ public class FallbackPreparer {
}
}
public static JoinGame getJoinPacketForVersion(final @NotNull ProtocolVersion protocolVersion) {
public static FallbackPacket getJoinPacketForVersion(final @NotNull ProtocolVersion protocolVersion) {
if (protocolVersion.compareTo(MINECRAFT_1_15_2) <= 0) {
return legacyJoinGame; // 1.7-1.15.2
return legacyJoinGame; // 1.7.2-1.15.2
}
if (protocolVersion.compareTo(MINECRAFT_1_16_1) <= 0) {
return joinGame116; // 1.16-1.16.1
@@ -161,13 +167,10 @@ public class FallbackPreparer {
if (protocolVersion.compareTo(MINECRAFT_1_19_3) <= 0) {
return joinGame1191; // 1.19.1-1.19.3
}
if (protocolVersion.compareTo(MINECRAFT_1_19_4) <= 0) {
if (protocolVersion.compareTo(MINECRAFT_1_19_4) == 0) {
return joinGame1194; // 1.19.4
}
if (protocolVersion.compareTo(MINECRAFT_1_20) <= 0) {
return joinGame120; // 1.20-1.20.1
}
throw new IllegalStateException("Unsupported protocol version");
return joinGame120; // 1.20-latest
}
// Partially taken from
@@ -180,7 +183,7 @@ public class FallbackPreparer {
joinGame.setDimension(0);
joinGame.setReducedDebugInfo(true);
// 1.7/1.8 don't need dimension information
// 1.7.2-1.8.9 don't need any dimension information
if (protocolVersion.compareTo(MINECRAFT_1_8) <= 0) {
return joinGame;
}
@@ -23,10 +23,12 @@ import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import java.util.function.Function;
@SuppressWarnings("unused")
@RequiredArgsConstructor
public enum BlockType {
STONE(protocolVersion -> 1),
BARRIER(protocolVersion -> {
// Link: https://github.com/PrismarineJS/minecraft-data/blob/master/data/pc/1.20/blocks.json
switch (protocolVersion) {
case MINECRAFT_1_7_2:
case MINECRAFT_1_7_6:
@@ -71,8 +73,7 @@ public enum BlockType {
case MINECRAFT_1_20:
return 10225;
case MINECRAFT_1_20_2:
// https://github.com/PrismarineJS/minecraft-data/blob/master/data/pc/1.20/blocks.json
return 1; // TODO: 1.20.2 support
return 1; // TODO: https://github.com/PrismarineJS/minecraft-data/issues/779
}
});
@@ -0,0 +1,40 @@
/*
* Copyright (C) 2023 Sonar 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 xyz.jonesdev.sonar.common.fallback.protocol.packets.config;
import io.netty.buffer.ByteBuf;
import lombok.ToString;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
@ToString
public final class FinishConfiguration implements FallbackPacket {
@Override
public void encode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
}
@Override
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
}
@Override
public int expectedMaxLength(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
return 0;
}
}
@@ -15,48 +15,41 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.protocol.packets;
package xyz.jonesdev.sonar.common.fallback.protocol.packets.config;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufOutputStream;
import io.netty.handler.codec.EncoderException;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
import net.kyori.adventure.nbt.BinaryTagTypes;
import net.kyori.adventure.nbt.CompoundBinaryTag;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_14;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_17;
import java.io.IOException;
@Getter
@ToString
@NoArgsConstructor
@AllArgsConstructor
public final class DefaultSpawnPosition implements FallbackPacket {
private int x, y, z;
private float angle;
public final class RegistrySync implements FallbackPacket {
private CompoundBinaryTag dimensionRegistry;
@Override
public void encode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_8) < 0) {
byteBuf.writeInt(x);
byteBuf.writeInt(y);
byteBuf.writeInt(z);
} else {
final long encoded = protocolVersion.compareTo(MINECRAFT_1_14) < 0
? ((x & 0x3FFFFFFL) << 38) | ((y & 0xFFFL) << 26) | (z & 0x3FFFFFFL)
: ((x & 0x3FFFFFFL) << 38) | ((y & 0x3FFFFFFL) << 12) | (z & 0xFFFL);
byteBuf.writeLong(encoded);
if (protocolVersion.compareTo(MINECRAFT_1_17) >= 0) {
byteBuf.writeFloat(angle);
}
public void encode(final @NotNull ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
try (final ByteBufOutputStream stream = new ByteBufOutputStream(byteBuf)) {
stream.writeByte(10); // CompoundTag ID
BinaryTagTypes.COMPOUND.write(dimensionRegistry, stream);
} catch (IOException exception) {
throw new EncoderException("Cannot write NBT CompoundTag");
}
}
@Override
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
throw new UnsupportedOperationException();
}
}
@@ -0,0 +1,33 @@
/*
* Copyright (C) 2023 Sonar 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 xyz.jonesdev.sonar.common.fallback.protocol.packets.login;
import io.netty.buffer.ByteBuf;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
public final class LoginAcknowledged implements FallbackPacket {
@Override
public void encode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
}
@Override
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
}
}
@@ -15,12 +15,13 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.protocol.packets;
package xyz.jonesdev.sonar.common.fallback.protocol.packets.login;
import io.netty.buffer.ByteBuf;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.netty.FastUUID;
@@ -34,12 +35,12 @@ import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.writeVarInt;
@Getter
@NoArgsConstructor
@AllArgsConstructor
public final class ServerLoginSuccess implements FallbackPacket {
public final class LoginSuccess implements FallbackPacket {
private String username;
private UUID uuid;
@Override
public void encode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
public void encode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
if (protocolVersion.compareTo(MINECRAFT_1_19) >= 0) {
writeUUID(byteBuf, uuid);
} else if (protocolVersion.compareTo(MINECRAFT_1_16) >= 0) {
@@ -15,13 +15,14 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.protocol.packets;
package xyz.jonesdev.sonar.common.fallback.protocol.packets.play;
import io.netty.buffer.ByteBuf;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
@@ -34,14 +35,14 @@ public class Abilities implements FallbackPacket {
private float flySpeed, walkSpeed;
@Override
public void encode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
public void encode(final @NotNull ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
byteBuf.writeByte(encodedFlags);
byteBuf.writeFloat(flySpeed);
byteBuf.writeFloat(walkSpeed);
}
@Override
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
public void decode(final @NotNull ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
encodedFlags = byteBuf.readByte();
flySpeed = byteBuf.readFloat();
walkSpeed = byteBuf.readFloat();
@@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.protocol.packets;
package xyz.jonesdev.sonar.common.fallback.protocol.packets.play;
import io.netty.buffer.ByteBuf;
import lombok.AllArgsConstructor;
@@ -24,6 +24,7 @@ import lombok.NoArgsConstructor;
import lombok.ToString;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.json.JSONComponentSerializer;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
@@ -44,7 +45,7 @@ public final class Chat implements FallbackPacket {
private byte position;
@Override
public void encode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
public void encode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
final String serialized = JSONComponentSerializer.json().serialize(component);
writeString(byteBuf, serialized);
@@ -15,13 +15,14 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.protocol.packets;
package xyz.jonesdev.sonar.common.fallback.protocol.packets.play;
import io.netty.buffer.ByteBuf;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
@@ -55,7 +56,7 @@ public final class ClientSettings implements FallbackPacket {
}
@Override
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
public void decode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
locale = readString(byteBuf, 16);
viewDistance = byteBuf.readByte();
chatVisibility = readVarInt(byteBuf);
@@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.protocol.packets;
package xyz.jonesdev.sonar.common.fallback.protocol.packets.play;
import io.netty.buffer.ByteBuf;
import lombok.AllArgsConstructor;
@@ -24,6 +24,7 @@ import lombok.NoArgsConstructor;
import lombok.ToString;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.json.JSONComponentSerializer;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
@@ -49,7 +50,7 @@ public final class Disconnect implements FallbackPacket {
throw new UnsupportedOperationException();
}
public static Disconnect create(final Component component) {
public static @NotNull Disconnect create(final Component component) {
return new Disconnect(JSONComponentSerializer.json().serialize(component));
}
}
@@ -15,30 +15,28 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.protocol.packets;
package xyz.jonesdev.sonar.common.fallback.protocol.packets.play;
import io.netty.buffer.ByteBuf;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
import net.kyori.adventure.nbt.CompoundBinaryTag;
import net.kyori.adventure.nbt.LongArrayBinaryTag;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
import java.util.BitSet;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.writeArray;
import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.writeCompoundTag;
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.writeVarInt;
// Partially taken from
// https://github.com/Leymooo/BungeeCord/blob/master/protocol/src/main/java/ru/leymooo/botfilter/packets/EmptyChunkPacket.java
@Getter
@NoArgsConstructor
@AllArgsConstructor
@ToString(of = {"x", "z"})
public final class EmptyChunkData implements FallbackPacket {
private int x, z;
@@ -47,22 +45,12 @@ public final class EmptyChunkData implements FallbackPacket {
private static final byte[] LEGACY_FILLER_BYTES_17 = new byte[2];
private static final byte[] LEGACY_FILLER_BYTES = new byte[256];
private static final byte[] MODERN_FILLER_BYTES = new byte[1024];
private static final long[] MASK;
private static final byte[] MODERN_FILLER_BYTES = new byte[256 * 4];
// Prepare nbt for 1.18 and pre-1.18
private static final CompoundBinaryTag MODERN_TAG, LEGACY_TAG;
static {
final BitSet bitSet = new BitSet();
for (int i = 0; i < 16; i++) {
bitSet.set(i, false);
}
MASK = bitSet.toLongArray();
MODERN_TAG = prepareNBT(false);
LEGACY_TAG = prepareNBT(true);
}
@@ -80,30 +68,26 @@ public final class EmptyChunkData implements FallbackPacket {
}
@Override
public void encode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
public void encode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
byteBuf.writeInt(x);
byteBuf.writeInt(z);
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_17) >= 0) {
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_17_1) <= 0) {
writeVarInt(byteBuf, MASK.length);
for (final long l : MASK) {
byteBuf.writeLong(l);
}
if (protocolVersion.compareTo(MINECRAFT_1_17) >= 0) {
if (protocolVersion.compareTo(MINECRAFT_1_17_1) <= 0) {
writeVarInt(byteBuf, 0); // mask
}
} else {
byteBuf.writeBoolean(true); // Full chunk
byteBuf.writeBoolean(true); // full chunk
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_16) >= 0
&& protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_16_2) < 0) {
byteBuf.writeBoolean(true); // Ignore old data
if (protocolVersion.compareTo(MINECRAFT_1_16) >= 0
&& protocolVersion.compareTo(MINECRAFT_1_16_2) < 0) {
byteBuf.writeBoolean(true); // ignore old data
}
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_8) > 0) {
if (protocolVersion.compareTo(MINECRAFT_1_8) > 0) {
writeVarInt(byteBuf, 0);
} else {
byteBuf.writeShort(1); // Fix void chunk
byteBuf.writeShort(1); // fix void chunk
}
}
@@ -129,7 +113,7 @@ public final class EmptyChunkData implements FallbackPacket {
if (protocolVersion.compareTo(MINECRAFT_1_8) >= 0) {
writeArray(byteBuf, LEGACY_FILLER_BYTES); // 1.8 - 1.12.2
} else {
byteBuf.writeInt(0); // Compressed size
byteBuf.writeInt(0); // compressed size
byteBuf.writeBytes(LEGACY_FILLER_BYTES_17); // 1.7
}
} else if (protocolVersion.compareTo(MINECRAFT_1_15) < 0) {
@@ -15,26 +15,22 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.protocol.packets;
package xyz.jonesdev.sonar.common.fallback.protocol.packets.play;
import io.netty.buffer.ByteBuf;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import net.kyori.adventure.nbt.CompoundBinaryTag;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.dimension.DimensionInfo;
import java.util.Objects;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.*;
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.writeVarInt;
// Mostly taken from
// https://github.com/PaperMC/Velocity/blob/dev/3.0.0/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/JoinGame.java
@Data
@NoArgsConstructor
@AllArgsConstructor
@@ -49,81 +45,79 @@ public final class JoinGame implements FallbackPacket {
private int viewDistance; // 1.14+
private boolean reducedDebugInfo;
private boolean showRespawnScreen;
private boolean limitedCrafting; // 1.20.2+
private String[] levelNames; // 1.16+
private CompoundBinaryTag registry; // 1.16+
private DimensionInfo dimensionInfo; // 1.16+
private CompoundBinaryTag currentDimensionData; // 1.16.2+
private short previousGamemode; // 1.16+
private int simulationDistance; // 1.18+
private int portalCooldown; // 1.20+
// Mostly taken from
// https://github.com/Nan1t/NanoLimbo/blob/main/src/main/java/ua/nanit/limbo/protocol/packets/play/PacketJoinGame.java
@Override
public void encode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
if (protocolVersion.compareTo(MINECRAFT_1_16) >= 0) {
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_20_2) >= 0) {
// haha funny, they made 1.20.2 more complicated
encode1202Up(byteBuf, protocolVersion);
} else if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_16) >= 0) {
// Minecraft 1.16 and above have significantly more complicated logic for reading this packet,
// so separate it out.
encode116Up(byteBuf, protocolVersion);
} else {
encodeLegacy(byteBuf, protocolVersion);
}
}
private void encodeLegacy(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
private void encodeLegacy(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
byteBuf.writeInt(entityId);
if (protocolVersion.compareTo(MINECRAFT_1_16_2) >= 0) {
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_16_2) >= 0) {
byteBuf.writeBoolean(isHardcore);
byteBuf.writeByte(gamemode);
} else {
byteBuf.writeByte(isHardcore ? gamemode | 0x8 : gamemode);
}
if (protocolVersion.compareTo(MINECRAFT_1_9_1) >= 0) {
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_9_1) >= 0) {
byteBuf.writeInt(dimension);
} else {
byteBuf.writeByte(dimension);
}
if (protocolVersion.compareTo(MINECRAFT_1_13_2) <= 0) {
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_13_2) <= 0) {
byteBuf.writeByte(difficulty);
}
if (protocolVersion.compareTo(MINECRAFT_1_15) >= 0) {
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_15) >= 0) {
byteBuf.writeLong(partialHashedSeed);
}
byteBuf.writeByte(1); // max players
writeString(byteBuf, Objects.requireNonNull(levelType));
if (protocolVersion.compareTo(MINECRAFT_1_14) >= 0) {
if (levelType == null) {
throw new IllegalStateException("No level type specified.");
}
writeString(byteBuf, levelType);
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_14) >= 0) {
writeVarInt(byteBuf, viewDistance);
}
if (protocolVersion.compareTo(MINECRAFT_1_8) >= 0) {
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_8) >= 0) {
byteBuf.writeBoolean(reducedDebugInfo);
}
if (protocolVersion.compareTo(MINECRAFT_1_15) >= 0) {
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_15) >= 0) {
byteBuf.writeBoolean(showRespawnScreen);
}
}
private void encode116Up(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
private void encode116Up(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
byteBuf.writeInt(entityId);
if (protocolVersion.compareTo(MINECRAFT_1_16_2) >= 0) {
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_16_2) >= 0) {
byteBuf.writeBoolean(isHardcore);
byteBuf.writeByte(gamemode);
} else {
byteBuf.writeByte(isHardcore ? gamemode | 0x8 : gamemode);
}
byteBuf.writeByte(previousGamemode);
writeStringArray(byteBuf, levelNames);
writeCompoundTag(byteBuf, registry);
if (protocolVersion.compareTo(MINECRAFT_1_16_2) >= 0
&& protocolVersion.compareTo(MINECRAFT_1_19) < 0) {
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_16_2) >= 0
&& protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_19) < 0) {
writeCompoundTag(byteBuf, currentDimensionData);
writeString(byteBuf, dimensionInfo.getIdentifier());
} else {
@@ -132,15 +126,14 @@ public final class JoinGame implements FallbackPacket {
}
byteBuf.writeLong(partialHashedSeed);
if (protocolVersion.compareTo(MINECRAFT_1_16_2) >= 0) {
writeVarInt(byteBuf, 1); // max players
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_16_2) >= 0) {
writeVarInt(byteBuf, 0); // max players
} else {
byteBuf.writeByte(1); // max players
byteBuf.writeByte(0); // max players
}
writeVarInt(byteBuf, viewDistance);
if (protocolVersion.compareTo(MINECRAFT_1_18) >= 0) {
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_18) >= 0) {
writeVarInt(byteBuf, simulationDistance);
}
@@ -150,16 +143,47 @@ public final class JoinGame implements FallbackPacket {
byteBuf.writeBoolean(dimensionInfo.isDebug());
byteBuf.writeBoolean(dimensionInfo.isFlat());
if (protocolVersion.compareTo(MINECRAFT_1_19) >= 0) {
// no last death location
byteBuf.writeBoolean(false);
// optional death location
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_19) >= 0) {
byteBuf.writeBoolean(false); // no last death position
}
if (protocolVersion.compareTo(MINECRAFT_1_20) >= 0) {
writeVarInt(byteBuf, portalCooldown);
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_20) >= 0) {
writeVarInt(byteBuf, 0); // portal cooldown
}
}
private void encode1202Up(final @NotNull ByteBuf byteBuf,
final @SuppressWarnings("unused") ProtocolVersion protocolVersion) {
byteBuf.writeInt(entityId);
byteBuf.writeBoolean(isHardcore);
writeStringArray(byteBuf, levelNames);
writeVarInt(byteBuf, 1); // max players
writeVarInt(byteBuf, viewDistance);
writeVarInt(byteBuf, simulationDistance);
byteBuf.writeBoolean(reducedDebugInfo);
byteBuf.writeBoolean(showRespawnScreen);
byteBuf.writeBoolean(limitedCrafting);
writeString(byteBuf, dimensionInfo.getIdentifier());
writeString(byteBuf, dimensionInfo.getLevelName());
byteBuf.writeLong(partialHashedSeed);
byteBuf.writeByte(gamemode);
byteBuf.writeByte(previousGamemode);
byteBuf.writeBoolean(dimensionInfo.isDebug());
byteBuf.writeBoolean(dimensionInfo.isFlat());
byteBuf.writeBoolean(false); // no last death position
writeVarInt(byteBuf, 0); // portal cooldown
}
@Override
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
throw new UnsupportedOperationException();
@@ -15,12 +15,14 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.protocol.packets;
package xyz.jonesdev.sonar.common.fallback.protocol.packets.play;
import io.netty.buffer.ByteBuf;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
@@ -30,13 +32,14 @@ import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.readVarInt;
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.writeVarInt;
@Getter
@ToString
@NoArgsConstructor
@AllArgsConstructor
public final class KeepAlive implements FallbackPacket {
private long id;
@Override
public void encode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
public void encode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
if (protocolVersion.compareTo(MINECRAFT_1_12_2) >= 0) {
byteBuf.writeLong(id);
} else if (protocolVersion.compareTo(MINECRAFT_1_8) >= 0) {
@@ -47,7 +50,7 @@ public final class KeepAlive implements FallbackPacket {
}
@Override
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
public void decode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
if (protocolVersion.compareTo(MINECRAFT_1_12_2) >= 0) {
id = byteBuf.readLong();
} else if (protocolVersion.compareTo(MINECRAFT_1_8) >= 0) {
@@ -15,13 +15,14 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.protocol.packets;
package xyz.jonesdev.sonar.common.fallback.protocol.packets.play;
import io.netty.buffer.ByteBuf;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
@@ -33,7 +34,7 @@ public final class Player implements FallbackPacket {
private boolean onGround;
@Override
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
public void decode(final @NotNull ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
onGround = byteBuf.readBoolean();
}
@@ -15,11 +15,12 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.protocol.packets;
package xyz.jonesdev.sonar.common.fallback.protocol.packets.play;
import io.netty.buffer.ByteBuf;
import lombok.Getter;
import lombok.ToString;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.netty.DeferredByteBufHolder;
@@ -50,7 +51,7 @@ public final class PluginMessage extends DeferredByteBufHolder implements Fallba
}
@Override
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
public void decode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
channel = readString(byteBuf);
if (protocolVersion.compareTo(MINECRAFT_1_13) >= 0) {
@@ -15,13 +15,14 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.protocol.packets;
package xyz.jonesdev.sonar.common.fallback.protocol.packets.play;
import io.netty.buffer.ByteBuf;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
@@ -34,7 +35,7 @@ public final class Position implements FallbackPacket {
private boolean onGround;
@Override
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
public void decode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
x = byteBuf.readDouble();
// https://github.com/jonesdevelopment/sonar/issues/20
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_8) < 0) {
@@ -51,7 +52,7 @@ public final class Position implements FallbackPacket {
}
@Override
public int expectedMaxLength(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
public int expectedMaxLength(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
return protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_8) < 0 ? 33 : 25;
}
@@ -15,13 +15,14 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.protocol.packets;
package xyz.jonesdev.sonar.common.fallback.protocol.packets.play;
import io.netty.buffer.ByteBuf;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
@@ -39,7 +40,7 @@ public final class PositionLook implements FallbackPacket {
private boolean onGround;
@Override
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
public void decode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
x = byteBuf.readDouble();
// https://github.com/jonesdevelopment/sonar/issues/20
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_8) < 0) {
@@ -53,7 +54,7 @@ public final class PositionLook implements FallbackPacket {
}
@Override
public void encode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
public void encode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
byteBuf.writeDouble(x);
byteBuf.writeDouble(y);
byteBuf.writeDouble(z);
@@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.protocol.packets;
package xyz.jonesdev.sonar.common.fallback.protocol.packets.play;
import io.netty.buffer.ByteBuf;
import lombok.AllArgsConstructor;
@@ -15,13 +15,14 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.protocol.packets;
package xyz.jonesdev.sonar.common.fallback.protocol.packets.play;
import io.netty.buffer.ByteBuf;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
@@ -36,7 +37,7 @@ public final class Transaction implements FallbackPacket {
private boolean accepted;
@Override
public void encode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
public void encode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
if (protocolVersion.compareTo(MINECRAFT_1_17) < 0) {
byteBuf.writeByte(windowId);
byteBuf.writeShort((short) id);
@@ -49,7 +50,7 @@ public final class Transaction implements FallbackPacket {
}
@Override
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
public void decode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
if (protocolVersion.compareTo(MINECRAFT_1_17) < 0) {
windowId = byteBuf.readByte();
id = byteBuf.readShort();
@@ -15,13 +15,14 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.protocol.packets;
package xyz.jonesdev.sonar.common.fallback.protocol.packets.play;
import io.netty.buffer.ByteBuf;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.block.ChangedBlock;
@@ -39,7 +40,7 @@ public final class UpdateSectionBlocks implements FallbackPacket {
private ChangedBlock[] changedBlocks;
@Override
public void encode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) throws Exception {
public void encode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) throws Exception {
if (protocolVersion.compareTo(MINECRAFT_1_16_2) < 0) {
byteBuf.writeInt(sectionX);
byteBuf.writeInt(sectionZ);
@@ -66,15 +67,9 @@ public final class UpdateSectionBlocks implements FallbackPacket {
}
}
} else {
final ChangedBlock lastChangedBlock = changedBlocks[changedBlocks.length - 1];
final int sectionY = lastChangedBlock.getPosition().getY() >> 4;
final int sectionY = changedBlocks[0].getPosition().getY() >> 4;
// Why is Mojang doing this? :(
byteBuf.writeLong(
((long) (sectionX & 0x3FFFFF) << 42)
| (sectionY & 0xFFFFF)
| ((long) (sectionZ & 0x3FFFFF) << 20)
);
byteBuf.writeLong(((sectionX & 0x3FFFFFL) << 42) | (sectionY & 0xFFFFF) | ((sectionZ & 0x3FFFFFL) << 20));
// 1.20+ don't have light update suppression
if (protocolVersion.compareTo(MINECRAFT_1_20) < 0) {
@@ -27,13 +27,11 @@ public class TrafficChannelHooker implements FallbackPipelines {
public void hook(final @NotNull ChannelPipeline pipeline,
final @NotNull String decoder,
final @NotNull String encoder) {
pipeline.channel().eventLoop().execute(() -> {
if (pipeline.get(FALLBACK_IN_TRAFFIC) == null && pipeline.get(decoder) != null) {
pipeline.addBefore(decoder, FALLBACK_IN_TRAFFIC, IncomingBandwidthDecoder.INSTANCE);
}
if (pipeline.get(FALLBACK_OUT_TRAFFIC) == null && pipeline.get(encoder) != null) {
pipeline.addBefore(encoder, FALLBACK_OUT_TRAFFIC, OutgoingBandwidthEncoder.INSTANCE);
}
});
if (pipeline.get(FALLBACK_IN_TRAFFIC) == null && pipeline.get(decoder) != null) {
pipeline.addBefore(decoder, FALLBACK_IN_TRAFFIC, IncomingBandwidthDecoder.INSTANCE);
}
if (pipeline.get(FALLBACK_OUT_TRAFFIC) == null && pipeline.get(encoder) != null) {
pipeline.addBefore(encoder, FALLBACK_OUT_TRAFFIC, OutgoingBandwidthEncoder.INSTANCE);
}
}
}
@@ -49,7 +49,7 @@ public final class BlacklistCommand extends Subcommand {
}
final String rawInetAddress = invocation.getRawArguments()[2];
final InetAddress inetAddress = checkIP(invocation.getSender(), rawInetAddress);
final InetAddress inetAddress = getInetAddressIfValid(invocation.getSender(), rawInetAddress);
// Make sure the given IP address is valid
if (inetAddress == null) return;
@@ -80,7 +80,7 @@ public final class BlacklistCommand extends Subcommand {
}
final String rawInetAddress = invocation.getRawArguments()[2];
final InetAddress inetAddress = checkIP(invocation.getSender(), rawInetAddress);
final InetAddress inetAddress = getInetAddressIfValid(invocation.getSender(), rawInetAddress);
// Make sure the given IP address is valid
if (inetAddress == null) return;
@@ -53,7 +53,7 @@ public final class VerifiedCommand extends Subcommand {
}
final String rawInetAddress = invocation.getRawArguments()[2];
final InetAddress inetAddress = checkIP(invocation.getSender(), rawInetAddress);
final InetAddress inetAddress = getInetAddressIfValid(invocation.getSender(), rawInetAddress);
// Make sure the given IP address is valid
if (inetAddress == null) return;
@@ -85,7 +85,7 @@ public final class VerifiedCommand extends Subcommand {
return;
}
final InetAddress inetAddress = checkIP(invocation.getSender(), rawInetAddress);
final InetAddress inetAddress = getInetAddressIfValid(invocation.getSender(), rawInetAddress);
// Make sure the given IP address is valid
if (inetAddress == null) return;
@@ -38,24 +38,39 @@ public class UpdateChecker {
}
};
private int convertVersion(final @NotNull String version) throws NumberFormatException, IllegalStateException {
final String[] convertedParts = version.split("\\.");
// Validate the length of the converted parts
if (convertedParts.length != 3) {
throw new IllegalStateException("Converted version length mismatch");
}
// Major updates are more important than minor updates,
// and minor updates are more important than patches.
final int convertedMajor = Integer.parseInt(convertedParts[0]) * 100000; // multiply by weight
final int convertedMinor = Integer.parseInt(convertedParts[1]) * 10000; // multiply by weight
final int patch = Integer.parseInt(convertedParts[2]);
return convertedMajor + convertedMinor + patch; // sum the converted version parts
}
public void checkForUpdates() {
ASYNC_EXECUTOR.execute(() -> {
try {
final URL url = new URL("https://api.github.com/repos/jonesdevelopment/sonar/releases/latest");
final HttpsURLConnection urlConnection = prepareConnection(url);
final JsonObject json = parseJson(urlConnection.getInputStream());
final String latestStableVersion = json.get("tag_name").getAsString();
final String latestStableRelease = json.get("tag_name").getAsString();
final int convertedLatestVersion = convertVersion(latestStableRelease);
final int convertedCurrentVersion = convertVersion(Sonar.get().getVersion().getSemanticVersion());
if (!latestStableVersion.equals(Sonar.get().getVersion().getSemanticVersion())) {
LOGGER.warn("A new version of Sonar is available: {}", latestStableVersion);
if (convertedCurrentVersion < convertedLatestVersion) {
LOGGER.warn("A new version of Sonar is available: {}", latestStableRelease);
LOGGER.warn("Please make sure to update to the latest version to ensure stability and security:");
LOGGER.warn("https://github.com/jonesdevelopment/sonar/releases");
} else if (Sonar.get().getVersion().isOnMainBranch()) {
LOGGER.info("You are currently using the latest stable release of Sonar!");
LOGGER.warn("https://github.com/jonesdevelopment/sonar/releases/tag/{}", latestStableRelease);
} else if (convertedCurrentVersion > convertedLatestVersion || !Sonar.get().getVersion().isOnMainBranch()) {
LOGGER.warn("You are currently using an unreleased version of Sonar!");
LOGGER.warn("The contributors of Sonar are not responsible for any damage done by using an unstable version");
} else {
LOGGER.warn("You are currently using the latest development version of Sonar!");
LOGGER.warn("The contributors of Sonar are not responsible for any damage done by using an unstable version" +
".");
LOGGER.info("You are currently using the latest stable release of Sonar!");
}
} catch (Exception exception) {
LOGGER.warn("Could not retrieve latest version information: {}", exception);
@@ -34,7 +34,7 @@ import java.nio.charset.StandardCharsets;
import java.util.UUID;
import java.util.regex.Pattern;
// Mostly taken from
// Taken from
// https://github.com/PaperMC/Velocity/blob/dev/3.0.0/proxy/src/main/java/com/velocitypowered/proxy/protocol/ProtocolUtils.java
@UtilityClass
public class ProtocolUtil {
+1 -2
View File
@@ -20,9 +20,8 @@ dependencies {
tasks {
processResources {
val props = mapOf(
"name" to rootProject.name,
"version" to rootProject.version.toString().split("-")[0],
"description" to "Effective Anti-bot plugin for Velocity, BungeeCord, and Bukkit (1.7-latest)",
"description" to rootProject.description,
"url" to "https://jonesdev.xyz/discord/",
"main" to "xyz.jonesdev.sonar.velocity.SonarVelocityPlugin"
)
@@ -43,10 +43,13 @@ public final class VelocitySonarCommand implements SimpleCommand, SonarCommand {
@Override
public void execute(final @NotNull Invocation invocation) {
// Create our own invocation source wrapper to handle messages properly
final InvocationSource invocationSource = new VelocityInvocationSource(invocation.source());
if (!(invocation.source() instanceof ConsoleCommandSource)) {
// Check if the player actually has the permission to run the command
if (!invocation.source().hasPermission("sonar.command")) {
invocation.source().sendMessage(Component.text(Sonar.get().getConfig().getNoPermission()));
invocationSource.sendMessage(Sonar.get().getConfig().getNoPermission());
return;
}
// Checking if it contains will only break more since it can throw
@@ -58,14 +61,14 @@ public final class VelocitySonarCommand implements SimpleCommand, SonarCommand {
// Spamming should be prevented, especially if some heavy operations are done,
// which is not the case here but let's still stay safe!
if (mapTimestamp > 0L) {
invocation.source().sendMessage(Component.text(Sonar.get().getConfig().getCommands().getCommandCoolDown()));
invocationSource.sendMessage(Sonar.get().getConfig().getCommands().getCommandCoolDown());
// Format delay
final long timestamp = System.currentTimeMillis();
final double left = 0.5D - (timestamp - mapTimestamp) / 1000D;
invocation.source().sendMessage(Component.text(Sonar.get().getConfig().getCommands().getCommandCoolDownLeft()
.replace("%time-left%", Sonar.DECIMAL_FORMAT.format(left))));
invocationSource.sendMessage(Sonar.get().getConfig().getCommands().getCommandCoolDownLeft()
.replace("%time-left%", Sonar.DECIMAL_FORMAT.format(left)));
return;
}
@@ -74,8 +77,6 @@ public final class VelocitySonarCommand implements SimpleCommand, SonarCommand {
Optional<Subcommand> subcommand = Optional.empty();
final InvocationSource invocationSource = new VelocityInvocationSource(invocation.source());
if (invocation.arguments().length > 0) {
// Search subcommand if command arguments are present
subcommand = Sonar.get().getSubcommandRegistry().getSubcommands().parallelStream()
@@ -21,7 +21,9 @@ import com.velocitypowered.api.event.PostOrder;
import com.velocitypowered.api.event.Subscribe;
import com.velocitypowered.api.event.connection.LoginEvent;
import com.velocitypowered.api.event.connection.PreLoginEvent;
import com.velocitypowered.api.proxy.crypto.IdentifiedKey;
import com.velocitypowered.api.util.GameProfile;
import com.velocitypowered.proxy.VelocityServer;
import com.velocitypowered.proxy.connection.MinecraftConnection;
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
@@ -37,23 +39,32 @@ import net.kyori.adventure.text.Component;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.ReflectiveOperationException;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.event.impl.UserVerifyJoinEvent;
import xyz.jonesdev.sonar.api.fallback.Fallback;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.api.statistics.Statistics;
import xyz.jonesdev.sonar.common.fallback.FallbackChannelHandler;
import xyz.jonesdev.sonar.common.fallback.FallbackTimeoutHandler;
import xyz.jonesdev.sonar.common.fallback.FallbackVerificationHandler;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketDecoder;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketEncoder;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.login.LoginSuccess;
import xyz.jonesdev.sonar.common.fallback.traffic.TrafficChannelHooker;
import xyz.jonesdev.sonar.velocity.SonarVelocity;
import xyz.jonesdev.sonar.velocity.fallback.dummy.DummyConnection;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.reflect.Field;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import static com.velocitypowered.proxy.network.Connections.*;
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_HANDLER;
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.*;
import static xyz.jonesdev.sonar.common.utility.geyser.GeyserUtil.isGeyserConnection;
@RequiredArgsConstructor
@@ -64,12 +75,35 @@ public final class FallbackListener {
private static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup();
private static final MethodHandle INITIAL_CONNECTION;
private static final MethodHandle CONNECTED_PLAYER;
private static final Field CONNECTION_FIELD;
private static Field SESSION_HANDLER_FIELD;
static {
CLOSED_MINECRAFT_CONNECTION = new DummyConnection(null);
try {
CONNECTED_PLAYER = MethodHandles.privateLookupIn(ConnectedPlayer.class, MethodHandles.lookup())
.findConstructor(ConnectedPlayer.class,
MethodType.methodType(
void.class,
VelocityServer.class,
GameProfile.class,
MinecraftConnection.class,
InetSocketAddress.class,
boolean.class,
IdentifiedKey.class
)
);
try {
SESSION_HANDLER_FIELD = MinecraftConnection.class.getDeclaredField("activeSessionHandler");
} catch (Throwable throwable) {
// Velocity b266 changed the field name to "activeSessionHandler"
SESSION_HANDLER_FIELD = MinecraftConnection.class.getDeclaredField("sessionHandler");
}
SESSION_HANDLER_FIELD.setAccessible(true);
CONNECTION_FIELD = InitialLoginSessionHandler.class.getDeclaredField("mcConnection");
CONNECTION_FIELD.setAccessible(true);
@@ -94,84 +128,11 @@ public final class FallbackListener {
public void handle(final @NotNull PreLoginEvent event) throws Throwable {
Statistics.TOTAL_TRAFFIC.increment();
final InetAddress inetAddress = event.getConnection().getRemoteAddress().getAddress();
val inboundConnection = (LoginInboundConnection) event.getConnection();
val initialConnection = (InitialInboundConnection) INITIAL_CONNECTION.invokeExact(inboundConnection);
final MinecraftConnection mcConnection = initialConnection.getConnection();
final Channel channel = mcConnection.getChannel();
final ChannelPipeline pipeline = channel.pipeline();
// Hook the custom traffic pipeline, so we can count the incoming and outgoing traffic
TrafficChannelHooker.hook(pipeline, MINECRAFT_DECODER, MINECRAFT_ENCODER);
// Check the blacklist here since we cannot let the player "ghost join"
if (fallback.getBlacklisted().has(inetAddress.toString())) {
markConnectionAsDead(mcConnection.getSessionHandler());
initialConnection.getConnection().closeWith(Disconnect.create(
Sonar.get().getConfig().getVerification().getBlacklisted(),
inboundConnection.getProtocolVersion()
));
return;
}
// Don't continue the verification process if the verification is disabled
if (!Sonar.get().getConfig().getVerification().isEnabled()) return;
// Check if the player is already verified.
// No one wants to be verified over and over again.
final GameProfile gameProfile = GameProfile.forOfflinePlayer(event.getUsername());
if (Sonar.get().getVerifiedPlayerController().has(inetAddress, gameProfile.getId())) return;
// Completely skip Geyser connections (for now)
if (isGeyserConnection(channel)) {
// TODO: Do we need to log this?
fallback.getLogger().info("Allowing Geyser connection: " + inetAddress);
return;
}
// We now mark the connection as dead by using our dummy connection
markConnectionAsDead(mcConnection.getSessionHandler());
// Check if the player is already queued since we don't want bots to flood the queue
if (fallback.getQueue().getQueuedPlayers().containsKey(inetAddress)) {
initialConnection.getConnection().closeWith(Disconnect.create(
Sonar.get().getConfig().getVerification().getAlreadyQueued(),
inboundConnection.getProtocolVersion()
));
return;
}
// Check if Fallback is already verifying a player
// → is another player with the same IP address connected to Fallback?
if (fallback.getConnected().containsKey(event.getUsername())
|| fallback.getConnected().containsValue(inetAddress)) {
initialConnection.getConnection().closeWith(Disconnect.create(
Sonar.get().getConfig().getVerification().getAlreadyVerifying(),
inboundConnection.getProtocolVersion()
));
return;
}
// We cannot allow too many players on our Fallback server
// There's technically no reason for limiting this, but we'll better stay safe.
if (fallback.getConnected().size() > Sonar.get().getConfig().getVerification().getMaxVerifyingPlayers()) {
initialConnection.getConnection().closeWith(Disconnect.create(
Sonar.get().getConfig().getVerification().getTooManyPlayers(),
inboundConnection.getProtocolVersion()
));
return;
}
// Check if the IP address is currently being rate-limited
if (!fallback.getRatelimiter().attempt(inetAddress)) {
initialConnection.getConnection().closeWith(Disconnect.create(
Sonar.get().getConfig().getVerification().getTooFastReconnect(),
inboundConnection.getProtocolVersion()
));
return;
}
// Run in the channel's event loop
channel.eventLoop().execute(() -> {
@@ -179,44 +140,204 @@ public final class FallbackListener {
// Do not continue if the connection is closed or marked as disconnected
if (mcConnection.isClosed() || mcConnection.isKnownDisconnect()) return;
// We have to add this pipeline to monitor whenever the client disconnects
// to remove them from the list of connected and queued players
pipeline.addFirst(FALLBACK_HANDLER, new FallbackChannelHandler(event.getUsername()));
// Hook the custom traffic pipeline, so we can count the incoming and outgoing traffic
final ChannelPipeline pipeline = channel.pipeline();
TrafficChannelHooker.hook(pipeline, MINECRAFT_DECODER, MINECRAFT_ENCODER);
// Queue the connection for further processing
fallback.getQueue().queue(inetAddress, () -> channel.eventLoop().execute(() -> {
final InetAddress inetAddress = event.getConnection().getRemoteAddress().getAddress();
// Do not continue if the connection is closed or marked as disconnected
if (mcConnection.isClosed() || mcConnection.isKnownDisconnect()) return;
try {
// Backwards compatibility for Velocity b265 and below
val activeSessionHandler = (MinecraftSessionHandler) SESSION_HANDLER_FIELD.get(mcConnection);
// Check if the username matches the valid name regex in order to prevent
// UTF-16 names or other types of flood attacks
if (!Sonar.get().getConfig().getVerification().getValidNameRegex().matcher(event.getUsername()).matches()) {
mcConnection.closeWith(Disconnect.create(Sonar.get().getConfig().getVerification().getInvalidUsername(),
mcConnection.getProtocolVersion()));
// Check the blacklist here since we cannot let the player "ghost join"
if (fallback.getBlacklisted().has(inetAddress.toString())) {
markConnectionAsDead(activeSessionHandler);
initialConnection.getConnection().closeWith(Disconnect.create(
Sonar.get().getConfig().getVerification().getBlacklisted(),
inboundConnection.getProtocolVersion()
));
return;
}
// Add better timeout handler to avoid known exploits or issues
// We also want to timeout bots quickly to avoid flooding
pipeline.replace(
READ_TIMEOUT,
READ_TIMEOUT,
new FallbackTimeoutHandler(Sonar.get().getConfig().getVerification().getReadTimeout(), TimeUnit.MILLISECONDS)
);
// Don't continue the verification process if the verification is disabled
if (!Sonar.get().getConfig().getVerification().isEnabled()) return;
// We need to determine if the player is premium before we handle the connection,
// so we can create a ConnectedPlayer instance without having to spoof this
final boolean onlineMode = !event.getResult().isForceOfflineMode()
&& (SonarVelocity.INSTANCE.getPlugin().getServer().getConfiguration().isOnlineMode()
|| event.getResult().isOnlineModeAllowed());
// Check if the player is already verified.
// No one wants to be verified over and over again.
final GameProfile gameProfile = GameProfile.forOfflinePlayer(event.getUsername());
if (Sonar.get().getVerifiedPlayerController().has(inetAddress, gameProfile.getId())) return;
// Replace the session handler to intercept all packets and handle them
mcConnection.setSessionHandler(new FallbackSessionHandler(
fallback, mcConnection, inboundConnection,
gameProfile, inetAddress, onlineMode
));
}));
// Completely skip Geyser connections (for now)
if (isGeyserConnection(channel)) {
// TODO: Do we need to log this?
fallback.getLogger().info("Allowing Geyser connection: {}", inetAddress);
return;
}
// We now mark the connection as dead by using our fake connection
markConnectionAsDead(activeSessionHandler);
// Check if the player is already queued since we don't want bots to flood the queue
if (fallback.getQueue().getQueuedPlayers().containsKey(inetAddress)) {
initialConnection.getConnection().closeWith(Disconnect.create(
Sonar.get().getConfig().getVerification().getAlreadyQueued(),
inboundConnection.getProtocolVersion()
));
return;
}
// Check if Fallback is already verifying a player
// → is another player with the same IP address connected to Fallback?
if (fallback.getConnected().containsKey(event.getUsername())
|| fallback.getConnected().containsValue(inetAddress)) {
initialConnection.getConnection().closeWith(Disconnect.create(
Sonar.get().getConfig().getVerification().getAlreadyVerifying(),
inboundConnection.getProtocolVersion()
));
return;
}
// We cannot allow too many players on our Fallback server
// There's technically no reason for limiting this, but we'll better stay safe.
if (fallback.getConnected().size() > Sonar.get().getConfig().getVerification().getMaxVerifyingPlayers()) {
initialConnection.getConnection().closeWith(Disconnect.create(
Sonar.get().getConfig().getVerification().getTooManyPlayers(),
inboundConnection.getProtocolVersion()
));
return;
}
// Check if the IP address is currently being rate-limited
if (!fallback.getRatelimiter().attempt(inetAddress)) {
initialConnection.getConnection().closeWith(Disconnect.create(
Sonar.get().getConfig().getVerification().getTooFastReconnect(),
inboundConnection.getProtocolVersion()
));
return;
}
// We have to add this pipeline to monitor whenever the client disconnects
// to remove them from the list of connected and queued players
pipeline.addFirst(FALLBACK_HANDLER, new FallbackChannelHandler(event.getUsername()));
// Queue the connection for further processing
fallback.getQueue().queue(inetAddress, () -> channel.eventLoop().execute(() -> {
// Do not continue if the connection is closed or marked as disconnected
if (mcConnection.isClosed() || mcConnection.isKnownDisconnect()) return;
// Check if the username matches the valid name regex to prevent
// UTF-16 names or other types of exploits
if (!Sonar.get().getConfig().getVerification().getValidNameRegex().matcher(event.getUsername()).matches()) {
mcConnection.closeWith(Disconnect.create(Sonar.get().getConfig().getVerification().getInvalidUsername(),
mcConnection.getProtocolVersion()));
return;
}
// Add better timeout handler to avoid known exploits or issues
// We also want to timeout bots quickly to avoid flooding
pipeline.replace(
READ_TIMEOUT,
READ_TIMEOUT,
new FallbackTimeoutHandler(Sonar.get().getConfig().getVerification().getReadTimeout(),
TimeUnit.MILLISECONDS)
);
// We need to determine if the player is premium before we handle the connection,
// so we can create a ConnectedPlayer instance without having to spoof this
final boolean onlineMode = !event.getResult().isForceOfflineMode()
&& (SonarVelocity.INSTANCE.getPlugin().getServer().getConfiguration().isOnlineMode()
|| event.getResult().isOnlineModeAllowed());
// Don't allow exceptions or disconnect messages
mcConnection.setAssociation(null);
// Create an instance for the connected player
final ConnectedPlayer connectedPlayer;
try {
connectedPlayer = (ConnectedPlayer) CONNECTED_PLAYER.invokeExact(
mcConnection.server,
gameProfile,
mcConnection,
inboundConnection.getVirtualHost().orElse(null),
onlineMode,
inboundConnection.getIdentifiedKey()
);
} catch (Throwable throwable) {
// This should not happen
fallback.getLogger().error("Error processing {}: {}", gameProfile.getName(), throwable);
mcConnection.close(true);
return;
}
// Create an instance for the Fallback connection
final FallbackUserWrapper fallbackPlayer = new FallbackUserWrapper(
fallback, connectedPlayer, mcConnection, mcConnection.getChannel(),
mcConnection.getChannel().pipeline(), inetAddress,
ProtocolVersion.fromId(connectedPlayer.getProtocolVersion().getProtocol())
);
// Disconnect if the protocol version could not be resolved
if (fallbackPlayer.getProtocolVersion().isUnknown()) {
fallbackPlayer.disconnect(Sonar.get().getConfig().getVerification().getInvalidProtocol());
return;
}
// Check if the player is already connected to the proxy but still tries to verify
if (!mcConnection.server.canRegisterConnection(connectedPlayer)) {
fallbackPlayer.disconnect(Sonar.get().getConfig().getVerification().getAlreadyConnected());
return;
}
// The player joined the verification
Statistics.REAL_TRAFFIC.increment();
if (Sonar.get().getConfig().getVerification().isLogConnections()) {
// Only log the processing message if the server isn't under attack.
// We let the user override this through the configuration.
if (!fallback.isPotentiallyUnderAttack() || Sonar.get().getConfig().getVerification().isLogDuringAttack()) {
fallback.getLogger().info(Sonar.get().getConfig().getVerification().getConnectLog()
.replace("%name%", connectedPlayer.getUsername())
.replace("%ip%", Sonar.get().getConfig().formatAddress(fallbackPlayer.getInetAddress()))
.replace("%protocol%", String.valueOf(fallbackPlayer.getProtocolVersion().getProtocol())));
}
}
// Call the VerifyJoinEvent for external API usage
Sonar.get().getEventManager().publish(new UserVerifyJoinEvent(gameProfile.getName(), fallbackPlayer));
// Mark the player as connected → verifying players
fallback.getConnected().put(gameProfile.getName(), inetAddress);
// This sometimes happens when the channel hangs, but the player is still connecting
// This also fixes a weird issue with TCPShield and other reverse proxies
if (fallbackPlayer.getPipeline().get(MINECRAFT_ENCODER) == null
|| fallbackPlayer.getPipeline().get(MINECRAFT_DECODER) == null) {
mcConnection.close(true);
return;
}
// Replace normal encoder to allow custom packets
final FallbackPacketEncoder encoder = new FallbackPacketEncoder(fallbackPlayer.getProtocolVersion());
fallbackPlayer.getPipeline().replace(MINECRAFT_ENCODER, FALLBACK_PACKET_ENCODER, encoder);
// Send LoginSuccess packet to make the client think they are joining the server
fallbackPlayer.write(new LoginSuccess(gameProfile.getName(), gameProfile.getId()));
// The LoginSuccess packet has been sent, now we can change the registry state
encoder.updateRegistry(fallbackPlayer.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_20_2) >= 0
? FallbackPacketRegistry.CONFIG : FallbackPacketRegistry.GAME);
// Replace normal decoder to allow custom packets
fallbackPlayer.getPipeline().replace(
MINECRAFT_DECODER, FALLBACK_PACKET_DECODER, new FallbackPacketDecoder(fallbackPlayer,
new FallbackVerificationHandler(fallbackPlayer, gameProfile.getName(), connectedPlayer.getUniqueId())
));
}));
} catch (Throwable throwable) {
throw new ReflectiveOperationException(throwable);
}
});
}
@@ -225,7 +346,7 @@ public final class FallbackListener {
val connectedPlayer = (ConnectedPlayer) event.getPlayer();
if (Sonar.get().getConfig().getLockdown().isEnabled()) {
if (!event.getPlayer().hasPermission("sonar.lockdown.bypass")) {
if (!event.getPlayer().hasPermission(Sonar.get().getConfig().getLockdown().getBypassPermission())) {
connectedPlayer.getConnection().closeWith(Disconnect.create(
Sonar.get().getConfig().getLockdown().getDisconnect(), connectedPlayer.getProtocolVersion()));
@@ -1,172 +0,0 @@
/*
* Copyright (C) 2023 Sonar 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 xyz.jonesdev.sonar.velocity.fallback;
import com.velocitypowered.api.proxy.crypto.IdentifiedKey;
import com.velocitypowered.api.util.GameProfile;
import com.velocitypowered.proxy.VelocityServer;
import com.velocitypowered.proxy.connection.MinecraftConnection;
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
import com.velocitypowered.proxy.connection.client.LoginInboundConnection;
import io.netty.buffer.ByteBuf;
import lombok.Getter;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.ReflectiveOperationException;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.event.impl.UserVerifyJoinEvent;
import xyz.jonesdev.sonar.api.fallback.Fallback;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.api.statistics.Statistics;
import xyz.jonesdev.sonar.common.fallback.FallbackVerificationHandler;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketDecoder;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketEncoder;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.ServerLoginSuccess;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import static com.velocitypowered.proxy.network.Connections.MINECRAFT_DECODER;
import static com.velocitypowered.proxy.network.Connections.MINECRAFT_ENCODER;
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_PACKET_DECODER;
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_PACKET_ENCODER;
@Getter
public final class FallbackSessionHandler implements MinecraftSessionHandler {
private final Fallback fallback;
private final MinecraftConnection mcConnection;
private final LoginInboundConnection inboundConnection;
private static final MethodHandle CONNECTED_PLAYER;
static {
try {
CONNECTED_PLAYER = MethodHandles.privateLookupIn(ConnectedPlayer.class, MethodHandles.lookup())
.findConstructor(ConnectedPlayer.class,
MethodType.methodType(
void.class,
VelocityServer.class,
GameProfile.class,
MinecraftConnection.class,
InetSocketAddress.class,
boolean.class,
IdentifiedKey.class
)
);
} catch (Throwable throwable) {
throw new ReflectiveOperationException(throwable);
}
}
FallbackSessionHandler(final Fallback fallback,
final @NotNull MinecraftConnection mcConnection,
final LoginInboundConnection inboundConnection,
final GameProfile gameProfile,
final InetAddress inetAddress,
final boolean onlineMode) {
this.fallback = fallback;
this.mcConnection = mcConnection;
this.inboundConnection = inboundConnection;
// Don't allow exceptions or disconnect messages
mcConnection.setAssociation(null);
// Create an instance for the connected player
final ConnectedPlayer connectedPlayer;
try {
connectedPlayer = (ConnectedPlayer) CONNECTED_PLAYER.invokeExact(
mcConnection.server,
gameProfile,
mcConnection,
inboundConnection.getVirtualHost().orElse(null),
onlineMode,
inboundConnection.getIdentifiedKey()
);
} catch (Throwable throwable) {
// This should not happen
fallback.getLogger().error("Error processing {}: {}", gameProfile.getName(), throwable);
mcConnection.close(true);
return;
}
// Create an instance for the Fallback connection
final FallbackUserWrapper fallbackPlayer = new FallbackUserWrapper(
fallback, connectedPlayer, mcConnection, mcConnection.getChannel(),
mcConnection.getChannel().pipeline(), inetAddress,
ProtocolVersion.fromId(connectedPlayer.getProtocolVersion().getProtocol())
);
// Check if the player is already connected to the proxy
if (!mcConnection.server.canRegisterConnection(connectedPlayer)) {
fallbackPlayer.disconnect("Could not find any available servers.");
return;
}
// The player joined the verification
Statistics.REAL_TRAFFIC.increment();
if (Sonar.get().getConfig().getVerification().isLogConnections()) {
// Only log the processing message if the server isn't under attack.
// We let the user override this through the configuration.
if (!fallback.isPotentiallyUnderAttack() || Sonar.get().getConfig().getVerification().isLogDuringAttack()) {
fallback.getLogger().info(Sonar.get().getConfig().getVerification().getConnectLog()
.replace("%name%", connectedPlayer.getUsername())
.replace("%ip%", Sonar.get().getConfig().formatAddress(fallbackPlayer.getInetAddress()))
.replace("%protocol%", String.valueOf(fallbackPlayer.getProtocolVersion().getProtocol())));
}
}
// Call the VerifyJoinEvent for external API usage
Sonar.get().getEventManager().publish(new UserVerifyJoinEvent(gameProfile.getName(), fallbackPlayer));
// Mark the player as connected → verifying players
fallback.getConnected().put(gameProfile.getName(), inetAddress);
// This sometimes happens when the channel hangs, but the player is still connecting
// This also fixes a weird issue with TCPShield and other reverse proxies
if (fallbackPlayer.getPipeline().get(MINECRAFT_ENCODER) == null
|| fallbackPlayer.getPipeline().get(MINECRAFT_DECODER) == null) {
mcConnection.close(true);
return;
}
// Replace normal encoder to allow custom packets
final FallbackPacketEncoder encoder = new FallbackPacketEncoder(fallbackPlayer.getProtocolVersion());
fallbackPlayer.getPipeline().replace(MINECRAFT_ENCODER, FALLBACK_PACKET_ENCODER, encoder);
// Send LoginSuccess packet to make the client think they are joining the server
fallbackPlayer.write(new ServerLoginSuccess(gameProfile.getName(), gameProfile.getId()));
// The LoginSuccess packet has been sent, now we can change the registry state
encoder.loginSuccess();
// Replace normal decoder to allow custom packets
fallbackPlayer.getPipeline().replace(
MINECRAFT_DECODER, FALLBACK_PACKET_DECODER, new FallbackPacketDecoder(fallbackPlayer,
new FallbackVerificationHandler(fallbackPlayer, gameProfile.getName(), connectedPlayer.getUniqueId())
));
}
@Override
public void handleUnknown(final ByteBuf buf) {
mcConnection.close(true);
}
}
@@ -30,7 +30,7 @@ import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.Fallback;
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.Disconnect;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.Disconnect;
import java.net.InetAddress;
@@ -1,6 +1,6 @@
{
"id": "sonar",
"name": "$name",
"name": "Sonar",
"version": "$version",
"description": "$description",
"url": "$url",