Compare commits

..
19 Commits
Author SHA1 Message Date
Jones 87e768da0e Release 2.1.4 (#375) 2024-09-03 16:29:22 +02:00
Jones daa773bff1 General code style improvements and API fixes 2024-08-31 19:11:55 +02:00
Jones 1e43835d0b Quieten decoder exceptions to improve performance (#373)
* fix: use QuietDecoderException unless debug is enabled

* fix(bukkit): allow for more debug in the decoder
2024-08-31 13:57:41 +02:00
Jones efb8b3cd39 Don't handle the handshake for server pings 2024-08-31 13:49:32 +02:00
Jones 540899e92c Fix UserBlacklistedEvent not being called 2024-08-31 13:22:39 +02:00
Jones ef6140d47f Fix wrong usage for getting the blacklist score 2024-08-30 22:12:29 +02:00
Jones 27ca5a8368 Merge failCountCache and blacklist into one 2024-08-30 22:11:34 +02:00
Jones 1a43242c24 Clean up some inconsistent code style 2024-08-30 21:17:26 +02:00
Jones 65eeec6e17 Optimize login check order/priority 2024-08-30 21:17:14 +02:00
Jones d8e4dbd6cf Use host address instead of raw InetAddress 2024-08-30 21:12:03 +02:00
Jones 32973e674f Move ratelimiter implementations to the common module 2024-08-30 20:44:14 +02:00
Jones 05fa6de119 Fix some Java naming convention violations 2024-08-30 13:15:16 +02:00
Jones cf0f4e0b96 Code style improvements and micro-optimizations 2024-08-30 12:47:34 +02:00
Jones 0ae3c6c67b Remove unnecessary ctx.channel() calls 2024-08-30 12:37:25 +02:00
Jones aae564d16c Fix missing parentheses potentially causing problems 2024-08-29 18:38:16 +02:00
Jones f6338ba844 Recode the join rate-limiter 2024-08-29 18:29:48 +02:00
Jones d892645779 Reduce usage of channel.isActive() 2024-08-29 18:14:09 +02:00
Jones def40be5c0 Release 2.1.3 (#369) 2024-08-28 15:33:53 +02:00
Jones d4bdf86fcb Fix NullPointerException 2024-08-28 15:28:10 +02:00
85 changed files with 534 additions and 563 deletions

No files matched your search

+1 -1
View File
@@ -1,3 +1,3 @@
group=xyz.jonesdev.sonar
version=2.1.2
version=2.1.4
description=A lightweight, effective, and easy-to-use anti-bot plugin for Velocity, BungeeCord, and Bukkit.
@@ -21,17 +21,17 @@ import net.kyori.adventure.audience.Audience;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import xyz.jonesdev.sonar.api.attack.AttackTracker;
import xyz.jonesdev.sonar.api.command.subcommand.SubcommandRegistry;
import xyz.jonesdev.sonar.api.config.SonarConfiguration;
import xyz.jonesdev.sonar.api.controller.VerifiedPlayerController;
import xyz.jonesdev.sonar.api.database.controller.VerifiedPlayerController;
import xyz.jonesdev.sonar.api.event.SonarEventManager;
import xyz.jonesdev.sonar.api.fallback.Fallback;
import xyz.jonesdev.sonar.api.logger.LoggerWrapper;
import xyz.jonesdev.sonar.api.notification.ActionBarNotificationHandler;
import xyz.jonesdev.sonar.api.notification.ChatNotificationHandler;
import xyz.jonesdev.sonar.api.statistics.SonarStatistics;
import xyz.jonesdev.sonar.api.timer.SystemTimer;
import xyz.jonesdev.sonar.api.verbose.Notification;
import xyz.jonesdev.sonar.api.verbose.Verbose;
import xyz.jonesdev.sonar.api.tracker.AttackTracker;
import java.text.DecimalFormat;
import java.util.UUID;
@@ -41,7 +41,7 @@ public interface Sonar {
/**
* Since we want to use Adventure on every server platform,
* we have to use their platform module to support BungeeCord and Bukkit
* we have to use their platform module to support BungeeCord and Bukkit.
*/
@ApiStatus.Internal
@Nullable Audience audience(final @Nullable UUID uniqueId);
@@ -49,14 +49,8 @@ public interface Sonar {
@ApiStatus.Internal
@NotNull Audience sender(final @NotNull Object object);
/**
* @return The platform the plugin is being run on
*/
@NotNull SonarPlatform getPlatform();
/**
* @return A small wrapper for the plugin logger so we can use the logger everywhere
*/
@NotNull LoggerWrapper getLogger();
@NotNull SonarConfiguration getConfig();
@@ -65,29 +59,20 @@ public interface Sonar {
VerifiedPlayerController getVerifiedPlayerController();
@NotNull Verbose getVerboseHandler();
@NotNull ActionBarNotificationHandler getActionBarNotificationHandler();
@NotNull Notification getNotificationHandler();
@NotNull ChatNotificationHandler getChatNotificationHandler();
@NotNull SystemTimer getLaunchTimer();
@NotNull SonarStatistics getStatistics();
/**
* Set a custom verbose handler
*/
@SuppressWarnings("unused")
void setVerboseHandler(final @NotNull Verbose verboseHandler);
void setActionBarNotificationHandler(final @NotNull ActionBarNotificationHandler notificationHandler);
/**
* Set a custom notification handler
*/
@SuppressWarnings("unused")
void setNotificationHandler(final @NotNull Notification notificationHandler);
void setChatNotificationHandler(final @NotNull ChatNotificationHandler notificationHandler);
/**
* Reloads the entire plugin
*/
void reload();
@NotNull
@@ -97,19 +82,16 @@ public interface Sonar {
@NotNull
default SonarEventManager getEventManager() {
// We don't want anyone else to create a new instance.
return SonarEventManager.INSTANCE;
}
@NotNull
default AttackTracker getAttackTracker() {
// We don't want anyone else to create a new instance.
return AttackTracker.INSTANCE;
}
@NotNull
default Fallback getFallback() {
// We don't want anyone else to create a new instance.
return Fallback.INSTANCE;
}
@@ -26,21 +26,15 @@ import java.util.function.Function;
@Getter
@RequiredArgsConstructor
public enum SonarPlatform {
BUKKIT("Bukkit", 19110,
pipeline -> pipeline.context("outbound_config") != null ? "outbound_config" : "encoder",
pipeline -> "packet_handler"),
BUNGEE("BungeeCord", 19109,
pipeline -> "packet-encoder",
pipeline -> "inbound-boss"),
VELOCITY("Velocity", 19107,
pipeline -> "minecraft-encoder",
pipeline -> "handler");
BUKKIT("Bukkit", 19110, "packet_handler",
pipeline -> pipeline.context("outbound_config") != null ? "outbound_config" : "encoder"),
BUNGEE("BungeeCord", 19109, "inbound-boss",
pipeline -> "packet-encoder"),
VELOCITY("Velocity", 19107, "handler",
pipeline -> "minecraft-encoder");
private final String displayName;
/**
* bStats service ID for the respective Sonar platform
*/
private final int metricsId;
private final Function<ChannelPipeline, String> encoderFunction;
private final Function<ChannelPipeline, String> handlerFunction;
private final String connectionHandler;
private final Function<ChannelPipeline, String> encoder;
}
@@ -19,21 +19,20 @@ package xyz.jonesdev.sonar.api;
import lombok.experimental.UtilityClass;
import org.jetbrains.annotations.NotNull;
import java.util.Objects;
import xyz.jonesdev.sonar.api.exception.AlreadyInitializedException;
@UtilityClass
public class SonarSupplier {
private Sonar sonar;
private Sonar _sonar;
public void set(final @NotNull Sonar s) {
if (sonar != null) {
throw new AlreadyInitializedException();
public void set(final @NotNull Sonar instance) {
if (_sonar != null) {
throw new AlreadyInitializedException("Sonar is already initialized");
}
sonar = Objects.requireNonNull(s);
_sonar = instance;
}
public @NotNull Sonar get() {
return sonar;
return _sonar;
}
}
@@ -42,7 +42,6 @@ public final class SonarVersion {
this.formatted = version + " (" + gitCommit + ")";
}
// Taken from
// https://github.com/PaperMC/Velocity/pull/1336/
private static @NotNull Manifest getManifest() {
final String classLocation = "/" + Sonar.class.getName().replace(".", "/") + ".class";
@@ -21,7 +21,6 @@ import lombok.Getter;
import lombok.RequiredArgsConstructor;
import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage;
import java.util.UUID;
import java.util.function.Predicate;
@@ -34,23 +33,12 @@ public final class InvocationSource {
private final Predicate<String> permissionFunction;
/**
* @return True, if {@link InvocationSource#uuid} is not null
* @apiNote This indicates a player as a player will always have a UUID
* @return true if {@link InvocationSource#uuid} is not null
*/
public boolean isPlayer() {
return uuid != null;
}
/**
* Sends a message to the command executor
*
* @apiNote We should probably use cached components...
* (See {@link InvocationSource#sendMessage(Component)})
*/
public void sendMessage(final String legacy) {
sendMessage(MiniMessage.miniMessage().deserialize(legacy));
}
/**
* Sends a message to the command executor
*/
@@ -25,34 +25,15 @@ import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface SubcommandInfo {
/**
* @return Name of the subcommand
*/
String name();
/**
* @return Aliases of the subcommand
*/
String[] aliases() default {};
/**
* @return Arguments of the subcommand
*/
String[] arguments() default {};
/**
* @return Whether the subcommands requires arguments
*/
boolean argumentsRequired() default true;
/**
* @return Whether the subcommands can only be executed by a player
*/
boolean onlyPlayers() default false;
/**
* @return Whether the subcommands can only be executed by console
*/
boolean onlyConsole() default false;
}
@@ -27,10 +27,10 @@ import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.command.SonarCommand;
import xyz.jonesdev.sonar.api.database.ormlite.H2DatabaseTypeAdapter;
import xyz.jonesdev.sonar.api.database.ormlite.MariaDbDatabaseTypeAdapter;
import xyz.jonesdev.sonar.api.database.ormlite.MysqlDatabaseTypeAdapter;
import xyz.jonesdev.sonar.api.logger.LoggerWrapper;
import xyz.jonesdev.sonar.api.ormlite.H2DatabaseTypeAdapter;
import xyz.jonesdev.sonar.api.ormlite.MariaDbDatabaseTypeAdapter;
import xyz.jonesdev.sonar.api.ormlite.MysqlDatabaseTypeAdapter;
import xyz.jonesdev.sonar.api.webhook.DiscordWebhook;
import java.io.File;
@@ -317,8 +317,8 @@ public final class SonarConfiguration {
return output;
}
public String formatAddress(final InetAddress inetAddress) {
return logPlayerAddresses ? inetAddress.toString() : "/<ip address withheld>";
public @NotNull String formatAddress(final @NotNull InetAddress inetAddress) {
return logPlayerAddresses ? inetAddress.getHostAddress() : "<ip address withheld>";
}
@Getter
@@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.api.controller;
package xyz.jonesdev.sonar.api.database.controller;
import com.alessiodp.libby.LibraryManager;
import com.j256.ormlite.dao.Dao;
@@ -31,8 +31,8 @@ import org.jetbrains.annotations.Range;
import org.jetbrains.annotations.Unmodifiable;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.config.SonarConfiguration;
import xyz.jonesdev.sonar.api.database.model.VerifiedPlayer;
import xyz.jonesdev.sonar.api.logger.LoggerWrapper;
import xyz.jonesdev.sonar.api.model.VerifiedPlayer;
import java.io.File;
import java.sql.SQLException;
@@ -74,10 +74,8 @@ public final class VerifiedPlayerController {
public VerifiedPlayerController(final @NotNull LibraryManager libraryManager) {
final SonarConfiguration.Database database = Sonar.get().getConfig().getDatabase();
// Cache selected database type, so we don't need to call Sonar.get() every time
cachedDatabaseType = database.getType();
// Don't establish a database connection if the type is NONE
if (cachedDatabaseType == SonarConfiguration.Database.Type.NONE) {
LOGGER.warn("Configure a database to save verified players.");
return;
@@ -92,7 +90,6 @@ public final class VerifiedPlayerController {
libraryManager.loadLibrary(cachedDatabaseType.getDatabaseDriver());
cachedDatabaseType.setDownloaded(true);
}
// Make sure to actually register the loader, so we can use it
cachedDatabaseType.getDatabaseType().loadDriver();
try {
@@ -120,10 +117,12 @@ public final class VerifiedPlayerController {
// Create database table
try {
TableUtils.createTableIfNotExists(connectionSource, VerifiedPlayer.class);
} catch (SQLException exception) {
// Duplicate index
// I know this isn't the best method of handling it,
// but I don't know how else I could address this issue.
} catch (SQLException ignored) {
/*
* This is caused by a duplicate index;
* I know this isn't the best method of handling it,
* but I don't know how else I could address this issue.
*/
}
dao = DaoManager.createDao(connectionSource, VerifiedPlayer.class);
@@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.api.model;
package xyz.jonesdev.sonar.api.database.model;
import com.j256.ormlite.field.DatabaseField;
import com.j256.ormlite.table.DatabaseTable;
@@ -63,7 +63,7 @@ public final class VerifiedPlayer {
public VerifiedPlayer(final @NotNull InetAddress inetAddress,
final @NotNull UUID playerUuid,
final long timestamp) {
this(inetAddress.toString(), playerUuid, timestamp);
this(inetAddress.getHostAddress(), playerUuid, timestamp);
}
public VerifiedPlayer(final @NotNull String inetAddress,
@@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.api.ormlite;
package xyz.jonesdev.sonar.api.database.ormlite;
import com.j256.ormlite.jdbc.db.H2DatabaseType;
import org.jetbrains.annotations.NotNull;
@@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.api.ormlite;
package xyz.jonesdev.sonar.api.database.ormlite;
import com.j256.ormlite.jdbc.db.MariaDbDatabaseType;
import org.jetbrains.annotations.NotNull;
@@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.api.ormlite;
package xyz.jonesdev.sonar.api.database.ormlite;
import com.j256.ormlite.jdbc.db.MysqlDatabaseType;
import org.jetbrains.annotations.NotNull;
@@ -35,12 +35,9 @@ public final class SonarEventManager {
private static final Collection<SonarEventListener> EVENT_LISTENERS = new Vector<>(0);
private static final ExecutorService EVENT_SERVICE = Executors.newSingleThreadExecutor();
/**
* Publishes the given event to all listeners
*/
@ApiStatus.Internal
public void publish(final @NotNull SonarEvent event) {
// Don't post a task if there are no listeners
// Don't post an event if there are no listeners
if (EVENT_LISTENERS.isEmpty()) {
return;
}
@@ -61,11 +58,13 @@ public final class SonarEventManager {
* Registers one (or more) event listeners
*
* @param listeners One (or more) listeners to register
* @see #unregisterListener(SonarEventListener...) Unregister an event listener
* @see #unregisterListener(SonarEventListener[]) Unregister an event listener
*/
@SuppressWarnings("unused") // External API usage
public synchronized void registerListener(final @NotNull SonarEventListener... listeners) {
EVENT_LISTENERS.addAll(Arrays.asList(listeners));
public void registerListener(final @NotNull SonarEventListener... listeners) {
synchronized (EVENT_LISTENERS) {
EVENT_LISTENERS.addAll(Arrays.asList(listeners));
}
}
/**
@@ -73,10 +72,12 @@ public final class SonarEventManager {
*
* @param listeners One (or more) listeners to unregister
* @apiNote This does not have any effect if the given listeners are not registered
* @see #registerListener(SonarEventListener...) Register an event listener
* @see #registerListener(SonarEventListener[]) Register an event listener
*/
@SuppressWarnings("unused") // External API usage
public synchronized void unregisterListener(final @NotNull SonarEventListener... listeners) {
EVENT_LISTENERS.removeAll(Arrays.asList(listeners));
public void unregisterListener(final @NotNull SonarEventListener... listeners) {
synchronized (EVENT_LISTENERS) {
EVENT_LISTENERS.removeAll(Arrays.asList(listeners));
}
}
}
@@ -20,8 +20,8 @@ package xyz.jonesdev.sonar.api.event.impl;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
import xyz.jonesdev.sonar.api.attack.AttackTracker;
import xyz.jonesdev.sonar.api.event.SonarEvent;
import xyz.jonesdev.sonar.api.tracker.AttackTracker;
@Getter
@ToString
@@ -15,7 +15,16 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.api;
package xyz.jonesdev.sonar.api.exception;
final class AlreadyInitializedException extends RuntimeException {
import org.jetbrains.annotations.NotNull;
public final class AlreadyInitializedException extends RuntimeException {
public AlreadyInitializedException(final @NotNull String message) {
super(message);
}
public Throwable fillInStackTrace() {
return this;
}
}
@@ -15,7 +15,9 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.api;
package xyz.jonesdev.sonar.api.exception;
import xyz.jonesdev.sonar.api.Sonar;
public final class ReflectiveOperationException extends RuntimeException {
public ReflectiveOperationException(final Throwable throwable) {
@@ -26,6 +26,7 @@ import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.config.SonarConfiguration;
import xyz.jonesdev.sonar.api.fallback.captcha.CaptchaGenerator;
import xyz.jonesdev.sonar.api.fallback.ratelimit.Ratelimiter;
import xyz.jonesdev.sonar.api.logger.LoggerWrapper;
import java.net.InetAddress;
@@ -43,14 +44,15 @@ public final class Fallback {
private final ConcurrentMap<InetAddress, Byte> connected = new ConcurrentHashMap<>(128);
// Cache of all blacklisted IP addresses to ensure each entry can expire after the given time
@Setter
private Cache<InetAddress, Byte> blacklist;
private Cache<String, Integer> blacklist;
@Setter
private long blacklistTime;
@Setter
private CaptchaGenerator captchaGenerator;
private final @NotNull FallbackQueue queue = new FallbackQueue();
private final @NotNull FallbackRatelimiter ratelimiter = new FallbackRatelimiter();
@Setter
private Ratelimiter<InetAddress> ratelimiter;
private final LoggerWrapper logger = new LoggerWrapper() {
@@ -1,65 +0,0 @@
/*
* Copyright (C) 2024 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.api.fallback;
import com.github.benmanes.caffeine.cache.Cache;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.jetbrains.annotations.NotNull;
import java.net.InetAddress;
@Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public final class FallbackRatelimiter {
private long timeout;
private Cache<InetAddress, Long> attemptCache;
@Getter
private Cache<InetAddress, Integer> failCountCache;
/**
* Attempts to rate-limit the client
*
* @param inetAddress IP address of the player
* @return Whether the player is allowed to verify
*/
public boolean attempt(final @NotNull InetAddress inetAddress) {
final long expectedTimestamp = System.currentTimeMillis() + timeout;
// Check if the time since the last join has not exceeded the timeout
// The idea for this check was Taken from Velocity
final long last = attemptCache.get(inetAddress, result -> expectedTimestamp);
return expectedTimestamp == last;
}
/**
* Checks if the player is failing verifications too often
* Increments the number of times this user has failed the verification
*
* @param inetAddress IP address of the player
* @param count Count of previously failed verifications
*/
public void incrementFails(final @NotNull InetAddress inetAddress, final int count) {
// Make sure to remove the old values from the cache
if (count > 0) {
failCountCache.invalidate(inetAddress);
}
failCountCache.put(inetAddress, count + 1);
}
}
@@ -15,12 +15,11 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.api.command.subcommand.argument;
package xyz.jonesdev.sonar.api.fallback.ratelimit;
public @interface Argument {
import org.jetbrains.annotations.NotNull;
/**
* @return Returns the name of the argument
*/
String value();
@FunctionalInterface
public interface Ratelimiter<T> {
boolean attempt(final @NotNull T t);
}
@@ -33,7 +33,6 @@ public interface LoggerWrapper {
void error(final String message, final Object... args);
// Taken from
// https://github.com/j256/ormlite-core/blob/master/src/main/java/com/j256/ormlite/logger/Logger.java
String ARG_STRING = "{}";
int ARG_STRING_LENGTH = ARG_STRING.length();
@@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.api.verbose;
package xyz.jonesdev.sonar.api.notification;
import lombok.Getter;
import net.kyori.adventure.audience.Audience;
@@ -23,21 +23,21 @@ import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.attack.AttackTracker;
import xyz.jonesdev.sonar.api.timer.SystemTimer;
import xyz.jonesdev.sonar.api.tracker.AttackTracker;
import java.util.UUID;
import static xyz.jonesdev.sonar.api.Sonar.DECIMAL_FORMAT;
import static xyz.jonesdev.sonar.api.jvm.JVMProcessInformation.*;
import static xyz.jonesdev.sonar.api.profiler.SimpleProcessProfiler.*;
import static xyz.jonesdev.sonar.api.timer.SystemTimer.DATE_FORMATTER;
@Getter
public final class Verbose extends Observable {
public final class ActionBarNotificationHandler extends NotificationHandler {
private int animationIndex;
@Override
public void observe() {
public void handleNotification() {
// Don't prepare component if there are no subscribers
if (subscribers.isEmpty()) {
return;
@@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.api.verbose;
package xyz.jonesdev.sonar.api.notification;
import lombok.Getter;
import net.kyori.adventure.audience.Audience;
@@ -29,13 +29,13 @@ import java.util.List;
import java.util.UUID;
import static xyz.jonesdev.sonar.api.Sonar.DECIMAL_FORMAT;
import static xyz.jonesdev.sonar.api.jvm.JVMProcessInformation.*;
import static xyz.jonesdev.sonar.api.profiler.SimpleProcessProfiler.*;
@Getter
public final class Notification extends Observable {
public final class ChatNotificationHandler extends NotificationHandler {
@Override
public void observe() {
public void handleNotification() {
// Don't prepare component if there are no subscribers
if (subscribers.isEmpty()) {
return;
@@ -74,7 +74,7 @@ public final class Notification extends Observable {
}
// Send the title and chat messages to all online players
for (final UUID subscriber : Sonar.get().getNotificationHandler().getSubscribers()) {
for (final UUID subscriber : Sonar.get().getChatNotificationHandler().getSubscribers()) {
final Audience audience = Sonar.get().audience(subscriber);
if (audience == null) continue;
@@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.api.verbose;
package xyz.jonesdev.sonar.api.notification;
import lombok.Getter;
import org.jetbrains.annotations.NotNull;
@@ -25,10 +25,10 @@ import java.util.UUID;
import java.util.Vector;
@Getter
public abstract class Observable {
public abstract class NotificationHandler {
protected final @NotNull Collection<UUID> subscribers = new Vector<>(0);
public abstract void observe();
public abstract void handleNotification();
/**
* @param uuid UUID of the player
@@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.api.jvm;
package xyz.jonesdev.sonar.api.profiler;
import com.sun.management.OperatingSystemMXBean;
import lombok.experimental.UtilityClass;
@@ -24,7 +24,7 @@ import java.lang.management.ManagementFactory;
import java.text.DecimalFormat;
@UtilityClass
public class JVMProcessInformation {
public class SimpleProcessProfiler {
private final Runtime RUNTIME = Runtime.getRuntime();
private final OperatingSystemMXBean MX = (OperatingSystemMXBean) ManagementFactory.getOperatingSystemMXBean();
private final char[] MEMORY_UNITS = {'K', 'M', 'G', 'T', 'P', 'E'};
@@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.api.attack;
package xyz.jonesdev.sonar.api.tracker;
import lombok.*;
import org.jetbrains.annotations.Nullable;
@@ -27,12 +27,13 @@ import xyz.jonesdev.sonar.api.timer.SystemTimer;
import java.util.Optional;
import static xyz.jonesdev.sonar.api.jvm.JVMProcessInformation.*;
import static xyz.jonesdev.sonar.api.profiler.SimpleProcessProfiler.*;
@Getter
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class AttackTracker {
public static final AttackTracker INSTANCE = new AttackTracker();
private @Nullable AttackStatistics currentAttack;
private int attackThreshold;
@@ -68,7 +69,7 @@ public final class AttackTracker {
currentAttack.successfulVerifications = Sonar.get().getVerifiedPlayerController().estimatedSize();
currentAttack.failedVerifications = Sonar.get().getStatistics().getTotalFailedVerifications();
Sonar.get().getEventManager().publish(new AttackDetectedEvent());
Sonar.get().getNotificationHandler().observe();
Sonar.get().getChatNotificationHandler().handleNotification();
} else {
// Reset attack timer if we're still under attack
currentAttack.timer.reset();
@@ -143,11 +144,9 @@ public final class AttackTracker {
// Reset the attack status
currentAttack = null;
// Reset threshold
attackThreshold = 0;
}
} else {
// Reset threshold
attackThreshold = 0;
}
}
@@ -120,8 +120,8 @@ public final class SonarBukkit extends SonarBootstrap<SonarBukkitPlugin> {
@Override
public void disable() {
// Make sure to properly stop the metrics
if (metrics != null) {
// Make sure to properly shutdown bStats metrics
metrics.shutdown();
}
}
@@ -45,7 +45,7 @@ public final class BukkitSonarCommand implements CommandExecutor, TabExecutor, S
sender::hasPermission);
// Pass the invocation source and command arguments to our command handler
handle(invocationSource, args);
return true; // Valid
return true;
}
@Override
@@ -1,3 +1,20 @@
/*
* Copyright (C) 2024 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.bukkit.fallback;
import io.netty.buffer.ByteBufAllocator;
@@ -7,6 +24,7 @@ import io.netty.util.AttributeKey;
import io.netty.util.concurrent.EventExecutor;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.NotNull;
import java.net.SocketAddress;
import java.util.HashSet;
@@ -16,7 +34,7 @@ public final class ChannelInactiveListener extends ChannelInboundHandlerAdapter
private final Set<ChannelInboundHandler> handlers = new HashSet<>();
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
public void channelInactive(final @NotNull ChannelHandlerContext ctx) throws Exception {
final ProxiedChannelHandlerContext proxy = new ProxiedChannelHandlerContext(ctx);
for (final ChannelInboundHandler handler : handlers) {
try {
@@ -28,9 +46,9 @@ public final class ChannelInactiveListener extends ChannelInboundHandlerAdapter
super.channelInactive(ctx);
}
public void add(ChannelHandler handler) {
if (handler instanceof ChannelInboundHandler) {
handlers.add((ChannelInboundHandler) handler);
public void add(final @NotNull ChannelHandler channelHandler) {
if (channelHandler instanceof ChannelInboundHandler) {
handlers.add((ChannelInboundHandler) channelHandler);
}
}
@@ -20,7 +20,7 @@ package xyz.jonesdev.sonar.bukkit.fallback;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.CorruptedFrameException;
import io.netty.handler.codec.DecoderException;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.FallbackInboundHandlerAdapter;
@@ -28,14 +28,15 @@ import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.handshake.HandshakePacket;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.login.LoginStartPacket;
import xyz.jonesdev.sonar.common.util.exception.QuietDecoderException;
import java.net.InetSocketAddress;
import java.util.Objects;
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_INACTIVE_LISTENER;
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_INBOUND_HANDLER;
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry.Direction.SERVERBOUND;
import static xyz.jonesdev.sonar.common.fallback.protocol.packets.handshake.HandshakePacket.*;
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.DEBUG;
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.readVarInt;
final class FallbackBukkitInboundHandler extends FallbackInboundHandlerAdapter {
@@ -44,7 +45,7 @@ final class FallbackBukkitInboundHandler extends FallbackInboundHandlerAdapter {
updateRegistry(FallbackPacketRegistry.HANDSHAKE, DEFAULT_PROTOCOL_VERSION);
channelRemovalListener = (pipeline, name, handler) -> {
final var inactiveListener = (ChannelInactiveListener) pipeline.get(FALLBACK_INACTIVE_LISTENER);
final ChannelInactiveListener inactiveListener = pipeline.get(ChannelInactiveListener.class);
if (inactiveListener != null) {
inactiveListener.add(handler);
@@ -66,8 +67,11 @@ final class FallbackBukkitInboundHandler extends FallbackInboundHandlerAdapter {
if (msg instanceof ByteBuf) {
final ByteBuf byteBuf = (ByteBuf) msg;
// Reset the reader index, so we can read the content
// Make sure to store the original reader index
/*
* Reset the reader index, so we can read the content;
* We also have to make sure to store the original reader index.
* We do this to 'spoof' the packet state - we want to read it without the server knowing.
*/
final int originalReaderIndex = byteBuf.readerIndex();
byteBuf.readerIndex(0);
@@ -82,7 +86,6 @@ final class FallbackBukkitInboundHandler extends FallbackInboundHandlerAdapter {
// Skip the packet if it's not registered within Sonar's packet registry
if (packet == null) {
// Release the ByteBuf to avoid memory leaks
byteBuf.release();
return;
}
@@ -91,27 +94,27 @@ final class FallbackBukkitInboundHandler extends FallbackInboundHandlerAdapter {
// Try to decode the packet for the given protocol version
packet.decode(byteBuf, protocolVersion == null ? DEFAULT_PROTOCOL_VERSION : protocolVersion);
} catch (Throwable throwable) {
// Release the ByteBuf to avoid memory leaks
byteBuf.release();
throw new CorruptedFrameException("Failed to decode packet");
throw DEBUG ? new DecoderException(throwable) : QuietDecoderException.INSTANCE;
}
// Check if the packet still has bytes left after we decoded it
if (byteBuf.isReadable()) {
// Release the ByteBuf to avoid memory leaks
byteBuf.release();
throw new CorruptedFrameException("Could not read packet to end");
throw DEBUG ? new DecoderException("Could not read packet to end") : QuietDecoderException.INSTANCE;
}
// Useful resources:
// https://wiki.vg/Protocol#Handshaking
// https://wiki.vg/Protocol#Login
/*
* Useful resources:
* https://wiki.vg/Protocol#Handshaking
* https://wiki.vg/Protocol#Login
*/
if (packet instanceof HandshakePacket) {
final HandshakePacket handshake = (HandshakePacket) packet;
switch (handshake.getIntent()) {
case STATUS:
// We don't care about server pings or transfers; remove the handler
ctx.channel().pipeline().remove(this);
ctx.pipeline().remove(this);
break;
case LOGIN:
case TRANSFER:
@@ -121,23 +124,22 @@ final class FallbackBukkitInboundHandler extends FallbackInboundHandlerAdapter {
updateRegistry(FallbackPacketRegistry.LOGIN, Objects.requireNonNull(protocolVersion));
break;
default:
throw new CorruptedFrameException("Unknown handshake intent " + handshake.getIntent());
throw DEBUG ? new DecoderException("Bad intent " + handshake.getIntent()) : QuietDecoderException.INSTANCE;
}
} else if (packet instanceof LoginStartPacket) {
final LoginStartPacket loginStart = (LoginStartPacket) packet;
final InetSocketAddress socketAddress = (InetSocketAddress) ctx.channel().remoteAddress();
// We've done our job - deject this pipeline
ctx.channel().pipeline().remove(this);
ctx.pipeline().remove(this);
// Let Sonar process the login packet
handleLogin(ctx.channel(), ctx, () -> {
byteBuf.readerIndex(originalReaderIndex);
ctx.fireChannelRead(byteBuf.retain());
final ChannelHandler inboundHandler = ctx.channel().pipeline().remove(FALLBACK_INBOUND_HANDLER);
final ChannelHandler inboundHandler = ctx.pipeline().remove(FALLBACK_INBOUND_HANDLER);
if (inboundHandler != null) {
channelRemovalListener.accept(ctx.pipeline(), FALLBACK_INBOUND_HANDLER, inboundHandler);
}
}, loginStart.getUsername(), socketAddress);
// Release the ByteBuf to avoid memory leaks
byteBuf.release();
return;
}
@@ -25,10 +25,8 @@ import lombok.Getter;
import lombok.experimental.UtilityClass;
import org.bukkit.Bukkit;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.ReflectiveOperationException;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.fallback.FallbackPipelines;
import xyz.jonesdev.sonar.bukkit.SonarBukkit;
import xyz.jonesdev.sonar.api.exception.ReflectiveOperationException;
import xyz.jonesdev.sonar.common.fallback.netty.FallbackInjectedChannelInitializer;
import java.lang.reflect.Field;
@@ -36,7 +34,9 @@ import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.List;
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_INACTIVE_LISTENER;
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_PACKET_DECODER;
import static xyz.jonesdev.sonar.bukkit.SonarBukkit.INITIALIZE_LISTENER;
// Check out these links if you want to see some more magic
// https://github.com/retrooper/packetevents/blob/2.0/spigot/src/main/java/io/github/retrooper/packetevents/util/SpigotReflectionUtil.java
@@ -196,8 +196,7 @@ public class FallbackBukkitInjector {
for (int i = 0; i < 2; i++) {
final Field field = getFieldAt(SERVER_CONNECTION_CLASS, List.class, i);
// Check if the field has the correct generic type;
// We need this field to be List<ChannelFuture>
// Check if the field has the correct generic type; We need this field to be List<ChannelFuture>
if (!field.getGenericType().getTypeName().contains(ChannelFuture.class.getName())) {
continue;
}
@@ -235,27 +234,29 @@ public class FallbackBukkitInjector {
childHandlerField.setAccessible(true);
final var originalInitializer = (ChannelInitializer<Channel>) childHandlerField.get(bootstrap);
ChannelHandler finalBootstrap = bootstrap;
/*
* Inject our own channel initializer into the original field
* If the plugin is not fully enabled but executes the injection,
* put an initializer to close the new connection automatically.
*/
if (!INITIALIZE_LISTENER.isDone()) {
childHandlerField.set(bootstrap, new ChannelInitializer<>() {
// Inject our own channel initializer into the original field
// If the plugin is not fully enabled but executes the injection,
// put an initializer to close the new connection automatically.
if (!SonarBukkit.INITIALIZE_LISTENER.isDone()) {
final ChannelInitializer<Channel> initializer = new ChannelInitializer<>() {
@Override
protected void initChannel(Channel channel) {
protected void initChannel(final @NotNull Channel channel) {
channel.close();
}
};
childHandlerField.set(finalBootstrap, initializer);
});
}
SonarBukkit.INITIALIZE_LISTENER.thenAccept(v -> {
final ChannelHandler _bootstrap = bootstrap;
INITIALIZE_LISTENER.thenAccept(__ -> {
try {
childHandlerField.set(finalBootstrap, new FallbackInjectedChannelInitializer(originalInitializer,
childHandlerField.set(_bootstrap, new FallbackInjectedChannelInitializer(originalInitializer,
pipeline -> {
pipeline.addAfter("splitter", FALLBACK_PACKET_DECODER, new FallbackBukkitInboundHandler());
pipeline.addFirst(FallbackPipelines.FALLBACK_INACTIVE_LISTENER, new ChannelInactiveListener());
pipeline.addFirst(FALLBACK_INACTIVE_LISTENER, new ChannelInactiveListener());
}
));
} catch (IllegalAccessException exception) {
@@ -34,7 +34,6 @@ import java.lang.reflect.Method;
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_BANDWIDTH;
// Taken from
// https://github.com/ViaVersion/ViaVersion/blob/master/bukkit/src/main/java/com/viaversion/viaversion/bukkit/listeners/JoinListener.java
public final class BukkitJoinListener implements Listener {
private static MethodHandle _handleMethod;
@@ -84,7 +83,7 @@ public final class BukkitJoinListener implements Listener {
}
// Make sure the player is not a fake player
if (FakeChannelUtil.isFakePlayer(channel)) {
if (FakeChannelUtil.isFakeChannel(channel)) {
return;
}
@@ -108,8 +108,8 @@ public final class SonarBungee extends SonarBootstrap<SonarBungeePlugin> {
@Override
public void disable() {
// Make sure to properly stop the metrics
if (metrics != null) {
// Make sure to properly shutdown bStats metrics
metrics.shutdown();
}
}
@@ -26,13 +26,15 @@ import net.md_5.bungee.protocol.PacketWrapper;
import net.md_5.bungee.protocol.packet.Handshake;
import net.md_5.bungee.protocol.packet.LoginRequest;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.ReflectiveOperationException;
import xyz.jonesdev.sonar.api.exception.ReflectiveOperationException;
import xyz.jonesdev.sonar.common.fallback.FallbackInboundHandlerAdapter;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.net.InetSocketAddress;
import static xyz.jonesdev.sonar.common.fallback.protocol.packets.handshake.HandshakePacket.STATUS;
final class FallbackBungeeInboundHandler extends FallbackInboundHandlerAdapter {
private static final MethodHandle CHANNEL_WRAPPER_GETTER;
@@ -48,20 +50,20 @@ final class FallbackBungeeInboundHandler extends FallbackInboundHandlerAdapter {
@Override
public void channelRead(final @NotNull ChannelHandlerContext ctx, final @NotNull Object msg) throws Exception {
// Intercept any packets processed by BungeeCord
if (msg instanceof PacketWrapper) {
final PacketWrapper packetWrapper = (PacketWrapper) msg;
final DefinedPacket wrappedPacket = packetWrapper.packet;
// Don't handle any invalid packets
// Skip any unknown or invalid packets
if (wrappedPacket != null) {
final Channel channel = ctx.channel();
// Intercept any handshake packet by the client
if (wrappedPacket instanceof Handshake) {
final Handshake handshake = (Handshake) wrappedPacket;
handleHandshake(channel, handshake.getHost(), handshake.getProtocolVersion());
// We don't care about server pings; remove the handler
if (handshake.getRequestedProtocol() == 1) {
ctx.channel().pipeline().remove(this);
if (handshake.getRequestedProtocol() == STATUS) {
ctx.pipeline().remove(this);
} else {
handleHandshake(channel, handshake.getHost(), handshake.getProtocolVersion());
}
}
// Intercept any server login packet by the client
@@ -77,7 +79,7 @@ final class FallbackBungeeInboundHandler extends FallbackInboundHandlerAdapter {
}
final InetSocketAddress socketAddress = (InetSocketAddress) channelWrapper.getRemoteAddress();
// We've done our job - deject this pipeline
ctx.channel().pipeline().remove(this);
ctx.pipeline().remove(this);
// Make sure to mark this packet as released
packetWrapper.trySingleRelease();
// Let Sonar process the login packet
@@ -86,7 +88,6 @@ final class FallbackBungeeInboundHandler extends FallbackInboundHandlerAdapter {
}
}
}
// Make sure to let the server handle the rest
ctx.fireChannelRead(msg);
}
}
@@ -47,12 +47,10 @@ public class FallbackBungeeInjector {
unsafeField.setAccessible(true);
final Unsafe unsafe = (Unsafe) unsafeField.get(null);
// Get the base object (the object containing the field)
final Object base = unsafe.staticFieldBase(childField);
// Get the offset of the static field within its class
final long offset = unsafe.staticFieldOffset(childField);
// Replace the original channel initializer
// Replace the original channel initializer with our new injected initializer
unsafe.putObject(base, offset, injectedInitializer);
} catch (Exception exception) {
Sonar.get().getLogger().error("An error occurred while injecting {}", exception);
@@ -31,30 +31,32 @@ import xyz.jonesdev.sonar.api.SonarPlatform;
import xyz.jonesdev.sonar.api.SonarSupplier;
import xyz.jonesdev.sonar.api.command.subcommand.SubcommandRegistry;
import xyz.jonesdev.sonar.api.config.SonarConfiguration;
import xyz.jonesdev.sonar.api.controller.VerifiedPlayerController;
import xyz.jonesdev.sonar.api.database.controller.VerifiedPlayerController;
import xyz.jonesdev.sonar.api.notification.ActionBarNotificationHandler;
import xyz.jonesdev.sonar.api.notification.ChatNotificationHandler;
import xyz.jonesdev.sonar.api.statistics.SonarStatistics;
import xyz.jonesdev.sonar.api.timer.SystemTimer;
import xyz.jonesdev.sonar.api.verbose.Notification;
import xyz.jonesdev.sonar.api.verbose.Verbose;
import xyz.jonesdev.sonar.captcha.StandardCaptchaGenerator;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer;
import xyz.jonesdev.sonar.common.fallback.ratelimit.CaffeineCacheRatelimiter;
import xyz.jonesdev.sonar.common.fallback.ratelimit.NoopCacheRatelimiter;
import xyz.jonesdev.sonar.common.service.SonarServiceManager;
import xyz.jonesdev.sonar.common.statistics.GlobalSonarStatistics;
import xyz.jonesdev.sonar.common.subcommands.*;
import xyz.jonesdev.sonar.common.subcommand.*;
import xyz.jonesdev.sonar.common.update.UpdateChecker;
import xyz.jonesdev.sonar.common.util.ProtocolUtil;
import java.io.File;
import java.time.Duration;
import java.util.concurrent.TimeUnit;
@Getter
@RequiredArgsConstructor
public abstract class SonarBootstrap<T> implements Sonar {
private T plugin;
@Setter
private Verbose verboseHandler;
private ActionBarNotificationHandler actionBarNotificationHandler;
@Setter
private Notification notificationHandler;
private ChatNotificationHandler chatNotificationHandler;
private SonarConfiguration config;
private VerifiedPlayerController verifiedPlayerController;
private final LibraryManager libraryManager;
@@ -79,8 +81,8 @@ public abstract class SonarBootstrap<T> implements Sonar {
this.platform = platform;
// Load the rest of the components
this.statistics = new GlobalSonarStatistics();
this.verboseHandler = new Verbose();
this.notificationHandler = new Notification();
this.actionBarNotificationHandler = new ActionBarNotificationHandler();
this.chatNotificationHandler = new ChatNotificationHandler();
this.config = new SonarConfiguration(dataDirectory);
// Register all subcommands
this.subcommandRegistry = new SubcommandRegistry();
@@ -148,30 +150,14 @@ public abstract class SonarBootstrap<T> implements Sonar {
getLogger().warn("Reloading after changing the database type is not recommended as it may cause data loss.");
}
// Update the CAPTCHA generator if necessary
if (getConfig().getVerification().getMap().getTiming() == SonarConfiguration.Verification.Timing.NEVER) {
getFallback().setCaptchaGenerator(null);
} else if (getFallback().getCaptchaGenerator() == null
|| getFallback().getCaptchaGenerator() instanceof StandardCaptchaGenerator) {
getFallback().setCaptchaGenerator(new StandardCaptchaGenerator(128, 128,
Sonar.get().getConfig().getVerification().getMap().getBackgroundImage()));
} else {
getLogger().info("Custom CAPTCHA generator detected, skipping reinitialization.");
}
// Prepare cached packets
getLogger().info("Taking cached snapshots of all packets...");
FallbackPreparer.prepare();
// Update ratelimiter caches
getFallback().getRatelimiter().setAttemptCache(Caffeine.newBuilder()
.expireAfterWrite(Duration.ofMillis(getConfig().getVerification().getReconnectDelay()))
.ticker(Ticker.systemTicker())
.build());
getFallback().getRatelimiter().setFailCountCache(Caffeine.newBuilder()
.expireAfterWrite(Duration.ofMillis(getConfig().getVerification().getRememberTime()))
.ticker(Ticker.systemTicker())
.build());
// Update ratelimiter cache
getFallback().setRatelimiter(getConfig().getVerification().getReconnectDelay() > 0L
? new CaffeineCacheRatelimiter(getConfig().getVerification().getReconnectDelay(), TimeUnit.MILLISECONDS)
: NoopCacheRatelimiter.INSTANCE);
// Update blacklist cache
final long blacklistTime = getConfig().getVerification().getBlacklistTime();
@@ -26,8 +26,8 @@ import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.jetbrains.annotations.NotNull;
import static xyz.jonesdev.sonar.common.statistics.CachedBandwidthStatistics.INCOMING;
import static xyz.jonesdev.sonar.common.statistics.CachedBandwidthStatistics.OUTGOING;
import static xyz.jonesdev.sonar.common.statistics.BandwidthStatistics.INCOMING;
import static xyz.jonesdev.sonar.common.statistics.BandwidthStatistics.OUTGOING;
@ChannelHandler.Sharable
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@@ -18,19 +18,18 @@
package xyz.jonesdev.sonar.common.fallback;
import io.netty.channel.*;
import io.netty.handler.codec.CorruptedFrameException;
import lombok.AllArgsConstructor;
import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import xyz.jonesdev.sonar.api.Sonar;
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.FallbackPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketEncoder;
import xyz.jonesdev.sonar.common.statistics.GlobalSonarStatistics;
import xyz.jonesdev.sonar.common.util.GeyserUtil;
import xyz.jonesdev.sonar.common.util.exception.QuietDecoderException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
@@ -51,8 +50,6 @@ public abstract class FallbackInboundHandlerAdapter extends ChannelInboundHandle
protected ProtocolVersion protocolVersion;
protected RemovalListener channelRemovalListener = RemovalListener.EMPTY;
protected static final Fallback FALLBACK = Sonar.get().getFallback();
/**
* Validates and handles incoming handshake packets
*
@@ -64,11 +61,11 @@ public abstract class FallbackInboundHandlerAdapter extends ChannelInboundHandle
final int protocol) throws Exception {
// Check if the hostname is invalid
if (hostname.isEmpty()) {
throw new CorruptedFrameException("Hostname is empty");
throw QuietDecoderException.INSTANCE;
}
// Check if the player has already sent a handshake packet
if (protocolVersion != null) {
throw new CorruptedFrameException("Already sent handshake");
throw QuietDecoderException.INSTANCE;
}
// Store the protocol version
protocolVersion = ProtocolVersion.fromId(protocol);
@@ -91,13 +88,13 @@ public abstract class FallbackInboundHandlerAdapter extends ChannelInboundHandle
final @NotNull InetSocketAddress socketAddress) throws Exception {
// Check if the player has already sent a login packet
if (this.username != null) {
throw new CorruptedFrameException("Already sent login");
throw QuietDecoderException.INSTANCE;
}
// Connections from unknown protocol versions will be discarded
// as this is the safest way of handling unwanted connections
if (protocolVersion.isUnknown()) {
// Sonar does not support snapshots or Minecraft versions older than 1.7.2
throw new CorruptedFrameException("Unknown protocol version");
throw QuietDecoderException.INSTANCE;
}
// Increase joins per second for the action bar verbose
GlobalSonarStatistics.countLogin();
@@ -105,15 +102,10 @@ public abstract class FallbackInboundHandlerAdapter extends ChannelInboundHandle
this.username = username;
final FallbackInboundHandler inboundHandler = channel.pipeline().get(FallbackInboundHandler.class);
Objects.requireNonNull(inboundHandler).setInetAddress(socketAddress.getAddress());
// Check the blacklist here since we cannot let the player "ghost join"
if (FALLBACK.getBlacklist().asMap().containsKey(inboundHandler.getInetAddress())) {
customDisconnect(channel, protocolVersion, blacklisted);
return;
}
final String hostAddress = socketAddress.getAddress().getHostAddress();
// Check if Fallback is already verifying a player with the same IP address
if (FALLBACK.getConnected().containsKey(inboundHandler.getInetAddress())) {
if (Sonar.get().getFallback().getConnected().containsKey(inboundHandler.getInetAddress())) {
customDisconnect(channel, protocolVersion, alreadyVerifying);
return;
}
@@ -125,6 +117,14 @@ public abstract class FallbackInboundHandlerAdapter extends ChannelInboundHandle
return;
}
// Check if the player failed the verification too many times
final int score = Sonar.get().getFallback().getBlacklist().asMap().getOrDefault(hostAddress, 0);
final int limit = Sonar.get().getConfig().getVerification().getBlacklistThreshold();
if (score >= limit) {
customDisconnect(channel, protocolVersion, blacklisted);
return;
}
// Don't continue the verification process if the verification is disabled
if (!Sonar.get().getFallback().shouldVerifyNewPlayers()) {
initialLogin(ctx, inboundHandler.getInetAddress(), loginPacket);
@@ -141,13 +141,13 @@ public abstract class FallbackInboundHandlerAdapter extends ChannelInboundHandle
// Make sure we actually have to verify the player
final String offlineUuidString = "OfflinePlayer:" + username;
final UUID offlineUuid = UUID.nameUUIDFromBytes(offlineUuidString.getBytes(StandardCharsets.UTF_8));
if (Sonar.get().getVerifiedPlayerController().has(inboundHandler.getInetAddress().toString(), offlineUuid)) {
if (Sonar.get().getVerifiedPlayerController().has(hostAddress, offlineUuid)) {
initialLogin(ctx, inboundHandler.getInetAddress(), loginPacket);
return;
}
// Check if the IP address is currently being rate-limited
if (!FALLBACK.getRatelimiter().attempt(inboundHandler.getInetAddress())) {
if (!Sonar.get().getFallback().getRatelimiter().attempt(inboundHandler.getInetAddress())) {
customDisconnect(channel, protocolVersion, reconnectedTooFast);
return;
}
@@ -156,7 +156,7 @@ public abstract class FallbackInboundHandlerAdapter extends ChannelInboundHandle
rewriteProtocol(ctx, channelRemovalListener);
// Queue the connection for further processing
FALLBACK.getQueue().getPlayers().compute(inboundHandler.getInetAddress(), (inetAddress, runnable) -> {
Sonar.get().getFallback().getQueue().getPlayers().compute(inboundHandler.getInetAddress(), (inetAddress, runnable) -> {
// Check if the player is already queued since we don't want bots to flood the queue
if (runnable != null) {
customDisconnect(channel, protocolVersion, alreadyQueued);
@@ -217,14 +217,14 @@ public abstract class FallbackInboundHandlerAdapter extends ChannelInboundHandle
final @NotNull InetAddress inetAddress,
final @NotNull Runnable loginPacket) throws Exception {
// Increment the number of accounts with the same IP
FALLBACK.getOnline().compute(inetAddress, (k, v) -> v == null ? 1 : v + 1);
Sonar.get().getFallback().getOnline().compute(inetAddress, (k, v) -> v == null ? 1 : v + 1);
final int maxOnlinePerIp = Sonar.get().getConfig().getMaxOnlinePerIp();
// Skip the maximum online per IP check if it's disabled in the configuration
if (maxOnlinePerIp > 0) {
// Check if the number of online players using the same IP address as
// the connecting player is greater than the configured amount
final int onlinePerIp = FALLBACK.getOnline().getOrDefault(inetAddress, 0);
final int onlinePerIp = Sonar.get().getFallback().getOnline().getOrDefault(inetAddress, 0);
if (onlinePerIp >= maxOnlinePerIp) {
customDisconnect(ctx.channel(), protocolVersion, tooManyOnlinePerIP);
return;
@@ -235,7 +235,6 @@ public abstract class FallbackInboundHandlerAdapter extends ChannelInboundHandle
loginPacket.run();
}
/**
* Disconnect the player before verification (during login)
* by replacing the encoder before running the method.
@@ -247,26 +246,28 @@ public abstract class FallbackInboundHandlerAdapter extends ChannelInboundHandle
private void customDisconnect(final @NotNull Channel channel,
final @NotNull ProtocolVersion protocolVersion,
final @NotNull FallbackPacket packet) {
if (channel.eventLoop().inEventLoop()) {
_customDisconnect(channel, protocolVersion, packet);
} else {
channel.eventLoop().execute(() -> _customDisconnect(channel, protocolVersion, packet));
if (channel.isActive()) {
if (channel.eventLoop().inEventLoop()) {
_customDisconnect(channel, protocolVersion, packet);
} else {
channel.eventLoop().execute(() -> _customDisconnect(channel, protocolVersion, packet));
}
}
}
private void _customDisconnect(final @NotNull Channel channel,
final @NotNull ProtocolVersion protocolVersion,
final @NotNull FallbackPacket packet) {
// Remove the main pipeline to completely take over the channel
final String handler = Sonar.get().getPlatform().getHandlerFunction().apply(channel.pipeline());
// Remove the connection handler pipeline to completely take over the channel
final String handler = Sonar.get().getPlatform().getConnectionHandler();
if (channel.pipeline().context(handler) != null) {
channel.pipeline().remove(handler);
}
final String encoder = Sonar.get().getPlatform().getEncoderFunction().apply(channel.pipeline());
final String encoder = Sonar.get().getPlatform().getEncoder().apply(channel.pipeline());
final ChannelHandler currentEncoder = channel.pipeline().get(encoder);
// Close the channel if no decoder exists
// Close the channel if no encoder exists
if (currentEncoder != null) {
// We don't need to update the encoder if it's already present
// We don't need to update the encoder if it has already been replaced by Sonar
if (!(currentEncoder instanceof FallbackPacketEncoder)) {
final FallbackPacketEncoder newEncoder = new FallbackPacketEncoder(protocolVersion);
channel.pipeline().replace(encoder, FALLBACK_PACKET_ENCODER, newEncoder);
@@ -20,7 +20,6 @@ package xyz.jonesdev.sonar.common.fallback;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelPipeline;
import io.netty.handler.codec.CorruptedFrameException;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@@ -42,6 +41,7 @@ import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.DisconnectPacket;
import xyz.jonesdev.sonar.common.fallback.session.FallbackLoginSessionHandler;
import xyz.jonesdev.sonar.common.statistics.GlobalSonarStatistics;
import xyz.jonesdev.sonar.common.util.exception.QuietDecoderException;
import java.net.InetAddress;
import java.util.UUID;
@@ -89,20 +89,16 @@ public final class FallbackUserWrapper implements FallbackUser {
@Override
public void hijack(final @NotNull String username, final @NotNull UUID offlineUuid) {
// The player has joined the verification
GlobalSonarStatistics.totalAttemptedVerifications++;
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 (Sonar.get().getAttackTracker().getCurrentAttack() == null
|| Sonar.get().getConfig().getVerification().isLogDuringAttack()) {
Sonar.get().getFallback().getLogger().info(
Sonar.get().getConfig().getMessagesConfig().getString("verification.logs.connection")
.replace("<username>", username)
.replace("<ip>", Sonar.get().getConfig().formatAddress(inetAddress))
.replace("<protocol>", String.valueOf(protocolVersion.getProtocol())));
}
if (Sonar.get().getConfig().getVerification().isLogConnections()
&& (Sonar.get().getAttackTracker().getCurrentAttack() == null
|| Sonar.get().getConfig().getVerification().isLogDuringAttack())) {
Sonar.get().getFallback().getLogger().info(
Sonar.get().getConfig().getMessagesConfig().getString("verification.logs.connection")
.replace("<username>", username)
.replace("<ip>", Sonar.get().getConfig().formatAddress(inetAddress))
.replace("<protocol>", String.valueOf(protocolVersion.getProtocol())));
}
// Call the VerifyJoinEvent for external API usage
@@ -143,10 +139,10 @@ public final class FallbackUserWrapper implements FallbackUser {
@Override
public void fail(final @NotNull String reason) {
GlobalSonarStatistics.totalFailedVerifications++;
disconnect(Sonar.get().getConfig().getVerification().getVerificationFailed());
// Only log the failed message if the server isn't currently under attack.
// However, we let the user override this through the configuration.
final boolean shouldLog = Sonar.get().getAttackTracker().getCurrentAttack() == null
|| Sonar.get().getConfig().getVerification().isLogDuringAttack();
@@ -158,49 +154,40 @@ public final class FallbackUserWrapper implements FallbackUser {
.replace("<reason>", reason));
}
// Increment number of total failed verifications
GlobalSonarStatistics.totalFailedVerifications++;
// Call the VerifyFailedEvent for external API usage
Sonar.get().getEventManager().publish(new UserVerifyFailedEvent(this, reason));
// Use a label, so we can easily add more code beneath this method in the future
blacklist: {
// Check if the player has too many failed attempts
final int blacklistThreshold = Sonar.get().getConfig().getVerification().getBlacklistThreshold();
final int limit = Sonar.get().getConfig().getVerification().getBlacklistThreshold();
// The user is allowed to disable the blacklist entirely by setting the threshold to 0
if (blacklistThreshold <= 0) break blacklist;
if (limit <= 0) break blacklist;
// Use 1 as the default amount of fails since we haven't cached anything yet
final int fails = Sonar.get().getFallback().getRatelimiter().getFailCountCache().get(inetAddress, ignored -> 1);
// Now we simply need to check if the threshold is reached
if (fails < blacklistThreshold) {
// Make sure we increment the number of fails
Sonar.get().getFallback().getRatelimiter().incrementFails(inetAddress, fails);
break blacklist;
}
final String hostAddress = inetAddress.getHostAddress();
final int score = Sonar.get().getFallback().getBlacklist().get(hostAddress, __ -> 0);
final int newScore = score + 1;
Sonar.get().getFallback().getBlacklist().put(hostAddress, newScore);
// The player hasn't been blacklisted yet, so skip this iteration
if (newScore < limit) break blacklist;
GlobalSonarStatistics.totalBlacklistedPlayers++;
// Call the BotBlacklistedEvent for external API usage
Sonar.get().getEventManager().publish(new UserBlacklistedEvent(this));
// Increment number of total blacklisted players
GlobalSonarStatistics.totalBlacklistedPlayers++;
Sonar.get().getFallback().getBlacklist().put(getInetAddress(), (byte) 0);
if (shouldLog) {
Sonar.get().getFallback().getLogger().info(
Sonar.get().getConfig().getMessagesConfig().getString("verification.logs.blacklisted")
.replace("<ip>", Sonar.get().getConfig().formatAddress(getInetAddress()))
.replace("<protocol>", String.valueOf(getProtocolVersion().getProtocol())));
}
// Invalidate the cached entry to ensure memory safety
Sonar.get().getFallback().getRatelimiter().getFailCountCache().invalidate(inetAddress);
}
// Throw an exception to avoid further code execution
throw new CorruptedFrameException();
throw QuietDecoderException.INSTANCE;
}
/**
@@ -211,18 +198,16 @@ public final class FallbackUserWrapper implements FallbackUser {
public static void closeWith(final @NotNull Channel channel,
final @NotNull ProtocolVersion protocolVersion,
final @NotNull Object msg) {
if (channel.isActive()) {
if (protocolVersion.compareTo(MINECRAFT_1_8) < 0
&& protocolVersion.compareTo(MINECRAFT_1_7_2) >= 0) {
channel.eventLoop().execute(() -> {
channel.config().setAutoRead(false);
channel.eventLoop().schedule(() -> {
channel.writeAndFlush(msg).addListener(ChannelFutureListener.CLOSE);
}, 250L, TimeUnit.MILLISECONDS);
});
} else {
channel.writeAndFlush(msg).addListener(ChannelFutureListener.CLOSE);
}
if (protocolVersion.compareTo(MINECRAFT_1_8) < 0
&& protocolVersion.compareTo(MINECRAFT_1_7_2) >= 0) {
channel.eventLoop().execute(() -> {
channel.config().setAutoRead(false);
channel.eventLoop().schedule(() -> {
channel.writeAndFlush(msg).addListener(ChannelFutureListener.CLOSE);
}, 250L, TimeUnit.MILLISECONDS);
});
} else {
channel.writeAndFlush(msg).addListener(ChannelFutureListener.CLOSE);
}
}
}
@@ -18,14 +18,13 @@
package xyz.jonesdev.sonar.common.fallback.netty;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.ReflectiveOperationException;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.SonarPlatform;
import xyz.jonesdev.sonar.api.exception.ReflectiveOperationException;
import xyz.jonesdev.sonar.common.fallback.FallbackInboundHandler;
import xyz.jonesdev.sonar.common.util.FakeChannelUtil;
@@ -63,11 +62,11 @@ public final class FallbackInjectedChannelInitializer extends ChannelInitializer
// Inject Sonar's channel handler into the pipeline;
// Also make sure the player is not a fake player to avoid compatibility issues
if (channel.isActive() && !FakeChannelUtil.isFakePlayer(channel)) {
final ChannelHandler inboundHandler = new FallbackInboundHandler(sonarPipelineInjector);
if (channel.isActive() && !FakeChannelUtil.isFakeChannel(channel)) {
final FallbackInboundHandler inboundHandler = new FallbackInboundHandler(sonarPipelineInjector);
// We need to be careful on Bukkit, as the encoder can be different
if (Sonar.get().getPlatform() == SonarPlatform.BUKKIT) {
final String encoder = Sonar.get().getPlatform().getEncoderFunction().apply(channel.pipeline());
final String encoder = Sonar.get().getPlatform().getEncoder().apply(channel.pipeline());
channel.pipeline().addBefore(encoder, FALLBACK_INBOUND_HANDLER, inboundHandler);
} else {
channel.pipeline().addFirst(FALLBACK_INBOUND_HANDLER, inboundHandler);
@@ -20,7 +20,6 @@ package xyz.jonesdev.sonar.common.fallback.netty;
import io.netty.util.ByteProcessor;
import lombok.Getter;
// Taken from
// https://github.com/PaperMC/Velocity/blob/dev/3.0.0/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/VarintByteDecoder.java
@Getter
final class FallbackVarInt21ByteDecoder implements ByteProcessor {
@@ -20,12 +20,11 @@ package xyz.jonesdev.sonar.common.fallback.netty;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
import io.netty.handler.codec.CorruptedFrameException;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.common.util.exception.QuietDecoderException;
import java.util.List;
// Taken from
// https://github.com/PaperMC/Velocity/blob/dev/3.0.0/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftVarintFrameDecoder.java
public final class FallbackVarInt21FrameDecoder extends ByteToMessageDecoder {
@@ -58,7 +57,7 @@ public final class FallbackVarInt21FrameDecoder extends ByteToMessageDecoder {
if (readVarInt < 0) {
in.clear();
throw new CorruptedFrameException("Invalid VarInt length " + readVarInt);
throw QuietDecoderException.INSTANCE;
} else if (readVarInt == 0) {
// Skip over the empty packet(s) and ignore it
in.readerIndex(end + 1);
@@ -71,7 +70,7 @@ public final class FallbackVarInt21FrameDecoder extends ByteToMessageDecoder {
}
} else if (reader.getResult() == FallbackVarInt21ByteDecoder.DecodeResult.TOO_BIG) {
in.clear();
throw new CorruptedFrameException("Too big VarInt " + reader.getReadVarInt());
throw QuietDecoderException.INSTANCE;
}
}
}
@@ -26,7 +26,6 @@ import org.jetbrains.annotations.NotNull;
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.varIntBytes;
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.writeVarInt;
// Taken from
// https://github.com/PaperMC/Velocity/blob/dev/3.0.0/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftVarintLengthEncoder.java
@ChannelHandler.Sharable
public final class FallbackVarInt21FrameEncoder extends MessageToByteEncoder<ByteBuf> {
@@ -20,15 +20,17 @@ package xyz.jonesdev.sonar.common.fallback.protocol;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.handler.codec.CorruptedFrameException;
import io.netty.handler.codec.DecoderException;
import lombok.Setter;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.util.exception.QuietDecoderException;
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.util.ProtocolUtil.DEBUG;
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.readVarInt;
public final class FallbackPacketDecoder extends ChannelInboundHandlerAdapter {
@@ -76,12 +78,12 @@ public final class FallbackPacketDecoder extends ChannelInboundHandlerAdapter {
// Try to decode the packet for the given protocol version
packet.decode(byteBuf, protocolVersion);
} catch (Throwable throwable) {
throw new CorruptedFrameException("Failed to decode packet");
throw DEBUG ? new DecoderException(throwable) : QuietDecoderException.INSTANCE;
}
// Check if the packet still has bytes left after we decoded it
if (byteBuf.isReadable()) {
throw new CorruptedFrameException("Could not read packet to end");
throw DEBUG ? new DecoderException("Could not read packet to end") : QuietDecoderException.INSTANCE;
}
// Let our verification handler process the packet
@@ -102,12 +104,12 @@ public final class FallbackPacketDecoder extends ChannelInboundHandlerAdapter {
final @NotNull FallbackPacket packet) throws Exception {
final int expectedMaxLen = packet.expectedMaxLength(byteBuf, protocolVersion);
if (expectedMaxLen != -1 && byteBuf.readableBytes() > expectedMaxLen) {
throw new CorruptedFrameException("Packet too large");
throw DEBUG ? new DecoderException("Packet too large") : QuietDecoderException.INSTANCE;
}
final int expectedMinLen = packet.expectedMinLength(byteBuf, protocolVersion);
if (expectedMinLen != -1 && byteBuf.readableBytes() < expectedMinLen) {
throw new CorruptedFrameException("Packet too small");
throw DEBUG ? new DecoderException("Packet too small") : QuietDecoderException.INSTANCE;
}
}
}
@@ -37,7 +37,6 @@ import java.util.function.Supplier;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
// Mostly taken from
// https://github.com/PaperMC/Velocity/blob/dev/3.0.0/proxy/src/main/java/com/velocitypowered/proxy/protocol/StateRegistry.java
public enum FallbackPacketRegistry {
HANDSHAKE {
@@ -29,7 +29,6 @@ import java.util.Map;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.ID_TO_PROTOCOL_CONSTANT;
// Mostly taken from
// https://github.com/Nan1t/NanoLimbo/blob/main/src/main/java/ua/nanit/limbo/protocol/PacketSnapshot.java
@Getter
public final class FallbackPacketSnapshot implements FallbackPacket {
@@ -22,6 +22,7 @@ import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.config.SonarConfiguration;
import xyz.jonesdev.sonar.captcha.StandardCaptchaGenerator;
import xyz.jonesdev.sonar.common.fallback.protocol.block.BlockType;
import xyz.jonesdev.sonar.common.fallback.protocol.block.BlockUpdate;
import xyz.jonesdev.sonar.common.fallback.protocol.captcha.CaptchaPreparer;
@@ -210,6 +211,15 @@ public class FallbackPreparer {
xpCountdown[i] = new FallbackPacketSnapshot(new SetExperiencePacket(bar, i, 0));
}
// Update the CAPTCHA generator if necessary
if (Sonar.get().getFallback().getCaptchaGenerator() == null
|| Sonar.get().getFallback().getCaptchaGenerator() instanceof StandardCaptchaGenerator) {
Sonar.get().getFallback().setCaptchaGenerator(new StandardCaptchaGenerator(128, 128,
Sonar.get().getConfig().getVerification().getMap().getBackgroundImage()));
} else {
Sonar.get().getLogger().info("Custom CAPTCHA generator detected, skipping reinitialization.");
}
// Prepare CAPTCHA answers
CaptchaPreparer.prepare();
} else {
@@ -217,6 +227,7 @@ public class FallbackPreparer {
enterCodeMessage = null;
incorrectCaptcha = null;
xpCountdown = null;
Sonar.get().getFallback().setCaptchaGenerator(null);
}
}
}
@@ -29,7 +29,6 @@ import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
import static xyz.jonesdev.sonar.common.fallback.protocol.dimension.DimensionRegistry.*;
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.*;
// Mostly taken from
// https://github.com/Nan1t/NanoLimbo/blob/main/src/main/java/ua/nanit/limbo/protocol/packets/play/PacketJoinGame.java
@Data
@NoArgsConstructor
@@ -18,12 +18,12 @@
package xyz.jonesdev.sonar.common.fallback.protocol.packets.play;
import io.netty.buffer.ByteBuf;
import io.netty.handler.codec.CorruptedFrameException;
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.util.exception.QuietDecoderException;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_8;
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.readExtendedForgeShort;
@@ -50,12 +50,12 @@ public final class PluginMessagePacket implements FallbackPacket {
if (protocolVersion.compareTo(MINECRAFT_1_8) >= 0) {
length = byteBuf.readableBytes();
if (length > Short.MAX_VALUE) {
throw new CorruptedFrameException("Got too much data: " + length);
throw QuietDecoderException.INSTANCE;
}
} else {
length = readExtendedForgeShort(byteBuf);
if (length > FORGE_MAX_ARRAY_LENGTH) {
throw new CorruptedFrameException("Got too much data: " + length);
throw QuietDecoderException.INSTANCE;
}
}
@@ -18,7 +18,6 @@
package xyz.jonesdev.sonar.common.fallback.protocol.packets.play;
import io.netty.buffer.ByteBuf;
import io.netty.handler.codec.CorruptedFrameException;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@@ -26,6 +25,7 @@ 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.util.ComponentHolder;
import xyz.jonesdev.sonar.common.util.exception.QuietDecoderException;
import java.util.UUID;
@@ -83,18 +83,18 @@ public final class SystemChatPacket implements FallbackPacket {
|| saltLong == 0L) && signatureBytes.length == 0) {
unsigned = true;
} else {
throw new CorruptedFrameException("Invalid signature");
throw QuietDecoderException.INSTANCE;
}
final boolean signedPreview = byteBuf.readBoolean();
if (signedPreview && unsigned) {
throw new CorruptedFrameException("Signature missing");
throw QuietDecoderException.INSTANCE;
}
if (protocolVersion.compareTo(MINECRAFT_1_19_1) >= 0) {
final int size = readVarInt(byteBuf);
if (size < 0 || size > 5) {
throw new CorruptedFrameException("Invalid previous messages");
throw QuietDecoderException.INSTANCE;
}
for (int i = 0; i < size; i++) {
@@ -0,0 +1,52 @@
/*
* Copyright (C) 2024 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.ratelimit;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.Ticker;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.ratelimit.Ratelimiter;
import java.net.InetAddress;
import java.util.concurrent.TimeUnit;
// Idea taken from Velocity
@Setter
@RequiredArgsConstructor
public final class CaffeineCacheRatelimiter implements Ratelimiter<InetAddress> {
private Cache<InetAddress, Long> expiringCache;
private long timeout;
public CaffeineCacheRatelimiter(final long timeout, final @NotNull TimeUnit unit) {
this.expiringCache = Caffeine.newBuilder()
.ticker(Ticker.systemTicker())
.expireAfterWrite(timeout, unit)
.build();
this.timeout = unit.toNanos(timeout);
}
@Override
public boolean attempt(final @NotNull InetAddress inetAddress) {
final long expectedNewTimestamp = System.nanoTime() + timeout;
final long last = expiringCache.get(inetAddress, result -> expectedNewTimestamp);
return expectedNewTimestamp == last;
}
}
@@ -0,0 +1,37 @@
/*
* Copyright (C) 2024 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.ratelimit;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.ratelimit.Ratelimiter;
import java.net.InetAddress;
// Idea taken from Velocity
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class NoopCacheRatelimiter implements Ratelimiter<InetAddress> {
public static final NoopCacheRatelimiter INSTANCE = new NoopCacheRatelimiter();
@Override
public boolean attempt(final @NotNull InetAddress inetAddress) {
// Accept all connections
return true;
}
}
@@ -17,7 +17,6 @@
package xyz.jonesdev.sonar.common.fallback.session;
import io.netty.handler.codec.CorruptedFrameException;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
@@ -26,6 +25,7 @@ import xyz.jonesdev.sonar.common.fallback.protocol.captcha.CaptchaPreparer;
import xyz.jonesdev.sonar.common.fallback.protocol.captcha.ItemType;
import xyz.jonesdev.sonar.common.fallback.protocol.captcha.MapCaptchaInfo;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.*;
import xyz.jonesdev.sonar.common.util.exception.QuietDecoderException;
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer.*;
@@ -51,7 +51,7 @@ public final class FallbackCAPTCHASessionHandler extends FallbackSessionHandler
// Disconnect the player if there is no CAPTCHA available at the moment
if (!CaptchaPreparer.isCaptchaAvailable()) {
user.disconnect(Sonar.get().getConfig().getVerification().getCurrentlyPreparing());
throw new CorruptedFrameException("No captcha available");
throw QuietDecoderException.INSTANCE;
}
this.tries = Sonar.get().getConfig().getVerification().getMap().getMaxTries();
@@ -20,9 +20,9 @@ package xyz.jonesdev.sonar.common.fallback.session;
import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.database.model.VerifiedPlayer;
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.common.fallback.protocol.FallbackPacketDecoder;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketEncoder;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketListener;
@@ -20,7 +20,7 @@ package xyz.jonesdev.sonar.common.service;
import lombok.experimental.UtilityClass;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.common.statistics.CachedBandwidthStatistics;
import xyz.jonesdev.sonar.common.statistics.BandwidthStatistics;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
@@ -43,21 +43,19 @@ public final class SonarServiceManager {
public void start() {
VERBOSE.scheduleAtFixedRate(() -> {
// Make sure to clean up the cached statistics
// since we don't want to display wrong values.
// Make sure to clean up the cached statistics since we don't want to display wrong values
Sonar.get().getStatistics().cleanUpCache();
Sonar.get().getFallback().getRatelimiter().getFailCountCache().cleanUp();
Sonar.get().getFallback().getBlacklist().cleanUp();
// Update the attack tracker
Sonar.get().getAttackTracker().checkIfUnderAttack();
// Update the action bar verbose
Sonar.get().getVerboseHandler().observe();
// Publish the action bar notifications
Sonar.get().getActionBarNotificationHandler().handleNotification();
}, 0L, 250L, TimeUnit.MILLISECONDS);
FALLBACK_QUEUE.scheduleAtFixedRate(() -> Sonar.get().getFallback().getQueue().poll(),
1L, 1L, TimeUnit.SECONDS);
STATISTICS.scheduleAtFixedRate(CachedBandwidthStatistics::reset,
STATISTICS.scheduleAtFixedRate(BandwidthStatistics::reset,
0L, 1L, TimeUnit.SECONDS);
}
@@ -19,13 +19,13 @@ package xyz.jonesdev.sonar.common.statistics;
import lombok.Getter;
import lombok.Setter;
import xyz.jonesdev.sonar.api.jvm.JVMProcessInformation;
import xyz.jonesdev.sonar.api.profiler.SimpleProcessProfiler;
/**
* This counts all incoming and outgoing traffic.
*/
@Getter
public enum CachedBandwidthStatistics {
public enum BandwidthStatistics {
INCOMING,
OUTGOING;
@@ -54,8 +54,8 @@ public enum CachedBandwidthStatistics {
private void cacheAndReset() {
ttl += curr; // increment total
cachedSecond = JVMProcessInformation.formatMemory(curr);
cachedTtl = JVMProcessInformation.formatMemory(ttl);
cachedSecond = SimpleProcessProfiler.formatMemory(curr);
cachedTtl = SimpleProcessProfiler.formatMemory(ttl);
curr = 0L; // reset current
}
}
@@ -80,42 +80,42 @@ public final class GlobalSonarStatistics implements SonarStatistics {
@Override
public long getCurrentIncomingBandwidth() {
return CachedBandwidthStatistics.INCOMING.getCurr();
return BandwidthStatistics.INCOMING.getCurr();
}
@Override
public long getCurrentOutgoingBandwidth() {
return CachedBandwidthStatistics.OUTGOING.getCurr();
return BandwidthStatistics.OUTGOING.getCurr();
}
@Override
public long getTotalIncomingBandwidth() {
return CachedBandwidthStatistics.INCOMING.getTtl();
return BandwidthStatistics.INCOMING.getTtl();
}
@Override
public long getTotalOutgoingBandwidth() {
return CachedBandwidthStatistics.OUTGOING.getTtl();
return BandwidthStatistics.OUTGOING.getTtl();
}
@Override
public String getCurrentIncomingBandwidthFormatted() {
return CachedBandwidthStatistics.INCOMING.getCachedSecond();
return BandwidthStatistics.INCOMING.getCachedSecond();
}
@Override
public String getCurrentOutgoingBandwidthFormatted() {
return CachedBandwidthStatistics.OUTGOING.getCachedSecond();
return BandwidthStatistics.OUTGOING.getCachedSecond();
}
@Override
public String getTotalIncomingBandwidthFormatted() {
return CachedBandwidthStatistics.INCOMING.getCachedTtl();
return BandwidthStatistics.INCOMING.getCachedTtl();
}
@Override
public String getTotalOutgoingBandwidthFormatted() {
return CachedBandwidthStatistics.OUTGOING.getCachedTtl();
return BandwidthStatistics.OUTGOING.getCachedTtl();
}
@Override
@@ -15,10 +15,8 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.subcommands;
package xyz.jonesdev.sonar.common.subcommand;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
import org.jetbrains.annotations.NotNull;
@@ -27,9 +25,6 @@ import xyz.jonesdev.sonar.api.command.CommandInvocation;
import xyz.jonesdev.sonar.api.command.subcommand.Subcommand;
import xyz.jonesdev.sonar.api.command.subcommand.SubcommandInfo;
import java.net.InetAddress;
import java.net.UnknownHostException;
@SubcommandInfo(
name = "blacklist",
arguments = {"add", "remove", "clear", "size"}
@@ -49,38 +44,29 @@ public final class BlacklistCommand extends Subcommand {
// Make sure the given IP address is valid
if (rawAddress == null) return;
try {
// Try to resolve the IP address, so we can actually do something with it
final InetAddress inetAddress = InetAddress.getAllByName(rawAddress)[0];
// Make sure the IP is not blacklisted already
if (Sonar.get().getFallback().getBlacklist().asMap().containsKey(inetAddress)) {
invocation.getSource().sendMessage(MiniMessage.miniMessage().deserialize(
Sonar.get().getConfig().getMessagesConfig().getString("commands.blacklist.ip-duplicate"),
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix()),
Placeholder.unparsed("ip", rawAddress)));
return;
}
// Display a warning if the IP is verified but being added to the blacklist
if (Sonar.get().getVerifiedPlayerController().has(inetAddress.toString())) {
invocation.getSource().sendMessage(MiniMessage.miniMessage().deserialize(
Sonar.get().getConfig().getMessagesConfig().getString("commands.blacklist.add-warning"),
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix()),
Placeholder.unparsed("ip", rawAddress)));
}
// Blacklist the given IP address
Sonar.get().getFallback().getBlacklist().put(inetAddress, (byte) 0);
// Make sure the IP is not blacklisted already
if (Sonar.get().getFallback().getBlacklist().asMap().containsKey(rawAddress)) {
invocation.getSource().sendMessage(MiniMessage.miniMessage().deserialize(
Sonar.get().getConfig().getMessagesConfig().getString("commands.blacklist.add"),
Sonar.get().getConfig().getMessagesConfig().getString("commands.blacklist.ip-duplicate"),
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix()),
Placeholder.unparsed("ip", rawAddress)));
} catch (UnknownHostException exception) {
invocation.getSource().sendMessage(Component.text(
"Unexpected error, check console.", NamedTextColor.RED));
exception.printStackTrace(System.err);
return;
}
// Display a warning if the IP is verified but being added to the blacklist
if (Sonar.get().getVerifiedPlayerController().has(rawAddress)) {
invocation.getSource().sendMessage(MiniMessage.miniMessage().deserialize(
Sonar.get().getConfig().getMessagesConfig().getString("commands.blacklist.add-warning"),
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix()),
Placeholder.unparsed("ip", rawAddress)));
}
// Blacklist the given IP address
Sonar.get().getFallback().getBlacklist().put(rawAddress, 1337 /* arbitrarily high number */);
invocation.getSource().sendMessage(MiniMessage.miniMessage().deserialize(
Sonar.get().getConfig().getMessagesConfig().getString("commands.blacklist.add"),
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix()),
Placeholder.unparsed("ip", rawAddress)));
break;
}
@@ -94,29 +80,20 @@ public final class BlacklistCommand extends Subcommand {
// Make sure the given IP address is valid
if (rawAddress == null) return;
try {
// Try to resolve the IP address, so we can actually do something with it
final InetAddress inetAddress = InetAddress.getAllByName(rawAddress)[0];
// Make sure the IP is blacklisted
if (!Sonar.get().getFallback().getBlacklist().asMap().containsKey(inetAddress)) {
invocation.getSource().sendMessage(MiniMessage.miniMessage().deserialize(
Sonar.get().getConfig().getMessagesConfig().getString("commands.blacklist.ip-not-found"),
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix())));
return;
}
// Invalidate the cache entry of the blacklisted IP address
Sonar.get().getFallback().getBlacklist().invalidate(inetAddress);
// Make sure the IP is blacklisted
if (!Sonar.get().getFallback().getBlacklist().asMap().containsKey(rawAddress)) {
invocation.getSource().sendMessage(MiniMessage.miniMessage().deserialize(
Sonar.get().getConfig().getMessagesConfig().getString("commands.blacklist.remove"),
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix()),
Placeholder.unparsed("ip", rawAddress)));
} catch (UnknownHostException exception) {
invocation.getSource().sendMessage(Component.text(
"Unexpected error, check console.", NamedTextColor.RED));
exception.printStackTrace(System.err);
Sonar.get().getConfig().getMessagesConfig().getString("commands.blacklist.ip-not-found"),
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix())));
return;
}
// Invalidate the cache entry of the blacklisted IP address
Sonar.get().getFallback().getBlacklist().invalidate(rawAddress);
invocation.getSource().sendMessage(MiniMessage.miniMessage().deserialize(
Sonar.get().getConfig().getMessagesConfig().getString("commands.blacklist.remove"),
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix()),
Placeholder.unparsed("ip", rawAddress)));
break;
}
@@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.subcommands;
package xyz.jonesdev.sonar.common.subcommand;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
@@ -32,7 +32,7 @@ import java.lang.management.RuntimeMXBean;
import java.util.List;
import java.util.WeakHashMap;
import static xyz.jonesdev.sonar.api.jvm.JVMProcessInformation.*;
import static xyz.jonesdev.sonar.api.profiler.SimpleProcessProfiler.*;
// This command is only used to display helpful information to help
// the Sonar contributors to fix issues more quickly
@@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.subcommands;
package xyz.jonesdev.sonar.common.subcommand;
import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
@@ -33,15 +33,15 @@ public final class NotifyCommand extends Subcommand {
@Override
protected void execute(final @NotNull CommandInvocation invocation) {
if (Sonar.get().getNotificationHandler().isSubscribed(invocation.getSource().getUuid())) {
Sonar.get().getNotificationHandler().unsubscribe(invocation.getSource().getUuid());
if (Sonar.get().getChatNotificationHandler().isSubscribed(invocation.getSource().getUuid())) {
Sonar.get().getChatNotificationHandler().unsubscribe(invocation.getSource().getUuid());
invocation.getSource().sendMessage(MiniMessage.miniMessage().deserialize(
Sonar.get().getConfig().getMessagesConfig().getString("commands.notify.unsubscribe"),
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix())));
return;
}
Sonar.get().getNotificationHandler().subscribe(invocation.getSource().getUuid());
Sonar.get().getChatNotificationHandler().subscribe(invocation.getSource().getUuid());
invocation.getSource().sendMessage(MiniMessage.miniMessage().deserialize(
Sonar.get().getConfig().getMessagesConfig().getString("commands.notify.subscribe"),
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix())));
@@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.subcommands;
package xyz.jonesdev.sonar.common.subcommand;
import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
@@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.subcommands;
package xyz.jonesdev.sonar.common.subcommand;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage;
@@ -26,10 +26,10 @@ import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.command.CommandInvocation;
import xyz.jonesdev.sonar.api.command.subcommand.Subcommand;
import xyz.jonesdev.sonar.api.command.subcommand.SubcommandInfo;
import xyz.jonesdev.sonar.common.statistics.CachedBandwidthStatistics;
import xyz.jonesdev.sonar.common.statistics.BandwidthStatistics;
import static xyz.jonesdev.sonar.api.Sonar.DECIMAL_FORMAT;
import static xyz.jonesdev.sonar.api.jvm.JVMProcessInformation.*;
import static xyz.jonesdev.sonar.api.profiler.SimpleProcessProfiler.*;
@SubcommandInfo(
name = "statistics",
@@ -109,10 +109,10 @@ public final class StatisticsCommand extends Subcommand {
case "network": {
placeholders = new TagResolver.Single[]{
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix()),
Placeholder.unparsed("incoming-traffic", CachedBandwidthStatistics.INCOMING.getCachedSecond()),
Placeholder.unparsed("outgoing-traffic", CachedBandwidthStatistics.OUTGOING.getCachedSecond()),
Placeholder.unparsed("incoming-traffic-ttl", CachedBandwidthStatistics.INCOMING.getCachedTtl()),
Placeholder.unparsed("outgoing-traffic-ttl", CachedBandwidthStatistics.OUTGOING.getCachedTtl())
Placeholder.unparsed("incoming-traffic", BandwidthStatistics.INCOMING.getCachedSecond()),
Placeholder.unparsed("outgoing-traffic", BandwidthStatistics.OUTGOING.getCachedSecond()),
Placeholder.unparsed("incoming-traffic-ttl", BandwidthStatistics.INCOMING.getCachedTtl()),
Placeholder.unparsed("outgoing-traffic-ttl", BandwidthStatistics.OUTGOING.getCachedTtl())
};
break;
}
@@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.subcommands;
package xyz.jonesdev.sonar.common.subcommand;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage;
@@ -34,8 +34,8 @@ public final class VerboseCommand extends Subcommand {
@Override
protected void execute(final @NotNull CommandInvocation invocation) {
if (Sonar.get().getVerboseHandler().isSubscribed(invocation.getSource().getUuid())) {
Sonar.get().getVerboseHandler().unsubscribe(invocation.getSource().getUuid());
if (Sonar.get().getActionBarNotificationHandler().isSubscribed(invocation.getSource().getUuid())) {
Sonar.get().getActionBarNotificationHandler().unsubscribe(invocation.getSource().getUuid());
// Reset ActionBar component when unsubscribing
invocation.getSource().getAudience().sendActionBar(Component.empty());
invocation.getSource().sendMessage(MiniMessage.miniMessage().deserialize(
@@ -44,7 +44,7 @@ public final class VerboseCommand extends Subcommand {
return;
}
Sonar.get().getVerboseHandler().subscribe(invocation.getSource().getUuid());
Sonar.get().getActionBarNotificationHandler().subscribe(invocation.getSource().getUuid());
invocation.getSource().sendMessage(MiniMessage.miniMessage().deserialize(
Sonar.get().getConfig().getMessagesConfig().getString("commands.verbose.subscribe"),
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix())));
@@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.subcommands;
package xyz.jonesdev.sonar.common.subcommand;
import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
@@ -24,7 +24,7 @@ import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.command.CommandInvocation;
import xyz.jonesdev.sonar.api.command.subcommand.Subcommand;
import xyz.jonesdev.sonar.api.command.subcommand.SubcommandInfo;
import xyz.jonesdev.sonar.api.model.VerifiedPlayer;
import xyz.jonesdev.sonar.api.database.model.VerifiedPlayer;
import java.nio.charset.StandardCharsets;
import java.util.UUID;
@@ -44,6 +44,7 @@ public final class VerifiedCommand extends Subcommand {
return;
}
// TODO: Remove duplicate code
final String rawAddress = validateIP(invocation.getSource(), invocation.getRawArguments()[2]);
// Make sure the given IP address is valid
if (rawAddress == null) return;
@@ -76,21 +77,20 @@ public final class VerifiedCommand extends Subcommand {
return;
}
// TODO: Remove duplicate code
final String rawAddress = validateIP(invocation.getSource(), invocation.getRawArguments()[2]);
// Make sure the given IP address is valid
if (rawAddress == null) return;
// An InetAddress starts with /, we need to account for this
final String inetAddress = "/" + rawAddress;
// Make sure the player is verified already
if (!Sonar.get().getVerifiedPlayerController().has(inetAddress)) {
if (!Sonar.get().getVerifiedPlayerController().has(rawAddress)) {
invocation.getSource().sendMessage(MiniMessage.miniMessage().deserialize(
Sonar.get().getConfig().getMessagesConfig().getString("commands.verified.ip-not-found"),
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix())));
return;
}
Sonar.get().getVerifiedPlayerController().remove(inetAddress);
Sonar.get().getVerifiedPlayerController().remove(rawAddress);
invocation.getSource().sendMessage(MiniMessage.miniMessage().deserialize(
Sonar.get().getConfig().getMessagesConfig().getString("commands.verified.remove"),
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix()),
@@ -107,8 +107,6 @@ public final class VerifiedCommand extends Subcommand {
final String rawAddress = validateIP(invocation.getSource(), invocation.getRawArguments()[2]);
// Make sure the given IP address is valid
if (rawAddress == null) return;
// An InetAddress starts with /, we need to account for this
final String inetAddress = "/" + rawAddress;
// Try to parse the UUID (from the username, if needed)
final String rawUUID = invocation.getRawArguments()[3];
@@ -116,7 +114,7 @@ public final class VerifiedCommand extends Subcommand {
: UUID.nameUUIDFromBytes(("OfflinePlayer:" + rawUUID).getBytes(StandardCharsets.UTF_8));
// Make sure the player is verified already
if (Sonar.get().getVerifiedPlayerController().has(inetAddress, uuid)) {
if (Sonar.get().getVerifiedPlayerController().has(rawAddress, uuid)) {
invocation.getSource().sendMessage(MiniMessage.miniMessage().deserialize(
Sonar.get().getConfig().getMessagesConfig().getString("commands.verified.already"),
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix())));
@@ -125,7 +123,7 @@ public final class VerifiedCommand extends Subcommand {
// Add verified player to the database
final long timestamp = System.currentTimeMillis();
Sonar.get().getVerifiedPlayerController().add(new VerifiedPlayer(inetAddress, uuid, timestamp));
Sonar.get().getVerifiedPlayerController().add(new VerifiedPlayer(rawAddress, uuid, timestamp));
invocation.getSource().sendMessage(MiniMessage.miniMessage().deserialize(
Sonar.get().getConfig().getMessagesConfig().getString("commands.verified.add"),
@@ -43,7 +43,6 @@ public final class ComponentHolder {
this.serializedComponent = GsonComponentSerializer.gson().serialize(component);
}
// Mostly taken from
// https://github.com/PaperMC/Velocity/blob/dev/3.0.0/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/ComponentHolder.java
private BinaryTag serialize(final JsonElement json) {
if (json instanceof JsonPrimitive) {
@@ -26,10 +26,12 @@ import org.jetbrains.annotations.NotNull;
public class FakeChannelUtil {
/**
* @param channel Channel of the player
* @return Whether the channel is spoofed/faked
* Checks if the provided channel is a fake channel, typically used by player spoofing plugins.
*
* @param channel The channel to be checked.
* @return true if the channel is identified as a fake channel
*/
public boolean isFakePlayer(final @NotNull Channel channel) {
public boolean isFakeChannel(final @NotNull Channel channel) {
final String simpleClassName = channel.getClass().getSimpleName();
// Player spoof plugins use fake channels (e.g., Spoof Engine uses "FakeChannel")
return "FakeChannel".equals(simpleClassName) || "SpoofedChannel".equals(simpleClassName);
@@ -46,7 +46,6 @@ import java.util.UUID;
* SOFTWARE.
*/
// Taken from
// https://github.com/PaperMC/Velocity/blob/dev/3.0.0/api/src/main/java/com/velocitypowered/api/util/FastUuidSansHyphens.java
/**
* This is a modified FastUUID implementation. The primary difference is that it does not dash its
@@ -29,13 +29,12 @@ public class GeyserUtil {
// https://github.com/GeyserMC/Floodgate/blob/master/core/src/main/java/org/geysermc/floodgate/module/CommonModule.java#L206
private final AttributeKey<Object> PLAYER_ATTRIBUTE = AttributeKey.valueOf("floodgate-player");
/**
* @param channel Channel of the player
* @return Whether the player is on GeyserMC or not
*/
public boolean isGeyserConnection(final @NotNull Channel channel,
final @NotNull InetSocketAddress originalAddress) {
return originalAddress.getPort() == 0 // check for floodgate on the server
|| channel.attr(PLAYER_ATTRIBUTE).get() != null; // check for standalone Geyser proxy connection
public boolean isGeyserConnection(final @NotNull Channel channel, final @NotNull InetSocketAddress originalAddress) {
/*
* This method performs two checks:
* 1. It checks if the player is joining through Floodgate, which is installed on the server.
* 2. It checks if the player is joining through a Floodgate proxy hosted externally.
*/
return originalAddress.getPort() == 0 || channel.attr(PLAYER_ATTRIBUTE).get() != null;
}
}
@@ -41,12 +41,13 @@ package xyz.jonesdev.sonar.common.util;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufOutputStream;
import io.netty.buffer.ByteBufUtil;
import io.netty.handler.codec.CorruptedFrameException;
import io.netty.handler.codec.DecoderException;
import io.netty.handler.codec.EncoderException;
import io.netty.util.Version;
import lombok.experimental.UtilityClass;
import net.kyori.adventure.nbt.*;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.common.util.exception.QuietDecoderException;
import java.io.DataOutput;
import java.io.IOException;
@@ -56,10 +57,10 @@ import java.util.BitSet;
import java.util.EnumSet;
import java.util.UUID;
// Mostly 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 {
public static final boolean DEBUG = Boolean.getBoolean("sonar.debug-traces");
public static final String BRAND_CHANNEL_LEGACY = "MC|Brand";
public static final String BRAND_CHANNEL = "minecraft:brand";
@@ -85,7 +86,7 @@ public class ProtocolUtil {
public static int readVarInt(final ByteBuf byteBuf) {
int read = readVarIntSafely(byteBuf);
if (read == Integer.MIN_VALUE) {
throw new CorruptedFrameException("Corrupt VarInt");
throw DEBUG ? new DecoderException("Bad VarInt") : QuietDecoderException.INSTANCE;
}
return read;
}
@@ -224,33 +225,33 @@ public class ProtocolUtil {
public static byte @NotNull [] readByteArray(final ByteBuf byteBuf, final int cap) {
int length = readVarInt(byteBuf);
checkFrame(length >= 0, "Got a negative-length array");
checkFrame(length <= cap, "Bad array size");
checkFrame(byteBuf.isReadable(length), "Trying to read an array that is too long");
checkState(length >= 0, "Got a negative-length array");
checkState(length <= cap, "Bad array size");
checkState(byteBuf.isReadable(length), "Trying to read an array that is too long");
byte[] array = new byte[length];
byteBuf.readBytes(array);
return array;
}
public static @NotNull String readString(final ByteBuf byteBuf) throws CorruptedFrameException {
public static @NotNull String readString(final ByteBuf byteBuf) throws DecoderException {
return readString(byteBuf, Short.MAX_VALUE);
}
public static @NotNull String readString(final ByteBuf byteBuf,
final int cap) throws CorruptedFrameException {
final int cap) throws DecoderException {
final int length = readVarInt(byteBuf);
return readString(byteBuf, cap, length);
}
public static @NotNull String readString(final @NotNull ByteBuf byteBuf,
final int cap,
final int length) throws CorruptedFrameException {
checkFrame(length >= 0, "Got a negative-length string");
checkFrame(length <= cap * 3, "Bad string size");
checkFrame(byteBuf.isReadable(length), "Tried to read a too-long string");
final int length) throws DecoderException {
checkState(length >= 0, "Got a negative-length string");
checkState(length <= cap * 3, "Bad string size");
checkState(byteBuf.isReadable(length), "Tried to read a too-long string");
final String str = byteBuf.toString(byteBuf.readerIndex(), length, StandardCharsets.UTF_8);
byteBuf.readerIndex(byteBuf.readerIndex() + length);
checkFrame(str.length() <= cap, "Got a too-long string");
checkState(str.length() <= cap, "Got a too-long string");
return str;
}
@@ -273,7 +274,7 @@ public class ProtocolUtil {
}
public static void writeArray(final ByteBuf byteBuf, final byte @NotNull [] bytes) {
checkFrame(bytes.length < Short.MAX_VALUE, "Too long array");
checkState(bytes.length < Short.MAX_VALUE, "Too long array");
writeVarInt(byteBuf, bytes.length);
byteBuf.writeBytes(bytes);
}
@@ -285,7 +286,6 @@ public class ProtocolUtil {
}
}
// Taken from
// https://github.com/Nan1t/NanoLimbo/blob/main/src/main/java/ua/nanit/limbo/protocol/ByteMessage.java#L276
public <E extends Enum<E>> void writeEnumSet(final ByteBuf byteBuf,
final EnumSet<E> enumset,
@@ -315,7 +315,6 @@ public class ProtocolUtil {
}
}
// Taken from
// https://github.com/Nan1t/NanoLimbo/blob/main/src/main/java/ua/nanit/limbo/protocol/ByteMessage.java#L219
public static void writeNamelessCompoundTag(final @NotNull ByteBuf byteBuf, final @NotNull BinaryTag binaryTag) {
try (final ByteBufOutputStream output = new ByteBufOutputStream(byteBuf)) {
@@ -364,9 +363,9 @@ public class ProtocolUtil {
return ((high & 0xFF) << 15) | low;
}
private void checkFrame(final boolean expression, final String message) {
private void checkState(final boolean expression, final String message) {
if (!expression) {
throw new CorruptedFrameException(message);
throw DEBUG ? new DecoderException(message) : QuietDecoderException.INSTANCE;
}
}
}
@@ -0,0 +1,29 @@
/*
* Copyright (C) 2024 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.util.exception;
import io.netty.handler.codec.DecoderException;
public final class QuietDecoderException extends DecoderException {
public static final QuietDecoderException INSTANCE = new QuietDecoderException();
@Override
public Throwable fillInStackTrace() {
return this;
}
}
@@ -181,7 +181,7 @@ verification:
# Překlady pro všechny ladící zprávy během ověření
logs:
# Zpráva zaznamenána do konzole, kdykoli se nový hráč připojí k serveru
connection: '<username><ip> (<protocol>) se připojil.'
connection: '<username>/<ip> (<protocol>) se připojil.'
# Zpráva zaznamenána do konzole, kdykoli hráč selže při ověření
failed: '<ip> (<protocol>) neprošel kontrolou bota z důvodu: <reason>'
# Zpráva zaznamenána do konzole, kdykoli je hráč zařazen na černou listinu
@@ -182,7 +182,7 @@ verification:
# Übersetzungen für alle Debug-Nachrichten während der Überprüfung
logs:
# Nachricht, die im Protokoll angezeigt wird, wenn ein neuer Spieler den Server betretet
connection: '<username><ip> (<protocol>) hat sich verbunden.'
connection: '<username>/<ip> (<protocol>) hat sich verbunden.'
# Nachricht, die im Protokoll angezeigt wird, wenn ein Spieler die Verifizierung nicht besteht
failed: '<ip> (<protocol>) hat den Bot-Check nicht bestanden wegen: <reason>'
# Nachricht, die im Protokoll angezeigt wird, wenn ein Spieler aufgrund zu vieler fehlgeschlagener Versuche auf die Blacklist gesetzt wird
@@ -181,7 +181,7 @@ verification:
# Translations for all debug messages during the verification
logs:
# Message logged to console whenever a new player joins the server
connection: '<username><ip> (<protocol>) has connected.'
connection: '<username>/<ip> (<protocol>) has connected.'
# Message logged to console whenever a player fails verification
failed: '<ip> (<protocol>) has failed the bot check for: <reason>'
# Message logged to console whenever a player is blacklisted
@@ -181,7 +181,7 @@ verification:
# Translations for all debug messages during the verification
logs:
# Message logged to console whenever a new player joins the server
connection: '<username><ip> (<protocol>) se ha conectado.'
connection: '<username>/<ip> (<protocol>) se ha conectado.'
# Message logged to console whenever a player fails verification
failed: '<ip> (<protocol>) no ha pasado la verificación de bot por: <reason>'
# Message logged to console whenever a player is blacklisted
@@ -181,7 +181,7 @@ verification:
# Traductions pour tous les messages de débogage pendant la vérification
logs:
# Message enregistré dans la console chaque fois qu'un nouveau joueur rejoint le serveur
connection: "<username><ip> (<protocol>) s'est connecté."
connection: "<username>/<ip> (<protocol>) s'est connecté."
# Message enregistré dans la console chaque fois qu'un joueur échoue à la vérification
failed: '<ip> (<protocol>) a échoué au test du bot pour : <reason>'
# Message enregistré dans la console chaque fois qu'un joueur est sur liste noire
@@ -181,7 +181,7 @@ verification:
# Terjemahan untuk semua pesan debug selama proses verifikasi
logs:
# Pesan yang dicatat ke konsol setiap kali pemain baru bergabung ke server
connection: '<username><ip> (<protocol>) telah terhubung.'
connection: '<username>/<ip> (<protocol>) telah terhubung.'
# Pesan yang dicatat ke konsol setiap kali pemain gagal verifikasi
failed: '<ip> (<protocol>) telah gagal dalam pemeriksaan bot untuk: <reason>'
# Pesan yang dicatat ke konsol setiap kali pemain masuk daftar hitam
@@ -181,7 +181,7 @@ verification:
# თარგმანი ყველა დეტალური ინფორმაციის მესიჯებისა
logs:
# მესიჯი ჩაწერილი კონსოლში, როცა ახალი მოთამაშე შემოდის
connection: '<username><ip> (<protocol>) დაკავშირდა.'
connection: '<username>/<ip> (<protocol>) დაკავშირდა.'
# მესიჯი ჩაწერილი კონსოლში, როცა მოთამაშე წარუმატებლად ასრულებს შემოწმებას
failed: '<ip> (<protocol>) წარუმატებლად გაირა შემოწმება: <reason>'
# მესიჯი ჩაწერილი კონსოლში, როცა მოთამაშე შავ ნუსხაშია
@@ -181,7 +181,7 @@ verification:
# Vertalingen voor alle debug-berichten tijdens de verificatie
logs:
# Bericht dat in de console wordt geregistreerd wanneer een nieuwe speler zich bij de server voegt
connection: '<username><ip> (<protocol>) heeft verbinding gemaakt.'
connection: '<username>/<ip> (<protocol>) heeft verbinding gemaakt.'
# Bericht dat in de console wordt geregistreerd wanneer de verificatie van een speler mislukt
failed: '<ip> (<protocol>) heeft de bot-controle niet doorstaan vanwege: <reason>'
# Bericht dat op de console wordt geregistreerd wanneer een speler op de zwarte lijst staat
@@ -184,7 +184,7 @@ verification:
# Translations for all debug messages during the verification
logs:
# Message logged to console whenever a new player joins the server
connection: '<username><ip> (<protocol>) połączył się.'
connection: '<username>/<ip> (<protocol>) połączył się.'
# Message logged to console whenever a player fails verification
failed: '<ip> (<protocol>) nie powiodło się sprawdzenie bota dla: <reason>'
# Message logged to console whenever a player is blacklisted
@@ -181,7 +181,7 @@ verification:
# Переводы всех консольных сообщений во время проверки
logs:
# Сообщение, которое показыватся когда на сервер заходит новый игрок
connection: '<username><ip> (<protocol>) подключился.'
connection: '<username>/<ip> (<protocol>) подключился.'
# Сообщение, которое показывается когда кто-то не прошел проверку на бота
failed: '<ip> (<protocol>) не прошел проверку бота на: <reason>'
# Сообщение, которое показывается когда кто-то было занесен в черный список
@@ -181,7 +181,7 @@ verification:
# Doğrulama sırasında tüm hata ayıklama mesajları için çeviriler
logs:
# Sunucuya yeni bir oyuncu katıldığında konsola kaydedilen mesaj
connection: '<username><ip> (<protocol>) sunucuya bağlandı.'
connection: '<username>/<ip> (<protocol>) sunucuya bağlandı.'
# Bir oyuncu doğrulamada başarısız olduğunda konsola kaydedilen mesaj
failed: '<ip> (<protocol>) bot korumasında başarısız oldu: <reason>'
# Bir oyuncu kara listeye alındığında konsola kaydedilen mesaj
@@ -181,7 +181,7 @@ verification:
# 验证过程中向控制台打印的调试信息
logs:
# 当玩家进入服务器并验证时显示的消息
connection: '<username><ip> (<protocol>) 连接到了服务器.'
connection: '<username>/<ip> (<protocol>) 连接到了服务器.'
# 当玩家未通过检查时打印的消息
failed: '<ip> (<protocol>) 由于原因 <reason> 无法通过检查.'
# 当玩家被自动列入黑名单时打印的消息
@@ -108,8 +108,8 @@ public final class SonarVelocity extends SonarBootstrap<SonarVelocityPlugin> {
@Override
public void disable() {
// Make sure to properly stop the metrics
if (metrics != null) {
// Make sure to properly shutdown bStats metrics
metrics.shutdown();
}
}
@@ -27,31 +27,28 @@ import xyz.jonesdev.sonar.common.fallback.FallbackInboundHandlerAdapter;
import java.net.InetSocketAddress;
import static com.velocitypowered.proxy.network.Connections.HANDLER;
import static xyz.jonesdev.sonar.common.fallback.protocol.packets.handshake.HandshakePacket.STATUS;
final class FallbackVelocityInboundHandler extends FallbackInboundHandlerAdapter {
@Override
public void channelRead(final @NotNull ChannelHandlerContext ctx, final @NotNull Object msg) throws Exception {
// Intercept any handshake packet by the client
if (msg instanceof HandshakePacket handshake) {
handleHandshake(ctx.channel(), handshake.getServerAddress(), handshake.getProtocolVersion().getProtocol());
// We don't care about server pings; remove the handler
if (handshake.getNextStatus() == 1) {
ctx.channel().pipeline().remove(this);
if (handshake.getNextStatus() == STATUS) {
ctx.pipeline().remove(this);
} else {
handleHandshake(ctx.channel(), handshake.getServerAddress(), handshake.getProtocolVersion().getProtocol());
}
}
// Intercept any server login packet by the client
else if (msg instanceof ServerLoginPacket serverLogin) {
} else if (msg instanceof ServerLoginPacket serverLogin) {
// Make sure to use the potentially modified, original IP
final MinecraftConnection minecraftConnection = (MinecraftConnection) ctx.channel().pipeline().get(HANDLER);
final MinecraftConnection minecraftConnection = (MinecraftConnection) ctx.pipeline().get(HANDLER);
final InetSocketAddress socketAddress = (InetSocketAddress) minecraftConnection.getRemoteAddress();
// We've done our job - deject this pipeline
ctx.channel().pipeline().remove(this);
// Let Sonar process the login packet
// Deject this pipeline and let Sonar process the login packet
ctx.pipeline().remove(this);
handleLogin(ctx.channel(), ctx, () -> ctx.fireChannelRead(msg), serverLogin.getUsername(), socketAddress);
return;
}
// Make sure to let the server handle the rest
ctx.fireChannelRead(msg);
}
}
@@ -23,7 +23,7 @@ import com.velocitypowered.proxy.network.ServerChannelInitializerHolder;
import io.netty.channel.Channel;
import io.netty.channel.ChannelInitializer;
import lombok.experimental.UtilityClass;
import xyz.jonesdev.sonar.api.ReflectiveOperationException;
import xyz.jonesdev.sonar.api.exception.ReflectiveOperationException;
import xyz.jonesdev.sonar.common.fallback.netty.FallbackInjectedChannelInitializer;
import java.lang.reflect.Field;