mirror of
https://github.com/jonesdevelopment/sonar.git
synced 2026-11-05 09:20:15 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
15af0f3823 | ||
|
|
998a248819 | ||
|
|
3846007613 | ||
|
|
c5446f9507 | ||
|
|
5a13b54350 | ||
|
|
d516da7f9e | ||
|
|
a58b8c8a83 | ||
|
|
3be87006aa | ||
|
|
c70c81eb11 | ||
|
|
d71334c1b4 | ||
|
|
59f141d1be | ||
|
|
9bc3d73cb7 | ||
|
|
d3aaba4650 | ||
|
|
68d99dd59b | ||
|
|
5f77f21ee9 | ||
|
|
c5e1d12424 | ||
|
|
25dbbf0b1f | ||
|
|
cbf04f6f2e | ||
|
|
8966d7d0e8 | ||
|
|
064a3cee2a | ||
|
|
260af8b2be | ||
|
|
a7d52467b8 | ||
|
|
0ed6b0351c | ||
|
|
a152633b41 |
No files matched your search
@@ -0,0 +1,8 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Documentation
|
||||
url: https://docs.jonesdev.xyz/
|
||||
about: Need help configuring Sonar or have questions regarding the installation and usage?
|
||||
- name: Discord
|
||||
url: https://jonesdev.xyz/discord/
|
||||
about: If you have any further questions, please feel free to join the Discord server and open a ticket.
|
||||
+1
-1
@@ -63,7 +63,7 @@ Fallback is unlikely to ever falsely prevent a player from joining the server si
|
||||
|
||||
## Building
|
||||
|
||||
If you want to build your own version of Sonar, please take a look at the [Sonar building documentation](https://docs.jonesdev.xyz/sonar/building).
|
||||
If you want to build your own version of Sonar, please take a look at the [Sonar building documentation](https://docs.jonesdev.xyz/development/building).
|
||||
<br>
|
||||
You can also take a look at the [gradle documentation](https://docs.gradle.org/current/userguide/userguide.html) for a better understanding of Gradle.
|
||||
|
||||
|
||||
+19
-26
@@ -1,17 +1,9 @@
|
||||
buildscript {
|
||||
dependencies {
|
||||
classpath("gradle.plugin.io.toolebox:gradle-git-versioner:1.6.7")
|
||||
}
|
||||
}
|
||||
|
||||
plugins {
|
||||
id("java")
|
||||
id("io.github.goooler.shadow") version "8.1.7"
|
||||
id("io.toolebox.git-versioner") version "1.6.7"
|
||||
java
|
||||
alias(libs.plugins.shadow)
|
||||
alias(libs.plugins.versioner) apply true
|
||||
}
|
||||
|
||||
apply(plugin = "io.toolebox.git-versioner")
|
||||
|
||||
versioner {
|
||||
pattern {
|
||||
pattern = "$version-%h-%c-%b"
|
||||
@@ -20,7 +12,7 @@ versioner {
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
mavenCentral() // Lombok
|
||||
mavenCentral()
|
||||
maven(url = "https://repo.jonesdev.xyz/releases/") // Bungee & Velocity proxy module
|
||||
maven(url = "https://s01.oss.sonatype.org/content/repositories/snapshots/") // libby
|
||||
}
|
||||
@@ -29,22 +21,18 @@ allprojects {
|
||||
apply(plugin = "io.github.goooler.shadow")
|
||||
|
||||
dependencies {
|
||||
compileOnly("org.projectlombok:lombok:1.18.32")
|
||||
annotationProcessor("org.projectlombok:lombok:1.18.32")
|
||||
compileOnly(rootProject.libs.lombok)
|
||||
annotationProcessor(rootProject.libs.lombok)
|
||||
|
||||
testCompileOnly("org.projectlombok:lombok:1.18.32")
|
||||
testAnnotationProcessor("org.projectlombok:lombok:1.18.32")
|
||||
testCompileOnly(rootProject.libs.lombok)
|
||||
testAnnotationProcessor(rootProject.libs.lombok)
|
||||
|
||||
// adventure
|
||||
compileOnly("net.kyori:adventure-text-minimessage:4.16.0")
|
||||
compileOnly("net.kyori:adventure-text-serializer-gson:4.16.0")
|
||||
|
||||
compileOnly("com.j256.ormlite:ormlite-jdbc:6.1") // ORMLite
|
||||
compileOnly("com.github.ben-manes.caffeine:caffeine:3.1.8") // caching
|
||||
compileOnly("io.netty:netty-all:4.1.109.Final") // netty
|
||||
|
||||
// Library/dependency loading
|
||||
compileOnly("com.alessiodp.libby:libby-core:2.0.0-SNAPSHOT")
|
||||
compileOnly(rootProject.libs.adventure.minimessage)
|
||||
compileOnly(rootProject.libs.adventure.serializer)
|
||||
compileOnly(rootProject.libs.ormlite)
|
||||
compileOnly(rootProject.libs.caffeine)
|
||||
compileOnly(rootProject.libs.netty)
|
||||
compileOnly(rootProject.libs.libby.core)
|
||||
}
|
||||
|
||||
tasks {
|
||||
@@ -68,6 +56,11 @@ allprojects {
|
||||
|
||||
// Exclude unnecessary metadata information
|
||||
exclude("META-INF/*/**")
|
||||
|
||||
// Minimize shadowed jar file
|
||||
minimize {
|
||||
exclude(project(":api"))
|
||||
}
|
||||
}
|
||||
|
||||
compileJava {
|
||||
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
group=xyz.jonesdev.sonar
|
||||
version=2.0.29
|
||||
version=2.0.32
|
||||
description=Effective Anti-bot plugin for Velocity, BungeeCord, and Bukkit (1.7-latest)
|
||||
@@ -0,0 +1,35 @@
|
||||
[versions]
|
||||
netty = "4.1.110.Final"
|
||||
adventure = "4.17.0"
|
||||
adventure-platform = "4.3.3"
|
||||
bstats = "3.0.2"
|
||||
libby = "2.0.0-SNAPSHOT"
|
||||
|
||||
[plugins]
|
||||
versioner = "io.toolebox.git-versioner:1.6.7"
|
||||
shadow = "io.github.goooler.shadow:8.1.7"
|
||||
|
||||
[libraries]
|
||||
netty = { module = "io.netty:netty-all", version.ref = "netty" }
|
||||
adventure-minimessage = { module = "net.kyori:adventure-text-minimessage", version.ref = "adventure" }
|
||||
adventure-serializer = { module = "net.kyori:adventure-text-serializer-gson", version.ref = "adventure" }
|
||||
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" }
|
||||
libby-velocity = { module = "com.alessiodp.libby:libby-velocity", version.ref = "libby" }
|
||||
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"
|
||||
ormlite = "com.j256.ormlite:ormlite-jdbc:6.1"
|
||||
lombok = "org.projectlombok:lombok:1.18.32"
|
||||
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
|
||||
spigot = "org.spigotmc:spigot-api:1.8.8-R0.1-SNAPSHOT"
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
||||
@@ -83,7 +83,8 @@ done
|
||||
# This is normally unused
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
@@ -144,7 +145,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC3045
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
@@ -152,7 +153,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC3045
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
@@ -201,11 +202,11 @@ fi
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Collect all arguments for the java command;
|
||||
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
|
||||
# shell script including quotes and variable substitutions, so put them in
|
||||
# double quotes to make sure that they get re-expanded; and
|
||||
# * put everything else in single quotes, so that it's not re-expanded.
|
||||
# Collect all arguments for the java command:
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||
# and any embedded shellness will be escaped.
|
||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||
# treated as '${Hostname}' itself on the command line.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
|
||||
Vendored
+10
-10
@@ -43,11 +43,11 @@ set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
@@ -57,11 +57,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
|
||||
@@ -33,8 +33,6 @@ import xyz.jonesdev.sonar.api.timer.SystemTimer;
|
||||
import xyz.jonesdev.sonar.api.verbose.Notification;
|
||||
import xyz.jonesdev.sonar.api.verbose.Verbose;
|
||||
|
||||
import java.io.File;
|
||||
import java.net.InetAddress;
|
||||
import java.text.DecimalFormat;
|
||||
import java.util.UUID;
|
||||
|
||||
@@ -53,18 +51,11 @@ public interface Sonar {
|
||||
*/
|
||||
@NotNull SonarPlatform getPlatform();
|
||||
|
||||
/**
|
||||
* @return If the amount of players online with the same IP address is greater than a maximum
|
||||
*/
|
||||
boolean hasTooManyAccounts(final @NotNull InetAddress inetAddress, final int limit);
|
||||
|
||||
/**
|
||||
* @return A small wrapper for the plugin logger so we can use the logger everywhere
|
||||
*/
|
||||
@NotNull LoggerWrapper getLogger();
|
||||
|
||||
@NotNull File getDataDirectory();
|
||||
|
||||
@NotNull SonarConfiguration getConfig();
|
||||
|
||||
@NotNull SubcommandRegistry getSubcommandRegistry();
|
||||
|
||||
@@ -19,6 +19,7 @@ package xyz.jonesdev.sonar.api.command;
|
||||
|
||||
import com.github.benmanes.caffeine.cache.Cache;
|
||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||
import com.github.benmanes.caffeine.cache.Ticker;
|
||||
import net.kyori.adventure.audience.Audience;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.minimessage.MiniMessage;
|
||||
@@ -40,6 +41,7 @@ public interface SonarCommand {
|
||||
|
||||
Cache<Audience, Long> COMMAND_DELAY = Caffeine.newBuilder()
|
||||
.expireAfterWrite(Duration.ofMillis(500))
|
||||
.ticker(Ticker.systemTicker())
|
||||
.build();
|
||||
|
||||
List<Component> CACHED_HELP_MESSAGE = new ArrayList<>();
|
||||
@@ -52,10 +54,8 @@ public interface SonarCommand {
|
||||
return;
|
||||
}
|
||||
|
||||
COMMAND_DELAY.cleanUp(); // Clean up the cache
|
||||
|
||||
final long timestamp = System.currentTimeMillis();
|
||||
final long mapTimestamp = COMMAND_DELAY.asMap().getOrDefault(source.getAudience(), -1L);
|
||||
final long mapTimestamp = COMMAND_DELAY.get(source.getAudience(), audience -> -1L);
|
||||
|
||||
// There were some exploits with spamming commands in the past.
|
||||
// Spamming should be prevented, especially if some heavy operations are done,
|
||||
|
||||
@@ -172,6 +172,15 @@ public final class SonarConfiguration {
|
||||
|
||||
// Verification
|
||||
verification.timing = Verification.Timing.valueOf(generalConfig.getString("verification.timing"));
|
||||
// Warn the user if the verification timing is set to NEVER
|
||||
if (verification.timing == Verification.Timing.NEVER) {
|
||||
LOGGER.warn(" ");
|
||||
LOGGER.warn("You have set the verification timing to 'NEVER'.");
|
||||
LOGGER.warn("Sonar will NOT perform the bot verification at all, therefore making it useless.");
|
||||
LOGGER.warn("It is highly suggested to set this option to either 'DURING_ATTACK' or 'ALWAYS'.");
|
||||
LOGGER.warn("Please only edit this option if you really know what you are doing.");
|
||||
LOGGER.warn(" ");
|
||||
}
|
||||
|
||||
verification.gravity.enabled = generalConfig.getBoolean("verification.checks.gravity.enabled");
|
||||
verification.gravity.checkCollisions = generalConfig.getBoolean("verification.checks.gravity.check-collisions");
|
||||
|
||||
+2
-2
@@ -238,11 +238,11 @@ public final class VerifiedPlayerController {
|
||||
*/
|
||||
private void _add(final @NotNull VerifiedPlayer player) {
|
||||
cache.computeIfAbsent(player.getInetAddress(), v -> new ArrayList<>())
|
||||
.add(player.getPlayerUUID());
|
||||
.add(player.getPlayerUuid());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the amount of verified IP addresses
|
||||
* Returns the number of verified IP addresses
|
||||
*
|
||||
* @return Estimated size of the local cache
|
||||
*/
|
||||
|
||||
@@ -28,46 +28,45 @@ import xyz.jonesdev.sonar.api.config.SonarConfiguration;
|
||||
import xyz.jonesdev.sonar.api.logger.LoggerWrapper;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
|
||||
@Getter
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public final class Fallback {
|
||||
public static final Fallback INSTANCE = new Fallback();
|
||||
private static final @NotNull Sonar SONAR = Objects.requireNonNull(Sonar.get());
|
||||
|
||||
// Map of all connected usernames and their respective IP addresses (used for fast checking)
|
||||
private final Map<String, InetAddress> connected = new ConcurrentHashMap<>(64, 0.75f);
|
||||
// Map of all players connected to the server in general
|
||||
private final ConcurrentMap<InetAddress, Integer> online = new ConcurrentHashMap<>(128);
|
||||
// Map of all connected IP addresses (used for fast checking)
|
||||
private final ConcurrentMap<InetAddress, Byte> connected = new ConcurrentHashMap<>(128);
|
||||
// Cache of all blacklisted IP addresses to ensure each entry can expire after the given time
|
||||
@Setter
|
||||
private Cache<InetAddress, Byte> blacklist;
|
||||
@Setter
|
||||
private long blacklistTime;
|
||||
|
||||
private final @NotNull FallbackQueue queue = FallbackQueue.INSTANCE;
|
||||
private final @NotNull FallbackRatelimiter ratelimiter = FallbackRatelimiter.INSTANCE;
|
||||
private final @NotNull FallbackQueue queue = new FallbackQueue();
|
||||
private final @NotNull FallbackRatelimiter ratelimiter = new FallbackRatelimiter();
|
||||
|
||||
private final LoggerWrapper logger = new LoggerWrapper() {
|
||||
|
||||
@Override
|
||||
public void info(final String message, final Object... args) {
|
||||
SONAR.getLogger().info("[fallback] " + message, args);
|
||||
Sonar.get().getLogger().info("[fallback] " + message, args);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void warn(final String message, final Object... args) {
|
||||
SONAR.getLogger().warn("[fallback] " + message, args);
|
||||
Sonar.get().getLogger().warn("[fallback] " + message, args);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void error(final String message, final Object... args) {
|
||||
SONAR.getLogger().error("[fallback] " + message, args);
|
||||
Sonar.get().getLogger().error("[fallback] " + message, args);
|
||||
}
|
||||
};
|
||||
|
||||
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
|
||||
public boolean shouldVerifyNewPlayers() {
|
||||
return shouldPerform(Sonar.get().getConfig().getVerification().getTiming());
|
||||
}
|
||||
|
||||
@@ -26,34 +26,32 @@ import java.net.InetAddress;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.ForkJoinPool;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
@Getter
|
||||
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
@RequiredArgsConstructor(access = AccessLevel.PROTECTED)
|
||||
public final class FallbackQueue {
|
||||
static final FallbackQueue INSTANCE = new FallbackQueue();
|
||||
private final Map<InetAddress, Runnable> queuedPlayers = new ConcurrentHashMap<>(64);
|
||||
// Async executor for all new verifications
|
||||
public static final ExecutorService QUEUE_EXECUTOR = new ForkJoinPool(
|
||||
Math.min(0x7fff, Runtime.getRuntime().availableProcessors()),
|
||||
ForkJoinPool.defaultForkJoinWorkerThreadFactory, null, true);
|
||||
// Fixed thread pool executor for all new verifications
|
||||
private static final int THREAD_POOL_SIZE = Math.min(0x7fff, Runtime.getRuntime().availableProcessors() * 2);
|
||||
private static final ExecutorService QUEUE_EXECUTOR = Executors.newFixedThreadPool(THREAD_POOL_SIZE);
|
||||
|
||||
private final ConcurrentMap<InetAddress, Runnable> queuedPlayers = new ConcurrentHashMap<>(
|
||||
128, 0.75f, Runtime.getRuntime().availableProcessors());
|
||||
|
||||
public void poll() {
|
||||
final int maxQueuePolls = Sonar.get().getConfig().getQueue().getMaxQueuePolls();
|
||||
int index = 0;
|
||||
|
||||
// We need to be cautious here since we don't want any concurrency issues.
|
||||
// Iterate through the map and process up to maxQueuePolls entries
|
||||
// We need to be cautious here since we don't want any concurrency issues
|
||||
final Iterator<Map.Entry<InetAddress, Runnable>> iterator = queuedPlayers.entrySet().iterator();
|
||||
while (iterator.hasNext()) {
|
||||
// Break if we reached our maximum entries
|
||||
if (++index > maxQueuePolls) {
|
||||
break;
|
||||
}
|
||||
while (iterator.hasNext() && index++ < maxQueuePolls) {
|
||||
// Run the cached runnable
|
||||
final Map.Entry<InetAddress, Runnable> entry = iterator.next();
|
||||
QUEUE_EXECUTOR.execute(entry.getValue());
|
||||
// Remove runnable from iterator
|
||||
// Remove runnable from the map
|
||||
iterator.remove();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,40 +27,25 @@ import org.jetbrains.annotations.NotNull;
|
||||
import java.net.InetAddress;
|
||||
|
||||
@Setter
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||
public final class FallbackRatelimiter {
|
||||
static final FallbackRatelimiter INSTANCE = new FallbackRatelimiter();
|
||||
|
||||
private Cache<InetAddress, Byte> attemptCache;
|
||||
private long timeout;
|
||||
private Cache<InetAddress, Long> attemptCache;
|
||||
@Getter
|
||||
private Cache<InetAddress, Integer> failCountCache;
|
||||
|
||||
/**
|
||||
* We don't want to clean up the cache in the
|
||||
* {@link #attempt(InetAddress)} method because
|
||||
* it would take up too many resources.
|
||||
*/
|
||||
public void cleanUpCache() {
|
||||
attemptCache.cleanUp();
|
||||
failCountCache.cleanUp();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the player has tried verifying too fast
|
||||
* Attempts to rate-limit the client
|
||||
*
|
||||
* @param inetAddress IP address of the player
|
||||
* @return Whether the player is allowed to verify
|
||||
*/
|
||||
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
|
||||
public boolean attempt(final @NotNull InetAddress inetAddress) {
|
||||
// Cache the IP address if it's not already cached
|
||||
if (!attemptCache.asMap().containsKey(inetAddress)) {
|
||||
// Cache the IP address
|
||||
attemptCache.put(inetAddress, (byte) 0);
|
||||
return true;
|
||||
}
|
||||
// Deny the connection attempt
|
||||
return false;
|
||||
final long expectedTimestamp = System.currentTimeMillis() + timeout;
|
||||
// Check if the time since the last join has not exceeded the timeout
|
||||
// The idea for this check was Taken from Velocity
|
||||
final long last = attemptCache.get(inetAddress, result -> expectedTimestamp);
|
||||
return expectedTimestamp == last;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -18,11 +18,13 @@
|
||||
package xyz.jonesdev.sonar.api.fallback;
|
||||
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelPipeline;
|
||||
import io.netty.util.ReferenceCountUtil;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.api.timer.SystemTimer;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.util.UUID;
|
||||
@@ -31,6 +33,9 @@ public interface FallbackUser {
|
||||
@NotNull
|
||||
Channel getChannel();
|
||||
|
||||
@NotNull
|
||||
ChannelPipeline getPipeline();
|
||||
|
||||
@NotNull
|
||||
InetAddress getInetAddress();
|
||||
|
||||
@@ -38,20 +43,27 @@ public interface FallbackUser {
|
||||
ProtocolVersion getProtocolVersion();
|
||||
|
||||
@NotNull
|
||||
FallbackUserState getState();
|
||||
SystemTimer getLoginTimer();
|
||||
|
||||
boolean isReceivedClientSettings();
|
||||
|
||||
void setReceivedClientSettings(final boolean receivedClientSettings);
|
||||
|
||||
boolean isReceivedPluginMessage();
|
||||
|
||||
void setReceivedPluginMessage(final boolean receivedPluginMessage);
|
||||
|
||||
boolean isGeyser();
|
||||
|
||||
void setState(final FallbackUserState state);
|
||||
|
||||
/**
|
||||
* Disconnect the player during/after verification
|
||||
* using our custom Disconnect packet.
|
||||
*
|
||||
* @param reason Disconnect message component
|
||||
* @param reason Disconnect message component
|
||||
* @param duringLogin True, if the player has not joined yet
|
||||
*/
|
||||
@ApiStatus.Internal
|
||||
void disconnect(final @NotNull Component reason);
|
||||
void disconnect(final @NotNull Component reason, final boolean duringLogin);
|
||||
|
||||
/**
|
||||
* Takes over the channel and begins the verification process
|
||||
|
||||
@@ -1,41 +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.api.fallback;
|
||||
|
||||
public enum FallbackUserState {
|
||||
// 1.20.2 configuration state
|
||||
LOGIN_ACK, CONFIGURE,
|
||||
// pre-JOIN ping check
|
||||
KEEP_ALIVE,
|
||||
// post-JOIN checks
|
||||
CLIENT_SETTINGS, PLUGIN_MESSAGE, TRANSACTION,
|
||||
// PLAY checks
|
||||
TELEPORT, POSITION,
|
||||
// Vehicle check
|
||||
VEHICLE,
|
||||
// CAPTCHA
|
||||
CAPTCHA,
|
||||
// Done
|
||||
SUCCESS,
|
||||
// Placeholder
|
||||
FAILED;
|
||||
|
||||
public boolean canReceivePackets() {
|
||||
return this != FAILED && this != SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,9 @@ import lombok.AccessLevel;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.ToString;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.sql.Timestamp;
|
||||
import java.util.UUID;
|
||||
|
||||
@@ -50,7 +52,7 @@ public final class VerifiedPlayer {
|
||||
uniqueIndexName = "ip_address_player_uuid_idx",
|
||||
width = 36
|
||||
)
|
||||
private UUID playerUUID;
|
||||
private UUID playerUuid;
|
||||
|
||||
@DatabaseField(
|
||||
columnName = "timestamp",
|
||||
@@ -58,9 +60,11 @@ public final class VerifiedPlayer {
|
||||
)
|
||||
private Timestamp timestamp;
|
||||
|
||||
public VerifiedPlayer(final String inetAddress, final UUID playerUUID, final long timestamp) {
|
||||
this.inetAddress = inetAddress;
|
||||
this.playerUUID = playerUUID;
|
||||
public VerifiedPlayer(final @NotNull InetAddress inetAddress,
|
||||
final @NotNull UUID playerUuid,
|
||||
final long timestamp) {
|
||||
this.inetAddress = inetAddress.toString();
|
||||
this.playerUuid = playerUuid;
|
||||
this.timestamp = new Timestamp(timestamp);
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ package xyz.jonesdev.sonar.api.statistics;
|
||||
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public interface SonarStatistics {
|
||||
|
||||
/**
|
||||
@@ -108,7 +109,7 @@ public interface SonarStatistics {
|
||||
|
||||
/**
|
||||
* @return Total number of cases where an IP was blacklisted
|
||||
* @apiNote This does not describe the current amount of blacklisted IPs
|
||||
* @apiNote This does not describe the current number of blacklisted IPs
|
||||
*/
|
||||
long getTotalBlacklistSize();
|
||||
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
package xyz.jonesdev.sonar.api.verbose;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.val;
|
||||
import net.kyori.adventure.audience.Audience;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.TextReplacementConfig;
|
||||
@@ -66,7 +65,7 @@ public final class Verbose implements Observable {
|
||||
}
|
||||
|
||||
public String nextAnimation() {
|
||||
val animations = Sonar.get().getConfig().getVerbose().getAnimation();
|
||||
final var animations = Sonar.get().getConfig().getVerbose().getAnimation();
|
||||
final int nextIndex = ++animationIndex % animations.size();
|
||||
return animations.toArray(new String[0])[nextIndex];
|
||||
}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import net.minecrell.pluginyml.bukkit.BukkitPluginDescription
|
||||
|
||||
plugins {
|
||||
id("net.minecrell.plugin-yml.bukkit") version "0.6.0"
|
||||
id("net.minecrell.plugin-yml.bukkit") version "0.6.0" apply true
|
||||
}
|
||||
|
||||
apply(plugin = "net.minecrell.plugin-yml.bukkit")
|
||||
|
||||
bukkit {
|
||||
name = rootProject.name
|
||||
description = rootProject.description
|
||||
version = rootProject.version.toString().split("-")[0]
|
||||
main = "xyz.jonesdev.sonar.bukkit.SonarBukkitPlugin"
|
||||
authors = listOf("Jones Development", "Sonar Contributors")
|
||||
@@ -33,25 +32,14 @@ dependencies {
|
||||
implementation(project(":api"))
|
||||
implementation(project(":common"))
|
||||
|
||||
// adventure platform support
|
||||
implementation("net.kyori:adventure-platform-bukkit:4.3.2")
|
||||
implementation("net.kyori:adventure-platform-api:4.3.2")
|
||||
implementation("net.kyori:adventure-platform-facet:4.3.2")
|
||||
compileOnly(rootProject.libs.spigot)
|
||||
|
||||
// adventure minimessage
|
||||
compileOnly("net.kyori:adventure-text-minimessage:4.16.0")
|
||||
compileOnly("net.kyori:adventure-text-serializer-gson:4.16.0")
|
||||
// adventure nbt
|
||||
implementation("net.kyori:adventure-nbt:4.16.0")
|
||||
|
||||
// We have to use 1.8 for backwards compatibility
|
||||
compileOnly("org.spigotmc:spigot-api:1.8.8-R0.1-SNAPSHOT")
|
||||
|
||||
// Implement bStats.org for metrics
|
||||
implementation("org.bstats:bstats-bukkit:3.0.2")
|
||||
|
||||
// Library/dependency loading
|
||||
implementation("com.alessiodp.libby:libby-bukkit:2.0.0-SNAPSHOT")
|
||||
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.bstats.bukkit)
|
||||
implementation(rootProject.libs.libby.bukkit)
|
||||
}
|
||||
|
||||
tasks {
|
||||
|
||||
@@ -23,7 +23,6 @@ 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.entity.Player;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import xyz.jonesdev.sonar.api.SonarPlatform;
|
||||
@@ -31,7 +30,6 @@ import xyz.jonesdev.sonar.api.logger.LoggerWrapper;
|
||||
import xyz.jonesdev.sonar.bukkit.command.BukkitSonarCommand;
|
||||
import xyz.jonesdev.sonar.common.boot.SonarBootstrap;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
|
||||
@@ -40,9 +38,8 @@ public final class SonarBukkit extends SonarBootstrap<SonarBukkitPlugin> {
|
||||
public static SonarBukkit INSTANCE;
|
||||
|
||||
public SonarBukkit(final @NotNull SonarBukkitPlugin plugin) {
|
||||
super(plugin, new BukkitLibraryManager(
|
||||
plugin, plugin.getDataFolder().getName()),
|
||||
plugin.getDataFolder(), SonarPlatform.BUKKIT);
|
||||
super(plugin, SonarPlatform.BUKKIT, plugin.getDataFolder(),
|
||||
new BukkitLibraryManager(plugin, plugin.getDataFolder().getName()));
|
||||
INSTANCE = this;
|
||||
}
|
||||
|
||||
@@ -59,20 +56,6 @@ public final class SonarBukkit extends SonarBootstrap<SonarBukkitPlugin> {
|
||||
return bukkitAudiences.player(uniqueId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasTooManyAccounts(final @NotNull InetAddress inetAddress, final int limit) {
|
||||
int count = 1;
|
||||
for (final Player player : getPlugin().getServer().getOnlinePlayers()) {
|
||||
// I don't know why, but Bukkit is *special*
|
||||
if (Objects.isNull(player.getAddress())) continue;
|
||||
// Check if the IP address of the player is equal to the IP trying to connect
|
||||
if (!Objects.equals(player.getAddress().getAddress(), inetAddress)) continue;
|
||||
// Increment count of duplicated accounts
|
||||
if (++count >= limit) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a wrapper for the plugin logger, so we can use it outside
|
||||
* the velocity module.
|
||||
@@ -116,7 +99,9 @@ public final class SonarBukkit extends SonarBootstrap<SonarBukkitPlugin> {
|
||||
|
||||
@Override
|
||||
public void disable() {
|
||||
// Make sure to properly shutdown bStats metrics
|
||||
metrics.shutdown();
|
||||
if (metrics != null) {
|
||||
// Make sure to properly shutdown bStats metrics
|
||||
metrics.shutdown();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,10 @@
|
||||
plugins {
|
||||
id("net.minecrell.plugin-yml.bungee") version "0.6.0"
|
||||
id("net.minecrell.plugin-yml.bungee") version "0.6.0" apply true
|
||||
}
|
||||
|
||||
apply(plugin = "net.minecrell.plugin-yml.bungee")
|
||||
|
||||
bungee {
|
||||
name = rootProject.name
|
||||
description = rootProject.description
|
||||
version = rootProject.version.toString().split("-")[0]
|
||||
main = "xyz.jonesdev.sonar.bungee.SonarBungeePlugin"
|
||||
author = "Jones Development, Sonar Contributors"
|
||||
@@ -16,25 +15,15 @@ dependencies {
|
||||
implementation(project(":api"))
|
||||
implementation(project(":common"))
|
||||
|
||||
compileOnly("net.md_5:bungeecord-proxy:master-SNAPSHOT")
|
||||
testCompileOnly("net.md_5:bungeecord-proxy:master-SNAPSHOT")
|
||||
compileOnly(rootProject.libs.bungeecord)
|
||||
testCompileOnly(rootProject.libs.bungeecord)
|
||||
|
||||
// adventure platform support
|
||||
implementation("net.kyori:adventure-platform-bungeecord:4.3.2")
|
||||
implementation("net.kyori:adventure-platform-api:4.3.2")
|
||||
implementation("net.kyori:adventure-platform-facet:4.3.2")
|
||||
|
||||
// adventure minimessage
|
||||
compileOnly("net.kyori:adventure-text-minimessage:4.16.0")
|
||||
compileOnly("net.kyori:adventure-text-serializer-gson:4.16.0")
|
||||
// adventure nbt
|
||||
implementation("net.kyori:adventure-nbt:4.16.0")
|
||||
|
||||
// Implement bStats.org for metrics
|
||||
implementation("org.bstats:bstats-bungeecord:3.0.2")
|
||||
|
||||
// Library/dependency loading
|
||||
implementation("com.alessiodp.libby:libby-bungee:2.0.0-SNAPSHOT")
|
||||
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.bstats.bungee)
|
||||
implementation(rootProject.libs.libby.bungee)
|
||||
}
|
||||
|
||||
tasks {
|
||||
|
||||
@@ -21,7 +21,6 @@ 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.connection.ProxiedPlayer;
|
||||
import org.bstats.bungeecord.Metrics;
|
||||
import org.bstats.charts.SimplePie;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
@@ -32,8 +31,6 @@ import xyz.jonesdev.sonar.bungee.command.BungeeSonarCommand;
|
||||
import xyz.jonesdev.sonar.bungee.fallback.FallbackInjectionHelper;
|
||||
import xyz.jonesdev.sonar.common.boot.SonarBootstrap;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
|
||||
@Getter
|
||||
@@ -41,7 +38,7 @@ public final class SonarBungee extends SonarBootstrap<SonarBungeePlugin> {
|
||||
public static SonarBungee INSTANCE;
|
||||
|
||||
public SonarBungee(final @NotNull SonarBungeePlugin plugin) {
|
||||
super(plugin, new BungeeLibraryManager(plugin), plugin.getDataFolder(), SonarPlatform.BUNGEE);
|
||||
super(plugin, SonarPlatform.BUNGEE, plugin.getDataFolder(), new BungeeLibraryManager(plugin));
|
||||
INSTANCE = this;
|
||||
}
|
||||
|
||||
@@ -58,19 +55,6 @@ public final class SonarBungee extends SonarBootstrap<SonarBungeePlugin> {
|
||||
return bungeeAudiences.player(uniqueId);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("deprecation")
|
||||
public boolean hasTooManyAccounts(final @NotNull InetAddress inetAddress, final int limit) {
|
||||
int count = 1;
|
||||
for (final ProxiedPlayer player : getPlugin().getServer().getPlayers()) {
|
||||
// Check if the IP address of the player is equal to the IP trying to connect
|
||||
if (!Objects.equals(player.getAddress().getAddress(), inetAddress)) continue;
|
||||
// Increment count of duplicated accounts
|
||||
if (++count >= limit) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a wrapper for the plugin logger, so we can use it outside
|
||||
* the velocity module.
|
||||
@@ -117,7 +101,9 @@ public final class SonarBungee extends SonarBootstrap<SonarBungeePlugin> {
|
||||
|
||||
@Override
|
||||
public void disable() {
|
||||
// Make sure to properly shutdown bStats metrics
|
||||
metrics.shutdown();
|
||||
if (metrics != null) {
|
||||
// Make sure to properly shutdown bStats metrics
|
||||
metrics.shutdown();
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -54,7 +54,7 @@ public final class FallbackChannelHandler extends FallbackChannelHandlerAdapter
|
||||
@Override
|
||||
public void channelRead(final @NotNull ChannelHandlerContext ctx, final Object msg) throws Exception {
|
||||
// Intercept any packets processed by BungeeCord
|
||||
if (msg instanceof PacketWrapper) {
|
||||
if (listenForPackets && msg instanceof PacketWrapper) {
|
||||
final PacketWrapper packetWrapper = (PacketWrapper) msg;
|
||||
final DefinedPacket wrappedPacket = packetWrapper.packet;
|
||||
// Don't handle any invalid packets
|
||||
@@ -65,7 +65,7 @@ public final class FallbackChannelHandler extends FallbackChannelHandlerAdapter
|
||||
handleHandshake(handshake.getHost(), handshake.getProtocolVersion());
|
||||
}
|
||||
// Intercept any server login packet by the client
|
||||
if (wrappedPacket instanceof LoginRequest) {
|
||||
else if (wrappedPacket instanceof LoginRequest) {
|
||||
// Make sure to use the potentially modified, real IP
|
||||
final LoginRequest loginRequest = (LoginRequest) wrappedPacket;
|
||||
final HandlerBoss handlerBoss = channel.pipeline().get(HandlerBoss.class);
|
||||
|
||||
+7
-5
@@ -22,19 +22,21 @@ import io.netty.channel.ChannelInitializer;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import xyz.jonesdev.sonar.api.ReflectiveOperationException;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.invoke.MethodHandle;
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.lang.invoke.MethodType;
|
||||
|
||||
import static net.md_5.bungee.netty.PipelineUtils.PACKET_DECODER;
|
||||
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_HANDLER;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public final class FallbackChannelInitializer extends ChannelInitializer<Channel> {
|
||||
private static final Method INIT_CHANNEL_METHOD;
|
||||
private static final MethodHandle INIT_CHANNEL_METHOD;
|
||||
|
||||
static {
|
||||
try {
|
||||
INIT_CHANNEL_METHOD = ChannelInitializer.class.getDeclaredMethod("initChannel", Channel.class);
|
||||
INIT_CHANNEL_METHOD.setAccessible(true);
|
||||
INIT_CHANNEL_METHOD = MethodHandles.privateLookupIn(ChannelInitializer.class, MethodHandles.lookup())
|
||||
.findVirtual(ChannelInitializer.class, "initChannel", MethodType.methodType(void.class, Channel.class));
|
||||
} catch (Throwable throwable) {
|
||||
throw new ReflectiveOperationException(throwable);
|
||||
}
|
||||
@@ -46,7 +48,7 @@ public final class FallbackChannelInitializer extends ChannelInitializer<Channel
|
||||
protected void initChannel(final Channel channel) throws Exception {
|
||||
// Invoke the original method
|
||||
try {
|
||||
INIT_CHANNEL_METHOD.invoke(originalChannelInitializer, channel);
|
||||
INIT_CHANNEL_METHOD.invokeExact(originalChannelInitializer, channel);
|
||||
} catch (Throwable throwable) {
|
||||
throw new ReflectiveOperationException(throwable);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
dependencies {
|
||||
compileOnly(project(":api"))
|
||||
|
||||
// adventure nbt for packets
|
||||
compileOnly("net.kyori:adventure-nbt:4.16.0")
|
||||
compileOnly(rootProject.libs.adventure.nbt)
|
||||
|
||||
// CAPTCHA image generation
|
||||
implementation("xyz.jonesdev:capja:1.0.2")
|
||||
implementation(rootProject.libs.capja)
|
||||
}
|
||||
|
||||
java.sourceCompatibility = JavaVersion.VERSION_11
|
||||
|
||||
@@ -21,12 +21,11 @@ import com.alessiodp.libby.Library;
|
||||
import com.alessiodp.libby.LibraryManager;
|
||||
import lombok.experimental.UtilityClass;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.SonarPlatform;
|
||||
|
||||
@UtilityClass
|
||||
class LibraryLoader {
|
||||
void loadLibraries(final @NotNull LibraryManager libraryManager) {
|
||||
void loadLibraries(final @NotNull LibraryManager libraryManager, final @NotNull SonarPlatform platform) {
|
||||
libraryManager.addMavenCentral();
|
||||
libraryManager.addJitPack();
|
||||
libraryManager.loadLibraries(
|
||||
@@ -74,19 +73,19 @@ class LibraryLoader {
|
||||
.build()
|
||||
);
|
||||
|
||||
// Only load adventure dependencies if needed
|
||||
if (Sonar.get().getPlatform() != SonarPlatform.VELOCITY) {
|
||||
// Only load adventure if not on Velocity
|
||||
if (platform != SonarPlatform.VELOCITY) {
|
||||
libraryManager.loadLibraries(
|
||||
Library.builder()
|
||||
.groupId("net{}kyori")
|
||||
.artifactId("adventure-text-minimessage")
|
||||
.version("4.16.0")
|
||||
.version("4.17.0")
|
||||
.relocate("net{}kyori", "xyz{}jonesdev{}sonar{}libs{}kyori")
|
||||
.build(),
|
||||
Library.builder()
|
||||
.groupId("net{}kyori")
|
||||
.artifactId("adventure-text-serializer-gson")
|
||||
.version("4.16.0")
|
||||
.version("4.17.0")
|
||||
.relocate("net{}kyori", "xyz{}jonesdev{}sonar{}libs{}kyori")
|
||||
.build()
|
||||
);
|
||||
|
||||
@@ -19,6 +19,7 @@ package xyz.jonesdev.sonar.common.boot;
|
||||
|
||||
import com.alessiodp.libby.LibraryManager;
|
||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||
import com.github.benmanes.caffeine.cache.Ticker;
|
||||
import com.j256.ormlite.logger.Level;
|
||||
import com.j256.ormlite.logger.Logger;
|
||||
import lombok.Getter;
|
||||
@@ -54,27 +55,22 @@ public abstract class SonarBootstrap<T> implements Sonar {
|
||||
private Notification notificationHandler;
|
||||
private SonarConfiguration config;
|
||||
private VerifiedPlayerController verifiedPlayerController;
|
||||
private File dataDirectory;
|
||||
private final LibraryManager libraryManager;
|
||||
private final SonarStatistics statistics;
|
||||
private final SonarPlatform platform;
|
||||
private final SubcommandRegistry subcommandRegistry;
|
||||
private final SystemTimer launchTimer = new SystemTimer();
|
||||
|
||||
public SonarBootstrap(final @NotNull T plugin,
|
||||
final LibraryManager libraryManager,
|
||||
final File dataDirectory,
|
||||
final SonarPlatform platform) {
|
||||
final @NotNull SonarPlatform platform,
|
||||
final @NotNull File dataDirectory,
|
||||
final @NotNull LibraryManager libraryManager) {
|
||||
// Load all libraries before anything else
|
||||
LibraryLoader.loadLibraries(libraryManager, platform);
|
||||
// Set the Sonar API
|
||||
SonarSupplier.set(this);
|
||||
// Store our library manager for the API
|
||||
this.libraryManager = libraryManager;
|
||||
// Set the plugin instance
|
||||
this.plugin = plugin;
|
||||
this.dataDirectory = dataDirectory;
|
||||
this.platform = platform;
|
||||
// Load all libraries before anything else
|
||||
LibraryLoader.loadLibraries(libraryManager);
|
||||
// Load the rest of the components
|
||||
this.statistics = new GlobalSonarStatistics();
|
||||
this.verboseHandler = new Verbose();
|
||||
@@ -142,9 +138,11 @@ public abstract class SonarBootstrap<T> implements Sonar {
|
||||
// Update ratelimiter caches
|
||||
getFallback().getRatelimiter().setAttemptCache(Caffeine.newBuilder()
|
||||
.expireAfterWrite(Duration.ofMillis(getConfig().getVerification().getReconnectDelay()))
|
||||
.ticker(Ticker.systemTicker())
|
||||
.build());
|
||||
getFallback().getRatelimiter().setFailCountCache(Caffeine.newBuilder()
|
||||
.expireAfterWrite(Duration.ofMillis(getConfig().getVerification().getRememberTime()))
|
||||
.ticker(Ticker.systemTicker())
|
||||
.build());
|
||||
|
||||
// Update blacklist cache
|
||||
@@ -153,9 +151,10 @@ public abstract class SonarBootstrap<T> implements Sonar {
|
||||
// Make sure the blacklist is only set when we need it to prevent data loss
|
||||
if (!blacklistExists // Make sure we create a new cache if it doesn't exist yet
|
||||
|| getFallback().getBlacklistTime() != blacklistTime) {
|
||||
// Create new cache with the configured blacklist time
|
||||
// Create a new cache with the configured blacklist time
|
||||
getFallback().setBlacklist(Caffeine.newBuilder()
|
||||
.expireAfterWrite(Duration.ofMillis(getConfig().getVerification().getBlacklistTime()))
|
||||
.ticker(Ticker.systemTicker())
|
||||
.build());
|
||||
// Store the new blacklist time, so we don't have to reset the blacklist every reload
|
||||
getFallback().setBlacklistTime(blacklistTime);
|
||||
@@ -178,8 +177,10 @@ public abstract class SonarBootstrap<T> implements Sonar {
|
||||
getLogger().info("Starting shutdown process...");
|
||||
// Interrupt threads
|
||||
SonarServiceThreadManager.stop();
|
||||
// Close database connection
|
||||
verifiedPlayerController.close();
|
||||
// Close database connection if present
|
||||
if (verifiedPlayerController != null) {
|
||||
verifiedPlayerController.close();
|
||||
}
|
||||
// Run the per-platform disable method
|
||||
disable();
|
||||
// Thank the user for using Sonar
|
||||
|
||||
+2
-2
@@ -39,7 +39,7 @@ public final class FallbackBandwidthHandler extends ChannelDuplexHandler {
|
||||
final @NotNull Object msg) throws Exception {
|
||||
// We can only get the size of a message if it's a ByteBuf
|
||||
if (msg instanceof ByteBuf) {
|
||||
// Increment the incoming traffic by the amount of readable bytes
|
||||
// Increment the incoming traffic by the number of readable bytes
|
||||
INCOMING.increment(((ByteBuf) msg).readableBytes());
|
||||
}
|
||||
// Make sure to let the server handle the rest
|
||||
@@ -52,7 +52,7 @@ public final class FallbackBandwidthHandler extends ChannelDuplexHandler {
|
||||
final ChannelPromise promise) throws Exception {
|
||||
// We can only get the size of a message if it's a ByteBuf
|
||||
if (msg instanceof ByteBuf) {
|
||||
// Increment the outgoing traffic by the amount of readable bytes
|
||||
// Increment the outgoing traffic by the number of readable bytes
|
||||
OUTGOING.increment(((ByteBuf) msg).readableBytes());
|
||||
}
|
||||
// Make sure to let the server handle the rest
|
||||
|
||||
+32
-25
@@ -28,7 +28,6 @@ import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.fallback.Fallback;
|
||||
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer;
|
||||
import xyz.jonesdev.sonar.common.statistics.GlobalSonarStatistics;
|
||||
import xyz.jonesdev.sonar.common.util.GeyserDetection;
|
||||
|
||||
@@ -39,7 +38,6 @@ 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.FallbackUserWrapper.deject;
|
||||
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer.*;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
@@ -48,29 +46,38 @@ public class FallbackChannelHandlerAdapter extends ChannelInboundHandlerAdapter
|
||||
protected @Nullable String username;
|
||||
protected InetAddress inetAddress;
|
||||
protected ProtocolVersion protocolVersion;
|
||||
protected @Nullable FallbackUser user;
|
||||
protected boolean listenForPackets = true;
|
||||
|
||||
protected static final Fallback FALLBACK = Sonar.get().getFallback();
|
||||
|
||||
@Override
|
||||
public final void channelActive(final @NotNull ChannelHandlerContext ctx) {
|
||||
public final void channelActive(final @NotNull ChannelHandlerContext ctx) throws Exception {
|
||||
// Increase connections per second for the action bar verbose
|
||||
GlobalSonarStatistics.countConnection();
|
||||
// Hook the traffic listener
|
||||
channel.pipeline().addFirst(FALLBACK_BANDWIDTH, FallbackBandwidthHandler.INSTANCE);
|
||||
// Make sure to let the server handle the rest
|
||||
ctx.fireChannelActive();
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void channelInactive(final @NotNull ChannelHandlerContext ctx) {
|
||||
public final void channelInactive(final @NotNull ChannelHandlerContext ctx) throws Exception {
|
||||
// The player can disconnect without sending the login packet first
|
||||
if (username != null) {
|
||||
// Remove the username from the connected players
|
||||
FALLBACK.getConnected().remove(username);
|
||||
}
|
||||
// The player cannot be in the queue if the IP address is invalid
|
||||
// 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().remove(inetAddress);
|
||||
// Remove the IP address from the queue
|
||||
FALLBACK.getQueue().getQueuedPlayers().remove(inetAddress);
|
||||
// Remove this account from the online players
|
||||
// or decrement the number of accounts with the same IP
|
||||
FALLBACK.getOnline().compute(inetAddress, (key, value) -> {
|
||||
if (value == null || value <= 1) {
|
||||
return null;
|
||||
} else {
|
||||
return value - 1;
|
||||
}
|
||||
});
|
||||
}
|
||||
// Make sure to let the server handle the rest
|
||||
ctx.fireChannelInactive();
|
||||
@@ -82,7 +89,7 @@ public class FallbackChannelHandlerAdapter extends ChannelInboundHandlerAdapter
|
||||
@Override
|
||||
public final void exceptionCaught(final @NotNull ChannelHandlerContext ctx,
|
||||
final @NotNull Throwable cause) throws Exception {
|
||||
// Simply close the channel if we encounter any errors.
|
||||
// Close the channel if we encounter any errors.
|
||||
ctx.close();
|
||||
}
|
||||
|
||||
@@ -103,9 +110,6 @@ public class FallbackChannelHandlerAdapter extends ChannelInboundHandlerAdapter
|
||||
}
|
||||
// Store the protocol version
|
||||
protocolVersion = ProtocolVersion.fromId(protocol);
|
||||
// Hook the traffic listener
|
||||
// TODO: Can we implement this in channelActive?
|
||||
channel.pipeline().addFirst(FALLBACK_BANDWIDTH, FallbackBandwidthHandler.INSTANCE);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -131,8 +135,7 @@ public class FallbackChannelHandlerAdapter extends ChannelInboundHandlerAdapter
|
||||
// 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 unknown versions;
|
||||
// I'll try my best to stay up-to-date!
|
||||
// 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
|
||||
@@ -166,10 +169,8 @@ public class FallbackChannelHandlerAdapter extends ChannelInboundHandlerAdapter
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if Fallback is already verifying a player
|
||||
// → is another player with the same IP address connected to Fallback?
|
||||
if (FALLBACK.getConnected().containsKey(username)
|
||||
|| FALLBACK.getConnected().containsValue(inetAddress)) {
|
||||
// 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;
|
||||
}
|
||||
@@ -213,7 +214,7 @@ public class FallbackChannelHandlerAdapter extends ChannelInboundHandlerAdapter
|
||||
}
|
||||
|
||||
// Create an instance for the Fallback connection
|
||||
user = new FallbackUserWrapper(channel, inetAddress, protocolVersion, geyser);
|
||||
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);
|
||||
});
|
||||
@@ -229,19 +230,25 @@ public class FallbackChannelHandlerAdapter extends ChannelInboundHandlerAdapter
|
||||
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
|
||||
if (Sonar.get().hasTooManyAccounts(inetAddress, maxOnlinePerIp)) {
|
||||
customDisconnect(channel, protocolVersion, FallbackPreparer.tooManyOnlinePerIP, encoder, handler);
|
||||
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);
|
||||
// Deject the channel since we don't need it anymore
|
||||
deject(channel.pipeline());
|
||||
}
|
||||
}
|
||||
+9
-16
@@ -26,8 +26,6 @@ import org.jetbrains.annotations.NotNull;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public final class FallbackTimeoutHandler extends IdleStateHandler {
|
||||
private boolean knownDisconnect;
|
||||
|
||||
public FallbackTimeoutHandler(final long timeout, final TimeUnit timeUnit) {
|
||||
super(timeout, 0L, 0L, timeUnit);
|
||||
}
|
||||
@@ -35,21 +33,16 @@ public final class FallbackTimeoutHandler extends IdleStateHandler {
|
||||
@Override
|
||||
protected void channelIdle(final ChannelHandlerContext ctx,
|
||||
final @NotNull IdleStateEvent idleStateEvent) throws Exception {
|
||||
assert idleStateEvent.state() == IdleState.READER_IDLE;
|
||||
readTimedOut(ctx);
|
||||
}
|
||||
// We are only checking the read timeout state - skip all other states
|
||||
if (idleStateEvent.state() != IdleState.READER_IDLE) {
|
||||
return;
|
||||
}
|
||||
|
||||
private void readTimedOut(final ChannelHandlerContext ctx) {
|
||||
if (!knownDisconnect) {
|
||||
|
||||
// The netty (default) ReadTimeoutHandler would normally just throw an Exception
|
||||
// The default ReadTimeoutHandler does only check for the boolean 'closed' and
|
||||
// still throws the Exception even if the channel is closed
|
||||
if (ctx.channel().isActive()) {
|
||||
ctx.close();
|
||||
}
|
||||
|
||||
knownDisconnect = true;
|
||||
// The netty (default) ReadTimeoutHandler would normally just throw an Exception
|
||||
// The default ReadTimeoutHandler does only check for the boolean 'closed' and
|
||||
// still throws the Exception even if the channel is closed
|
||||
if (ctx.channel().isActive()) {
|
||||
ctx.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
+33
-50
@@ -21,6 +21,7 @@ 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;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
@@ -31,20 +32,22 @@ import xyz.jonesdev.sonar.api.event.impl.UserBlacklistedEvent;
|
||||
import xyz.jonesdev.sonar.api.event.impl.UserVerifyFailedEvent;
|
||||
import xyz.jonesdev.sonar.api.event.impl.UserVerifyJoinEvent;
|
||||
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
|
||||
import xyz.jonesdev.sonar.api.fallback.FallbackUserState;
|
||||
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.protocol.FallbackPacketDecoder;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketEncoder;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.DisconnectPacket;
|
||||
import xyz.jonesdev.sonar.common.fallback.session.FallbackLoginSessionHandler;
|
||||
import xyz.jonesdev.sonar.common.statistics.GlobalSonarStatistics;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.*;
|
||||
import static xyz.jonesdev.sonar.api.fallback.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.common.fallback.protocol.FallbackPacketRegistry.CONFIG;
|
||||
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry.GAME;
|
||||
@@ -56,9 +59,12 @@ public final class FallbackUserWrapper implements FallbackUser {
|
||||
private final ChannelPipeline pipeline;
|
||||
private final InetAddress inetAddress;
|
||||
private final ProtocolVersion protocolVersion;
|
||||
private final boolean geyser;
|
||||
@Setter
|
||||
private FallbackUserState state = FallbackUserState.LOGIN_ACK; // 1.20.2+
|
||||
private boolean receivedClientSettings;
|
||||
@Setter
|
||||
private boolean receivedPluginMessage;
|
||||
private final boolean geyser;
|
||||
private final SystemTimer loginTimer = new SystemTimer();
|
||||
|
||||
public FallbackUserWrapper(final @NotNull Channel channel,
|
||||
final @NotNull InetAddress inetAddress,
|
||||
@@ -72,8 +78,8 @@ public final class FallbackUserWrapper implements FallbackUser {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void disconnect(final @NotNull Component reason) {
|
||||
closeWith(channel, protocolVersion, DisconnectPacket.create(reason, false));
|
||||
public void disconnect(final @NotNull Component reason, final boolean duringLogin) {
|
||||
closeWith(channel, protocolVersion, DisconnectPacket.create(reason, duringLogin));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -99,7 +105,7 @@ public final class FallbackUserWrapper implements FallbackUser {
|
||||
Sonar.get().getEventManager().publish(new UserVerifyJoinEvent(username, this));
|
||||
|
||||
// Mark the player as connected → verifying players
|
||||
Sonar.get().getFallback().getConnected().put(username, inetAddress);
|
||||
Sonar.get().getFallback().getConnected().put(inetAddress, (byte) 0);
|
||||
|
||||
// Add better timeout handler to avoid known exploits or issues
|
||||
// We also want to timeout bots quickly to avoid flooding
|
||||
@@ -111,7 +117,7 @@ public final class FallbackUserWrapper implements FallbackUser {
|
||||
pipeline.replace(encoder, FALLBACK_PACKET_ENCODER, newEncoder);
|
||||
|
||||
channel.eventLoop().execute(() -> {
|
||||
// Remove main pipeline to completely take over the channel
|
||||
// Remove the main pipeline to completely take over the channel
|
||||
if (pipeline.get(handler) != null) {
|
||||
pipeline.remove(handler);
|
||||
}
|
||||
@@ -123,16 +129,11 @@ public final class FallbackUserWrapper implements FallbackUser {
|
||||
newEncoder.updateRegistry(protocolVersion.compareTo(MINECRAFT_1_20_2) >= 0 ? CONFIG : GAME);
|
||||
|
||||
try {
|
||||
// Prepare custom packet decoder and verification handler
|
||||
final FallbackVerificationHandler verification = new FallbackVerificationHandler(this, username, uuid);
|
||||
final FallbackPacketDecoder fallbackPacketDecoder = new FallbackPacketDecoder(this, verification);
|
||||
// Replace normal decoder to allow custom packets
|
||||
final FallbackPacketDecoder fallbackPacketDecoder = new FallbackPacketDecoder(protocolVersion);
|
||||
pipeline.replace(decoder, FALLBACK_PACKET_DECODER, fallbackPacketDecoder);
|
||||
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_20_2) < 0) {
|
||||
// Start initializing the actual join process for pre-1.20.2 clients
|
||||
verification.initialJoinProcess();
|
||||
}
|
||||
// Listen for all incoming packets by setting the packet listener
|
||||
fallbackPacketDecoder.setListener(new FallbackLoginSessionHandler(this, username, uuid));
|
||||
} catch (Throwable throwable) {
|
||||
// This rarely happens when the channel hangs and the player is still connecting
|
||||
// I honestly have no idea how else I'm supposed to fix this
|
||||
@@ -143,27 +144,21 @@ public final class FallbackUserWrapper implements FallbackUser {
|
||||
|
||||
@Override
|
||||
public void fail(final @NotNull String reason) {
|
||||
// Make sure to set the state to failed to drop all handlers
|
||||
setState(FallbackUserState.FAILED);
|
||||
disconnect(Sonar.get().getConfig().getVerification().getVerificationFailed(), false);
|
||||
|
||||
// Only log the failed message if the server isn't currently under attack.
|
||||
// However, we let the user override this through the configuration.
|
||||
final boolean shouldLog = Sonar.get().getAttackTracker().getCurrentAttack() == null
|
||||
|| Sonar.get().getConfig().getVerification().isLogDuringAttack();
|
||||
|
||||
// Only disconnect the player and log if the channel is active
|
||||
if (channel.isActive()) {
|
||||
disconnect(Sonar.get().getConfig().getVerification().getVerificationFailed());
|
||||
|
||||
if (shouldLog) {
|
||||
Sonar.get().getFallback().getLogger().info(Sonar.get().getConfig().getVerification().getFailedLog()
|
||||
.replace("%ip%", Sonar.get().getConfig().formatAddress(getInetAddress()))
|
||||
.replace("%protocol%", String.valueOf(getProtocolVersion().getProtocol()))
|
||||
.replace("%reason%", reason));
|
||||
}
|
||||
if (shouldLog) {
|
||||
Sonar.get().getFallback().getLogger().info(Sonar.get().getConfig().getVerification().getFailedLog()
|
||||
.replace("%ip%", Sonar.get().getConfig().formatAddress(getInetAddress()))
|
||||
.replace("%protocol%", String.valueOf(getProtocolVersion().getProtocol()))
|
||||
.replace("%reason%", reason));
|
||||
}
|
||||
|
||||
// Increment amount of total failed verifications
|
||||
// Increment number of total failed verifications
|
||||
GlobalSonarStatistics.totalFailedVerifications++;
|
||||
|
||||
// Call the VerifyFailedEvent for external API usage
|
||||
@@ -177,20 +172,18 @@ public final class FallbackUserWrapper implements FallbackUser {
|
||||
if (blacklistThreshold <= 0) break blacklist;
|
||||
|
||||
// Use 1 as the default amount of fails since we haven't cached anything yet
|
||||
final int failCount = Sonar.get().getFallback().getRatelimiter().getFailCountCache()
|
||||
.asMap().getOrDefault(inetAddress, 1);
|
||||
|
||||
final int fails = Sonar.get().getFallback().getRatelimiter().getFailCountCache().get(inetAddress, ignored -> 1);
|
||||
// Now we simply need to check if the threshold is reached
|
||||
if (failCount < blacklistThreshold) {
|
||||
if (fails < blacklistThreshold) {
|
||||
// Make sure we increment the number of fails
|
||||
Sonar.get().getFallback().getRatelimiter().incrementFails(inetAddress, failCount);
|
||||
Sonar.get().getFallback().getRatelimiter().incrementFails(inetAddress, fails);
|
||||
break blacklist;
|
||||
}
|
||||
|
||||
// Call the BotBlacklistedEvent for external API usage
|
||||
Sonar.get().getEventManager().publish(new UserBlacklistedEvent(this));
|
||||
|
||||
// Increment amount of total blacklisted players
|
||||
// Increment number of total blacklisted players
|
||||
GlobalSonarStatistics.totalBlacklistedPlayers++;
|
||||
|
||||
Sonar.get().getFallback().getBlacklist().put(getInetAddress(), (byte) 0);
|
||||
@@ -204,6 +197,9 @@ public final class FallbackUserWrapper implements FallbackUser {
|
||||
// Invalidate the cached entry to ensure memory safety
|
||||
Sonar.get().getFallback().getRatelimiter().getFailCountCache().invalidate(inetAddress);
|
||||
}
|
||||
|
||||
// Throw an exception to avoid further code execution
|
||||
throw new CorruptedFrameException();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -256,12 +252,12 @@ public final class FallbackUserWrapper implements FallbackUser {
|
||||
final @NotNull FallbackPacket packet,
|
||||
final @NotNull String encoder,
|
||||
final @NotNull String boss) {
|
||||
// Remove main pipeline to completely take over the channel
|
||||
// Remove the main pipeline to completely take over the channel
|
||||
if (channel.pipeline().get(boss) != null) {
|
||||
channel.pipeline().remove(boss);
|
||||
}
|
||||
final ChannelHandler currentEncoder = channel.pipeline().get(encoder);
|
||||
// Simply close the channel if no decoder exists
|
||||
// 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)) {
|
||||
@@ -273,17 +269,4 @@ public final class FallbackUserWrapper implements FallbackUser {
|
||||
channel.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes Sonar's pipelines from a channel to save
|
||||
* unnecessary RAM and CPU usage
|
||||
*
|
||||
* @param pipeline Pipeline object of the channel
|
||||
*/
|
||||
public static void deject(final @NotNull ChannelPipeline pipeline) {
|
||||
// Remove Sonar's pipelines if they exist
|
||||
if (pipeline.get(FALLBACK_HANDLER) != null) {
|
||||
pipeline.remove(FALLBACK_HANDLER);
|
||||
}
|
||||
}
|
||||
}
|
||||
-741
@@ -1,741 +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.buffer.ByteBuf;
|
||||
import io.netty.handler.codec.CorruptedFrameException;
|
||||
import io.netty.handler.codec.DecoderException;
|
||||
import lombok.val;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.event.impl.UserVerifySuccessEvent;
|
||||
import xyz.jonesdev.sonar.api.fallback.Fallback;
|
||||
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
|
||||
import xyz.jonesdev.sonar.api.fallback.FallbackUserState;
|
||||
import xyz.jonesdev.sonar.api.model.VerifiedPlayer;
|
||||
import xyz.jonesdev.sonar.api.timer.SystemTimer;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.*;
|
||||
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.entity.EntityType;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.config.FinishConfigurationPacket;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.login.LoginAcknowledgedPacket;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.*;
|
||||
import xyz.jonesdev.sonar.common.statistics.GlobalSonarStatistics;
|
||||
import xyz.jonesdev.sonar.common.util.ProtocolUtil;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.Random;
|
||||
import java.util.UUID;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.config.SonarConfiguration.Verification.Gravity.Gamemode.CREATIVE;
|
||||
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.FallbackUserState.*;
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
|
||||
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer.*;
|
||||
|
||||
public final class FallbackVerificationHandler implements FallbackPacketListener {
|
||||
private static final Random RANDOM = new Random();
|
||||
private static final Fallback FALLBACK = Sonar.get().getFallback();
|
||||
|
||||
private final SystemTimer login = new SystemTimer();
|
||||
private final @NotNull FallbackUser user;
|
||||
private final String username;
|
||||
private final UUID playerUuid;
|
||||
|
||||
// Checks
|
||||
private short expectedTransactionId;
|
||||
private int expectedKeepAliveId, expectedTeleportId = -1;
|
||||
private int tick, totalReceivedPackets;
|
||||
private double posX, posY, posZ, lastY, spawnYPosition;
|
||||
private boolean resolvedClientBrand, resolvedClientSettings;
|
||||
private boolean listenForMovements;
|
||||
private boolean receivedSteerBoat, receivedPlayerInput;
|
||||
|
||||
// CAPTCHA
|
||||
private @Nullable MapCaptchaInfo captcha;
|
||||
private int captchaTriesLeft, lastCountdownIndex;
|
||||
|
||||
// Cached options to make sure the original values don't change throughout the verification
|
||||
private final boolean performVehicle, performCaptcha, performGravity, performCollisions, transfer;
|
||||
|
||||
public FallbackVerificationHandler(final @NotNull FallbackUser user,
|
||||
final @NotNull String username,
|
||||
final @NotNull UUID playerUuid) {
|
||||
this.user = user;
|
||||
this.username = username;
|
||||
this.playerUuid = playerUuid;
|
||||
// We don't want to check Geyser players for valid gravity, as this might cause issues because of the protocol
|
||||
this.performGravity = !user.isGeyser() && Sonar.get().getConfig().getVerification().getGravity().isEnabled();
|
||||
this.performCollisions = Sonar.get().getConfig().getVerification().getGravity().isCheckCollisions();
|
||||
this.performCaptcha = FALLBACK.shouldPerformCaptcha();
|
||||
this.performVehicle = FALLBACK.shouldPerformVehicleCheck();
|
||||
this.transfer = Sonar.get().getConfig().getVerification().getTransfer().isEnabled()
|
||||
&& user.getProtocolVersion().compareTo(MINECRAFT_1_20_5) >= 0;
|
||||
}
|
||||
|
||||
private void configure() {
|
||||
// Set the state to CONFIGURE to avoid false positives
|
||||
user.setState(CONFIGURE);
|
||||
// Send the necessary configuration packets to the client
|
||||
if (user.getProtocolVersion().compareTo(MINECRAFT_1_20_5) >= 0) {
|
||||
for (final FallbackPacket packet : REGISTRY_SYNC_1_20_5) {
|
||||
user.delayedWrite(packet);
|
||||
}
|
||||
} else {
|
||||
user.delayedWrite(REGISTRY_SYNC_LEGACY);
|
||||
}
|
||||
user.delayedWrite(FINISH_CONFIGURATION);
|
||||
// Send all packets in one flush
|
||||
user.getChannel().flush();
|
||||
// Set decoder state to actually catch all packets
|
||||
updateEncoderDecoderState(FallbackPacketRegistry.CONFIG);
|
||||
}
|
||||
|
||||
private void updateEncoderDecoderState(final @NotNull FallbackPacketRegistry registry) {
|
||||
val decoder = (FallbackPacketDecoder) user.getChannel().pipeline().get(FALLBACK_PACKET_DECODER);
|
||||
val encoder = (FallbackPacketEncoder) user.getChannel().pipeline().get(FALLBACK_PACKET_ENCODER);
|
||||
if (decoder != null && encoder != null) {
|
||||
// Update the packet registry state to be able to listen for CONFIG packets
|
||||
decoder.updateRegistry(registry);
|
||||
encoder.updateRegistry(registry);
|
||||
} else {
|
||||
// Something went wrong - the decoder should not be null
|
||||
FALLBACK.getLogger().warn("Necessary pipelines for {} not found", username);
|
||||
// Close the channel to prevent bypasses or other potential exploits
|
||||
user.getChannel().close();
|
||||
}
|
||||
}
|
||||
|
||||
public void initialJoinProcess() {
|
||||
if (user.getProtocolVersion().compareTo(MINECRAFT_1_8) < 0) {
|
||||
// 1.7 players don't have KeepAlive packets in the login process
|
||||
sendJoinGamePacket();
|
||||
} else {
|
||||
// Set the state to KEEP_ALIVE to avoid false positives
|
||||
user.setState(KEEP_ALIVE);
|
||||
// Generate a random KeepAlive ID for the pre-join check
|
||||
expectedKeepAliveId = RANDOM.nextInt();
|
||||
// Send first KeepAlive to check if the connection is somewhat responsive
|
||||
user.write(new KeepAlivePacket(expectedKeepAliveId));
|
||||
}
|
||||
}
|
||||
|
||||
private void sendTransaction() {
|
||||
// Set the state to TRANSACTION to avoid false positives
|
||||
// and go on with the flow of the verification.
|
||||
user.setState(TRANSACTION);
|
||||
// Generate a random transaction ID
|
||||
expectedTransactionId = (short) RANDOM.nextInt();
|
||||
// Send a transaction with the given ID
|
||||
user.write(new TransactionPacket(0, expectedTransactionId, false));
|
||||
}
|
||||
|
||||
private void sendJoinGamePacket() {
|
||||
final boolean v1_20_2 = user.getProtocolVersion().compareTo(MINECRAFT_1_20_2) >= 0;
|
||||
if (!v1_20_2) {
|
||||
// Set the state to CLIENT_SETTINGS to avoid false positives
|
||||
// (1.20.2 needs the LOGIN_ACK state) and go on with
|
||||
// the flow of the verification.
|
||||
user.setState(CLIENT_SETTINGS);
|
||||
}
|
||||
// Send the JoinGame packet so we can continue the verification
|
||||
user.write(joinGame);
|
||||
if (v1_20_2) {
|
||||
// Perform the transaction check since the ClientSettings
|
||||
// and PluginMessage packets have already been validated.
|
||||
sendTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
private void sendAbilitiesAndTeleport() {
|
||||
// Set the state to TELEPORT to avoid false positives
|
||||
// and go on with the flow of the verification.
|
||||
user.setState(TELEPORT);
|
||||
// Make sure the player is unable to fly (e.g. if the player is in creative mode)
|
||||
if (Sonar.get().getConfig().getVerification().getGravity().getGamemode() == CREATIVE) {
|
||||
user.delayedWrite(DEFAULT_ABILITIES);
|
||||
}
|
||||
// Generate the current teleport ID
|
||||
expectedTeleportId = RANDOM.nextInt();
|
||||
// Add a little randomization to the spawn y coordinate
|
||||
spawnYPosition = dynamicSpawnYPosition + RANDOM.nextDouble(0.4);
|
||||
// Teleport the player to the spawn position
|
||||
user.delayedWrite(new PlayerPositionLookPacket(
|
||||
SPAWN_X_POSITION, spawnYPosition, SPAWN_Z_POSITION,
|
||||
0f, -90f, expectedTeleportId, false));
|
||||
// Make sure the player escapes the 1.18.2+ "Loading terrain" screen
|
||||
if (user.getProtocolVersion().compareTo(MINECRAFT_1_18_2) >= 0) {
|
||||
user.delayedWrite(dynamicSpawnPosition);
|
||||
}
|
||||
}
|
||||
|
||||
private void sendChunkData() {
|
||||
// If we don't have any checks, simply finish verification
|
||||
// This is *hopefully* a very rare case though...
|
||||
if (!performGravity && !performCaptcha && !performVehicle) {
|
||||
// Save some work by finishing before sending even more packets
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
// Set the state to POSITION to avoid false positives
|
||||
// and go on with the flow of the verification.
|
||||
user.setState(POSITION);
|
||||
// 1.20.3 packets introduced game events
|
||||
if (user.getProtocolVersion().compareTo(MINECRAFT_1_20_3) >= 0) {
|
||||
// Make sure the client knows that we're sending chunks next
|
||||
user.delayedWrite(START_WRITING_CHUNKS);
|
||||
}
|
||||
// Teleport player into the fake lobby by sending an empty chunk
|
||||
user.delayedWrite(EMPTY_CHUNK_DATA);
|
||||
// Checking gravity is disabled, just finish verification
|
||||
if (!performGravity) {
|
||||
// Switch to captcha state if needed
|
||||
vehicleCheckOrNext();
|
||||
return;
|
||||
}
|
||||
// Send an UpdateSectionBlocks packet with a platform of blocks
|
||||
// to check if the player collides with the solid platform.
|
||||
user.delayedWrite(updateSectionBlocks);
|
||||
// Send all packets in one flush
|
||||
user.getChannel().flush();
|
||||
}
|
||||
|
||||
private static boolean validateClientLocale(final @SuppressWarnings("unused") @NotNull FallbackUser user,
|
||||
final String locale) {
|
||||
// Check the client locale by performing a simple regex check on it
|
||||
final Pattern pattern = Sonar.get().getConfig().getVerification().getValidLocaleRegex();
|
||||
return pattern.matcher(locale).matches(); // Disallow non-ascii characters (by default)
|
||||
}
|
||||
|
||||
private static boolean validateClientBrand(final @NotNull FallbackUser user, final ByteBuf content) {
|
||||
// We have to catch every DecoderException, so we can fail and punish
|
||||
// the player instead of only disconnecting them due to an exception.
|
||||
try {
|
||||
// No need to check for empty brands since ProtocolUtil#readBrandMessage
|
||||
// already performs these checks by default.
|
||||
final String read = ProtocolUtil.readBrandMessage(content);
|
||||
// Check if the decoded client brand string is too long
|
||||
if (read.length() > Sonar.get().getConfig().getVerification().getBrand().getMaxLength()) {
|
||||
return false;
|
||||
}
|
||||
// Regex pattern for validating client brands
|
||||
final Pattern pattern = Sonar.get().getConfig().getVerification().getBrand().getValidRegex();
|
||||
return !read.equals("Vanilla") // The normal brand is always lowercase
|
||||
&& pattern.matcher(read).matches(); // Disallow non-ascii characters (by default)
|
||||
} catch (DecoderException exception) {
|
||||
// Fail if the string (client brand) could not be decoded properly
|
||||
user.fail("could not decode string");
|
||||
// Throw the exception so we don't continue checking
|
||||
throw exception;
|
||||
}
|
||||
}
|
||||
|
||||
private void handlePacketsCAPTCHA(final @NotNull FallbackPacket packet) {
|
||||
// Check if the player took too long to enter the captcha
|
||||
final int maxDuration = Sonar.get().getConfig().getVerification().getMap().getMaxDuration();
|
||||
checkFrame(!login.elapsed(maxDuration), "took too long to enter captcha");
|
||||
|
||||
// Handle incoming chat messages
|
||||
if (packet instanceof UniversalChatPacket) {
|
||||
final UniversalChatPacket chat = (UniversalChatPacket) packet;
|
||||
final String answer = Objects.requireNonNull(captcha).getAnswer();
|
||||
|
||||
if (chat.getMessage().equals(answer)) {
|
||||
// Captcha is correct
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
// Captcha is incorrect
|
||||
checkFrame(captchaTriesLeft-- > 0, "failed captcha too often");
|
||||
user.write(incorrectCaptcha);
|
||||
return;
|
||||
}
|
||||
|
||||
// Only expect idle (position) packets
|
||||
if (packet instanceof PlayerPositionPacket
|
||||
|| packet instanceof PlayerPositionLookPacket) {
|
||||
// A position packet is sent every second
|
||||
final long difference = maxDuration - login.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]);
|
||||
// Send a KeepAlive packet to prevent timeout
|
||||
user.write(CAPTCHA_KEEP_ALIVE);
|
||||
}
|
||||
lastCountdownIndex = index;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(final @NotNull FallbackPacket packet) {
|
||||
// The player has already been verified or failed the verification -> drop packet
|
||||
if (!user.getState().canReceivePackets()) return;
|
||||
|
||||
// Calculate maximum amount of packets sent by the client to the server
|
||||
final int maxPackets = Sonar.get().getConfig().getVerification().getMaxLoginPackets()
|
||||
+ maxMovementTick // Account for positions/fall check
|
||||
+ Sonar.get().getConfig().getVerification().getMap().getMaxDuration()
|
||||
+ Sonar.get().getConfig().getVerification().getMap().getMaxTries();
|
||||
checkFrame(++totalReceivedPackets < maxPackets, "too many packets");
|
||||
|
||||
// We are expecting a code in chat, drop all other packets
|
||||
if (user.getState() == CAPTCHA) {
|
||||
handlePacketsCAPTCHA(packet);
|
||||
return;
|
||||
}
|
||||
|
||||
if (packet instanceof PaddleBoatPacket) {
|
||||
// Check if we are currently expecting a PaddleBoatPacket packet
|
||||
assertState(VEHICLE);
|
||||
|
||||
// Only pass the player if both checks are passed
|
||||
if (receivedPlayerInput && receivedSteerBoat) {
|
||||
captchaOrNext();
|
||||
}
|
||||
|
||||
// Mark paddle boat check as passed
|
||||
receivedSteerBoat = true;
|
||||
}
|
||||
|
||||
if (packet instanceof PlayerInputPacket) {
|
||||
// Check if we are currently expecting a PlayerInputPacket packet
|
||||
assertState(VEHICLE);
|
||||
|
||||
final PlayerInputPacket inputPacket = (PlayerInputPacket) packet;
|
||||
|
||||
// Make sure the values sent by the client are legitimate
|
||||
checkFrame(inputPacket.getForward() <= 0.98f, "invalid vehicle speed (f)");
|
||||
checkFrame(inputPacket.getSideways() <= 0.98f, "invalid vehicle speed (s)");
|
||||
|
||||
// Don't verify if the player tries unmounting or jumping
|
||||
checkFrame(!inputPacket.isJump(), "tried to jump in vehicle");
|
||||
checkFrame(!inputPacket.isUnmount(), "tried to escape the vehicle");
|
||||
|
||||
// Only pass the player if both checks are passed
|
||||
if (receivedPlayerInput && receivedSteerBoat) {
|
||||
captchaOrNext();
|
||||
}
|
||||
|
||||
// pre-1.9 clients do not have a PaddleBoat packet
|
||||
if (user.getProtocolVersion().compareTo(MINECRAFT_1_9) < 0) {
|
||||
receivedSteerBoat = true;
|
||||
} else if (!user.isGeyser()) {
|
||||
// PaddleBoat → PlayerInput → PaddleBoat
|
||||
checkFrame(receivedSteerBoat, "invalid vehicle steering order");
|
||||
}
|
||||
// Mark player input check as passed
|
||||
receivedPlayerInput = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (packet instanceof LoginAcknowledgedPacket) {
|
||||
// Check if we are currently expecting a LoginAcknowledged packet
|
||||
assertState(LOGIN_ACK);
|
||||
|
||||
// Start the configuration process for 1.20.2 clients
|
||||
configure();
|
||||
return;
|
||||
}
|
||||
|
||||
if (packet instanceof FinishConfigurationPacket) {
|
||||
// Check if we are currently expecting a FinishConfiguration packet
|
||||
assertState(CONFIGURE);
|
||||
|
||||
// Check if the client has already sent valid ClientSettings and PluginMessage packets
|
||||
if (!user.isGeyser()) {
|
||||
checkFrame(resolvedClientBrand, "did not resolve client brand");
|
||||
checkFrame(resolvedClientSettings, "did not resolve client settings");
|
||||
}
|
||||
|
||||
// Start initializing the actual join process
|
||||
updateEncoderDecoderState(FallbackPacketRegistry.GAME);
|
||||
initialJoinProcess();
|
||||
return;
|
||||
}
|
||||
|
||||
if (packet instanceof KeepAlivePacket) {
|
||||
final KeepAlivePacket keepAlive = (KeepAlivePacket) packet;
|
||||
|
||||
// 1.7-1.8.9 are sending a KeepAlive packet with the ID 0 every 20 ticks
|
||||
if (keepAlive.getId() == 0 && user.getProtocolVersion().compareTo(MINECRAFT_1_8) <= 0) return;
|
||||
|
||||
// Check if we are currently expecting a KeepAlive packet
|
||||
assertState(KEEP_ALIVE);
|
||||
|
||||
checkFrame(keepAlive.getId() == expectedKeepAliveId, "invalid KeepAlive ID");
|
||||
|
||||
// The correct KeepAlive packet has been received
|
||||
sendJoinGamePacket();
|
||||
return;
|
||||
}
|
||||
|
||||
if (packet instanceof ClientSettingsPacket) {
|
||||
final ClientSettingsPacket clientSettings = (ClientSettingsPacket) packet;
|
||||
|
||||
// Validate the locale using a regex to filter unwanted characters.
|
||||
checkFrame(validateClientLocale(user, clientSettings.getLocale()), "invalid locale");
|
||||
|
||||
// Clients sometimes mess up the ClientSettings or PluginMessage packet.
|
||||
// Probably caused by some weird race condition in Minecraft 1.13+
|
||||
if (user.getState() == CLIENT_SETTINGS) {
|
||||
user.setState(PLUGIN_MESSAGE);
|
||||
}
|
||||
|
||||
// Make sure we mark the ClientSettings as valid
|
||||
resolvedClientSettings = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (packet instanceof PluginMessagePacket) {
|
||||
final PluginMessagePacket pluginMessage = (PluginMessagePacket) packet;
|
||||
|
||||
// Only the brand channel is important, drop the rest
|
||||
if (pluginMessage.getChannel().equals("MC|Brand")
|
||||
|| pluginMessage.getChannel().equals("minecraft:brand")) {
|
||||
// Check if the brand packet was sent twice,
|
||||
// which is not possible when using a vanilla Minecraft client.
|
||||
checkFrame(!resolvedClientBrand, "duplicate client brand packet");
|
||||
|
||||
// Check if the channel is correct - 1.13 uses the new namespace
|
||||
// system ('minecraft:' + channel) and anything below 1.13 uses
|
||||
// the legacy namespace system ('MC|' + channel).
|
||||
final boolean v1_13 = user.getProtocolVersion().compareTo(MINECRAFT_1_13) >= 0;
|
||||
checkFrame(pluginMessage.getChannel().equals("MC|Brand") || v1_13, "invalid channel");
|
||||
|
||||
// Validate the client branding using a regex to filter unwanted characters.
|
||||
if (Sonar.get().getConfig().getVerification().getBrand().isEnabled()) {
|
||||
checkFrame(validateClientBrand(user, pluginMessage.getSlicedBuffer()), "invalid client brand");
|
||||
}
|
||||
|
||||
// Clients sometimes mess up the ClientSettings or PluginMessage packet.
|
||||
if (user.getState() == PLUGIN_MESSAGE) {
|
||||
// Send the transaction packet
|
||||
sendTransaction();
|
||||
}
|
||||
|
||||
// Make sure we mark the PluginMessage as valid
|
||||
resolvedClientBrand = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (packet instanceof TransactionPacket) {
|
||||
// Check if we are currently expecting a Transaction packet
|
||||
assertState(TRANSACTION);
|
||||
|
||||
final TransactionPacket transaction = (TransactionPacket) packet;
|
||||
|
||||
// The transaction should always be accepted
|
||||
checkFrame(transaction.isAccepted(), "transaction not accepted?!");
|
||||
// Check if the transaction ID is valid
|
||||
checkFrame(transaction.getTransactionId() == expectedTransactionId, "invalid transaction id");
|
||||
|
||||
// First, send an Abilities packet to the client to make
|
||||
// sure the player falls even in spectator mode.
|
||||
// Then, teleport the player to the spawn position.
|
||||
sendAbilitiesAndTeleport();
|
||||
|
||||
// 1.7-1.8 clients do not have a TeleportConfirm packet
|
||||
if (user.getProtocolVersion().compareTo(MINECRAFT_1_8) <= 0) {
|
||||
// Immediately send the chunk data as 1.7-1.8 can't confirm teleport packets
|
||||
sendChunkData();
|
||||
} else {
|
||||
// Send all previously sent packets in one flush since we didn't flush
|
||||
// the channel earlier when we teleported the player.
|
||||
user.getChannel().flush();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (packet instanceof TeleportConfirmPacket) {
|
||||
// Check if we are currently expecting a TeleportConfirm packet
|
||||
assertState(TELEPORT);
|
||||
|
||||
// Check if the teleport ID is correct
|
||||
final TeleportConfirmPacket teleportConfirm = (TeleportConfirmPacket) packet;
|
||||
checkFrame(teleportConfirm.getTeleportId() == expectedTeleportId, "invalid teleport ID");
|
||||
|
||||
// Reset all values to ensure safety on teleport
|
||||
tick = 1;
|
||||
posY = lastY = expectedTeleportId = -1;
|
||||
|
||||
// Now we can send the chunk data
|
||||
sendChunkData();
|
||||
return;
|
||||
}
|
||||
|
||||
// Only handle position packets if we aren't in certain phases
|
||||
if (user.getState() != LOGIN_ACK && user.getState() != VEHICLE) {
|
||||
if (packet instanceof PlayerPositionPacket) {
|
||||
final PlayerPositionPacket position = (PlayerPositionPacket) packet;
|
||||
handlePositionUpdate(position.getX(), position.getY(), position.getZ(), position.isOnGround());
|
||||
return;
|
||||
}
|
||||
|
||||
if (packet instanceof PlayerPositionLookPacket) {
|
||||
final PlayerPositionLookPacket position = (PlayerPositionLookPacket) packet;
|
||||
handlePositionUpdate(position.getX(), position.getY(), position.getZ(), position.isOnGround());
|
||||
return;
|
||||
}
|
||||
|
||||
if (packet instanceof PlayerPositionIdlePacket) {
|
||||
final PlayerPositionIdlePacket player = (PlayerPositionIdlePacket) packet;
|
||||
handlePositionUpdate(posX, posY, posZ, player.isOnGround());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void handlePositionUpdate(final double x, final double y, final double z, final boolean ground) {
|
||||
try {
|
||||
// 1.8 does not have a TeleportConfirm packet, so we need to handle the spawning differently
|
||||
if (user.getProtocolVersion().compareTo(MINECRAFT_1_8) <= 0
|
||||
&& expectedTeleportId != -1 // Check if the teleport ID is currently unset
|
||||
// Then, check if the position is equal to the spawn position
|
||||
&& x == SPAWN_X_POSITION && y == spawnYPosition && z == SPAWN_Z_POSITION) {
|
||||
// Reset all values to ensure safety on teleport
|
||||
tick = 1;
|
||||
posY = expectedTeleportId = -1;
|
||||
// Check for ground state in the first packet
|
||||
checkFrame(!ground, "invalid ground state");
|
||||
}
|
||||
|
||||
posX = x;
|
||||
lastY = posY;
|
||||
posY = y;
|
||||
posZ = z;
|
||||
|
||||
// The player is not allowed to move away from the collision platform.
|
||||
// This should not happen unless the max movement tick is configured to a high number.
|
||||
checkFrame(Math.abs(x - BLOCKS_PER_ROW) < BLOCKS_PER_ROW, "moved too far (x)");
|
||||
checkFrame(Math.abs(z - BLOCKS_PER_ROW) < BLOCKS_PER_ROW, "moved too far (z)");
|
||||
|
||||
// Check if the client hasn't moved before sending the first movement packet
|
||||
if (!listenForMovements) {
|
||||
if (posX == SPAWN_X_POSITION && posZ == SPAWN_Z_POSITION) {
|
||||
// Now, once we verified the X and Z position, we can safely check for gravity
|
||||
listenForMovements = true;
|
||||
}
|
||||
|
||||
lastY = spawnYPosition;
|
||||
return;
|
||||
}
|
||||
|
||||
final double deltaY = lastY - y;
|
||||
// The deltaY is 0 whenever the player sends their first position packet.
|
||||
// We have to account for this or the player will fail the verification.
|
||||
if (deltaY == 0) return;
|
||||
|
||||
// Calculate the difference between the player's Y coordinate and the expected Y coordinate
|
||||
double collisionOffsetY = (DEFAULT_Y_COLLIDE_POSITION + blockType.getBlockHeight()) - y;
|
||||
if (user.getProtocolVersion().compareTo(MINECRAFT_1_8) < 0) {
|
||||
collisionOffsetY += 1.62f; // 1.7 is weird and sends the head position instead of the AABB minY
|
||||
}
|
||||
|
||||
// Check if the player is colliding by performing a basic Y offset check.
|
||||
// The offset cannot be greater than 0 since the blocks will not let the player fall through them.
|
||||
checkFrame(collisionOffsetY <= 0, "fell through blocks: " + collisionOffsetY);
|
||||
|
||||
if (tick > maxMovementTick) {
|
||||
// Log/debug position if enabled in the configuration
|
||||
if (Sonar.get().getConfig().getVerification().isDebugXYZPositions()) {
|
||||
FALLBACK.getLogger().info("{}: {}/{}/{} - deltaY: {} - ground: {} - collision: {}",
|
||||
username, x, y, z, deltaY, ground, collisionOffsetY);
|
||||
}
|
||||
|
||||
// If the collision check is disabled, finish verification
|
||||
if (!performCollisions) {
|
||||
vehicleCheckOrNext();
|
||||
return;
|
||||
}
|
||||
|
||||
// Perform the collision check
|
||||
if (ground) {
|
||||
// Make sure the player is actually colliding with the blocks and not only spoofing ground
|
||||
checkFrame(collisionOffsetY > -0.03, "illegal collision: " + collisionOffsetY);
|
||||
// The player is colliding to blocks, finish verification
|
||||
vehicleCheckOrNext();
|
||||
return;
|
||||
} else {
|
||||
// Make sure the player is colliding with blocks but is sending an invalid ground state
|
||||
checkFrame(collisionOffsetY != 0, "no ground: " + collisionOffsetY);
|
||||
}
|
||||
} else {
|
||||
// Check if the player is spoofing the ground state
|
||||
checkFrame(!ground, "spoofed ground state");
|
||||
}
|
||||
|
||||
// Make sure we don't run out of predicted Y motions
|
||||
checkFrame(tick < preparedCachedYMotions.length, "too many movements");
|
||||
|
||||
if (!ground && performGravity) {
|
||||
final double predictedY = preparedCachedYMotions[tick];
|
||||
final double offsetY = Math.abs(deltaY - predictedY);
|
||||
|
||||
// Log/debug position if enabled in the configuration
|
||||
if (Sonar.get().getConfig().getVerification().isDebugXYZPositions()) {
|
||||
FALLBACK.getLogger().info("{}: {}/{}/{} - deltaY: {} - prediction: {} - offset: {}",
|
||||
username, x, y, z, deltaY, predictedY, offsetY);
|
||||
}
|
||||
|
||||
// Check if the y motion is roughly equal to the predicted value
|
||||
checkFrame(offsetY < 5e-3, String.format("invalid gravity: %d, %.7f, %.10f, %.10f != %.10f",
|
||||
tick, y, offsetY, deltaY, predictedY));
|
||||
}
|
||||
tick++;
|
||||
} catch (CorruptedFrameException exception) {
|
||||
// Do not throw the exception if the user configured to display the CAPTCHA instead
|
||||
if (Sonar.get().getConfig().getVerification().getGravity().isCaptchaOnFail()) {
|
||||
handleCAPTCHA();
|
||||
return;
|
||||
}
|
||||
// Interrupt the netty eventLoop to immediately stop further processing
|
||||
throw new DecoderException(exception);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Maybe recode the flow with a better state system?
|
||||
private void vehicleCheckOrNext() {
|
||||
if (performVehicle) {
|
||||
// Initialize the vehicle check
|
||||
handleVehicleCheck();
|
||||
return;
|
||||
}
|
||||
captchaOrNext();
|
||||
}
|
||||
|
||||
private void captchaOrNext() {
|
||||
if (performCaptcha) {
|
||||
// Initialize the map captcha
|
||||
handleCAPTCHA();
|
||||
return;
|
||||
}
|
||||
// Finish the verification
|
||||
finish();
|
||||
}
|
||||
|
||||
private void handleVehicleCheck() {
|
||||
// Set the state to VEHICLE, so we don't handle any unnecessary packets
|
||||
user.setState(VEHICLE);
|
||||
// Send the necessary packets to mount the player on the vehicle
|
||||
final int vehicleType = EntityType.BOAT.getId(user.getProtocolVersion());
|
||||
user.delayedWrite(new SpawnEntityPacket(VEHICLE_ENTITY_ID, vehicleType, posX, posY, posZ));
|
||||
user.delayedWrite(setPassengers);
|
||||
user.getChannel().flush();
|
||||
}
|
||||
|
||||
private void handleCAPTCHA() {
|
||||
// Set the state to CAPTCHA, so we don't handle any unnecessary packets
|
||||
user.setState(CAPTCHA);
|
||||
if (!MAP_INFO_PREPARER.isCaptchaAvailable()) {
|
||||
// This should not happen, but we have to return if there is no captcha prepared
|
||||
user.disconnect(Sonar.get().getConfig().getVerification().getCurrentlyPreparing());
|
||||
return;
|
||||
}
|
||||
if (!performGravity
|
||||
&& user.getProtocolVersion().compareTo(MINECRAFT_1_18_2) >= 0) {
|
||||
// Make sure the player escapes the 1.18.2+ "Loading terrain" screen
|
||||
user.delayedWrite(CAPTCHA_SPAWN_POSITION);
|
||||
}
|
||||
if (performVehicle) {
|
||||
// Make sure the player exits the vehicle before sending the CAPTCHA
|
||||
user.delayedWrite(removeEntities);
|
||||
}
|
||||
// Reset max tries
|
||||
captchaTriesLeft = Sonar.get().getConfig().getVerification().getMap().getMaxTries();
|
||||
|
||||
// Set slot to map
|
||||
user.delayedWrite(new SetSlotPacket(36, 1,
|
||||
ItemType.FILLED_MAP.getId(user.getProtocolVersion()), SetSlotPacket.MAP_NBT));
|
||||
// Send random captcha to the player
|
||||
captcha = MAP_INFO_PREPARER.getRandomCaptcha();
|
||||
Objects.requireNonNull(captcha).delayedWrite(user);
|
||||
// Teleport the player to the position above the platform
|
||||
user.delayedWrite(CAPTCHA_POSITION);
|
||||
// Make sure the player cannot move
|
||||
user.delayedWrite(user.isGeyser() ? CAPTCHA_ABILITIES_BEDROCK : CAPTCHA_ABILITIES);
|
||||
// Make sure the player knows what to do
|
||||
user.delayedWrite(enterCodeMessage);
|
||||
// Send all packets in one flush
|
||||
user.getChannel().flush();
|
||||
}
|
||||
|
||||
private void finish() {
|
||||
// Make sure we drop all further packets
|
||||
user.setState(SUCCESS);
|
||||
|
||||
// Increment amount of total successful verifications
|
||||
GlobalSonarStatistics.totalSuccessfulVerifications++;
|
||||
|
||||
// Add verified player to the database
|
||||
final VerifiedPlayer verifiedPlayer = new VerifiedPlayer(user.getInetAddress().toString(), playerUuid,
|
||||
login.getStart());
|
||||
Sonar.get().getVerifiedPlayerController().add(verifiedPlayer);
|
||||
|
||||
// Call the VerifySuccessEvent for external API usage
|
||||
Sonar.get().getEventManager().publish(new UserVerifySuccessEvent(username, playerUuid, user, login.delay()));
|
||||
|
||||
// If enabled, transfer the player back to the origin server
|
||||
// This feature was introduced by Mojang in Minecraft version 1.20.5
|
||||
// Thanks a lot to @Kadeluxe for helping me out with this!
|
||||
if (transfer) {
|
||||
user.write(transferToOrigin);
|
||||
}
|
||||
|
||||
// Disconnect player with the verification success message
|
||||
user.disconnect(Sonar.get().getConfig().getVerification().getVerificationSuccess());
|
||||
|
||||
FALLBACK.getLogger().info(Sonar.get().getConfig().getVerification().getSuccessLog()
|
||||
.replace("%name%", username)
|
||||
.replace("%time%", login.toString()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Fails the verification if a certain state is unexpected.
|
||||
*
|
||||
* @param expectedState Expected state
|
||||
*/
|
||||
private void assertState(final @NotNull FallbackUserState expectedState) {
|
||||
final FallbackUserState state = user.getState();
|
||||
checkFrame(state == expectedState, "expected " + expectedState + ", got " + state);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a certain condition is met, fails the verification if not.
|
||||
*
|
||||
* @param condition Condition to fail if it's false
|
||||
* @param message Messages displayed in the stacktrace
|
||||
*/
|
||||
private void checkFrame(final boolean condition, final @NotNull String message) {
|
||||
if (!condition) {
|
||||
// Only fail the player if we are either not in the POSITION state,
|
||||
// or if the user configured Sonar to display a CAPTCHA instead of failing.
|
||||
if (user.getState() != POSITION
|
||||
|| !Sonar.get().getConfig().getVerification().getGravity().isCaptchaOnFail()) {
|
||||
user.fail(message);
|
||||
}
|
||||
throw new CorruptedFrameException(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
-3
@@ -20,10 +20,7 @@ package xyz.jonesdev.sonar.common.fallback.protocol;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public interface FallbackPacket {
|
||||
UUID EMPTY_UUID = new UUID(0L, 0L);
|
||||
|
||||
/**
|
||||
* Encodes the packet sent by the server
|
||||
|
||||
+19
-21
@@ -21,8 +21,9 @@ import io.netty.buffer.ByteBuf;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.channel.ChannelInboundHandlerAdapter;
|
||||
import io.netty.handler.codec.CorruptedFrameException;
|
||||
import lombok.Setter;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_20_2;
|
||||
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry.Direction.SERVERBOUND;
|
||||
@@ -31,19 +32,18 @@ import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry
|
||||
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.readVarInt;
|
||||
|
||||
public final class FallbackPacketDecoder extends ChannelInboundHandlerAdapter {
|
||||
private final FallbackUser user;
|
||||
private final FallbackPacketListener listener;
|
||||
private final ProtocolVersion protocolVersion;
|
||||
private FallbackPacketRegistry.ProtocolRegistry registry;
|
||||
@Setter
|
||||
private FallbackPacketListener listener;
|
||||
|
||||
public FallbackPacketDecoder(final @NotNull FallbackUser user,
|
||||
final @NotNull FallbackPacketListener listener) {
|
||||
this.user = user;
|
||||
this.listener = listener;
|
||||
updateRegistry(user.getProtocolVersion().compareTo(MINECRAFT_1_20_2) >= 0 ? LOGIN : GAME);
|
||||
public FallbackPacketDecoder(final @NotNull ProtocolVersion protocolVersion) {
|
||||
this.protocolVersion = protocolVersion;
|
||||
updateRegistry(protocolVersion.compareTo(MINECRAFT_1_20_2) >= 0 ? LOGIN : GAME);
|
||||
}
|
||||
|
||||
public void updateRegistry(final @NotNull FallbackPacketRegistry registry) {
|
||||
this.registry = registry.getProtocolRegistry(SERVERBOUND, user.getProtocolVersion());
|
||||
this.registry = registry.getProtocolRegistry(SERVERBOUND, protocolVersion);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -73,24 +73,24 @@ public final class FallbackPacketDecoder extends ChannelInboundHandlerAdapter {
|
||||
|
||||
try {
|
||||
// Ensure that the packet isn't too large or too small
|
||||
doLengthSanityChecks(byteBuf, packet);
|
||||
checkPacketSize(byteBuf, packet);
|
||||
|
||||
try {
|
||||
// Try to decode the packet for the given protocol version
|
||||
packet.decode(byteBuf, user.getProtocolVersion());
|
||||
packet.decode(byteBuf, protocolVersion);
|
||||
} catch (Throwable throwable) {
|
||||
user.fail("failed to decode packet (" + byteBuf.readableBytes() + " bytes)");
|
||||
throw new CorruptedFrameException("Failed to decode packet");
|
||||
}
|
||||
|
||||
// Check if the packet still has bytes left after we decoded it
|
||||
if (byteBuf.isReadable()) {
|
||||
user.fail("could not read packet to end (" + byteBuf.readableBytes() + " bytes left)");
|
||||
throw new CorruptedFrameException("Could not read packet to end");
|
||||
}
|
||||
|
||||
// Let our verification handler process the packet
|
||||
listener.handle(packet);
|
||||
if (listener != null) {
|
||||
listener.handle(packet);
|
||||
}
|
||||
|
||||
// Fire channel read to avoid timeout
|
||||
ctx.fireChannelRead(packet);
|
||||
@@ -101,17 +101,15 @@ public final class FallbackPacketDecoder extends ChannelInboundHandlerAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
private void doLengthSanityChecks(final @NotNull ByteBuf byteBuf,
|
||||
final @NotNull FallbackPacket packet) throws Exception {
|
||||
final int expectedMaxLen = packet.expectedMaxLength(byteBuf, user.getProtocolVersion());
|
||||
private void checkPacketSize(final @NotNull ByteBuf byteBuf,
|
||||
final @NotNull FallbackPacket packet) throws Exception {
|
||||
final int expectedMaxLen = packet.expectedMaxLength(byteBuf, protocolVersion);
|
||||
if (expectedMaxLen != -1 && byteBuf.readableBytes() > expectedMaxLen) {
|
||||
user.fail("packet too large (" + byteBuf.readableBytes() + " bytes)");
|
||||
throw new CorruptedFrameException("Packet too large");
|
||||
}
|
||||
|
||||
final int expectedMinLen = packet.expectedMinLength(byteBuf, user.getProtocolVersion());
|
||||
if (byteBuf.readableBytes() < expectedMinLen) {
|
||||
user.fail("packet too small (" + byteBuf.readableBytes() + " bytes)");
|
||||
final int expectedMinLen = packet.expectedMinLength(byteBuf, protocolVersion);
|
||||
if (expectedMinLen != -1 && byteBuf.readableBytes() < expectedMinLen) {
|
||||
throw new CorruptedFrameException("Packet too small");
|
||||
}
|
||||
}
|
||||
|
||||
+36
-25
@@ -19,11 +19,13 @@ package xyz.jonesdev.sonar.common.fallback.protocol;
|
||||
|
||||
import io.netty.util.collection.IntObjectHashMap;
|
||||
import io.netty.util.collection.IntObjectMap;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Data;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.config.FinishConfigurationPacket;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.config.RegistryDataPacket;
|
||||
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.LoginAcknowledgedPacket;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.login.LoginSuccessPacket;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.*;
|
||||
@@ -38,14 +40,13 @@ import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
|
||||
public enum FallbackPacketRegistry {
|
||||
LOGIN {
|
||||
{
|
||||
serverbound.register(LoginAcknowledgedPacket.class, LoginAcknowledgedPacket::new,
|
||||
map(0x03, MINECRAFT_1_20_2, false));
|
||||
|
||||
clientbound.register(LoginSuccessPacket.class, LoginSuccessPacket::new,
|
||||
map(0x02, MINECRAFT_1_7_2, false));
|
||||
|
||||
clientbound.register(DisconnectPacket.class, DisconnectPacket::new,
|
||||
map(0x00, MINECRAFT_1_7_2, true));
|
||||
clientbound.register(LoginSuccessPacket.class, LoginSuccessPacket::new,
|
||||
map(0x02, MINECRAFT_1_7_2, true));
|
||||
|
||||
serverbound.register(LoginAcknowledgedPacket.class, LoginAcknowledgedPacket::new,
|
||||
map(0x03, MINECRAFT_1_20_2, false));
|
||||
}
|
||||
},
|
||||
CONFIG {
|
||||
@@ -59,7 +60,6 @@ public enum FallbackPacketRegistry {
|
||||
clientbound.register(KeepAlivePacket.class, KeepAlivePacket::new,
|
||||
map(0x03, MINECRAFT_1_20_2, false),
|
||||
map(0x04, MINECRAFT_1_20_5, false));
|
||||
|
||||
clientbound.register(RegistryDataPacket.class, RegistryDataPacket::new,
|
||||
map(0x05, MINECRAFT_1_20_2, true),
|
||||
map(0x07, MINECRAFT_1_20_5, true));
|
||||
@@ -208,6 +208,22 @@ public enum FallbackPacketRegistry {
|
||||
map(0x0E, MINECRAFT_1_7_2, true),
|
||||
map(0x00, MINECRAFT_1_9, true),
|
||||
map(0x01, MINECRAFT_1_19_4, true));
|
||||
clientbound.register(PlayerInfoPacket.class, PlayerInfoPacket::new,
|
||||
map(0x38, MINECRAFT_1_7_2, true),
|
||||
map(0x2D, MINECRAFT_1_9, true),
|
||||
map(0x2E, MINECRAFT_1_12_1, true),
|
||||
map(0x30, MINECRAFT_1_13, true),
|
||||
map(0x33, MINECRAFT_1_14, true),
|
||||
map(0x34, MINECRAFT_1_15, true),
|
||||
map(0x33, MINECRAFT_1_16, true),
|
||||
map(0x32, MINECRAFT_1_16_2, true),
|
||||
map(0x36, MINECRAFT_1_17, true),
|
||||
map(0x34, MINECRAFT_1_19, true),
|
||||
map(0x37, MINECRAFT_1_19_1, true),
|
||||
map(0x36, MINECRAFT_1_19_3, true),
|
||||
map(0x3A, MINECRAFT_1_19_4, true),
|
||||
map(0x3C, MINECRAFT_1_20_2, true),
|
||||
map(0x3E, MINECRAFT_1_20_5, true));
|
||||
clientbound.register(UpdateSectionBlocksPacket.class, UpdateSectionBlocksPacket::new,
|
||||
map(0x22, MINECRAFT_1_7_2, true),
|
||||
map(0x10, MINECRAFT_1_9, true),
|
||||
@@ -511,7 +527,7 @@ public enum FallbackPacketRegistry {
|
||||
final PacketMapping next = (i + 1 < mappings.length) ? mappings[i + 1] : current;
|
||||
|
||||
final ProtocolVersion from = current.protocolVersion;
|
||||
final ProtocolVersion to = getProtocolVersion(current, next, from);
|
||||
final ProtocolVersion to = getProtocolVersion(current, next);
|
||||
|
||||
for (final ProtocolVersion protocol : EnumSet.range(from, to)) {
|
||||
if (protocol == to && next != current) {
|
||||
@@ -520,19 +536,18 @@ public enum FallbackPacketRegistry {
|
||||
|
||||
final ProtocolRegistry registry = versions.get(protocol);
|
||||
if (registry == null) {
|
||||
throw new IllegalArgumentException("Unknown protocol version "
|
||||
+ current.protocolVersion);
|
||||
throw new IllegalArgumentException("Unknown protocol version " + from);
|
||||
}
|
||||
|
||||
if (registry.packetIdToSupplier.containsKey(current.id)) {
|
||||
throw new IllegalArgumentException("Can not register class " + clazz.getSimpleName()
|
||||
+ " with id " + current.id + " for " + registry.version
|
||||
+ " with id " + current.id + " for " + registry.protocolVersion
|
||||
+ " because another packet is already registered");
|
||||
}
|
||||
|
||||
if (registry.packetClassToId.containsKey(clazz)) {
|
||||
throw new IllegalArgumentException(clazz.getSimpleName()
|
||||
+ " is already registered for version " + registry.version);
|
||||
+ " is already registered for version " + registry.protocolVersion);
|
||||
}
|
||||
|
||||
if (!current.encodeOnly) {
|
||||
@@ -545,23 +560,19 @@ public enum FallbackPacketRegistry {
|
||||
|
||||
@NotNull
|
||||
private static ProtocolVersion getProtocolVersion(final @NotNull PacketMapping current,
|
||||
final @NotNull PacketMapping next,
|
||||
final @NotNull ProtocolVersion from) {
|
||||
final @NotNull PacketMapping next) {
|
||||
return current == next ? LATEST_VERSION : next.protocolVersion;
|
||||
}
|
||||
}
|
||||
|
||||
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public static class ProtocolRegistry {
|
||||
private final ProtocolVersion version;
|
||||
private final ProtocolVersion protocolVersion;
|
||||
private final IntObjectMap<Supplier<? extends FallbackPacket>> packetIdToSupplier =
|
||||
new IntObjectHashMap<>(16, 0.5f);
|
||||
private final Map<Class<? extends FallbackPacket>, Integer> packetClassToId =
|
||||
new HashMap<>(16, 0.5f);
|
||||
|
||||
ProtocolRegistry(final ProtocolVersion version) {
|
||||
this.version = version;
|
||||
}
|
||||
|
||||
public FallbackPacket createPacket(final int id) {
|
||||
final Supplier<? extends FallbackPacket> supplier = packetIdToSupplier.get(id);
|
||||
|
||||
@@ -589,16 +600,16 @@ public enum FallbackPacketRegistry {
|
||||
|
||||
PacketMapping(final int id,
|
||||
final ProtocolVersion protocolVersion,
|
||||
final boolean packetDecoding) {
|
||||
final boolean encodeOnly) {
|
||||
this.id = id;
|
||||
this.protocolVersion = protocolVersion;
|
||||
this.encodeOnly = packetDecoding;
|
||||
this.encodeOnly = encodeOnly;
|
||||
}
|
||||
}
|
||||
|
||||
private static @NotNull PacketMapping map(final int id,
|
||||
final ProtocolVersion version,
|
||||
final ProtocolVersion protocolVersion,
|
||||
final boolean encodeOnly) {
|
||||
return new PacketMapping(id, version, encodeOnly);
|
||||
return new PacketMapping(id, protocolVersion, encodeOnly);
|
||||
}
|
||||
}
|
||||
+20
-12
@@ -24,8 +24,8 @@ 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.packets.config.FinishConfigurationPacket;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.config.RegistryDataPacket;
|
||||
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.*;
|
||||
|
||||
@@ -60,10 +60,16 @@ public class FallbackPreparer {
|
||||
public FallbackPacket joinGame;
|
||||
// Update Section Blocks
|
||||
public FallbackPacket updateSectionBlocks;
|
||||
// Player Info
|
||||
public final FallbackPacket PLAYER_INFO = new FallbackPacketSnapshot(new PlayerInfoPacket(
|
||||
"", new UUID(1L, 1L), 2));
|
||||
// Default Spawn Position
|
||||
public FallbackPacket dynamicSpawnPosition;
|
||||
public FallbackPacket defaultSpawnPosition;
|
||||
// Spawn Position
|
||||
public final int TELEPORT_ID = ThreadLocalRandom.current().nextInt();
|
||||
public FallbackPacket spawnPosition;
|
||||
// Transfer packet
|
||||
public FallbackPacket transferToOrigin;
|
||||
public static FallbackPacket transferToOrigin;
|
||||
|
||||
// Disconnect messages
|
||||
public FallbackPacket blacklisted;
|
||||
@@ -76,7 +82,7 @@ public class FallbackPreparer {
|
||||
|
||||
// Vehicle
|
||||
public FallbackPacket removeEntities;
|
||||
public FallbackPacket setPassengers;
|
||||
public static FallbackPacket setPassengers;
|
||||
public static final int VEHICLE_ENTITY_ID = PLAYER_ENTITY_ID + 1;
|
||||
|
||||
// Collisions
|
||||
@@ -91,10 +97,8 @@ public class FallbackPreparer {
|
||||
public final FallbackPacket CAPTCHA_SPAWN_POSITION = new FallbackPacketSnapshot(new DefaultSpawnPositionPacket(
|
||||
SPAWN_X_POSITION, 1337, SPAWN_Z_POSITION));
|
||||
|
||||
// Blocks
|
||||
private final BlockUpdate[] CHANGED_BLOCKS = new BlockUpdate[BLOCKS_PER_ROW * BLOCKS_PER_ROW];
|
||||
// Platform
|
||||
public BlockType blockType = BlockType.BARRIER;
|
||||
|
||||
public int maxMovementTick, dynamicSpawnYPosition;
|
||||
public double[] preparedCachedYMotions;
|
||||
public double maxFallDistance;
|
||||
@@ -126,10 +130,15 @@ public class FallbackPreparer {
|
||||
|
||||
// Set the dynamic block and collide Y position based on the maximum fall distance
|
||||
dynamicSpawnYPosition = DEFAULT_Y_COLLIDE_POSITION + (int) Math.ceil(maxFallDistance);
|
||||
dynamicSpawnPosition = new FallbackPacketSnapshot(new DefaultSpawnPositionPacket(
|
||||
defaultSpawnPosition = new FallbackPacketSnapshot(new DefaultSpawnPositionPacket(
|
||||
SPAWN_X_POSITION, dynamicSpawnYPosition, SPAWN_Z_POSITION));
|
||||
spawnPosition = new FallbackPacketSnapshot(new PlayerPositionLookPacket(
|
||||
SPAWN_X_POSITION, dynamicSpawnYPosition, SPAWN_Z_POSITION,
|
||||
0, -90, TELEPORT_ID, false));
|
||||
|
||||
// Prepare collision platform positions
|
||||
final BlockUpdate[] changedBlocks = new BlockUpdate[BLOCKS_PER_ROW * BLOCKS_PER_ROW];
|
||||
|
||||
int index = 0;
|
||||
for (int x = 0; x < BLOCKS_PER_ROW; x++) {
|
||||
for (int z = 0; z < BLOCKS_PER_ROW; z++) {
|
||||
@@ -138,12 +147,11 @@ public class FallbackPreparer {
|
||||
DEFAULT_Y_COLLIDE_POSITION,
|
||||
z + (BLOCKS_PER_ROW / 2),
|
||||
0, 0);
|
||||
CHANGED_BLOCKS[index++] = new BlockUpdate(position, blockType);
|
||||
changedBlocks[index++] = new BlockUpdate(position, blockType);
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare UpdateSectionBlocks packet
|
||||
updateSectionBlocks = new FallbackPacketSnapshot(new UpdateSectionBlocksPacket(0, 0, CHANGED_BLOCKS));
|
||||
updateSectionBlocks = new FallbackPacketSnapshot(new UpdateSectionBlocksPacket(0, 0, changedBlocks));
|
||||
|
||||
// Prepare disconnect packets during login
|
||||
blacklisted = new FallbackPacketSnapshot(DisconnectPacket.create(Sonar.get().getConfig().getVerification().getBlacklisted(), true));
|
||||
|
||||
+26
-28
@@ -65,36 +65,34 @@ public final class CaptchaPreparer {
|
||||
final SimpleCaptchaGenerator generator = new SimpleCaptchaGenerator(128, 128, backgroundImage);
|
||||
final char[] dictionary = config.getDictionary().toCharArray();
|
||||
|
||||
final SimpleScratchFilter scratchFilter = new SimpleScratchFilter(5);
|
||||
final BumpFilter bumpFilter = new BumpFilter();
|
||||
// Prepare the filters for the CAPTCHA
|
||||
final List<AbstractBufferedImageOp> filters = new ArrayList<>();
|
||||
if (Sonar.get().getConfig().getVerification().getMap().isScratches()) {
|
||||
filters.add(new SimpleScratchFilter(5));
|
||||
}
|
||||
if (Sonar.get().getConfig().getVerification().getMap().isRipple()) {
|
||||
final SimpleRippleFilter rippleFilter = new SimpleRippleFilter();
|
||||
rippleFilter.setXAmplitude(0);
|
||||
float yAmplitude = 10 - ThreadLocalRandom.current().nextInt(20);
|
||||
if (Math.abs(yAmplitude) < 3) {
|
||||
yAmplitude = yAmplitude >= 0 ? 3 : -3;
|
||||
}
|
||||
rippleFilter.setYAmplitude(yAmplitude);
|
||||
filters.add(rippleFilter);
|
||||
}
|
||||
if (Sonar.get().getConfig().getVerification().getMap().getDistortion().isEnabled()) {
|
||||
final SmearFilter smearFilter = new SmearFilter();
|
||||
smearFilter.setShape(Sonar.get().getConfig().getVerification().getMap().getDistortion().getShape());
|
||||
smearFilter.setMix(Sonar.get().getConfig().getVerification().getMap().getDistortion().getMix());
|
||||
smearFilter.setDensity(Sonar.get().getConfig().getVerification().getMap().getDistortion().getDensity());
|
||||
smearFilter.setDistance(Sonar.get().getConfig().getVerification().getMap().getDistortion().getDistance());
|
||||
filters.add(smearFilter);
|
||||
}
|
||||
if (Sonar.get().getConfig().getVerification().getMap().isBump()) {
|
||||
filters.add(new BumpFilter());
|
||||
}
|
||||
|
||||
for (preparedAmount = 0; preparedAmount < config.getPrecomputeAmount(); preparedAmount++) {
|
||||
// Prepare the filters per CAPTCHA for some more randomness
|
||||
final List<AbstractBufferedImageOp> filters = new ArrayList<>();
|
||||
if (Sonar.get().getConfig().getVerification().getMap().isScratches()) {
|
||||
filters.add(scratchFilter);
|
||||
}
|
||||
if (Sonar.get().getConfig().getVerification().getMap().isRipple()) {
|
||||
final SimpleRippleFilter rippleFilter = new SimpleRippleFilter();
|
||||
rippleFilter.setXAmplitude(0);
|
||||
float yAmplitude = 10 - ThreadLocalRandom.current().nextInt(20);
|
||||
if (Math.abs(yAmplitude) < 3) {
|
||||
yAmplitude = yAmplitude >= 0 ? 3 : -3;
|
||||
}
|
||||
rippleFilter.setYAmplitude(yAmplitude);
|
||||
filters.add(rippleFilter);
|
||||
}
|
||||
if (Sonar.get().getConfig().getVerification().getMap().getDistortion().isEnabled()) {
|
||||
final SmearFilter smearFilter = new SmearFilter();
|
||||
smearFilter.setShape(Sonar.get().getConfig().getVerification().getMap().getDistortion().getShape());
|
||||
smearFilter.setMix(Sonar.get().getConfig().getVerification().getMap().getDistortion().getMix());
|
||||
smearFilter.setDensity(Sonar.get().getConfig().getVerification().getMap().getDistortion().getDensity());
|
||||
smearFilter.setDistance(Sonar.get().getConfig().getVerification().getMap().getDistortion().getDistance());
|
||||
filters.add(smearFilter);
|
||||
}
|
||||
if (Sonar.get().getConfig().getVerification().getMap().isBump()) {
|
||||
filters.add(bumpFilter);
|
||||
}
|
||||
if (!Sonar.get().getConfig().getVerification().getMap().isAutoColor()) {
|
||||
// Generate a random gradient color if automatic coloring is disabled
|
||||
final Color color0 = Color.getHSBColor((float) Math.random(), 1, 1);
|
||||
|
||||
+7
-7
@@ -21,7 +21,6 @@ import lombok.Getter;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketSnapshot;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.MapDataPacket;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_8;
|
||||
@@ -44,21 +43,22 @@ public final class MapCaptchaInfo {
|
||||
}
|
||||
this.legacy = new FallbackPacket[grid.length];
|
||||
for (int i = 0; i < grid.length; i++) {
|
||||
this.legacy[i] = new FallbackPacketSnapshot(new MapDataPacket(grid[i], i, 0));
|
||||
this.legacy[i] = new MapDataPacket(grid[i], i, 0);
|
||||
}
|
||||
|
||||
// Prepare 1.8+ map data
|
||||
this.modern = new FallbackPacketSnapshot(new MapDataPacket(buffer, 0, 0));
|
||||
this.modern = new MapDataPacket(buffer, 0, 0);
|
||||
}
|
||||
|
||||
public void delayedWrite(final @NotNull FallbackUser user) {
|
||||
if (user.getProtocolVersion().compareTo(MINECRAFT_1_8) >= 0) {
|
||||
user.delayedWrite(modern);
|
||||
} else {
|
||||
// 1.7.2-1.7.10
|
||||
if (user.getProtocolVersion().compareTo(MINECRAFT_1_8) < 0) {
|
||||
// 1.7.2-1.7.10 needs separate packets for each axis
|
||||
for (final FallbackPacket legacyPacket : legacy) {
|
||||
user.delayedWrite(legacyPacket);
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Send modern packet
|
||||
user.delayedWrite(modern);
|
||||
}
|
||||
}
|
||||
+2
-3
@@ -15,14 +15,12 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.common.fallback.protocol.packets.config;
|
||||
package xyz.jonesdev.sonar.common.fallback.protocol.packets.configuration;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import lombok.ToString;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
|
||||
|
||||
@ToString
|
||||
public final class FinishConfigurationPacket implements FallbackPacket {
|
||||
|
||||
@Override
|
||||
@@ -35,6 +33,7 @@ public final class FinishConfigurationPacket implements FallbackPacket {
|
||||
|
||||
@Override
|
||||
public int expectedMaxLength(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
|
||||
// This packet is always empty; see https://wiki.vg/Protocol#Finish_Configuration
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -15,7 +15,7 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.common.fallback.protocol.packets.config;
|
||||
package xyz.jonesdev.sonar.common.fallback.protocol.packets.configuration;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import lombok.*;
|
||||
@@ -41,7 +41,7 @@ public final class RegistryDataPacket implements FallbackPacket {
|
||||
private List<RegistryDataPacket.Bundle> bundles;
|
||||
|
||||
@Override
|
||||
public void encode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
|
||||
public void encode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) throws Exception {
|
||||
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_20_5) < 0) {
|
||||
writeNamelessCompoundTag(byteBuf, tag);
|
||||
} else if (type != null) {
|
||||
+1
-1
@@ -34,7 +34,7 @@ public final class LoginAcknowledgedPacket implements FallbackPacket {
|
||||
|
||||
@Override
|
||||
public int expectedMaxLength(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
|
||||
// https://github.com/PaperMC/Velocity/blob/dev/3.0.0/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/LoginAcknowledgedPacket.java#L41
|
||||
// This packet is always empty; see https://wiki.vg/Protocol#Login_Acknowledged
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -41,7 +41,7 @@ public final class LoginSuccessPacket implements FallbackPacket {
|
||||
private String username;
|
||||
|
||||
@Override
|
||||
public void encode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
|
||||
public void encode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) throws Exception {
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_19) >= 0) {
|
||||
writeUUID(byteBuf, uuid);
|
||||
} else if (protocolVersion.compareTo(MINECRAFT_1_16) >= 0) {
|
||||
|
||||
+1
-1
@@ -66,7 +66,7 @@ public final class ChunkDataPacket implements FallbackPacket {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void encode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
|
||||
public void encode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) throws Exception {
|
||||
byteBuf.writeInt(x);
|
||||
byteBuf.writeInt(z);
|
||||
|
||||
|
||||
+1
-1
@@ -35,7 +35,7 @@ public final class ClientAbilitiesPacket implements FallbackPacket {
|
||||
private float flySpeed, walkSpeed;
|
||||
|
||||
@Override
|
||||
public void encode(final @NotNull ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
|
||||
public void encode(final @NotNull ByteBuf byteBuf, final ProtocolVersion protocolVersion) throws Exception {
|
||||
byteBuf.writeByte(encodedFlags);
|
||||
byteBuf.writeFloat(flySpeed);
|
||||
byteBuf.writeFloat(walkSpeed);
|
||||
|
||||
+1
-1
@@ -51,7 +51,7 @@ public final class ClientSettingsPacket implements FallbackPacket {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
|
||||
public void decode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) throws Exception {
|
||||
locale = readString(byteBuf, 16);
|
||||
viewDistance = byteBuf.readByte();
|
||||
chatVisibility = readVarInt(byteBuf);
|
||||
|
||||
+1
-1
@@ -37,7 +37,7 @@ public final class DefaultSpawnPositionPacket implements FallbackPacket {
|
||||
private int x, y, z;
|
||||
|
||||
@Override
|
||||
public void encode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
|
||||
public void encode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) throws Exception {
|
||||
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_8) < 0) {
|
||||
byteBuf.writeInt(x);
|
||||
byteBuf.writeInt(y);
|
||||
|
||||
+3
-4
@@ -37,7 +37,7 @@ public final class DisconnectPacket implements FallbackPacket {
|
||||
private boolean login;
|
||||
|
||||
@Override
|
||||
public void encode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
|
||||
public void encode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) throws Exception {
|
||||
componentHolder.write(byteBuf, login ? ProtocolVersion.MINECRAFT_1_20_2 : protocolVersion);
|
||||
}
|
||||
|
||||
@@ -46,8 +46,7 @@ public final class DisconnectPacket implements FallbackPacket {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
public static @NotNull DisconnectPacket create(final @NotNull Component component,
|
||||
final boolean login) {
|
||||
return new DisconnectPacket(new ComponentHolder(component), login);
|
||||
public static @NotNull DisconnectPacket create(final @NotNull Component component, final boolean duringLogin) {
|
||||
return new DisconnectPacket(new ComponentHolder(component), duringLogin);
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -33,7 +33,7 @@ public final class GameEventPacket implements FallbackPacket {
|
||||
private float value;
|
||||
|
||||
@Override
|
||||
public void encode(final @NotNull ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
|
||||
public void encode(final @NotNull ByteBuf byteBuf, final ProtocolVersion protocolVersion) throws Exception {
|
||||
byteBuf.writeByte(type);
|
||||
byteBuf.writeFloat(value);
|
||||
}
|
||||
|
||||
+1
-1
@@ -47,7 +47,7 @@ public final class JoinGamePacket implements FallbackPacket {
|
||||
private String levelName;
|
||||
|
||||
@Override
|
||||
public void encode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
|
||||
public void encode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) throws Exception {
|
||||
byteBuf.writeInt(entityId);
|
||||
|
||||
if (protocolVersion.inBetween(MINECRAFT_1_7_2, MINECRAFT_1_7_6)) {
|
||||
|
||||
+2
-2
@@ -39,7 +39,7 @@ public final class KeepAlivePacket implements FallbackPacket {
|
||||
private long id;
|
||||
|
||||
@Override
|
||||
public void encode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
|
||||
public void encode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) throws Exception {
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_12_2) >= 0) {
|
||||
byteBuf.writeLong(id);
|
||||
} else if (protocolVersion.compareTo(MINECRAFT_1_8) >= 0) {
|
||||
@@ -50,7 +50,7 @@ public final class KeepAlivePacket implements FallbackPacket {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
|
||||
public void decode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) throws Exception {
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_12_2) >= 0) {
|
||||
id = byteBuf.readLong();
|
||||
} else if (protocolVersion.compareTo(MINECRAFT_1_8) >= 0) {
|
||||
|
||||
+1
-1
@@ -39,7 +39,7 @@ public final class PaddleBoatPacket implements FallbackPacket {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
|
||||
public void decode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) throws Exception {
|
||||
leftPaddle = byteBuf.readBoolean();
|
||||
rightPaddle = byteBuf.readBoolean();
|
||||
}
|
||||
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* 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.play;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.ToString;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
|
||||
|
||||
import java.util.EnumSet;
|
||||
import java.util.UUID;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
|
||||
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.*;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public final class PlayerInfoPacket implements FallbackPacket {
|
||||
private String username;
|
||||
private UUID uuid;
|
||||
private int gamemode;
|
||||
|
||||
@Override
|
||||
public void encode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) throws Exception {
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_8) < 0) {
|
||||
writeString(byteBuf, username);
|
||||
byteBuf.writeBoolean(true); // online
|
||||
byteBuf.writeShort(0);
|
||||
return;
|
||||
}
|
||||
|
||||
// https://wiki.vg/Protocol#player-info:player-actions
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_19_3) >= 0) {
|
||||
final EnumSet<Action> actions = EnumSet.noneOf(Action.class);
|
||||
actions.add(Action.ADD_PLAYER);
|
||||
actions.add(Action.UPDATE_LISTED);
|
||||
actions.add(Action.UPDATE_GAMEMODE);
|
||||
writeEnumSet(byteBuf, actions, Action.class);
|
||||
|
||||
writeVarInt(byteBuf, 1); // size
|
||||
writeUUID(byteBuf, uuid);
|
||||
writeString(byteBuf, username);
|
||||
writeVarInt(byteBuf, 0); // properties
|
||||
|
||||
byteBuf.writeBoolean(true); // update listed
|
||||
writeVarInt(byteBuf, gamemode);
|
||||
return;
|
||||
}
|
||||
|
||||
writeVarInt(byteBuf, 0); // ADD_PLAYER
|
||||
writeVarInt(byteBuf, 1);
|
||||
writeUUID(byteBuf, uuid);
|
||||
writeString(byteBuf, username);
|
||||
writeVarInt(byteBuf, 0);
|
||||
writeVarInt(byteBuf, gamemode);
|
||||
writeVarInt(byteBuf, 60);
|
||||
byteBuf.writeBoolean(false);
|
||||
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_19) >= 0) {
|
||||
byteBuf.writeBoolean(false);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
public enum Action {
|
||||
ADD_PLAYER,
|
||||
INITIALIZE_CHAT,
|
||||
UPDATE_GAMEMODE,
|
||||
UPDATE_LISTED,
|
||||
UPDATE_LATENCY,
|
||||
UPDATE_DISPLAY_NAME
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -40,7 +40,7 @@ public final class PlayerInputPacket implements FallbackPacket {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
|
||||
public void decode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) throws Exception {
|
||||
sideways = byteBuf.readFloat();
|
||||
forward = byteBuf.readFloat();
|
||||
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_8) < 0) {
|
||||
|
||||
+1
-1
@@ -39,7 +39,7 @@ public final class PlayerPositionIdlePacket implements FallbackPacket {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decode(final @NotNull ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
|
||||
public void decode(final @NotNull ByteBuf byteBuf, final ProtocolVersion protocolVersion) throws Exception {
|
||||
onGround = byteBuf.readBoolean();
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -64,10 +64,10 @@ public final class PlayerPositionLookPacket implements FallbackPacket {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
|
||||
public void decode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) throws Exception {
|
||||
x = byteBuf.readDouble();
|
||||
y = byteBuf.readDouble();
|
||||
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_8) < 0) {
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_8) < 0) {
|
||||
y = byteBuf.readDouble(); // Account for weird y positions on 1.7
|
||||
}
|
||||
z = byteBuf.readDouble();
|
||||
@@ -78,7 +78,7 @@ public final class PlayerPositionLookPacket implements FallbackPacket {
|
||||
|
||||
@Override
|
||||
public int expectedMaxLength(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
|
||||
return protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_8) < 0 ? 41 : 33;
|
||||
return protocolVersion.compareTo(MINECRAFT_1_8) < 0 ? 41 : 33;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+5
-3
@@ -26,6 +26,8 @@ import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_8;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@NoArgsConstructor
|
||||
@@ -40,10 +42,10 @@ public final class PlayerPositionPacket implements FallbackPacket {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
|
||||
public void decode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) throws Exception {
|
||||
x = byteBuf.readDouble();
|
||||
y = byteBuf.readDouble();
|
||||
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_8) < 0) {
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_8) < 0) {
|
||||
y = byteBuf.readDouble(); // Account for weird y positions on 1.7
|
||||
}
|
||||
z = byteBuf.readDouble();
|
||||
@@ -52,7 +54,7 @@ public final class PlayerPositionPacket implements FallbackPacket {
|
||||
|
||||
@Override
|
||||
public int expectedMaxLength(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
|
||||
return protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_8) < 0 ? 33 : 25;
|
||||
return protocolVersion.compareTo(MINECRAFT_1_8) < 0 ? 33 : 25;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+4
-8
@@ -24,9 +24,9 @@ import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_13;
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_8;
|
||||
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.*;
|
||||
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.readRetainedByteBufSlice17;
|
||||
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.readString;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@@ -40,12 +40,8 @@ public final class PluginMessagePacket implements FallbackPacket {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
|
||||
channel = readString(byteBuf, 512);
|
||||
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_13) >= 0) {
|
||||
channel = transformLegacyToModernChannel(channel);
|
||||
}
|
||||
public void decode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) throws Exception {
|
||||
channel = readString(byteBuf, 64);
|
||||
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_8) >= 0) {
|
||||
slicedBuffer = byteBuf.readRetainedSlice(byteBuf.readableBytes());
|
||||
|
||||
+1
-1
@@ -36,7 +36,7 @@ public final class RemoveEntitiesPacket implements FallbackPacket {
|
||||
private int entityId; // We only need one entityId
|
||||
|
||||
@Override
|
||||
public void encode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
|
||||
public void encode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) throws Exception {
|
||||
// No idea why Mojang decided that this is a good idea, but whatever
|
||||
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_17) != 0) {
|
||||
writeVarInt(byteBuf, 1); // size
|
||||
|
||||
+1
-1
@@ -38,7 +38,7 @@ public final class SetExperiencePacket implements FallbackPacket {
|
||||
private int totalExperience;
|
||||
|
||||
@Override
|
||||
public void encode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
|
||||
public void encode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) throws Exception {
|
||||
byteBuf.writeFloat(experienceBar);
|
||||
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_8) < 0) {
|
||||
byteBuf.writeShort(level);
|
||||
|
||||
+1
-1
@@ -36,7 +36,7 @@ public final class SetPassengersPacket implements FallbackPacket {
|
||||
private int entityId, passengerId;
|
||||
|
||||
@Override
|
||||
public void encode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
|
||||
public void encode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) throws Exception {
|
||||
// You can find this in the EntityAttach packet,
|
||||
// which was later replaced by SetPassengers in 1.9+
|
||||
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_9) < 0) {
|
||||
|
||||
+1
-1
@@ -44,7 +44,7 @@ public final class SetSlotPacket implements FallbackPacket {
|
||||
.build();
|
||||
|
||||
@Override
|
||||
public void encode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
|
||||
public void encode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) throws Exception {
|
||||
byteBuf.writeByte(0); // windowId
|
||||
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_17_1) >= 0) {
|
||||
|
||||
+2
-3
@@ -27,8 +27,7 @@ import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
|
||||
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.writeUUID;
|
||||
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.writeVarInt;
|
||||
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.*;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@@ -39,7 +38,7 @@ public final class SpawnEntityPacket implements FallbackPacket {
|
||||
private double x, y, z;
|
||||
|
||||
@Override
|
||||
public void encode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
|
||||
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;
|
||||
|
||||
+1
-1
@@ -40,7 +40,7 @@ public final class TeleportConfirmPacket implements FallbackPacket {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
|
||||
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) throws Exception {
|
||||
teleportId = readVarInt(byteBuf);
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -37,7 +37,7 @@ public final class TransactionPacket implements FallbackPacket {
|
||||
private boolean accepted;
|
||||
|
||||
@Override
|
||||
public void decode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
|
||||
public void decode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) throws Exception {
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_17) < 0) {
|
||||
windowId = byteBuf.readByte();
|
||||
transactionId = byteBuf.readShort();
|
||||
@@ -51,7 +51,7 @@ public final class TransactionPacket implements FallbackPacket {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void encode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
|
||||
public void encode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) throws Exception {
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_17) < 0) {
|
||||
byteBuf.writeByte(windowId);
|
||||
byteBuf.writeShort((short) transactionId);
|
||||
|
||||
+1
-1
@@ -38,7 +38,7 @@ public final class TransferPacket implements FallbackPacket {
|
||||
private int destinationPort;
|
||||
|
||||
@Override
|
||||
public void encode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
|
||||
public void encode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) throws Exception {
|
||||
writeString(byteBuf, destinationHost);
|
||||
writeVarInt(byteBuf, destinationPort);
|
||||
}
|
||||
|
||||
+2
-2
@@ -56,7 +56,7 @@ public final class UniversalChatPacket implements FallbackPacket {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void encode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
|
||||
public void encode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) throws Exception {
|
||||
// Serialized message
|
||||
componentHolder.write(byteBuf, protocolVersion);
|
||||
|
||||
@@ -77,7 +77,7 @@ public final class UniversalChatPacket implements FallbackPacket {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
|
||||
public void decode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) throws Exception {
|
||||
message = readString(byteBuf, 256);
|
||||
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_19) >= 0) {
|
||||
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
* 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.session;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.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.*;
|
||||
|
||||
/**
|
||||
* Flow for this session handler
|
||||
*
|
||||
* <li>
|
||||
* {@link SetSlotPacket} and {@link MapDataPacket} packets are sent to the client,
|
||||
* therefore, setting the player's item to a map with a code on it (CAPTCHA).
|
||||
* <br>
|
||||
* See more: {@link FallbackCAPTCHASessionHandler}, {@link MapCaptchaInfo}
|
||||
* </li>
|
||||
* <li>
|
||||
* Then, we wait for the player to enter the {@link FallbackCAPTCHASessionHandler#answer} in chat.
|
||||
* </li>
|
||||
*/
|
||||
public final class FallbackCAPTCHASessionHandler extends FallbackSessionHandler {
|
||||
|
||||
public FallbackCAPTCHASessionHandler(final FallbackUser user, final String username, final UUID uuid) {
|
||||
super(user, username, uuid);
|
||||
|
||||
this.tries = Sonar.get().getConfig().getVerification().getMap().getMaxTries();
|
||||
|
||||
// Set slot to map
|
||||
user.delayedWrite(new SetSlotPacket(36, 1,
|
||||
ItemType.FILLED_MAP.getId(user.getProtocolVersion()), SetSlotPacket.MAP_NBT));
|
||||
// Send random captcha to the player
|
||||
final MapCaptchaInfo captcha = MAP_INFO_PREPARER.getRandomCaptcha();
|
||||
this.answer = captcha.getAnswer();
|
||||
captcha.delayedWrite(user);
|
||||
// Teleport the player to the position above the platform
|
||||
user.delayedWrite(CAPTCHA_POSITION);
|
||||
// Make sure the player cannot move
|
||||
user.delayedWrite(user.isGeyser() ? CAPTCHA_ABILITIES_BEDROCK : CAPTCHA_ABILITIES);
|
||||
// Make sure the player knows what to do
|
||||
user.delayedWrite(enterCodeMessage);
|
||||
// Send all packets in one flush
|
||||
user.getChannel().flush();
|
||||
}
|
||||
|
||||
private final String answer;
|
||||
private int tries, lastCountdownIndex, keepAliveStreak;
|
||||
|
||||
@Override
|
||||
public void handle(final @NotNull FallbackPacket packet) {
|
||||
// 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");
|
||||
|
||||
// Handle incoming chat messages
|
||||
if (packet instanceof UniversalChatPacket) {
|
||||
final UniversalChatPacket chat = (UniversalChatPacket) packet;
|
||||
|
||||
// Captcha is correct, finish verification
|
||||
if (chat.getMessage().equals(answer)) {
|
||||
finishVerification();
|
||||
return;
|
||||
}
|
||||
|
||||
// Captcha is incorrect, remove one try
|
||||
checkState(tries-- > 0, "failed captcha too often");
|
||||
user.write(incorrectCaptcha);
|
||||
} else if (packet instanceof PlayerPositionPacket
|
||||
|| packet instanceof PlayerPositionLookPacket) {
|
||||
// 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]);
|
||||
}
|
||||
lastCountdownIndex = index;
|
||||
// Send a KeepAlive packet every few seconds
|
||||
if (keepAliveStreak++ > 20) {
|
||||
keepAliveStreak = 0;
|
||||
// Send a KeepAlive packet to prevent timeout
|
||||
user.write(CAPTCHA_KEEP_ALIVE);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+276
@@ -0,0 +1,276 @@
|
||||
/*
|
||||
* 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.session;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketDecoder;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.*;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.config.SonarConfiguration.Verification.Gravity.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 FallbackUser user, final String username, final UUID uuid) {
|
||||
super(user, username, uuid);
|
||||
|
||||
// We don't want to check Geyser players for valid gravity, as this might cause issues because of the protocol
|
||||
this.enableGravityCheck = !user.isGeyser() && Sonar.get().getConfig().getVerification().getGravity().isEnabled();
|
||||
this.enableCollisionsCheck = Sonar.get().getConfig().getVerification().getGravity().isCheckCollisions();
|
||||
|
||||
// FIXME: 1.18.2-1.19.3 weird loading world issue
|
||||
// First, write the JoinGame packet to the buffer
|
||||
user.delayedWrite(joinGame);
|
||||
// Then, write the ClientAbilities packet to the buffer
|
||||
// This is only necessary if the player is in creative mode
|
||||
if (Sonar.get().getConfig().getVerification().getGravity().getGamemode() == CREATIVE) {
|
||||
user.delayedWrite(DEFAULT_ABILITIES);
|
||||
}
|
||||
// Write the PositionLook packet to the buffer
|
||||
user.delayedWrite(spawnPosition);
|
||||
// Write the DefaultSpawnPosition packet to the buffer
|
||||
if (user.getProtocolVersion().compareTo(MINECRAFT_1_19_3) >= 0) {
|
||||
user.delayedWrite(defaultSpawnPosition);
|
||||
}
|
||||
// Write the PlayerInfo packet to the buffer
|
||||
if (user.getProtocolVersion().compareTo(MINECRAFT_1_16_4) >= 0) {
|
||||
user.delayedWrite(PLAYER_INFO);
|
||||
}
|
||||
// 1.20.3+ introduced game events
|
||||
// Make sure the client knows that we're sending chunks next
|
||||
if (user.getProtocolVersion().compareTo(MINECRAFT_1_20_3) >= 0) {
|
||||
user.delayedWrite(START_WRITING_CHUNKS);
|
||||
}
|
||||
// Teleport player into an empty world by sending an empty chunk packet
|
||||
user.delayedWrite(EMPTY_CHUNK_DATA);
|
||||
// Spawn the invisible platform below the player
|
||||
if (this.enableCollisionsCheck) {
|
||||
user.delayedWrite(updateSectionBlocks);
|
||||
}
|
||||
// Send all packets at once
|
||||
user.getChannel().flush();
|
||||
|
||||
// 1.8 and below don't have TeleportConfirm packets, which is why we're skipping that check.
|
||||
if (user.getProtocolVersion().compareTo(MINECRAFT_1_9) < 0) {
|
||||
markTeleported();
|
||||
}
|
||||
}
|
||||
|
||||
private final boolean enableGravityCheck, enableCollisionsCheck;
|
||||
private boolean teleported, checkMovement;
|
||||
private short expectedTransactionId = 1;
|
||||
private double x, y, z, deltaY;
|
||||
private int movementTick;
|
||||
|
||||
private void markTeleported() {
|
||||
// Activate the movement checks
|
||||
teleported = true;
|
||||
}
|
||||
|
||||
private void markNextStage() {
|
||||
// Force-stop the movement checks
|
||||
teleported = false;
|
||||
|
||||
// Check if the player sent all necessary packets
|
||||
checkState(user.isReceivedClientSettings(), "didn't send client settings");
|
||||
// Don't check Geyser players for plugin messages
|
||||
if (!user.isGeyser()) {
|
||||
checkState(user.isReceivedPluginMessage(), "didn't send plugin message");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses ping packets to check for an immediate, legitimate response from the client
|
||||
* <br>
|
||||
* <a href="https://wiki.vg/Protocol#Ping_.28configuration.29">Wiki.vg - Ping (configuration)</a>
|
||||
* <a href="https://wiki.vg/Protocol#Ping_.28play.29">Wiki.vg - Ping (play)</a>
|
||||
*/
|
||||
private void sendTransaction() {
|
||||
// Interrupt any ongoing process/check
|
||||
markNextStage();
|
||||
|
||||
// Send a Transaction (Ping) packet with a random ID
|
||||
expectedTransactionId = (short) -(RANDOM.nextInt(Short.MAX_VALUE));
|
||||
user.write(new TransactionPacket(0, expectedTransactionId, false));
|
||||
}
|
||||
|
||||
private void forceCAPTCHA() {
|
||||
// Interrupt any ongoing process/check
|
||||
markNextStage();
|
||||
|
||||
// Forcefully make the player solve a CAPTCHA
|
||||
final var decoder = (FallbackPacketDecoder) user.getPipeline().get(FallbackPacketDecoder.class);
|
||||
decoder.setListener(new FallbackCAPTCHASessionHandler(user, username, uuid));
|
||||
}
|
||||
|
||||
private void markSuccess() {
|
||||
// Either send the player to the vehicle check,
|
||||
// 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, x, y, z));
|
||||
} else if (Sonar.get().getFallback().shouldPerformCaptcha()) {
|
||||
decoder.setListener(new FallbackCAPTCHASessionHandler(user, username, uuid));
|
||||
} else {
|
||||
// The player has passed all checks
|
||||
finishVerification();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(final @NotNull FallbackPacket packet) {
|
||||
if (packet instanceof TransactionPacket) {
|
||||
final TransactionPacket transaction = (TransactionPacket) packet;
|
||||
|
||||
// Make sure random transactions aren't counted
|
||||
checkState(expectedTransactionId <= 0, "unexpected transaction");
|
||||
// Make sure the transaction was accepted
|
||||
// This must - by vanilla protocol - always be accepted
|
||||
checkState(transaction.isAccepted(), "didn't accept transaction");
|
||||
// Also check if the transaction ID matches the expected ID
|
||||
final long transactionId = transaction.getTransactionId();
|
||||
checkState(transactionId == expectedTransactionId,
|
||||
"expected T ID " + expectedTransactionId + ", but got " + transactionId);
|
||||
|
||||
markSuccess();
|
||||
} else if (packet instanceof PlayerPositionLookPacket) {
|
||||
// Make sure the player has teleported before checking for position packets
|
||||
if (teleported) {
|
||||
final PlayerPositionLookPacket positionLook = (PlayerPositionLookPacket) packet;
|
||||
handleMovement(positionLook.getX(), positionLook.getY(), positionLook.getZ(), positionLook.isOnGround());
|
||||
}
|
||||
} else if (packet instanceof PlayerPositionPacket) {
|
||||
// Make sure the player has teleported before checking for position packets
|
||||
if (teleported) {
|
||||
final PlayerPositionPacket position = (PlayerPositionPacket) packet;
|
||||
handleMovement(position.getX(), position.getY(), position.getZ(), position.isOnGround());
|
||||
}
|
||||
} else if (packet instanceof TeleportConfirmPacket) {
|
||||
final TeleportConfirmPacket teleportConfirm = (TeleportConfirmPacket) packet;
|
||||
|
||||
// Only expect this packet to be sent once
|
||||
checkState(!teleported, "duplicate teleport confirm");
|
||||
// Check if the teleport ID matches the expected ID
|
||||
final int teleportId = teleportConfirm.getTeleportId();
|
||||
checkState(teleportId == TELEPORT_ID,
|
||||
"expected TP ID " + TELEPORT_ID + ", but got " + teleportId);
|
||||
|
||||
markTeleported();
|
||||
}
|
||||
// Make sure to catch all ClientSettings and PluginMessage packets during the play phase.
|
||||
else if (packet instanceof ClientSettingsPacket) {
|
||||
// Let the session handler itself know about this packet
|
||||
checkClientSettings((ClientSettingsPacket) packet);
|
||||
} else if (packet instanceof PluginMessagePacket) {
|
||||
// Let the session handler itself know about this packet
|
||||
checkPluginMessage((PluginMessagePacket) packet);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleMovement(final double x, final double y, final double z, final boolean isOnGround) {
|
||||
// Check if the client hasn't moved before sending the first movement packet
|
||||
if (!checkMovement) {
|
||||
// No need to continue checking if the gravity and collision checks are disabled
|
||||
// Continue with the next stage of the verification
|
||||
if (!enableGravityCheck && !enableCollisionsCheck) {
|
||||
sendTransaction();
|
||||
return;
|
||||
}
|
||||
|
||||
if (x == SPAWN_X_POSITION && z == SPAWN_Z_POSITION) {
|
||||
// Now, once we verified the X and Z position, we can safely check for gravity
|
||||
checkMovement = true;
|
||||
movementTick = 0;
|
||||
// Synchronize the Y coordinate
|
||||
this.y = dynamicSpawnYPosition;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate/store all necessary positions
|
||||
final double lastDeltaY = this.deltaY;
|
||||
final double lastY = this.y;
|
||||
this.deltaY = y - lastY;
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.z = z;
|
||||
|
||||
// Log/debug position if enabled in the configuration
|
||||
if (Sonar.get().getConfig().getVerification().isDebugXYZPositions()) {
|
||||
Sonar.get().getFallback().getLogger().info("{}: {}/{}/{}, ly={}/dy={}, {}",
|
||||
username, x, y, z, lastY, deltaY, isOnGround);
|
||||
}
|
||||
|
||||
// The player is not allowed to move away from the collision platform.
|
||||
// This should not happen unless the max movement tick is configured to a high number.
|
||||
checkState(Math.abs(x - BLOCKS_PER_ROW) < BLOCKS_PER_ROW, "moved too far (x)");
|
||||
checkState(Math.abs(z - BLOCKS_PER_ROW) < BLOCKS_PER_ROW, "moved too far (z)");
|
||||
|
||||
if (!isOnGround) {
|
||||
// The deltaY is 0 whenever the player sends their first position packet.
|
||||
// We have to account for this or the player will fail the verification.
|
||||
if (deltaY == 0) {
|
||||
// Reset the movement tick as a safety measure
|
||||
movementTick = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
if (enableGravityCheck) {
|
||||
// Predict the player's current motion based on the last motion
|
||||
// https://minecraft.wiki/w/Entity#Motion_of_entities
|
||||
final double predicted = (lastDeltaY - 0.08) * 0.98f;
|
||||
final double difference = Math.abs(deltaY - predicted);
|
||||
|
||||
// Check if the difference between the predicted motion and
|
||||
// the actual motion is greater than our minimum threshold.
|
||||
if (difference > 1e-7) {
|
||||
// Do not throw an exception if the user configured to display the CAPTCHA instead
|
||||
if (Sonar.get().getConfig().getVerification().getGravity().isCaptchaOnFail()) {
|
||||
forceCAPTCHA();
|
||||
return;
|
||||
}
|
||||
user.fail("incorrect gravity; predicted: " + predicted + " deltaY: " + deltaY + " y: " + y);
|
||||
}
|
||||
|
||||
// The player is obeying gravity, go on to the next stage if the collision check is disabled.
|
||||
if (++movementTick == maxMovementTick && !enableCollisionsCheck) {
|
||||
sendTransaction();
|
||||
}
|
||||
}
|
||||
} else if (enableCollisionsCheck) {
|
||||
// Calculate the difference between the player's Y coordinate and the expected Y coordinate
|
||||
double collisionOffsetY = (DEFAULT_Y_COLLIDE_POSITION + blockType.getBlockHeight()) - y;
|
||||
// 1.7 sends the head position instead of the AABB minY
|
||||
// This little hack accounts for the offset of approximately 1.62
|
||||
if (user.getProtocolVersion().compareTo(MINECRAFT_1_8) < 0) {
|
||||
collisionOffsetY += 1.62f;
|
||||
}
|
||||
// Make sure the player is actually colliding with the blocks and not only spoofing ground
|
||||
checkState(collisionOffsetY > -0.03, "illegal collision: " + collisionOffsetY);
|
||||
// The player has collided with the blocks, go on to the next stage
|
||||
sendTransaction();
|
||||
}
|
||||
}
|
||||
}
|
||||
+170
@@ -0,0 +1,170 @@
|
||||
/*
|
||||
* 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.session;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
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.FallbackPacketEncoder;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.configuration.FinishConfigurationPacket;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.login.LoginAcknowledgedPacket;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.ClientSettingsPacket;
|
||||
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.*;
|
||||
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer.*;
|
||||
|
||||
/**
|
||||
* Flow for this session handler
|
||||
*
|
||||
* <li>
|
||||
* A {@link KeepAlivePacket} packet is sent to the client*
|
||||
* <br>
|
||||
* See more: {@link FallbackLoginSessionHandler#initialize18()}
|
||||
* </li>
|
||||
* <li>
|
||||
* After the KeepAlive packet is validated, the client enters the configuration phase.**
|
||||
* </li>
|
||||
* <li>
|
||||
* Then, the session handler is set to the {@link FallbackGravitySessionHandler}.
|
||||
* <br>
|
||||
* See more: {@link FallbackLoginSessionHandler#markSuccess()}
|
||||
* </li>
|
||||
* <br>
|
||||
* * The KeepAlive check is skipped on 1.7, as KeepAlive packets don't exist during the LOGIN state.
|
||||
* <br>
|
||||
* ** The internals of the configuration phase were not mentioned.
|
||||
* Find out more about the client configuration and registry synchronization:
|
||||
* {@link #synchronizeClientRegistry()}, {@link #updateEncoderDecoderState(FallbackPacketRegistry)}
|
||||
*/
|
||||
public final class FallbackLoginSessionHandler extends FallbackSessionHandler {
|
||||
|
||||
public FallbackLoginSessionHandler(final FallbackUser user, final String username, final UUID uuid) {
|
||||
super(user, username, uuid);
|
||||
|
||||
// Start initializing the actual join process for pre-1.20.2 clients
|
||||
if (user.getProtocolVersion().compareTo(MINECRAFT_1_20_2) < 0) {
|
||||
// This trick helps in reducing unnecessary outgoing server traffic
|
||||
// by avoiding sending other packets to clients that are potentially bots.
|
||||
if (user.getProtocolVersion().compareTo(MINECRAFT_1_8) < 0) {
|
||||
user.getChannel().eventLoop().schedule(this::markSuccess, 100L, TimeUnit.MILLISECONDS);
|
||||
} else {
|
||||
initialize18();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean acknowledgedLogin;
|
||||
private int expectedKeepAliveId;
|
||||
|
||||
/**
|
||||
* The purpose of these KeepAlive packets is to confirm that the connection
|
||||
* is active and legitimate, thereby preventing bot connections that
|
||||
* could flood the server with login attempts and other unwanted traffic.
|
||||
*/
|
||||
private void initialize18() {
|
||||
// Send a KeepAlive packet with a random ID
|
||||
expectedKeepAliveId = RANDOM.nextInt();
|
||||
user.write(new KeepAlivePacket(expectedKeepAliveId));
|
||||
}
|
||||
|
||||
private void markSuccess() {
|
||||
// Make sure we can actually switch over to the next check
|
||||
updateEncoderDecoderState(FallbackPacketRegistry.GAME);
|
||||
// Pass the player to the next verification handler
|
||||
final FallbackGravitySessionHandler gravitySessionHandler = new FallbackGravitySessionHandler(user, username, uuid);
|
||||
final var decoder = (FallbackPacketDecoder) user.getPipeline().get(FallbackPacketDecoder.class);
|
||||
decoder.setListener(gravitySessionHandler);
|
||||
}
|
||||
|
||||
private void markAcknowledged() {
|
||||
acknowledgedLogin = true;
|
||||
|
||||
synchronizeClientRegistry();
|
||||
// Write the FinishConfiguration packet to the buffer
|
||||
user.delayedWrite(FINISH_CONFIGURATION);
|
||||
// Send all packets in one flush
|
||||
user.getChannel().flush();
|
||||
// Set decoder state to actually catch all packets
|
||||
updateEncoderDecoderState(FallbackPacketRegistry.CONFIG);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(final @NotNull FallbackPacket packet) {
|
||||
if (packet instanceof KeepAlivePacket) {
|
||||
// This is the first packet we expect from the client
|
||||
final KeepAlivePacket keepAlive = (KeepAlivePacket) packet;
|
||||
|
||||
// Check if the KeepAlive ID matches the expected ID
|
||||
final long keepAliveId = keepAlive.getId();
|
||||
checkState(keepAliveId == expectedKeepAliveId,
|
||||
"expected K ID " + expectedKeepAliveId + " but got " + keepAliveId);
|
||||
|
||||
// 1.8 clients send KeepAlive packets with the ID 0 every second
|
||||
// while the player is in the "Downloading terrain" screen.
|
||||
expectedKeepAliveId = 0;
|
||||
|
||||
// Spawn the player in the virtual world if the client does not need
|
||||
// any configuration (pre-1.20.2).
|
||||
if (user.getProtocolVersion().compareTo(MINECRAFT_1_20_2) < 0) {
|
||||
markSuccess();
|
||||
}
|
||||
} else if (packet instanceof LoginAcknowledgedPacket) {
|
||||
// Prevent users from sending multiple LoginAcknowledged packets
|
||||
checkState(!acknowledgedLogin, "sent duplicate login ack");
|
||||
markAcknowledged();
|
||||
} else if (packet instanceof FinishConfigurationPacket) {
|
||||
markSuccess();
|
||||
}
|
||||
// Make sure to catch all ClientSettings and PluginMessage packets during the configuration phase.
|
||||
else if (packet instanceof ClientSettingsPacket) {
|
||||
// Let the session handler itself know about this packet
|
||||
checkClientSettings((ClientSettingsPacket) packet);
|
||||
} else if (packet instanceof PluginMessagePacket) {
|
||||
// Let the session handler itself know about this packet
|
||||
checkPluginMessage((PluginMessagePacket) packet);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateEncoderDecoderState(final @NotNull FallbackPacketRegistry registry) {
|
||||
final var decoder = (FallbackPacketDecoder) user.getPipeline().get(FallbackPacketDecoder.class);
|
||||
final var encoder = (FallbackPacketEncoder) user.getPipeline().get(FallbackPacketEncoder.class);
|
||||
// Update the packet registry state in the encoder and decoder pipelines
|
||||
decoder.updateRegistry(registry);
|
||||
encoder.updateRegistry(registry);
|
||||
}
|
||||
|
||||
private void synchronizeClientRegistry() {
|
||||
// 1.20.5+ adds new "game bundle features" which overcomplicate all of this...
|
||||
if (user.getProtocolVersion().compareTo(MINECRAFT_1_20_5) >= 0) {
|
||||
// Write the new RegistrySync packets to the buffer
|
||||
for (final FallbackPacket syncPacket : REGISTRY_SYNC_1_20_5) {
|
||||
user.delayedWrite(syncPacket);
|
||||
}
|
||||
} else {
|
||||
// Write the old RegistrySync packet to the buffer
|
||||
user.delayedWrite(REGISTRY_SYNC_LEGACY);
|
||||
}
|
||||
}
|
||||
}
|
||||
+143
@@ -0,0 +1,143 @@
|
||||
/*
|
||||
* 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.session;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.event.impl.UserVerifySuccessEvent;
|
||||
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
|
||||
import xyz.jonesdev.sonar.api.model.VerifiedPlayer;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketListener;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.ClientSettingsPacket;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.PluginMessagePacket;
|
||||
import xyz.jonesdev.sonar.common.statistics.GlobalSonarStatistics;
|
||||
import xyz.jonesdev.sonar.common.util.ProtocolUtil;
|
||||
|
||||
import java.util.Random;
|
||||
import java.util.UUID;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_13;
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_20_5;
|
||||
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer.transferToOrigin;
|
||||
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.BRAND_CHANNEL;
|
||||
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.BRAND_CHANNEL_LEGACY;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public abstract class FallbackSessionHandler implements FallbackPacketListener {
|
||||
protected final FallbackUser user;
|
||||
protected final String username;
|
||||
protected final UUID uuid;
|
||||
|
||||
protected static final Random RANDOM = new Random();
|
||||
|
||||
protected void checkState(final boolean state, final String failReason) {
|
||||
// Fails the verification if the condition is not met
|
||||
if (!state) {
|
||||
// Let the API know that the user has failed the verification
|
||||
user.fail(failReason);
|
||||
}
|
||||
}
|
||||
|
||||
protected final void finishVerification() {
|
||||
// Increment number of total successful verifications
|
||||
GlobalSonarStatistics.totalSuccessfulVerifications++;
|
||||
|
||||
// Add verified player to the database
|
||||
final long timeTaken = user.getLoginTimer().delay();
|
||||
Sonar.get().getVerifiedPlayerController().add(new VerifiedPlayer(user.getInetAddress(), uuid, timeTaken));
|
||||
|
||||
// Call the VerifySuccessEvent for external API usage
|
||||
Sonar.get().getEventManager().publish(new UserVerifySuccessEvent(username, uuid, user, timeTaken));
|
||||
|
||||
// If enabled, transfer the player back to the origin server.
|
||||
// This feature was introduced by Mojang in Minecraft version 1.20.5.
|
||||
if (Sonar.get().getConfig().getVerification().getTransfer().isEnabled()
|
||||
&& user.getProtocolVersion().compareTo(MINECRAFT_1_20_5) >= 0) {
|
||||
user.write(transferToOrigin);
|
||||
// Close the channel to ensure that the connection is closed
|
||||
user.getChannel().close();
|
||||
} else {
|
||||
// Disconnect player with the verification success message
|
||||
user.disconnect(Sonar.get().getConfig().getVerification().getVerificationSuccess(), false);
|
||||
}
|
||||
|
||||
Sonar.get().getFallback().getLogger().info(Sonar.get().getConfig().getVerification().getSuccessLog()
|
||||
.replace("%name%", username)
|
||||
.replace("%time%", user.getLoginTimer().toString()));
|
||||
}
|
||||
|
||||
protected final void checkClientSettings(final @NotNull ClientSettingsPacket clientSettings) {
|
||||
// Ensure that the client locale is correct
|
||||
validateClientLocale(clientSettings.getLocale());
|
||||
// Check if the player sent an unused bit flag in the skin section
|
||||
checkState((clientSettings.getSkinParts() & 0x80) == 0, "sent unused bit flag");
|
||||
|
||||
// Mark the ClientSettings packet as received
|
||||
user.setReceivedClientSettings(true);
|
||||
}
|
||||
|
||||
protected final void checkPluginMessage(final @NotNull PluginMessagePacket pluginMessage) {
|
||||
final boolean usingModernChannel = pluginMessage.getChannel().equals(BRAND_CHANNEL);
|
||||
final boolean usingLegacyChannel = pluginMessage.getChannel().equals(BRAND_CHANNEL_LEGACY);
|
||||
|
||||
// Skip this payload if it does not contain client brand information
|
||||
if (!usingModernChannel && !usingLegacyChannel) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Make sure the player isn't sending the client brand multiple times
|
||||
checkState(!user.isReceivedPluginMessage(), "sent duplicate plugin message");
|
||||
// Check if the channel is correct - 1.13 uses the new namespace
|
||||
// system ('minecraft:' + channel) and anything below 1.13 uses
|
||||
// the legacy namespace system ('MC|' + channel).
|
||||
final boolean v1_13 = user.getProtocolVersion().compareTo(MINECRAFT_1_13) >= 0;
|
||||
checkState(usingLegacyChannel || v1_13, "invalid brand channel");
|
||||
|
||||
// Validate the client branding using a regex to filter unwanted characters.
|
||||
if (Sonar.get().getConfig().getVerification().getBrand().isEnabled()) {
|
||||
validateClientBrand(pluginMessage.getSlicedBuffer());
|
||||
}
|
||||
|
||||
// Mark the PluginMessage packet as received
|
||||
user.setReceivedPluginMessage(true);
|
||||
}
|
||||
|
||||
protected final void validateClientBrand(final @NotNull ByteBuf content) {
|
||||
// No need to check for empty brands since ProtocolUtil#readBrandMessage
|
||||
// already performs invalid string checks by default.
|
||||
final String read = ProtocolUtil.readBrandMessage(content);
|
||||
// Check if the decoded client brand string is too long
|
||||
checkState(read.length() < Sonar.get().getConfig().getVerification().getBrand().getMaxLength(),
|
||||
"client brand is too long: " + read.length());
|
||||
// Check for illegal client brands
|
||||
checkState(!read.equals("Vanilla"), "illegal client brand: " + read);
|
||||
// Regex pattern for validating client brands
|
||||
final Pattern pattern = Sonar.get().getConfig().getVerification().getBrand().getValidRegex();
|
||||
checkState(pattern.matcher(read).matches(), "client brand does not match pattern");
|
||||
}
|
||||
|
||||
protected final void validateClientLocale(final @NotNull String locale) {
|
||||
// Check the client locale by performing a simple regex check
|
||||
// that disallows non-ascii characters by default.
|
||||
final Pattern pattern = Sonar.get().getConfig().getVerification().getValidLocaleRegex();
|
||||
checkState(pattern.matcher(locale).matches(), "client locale does not match pattern");
|
||||
}
|
||||
}
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
/*
|
||||
* 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.session;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketDecoder;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.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 static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_9;
|
||||
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer.*;
|
||||
|
||||
/**
|
||||
* Flow for this session handler
|
||||
*
|
||||
* <li>
|
||||
* {@link SpawnEntityPacket} and {@link SetPassengersPacket} packets are sent to the client,
|
||||
* therefore, making the player enter a boat.
|
||||
* <br>
|
||||
* See more: {@link FallbackVehicleSessionHandler}
|
||||
* </li>
|
||||
* <li>
|
||||
* Then, all we do is 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 double x, final double y, final double z) {
|
||||
super(user, username, uuid);
|
||||
|
||||
// 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, x, y, z));
|
||||
user.delayedWrite(setPassengers);
|
||||
user.getChannel().flush();
|
||||
}
|
||||
|
||||
private boolean receivedPaddle, receivedInput;
|
||||
|
||||
private void markSuccess() {
|
||||
// Pass the player to the next best verification handler
|
||||
if (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.
|
||||
final var decoder = (FallbackPacketDecoder) user.getPipeline().get(FallbackPacketDecoder.class);
|
||||
// Send the player to the CAPTCHA handler
|
||||
decoder.setListener(new FallbackCAPTCHASessionHandler(user, username, uuid));
|
||||
} else {
|
||||
// The player has passed all checks
|
||||
finishVerification();
|
||||
}
|
||||
}
|
||||
|
||||
@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) {
|
||||
final PlayerInputPacket playerInput = (PlayerInputPacket) packet;
|
||||
|
||||
// Check if the player is sending invalid vehicle speed values
|
||||
checkState(Math.abs(playerInput.getForward()) <= 0.98f, "illegal vehicle speed (f)");
|
||||
checkState(Math.abs(playerInput.getSideways()) <= 0.98f, "illegal vehicle speed (s)");
|
||||
|
||||
// Only mark this packet as correct if the player is not moving the vehicle
|
||||
if (playerInput.isJump() || playerInput.isUnmount()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Mark this check as success if the player has sent
|
||||
// two PlayerInput and a minimum of one PaddleBoat packets.
|
||||
if (receivedInput && receivedPaddle) {
|
||||
markSuccess();
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
// Mark the PlayerInput packet as received
|
||||
receivedInput = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
+16
-19
@@ -23,25 +23,22 @@ import xyz.jonesdev.sonar.common.statistics.CachedBandwidthStatistics;
|
||||
|
||||
@UtilityClass
|
||||
public final class SonarServiceThreadManager {
|
||||
private static final SonarServiceThread VERBOSE = new SonarServiceThread("sonar-verbose-thread", 250L,
|
||||
() -> {
|
||||
// Make sure to clean up the cached statistics
|
||||
// since we don't want to display wrong values.
|
||||
Sonar.get().getStatistics().cleanUpCache();
|
||||
// Update the attack tracker
|
||||
Sonar.get().getAttackTracker().checkIfUnderAttack();
|
||||
// Update the action bar verbose
|
||||
Sonar.get().getVerboseHandler().update();
|
||||
});
|
||||
private static final SonarServiceThread FALLBACK_QUEUE = new SonarServiceThread("sonar-fallback-queue-thread", 1000L,
|
||||
() -> Sonar.get().getFallback().getQueue().poll());
|
||||
private static final SonarServiceThread STATISTICS = new SonarServiceThread("sonar-statistics-thread", 1000L,
|
||||
() -> {
|
||||
// Clean up bandwidth statistics
|
||||
CachedBandwidthStatistics.reset();
|
||||
// Clean up the cache of rate-limited IPs
|
||||
Sonar.get().getFallback().getRatelimiter().cleanUpCache();
|
||||
});
|
||||
private static final SonarServiceThread VERBOSE = new SonarServiceThread("sonar-verbose-thread",
|
||||
250L, () -> {
|
||||
// Make sure to clean up the cached statistics
|
||||
// since we don't want to display wrong values.
|
||||
Sonar.get().getStatistics().cleanUpCache();
|
||||
Sonar.get().getFallback().getRatelimiter().getFailCountCache().cleanUp();
|
||||
Sonar.get().getFallback().getBlacklist().cleanUp();
|
||||
// Update the attack tracker
|
||||
Sonar.get().getAttackTracker().checkIfUnderAttack();
|
||||
// Update the action bar verbose
|
||||
Sonar.get().getVerboseHandler().update();
|
||||
});
|
||||
private static final SonarServiceThread FALLBACK_QUEUE = new SonarServiceThread("sonar-fallback-queue-thread",
|
||||
1000L, () -> Sonar.get().getFallback().getQueue().poll());
|
||||
private static final SonarServiceThread STATISTICS = new SonarServiceThread("sonar-statistics-thread",
|
||||
1000L, CachedBandwidthStatistics::reset);
|
||||
|
||||
public void start() {
|
||||
VERBOSE.start();
|
||||
|
||||
-2
@@ -66,8 +66,6 @@ public final class GlobalSonarStatistics implements SonarStatistics {
|
||||
public void cleanUpCache() {
|
||||
LOGINS_PER_SECOND.cleanUp();
|
||||
CONNECTIONS_PER_SECOND.cleanUp();
|
||||
// Also clean up the blacklist
|
||||
Sonar.get().getFallback().getBlacklist().cleanUp();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+1
-2
@@ -20,7 +20,6 @@ package xyz.jonesdev.sonar.common.subcommand.impl;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.val;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Unmodifiable;
|
||||
import xyz.jonesdev.sonar.api.SonarPlatform;
|
||||
@@ -56,7 +55,7 @@ public final class DumpCommand extends Subcommand {
|
||||
|
||||
@Unmodifiable
|
||||
private @NotNull Map<String, Object> collectMappedInformation() {
|
||||
val mappings = new WeakHashMap<String, Object>();
|
||||
final var mappings = new WeakHashMap<String, Object>();
|
||||
mappings.put("sonar", new Dump.Sonar(
|
||||
SONAR.getVersion().getFull(),
|
||||
SONAR.getPlatform(),
|
||||
|
||||
+1
-2
@@ -145,8 +145,7 @@ public final class VerifiedCommand extends Subcommand {
|
||||
LOCK.add(rawInetAddress);
|
||||
// Add verified player to the database
|
||||
final long timestamp = System.currentTimeMillis();
|
||||
final VerifiedPlayer verifiedPlayer = new VerifiedPlayer(inetAddress.toString(), uuid, timestamp);
|
||||
Sonar.get().getVerifiedPlayerController().add(verifiedPlayer);
|
||||
Sonar.get().getVerifiedPlayerController().add(new VerifiedPlayer(inetAddress, uuid, timestamp));
|
||||
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getVerifiedAdd()
|
||||
.replace("%ip%", rawInetAddress));
|
||||
|
||||
@@ -30,22 +30,21 @@ import org.jetbrains.annotations.NotNull;
|
||||
import java.io.DataOutput;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Arrays;
|
||||
import java.util.BitSet;
|
||||
import java.util.EnumSet;
|
||||
import java.util.UUID;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
// Taken from
|
||||
// Mostly taken from
|
||||
// https://github.com/PaperMC/Velocity/blob/dev/3.0.0/proxy/src/main/java/com/velocitypowered/proxy/protocol/ProtocolUtils.java
|
||||
@UtilityClass
|
||||
public class ProtocolUtil {
|
||||
public static final UUID EMPTY_UUID = new UUID(0L, 0L);
|
||||
|
||||
private static final int FORGE_MAX_ARRAY_LENGTH = Integer.MAX_VALUE & 0x1FFF9A;
|
||||
|
||||
private static final String BRAND_CHANNEL_LEGACY = "MC|Brand";
|
||||
private static final String BRAND_CHANNEL = "minecraft:brand";
|
||||
private static final String REGISTER_CHANNEL_LEGACY = "REGISTER";
|
||||
private static final String REGISTER_CHANNEL = "minecraft:register";
|
||||
private static final String UNREGISTER_CHANNEL_LEGACY = "UNREGISTER";
|
||||
private static final String UNREGISTER_CHANNEL = "minecraft:unregister";
|
||||
private static final Pattern INVALID_IDENTIFIER_REGEX = Pattern.compile("[^a-z0-9\\-_]*");
|
||||
public static final String BRAND_CHANNEL_LEGACY = "MC|Brand";
|
||||
public static final String BRAND_CHANNEL = "minecraft:brand";
|
||||
|
||||
public static int readVarInt(final ByteBuf byteBuf) {
|
||||
int read = readVarIntSafely(byteBuf);
|
||||
@@ -247,7 +246,29 @@ public class ProtocolUtil {
|
||||
}
|
||||
}
|
||||
|
||||
public static void writeCompoundTag(final ByteBuf byteBuf, final CompoundBinaryTag compoundTag) {
|
||||
// Taken from
|
||||
// https://github.com/Nan1t/NanoLimbo/blob/main/src/main/java/ua/nanit/limbo/protocol/ByteMessage.java#L276
|
||||
public <E extends Enum<E>> void writeEnumSet(final ByteBuf byteBuf,
|
||||
final EnumSet<E> enumset,
|
||||
final @NotNull Class<E> oclass) {
|
||||
final E[] enums = oclass.getEnumConstants();
|
||||
final BitSet bits = new BitSet(enums.length);
|
||||
|
||||
for (int i = 0; i < enums.length; ++i) {
|
||||
bits.set(i, enumset.contains(enums[i]));
|
||||
}
|
||||
|
||||
writeFixedBitSet(byteBuf, bits, enums.length);
|
||||
}
|
||||
|
||||
private static void writeFixedBitSet(final ByteBuf byteBuf, final @NotNull BitSet bits, final int size) {
|
||||
if (bits.length() > size) {
|
||||
throw new StackOverflowError("BitSet too large (expected " + size + " got " + bits.size() + ")");
|
||||
}
|
||||
byteBuf.writeBytes(Arrays.copyOf(bits.toByteArray(), (size + 8) >> 3));
|
||||
}
|
||||
|
||||
public static void writeCompoundTag(final @NotNull ByteBuf byteBuf, final @NotNull CompoundBinaryTag compoundTag) {
|
||||
try {
|
||||
BinaryTagIO.writer().write(compoundTag, (DataOutput) new ByteBufOutputStream(byteBuf));
|
||||
} catch (IOException exception) {
|
||||
@@ -256,7 +277,7 @@ public class ProtocolUtil {
|
||||
}
|
||||
|
||||
// Taken from
|
||||
// https://github.com/Nan1t/NanoLimbo/pull/79/files#diff-4aa8208044741102c6326c7e85086e6fa8fcc7c064f7df6fd0411baf5f2b4504
|
||||
// https://github.com/Nan1t/NanoLimbo/blob/main/src/main/java/ua/nanit/limbo/protocol/ByteMessage.java#L219
|
||||
public static void writeNamelessCompoundTag(final @NotNull ByteBuf byteBuf, final @NotNull BinaryTag binaryTag) {
|
||||
try (final ByteBufOutputStream output = new ByteBufOutputStream(byteBuf)) {
|
||||
// TODO: Find a way to improve this...
|
||||
@@ -310,26 +331,6 @@ public class ProtocolUtil {
|
||||
return byteBuf.readRetainedSlice(length);
|
||||
}
|
||||
|
||||
public static @NotNull String transformLegacyToModernChannel(@NotNull final String name) {
|
||||
if (name.indexOf(':') != -1) {
|
||||
return name;
|
||||
}
|
||||
|
||||
switch (name) {
|
||||
case REGISTER_CHANNEL_LEGACY:
|
||||
return REGISTER_CHANNEL;
|
||||
case UNREGISTER_CHANNEL_LEGACY:
|
||||
return UNREGISTER_CHANNEL;
|
||||
case BRAND_CHANNEL_LEGACY:
|
||||
return BRAND_CHANNEL;
|
||||
case "BungeeCord":
|
||||
return "bungeecord:main";
|
||||
default:
|
||||
final String lower = name.toLowerCase();
|
||||
return "legacy:" + INVALID_IDENTIFIER_REGEX.matcher(lower).replaceAll("");
|
||||
}
|
||||
}
|
||||
|
||||
private void checkFrame(final boolean expression, final String message) {
|
||||
if (!expression) {
|
||||
throw new CorruptedFrameException(message);
|
||||
|
||||
@@ -28,7 +28,7 @@ attack-tracker:
|
||||
# Amount of time that has to pass in order for an attack to be over
|
||||
# (This value represents the time in milliseconds: 1 second = 1000 milliseconds)
|
||||
min-attack-duration: 30000
|
||||
# Number of times an incident has to be reported in order to be acknowledged as an attack
|
||||
# Number of times an incident has to be reported to be acknowledged as an attack
|
||||
# This number acts as a buffer to filter out false attack notifications
|
||||
min-attack-threshold: 2
|
||||
# Amount of time that has to pass in order for a new attack to be detected
|
||||
@@ -71,7 +71,7 @@ verification:
|
||||
timing: ALWAYS
|
||||
checks:
|
||||
# Checks if the players' falling motion is following Minecraft's gravity formula
|
||||
# All predicted motions are precalculated in order to save performance
|
||||
# All predicted motions are precalculated to save performance
|
||||
gravity:
|
||||
# Should Sonar check for valid client gravity? (Recommended)
|
||||
enabled: true
|
||||
@@ -79,7 +79,7 @@ verification:
|
||||
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 in order to be allowed to hit the platform
|
||||
# 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
|
||||
@@ -132,7 +132,7 @@ verification:
|
||||
mix: 0.15
|
||||
# Path to the background image Sonar uses for the CAPTCHA
|
||||
# If the image isn't found, a noise will be used as the default background
|
||||
# You can simply put the name of the file here if file is in the plugin directory
|
||||
# You can put the name of the file here if the file is in the plugin directory
|
||||
background: ''
|
||||
# Should Sonar automatically detect the background color?
|
||||
# This is done to create a contrast between the background image and the text
|
||||
@@ -150,7 +150,7 @@ verification:
|
||||
# It is not recommended to add numbers or letters that might look like one another
|
||||
dictionary: '1235689'
|
||||
|
||||
# Checks if the players is sending a valid client brand to the server
|
||||
# Checks if the players are sending a valid client brand to the server
|
||||
client-brand:
|
||||
# Should Sonar check for valid client brands? (Recommended)
|
||||
enabled: true
|
||||
@@ -163,7 +163,7 @@ verification:
|
||||
valid-name-regex: ^[a-zA-Z0-9_]+$
|
||||
# Regex for validating client locale during verification
|
||||
valid-locale-regex: ^[a-zA-Z_]+$
|
||||
# Ping a player has to have in order to timeout
|
||||
# Ping a player has to have to timeout
|
||||
# (This value represents the time in milliseconds: 1 second = 1000 milliseconds)
|
||||
max-ping: 10000
|
||||
# Maximum number of login packets the player has to send in order to be kicked
|
||||
|
||||
@@ -0,0 +1,222 @@
|
||||
#
|
||||
# /$$$$$$
|
||||
# /$$__ $$
|
||||
# | $$ \__/ /$$$$$$ /$$$$$$$ /$$$$$$ /$$$$$$
|
||||
# | $$$$$$ /$$__ $$| $$__ $$ |____ $$ /$$__ $$
|
||||
# \____ $$| $$ \ $$| $$ \ $$ /$$$$$$$| $$ \__/
|
||||
# /$$ \ $$| $$ | $$| $$ | $$ /$$__ $$| $$
|
||||
# | $$$$$$/| $$$$$$/| $$ | $$| $$$$$$$| $$
|
||||
# \______/ \______/ |__/ |__/ \_______/|__/
|
||||
#
|
||||
# Besoin d'aide ou d'autres questions ? https://jonesdev.xyz/discord
|
||||
# Code source : https://github.com/jonesdevelopment/sonar
|
||||
# Droit d'auteur Jones Development, un contributeur Sonar, licence sous GPL-3.0
|
||||
#
|
||||
|
||||
# Paramètre général de Sonar
|
||||
general:
|
||||
# Devrait Sonar enregistrer les adresses IP des joueurs dans la console ?
|
||||
log-player-addresses: true
|
||||
# Nombre maximum de joueurs en ligne avec la même IP
|
||||
max-online-per-ip: 3
|
||||
|
||||
# Paramètre de détection d'attaque
|
||||
# Modifier uniquement si vous savez ce que vous faites
|
||||
attack-tracker:
|
||||
# Nombre minimum de nouveaux joueurs pour qu'une attaque soit détectée
|
||||
min-players-for-attack: 8
|
||||
# Durée nécessaire pour qu'une attaque soit considérée comme terminée
|
||||
# (Cette valeur représente le temps en millisecondes : 1 seconde = 1000 millisecondes)
|
||||
min-attack-duration: 30000
|
||||
# Nombre de fois qu'un incident doit être signalé pour être reconnu comme une attaque
|
||||
# Ce nombre sert de tampon pour filtrer les fausses notifications d'attaque
|
||||
min-attack-threshold: 2
|
||||
# Durée nécessaire pour qu'une nouvelle attaque soit détectée
|
||||
# (Cette valeur représente le temps en millisecondes : 1 seconde = 1000 millisecondes)
|
||||
attack-cooldown-delay: 3000
|
||||
|
||||
# Vous pouvez connecter Sonar à une base de données pour conserver les joueurs vérifiés même après le redémarrage de votre serveur
|
||||
# Remarque : Les adresses IP sont enregistrées en texte brut. Vous êtes responsable de la sécurité de votre base de données !
|
||||
database:
|
||||
# Type de base de données utilisé par Sonar pour stocker les joueurs vérifiés
|
||||
# Types possibles : NONE, MYSQL, MARIADB
|
||||
type: NONE
|
||||
# Adresse de l'hôte pour l'authentification avec la base de données SQL
|
||||
host: localhost
|
||||
# Port pour l'authentification avec la base de données SQL
|
||||
port: 3306
|
||||
# Nom de la base de données SQL
|
||||
name: sonar
|
||||
# Nom d'utilisateur pour l'authentification avec la base de données SQL
|
||||
username: ''
|
||||
# Mot de passe pour l'authentification avec la base de données SQL
|
||||
password: ''
|
||||
# Pendant combien de jours Sonar doit-il conserver les joueurs vérifiés dans la base de données ?
|
||||
maximum-age: 5
|
||||
|
||||
# Chaque nouvelle demande de connexion sera mise en file d'attente pour éviter les attaques d'inscription par spam
|
||||
# La file d'attente est mise à jour toutes les 1 seconde (20 ticks)
|
||||
queue:
|
||||
# Nombre maximum de demandes de connexion en file d'attente simultanées par seconde
|
||||
max-polls: 30
|
||||
|
||||
# Chaque nouveau joueur qui se connecte pour la première fois sera envoyé à
|
||||
# un serveur limbo léger où des vérifications avancées de bot sont effectuées
|
||||
verification:
|
||||
# Quand Sonar devrait-il vérifier les nouveaux joueurs ?
|
||||
# Types possibles : ALWAYS, DURING_ATTACK, NEVER
|
||||
# - ALWAYS : Les nouveaux joueurs seront toujours vérifiés (Recommandé)
|
||||
# - DURING_ATTACK : Les nouveaux joueurs ne seront vérifiés que pendant une attaque
|
||||
# - NEVER : Les nouveaux joueurs ne seront jamais vérifiés
|
||||
timing: ALWAYS
|
||||
checks:
|
||||
# Vérifie si le mouvement de chute des joueurs suit la formule de la gravité de Minecraft
|
||||
# Tous les mouvements prévus sont précalculés afin d'économiser les performances
|
||||
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 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
|
||||
|
||||
# Fait taper au joueur un code à partir d'une carte virtuelle dans le chat
|
||||
map-captcha:
|
||||
# Quand Sonar devrait-il faire résoudre un CAPTCHA au joueur ?
|
||||
# Types possibles : ALWAYS, DURING_ATTACK, NEVER
|
||||
# - ALWAYS : Les nouveaux joueurs recevront toujours un CAPTCHA
|
||||
# - DURING_ATTACK : Les nouveaux joueurs ne recevront un CAPTCHA que pendant une attaque
|
||||
# - NEVER : Les nouveaux joueurs ne recevront jamais de CAPTCHA (Recommandé)
|
||||
timing: NEVER
|
||||
# Options pour les effets sur l'image montrée au joueur
|
||||
# Vous ne devriez modifier cela que si vous savez ce que vous faites
|
||||
effects:
|
||||
# Ajoute des lignes aléatoires à l'image
|
||||
scratches: true
|
||||
# Applique un filtre d'ondulation (effet ondulé)
|
||||
ripple: true
|
||||
# Applique un filtre de bosse (texte accentué)
|
||||
bump: true
|
||||
# Filtre de distorsion
|
||||
distortion:
|
||||
# Sonar devrait-il appliquer un filtre de distorsion ?
|
||||
enabled: true
|
||||
# Quelle forme Sonar devrait-il utiliser pour le filtre de distorsion ?
|
||||
# Types possibles : 0, 1, 2, 3
|
||||
# - 0 : Croix
|
||||
# - 1 : Lignes
|
||||
# - 2 : Cercles
|
||||
# - 3 : Carrés
|
||||
shape: 2
|
||||
# Distance entre les pixels/objets déformés
|
||||
distance: 5
|
||||
# Densité des pixels/objets déformés
|
||||
density: 0.1
|
||||
# Quantité de mélange entre les couleurs des objets
|
||||
mix: 0.15
|
||||
# Chemin vers l'image d'arrière-plan que Sonar utilise pour le CAPTCHA
|
||||
# Si l'image n'est pas trouvée, un bruit sera utilisé comme arrière-plan par défaut
|
||||
# Vous pouvez simplement mettre le nom du fichier ici si le fichier est dans le répertoire du plugin
|
||||
background: ''
|
||||
# Sonar devrait-il détecter automatiquement la couleur de fond ?
|
||||
# Cela est fait pour créer un contraste entre l'image d'arrière-plan et le texte
|
||||
# Si cela est désactivé, Sonar utilisera un dégradé aléatoire pour les couleurs
|
||||
auto-color: false
|
||||
# Combien de réponses Sonar devrait-il préparer ?
|
||||
# Cette tâche se déroule de manière asynchrone en arrière-plan
|
||||
precompute: 500
|
||||
# Combien de temps Sonar devrait-il attendre jusqu'à ce que le joueur échoue le CAPTCHA ?
|
||||
# (Cette valeur représente le temps en millisecondes : 1 seconde = 1000 millisecondes)
|
||||
max-duration: 30000
|
||||
# Combien de fois un joueur doit-il échouer au CAPTCHA avant d'échouer la vérification ?
|
||||
max-tries: 3
|
||||
# Caractères (lettres et chiffres) autorisés à apparaître dans la réponse au CAPTCHA
|
||||
# Il n'est pas recommandé d'ajouter des chiffres ou des lettres qui pourraient se ressembler
|
||||
dictionary: '1235689'
|
||||
|
||||
# Vérifie si le joueur envoie une marque client valide au serveur
|
||||
client-brand:
|
||||
# Sonar devrait-il vérifier les marques client valides ? (Recommandé)
|
||||
enabled: true
|
||||
# Regex pour valider les marques client lors de la vérification
|
||||
valid-regex: ^[!-~ ]+$
|
||||
# Longueur maximale de la marque client lors de la vérification
|
||||
max-length: 64
|
||||
|
||||
# Regex pour valider les noms d'utilisateur lors de la vérification
|
||||
valid-name-regex: ^[a-zA-Z0-9_]+$
|
||||
# Regex pour valider la locale client lors de la vérification
|
||||
valid-locale-regex: ^[a-zA-Z_]+$
|
||||
# Le ping qu'un joueur doit avoir pour dépasser le délai d'attente
|
||||
# (Cette valeur représente le temps en millisecondes : 1 seconde = 1000 millisecondes)
|
||||
max-ping: 10000
|
||||
# Nombre maximal de paquets de connexion que le joueur doit envoyer pour être expulsé
|
||||
max-login-packets: 256
|
||||
|
||||
# S'il est activé, le joueur sera renvoyé au serveur d'origine
|
||||
# après avoir passé avec succès la vérification du bot.
|
||||
# Cette fonctionnalité a été introduite par Mojang dans la version Minecraft 1.20.5
|
||||
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
|
||||
# De plus, vous voudrez peut-être réduire la quantité de limitation de débit de connexion
|
||||
# effectuée par Velocity ou d'autres proxys/plugins, car cela pourrait empêcher le
|
||||
# joueur d'être transféré correctement.
|
||||
enabled: false
|
||||
# Quel serveur Sonar devrait-il utiliser pour transférer le joueur une fois la vérification réussie ?
|
||||
# Veuillez entrer l'adresse IP du serveur que les joueurs utilisent normalement pour se connecter à votre serveur.
|
||||
# Par exemple, vous pouvez mettre "mc.hypixel.net" ou une IP directe comme "1.1.1.1"
|
||||
destination-host: "play.my-server.com"
|
||||
# Quel port Sonar devrait-il utiliser pour transférer le joueur au serveur d'origine ?
|
||||
# Si votre serveur n'a pas besoin d'un port pour se connecter, vous pouvez laisser celui-ci comme 25565.
|
||||
destination-port: 25565
|
||||
|
||||
# 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 ?
|
||||
log-during-attack: false
|
||||
# Sonar devrait-il journaliser chaque changement de position/mouvement pendant la vérification ?
|
||||
# Ce n'est pas recommandé pour les serveurs de production mais peut être utile pour repérer les erreurs.
|
||||
debug-xyz-positions: false
|
||||
# Sonar devrait-il également vérifier les joueurs Geyser (Bedrock) ?
|
||||
# Cette fonctionnalité est expérimentale et peut causer des problèmes
|
||||
# Si cela est désactivé, les joueurs Bedrock seront ignorés
|
||||
check-geyser-players: true
|
||||
# Durée avant qu'un joueur ne soit déconnecté
|
||||
# (Cette valeur représente le temps en millisecondes : 1 seconde = 1000 millisecondes)
|
||||
read-timeout: 8000
|
||||
# Combien de temps un joueur doit-il attendre avant de se reconnecter pendant la vérification
|
||||
# (Cette valeur représente le temps en millisecondes : 1 seconde = 1000 millisecondes)
|
||||
rejoin-delay: 8000
|
||||
# Combien de temps Sonar doit-il se souvenir du nombre de vérifications échouées pour un joueur ?
|
||||
# (Cette valeur représente le temps en millisecondes : 1 seconde = 1000 millisecondes)
|
||||
remember-time: 120000
|
||||
# Combien de temps une adresse IP doit-elle être interdite de connexion en cas d'échec trop fréquent de la vérification ?
|
||||
# (Cette valeur représente le temps en millisecondes : 1 seconde = 1000 millisecondes)
|
||||
blacklist-time: 600000
|
||||
# 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)
|
||||
# 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: []
|
||||
@@ -66,7 +66,7 @@ verification:
|
||||
# როდის უნდა გადაამოწმოს Sonar-მა ახალი მოთამაშეები?
|
||||
# შესაძლო ტიპები: ALWAYS, DURING_ATTACK, NEVER
|
||||
# - ALWAYS: ახალი მოთამაშეები ყოველთვის მოწმდება (რეკომენდებულია)
|
||||
# - DURING_ATTACK: ახალი მოთამაშეები შემოწმდებიან მხოლოდ შეტევის დროს
|
||||
# - DURING_ATTACK: ახალი მოთამაშეები შემოწმდებიან მხოლოდ შეტევის დროსდროს
|
||||
# - NEVER: ახალი მოთამაშეები არასოდეს შემოწმდებიან
|
||||
timing: ALWAYS
|
||||
checks:
|
||||
@@ -88,12 +88,12 @@ verification:
|
||||
# - ADVENTURE: ყველა UI კომპონენტი ჩანს
|
||||
gamemode: ADVENTURE
|
||||
|
||||
# Checks if the client sends proper packets when mounting a vehicle
|
||||
# ამოწმებს, აგზავნის თუ არა კლიენტი შესაბამის პაკეტებს სატრანსპორტო საშუალებაზე დაჯდომის დროს
|
||||
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
|
||||
# შესაძლო ტიპები: ALWAYS, DURING_ATTACK, NEVER
|
||||
# - ALWAYS: ახალი მოთამაშეები ყოველთვის დაჯდებიან სატრანსპორტო საშუალებაზე
|
||||
# - DURING_ATTACK: ახალი მოთამაშეები მხოლოდ ბოტების შემოტევის დროს დაჯდებიან სატრანსპორტო საშუალებაზე
|
||||
# - NEVER: ახალი მოთამაშეები არასოდეს დაჯდებიან სატრანსპორტო საშუალებაზე (რეკომენდებულია)
|
||||
timing: ALWAYS
|
||||
|
||||
# დააწერინეთ მოთამაშეს კოდი ჩატში, რომელიც იქნება ნაჩვენები ვირტუალურ მაპზე, რომელიც ხელში ეჭირებად
|
||||
@@ -104,38 +104,39 @@ verification:
|
||||
# - DURING_ATTACK: ახალი მოთამაშებს მოუწევთ CAPTCHA-ს გაკეთება მხოლოდ შეტევის დროს
|
||||
# - NEVER: ახალ მოთამაშეებს არასდროს მოუწევთ CAPTCHA-ს გაკეთება (Recommended)
|
||||
timing: NEVER
|
||||
# Options for the effects on the image shown to the player
|
||||
# You should only edit this if you know what you're doing
|
||||
# ეფექტების პარამეტრები გამოსახულებაზე, რომელიც აჩვენა მოთამაშეს
|
||||
# თქვენ უნდა შეცვალოთ ეს მხოლოდ იმ შემთხვევაში, თუ იცით, რას აკეთებთ
|
||||
effects:
|
||||
# Adds random lines to the image
|
||||
# მატებს შემთხვევითი ხაზების ეფექტს გამოსახულებაზე
|
||||
scratches: true
|
||||
# Applies a ripple filter (wavy effect)
|
||||
# აყენებს ტალღური ეფექტის ფილტრს (ტალღისებური ეფექტი)
|
||||
ripple: true
|
||||
# Applies a bump filter (sharpened text)
|
||||
# აყენებს ამობურცული ტექსტის ფილტრს (გამხრწნილი ტექსტი)
|
||||
bump: true
|
||||
# Distortion filter
|
||||
# დამახინჯების ფილტრი
|
||||
distortion:
|
||||
# Should Sonar apply a distortion filter?
|
||||
# უნდა დაამატოს თუ არა Sonar-მა დამახინჯების ფილტრი?
|
||||
enabled: true
|
||||
# What shape should Sonar use for the distortion filter?
|
||||
# Possible types: 0, 1, 2, 3
|
||||
# - 0: Crosses
|
||||
# - 1: Lines
|
||||
# - 2: Circles
|
||||
# - 3: Squares
|
||||
# რა ფორმას უნდა გამოიყენოს Sonar-მა დამახინჯების ფილტრისთვის?
|
||||
# შესაძლო ტიპები: 0, 1, 2, 3
|
||||
# - 0: ჯვრები
|
||||
# - 1: ხაზები
|
||||
# - 2: წრეები
|
||||
# - 3: კვადრატები
|
||||
shape: 2
|
||||
# Distance between distorted pixels/objects
|
||||
# მანძილი დამახინჯებულ პიქსელებს/ობიექტებს შორის
|
||||
distance: 5
|
||||
# Density of the distorted pixels/objects
|
||||
# დამახინჯებული პიქსელების/ობიექტების სიხშირე
|
||||
density: 0.1
|
||||
# Amount of blending between the colors of objects
|
||||
# შერევის ხარისხი ობიექტების ფერებს შორის
|
||||
mix: 0.15
|
||||
# Path to the background image Sonar uses for the CAPTCHA
|
||||
# If the image isn't found, a noise will be used as the default background
|
||||
# CAPTCHA-სთვის Sonar-ის მიერ გამოყენებული ფონური გამოსახულების ბილიკი
|
||||
# თუ გამოსახულება არ მოიძებნა, ფონად გამოყენებული იქნება noise ი
|
||||
# შეგიძლიათ უბრალოდ ჩაწეროთ ფაილის სახელი, თუ ის პლაგინის დირექტორიაშია
|
||||
background: ''
|
||||
# Should Sonar automatically detect the background color?
|
||||
# This is done to create a contrast between the background image and the text
|
||||
# You can simply put the name of the file here if file is in the plugin directory
|
||||
# უნდა განსაზღვროს თუ არა Sonar-მა ავტომატურად ფონის ფერი?
|
||||
# ეს კეთდება იმისთვის, რომ შეიქმნას კონტრასტი ფონის გამოსახულებასა და ტექსტს შორის
|
||||
# თუ ეს გამორთულია, Sonar-ი გამოიყენებს შემთხვევით გრადიენტს ფერებისთვის
|
||||
auto-color: false
|
||||
# რამდენი პასუხი უნდა მოამზადოს Sonar-მა?
|
||||
# ეს ხდება ასინქრონულად უკანა ფონზე
|
||||
@@ -146,10 +147,10 @@ verification:
|
||||
# რამდენჯერ ვერ უდნა გაიაროს CAPTCHA მოთამაშემ სანამ ვერიფიკაციას ვერ გაივლის?
|
||||
max-tries: 3
|
||||
# სიმბოლოები (ასოები და რიცხვები), რომლებიც დაშვებულია გამოჩნდეს CAPTCHA-ს პასუხში
|
||||
# It is not recommended to add numbers or letters that might look like one another
|
||||
# არ არის რეკომენდებული რიცხვების ან ასოების დამატება, რომლებიც შეიძლება ერთმანეთს ჰგავდეს
|
||||
dictionary: '1235689'
|
||||
|
||||
#ამოწმებს, აგზავნიან თუ არა მოთამაშეები სერვერზე რეალურ კლიენტის სახელს
|
||||
# ამოწმებს, აგზავნიან თუ არა მოთამაშეები სერვერზე რეალურ კლიენტის სახელს
|
||||
client-brand:
|
||||
# უნდა შეამოწმოს თუ არა Sonar-მა ნამდვილია თუ არა მოთამაშის კლიენტის სახელი? (რეკომენდებულია)
|
||||
enabled: true
|
||||
@@ -168,22 +169,21 @@ verification:
|
||||
# მაქსიმალური რაოდენობა ლოგინ პაკეტების, რომელიც მოთამაშემ უნდა გააგზავნოს სანამ გაიკიკება სერვერიდან
|
||||
max-login-packets: 256
|
||||
|
||||
# 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
|
||||
# თუ ჩართულია, მოთამაშე გადავა საწყისი სერვერში ერიფიკაციის წარმატებით გავლის შემდეგ
|
||||
# ეს ფუნქცია დაემატა Mojang-ის მიერ Minecraft-ის 1.20.5 ვერსიაში
|
||||
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
|
||||
# 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.
|
||||
# უნდა გადაიყვანოს თუ არა Sonar-მა მოთამაშე საწყის სერვერზე (მათი გაკიკვის ნაცვლად)?
|
||||
# ამისთვის, თქვენ უნდა ჩართოთ ეს ფუნქცია თქვენი სერვერის კონფიგურაციაში
|
||||
# დამატებით, შეიძლება გინდოდეთ შეამციროთ შესვლის rate-limiting ი
|
||||
# რომელსაც ახორციელებს Velocity ან სხვა პროქსები/პლაგინები, რადგან ეს შეიძლება ხელს უშლიდეს
|
||||
# მოთამაშის სწორად გადაყვანას
|
||||
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.
|
||||
# For example, you can put "mc.hypixel.net" or a direct IP like "1.1.1.1"
|
||||
# რომელ სერვერზე უნდა გადაიყვანოს Sonar-მა მოთამაშე ვერიფიკაციის გავლის შემდეგ?
|
||||
# გთხოვთ, შეიყვანოთ სერვერის IP მისამართი, რომლითაც მოთამაშეები ჩვეულებრივ უერთდებიან თქვენს სერვერს
|
||||
# მაგალითად, შეგიძლიათ ჩაწეროთ "mc.hypixel.net" ან პირდაპირი IP როგორიცაა "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.
|
||||
# რომელი პორტი უნდა გამოიყენოს Sonar-მა მოთამაშის საწყის სერვერზე გადაყვანისას?
|
||||
# თუ თქვენს სერვერს არ სჭირდება პორტი შესასვლელად, შეგიძლიათ დატოვოთ ეს 25565-ზე
|
||||
destination-port: 25565
|
||||
|
||||
# ჩაიწეროს თუ არა Sonar-მა ახალი ვერიფიკაციის მცდელობები?
|
||||
@@ -193,9 +193,9 @@ verification:
|
||||
# ჩაიწეროს თუ არა Sonar-მა ყოველი მოძრაობა/პოზიცია ვერიფიკაციის დროს?
|
||||
# ეს არ არის რეკომენდირებული აქტიურ სერვერებისთვის, მაგრამ შეიძლება გამოსადეგი იყოს ცრუ პოსიტივების გამოსასწორებლად.
|
||||
debug-xyz-positions: false
|
||||
# Should Sonar also check Geyser (Bedrock) players?
|
||||
# This feature is experimental and might cause issues
|
||||
# If this is disabled, Bedrock players will be skipped
|
||||
# უნდა შეამოწმოს თუ არა Sonar-მა Geyser (Bedrock) მოთამაშეები?
|
||||
# ეს ფუნქცია ექსპერიმენტულია და შეიძლება გამოიწვიოს პრობლემები
|
||||
# თუ ეს გამორთულია, Bedrock მოთამაშეები გამოვლენილი იქნება
|
||||
check-geyser-players: true
|
||||
# დროის ოდენობა, რომელიც უნდა გავიდეს, სანამ მოთამაშე ტაიმაუტდება სერვერიდან.
|
||||
# (ეს არის ნაჩვენები მილიწამებში: 1 წამი = 1000 მილიწამი)
|
||||
@@ -203,7 +203,7 @@ verification:
|
||||
# რამდენ ხანს უნდა დაელოდოს მოთამაშე ვერიფიკაციის დროს ხელახლა დაკავშირებამდე?
|
||||
# (ეს არის ნაჩვენები მილიწამებში: 1 წამი = 1000 მილიწამი)
|
||||
rejoin-delay: 8000
|
||||
# How long should Sonar remember the amount of failed verifications for a player?
|
||||
# რამდენ ხანს უნდა ახსოვდეს Sonar-ს მოთამაშე რამდენჯერ იყო წარუმატებელი ვერიფიკაციის დროს?
|
||||
# (ეს არის ნაჩვენები მილიწამებში: 1 წამი = 1000 მილიწამი)
|
||||
remember-time: 120000
|
||||
# რამდენ ხნით უნდა აეკრძალოს IP მისამართს შესვლა, როდესაც ვერიფიკაციას ძალიან ხშირად ვერ აკეთებს?
|
||||
|
||||
@@ -24,7 +24,7 @@ header:
|
||||
# Placeholder for every '%footer%' in this configuration file
|
||||
footer:
|
||||
- <gray>If you believe that this is an error, contact an administrator.
|
||||
# Disconnect message that is shown when someone joins but there are too many online players with their IP address
|
||||
# Disconnect message that is shown when someone joins, but there are too many online players with their IP address
|
||||
too-many-online-per-ip:
|
||||
- '%header%'
|
||||
- <red>There are too many players online with your IP address.
|
||||
@@ -32,21 +32,21 @@ too-many-online-per-ip:
|
||||
|
||||
# Translations for commands
|
||||
commands:
|
||||
# Message that is shown when a player tries running /sonar without permission
|
||||
# Message shown when a player tries running /sonar without permission
|
||||
no-permission: '%prefix%<red>You do not have permission to execute this command.'
|
||||
# Message that is shown when someone uses a command incorrectly
|
||||
# Message shown when someone uses a command incorrectly
|
||||
incorrect-usage: '%prefix%<red>Usage: /sonar %usage%'
|
||||
# Message that is shown when someone provides an invalid IP address (Invalid format)
|
||||
# Message shown when someone provides an invalid IP address (Invalid format)
|
||||
invalid-ip-address: '%prefix%The IP address you provided seems to be invalid.'
|
||||
# Message that is shown when the console runs a command that is player-only
|
||||
# Message shown when the console runs a command that is player-only
|
||||
player-only: '%prefix%<red>You can only execute this command as a player.'
|
||||
# Message that is shown when a player runs a command that is console-only
|
||||
# Message shown when a player runs a command that is console-only
|
||||
console-only: '%prefix%<red>For security reasons, you can only execute this command
|
||||
through console.'
|
||||
# Message that is shown when a player executes Sonar commands too quickly
|
||||
# Message shown when a player executes Sonar commands too quickly
|
||||
cool-down: '%prefix%<red>You can only execute this command every 0.5 seconds.'
|
||||
cool-down-left: '%prefix%<red>Please wait another <bold>%time-left%s<reset><red>.'
|
||||
# Message that is shown when a player does not have permission to execute a certain subcommand
|
||||
# Message shown when a player does not have permission to execute a certain subcommand
|
||||
subcommand-no-permission: '%prefix%<red>You do not have permission to execute this
|
||||
subcommand. <gray>(%permission%)'
|
||||
|
||||
@@ -71,73 +71,73 @@ commands:
|
||||
|
||||
# Translations for '/sonar reload'
|
||||
reload:
|
||||
# Message that is shown when someone starts reloading Sonar
|
||||
# Message shown when someone starts reloading Sonar
|
||||
start: '%prefix%Reloading Sonar...'
|
||||
# Message that is shown when Sonar has finished reloading
|
||||
# Message shown when Sonar has finished reloading
|
||||
finish: '%prefix%<green>Successfully reloaded <gray>(%taken%ms)'
|
||||
|
||||
# Translations for '/sonar verbose'
|
||||
verbose:
|
||||
# Message that is shown when a player subscribes to Sonar verbose
|
||||
# Message shown when a player subscribes to Sonar verbose
|
||||
subscribed: '%prefix%You are now viewing Sonar verbose.'
|
||||
# Message that is shown when a player unsubscribes from Sonar verbose
|
||||
# Message shown when a player unsubscribes from Sonar verbose
|
||||
unsubscribed: '%prefix%You are no longer viewing Sonar verbose.'
|
||||
|
||||
# Translations for '/sonar notify'
|
||||
notify:
|
||||
# Message that is shown when a player subscribes to Sonar attack notifications
|
||||
# Message shown when a player subscribes to Sonar attack notifications
|
||||
subscribed: '%prefix%You are now viewing Sonar attack notifications.'
|
||||
# Message that is shown when a player unsubscribes from Sonar attack notifications
|
||||
# Message shown when a player unsubscribes from Sonar attack notifications
|
||||
unsubscribed: '%prefix%You are no longer viewing Sonar attack notifications.'
|
||||
|
||||
# Translations for '/sonar blacklist'
|
||||
blacklist:
|
||||
# Message that is shown when someone tries clearing the blacklist but is empty
|
||||
# Message shown when someone tries clearing the blacklist but is empty
|
||||
empty: '%prefix%The blacklist is currently empty. Therefore, no IP addresses were
|
||||
removed from the blacklist.'
|
||||
# Message that is shown when someone clears the blacklist
|
||||
# Message shown when someone clears the blacklist
|
||||
cleared: '%prefix%You successfully removed a total of %removed% IP address(es)
|
||||
from the blacklist.'
|
||||
# Message that is shown when someone checks the size of the blacklist
|
||||
# Message shown when someone checks the size of the blacklist
|
||||
size: '%prefix%The blacklist currently contains %amount% IP address(es).'
|
||||
# Message that is shown when someone adds an IP address to the blacklist
|
||||
# Message shown when someone adds an IP address to the blacklist
|
||||
added: '%prefix%Successfully added %ip% to the blacklist.'
|
||||
# Message that is shown when someone adds an IP address to the blacklist that is verified
|
||||
# Message shown when someone adds an IP address to the blacklist that is verified
|
||||
added-warning: '%prefix%<red>Warning: <white>%ip% is currently whitelisted. Consider
|
||||
removing the IP address from the list of verified players to avoid potential
|
||||
issues.'
|
||||
# Message that is shown when someone removes an IP address from the blacklist
|
||||
# Message shown when someone removes an IP address from the blacklist
|
||||
removed: '%prefix%Successfully removed %ip% from the blacklist.'
|
||||
# Message that is shown when someone adds an IP address to the blacklist, but it is already blacklisted
|
||||
# Message shown when someone adds an IP address to the blacklist, but it is already blacklisted
|
||||
duplicate-ip: '%prefix%The IP address you provided is already blacklisted.'
|
||||
# Message that is shown when someone removes an IP address from the blacklist, but it is not blacklisted
|
||||
# Message shown when someone removes an IP address from the blacklist, but it is not blacklisted
|
||||
ip-not-found: '%prefix%The IP address you provided is not blacklisted.'
|
||||
|
||||
# Translations for '/sonar verified'
|
||||
verified:
|
||||
# Message that is shown when someone tries clearing the list of verified players but is empty
|
||||
# Message shown when someone tries clearing the list of verified players but is empty
|
||||
empty: '%prefix%The list of verified players is currently empty. Therefore, no
|
||||
players were unverified.'
|
||||
# Message that is shown when someone clears the list of verified players
|
||||
# Message shown when someone clears the list of verified players
|
||||
cleared: '%prefix%You successfully unverified a total of %removed% unique player(s).'
|
||||
# Message that is shown when someone checks the size of the list of verified players
|
||||
# Message shown when someone checks the size of the list of verified players
|
||||
size: '%prefix%There are currently %amount% unique player(s) verified.'
|
||||
# Message that is shown when someone un-verifies an IP address
|
||||
# Message shown when someone un-verifies an IP address
|
||||
removed: '%prefix%Successfully unverified %ip%.'
|
||||
# Message that is shown when someone verifies an IP address
|
||||
# Message shown when someone verifies an IP address
|
||||
added: '%prefix%Successfully verified %ip%.'
|
||||
# Message that is shown when someone tries to verify an IP address/UUID that are already verified
|
||||
# Message shown when someone tries to verify an IP address/UUID that are already verified
|
||||
already: '%prefix%The IP address and UUID you provided are already verified.'
|
||||
# Message that is shown when someone un-verifies an IP address, but it is not verified
|
||||
# Message shown when someone un-verifies an IP address, but it is not verified
|
||||
ip-not-found: '%prefix%The IP address you provided is not verified.'
|
||||
# Message that is shown when someone tries un-verifying the same IP address twice (double operation)
|
||||
# Message shown when someone tries un-verifying the same IP address twice (double operation)
|
||||
blocked: '%prefix%Please wait for the current operation to finish.'
|
||||
|
||||
# Translations for '/sonar statistics'
|
||||
statistics:
|
||||
# Informational message that is shown above everything when viewing the statistics
|
||||
header: '%prefix%Showing %type% statistics for this session:'
|
||||
# Message that is shown when a player tries viewing an unknown statistic
|
||||
# Message shown when a player tries viewing an unknown statistic
|
||||
unknown-type: '%prefix%<red>Unknown statistics type! Available statistics: <gray>%statistics%'
|
||||
# Format of the general statistics message
|
||||
general:
|
||||
@@ -174,21 +174,21 @@ commands:
|
||||
verification:
|
||||
# Translations for all debug messages during the verification
|
||||
logs:
|
||||
# Message that is logged to console whenever a new player joins the server
|
||||
# Message logged to console whenever a new player joins the server
|
||||
connection: '%name%%ip% (%protocol%) has connected.'
|
||||
# Message that is logged to console whenever a player fails verification
|
||||
# Message logged to console whenever a player fails verification
|
||||
failed: '%ip% (%protocol%) has failed the bot check for: %reason%'
|
||||
# Message that is logged to console whenever a player is blacklisted
|
||||
# Message logged to console whenever a player is blacklisted
|
||||
blacklisted: '%ip% (%protocol%) was blacklisted for too many failed attempts'
|
||||
# Message that is logged to console whenever a player is verified
|
||||
# Message logged to console whenever a player is verified
|
||||
successful: '%name% has been verified successfully (%time%s!).'
|
||||
|
||||
# Messages that are shown to the player when they are supposed to enter the CAPTCHA
|
||||
captcha:
|
||||
# Message that is shown to the player when they have to enter the answer to the captcha
|
||||
# Message shown to the player when they have to enter the answer to the captcha
|
||||
enter-code: '%prefix%<green>Please enter the code in chat that is displayed on
|
||||
the map.'
|
||||
# Message that is shown to the player when they enter the wrong answer in chat
|
||||
# Message shown to the player when they enter the wrong answer in chat
|
||||
incorrect: '%prefix%<red>You have entered the wrong code. Please try again.'
|
||||
|
||||
# Disconnect message that is shown when someone joins while the captcha hasn't been prepared yet
|
||||
|
||||
@@ -0,0 +1,300 @@
|
||||
#
|
||||
# /$$$$$$
|
||||
# /$$__ $$
|
||||
# | $$ \__/ /$$$$$$ /$$$$$$$ /$$$$$$ /$$$$$$
|
||||
# | $$$$$$ /$$__ $$| $$__ $$ |____ $$ /$$__ $$
|
||||
# \____ $$| $$ \ $$| $$ \ $$ /$$$$$$$| $$ \__/
|
||||
# /$$ \ $$| $$ | $$| $$ | $$ /$$__ $$| $$
|
||||
# | $$$$$$/| $$$$$$/| $$ | $$| $$$$$$$| $$
|
||||
# \______/ \______/ |__/ |__/ \_______/|__/
|
||||
#
|
||||
# Besoin d'aide ou d'autres questions ? https://jonesdev.xyz/discord
|
||||
# Code source : https://github.com/jonesdevelopment/sonar
|
||||
# Droit d'auteur Jones Development, un contributeur Sonar, licence sous GPL-3.0
|
||||
#
|
||||
|
||||
# Espace réservé pour tout '%prefix%' dans ce fichier de configuration
|
||||
prefix: <b><yellow>Sonar</b> <gray>» <white>
|
||||
# Espace réservé pour tout '%support-url%' dans ce fichier de configuration
|
||||
support-url: https://jonesdev.xyz/discord/
|
||||
# Espace réservé pour tout '%header%' dans ce fichier de configuration
|
||||
header:
|
||||
- <b><yellow>Sonar</b><reset>
|
||||
- ''
|
||||
# Espace réservé pour tout '%footer%' dans ce fichier de configuration
|
||||
footer:
|
||||
- <gray>Si vous pensez que c'est une erreur, veuillez contacter un administrateur.
|
||||
# Message de déconnexion affiché lorsqu'un joueur se connecte mais qu'il y a trop de joueurs en ligne avec leur adresse IP
|
||||
too-many-online-per-ip:
|
||||
- '%header%'
|
||||
- <red>Il y a trop de joueurs en ligne avec votre adresse IP.
|
||||
- '%footer%'
|
||||
|
||||
# Traductions pour les commandes
|
||||
commands:
|
||||
# Message affiché lorsqu'un joueur tente d'exécuter /sonar sans permission
|
||||
no-permission: "%prefix%<red>Vous n'avez pas la permission d'exécuter cette commande."
|
||||
# Message affiché lorsqu'une commande est utilisée de manière incorrecte
|
||||
incorrect-usage: '%prefix%<red>Utilisation : /sonar %usage%'
|
||||
# Message affiché lorsqu'une adresse IP invalide est fournie (format invalide)
|
||||
invalid-ip-address: "%prefix%L'adresse IP que vous avez fournie semble invalide."
|
||||
# Message affiché lorsque la console exécute une commande qui est uniquement pour les joueurs
|
||||
player-only: "%prefix%<red>Vous ne pouvez exécuter cette commande qu'en tant que joueur."
|
||||
# Message affiché lorsqu'un joueur exécute une commande qui est uniquement pour la console
|
||||
console-only: '%prefix%<red>Pour des raisons de sécurité, vous ne pouvez exécuter cette commande que via la console.'
|
||||
# Message affiché lorsqu'un joueur exécute trop rapidement les commandes Sonar
|
||||
cool-down: "%prefix%<red>Vous ne pouvez exécuter cette commande qu'une fois toutes les 0,5 secondes."
|
||||
cool-down-left: '%prefix%<red>Veuillez attendre encore <bold>%time-left%<reset><red>.'
|
||||
# Message affiché lorsqu'un joueur n'a pas la permission d'exécuter une certaine sous-commande
|
||||
subcommand-no-permission: "%prefix%<red>Vous n'avez pas la permission d'exécuter cette sous-commande. <gray>(%permission%)"
|
||||
|
||||
# Traductions pour '/sonar'
|
||||
main:
|
||||
# Message informatif affiché au-dessus de tout lors de l'exécution de la commande principale
|
||||
header:
|
||||
- <yellow>Exécution de <b>Sonar</b> %version% sur %platform%.
|
||||
- <gray><click:open_url:'https://github.com/jonesdevelopment/sonar'>(C) %copyright_year%
|
||||
Jones Development et les contributeurs de Sonar</click>
|
||||
- ''
|
||||
- <gray>Besoin d'aide ou avez-vous des questions ?
|
||||
- <white><click:open_url:'https://jonesdev.xyz/discord/'><hover:show_text:'(Cliquez
|
||||
pour ouvrir Discord)'>Ouvrez un ticket sur Discord </hover></click><click:open_url:'https://github.com/jonesdevelopment/sonar/issues'><hover:show_text:'(Cliquez
|
||||
pour ouvrir GitHub)'>ou ouvrez un nouveau problème sur GitHub.
|
||||
- ''
|
||||
# Format de la liste des sous-commandes affichée lors de l'exécution de la commande principale
|
||||
subcommands: '<click:suggest_command:''/sonar %subcommand% ''><hover:show_text:''<gray>Seulement pour les joueurs : </gray>%only_players%<br><gray>Nécessite la console : </gray>%require_console%<br><gray>Permission : </gray><white>%permission%<br><gray>Alias : </gray>%aliases%''><dark_aqua> ▪ <gray>/sonar %subcommand% <white>%description%'
|
||||
|
||||
# Traductions pour '/sonar reload'
|
||||
reload:
|
||||
# Message affiché lorsque quelqu'un commence à recharger Sonar
|
||||
start: '%prefix%Rechargement de Sonar...'
|
||||
# Message affiché lorsque Sonar a fini de recharger
|
||||
finish: '%prefix%<green>Rechargement réussi <gray>(%taken%ms)'
|
||||
|
||||
# Traductions pour '/sonar verbose'
|
||||
verbose:
|
||||
# Message affiché lorsqu'un joueur s'abonne aux détails verbeux de Sonar
|
||||
subscribed: '%prefix%Vous visualisez maintenant les détails verbeux de Sonar.'
|
||||
# Message affiché lorsqu'un joueur se désabonne des détails verbeux de Sonar
|
||||
unsubscribed: '%prefix%Vous ne visualisez plus les détails verbeux de Sonar.'
|
||||
|
||||
# Traductions pour '/sonar notify'
|
||||
notify:
|
||||
# Message affiché lorsqu'un joueur s'abonne aux notifications d'attaque de Sonar
|
||||
subscribed: "%prefix%Vous visualisez maintenant les notifications d'attaque de Sonar."
|
||||
# Message affiché lorsqu'un joueur se désabonne des notifications d'attaque de Sonar
|
||||
unsubscribed: "%prefix%Vous ne visualisez plus les notifications d'attaque de Sonar."
|
||||
|
||||
# Traductions pour '/sonar blacklist'
|
||||
blacklist:
|
||||
# Message affiché lorsque quelqu'un tente de vider la liste noire mais qu'elle est vide
|
||||
empty: "%prefix%La liste noire est actuellement vide. Par conséquent, aucune adresse IP n'a été supprimée de la liste noire."
|
||||
# Message affiché lorsque quelqu'un vide la liste noire
|
||||
cleared: '%prefix%Vous avez réussi à supprimer un total de %removed% adresse(s) IP de la liste noire.'
|
||||
# Message affiché lorsque quelqu'un vérifie la taille de la liste noire
|
||||
size: '%prefix%La liste noire contient actuellement %amount% adresse(s) IP.'
|
||||
# Message affiché lorsque quelqu'un ajoute une adresse IP à la liste noire
|
||||
added: '%prefix%Adresse IP %ip% ajoutée avec succès à la liste noire.'
|
||||
# Message affiché lorsque quelqu'un ajoute une adresse IP à la liste noire qui est vérifiée
|
||||
added-warning: "%prefix%<red>Avertissement : <white>%ip% est actuellement sur liste blanche. Considérez la suppression de l'adresse IP de la liste des joueurs vérifiés pour éviter des problèmes potentiels."
|
||||
# Message affiché lorsque quelqu'un supprime une adresse IP de la liste noire
|
||||
removed: '%prefix%Adresse IP %ip% supprimée avec succès de la liste noire.'
|
||||
# Message affiché lorsque quelqu'un ajoute une adresse IP à la liste noire, mais qu'elle est déjà en liste noire
|
||||
duplicate-ip: "%prefix%L'adresse IP que vous avez fournie est déjà sur la liste noire."
|
||||
# Message affiché lorsque quelqu'un supprime une adresse IP de la liste noire, mais qu'elle n'est pas en liste noire
|
||||
ip-not-found: "%prefix%L'adresse IP que vous avez fournie ne figure pas sur la liste noire."
|
||||
|
||||
# Traductions pour '/sonar verified'
|
||||
verified:
|
||||
# Message affiché lorsque quelqu'un tente de vider la liste des joueurs vérifiés mais qu'elle est vide
|
||||
empty: "%prefix%La liste des joueurs vérifiés est actuellement vide. Par conséquent, aucun joueur n'a été désapprouvé."
|
||||
# Message affiché lorsque quelqu'un vide la liste des joueurs vérifiés
|
||||
cleared: '%prefix%Vous avez réussi à désapprouver un total de %removed% joueur(s) unique(s).'
|
||||
# Message affiché lorsque quelqu'un vérifie la taille de la liste des joueurs vérifiés
|
||||
size: '%prefix%Il y a actuellement %amount% joueur(s) unique(s) vérifié(s).'
|
||||
# Message affiché lorsque quelqu'un désapprouve une adresse IP
|
||||
removed: '%prefix%Adresse IP %ip% désapprouvée avec succès.'
|
||||
# Message affiché lorsque quelqu'un vérifie une adresse IP
|
||||
added: '%prefix%Adresse IP %ip% vérifiée avec succès.'
|
||||
# Message affiché lorsque quelqu'un tente de vérifier une adresse IP/UUID qui est déjà vérifiée
|
||||
already: "%prefix%L'adresse IP et l'UUID que vous avez fournis sont déjà vérifiés."
|
||||
# Message affiché lorsque quelqu'un désapprouve une adresse IP, mais qu'elle n'est pas vérifiée
|
||||
ip-not-found: "%prefix%L'adresse IP que vous avez fournie n'est pas vérifiée."
|
||||
# Message affiché lorsque quelqu'un tente de désapprouver la même adresse IP deux fois (double opération)
|
||||
blocked: "%prefix%Veuillez attendre la fin de l'opération en cours."
|
||||
|
||||
# Traductions pour '/sonar statistics'
|
||||
statistics:
|
||||
# Message informatif affiché au-dessus de tout lors de la visualisation des statistiques
|
||||
header: '%prefix%Affichage des statistiques %type% pour cette session :'
|
||||
# Message affiché lorsqu'un joueur tente de visualiser une statistique inconnue
|
||||
unknown-type: '%prefix%<red>Type de statistiques inconnu ! Statistiques disponibles : <gray>%statistics%'
|
||||
# Format du message de statistiques général
|
||||
general:
|
||||
- ' <dark_aqua>▪ <gray>Adresses IP vérifiées : <white>%verified%'
|
||||
- ' <dark_aqua>▪ <gray>En train de vérifier les adresses IP : <white>%verifying%'
|
||||
- ' <dark_aqua>▪ <gray>Adresses IP en liste noire : <white>%blacklisted%'
|
||||
- ' <dark_aqua>▪ <gray>Connexions en attente actuelles : <white>%queued%'
|
||||
- ' <dark_aqua>▪ <gray>Total des connexions non uniques : <white>%total_joins%'
|
||||
- ' <dark_aqua>▪ <gray>Total des tentatives de vérification : <white>%total_attempts%'
|
||||
- ' <dark_aqua>▪ <gray>Total des vérifications échouées : <white>%total_failed%'
|
||||
- ' <dark_aqua>▪ <gray>Temps de fonctionnement du serveur : <white>%uptime%'
|
||||
# Format du message de statistiques CPU
|
||||
cpu:
|
||||
- ' <dark_aqua>▪ <gray>Utilisation actuelle du CPU du processus : <white>%process_cpu%%'
|
||||
- ' <dark_aqua>▪ <gray>Utilisation actuelle du CPU du système : <white>%system_cpu%%'
|
||||
- ' <dark_aqua>▪ <gray>Utilisation du CPU par cœur du processus : <white>%average_process_cpu%%'
|
||||
- ' <dark_aqua>▪ <gray>Utilisation du CPU par cœur du système : <white>%average_system_cpu%%'
|
||||
- ' <dark_aqua>▪ <gray>Charge système générale : <white>%load_average%%'
|
||||
- ' <dark_aqua>▪ <gray>Nombre total de processeurs virtuels : <white>%virtual_cores%'
|
||||
# Format du message de statistiques de mémoire
|
||||
memory:
|
||||
- ' <dark_aqua>▪ <gray>Mémoire libre totale : <white>%free_memory%'
|
||||
- ' <dark_aqua>▪ <gray>Mémoire utilisée totale : <white>%used_memory%'
|
||||
- ' <dark_aqua>▪ <gray>Mémoire maximale totale : <white>%max_memory%'
|
||||
- ' <dark_aqua>▪ <gray>Mémoire allouée totale : <white>%total_memory%'
|
||||
# Format du message de statistiques réseau
|
||||
network:
|
||||
- ' <dark_aqua>▪ <gray>Bande passante entrante actuelle utilisée : <white>%incoming%'
|
||||
- ' <dark_aqua>▪ <gray>Bande passante sortante actuelle utilisée : <white>%outgoing%'
|
||||
- ' <dark_aqua>▪ <gray>Bande passante entrante totale utilisée : <white>%ttl_incoming%'
|
||||
- ' <dark_aqua>▪ <gray>Bande passante sortante totale utilisée : <white>%ttl_outgoing%'
|
||||
|
||||
# Traductions pour tous les messages pendant le processus de vérification
|
||||
verification:
|
||||
# Traductions pour tous les messages de débogage pendant la vérification
|
||||
logs:
|
||||
# Message enregistré dans la console chaque fois qu'un nouveau joueur rejoint le serveur
|
||||
connection: "%name%%ip% (%protocol%) s'est connecté."
|
||||
# Message enregistré dans la console chaque fois qu'un joueur échoue à la vérification
|
||||
failed: '%ip% (%protocol%) a échoué au test du bot pour : %reason%'
|
||||
# Message enregistré dans la console chaque fois qu'un joueur est sur liste noire
|
||||
blacklisted: '%ip% (%protocol%) a été mis sur liste noire pour trop de tentatives échouées'
|
||||
# Message enregistré dans la console chaque fois qu'un joueur est vérifié
|
||||
successful: '%name% a été vérifié avec succès (%time%s !).'
|
||||
|
||||
# Messages affichés au joueur lorsqu'il doit entrer le CAPTCHA
|
||||
captcha:
|
||||
# Message affiché au joueur lorsqu'il doit entrer la réponse au CAPTCHA
|
||||
enter-code: '%prefix%<green>Veuillez entrer le code affiché sur la carte.'
|
||||
# Message affiché au joueur lorsqu'il entre la mauvaise réponse dans le chat
|
||||
incorrect: '%prefix%<red>Vous avez entré le mauvais code. Veuillez réessayer.'
|
||||
|
||||
# Message de déconnexion affiché lorsque quelqu'un rejoint alors que le CAPTCHA n'a pas été préparé
|
||||
currently-preparing:
|
||||
- '%header%'
|
||||
- <yellow>Vos données anti-bot n'ont pas encore été préparées.
|
||||
- <gray>Veuillez patienter quelques secondes avant d'essayer de vous vérifier à nouveau.
|
||||
- '%footer%'
|
||||
# Message de déconnexion affiché lorsque quelqu'un se reconnecte trop rapidement pendant la vérification
|
||||
too-fast-reconnect:
|
||||
- '%header%'
|
||||
- <gold>Vous avez rejoint trop rapidement, réessayez plus tard.
|
||||
- <gray>Veuillez patienter quelques secondes avant d'essayer de vous vérifier à nouveau.
|
||||
- '%footer%'
|
||||
# Message de déconnexion affiché lorsque quelqu'un rejoint mais est déjà en vérification
|
||||
already-verifying:
|
||||
- '%header%'
|
||||
- <red>Votre adresse IP est actuellement en cours de vérification.
|
||||
- <red>Veuillez patienter quelques secondes avant d'essayer de vous vérifier à nouveau.
|
||||
- '%footer%'
|
||||
# Message de déconnexion affiché lorsque quelqu'un rejoint mais est déjà en attente de vérification
|
||||
already-queued:
|
||||
- '%header%'
|
||||
- <red>Votre adresse IP est actuellement en attente de vérification.
|
||||
- <red>Veuillez patienter quelques minutes avant d'essayer de vous vérifier à nouveau.
|
||||
- '%footer%'
|
||||
# Message de déconnexion affiché lorsque quelqu'un rejoint mais est temporairement mis sur liste noire
|
||||
blacklisted:
|
||||
- '%header%'
|
||||
- <red>Vous êtes actuellement interdit d'entrer sur le serveur.
|
||||
- <red>Veuillez patienter quelques minutes pour pouvoir rejoindre le serveur à nouveau.
|
||||
- <gold>Erreur ? <gray>%support-url%
|
||||
- '%footer%'
|
||||
# Message de déconnexion affiché lorsque quelqu'un rejoint avec un nom d'utilisateur non valide
|
||||
invalid-username:
|
||||
- '%header%'
|
||||
- <red>Votre nom d'utilisateur contient des caractères non valides.
|
||||
- '%footer%'
|
||||
# Message de déconnexion affiché lorsque quelqu'un rejoint avec une version sur liste noire
|
||||
blacklisted-protocol:
|
||||
- '%header%'
|
||||
- <red>Vous utilisez une version non autorisée sur notre serveur.
|
||||
- <gold>Besoin d'aide pour vous connecter ? <gray>%support-url%
|
||||
- '%footer%'
|
||||
# Message de déconnexion affiché lorsque quelqu'un réussit la vérification
|
||||
success:
|
||||
- '%header%'
|
||||
- <green>Vous avez réussi la vérification.
|
||||
- <white>Vous pouvez maintenant jouer sur le serveur lorsque vous vous reconnectez.
|
||||
# Message de déconnexion affiché lorsque quelqu'un échoue à la vérification
|
||||
failed:
|
||||
- '%header%'
|
||||
- <red>Vous avez échoué à la vérification du bot.
|
||||
- <gray>Veuillez patienter quelques secondes avant d'essayer de vous vérifier à nouveau.
|
||||
- <gold>Besoin d'aide ? <gray>%support-url%
|
||||
- '%footer%'
|
||||
|
||||
# Translations for Sonar's actionbar verbose messages
|
||||
verbose:
|
||||
layout:
|
||||
# Mise en page pour le verbeux de la barre d'action de Sonar
|
||||
# Espacements réservés :
|
||||
# - '%queued%' Nombre de connexions en attente
|
||||
# - '%verifying%' Nombre de connexions en cours de vérification
|
||||
# - '%blacklisted%' Nombre d'adresses IP sur liste noire
|
||||
# - '%total-joins%' Nombre total de connexions tentées
|
||||
# - '%verify-total%' Nombre total de tentatives de vérification
|
||||
# - '%verify-success%' Nombre d'adresses IP vérifiées
|
||||
# - '%verify-failed%' Nombre de vérifications échouées
|
||||
# - '%logins-per-second%' Nombre de connexions par seconde
|
||||
# - '%connections-per-second%' Nombre de connexions par seconde
|
||||
# - '%attack-duration%' Durée de l'attaque actuelle
|
||||
# - '%incoming-traffic%' Utilisation de la bande passante entrante par seconde
|
||||
# - '%outgoing-traffic%' Utilisation de la bande passante sortante par seconde
|
||||
# - '%incoming-traffic-ttl%' Utilisation totale de la bande passante entrante
|
||||
# - '%outgoing-traffic-ttl%' Utilisation totale de la bande passante sortante
|
||||
# - '%used-memory%' Quantité de mémoire utilisée par le processus
|
||||
# - '%total-memory%' Quantité de mémoire totale du processus
|
||||
# - '%max-memory%' Quantité de mémoire maximale du processus
|
||||
# - '%free-memory%' Quantité de mémoire libre du processus
|
||||
# - '%animation%' Symbole animé personnalisable
|
||||
# Traductions pour la sortie verbeuse normale de Sonar
|
||||
normal: '%prefix%<gray>CPS <white>%connections-per-second% <dark_aqua>╺ <gray>Logins/s
|
||||
<white>%logins-per-second% <dark_aqua>╺ <gray>Vérification <white>%verifying% <dark_aqua>|
|
||||
<green>⬆ <white>%outgoing-traffic%/s <red>⬇ <white>%incoming-traffic%/s <green><bold>%animation%<reset>'
|
||||
# Traductions pour la sortie verbeuse lors d'une attaque
|
||||
attack: '%prefix%<gray>CPS <white>%connections-per-second% <dark_aqua>╺ <gray>Logins/s
|
||||
<white>%logins-per-second% <dark_aqua>╺ <gray>Queue <white>%queued% <dark_aqua>╺
|
||||
<gray>Vérification <white>%verifying% <dark_aqua>╺ <gray>Durée <white>%attack-duration%
|
||||
<dark_aqua>| <green>⬆ <white>%outgoing-traffic%/s <red>⬇ <white>%incoming-traffic%/s <green><bold>%animation%<reset>'
|
||||
# Animation pour la sortie verbeuse
|
||||
# Alternatives:
|
||||
# - ▙, ▛, ▜, ▟
|
||||
# - ⬈, ⬊, ⬋, ⬉
|
||||
animation:
|
||||
- ◜
|
||||
- ◝
|
||||
- ◞
|
||||
- ◟
|
||||
|
||||
# Traductions pour les messages affichés en cas d'attaque
|
||||
notifications:
|
||||
# Mise en page du titre de la notification d'attaque de Sonar
|
||||
title: <yellow><b>Sonar<reset>
|
||||
# Mise en page du sous-titre de la notification d'attaque de Sonar
|
||||
subtitle: <gray>A bot attack has been detected
|
||||
# Mise en page de la notification d'attaque de Sonar dans le chat
|
||||
chat:
|
||||
- ''
|
||||
- <yellow><b>Sonar<reset>
|
||||
- ''
|
||||
- <gray>Une attaque de bot a été détectée.
|
||||
- ' <dark_aqua>▪ <gray>Connexions par seconde: <white>%logins-per-second%'
|
||||
- ' <dark_aqua>▪ <gray>Vérification des joueurs: <white>%verifying%'
|
||||
- ' <dark_aqua>▪ <gray>Joueurs en file d''attente: <white>%queued%'
|
||||
- ''
|
||||
- <gray>View more information using <white><click:run_command:'/sonar verbose'><hover:show_text:'(Click
|
||||
to run)'>/sonar verbose</hover></click><gray> or <white><click:run_command:'/sonar
|
||||
stats'><hover:show_text:'(Click to run)'>/sonar stats</hover></click><gray>.
|
||||
- ''
|
||||
@@ -294,7 +294,7 @@ notifications:
|
||||
# Lay-out voor de titel van de aanval meldingen van Sonar
|
||||
title: <yellow><b>Sonar<reset>
|
||||
# Lay-out voor de ondertitel van Sonar's aanval meldingen
|
||||
subtitle: <gray>A bot attack has been detected
|
||||
subtitle: <gray>Er is een bot-aanval gedetecteerd
|
||||
# Lay-out voor de aanval meldingen van Sonar in de chat
|
||||
chat:
|
||||
- ''
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
#
|
||||
# /$$$$$$
|
||||
# /$$__ $$
|
||||
# | $$ \__/ /$$$$$$ /$$$$$$$ /$$$$$$ /$$$$$$
|
||||
# | $$$$$$ /$$__ $$| $$__ $$ |____ $$ /$$__ $$
|
||||
# \____ $$| $$ \ $$| $$ \ $$ /$$$$$$$| $$ \__/
|
||||
# /$$ \ $$| $$ | $$| $$ | $$ /$$__ $$| $$
|
||||
# | $$$$$$/| $$$$$$/| $$ | $$| $$$$$$$| $$
|
||||
# \______/ \______/ |__/ |__/ \_______/|__/
|
||||
#
|
||||
# Besoin d'aide ou d'autres questions ? https://jonesdev.xyz/discord
|
||||
# Code source : https://github.com/jonesdevelopment/sonar
|
||||
# Droit d'auteur Jones Development, un contributeur Sonar, licence sous GPL-3.0
|
||||
#
|
||||
|
||||
# Les notifications d'attaque bot peuvent être envoyé à discord via un webhook
|
||||
webhook:
|
||||
# Lien du webhook discord (Mettez '' pour désactiver les webhooks)
|
||||
url: ''
|
||||
# Nom d'utilisateur du webhook
|
||||
username: Sonar
|
||||
# Lien vers l'avatar du webhook (Mettez '' pour désactiver)
|
||||
avatar-url: ''
|
||||
# Message à envoyer (Mettez '' pour désactiver)
|
||||
# Vous pouvez mentionner des personnes en utilisant <@idUtilisateur>
|
||||
# Si vous voulez mentionner un rôle vous devriez utiliser <@&idRole>
|
||||
content: ''
|
||||
# Embed qui sera envoyé quand il y a une attaque
|
||||
embed:
|
||||
# Petit footer de l'embed
|
||||
footer:
|
||||
# Message du footer
|
||||
text: © Jones Development et Contributeurs Sonar
|
||||
# Lien de l'icône du footer
|
||||
icon-url: ''
|
||||
# Titre du webhook discord
|
||||
title: '✅ Attaque mitigé'
|
||||
# Lien clickable du titre
|
||||
title-url: ''
|
||||
# Description (message) de l'embed
|
||||
description:
|
||||
- 'L''attaque sur votre serveur a été mitigé'
|
||||
- ''
|
||||
- 'Démarré à: <t:%start-timestamp%:T>'
|
||||
- 'Fini à: <t:%end-timestamp%:T>'
|
||||
- 'Durée: %duration%'
|
||||
- ''
|
||||
- 'Pique du processeur: %peak-cpu%%'
|
||||
- 'Pique de la mémoire ram: %peak-memory%'
|
||||
- 'Pique du nombre de bots par secondes: %peak-bps%'
|
||||
- ''
|
||||
- "IP Blacklisté durant l'attaque: %total-blacklisted%"
|
||||
- "Vérification échoué durant l'attaque: %total-failed%"
|
||||
- "Vérification réussi durant l'attaque: %total-success%"
|
||||
# Code couleur RGB pour l'embed
|
||||
# Sélectionner la couleur: https://www.rapidtables.com/web/color/RGB_Color.html
|
||||
color:
|
||||
red: 0
|
||||
green: 255
|
||||
blue: 0
|
||||
@@ -1,17 +1,12 @@
|
||||
val velocityVersion = "3.3.0-SNAPSHOT"
|
||||
|
||||
dependencies {
|
||||
implementation(project(":api"))
|
||||
implementation(project(":common"))
|
||||
|
||||
compileOnly("com.velocitypowered:velocity-proxy:$velocityVersion")
|
||||
testCompileOnly("com.velocitypowered:velocity-proxy:$velocityVersion")
|
||||
compileOnly(rootProject.libs.velocity)
|
||||
testCompileOnly(rootProject.libs.velocity)
|
||||
|
||||
// Implement bStats.org for metrics
|
||||
implementation("org.bstats:bstats-velocity:3.0.2")
|
||||
|
||||
// Library/dependency loading
|
||||
implementation("com.alessiodp.libby:libby-velocity:2.0.0-SNAPSHOT")
|
||||
implementation(rootProject.libs.bstats.velocity)
|
||||
implementation(rootProject.libs.libby.velocity)
|
||||
}
|
||||
|
||||
tasks {
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
package xyz.jonesdev.sonar.velocity;
|
||||
|
||||
import com.alessiodp.libby.VelocityLibraryManager;
|
||||
import com.velocitypowered.api.proxy.Player;
|
||||
import lombok.Getter;
|
||||
import net.kyori.adventure.audience.Audience;
|
||||
import org.bstats.charts.SimplePie;
|
||||
@@ -31,8 +30,6 @@ import xyz.jonesdev.sonar.common.boot.SonarBootstrap;
|
||||
import xyz.jonesdev.sonar.velocity.command.VelocitySonarCommand;
|
||||
import xyz.jonesdev.sonar.velocity.fallback.FallbackInjectionHelper;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
|
||||
@Getter
|
||||
@@ -40,9 +37,9 @@ public final class SonarVelocity extends SonarBootstrap<SonarVelocityPlugin> {
|
||||
public static SonarVelocity INSTANCE;
|
||||
|
||||
public SonarVelocity(final @NotNull SonarVelocityPlugin plugin) {
|
||||
super(plugin, new VelocityLibraryManager<>(
|
||||
plugin, plugin.getLogger(), plugin.getDataDirectory(), plugin.getServer().getPluginManager()),
|
||||
plugin.getDataDirectory().toFile(), SonarPlatform.VELOCITY);
|
||||
super(plugin, SonarPlatform.VELOCITY, plugin.getDataDirectory().toFile(),
|
||||
new VelocityLibraryManager<>(plugin, plugin.getLogger(),
|
||||
plugin.getDataDirectory(), plugin.getServer().getPluginManager()));
|
||||
INSTANCE = this;
|
||||
}
|
||||
|
||||
@@ -57,18 +54,6 @@ public final class SonarVelocity extends SonarBootstrap<SonarVelocityPlugin> {
|
||||
return getPlugin().getServer().getPlayer(uniqueId).orElse(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasTooManyAccounts(final @NotNull InetAddress inetAddress, final int limit) {
|
||||
int count = 1;
|
||||
for (final Player player : getPlugin().getServer().getAllPlayers()) {
|
||||
// Check if the IP address of the player is equal to the IP trying to connect
|
||||
if (!Objects.equals(player.getRemoteAddress().getAddress(), inetAddress)) continue;
|
||||
// Increment count of duplicated accounts
|
||||
if (++count >= limit) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a wrapper for the plugin logger, so we can use it outside
|
||||
* the velocity module.
|
||||
@@ -115,7 +100,9 @@ public final class SonarVelocity extends SonarBootstrap<SonarVelocityPlugin> {
|
||||
|
||||
@Override
|
||||
public void disable() {
|
||||
// Make sure to properly shutdown bStats metrics
|
||||
metrics.shutdown();
|
||||
if (metrics != null) {
|
||||
// Make sure to properly shutdown bStats metrics
|
||||
metrics.shutdown();
|
||||
}
|
||||
}
|
||||
}
|
||||
+13
-11
@@ -38,17 +38,19 @@ public final class FallbackChannelHandler extends FallbackChannelHandlerAdapter
|
||||
@Override
|
||||
public void channelRead(final @NotNull ChannelHandlerContext ctx, final Object msg) throws Exception {
|
||||
// Intercept any handshake packet by the client
|
||||
if (msg instanceof HandshakePacket handshake) {
|
||||
handleHandshake(handshake.getServerAddress(), handshake.getProtocolVersion().getProtocol());
|
||||
}
|
||||
// Intercept any server login packet by the client
|
||||
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 (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;
|
||||
}
|
||||
}
|
||||
// Make sure to let the server handle the rest
|
||||
ctx.fireChannelRead(msg);
|
||||
|
||||
+7
-5
@@ -22,19 +22,21 @@ import io.netty.channel.ChannelInitializer;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import xyz.jonesdev.sonar.api.ReflectiveOperationException;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.invoke.MethodHandle;
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.lang.invoke.MethodType;
|
||||
|
||||
import static com.velocitypowered.proxy.network.Connections.MINECRAFT_DECODER;
|
||||
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_HANDLER;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public final class FallbackChannelInitializer extends ChannelInitializer<Channel> {
|
||||
private static final Method INIT_CHANNEL_METHOD;
|
||||
private static final MethodHandle INIT_CHANNEL_METHOD;
|
||||
|
||||
static {
|
||||
try {
|
||||
INIT_CHANNEL_METHOD = ChannelInitializer.class.getDeclaredMethod("initChannel", Channel.class);
|
||||
INIT_CHANNEL_METHOD.setAccessible(true);
|
||||
INIT_CHANNEL_METHOD = MethodHandles.privateLookupIn(ChannelInitializer.class, MethodHandles.lookup())
|
||||
.findVirtual(ChannelInitializer.class, "initChannel", MethodType.methodType(void.class, Channel.class));
|
||||
} catch (Throwable throwable) {
|
||||
throw new ReflectiveOperationException(throwable);
|
||||
}
|
||||
@@ -46,7 +48,7 @@ public final class FallbackChannelInitializer extends ChannelInitializer<Channel
|
||||
protected void initChannel(final Channel channel) throws Exception {
|
||||
// Invoke the original method
|
||||
try {
|
||||
INIT_CHANNEL_METHOD.invoke(originalChannelInitializer, channel);
|
||||
INIT_CHANNEL_METHOD.invokeExact(originalChannelInitializer, channel);
|
||||
} catch (Throwable throwable) {
|
||||
throw new ReflectiveOperationException(throwable);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user