Compare commits

...
8 Commits
Author SHA1 Message Date
Jones 0137fdf7e9 Release 2.1.0 (#361) 2024-08-10 14:25:39 +02:00
Jones df14e42a0b Improve the collision check (#360)
* feat: add iron trapdoor block

* feat: add enchantment table block

* fix: rename Captcha to CAPTCHA

* feat: randomize vehicle entity ID

* feat: make collision platform block type configurable

* fix: only send experience bar if it's visible

* feat: change vehicle "timing" to "enabled"

* fix(fallback): captcha-on-fail not catching every scenario

* style: oopsie doopsie
2024-08-10 14:08:28 +02:00
Michel Elkenwaat c30dd0225b Implement missing "fell through blocks" check 2024-08-09 04:22:31 +02:00
Michel Elkenwaat 626fa422bc Fix some grammar mistakes in comments 2024-08-09 01:32:59 +02:00
Michel Elkenwaat f08d9239fe Fix verification.currently-preparing not being used 2024-08-08 23:29:14 +02:00
Michel Elkenwaat 1edd71df04 Fix Bedrock players not being able to be transferred (https://github.com/jonesdevelopment/sonar/issues/356) 2024-08-08 22:28:39 +02:00
aff3bd4a64 Implement Bukkit Support (#347)
* feat: deject packet listener if not needed anymore

* fix(db): dynamically load drivers libraries

* refactor: outline VerifiedPlayerController#exactSize

* fix(db): small synchronization issues in VerifiedPlayerController

* feat: enforce Netty 4.1.x (or higher)

* feat: [wip] Bukkit injection (NMS/OBC stuff)

* fix: include MIT license header for fast varint reading

* fix: allow Netty version to remain unknown

* fix: optimize "command sender -> audience" logic

* fix: extremely rare NoSuchElementException

* fix: remove equals and hashcode annotations

* build(deps): add annotations and simpleyaml to catalog

* feat: [wip] implement Bukkit injection

* feat: [wip] Bukkit packet reading and general improvements

* refactor: small code style improvements

* style: link wiki.vg in comments

* fix: don't store unnecessary pipeline names

* fix: transfer state not working on Bukkit

* fix: possible bypass with the transfer mode exemption

* fix(bukkit): library folder having the wrong name

* feat: make cached player username configurable

* fix: add missing configuration entry to pl.yml

* fix: pipeline order breaking everything ¯\_(ツ)_/¯

* fix: exempt geyser pipelines from being removed... oops

* fix: injection error on 1.20.5+ servers

* build(bukkit): use 1.13.2 spigot api instead of 1.8.8

* fix: legacy plugin warning by setting the api version to 1.13

* fix: use entity type in SpawnEntityPacket

* feat: improve the vehicle check by a lot

* fix: pipeline exception tail error

* fix: only compute player if actually connected (https://github.com/jonesdevelopment/sonar/issues/349)

* style: static import consistency

* fix: MessageToMessageDecoder literally breaking HAProxy

* feat: add more bStats metrics (language and db type)

* style: small code clean up

* feat: remove unnecessary plugin-yml generation arguments

* fix(bukkit): use the actual server name for debugging

* feat: don't shadow adventure-nbt

* chore: link transfer tutorial in configuration

* fix: CI pipeline build error

* build: mark all gradle tasks as non-deamon

* fix: vehicle check 2.0 not being compatible with Geyser

* refactor: *PacketDecoder -> *InboundHandler

* fix: SetPlayerPositionRotationPacket onGround not being used

* feat: recode vehicle check to work with laggy connections

* fix: remove debug message(s)

* feat: recode vehicle check again

* fix: don't remove the entity twice

* feat: [wip] implement Bukkit support (it's looking good)

* fix: wrong exception being caught

* fix: improve error handling in reflective operations

* feat: optimize verification routines

* feat: optimize and improve queue computing

* fix: double/float comparison being broken haha

* style: small comment improvements

* style: fix invalid comments in vehicle check

* Bug fixes in `bukkit-support` branch (#358)

* Lazy load for ServerConnection to support late-bind

* Remove `sonar-inbound-handler` when join server

* Add missing license header in `Lazy.java`

* Update FallbackBukkitInjector

* Change `Bukkit#getScheduler` to `getPlugin().getServer().getScheduler()`

* Inject the new initializer to close connection if the plugin is not enabled.

* Don't remove `DefaultChannelPipeline$TailContext#0` in pipeline.

* Add listener for close uninject player's connection if `late-bind` is disabled.

* Fixed an issue where `max-online-per-ip` was not working properly.

* Get `ServerConnection` in method.

* Change overwrite methods position.

* Move `ChannelInactiveListener` to `sonar-api`

* style(bukkit): create unreflected fields in a static context

* style(bukkit): use lombok Getter for late bind

* build: fix compilation process

* refactor: move `INITIALIZE_LISTENER` from SonarBukkitPlugin to SonarBukkit

* style(bukkit): improve codestyle in the inbound handler

* style: fix wrong comment

---------

Co-authored-by: Michel Elkenwaat <admin@jonesdev.xyz>

* style: add missing credits

* build(deps): bump adventure-platform from 4.3.3 to 4.3.4

* fix: don't assign unused fields

* fix: use encoder function instead of manual encoder check

* fix: use encoder function, not handler function x)

* fix: optimize bukkit encoder function by using `context`, not `get`

* fix: memory leak in the login packet decoder

---------

Co-authored-by: Akkariin <fallencrystal@outlook.com>
2024-08-08 21:13:16 +02:00
Michel Elkenwaat ce1d623d60 Allow general.max-online-per-ip to be 0 2024-08-03 19:29:15 +02:00
70 changed files with 2445 additions and 812 deletions

No files matched your search

+2 -1
View File
@@ -1,3 +1,4 @@
org.gradle.daemon=false
group=xyz.jonesdev.sonar
version=2.0.38
version=2.1.0
description=A lightweight, effective, and easy-to-use anti-bot plugin for Velocity, BungeeCord, and Bukkit.
+4 -4
View File
@@ -1,7 +1,7 @@
[versions]
netty = "4.1.112.Final"
adventure = "4.17.0"
adventure-platform = "4.3.3"
adventure-platform = "4.3.4"
bstats = "3.0.2"
libby = "2.0.0-SNAPSHOT"
@@ -16,8 +16,6 @@ adventure-serializer = { module = "net.kyori:adventure-text-serializer-gson", ve
adventure-nbt = { module = "net.kyori:adventure-nbt", version.ref = "adventure" }
adventure-platform-bukkit = { module = "net.kyori:adventure-platform-bukkit", version.ref = "adventure-platform" }
adventure-platform-bungee = { module = "net.kyori:adventure-platform-bungeecord", version.ref = "adventure-platform" }
adventure-platform-facet = { module = "net.kyori:adventure-platform-facet", version.ref = "adventure-platform" }
adventure-platform-api = { module = "net.kyori:adventure-platform-api", version.ref = "adventure-platform" }
libby-core = { module = "com.alessiodp.libby:libby-core", version.ref = "libby" }
libby-bungee = { module = "com.alessiodp.libby:libby-bungee", version.ref = "libby" }
libby-bukkit = { module = "com.alessiodp.libby:libby-bukkit", version.ref = "libby" }
@@ -26,10 +24,12 @@ bstats-bungee = { module = "org.bstats:bstats-bungeecord", version.ref = "bstats
bstats-bukkit = { module = "org.bstats:bstats-bukkit", version.ref = "bstats" }
bstats-velocity = { module = "org.bstats:bstats-velocity", version.ref = "bstats" }
caffeine = "com.github.ben-manes.caffeine:caffeine:3.1.8"
annotations = "org.jetbrains:annotations:24.0.1"
simpleyaml = "com.github.Carleslc.Simple-YAML:Simple-Yaml:1.8.4"
ormlite = "com.j256.ormlite:ormlite-jdbc:6.1"
lombok = "org.projectlombok:lombok:1.18.34"
capja = "xyz.jonesdev:capja:1.0.3"
bungeecord = "net.md_5:bungeecord-proxy:master-SNAPSHOT"
velocity = "com.velocitypowered:velocity-proxy:3.3.0-SNAPSHOT"
# We have to use 1.8 for backwards compatibility
# We have to use 1.8.8 for backwards compatibility
spigot = "org.spigotmc:spigot-api:1.8.8-R0.1-SNAPSHOT"
+2 -3
View File
@@ -3,11 +3,10 @@ repositories {
}
dependencies {
compileOnly("com.github.Carleslc.Simple-YAML:Simple-Yaml:1.8.4") {
compileOnly(rootProject.libs.simpleyaml) {
exclude(group = "org.yaml")
}
compileOnly("org.jetbrains:annotations:24.0.1")
compileOnly(rootProject.libs.annotations)
}
java.sourceCompatibility = JavaVersion.VERSION_11
@@ -46,6 +46,9 @@ public interface Sonar {
@ApiStatus.Internal
@Nullable Audience audience(final @Nullable UUID uniqueId);
@ApiStatus.Internal
@NotNull Audience sender(final @NotNull Object object);
/**
* @return The platform the plugin is being run on
*/
@@ -17,19 +17,30 @@
package xyz.jonesdev.sonar.api;
import io.netty.channel.ChannelPipeline;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.util.function.Function;
@Getter
@RequiredArgsConstructor
public enum SonarPlatform {
BUKKIT("Bukkit", 19110),
BUNGEE("BungeeCord", 19109),
VELOCITY("Velocity", 19107);
BUKKIT("Bukkit", 19110,
pipeline -> pipeline.context("outbound_config") != null ? "outbound_config" : "encoder",
pipeline -> "packet_handler"),
BUNGEE("BungeeCord", 19109,
pipeline -> "packet-encoder",
pipeline -> "inbound-boss"),
VELOCITY("Velocity", 19107,
pipeline -> "minecraft-encoder",
pipeline -> "handler");
private final String displayName;
/**
* bStats service ID for the respective Sonar platform
*/
private final int metricsId;
private final Function<ChannelPipeline, String> encoderFunction;
private final Function<ChannelPipeline, String> handlerFunction;
}
@@ -21,7 +21,7 @@ import lombok.Getter;
import org.jetbrains.annotations.NotNull;
@Getter
enum Language {
public enum Language {
SYSTEM("system"),
AB("Abkhazian"),
AA("Afar"),
@@ -17,6 +17,7 @@
package xyz.jonesdev.sonar.api.config;
import com.alessiodp.libby.Library;
import com.j256.ormlite.db.DatabaseType;
import lombok.*;
import net.kyori.adventure.text.Component;
@@ -48,6 +49,8 @@ public final class SonarConfiguration {
private final SimpleYamlConfig generalConfig, messagesConfig, webhookConfig;
@Getter
private final File languageFile, pluginFolder;
@Getter
private Language language;
static final LoggerWrapper LOGGER = new LoggerWrapper() {
@@ -121,27 +124,27 @@ public final class SonarConfiguration {
}
// Generate the language file and check what it's set to
Language preferredLanguage = getPreferredLanguage();
if (preferredLanguage == Language.SYSTEM) {
language = getPreferredLanguage();
if (language == Language.SYSTEM) {
try {
// Try using the system language to determine the language file
final String property = System.getProperty("user.language", "en");
preferredLanguage = Language.fromCode(property);
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.", preferredLanguage);
LOGGER.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.", preferredLanguage);
LOGGER.warn("Using default language ({}) for translations.", language);
}
} else {
LOGGER.info("Using custom language ({}) for translations.", preferredLanguage);
LOGGER.info("Using custom language ({}) for translations.", language);
}
// Load all configurations
try {
generalConfig.load(getAsset("config", preferredLanguage));
messagesConfig.load(getAsset("messages", preferredLanguage));
webhookConfig.load(getAsset("webhook", preferredLanguage));
generalConfig.load(getAsset("config", language));
messagesConfig.load(getAsset("messages", language));
webhookConfig.load(getAsset("webhook", language));
} catch (Exception exception) {
throw new IllegalStateException("Error loading configuration", exception);
}
@@ -155,7 +158,7 @@ public final class SonarConfiguration {
// General settings
logPlayerAddresses = generalConfig.getBoolean("general.log-player-addresses");
maxOnlinePerIp = clamp(generalConfig.getInt("general.max-online-per-ip"), 1, 100);
maxOnlinePerIp = clamp(generalConfig.getInt("general.max-online-per-ip"), 0, 99);
// Attack tracker
minPlayersForAttack = clamp(generalConfig.getInt("attack-tracker.min-players-for-attack"), 2, 1024);
@@ -165,12 +168,6 @@ public final class SonarConfiguration {
// Database
database.type = Database.Type.valueOf(generalConfig.getString("database.type").toUpperCase());
database.filename = generalConfig.getString("database.filename");
database.host = generalConfig.getString("database.host");
database.port = generalConfig.getInt("database.port");
database.name = generalConfig.getString("database.name");
database.username = generalConfig.getString("database.username");
database.password = generalConfig.getString("database.password");
database.maximumAge = clamp(generalConfig.getInt("database.maximum-age"), 1, 365);
// Queue
@@ -189,12 +186,11 @@ public final class SonarConfiguration {
}
verification.gravity.enabled = generalConfig.getBoolean("verification.checks.gravity.enabled");
verification.gravity.checkCollisions = generalConfig.getBoolean("verification.checks.gravity.check-collisions");
verification.gravity.checkCollisions = generalConfig.getBoolean("verification.checks.collision.enabled");
verification.gravity.captchaOnFail = generalConfig.getBoolean("verification.checks.gravity.captcha-on-fail");
verification.gravity.maxMovementTicks = clamp(generalConfig.getInt("verification.checks.gravity.max-movement-ticks"), 2, 100);
verification.gravity.gamemode = Verification.Gravity.Gamemode.valueOf(generalConfig.getString("verification.checks.gravity.gamemode"));
verification.vehicle.timing = Verification.Timing.valueOf(generalConfig.getString("verification.checks.vehicle.timing"));
verification.vehicle.enabled = generalConfig.getBoolean("verification.checks.vehicle.enabled");
verification.map.timing = Verification.Timing.valueOf(generalConfig.getString("verification.checks.map-captcha.timing"));
verification.map.scratches = generalConfig.getBoolean("verification.checks.map-captcha.effects.scratches");
@@ -227,6 +223,7 @@ public final class SonarConfiguration {
verification.brand.validRegex = Pattern.compile(generalConfig.getString("verification.checks.client-brand.valid-regex"));
verification.brand.maxLength = generalConfig.getInt("verification.checks.client-brand.max-length");
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);
@@ -241,8 +238,6 @@ public final class SonarConfiguration {
verification.rememberTime = clamp(generalConfig.getInt("verification.remember-time"), 0, 86400000);
verification.blacklistTime = clamp(generalConfig.getInt("verification.blacklist-time"), 0, 86400000);
verification.blacklistThreshold = clamp(generalConfig.getInt("verification.blacklist-threshold"), 0, 100);
verification.whitelistedProtocols.clear();
verification.whitelistedProtocols.addAll(generalConfig.getIntList("verification.whitelisted-protocols"));
verification.blacklistedProtocols.clear();
verification.blacklistedProtocols.addAll(generalConfig.getIntList("verification.blacklisted-protocols"));
@@ -424,25 +419,12 @@ public final class SonarConfiguration {
private boolean enabled;
private boolean checkCollisions;
private boolean captchaOnFail;
private Gamemode gamemode;
private int maxMovementTicks;
@Getter
@RequiredArgsConstructor
public enum Gamemode {
SURVIVAL(0),
CREATIVE(1),
ADVENTURE(2),
// Keep this for backwards compatibility
SPECTATOR(2);
private final int id;
}
}
@Getter
public static final class Vehicle {
private Timing timing;
private boolean enabled;
}
@Getter
@@ -452,6 +434,23 @@ public final class SonarConfiguration {
private Pattern validRegex;
}
private Gamemode gamemode;
@Getter
@RequiredArgsConstructor
public enum Gamemode {
NOT_SET(-1),
SURVIVAL(0),
CREATIVE(1),
ADVENTURE(2);
private final int id;
public boolean isSurvivalOrAdventure() {
return this == SURVIVAL || this == ADVENTURE;
}
}
private boolean checkGeyser;
private boolean logConnections;
private boolean logDuringAttack;
@@ -466,7 +465,6 @@ public final class SonarConfiguration {
private int rememberTime;
private int blacklistTime;
private int blacklistThreshold;
private final Collection<Integer> whitelistedProtocols = new HashSet<>(0);
private final Collection<Integer> blacklistedProtocols = new HashSet<>(0);
private Component tooFastReconnect;
@@ -487,22 +485,38 @@ public final class SonarConfiguration {
@Getter
@RequiredArgsConstructor
public enum Type {
MYSQL("jdbc:mysql://%s:%d/%s", new MysqlDatabaseTypeAdapter()),
MARIADB("jdbc:mariadb://%s:%d/%s", new MariaDbDatabaseTypeAdapter()),
H2("jdbc:h2:file:%s", new H2DatabaseTypeAdapter()),
NONE(null, null);
MYSQL("MySQL", "jdbc:mysql://%s:%d/%s", new MysqlDatabaseTypeAdapter(),
Library.builder()
.groupId("com{}mysql")
.artifactId("mysql-connector-j")
.version("8.4.0")
.relocate("com{}mysql", "xyz{}jonesdev{}sonar{}libs{}mysql")
.build()),
MARIADB("MariaDB", "jdbc:mariadb://%s:%d/%s", new MariaDbDatabaseTypeAdapter(),
Library.builder()
.groupId("org{}mariadb{}jdbc")
.artifactId("mariadb-java-client")
.version("3.4.0")
.relocate("org{}mariadb", "xyz{}jonesdev{}sonar{}libs{}mariadb")
.build()),
H2("H2", "jdbc:h2:file:%s", new H2DatabaseTypeAdapter(),
Library.builder()
.groupId("com{}h2database")
.artifactId("h2")
.version("2.1.214")
.relocate("org{}h2", "xyz{}jonesdev{}sonar{}libs{}h2")
.build()),
NONE("None", null, null, null);
private final String displayName;
private final String connectionString;
private final DatabaseType databaseType;
private final Library databaseDriver;
@Setter
private boolean downloaded;
}
private Type type;
private String filename;
private String host;
private int port;
private String name;
private String username;
private String password;
private int maximumAge;
}
@@ -17,6 +17,7 @@
package xyz.jonesdev.sonar.api.controller;
import com.alessiodp.libby.LibraryManager;
import com.j256.ormlite.dao.Dao;
import com.j256.ormlite.dao.DaoManager;
import com.j256.ormlite.jdbc.JdbcPooledConnectionSource;
@@ -71,7 +72,7 @@ public final class VerifiedPlayerController {
@Getter
private final @NotNull SonarConfiguration.Database.Type cachedDatabaseType;
public VerifiedPlayerController() {
public VerifiedPlayerController(final @NotNull LibraryManager libraryManager) {
final SonarConfiguration.Database database = Sonar.get().getConfig().getDatabase();
// Cache selected database type, so we don't need to call Sonar.get() every time
cachedDatabaseType = database.getType();
@@ -82,22 +83,39 @@ public final class VerifiedPlayerController {
return;
}
database.getType().getDatabaseType().loadDriver();
// Make sure to only download the driver once
if (!cachedDatabaseType.isDownloaded()) {
LOGGER.info("Downloading {} driver version {}",
cachedDatabaseType.getDatabaseType().getDatabaseName(),
cachedDatabaseType.getDatabaseDriver().getVersion());
// Download and load the driver for the current database
libraryManager.loadLibrary(cachedDatabaseType.getDatabaseDriver());
cachedDatabaseType.setDownloaded(true);
}
// Make sure to actually register the loader, so we can use it
cachedDatabaseType.getDatabaseType().loadDriver();
try {
String jdbcURL = String.format(database.getType().getConnectionString(),
database.getHost(), database.getPort(), database.getName());
final String jdbcURL;
// H2 has a different JDBC URL layout
// https://www.codejava.net/java-se/jdbc/connect-to-h2-database-examples
if (cachedDatabaseType == SonarConfiguration.Database.Type.H2) {
final File file = new File(Sonar.get().getConfig().getPluginFolder(),
Sonar.get().getConfig().getDatabase().getFilename());
jdbcURL = String.format(database.getType().getConnectionString(), file.getAbsolutePath());
Sonar.get().getConfig().getGeneralConfig().getString("database.filename"));
jdbcURL = String.format(cachedDatabaseType.getConnectionString(), file.getAbsolutePath());
} else {
// Normal JDBC URL layout for MySQL/MariaDB/...
jdbcURL = String.format(cachedDatabaseType.getConnectionString(),
Sonar.get().getConfig().getGeneralConfig().getString("database.host"),
Sonar.get().getConfig().getGeneralConfig().getInt("database.port"),
Sonar.get().getConfig().getGeneralConfig().getString("database.name"));
}
connectionSource = new JdbcPooledConnectionSource(jdbcURL,
database.getUsername(), database.getPassword(), database.getType().getDatabaseType());
Sonar.get().getConfig().getGeneralConfig().getString("database.username"),
Sonar.get().getConfig().getGeneralConfig().getString("database.password"),
cachedDatabaseType.getDatabaseType());
// Create database table
try {
@@ -253,37 +271,44 @@ public final class VerifiedPlayerController {
*
* @return Estimated size of the local cache
*/
public synchronized int estimatedSize() {
/*
* TODO: Work on this
* return MAP.values().stream()
* .mapToInt(Collection::size)
* .sum();
*/
public int estimatedSize() {
return cache.size();
}
/**
* @return List of UUIDs associated with one IP address
* 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 then if the database type is set,
* Clear the local cache, and, if the database type is set,
* remove the table from the database.
*/
public void clearAll() {
try {
cache.clear();
public synchronized void clearAll() {
cache.clear();
// Only update the column if the database type is not NONE
if (cachedDatabaseType != SonarConfiguration.Database.Type.NONE) {
// Delete the entire table from the database, if necessary
if (cachedDatabaseType != SonarConfiguration.Database.Type.NONE) {
try {
dao.deleteBuilder().delete();
} catch (SQLException exception) {
LOGGER.error("Error trying to clear entries: {}", exception);
}
} catch (SQLException exception) {
LOGGER.error("Error trying to clear entries: {}", exception);
}
}
@@ -17,7 +17,6 @@
package xyz.jonesdev.sonar.api.event.impl;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
@@ -26,7 +25,6 @@ import xyz.jonesdev.sonar.api.event.SonarEvent;
@Getter
@ToString
@EqualsAndHashCode
@RequiredArgsConstructor
public final class AttackMitigatedEvent implements SonarEvent {
private final AttackTracker.AttackStatistics attackStatistics;
@@ -17,7 +17,6 @@
package xyz.jonesdev.sonar.api.event.impl;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
@@ -27,7 +26,6 @@ import xyz.jonesdev.sonar.api.fallback.FallbackUser;
@Getter
@ToString
@EqualsAndHashCode
@RequiredArgsConstructor
public final class UserBlacklistedEvent implements SonarEvent {
@ApiStatus.Experimental
@@ -17,7 +17,6 @@
package xyz.jonesdev.sonar.api.event.impl;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
@@ -27,7 +26,6 @@ import xyz.jonesdev.sonar.api.fallback.FallbackUser;
@Getter
@ToString
@EqualsAndHashCode
@RequiredArgsConstructor
public final class UserVerifyFailedEvent implements SonarEvent {
private final @NotNull FallbackUser user;
@@ -17,7 +17,6 @@
package xyz.jonesdev.sonar.api.event.impl;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
@@ -26,7 +25,6 @@ import xyz.jonesdev.sonar.api.fallback.FallbackUser;
@Getter
@ToString
@EqualsAndHashCode
@RequiredArgsConstructor
public final class UserVerifyJoinEvent implements SonarEvent {
private final String username;
@@ -17,7 +17,6 @@
package xyz.jonesdev.sonar.api.event.impl;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
@@ -28,7 +27,6 @@ import java.util.UUID;
@Getter
@ToString
@EqualsAndHashCode
@RequiredArgsConstructor
public final class UserVerifySuccessEvent implements SonarEvent {
private final String username;
@@ -75,10 +75,6 @@ public final class Fallback {
return shouldPerform(Sonar.get().getConfig().getVerification().getMap().getTiming());
}
public boolean shouldPerformVehicleCheck() {
return shouldPerform(Sonar.get().getConfig().getVerification().getVehicle().getTiming());
}
private static boolean shouldPerform(final SonarConfiguration.Verification.Timing timing) {
return timing == SonarConfiguration.Verification.Timing.ALWAYS
|| (timing == SonarConfiguration.Verification.Timing.DURING_ATTACK
@@ -18,8 +18,14 @@
package xyz.jonesdev.sonar.api.fallback;
public interface FallbackPipelines {
String FALLBACK_HANDLER = "sonar-fallback-handler";
String FALLBACK_PACKET_ENCODER = "sonar-fallback-encoder";
String FALLBACK_PACKET_DECODER = "sonar-fallback-decoder";
String FALLBACK_BANDWIDTH = "sonar-fallback-bandwidth";
String FALLBACK_INACTIVE_LISTENER = "sonar-inactive-listener";
String FALLBACK_INBOUND_HANDLER = "sonar-inbound-handler";
String FALLBACK_FRAME_DECODER = "sonar-frame-decoder";
String FALLBACK_FRAME_ENCODER = "sonar-frame-encoder";
String FALLBACK_TIMEOUT = "sonar-timeout";
String FALLBACK_PACKET_HANDLER = "sonar-packet-handler";
String FALLBACK_PACKET_ENCODER = "sonar-packet-encoder";
String FALLBACK_PACKET_DECODER = "sonar-packet-decoder";
String FALLBACK_TAIL_EXCEPTIONS = "sonar-exception-tail";
String FALLBACK_BANDWIDTH = "sonar-bandwidth-counter";
}
@@ -44,6 +44,9 @@ public interface FallbackUser {
@NotNull
SystemTimer getLoginTimer();
@NotNull
UUID getOfflineUuid();
boolean isReceivedClientSettings();
void setReceivedClientSettings(final boolean receivedClientSettings);
@@ -65,16 +68,10 @@ public interface FallbackUser {
/**
* Takes over the channel and begins the verification process
*
* @param username Username of the player
* @param uuid UUID of the player
* @param encoder Name of the encoder pipeline
* @param decoder Name of the decoder pipeline
* @param timeout Name of the read timeout pipeline
* @param handler Name of the main pipeline
* @param username Username of the player
* @param offlineUuid UUID of the player
*/
void hijack(final @NotNull String username, final @NotNull UUID uuid,
final @NotNull String encoder, final @NotNull String decoder,
final @NotNull String timeout, final @NotNull String handler);
void hijack(final @NotNull String username, final @NotNull UUID offlineUuid);
/**
* Sends a packet/message to the player
+5 -6
View File
@@ -6,14 +6,13 @@ plugins {
bukkit {
name = rootProject.name
description = rootProject.description
version = rootProject.version.toString()
main = "xyz.jonesdev.sonar.bukkit.SonarBukkitPlugin"
authors = listOf("Jones Development", "Sonar Contributors")
website = "https://jonesdev.xyz/discord/"
load = BukkitPluginDescription.PluginLoadOrder.POSTWORLD
softDepend = listOf("Geyser-Spigot", "floodgate", "Protocolize", "ProtocolSupport",
"ViaVersion", "packetevents", "ProtocolLib", "FastLogin")
apiVersion = "1.13" // ignore legacy plugin warning
foliaSupported = true
commands {
@@ -33,11 +32,11 @@ dependencies {
implementation(project(":common"))
compileOnly(rootProject.libs.spigot)
testCompileOnly(rootProject.libs.spigot)
implementation(rootProject.libs.adventure.platform.bukkit)
implementation(rootProject.libs.adventure.platform.api)
implementation(rootProject.libs.adventure.platform.facet)
implementation(rootProject.libs.adventure.nbt)
implementation(rootProject.libs.adventure.platform.bukkit) {
exclude(module = "adventure-nbt")
}
implementation(rootProject.libs.bstats.bukkit)
implementation(rootProject.libs.libby.bukkit)
}
@@ -23,24 +23,24 @@ import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.platform.bukkit.BukkitAudiences;
import org.bstats.bukkit.Metrics;
import org.bstats.charts.SimplePie;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import xyz.jonesdev.sonar.api.SonarPlatform;
import xyz.jonesdev.sonar.api.logger.LoggerWrapper;
import xyz.jonesdev.sonar.bukkit.command.BukkitSonarCommand;
import xyz.jonesdev.sonar.bukkit.fallback.FallbackBukkitInjector;
import xyz.jonesdev.sonar.bukkit.listener.BukkitJoinListener;
import xyz.jonesdev.sonar.common.boot.SonarBootstrap;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
@Getter
public final class SonarBukkit extends SonarBootstrap<SonarBukkitPlugin> {
public static SonarBukkit INSTANCE;
public SonarBukkit(final @NotNull SonarBukkitPlugin plugin) {
super(plugin, SonarPlatform.BUKKIT, plugin.getDataFolder(),
new BukkitLibraryManager(plugin, plugin.getDataFolder().getName()));
INSTANCE = this;
super(plugin, SonarPlatform.BUKKIT, plugin.getDataFolder(), new BukkitLibraryManager(plugin));
}
/**
@@ -56,6 +56,11 @@ public final class SonarBukkit extends SonarBootstrap<SonarBukkitPlugin> {
return bukkitAudiences.player(uniqueId);
}
@Override
public @NotNull Audience sender(final @NotNull Object object) {
return bukkitAudiences.sender((CommandSender) object);
}
/**
* Create a wrapper for the plugin logger, so we can use it outside
* the velocity module.
@@ -82,6 +87,8 @@ public final class SonarBukkit extends SonarBootstrap<SonarBukkitPlugin> {
private Metrics metrics;
public static final CompletableFuture<Void> INITIALIZE_LISTENER = new CompletableFuture<>();
@Override
public void enable() {
// Initialize bStats.org metrics
@@ -92,9 +99,23 @@ public final class SonarBukkit extends SonarBootstrap<SonarBukkitPlugin> {
() -> getConfig().getVerification().getTiming().getDisplayName()));
metrics.addCustomChart(new SimplePie("captcha",
() -> getConfig().getVerification().getMap().getTiming().getDisplayName()));
metrics.addCustomChart(new SimplePie("language",
() -> getConfig().getLanguage().getName()));
metrics.addCustomChart(new SimplePie("database_type",
() -> getConfig().getDatabase().getType().getDisplayName()));
// Register Sonar command
Objects.requireNonNull(getPlugin().getCommand("sonar")).setExecutor(new BukkitSonarCommand());
// Try to inject into the server
if (FallbackBukkitInjector.isLateBindEnabled()) {
getPlugin().getServer().getScheduler().runTask(getPlugin(), FallbackBukkitInjector::inject);
} else {
getPlugin().getServer().getPluginManager().registerEvents(new BukkitJoinListener(), getPlugin());
}
// Let the injector know that the plugin has been enabled
INITIALIZE_LISTENER.complete(null);
}
@Override
@@ -18,10 +18,19 @@
package xyz.jonesdev.sonar.bukkit;
import org.bukkit.plugin.java.JavaPlugin;
import xyz.jonesdev.sonar.bukkit.fallback.FallbackBukkitInjector;
public final class SonarBukkitPlugin extends JavaPlugin {
private SonarBukkit bootstrap;
@Override
public void onLoad() {
// Inject early if late-bind is disabled
if (!FallbackBukkitInjector.isLateBindEnabled()) {
FallbackBukkitInjector.inject();
}
}
@Override
public void onEnable() {
bootstrap = new SonarBukkit(this);
@@ -23,9 +23,9 @@ import org.bukkit.command.CommandSender;
import org.bukkit.command.TabExecutor;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.command.InvocationSource;
import xyz.jonesdev.sonar.api.command.SonarCommand;
import xyz.jonesdev.sonar.bukkit.SonarBukkit;
import java.util.List;
@@ -41,7 +41,7 @@ public final class BukkitSonarCommand implements CommandExecutor, TabExecutor, S
// Create our own invocation source wrapper to handle messages properly
final InvocationSource invocationSource = new InvocationSource(
sender instanceof Player ? ((Player) sender).getUniqueId() : null,
SonarBukkit.INSTANCE.getBukkitAudiences().sender(sender),
Sonar.get().sender(sender),
sender::hasPermission);
// Pass the invocation source and command arguments to our command handler
handle(invocationSource, args);
@@ -0,0 +1,95 @@
/*
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.bukkit.fallback;
import lombok.Getter;
@Getter
public enum BukkitServerVersion {
MINECRAFT_1_7_10,
MINECRAFT_1_8,
MINECRAFT_1_8_3,
MINECRAFT_1_8_8,
MINECRAFT_1_9,
MINECRAFT_1_9_2,
MINECRAFT_1_9_4,
MINECRAFT_1_10,
MINECRAFT_1_10_1,
MINECRAFT_1_10_2,
MINECRAFT_1_11,
MINECRAFT_1_11_2,
MINECRAFT_1_12,
MINECRAFT_1_12_1,
MINECRAFT_1_12_2,
MINECRAFT_1_13,
MINECRAFT_1_13_1,
MINECRAFT_1_13_2,
MINECRAFT_1_14,
MINECRAFT_1_14_1,
MINECRAFT_1_14_2,
MINECRAFT_1_14_3,
MINECRAFT_1_14_4,
MINECRAFT_1_15,
MINECRAFT_1_15_1,
MINECRAFT_1_15_2,
MINECRAFT_1_16,
MINECRAFT_1_16_1,
MINECRAFT_1_16_2,
MINECRAFT_1_16_3,
MINECRAFT_1_16_4,
MINECRAFT_1_16_5,
MINECRAFT_1_17,
MINECRAFT_1_17_1,
MINECRAFT_1_18,
MINECRAFT_1_18_1,
MINECRAFT_1_18_2,
MINECRAFT_1_19,
MINECRAFT_1_19_1,
MINECRAFT_1_19_2,
MINECRAFT_1_19_3,
MINECRAFT_1_19_4,
MINECRAFT_1_20,
MINECRAFT_1_20_1,
MINECRAFT_1_20_2,
MINECRAFT_1_20_3,
MINECRAFT_1_20_4,
MINECRAFT_1_20_5,
MINECRAFT_1_20_6,
MINECRAFT_1_21;
static final BukkitServerVersion[] REVERSED_VALUES;
static {
REVERSED_VALUES = values().clone();
final int size = REVERSED_VALUES.length;
final int half = size / 2;
for (int i = 0; i < half; i++) {
final BukkitServerVersion temp = REVERSED_VALUES[i];
REVERSED_VALUES[i] = REVERSED_VALUES[size - 1 - i];
REVERSED_VALUES[size - 1 - i] = temp;
}
}
private final String release;
BukkitServerVersion() {
this.release = name().substring(10).replace("_", ".");
}
}
@@ -0,0 +1,250 @@
package xyz.jonesdev.sonar.bukkit.fallback;
import io.netty.buffer.ByteBufAllocator;
import io.netty.channel.*;
import io.netty.util.Attribute;
import io.netty.util.AttributeKey;
import io.netty.util.concurrent.EventExecutor;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import java.net.SocketAddress;
import java.util.HashSet;
import java.util.Set;
public final class ChannelInactiveListener extends ChannelInboundHandlerAdapter {
private final Set<ChannelInboundHandler> handlers = new HashSet<>();
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
final ProxiedChannelHandlerContext proxy = new ProxiedChannelHandlerContext(ctx);
for (final ChannelInboundHandler handler : handlers) {
try {
handler.channelInactive(proxy);
} catch (Exception exception) {
handler.exceptionCaught(proxy, exception);
}
}
super.channelInactive(ctx);
}
public void add(ChannelHandler handler) {
if (handler instanceof ChannelInboundHandler) {
handlers.add((ChannelInboundHandler) handler);
}
}
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
public static final class ProxiedChannelHandlerContext implements ChannelHandlerContext {
private final ChannelHandlerContext ctx;
// Overwrite to prevent call this method on next handler
@Override
public ChannelHandlerContext fireChannelInactive() {
return this;
}
@Override
public ChannelHandlerContext fireExceptionCaught(Throwable throwable) {
return this;
}
@Override
public Channel channel() {
return ctx.channel();
}
@Override
public EventExecutor executor() {
return ctx.executor();
}
@Override
public String name() {
return ctx.name();
}
@Override
public ChannelHandler handler() {
return ctx.handler();
}
@Override
public boolean isRemoved() {
return ctx.isRemoved();
}
@Override
public ChannelHandlerContext fireChannelRegistered() {
return ctx.fireChannelRegistered();
}
@Override
public ChannelHandlerContext fireChannelUnregistered() {
return ctx.fireChannelUnregistered();
}
@Override
public ChannelHandlerContext fireChannelActive() {
return ctx.fireChannelActive();
}
@Override
public ChannelHandlerContext fireUserEventTriggered(Object o) {
return ctx.fireUserEventTriggered(o);
}
@Override
public ChannelHandlerContext fireChannelRead(Object o) {
return ctx.fireChannelRead(o);
}
@Override
public ChannelHandlerContext fireChannelReadComplete() {
return ctx.fireChannelReadComplete();
}
@Override
public ChannelHandlerContext fireChannelWritabilityChanged() {
return ctx.fireChannelWritabilityChanged();
}
@Override
public ChannelFuture bind(SocketAddress socketAddress) {
return ctx.bind(socketAddress);
}
@Override
public ChannelFuture connect(SocketAddress socketAddress) {
return ctx.connect(socketAddress);
}
@Override
public ChannelFuture connect(SocketAddress socketAddress, SocketAddress socketAddress1) {
return ctx.connect(socketAddress, socketAddress1);
}
@Override
public ChannelFuture disconnect() {
return ctx.disconnect();
}
@Override
public ChannelFuture close() {
return ctx.close();
}
@Override
public ChannelFuture deregister() {
return ctx.deregister();
}
@Override
public ChannelFuture bind(SocketAddress socketAddress, ChannelPromise channelPromise) {
return ctx.bind(socketAddress, channelPromise);
}
@Override
public ChannelFuture connect(SocketAddress socketAddress, ChannelPromise channelPromise) {
return ctx.connect(socketAddress, channelPromise);
}
@Override
public ChannelFuture connect(SocketAddress socketAddress, SocketAddress socketAddress1, ChannelPromise channelPromise) {
return ctx.connect(socketAddress, socketAddress1, channelPromise);
}
@Override
public ChannelFuture disconnect(ChannelPromise channelPromise) {
return ctx.disconnect(channelPromise);
}
@Override
public ChannelFuture close(ChannelPromise channelPromise) {
return ctx.close(channelPromise);
}
@Override
public ChannelFuture deregister(ChannelPromise channelPromise) {
return ctx.deregister(channelPromise);
}
@Override
public ChannelHandlerContext read() {
return ctx.read();
}
@Override
public ChannelFuture write(Object o) {
return ctx.write(o);
}
@Override
public ChannelFuture write(Object o, ChannelPromise channelPromise) {
return ctx.write(o, channelPromise);
}
@Override
public ChannelHandlerContext flush() {
return ctx.flush();
}
@Override
public ChannelFuture writeAndFlush(Object o, ChannelPromise channelPromise) {
return ctx.writeAndFlush(o, channelPromise);
}
@Override
public ChannelFuture writeAndFlush(Object o) {
return ctx.writeAndFlush(o);
}
@Override
public ChannelPromise newPromise() {
return ctx.newPromise();
}
@Override
public ChannelProgressivePromise newProgressivePromise() {
return ctx.newProgressivePromise();
}
@Override
public ChannelFuture newSucceededFuture() {
return ctx.newSucceededFuture();
}
@Override
public ChannelFuture newFailedFuture(Throwable throwable) {
return ctx.newFailedFuture(throwable);
}
@Override
public ChannelPromise voidPromise() {
return ctx.voidPromise();
}
@Override
public ChannelPipeline pipeline() {
return ctx.pipeline();
}
@Override
public ByteBufAllocator alloc() {
return ctx.alloc();
}
@Override
@Deprecated
public <T> Attribute<T> attr(AttributeKey<T> attributeKey) {
return ctx.attr(attributeKey);
}
@Override
@Deprecated
public <T> boolean hasAttr(AttributeKey<T> attributeKey) {
return ctx.hasAttr(attributeKey);
}
}
}
@@ -0,0 +1,149 @@
/*
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.bukkit.fallback;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.CorruptedFrameException;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.FallbackInboundHandlerAdapter;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.handshake.HandshakePacket;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.login.LoginStartPacket;
import java.net.InetSocketAddress;
import java.util.Objects;
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_INACTIVE_LISTENER;
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_INBOUND_HANDLER;
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry.Direction.SERVERBOUND;
import static xyz.jonesdev.sonar.common.fallback.protocol.packets.handshake.HandshakePacket.*;
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.readVarInt;
final class FallbackBukkitInboundHandler extends FallbackInboundHandlerAdapter {
FallbackBukkitInboundHandler() {
updateRegistry(FallbackPacketRegistry.HANDSHAKE, DEFAULT_PROTOCOL_VERSION);
channelRemovalListener = (pipeline, name, handler) -> {
final var inactiveListener = (ChannelInactiveListener) pipeline.get(FALLBACK_INACTIVE_LISTENER);
if (inactiveListener != null) {
inactiveListener.add(handler);
}
};
}
private FallbackPacketRegistry.ProtocolRegistry registry;
private static final ProtocolVersion DEFAULT_PROTOCOL_VERSION = ProtocolVersion.MINECRAFT_1_7_2;
public void updateRegistry(final @NotNull FallbackPacketRegistry registry,
final @NotNull ProtocolVersion protocolVersion) {
this.registry = registry.getProtocolRegistry(SERVERBOUND, protocolVersion);
}
@Override
public void channelRead(final @NotNull ChannelHandlerContext ctx, final @NotNull Object msg) throws Exception {
if (msg instanceof ByteBuf) {
final ByteBuf byteBuf = (ByteBuf) msg;
// Reset the reader index, so we can read the content
// Make sure to store the original reader index
final int originalReaderIndex = byteBuf.readerIndex();
byteBuf.readerIndex(0);
// Don't continue if the ByteBuf isn't readable or the channel is closed
if (!ctx.channel().isActive() || !byteBuf.isReadable()) {
byteBuf.release();
return;
}
final int packetId = readVarInt(byteBuf);
final FallbackPacket packet = registry.createPacket(packetId);
// Skip the packet if it's not registered within Sonar's packet registry
if (packet == null) {
// Release the ByteBuf to avoid memory leaks
byteBuf.release();
return;
}
try {
// Try to decode the packet for the given protocol version
packet.decode(byteBuf, protocolVersion == null ? DEFAULT_PROTOCOL_VERSION : protocolVersion);
} catch (Throwable throwable) {
// Release the ByteBuf to avoid memory leaks
byteBuf.release();
throw new CorruptedFrameException("Failed to decode packet");
}
// Check if the packet still has bytes left after we decoded it
if (byteBuf.isReadable()) {
// Release the ByteBuf to avoid memory leaks
byteBuf.release();
throw new CorruptedFrameException("Could not read packet to end");
}
// Useful resources:
// https://wiki.vg/Protocol#Handshaking
// https://wiki.vg/Protocol#Login
if (packet instanceof HandshakePacket) {
final HandshakePacket handshake = (HandshakePacket) packet;
switch (handshake.getIntent()) {
case STATUS:
// We don't care about server pings or transfers; remove the handler
ctx.channel().pipeline().remove(this);
break;
case LOGIN:
case TRANSFER:
// Let the actual handler know about the handshake packet
handleHandshake(ctx.channel(), handshake.getHostname(), handshake.getProtocolVersionId());
// Be ready for the next packet (which is supposed to be a login packet)
updateRegistry(FallbackPacketRegistry.LOGIN, Objects.requireNonNull(protocolVersion));
break;
default:
throw new CorruptedFrameException("Unknown handshake intent " + handshake.getIntent());
}
} else if (packet instanceof LoginStartPacket) {
final LoginStartPacket loginStart = (LoginStartPacket) packet;
final InetSocketAddress socketAddress = (InetSocketAddress) ctx.channel().remoteAddress();
// We've done our job - deject this pipeline
ctx.channel().pipeline().remove(this);
// Let Sonar process the login packet
handleLogin(ctx.channel(), ctx, () -> {
byteBuf.readerIndex(originalReaderIndex);
ctx.fireChannelRead(byteBuf.retain());
final ChannelHandler inboundHandler = ctx.channel().pipeline().remove(FALLBACK_INBOUND_HANDLER);
if (inboundHandler != null) {
channelRemovalListener.accept(ctx.pipeline(), FALLBACK_INBOUND_HANDLER, inboundHandler);
}
}, loginStart.getUsername(), socketAddress);
// Release the ByteBuf to avoid memory leaks
byteBuf.release();
return;
}
byteBuf.readerIndex(originalReaderIndex);
ctx.fireChannelRead(byteBuf);
}
}
}
@@ -0,0 +1,272 @@
/*
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.bukkit.fallback;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelInitializer;
import lombok.Getter;
import lombok.experimental.UtilityClass;
import org.bukkit.Bukkit;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.ReflectiveOperationException;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.fallback.FallbackPipelines;
import xyz.jonesdev.sonar.bukkit.SonarBukkit;
import xyz.jonesdev.sonar.common.fallback.netty.FallbackInjectedChannelInitializer;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.List;
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_PACKET_DECODER;
// Check out these links if you want to see some more magic
// https://github.com/retrooper/packetevents/blob/2.0/spigot/src/main/java/io/github/retrooper/packetevents/util/SpigotReflectionUtil.java
// https://github.com/dmulloy2/ProtocolLib/blob/master/TinyProtocol/src/main/java/com/comphenix/tinyprotocol/TinyProtocol.java
// https://github.com/ViaVersion/ViaVersion/blob/master/bukkit/src/main/java/com/viaversion/viaversion/bukkit/handlers/BukkitChannelInitializer.java
@UtilityClass
public class FallbackBukkitInjector {
private final String LEGACY_NMS_PACKAGE;
private final String OBC_PACKAGE;
private final BukkitServerVersion SERVER_VERSION;
private final boolean USES_LEGACY_PACKAGING, OBFUSCATED;
private final Class<?> MINECRAFT_SERVER_CLASS;
private final Class<?> CRAFTBUKKIT_SERVER_CLASS;
private final Class<?> SERVER_CONNECTION_CLASS;
private final Object SERVER_INSTANCE;
@Getter
private boolean lateBindEnabled;
static {
SERVER_VERSION = resolveServerVersion();
USES_LEGACY_PACKAGING = SERVER_VERSION.compareTo(BukkitServerVersion.MINECRAFT_1_17) < 0;
try {
final String bukkitPackage = Bukkit.getServer().getClass().getPackage().getName();
String modifiedPackageName;
if (SERVER_VERSION.compareTo(BukkitServerVersion.MINECRAFT_1_20_5) >= 0) {
// Example: org.bukkit.craftbukkit
modifiedPackageName = bukkitPackage.split("\\.")[2];
} else {
// Example: org.bukkit.craftbukkit.v1_16_R3
modifiedPackageName = bukkitPackage.split("\\.")[3];
}
LEGACY_NMS_PACKAGE = "net.minecraft.server." + modifiedPackageName + ".";
OBC_PACKAGE = bukkitPackage + ".";
OBFUSCATED = isObfuscated();
// Minecraft classes
MINECRAFT_SERVER_CLASS = getNMSClass("server.MinecraftServer", "MinecraftServer");
SERVER_CONNECTION_CLASS = getNMSClass(OBFUSCATED ? "server.network.ServerConnection" : "server.network.ServerConnectionListener", "ServerConnection");
// CraftBukkit classes
CRAFTBUKKIT_SERVER_CLASS = getOBCClass("CraftServer");
Object minecraftServerInstance;
try {
// 1.20.5+
minecraftServerInstance = getFieldAt(MINECRAFT_SERVER_CLASS, MINECRAFT_SERVER_CLASS, 0).get(null);
} catch (Exception exception) {
minecraftServerInstance = getFieldAt(CRAFTBUKKIT_SERVER_CLASS, MINECRAFT_SERVER_CLASS, 0).get(Bukkit.getServer());
}
SERVER_INSTANCE = minecraftServerInstance;
checkLateBind();
} catch (Exception exception) {
throw new ReflectiveOperationException(exception);
}
}
private void checkLateBind() {
try {
try {
Class.forName("org.bukkit.event.server.ServerLoadEvent");
// late-bind does not exist on these versions, ignore this case
return;
} catch (ClassNotFoundException ignored) {
}
final Class<?> spigotConfiguration = Class.forName("org.spigotmc.SpigotConfig");
final Method initFieldMethod = spigotConfiguration.getDeclaredMethod("lateBind");
initFieldMethod.setAccessible(true);
initFieldMethod.invoke(null);
if ((boolean) spigotConfiguration.getField("lateBind").get(null)) {
lateBindEnabled = true;
}
} catch (java.lang.ReflectiveOperationException ignore) {
}
}
public @NotNull Field getFieldAt(final @NotNull Class<?> clazz,
final @NotNull Class<?> type,
final int index) {
int currentIndex = 0;
for (final Field field : clazz.getDeclaredFields()) {
field.setAccessible(true);
if (type.isAssignableFrom(field.getType())) {
if (currentIndex++ == index) {
return field;
}
}
}
if (clazz.getSuperclass() != null) {
return getFieldAt(clazz.getSuperclass(), type, index);
}
throw new IllegalStateException("Could not find field #" + index + " in " + clazz.getName());
}
public static @NotNull Field findField(final boolean checkSuperClass,
final @NotNull Class<?> clazz,
final String @NotNull ... types) throws NoSuchFieldException {
for (final Field field : clazz.getDeclaredFields()) {
final String fieldTypeName = field.getType().getSimpleName();
for (final String type : types) {
if (fieldTypeName.equals(type)) {
if (!Modifier.isPublic(field.getModifiers())) {
field.setAccessible(true);
}
return field;
}
}
}
if (checkSuperClass && clazz != Object.class && clazz.getSuperclass() != null) {
return findField(true, clazz.getSuperclass(), types);
}
throw new NoSuchFieldException(types[0]);
}
private @NotNull BukkitServerVersion resolveServerVersion() {
final String bukkitVersion = Bukkit.getBukkitVersion();
for (final BukkitServerVersion serverVersion : BukkitServerVersion.REVERSED_VALUES) {
if (bukkitVersion.contains(serverVersion.getRelease())) {
return serverVersion;
}
}
// Throw an error if we cannot find the server version
throw new IllegalStateException("Could not find server version " + bukkitVersion);
}
private boolean isObfuscated() {
try {
Class.forName("net.minecraft.server.network.PlayerConnection");
return true;
} catch (ClassNotFoundException exception) {
return false;
}
}
private @NotNull Class<?> getNMSClass(final String modern, final String legacy) throws ClassNotFoundException {
return Class.forName(USES_LEGACY_PACKAGING ? LEGACY_NMS_PACKAGE + legacy : "net.minecraft." + modern);
}
public @NotNull Class<?> getOBCClass(final String clazz) throws ClassNotFoundException {
return Class.forName(OBC_PACKAGE + clazz);
}
@SuppressWarnings("unchecked")
public void inject() {
try {
for (int i = 0; i < 2; i++) {
final Field field = getFieldAt(SERVER_CONNECTION_CLASS, List.class, i);
// Check if the field has the correct generic type;
// We need this field to be List<ChannelFuture>
if (!field.getGenericType().getTypeName().contains(ChannelFuture.class.getName())) {
continue;
}
final var list = (List<?>) field.get(getFieldAt(MINECRAFT_SERVER_CLASS, SERVER_CONNECTION_CLASS, 0).get(SERVER_INSTANCE));
for (final Object object : list) {
final ChannelFuture channelFuture = (ChannelFuture) object;
final List<String> names = channelFuture.channel().pipeline().names();
ChannelHandler bootstrap = null;
for (final String name : names) {
try {
final ChannelHandler handler = channelFuture.channel().pipeline().get(name);
final Field childHandlerField = handler.getClass().getDeclaredField("childHandler");
childHandlerField.setAccessible(true);
final var childHandler = (ChannelInitializer<?>) childHandlerField.get(handler);
if (childHandler != null) {
bootstrap = handler;
}
break;
} catch (Exception exception) {
// Some plugins modify this, so we better skip any exceptions
}
}
if (bootstrap == null) {
// Default to the first channel future if we couldn't find a childHandler
bootstrap = channelFuture.channel().pipeline().first();
}
final Field childHandlerField = bootstrap.getClass().getDeclaredField("childHandler");
childHandlerField.setAccessible(true);
final var originalInitializer = (ChannelInitializer<Channel>) childHandlerField.get(bootstrap);
ChannelHandler finalBootstrap = bootstrap;
// Inject our own channel initializer into the original field
// If the plugin is not fully enabled but executes the injection,
// put an initializer to close the new connection automatically.
if (!SonarBukkit.INITIALIZE_LISTENER.isDone()) {
final ChannelInitializer<Channel> initializer = new ChannelInitializer<>() {
@Override
protected void initChannel(Channel channel) {
channel.close();
}
};
childHandlerField.set(finalBootstrap, initializer);
}
SonarBukkit.INITIALIZE_LISTENER.thenAccept(v -> {
try {
childHandlerField.set(finalBootstrap, new FallbackInjectedChannelInitializer(originalInitializer,
pipeline -> {
pipeline.addAfter("splitter", FALLBACK_PACKET_DECODER, new FallbackBukkitInboundHandler());
pipeline.addFirst(FallbackPipelines.FALLBACK_INACTIVE_LISTENER, new ChannelInactiveListener());
}
));
} catch (IllegalAccessException exception) {
throw new ReflectiveOperationException(exception);
}
});
}
return;
}
} catch (Exception exception) {
Sonar.get().getLogger().error("An error occurred while injecting {}", exception);
}
}
}
@@ -0,0 +1,87 @@
/*
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.bukkit.listener;
import io.netty.channel.Channel;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.bukkit.fallback.FallbackBukkitInjector;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_BANDWIDTH;
// Taken from
// https://github.com/ViaVersion/ViaVersion/blob/master/bukkit/src/main/java/com/viaversion/viaversion/bukkit/listeners/JoinListener.java
public final class BukkitJoinListener implements Listener {
private static MethodHandle handleMethod;
// ServerGamePacketListenerImpl aka. PlayerConnection
private static MethodHandle packetListenerField;
private static MethodHandle managerField;
private static MethodHandle channelField;
private static boolean initialized;
static {
try {
final Method handleMethod = FallbackBukkitInjector.getOBCClass("entity.CraftPlayer")
.getDeclaredMethod("getHandle");
final Field listenerField = FallbackBukkitInjector.findField(false, handleMethod.getReturnType(),
"PlayerConnection", "ServerGamePacketListenerImpl");
final Field managerField = FallbackBukkitInjector.findField(true, listenerField.getType(),
"NetworkManager", "Connection");
final Field channelField = FallbackBukkitInjector.getFieldAt(managerField.getType(), Channel.class, 0);
BukkitJoinListener.handleMethod = MethodHandles.lookup().unreflect(handleMethod);
packetListenerField = MethodHandles.lookup().unreflectGetter(listenerField);
BukkitJoinListener.managerField = MethodHandles.lookup().unreflectGetter(managerField);
BukkitJoinListener.channelField = MethodHandles.lookup().unreflectGetter(channelField);
initialized = true;
} catch (Exception exception) {
initialized = false;
}
}
@EventHandler(priority = EventPriority.HIGHEST)
public void handle(final @NotNull PlayerJoinEvent event) {
if (!initialized || event.getPlayer().hasMetadata("NPC")) {
return;
}
try {
final Object handle = handleMethod.invoke(event.getPlayer());
final Object listener = packetListenerField.invoke(handle);
final Object networkManager = managerField.invoke(listener);
final Channel channel = (Channel) channelField.invoke(networkManager);
// Close the channel if Sonar's handlers are not found when the PlayerJoinEvent is called
if (channel != null && channel.isActive() && channel.pipeline().context(FALLBACK_BANDWIDTH) == null) {
channel.close();
}
} catch (Throwable throwable) {
Sonar.get().getLogger().warn("Failed to the channel for {}: {}", event.getPlayer().getName(), throwable);
}
}
}
+3 -6
View File
@@ -4,8 +4,6 @@ plugins {
bungee {
name = rootProject.name
description = rootProject.description
version = rootProject.version.toString()
main = "xyz.jonesdev.sonar.bungee.SonarBungeePlugin"
author = "Jones Development, Sonar Contributors"
softDepends = setOf("Geyser-BungeeCord", "floodgate", "Protocolize", "ViaVersion", "packetevents", "FastLogin")
@@ -18,10 +16,9 @@ dependencies {
compileOnly(rootProject.libs.bungeecord)
testCompileOnly(rootProject.libs.bungeecord)
implementation(rootProject.libs.adventure.platform.bungee)
implementation(rootProject.libs.adventure.platform.api)
implementation(rootProject.libs.adventure.platform.facet)
implementation(rootProject.libs.adventure.nbt)
implementation(rootProject.libs.adventure.platform.bungee) {
exclude(module = "adventure-nbt")
}
implementation(rootProject.libs.bstats.bungee)
implementation(rootProject.libs.libby.bungee)
}
@@ -21,6 +21,7 @@ import com.alessiodp.libby.BungeeLibraryManager;
import lombok.Getter;
import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.platform.bungeecord.BungeeAudiences;
import net.md_5.bungee.api.CommandSender;
import org.bstats.bungeecord.Metrics;
import org.bstats.charts.SimplePie;
import org.jetbrains.annotations.NotNull;
@@ -35,11 +36,8 @@ import java.util.UUID;
@Getter
public final class SonarBungee extends SonarBootstrap<SonarBungeePlugin> {
public static SonarBungee INSTANCE;
public SonarBungee(final @NotNull SonarBungeePlugin plugin) {
super(plugin, SonarPlatform.BUNGEE, plugin.getDataFolder(), new BungeeLibraryManager(plugin));
INSTANCE = this;
}
/**
@@ -55,6 +53,11 @@ public final class SonarBungee extends SonarBootstrap<SonarBungeePlugin> {
return bungeeAudiences.player(uniqueId);
}
@Override
public @NotNull Audience sender(final @NotNull Object object) {
return bungeeAudiences.sender((CommandSender) object);
}
/**
* Create a wrapper for the plugin logger, so we can use it outside
* the velocity module.
@@ -91,6 +94,10 @@ public final class SonarBungee extends SonarBootstrap<SonarBungeePlugin> {
() -> getConfig().getVerification().getTiming().getDisplayName()));
metrics.addCustomChart(new SimplePie("captcha",
() -> getConfig().getVerification().getMap().getTiming().getDisplayName()));
metrics.addCustomChart(new SimplePie("language",
() -> getConfig().getLanguage().getName()));
metrics.addCustomChart(new SimplePie("database_type",
() -> getConfig().getDatabase().getType().getDisplayName()));
// Register Sonar command
getPlugin().getServer().getPluginManager().registerCommand(getPlugin(), new BungeeSonarCommand());
@@ -22,9 +22,9 @@ import net.md_5.bungee.api.connection.ProxiedPlayer;
import net.md_5.bungee.api.plugin.Command;
import net.md_5.bungee.api.plugin.TabExecutor;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.command.InvocationSource;
import xyz.jonesdev.sonar.api.command.SonarCommand;
import xyz.jonesdev.sonar.bungee.SonarBungee;
import static java.util.Collections.emptyList;
@@ -38,7 +38,7 @@ public final class BungeeSonarCommand extends Command implements TabExecutor, So
// Create our own invocation source wrapper to handle messages properly
final InvocationSource invocationSource = new InvocationSource(
sender instanceof ProxiedPlayer ? ((ProxiedPlayer) sender).getUniqueId() : null,
SonarBungee.INSTANCE.getBungeeAudiences().sender(sender),
Sonar.get().sender(sender),
sender::hasPermission);
// Pass the invocation source and command arguments to our command handler
handle(invocationSource, args);
@@ -27,19 +27,13 @@ import net.md_5.bungee.protocol.packet.Handshake;
import net.md_5.bungee.protocol.packet.LoginRequest;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.ReflectiveOperationException;
import xyz.jonesdev.sonar.common.fallback.FallbackChannelHandlerAdapter;
import xyz.jonesdev.sonar.common.fallback.FallbackInboundHandlerAdapter;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.net.InetSocketAddress;
import static net.md_5.bungee.netty.PipelineUtils.*;
public final class FallbackBungeeChannelHandler extends FallbackChannelHandlerAdapter {
public FallbackBungeeChannelHandler(final @NotNull Channel channel) {
super(channel);
}
final class FallbackBungeeInboundHandler extends FallbackInboundHandlerAdapter {
private static final MethodHandle CHANNEL_WRAPPER_GETTER;
@@ -53,18 +47,22 @@ public final class FallbackBungeeChannelHandler extends FallbackChannelHandlerAd
}
@Override
public void channelRead(final @NotNull ChannelHandlerContext ctx, final Object msg) throws Exception {
// TODO: put this into a separate handler
public void channelRead(final @NotNull ChannelHandlerContext ctx, final @NotNull Object msg) throws Exception {
// Intercept any packets processed by BungeeCord
if (listenForPackets && msg instanceof PacketWrapper) {
if (msg instanceof PacketWrapper) {
final PacketWrapper packetWrapper = (PacketWrapper) msg;
final DefinedPacket wrappedPacket = packetWrapper.packet;
// Don't handle any invalid packets
if (wrappedPacket != null) {
final Channel channel = ctx.channel();
// Intercept any handshake packet by the client
if (wrappedPacket instanceof Handshake) {
final Handshake handshake = (Handshake) wrappedPacket;
handleHandshake(handshake.getHost(), handshake.getProtocolVersion());
handleHandshake(channel, handshake.getHost(), handshake.getProtocolVersion());
// We don't care about server pings; remove the handler
if (handshake.getRequestedProtocol() == 1) {
ctx.channel().pipeline().remove(this);
}
}
// Intercept any server login packet by the client
else if (wrappedPacket instanceof LoginRequest) {
@@ -78,9 +76,12 @@ public final class FallbackBungeeChannelHandler extends FallbackChannelHandlerAd
throw new ReflectiveOperationException(throwable);
}
final InetSocketAddress socketAddress = (InetSocketAddress) channelWrapper.getRemoteAddress();
handleLogin(ctx, msg, loginRequest.getData(), socketAddress,
PACKET_ENCODER, PACKET_DECODER, TIMEOUT_HANDLER, BOSS_HANDLER);
// We've done our job - deject this pipeline
ctx.channel().pipeline().remove(this);
// Make sure to mark this packet as released
packetWrapper.trySingleRelease();
// Let Sonar process the login packet
handleLogin(channel, ctx, () -> ctx.fireChannelRead(msg), loginRequest.getData(), socketAddress);
return;
}
}
@@ -22,13 +22,13 @@ import io.netty.channel.ChannelInitializer;
import lombok.experimental.UtilityClass;
import net.md_5.bungee.netty.PipelineUtils;
import sun.misc.Unsafe;
import xyz.jonesdev.sonar.api.ReflectiveOperationException;
import xyz.jonesdev.sonar.common.fallback.injection.FallbackInjectedChannelInitializer;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.common.fallback.netty.FallbackInjectedChannelInitializer;
import java.lang.reflect.Field;
import static net.md_5.bungee.netty.PipelineUtils.PACKET_DECODER;
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_HANDLER;
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_PACKET_HANDLER;
@UtilityClass
public class FallbackBungeeInjector {
@@ -39,9 +39,9 @@ public class FallbackBungeeInjector {
// Make sure to store the original channel initializer
final ChannelInitializer<Channel> originalInitializer = PipelineUtils.SERVER_CHILD;
final ChannelInitializer<Channel> fallbackInitializer = new FallbackInjectedChannelInitializer(
originalInitializer, pipeline -> pipeline.addAfter(PACKET_DECODER, FALLBACK_HANDLER,
new FallbackBungeeChannelHandler(pipeline.channel())));
final ChannelInitializer<Channel> injectedInitializer = new FallbackInjectedChannelInitializer(
originalInitializer, pipeline -> pipeline.addAfter(PACKET_DECODER, FALLBACK_PACKET_HANDLER,
new FallbackBungeeInboundHandler()));
final Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe");
unsafeField.setAccessible(true);
@@ -53,9 +53,9 @@ public class FallbackBungeeInjector {
final long offset = unsafe.staticFieldOffset(childField);
// Replace the original channel initializer
unsafe.putObject(base, offset, fallbackInitializer);
unsafe.putObject(base, offset, injectedInitializer);
} catch (Exception exception) {
throw new ReflectiveOperationException(exception);
Sonar.get().getLogger().error("An error occurred while injecting {}", exception);
}
}
}
@@ -56,27 +56,6 @@ class LibraryLoader {
.artifactId("ormlite-jdbc")
.version("6.1")
.relocate("com{}j256{}ormlite", "xyz{}jonesdev{}sonar{}libs{}ormlite")
.build(),
// MySQL driver
Library.builder()
.groupId("com{}mysql")
.artifactId("mysql-connector-j")
.version("8.4.0")
.relocate("com{}mysql", "xyz{}jonesdev{}sonar{}libs{}mysql")
.build(),
// MariaDB driver
Library.builder()
.groupId("org{}mariadb{}jdbc")
.artifactId("mariadb-java-client")
.version("3.4.0")
.relocate("org{}mariadb", "xyz{}jonesdev{}sonar{}libs{}mariadb")
.build(),
// H2 driver
Library.builder()
.groupId("com{}h2database")
.artifactId("h2")
.version("2.1.214")
.relocate("org{}h2", "xyz{}jonesdev{}sonar{}libs{}h2")
.build()
);
@@ -94,6 +73,12 @@ class LibraryLoader {
.artifactId("adventure-text-serializer-gson")
.version("4.17.0")
.relocate("net{}kyori", "xyz{}jonesdev{}sonar{}libs{}kyori")
.build(),
Library.builder()
.groupId("net{}kyori")
.artifactId("adventure-nbt")
.version("4.17.0")
.relocate("net{}kyori", "xyz{}jonesdev{}sonar{}libs{}kyori")
.build()
);
}
@@ -41,6 +41,7 @@ import xyz.jonesdev.sonar.common.service.SonarServiceManager;
import xyz.jonesdev.sonar.common.statistics.GlobalSonarStatistics;
import xyz.jonesdev.sonar.common.subcommands.*;
import xyz.jonesdev.sonar.common.update.UpdateChecker;
import xyz.jonesdev.sonar.common.util.ProtocolUtil;
import java.io.File;
import java.time.Duration;
@@ -55,6 +56,7 @@ public abstract class SonarBootstrap<T> implements Sonar {
private Notification notificationHandler;
private SonarConfiguration config;
private VerifiedPlayerController verifiedPlayerController;
private final LibraryManager libraryManager;
private final SonarStatistics statistics;
private final SonarPlatform platform;
private final SubcommandRegistry subcommandRegistry;
@@ -66,6 +68,9 @@ public abstract class SonarBootstrap<T> implements Sonar {
final @NotNull LibraryManager libraryManager) {
// Load all libraries before anything else
LibraryLoader.loadLibraries(libraryManager, platform);
this.libraryManager = libraryManager;
// Check if Netty is up-to-date
ProtocolUtil.checkNettyVersion();
// Set the Sonar API
SonarSupplier.set(this);
// Set the plugin instance
@@ -180,7 +185,7 @@ public abstract class SonarBootstrap<T> implements Sonar {
// Close the old connection first
verifiedPlayerController.close();
}
verifiedPlayerController = new VerifiedPlayerController();
verifiedPlayerController = new VerifiedPlayerController(libraryManager);
}
public final void shutdown() {
@@ -1,247 +0,0 @@
/*
* Copyright (C) 2023-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;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.handler.codec.CorruptedFrameException;
import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.fallback.Fallback;
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.statistics.GlobalSonarStatistics;
import xyz.jonesdev.sonar.common.util.GeyserDetection;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.util.UUID;
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_BANDWIDTH;
import static xyz.jonesdev.sonar.common.fallback.FallbackUserWrapper.customDisconnect;
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer.*;
@RequiredArgsConstructor
public class FallbackChannelHandlerAdapter extends ChannelInboundHandlerAdapter {
protected final Channel channel;
protected @Nullable String username;
protected InetAddress inetAddress;
protected ProtocolVersion protocolVersion;
protected boolean listenForPackets = true;
protected static final Fallback FALLBACK = Sonar.get().getFallback();
@Override
public final 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
ctx.fireChannelActive();
}
@Override
public final void channelInactive(final @NotNull ChannelHandlerContext ctx) throws Exception {
// 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
FALLBACK.getConnected().compute(inetAddress, (key, value) -> null);
// Remove the IP address from the queue
FALLBACK.getQueue().getPlayers().compute(inetAddress, (key, value) -> null);
// Remove this account from the online players or decrement the number of accounts with the same IP
FALLBACK.getOnline().compute(inetAddress, (key, value) -> value == null || value <= 1 ? null : value - 1);
}
// 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.
// Additionally, this will also run after our custom decoder.
@Override
public final void exceptionCaught(final @NotNull ChannelHandlerContext ctx,
final @NotNull Throwable cause) throws Exception {
// Close the channel if we encounter any errors.
ctx.close();
}
/**
* Validates and handles incoming handshake packets
*
* @param hostname Hostname (server address) sent by the client
* @param protocol Protocol version number sent by the client
*/
protected final void handleHandshake(final @NotNull String hostname, final int protocol) throws Exception {
// Check if the hostname is invalid
if (hostname.isEmpty()) {
throw new CorruptedFrameException("Hostname is empty");
}
// Check if the player has already sent a handshake packet
if (protocolVersion != null) {
throw new CorruptedFrameException("Already sent handshake");
}
// Store the protocol version
protocolVersion = ProtocolVersion.fromId(protocol);
// Hook the traffic listener
channel.pipeline().addFirst(FALLBACK_BANDWIDTH, FallbackBandwidthHandler.INSTANCE);
}
/**
* Validates and handles incoming login packets
*
* @param ctx Forwarded channel handler context
* @param loginPacket Login packet sent by the client
* @param username Username sent by the client
* @param socketAddress Socket address of the client
*/
protected final void handleLogin(final @NotNull ChannelHandlerContext ctx,
final @NotNull Object loginPacket,
final @NotNull String username,
final @NotNull InetSocketAddress socketAddress,
final @NotNull String encoder,
final @NotNull String decoder,
final @NotNull String timeout,
final @NotNull String handler) throws Exception {
// Check if the player has already sent a login packet
if (this.username != null) {
throw new CorruptedFrameException("Already sent login");
}
// Connections from unknown protocol versions will be discarded
// as this is the safest way of handling unwanted connections
if (protocolVersion.isUnknown()) {
// Sonar does not support snapshots or Minecraft versions older than 1.7.2
throw new CorruptedFrameException("Unknown protocol version");
}
// Increase joins per second for the action bar verbose
GlobalSonarStatistics.countLogin();
// Store the username and IP address
this.username = username;
this.inetAddress = socketAddress.getAddress();
// Check the blacklist here since we cannot let the player "ghost join"
if (FALLBACK.getBlacklist().asMap().containsKey(inetAddress)) {
customDisconnect(channel, protocolVersion, blacklisted, encoder, handler);
return;
}
// Check if the player is already queued since we don't want bots to flood the queue
if (FALLBACK.getQueue().getPlayers().containsKey(inetAddress)) {
customDisconnect(channel, protocolVersion, alreadyQueued, encoder, handler);
return;
}
// Check if Fallback is already verifying a player with the same IP address
if (FALLBACK.getConnected().containsKey(inetAddress)) {
customDisconnect(channel, protocolVersion, alreadyVerifying, encoder, handler);
return;
}
// Don't continue the verification process if the verification is disabled
if (!Sonar.get().getFallback().shouldVerifyNewPlayers()) {
initialLogin(ctx, loginPacket, encoder, handler);
return;
}
// Completely skip Geyser connections if configured
final boolean geyser = GeyserDetection.isGeyserConnection(channel, socketAddress);
if (geyser && !Sonar.get().getConfig().getVerification().isCheckGeyser()) {
initialLogin(ctx, loginPacket, encoder, handler);
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())) {
customDisconnect(channel, protocolVersion, protocolBlacklisted, encoder, handler);
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(inetAddress.toString(), offlineUUID)) {
initialLogin(ctx, loginPacket, encoder, handler);
return;
}
// Check if the IP address is currently being rate-limited
if (!FALLBACK.getRatelimiter().attempt(inetAddress)) {
customDisconnect(channel, protocolVersion, reconnectedTooFast, encoder, handler);
return;
}
// Check if the protocol ID of the player is allowed to bypass verification
if (Sonar.get().getConfig().getVerification().getWhitelistedProtocols()
.contains(protocolVersion.getProtocol())) {
initialLogin(ctx, loginPacket, encoder, handler);
return;
}
// Queue the connection for further processing
FALLBACK.getQueue().getPlayers().compute(inetAddress, (key, value) -> () -> {
// 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()) {
customDisconnect(channel, protocolVersion, invalidUsername, encoder, handler);
return;
}
// Create an instance for the Fallback connection
final FallbackUser user = new FallbackUserWrapper(channel, inetAddress, protocolVersion, geyser);
// Let the verification handler take over the channel
user.hijack(username, offlineUUID, encoder, decoder, timeout, handler);
});
}
/**
* Executes the maximum accounts per IP limit check before letting the player join
*
* @param ctx Forwarded channel handler context
* @param loginPacket Login packet sent by the client
*/
protected final void initialLogin(final @NotNull ChannelHandlerContext ctx,
final @NotNull Object loginPacket,
final @NotNull String encoder,
final @NotNull String handler) throws Exception {
// Increment the number of accounts with the same IP
FALLBACK.getOnline().compute(inetAddress, (key, value) -> value == null ? 1 : value + 1);
final int maxOnlinePerIp = Sonar.get().getConfig().getMaxOnlinePerIp();
// Skip the maximum online per IP check if it's disabled in the configuration
if (maxOnlinePerIp > 0) {
// Check if the number of online players using the same IP address as
// the connecting player is greater than the configured amount
final int onlinePerIp = FALLBACK.getOnline().getOrDefault(inetAddress, 0);
if (onlinePerIp >= maxOnlinePerIp) {
customDisconnect(channel, protocolVersion, tooManyOnlinePerIP, encoder, handler);
return;
}
}
// Don't listen for further packets
// TODO: deject the pipeline without breaking disconnect logic
listenForPackets = false;
// Let the server know about the login packet
ctx.fireChannelRead(loginPacket);
}
}
@@ -0,0 +1,74 @@
/*
* Copyright (C) 2023-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;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelPipeline;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.common.statistics.GlobalSonarStatistics;
import java.net.InetAddress;
import java.util.function.Consumer;
@RequiredArgsConstructor
public final class FallbackInboundHandler extends ChannelInboundHandlerAdapter {
private final Consumer<ChannelPipeline> sonarPipelineInjector;
@Setter
@Getter
private InetAddress inetAddress;
@Override
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
ctx.fireChannelActive();
// Add the packet handler pipeline
sonarPipelineInjector.accept(ctx.pipeline());
}
@Override
public void channelInactive(final @NotNull ChannelHandlerContext ctx) throws Exception {
// 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);
}
// 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.
@Override
public void exceptionCaught(final @NotNull ChannelHandlerContext ctx,
final @NotNull Throwable cause) throws Exception {
// Close the channel if we encounter any errors.
ctx.close();
}
}
@@ -0,0 +1,293 @@
/*
* Copyright (C) 2023-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;
import io.netty.channel.*;
import io.netty.handler.codec.CorruptedFrameException;
import lombok.AllArgsConstructor;
import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.fallback.Fallback;
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.netty.FallbackVarInt21FrameDecoder;
import xyz.jonesdev.sonar.common.fallback.netty.FallbackVarInt21FrameEncoder;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketEncoder;
import xyz.jonesdev.sonar.common.statistics.GlobalSonarStatistics;
import xyz.jonesdev.sonar.common.util.GeyserUtil;
import 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.*;
@AllArgsConstructor
@RequiredArgsConstructor
public abstract class FallbackInboundHandlerAdapter extends ChannelInboundHandlerAdapter {
protected @Nullable String username;
protected ProtocolVersion protocolVersion;
protected RemovalListener channelRemovalListener = RemovalListener.EMPTY;
protected static final Fallback FALLBACK = Sonar.get().getFallback();
/**
* Validates and handles incoming handshake packets
*
* @param hostname Hostname (server address) sent by the client
* @param protocol Protocol version number sent by the client
*/
protected final void handleHandshake(final @NotNull Channel channel,
final @NotNull String hostname,
final int protocol) throws Exception {
// Check if the hostname is invalid
if (hostname.isEmpty()) {
throw new CorruptedFrameException("Hostname is empty");
}
// Check if the player has already sent a handshake packet
if (protocolVersion != null) {
throw new CorruptedFrameException("Already sent handshake");
}
// Store the protocol version
protocolVersion = ProtocolVersion.fromId(protocol);
// Hook the traffic listener
channel.pipeline().addFirst(FALLBACK_BANDWIDTH, FallbackBandwidthHandler.INSTANCE);
}
/**
* Validates and handles incoming login packets
*
* @param channel Forwarded client channel
* @param loginPacket Login packet sent by the client
* @param username Username sent by the client
* @param socketAddress Socket address of the client
*/
protected final void handleLogin(final @NotNull Channel channel,
final @NotNull ChannelHandlerContext ctx,
final @NotNull Runnable loginPacket,
final @NotNull String username,
final @NotNull InetSocketAddress socketAddress) throws Exception {
// Check if the player has already sent a login packet
if (this.username != null) {
throw new CorruptedFrameException("Already sent login");
}
// Connections from unknown protocol versions will be discarded
// as this is the safest way of handling unwanted connections
if (protocolVersion.isUnknown()) {
// Sonar does not support snapshots or Minecraft versions older than 1.7.2
throw new CorruptedFrameException("Unknown protocol version");
}
// 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());
// Check the blacklist here since we cannot let the player "ghost join"
if (FALLBACK.getBlacklist().asMap().containsKey(inboundHandler.getInetAddress())) {
customDisconnect(channel, protocolVersion, blacklisted);
return;
}
// Check if Fallback is already verifying a player with the same IP address
if (FALLBACK.getConnected().containsKey(inboundHandler.getInetAddress())) {
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())) {
customDisconnect(channel, protocolVersion, protocolBlacklisted);
return;
}
// Don't continue the verification process if the verification is disabled
if (!Sonar.get().getFallback().shouldVerifyNewPlayers()) {
initialLogin(ctx, inboundHandler.getInetAddress(), 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);
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(inboundHandler.getInetAddress().toString(), offlineUuid)) {
initialLogin(ctx, inboundHandler.getInetAddress(), loginPacket);
return;
}
// Check if the IP address is currently being rate-limited
if (!FALLBACK.getRatelimiter().attempt(inboundHandler.getInetAddress())) {
customDisconnect(channel, protocolVersion, reconnectedTooFast);
return;
}
// Remove all other pipelines that could still mess up something
rewriteProtocol(ctx, channelRemovalListener);
// Queue the connection for further processing
FALLBACK.getQueue().getPlayers().compute(inboundHandler.getInetAddress(), (inetAddress, runnable) -> {
// Check if the player is already queued since we don't want bots to flood the queue
if (runnable != null) {
customDisconnect(channel, protocolVersion, alreadyQueued);
// Remove other instances of this IP address from the queue
return null;
}
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()) {
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);
};
});
}
/**
* Removes all pipelines and rewrites them using our custom handlers
*/
private static void rewriteProtocol(final @NotNull ChannelHandlerContext ctx,
final @NotNull RemovalListener removalListener) {
for (final Map.Entry<String, ChannelHandler> entry : ctx.pipeline()) {
// Don't accidentally remove Sonar's handlers
if (entry.getKey().startsWith("sonar")
// Don't remove floodgate's pipelines
|| entry.getKey().startsWith("geyser")
|| entry.getKey().startsWith("floodgate")
|| entry.getKey().equals("DefaultChannelPipeline$TailContext#0")) {
continue;
}
ctx.pipeline().remove(entry.getValue());
removalListener.accept(ctx.pipeline(), entry.getKey(), entry.getValue());
}
// Add our custom pipelines
ctx.pipeline()
.addFirst(FALLBACK_FRAME_ENCODER, FallbackVarInt21FrameEncoder.INSTANCE)
.addFirst(FALLBACK_TIMEOUT, new FallbackTimeoutHandler(
Sonar.get().getConfig().getVerification().getReadTimeout(),
Sonar.get().getConfig().getVerification().getWriteTimeout(),
TimeUnit.MILLISECONDS))
.addFirst(FALLBACK_FRAME_DECODER, new FallbackVarInt21FrameDecoder());
}
/**
* Executes the maximum accounts per IP limit check before letting the player join
*
* @param ctx Forwarded client channel context
* @param inetAddress Resolved client IP address
* @param loginPacket Login packet sent by the client
*/
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
FALLBACK.getOnline().compute(inetAddress, (k, v) -> v == null ? 1 : v + 1);
final int maxOnlinePerIp = Sonar.get().getConfig().getMaxOnlinePerIp();
// Skip the maximum online per IP check if it's disabled in the configuration
if (maxOnlinePerIp > 0) {
// Check if the number of online players using the same IP address as
// the connecting player is greater than the configured amount
final int onlinePerIp = FALLBACK.getOnline().getOrDefault(inetAddress, 0);
if (onlinePerIp >= maxOnlinePerIp) {
customDisconnect(ctx.channel(), protocolVersion, tooManyOnlinePerIP);
return;
}
}
// Let the server know about the login packet
loginPacket.run();
}
/**
* Disconnect the player before verification (during login)
* by replacing the encoder before running the method.
*
* @param packet Disconnect packet
* @param channel Channel of the player
* @param protocolVersion Protocol version of the player
*/
private void customDisconnect(final @NotNull Channel channel,
final @NotNull ProtocolVersion protocolVersion,
final @NotNull FallbackPacket packet) {
if (channel.eventLoop().inEventLoop()) {
_customDisconnect(channel, protocolVersion, packet);
} else {
channel.eventLoop().execute(() -> _customDisconnect(channel, protocolVersion, packet));
}
}
private void _customDisconnect(final @NotNull Channel channel,
final @NotNull ProtocolVersion protocolVersion,
final @NotNull FallbackPacket packet) {
// Remove the main pipeline to completely take over the channel
final String handler = Sonar.get().getPlatform().getHandlerFunction().apply(channel.pipeline());
if (channel.pipeline().context(handler) != null) {
channel.pipeline().remove(handler);
}
final String encoder = Sonar.get().getPlatform().getEncoderFunction().apply(channel.pipeline());
final ChannelHandler currentEncoder = channel.pipeline().get(encoder);
// Close the channel if no decoder exists
if (currentEncoder != null) {
// We don't need to update the encoder if it's already present
if (!(currentEncoder instanceof FallbackPacketEncoder)) {
final FallbackPacketEncoder newEncoder = new FallbackPacketEncoder(protocolVersion);
channel.pipeline().replace(encoder, FALLBACK_PACKET_ENCODER, newEncoder);
}
closeWith(channel, protocolVersion, packet);
} else {
channel.close();
}
}
@FunctionalInterface
public interface RemovalListener {
void accept(final @NotNull ChannelPipeline pipeline,
final @NotNull String name,
final @NotNull ChannelHandler handler);
RemovalListener EMPTY = (pipeline, name, handler) -> {};
}
}
@@ -19,7 +19,6 @@ package xyz.jonesdev.sonar.common.fallback;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelPipeline;
import io.netty.handler.codec.CorruptedFrameException;
import lombok.Getter;
@@ -34,7 +33,7 @@ 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.protocol.FallbackPacket;
import xyz.jonesdev.sonar.common.fallback.netty.FallbackTailExceptionsHandler;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketDecoder;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketEncoder;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer;
@@ -46,9 +45,8 @@ import java.net.InetAddress;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_PACKET_DECODER;
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_PACKET_ENCODER;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_20_2;
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.*;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry.CONFIG;
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry.GAME;
@@ -59,6 +57,7 @@ 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
@@ -69,11 +68,13 @@ public final class FallbackUserWrapper implements FallbackUser {
public FallbackUserWrapper(final @NotNull Channel channel,
final @NotNull InetAddress inetAddress,
final @NotNull ProtocolVersion protocolVersion,
final @NotNull UUID offlineUuid,
final boolean geyser) {
this.channel = channel;
this.pipeline = channel.pipeline();
this.inetAddress = inetAddress;
this.protocolVersion = protocolVersion;
this.offlineUuid = offlineUuid;
this.geyser = geyser;
}
@@ -85,9 +86,7 @@ public final class FallbackUserWrapper implements FallbackUser {
}
@Override
public void hijack(final @NotNull String username, final @NotNull UUID uuid,
final @NotNull String encoder, final @NotNull String decoder,
final @NotNull String timeout, final @NotNull String handler) {
public void hijack(final @NotNull String username, final @NotNull UUID offlineUuid) {
// The player has joined the verification
GlobalSonarStatistics.totalAttemptedVerifications++;
@@ -107,37 +106,34 @@ public final class FallbackUserWrapper implements FallbackUser {
// Call the VerifyJoinEvent for external API usage
Sonar.get().getEventManager().publish(new UserVerifyJoinEvent(username, this));
// Mark the player as connected by caching them in a map of verifying players
Sonar.get().getFallback().getConnected().compute(inetAddress, (key, value) -> (byte) 0);
// Run this in the channel's event loop to avoid issues
channel.eventLoop().execute(() -> {
// Add better timeout handler to avoid known exploits or issues
// We also want to timeout bots quickly to avoid flooding
pipeline.replace(timeout, timeout, new FallbackTimeoutHandler(
Sonar.get().getConfig().getVerification().getReadTimeout(),
Sonar.get().getConfig().getVerification().getWriteTimeout(),
TimeUnit.MILLISECONDS));
// Make sure the channel is still active
if (!channel.isActive()) {
return;
}
// Mark the player as connected by caching them in a map of verifying players
Sonar.get().getFallback().getConnected().compute(inetAddress, (_k, _v) -> (byte) 0);
// Replace normal encoder to allow custom packets
final FallbackPacketEncoder newEncoder = new FallbackPacketEncoder(protocolVersion);
pipeline.replace(encoder, FALLBACK_PACKET_ENCODER, newEncoder);
// Remove the main pipeline to completely take over the channel
if (pipeline.get(handler) != null) {
pipeline.remove(handler);
}
pipeline.addLast(FALLBACK_PACKET_ENCODER, newEncoder);
// Send LoginSuccess packet to make the client think they are joining the server
write(FallbackPreparer.LOGIN_SUCCESS);
write(FallbackPreparer.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);
pipeline.replace(decoder, FALLBACK_PACKET_DECODER, fallbackPacketDecoder);
pipeline.addLast(FALLBACK_PACKET_DECODER, fallbackPacketDecoder);
// Listen for all incoming packets by setting the packet listener
fallbackPacketDecoder.setListener(new FallbackLoginSessionHandler(this, username, uuid));
fallbackPacketDecoder.setListener(new FallbackLoginSessionHandler(this, username));
// Make sure to catch all exceptions during the verification
pipeline.addLast(FALLBACK_TAIL_EXCEPTIONS, FallbackTailExceptionsHandler.INSTANCE);
});
}
@@ -212,8 +208,8 @@ public final class FallbackUserWrapper implements FallbackUser {
final @NotNull ProtocolVersion protocolVersion,
final @NotNull Object msg) {
if (channel.isActive()) {
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_8) < 0
&& protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_7_2) >= 0) {
if (protocolVersion.compareTo(MINECRAFT_1_8) < 0
&& protocolVersion.compareTo(MINECRAFT_1_7_2) >= 0) {
channel.eventLoop().execute(() -> {
channel.config().setAutoRead(false);
channel.eventLoop().schedule(() -> {
@@ -225,49 +221,4 @@ public final class FallbackUserWrapper implements FallbackUser {
}
}
}
/**
* Disconnect the player before verification (during login)
* by replacing the encoder before running the method.
*
* @param packet Disconnect packet
* @param encoder Encoder to replace
* @param handler Main pipeline to remove
* @param channel Channel of the player
* @param protocolVersion Protocol version of the player
*/
public static void customDisconnect(final @NotNull Channel channel,
final @NotNull ProtocolVersion protocolVersion,
final @NotNull FallbackPacket packet,
final @NotNull String encoder,
final @NotNull String handler) {
if (channel.eventLoop().inEventLoop()) {
_customDisconnect(channel, protocolVersion, packet, encoder, handler);
} else {
channel.eventLoop().execute(() -> _customDisconnect(channel, protocolVersion, packet, encoder, handler));
}
}
private static void _customDisconnect(final @NotNull Channel channel,
final @NotNull ProtocolVersion protocolVersion,
final @NotNull FallbackPacket packet,
final @NotNull String encoder,
final @NotNull String boss) {
// Remove the main pipeline to completely take over the channel
if (channel.pipeline().context(boss) != null) {
channel.pipeline().remove(boss);
}
final ChannelHandler currentEncoder = channel.pipeline().get(encoder);
// Close the channel if no decoder exists
if (currentEncoder != null) {
// We don't need to update the encoder if it's already present
if (!(currentEncoder instanceof FallbackPacketEncoder)) {
final FallbackPacketEncoder newEncoder = new FallbackPacketEncoder(protocolVersion);
channel.pipeline().replace(encoder, FALLBACK_PACKET_ENCODER, newEncoder);
}
closeWith(channel, protocolVersion, packet);
} else {
channel.close();
}
}
}
@@ -15,19 +15,26 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.injection;
package xyz.jonesdev.sonar.common.fallback.netty;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.ReflectiveOperationException;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.SonarPlatform;
import xyz.jonesdev.sonar.common.fallback.FallbackInboundHandler;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.util.function.Consumer;
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_INBOUND_HANDLER;
@RequiredArgsConstructor
public final class FallbackInjectedChannelInitializer extends ChannelInitializer<Channel> {
private static final MethodHandle INIT_CHANNEL_METHOD;
@@ -41,8 +48,8 @@ public final class FallbackInjectedChannelInitializer extends ChannelInitializer
}
}
private final ChannelInitializer<Channel> originalChannelInitializer;
private final Consumer<ChannelPipeline> sonarPipelineInjector;
private final @NotNull ChannelInitializer<Channel> originalChannelInitializer;
private final @NotNull Consumer<ChannelPipeline> sonarPipelineInjector;
@Override
protected void initChannel(final Channel channel) throws Exception {
@@ -55,7 +62,14 @@ public final class FallbackInjectedChannelInitializer extends ChannelInitializer
// Inject Sonar's channel handler into the pipeline
if (channel.isActive()) {
sonarPipelineInjector.accept(channel.pipeline());
final ChannelHandler inboundHandler = new FallbackInboundHandler(sonarPipelineInjector);
// We need to be careful on Bukkit, as the encoder can be different
if (Sonar.get().getPlatform() == SonarPlatform.BUKKIT) {
final String encoder = Sonar.get().getPlatform().getEncoderFunction().apply(channel.pipeline());
channel.pipeline().addBefore(encoder, FALLBACK_INBOUND_HANDLER, inboundHandler);
} else {
channel.pipeline().addFirst(FALLBACK_INBOUND_HANDLER, inboundHandler);
}
}
}
}
@@ -0,0 +1,38 @@
/*
* Copyright (C) 2023-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.channel.ChannelDuplexHandler;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import org.jetbrains.annotations.NotNull;
@ChannelHandler.Sharable
public final class FallbackTailExceptionsHandler extends ChannelDuplexHandler {
public static final FallbackTailExceptionsHandler INSTANCE = new FallbackTailExceptionsHandler();
// We can override the default exceptionCaught method since the server
// does not have any other pipelines that could handle this error.
// Additionally, this will also run after our custom decoder.
@Override
public void exceptionCaught(final @NotNull ChannelHandlerContext ctx,
final @NotNull Throwable cause) throws Exception {
// Close the channel if we encounter any errors.
ctx.close();
}
}
@@ -0,0 +1,57 @@
/*
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.netty;
import io.netty.util.ByteProcessor;
import lombok.Getter;
// Taken from
// https://github.com/PaperMC/Velocity/blob/dev/3.0.0/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/VarintByteDecoder.java
@Getter
final class FallbackVarInt21ByteDecoder implements ByteProcessor {
private int readVarInt, bytesRead;
private DecodeResult result = DecodeResult.TOO_SHORT;
@Override
public boolean process(byte k) {
if (k == 0 && bytesRead == 0) {
result = DecodeResult.RUN_OF_ZEROES;
return true;
}
if (result == DecodeResult.RUN_OF_ZEROES) {
return false;
}
readVarInt |= (k & 0x7F) << bytesRead++ * 7;
if (bytesRead > 3) {
result = DecodeResult.TOO_BIG;
return false;
}
if ((k & 0x80) != 128) {
result = DecodeResult.SUCCESS;
return false;
}
return true;
}
public enum DecodeResult {
SUCCESS,
TOO_SHORT,
TOO_BIG,
RUN_OF_ZEROES
}
}
@@ -0,0 +1,77 @@
/*
* Copyright (C) 2023-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.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
import io.netty.handler.codec.CorruptedFrameException;
import org.jetbrains.annotations.NotNull;
import java.util.List;
// Taken from
// https://github.com/PaperMC/Velocity/blob/dev/3.0.0/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftVarintFrameDecoder.java
public final class FallbackVarInt21FrameDecoder extends ByteToMessageDecoder {
@Override
protected void decode(final @NotNull ChannelHandlerContext ctx, final ByteBuf in, final List<Object> out) {
if (!ctx.channel().isActive()) {
in.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();
}
return;
}
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();
if (readVarInt < 0) {
in.clear();
throw new CorruptedFrameException("Invalid VarInt length " + readVarInt);
} else if (readVarInt == 0) {
// Skip over the empty packet(s) and ignore it
in.readerIndex(end + 1);
} else {
int minimumRead = bytesRead + readVarInt;
if (in.isReadable(minimumRead)) {
out.add(in.retainedSlice(end + 1, readVarInt));
in.skipBytes(minimumRead);
}
}
} else if (reader.getResult() == FallbackVarInt21ByteDecoder.DecodeResult.TOO_BIG) {
in.clear();
throw new CorruptedFrameException("Too big VarInt " + reader.getReadVarInt());
}
}
}
@@ -0,0 +1,50 @@
/*
* Copyright (C) 2023-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.buffer.ByteBuf;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;
import org.jetbrains.annotations.NotNull;
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.varIntBytes;
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.writeVarInt;
// Taken from
// https://github.com/PaperMC/Velocity/blob/dev/3.0.0/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftVarintLengthEncoder.java
@ChannelHandler.Sharable
public final class FallbackVarInt21FrameEncoder extends MessageToByteEncoder<ByteBuf> {
public static final FallbackVarInt21FrameEncoder INSTANCE = new FallbackVarInt21FrameEncoder();
@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);
}
@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);
}
}
@@ -26,7 +26,9 @@ import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.configuration.FinishConfigurationPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.configuration.RegistryDataPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.handshake.HandshakePacket;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.login.LoginAcknowledgedPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.login.LoginStartPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.login.LoginSuccessPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.*;
@@ -38,6 +40,12 @@ import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
// Mostly taken from
// https://github.com/PaperMC/Velocity/blob/dev/3.0.0/proxy/src/main/java/com/velocitypowered/proxy/protocol/StateRegistry.java
public enum FallbackPacketRegistry {
HANDSHAKE {
{
serverbound.register(HandshakePacket.class, HandshakePacket::new,
map(0x00, MINECRAFT_1_7_2, false));
}
},
LOGIN {
{
clientbound.register(DisconnectPacket.class, DisconnectPacket::new,
@@ -45,6 +53,8 @@ public enum FallbackPacketRegistry {
clientbound.register(LoginSuccessPacket.class, LoginSuccessPacket::new,
map(0x02, MINECRAFT_1_7_2, true));
serverbound.register(LoginStartPacket.class, LoginStartPacket::new,
map(0x00, MINECRAFT_1_7_2, false));
serverbound.register(LoginAcknowledgedPacket.class, LoginAcknowledgedPacket::new,
map(0x03, MINECRAFT_1_20_2, false));
}
@@ -124,7 +134,7 @@ public enum FallbackPacketRegistry {
map(0x1A, MINECRAFT_1_19_4, true),
map(0x1B, MINECRAFT_1_20_2, true),
map(0x1D, MINECRAFT_1_20_5, true));
clientbound.register(SetPlayerPositionRotation.class, SetPlayerPositionRotation::new,
clientbound.register(SetPlayerPositionRotationPacket.class, SetPlayerPositionRotationPacket::new,
map(0x08, MINECRAFT_1_7_2, true),
map(0x2E, MINECRAFT_1_9, true),
map(0x2F, MINECRAFT_1_12_1, true),
@@ -444,7 +454,7 @@ public enum FallbackPacketRegistry {
map(0x0D, MINECRAFT_1_19_4, false),
map(0x0F, MINECRAFT_1_20_2, false),
map(0x12, MINECRAFT_1_21, false));
serverbound.register(SetPlayerPositionRotation.class, SetPlayerPositionRotation::new,
serverbound.register(SetPlayerPositionRotationPacket.class, SetPlayerPositionRotationPacket::new,
map(0x06, MINECRAFT_1_7_2, false),
map(0x0D, MINECRAFT_1_9, false),
map(0x0F, MINECRAFT_1_12, false),
@@ -26,20 +26,21 @@ import xyz.jonesdev.sonar.common.fallback.protocol.block.BlockType;
import xyz.jonesdev.sonar.common.fallback.protocol.block.BlockUpdate;
import xyz.jonesdev.sonar.common.fallback.protocol.captcha.CaptchaPreparer;
import xyz.jonesdev.sonar.common.fallback.protocol.dimension.DimensionRegistry;
import xyz.jonesdev.sonar.common.fallback.protocol.entity.EntityType;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.configuration.FinishConfigurationPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.configuration.RegistryDataPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.login.LoginSuccessPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.*;
import xyz.jonesdev.sonar.common.util.ComponentHolder;
import java.security.SecureRandom;
import java.util.UUID;
import java.util.concurrent.ThreadLocalRandom;
@UtilityClass
public class FallbackPreparer {
private final SecureRandom RANDOM = new SecureRandom();
// LoginSuccess
public final FallbackPacket LOGIN_SUCCESS = new FallbackPacketSnapshot(new LoginSuccessPacket(new UUID(1L, 1L), "Sonar"));
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));
@@ -60,7 +61,7 @@ public class FallbackPreparer {
public FallbackPacket enterCodeMessage;
public FallbackPacket incorrectCaptcha;
// JoinGame
public final int PLAYER_ENTITY_ID = ThreadLocalRandom.current().nextInt(10);
public final int PLAYER_ENTITY_ID = RANDOM.nextInt(10);
public FallbackPacket joinGame;
// Update Section Blocks
public FallbackPacket updateSectionBlocks;
@@ -70,7 +71,7 @@ public class FallbackPreparer {
// Default Spawn Position
public FallbackPacket defaultSpawnPosition;
// Spawn Position
public final int TELEPORT_ID = ThreadLocalRandom.current().nextInt();
public final int TELEPORT_ID = RANDOM.nextInt();
public FallbackPacket spawnPosition;
// Transfer packet
public static FallbackPacket transferToOrigin;
@@ -86,21 +87,23 @@ public class FallbackPreparer {
// Vehicle
public FallbackPacket removeEntities;
public FallbackPacket spawnEntity;
public static FallbackPacket setPassengers;
public static final int VEHICLE_ENTITY_ID = PLAYER_ENTITY_ID + 1;
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 SetPlayerPositionRotation(
SPAWN_X_POSITION, 1337, SPAWN_Z_POSITION, 0f, 90f, 0, false));
// 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 = BlockType.BARRIER;
public BlockType blockType;
public int maxMovementTick, dynamicSpawnYPosition;
public double[] preparedCachedYMotions;
public double maxFallDistance;
@@ -117,9 +120,13 @@ public class FallbackPreparer {
Sonar.get().getLogger().info("Preloading all registered packets...");
FallbackPacketRegistry.values();
// Prepare LoginSuccess packet
loginSuccess = new FallbackPacketSnapshot(new LoginSuccessPacket(new UUID(1L, 1L),
Sonar.get().getConfig().getGeneralConfig().getString("verification.cached-username")));
// Prepare JoinGame packet
joinGame = new FallbackPacketSnapshot(new JoinGamePacket(PLAYER_ENTITY_ID,
Sonar.get().getConfig().getVerification().getGravity().getGamemode().getId(),
Sonar.get().getConfig().getVerification().getGamemode().getId(),
0, false, 0,
true, false, false,
new String[]{"minecraft:overworld"}, "minecraft:overworld"));
@@ -139,11 +146,13 @@ public class FallbackPreparer {
dynamicSpawnYPosition = DEFAULT_Y_COLLIDE_POSITION + (int) Math.ceil(maxFallDistance);
defaultSpawnPosition = new FallbackPacketSnapshot(new SetDefaultSpawnPositionPacket(
SPAWN_X_POSITION, dynamicSpawnYPosition, SPAWN_Z_POSITION));
spawnPosition = new FallbackPacketSnapshot(new SetPlayerPositionRotation(
spawnPosition = new FallbackPacketSnapshot(new SetPlayerPositionRotationPacket(
SPAWN_X_POSITION, dynamicSpawnYPosition, SPAWN_Z_POSITION,
0, -90, TELEPORT_ID, 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];
int index = 0;
@@ -180,6 +189,8 @@ 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));
if (Sonar.get().getConfig().getVerification().getMap().getTiming() != SonarConfiguration.Verification.Timing.NEVER
@@ -27,12 +27,111 @@ import java.util.function.Function;
@SuppressWarnings("unused")
@RequiredArgsConstructor
public enum BlockType {
STONE(protocolVersion -> 1, 1D),
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
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;
}
}, 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;
}
}, 0.1875),
BARRIER(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) {
case MINECRAFT_1_7_2:
case MINECRAFT_1_7_6:
@@ -82,7 +181,7 @@ public enum BlockType {
case MINECRAFT_1_21:
return 10366;
}
}, 1D);
}, 1);
private final Function<ProtocolVersion, Integer> idFunction;
// TODO: Implement per-version block heights?
@@ -0,0 +1,57 @@
/*
* Copyright (C) 2023-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.handshake;
import io.netty.buffer.ByteBuf;
import lombok.Getter;
import lombok.ToString;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.readString;
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.readVarInt;
@Getter
@ToString(of = {"protocolVersionId", "hostname", "port"})
public final class HandshakePacket implements FallbackPacket {
private int protocolVersionId;
private String hostname;
private int port;
private int intent;
private static final String FORGE_TOKEN = "\0FML\0";
private static final int MAXIMUM_HOSTNAME_LENGTH = 255 + FORGE_TOKEN.length() + 1;
// https://wiki.vg/Protocol#Handshaking
public static final int STATUS = 1;
public static final int LOGIN = 2;
public static final int TRANSFER = 3;
@Override
public void encode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
throw new UnsupportedOperationException();
}
@Override
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
protocolVersionId = readVarInt(byteBuf);
hostname = readString(byteBuf, MAXIMUM_HOSTNAME_LENGTH);
port = byteBuf.readUnsignedShort();
intent = readVarInt(byteBuf);
}
}
@@ -0,0 +1,76 @@
/*
* Copyright (C) 2023-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.login;
import io.netty.buffer.ByteBuf;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
import java.util.UUID;
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.*;
@Getter
@ToString
@NoArgsConstructor
@AllArgsConstructor
public final class LoginStartPacket implements FallbackPacket {
private String username;
private @Nullable UUID uuid;
@Override
public void encode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) throws Exception {
throw new UnsupportedOperationException();
}
@Override
public void decode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
username = readString(byteBuf, 16);
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_19) >= 0) {
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_19_3) < 0) {
if (byteBuf.readBoolean()) {
readPlayerKey(byteBuf);
}
}
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_20_2) >= 0) {
uuid = readUUID(byteBuf);
return;
}
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_19_1) >= 0) {
if (byteBuf.readBoolean()) {
uuid = readUUID(byteBuf);
}
}
}
}
private static void readPlayerKey(final @NotNull ByteBuf byteBuf) {
final long expiry = byteBuf.readLong();
final byte[] key = readByteArray(byteBuf);
final byte[] signature = readByteArray(byteBuf, 4096);
}
}
@@ -34,7 +34,7 @@ public final class PaddleBoatPacket implements FallbackPacket {
private boolean leftPaddle, rightPaddle;
@Override
public void encode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
public void encode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
throw new UnsupportedOperationException();
}
@@ -33,7 +33,7 @@ import static xyz.jonesdev.sonar.common.util.ProtocolUtil.writeVarInt;
@ToString
@NoArgsConstructor
@AllArgsConstructor
public final class SetPlayerPositionRotation implements FallbackPacket {
public final class SetPlayerPositionRotationPacket implements FallbackPacket {
private double x, y, z;
private float yaw, pitch;
private int teleportId;
@@ -46,15 +46,10 @@ public final class SetPlayerPositionRotation implements FallbackPacket {
byteBuf.writeDouble(z);
byteBuf.writeFloat(yaw);
byteBuf.writeFloat(pitch);
byteBuf.writeBoolean(onGround);
if (protocolVersion.compareTo(MINECRAFT_1_8) < 0) {
byteBuf.writeBoolean(onGround);
} else {
byteBuf.writeByte(0x00);
if (protocolVersion.compareTo(MINECRAFT_1_9) >= 0) {
writeVarInt(byteBuf, teleportId);
}
if (protocolVersion.compareTo(MINECRAFT_1_8) > 0) {
writeVarInt(byteBuf, teleportId);
if (protocolVersion.compareTo(MINECRAFT_1_17) >= 0
&& protocolVersion.compareTo(MINECRAFT_1_19_3) <= 0) {
@@ -25,6 +25,7 @@ import lombok.ToString;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.entity.EntityType;
import java.util.UUID;
@@ -37,23 +38,24 @@ import static xyz.jonesdev.sonar.common.util.ProtocolUtil.writeVarInt;
@NoArgsConstructor
@AllArgsConstructor
public final class SpawnEntityPacket implements FallbackPacket {
private int entityId, entityType;
private int entityId;
private EntityType entityType;
private double x, y, z;
@Override
public void encode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) throws Exception {
writeVarInt(byteBuf, entityId);
final boolean v1_9orHigher = protocolVersion.compareTo(MINECRAFT_1_8) > 0;
final boolean v1_9orHigher = protocolVersion.compareTo(MINECRAFT_1_9) >= 0;
if (v1_9orHigher) {
writeUUID(byteBuf, UUID.randomUUID());
}
if (protocolVersion.compareTo(MINECRAFT_1_14) >= 0) {
writeVarInt(byteBuf, entityType);
writeVarInt(byteBuf, entityType.getId(protocolVersion));
} else {
byteBuf.writeByte(entityType);
byteBuf.writeByte(entityType.getId(protocolVersion));
}
if (v1_9orHigher) {
@@ -61,9 +63,9 @@ public final class SpawnEntityPacket implements FallbackPacket {
byteBuf.writeDouble(y);
byteBuf.writeDouble(z);
} else {
byteBuf.writeInt(floor(x * 32D));
byteBuf.writeInt(floor(y * 32D));
byteBuf.writeInt(floor(z * 32D));
byteBuf.writeInt((int) (x * 32D));
byteBuf.writeInt((int) (y * 32D));
byteBuf.writeInt((int) (z * 32D));
}
byteBuf.writeByte(0); // pitch or yaw
@@ -83,11 +85,6 @@ public final class SpawnEntityPacket implements FallbackPacket {
}
}
private static int floor(final double value) {
final int __value = (int) value;
return value < (double) __value ? __value - 1 : __value;
}
@Override
public void decode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
throw new UnsupportedOperationException();
@@ -17,6 +17,7 @@
package xyz.jonesdev.sonar.common.fallback.session;
import io.netty.handler.codec.CorruptedFrameException;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
@@ -25,8 +26,6 @@ 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 java.util.UUID;
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer.*;
/**
@@ -44,13 +43,20 @@ import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer.*;
*/
public final class FallbackCAPTCHASessionHandler extends FallbackSessionHandler {
public FallbackCAPTCHASessionHandler(final FallbackUser user, final String username, final UUID uuid) {
super(user, username, uuid);
public FallbackCAPTCHASessionHandler(final @NotNull FallbackUser user,
final @NotNull String username) {
super(user, username);
// Disconnect the player if there is no CAPTCHA available at the moment
if (!MAP_INFO_PREPARER.isCaptchaAvailable()) {
user.disconnect(Sonar.get().getConfig().getVerification().getCurrentlyPreparing());
throw new CorruptedFrameException("No captcha available");
}
this.tries = Sonar.get().getConfig().getVerification().getMap().getMaxTries();
// 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.
// 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));
// Send random captcha to the player
@@ -61,7 +67,7 @@ public final class FallbackCAPTCHASessionHandler extends FallbackSessionHandler
user.delayedWrite(CAPTCHA_POSITION);
// Make sure the player cannot move
user.delayedWrite(user.isGeyser() ? CAPTCHA_ABILITIES_BEDROCK : CAPTCHA_ABILITIES);
// Make sure the player knows what to do
// Make sure the player knows that they have to enter the code in chat
user.delayedWrite(enterCodeMessage);
// Send all packets in one flush
user.getChannel().flush();
@@ -72,34 +78,34 @@ public final class FallbackCAPTCHASessionHandler extends FallbackSessionHandler
@Override
public void handle(final @NotNull FallbackPacket packet) {
// Check if the player took too long to enter the captcha
// Check if the player took too long to enter the CAPTCHA
final int maxDuration = Sonar.get().getConfig().getVerification().getMap().getMaxDuration();
checkState(!user.getLoginTimer().elapsed(maxDuration), "took too long to enter captcha");
checkState(!user.getLoginTimer().elapsed(maxDuration), "took too long to enter CAPTCHA");
// Handle incoming chat messages
if (packet instanceof SystemChatPacket) {
final SystemChatPacket chat = (SystemChatPacket) packet;
// Captcha is correct, finish verification
// Finish the verification if the player entered the correct code
if (chat.getMessage().equals(answer)) {
finishVerification();
return;
}
// Captcha is incorrect, remove one try
checkState(tries-- > 0, "failed captcha too often");
// Decrement the number of tries left
checkState(tries-- > 0, "failed CAPTCHA too often");
// Send the player a chat message to let them know that the code they entered is incorrect
user.write(incorrectCaptcha);
} else if (packet instanceof SetPlayerPositionPacket
|| packet instanceof SetPlayerPositionRotation) {
|| packet instanceof SetPlayerPositionRotationPacket) {
// A position packet is sent approximately every second
final long difference = maxDuration - user.getLoginTimer().delay();
final int index = (int) (difference / 1000D);
// Make sure we can actually 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]);
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
if (lastCountdownIndex != index && index >= 0 && xpCountdown.length > index) {
// Send the countdown using the experience bar
user.write(xpCountdown[index]);
}
lastCountdownIndex = index;
}
lastCountdownIndex = index;
// Send a KeepAlive packet every few seconds
if (keepAliveStreak++ > 20) {
keepAliveStreak = 0;
@@ -24,18 +24,15 @@ 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 java.util.UUID;
import static xyz.jonesdev.sonar.api.config.SonarConfiguration.Verification.Gravity.Gamemode.CREATIVE;
import static xyz.jonesdev.sonar.api.config.SonarConfiguration.Verification.Gamemode.CREATIVE;
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 FallbackGravitySessionHandler(final @NotNull FallbackUser user,
final @NotNull String username,
final @NotNull UUID uuid) {
super(user, username, uuid);
final @NotNull String username) {
super(user, username);
// 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();
@@ -46,7 +43,7 @@ public final class FallbackGravitySessionHandler extends FallbackSessionHandler
user.delayedWrite(joinGame);
// Then, write the ClientAbilities packet to the buffer
// This is only necessary if the player is in creative mode
if (Sonar.get().getConfig().getVerification().getGravity().getGamemode() == CREATIVE) {
if (Sonar.get().getConfig().getVerification().getGamemode() == CREATIVE) {
user.delayedWrite(DEFAULT_ABILITIES);
}
// Write the PositionLook packet to the buffer
@@ -102,15 +99,15 @@ public final class FallbackGravitySessionHandler extends FallbackSessionHandler
// Send the player to the protocol check
final var decoder = (FallbackPacketDecoder) user.getPipeline().get(FallbackPacketDecoder.class);
decoder.setListener(new FallbackProtocolSessionHandler(user, username, uuid, forceCAPTCHA));
decoder.setListener(new FallbackProtocolSessionHandler(user, username, forceCAPTCHA));
}
@Override
public void handle(final @NotNull FallbackPacket packet) {
if (packet instanceof SetPlayerPositionRotation) {
if (packet instanceof SetPlayerPositionRotationPacket) {
// Make sure the player has teleported before checking for position packets
if (teleported) {
final SetPlayerPositionRotation positionLook = (SetPlayerPositionRotation) packet;
final SetPlayerPositionRotationPacket positionLook = (SetPlayerPositionRotationPacket) packet;
handleMovement(positionLook.getX(), positionLook.getY(), positionLook.getZ(), positionLook.isOnGround());
}
} else if (packet instanceof SetPlayerPositionPacket) {
@@ -191,6 +188,10 @@ public final class FallbackGravitySessionHandler extends FallbackSessionHandler
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;
@@ -220,7 +221,7 @@ public final class FallbackGravitySessionHandler extends FallbackSessionHandler
markSuccess(true);
return;
}
user.fail("illegal collision tick: " + movementTick);
user.fail("illegal collision tick; tick: " + movementTick);
}
// Calculate the difference between the player's Y coordinate and the expected Y coordinate
double collisionOffsetY = (DEFAULT_Y_COLLIDE_POSITION + blockType.getBlockHeight()) - y;
@@ -230,7 +231,14 @@ public final class FallbackGravitySessionHandler extends FallbackSessionHandler
collisionOffsetY += 1.62f;
}
// Make sure the player is actually colliding with the blocks and not only spoofing ground
checkState(collisionOffsetY > -0.03, "illegal collision: " + collisionOffsetY);
if (collisionOffsetY < -0.03) {
// Do not throw an exception if the user configured to display the CAPTCHA instead
if (Sonar.get().getConfig().getVerification().getGravity().isCaptchaOnFail()) {
markSuccess(true);
return;
}
user.fail("illegal collision; offset: " + collisionOffsetY + " tick: " + movementTick + " y: " + y);
}
// The player has collided with the blocks, go on to the next stage
markSuccess(false);
}
@@ -29,7 +29,6 @@ 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.util.UUID;
import java.util.concurrent.TimeUnit;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
@@ -61,9 +60,8 @@ import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer.*;
public final class FallbackLoginSessionHandler extends FallbackSessionHandler {
public FallbackLoginSessionHandler(final @NotNull FallbackUser user,
final @NotNull String username,
final @NotNull UUID uuid) {
super(user, username, uuid);
final @NotNull String username) {
super(user, username);
// Start initializing the actual join process for pre-1.20.2 clients
if (user.getProtocolVersion().compareTo(MINECRAFT_1_20_2) < 0) {
@@ -95,7 +93,7 @@ public final class FallbackLoginSessionHandler extends FallbackSessionHandler {
// 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, uuid);
final FallbackGravitySessionHandler gravitySessionHandler = new FallbackGravitySessionHandler(user, username);
final var decoder = (FallbackPacketDecoder) user.getPipeline().get(FallbackPacketDecoder.class);
decoder.setListener(gravitySessionHandler);
}
@@ -25,15 +25,12 @@ import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketDecoder;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.SetHeldItemPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.TransactionPacket;
import java.util.UUID;
public final class FallbackProtocolSessionHandler extends FallbackSessionHandler {
public FallbackProtocolSessionHandler(final @NotNull FallbackUser user,
final @NotNull String username,
final @NotNull UUID uuid,
final boolean forceCAPTCHA) {
super(user, username, uuid);
super(user, username);
this.forceCAPTCHA = forceCAPTCHA;
@@ -80,10 +77,10 @@ public final class FallbackProtocolSessionHandler extends FallbackSessionHandler
// send the player to the CAPTCHA, or finish the verification.
final var decoder = (FallbackPacketDecoder) user.getPipeline().get(FallbackPacketDecoder.class);
// Pass the player to the next best verification handler
if (Sonar.get().getFallback().shouldPerformVehicleCheck()) {
decoder.setListener(new FallbackVehicleSessionHandler(user, username, uuid, forceCAPTCHA));
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, uuid));
decoder.setListener(new FallbackCAPTCHASessionHandler(user, username));
} else {
// The player has passed all checks
finishVerification();
@@ -30,7 +30,6 @@ import xyz.jonesdev.sonar.common.statistics.GlobalSonarStatistics;
import java.nio.charset.StandardCharsets;
import java.util.Random;
import java.util.UUID;
import java.util.regex.Pattern;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
@@ -43,7 +42,6 @@ import static xyz.jonesdev.sonar.common.util.ProtocolUtil.BRAND_CHANNEL_LEGACY;
public abstract class FallbackSessionHandler implements FallbackPacketListener {
protected final FallbackUser user;
protected final String username;
protected final UUID uuid;
protected static final Random RANDOM = new Random();
@@ -61,18 +59,22 @@ public abstract class FallbackSessionHandler implements FallbackPacketListener {
// Add verified player to the database
Sonar.get().getVerifiedPlayerController().add(new VerifiedPlayer(
user.getInetAddress(), uuid, user.getLoginTimer().getStart()));
user.getInetAddress(), user.getOfflineUuid(), user.getLoginTimer().getStart()));
// Call the VerifySuccessEvent for external API usage
Sonar.get().getEventManager().publish(new UserVerifySuccessEvent(
username, uuid, user, user.getLoginTimer().delay()));
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
closeWith(user.getChannel(), user.getProtocolVersion(), transferToOrigin);
if (user.isGeyser()) {
user.write(transferToOrigin);
} else {
closeWith(user.getChannel(), user.getProtocolVersion(), transferToOrigin);
}
} else {
// Disconnect player with the verification success message
user.disconnect(Sonar.get().getConfig().getVerification().getVerificationSuccess());
@@ -22,14 +22,9 @@ 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.entity.EntityType;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.PaddleBoatPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.PlayerInputPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.SetPassengersPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.SpawnEntityPacket;
import java.util.UUID;
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.*;
@@ -40,54 +35,83 @@ import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer.*;
* {@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, all we do is listen for incoming {@link PlayerInputPacket} and {@link PaddleBoatPacket} packets.
* 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 @NotNull UUID uuid,
final boolean forceCAPTCHA) {
super(user, username, uuid);
super(user, username);
this.forceCAPTCHA = forceCAPTCHA;
// Send the necessary packets to mount the player on the vehicle
final int entityTypeId = EntityType.BOAT.getId(user.getProtocolVersion());
user.delayedWrite(new SpawnEntityPacket(VEHICLE_ENTITY_ID, entityTypeId,
SPAWN_X_POSITION, DEFAULT_Y_COLLIDE_POSITION, SPAWN_Z_POSITION));
user.delayedWrite(spawnEntity);
user.delayedWrite(setPassengers);
user.getChannel().flush();
}
private final boolean forceCAPTCHA;
private boolean receivedPaddle, receivedInput;
private int paddlePackets, inputPackets, positionPackets;
private boolean expectMovement;
private static final int MINIMUM_REQUIRED_PACKETS = 2;
private void markSuccess() {
// Pass the player to the next best verification handler
if (forceCAPTCHA || Sonar.get().getFallback().shouldPerformCaptcha()) {
// Make sure the player exits the vehicle before sending the CAPTCHA
user.delayedWrite(removeEntities);
// Either send the player to the CAPTCHA, or finish the verification.
// Either send the player to the CAPTCHA or finish the verification.
final var decoder = (FallbackPacketDecoder) user.getPipeline().get(FallbackPacketDecoder.class);
// Send the player to the CAPTCHA handler
decoder.setListener(new FallbackCAPTCHASessionHandler(user, username, uuid));
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 move(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++ > MINIMUM_REQUIRED_PACKETS) {
markSuccess();
}
}
@Override
public void handle(final @NotNull FallbackPacket packet) {
if (packet instanceof PaddleBoatPacket) {
// Mark the PaddleBoat packet as received
receivedPaddle = true;
} else if (packet instanceof PlayerInputPacket) {
if (packet instanceof SetPlayerPositionRotationPacket) {
if (expectMovement) {
final SetPlayerPositionRotationPacket posRot = (SetPlayerPositionRotationPacket) packet;
move(posRot.getY());
}
} else if (packet instanceof SetPlayerPositionPacket) {
if (expectMovement) {
final SetPlayerPositionPacket position = (SetPlayerPositionPacket) packet;
move(position.getY());
}
} 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
@@ -99,19 +123,23 @@ public final class FallbackVehicleSessionHandler extends FallbackSessionHandler
return;
}
// Mark this check as success if the player has sent
// two PlayerInput and a minimum of one PaddleBoat packets.
if (receivedInput && receivedPaddle) {
markSuccess();
// Once the player sent enough packets, go to the next stage
if (paddlePackets > MINIMUM_REQUIRED_PACKETS && inputPackets > MINIMUM_REQUIRED_PACKETS) {
// 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;
return;
}
// 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) {
receivedPaddle = true;
paddlePackets++;
}
// Mark the PlayerInput packet as received
receivedInput = true;
inputPackets++;
}
}
}
@@ -28,7 +28,7 @@ import java.net.InetSocketAddress;
* Simple utility to determine if someone joins using GeyserMC
*/
@UtilityClass
public class GeyserDetection {
public class GeyserUtil {
// https://github.com/GeyserMC/Floodgate/blob/master/core/src/main/java/org/geysermc/floodgate/module/CommonModule.java#L206
private final AttributeKey<Object> PLAYER_ATTRIBUTE = AttributeKey.valueOf("floodgate-player");
@@ -15,6 +15,27 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
/*
* Copyright 2021 Andrew Steinborn
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
* of the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all copies
* or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
* PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
* USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package xyz.jonesdev.sonar.common.util;
import io.netty.buffer.ByteBuf;
@@ -22,6 +43,7 @@ import io.netty.buffer.ByteBufOutputStream;
import io.netty.buffer.ByteBufUtil;
import io.netty.handler.codec.CorruptedFrameException;
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;
@@ -41,6 +63,25 @@ public class ProtocolUtil {
public static final String BRAND_CHANNEL_LEGACY = "MC|Brand";
public static final String BRAND_CHANNEL = "minecraft:brand";
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 int readVarInt(final ByteBuf byteBuf) {
int read = readVarIntSafely(byteBuf);
if (read == Integer.MIN_VALUE) {
@@ -97,6 +138,19 @@ 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.
@@ -160,9 +214,8 @@ public class ProtocolUtil {
}
}
public static void readUUID(final @NotNull ByteBuf byteBuf) {
byteBuf.readLong(); // least
byteBuf.readLong(); // most
public static @NotNull UUID readUUID(final @NotNull ByteBuf byteBuf) {
return new UUID(byteBuf.readLong(), byteBuf.readLong());
}
public static byte @NotNull [] readByteArray(final ByteBuf byteBuf) {
@@ -81,26 +81,24 @@ verification:
gravity:
# Měl by Sonar zkoušet pro gravitaci hráče (Doporučováno)
enabled: true
# Měl by Sonar zkoušet jestli se hráč dotýka bloku? (Doporučováno)
check-collisions: true
# Měl by Sonar přimět hráče, aby vyřešil CAPTCHA, pokud neuspějí při kontrole gravitace?
captcha-on-fail: false
# Maximální počet ticků, který má hráč padat aby byl puštěn na verifikační platformu
max-movement-ticks: 8
# Gamemode hráče při verifikaci
# Typy: SURVIVAL, CREATIVE, ADVENTURE
# - SURVIVAL: všechny komponenty UI jsou zobrazeny
# - CREATIVE: životy a hunger neni zobrazen
# - ADVENTURE: všechny komponenty UI jsou zobrazeny
gamemode: ADVENTURE
# Kontroluje, zda hráč naráží do bloků, které se vytvořily pod ním
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:
# Typy: ALWAYS, DURING_ATTACK, NEVER
# - ALWAYS: Noví hráči budou vždy namontováni na vozidle (Doporučeno)
# - DURING_ATTACK: Noví hráči budou namontováni na vozidlo pouze během útoku
# - NEVER: Noví hráči nebudou nikdy namontováni na vozidlo
timing: ALWAYS
# Měl by sonar kontrolovat, zda hráč správně vstoupil na loď?
enabled: true
# Nechá hráče verifikovat pomocí kódu na mapě ve hře
map-captcha:
@@ -178,6 +176,7 @@ verification:
# Je-li povoleno, hráč bude přenesen zpět na původní server
# po úspěšném absolvování ověření bota.
# Tuto funkci představil Mojang ve verzi Minecraft 1.20.5
# Tutoriál: https://youtu.be/aSWrzQVjGnc?t=71
transfer:
# Měl by Sonar přenést hráče na původní server (místo toho, aby je vyhodil)?
# Aby to fungovalo, musíte tuto funkci povolit v konfiguraci vašeho serveru
@@ -193,6 +192,16 @@ verification:
# Pokud váš server nepotřebuje port pro připojení, můžete toto ponechat jako 25565.
destination-port: 25565
# Gamemode hráče při verifikaci
# Typy: SURVIVAL, CREATIVE, ADVENTURE
# - SURVIVAL: všechny komponenty UI jsou zobrazeny
# - CREATIVE: životy a hunger neni zobrazen
# - ADVENTURE: všechny komponenty UI jsou zobrazeny
gamemode: ADVENTURE
# Sonar ukládá do mezipaměti většinu paketů, které odesílá klientovi, včetně paketu LoginSuccess.
# Které uživatelské jméno má Sonar použít pro paket LoginSuccess?
# Uživatelské jméno nebude pro hráče viditelné bez úpravy klienta
cached-username: "Sonar"
# Měl by Sonar posílat nové verifikace?
log-connections: true
# Měl by Sonar posílat nové verifikace při útoku?
@@ -222,10 +231,8 @@ verification:
# Kolikrát musí hráč selhat verifikaci aby byl přídán do černé listiny
# (Pokuď černou listinu nechcete dejte tuto hodnotu na 0)
blacklist-threshold: 2
# List ID protokolů (verzí), které nebudou kontrolovány Sonarem (bypass verifikace)
# List ID protokolů které se nebudou moct připojit vůbec
# Celý list ID protokolů můžete najít zde
# https://wiki.vg/Protocol_version_numbers
# Například, Minecraft 1.20 má ID 763.
whitelisted-protocols: []
# List ID protokolů které se nebudou moct připojit vůbec
blacklisted-protocols: []
@@ -78,26 +78,24 @@ verification:
gravity:
# Soll Sonar auf gültige Client-Gravitation prüfen? (Empfohlen)
enabled: true
# Soll Sonar prüfen, ob der Spieler mit Blöcken kollidiert? (Empfohlen)
check-collisions: true
# Soll Sonar den Spieler dazu bringen, ein CAPTCHA zu lösen, wenn er die Schwerkraftprüfung nicht besteht?
captcha-on-fail: false
# Maximale Anzahl an Ticks, die der Spieler fallen muss, um die Plattform erreichen zu dürfen
max-movement-ticks: 8
# Der Spielmodus des Spielers während der Überprüfung
# Mögliche Typen: SURVIVAL, CREATIVE, ADVENTURE
# - SURVIVAL: alle UI-Komponenten sind sichtbar
# - CREATIVE: Gesundheit und Hunger sind versteckt
# - ADVENTURE: alle UI-Komponenten sind sichtbar
gamemode: ADVENTURE
# Prüft, ob der Spieler mit Blöcken kollidiert, die unter ihm gespawnt sind
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:
# Mögliche Typen: ALWAYS, DURING_ATTACK, NEVER
# - ALWAYS: Neue Spieler werden immer auf einem Fahrzeug montiert (Empfohlen)
# - DURING_ATTACK: Neue Spieler werden nur während eines Angriffs auf ein Fahrzeug gesetzt
# - NEVER: Neue Spieler werden niemals in ein Fahrzeug eingebaut
timing: ALWAYS
# Sollte Sonar prüfen, ob der Spieler richtig in ein Fahrzeug einsteigt?
enabled: true
# Lässt den Spieler einen Code von einer virtuellen Karte im Chat eingeben
map-captcha:
@@ -172,12 +170,13 @@ verification:
# 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
# Wenn aktiviert, wird der Player zurück zum Ursprungsserver gesendet,
# falls dieser die der Bot-Verifizierung erfolgreich besteht.
# Diese Funktion wurde von Mojang in Minecraft Version 1.20.5 eingeführt
# Anleitung: https://youtu.be/aSWrzQVjGnc?t=71
transfer:
# Sollte Sonar den Spieler auf den Ursprungsserver senden (anstatt ihn zu kicken)?
# Damit dies funktioniert, müssen Sie die Funktion in der Konfiguration Ihres Servers aktivieren
# Damit dies funktioniert, müssen Sie die Funktion in der Konfiguration Ihres Servers aktivieren.
# 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.
@@ -190,6 +189,16 @@ verification:
# Wenn Ihr Server keinen Port zum Herstellen einer Verbindung benötigt, können Sie diesen auf 25565 belassen.
destination-port: 25565
# Der Spielmodus des Spielers während der Überprüfung
# Mögliche Typen: SURVIVAL, CREATIVE, ADVENTURE
# - SURVIVAL: alle UI-Komponenten sind sichtbar
# - CREATIVE: Gesundheit und Hunger sind versteckt
# - ADVENTURE: alle UI-Komponenten sind sichtbar
gamemode: ADVENTURE
# Sonar speichert die meisten Pakete, die es an den Client sendet, einschließlich des LoginSuccess-Pakets.
# Welchen Benutzernamen soll Sonar für das LoginSuccess-Paket verwenden?
# Der Benutzername ist für den Spieler ohne Änderungen am Client nicht sichtbar
cached-username: "Sonar"
# Soll Sonar neue Überprüfungsversuche protokollieren?
log-connections: true
# Soll Sonar neue Überprüfungsversuche während Angriffen protokollieren?
@@ -219,10 +228,8 @@ verification:
# Wie oft muss ein Spieler die Überprüfung nicht bestehen, um auf die Schwarze Liste gesetzt zu werden?
# (Setzen Sie diesen Wert auf 0, um die Schwarze Liste vollständig zu deaktivieren)
blacklist-threshold: 2
# Liste von Protokoll-IDs, die von Sonar nicht überprüft werden (Überprüfungsumgehung)
# Liste von Protokoll-IDs, die den Server überhaupt nicht betreten dürfen
# Die vollständige Liste aller Protokoll-IDs finden Sie hier:
# https://wiki.vg/Protocol_version_numbers
# Zum Beispiel hat Minecraft 1.20 die ID 763.
whitelisted-protocols: []
# Liste von Protokoll-IDs, die den Server überhaupt nicht betreten dürfen
blacklisted-protocols: []
@@ -80,26 +80,24 @@ verification:
gravity:
# Should Sonar check for valid client gravity? (Recommended)
enabled: true
# Should Sonar check if the player collides with blocks? (Recommended)
check-collisions: 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
max-movement-ticks: 8
# The gamemode of the player during verification
# Possible types: SURVIVAL, CREATIVE, ADVENTURE
# - SURVIVAL: all UI components are visible
# - CREATIVE: health and hunger are hidden
# - ADVENTURE: all UI components are visible
gamemode: ADVENTURE
# Checks if the client sends proper packets when mounting a vehicle
# 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:
# Possible types: ALWAYS, DURING_ATTACK, NEVER
# - ALWAYS: New players will always be mounted on a vehicle (Recommended)
# - DURING_ATTACK: New players will only be mounted on a vehicle during an attack
# - NEVER: New players will never be mounted on a vehicle
timing: ALWAYS
# Should Sonar check if the player properly enters a boat?
enabled: true
# Make the player type a code from a virtual map in chat
map-captcha:
@@ -177,21 +175,32 @@ verification:
# If enabled, the player will be transferred back to the origin server
# after successfully passing the bot verification.
# This feature was introduced by Mojang in Minecraft version 1.20.5
# Tutorial: https://youtu.be/aSWrzQVjGnc?t=71
transfer:
# Should Sonar transfer the player to the origin server (instead of kicking them)?
# For this to work, you must enable the feature in your server's configuration
# For this to work, you must enable the feature in your server's configuration.
# 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.
enabled: false
# Which server should Sonar transfer the player to when the verification is passed?
# Please enter the server IP that is used by players to normally connect to your server.
# Please enter the server IP used by players to normally connect to your server.
# For example, you can put "mc.hypixel.net" or a direct IP like "1.1.1.1"
destination-host: "play.my-server.com"
# Which port should Sonar use when transferring the player to the origin server?
# If your server does not need a port to connect to, you can leave this as 25565.
destination-port: 25565
# The gamemode of the player during verification
# Possible types: SURVIVAL, CREATIVE, ADVENTURE
# - SURVIVAL: all UI components are visible
# - CREATIVE: health and hunger are hidden
# - ADVENTURE: all UI components are visible
gamemode: ADVENTURE
# Sonar caches most packets it sends to the client, including the LoginSuccess packet.
# Which username should Sonar use for the LoginSuccess packet?
# The username will not be visible to the player without client modifications
cached-username: "Sonar"
# Should Sonar log new verification attempts?
log-connections: true
# Should Sonar log new verification attempts during attacks?
@@ -221,10 +230,8 @@ verification:
# How often does a player have to fail the verification in order to be blacklisted?
# (Set this value to 0 to disable the blacklist entirely)
blacklist-threshold: 2
# List of protocol IDs which are not checked by Sonar (verification bypass)
# List of protocol IDs which are unable to join the server at all
# You can find the full list of all protocol IDs here:
# https://wiki.vg/Protocol_version_numbers
# For example, Minecraft 1.20 has the ID 763.
whitelisted-protocols: []
# List of protocol IDs which are unable to join the server at all
blacklisted-protocols: []
@@ -80,26 +80,24 @@ verification:
gravity:
# Sonar devrait-il vérifier la gravité du client ? (Recommandé)
enabled: true
# Sonar devrait-il vérifier si le joueur entre en collision avec des blocs ? (Recommandé)
check-collisions: true
# Sonar devrait-il faire résoudre un CAPTCHA au joueur s'il échoue à la vérification de la gravité ?
captcha-on-fail: false
# Nombre maximum de ticks que le joueur a pour tomber avant de pouvoir toucher la plateforme
max-movement-ticks: 8
# Le mode de jeu du joueur pendant la vérification
# Types possibles : SURVIVAL, CREATIVE, ADVENTURE
# - SURVIVAL : tous les composants de l'interface utilisateur sont visibles
# - CREATIVE : la santé et la faim sont cachées
# - ADVENTURE : tous les composants de l'interface utilisateur sont visibles
gamemode: ADVENTURE
# Vérifie si le joueur entre en collision avec les blocs apparus en dessous de lui
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:
# Types possibles : ALWAYS, DURING_ATTACK, NEVER
# - ALWAYS : Les nouveaux joueurs seront toujours montés dans un véhicule (Recommandé)
# - DURING_ATTACK : Les nouveaux joueurs ne seront montés dans un véhicule que pendant une attaque
# - NEVER : Les nouveaux joueurs ne seront jamais montés dans un véhicule
timing: ALWAYS
# Sonar devrait-il vérifier si le joueur entre correctement dans un bateau ?
enabled: true
# Fait taper au joueur un code à partir d'une carte virtuelle dans le chat
map-captcha:
@@ -177,6 +175,7 @@ verification:
# S'il est activé, le joueur sera renvoyé au serveur d'origine
# après avoir passé avec succès la vérification du bot.
# Cette fonctionnalité a été introduite par Mojang dans la version Minecraft 1.20.5
# Tutoriel: https://youtu.be/aSWrzQVjGnc?t=71
transfer:
# Sonar devrait-il transférer le joueur au serveur d'origine (au lieu de l'expulser) ?
# Pour que cela fonctionne, vous devez activer la fonctionnalité dans la configuration de votre serveur
@@ -192,6 +191,16 @@ verification:
# Si votre serveur n'a pas besoin d'un port pour se connecter, vous pouvez laisser celui-ci comme 25565.
destination-port: 25565
# Le mode de jeu du joueur pendant la vérification
# Types possibles : SURVIVAL, CREATIVE, ADVENTURE
# - SURVIVAL : tous les composants de l'interface utilisateur sont visibles
# - CREATIVE : la santé et la faim sont cachées
# - ADVENTURE : tous les composants de l'interface utilisateur sont visibles
gamemode: ADVENTURE
# Sonar met en cache la plupart des paquets qu'il envoie au client, y compris le paquet LoginSuccess.
# Quel nom d'utilisateur Sonar doit-il utiliser pour le paquet LoginSuccess ?
# Le nom d'utilisateur ne sera pas visible par le joueur sans modification du client
cached-username: "Sonar"
# Sonar devrait-il journaliser les nouvelles tentatives de vérification ?
log-connections: true
# Sonar devrait-il journaliser les nouvelles tentatives de vérification pendant les attaques ?
@@ -221,10 +230,8 @@ verification:
# Combien de fois un joueur doit-il échouer à la vérification pour être placé sur liste noire ?
# (Réglez cette valeur à 0 pour désactiver la liste noire entièrement)
blacklist-threshold: 2
# Liste des identifiants de protocole qui ne sont pas vérifiés par Sonar (bypass de vérification)
# Liste des identifiants de protocole qui ne peuvent pas du tout rejoindre le serveur
# Vous pouvez trouver la liste complète de tous les identifiants de protocole ici :
# https://wiki.vg/Protocol_version_numbers
# Par exemple, Minecraft 1.20 a l'ID 763.
whitelisted-protocols: []
# Liste des identifiants de protocole qui ne peuvent pas du tout rejoindre le serveur
blacklisted-protocols: []
@@ -80,26 +80,24 @@ verification:
gravity:
# უნდა შეამოწმოს თუ არა Sonar-მა მოთამაშის (ან ბოტის) გრავიტაცია? (რეკომენდებულია)
enabled: true
# უნდა შეამოწმოს თუ არა Sonar-მა, ეტაკება თუ არა მოთამაშე ბლოკებს?
check-collisions: true
# უნდა აიძულოს თუ არა Sonar-მა მოთამაშეს შეასრულოს CAPTCHA, თუ ისინი ვერ შეასრულებენ სიმძიმის შემოწმებას?
captcha-on-fail: false
# ტიკების მაქსიმალური რაოდენობა რომელითაც მოთამაშე უნდა დაეცეს, რათა პლატფორმაზე დაცემა შეეძლოს. (20 ტიკი = 1 წამი)
max-movement-ticks: 8
# მოთამაშის თამაშის რეჟიმი გადამოწმების დროს
# შესაძლო ტიპები: SURVIVAL, CREATIVE, ADVENTURE
# - SURVIVAL: ყველა UI კომპონენტი ჩანს
# - CREATIVE: სიცოცხლის და საჭმლის მაჩვენებლები იშლება
# - ADVENTURE: ყველა UI კომპონენტი ჩანს
gamemode: ADVENTURE
# Checks if the player is colliding with blocks spawned below them
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:
# შესაძლო ტიპები: ALWAYS, DURING_ATTACK, NEVER
# - ALWAYS (ყოველთვის): ახალი მოთამაშეები ყოველთვის დაჯდებიან & შემოწმდებიან სატრანსპორტო საშუალებაზე
# - DURING_ATTACK (შეტევის დროს): ახალი მოთამაშეები მხოლოდ შეტევის დროს დაჯდებიან დაჯდებიან & შემოწმდებიან სატრანსპორტო საშუალებაზე
# - NEVER (არასდროს): ახალი მოთამაშეები არასოდეს დაჯდებიან & შემოწმდებიან სატრანსპორტო საშუალებაზე (რეკომენდებულია)
timing: ALWAYS
# Should Sonar check if the player properly enters a boat?
enabled: true
# დააწერინეთ მოთამაშეს კოდი ჩატში, რომელიც იქნება ნაჩვენები ვირტუალურ მაპზე, რომელიც ხელში ეჭირებათ
map-captcha:
@@ -109,7 +107,7 @@ verification:
# - DURING_ATTACK (შეტევის დროს): ახალი მოთამაშებს მოუწევთ CAPTCHA-ს გაკეთება მხოლოდ შეტევის დროს.
# - NEVER (არასდროს): ახალ მოთამაშეებს არასდროს მოუწევთ CAPTCHA-ს გაკეთება (Recommended).
timing: NEVER
# ეფექტების პარამეტრები Captcha გამოსახულებაზე, რომელსაც ვანახებთ მოთამაშეს
# ეფექტების პარამეტრები CAPTCHA გამოსახულებაზე, რომელსაც ვანახებთ მოთამაშეს
# თქვენ უნდა შეცვალოთ ეს მხოლოდ იმ შემთხვევაში, თუ იცით, რას აკეთებთ
effects:
# მატებს შემთხვევითი ხაზების ეფექტს გამოსახულებაზე
@@ -176,6 +174,7 @@ verification:
# თუ ჩართულია, მოთამაშე გადავა საწყის სერვერში ვერიფიკაციის წარმატებით გავლის შემდეგ
# ეს ფუნქცია დაემატა Mojang-ის მიერ Minecraft-ის 1.20.5 ვერსიაში
# Tutorial: https://youtu.be/aSWrzQVjGnc?t=71
transfer:
# უნდა გადაიყვანოს თუ არა Sonar-მა მოთამაშე საწყის სერვერზე (მათი გაგდების ნაცვლად)?
# ამისთვის, თქვენ უნდა ჩართოთ ეს ფუნქცია თქვენი სერვერის კონფიგურაციაში
@@ -191,6 +190,16 @@ verification:
# თუ თქვენს სერვერს არ სჭირდება პორტი შესასვლელად, შეგიძლიათ დატოვოთ ეს 25565-ზე
destination-port: 25565
# მოთამაშის თამაშის რეჟიმი გადამოწმების დროს
# შესაძლო ტიპები: SURVIVAL, CREATIVE, ADVENTURE
# - SURVIVAL: ყველა UI კომპონენტი ჩანს
# - CREATIVE: სიცოცხლის და საჭმლის მაჩვენებლები იშლება
# - ADVENTURE: ყველა UI კომპონენტი ჩანს
gamemode: ADVENTURE
# Sonar caches most packets it sends to the client, including the LoginSuccess packet.
# Which username should Sonar use for the LoginSuccess packet?
# The username will not be visible to the player without client modifications
cached-username: "Sonar"
# ჩაიწეროს თუ არა Sonar-მა ახალი ვერიფიკაციის მცდელობები?
log-connections: true
# ჩაწეროს თუ არა Sonar-მა ახალი ვერიფიკაციის მცდელობები შეტევის დროს?
@@ -220,10 +229,8 @@ verification:
# რამდენად ხშირად უნდა გაიაროს მოთამაშემ ვერიფიკაცია წარუმატებლად, რომ ის მოხვდეს შავ სიაში?
# (დააყენეთ ეს 0 ზე, რომ მთლიანად გათიშოთ შავი სია)
blacklist-threshold: 2
# პროტოკოლის ID-ების სია, რომლებიც არ არის შემოწმებული Sonar-ის მიერ (დამოწმების შემოვლითი გზა)
# პროტოკოლის ID-ების სია, რომლებსაც საერთოდ არ შეუძლიათ სერვერზე შემოსვლა
# თქვენ შეგიძლიათ იპოვოთ ყველა პროტოკოლის ID-ების სრული სია აქ:
# https://wiki.vg/Protocol_version_numbers
# მაგალითად, Minecraft 1.20-ს აქვს ID 763.
whitelisted-protocols: []
# პროტოკოლის ID-ების სია, რომლებსაც საერთოდ არ შეუძლიათ სერვერზე შემოსვლა
blacklisted-protocols: []
@@ -80,26 +80,24 @@ verification:
gravity:
# Moet Sonar controleren op geldige zwaartekracht van de cliënt? (Aanbevolen)
enabled: true
# Moet Sonar controleren of de speler tegen blokken botst? (Aanbevolen)
check-collisions: true
# Moet Sonar de speler een CAPTCHA laten oplossen als hij niet slaagt voor de zwaartekracht controle?
captcha-on-fail: false
# Maximaal aantal ticks dat de speler moet laten vallen om het platform te mogen raken
max-movement-ticks: 8
# De spelmodus van de speler tijdens verificatie
# Mogelijke typen: SURVIVAL, CREATIVE, ADVENTURE
# - SURVIVAL: alle UI-componenten zijn zichtbaar
# - CREATIVE: gezondheid en honger zijn verborgen
# - ADVENTURE: alle UI-componenten zijn zichtbaar
gamemode: ADVENTURE
# Controleert of de speler botst met blokken die onder hem zijn gespawned
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:
# Mogelijke typen: ALWAYS, DURING_ATTACK, NEVER
# - ALWAYS: Nieuwe spelers worden altijd op een voertuig gemonteerd (Aanbevolen)
# - DURING_ATTACK: Nieuwe spelers worden alleen tijdens een aanval op een voertuig gemonteerd
# - NEVER: Nieuwe spelers zullen nooit op een voertuig worden gemonteerd
timing: ALWAYS
# Moet Sonar controleren of de speler goed in een boot stapt?
enabled: true
# Laat de speler een code van een virtuele kaart in de chat typen
map-captcha:
@@ -177,6 +175,7 @@ verification:
# Indien ingeschakeld, wordt de speler teruggestuurd naar de oorspronkelijke server
# nadat u de bot-verificatie met succes heeft doorstaan.
# Deze functie is geïntroduceerd door Mojang in Minecraft versie 1.20.5
# Handleiding: https://youtu.be/aSWrzQVjGnc?t=71
transfer:
# Moet Sonar de speler overbrengen naar de oorspronkelijke server (in plaats van hem te schoppen)?
# Om dit te laten werken, moet u de functie inschakelen in de configuratie van uw server
@@ -192,6 +191,16 @@ verification:
# Als uw server geen poort nodig heeft om verbinding mee te maken, kunt u dit op 25565 laten staan.
destination-port: 25565
# De spelmodus van de speler tijdens verificatie
# Mogelijke typen: SURVIVAL, CREATIVE, ADVENTURE
# - SURVIVAL: alle UI-componenten zijn zichtbaar
# - CREATIVE: gezondheid en honger zijn verborgen
# - ADVENTURE: alle UI-componenten zijn zichtbaar
gamemode: ADVENTURE
# Sonar cached de meeste pakketten die het naar de client stuurt, inclusief het LoginSuccess pakket.
# Welke gebruikersnaam moet Sonar gebruiken voor het LoginSucces pakket?
# De gebruikersnaam zal niet zichtbaar zijn voor de speler zonder client aanpassingen
cached-username: "Sonar"
# Moet Sonar nieuwe verificatiepogingen registreren?
log-connections: true
# Moet Sonar nieuwe verificatiepogingen registreren tijdens aanvallen?
@@ -221,10 +230,8 @@ verification:
# Hoe vaak moet een speler de verificatie niet doorstaan om op de zwarte lijst te worden geplaatst?
# (Stel deze waarde in op 0 om de zwarte lijst volledig uit te schakelen)
blacklist-threshold: 2
# Lijst met protocol-ID's die niet door Sonar worden gecontroleerd (verificatie-bypass)
# Lijst met protocol-ID's die helemaal geen verbinding kunnen maken met de server
# De volledige lijst met alle protocol-ID's vindt u hier:
# https://wiki.vg/Protocol_version_numbers
# Minecraft 1.20 heeft bijvoorbeeld de ID 763.
whitelisted-protocols: []
# Lijst met protocol-ID's die helemaal geen verbinding kunnen maken met de server
blacklisted-protocols: []
@@ -80,26 +80,24 @@ verification:
gravity:
# Czy Sonar powinien sprawdzać poprawność grawitacji gracza? (Zalecane)
enabled: true
# Czy Sonar powinien sprawdzać, czy gracz zderza się z blokami? (Zalecane)
check-collisions: true
# Czy Sonar powinien zmusić gracza do rozwiązania CAPTCHA, jeśli nie uda mu się przejść testu grawitacji?
captcha-on-fail: false
# Maksymalna liczba tików, po których gracz musi spaść, aby móc uderzyć w platformę.
max-movement-ticks: 8
# Tryb gry gracza (gamemode) podczas weryfikacji
# Dostępne rodzaje: SURVIVAL, CREATIVE, ADVENTURE
# - SURVIVAL: Wszystkie komponenty interfejsu są widoczne
# - CREATIVE: pasek życia i głodu jest ukryty
# - ADVENTURE: wszystkie komponenty interfejsu są widoczne
gamemode: ADVENTURE
# Sprawdza, czy gracz zderza się z blokami, które pojawiły się pod nim
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:
# Dostępne rodzaje: ALWAYS, DURING_ATTACK, NEVER
# - ALWAYS: Nowi gracze będą zawsze umieszczani na pojeździe (zalecane).
# - DURING_ATTACK: Nowi gracze będą umieszczani na pojeździe tylko podczas ataku.
# - NEVER: Nowi gracze nigdy nie zostaną umieszczeni na pojeździe.
timing: ALWAYS
# Czy Sonar powinien sprawdzać, czy gracz prawidłowo wsiada do łodzi?
enabled: true
# Zmuszenie gracza do wpisania kodu z wirtualnej mapy na czacie (CAPTCHA).
map-captcha:
@@ -177,6 +175,7 @@ verification:
# Jeśli opcja ta jest włączona, gracz zostanie przeniesiony z powrotem
# na serwer początkowy po pomyślnym przejściu weryfikacji antybotowej.
# Funkcja ta została wprowadzona przez Mojang w wersji 1.20.5
# Tutorial: https://youtu.be/aSWrzQVjGnc?t=71
transfer:
# Czy Sonar powinien przenieść gracza na serwer początkowy (zamiast go wyrzucać)?
# Aby to zadziałało, musisz włączyć tę funkcję w konfiguracji serwera.
@@ -192,6 +191,16 @@ verification:
# Jeśli serwer nie potrzebuje portu do połączenia, można pozostawić 25565.
destination-port: 25565
# Tryb gry gracza (gamemode) podczas weryfikacji
# Dostępne rodzaje: SURVIVAL, CREATIVE, ADVENTURE
# - SURVIVAL: Wszystkie komponenty interfejsu są widoczne
# - CREATIVE: pasek życia i głodu jest ukryty
# - ADVENTURE: wszystkie komponenty interfejsu są widoczne
gamemode: ADVENTURE
# Sonar buforuje większość pakietów, które wysyła do klienta, w tym pakiet LoginSuccess.
# Jakiej nazwy użytkownika powinien używać Sonar dla pakietu LoginSuccess?
# Nazwa użytkownika nie będzie widoczna dla gracza bez modyfikacji klienta
cached-username: "Sonar"
# Czy Sonar powinien rejestrować połączenia nowych graczy?
log-connections: true
# Czy Sonar powinien rejestrować nowe próby połączenia podczas ataku?
@@ -221,10 +230,8 @@ verification:
# Jak często gracz ma nie przechodzić weryfikacji, aby znaleźć się na czarnej liście?
# (Ustaw tę wartość na 0, aby całkowicie wyłączyć czarną listę).
blacklist-threshold: 2
# Lista protokołów (wersji gry), które nie będą sprawdzane przez Sonar
# Lista protokołów, które nie mogą w ogóle dołączyć do serwera
# Pełną listę wszystkich protokołów można znaleźć tutaj:
# https://wiki.vg/Protocol_version_numbers
# Na przykład Minecraft 1.20 ma protokół 763.
whitelisted-protocols: []
# Lista protokołów, które nie mogą w ogóle dołączyć do serwera
blacklisted-protocols: []
@@ -80,26 +80,24 @@ verification:
gravity:
# Должен ли Sonar проверять наличие действительной гравитации клиента? (Рекомендуется)
enabled: true
# Должен ли Sonar проверять, сталкивается ли игрок с блоками? (Рекомендуется)
check-collisions: true
# Должен ли Sonar заставлять игрока решать CAPTCHA, если он не проходит проверку на гравитацию?
captcha-on-fail: false
# Максимальное количество тиков, на которое должен упасть игрок, чтобы ему было разрешено упасть на платформу
max-movement-ticks: 8
# Режим игрока во время проверки
# Возможные типы: SURVIVAL, CREATIVE, ADVENTURE
# - SURVIVAL: все компоненты пользовательского интерфейса видны
# - CREATIVE: здоровье и голод скрыты
# - ADVENTURE: все компоненты пользовательского интерфейса видны
gamemode: ADVENTURE
# Проверяет, сталкивается ли игрок с блоками, порожденными под ним
collision:
# Должен ли Sonar проверять, сталкивается ли игрок с блоками? (Рекомендуется)
enabled: true
# Какой тип блока следует использовать в Sonar для платформы столкновений?
# Возможные типы: STONE, BARRIER, TRAPDOOR, ENCHANTMENT_TABLE
# Рекомендуется редактировать только если вы знаете, что делаете
collision-block-type: TRAPDOOR
# Проверяет, отправляет ли клиент правильные пакеты при садке на транспортное средство
vehicle:
# Возможные типы: ALWAYS, DURING_ATTACK, NEVER
# - ALWAYS: Новые игроки всегда будут садиться на транспортное средство (Рекомендуется)
# - DURING_ATTACK: Новые игроки будут садиться на транспортное средство только во время атаки
# - NEVER: Новые игроки никогда не будут садиться на транспортное средство
timing: ALWAYS
# Должен ли сонар проверять, правильно ли игрок входит в лодку?
enabled: true
# Заставить игрока ввести код с картинки в чат
map-captcha:
@@ -176,6 +174,7 @@ verification:
# Если включено, игрок будет возвращен на исходный сервер
# после успешного прохождения проверки на бота.
# Эта функция была введена Mojang в версии Minecraft 1.20.5
# Учебник: https://youtu.be/aSWrzQVjGnc?t=71
transfer:
# Должен ли Sonar переместить игрока на исходный сервер (вместо выкидывания его)?
# Для этого необходимо включить функцию в конфигурации вашего сервера
@@ -191,6 +190,16 @@ verification:
# Если ваш сервер не требует порта для подключения, вы можете оставить это как 25565.
destination-port: 25565
# Режим игрока во время проверки
# Возможные типы: SURVIVAL, CREATIVE, ADVENTURE
# - SURVIVAL: все компоненты пользовательского интерфейса видны
# - CREATIVE: здоровье и голод скрыты
# - ADVENTURE: все компоненты пользовательского интерфейса видны
gamemode: ADVENTURE
# Sonar кэширует большинство пакетов, которые он отправляет клиенту, включая пакет LoginSuccess.
# Какое имя пользователя должен использовать Sonar для пакета LoginSuccess?
# Имя пользователя не будет видно игроку без модификации клиента
cached-username: "Sonar"
# Должен ли Sonar регистрировать новые попытки проверки?
log-connections: true
# Должен ли Sonar регистрировать новые попытки проверки во время атак?
@@ -220,10 +229,8 @@ verification:
# Как часто игрок должен проваливать проверку, чтобы попасть в черный список?
# (Установите это значение на 0, чтобы полностью отключить черный список)
blacklist-threshold: 2
# Список протоколов, которые не проверяются Sonar (обход проверки)
# Список протоколов, которые вообще не могут присоединиться к серверу
# Полный список всех идентификаторов протоколов можно найти здесь:
# https://wiki.vg/Protocol_version_numbers
# Например, Minecraft 1.20 имеет ID 763.
whitelisted-protocols: []
# Список протоколов, которые вообще не могут присоединиться к серверу
blacklisted-protocols: []
@@ -15,7 +15,7 @@
# 未归类的设置
general:
# Sonar 是否应该自动检查新的更新?
# Sonar 是否应该自动检查新的更新?
# Sonar 使用 GitHub API 检查当前版本是否过时
check-for-updates: true
# Sonar 应该在控制台中打印玩家的 IP 地址吗?
@@ -78,26 +78,24 @@ verification:
gravity:
# Sonar 是否应该检查玩家的下落移动? (推荐启用)
enabled: true
# Sonar 是否应该生成方块平台以检查玩家下落与方块之间的碰撞? (推荐启用)
check-collisions: true
# 当玩家验证失败时 是否向他们发送验证码?
captcha-on-fail: false
# 玩家跌落在平台上多少tick后才能被算作站在平台上?
max-movement-ticks: 8
# 玩家在虚拟服务器中的游戏模式
# 可用类型: SURVIVAL, CREATIVE, ADVENTURE
# - SURVIVAL: 生存模式, 所有UI可见
# - CREATIVE: 创造模式, 隐藏血量和饱食度
# - ADVENTURE: 冒险模式, 所有UI可见
gamemode: ADVENTURE
# 检查玩家是否与下方生成的方块发生碰撞
collision:
# Sonar 是否应该生成方块平台以检查玩家下落与方块之间的碰撞? (推荐启用)
enabled: true
# Sonar 应该使用哪种类型的块作为碰撞平台?
# 可用类型: STONE, BARRIER, TRAPDOOR, ENCHANTMENT_TABLE
# 只有当您知道您在做什么时, 才应该编辑它.
collision-block-type: TRAPDOOR
# 检查客户端坐在实体上时是否发送了正确的数据包
vehicle:
# 可用类型: ALWAYS, DURING_ATTACK, NEVER
# - ALWAYS: 始终应用该检查
# - DURING_ATTACK: 仅在疑似遭受攻击时应用该检查
# - NEVER: 永远不应用该检查
timing: ALWAYS
# 声纳是否应该检查玩家是否正确进入船只?
enabled: true
# 地图验证码
map-captcha:
@@ -143,7 +141,8 @@ verification:
auto-color: false
# Sonar 应该一次准备多少个验证码?
precompute: 500
# Sonar 应该给玩家多少时间完成验证码? (该值为毫秒. 1秒等于1000毫秒.)
# Sonar 应该给玩家多少时间完成验证码?
# (该值为毫秒. 1秒等于1000毫秒.)
max-duration: 30000
# 玩家最多能输入多少次错误的验证码?
max-tries: 3
@@ -171,6 +170,7 @@ verification:
# 开启后 玩家将在验证成功后被重定向到指定的服务器
# 该功能由 Mojang 在 Minecraft 1.20.5 版本中引入
# 教程: https://youtu.be/aSWrzQVjGnc?t=71
transfer:
# Sonar 是否应该将玩家重定向到指定的服务器而不是踢出玩家?
# 为此您必须在服务器软件的配置文件中允许接受来自transfer的连接.
@@ -185,6 +185,16 @@ verification:
# 如果服务器地址不包含端口 请保留25565.
destination-port: 25565
# 玩家在虚拟服务器中的游戏模式
# 可用类型: SURVIVAL, CREATIVE, ADVENTURE
# - SURVIVAL: 生存模式, 所有UI可见
# - CREATIVE: 创造模式, 隐藏血量和饱食度
# - ADVENTURE: 冒险模式, 所有UI可见
gamemode: ADVENTURE
# Sonar 缓存它发送到客户端的大部分数据包, 包括 LoginSuccess 数据包.
# Sonar 应该使用哪个用户名来接收 LoginSuccess 数据包?
# 如果不修改客户端, 用户名将不会对玩家可见
cached-username: "Sonar"
# 当 Sonar 尝试验证玩家时 是否在控制台中打印它?
log-connections: true
# Sonar 是否应该记录在攻击期间内的验证会话?
@@ -213,9 +223,7 @@ verification:
blacklist-time: 600000
# 玩家必须验证失败多少次才会被列入黑名单? (设置为0以完全禁用)
blacklist-threshold: 2
# Sonar 不应该检查哪些协议? (即 跳过检查)
# 黑名单中的协议将阻止所有使用该协议的客户端.
# 您可以在 https://wiki.vg/Protocol_version_numbers 找到所有协议号
# 例如 Minecraft 1.20 对应的协议号为763.
whitelisted-protocols: []
# 黑名单中的协议将阻止所有使用该协议的客户端.
blacklisted-protocols: []
@@ -191,12 +191,12 @@ verification:
# CAPTCHA'yı girmeleri gerektiğinde oyuncuya gösterilen mesajlar
captcha:
# Captcha'nın cevabını girmeleri gerektiğinde oyuncuya gösterilen mesaj
# CAPTCHA'nın cevabını girmeleri gerektiğinde oyuncuya gösterilen mesaj
enter-code: '<prefix><green>Haritada gösterilen kodu lütfen sohbete girin.'
# Message that is shown to the player when they enter the wrong answer in chat
incorrect: '<prefix><red>Yanlış kodu girdiniz. Lütfen tekrar deneyin.'
# Captcha henüz hazırlanmamışken birisi katıldığında gösterilen bağlantıyı kesme mesajı
# CAPTCHA henüz hazırlanmamışken birisi katıldığında gösterilen bağlantıyı kesme mesajı
currently-preparing:
- '<header>'
- <yellow>Anti-bot verileriniz henüz hazır değil.
@@ -34,13 +34,10 @@ import java.util.UUID;
@Getter
public final class SonarVelocity extends SonarBootstrap<SonarVelocityPlugin> {
public static SonarVelocity INSTANCE;
public SonarVelocity(final @NotNull SonarVelocityPlugin plugin) {
super(plugin, SonarPlatform.VELOCITY, plugin.getDataDirectory().toFile(),
new VelocityLibraryManager<>(plugin, plugin.getLogger(),
plugin.getDataDirectory(), plugin.getServer().getPluginManager()));
INSTANCE = this;
}
/**
@@ -54,6 +51,13 @@ public final class SonarVelocity extends SonarBootstrap<SonarVelocityPlugin> {
return getPlugin().getServer().getPlayer(uniqueId).orElse(null);
}
@Override
@Deprecated
public @NotNull Audience sender(final @NotNull Object object) {
// We don't have to do this on Velocity since all command senders are also audiences.
throw new UnsupportedOperationException();
}
/**
* Create a wrapper for the plugin logger, so we can use it outside
* the velocity module.
@@ -90,6 +94,10 @@ public final class SonarVelocity extends SonarBootstrap<SonarVelocityPlugin> {
() -> getConfig().getVerification().getTiming().getDisplayName()));
metrics.addCustomChart(new SimplePie("captcha",
() -> getConfig().getVerification().getMap().getTiming().getDisplayName()));
metrics.addCustomChart(new SimplePie("language",
() -> getConfig().getLanguage().getName()));
metrics.addCustomChart(new SimplePie("database_type",
() -> getConfig().getDatabase().getType().getDisplayName()));
// Register Sonar command
getPlugin().getServer().getCommandManager().register("sonar", new VelocitySonarCommand());
@@ -20,39 +20,37 @@ package xyz.jonesdev.sonar.velocity.fallback;
import com.velocitypowered.proxy.connection.MinecraftConnection;
import com.velocitypowered.proxy.protocol.packet.HandshakePacket;
import com.velocitypowered.proxy.protocol.packet.ServerLoginPacket;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.common.fallback.FallbackChannelHandlerAdapter;
import xyz.jonesdev.sonar.common.fallback.FallbackInboundHandlerAdapter;
import java.net.InetSocketAddress;
import static com.velocitypowered.proxy.network.Connections.*;
import static com.velocitypowered.proxy.network.Connections.HANDLER;
public final class FallbackVelocityChannelHandler extends FallbackChannelHandlerAdapter {
public FallbackVelocityChannelHandler(final @NotNull Channel channel) {
super(channel);
}
final class FallbackVelocityInboundHandler extends FallbackInboundHandlerAdapter {
@Override
public void channelRead(final @NotNull ChannelHandlerContext ctx, final Object msg) throws Exception {
// TODO: put this into a separate handler
public void channelRead(final @NotNull ChannelHandlerContext ctx, final @NotNull Object msg) throws Exception {
// Intercept any handshake packet by the client
if (listenForPackets) {
if (msg instanceof HandshakePacket handshake) {
handleHandshake(handshake.getServerAddress(), handshake.getProtocolVersion().getProtocol());
}
// Intercept any server login packet by the client
else if (msg instanceof ServerLoginPacket serverLogin) {
// Make sure to use the potentially modified, original IP
final MinecraftConnection minecraftConnection = (MinecraftConnection) channel.pipeline().get(HANDLER);
final InetSocketAddress socketAddress = (InetSocketAddress) minecraftConnection.getRemoteAddress();
handleLogin(ctx, serverLogin, serverLogin.getUsername(), socketAddress,
MINECRAFT_ENCODER, MINECRAFT_DECODER, READ_TIMEOUT, HANDLER);
return;
if (msg instanceof HandshakePacket handshake) {
handleHandshake(ctx.channel(), handshake.getServerAddress(), handshake.getProtocolVersion().getProtocol());
// We don't care about server pings; remove the handler
if (handshake.getNextStatus() == 1) {
ctx.channel().pipeline().remove(this);
}
}
// Intercept any server login packet by the client
else if (msg instanceof ServerLoginPacket serverLogin) {
// Make sure to use the potentially modified, original IP
final MinecraftConnection minecraftConnection = (MinecraftConnection) ctx.channel().pipeline().get(HANDLER);
final InetSocketAddress socketAddress = (InetSocketAddress) minecraftConnection.getRemoteAddress();
// We've done our job - deject this pipeline
ctx.channel().pipeline().remove(this);
// Let Sonar process the login packet
handleLogin(ctx.channel(), ctx, () -> ctx.fireChannelRead(msg), serverLogin.getUsername(), socketAddress);
return;
}
// Make sure to let the server handle the rest
ctx.fireChannelRead(msg);
}
@@ -24,12 +24,12 @@ import io.netty.channel.Channel;
import io.netty.channel.ChannelInitializer;
import lombok.experimental.UtilityClass;
import xyz.jonesdev.sonar.api.ReflectiveOperationException;
import xyz.jonesdev.sonar.common.fallback.injection.FallbackInjectedChannelInitializer;
import xyz.jonesdev.sonar.common.fallback.netty.FallbackInjectedChannelInitializer;
import java.lang.reflect.Field;
import static com.velocitypowered.proxy.network.Connections.MINECRAFT_DECODER;
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_HANDLER;
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_PACKET_HANDLER;
@UtilityClass
public class FallbackVelocityInjector {
@@ -54,12 +54,12 @@ public class FallbackVelocityInjector {
// Make sure to store the original channel initializer
final ChannelInitializer<Channel> originalInitializer = connectionManager.serverChannelInitializer.get();
final ChannelInitializer<Channel> fallbackInitializer = new FallbackInjectedChannelInitializer(
originalInitializer, pipeline -> pipeline.addAfter(MINECRAFT_DECODER, FALLBACK_HANDLER,
new FallbackVelocityChannelHandler(pipeline.channel())));
final ChannelInitializer<Channel> injectedInitializer = new FallbackInjectedChannelInitializer(
originalInitializer, pipeline -> pipeline.addAfter(MINECRAFT_DECODER, FALLBACK_PACKET_HANDLER,
new FallbackVelocityInboundHandler()));
// Replace the original channel initializer
SERVER_CHANNEL_INITIALIZER_FIELD.set(connectionManager.getServerChannelInitializer(), fallbackInitializer);
SERVER_CHANNEL_INITIALIZER_FIELD.set(connectionManager.getServerChannelInitializer(), injectedInitializer);
} catch (Throwable throwable) {
throw new ReflectiveOperationException(throwable);
}