Compare commits

..
50 Commits
Author SHA1 Message Date
Jones 6e78c875a2 Release 2.0.34 (#337) 2024-06-18 19:00:10 +02:00
Michel Elkenwaat 18486b9286 Remove unnecessary UncaughtExceptionHandler 2024-06-18 17:06:24 +02:00
Michel Elkenwaat 644d1387ce Fix notifications breaking scheduled tasks 2024-06-18 16:57:20 +02:00
Jones e80311bf24 Release 2.0.33 (#336)
* build: bump version from 2.0.32 to 2.0.33

* fix: yes, yes, grammar
2024-06-18 14:56:42 +02:00
Akkariin f42dd4d39f Update Chinese translations (#335)
* feat: add and fix some message in `config.yml` and `messages.yml`

* feat: added chinese translate for webhook.
2024-06-18 11:05:28 +02:00
Michel Elkenwaat 7717fb26a0 Make server write timeout configurable 2024-06-18 02:31:48 +02:00
Michel Elkenwaat 97ff082fb4 Add some missing translations 2024-06-18 02:19:36 +02:00
Michel Elkenwaat 9bdb884302 Allow user to disable automatic update checking (https://github.com/jonesdevelopment/sonar/issues/332) 2024-06-18 02:13:16 +02:00
Michel Elkenwaat 19d2c141de Update priorities of checks performed on login 2024-06-18 02:08:50 +02:00
Michel Elkenwaat 5ea02039f2 Use compute for more efficient ConcurrentMap I/Os 2024-06-18 02:03:55 +02:00
Jones 5a5c91b52d Migrate placeholders to MiniMessage components (#329)
* feat: [wip] begin migrating everything to MiniMessage

* feat: [wip] migrate to MiniMessage placeholders

* fix: recode verbose placeholder handling

* fix: throw away packets if not needed

* fix: main command help hovering being broken

* fix(bungee): lag when checking invalid IP addresses

* fix: add missing permission check to sonar command

* fix: add missing prefix placeholder to no permission message

* feat: remove system load average from profiler

* feat: add more debug to missing language error

* fix: make sure it can actually compile B)

* fix: only resolve an IP address if we have to

* fix: remove io lock in /sonar verify
2024-06-18 02:01:11 +02:00
Michel Elkenwaat 128e4aa0ca Fix CAPTCHA being on the wrong slot for Geyser 2024-06-17 22:57:01 +02:00
Michel Elkenwaat 504e5f86ae Add missing information to the README 2024-06-17 17:02:39 +02:00
dependabot[bot] 932849ba22 Bump netty-all from 4.1.110.Final to 4.1.111.Final (#333)
Bumps [io.netty:netty-all](https://github.com/netty/netty) from 4.1.110.Final to 4.1.111.Final.
- [Commits](https://github.com/netty/netty/compare/netty-4.1.110.Final...netty-4.1.111.Final)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-17 16:21:01 +02:00
Jones 6ba8148604 Use ScheduledExecutorService instead of Thread (#328) 2024-06-17 14:33:45 +02:00
Michel Elkenwaat 3fd23cb853 Rewrite some sections in the README 2024-06-16 21:50:54 +02:00
Michel Elkenwaat bed374e0f8 Fix invalid timestamp in database (https://github.com/jonesdevelopment/sonar/issues/331) 2024-06-16 15:31:53 +02:00
AkkariinandMichel Elkenwaat bb3706a949 Movement tick should be incremented even if check disabled (#330)
* fix: Tick movement shouldn't be ignored because disable checks.

* refactor: don't store movementTick in localVar

---------

Co-authored-by: Michel Elkenwaat <admin@jonesdev.xyz>
2024-06-16 10:47:05 +02:00
Michel Elkenwaat 4fa3a0a313 Improve collision check and fix potential bypass 2024-06-15 21:16:37 +02:00
Michel Elkenwaat 62ce6ded18 Fix rare NoSuchElementException 2024-06-15 21:08:42 +02:00
Michel Elkenwaat 7d45b2d0db Extract protocol checks to their own session handler 2024-06-15 20:48:39 +02:00
Michel Elkenwaat b578f813dc Fix map item not being put in the correct slot 2024-06-15 20:46:00 +02:00
Michel Elkenwaat 2e0f1ad8ff Improve clarity by using the official packet names 2024-06-15 20:29:43 +02:00
Michel Elkenwaat dd21a7df58 Bump Gradle from 8.7 to 8.8 2024-06-15 19:56:57 +02:00
Michel Elkenwaat 56d5b47bfe Exempt Geyser players from SetHeldItem check 2024-06-15 12:19:08 +02:00
Akkariin 931cba82ed Single-release hijacked packet on Bungee (#326)
* fix: `PacketWrapper#trySingleRelease` after handleLogin on bungee.

* style: `CHANNEL_WRAPPER_GETTER` assignment [ci skip]
2024-06-15 11:05:35 +02:00
Michel Elkenwaat a1fffe2e74 Take a snapshot of the JoinGame packet 2024-06-14 23:52:30 +02:00
Michel Elkenwaat 263060e517 Hook bandwidth handler after handshake packet 2024-06-14 23:39:49 +02:00
Michel Elkenwaat cffe9c4dd7 Preload all registered packets on start 2024-06-14 23:38:58 +02:00
Michel Elkenwaat d96fcca78e Recode PluginMessage handling to fix memory leak 2024-06-14 23:20:52 +02:00
Michel Elkenwaat 0027f5988b Fix memory leak on BungeeCord (https://github.com/jonesdevelopment/sonar/issues/325) 2024-06-14 22:23:28 +02:00
Michel Elkenwaat d4d3b91240 Fix exception when cancelling login on Bungee 2024-06-14 22:23:15 +02:00
Michel Elkenwaat ebc8ade9e4 Check Geyser players for out-of-range slots 2024-06-15 02:24:11 +08:00
Michel Elkenwaat ef846b137b Simplify initialization of the ProtocolVersion API 2024-06-15 02:23:33 +08:00
Michel Elkenwaat 0107a7e569 Fix 1.20.6 SetHeldItem packet ID 2024-06-15 02:22:08 +08:00
Akkariin 1055f695c6 Remove useless biome codecs (#324)
* chore:  remove useless biome

* feat: merge `worldgen/biome` from the codec_1_20 into codec_1.21
2024-06-14 15:13:55 +02:00
Jones b8bb1839a2 Implement new slot check (#323)
* feat: implement new SetHeldItemPacket check

* fix: the expectedSlotId can be 0 if the client synced before

* fix: register all missing packet ids for the SetHeldItem packet

* fix: [wip] exempt Geyser players from the slot PO check

* fix: implement packet ids for 1.21
2024-06-14 13:59:49 +02:00
AkkariinandMichel Elkenwaat 1d852e4eb2 1.21 Support (#322)
* feat: add 1.21 protocol.

* fix: plugin message packet id from wiki.vg

* chore: add debug for encoder/decoder

* fix: should use `originalPacket` instead of `packet`

* feat: add codec for 1.21

* feat: update codec and encode registry for 1.21

* fix: inline registry packets if-check

* fix: apply 1.20.5 block/item ids for 1.21

* fix: apply 1.20.5 entity id for 1.21

* chore: remove debug message from encoder/decoder

* style: accidental deletion of line breaks in decoder

---------

Co-authored-by: Michel Elkenwaat <admin@jonesdev.xyz>
2024-06-14 13:54:37 +02:00
Michel Elkenwaat 4d35634caa Remove an unused DefaultSpawnPosition packet for the CAPTCHA 2024-06-13 20:19:39 +02:00
Jones 15af0f3823 Release 2.0.32 (#320) 2024-06-12 15:18:11 +02:00
Jones 998a248819 Recode verification system (#319)
* feat: [wip] recode the entire verification

* feat: [wip] add CAPTCHA session handler

* feat: add CAPTCHA xp bar and KeepAlive

* fix(fallback): migrate "connected" map to hashset

* fix(fallback): 1.8 not being able to connect

* fix(fallback): NullPointerException

* fix(fallback): 1.7.2-1.7.10 not being able to verify

* style: fix some doc comments

* style: add doc comment to FallbackVehicleSessionHandler

* style: add doc comment to FallbackCAPTCHASessionHandler

* fix: prevent users from sending multiple login ack packets

* style: rename variable for clarification

* fix: [wip] gravity check not being executed properly

* fix: use absolute value for motion prediction in gravity check

* fix: check for duplicate PluginMessage packets

* fix: oops, I broke the plugin message check

* refactor: restructure verification failing logic

* fix: mark LoginSuccess packet as encode-only

* refactor: store channel pipeline in FallbackUser

* feat: hook the traffic listener in channelActive to allow listening to handshakes

* refactor(common): small maintainability changes

* style(common): code clean up in packet registry

* refactor(all): replace lombok's val with Java's var

* fix: remove unnecessary packet filter in vehicle check

* fix: don't send the disconnect message if the player is transferred

* style: improve/simplify initialization of VerifiedPlayer

* fix: wrong name resulting in compilation error

* chore: fix a broken documentation link in the readme

* fix: don't check Geyser players for plugin messages

* fix: mark 1.7 players successful after 100ms, not 250ms

* fix: skip movements if gravity and collision checks are disabled

* style: don't store Sonar API instance in Fallback

* fix: revert https://github.com/jonesdevelopment/sonar/pull/319/commits/f9c4f42503dfebb6fbba754ff26662e69f7eed00
2024-06-09 12:55:24 +02:00
Jones 3846007613 Bump adventure-platform from 4.3.2 to 4.3.3 (#318) 2024-06-03 00:51:27 +02:00
Jones c5446f9507 Fix several concurrency issues (#317)
* fix: several concurrency issues in Sonar

* fix: recode fallback queue polling and threading

* fix: recode fallback ratelimiter
2024-05-31 15:02:48 +02:00
Jones 5a13b54350 Optimize player data handling (#316)
* fix: improve how fallback stores connected player data

* fix: improve Fallback's custom read timeout handler

* fix: max players per IP check not working as intended
2024-05-31 13:27:24 +02:00
Jones d516da7f9e Recode too many accounts check (#315) 2024-05-31 13:18:06 +02:00
Jones a58b8c8a83 Implement 'verification disabled' warning (#313) 2024-05-29 17:54:54 +02:00
Jones 3be87006aa Add helpful links to issues configuration (#312) 2024-05-28 16:31:43 +02:00
Jones c70c81eb11 Minimize shadowed jar file (#311) 2024-05-28 14:28:21 +02:00
Jones d71334c1b4 Migrate to Gradle version catalog (#310) 2024-05-28 14:10:23 +02:00
dependabot[bot] 59f141d1be Bump netty-all from 4.1.109.Final to 4.1.110.Final (#309)
Bumps [io.netty:netty-all](https://github.com/netty/netty) from 4.1.109.Final to 4.1.110.Final.
- [Commits](https://github.com/netty/netty/compare/netty-4.1.109.Final...netty-4.1.110.Final)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-27 14:08:19 +02:00
115 changed files with 3100 additions and 3180 deletions

No files matched your search

+8
View File
@@ -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
View File
@@ -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/badge.svg)](https://github.com/jonesdevelopment/sonar/actions/workflows/gradle.yml)
[![](https://www.codefactor.io/repository/github/jonesdevelopment/sonar/badge/main)](https://www.codefactor.io/repository/github/jonesdevelopment/sonar/overview/main)
@@ -15,7 +14,6 @@
[![](https://img.shields.io/discord/923308209769426994.svg?logo=discord)](https://jonesdev.xyz/discord)
[![](https://img.shields.io/badge/License-GPLv3-blue.svg)](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).
+19 -26
View File
@@ -1,17 +1,9 @@
buildscript {
dependencies {
classpath("gradle.plugin.io.toolebox:gradle-git-versioner:1.6.7")
}
}
plugins {
id("java")
id("io.github.goooler.shadow") version "8.1.7"
id("io.toolebox.git-versioner") version "1.6.7"
java
alias(libs.plugins.shadow)
alias(libs.plugins.versioner) apply true
}
apply(plugin = "io.toolebox.git-versioner")
versioner {
pattern {
pattern = "$version-%h-%c-%b"
@@ -20,7 +12,7 @@ versioner {
allprojects {
repositories {
mavenCentral() // Lombok
mavenCentral()
maven(url = "https://repo.jonesdev.xyz/releases/") // Bungee & Velocity proxy module
maven(url = "https://s01.oss.sonatype.org/content/repositories/snapshots/") // libby
}
@@ -29,22 +21,18 @@ allprojects {
apply(plugin = "io.github.goooler.shadow")
dependencies {
compileOnly("org.projectlombok:lombok:1.18.32")
annotationProcessor("org.projectlombok:lombok:1.18.32")
compileOnly(rootProject.libs.lombok)
annotationProcessor(rootProject.libs.lombok)
testCompileOnly("org.projectlombok:lombok:1.18.32")
testAnnotationProcessor("org.projectlombok:lombok:1.18.32")
testCompileOnly(rootProject.libs.lombok)
testAnnotationProcessor(rootProject.libs.lombok)
// adventure
compileOnly("net.kyori:adventure-text-minimessage:4.17.0")
compileOnly("net.kyori:adventure-text-serializer-gson:4.17.0")
compileOnly("com.j256.ormlite:ormlite-jdbc:6.1") // ORMLite
compileOnly("com.github.ben-manes.caffeine:caffeine:3.1.8") // caching
compileOnly("io.netty:netty-all:4.1.109.Final") // netty
// Library/dependency loading
compileOnly("com.alessiodp.libby:libby-core:2.0.0-SNAPSHOT")
compileOnly(rootProject.libs.adventure.minimessage)
compileOnly(rootProject.libs.adventure.serializer)
compileOnly(rootProject.libs.ormlite)
compileOnly(rootProject.libs.caffeine)
compileOnly(rootProject.libs.netty)
compileOnly(rootProject.libs.libby.core)
}
tasks {
@@ -68,6 +56,11 @@ allprojects {
// Exclude unnecessary metadata information
exclude("META-INF/*/**")
// Minimize shadowed jar file
minimize {
exclude(project(":api"))
}
}
compileJava {
+2 -2
View File
@@ -1,3 +1,3 @@
group=xyz.jonesdev.sonar
version=2.0.31
description=Effective Anti-bot plugin for Velocity, BungeeCord, and Bukkit (1.7-latest)
version=2.0.34
description=A lightweight, effective, and easy-to-use anti-bot plugin for Velocity, BungeeCord, and Bukkit.
+35
View File
@@ -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]
versioner = "io.toolebox.git-versioner:1.6.7"
shadow = "io.github.goooler.shadow:8.1.7"
[libraries]
netty = { module = "io.netty:netty-all", version.ref = "netty" }
adventure-minimessage = { module = "net.kyori:adventure-text-minimessage", version.ref = "adventure" }
adventure-serializer = { module = "net.kyori:adventure-text-serializer-gson", version.ref = "adventure" }
adventure-nbt = { module = "net.kyori:adventure-nbt", version.ref = "adventure" }
adventure-platform-bukkit = { module = "net.kyori:adventure-platform-bukkit", version.ref = "adventure-platform" }
adventure-platform-bungee = { module = "net.kyori:adventure-platform-bungeecord", version.ref = "adventure-platform" }
adventure-platform-facet = { module = "net.kyori:adventure-platform-facet", version.ref = "adventure-platform" }
adventure-platform-api = { module = "net.kyori:adventure-platform-api", version.ref = "adventure-platform" }
libby-core = { module = "com.alessiodp.libby:libby-core", version.ref = "libby" }
libby-bungee = { module = "com.alessiodp.libby:libby-bungee", version.ref = "libby" }
libby-bukkit = { module = "com.alessiodp.libby:libby-bukkit", version.ref = "libby" }
libby-velocity = { module = "com.alessiodp.libby:libby-velocity", version.ref = "libby" }
bstats-bungee = { module = "org.bstats:bstats-bungeecord", version.ref = "bstats" }
bstats-bukkit = { module = "org.bstats:bstats-bukkit", version.ref = "bstats" }
bstats-velocity = { module = "org.bstats:bstats-velocity", version.ref = "bstats" }
caffeine = "com.github.ben-manes.caffeine:caffeine:3.1.8"
ormlite = "com.j256.ormlite:ormlite-jdbc:6.1"
lombok = "org.projectlombok:lombok:1.18.32"
capja = "xyz.jonesdev:capja:1.0.3"
bungeecord = "net.md_5:bungeecord-proxy:master-SNAPSHOT"
velocity = "com.velocitypowered:velocity-proxy:3.3.0-SNAPSHOT"
# We have to use 1.8 for backwards compatibility
spigot = "org.spigotmc:spigot-api:1.8.8-R0.1-SNAPSHOT"
Binary file not shown.
+1 -1
View File
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
@@ -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.net.InetAddress;
import java.text.DecimalFormat;
import java.util.UUID;
@@ -52,11 +51,6 @@ public interface Sonar {
*/
@NotNull SonarPlatform getPlatform();
/**
* @return If the amount of players online with the same IP address is greater than a maximum
*/
boolean hasTooManyAccounts(final @NotNull InetAddress inetAddress, final int limit);
/**
* @return A small wrapper for the plugin logger so we can use the logger everywhere
*/
@@ -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());
});
@@ -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,17 +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;
@@ -38,93 +38,64 @@ 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("<green>✔</green>");
final Component no = MiniMessage.miniMessage().deserialize("<red>✗</red>");
Sonar.get().getSubcommandRegistry().getSubcommands().forEach(command -> {
source.sendMessage(MiniMessage.miniMessage().deserialize(
Sonar.get().getConfig().getMessagesConfig().getString("commands.main.subcommands"),
TagResolver.resolver("suggest-subcommand", (argumentQueue, context) -> {
return Tag.styling(ClickEvent.suggestCommand("/sonar " + command.getInfo().name()));
}),
Placeholder.unparsed("subcommand", command.getInfo().name()),
Placeholder.unparsed("description", command.getInfo().description()),
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
@@ -18,6 +18,8 @@
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;
@@ -25,9 +27,8 @@ 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.regex.Pattern;
import java.util.stream.Collectors;
@Getter
@@ -35,60 +36,66 @@ 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 = 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 ? ""
this.arguments = info.arguments().length == 0 ? ""
: Arrays.stream(info.arguments())
.map(Argument::value)
.collect(Collectors.joining(", "));
}
protected static @Nullable InetAddress getInetAddressIfValid(final InvocationSource source, final String rawIP) {
final InetAddress inetAddress;
try {
inetAddress = InetAddress.getByName(rawIP);
} catch (UnknownHostException exception) {
source.sendMessage(SONAR.getConfig().getCommands().getInvalidIpAddress());
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]))");
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));
}
protected abstract void execute(final @NotNull CommandInvocation invocation);
@@ -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,7 +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.title.Title;
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;
@@ -102,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;
@@ -172,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");
@@ -216,19 +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.transfer.enabled = generalConfig.getBoolean("verification.transfer.enabled");
verification.transfer.host = generalConfig.getString("verification.transfer.destination-host");
verification.transfer.port = clamp(generalConfig.getInt("verification.transfer.destination-port"), 0, 0xffff);
if (verification.transfer.enabled) {
Sonar.get().getLogger().info("Transferring 1.20.5+ clients is enabled. Please make sure to follow the instructions in order for this to work properly.");
}
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);
@@ -251,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");
@@ -268,87 +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.invalidIpAddress = formatString(messagesConfig.getString("commands.invalid-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.verifiedAdd = formatString(messagesConfig.getString("commands.verified.added"));
commands.verifiedAlready = formatString(messagesConfig.getString("commands.verified.already"));
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.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 = deserialize(formatString(messagesConfig.getString("verbose.layout.normal")));
verbose.actionBarLayoutDuringAttack = deserialize(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.title = Title.title(
Sonar.get().getConfig().getNotifications().getNotificationTitle(),
Sonar.get().getConfig().getNotifications().getNotificationSubtitle());
notifications.notificationChat = deserialize(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) {
@@ -373,26 +333,6 @@ public final class SonarConfiguration {
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
@@ -401,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;
@@ -423,23 +362,8 @@ public final class SonarConfiguration {
private int attackCooldownDelay;
@Getter
private Component tooManyOnlinePerIp;
@Getter
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public static final class Verbose {
private Component actionBarLayout;
private Component actionBarLayoutDuringAttack;
private List<String> animation;
}
@Getter
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public static final class Notifications {
private Title title;
private Component notificationTitle;
private Component notificationSubtitle;
private Component notificationChat;
}
private List<String> verboseAnimation;
@Getter
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@@ -466,7 +390,6 @@ public final class SonarConfiguration {
private final Gravity gravity = new Gravity();
private final Vehicle vehicle = new Vehicle();
private final Brand brand = new Brand();
private final Transfer transfer = new Transfer();
@Getter
public static final class Map {
@@ -480,8 +403,6 @@ public final class SonarConfiguration {
private int maxDuration;
private int maxTries;
private String dictionary;
private Component enterCode;
private Component failedCaptcha;
private Smear distortion = new Smear();
@@ -528,26 +449,16 @@ public final class SonarConfiguration {
private Pattern validRegex;
}
@Getter
public static final class Transfer {
private boolean enabled;
private String host;
private int port;
}
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;
@@ -566,55 +477,6 @@ public final class SonarConfiguration {
private Component currentlyPreparing;
}
@Getter
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public static final class Commands {
private String incorrectCommandUsage;
private String invalidIpAddress;
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 verifiedAdd;
private String verifiedAlready;
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 {
@@ -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
@@ -28,46 +28,45 @@ import xyz.jonesdev.sonar.api.config.SonarConfiguration;
import xyz.jonesdev.sonar.api.logger.LoggerWrapper;
import java.net.InetAddress;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
@Getter
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class Fallback {
public static final Fallback INSTANCE = new Fallback();
private static final @NotNull Sonar SONAR = Objects.requireNonNull(Sonar.get());
// Map of all connected usernames and their respective IP addresses (used for fast checking)
private final Map<String, InetAddress> connected = new ConcurrentHashMap<>(64, 0.75f);
// Map of all players connected to the server in general
private final ConcurrentMap<InetAddress, Integer> online = new ConcurrentHashMap<>(128);
// Map of all connected IP addresses (used for fast checking)
private final ConcurrentMap<InetAddress, Byte> connected = new ConcurrentHashMap<>(128);
// Cache of all blacklisted IP addresses to ensure each entry can expire after the given time
@Setter
private Cache<InetAddress, Byte> blacklist;
@Setter
private long blacklistTime;
private final @NotNull FallbackQueue queue = FallbackQueue.INSTANCE;
private final @NotNull FallbackRatelimiter ratelimiter = FallbackRatelimiter.INSTANCE;
private final @NotNull FallbackQueue queue = new FallbackQueue();
private final @NotNull FallbackRatelimiter ratelimiter = new FallbackRatelimiter();
private final LoggerWrapper logger = new LoggerWrapper() {
@Override
public void info(final String message, final Object... args) {
SONAR.getLogger().info("[fallback] " + message, args);
Sonar.get().getLogger().info("[fallback] " + message, args);
}
@Override
public void warn(final String message, final Object... args) {
SONAR.getLogger().warn("[fallback] " + message, args);
Sonar.get().getLogger().warn("[fallback] " + message, args);
}
@Override
public void error(final String message, final Object... args) {
SONAR.getLogger().error("[fallback] " + message, args);
Sonar.get().getLogger().error("[fallback] " + message, args);
}
};
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
public boolean shouldVerifyNewPlayers() {
return shouldPerform(Sonar.get().getConfig().getVerification().getTiming());
}
@@ -26,34 +26,32 @@ import java.net.InetAddress;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.Executors;
@Getter
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
@RequiredArgsConstructor(access = AccessLevel.PROTECTED)
public final class FallbackQueue {
static final FallbackQueue INSTANCE = new FallbackQueue();
private final Map<InetAddress, Runnable> queuedPlayers = new ConcurrentHashMap<>(64);
// Async executor for all new verifications
public static final ExecutorService QUEUE_EXECUTOR = new ForkJoinPool(
Math.min(0x7fff, Runtime.getRuntime().availableProcessors()),
ForkJoinPool.defaultForkJoinWorkerThreadFactory, null, true);
// Fixed thread pool executor for all new verifications
private static final int THREAD_POOL_SIZE = Math.min(0x7fff, Runtime.getRuntime().availableProcessors() * 2);
private static final ExecutorService QUEUE_EXECUTOR = Executors.newFixedThreadPool(THREAD_POOL_SIZE);
private final ConcurrentMap<InetAddress, Runnable> 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();
QUEUE_EXECUTOR.execute(entry.getValue());
// Remove runnable from iterator
// Remove runnable from the map
iterator.remove();
}
}
@@ -27,40 +27,25 @@ import org.jetbrains.annotations.NotNull;
import java.net.InetAddress;
@Setter
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public final class FallbackRatelimiter {
static final FallbackRatelimiter INSTANCE = new FallbackRatelimiter();
private Cache<InetAddress, Byte> attemptCache;
private long timeout;
private Cache<InetAddress, Long> attemptCache;
@Getter
private Cache<InetAddress, Integer> failCountCache;
/**
* We don't want to clean up the cache in the
* {@link #attempt(InetAddress)} method because
* it would take up too many resources.
*/
public void cleanUpCache() {
attemptCache.cleanUp();
failCountCache.cleanUp();
}
/**
* Checks if the player has tried verifying too fast
* Attempts to rate-limit the client
*
* @param inetAddress IP address of the player
* @return Whether the player is allowed to verify
*/
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
public boolean attempt(final @NotNull InetAddress inetAddress) {
// Cache the IP address if it's not already cached
if (!attemptCache.asMap().containsKey(inetAddress)) {
// Cache the IP address
attemptCache.put(inetAddress, (byte) 0);
return true;
}
// Deny the connection attempt
return false;
final long expectedTimestamp = System.currentTimeMillis() + timeout;
// Check if the time since the last join has not exceeded the timeout
// The idea for this check was Taken from Velocity
final long last = attemptCache.get(inetAddress, result -> expectedTimestamp);
return expectedTimestamp == last;
}
/**
@@ -18,11 +18,13 @@
package xyz.jonesdev.sonar.api.fallback;
import io.netty.channel.Channel;
import io.netty.channel.ChannelPipeline;
import io.netty.util.ReferenceCountUtil;
import net.kyori.adventure.text.Component;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.api.timer.SystemTimer;
import java.net.InetAddress;
import java.util.UUID;
@@ -31,6 +33,9 @@ public interface FallbackUser {
@NotNull
Channel getChannel();
@NotNull
ChannelPipeline getPipeline();
@NotNull
InetAddress getInetAddress();
@@ -38,20 +43,27 @@ public interface FallbackUser {
ProtocolVersion getProtocolVersion();
@NotNull
FallbackUserState getState();
SystemTimer getLoginTimer();
boolean isReceivedClientSettings();
void setReceivedClientSettings(final boolean receivedClientSettings);
boolean isReceivedPluginMessage();
void setReceivedPluginMessage(final boolean receivedPluginMessage);
boolean isGeyser();
void setState(final FallbackUserState state);
/**
* Disconnect the player during/after verification
* using our custom Disconnect packet.
*
* @param reason Disconnect message component
* @param reason Disconnect message component
* @param duringLogin True, if the player has not joined yet
*/
@ApiStatus.Internal
void disconnect(final @NotNull Component reason);
void disconnect(final @NotNull Component reason, final boolean duringLogin);
/**
* Takes over the channel and begins the verification process
@@ -1,41 +0,0 @@
/*
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.api.fallback;
public enum FallbackUserState {
// 1.20.2 configuration state
LOGIN_ACK, CONFIGURE,
// pre-JOIN ping check
KEEP_ALIVE,
// post-JOIN checks
CLIENT_SETTINGS, PLUGIN_MESSAGE, TRANSACTION,
// PLAY checks
TELEPORT, POSITION,
// Vehicle check
VEHICLE,
// CAPTCHA
CAPTCHA,
// Done
SUCCESS,
// Placeholder
FAILED;
public boolean canReceivePackets() {
return this != FAILED && this != SUCCESS;
}
}
@@ -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
@@ -69,7 +67,8 @@ public enum ProtocolVersion {
MINECRAFT_1_20(763),
MINECRAFT_1_20_2(764),
MINECRAFT_1_20_3(765),
MINECRAFT_1_20_5(766);
MINECRAFT_1_20_5(766),
MINECRAFT_1_21(767);
private final int protocol;
@@ -77,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) {
@@ -77,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();
@@ -20,33 +20,70 @@ package xyz.jonesdev.sonar.api.verbose;
import lombok.Getter;
import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.text.Component;
import org.jetbrains.annotations.NotNull;
import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
import net.kyori.adventure.title.Title;
import xyz.jonesdev.sonar.api.Sonar;
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 {
public void sendAttackNotification() {
@Override
public void observe() {
// Don't prepare component if there are no subscribers
Component chat = null;
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())));
}
// 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;
// Only prepare component if there are subscribers
if (chat == null) {
chat = replaceStatistic(Sonar.get().getConfig().getNotifications().getNotificationChat());
}
// Send the message to all online subscribers
audience.sendMessage(chat);
for (Component component : chatNotificationComponents) {
audience.sendMessage(component);
}
// Send the title to all online subscribers
audience.showTitle(Sonar.get().getConfig().getNotifications().getTitle());
audience.showTitle(title);
}
}
}
@@ -17,97 +17,38 @@
package xyz.jonesdev.sonar.api.verbose;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TextReplacementConfig;
import lombok.Getter;
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.UUID;
import java.util.Vector;
import static xyz.jonesdev.sonar.api.Sonar.DECIMAL_FORMAT;
import static xyz.jonesdev.sonar.api.jvm.JVMProcessInformation.*;
@Getter
public abstract class Observable {
protected final @NotNull Collection<UUID> subscribers = new Vector<>(0);
public interface Observable {
Collection<UUID> getSubscribers();
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);
}
// TODO: Make this a bit prettier and better for performance
default @NotNull Component replaceStatistic(final @NotNull Component component) {
final SonarStatistics statistics = Sonar.get().getStatistics();
return component
.replaceText(TextReplacementConfig.builder().once().matchLiteral("%queued%")
.replacement(DECIMAL_FORMAT.format(Sonar.get().getFallback().getQueue().getQueuedPlayers().size()))
.build())
.replaceText(TextReplacementConfig.builder().once().matchLiteral("%verifying%")
.replacement(DECIMAL_FORMAT.format(Sonar.get().getFallback().getConnected().size()))
.build())
.replaceText(TextReplacementConfig.builder().once().matchLiteral("%blacklisted%")
.replacement(DECIMAL_FORMAT.format(Sonar.get().getFallback().getBlacklist().estimatedSize()))
.build())
.replaceText(TextReplacementConfig.builder().once().matchLiteral("%total-joins%")
.replacement(DECIMAL_FORMAT.format(statistics.getTotalPlayersJoined()))
.build())
.replaceText(TextReplacementConfig.builder().once().matchLiteral("%logins-per-second%")
.replacement(DECIMAL_FORMAT.format(statistics.getLoginsPerSecond()))
.build())
.replaceText(TextReplacementConfig.builder().once().matchLiteral("%connections-per-second%")
.replacement(DECIMAL_FORMAT.format(statistics.getConnectionsPerSecond()))
.build())
.replaceText(TextReplacementConfig.builder().once().matchLiteral("%verify-total%")
.replacement(DECIMAL_FORMAT.format(statistics.getTotalAttemptedVerifications()))
.build())
.replaceText(TextReplacementConfig.builder().once().matchLiteral("%verify-success%")
.replacement(DECIMAL_FORMAT.format(statistics.getTotalSuccessfulVerifications()))
.build())
.replaceText(TextReplacementConfig.builder().once().matchLiteral("%verify-failed%")
.replacement(DECIMAL_FORMAT.format(statistics.getTotalFailedVerifications()))
.build())
.replaceText(TextReplacementConfig.builder().once().matchLiteral("%incoming-traffic%")
.replacement(statistics.getCurrentIncomingBandwidthFormatted())
.build())
.replaceText(TextReplacementConfig.builder().once().matchLiteral("%outgoing-traffic%")
.replacement(statistics.getCurrentOutgoingBandwidthFormatted())
.build())
.replaceText(TextReplacementConfig.builder().once().matchLiteral("%incoming-traffic-ttl%")
.replacement(statistics.getTotalIncomingBandwidthFormatted())
.build())
.replaceText(TextReplacementConfig.builder().once().matchLiteral("%outgoing-traffic-ttl%")
.replacement(statistics.getTotalOutgoingBandwidthFormatted())
.build())
.replaceText(TextReplacementConfig.builder().once().matchLiteral("%used-memory%")
.replacement(formatMemory(getUsedMemory()))
.build())
.replaceText(TextReplacementConfig.builder().once().matchLiteral("%free-memory%")
.replacement(formatMemory(getFreeMemory()))
.build())
.replaceText(TextReplacementConfig.builder().once().matchLiteral("%total-memory%")
.replacement(formatMemory(getTotalMemory()))
.build())
.replaceText(TextReplacementConfig.builder().once().matchLiteral("%max-memory%")
.replacement(formatMemory(getMaxMemory()))
.build());
}
}
@@ -18,55 +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.TextReplacementConfig;
import org.jetbrains.annotations.NotNull;
import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.attack.AttackTracker;
import xyz.jonesdev.sonar.api.timer.SystemTimer;
import 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() {
@Override
public void observe() {
// Don't prepare component if there are no subscribers
Component component = null;
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;
// Only prepare component if there are subscribers
if (component == null) {
final AttackTracker.AttackStatistics attackStatistics = Sonar.get().getAttackTracker().getCurrentAttack();
final SystemTimer attackDuration = attackStatistics == null ? null : attackStatistics.getDuration();
component = replaceStatistic(attackDuration != null
? Sonar.get().getConfig().getVerbose().getActionBarLayoutDuringAttack()
: Sonar.get().getConfig().getVerbose().getActionBarLayout())
.replaceText(TextReplacementConfig.builder().once().matchLiteral("%attack-duration%")
.replacement(attackDuration == null ? "00:00" : DATE_FORMATTER.format(attackDuration.delay()))
.build())
.replaceText(TextReplacementConfig.builder().once().matchLiteral("%animation%")
.replacement(nextAnimation())
.build());
}
// Send the action bar to all online subscribers
audience.sendActionBar(component);
audience.sendActionBar(actionBarComponent);
}
}
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];
}
+9 -17
View File
@@ -1,13 +1,12 @@
import net.minecrell.pluginyml.bukkit.BukkitPluginDescription
plugins {
id("net.minecrell.plugin-yml.bukkit") version "0.6.0"
id("net.minecrell.plugin-yml.bukkit") version "0.6.0" apply true
}
apply(plugin = "net.minecrell.plugin-yml.bukkit")
bukkit {
name = rootProject.name
description = rootProject.description
version = rootProject.version.toString().split("-")[0]
main = "xyz.jonesdev.sonar.bukkit.SonarBukkitPlugin"
authors = listOf("Jones Development", "Sonar Contributors")
@@ -33,21 +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")
// adventure nbt
implementation("net.kyori:adventure-nbt:4.17.0")
compileOnly(rootProject.libs.spigot)
// We have to use 1.8 for backwards compatibility
compileOnly("org.spigotmc:spigot-api:1.8.8-R0.1-SNAPSHOT")
// Implement bStats.org for metrics
implementation("org.bstats:bstats-bukkit:3.0.2")
// Library/dependency loading
implementation("com.alessiodp.libby:libby-bukkit:2.0.0-SNAPSHOT")
implementation(rootProject.libs.adventure.platform.bukkit)
implementation(rootProject.libs.adventure.platform.api)
implementation(rootProject.libs.adventure.platform.facet)
implementation(rootProject.libs.adventure.nbt)
implementation(rootProject.libs.bstats.bukkit)
implementation(rootProject.libs.libby.bukkit)
}
tasks {
@@ -23,7 +23,6 @@ import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.platform.bukkit.BukkitAudiences;
import org.bstats.bukkit.Metrics;
import org.bstats.charts.SimplePie;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import xyz.jonesdev.sonar.api.SonarPlatform;
@@ -31,7 +30,6 @@ import xyz.jonesdev.sonar.api.logger.LoggerWrapper;
import xyz.jonesdev.sonar.bukkit.command.BukkitSonarCommand;
import xyz.jonesdev.sonar.common.boot.SonarBootstrap;
import java.net.InetAddress;
import java.util.Objects;
import java.util.UUID;
@@ -58,20 +56,6 @@ public final class SonarBukkit extends SonarBootstrap<SonarBukkitPlugin> {
return bukkitAudiences.player(uniqueId);
}
@Override
public boolean hasTooManyAccounts(final @NotNull InetAddress inetAddress, final int limit) {
int count = 1;
for (final Player player : getPlugin().getServer().getOnlinePlayers()) {
// I don't know why, but Bukkit is *special*
if (Objects.isNull(player.getAddress())) continue;
// Check if the IP address of the player is equal to the IP trying to connect
if (!Objects.equals(player.getAddress().getAddress(), inetAddress)) continue;
// Increment count of duplicated accounts
if (++count >= limit) return true;
}
return false;
}
/**
* Create a wrapper for the plugin logger, so we can use it outside
* the velocity module.
+10 -17
View File
@@ -1,11 +1,10 @@
plugins {
id("net.minecrell.plugin-yml.bungee") version "0.6.0"
id("net.minecrell.plugin-yml.bungee") version "0.6.0" apply true
}
apply(plugin = "net.minecrell.plugin-yml.bungee")
bungee {
name = rootProject.name
description = rootProject.description
version = rootProject.version.toString().split("-")[0]
main = "xyz.jonesdev.sonar.bungee.SonarBungeePlugin"
author = "Jones Development, Sonar Contributors"
@@ -16,21 +15,15 @@ dependencies {
implementation(project(":api"))
implementation(project(":common"))
compileOnly("net.md_5:bungeecord-proxy:master-SNAPSHOT")
testCompileOnly("net.md_5:bungeecord-proxy:master-SNAPSHOT")
compileOnly(rootProject.libs.bungeecord)
testCompileOnly(rootProject.libs.bungeecord)
// adventure platform support
implementation("net.kyori:adventure-platform-bungeecord:4.3.2")
implementation("net.kyori:adventure-platform-api:4.3.2")
implementation("net.kyori:adventure-platform-facet:4.3.2")
// adventure nbt
implementation("net.kyori:adventure-nbt:4.17.0")
// Implement bStats.org for metrics
implementation("org.bstats:bstats-bungeecord:3.0.2")
// Library/dependency loading
implementation("com.alessiodp.libby:libby-bungee:2.0.0-SNAPSHOT")
implementation(rootProject.libs.adventure.platform.bungee)
implementation(rootProject.libs.adventure.platform.api)
implementation(rootProject.libs.adventure.platform.facet)
implementation(rootProject.libs.adventure.nbt)
implementation(rootProject.libs.bstats.bungee)
implementation(rootProject.libs.libby.bungee)
}
tasks {
@@ -21,7 +21,6 @@ import com.alessiodp.libby.BungeeLibraryManager;
import lombok.Getter;
import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.platform.bungeecord.BungeeAudiences;
import net.md_5.bungee.api.connection.ProxiedPlayer;
import org.bstats.bungeecord.Metrics;
import org.bstats.charts.SimplePie;
import org.jetbrains.annotations.NotNull;
@@ -32,8 +31,6 @@ import xyz.jonesdev.sonar.bungee.command.BungeeSonarCommand;
import xyz.jonesdev.sonar.bungee.fallback.FallbackInjectionHelper;
import xyz.jonesdev.sonar.common.boot.SonarBootstrap;
import java.net.InetAddress;
import java.util.Objects;
import java.util.UUID;
@Getter
@@ -58,19 +55,6 @@ public final class SonarBungee extends SonarBootstrap<SonarBungeePlugin> {
return bungeeAudiences.player(uniqueId);
}
@Override
@SuppressWarnings("deprecation")
public boolean hasTooManyAccounts(final @NotNull InetAddress inetAddress, final int limit) {
int count = 1;
for (final ProxiedPlayer player : getPlugin().getServer().getPlayers()) {
// Check if the IP address of the player is equal to the IP trying to connect
if (!Objects.equals(player.getAddress().getAddress(), inetAddress)) continue;
// Increment count of duplicated accounts
if (++count >= limit) return true;
}
return false;
}
/**
* Create a wrapper for the plugin logger, so we can use it outside
* the velocity module.
@@ -29,7 +29,8 @@ import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.ReflectiveOperationException;
import xyz.jonesdev.sonar.common.fallback.FallbackChannelHandlerAdapter;
import java.lang.reflect.Field;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.net.InetSocketAddress;
import static net.md_5.bungee.netty.PipelineUtils.*;
@@ -40,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);
}
@@ -54,7 +55,7 @@ public final class FallbackChannelHandler extends FallbackChannelHandlerAdapter
@Override
public void channelRead(final @NotNull ChannelHandlerContext ctx, final Object msg) throws Exception {
// Intercept any packets processed by BungeeCord
if (msg instanceof PacketWrapper) {
if (listenForPackets && msg instanceof PacketWrapper) {
final PacketWrapper packetWrapper = (PacketWrapper) msg;
final DefinedPacket wrappedPacket = packetWrapper.packet;
// Don't handle any invalid packets
@@ -65,14 +66,20 @@ public final class FallbackChannelHandler extends FallbackChannelHandlerAdapter
handleHandshake(handshake.getHost(), handshake.getProtocolVersion());
}
// Intercept any server login packet by the client
if (wrappedPacket instanceof LoginRequest) {
else if (wrappedPacket instanceof LoginRequest) {
// Make sure to use the potentially modified, real IP
final LoginRequest loginRequest = (LoginRequest) wrappedPacket;
final HandlerBoss handlerBoss = channel.pipeline().get(HandlerBoss.class);
final ChannelWrapper channelWrapper = (ChannelWrapper) CHANNEL_WRAPPER_FIELD.get(handlerBoss);
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;
}
}
+2 -4
View File
@@ -1,11 +1,9 @@
dependencies {
compileOnly(project(":api"))
// adventure nbt for packets
compileOnly("net.kyori:adventure-nbt:4.17.0")
compileOnly(rootProject.libs.adventure.nbt)
// CAPTCHA image generation
implementation("xyz.jonesdev:capja:1.0.3")
implementation(rootProject.libs.capja)
}
java.sourceCompatibility = JavaVersion.VERSION_11
@@ -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,7 +37,7 @@ 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.SonarServiceThreadManager;
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.update.UpdateChecker;
@@ -103,7 +104,7 @@ public abstract class SonarBootstrap<T> implements Sonar {
// Start threads
getLogger().info("Starting all managed threads...");
SonarServiceThreadManager.start();
SonarServiceManager.start();
// Done
getLogger().info("Done ({}s)!", launchTimer);
@@ -114,8 +115,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();
@@ -137,9 +140,11 @@ public abstract class SonarBootstrap<T> implements Sonar {
// Update ratelimiter caches
getFallback().getRatelimiter().setAttemptCache(Caffeine.newBuilder()
.expireAfterWrite(Duration.ofMillis(getConfig().getVerification().getReconnectDelay()))
.ticker(Ticker.systemTicker())
.build());
getFallback().getRatelimiter().setFailCountCache(Caffeine.newBuilder()
.expireAfterWrite(Duration.ofMillis(getConfig().getVerification().getRememberTime()))
.ticker(Ticker.systemTicker())
.build());
// Update blacklist cache
@@ -148,9 +153,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);
@@ -169,10 +175,10 @@ public abstract class SonarBootstrap<T> implements Sonar {
}
public final void shutdown() {
// Initialize shutdown process
// Initialize the shutdown process
getLogger().info("Starting shutdown process...");
// Interrupt threads
SonarServiceThreadManager.stop();
SonarServiceManager.stop();
// Close database connection if present
if (verifiedPlayerController != null) {
verifiedPlayerController.close();
@@ -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
@@ -38,7 +38,6 @@ import java.util.UUID;
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_BANDWIDTH;
import static xyz.jonesdev.sonar.common.fallback.FallbackUserWrapper.customDisconnect;
import static xyz.jonesdev.sonar.common.fallback.FallbackUserWrapper.deject;
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer.*;
@RequiredArgsConstructor
@@ -47,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
@@ -60,16 +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().getQueuedPlayers().remove(inetAddress);
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();
@@ -81,7 +80,7 @@ public class FallbackChannelHandlerAdapter extends ChannelInboundHandlerAdapter
@Override
public final void exceptionCaught(final @NotNull ChannelHandlerContext ctx,
final @NotNull Throwable cause) throws Exception {
// Simply close the channel if we encounter any errors.
// Close the channel if we encounter any errors.
ctx.close();
}
@@ -103,7 +102,6 @@ public class FallbackChannelHandlerAdapter extends ChannelInboundHandlerAdapter
// Store the protocol version
protocolVersion = ProtocolVersion.fromId(protocol);
// Hook the traffic listener
// TODO: Can we implement this in channelActive?
channel.pipeline().addFirst(FALLBACK_BANDWIDTH, FallbackBandwidthHandler.INSTANCE);
}
@@ -130,8 +128,7 @@ public class FallbackChannelHandlerAdapter extends ChannelInboundHandlerAdapter
// Connections from unknown protocol versions will be discarded
// as this is the safest way of handling unwanted connections
if (protocolVersion.isUnknown()) {
// Sonar does NOT support snapshots or unknown versions;
// I'll try my best to stay up-to-date!
// Sonar does not support snapshots or Minecraft versions older than 1.7.2
throw new CorruptedFrameException("Unknown protocol version");
}
// Increase joins per second for the action bar verbose
@@ -146,6 +143,18 @@ public class FallbackChannelHandlerAdapter extends ChannelInboundHandlerAdapter
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);
@@ -159,20 +168,6 @@ public class FallbackChannelHandlerAdapter extends ChannelInboundHandlerAdapter
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, encoder, 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(username)
|| FALLBACK.getConnected().containsValue(inetAddress)) {
customDisconnect(channel, protocolVersion, alreadyVerifying, 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())) {
@@ -183,7 +178,7 @@ public class FallbackChannelHandlerAdapter extends ChannelInboundHandlerAdapter
// 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, offlineUUID)) {
if (Sonar.get().getVerifiedPlayerController().has(inetAddress.toString(), offlineUUID)) {
initialLogin(ctx, loginPacket, encoder, handler);
return;
}
@@ -202,7 +197,7 @@ public class FallbackChannelHandlerAdapter extends ChannelInboundHandlerAdapter
}
// Queue the connection for further processing
FALLBACK.getQueue().getQueuedPlayers().put(inetAddress, () -> {
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()
@@ -212,7 +207,7 @@ public class FallbackChannelHandlerAdapter extends ChannelInboundHandlerAdapter
}
// Create an instance for the Fallback connection
user = new FallbackUserWrapper(channel, inetAddress, protocolVersion, geyser);
final FallbackUser user = new FallbackUserWrapper(channel, inetAddress, protocolVersion, geyser);
// Let the verification handler take over the channel
user.hijack(username, offlineUUID, encoder, decoder, timeout, handler);
});
@@ -228,19 +223,25 @@ public class FallbackChannelHandlerAdapter extends ChannelInboundHandlerAdapter
final @NotNull Object loginPacket,
final @NotNull String encoder,
final @NotNull String handler) throws Exception {
// Increment the number of accounts with the same IP
FALLBACK.getOnline().compute(inetAddress, (key, value) -> value == null ? 1 : value + 1);
final int maxOnlinePerIp = Sonar.get().getConfig().getMaxOnlinePerIp();
// Skip the maximum online per IP check if it's disabled in the configuration
if (maxOnlinePerIp > 0) {
// Check if the number of online players using the same IP address as
// the connecting player is greater than the configured amount
if (Sonar.get().hasTooManyAccounts(inetAddress, maxOnlinePerIp)) {
final int onlinePerIp = FALLBACK.getOnline().getOrDefault(inetAddress, 0);
if (onlinePerIp > maxOnlinePerIp) {
customDisconnect(channel, protocolVersion, tooManyOnlinePerIP, encoder, handler);
return;
}
}
// Don't listen for further packets
// TODO: deject the pipeline without breaking disconnect logic
listenForPackets = false;
// Let the server know about the login packet
ctx.fireChannelRead(loginPacket);
// Deject the channel since we don't need it anymore
deject(channel.pipeline());
}
}
@@ -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();
}
}
}
@@ -21,6 +21,7 @@ import io.netty.channel.Channel;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelPipeline;
import io.netty.handler.codec.CorruptedFrameException;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@@ -31,20 +32,22 @@ import xyz.jonesdev.sonar.api.event.impl.UserBlacklistedEvent;
import xyz.jonesdev.sonar.api.event.impl.UserVerifyFailedEvent;
import xyz.jonesdev.sonar.api.event.impl.UserVerifyJoinEvent;
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
import xyz.jonesdev.sonar.api.fallback.FallbackUserState;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.api.timer.SystemTimer;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketDecoder;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketEncoder;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.DisconnectPacket;
import xyz.jonesdev.sonar.common.fallback.session.FallbackLoginSessionHandler;
import xyz.jonesdev.sonar.common.statistics.GlobalSonarStatistics;
import java.net.InetAddress;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.*;
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_PACKET_DECODER;
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_PACKET_ENCODER;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_20_2;
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry.CONFIG;
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry.GAME;
@@ -56,9 +59,12 @@ public final class FallbackUserWrapper implements FallbackUser {
private final ChannelPipeline pipeline;
private final InetAddress inetAddress;
private final ProtocolVersion protocolVersion;
private final boolean geyser;
@Setter
private FallbackUserState state = FallbackUserState.LOGIN_ACK; // 1.20.2+
private boolean receivedClientSettings;
@Setter
private boolean receivedPluginMessage;
private final boolean geyser;
private final SystemTimer loginTimer = new SystemTimer();
public FallbackUserWrapper(final @NotNull Channel channel,
final @NotNull InetAddress inetAddress,
@@ -72,8 +78,8 @@ public final class FallbackUserWrapper implements FallbackUser {
}
@Override
public void disconnect(final @NotNull Component reason) {
closeWith(channel, protocolVersion, DisconnectPacket.create(reason, false));
public void disconnect(final @NotNull Component reason, final boolean duringLogin) {
closeWith(channel, protocolVersion, DisconnectPacket.create(reason, duringLogin));
}
@Override
@@ -88,30 +94,33 @@ public final class FallbackUserWrapper implements FallbackUser {
// We let the user override this through the configuration.
if (Sonar.get().getAttackTracker().getCurrentAttack() == null
|| Sonar.get().getConfig().getVerification().isLogDuringAttack()) {
Sonar.get().getFallback().getLogger().info(Sonar.get().getConfig().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);
// 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));
// Replace normal encoder to allow custom packets
final FallbackPacketEncoder newEncoder = new FallbackPacketEncoder(protocolVersion);
pipeline.replace(encoder, FALLBACK_PACKET_ENCODER, newEncoder);
// Mark the player as connected by caching them in a map of verifying players
Sonar.get().getFallback().getConnected().compute(inetAddress, (key, value) -> (byte) 0);
channel.eventLoop().execute(() -> {
// Remove main pipeline to completely take over the channel
// 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);
// Remove the main pipeline to completely take over the channel
if (pipeline.get(handler) != null) {
pipeline.remove(handler);
}
@@ -123,16 +132,11 @@ public final class FallbackUserWrapper implements FallbackUser {
newEncoder.updateRegistry(protocolVersion.compareTo(MINECRAFT_1_20_2) >= 0 ? CONFIG : GAME);
try {
// Prepare custom packet decoder and verification handler
final FallbackVerificationHandler verification = new FallbackVerificationHandler(this, username, uuid);
final FallbackPacketDecoder fallbackPacketDecoder = new FallbackPacketDecoder(this, verification);
// Replace normal decoder to allow custom packets
final FallbackPacketDecoder fallbackPacketDecoder = new FallbackPacketDecoder(protocolVersion);
pipeline.replace(decoder, FALLBACK_PACKET_DECODER, fallbackPacketDecoder);
if (protocolVersion.compareTo(MINECRAFT_1_20_2) < 0) {
// Start initializing the actual join process for pre-1.20.2 clients
verification.initialJoinProcess();
}
// Listen for all incoming packets by setting the packet listener
fallbackPacketDecoder.setListener(new FallbackLoginSessionHandler(this, username, uuid));
} catch (Throwable throwable) {
// This rarely happens when the channel hangs and the player is still connecting
// I honestly have no idea how else I'm supposed to fix this
@@ -143,27 +147,22 @@ public final class FallbackUserWrapper implements FallbackUser {
@Override
public void fail(final @NotNull String reason) {
// Make sure to set the state to failed to drop all handlers
setState(FallbackUserState.FAILED);
disconnect(Sonar.get().getConfig().getVerification().getVerificationFailed(), false);
// Only log the failed message if the server isn't currently under attack.
// However, we let the user override this through the configuration.
final boolean shouldLog = Sonar.get().getAttackTracker().getCurrentAttack() == null
|| Sonar.get().getConfig().getVerification().isLogDuringAttack();
// Only disconnect the player and log if the channel is active
if (channel.isActive()) {
disconnect(Sonar.get().getConfig().getVerification().getVerificationFailed());
if (shouldLog) {
Sonar.get().getFallback().getLogger().info(Sonar.get().getConfig().getVerification().getFailedLog()
.replace("%ip%", Sonar.get().getConfig().formatAddress(getInetAddress()))
.replace("%protocol%", String.valueOf(getProtocolVersion().getProtocol()))
.replace("%reason%", reason));
}
if (shouldLog) {
Sonar.get().getFallback().getLogger().info(
Sonar.get().getConfig().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
@@ -177,33 +176,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();
}
/**
@@ -256,12 +257,12 @@ public final class FallbackUserWrapper implements FallbackUser {
final @NotNull FallbackPacket packet,
final @NotNull String encoder,
final @NotNull String boss) {
// Remove main pipeline to completely take over the channel
if (channel.pipeline().get(boss) != null) {
// 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)) {
@@ -273,17 +274,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);
}
}
}
@@ -1,745 +0,0 @@
/*
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback;
import io.netty.buffer.ByteBuf;
import io.netty.handler.codec.CorruptedFrameException;
import io.netty.handler.codec.DecoderException;
import lombok.val;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.event.impl.UserVerifySuccessEvent;
import xyz.jonesdev.sonar.api.fallback.Fallback;
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
import xyz.jonesdev.sonar.api.fallback.FallbackUserState;
import xyz.jonesdev.sonar.api.model.VerifiedPlayer;
import xyz.jonesdev.sonar.api.timer.SystemTimer;
import xyz.jonesdev.sonar.common.fallback.protocol.*;
import xyz.jonesdev.sonar.common.fallback.protocol.captcha.ItemType;
import xyz.jonesdev.sonar.common.fallback.protocol.captcha.MapCaptchaInfo;
import xyz.jonesdev.sonar.common.fallback.protocol.entity.EntityType;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.config.FinishConfigurationPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.login.LoginAcknowledgedPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.*;
import xyz.jonesdev.sonar.common.statistics.GlobalSonarStatistics;
import xyz.jonesdev.sonar.common.util.ProtocolUtil;
import java.util.Objects;
import java.util.Random;
import java.util.UUID;
import java.util.regex.Pattern;
import static xyz.jonesdev.sonar.api.config.SonarConfiguration.Verification.Gravity.Gamemode.CREATIVE;
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_PACKET_DECODER;
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_PACKET_ENCODER;
import static xyz.jonesdev.sonar.api.fallback.FallbackUserState.*;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer.*;
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.BRAND_CHANNEL;
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.BRAND_CHANNEL_LEGACY;
public final class FallbackVerificationHandler implements FallbackPacketListener {
private static final Random RANDOM = new Random();
private static final Fallback FALLBACK = Sonar.get().getFallback();
private final SystemTimer login = new SystemTimer();
private final @NotNull FallbackUser user;
private final String username;
private final UUID playerUuid;
// Checks
private short expectedTransactionId;
private int expectedKeepAliveId, expectedTeleportId = -1;
private int tick, totalReceivedPackets;
private double posX, posY, posZ, lastY, spawnYPosition;
private boolean resolvedClientBrand, resolvedClientSettings;
private boolean listenForMovements;
private boolean receivedSteerBoat, receivedPlayerInput;
// CAPTCHA
private @Nullable MapCaptchaInfo captcha;
private int captchaTriesLeft, lastCountdownIndex;
// Cached options to make sure the original values don't change throughout the verification
private final boolean performVehicle, performCaptcha, performGravity, performCollisions, transfer;
public FallbackVerificationHandler(final @NotNull FallbackUser user,
final @NotNull String username,
final @NotNull UUID playerUuid) {
this.user = user;
this.username = username;
this.playerUuid = playerUuid;
// We don't want to check Geyser players for valid gravity, as this might cause issues because of the protocol
this.performGravity = !user.isGeyser() && Sonar.get().getConfig().getVerification().getGravity().isEnabled();
this.performCollisions = Sonar.get().getConfig().getVerification().getGravity().isCheckCollisions();
this.performCaptcha = FALLBACK.shouldPerformCaptcha();
this.performVehicle = FALLBACK.shouldPerformVehicleCheck();
this.transfer = Sonar.get().getConfig().getVerification().getTransfer().isEnabled()
&& user.getProtocolVersion().compareTo(MINECRAFT_1_20_5) >= 0;
}
private void configure() {
// Set the state to CONFIGURE to avoid false positives
user.setState(CONFIGURE);
// Send the necessary configuration packets to the client
if (user.getProtocolVersion().compareTo(MINECRAFT_1_20_5) >= 0) {
for (final FallbackPacket packet : REGISTRY_SYNC_1_20_5) {
user.delayedWrite(packet);
}
} else {
user.delayedWrite(REGISTRY_SYNC_LEGACY);
}
user.delayedWrite(FINISH_CONFIGURATION);
// Send all packets in one flush
user.getChannel().flush();
// Set decoder state to actually catch all packets
updateEncoderDecoderState(FallbackPacketRegistry.CONFIG);
}
private void updateEncoderDecoderState(final @NotNull FallbackPacketRegistry registry) {
val decoder = (FallbackPacketDecoder) user.getChannel().pipeline().get(FALLBACK_PACKET_DECODER);
val encoder = (FallbackPacketEncoder) user.getChannel().pipeline().get(FALLBACK_PACKET_ENCODER);
if (decoder != null && encoder != null) {
// Update the packet registry state to be able to listen for CONFIG packets
decoder.updateRegistry(registry);
encoder.updateRegistry(registry);
} else {
// Something went wrong - the decoder should not be null
FALLBACK.getLogger().warn("Necessary pipelines for {} not found", username);
// Close the channel to prevent bypasses or other potential exploits
user.getChannel().close();
}
}
public void initialJoinProcess() {
if (user.getProtocolVersion().compareTo(MINECRAFT_1_8) < 0) {
// 1.7 players don't have KeepAlive packets in the login process
sendJoinGamePacket();
} else {
// Set the state to KEEP_ALIVE to avoid false positives
user.setState(KEEP_ALIVE);
// Generate a random KeepAlive ID for the pre-join check
expectedKeepAliveId = RANDOM.nextInt();
// Send first KeepAlive to check if the connection is somewhat responsive
user.write(new KeepAlivePacket(expectedKeepAliveId));
}
}
private void sendTransaction() {
// Set the state to TRANSACTION to avoid false positives
// and go on with the flow of the verification.
user.setState(TRANSACTION);
// Generate a random transaction ID
expectedTransactionId = (short) RANDOM.nextInt();
// Send a transaction with the given ID
user.write(new TransactionPacket(0, expectedTransactionId, false));
}
private void sendJoinGamePacket() {
final boolean v1_20_2 = user.getProtocolVersion().compareTo(MINECRAFT_1_20_2) >= 0;
if (!v1_20_2) {
// Set the state to CLIENT_SETTINGS to avoid false positives
// (1.20.2 needs the LOGIN_ACK state) and go on with
// the flow of the verification.
user.setState(CLIENT_SETTINGS);
}
// Send the JoinGame packet so we can continue the verification
user.write(joinGame);
if (v1_20_2) {
// Perform the transaction check since the ClientSettings
// and PluginMessage packets have already been validated.
sendTransaction();
}
}
private void sendAbilitiesAndTeleport() {
// Set the state to TELEPORT to avoid false positives
// and go on with the flow of the verification.
user.setState(TELEPORT);
// Make sure the player is unable to fly (e.g. if the player is in creative mode)
if (Sonar.get().getConfig().getVerification().getGravity().getGamemode() == CREATIVE) {
user.delayedWrite(DEFAULT_ABILITIES);
}
// Generate the current teleport ID
expectedTeleportId = RANDOM.nextInt();
// Add a little randomization to the spawn y coordinate
spawnYPosition = dynamicSpawnYPosition + RANDOM.nextDouble(0.4);
// Teleport the player to the spawn position
user.delayedWrite(new PlayerPositionLookPacket(
SPAWN_X_POSITION, spawnYPosition, SPAWN_Z_POSITION,
0f, -90f, expectedTeleportId, false));
// Make sure the player escapes the 1.18.2+ "Loading terrain" screen
if (user.getProtocolVersion().compareTo(MINECRAFT_1_18_2) >= 0) {
user.delayedWrite(dynamicSpawnPosition);
}
}
private void sendChunkData() {
// If we don't have any checks, simply finish verification
// This is *hopefully* a very rare case though...
if (!performGravity && !performCaptcha && !performVehicle) {
// Save some work by finishing before sending even more packets
finish();
return;
}
// Set the state to POSITION to avoid false positives
// and go on with the flow of the verification.
user.setState(POSITION);
// 1.20.3 packets introduced game events
if (user.getProtocolVersion().compareTo(MINECRAFT_1_20_3) >= 0) {
// Make sure the client knows that we're sending chunks next
user.delayedWrite(START_WRITING_CHUNKS);
}
// Teleport player into the fake lobby by sending an empty chunk
user.delayedWrite(EMPTY_CHUNK_DATA);
// Checking gravity is disabled, just finish verification
if (!performGravity) {
// Switch to captcha state if needed
vehicleCheckOrNext();
return;
}
// Send an UpdateSectionBlocks packet with a platform of blocks
// to check if the player collides with the solid platform.
user.delayedWrite(updateSectionBlocks);
// Send all packets in one flush
user.getChannel().flush();
}
private static boolean validateClientLocale(final @SuppressWarnings("unused") @NotNull FallbackUser user,
final String locale) {
// Check the client locale by performing a simple regex check on it
final Pattern pattern = Sonar.get().getConfig().getVerification().getValidLocaleRegex();
return pattern.matcher(locale).matches(); // Disallow non-ascii characters (by default)
}
private static boolean validateClientBrand(final @NotNull FallbackUser user, final ByteBuf content) {
// We have to catch every DecoderException, so we can fail and punish
// the player instead of only disconnecting them due to an exception.
try {
// No need to check for empty brands since ProtocolUtil#readBrandMessage
// already performs these checks by default.
final String read = ProtocolUtil.readBrandMessage(content);
// Check if the decoded client brand string is too long
if (read.length() > Sonar.get().getConfig().getVerification().getBrand().getMaxLength()) {
return false;
}
// Regex pattern for validating client brands
final Pattern pattern = Sonar.get().getConfig().getVerification().getBrand().getValidRegex();
return !read.equals("Vanilla") // The normal brand is always lowercase
&& pattern.matcher(read).matches(); // Disallow non-ascii characters (by default)
} catch (DecoderException exception) {
// Fail if the string (client brand) could not be decoded properly
user.fail("could not decode string");
// Throw the exception so we don't continue checking
throw exception;
}
}
private void handlePacketsCAPTCHA(final @NotNull FallbackPacket packet) {
// Check if the player took too long to enter the captcha
final int maxDuration = Sonar.get().getConfig().getVerification().getMap().getMaxDuration();
checkFrame(!login.elapsed(maxDuration), "took too long to enter captcha");
// Handle incoming chat messages
if (packet instanceof UniversalChatPacket) {
final UniversalChatPacket chat = (UniversalChatPacket) packet;
final String answer = Objects.requireNonNull(captcha).getAnswer();
if (chat.getMessage().equals(answer)) {
// Captcha is correct
finish();
return;
}
// Captcha is incorrect
checkFrame(captchaTriesLeft-- > 0, "failed captcha too often");
user.write(incorrectCaptcha);
return;
}
// Only expect idle (position) packets
if (packet instanceof PlayerPositionPacket
|| packet instanceof PlayerPositionLookPacket) {
// A position packet is sent every second
final long difference = maxDuration - login.delay();
final int index = (int) (difference / 1000D);
// Make sure we can actually safely get and send the packet
if (lastCountdownIndex != index && index >= 0 && xpCountdown.length > index) {
// Send the countdown using the experience bar
user.write(xpCountdown[index]);
// Send a KeepAlive packet to prevent timeout
user.write(CAPTCHA_KEEP_ALIVE);
}
lastCountdownIndex = index;
}
}
@Override
public void handle(final @NotNull FallbackPacket packet) {
// The player has already been verified or failed the verification -> drop packet
if (!user.getState().canReceivePackets()) return;
// Calculate maximum amount of packets sent by the client to the server
final int maxPackets = Sonar.get().getConfig().getVerification().getMaxLoginPackets()
+ maxMovementTick // Account for positions/fall check
+ Sonar.get().getConfig().getVerification().getMap().getMaxDuration()
+ Sonar.get().getConfig().getVerification().getMap().getMaxTries();
checkFrame(++totalReceivedPackets < maxPackets, "too many packets");
// We are expecting a code in chat, drop all other packets
if (user.getState() == CAPTCHA) {
handlePacketsCAPTCHA(packet);
return;
}
if (packet instanceof PaddleBoatPacket) {
// Check if we are currently expecting a PaddleBoatPacket packet
assertState(VEHICLE);
// Only pass the player if both checks are passed
if (receivedPlayerInput && receivedSteerBoat) {
captchaOrNext();
}
// Mark paddle boat check as passed
receivedSteerBoat = true;
}
if (packet instanceof PlayerInputPacket) {
// Check if we are currently expecting a PlayerInputPacket packet
assertState(VEHICLE);
final PlayerInputPacket inputPacket = (PlayerInputPacket) packet;
// Make sure the values sent by the client are legitimate
checkFrame(inputPacket.getForward() <= 0.98f, "invalid vehicle speed (f)");
checkFrame(inputPacket.getSideways() <= 0.98f, "invalid vehicle speed (s)");
// Don't verify if the player tries unmounting or jumping
checkFrame(!inputPacket.isJump(), "tried to jump in vehicle");
checkFrame(!inputPacket.isUnmount(), "tried to escape the vehicle");
// Only pass the player if both checks are passed
if (receivedPlayerInput && receivedSteerBoat) {
captchaOrNext();
}
// pre-1.9 clients do not have a PaddleBoat packet
if (user.getProtocolVersion().compareTo(MINECRAFT_1_9) < 0) {
receivedSteerBoat = true;
} else if (!user.isGeyser()) {
// PaddleBoat → PlayerInput → PaddleBoat
checkFrame(receivedSteerBoat, "invalid vehicle steering order");
}
// Mark player input check as passed
receivedPlayerInput = true;
return;
}
if (packet instanceof LoginAcknowledgedPacket) {
// Check if we are currently expecting a LoginAcknowledged packet
assertState(LOGIN_ACK);
// Start the configuration process for 1.20.2 clients
configure();
return;
}
if (packet instanceof FinishConfigurationPacket) {
// Check if we are currently expecting a FinishConfiguration packet
assertState(CONFIGURE);
// Check if the client has already sent valid ClientSettings and PluginMessage packets
if (!user.isGeyser()) {
checkFrame(resolvedClientBrand, "did not resolve client brand");
checkFrame(resolvedClientSettings, "did not resolve client settings");
}
// Start initializing the actual join process
updateEncoderDecoderState(FallbackPacketRegistry.GAME);
initialJoinProcess();
return;
}
if (packet instanceof KeepAlivePacket) {
final KeepAlivePacket keepAlive = (KeepAlivePacket) packet;
// 1.7-1.8.9 are sending a KeepAlive packet with the ID 0 every 20 ticks
if (keepAlive.getId() == 0 && user.getProtocolVersion().compareTo(MINECRAFT_1_8) <= 0) return;
// Check if we are currently expecting a KeepAlive packet
assertState(KEEP_ALIVE);
checkFrame(keepAlive.getId() == expectedKeepAliveId, "invalid KeepAlive ID");
// The correct KeepAlive packet has been received
sendJoinGamePacket();
return;
}
if (packet instanceof ClientSettingsPacket) {
final ClientSettingsPacket clientSettings = (ClientSettingsPacket) packet;
// Validate the locale using a regex to filter unwanted characters.
checkFrame(validateClientLocale(user, clientSettings.getLocale()), "invalid locale");
// Clients sometimes mess up the ClientSettings or PluginMessage packet.
// Probably caused by some 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;
final boolean usingModernChannel = pluginMessage.getChannel().equals(BRAND_CHANNEL);
final boolean usingLegacyChannel = pluginMessage.getChannel().equals(BRAND_CHANNEL_LEGACY);
// Only the brand channel is important, drop the rest
if (usingModernChannel || usingLegacyChannel) {
// 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(usingLegacyChannel || v1_13, "invalid channel");
// Validate the client branding using a regex to filter unwanted characters.
if (Sonar.get().getConfig().getVerification().getBrand().isEnabled()) {
checkFrame(validateClientBrand(user, pluginMessage.getSlicedBuffer()), "invalid client brand");
}
// Clients sometimes mess up the ClientSettings or PluginMessage packet.
if (user.getState() == PLUGIN_MESSAGE) {
// Send the transaction packet
sendTransaction();
}
// Make sure we mark the PluginMessage as valid
resolvedClientBrand = true;
}
return;
}
if (packet instanceof TransactionPacket) {
// Check if we are currently expecting a Transaction packet
assertState(TRANSACTION);
final TransactionPacket transaction = (TransactionPacket) packet;
// The transaction should always be accepted
checkFrame(transaction.isAccepted(), "transaction not accepted?!");
// Check if the transaction ID is valid
checkFrame(transaction.getTransactionId() == expectedTransactionId, "invalid transaction id");
// First, send an Abilities packet to the client to make
// sure the player falls even in spectator mode.
// Then, teleport the player to the spawn position.
sendAbilitiesAndTeleport();
// 1.7-1.8 clients do not have a TeleportConfirm packet
if (user.getProtocolVersion().compareTo(MINECRAFT_1_8) <= 0) {
// Immediately send the chunk data as 1.7-1.8 can't confirm teleport packets
sendChunkData();
} else {
// Send all previously sent packets in one flush since we didn't flush
// the channel earlier when we teleported the player.
user.getChannel().flush();
}
return;
}
if (packet instanceof TeleportConfirmPacket) {
// Check if we are currently expecting a TeleportConfirm packet
assertState(TELEPORT);
// Check if the teleport ID is correct
final TeleportConfirmPacket teleportConfirm = (TeleportConfirmPacket) packet;
checkFrame(teleportConfirm.getTeleportId() == expectedTeleportId, "invalid teleport ID");
// Reset all values to ensure safety on teleport
tick = 1;
posY = lastY = expectedTeleportId = -1;
// Now we can send the chunk data
sendChunkData();
return;
}
// Only handle position packets if we aren't in certain phases
if (user.getState() != LOGIN_ACK && user.getState() != VEHICLE) {
if (packet instanceof PlayerPositionPacket) {
final PlayerPositionPacket position = (PlayerPositionPacket) packet;
handlePositionUpdate(position.getX(), position.getY(), position.getZ(), position.isOnGround());
return;
}
if (packet instanceof PlayerPositionLookPacket) {
final PlayerPositionLookPacket position = (PlayerPositionLookPacket) packet;
handlePositionUpdate(position.getX(), position.getY(), position.getZ(), position.isOnGround());
return;
}
if (packet instanceof PlayerPositionIdlePacket) {
final PlayerPositionIdlePacket player = (PlayerPositionIdlePacket) packet;
handlePositionUpdate(posX, posY, posZ, player.isOnGround());
}
}
}
private void handlePositionUpdate(final double x, final double y, final double z, final boolean ground) {
try {
// 1.8 does not have a TeleportConfirm packet, so we need to handle the spawning differently
if (user.getProtocolVersion().compareTo(MINECRAFT_1_8) <= 0
&& expectedTeleportId != -1 // Check if the teleport ID is currently unset
// Then, check if the position is equal to the spawn position
&& x == SPAWN_X_POSITION && y == spawnYPosition && z == SPAWN_Z_POSITION) {
// Reset all values to ensure safety on teleport
tick = 1;
posY = expectedTeleportId = -1;
// Check for ground state in the first packet
checkFrame(!ground, "invalid ground state");
}
posX = x;
lastY = posY;
posY = y;
posZ = z;
// The player is not allowed to move away from the collision platform.
// This should not happen unless the max movement tick is configured to a high number.
checkFrame(Math.abs(x - BLOCKS_PER_ROW) < BLOCKS_PER_ROW, "moved too far (x)");
checkFrame(Math.abs(z - BLOCKS_PER_ROW) < BLOCKS_PER_ROW, "moved too far (z)");
// Check if the client hasn't moved before sending the first movement packet
if (!listenForMovements) {
if (posX == SPAWN_X_POSITION && posZ == SPAWN_Z_POSITION) {
// Now, once we verified the X and Z position, we can safely check for gravity
listenForMovements = true;
}
lastY = spawnYPosition;
return;
}
final double deltaY = lastY - y;
// The deltaY is 0 whenever the player sends their first position packet.
// We have to account for this or the player will fail the verification.
if (deltaY == 0) return;
// Calculate the difference between the player's Y coordinate and the expected Y coordinate
double collisionOffsetY = (DEFAULT_Y_COLLIDE_POSITION + blockType.getBlockHeight()) - y;
if (user.getProtocolVersion().compareTo(MINECRAFT_1_8) < 0) {
collisionOffsetY += 1.62f; // 1.7 is weird and sends the head position instead of the AABB minY
}
// Check if the player is colliding by performing a basic Y offset check.
// The offset cannot be greater than 0 since the blocks will not let the player fall through them.
checkFrame(collisionOffsetY <= 0, "fell through blocks: " + collisionOffsetY);
if (tick > maxMovementTick) {
// Log/debug position if enabled in the configuration
if (Sonar.get().getConfig().getVerification().isDebugXYZPositions()) {
FALLBACK.getLogger().info("{}: {}/{}/{} - deltaY: {} - ground: {} - collision: {}",
username, x, y, z, deltaY, ground, collisionOffsetY);
}
// If the collision check is disabled, finish verification
if (!performCollisions) {
vehicleCheckOrNext();
return;
}
// Perform the collision check
if (ground) {
// Make sure the player is actually colliding with the blocks and not only spoofing ground
checkFrame(collisionOffsetY > -0.03, "illegal collision: " + collisionOffsetY);
// The player is colliding to blocks, finish verification
vehicleCheckOrNext();
return;
} else {
// Make sure the player is colliding with blocks but is sending an invalid ground state
checkFrame(collisionOffsetY != 0, "no ground: " + collisionOffsetY);
}
} else {
// Check if the player is spoofing the ground state
checkFrame(!ground, "spoofed ground state");
}
// Make sure we don't run out of predicted Y motions
checkFrame(tick < preparedCachedYMotions.length, "too many movements");
if (!ground && performGravity) {
final double predictedY = preparedCachedYMotions[tick];
final double offsetY = Math.abs(deltaY - predictedY);
// Log/debug position if enabled in the configuration
if (Sonar.get().getConfig().getVerification().isDebugXYZPositions()) {
FALLBACK.getLogger().info("{}: {}/{}/{} - deltaY: {} - prediction: {} - offset: {}",
username, x, y, z, deltaY, predictedY, offsetY);
}
// Check if the y motion is roughly equal to the predicted value
checkFrame(offsetY < 5e-3, String.format("invalid gravity: %d, %.7f, %.10f, %.10f != %.10f",
tick, y, offsetY, deltaY, predictedY));
}
tick++;
} catch (CorruptedFrameException exception) {
// Do not throw the exception if the user configured to display the CAPTCHA instead
if (Sonar.get().getConfig().getVerification().getGravity().isCaptchaOnFail()) {
handleCAPTCHA();
return;
}
// Interrupt the netty eventLoop to immediately stop further processing
throw new DecoderException(exception);
}
}
// TODO: Maybe recode the flow with a better state system?
private void vehicleCheckOrNext() {
if (performVehicle) {
// Initialize the vehicle check
handleVehicleCheck();
return;
}
captchaOrNext();
}
private void captchaOrNext() {
if (performCaptcha) {
// Initialize the map captcha
handleCAPTCHA();
return;
}
// Finish the verification
finish();
}
private void handleVehicleCheck() {
// Set the state to VEHICLE, so we don't handle any unnecessary packets
user.setState(VEHICLE);
// Send the necessary packets to mount the player on the vehicle
final int vehicleType = EntityType.BOAT.getId(user.getProtocolVersion());
user.delayedWrite(new SpawnEntityPacket(VEHICLE_ENTITY_ID, vehicleType, posX, posY, posZ));
user.delayedWrite(setPassengers);
user.getChannel().flush();
}
private void handleCAPTCHA() {
// Set the state to CAPTCHA, so we don't handle any unnecessary packets
user.setState(CAPTCHA);
if (!MAP_INFO_PREPARER.isCaptchaAvailable()) {
// This should not happen, but we have to return if there is no captcha prepared
user.disconnect(Sonar.get().getConfig().getVerification().getCurrentlyPreparing());
return;
}
if (!performGravity
&& user.getProtocolVersion().compareTo(MINECRAFT_1_18_2) >= 0) {
// Make sure the player escapes the 1.18.2+ "Loading terrain" screen
user.delayedWrite(CAPTCHA_SPAWN_POSITION);
}
if (performVehicle) {
// Make sure the player exits the vehicle before sending the CAPTCHA
user.delayedWrite(removeEntities);
}
// Reset max tries
captchaTriesLeft = Sonar.get().getConfig().getVerification().getMap().getMaxTries();
// Set slot to map
user.delayedWrite(new SetSlotPacket(36, 1,
ItemType.FILLED_MAP.getId(user.getProtocolVersion()), SetSlotPacket.MAP_NBT));
// Send random captcha to the player
captcha = MAP_INFO_PREPARER.getRandomCaptcha();
Objects.requireNonNull(captcha).delayedWrite(user);
// Teleport the player to the position above the platform
user.delayedWrite(CAPTCHA_POSITION);
// Make sure the player cannot move
user.delayedWrite(user.isGeyser() ? CAPTCHA_ABILITIES_BEDROCK : CAPTCHA_ABILITIES);
// Make sure the player knows what to do
user.delayedWrite(enterCodeMessage);
// Send all packets in one flush
user.getChannel().flush();
}
private void finish() {
// Make sure we drop all further packets
user.setState(SUCCESS);
// Increment amount of total successful verifications
GlobalSonarStatistics.totalSuccessfulVerifications++;
// Add verified player to the database
final VerifiedPlayer verifiedPlayer = new VerifiedPlayer(user.getInetAddress().toString(), playerUuid,
login.getStart());
Sonar.get().getVerifiedPlayerController().add(verifiedPlayer);
// Call the VerifySuccessEvent for external API usage
Sonar.get().getEventManager().publish(new UserVerifySuccessEvent(username, playerUuid, user, login.delay()));
// If enabled, transfer the player back to the origin server
// This feature was introduced by Mojang in Minecraft version 1.20.5
// Thanks a lot to @Kadeluxe for helping me out with this!
if (transfer) {
user.write(transferToOrigin);
}
// Disconnect player with the verification success message
user.disconnect(Sonar.get().getConfig().getVerification().getVerificationSuccess());
FALLBACK.getLogger().info(Sonar.get().getConfig().getVerification().getSuccessLog()
.replace("%name%", username)
.replace("%time%", login.toString()));
}
/**
* Fails the verification if a certain state is unexpected.
*
* @param expectedState Expected state
*/
private void assertState(final @NotNull FallbackUserState expectedState) {
final FallbackUserState state = user.getState();
checkFrame(state == expectedState, "expected " + expectedState + ", got " + state);
}
/**
* Checks if a certain condition is met, fails the verification if not.
*
* @param condition Condition to fail if it's false
* @param message Messages displayed in the stacktrace
*/
private void checkFrame(final boolean condition, final @NotNull String message) {
if (!condition) {
// Only fail the player if we are either not in the POSITION state,
// or if the user configured Sonar to display a CAPTCHA instead of failing.
if (user.getState() != POSITION
|| !Sonar.get().getConfig().getVerification().getGravity().isCaptchaOnFail()) {
user.fail(message);
}
throw new CorruptedFrameException(message);
}
}
}
@@ -20,10 +20,7 @@ package xyz.jonesdev.sonar.common.fallback.protocol;
import io.netty.buffer.ByteBuf;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import java.util.UUID;
public interface FallbackPacket {
UUID EMPTY_UUID = new UUID(0L, 0L);
/**
* Encodes the packet sent by the server
@@ -21,8 +21,9 @@ import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.handler.codec.CorruptedFrameException;
import lombok.Setter;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_20_2;
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry.Direction.SERVERBOUND;
@@ -31,19 +32,18 @@ import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.readVarInt;
public final class FallbackPacketDecoder extends ChannelInboundHandlerAdapter {
private final FallbackUser user;
private final FallbackPacketListener listener;
private final ProtocolVersion protocolVersion;
private FallbackPacketRegistry.ProtocolRegistry registry;
@Setter
private FallbackPacketListener listener;
public FallbackPacketDecoder(final @NotNull FallbackUser user,
final @NotNull FallbackPacketListener listener) {
this.user = user;
this.listener = listener;
updateRegistry(user.getProtocolVersion().compareTo(MINECRAFT_1_20_2) >= 0 ? LOGIN : GAME);
public FallbackPacketDecoder(final @NotNull ProtocolVersion protocolVersion) {
this.protocolVersion = protocolVersion;
updateRegistry(protocolVersion.compareTo(MINECRAFT_1_20_2) >= 0 ? LOGIN : GAME);
}
public void updateRegistry(final @NotNull FallbackPacketRegistry registry) {
this.registry = registry.getProtocolRegistry(SERVERBOUND, user.getProtocolVersion());
this.registry = registry.getProtocolRegistry(SERVERBOUND, protocolVersion);
}
@Override
@@ -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 @NotNull ByteBuf byteBuf,
final @NotNull FallbackPacket packet) throws Exception {
final int expectedMaxLen = packet.expectedMaxLength(byteBuf, user.getProtocolVersion());
private void checkPacketSize(final @NotNull ByteBuf byteBuf,
final @NotNull FallbackPacket packet) throws Exception {
final int expectedMaxLen = packet.expectedMaxLength(byteBuf, protocolVersion);
if (expectedMaxLen != -1 && byteBuf.readableBytes() > expectedMaxLen) {
user.fail("packet too large (" + byteBuf.readableBytes() + " bytes)");
throw new CorruptedFrameException("Packet too large");
}
final int expectedMinLen = packet.expectedMinLength(byteBuf, user.getProtocolVersion());
if (byteBuf.readableBytes() < expectedMinLen) {
user.fail("packet too small (" + byteBuf.readableBytes() + " bytes)");
final int expectedMinLen = packet.expectedMinLength(byteBuf, protocolVersion);
if (expectedMinLen != -1 && byteBuf.readableBytes() < expectedMinLen) {
throw new CorruptedFrameException("Packet too small");
}
}
@@ -19,11 +19,13 @@ package xyz.jonesdev.sonar.common.fallback.protocol;
import io.netty.util.collection.IntObjectHashMap;
import io.netty.util.collection.IntObjectMap;
import lombok.AccessLevel;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.config.FinishConfigurationPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.config.RegistryDataPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.configuration.FinishConfigurationPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.configuration.RegistryDataPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.login.LoginAcknowledgedPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.login.LoginSuccessPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.*;
@@ -38,14 +40,13 @@ import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
public enum FallbackPacketRegistry {
LOGIN {
{
serverbound.register(LoginAcknowledgedPacket.class, LoginAcknowledgedPacket::new,
map(0x03, MINECRAFT_1_20_2, false));
clientbound.register(LoginSuccessPacket.class, LoginSuccessPacket::new,
map(0x02, MINECRAFT_1_7_2, false));
clientbound.register(DisconnectPacket.class, DisconnectPacket::new,
map(0x00, MINECRAFT_1_7_2, true));
clientbound.register(LoginSuccessPacket.class, LoginSuccessPacket::new,
map(0x02, MINECRAFT_1_7_2, true));
serverbound.register(LoginAcknowledgedPacket.class, LoginAcknowledgedPacket::new,
map(0x03, MINECRAFT_1_20_2, false));
}
},
CONFIG {
@@ -59,12 +60,11 @@ public enum FallbackPacketRegistry {
clientbound.register(KeepAlivePacket.class, KeepAlivePacket::new,
map(0x03, MINECRAFT_1_20_2, false),
map(0x04, MINECRAFT_1_20_5, false));
clientbound.register(RegistryDataPacket.class, RegistryDataPacket::new,
map(0x05, MINECRAFT_1_20_2, true),
map(0x07, MINECRAFT_1_20_5, true));
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),
@@ -124,7 +124,7 @@ public enum FallbackPacketRegistry {
map(0x1A, MINECRAFT_1_19_4, true),
map(0x1B, MINECRAFT_1_20_2, true),
map(0x1D, MINECRAFT_1_20_5, true));
clientbound.register(PlayerPositionLookPacket.class, PlayerPositionLookPacket::new,
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),
@@ -140,7 +140,7 @@ public enum FallbackPacketRegistry {
map(0x3C, MINECRAFT_1_19_4, true),
map(0x3E, MINECRAFT_1_20_2, true),
map(0x40, MINECRAFT_1_20_5, true));
clientbound.register(ClientAbilitiesPacket.class, ClientAbilitiesPacket::new,
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),
@@ -208,6 +208,22 @@ public enum FallbackPacketRegistry {
map(0x0E, MINECRAFT_1_7_2, true),
map(0x00, MINECRAFT_1_9, true),
map(0x01, MINECRAFT_1_19_4, true));
clientbound.register(PlayerInfoPacket.class, PlayerInfoPacket::new,
map(0x38, MINECRAFT_1_7_2, true),
map(0x2D, MINECRAFT_1_9, true),
map(0x2E, MINECRAFT_1_12_1, true),
map(0x30, MINECRAFT_1_13, true),
map(0x33, MINECRAFT_1_14, true),
map(0x34, MINECRAFT_1_15, true),
map(0x33, MINECRAFT_1_16, true),
map(0x32, MINECRAFT_1_16_2, true),
map(0x36, MINECRAFT_1_17, true),
map(0x34, MINECRAFT_1_19, true),
map(0x37, MINECRAFT_1_19_1, true),
map(0x36, MINECRAFT_1_19_3, true),
map(0x3A, MINECRAFT_1_19_4, true),
map(0x3C, MINECRAFT_1_20_2, true),
map(0x3E, MINECRAFT_1_20_5, true));
clientbound.register(UpdateSectionBlocksPacket.class, UpdateSectionBlocksPacket::new,
map(0x22, MINECRAFT_1_7_2, true),
map(0x10, MINECRAFT_1_9, true),
@@ -237,7 +253,7 @@ public enum FallbackPacketRegistry {
map(0x32, MINECRAFT_1_19_4, true),
map(0x33, MINECRAFT_1_20_2, true),
map(0x35, MINECRAFT_1_20_5, true));
clientbound.register(DefaultSpawnPositionPacket.class, DefaultSpawnPositionPacket::new,
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),
@@ -268,7 +284,7 @@ public enum FallbackPacketRegistry {
map(0x29, MINECRAFT_1_19_4, true),
map(0x2A, MINECRAFT_1_20_2, true),
map(0x2C, MINECRAFT_1_20_5, true));
clientbound.register(SetSlotPacket.class, SetSlotPacket::new,
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),
@@ -296,7 +312,7 @@ public enum FallbackPacketRegistry {
map(0x58, MINECRAFT_1_20_2, true),
map(0x5A, MINECRAFT_1_20_3, true),
map(0x5C, MINECRAFT_1_20_5, true));
clientbound.register(UniversalChatPacket.class, UniversalChatPacket::new,
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),
@@ -315,7 +331,37 @@ public enum FallbackPacketRegistry {
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),
@@ -346,7 +392,7 @@ public enum FallbackPacketRegistry {
map(0x22, MINECRAFT_1_20_2, false),
map(0x23, MINECRAFT_1_20_3, false),
map(0x26, MINECRAFT_1_20_5, false));
serverbound.register(UniversalChatPacket.class, UniversalChatPacket::new,
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),
@@ -372,7 +418,7 @@ public enum FallbackPacketRegistry {
map(0x14, MINECRAFT_1_20_2, false),
map(0x15, MINECRAFT_1_20_3, false),
map(0x18, MINECRAFT_1_20_5, false));
serverbound.register(ClientSettingsPacket.class, ClientSettingsPacket::new,
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),
@@ -396,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),
@@ -413,9 +460,9 @@ public enum FallbackPacketRegistry {
map(0x17, MINECRAFT_1_20_2, false),
map(0x18, MINECRAFT_1_20_3, false),
map(0x1B, MINECRAFT_1_20_5, false));
serverbound.register(TeleportConfirmPacket.class, TeleportConfirmPacket::new,
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),
@@ -431,7 +478,7 @@ public enum FallbackPacketRegistry {
map(0x16, MINECRAFT_1_20_2, false),
map(0x17, MINECRAFT_1_20_3, false),
map(0x1A, MINECRAFT_1_20_5, false));
serverbound.register(PlayerPositionIdlePacket.class, PlayerPositionIdlePacket::new,
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),
@@ -511,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) {
@@ -520,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) {
@@ -545,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);
@@ -589,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);
}
}
@@ -18,16 +18,19 @@
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.BlockType;
import xyz.jonesdev.sonar.common.fallback.protocol.block.BlockUpdate;
import xyz.jonesdev.sonar.common.fallback.protocol.captcha.CaptchaPreparer;
import xyz.jonesdev.sonar.common.fallback.protocol.dimension.DimensionRegistry;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.config.FinishConfigurationPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.config.RegistryDataPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.configuration.FinishConfigurationPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.configuration.RegistryDataPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.login.LoginSuccessPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.*;
import xyz.jonesdev.sonar.common.util.ComponentHolder;
import java.util.UUID;
import java.util.concurrent.ThreadLocalRandom;
@@ -38,9 +41,9 @@ 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 FallbackPacketSnapshot(new ClientAbilitiesPacket(0x00, 0f, 0f));
public final FallbackPacket CAPTCHA_ABILITIES = new FallbackPacketSnapshot(new ClientAbilitiesPacket(0x02, 0f, 0f));
public final FallbackPacket CAPTCHA_ABILITIES_BEDROCK = new FallbackPacketSnapshot(new ClientAbilitiesPacket(0x06, 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 FallbackPacketSnapshot(new ChunkDataPacket(0, 0));
// Finish Configuration
@@ -48,6 +51,7 @@ public class FallbackPreparer {
// Synchronize Registry
public final FallbackPacket REGISTRY_SYNC_LEGACY = new FallbackPacketSnapshot(new RegistryDataPacket(DimensionRegistry.CODEC_1_20, null, null));
public final FallbackPacket[] REGISTRY_SYNC_1_20_5 = RegistryDataPacket.of(DimensionRegistry.CODEC_1_20);
public final FallbackPacket[] REGISTRY_SYNC_1_21 = RegistryDataPacket.of(DimensionRegistry.CODEC_1_21);
// Keep Alive
public final FallbackPacket CAPTCHA_KEEP_ALIVE = new FallbackPacketSnapshot(new KeepAlivePacket(-1337L));
// Game Event (1.20.3+)
@@ -60,10 +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 FallbackPacket transferToOrigin;
public static FallbackPacket transferToOrigin;
// Disconnect messages
public FallbackPacket blacklisted;
@@ -76,7 +86,7 @@ public class FallbackPreparer {
// Vehicle
public FallbackPacket removeEntities;
public FallbackPacket setPassengers;
public static FallbackPacket setPassengers;
public static final int VEHICLE_ENTITY_ID = PLAYER_ENTITY_ID + 1;
// Collisions
@@ -86,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 FallbackPacketSnapshot(new PlayerPositionLookPacket(
public final FallbackPacket CAPTCHA_POSITION = new FallbackPacketSnapshot(new SetPlayerPositionRotation(
SPAWN_X_POSITION, 1337, SPAWN_Z_POSITION, 0f, 90f, 0, false));
public final FallbackPacket CAPTCHA_SPAWN_POSITION = new FallbackPacketSnapshot(new DefaultSpawnPositionPacket(
SPAWN_X_POSITION, 1337, SPAWN_Z_POSITION));
// Blocks
private final BlockUpdate[] CHANGED_BLOCKS = new BlockUpdate[BLOCKS_PER_ROW * BLOCKS_PER_ROW];
// Platform
public BlockType blockType = BlockType.BARRIER;
public int maxMovementTick, dynamicSpawnYPosition;
public double[] preparedCachedYMotions;
public double maxFallDistance;
@@ -105,13 +111,18 @@ public class FallbackPreparer {
// XP packets
public FallbackPacket[] xpCountdown;
@SuppressWarnings("ResultOfMethodCallIgnored")
public void prepare() {
// 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 JoinGamePacket(PLAYER_ENTITY_ID,
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;
@@ -126,10 +137,15 @@ public class FallbackPreparer {
// Set the dynamic block and collide Y position based on the maximum fall distance
dynamicSpawnYPosition = DEFAULT_Y_COLLIDE_POSITION + (int) Math.ceil(maxFallDistance);
dynamicSpawnPosition = new FallbackPacketSnapshot(new DefaultSpawnPositionPacket(
defaultSpawnPosition = new FallbackPacketSnapshot(new 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++) {
@@ -138,12 +154,11 @@ public class FallbackPreparer {
DEFAULT_Y_COLLIDE_POSITION,
z + (BLOCKS_PER_ROW / 2),
0, 0);
CHANGED_BLOCKS[index++] = new BlockUpdate(position, blockType);
changedBlocks[index++] = new BlockUpdate(position, blockType);
}
}
// Prepare UpdateSectionBlocks packet
updateSectionBlocks = new FallbackPacketSnapshot(new UpdateSectionBlocksPacket(0, 0, CHANGED_BLOCKS));
updateSectionBlocks = new FallbackPacketSnapshot(new UpdateSectionBlocksPacket(0, 0, changedBlocks));
// Prepare disconnect packets during login
blacklisted = new FallbackPacketSnapshot(DisconnectPacket.create(Sonar.get().getConfig().getVerification().getBlacklisted(), true));
@@ -155,9 +170,13 @@ public class FallbackPreparer {
tooManyOnlinePerIP = new FallbackPacketSnapshot(DisconnectPacket.create(Sonar.get().getConfig().getTooManyOnlinePerIp(), true));
// Prepare transfer packet
transferToOrigin = new FallbackPacketSnapshot(new TransferPacket(
Sonar.get().getConfig().getVerification().getTransfer().getHost(),
Sonar.get().getConfig().getVerification().getTransfer().getPort()));
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 FallbackPacketSnapshot(new RemoveEntitiesPacket(VEHICLE_ENTITY_ID));
@@ -166,10 +185,14 @@ public class FallbackPreparer {
if (Sonar.get().getConfig().getVerification().getMap().getTiming() != SonarConfiguration.Verification.Timing.NEVER
|| Sonar.get().getConfig().getVerification().getGravity().isCaptchaOnFail()) {
// Prepare CAPTCHA messages
enterCodeMessage = new FallbackPacketSnapshot(new UniversalChatPacket(
Sonar.get().getConfig().getVerification().getMap().getEnterCode(), UniversalChatPacket.SYSTEM_TYPE));
incorrectCaptcha = new FallbackPacketSnapshot(new UniversalChatPacket(
Sonar.get().getConfig().getVerification().getMap().getFailedCaptcha(), UniversalChatPacket.SYSTEM_TYPE));
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];
@@ -181,6 +204,11 @@ public class FallbackPreparer {
// Prepare CAPTCHA answers
MAP_INFO_PREPARER.prepare();
} else {
// Throw away if not needed
enterCodeMessage = null;
incorrectCaptcha = null;
xpCountdown = null;
}
}
}
@@ -79,6 +79,7 @@ public enum BlockType {
case MINECRAFT_1_20_2:
case MINECRAFT_1_20_3:
case MINECRAFT_1_20_5:
case MINECRAFT_1_21:
return 10366;
}
}, 1D);
@@ -73,6 +73,7 @@ public enum ItemType {
case MINECRAFT_1_20_3:
return 979;
case MINECRAFT_1_20_5:
case MINECRAFT_1_21:
return 982;
}
});
@@ -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);
@@ -78,6 +78,7 @@ public enum EntityType {
case MINECRAFT_1_20_3:
return 9;
case MINECRAFT_1_20_5:
case MINECRAFT_1_21:
return 10;
}
});
@@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.protocol.packets.config;
package xyz.jonesdev.sonar.common.fallback.protocol.packets.configuration;
import io.netty.buffer.ByteBuf;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
@@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.protocol.packets.config;
package xyz.jonesdev.sonar.common.fallback.protocol.packets.configuration;
import io.netty.buffer.ByteBuf;
import lombok.*;
@@ -34,7 +34,7 @@ import static xyz.jonesdev.sonar.common.util.ProtocolUtil.readVarInt;
@ToString
@NoArgsConstructor
@AllArgsConstructor
public final class ClientSettingsPacket implements FallbackPacket {
public final class ClientInformationPacket implements FallbackPacket {
private String locale;
private byte viewDistance;
private int chatVisibility;
@@ -31,7 +31,7 @@ import static xyz.jonesdev.sonar.common.util.ProtocolUtil.readVarInt;
@ToString
@NoArgsConstructor
@AllArgsConstructor
public final class TeleportConfirmPacket implements FallbackPacket {
public final class ConfirmTeleportationPacket implements FallbackPacket {
private int teleportId;
@Override
@@ -30,7 +30,7 @@ import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
@ToString
@NoArgsConstructor
@AllArgsConstructor
public final class ClientAbilitiesPacket implements FallbackPacket {
public final class PlayerAbilitiesPacket implements FallbackPacket {
private int encodedFlags;
private float flySpeed, walkSpeed;
@@ -42,7 +42,7 @@ public final class ClientAbilitiesPacket implements FallbackPacket {
}
@Override
public void decode(final @NotNull ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
throw new UnsupportedOperationException();
}
}
@@ -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
}
}
@@ -18,6 +18,7 @@
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;
@@ -25,14 +26,16 @@ import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_8;
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.readRetainedByteBufSlice17;
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) {
@@ -41,13 +44,23 @@ public final class PluginMessagePacket implements FallbackPacket {
@Override
public void decode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) throws Exception {
channel = readString(byteBuf, 64);
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
@@ -35,7 +35,7 @@ import static xyz.jonesdev.sonar.common.util.ProtocolUtil.*;
@ToString
@NoArgsConstructor
@AllArgsConstructor
public final class SetSlotPacket implements FallbackPacket {
public final class SetContainerSlotPacket implements FallbackPacket {
private int slot, count, itemId;
private CompoundBinaryTag compoundBinaryTag;
@@ -33,7 +33,7 @@ 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
@@ -0,0 +1,45 @@
/*
* 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 SetHeldItemPacket implements FallbackPacket {
private int slot;
@Override
public void encode(final @NotNull ByteBuf byteBuf, final ProtocolVersion protocolVersion) throws Exception {
byteBuf.writeByte(slot);
}
@Override
public void decode(final @NotNull ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
slot = byteBuf.readShort();
}
}
@@ -30,7 +30,7 @@ import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
@ToString
@NoArgsConstructor
@AllArgsConstructor
public final class PlayerPositionIdlePacket implements FallbackPacket {
public final class SetPlayerOnGround implements FallbackPacket {
private boolean onGround;
@Override
@@ -32,7 +32,7 @@ import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT
@ToString
@NoArgsConstructor
@AllArgsConstructor
public final class PlayerPositionPacket implements FallbackPacket {
public final class SetPlayerPositionPacket implements FallbackPacket {
private double x, y, z;
private boolean onGround;
@@ -46,7 +46,7 @@ public final class PlayerPositionPacket implements FallbackPacket {
x = byteBuf.readDouble();
y = byteBuf.readDouble();
if (protocolVersion.compareTo(MINECRAFT_1_8) < 0) {
y = byteBuf.readDouble(); // Account for weird y positions on 1.7
y = byteBuf.readDouble(); // Account 1.7 bounding box
}
z = byteBuf.readDouble();
onGround = byteBuf.readBoolean();
@@ -33,7 +33,7 @@ import static xyz.jonesdev.sonar.common.util.ProtocolUtil.writeVarInt;
@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;
@@ -68,7 +68,7 @@ public final class PlayerPositionLookPacket implements FallbackPacket {
x = byteBuf.readDouble();
y = byteBuf.readDouble();
if (protocolVersion.compareTo(MINECRAFT_1_8) < 0) {
y = byteBuf.readDouble(); // Account for weird y positions on 1.7
y = byteBuf.readDouble(); // Account 1.7 bounding box
}
z = byteBuf.readDouble();
yaw = byteBuf.readFloat();
@@ -26,6 +26,8 @@ import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
import java.util.UUID;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.writeUUID;
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.writeVarInt;
@@ -45,7 +47,7 @@ public final class SpawnEntityPacket implements FallbackPacket {
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) {
@@ -22,37 +22,27 @@ 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.util.ComponentHolder;
import java.util.UUID;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
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 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
@@ -62,17 +52,17 @@ public final class UniversalChatPacket implements FallbackPacket {
// 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());
}
}
@@ -88,7 +78,7 @@ public final class UniversalChatPacket implements FallbackPacket {
boolean unsigned = false;
if (saltLong != 0L && signatureBytes.length > 0) {
signature = signatureBytes;
// No need to store the valid signature
} else if ((protocolVersion.compareTo(MINECRAFT_1_19_1) >= 0
|| saltLong == 0L) && signatureBytes.length == 0) {
unsigned = true;
@@ -125,9 +115,6 @@ public final class UniversalChatPacket implements FallbackPacket {
if (signed) {
final byte[] sign = new byte[256];
byteBuf.readBytes(sign);
signature = sign;
} else {
signature = new byte[0];
}
readVarInt(byteBuf);
@@ -55,8 +55,6 @@ public final class TransactionPacket implements FallbackPacket {
if (protocolVersion.compareTo(MINECRAFT_1_17) < 0) {
byteBuf.writeByte(windowId);
byteBuf.writeShort((short) transactionId);
// The "accepted" field is actually really unnecessary since
// it's never even used in the client.
byteBuf.writeBoolean(accepted);
} else {
byteBuf.writeInt(transactionId);
@@ -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);
}
}
}
}
@@ -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);
}
}
}
@@ -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);
}
}
}
@@ -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;
}
}
}
@@ -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.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) {
user.write(transferToOrigin);
// Close the channel to ensure that the connection is closed
user.getChannel().close();
} else {
// Disconnect player with the verification success message
user.disconnect(Sonar.get().getConfig().getVerification().getVerificationSuccess(), false);
}
Sonar.get().getFallback().getLogger().info(
Sonar.get().getConfig().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");
}
}
@@ -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;
}
}
}
@@ -0,0 +1,69 @@
/*
* 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.service;
import lombok.experimental.UtilityClass;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.common.statistics.CachedBandwidthStatistics;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
@UtilityClass
public final class SonarServiceManager {
private final ScheduledExecutorService VERBOSE = createScheduledExecutor("sonar-verbose-thread");
private final ScheduledExecutorService FALLBACK_QUEUE = createScheduledExecutor("sonar-queue-thread");
private final ScheduledExecutorService STATISTICS = createScheduledExecutor("sonar-statistics-thread");
private @NotNull ScheduledExecutorService createScheduledExecutor(final String threadName) {
return Executors.newSingleThreadScheduledExecutor(runnable -> {
final Thread thread = Executors.defaultThreadFactory().newThread(runnable);
thread.setName(threadName);
thread.setDaemon(true);
return thread;
});
}
public void start() {
VERBOSE.scheduleAtFixedRate(() -> {
// Make sure to clean up the cached statistics
// since we don't want to display wrong values.
Sonar.get().getStatistics().cleanUpCache();
Sonar.get().getFallback().getRatelimiter().getFailCountCache().cleanUp();
Sonar.get().getFallback().getBlacklist().cleanUp();
// Update the attack tracker
Sonar.get().getAttackTracker().checkIfUnderAttack();
// Update the action bar verbose
Sonar.get().getVerboseHandler().observe();
}, 0L, 250L, TimeUnit.MILLISECONDS);
FALLBACK_QUEUE.scheduleAtFixedRate(() -> Sonar.get().getFallback().getQueue().poll(),
1L, 1L, TimeUnit.SECONDS);
STATISTICS.scheduleAtFixedRate(CachedBandwidthStatistics::reset,
0L, 1L, TimeUnit.SECONDS);
}
public void stop() {
VERBOSE.shutdown();
FALLBACK_QUEUE.shutdown();
STATISTICS.shutdown();
}
}
@@ -1,42 +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.service;
public final class SonarServiceThread extends Thread {
private final long delay;
private final Runnable runnable;
public SonarServiceThread(final String name, final long delay, final Runnable runnable) {
setName(name);
this.delay = delay;
this.runnable = runnable;
}
@Override
public void run() {
try {
while (!isInterrupted()) {
runnable.run();
// Sleep thread by specified delay
Thread.sleep(delay);
}
} catch (InterruptedException interruptedException) {
//
}
}
}
@@ -1,57 +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.service;
import lombok.experimental.UtilityClass;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.common.statistics.CachedBandwidthStatistics;
@UtilityClass
public final class SonarServiceThreadManager {
private static final SonarServiceThread VERBOSE = new SonarServiceThread("sonar-verbose-thread", 250L,
() -> {
// Make sure to clean up the cached statistics
// since we don't want to display wrong values.
Sonar.get().getStatistics().cleanUpCache();
// Update the attack tracker
Sonar.get().getAttackTracker().checkIfUnderAttack();
// Update the action bar verbose
Sonar.get().getVerboseHandler().update();
});
private static final SonarServiceThread FALLBACK_QUEUE = new SonarServiceThread("sonar-fallback-queue-thread", 1000L,
() -> Sonar.get().getFallback().getQueue().poll());
private static final SonarServiceThread STATISTICS = new SonarServiceThread("sonar-statistics-thread", 1000L,
() -> {
// Clean up bandwidth statistics
CachedBandwidthStatistics.reset();
// Clean up the cache of rate-limited IPs
Sonar.get().getFallback().getRatelimiter().cleanUpCache();
});
public void start() {
VERBOSE.start();
FALLBACK_QUEUE.start();
STATISTICS.start();
}
public void stop() {
VERBOSE.interrupt();
FALLBACK_QUEUE.interrupt();
STATISTICS.interrupt();
}
}
@@ -66,8 +66,6 @@ public final class GlobalSonarStatistics implements SonarStatistics {
public void cleanUpCache() {
LOGINS_PER_SECOND.cleanUp();
CONNECTIONS_PER_SECOND.cleanUp();
// Also clean up the blacklist
Sonar.get().getFallback().getBlacklist().cleanUp();
}
@Override
@@ -17,6 +17,10 @@
package xyz.jonesdev.sonar.common.subcommand.impl;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.command.CommandInvocation;
@@ -25,6 +29,7 @@ import xyz.jonesdev.sonar.api.command.subcommand.SubcommandInfo;
import xyz.jonesdev.sonar.api.command.subcommand.argument.Argument;
import java.net.InetAddress;
import java.net.UnknownHostException;
@SubcommandInfo(
name = "blacklist",
@@ -43,83 +48,116 @@ public final class BlacklistCommand extends Subcommand {
switch (invocation.getRawArguments()[1].toLowerCase()) {
case "add": {
if (invocation.getRawArguments().length <= 2) {
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getIncorrectCommandUsage()
.replace("%usage%", "blacklist add <IP address>"));
incorrectUsage(invocation.getSource(), "blacklist add <IP address>");
return;
}
final String rawInetAddress = invocation.getRawArguments()[2];
final InetAddress inetAddress = getInetAddressIfValid(invocation.getSender(), rawInetAddress);
final String rawAddress = validateIP(invocation.getSource(), invocation.getRawArguments()[2]);
// Make sure the given IP address is valid
if (inetAddress == null) return;
if (rawAddress == null) return;
// Make sure the IP is not blacklisted already
if (SONAR.getFallback().getBlacklist().asMap().containsKey(inetAddress)) {
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getBlacklistDuplicate());
return;
try {
// Try to resolve the IP address, so we can actually do something with it
final InetAddress inetAddress = InetAddress.getAllByName(rawAddress)[0];
// Make sure the IP is not blacklisted already
if (Sonar.get().getFallback().getBlacklist().asMap().containsKey(inetAddress)) {
invocation.getSource().sendMessage(MiniMessage.miniMessage().deserialize(
Sonar.get().getConfig().getMessagesConfig().getString("commands.blacklist.ip-duplicate"),
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix()),
Placeholder.unparsed("ip", rawAddress)));
return;
}
// Display a warning if the IP is verified but being added to the blacklist
if (Sonar.get().getVerifiedPlayerController().has(rawAddress)) {
invocation.getSource().sendMessage(MiniMessage.miniMessage().deserialize(
Sonar.get().getConfig().getMessagesConfig().getString("commands.blacklist.add-warning"),
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix()),
Placeholder.unparsed("ip", rawAddress)));
}
// Blacklist the given IP address
Sonar.get().getFallback().getBlacklist().put(inetAddress, (byte) 0);
invocation.getSource().sendMessage(MiniMessage.miniMessage().deserialize(
Sonar.get().getConfig().getMessagesConfig().getString("commands.blacklist.add"),
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix()),
Placeholder.unparsed("ip", rawAddress)));
} catch (UnknownHostException exception) {
invocation.getSource().sendMessage(Component.text(
"Unexpected error, check console.", NamedTextColor.RED));
exception.printStackTrace(System.err);
}
// Display a warning if the IP is verified but being added to the blacklist
if (SONAR.getVerifiedPlayerController().has(inetAddress.toString())) {
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getBlacklistAddWarning()
.replace("%ip%", rawInetAddress));
}
// Blacklist the given IP address
SONAR.getFallback().getBlacklist().put(inetAddress, (byte) 0);
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getBlacklistAdd()
.replace("%ip%", rawInetAddress));
break;
}
case "remove": {
if (invocation.getRawArguments().length <= 2) {
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getIncorrectCommandUsage()
.replace("%usage%", "blacklist remove <IP address>"));
incorrectUsage(invocation.getSource(), "blacklist remove <IP address>");
return;
}
final String rawInetAddress = invocation.getRawArguments()[2];
final InetAddress inetAddress = getInetAddressIfValid(invocation.getSender(), rawInetAddress);
final String rawAddress = validateIP(invocation.getSource(), invocation.getRawArguments()[2]);
// Make sure the given IP address is valid
if (inetAddress == null) return;
if (rawAddress == null) return;
// Make sure the IP is blacklisted
if (!SONAR.getFallback().getBlacklist().asMap().containsKey(inetAddress)) {
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getBlacklistNotFound());
return;
try {
// Try to resolve the IP address, so we can actually do something with it
final InetAddress inetAddress = InetAddress.getAllByName(rawAddress)[0];
// Make sure the IP is blacklisted
if (!Sonar.get().getFallback().getBlacklist().asMap().containsKey(inetAddress)) {
invocation.getSource().sendMessage(MiniMessage.miniMessage().deserialize(
Sonar.get().getConfig().getMessagesConfig().getString("commands.blacklist.ip-not-found"),
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix())));
return;
}
// Invalidate the cache entry of the blacklisted IP address
Sonar.get().getFallback().getBlacklist().invalidate(inetAddress);
invocation.getSource().sendMessage(MiniMessage.miniMessage().deserialize(
Sonar.get().getConfig().getMessagesConfig().getString("commands.blacklist.remove"),
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix()),
Placeholder.unparsed("ip", rawAddress)));
} catch (UnknownHostException exception) {
invocation.getSource().sendMessage(Component.text(
"Unexpected error, check console.", NamedTextColor.RED));
exception.printStackTrace(System.err);
}
// Invalidate the cache entry of the blacklisted IP address
SONAR.getFallback().getBlacklist().invalidate(inetAddress);
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getBlacklistRemove()
.replace("%ip%", rawInetAddress));
break;
}
case "clear": {
final long blacklisted = SONAR.getFallback().getBlacklist().estimatedSize();
final long blacklistSize = Sonar.get().getFallback().getBlacklist().estimatedSize();
if (blacklisted == 0) {
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getBlacklistEmpty());
if (blacklistSize == 0) {
invocation.getSource().sendMessage(MiniMessage.miniMessage().deserialize(
Sonar.get().getConfig().getMessagesConfig().getString("commands.blacklist.empty"),
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix())));
return;
}
// Invalidate all cache entries
SONAR.getFallback().getBlacklist().invalidateAll();
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getBlacklistCleared()
.replace("%removed%", Sonar.DECIMAL_FORMAT.format(blacklisted)));
Sonar.get().getFallback().getBlacklist().invalidateAll();
invocation.getSource().sendMessage(MiniMessage.miniMessage().deserialize(
Sonar.get().getConfig().getMessagesConfig().getString("commands.blacklist.clear"),
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix()),
Placeholder.unparsed("amount", Sonar.DECIMAL_FORMAT.format(blacklistSize))));
break;
}
case "size": {
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getBlacklistSize()
.replace("%amount%", Sonar.DECIMAL_FORMAT.format(SONAR.getFallback().getBlacklist().estimatedSize())));
final long blacklistSize = Sonar.get().getFallback().getBlacklist().estimatedSize();
invocation.getSource().sendMessage(MiniMessage.miniMessage().deserialize(
Sonar.get().getConfig().getMessagesConfig().getString("commands.blacklist.size"),
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix()),
Placeholder.unparsed("amount", Sonar.DECIMAL_FORMAT.format(blacklistSize))));
break;
}
default: {
incorrectUsage(invocation.getSender());
incorrectUsage(invocation.getSource());
break;
}
}
@@ -20,9 +20,8 @@ package xyz.jonesdev.sonar.common.subcommand.impl;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import lombok.RequiredArgsConstructor;
import lombok.val;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Unmodifiable;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.SonarPlatform;
import xyz.jonesdev.sonar.api.command.CommandInvocation;
import xyz.jonesdev.sonar.api.command.subcommand.Subcommand;
@@ -31,7 +30,6 @@ import xyz.jonesdev.sonar.api.command.subcommand.SubcommandInfo;
import java.lang.management.ManagementFactory;
import java.lang.management.RuntimeMXBean;
import java.util.List;
import java.util.Map;
import java.util.WeakHashMap;
import static xyz.jonesdev.sonar.api.jvm.JVMProcessInformation.*;
@@ -50,17 +48,11 @@ public final class DumpCommand extends Subcommand {
@Override
protected void execute(final @NotNull CommandInvocation invocation) {
final String json = GSON.toJson(collectMappedInformation());
SONAR.getLogger().info("Generated dump: {}", json);
}
@Unmodifiable
private @NotNull Map<String, Object> collectMappedInformation() {
val mappings = new WeakHashMap<String, Object>();
final var mappings = new WeakHashMap<String, Object>();
mappings.put("sonar", new Dump.Sonar(
SONAR.getVersion().getFull(),
SONAR.getPlatform(),
SONAR.getVersion().isOnMainBranch()
Sonar.get().getVersion().getFull(),
Sonar.get().getPlatform(),
Sonar.get().getVersion().isOnMainBranch()
));
mappings.put("runtime", new Dump.Runtime(
getVirtualCores(),
@@ -80,7 +72,7 @@ public final class DumpCommand extends Subcommand {
formatMemory(getFreeMemory()),
formatMemory(getUsedMemory())
));
return mappings;
Sonar.get().getLogger().info("Generated dump: {}", GSON.toJson(mappings));
}
@SuppressWarnings("unused")
@@ -17,7 +17,10 @@
package xyz.jonesdev.sonar.common.subcommand.impl;
import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.command.CommandInvocation;
import xyz.jonesdev.sonar.api.command.subcommand.Subcommand;
import xyz.jonesdev.sonar.api.command.subcommand.SubcommandInfo;
@@ -31,13 +34,17 @@ public final class NotifyCommand extends Subcommand {
@Override
protected void execute(final @NotNull CommandInvocation invocation) {
if (SONAR.getNotificationHandler().isSubscribed(invocation.getSender().getUuid())) {
SONAR.getNotificationHandler().unsubscribe(invocation.getSender().getUuid());
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getNotificationsUnsubscribed());
if (Sonar.get().getNotificationHandler().isSubscribed(invocation.getSource().getUuid())) {
Sonar.get().getNotificationHandler().unsubscribe(invocation.getSource().getUuid());
invocation.getSource().sendMessage(MiniMessage.miniMessage().deserialize(
Sonar.get().getConfig().getMessagesConfig().getString("commands.notify.unsubscribe"),
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix())));
return;
}
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getNotificationsSubscribed());
SONAR.getNotificationHandler().subscribe(invocation.getSender().getUuid());
Sonar.get().getNotificationHandler().subscribe(invocation.getSource().getUuid());
invocation.getSource().sendMessage(MiniMessage.miniMessage().deserialize(
Sonar.get().getConfig().getMessagesConfig().getString("commands.notify.subscribe"),
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix())));
}
}
@@ -17,7 +17,10 @@
package xyz.jonesdev.sonar.common.subcommand.impl;
import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.command.CommandInvocation;
import xyz.jonesdev.sonar.api.command.subcommand.Subcommand;
import xyz.jonesdev.sonar.api.command.subcommand.SubcommandInfo;
@@ -31,12 +34,17 @@ public final class ReloadCommand extends Subcommand {
@Override
protected void execute(final @NotNull CommandInvocation invocation) {
invocation.getSource().sendMessage(MiniMessage.miniMessage().deserialize(
Sonar.get().getConfig().getMessagesConfig().getString("commands.reload.start"),
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix())));
final SystemTimer timer = new SystemTimer();
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getReloading());
SONAR.reload();
Sonar.get().reload();
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getReloaded()
.replace("%taken%", String.valueOf(timer.delay())));
invocation.getSource().sendMessage(MiniMessage.miniMessage().deserialize(
Sonar.get().getConfig().getMessagesConfig().getString("commands.reload.finish"),
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix()),
Placeholder.unparsed("time-taken", String.valueOf(timer.delay()))));
}
}
@@ -18,6 +18,9 @@
package xyz.jonesdev.sonar.common.subcommand.impl;
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.text.minimessage.tag.resolver.TagResolver;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.command.CommandInvocation;
@@ -41,76 +44,88 @@ import static xyz.jonesdev.sonar.api.jvm.JVMProcessInformation.*;
argumentsRequired = false
)
public final class StatisticsCommand extends Subcommand {
private enum StatisticType {
GENERAL,
NETWORK,
MEMORY,
CPU
}
@Override
protected void execute(final @NotNull CommandInvocation invocation) {
StatisticType type = StatisticType.GENERAL;
String type = "general";
if (invocation.getRawArguments().length >= 2) {
try {
type = StatisticType.valueOf(invocation.getRawArguments()[1].toUpperCase());
type = invocation.getRawArguments()[1].toLowerCase();
} catch (Exception exception) {
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getUnknownStatisticType()
.replace("%statistics%", getArguments()));
invocation.getSource().sendMessage(MiniMessage.miniMessage().deserialize(
Sonar.get().getConfig().getMessagesConfig().getString("commands.statistics.unknown-type"),
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix()),
Placeholder.unparsed("statistics", getArguments())));
return;
}
}
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getStatisticsHeader()
.replace("%type%", type.name().toLowerCase()));
invocation.getSender().sendMessage(Component.empty());
invocation.getSource().sendMessage(MiniMessage.miniMessage().deserialize(
Sonar.get().getConfig().getMessagesConfig().getString("commands.statistics.header"),
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix()),
Placeholder.unparsed("statistics-type", type)));
invocation.getSource().sendMessage(Component.empty());
TagResolver.@NotNull Single[] placeholders = null;
switch (type) {
case GENERAL: {
case "general": {
final long seconds = Sonar.get().getLaunchTimer().delay() / 1000L;
final long days = seconds / (24L * 60L * 60L);
final long hours = (seconds % (24L * 60L * 60L)) / (60L * 60L);
final long minutes = (seconds % (60L * 60L)) / 60L;
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getGeneralStatistics()
.replace("%verified%", DECIMAL_FORMAT.format(SONAR.getVerifiedPlayerController().estimatedSize()))
.replace("%verifying%", DECIMAL_FORMAT.format(SONAR.getFallback().getConnected().size()))
.replace("%blacklisted%", DECIMAL_FORMAT.format(SONAR.getFallback().getBlacklist().estimatedSize()))
.replace("%queued%", DECIMAL_FORMAT.format(SONAR.getFallback().getQueue().getQueuedPlayers().size()))
.replace("%uptime%", String.format("%dd %dh %dm %ds", days, hours, minutes, seconds % 60L))
.replace("%total_joins%", DECIMAL_FORMAT.format(Sonar.get().getStatistics().getTotalPlayersJoined()))
.replace("%total_attempts%", DECIMAL_FORMAT.format(Sonar.get().getStatistics().getTotalAttemptedVerifications()))
.replace("%total_failed%", DECIMAL_FORMAT.format(Sonar.get().getStatistics().getTotalFailedVerifications())));
placeholders = new TagResolver.Single[]{
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix()),
Placeholder.unparsed("verified", DECIMAL_FORMAT.format(Sonar.get().getVerifiedPlayerController().estimatedSize())),
Placeholder.unparsed("verifying", DECIMAL_FORMAT.format(Sonar.get().getFallback().getConnected().size())),
Placeholder.unparsed("blacklisted", DECIMAL_FORMAT.format(Sonar.get().getFallback().getBlacklist().estimatedSize())),
Placeholder.unparsed("queued", DECIMAL_FORMAT.format(Sonar.get().getFallback().getQueue().getPlayers().size())),
Placeholder.unparsed("server-uptime", String.format("%dd %dh %dm %ds", days, hours, minutes, seconds % 60L)),
Placeholder.unparsed("total-joins", DECIMAL_FORMAT.format(Sonar.get().getStatistics().getTotalPlayersJoined())),
Placeholder.unparsed("total-attempts", DECIMAL_FORMAT.format(Sonar.get().getStatistics().getTotalAttemptedVerifications())),
Placeholder.unparsed("total-failed", DECIMAL_FORMAT.format(Sonar.get().getStatistics().getTotalFailedVerifications()))
};
break;
}
case CPU: {
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getCpuStatistics()
.replace("%process_cpu%", DECIMAL_FORMAT.format(getProcessCPUUsage()))
.replace("%system_cpu%", DECIMAL_FORMAT.format(getSystemCPUUsage()))
.replace("%average_process_cpu%", DECIMAL_FORMAT.format(getAverageProcessCPUUsage()))
.replace("%average_system_cpu%", DECIMAL_FORMAT.format(getAverageSystemCPUUsage()))
.replace("%load_average%", DECIMAL_FORMAT.format(getSystemLoadAverage()))
.replace("%virtual_cores%", DECIMAL_FORMAT.format(getVirtualCores())));
case "cpu": {
placeholders = new TagResolver.Single[]{
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix()),
Placeholder.unparsed("process-cpu", DECIMAL_FORMAT.format(getProcessCPUUsage())),
Placeholder.unparsed("system-cpu", DECIMAL_FORMAT.format(getSystemCPUUsage())),
Placeholder.unparsed("average-process-cpu", DECIMAL_FORMAT.format(getAverageProcessCPUUsage())),
Placeholder.unparsed("average-system-cpu", DECIMAL_FORMAT.format(getAverageSystemCPUUsage())),
Placeholder.unparsed("virtual-core-count", DECIMAL_FORMAT.format(getVirtualCores()))
};
break;
}
case MEMORY: {
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getMemoryStatistics()
.replace("%free_memory%", formatMemory(getFreeMemory()))
.replace("%used_memory%", formatMemory(getUsedMemory()))
.replace("%max_memory%", formatMemory(getMaxMemory()))
.replace("%total_memory%", formatMemory(getTotalMemory())));
case "memory": {
placeholders = new TagResolver.Single[]{
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix()),
Placeholder.unparsed("free-memory", formatMemory(getFreeMemory())),
Placeholder.unparsed("used-memory", formatMemory(getUsedMemory())),
Placeholder.unparsed("max-memory", formatMemory(getMaxMemory())),
Placeholder.unparsed("total-memory", formatMemory(getTotalMemory()))
};
break;
}
case NETWORK: {
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getNetworkStatistics()
.replace("%incoming%", CachedBandwidthStatistics.INCOMING.getCachedSecond())
.replace("%outgoing%", CachedBandwidthStatistics.OUTGOING.getCachedSecond())
.replace("%ttl_incoming%", CachedBandwidthStatistics.INCOMING.getCachedTtl())
.replace("%ttl_outgoing%", CachedBandwidthStatistics.OUTGOING.getCachedTtl()));
case "network": {
placeholders = new TagResolver.Single[]{
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix()),
Placeholder.unparsed("incoming-traffic", CachedBandwidthStatistics.INCOMING.getCachedSecond()),
Placeholder.unparsed("outgoing-traffic", CachedBandwidthStatistics.OUTGOING.getCachedSecond()),
Placeholder.unparsed("incoming-traffic-ttl", CachedBandwidthStatistics.INCOMING.getCachedTtl()),
Placeholder.unparsed("outgoing-traffic-ttl", CachedBandwidthStatistics.OUTGOING.getCachedTtl())
};
break;
}
}
for (final String msg : Sonar.get().getConfig().getMessagesConfig().getStringList("commands.statistics." + type)) {
invocation.getSource().sendMessage(MiniMessage.miniMessage().deserialize(msg, placeholders));
}
}
}
@@ -18,7 +18,10 @@
package xyz.jonesdev.sonar.common.subcommand.impl;
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 xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.command.CommandInvocation;
import xyz.jonesdev.sonar.api.command.subcommand.Subcommand;
import xyz.jonesdev.sonar.api.command.subcommand.SubcommandInfo;
@@ -32,15 +35,19 @@ public final class VerboseCommand extends Subcommand {
@Override
protected void execute(final @NotNull CommandInvocation invocation) {
if (SONAR.getVerboseHandler().isSubscribed(invocation.getSender().getUuid())) {
SONAR.getVerboseHandler().unsubscribe(invocation.getSender().getUuid());
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getVerboseUnsubscribed());
if (Sonar.get().getVerboseHandler().isSubscribed(invocation.getSource().getUuid())) {
Sonar.get().getVerboseHandler().unsubscribe(invocation.getSource().getUuid());
// Reset ActionBar component when unsubscribing
invocation.getSender().getAudience().sendActionBar(Component.empty());
invocation.getSource().getAudience().sendActionBar(Component.empty());
invocation.getSource().sendMessage(MiniMessage.miniMessage().deserialize(
Sonar.get().getConfig().getMessagesConfig().getString("commands.verbose.unsubscribe"),
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix())));
return;
}
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getVerboseSubscribed());
SONAR.getVerboseHandler().subscribe(invocation.getSender().getUuid());
Sonar.get().getVerboseHandler().subscribe(invocation.getSource().getUuid());
invocation.getSource().sendMessage(MiniMessage.miniMessage().deserialize(
Sonar.get().getConfig().getMessagesConfig().getString("commands.verbose.subscribe"),
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix())));
}
}
@@ -17,6 +17,8 @@
package xyz.jonesdev.sonar.common.subcommand.impl;
import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.command.CommandInvocation;
@@ -25,11 +27,8 @@ import xyz.jonesdev.sonar.api.command.subcommand.SubcommandInfo;
import xyz.jonesdev.sonar.api.command.subcommand.argument.Argument;
import xyz.jonesdev.sonar.api.model.VerifiedPlayer;
import java.net.InetAddress;
import java.nio.charset.StandardCharsets;
import java.util.Queue;
import java.util.UUID;
import java.util.concurrent.LinkedBlockingQueue;
@SubcommandInfo(
name = "verified",
@@ -43,91 +42,77 @@ import java.util.concurrent.LinkedBlockingQueue;
}
)
public final class VerifiedCommand extends Subcommand {
private static final Queue<String> LOCK = new LinkedBlockingQueue<>(1);
@Override
protected void execute(final @NotNull CommandInvocation invocation) {
switch (invocation.getRawArguments()[1].toLowerCase()) {
case "history": {
if (invocation.getRawArguments().length <= 2) {
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getIncorrectCommandUsage()
.replace("%usage%", "verified history <IP address>"));
incorrectUsage(invocation.getSource(), "verified history <IP address>");
return;
}
final String rawInetAddress = invocation.getRawArguments()[2];
final InetAddress inetAddress = getInetAddressIfValid(invocation.getSender(), rawInetAddress);
final String rawAddress = validateIP(invocation.getSource(), invocation.getRawArguments()[2]);
// Make sure the given IP address is valid
if (inetAddress == null) return;
if (rawAddress == null) return;
// Make sure the IP is verified already
if (!SONAR.getVerifiedPlayerController().has(inetAddress.toString())) {
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getVerifiedNotFound());
if (!Sonar.get().getVerifiedPlayerController().has(rawAddress)) {
invocation.getSource().sendMessage(MiniMessage.miniMessage().deserialize(
Sonar.get().getConfig().getMessagesConfig().getString("commands.verified.ip-not-found"),
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix())));
return;
}
invocation.getSender().sendMessage("<yellow>Previous UUIDs for " + rawInetAddress + ":");
for (final UUID uuid : SONAR.getVerifiedPlayerController().getUUIDs(inetAddress.toString())) {
invocation.getSender().sendMessage(" <gray>▪ <white>" + uuid.toString());
invocation.getSource().sendMessage(MiniMessage.miniMessage().deserialize(
Sonar.get().getConfig().getMessagesConfig().getString("commands.verified.history"),
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix()),
Placeholder.unparsed("ip", rawAddress)));
for (final UUID uuid : Sonar.get().getVerifiedPlayerController().getUUIDs(rawAddress)) {
invocation.getSource().sendMessage(MiniMessage.miniMessage().deserialize(
Sonar.get().getConfig().getMessagesConfig().getString("commands.verified.history-entry"),
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix()),
Placeholder.unparsed("uuid", uuid.toString())));
}
break;
}
case "remove": {
if (invocation.getRawArguments().length <= 2) {
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getIncorrectCommandUsage()
.replace("%usage%", "verified remove <IP address>"));
incorrectUsage(invocation.getSource(), "verified remove <IP address>");
return;
}
final String rawInetAddress = invocation.getRawArguments()[2];
// Make sure we aren't currently locking the IP address to avoid a double I/O operation
if (LOCK.contains(rawInetAddress)) {
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getVerifiedBlocked());
return;
}
final InetAddress inetAddress = getInetAddressIfValid(invocation.getSender(), rawInetAddress);
final String rawAddress = validateIP(invocation.getSource(), invocation.getRawArguments()[2]);
// Make sure the given IP address is valid
if (inetAddress == null) return;
if (rawAddress == null) return;
// Make sure the player is verified already
if (!SONAR.getVerifiedPlayerController().has(inetAddress.toString())) {
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getVerifiedNotFound());
if (!Sonar.get().getVerifiedPlayerController().has(rawAddress)) {
invocation.getSource().sendMessage(MiniMessage.miniMessage().deserialize(
Sonar.get().getConfig().getMessagesConfig().getString("commands.verified.ip-not-found"),
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix())));
return;
}
// Lock the IP address
// Make sure we don't accidentally run 2 operations at the same time
LOCK.add(rawInetAddress);
SONAR.getVerifiedPlayerController().remove(inetAddress.toString());
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getVerifiedRemove()
.replace("%ip%", rawInetAddress));
// Unlock the IP address
LOCK.remove(rawInetAddress);
Sonar.get().getVerifiedPlayerController().remove(rawAddress);
invocation.getSource().sendMessage(MiniMessage.miniMessage().deserialize(
Sonar.get().getConfig().getMessagesConfig().getString("commands.verified.remove"),
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix()),
Placeholder.unparsed("ip", rawAddress)));
break;
}
case "add": {
if (invocation.getRawArguments().length <= 3) {
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getIncorrectCommandUsage()
.replace("%usage%", "verified add <IP address> <UUID/username>"));
incorrectUsage(invocation.getSource(), "verified add <IP address> <UUID/username>");
return;
}
final String rawInetAddress = invocation.getRawArguments()[2];
// Make sure we aren't currently locking the IP address to avoid a double I/O operation
if (LOCK.contains(rawInetAddress)) {
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getVerifiedBlocked());
return;
}
final InetAddress inetAddress = getInetAddressIfValid(invocation.getSender(), rawInetAddress);
final String rawAddress = validateIP(invocation.getSource(), invocation.getRawArguments()[2]);
// Make sure the given IP address is valid
if (inetAddress == null) return;
if (rawAddress == null) return;
// Try to parse the UUID (from the username, if needed)
final String rawUUID = invocation.getRawArguments()[3];
@@ -135,49 +120,55 @@ public final class VerifiedCommand extends Subcommand {
: UUID.nameUUIDFromBytes(("OfflinePlayer:" + rawUUID).getBytes(StandardCharsets.UTF_8));
// Make sure the player is verified already
if (SONAR.getVerifiedPlayerController().has(inetAddress.toString(), uuid)) {
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getVerifiedAlready());
if (Sonar.get().getVerifiedPlayerController().has(rawAddress, uuid)) {
invocation.getSource().sendMessage(MiniMessage.miniMessage().deserialize(
Sonar.get().getConfig().getMessagesConfig().getString("commands.verified.already"),
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix())));
return;
}
// Lock the IP address
// Make sure we don't accidentally run 2 operations at the same time
LOCK.add(rawInetAddress);
// Add verified player to the database
final long timestamp = System.currentTimeMillis();
final VerifiedPlayer verifiedPlayer = new VerifiedPlayer(inetAddress.toString(), uuid, timestamp);
Sonar.get().getVerifiedPlayerController().add(verifiedPlayer);
Sonar.get().getVerifiedPlayerController().add(new VerifiedPlayer(rawAddress, uuid, timestamp));
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getVerifiedAdd()
.replace("%ip%", rawInetAddress));
// Unlock the IP address
LOCK.remove(rawInetAddress);
invocation.getSource().sendMessage(MiniMessage.miniMessage().deserialize(
Sonar.get().getConfig().getMessagesConfig().getString("commands.verified.add"),
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix()),
Placeholder.unparsed("ip", rawAddress)));
break;
}
case "clear": {
final int verified = SONAR.getVerifiedPlayerController().estimatedSize();
final int verifiedSize = Sonar.get().getVerifiedPlayerController().estimatedSize();
if (verified == 0) {
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getVerifiedEmpty());
if (verifiedSize == 0) {
invocation.getSource().sendMessage(MiniMessage.miniMessage().deserialize(
Sonar.get().getConfig().getMessagesConfig().getString("commands.verified.empty"),
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix())));
return;
}
// Invalidate all cache entries
SONAR.getVerifiedPlayerController().clearAll();
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getVerifiedCleared()
.replace("%removed%", Sonar.DECIMAL_FORMAT.format(verified)));
Sonar.get().getVerifiedPlayerController().clearAll();
invocation.getSource().sendMessage(MiniMessage.miniMessage().deserialize(
Sonar.get().getConfig().getMessagesConfig().getString("commands.verified.clear"),
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix()),
Placeholder.unparsed("amount", Sonar.DECIMAL_FORMAT.format(verifiedSize))));
break;
}
case "size": {
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getVerifiedSize()
.replace("%amount%", Sonar.DECIMAL_FORMAT.format(SONAR.getVerifiedPlayerController().estimatedSize())));
final int verifiedSize = Sonar.get().getVerifiedPlayerController().estimatedSize();
invocation.getSource().sendMessage(MiniMessage.miniMessage().deserialize(
Sonar.get().getConfig().getMessagesConfig().getString("commands.verified.size"),
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix()),
Placeholder.unparsed("amount", Sonar.DECIMAL_FORMAT.format(verifiedSize))));
break;
}
default: {
incorrectUsage(invocation.getSender());
incorrectUsage(invocation.getSource());
break;
}
}
@@ -31,11 +31,10 @@ import java.util.List;
import java.util.Map;
import java.util.Objects;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_20_3;
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.writeNamelessCompoundTag;
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.writeString;
// Mostly taken from
// https://github.com/PaperMC/Velocity/blob/dev/3.0.0/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/ComponentHolder.java
public final class ComponentHolder {
private final String serializedComponent;
private BinaryTag cachedBinaryTag;
@@ -44,6 +43,8 @@ public final class ComponentHolder {
this.serializedComponent = GsonComponentSerializer.gson().serialize(component);
}
// Mostly taken from
// https://github.com/PaperMC/Velocity/blob/dev/3.0.0/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/ComponentHolder.java
private BinaryTag serialize(final JsonElement json) {
if (json instanceof JsonPrimitive) {
final JsonPrimitive jsonPrimitive = (JsonPrimitive) json;
@@ -143,7 +144,7 @@ public final class ComponentHolder {
public void write(final @NotNull ByteBuf byteBuf,
final @NotNull ProtocolVersion protocolVersion) {
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_20_3) >= 0) {
if (protocolVersion.compareTo(MINECRAFT_1_20_3) >= 0) {
if (cachedBinaryTag == null) {
cachedBinaryTag = serialize(new JsonParser().parse(serializedComponent));
}
@@ -21,7 +21,6 @@ import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufOutputStream;
import io.netty.buffer.ByteBufUtil;
import io.netty.handler.codec.CorruptedFrameException;
import io.netty.handler.codec.DecoderException;
import io.netty.handler.codec.EncoderException;
import lombok.experimental.UtilityClass;
import net.kyori.adventure.nbt.*;
@@ -30,14 +29,15 @@ import org.jetbrains.annotations.NotNull;
import java.io.DataOutput;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.BitSet;
import java.util.EnumSet;
import java.util.UUID;
// Mostly taken from
// https://github.com/PaperMC/Velocity/blob/dev/3.0.0/proxy/src/main/java/com/velocitypowered/proxy/protocol/ProtocolUtils.java
@UtilityClass
public class ProtocolUtil {
private static final int FORGE_MAX_ARRAY_LENGTH = Integer.MAX_VALUE & 0x1FFF9A;
public static final String BRAND_CHANNEL_LEGACY = "MC|Brand";
public static final String BRAND_CHANNEL = "minecraft:brand";
@@ -179,15 +179,6 @@ public class ProtocolUtil {
return array;
}
public static @NotNull String readBrandMessage(final @NotNull ByteBuf content) throws DecoderException {
final ByteBuf slice = content.slice();
try {
return readString(slice);
} catch (DecoderException exception) {
return readString(slice, 65536, slice.readableBytes());
}
}
public static @NotNull String readString(final ByteBuf byteBuf) throws CorruptedFrameException {
return readString(byteBuf, Short.MAX_VALUE);
}
@@ -205,7 +196,7 @@ public class ProtocolUtil {
checkFrame(length <= cap * 3, "Bad string size");
checkFrame(byteBuf.isReadable(length), "Tried to read a too-long string");
final String str = byteBuf.toString(byteBuf.readerIndex(), length, StandardCharsets.UTF_8);
byteBuf.skipBytes(length);
byteBuf.readerIndex(byteBuf.readerIndex() + length);
checkFrame(str.length() <= cap, "Got a too-long string");
return str;
}
@@ -241,6 +232,28 @@ public class ProtocolUtil {
}
}
// Taken from
// https://github.com/Nan1t/NanoLimbo/blob/main/src/main/java/ua/nanit/limbo/protocol/ByteMessage.java#L276
public <E extends Enum<E>> void writeEnumSet(final ByteBuf byteBuf,
final EnumSet<E> enumset,
final @NotNull Class<E> oclass) {
final E[] enums = oclass.getEnumConstants();
final BitSet bits = new BitSet(enums.length);
for (int i = 0; i < enums.length; ++i) {
bits.set(i, enumset.contains(enums[i]));
}
writeFixedBitSet(byteBuf, bits, enums.length);
}
private static void writeFixedBitSet(final ByteBuf byteBuf, final @NotNull BitSet bits, final int size) {
if (bits.length() > size) {
throw new StackOverflowError("BitSet too large (expected " + size + " got " + bits.size() + ")");
}
byteBuf.writeBytes(Arrays.copyOf(bits.toByteArray(), (size + 8) >> 3));
}
public static void writeCompoundTag(final @NotNull ByteBuf byteBuf, final @NotNull CompoundBinaryTag compoundTag) {
try {
BinaryTagIO.writer().write(compoundTag, (DataOutput) new ByteBufOutputStream(byteBuf));
@@ -250,7 +263,7 @@ public class ProtocolUtil {
}
// Taken from
// https://github.com/Nan1t/NanoLimbo/pull/79/files#diff-4aa8208044741102c6326c7e85086e6fa8fcc7c064f7df6fd0411baf5f2b4504
// https://github.com/Nan1t/NanoLimbo/blob/main/src/main/java/ua/nanit/limbo/protocol/ByteMessage.java#L219
public static void writeNamelessCompoundTag(final @NotNull ByteBuf byteBuf, final @NotNull BinaryTag binaryTag) {
try (final ByteBufOutputStream output = new ByteBufOutputStream(byteBuf)) {
// TODO: Find a way to improve this...
@@ -288,7 +301,7 @@ public class ProtocolUtil {
}
}
private static int readExtendedForgeShort(final @NotNull ByteBuf byteBuf) {
public static int readExtendedForgeShort(final @NotNull ByteBuf byteBuf) {
int low = byteBuf.readUnsignedShort();
int high = 0;
if ((low & 0x8000) != 0) {
@@ -298,12 +311,6 @@ public class ProtocolUtil {
return ((high & 0xFF) << 15) | low;
}
public static ByteBuf readRetainedByteBufSlice17(final ByteBuf byteBuf) {
final int length = readExtendedForgeShort(byteBuf);
checkFrame(length <= FORGE_MAX_ARRAY_LENGTH, "Too long");
return byteBuf.readRetainedSlice(length);
}
private void checkFrame(final boolean expression, final String message) {
if (!expression) {
throw new CorruptedFrameException(message);
@@ -15,6 +15,9 @@
# Obecné nastavení pro Sonar
general:
# Měl by Sonar automaticky kontrolovat nové aktualizace?
# Sonar používá GitHub API ke kontrole, zda je aktuální verze zastaralá.
check-for-updates: true
# Měl by Sonar vypisovat IP adresy hráču do konzole?
log-player-addresses: true
# Maximum hráču online se stejnou IP adresou
@@ -199,9 +202,12 @@ verification:
# Tato funkce je experimentální a může způsobit problémy
# Pokud je toto zakázáno, hráči Bedrock budou přeskočeni
check-geyser-players: true
# Množství času který musí uběhnout aby hráč byl odpojen
# Doba, za kterou hráč přestal odesílat pakety před odpojením
# (Hodnota udávaná v milisekundách: 1 sekunda = 1000 milisekund)
read-timeout: 8000
# Doba od okamžiku, kdy server přestal posílat pakety před odpojením hráče
# (Hodnota udávaná v milisekundách: 1 sekunda = 1000 milisekund)
write-timeout: 10000
# Jak dlouho by měl hráč čekat, než se při ověřování znovu připojí
# (Hodnota udávaná v milisekundách: 1 sekunda = 1000 milisekund)
rejoin-delay: 8000
@@ -15,28 +15,31 @@
# Generelle Einstellungen für Sonar
general:
# Should Sonar automatically check for new updates?
# Sonar uses the GitHub API to check if the current version is outdated
check-for-updates: true
# Soll Sonar die IP Adressen von Spielern in der Konsole anzeigen?
log-player-addresses: true
# Maximale Anzahl an Spielern mit der selben IP Adresse
# Maximale Anzahl an Spielern mit derselben IP Adresse
max-online-per-ip: 3
# Sensitivitätseinstellungen für Angriffstracker
# Bearbeite diese Einstellungen nur wenn du weißt was du tust!
# Bearbeite diese Einstellungen, nur wenn du weißt, was du tust!
attack-tracker:
# Minimale Anzahl an neuen Spielern damit ein Angriff als solcher erkannt wird
min-players-for-attack: 8
# Anzahl an Millisekunden bis ein Angriff vorbei ist
min-attack-duration: 30000
# Anzahl an gemeldenten Vorfällen damit ein Angriff als solcher erkannt wird
# Diese Zahl repräsentiert einen Puffer um falsche Angriffsbenachrichtigungen zu filtern
# Diese Zahl repräsentiert einen Puffer, um falsche Angriffsbenachrichtigungen zu filtern
min-attack-threshold: 2
# Anzahl an Millisekunden die vergehen muss damit eine neue Attacke erkannt wird
attack-cooldown-delay: 3000
# Du kannst Sonar mit einer Datenbank verbinden um verifizierte Spieler zu speichern
# Du kannst Sonar mit einer Datenbank verbinden, um verifizierte Spieler zu speichern
# Beachte: IP adressen werden im Klartext gespeichert. Du bist für die Sicherheit der Datenbank verantwortlich
database:
# Typ von Datenbank die Sonar verwenden soll um verifizierte Spielerdaten zu speichern
# Typ von Datenbank die Sonar verwenden soll, um verifizierte Spielerdaten zu speichern
# Mögliche Typen: NONE, MYSQL, MARIADB
type: NONE
# HOST für die Authentifizierung mit der SQL Datenbank
@@ -196,9 +199,12 @@ verification:
# Diese Funktion ist experimentell und kann Probleme verursachen
# Wenn dies deaktiviert ist, werden Bedrock-Spieler übersprungen
check-geyser-players: true
# Zeit, die verstreichen muss, bevor ein Spieler ein Timeout erleidet
# Die Zeit, seit der der Spieler keine Pakete mehr sendet, bevor er die Verbindung trennt
# (Dieser Wert stellt die Zeit in Millisekunden dar: 1 Sekunde = 1000 Millisekunden)
read-timeout: 8000
# Die Zeit, seit der der Server keine Pakete mehr sendet, bevor er die Verbindung zum Spieler trennt
# (Dieser Wert stellt die Zeit in Millisekunden dar: 1 Sekunde = 1000 Millisekunden)
write-timeout: 10000
# Wie lange sollte ein Spieler warten, bevor er während der Überprüfung erneut verbinden kann
# (Dieser Wert stellt die Zeit in Millisekunden dar: 1 Sekunde = 1000 Millisekunden)
rejoin-delay: 8000
@@ -15,6 +15,9 @@
# General settings for Sonar
general:
# Should Sonar automatically check for new updates?
# Sonar uses the GitHub API to check if the current version is outdated
check-for-updates: true
# Should Sonar log players' IP addresses in console?
log-player-addresses: true
# Maximum number of players online with the same IP address
@@ -198,13 +201,16 @@ verification:
# This feature is experimental and might cause issues
# If this is disabled, Bedrock players will be skipped
check-geyser-players: true
# Amount of time that has to pass before a player times out
# The time since the player stopped sending packets before disconnecting
# (This value represents the time in milliseconds: 1 second = 1000 milliseconds)
read-timeout: 8000
# The time since the server stopped sending packets before disconnecting the player
# (This value represents the time in milliseconds: 1 second = 1000 milliseconds)
write-timeout: 10000
# How long should a player wait before reconnecting during verification
# (This value represents the time in milliseconds: 1 second = 1000 milliseconds)
rejoin-delay: 8000
# How long should Sonar remember the amount of failed verifications for a player?
# How long should Sonar remember the number of failed verifications for a player?
# (This value represents the time in milliseconds: 1 second = 1000 milliseconds)
remember-time: 120000
# How long should an IP address be denied from logging in when failing the verification too often?
@@ -15,6 +15,9 @@
# Paramètre général de Sonar
general:
# Devrait Sonar vérifier automatiquement les nouvelles mises à jour ?
# Sonar utilise l'API GitHub pour vérifier si la version actuelle est obsolète.
check-for-updates: true
# Devrait Sonar enregistrer les adresses IP des joueurs dans la console ?
log-player-addresses: true
# Nombre maximum de joueurs en ligne avec la même IP
@@ -198,9 +201,12 @@ verification:
# Cette fonctionnalité est expérimentale et peut causer des problèmes
# Si cela est désactivé, les joueurs Bedrock seront ignorés
check-geyser-players: true
# Durée avant qu'un joueur ne soit déconnecté
# Temps écoulé depuis que le joueur a cessé d'envoyer des paquets avant de se déconnecter
# (Cette valeur représente le temps en millisecondes : 1 seconde = 1000 millisecondes)
read-timeout: 8000
# Temps écoulé depuis que le serveur a cessé d'envoyer des paquets avant de se déconnecter
# (Cette valeur représente le temps en millisecondes : 1 seconde = 1000 millisecondes)
write-timeout: 10000
# Combien de temps un joueur doit-il attendre avant de se reconnecter pendant la vérification
# (Cette valeur représente le temps en millisecondes : 1 seconde = 1000 millisecondes)
rejoin-delay: 8000
@@ -15,6 +15,9 @@
# ზოგადი პარამეტრები Sonar-ისთვის
general:
# Should Sonar automatically check for new updates?
# Sonar uses the GitHub API to check if the current version is outdated
check-for-updates: true
# უნდა ჩაწეროს თუ არა Sonar-მა მოთამაშეების IP მისამართები კონსოლში?
log-player-addresses: true
# მაქსიმალური ერთი და იგივე IP ზე ონლაინ მოთამაშეთა რაოდენობა
@@ -200,6 +203,9 @@ verification:
# დროის ოდენობა, რომელიც უნდა გავიდეს, სანამ მოთამაშე ტაიმაუტდება სერვერიდან.
# (ეს არის ნაჩვენები მილიწამებში: 1 წამი = 1000 მილიწამი)
read-timeout: 8000
# The time since the server stopped sending packets before disconnecting the player
# (This value represents the time in milliseconds: 1 second = 1000 milliseconds)
write-timeout: 10000
# რამდენ ხანს უნდა დაელოდოს მოთამაშე ვერიფიკაციის დროს ხელახლა დაკავშირებამდე?
# (ეს არის ნაჩვენები მილიწამებში: 1 წამი = 1000 მილიწამი)
rejoin-delay: 8000
@@ -15,6 +15,9 @@
# Algemene instellingen voor Sonar
general:
# Moet Sonar automatisch controleren op nieuwe updates?
# Sonar gebruikt de GitHub API om te controleren of de huidige versie verouderd is
check-for-updates: true
# Moet Sonar de IP-adressen van spelers in de console registreren?
log-player-addresses: true
# Maximaal aantal spelers online met hetzelfde IP-adres
@@ -201,9 +204,12 @@ verification:
# De hoeveelheid tijd die moet verstrijken voordat een speler een time-out krijgt
# (Deze waarde vertegenwoordigt de tijd in milliseconden: 1 seconde = 1000 milliseconden)
read-timeout: 8000
# Hoe lang moet een speler wachten voordat hij tijdens de verificatie opnieuw verbinding maakt?
# De tijd sinds de speler stopte met het verzenden van pakketten voordat de verbinding werd verbroken
# (Deze waarde vertegenwoordigt de tijd in milliseconden: 1 seconde = 1000 milliseconden)
rejoin-delay: 8000
# De tijd sinds de server stopte met het versturen van pakketten voordat de verbinding met de speler werd verbroken
# (Deze waarde vertegenwoordigt de tijd in milliseconden: 1 seconde = 1000 milliseconden)
write-timeout: 10000
# Hoe lang moet Sonar het aantal mislukte verificaties voor een speler onthouden?
# (Deze waarde vertegenwoordigt de tijd in milliseconden: 1 seconde = 1000 milliseconden)
remember-time: 120000
@@ -15,6 +15,9 @@
# Основные настройки
general:
# Должен ли Sonar автоматически проверять наличие новых обновлений?
# Sonar использует API GitHub, чтобы проверить, не устарела ли текущая версия
check-for-updates: true
# Должен ли Sonar логировать IP-адреса игроков в консоли?
log-player-addresses: true
# Максимальное количество игроков онлайн с одного IP-адреса
@@ -197,13 +200,16 @@ verification:
# Эта функция экспериментальна и может вызывать проблемы
# Если это отключено, игроки Bedrock будут пропущены
check-geyser-players: true
# Количество времени, которое должно пройти, прежде чем игрока кикнет.
# Время, прошедшее с момента прекращения отправки пакетов игроком до отключения.
# (Это значение представляет время в миллисекундах: 1 секунда = 1000 миллисекунд)
read-timeout: 8000
# Время, прошедшее с момента прекращения отправки сервером пакетов до отключения игрока
# (Это значение представляет время в миллисекундах: 1 секунда = 1000 миллисекунд)
write-timeout: 10000
# Сколько времени игрок должен ждать перед повторным подключением во время проверки
# (Это значение представляет время в миллисекундах: 1 секунда = 1000 миллисекунд)
rejoin-delay: 8000
# How long should Sonar remember the amount of failed verifications for a player?
# Как долго Sonar должен помнить количество неудачных проверок для игрока?
# (Это значение представляет время в миллисекундах: 1 секунда = 1000 миллисекунд)
remember-time: 120000
# Как долго IP-адресу должно быть запрещено входить на сервер, если он слишком часто не проходит проверку?
@@ -8,13 +8,16 @@
# | $$$$$$/| $$$$$$/| $$ | $$| $$$$$$$| $$
# \______/ \______/ |__/ |__/ \_______/|__/
#
# 有问题或需要帮助? https://jonesdev.xyz/discord
# 需要帮助吗? https://jonesdev.xyz/discord
# 源代码: https://github.com/jonesdevelopment/sonar
# Copyright Jones Development and Sonar Contributors, licensed under GPL-3.0
#
# 未归类的设置
general:
# Sonar 是否应该自动检查新的更新?
# Sonar 使用 GitHub API 检查当前版本是否过时
check-for-updates: true
# Sonar 应该在控制台中打印玩家的 IP 地址吗?
log-player-addresses: true
# 具有相同 IP 地址的在线玩家的最大数量
@@ -103,37 +106,38 @@ verification:
# - NEVER: 永远不应用该检查
timing: NEVER
# 图像效果选项 (用于混淆自动图像识别)
# You should only edit this if you know what you're doing
# 只有当您知道您在做什么时, 才应该编辑它.
effects:
# Adds random lines to the image
# 是否向图像添加随机线条
scratches: true
# Applies a ripple filter (wavy effect)
# 是否应用纹波滤镜
ripple: true
# Applies a bump filter (sharpened text)
# 是否锐化文本
bump: true
# Distortion filter
# 失真滤镜
distortion:
# Should Sonar apply a distortion filter?
# 是否在验证码上应用失真滤镜?
enabled: true
# What shape should Sonar use for the distortion filter?
# Possible types: 0, 1, 2, 3
# - 0: Crosses
# - 1: Lines
# - 2: Circles
# - 3: Squares
# Sonar 应该应用什么形状的失真滤镜?
# 支持的选项: 0, 1, 2, 3
# - 0: 交叉
# - 1: 线条
# - 2: 圆形
# - 3: 方形
shape: 2
# Distance between distorted pixels/objects
# 畸变的像素/对象之间的距离
distance: 5
# Density of the distorted pixels/objects
# 畸变的像素/对象之间的密度
density: 0.1
# Amount of blending between the colors of objects
# 对象与另一个对象之间的颜色的混合的量
mix: 0.15
# Path to the background image Sonar uses for the CAPTCHA
# If the image isn't found, a noise will be used as the default background
# 用于当作验证码的背景的图片
# 如果未找到指定的图片, 则自动通过噪音生成一个随机的背景图片.
# 如果图片在该插件的配置文件夹下, 则可以简单地输入图片的文件名.
background: ''
# Should Sonar automatically detect the background color?
# This is done to create a contrast between the background image and the text
# You can simply put the name of the file here if file is in the plugin directory
# Sonar 应该检测背景图片的颜色吗?
# 这样做是为了避免无法区分背景和验证码的颜色.
# 如果禁用该功能, Sonar 将会随机应用渐变的颜色.
auto-color: false
# Sonar 应该一次准备多少个验证码?
precompute: 500
@@ -190,9 +194,12 @@ verification:
# 此功能是实验性的. 请报告任何因为误判而导致无法通过检查的问题
# 如果关闭该选项 Sonar 将不检查这些玩家以便直接连接到服务器.
check-geyser-players: true
# 要在客户端未响应多少毫秒后因超时而被踢出服务器?
# 播放器断开连接前停止发送数据包的时间
# (该值为毫秒. 1秒等于1000毫秒.)
read-timeout: 8000
# 服务器在断开玩家连接前停止发送数据包的时间
# (该值为毫秒. 1秒等于1000毫秒.)
write-timeout: 10000
# 当玩家在需要验证时(即还未通过检查的情况下)需要等待多少毫秒才可以重新连接?
# (该值为毫秒. 1秒等于1000毫秒.)
rejoin-delay: 8000
@@ -13,268 +13,230 @@
# Copyright Jones Development and Sonar Contributors, licensed under GPL-3.0
#
# Platzhalter für jedes '%prefix%' in dieser Konfigurationsdatei
# Platzhalter für jedes '<prefix>' in dieser Konfigurationsdatei
prefix: <b><yellow>Sonar</b> <gray>» <white>
# Platzhalter für jede '%support-url%' in dieser Konfigurationsdatei
# Platzhalter für jede '<support-url>' in dieser Konfigurationsdatei
support-url: https://jonesdev.xyz/discord/
# Platzhalter für jedes '%header%' in dieser Konfigurationsdatei
# Platzhalter für jedes '<header>' in dieser Konfigurationsdatei
header:
- <b><yellow>Sonar</b><reset>
- ''
# Platzhalter für jedes '%footer%' in dieser Konfigurationsdatei
# Platzhalter für jedes '<footer>' in dieser Konfigurationsdatei
footer:
- <gray>Falls du glaubst, dass dies ein Fehler ist, kontaktiere einen Administrator.
# Nachricht, die angezeigt wird, wenn jemand beitritt, aber zu viele Online-Spieler mit ihrer IP-Adresse vorhanden sind
too-many-online-per-ip:
- '%header%'
- '<header>'
- <red>Es sind zu viele Spieler mit deiner IP-Adresse online.
- '%footer%'
- '<footer>'
# Übersetzungen für Befehle
commands:
# Nachricht, die angezeigt wird, wenn ein Spieler versucht, /sonar ohne Berechtigung auszuführen
no-permission: '%prefix%<red>Du hast keine Berechtigung, diesen Befehl auszuführen.'
no-permission: '<prefix><red>Du hast keine Berechtigung, diesen Befehl auszuführen.'
# Nachricht, die angezeigt wird, wenn jemand einen Befehl falsch verwendet
incorrect-usage: '%prefix%<red>Benutzung: /sonar %usage%'
incorrect-usage: '<prefix><red>Benutzung: /sonar <subcommand-usage>'
# Nachricht, die angezeigt wird, wenn jemand eine ungültige IP-Adresse angibt (Ungültiges Format)
invalid-ip-address: '%prefix%Die angegebene IP-Adresse scheint ungültig zu sein.'
invalid-ip-address: '<prefix>Die angegebene IP-Adresse scheint ungültig zu sein.'
# Nachricht, die angezeigt wird, wenn die Konsole einen Befehl ausführt, der nur für Spieler ist
player-only: '%prefix%<red>Diesen Befehl kannst du nur als Spieler ausführen.'
player-only: '<prefix><red>Diesen Befehl kannst du nur als Spieler ausführen.'
# Nachricht, die angezeigt wird, wenn ein Spieler einen Befehl ausführt, der nur für die Konsole ist
console-only: '%prefix%<red>Aus Sicherheitsgründen kannst du diesen Befehl nur über die Konsole ausführen.'
# Nachricht, die angezeigt wird, wenn ein Spieler Sonar-Befehle zu schnell ausführt
cool-down: '%prefix%<red>Du kannst diesen Befehl nur alle 0,5 Sekunden ausführen.'
cool-down-left: '%prefix%<red>Bitte warte noch <bold>%time-left%s<reset><red>.'
console-only: '<prefix><red>Aus Sicherheitsgründen kannst du diesen Befehl nur über die Konsole ausführen.'
# Nachricht, die angezeigt wird, wenn ein Spieler keine Berechtigung hat, einen bestimmten Unterbefehl auszuführen
subcommand-no-permission: '%prefix%<red>Du hast keine Berechtigung, diesen Unterbefehl auszuführen. <gray>(%permission%)'
subcommand-no-permission: '<prefix><red>Du hast keine Berechtigung, diesen Unterbefehl auszuführen. <gray>(<permission>)'
# Übersetzungen für '/sonar'
main:
# Informationsnachricht, die oben angezeigt wird, wenn der Hauptbefehl ausgeführt wird
header:
- <yellow>Ausführung von <b>Sonar</b> %version% auf %platform%.
- <gray><click:open_url:'https://github.com/jonesdevelopment/sonar'>(C) %copyright_year%
Jones Development und Sonar Mitwirkende</click>
- <yellow>Ausführung von <b>Sonar</b> <version> auf <platform>.
- <gray><click:open_url:'https://github.com/jonesdevelopment/sonar'>(C) <copyright-year> Jones Development und Sonar Mitwirkende</click>
- ''
- <gray>Brauchst du Hilfe oder hast du Fragen?
- <white><click:open_url:'https://jonesdev.xyz/discord/'><hover:show_text:'(Klicken
um Discord zu öffnen)'>Öffne ein Ticket auf Discord </hover></click><click:open_url:'https://github.com/jonesdevelopment/sonar/issues'><hover:show_text:'(Klicken
um GitHub zu öffnen)'>oder erstelle ein neues Issue auf GitHub.
- <white><click:open_url:'https://jonesdev.xyz/discord/'><hover:show_text:'(Klicken um Discord zu öffnen)'>Öffne ein Ticket auf Discord </hover></click><click:open_url:'https://github.com/jonesdevelopment/sonar/issues'><hover:show_text:'(Klicken um GitHub zu öffnen)'>oder erstelle ein neues Issue auf GitHub.
- ''
# Formatierung der Liste der Unterbefehle, die angezeigt wird, wenn der Hauptbefehl ausgeführt wird
subcommands: '<click:suggest_command:''/sonar %subcommand% ''><hover:show_text:''<gray>Nur
Spieler: </gray>%only_players%<br><gray>Erfordert Konsole: </gray>%require_console%<br><gray>Berechtigung:
</gray><white>%permission%<br><gray>Aliase: </gray>%aliases%''><dark_aqua>
▪ <gray>/sonar %subcommand% <white>%description%'
subcommands: '<suggest-subcommand><hover:show_text:''<gray>Nur Spieler: </gray><only-players><br><gray>Erfordert Konsole: </gray><only-console><br><gray>Berechtigung: </gray><white><permission><br><gray>Aliase: </gray><aliases>''><dark_aqua> ▪ <gray>/sonar <subcommand> <white><description></hover></suggest-subcommand>'
# Übersetzungen für '/sonar reload'
reload:
# Nachricht, die angezeigt wird, wenn jemand mit dem Neuladen von Sonar beginnt
start: '%prefix%Sonar wird neu geladen...'
start: '<prefix>Sonar wird neu geladen...'
# Nachricht, die angezeigt wird, wenn Sonar erfolgreich neu geladen wurde
finish: '%prefix%<green>Erfolgreich neu geladen <gray>(%taken%ms)'
finish: '<prefix><green>Erfolgreich neu geladen <gray>(<time-taken>ms)'
# Übersetzungen für '/sonar verbose'
verbose:
# Nachricht, die angezeigt wird, wenn ein Spieler Sonar Verbose-Informationen abonniert
subscribed: '%prefix%Du siehst jetzt Sonar Verbose-Informationen.'
subscribe: '<prefix>Du siehst jetzt Sonar Verbose-Informationen.'
# Nachricht, die angezeigt wird, wenn ein Spieler Sonar Verbose-Informationen abbestellt
unsubscribed: '%prefix%Du siehst keine Sonar Verbose-Informationen mehr.'
unsubscribe: '<prefix>Du siehst keine Sonar Verbose-Informationen mehr.'
# Übersetzungen für '/sonar notify'
notify:
# Nachricht, die angezeigt wird, wenn ein Spieler Sonar Angriffsbenachrichtigungen abonniert
subscribed: '%prefix%Du siehst jetzt Sonar Angriffsbenachrichtigungen.'
subscribe: '<prefix>Du siehst jetzt Sonar Angriffsbenachrichtigungen.'
# Nachricht, die angezeigt wird, wenn ein Spieler Sonar Angriffsbenachrichtigungen abbestellt
unsubscribed: '%prefix%Du siehst keine Sonar Angriffsbenachrichtigungen mehr.'
unsubscribe: '<prefix>Du siehst keine Sonar Angriffsbenachrichtigungen mehr.'
# Übersetzungen für '/sonar blacklist'
blacklist:
# Nachricht, die angezeigt wird, wenn jemand versucht, die Blacklist zu leeren, aber sie leer ist
empty: '%prefix%Die Blacklist ist derzeit leer. Daher wurden keine IP-Adressen von der Blacklist entfernt.'
empty: '<prefix>Die Blacklist ist derzeit leer. Daher wurden keine IP-Adressen von der Blacklist entfernt.'
# Nachricht, die angezeigt wird, wenn jemand die Blacklist leert
cleared: '%prefix%Du hast erfolgreich insgesamt %removed% IP-Adresse(n) von der Blacklist entfernt.'
clear: '<prefix>Du hast erfolgreich insgesamt <amount> IP-Adresse(n) von der Blacklist entfernt.'
# Nachricht, die angezeigt wird, wenn jemand die Größe der Blacklist überprüft
size: '%prefix%Die Blacklist enthält derzeit %amount% IP-Adresse(n).'
size: '<prefix>Die Blacklist enthält derzeit <amount> IP-Adresse(n).'
# Nachricht, die angezeigt wird, wenn jemand eine IP-Adresse zur Blacklist hinzufügt
added: '%prefix%Erfolgreich %ip% zur Blacklist hinzugefügt.'
add: '<prefix>Erfolgreich <ip> zur Blacklist hinzugefügt.'
# Nachricht, die angezeigt wird, wenn jemand eine IP-Adresse zur Blacklist hinzufügt, die verifiziert ist
added-warning: '%prefix%<red>Warnung: <white>%ip% ist derzeit auf der Whitelist. Erwäge,
die IP-Adresse aus der Liste der verifizierten Spieler zu entfernen, um potenzielle
Probleme zu vermeiden.'
add-warning: '<prefix><red>Warnung: <white><ip> ist derzeit auf der Whitelist. Erwäge, die IP-Adresse aus der Liste der verifizierten Spieler zu entfernen, um potenzielle Probleme zu vermeiden.'
# Nachricht, die angezeigt wird, wenn jemand eine IP-Adresse von der Blacklist entfernt
removed: '%prefix%Erfolgreich %ip% von der Blacklist entfernt.'
remove: '<prefix>Erfolgreich <ip> von der Blacklist entfernt.'
# Nachricht, die angezeigt wird, wenn jemand eine IP-Adresse zur Blacklist hinzufügt, die bereits auf der Blacklist steht
duplicate-ip: '%prefix%Die angegebene IP-Adresse ist bereits auf der Blacklist.'
ip-duplicate: '<prefix>Die angegebene IP-Adresse ist bereits auf der Blacklist.'
# Nachricht, die angezeigt wird, wenn jemand eine IP-Adresse von der Blacklist entfernt, die nicht auf der Blacklist steht
ip-not-found: '%prefix%Die angegebene IP-Adresse ist nicht auf der Blacklist.'
ip-not-found: '<prefix>Die angegebene IP-Adresse ist nicht auf der Blacklist.'
# Übersetzungen für '/sonar verified'
verified:
# Nachricht, die angezeigt wird, wenn jemand versucht, die Liste der verifizierten Spieler zu leeren, aber sie leer ist
empty: '%prefix%Die Liste der verifizierten Spieler ist derzeit leer. Daher wurden keine
Spieler unverifiziert.'
empty: '<prefix>Die Liste der verifizierten Spieler ist derzeit leer. Daher wurden keine Spieler unverifiziert.'
# Nachricht, die angezeigt wird, wenn jemand die Liste der verifizierten Spieler leert
cleared: '%prefix%Du hast erfolgreich insgesamt %removed% einzigartige Spieler unverifiziert.'
clear: '<prefix>Du hast erfolgreich insgesamt <amount> einzigartige Spieler unverifiziert.'
# Nachricht, die angezeigt wird, wenn jemand die Größe der Liste der verifizierten Spieler überprüft
size: '%prefix%Es sind derzeit %amount% einzigartige Spieler verifiziert.'
size: '<prefix>Es sind derzeit <amount> einzigartige Spieler verifiziert.'
# Nachricht, die angezeigt wird, wenn jemand eine IP-Adresse unverifiziert macht
removed: '%prefix%Erfolgreich %ip% unverifiziert.'
remove: '<prefix>Erfolgreich <ip> unverifiziert.'
# Nachricht, die angezeigt wird, wenn jemand eine IP-Adresse verifiziert macht
added: '%prefix%Erfolgreich %ip% verifiziert.'
add: '<prefix>Erfolgreich <ip> verifiziert.'
# Message that is shown when someone tries to verify an IP address/UUID that are already verified
already: '%prefix%The IP address and UUID you provided are already verified.'
already: '<prefix>The IP address and UUID you provided are already verified.'
# Nachricht, die angezeigt wird, wenn jemand eine IP-Adresse unverifiziert macht, die nicht verifiziert ist
ip-not-found: '%prefix%Die angegebene IP-Adresse ist nicht verifiziert.'
# Nachricht, die angezeigt wird, wenn jemand versucht, die gleiche IP-Adresse zweimal zu unverifizieren (doppelte Operation)
blocked: '%prefix%Bitte warte, bis der aktuelle Vorgang abgeschlossen ist.'
ip-not-found: '<prefix>Die angegebene IP-Adresse ist nicht verifiziert.'
# Message shown when someone looks up the verification history of an IP address
history: '<prefix><yellow>Bisherige UUIDs für <ip>:'
history-entry: ' <gray>▪ <white><uuid>'
# Übersetzungen für '/sonar statistics'
statistics:
# Informationsnachricht, die oben angezeigt wird, wenn die Statistiken angesehen werden
header: '%prefix%Anzeige der %type%-Statistiken für diese Sitzung:'
header: '<prefix>Anzeige der <statistics-type>-Statistiken für diese Sitzung:'
# Nachricht, die angezeigt wird, wenn ein Spieler versucht, eine unbekannte Statistik anzusehen
unknown-type: '%prefix%<red>Unbekannter Statistiktyp! Verfügbare Statistiken: <gray>%statistics%'
unknown-type: '<prefix><red>Unbekannter Statistiktyp! Verfügbare Statistiken: <gray><statistics>'
# Format der allgemeinen Statistiknachricht
general:
- ' <dark_aqua>▪ <gray>Verifizierte IP-Adressen: <white>%verified%'
- ' <dark_aqua>▪ <gray>IP-Adressen in Überprüfung: <white>%verifying%'
- ' <dark_aqua>▪ <gray>Auf der Blacklist stehende IP-Adressen: <white>%blacklisted%'
- ' <dark_aqua>▪ <gray>Aktuell in der Warteschlange befindliche Anmeldungen: <white>%queued%'
- ' <dark_aqua>▪ <gray>Gesamte nicht-einzigartige Beitritte: <white>%total_joins%'
- ' <dark_aqua>▪ <gray>Gesamte Überprüfungsversuche: <white>%total_attempts%'
- ' <dark_aqua>▪ <gray>Gesamte fehlgeschlagene Überprüfungen: <white>%total_failed%'
- ' <dark_aqua>▪ <gray>Server-Laufzeit: <white>%uptime%'
- ' <dark_aqua>▪ <gray>Verifizierte IP-Adressen: <white><verified>'
- ' <dark_aqua>▪ <gray>IP-Adressen in Überprüfung: <white><verifying>'
- ' <dark_aqua>▪ <gray>Auf der Blacklist stehende IP-Adressen: <white><blacklisted>'
- ' <dark_aqua>▪ <gray>Aktuell in der Warteschlange befindliche Anmeldungen: <white><queued>'
- ' <dark_aqua>▪ <gray>Gesamte nicht-einzigartige Beitritte: <white><total-joins>'
- ' <dark_aqua>▪ <gray>Gesamte Überprüfungsversuche: <white><total-attempts>'
- ' <dark_aqua>▪ <gray>Gesamte fehlgeschlagene Überprüfungen: <white><total-failed>'
- ' <dark_aqua>▪ <gray>Server-Laufzeit: <white><server-uptime>'
# Format der CPU-Statistiknachricht
cpu:
- ' <dark_aqua>▪ <gray>Aktuelle CPU-Nutzung des Prozesses: <white>%process_cpu%%'
- ' <dark_aqua>▪ <gray>Aktuelle CPU-Nutzung des Systems: <white>%system_cpu%%'
- ' <dark_aqua>▪ <gray>Durchschnittliche CPU-Nutzung des Prozesses pro Kern: <white>%average_process_cpu%%'
- ' <dark_aqua>▪ <gray>Durchschnittliche CPU-Nutzung des Systems pro Kern: <white>%average_system_cpu%%'
- ' <dark_aqua>▪ <gray>Allgemeine Systemlast im Durchschnitt: <white>%load_average%%'
- ' <dark_aqua>▪ <gray>Gesamtanzahl virtueller CPUs: <white>%virtual_cores%'
- ' <dark_aqua>▪ <gray>Aktuelle CPU-Nutzung des Prozesses: <white><process-cpu>%'
- ' <dark_aqua>▪ <gray>Aktuelle CPU-Nutzung des Systems: <white><system-cpu>%'
- ' <dark_aqua>▪ <gray>Durchschnittliche CPU-Nutzung des Prozesses pro Kern: <white><average-process-cpu>%'
- ' <dark_aqua>▪ <gray>Durchschnittliche CPU-Nutzung des Systems pro Kern: <white><average-system-cpu>%'
- ' <dark_aqua>▪ <gray>Gesamtanzahl virtueller CPUs: <white><virtual-core-count>'
# Format der Speicherstatistiknachricht
memory:
- ' <dark_aqua>▪ <gray>Gesamter freier Speicher: <white>%free_memory%'
- ' <dark_aqua>▪ <gray>Gesamter genutzter Speicher: <white>%used_memory%'
- ' <dark_aqua>▪ <gray>Gesamter maximaler Speicher: <white>%max_memory%'
- ' <dark_aqua>▪ <gray>Gesamter zugewiesener Speicher: <white>%total_memory%'
- ' <dark_aqua>▪ <gray>Gesamter freier Speicher: <white><free-memory>'
- ' <dark_aqua>▪ <gray>Gesamter genutzter Speicher: <white><used-memory>'
- ' <dark_aqua>▪ <gray>Gesamter maximaler Speicher: <white><max-memory>'
- ' <dark_aqua>▪ <gray>Gesamter zugewiesener Speicher: <white><total-memory>'
# Format der Netzwerkstatistiknachricht
network:
- ' <dark_aqua>▪ <gray>Aktuell genutzte eingehende Bandbreite: <white>%incoming%'
- ' <dark_aqua>▪ <gray>Aktuell genutzte ausgehende Bandbreite: <white>%outgoing%'
- ' <dark_aqua>▪ <gray>Gesamte genutzte eingehende Bandbreite: <white>%ttl_incoming%'
- ' <dark_aqua>▪ <gray>Gesamte genutzte ausgehende Bandbreite: <white>%ttl_outgoing%'
- ' <dark_aqua>▪ <gray>Aktuell genutzte eingehende Bandbreite: <white><incoming-traffic>'
- ' <dark_aqua>▪ <gray>Aktuell genutzte ausgehende Bandbreite: <white><outgoing-traffic>'
- ' <dark_aqua>▪ <gray>Gesamte genutzte eingehende Bandbreite: <white><incoming-traffic-ttl>'
- ' <dark_aqua>▪ <gray>Gesamte genutzte ausgehende Bandbreite: <white><outgoing-traffic-ttl>'
# Übersetzungen für alle Nachrichten während des Übergabeprozesses
verification:
# Übersetzungen für alle Debug-Nachrichten während der Überprüfung
logs:
# Nachricht, die im Protokoll angezeigt wird, wenn ein neuer Spieler den Server betretet
connection: '%name%%ip% (%protocol%) hat sich verbunden.'
connection: '<username><ip> (<protocol>) hat sich verbunden.'
# Nachricht, die im Protokoll angezeigt wird, wenn ein Spieler die Verifizierung nicht besteht
failed: '%ip% (%protocol%) hat den Bot-Check nicht bestanden wegen: %reason%'
failed: '<ip> (<protocol>) hat den Bot-Check nicht bestanden wegen: <reason>'
# Nachricht, die im Protokoll angezeigt wird, wenn ein Spieler aufgrund zu vieler fehlgeschlagener Versuche auf die Blacklist gesetzt wird
blacklisted: '%ip% (%protocol%) wurde aufgrund zu vieler fehlgeschlagener Versuche auf die Blacklist gesetzt'
blacklisted: '<ip> (<protocol>) wurde aufgrund zu vieler fehlgeschlagener Versuche auf die Blacklist gesetzt'
# Nachricht, die im Protokoll angezeigt wird, wenn ein Spieler erfolgreich verifiziert wurde
successful: '%name% wurde erfolgreich verifiziert (%time%s!).'
successful: '<username> wurde erfolgreich verifiziert (<time-taken>s!).'
# Nachrichten, die dem Spieler angezeigt werden, wenn er den CAPTCHA eingeben soll
captcha:
# Nachricht, die dem Spieler angezeigt wird, wenn er den CAPTCHA-Code im Chat eingeben muss
enter-code: '%prefix%<green>Bitte gib den Code im Chat ein, der auf der Karte angezeigt wird.'
enter-code: '<prefix><green>Bitte gib den Code im Chat ein, der auf der Karte angezeigt wird.'
# Nachricht, die dem Spieler angezeigt wird, wenn er den falschen Code im Chat eingibt
incorrect: '%prefix%<red>Du hast den falschen Code eingegeben. Bitte versuche es erneut.'
incorrect: '<prefix><red>Du hast den falschen Code eingegeben. Bitte versuche es erneut.'
# Trennungs-Nachricht, die angezeigt wird, wenn jemand beitritt, während der CAPTCHA noch nicht vorbereitet wurde
currently-preparing:
- '%header%'
- '<header>'
- <yellow>Deine Anti-Bot-Daten wurden noch nicht vorbereitet.
- <gray>Bitte warte einige Sekunden bevor du es erneut versuchst.
- '%footer%'
- '<footer>'
# Trennungs-Nachricht, die angezeigt wird, wenn jemand zu schnell während der Verifizierung erneut beitritt
too-fast-reconnect:
- '%header%'
- '<header>'
- <gold>Du hast zu schnell erneut eine Verbindung hergestellt, versuche es später noch einmal.
- <gray>Bitte warte einige Sekunden bevor du es erneut versuchst.
- '%footer%'
- '<footer>'
# Trennungs-Nachricht, die angezeigt wird, wenn jemand beitritt, aber bereits verifiziert wird
already-verifying:
- '%header%'
- '<header>'
- <red>Deine IP-Adresse wird derzeit verifiziert.
- <red>Bitte warte einige Sekunden bevor du es erneut versuchst.
- '%footer%'
- '<footer>'
# Trennungs-Nachricht, die angezeigt wird, wenn jemand beitritt, aber bereits in der Warteschlange für die Verifizierung ist
already-queued:
- '%header%'
- '<header>'
- <red>Deine IP-Adresse befindet sich derzeit in der Warteschlange für die Verifizierung.
- <red>Bitte warte einige Minuten, bevor du es erneut versuchst.
- '%footer%'
- '<footer>'
# Trennungs-Nachricht, die angezeigt wird, wenn jemand beitritt, aber vorübergehend auf der Blacklist steht
blacklisted:
- '%header%'
- '<header>'
- <red>Du bist derzeit vom Betreten des Servers ausgeschlossen.
- <red>Bitte warte einige Minuten, um den Server wieder betreten zu können.
- <gold>Falschmeldung? <gray>%support-url%
- '%footer%'
- <gold>Falschmeldung? <gray><support-url>
- '<footer>'
# Nachricht, die angezeigt wird, wenn jemand mit einem ungültigen Benutzernamen beitritt
invalid-username:
- '%header%'
- '<header>'
- <red>Dein Benutzername enthält ungültige Zeichen.
- '%footer%'
- '<footer>'
# Nachricht, die angezeigt wird, wenn jemand mit einer auf der Blacklist stehenden Version beitritt
blacklisted-protocol:
- '%header%'
- '<header>'
- <red>Du verwendest eine Version, die auf unserem Server nicht erlaubt ist.
- <gold>Brauchst du Hilfe beim Einloggen? <gray>%support-url%
- '%footer%'
- <gold>Brauchst du Hilfe beim Einloggen? <gray><support-url>
- '<footer>'
# Nachricht, die angezeigt wird, wenn jemand die Verifizierung erfolgreich abschließt
success:
- '%header%'
- '<header>'
- <green>Du hast die Verifizierung erfolgreich bestanden.
- <white>Verbinde dich erneut um auf dem Server zu joinen.
# Nachricht, die angezeigt wird, wenn jemand die Verifizierung nicht besteht
failed:
- '%header%'
- '<header>'
- <red>Du hast die Bot-Verifizierung nicht bestanden.
- <gray>Bitte warte einige Sekunden bevor du es erneut versuchst.
- <gold>Brauchst du Hilfe? <gray>%support-url%
- '%footer%'
- <gold>Brauchst du Hilfe beim Einloggen? <gray><support-url>
- '<footer>'
# Übersetzungen für Sonars ausführliche Aktionsleisten-Nachrichten
verbose:
layout:
# Layout für Sonars ausführliche Aktionsleiste
# Platzhalter:
# - '%queued%' Anzahl der in der Warteschlange befindlichen Verbindungen
# - '%verifying%' Anzahl der Verifizierungen
# - '%blacklisted%' Anzahl der IP-Adressen auf der Blacklist
# - '%total-joins%' Gesamtzahl der Versuche beizutreten
# - '%verify-total%' Gesamtzahl der Verifizierungsversuche
# - '%verify-success%' Anzahl der verifizierten IP-Adressen
# - '%verify-failed%' Anzahl der gescheiterten Verifizierungen
# - '%logins-per-second%' Anzahl der Anmeldungen pro Sekunde
# - '%connections-per-second%' Anzahl der Verbindungen pro Sekunde
# - '%attack-duration%' Dauer des aktuellen Angriffs
# - '%incoming-traffic%' Eingehende Bandbreitennutzung pro Sekunde
# - '%outgoing-traffic%' Ausgehende Bandbreitennutzung pro Sekunde
# - '%incoming-traffic-ttl%' Gesamte eingehende Bandbreitennutzung
# - '%outgoing-traffic-ttl%' Gesamte ausgehende Bandbreitennutzung
# - '%used-memory%' Menge des genutzten Arbeitsspeichers des Prozesses
# - '%total-memory%' Menge des gesamten Arbeitsspeichers des Prozesses
# - '%max-memory%' Menge des maximalen Arbeitsspeichers des Prozesses
# - '%free-memory%' Menge des freien Arbeitsspeichers des Prozesses
# - '%animation%' Anpassbares animiertes Symbol
# Übersetzungen für Sonars normale ausführliche Ausgabe
normal: '%prefix%<gray>CPS <white>%connections-per-second% <dark_aqua>╺ <gray>Anmeldungen/s
<white>%logins-per-second% <dark_aqua>╺ <gray>Verifizierung <white>%verifying% <dark_aqua>|
<green>⬆ <white>%outgoing-traffic%/s <red>⬇ <white>%incoming-traffic%/s <green><bold>%animation%<reset>'
normal: '<prefix><gray>CPS <white><connections-per-second> <dark_aqua>╺ <gray>Anmeldungen/s <white><logins-per-second> <dark_aqua>╺ <gray>Verifizierung <white><verifying> <dark_aqua>| <green>⬆ <white><outgoing-traffic>/s <red>⬇ <white><incoming-traffic>/s <green><bold><animation>'
# Layout für Sonars ausführliche Aktionsleiste während eines Angriffs
attack: '%prefix%<gray>CPS <white>%connections-per-second% <dark_aqua>╺ <gray>Anmeldungen/s
<white>%logins-per-second% <dark_aqua>╺ <gray>Warteschlange <white>%queued% <dark_aqua>╺
<gray>Verifizierung <white>%verifying% <dark_aqua>╺ <gray>Dauer <white>%attack-duration%
<dark_aqua>| <green>⬆ <white>%outgoing-traffic%/s <red>⬇ <white>%incoming-traffic%/s <green><bold>%animation%<reset>'
attack: '<prefix><gray>CPS <white><connections-per-second> <dark_aqua>╺ <gray>Anmeldungen/s <white><logins-per-second> <dark_aqua>╺ <gray>Warteschlange <white><queued> <dark_aqua>╺ <gray>Verifizierung <white><verifying> <dark_aqua>╺ <gray>Dauer <white><attack-duration> <dark_aqua>| <green>⬆ <white><outgoing-traffic>/s <red>⬇ <white><incoming-traffic>/s <green><bold><animation>'
# Animation für die Aktionsleiste
# Alternativen:
# - ▙, ▛, ▜, ▟
@@ -297,11 +259,9 @@ notifications:
- <yellow><b>Sonar<reset>
- ''
- <gray>Ein Bot-Angriff wurde erkannt.
- ' <dark_aqua>▪ <gray>Anmeldungen pro Sekunde: <white>%logins-per-second%'
- ' <dark_aqua>▪ <gray>Verifizierende Spieler: <white>%verifying%'
- ' <dark_aqua>▪ <gray>Spieler in der Warteschlange: <white>%queued%'
- ' <dark_aqua>▪ <gray>Anmeldungen pro Sekunde: <white><logins-per-second>'
- ' <dark_aqua>▪ <gray>Verifizierende Spieler: <white><verifying>'
- ' <dark_aqua>▪ <gray>Spieler in der Warteschlange: <white><queued>'
- ''
- <gray>Zeige mehr Informationen mit <white><click:run_command:'/sonar verbose'><hover:show_text:'(Click
to run)'>/sonar verbose</hover></click><gray> oder <white><click:run_command:'/sonar
stats'><hover:show_text:'(Click to run)'>/sonar stats</hover></click><gray>.
- <gray>Zeige mehr Informationen mit <white><click:run_command:'/sonar verbose'><hover:show_text:'(Click to run)'>/sonar verbose</hover></click><gray> oder <white><click:run_command:'/sonar stats'><hover:show_text:'(Click to run)'>/sonar stats</hover></click><gray>.
- ''
@@ -13,272 +13,229 @@
# Copyright Jones Development and Sonar Contributors, licensed under GPL-3.0
#
# Placeholder for every '%prefix%' in this configuration file
# Placeholder for every '<prefix>' in this configuration file
prefix: <b><yellow>Sonar</b> <gray>» <white>
# Placeholder for every '%support-url%' in this configuration file
# Placeholder for every '<support-url>' in this configuration file
support-url: https://jonesdev.xyz/discord/
# Placeholder for every '%header%' in this configuration file
# Placeholder for every '<header>' in this configuration file
header:
- <b><yellow>Sonar</b><reset>
- ''
# Placeholder for every '%footer%' in this configuration file
# Placeholder for every '<footer>' in this configuration file
footer:
- <gray>If you believe that this is an error, contact an administrator.
# Disconnect message that is shown when someone joins, but there are too many online players with their IP address
too-many-online-per-ip:
- '%header%'
- '<header>'
- <red>There are too many players online with your IP address.
- '%footer%'
- '<footer>'
# Translations for commands
commands:
# Message shown when a player tries running /sonar without permission
no-permission: '%prefix%<red>You do not have permission to execute this command.'
no-permission: '<prefix><red>You do not have permission to execute this command.'
# Message shown when someone uses a command incorrectly
incorrect-usage: '%prefix%<red>Usage: /sonar %usage%'
incorrect-usage: '<prefix><red>Usage: /sonar <subcommand-usage>'
# Message shown when someone provides an invalid IP address (Invalid format)
invalid-ip-address: '%prefix%The IP address you provided seems to be invalid.'
invalid-ip-address: '<prefix>The IP address you provided seems to be invalid.'
# Message shown when the console runs a command that is player-only
player-only: '%prefix%<red>You can only execute this command as a player.'
player-only: '<prefix><red>You can only execute this command as a player.'
# Message shown when a player runs a command that is console-only
console-only: '%prefix%<red>For security reasons, you can only execute this command
through console.'
# Message shown when a player executes Sonar commands too quickly
cool-down: '%prefix%<red>You can only execute this command every 0.5 seconds.'
cool-down-left: '%prefix%<red>Please wait another <bold>%time-left%s<reset><red>.'
console-only: '<prefix><red>For security reasons, you can only execute this command through console.'
# Message shown when a player does not have permission to execute a certain subcommand
subcommand-no-permission: '%prefix%<red>You do not have permission to execute this
subcommand. <gray>(%permission%)'
subcommand-no-permission: '<prefix><red>You do not have permission to execute this subcommand. <gray>(<permission>)'
# Translations for '/sonar'
main:
# Informational message that is shown above everything when running the main command
header:
- <yellow>Running <b>Sonar</b> %version% on %platform%.
- <gray><click:open_url:'https://github.com/jonesdevelopment/sonar'>(C) %copyright_year%
Jones Development and Sonar Contributors</click>
- <yellow>Running <b>Sonar</b> <version> on <platform>.
- <gray><click:open_url:'https://github.com/jonesdevelopment/sonar'>(C) <copyright-year> Jones Development and Sonar Contributors</click>
- ''
- <gray>Need help or have any questions?
- <white><click:open_url:'https://jonesdev.xyz/discord/'><hover:show_text:'(Click
to open Discord)'>Open a ticket on the Discord </hover></click><click:open_url:'https://github.com/jonesdevelopment/sonar/issues'><hover:show_text:'(Click
to open GitHub)'>or open a new issue on GitHub.
- <white><click:open_url:'https://jonesdev.xyz/discord/'><hover:show_text:'(Click to open Discord)'>Open a ticket on the Discord </hover></click><click:open_url:'https://github.com/jonesdevelopment/sonar/issues'><hover:show_text:'(Click to open GitHub)'>or open a new issue on GitHub.
- ''
# Formatting of the list of subcommands shown when running the main command
subcommands: '<click:suggest_command:''/sonar %subcommand% ''><hover:show_text:''<gray>Only
players: </gray>%only_players%<br><gray>Require console: </gray>%require_console%<br><gray>Permission:
</gray><white>%permission%<br><gray>Aliases: </gray>%aliases%''><dark_aqua>
▪ <gray>/sonar %subcommand% <white>%description%'
subcommands: '<suggest-subcommand><hover:show_text:''<gray>Only players: </gray><only-players><br><gray>Require console: </gray><only-console><br><gray>Permission: </gray><white><permission><br><gray>Aliases: </gray><aliases>''><dark_aqua> ▪ <gray>/sonar <subcommand> <white><description></hover></suggest-subcommand>'
# Translations for '/sonar reload'
reload:
# Message shown when someone starts reloading Sonar
start: '%prefix%Reloading Sonar...'
start: '<prefix>Reloading Sonar...'
# Message shown when Sonar has finished reloading
finish: '%prefix%<green>Successfully reloaded <gray>(%taken%ms)'
finish: '<prefix><green>Successfully reloaded <gray>(<time-taken>ms)'
# Translations for '/sonar verbose'
verbose:
# Message shown when a player subscribes to Sonar verbose
subscribed: '%prefix%You are now viewing Sonar verbose.'
subscribe: '<prefix>You are now viewing Sonar verbose.'
# Message shown when a player unsubscribes from Sonar verbose
unsubscribed: '%prefix%You are no longer viewing Sonar verbose.'
unsubscribe: '<prefix>You are no longer viewing Sonar verbose.'
# Translations for '/sonar notify'
notify:
# Message shown when a player subscribes to Sonar attack notifications
subscribed: '%prefix%You are now viewing Sonar attack notifications.'
subscribe: '<prefix>You are now viewing Sonar attack notifications.'
# Message shown when a player unsubscribes from Sonar attack notifications
unsubscribed: '%prefix%You are no longer viewing Sonar attack notifications.'
unsubscribe: '<prefix>You are no longer viewing Sonar attack notifications.'
# Translations for '/sonar blacklist'
blacklist:
# Message shown when someone tries clearing the blacklist but is empty
empty: '%prefix%The blacklist is currently empty. Therefore, no IP addresses were
removed from the blacklist.'
empty: '<prefix>The blacklist is currently empty. Therefore, no IP addresses were removed from the blacklist.'
# Message shown when someone clears the blacklist
cleared: '%prefix%You successfully removed a total of %removed% IP address(es)
from the blacklist.'
clear: '<prefix>You successfully removed a total of <amount> IP address(es) from the blacklist.'
# Message shown when someone checks the size of the blacklist
size: '%prefix%The blacklist currently contains %amount% IP address(es).'
# Message shown when someone adds an IP address to the blacklist
added: '%prefix%Successfully added %ip% to the blacklist.'
# Message shown when someone adds an IP address to the blacklist that is verified
added-warning: '%prefix%<red>Warning: <white>%ip% is currently whitelisted. Consider
removing the IP address from the list of verified players to avoid potential
issues.'
size: '<prefix>The blacklist currently contains <amount> IP address(es).'
# Message shown when someone removes an IP address from the blacklist
removed: '%prefix%Successfully removed %ip% from the blacklist.'
remove: '<prefix>Successfully removed <ip> from the blacklist.'
# Message shown when someone adds an IP address to the blacklist
add: '<prefix>Successfully added <ip> to the blacklist.'
# Message shown when someone adds an IP address to the blacklist that is verified
add-warning: '<prefix><red>Warning: <white><ip> is currently whitelisted. Consider removing the IP address from the list of verified players to avoid potential issues.'
# Message shown when someone adds an IP address to the blacklist, but it is already blacklisted
duplicate-ip: '%prefix%The IP address you provided is already blacklisted.'
ip-duplicate: '<prefix>The IP address you provided is already blacklisted.'
# Message shown when someone removes an IP address from the blacklist, but it is not blacklisted
ip-not-found: '%prefix%The IP address you provided is not blacklisted.'
ip-not-found: '<prefix>The IP address you provided is not blacklisted.'
# Translations for '/sonar verified'
verified:
# Message shown when someone tries clearing the list of verified players but is empty
empty: '%prefix%The list of verified players is currently empty. Therefore, no
players were unverified.'
empty: '<prefix>The list of verified players is currently empty. Therefore, no players were unverified.'
# Message shown when someone clears the list of verified players
cleared: '%prefix%You successfully unverified a total of %removed% unique player(s).'
clear: '<prefix>You successfully unverified a total of <amount> unique player(s).'
# Message shown when someone checks the size of the list of verified players
size: '%prefix%There are currently %amount% unique player(s) verified.'
size: '<prefix>There are currently <amount> unique player(s) verified.'
# Message shown when someone un-verifies an IP address
removed: '%prefix%Successfully unverified %ip%.'
remove: '<prefix>Successfully unverified <ip>.'
# Message shown when someone verifies an IP address
added: '%prefix%Successfully verified %ip%.'
add: '<prefix>Successfully verified <ip>.'
# Message shown when someone tries to verify an IP address/UUID that are already verified
already: '%prefix%The IP address and UUID you provided are already verified.'
already: '<prefix>The IP address and UUID you provided are already verified.'
# Message shown when someone un-verifies an IP address, but it is not verified
ip-not-found: '%prefix%The IP address you provided is not verified.'
# Message shown when someone tries un-verifying the same IP address twice (double operation)
blocked: '%prefix%Please wait for the current operation to finish.'
ip-not-found: '<prefix>The IP address you provided is not verified.'
# Message shown when someone looks up the verification history of an IP address
history: '<prefix><yellow>Previous UUIDs for <ip>:'
history-entry: ' <gray>▪ <white><uuid>'
# Translations for '/sonar statistics'
statistics:
# Informational message that is shown above everything when viewing the statistics
header: '%prefix%Showing %type% statistics for this session:'
header: '<prefix>Showing <statistics-type> statistics for this session:'
# Message shown when a player tries viewing an unknown statistic
unknown-type: '%prefix%<red>Unknown statistics type! Available statistics: <gray>%statistics%'
unknown-type: '<prefix><red>Unknown statistics type! Available statistics: <gray><statistics>'
# Format of the general statistics message
general:
- ' <dark_aqua>▪ <gray>Verified IP addresses: <white>%verified%'
- ' <dark_aqua>▪ <gray>Verifying IP addresses: <white>%verifying%'
- ' <dark_aqua>▪ <gray>Blacklisted IP addresses: <white>%blacklisted%'
- ' <dark_aqua>▪ <gray>Currently queued logins: <white>%queued%'
- ' <dark_aqua>▪ <gray>Total non-unique joins: <white>%total_joins%'
- ' <dark_aqua>▪ <gray>Total verification attempts: <white>%total_attempts%'
- ' <dark_aqua>▪ <gray>Total failed verifications: <white>%total_failed%'
- ' <dark_aqua>▪ <gray>Server uptime: <white>%uptime%'
- ' <dark_aqua>▪ <gray>Verified IP addresses: <white><verified>'
- ' <dark_aqua>▪ <gray>Verifying IP addresses: <white><verifying>'
- ' <dark_aqua>▪ <gray>Blacklisted IP addresses: <white><blacklisted>'
- ' <dark_aqua>▪ <gray>Currently queued logins: <white><queued>'
- ' <dark_aqua>▪ <gray>Total non-unique joins: <white><total-joins>'
- ' <dark_aqua>▪ <gray>Total verification attempts: <white><total-attempts>'
- ' <dark_aqua>▪ <gray>Total failed verifications: <white><total-failed>'
- ' <dark_aqua>▪ <gray>Server uptime: <white><server-uptime>'
# Format of the CPU statistics message
cpu:
- ' <dark_aqua>▪ <gray>Process CPU usage right now: <white>%process_cpu%%'
- ' <dark_aqua>▪ <gray>System CPU usage right now: <white>%system_cpu%%'
- ' <dark_aqua>▪ <gray>Per-core process CPU usage: <white>%average_process_cpu%%'
- ' <dark_aqua>▪ <gray>Per-core system CPU usage: <white>%average_system_cpu%%'
- ' <dark_aqua>▪ <gray>General system load average: <white>%load_average%%'
- ' <dark_aqua>▪ <gray>Total amount of virtual cpus: <white>%virtual_cores%'
- ' <dark_aqua>▪ <gray>Process CPU usage right now: <white><process-cpu>%'
- ' <dark_aqua>▪ <gray>System CPU usage right now: <white><system-cpu>%'
- ' <dark_aqua>▪ <gray>Per-core process CPU usage: <white><average-process-cpu>%'
- ' <dark_aqua>▪ <gray>Per-core system CPU usage: <white><average-system-cpu>%'
- ' <dark_aqua>▪ <gray>Total amount of virtual cpus: <white><virtual-core-count>'
# Format of the memory statistics message
memory:
- ' <dark_aqua>▪ <gray>Total free memory: <white>%free_memory%'
- ' <dark_aqua>▪ <gray>Total used memory: <white>%used_memory%'
- ' <dark_aqua>▪ <gray>Total maximum memory: <white>%max_memory%'
- ' <dark_aqua>▪ <gray>Total allocated memory: <white>%total_memory%'
- ' <dark_aqua>▪ <gray>Total free memory: <white><free-memory>'
- ' <dark_aqua>▪ <gray>Total used memory: <white><used-memory>'
- ' <dark_aqua>▪ <gray>Total maximum memory: <white><max-memory>'
- ' <dark_aqua>▪ <gray>Total allocated memory: <white><total-memory>'
# Format of the network statistics message
network:
- ' <dark_aqua>▪ <gray>Current incoming used bandwidth: <white>%incoming%'
- ' <dark_aqua>▪ <gray>Current outgoing used bandwidth: <white>%outgoing%'
- ' <dark_aqua>▪ <gray>Total incoming used bandwidth: <white>%ttl_incoming%'
- ' <dark_aqua>▪ <gray>Total outgoing used bandwidth: <white>%ttl_outgoing%'
- ' <dark_aqua>▪ <gray>Current incoming used bandwidth: <white><incoming-traffic>'
- ' <dark_aqua>▪ <gray>Current outgoing used bandwidth: <white><outgoing-traffic>'
- ' <dark_aqua>▪ <gray>Total incoming used bandwidth: <white><incoming-traffic-ttl>'
- ' <dark_aqua>▪ <gray>Total outgoing used bandwidth: <white><incoming-traffic-ttl>'
# Translations for all messages during the verification process
verification:
# Translations for all debug messages during the verification
logs:
# Message logged to console whenever a new player joins the server
connection: '%name%%ip% (%protocol%) has connected.'
connection: '<username><ip> (<protocol>) has connected.'
# Message logged to console whenever a player fails verification
failed: '%ip% (%protocol%) has failed the bot check for: %reason%'
failed: '<ip> (<protocol>) has failed the bot check for: <reason>'
# Message logged to console whenever a player is blacklisted
blacklisted: '%ip% (%protocol%) was blacklisted for too many failed attempts'
blacklisted: '<ip> (<protocol>) was blacklisted for too many failed attempts'
# Message logged to console whenever a player is verified
successful: '%name% has been verified successfully (%time%s!).'
successful: '<username> has been verified successfully (<time-taken>s!).'
# Messages that are shown to the player when they are supposed to enter the CAPTCHA
captcha:
# Message shown to the player when they have to enter the answer to the captcha
enter-code: '%prefix%<green>Please enter the code in chat that is displayed on
the map.'
enter-code: '<prefix><green>Please enter the code in chat that is displayed on the map.'
# Message shown to the player when they enter the wrong answer in chat
incorrect: '%prefix%<red>You have entered the wrong code. Please try again.'
incorrect: '<prefix><red>You have entered the wrong code. Please try again.'
# Disconnect message that is shown when someone joins while the captcha hasn't been prepared yet
currently-preparing:
- '%header%'
- '<header>'
- <yellow>Your anti-bot data has not been prepared yet.
- <gray>Please wait a few seconds before trying to verify again.
- '%footer%'
- '<footer>'
# Disconnect message that is shown when someone rejoins too fast during verification
too-fast-reconnect:
- '%header%'
- '<header>'
- <gold>You reconnected too fast, try again later.
- <gray>Please wait a few seconds before trying to verify again.
- '%footer%'
- '<footer>'
# Disconnect message that is shown when someone joins but is already verifying
already-verifying:
- '%header%'
- '<header>'
- <red>Your IP address is currently being verified.
- <red>Please wait a few seconds before trying to verify again.
- '%footer%'
- '<footer>'
# Disconnect message that is shown when someone joins but is already queued for verification
already-queued:
- '%header%'
- '<header>'
- <red>Your IP address is currently queued for verification.
- <red>Please wait a few minutes before trying to verify again.
- '%footer%'
- '<footer>'
# Disconnect message that is shown when someone joins but is temporarily blacklisted
blacklisted:
- '%header%'
- '<header>'
- <red>You are currently denied from entering the server.
- <red>Please wait a few minutes to be able to join the server again.
- <gold>False positive? <gray>%support-url%
- '%footer%'
- <gold>False positive? <gray><support-url>
- '<footer>'
# Disconnect message that is shown when someone joins with an invalid username
invalid-username:
- '%header%'
- '<header>'
- <red>Your username contains invalid characters.
- '%footer%'
- '<footer>'
# Disconnect message that is shown when someone joins with a blacklisted version
blacklisted-protocol:
- '%header%'
- '<header>'
- <red>You are using a version that is not allowed on our server.
- <gold>Need help logging in? <gray>%support-url%
- '%footer%'
- <gold>Need help logging in? <gray><support-url>
- '<footer>'
# Disconnect message that is shown when someone verifies successfully
success:
- '%header%'
- '<header>'
- <green>You have successfully passed the verification.
- <white>You are now able to play on the server when you reconnect.
# Disconnect message that is shown when someone fails verification
failed:
- '%header%'
- '<header>'
- <red>You have failed the bot verification.
- <gray>Please wait a few seconds before trying to verify again.
- <gold>Need help? <gray>%support-url%
- '%footer%'
- <gold>Need help logging in? <gray><support-url>
- '<footer>'
# Translations for Sonar's actionbar verbose messages
verbose:
layout:
# Layout for Sonar's actionbar verbose
# Placeholders:
# - '%queued%' Number of queued connections
# - '%verifying%' Number of verifying connections
# - '%blacklisted%' Number of blacklisted IP addresses
# - '%total-joins%' Number of total attempted joins
# - '%verify-total%' Number of total verification attempts
# - '%verify-success%' Number of verified IP addresses
# - '%verify-failed%' Number of failed verifications
# - '%logins-per-second%' Number of logins per second
# - '%connections-per-second%' Number of connections per second
# - '%attack-duration%' Duration of the current attack
# - '%incoming-traffic%' Incoming bandwidth usage per second
# - '%outgoing-traffic%' Outgoing bandwidth usage per second
# - '%incoming-traffic-ttl%' Total incoming bandwidth usage
# - '%outgoing-traffic-ttl%' Total outgoing bandwidth usage
# - '%used-memory%' Amount of used memory of the process
# - '%total-memory%' Amount of total memory of the process
# - '%max-memory%' Amount of max memory of the process
# - '%free-memory%' Amount of free memory of the process
# - '%animation%' Customizable animated symbol
# Translations for Sonar's normal verbose output
normal: '%prefix%<gray>CPS <white>%connections-per-second% <dark_aqua>╺ <gray>Logins/s
<white>%logins-per-second% <dark_aqua>╺ <gray>Verifying <white>%verifying% <dark_aqua>|
<green>⬆ <white>%outgoing-traffic%/s <red>⬇ <white>%incoming-traffic%/s <green><bold>%animation%<reset>'
normal: '<prefix><gray>CPS <white><connections-per-second> <dark_aqua>╺ <gray>Logins/s <white><logins-per-second> <dark_aqua>╺ <gray>Verifying <white><verifying> <dark_aqua>| <green>⬆ <white><outgoing-traffic>/s <red>⬇ <white><incoming-traffic>/s <green><bold><animation>'
# Layout for Sonar's actionbar verbose during an attack
attack: '%prefix%<gray>CPS <white>%connections-per-second% <dark_aqua>╺ <gray>Logins/s
<white>%logins-per-second% <dark_aqua>╺ <gray>Queue <white>%queued% <dark_aqua>╺
<gray>Verifying <white>%verifying% <dark_aqua>╺ <gray>Duration <white>%attack-duration%
<dark_aqua>| <green>⬆ <white>%outgoing-traffic%/s <red>⬇ <white>%incoming-traffic%/s <green><bold>%animation%<reset>'
attack: '<prefix><gray>CPS <white><connections-per-second> <dark_aqua>╺ <gray>Logins/s <white><logins-per-second> <dark_aqua>╺ <gray>Queue <white><queued> <dark_aqua>╺ <gray>Verifying <white><verifying> <dark_aqua>╺ <gray>Duration <white><attack-duration> <dark_aqua>| <green>⬆ <white><outgoing-traffic>/s <red>⬇ <white><incoming-traffic>/s <green><bold><animation>'
# Animation for the action bar
# Alternatives:
# - ▙, ▛, ▜, ▟
@@ -301,11 +258,9 @@ notifications:
- <yellow><b>Sonar<reset>
- ''
- <gray>A bot attack has been detected.
- ' <dark_aqua>▪ <gray>Logins per second: <white>%logins-per-second%'
- ' <dark_aqua>▪ <gray>Verifying players: <white>%verifying%'
- ' <dark_aqua>▪ <gray>Queued players: <white>%queued%'
- ' <dark_aqua>▪ <gray>Logins per second: <white><logins-per-second>'
- ' <dark_aqua>▪ <gray>Verifying players: <white><verifying>'
- ' <dark_aqua>▪ <gray>Queued players: <white><queued>'
- ''
- <gray>View more information using <white><click:run_command:'/sonar verbose'><hover:show_text:'(Click
to run)'>/sonar verbose</hover></click><gray> or <white><click:run_command:'/sonar
stats'><hover:show_text:'(Click to run)'>/sonar stats</hover></click><gray>.
- <gray>View more information using <white><click:run_command:'/sonar verbose'><hover:show_text:'(Click to run)'>/sonar verbose</hover></click><gray> or <white><click:run_command:'/sonar stats'><hover:show_text:'(Click to run)'>/sonar stats</hover></click><gray>.
- ''
@@ -13,261 +13,229 @@
# Droit d'auteur Jones Development, un contributeur Sonar, licence sous GPL-3.0
#
# Espace réservé pour tout '%prefix%' dans ce fichier de configuration
# Espace réservé pour tout '<prefix>' dans ce fichier de configuration
prefix: <b><yellow>Sonar</b> <gray>» <white>
# Espace réservé pour tout '%support-url%' dans ce fichier de configuration
# Espace réservé pour tout '<support-url>' dans ce fichier de configuration
support-url: https://jonesdev.xyz/discord/
# Espace réservé pour tout '%header%' dans ce fichier de configuration
# Espace réservé pour tout '<header>' dans ce fichier de configuration
header:
- <b><yellow>Sonar</b><reset>
- ''
# Espace réservé pour tout '%footer%' dans ce fichier de configuration
# Espace réservé pour tout '<footer>' dans ce fichier de configuration
footer:
- <gray>Si vous pensez que c'est une erreur, veuillez contacter un administrateur.
# Message de déconnexion affiché lorsqu'un joueur se connecte mais qu'il y a trop de joueurs en ligne avec leur adresse IP
too-many-online-per-ip:
- '%header%'
- '<header>'
- <red>Il y a trop de joueurs en ligne avec votre adresse IP.
- '%footer%'
- '<footer>'
# Traductions pour les commandes
commands:
# Message affiché lorsqu'un joueur tente d'exécuter /sonar sans permission
no-permission: "%prefix%<red>Vous n'avez pas la permission d'exécuter cette commande."
no-permission: "<prefix><red>Vous n'avez pas la permission d'exécuter cette commande."
# Message affiché lorsqu'une commande est utilisée de manière incorrecte
incorrect-usage: '%prefix%<red>Utilisation : /sonar %usage%'
incorrect-usage: '<prefix><red>Utilisation : /sonar <subcommand-usage>'
# Message affiché lorsqu'une adresse IP invalide est fournie (format invalide)
invalid-ip-address: "%prefix%L'adresse IP que vous avez fournie semble invalide."
invalid-ip-address: "<prefix>L'adresse IP que vous avez fournie semble invalide."
# Message affiché lorsque la console exécute une commande qui est uniquement pour les joueurs
player-only: "%prefix%<red>Vous ne pouvez exécuter cette commande qu'en tant que joueur."
player-only: "<prefix><red>Vous ne pouvez exécuter cette commande qu'en tant que joueur."
# Message affiché lorsqu'un joueur exécute une commande qui est uniquement pour la console
console-only: '%prefix%<red>Pour des raisons de sécurité, vous ne pouvez exécuter cette commande que via la console.'
# Message affiché lorsqu'un joueur exécute trop rapidement les commandes Sonar
cool-down: "%prefix%<red>Vous ne pouvez exécuter cette commande qu'une fois toutes les 0,5 secondes."
cool-down-left: '%prefix%<red>Veuillez attendre encore <bold>%time-left%<reset><red>.'
console-only: '<prefix><red>Pour des raisons de sécurité, vous ne pouvez exécuter cette commande que via la console.'
# Message affiché lorsqu'un joueur n'a pas la permission d'exécuter une certaine sous-commande
subcommand-no-permission: "%prefix%<red>Vous n'avez pas la permission d'exécuter cette sous-commande. <gray>(%permission%)"
subcommand-no-permission: "<prefix><red>Vous n'avez pas la permission d'exécuter cette sous-commande. <gray>(<permission>)"
# Traductions pour '/sonar'
main:
# Message informatif affiché au-dessus de tout lors de l'exécution de la commande principale
header:
- <yellow>Exécution de <b>Sonar</b> %version% sur %platform%.
- <gray><click:open_url:'https://github.com/jonesdevelopment/sonar'>(C) %copyright_year%
Jones Development et les contributeurs de Sonar</click>
- <yellow>Exécution de <b>Sonar</b> <version> sur <platform>.
- <gray><click:open_url:'https://github.com/jonesdevelopment/sonar'>(C) <copyright-year> Jones Development et les contributeurs de Sonar</click>
- ''
- <gray>Besoin d'aide ou avez-vous des questions ?
- <white><click:open_url:'https://jonesdev.xyz/discord/'><hover:show_text:'(Cliquez
pour ouvrir Discord)'>Ouvrez un ticket sur Discord </hover></click><click:open_url:'https://github.com/jonesdevelopment/sonar/issues'><hover:show_text:'(Cliquez
pour ouvrir GitHub)'>ou ouvrez un nouveau problème sur GitHub.
- <white><click:open_url:'https://jonesdev.xyz/discord/'><hover:show_text:'(Cliquez pour ouvrir Discord)'>Ouvrez un ticket sur Discord </hover></click><click:open_url:'https://github.com/jonesdevelopment/sonar/issues'><hover:show_text:'(Cliquez pour ouvrir GitHub)'>ou ouvrez un nouveau problème sur GitHub.
- ''
# Format de la liste des sous-commandes affichée lors de l'exécution de la commande principale
subcommands: '<click:suggest_command:''/sonar %subcommand% ''><hover:show_text:''<gray>Seulement pour les joueurs : </gray>%only_players%<br><gray>Nécessite la console : </gray>%require_console%<br><gray>Permission : </gray><white>%permission%<br><gray>Alias : </gray>%aliases%''><dark_aqua> ▪ <gray>/sonar %subcommand% <white>%description%'
subcommands: '<suggest-subcommand><hover:show_text:''<gray>Seulement pour les joueurs : </gray><only-players><br><gray>Nécessite la console : </gray><only-console><br><gray>Permission : </gray><white><permission><br><gray>Alias : </gray><aliases>''><dark_aqua> ▪ <gray>/sonar <subcommand> <white><description></hover></suggest-subcommand>'
# Traductions pour '/sonar reload'
reload:
# Message affiché lorsque quelqu'un commence à recharger Sonar
start: '%prefix%Rechargement de Sonar...'
start: '<prefix>Rechargement de Sonar...'
# Message affiché lorsque Sonar a fini de recharger
finish: '%prefix%<green>Rechargement réussi <gray>(%taken%ms)'
finish: '<prefix><green>Rechargement réussi <gray>(<time-taken>ms)'
# Traductions pour '/sonar verbose'
verbose:
# Message affiché lorsqu'un joueur s'abonne aux détails verbeux de Sonar
subscribed: '%prefix%Vous visualisez maintenant les détails verbeux de Sonar.'
subscribe: '<prefix>Vous visualisez maintenant les détails verbeux de Sonar.'
# Message affiché lorsqu'un joueur se désabonne des détails verbeux de Sonar
unsubscribed: '%prefix%Vous ne visualisez plus les détails verbeux de Sonar.'
unsubscribe: '<prefix>Vous ne visualisez plus les détails verbeux de Sonar.'
# Traductions pour '/sonar notify'
notify:
# Message affiché lorsqu'un joueur s'abonne aux notifications d'attaque de Sonar
subscribed: "%prefix%Vous visualisez maintenant les notifications d'attaque de Sonar."
subscribed: "<prefix>Vous visualisez maintenant les notifications d'attaque de Sonar."
# Message affiché lorsqu'un joueur se désabonne des notifications d'attaque de Sonar
unsubscribed: "%prefix%Vous ne visualisez plus les notifications d'attaque de Sonar."
unsubscribed: "<prefix>Vous ne visualisez plus les notifications d'attaque de Sonar."
# Traductions pour '/sonar blacklist'
blacklist:
# Message affiché lorsque quelqu'un tente de vider la liste noire mais qu'elle est vide
empty: "%prefix%La liste noire est actuellement vide. Par conséquent, aucune adresse IP n'a été supprimée de la liste noire."
empty: "<prefix>La liste noire est actuellement vide. Par conséquent, aucune adresse IP n'a été supprimée de la liste noire."
# Message affiché lorsque quelqu'un vide la liste noire
cleared: '%prefix%Vous avez réussi à supprimer un total de %removed% adresse(s) IP de la liste noire.'
clear: '<prefix>Vous avez réussi à supprimer un total de <amount> adresse(s) IP de la liste noire.'
# Message affiché lorsque quelqu'un vérifie la taille de la liste noire
size: '%prefix%La liste noire contient actuellement %amount% adresse(s) IP.'
size: '<prefix>La liste noire contient actuellement <amount> adresse(s) IP.'
# Message affiché lorsque quelqu'un ajoute une adresse IP à la liste noire
added: '%prefix%Adresse IP %ip% ajoutée avec succès à la liste noire.'
add: '<prefix>Adresse IP <ip> ajoutée avec succès à la liste noire.'
# Message affiché lorsque quelqu'un ajoute une adresse IP à la liste noire qui est vérifiée
added-warning: "%prefix%<red>Avertissement : <white>%ip% est actuellement sur liste blanche. Considérez la suppression de l'adresse IP de la liste des joueurs vérifiés pour éviter des problèmes potentiels."
add-warning: "<prefix><red>Avertissement : <white><ip> est actuellement sur liste blanche. Considérez la suppression de l'adresse IP de la liste des joueurs vérifiés pour éviter des problèmes potentiels."
# Message affiché lorsque quelqu'un supprime une adresse IP de la liste noire
removed: '%prefix%Adresse IP %ip% supprimée avec succès de la liste noire.'
remove: '<prefix>Adresse IP <ip> supprimée avec succès de la liste noire.'
# Message affiché lorsque quelqu'un ajoute une adresse IP à la liste noire, mais qu'elle est déjà en liste noire
duplicate-ip: "%prefix%L'adresse IP que vous avez fournie est déjà sur la liste noire."
duplicate-ip: "<prefix>L'adresse IP que vous avez fournie est déjà sur la liste noire."
# Message affiché lorsque quelqu'un supprime une adresse IP de la liste noire, mais qu'elle n'est pas en liste noire
ip-not-found: "%prefix%L'adresse IP que vous avez fournie ne figure pas sur la liste noire."
ip-not-found: "<prefix>L'adresse IP que vous avez fournie ne figure pas sur la liste noire."
# Traductions pour '/sonar verified'
verified:
# Message affiché lorsque quelqu'un tente de vider la liste des joueurs vérifiés mais qu'elle est vide
empty: "%prefix%La liste des joueurs vérifiés est actuellement vide. Par conséquent, aucun joueur n'a été désapprouvé."
empty: "<prefix>La liste des joueurs vérifiés est actuellement vide. Par conséquent, aucun joueur n'a été désapprouvé."
# Message affiché lorsque quelqu'un vide la liste des joueurs vérifiés
cleared: '%prefix%Vous avez réussi à désapprouver un total de %removed% joueur(s) unique(s).'
clear: '<prefix>Vous avez réussi à désapprouver un total de <amount> joueur(s) unique(s).'
# Message affiché lorsque quelqu'un vérifie la taille de la liste des joueurs vérifiés
size: '%prefix%Il y a actuellement %amount% joueur(s) unique(s) vérifié(s).'
size: '<prefix>Il y a actuellement <amount> joueur(s) unique(s) vérifié(s).'
# Message affiché lorsque quelqu'un désapprouve une adresse IP
removed: '%prefix%Adresse IP %ip% désapprouvée avec succès.'
remove: '<prefix>Adresse IP <ip> désapprouvée avec succès.'
# Message affiché lorsque quelqu'un vérifie une adresse IP
added: '%prefix%Adresse IP %ip% vérifiée avec succès.'
add: '<prefix>Adresse IP <ip> vérifiée avec succès.'
# Message affiché lorsque quelqu'un tente de vérifier une adresse IP/UUID qui est déjà vérifiée
already: "%prefix%L'adresse IP et l'UUID que vous avez fournis sont déjà vérifiés."
already: "<prefix>L'adresse IP et l'UUID que vous avez fournis sont déjà vérifiés."
# Message affiché lorsque quelqu'un désapprouve une adresse IP, mais qu'elle n'est pas vérifiée
ip-not-found: "%prefix%L'adresse IP que vous avez fournie n'est pas vérifiée."
# Message affiché lorsque quelqu'un tente de désapprouver la même adresse IP deux fois (double opération)
blocked: "%prefix%Veuillez attendre la fin de l'opération en cours."
ip-not-found: "<prefix>L'adresse IP que vous avez fournie n'est pas vérifiée."
# Message shown when someone looks up the verification history of an IP address
history: '<prefix><yellow>UUID précédents pour <ip>:'
history-entry: ' <gray>▪ <white><uuid>'
# Traductions pour '/sonar statistics'
statistics:
# Message informatif affiché au-dessus de tout lors de la visualisation des statistiques
header: '%prefix%Affichage des statistiques %type% pour cette session :'
header: '<prefix>Affichage des statistiques <statistics-type> pour cette session :'
# Message affiché lorsqu'un joueur tente de visualiser une statistique inconnue
unknown-type: '%prefix%<red>Type de statistiques inconnu ! Statistiques disponibles : <gray>%statistics%'
unknown-type: '<prefix><red>Type de statistiques inconnu ! Statistiques disponibles : <gray><statistics>'
# Format du message de statistiques général
general:
- ' <dark_aqua>▪ <gray>Adresses IP vérifiées : <white>%verified%'
- ' <dark_aqua>▪ <gray>En train de vérifier les adresses IP : <white>%verifying%'
- ' <dark_aqua>▪ <gray>Adresses IP en liste noire : <white>%blacklisted%'
- ' <dark_aqua>▪ <gray>Connexions en attente actuelles : <white>%queued%'
- ' <dark_aqua>▪ <gray>Total des connexions non uniques : <white>%total_joins%'
- ' <dark_aqua>▪ <gray>Total des tentatives de vérification : <white>%total_attempts%'
- ' <dark_aqua>▪ <gray>Total des vérifications échouées : <white>%total_failed%'
- ' <dark_aqua>▪ <gray>Temps de fonctionnement du serveur : <white>%uptime%'
- ' <dark_aqua>▪ <gray>Adresses IP vérifiées : <white><verified>'
- ' <dark_aqua>▪ <gray>En train de vérifier les adresses IP : <white><verifying>'
- ' <dark_aqua>▪ <gray>Adresses IP en liste noire : <white><blacklisted>'
- ' <dark_aqua>▪ <gray>Connexions en attente actuelles : <white><queued>'
- ' <dark_aqua>▪ <gray>Total des connexions non uniques : <white><total-joins>'
- ' <dark_aqua>▪ <gray>Total des tentatives de vérification : <white><total-attempts>'
- ' <dark_aqua>▪ <gray>Total des vérifications échouées : <white><total-failed>'
- ' <dark_aqua>▪ <gray>Temps de fonctionnement du serveur : <white><server-uptime>'
# Format du message de statistiques CPU
cpu:
- ' <dark_aqua>▪ <gray>Utilisation actuelle du CPU du processus : <white>%process_cpu%%'
- ' <dark_aqua>▪ <gray>Utilisation actuelle du CPU du système : <white>%system_cpu%%'
- ' <dark_aqua>▪ <gray>Utilisation du CPU par cœur du processus : <white>%average_process_cpu%%'
- ' <dark_aqua>▪ <gray>Utilisation du CPU par cœur du système : <white>%average_system_cpu%%'
- ' <dark_aqua>▪ <gray>Charge système générale : <white>%load_average%%'
- ' <dark_aqua>▪ <gray>Nombre total de processeurs virtuels : <white>%virtual_cores%'
- ' <dark_aqua>▪ <gray>Utilisation actuelle du CPU du processus : <white><process-cpu>%'
- ' <dark_aqua>▪ <gray>Utilisation actuelle du CPU du système : <white><system-cpu>%'
- ' <dark_aqua>▪ <gray>Utilisation du CPU par cœur du processus : <white><average-process-cpu>%'
- ' <dark_aqua>▪ <gray>Utilisation du CPU par cœur du système : <white><average-system-cpu>%'
- ' <dark_aqua>▪ <gray>Nombre total de processeurs virtuels : <white><virtual-core-count>'
# Format du message de statistiques de mémoire
memory:
- ' <dark_aqua>▪ <gray>Mémoire libre totale : <white>%free_memory%'
- ' <dark_aqua>▪ <gray>Mémoire utilisée totale : <white>%used_memory%'
- ' <dark_aqua>▪ <gray>Mémoire maximale totale : <white>%max_memory%'
- ' <dark_aqua>▪ <gray>Mémoire allouée totale : <white>%total_memory%'
- ' <dark_aqua>▪ <gray>Mémoire libre totale : <white><free-memory>'
- ' <dark_aqua>▪ <gray>Mémoire utilisée totale : <white><used-memory>'
- ' <dark_aqua>▪ <gray>Mémoire maximale totale : <white><max-memory>'
- ' <dark_aqua>▪ <gray>Mémoire allouée totale : <white><total-memory>'
# Format du message de statistiques réseau
network:
- ' <dark_aqua>▪ <gray>Bande passante entrante actuelle utilisée : <white>%incoming%'
- ' <dark_aqua>▪ <gray>Bande passante sortante actuelle utilisée : <white>%outgoing%'
- ' <dark_aqua>▪ <gray>Bande passante entrante totale utilisée : <white>%ttl_incoming%'
- ' <dark_aqua>▪ <gray>Bande passante sortante totale utilisée : <white>%ttl_outgoing%'
- ' <dark_aqua>▪ <gray>Bande passante entrante actuelle utilisée : <white><incoming-traffic>'
- ' <dark_aqua>▪ <gray>Bande passante sortante actuelle utilisée : <white><outgoing-traffic>'
- ' <dark_aqua>▪ <gray>Bande passante entrante totale utilisée : <white><incoming-traffic-ttl>'
- ' <dark_aqua>▪ <gray>Bande passante sortante totale utilisée : <white><outgoing-traffic-ttl>'
# Traductions pour tous les messages pendant le processus de vérification
verification:
# Traductions pour tous les messages de débogage pendant la vérification
logs:
# Message enregistré dans la console chaque fois qu'un nouveau joueur rejoint le serveur
connection: "%name%%ip% (%protocol%) s'est connecté."
connection: "<username><ip> (<protocol>) s'est connecté."
# Message enregistré dans la console chaque fois qu'un joueur échoue à la vérification
failed: '%ip% (%protocol%) a échoué au test du bot pour : %reason%'
failed: '<ip> (<protocol>) a échoué au test du bot pour : <reason>'
# Message enregistré dans la console chaque fois qu'un joueur est sur liste noire
blacklisted: '%ip% (%protocol%) a été mis sur liste noire pour trop de tentatives échouées'
blacklisted: '<ip> (<protocol>) a été mis sur liste noire pour trop de tentatives échouées'
# Message enregistré dans la console chaque fois qu'un joueur est vérifié
successful: '%name% a été vérifié avec succès (%time%s !).'
successful: '<username> a été vérifié avec succès (<time-taken>s !).'
# Messages affichés au joueur lorsqu'il doit entrer le CAPTCHA
captcha:
# Message affiché au joueur lorsqu'il doit entrer la réponse au CAPTCHA
enter-code: '%prefix%<green>Veuillez entrer le code affiché sur la carte.'
enter-code: '<prefix><green>Veuillez entrer le code affiché sur la carte.'
# Message affiché au joueur lorsqu'il entre la mauvaise réponse dans le chat
incorrect: '%prefix%<red>Vous avez entré le mauvais code. Veuillez réessayer.'
incorrect: '<prefix><red>Vous avez entré le mauvais code. Veuillez réessayer.'
# Message de déconnexion affiché lorsque quelqu'un rejoint alors que le CAPTCHA n'a pas été préparé
currently-preparing:
- '%header%'
- '<header>'
- <yellow>Vos données anti-bot n'ont pas encore été préparées.
- <gray>Veuillez patienter quelques secondes avant d'essayer de vous vérifier à nouveau.
- '%footer%'
- '<footer>'
# Message de déconnexion affiché lorsque quelqu'un se reconnecte trop rapidement pendant la vérification
too-fast-reconnect:
- '%header%'
- '<header>'
- <gold>Vous avez rejoint trop rapidement, réessayez plus tard.
- <gray>Veuillez patienter quelques secondes avant d'essayer de vous vérifier à nouveau.
- '%footer%'
- '<footer>'
# Message de déconnexion affiché lorsque quelqu'un rejoint mais est déjà en vérification
already-verifying:
- '%header%'
- '<header>'
- <red>Votre adresse IP est actuellement en cours de vérification.
- <red>Veuillez patienter quelques secondes avant d'essayer de vous vérifier à nouveau.
- '%footer%'
- '<footer>'
# Message de déconnexion affiché lorsque quelqu'un rejoint mais est déjà en attente de vérification
already-queued:
- '%header%'
- '<header>'
- <red>Votre adresse IP est actuellement en attente de vérification.
- <red>Veuillez patienter quelques minutes avant d'essayer de vous vérifier à nouveau.
- '%footer%'
- '<footer>'
# Message de déconnexion affiché lorsque quelqu'un rejoint mais est temporairement mis sur liste noire
blacklisted:
- '%header%'
- '<header>'
- <red>Vous êtes actuellement interdit d'entrer sur le serveur.
- <red>Veuillez patienter quelques minutes pour pouvoir rejoindre le serveur à nouveau.
- <gold>Erreur ? <gray>%support-url%
- '%footer%'
- <gold>Erreur ? <gray><support-url>
- '<footer>'
# Message de déconnexion affiché lorsque quelqu'un rejoint avec un nom d'utilisateur non valide
invalid-username:
- '%header%'
- '<header>'
- <red>Votre nom d'utilisateur contient des caractères non valides.
- '%footer%'
- '<footer>'
# Message de déconnexion affiché lorsque quelqu'un rejoint avec une version sur liste noire
blacklisted-protocol:
- '%header%'
- '<header>'
- <red>Vous utilisez une version non autorisée sur notre serveur.
- <gold>Besoin d'aide pour vous connecter ? <gray>%support-url%
- '%footer%'
- <gold>Besoin d'aide pour vous connecter ? <gray><support-url>
- '<footer>'
# Message de déconnexion affiché lorsque quelqu'un réussit la vérification
success:
- '%header%'
- '<header>'
- <green>Vous avez réussi la vérification.
- <white>Vous pouvez maintenant jouer sur le serveur lorsque vous vous reconnectez.
# Message de déconnexion affiché lorsque quelqu'un échoue à la vérification
failed:
- '%header%'
- '<header>'
- <red>Vous avez échoué à la vérification du bot.
- <gray>Veuillez patienter quelques secondes avant d'essayer de vous vérifier à nouveau.
- <gold>Besoin d'aide ? <gray>%support-url%
- '%footer%'
- <gold>Besoin d'aide pour vous connecter ? <gray><support-url>
- '<footer>'
# Translations for Sonar's actionbar verbose messages
verbose:
layout:
# Mise en page pour le verbeux de la barre d'action de Sonar
# Espacements réservés :
# - '%queued%' Nombre de connexions en attente
# - '%verifying%' Nombre de connexions en cours de vérification
# - '%blacklisted%' Nombre d'adresses IP sur liste noire
# - '%total-joins%' Nombre total de connexions tentées
# - '%verify-total%' Nombre total de tentatives de vérification
# - '%verify-success%' Nombre d'adresses IP vérifiées
# - '%verify-failed%' Nombre de vérifications échouées
# - '%logins-per-second%' Nombre de connexions par seconde
# - '%connections-per-second%' Nombre de connexions par seconde
# - '%attack-duration%' Durée de l'attaque actuelle
# - '%incoming-traffic%' Utilisation de la bande passante entrante par seconde
# - '%outgoing-traffic%' Utilisation de la bande passante sortante par seconde
# - '%incoming-traffic-ttl%' Utilisation totale de la bande passante entrante
# - '%outgoing-traffic-ttl%' Utilisation totale de la bande passante sortante
# - '%used-memory%' Quantité de mémoire utilisée par le processus
# - '%total-memory%' Quantité de mémoire totale du processus
# - '%max-memory%' Quantité de mémoire maximale du processus
# - '%free-memory%' Quantité de mémoire libre du processus
# - '%animation%' Symbole animé personnalisable
# Traductions pour la sortie verbeuse normale de Sonar
normal: '%prefix%<gray>CPS <white>%connections-per-second% <dark_aqua>╺ <gray>Logins/s
<white>%logins-per-second% <dark_aqua>╺ <gray>Vérification <white>%verifying% <dark_aqua>|
<green>⬆ <white>%outgoing-traffic%/s <red>⬇ <white>%incoming-traffic%/s <green><bold>%animation%<reset>'
normal: '<prefix><gray>CPS <white><connections-per-second> <dark_aqua>╺ <gray>Logins/s <white><logins-per-second> <dark_aqua>╺ <gray>Vérification <white><verifying> <dark_aqua>| <green>⬆ <white><outgoing-traffic>/s <red>⬇ <white><incoming-traffic>/s <green><bold><animation>'
# Traductions pour la sortie verbeuse lors d'une attaque
attack: '%prefix%<gray>CPS <white>%connections-per-second% <dark_aqua>╺ <gray>Logins/s
<white>%logins-per-second% <dark_aqua>╺ <gray>Queue <white>%queued% <dark_aqua>╺
<gray>Vérification <white>%verifying% <dark_aqua>╺ <gray>Durée <white>%attack-duration%
<dark_aqua>| <green>⬆ <white>%outgoing-traffic%/s <red>⬇ <white>%incoming-traffic%/s <green><bold>%animation%<reset>'
attack: '<prefix><gray>CPS <white><connections-per-second> <dark_aqua>╺ <gray>Logins/s <white><logins-per-second> <dark_aqua>╺ <gray>Queue <white><queued> <dark_aqua>╺ <gray>Vérification <white><verifying> <dark_aqua>╺ <gray>Durée <white><attack-duration> <dark_aqua>| <green>⬆ <white><outgoing-traffic>/s <red>⬇ <white><incoming-traffic>/s <green><bold><animation>'
# Animation pour la sortie verbeuse
# Alternatives:
# - ▙, ▛, ▜, ▟
@@ -290,11 +258,9 @@ notifications:
- <yellow><b>Sonar<reset>
- ''
- <gray>Une attaque de bot a été détectée.
- ' <dark_aqua>▪ <gray>Connexions par seconde: <white>%logins-per-second%'
- ' <dark_aqua>▪ <gray>Vérification des joueurs: <white>%verifying%'
- ' <dark_aqua>▪ <gray>Joueurs en file d''attente: <white>%queued%'
- ' <dark_aqua>▪ <gray>Connexions par seconde: <white><logins-per-second>'
- ' <dark_aqua>▪ <gray>Vérification des joueurs: <white><verifying>'
- ' <dark_aqua>▪ <gray>Joueurs en file d''attente: <white><queued>'
- ''
- <gray>View more information using <white><click:run_command:'/sonar verbose'><hover:show_text:'(Click
to run)'>/sonar verbose</hover></click><gray> or <white><click:run_command:'/sonar
stats'><hover:show_text:'(Click to run)'>/sonar stats</hover></click><gray>.
- <gray>View more information using <white><click:run_command:'/sonar verbose'><hover:show_text:'(Click to run)'>/sonar verbose</hover></click><gray> or <white><click:run_command:'/sonar stats'><hover:show_text:'(Click to run)'>/sonar stats</hover></click><gray>.
- ''
Loaded 100 of 115 files, more files were not shown because too many files have changed in this diff. Show more