mirror of
https://github.com/jonesdevelopment/sonar.git
synced 2026-11-05 09:20:15 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
21eb949bae | ||
|
|
3a978b39ee | ||
|
|
efc435df61 | ||
|
|
8baa920279 | ||
|
|
647931c31e | ||
|
|
c8c3685355 | ||
|
|
b759082416 | ||
|
|
31a09d60ac | ||
|
|
45fb866c1c | ||
|
|
9f8d828622 | ||
|
|
6e78c875a2 | ||
|
|
18486b9286 | ||
|
|
644d1387ce | ||
|
|
e80311bf24 | ||
|
|
f42dd4d39f | ||
|
|
7717fb26a0 | ||
|
|
97ff082fb4 | ||
|
|
9bdb884302 | ||
|
|
19d2c141de | ||
|
|
5ea02039f2 | ||
|
|
5a5c91b52d | ||
|
|
128e4aa0ca | ||
|
|
504e5f86ae | ||
|
|
932849ba22 | ||
|
|
6ba8148604 | ||
|
|
3fd23cb853 | ||
|
|
bed374e0f8 | ||
|
|
bb3706a949 | ||
|
|
4fa3a0a313 | ||
|
|
62ce6ded18 | ||
|
|
7d45b2d0db | ||
|
|
b578f813dc | ||
|
|
2e0f1ad8ff | ||
|
|
dd21a7df58 | ||
|
|
56d5b47bfe | ||
|
|
931cba82ed | ||
|
|
a1fffe2e74 | ||
|
|
263060e517 | ||
|
|
cffe9c4dd7 | ||
|
|
d96fcca78e | ||
|
|
0027f5988b | ||
|
|
d4d3b91240 | ||
|
|
ebc8ade9e4 | ||
|
|
ef846b137b | ||
|
|
0107a7e569 | ||
|
|
1055f695c6 | ||
|
|
b8bb1839a2 | ||
|
|
1d852e4eb2 | ||
|
|
4d35634caa | ||
|
|
15af0f3823 | ||
|
|
998a248819 | ||
|
|
3846007613 | ||
|
|
c5446f9507 | ||
|
|
5a13b54350 | ||
|
|
d516da7f9e | ||
|
|
a58b8c8a83 | ||
|
|
3be87006aa | ||
|
|
c70c81eb11 | ||
|
|
d71334c1b4 | ||
|
|
59f141d1be | ||
|
|
9bc3d73cb7 | ||
|
|
d3aaba4650 | ||
|
|
68d99dd59b | ||
|
|
5f77f21ee9 | ||
|
|
c5e1d12424 | ||
|
|
25dbbf0b1f | ||
|
|
cbf04f6f2e | ||
|
|
8966d7d0e8 | ||
|
|
064a3cee2a | ||
|
|
260af8b2be | ||
|
|
a7d52467b8 | ||
|
|
0ed6b0351c | ||
|
|
a152633b41 | ||
|
|
f618d1ce77 | ||
|
|
ea35904d33 | ||
|
|
ececb66bf1 | ||
|
|
6caa37e3e0 | ||
|
|
a057199e0b | ||
|
|
d0bbcdc600 | ||
|
|
0a5ce856f3 | ||
|
|
261acbd9e2 | ||
|
|
595b1378ab | ||
|
|
3012ed0d7a | ||
|
|
4ccecde901 | ||
|
|
87fcfb68f3 | ||
|
|
a17b726798 | ||
|
|
50d3b43f63 | ||
|
|
0ff0194894 | ||
|
|
279fe2e3e9 | ||
|
|
ffd3a7e5c2 | ||
|
|
4474f44b0a | ||
|
|
b159947d6d | ||
|
|
a29dfa15e4 | ||
|
|
cb9fe75db8 |
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.
|
||||
+15
-33
@@ -1,12 +1,11 @@
|
||||
<div align="center">
|
||||
<!-- Introduction -->
|
||||
<p>
|
||||
<h1>💫 Sonar</h1>
|
||||
Lightweight and easy-to-use anti-bot plugin for your Minecraft server. Supporting every client and server version.
|
||||
<br>
|
||||
An effective and extensible solution for protecting your Minecraft server against all kinds of bot attacks
|
||||
</p>
|
||||
|
||||
<h1>💫 Sonar</h1>
|
||||
Lightweight and easy-to-use anti-bot plugin for your Minecraft server.
|
||||
<br>
|
||||
Sonar keeps your server safe from common and even sophisticated bot attacks.
|
||||
<br><br>
|
||||
|
||||
<!-- Badges & icons -->
|
||||
[](https://github.com/jonesdevelopment/sonar/actions/workflows/gradle.yml)
|
||||
[](https://www.codefactor.io/repository/github/jonesdevelopment/sonar/overview/main)
|
||||
@@ -15,7 +14,6 @@
|
||||
[](https://jonesdev.xyz/discord)
|
||||
[](https://www.gnu.org/licenses/gpl-3.0)
|
||||
<br>
|
||||
<br>
|
||||
<!-- Quick navigation -->
|
||||
[Releases](https://github.com/jonesdevelopment/sonar/releases)
|
||||
|
|
||||
@@ -29,7 +27,6 @@
|
||||
</div>
|
||||
|
||||
## Design and Goal
|
||||
|
||||
* Effective, lightweight, and easy-to-use
|
||||
* No unnecessary features and clean code
|
||||
* Protection against all kinds of bot attacks
|
||||
@@ -38,44 +35,29 @@
|
||||
* Multi-platform support (See [supported versions](https://docs.jonesdev.xyz/sonar/supported-versions))
|
||||
|
||||
## Checks
|
||||
Sonar analyzes a player's behavior before joining the actual server, therefore stopping malicious traffic from ever reaching the backend. It is supposed to be an instant, powerful, and simple method of verification that should prevent all typical and advanced types of bots.
|
||||
|
||||
- [Fallback](https://github.com/jonesdevelopment/sonar#fallback) is Sonar's main anti-bot component
|
||||
- Sonar queues new player logins to prevent spam login attacks
|
||||
- Sonar checks the handshake packets for legitimacy
|
||||
- Sonar makes sure some packets cannot be duplicated illegitimately
|
||||
1. Sonar sends the player to a lightweight fake server when they connect for the first time.
|
||||
2. Sonar verifies that players obey the laws of Minecraft's physics, including gravity and proper block collision.
|
||||
3. Sonar verifies that players send legitimate packets when interacting with vehicles (e.g. boats).
|
||||
4. Sonar makes sure that players send legitimate packets according to the [vanilla Minecraft protocol](<https://wiki.vg/Protocol>).
|
||||
|
||||
### Fallback
|
||||
|
||||
Fallback analyzes a player's behavior before joining the actual server, therefore stopping malicious traffic from ever reaching the backend.
|
||||
It is supposed to be an instant, powerful, and invisible method of verification that should prevent all typical and advanced types of bots.
|
||||
|
||||
* Sends the player to a lightweight fake server when they connect for the first time.
|
||||
* Analyzes if the player is sending the necessary packets.
|
||||
* Analyzes if the player is sending legitimate packets.
|
||||
* Checks if the player is obeying client gravity.
|
||||
* Checks if the player is colliding with blocks correctly.
|
||||
|
||||
Fallback also protects against huge spambot attacks since it queues the incoming connections, therefore making it technically impossible to have a ton of bots join the server at the same time.
|
||||
Sonar also protects against spambot attacks since it queues the incoming connections, therefore making it technically impossible to have a ton of bots join the server at the same time.
|
||||
|
||||
### False positives
|
||||
|
||||
Fallback is unlikely to ever falsely prevent a player from joining the server since Minecraft uses the TCP protocol, which means that packets are always sent in the correct order. Therefore, lag or ping should not affect the bot check. However, there are some edge cases where Fallback might not receive packets within the necessary time period. In this case, Sonar tries to account for these edge cases in order to prevent false positives. If you or one of your players experiences a false positive, make sure to report it by opening a [GitHub issue](https://github.com/jonesdevelopment/sonar/issues) or a ticket on [Discord](https://jonesdev.xyz/discord/).
|
||||
Sonar is unlikely to ever falsely prevent a player from joining the server since Minecraft uses the TCP protocol, which means that packets are always sent in the correct order. Therefore, lag or ping should not affect the bot check.
|
||||
If you or one of your players experiences a false positive, make sure to report it by opening a [GitHub issue](https://github.com/jonesdevelopment/sonar/issues/new/choose) or a ticket on the [Discord server](https://jonesdev.xyz/discord/).
|
||||
|
||||
## 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.
|
||||
|
||||
## Contributing
|
||||
|
||||
If you are interested in contributing, you can check out the [Contributing Guidelines](https://github.com/jonesdevelopment/sonar/blob/main/.github/CONTRIBUTING.md) for detailed instructions.
|
||||
|
||||
## License
|
||||
|
||||
Sonar is licensed under the [GNU General Public License 3.0](https://www.gnu.org/licenses/gpl-3.0.en.html).
|
||||
|
||||
## Credits
|
||||
|
||||
- Special thanks to the [contributors of Sonar](https://github.com/jonesdevelopment/sonar/graphs/contributors).
|
||||
- The Varint decoding was taken from [Velocity](https://github.com/PaperMC/Velocity).
|
||||
+28
-31
@@ -1,26 +1,14 @@
|
||||
buildscript {
|
||||
dependencies {
|
||||
classpath("gradle.plugin.io.toolebox:gradle-git-versioner:1.6.7")
|
||||
}
|
||||
}
|
||||
import net.kyori.indra.git.IndraGitExtension
|
||||
|
||||
plugins {
|
||||
id("java")
|
||||
id("io.github.goooler.shadow") version "8.1.7"
|
||||
id("io.toolebox.git-versioner") version "1.6.7"
|
||||
}
|
||||
|
||||
apply(plugin = "io.toolebox.git-versioner")
|
||||
|
||||
versioner {
|
||||
pattern {
|
||||
pattern = "$version-%h-%c-%b"
|
||||
}
|
||||
java
|
||||
alias(libs.plugins.shadow)
|
||||
alias(libs.plugins.indra.git) apply true
|
||||
}
|
||||
|
||||
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 +17,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.108.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 +52,11 @@ allprojects {
|
||||
|
||||
// Exclude unnecessary metadata information
|
||||
exclude("META-INF/*/**")
|
||||
|
||||
// Minimize shadowed jar file
|
||||
minimize {
|
||||
exclude(project(":api"))
|
||||
}
|
||||
}
|
||||
|
||||
compileJava {
|
||||
@@ -76,10 +65,18 @@ allprojects {
|
||||
|
||||
jar {
|
||||
manifest {
|
||||
val indra = rootProject.extensions.getByType(IndraGitExtension::class.java)
|
||||
val gitBranch = indra.branchName() ?: "unknown"
|
||||
val gitCommit = indra.commit()?.name?.substring(0, 8) ?: "unknown"
|
||||
|
||||
// Set the implementation version, so we can create exact version
|
||||
// information in-game and make it accessible to the user.
|
||||
attributes["Implementation-Title"] = rootProject.name
|
||||
attributes["Implementation-Version"] = rootProject.version
|
||||
attributes["Implementation-Vendor"] = "Jones Development, Sonar Contributors"
|
||||
// Include the Git branch and Git commit SHA
|
||||
attributes["Git-Branch"] = gitBranch
|
||||
attributes["Git-Commit"] = gitCommit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
group=xyz.jonesdev.sonar
|
||||
version=2.0.25
|
||||
description=Effective Anti-bot plugin for Velocity, BungeeCord, and Bukkit (1.7-latest)
|
||||
version=2.0.35
|
||||
description=A lightweight, effective, and easy-to-use anti-bot plugin for Velocity, BungeeCord, and Bukkit.
|
||||
@@ -0,0 +1,35 @@
|
||||
[versions]
|
||||
netty = "4.1.111.Final"
|
||||
adventure = "4.17.0"
|
||||
adventure-platform = "4.3.3"
|
||||
bstats = "3.0.2"
|
||||
libby = "2.0.0-SNAPSHOT"
|
||||
|
||||
[plugins]
|
||||
indra-git = "net.kyori.indra.git:3.1.3"
|
||||
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"
|
||||
Vendored
BIN
Binary file not shown.
+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.8-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,7 +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.text.DecimalFormat;
|
||||
import java.util.UUID;
|
||||
|
||||
@@ -57,8 +56,6 @@ public interface Sonar {
|
||||
*/
|
||||
@NotNull LoggerWrapper getLogger();
|
||||
|
||||
@NotNull File getDataDirectory();
|
||||
|
||||
@NotNull SonarConfiguration getConfig();
|
||||
|
||||
@NotNull SubcommandRegistry getSubcommandRegistry();
|
||||
@@ -92,7 +89,7 @@ public interface Sonar {
|
||||
|
||||
@NotNull
|
||||
default SonarVersion getVersion() {
|
||||
return SonarVersion.GET;
|
||||
return SonarVersion.INSTANCE;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
|
||||
@@ -18,35 +18,49 @@
|
||||
package xyz.jonesdev.sonar.api;
|
||||
|
||||
import lombok.Getter;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URL;
|
||||
import java.util.jar.Manifest;
|
||||
|
||||
@Getter
|
||||
public final class SonarVersion {
|
||||
static final SonarVersion GET = new SonarVersion();
|
||||
private final String semanticVersion, full, formatted, commitSHA;
|
||||
private final int build;
|
||||
private final boolean isOnMainBranch;
|
||||
static final SonarVersion INSTANCE = new SonarVersion();
|
||||
|
||||
private final String version, formatted, gitBranch, gitCommit;
|
||||
|
||||
SonarVersion() {
|
||||
final Package manifest = Sonar.class.getPackage();
|
||||
final String versionString = manifest.getImplementationVersion();
|
||||
// No need to null-check since we'll always just throw an exception if
|
||||
// we can't find the version information in this file.
|
||||
final Manifest manifest = getManifest();
|
||||
|
||||
if (versionString == null) {
|
||||
this.full = "<could not retrieve version>";
|
||||
this.semanticVersion = "<unknown version>";
|
||||
this.commitSHA = "<unknown commit>";
|
||||
this.build = -1;
|
||||
this.isOnMainBranch = false;
|
||||
this.formatted = full;
|
||||
Sonar.get().getLogger().warn("Could not find version information. Is the manifest missing?");
|
||||
return;
|
||||
this.version = manifest.getMainAttributes().getValue("Implementation-Version");
|
||||
this.gitBranch = manifest.getMainAttributes().getValue("Git-Branch");
|
||||
this.gitCommit = manifest.getMainAttributes().getValue("Git-Commit");
|
||||
this.formatted = version + " (" + gitCommit + ")";
|
||||
}
|
||||
|
||||
// Taken from
|
||||
// https://github.com/PaperMC/Velocity/pull/1336/
|
||||
private static @NotNull Manifest getManifest() {
|
||||
final String classLocation = "/" + Sonar.class.getName().replace(".", "/") + ".class";
|
||||
final URL resource = Sonar.class.getResource(classLocation);
|
||||
|
||||
if (resource == null) {
|
||||
throw new IllegalStateException("Could not find version information. Is the manifest missing?");
|
||||
}
|
||||
|
||||
this.full = versionString;
|
||||
this.semanticVersion = versionString.split("-")[0];
|
||||
this.commitSHA = versionString.split("-")[1];
|
||||
this.build = Integer.parseInt(versionString.split("-")[2]);
|
||||
this.isOnMainBranch = versionString.split("-")[3].equals("main");
|
||||
this.formatted = semanticVersion + " build " + build + " (" + commitSHA + ")";
|
||||
final String classFilePath = resource.toString().replace("\\", "/");
|
||||
final String archivePath = classFilePath.substring(0, classFilePath.length() - classLocation.length());
|
||||
final String manifestPath = archivePath + "/META-INF/MANIFEST.MF";
|
||||
|
||||
try (final InputStream stream = new URL(manifestPath).openStream()) {
|
||||
return new Manifest(stream);
|
||||
} catch (IOException exception) {
|
||||
throw new IllegalStateException(exception);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -53,7 +53,7 @@ public final class AttackTracker {
|
||||
public void checkIfUnderAttack() {
|
||||
final long joinsPerSecond = Sonar.get().getStatistics().getLoginsPerSecond();
|
||||
final int verifyingPlayers = Sonar.get().getFallback().getConnected().size();
|
||||
final int queuedPlayers = Sonar.get().getFallback().getQueue().getQueuedPlayers().size();
|
||||
final int queuedPlayers = Sonar.get().getFallback().getQueue().getPlayers().size();
|
||||
final int minPlayers = Sonar.get().getConfig().getMinPlayersForAttack();
|
||||
|
||||
if (joinsPerSecond > minPlayers // Check the number of bots/joins per second.
|
||||
@@ -68,7 +68,7 @@ public final class AttackTracker {
|
||||
currentAttack.successfulVerifications = Sonar.get().getVerifiedPlayerController().estimatedSize();
|
||||
currentAttack.failedVerifications = Sonar.get().getStatistics().getTotalFailedVerifications();
|
||||
Sonar.get().getEventManager().publish(new AttackDetectedEvent());
|
||||
Sonar.get().getNotificationHandler().sendAttackNotification();
|
||||
Sonar.get().getNotificationHandler().observe();
|
||||
} else {
|
||||
// Reset attack timer if we're still under attack
|
||||
currentAttack.timer.reset();
|
||||
@@ -125,16 +125,16 @@ public final class AttackTracker {
|
||||
webhook.post(() -> {
|
||||
final SonarConfiguration.Webhook.Embed config = Sonar.get().getConfig().getWebhook().getEmbed();
|
||||
final String description = config.getDescription()
|
||||
.replace("%start-timestamp%", startTimestamp)
|
||||
.replace("%end-timestamp%", endTimestamp)
|
||||
.replace("%duration%", formattedDuration)
|
||||
.replace("%peak-cpu%", peakCPU)
|
||||
.replace("%peak-memory%", peakMem)
|
||||
.replace("%peak-bps%", peakBPS)
|
||||
.replace("%peak-cps%", peakCPS)
|
||||
.replace("%total-blacklisted%", Sonar.DECIMAL_FORMAT.format(blacklisted))
|
||||
.replace("%total-failed%", Sonar.DECIMAL_FORMAT.format(failed))
|
||||
.replace("%total-success%", Sonar.DECIMAL_FORMAT.format(verified));
|
||||
.replace("<start-timestamp>", startTimestamp)
|
||||
.replace("<end-timestamp>", endTimestamp)
|
||||
.replace("<attack-duration>", formattedDuration)
|
||||
.replace("<peak-cpu>", peakCPU)
|
||||
.replace("<peak-memory>", peakMem)
|
||||
.replace("<peak-bps>", peakBPS)
|
||||
.replace("<peak-cps>", peakCPS)
|
||||
.replace("<total-blacklisted>", Sonar.DECIMAL_FORMAT.format(blacklisted))
|
||||
.replace("<total-failed>", Sonar.DECIMAL_FORMAT.format(failed))
|
||||
.replace("<total-success>", Sonar.DECIMAL_FORMAT.format(verified));
|
||||
return new SonarConfiguration.Webhook.Embed(
|
||||
config.getTitle(), config.getTitleUrl(), description, config.getR(), config.getG(), config.getB());
|
||||
});
|
||||
@@ -151,10 +151,4 @@ public final class AttackTracker {
|
||||
attackThreshold = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
|
||||
public boolean isCurrentlyUnderAttack() {
|
||||
final int minDelay = Sonar.get().getConfig().getAttackCooldownDelay();
|
||||
return currentAttack != null && currentAttack.timer.delay() < minDelay;
|
||||
}
|
||||
}
|
||||
@@ -20,13 +20,11 @@ package xyz.jonesdev.sonar.api.command;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.ToString;
|
||||
import xyz.jonesdev.sonar.api.command.subcommand.Subcommand;
|
||||
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
@ToString(of = "rawArguments")
|
||||
public final class CommandInvocation {
|
||||
private final InvocationSource sender;
|
||||
private final Subcommand subcommand;
|
||||
private final InvocationSource source;
|
||||
private final String[] rawArguments;
|
||||
}
|
||||
@@ -17,19 +17,17 @@
|
||||
|
||||
package xyz.jonesdev.sonar.api.command;
|
||||
|
||||
import com.github.benmanes.caffeine.cache.Cache;
|
||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||
import net.kyori.adventure.audience.Audience;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.event.ClickEvent;
|
||||
import net.kyori.adventure.text.minimessage.MiniMessage;
|
||||
import net.kyori.adventure.text.minimessage.tag.Tag;
|
||||
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
|
||||
import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.command.subcommand.Subcommand;
|
||||
import xyz.jonesdev.sonar.api.command.subcommand.argument.Argument;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static java.util.Collections.emptyList;
|
||||
|
||||
@@ -38,93 +36,65 @@ public interface SonarCommand {
|
||||
|
||||
Map<String, List<String>> ARG_TAB_SUGGESTIONS = new HashMap<>();
|
||||
|
||||
Cache<Audience, Long> COMMAND_DELAY = Caffeine.newBuilder()
|
||||
.expireAfterWrite(Duration.ofMillis(500))
|
||||
.build();
|
||||
|
||||
List<Component> CACHED_HELP_MESSAGE = new ArrayList<>();
|
||||
|
||||
default void handle(final @NotNull InvocationSource source, final String[] args) {
|
||||
if (source.isPlayer()) {
|
||||
// Check if the player actually has the permission to run the command
|
||||
if (!source.getPermissionFunction().test("sonar.command")) {
|
||||
source.sendMessage(Sonar.get().getConfig().getNoPermission());
|
||||
return;
|
||||
}
|
||||
|
||||
COMMAND_DELAY.cleanUp(); // Clean up the cache
|
||||
|
||||
final long timestamp = System.currentTimeMillis();
|
||||
final long mapTimestamp = COMMAND_DELAY.asMap().getOrDefault(source.getAudience(), -1L);
|
||||
|
||||
// There were some exploits with spamming commands in the past.
|
||||
// Spamming should be prevented, especially if some heavy operations are done,
|
||||
// which is not the case here but let's still stay safe!
|
||||
if (mapTimestamp > 0L) {
|
||||
source.sendMessage(Sonar.get().getConfig().getCommands().getCommandCoolDown());
|
||||
|
||||
// Format delay
|
||||
final double left = 0.5D - (timestamp - mapTimestamp) / 1000D;
|
||||
|
||||
source.sendMessage(Sonar.get().getConfig().getCommands().getCommandCoolDownLeft()
|
||||
.replace("%time-left%", Sonar.DECIMAL_FORMAT.format(left)));
|
||||
return;
|
||||
}
|
||||
|
||||
COMMAND_DELAY.put(source.getAudience(), timestamp);
|
||||
default void handle(final @NotNull InvocationSource source, final String @NotNull [] args) {
|
||||
// Check if the player actually has the permission to run the command
|
||||
if (source.isPlayer() && !source.getPermissionFunction().test("sonar.command")) {
|
||||
source.sendMessage(Sonar.get().getConfig().getNoPermission());
|
||||
return;
|
||||
}
|
||||
|
||||
Optional<Subcommand> subcommand = Optional.empty();
|
||||
|
||||
if (args.length > 0) {
|
||||
// Search subcommand if command arguments are present
|
||||
final Optional<Subcommand> subcommand = Sonar.get().getSubcommandRegistry().getSubcommands().stream()
|
||||
subcommand = Sonar.get().getSubcommandRegistry().getSubcommands().stream()
|
||||
.filter(sub -> sub.getInfo().name().equalsIgnoreCase(args[0])
|
||||
|| Arrays.stream(sub.getInfo().aliases())
|
||||
.anyMatch(alias -> alias.equalsIgnoreCase(args[0])))
|
||||
|| Arrays.stream(sub.getInfo().aliases()).anyMatch(alias -> alias.equalsIgnoreCase(args[0])))
|
||||
.findFirst();
|
||||
}
|
||||
|
||||
subcommand.ifPresentOrElse(command -> {
|
||||
// Check permissions for subcommands
|
||||
if (subcommand.isPresent()) {
|
||||
if (!subcommand.get().getInfo().onlyConsole()
|
||||
&& !source.getPermissionFunction().test(subcommand.get().getPermission())) {
|
||||
source.sendMessage(Sonar.get().getConfig().getCommands().getSubCommandNoPerm()
|
||||
.replace("%permission%", subcommand.get().getPermission()));
|
||||
return;
|
||||
}
|
||||
subcommand.get().invoke(source, args);
|
||||
if (!command.getInfo().onlyConsole()
|
||||
&& !source.getPermissionFunction().test(command.getPermission())) {
|
||||
source.sendMessage(MiniMessage.miniMessage().deserialize(
|
||||
Sonar.get().getConfig().getMessagesConfig().getString("commands.subcommand-no-permission"),
|
||||
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix()),
|
||||
Placeholder.unparsed("permission", command.getPermission())));
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Invoke subcommand
|
||||
command.invoke(source, args);
|
||||
}, () -> {
|
||||
// Print standard help message
|
||||
source.sendMessage(MiniMessage.miniMessage().deserialize(
|
||||
String.join("<newline>",
|
||||
Sonar.get().getConfig().getMessagesConfig().getStringList("commands.main.header")),
|
||||
Placeholder.unparsed("version", Sonar.get().getVersion().getFormatted()),
|
||||
Placeholder.unparsed("platform", Sonar.get().getPlatform().getDisplayName()),
|
||||
Placeholder.unparsed("copyright-year", String.valueOf(Calendar.getInstance().get(Calendar.YEAR)))));
|
||||
|
||||
// Re-use the old, cached help message since we don't want to scan
|
||||
// for each subcommand and it's arguments/attributes every time
|
||||
// someone runs /sonar since the subcommand don't change
|
||||
for (final Component component : CACHED_HELP_MESSAGE) {
|
||||
source.sendMessage(component);
|
||||
}
|
||||
final Component yes = MiniMessage.miniMessage().deserialize(
|
||||
Sonar.get().getConfig().getMessagesConfig().getString("commands.main.tick"));
|
||||
final Component no = MiniMessage.miniMessage().deserialize(
|
||||
Sonar.get().getConfig().getMessagesConfig().getString("commands.main.cross"));
|
||||
|
||||
Sonar.get().getSubcommandRegistry().getSubcommands().forEach(command -> {
|
||||
source.sendMessage(MiniMessage.miniMessage().deserialize(
|
||||
Sonar.get().getConfig().getMessagesConfig().getString("commands.main.subcommands"),
|
||||
TagResolver.resolver("suggest-subcommand",
|
||||
(queue, context) -> Tag.styling(ClickEvent.suggestCommand("/sonar " + command.getInfo().name()))),
|
||||
Placeholder.unparsed("subcommand", command.getInfo().name()),
|
||||
Placeholder.unparsed("description", command.getDescription()),
|
||||
Placeholder.unparsed("permission", command.getPermission()),
|
||||
Placeholder.unparsed("aliases", command.getAliases()),
|
||||
Placeholder.component("only-players", command.getInfo().onlyPlayers() ? yes : no),
|
||||
Placeholder.component("only-console", command.getInfo().onlyConsole() ? yes : no)));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
static void prepareCachedMessages() {
|
||||
// Cache help message
|
||||
CACHED_HELP_MESSAGE.clear();
|
||||
for (final String message : Sonar.get().getConfig().getCommands().getHelpHeader()) {
|
||||
CACHED_HELP_MESSAGE.add(MiniMessage.miniMessage().deserialize(message
|
||||
.replace("%version%", Sonar.get().getVersion().getFormatted())
|
||||
.replace("%platform%", Sonar.get().getPlatform().getDisplayName())
|
||||
.replace("%copyright_year%", String.valueOf(Calendar.getInstance().get(Calendar.YEAR)))));
|
||||
}
|
||||
|
||||
final String subcommandFormat = Sonar.get().getConfig().getCommands().getHelpSubcommands();
|
||||
Sonar.get().getSubcommandRegistry().getSubcommands().forEach(subcommand -> {
|
||||
final Component deserialized = MiniMessage.miniMessage().deserialize(subcommandFormat
|
||||
.replace("%subcommand%", subcommand.getInfo().name())
|
||||
.replace("%description%", subcommand.getInfo().description())
|
||||
.replace("%only_players%", subcommand.getInfo().onlyPlayers() ? "<green>✔</green>" : "<red>✗</red>")
|
||||
.replace("%require_console%", subcommand.getInfo().onlyConsole() ? "<green>✔</green>" : "<red>✗</red>")
|
||||
.replace("%permission%", subcommand.getPermission())
|
||||
.replace("%aliases%", subcommand.getAliases()));
|
||||
CACHED_HELP_MESSAGE.add(deserialized);
|
||||
});
|
||||
|
||||
// Don't re-cache tab suggestions
|
||||
if (!TAB_SUGGESTIONS.isEmpty()) return;
|
||||
// Cache tab suggestions
|
||||
@@ -133,12 +103,10 @@ public interface SonarCommand {
|
||||
if (subcommand.getInfo().aliases().length > 0) {
|
||||
TAB_SUGGESTIONS.addAll(Arrays.asList(subcommand.getInfo().aliases()));
|
||||
}
|
||||
final List<String> parsedArguments = Arrays.stream(subcommand.getInfo().arguments())
|
||||
.map(Argument::value)
|
||||
.collect(Collectors.toList());
|
||||
ARG_TAB_SUGGESTIONS.put(subcommand.getInfo().name(), parsedArguments);
|
||||
final List<String> arguments = Arrays.asList(subcommand.getInfo().arguments());
|
||||
ARG_TAB_SUGGESTIONS.put(subcommand.getInfo().name(), arguments);
|
||||
for (final String alias : subcommand.getInfo().aliases()) {
|
||||
ARG_TAB_SUGGESTIONS.put(alias, parsedArguments);
|
||||
ARG_TAB_SUGGESTIONS.put(alias, arguments);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -147,8 +115,8 @@ public interface SonarCommand {
|
||||
if (arguments.length <= 1) {
|
||||
return TAB_SUGGESTIONS;
|
||||
} else if (arguments.length == 2) {
|
||||
final String subCommandName = arguments[0].toLowerCase();
|
||||
return ARG_TAB_SUGGESTIONS.getOrDefault(subCommandName, emptyList());
|
||||
final String subcommandName = arguments[0].toLowerCase();
|
||||
return ARG_TAB_SUGGESTIONS.getOrDefault(subcommandName, emptyList());
|
||||
}
|
||||
return emptyList();
|
||||
}
|
||||
|
||||
@@ -18,82 +18,85 @@
|
||||
package xyz.jonesdev.sonar.api.command.subcommand;
|
||||
|
||||
import lombok.Getter;
|
||||
import net.kyori.adventure.text.minimessage.MiniMessage;
|
||||
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.command.CommandInvocation;
|
||||
import xyz.jonesdev.sonar.api.command.InvocationSource;
|
||||
import xyz.jonesdev.sonar.api.command.subcommand.argument.Argument;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.Arrays;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.Objects;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@Getter
|
||||
public abstract class Subcommand {
|
||||
private final @NotNull SubcommandInfo info;
|
||||
private final String permission, aliases, arguments;
|
||||
|
||||
protected static final Sonar SONAR = Sonar.get();
|
||||
|
||||
public Subcommand() {
|
||||
info = getClass().getAnnotation(SubcommandInfo.class);
|
||||
permission = "sonar." + info.name();
|
||||
aliases = info.aliases().length == 0 ? "No aliases."
|
||||
this.info = Objects.requireNonNull(getClass().getAnnotation(SubcommandInfo.class));
|
||||
this.permission = "sonar." + info.name();
|
||||
this.aliases = info.aliases().length == 0 ? "No aliases."
|
||||
: String.join(", ", info.aliases());
|
||||
|
||||
arguments = info.arguments().length == 0 ? ""
|
||||
: Arrays.stream(info.arguments())
|
||||
.map(Argument::value)
|
||||
.collect(Collectors.joining(", "));
|
||||
this.arguments = info.arguments().length == 0 ? ""
|
||||
: String.join(", ", info.arguments());
|
||||
}
|
||||
|
||||
protected static @Nullable InetAddress getInetAddressIfValid(final InvocationSource source, final String rawIP) {
|
||||
final InetAddress inetAddress;
|
||||
try {
|
||||
inetAddress = InetAddress.getByName(rawIP);
|
||||
private static final Pattern IPv4_REGEX = Pattern.compile("^((25[0-5]|(2[0-4]|1\\d|[1-9]|)\\d)\\.?\\b){4}$");
|
||||
private static final Pattern IPv6_REGEX = Pattern.compile("(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))");
|
||||
|
||||
if (inetAddress.isAnyLocalAddress() || inetAddress.isLoopbackAddress()) {
|
||||
source.sendMessage(SONAR.getConfig().getCommands().getIllegalIpAddress());
|
||||
return null;
|
||||
}
|
||||
} catch (UnknownHostException exception) {
|
||||
source.sendMessage(SONAR.getConfig().getCommands().getIncorrectIpAddress());
|
||||
protected static @Nullable String validateIP(final InvocationSource source, final String raw) {
|
||||
if (!IPv4_REGEX.matcher(raw).matches() && !IPv6_REGEX.matcher(raw).matches()) {
|
||||
source.sendMessage(MiniMessage.miniMessage().deserialize(
|
||||
Sonar.get().getConfig().getMessagesConfig().getString("commands.invalid-ip-address"),
|
||||
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix())));
|
||||
return null;
|
||||
}
|
||||
return inetAddress;
|
||||
return raw;
|
||||
}
|
||||
|
||||
protected final void incorrectUsage(final @NotNull InvocationSource sender) {
|
||||
sender.sendMessage(SONAR.getConfig().getCommands().getIncorrectCommandUsage()
|
||||
.replace("%usage%", info.name() + " (" + arguments + ")"));
|
||||
protected final void incorrectUsage(final @NotNull InvocationSource invocationSource) {
|
||||
incorrectUsage(invocationSource, info.name() + " (" + arguments + ")");
|
||||
}
|
||||
|
||||
protected final void incorrectUsage(final @NotNull InvocationSource invocationSource, final String usage) {
|
||||
invocationSource.sendMessage(MiniMessage.miniMessage().deserialize(
|
||||
Sonar.get().getConfig().getMessagesConfig().getString("commands.incorrect-usage"),
|
||||
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix()),
|
||||
Placeholder.unparsed("subcommand-usage", usage)));
|
||||
}
|
||||
|
||||
public final void invoke(final @NotNull InvocationSource invocationSource, final String @NotNull [] arguments) {
|
||||
// Check if the subcommand can only be executed by players
|
||||
if (getInfo().onlyPlayers() && !invocationSource.isPlayer()) {
|
||||
invocationSource.sendMessage(Sonar.get().getConfig().getCommands().getPlayersOnly());
|
||||
if (info.onlyPlayers() && !invocationSource.isPlayer()) {
|
||||
invocationSource.sendMessage(MiniMessage.miniMessage().deserialize(
|
||||
Sonar.get().getConfig().getMessagesConfig().getString("commands.player-only"),
|
||||
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix())));
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the subcommand can only be executed though console
|
||||
if (getInfo().onlyConsole() && invocationSource.isPlayer()) {
|
||||
invocationSource.sendMessage(Sonar.get().getConfig().getCommands().getConsoleOnly());
|
||||
if (info.onlyConsole() && invocationSource.isPlayer()) {
|
||||
invocationSource.sendMessage(MiniMessage.miniMessage().deserialize(
|
||||
Sonar.get().getConfig().getMessagesConfig().getString("commands.console-only"),
|
||||
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix())));
|
||||
return;
|
||||
}
|
||||
|
||||
final CommandInvocation commandInvocation = new CommandInvocation(invocationSource, this, arguments);
|
||||
|
||||
// The subcommands has arguments which are not present in the executed command
|
||||
if (getInfo().argumentsRequired() && getInfo().arguments().length > 0 && arguments.length <= 1) {
|
||||
invocationSource.sendMessage(Sonar.get().getConfig().getCommands().getIncorrectCommandUsage()
|
||||
.replace("%usage%", getInfo().name() + " (" + getArguments() + ")"));
|
||||
if (info.argumentsRequired() && info.arguments().length > 0 && arguments.length <= 1) {
|
||||
incorrectUsage(invocationSource);
|
||||
return;
|
||||
}
|
||||
|
||||
// Execute the sub command
|
||||
execute(commandInvocation);
|
||||
// Execute the sub command from the invocation source with the given arguments
|
||||
execute(new CommandInvocation(invocationSource, arguments));
|
||||
}
|
||||
|
||||
public final @NotNull String getDescription() {
|
||||
final String path = String.format("commands.%s.description", info.name());
|
||||
return Objects.requireNonNull(Sonar.get().getConfig().getMessagesConfig().getString(path));
|
||||
}
|
||||
|
||||
protected abstract void execute(final @NotNull CommandInvocation invocation);
|
||||
|
||||
+1
-8
@@ -17,8 +17,6 @@
|
||||
|
||||
package xyz.jonesdev.sonar.api.command.subcommand;
|
||||
|
||||
import xyz.jonesdev.sonar.api.command.subcommand.argument.Argument;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
@@ -33,11 +31,6 @@ public @interface SubcommandInfo {
|
||||
*/
|
||||
String name();
|
||||
|
||||
/**
|
||||
* @return Description of the subcommand
|
||||
*/
|
||||
String description();
|
||||
|
||||
/**
|
||||
* @return Aliases of the subcommand
|
||||
*/
|
||||
@@ -46,7 +39,7 @@ public @interface SubcommandInfo {
|
||||
/**
|
||||
* @return Arguments of the subcommand
|
||||
*/
|
||||
Argument[] arguments() default {};
|
||||
String[] arguments() default {};
|
||||
|
||||
/**
|
||||
* @return Whether the subcommands requires arguments
|
||||
|
||||
+8
-14
@@ -17,30 +17,24 @@
|
||||
|
||||
package xyz.jonesdev.sonar.api.command.subcommand;
|
||||
|
||||
import lombok.Getter;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Unmodifiable;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Vector;
|
||||
|
||||
public interface SubcommandRegistry {
|
||||
List<Subcommand> SUBCOMMANDS = new Vector<>();
|
||||
|
||||
@NotNull
|
||||
@Unmodifiable
|
||||
default List<Subcommand> getSubcommands() {
|
||||
return Collections.unmodifiableList(SUBCOMMANDS);
|
||||
}
|
||||
@Getter
|
||||
public final class SubcommandRegistry {
|
||||
private final List<Subcommand> subcommands = new Vector<>();
|
||||
|
||||
/**
|
||||
* Registers a single or multiple subcommand(s)
|
||||
*
|
||||
* @param subcommand Array of Subcommands to register
|
||||
*/
|
||||
default void register(final Subcommand @NotNull ... subcommand) {
|
||||
SUBCOMMANDS.addAll(Arrays.asList(subcommand));
|
||||
public void register(final Subcommand @NotNull ... subcommand) {
|
||||
subcommands.addAll(Arrays.asList(subcommand));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -49,7 +43,7 @@ public interface SubcommandRegistry {
|
||||
* @param subcommand Array of Subcommands to unregister
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
default void unregister(final Subcommand @NotNull ... subcommand) {
|
||||
SUBCOMMANDS.removeAll(Arrays.asList(subcommand));
|
||||
public void unregister(final Subcommand @NotNull ... subcommand) {
|
||||
subcommands.removeAll(Arrays.asList(subcommand));
|
||||
}
|
||||
}
|
||||
@@ -88,11 +88,6 @@ public final class SimpleYamlConfig {
|
||||
yaml.set(path, v);
|
||||
}
|
||||
|
||||
public int getInt(final String path, final int def) {
|
||||
yaml.addDefault(path, def);
|
||||
return yaml.getInt(path, def);
|
||||
}
|
||||
|
||||
public int getInt(final String path) {
|
||||
if (!yaml.contains(path)) {
|
||||
LOGGER.warn("Could not find {} in {}.", path, file.getName());
|
||||
@@ -101,11 +96,6 @@ public final class SimpleYamlConfig {
|
||||
return yaml.getInt(path);
|
||||
}
|
||||
|
||||
public boolean getBoolean(final String path, final boolean def) {
|
||||
yaml.addDefault(path, def);
|
||||
return yaml.getBoolean(path, def);
|
||||
}
|
||||
|
||||
public boolean getBoolean(final String path) {
|
||||
if (!yaml.contains(path)) {
|
||||
LOGGER.warn("Could not find {} in {}.", path, file.getName());
|
||||
@@ -114,42 +104,18 @@ public final class SimpleYamlConfig {
|
||||
return yaml.getBoolean(path);
|
||||
}
|
||||
|
||||
public String getString(final String path, final String def) {
|
||||
final Object object = getObject(path, def);
|
||||
if (object instanceof String) {
|
||||
return (String) object;
|
||||
}
|
||||
LOGGER.info("[config] Migrated {} to {}", path, def);
|
||||
set(path, def);
|
||||
return def;
|
||||
}
|
||||
|
||||
public @NotNull String getString(final String path) {
|
||||
if (!yaml.contains(path)) {
|
||||
LOGGER.warn("Could not find {} in {}.", path, file.getName());
|
||||
return "";
|
||||
}
|
||||
final Object object = getObject(path);
|
||||
final Object object = yaml.get(path);
|
||||
if (object instanceof String) {
|
||||
return (String) object;
|
||||
}
|
||||
throw new IllegalStateException("Invalid entry " + path);
|
||||
}
|
||||
|
||||
public Object getObject(final String path, final Object def) {
|
||||
yaml.addDefault(path, def);
|
||||
return yaml.get(path, def);
|
||||
}
|
||||
|
||||
public Object getObject(final String path) {
|
||||
return yaml.get(path);
|
||||
}
|
||||
|
||||
public List<String> getStringList(final String path, final List<String> def) {
|
||||
yaml.addDefault(path, def);
|
||||
return yaml.getStringList(path);
|
||||
}
|
||||
|
||||
public List<String> getStringList(final String path) {
|
||||
if (!yaml.contains(path)) {
|
||||
LOGGER.warn("Could not find {} in {}.", path, file.getName());
|
||||
@@ -158,11 +124,6 @@ public final class SimpleYamlConfig {
|
||||
return yaml.getStringList(path);
|
||||
}
|
||||
|
||||
public List<Integer> getIntList(final String path, final List<Integer> def) {
|
||||
yaml.addDefault(path, def);
|
||||
return yaml.getIntegerList(path);
|
||||
}
|
||||
|
||||
public List<Integer> getIntList(final String path) {
|
||||
if (!yaml.contains(path)) {
|
||||
LOGGER.warn("Could not find {} in {}.", path, file.getName());
|
||||
|
||||
@@ -21,6 +21,7 @@ import com.j256.ormlite.db.DatabaseType;
|
||||
import lombok.*;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.minimessage.MiniMessage;
|
||||
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
@@ -101,8 +102,11 @@ public final class SonarConfiguration {
|
||||
return Language.fromCode(property);
|
||||
} catch (Throwable throwable) {
|
||||
LOGGER.error("Could not find requested language: {}", throwable);
|
||||
LOGGER.error("You can view a full list of valid language codes here:");
|
||||
LOGGER.error("https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes");
|
||||
LOGGER.error("If a translation does not exist yet, Sonar will use English (en).");
|
||||
}
|
||||
} catch (Exception exception) {
|
||||
} catch (IOException exception) {
|
||||
LOGGER.error("Error reading language file: {}", exception);
|
||||
}
|
||||
return DEFAULT_FALLBACK_LANGUAGE;
|
||||
@@ -171,6 +175,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");
|
||||
@@ -181,20 +194,31 @@ public final class SonarConfiguration {
|
||||
verification.vehicle.timing = Verification.Timing.valueOf(generalConfig.getString("verification.checks.vehicle.timing"));
|
||||
|
||||
verification.map.timing = Verification.Timing.valueOf(generalConfig.getString("verification.checks.map-captcha.timing"));
|
||||
verification.map.flare = generalConfig.getBoolean("verification.checks.map-captcha.effects.flare");
|
||||
verification.map.scratches = generalConfig.getBoolean("verification.checks.map-captcha.effects.scratches");
|
||||
verification.map.ripple = generalConfig.getBoolean("verification.checks.map-captcha.effects.ripple");
|
||||
verification.map.smear = generalConfig.getBoolean("verification.checks.map-captcha.effects.smear");
|
||||
verification.map.pinch = generalConfig.getBoolean("verification.checks.map-captcha.effects.pinch");
|
||||
verification.map.saturation = (float) generalConfig.getYaml().getDouble(
|
||||
"verification.checks.map-captcha.effects.saturation", 0.3);
|
||||
verification.map.distortion = (float) generalConfig.getYaml().getDouble(
|
||||
"verification.checks.map-captcha.effects.distortion", 2);
|
||||
verification.map.precomputeAmount = generalConfig.getInt("verification.checks.map-captcha.precompute");
|
||||
verification.map.maxDuration = generalConfig.getInt("verification.checks.map-captcha.max-duration");
|
||||
verification.map.bump = generalConfig.getBoolean("verification.checks.map-captcha.effects.bump");
|
||||
|
||||
verification.map.distortion.enabled = generalConfig.getBoolean("verification.checks.map-captcha.effects.distortion.enabled");
|
||||
verification.map.distortion.shape = generalConfig.getInt("verification.checks.map-captcha.effects.distortion.shape");
|
||||
verification.map.distortion.distance = generalConfig.getInt("verification.checks.map-captcha.effects.distortion.distance");
|
||||
verification.map.distortion.density = (float) generalConfig.getYaml().getDouble("verification.checks.map-captcha.effects.distortion.density");
|
||||
verification.map.distortion.mix = (float) generalConfig.getYaml().getDouble("verification.checks.map-captcha.effects.distortion.mix");
|
||||
|
||||
final String backgroundImagePath = generalConfig.getString("verification.checks.map-captcha.background");
|
||||
if (!backgroundImagePath.isEmpty()) {
|
||||
verification.map.backgroundImage = new File(pluginFolder, backgroundImagePath);
|
||||
if (!verification.map.backgroundImage.exists()) {
|
||||
Sonar.get().getLogger().error("The background image does not exist! Please check the configuration.");
|
||||
}
|
||||
} else {
|
||||
verification.map.backgroundImage = null;
|
||||
}
|
||||
|
||||
verification.map.autoColor = generalConfig.getBoolean("verification.checks.map-captcha.auto-color");
|
||||
verification.map.precomputeAmount = clamp(generalConfig.getInt("verification.checks.map-captcha.precompute"), 1, 1000);
|
||||
verification.map.maxDuration = clamp(generalConfig.getInt("verification.checks.map-captcha.max-duration"), 5000, 360000);
|
||||
verification.map.maxTries = generalConfig.getInt("verification.checks.map-captcha.max-tries");
|
||||
verification.map.dictionary = generalConfig.getString("verification.checks.map-captcha.dictionary");
|
||||
verification.map.fonts = generalConfig.getStringList("verification.checks.map-captcha.font-names");
|
||||
|
||||
verification.brand.enabled = generalConfig.getBoolean("verification.checks.client-brand.enabled");
|
||||
verification.brand.validRegex = Pattern.compile(generalConfig.getString("verification.checks.client-brand.valid-regex"));
|
||||
@@ -204,10 +228,12 @@ public final class SonarConfiguration {
|
||||
verification.validLocaleRegex = Pattern.compile(generalConfig.getString("verification.checks.valid-locale-regex"));
|
||||
verification.maxLoginPackets = clamp(generalConfig.getInt("verification.checks.max-login-packets"), 128, 8192);
|
||||
|
||||
verification.checkGeyser = generalConfig.getBoolean("verification.check-geyser-players");
|
||||
verification.logConnections = generalConfig.getBoolean("verification.log-connections");
|
||||
verification.logDuringAttack = generalConfig.getBoolean("verification.log-during-attack");
|
||||
verification.debugXYZPositions = generalConfig.getBoolean("verification.debug-xyz-positions");
|
||||
verification.readTimeout = clamp(generalConfig.getInt("verification.read-timeout"), 500, 30000);
|
||||
verification.readTimeout = clamp(generalConfig.getInt("verification.read-timeout"), 1000, 30000);
|
||||
verification.writeTimeout = clamp(generalConfig.getInt("verification.write-timeout"), 1000, 30000);
|
||||
verification.reconnectDelay = clamp(generalConfig.getInt("verification.rejoin-delay"), 0, 100000);
|
||||
verification.rememberTime = clamp(generalConfig.getInt("verification.remember-time"), 0, 86400000);
|
||||
verification.blacklistTime = clamp(generalConfig.getInt("verification.blacklist-time"), 0, 86400000);
|
||||
@@ -230,8 +256,7 @@ public final class SonarConfiguration {
|
||||
|
||||
webhook.embed.title = webhookConfig.getString(embedPath + "title");
|
||||
webhook.embed.titleUrl = webhookConfig.getString(embedPath + "title-url");
|
||||
webhook.embed.description = formatString(String.join("\n",
|
||||
webhookConfig.getStringList(embedPath + "description")));
|
||||
webhook.embed.description = String.join("\n", webhookConfig.getStringList(embedPath + "description"));
|
||||
webhook.embed.r = webhookConfig.getInt(embedPath + "color.red");
|
||||
webhook.embed.g = webhookConfig.getInt(embedPath + "color.green");
|
||||
webhook.embed.b = webhookConfig.getInt(embedPath + "color.blue");
|
||||
@@ -247,84 +272,43 @@ public final class SonarConfiguration {
|
||||
}
|
||||
|
||||
// Messages
|
||||
prefix = formatString(messagesConfig.getString("prefix"));
|
||||
supportUrl = messagesConfig.getString("support-url");
|
||||
|
||||
header = fromList(messagesConfig.getStringList("header"));
|
||||
footer = fromList(messagesConfig.getStringList("footer"));
|
||||
tooManyOnlinePerIp = deserialize(fromList(messagesConfig.getStringList("too-many-online-per-ip")));
|
||||
|
||||
noPermission = formatString(messagesConfig.getString("commands.no-permission"));
|
||||
commands.incorrectCommandUsage = formatString(messagesConfig.getString("commands.incorrect-usage"));
|
||||
commands.incorrectIpAddress = formatString(messagesConfig.getString("commands.invalid-ip-address"));
|
||||
commands.illegalIpAddress = formatString(messagesConfig.getString("commands.illegal-ip-address"));
|
||||
commands.playersOnly = formatString(messagesConfig.getString("commands.player-only"));
|
||||
commands.consoleOnly = formatString(messagesConfig.getString("commands.console-only"));
|
||||
commands.commandCoolDown = formatString(messagesConfig.getString("commands.cool-down"));
|
||||
commands.commandCoolDownLeft = formatString(messagesConfig.getString("commands.cool-down-left"));
|
||||
commands.subCommandNoPerm = formatString(messagesConfig.getString("commands.subcommand-no-permission"));
|
||||
commands.helpHeader = messagesConfig.getStringList("commands.main.header");
|
||||
commands.helpSubcommands = formatString(messagesConfig.getString("commands.main.subcommands"));
|
||||
prefix = MiniMessage.miniMessage().deserialize(messagesConfig.getString("prefix"));
|
||||
noPermission = MiniMessage.miniMessage().deserialize(messagesConfig.getString("commands.no-permission"),
|
||||
Placeholder.component("prefix", prefix));
|
||||
|
||||
SonarCommand.prepareCachedMessages();
|
||||
|
||||
commands.reloading = formatString(messagesConfig.getString("commands.reload.start"));
|
||||
commands.reloaded = formatString(messagesConfig.getString("commands.reload.finish"));
|
||||
supportUrl = messagesConfig.getString("support-url");
|
||||
header = MiniMessage.miniMessage().deserialize(
|
||||
String.join("<newline>", messagesConfig.getStringList("header")),
|
||||
Placeholder.unparsed("support-url", supportUrl),
|
||||
Placeholder.component("prefix", prefix));
|
||||
footer = MiniMessage.miniMessage().deserialize(
|
||||
String.join("<newline>", messagesConfig.getStringList("footer")),
|
||||
Placeholder.unparsed("support-url", supportUrl),
|
||||
Placeholder.component("prefix", prefix));
|
||||
|
||||
commands.verboseSubscribed = formatString(messagesConfig.getString("commands.verbose.subscribed"));
|
||||
commands.verboseUnsubscribed = formatString(messagesConfig.getString("commands.verbose.unsubscribed"));
|
||||
tooManyOnlinePerIp = deserializeDisconnectMessage("too-many-online-per-ip");
|
||||
verification.currentlyPreparing = deserializeDisconnectMessage("verification.currently-preparing");
|
||||
verification.tooFastReconnect = deserializeDisconnectMessage("verification.too-fast-reconnect");
|
||||
verification.alreadyVerifying = deserializeDisconnectMessage("verification.already-verifying");
|
||||
verification.alreadyQueued = deserializeDisconnectMessage("verification.already-queued");
|
||||
verification.blacklisted = deserializeDisconnectMessage("verification.blacklisted");
|
||||
verification.invalidUsername = deserializeDisconnectMessage("verification.invalid-username");
|
||||
verification.protocolBlacklisted = deserializeDisconnectMessage("verification.blacklisted-protocol");
|
||||
verification.verificationSuccess = deserializeDisconnectMessage("verification.success");
|
||||
verification.verificationFailed = deserializeDisconnectMessage("verification.failed");
|
||||
|
||||
commands.notificationsSubscribed = formatString(messagesConfig.getString("commands.notify.subscribed"));
|
||||
commands.notificationsUnsubscribed = formatString(messagesConfig.getString("commands.notify.unsubscribed"));
|
||||
verboseAnimation = Collections.unmodifiableList(messagesConfig.getStringList("verbose.animation"));
|
||||
}
|
||||
|
||||
commands.blacklistEmpty = formatString(messagesConfig.getString("commands.blacklist.empty"));
|
||||
commands.blacklistCleared = formatString(messagesConfig.getString("commands.blacklist.cleared"));
|
||||
commands.blacklistSize = formatString(messagesConfig.getString("commands.blacklist.size"));
|
||||
commands.blacklistAdd = formatString(messagesConfig.getString("commands.blacklist.added"));
|
||||
commands.blacklistAddWarning = formatString(messagesConfig.getString("commands.blacklist.added-warning"));
|
||||
commands.blacklistRemove = formatString(messagesConfig.getString("commands.blacklist.removed"));
|
||||
commands.blacklistDuplicate = formatString(messagesConfig.getString("commands.blacklist.duplicate-ip"));
|
||||
commands.blacklistNotFound = formatString(messagesConfig.getString("commands.blacklist.ip-not-found"));
|
||||
|
||||
commands.verifiedEmpty = formatString(messagesConfig.getString("commands.verified.empty"));
|
||||
commands.verifiedCleared = formatString(messagesConfig.getString("commands.verified.cleared"));
|
||||
commands.verifiedSize = formatString(messagesConfig.getString("commands.verified.size"));
|
||||
commands.verifiedRemove = formatString(messagesConfig.getString("commands.verified.removed"));
|
||||
commands.verifiedNotFound = formatString(messagesConfig.getString("commands.verified.ip-not-found"));
|
||||
commands.verifiedBlocked = formatString(messagesConfig.getString("commands.verified.blocked"));
|
||||
|
||||
commands.statisticsHeader = formatString(messagesConfig.getString("commands.statistics.header"));
|
||||
commands.unknownStatisticType = formatString(messagesConfig.getString("commands.statistics.unknown-type"));
|
||||
commands.generalStatistics = formatString(fromList(messagesConfig.getStringList("commands.statistics.general")));
|
||||
commands.cpuStatistics = formatString(fromList(messagesConfig.getStringList("commands.statistics.cpu")));
|
||||
commands.memoryStatistics = formatString(fromList(messagesConfig.getStringList("commands.statistics.memory")));
|
||||
commands.networkStatistics = formatString(fromList(messagesConfig.getStringList("commands.statistics.network")));
|
||||
|
||||
verification.connectLog = formatString(messagesConfig.getString("verification.logs.connection"));
|
||||
verification.failedLog = formatString(messagesConfig.getString("verification.logs.failed"));
|
||||
verification.blacklistLog = formatString(messagesConfig.getString("verification.logs.blacklisted"));
|
||||
verification.successLog = formatString(messagesConfig.getString("verification.logs.successful"));
|
||||
|
||||
verification.map.enterCode = deserialize(formatString(messagesConfig.getString("verification.captcha.enter-code")));
|
||||
verification.map.enterCodeActionBar = formatString(messagesConfig.getString("verification.captcha.action-bar"));
|
||||
verification.map.failedCaptcha = deserialize(formatString(messagesConfig.getString("verification.captcha.incorrect")));
|
||||
verification.currentlyPreparing = deserialize(fromList(messagesConfig.getStringList("verification.currently-preparing")));
|
||||
verification.tooFastReconnect = deserialize(fromList(messagesConfig.getStringList("verification.too-fast-reconnect")));
|
||||
verification.alreadyVerifying = deserialize(fromList(messagesConfig.getStringList("verification.already-verifying")));
|
||||
verification.alreadyQueued = deserialize(fromList(messagesConfig.getStringList("verification.already-queued")));
|
||||
verification.blacklisted = deserialize(fromList(messagesConfig.getStringList("verification.blacklisted")));
|
||||
verification.invalidUsername = deserialize(fromList(messagesConfig.getStringList("verification.invalid-username")));
|
||||
verification.protocolBlacklisted = deserialize(fromList(messagesConfig.getStringList("verification.blacklisted-protocol")));
|
||||
verification.verificationSuccess = deserialize(fromList(messagesConfig.getStringList("verification.success")));
|
||||
verification.verificationFailed = deserialize(fromList(messagesConfig.getStringList("verification.failed")));
|
||||
|
||||
verbose.actionBarLayout = formatString(messagesConfig.getString("verbose.layout.normal"));
|
||||
verbose.actionBarLayoutDuringAttack = formatString(messagesConfig.getString("verbose.layout.attack"));
|
||||
verbose.animation = Collections.unmodifiableList(messagesConfig.getStringList("verbose.animation"));
|
||||
|
||||
notifications.notificationTitle = deserialize(formatString(messagesConfig.getString("notifications.title")));
|
||||
notifications.notificationSubtitle = deserialize(formatString(messagesConfig.getString("notifications.subtitle")));
|
||||
notifications.notificationChat = formatString(fromList(messagesConfig.getStringList("notifications.chat")));
|
||||
private @NotNull Component deserializeDisconnectMessage(final String path) {
|
||||
return MiniMessage.miniMessage().deserialize(
|
||||
String.join("<newline>", messagesConfig.getStringList(path)),
|
||||
Placeholder.component("prefix", prefix),
|
||||
Placeholder.component("header", header),
|
||||
Placeholder.component("footer", footer),
|
||||
Placeholder.unparsed("support-url", supportUrl));
|
||||
}
|
||||
|
||||
private @NotNull URL getAsset(final String url, final @NotNull Language language) {
|
||||
@@ -338,33 +322,17 @@ public final class SonarConfiguration {
|
||||
}
|
||||
|
||||
private static int clamp(final int v, final int max, final int min) {
|
||||
return Math.max(Math.min(v, min), max);
|
||||
final int output = Math.max(Math.min(v, min), max);
|
||||
if (output != v) {
|
||||
Sonar.get().getLogger().warn("Clamped configuration value {} to {}", v, output);
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
public String formatAddress(final InetAddress inetAddress) {
|
||||
return logPlayerAddresses ? inetAddress.toString() : "/<ip address withheld>";
|
||||
}
|
||||
|
||||
private @NotNull String fromList(final @NotNull Collection<String> list) {
|
||||
return formatString(String.join("<newline>", list));
|
||||
}
|
||||
|
||||
private static @NotNull Component deserialize(final String legacy) {
|
||||
return MiniMessage.miniMessage().deserialize(legacy);
|
||||
}
|
||||
|
||||
private @NotNull String formatString(final @NotNull String str) {
|
||||
return str
|
||||
.replace("%prefix%", prefix == null ? "" : prefix)
|
||||
.replace("%support-url%", supportUrl == null ? "" : supportUrl)
|
||||
.replace("%header%", header == null ? "" : header)
|
||||
.replace("%footer%", footer == null ? "" : footer);
|
||||
}
|
||||
|
||||
@Getter
|
||||
private final Verbose verbose = new Verbose();
|
||||
@Getter
|
||||
private final Commands commands = new Commands();
|
||||
@Getter
|
||||
private final Queue queue = new Queue();
|
||||
@Getter
|
||||
@@ -373,15 +341,14 @@ public final class SonarConfiguration {
|
||||
private final Database database = new Database();
|
||||
@Getter
|
||||
private final Webhook webhook = new Webhook();
|
||||
@Getter
|
||||
private final Notifications notifications = new Notifications();
|
||||
|
||||
@Getter
|
||||
private String prefix;
|
||||
private Component prefix;
|
||||
private String supportUrl;
|
||||
private Component header;
|
||||
private Component footer;
|
||||
@Getter
|
||||
private String noPermission;
|
||||
private String header, footer;
|
||||
private Component noPermission;
|
||||
private boolean logPlayerAddresses;
|
||||
@Getter
|
||||
private int maxOnlinePerIp;
|
||||
@@ -395,22 +362,8 @@ public final class SonarConfiguration {
|
||||
private int attackCooldownDelay;
|
||||
@Getter
|
||||
private Component tooManyOnlinePerIp;
|
||||
|
||||
@Getter
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public static final class Verbose {
|
||||
private String actionBarLayout;
|
||||
private String actionBarLayoutDuringAttack;
|
||||
private List<String> animation;
|
||||
}
|
||||
|
||||
@Getter
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public static final class Notifications {
|
||||
private Component notificationTitle;
|
||||
private Component notificationSubtitle;
|
||||
private String notificationChat;
|
||||
}
|
||||
private List<String> verboseAnimation;
|
||||
|
||||
@Getter
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
@@ -441,21 +394,26 @@ public final class SonarConfiguration {
|
||||
@Getter
|
||||
public static final class Map {
|
||||
private Timing timing;
|
||||
private boolean flare;
|
||||
private boolean scratches;
|
||||
private boolean ripple;
|
||||
private boolean smear;
|
||||
private boolean pinch;
|
||||
private float saturation;
|
||||
private float distortion;
|
||||
private boolean bump;
|
||||
private File backgroundImage;
|
||||
private boolean autoColor;
|
||||
private int precomputeAmount;
|
||||
private int maxDuration;
|
||||
private int maxTries;
|
||||
private String dictionary;
|
||||
private Component enterCode;
|
||||
private Component failedCaptcha;
|
||||
private String enterCodeActionBar;
|
||||
private List<String> fonts;
|
||||
|
||||
private Smear distortion = new Smear();
|
||||
|
||||
@Getter
|
||||
public static final class Smear {
|
||||
private boolean enabled;
|
||||
private int shape;
|
||||
private int distance;
|
||||
private float density;
|
||||
private float mix;
|
||||
}
|
||||
}
|
||||
|
||||
@Getter
|
||||
@@ -491,18 +449,16 @@ public final class SonarConfiguration {
|
||||
private Pattern validRegex;
|
||||
}
|
||||
|
||||
private boolean checkGeyser;
|
||||
private boolean logConnections;
|
||||
private boolean logDuringAttack;
|
||||
private boolean debugXYZPositions;
|
||||
private Pattern validNameRegex;
|
||||
private Pattern validLocaleRegex;
|
||||
private String connectLog;
|
||||
private String failedLog;
|
||||
private String successLog;
|
||||
private String blacklistLog;
|
||||
|
||||
private int maxLoginPackets;
|
||||
private int readTimeout;
|
||||
private int writeTimeout;
|
||||
private int reconnectDelay;
|
||||
private int rememberTime;
|
||||
private int blacklistTime;
|
||||
@@ -521,54 +477,6 @@ public final class SonarConfiguration {
|
||||
private Component currentlyPreparing;
|
||||
}
|
||||
|
||||
@Getter
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public static final class Commands {
|
||||
private String incorrectCommandUsage;
|
||||
private String incorrectIpAddress;
|
||||
private String illegalIpAddress;
|
||||
private String subCommandNoPerm;
|
||||
private String playersOnly;
|
||||
private String consoleOnly;
|
||||
private String commandCoolDown;
|
||||
private String commandCoolDownLeft;
|
||||
|
||||
private String blacklistEmpty;
|
||||
private String blacklistAdd;
|
||||
private String blacklistAddWarning;
|
||||
private String blacklistDuplicate;
|
||||
private String blacklistNotFound;
|
||||
private String blacklistRemove;
|
||||
private String blacklistCleared;
|
||||
private String blacklistSize;
|
||||
|
||||
private String verifiedRemove;
|
||||
private String verifiedNotFound;
|
||||
private String verifiedCleared;
|
||||
private String verifiedSize;
|
||||
private String verifiedEmpty;
|
||||
private String verifiedBlocked;
|
||||
|
||||
private String statisticsHeader;
|
||||
private String unknownStatisticType;
|
||||
private String generalStatistics;
|
||||
private String memoryStatistics;
|
||||
private String networkStatistics;
|
||||
private String cpuStatistics;
|
||||
|
||||
private List<String> helpHeader;
|
||||
private String helpSubcommands;
|
||||
|
||||
private String verboseSubscribed;
|
||||
private String verboseUnsubscribed;
|
||||
|
||||
private String notificationsSubscribed;
|
||||
private String notificationsUnsubscribed;
|
||||
|
||||
private String reloading;
|
||||
private String reloaded;
|
||||
}
|
||||
|
||||
@Getter
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public static final class Database {
|
||||
|
||||
+2
-22
@@ -33,7 +33,6 @@ import xyz.jonesdev.sonar.api.config.SonarConfiguration;
|
||||
import xyz.jonesdev.sonar.api.logger.LoggerWrapper;
|
||||
import xyz.jonesdev.sonar.api.model.VerifiedPlayer;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Timestamp;
|
||||
import java.time.Instant;
|
||||
@@ -238,11 +237,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
|
||||
*/
|
||||
@@ -293,16 +292,6 @@ public final class VerifiedPlayerController {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param inetAddress IP address
|
||||
* @param uuid UUID associated to the IP
|
||||
* @return Whether the local cache contains the IP and UUID
|
||||
* @see #has(String, UUID)
|
||||
*/
|
||||
public boolean has(final @NotNull InetAddress inetAddress, final @NotNull UUID uuid) {
|
||||
return has(inetAddress.toString(), uuid);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param inetAddress IP address
|
||||
* @return Whether the local cache contains the IP
|
||||
@@ -310,13 +299,4 @@ public final class VerifiedPlayerController {
|
||||
public boolean has(final @NotNull String inetAddress) {
|
||||
return cache.containsKey(inetAddress);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param inetAddress IP address
|
||||
* @return Whether the local cache contains the IP
|
||||
* @see #has(String)
|
||||
*/
|
||||
public boolean has(final @NotNull InetAddress inetAddress) {
|
||||
return has(inetAddress.toString());
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
@@ -83,6 +82,6 @@ public final class Fallback {
|
||||
private static boolean shouldPerform(final SonarConfiguration.Verification.Timing timing) {
|
||||
return timing == SonarConfiguration.Verification.Timing.ALWAYS
|
||||
|| (timing == SonarConfiguration.Verification.Timing.DURING_ATTACK
|
||||
&& Sonar.get().getAttackTracker().isCurrentlyUnderAttack());
|
||||
&& Sonar.get().getAttackTracker().getCurrentAttack() != null);
|
||||
}
|
||||
}
|
||||
@@ -26,51 +26,33 @@ 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.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);
|
||||
// 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);
|
||||
|
||||
// Runnable task executed for polling the queue
|
||||
private final Runnable pollTask = () -> {
|
||||
private final ConcurrentMap<InetAddress, Runnable> players = 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.
|
||||
final Iterator<Map.Entry<InetAddress, Runnable>> iterator = queuedPlayers.entrySet().iterator();
|
||||
while (iterator.hasNext()) {
|
||||
// Break if we reached our maximum entries
|
||||
if (++index > maxQueuePolls) {
|
||||
break;
|
||||
}
|
||||
// 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 = players.entrySet().iterator();
|
||||
while (iterator.hasNext() && index++ < maxQueuePolls) {
|
||||
// Run the cached runnable
|
||||
final Map.Entry<InetAddress, Runnable> entry = iterator.next();
|
||||
entry.getValue().run();
|
||||
// Remove runnable from iterator
|
||||
QUEUE_EXECUTOR.execute(entry.getValue());
|
||||
// Remove runnable from the map
|
||||
iterator.remove();
|
||||
}
|
||||
|
||||
// Run the attack check task
|
||||
Sonar.get().getAttackTracker().checkIfUnderAttack();
|
||||
// Clean up the cache of rate-limited IPs
|
||||
Sonar.get().getFallback().getRatelimiter().cleanUpCache();
|
||||
};
|
||||
|
||||
/**
|
||||
* @param inetAddress IP address of the player
|
||||
* @param runnable Queued action on the netty thread
|
||||
* @see #remove
|
||||
*/
|
||||
public void queue(final InetAddress inetAddress, final Runnable runnable) {
|
||||
queuedPlayers.put(inetAddress, runnable);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param inetAddress IP address of the player
|
||||
*/
|
||||
public void remove(final InetAddress inetAddress) {
|
||||
queuedPlayers.remove(inetAddress);
|
||||
}
|
||||
}
|
||||
@@ -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,33 +18,48 @@
|
||||
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;
|
||||
|
||||
public interface FallbackUser {
|
||||
@NotNull Channel getChannel();
|
||||
@NotNull
|
||||
Channel getChannel();
|
||||
|
||||
@NotNull InetAddress getInetAddress();
|
||||
@NotNull
|
||||
ChannelPipeline getPipeline();
|
||||
|
||||
@NotNull ProtocolVersion getProtocolVersion();
|
||||
@NotNull
|
||||
InetAddress getInetAddress();
|
||||
|
||||
@NotNull FallbackUserState getState();
|
||||
@NotNull
|
||||
ProtocolVersion getProtocolVersion();
|
||||
|
||||
void setState(final FallbackUserState state);
|
||||
@NotNull
|
||||
SystemTimer getLoginTimer();
|
||||
|
||||
boolean isReceivedClientSettings();
|
||||
|
||||
void setReceivedClientSettings(final boolean receivedClientSettings);
|
||||
|
||||
boolean isReceivedPluginMessage();
|
||||
|
||||
void setReceivedPluginMessage(final boolean receivedPluginMessage);
|
||||
|
||||
boolean isGeyser();
|
||||
|
||||
/**
|
||||
* Disconnect the player during/after verification
|
||||
* using our custom Disconnect packet.
|
||||
*
|
||||
* @param reason Disconnect message component
|
||||
* @param reason Disconnect message component
|
||||
*/
|
||||
@ApiStatus.Internal
|
||||
void disconnect(final @NotNull Component reason);
|
||||
|
||||
/**
|
||||
@@ -55,11 +70,11 @@ public interface FallbackUser {
|
||||
* @param encoder Name of the encoder pipeline
|
||||
* @param decoder Name of the decoder pipeline
|
||||
* @param timeout Name of the read timeout pipeline
|
||||
* @param boss Name of the main pipeline
|
||||
* @param handler Name of the main pipeline
|
||||
*/
|
||||
void hijack(final @NotNull String username, final @NotNull UUID uuid,
|
||||
final @NotNull String encoder, final @NotNull String decoder,
|
||||
final @NotNull String timeout, final @NotNull String boss);
|
||||
final @NotNull String timeout, final @NotNull String handler);
|
||||
|
||||
/**
|
||||
* Sends a packet/message to the player
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
+8
-21
@@ -20,10 +20,8 @@ package xyz.jonesdev.sonar.api.fallback.protocol;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import java.util.EnumSet;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
@@ -68,7 +66,9 @@ public enum ProtocolVersion {
|
||||
MINECRAFT_1_19_4(762),
|
||||
MINECRAFT_1_20(763),
|
||||
MINECRAFT_1_20_2(764),
|
||||
MINECRAFT_1_20_3(765);
|
||||
MINECRAFT_1_20_3(765),
|
||||
MINECRAFT_1_20_5(766),
|
||||
MINECRAFT_1_21(767);
|
||||
|
||||
private final int protocol;
|
||||
|
||||
@@ -76,27 +76,14 @@ public enum ProtocolVersion {
|
||||
public static final Map<Integer, ProtocolVersion> ID_TO_PROTOCOL_CONSTANT;
|
||||
|
||||
static {
|
||||
{
|
||||
final Map<Integer, ProtocolVersion> versions = new HashMap<>();
|
||||
final Map<Integer, ProtocolVersion> versions = new HashMap<>();
|
||||
|
||||
for (final ProtocolVersion version : values()) {
|
||||
versions.putIfAbsent(version.protocol, version);
|
||||
}
|
||||
|
||||
ID_TO_PROTOCOL_CONSTANT = versions;
|
||||
for (final ProtocolVersion version : values()) {
|
||||
versions.putIfAbsent(version.protocol, version);
|
||||
}
|
||||
|
||||
{
|
||||
final Set<ProtocolVersion> versions = EnumSet.noneOf(ProtocolVersion.class);
|
||||
|
||||
for (final ProtocolVersion value : values()) {
|
||||
if (!value.isUnknown()) {
|
||||
versions.add(value);
|
||||
}
|
||||
}
|
||||
|
||||
LATEST_VERSION = (ProtocolVersion) versions.toArray()[versions.size() - 1];
|
||||
}
|
||||
ID_TO_PROTOCOL_CONSTANT = versions;
|
||||
LATEST_VERSION = values()[values().length - 1];
|
||||
}
|
||||
|
||||
public static ProtocolVersion fromId(final int protocol) {
|
||||
|
||||
@@ -21,12 +21,14 @@ import com.sun.management.OperatingSystemMXBean;
|
||||
import lombok.experimental.UtilityClass;
|
||||
|
||||
import java.lang.management.ManagementFactory;
|
||||
import java.text.DecimalFormat;
|
||||
|
||||
@UtilityClass
|
||||
public class JVMProcessInformation {
|
||||
private final Runtime RUNTIME = Runtime.getRuntime();
|
||||
private final OperatingSystemMXBean MX = (OperatingSystemMXBean) ManagementFactory.getOperatingSystemMXBean();
|
||||
private final char[] MEMORY_UNITS = {'K', 'M', 'G', 'T', 'P', 'E'};
|
||||
private final DecimalFormat DECIMAL_FORMAT = new DecimalFormat("#.#");
|
||||
|
||||
public String formatMemory(final long size) {
|
||||
if (size < 1024L) {
|
||||
@@ -37,7 +39,7 @@ public class JVMProcessInformation {
|
||||
final double formattedSize = (double) size / (1L << (group * 10));
|
||||
final char unit = MEMORY_UNITS[group - 1];
|
||||
// https://en.wikipedia.org/wiki/Byte#Multiple-byte_units
|
||||
return String.format("%.1f %siB", formattedSize, unit);
|
||||
return DECIMAL_FORMAT.format(formattedSize) + " " + unit + "iB";
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -75,14 +77,6 @@ public class JVMProcessInformation {
|
||||
return getSystemCPUUsage() / getVirtualCores();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Current system load average in percent
|
||||
* @apiNote This returns the global process load, not the CPU usage
|
||||
*/
|
||||
public double getSystemLoadAverage() {
|
||||
return MX.getSystemLoadAverage() * 100;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Memory (in bytes) the JVM will attempt to use
|
||||
*/
|
||||
|
||||
@@ -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,17 @@ public final class VerifiedPlayer {
|
||||
)
|
||||
private Timestamp timestamp;
|
||||
|
||||
public VerifiedPlayer(final String inetAddress, final UUID playerUUID, final long timestamp) {
|
||||
public VerifiedPlayer(final @NotNull InetAddress inetAddress,
|
||||
final @NotNull UUID playerUuid,
|
||||
final long timestamp) {
|
||||
this(inetAddress.toString(), playerUuid, timestamp);
|
||||
}
|
||||
|
||||
public VerifiedPlayer(final @NotNull String inetAddress,
|
||||
final @NotNull UUID playerUuid,
|
||||
final long timestamp) {
|
||||
this.inetAddress = inetAddress;
|
||||
this.playerUUID = playerUUID;
|
||||
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,13 +18,12 @@
|
||||
package xyz.jonesdev.sonar.api.timer;
|
||||
|
||||
import lombok.Getter;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
|
||||
@Getter
|
||||
public final class SystemTimer {
|
||||
private static final SimpleDateFormat FORMATTER = new SimpleDateFormat("mm:ss");
|
||||
public static final SimpleDateFormat DATE_FORMATTER = new SimpleDateFormat("mm:ss");
|
||||
|
||||
private long start = System.currentTimeMillis();
|
||||
|
||||
@@ -40,19 +39,12 @@ public final class SystemTimer {
|
||||
}
|
||||
|
||||
/**
|
||||
* @apiNote Read more: {@link SystemTimer#delay}, {@link SystemTimer#formattedDelay}
|
||||
* @apiNote Read more: {@link SystemTimer#delay}
|
||||
*/
|
||||
public boolean elapsed(final long amount) {
|
||||
return delay() >= amount;
|
||||
}
|
||||
|
||||
/**
|
||||
* @apiNote Read more: {@link SystemTimer#delay}
|
||||
*/
|
||||
public @NotNull String formattedDelay() {
|
||||
return FORMATTER.format(delay());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("%.3f", delay() / 1000D);
|
||||
|
||||
@@ -21,56 +21,69 @@ import lombok.Getter;
|
||||
import net.kyori.adventure.audience.Audience;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.minimessage.MiniMessage;
|
||||
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
|
||||
import net.kyori.adventure.title.Title;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.statistics.SonarStatistics;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.Vector;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.Sonar.DECIMAL_FORMAT;
|
||||
import static xyz.jonesdev.sonar.api.jvm.JVMProcessInformation.*;
|
||||
|
||||
@Getter
|
||||
public final class Notification implements Observable {
|
||||
private final @NotNull Collection<UUID> subscribers = new Vector<>(0);
|
||||
public final class Notification extends Observable {
|
||||
|
||||
@Override
|
||||
public void observe() {
|
||||
// Don't prepare component if there are no subscribers
|
||||
if (subscribers.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Prepare the chat notification
|
||||
final Component notificationTitle = MiniMessage.miniMessage().deserialize(
|
||||
Sonar.get().getConfig().getMessagesConfig().getString("notifications.title"));
|
||||
final Component notificationSubtitle = MiniMessage.miniMessage().deserialize(
|
||||
Sonar.get().getConfig().getMessagesConfig().getString("notifications.subtitle"));
|
||||
final Title title = Title.title(notificationTitle, notificationSubtitle);
|
||||
|
||||
final List<String> chatNotification = Sonar.get().getConfig().getMessagesConfig().getStringList("notifications.chat");
|
||||
final Component[] chatNotificationComponents = new Component[chatNotification.size()];
|
||||
|
||||
for (int i = 0; i < chatNotificationComponents.length; i++) {
|
||||
chatNotificationComponents[i] = MiniMessage.miniMessage().deserialize(chatNotification.get(i),
|
||||
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix()),
|
||||
Placeholder.unparsed("queued", DECIMAL_FORMAT.format(Sonar.get().getFallback().getQueue().getPlayers().size())),
|
||||
Placeholder.unparsed("verifying", DECIMAL_FORMAT.format(Sonar.get().getFallback().getConnected().size())),
|
||||
Placeholder.unparsed("blacklisted", DECIMAL_FORMAT.format(Sonar.get().getFallback().getBlacklist().estimatedSize())),
|
||||
Placeholder.unparsed("total-joins", DECIMAL_FORMAT.format(Sonar.get().getStatistics().getTotalPlayersJoined())),
|
||||
Placeholder.unparsed("logins-per-second", DECIMAL_FORMAT.format(Sonar.get().getStatistics().getLoginsPerSecond())),
|
||||
Placeholder.unparsed("connections-per-second", DECIMAL_FORMAT.format(Sonar.get().getStatistics().getConnectionsPerSecond())),
|
||||
Placeholder.unparsed("verify-total", DECIMAL_FORMAT.format(Sonar.get().getStatistics().getTotalAttemptedVerifications())),
|
||||
Placeholder.unparsed("verify-success", DECIMAL_FORMAT.format(Sonar.get().getStatistics().getTotalSuccessfulVerifications())),
|
||||
Placeholder.unparsed("verify-failed", DECIMAL_FORMAT.format(Sonar.get().getStatistics().getTotalFailedVerifications())),
|
||||
Placeholder.unparsed("incoming-traffic", Sonar.get().getStatistics().getCurrentIncomingBandwidthFormatted()),
|
||||
Placeholder.unparsed("outgoing-traffic", Sonar.get().getStatistics().getCurrentOutgoingBandwidthFormatted()),
|
||||
Placeholder.unparsed("incoming-traffic-ttl", Sonar.get().getStatistics().getTotalIncomingBandwidthFormatted()),
|
||||
Placeholder.unparsed("outgoing-traffic-ttl", Sonar.get().getStatistics().getTotalOutgoingBandwidthFormatted()),
|
||||
Placeholder.unparsed("used-memory", formatMemory(getUsedMemory())),
|
||||
Placeholder.unparsed("free-memory", formatMemory(getFreeMemory())),
|
||||
Placeholder.unparsed("total-memory", formatMemory(getTotalMemory())),
|
||||
Placeholder.unparsed("max-memory", formatMemory(getMaxMemory())));
|
||||
}
|
||||
|
||||
public void sendAttackNotification() {
|
||||
// No need to do anything if we have no subscribers
|
||||
if (subscribers.isEmpty()) return;
|
||||
final SonarStatistics statistics = Sonar.get().getStatistics();
|
||||
// Prepare the title
|
||||
// TODO: Why do custom title times not work?
|
||||
// Title.Times.times(Ticks.duration(5L), Ticks.duration(60L), Ticks.duration(10L));
|
||||
final Title title = Title.title(
|
||||
Sonar.get().getConfig().getNotifications().getNotificationTitle(),
|
||||
Sonar.get().getConfig().getNotifications().getNotificationSubtitle());
|
||||
// Prepare the chat message
|
||||
final Component chat = MiniMessage.miniMessage().deserialize(
|
||||
Sonar.get().getConfig().getNotifications().getNotificationChat()
|
||||
.replace("%queued%",
|
||||
DECIMAL_FORMAT.format(Sonar.get().getFallback().getQueue().getQueuedPlayers().size()))
|
||||
.replace("%verifying%", DECIMAL_FORMAT.format(Sonar.get().getFallback().getConnected().size()))
|
||||
.replace("%blacklisted%",
|
||||
DECIMAL_FORMAT.format(Sonar.get().getFallback().getBlacklist().estimatedSize()))
|
||||
.replace("%total-joins%", DECIMAL_FORMAT.format(statistics.getTotalPlayersJoined()))
|
||||
.replace("%logins-per-second%", DECIMAL_FORMAT.format(statistics.getLoginsPerSecond()))
|
||||
.replace("%connections-per-second%", DECIMAL_FORMAT.format(statistics.getConnectionsPerSecond()))
|
||||
.replace("%verify-total%", DECIMAL_FORMAT.format(statistics.getTotalAttemptedVerifications()))
|
||||
.replace("%verify-success%",
|
||||
DECIMAL_FORMAT.format(Sonar.get().getVerifiedPlayerController().estimatedSize()))
|
||||
.replace("%verify-failed%", DECIMAL_FORMAT.format(statistics.getTotalFailedVerifications()))
|
||||
.replace("%incoming-traffic%", statistics.getCurrentIncomingBandwidthFormatted())
|
||||
.replace("%outgoing-traffic%", statistics.getCurrentOutgoingBandwidthFormatted())
|
||||
.replace("%incoming-traffic-ttl%", statistics.getTotalIncomingBandwidthFormatted())
|
||||
.replace("%outgoing-traffic-ttl%", statistics.getTotalOutgoingBandwidthFormatted()));
|
||||
// Send the title and chat messages to all online players
|
||||
for (final UUID subscriber : Sonar.get().getNotificationHandler().getSubscribers()) {
|
||||
final Audience audience = Sonar.get().audience(subscriber);
|
||||
if (audience == null) continue;
|
||||
|
||||
// Send the message to all online subscribers
|
||||
for (Component component : chatNotificationComponents) {
|
||||
audience.sendMessage(component);
|
||||
}
|
||||
// Send the title to all online subscribers
|
||||
audience.showTitle(title);
|
||||
audience.sendMessage(chat);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,33 +17,38 @@
|
||||
|
||||
package xyz.jonesdev.sonar.api.verbose;
|
||||
|
||||
import lombok.Getter;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.UUID;
|
||||
import java.util.Vector;
|
||||
|
||||
public interface Observable {
|
||||
Collection<UUID> getSubscribers();
|
||||
@Getter
|
||||
public abstract class Observable {
|
||||
protected final @NotNull Collection<UUID> subscribers = new Vector<>(0);
|
||||
|
||||
public abstract void observe();
|
||||
|
||||
/**
|
||||
* @param uuid UUID of the player
|
||||
* @return Whether the audience is subscribed or not
|
||||
*/
|
||||
default boolean isSubscribed(final @NotNull UUID uuid) {
|
||||
public final boolean isSubscribed(final @NotNull UUID uuid) {
|
||||
return getSubscribers().contains(uuid);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param uuid UUID of the player
|
||||
*/
|
||||
default void subscribe(final UUID uuid) {
|
||||
public final void subscribe(final UUID uuid) {
|
||||
getSubscribers().add(uuid);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param uuid UUID of the player
|
||||
*/
|
||||
default void unsubscribe(final UUID uuid) {
|
||||
public final void unsubscribe(final UUID uuid) {
|
||||
getSubscribers().remove(uuid);
|
||||
}
|
||||
}
|
||||
@@ -18,80 +18,72 @@
|
||||
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.minimessage.MiniMessage;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.attack.AttackTracker;
|
||||
import xyz.jonesdev.sonar.api.statistics.SonarStatistics;
|
||||
import xyz.jonesdev.sonar.api.timer.SystemTimer;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.UUID;
|
||||
import java.util.Vector;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.Sonar.DECIMAL_FORMAT;
|
||||
import static xyz.jonesdev.sonar.api.jvm.JVMProcessInformation.*;
|
||||
import static xyz.jonesdev.sonar.api.timer.SystemTimer.DATE_FORMATTER;
|
||||
|
||||
@Getter
|
||||
public final class Verbose implements Observable {
|
||||
private final @NotNull Collection<UUID> subscribers = new Vector<>(0);
|
||||
public final class Verbose extends Observable {
|
||||
private int animationIndex;
|
||||
|
||||
// Run action bar verbose
|
||||
public void update() {
|
||||
// Make sure to clean up the cached statistics
|
||||
Sonar.get().getStatistics().cleanUpCache();
|
||||
@Override
|
||||
public void observe() {
|
||||
// Don't prepare component if there are no subscribers
|
||||
if (subscribers.isEmpty()) return;
|
||||
// Prepare the action bar format component
|
||||
final Component component = prepareActionBarFormat();
|
||||
if (subscribers.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Prepare the action bar verbose
|
||||
final AttackTracker.AttackStatistics attackStatistics = Sonar.get().getAttackTracker().getCurrentAttack();
|
||||
final SystemTimer attackTimer = attackStatistics == null ? null : attackStatistics.getDuration();
|
||||
final String attackDuration = attackTimer == null ? "00:00" : DATE_FORMATTER.format(attackTimer.delay());
|
||||
|
||||
final Component actionBarComponent = MiniMessage.miniMessage().deserialize(attackTimer == null
|
||||
? Sonar.get().getConfig().getMessagesConfig().getString("verbose.layout.normal")
|
||||
: Sonar.get().getConfig().getMessagesConfig().getString("verbose.layout.attack"),
|
||||
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix()),
|
||||
Placeholder.unparsed("attack-duration", attackDuration),
|
||||
Placeholder.unparsed("animation", nextAnimation()),
|
||||
Placeholder.unparsed("queued", DECIMAL_FORMAT.format(Sonar.get().getFallback().getQueue().getPlayers().size())),
|
||||
Placeholder.unparsed("verifying", DECIMAL_FORMAT.format(Sonar.get().getFallback().getConnected().size())),
|
||||
Placeholder.unparsed("blacklisted", DECIMAL_FORMAT.format(Sonar.get().getFallback().getBlacklist().estimatedSize())),
|
||||
Placeholder.unparsed("total-joins", DECIMAL_FORMAT.format(Sonar.get().getStatistics().getTotalPlayersJoined())),
|
||||
Placeholder.unparsed("logins-per-second", DECIMAL_FORMAT.format(Sonar.get().getStatistics().getLoginsPerSecond())),
|
||||
Placeholder.unparsed("connections-per-second", DECIMAL_FORMAT.format(Sonar.get().getStatistics().getConnectionsPerSecond())),
|
||||
Placeholder.unparsed("verify-total", DECIMAL_FORMAT.format(Sonar.get().getStatistics().getTotalAttemptedVerifications())),
|
||||
Placeholder.unparsed("verify-success", DECIMAL_FORMAT.format(Sonar.get().getStatistics().getTotalSuccessfulVerifications())),
|
||||
Placeholder.unparsed("verify-failed", DECIMAL_FORMAT.format(Sonar.get().getStatistics().getTotalFailedVerifications())),
|
||||
Placeholder.unparsed("incoming-traffic", Sonar.get().getStatistics().getCurrentIncomingBandwidthFormatted()),
|
||||
Placeholder.unparsed("outgoing-traffic", Sonar.get().getStatistics().getCurrentOutgoingBandwidthFormatted()),
|
||||
Placeholder.unparsed("incoming-traffic-ttl", Sonar.get().getStatistics().getTotalIncomingBandwidthFormatted()),
|
||||
Placeholder.unparsed("outgoing-traffic-ttl", Sonar.get().getStatistics().getTotalOutgoingBandwidthFormatted()),
|
||||
Placeholder.unparsed("used-memory", formatMemory(getUsedMemory())),
|
||||
Placeholder.unparsed("free-memory", formatMemory(getFreeMemory())),
|
||||
Placeholder.unparsed("total-memory", formatMemory(getTotalMemory())),
|
||||
Placeholder.unparsed("max-memory", formatMemory(getMaxMemory())));
|
||||
|
||||
// Send the action bar to all online players
|
||||
for (final UUID subscriber : subscribers) {
|
||||
final Audience audience = Sonar.get().audience(subscriber);
|
||||
if (audience == null) continue;
|
||||
audience.sendActionBar(component);
|
||||
|
||||
// Send the action bar to all online subscribers
|
||||
audience.sendActionBar(actionBarComponent);
|
||||
}
|
||||
}
|
||||
|
||||
@ApiStatus.Internal
|
||||
public @NotNull Component prepareActionBarFormat() {
|
||||
final SonarStatistics statistics = Sonar.get().getStatistics();
|
||||
final AttackTracker.AttackStatistics attackStatistics = Sonar.get().getAttackTracker().getCurrentAttack();
|
||||
final SystemTimer attackDuration = attackStatistics == null ? null : attackStatistics.getDuration();
|
||||
return MiniMessage.miniMessage().deserialize((attackDuration != null
|
||||
? Sonar.get().getConfig().getVerbose().getActionBarLayoutDuringAttack()
|
||||
: Sonar.get().getConfig().getVerbose().getActionBarLayout())
|
||||
.replace("%queued%",
|
||||
DECIMAL_FORMAT.format(Sonar.get().getFallback().getQueue().getQueuedPlayers().size()))
|
||||
.replace("%verifying%", DECIMAL_FORMAT.format(Sonar.get().getFallback().getConnected().size()))
|
||||
.replace("%blacklisted%",
|
||||
DECIMAL_FORMAT.format(Sonar.get().getFallback().getBlacklist().estimatedSize()))
|
||||
.replace("%blacklisted%", DECIMAL_FORMAT.format(statistics.getCurrentBlacklistSize()))
|
||||
.replace("%total-joins%", DECIMAL_FORMAT.format(statistics.getTotalPlayersJoined()))
|
||||
.replace("%logins-per-second%", DECIMAL_FORMAT.format(statistics.getLoginsPerSecond()))
|
||||
.replace("%connections-per-second%", DECIMAL_FORMAT.format(statistics.getConnectionsPerSecond()))
|
||||
.replace("%verify-total%", DECIMAL_FORMAT.format(statistics.getTotalAttemptedVerifications()))
|
||||
.replace("%verify-success%",
|
||||
DECIMAL_FORMAT.format(Sonar.get().getVerifiedPlayerController().estimatedSize()))
|
||||
.replace("%verify-failed%", DECIMAL_FORMAT.format(statistics.getTotalFailedVerifications()))
|
||||
.replace("%attack-duration%", attackDuration == null ? "00:00" : attackDuration.formattedDelay())
|
||||
.replace("%incoming-traffic%", statistics.getCurrentIncomingBandwidthFormatted())
|
||||
.replace("%outgoing-traffic%", statistics.getCurrentOutgoingBandwidthFormatted())
|
||||
.replace("%incoming-traffic-ttl%", statistics.getTotalIncomingBandwidthFormatted())
|
||||
.replace("%outgoing-traffic-ttl%", statistics.getTotalOutgoingBandwidthFormatted())
|
||||
.replace("%used-memory%", formatMemory(getUsedMemory()))
|
||||
.replace("%free-memory%", formatMemory(getFreeMemory()))
|
||||
.replace("%total-memory%", formatMemory(getTotalMemory()))
|
||||
.replace("%max-memory%", formatMemory(getMaxMemory()))
|
||||
.replace("%animation%", nextAnimation()));
|
||||
}
|
||||
|
||||
public String nextAnimation() {
|
||||
val animations = Sonar.get().getConfig().getVerbose().getAnimation();
|
||||
final var animations = Sonar.get().getConfig().getVerboseAnimation();
|
||||
final int nextIndex = ++animationIndex % animations.size();
|
||||
return animations.toArray(new String[0])[nextIndex];
|
||||
}
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
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
|
||||
version = rootProject.version.toString().split("-")[0]
|
||||
description = rootProject.description
|
||||
version = rootProject.version.toString()
|
||||
main = "xyz.jonesdev.sonar.bukkit.SonarBukkitPlugin"
|
||||
authors = listOf("Jones Development", "Sonar Contributors")
|
||||
website = "https://jonesdev.xyz/discord/"
|
||||
load = BukkitPluginDescription.PluginLoadOrder.POSTWORLD
|
||||
softDepend = listOf("Geyser-Spigot", "floodgate", "Protocolize", "packetevents", "ProtocolLib", "ViaVersion")
|
||||
softDepend = listOf("Geyser-Spigot", "floodgate", "Protocolize", "ProtocolSupport",
|
||||
"ViaVersion", "packetevents", "ProtocolLib", "FastLogin")
|
||||
foliaSupported = true
|
||||
|
||||
commands {
|
||||
@@ -32,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 {
|
||||
|
||||
@@ -25,12 +25,10 @@ import org.bstats.bukkit.Metrics;
|
||||
import org.bstats.charts.SimplePie;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.SonarPlatform;
|
||||
import xyz.jonesdev.sonar.api.logger.LoggerWrapper;
|
||||
import xyz.jonesdev.sonar.bukkit.command.BukkitSonarCommand;
|
||||
import xyz.jonesdev.sonar.common.boot.SonarBootstrap;
|
||||
import xyz.jonesdev.sonar.common.statistics.CachedBandwidthStatistics;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -98,23 +95,13 @@ public final class SonarBukkit extends SonarBootstrap<SonarBukkitPlugin> {
|
||||
|
||||
// Register Sonar command
|
||||
Objects.requireNonNull(getPlugin().getCommand("sonar")).setExecutor(new BukkitSonarCommand());
|
||||
|
||||
// Register traffic service
|
||||
getPlugin().getServer().getScheduler().runTaskTimerAsynchronously(getPlugin(),
|
||||
CachedBandwidthStatistics::reset, 20L, 20L);
|
||||
|
||||
// Register queue service
|
||||
getPlugin().getServer().getScheduler().runTaskTimerAsynchronously(getPlugin(),
|
||||
getFallback().getQueue().getPollTask(), 10L, 10L);
|
||||
|
||||
// Register verbose service
|
||||
getPlugin().getServer().getScheduler().runTaskTimerAsynchronously(getPlugin(),
|
||||
Sonar.get().getVerboseHandler()::update, 4L, 4L);
|
||||
}
|
||||
|
||||
@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,40 +1,29 @@
|
||||
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
|
||||
version = rootProject.version.toString().split("-")[0]
|
||||
description = rootProject.description
|
||||
version = rootProject.version.toString()
|
||||
main = "xyz.jonesdev.sonar.bungee.SonarBungeePlugin"
|
||||
author = "Jones Development, Sonar Contributors"
|
||||
softDepends = setOf("Geyser-BungeeCord", "floodgate", "Protocolize", "ViaVersion")
|
||||
softDepends = setOf("Geyser-BungeeCord", "floodgate", "Protocolize", "ViaVersion", "packetevents", "FastLogin")
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||
@@ -25,24 +25,20 @@ import org.bstats.bungeecord.Metrics;
|
||||
import org.bstats.charts.SimplePie;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.SonarPlatform;
|
||||
import xyz.jonesdev.sonar.api.logger.LoggerWrapper;
|
||||
import xyz.jonesdev.sonar.bungee.command.BungeeSonarCommand;
|
||||
import xyz.jonesdev.sonar.bungee.fallback.FallbackInjectionHelper;
|
||||
import xyz.jonesdev.sonar.bungee.fallback.FallbackLoginListener;
|
||||
import xyz.jonesdev.sonar.common.boot.SonarBootstrap;
|
||||
import xyz.jonesdev.sonar.common.statistics.CachedBandwidthStatistics;
|
||||
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@Getter
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -99,28 +95,15 @@ public final class SonarBungee extends SonarBootstrap<SonarBungeePlugin> {
|
||||
// Register Sonar command
|
||||
getPlugin().getServer().getPluginManager().registerCommand(getPlugin(), new BungeeSonarCommand());
|
||||
|
||||
// Register Fallback listener
|
||||
getPlugin().getServer().getPluginManager().registerListener(getPlugin(), new FallbackLoginListener());
|
||||
|
||||
// Register traffic service
|
||||
getPlugin().getServer().getScheduler().schedule(getPlugin(), CachedBandwidthStatistics::reset,
|
||||
1L, 1L, TimeUnit.SECONDS);
|
||||
|
||||
// Register queue service
|
||||
getPlugin().getServer().getScheduler().schedule(getPlugin(), getFallback().getQueue().getPollTask(),
|
||||
500L, 500L, TimeUnit.MILLISECONDS);
|
||||
|
||||
// Register verbose service
|
||||
getPlugin().getServer().getScheduler().schedule(getPlugin(), Sonar.get().getVerboseHandler()::update,
|
||||
200L, 200L, TimeUnit.MILLISECONDS);
|
||||
|
||||
// Make sure to inject into the server's connection handler
|
||||
FallbackInjectionHelper.inject();
|
||||
}
|
||||
|
||||
@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();
|
||||
}
|
||||
}
|
||||
}
|
||||
+22
-142
@@ -19,7 +19,6 @@ package xyz.jonesdev.sonar.bungee.fallback;
|
||||
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.handler.codec.CorruptedFrameException;
|
||||
import net.md_5.bungee.netty.ChannelWrapper;
|
||||
import net.md_5.bungee.netty.HandlerBoss;
|
||||
import net.md_5.bungee.protocol.DefinedPacket;
|
||||
@@ -28,24 +27,13 @@ import net.md_5.bungee.protocol.packet.Handshake;
|
||||
import net.md_5.bungee.protocol.packet.LoginRequest;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.ReflectiveOperationException;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.common.fallback.FallbackBandwidthHandler;
|
||||
import xyz.jonesdev.sonar.common.fallback.FallbackChannelHandlerAdapter;
|
||||
import xyz.jonesdev.sonar.common.fallback.FallbackUserWrapper;
|
||||
import xyz.jonesdev.sonar.common.statistics.GlobalSonarStatistics;
|
||||
import xyz.jonesdev.sonar.common.utility.geyser.GeyserUtil;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.invoke.MethodHandle;
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.UUID;
|
||||
|
||||
import static net.md_5.bungee.netty.PipelineUtils.*;
|
||||
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.*;
|
||||
|
||||
public final class FallbackChannelHandler extends FallbackChannelHandlerAdapter {
|
||||
|
||||
@@ -53,12 +41,12 @@ public final class FallbackChannelHandler extends FallbackChannelHandlerAdapter
|
||||
super(channel);
|
||||
}
|
||||
|
||||
private static final Field CHANNEL_WRAPPER_FIELD;
|
||||
private static final MethodHandle CHANNEL_WRAPPER_GETTER;
|
||||
|
||||
static {
|
||||
try {
|
||||
CHANNEL_WRAPPER_FIELD = HandlerBoss.class.getDeclaredField("channel");
|
||||
CHANNEL_WRAPPER_FIELD.setAccessible(true);
|
||||
CHANNEL_WRAPPER_GETTER = MethodHandles.privateLookupIn(HandlerBoss.class, MethodHandles.lookup())
|
||||
.findGetter(HandlerBoss.class, "channel", ChannelWrapper.class);
|
||||
} catch (Exception exception) {
|
||||
throw new ReflectiveOperationException(exception);
|
||||
}
|
||||
@@ -67,18 +55,31 @@ 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
|
||||
if (wrappedPacket != null) {
|
||||
// Intercept any handshake packet by the client
|
||||
if (wrappedPacket instanceof Handshake) {
|
||||
handleHandshake((Handshake) wrappedPacket);
|
||||
final Handshake handshake = (Handshake) wrappedPacket;
|
||||
handleHandshake(handshake.getHost(), handshake.getProtocolVersion());
|
||||
}
|
||||
// Intercept any server login packet by the client
|
||||
if (wrappedPacket instanceof LoginRequest) {
|
||||
handleLogin(ctx, (LoginRequest) wrappedPacket, msg);
|
||||
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);
|
||||
final ChannelWrapper channelWrapper;
|
||||
try {
|
||||
channelWrapper = (ChannelWrapper) CHANNEL_WRAPPER_GETTER.invokeExact(handlerBoss);
|
||||
} catch (final Throwable throwable) {
|
||||
throw new ReflectiveOperationException(throwable);
|
||||
}
|
||||
final InetSocketAddress socketAddress = (InetSocketAddress) channelWrapper.getRemoteAddress();
|
||||
handleLogin(ctx, msg, loginRequest.getData(), socketAddress,
|
||||
PACKET_ENCODER, PACKET_DECODER, TIMEOUT_HANDLER, BOSS_HANDLER);
|
||||
packetWrapper.trySingleRelease();
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -86,125 +87,4 @@ public final class FallbackChannelHandler extends FallbackChannelHandlerAdapter
|
||||
// Make sure to let the server handle the rest
|
||||
ctx.fireChannelRead(msg);
|
||||
}
|
||||
|
||||
private void handleHandshake(final @NotNull Handshake handshake) throws Exception {
|
||||
// Check if the player has already sent a handshake packet
|
||||
if (protocolVersion != null) {
|
||||
throw new CorruptedFrameException("Already sent handshake");
|
||||
}
|
||||
// Check if the hostname is invalid
|
||||
if (handshake.getHost().isEmpty()) {
|
||||
throw new CorruptedFrameException("Hostname is empty");
|
||||
}
|
||||
// Store the protocol version
|
||||
protocolVersion = ProtocolVersion.fromId(handshake.getProtocolVersion());
|
||||
// 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!
|
||||
throw new CorruptedFrameException("Unknown protocol");
|
||||
}
|
||||
// Hook the traffic listener
|
||||
// TODO: Can we implement this in channelActive?
|
||||
channel.pipeline().addFirst(FALLBACK_BANDWIDTH, FallbackBandwidthHandler.INSTANCE);
|
||||
}
|
||||
|
||||
private void handleLogin(final @NotNull ChannelHandlerContext ctx,
|
||||
final @NotNull LoginRequest loginRequest,
|
||||
final @NotNull Object wrappedMessage) throws Exception {
|
||||
// Check if the player has already sent a login packet
|
||||
if (username != null) {
|
||||
throw new CorruptedFrameException("Already logged on");
|
||||
}
|
||||
// Increase joins per second for the action bar verbose
|
||||
GlobalSonarStatistics.countLogin();
|
||||
// Store the username
|
||||
username = loginRequest.getData();
|
||||
// Make sure to use the potentially modified, real IP
|
||||
final HandlerBoss handlerBoss = channel.pipeline().get(HandlerBoss.class);
|
||||
final ChannelWrapper channelWrapper = (ChannelWrapper) CHANNEL_WRAPPER_FIELD.get(handlerBoss);
|
||||
final InetSocketAddress socketAddress = (InetSocketAddress) channelWrapper.getRemoteAddress();
|
||||
inetAddress = socketAddress.getAddress();
|
||||
|
||||
// Check the blacklist here since we cannot let the player "ghost join"
|
||||
if (FALLBACK.getBlacklist().asMap().containsKey(inetAddress)) {
|
||||
customDisconnect(channel, protocolVersion, blacklisted, PACKET_ENCODER, BOSS_HANDLER);
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't continue the verification process if the verification is disabled
|
||||
if (!Sonar.get().getFallback().shouldVerifyNewPlayers()) {
|
||||
ctx.fireChannelRead(wrappedMessage);
|
||||
deject(channel.pipeline());
|
||||
return;
|
||||
}
|
||||
|
||||
// Completely skip Geyser connections
|
||||
if (GeyserUtil.isGeyserConnection(channel, socketAddress)) {
|
||||
ctx.fireChannelRead(wrappedMessage);
|
||||
deject(channel.pipeline());
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the player is already queued since we don't want bots to flood the queue
|
||||
if (FALLBACK.getQueue().getQueuedPlayers().containsKey(inetAddress)) {
|
||||
customDisconnect(channel, protocolVersion, alreadyQueued, PACKET_ENCODER, BOSS_HANDLER);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if Fallback is already verifying a player
|
||||
// → is another player with the same IP address connected to Fallback?
|
||||
if (FALLBACK.getConnected().containsKey(loginRequest.getData())
|
||||
|| FALLBACK.getConnected().containsValue(inetAddress)) {
|
||||
customDisconnect(channel, protocolVersion, alreadyVerifying, PACKET_ENCODER, BOSS_HANDLER);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the protocol ID of the player is not allowed to enter the server
|
||||
if (Sonar.get().getConfig().getVerification().getBlacklistedProtocols()
|
||||
.contains(protocolVersion.getProtocol())) {
|
||||
customDisconnect(channel, protocolVersion, protocolBlacklisted, PACKET_ENCODER, BOSS_HANDLER);
|
||||
return;
|
||||
}
|
||||
|
||||
// Make sure we actually have to verify the player
|
||||
final String offlineUUIDString = "OfflinePlayer:" + loginRequest.getData();
|
||||
final UUID offlineUUID = UUID.nameUUIDFromBytes(offlineUUIDString.getBytes(StandardCharsets.UTF_8));
|
||||
if (Sonar.get().getVerifiedPlayerController().has(inetAddress, offlineUUID)) {
|
||||
ctx.fireChannelRead(wrappedMessage);
|
||||
deject(channel.pipeline());
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the IP address is currently being rate-limited
|
||||
if (!FALLBACK.getRatelimiter().attempt(inetAddress)) {
|
||||
customDisconnect(channel, protocolVersion, reconnectedTooFast, PACKET_ENCODER, BOSS_HANDLER);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the protocol ID of the player is allowed to bypass verification
|
||||
if (Sonar.get().getConfig().getVerification().getWhitelistedProtocols()
|
||||
.contains(protocolVersion.getProtocol())) {
|
||||
ctx.fireChannelRead(wrappedMessage);
|
||||
deject(channel.pipeline());
|
||||
return;
|
||||
}
|
||||
|
||||
// Queue the connection for further processing
|
||||
FALLBACK.getQueue().queue(inetAddress, () -> channel.eventLoop().execute(() -> {
|
||||
// Check if the username matches the valid name regex to prevent
|
||||
// UTF-16 names or other types of exploits
|
||||
if (!Sonar.get().getConfig().getVerification().getValidNameRegex()
|
||||
.matcher(loginRequest.getData()).matches()) {
|
||||
customDisconnect(channel, protocolVersion, invalidUsername, PACKET_ENCODER, BOSS_HANDLER);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create an instance for the Fallback connection
|
||||
user = new FallbackUserWrapper(channel, inetAddress, protocolVersion);
|
||||
// Let the verification handler take over the channel
|
||||
user.hijack(loginRequest.getData(), offlineUUID, PACKET_ENCODER, PACKET_DECODER, TIMEOUT_HANDLER, BOSS_HANDLER);
|
||||
}));
|
||||
}
|
||||
}
|
||||
+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);
|
||||
}
|
||||
|
||||
-84
@@ -1,84 +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.bungee.fallback;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer;
|
||||
import net.md_5.bungee.api.connection.PendingConnection;
|
||||
import net.md_5.bungee.api.event.PreLoginEvent;
|
||||
import net.md_5.bungee.api.plugin.Listener;
|
||||
import net.md_5.bungee.chat.ComponentSerializer;
|
||||
import net.md_5.bungee.connection.InitialHandler;
|
||||
import net.md_5.bungee.event.EventHandler;
|
||||
import net.md_5.bungee.event.EventPriority;
|
||||
import net.md_5.bungee.netty.ChannelWrapper;
|
||||
import net.md_5.bungee.protocol.packet.Kick;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.ReflectiveOperationException;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.bungee.SonarBungee;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.net.InetAddress;
|
||||
import java.util.Objects;
|
||||
|
||||
import static xyz.jonesdev.sonar.common.fallback.FallbackUserWrapper.closeWith;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public final class FallbackLoginListener implements Listener {
|
||||
|
||||
private static final Field CHANNEL_FIELD;
|
||||
|
||||
static {
|
||||
try {
|
||||
CHANNEL_FIELD = InitialHandler.class.getDeclaredField("ch");
|
||||
CHANNEL_FIELD.setAccessible(true);
|
||||
} catch (NoSuchFieldException exception) {
|
||||
throw new ReflectiveOperationException(exception);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@EventHandler(priority = EventPriority.LOWEST)
|
||||
public void handle(final @NotNull PreLoginEvent event) throws IllegalAccessException {
|
||||
// Check if the number of online players using the same IP address as
|
||||
// the connecting player is greater than the configured amount
|
||||
final int maxOnlinePerIp = Sonar.get().getConfig().getMaxOnlinePerIp();
|
||||
// Don't do anything if the setting is disabled
|
||||
if (maxOnlinePerIp <= 0) return;
|
||||
|
||||
final InetAddress inetAddress = event.getConnection().getAddress().getAddress();
|
||||
|
||||
final long onlinePerIp = SonarBungee.INSTANCE.getPlugin().getServer().getPlayers().stream()
|
||||
.filter(player -> Objects.equals(player.getAddress().getAddress(), inetAddress))
|
||||
.count()
|
||||
+ 1 /* add 1 because the player hasn't been added to the list of online players yet */;
|
||||
|
||||
// We use '>=' because the player connecting to the server hasn't joined yet
|
||||
if (onlinePerIp >= maxOnlinePerIp) {
|
||||
final Component component = Sonar.get().getConfig().getTooManyOnlinePerIp();
|
||||
final String serialized = GsonComponentSerializer.gson().serialize(component);
|
||||
final PendingConnection connection = event.getConnection();
|
||||
closeWith(((ChannelWrapper) CHANNEL_FIELD.get(connection)).getHandle(),
|
||||
ProtocolVersion.fromId(connection.getVersion()),
|
||||
new Kick(ComponentSerializer.deserialize(serialized)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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:capja:0.1.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;
|
||||
@@ -36,8 +37,9 @@ import xyz.jonesdev.sonar.api.timer.SystemTimer;
|
||||
import xyz.jonesdev.sonar.api.verbose.Notification;
|
||||
import xyz.jonesdev.sonar.api.verbose.Verbose;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer;
|
||||
import xyz.jonesdev.sonar.common.service.SonarServiceManager;
|
||||
import xyz.jonesdev.sonar.common.statistics.GlobalSonarStatistics;
|
||||
import xyz.jonesdev.sonar.common.subcommand.SubcommandRegistryHolder;
|
||||
import xyz.jonesdev.sonar.common.subcommands.*;
|
||||
import xyz.jonesdev.sonar.common.update.UpdateChecker;
|
||||
|
||||
import java.io.File;
|
||||
@@ -53,40 +55,44 @@ 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();
|
||||
this.notificationHandler = new Notification();
|
||||
this.config = new SonarConfiguration(dataDirectory);
|
||||
this.subcommandRegistry = new SubcommandRegistryHolder();
|
||||
this.subcommandRegistry = new SubcommandRegistry();
|
||||
// Register all subcommands
|
||||
this.subcommandRegistry.register(
|
||||
new BlacklistCommand(),
|
||||
new VerifiedCommand(),
|
||||
new StatisticsCommand(),
|
||||
new VerboseCommand(),
|
||||
new ReloadCommand(),
|
||||
new DumpCommand(),
|
||||
new NotifyCommand());
|
||||
// Hide unnecessary debug information
|
||||
Logger.setGlobalLogLevel(Level.WARNING);
|
||||
}
|
||||
|
||||
public final void initialize() {
|
||||
// Check if the branch is not the main branch to warn about unstable versions
|
||||
if (!getVersion().isOnMainBranch()) {
|
||||
if (!getVersion().getGitBranch().equals("main")) {
|
||||
getLogger().warn("You are currently using an unofficial experimental branch.");
|
||||
getLogger().warn("It is highly recommended to use the latest stable release of Sonar:");
|
||||
getLogger().warn("https://github.com/jonesdevelopment/sonar/releases");
|
||||
@@ -105,6 +111,10 @@ public abstract class SonarBootstrap<T> implements Sonar {
|
||||
// Run the per-platform initialization method
|
||||
enable();
|
||||
|
||||
// Start threads
|
||||
getLogger().info("Starting all managed threads...");
|
||||
SonarServiceManager.start();
|
||||
|
||||
// Done
|
||||
getLogger().info("Done ({}s)!", launchTimer);
|
||||
} catch (Throwable throwable) {
|
||||
@@ -114,8 +124,10 @@ public abstract class SonarBootstrap<T> implements Sonar {
|
||||
return; // Do not check for updates if the launch failed
|
||||
}
|
||||
|
||||
// Check if a new version has been released
|
||||
UpdateChecker.checkForUpdates();
|
||||
// Check if a new version has been released if enabled in the configuration
|
||||
if (Sonar.get().getConfig().getGeneralConfig().getBoolean("general.check-for-updates")) {
|
||||
UpdateChecker.checkForUpdates();
|
||||
}
|
||||
}
|
||||
|
||||
public abstract void enable();
|
||||
@@ -131,14 +143,17 @@ public abstract class SonarBootstrap<T> implements Sonar {
|
||||
}
|
||||
|
||||
// Prepare cached packets
|
||||
getLogger().info("Taking cached snapshots of all packets...");
|
||||
FallbackPreparer.prepare();
|
||||
|
||||
// Update ratelimiter caches
|
||||
getFallback().getRatelimiter().setAttemptCache(Caffeine.newBuilder()
|
||||
.expireAfterWrite(Duration.ofMillis(getConfig().getVerification().getReconnectDelay()))
|
||||
.ticker(Ticker.systemTicker())
|
||||
.build());
|
||||
getFallback().getRatelimiter().setFailCountCache(Caffeine.newBuilder()
|
||||
.expireAfterWrite(Duration.ofMillis(getConfig().getVerification().getRememberTime()))
|
||||
.ticker(Ticker.systemTicker())
|
||||
.build());
|
||||
|
||||
// Update blacklist cache
|
||||
@@ -147,9 +162,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);
|
||||
@@ -168,10 +184,14 @@ public abstract class SonarBootstrap<T> implements Sonar {
|
||||
}
|
||||
|
||||
public final void shutdown() {
|
||||
// Initialize shutdown process
|
||||
// Initialize the shutdown process
|
||||
getLogger().info("Starting shutdown process...");
|
||||
// Close database connection
|
||||
verifiedPlayerController.close();
|
||||
// Interrupt threads
|
||||
SonarServiceManager.stop();
|
||||
// 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
|
||||
|
||||
+180
-16
@@ -20,6 +20,7 @@ package xyz.jonesdev.sonar.common.fallback;
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.channel.ChannelInboundHandlerAdapter;
|
||||
import io.netty.handler.codec.CorruptedFrameException;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
@@ -28,8 +29,16 @@ import xyz.jonesdev.sonar.api.fallback.Fallback;
|
||||
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.common.statistics.GlobalSonarStatistics;
|
||||
import xyz.jonesdev.sonar.common.util.GeyserDetection;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.UUID;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_BANDWIDTH;
|
||||
import static xyz.jonesdev.sonar.common.fallback.FallbackUserWrapper.customDisconnect;
|
||||
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer.*;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public class FallbackChannelHandlerAdapter extends ChannelInboundHandlerAdapter {
|
||||
@@ -37,12 +46,12 @@ 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();
|
||||
// Make sure to let the server handle the rest
|
||||
@@ -50,22 +59,16 @@ public class FallbackChannelHandlerAdapter extends ChannelInboundHandlerAdapter
|
||||
}
|
||||
|
||||
@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().compute(inetAddress, (key, value) -> null);
|
||||
// Remove the IP address from the queue
|
||||
FALLBACK.getQueue().remove(inetAddress);
|
||||
}
|
||||
// The player cannot be in the verification if the user object doesn't exist,
|
||||
// or if the user failed/succeeded the bot verification process.
|
||||
if (user != null && user.getState().canReceivePackets()) {
|
||||
// Mark the verification attempt as failed
|
||||
user.fail("disconnected during verification");
|
||||
FALLBACK.getQueue().getPlayers().compute(inetAddress, (key, value) -> null);
|
||||
// Remove this account from the online players or decrement the number of accounts with the same IP
|
||||
FALLBACK.getOnline().compute(inetAddress, (key, value) -> value == null || value <= 1 ? null : value - 1);
|
||||
}
|
||||
// Make sure to let the server handle the rest
|
||||
ctx.fireChannelInactive();
|
||||
@@ -77,7 +80,168 @@ 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();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates and handles incoming handshake packets
|
||||
*
|
||||
* @param hostname Hostname (server address) sent by the client
|
||||
* @param protocol Protocol version number sent by the client
|
||||
*/
|
||||
protected final void handleHandshake(final @NotNull String hostname, final int protocol) throws Exception {
|
||||
// Check if the hostname is invalid
|
||||
if (hostname.isEmpty()) {
|
||||
throw new CorruptedFrameException("Hostname is empty");
|
||||
}
|
||||
// Check if the player has already sent a handshake packet
|
||||
if (protocolVersion != null) {
|
||||
throw new CorruptedFrameException("Already sent handshake");
|
||||
}
|
||||
// Store the protocol version
|
||||
protocolVersion = ProtocolVersion.fromId(protocol);
|
||||
// Hook the traffic listener
|
||||
channel.pipeline().addFirst(FALLBACK_BANDWIDTH, FallbackBandwidthHandler.INSTANCE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates and handles incoming login packets
|
||||
*
|
||||
* @param ctx Forwarded channel handler context
|
||||
* @param loginPacket Login packet sent by the client
|
||||
* @param username Username sent by the client
|
||||
* @param socketAddress Socket address of the client
|
||||
*/
|
||||
protected final void handleLogin(final @NotNull ChannelHandlerContext ctx,
|
||||
final @NotNull Object loginPacket,
|
||||
final @NotNull String username,
|
||||
final @NotNull InetSocketAddress socketAddress,
|
||||
final @NotNull String encoder,
|
||||
final @NotNull String decoder,
|
||||
final @NotNull String timeout,
|
||||
final @NotNull String handler) throws Exception {
|
||||
// Check if the player has already sent a login packet
|
||||
if (this.username != null) {
|
||||
throw new CorruptedFrameException("Already sent login");
|
||||
}
|
||||
// Connections from unknown protocol versions will be discarded
|
||||
// as this is the safest way of handling unwanted connections
|
||||
if (protocolVersion.isUnknown()) {
|
||||
// Sonar does not support snapshots or Minecraft versions older than 1.7.2
|
||||
throw new CorruptedFrameException("Unknown protocol version");
|
||||
}
|
||||
// Increase joins per second for the action bar verbose
|
||||
GlobalSonarStatistics.countLogin();
|
||||
// Store the username and IP address
|
||||
this.username = username;
|
||||
this.inetAddress = socketAddress.getAddress();
|
||||
|
||||
// Check the blacklist here since we cannot let the player "ghost join"
|
||||
if (FALLBACK.getBlacklist().asMap().containsKey(inetAddress)) {
|
||||
customDisconnect(channel, protocolVersion, blacklisted, encoder, handler);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the player is already queued since we don't want bots to flood the queue
|
||||
if (FALLBACK.getQueue().getPlayers().containsKey(inetAddress)) {
|
||||
customDisconnect(channel, protocolVersion, alreadyQueued, encoder, handler);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if Fallback is already verifying a player with the same IP address
|
||||
if (FALLBACK.getConnected().containsKey(inetAddress)) {
|
||||
customDisconnect(channel, protocolVersion, alreadyVerifying, encoder, handler);
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't continue the verification process if the verification is disabled
|
||||
if (!Sonar.get().getFallback().shouldVerifyNewPlayers()) {
|
||||
initialLogin(ctx, loginPacket, encoder, handler);
|
||||
return;
|
||||
}
|
||||
|
||||
// Completely skip Geyser connections if configured
|
||||
final boolean geyser = GeyserDetection.isGeyserConnection(channel, socketAddress);
|
||||
if (geyser && !Sonar.get().getConfig().getVerification().isCheckGeyser()) {
|
||||
initialLogin(ctx, loginPacket, encoder, handler);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the protocol ID of the player is not allowed to enter the server
|
||||
if (Sonar.get().getConfig().getVerification().getBlacklistedProtocols()
|
||||
.contains(protocolVersion.getProtocol())) {
|
||||
customDisconnect(channel, protocolVersion, protocolBlacklisted, encoder, handler);
|
||||
return;
|
||||
}
|
||||
|
||||
// Make sure we actually have to verify the player
|
||||
final String offlineUUIDString = "OfflinePlayer:" + username;
|
||||
final UUID offlineUUID = UUID.nameUUIDFromBytes(offlineUUIDString.getBytes(StandardCharsets.UTF_8));
|
||||
if (Sonar.get().getVerifiedPlayerController().has(inetAddress.toString(), offlineUUID)) {
|
||||
initialLogin(ctx, loginPacket, encoder, handler);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the IP address is currently being rate-limited
|
||||
if (!FALLBACK.getRatelimiter().attempt(inetAddress)) {
|
||||
customDisconnect(channel, protocolVersion, reconnectedTooFast, encoder, handler);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the protocol ID of the player is allowed to bypass verification
|
||||
if (Sonar.get().getConfig().getVerification().getWhitelistedProtocols()
|
||||
.contains(protocolVersion.getProtocol())) {
|
||||
initialLogin(ctx, loginPacket, encoder, handler);
|
||||
return;
|
||||
}
|
||||
|
||||
// Queue the connection for further processing
|
||||
FALLBACK.getQueue().getPlayers().compute(inetAddress, (key, value) -> () -> {
|
||||
// Check if the username matches the valid name regex to prevent
|
||||
// UTF-16 names or other types of exploits
|
||||
if (!Sonar.get().getConfig().getVerification().getValidNameRegex()
|
||||
.matcher(username).matches()) {
|
||||
customDisconnect(channel, protocolVersion, invalidUsername, encoder, handler);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create an instance for the Fallback connection
|
||||
final FallbackUser user = new FallbackUserWrapper(channel, inetAddress, protocolVersion, geyser);
|
||||
// Let the verification handler take over the channel
|
||||
user.hijack(username, offlineUUID, encoder, decoder, timeout, handler);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the maximum accounts per IP limit check before letting the player join
|
||||
*
|
||||
* @param ctx Forwarded channel handler context
|
||||
* @param loginPacket Login packet sent by the client
|
||||
*/
|
||||
protected final void initialLogin(final @NotNull ChannelHandlerContext ctx,
|
||||
final @NotNull Object loginPacket,
|
||||
final @NotNull String encoder,
|
||||
final @NotNull String handler) throws Exception {
|
||||
// Increment the number of accounts with the same IP
|
||||
FALLBACK.getOnline().compute(inetAddress, (key, value) -> value == null ? 1 : value + 1);
|
||||
|
||||
final int maxOnlinePerIp = Sonar.get().getConfig().getMaxOnlinePerIp();
|
||||
// Skip the maximum online per IP check if it's disabled in the configuration
|
||||
if (maxOnlinePerIp > 0) {
|
||||
// Check if the number of online players using the same IP address as
|
||||
// the connecting player is greater than the configured amount
|
||||
final int onlinePerIp = FALLBACK.getOnline().getOrDefault(inetAddress, 0);
|
||||
if (onlinePerIp >= maxOnlinePerIp) {
|
||||
customDisconnect(channel, protocolVersion, tooManyOnlinePerIP, encoder, handler);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Don't listen for further packets
|
||||
// TODO: deject the pipeline without breaking disconnect logic
|
||||
listenForPackets = false;
|
||||
// Let the server know about the login packet
|
||||
ctx.fireChannelRead(loginPacket);
|
||||
}
|
||||
}
|
||||
+7
-20
@@ -18,7 +18,6 @@
|
||||
package xyz.jonesdev.sonar.common.fallback;
|
||||
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.handler.timeout.IdleState;
|
||||
import io.netty.handler.timeout.IdleStateEvent;
|
||||
import io.netty.handler.timeout.IdleStateHandler;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
@@ -26,30 +25,18 @@ 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);
|
||||
public FallbackTimeoutHandler(final int readTimeout, final int writeTimeout, final TimeUnit timeUnit) {
|
||||
super(readTimeout, writeTimeout, 0L, timeUnit);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void channelIdle(final ChannelHandlerContext ctx,
|
||||
final @NotNull IdleStateEvent idleStateEvent) throws Exception {
|
||||
assert idleStateEvent.state() == IdleState.READER_IDLE;
|
||||
readTimedOut(ctx);
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
+92
-84
@@ -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,127 +32,133 @@ 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.FallbackPacketRegistry;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.login.LoginSuccessPacket;
|
||||
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;
|
||||
|
||||
@Getter
|
||||
@ToString(of = {"protocolVersion", "inetAddress"})
|
||||
@ToString(of = {"protocolVersion", "inetAddress", "geyser"})
|
||||
public final class FallbackUserWrapper implements FallbackUser {
|
||||
private final Channel channel;
|
||||
private final ChannelPipeline pipeline;
|
||||
private final InetAddress inetAddress;
|
||||
private final ProtocolVersion protocolVersion;
|
||||
@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,
|
||||
final @NotNull ProtocolVersion protocolVersion) {
|
||||
final @NotNull ProtocolVersion protocolVersion,
|
||||
final boolean geyser) {
|
||||
this.channel = channel;
|
||||
this.pipeline = channel.pipeline();
|
||||
this.inetAddress = inetAddress;
|
||||
this.protocolVersion = protocolVersion;
|
||||
this.geyser = geyser;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void disconnect(final @NotNull Component reason) {
|
||||
closeWith(channel, protocolVersion, DisconnectPacket.create(reason, false));
|
||||
final FallbackPacketEncoder encoder = pipeline.get(FallbackPacketEncoder.class);
|
||||
final boolean duringLogin = encoder != null && encoder.getPacketRegistry() != GAME;
|
||||
closeWith(channel, protocolVersion, DisconnectPacket.create(reason, duringLogin));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hijack(final @NotNull String username, final @NotNull UUID uuid,
|
||||
final @NotNull String encoder, final @NotNull String decoder,
|
||||
final @NotNull String timeout, final @NotNull String boss) {
|
||||
// This rarely happens when the channel hangs, but the player is still connecting
|
||||
// This also fixes a unique issue with TCPShield and other reverse proxies
|
||||
if (pipeline.get(encoder) == null || pipeline.get(decoder) == null) {
|
||||
channel.close();
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove main pipeline to completely take over the channel
|
||||
if (pipeline.get(boss) != null) {
|
||||
pipeline.remove(boss);
|
||||
}
|
||||
|
||||
final @NotNull String timeout, final @NotNull String handler) {
|
||||
// The player has joined the verification
|
||||
GlobalSonarStatistics.totalAttemptedVerifications++;
|
||||
|
||||
if (Sonar.get().getConfig().getVerification().isLogConnections()) {
|
||||
// Only log the processing message if the server isn't under attack.
|
||||
// We let the user override this through the configuration.
|
||||
if (!Sonar.get().getAttackTracker().isCurrentlyUnderAttack()
|
||||
if (Sonar.get().getAttackTracker().getCurrentAttack() == null
|
||||
|| Sonar.get().getConfig().getVerification().isLogDuringAttack()) {
|
||||
Sonar.get().getFallback().getLogger().info(Sonar.get().getConfig().getVerification().getConnectLog()
|
||||
.replace("%name%", username)
|
||||
.replace("%ip%", Sonar.get().getConfig().formatAddress(inetAddress))
|
||||
.replace("%protocol%", String.valueOf(protocolVersion.getProtocol())));
|
||||
Sonar.get().getFallback().getLogger().info(
|
||||
Sonar.get().getConfig().getMessagesConfig().getString("verification.logs.connection")
|
||||
.replace("<username>", username)
|
||||
.replace("<ip>", Sonar.get().getConfig().formatAddress(inetAddress))
|
||||
.replace("<protocol>", String.valueOf(protocolVersion.getProtocol())));
|
||||
}
|
||||
}
|
||||
|
||||
// Call the VerifyJoinEvent for external API usage
|
||||
Sonar.get().getEventManager().publish(new UserVerifyJoinEvent(username, this));
|
||||
|
||||
// Mark the player as connected → verifying players
|
||||
Sonar.get().getFallback().getConnected().put(username, inetAddress);
|
||||
// Mark the player as connected by caching them in a map of verifying players
|
||||
Sonar.get().getFallback().getConnected().compute(inetAddress, (key, value) -> (byte) 0);
|
||||
|
||||
// Add better timeout handler to avoid known exploits or issues
|
||||
// We also want to timeout bots quickly to avoid flooding
|
||||
final int readTimeout = Sonar.get().getConfig().getVerification().getReadTimeout();
|
||||
pipeline.replace(timeout, timeout, new FallbackTimeoutHandler(readTimeout, TimeUnit.MILLISECONDS));
|
||||
channel.eventLoop().execute(() -> {
|
||||
// Add better timeout handler to avoid known exploits or issues
|
||||
// We also want to timeout bots quickly to avoid flooding
|
||||
pipeline.replace(timeout, timeout, new FallbackTimeoutHandler(
|
||||
Sonar.get().getConfig().getVerification().getReadTimeout(),
|
||||
Sonar.get().getConfig().getVerification().getWriteTimeout(),
|
||||
TimeUnit.MILLISECONDS));
|
||||
|
||||
// Replace normal encoder to allow custom packets
|
||||
final FallbackPacketEncoder newEncoder = new FallbackPacketEncoder(protocolVersion);
|
||||
pipeline.replace(encoder, FALLBACK_PACKET_ENCODER, newEncoder);
|
||||
// Replace normal encoder to allow custom packets
|
||||
final FallbackPacketEncoder newEncoder = new FallbackPacketEncoder(protocolVersion);
|
||||
pipeline.replace(encoder, FALLBACK_PACKET_ENCODER, newEncoder);
|
||||
|
||||
// Send LoginSuccess packet to make the client think they are joining the server
|
||||
write(new LoginSuccessPacket(username, uuid));
|
||||
// Remove the main pipeline to completely take over the channel
|
||||
if (pipeline.get(handler) != null) {
|
||||
pipeline.remove(handler);
|
||||
}
|
||||
|
||||
// The LoginSuccess packet has been sent, now we can change the registry state
|
||||
newEncoder.updateRegistry(protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_20_2) >= 0
|
||||
? FallbackPacketRegistry.CONFIG : FallbackPacketRegistry.GAME);
|
||||
// Send LoginSuccess packet to make the client think they are joining the server
|
||||
write(FallbackPreparer.LOGIN_SUCCESS);
|
||||
|
||||
// Replace normal decoder to allow custom packets
|
||||
pipeline.replace(decoder, FALLBACK_PACKET_DECODER,
|
||||
new FallbackPacketDecoder(this, new FallbackVerificationHandler(this, username, uuid)));
|
||||
// The LoginSuccess packet has been sent, now we can change the registry state
|
||||
newEncoder.updateRegistry(protocolVersion.compareTo(MINECRAFT_1_20_2) >= 0 ? CONFIG : GAME);
|
||||
|
||||
// Replace normal decoder to allow custom packets
|
||||
final FallbackPacketDecoder fallbackPacketDecoder = new FallbackPacketDecoder(protocolVersion);
|
||||
pipeline.replace(decoder, FALLBACK_PACKET_DECODER, fallbackPacketDecoder);
|
||||
// Listen for all incoming packets by setting the packet listener
|
||||
fallbackPacketDecoder.setListener(new FallbackLoginSessionHandler(this, username, uuid));
|
||||
});
|
||||
}
|
||||
|
||||
@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());
|
||||
|
||||
// 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().isCurrentlyUnderAttack()
|
||||
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().getMessagesConfig().getString("verification.logs.failed")
|
||||
.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
|
||||
@@ -165,33 +172,35 @@ 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);
|
||||
|
||||
if (shouldLog) {
|
||||
Sonar.get().getFallback().getLogger().info(Sonar.get().getConfig().getVerification().getBlacklistLog()
|
||||
.replace("%ip%", Sonar.get().getConfig().formatAddress(getInetAddress()))
|
||||
.replace("%protocol%", String.valueOf(getProtocolVersion().getProtocol())));
|
||||
Sonar.get().getFallback().getLogger().info(
|
||||
Sonar.get().getConfig().getMessagesConfig().getString("verification.logs.blacklisted")
|
||||
.replace("<ip>", Sonar.get().getConfig().formatAddress(getInetAddress()))
|
||||
.replace("<protocol>", String.valueOf(getProtocolVersion().getProtocol())));
|
||||
}
|
||||
|
||||
// Invalidate the cached entry to ensure memory safety
|
||||
Sonar.get().getFallback().getRatelimiter().getFailCountCache().invalidate(inetAddress);
|
||||
}
|
||||
|
||||
// Throw an exception to avoid further code execution
|
||||
throw new CorruptedFrameException();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -223,7 +232,7 @@ public final class FallbackUserWrapper implements FallbackUser {
|
||||
*
|
||||
* @param packet Disconnect packet
|
||||
* @param encoder Encoder to replace
|
||||
* @param boss Main pipeline to remove
|
||||
* @param handler Main pipeline to remove
|
||||
* @param channel Channel of the player
|
||||
* @param protocolVersion Protocol version of the player
|
||||
*/
|
||||
@@ -231,13 +240,25 @@ public final class FallbackUserWrapper implements FallbackUser {
|
||||
final @NotNull ProtocolVersion protocolVersion,
|
||||
final @NotNull FallbackPacket packet,
|
||||
final @NotNull String encoder,
|
||||
final @NotNull String boss) {
|
||||
// Remove main pipeline to completely take over the channel
|
||||
if (channel.pipeline().get(boss) != null) {
|
||||
final @NotNull String handler) {
|
||||
if (channel.eventLoop().inEventLoop()) {
|
||||
_customDisconnect(channel, protocolVersion, packet, encoder, handler);
|
||||
} else {
|
||||
channel.eventLoop().execute(() -> _customDisconnect(channel, protocolVersion, packet, encoder, handler));
|
||||
}
|
||||
}
|
||||
|
||||
private static void _customDisconnect(final @NotNull Channel channel,
|
||||
final @NotNull ProtocolVersion protocolVersion,
|
||||
final @NotNull FallbackPacket packet,
|
||||
final @NotNull String encoder,
|
||||
final @NotNull String boss) {
|
||||
// Remove the main pipeline to completely take over the channel
|
||||
if (channel.pipeline().context(boss) != null) {
|
||||
channel.pipeline().remove(boss);
|
||||
}
|
||||
final ChannelHandler currentEncoder = channel.pipeline().get(encoder);
|
||||
// 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)) {
|
||||
@@ -249,17 +270,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
-709
@@ -1,709 +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 net.kyori.adventure.text.minimessage.MiniMessage;
|
||||
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.map.ItemType;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.map.MapCaptchaInfo;
|
||||
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.fallback.protocol.vehicle.EntityType;
|
||||
import xyz.jonesdev.sonar.common.statistics.GlobalSonarStatistics;
|
||||
import xyz.jonesdev.sonar.common.utility.protocol.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;
|
||||
|
||||
// CAPTCHA
|
||||
private final SystemTimer keepAlive = new SystemTimer();
|
||||
private final SystemTimer actionBar = new SystemTimer();
|
||||
private @Nullable MapCaptchaInfo captcha;
|
||||
private int captchaTriesLeft;
|
||||
|
||||
// Cached options to make sure the original values don't change throughout the verification
|
||||
private final boolean performVehicle, performCaptcha, performGravity, performCollisions;
|
||||
|
||||
public FallbackVerificationHandler(final @NotNull FallbackUser user,
|
||||
final @NotNull String username,
|
||||
final @NotNull UUID playerUuid) {
|
||||
this.user = user;
|
||||
this.username = username;
|
||||
this.playerUuid = playerUuid;
|
||||
this.performGravity = Sonar.get().getConfig().getVerification().getGravity().isEnabled();
|
||||
this.performCollisions = Sonar.get().getConfig().getVerification().getGravity().isCheckCollisions();
|
||||
this.performCaptcha = FALLBACK.shouldPerformCaptcha();
|
||||
this.performVehicle = FALLBACK.shouldPerformVehicleCheck();
|
||||
|
||||
if (user.getProtocolVersion().compareTo(MINECRAFT_1_20_2) < 0) {
|
||||
// Start initializing the actual join process
|
||||
initialJoinProcess();
|
||||
}
|
||||
}
|
||||
|
||||
private void configure() {
|
||||
// Set the state to CONFIGURE to avoid false positives
|
||||
user.setState(CONFIGURE);
|
||||
// Send the necessary configuration packets to the client
|
||||
user.delayedWrite(REGISTRY_SYNC);
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
private 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) {
|
||||
checkFrame(captcha != null, "Captcha not sent yet");
|
||||
final UniversalChatPacket chat = (UniversalChatPacket) packet;
|
||||
if (!chat.getMessage().equals(captcha.getAnswer())) {
|
||||
// Captcha is incorrect
|
||||
checkFrame(captchaTriesLeft-- > 0, "failed captcha too often");
|
||||
user.write(incorrectCaptcha);
|
||||
return;
|
||||
}
|
||||
// Captcha is correct
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
// Every second
|
||||
if (actionBar.elapsed(user.getProtocolVersion().compareTo(MINECRAFT_1_8) < 0 ? 10_000L : 1_000L)) {
|
||||
final String actionBarMessage = Sonar.get().getConfig().getVerification().getMap().getEnterCodeActionBar();
|
||||
// Only send action bar if the message is actually supposed to be sent
|
||||
if (!actionBarMessage.isEmpty()) {
|
||||
final String timeLeft = String.format("%.0f", (maxDuration - login.delay()) / 1000D);
|
||||
user.write(new UniversalChatPacket(MiniMessage.miniMessage().deserialize(
|
||||
actionBarMessage.replace("%time-left%", timeLeft)), UniversalChatPacket.GAME_INFO_TYPE));
|
||||
// Make sure to reset the timer
|
||||
actionBar.reset();
|
||||
}
|
||||
}
|
||||
|
||||
// Every 10 seconds
|
||||
if (keepAlive.elapsed(10_000L)) {
|
||||
// Send a KeepAlive packet to prevent timeout
|
||||
user.write(CAPTCHA_KEEP_ALIVE);
|
||||
// Make sure to reset the timer
|
||||
keepAlive.reset();
|
||||
}
|
||||
}
|
||||
|
||||
@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 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
|
||||
// We can't fail since the player is able to jump or sneak
|
||||
if (inputPacket.isJump() || inputPacket.isUnmount()) return;
|
||||
|
||||
// Continue checking or verify the player
|
||||
captchaOrNext();
|
||||
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
|
||||
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.getId() == 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 = -1;
|
||||
expectedTeleportId = -1;
|
||||
|
||||
// Now we can send the chunk data
|
||||
sendChunkData();
|
||||
return;
|
||||
}
|
||||
|
||||
if (user.getState() != LOGIN_ACK) {
|
||||
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 PlayerTickPacket) {
|
||||
final PlayerTickPacket player = (PlayerTickPacket) 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 = -1;
|
||||
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) {
|
||||
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 < 0.005, 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(0, 36, 1, 0,
|
||||
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(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()));
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+36
-41
@@ -21,29 +21,29 @@ 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;
|
||||
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry.GAME;
|
||||
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry.LOGIN;
|
||||
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.readVarInt;
|
||||
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
|
||||
@@ -52,45 +52,42 @@ public final class FallbackPacketDecoder extends ChannelInboundHandlerAdapter {
|
||||
if (msg instanceof ByteBuf) {
|
||||
final ByteBuf byteBuf = (ByteBuf) msg;
|
||||
|
||||
// Release the ByteBuf if the connection is not active
|
||||
// or the ByteBuf doesn't contain any data to avoid
|
||||
// memory leaks or other potential exploits.
|
||||
if (!ctx.channel().isActive() || !byteBuf.isReadable()) {
|
||||
byteBuf.release();
|
||||
return;
|
||||
}
|
||||
|
||||
final int originalReaderIndex = byteBuf.readerIndex();
|
||||
// Read the packet ID and then create the packet from it
|
||||
final int packetId = readVarInt(byteBuf);
|
||||
final FallbackPacket packet = registry.createPacket(packetId);
|
||||
|
||||
// If the packet isn't found, skip it
|
||||
if (packet == null) {
|
||||
byteBuf.readerIndex(originalReaderIndex);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Release the ByteBuf if the connection is not active
|
||||
// or the ByteBuf doesn't contain any data to avoid
|
||||
// memory leaks or other potential exploits.
|
||||
if (!ctx.channel().isActive() || !byteBuf.isReadable()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Read the packet ID and then create the packet from it
|
||||
final int packetId = readVarInt(byteBuf);
|
||||
final FallbackPacket packet = registry.createPacket(packetId);
|
||||
|
||||
// Skip the packet if it's not registered within Sonar's packet registry
|
||||
if (packet == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 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 +98,15 @@ public final class FallbackPacketDecoder extends ChannelInboundHandlerAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
private void doLengthSanityChecks(final 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");
|
||||
}
|
||||
}
|
||||
|
||||
+13
-7
@@ -20,32 +20,38 @@ package xyz.jonesdev.sonar.common.fallback.protocol;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.handler.codec.MessageToByteEncoder;
|
||||
import lombok.Getter;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
|
||||
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry.Direction.CLIENTBOUND;
|
||||
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry.LOGIN;
|
||||
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.writeVarInt;
|
||||
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.writeVarInt;
|
||||
|
||||
public final class FallbackPacketEncoder extends MessageToByteEncoder<FallbackPacket> {
|
||||
private final ProtocolVersion protocolVersion;
|
||||
private FallbackPacketRegistry.ProtocolRegistry registry;
|
||||
@Getter
|
||||
private FallbackPacketRegistry packetRegistry;
|
||||
private FallbackPacketRegistry.ProtocolRegistry protocolRegistry;
|
||||
|
||||
public FallbackPacketEncoder(final ProtocolVersion protocolVersion) {
|
||||
this.protocolVersion = protocolVersion;
|
||||
updateRegistry(LOGIN);
|
||||
}
|
||||
|
||||
public void updateRegistry(final @NotNull FallbackPacketRegistry registry) {
|
||||
this.registry = registry.getProtocolRegistry(CLIENTBOUND, protocolVersion);
|
||||
public void updateRegistry(final @NotNull FallbackPacketRegistry packetRegistry) {
|
||||
this.packetRegistry = packetRegistry;
|
||||
this.protocolRegistry = packetRegistry.getProtocolRegistry(CLIENTBOUND, protocolVersion);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void encode(final ChannelHandlerContext ctx,
|
||||
final FallbackPacket msg,
|
||||
final @NotNull FallbackPacket packet,
|
||||
final ByteBuf out) throws Exception {
|
||||
final int packetId = registry.getPacketId(msg);
|
||||
final FallbackPacket originalPacket = packet instanceof FallbackPacketSnapshot
|
||||
? ((FallbackPacketSnapshot) packet).getOriginalPacket() : packet;
|
||||
final int packetId = protocolRegistry.getPacketId(originalPacket);
|
||||
writeVarInt(out, packetId);
|
||||
msg.encode(out, protocolVersion);
|
||||
packet.encode(out, protocolVersion);
|
||||
}
|
||||
}
|
||||
+171
-67
@@ -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.*;
|
||||
@@ -33,40 +35,46 @@ import java.util.function.Supplier;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
|
||||
|
||||
// Most of this is taken from
|
||||
// Mostly taken from
|
||||
// https://github.com/PaperMC/Velocity/blob/dev/3.0.0/proxy/src/main/java/com/velocitypowered/proxy/protocol/StateRegistry.java
|
||||
public enum FallbackPacketRegistry {
|
||||
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 {
|
||||
{
|
||||
clientbound.register(DisconnectPacket.class, DisconnectPacket::new,
|
||||
map(0x01, MINECRAFT_1_20_2, true));
|
||||
map(0x01, MINECRAFT_1_20_2, true),
|
||||
map(0x02, MINECRAFT_1_20_5, true));
|
||||
clientbound.register(FinishConfigurationPacket.class, FinishConfigurationPacket::new,
|
||||
map(0x02, MINECRAFT_1_20_2, false));
|
||||
map(0x02, MINECRAFT_1_20_2, false),
|
||||
map(0x03, MINECRAFT_1_20_5, false));
|
||||
clientbound.register(KeepAlivePacket.class, KeepAlivePacket::new,
|
||||
map(0x03, MINECRAFT_1_20_2, false));
|
||||
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(0x05, MINECRAFT_1_20_2, true),
|
||||
map(0x07, MINECRAFT_1_20_5, true));
|
||||
|
||||
serverbound.register(ClientSettingsPacket.class, ClientSettingsPacket::new,
|
||||
serverbound.register(ClientInformationPacket.class, ClientInformationPacket::new,
|
||||
map(0x00, MINECRAFT_1_20_2, false));
|
||||
serverbound.register(PluginMessagePacket.class, PluginMessagePacket::new,
|
||||
map(0x01, MINECRAFT_1_20_2, false));
|
||||
map(0x01, MINECRAFT_1_20_2, false),
|
||||
map(0x02, MINECRAFT_1_20_5, false));
|
||||
serverbound.register(FinishConfigurationPacket.class, FinishConfigurationPacket::new,
|
||||
map(0x02, MINECRAFT_1_20_2, false));
|
||||
map(0x02, MINECRAFT_1_20_2, false),
|
||||
map(0x03, MINECRAFT_1_20_5, false));
|
||||
serverbound.register(KeepAlivePacket.class, KeepAlivePacket::new,
|
||||
map(0x03, MINECRAFT_1_20_2, false));
|
||||
map(0x03, MINECRAFT_1_20_2, false),
|
||||
map(0x04, MINECRAFT_1_20_5, false));
|
||||
}
|
||||
},
|
||||
GAME {
|
||||
@@ -84,7 +92,8 @@ public enum FallbackPacketRegistry {
|
||||
map(0x25, MINECRAFT_1_19_1, true),
|
||||
map(0x24, MINECRAFT_1_19_3, true),
|
||||
map(0x28, MINECRAFT_1_19_4, true),
|
||||
map(0x29, MINECRAFT_1_20_2, true));
|
||||
map(0x29, MINECRAFT_1_20_2, true),
|
||||
map(0x2B, MINECRAFT_1_20_5, true));
|
||||
clientbound.register(KeepAlivePacket.class, KeepAlivePacket::new,
|
||||
map(0x00, MINECRAFT_1_7_2, false),
|
||||
map(0x1F, MINECRAFT_1_9, false),
|
||||
@@ -98,7 +107,8 @@ public enum FallbackPacketRegistry {
|
||||
map(0x20, MINECRAFT_1_19_1, false),
|
||||
map(0x1F, MINECRAFT_1_19_3, false),
|
||||
map(0x23, MINECRAFT_1_19_4, false),
|
||||
map(0x24, MINECRAFT_1_20_2, false));
|
||||
map(0x24, MINECRAFT_1_20_2, false),
|
||||
map(0x26, MINECRAFT_1_20_5, false));
|
||||
clientbound.register(DisconnectPacket.class, DisconnectPacket::new,
|
||||
map(0x40, MINECRAFT_1_7_2, true),
|
||||
map(0x1A, MINECRAFT_1_9, true),
|
||||
@@ -112,8 +122,9 @@ public enum FallbackPacketRegistry {
|
||||
map(0x19, MINECRAFT_1_19_1, true),
|
||||
map(0x17, MINECRAFT_1_19_3, true),
|
||||
map(0x1A, MINECRAFT_1_19_4, true),
|
||||
map(0x1B, MINECRAFT_1_20_2, true));
|
||||
clientbound.register(PlayerPositionLookPacket.class, PlayerPositionLookPacket::new,
|
||||
map(0x1B, MINECRAFT_1_20_2, true),
|
||||
map(0x1D, MINECRAFT_1_20_5, true));
|
||||
clientbound.register(SetPlayerPositionRotation.class, SetPlayerPositionRotation::new,
|
||||
map(0x08, MINECRAFT_1_7_2, true),
|
||||
map(0x2E, MINECRAFT_1_9, true),
|
||||
map(0x2F, MINECRAFT_1_12_1, true),
|
||||
@@ -127,8 +138,9 @@ public enum FallbackPacketRegistry {
|
||||
map(0x39, MINECRAFT_1_19_1, true),
|
||||
map(0x38, MINECRAFT_1_19_3, true),
|
||||
map(0x3C, MINECRAFT_1_19_4, true),
|
||||
map(0x3E, MINECRAFT_1_20_2, true));
|
||||
clientbound.register(ClientAbilitiesPacket.class, ClientAbilitiesPacket::new,
|
||||
map(0x3E, MINECRAFT_1_20_2, true),
|
||||
map(0x40, MINECRAFT_1_20_5, true));
|
||||
clientbound.register(PlayerAbilitiesPacket.class, PlayerAbilitiesPacket::new,
|
||||
map(0x39, MINECRAFT_1_7_2, true),
|
||||
map(0x2B, MINECRAFT_1_9, true),
|
||||
map(0x2C, MINECRAFT_1_12_1, true),
|
||||
@@ -142,7 +154,8 @@ public enum FallbackPacketRegistry {
|
||||
map(0x31, MINECRAFT_1_19_1, true),
|
||||
map(0x30, MINECRAFT_1_19_3, true),
|
||||
map(0x34, MINECRAFT_1_19_4, true),
|
||||
map(0x36, MINECRAFT_1_20_2, true));
|
||||
map(0x36, MINECRAFT_1_20_2, true),
|
||||
map(0x38, MINECRAFT_1_20_5, true));
|
||||
clientbound.register(ChunkDataPacket.class, ChunkDataPacket::new,
|
||||
map(0x21, MINECRAFT_1_7_2, true),
|
||||
map(0x20, MINECRAFT_1_9, true),
|
||||
@@ -156,7 +169,8 @@ public enum FallbackPacketRegistry {
|
||||
map(0x21, MINECRAFT_1_19_1, true),
|
||||
map(0x20, MINECRAFT_1_19_3, true),
|
||||
map(0x24, MINECRAFT_1_19_4, true),
|
||||
map(0x25, MINECRAFT_1_20_2, true));
|
||||
map(0x25, MINECRAFT_1_20_2, true),
|
||||
map(0x27, MINECRAFT_1_20_5, true));
|
||||
clientbound.register(RemoveEntitiesPacket.class, RemoveEntitiesPacket::new,
|
||||
map(0x13, MINECRAFT_1_7_2, true),
|
||||
map(0x30, MINECRAFT_1_9, true),
|
||||
@@ -173,7 +187,8 @@ public enum FallbackPacketRegistry {
|
||||
map(0x3A, MINECRAFT_1_19_3, true),
|
||||
map(0x3E, MINECRAFT_1_19_4, true),
|
||||
map(0x3E, MINECRAFT_1_19_4, true),
|
||||
map(0x40, MINECRAFT_1_20_2, true));
|
||||
map(0x40, MINECRAFT_1_20_2, true),
|
||||
map(0x42, MINECRAFT_1_20_5, true));
|
||||
clientbound.register(SetPassengersPacket.class, SetPassengersPacket::new,
|
||||
map(0x1B, MINECRAFT_1_7_2, true),
|
||||
map(0x40, MINECRAFT_1_9, true),
|
||||
@@ -187,11 +202,28 @@ public enum FallbackPacketRegistry {
|
||||
map(0x55, MINECRAFT_1_19_3, true),
|
||||
map(0x59, MINECRAFT_1_19_4, true),
|
||||
map(0x5B, MINECRAFT_1_20_2, true),
|
||||
map(0x5D, MINECRAFT_1_20_3, true));
|
||||
map(0x5D, MINECRAFT_1_20_3, true),
|
||||
map(0x5F, MINECRAFT_1_20_5, true));
|
||||
clientbound.register(SpawnEntityPacket.class, SpawnEntityPacket::new,
|
||||
map(0x0E, MINECRAFT_1_7_2, true),
|
||||
map(0x00, MINECRAFT_1_9, true),
|
||||
map(0x01, MINECRAFT_1_19_4, true));
|
||||
clientbound.register(PlayerInfoPacket.class, PlayerInfoPacket::new,
|
||||
map(0x38, MINECRAFT_1_7_2, true),
|
||||
map(0x2D, MINECRAFT_1_9, true),
|
||||
map(0x2E, MINECRAFT_1_12_1, true),
|
||||
map(0x30, MINECRAFT_1_13, true),
|
||||
map(0x33, MINECRAFT_1_14, true),
|
||||
map(0x34, MINECRAFT_1_15, true),
|
||||
map(0x33, MINECRAFT_1_16, true),
|
||||
map(0x32, MINECRAFT_1_16_2, true),
|
||||
map(0x36, MINECRAFT_1_17, true),
|
||||
map(0x34, MINECRAFT_1_19, true),
|
||||
map(0x37, MINECRAFT_1_19_1, true),
|
||||
map(0x36, MINECRAFT_1_19_3, true),
|
||||
map(0x3A, MINECRAFT_1_19_4, true),
|
||||
map(0x3C, MINECRAFT_1_20_2, true),
|
||||
map(0x3E, MINECRAFT_1_20_5, true));
|
||||
clientbound.register(UpdateSectionBlocksPacket.class, UpdateSectionBlocksPacket::new,
|
||||
map(0x22, MINECRAFT_1_7_2, true),
|
||||
map(0x10, MINECRAFT_1_9, true),
|
||||
@@ -205,7 +237,8 @@ public enum FallbackPacketRegistry {
|
||||
map(0x3F, MINECRAFT_1_19_3, true),
|
||||
map(0x43, MINECRAFT_1_19_4, true),
|
||||
map(0x45, MINECRAFT_1_20_2, true),
|
||||
map(0x47, MINECRAFT_1_20_3, true));
|
||||
map(0x47, MINECRAFT_1_20_3, true),
|
||||
map(0x49, MINECRAFT_1_20_5, true));
|
||||
clientbound.register(TransactionPacket.class, TransactionPacket::new,
|
||||
map(0x32, MINECRAFT_1_7_2, true),
|
||||
map(0x11, MINECRAFT_1_9, true),
|
||||
@@ -218,8 +251,9 @@ public enum FallbackPacketRegistry {
|
||||
map(0x2F, MINECRAFT_1_19_1, true),
|
||||
map(0x2E, MINECRAFT_1_19_3, true),
|
||||
map(0x32, MINECRAFT_1_19_4, true),
|
||||
map(0x33, MINECRAFT_1_20_2, true));
|
||||
clientbound.register(DefaultSpawnPositionPacket.class, DefaultSpawnPositionPacket::new,
|
||||
map(0x33, MINECRAFT_1_20_2, true),
|
||||
map(0x35, MINECRAFT_1_20_5, true));
|
||||
clientbound.register(SetDefaultSpawnPositionPacket.class, SetDefaultSpawnPositionPacket::new,
|
||||
map(0x05, MINECRAFT_1_7_2, true),
|
||||
map(0x43, MINECRAFT_1_9, true),
|
||||
map(0x45, MINECRAFT_1_12, true),
|
||||
@@ -234,7 +268,8 @@ public enum FallbackPacketRegistry {
|
||||
map(0x4C, MINECRAFT_1_19_3, true),
|
||||
map(0x50, MINECRAFT_1_19_4, true),
|
||||
map(0x52, MINECRAFT_1_20_2, true),
|
||||
map(0x54, MINECRAFT_1_20_3, true));
|
||||
map(0x54, MINECRAFT_1_20_3, true),
|
||||
map(0x56, MINECRAFT_1_20_5, true));
|
||||
clientbound.register(MapDataPacket.class, MapDataPacket::new,
|
||||
map(0x34, MINECRAFT_1_7_2, true),
|
||||
map(0x24, MINECRAFT_1_9, true),
|
||||
@@ -247,8 +282,9 @@ public enum FallbackPacketRegistry {
|
||||
map(0x26, MINECRAFT_1_19_1, true),
|
||||
map(0x25, MINECRAFT_1_19_3, true),
|
||||
map(0x29, MINECRAFT_1_19_4, true),
|
||||
map(0x2A, MINECRAFT_1_20_2, true));
|
||||
clientbound.register(SetSlotPacket.class, SetSlotPacket::new,
|
||||
map(0x2A, MINECRAFT_1_20_2, true),
|
||||
map(0x2C, MINECRAFT_1_20_5, true));
|
||||
clientbound.register(SetContainerSlotPacket.class, SetContainerSlotPacket::new,
|
||||
map(0x2F, MINECRAFT_1_7_2, true),
|
||||
map(0x16, MINECRAFT_1_9, true),
|
||||
map(0x17, MINECRAFT_1_13, true),
|
||||
@@ -261,7 +297,22 @@ public enum FallbackPacketRegistry {
|
||||
map(0x12, MINECRAFT_1_19_3, true),
|
||||
map(0x14, MINECRAFT_1_19_4, true),
|
||||
map(0x15, MINECRAFT_1_20_2, true));
|
||||
clientbound.register(UniversalChatPacket.class, UniversalChatPacket::new,
|
||||
clientbound.register(SetExperiencePacket.class, SetExperiencePacket::new,
|
||||
map(0x1F, MINECRAFT_1_7_2, true),
|
||||
map(0x3D, MINECRAFT_1_9, true),
|
||||
map(0x3F, MINECRAFT_1_12, true),
|
||||
map(0x40, MINECRAFT_1_12_1, true),
|
||||
map(0x43, MINECRAFT_1_13, true),
|
||||
map(0x47, MINECRAFT_1_14, true),
|
||||
map(0x48, MINECRAFT_1_15, true),
|
||||
map(0x51, MINECRAFT_1_17, true),
|
||||
map(0x54, MINECRAFT_1_19_1, true),
|
||||
map(0x52, MINECRAFT_1_19_3, true),
|
||||
map(0x56, MINECRAFT_1_19_4, true),
|
||||
map(0x58, MINECRAFT_1_20_2, true),
|
||||
map(0x5A, MINECRAFT_1_20_3, true),
|
||||
map(0x5C, MINECRAFT_1_20_5, true));
|
||||
clientbound.register(SystemChatPacket.class, SystemChatPacket::new,
|
||||
map(0x02, MINECRAFT_1_7_2, false),
|
||||
map(0x0F, MINECRAFT_1_9, false),
|
||||
map(0x0E, MINECRAFT_1_13, false),
|
||||
@@ -273,10 +324,59 @@ public enum FallbackPacketRegistry {
|
||||
map(0x60, MINECRAFT_1_19_3, false),
|
||||
map(0x64, MINECRAFT_1_19_4, false),
|
||||
map(0x67, MINECRAFT_1_20_2, false),
|
||||
map(0x69, MINECRAFT_1_20_3, false));
|
||||
map(0x69, MINECRAFT_1_20_3, false),
|
||||
map(0x6C, MINECRAFT_1_20_5, false));
|
||||
clientbound.register(GameEventPacket.class, GameEventPacket::new,
|
||||
map(0x20, MINECRAFT_1_20_3, true));
|
||||
map(0x20, MINECRAFT_1_20_3, true),
|
||||
map(0x22, MINECRAFT_1_20_5, true));
|
||||
clientbound.register(TransferPacket.class, TransferPacket::new,
|
||||
map(0x73, MINECRAFT_1_20_5, true));
|
||||
clientbound.register(SetHeldItemPacket.class, SetHeldItemPacket::new,
|
||||
map(0x09, MINECRAFT_1_7_2, true),
|
||||
map(0x37, MINECRAFT_1_9, true),
|
||||
map(0x39, MINECRAFT_1_12, true),
|
||||
map(0x3A, MINECRAFT_1_12_1, true),
|
||||
map(0x3D, MINECRAFT_1_13, true),
|
||||
map(0x3F, MINECRAFT_1_14, true),
|
||||
map(0x40, MINECRAFT_1_15, true),
|
||||
map(0x3F, MINECRAFT_1_16, true),
|
||||
map(0x48, MINECRAFT_1_17, true),
|
||||
map(0x47, MINECRAFT_1_19, true),
|
||||
map(0x4A, MINECRAFT_1_19_1, true),
|
||||
map(0x49, MINECRAFT_1_19_3, true),
|
||||
map(0x4D, MINECRAFT_1_19_4, true),
|
||||
map(0x4F, MINECRAFT_1_20_2, true),
|
||||
map(0x51, MINECRAFT_1_20_3, true),
|
||||
map(0x53, MINECRAFT_1_20_5, true));
|
||||
|
||||
serverbound.register(SetHeldItemPacket.class, SetHeldItemPacket::new,
|
||||
map(0x09, MINECRAFT_1_7_2, false),
|
||||
map(0x17, MINECRAFT_1_9, false),
|
||||
map(0x1A, MINECRAFT_1_12, false),
|
||||
map(0x21, MINECRAFT_1_13, false),
|
||||
map(0x23, MINECRAFT_1_14, false),
|
||||
map(0x24, MINECRAFT_1_16, false),
|
||||
map(0x25, MINECRAFT_1_16_2, false),
|
||||
map(0x27, MINECRAFT_1_19, false),
|
||||
map(0x28, MINECRAFT_1_19_1, false),
|
||||
map(0x2B, MINECRAFT_1_20_2, false),
|
||||
map(0x2C, MINECRAFT_1_20_3, false),
|
||||
map(0x2F, MINECRAFT_1_20_5, false));
|
||||
serverbound.register(PaddleBoatPacket.class, PaddleBoatPacket::new,
|
||||
map(0x11, MINECRAFT_1_9, false),
|
||||
map(0x12, MINECRAFT_1_12, false),
|
||||
map(0x11, MINECRAFT_1_12_1, false),
|
||||
map(0x14, MINECRAFT_1_13, false),
|
||||
map(0x16, MINECRAFT_1_14, false),
|
||||
map(0x17, MINECRAFT_1_16, false),
|
||||
map(0x16, MINECRAFT_1_17, false),
|
||||
map(0x18, MINECRAFT_1_19, false),
|
||||
map(0x19, MINECRAFT_1_19_1, false),
|
||||
map(0x18, MINECRAFT_1_19_3, false),
|
||||
map(0x19, MINECRAFT_1_19_4, false),
|
||||
map(0x1B, MINECRAFT_1_20_2, false),
|
||||
map(0x1C, MINECRAFT_1_20_3, false),
|
||||
map(0x1F, MINECRAFT_1_20_5, false));
|
||||
serverbound.register(PlayerInputPacket.class, PlayerInputPacket::new,
|
||||
map(0x0C, MINECRAFT_1_7_2, false),
|
||||
map(0x15, MINECRAFT_1_9, false),
|
||||
@@ -290,8 +390,9 @@ public enum FallbackPacketRegistry {
|
||||
map(0x1E, MINECRAFT_1_19_3, false),
|
||||
map(0x1F, MINECRAFT_1_19_4, false),
|
||||
map(0x22, MINECRAFT_1_20_2, false),
|
||||
map(0x23, MINECRAFT_1_20_3, false));
|
||||
serverbound.register(UniversalChatPacket.class, UniversalChatPacket::new,
|
||||
map(0x23, MINECRAFT_1_20_3, false),
|
||||
map(0x26, MINECRAFT_1_20_5, false));
|
||||
serverbound.register(SystemChatPacket.class, SystemChatPacket::new,
|
||||
map(0x01, MINECRAFT_1_7_2, false),
|
||||
map(0x02, MINECRAFT_1_9, false),
|
||||
map(0x03, MINECRAFT_1_12, false),
|
||||
@@ -299,7 +400,8 @@ public enum FallbackPacketRegistry {
|
||||
map(0x03, MINECRAFT_1_14, false),
|
||||
map(0x04, MINECRAFT_1_19, false),
|
||||
map(0x05, MINECRAFT_1_19_1, false),
|
||||
map(0x05, MINECRAFT_1_19_3, false));
|
||||
map(0x05, MINECRAFT_1_19_3, false),
|
||||
map(0x06, MINECRAFT_1_20_5, false));
|
||||
serverbound.register(KeepAlivePacket.class, KeepAlivePacket::new,
|
||||
map(0x00, MINECRAFT_1_7_2, false),
|
||||
map(0x0B, MINECRAFT_1_9, false),
|
||||
@@ -314,8 +416,9 @@ public enum FallbackPacketRegistry {
|
||||
map(0x11, MINECRAFT_1_19_3, false),
|
||||
map(0x12, MINECRAFT_1_19_4, false),
|
||||
map(0x14, MINECRAFT_1_20_2, false),
|
||||
map(0x15, MINECRAFT_1_20_3, false));
|
||||
serverbound.register(ClientSettingsPacket.class, ClientSettingsPacket::new,
|
||||
map(0x15, MINECRAFT_1_20_3, false),
|
||||
map(0x18, MINECRAFT_1_20_5, false));
|
||||
serverbound.register(ClientInformationPacket.class, ClientInformationPacket::new,
|
||||
map(0x15, MINECRAFT_1_7_2, false),
|
||||
map(0x04, MINECRAFT_1_9, false),
|
||||
map(0x05, MINECRAFT_1_12, false),
|
||||
@@ -325,7 +428,8 @@ public enum FallbackPacketRegistry {
|
||||
map(0x08, MINECRAFT_1_19_1, false),
|
||||
map(0x07, MINECRAFT_1_19_3, false),
|
||||
map(0x08, MINECRAFT_1_19_4, false),
|
||||
map(0x09, MINECRAFT_1_20_2, false));
|
||||
map(0x09, MINECRAFT_1_20_2, false),
|
||||
map(0x0A, MINECRAFT_1_20_5, false));
|
||||
serverbound.register(PluginMessagePacket.class, PluginMessagePacket::new,
|
||||
map(0x17, MINECRAFT_1_7_2, false),
|
||||
map(0x09, MINECRAFT_1_9, false),
|
||||
@@ -338,8 +442,9 @@ public enum FallbackPacketRegistry {
|
||||
map(0x0D, MINECRAFT_1_19_1, false),
|
||||
map(0x0C, MINECRAFT_1_19_3, false),
|
||||
map(0x0D, MINECRAFT_1_19_4, false),
|
||||
map(0x0F, MINECRAFT_1_20_2, false));
|
||||
serverbound.register(PlayerPositionLookPacket.class, PlayerPositionLookPacket::new,
|
||||
map(0x0F, MINECRAFT_1_20_2, false),
|
||||
map(0x12, MINECRAFT_1_21, false));
|
||||
serverbound.register(SetPlayerPositionRotation.class, SetPlayerPositionRotation::new,
|
||||
map(0x06, MINECRAFT_1_7_2, false),
|
||||
map(0x0D, MINECRAFT_1_9, false),
|
||||
map(0x0F, MINECRAFT_1_12, false),
|
||||
@@ -353,10 +458,11 @@ public enum FallbackPacketRegistry {
|
||||
map(0x14, MINECRAFT_1_19_3, false),
|
||||
map(0x15, MINECRAFT_1_19_4, false),
|
||||
map(0x17, MINECRAFT_1_20_2, false),
|
||||
map(0x18, MINECRAFT_1_20_3, false));
|
||||
serverbound.register(TeleportConfirmPacket.class, TeleportConfirmPacket::new,
|
||||
map(0x18, MINECRAFT_1_20_3, false),
|
||||
map(0x1B, MINECRAFT_1_20_5, false));
|
||||
serverbound.register(ConfirmTeleportationPacket.class, ConfirmTeleportationPacket::new,
|
||||
map(0x00, MINECRAFT_1_9, false));
|
||||
serverbound.register(PlayerPositionPacket.class, PlayerPositionPacket::new,
|
||||
serverbound.register(SetPlayerPositionPacket.class, SetPlayerPositionPacket::new,
|
||||
map(0x04, MINECRAFT_1_7_2, false),
|
||||
map(0x0C, MINECRAFT_1_9, false),
|
||||
map(0x0E, MINECRAFT_1_12, false),
|
||||
@@ -370,8 +476,9 @@ public enum FallbackPacketRegistry {
|
||||
map(0x13, MINECRAFT_1_19_3, false),
|
||||
map(0x14, MINECRAFT_1_19_4, false),
|
||||
map(0x16, MINECRAFT_1_20_2, false),
|
||||
map(0x17, MINECRAFT_1_20_3, false));
|
||||
serverbound.register(PlayerTickPacket.class, PlayerTickPacket::new,
|
||||
map(0x17, MINECRAFT_1_20_3, false),
|
||||
map(0x1A, MINECRAFT_1_20_5, false));
|
||||
serverbound.register(SetPlayerOnGround.class, SetPlayerOnGround::new,
|
||||
map(0x03, MINECRAFT_1_7_2, false),
|
||||
map(0x0F, MINECRAFT_1_9, false),
|
||||
map(0x0D, MINECRAFT_1_12, false),
|
||||
@@ -385,7 +492,8 @@ public enum FallbackPacketRegistry {
|
||||
map(0x16, MINECRAFT_1_19_3, false),
|
||||
map(0x17, MINECRAFT_1_19_4, false),
|
||||
map(0x19, MINECRAFT_1_20_2, false),
|
||||
map(0x1A, MINECRAFT_1_20_3, false));
|
||||
map(0x1A, MINECRAFT_1_20_3, false),
|
||||
map(0x1D, MINECRAFT_1_20_5, false));
|
||||
serverbound.register(TransactionPacket.class, TransactionPacket::new,
|
||||
map(0x0F, MINECRAFT_1_7_2, false),
|
||||
map(0x05, MINECRAFT_1_9, false),
|
||||
@@ -399,7 +507,8 @@ public enum FallbackPacketRegistry {
|
||||
map(0x1F, MINECRAFT_1_19_3, false),
|
||||
map(0x20, MINECRAFT_1_19_4, false),
|
||||
map(0x23, MINECRAFT_1_20_2, false),
|
||||
map(0x24, MINECRAFT_1_20_3, false));
|
||||
map(0x24, MINECRAFT_1_20_3, false),
|
||||
map(0x27, MINECRAFT_1_20_5, false));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -449,7 +558,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) {
|
||||
@@ -458,19 +567,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) {
|
||||
@@ -483,23 +591,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);
|
||||
|
||||
@@ -527,16 +631,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);
|
||||
}
|
||||
}
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import lombok.Getter;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.ID_TO_PROTOCOL_CONSTANT;
|
||||
|
||||
// Mostly taken from
|
||||
// https://github.com/Nan1t/NanoLimbo/blob/main/src/main/java/ua/nanit/limbo/protocol/PacketSnapshot.java
|
||||
@Getter
|
||||
public final class FallbackPacketSnapshot implements FallbackPacket {
|
||||
private final Map<Integer, Integer> mappings = new HashMap<>(ID_TO_PROTOCOL_CONSTANT.size());
|
||||
private final Map<Integer, byte[]> cachedBytes = new HashMap<>(ID_TO_PROTOCOL_CONSTANT.size());
|
||||
private final FallbackPacket originalPacket;
|
||||
|
||||
public FallbackPacketSnapshot(final @NotNull FallbackPacket originalPacket) {
|
||||
final Map<Integer, Integer> hashes = new HashMap<>(ID_TO_PROTOCOL_CONSTANT.size());
|
||||
|
||||
for (final ProtocolVersion protocolVersion : ID_TO_PROTOCOL_CONSTANT.values()) {
|
||||
// Allocate a buffer for each protocol version
|
||||
final ByteBuf byteBuf = Unpooled.buffer();
|
||||
try {
|
||||
originalPacket.encode(byteBuf, protocolVersion);
|
||||
} catch (Exception exception) {
|
||||
Sonar.get().getLogger().error("Could not encode packet {} for version {}: {}",
|
||||
originalPacket.toString(), protocolVersion, exception);
|
||||
break;
|
||||
}
|
||||
|
||||
// Make sure we don't unnecessarily fill the RAM
|
||||
final int protocol = protocolVersion.getProtocol();
|
||||
final int hash = byteBuf.hashCode();
|
||||
final int hashed = hashes.getOrDefault(hash, -1);
|
||||
if (hashed != -1) {
|
||||
mappings.put(protocol, hashed);
|
||||
} else {
|
||||
hashes.put(hash, protocol);
|
||||
mappings.put(protocol, protocol);
|
||||
// Cache the raw bytes of the encoded packet
|
||||
final byte[] bytes = new byte[byteBuf.readableBytes()];
|
||||
byteBuf.readBytes(bytes);
|
||||
cachedBytes.put(protocol, bytes);
|
||||
}
|
||||
// Make sure to release the buffer to avoid memory leaks
|
||||
byteBuf.release();
|
||||
}
|
||||
this.originalPacket = originalPacket;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void encode(final @NotNull ByteBuf byteBuf,
|
||||
final @NotNull ProtocolVersion protocolVersion) throws Exception {
|
||||
final int hash = mappings.get(protocolVersion.getProtocol());
|
||||
final byte[] bytes = cachedBytes.get(hash);
|
||||
|
||||
if (bytes != null) {
|
||||
byteBuf.writeBytes(bytes);
|
||||
} else {
|
||||
Sonar.get().getLogger().error("Could not find cached packet {} for version {}",
|
||||
toString(), protocolVersion);
|
||||
throw new IllegalStateException("Unable to find cached packet. Contact the developer!");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) throws Exception {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
||||
+103
-44
@@ -18,34 +18,44 @@
|
||||
package xyz.jonesdev.sonar.common.fallback.protocol;
|
||||
|
||||
import lombok.experimental.UtilityClass;
|
||||
import net.kyori.adventure.text.minimessage.MiniMessage;
|
||||
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.config.SonarConfiguration;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.block.BlockPosition;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.block.BlockType;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.block.ChangedBlock;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.map.CaptchaPreparer;
|
||||
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.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.configuration.FinishConfigurationPacket;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.configuration.RegistryDataPacket;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.login.LoginSuccessPacket;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.*;
|
||||
import xyz.jonesdev.sonar.common.util.ComponentHolder;
|
||||
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
|
||||
@UtilityClass
|
||||
public class FallbackPreparer {
|
||||
|
||||
// LoginSuccess
|
||||
public final FallbackPacket LOGIN_SUCCESS = new FallbackPacketSnapshot(new LoginSuccessPacket(new UUID(1L, 1L), "Sonar"));
|
||||
// Abilities
|
||||
public final FallbackPacket DEFAULT_ABILITIES = new ClientAbilitiesPacket(0x00, 0f, 0f);
|
||||
public final FallbackPacket CAPTCHA_ABILITIES = new ClientAbilitiesPacket(0x02, 0f, 0f);
|
||||
public final FallbackPacket DEFAULT_ABILITIES = new FallbackPacketSnapshot(new PlayerAbilitiesPacket(0x00, 0f, 0f));
|
||||
public final FallbackPacket CAPTCHA_ABILITIES = new FallbackPacketSnapshot(new PlayerAbilitiesPacket(0x02, 0f, 0f));
|
||||
public final FallbackPacket CAPTCHA_ABILITIES_BEDROCK = new FallbackPacketSnapshot(new PlayerAbilitiesPacket(0x06, 0f, 0f));
|
||||
// Chunks
|
||||
public final FallbackPacket EMPTY_CHUNK_DATA = new ChunkDataPacket(0, 0);
|
||||
public final FallbackPacket EMPTY_CHUNK_DATA = new FallbackPacketSnapshot(new ChunkDataPacket(0, 0));
|
||||
// Finish Configuration
|
||||
public final FallbackPacket FINISH_CONFIGURATION = new FinishConfigurationPacket();
|
||||
public final FallbackPacket FINISH_CONFIGURATION = new FinishConfigurationPacket(); // No snapshot needed because it's empty either way
|
||||
// Synchronize Registry
|
||||
public final FallbackPacket REGISTRY_SYNC = new RegistryDataPacket();
|
||||
public final FallbackPacket REGISTRY_SYNC_LEGACY = new FallbackPacketSnapshot(new RegistryDataPacket(DimensionRegistry.CODEC_1_20, null, null));
|
||||
public final FallbackPacket[] REGISTRY_SYNC_1_20_5 = RegistryDataPacket.of(DimensionRegistry.CODEC_1_20);
|
||||
public final FallbackPacket[] REGISTRY_SYNC_1_21 = RegistryDataPacket.of(DimensionRegistry.CODEC_1_21);
|
||||
// Keep Alive
|
||||
public final FallbackPacket CAPTCHA_KEEP_ALIVE = new KeepAlivePacket(0L);
|
||||
public final FallbackPacket CAPTCHA_KEEP_ALIVE = new FallbackPacketSnapshot(new KeepAlivePacket(-1337L));
|
||||
// Game Event (1.20.3+)
|
||||
public final FallbackPacket START_WRITING_CHUNKS = new GameEventPacket(13, 0);
|
||||
public final FallbackPacket START_WRITING_CHUNKS = new FallbackPacketSnapshot(new GameEventPacket(13, 0));
|
||||
// Chat
|
||||
public FallbackPacket enterCodeMessage;
|
||||
public FallbackPacket incorrectCaptcha;
|
||||
@@ -54,8 +64,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 static FallbackPacket transferToOrigin;
|
||||
|
||||
// Disconnect messages
|
||||
public FallbackPacket blacklisted;
|
||||
@@ -64,10 +82,11 @@ public class FallbackPreparer {
|
||||
public FallbackPacket reconnectedTooFast;
|
||||
public FallbackPacket protocolBlacklisted;
|
||||
public FallbackPacket invalidUsername;
|
||||
public FallbackPacket tooManyOnlinePerIP;
|
||||
|
||||
// Vehicle
|
||||
public FallbackPacket removeEntities;
|
||||
public FallbackPacket setPassengers;
|
||||
public static FallbackPacket setPassengers;
|
||||
public static final int VEHICLE_ENTITY_ID = PLAYER_ENTITY_ID + 1;
|
||||
|
||||
// Collisions
|
||||
@@ -77,15 +96,11 @@ public class FallbackPreparer {
|
||||
public final int DEFAULT_Y_COLLIDE_POSITION = 255; // 255 is the maximum Y position allowed
|
||||
|
||||
// Captcha position
|
||||
public final FallbackPacket CAPTCHA_POSITION = new PlayerPositionLookPacket(
|
||||
SPAWN_X_POSITION, 1337, SPAWN_Z_POSITION, 0f, 90f, 0, false);
|
||||
public final FallbackPacket CAPTCHA_SPAWN_POSITION = new DefaultSpawnPositionPacket(
|
||||
SPAWN_X_POSITION, 1337, SPAWN_Z_POSITION);
|
||||
public final FallbackPacket CAPTCHA_POSITION = new FallbackPacketSnapshot(new SetPlayerPositionRotation(
|
||||
SPAWN_X_POSITION, 1337, SPAWN_Z_POSITION, 0f, 90f, 0, false));
|
||||
|
||||
// Blocks
|
||||
private final ChangedBlock[] CHANGED_BLOCKS = new ChangedBlock[BLOCKS_PER_ROW * BLOCKS_PER_ROW];
|
||||
// Platform
|
||||
public BlockType blockType = BlockType.BARRIER;
|
||||
|
||||
public int maxMovementTick, dynamicSpawnYPosition;
|
||||
public double[] preparedCachedYMotions;
|
||||
public double maxFallDistance;
|
||||
@@ -93,13 +108,23 @@ public class FallbackPreparer {
|
||||
// CAPTCHA
|
||||
public final CaptchaPreparer MAP_INFO_PREPARER = new CaptchaPreparer();
|
||||
|
||||
// XP packets
|
||||
public FallbackPacket[] xpCountdown;
|
||||
|
||||
@SuppressWarnings("ResultOfMethodCallIgnored")
|
||||
public void prepare() {
|
||||
joinGame = new JoinGamePacket(PLAYER_ENTITY_ID,
|
||||
// Preload the packet registry to avoid CPU/RAM issues on 1st connection
|
||||
Sonar.get().getLogger().info("Preloading all registered packets...");
|
||||
FallbackPacketRegistry.values();
|
||||
|
||||
// Prepare JoinGame packet
|
||||
joinGame = new FallbackPacketSnapshot(new JoinGamePacket(PLAYER_ENTITY_ID,
|
||||
Sonar.get().getConfig().getVerification().getGravity().getGamemode().getId(),
|
||||
0, false, 0,
|
||||
true, false, false,
|
||||
new String[]{"minecraft:overworld"}, "minecraft:overworld");
|
||||
new String[]{"minecraft:overworld"}, "minecraft:overworld"));
|
||||
|
||||
// Prepare cached motions for the gravity check
|
||||
maxFallDistance = 0;
|
||||
maxMovementTick = Sonar.get().getConfig().getVerification().getGravity().getMaxMovementTicks();
|
||||
preparedCachedYMotions = new double[maxMovementTick + 8];
|
||||
@@ -112,44 +137,78 @@ 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 DefaultSpawnPositionPacket(SPAWN_X_POSITION, dynamicSpawnYPosition, SPAWN_Z_POSITION);
|
||||
defaultSpawnPosition = new FallbackPacketSnapshot(new SetDefaultSpawnPositionPacket(
|
||||
SPAWN_X_POSITION, dynamicSpawnYPosition, SPAWN_Z_POSITION));
|
||||
spawnPosition = new FallbackPacketSnapshot(new SetPlayerPositionRotation(
|
||||
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++) {
|
||||
final BlockPosition position = new BlockPosition(
|
||||
final BlockUpdate.BlockPosition position = new BlockUpdate.BlockPosition(
|
||||
x + (BLOCKS_PER_ROW / 2),
|
||||
DEFAULT_Y_COLLIDE_POSITION,
|
||||
z + (BLOCKS_PER_ROW / 2),
|
||||
0, 0);
|
||||
CHANGED_BLOCKS[index++] = new ChangedBlock(position, blockType);
|
||||
changedBlocks[index++] = new BlockUpdate(position, blockType);
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare UpdateSectionBlocks packet
|
||||
updateSectionBlocks = new UpdateSectionBlocksPacket(0, 0, CHANGED_BLOCKS);
|
||||
updateSectionBlocks = new FallbackPacketSnapshot(new UpdateSectionBlocksPacket(0, 0, changedBlocks));
|
||||
|
||||
// Prepare disconnect packets during login
|
||||
blacklisted = DisconnectPacket.create(Sonar.get().getConfig().getVerification().getBlacklisted(), true);
|
||||
alreadyVerifying = DisconnectPacket.create(Sonar.get().getConfig().getVerification().getAlreadyVerifying(), true);
|
||||
alreadyQueued = DisconnectPacket.create(Sonar.get().getConfig().getVerification().getAlreadyQueued(), true);
|
||||
protocolBlacklisted = DisconnectPacket.create(Sonar.get().getConfig().getVerification().getProtocolBlacklisted(), true);
|
||||
reconnectedTooFast = DisconnectPacket.create(Sonar.get().getConfig().getVerification().getTooFastReconnect(), true);
|
||||
invalidUsername = DisconnectPacket.create(Sonar.get().getConfig().getVerification().getInvalidUsername(), true);
|
||||
blacklisted = new FallbackPacketSnapshot(DisconnectPacket.create(Sonar.get().getConfig().getVerification().getBlacklisted(), true));
|
||||
alreadyVerifying = new FallbackPacketSnapshot(DisconnectPacket.create(Sonar.get().getConfig().getVerification().getAlreadyVerifying(), true));
|
||||
alreadyQueued = new FallbackPacketSnapshot(DisconnectPacket.create(Sonar.get().getConfig().getVerification().getAlreadyQueued(), true));
|
||||
protocolBlacklisted = new FallbackPacketSnapshot(DisconnectPacket.create(Sonar.get().getConfig().getVerification().getProtocolBlacklisted(), true));
|
||||
reconnectedTooFast = new FallbackPacketSnapshot(DisconnectPacket.create(Sonar.get().getConfig().getVerification().getTooFastReconnect(), true));
|
||||
invalidUsername = new FallbackPacketSnapshot(DisconnectPacket.create(Sonar.get().getConfig().getVerification().getInvalidUsername(), true));
|
||||
tooManyOnlinePerIP = new FallbackPacketSnapshot(DisconnectPacket.create(Sonar.get().getConfig().getTooManyOnlinePerIp(), true));
|
||||
|
||||
// Precompute captcha answers
|
||||
if (Sonar.get().getConfig().getVerification().getMap().getTiming() != SonarConfiguration.Verification.Timing.NEVER
|
||||
|| Sonar.get().getConfig().getVerification().getGravity().isCaptchaOnFail()) {
|
||||
enterCodeMessage = new UniversalChatPacket(Sonar.get().getConfig().getVerification().getMap().getEnterCode(), UniversalChatPacket.SYSTEM_TYPE);
|
||||
incorrectCaptcha = new UniversalChatPacket(
|
||||
Sonar.get().getConfig().getVerification().getMap().getFailedCaptcha(), UniversalChatPacket.SYSTEM_TYPE);
|
||||
|
||||
MAP_INFO_PREPARER.prepare();
|
||||
// Prepare transfer packet
|
||||
if (Sonar.get().getConfig().getGeneralConfig().getBoolean("verification.transfer.enabled")) {
|
||||
transferToOrigin = new FallbackPacketSnapshot(new TransferPacket(
|
||||
Sonar.get().getConfig().getGeneralConfig().getString("verification.transfer.destination-host"),
|
||||
Sonar.get().getConfig().getGeneralConfig().getInt("verification.transfer.destination-port")));
|
||||
} else {
|
||||
transferToOrigin = null;
|
||||
}
|
||||
|
||||
// Prepare packets for the vehicle check
|
||||
removeEntities = new RemoveEntitiesPacket(VEHICLE_ENTITY_ID);
|
||||
setPassengers = new SetPassengersPacket(VEHICLE_ENTITY_ID, PLAYER_ENTITY_ID);
|
||||
removeEntities = new FallbackPacketSnapshot(new RemoveEntitiesPacket(VEHICLE_ENTITY_ID));
|
||||
setPassengers = new FallbackPacketSnapshot(new SetPassengersPacket(VEHICLE_ENTITY_ID, PLAYER_ENTITY_ID));
|
||||
|
||||
if (Sonar.get().getConfig().getVerification().getMap().getTiming() != SonarConfiguration.Verification.Timing.NEVER
|
||||
|| Sonar.get().getConfig().getVerification().getGravity().isCaptchaOnFail()) {
|
||||
// Prepare CAPTCHA messages
|
||||
enterCodeMessage = new FallbackPacketSnapshot(new SystemChatPacket(new ComponentHolder(
|
||||
MiniMessage.miniMessage().deserialize(
|
||||
Sonar.get().getConfig().getMessagesConfig().getString("verification.captcha.enter-code"),
|
||||
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix())))));
|
||||
incorrectCaptcha = new FallbackPacketSnapshot(new SystemChatPacket(new ComponentHolder(
|
||||
MiniMessage.miniMessage().deserialize(
|
||||
Sonar.get().getConfig().getMessagesConfig().getString("verification.captcha.incorrect"),
|
||||
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix())))));
|
||||
|
||||
// Prepare countdown
|
||||
xpCountdown = new FallbackPacket[Sonar.get().getConfig().getVerification().getMap().getMaxDuration() / 1000];
|
||||
|
||||
for (int i = 0; i < xpCountdown.length; i++) {
|
||||
final float bar = (float) i / xpCountdown.length;
|
||||
xpCountdown[i] = new FallbackPacketSnapshot(new SetExperiencePacket(bar, i, i));
|
||||
}
|
||||
|
||||
// Prepare CAPTCHA answers
|
||||
MAP_INFO_PREPARER.prepare();
|
||||
} else {
|
||||
// Throw away if not needed
|
||||
enterCodeMessage = null;
|
||||
incorrectCaptcha = null;
|
||||
xpCountdown = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
-30
@@ -1,30 +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.protocol.block;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.ToString;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@RequiredArgsConstructor
|
||||
public final class BlockPosition {
|
||||
private final int x, y, z;
|
||||
private final int chunkX, chunkZ;
|
||||
}
|
||||
+2
@@ -78,6 +78,8 @@ public enum BlockType {
|
||||
return 10225;
|
||||
case MINECRAFT_1_20_2:
|
||||
case MINECRAFT_1_20_3:
|
||||
case MINECRAFT_1_20_5:
|
||||
case MINECRAFT_1_21:
|
||||
return 10366;
|
||||
}
|
||||
}, 1D);
|
||||
|
||||
+12
-2
@@ -18,15 +18,17 @@
|
||||
package xyz.jonesdev.sonar.common.fallback.protocol.block;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.ToString;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
@Getter
|
||||
public final class ChangedBlock {
|
||||
public final class BlockUpdate {
|
||||
private final @NotNull BlockPosition position;
|
||||
private final BlockType type;
|
||||
private final int legacyBlockState, blockState;
|
||||
|
||||
public ChangedBlock(final @NotNull BlockPosition position, final BlockType type) {
|
||||
public BlockUpdate(final @NotNull BlockPosition position, final BlockType type) {
|
||||
this.position = position;
|
||||
this.type = type;
|
||||
final int x = position.getX() - (position.getChunkX() << 4);
|
||||
@@ -35,4 +37,12 @@ public final class ChangedBlock {
|
||||
this.legacyBlockState = x << 12 | z << 8 | y;
|
||||
this.blockState = x << 8 | z << 4 | y - ((y >> 4) << 4);
|
||||
}
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@RequiredArgsConstructor
|
||||
public static final class BlockPosition {
|
||||
private final int x, y, z;
|
||||
private final int chunkX, chunkZ;
|
||||
}
|
||||
}
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
/*
|
||||
* 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.captcha;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import xyz.jonesdev.capja.SimpleCaptchaGenerator;
|
||||
import xyz.jonesdev.capja.filter.SimpleRippleFilter;
|
||||
import xyz.jonesdev.capja.filter.SimpleScratchFilter;
|
||||
import xyz.jonesdev.capja.libs.jhlabs.image.AbstractBufferedImageOp;
|
||||
import xyz.jonesdev.capja.libs.jhlabs.image.BumpFilter;
|
||||
import xyz.jonesdev.capja.libs.jhlabs.image.SmearFilter;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.config.SonarConfiguration;
|
||||
import xyz.jonesdev.sonar.api.timer.SystemTimer;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
|
||||
public final class CaptchaPreparer {
|
||||
private static final ExecutorService PREPARATION_SERVICE = Executors.newSingleThreadExecutor();
|
||||
private static final Random RANDOM = new Random();
|
||||
|
||||
private MapCaptchaInfo[] cached;
|
||||
private int preparedAmount;
|
||||
|
||||
public void prepare() {
|
||||
// Make sure we're not preparing when Sonar is already preparing answers
|
||||
if (cached != null && preparedAmount != cached.length) return;
|
||||
preparedAmount = 0;
|
||||
|
||||
final SystemTimer timer = new SystemTimer();
|
||||
Sonar.get().getLogger().info("Asynchronously preparing CAPTCHA answers...");
|
||||
Sonar.get().getLogger().info("Players will be able to join even if the preparation isn't finished");
|
||||
|
||||
// Prepare cache
|
||||
final SonarConfiguration.Verification.Map config = Sonar.get().getConfig().getVerification().getMap();
|
||||
cached = new MapCaptchaInfo[config.getPrecomputeAmount()];
|
||||
|
||||
// Prepare everything asynchronously
|
||||
PREPARATION_SERVICE.execute(() -> {
|
||||
// Create the images using capja
|
||||
final @Nullable File backgroundImage = Sonar.get().getConfig().getVerification().getMap().getBackgroundImage();
|
||||
final SimpleCaptchaGenerator generator = new SimpleCaptchaGenerator(128, 128, backgroundImage);
|
||||
final char[] dictionary = config.getDictionary().toCharArray();
|
||||
|
||||
// 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++) {
|
||||
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);
|
||||
final Color color1 = Color.getHSBColor((float) Math.random(), 1, 0.5f);
|
||||
final GradientPaint gradient = new GradientPaint(0, 0, color0,
|
||||
generator.getWidth(), generator.getHeight(), color1);
|
||||
generator.setGradient(gradient);
|
||||
}
|
||||
// Generate CAPTCHA answer
|
||||
final char[] answer = new char[5];
|
||||
for (int j = 0; j < answer.length; j++) {
|
||||
answer[j] = dictionary[ThreadLocalRandom.current().nextInt(dictionary.length)];
|
||||
}
|
||||
final BufferedImage image = generator.createImage(answer, filters);
|
||||
// Convert and cache converted Minecraft map bytes
|
||||
final int[] buffer = MapColorPalette.getBufferFromImage(image);
|
||||
cached[preparedAmount] = new MapCaptchaInfo(new String(answer), buffer);
|
||||
}
|
||||
|
||||
Sonar.get().getLogger().info("Finished preparing {} CAPTCHA answers ({}s)!", preparedAmount, timer);
|
||||
});
|
||||
}
|
||||
|
||||
public boolean isCaptchaAvailable() {
|
||||
return cached != null && cached[0] != null;
|
||||
}
|
||||
|
||||
public MapCaptchaInfo getRandomCaptcha() {
|
||||
// Give the player a random CAPTCHA out of the ones that we've already prepared
|
||||
return cached[RANDOM.nextInt(preparedAmount)];
|
||||
}
|
||||
}
|
||||
+4
-1
@@ -15,7 +15,7 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.common.fallback.protocol.map;
|
||||
package xyz.jonesdev.sonar.common.fallback.protocol.captcha;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
@@ -72,6 +72,9 @@ public enum ItemType {
|
||||
return 941;
|
||||
case MINECRAFT_1_20_3:
|
||||
return 979;
|
||||
case MINECRAFT_1_20_5:
|
||||
case MINECRAFT_1_21:
|
||||
return 982;
|
||||
}
|
||||
});
|
||||
|
||||
+15
-16
@@ -15,11 +15,12 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.common.fallback.protocol.map;
|
||||
package xyz.jonesdev.sonar.common.fallback.protocol.captcha;
|
||||
|
||||
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.packets.play.MapDataPacket;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_8;
|
||||
@@ -27,22 +28,20 @@ import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT
|
||||
@Getter
|
||||
public final class MapCaptchaInfo {
|
||||
private final String answer;
|
||||
private final byte[] buffer;
|
||||
|
||||
private final MapDataPacket[] legacy;
|
||||
private final MapDataPacket modern;
|
||||
private final FallbackPacket[] legacy;
|
||||
private final FallbackPacket modern;
|
||||
|
||||
public MapCaptchaInfo(final @NotNull String answer, final byte @NotNull [] buffer) {
|
||||
public MapCaptchaInfo(final @NotNull String answer, final int @NotNull [] buffer) {
|
||||
this.answer = answer;
|
||||
this.buffer = buffer;
|
||||
|
||||
// Prepare 1.7 map data using a grid
|
||||
final byte[][] grid = new byte[128][128];
|
||||
final int[][] grid = new int[128][128];
|
||||
for (int i = 0; i < buffer.length; i++) {
|
||||
final byte buf = buffer[i];
|
||||
final int buf = buffer[i];
|
||||
grid[i & Byte.MAX_VALUE][i >> 7] = buf;
|
||||
}
|
||||
this.legacy = new MapDataPacket[grid.length];
|
||||
this.legacy = new FallbackPacket[grid.length];
|
||||
for (int i = 0; i < grid.length; i++) {
|
||||
this.legacy[i] = new MapDataPacket(grid[i], i, 0);
|
||||
}
|
||||
@@ -52,14 +51,14 @@ public final class MapCaptchaInfo {
|
||||
}
|
||||
|
||||
public void delayedWrite(final @NotNull FallbackUser user) {
|
||||
if (user.getProtocolVersion().compareTo(MINECRAFT_1_8) >= 0) {
|
||||
user.delayedWrite(modern);
|
||||
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;
|
||||
}
|
||||
|
||||
// 1.7.2-1.7.10
|
||||
for (final MapDataPacket legacyPacket : legacy) {
|
||||
user.delayedWrite(legacyPacket);
|
||||
}
|
||||
// Send modern packet
|
||||
user.delayedWrite(modern);
|
||||
}
|
||||
}
|
||||
+44
-28
@@ -15,7 +15,7 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.common.fallback.protocol.map;
|
||||
package xyz.jonesdev.sonar.common.fallback.protocol.captcha;
|
||||
|
||||
import lombok.experimental.UtilityClass;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
@@ -26,10 +26,10 @@ import java.awt.image.BufferedImage;
|
||||
public class MapColorPalette {
|
||||
// https://minecraft.wiki/w/Map_item_format#Full_color_tables
|
||||
public static final int[] COLOR_MAP = {
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
-16777216,
|
||||
-16777216,
|
||||
-16777216,
|
||||
-16777216,
|
||||
-10912473,
|
||||
-9594576,
|
||||
-8408520,
|
||||
@@ -38,10 +38,10 @@ public class MapColorPalette {
|
||||
-2766452,
|
||||
-530013,
|
||||
-8225962,
|
||||
-7697782,
|
||||
-5658199,
|
||||
-3815995,
|
||||
-9934744,
|
||||
-7566196,
|
||||
-5526613,
|
||||
-3684409,
|
||||
-9868951,
|
||||
-4980736,
|
||||
-2359296,
|
||||
-65536,
|
||||
@@ -66,22 +66,22 @@ public class MapColorPalette {
|
||||
-7499618,
|
||||
-5986120,
|
||||
-11118495,
|
||||
-8304095,
|
||||
-6464728,
|
||||
-4756945,
|
||||
-10471400,
|
||||
-9810890,
|
||||
-8233406,
|
||||
-6853299,
|
||||
-11585240,
|
||||
-11579569,
|
||||
-10461088,
|
||||
-9408400,
|
||||
-12895429,
|
||||
-10136525,
|
||||
-8689858,
|
||||
-7375032,
|
||||
-11780314,
|
||||
-13816396,
|
||||
-13158436,
|
||||
-12566273,
|
||||
-14605945,
|
||||
-10202062,
|
||||
-8690114,
|
||||
-7375032,
|
||||
-11845850,
|
||||
-4935252,
|
||||
-2303533,
|
||||
-779,
|
||||
@@ -153,19 +153,34 @@ public class MapColorPalette {
|
||||
-12543338,
|
||||
-11551561,
|
||||
-10691627,
|
||||
-13601936,
|
||||
-13346124,
|
||||
-12620068,
|
||||
-11894529,
|
||||
-14204025,
|
||||
-16738008,
|
||||
-16729294,
|
||||
-16721606,
|
||||
-16748002,
|
||||
-10798046,
|
||||
-9483734,
|
||||
-8301007,
|
||||
-12309223,
|
||||
-11599616,
|
||||
-10485504,
|
||||
-9436672,
|
||||
-12910336,
|
||||
};
|
||||
|
||||
public byte[] getBufferFromImage(final @NotNull BufferedImage bufferedImage) {
|
||||
final int width = bufferedImage.getWidth();
|
||||
final int height = bufferedImage.getHeight();
|
||||
public int[] getBufferFromImage(final @NotNull BufferedImage bufferedImage) {
|
||||
final int[] buffer = new int[128 * 128];
|
||||
|
||||
final int[] pixels = new int[128 * 128];
|
||||
final byte[] buffer = new byte[128 * 128];
|
||||
bufferedImage.getRGB(0, 0,
|
||||
bufferedImage.getWidth(), bufferedImage.getHeight(),
|
||||
buffer, 0, bufferedImage.getWidth());
|
||||
|
||||
bufferedImage.getRGB(0, 0, width, height, pixels, 0, width);
|
||||
|
||||
for (int i = 0; i < pixels.length; i++) {
|
||||
buffer[i] = mapColorToMinecraft(pixels[i]);
|
||||
for (int i = 0; i < buffer.length; i++) {
|
||||
buffer[i] = mapColorToMinecraft(buffer[i]);
|
||||
}
|
||||
return buffer;
|
||||
}
|
||||
@@ -173,11 +188,12 @@ public class MapColorPalette {
|
||||
private byte mapColorToMinecraft(final int argb) {
|
||||
int minDistance = Integer.MAX_VALUE;
|
||||
byte closestIndex = 0;
|
||||
for (byte i = 0; i < COLOR_MAP.length; i++) {
|
||||
for (int i = 0; i < COLOR_MAP.length; i++) {
|
||||
final byte index = (byte) (i < 128 ? i : -129 + (i - 127));
|
||||
final int distance = calculateColorDistance(argb, COLOR_MAP[i]);
|
||||
if (distance < minDistance) {
|
||||
minDistance = distance;
|
||||
closestIndex = i;
|
||||
closestIndex = index;
|
||||
}
|
||||
}
|
||||
return closestIndex;
|
||||
+2
@@ -39,6 +39,7 @@ public final class DimensionRegistry {
|
||||
public final @NotNull CompoundBinaryTag CODEC_1_19_1;
|
||||
public final @NotNull CompoundBinaryTag CODEC_1_19_4;
|
||||
public final @NotNull CompoundBinaryTag CODEC_1_20;
|
||||
public final @NotNull CompoundBinaryTag CODEC_1_21;
|
||||
public final @NotNull CompoundBinaryTag OLD_CODEC;
|
||||
|
||||
static {
|
||||
@@ -48,6 +49,7 @@ public final class DimensionRegistry {
|
||||
CODEC_1_19_1 = getCodec("codec_1_19_1.nbt");
|
||||
CODEC_1_19_4 = getCodec("codec_1_19_4.nbt");
|
||||
CODEC_1_20 = getCodec("codec_1_20.nbt");
|
||||
CODEC_1_21 = getCodec("codec_1_21.nbt");
|
||||
OLD_CODEC = getCodec("codec_old.nbt");
|
||||
|
||||
DEFAULT_DIMENSION_1_16 = getDimension(CODEC_1_16);
|
||||
|
||||
+4
-1
@@ -15,7 +15,7 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.common.fallback.protocol.vehicle;
|
||||
package xyz.jonesdev.sonar.common.fallback.protocol.entity;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
@@ -77,6 +77,9 @@ public enum EntityType {
|
||||
case MINECRAFT_1_20_2:
|
||||
case MINECRAFT_1_20_3:
|
||||
return 9;
|
||||
case MINECRAFT_1_20_5:
|
||||
case MINECRAFT_1_21:
|
||||
return 10;
|
||||
}
|
||||
});
|
||||
|
||||
-87
@@ -1,87 +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.protocol.map;
|
||||
|
||||
import xyz.jonesdev.capja.CaptchaGenerator;
|
||||
import xyz.jonesdev.capja.CaptchaHolder;
|
||||
import xyz.jonesdev.capja.config.CaptchaConfiguration;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.config.SonarConfiguration;
|
||||
import xyz.jonesdev.sonar.api.timer.SystemTimer;
|
||||
|
||||
import java.awt.*;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
public final class CaptchaPreparer {
|
||||
private static final ExecutorService PREPARATION_SERVICE = Executors.newSingleThreadExecutor();
|
||||
private static final Random RANDOM = new Random();
|
||||
|
||||
private MapCaptchaInfo[] cached;
|
||||
private int preparedAmount;
|
||||
|
||||
public void prepare() {
|
||||
// Make sure we're not preparing when Sonar is already preparing answers
|
||||
if (cached != null && preparedAmount != cached.length) return;
|
||||
preparedAmount = 0;
|
||||
|
||||
final SystemTimer timer = new SystemTimer();
|
||||
Sonar.get().getLogger().info("Asynchronously preparing CAPTCHA answers...");
|
||||
Sonar.get().getLogger().info("Players will be able to join even if the preparation isn't finished");
|
||||
|
||||
// Prepare cache
|
||||
final SonarConfiguration.Verification.Map config = Sonar.get().getConfig().getVerification().getMap();
|
||||
cached = new MapCaptchaInfo[config.getPrecomputeAmount()];
|
||||
|
||||
// Prepare everything asynchronously
|
||||
PREPARATION_SERVICE.execute(() -> {
|
||||
final List<String> fonts = config.getFonts();
|
||||
if (fonts.isEmpty()) {
|
||||
throw new IllegalArgumentException("No fonts specified!");
|
||||
}
|
||||
|
||||
// Create the configuration for the image generation
|
||||
final String[] fontNames = fonts.toArray(new String[0]);
|
||||
final CaptchaConfiguration configuration = new CaptchaConfiguration(
|
||||
128, 128, config.getDictionary().toCharArray(), 5,
|
||||
config.isFlare(), config.isScratches(), config.isRipple(), config.isSmear(), config.isPinch(),
|
||||
config.getSaturation(), config.getDistortion(), new int[]{Font.PLAIN, Font.BOLD}, fontNames);
|
||||
|
||||
for (int i = 0; i < config.getPrecomputeAmount(); i++) {
|
||||
final CaptchaGenerator captchaGenerator = new CaptchaGenerator(configuration);
|
||||
final CaptchaHolder captchaHolder = captchaGenerator.generate();
|
||||
final byte[] buffer = MapColorPalette.getBufferFromImage(captchaHolder.getImage());
|
||||
cached[i] = new MapCaptchaInfo(captchaHolder.getAnswer(), buffer);
|
||||
preparedAmount++;
|
||||
}
|
||||
|
||||
Sonar.get().getLogger().info("Finished preparing {} CAPTCHA answers ({}s)!", preparedAmount, timer);
|
||||
});
|
||||
}
|
||||
|
||||
public boolean isCaptchaAvailable() {
|
||||
return cached != null && cached[0] != null;
|
||||
}
|
||||
|
||||
public MapCaptchaInfo getRandomCaptcha() {
|
||||
// Give the player a random CAPTCHA out of the ones that we've already prepared
|
||||
return cached[RANDOM.nextInt(preparedAmount)];
|
||||
}
|
||||
}
|
||||
+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;
|
||||
}
|
||||
}
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* 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.configuration;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import lombok.*;
|
||||
import net.kyori.adventure.nbt.BinaryTag;
|
||||
import net.kyori.adventure.nbt.CompoundBinaryTag;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketSnapshot;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.*;
|
||||
|
||||
@Getter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public final class RegistryDataPacket implements FallbackPacket {
|
||||
private CompoundBinaryTag tag;
|
||||
private String type;
|
||||
private List<RegistryDataPacket.Bundle> bundles;
|
||||
|
||||
@Override
|
||||
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) {
|
||||
writeString(byteBuf, type);
|
||||
writeVarInt(byteBuf, bundles.size());
|
||||
|
||||
for (final RegistryDataPacket.Bundle bundle : bundles) {
|
||||
writeString(byteBuf, bundle.getName());
|
||||
// Write the bundle tag
|
||||
final CompoundBinaryTag tag = bundle.getTag();
|
||||
if (tag != null) {
|
||||
byteBuf.writeBoolean(true);
|
||||
writeNamelessCompoundTag(byteBuf, tag);
|
||||
} else {
|
||||
byteBuf.writeBoolean(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
public static FallbackPacket @NotNull [] of(final @NotNull CompoundBinaryTag rootTag) {
|
||||
final FallbackPacket[] packets = new FallbackPacket[rootTag.size()];
|
||||
int index = 0;
|
||||
for (final String type : rootTag.keySet()) {
|
||||
final ArrayList<RegistryDataPacket.Bundle> bundles = new ArrayList<>();
|
||||
for (final BinaryTag binaryTag : rootTag.getCompound(type).getList("value")) {
|
||||
final CompoundBinaryTag tag = (CompoundBinaryTag) binaryTag;
|
||||
bundles.add(new Bundle(tag.getString("name"), tag.getCompound("element")));
|
||||
}
|
||||
packets[index++] = new FallbackPacketSnapshot(new RegistryDataPacket(rootTag, type, bundles));
|
||||
}
|
||||
return packets;
|
||||
}
|
||||
|
||||
@Data
|
||||
@Value
|
||||
public static class Bundle {
|
||||
@NotNull String name;
|
||||
// In the protocol, it is able to be null
|
||||
@Nullable CompoundBinaryTag tag;
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -25,6 +25,7 @@ public final class LoginAcknowledgedPacket implements FallbackPacket {
|
||||
|
||||
@Override
|
||||
public void encode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -33,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;
|
||||
}
|
||||
}
|
||||
+8
-6
@@ -24,25 +24,24 @@ 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.netty.FastUUID;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
|
||||
import xyz.jonesdev.sonar.common.util.FastUuidSansHyphens;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
|
||||
import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.*;
|
||||
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.writeVarInt;
|
||||
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.*;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public final class LoginSuccessPacket implements FallbackPacket {
|
||||
private String username;
|
||||
private UUID uuid;
|
||||
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) {
|
||||
@@ -50,7 +49,7 @@ public final class LoginSuccessPacket implements FallbackPacket {
|
||||
} else if (protocolVersion.compareTo(MINECRAFT_1_7_6) >= 0) {
|
||||
writeString(byteBuf, uuid.toString());
|
||||
} else {
|
||||
writeString(byteBuf, FastUUID.toString(uuid));
|
||||
writeString(byteBuf, FastUuidSansHyphens.toString(uuid));
|
||||
}
|
||||
|
||||
writeString(byteBuf, username);
|
||||
@@ -58,6 +57,9 @@ public final class LoginSuccessPacket implements FallbackPacket {
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_19) >= 0) {
|
||||
writeVarInt(byteBuf, 0); // properties
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_20_5) >= 0) {
|
||||
byteBuf.writeBoolean(false); // should authenticate
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+2
-3
@@ -29,8 +29,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.utility.protocol.ProtocolUtil.*;
|
||||
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.writeVarInt;
|
||||
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.*;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@@ -67,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);
|
||||
|
||||
|
||||
+5
-5
@@ -27,19 +27,19 @@ 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.utility.protocol.ProtocolUtil.readString;
|
||||
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.readVarInt;
|
||||
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.readString;
|
||||
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.readVarInt;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public final class ClientSettingsPacket implements FallbackPacket {
|
||||
public final class ClientInformationPacket implements FallbackPacket {
|
||||
private String locale;
|
||||
private byte viewDistance;
|
||||
private int chatVisibility;
|
||||
private boolean chatColors;
|
||||
private byte difficulty; // 1.7 Protocol
|
||||
private byte difficulty; // 1.7
|
||||
private short skinParts;
|
||||
private int mainHand;
|
||||
private boolean chatFilteringEnabled; // Added in 1.17
|
||||
@@ -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);
|
||||
+3
-3
@@ -25,13 +25,13 @@ import lombok.ToString;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
|
||||
|
||||
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.readVarInt;
|
||||
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.readVarInt;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public final class TeleportConfirmPacket implements FallbackPacket {
|
||||
public final class ConfirmTeleportationPacket implements FallbackPacket {
|
||||
private int teleportId;
|
||||
|
||||
@Override
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
+6
-7
@@ -26,7 +26,7 @@ import net.kyori.adventure.text.Component;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
|
||||
import xyz.jonesdev.sonar.common.utility.component.ComponentHolder;
|
||||
import xyz.jonesdev.sonar.common.util.ComponentHolder;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@@ -34,11 +34,11 @@ import xyz.jonesdev.sonar.common.utility.component.ComponentHolder;
|
||||
@AllArgsConstructor
|
||||
public final class DisconnectPacket implements FallbackPacket {
|
||||
private @NotNull ComponentHolder componentHolder;
|
||||
private boolean login;
|
||||
private boolean duringLogin;
|
||||
|
||||
@Override
|
||||
public void encode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
|
||||
componentHolder.write(byteBuf, login ? ProtocolVersion.MINECRAFT_1_20_2 : protocolVersion);
|
||||
public void encode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) throws Exception {
|
||||
componentHolder.write(byteBuf, duringLogin ? ProtocolVersion.MINECRAFT_1_20_2 : protocolVersion);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
+26
-4
@@ -27,8 +27,7 @@ import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
|
||||
import static xyz.jonesdev.sonar.common.fallback.protocol.dimension.DimensionRegistry.*;
|
||||
import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.*;
|
||||
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.writeVarInt;
|
||||
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.*;
|
||||
|
||||
// Mostly taken from
|
||||
// https://github.com/Nan1t/NanoLimbo/blob/main/src/main/java/ua/nanit/limbo/protocol/packets/play/PacketJoinGame.java
|
||||
@@ -48,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)) {
|
||||
@@ -210,7 +209,7 @@ public final class JoinGamePacket implements FallbackPacket {
|
||||
return;
|
||||
}
|
||||
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_20_2) >= 0) {
|
||||
if (protocolVersion.inBetween(MINECRAFT_1_20_2, MINECRAFT_1_20_3)) {
|
||||
byteBuf.writeBoolean(isHardcore);
|
||||
writeStringArray(byteBuf, levelNames);
|
||||
writeVarInt(byteBuf, 0); // max players
|
||||
@@ -228,6 +227,29 @@ public final class JoinGamePacket implements FallbackPacket {
|
||||
byteBuf.writeBoolean(false); // flat
|
||||
byteBuf.writeBoolean(false); // no last death location
|
||||
writeVarInt(byteBuf, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_20_5) >= 0) {
|
||||
byteBuf.writeBoolean(isHardcore);
|
||||
writeStringArray(byteBuf, levelNames);
|
||||
writeVarInt(byteBuf, 0); // max players
|
||||
writeVarInt(byteBuf, viewDistance);
|
||||
writeVarInt(byteBuf, viewDistance); // simulation distance
|
||||
byteBuf.writeBoolean(reducedDebugInfo);
|
||||
byteBuf.writeBoolean(showRespawnScreen);
|
||||
byteBuf.writeBoolean(false); // limited crafting
|
||||
// It depends on the dimension data we send in RegistryData. If we only send one dimension. It should be 0.
|
||||
writeVarInt(byteBuf, 0);
|
||||
writeString(byteBuf, levelName);
|
||||
byteBuf.writeLong(partialHashedSeed);
|
||||
byteBuf.writeByte(gamemode);
|
||||
byteBuf.writeByte(-1); // previous gamemode
|
||||
byteBuf.writeBoolean(false); // debug type
|
||||
byteBuf.writeBoolean(false); // flat
|
||||
byteBuf.writeBoolean(false); // no last death location
|
||||
writeVarInt(byteBuf, 0); // pearl cooldown
|
||||
byteBuf.writeBoolean(false); // secure profile
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+4
-4
@@ -28,8 +28,8 @@ import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_12_2;
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_8;
|
||||
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.readVarInt;
|
||||
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.writeVarInt;
|
||||
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.readVarInt;
|
||||
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.writeVarInt;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@@ -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) {
|
||||
|
||||
+10
-7
@@ -21,19 +21,17 @@ import io.netty.buffer.ByteBuf;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.ToString;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
|
||||
|
||||
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.writeVarInt;
|
||||
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.writeVarInt;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public final class MapDataPacket implements FallbackPacket {
|
||||
private byte[] buffer;
|
||||
private int[] buffer;
|
||||
private int x, y;
|
||||
|
||||
@Override
|
||||
@@ -45,8 +43,7 @@ public final class MapDataPacket implements FallbackPacket {
|
||||
byteBuf.writeByte(0); // scaling
|
||||
byteBuf.writeByte(x);
|
||||
byteBuf.writeByte(y);
|
||||
|
||||
byteBuf.writeBytes(buffer);
|
||||
writeArray(byteBuf, buffer);
|
||||
} else {
|
||||
byteBuf.writeByte(0); // scaling
|
||||
|
||||
@@ -71,7 +68,13 @@ public final class MapDataPacket implements FallbackPacket {
|
||||
byteBuf.writeByte(y);
|
||||
|
||||
writeVarInt(byteBuf, buffer.length);
|
||||
byteBuf.writeBytes(buffer);
|
||||
writeArray(byteBuf, buffer);
|
||||
}
|
||||
}
|
||||
|
||||
private static void writeArray(final ByteBuf byteBuf, final int @NotNull [] array) {
|
||||
for (final int i : array) {
|
||||
byteBuf.writeByte(i);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public final class PaddleBoatPacket implements FallbackPacket {
|
||||
private boolean leftPaddle, rightPaddle;
|
||||
|
||||
@Override
|
||||
public void encode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) throws Exception {
|
||||
leftPaddle = byteBuf.readBoolean();
|
||||
rightPaddle = byteBuf.readBoolean();
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -30,19 +30,19 @@ import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
|
||||
@ToString
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class ClientAbilitiesPacket implements FallbackPacket {
|
||||
public final class PlayerAbilitiesPacket implements FallbackPacket {
|
||||
private int encodedFlags;
|
||||
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);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decode(final @NotNull ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
|
||||
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
||||
+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
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -30,7 +30,7 @@ import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
|
||||
@ToString
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class PlayerInputPacket implements FallbackPacket {
|
||||
public final class PlayerInputPacket implements FallbackPacket {
|
||||
private float sideways, forward;
|
||||
private boolean jump, unmount;
|
||||
|
||||
@@ -40,7 +40,7 @@ public 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) {
|
||||
|
||||
+20
-11
@@ -18,21 +18,24 @@
|
||||
package xyz.jonesdev.sonar.common.fallback.protocol.packets.play;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.handler.codec.CorruptedFrameException;
|
||||
import lombok.Getter;
|
||||
import lombok.ToString;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
|
||||
|
||||
import 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.utility.protocol.ProtocolUtil.*;
|
||||
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.readExtendedForgeShort;
|
||||
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.readString;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
public final class PluginMessagePacket implements FallbackPacket {
|
||||
private String channel;
|
||||
private ByteBuf slicedBuffer;
|
||||
private byte[] data;
|
||||
|
||||
private static final int FORGE_MAX_ARRAY_LENGTH = Integer.MAX_VALUE & 0x1FFF9A;
|
||||
|
||||
@Override
|
||||
public void encode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
|
||||
@@ -40,18 +43,24 @@ 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, 48);
|
||||
|
||||
final int length;
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_8) >= 0) {
|
||||
slicedBuffer = byteBuf.readRetainedSlice(byteBuf.readableBytes());
|
||||
length = byteBuf.readableBytes();
|
||||
if (length > Short.MAX_VALUE) {
|
||||
throw new CorruptedFrameException("Got too much data: " + length);
|
||||
}
|
||||
} else {
|
||||
slicedBuffer = readRetainedByteBufSlice17(byteBuf);
|
||||
length = readExtendedForgeShort(byteBuf);
|
||||
if (length > FORGE_MAX_ARRAY_LENGTH) {
|
||||
throw new CorruptedFrameException("Got too much data: " + length);
|
||||
}
|
||||
}
|
||||
|
||||
data = new byte[length];
|
||||
byteBuf.readBytes(data);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+2
-2
@@ -26,7 +26,7 @@ import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
|
||||
|
||||
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.writeVarInt;
|
||||
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.writeVarInt;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@@ -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
|
||||
|
||||
+28
-18
@@ -29,20 +29,14 @@ 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.utility.protocol.ProtocolUtil.writeCompoundTag;
|
||||
import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.writeNamelessCompoundTag;
|
||||
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.writeVarInt;
|
||||
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.*;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public final class SetSlotPacket implements FallbackPacket {
|
||||
private int windowId;
|
||||
private int slot;
|
||||
private int count;
|
||||
private int data;
|
||||
private int itemId;
|
||||
public final class SetContainerSlotPacket implements FallbackPacket {
|
||||
private int slot, count, itemId;
|
||||
private CompoundBinaryTag compoundBinaryTag;
|
||||
|
||||
public static final CompoundBinaryTag MAP_NBT = CompoundBinaryTag.builder()
|
||||
@@ -50,8 +44,8 @@ public final class SetSlotPacket implements FallbackPacket {
|
||||
.build();
|
||||
|
||||
@Override
|
||||
public void encode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
|
||||
byteBuf.writeByte(windowId);
|
||||
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) {
|
||||
writeVarInt(byteBuf, 0);
|
||||
@@ -59,31 +53,47 @@ public final class SetSlotPacket implements FallbackPacket {
|
||||
|
||||
byteBuf.writeShort(slot);
|
||||
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_13_2) >= 0) {
|
||||
if (protocolVersion.inBetween(MINECRAFT_1_13_2, MINECRAFT_1_20_3)) {
|
||||
byteBuf.writeBoolean(true);
|
||||
}
|
||||
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_20_5) >= 0) {
|
||||
writeVarInt(byteBuf, count);
|
||||
}
|
||||
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_13_2) < 0) {
|
||||
byteBuf.writeShort(itemId);
|
||||
} else {
|
||||
writeVarInt(byteBuf, itemId);
|
||||
}
|
||||
byteBuf.writeByte(count);
|
||||
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_20_5) < 0) {
|
||||
byteBuf.writeByte(count);
|
||||
}
|
||||
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_13) < 0) {
|
||||
byteBuf.writeShort(data);
|
||||
byteBuf.writeShort(0); // data
|
||||
}
|
||||
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_17) < 0) {
|
||||
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_8) < 0) {
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_8) < 0) {
|
||||
byteBuf.writeShort(-1);
|
||||
} else {
|
||||
byteBuf.writeByte(0);
|
||||
}
|
||||
} else {
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_20_2) >= 0) {
|
||||
writeNamelessCompoundTag(byteBuf, compoundBinaryTag);
|
||||
} else {
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_20_2) < 0) {
|
||||
writeCompoundTag(byteBuf, compoundBinaryTag);
|
||||
} else if (protocolVersion.compareTo(MINECRAFT_1_20_5) < 0) {
|
||||
writeNamelessCompoundTag(byteBuf, compoundBinaryTag);
|
||||
} else { // 1.20.5
|
||||
// TODO: find a way to improve this
|
||||
// component
|
||||
writeVarInt(byteBuf, 1); // component count to add
|
||||
writeVarInt(byteBuf, 0); // component count to remove
|
||||
// single VarInt component
|
||||
writeVarInt(byteBuf, 26); // map component
|
||||
writeVarInt(byteBuf, 0); // map id
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -33,11 +33,11 @@ import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT
|
||||
@ToString
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public final class DefaultSpawnPositionPacket implements FallbackPacket {
|
||||
public final class SetDefaultSpawnPositionPacket 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);
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* 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 static xyz.jonesdev.sonar.common.util.ProtocolUtil.writeVarInt;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public final class SetExperiencePacket implements FallbackPacket {
|
||||
private float experienceBar;
|
||||
private int level;
|
||||
private int totalExperience;
|
||||
|
||||
@Override
|
||||
public void encode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) throws Exception {
|
||||
byteBuf.writeFloat(experienceBar);
|
||||
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_8) < 0) {
|
||||
byteBuf.writeShort(level);
|
||||
byteBuf.writeShort(totalExperience);
|
||||
} else {
|
||||
writeVarInt(byteBuf, level);
|
||||
writeVarInt(byteBuf, totalExperience);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
||||
+11
-9
@@ -15,29 +15,31 @@
|
||||
* 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.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 xyz.jonesdev.sonar.common.fallback.protocol.dimension.DimensionRegistry;
|
||||
|
||||
import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.writeNamelessCompoundTag;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
public final class RegistryDataPacket implements FallbackPacket {
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public final class SetHeldItemPacket implements FallbackPacket {
|
||||
private int slot;
|
||||
|
||||
@Override
|
||||
public void encode(final @NotNull ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
|
||||
writeNamelessCompoundTag(byteBuf, DimensionRegistry.CODEC_1_20);
|
||||
public void encode(final @NotNull ByteBuf byteBuf, final ProtocolVersion protocolVersion) throws Exception {
|
||||
byteBuf.writeByte(slot);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
|
||||
throw new UnsupportedOperationException();
|
||||
public void decode(final @NotNull ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
|
||||
slot = byteBuf.readShort();
|
||||
}
|
||||
}
|
||||
+3
-4
@@ -26,18 +26,17 @@ 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.common.utility.protocol.VarIntUtil.writeVarInt;
|
||||
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.writeVarInt;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public final class SetPassengersPacket implements FallbackPacket {
|
||||
private int entityId;
|
||||
private int passengerId;
|
||||
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) {
|
||||
|
||||
+2
-2
@@ -30,7 +30,7 @@ import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
|
||||
@ToString
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public final class PlayerTickPacket implements FallbackPacket {
|
||||
public final class SetPlayerOnGround implements FallbackPacket {
|
||||
private boolean onGround;
|
||||
|
||||
@Override
|
||||
@@ -39,7 +39,7 @@ public final class PlayerTickPacket 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();
|
||||
}
|
||||
}
|
||||
+7
-5
@@ -26,11 +26,13 @@ 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
|
||||
@AllArgsConstructor
|
||||
public final class PlayerPositionPacket implements FallbackPacket {
|
||||
public final class SetPlayerPositionPacket implements FallbackPacket {
|
||||
private double x, y, z;
|
||||
private boolean onGround;
|
||||
|
||||
@@ -40,11 +42,11 @@ 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) {
|
||||
y = byteBuf.readDouble(); // Account for weird y positions on 1.7
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_8) < 0) {
|
||||
y = byteBuf.readDouble(); // Account 1.7 bounding box
|
||||
}
|
||||
z = byteBuf.readDouble();
|
||||
onGround = byteBuf.readBoolean();
|
||||
@@ -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
|
||||
+6
-6
@@ -27,13 +27,13 @@ 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.utility.protocol.VarIntUtil.writeVarInt;
|
||||
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.writeVarInt;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public final class PlayerPositionLookPacket implements FallbackPacket {
|
||||
public final class SetPlayerPositionRotation implements FallbackPacket {
|
||||
private double x, y, z;
|
||||
private float yaw, pitch;
|
||||
private int teleportId;
|
||||
@@ -64,11 +64,11 @@ 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) {
|
||||
y = byteBuf.readDouble(); // Account for weird y positions on 1.7
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_8) < 0) {
|
||||
y = byteBuf.readDouble(); // Account 1.7 bounding box
|
||||
}
|
||||
z = byteBuf.readDouble();
|
||||
yaw = byteBuf.readFloat();
|
||||
@@ -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
|
||||
+6
-5
@@ -26,10 +26,11 @@ import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
|
||||
import static xyz.jonesdev.sonar.common.fallback.netty.FastUUID.EMPTY_UUID;
|
||||
import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.writeUUID;
|
||||
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.writeVarInt;
|
||||
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.writeUUID;
|
||||
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.writeVarInt;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@@ -40,13 +41,13 @@ 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;
|
||||
|
||||
if (v1_9orHigher) {
|
||||
writeUUID(byteBuf, EMPTY_UUID);
|
||||
writeUUID(byteBuf, UUID.randomUUID());
|
||||
}
|
||||
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_14) >= 0) {
|
||||
|
||||
+25
-49
@@ -22,98 +22,76 @@ import io.netty.handler.codec.CorruptedFrameException;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
|
||||
import xyz.jonesdev.sonar.common.utility.component.ComponentHolder;
|
||||
import xyz.jonesdev.sonar.common.util.ComponentHolder;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.UUID;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
|
||||
import static xyz.jonesdev.sonar.common.fallback.netty.FastUUID.EMPTY_UUID;
|
||||
import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.*;
|
||||
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.readVarInt;
|
||||
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.writeVarInt;
|
||||
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.*;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public final class UniversalChatPacket implements FallbackPacket {
|
||||
public final class SystemChatPacket implements FallbackPacket {
|
||||
private static final int DIV_FLOOR = -Math.floorDiv(-20, 8);
|
||||
|
||||
public static final byte CHAT_TYPE = (byte) 0;
|
||||
public static final byte SYSTEM_TYPE = (byte) 1;
|
||||
public static final byte GAME_INFO_TYPE = (byte) 2;
|
||||
|
||||
private ComponentHolder componentHolder;
|
||||
private String message;
|
||||
private byte type;
|
||||
private boolean signedPreview;
|
||||
private boolean unsigned = false;
|
||||
private Instant timestamp;
|
||||
private Instant expiry;
|
||||
private long salt;
|
||||
private boolean signed;
|
||||
private byte[] signature;
|
||||
|
||||
public UniversalChatPacket(final @NotNull Component component,
|
||||
final byte type) {
|
||||
this(new ComponentHolder(component), type);
|
||||
}
|
||||
|
||||
public UniversalChatPacket(final ComponentHolder componentHolder, final byte type) {
|
||||
public SystemChatPacket(final @NotNull ComponentHolder componentHolder) {
|
||||
this.componentHolder = componentHolder;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
@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);
|
||||
|
||||
// Type
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_19_1) >= 0) {
|
||||
byteBuf.writeBoolean(type == GAME_INFO_TYPE);
|
||||
byteBuf.writeBoolean(false); // it's not the GAME_INFO type
|
||||
} else if (protocolVersion.compareTo(MINECRAFT_1_19) >= 0) {
|
||||
writeVarInt(byteBuf, type);
|
||||
writeVarInt(byteBuf, 1); // system chat
|
||||
} else if (protocolVersion.compareTo(MINECRAFT_1_8) >= 0) {
|
||||
byteBuf.writeByte(type);
|
||||
byteBuf.writeByte(1); // system chat
|
||||
}
|
||||
|
||||
// Sender
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_16) >= 0
|
||||
&& protocolVersion.compareTo(MINECRAFT_1_19) < 0) {
|
||||
writeUUID(byteBuf, EMPTY_UUID);
|
||||
writeUUID(byteBuf, UUID.randomUUID());
|
||||
}
|
||||
}
|
||||
|
||||
@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(ProtocolVersion.MINECRAFT_1_19) >= 0) {
|
||||
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_19_1) <= 0) {
|
||||
final long expiresAt = byteBuf.readLong();
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_19) >= 0) {
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_19_1) <= 0) {
|
||||
byteBuf.readLong(); // expiresAt
|
||||
final long saltLong = byteBuf.readLong();
|
||||
final byte[] signatureBytes = readByteArray(byteBuf);
|
||||
boolean unsigned = false;
|
||||
|
||||
if (saltLong != 0L && signatureBytes.length > 0) {
|
||||
signature = signatureBytes;
|
||||
expiry = Instant.ofEpochMilli(expiresAt);
|
||||
} else if ((protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_19_1) >= 0
|
||||
// No need to store the valid signature
|
||||
} else if ((protocolVersion.compareTo(MINECRAFT_1_19_1) >= 0
|
||||
|| saltLong == 0L) && signatureBytes.length == 0) {
|
||||
unsigned = true;
|
||||
} else {
|
||||
throw new CorruptedFrameException("Invalid signature");
|
||||
}
|
||||
|
||||
signedPreview = byteBuf.readBoolean();
|
||||
final boolean signedPreview = byteBuf.readBoolean();
|
||||
if (signedPreview && unsigned) {
|
||||
throw new CorruptedFrameException("Signature missing");
|
||||
}
|
||||
|
||||
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_19_1) >= 0) {
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_19_1) >= 0) {
|
||||
final int size = readVarInt(byteBuf);
|
||||
if (size < 0 || size > 5) {
|
||||
throw new CorruptedFrameException("Invalid previous messages");
|
||||
@@ -130,19 +108,17 @@ public final class UniversalChatPacket implements FallbackPacket {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
timestamp = Instant.ofEpochMilli(byteBuf.readLong());
|
||||
salt = byteBuf.readLong();
|
||||
signed = byteBuf.readBoolean();
|
||||
byteBuf.readLong(); // timestamp
|
||||
byteBuf.readLong(); // salt
|
||||
final boolean signed = byteBuf.readBoolean();
|
||||
|
||||
if (signed) {
|
||||
byte[] sign = new byte[256];
|
||||
final byte[] sign = new byte[256];
|
||||
byteBuf.readBytes(sign);
|
||||
signature = sign;
|
||||
} else {
|
||||
signature = new byte[0];
|
||||
}
|
||||
|
||||
readVarInt(byteBuf);
|
||||
byte[] bytes = new byte[DIV_FLOOR];
|
||||
final byte[] bytes = new byte[DIV_FLOOR];
|
||||
byteBuf.readBytes(bytes);
|
||||
}
|
||||
}
|
||||
+7
-9
@@ -33,17 +33,17 @@ import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public final class TransactionPacket implements FallbackPacket {
|
||||
private int windowId, id;
|
||||
private int windowId, transactionId;
|
||||
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();
|
||||
id = byteBuf.readShort();
|
||||
transactionId = byteBuf.readShort();
|
||||
accepted = byteBuf.readBoolean();
|
||||
} else {
|
||||
id = byteBuf.readInt();
|
||||
transactionId = byteBuf.readInt();
|
||||
// Always set accepted to true since 1.17 or higher don't use
|
||||
// transactions for inventory confirmation anymore.
|
||||
accepted = true;
|
||||
@@ -51,15 +51,13 @@ 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) id);
|
||||
// The "accepted" field is actually really unnecessary since
|
||||
// it's never even used in the client.
|
||||
byteBuf.writeShort((short) transactionId);
|
||||
byteBuf.writeBoolean(accepted);
|
||||
} else {
|
||||
byteBuf.writeInt(id);
|
||||
byteBuf.writeInt(transactionId);
|
||||
}
|
||||
}
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Copyright (C) 2024 jones
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.common.fallback.protocol.packets.play;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.ToString;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
|
||||
|
||||
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.writeString;
|
||||
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.writeVarInt;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public final class TransferPacket implements FallbackPacket {
|
||||
private String destinationHost;
|
||||
private int destinationPort;
|
||||
|
||||
@Override
|
||||
public void encode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) throws Exception {
|
||||
writeString(byteBuf, destinationHost);
|
||||
writeVarInt(byteBuf, destinationPort);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
||||
+12
-12
@@ -25,19 +25,19 @@ import lombok.ToString;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.block.ChangedBlock;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.block.BlockUpdate;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
|
||||
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.writeVarInt;
|
||||
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.writeVarLong;
|
||||
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.writeVarInt;
|
||||
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.writeVarLong;
|
||||
|
||||
@Getter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@ToString(exclude = {"changedBlocks"})
|
||||
@ToString(exclude = {"blockUpdates"})
|
||||
public final class UpdateSectionBlocksPacket implements FallbackPacket {
|
||||
private int sectionX, sectionZ;
|
||||
private ChangedBlock[] changedBlocks;
|
||||
private BlockUpdate[] blockUpdates;
|
||||
|
||||
@Override
|
||||
public void encode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) throws Exception {
|
||||
@@ -46,13 +46,13 @@ public final class UpdateSectionBlocksPacket implements FallbackPacket {
|
||||
byteBuf.writeInt(sectionZ);
|
||||
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_8) < 0) {
|
||||
byteBuf.writeShort(changedBlocks.length);
|
||||
byteBuf.writeInt(4 * changedBlocks.length);
|
||||
byteBuf.writeShort(blockUpdates.length);
|
||||
byteBuf.writeInt(4 * blockUpdates.length);
|
||||
} else {
|
||||
writeVarInt(byteBuf, changedBlocks.length);
|
||||
writeVarInt(byteBuf, blockUpdates.length);
|
||||
}
|
||||
|
||||
for (final ChangedBlock block : changedBlocks) {
|
||||
for (final BlockUpdate block : blockUpdates) {
|
||||
byteBuf.writeShort(block.getLegacyBlockState());
|
||||
final int blockId = block.getType().getId(protocolVersion);
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_13) >= 0) {
|
||||
@@ -68,7 +68,7 @@ public final class UpdateSectionBlocksPacket implements FallbackPacket {
|
||||
}
|
||||
} else {
|
||||
// We only need one Y position
|
||||
final int sectionY = changedBlocks[0].getPosition().getY() >> 4;
|
||||
final int sectionY = blockUpdates[0].getPosition().getY() >> 4;
|
||||
|
||||
// https://wiki.vg/Protocol#Update_Section_Blocks
|
||||
byteBuf.writeLong(((sectionX & 0x3FFFFFL) << 42) | (sectionY & 0xFFFFF) | ((sectionZ & 0x3FFFFFL) << 20));
|
||||
@@ -78,9 +78,9 @@ public final class UpdateSectionBlocksPacket implements FallbackPacket {
|
||||
byteBuf.writeBoolean(true); // suppress light updates
|
||||
}
|
||||
|
||||
writeVarInt(byteBuf, changedBlocks.length);
|
||||
writeVarInt(byteBuf, blockUpdates.length);
|
||||
|
||||
for (final ChangedBlock block : changedBlocks) {
|
||||
for (final BlockUpdate block : blockUpdates) {
|
||||
// https://wiki.vg/Protocol#Update_Section_Blocks
|
||||
final int shiftedBlockId = block.getType().getId(protocolVersion) << 12;
|
||||
final long positionIdValue = shiftedBlockId | block.getBlockState();
|
||||
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
* 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 SetContainerSlotPacket} and {@link MapDataPacket} packets are sent to the client,
|
||||
* therefore, setting the player's item to a map with a code on it (CAPTCHA).
|
||||
* <br>
|
||||
* See more: {@link FallbackCAPTCHASessionHandler}, {@link MapCaptchaInfo}
|
||||
* </li>
|
||||
* <li>
|
||||
* Then, we wait for the player to enter the {@link FallbackCAPTCHASessionHandler#answer} in chat.
|
||||
* </li>
|
||||
*/
|
||||
public final class FallbackCAPTCHASessionHandler extends FallbackSessionHandler {
|
||||
|
||||
public FallbackCAPTCHASessionHandler(final FallbackUser user, final String username, final UUID uuid) {
|
||||
super(user, username, uuid);
|
||||
|
||||
this.tries = Sonar.get().getConfig().getVerification().getMap().getMaxTries();
|
||||
|
||||
// If the player is on Java, set the 5th slot (ID 4) in the player's hotbar to the map.
|
||||
// If the player is on Bedrock, set the 1st slot (ID 0) in the player's hotbar to the map.
|
||||
user.delayedWrite(new SetContainerSlotPacket(user.isGeyser() ? 36 : 40, 1,
|
||||
ItemType.FILLED_MAP.getId(user.getProtocolVersion()), SetContainerSlotPacket.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 SystemChatPacket) {
|
||||
final SystemChatPacket chat = (SystemChatPacket) 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 SetPlayerPositionPacket
|
||||
|| packet instanceof SetPlayerPositionRotation) {
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+238
@@ -0,0 +1,238 @@
|
||||
/*
|
||||
* 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 @NotNull FallbackUser user,
|
||||
final @NotNull String username,
|
||||
final @NotNull 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 double y, deltaY;
|
||||
private int movementTick;
|
||||
|
||||
private void markTeleported() {
|
||||
// Activate the movement checks
|
||||
teleported = true;
|
||||
}
|
||||
|
||||
private void markSuccess(final boolean forceCAPTCHA) {
|
||||
// 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");
|
||||
}
|
||||
|
||||
// Send the player to the protocol check
|
||||
final var decoder = (FallbackPacketDecoder) user.getPipeline().get(FallbackPacketDecoder.class);
|
||||
decoder.setListener(new FallbackProtocolSessionHandler(user, username, uuid, forceCAPTCHA));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(final @NotNull FallbackPacket packet) {
|
||||
if (packet instanceof SetPlayerPositionRotation) {
|
||||
// Make sure the player has teleported before checking for position packets
|
||||
if (teleported) {
|
||||
final SetPlayerPositionRotation positionLook = (SetPlayerPositionRotation) packet;
|
||||
handleMovement(positionLook.getX(), positionLook.getY(), positionLook.getZ(), positionLook.isOnGround());
|
||||
}
|
||||
} else if (packet instanceof SetPlayerPositionPacket) {
|
||||
// Make sure the player has teleported before checking for position packets
|
||||
if (teleported) {
|
||||
final SetPlayerPositionPacket position = (SetPlayerPositionPacket) packet;
|
||||
handleMovement(position.getX(), position.getY(), position.getZ(), position.isOnGround());
|
||||
}
|
||||
} else if (packet instanceof ConfirmTeleportationPacket) {
|
||||
final ConfirmTeleportationPacket teleportConfirm = (ConfirmTeleportationPacket) 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 ClientInformationPacket) {
|
||||
// Let the session handler itself know about this packet
|
||||
checkClientInformation((ClientInformationPacket) packet);
|
||||
} else if (packet instanceof PluginMessagePacket) {
|
||||
// Let the session handler itself know about this packet
|
||||
checkPluginMessage((PluginMessagePacket) packet);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleMovement(final double x, final double y, final double z, final boolean isOnGround) {
|
||||
// Check if the client hasn't moved before sending the first movement packet
|
||||
if (!checkMovement) {
|
||||
// No need to continue checking if the gravity and collision checks are disabled
|
||||
// Continue with the next stage of the verification
|
||||
if (!enableGravityCheck && !enableCollisionsCheck) {
|
||||
markSuccess(false);
|
||||
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.y = y;
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
// The movement tick should not be ignored when gravity check is disabled.
|
||||
movementTick++;
|
||||
|
||||
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()) {
|
||||
markSuccess(true);
|
||||
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) {
|
||||
markSuccess(false);
|
||||
}
|
||||
}
|
||||
} else if (enableCollisionsCheck) {
|
||||
// Make sure the player has actually moved before reaching the platform
|
||||
if (movementTick < maxMovementTick) {
|
||||
// Do not throw an exception if the user configured to display the CAPTCHA instead
|
||||
if (Sonar.get().getConfig().getVerification().getGravity().isCaptchaOnFail()) {
|
||||
markSuccess(true);
|
||||
return;
|
||||
}
|
||||
user.fail("illegal collision tick: " + movementTick);
|
||||
}
|
||||
// Calculate the difference between the player's Y coordinate and the expected Y coordinate
|
||||
double collisionOffsetY = (DEFAULT_Y_COLLIDE_POSITION + blockType.getBlockHeight()) - y;
|
||||
// 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
|
||||
markSuccess(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
+173
@@ -0,0 +1,173 @@
|
||||
/*
|
||||
* 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.ClientInformationPacket;
|
||||
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 @NotNull FallbackUser user,
|
||||
final @NotNull String username,
|
||||
final @NotNull 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 ClientInformationPacket) {
|
||||
// Let the session handler itself know about this packet
|
||||
checkClientInformation((ClientInformationPacket) packet);
|
||||
} else if (packet instanceof PluginMessagePacket) {
|
||||
// Let the session handler itself know about this packet
|
||||
checkPluginMessage((PluginMessagePacket) packet);
|
||||
}
|
||||
}
|
||||
|
||||
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 : user.getProtocolVersion().compareTo(MINECRAFT_1_21) < 0
|
||||
? REGISTRY_SYNC_1_20_5 : REGISTRY_SYNC_1_21) {
|
||||
user.delayedWrite(syncPacket);
|
||||
}
|
||||
} else {
|
||||
// Write the old RegistrySync packet to the buffer
|
||||
user.delayedWrite(REGISTRY_SYNC_LEGACY);
|
||||
}
|
||||
}
|
||||
}
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
/*
|
||||
* 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.SetHeldItemPacket;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.TransactionPacket;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public final class FallbackProtocolSessionHandler extends FallbackSessionHandler {
|
||||
|
||||
public FallbackProtocolSessionHandler(final @NotNull FallbackUser user,
|
||||
final @NotNull String username,
|
||||
final @NotNull UUID uuid,
|
||||
final boolean forceCAPTCHA) {
|
||||
super(user, username, uuid);
|
||||
|
||||
this.forceCAPTCHA = forceCAPTCHA;
|
||||
|
||||
// Immediately send the player the transaction packet
|
||||
sendTransaction();
|
||||
}
|
||||
|
||||
private final boolean forceCAPTCHA;
|
||||
private short expectedTransactionId;
|
||||
private int currentClientSlotId, expectedSlotId = -1;
|
||||
|
||||
/**
|
||||
* Uses Transaction (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() {
|
||||
// Send a Transaction (Ping) packet with a random ID
|
||||
expectedTransactionId = (short) -(RANDOM.nextInt(Short.MAX_VALUE));
|
||||
user.write(new TransactionPacket(0, expectedTransactionId, false));
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses SetHeldItem packets to check for a legitimate response from the client
|
||||
* <br>
|
||||
* <a href="https://wiki.vg/Protocol#Set_Held_Item_.28serverbound.29">Wiki.vg - SetHeldItem (play)</a>
|
||||
*/
|
||||
private void sendSetHeldItem() {
|
||||
// Move the player's slot by 4 (slot limit divided by 2),
|
||||
// and then modulo it by the slot limit (8) to ensure that we don't send invalid slot IDs.
|
||||
expectedSlotId = (currentClientSlotId + 4) % 8;
|
||||
// Send two SetHeldItem packets with the same slot to check if the player responds with the correct slot.
|
||||
// By vanilla protocol, the client does not respond to duplicate SetHeldItem packets.
|
||||
// We can take advantage of this by sending two packets with the same content to check for a valid response.
|
||||
final SetHeldItemPacket heldItemPacket = new SetHeldItemPacket(expectedSlotId);
|
||||
user.delayedWrite(heldItemPacket);
|
||||
user.delayedWrite(heldItemPacket);
|
||||
user.getChannel().flush();
|
||||
}
|
||||
|
||||
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, forceCAPTCHA));
|
||||
} else if (forceCAPTCHA || 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);
|
||||
|
||||
// Only perform the SetHeldItem check on Java players since the Bedrock protocol breaks this check.
|
||||
// I don't really know why this doesn't work on Bedrock,
|
||||
// but I think it has to do with how Geyser/floodgate translates this packet:
|
||||
// https://wiki.vg/Bedrock_Protocol#Player_Hotbar
|
||||
if (user.isGeyser()) {
|
||||
markSuccess();
|
||||
} else {
|
||||
sendSetHeldItem();
|
||||
}
|
||||
} else if (packet instanceof SetHeldItemPacket) {
|
||||
final SetHeldItemPacket heldItemPacket = (SetHeldItemPacket) packet;
|
||||
|
||||
final int slotId = heldItemPacket.getSlot();
|
||||
// Also check if the player sent an invalid slot which is impossible by vanilla protocol
|
||||
checkState(slotId >= 0 && slotId <= 8, "slot out of range: " + slotId);
|
||||
// Check if the player sent a duplicate slot packet which is impossible by vanilla protocol
|
||||
checkState(slotId != currentClientSlotId, "invalid slot: " + slotId);
|
||||
|
||||
// Only continue checking if we're actually expecting a SetHeldItem packet
|
||||
// The player can send a SetHeldItem packet by themselves -> exempt
|
||||
if (expectedSlotId != -1
|
||||
// Check if the slot ID matches the expected slot ID
|
||||
// This can false flag if a player spams these packets, which is why we don't fail for this
|
||||
&& slotId == expectedSlotId) {
|
||||
markSuccess();
|
||||
}
|
||||
|
||||
currentClientSlotId = slotId;
|
||||
}
|
||||
}
|
||||
}
|
||||
+148
@@ -0,0 +1,148 @@
|
||||
/*
|
||||
* 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 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.ClientInformationPacket;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.PluginMessagePacket;
|
||||
import xyz.jonesdev.sonar.common.statistics.GlobalSonarStatistics;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Random;
|
||||
import java.util.UUID;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
|
||||
import static xyz.jonesdev.sonar.common.fallback.FallbackUserWrapper.closeWith;
|
||||
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer.transferToOrigin;
|
||||
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.BRAND_CHANNEL;
|
||||
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.BRAND_CHANNEL_LEGACY;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public abstract class FallbackSessionHandler implements FallbackPacketListener {
|
||||
protected final FallbackUser user;
|
||||
protected final String username;
|
||||
protected 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
|
||||
Sonar.get().getVerifiedPlayerController().add(new VerifiedPlayer(
|
||||
user.getInetAddress(), uuid, user.getLoginTimer().getStart()));
|
||||
|
||||
// Call the VerifySuccessEvent for external API usage
|
||||
Sonar.get().getEventManager().publish(new UserVerifySuccessEvent(
|
||||
username, uuid, user, user.getLoginTimer().delay()));
|
||||
|
||||
// If enabled, transfer the player back to the origin server.
|
||||
// This feature was introduced by Mojang in Minecraft version 1.20.5.
|
||||
if (transferToOrigin != null
|
||||
&& user.getProtocolVersion().compareTo(MINECRAFT_1_20_5) >= 0) {
|
||||
// Send the transfer packet to the player and close the channel
|
||||
closeWith(user.getChannel(), user.getProtocolVersion(), transferToOrigin);
|
||||
} else {
|
||||
// Disconnect player with the verification success message
|
||||
user.disconnect(Sonar.get().getConfig().getVerification().getVerificationSuccess());
|
||||
}
|
||||
|
||||
Sonar.get().getFallback().getLogger().info(
|
||||
Sonar.get().getConfig().getMessagesConfig().getString("verification.logs.successful")
|
||||
.replace("<username>", username)
|
||||
.replace("<time-taken>", user.getLoginTimer().toString()));
|
||||
}
|
||||
|
||||
protected final void checkClientInformation(final @NotNull ClientInformationPacket clientSettings) {
|
||||
// Ensure that the client locale is correct
|
||||
validateClientLocale(clientSettings.getLocale());
|
||||
// Check if the player sent an unused bit flag in the skin section
|
||||
checkState((clientSettings.getSkinParts() & 0x80) == 0, "sent unused bit flag");
|
||||
|
||||
// Mark the ClientSettings packet as received
|
||||
user.setReceivedClientSettings(true);
|
||||
}
|
||||
|
||||
protected final void checkPluginMessage(final @NotNull PluginMessagePacket pluginMessage) {
|
||||
final boolean usingModernChannel = pluginMessage.getChannel().equals(BRAND_CHANNEL);
|
||||
final boolean usingLegacyChannel = pluginMessage.getChannel().equals(BRAND_CHANNEL_LEGACY);
|
||||
|
||||
// Skip this payload if it does not contain client brand information
|
||||
if (!usingModernChannel && !usingLegacyChannel) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Make sure the player isn't sending the client brand multiple times
|
||||
checkState(!user.isReceivedPluginMessage(), "sent duplicate plugin message");
|
||||
// Check if the channel is correct - 1.13 uses the new namespace
|
||||
// system ('minecraft:' + channel) and anything below 1.13 uses
|
||||
// the legacy namespace system ('MC|' + channel).
|
||||
final boolean v1_13 = user.getProtocolVersion().compareTo(MINECRAFT_1_13) >= 0;
|
||||
checkState(usingLegacyChannel || v1_13, "invalid brand channel");
|
||||
|
||||
// Validate the client branding using a regex to filter unwanted characters.
|
||||
if (Sonar.get().getConfig().getVerification().getBrand().isEnabled()) {
|
||||
validateClientBrand(pluginMessage.getData());
|
||||
}
|
||||
|
||||
// Mark the PluginMessage packet as received
|
||||
user.setReceivedPluginMessage(true);
|
||||
}
|
||||
|
||||
protected final void validateClientBrand(final byte @NotNull [] data) {
|
||||
// No need to check for empty brands since ProtocolUtil#readBrandMessage
|
||||
// already performs invalid string checks by default.
|
||||
String read = new String(data, StandardCharsets.UTF_8);
|
||||
checkState(read.length() > 1, "client brand is too short");
|
||||
// Remove the invalid character at the beginning of the client brand
|
||||
if (user.getProtocolVersion().compareTo(MINECRAFT_1_8) >= 0) {
|
||||
read = read.substring(1);
|
||||
}
|
||||
// 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");
|
||||
}
|
||||
}
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
* 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 boolean forceCAPTCHA) {
|
||||
super(user, username, uuid);
|
||||
|
||||
this.forceCAPTCHA = forceCAPTCHA;
|
||||
|
||||
// Send the necessary packets to mount the player on the vehicle
|
||||
final int entityTypeId = EntityType.BOAT.getId(user.getProtocolVersion());
|
||||
user.delayedWrite(new SpawnEntityPacket(VEHICLE_ENTITY_ID, entityTypeId,
|
||||
SPAWN_X_POSITION, DEFAULT_Y_COLLIDE_POSITION, SPAWN_Z_POSITION));
|
||||
user.delayedWrite(setPassengers);
|
||||
user.getChannel().flush();
|
||||
}
|
||||
|
||||
private final boolean forceCAPTCHA;
|
||||
private boolean receivedPaddle, receivedInput;
|
||||
|
||||
private void markSuccess() {
|
||||
// Pass the player to the next best verification handler
|
||||
if (forceCAPTCHA || Sonar.get().getFallback().shouldPerformCaptcha()) {
|
||||
// Make sure the player exits the vehicle before sending the CAPTCHA
|
||||
user.delayedWrite(removeEntities);
|
||||
// Either send the player to the CAPTCHA, or finish the verification.
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Loaded 100 of 159 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user