Compare commits

...
41 Commits
Author SHA1 Message Date
Jones 47cb4d0840 Release 2.1.5 (#377) 2024-09-09 14:53:56 +02:00
dependabot[bot] 38ecb9ac06 Bump io.netty:netty-all from 4.1.112.Final to 4.1.113.Final (#376)
Bumps [io.netty:netty-all](https://github.com/netty/netty) from 4.1.112.Final to 4.1.113.Final.
- [Commits](https://github.com/netty/netty/compare/netty-4.1.112.Final...netty-4.1.113.Final)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-09 14:31:43 +02:00
Jones 6657a3d674 Merge all custom logger wrappers into one 2024-09-08 18:32:35 +02:00
Jones 823ee21cea Clear up a small misconception in the code 2024-09-08 18:16:51 +02:00
Jones 4d206af0ae Don't allow players to send empty packets 2024-09-08 00:47:25 +02:00
Jones 2ff7b7efaa Fix gravity check false positive on 1.7 2024-09-07 23:19:10 +02:00
Jones a5efc1c796 Recode Y position and gravity calculation 2024-09-07 20:00:52 +02:00
Jones 85d5ce77b1 Implement SetPlayerRotationPacket packet order check 2024-09-07 19:34:46 +02:00
Jones 6c4d4b79d6 Implement PaddleBoatPacket packet order check 2024-09-07 19:34:19 +02:00
Jones 364b1c5b31 Optimize image RGB buffer writing 2024-09-07 14:48:55 +02:00
Jones 4edf04ede5 Randomize some placeholder variables during login 2024-09-07 12:53:24 +02:00
Jones 01da1a70ea Remove useless PlayerInfoPacket 2024-09-07 12:00:53 +02:00
Jones 9bb7c7cd60 Only log expired database entries if there are any 2024-09-07 11:20:12 +02:00
Jones aaa7588dcd Don't load the database driver twice 2024-09-07 11:19:06 +02:00
Jones 8d378d56ef Use regular JdbcConnectionSource, not pooled 2024-09-07 11:17:49 +02:00
Jones 476b1704b8 VarInt encoding optimizations (https://github.com/PaperMC/Velocity/commit/f034c0277d60e9f0a80e484832aec831cbc298f7) 2024-09-06 23:16:41 +02:00
Jones 26c31ceeaf VarInt reading optimizations (https://github.com/PaperMC/Velocity/pull/1418) 2024-09-06 23:07:26 +02:00
Jones 40f65ef5eb Rename 1.16(.2) codecs for more clarity 2024-09-06 22:54:50 +02:00
Jones 7736ccff86 Fix multiple block types being incorrect on 1.16 2024-09-06 22:53:24 +02:00
Jones 437a547ec3 [ci skip] Fix translation errors in copyright notice 2024-09-06 22:42:03 +02:00
Jones 4ec65218da [ci skip] Remove unused configuration entries 2024-09-06 22:31:15 +02:00
Jones de45e9f808 Mention TransferTool for Geyser in the config 2024-09-04 17:38:02 +02:00
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
117 changed files with 836 additions and 1059 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.5
description=A lightweight, effective, and easy-to-use anti-bot plugin for Velocity, BungeeCord, and Bukkit.
+1 -1
View File
@@ -1,5 +1,5 @@
[versions]
netty = "4.1.112.Final"
netty = "4.1.113.Final"
adventure = "4.17.0"
adventure-platform = "4.3.4"
bstats = "3.0.3"
@@ -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;
}
@@ -21,6 +21,7 @@ import lombok.Getter;
import org.jetbrains.annotations.NotNull;
import org.simpleyaml.configuration.MemorySection;
import org.simpleyaml.configuration.file.YamlFile;
import xyz.jonesdev.sonar.api.Sonar;
import java.io.File;
import java.io.InputStream;
@@ -31,8 +32,6 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import static xyz.jonesdev.sonar.api.config.SonarConfiguration.LOGGER;
@Getter
public final class SimpleYamlConfig {
private final File file;
@@ -90,7 +89,7 @@ public final class SimpleYamlConfig {
public int getInt(final String path) {
if (!yaml.contains(path)) {
LOGGER.warn("Could not find {} in {}.", path, file.getName());
Sonar.get().getLogger().warn("Could not find {} in {}.", path, file.getName());
return 0;
}
return yaml.getInt(path);
@@ -98,7 +97,7 @@ public final class SimpleYamlConfig {
public boolean getBoolean(final String path) {
if (!yaml.contains(path)) {
LOGGER.warn("Could not find {} in {}.", path, file.getName());
Sonar.get().getLogger().warn("Could not find {} in {}.", path, file.getName());
return false;
}
return yaml.getBoolean(path);
@@ -106,7 +105,7 @@ public final class SimpleYamlConfig {
public @NotNull String getString(final String path) {
if (!yaml.contains(path)) {
LOGGER.warn("Could not find {} in {}.", path, file.getName());
Sonar.get().getLogger().warn("Could not find {} in {}.", path, file.getName());
return "";
}
final Object object = yaml.get(path);
@@ -118,7 +117,7 @@ public final class SimpleYamlConfig {
public List<String> getStringList(final String path) {
if (!yaml.contains(path)) {
LOGGER.warn("Could not find {} in {}.", path, file.getName());
Sonar.get().getLogger().warn("Could not find {} in {}.", path, file.getName());
return new ArrayList<>(0);
}
return yaml.getStringList(path);
@@ -126,7 +125,7 @@ public final class SimpleYamlConfig {
public List<Integer> getIntList(final String path) {
if (!yaml.contains(path)) {
LOGGER.warn("Could not find {} in {}.", path, file.getName());
Sonar.get().getLogger().warn("Could not find {} in {}.", path, file.getName());
return new ArrayList<>(0);
}
return yaml.getIntegerList(path);
@@ -27,10 +27,9 @@ 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.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.database.ormlite.H2DatabaseTypeAdapter;
import xyz.jonesdev.sonar.api.database.ormlite.MariaDbDatabaseTypeAdapter;
import xyz.jonesdev.sonar.api.database.ormlite.MysqlDatabaseTypeAdapter;
import xyz.jonesdev.sonar.api.webhook.DiscordWebhook;
import java.io.File;
@@ -52,24 +51,6 @@ public final class SonarConfiguration {
@Getter
private Language language;
static final LoggerWrapper LOGGER = new LoggerWrapper() {
@Override
public void info(final String message, final Object... args) {
Sonar.get().getLogger().info("[config] " + message, args);
}
@Override
public void warn(final String message, final Object... args) {
Sonar.get().getLogger().warn("[config] " + message, args);
}
@Override
public void error(final String message, final Object... args) {
Sonar.get().getLogger().error("[config] " + message, args);
}
};
public SonarConfiguration(final @NotNull File pluginFolder) {
this.pluginFolder = pluginFolder;
this.messagesConfig = new SimpleYamlConfig(new File(pluginFolder, "messages.yml"));
@@ -85,14 +66,14 @@ public final class SonarConfiguration {
final URL defaultLanguageFile = Sonar.class.getResource("/assets/language.properties");
// Make sure the file actually exists before trying to copy it
if (defaultLanguageFile == null) {
LOGGER.error("Cannot check for custom language (is the file missing?)");
Sonar.get().getLogger().error("Cannot check for custom language (is the file missing?)");
return DEFAULT_FALLBACK_LANGUAGE;
}
// Copy the file to the plugin data directory
try (final InputStream inputStream = defaultLanguageFile.openStream()) {
Files.copy(inputStream, languageFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
} catch (IOException exception) {
LOGGER.error("Error copying file: {}", exception);
Sonar.get().getLogger().error("Error copying file: {}", exception);
return DEFAULT_FALLBACK_LANGUAGE;
}
}
@@ -106,13 +87,13 @@ public final class SonarConfiguration {
// Try parsing the property as a language
return Language.fromCode(property);
} catch (Throwable throwable) {
LOGGER.error("Could not find requested language: {}", throwable);
LOGGER.error("You can view a full list of valid language codes here:");
LOGGER.error("https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes");
LOGGER.error("If a translation does not exist yet, Sonar will use English (en).");
Sonar.get().getLogger().error("Could not find requested language: {}", throwable);
Sonar.get().getLogger().error("You can view a full list of valid language codes here:");
Sonar.get().getLogger().error("https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes");
Sonar.get().getLogger().error("If a translation does not exist yet, Sonar will use English (en).");
}
} catch (IOException exception) {
LOGGER.error("Error reading language file: {}", exception);
Sonar.get().getLogger().error("Error reading language file: {}", exception);
}
return DEFAULT_FALLBACK_LANGUAGE;
}
@@ -131,13 +112,13 @@ public final class SonarConfiguration {
final String property = System.getProperty("user.language", "en");
language = Language.fromCode(property);
// Make sure the user knows that we're using the system language for translations
LOGGER.info("Using system language ({}) for translations.", language);
Sonar.get().getLogger().info("Using system language ({}) for translations.", language);
} catch (Exception exception) {
LOGGER.warn("Could not use system language for translations.");
LOGGER.warn("Using default language ({}) for translations.", language);
Sonar.get().getLogger().warn("Could not use system language for translations.");
Sonar.get().getLogger().warn("Using default language ({}) for translations.", language);
}
} else {
LOGGER.info("Using custom language ({}) for translations.", language);
Sonar.get().getLogger().info("Using custom language ({}) for translations.", language);
}
// Load all configurations
@@ -177,12 +158,12 @@ public final class SonarConfiguration {
verification.timing = Verification.Timing.valueOf(generalConfig.getString("verification.timing"));
// Warn the user if the verification timing is set to NEVER
if (verification.timing == Verification.Timing.NEVER) {
LOGGER.warn(" ");
LOGGER.warn("You have set the verification timing to 'NEVER'.");
LOGGER.warn("Sonar will NOT perform the bot verification at all, therefore making it useless.");
LOGGER.warn("It is highly suggested to set this option to either 'DURING_ATTACK' or 'ALWAYS'.");
LOGGER.warn("Please only edit this option if you really know what you are doing.");
LOGGER.warn(" ");
Sonar.get().getLogger().warn(" ");
Sonar.get().getLogger().warn("You have set the verification timing to 'NEVER'.");
Sonar.get().getLogger().warn("Sonar will NOT perform the bot verification at all, therefore making it useless.");
Sonar.get().getLogger().warn("It is highly suggested to set this option to either 'DURING_ATTACK' or 'ALWAYS'.");
Sonar.get().getLogger().warn("Please only edit this option if you really know what you are doing.");
Sonar.get().getLogger().warn(" ");
}
verification.gravity.enabled = generalConfig.getBoolean("verification.checks.gravity.enabled");
@@ -303,7 +284,7 @@ public final class SonarConfiguration {
final String resourceName = url + "/" + language.getCode() + ".yml";
URL result = Sonar.class.getResource("/assets/" + resourceName);
if (result == null) {
LOGGER.warn("Could not find " + resourceName + "! Using en.yml!");
Sonar.get().getLogger().warn("Could not find " + resourceName + "! Using en.yml!");
result = Objects.requireNonNull(Sonar.class.getResource("/assets/" + url + "/en.yml"));
}
return result;
@@ -317,8 +298,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
@@ -489,7 +470,7 @@ public final class SonarConfiguration {
private final DatabaseType databaseType;
private final Library databaseDriver;
@Setter
private boolean downloaded;
private boolean loaded;
}
private Type type;
@@ -15,12 +15,12 @@
* 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;
import com.j256.ormlite.dao.DaoManager;
import com.j256.ormlite.jdbc.JdbcPooledConnectionSource;
import com.j256.ormlite.jdbc.JdbcConnectionSource;
import com.j256.ormlite.stmt.QueryBuilder;
import com.j256.ormlite.support.ConnectionSource;
import com.j256.ormlite.table.TableUtils;
@@ -31,8 +31,7 @@ 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.logger.LoggerWrapper;
import xyz.jonesdev.sonar.api.model.VerifiedPlayer;
import xyz.jonesdev.sonar.api.database.model.VerifiedPlayer;
import java.io.File;
import java.sql.SQLException;
@@ -47,24 +46,6 @@ import java.util.concurrent.Executors;
public final class VerifiedPlayerController {
private static final ExecutorService DB_UPDATE_SERVICE = Executors.newSingleThreadExecutor();
private static final LoggerWrapper LOGGER = new LoggerWrapper() {
@Override
public void info(final String message, final Object... args) {
Sonar.get().getLogger().info("[database] " + message, args);
}
@Override
public void warn(final String message, final Object... args) {
Sonar.get().getLogger().warn("[database] " + message, args);
}
@Override
public void error(final String message, final Object... args) {
Sonar.get().getLogger().error("[database] " + message, args);
}
};
private final Map<String, Collection<UUID>> cache = new ConcurrentHashMap<>(128);
private @Nullable ConnectionSource connectionSource;
private Dao<VerifiedPlayer, Integer> dao;
@@ -74,26 +55,21 @@ 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.");
Sonar.get().getLogger().warn("Configure a database to save verified players.");
return;
}
// Make sure to only download the driver once
if (!cachedDatabaseType.isDownloaded()) {
LOGGER.info("Downloading {} driver version {}",
// Make sure to only load the driver once per database type
if (!cachedDatabaseType.isLoaded()) {
Sonar.get().getLogger().info("Loading {} driver version {}",
cachedDatabaseType.getDatabaseType().getDatabaseName(),
cachedDatabaseType.getDatabaseDriver().getVersion());
// Download and load the driver for the current database
libraryManager.loadLibrary(cachedDatabaseType.getDatabaseDriver());
cachedDatabaseType.setDownloaded(true);
cachedDatabaseType.setLoaded(true);
}
// Make sure to actually register the loader, so we can use it
cachedDatabaseType.getDatabaseType().loadDriver();
try {
final String jdbcURL;
@@ -112,7 +88,7 @@ public final class VerifiedPlayerController {
Sonar.get().getConfig().getGeneralConfig().getString("database.name"));
}
connectionSource = new JdbcPooledConnectionSource(jdbcURL,
connectionSource = new JdbcConnectionSource(jdbcURL,
Sonar.get().getConfig().getGeneralConfig().getString("database.username"),
Sonar.get().getConfig().getGeneralConfig().getString("database.password"),
cachedDatabaseType.getDatabaseType());
@@ -120,10 +96,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);
@@ -138,11 +116,11 @@ public final class VerifiedPlayerController {
// Add all entries from the database to the cache
dao.queryForAll().forEach(this::_add);
} catch (SQLException exception) {
LOGGER.error("Error initializing database: {}", exception);
Sonar.get().getLogger().error("Error initializing database: {}", exception);
}
});
} catch (SQLException exception) {
LOGGER.error("Error setting up database: {}", exception);
Sonar.get().getLogger().error("Error setting up database: {}", exception);
}
}
@@ -155,7 +133,7 @@ public final class VerifiedPlayerController {
try {
connectionSource.close();
} catch (Exception exception) {
LOGGER.error("Error closing database: {}", exception);
Sonar.get().getLogger().error("Error closing database: {}", exception);
}
}
}
@@ -172,11 +150,11 @@ public final class VerifiedPlayerController {
.lt("timestamp", new Timestamp(timestamp))
.query();
if (oldEntries != null) {
if (oldEntries != null && !oldEntries.isEmpty()) {
for (final VerifiedPlayer player : oldEntries) {
dao.delete(player);
}
LOGGER.info("Removed {} database entries older than {} days.",
Sonar.get().getLogger().info("Removed {} database entries older than {} days.",
oldEntries.size(), maximumAge);
}
}
@@ -213,7 +191,7 @@ public final class VerifiedPlayerController {
}
}
} catch (SQLException exception) {
LOGGER.error("Error trying to remove entry: {}", exception);
Sonar.get().getLogger().error("Error trying to remove entry: {}", exception);
}
});
}
@@ -251,7 +229,7 @@ public final class VerifiedPlayerController {
try {
dao.createIfNotExists(player);
} catch (SQLException exception) {
LOGGER.error("Error trying to add entry: {}", exception);
Sonar.get().getLogger().error("Error trying to add entry: {}", exception);
}
});
}
@@ -307,7 +285,7 @@ public final class VerifiedPlayerController {
try {
dao.deleteBuilder().delete();
} catch (SQLException exception) {
LOGGER.error("Error trying to clear entries: {}", exception);
Sonar.get().getLogger().error("Error trying to clear entries: {}", exception);
}
}
}
@@ -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,7 +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.logger.LoggerWrapper;
import xyz.jonesdev.sonar.api.fallback.ratelimit.Ratelimiter;
import java.net.InetAddress;
import java.util.concurrent.ConcurrentHashMap;
@@ -43,32 +43,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();
private final LoggerWrapper logger = new LoggerWrapper() {
@Override
public void info(final String message, final Object... args) {
Sonar.get().getLogger().info("[fallback] " + message, args);
}
@Override
public void warn(final String message, final Object... args) {
Sonar.get().getLogger().warn("[fallback] " + message, args);
}
@Override
public void error(final String message, final Object... args) {
Sonar.get().getLogger().error("[fallback] " + message, args);
}
};
@Setter
private Ratelimiter<InetAddress> ratelimiter;
public boolean shouldVerifyNewPlayers() {
return shouldPerform(Sonar.get().getConfig().getVerification().getTiming());
@@ -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;
@@ -35,13 +34,14 @@ import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.api.timer.SystemTimer;
import xyz.jonesdev.sonar.common.fallback.netty.FallbackTailExceptionsHandler;
import xyz.jonesdev.sonar.common.fallback.netty.FallbackVarInt21FrameDecoder;
import xyz.jonesdev.sonar.common.fallback.netty.FallbackVarInt21FrameEncoder;
import xyz.jonesdev.sonar.common.fallback.netty.FallbackVarIntLengthEncoder;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketDecoder;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketEncoder;
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().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
@@ -120,7 +116,7 @@ public final class FallbackUserWrapper implements FallbackUser {
// Replace normal encoder to allow custom packets
final FallbackPacketEncoder newEncoder = new FallbackPacketEncoder(protocolVersion);
pipeline.addFirst(FALLBACK_FRAME_ENCODER, FallbackVarInt21FrameEncoder.INSTANCE);
pipeline.addFirst(FALLBACK_FRAME_ENCODER, FallbackVarIntLengthEncoder.INSTANCE);
pipeline.addLast(FALLBACK_PACKET_ENCODER, newEncoder);
// Send LoginSuccess packet to make the client think they are joining the server
@@ -143,64 +139,55 @@ 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();
if (shouldLog) {
Sonar.get().getFallback().getLogger().info(
Sonar.get().getLogger().info(
Sonar.get().getConfig().getMessagesConfig().getString("verification.logs.failed")
.replace("<ip>", Sonar.get().getConfig().formatAddress(getInetAddress()))
.replace("<protocol>", String.valueOf(getProtocolVersion().getProtocol()))
.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().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);
@@ -1,57 +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.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 {
private int readVarInt, bytesRead;
private DecodeResult result = DecodeResult.TOO_SHORT;
@Override
public boolean process(byte k) {
if (k == 0 && bytesRead == 0) {
result = DecodeResult.RUN_OF_ZEROES;
return true;
}
if (result == DecodeResult.RUN_OF_ZEROES) {
return false;
}
readVarInt |= (k & 0x7F) << bytesRead++ * 7;
if (bytesRead > 3) {
result = DecodeResult.TOO_BIG;
return false;
}
if ((k & 0x80) != 128) {
result = DecodeResult.SUCCESS;
return false;
}
return true;
}
public enum DecodeResult {
SUCCESS,
TOO_SHORT,
TOO_BIG,
RUN_OF_ZEROES
}
}
@@ -20,58 +20,102 @@ 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 io.netty.handler.codec.DecoderException;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.common.util.exception.QuietDecoderException;
import java.util.List;
// Taken from
import static io.netty.util.ByteProcessor.FIND_NON_NUL;
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.DEBUG;
// 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 {
@Override
protected void decode(final @NotNull ChannelHandlerContext ctx, final ByteBuf in, final List<Object> out) {
protected void decode(final @NotNull ChannelHandlerContext ctx,
final @NotNull ByteBuf byteBuf,
final @NotNull List<Object> out) throws Exception {
if (!ctx.channel().isActive()) {
in.clear();
byteBuf.clear();
return;
}
final FallbackVarInt21ByteDecoder reader = new FallbackVarInt21ByteDecoder();
final int end = in.forEachByte(reader);
if (end == -1) {
// We tried to go beyond the end of the buffer.
// This is probably a good sign that the buffer was too short to hold a proper varint.
if (reader.getResult() == FallbackVarInt21ByteDecoder.DecodeResult.RUN_OF_ZEROES) {
// Special case where the entire packet is just a run of zeroes. We ignore them all.
in.clear();
}
// Skip any runs of 0x00 we might find
final int packetStart = byteBuf.forEachByte(FIND_NON_NUL);
if (packetStart == -1) {
return;
}
byteBuf.readerIndex(packetStart);
if (reader.getResult() == FallbackVarInt21ByteDecoder.DecodeResult.RUN_OF_ZEROES) {
// This will return to the point where the next varint starts
in.readerIndex(end);
} else if (reader.getResult() == FallbackVarInt21ByteDecoder.DecodeResult.SUCCESS) {
final int readVarInt = reader.getReadVarInt();
final int bytesRead = reader.getBytesRead();
// Try to read the length of the packet
byteBuf.markReaderIndex();
final int preIndex = byteBuf.readerIndex();
final int length = readRawVarInt21(byteBuf);
if (preIndex == byteBuf.readerIndex()) {
return;
}
if (length <= 0) {
throw DEBUG ? new DecoderException("Bad VarInt length: " + length) : QuietDecoderException.INSTANCE;
}
if (readVarInt < 0) {
in.clear();
throw new CorruptedFrameException("Invalid VarInt length " + readVarInt);
} else if (readVarInt == 0) {
// Skip over the empty packet(s) and ignore it
in.readerIndex(end + 1);
} else {
int minimumRead = bytesRead + readVarInt;
if (in.isReadable(minimumRead)) {
out.add(in.retainedSlice(end + 1, readVarInt));
in.skipBytes(minimumRead);
}
}
} else if (reader.getResult() == FallbackVarInt21ByteDecoder.DecodeResult.TOO_BIG) {
in.clear();
throw new CorruptedFrameException("Too big VarInt " + reader.getReadVarInt());
if (byteBuf.readableBytes() < length) {
byteBuf.resetReaderIndex();
} else {
out.add(byteBuf.readRetainedSlice(length));
}
}
private static int readRawVarInt21(final @NotNull ByteBuf byteBuf) {
if (byteBuf.readableBytes() < 4) {
return readRawVarIntSmallBuffer(byteBuf);
}
// take the last three bytes and check if any of them have the high bit set
final int wholeOrMore = byteBuf.getIntLE(byteBuf.readerIndex());
final int atStop = ~wholeOrMore & 0x808080;
if (atStop == 0) {
throw DEBUG ? new DecoderException("VarInt too big") : QuietDecoderException.INSTANCE;
}
final int bitsToKeep = Integer.numberOfTrailingZeros(atStop) + 1;
byteBuf.skipBytes(bitsToKeep >> 3);
// https://github.com/netty/netty/pull/14050#issuecomment-2107750734
int preservedBytes = wholeOrMore & (atStop ^ (atStop - 1));
// https://github.com/netty/netty/pull/14050#discussion_r1597896639
preservedBytes = (preservedBytes & 0x007F007F) | ((preservedBytes & 0x00007F00) >> 1);
preservedBytes = (preservedBytes & 0x00003FFF) | ((preservedBytes & 0x3FFF0000) >> 2);
return preservedBytes;
}
private static int readRawVarIntSmallBuffer(final @NotNull ByteBuf byteBuf) {
if (!byteBuf.isReadable()) {
return 0;
}
byteBuf.markReaderIndex();
byte tmp = byteBuf.readByte();
if (tmp >= 0) {
return tmp;
}
int result = tmp & 0x7F;
if (!byteBuf.isReadable()) {
byteBuf.resetReaderIndex();
return 0;
}
if ((tmp = byteBuf.readByte()) >= 0) {
return result | tmp << 7;
}
result |= (tmp & 0x7F) << 7;
if (!byteBuf.isReadable()) {
byteBuf.resetReaderIndex();
return 0;
}
if ((tmp = byteBuf.readByte()) >= 0) {
return result | tmp << 14;
}
return result | (tmp & 0x7F) << 14;
}
}
@@ -20,31 +20,30 @@ package xyz.jonesdev.sonar.common.fallback.netty;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;
import io.netty.handler.codec.MessageToMessageEncoder;
import org.jetbrains.annotations.NotNull;
import java.util.List;
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> {
public static final FallbackVarInt21FrameEncoder INSTANCE = new FallbackVarInt21FrameEncoder();
public final class FallbackVarIntLengthEncoder extends MessageToMessageEncoder<ByteBuf> {
public static final FallbackVarIntLengthEncoder INSTANCE = new FallbackVarIntLengthEncoder();
@Override
protected void encode(final @NotNull ChannelHandlerContext ctx,
final @NotNull ByteBuf msg,
final @NotNull ByteBuf out) throws Exception {
writeVarInt(out, msg.readableBytes());
out.writeBytes(msg);
}
final @NotNull ByteBuf byteBuf,
final @NotNull List<Object> out) throws Exception {
final int length = byteBuf.readableBytes();
final int varIntLength = varIntBytes(length);
@Override
protected ByteBuf allocateBuffer(final @NotNull ChannelHandlerContext ctx,
final @NotNull ByteBuf msg,
final boolean preferDirect) throws Exception {
final int anticipatedRequiredCapacity = varIntBytes(msg.readableBytes()) + msg.readableBytes();
return ctx.alloc().buffer(anticipatedRequiredCapacity);
final ByteBuf lenBuf = ctx.alloc().buffer(varIntLength);
writeVarInt(lenBuf, length);
out.add(lenBuf);
out.add(byteBuf.retain());
}
}
@@ -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 {
@@ -218,22 +217,6 @@ public enum FallbackPacketRegistry {
map(0x0E, MINECRAFT_1_7_2, true),
map(0x00, MINECRAFT_1_9, true),
map(0x01, MINECRAFT_1_19_4, true));
clientbound.register(PlayerInfoPacket.class, PlayerInfoPacket::new,
map(0x38, MINECRAFT_1_7_2, true),
map(0x2D, MINECRAFT_1_9, true),
map(0x2E, MINECRAFT_1_12_1, true),
map(0x30, MINECRAFT_1_13, true),
map(0x33, MINECRAFT_1_14, true),
map(0x34, MINECRAFT_1_15, true),
map(0x33, MINECRAFT_1_16, true),
map(0x32, MINECRAFT_1_16_2, true),
map(0x36, MINECRAFT_1_17, true),
map(0x34, MINECRAFT_1_19, true),
map(0x37, MINECRAFT_1_19_1, true),
map(0x36, MINECRAFT_1_19_3, true),
map(0x3A, MINECRAFT_1_19_4, true),
map(0x3C, MINECRAFT_1_20_2, true),
map(0x3E, MINECRAFT_1_20_5, true));
clientbound.register(UpdateSectionBlocksPacket.class, UpdateSectionBlocksPacket::new,
map(0x22, MINECRAFT_1_7_2, true),
map(0x10, MINECRAFT_1_9, true),
@@ -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;
@@ -33,12 +34,12 @@ import xyz.jonesdev.sonar.common.fallback.protocol.packets.login.LoginSuccessPac
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.*;
import xyz.jonesdev.sonar.common.util.ComponentHolder;
import java.security.SecureRandom;
import java.util.Random;
import java.util.UUID;
@UtilityClass
public class FallbackPreparer {
private final SecureRandom RANDOM = new SecureRandom();
private final Random RANDOM = new Random();
// LoginSuccess
public FallbackPacket loginSuccess;
// Abilities
@@ -65,9 +66,6 @@ public class FallbackPreparer {
public FallbackPacket joinGame;
// Update Section Blocks
public FallbackPacket updateSectionBlocks;
// Player Info
public final FallbackPacket PLAYER_INFO = new FallbackPacketSnapshot(new PlayerInfoPacket(
"", new UUID(1L, 1L), 2));
// Default Spawn Position
public FallbackPacket defaultSpawnPosition;
// Spawn Position
@@ -95,7 +93,7 @@ public class FallbackPreparer {
public final int BLOCKS_PER_ROW = 8; // 8 * 8 = 64 (protocol maximum)
public final int SPAWN_X_POSITION = 16 / 2; // middle of the chunk
public final int SPAWN_Z_POSITION = 16 / 2; // middle of the chunk
public final int DEFAULT_Y_COLLIDE_POSITION = 255; // 255 is the maximum Y position allowed
public final int DEFAULT_Y_COLLIDE_POSITION = 155 + RANDOM.nextInt(101); // 255 is the maximum Y position
public final int IN_AIR_Y_POSITION = 1337;
// CAPTCHA position
@@ -105,7 +103,6 @@ public class FallbackPreparer {
// Platform
public BlockType blockType;
public int maxMovementTick, dynamicSpawnYPosition;
public double[] preparedCachedYMotions;
public double maxFallDistance;
// XP packets
@@ -118,25 +115,24 @@ public class FallbackPreparer {
FallbackPacketRegistry.values();
// Prepare LoginSuccess packet
loginSuccess = new FallbackPacketSnapshot(new LoginSuccessPacket(new UUID(1L, 1L),
loginSuccess = new FallbackPacketSnapshot(new LoginSuccessPacket(UUID.randomUUID(),
Sonar.get().getConfig().getGeneralConfig().getString("verification.cached-username")));
// Prepare JoinGame packet
joinGame = new FallbackPacketSnapshot(new JoinGamePacket(PLAYER_ENTITY_ID,
Sonar.get().getConfig().getVerification().getGamemode().getId(),
0, false, 0,
RANDOM.nextLong(), false, 0,
true, false, false,
new String[]{"minecraft:overworld"}, "minecraft:overworld"));
// Prepare cached motions for the gravity check
maxFallDistance = 0;
// Prepare the gravity check
maxMovementTick = Sonar.get().getConfig().getVerification().getGravity().getMaxMovementTicks();
preparedCachedYMotions = new double[maxMovementTick + 8];
maxFallDistance = 1;
for (int i = 0; i < preparedCachedYMotions.length; i++) {
final double gravity = -((Math.pow(0.98, i) - 1) * 3.92);
preparedCachedYMotions[i] = gravity;
maxFallDistance += gravity;
double motionY = -0.08 * 0.98f;
for (int i = 0; i < maxMovementTick; i++) {
motionY = (motionY - 0.08) * 0.98f;
maxFallDistance += Math.abs(motionY);
}
// Set the dynamic block and collide Y position based on the maximum fall distance
@@ -210,6 +206,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 +222,7 @@ public class FallbackPreparer {
enterCodeMessage = null;
incorrectCaptcha = null;
xpCountdown = null;
Sonar.get().getFallback().setCaptchaGenerator(null);
}
}
}
@@ -52,8 +52,8 @@ public enum BlockType {
case MINECRAFT_1_15:
case MINECRAFT_1_15_1:
case MINECRAFT_1_15_2:
case MINECRAFT_1_16:
return 5116;
case MINECRAFT_1_16:
case MINECRAFT_1_16_1:
return 5132;
case MINECRAFT_1_16_2:
@@ -102,8 +102,8 @@ public enum BlockType {
case MINECRAFT_1_15:
case MINECRAFT_1_15_1:
case MINECRAFT_1_15_2:
case MINECRAFT_1_16:
return 7016;
case MINECRAFT_1_16:
case MINECRAFT_1_16_1:
return 7552;
case MINECRAFT_1_16_2:
@@ -54,15 +54,14 @@ public class CaptchaPreparer {
// Prepare everything asynchronously
PREPARATION_SERVICE.execute(() -> {
for (preparedAmount = 0; preparedAmount < precomputeAmount;) {
// Generate CAPTCHA answer
// Generate CAPTCHA
final char[] answer = new char[3 + RANDOM.nextInt(2)];
for (int j = 0; j < answer.length; j++) {
answer[j] = alphabet[RANDOM.nextInt(alphabet.length)];
}
final BufferedImage image = Sonar.get().getFallback().getCaptchaGenerator().createImage(answer);
// Convert and cache converted Minecraft map bytes
final int[] buffer = MapColorPalette.getBufferFromImage(image);
cached[preparedAmount++] = new MapCaptchaInfo(new String(answer), buffer);
cached[preparedAmount++] = new MapCaptchaInfo(new String(answer), MapColorPalette.imageToBuffer(image));
}
Sonar.get().getLogger().info("Finished preparing {} CAPTCHA answers ({}s)!", preparedAmount, timer);
@@ -32,14 +32,13 @@ public final class MapCaptchaInfo {
private final FallbackPacket[] legacy;
private final FallbackPacket modern;
public MapCaptchaInfo(final @NotNull String answer, final int @NotNull [] buffer) {
public MapCaptchaInfo(final @NotNull String answer, final byte @NotNull [] buffer) {
this.answer = answer;
// Prepare 1.7 map data using a grid
final int[][] grid = new int[128][128];
final byte[][] grid = new byte[128][128];
for (int i = 0; i < buffer.length; i++) {
final int buf = buffer[i];
grid[i & Byte.MAX_VALUE][i >> 7] = buf;
grid[i & Byte.MAX_VALUE][i >> 7] = buffer[i];
}
this.legacy = new FallbackPacket[grid.length];
for (int i = 0; i < grid.length; i++) {
@@ -21,6 +21,7 @@ import lombok.experimental.UtilityClass;
import org.jetbrains.annotations.NotNull;
import java.awt.image.BufferedImage;
import java.awt.image.Raster;
@UtilityClass
public class MapColorPalette {
@@ -172,25 +173,32 @@ public class MapColorPalette {
-12910336,
};
public int[] getBufferFromImage(final @NotNull BufferedImage bufferedImage) {
final int[] buffer = new int[128 * 128];
bufferedImage.getRGB(0, 0,
bufferedImage.getWidth(), bufferedImage.getHeight(),
buffer, 0, bufferedImage.getWidth());
for (int i = 0; i < buffer.length; i++) {
buffer[i] = mapColorToMinecraft(buffer[i]);
}
public byte[] imageToBuffer(final @NotNull BufferedImage bufferedImage) {
final byte[] buffer = new byte[128 * 128];
writeRGBBuffer(bufferedImage, buffer);
return buffer;
}
private byte mapColorToMinecraft(final int argb) {
private void writeRGBBuffer(final @NotNull BufferedImage bufferedImage, final byte @NotNull [] rgbArray) {
final Raster raster = bufferedImage.getRaster();
final Object data = new byte[raster.getNumBands()];
int yoff = 0, off;
for (int y = 0; y < raster.getHeight(); y++, yoff += raster.getWidth()) {
off = yoff;
for (int x = 0; x < raster.getWidth(); x++) {
final int rgb = bufferedImage.getColorModel().getRGB(raster.getDataElements(x, y, data));
rgbArray[off++] = rgbToNearestMinecraft(rgb);
}
}
}
private byte rgbToNearestMinecraft(final int rgb) {
int minDistance = Integer.MAX_VALUE;
byte closestIndex = 0;
for (int i = 0; i < COLOR_MAP.length; i++) {
final byte index = (byte) (i < 128 ? i : -129 + (i - 127));
final int distance = calculateColorDistance(argb, COLOR_MAP[i]);
final int distance = calculateColorDistance(rgb, COLOR_MAP[i]);
if (distance < minDistance) {
minDistance = distance;
closestIndex = index;
@@ -30,38 +30,32 @@ import java.util.Objects;
@UtilityClass
public final class DimensionRegistry {
public final CompoundBinaryTag CODEC_1_16;
public final CompoundBinaryTag CODEC_1_16_2;
public final CompoundBinaryTag CODEC_1_18_2;
public final CompoundBinaryTag CODEC_1_19;
public final CompoundBinaryTag CODEC_1_19_1;
public final CompoundBinaryTag CODEC_1_19_4;
public final CompoundBinaryTag CODEC_1_20;
public final CompoundBinaryTag CODEC_1_21;
public final DimensionInfo DEFAULT_DIMENSION_1_16;
public final DimensionInfo DEFAULT_DIMENSION_1_18_2;
public final @NotNull CompoundBinaryTag CODEC_1_16;
public final @NotNull CompoundBinaryTag CODEC_1_18_2;
public final @NotNull CompoundBinaryTag CODEC_1_19;
public final @NotNull CompoundBinaryTag CODEC_1_19_1;
public final @NotNull CompoundBinaryTag CODEC_1_19_4;
public final @NotNull CompoundBinaryTag CODEC_1_20;
public final @NotNull CompoundBinaryTag CODEC_1_21;
public final @NotNull CompoundBinaryTag OLD_CODEC;
static {
CODEC_1_16 = getCodec("codec_1_16.nbt");
CODEC_1_16_2 = getCodec("codec_1_16_2.nbt");
CODEC_1_18_2 = getCodec("codec_1_18_2.nbt");
CODEC_1_19 = getCodec("codec_1_19.nbt");
CODEC_1_19_1 = getCodec("codec_1_19_1.nbt");
CODEC_1_19_4 = getCodec("codec_1_19_4.nbt");
CODEC_1_20 = getCodec("codec_1_20.nbt");
CODEC_1_21 = getCodec("codec_1_21.nbt");
OLD_CODEC = getCodec("codec_old.nbt");
DEFAULT_DIMENSION_1_16 = getDimension(CODEC_1_16);
DEFAULT_DIMENSION_1_16 = getDimension(CODEC_1_16_2);
DEFAULT_DIMENSION_1_18_2 = getDimension(CODEC_1_18_2);
}
private static @NotNull DimensionInfo getDimension(final @NotNull CompoundBinaryTag tag) {
final ListBinaryTag dimensions = tag.getCompound("minecraft:dimension_type").getList("value");
final BinaryTag elementTag = ((CompoundBinaryTag) dimensions.get(0)).get("element");
return new DimensionInfo("minecraft:overworld", 0, (CompoundBinaryTag) elementTag);
}
private @NotNull CompoundBinaryTag getCodec(final @NotNull String fileName) {
try (final InputStream inputStream = Sonar.class.getResourceAsStream("/assets/codecs/" + fileName)) {
return BinaryTagIO.reader().read(Objects.requireNonNull(inputStream), BinaryTagIO.Compression.GZIP);
@@ -70,4 +64,10 @@ public final class DimensionRegistry {
throw new IllegalStateException(throwable);
}
}
private static @NotNull DimensionInfo getDimension(final @NotNull CompoundBinaryTag tag) {
final ListBinaryTag dimensions = tag.getCompound("minecraft:dimension_type").getList("value");
final BinaryTag elementTag = ((CompoundBinaryTag) dimensions.get(0)).get("element");
return new DimensionInfo("minecraft:overworld", 0, (CompoundBinaryTag) elementTag);
}
}
@@ -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
@@ -105,7 +104,7 @@ public final class JoinGamePacket implements FallbackPacket {
byteBuf.writeByte(gamemode);
byteBuf.writeByte(-1); // previous gamemode
writeStringArray(byteBuf, levelNames);
writeCompoundTag(byteBuf, OLD_CODEC);
writeCompoundTag(byteBuf, CODEC_1_16);
writeString(byteBuf, DEFAULT_DIMENSION_1_16.getIdentifier());
writeString(byteBuf, levelName);
byteBuf.writeLong(partialHashedSeed);
@@ -123,7 +122,7 @@ public final class JoinGamePacket implements FallbackPacket {
byteBuf.writeByte(gamemode);
byteBuf.writeByte(-1); // previous gamemode
writeStringArray(byteBuf, levelNames);
writeCompoundTag(byteBuf, CODEC_1_16);
writeCompoundTag(byteBuf, CODEC_1_16_2);
writeCompoundTag(byteBuf, DEFAULT_DIMENSION_1_16.getTag());
writeString(byteBuf, levelName);
byteBuf.writeLong(partialHashedSeed);
@@ -31,7 +31,7 @@ import static xyz.jonesdev.sonar.common.util.ProtocolUtil.writeVarInt;
@NoArgsConstructor
@AllArgsConstructor
public final class MapDataPacket implements FallbackPacket {
private int[] buffer;
private byte[] buffer;
private int x, y;
@Override
@@ -43,7 +43,7 @@ public final class MapDataPacket implements FallbackPacket {
byteBuf.writeByte(0); // scaling
byteBuf.writeByte(x);
byteBuf.writeByte(y);
writeArray(byteBuf, buffer);
byteBuf.writeBytes(buffer);
} else {
byteBuf.writeByte(0); // scaling
@@ -68,13 +68,7 @@ public final class MapDataPacket implements FallbackPacket {
byteBuf.writeByte(y);
writeVarInt(byteBuf, buffer.length);
writeArray(byteBuf, buffer);
}
}
private static void writeArray(final ByteBuf byteBuf, final int @NotNull [] array) {
for (final int i : array) {
byteBuf.writeByte(i);
byteBuf.writeBytes(buffer);
}
}
@@ -1,98 +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.common.fallback.protocol.packets.play;
import io.netty.buffer.ByteBuf;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
import java.util.EnumSet;
import java.util.UUID;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.*;
@Getter
@ToString
@NoArgsConstructor
@AllArgsConstructor
public final class PlayerInfoPacket implements FallbackPacket {
private String username;
private UUID uuid;
private int gamemode;
@Override
public void encode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) throws Exception {
if (protocolVersion.compareTo(MINECRAFT_1_8) < 0) {
writeString(byteBuf, username);
byteBuf.writeBoolean(true); // online
byteBuf.writeShort(0);
return;
}
// https://wiki.vg/Protocol#player-info:player-actions
if (protocolVersion.compareTo(MINECRAFT_1_19_3) >= 0) {
final EnumSet<Action> actions = EnumSet.noneOf(Action.class);
actions.add(Action.ADD_PLAYER);
actions.add(Action.UPDATE_LISTED);
actions.add(Action.UPDATE_GAMEMODE);
writeEnumSet(byteBuf, actions, Action.class);
writeVarInt(byteBuf, 1); // size
writeUUID(byteBuf, uuid);
writeString(byteBuf, username);
writeVarInt(byteBuf, 0); // properties
byteBuf.writeBoolean(true); // update listed
writeVarInt(byteBuf, gamemode);
return;
}
writeVarInt(byteBuf, 0); // ADD_PLAYER
writeVarInt(byteBuf, 1);
writeUUID(byteBuf, uuid);
writeString(byteBuf, username);
writeVarInt(byteBuf, 0);
writeVarInt(byteBuf, gamemode);
writeVarInt(byteBuf, 60);
byteBuf.writeBoolean(false);
if (protocolVersion.compareTo(MINECRAFT_1_19) >= 0) {
byteBuf.writeBoolean(false);
}
}
@Override
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
throw new UnsupportedOperationException();
}
public enum Action {
ADD_PLAYER,
INITIALIZE_CHAT,
UPDATE_GAMEMODE,
UPDATE_LISTED,
UPDATE_LATENCY,
UPDATE_DISPLAY_NAME
}
}
@@ -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();
@@ -52,10 +52,6 @@ public final class FallbackGravitySessionHandler extends FallbackSessionHandler
if (user.getProtocolVersion().compareTo(MINECRAFT_1_19_3) >= 0) {
user.delayedWrite(defaultSpawnPosition);
}
// Write the PlayerInfo packet to the buffer
if (user.getProtocolVersion().compareTo(MINECRAFT_1_16_4) >= 0) {
user.delayedWrite(PLAYER_INFO);
}
// 1.20.3+ introduced game events
// Make sure the client knows that we're sending chunks next
if (user.getProtocolVersion().compareTo(MINECRAFT_1_20_3) >= 0) {
@@ -166,7 +162,7 @@ public final class FallbackGravitySessionHandler extends FallbackSessionHandler
// Log/debug position if enabled in the configuration
if (Sonar.get().getConfig().getVerification().isDebugXYZPositions()) {
Sonar.get().getFallback().getLogger().info("{}: {}/{}/{}, ly={}/dy={}, {}",
Sonar.get().getLogger().info("{}: {}/{}/{}, ly={}/dy={}, {}",
username, x, y, z, lastY, deltaY, isOnGround);
}
@@ -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;
@@ -83,7 +83,7 @@ public abstract class FallbackSessionHandler implements FallbackPacketListener {
user.disconnect(Sonar.get().getConfig().getVerification().getVerificationSuccess());
}
Sonar.get().getFallback().getLogger().info(
Sonar.get().getLogger().info(
Sonar.get().getConfig().getMessagesConfig().getString("verification.logs.successful")
.replace("<username>", username)
.replace("<time-taken>", user.getLoginTimer().toString()));
@@ -148,12 +148,18 @@ public final class FallbackVehicleSessionHandler extends FallbackSessionHandler
// Don't ask me why; Microsoft is doing some *fascinating* things with Bedrock...
if (user.isGeyser()) {
handleRotation();
} else {
checkState(rotationPackets >= inputPackets,
"illegal packet order; r/i " + rotationPackets + "/" + inputPackets);
}
// 1.8 and below do not have PaddleBoat packets,
// so we simply exempt them from the PaddleBoat check.
if (user.getProtocolVersion().compareTo(MINECRAFT_1_9) < 0) {
paddlePackets++;
} else {
checkState(paddlePackets >= inputPackets,
"illegal packet order; i/p " + inputPackets + "/" + paddlePackets);
}
inputPackets++;
}
@@ -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"),
@@ -22,7 +22,6 @@ import com.google.gson.JsonParser;
import lombok.experimental.UtilityClass;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.logger.LoggerWrapper;
import javax.net.ssl.HttpsURLConnection;
import java.io.BufferedReader;
@@ -37,24 +36,6 @@ import java.util.concurrent.Executors;
public class UpdateChecker {
private final ExecutorService ASYNC_EXECUTOR = Executors.newSingleThreadExecutor();
private final LoggerWrapper LOGGER = new LoggerWrapper() {
@Override
public void info(final String message, final Object... args) {
Sonar.get().getLogger().info("[update-checker] " + message, args);
}
@Override
public void warn(final String message, final Object... args) {
Sonar.get().getLogger().warn("[update-checker] " + message, args);
}
@Override
public void error(final String message, final Object... args) {
Sonar.get().getLogger().error("[update-checker] " + message, args);
}
};
public void checkForUpdates() {
ASYNC_EXECUTOR.execute(() -> {
try {
@@ -66,17 +47,17 @@ public class UpdateChecker {
final int convertedCurrentVersion = convertVersion(Sonar.get().getVersion().getVersion());
if (convertedCurrentVersion < convertedLatestVersion) {
LOGGER.warn("A new version of Sonar is available: {}", latestStableRelease);
LOGGER.warn("Please make sure to update to the latest version to ensure stability and security:");
LOGGER.warn("https://github.com/jonesdevelopment/sonar/releases/tag/{}", latestStableRelease);
Sonar.get().getLogger().warn("A new version of Sonar is available: {}", latestStableRelease);
Sonar.get().getLogger().warn("Please make sure to update to the latest version to ensure stability and security:");
Sonar.get().getLogger().warn("https://github.com/jonesdevelopment/sonar/releases/tag/{}", latestStableRelease);
} else if (convertedCurrentVersion > convertedLatestVersion || !Sonar.get().getVersion().getGitBranch().equals("main")) {
LOGGER.warn("You are currently using an unreleased version of Sonar!");
LOGGER.warn("The contributors of Sonar are not responsible for any damage done by using an unstable version");
Sonar.get().getLogger().warn("You are currently using an unreleased version of Sonar!");
Sonar.get().getLogger().warn("The contributors of Sonar are not responsible for any damage done by using an unstable version");
} else {
LOGGER.info("You are currently using the latest stable release of Sonar!");
Sonar.get().getLogger().info("You are currently using the latest stable release of Sonar!");
}
} catch (Throwable throwable) {
LOGGER.warn("Unable to retrieve version information: {}", throwable);
Sonar.get().getLogger().warn("Unable to retrieve version information: {}", throwable);
}
});
}
@@ -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,12 +57,24 @@ 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";
private static final int[] VAR_INT_LENGTHS = new int[65];
static {
for (int i = 0; i <= 32; ++i) {
VAR_INT_LENGTHS[i] = (int) Math.ceil((31d - (i - 1)) / 7d);
}
VAR_INT_LENGTHS[32] = 1;
}
public static int varIntBytes(final int value) {
return VAR_INT_LENGTHS[Integer.numberOfLeadingZeros(value)];
}
public static void checkNettyVersion() {
final Version version = Version.identify().getOrDefault("netty-all", Version.identify().get("netty-common"));
@@ -82,25 +95,29 @@ public class ProtocolUtil {
}
}
public static int readVarInt(final ByteBuf byteBuf) {
int read = readVarIntSafely(byteBuf);
if (read == Integer.MIN_VALUE) {
throw new CorruptedFrameException("Corrupt VarInt");
public static int readVarInt(final @NotNull ByteBuf byteBuf) {
final int readable = byteBuf.readableBytes();
if (readable == 0) {
throw DEBUG ? new DecoderException("Empty buffer") : QuietDecoderException.INSTANCE;
}
return read;
}
public static int readVarIntSafely(final @NotNull ByteBuf byteBuf) {
int i = 0;
int maxRead = Math.min(5, byteBuf.readableBytes());
for (int j = 0; j < maxRead; j++) {
int k = byteBuf.readByte();
// We can read at least one byte, and this should be a common case
int k = byteBuf.readByte();
if ((k & 0x80) != 128) {
return k;
}
// In case decoding one byte was not enough, use a loop to decode up to the next 4 bytes
final int maxRead = Math.min(5, readable);
int i = k & 0x7F;
for (int j = 1; j < maxRead; j++) {
k = byteBuf.readByte();
i |= (k & 0x7F) << j * 7;
if ((k & 0x80) != 128) {
return i;
}
}
return Integer.MIN_VALUE;
throw DEBUG ? new DecoderException("Bad VarInt") : QuietDecoderException.INSTANCE;
}
public static void writeVarInt(final ByteBuf byteBuf, final int value) {
@@ -138,19 +155,6 @@ public class ProtocolUtil {
}
}
private static final int[] VARINT_EXACT_BYTE_LENGTHS = new int[33];
static {
for (int i = 0; i <= 32; ++i) {
VARINT_EXACT_BYTE_LENGTHS[i] = (int) Math.ceil((31d - (i - 1)) / 7d);
}
VARINT_EXACT_BYTE_LENGTHS[32] = 1;
}
public static int varIntBytes(final int value) {
return VARINT_EXACT_BYTE_LENGTHS[Integer.numberOfLeadingZeros(value)];
}
public static void writeVarLong(final ByteBuf byteBuf, final long value) {
// Peel the one and two byte count cases explicitly as they are the most common VarLong sizes
// that the proxy will write, to improve inlining.
@@ -224,33 +228,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 +277,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 +289,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 +318,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 +366,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;
}
}
@@ -10,7 +10,8 @@
#
# Potřebujete pomoc nebo máte otázky? https://jonesdev.xyz/discord
# Zdrojový kód: https://github.com/jonesdevelopment/sonar
# Copyright Jones Development a Sonar Contributors, licencováno pod GPL-3.0
# Dokumentace: https://docs.jonesdev.xyz/administration/configuring-sonar
# Copyright Jones Development and Sonar Contributors, licencováno pod GPL-3.0
#
# Obecné nastavení pro Sonar
@@ -139,9 +140,6 @@ verification:
valid-name-regex: ^[a-zA-Z0-9_]+$
# Regex pro validaci "client locale" při verifikaci
valid-locale-regex: ^[a-zA-Z_]+$
# Maximální odezva hráče aby byl odpojen
# (Hodnota udávaná v milisekundách: 1 sekunda = 1000 milisekund)
max-ping: 10000
# Maximální počet login packetů, který musí hřáč poslat aby byl vyhozen
max-login-packets: 256
@@ -155,6 +153,9 @@ verification:
# Kromě toho možná budete chtít snížit množství omezení rychlosti přihlášení
# provádí Velocity nebo jiné proxy/pluginy, protože to může zabránit
# hráči správné přenesení.
# Pokud váš server umožňuje hráčům Bedrocku připojit se přes Geyser, nezapomeňte nastavit.
# TransferTool pro Geyser, aby byli hráči Bedrocku přesměrováni na správný server.
# https://github.com/onebeastchris/TransferTool
enabled: false
# Na který server by měl Sonar přenést hráče, když projde ověřením?
# Zadejte IP serveru, který hráči používají k běžnému připojení k vašemu serveru.
@@ -10,7 +10,8 @@
#
# Brauchst du Hilfe? https://jonesdev.xyz/discord
# Source code: https://github.com/jonesdevelopment/sonar
# Copyright Jones Development and Sonar Contributors, licensed under GPL-3.0
# Dokumentation: https://docs.jonesdev.xyz/administration/configuring-sonar
# Copyright Jones Development and Sonar Contributors, lizenziert unter GPL-3.0
#
# Generelle Einstellungen für Sonar
@@ -136,9 +137,6 @@ verification:
valid-name-regex: ^[a-zA-Z0-9_]+$
# Regex zur Validierung der Client-Spracheinstellung während der Überprüfung
valid-locale-regex: ^[a-zA-Z_]+$
# Ping, den ein Spieler haben muss, um ein Timeout zu erleiden
# (Dieser Wert stellt die Zeit in Millisekunden dar: 1 Sekunde = 1000 Millisekunden)
max-ping: 10000
# Maximale Anzahl an Login-Paketen, die der Spieler senden muss, um gekickt zu werden
max-login-packets: 256
@@ -152,6 +150,9 @@ verification:
# Darüber hinaus möchten Sie möglicherweise die Begrenzung der Anmelderate reduzieren
# wird von Velocity oder anderen Proxys/Plugins durchgeführt, da dies das verhindern könnte
# Spieler konnte nicht korrekt übertragen werden.
# Wenn Ihr Server es erlaubt, dass Bedrock-Spieler über Geyser beitreten können, stellen Sie sicher, dass Sie das
# TransferTool für Geyser einrichten, damit Bedrock-Spieler auf den richtigen Server umgeleitet werden.
# https://github.com/onebeastchris/TransferTool
enabled: false
# Auf welchen Server soll Sonar den Spieler übertragen, wenn die Verifizierung bestanden wurde?
# Bitte geben Sie die Server-IP ein, die von Spielern verwendet wird, um sich zu verbinden.
@@ -10,6 +10,7 @@
#
# Need help or have questions? https://jonesdev.xyz/discord
# Source code: https://github.com/jonesdevelopment/sonar
# Documentation: https://docs.jonesdev.xyz/administration/configuring-sonar
# Copyright Jones Development and Sonar Contributors, licensed under GPL-3.0
#
@@ -62,7 +63,7 @@ database:
# Every new login request will be queued to avoid spam join attacks
# The queue is updated every 1 second (20 ticks)
queue:
# Maximum number of concurrent queue polls per 1 second
# Number of concurrent queue polls per 1 second
max-polls: 30
# Every new player that joins for the first time will be sent to
@@ -82,7 +83,7 @@ verification:
enabled: true
# Should Sonar make the player solve a CAPTCHA if they fail the gravity check?
captcha-on-fail: false
# Maximum number of ticks the player has to fall to be allowed to hit the platform
# The number of ticks the player has to fall to be allowed to hit the platform
max-movement-ticks: 8
# Checks if the player is colliding with blocks spawned below them
@@ -110,8 +111,8 @@ verification:
# - NEVER: New players will never receive a CAPTCHA (Recommended)
timing: NEVER
# Path to the background image Sonar uses for the CAPTCHA
# If the image isn't found, a noise will be used as the default background
# You can put the name of the file here if the file is in the plugin directory
# If the image isn't found, a noise will be used as the default background.
# You can put the name of the file here if the file is in Sonar's plugin directory
background: ''
# How many CAPTCHA answers should Sonar prepare?
# This task happens asynchronously in the background
@@ -139,10 +140,7 @@ verification:
valid-name-regex: ^[a-zA-Z0-9_]+$
# Regex for validating client locale during verification
valid-locale-regex: ^[a-zA-Z_]+$
# Ping a player has to have to timeout
# (This value represents the time in milliseconds: 1 second = 1000 milliseconds)
max-ping: 10000
# Maximum number of login packets the player has to send in order to be kicked
# The number of login packets the player has to send in order to be kicked
max-login-packets: 256
# If enabled, the player will be transferred back to the origin server
@@ -155,6 +153,9 @@ verification:
# Additionally, you might want to reduce the amount of login rate-limiting
# performed by Velocity or other proxies/plugins, as this might prevent the
# player from being transferred correctly.
# If your server allows Bedrock players to join via Geyser, make sure to set up
# TransferTool for Geyser, so Bedrock players are redirected to the correct server.
# https://github.com/onebeastchris/TransferTool
enabled: false
# Which server should Sonar transfer the player to when the verification is passed?
# Please enter the server IP used by players to normally connect to your server.
@@ -10,7 +10,8 @@
#
# Besoin d'aide ou d'autres questions ? https://jonesdev.xyz/discord
# Code source : https://github.com/jonesdevelopment/sonar
# Droit d'auteur Jones Development, un contributeur Sonar, licence sous GPL-3.0
# Documentation: https://docs.jonesdev.xyz/administration/configuring-sonar
# Copyright Jones Development and Sonar Contributors, licence sous GPL-3.0
#
# Paramètre général de Sonar
@@ -138,9 +139,6 @@ verification:
valid-name-regex: ^[a-zA-Z0-9_]+$
# Regex pour valider la locale client lors de la vérification
valid-locale-regex: ^[a-zA-Z_]+$
# Le ping qu'un joueur doit avoir pour dépasser le délai d'attente
# (Cette valeur représente le temps en millisecondes : 1 seconde = 1000 millisecondes)
max-ping: 10000
# Nombre maximal de paquets de connexion que le joueur doit envoyer pour être expulsé
max-login-packets: 256
@@ -154,6 +152,9 @@ verification:
# De plus, vous voudrez peut-être réduire la quantité de limitation de débit de connexion
# effectuée par Velocity ou d'autres proxys/plugins, car cela pourrait empêcher le
# joueur d'être transféré correctement.
# Si votre serveur permet aux joueurs de Bedrock de s'inscrire via Geyser, assurez-vous de configurer
# TransferTool pour Geyser, afin que les joueurs Bedrock soient redirigés vers le bon serveur.
# https://github.com/onebeastchris/TransferTool
enabled: false
# Quel serveur Sonar devrait-il utiliser pour transférer le joueur une fois la vérification réussie ?
# Veuillez entrer l'adresse IP du serveur que les joueurs utilisent normalement pour se connecter à votre serveur.
@@ -10,6 +10,7 @@
#
# დახმარება გჭირდება ან კითხვა გაქვს? https://jonesdev.xyz/discord
# საწყისი კოდი: https://github.com/jonesdevelopment/sonar
# Documentation: https://docs.jonesdev.xyz/administration/configuring-sonar
# Copyright Jones Development and Sonar Contributors, licensed under GPL-3.0
#
@@ -138,9 +139,6 @@ verification:
valid-name-regex: ^[a-zA-Z0-9_]+$
# Regex-ი კლიენტის ადგილის დასადასტურებლად ვერიფიკაციის დროს
valid-locale-regex: ^[a-zA-Z_]+$
# პინგი რამდენიც უნდა ქონდეს მოთამაშეს იმისთვის, რომ დროებით დაიბლოკოს (გავარდეს)
# (ეს არის ნაჩვენები მილიწამებში: 1 წამი = 1000 მილიწამი)
max-ping: 10000
# მაქსიმალური რაოდენობა შესვლის (LOGIN) პაკეტების, რომელიც მოთამაშემ უნდა გააგზავნოს სანამ გავარდება სერვერიდან
max-login-packets: 256
@@ -153,6 +151,9 @@ verification:
# დამატებით, შეიძლება გინდოდეთ შეამციროთ შესვლის სიხშირის ლიმიტი
# რომელსაც ახორციელებს Velocity ან სხვა პროქსიები/პლაგინები, რადგან ეს შეიძლება ხელს უშლიდეს
# მოთამაშის სწორად გადაყვანას
# If your server allows Bedrock players to join via Geyser, make sure to set up
# TransferTool for Geyser, so Bedrock players are redirected to the correct server.
# https://github.com/onebeastchris/TransferTool
enabled: false
# რომელ სერვერზე უნდა გადაიყვანოს Sonar-მა მოთამაშე ვერიფიკაციის გავლის შემდეგ?
# გთხოვთ, შეიყვანოთ სერვერის IP მისამართი, რომლითაც მოთამაშეები ჩვეულებრივ უერთდებიან თქვენს სერვერს
@@ -10,7 +10,8 @@
#
# Hulp nodig of vragen? https://jonesdev.xyz/discord
# Broncode: https://github.com/jonesdevelopment/sonar
# Copyright Jones Development and Sonar Contributors, licensed under GPL-3.0
# Documentatie: https://docs.jonesdev.xyz/administration/configuring-sonar
# Copyright Jones Development and Sonar Contributors, gelicentieerd onder GPL-3.0
#
# Algemene instellingen voor Sonar
@@ -138,9 +139,6 @@ verification:
valid-name-regex: ^[a-zA-Z0-9_]+$
# Regex voor het valideren van de landinstelling van de client tijdens verificatie
valid-locale-regex: ^[a-zA-Z_]+$
# Ping moet een speler hebben om een time-out te krijgen
# (Deze waarde vertegenwoordigt de tijd in milliseconden: 1 seconde = 1000 milliseconden)
max-ping: 10000
# Maximaal aantal loginpakketten dat de speler moet verzenden om te worden geschopt
max-login-packets: 256
@@ -154,6 +152,9 @@ verification:
# Bovendien wilt u mogelijk de beperking van de inlegsnelheid verminderen
# uitgevoerd door Velocity van andere proxy's/plugins, omdat dit de
# speler wordt niet correct weergegeven.
# Als je server toestaat dat Bedrock spelers meedoen via Geyser, zorg er dan voor dat je
# TransferTool voor Geyser, zodat Bedrock spelers worden doorgestuurd naar de juiste server.
# https://github.com/onebeastchris/TransferTool
enabled: false
# Naar welke server moet Sonar de speler overbrengen wanneer de verificatie is geslaagd?
# Voer het server-IP in dat door spelers wordt gebruikt om normaal verbinding te maken met uw server.
@@ -10,7 +10,8 @@
#
# Potrzebujesz pomocy lub masz pytania? https://jonesdev.xyz/discord
# Source code: https://github.com/jonesdevelopment/sonar
# Copyright Jones Development and Sonar Contributors, licensed under GPL-3.0
# Dokumentacja: https://docs.jonesdev.xyz/administration/configuring-sonar
# Copyright Jones Development and Sonar Contributors, na licencji GPL-3.0
#
# Ustawienia ogólne Sonar
@@ -138,9 +139,6 @@ verification:
valid-name-regex: ^[a-zA-Z0-9_]+$
# Regex do sprawdzania ustawień regionalnych klienta podczas weryfikacji
valid-locale-regex: ^[a-zA-Z_]+$
# Maksymalny ping gracza, zanim zostanie wyrzucony z serwera.
# (Wartość ta reprezentuje czas w milisekundach: 1 sekunda = 1000 milisekund)
max-ping: 10000
# Liczba pakietów logowania, które gracz musi wysłać, aby zostać wyrzuconym.
max-login-packets: 256
@@ -154,6 +152,9 @@ verification:
# Dodatkowo, możesz chcieć zmniejszyć limitowanie prędkości logowania (ratelimit).
# wykonywane przez Velocity lub inne silniki/pluginy, ponieważ
# może to uniemożliwić prawidłowe przeniesienie gracza.
# Jeśli twój serwer pozwala graczom Bedrock na dołączenie przez Geyser, upewnij się, że skonfigurowałeś
# TransferTool dla Geyser, aby gracze Bedrock byli przekierowywani na właściwy serwer.
# https://github.com/onebeastchris/TransferTool
enabled: false
# Na który serwer Sonar powinien przenieść gracza po pomyślnym przejściu weryfikacji?
# Wprowadź adres IP serwera, który jest używany przez graczy do normalnego łączenia się z serwerem.
@@ -10,7 +10,8 @@
#
# Нужна помощь или есть вопросы? https://jonesdev.xyz/discord
# Исходный код: https://github.com/jonesdevelopment/sonar
# Copyright Jones Development and Sonar Contributors, licensed under GPL-3.0
# Документация: https://docs.jonesdev.xyz/administration/configuring-sonar
# Copyright Jones Development and Sonar Contributors, под лицензией GPL-3.0
#
# Основные настройки
@@ -137,9 +138,6 @@ verification:
valid-name-regex: ^[a-zA-Z0-9_]+$
# Regex для проверки локализации клиента при верификации
valid-locale-regex: ^[a-zA-Z_]+$
# Пинг который должен быть у игрока чтобы не пройти проверку
# (Это значение представляет собой время в миллисекундах: 1 секунда = 1000 миллисекунд)
max-ping: 10000
# Максимальное количество пакетов для входа на сервер, которое игрок должен отправить, чтобы быть выгнанным
max-login-packets: 256
@@ -153,6 +151,9 @@ verification:
# Кроме того, вы можете пожелать снизить количество ограничений на скорость входа
# выполняемых Velocity или другими прокси/плагинами, так как это может помешать
# корректному перемещению игрока.
# Если ваш сервер позволяет игрокам Bedrock присоединяться через Geyser, убедитесь, что вы настроили
# TransferTool для Geyser, чтобы игроки Bedrock перенаправлялись на правильный сервер.
# https://github.com/onebeastchris/TransferTool
enabled: false
# На какой сервер должен Sonar переместить игрока после прохождения проверки?
# Введите IP-адрес сервера, который обычно используется игроками для подключения к вашему серверу.
@@ -10,7 +10,8 @@
#
# 需要帮助吗? https://jonesdev.xyz/discord
# 源代码: https://github.com/jonesdevelopment/sonar
# Copyright Jones Development and Sonar Contributors, licensed under GPL-3.0
# 文件: https://docs.jonesdev.xyz/administration/configuring-sonar
# Copyright Jones Development and Sonar Contributors, 以 GPL-3.0 许可
#
# 未归类的设置
@@ -135,8 +136,6 @@ verification:
valid-name-regex: ^[a-zA-Z0-9_]+$
# 用于检查客户端语言(例如 zh_CN)的正则表达式
valid-locale-regex: ^[a-zA-Z_]+$
# 玩家必须在多少毫秒内回复Ping?
max-ping: 10000
# 玩家最多能发送多少个数据包用于登录?
max-login-packets: 256
@@ -148,6 +147,9 @@ verification:
# 为此您必须在服务器软件的配置文件中允许接受来自transfer的连接.
# 此外 您可能希望减少 Velocity 或其它服务器软件/插件的登录速率限制
# 因为这可能会阻止玩家快速地重新连接到服务器
# 如果你的服务器允许 Bedrock 玩家通过间歇泉加入, 请确保为间歇泉设置了
# 传输工具 TransferTool, 以便将 Bedrock 玩家重定向到正确的服务器.
# https://github.com/onebeastchris/TransferTool
enabled: false
# 当玩家通过验证后, Sonar 应该将玩家转移到哪个服务器
# 请输入玩家正常连接服务器时使用的服务器IP或域名
@@ -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
@@ -10,7 +10,7 @@
#
# Brauchst du Hilfe? https://jonesdev.xyz/discord
# Source code: https://github.com/jonesdevelopment/sonar
# Copyright Jones Development and Sonar Contributors, licensed under GPL-3.0
# Copyright Jones Development and Sonar Contributors, lizenziert unter GPL-3.0
#
# Platzhalter für jedes '<prefix>' in dieser Konfigurationsdatei
@@ -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
@@ -10,7 +10,7 @@
#
# Besoin d'aide ou d'autres questions ? https://jonesdev.xyz/discord
# Code source : https://github.com/jonesdevelopment/sonar
# Droit d'auteur Jones Development, un contributeur Sonar, licence sous GPL-3.0
# Copyright Jones Development and Sonar Contributors, licence sous GPL-3.0
#
# Espace réservé pour tout '<prefix>' dans ce fichier de configuration
@@ -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
@@ -10,7 +10,7 @@
#
# Butuh bantuan atau memiliki pertanyaan? https://jonesdev.xyz/discord
# Kode sumber: https://github.com/jonesdevelopment/sonar
# Hak cipta Jones Development dan Kontributor Sonar, dilisensikan di bawah GPL-3.0
# Copyright Jones Development and Sonar Contributors, dilisensikan di bawah GPL-3.0
#
# Placeholder untuk setiap '<prefix>' dalam file konfigurasi ini
@@ -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
Loaded 100 of 117 files, more files were not shown because too many files have changed in this diff. Show more