Compare commits

...
34 Commits
Author SHA1 Message Date
Jones 59731a93f1 Release 2.1.7 (#381) 2024-09-15 14:09:43 +02:00
Jones ace55fbe7b Fix a small compilation error 2024-09-15 14:08:18 +02:00
Jones 696d3c5c81 Fix some Bedrock false positives 2024-09-15 14:03:00 +02:00
Jones a1e4ea3f5b Fix exception while serializing JsonArray (https://github.com/PaperMC/Velocity/pull/1426) 2024-09-14 15:45:19 +02:00
Jones 6b054bd56c Fix invalid block mappings for 1.14-1.14.3 2024-09-14 10:35:51 +02:00
Jones c8e2142dd9 Cap username length in LoginSuccess (clientbound) 2024-09-12 18:20:11 +02:00
Jones 1cf7ef6994 Add invalid window ID check to transactions 2024-09-11 21:14:08 +02:00
Jones af8aa103bf Randomize some more position-related values 2024-09-11 19:31:31 +02:00
Jones d1f2c15a8f Fix a false positive with the moved too far check 2024-09-11 19:28:29 +02:00
Jones 5a68c4d557 Added another vehicle Y position check 2024-09-11 19:23:58 +02:00
Jones 1438856531 Release 2.1.6 (#379) 2024-09-11 16:36:00 +02:00
AkkariinandJones 694725b798 Implement more interactionless checks (#378)
* Add minecart entity

* Add `VehicleMovePacket`

* Add `Animation` packet

* Add animation check.

* Fix the wrong entity minecart id for 1.21

* Add minecart for vehicle check.

* Exempt minecart check for 1.7-1.8 clients.

* Fix minecart id for lower than minecraft version 1.14.

* Fix wrong minecart entity id for 1.15

* Add missing license header

* Exempt animation check for 1.7.2 client due unknown disconnect error.

* style: small code style improvements

* style: even more style improvements

* refactor: inline some stuff

* style: small code cleanup

* fix: slot check not working in the first place

* style: don't use return; use an if-else block

* Fix animation check not working on 1.7.2+

* style: minor code style improvements

* fix: lombok compile error

* style: minor code style adjustments

* fix: optimize AnimationPacket

* fix: don't reassign unnecessary hand value

* style: code style fixes in *types

* style: add some comments to the *types

* [ci skip] style: small code style improvements

* Don't exempt 1.7-1.8 from the Minecart check

---------

Co-authored-by: Jones <admin@jonesdev.xyz>
2024-09-11 16:29:30 +02:00
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
88 changed files with 1981 additions and 1850 deletions

No files matched your search

+1 -1
View File
@@ -1,3 +1,3 @@
group=xyz.jonesdev.sonar
version=2.1.4
version=2.1.7
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,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);
@@ -30,7 +30,6 @@ import xyz.jonesdev.sonar.api.command.SonarCommand;
import xyz.jonesdev.sonar.api.database.ormlite.H2DatabaseTypeAdapter;
import xyz.jonesdev.sonar.api.database.ormlite.MariaDbDatabaseTypeAdapter;
import xyz.jonesdev.sonar.api.database.ormlite.MysqlDatabaseTypeAdapter;
import xyz.jonesdev.sonar.api.logger.LoggerWrapper;
import xyz.jonesdev.sonar.api.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");
@@ -216,7 +197,6 @@ public final class SonarConfiguration {
verification.gamemode = Verification.Gamemode.valueOf(generalConfig.getString("verification.gamemode"));
verification.validNameRegex = Pattern.compile(generalConfig.getString("verification.checks.valid-name-regex"));
verification.validLocaleRegex = Pattern.compile(generalConfig.getString("verification.checks.valid-locale-regex"));
verification.maxLoginPackets = clamp(generalConfig.getInt("verification.checks.max-login-packets"), 128, 8192);
verification.checkGeyser = generalConfig.getBoolean("verification.check-geyser-players");
verification.logConnections = generalConfig.getBoolean("verification.log-connections");
@@ -303,7 +283,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;
@@ -434,7 +414,6 @@ public final class SonarConfiguration {
private Pattern validNameRegex;
private Pattern validLocaleRegex;
private int maxLoginPackets;
private int readTimeout;
private int writeTimeout;
private int reconnectDelay;
@@ -489,7 +468,7 @@ public final class SonarConfiguration {
private final DatabaseType databaseType;
private final Library databaseDriver;
@Setter
private boolean downloaded;
private boolean loaded;
}
private Type type;
@@ -20,7 +20,7 @@ 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;
@@ -28,44 +28,27 @@ import lombok.Getter;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.Range;
import org.jetbrains.annotations.Unmodifiable;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.config.SonarConfiguration;
import xyz.jonesdev.sonar.api.database.model.VerifiedPlayer;
import xyz.jonesdev.sonar.api.logger.LoggerWrapper;
import java.io.File;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ExecutorService;
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);
@Getter
private final Set<String> cache = Collections.synchronizedSet(new HashSet<>());
private @Nullable ConnectionSource connectionSource;
private Dao<VerifiedPlayer, Integer> dao;
private QueryBuilder<VerifiedPlayer, Integer> queryBuilder;
@@ -77,20 +60,18 @@ public final class VerifiedPlayerController {
cachedDatabaseType = database.getType();
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);
}
cachedDatabaseType.getDatabaseType().loadDriver();
try {
final String jdbcURL;
@@ -109,7 +90,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());
@@ -118,6 +99,7 @@ public final class VerifiedPlayerController {
try {
TableUtils.createTableIfNotExists(connectionSource, VerifiedPlayer.class);
} catch (SQLException ignored) {
ignored.printStackTrace();
/*
* This is caused by a duplicate index;
* I know this isn't the best method of handling it,
@@ -135,13 +117,13 @@ public final class VerifiedPlayerController {
// Make sure to clear all outdated entries first
clearOld(database.getMaximumAge());
// Add all entries from the database to the cache
dao.queryForAll().forEach(this::_add);
dao.queryForAll().forEach(verifiedPlayer -> cache.add(verifiedPlayer.getFingerprint()));
} catch (SQLException exception) {
LOGGER.error("Error initializing database: {}", exception);
exception.printStackTrace(System.err);
}
});
} catch (SQLException exception) {
LOGGER.error("Error setting up database: {}", exception);
exception.printStackTrace(System.err);
}
}
@@ -154,7 +136,7 @@ public final class VerifiedPlayerController {
try {
connectionSource.close();
} catch (Exception exception) {
LOGGER.error("Error closing database: {}", exception);
exception.printStackTrace(System.err);
}
}
}
@@ -171,11 +153,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);
}
}
@@ -184,10 +166,10 @@ public final class VerifiedPlayerController {
* First, remove the player from the local cache and then,
* secondly, asynchronously add the player to the database.
*
* @param inetAddress InetAddress of the VerifiedPlayer model
* @param fingerprint Fingerprint of the verified player
*/
public void remove(final @NotNull String inetAddress) {
_remove(inetAddress);
public void remove(final @NotNull String fingerprint) {
cache.remove(fingerprint);
// Don't try to update the column if the database type is NONE
if (cachedDatabaseType == SonarConfiguration.Database.Type.NONE) {
@@ -203,7 +185,7 @@ public final class VerifiedPlayerController {
try {
final List<VerifiedPlayer> verifiedPlayer = queryBuilder.where()
.eq("ip_address", inetAddress)
.eq("fingerprint", fingerprint)
.query();
if (verifiedPlayer != null) {
@@ -212,20 +194,11 @@ public final class VerifiedPlayerController {
}
}
} catch (SQLException exception) {
LOGGER.error("Error trying to remove entry: {}", exception);
exception.printStackTrace(System.err);
}
});
}
/**
* Locally remove the object from the cache
*
* @param inetAddress IP address of the player
*/
private void _remove(final @NotNull String inetAddress) {
cache.remove(inetAddress);
}
/**
* First, cache the player locally and then,
* secondly, asynchronously add the player to the database.
@@ -233,7 +206,7 @@ public final class VerifiedPlayerController {
* @param player VerifiedPlayer model
*/
public void add(final @NotNull VerifiedPlayer player) {
_add(player);
cache.add(player.getFingerprint());
// Don't try to update the column if the database type is NONE
if (cachedDatabaseType == SonarConfiguration.Database.Type.NONE) {
@@ -250,21 +223,11 @@ public final class VerifiedPlayerController {
try {
dao.createIfNotExists(player);
} catch (SQLException exception) {
LOGGER.error("Error trying to add entry: {}", exception);
exception.printStackTrace(System.err);
}
});
}
/**
* Locally cache the object
*
* @param player VerifiedPlayer model
*/
private void _add(final @NotNull VerifiedPlayer player) {
cache.computeIfAbsent(player.getInetAddress(), v -> new ArrayList<>())
.add(player.getPlayerUuid());
}
/**
* Returns the number of verified IP addresses
*
@@ -274,26 +237,6 @@ public final class VerifiedPlayerController {
return cache.size();
}
/**
* Returns the sum of verified IP addresses
* and their respective collection of UUIDs
*
* @return Exact size of all total verified players
*/
@Deprecated
public synchronized int exactSize() {
return cache.values().stream()
.mapToInt(Collection::size)
.sum();
}
/**
* @return {@link java.util.Collection} of UUIDs associated with an IP address
*/
public @Unmodifiable Collection<UUID> getUUIDs(final @NotNull String inetAddress) {
return cache.getOrDefault(inetAddress, Collections.emptyList());
}
/**
* Clear the local cache, and, if the database type is set,
* remove the table from the database.
@@ -306,29 +249,8 @@ public final class VerifiedPlayerController {
try {
dao.deleteBuilder().delete();
} catch (SQLException exception) {
LOGGER.error("Error trying to clear entries: {}", exception);
exception.printStackTrace(System.err);
}
}
}
/**
* @param inetAddress IP address
* @param uuid UUID associated to the IP
* @return Whether the local cache contains the IP and UUID
*/
public boolean has(final @NotNull String inetAddress, final @NotNull UUID uuid) {
final Collection<UUID> got = cache.get(inetAddress);
if (got != null) {
return got.contains(uuid);
}
return false;
}
/**
* @param inetAddress IP address
* @return Whether the local cache contains the IP
*/
public boolean has(final @NotNull String inetAddress) {
return cache.containsKey(inetAddress);
}
}
@@ -25,34 +25,23 @@ import lombok.NoArgsConstructor;
import lombok.ToString;
import org.jetbrains.annotations.NotNull;
import java.net.InetAddress;
import java.sql.Timestamp;
import java.util.UUID;
@Getter
@ToString
@DatabaseTable(tableName = "sonar_fingerprints")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@DatabaseTable(tableName = "sonar_verified_players")
public final class VerifiedPlayer {
@SuppressWarnings("unused")
@DatabaseField(generatedId = true)
private int id;
@DatabaseField(
columnName = "ip_address",
columnName = "fingerprint",
canBeNull = false,
uniqueIndexName = "ip_address_player_uuid_idx",
width = 16
width = 48
)
private String inetAddress;
@DatabaseField(
columnName = "player_uuid",
canBeNull = false,
uniqueIndexName = "ip_address_player_uuid_idx",
width = 36
)
private UUID playerUuid;
private String fingerprint;
@DatabaseField(
columnName = "timestamp",
@@ -60,17 +49,9 @@ public final class VerifiedPlayer {
)
private Timestamp timestamp;
public VerifiedPlayer(final @NotNull InetAddress inetAddress,
final @NotNull UUID playerUuid,
public VerifiedPlayer(final @NotNull String fingerprint,
final long timestamp) {
this(inetAddress.getHostAddress(), playerUuid, timestamp);
}
public VerifiedPlayer(final @NotNull String inetAddress,
final @NotNull UUID playerUuid,
final long timestamp) {
this.inetAddress = inetAddress;
this.playerUuid = playerUuid;
this.fingerprint = fingerprint;
this.timestamp = new Timestamp(timestamp);
}
}
@@ -22,15 +22,12 @@ import lombok.RequiredArgsConstructor;
import lombok.ToString;
import xyz.jonesdev.sonar.api.event.SonarEvent;
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
import java.util.UUID;
import xyz.jonesdev.sonar.api.timer.SystemTimer;
@Getter
@ToString
@RequiredArgsConstructor
public final class UserVerifySuccessEvent implements SonarEvent {
private final String username;
private final UUID offlineUuid;
private final FallbackUser user;
private final long timeTakenToVerify;
private final SystemTimer loginTimer;
}
@@ -27,7 +27,6 @@ import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.config.SonarConfiguration;
import xyz.jonesdev.sonar.api.fallback.captcha.CaptchaGenerator;
import xyz.jonesdev.sonar.api.fallback.ratelimit.Ratelimiter;
import xyz.jonesdev.sonar.api.logger.LoggerWrapper;
import java.net.InetAddress;
import java.util.concurrent.ConcurrentHashMap;
@@ -41,7 +40,9 @@ public final class Fallback {
// Map of all players connected to the server in general
private final ConcurrentMap<InetAddress, Integer> online = new ConcurrentHashMap<>(128);
// Map of all connected IP addresses (used for fast checking)
private final ConcurrentMap<InetAddress, Byte> connected = new ConcurrentHashMap<>(128);
// TODO: Is there a way to improve this?
// (E.g. no map needed without concurrency issues)
private final ConcurrentMap<InetAddress, Byte> connected = new ConcurrentHashMap<>(256);
// Cache of all blacklisted IP addresses to ensure each entry can expire after the given time
@Setter
private Cache<String, Integer> blacklist;
@@ -54,24 +55,6 @@ public final class Fallback {
@Setter
private Ratelimiter<InetAddress> ratelimiter;
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);
}
};
public boolean shouldVerifyNewPlayers() {
return shouldPerform(Sonar.get().getConfig().getVerification().getTiming());
}
@@ -26,7 +26,6 @@ import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.api.timer.SystemTimer;
import java.net.InetAddress;
import java.util.UUID;
public interface FallbackUser {
@NotNull
@@ -45,15 +44,14 @@ public interface FallbackUser {
SystemTimer getLoginTimer();
@NotNull
UUID getOfflineUuid();
String getFingerprint();
boolean isReceivedClientSettings();
@NotNull
String getUsername();
void setReceivedClientSettings(final boolean receivedClientSettings);
boolean isForceCaptcha();
boolean isReceivedPluginMessage();
void setReceivedPluginMessage(final boolean receivedPluginMessage);
void setForceCaptcha(final boolean isForceCaptcha);
boolean isGeyser();
@@ -65,14 +63,6 @@ public interface FallbackUser {
*/
void disconnect(final @NotNull Component reason);
/**
* Takes over the channel and begins the verification process
*
* @param username Username of the player
* @param offlineUuid UUID of the player
*/
void hijack(final @NotNull String username, final @NotNull UUID offlineUuid);
/**
* Sends a packet/message to the player
*
@@ -97,16 +87,4 @@ public interface FallbackUser {
ReferenceCountUtil.release(msg);
}
}
/**
* Disconnects the player who failed the verification
* and caches them in FAILED_VERIFICATIONS.
* If the player fails the verification twice,
* the player will be temporarily denied from verifying.
*
* @param reason Reason for failing the verification
* @apiNote The {@link xyz.jonesdev.sonar.api.event.impl.UserVerifyFailedEvent}
* will not be thrown if no reason is given
*/
void fail(final @NotNull String reason);
}
@@ -0,0 +1,38 @@
/*
* 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.fingerprint;
import lombok.experimental.UtilityClass;
import org.jetbrains.annotations.NotNull;
@UtilityClass
public class FingerprintingUtil {
/**
* Returns a hex string representing a hash of the username and IP address
*/
public @NotNull String getFingerprint(final @NotNull String username,
final @NotNull String hostAddress) {
final int hash0 = username.hashCode();
final int hash1 = hostAddress.hashCode();
final int combined = hash0 + hash1;
return Integer.toHexString(hash0 >> 4)
+ Integer.toHexString(combined << 2)
+ Integer.toHexString(hash1 >> 4);
}
}
@@ -41,7 +41,7 @@ public final class FallbackInboundHandler extends ChannelInboundHandlerAdapter {
public void channelActive(final @NotNull ChannelHandlerContext ctx) throws Exception {
// Increase connections per second for the action bar verbose
GlobalSonarStatistics.countConnection();
// Make sure to let the server handle the rest
// Make sure to let the server handle this
ctx.fireChannelActive();
// Add the packet handler pipeline
sonarPipelineInjector.accept(ctx.pipeline());
@@ -49,26 +49,42 @@ public final class FallbackInboundHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelInactive(final @NotNull ChannelHandlerContext ctx) throws Exception {
// Make sure to let the server handle this
ctx.fireChannelInactive();
// The player can disconnect without sending the login packet first
// Account for this by checking if the inetAddress has been set yet
if (inetAddress != null) {
// Remove the IP address from the connected players
Sonar.get().getFallback().getConnected().compute(inetAddress, (k, v) -> null);
// Remove the IP address from the queue
Sonar.get().getFallback().getQueue().getPlayers().compute(inetAddress, (k, v) -> null);
// Remove this account from the online players or decrement the number of accounts with the same IP
Sonar.get().getFallback().getOnline().compute(inetAddress, (k, v) -> v == null || v <= 1 ? null : v - 1);
Sonar.get().getFallback().getQueue().getPlayers().compute(inetAddress, (ignored, action) -> {
// The player is not queued, so we need to remove them from other maps as well
if (action == null) {
// Remove the IP address from the connected players, if needed
Sonar.get().getFallback().getConnected().compute(inetAddress, (__, v) -> {
/*
* Remove this account from the online players or decrement the number of accounts
* with the same IP, but only if the player is logging into the backend server.
* We don't need to decrement the count if the player was just verified since
* we've never actually incremented it in the first place ¯\_(ツ)_/¯
*/
if (v == null) {
Sonar.get().getFallback().getOnline().compute(inetAddress,
(ignored1, count) -> count == null || count <= 1 ? null : count - 1);
}
return null;
});
}
return null;
});
}
// Make sure to let the server handle the rest
ctx.fireChannelInactive();
}
// We can override the default exceptionCaught method since this handler
// will run before the connection knows that there has been an error.
/*
* We can override the default exceptionCaught method since this handler
* will run before the connection knows that there has been an error.
*/
@Override
public void exceptionCaught(final @NotNull ChannelHandlerContext ctx,
final @NotNull Throwable cause) throws Exception {
// Close the channel if we encounter any errors.
ctx.close();
}
}
@@ -23,8 +23,8 @@ 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.FallbackUser;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.api.fingerprint.FingerprintingUtil;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketEncoder;
import xyz.jonesdev.sonar.common.statistics.GlobalSonarStatistics;
@@ -33,15 +33,12 @@ import xyz.jonesdev.sonar.common.util.exception.QuietDecoderException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.*;
import static xyz.jonesdev.sonar.common.fallback.FallbackUserWrapper.closeWith;
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer.*;
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.closeWith;
@AllArgsConstructor
@RequiredArgsConstructor
@@ -67,9 +64,7 @@ public abstract class FallbackInboundHandlerAdapter extends ChannelInboundHandle
if (protocolVersion != null) {
throw QuietDecoderException.INSTANCE;
}
// Store the protocol version
protocolVersion = ProtocolVersion.fromId(protocol);
// Hook the traffic listener
channel.pipeline().addFirst(FALLBACK_BANDWIDTH, FallbackBandwidthHandler.INSTANCE);
}
@@ -92,62 +87,61 @@ public abstract class FallbackInboundHandlerAdapter extends ChannelInboundHandle
}
// Connections from unknown protocol versions will be discarded
// as this is the safest way of handling unwanted connections
// Sonar does not support snapshots or Minecraft versions older than 1.7.2
if (protocolVersion.isUnknown()) {
// Sonar does not support snapshots or Minecraft versions older than 1.7.2
throw QuietDecoderException.INSTANCE;
}
// Increase joins per second for the action bar verbose
GlobalSonarStatistics.countLogin();
// Store the username and IP address
this.username = username;
final FallbackInboundHandler inboundHandler = channel.pipeline().get(FallbackInboundHandler.class);
Objects.requireNonNull(inboundHandler).setInetAddress(socketAddress.getAddress());
final String hostAddress = socketAddress.getAddress().getHostAddress();
final InetAddress inetAddress = socketAddress.getAddress();
channel.pipeline().get(FallbackInboundHandler.class).setInetAddress(inetAddress);
// Check if Fallback is already verifying a player with the same IP address
if (Sonar.get().getFallback().getConnected().containsKey(inboundHandler.getInetAddress())) {
if (Sonar.get().getFallback().getConnected().containsKey(inetAddress)) {
customDisconnect(channel, protocolVersion, alreadyVerifying);
return;
}
// Check if the protocol ID of the player is not allowed to enter the server
if (Sonar.get().getConfig().getVerification().getBlacklistedProtocols()
.contains(protocolVersion.getProtocol())) {
if (Sonar.get().getConfig().getVerification().getBlacklistedProtocols().contains(protocolVersion.getProtocol())) {
customDisconnect(channel, protocolVersion, protocolBlacklisted);
return;
}
final String hostAddress = inetAddress.getHostAddress();
// 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;
if (limit > 0) {
final int score = Sonar.get().getFallback().getBlacklist().asMap().getOrDefault(hostAddress, 0);
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);
initialLogin(ctx, inetAddress, loginPacket);
return;
}
// Completely skip Geyser connections if configured
final boolean geyser = GeyserUtil.isGeyserConnection(channel, socketAddress);
if (geyser && !Sonar.get().getConfig().getVerification().isCheckGeyser()) {
initialLogin(ctx, inboundHandler.getInetAddress(), loginPacket);
initialLogin(ctx, inetAddress, loginPacket);
return;
}
// 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(hostAddress, offlineUuid)) {
initialLogin(ctx, inboundHandler.getInetAddress(), loginPacket);
final String fingerprint = FingerprintingUtil.getFingerprint(username, hostAddress);
if (Sonar.get().getVerifiedPlayerController().getCache().contains(fingerprint)) {
initialLogin(ctx, inetAddress, loginPacket);
return;
}
// Check if the IP address is currently being rate-limited
if (!Sonar.get().getFallback().getRatelimiter().attempt(inboundHandler.getInetAddress())) {
if (!Sonar.get().getFallback().getRatelimiter().attempt(inetAddress)) {
customDisconnect(channel, protocolVersion, reconnectedTooFast);
return;
}
@@ -156,9 +150,9 @@ public abstract class FallbackInboundHandlerAdapter extends ChannelInboundHandle
rewriteProtocol(ctx, channelRemovalListener);
// Queue the connection for further processing
Sonar.get().getFallback().getQueue().getPlayers().compute(inboundHandler.getInetAddress(), (inetAddress, runnable) -> {
Sonar.get().getFallback().getQueue().getPlayers().compute(inetAddress, (__, action) -> {
// Check if the player is already queued since we don't want bots to flood the queue
if (runnable != null) {
if (action != null) {
customDisconnect(channel, protocolVersion, alreadyQueued);
// Remove other instances of this IP address from the queue
return null;
@@ -167,17 +161,13 @@ public abstract class FallbackInboundHandlerAdapter extends ChannelInboundHandle
return () -> {
// Check if the username matches the valid name regex to prevent
// UTF-16 names or other types of exploits
if (!Sonar.get().getConfig().getVerification().getValidNameRegex()
.matcher(username).matches()) {
if (!Sonar.get().getConfig().getVerification().getValidNameRegex().matcher(username).matches()) {
customDisconnect(channel, protocolVersion, invalidUsername);
return;
}
// Create an instance for the Fallback connection
final FallbackUser user = new FallbackUserWrapper(
channel, inboundHandler.getInetAddress(), protocolVersion, offlineUuid, geyser);
// Let the verification handler take over the channel
user.hijack(username, offlineUuid);
// Create an instance for the user and let the verification handler take over the channel
new FallbackUserWrapper(channel, inetAddress, protocolVersion, username, fingerprint, geyser);
};
});
}
@@ -216,23 +206,23 @@ public abstract class FallbackInboundHandlerAdapter extends ChannelInboundHandle
protected final void initialLogin(final @NotNull ChannelHandlerContext ctx,
final @NotNull InetAddress inetAddress,
final @NotNull Runnable loginPacket) throws Exception {
// Increment the number of accounts with the same IP
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 = Sonar.get().getFallback().getOnline().getOrDefault(inetAddress, 0);
if (onlinePerIp >= maxOnlinePerIp) {
customDisconnect(ctx.channel(), protocolVersion, tooManyOnlinePerIP);
return;
Sonar.get().getFallback().getOnline().compute(inetAddress, (__, count) -> {
final int maxOnlinePerIp = Sonar.get().getConfig().getMaxOnlinePerIp();
// Skip the maximum online per IP check if it's disabled in the configuration
if (count != null && maxOnlinePerIp > 0) {
// Check if the number of online players using the same IP address as
// the connecting player is greater than the configured amount
if (count >= maxOnlinePerIp) {
customDisconnect(ctx.channel(), protocolVersion, tooManyOnlinePerIP);
return count + 1;
}
}
}
// Let the server know about the login packet
loginPacket.run();
// Let the server know about the login packet
loginPacket.run();
// Increment the number of accounts with the same IP
return count == null ? 1 : count + 1;
});
}
/**
@@ -18,7 +18,6 @@
package xyz.jonesdev.sonar.common.fallback;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelPipeline;
import lombok.Getter;
import lombok.Setter;
@@ -26,31 +25,27 @@ import lombok.ToString;
import net.kyori.adventure.text.Component;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.event.impl.UserBlacklistedEvent;
import xyz.jonesdev.sonar.api.event.impl.UserVerifyFailedEvent;
import xyz.jonesdev.sonar.api.event.impl.UserVerifyJoinEvent;
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
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.fallback.verification.FallbackPreJoinHandler;
import xyz.jonesdev.sonar.common.statistics.GlobalSonarStatistics;
import xyz.jonesdev.sonar.common.util.exception.QuietDecoderException;
import java.net.InetAddress;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.*;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_20_2;
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry.CONFIG;
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry.GAME;
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer.loginSuccess;
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.closeWith;
@Getter
@ToString(of = {"protocolVersion", "inetAddress", "geyser"})
@@ -59,42 +54,34 @@ public final class FallbackUserWrapper implements FallbackUser {
private final ChannelPipeline pipeline;
private final InetAddress inetAddress;
private final ProtocolVersion protocolVersion;
private final UUID offlineUuid;
@Setter
private boolean receivedClientSettings;
@Setter
private boolean receivedPluginMessage;
private final String fingerprint;
private final String username;
private final boolean geyser;
private final SystemTimer loginTimer = new SystemTimer();
private final SystemTimer loginTimer;
@Setter
private boolean forceCaptcha;
public FallbackUserWrapper(final @NotNull Channel channel,
final @NotNull InetAddress inetAddress,
final @NotNull ProtocolVersion protocolVersion,
final @NotNull UUID offlineUuid,
final @NotNull String username,
final @NotNull String fingerprint,
final boolean geyser) {
this.channel = channel;
this.pipeline = channel.pipeline();
this.inetAddress = inetAddress;
this.protocolVersion = protocolVersion;
this.offlineUuid = offlineUuid;
this.username = username;
this.fingerprint = fingerprint;
this.geyser = geyser;
}
this.loginTimer = new SystemTimer();
@Override
public void disconnect(final @NotNull Component reason) {
final FallbackPacketEncoder encoder = pipeline.get(FallbackPacketEncoder.class);
final boolean duringLogin = encoder != null && encoder.getPacketRegistry() != GAME;
closeWith(channel, protocolVersion, DisconnectPacket.create(reason, duringLogin));
}
@Override
public void hijack(final @NotNull String username, final @NotNull UUID offlineUuid) {
GlobalSonarStatistics.totalAttemptedVerifications++;
if (Sonar.get().getConfig().getVerification().isLogConnections()
&& (Sonar.get().getAttackTracker().getCurrentAttack() == null
|| Sonar.get().getConfig().getVerification().isLogDuringAttack())) {
Sonar.get().getFallback().getLogger().info(
Sonar.get().getLogger().info(
Sonar.get().getConfig().getMessagesConfig().getString("verification.logs.connection")
.replace("<username>", username)
.replace("<ip>", Sonar.get().getConfig().formatAddress(inetAddress))
@@ -112,25 +99,25 @@ public final class FallbackUserWrapper implements FallbackUser {
}
// Mark the player as connected by caching them in a map of verifying players
Sonar.get().getFallback().getConnected().compute(inetAddress, (_k, _v) -> (byte) 0);
Sonar.get().getFallback().getConnected().compute(inetAddress, (__, v) -> (byte) 0);
// 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
write(FallbackPreparer.loginSuccess);
write(loginSuccess);
// The LoginSuccess packet has been sent, now we can change the registry state
newEncoder.updateRegistry(protocolVersion.compareTo(MINECRAFT_1_20_2) >= 0 ? CONFIG : GAME);
// Replace normal decoder to allow custom packets
final FallbackPacketDecoder fallbackPacketDecoder = new FallbackPacketDecoder(protocolVersion);
final FallbackPacketDecoder newDecoder = new FallbackPacketDecoder(protocolVersion);
pipeline.addFirst(FALLBACK_FRAME_DECODER, new FallbackVarInt21FrameDecoder());
pipeline.addLast(FALLBACK_PACKET_DECODER, fallbackPacketDecoder);
pipeline.addLast(FALLBACK_PACKET_DECODER, newDecoder);
// Listen for all incoming packets by setting the packet listener
fallbackPacketDecoder.setListener(new FallbackLoginSessionHandler(this, username));
newDecoder.setListener(new FallbackPreJoinHandler(this));
// Make sure to catch all exceptions during the verification
pipeline.addLast(FALLBACK_TAIL_EXCEPTIONS, FallbackTailExceptionsHandler.INSTANCE);
@@ -138,76 +125,9 @@ public final class FallbackUserWrapper implements FallbackUser {
}
@Override
public void fail(final @NotNull String reason) {
GlobalSonarStatistics.totalFailedVerifications++;
disconnect(Sonar.get().getConfig().getVerification().getVerificationFailed());
final boolean shouldLog = Sonar.get().getAttackTracker().getCurrentAttack() == null
|| Sonar.get().getConfig().getVerification().isLogDuringAttack();
if (shouldLog) {
Sonar.get().getFallback().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));
}
// 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 limit = Sonar.get().getConfig().getVerification().getBlacklistThreshold();
// The user is allowed to disable the blacklist entirely by setting the threshold to 0
if (limit <= 0) 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));
if (shouldLog) {
Sonar.get().getFallback().getLogger().info(
Sonar.get().getConfig().getMessagesConfig().getString("verification.logs.blacklisted")
.replace("<ip>", Sonar.get().getConfig().formatAddress(getInetAddress()))
.replace("<protocol>", String.valueOf(getProtocolVersion().getProtocol())));
}
}
// Throw an exception to avoid further code execution
throw QuietDecoderException.INSTANCE;
}
/**
* @param channel Channel to close
* @param protocolVersion Protocol version
* @param msg Packet to close the channel with
*/
public static void closeWith(final @NotNull Channel channel,
final @NotNull ProtocolVersion protocolVersion,
final @NotNull Object msg) {
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);
}
public void disconnect(final @NotNull Component reason) {
final FallbackPacketEncoder encoder = pipeline.get(FallbackPacketEncoder.class);
final boolean duringLogin = encoder != null && encoder.getPacketRegistry() != GAME;
closeWith(channel, protocolVersion, DisconnectPacket.create(reason, duringLogin));
}
}
@@ -1,56 +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;
// 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,57 +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.DecoderException;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.common.util.exception.QuietDecoderException;
import java.util.List;
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 QuietDecoderException.INSTANCE;
} 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 QuietDecoderException.INSTANCE;
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,30 +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;
// 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());
}
}
@@ -30,6 +30,7 @@ import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT
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.fallback.protocol.FallbackPreparer.maxTotalPacketsSent;
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.DEBUG;
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.readVarInt;
@@ -38,6 +39,7 @@ public final class FallbackPacketDecoder extends ChannelInboundHandlerAdapter {
private FallbackPacketRegistry.ProtocolRegistry registry;
@Setter
private FallbackPacketListener listener;
private int totalPacketsSent;
public FallbackPacketDecoder(final @NotNull ProtocolVersion protocolVersion) {
this.protocolVersion = protocolVersion;
@@ -62,6 +64,11 @@ public final class FallbackPacketDecoder extends ChannelInboundHandlerAdapter {
return;
}
// Don't allow the player to spam packets to overload netty
if (++totalPacketsSent > maxTotalPacketsSent) {
throw DEBUG ? new DecoderException("Sent too many packets") : QuietDecoderException.INSTANCE;
}
// Read the packet ID and then create the packet from it
final int packetId = readVarInt(byteBuf);
final FallbackPacket packet = registry.createPacket(packetId);
@@ -102,14 +109,14 @@ public final class FallbackPacketDecoder extends ChannelInboundHandlerAdapter {
private void checkPacketSize(final @NotNull ByteBuf byteBuf,
final @NotNull FallbackPacket packet) throws Exception {
final int packetSize = byteBuf.readableBytes();
final int expectedMaxLen = packet.expectedMaxLength(byteBuf, protocolVersion);
if (expectedMaxLen != -1 && byteBuf.readableBytes() > expectedMaxLen) {
throw DEBUG ? new DecoderException("Packet too large") : QuietDecoderException.INSTANCE;
if (expectedMaxLen != -1 && packetSize > expectedMaxLen) {
throw DEBUG ? new DecoderException("Packet too large: " + packetSize) : QuietDecoderException.INSTANCE;
}
final int expectedMinLen = packet.expectedMinLength(byteBuf, protocolVersion);
if (expectedMinLen != -1 && byteBuf.readableBytes() < expectedMinLen) {
throw DEBUG ? new DecoderException("Packet too small") : QuietDecoderException.INSTANCE;
if (expectedMinLen != -1 && packetSize < expectedMinLen) {
throw DEBUG ? new DecoderException("Packet too small: " + packetSize) : QuietDecoderException.INSTANCE;
}
}
}
@@ -213,26 +213,24 @@ public enum FallbackPacketRegistry {
map(0x5B, MINECRAFT_1_20_2, true),
map(0x5D, MINECRAFT_1_20_3, true),
map(0x5F, MINECRAFT_1_20_5, true));
clientbound.register(VehicleMovePacket.class, VehicleMovePacket::new,
map(0x29, MINECRAFT_1_9, false),
map(0x2B, MINECRAFT_1_13, false),
map(0x2C, MINECRAFT_1_14, false),
map(0x2D, MINECRAFT_1_15, false),
map(0x2C, MINECRAFT_1_16, false),
map(0x2B, MINECRAFT_1_16_2, false),
map(0x2C, MINECRAFT_1_17, false),
map(0x29, MINECRAFT_1_19, false),
map(0x2B, MINECRAFT_1_19_1, false),
map(0x2A, MINECRAFT_1_19_3, false),
map(0x2E, MINECRAFT_1_19_4, false),
map(0x2F, MINECRAFT_1_20_2, false),
map(0x31, MINECRAFT_1_20_5, false));
clientbound.register(SpawnEntityPacket.class, SpawnEntityPacket::new,
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),
@@ -357,6 +355,33 @@ public enum FallbackPacketRegistry {
map(0x4F, MINECRAFT_1_20_2, true),
map(0x51, MINECRAFT_1_20_3, true),
map(0x53, MINECRAFT_1_20_5, true));
clientbound.register(EntityAnimationPacket.class, EntityAnimationPacket::new,
map(0x0B, MINECRAFT_1_7_2, true),
map(0x06, MINECRAFT_1_9, true),
map(0x05, MINECRAFT_1_16, true),
map(0x06, MINECRAFT_1_17, true),
map(0x03, MINECRAFT_1_19, true),
map(0x04, MINECRAFT_1_19_4, true),
map(0x03, MINECRAFT_1_20_2, true));
clientbound.register(TeleportEntityPacket.class, TeleportEntityPacket::new,
map(0x18, MINECRAFT_1_7_2, true),
map(0x4A, MINECRAFT_1_9, true),
map(0x49, MINECRAFT_1_9_4, true),
map(0x4B, MINECRAFT_1_12, true),
map(0x4C, MINECRAFT_1_12_1, true),
map(0x50, MINECRAFT_1_13, true),
map(0x56, MINECRAFT_1_14, true),
map(0x57, MINECRAFT_1_15, true),
map(0x56, MINECRAFT_1_16, true),
map(0x61, MINECRAFT_1_17, true),
map(0x62, MINECRAFT_1_18, true),
map(0x63, MINECRAFT_1_19, true),
map(0x66, MINECRAFT_1_19_1, true),
map(0x64, MINECRAFT_1_19_3, true),
map(0x68, MINECRAFT_1_19_4, true),
map(0x6B, MINECRAFT_1_20_2, true),
map(0x6D, MINECRAFT_1_20_3, true),
map(0x70, MINECRAFT_1_20_5, true));
serverbound.register(SetHeldItemPacket.class, SetHeldItemPacket::new,
map(0x09, MINECRAFT_1_7_2, false),
@@ -401,6 +426,21 @@ public enum FallbackPacketRegistry {
map(0x22, MINECRAFT_1_20_2, false),
map(0x23, MINECRAFT_1_20_3, false),
map(0x26, MINECRAFT_1_20_5, false));
serverbound.register(VehicleMovePacket.class, VehicleMovePacket::new,
map(0x10, MINECRAFT_1_9, false),
map(0x11, MINECRAFT_1_12, false),
map(0x10, MINECRAFT_1_12_1, false),
map(0x13, MINECRAFT_1_13, false),
map(0x15, MINECRAFT_1_14, false),
map(0x16, MINECRAFT_1_16, false),
map(0x15, MINECRAFT_1_17, false),
map(0x17, MINECRAFT_1_19, false),
map(0x18, MINECRAFT_1_19_1, false),
map(0x17, MINECRAFT_1_19_3, false),
map(0x18, MINECRAFT_1_19_4, false),
map(0x1A, MINECRAFT_1_20_2, false),
map(0x1B, MINECRAFT_1_20_3, false),
map(0x1E, MINECRAFT_1_20_5, false));
serverbound.register(SystemChatPacket.class, SystemChatPacket::new,
map(0x01, MINECRAFT_1_7_2, false),
map(0x02, MINECRAFT_1_9, false),
@@ -534,6 +574,19 @@ public enum FallbackPacketRegistry {
map(0x23, MINECRAFT_1_20_2, false),
map(0x24, MINECRAFT_1_20_3, false),
map(0x27, MINECRAFT_1_20_5, false));
serverbound.register(AnimationPacket.class, AnimationPacket::new,
map(0x0A, MINECRAFT_1_7_2, false),
map(0x1A, MINECRAFT_1_9, false),
map(0x1D, MINECRAFT_1_12, false),
map(0x27, MINECRAFT_1_13, false),
map(0x2A, MINECRAFT_1_14, false),
map(0x2B, MINECRAFT_1_16, false),
map(0x2C, MINECRAFT_1_16_2, false),
map(0x2E, MINECRAFT_1_19, false),
map(0x2F, MINECRAFT_1_19_1, false),
map(0x32, MINECRAFT_1_20_2, false),
map(0x33, MINECRAFT_1_20_3, false),
map(0x36, MINECRAFT_1_20_5, false));
}
};
@@ -18,6 +18,8 @@
package xyz.jonesdev.sonar.common.fallback.protocol;
import lombok.experimental.UtilityClass;
import net.kyori.adventure.nbt.CompoundBinaryTag;
import net.kyori.adventure.nbt.IntBinaryTag;
import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
import xyz.jonesdev.sonar.api.Sonar;
@@ -34,50 +36,57 @@ 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();
// LoginSuccess
public FallbackPacket loginSuccess;
// Abilities
public final FallbackPacket DEFAULT_ABILITIES = new FallbackPacketSnapshot(new PlayerAbilitiesPacket(0x00, 0f, 0f));
public final FallbackPacket CAPTCHA_ABILITIES = new FallbackPacketSnapshot(new PlayerAbilitiesPacket(0x02, 0f, 0f));
public final FallbackPacket CAPTCHA_ABILITIES_BEDROCK = new FallbackPacketSnapshot(new PlayerAbilitiesPacket(0x06, 0f, 0f));
// Chunks
private final Random RANDOM = new Random();
public static final int PLAYER_ENTITY_ID = RANDOM.nextInt(50);
public static final int VEHICLE_BOAT_ENTITY_ID = PLAYER_ENTITY_ID + 1 + RANDOM.nextInt(10);
public static final int VEHICLE_MINECART_ENTITY_ID = VEHICLE_BOAT_ENTITY_ID + 1 + RANDOM.nextInt(10);
public static final CompoundBinaryTag MAP_ITEM_NBT = CompoundBinaryTag.builder()
.put("map", IntBinaryTag.intBinaryTag(0)) // map type
.build();
public static final BlockType[] POSSIBLE_BLOCK_TYPES = BlockType.values();
public final FallbackPacket[] BLOCKS_PACKETS = new FallbackPacket[POSSIBLE_BLOCK_TYPES.length];
public final int BLOCKS_PER_ROW = 8; // 8 * 8 = 64 (vanilla 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 = 1 + RANDOM.nextInt(255); // 255 is the maximum Y position
public final int IN_AIR_Y_POSITION = 1000 + RANDOM.nextInt(338); // High altitude (randomized)
public final int IN_VOID_Y_POSITION = -(120 + RANDOM.nextInt(120)); // Low altitude (randomized)
public final int FIRST_TELEPORT_ID = RANDOM.nextInt();
public final int SECOND_TELEPORT_ID = RANDOM.nextInt();
public final FallbackPacket DEFAULT_ABILITIES = new PlayerAbilitiesPacket(0x00, 0, 0);
public final FallbackPacket NO_MOVE_ABILITIES = new PlayerAbilitiesPacket(0x02, 0, 0);
public final FallbackPacket NO_MOVE_ABILITIES_BEDROCK = new PlayerAbilitiesPacket(0x06, 0, 0);
public final FallbackPacket CAPTCHA_POSITION = new FallbackPacketSnapshot(new SetPlayerPositionRotationPacket(
SPAWN_X_POSITION, IN_AIR_Y_POSITION, SPAWN_Z_POSITION, 0, 90, 0, 0, false));
public final FallbackPacket EMPTY_CHUNK_DATA = new FallbackPacketSnapshot(new ChunkDataPacket(0, 0));
// Finish Configuration
public final FallbackPacket FINISH_CONFIGURATION = new FinishConfigurationPacket(); // No snapshot needed because it's empty either way
// Synchronize Registry
public final FallbackPacket FINISH_CONFIGURATION = new FinishConfigurationPacket();
public final FallbackPacket REGISTRY_SYNC_LEGACY = new FallbackPacketSnapshot(new RegistryDataPacket(DimensionRegistry.CODEC_1_20, null, null));
public final FallbackPacket[] REGISTRY_SYNC_1_20_5 = RegistryDataPacket.of(DimensionRegistry.CODEC_1_20);
public final FallbackPacket[] REGISTRY_SYNC_1_21 = RegistryDataPacket.of(DimensionRegistry.CODEC_1_21);
// Keep Alive
public final FallbackPacket CAPTCHA_KEEP_ALIVE = new FallbackPacketSnapshot(new KeepAlivePacket(-1337L));
// Game Event (1.20.3+)
public final FallbackPacket START_WRITING_CHUNKS = new FallbackPacketSnapshot(new GameEventPacket(13, 0));
// Chat
public final FallbackPacket START_WRITING_CHUNKS = new GameEventPacket(13, 0);
public final static FallbackPacket CACHED_HELD_ITEM_RESET = new SetHeldItemPacket(~4);
public final FallbackPacket CAPTCHA_KEEP_ALIVE = new KeepAlivePacket(RANDOM.nextInt());
public static FallbackPacket loginSuccess;
public FallbackPacket enterCodeMessage;
public FallbackPacket incorrectCaptcha;
// JoinGame
public final int PLAYER_ENTITY_ID = RANDOM.nextInt(10);
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 static FallbackPacket joinGame;
public FallbackPacket defaultSpawnPosition;
// Spawn Position
public final int TELEPORT_ID = RANDOM.nextInt();
public FallbackPacket spawnPosition;
// Transfer packet
public FallbackPacket fallStartPosition;
public FallbackPacket fallStartPositionLegacy;
public static FallbackPacket transferToOrigin;
// Disconnect messages
public FallbackPacket blacklisted;
public FallbackPacket alreadyQueued;
public FallbackPacket alreadyVerifying;
@@ -85,33 +94,16 @@ public class FallbackPreparer {
public FallbackPacket protocolBlacklisted;
public FallbackPacket invalidUsername;
public FallbackPacket tooManyOnlinePerIP;
// Vehicle
public FallbackPacket removeEntities;
public FallbackPacket spawnEntity;
public static FallbackPacket setPassengers;
public static final int VEHICLE_ENTITY_ID = PLAYER_ENTITY_ID + 1 + RANDOM.nextInt(10);
// Collisions
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 IN_AIR_Y_POSITION = 1337;
// CAPTCHA position
public final FallbackPacket CAPTCHA_POSITION = new FallbackPacketSnapshot(new SetPlayerPositionRotationPacket(
SPAWN_X_POSITION, IN_AIR_Y_POSITION, SPAWN_Z_POSITION, 0, 90, 0, false));
// Platform
public BlockType blockType;
public int maxMovementTick, dynamicSpawnYPosition;
public double[] preparedCachedYMotions;
public double maxFallDistance;
// XP packets
public FallbackPacket removeBoat;
public FallbackPacket teleportMinecart;
public FallbackPacket spawnBoatEntity;
public FallbackPacket spawnMinecartEntity;
public FallbackPacket setBoatPassengers;
public FallbackPacket setMinecartPassengers;
public FallbackPacket[] xpCountdown;
public static int maxMovementTick, dynamicSpawnYPosition, maxTotalPacketsSent;
@SuppressWarnings("ResultOfMethodCallIgnored")
public void prepare() {
// Preload the packet registry to avoid CPU/RAM issues on 1st connection
@@ -119,54 +111,56 @@ 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];
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, fallDistance = 1;
for (int i = 0; i < maxMovementTick; i++) {
motionY = (motionY - 0.1) * 0.98f;
fallDistance += Math.abs(motionY);
}
// Set the dynamic block and collide Y position based on the maximum fall distance
dynamicSpawnYPosition = DEFAULT_Y_COLLIDE_POSITION + (int) Math.ceil(maxFallDistance);
dynamicSpawnYPosition = DEFAULT_Y_COLLIDE_POSITION + (int) Math.ceil(fallDistance);
defaultSpawnPosition = new FallbackPacketSnapshot(new SetDefaultSpawnPositionPacket(
SPAWN_X_POSITION, dynamicSpawnYPosition, SPAWN_Z_POSITION));
SPAWN_X_POSITION, IN_AIR_Y_POSITION, SPAWN_Z_POSITION));
spawnPosition = new FallbackPacketSnapshot(new SetPlayerPositionRotationPacket(
SPAWN_X_POSITION, IN_AIR_Y_POSITION, SPAWN_Z_POSITION,
0, 0, FIRST_TELEPORT_ID, 0, false));
fallStartPosition = new FallbackPacketSnapshot(new SetPlayerPositionRotationPacket(
SPAWN_X_POSITION, dynamicSpawnYPosition - IN_AIR_Y_POSITION, SPAWN_Z_POSITION,
0, -90, SECOND_TELEPORT_ID, 1 << 1, false));
fallStartPositionLegacy = new FallbackPacketSnapshot(new SetPlayerPositionRotationPacket(
SPAWN_X_POSITION, dynamicSpawnYPosition, SPAWN_Z_POSITION,
0, -90, TELEPORT_ID, false));
0, -90, 0, 0, false));
// Prepare collision platform positions
blockType = BlockType.valueOf(Sonar.get().getConfig().getGeneralConfig().getString(
"verification.checks.collision.collision-block-type").toUpperCase());
final BlockUpdate[] changedBlocks = new BlockUpdate[BLOCKS_PER_ROW * BLOCKS_PER_ROW];
final int length = BLOCKS_PER_ROW / 2;
for (int i = 0; i < BLOCKS_PACKETS.length; i++) {
final BlockUpdate[] blocks = new BlockUpdate[BLOCKS_PER_ROW * BLOCKS_PER_ROW];
int index = 0;
for (int x = 0; x < BLOCKS_PER_ROW; x++) {
for (int z = 0; z < BLOCKS_PER_ROW; z++) {
final BlockUpdate.BlockPosition position = new BlockUpdate.BlockPosition(
x + (BLOCKS_PER_ROW / 2),
DEFAULT_Y_COLLIDE_POSITION,
z + (BLOCKS_PER_ROW / 2),
0, 0);
changedBlocks[index++] = new BlockUpdate(position, blockType);
int index = 0;
for (int x = 0; x < BLOCKS_PER_ROW; x++) {
for (int z = 0; z < BLOCKS_PER_ROW; z++) {
final BlockUpdate.BlockPosition position = new BlockUpdate.BlockPosition(
x + length, DEFAULT_Y_COLLIDE_POSITION, z + length, 0, 0);
blocks[index++] = new BlockUpdate(position, POSSIBLE_BLOCK_TYPES[i]);
}
}
BLOCKS_PACKETS[i] = new UpdateSectionBlocksPacket(0, 0, blocks);
}
updateSectionBlocks = new FallbackPacketSnapshot(new UpdateSectionBlocksPacket(0, 0, changedBlocks));
// Prepare disconnect packets during login
blacklisted = new FallbackPacketSnapshot(DisconnectPacket.create(Sonar.get().getConfig().getVerification().getBlacklisted(), true));
alreadyVerifying = new FallbackPacketSnapshot(DisconnectPacket.create(Sonar.get().getConfig().getVerification().getAlreadyVerifying(), true));
@@ -186,10 +180,15 @@ public class FallbackPreparer {
}
// Prepare packets for the vehicle check
removeEntities = new FallbackPacketSnapshot(new RemoveEntitiesPacket(VEHICLE_ENTITY_ID));
spawnEntity = new FallbackPacketSnapshot(new SpawnEntityPacket(
VEHICLE_ENTITY_ID, EntityType.BOAT, SPAWN_X_POSITION, IN_AIR_Y_POSITION, SPAWN_Z_POSITION));
setPassengers = new FallbackPacketSnapshot(new SetPassengersPacket(VEHICLE_ENTITY_ID, PLAYER_ENTITY_ID));
removeBoat = new RemoveEntitiesPacket(VEHICLE_BOAT_ENTITY_ID);
teleportMinecart = new TeleportEntityPacket(
VEHICLE_MINECART_ENTITY_ID, SPAWN_X_POSITION, IN_VOID_Y_POSITION, SPAWN_Z_POSITION, false);
spawnBoatEntity = new FallbackPacketSnapshot(new SpawnEntityPacket(
VEHICLE_BOAT_ENTITY_ID, EntityType.BOAT, SPAWN_X_POSITION, IN_AIR_Y_POSITION, SPAWN_Z_POSITION));
spawnMinecartEntity = new FallbackPacketSnapshot(new SpawnEntityPacket(
VEHICLE_MINECART_ENTITY_ID, EntityType.MINECART, SPAWN_X_POSITION, IN_AIR_Y_POSITION, SPAWN_Z_POSITION));
setBoatPassengers = new FallbackPacketSnapshot(new SetPassengersPacket(VEHICLE_BOAT_ENTITY_ID, PLAYER_ENTITY_ID));
setMinecartPassengers = new FallbackPacketSnapshot(new SetPassengersPacket(VEHICLE_MINECART_ENTITY_ID, PLAYER_ENTITY_ID));
if (Sonar.get().getConfig().getVerification().getMap().getTiming() != SonarConfiguration.Verification.Timing.NEVER
|| Sonar.get().getConfig().getVerification().getGravity().isCaptchaOnFail()) {
@@ -229,5 +228,11 @@ public class FallbackPreparer {
xpCountdown = null;
Sonar.get().getFallback().setCaptchaGenerator(null);
}
maxTotalPacketsSent = maxMovementTick
+ (xpCountdown == null ? 0 : xpCountdown.length) * 3
+ Sonar.get().getConfig().getVerification().getVehicle().getMinimumPackets() * 4
+ Sonar.get().getConfig().getVerification().getMap().getMaxTries()
+ 150 /* some arbitrary leeway */;
}
}
@@ -24,167 +24,266 @@ import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import java.util.function.Function;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
// TODO: load mappings from a separate file
@SuppressWarnings("unused")
@RequiredArgsConstructor
public enum BlockType {
STONE(protocolVersion -> 1, 1),
// Useful resources:
// - https://github.com/PrismarineJS/minecraft-data/blob/master/data/pc/
// - https://pokechu22.github.io/Burger/
// - https://github.com/ViaVersion/Mappings/tree/main/mappings
//STONE(protocolVersion -> 1, 1),
ENCHANTMENT_TABLE(protocolVersion -> {
switch (protocolVersion) {
case MINECRAFT_1_7_2:
case MINECRAFT_1_7_6:
default:
// 1.7.2-1.12.2
return 116;
case MINECRAFT_1_13:
case MINECRAFT_1_13_1:
return 4612;
case MINECRAFT_1_13_2:
case MINECRAFT_1_14:
case MINECRAFT_1_14_1:
case MINECRAFT_1_14_2:
case MINECRAFT_1_14_3:
return 4613;
case MINECRAFT_1_14_4:
case MINECRAFT_1_15:
case MINECRAFT_1_15_1:
case MINECRAFT_1_15_2:
case MINECRAFT_1_16:
return 5116;
case MINECRAFT_1_16_1:
return 5132;
case MINECRAFT_1_16_2:
case MINECRAFT_1_16_3:
case MINECRAFT_1_16_4:
return 5136;
case MINECRAFT_1_17:
case MINECRAFT_1_17_1:
case MINECRAFT_1_18:
case MINECRAFT_1_18_2:
return 5333;
case MINECRAFT_1_19:
case MINECRAFT_1_19_1:
return 5719;
case MINECRAFT_1_19_3:
return 7159;
case MINECRAFT_1_19_4:
return 7385;
case MINECRAFT_1_20:
case MINECRAFT_1_20_2:
case MINECRAFT_1_20_3:
case MINECRAFT_1_20_5:
case MINECRAFT_1_21:
return 7389;
if (protocolVersion.compareTo(MINECRAFT_1_12_2) <= 0) {
return 116;
}
if (protocolVersion.compareTo(MINECRAFT_1_13_1) <= 0) {
return 4612;
}
if (protocolVersion.compareTo(MINECRAFT_1_13_2) <= 0) {
return 4613;
}
if (protocolVersion.compareTo(MINECRAFT_1_15_2) <= 0) {
return 5116;
}
if (protocolVersion.compareTo(MINECRAFT_1_16_1) <= 0) {
return 5132;
}
if (protocolVersion.compareTo(MINECRAFT_1_16_4) <= 0) {
return 5136;
}
if (protocolVersion.compareTo(MINECRAFT_1_18_2) <= 0) {
return 5333;
}
if (protocolVersion.compareTo(MINECRAFT_1_19_1) <= 0) {
return 5719;
}
if (protocolVersion.compareTo(MINECRAFT_1_19_3) <= 0) {
return 7159;
}
if (protocolVersion.compareTo(MINECRAFT_1_19_4) <= 0) {
return 7385;
}
return 7389;
}, 0.75f),
TRAPDOOR(protocolVersion -> {
switch (protocolVersion) {
case MINECRAFT_1_7_2:
case MINECRAFT_1_7_6:
// We have to use wooden trapdoors for 1.7 since 1.7 doesn't have iron trapdoors
return 96;
default:
// 1.8-1.12.2
return 167;
case MINECRAFT_1_13:
case MINECRAFT_1_13_1:
return 6509;
case MINECRAFT_1_13_2:
case MINECRAFT_1_14:
case MINECRAFT_1_14_1:
case MINECRAFT_1_14_2:
case MINECRAFT_1_14_3:
return 6510;
case MINECRAFT_1_14_4:
case MINECRAFT_1_15:
case MINECRAFT_1_15_1:
case MINECRAFT_1_15_2:
case MINECRAFT_1_16:
return 7016;
case MINECRAFT_1_16_1:
return 7552;
case MINECRAFT_1_16_2:
case MINECRAFT_1_16_3:
case MINECRAFT_1_16_4:
return 7556;
case MINECRAFT_1_17:
case MINECRAFT_1_17_1:
case MINECRAFT_1_18:
case MINECRAFT_1_18_2:
return 7802;
case MINECRAFT_1_19:
case MINECRAFT_1_19_1:
return 8293;
case MINECRAFT_1_19_3:
return 9937;
case MINECRAFT_1_19_4:
return 10269;
case MINECRAFT_1_20:
return 10273;
case MINECRAFT_1_20_2:
case MINECRAFT_1_20_3:
case MINECRAFT_1_20_5:
case MINECRAFT_1_21:
return 10414;
// We have to use wooden trapdoors for 1.7 since 1.7 doesn't have iron trapdoors
if (protocolVersion.compareTo(MINECRAFT_1_7_6) <= 0) {
return 96;
}
if (protocolVersion.compareTo(MINECRAFT_1_12_2) <= 0) {
return 167;
}
if (protocolVersion.compareTo(MINECRAFT_1_13_1) <= 0) {
return 6509;
}
if (protocolVersion.compareTo(MINECRAFT_1_13_2) <= 0) {
return 6510;
}
if (protocolVersion.compareTo(MINECRAFT_1_15_2) <= 0) {
return 7016;
}
if (protocolVersion.compareTo(MINECRAFT_1_16_1) <= 0) {
return 7552;
}
if (protocolVersion.compareTo(MINECRAFT_1_16_4) <= 0) {
return 7556;
}
if (protocolVersion.compareTo(MINECRAFT_1_18_2) <= 0) {
return 7802;
}
if (protocolVersion.compareTo(MINECRAFT_1_19_1) <= 0) {
return 8293;
}
if (protocolVersion.compareTo(MINECRAFT_1_19_3) <= 0) {
return 9937;
}
if (protocolVersion.compareTo(MINECRAFT_1_19_4) <= 0) {
return 10269;
}
if (protocolVersion.compareTo(MINECRAFT_1_20) <= 0) {
return 10273;
}
return 10414;
}, 0.1875),
BARRIER(protocolVersion -> {
switch (protocolVersion) {
case MINECRAFT_1_7_2:
case MINECRAFT_1_7_6:
// We have to use glass for 1.7 since 1.7 doesn't have barrier blocks
return 20;
default:
// 1.8-1.12.2
return 166;
case MINECRAFT_1_13:
case MINECRAFT_1_13_1:
return 6493;
case MINECRAFT_1_13_2:
return 6494;
case MINECRAFT_1_14:
case MINECRAFT_1_14_1:
case MINECRAFT_1_14_2:
case MINECRAFT_1_14_3:
case MINECRAFT_1_14_4:
case MINECRAFT_1_15:
case MINECRAFT_1_15_1:
case MINECRAFT_1_15_2:
return 7000;
case MINECRAFT_1_16:
case MINECRAFT_1_16_1:
return 7536;
case MINECRAFT_1_16_2:
case MINECRAFT_1_16_3:
case MINECRAFT_1_16_4:
return 7540;
case MINECRAFT_1_17:
case MINECRAFT_1_17_1:
case MINECRAFT_1_18:
case MINECRAFT_1_18_2:
return 7754;
case MINECRAFT_1_19:
case MINECRAFT_1_19_1:
return 8245;
case MINECRAFT_1_19_3:
return 9889;
case MINECRAFT_1_19_4:
return 10221;
case MINECRAFT_1_20:
return 10225;
case MINECRAFT_1_20_2:
case MINECRAFT_1_20_3:
case MINECRAFT_1_20_5:
case MINECRAFT_1_21:
return 10366;
END_PORTAL_FRAME(protocolVersion -> {
if (protocolVersion.compareTo(MINECRAFT_1_12_2) <= 0) {
return 120;
}
}, 1);
if (protocolVersion.compareTo(MINECRAFT_1_13_1) <= 0) {
return 4633;
}
if (protocolVersion.compareTo(MINECRAFT_1_13_2) <= 0) {
return 4634;
}
if (protocolVersion.compareTo(MINECRAFT_1_15_2) <= 0) {
return 5137;
}
if (protocolVersion.compareTo(MINECRAFT_1_16_1) <= 0) {
return 5153;
}
if (protocolVersion.compareTo(MINECRAFT_1_16_4) <= 0) {
return 5157;
}
if (protocolVersion.compareTo(MINECRAFT_1_18_2) <= 0) {
return 5358;
}
if (protocolVersion.compareTo(MINECRAFT_1_19_1) <= 0) {
return 5744;
}
if (protocolVersion.compareTo(MINECRAFT_1_19_3) <= 0) {
return 7184;
}
if (protocolVersion.compareTo(MINECRAFT_1_19_4) <= 0) {
return 7410;
}
return 7414;
}, 0.8125),
DAYLIGHT_SENSOR(protocolVersion -> {
if (protocolVersion.compareTo(MINECRAFT_1_12_2) <= 0) {
return 151;
}
if (protocolVersion.compareTo(MINECRAFT_1_13_1) <= 0) {
return 5651;
}
if (protocolVersion.compareTo(MINECRAFT_1_13_2) <= 0) {
return 5652;
}
if (protocolVersion.compareTo(MINECRAFT_1_15_2) <= 0) {
return 6158;
}
if (protocolVersion.compareTo(MINECRAFT_1_16_1) <= 0) {
return 6694;
}
if (protocolVersion.compareTo(MINECRAFT_1_16_4) <= 0) {
return 6698;
}
if (protocolVersion.compareTo(MINECRAFT_1_18_2) <= 0) {
return 6916;
}
if (protocolVersion.compareTo(MINECRAFT_1_19_1) <= 0) {
return 7327;
}
if (protocolVersion.compareTo(MINECRAFT_1_19_3) <= 0) {
return 8811;
}
if (protocolVersion.compareTo(MINECRAFT_1_19_4) <= 0) {
return 9063;
}
if (protocolVersion.compareTo(MINECRAFT_1_20) <= 0) {
return 9067;
}
return 9207;
}, 0.375),
COBBLESTONE_WALL(protocolVersion -> {
if (protocolVersion.compareTo(MINECRAFT_1_12_2) <= 0) {
return 139;
}
if (protocolVersion.compareTo(MINECRAFT_1_13_1) <= 0) {
return 5196;
}
if (protocolVersion.compareTo(MINECRAFT_1_13_2) <= 0) {
return 5197;
}
if (protocolVersion.compareTo(MINECRAFT_1_15_2) <= 0) {
return 5700;
}
if (protocolVersion.compareTo(MINECRAFT_1_16_1) <= 0) {
return 5660;
}
if (protocolVersion.compareTo(MINECRAFT_1_16_4) <= 0) {
return 5664;
}
if (protocolVersion.compareTo(MINECRAFT_1_18_2) <= 0) {
return 5866;
}
if (protocolVersion.compareTo(MINECRAFT_1_19_1) <= 0) {
return 6252;
}
if (protocolVersion.compareTo(MINECRAFT_1_19_3) <= 0) {
return 7692;
}
if (protocolVersion.compareTo(MINECRAFT_1_19_4) <= 0) {
return 7918;
}
return 7922;
}, 1.5),
STONE_SLABS(protocolVersion -> {
if (protocolVersion.compareTo(MINECRAFT_1_12_2) <= 0) {
return 44;
}
if (protocolVersion.compareTo(MINECRAFT_1_13_1) <= 0) {
return 7296;
}
if (protocolVersion.compareTo(MINECRAFT_1_13_2) <= 0) {
return 7297;
}
if (protocolVersion.compareTo(MINECRAFT_1_15_2) <= 0) {
return 7809;
}
if (protocolVersion.compareTo(MINECRAFT_1_16_1) <= 0) {
return 8345;
}
if (protocolVersion.compareTo(MINECRAFT_1_16_4) <= 0) {
return 8349;
}
if (protocolVersion.compareTo(MINECRAFT_1_18_2) <= 0) {
return 8595;
}
if (protocolVersion.compareTo(MINECRAFT_1_19_1) <= 0) {
return 9092;
}
if (protocolVersion.compareTo(MINECRAFT_1_19_3) <= 0) {
return 10748;
}
if (protocolVersion.compareTo(MINECRAFT_1_19_4) <= 0) {
return 11086;
}
if (protocolVersion.compareTo(MINECRAFT_1_20) <= 0) {
return 11090;
}
return 11231;
}, 0.5),
WHITE_CARPET(protocolVersion -> {
if (protocolVersion.compareTo(MINECRAFT_1_12_2) <= 0) {
return 171;
}
if (protocolVersion.compareTo(MINECRAFT_1_13_1) <= 0) {
return 6823;
}
if (protocolVersion.compareTo(MINECRAFT_1_13_2) <= 0) {
return 6824;
}
if (protocolVersion.compareTo(MINECRAFT_1_15_2) <= 0) {
return 7330;
}
if (protocolVersion.compareTo(MINECRAFT_1_16_1) <= 0) {
return 7866;
}
if (protocolVersion.compareTo(MINECRAFT_1_16_4) <= 0) {
return 7870;
}
if (protocolVersion.compareTo(MINECRAFT_1_18_2) <= 0) {
return 8116;
}
if (protocolVersion.compareTo(MINECRAFT_1_19_1) <= 0) {
return 8607;
}
if (protocolVersion.compareTo(MINECRAFT_1_19_3) <= 0) {
return 10251;
}
if (protocolVersion.compareTo(MINECRAFT_1_19_4) <= 0) {
return 10583;
}
if (protocolVersion.compareTo(MINECRAFT_1_20) <= 0) {
return 10587;
}
return 10728;
}, 0.0625);
private final Function<ProtocolVersion, Integer> idFunction;
// TODO: Implement per-version block heights?
@Getter
private final double blockHeight;
@@ -25,12 +25,12 @@ import org.jetbrains.annotations.NotNull;
@Getter
public final class BlockUpdate {
private final @NotNull BlockPosition position;
private final BlockType type;
private final BlockType blockType;
private final int legacyBlockState, blockState;
public BlockUpdate(final @NotNull BlockPosition position, final BlockType type) {
public BlockUpdate(final @NotNull BlockPosition position, final BlockType blockType) {
this.position = position;
this.type = type;
this.blockType = blockType;
final int x = position.getX() - (position.getChunkX() << 4);
final int y = position.getY();
final int z = position.getZ() - (position.getChunkZ() << 4);
@@ -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);
@@ -23,64 +23,55 @@ import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import java.util.function.Function;
@SuppressWarnings("unused")
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
// TODO: load mappings from a separate file
@RequiredArgsConstructor
public enum ItemType {
// Useful resources:
// - https://github.com/PrismarineJS/minecraft-data/blob/master/data/pc/
// - https://pokechu22.github.io/Burger/1.20.4.html
// - https://github.com/ViaVersion/Mappings/tree/main/mappings
FILLED_MAP(protocolVersion -> {
// Useful resources:
// - https://github.com/PrismarineJS/minecraft-data/blob/master/data/pc/
// - https://pokechu22.github.io/Burger/1.20.4.html
// - https://github.com/ViaVersion/Mappings/tree/main/mappings
switch (protocolVersion) {
default:
// 1.7-1.12.2
return 358;
case MINECRAFT_1_13:
case MINECRAFT_1_13_1:
return 608;
case MINECRAFT_1_13_2:
return 613;
case MINECRAFT_1_14:
case MINECRAFT_1_14_1:
case MINECRAFT_1_14_2:
case MINECRAFT_1_14_3:
case MINECRAFT_1_14_4:
case MINECRAFT_1_15:
case MINECRAFT_1_15_1:
case MINECRAFT_1_15_2:
return 671;
case MINECRAFT_1_16:
case MINECRAFT_1_16_1:
case MINECRAFT_1_16_2:
case MINECRAFT_1_16_3:
case MINECRAFT_1_16_4:
return 733;
case MINECRAFT_1_17:
case MINECRAFT_1_17_1:
case MINECRAFT_1_18:
case MINECRAFT_1_18_2:
return 847;
case MINECRAFT_1_19:
case MINECRAFT_1_19_1:
return 886;
case MINECRAFT_1_19_3:
return 914;
case MINECRAFT_1_19_4:
return 937;
case MINECRAFT_1_20:
case MINECRAFT_1_20_2:
return 941;
case MINECRAFT_1_20_3:
return 979;
case MINECRAFT_1_20_5:
case MINECRAFT_1_21:
return 982;
if (protocolVersion.compareTo(MINECRAFT_1_12_2) <= 0) {
return 358;
}
if (protocolVersion.compareTo(MINECRAFT_1_13_1) <= 0) {
return 608;
}
if (protocolVersion.compareTo(MINECRAFT_1_13_2) <= 0) {
return 613;
}
if (protocolVersion.compareTo(MINECRAFT_1_15_2) <= 0) {
return 671;
}
if (protocolVersion.compareTo(MINECRAFT_1_16_4) <= 0) {
return 733;
}
if (protocolVersion.compareTo(MINECRAFT_1_18_2) <= 0) {
return 847;
}
if (protocolVersion.compareTo(MINECRAFT_1_19_1) <= 0) {
return 886;
}
if (protocolVersion.compareTo(MINECRAFT_1_19_3) <= 0) {
return 914;
}
if (protocolVersion.compareTo(MINECRAFT_1_19_4) <= 0) {
return 937;
}
if (protocolVersion.compareTo(MINECRAFT_1_20_2) <= 0) {
return 941;
}
if (protocolVersion.compareTo(MINECRAFT_1_20_3) <= 0) {
return 979;
}
return 982;
});
private final Function<ProtocolVersion, Integer> function;
private final Function<ProtocolVersion, Integer> idFunction;
public int getId(final @NotNull ProtocolVersion protocolVersion) {
return function.apply(protocolVersion);
return idFunction.apply(protocolVersion);
}
}
@@ -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);
}
}
@@ -23,64 +23,64 @@ import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import java.util.function.Function;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
// TODO: load mappings from a separate file
@RequiredArgsConstructor
public enum EntityType {
// Useful resources:
// - https://github.com/PrismarineJS/minecraft-data/blob/master/data/pc/
// - https://pokechu22.github.io/Burger/1.20.4.html
BOAT(protocolVersion -> {
// Useful resources:
// - https://github.com/PrismarineJS/minecraft-data/blob/master/data/pc/
// - https://pokechu22.github.io/Burger/1.20.4.html
switch (protocolVersion) {
default:
case MINECRAFT_1_7_2:
case MINECRAFT_1_7_6:
case MINECRAFT_1_8:
case MINECRAFT_1_9:
case MINECRAFT_1_9_1:
case MINECRAFT_1_9_2:
case MINECRAFT_1_9_4:
case MINECRAFT_1_10:
case MINECRAFT_1_11:
case MINECRAFT_1_11_1:
case MINECRAFT_1_12:
case MINECRAFT_1_12_1:
case MINECRAFT_1_12_2:
case MINECRAFT_1_13:
case MINECRAFT_1_13_1:
case MINECRAFT_1_13_2:
return 1;
case MINECRAFT_1_14:
case MINECRAFT_1_14_1:
case MINECRAFT_1_14_2:
case MINECRAFT_1_14_3:
case MINECRAFT_1_14_4:
return 5;
case MINECRAFT_1_15:
case MINECRAFT_1_15_1:
case MINECRAFT_1_15_2:
case MINECRAFT_1_16:
case MINECRAFT_1_16_1:
case MINECRAFT_1_16_2:
case MINECRAFT_1_16_3:
case MINECRAFT_1_16_4:
return 6;
case MINECRAFT_1_17:
case MINECRAFT_1_17_1:
case MINECRAFT_1_18:
case MINECRAFT_1_18_2:
return 7;
case MINECRAFT_1_19:
case MINECRAFT_1_19_1:
case MINECRAFT_1_19_3:
return 8;
case MINECRAFT_1_19_4:
case MINECRAFT_1_20:
case MINECRAFT_1_20_2:
case MINECRAFT_1_20_3:
return 9;
case MINECRAFT_1_20_5:
case MINECRAFT_1_21:
return 10;
if (protocolVersion.compareTo(MINECRAFT_1_13_2) <= 0) {
return 1;
}
if (protocolVersion.compareTo(MINECRAFT_1_14_4) <= 0) {
return 5;
}
if (protocolVersion.compareTo(MINECRAFT_1_16_4) <= 0) {
return 6;
}
if (protocolVersion.compareTo(MINECRAFT_1_18_2) <= 0) {
return 7;
}
if (protocolVersion.compareTo(MINECRAFT_1_19_3) <= 0) {
return 8;
}
if (protocolVersion.compareTo(MINECRAFT_1_20_3) <= 0) {
return 9;
}
return 10;
}),
MINECART(protocolVersion -> {
if (protocolVersion.compareTo(MINECRAFT_1_13_2) <= 0) {
return 10;
}
if (protocolVersion.compareTo(MINECRAFT_1_14_4) <= 0) {
return 41;
}
if (protocolVersion.compareTo(MINECRAFT_1_15_2) <= 0) {
return 42;
}
if (protocolVersion.compareTo(MINECRAFT_1_16_4) <= 0) {
return 45;
}
if (protocolVersion.compareTo(MINECRAFT_1_18_2) <= 0) {
return 50;
}
if (protocolVersion.compareTo(MINECRAFT_1_19_1) <= 0) {
return 53;
}
if (protocolVersion.compareTo(MINECRAFT_1_19_3) <= 0) {
return 54;
}
if (protocolVersion.compareTo(MINECRAFT_1_20_2) <= 0) {
return 64;
}
if (protocolVersion.compareTo(MINECRAFT_1_20_3) <= 0) {
return 65;
}
return 69;
});
private final Function<ProtocolVersion, Integer> function;
@@ -52,6 +52,10 @@ public final class LoginSuccessPacket implements FallbackPacket {
writeString(byteBuf, FastUuidSansHyphens.toString(uuid));
}
// Cap the length of the username to 16 characters
if (username.length() > 16) {
username = username.substring(0, 16);
}
writeString(byteBuf, username);
if (protocolVersion.compareTo(MINECRAFT_1_19) >= 0) {
@@ -0,0 +1,97 @@
/*
* 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.*;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_8;
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.readVarInt;
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@ToString(of = {"entityId", "hand"})
public final class AnimationPacket implements FallbackPacket {
private int entityId = -1;
private int hand = MAIN_HAND;
private LegacyAnimationType type = LegacyAnimationType.SWING_ARM;
public static final int MAIN_HAND = 0;
@Override
public void encode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) throws Exception {
throw new UnsupportedOperationException();
}
@Override
public void decode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) throws Exception {
if (protocolVersion.compareTo(MINECRAFT_1_8) < 0) {
entityId = byteBuf.readInt();
type = LegacyAnimationType.getById(byteBuf.readByte());
} else if (protocolVersion.compareTo(MINECRAFT_1_8) > 0) {
// Only 1.9+ clients have an offhand
hand = readVarInt(byteBuf);
}
}
@Getter
public enum LegacyAnimationType {
NO_ANIMATION,
SWING_ARM,
DAMAGE_ANIMATION,
LEAVE_BED,
EAT_FOOD,
CRITICAL_EFFECT,
MAGIC_CRITICAL_EFFECT,
UNKNOWN(102),
CROUCH(104),
@SuppressWarnings("SpellCheckingInspection")
UNCROUCH(105);
private final int id;
LegacyAnimationType(int id) {
this.id = id;
}
LegacyAnimationType() {
this.id = ordinal();
}
public static LegacyAnimationType getById(int id) {
if (id >= 0 && id <= 7) {
return LegacyAnimationType.values()[id];
}
switch (id) {
case 102:
return UNKNOWN;
case 104:
return CROUCH;
case 105:
return UNCROUCH;
default:
throw new IllegalArgumentException("Unknown type with id: " + id);
}
}
}
}
@@ -36,7 +36,7 @@ import static xyz.jonesdev.sonar.common.util.ProtocolUtil.*;
@NoArgsConstructor
@AllArgsConstructor
public final class ChunkDataPacket implements FallbackPacket {
private int x, z;
private int sectionX, sectionZ;
private static final byte[] SECTION_BYTES = new byte[]{0, 0, 0, 0, 0, 0, 1, 0};
private static final byte[] LIGHT_BYTES = new byte[]{1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 3, -1, -1, 0, 0};
@@ -67,8 +67,8 @@ public final class ChunkDataPacket implements FallbackPacket {
@Override
public void encode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) throws Exception {
byteBuf.writeInt(x);
byteBuf.writeInt(z);
byteBuf.writeInt(sectionX);
byteBuf.writeInt(sectionZ);
if (protocolVersion.compareTo(MINECRAFT_1_17) >= 0) {
if (protocolVersion.compareTo(MINECRAFT_1_17_1) <= 0) {
@@ -113,13 +113,13 @@ public final class ChunkDataPacket implements FallbackPacket {
if (protocolVersion.compareTo(MINECRAFT_1_13) < 0) {
if (protocolVersion.compareTo(MINECRAFT_1_8) >= 0) {
writeArray(byteBuf, LEGACY_FILLER_BYTES); // 1.8 - 1.12.2
writeByteArray(byteBuf, LEGACY_FILLER_BYTES); // 1.8 - 1.12.2
} else {
byteBuf.writeInt(0); // compressed size
byteBuf.writeBytes(LEGACY_FILLER_BYTES_17); // 1.7
}
} else if (protocolVersion.compareTo(MINECRAFT_1_15) < 0) {
writeArray(byteBuf, MODERN_FILLER_BYTES); // 1.13 - 1.14.4
writeByteArray(byteBuf, MODERN_FILLER_BYTES); // 1.13 - 1.14.4
} else if (protocolVersion.compareTo(MINECRAFT_1_18) < 0) {
writeVarInt(byteBuf, 0); // 1.15 - 1.17.1
} else {
@@ -0,0 +1,59 @@
/*
* 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.Setter;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.writeVarInt;
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public final class EntityAnimationPacket implements FallbackPacket {
private int entityId;
private Type type;
@Override
public void encode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) throws Exception {
writeVarInt(byteBuf, entityId);
byteBuf.writeByte(type.ordinal());
}
@Override
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) throws Exception {
throw new UnsupportedOperationException();
}
public enum Type {
SWING_MAIN_ARM,
HURT,
WAKE_UP,
// 1.9+?
SWING_OFF_HAND, // Eat food on 1.7
CRITICAL_HIT,
MAGIC_CRITICAL_HIT
// unknown (102), crouch (104), uncrouch(105) only exist on 1.7 and unused here
}
}
@@ -104,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);
@@ -122,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
}
}
@@ -26,6 +26,8 @@ import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_8;
@Getter
@ToString
@NoArgsConstructor
@@ -43,7 +45,8 @@ public final class PlayerInputPacket implements FallbackPacket {
public void decode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) throws Exception {
sideways = byteBuf.readFloat();
forward = byteBuf.readFloat();
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_8) < 0) {
if (protocolVersion.compareTo(MINECRAFT_1_8) < 0) {
jump = byteBuf.readBoolean();
unmount = byteBuf.readBoolean();
} else {
@@ -23,10 +23,10 @@ import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
import net.kyori.adventure.nbt.CompoundBinaryTag;
import net.kyori.adventure.nbt.IntBinaryTag;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.captcha.ItemType;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.*;
@@ -36,16 +36,13 @@ import static xyz.jonesdev.sonar.common.util.ProtocolUtil.*;
@NoArgsConstructor
@AllArgsConstructor
public final class SetContainerSlotPacket implements FallbackPacket {
private int slot, count, itemId;
private int windowId, slot, count;
private ItemType itemType;
private CompoundBinaryTag compoundBinaryTag;
public static final CompoundBinaryTag MAP_NBT = CompoundBinaryTag.builder()
.put("map", IntBinaryTag.intBinaryTag(0)) // map type
.build();
@Override
public void encode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) throws Exception {
byteBuf.writeByte(0); // windowId
byteBuf.writeByte(windowId);
if (protocolVersion.compareTo(MINECRAFT_1_17_1) >= 0) {
writeVarInt(byteBuf, 0);
@@ -62,9 +59,9 @@ public final class SetContainerSlotPacket implements FallbackPacket {
}
if (protocolVersion.compareTo(MINECRAFT_1_13_2) < 0) {
byteBuf.writeShort(itemId);
byteBuf.writeShort(itemType.getId(protocolVersion));
} else {
writeVarInt(byteBuf, itemId);
writeVarInt(byteBuf, itemType.getId(protocolVersion));
}
if (protocolVersion.compareTo(MINECRAFT_1_20_5) < 0) {
@@ -86,7 +83,7 @@ public final class SetContainerSlotPacket implements FallbackPacket {
writeCompoundTag(byteBuf, compoundBinaryTag);
} else if (protocolVersion.compareTo(MINECRAFT_1_20_5) < 0) {
writeNamelessCompoundTag(byteBuf, compoundBinaryTag);
} else { // 1.20.5
} else { // 1.20.5+
// TODO: find a way to improve this
// component
writeVarInt(byteBuf, 1); // component count to add
@@ -26,6 +26,7 @@ import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_8;
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.writeVarInt;
@Getter
@@ -40,7 +41,7 @@ public final class SetExperiencePacket implements FallbackPacket {
@Override
public void encode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) throws Exception {
byteBuf.writeFloat(experienceBar);
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_8) < 0) {
if (protocolVersion.compareTo(MINECRAFT_1_8) < 0) {
byteBuf.writeShort(level);
byteBuf.writeShort(totalExperience);
} else {
@@ -26,6 +26,7 @@ import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_9;
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.writeVarInt;
@Getter
@@ -39,7 +40,7 @@ public final class SetPassengersPacket implements FallbackPacket {
public void encode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) throws Exception {
// You can find this in the EntityAttach packet,
// which was later replaced by SetPassengers in 1.9+
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_9) < 0) {
if (protocolVersion.compareTo(MINECRAFT_1_9) < 0) {
byteBuf.writeInt(passengerId);
byteBuf.writeInt(entityId);
byteBuf.writeByte(0); // leash
@@ -36,7 +36,7 @@ import static xyz.jonesdev.sonar.common.util.ProtocolUtil.writeVarInt;
public final class SetPlayerPositionRotationPacket implements FallbackPacket {
private double x, y, z;
private float yaw, pitch;
private int teleportId;
private int teleportId, relativeMask;
private boolean onGround;
@Override
@@ -46,7 +46,7 @@ public final class SetPlayerPositionRotationPacket implements FallbackPacket {
byteBuf.writeDouble(z);
byteBuf.writeFloat(yaw);
byteBuf.writeFloat(pitch);
byteBuf.writeBoolean(onGround);
byteBuf.writeByte(relativeMask);
if (protocolVersion.compareTo(MINECRAFT_1_8) > 0) {
writeVarInt(byteBuf, teleportId);
@@ -63,7 +63,7 @@ public final class SetPlayerPositionRotationPacket implements FallbackPacket {
x = byteBuf.readDouble();
y = byteBuf.readDouble();
if (protocolVersion.compareTo(MINECRAFT_1_8) < 0) {
y = byteBuf.readDouble(); // Account 1.7 bounding box
y = byteBuf.readDouble(); // Account for 1.7 bounding box
}
z = byteBuf.readDouble();
yaw = byteBuf.readFloat();
@@ -0,0 +1,72 @@
/*
* 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 static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_8;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_9;
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.writeVarInt;
@Getter
@ToString
@NoArgsConstructor
@AllArgsConstructor
public final class TeleportEntityPacket implements FallbackPacket {
private int entityId;
private double x, y, z;
private boolean onGround;
@Override
public void encode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
if (protocolVersion.compareTo(MINECRAFT_1_8) >= 0) {
writeVarInt(byteBuf, entityId);
} else {
byteBuf.writeInt(entityId);
}
if (protocolVersion.compareTo(MINECRAFT_1_9) >= 0) {
byteBuf.writeDouble(x);
byteBuf.writeDouble(y);
byteBuf.writeDouble(z);
} else {
byteBuf.writeInt((int) (x * 32D));
byteBuf.writeInt((int) (y * 32D));
byteBuf.writeInt((int) (z * 32D));
}
byteBuf.writeByte(0); // pitch or yaw
byteBuf.writeByte(0); // yaw or pitch
if (protocolVersion.compareTo(MINECRAFT_1_8) >= 0) {
byteBuf.writeBoolean(onGround);
}
}
@Override
public void decode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) throws Exception {
throw new UnsupportedOperationException();
}
}
@@ -54,7 +54,7 @@ public final class UpdateSectionBlocksPacket implements FallbackPacket {
for (final BlockUpdate block : blockUpdates) {
byteBuf.writeShort(block.getLegacyBlockState());
final int blockId = block.getType().getId(protocolVersion);
final int blockId = block.getBlockType().getId(protocolVersion);
if (protocolVersion.compareTo(MINECRAFT_1_13) >= 0) {
writeVarInt(byteBuf, blockId);
} else {
@@ -82,7 +82,7 @@ public final class UpdateSectionBlocksPacket implements FallbackPacket {
for (final BlockUpdate block : blockUpdates) {
// https://wiki.vg/Protocol#Update_Section_Blocks
final int shiftedBlockId = block.getType().getId(protocolVersion) << 12;
final int shiftedBlockId = block.getBlockType().getId(protocolVersion) << 12;
final long positionIdValue = shiftedBlockId | block.getBlockState();
writeVarLong(byteBuf, positionIdValue);
}
@@ -0,0 +1,54 @@
/*
* 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;
@Getter
@ToString
@NoArgsConstructor
@AllArgsConstructor
public final class VehicleMovePacket implements FallbackPacket {
private double x, y, z;
private float yaw, pitch;
@Override
public void encode(final @NotNull ByteBuf byteBuf, final ProtocolVersion protocolVersion) throws Exception {
byteBuf.writeDouble(x);
byteBuf.writeDouble(y);
byteBuf.writeDouble(z);
byteBuf.writeFloat(yaw);
byteBuf.writeFloat(pitch);
}
@Override
public void decode(final @NotNull ByteBuf byteBuf, final ProtocolVersion protocolVersion) throws Exception {
x = byteBuf.readDouble();
y = byteBuf.readDouble();
z = byteBuf.readDouble();
yaw = byteBuf.readFloat();
pitch = byteBuf.readFloat();
}
}
@@ -1,153 +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.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.common.fallback.protocol.FallbackPacketDecoder;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketEncoder;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketListener;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.ClientInformationPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.PluginMessagePacket;
import xyz.jonesdev.sonar.common.statistics.GlobalSonarStatistics;
import java.nio.charset.StandardCharsets;
import java.util.Random;
import java.util.regex.Pattern;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
import static xyz.jonesdev.sonar.common.fallback.FallbackUserWrapper.closeWith;
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer.transferToOrigin;
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.BRAND_CHANNEL;
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.BRAND_CHANNEL_LEGACY;
@RequiredArgsConstructor
public abstract class FallbackSessionHandler implements FallbackPacketListener {
protected final FallbackUser user;
protected final String username;
protected static final Random RANDOM = new Random();
protected void checkState(final boolean state, final String failReason) {
// Fails the verification if the condition is not met
if (!state) {
user.fail(failReason);
}
}
protected final void finishVerification() {
GlobalSonarStatistics.totalSuccessfulVerifications++;
// Add verified player to the database
Sonar.get().getVerifiedPlayerController().add(new VerifiedPlayer(
user.getInetAddress(), user.getOfflineUuid(), user.getLoginTimer().getStart()));
// Call the VerifySuccessEvent for external API usage
Sonar.get().getEventManager().publish(new UserVerifySuccessEvent(
username, user.getOfflineUuid(), user, user.getLoginTimer().delay()));
// If enabled, transfer the player back to the origin server.
// This feature was introduced by Mojang in Minecraft version 1.20.5.
if (transferToOrigin != null
&& user.getProtocolVersion().compareTo(MINECRAFT_1_20_5) >= 0) {
// Send the transfer packet to the player (and close the channel if on Java Edition)
if (user.isGeyser()) {
user.write(transferToOrigin);
// Make sure we cannot receive any more packets from the player
user.getPipeline().remove(FallbackPacketDecoder.class);
user.getPipeline().remove(FallbackPacketEncoder.class);
} else {
closeWith(user.getChannel(), user.getProtocolVersion(), transferToOrigin);
}
} else {
// Disconnect player with the verification success message
user.disconnect(Sonar.get().getConfig().getVerification().getVerificationSuccess());
}
Sonar.get().getFallback().getLogger().info(
Sonar.get().getConfig().getMessagesConfig().getString("verification.logs.successful")
.replace("<username>", username)
.replace("<time-taken>", user.getLoginTimer().toString()));
}
protected final void checkClientInformation(final @NotNull ClientInformationPacket clientSettings) {
// Ensure that the client locale is correct
validateClientLocale(clientSettings.getLocale());
// Check if the player sent an unused bit flag in the skin section
checkState((clientSettings.getSkinParts() & 0x80) == 0, "sent unused bit flag");
// Mark the ClientSettings packet as received
user.setReceivedClientSettings(true);
}
protected final void checkPluginMessage(final @NotNull PluginMessagePacket pluginMessage) {
final boolean usingModernChannel = pluginMessage.getChannel().equals(BRAND_CHANNEL);
final boolean usingLegacyChannel = pluginMessage.getChannel().equals(BRAND_CHANNEL_LEGACY);
// Skip this payload if it does not contain client brand information
if (!usingModernChannel && !usingLegacyChannel) {
return;
}
// Make sure the player isn't sending the client brand multiple times
checkState(!user.isReceivedPluginMessage(), "sent duplicate plugin message");
// Check if the channel is correct - 1.13 uses the new namespace
// system ('minecraft:' + channel) and anything below 1.13 uses
// the legacy namespace system ('MC|' + channel).
final boolean v1_13 = user.getProtocolVersion().compareTo(MINECRAFT_1_13) >= 0;
checkState(usingLegacyChannel || v1_13, "invalid brand channel");
// Validate the client branding using a regex to filter unwanted characters.
if (Sonar.get().getConfig().getVerification().getBrand().isEnabled()) {
validateClientBrand(pluginMessage.getData());
}
// Mark the PluginMessage packet as received
user.setReceivedPluginMessage(true);
}
protected final void validateClientBrand(final byte @NotNull [] data) {
// Check if the client brand is too short. It has to have at least 2 bytes.
checkState(data.length > 1, "client brand is too short");
// Check if the decoded client brand string is too long
checkState(data.length < Sonar.get().getConfig().getVerification().getBrand().getMaxLength(),
"client brand contains too much data: " + data.length);
// https://discord.com/channels/923308209769426994/1116066363887321199/1256929441053933608
String brand = new String(data, StandardCharsets.UTF_8);
// Remove the invalid character at the beginning of the client brand
if (user.getProtocolVersion().compareTo(MINECRAFT_1_8) >= 0) {
brand = brand.substring(1);
}
// Check for illegal client brands
checkState(!brand.equals("Vanilla"), "illegal client brand: " + brand);
// Regex pattern for validating client brands
final Pattern pattern = Sonar.get().getConfig().getVerification().getBrand().getValidRegex();
checkState(pattern.matcher(brand).matches(), "client brand does not match pattern: " + brand);
}
protected final void validateClientLocale(final @NotNull String locale) {
// Check the client locale by performing a simple regex check
// that disallows non-ascii characters by default.
final Pattern pattern = Sonar.get().getConfig().getVerification().getValidLocaleRegex();
checkState(pattern.matcher(locale).matches(), "client locale does not match pattern: " + locale);
}
}
@@ -1,161 +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.session;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketDecoder;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.*;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_8;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_9;
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer.*;
/**
* Flow for this session handler
*
* <li>
* {@link SpawnEntityPacket} and {@link SetPassengersPacket} packets are sent to the client,
* therefore, making the player enter a boat.
* <br>
* The boat will be spawned at y 1337.
* <br>
* After 3 {@link PlayerInputPacket} and {@link PaddleBoatPacket} packets,
* the boat is removed.
* <br>
* Now we can check if the player sends the correct position after the entity is removed.
* Please note that we don't tell the client to exit the vehicle, therefore, possibly confusing some bots.
* Removing the boat results in the player exiting the vehicle on vanilla clients.
* <br>
* See more: {@link FallbackVehicleSessionHandler}
* </li>
* <li>
* Then, we listen for incoming {@link PlayerInputPacket} and {@link PaddleBoatPacket} packets.
* </li>
*/
public final class FallbackVehicleSessionHandler extends FallbackSessionHandler {
public FallbackVehicleSessionHandler(final @NotNull FallbackUser user,
final @NotNull String username,
final boolean forceCAPTCHA) {
super(user, username);
this.forceCAPTCHA = forceCAPTCHA;
// Send the necessary packets to mount the player on the vehicle
user.delayedWrite(spawnEntity);
user.delayedWrite(setPassengers);
user.getChannel().flush();
}
private final boolean forceCAPTCHA;
private int paddlePackets, inputPackets, positionPackets, rotationPackets;
private boolean expectMovement;
private void markSuccess() {
// Pass the player to the next best verification handler
if (forceCAPTCHA || Sonar.get().getFallback().shouldPerformCaptcha()) {
// Either send the player to the CAPTCHA or finish the verification.
final var decoder = user.getPipeline().get(FallbackPacketDecoder.class);
// Send the player to the CAPTCHA handler
decoder.setListener(new FallbackCAPTCHASessionHandler(user, username));
} else {
// The player has passed all checks
finishVerification();
}
}
// We can abuse the entity remove mechanic and check for position packets when the entity dies
private void handleMovement(double y) {
if (user.getProtocolVersion().compareTo(MINECRAFT_1_8) < 0) {
y -= 1.62f; // Account for 1.7 bounding box
}
// Check the Y position of the player
checkState(y <= IN_AIR_Y_POSITION, "invalid y position");
// Mark this check as successful if the player sent a few position packets
if (positionPackets++ > Sonar.get().getConfig().getVerification().getVehicle().getMinimumPackets()) {
markSuccess();
}
}
private void handleRotation() {
// Once the player sent enough packets, go to the next stage
final int minimumPackets = Sonar.get().getConfig().getVerification().getVehicle().getMinimumPackets();
if (paddlePackets > minimumPackets
&& inputPackets > minimumPackets
&& rotationPackets > minimumPackets) {
// Remove the entity
// The next y coordinate the player will send is going
// to be the vehicle spawn position (y 64 in this case).
user.write(removeEntities);
// Listen for next movement packet(s)
expectMovement = true;
}
rotationPackets++;
}
@Override
public void handle(final @NotNull FallbackPacket packet) {
if (packet instanceof SetPlayerPositionRotationPacket) {
if (expectMovement) {
final SetPlayerPositionRotationPacket posRot = (SetPlayerPositionRotationPacket) packet;
handleMovement(posRot.getY());
}
} else if (packet instanceof SetPlayerPositionPacket) {
if (expectMovement) {
final SetPlayerPositionPacket position = (SetPlayerPositionPacket) packet;
handleMovement(position.getY());
}
} else if (packet instanceof SetPlayerRotationPacket) {
if (!expectMovement && !user.isGeyser()) {
handleRotation();
}
} else if (packet instanceof PaddleBoatPacket) {
paddlePackets++;
} else if (packet instanceof PlayerInputPacket && !expectMovement) {
final PlayerInputPacket playerInput = (PlayerInputPacket) packet;
// Check if the player is sending invalid vehicle speed values
final float forward = Math.abs(playerInput.getForward());
final float sideways = Math.abs(playerInput.getSideways());
final float maxVehicleSpeed = user.isGeyser() ? 1 : 0.98f;
checkState(forward <= maxVehicleSpeed, "illegal speed (f): " + forward);
checkState(sideways <= maxVehicleSpeed, "illegal speed (s): " + sideways);
// Only mark this packet as correct if the player is not moving the vehicle
if (playerInput.isJump() || playerInput.isUnmount()) {
return;
}
// Bedrock users do not send SetPlayerPositionRotation and SetPlayerRotation packets
// Don't ask me why; Microsoft is doing some *fascinating* things with Bedrock...
if (user.isGeyser()) {
handleRotation();
}
// 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++;
}
inputPackets++;
}
}
}
@@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.session;
package xyz.jonesdev.sonar.common.fallback.verification;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.Sonar;
@@ -24,29 +24,18 @@ import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
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.fallback.protocol.packets.play.SetContainerSlotPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.SetPlayerPositionPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.SetPlayerPositionRotationPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.SystemChatPacket;
import xyz.jonesdev.sonar.common.util.exception.QuietDecoderException;
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer.*;
/**
* Flow for this session handler
*
* <li>
* {@link SetContainerSlotPacket} and {@link MapDataPacket} packets are sent to the client,
* therefore, setting the player's item to a map with a code on it (CAPTCHA).
* <br>
* See more: {@link FallbackCAPTCHASessionHandler}, {@link MapCaptchaInfo}
* </li>
* <li>
* Then, we wait for the player to enter the {@link FallbackCAPTCHASessionHandler#answer} in chat.
* </li>
*/
public final class FallbackCAPTCHASessionHandler extends FallbackSessionHandler {
public final class FallbackCaptchaHandler extends FallbackVerificationHandler {
public FallbackCAPTCHASessionHandler(final @NotNull FallbackUser user,
final @NotNull String username) {
super(user, username);
public FallbackCaptchaHandler(final @NotNull FallbackUser user) {
super(user);
// Disconnect the player if there is no CAPTCHA available at the moment
if (!CaptchaPreparer.isCaptchaAvailable()) {
@@ -58,8 +47,8 @@ public final class FallbackCAPTCHASessionHandler extends FallbackSessionHandler
// If the player is on Java, set the 5th slot (ID 4) in the player's hotbar to the map
// If the player is on Bedrock, set the 1st slot (ID 0) in the player's hotbar to the map
user.delayedWrite(new SetContainerSlotPacket(user.isGeyser() ? 36 : 40, 1,
ItemType.FILLED_MAP.getId(user.getProtocolVersion()), SetContainerSlotPacket.MAP_NBT));
final int slotId = user.isGeyser() ? 36 : 40;
user.delayedWrite(new SetContainerSlotPacket(0, slotId, 1, ItemType.FILLED_MAP, MAP_ITEM_NBT));
// Send random captcha to the player
final MapCaptchaInfo captcha = CaptchaPreparer.getRandomCaptcha();
this.answer = captcha.getAnswer().toLowerCase();
@@ -67,7 +56,7 @@ public final class FallbackCAPTCHASessionHandler extends FallbackSessionHandler
// Teleport the player to the position above the platform
user.delayedWrite(CAPTCHA_POSITION);
// Make sure the player cannot move
user.delayedWrite(user.isGeyser() ? CAPTCHA_ABILITIES_BEDROCK : CAPTCHA_ABILITIES);
user.delayedWrite(user.isGeyser() ? NO_MOVE_ABILITIES_BEDROCK : NO_MOVE_ABILITIES);
// Make sure the player knows that they have to enter the code in chat
user.delayedWrite(enterCodeMessage);
// Send all packets in one flush
@@ -100,9 +89,8 @@ public final class FallbackCAPTCHASessionHandler extends FallbackSessionHandler
if (Sonar.get().getConfig().getVerification().getGamemode().isSurvivalOrAdventure()) {
final long difference = maxDuration - user.getLoginTimer().delay();
final int index = (int) (difference / 1000D);
// Make sure we can actually safely get and send the packet
// Make sure we can safely get and send the packet
if (lastCountdownIndex != index && index >= 0 && xpCountdown.length > index) {
// Send the countdown using the experience bar
user.write(xpCountdown[index]);
}
lastCountdownIndex = index;
@@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.session;
package xyz.jonesdev.sonar.common.fallback.verification;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.Sonar;
@@ -28,17 +28,19 @@ import static xyz.jonesdev.sonar.api.config.SonarConfiguration.Verification.Game
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer.*;
public final class FallbackGravitySessionHandler extends FallbackSessionHandler {
public final class FallbackGravityHandler extends FallbackVerificationHandler {
public FallbackGravitySessionHandler(final @NotNull FallbackUser user,
final @NotNull String username) {
super(user, username);
public FallbackGravityHandler(final @NotNull FallbackUser user,
final @NotNull FallbackPreJoinHandler preJoinHandler) {
super(user);
this.preJoinHandler = preJoinHandler;
// Bedrock users start falling immediately
this.canFall = user.isGeyser();
// We don't want to check Geyser players for valid gravity, as this might cause issues because of the protocol
this.enableGravityCheck = !user.isGeyser() && Sonar.get().getConfig().getVerification().getGravity().isEnabled();
this.enableCollisionsCheck = Sonar.get().getConfig().getVerification().getGravity().isCheckCollisions();
this.enableCollisionsCheck = !user.isGeyser() && Sonar.get().getConfig().getVerification().getGravity().isCheckCollisions();
// FIXME: 1.18.2-1.19.3 weird loading world issue
// First, write the JoinGame packet to the buffer
user.delayedWrite(joinGame);
// Then, write the ClientAbilities packet to the buffer
@@ -46,15 +48,17 @@ public final class FallbackGravitySessionHandler extends FallbackSessionHandler
if (Sonar.get().getConfig().getVerification().getGamemode() == CREATIVE) {
user.delayedWrite(DEFAULT_ABILITIES);
}
// Write the PositionLook packet to the buffer
user.delayedWrite(spawnPosition);
// Write the DefaultSpawnPosition packet to the buffer
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);
// Teleport the player to the position where we're starting to check them
if (user.getProtocolVersion().compareTo(MINECRAFT_1_8) >= 0) {
user.delayedWrite(spawnPosition);
user.delayedWrite(fallStartPosition);
} else {
// 1.7.2-1.7.10 clients do not have relative teleports
user.delayedWrite(fallStartPositionLegacy);
}
// 1.20.3+ introduced game events
// Make sure the client knows that we're sending chunks next
@@ -64,42 +68,37 @@ public final class FallbackGravitySessionHandler extends FallbackSessionHandler
// Teleport player into an empty world by sending an empty chunk packet
user.delayedWrite(EMPTY_CHUNK_DATA);
// Spawn the invisible platform below the player
if (this.enableCollisionsCheck) {
user.delayedWrite(updateSectionBlocks);
if (enableCollisionsCheck) {
final int index = RANDOM.nextInt(BLOCKS_PACKETS.length);
blockHeight = POSSIBLE_BLOCK_TYPES[index].getBlockHeight();
user.delayedWrite(BLOCKS_PACKETS[index]);
}
// Send all packets at once
user.getChannel().flush();
// 1.8 and below don't have TeleportConfirm packets, which is why we're skipping that check.
if (user.getProtocolVersion().compareTo(MINECRAFT_1_9) < 0) {
markTeleported();
// Enable the movement checks
teleported = true;
}
}
private final FallbackPreJoinHandler preJoinHandler;
private final boolean enableGravityCheck, enableCollisionsCheck;
private boolean teleported, checkMovement;
private double y, deltaY;
private int movementTick;
private boolean teleported, canFall, checkMovement;
private double y, deltaY, blockHeight;
private int movementTick, expectedTeleportId = FIRST_TELEPORT_ID;
private void markTeleported() {
// Activate the movement checks
teleported = true;
}
private void markSuccess(final boolean forceCAPTCHA) {
private void markSuccess() {
// Force-stop the movement checks
teleported = false;
// Check if the player sent all necessary packets
checkState(user.isReceivedClientSettings(), "didn't send client settings");
// Don't check Geyser players for plugin messages
if (!user.isGeyser()) {
checkState(user.isReceivedPluginMessage(), "didn't send plugin message");
// Exempt pre-1.20.2 since they've already passed that check in the configuration phase
if (user.getProtocolVersion().compareTo(MINECRAFT_1_20_2) < 0) {
preJoinHandler.checkClientInformation();
}
// Send the player to the protocol check
// Send the player to the next verification handler
final var decoder = user.getPipeline().get(FallbackPacketDecoder.class);
decoder.setListener(new FallbackProtocolSessionHandler(user, username, forceCAPTCHA));
decoder.setListener(new FallbackProtocolHandler(user));
}
@Override
@@ -123,38 +122,34 @@ public final class FallbackGravitySessionHandler extends FallbackSessionHandler
checkState(!teleported, "duplicate teleport confirm");
// Check if the teleport ID matches the expected ID
final int teleportId = teleportConfirm.getTeleportId();
checkState(teleportId == TELEPORT_ID,
"expected TP ID " + TELEPORT_ID + ", but got " + teleportId);
checkState(teleportId == expectedTeleportId,
"expected TP ID " + expectedTeleportId + ", but got " + teleportId);
markTeleported();
}
// Make sure to catch all ClientSettings and PluginMessage packets during the play phase.
else if (packet instanceof ClientInformationPacket) {
// Let the session handler itself know about this packet
checkClientInformation((ClientInformationPacket) packet);
} else if (packet instanceof PluginMessagePacket) {
// Let the session handler itself know about this packet
checkPluginMessage((PluginMessagePacket) packet);
// The first teleport ID is not useful for us in this context, skip it
if (expectedTeleportId == FIRST_TELEPORT_ID) {
expectedTeleportId = SECOND_TELEPORT_ID;
} else {
// Enable the movement checks
teleported = true;
}
} else if (packet instanceof ClientInformationPacket
|| packet instanceof PluginMessagePacket) {
// Pass these packets back to the login handler
preJoinHandler.handle(packet);
}
}
private void handleMovement(final double x, final double y, final double z, final boolean isOnGround) {
// Check if the client hasn't moved before sending the first movement packet
if (!checkMovement) {
// No need to continue checking if the gravity and collision checks are disabled
// Continue with the next stage of the verification
if (!enableGravityCheck && !enableCollisionsCheck) {
markSuccess(false);
markSuccess();
return;
}
if (x == SPAWN_X_POSITION && z == SPAWN_Z_POSITION) {
// Now, once we verified the X and Z position, we can safely check for gravity
checkMovement = true;
movementTick = 0;
// Synchronize the Y coordinate
this.y = dynamicSpawnYPosition;
}
// Synchronize the Y coordinate
this.y = dynamicSpawnYPosition;
checkMovement = true;
return;
}
@@ -166,81 +161,91 @@ 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={}, {}",
username, x, y, z, lastY, deltaY, isOnGround);
Sonar.get().getLogger().info("{}: {}/{}/{} ly={}, dy={}, h={}, g={}",
user.getUsername(), x, y, z, lastY, deltaY, blockHeight, isOnGround);
}
// Ensure that the player's Y coordinate is above the collision platform
checkState(y >= DEFAULT_Y_COLLIDE_POSITION,
"fell through blocks: " + y + "/" + deltaY + "/" + movementTick);
// The player is not allowed to move away from the collision platform.
// This should not happen unless the max movement tick is configured to a high number.
checkState(Math.abs(x - BLOCKS_PER_ROW) < BLOCKS_PER_ROW, "moved too far (x)");
checkState(Math.abs(z - BLOCKS_PER_ROW) < BLOCKS_PER_ROW, "moved too far (z)");
checkState(Math.abs(Math.abs(x) - BLOCKS_PER_ROW) < BLOCKS_PER_ROW, "illegal x offset: " + x);
checkState(Math.abs(Math.abs(z) - BLOCKS_PER_ROW) < BLOCKS_PER_ROW, "illegal z offset: " + z);
if (!isOnGround) {
// The deltaY is 0 whenever the player sends their first position packet.
// We have to account for this or the player will fail the verification.
// We have to account for this or the player will falsely fail the verification.
if (deltaY == 0) {
// Reset the movement tick as a safety measure
movementTick = 0;
checkState(movementTick == 0, "illegal y motion: " + movementTick);
// 1.7 clients immediately start falling after this packet
if (user.getProtocolVersion().compareTo(MINECRAFT_1_8) < 0) {
movementTick++;
}
// We've received the first position packet; the player will now start falling
canFall = true;
return;
}
// Make sure we're actually expecting the player to fall at this point in time
checkState(canFall, "unexpected y motion: " + deltaY);
// The movement tick should not be ignored when gravity check is disabled.
movementTick++;
if (enableGravityCheck) {
// Ensure that the player is above the collision platform
checkState(y > DEFAULT_Y_COLLIDE_POSITION,
"fell through blocks; y: " + y + " deltaY: " + deltaY + " tick: " + movementTick);
// Predict the player's current motion based on the last motion
// https://minecraft.wiki/w/Entity#Motion_of_entities
final double predicted = (lastDeltaY - 0.08) * 0.98f;
final double difference = Math.abs(deltaY - predicted);
// Check if the difference between the predicted motion and
// the actual motion is greater than our minimum threshold.
// Check if the difference between the predicted and actual motion is too large
if (difference > 1e-7) {
// Do not throw an exception if the user configured to display the CAPTCHA instead
if (Sonar.get().getConfig().getVerification().getGravity().isCaptchaOnFail()) {
markSuccess(true);
user.setForceCaptcha(true);
markSuccess();
return;
}
user.fail("incorrect gravity; predicted: " + predicted + " deltaY: " + deltaY + " y: " + y);
fail("incorrect gravity: " + predicted + "/ " + deltaY + "/" + y);
}
// The player is obeying gravity, go on to the next stage if the collision check is disabled.
if (movementTick == maxMovementTick && !enableCollisionsCheck) {
markSuccess(false);
if (!enableCollisionsCheck && movementTick == maxMovementTick) {
markSuccess();
}
}
} else if (enableCollisionsCheck) {
// Make sure the player has actually moved before reaching the platform
if (movementTick < maxMovementTick) {
if (enableGravityCheck && movementTick < maxMovementTick) {
// Do not throw an exception if the user configured to display the CAPTCHA instead
if (Sonar.get().getConfig().getVerification().getGravity().isCaptchaOnFail()) {
markSuccess(true);
user.setForceCaptcha(true);
markSuccess();
return;
}
user.fail("illegal collision tick; tick: " + movementTick);
fail("illegal collision tick: " + movementTick + "/" + blockHeight);
}
// Calculate the difference between the player's Y coordinate and the expected Y coordinate
double collisionOffsetY = (DEFAULT_Y_COLLIDE_POSITION + blockType.getBlockHeight()) - y;
double collisionOffsetY = (DEFAULT_Y_COLLIDE_POSITION + blockHeight) - y;
// 1.7 sends the head position instead of the AABB minY
// This little hack accounts for the offset of approximately 1.62
if (user.getProtocolVersion().compareTo(MINECRAFT_1_8) < 0) {
collisionOffsetY += 1.62f;
}
// Make sure the player is actually colliding with the blocks and not only spoofing ground
if (collisionOffsetY < -0.03) {
if (collisionOffsetY < -3e-2) {
// Do not throw an exception if the user configured to display the CAPTCHA instead
if (Sonar.get().getConfig().getVerification().getGravity().isCaptchaOnFail()) {
markSuccess(true);
user.setForceCaptcha(true);
markSuccess();
return;
}
user.fail("illegal collision; offset: " + collisionOffsetY + " tick: " + movementTick + " y: " + y);
fail("illegal collision: " + collisionOffsetY + "/" + y + "/" + movementTick + "/" + blockHeight);
}
// The player has collided with the blocks, go on to the next stage
markSuccess(false);
markSuccess();
}
}
}
@@ -15,9 +15,10 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.session;
package xyz.jonesdev.sonar.common.fallback.verification;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketDecoder;
@@ -29,39 +30,19 @@ import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.ClientInformatio
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.KeepAlivePacket;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.PluginMessagePacket;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer.*;
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.BRAND_CHANNEL;
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.BRAND_CHANNEL_LEGACY;
/**
* Flow for this session handler
*
* <li>
* A {@link KeepAlivePacket} packet is sent to the client*
* <br>
* See more: {@link FallbackLoginSessionHandler#initialize18()}
* </li>
* <li>
* After the KeepAlive packet is validated, the client enters the configuration phase.**
* </li>
* <li>
* Then, the session handler is set to the {@link FallbackGravitySessionHandler}.
* <br>
* See more: {@link FallbackLoginSessionHandler#markSuccess()}
* </li>
* <br>
* * The KeepAlive check is skipped on 1.7, as KeepAlive packets don't exist during the LOGIN state.
* <br>
* ** The internals of the configuration phase were not mentioned.
* Find out more about the client configuration and registry synchronization:
* {@link #synchronizeClientRegistry()}, {@link #updateEncoderDecoderState(FallbackPacketRegistry)}
*/
public final class FallbackLoginSessionHandler extends FallbackSessionHandler {
public class FallbackPreJoinHandler extends FallbackVerificationHandler {
public FallbackLoginSessionHandler(final @NotNull FallbackUser user,
final @NotNull String username) {
super(user, username);
public FallbackPreJoinHandler(final @NotNull FallbackUser user) {
super(user);
// Start initializing the actual join process for pre-1.20.2 clients
if (user.getProtocolVersion().compareTo(MINECRAFT_1_20_2) < 0) {
@@ -70,11 +51,12 @@ public final class FallbackLoginSessionHandler extends FallbackSessionHandler {
if (user.getProtocolVersion().compareTo(MINECRAFT_1_8) < 0) {
user.getChannel().eventLoop().schedule(this::markSuccess, 100L, TimeUnit.MILLISECONDS);
} else {
initialize18();
sendKeepAlive();
}
}
}
protected boolean receivedClientInfo, receivedClientBrand;
private boolean acknowledgedLogin;
private int expectedKeepAliveId;
@@ -83,31 +65,35 @@ public final class FallbackLoginSessionHandler extends FallbackSessionHandler {
* is active and legitimate, thereby preventing bot connections that
* could flood the server with login attempts and other unwanted traffic.
*/
private void initialize18() {
private void sendKeepAlive() {
// Send a KeepAlive packet with a random ID
expectedKeepAliveId = RANDOM.nextInt();
user.write(new KeepAlivePacket(expectedKeepAliveId));
}
private void markSuccess() {
// Make sure we can actually switch over to the next check
updateEncoderDecoderState(FallbackPacketRegistry.GAME);
// Pass the player to the next verification handler
final FallbackGravitySessionHandler gravitySessionHandler = new FallbackGravitySessionHandler(user, username);
final var decoder = user.getPipeline().get(FallbackPacketDecoder.class);
decoder.setListener(gravitySessionHandler);
}
private void markAcknowledged() {
acknowledgedLogin = true;
// Update state, so we're able to send/receive configuration packets
updateEncoderDecoderState(FallbackPacketRegistry.CONFIG);
synchronizeClientRegistry();
// Write the FinishConfiguration packet to the buffer
user.delayedWrite(FINISH_CONFIGURATION);
// Send all packets in one flush
user.getChannel().flush();
// Set decoder state to actually catch all packets
updateEncoderDecoderState(FallbackPacketRegistry.CONFIG);
}
protected final void checkClientInformation() {
checkState(receivedClientInfo, "didn't send client settings");
// Don't check Geyser players for plugin messages as they don't have them
if (!user.isGeyser()) {
checkState(receivedClientBrand, "didn't send plugin message");
}
}
private void markSuccess() {
// Pass the player to the next verification handler
final var decoder = user.getPipeline().get(FallbackPacketDecoder.class);
decoder.setListener(new FallbackGravityHandler(user, this));
}
@Override
@@ -125,8 +111,7 @@ public final class FallbackLoginSessionHandler extends FallbackSessionHandler {
// while the player is in the "Downloading terrain" screen.
expectedKeepAliveId = 0;
// Spawn the player in the virtual world if the client does not need
// any configuration (pre-1.20.2).
// Immediately verify the player if they do not need any configuration (pre-1.20.2)
if (user.getProtocolVersion().compareTo(MINECRAFT_1_20_2) < 0) {
markSuccess();
}
@@ -135,21 +120,52 @@ public final class FallbackLoginSessionHandler extends FallbackSessionHandler {
checkState(!acknowledgedLogin, "sent duplicate login ack");
markAcknowledged();
} else if (packet instanceof FinishConfigurationPacket) {
checkClientInformation();
// Update the encoder and decoder state because we're currently in the CONFIG state
updateEncoderDecoderState(FallbackPacketRegistry.GAME);
markSuccess();
}
// Make sure to catch all ClientSettings and PluginMessage packets during the configuration phase.
else if (packet instanceof ClientInformationPacket) {
// Let the session handler itself know about this packet
checkClientInformation((ClientInformationPacket) packet);
} else if (packet instanceof ClientInformationPacket) {
final ClientInformationPacket clientInformation = (ClientInformationPacket) packet;
// Ensure that the client locale is correct
validateClientLocale(clientInformation.getLocale());
// Check if the player sent an unused bit flag in the skin section
// TODO: check if this causes issues with cosmetics in pvp clients
checkState((clientInformation.getSkinParts() & 0x80) == 0,
"sent unused bit flag: " + clientInformation.getSkinParts());
receivedClientInfo = true;
} else if (packet instanceof PluginMessagePacket) {
// Let the session handler itself know about this packet
checkPluginMessage((PluginMessagePacket) packet);
final PluginMessagePacket pluginMessage = (PluginMessagePacket) packet;
final boolean usingModernChannel = pluginMessage.getChannel().equals(BRAND_CHANNEL);
final boolean usingLegacyChannel = pluginMessage.getChannel().equals(BRAND_CHANNEL_LEGACY);
// Skip this payload if it does not contain client brand information
if (!usingModernChannel && !usingLegacyChannel) {
return;
}
// Make sure the player isn't sending the client brand multiple times
checkState(!receivedClientBrand, "sent duplicate plugin message");
// Check if the channel is correct - 1.13 uses the new namespace
// system ('minecraft:' + channel) and anything below 1.13 uses
// the legacy namespace system ('MC|' + channel).
checkState(usingLegacyChannel || user.getProtocolVersion().compareTo(MINECRAFT_1_13) >= 0,
"illegal PluginMessage channel: " + pluginMessage.getChannel());
// Validate the client branding using a regex to filter unwanted characters.
if (Sonar.get().getConfig().getVerification().getBrand().isEnabled()) {
validateClientBrand(pluginMessage.getData());
}
receivedClientBrand = true;
}
}
private void updateEncoderDecoderState(final @NotNull FallbackPacketRegistry registry) {
final var decoder = user.getPipeline().get(FallbackPacketDecoder.class);
final var encoder = (FallbackPacketEncoder) user.getPipeline().get(FallbackPacketEncoder.class);
final var encoder = user.getPipeline().get(FallbackPacketEncoder.class);
// Update the packet registry state in the encoder and decoder pipelines
decoder.updateRegistry(registry);
encoder.updateRegistry(registry);
@@ -168,4 +184,30 @@ public final class FallbackLoginSessionHandler extends FallbackSessionHandler {
user.delayedWrite(REGISTRY_SYNC_LEGACY);
}
}
private void validateClientBrand(final byte @NotNull [] data) {
// Check if the client brand is too short. It has to have at least 2 bytes.
checkState(data.length > 1, "client brand is too short");
// Check if the decoded client brand string is too long
checkState(data.length < Sonar.get().getConfig().getVerification().getBrand().getMaxLength(),
"client brand contains too much data: " + data.length);
// https://discord.com/channels/923308209769426994/1116066363887321199/1256929441053933608
String brand = new String(data, StandardCharsets.UTF_8);
// Remove the invalid character at the beginning of the client brand
if (user.getProtocolVersion().compareTo(MINECRAFT_1_8) >= 0) {
brand = brand.substring(1);
}
// Check for illegal client brands
checkState(!brand.equals("Vanilla"), "illegal client brand: " + brand);
// Regex pattern for validating client brands
final Pattern pattern = Sonar.get().getConfig().getVerification().getBrand().getValidRegex();
checkState(pattern.matcher(brand).matches(), "client brand does not match pattern: " + brand);
}
private void validateClientLocale(final @NotNull String locale) {
// Check the client locale by performing a simple regex check
// that disallows non-ascii characters by default.
final Pattern pattern = Sonar.get().getConfig().getVerification().getValidLocaleRegex();
checkState(pattern.matcher(locale).matches(), "client locale does not match pattern: " + locale);
}
}
@@ -15,30 +15,32 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.session;
package xyz.jonesdev.sonar.common.fallback.verification;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketDecoder;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.AnimationPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.EntityAnimationPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.SetHeldItemPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.TransactionPacket;
public final class FallbackProtocolSessionHandler extends FallbackSessionHandler {
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_8;
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer.CACHED_HELD_ITEM_RESET;
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer.PLAYER_ENTITY_ID;
public FallbackProtocolSessionHandler(final @NotNull FallbackUser user,
final @NotNull String username,
final boolean forceCAPTCHA) {
super(user, username);
public final class FallbackProtocolHandler extends FallbackVerificationHandler {
this.forceCAPTCHA = forceCAPTCHA;
public FallbackProtocolHandler(final @NotNull FallbackUser user) {
super(user);
// Immediately send the player the transaction packet
sendTransaction();
}
private final boolean forceCAPTCHA;
private boolean waitingSwingArm, waitingSlotConfirm;
private short expectedTransactionId;
private int currentClientSlotId, expectedSlotId = -1;
@@ -50,7 +52,7 @@ public final class FallbackProtocolSessionHandler extends FallbackSessionHandler
*/
private void sendTransaction() {
// Send a Transaction (Ping) packet with a random ID
expectedTransactionId = (short) -(RANDOM.nextInt(Short.MAX_VALUE));
expectedTransactionId = (short) -RANDOM.nextInt(Short.MAX_VALUE);
user.write(new TransactionPacket(0, expectedTransactionId, false));
}
@@ -63,6 +65,9 @@ public final class FallbackProtocolSessionHandler extends FallbackSessionHandler
// Move the player's slot by 4 (slot limit divided by 2),
// and then modulo it by the slot limit (8) to ensure that we don't send invalid slot IDs.
expectedSlotId = (currentClientSlotId + 4) % 8;
// Reset the player's slot to 4, so we don't get false positives if the client changes the slot
// This doesn't *really* work for 1.7-1.8 clients, but it's better to still send a packet if they're de-sync
user.delayedWrite(CACHED_HELD_ITEM_RESET);
// Send two SetHeldItem packets with the same slot to check if the player responds with the correct slot.
// By vanilla protocol, the client does not respond to duplicate SetHeldItem packets.
// We can take advantage of this by sending two packets with the same content to check for a valid response.
@@ -72,15 +77,26 @@ public final class FallbackProtocolSessionHandler extends FallbackSessionHandler
user.getChannel().flush();
}
/**
* Uses EntityAnimation packets to check for the excepted Animation (SwingArm) from the client.
* <br>
* <a href="https://wiki.vg/Protocol#Entity_Animation">Wiki.vg - EntityAnimation</a>
* <a href="https://wiki.vg/Protocol#Swing_Arm">Wiki.vg - SwingArm</a>
*/
private void sendArmAnimation() {
waitingSwingArm = true;
user.write(new EntityAnimationPacket(PLAYER_ENTITY_ID, EntityAnimationPacket.Type.SWING_MAIN_ARM));
}
private void markSuccess() {
// Either send the player to the vehicle check,
// send the player to the CAPTCHA, or finish the verification.
final var decoder = user.getPipeline().get(FallbackPacketDecoder.class);
// Pass the player to the next best verification handler
if (Sonar.get().getConfig().getVerification().getVehicle().isEnabled()) {
decoder.setListener(new FallbackVehicleSessionHandler(user, username, forceCAPTCHA));
} else if (forceCAPTCHA || Sonar.get().getFallback().shouldPerformCaptcha()) {
decoder.setListener(new FallbackCAPTCHASessionHandler(user, username));
decoder.setListener(new FallbackVehicleHandler(user));
} else if (user.isForceCaptcha() || Sonar.get().getFallback().shouldPerformCaptcha()) {
decoder.setListener(new FallbackCaptchaHandler(user));
} else {
// The player has passed all checks
finishVerification();
@@ -94,6 +110,8 @@ public final class FallbackProtocolSessionHandler extends FallbackSessionHandler
// Make sure random transactions aren't counted
checkState(expectedTransactionId <= 0, "unexpected transaction");
// Make sure the window ID is valid
checkState(transaction.getWindowId() == 0, "wrong window ID " + transaction.getWindowId());
// Make sure the transaction was accepted
// This must - by vanilla protocol - always be accepted
checkState(transaction.isAccepted(), "didn't accept transaction");
@@ -108,9 +126,20 @@ public final class FallbackProtocolSessionHandler extends FallbackSessionHandler
// https://wiki.vg/Bedrock_Protocol#Player_Hotbar
if (user.isGeyser()) {
markSuccess();
} else if (waitingSlotConfirm) {
waitingSlotConfirm = false;
expectedSlotId = -1;
// The player did not send duplicate packets, so they pass this check
if (user.isGeyser()) {
markSuccess();
} else {
sendArmAnimation();
}
} else {
sendSetHeldItem();
}
expectedTransactionId = 0;
} else if (packet instanceof SetHeldItemPacket) {
final SetHeldItemPacket heldItemPacket = (SetHeldItemPacket) packet;
@@ -125,11 +154,33 @@ public final class FallbackProtocolSessionHandler extends FallbackSessionHandler
if (expectedSlotId != -1
// Check if the slot ID matches the expected slot ID
// This can false flag if a player spams these packets, which is why we don't fail for this
&& slotId == expectedSlotId) {
markSuccess();
&& slotId == expectedSlotId
// Make sure we actually want to send a transaction at this point in time
&& !waitingSlotConfirm) {
sendTransaction();
waitingSlotConfirm = true;
}
currentClientSlotId = slotId;
} else if (packet instanceof AnimationPacket) {
// Make sure we are awaiting an AnimationPacket packet
if (waitingSwingArm) {
final AnimationPacket animationPacket = (AnimationPacket) packet;
checkState(animationPacket.getHand() == AnimationPacket.MAIN_HAND,
"invalid hand " + animationPacket.getHand());
// Check that the 1.7 client is responding to the correct entity id and animation type.
if (user.getProtocolVersion().compareTo(MINECRAFT_1_8) < 0) {
// Check entity id is player itself and if the player is sending the correct animation type
if (animationPacket.getEntityId() == PLAYER_ENTITY_ID
&& animationPacket.getType() == AnimationPacket.LegacyAnimationType.SWING_ARM) {
markSuccess();
waitingSwingArm = false;
}
} else {
markSuccess();
waitingSwingArm = false;
}
}
}
}
}
@@ -0,0 +1,222 @@
/*
* 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.verification;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketDecoder;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.*;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_8;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_9;
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer.*;
public final class FallbackVehicleHandler extends FallbackVerificationHandler {
public FallbackVehicleHandler(final @NotNull FallbackUser user) {
super(user);
// Send the necessary packets to mount the player on the boat vehicle
user.delayedWrite(spawnBoatEntity);
user.delayedWrite(setBoatPassengers);
user.getChannel().flush();
}
private int expectedTransactionId;
private int paddlePackets, inputPackets, positionPackets, rotationPackets, vehicleMovePackets;
private boolean expectMovement, inVoid, inMinecart, waitingSpawnMinecart, waitingRemoveMinecart;
private double minecartMotion, minecartY = IN_AIR_Y_POSITION;
private void markSuccess() {
// Pass the player to the next best verification handler
if (user.isForceCaptcha() || Sonar.get().getFallback().shouldPerformCaptcha()) {
// Either send the player to the CAPTCHA or finish the verification.
final var decoder = user.getPipeline().get(FallbackPacketDecoder.class);
// Send the player to the CAPTCHA handler
decoder.setListener(new FallbackCaptchaHandler(user));
} else {
// The player has passed all checks
finishVerification();
}
}
// We can abuse the entity remove mechanic and check for position packets when the entity dies
private void handleMovement(double y, final boolean onGround) {
// The player cannot be on the ground if the Y position is less than 0
checkState(!onGround || !inVoid || waitingRemoveMinecart, "invalid ground state: " + y);
if (user.getProtocolVersion().compareTo(MINECRAFT_1_8) < 0) {
y -= 1.62f; // Account for 1.7 bounding box
}
// Check the Y position of the player
final double minimumY = inVoid ? IN_VOID_Y_POSITION : IN_AIR_Y_POSITION;
checkState(y <= minimumY, "illegal y position: " + y + "/" + minimumY);
// Mark this check as successful if the player sent a few position packets
if (positionPackets++ > Sonar.get().getConfig().getVerification().getVehicle().getMinimumPackets()) {
if (user.isGeyser() || inMinecart || inVoid) {
markSuccess();
} else {
spawnMinecart();
}
}
}
private void handleRotation() {
// Once the player sent enough packets, go to the next stage
final int minimumPackets = Sonar.get().getConfig().getVerification().getVehicle().getMinimumPackets();
if (paddlePackets > minimumPackets
&& inputPackets > minimumPackets
&& rotationPackets > minimumPackets
&& vehicleMovePackets > minimumPackets) {
// If the player is riding a Minecart, teleport the entity to < -100
// to see how the player reacts to the invalid position of the entity.
if (inMinecart) {
user.delayedWrite(teleportMinecart);
// We're using transactions to avoid false positives with lag
waitingRemoveMinecart = true;
expectedTransactionId = (short) -RANDOM.nextInt(Short.MAX_VALUE);
user.delayedWrite(new TransactionPacket(0, expectedTransactionId, false));
user.getChannel().flush();
} else {
// If the player is riding a boat, remove the entity
// The next Y coordinate the player will send is going
// to be the vehicle spawn position (64 in this case).
user.write(removeBoat);
}
// Listen for next movement packet(s)
expectMovement = true;
}
rotationPackets++;
}
private void spawnMinecart() {
expectMovement = false;
paddlePackets = inputPackets = positionPackets = rotationPackets = 0;
// Use transaction packets to confirm that the entity has spawned
waitingSpawnMinecart = true;
minecartMotion = 0.03999999910593033D * 2 * -1;
expectedTransactionId = (short) -RANDOM.nextInt(Short.MAX_VALUE);
user.delayedWrite(new TransactionPacket(0, expectedTransactionId, false));
user.delayedWrite(spawnMinecartEntity);
user.delayedWrite(setMinecartPassengers);
user.getChannel().flush();
}
@Override
public void handle(final @NotNull FallbackPacket packet) {
if (packet instanceof TransactionPacket) {
final TransactionPacket transaction = (TransactionPacket) packet;
// Make sure random transactions aren't counted
checkState(expectedTransactionId <= 0, "unexpected transaction");
// Make sure the window ID is valid
checkState(transaction.getWindowId() == 0, "wrong window ID " + transaction.getWindowId());
// Make sure the transaction was accepted
// This must - by vanilla protocol - always be accepted
checkState(transaction.isAccepted(), "didn't accept transaction");
// Also check if the transaction ID matches the expected ID
checkState(expectedTransactionId == transaction.getTransactionId(),
"expected T ID " + expectedTransactionId + ", but got " + transaction.getTransactionId());
// We're "lag compensating" the Minecart to avoid false positives
if (waitingSpawnMinecart) {
waitingSpawnMinecart = false;
inMinecart = true;
} else if (waitingRemoveMinecart) {
waitingRemoveMinecart = inMinecart = false;
inVoid = true;
positionPackets = 0;
}
expectedTransactionId = 0;
} else if (packet instanceof SetPlayerPositionRotationPacket) {
if (expectMovement) {
final SetPlayerPositionRotationPacket posRot = (SetPlayerPositionRotationPacket) packet;
handleMovement(posRot.getY(), posRot.isOnGround());
}
} else if (packet instanceof SetPlayerPositionPacket) {
if (expectMovement) {
final SetPlayerPositionPacket position = (SetPlayerPositionPacket) packet;
handleMovement(position.getY(), position.isOnGround());
}
} else if (packet instanceof SetPlayerRotationPacket) {
if (!expectMovement && !user.isGeyser()) {
handleRotation();
}
} else if (packet instanceof PaddleBoatPacket) {
checkState(!inMinecart, "invalid packet order (unexpected PaddleBoatPacket)");
paddlePackets++;
} else if (packet instanceof VehicleMovePacket) {
checkState(!inMinecart, "invalid packet order (unexpected VehicleMovePacket)");
final VehicleMovePacket vehicleMove = (VehicleMovePacket) packet;
// Check the Y position of the vehicle
checkState(vehicleMove.getY() <= IN_AIR_Y_POSITION, "invalid y position: " + vehicleMove.getY());
if (!user.isGeyser()) {
// Predict the Y motion of the minecart
final double lastMinecartMotion = minecartMotion;
final double lastMinecartY = minecartY;
minecartY = vehicleMove.getY();
minecartMotion = minecartY - lastMinecartY;
final double predicted = lastMinecartMotion - 0.03999999910593033D;
final double difference = Math.abs(minecartMotion - predicted);
// Check if the difference between the predicted and actual motion is too large
checkState(difference < 1e-7,
"invalid vehicle gravity: " + predicted + "/" + minecartMotion + "/" + difference);
}
vehicleMovePackets++;
} else if (packet instanceof PlayerInputPacket) {
if (!expectMovement) {
final PlayerInputPacket playerInput = (PlayerInputPacket) packet;
// Check if the player is sending invalid vehicle speed values
final float forward = Math.abs(playerInput.getForward());
final float sideways = Math.abs(playerInput.getSideways());
final float maxVehicleSpeed = user.isGeyser() ? 1 : 0.98f;
checkState(forward <= maxVehicleSpeed, "illegal speed (f): " + forward);
checkState(sideways <= maxVehicleSpeed, "illegal speed (s): " + sideways);
// Only mark this packet as correct if the player is not moving the vehicle
if (playerInput.isJump() || playerInput.isUnmount()) {
return;
}
// Bedrock users do not send SetPlayerPositionRotation and SetPlayerRotation packets
// 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.
// Clients don't send PaddleBoat & VehicleMovePacket packets while riding minecarts.
if (user.getProtocolVersion().compareTo(MINECRAFT_1_9) < 0 || inMinecart) {
paddlePackets++;
vehicleMovePackets++;
} else {
checkState(paddlePackets >= inputPackets,
"illegal packet order; i/p " + inputPackets + "/" + paddlePackets);
checkState(vehicleMovePackets >= inputPackets,
"illegal packet order; i/v " + inputPackets + "/" + vehicleMovePackets);
}
inputPackets++;
}
}
}
}
@@ -0,0 +1,141 @@
/*
* 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.verification;
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.UserBlacklistedEvent;
import xyz.jonesdev.sonar.api.event.impl.UserVerifyFailedEvent;
import xyz.jonesdev.sonar.api.event.impl.UserVerifySuccessEvent;
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
import xyz.jonesdev.sonar.common.fallback.netty.FallbackVarInt21FrameDecoder;
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.FallbackPacketListener;
import xyz.jonesdev.sonar.common.statistics.GlobalSonarStatistics;
import xyz.jonesdev.sonar.common.util.exception.QuietDecoderException;
import java.util.Random;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_20_5;
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer.transferToOrigin;
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.closeWith;
@RequiredArgsConstructor
public abstract class FallbackVerificationHandler implements FallbackPacketListener {
protected final FallbackUser user;
protected static final Random RANDOM = new Random();
protected final void finishVerification() {
GlobalSonarStatistics.totalSuccessfulVerifications++;
// Add verified player to the database
Sonar.get().getVerifiedPlayerController().add(new VerifiedPlayer(
user.getFingerprint(), user.getLoginTimer().getStart()));
// Call the VerifySuccessEvent for external API usage
Sonar.get().getEventManager().publish(new UserVerifySuccessEvent(user, user.getLoginTimer()));
// If enabled, transfer the player back to the origin server.
// This feature was introduced by Mojang in Minecraft version 1.20.5.
if (transferToOrigin != null
&& user.getProtocolVersion().compareTo(MINECRAFT_1_20_5) >= 0) {
// Send the transfer packet to the player (and close the channel if on Java Edition)
if (user.isGeyser()) {
user.write(transferToOrigin);
// Make sure we cannot receive any more packets from the player
user.getPipeline().remove(FallbackPacketDecoder.class);
user.getPipeline().remove(FallbackPacketEncoder.class);
user.getPipeline().remove(FallbackVarInt21FrameDecoder.class);
user.getPipeline().remove(FallbackVarIntLengthEncoder.class);
} else {
closeWith(user.getChannel(), user.getProtocolVersion(), transferToOrigin);
}
} else {
// Disconnect player with the verification success message
user.disconnect(Sonar.get().getConfig().getVerification().getVerificationSuccess());
}
Sonar.get().getLogger().info(
Sonar.get().getConfig().getMessagesConfig().getString("verification.logs.successful")
.replace("<username>", user.getUsername())
.replace("<time-taken>", user.getLoginTimer().toString()));
}
protected final void fail(final @NotNull String reason) {
GlobalSonarStatistics.totalFailedVerifications++;
user.disconnect(Sonar.get().getConfig().getVerification().getVerificationFailed());
final boolean shouldLog = Sonar.get().getAttackTracker().getCurrentAttack() == null
|| Sonar.get().getConfig().getVerification().isLogDuringAttack();
if (shouldLog) {
Sonar.get().getLogger().info(
Sonar.get().getConfig().getMessagesConfig().getString("verification.logs.failed")
.replace("<username>", user.getUsername())
.replace("<ip>", Sonar.get().getConfig().formatAddress(user.getInetAddress()))
.replace("<protocol>", String.valueOf(user.getProtocolVersion().getProtocol()))
.replace("<reason>", reason));
}
// Call the VerifyFailedEvent for external API usage
Sonar.get().getEventManager().publish(new UserVerifyFailedEvent(user, reason));
// Use a label, so we can easily add more code beneath this method in the future
blacklist: {
final String hostAddress = user.getInetAddress().getHostAddress();
final int score = Sonar.get().getFallback().getBlacklist().get(hostAddress, __ -> 0);
final int newScore = score + 1;
Sonar.get().getFallback().getBlacklist().put(hostAddress, newScore);
// The user is allowed to disable the blacklist entirely by setting the threshold to 0
final int limit = Sonar.get().getConfig().getVerification().getBlacklistThreshold();
// 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(user));
if (shouldLog) {
Sonar.get().getLogger().info(
Sonar.get().getConfig().getMessagesConfig().getString("verification.logs.blacklisted")
.replace("<username>", user.getUsername())
.replace("<ip>", Sonar.get().getConfig().formatAddress(user.getInetAddress()))
.replace("<protocol>", String.valueOf(user.getProtocolVersion().getProtocol())));
}
}
// Throw an exception to avoid further code execution
throw QuietDecoderException.INSTANCE;
}
protected final void checkState(final boolean state, final String failReason) {
// Fails the verification if the condition is not met
if (!state) {
fail(failReason);
}
}
}
@@ -21,6 +21,7 @@ import lombok.Getter;
import lombok.Setter;
import xyz.jonesdev.sonar.api.profiler.SimpleProcessProfiler;
// TODO: maybe recode this?
/**
* This counts all incoming and outgoing traffic.
*/
@@ -44,7 +44,6 @@ public final class BlacklistCommand extends Subcommand {
// Make sure the given IP address is valid
if (rawAddress == null) return;
// 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.ip-duplicate"),
@@ -53,15 +52,6 @@ public final class BlacklistCommand extends Subcommand {
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"),
@@ -80,7 +70,6 @@ public final class BlacklistCommand extends Subcommand {
// Make sure the given IP address is valid
if (rawAddress == null) return;
// 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.ip-not-found"),
@@ -88,7 +77,6 @@ public final class BlacklistCommand extends Subcommand {
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"),
@@ -107,7 +95,6 @@ public final class BlacklistCommand extends Subcommand {
return;
}
// Invalidate all cache entries
Sonar.get().getFallback().getBlacklist().invalidateAll();
invocation.getSource().sendMessage(MiniMessage.miniMessage().deserialize(
Sonar.get().getConfig().getMessagesConfig().getString("commands.blacklist.clear"),
@@ -25,110 +25,72 @@ 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.database.model.VerifiedPlayer;
import java.nio.charset.StandardCharsets;
import java.util.UUID;
import xyz.jonesdev.sonar.api.fingerprint.FingerprintingUtil;
@SubcommandInfo(
name = "verified",
arguments = {"add", "remove", "clear", "size", "history"}
arguments = {"add", "remove", "clear", "size"}
)
public final class VerifiedCommand extends Subcommand {
@Override
protected void execute(final @NotNull CommandInvocation invocation) {
switch (invocation.getRawArguments()[1].toLowerCase()) {
case "history": {
if (invocation.getRawArguments().length <= 2) {
incorrectUsage(invocation.getSource(), "verified history <IP address>");
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;
// Make sure the IP is verified already
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;
}
invocation.getSource().sendMessage(MiniMessage.miniMessage().deserialize(
Sonar.get().getConfig().getMessagesConfig().getString("commands.verified.history"),
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix()),
Placeholder.unparsed("ip", rawAddress)));
for (final UUID uuid : Sonar.get().getVerifiedPlayerController().getUUIDs(rawAddress)) {
invocation.getSource().sendMessage(MiniMessage.miniMessage().deserialize(
Sonar.get().getConfig().getMessagesConfig().getString("commands.verified.history-entry"),
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix()),
Placeholder.unparsed("uuid", uuid.toString())));
}
break;
}
case "remove": {
if (invocation.getRawArguments().length <= 2) {
incorrectUsage(invocation.getSource(), "verified remove <IP address>");
if (invocation.getRawArguments().length <= 3) {
incorrectUsage(invocation.getSource(), "verified remove <IP address> <username>");
return;
}
// TODO: Remove duplicate code
final String rawAddress = validateIP(invocation.getSource(), invocation.getRawArguments()[2]);
final String hostAddress = validateIP(invocation.getSource(), invocation.getRawArguments()[2]);
// Make sure the given IP address is valid
if (rawAddress == null) return;
if (hostAddress == null) return;
// Make sure the player is verified already
if (!Sonar.get().getVerifiedPlayerController().has(rawAddress)) {
final String username = invocation.getRawArguments()[3];
final String fingerprint = FingerprintingUtil.getFingerprint(username, hostAddress);
if (!Sonar.get().getVerifiedPlayerController().getCache().contains(fingerprint)) {
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(rawAddress);
Sonar.get().getVerifiedPlayerController().remove(fingerprint);
invocation.getSource().sendMessage(MiniMessage.miniMessage().deserialize(
Sonar.get().getConfig().getMessagesConfig().getString("commands.verified.remove"),
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix()),
Placeholder.unparsed("ip", rawAddress)));
Placeholder.unparsed("ip", hostAddress),
Placeholder.unparsed("username", username)));
break;
}
case "add": {
if (invocation.getRawArguments().length <= 3) {
incorrectUsage(invocation.getSource(), "verified add <IP address> <UUID/username>");
incorrectUsage(invocation.getSource(), "verified add <IP address> <username>");
return;
}
final String rawAddress = validateIP(invocation.getSource(), invocation.getRawArguments()[2]);
final String hostAddress = validateIP(invocation.getSource(), invocation.getRawArguments()[2]);
// Make sure the given IP address is valid
if (rawAddress == null) return;
if (hostAddress == null) return;
// Try to parse the UUID (from the username, if needed)
final String rawUUID = invocation.getRawArguments()[3];
final UUID uuid = rawUUID.length() == 36 ? UUID.fromString(rawUUID)
: UUID.nameUUIDFromBytes(("OfflinePlayer:" + rawUUID).getBytes(StandardCharsets.UTF_8));
final String username = invocation.getRawArguments()[3];
final String fingerprint = FingerprintingUtil.getFingerprint(username, hostAddress);
// Make sure the player is verified already
if (Sonar.get().getVerifiedPlayerController().has(rawAddress, uuid)) {
if (Sonar.get().getVerifiedPlayerController().getCache().contains(fingerprint)) {
invocation.getSource().sendMessage(MiniMessage.miniMessage().deserialize(
Sonar.get().getConfig().getMessagesConfig().getString("commands.verified.already"),
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix())));
return;
}
// Add verified player to the database
final long timestamp = System.currentTimeMillis();
Sonar.get().getVerifiedPlayerController().add(new VerifiedPlayer(rawAddress, uuid, timestamp));
Sonar.get().getVerifiedPlayerController().add(new VerifiedPlayer(fingerprint, System.currentTimeMillis()));
invocation.getSource().sendMessage(MiniMessage.miniMessage().deserialize(
Sonar.get().getConfig().getMessagesConfig().getString("commands.verified.add"),
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix()),
Placeholder.unparsed("ip", rawAddress)));
Placeholder.unparsed("ip", hostAddress),
Placeholder.unparsed("username", username)));
break;
}
@@ -142,7 +104,6 @@ public final class VerifiedCommand extends Subcommand {
return;
}
// Invalidate all cache entries
Sonar.get().getVerifiedPlayerController().clearAll();
invocation.getSource().sendMessage(MiniMessage.miniMessage().deserialize(
Sonar.get().getConfig().getMessagesConfig().getString("commands.verified.clear"),
@@ -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);
}
});
}
@@ -106,21 +106,21 @@ public final class ComponentHolder {
case 1://BinaryTagTypes.BYTE:
final byte[] bytes = new byte[jsonArray.size()];
for (int i = 0; i < bytes.length; i++) {
bytes[i] = (Byte) jsonArray.get(i).getAsNumber();
bytes[i] = jsonArray.get(i).getAsNumber().byteValue();
}
return ByteArrayBinaryTag.byteArrayBinaryTag(bytes);
case 3://BinaryTagTypes.INT:
final int[] ints = new int[jsonArray.size()];
for (int i = 0; i < ints.length; i++) {
ints[i] = (Integer) jsonArray.get(i).getAsNumber();
ints[i] = jsonArray.get(i).getAsNumber().intValue();
}
return IntArrayBinaryTag.intArrayBinaryTag(ints);
case 4://BinaryTagTypes.LONG:
final long[] longs = new long[jsonArray.size()];
for (int i = 0; i < longs.length; i++) {
longs[i] = (Long) jsonArray.get(i).getAsNumber();
longs[i] = jsonArray.get(i).getAsNumber().longValue();
}
return LongArrayBinaryTag.longArrayBinaryTag(longs);
@@ -41,21 +41,25 @@ package xyz.jonesdev.sonar.common.util;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufOutputStream;
import io.netty.buffer.ByteBufUtil;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFutureListener;
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.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.util.exception.QuietDecoderException;
import java.io.DataOutput;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.BitSet;
import java.util.EnumSet;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_7_2;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_8;
// https://github.com/PaperMC/Velocity/blob/dev/3.0.0/proxy/src/main/java/com/velocitypowered/proxy/protocol/ProtocolUtils.java
@UtilityClass
@@ -63,45 +67,42 @@ 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];
public static void checkNettyVersion() {
final Version version = Version.identify().getOrDefault("netty-all", Version.identify().get("netty-common"));
// We're pretty much only doing this to avoid incompatibilities on Bukkit,
// so we don't really care if the version couldn't be resolved.
if (version == null) {
return;
}
final String[] artifactVersion = version.artifactVersion().split("\\.");
final int major = Integer.parseInt(artifactVersion[0]);
final int minor = Integer.parseInt(artifactVersion[1]);
// Enforce Netty >4.1.x
if (major < 4 || (major == 4 && minor < 1)) {
throw new IllegalStateException("Your Netty version is too old to run Sonar! Please use Netty >4.1.x.");
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 readVarInt(final ByteBuf byteBuf) {
int read = readVarIntSafely(byteBuf);
if (read == Integer.MIN_VALUE) {
throw DEBUG ? new DecoderException("Bad VarInt") : QuietDecoderException.INSTANCE;
}
return read;
public static int varIntBytes(final int value) {
return VAR_INT_LENGTHS[Integer.numberOfLeadingZeros(value)];
}
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();
public static int readVarInt(final @NotNull ByteBuf byteBuf) {
final int readable = byteBuf.readableBytes();
if (readable == 0) {
throw DEBUG ? new DecoderException("Empty buffer") : QuietDecoderException.INSTANCE;
}
// 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) {
@@ -139,19 +140,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.
@@ -215,6 +203,41 @@ public class ProtocolUtil {
}
}
public static void checkNettyVersion() {
final Version version = Version.identify().getOrDefault("netty-all", Version.identify().get("netty-common"));
// We're pretty much only doing this to avoid incompatibilities on Bukkit,
// so we don't really care if the version couldn't be resolved.
if (version == null) {
return;
}
final String[] artifactVersion = version.artifactVersion().split("\\.");
final int major = Integer.parseInt(artifactVersion[0]);
final int minor = Integer.parseInt(artifactVersion[1]);
// Enforce Netty >4.1.x
if (major < 4 || (major == 4 && minor < 1)) {
throw new IllegalStateException("Your Netty version is too old to run Sonar! Please use Netty >4.1.x.");
}
}
public static void closeWith(final @NotNull Channel channel,
final @NotNull ProtocolVersion protocolVersion,
final @NotNull Object msg) {
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);
}
}
public static @NotNull UUID readUUID(final @NotNull ByteBuf byteBuf) {
return new UUID(byteBuf.readLong(), byteBuf.readLong());
}
@@ -224,28 +247,23 @@ public class ProtocolUtil {
}
public static byte @NotNull [] readByteArray(final ByteBuf byteBuf, final int cap) {
int length = readVarInt(byteBuf);
final int length = readVarInt(byteBuf);
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];
final byte[] array = new byte[length];
byteBuf.readBytes(array);
return array;
}
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 DecoderException {
public static @NotNull String readString(final ByteBuf byteBuf, 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 DecoderException {
private static @NotNull String readString(final @NotNull ByteBuf byteBuf,
final int cap,
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");
@@ -273,7 +291,7 @@ public class ProtocolUtil {
byteBuf.writeInt((int) uuid.getLeastSignificantBits());
}
public static void writeArray(final ByteBuf byteBuf, final byte @NotNull [] bytes) {
public static void writeByteArray(final ByteBuf byteBuf, final byte @NotNull [] bytes) {
checkState(bytes.length < Short.MAX_VALUE, "Too long array");
writeVarInt(byteBuf, bytes.length);
byteBuf.writeBytes(bytes);
@@ -286,27 +304,6 @@ public class ProtocolUtil {
}
}
// 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,
final @NotNull Class<E> oclass) {
final E[] enums = oclass.getEnumConstants();
final BitSet bits = new BitSet(enums.length);
for (int i = 0; i < enums.length; ++i) {
bits.set(i, enumset.contains(enums[i]));
}
writeFixedBitSet(byteBuf, bits, enums.length);
}
private static void writeFixedBitSet(final ByteBuf byteBuf, final @NotNull BitSet bits, final int size) {
if (bits.length() > size) {
throw new StackOverflowError("BitSet too large (expected " + size + " got " + bits.size() + ")");
}
byteBuf.writeBytes(Arrays.copyOf(bits.toByteArray(), (size + 8) >> 3));
}
public static void writeCompoundTag(final @NotNull ByteBuf byteBuf, final @NotNull CompoundBinaryTag compoundTag) {
try {
BinaryTagIO.writer().write(compoundTag, (DataOutput) new ByteBufOutputStream(byteBuf));
@@ -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
@@ -90,10 +91,6 @@ verification:
collision:
# Měl by Sonar zkoušet jestli se hráč dotýka bloku? (Doporučováno)
enabled: true
# Jaký typ bloku by měl Sonar použít pro kolizní platformu?
# Typy: STONE, BARRIER, TRAPDOOR, ENCHANTMENT_TABLE
# Toto byste měli upravovat, pouze pokud víte, co děláte
collision-block-type: TRAPDOOR
# Kontroluje, zda klient odesílá správné pakety při montáži vozidla
vehicle:
@@ -139,11 +136,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
# Je-li povoleno, hráč bude přenesen zpět na původní server
# po úspěšném absolvování ověření bota.
@@ -155,6 +147,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
@@ -87,10 +88,6 @@ verification:
collision:
# Soll Sonar prüfen, ob der Spieler mit Blöcken kollidiert? (Empfohlen)
enabled: true
# Welche Art von Block sollte Sonar für die Kollisionsplattform verwenden?
# Mögliche Typen: STONE, BARRIER, TRAPDOOR, ENCHANTMENT_TABLE
# Sie sollten dies nur bearbeiten, wenn Sie wissen, was Sie tun
collision-block-type: TRAPDOOR
# Überprüft, ob der Spieler beim Einsteigen in ein Boot die richtigen Pakete sendet
vehicle:
@@ -136,11 +133,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
# Wenn aktiviert, wird der Player zurück zum Ursprungsserver gesendet,
# falls dieser die der Bot-Verifizierung erfolgreich besteht.
@@ -152,6 +144,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,17 +83,13 @@ 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
collision:
# Should Sonar check if the player collides with blocks? (Recommended)
enabled: true
# Which type of block should Sonar use for the collision platform?
# Possible types: STONE, BARRIER, TRAPDOOR, ENCHANTMENT_TABLE
# You should only edit this if you know what you're doing
collision-block-type: TRAPDOOR
# Checks if the client sends proper packets when entering a boat
vehicle:
@@ -110,8 +107,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,11 +136,6 @@ 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
max-login-packets: 256
# If enabled, the player will be transferred back to the origin server
# after successfully passing the bot verification.
@@ -155,6 +147,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
@@ -89,10 +90,6 @@ verification:
collision:
# Sonar devrait-il vérifier si le joueur entre en collision avec des blocs ? (Recommandé)
enabled: true
# Quel type de bloc Sonar doit-il utiliser pour la plate-forme de collision ?
# Types possibles : STONE, BARRIER, TRAPDOOR, ENCHANTMENT_TABLE
# Vous ne devriez modifier cela que si vous savez ce que vous faites
collision-block-type: TRAPDOOR
# Vérifie si le client envoie les paquets appropriés lorsqu'il monte dans un véhicule
vehicle:
@@ -138,11 +135,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
# S'il est activé, le joueur sera renvoyé au serveur d'origine
# après avoir passé avec succès la vérification du bot.
@@ -154,6 +146,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
#
@@ -89,10 +90,6 @@ verification:
collision:
# უნდა შეამოწმოს თუ არა Sonar-მა, ეტაკება თუ არა მოთამაშე ბლოკებს? (რეკომენდებულია)
enabled: true
# Which type of block should Sonar use for the collision platform?
# შესაძლო ტიპები: STONE, BARRIER, TRAPDOOR, ENCHANTMENT_TABLE
# თქვენ უნდა შეცვალოთ ეს მხოლოდ იმ შემთხვევაში, თუ იცით, რას აკეთებთ
collision-block-type: TRAPDOOR
# ამოწმებს, აგზავნის თუ არა კლიენტი შესაბამის პაკეტებს სატრანსპორტო საშუალებაზე დაჯდომის დროს
vehicle:
@@ -138,11 +135,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
# თუ ჩართულია, მოთამაშე გადავა საწყის სერვერში ვერიფიკაციის წარმატებით გავლის შემდეგ
# ეს ფუნქცია დაემატა Mojang-ის მიერ Minecraft-ის 1.20.5 ვერსიაში
@@ -153,6 +145,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
@@ -89,10 +90,6 @@ verification:
collision:
# Moet Sonar controleren of de speler tegen blokken botst? (Aanbevolen)
enabled: true
# Welk type blok moet Sonar gebruiken voor het platform?
# Mogelijke typen: STONE, BARRIER, TRAPDOOR, ENCHANTMENT_TABLE
# Je moet dit alleen bewerken als je weet wat je doet
collision-block-type: TRAPDOOR
# Controleert of de klant de juiste pakketten verzendt bij het monteren van een voertuig
vehicle:
@@ -138,11 +135,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
# Indien ingeschakeld, wordt de speler teruggestuurd naar de oorspronkelijke server
# nadat u de bot-verificatie met succes heeft doorstaan.
@@ -154,6 +146,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
@@ -89,10 +90,6 @@ verification:
collision:
# Czy Sonar powinien sprawdzać, czy gracz zderza się z blokami? (Zalecane)
enabled: true
# Jakiego typu bloku powinien użyć Sonar dla platformy kolizyjnej?
# Możliwe rodzaje: STONE, BARRIER, TRAPDOOR, ENCHANTMENT_TABLE
# Powinieneś to edytować tylko wtedy, gdy wiesz, co robisz
collision-block-type: TRAPDOOR
# Sprawdza, czy gracz wysyła odpowiednie pakiety podczas umieszczania na pojeździe.
vehicle:
@@ -138,11 +135,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
# Jeśli opcja ta jest włączona, gracz zostanie przeniesiony z powrotem
# na serwer początkowy po pomyślnym przejściu weryfikacji antybotowej.
@@ -154,6 +146,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
#
# Основные настройки
@@ -89,10 +90,6 @@ verification:
collision:
# Должен ли Sonar проверять, сталкивается ли игрок с блоками? (Рекомендуется)
enabled: true
# Какой тип блока следует использовать в Sonar для платформы столкновений?
# Возможные типы: STONE, BARRIER, TRAPDOOR, ENCHANTMENT_TABLE
# Рекомендуется редактировать только если вы знаете, что делаете
collision-block-type: TRAPDOOR
# Проверяет, отправляет ли клиент правильные пакеты при садке на транспортное средство
vehicle:
@@ -137,11 +134,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 +145,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 许可
#
# 未归类的设置
@@ -87,10 +88,6 @@ verification:
collision:
# Sonar 是否应该生成方块平台以检查玩家下落与方块之间的碰撞? (推荐启用)
enabled: true
# Sonar 应该使用哪种类型的块作为碰撞平台?
# 可用类型: STONE, BARRIER, TRAPDOOR, ENCHANTMENT_TABLE
# 只有当您知道您在做什么时, 才应该编辑它.
collision-block-type: TRAPDOOR
# 检查客户端坐在实体上时是否发送了正确的数据包
vehicle:
@@ -135,10 +132,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
# 开启后 玩家将在验证成功后被重定向到指定的服务器
# 该功能由 Mojang 在 Minecraft 1.20.5 版本中引入
@@ -148,6 +141,9 @@ verification:
# 为此您必须在服务器软件的配置文件中允许接受来自transfer的连接.
# 此外 您可能希望减少 Velocity 或其它服务器软件/插件的登录速率限制
# 因为这可能会阻止玩家快速地重新连接到服务器
# 如果你的服务器允许 Bedrock 玩家通过间歇泉加入, 请确保为间歇泉设置了
# 传输工具 TransferTool, 以便将 Bedrock 玩家重定向到正确的服务器.
# https://github.com/onebeastchris/TransferTool
enabled: false
# 当玩家通过验证后, Sonar 应该将玩家转移到哪个服务器
# 请输入玩家正常连接服务器时使用的服务器IP或域名
@@ -102,8 +102,6 @@ commands:
remove: '<prefix>Úspěšně odstraněno <ip> z černé listiny.'
# Zpráva zobrazená, když někdo přidá IP adresu na černou listinu
add: '<prefix>Úspěšně přidáno <ip> na černou listinu.'
# Zpráva zobrazená, když někdo přidá IP adresu na černou listinu, která je ověřená
add-warning: '<prefix><red>Varování: <white><ip> je momentálně na bílé listině. Zvažte odstranění IP adresy ze seznamu ověřených hráčů, abyste se vyhnuli potenciálním problémům.'
# Zpráva zobrazená, když někdo přidá IP adresu na černou listinu, ale již je na černé listině
ip-duplicate: '<prefix>IP adresa, kterou jste zadali, je již na černé listině.'
# Zpráva zobrazená, když někdo odstraní IP adresu z černé listiny, ale není na černé listině
@@ -183,9 +181,9 @@ verification:
# Zpráva zaznamenána do konzole, kdykoli se nový hráč připojí k serveru
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>'
failed: '<username>/<ip> (<protocol>) neprošel kontrolou bota z důvodu: <reason>'
# Zpráva zaznamenána do konzole, kdykoli je hráč zařazen na černou listinu
blacklisted: '<ip> (<protocol>) byl zařazen na černou listinu kvůli příliš mnoha neúspěšným pokusům'
blacklisted: '<username>/<ip> (<protocol>) byl zařazen na černou listinu kvůli příliš mnoha neúspěšným pokusům'
# Zpráva zaznamenána do konzole, kdykoli je hráč ověřen
successful: '<username> byl úspěšně ověřen (<time-taken>s!).'
@@ -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
@@ -101,8 +101,6 @@ commands:
size: '<prefix>Die Blacklist enthält derzeit <amount> IP-Adresse(n).'
# Nachricht, die angezeigt wird, wenn jemand eine IP-Adresse zur Blacklist hinzufügt
add: '<prefix>Erfolgreich <ip> zur Blacklist hinzugefügt.'
# Nachricht, die angezeigt wird, wenn jemand eine IP-Adresse zur Blacklist hinzufügt, die verifiziert ist
add-warning: '<prefix><red>Warnung: <white><ip> ist derzeit auf der Whitelist. Erwäge, die IP-Adresse aus der Liste der verifizierten Spieler zu entfernen, um potenzielle Probleme zu vermeiden.'
# Nachricht, die angezeigt wird, wenn jemand eine IP-Adresse von der Blacklist entfernt
remove: '<prefix>Erfolgreich <ip> von der Blacklist entfernt.'
# Nachricht, die angezeigt wird, wenn jemand eine IP-Adresse zur Blacklist hinzufügt, die bereits auf der Blacklist steht
@@ -184,9 +182,9 @@ verification:
# Nachricht, die im Protokoll angezeigt wird, wenn ein neuer Spieler den Server betretet
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>'
failed: '<username>/<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
blacklisted: '<ip> (<protocol>) wurde aufgrund zu vieler fehlgeschlagener Versuche auf die Blacklist gesetzt'
blacklisted: '<username>/<ip> (<protocol>) wurde aufgrund zu vieler fehlgeschlagener Versuche auf die Blacklist gesetzt'
# Nachricht, die im Protokoll angezeigt wird, wenn ein Spieler erfolgreich verifiziert wurde
successful: '<username> wurde erfolgreich verifiziert (<time-taken>s!).'
@@ -102,8 +102,6 @@ commands:
remove: '<prefix>Successfully removed <ip> from the blacklist.'
# Message shown when someone adds an IP address to the blacklist
add: '<prefix>Successfully added <ip> to the blacklist.'
# Message shown when someone adds an IP address to the blacklist that is verified
add-warning: '<prefix><red>Warning: <white><ip> is currently whitelisted. Consider removing the IP address from the list of verified players to avoid potential issues.'
# Message shown when someone adds an IP address to the blacklist, but it is already blacklisted
ip-duplicate: '<prefix>The IP address you provided is already blacklisted.'
# Message shown when someone removes an IP address from the blacklist, but it is not blacklisted
@@ -183,9 +181,9 @@ verification:
# Message logged to console whenever a new player joins the server
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>'
failed: '<username>/<ip> (<protocol>) has failed the bot check for: <reason>'
# Message logged to console whenever a player is blacklisted
blacklisted: '<ip> (<protocol>) was blacklisted for too many failed attempts'
blacklisted: '<username>/<ip> (<protocol>) was blacklisted for too many failed attempts'
# Message logged to console whenever a player is verified
successful: '<username> has been verified successfully (<time-taken>s!).'
@@ -102,8 +102,6 @@ commands:
remove: '<prefix>Se eliminó con éxito <ip> de la lista negra.'
# Message shown when someone adds an IP address to the blacklist
add: '<prefix>Añadida con éxito <ip> a la lista negra.'
# Message shown when someone adds an IP address to the blacklist that is verified
add-warning: '<prefix><red>Advertencia: <white><ip> Actualmente está en la lista blanca. Considere eliminar la dirección IP de la lista de jugadores verificados para evitar posibles problemas.'
# Message shown when someone adds an IP address to the blacklist, but it is already blacklisted
ip-duplicate: '<prefix>La dirección IP que proporcionó ya está en la lista negra.'
# Message shown when someone removes an IP address from the blacklist, but it is not blacklisted
@@ -183,9 +181,9 @@ verification:
# Message logged to console whenever a new player joins the server
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>'
failed: '<username>/<ip> (<protocol>) no ha pasado la verificación de bot por: <reason>'
# Message logged to console whenever a player is blacklisted
blacklisted: '<ip> (<protocol>) fue incluido en la lista negra por demasiados intentos fallidos.'
blacklisted: '<username>/<ip> (<protocol>) fue incluido en la lista negra por demasiados intentos fallidos.'
# Message logged to console whenever a player is verified
successful: '<username> ha sido verificado exitosamente. (<time-taken>s!).'
@@ -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
@@ -100,8 +100,6 @@ commands:
size: '<prefix>La liste noire contient actuellement <amount> adresse(s) IP.'
# Message affiché lorsque quelqu'un ajoute une adresse IP à la liste noire
add: '<prefix>Adresse IP <ip> ajoutée avec succès à la liste noire.'
# Message affiché lorsque quelqu'un ajoute une adresse IP à la liste noire qui est vérifiée
add-warning: "<prefix><red>Avertissement : <white><ip> est actuellement sur liste blanche. Considérez la suppression de l'adresse IP de la liste des joueurs vérifiés pour éviter des problèmes potentiels."
# Message affiché lorsque quelqu'un supprime une adresse IP de la liste noire
remove: '<prefix>Adresse IP <ip> supprimée avec succès de la liste noire.'
# Message affiché lorsque quelqu'un ajoute une adresse IP à la liste noire, mais qu'elle est déjà en liste noire
@@ -183,9 +181,9 @@ verification:
# Message enregistré dans la console chaque fois qu'un nouveau joueur rejoint le serveur
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>'
failed: '<username>/<ip> (<protocol>) a échoué au test du bot pour : <reason>'
# Message enregistré dans la console chaque fois qu'un joueur est sur liste noire
blacklisted: '<ip> (<protocol>) a été mis sur liste noire pour trop de tentatives échouées'
blacklisted: '<username>/<ip> (<protocol>) a été mis sur liste noire pour trop de tentatives échouées'
# Message enregistré dans la console chaque fois qu'un joueur est vérifié
successful: '<username> a été vérifié avec succès (<time-taken>s !).'
@@ -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
@@ -102,8 +102,6 @@ commands:
remove: '<prefix>Berhasil menghapus <ip> dari daftar hitam.'
# Pesan yang ditampilkan saat seseorang menambahkan alamat IP ke daftar hitam
add: '<prefix>Berhasil menambahkan <ip> ke daftar hitam.'
# Pesan yang ditampilkan saat seseorang menambahkan alamat IP ke daftar hitam yang diverifikasi
add-warning: '<prefix><red>Peringatan: <white><ip> saat ini di daftar putih. Pertimbangkan untuk menghapus alamat IP dari daftar pemain yang diverifikasi untuk menghindari masalah potensial.'
# Pesan yang ditampilkan saat seseorang menambahkan alamat IP ke daftar hitam, tetapi sudah masuk daftar hitam
ip-duplicate: '<prefix>Alamat IP yang Anda berikan sudah masuk daftar hitam.'
# Pesan yang ditampilkan saat seseorang menghapus alamat IP dari daftar hitam, tetapi tidak masuk daftar hitam
@@ -183,9 +181,9 @@ verification:
# Pesan yang dicatat ke konsol setiap kali pemain baru bergabung ke server
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>'
failed: '<username>/<ip> (<protocol>) telah gagal dalam pemeriksaan bot untuk: <reason>'
# Pesan yang dicatat ke konsol setiap kali pemain masuk daftar hitam
blacklisted: '<ip> (<protocol>) masuk daftar hitam karena terlalu banyak percobaan yang gagal'
blacklisted: '<username>/<ip> (<protocol>) masuk daftar hitam karena terlalu banyak percobaan yang gagal'
# Pesan yang dicatat ke konsol setiap kali pemain berhasil diverifikasi
successful: '<username> berhasil diverifikasi dalam waktu (<time-taken>s!).'
@@ -102,8 +102,6 @@ commands:
remove: '<prefix>თქვენ წარმატებით მოაშორეთ <ip> შავი ნუსხიდან.'
# მესიჯი, რომელიც იქნება ნაჩვენები, როცა ვინმე დაამატებს IP მისამართს შავ ნუსხაში.
add: '<prefix>თქვენ წარმატებით დაამატეთ <ip> შავ ნუსხაში.'
# მესიჯი, რომელიც იქნება ნაჩვენები, როცა ვინმე დაამატებს IP მისამართს შავ ნუსხაში მაგრამ ეს მისამართი ვერიფიცირებულია
add-warning: '<prefix><red>გაფრთხილება: <white><ip> ამჟამად არის თეთრ ნუსხაში. გთხოვთ მოაშოროთ ეს IP მისამართი თეთრი სიიდან რათა აიცილოთ თავიდან პოტენციური პრობლემები.'
# მესიჯი, რომელიც იქნება ნაჩვენები, როცა ვინმე დაამატებს IP მისამართს შავ ნუსხაში მაგრამ იგი უკვე არის იქ.
ip-duplicate: '<prefix>ეს IP მისამართი ისედაც შავ ნუსხაშია.'
# მესიჯი, რომელიც იქნება ნაჩვენები, როცა ვინმე მოაშორებს IP მისამართს შავი ნუსხიდან მაგრამ იგი ისედაც არ არის იქ.
@@ -183,9 +181,9 @@ verification:
# მესიჯი ჩაწერილი კონსოლში, როცა ახალი მოთამაშე შემოდის
connection: '<username>/<ip> (<protocol>) დაკავშირდა.'
# მესიჯი ჩაწერილი კონსოლში, როცა მოთამაშე წარუმატებლად ასრულებს შემოწმებას
failed: '<ip> (<protocol>) წარუმატებლად გაირა შემოწმება: <reason>'
failed: '<username>/<ip> (<protocol>) წარუმატებლად გაირა შემოწმება: <reason>'
# მესიჯი ჩაწერილი კონსოლში, როცა მოთამაშე შავ ნუსხაშია
blacklisted: '<ip> (<protocol>) ბევრი წარუმატებელი შემოწმების გამო მოთამაშე შავ ნუსხაში შევიდა'
blacklisted: '<username>/<ip> (<protocol>) ბევრი წარუმატებელი შემოწმების გამო მოთამაშე შავ ნუსხაში შევიდა'
# მესიჯი ჩაწერილი კონსოლში, როცა მოთამაშე შემოწმდა წარმატებულად
successful: '<username> წარმატებულად შემოწმდა (<time-taken>s!).'
@@ -10,7 +10,7 @@
#
# 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
# Copyright Jones Development and Sonar Contributors, gelicentieerd onder GPL-3.0
#
# Tijdelijke aanduiding voor elke '<prefix>' in dit configuratiebestand
@@ -100,8 +100,6 @@ commands:
size: '<prefix>De zwarte lijst bevat momenteel <amount> IP-adres(sen).'
# Bericht dat wordt getoond wanneer iemand een IP-adres aan de zwarte lijst toevoegt
add: '<prefix><ip> is succesvol toegevoegd aan de zwarte lijst.'
# Bericht dat wordt getoond wanneer iemand een IP-adres toevoegt aan de zwarte lijst die is geverifieerd
add-warning: '<prefix><red>Attentie: <white><ip> staat momenteel op de witte lijst. Overwegen het verwijderen van het IP-adres uit de lijst met geverifieerde spelers om potentieel te voorkomen problemen.'
# Bericht dat verschijnt wanneer iemand een IP-adres van de zwarte lijst verwijdert
remove: '<prefix><ip> is met succes verwijderd van de zwarte lijst.'
# Bericht dat wordt weergegeven wanneer iemand een IP-adres aan de zwarte lijst toevoegt, maar dit al op de zwarte lijst staat
@@ -183,9 +181,9 @@ verification:
# Bericht dat in de console wordt geregistreerd wanneer een nieuwe speler zich bij de server voegt
connection: '<username>/<ip> (<protocol>) heeft verbinding gemaakt.'
# Bericht dat in de console wordt geregistreerd wanneer de verificatie van een speler mislukt
failed: '<ip> (<protocol>) heeft de bot-controle niet doorstaan vanwege: <reason>'
failed: '<username>/<ip> (<protocol>) heeft de bot-controle niet doorstaan vanwege: <reason>'
# Bericht dat op de console wordt geregistreerd wanneer een speler op de zwarte lijst staat
blacklisted: '<ip> (<protocol>) stond op de zwarte lijst vanwege te veel mislukte pogingen'
blacklisted: '<username>/<ip> (<protocol>) stond op de zwarte lijst vanwege te veel mislukte pogingen'
# Bericht dat op de console wordt geregistreerd wanneer een speler is geverifieerd
successful: '<username> is succesvol geverifieerd (<time-taken>s!).'
@@ -10,7 +10,7 @@
#
# 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
# Copyright Jones Development and Sonar Contributors, na licencji GPL-3.0
#
# Placeholder for every '<prefix>' in this configuration file
@@ -105,8 +105,6 @@ commands:
remove: <prefix>Pomyślnie usunięto <ip> z czarnej listy.
# Message shown when someone adds an IP address to the blacklist
add: <prefix>Pomyślnie dodano <ip> do czarnej listy.
# Message shown when someone adds an IP address to the blacklist that is verified
add-warning: '<prefix><red>Ostrzeżenie: <white><ip> znajduje się obecnie na białej liście. Rozważ usunięcie adresu IP z listy zweryfikowanych graczy, aby uniknąć potencjalnych problemów.'
# Message shown when someone adds an IP address to the blacklist, but it is already blacklisted
ip-duplicate: <prefix>Podany adres IP znajduje się już na czarnej liście.
# Message shown when someone removes an IP address from the blacklist, but it is not blacklisted
@@ -186,9 +184,9 @@ verification:
# Message logged to console whenever a new player joins the server
connection: '<username>/<ip> (<protocol>) połączył się.'
# Message logged to console whenever a player fails verification
failed: '<ip> (<protocol>) nie powiodło się sprawdzenie bota dla: <reason>'
failed: '<username>/<ip> (<protocol>) nie powiodło się sprawdzenie bota dla: <reason>'
# Message logged to console whenever a player is blacklisted
blacklisted: '<ip> (<protocol>) został umieszczony na czarnej liście z powodu zbyt wielu nieudanych prób'
blacklisted: '<username>/<ip> (<protocol>) został umieszczony na czarnej liście z powodu zbyt wielu nieudanych prób'
# Message logged to console whenever a player is verified
successful: '<username> został pomyślnie zweryfikowany (<time-taken>s!).'
@@ -10,7 +10,7 @@
#
# Нужна помощь или есть вопросы? https://jonesdev.xyz/discord
# Исходный код: https://github.com/jonesdevelopment/sonar
# Copyright Jones Development and Sonar Contributors, licensed under GPL-3.0
# Copyright Jones Development and Sonar Contributors, под лицензией GPL-3.0
#
# Плейсхолдер для '<prefix>' в конфиге
@@ -100,8 +100,6 @@ commands:
size: '<prefix>В настоящее время черный список содержит <amount> IP-адрес(ов).'
# Сообщение, которое показывается, когда кто-то добавляет IP-адрес в черный список
add: '<prefix>Успешно добавлен <ip> в черный список.'
# Сообщение, которое отображается, когда кто-то добавляет IP-адрес в черный список, который находится в белом списке
add-warning: '<prefix><red>Предупреждение: <white><ip> находится в белом списке. Рассмотрите возможность удалить IP-адрес из белого списка игроков, чтобы избежать потенциальных ошибок.'
# Сообщение, которое показывается, когда кто-то удаляет IP-адрес из черного списка
remove: '<prefix>Успешно удален <ip> из черного списка.'
# Сообщение, которое отображается, когда кто-то добавляет IP-адрес в черный список, но он уже находится в черном списке
@@ -183,9 +181,9 @@ verification:
# Сообщение, которое показыватся когда на сервер заходит новый игрок
connection: '<username>/<ip> (<protocol>) подключился.'
# Сообщение, которое показывается когда кто-то не прошел проверку на бота
failed: '<ip> (<protocol>) не прошел проверку бота на: <reason>'
failed: '<username>/<ip> (<protocol>) не прошел проверку бота на: <reason>'
# Сообщение, которое показывается когда кто-то было занесен в черный список
blacklisted: '<ip> (<protocol>) был занесен в черный список за слишком большое количество неудачных попыток'
blacklisted: '<username>/<ip> (<protocol>) был занесен в черный список за слишком большое количество неудачных попыток'
# Сообщение, которое выводится в консоль при успешной проверке игрока
successful: '<username> прошел проверку (<time-taken>s!).'
@@ -10,7 +10,7 @@
#
# Yardıma mı ihtiyacınız var veya sorularınız mı var? https://jonesdev.xyz/discord
# Kaynak kodu: https://github.com/jonesdevelopment/sonar
# Jones Development ve Sonar Katkıda Bulunanlar tarafından GPL-3.0 lisansı altında lisanslanmıştır
# Copyright Jones Development and Sonar Contributors, GPL-3.0 altında lisanslanmıştır
#
# Bu yapılandırma dosyasındaki her '<prefix>' için yer tutucu
@@ -100,8 +100,6 @@ commands:
size: '<prefix>Kara liste şu anda <amount> IP adres(ler)i içeriyor.'
# Birisi bir IP adresini kara listeye eklediğinde gösterilen mesaj
add: '<prefix>Başarıyla <ip> kara listeye eklendi.'
# Birisi doğrulanmış olarak işaretlenmiş bir IP adresini kara listeye eklediğinde gösterilen mesaj
add-warning: '<prefix><red>Uyarı: <white><ip> şu anda beyaz listede. Potansiyel sorunları önlemek için IP adresini doğrulanmış oyuncular listesinden kaldırmayı düşünün.'
# Birisi bir IP adresini kara listeden çıkardığında gösterilen mesaj
remove: '<prefix>Başarıyla <ip> kara listeden çıkarıldı.'
# Birisi bir IP adresini kara listeye eklemeye çalıştığında ama zaten kara listede olduğunda gösterilen mesaj
@@ -183,9 +181,9 @@ verification:
# Sunucuya yeni bir oyuncu katıldığında konsola kaydedilen mesaj
connection: '<username>/<ip> (<protocol>) sunucuya bağlandı.'
# Bir oyuncu doğrulamada başarısız olduğunda konsola kaydedilen mesaj
failed: '<ip> (<protocol>) bot korumasında başarısız oldu: <reason>'
failed: '<username>/<ip> (<protocol>) bot korumasında başarısız oldu: <reason>'
# Bir oyuncu kara listeye alındığında konsola kaydedilen mesaj
blacklisted: '<ip> (<protocol>) çok fazla doğrulama denemesinden dolayı kara listeye alındı'
blacklisted: '<username>/<ip> (<protocol>) çok fazla doğrulama denemesinden dolayı kara listeye alındı'
# Bir oyuncu doğrulandığında konsola kaydedilen mesaj
successful: '<username> başarıyla doğrulandı! (<time-taken>s).'
@@ -10,7 +10,7 @@
#
# 需要帮助吗? https://jonesdev.xyz/discord
# 源代码: https://github.com/jonesdevelopment/sonar
# Copyright Jones Development and Sonar Contributors, licensed under GPL-3.0
# Copyright Jones Development and Sonar Contributors, 以 GPL-3.0 许可
#
# <prefix> 占位符
@@ -100,8 +100,6 @@ commands:
size: '<prefix>黑名单中目前有 <amount> 个 IP 地址.'
# 当手动将一个 IP 地址添加到黑名单时显示的消息
add: '<prefix>成功将 IP 地址 <ip> 列入黑名单.'
# 当尝试将白名单中的 IP 地址添加到黑名单中时显示的警告消息
add-warning: '<prefix><red>警告: <white>IP 地址 <ip> 在白名单中. 添加到黑名单则意味着它将被同时从白名单中移除.'
# 当尝试从黑名单中移除一个 IP 地址时显示的消息
remove: '<prefix>成功将 IP 地址 <ip> 移出黑名单.'
# 当尝试向黑名单添加一个已经在黑名单中的 IP 地址时显示的消息
@@ -183,9 +181,9 @@ verification:
# 当玩家进入服务器并验证时显示的消息
connection: '<username>/<ip> (<protocol>) 连接到了服务器.'
# 当玩家未通过检查时打印的消息
failed: '<ip> (<protocol>) 由于原因 <reason> 无法通过检查.'
failed: '<username>/<ip> (<protocol>) 由于原因 <reason> 无法通过检查.'
# 当玩家被自动列入黑名单时打印的消息
blacklisted: '<ip> (<protocol>) 因太多次无法完成验证而被列入黑名单.'
blacklisted: '<username>/<ip> (<protocol>) 因太多次无法完成验证而被列入黑名单.'
# 当玩家完成验证时打印的消息
successful: '<username> 成功通过了验证 (耗时 <time-taken> 秒!).'
@@ -10,7 +10,7 @@
#
# 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
# Copyright Jones Development and Sonar Contributors, licencováno pod GPL-3.0
#
# Notifikace útoků můžou být poslány na discord server pomocí webhooků
@@ -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
#
# Bot-Angriffsbenachrichtigungen können auch über Webhooks an deinen Discord-Server gesendet werden
@@ -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
#
# Les notifications d'attaque bot peuvent être envoyé à discord via un webhook
@@ -10,7 +10,7 @@
#
# Butuh bantuan atau punya 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
#
# Notifikasi serangan bot juga dapat dikirim ke server Discord Anda menggunakan webhook
@@ -10,7 +10,7 @@
#
# 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
# Copyright Jones Development and Sonar Contributors, gelicentieerd onder GPL-3.0
#
# Bot aanval meldingen kunnen ook met behulp van webhooks naar uw Discord-server worden verzonden
@@ -10,7 +10,7 @@
#
# 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
# Copyright Jones Development and Sonar Contributors, na licencji GPL-3.0
#
# Bot attack notifications can also be sent to your Discord server using webhooks
@@ -10,7 +10,7 @@
#
# Нужна помощь или есть вопросы? https://jonesdev.xyz/discord
# Исходный код: https://github.com/jonesdevelopment/sonar
# Copyright Jones Development and Sonar Contributors, licensed under GPL-3.0
# Copyright Jones Development and Sonar Contributors, под лицензией GPL-3.0
#
# Уведомления о бот-атаках могут быть отправлены на ваш сервер Discord с помощью вебхуков
@@ -10,7 +10,7 @@
#
# 需要帮助吗? https://jonesdev.xyz/discord
# 源代码: https://github.com/jonesdevelopment/sonar
# Copyright Jones Development and Sonar Contributors, licensed under GPL-3.0
# Copyright Jones Development and Sonar Contributors, 以 GPL-3.0 许可
#
# 当遭到机器人攻击时也可以将webhook消息发送到Discord服务器中.