Compare commits

..
70 Commits
Author SHA1 Message Date
Jones f618d1ce77 Release 2.0.29 (#291) 2024-05-11 13:34:46 +02:00
Jones ea35904d33 Add more softdepends for compatibility (#290)
* fix(velocity): add more soft dependencies

* fix(bukkit): add more soft dependencies

* fix(bungee): add more soft dependencies
2024-05-11 13:27:37 +02:00
Jones ececb66bf1 Update translations (#289)
* feat: add missing German translations

* feat: add missing Czech translations

* feat: add missing Russian translations (thanks to @F3F5)

* fix: typo in messages/en.yml

* feat: add Dutch translations
2024-05-10 20:22:31 +02:00
Jones 6caa37e3e0 Recode the map CAPTCHA (#288)
* feat: [wip] upgrade to capja 1.0.0

* fix: don't store buffer to avoid memory issues

* fix: use one buffer array for storing RGB and indexes

* fix: color palette sometimes showing incorrect colors

* build(deps): bump capja from 1.0.0 to 1.0.1

* fix: actually let the user customize the background

* build(deps): bump capja from 1.0.1 to 1.0.2
2024-05-10 19:04:44 +02:00
Jones a057199e0b Generalize packets and clean up code (#287)
* refactor: some renaming to better understand the functionality

* refactor: port pack modified FastUUID for LoginSuccessPacket

* refactor: rename package utility to util

* fix: don't create a snapshot of FinishConfigurationPacket

* fix: don't allow encoding of LoginAcknowledgedPacket

* refactor: PlayerTickPacket -> PlayerPositionIdlePacket

* style: rename some variables for clarity

* fix: optimize UniversalChatPacket

* refactor: ChangedBlock -> BlockUpdate

* style: codestyle consistency
2024-05-05 13:45:37 +02:00
Jones d0bbcdc600 Use pre-encoded packet snapshots to reduce CPU usage (#286)
* feat: cache most packets Sonar needs to encode

* fix: make sure to also cache MapInfo packets

* fix: use NanoLimbo's hashing algorithm to consume less RAM

* fix: don't print every byte for the MapDataPacket

* feat: notify user about packet preparing
2024-05-04 21:42:36 +02:00
Jones 0a5ce856f3 Small microoptimizations (#285)
* fix: optimize experience bar and keepalive packets

* fix: optimize chat message reading

* feat: cache LoginSuccessPacket
2024-05-04 20:49:47 +02:00
Jones 261acbd9e2 Use experience bar instead of action bar (#284) 2024-05-04 20:18:34 +02:00
Jones 595b1378ab Release 2.0.28 (#282) 2024-05-03 16:59:52 +02:00
AkkariinandMichel Elkenwaat 3012ed0d7a Add experimental Geyser support (#281)
* feat: added experimental geyser support.

* feat: send a special `PlayerAbilities` packet for the bedrock client
write 0x06 (`Flying` + `Allow Flying`) to prevent the bedrock clients falling during the `map-captcha` phase.
After testing, it has been confirmed that the bedrock client is unable to attempt to de-fly status. So just add it.

* fix: if the first move are success passed predicted. then don't reset tick.

* fix: tick skipping is allowed for bedrock clients.

* feat: fix limit & only set `bedrockSpawned` when client is bedrock.

* feat: make geyser checks configurable

* style: small codestyle improvements

* style: make code more human-readable

* fix: only check first packet if it's actually the first packet

* fix: [wip] completely exclude geyser from the gravity check [ci skip]

* style: fix comment

* chore: update `zh` language file. [ci skip]

* fix: Geyser users timing out because of a stupid mistake

* fix: only skip vehicle state in position check

* feat: add missing German translations [ci skip]

* feat: add missing Czech translations [ci skip]

* fix: make sure the vehicle check is recommended [ci skip]

* style: inline some stuff for better readability

* fix(geyser): completely skip gravity check for consistency [ci skip]

* fix(geyser): skip vehicle packet order check (UDP can mess this up)

* fix: add missing translations [ci skip]

---------

Co-authored-by: Michel Elkenwaat <admin@jonesdev.xyz>
2024-05-02 14:20:21 +02:00
Akkariin 4ccecde901 Add config & messages files for Chinese Simplified (#280)
* chore: add config & messages files for Chinese Simplified.

* style: rename `cn` to `zh`. [ci skip]
2024-04-28 19:16:55 +02:00
AkkariinandMichel Elkenwaat 87fcfb68f3 1.20.5 Support (#278)
* feat: support 1.20.5

* fix: vehicle & barrier block id

* fix: missing abilities & remove entities packet id

* refactor: merge RegistryBundlePacket into RegistryDataPacket

* style: clean up some code

* feat: implement transfer packet logic for 1.20.5+

* fix: check hostname before protocolVersion

* fix: transfer configuration

* fix: SetSlotPacket for 1.20.5

* chore: remove encoder/decoder debugging message. [ci skip]

* style: remove double empty line [ci skip]

---------

Co-authored-by: Michel Elkenwaat <admin@jonesdev.xyz>
2024-04-28 12:50:17 +02:00
Jones a17b726798 Release 2.0.27 (#277) 2024-04-25 16:57:21 +02:00
Jones 50d3b43f63 Don't fail players when disconnecting (#276) 2024-04-24 15:19:29 +02:00
Jones 0ff0194894 Also check for PaddleBoat packets (#275)
* feat: also check for PaddleBoat packets

* fix: remove debug messages
2024-04-24 15:15:00 +02:00
dependabot[bot] 279fe2e3e9 Bump netty-all from 4.1.108.Final to 4.1.109.Final (#273)
Bumps [io.netty:netty-all](https://github.com/netty/netty) from 4.1.108.Final to 4.1.109.Final.
- [Commits](https://github.com/netty/netty/compare/netty-4.1.108.Final...netty-4.1.109.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-04-23 14:50:13 +02:00
Jones ffd3a7e5c2 Improve performance through multi-threading (#271)
* fix: optimize JVMProcessInformation#formatMemory

* fix: formatting (spacing) in formatMemory

* fix: don't re-serialize action bar format forever

* fix: optimize chat and title notifications

* feat: change task timers to run less tasks more efficiently

* feat: run all scheduled tasks asynchronously

* fix: small enable message issue

* fix(velocity): don't unnecessarily store the handshake packet

* fix: oh yes I love exclamation marks

* feat: move handshake handling logic to common module

* feat: move login handling logic to common module

* fix(bungee): decoding error caused by sending wrong original packet

* style: make FallbackChannelHandlerAdapter#initialLogin final

* fix: rearrange task priorities and improve performance by removing dead code

* fix: better debug if protocol is invalid

* fix: update default read timeout value to prevent laggy players from being blacklisted

* fix: move the unknown protocol version check to login
2024-04-23 14:49:56 +02:00
Jones 4474f44b0a Release 2.0.26 (#269) 2024-04-20 10:14:13 +02:00
Jones b159947d6d Recode max accounts per IP check (#268) 2024-04-18 18:39:23 +02:00
Jones a29dfa15e4 Implement max accounts per IP check on Bukkit (#266)
* feat(bukkit): add max online per IP check

* fix: remove debug message haha lol
2024-04-18 17:57:00 +02:00
Jones cb9fe75db8 Allow user to manually verify players (#265)
* feat: add /sonar verified add <ip> <uuid/username>

* fix: remove useless and stupid local ip check

* fix: use correct translation for add message
2024-04-18 17:55:09 +02:00
Jones 23815351f0 Release 2.0.25 (#261) 2024-04-07 14:44:06 +02:00
Jones 5a68ea8af6 Restructure dependency management (#260)
* feat: recode command invocation source system

* feat: remove obsolete audience listener on Velocity

* fix: invalid console action bar sending when uuid is null

* feat: microoptimize InvocationSource#isPlayer

* build: recode and restructure dependency management

* build(bungee,bukkit): downgrade to adventure-platform 4.3.2

* build(all): minimize JAR (excluding api)

* build: fix minimizing breaking kyori adventure

* style: mark some api functions as internal as they would require relocating when used

* build(bukkit): mark as folia supported

* build: recode `build-sonar` task

* fix: remove unused/outdated files

* chore: I think I was dead when writing this

* build(deps): load minimessage and serializer-gson using libby

* refactor: remove some duplicate/unnecessary code
2024-04-07 14:38:40 +02:00
Jones 9b54b484fb Update Russian translations (#259) 2024-04-05 15:02:45 +02:00
Jones 4fa56f2f3c Hide blacklist logs during attack (#257) 2024-04-03 13:11:36 +02:00
Jones 51751584dc Improve code readability and maintainability (#255)
* style: add comments explaining certain functions/methods

* style: add link to stackoverflow comment explaining memory formatting

* style: add some more documentation and explanations to the API

* style: improve Discord webhook API readability
2024-04-03 13:04:48 +02:00
Jones 3955f9bc8a Improve verification limits and state system (#254)
* fix: fail for disconnecting during verification

* chore: update bot check failed message

* feat: improve state, failing and blacklist system
2024-04-02 21:20:20 +02:00
Jones e333162032 Implement vehicle mounting check (#251)
* fix: recode captcha-on-fail mechanic

* feat: remove unnecessary max-ping option

* feat: [wip] vehicle check and other improvements

* feat: add RemoveEntitiesPacket

* fix: RemoveEntitiesPacket for 1.7

* fix: PlayerInputPacket for 1.7

* feat: validate jump and unmount state (but don't fail)

* fix: RemoveEntitiesPacket not working on 1.17

* fix: boat entity type being broken on 1.13

* feat: [wip] support all versions (1.13-1.13.2 are broken)

* fix: boat entity type for 1.13-1.13.2

* feat: add missing vehicle configurations

* fix: wait 10s between each action bar on 1.7

* fix: remove unnecessary flush in KeepAlive

* fix: only expect PlayerInput packets when needed
2024-03-30 19:16:27 +01:00
dependabot[bot] b3a7f4de09 Bump netty-all from 4.1.107.Final to 4.1.108.Final (#248)
Bumps [io.netty:netty-all](https://github.com/netty/netty) from 4.1.107.Final to 4.1.108.Final.
- [Commits](https://github.com/netty/netty/compare/netty-4.1.107.Final...netty-4.1.108.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-03-25 13:45:29 +01:00
dependabot[bot] 5752f1e208 Bump lombok from 1.18.30 to 1.18.32 (#249)
Bumps [org.projectlombok:lombok](https://github.com/projectlombok/lombok) from 1.18.30 to 1.18.32.
- [Changelog](https://github.com/projectlombok/lombok/blob/master/doc/changelog.markdown)
- [Commits](https://github.com/projectlombok/lombok/compare/v1.18.30...v1.18.32)

---
updated-dependencies:
- dependency-name: org.projectlombok:lombok
  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-03-25 13:45:16 +01:00
MISHA 1c6ba7050d Add a missed line to Russian translations (#247) 2024-03-24 20:55:35 +01:00
MISHA 340d4a225d Add the missing Russian language in messages (#245)
* Update ru.yml

* Update ru.yml
2024-03-24 20:39:10 +01:00
Jones 475bf28ecf Release 2.0.24 (#244) 2024-03-24 20:25:30 +01:00
Jones 9c29753084 Recode map CAPTCHA (#243)
* feat: implement capja for CAPTCHA generation

* feat: implement map color palette for 1.7.2-1.20.4
2024-03-24 20:17:18 +01:00
Jones f0f5196fbe Don't drop messages that aren't ByteBufs (#242) 2024-03-22 14:01:32 +01:00
Jones d98ce28a59 Implement missing packet limits (#241) 2024-03-22 13:56:55 +01:00
MISHA e0d5c67cdf Fix translation error in the Russian language (#240) 2024-03-16 11:25:43 +01:00
Jones 65e059368c Release 2.0.23 (#239) 2024-03-13 16:15:38 +01:00
dependabot[bot] b7a031da12 Bump io.github.goooler.shadow from 8.1.5 to 8.1.7 (#238)
Bumps [io.github.goooler.shadow](https://github.com/Goooler/shadow) from 8.1.5 to 8.1.7.
- [Release notes](https://github.com/Goooler/shadow/releases)
- [Commits](https://github.com/Goooler/shadow/compare/v8.1.5...v8.1.7)

---
updated-dependencies:
- dependency-name: io.github.goooler.shadow
  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-03-11 19:11:17 +01:00
Jones e15206a382 Implement CAPTCHA-on-fail option (#237)
* feat: add option to display a CAPTCHA on fail

* fix: remove debug message
2024-03-10 13:11:09 +01:00
Jones 5e4af3053e Fix local database cache duplicating after reload (#236) 2024-03-07 20:19:42 +01:00
Jones 7bb231ae0d Release 2.0.22 (#234) 2024-03-05 18:07:55 +01:00
Jones a57dfc4e98 Correctly close database connection (#233) 2024-03-04 17:29:39 +01:00
Jones 618db4804b Recode bStats metrics implementation (#232)
* feat: add verification timing and map captcha timing to bstats charts

* fix: use correct char ids for bStats

* fix: implement shutdown method to correctly stop certain services

* style: small codestyle improvements
2024-03-03 18:31:27 +01:00
Jones 613f793dbf Relocate database drivers to avoid compatibility issues (#231) 2024-03-03 10:03:48 +01:00
Jones 40964d431c Release 2.0.21 (#230) 2024-03-01 22:58:39 +01:00
Jones b9deff9c0b Remove unused protocol ids to avoid exploits (#229) 2024-03-01 22:55:33 +01:00
Jones e3f27171cf Fix NullPointerException (#227) 2024-03-01 22:50:46 +01:00
Jones d3601e10fe Remove pipelines if they are not needed anymore (#226)
* fix: optimize counting of connections and logins

* fix: remove Sonar's pipelines if they aren't needed anymore
2024-03-01 19:25:13 +01:00
Jones 258e425ba1 Use correct packet limits for PluginMessagePacket (#225) 2024-03-01 17:04:24 +01:00
Jones 915e3f593a Release 2.0.20 (#224) 2024-03-01 15:31:01 +01:00
Jones ef525ce718 Only calculate mitigation stats if webhook is enabled (#223) 2024-03-01 15:14:52 +01:00
Jones 0fbb71a09f Also check protocols on BungeeCord servers (#222) 2024-03-01 14:56:36 +01:00
Jones 6ebe44bad3 Drop connections from unknown protocol versions (#221) 2024-03-01 14:51:43 +01:00
Jones 153d56eeb4 Implement experimental support for MariaDB (#220)
* feat: add MariaDB as another db type in the config

* fix: make sure to disable logging for unnecessary ORM stuff

* fix: use pooled connection source to remove unnecessary reflection

* feat: support MariaDB (experimental)
2024-02-29 17:56:58 +01:00
Jones c214d43b3e Use correct multiple-byte unit for formatting (#219) 2024-02-25 12:58:02 +01:00
Jones f3444de4b8 Optimize codecs by removing unused dimensions (#218) 2024-02-24 23:18:34 +01:00
Jones a5bf0947b5 Remove duplicate audience listener (#217) 2024-02-24 14:29:28 +01:00
Jones 3dd5b345cb Release 2.0.19 (#216) 2024-02-23 18:09:17 +01:00
Jones c7581f51d2 Cache held components' NBT and JSON (#215)
* feat: cache nbt and serialized json in ComponentHolder

* style: small clean up of all files
2024-02-22 19:14:16 +01:00
Jones d556275bf9 Update the packet classes to include the "Packet" suffix (#214) 2024-02-22 19:03:25 +01:00
Jones ac662433db Bump net.kyori.adventure to 4.16.0 (#213) 2024-02-22 18:56:49 +01:00
Jones 03cb75bf91 Fix FileNotFoundException (#212) 2024-02-21 19:56:18 +01:00
micartey 541933d472 Add German translations (#211)
* feat: german translations

* fix: remove/change some outdated yaml keys
2024-02-21 13:58:59 +01:00
Jones 8336279136 Fix Discord webhook embed new lines not working (#210) 2024-02-20 15:39:59 +01:00
MISHA ea9e53a350 Correct Russian language in messages (#206)
* correct russian language

* to look like original style
2024-02-19 20:35:06 +01:00
Jones 0477a574e2 Use a shared channel handler for Fallback (#209) 2024-02-19 20:32:38 +01:00
Jones c8cdba4504 Recode Sonar's statistics system (#208)
* feat(all): completely recode statistics system

* fix(api): remove unused message translations
2024-02-19 20:24:22 +01:00
Jones 83dbfcb8a8 Completely recode component handling logic (#207)
* feat(all): completely recode component handling logic

* fix: the channel is always closed/unusable if there's no decoder
2024-02-19 20:05:00 +01:00
dependabot[bot] 5b62396f9b Bump netty-all from 4.1.106.Final to 4.1.107.Final (#203)
Bumps [io.netty:netty-all](https://github.com/netty/netty) from 4.1.106.Final to 4.1.107.Final.
- [Commits](https://github.com/netty/netty/compare/netty-4.1.106.Final...netty-4.1.107.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-02-19 13:50:20 +01:00
141 changed files with 6110 additions and 3176 deletions

No files matched your search

-1
View File
@@ -8,7 +8,6 @@ joining the [Discord](https://jonesdev.xyz/discord).
2. Follow the coding guidelines for your code.
3. Test your code before committing and pushing it.
4. Use [semantic](https://gist.github.com/joshbuchea/6f47e86d2510bce28f8e7f42ae84c716) commit messages for your commits.
5. Create a feature branch or push to the `dev` branch.
## Coding Guidelines:
-1
View File
@@ -30,6 +30,5 @@ gradlew*
*.jpg
*.jpeg
*.log
*.yml
*.svg
*.txt
+47 -67
View File
@@ -6,7 +6,7 @@ buildscript {
plugins {
id("java")
id("com.github.johnrengelman.shadow") version "8.1.1"
id("io.github.goooler.shadow") version "8.1.7"
id("io.toolebox.git-versioner") version "1.6.7"
}
@@ -26,90 +26,70 @@ allprojects {
}
apply(plugin = "java")
apply(plugin = "com.github.johnrengelman.shadow")
apply(plugin = "io.github.goooler.shadow")
dependencies {
compileOnly("org.projectlombok:lombok:1.18.30")
annotationProcessor("org.projectlombok:lombok:1.18.30")
compileOnly("org.projectlombok:lombok:1.18.32")
annotationProcessor("org.projectlombok:lombok:1.18.32")
testCompileOnly("org.projectlombok:lombok:1.18.30")
testAnnotationProcessor("org.projectlombok:lombok:1.18.30")
val adventureVersion = "4.15.0"
testCompileOnly("org.projectlombok:lombok:1.18.32")
testAnnotationProcessor("org.projectlombok:lombok:1.18.32")
// adventure
implementation("net.kyori:adventure-text-minimessage:$adventureVersion")
implementation("net.kyori:adventure-text-serializer-gson:$adventureVersion")
// adventure nbt
implementation("net.kyori:adventure-nbt:$adventureVersion")
compileOnly("net.kyori:adventure-text-minimessage:4.16.0")
compileOnly("net.kyori:adventure-text-serializer-gson:4.16.0")
compileOnly("com.j256.ormlite:ormlite-jdbc:6.1") // ORMLite
compileOnly("com.github.ben-manes.caffeine:caffeine:3.1.8") // caching
compileOnly("io.netty:netty-all:4.1.106.Final") // netty
compileOnly("io.netty:netty-all:4.1.109.Final") // netty
// Library/dependency loading
compileOnly("com.alessiodp.libby:libby-core:2.0.0-SNAPSHOT")
}
}
dependencies {
sequenceOf("api", "bukkit", "bungee", "common", "velocity").forEach {
// We want the jar to actually contain our modules
implementation(project(":$it"))
tasks {
shadowJar {
// Set the file name of the shadowed jar
archiveFileName.set("${rootProject.name}-${project.name.replaceFirstChar { it.uppercase() }}.jar")
// Remove file timestamps
isPreserveFileTimestamps = false
// Relocate libraries
relocate("org.bstats", "xyz.jonesdev.sonar.libs.bstats")
relocate("com.alessiodp.libby", "xyz.jonesdev.sonar.libs.libby")
relocate("com.simpleyaml", "xyz.jonesdev.sonar.libs.yaml")
relocate("com.google.gson", "xyz.jonesdev.sonar.libs.gson")
relocate("com.j256.ormlite", "xyz.jonesdev.sonar.libs.ormlite")
relocate("com.github.benmanes.caffeine", "xyz.jonesdev.sonar.libs.caffeine")
relocate("com.mysql", "xyz.jonesdev.sonar.libs.mysql")
relocate("org.mariadb", "xyz.jonesdev.sonar.libs.mariadb")
relocate("xyz.jonesdev.capja", "xyz.jonesdev.sonar.libs.capja")
// Exclude unnecessary metadata information
exclude("META-INF/*/**")
}
compileJava {
options.encoding = "UTF-8"
}
jar {
manifest {
// Set the implementation version, so we can create exact version
// information in-game and make it accessible to the user.
attributes["Implementation-Version"] = rootProject.version
attributes["Implementation-Vendor"] = "Jones Development, Sonar Contributors"
}
}
}
}
tasks {
jar {
manifest {
// Set the implementation version, so we can create exact version
// information in-game and make it accessible to the user.
attributes["Implementation-Version"] = version
attributes["Implementation-Vendor"] = "Jones Development, Sonar Contributors"
}
}
shadowJar {
// Set the file name of the shadowed jar
archiveFileName.set("${rootProject.name}.jar")
// Remove file timestamps
isPreserveFileTimestamps = false
// bStats has to be relocated to the Sonar package otherwise it throws an exception
// https://github.com/Bastian/bstats-metrics/blob/master/base/src/main/java/org/bstats/MetricsBase.java#L251
relocate("org.bstats", "xyz.jonesdev.sonar.libs.bstats")
// https://github.com/jonesdevelopment/sonar/issues/46
// Relocate some packages, so we don't run into issues where we accidentally use Velocity's classes
relocate("com.alessiodp.libby", "xyz.jonesdev.sonar.libs.libby")
relocate("xyz.jonesdev.cappuccino", "xyz.jonesdev.sonar.libs.cappuccino")
// We have to be careful here, so we don't accidentally break adventure
relocate("net.kyori.adventure", "xyz.jonesdev.sonar.libs.adventure") {
exclude("net.kyori.adventure.text.**")
exclude("net.kyori.adventure.audience.Audience")
exclude("net.kyori.adventure.title.*")
}
relocate("net.kyori.examination", "xyz.jonesdev.sonar.libs.examination")
relocate("net.kyori.option", "xyz.jonesdev.sonar.libs.option")
// Relocate dynamically loaded libraries
relocate("com.simpleyaml", "xyz.jonesdev.sonar.libs.yaml")
relocate("com.google.code.gson", "xyz.jonesdev.sonar.libs.gson")
relocate("com.j256.ormlite", "xyz.jonesdev.sonar.libs.ormlite")
relocate("com.github.benmanes.caffeine", "xyz.jonesdev.sonar.libs.caffeine")
// We want to load our own Gson dynamically
exclude("com/google/gson/**")
// Exclude unnecessary metadata information
exclude("META-INF/versions/**")
}
compileJava {
options.encoding = "UTF-8"
}
// This is a small wrapper tasks to simplify the building process
register("build-sonar") {
dependsOn(clean, shadowJar)
val subprojects = listOf("api", "common", "bukkit", "bungee", "velocity")
val buildTasks = subprojects.flatMap { listOf("$it:clean", "$it:shadowJar") }
dependsOn(buildTasks)
}
}
-6
View File
@@ -1,6 +0,0 @@
#!/usr/bin/env bash
# Fix permissions so we are actually able to run our gradle script
chmod +x gradle*
# You can also use clean shadowJar if something goes wrong here
# e.g. "./gradlew clean shadowJar --stacktrace"
./gradlew build-sonar --stacktrace
+1 -1
View File
@@ -1,3 +1,3 @@
group=xyz.jonesdev.sonar
version=2.0.18
version=2.0.29
description=Effective Anti-bot plugin for Velocity, BungeeCord, and Bukkit (1.7-latest)
+1 -1
View File
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
@@ -17,11 +17,10 @@
package xyz.jonesdev.sonar.api;
import com.alessiodp.libby.LibraryManager;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import net.kyori.adventure.audience.Audience;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import xyz.jonesdev.sonar.api.attack.AttackTracker;
import xyz.jonesdev.sonar.api.command.subcommand.SubcommandRegistry;
import xyz.jonesdev.sonar.api.config.SonarConfiguration;
@@ -29,34 +28,36 @@ import xyz.jonesdev.sonar.api.controller.VerifiedPlayerController;
import xyz.jonesdev.sonar.api.event.SonarEventManager;
import xyz.jonesdev.sonar.api.fallback.Fallback;
import xyz.jonesdev.sonar.api.logger.LoggerWrapper;
import xyz.jonesdev.sonar.api.statistics.SonarStatistics;
import xyz.jonesdev.sonar.api.timer.SystemTimer;
import xyz.jonesdev.sonar.api.verbose.Notification;
import xyz.jonesdev.sonar.api.verbose.Verbose;
import java.io.File;
import java.net.InetAddress;
import java.text.DecimalFormat;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.UUID;
public interface Sonar {
DecimalFormat DECIMAL_FORMAT = new DecimalFormat("#,###.##");
String LINE_SEPARATOR = "\n"; // Using System.lineSeparator is broken, for some reason...
Gson GENERAL_GSON = new GsonBuilder()
.disableInnerClassSerialization()
.create();
/**
* Since we want to use Adventure on every server platform,
* we have to use their platform module to support BungeeCord and Bukkit
*/
Map<String, Audience> AUDIENCES = new ConcurrentHashMap<>();
@ApiStatus.Internal
@Nullable Audience audience(final @Nullable UUID uniqueId);
/**
* @return The platform the plugin is being run on
*/
@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
*/
@@ -76,7 +77,7 @@ public interface Sonar {
@NotNull SystemTimer getLaunchTimer();
@NotNull LibraryManager getLibraryManager();
@NotNull SonarStatistics getStatistics();
/**
* Set a custom verbose handler
@@ -23,16 +23,15 @@ import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.config.SonarConfiguration;
import xyz.jonesdev.sonar.api.event.impl.AttackDetectedEvent;
import xyz.jonesdev.sonar.api.event.impl.AttackMitigatedEvent;
import xyz.jonesdev.sonar.api.profiler.JVMProfiler;
import xyz.jonesdev.sonar.api.statistics.Counters;
import xyz.jonesdev.sonar.api.statistics.Statistics;
import xyz.jonesdev.sonar.api.timer.SystemTimer;
import java.util.Optional;
import static xyz.jonesdev.sonar.api.jvm.JVMProcessInformation.*;
@Getter
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class AttackTracker implements JVMProfiler {
public final class AttackTracker {
public static final AttackTracker INSTANCE = new AttackTracker();
private @Nullable AttackStatistics currentAttack;
private int attackThreshold;
@@ -52,7 +51,7 @@ public final class AttackTracker implements JVMProfiler {
}
public void checkIfUnderAttack() {
final long joinsPerSecond = Counters.LOGINS_PER_SECOND.estimatedSize();
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 minPlayers = Sonar.get().getConfig().getMinPlayersForAttack();
@@ -67,7 +66,7 @@ public final class AttackTracker implements JVMProfiler {
if (currentAttack == null) {
currentAttack = new AttackStatistics();
currentAttack.successfulVerifications = Sonar.get().getVerifiedPlayerController().estimatedSize();
currentAttack.failedVerifications = Statistics.FAILED_VERIFICATIONS.get();
currentAttack.failedVerifications = Sonar.get().getStatistics().getTotalFailedVerifications();
Sonar.get().getEventManager().publish(new AttackDetectedEvent());
Sonar.get().getNotificationHandler().sendAttackNotification();
} else {
@@ -80,7 +79,7 @@ public final class AttackTracker implements JVMProfiler {
// Update joins per second peak if necessary
currentAttack.peakJoinsPerSecond = joinsPerSecond;
}
final long connectionsPerSecond = Counters.CONNECTIONS_PER_SECOND.estimatedSize();
final long connectionsPerSecond = Sonar.get().getStatistics().getConnectionsPerSecond();
if (connectionsPerSecond > currentAttack.peakConnectionsPerSecond) {
// Update connections per second peak if necessary
currentAttack.peakConnectionsPerSecond = connectionsPerSecond;
@@ -102,42 +101,44 @@ public final class AttackTracker implements JVMProfiler {
Sonar.get().getEventManager().publish(new AttackMitigatedEvent(currentAttack));
if (++attackThreshold > Sonar.get().getConfig().getMinAttackThreshold()) {
// Save attack statistics
final long deltaInMillis = currentAttack.duration.delay();
final String peakCPU = Sonar.DECIMAL_FORMAT.format(currentAttack.peakProcessCPUUsage);
final String peakMem = formatMemory(currentAttack.peakProcessMemoryUsage);
final String peakBPS = Sonar.DECIMAL_FORMAT.format(currentAttack.peakJoinsPerSecond);
final String peakCPS = Sonar.DECIMAL_FORMAT.format(currentAttack.peakConnectionsPerSecond);
final long minutes = deltaInMillis / (60 * 1000); // Convert milliseconds to minutes
final double seconds = (deltaInMillis % (60 * 1000)) / 1000D; // Convert remaining milliseconds to seconds
final String formattedDuration = String.format("%d minutes, %.0f seconds", minutes, seconds);
final String startTimestamp = String.valueOf(currentAttack.duration.getStart() / 1000L);
final String endTimestamp = String.valueOf(System.currentTimeMillis() / 1000L);
final long blacklisted = Sonar.get().getFallback().getBlacklist().estimatedSize();
// Calculate during-attack-statistics using their deltas
final long totalVerified = Sonar.get().getVerifiedPlayerController().estimatedSize();
final long verified = Math.max(totalVerified - currentAttack.successfulVerifications, 0);
final long totalFailed = Statistics.FAILED_VERIFICATIONS.get();
final long failed = Math.max(totalFailed - currentAttack.failedVerifications, 0);
// Post webhook to Discord
Optional.ofNullable(Sonar.get().getConfig().getWebhook().getDiscordWebhook())
.ifPresent(webhook -> 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));
return new SonarConfiguration.Webhook.Embed(
config.getTitle(), config.getTitleUrl(), description, config.getR(), config.getG(), config.getB());
}));
.ifPresent(webhook -> {
// Save attack statistics
final long deltaInMillis = currentAttack.duration.delay();
final String peakCPU = Sonar.DECIMAL_FORMAT.format(currentAttack.peakProcessCPUUsage);
final String peakMem = formatMemory(currentAttack.peakProcessMemoryUsage);
final String peakBPS = Sonar.DECIMAL_FORMAT.format(currentAttack.peakJoinsPerSecond);
final String peakCPS = Sonar.DECIMAL_FORMAT.format(currentAttack.peakConnectionsPerSecond);
final long minutes = deltaInMillis / (60 * 1000); // Convert milliseconds to minutes
final double seconds = (deltaInMillis % (60 * 1000)) / 1000D; // Convert remaining milliseconds to seconds
final String formattedDuration = String.format("%d minutes, %.0f seconds", minutes, seconds);
final String startTimestamp = String.valueOf(currentAttack.duration.getStart() / 1000L);
final String endTimestamp = String.valueOf(System.currentTimeMillis() / 1000L);
final long blacklisted = Sonar.get().getFallback().getBlacklist().estimatedSize();
// Calculate during-attack-statistics using their deltas
final long totalVerified = Sonar.get().getVerifiedPlayerController().estimatedSize();
final long verified = Math.max(totalVerified - currentAttack.successfulVerifications, 0);
final long totalFailed = Sonar.get().getStatistics().getTotalFailedVerifications();
final long failed = Math.max(totalFailed - currentAttack.failedVerifications, 0);
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));
return new SonarConfiguration.Webhook.Embed(
config.getTitle(), config.getTitleUrl(), description, config.getR(), config.getG(), config.getB());
});
});
}
// Reset the attack status
@@ -150,10 +151,4 @@ public final class AttackTracker implements JVMProfiler {
attackThreshold = 0;
}
}
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
public boolean isCurrentlyUnderAttack() {
final int minDelay = Sonar.get().getConfig().getAttackCooldownDelay();
return currentAttack != null && currentAttack.timer.delay() < minDelay;
}
}
@@ -23,34 +23,38 @@ import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage;
import java.util.UUID;
import java.util.function.Predicate;
@Getter
@RequiredArgsConstructor
public abstract class InvocationSource {
private final String name;
public final class InvocationSource {
private final UUID uuid;
private final Audience audience;
private final boolean player;
private final Predicate<String> permissionFunction;
/**
* Sends an empty chat message to the command executor
* @return True, if {@link InvocationSource#uuid} is not null
* @apiNote This indicates a player as a player will always have a UUID
*/
public final void sendMessage() {
sendMessage(Component.empty());
public boolean isPlayer() {
return uuid != null;
}
/**
* Sends a message to the command executor
*
* @apiNote We should probably use cached components...
* (See {@link InvocationSource#sendMessage(Component)})
*/
public final void sendMessage(final String legacy) {
public void sendMessage(final String legacy) {
sendMessage(MiniMessage.miniMessage().deserialize(legacy));
}
/**
* Sends a message to the command executor
*/
public final void sendMessage(final Component component) {
public void sendMessage(final Component component) {
audience.sendMessage(component);
}
}
@@ -53,13 +53,8 @@ public abstract class Subcommand {
final InetAddress inetAddress;
try {
inetAddress = InetAddress.getByName(rawIP);
if (inetAddress.isAnyLocalAddress() || inetAddress.isLoopbackAddress()) {
source.sendMessage(SONAR.getConfig().getCommands().getIllegalIpAddress());
return null;
}
} catch (UnknownHostException exception) {
source.sendMessage(SONAR.getConfig().getCommands().getIncorrectIpAddress());
source.sendMessage(SONAR.getConfig().getCommands().getInvalidIpAddress());
return null;
}
return inetAddress;
@@ -17,14 +17,18 @@
package xyz.jonesdev.sonar.api.config;
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 org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.command.SonarCommand;
import xyz.jonesdev.sonar.api.logger.LoggerWrapper;
import xyz.jonesdev.sonar.api.ormlite.MariaDbDatabaseTypeAdapter;
import xyz.jonesdev.sonar.api.ormlite.MysqlDatabaseTypeAdapter;
import xyz.jonesdev.sonar.api.webhook.DiscordWebhook;
import java.io.File;
@@ -41,7 +45,7 @@ import java.util.regex.Pattern;
public final class SonarConfiguration {
@Getter
private final SimpleYamlConfig generalConfig, messagesConfig, webhookConfig;
private final File languageFile;
private final File languageFile, pluginFolder;
static final LoggerWrapper LOGGER = new LoggerWrapper() {
@@ -62,6 +66,7 @@ public final class SonarConfiguration {
};
public SonarConfiguration(final @NotNull File pluginFolder) {
this.pluginFolder = pluginFolder;
this.messagesConfig = new SimpleYamlConfig(new File(pluginFolder, "messages.yml"));
this.generalConfig = new SimpleYamlConfig(new File(pluginFolder, "config.yml"));
this.webhookConfig = new SimpleYamlConfig(new File(pluginFolder, "webhook.yml"));
@@ -105,6 +110,11 @@ public final class SonarConfiguration {
}
public void load() {
// Make sure the plugin folder actually exists before trying to copy files into it
if (!pluginFolder.exists() && !pluginFolder.mkdirs()) {
throw new IllegalStateException("Could not create plugin folder (insufficient permissions?)");
}
// Generate the language file and check what it's set to
Language preferredLanguage = getPreferredLanguage();
if (preferredLanguage == Language.SYSTEM) {
@@ -162,26 +172,41 @@ public final class SonarConfiguration {
// Verification
verification.timing = Verification.Timing.valueOf(generalConfig.getString("verification.timing"));
verification.gravity.enabled = generalConfig.getBoolean("verification.checks.gravity.enabled");
verification.gravity.checkCollisions = generalConfig.getBoolean("verification.checks.gravity.check-collisions");
verification.gravity.captchaOnFail = generalConfig.getBoolean("verification.checks.gravity.captcha-on-fail");
verification.gravity.maxMovementTicks = clamp(generalConfig.getInt("verification.checks.gravity.max-movement-ticks"), 2, 100);
verification.gravity.maxIgnoredTicks = clamp(generalConfig.getInt("verification.checks.gravity.max-ignored-ticks"), 1, 128);
verification.gravity.gamemode = Verification.Gravity.Gamemode.valueOf(generalConfig.getString("verification.checks.gravity.gamemode"));
verification.vehicle.timing = Verification.Timing.valueOf(generalConfig.getString("verification.checks.vehicle.timing"));
verification.map.timing = Verification.Timing.valueOf(generalConfig.getString("verification.checks.map-captcha.timing"));
verification.map.randomizePositions = generalConfig.getBoolean("verification.checks.map-captcha.random-position");
verification.map.distortionsFactorX = clamp(generalConfig.getInt(
"verification.checks.map-captcha.distortions-factor-x"), 0, 100) / 100D;
verification.map.distortionsFactorY = clamp(generalConfig.getInt(
"verification.checks.map-captcha.distortions-factor-y"), 0, 100) / 100D;
verification.map.randomLinesAmount = generalConfig.getInt("verification.checks.map-captcha.random-lines");
verification.map.randomOvalsAmount = generalConfig.getInt("verification.checks.map-captcha.random-ovals");
verification.map.randomizeFontSize = generalConfig.getBoolean("verification.checks.map-captcha.random-font-size");
verification.map.precomputeAmount = generalConfig.getInt("verification.checks.map-captcha.precompute");
verification.map.maxDuration = generalConfig.getInt("verification.checks.map-captcha.max-duration");
verification.map.scratches = generalConfig.getBoolean("verification.checks.map-captcha.effects.scratches");
verification.map.ripple = generalConfig.getBoolean("verification.checks.map-captcha.effects.ripple");
verification.map.bump = generalConfig.getBoolean("verification.checks.map-captcha.effects.bump");
verification.map.distortion.enabled = generalConfig.getBoolean("verification.checks.map-captcha.effects.distortion.enabled");
verification.map.distortion.shape = generalConfig.getInt("verification.checks.map-captcha.effects.distortion.shape");
verification.map.distortion.distance = generalConfig.getInt("verification.checks.map-captcha.effects.distortion.distance");
verification.map.distortion.density = (float) generalConfig.getYaml().getDouble("verification.checks.map-captcha.effects.distortion.density");
verification.map.distortion.mix = (float) generalConfig.getYaml().getDouble("verification.checks.map-captcha.effects.distortion.mix");
final String backgroundImagePath = generalConfig.getString("verification.checks.map-captcha.background");
if (!backgroundImagePath.isEmpty()) {
verification.map.backgroundImage = new File(pluginFolder, backgroundImagePath);
if (!verification.map.backgroundImage.exists()) {
Sonar.get().getLogger().error("The background image does not exist! Please check the configuration.");
}
} else {
verification.map.backgroundImage = null;
}
verification.map.autoColor = generalConfig.getBoolean("verification.checks.map-captcha.auto-color");
verification.map.precomputeAmount = clamp(generalConfig.getInt("verification.checks.map-captcha.precompute"), 1, 1000);
verification.map.maxDuration = clamp(generalConfig.getInt("verification.checks.map-captcha.max-duration"), 5000, 360000);
verification.map.maxTries = generalConfig.getInt("verification.checks.map-captcha.max-tries");
verification.map.dictionary = generalConfig.getString("verification.checks.map-captcha.dictionary");
verification.map.fonts = generalConfig.getStringList("verification.checks.map-captcha.fonts");
verification.brand.enabled = generalConfig.getBoolean("verification.checks.client-brand.enabled");
verification.brand.validRegex = Pattern.compile(generalConfig.getString("verification.checks.client-brand.valid-regex"));
@@ -189,14 +214,23 @@ public final class SonarConfiguration {
verification.validNameRegex = Pattern.compile(generalConfig.getString("verification.checks.valid-name-regex"));
verification.validLocaleRegex = Pattern.compile(generalConfig.getString("verification.checks.valid-locale-regex"));
verification.maxPing = clamp(generalConfig.getInt("verification.checks.max-ping"), 500, 30000);
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.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);
verification.blacklistThreshold = clamp(generalConfig.getInt("verification.blacklist-threshold"), 0, 100);
verification.whitelistedProtocols.clear();
@@ -217,7 +251,8 @@ public final class SonarConfiguration {
webhook.embed.title = webhookConfig.getString(embedPath + "title");
webhook.embed.titleUrl = webhookConfig.getString(embedPath + "title-url");
webhook.embed.description = fromList(webhookConfig.getStringList(embedPath + "description"));
webhook.embed.description = formatString(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");
@@ -242,8 +277,7 @@ public final class SonarConfiguration {
noPermission = formatString(messagesConfig.getString("commands.no-permission"));
commands.incorrectCommandUsage = formatString(messagesConfig.getString("commands.incorrect-usage"));
commands.incorrectIpAddress = formatString(messagesConfig.getString("commands.invalid-ip-address"));
commands.illegalIpAddress = formatString(messagesConfig.getString("commands.illegal-ip-address"));
commands.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"));
@@ -276,6 +310,8 @@ public final class SonarConfiguration {
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"));
@@ -291,30 +327,28 @@ public final class SonarConfiguration {
verification.blacklistLog = formatString(messagesConfig.getString("verification.logs.blacklisted"));
verification.successLog = formatString(messagesConfig.getString("verification.logs.successful"));
verification.gravity.youAreBeingChecked = deserialize(formatString(messagesConfig.getString("verification.welcome")));
verification.map.enterCode = deserialize(formatString(messagesConfig.getString("verification.captcha.enter-code")));
verification.map.enterCodeActionBar = formatString(messagesConfig.getString("verification.captcha.action-bar"));
verification.map.failedCaptcha = deserialize(formatString(messagesConfig.getString("verification.captcha.incorrect")));
verification.currentlyPreparing = deserialize(fromList(messagesConfig.getStringList("verification.currently-preparing")));
verification.tooManyPlayers = deserialize(fromList(messagesConfig.getStringList("verification.too-many-players")));
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.alreadyConnected = deserialize(fromList(messagesConfig.getStringList("verification.already-online")));
verification.verificationSuccess = deserialize(fromList(messagesConfig.getStringList("verification.success")));
verification.verificationFailed = deserialize(fromList(messagesConfig.getStringList("verification.failed")));
verbose.actionBarLayout = formatString(messagesConfig.getString("verbose.layout.normal"));
verbose.actionBarLayoutDuringAttack = formatString(messagesConfig.getString("verbose.layout.attack"));
verbose.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.notificationChat = formatString(fromList(messagesConfig.getStringList("notifications.chat")));
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 URL getAsset(final String url, final @NotNull Language language) {
@@ -328,7 +362,11 @@ public final class SonarConfiguration {
}
private static int clamp(final int v, final int max, final int min) {
return Math.max(Math.min(v, min), max);
final int output = Math.max(Math.min(v, min), max);
if (output != v) {
Sonar.get().getLogger().warn("Clamped configuration value {} to {}", v, output);
}
return output;
}
public String formatAddress(final InetAddress inetAddress) {
@@ -389,17 +427,18 @@ public final class SonarConfiguration {
@Getter
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public static final class Verbose {
private String actionBarLayout;
private String actionBarLayoutDuringAttack;
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 String notificationChat;
private Component notificationChat;
}
@Getter
@@ -414,41 +453,55 @@ public final class SonarConfiguration {
private Timing timing;
@Getter
@RequiredArgsConstructor
public enum Timing {
ALWAYS, DURING_ATTACK, NEVER
ALWAYS("Always"),
DURING_ATTACK("During Attack"),
NEVER("Never");
private final String displayName;
}
private final Map map = new Map();
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 {
private Timing timing;
private boolean randomizePositions;
private boolean randomizeFontSize;
private double distortionsFactorX;
private double distortionsFactorY;
private int randomLinesAmount;
private int randomOvalsAmount;
private boolean scratches;
private boolean ripple;
private boolean bump;
private File backgroundImage;
private boolean autoColor;
private int precomputeAmount;
private int maxDuration;
private int maxTries;
private String dictionary;
private Component enterCode;
private Component failedCaptcha;
private String enterCodeActionBar;
private List<String> fonts;
private Smear distortion = new Smear();
@Getter
public static final class Smear {
private boolean enabled;
private int shape;
private int distance;
private float density;
private float mix;
}
}
@Getter
public static final class Gravity {
private boolean enabled;
private boolean checkCollisions;
private boolean captchaOnFail;
private Gamemode gamemode;
private int maxMovementTicks;
private int maxIgnoredTicks;
private Component youAreBeingChecked;
@Getter
@RequiredArgsConstructor
@@ -463,6 +516,11 @@ public final class SonarConfiguration {
}
}
@Getter
public static final class Vehicle {
private Timing timing;
}
@Getter
public static final class Brand {
private boolean enabled;
@@ -470,6 +528,14 @@ 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;
@@ -481,18 +547,16 @@ public final class SonarConfiguration {
private String blacklistLog;
private int maxLoginPackets;
private int maxPing;
private int readTimeout;
private int reconnectDelay;
private int rememberTime;
private int blacklistTime;
private int blacklistThreshold;
private final Collection<Integer> whitelistedProtocols = new HashSet<>(0);
private final Collection<Integer> blacklistedProtocols = new HashSet<>(0);
private Component tooManyPlayers;
private Component tooFastReconnect;
private Component invalidUsername;
private Component alreadyConnected;
private Component verificationSuccess;
private Component verificationFailed;
private Component alreadyVerifying;
@@ -506,8 +570,7 @@ public final class SonarConfiguration {
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public static final class Commands {
private String incorrectCommandUsage;
private String incorrectIpAddress;
private String illegalIpAddress;
private String invalidIpAddress;
private String subCommandNoPerm;
private String playersOnly;
private String consoleOnly;
@@ -524,6 +587,8 @@ public final class SonarConfiguration {
private String blacklistSize;
private String verifiedRemove;
private String verifiedAdd;
private String verifiedAlready;
private String verifiedNotFound;
private String verifiedCleared;
private String verifiedSize;
@@ -557,10 +622,12 @@ public final class SonarConfiguration {
@Getter
@RequiredArgsConstructor
public enum Type {
MYSQL("com.mysql.cj.jdbc.NonRegisteringDriver"),
NONE(null);
MYSQL("jdbc:mysql://%s:%d/%s", new MysqlDatabaseTypeAdapter()),
MARIADB("jdbc:mariadb://%s:%d/%s", new MariaDbDatabaseTypeAdapter()),
NONE(null, null);
private final String driverClassName;
private final String connectionString;
private final DatabaseType databaseType;
}
private Type type;
@@ -19,7 +19,7 @@ package xyz.jonesdev.sonar.api.controller;
import com.j256.ormlite.dao.Dao;
import com.j256.ormlite.dao.DaoManager;
import com.j256.ormlite.jdbc.JdbcSingleConnectionSource;
import com.j256.ormlite.jdbc.JdbcPooledConnectionSource;
import com.j256.ormlite.stmt.QueryBuilder;
import com.j256.ormlite.support.ConnectionSource;
import com.j256.ormlite.table.TableUtils;
@@ -27,14 +27,15 @@ import lombok.Getter;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.Range;
import org.jetbrains.annotations.Unmodifiable;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.config.SonarConfiguration;
import xyz.jonesdev.sonar.api.logger.LoggerWrapper;
import xyz.jonesdev.sonar.api.model.VerifiedPlayer;
import java.lang.reflect.Method;
import java.net.InetAddress;
import java.sql.*;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.*;
@@ -43,7 +44,6 @@ import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public final class VerifiedPlayerController {
private static final Map<String, Collection<UUID>> MAP = new ConcurrentHashMap<>();
private static final ExecutorService DB_UPDATE_SERVICE = Executors.newSingleThreadExecutor();
private static final LoggerWrapper LOGGER = new LoggerWrapper() {
@@ -64,6 +64,7 @@ public final class VerifiedPlayerController {
}
};
private final Map<String, Collection<UUID>> cache = new ConcurrentHashMap<>(128);
private @Nullable ConnectionSource connectionSource;
private Dao<VerifiedPlayer, Integer> dao;
private QueryBuilder<VerifiedPlayer, Integer> queryBuilder;
@@ -71,8 +72,9 @@ public final class VerifiedPlayerController {
private final @NotNull SonarConfiguration.Database.Type cachedDatabaseType;
public VerifiedPlayerController() {
final SonarConfiguration.Database database = Sonar.get().getConfig().getDatabase();
// Cache selected database type, so we don't need to call Sonar.get() every time
cachedDatabaseType = Sonar.get().getConfig().getDatabase().getType();
cachedDatabaseType = database.getType();
// Don't establish a database connection if the type is NONE
if (cachedDatabaseType == SonarConfiguration.Database.Type.NONE) {
@@ -80,8 +82,14 @@ public final class VerifiedPlayerController {
return;
}
try (final ConnectionSource connectionSource = setupDriverAndConnect()) {
this.connectionSource = connectionSource;
// Initialize MySQL/MariaDB driver
database.getType().getDatabaseType().loadDriver();
try {
connectionSource = new JdbcPooledConnectionSource(
String.format(database.getType().getConnectionString(),
database.getHost(), database.getPort(), database.getName()),
database.getUsername(), database.getPassword(), database.getType().getDatabaseType());
// Create table
try {
@@ -98,83 +106,53 @@ public final class VerifiedPlayerController {
// Make sure to run the clean task and the caching task in the same thread
// https://github.com/jonesdevelopment/sonar/issues/150
DB_UPDATE_SERVICE.execute(() -> {
// Make sure to clear all outdated entries first
clearOld(Sonar.get().getConfig().getDatabase().getMaximumAge());
// Add all entries from the database to the cache
try {
// Make sure to clear all outdated entries first
clearOld(database.getMaximumAge());
// Add all entries from the database to the cache
dao.queryForAll().forEach(this::_add);
} catch (SQLException exception) {
LOGGER.error("Could not cache entries: {}", exception);
LOGGER.error("Error initializing database: {}", exception);
}
});
} catch (Throwable throwable) {
LOGGER.error("Error setting up database: {}", throwable);
throwable.printStackTrace(System.err);
} catch (SQLException exception) {
LOGGER.error("Error setting up database: {}", exception);
}
}
/**
* Automatically sets up the database connection using the configured credentials
* Closes the connection if instantiated
*/
private static @NotNull ConnectionSource setupDriverAndConnect() throws Throwable {
final SonarConfiguration.Database database = Sonar.get().getConfig().getDatabase();
// Prepare the JDBC connection string
// TODO: automatically build best connection string for database type
final String jdbcURL = String.format("jdbc:%s://%s:%d/%s",
database.getType().name().toLowerCase(), database.getHost(), database.getPort(), database.getName());
// Use reflection to uncover the driver class
final Class<?> driverClass = Class.forName(database.getType().getDriverClassName());
// Make sure we send the username and password
final Properties credentials = new Properties();
credentials.put("user", database.getUsername());
credentials.put("password", database.getPassword());
// Return new JDBC connection instance for our controller to handle
return new JdbcSingleConnectionSource(jdbcURL, invoke(jdbcURL, driverClass, credentials));
}
/**
* Uses reflection to load the database driver from the injected library
*
* @param connectionString URL of the database
* @param driverClass Class of the database driver
* @param credentials Credentials for the database
*/
private static Connection invoke(final @NotNull String connectionString,
final @NotNull Class<?> driverClass,
final @NotNull Properties credentials) throws Throwable {
final Object driver = driverClass.getDeclaredConstructor().newInstance();
DriverManager.registerDriver((Driver) driver);
final Method connect = driverClass.getDeclaredMethod("connect", String.class, Properties.class);
connect.setAccessible(true);
return (Connection) connect.invoke(driver, connectionString, credentials);
public void close() {
// The connection source will always be null if the database type is NONE.
if (connectionSource != null) {
try {
// Properly close the connection
connectionSource.close();
} catch (Exception exception) {
LOGGER.error("Error closing database: {}", exception);
}
}
}
/**
* Clear all old entries using the given timestamp.
*/
private void clearOld(final @Range(from = 1, to = 365) int maximumAge) {
try {
final long timestamp = Instant.now()
.minus(maximumAge, ChronoUnit.DAYS)
.getEpochSecond() * 1000L; // convert to ms
private void clearOld(final @Range(from = 1, to = 365) int maximumAge) throws SQLException {
final long timestamp = Instant.now()
.minus(maximumAge, ChronoUnit.DAYS)
.getEpochSecond() * 1000L; // convert to ms
final List<VerifiedPlayer> oldEntries = queryBuilder.where()
.lt("timestamp", new Timestamp(timestamp))
.query();
final List<VerifiedPlayer> oldEntries = queryBuilder.where()
.lt("timestamp", new Timestamp(timestamp))
.query();
if (oldEntries != null) {
for (final VerifiedPlayer player : oldEntries) {
dao.delete(player);
}
LOGGER.info("Removed {} database entries older than {} days.",
oldEntries.size(), maximumAge);
if (oldEntries != null) {
for (final VerifiedPlayer player : oldEntries) {
dao.delete(player);
}
} catch (SQLException exception) {
LOGGER.error("Error trying to clear old entries: {}", exception);
LOGGER.info("Removed {} database entries older than {} days.",
oldEntries.size(), maximumAge);
}
}
@@ -221,7 +199,7 @@ public final class VerifiedPlayerController {
* @param inetAddress IP address of the player
*/
private void _remove(final @NotNull String inetAddress) {
MAP.remove(inetAddress);
cache.remove(inetAddress);
}
/**
@@ -259,24 +237,30 @@ public final class VerifiedPlayerController {
* @param player VerifiedPlayer model
*/
private void _add(final @NotNull VerifiedPlayer player) {
MAP.computeIfAbsent(player.getInetAddress(), v -> new Vector<>())
cache.computeIfAbsent(player.getInetAddress(), v -> new ArrayList<>())
.add(player.getPlayerUUID());
}
/**
* Returns the amount of verified IP addresses
*
* @return Estimated size of the local cache
*/
public synchronized int estimatedSize() {
return MAP.values().stream()
.mapToInt(Collection::size)
.sum();
/*
* TODO: Work on this
* return MAP.values().stream()
* .mapToInt(Collection::size)
* .sum();
*/
return cache.size();
}
/**
* @return List of UUIDs associated with one IP address
*/
public Collection<UUID> getUUIDs(final @NotNull String inetAddress) {
return MAP.getOrDefault(inetAddress, Collections.emptyList());
public @Unmodifiable Collection<UUID> getUUIDs(final @NotNull String inetAddress) {
return cache.getOrDefault(inetAddress, Collections.emptyList());
}
/**
@@ -285,7 +269,7 @@ public final class VerifiedPlayerController {
*/
public void clearAll() {
try {
MAP.clear();
cache.clear();
// Only update the column if the database type is not NONE
if (cachedDatabaseType != SonarConfiguration.Database.Type.NONE) {
@@ -302,7 +286,7 @@ public final class VerifiedPlayerController {
* @return Whether the local cache contains the IP and UUID
*/
public boolean has(final @NotNull String inetAddress, final @NotNull UUID uuid) {
final Collection<UUID> got = MAP.get(inetAddress);
final Collection<UUID> got = cache.get(inetAddress);
if (got != null) {
return got.contains(uuid);
}
@@ -324,15 +308,6 @@ public final class VerifiedPlayerController {
* @return Whether the local cache contains the IP
*/
public boolean has(final @NotNull String inetAddress) {
return MAP.containsKey(inetAddress);
}
/**
* @param inetAddress IP address
* @return Whether the local cache contains the IP
* @see #has(String)
*/
public boolean has(final @NotNull InetAddress inetAddress) {
return has(inetAddress.toString());
return cache.containsKey(inetAddress);
}
}
@@ -19,23 +19,17 @@ package xyz.jonesdev.sonar.api.event.impl;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.event.SonarEvent;
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
import java.util.Optional;
@Getter
@ToString
@EqualsAndHashCode
@RequiredArgsConstructor
public final class UserVerifyFailedEvent implements SonarEvent {
private final FallbackUser user;
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
private final Optional<String> reason;
public UserVerifyFailedEvent(final FallbackUser user, final @Nullable String reason) {
this.user = user;
this.reason = Optional.ofNullable(reason);
}
private final @NotNull FallbackUser user;
private final @NotNull String reason;
}
@@ -69,16 +69,20 @@ public final class Fallback {
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
public boolean shouldVerifyNewPlayers() {
return performTimingCheck(Sonar.get().getConfig().getVerification().getTiming());
return shouldPerform(Sonar.get().getConfig().getVerification().getTiming());
}
public boolean shouldDoMapCaptcha() {
return performTimingCheck(Sonar.get().getConfig().getVerification().getMap().getTiming());
public boolean shouldPerformCaptcha() {
return shouldPerform(Sonar.get().getConfig().getVerification().getMap().getTiming());
}
private boolean performTimingCheck(final SonarConfiguration.Verification.Timing timing) {
public boolean shouldPerformVehicleCheck() {
return shouldPerform(Sonar.get().getConfig().getVerification().getVehicle().getTiming());
}
private static boolean shouldPerform(final SonarConfiguration.Verification.Timing timing) {
return timing == SonarConfiguration.Verification.Timing.ALWAYS
|| (timing == SonarConfiguration.Verification.Timing.DURING_ATTACK
&& Sonar.get().getAttackTracker().isCurrentlyUnderAttack());
&& Sonar.get().getAttackTracker().getCurrentAttack() != null);
}
}
@@ -26,15 +26,20 @@ import java.net.InetAddress;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ForkJoinPool;
@Getter
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
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);
// Runnable task executed for polling the queue
private final Runnable pollTask = () -> {
public void poll() {
final int maxQueuePolls = Sonar.get().getConfig().getQueue().getMaxQueuePolls();
int index = 0;
@@ -47,30 +52,9 @@ public final class FallbackQueue {
}
// Run the cached runnable
final Map.Entry<InetAddress, Runnable> entry = iterator.next();
entry.getValue().run();
QUEUE_EXECUTOR.execute(entry.getValue());
// Remove runnable from iterator
iterator.remove();
}
// Run the attack check task
Sonar.get().getAttackTracker().checkIfUnderAttack();
// Clean up the cache of rate-limited IPs
Sonar.get().getFallback().getRatelimiter().cleanUpCache();
};
/**
* @param inetAddress IP address of the player
* @param runnable Queued action on the netty thread
* @see #remove
*/
public void queue(final InetAddress inetAddress, final Runnable runnable) {
queuedPlayers.put(inetAddress, runnable);
}
/**
* @param inetAddress IP address of the player
*/
public void remove(final InetAddress inetAddress) {
queuedPlayers.remove(inetAddress);
}
}
@@ -19,6 +19,7 @@ package xyz.jonesdev.sonar.api.fallback;
import com.github.benmanes.caffeine.cache.Cache;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.jetbrains.annotations.NotNull;
@@ -29,7 +30,10 @@ import java.net.InetAddress;
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class FallbackRatelimiter {
static final FallbackRatelimiter INSTANCE = new FallbackRatelimiter();
private Cache<InetAddress, Byte> expiringCache;
private Cache<InetAddress, Byte> attemptCache;
@Getter
private Cache<InetAddress, Integer> failCountCache;
/**
* We don't want to clean up the cache in the
@@ -37,7 +41,8 @@ public final class FallbackRatelimiter {
* it would take up too many resources.
*/
public void cleanUpCache() {
expiringCache.cleanUp();
attemptCache.cleanUp();
failCountCache.cleanUp();
}
/**
@@ -49,12 +54,27 @@ public final class FallbackRatelimiter {
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
public boolean attempt(final @NotNull InetAddress inetAddress) {
// Cache the IP address if it's not already cached
if (!expiringCache.asMap().containsKey(inetAddress)) {
if (!attemptCache.asMap().containsKey(inetAddress)) {
// Cache the IP address
expiringCache.put(inetAddress, (byte) 0);
attemptCache.put(inetAddress, (byte) 0);
return true;
}
// Deny the connection attempt
return false;
}
/**
* Checks if the player is failing verifications too often
* Increments the number of times this user has failed the verification
*
* @param inetAddress IP address of the player
* @param count Count of previously failed verifications
*/
public void incrementFails(final @NotNull InetAddress inetAddress, final int count) {
// Make sure to remove the old values from the cache
if (count > 0) {
failCountCache.invalidate(inetAddress);
}
failCountCache.put(inetAddress, count + 1);
}
}
@@ -17,37 +17,40 @@
package xyz.jonesdev.sonar.api.fallback;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import io.netty.channel.Channel;
import io.netty.util.ReferenceCountUtil;
import net.kyori.adventure.text.Component;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.event.impl.UserBlacklistedEvent;
import xyz.jonesdev.sonar.api.event.impl.UserVerifyFailedEvent;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.api.statistics.Statistics;
import java.net.InetAddress;
import java.time.Duration;
import java.util.Objects;
import java.util.UUID;
public interface FallbackUser {
@NotNull Channel getChannel();
@NotNull
Channel getChannel();
@NotNull InetAddress getInetAddress();
@NotNull
InetAddress getInetAddress();
@NotNull ProtocolVersion getProtocolVersion();
@NotNull
ProtocolVersion getProtocolVersion();
@NotNull
FallbackUserState getState();
boolean isGeyser();
void setState(final FallbackUserState state);
/**
* Kicks the player from the server with
* the given disconnect message.
* Disconnect the player during/after verification
* using our custom Disconnect packet.
*
* @param reason Disconnect message component
*/
@ApiStatus.Internal
void disconnect(final @NotNull Component reason);
/**
@@ -58,11 +61,11 @@ public interface FallbackUser {
* @param encoder Name of the encoder pipeline
* @param decoder Name of the decoder pipeline
* @param timeout Name of the read timeout pipeline
* @param boss Name of the main pipeline
* @param handler Name of the main pipeline
*/
void hijack(final @NotNull String username, final @NotNull UUID uuid,
final @NotNull String encoder, final @NotNull String decoder,
final @NotNull String timeout, final @NotNull String boss);
final @NotNull String timeout, final @NotNull String handler);
/**
* Sends a packet/message to the player
@@ -89,40 +92,6 @@ public interface FallbackUser {
}
}
Cache<InetAddress, Integer> GLOBAL_FAIL_COUNT = Caffeine.newBuilder()
.expireAfterWrite(Duration.ofMinutes(5)) // expire after 5 minutes
.build();
/**
* Increments the number of times this user (by IP address) has failed the verification
*/
default void incrementFails() {
final InetAddress inetAddress = Objects.requireNonNull(getInetAddress());
final int failCount = getFailedCount();
// Make sure to remove the old values from the cache
if (failCount > 0) {
GLOBAL_FAIL_COUNT.invalidate(inetAddress);
}
GLOBAL_FAIL_COUNT.put(inetAddress, failCount + 1);
}
/**
* @return The number of times this user (by IP address) has failed the verification
* @apiNote Returns 0 if no key/value is present in the cache
*/
default int getFailedCount() {
// Make sure to clean up the cache before we try to get cached values
GLOBAL_FAIL_COUNT.cleanUp();
return GLOBAL_FAIL_COUNT.asMap().getOrDefault(Objects.requireNonNull(getInetAddress()), 0);
}
/**
* Removes all previously failed verifications
*/
default void invalidateFails() {
GLOBAL_FAIL_COUNT.invalidate(Objects.requireNonNull(getInetAddress()));
}
/**
* Disconnects the player who failed the verification
* and caches them in FAILED_VERIFICATIONS.
@@ -130,55 +99,8 @@ public interface FallbackUser {
* the player will be temporarily denied from verifying.
*
* @param reason Reason for failing the verification
* @apiNote The {@link xyz.jonesdev.sonar.api.event.impl.UserVerifyFailedEvent}
* will not be thrown if no reason is given
*/
default void fail(final @Nullable String reason) {
if (getChannel().isActive()) {
disconnect(Sonar.get().getConfig().getVerification().getVerificationFailed());
if (reason != null) {
// Only log the failed message if the server isn't under attack.
// We let the user override this through the configuration.
if (!Sonar.get().getAttackTracker().isCurrentlyUnderAttack()
|| Sonar.get().getConfig().getVerification().isLogDuringAttack()) {
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));
}
}
}
Statistics.FAILED_VERIFICATIONS.increment();
// Call the VerifyFailedEvent for external API usage
Sonar.get().getEventManager().publish(new UserVerifyFailedEvent(this, reason));
// Use a label, so we can easily add more code beneath this method
blacklist: {
// Check if the player has too many failed attempts
final int blacklistThreshold = Sonar.get().getConfig().getVerification().getBlacklistThreshold();
// The user is allowed to disable the blacklist entirely by setting the threshold to 0
if (blacklistThreshold <= 0) break blacklist;
// Add 1 to the amount of fails since we haven't updated the cached entries yet (for performance)
final int failCount = getFailedCount() + 1;
// Now we simply need to check if the threshold is reached
if (failCount >= blacklistThreshold) {
// Call the BotBlacklistedEvent for external API usage
Sonar.get().getEventManager().publish(new UserBlacklistedEvent(this));
Sonar.get().getFallback().getBlacklist().put(getInetAddress(), (byte) 0);
Sonar.get().getFallback().getLogger().info(Sonar.get().getConfig().getVerification().getBlacklistLog()
.replace("%ip%", Sonar.get().getConfig().formatAddress(getInetAddress()))
.replace("%protocol%", String.valueOf(getProtocolVersion().getProtocol())));
// Invalidate the cached entry to ensure memory safety
invalidateFails();
break blacklist;
}
// Make sure we increment the number of fails
incrementFails();
}
}
void fail(final @NotNull String reason);
}
@@ -15,16 +15,27 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.protocol.block;
package xyz.jonesdev.sonar.api.fallback;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
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;
@Getter
@ToString
@RequiredArgsConstructor
public final class BlockPosition {
private final int x, y, z;
private final int chunkX, chunkZ;
public boolean canReceivePackets() {
return this != FAILED && this != SUCCESS;
}
}
@@ -20,13 +20,15 @@ package xyz.jonesdev.sonar.api.fallback.protocol;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.util.*;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
@Getter
@RequiredArgsConstructor
public enum ProtocolVersion {
UNKNOWN(-1),
LEGACY(-2),
MINECRAFT_1_7_2(4),
MINECRAFT_1_7_6(5),
MINECRAFT_1_8(47),
@@ -66,14 +68,13 @@ public enum ProtocolVersion {
MINECRAFT_1_19_4(762),
MINECRAFT_1_20(763),
MINECRAFT_1_20_2(764),
MINECRAFT_1_20_3(765);
MINECRAFT_1_20_3(765),
MINECRAFT_1_20_5(766);
private final int protocol;
public static final ProtocolVersion MINIMUM_VERSION = MINECRAFT_1_7_2;
public static final ProtocolVersion LATEST_VERSION;
public static final Map<Integer, ProtocolVersion> ID_TO_PROTOCOL_CONSTANT;
public static final Set<ProtocolVersion> SUPPORTED_VERSIONS;
static {
{
@@ -90,12 +91,11 @@ public enum ProtocolVersion {
final Set<ProtocolVersion> versions = EnumSet.noneOf(ProtocolVersion.class);
for (final ProtocolVersion value : values()) {
if (!value.isUnknown() && !value.isLegacy()) {
if (!value.isUnknown()) {
versions.add(value);
}
}
SUPPORTED_VERSIONS = Collections.unmodifiableSet(versions);
LATEST_VERSION = (ProtocolVersion) versions.toArray()[versions.size() - 1];
}
}
@@ -111,9 +111,4 @@ public enum ProtocolVersion {
public boolean isUnknown() {
return this == UNKNOWN;
}
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
public boolean isLegacy() {
return this == LEGACY;
}
}
@@ -0,0 +1,115 @@
/*
* 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.jvm;
import com.sun.management.OperatingSystemMXBean;
import lombok.experimental.UtilityClass;
import java.lang.management.ManagementFactory;
import java.text.DecimalFormat;
@UtilityClass
public class JVMProcessInformation {
private final Runtime RUNTIME = Runtime.getRuntime();
private final OperatingSystemMXBean MX = (OperatingSystemMXBean) ManagementFactory.getOperatingSystemMXBean();
private final char[] MEMORY_UNITS = {'K', 'M', 'G', 'T', 'P', 'E'};
private final DecimalFormat DECIMAL_FORMAT = new DecimalFormat("#.#");
public String formatMemory(final long size) {
if (size < 1024L) {
return size + " B";
}
// https://stackoverflow.com/questions/3758606/how-can-i-convert-byte-size-into-a-human-readable-format-in-java
final int group = (63 - Long.numberOfLeadingZeros(size)) / 10;
final double formattedSize = (double) size / (1L << (group * 10));
final char unit = MEMORY_UNITS[group - 1];
// https://en.wikipedia.org/wiki/Byte#Multiple-byte_units
return DECIMAL_FORMAT.format(formattedSize) + " " + unit + "iB";
}
/**
* @return Number of available processors for the JVM
*/
public int getVirtualCores() {
return RUNTIME.availableProcessors();
}
/**
* @return Current process CPU usage in percent
*/
public double getProcessCPUUsage() {
return MX.getProcessCpuLoad() * 100;
}
/**
* @return Current per-core process CPU usage in percent
*/
public double getAverageProcessCPUUsage() {
return getProcessCPUUsage() / getVirtualCores();
}
/**
* @return Current system CPU usage in percent
*/
public double getSystemCPUUsage() {
return MX.getSystemCpuLoad() * 100;
}
/**
* @return Current per-core system CPU usage in percent
*/
public double getAverageSystemCPUUsage() {
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
*/
public long getMaxMemory() {
return RUNTIME.maxMemory();
}
/**
* @return Memory (in bytes) available to the host machine
*/
public long getTotalMemory() {
return RUNTIME.totalMemory();
}
/**
* @return Memory (in bytes) the JVM can allocate
*/
public long getFreeMemory() {
return RUNTIME.freeMemory();
}
/**
* @return Memory (in bytes) the JVM is using
*/
public long getUsedMemory() {
return getTotalMemory() - getFreeMemory();
}
}
@@ -15,18 +15,20 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.velocity.command;
package xyz.jonesdev.sonar.api.ormlite;
import com.velocitypowered.api.command.CommandSource;
import com.velocitypowered.api.proxy.Player;
import com.j256.ormlite.jdbc.db.MariaDbDatabaseType;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.command.InvocationSource;
public final class VelocityInvocationSource extends InvocationSource {
public VelocityInvocationSource(final @NotNull CommandSource sender) {
super(sender instanceof Player ? ((Player) sender).getUsername() : "Console",
sender,
sender instanceof Player,
sender::hasPermission);
public final class MariaDbDatabaseTypeAdapter extends MariaDbDatabaseType {
/**
* We need to override the default driver class name
* in order to use the custom relocated MariaDB driver
* Read more: {@link MysqlDatabaseTypeAdapter}
*/
@Override
protected String @NotNull [] getDriverClassNames() {
return new String[]{"xyz.jonesdev.sonar.libs.mariadb.jdbc.Driver"};
}
}
@@ -15,19 +15,20 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.bukkit.command;
package xyz.jonesdev.sonar.api.ormlite;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import com.j256.ormlite.jdbc.db.MysqlDatabaseType;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.command.InvocationSource;
import xyz.jonesdev.sonar.bukkit.SonarBukkit;
public final class BukkitInvocationSource extends InvocationSource {
public BukkitInvocationSource(final @NotNull CommandSender sender) {
super(sender.getName(),
SonarBukkit.INSTANCE.getBukkitAudiences().sender(sender),
sender instanceof Player,
sender::hasPermission);
public final class MysqlDatabaseTypeAdapter extends MysqlDatabaseType {
/**
* We need to override the default driver class name
* in order to use the custom relocated MySQL driver
* Read more: {@link MariaDbDatabaseTypeAdapter}
*/
@Override
protected String @NotNull [] getDriverClassNames() {
return new String[]{"xyz.jonesdev.sonar.libs.mysql.cj.jdbc.Driver"};
}
}
@@ -1,80 +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.profiler;
import com.sun.management.OperatingSystemMXBean;
import java.lang.management.ManagementFactory;
import java.lang.management.RuntimeMXBean;
public interface JVMProfiler {
Runtime RUNTIME = Runtime.getRuntime();
OperatingSystemMXBean OPERATING_SYSTEM_MX_BEAN = (OperatingSystemMXBean) ManagementFactory.getOperatingSystemMXBean();
RuntimeMXBean RUNTIME_MX_BEAN = ManagementFactory.getRuntimeMXBean();
char[] MEMORY_UNITS = {'k', 'M', 'G', 'T', 'P', 'E'};
default String formatMemory(final long size) {
if (size < 1024L) {
return size + " B";
}
final int group = (63 - Long.numberOfLeadingZeros(size)) / 10;
final double formattedSize = (double) size / (1L << (group * 10));
final char unit = MEMORY_UNITS[group - 1];
return String.format("%.1f %sB", formattedSize, unit);
}
default int getVirtualCores() {
return RUNTIME.availableProcessors();
}
default double getProcessCPUUsage() {
return OPERATING_SYSTEM_MX_BEAN.getProcessCpuLoad() * 100;
}
default double getAverageProcessCPUUsage() {
return getProcessCPUUsage() / getVirtualCores();
}
default double getSystemCPUUsage() {
return OPERATING_SYSTEM_MX_BEAN.getSystemCpuLoad() * 100;
}
default double getAverageSystemCPUUsage() {
return getSystemCPUUsage() / getVirtualCores();
}
default double getSystemLoadAverage() {
return OPERATING_SYSTEM_MX_BEAN.getSystemLoadAverage() * 100;
}
default long getMaxMemory() {
return RUNTIME.maxMemory();
}
default long getTotalMemory() {
return RUNTIME.totalMemory();
}
default long getFreeMemory() {
return RUNTIME.freeMemory();
}
default long getUsedMemory() {
return getTotalMemory() - getFreeMemory();
}
}
@@ -1,45 +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.statistics;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.jetbrains.annotations.ApiStatus;
import java.time.Duration;
@SuppressWarnings("unused")
public interface Counters {
@ApiStatus.Internal
Cache<Long, Byte> LOGINS_PER_SECOND = Caffeine.newBuilder()
.expireAfterWrite(Duration.ofSeconds(1))
.build();
@ApiStatus.Internal
Cache<Long, Byte> CONNECTIONS_PER_SECOND = Caffeine.newBuilder()
.expireAfterWrite(Duration.ofSeconds(1))
.build();
static long getConnectionsPerSecond() {
return CONNECTIONS_PER_SECOND.estimatedSize();
}
static long getLoginsPerSecond() {
return LOGINS_PER_SECOND.estimatedSize();
}
}
@@ -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.api.statistics;
import org.jetbrains.annotations.ApiStatus;
public interface SonarStatistics {
/**
* @return Number of connection establishments per second
*/
long getConnectionsPerSecond();
/**
* @return Number of logins per second
*/
long getLoginsPerSecond();
/**
* @return Currently incoming bytes per second
*/
long getCurrentIncomingBandwidth();
/**
* @return Currently outgoing bytes per second
*/
long getCurrentOutgoingBandwidth();
/**
* @return Total incoming bytes per second
*/
long getTotalIncomingBandwidth();
/**
* @return Total outgoing bytes per second
*/
long getTotalOutgoingBandwidth();
/**
* @return Same as {@link SonarStatistics#getCurrentIncomingBandwidth} but formatted
*/
String getCurrentIncomingBandwidthFormatted();
/**
* @return Same as {@link SonarStatistics#getCurrentOutgoingBandwidth} but formatted
*/
String getCurrentOutgoingBandwidthFormatted();
/**
* @return Same as {@link SonarStatistics#getTotalIncomingBandwidth} but formatted
*/
String getTotalIncomingBandwidthFormatted();
/**
* @return Same as {@link SonarStatistics#getTotalOutgoingBandwidth} but formatted
*/
String getTotalOutgoingBandwidthFormatted();
/**
* @return Total number of players who logged into the server
*/
int getTotalPlayersJoined();
/**
* @return Total number of players who tried to verify
*/
int getTotalPlayersVerified();
/**
* @return Total number of players who passed the verification
*/
int getTotalSuccessfulVerifications();
/**
* @return Total number of players who failed the verification
*/
int getTotalFailedVerifications();
/**
* @return Total number of players who passed the verification
*/
long getCurrentAttemptedVerifications();
/**
* @return Total number of players who tried verifying
*/
int getTotalAttemptedVerifications();
/**
* @return Current number of IP addresses that are blacklisted
*/
long getCurrentBlacklistSize();
/**
* @return Total number of cases where an IP was blacklisted
* @apiNote This does not describe the current amount of blacklisted IPs
*/
long getTotalBlacklistSize();
@ApiStatus.Internal
void cleanUpCache();
}
@@ -1,68 +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.statistics;
public enum Statistics {
TOTAL_TRAFFIC,
REAL_TRAFFIC,
FAILED_VERIFICATIONS;
private int val = -1;
/**
* Increments the current value by 1 with the default value of 0
*
* @see #get(int)
* @see #set(int)
*/
public void increment() {
increment(0);
}
/**
* Increments the current value by 1 with a custom default value
*
* @see #get(int)
* @see #set(int)
*/
public void increment(final int def) {
set(get(def) + 1);
}
/**
* @return The current value with the default value of 0
* @see #get(int)
*/
public int get() {
return get(0);
}
/**
* @return The current value with a custom default value
*/
public int get(final int def) {
return val == -1 ? def : val;
}
/**
* Set current value to a new value
*/
public void set(final int value) {
val = value;
}
}
@@ -18,13 +18,12 @@
package xyz.jonesdev.sonar.api.timer;
import lombok.Getter;
import org.jetbrains.annotations.NotNull;
import java.text.SimpleDateFormat;
@Getter
public final class SystemTimer {
private static final SimpleDateFormat FORMATTER = new SimpleDateFormat("mm:ss");
public static final SimpleDateFormat DATE_FORMATTER = new SimpleDateFormat("mm:ss");
private long start = System.currentTimeMillis();
@@ -32,18 +31,20 @@ public final class SystemTimer {
start = System.currentTimeMillis();
}
/**
* @return Time between the current timestamp and the start timestamp
*/
public long delay() {
return System.currentTimeMillis() - start;
}
/**
* @apiNote Read more: {@link SystemTimer#delay}
*/
public boolean elapsed(final long amount) {
return delay() >= amount;
}
public @NotNull String formattedDelay() {
return FORMATTER.format(delay());
}
@Override
public String toString() {
return String.format("%.3f", delay() / 1000D);
@@ -20,60 +20,33 @@ package xyz.jonesdev.sonar.api.verbose;
import lombok.Getter;
import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.title.Title;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.statistics.Counters;
import xyz.jonesdev.sonar.api.statistics.Statistics;
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.statistics.Bandwidth.INCOMING;
import static xyz.jonesdev.sonar.api.statistics.Bandwidth.OUTGOING;
@Getter
public final class Notification implements Observable, Counters {
private final @NotNull Collection<String> subscribers = new Vector<>(0);
public final class Notification implements Observable {
private final @NotNull Collection<UUID> subscribers = new Vector<>(0);
public void sendAttackNotification() {
// No need to do anything if we have no subscribers
if (subscribers.isEmpty()) {
return;
}
// Prepare the title
// TODO: Why do custom title times not work?
// Title.Times.times(Ticks.duration(5L), Ticks.duration(60L), Ticks.duration(10L));
final Title title = Title.title(
Sonar.get().getConfig().getNotifications().getNotificationTitle(),
Sonar.get().getConfig().getNotifications().getNotificationSubtitle());
// Prepare the chat message
final Component chat = MiniMessage.miniMessage().deserialize(
Sonar.get().getConfig().getNotifications().getNotificationChat()
.replace("%queued%",
DECIMAL_FORMAT.format(Sonar.get().getFallback().getQueue().getQueuedPlayers().size()))
.replace("%verifying%", DECIMAL_FORMAT.format(Sonar.get().getFallback().getConnected().size()))
.replace("%blacklisted%",
DECIMAL_FORMAT.format(Sonar.get().getFallback().getBlacklist().estimatedSize()))
.replace("%total-joins%", DECIMAL_FORMAT.format(Statistics.TOTAL_TRAFFIC.get()))
.replace("%logins-per-second%", DECIMAL_FORMAT.format(LOGINS_PER_SECOND.estimatedSize()))
.replace("%connections-per-second%", DECIMAL_FORMAT.format(CONNECTIONS_PER_SECOND.estimatedSize()))
.replace("%verify-total%", DECIMAL_FORMAT.format(Statistics.REAL_TRAFFIC.get()))
.replace("%verify-success%",
DECIMAL_FORMAT.format(Sonar.get().getVerifiedPlayerController().estimatedSize()))
.replace("%verify-failed%", DECIMAL_FORMAT.format(Statistics.FAILED_VERIFICATIONS.get()))
.replace("%incoming-traffic%", INCOMING.getCachedSecond())
.replace("%outgoing-traffic%", OUTGOING.getCachedSecond())
.replace("%incoming-traffic-ttl%", INCOMING.getCachedTtl())
.replace("%outgoing-traffic-ttl%", OUTGOING.getCachedTtl()));
// Don't prepare component if there are no subscribers
Component chat = null;
// Send the title and chat messages to all online players
for (final String subscriber : Sonar.get().getNotificationHandler().getSubscribers()) {
final Audience audience = Sonar.AUDIENCES.get(subscriber);
for (final UUID subscriber : Sonar.get().getNotificationHandler().getSubscribers()) {
final Audience audience = Sonar.get().audience(subscriber);
if (audience == null) continue;
audience.showTitle(title);
// 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);
// Send the title to all online subscribers
audience.showTitle(Sonar.get().getConfig().getNotifications().getTitle());
}
}
}
@@ -17,32 +17,97 @@
package xyz.jonesdev.sonar.api.verbose;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TextReplacementConfig;
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 static xyz.jonesdev.sonar.api.Sonar.DECIMAL_FORMAT;
import static xyz.jonesdev.sonar.api.jvm.JVMProcessInformation.*;
public interface Observable {
Collection<String> getSubscribers();
Collection<UUID> getSubscribers();
/**
* @param name Name of the audience
* @param uuid UUID of the player
* @return Whether the audience is subscribed or not
*/
default boolean isSubscribed(final @NotNull String name) {
return getSubscribers().contains(name);
default boolean isSubscribed(final @NotNull UUID uuid) {
return getSubscribers().contains(uuid);
}
/**
* @param name Name of the audience to subscribe
* @param uuid UUID of the player
*/
default void subscribe(final String name) {
getSubscribers().add(name);
default void subscribe(final UUID uuid) {
getSubscribers().add(uuid);
}
/**
* @param name Name of the audience to unsubscribe
* @param uuid UUID of the player
*/
default void unsubscribe(final String name) {
getSubscribers().remove(name);
default 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());
}
}
@@ -21,77 +21,50 @@ import lombok.Getter;
import lombok.val;
import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.text.TextReplacementConfig;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.attack.AttackTracker;
import xyz.jonesdev.sonar.api.profiler.JVMProfiler;
import xyz.jonesdev.sonar.api.statistics.Counters;
import xyz.jonesdev.sonar.api.statistics.Statistics;
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.statistics.Bandwidth.INCOMING;
import static xyz.jonesdev.sonar.api.statistics.Bandwidth.OUTGOING;
import static xyz.jonesdev.sonar.api.timer.SystemTimer.DATE_FORMATTER;
@Getter
public final class Verbose implements Observable, JVMProfiler, Counters {
private final @NotNull Collection<String> subscribers = new Vector<>(0);
public final class Verbose implements Observable {
private final @NotNull Collection<UUID> subscribers = new Vector<>(0);
private int animationIndex;
// Run action bar verbose
public void update() {
// Clean up all blacklisted IPs
Sonar.get().getFallback().getBlacklist().cleanUp();
// Clean up all counters
LOGINS_PER_SECOND.cleanUp();
CONNECTIONS_PER_SECOND.cleanUp();
// Don't prepare component if there are no subscribers
if (subscribers.isEmpty()) return;
// Prepare the action bar format component
final Component component = prepareActionBarFormat();
// Send the action bar to all online players
for (final String subscriber : subscribers) {
final Audience audience = Sonar.AUDIENCES.get(subscriber);
Component component = null;
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);
}
}
public @NotNull Component prepareActionBarFormat() {
final AttackTracker.AttackStatistics attackStatistics = Sonar.get().getAttackTracker().getCurrentAttack();
final SystemTimer attackDuration = attackStatistics == null ? null : attackStatistics.getDuration();
return MiniMessage.miniMessage().deserialize((attackDuration != null
? Sonar.get().getConfig().getVerbose().getActionBarLayoutDuringAttack()
: Sonar.get().getConfig().getVerbose().getActionBarLayout())
.replace("%queued%",
DECIMAL_FORMAT.format(Sonar.get().getFallback().getQueue().getQueuedPlayers().size()))
.replace("%verifying%", DECIMAL_FORMAT.format(Sonar.get().getFallback().getConnected().size()))
.replace("%blacklisted%",
DECIMAL_FORMAT.format(Sonar.get().getFallback().getBlacklist().estimatedSize()))
.replace("%total-joins%", DECIMAL_FORMAT.format(Statistics.TOTAL_TRAFFIC.get()))
.replace("%logins-per-second%", DECIMAL_FORMAT.format(LOGINS_PER_SECOND.estimatedSize()))
.replace("%connections-per-second%", DECIMAL_FORMAT.format(CONNECTIONS_PER_SECOND.estimatedSize()))
.replace("%verify-total%", DECIMAL_FORMAT.format(Statistics.REAL_TRAFFIC.get()))
.replace("%verify-success%",
DECIMAL_FORMAT.format(Sonar.get().getVerifiedPlayerController().estimatedSize()))
.replace("%verify-failed%", DECIMAL_FORMAT.format(Statistics.FAILED_VERIFICATIONS.get()))
.replace("%attack-duration%", attackDuration == null ? "00:00" : attackDuration.formattedDelay())
.replace("%incoming-traffic%", INCOMING.getCachedSecond())
.replace("%outgoing-traffic%", OUTGOING.getCachedSecond())
.replace("%incoming-traffic-ttl%", INCOMING.getCachedTtl())
.replace("%outgoing-traffic-ttl%", OUTGOING.getCachedTtl())
.replace("%used-memory%", formatMemory(getUsedMemory()))
.replace("%free-memory%", formatMemory(getFreeMemory()))
.replace("%total-memory%", formatMemory(getTotalMemory()))
.replace("%max-memory%", formatMemory(getMaxMemory()))
.replace("%animation%", nextAnimation()));
}
public String nextAnimation() {
val animations = Sonar.get().getConfig().getVerbose().getAnimation();
final int nextIndex = ++animationIndex % animations.size();
@@ -17,6 +17,8 @@
package xyz.jonesdev.sonar.api.webhook;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.Sonar;
@@ -32,12 +34,18 @@ import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.function.Supplier;
import static java.net.HttpURLConnection.HTTP_NO_CONTENT;
@RequiredArgsConstructor
public final class DiscordWebhook {
private final String url;
private static final Gson GSON = new GsonBuilder().create();
private static final ExecutorService HTTP_REQUEST_SERVICE = Executors.newSingleThreadExecutor();
/**
* Asynchronously posts a Discord webhook via HTTPS
*/
public void post(final Supplier<SonarConfiguration.Webhook.Embed> embed) {
HTTP_REQUEST_SERVICE.execute(() -> {
try {
@@ -47,9 +55,12 @@ public final class DiscordWebhook {
try (final OutputStream outputStream = urlConnection.getOutputStream()) {
outputStream.write(serializedContent.getBytes(StandardCharsets.UTF_8));
}
// Check if the connection was a success
if (urlConnection.getResponseCode() != 204) {
throw new IllegalStateException("Unexpected response code " + urlConnection.getResponseCode());
// Warn the user if the connection might've not been successful
// I don't know why Discord doesn't respond with OK, but I guess this should be fine
final int code = urlConnection.getResponseCode();
if (code != HTTP_NO_CONTENT) {
Sonar.get().getLogger().warn("Unexpected Discord webhook response code {}", code);
}
} catch (Exception exception) {
Sonar.get().getLogger().error("Could not send webhook: {}", exception);
@@ -57,7 +68,7 @@ public final class DiscordWebhook {
});
}
private String prepareSerializedContent(final SonarConfiguration.Webhook.@NotNull Embed embed) {
private static String prepareSerializedContent(final SonarConfiguration.Webhook.@NotNull Embed embed) {
final String content = Sonar.get().getConfig().getWebhook().getContent();
final String username = Sonar.get().getConfig().getWebhook().getUsername();
final String avatarUrl = Sonar.get().getConfig().getWebhook().getAvatarUrl();
@@ -73,7 +84,7 @@ public final class DiscordWebhook {
final List<Webhook.EmbedMessage> embeds = Collections.singletonList(embedMessage);
final Webhook webhook = new Webhook(content, username, avatarUrl, false, embeds);
return Sonar.GENERAL_GSON.toJson(webhook);
return GSON.toJson(webhook);
}
private @NotNull HttpsURLConnection prepareConnection(final @NotNull URL url,
+20 -4
View File
@@ -13,7 +13,9 @@ bukkit {
authors = listOf("Jones Development", "Sonar Contributors")
website = "https://jonesdev.xyz/discord/"
load = BukkitPluginDescription.PluginLoadOrder.POSTWORLD
softDepend = listOf("Geyser-Spigot", "floodgate", "Protocolize", "packetevents", "ProtocolLib", "ViaVersion")
softDepend = listOf("Geyser-Spigot", "floodgate", "Protocolize", "ProtocolSupport",
"ViaVersion", "packetevents", "ProtocolLib", "FastLogin")
foliaSupported = true
commands {
register("sonar") {
@@ -28,11 +30,19 @@ repositories {
}
dependencies {
compileOnly(project(":api"))
compileOnly(project(":common"))
implementation(project(":api"))
implementation(project(":common"))
// MiniMessage platform support
// 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 minimessage
compileOnly("net.kyori:adventure-text-minimessage:4.16.0")
compileOnly("net.kyori:adventure-text-serializer-gson:4.16.0")
// adventure nbt
implementation("net.kyori:adventure-nbt:4.16.0")
// We have to use 1.8 for backwards compatibility
compileOnly("org.spigotmc:spigot-api:1.8.8-R0.1-SNAPSHOT")
@@ -44,5 +54,11 @@ dependencies {
implementation("com.alessiodp.libby:libby-bukkit:2.0.0-SNAPSHOT")
}
tasks {
shadowJar {
relocate("net.kyori", "xyz.jonesdev.sonar.libs.kyori")
}
}
java.sourceCompatibility = JavaVersion.VERSION_11
java.targetCompatibility = JavaVersion.VERSION_11
@@ -19,18 +19,21 @@ package xyz.jonesdev.sonar.bukkit;
import com.alessiodp.libby.BukkitLibraryManager;
import lombok.Getter;
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 xyz.jonesdev.sonar.api.Sonar;
import org.jetbrains.annotations.Nullable;
import xyz.jonesdev.sonar.api.SonarPlatform;
import xyz.jonesdev.sonar.api.logger.LoggerWrapper;
import xyz.jonesdev.sonar.api.statistics.Bandwidth;
import xyz.jonesdev.sonar.bukkit.audience.AudienceListener;
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;
@Getter
public final class SonarBukkit extends SonarBootstrap<SonarBukkitPlugin> {
@@ -38,7 +41,7 @@ public final class SonarBukkit extends SonarBootstrap<SonarBukkitPlugin> {
public SonarBukkit(final @NotNull SonarBukkitPlugin plugin) {
super(plugin, new BukkitLibraryManager(
plugin, plugin.getDataFolder().getName()),
plugin, plugin.getDataFolder().getName()),
plugin.getDataFolder(), SonarPlatform.BUKKIT);
INSTANCE = this;
}
@@ -48,6 +51,28 @@ public final class SonarBukkit extends SonarBootstrap<SonarBukkitPlugin> {
*/
private final BukkitAudiences bukkitAudiences = BukkitAudiences.create(getPlugin());
@Override
public @Nullable Audience audience(final @Nullable UUID uniqueId) {
if (uniqueId == null) {
return null;
}
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.
@@ -72,28 +97,26 @@ public final class SonarBukkit extends SonarBootstrap<SonarBukkitPlugin> {
}
};
private Metrics metrics;
@Override
public void enable() {
// Initialize bStats.org metrics
new Metrics(getPlugin(), getPlatform().getMetricsId());
metrics = new Metrics(getPlugin(), getPlatform().getMetricsId());
// Add charts for some configuration options
metrics.addCustomChart(new SimplePie("verification",
() -> getConfig().getVerification().getTiming().getDisplayName()));
metrics.addCustomChart(new SimplePie("captcha",
() -> getConfig().getVerification().getMap().getTiming().getDisplayName()));
// Register Sonar command
Objects.requireNonNull(getPlugin().getCommand("sonar")).setExecutor(new BukkitSonarCommand());
}
// Register audience register listener
getPlugin().getServer().getPluginManager().registerEvents(new AudienceListener(), getPlugin());
// Register traffic service
getPlugin().getServer().getScheduler().runTaskTimerAsynchronously(getPlugin(),
Bandwidth::reset, 20L, 20L);
// Register queue service
getPlugin().getServer().getScheduler().runTaskTimerAsynchronously(getPlugin(),
getFallback().getQueue().getPollTask(), 10L, 10L);
// Register verbose service
getPlugin().getServer().getScheduler().runTaskTimerAsynchronously(getPlugin(),
Sonar.get().getVerboseHandler()::update, 4L, 4L);
@Override
public void disable() {
// Make sure to properly shutdown bStats metrics
metrics.shutdown();
}
}
@@ -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.bukkit.audience;
import net.kyori.adventure.audience.Audience;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.bukkit.SonarBukkit;
public final class AudienceListener implements Listener {
@EventHandler(priority = EventPriority.HIGHEST)
public void handle(final @NotNull PlayerJoinEvent event) {
final Audience audience = SonarBukkit.INSTANCE.getBukkitAudiences().player(event.getPlayer());
Sonar.AUDIENCES.put(event.getPlayer().getName(), audience);
}
@EventHandler(priority = EventPriority.HIGHEST)
public void handle(final @NotNull PlayerQuitEvent event) {
Sonar.AUDIENCES.remove(event.getPlayer().getName());
}
}
@@ -21,9 +21,11 @@ import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabExecutor;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.command.InvocationSource;
import xyz.jonesdev.sonar.api.command.SonarCommand;
import xyz.jonesdev.sonar.bukkit.SonarBukkit;
import java.util.List;
@@ -37,7 +39,10 @@ public final class BukkitSonarCommand implements CommandExecutor, TabExecutor, S
final String label,
final String[] args) {
// Create our own invocation source wrapper to handle messages properly
final InvocationSource invocationSource = new BukkitInvocationSource(sender);
final InvocationSource invocationSource = new InvocationSource(
sender instanceof Player ? ((Player) sender).getUniqueId() : null,
SonarBukkit.INSTANCE.getBukkitAudiences().sender(sender),
sender::hasPermission);
// Pass the invocation source and command arguments to our command handler
handle(invocationSource, args);
return true; // Valid
+18 -4
View File
@@ -9,18 +9,26 @@ bungee {
version = rootProject.version.toString().split("-")[0]
main = "xyz.jonesdev.sonar.bungee.SonarBungeePlugin"
author = "Jones Development, Sonar Contributors"
softDepends = setOf("Geyser-BungeeCord", "floodgate", "Protocolize", "ViaVersion")
softDepends = setOf("Geyser-BungeeCord", "floodgate", "Protocolize", "ViaVersion", "packetevents", "FastLogin")
}
dependencies {
compileOnly(project(":api"))
compileOnly(project(":common"))
implementation(project(":api"))
implementation(project(":common"))
compileOnly("net.md_5:bungeecord-proxy:master-SNAPSHOT")
testCompileOnly("net.md_5:bungeecord-proxy:master-SNAPSHOT")
// MiniMessage platform support
// adventure platform support
implementation("net.kyori:adventure-platform-bungeecord:4.3.2")
implementation("net.kyori:adventure-platform-api:4.3.2")
implementation("net.kyori:adventure-platform-facet:4.3.2")
// adventure minimessage
compileOnly("net.kyori:adventure-text-minimessage:4.16.0")
compileOnly("net.kyori:adventure-text-serializer-gson:4.16.0")
// adventure nbt
implementation("net.kyori:adventure-nbt:4.16.0")
// Implement bStats.org for metrics
implementation("org.bstats:bstats-bungeecord:3.0.2")
@@ -29,5 +37,11 @@ dependencies {
implementation("com.alessiodp.libby:libby-bungee:2.0.0-SNAPSHOT")
}
tasks {
shadowJar {
relocate("net.kyori", "xyz.jonesdev.sonar.libs.kyori")
}
}
java.sourceCompatibility = JavaVersion.VERSION_11
java.targetCompatibility = JavaVersion.VERSION_11
@@ -19,20 +19,22 @@ package xyz.jonesdev.sonar.bungee;
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;
import xyz.jonesdev.sonar.api.Sonar;
import org.jetbrains.annotations.Nullable;
import xyz.jonesdev.sonar.api.SonarPlatform;
import xyz.jonesdev.sonar.api.logger.LoggerWrapper;
import xyz.jonesdev.sonar.api.statistics.Bandwidth;
import xyz.jonesdev.sonar.bungee.audience.AudienceListener;
import xyz.jonesdev.sonar.bungee.command.BungeeSonarCommand;
import xyz.jonesdev.sonar.bungee.fallback.FallbackInjectionHelper;
import xyz.jonesdev.sonar.bungee.fallback.FallbackLoginListener;
import xyz.jonesdev.sonar.common.boot.SonarBootstrap;
import java.util.concurrent.TimeUnit;
import java.net.InetAddress;
import java.util.Objects;
import java.util.UUID;
@Getter
public final class SonarBungee extends SonarBootstrap<SonarBungeePlugin> {
@@ -48,6 +50,27 @@ public final class SonarBungee extends SonarBootstrap<SonarBungeePlugin> {
*/
private final BungeeAudiences bungeeAudiences = BungeeAudiences.create(getPlugin());
@Override
public @Nullable Audience audience(final @Nullable UUID uniqueId) {
if (uniqueId == null) {
return null;
}
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.
@@ -72,34 +95,29 @@ public final class SonarBungee extends SonarBootstrap<SonarBungeePlugin> {
}
};
private Metrics metrics;
@Override
public void enable() {
// Initialize bStats.org metrics
new Metrics(getPlugin(), getPlatform().getMetricsId());
metrics = new Metrics(getPlugin(), getPlatform().getMetricsId());
// Add charts for some configuration options
metrics.addCustomChart(new SimplePie("verification",
() -> getConfig().getVerification().getTiming().getDisplayName()));
metrics.addCustomChart(new SimplePie("captcha",
() -> getConfig().getVerification().getMap().getTiming().getDisplayName()));
// Register Sonar command
getPlugin().getServer().getPluginManager().registerCommand(getPlugin(), new BungeeSonarCommand());
// Register Fallback listener
getPlugin().getServer().getPluginManager().registerListener(getPlugin(), new FallbackLoginListener());
// Register audience register listener
getPlugin().getServer().getPluginManager().registerListener(getPlugin(), new AudienceListener());
// Register traffic service
getPlugin().getServer().getScheduler().schedule(getPlugin(), Bandwidth::reset,
1L, 1L, TimeUnit.SECONDS);
// Register queue service
getPlugin().getServer().getScheduler().schedule(getPlugin(), getFallback().getQueue().getPollTask(),
500L, 500L, TimeUnit.MILLISECONDS);
// Register verbose service
getPlugin().getServer().getScheduler().schedule(getPlugin(), Sonar.get().getVerboseHandler()::update,
200L, 200L, TimeUnit.MILLISECONDS);
// Make sure to inject into the server's connection handler
FallbackInjectionHelper.inject();
}
@Override
public void disable() {
// Make sure to properly shutdown bStats metrics
metrics.shutdown();
}
}
@@ -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.bungee.audience;
import net.kyori.adventure.audience.Audience;
import net.md_5.bungee.api.event.PlayerDisconnectEvent;
import net.md_5.bungee.api.event.PostLoginEvent;
import net.md_5.bungee.api.plugin.Listener;
import net.md_5.bungee.event.EventHandler;
import net.md_5.bungee.event.EventPriority;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.bungee.SonarBungee;
public final class AudienceListener implements Listener {
@EventHandler(priority = EventPriority.HIGHEST)
public void handle(final @NotNull PostLoginEvent event) {
final Audience audience = SonarBungee.INSTANCE.getBungeeAudiences().player(event.getPlayer());
Sonar.AUDIENCES.put(event.getPlayer().getName(), audience);
}
@EventHandler(priority = EventPriority.HIGHEST)
public void handle(final @NotNull PlayerDisconnectEvent event) {
Sonar.AUDIENCES.remove(event.getPlayer().getName());
}
}
@@ -18,11 +18,13 @@
package xyz.jonesdev.sonar.bungee.command;
import net.md_5.bungee.api.CommandSender;
import net.md_5.bungee.api.connection.ProxiedPlayer;
import net.md_5.bungee.api.plugin.Command;
import net.md_5.bungee.api.plugin.TabExecutor;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.command.InvocationSource;
import xyz.jonesdev.sonar.api.command.SonarCommand;
import xyz.jonesdev.sonar.bungee.SonarBungee;
import static java.util.Collections.emptyList;
@@ -34,7 +36,10 @@ public final class BungeeSonarCommand extends Command implements TabExecutor, So
@Override
public void execute(final @NotNull CommandSender sender, final String[] args) {
// Create our own invocation source wrapper to handle messages properly
final InvocationSource invocationSource = new BungeeInvocationSource(sender);
final InvocationSource invocationSource = new InvocationSource(
sender instanceof ProxiedPlayer ? ((ProxiedPlayer) sender).getUniqueId() : null,
SonarBungee.INSTANCE.getBungeeAudiences().sender(sender),
sender::hasPermission);
// Pass the invocation source and command arguments to our command handler
handle(invocationSource, args);
}
@@ -17,56 +17,28 @@
package xyz.jonesdev.sonar.bungee.fallback;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.handler.codec.CorruptedFrameException;
import lombok.RequiredArgsConstructor;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.json.JSONComponentSerializer;
import net.md_5.bungee.api.chat.BaseComponent;
import net.md_5.bungee.chat.ComponentSerializer;
import net.md_5.bungee.netty.ChannelWrapper;
import net.md_5.bungee.netty.HandlerBoss;
import net.md_5.bungee.protocol.DefinedPacket;
import net.md_5.bungee.protocol.PacketWrapper;
import net.md_5.bungee.protocol.packet.Handshake;
import net.md_5.bungee.protocol.packet.Kick;
import net.md_5.bungee.protocol.packet.LoginRequest;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import xyz.jonesdev.sonar.api.ReflectiveOperationException;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.fallback.Fallback;
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.api.statistics.Counters;
import xyz.jonesdev.sonar.api.statistics.Statistics;
import xyz.jonesdev.sonar.common.fallback.FallbackBandwidthHandler;
import xyz.jonesdev.sonar.common.fallback.FallbackUserWrapper;
import xyz.jonesdev.sonar.common.utility.geyser.GeyserUtil;
import xyz.jonesdev.sonar.common.fallback.FallbackChannelHandlerAdapter;
import java.lang.reflect.Field;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.util.UUID;
import static net.md_5.bungee.netty.PipelineUtils.*;
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_BANDWIDTH;
import static xyz.jonesdev.sonar.common.fallback.FallbackUserWrapper.closeWith;
@RequiredArgsConstructor
public final class FallbackChannelHandler extends ChannelInboundHandlerAdapter {
private final Channel channel;
private @Nullable String username;
private ProtocolVersion protocolVersion;
private InetAddress inetAddress;
public final class FallbackChannelHandler extends FallbackChannelHandlerAdapter {
private static final Fallback FALLBACK = Sonar.get().getFallback();
private static final Cache<Component, Kick> CACHED_KICK_PACKETS = Caffeine.newBuilder().build();
public FallbackChannelHandler(final @NotNull Channel channel) {
super(channel);
}
private static final Field CHANNEL_WRAPPER_FIELD;
@@ -79,41 +51,6 @@ public final class FallbackChannelHandler extends ChannelInboundHandlerAdapter {
}
}
public static @NotNull Kick getCachedKickPacket(final Component component) {
Kick packet = CACHED_KICK_PACKETS.asMap().get(component);
if (packet == null) {
final String serialized = JSONComponentSerializer.json().serialize(component);
final BaseComponent baseComponent = ComponentSerializer.deserialize(serialized);
packet = new Kick(baseComponent);
CACHED_KICK_PACKETS.put(component, packet);
}
return packet;
}
@Override
public void channelActive(final @NotNull ChannelHandlerContext ctx) throws Exception {
// Increase connections per second for the action bar verbose
Counters.CONNECTIONS_PER_SECOND.put(System.nanoTime(), (byte) 0);
// Make sure to let the server handle the rest
ctx.fireChannelActive();
}
@Override
public 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
if (inetAddress != null) {
// Remove the IP address from the queue
FALLBACK.getQueue().remove(inetAddress);
}
// Make sure to let the server handle the rest
ctx.fireChannelInactive();
}
@Override
public void channelRead(final @NotNull ChannelHandlerContext ctx, final Object msg) throws Exception {
// Intercept any packets processed by BungeeCord
@@ -124,11 +61,18 @@ public final class FallbackChannelHandler extends ChannelInboundHandlerAdapter {
if (wrappedPacket != null) {
// Intercept any handshake packet by the client
if (wrappedPacket instanceof Handshake) {
handleHandshake((Handshake) wrappedPacket);
final Handshake handshake = (Handshake) wrappedPacket;
handleHandshake(handshake.getHost(), handshake.getProtocolVersion());
}
// Intercept any server login packet by the client
if (wrappedPacket instanceof LoginRequest) {
handleLogin(ctx, (LoginRequest) wrappedPacket, msg);
// 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 InetSocketAddress socketAddress = (InetSocketAddress) channelWrapper.getRemoteAddress();
handleLogin(ctx, msg, loginRequest.getData(), socketAddress,
PACKET_ENCODER, PACKET_DECODER, TIMEOUT_HANDLER, BOSS_HANDLER);
return;
}
}
@@ -136,134 +80,4 @@ public final class FallbackChannelHandler extends ChannelInboundHandlerAdapter {
// Make sure to let the server handle the rest
ctx.fireChannelRead(msg);
}
// We can override the default exceptionCaught method since this handler
// will run before the MinecraftConnection knows that there has been an error.
// Additionally, this will also run after our custom decoder.
@Override
public void exceptionCaught(final @NotNull ChannelHandlerContext ctx,
final @NotNull Throwable cause) throws Exception {
// Sonar sometimes uses exceptions to quickly close
// the channel and interrupt any other ongoing process.
// Simply close the channel if we encounter any errors.
ctx.close();
}
private void handleHandshake(final @NotNull Handshake handshake) throws Exception {
// Check if the player has already sent a handshake packet
if (protocolVersion != null) {
throw new CorruptedFrameException("Already sent handshake");
}
// Check if the hostname is invalid
if (handshake.getHost().isEmpty()) {
throw new CorruptedFrameException("Hostname is empty");
}
// Store the protocol version
protocolVersion = ProtocolVersion.fromId(handshake.getProtocolVersion());
// Hook the traffic listener
// TODO: Can we implement this in channelActive?
channel.pipeline().addFirst(FALLBACK_BANDWIDTH, FallbackBandwidthHandler.INSTANCE);
}
private void handleLogin(final @NotNull ChannelHandlerContext ctx,
final @NotNull LoginRequest loginRequest,
final @NotNull Object wrappedMessage) throws Exception {
// Check if the player has already sent a login packet
if (username != null) {
throw new CorruptedFrameException("Already logged on");
}
// Increase joins per second for the action bar verbose
Counters.LOGINS_PER_SECOND.put(System.nanoTime(), (byte) 0);
// Increase total traffic statistic
Statistics.TOTAL_TRAFFIC.increment();
// Store the username
username = loginRequest.getData();
// Make sure to use the potentially modified, real IP
final HandlerBoss handlerBoss = channel.pipeline().get(HandlerBoss.class);
final ChannelWrapper channelWrapper = (ChannelWrapper) CHANNEL_WRAPPER_FIELD.get(handlerBoss);
final InetSocketAddress socketAddress = (InetSocketAddress) channelWrapper.getRemoteAddress();
inetAddress = socketAddress.getAddress();
// Check the blacklist here since we cannot let the player "ghost join"
if (FALLBACK.getBlacklist().asMap().containsKey(inetAddress)) {
closeWith(channel, protocolVersion,
getCachedKickPacket(Sonar.get().getConfig().getVerification().getBlacklisted()));
return;
}
// Don't continue the verification process if the verification is disabled
if (!Sonar.get().getFallback().shouldVerifyNewPlayers()) {
ctx.fireChannelRead(wrappedMessage);
return;
}
// Completely skip Geyser connections
if (GeyserUtil.isGeyserConnection(channel, socketAddress)) {
ctx.fireChannelRead(wrappedMessage);
return;
}
// Check if the player is already queued since we don't want bots to flood the queue
if (FALLBACK.getQueue().getQueuedPlayers().containsKey(inetAddress)) {
closeWith(channel, protocolVersion,
getCachedKickPacket(Sonar.get().getConfig().getVerification().getAlreadyQueued()));
return;
}
// Check if Fallback is already verifying a player
// → is another player with the same IP address connected to Fallback?
if (FALLBACK.getConnected().containsKey(loginRequest.getData())
|| FALLBACK.getConnected().containsValue(inetAddress)) {
closeWith(channel, protocolVersion,
getCachedKickPacket(Sonar.get().getConfig().getVerification().getAlreadyVerifying()));
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())) {
closeWith(channel, protocolVersion,
getCachedKickPacket(Sonar.get().getConfig().getVerification().getProtocolBlacklisted()));
return;
}
// Make sure we actually have to verify the player
final String offlineUUIDString = "OfflinePlayer:" + loginRequest.getData();
final UUID offlineUUID = UUID.nameUUIDFromBytes(offlineUUIDString.getBytes(StandardCharsets.UTF_8));
if (Sonar.get().getVerifiedPlayerController().has(inetAddress, offlineUUID)) {
ctx.fireChannelRead(wrappedMessage);
return;
}
// Check if the IP address is currently being rate-limited
if (!FALLBACK.getRatelimiter().attempt(inetAddress)) {
closeWith(channel, protocolVersion,
getCachedKickPacket(Sonar.get().getConfig().getVerification().getTooFastReconnect()));
return;
}
// Check if the protocol ID of the player is allowed to bypass verification
if (Sonar.get().getConfig().getVerification().getWhitelistedProtocols()
.contains(protocolVersion.getProtocol())) {
ctx.fireChannelRead(wrappedMessage);
return;
}
// Queue the connection for further processing
FALLBACK.getQueue().queue(inetAddress, () -> channel.eventLoop().execute(() -> {
// Check if the username matches the valid name regex to prevent
// UTF-16 names or other types of exploits
if (!Sonar.get().getConfig().getVerification().getValidNameRegex()
.matcher(loginRequest.getData()).matches()) {
closeWith(channel, protocolVersion,
Sonar.get().getConfig().getVerification().getInvalidUsername());
return;
}
// Create an instance for the Fallback connection
final FallbackUser user = new FallbackUserWrapper(channel, inetAddress, protocolVersion);
// Let the verification handler take over the channel
user.hijack(loginRequest.getData(), offlineUUID, PACKET_ENCODER, PACKET_DECODER, TIMEOUT_HANDLER, BOSS_HANDLER);
}));
}
}
@@ -51,6 +51,8 @@ public final class FallbackChannelInitializer extends ChannelInitializer<Channel
throw new ReflectiveOperationException(throwable);
}
channel.pipeline().addAfter(PACKET_DECODER, FALLBACK_HANDLER, new FallbackChannelHandler(channel));
if (channel.pipeline().get(PACKET_DECODER) != null) {
channel.pipeline().addAfter(PACKET_DECODER, FALLBACK_HANDLER, new FallbackChannelHandler(channel));
}
}
}
@@ -1,80 +0,0 @@
/*
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.bungee.fallback;
import lombok.RequiredArgsConstructor;
import net.kyori.adventure.text.Component;
import net.md_5.bungee.api.connection.PendingConnection;
import net.md_5.bungee.api.event.LoginEvent;
import net.md_5.bungee.api.plugin.Listener;
import net.md_5.bungee.connection.InitialHandler;
import net.md_5.bungee.event.EventHandler;
import net.md_5.bungee.event.EventPriority;
import net.md_5.bungee.netty.ChannelWrapper;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.ReflectiveOperationException;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.bungee.SonarBungee;
import java.lang.reflect.Field;
import java.net.InetAddress;
import java.util.Objects;
import static xyz.jonesdev.sonar.bungee.fallback.FallbackChannelHandler.getCachedKickPacket;
import static xyz.jonesdev.sonar.common.fallback.FallbackUserWrapper.closeWith;
@RequiredArgsConstructor
public final class FallbackLoginListener implements Listener {
private static final Field CHANNEL_FIELD;
static {
try {
CHANNEL_FIELD = InitialHandler.class.getDeclaredField("ch");
CHANNEL_FIELD.setAccessible(true);
} catch (NoSuchFieldException exception) {
throw new ReflectiveOperationException(exception);
}
}
@SuppressWarnings("deprecation")
@EventHandler(priority = EventPriority.LOWEST)
public void handle(final @NotNull LoginEvent event) throws IllegalAccessException {
// Check if the number of online players using the same IP address as
// the connecting player is greater than the configured amount
final int maxOnlinePerIp = Sonar.get().getConfig().getMaxOnlinePerIp();
// Don't do anything if the setting is disabled
if (maxOnlinePerIp <= 0) return;
final InetAddress inetAddress = event.getConnection().getAddress().getAddress();
final long onlinePerIp = SonarBungee.INSTANCE.getPlugin().getServer().getPlayers().stream()
.filter(player -> Objects.equals(player.getAddress().getAddress(), inetAddress))
.count()
+ 1 /* add 1 because the player hasn't been added to the list of online players yet */;
// We use '>=' because the player connecting to the server hasn't joined yet
if (onlinePerIp >= maxOnlinePerIp) {
final Component component = Sonar.get().getConfig().getTooManyOnlinePerIp();
final PendingConnection connection = event.getConnection();
closeWith(((ChannelWrapper) CHANNEL_FIELD.get(connection)).getHandle(),
ProtocolVersion.fromId(connection.getVersion()), getCachedKickPacket(component));
}
}
}
+6
View File
@@ -1,5 +1,11 @@
dependencies {
compileOnly(project(":api"))
// adventure nbt for packets
compileOnly("net.kyori:adventure-nbt:4.16.0")
// CAPTCHA image generation
implementation("xyz.jonesdev:capja:1.0.2")
}
java.sourceCompatibility = JavaVersion.VERSION_11
@@ -21,6 +21,8 @@ import com.alessiodp.libby.Library;
import com.alessiodp.libby.LibraryManager;
import lombok.experimental.UtilityClass;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.SonarPlatform;
@UtilityClass
class LibraryLoader {
@@ -40,7 +42,7 @@ class LibraryLoader {
.groupId("com{}google{}code{}gson")
.artifactId("gson")
.version("2.10.1")
.relocate("com{}google{}code{}gson", "xyz{}jonesdev{}sonar{}libs{}gson")
.relocate("com{}google{}gson", "xyz{}jonesdev{}sonar{}libs{}gson")
.build(),
// Caffeine
Library.builder()
@@ -61,7 +63,33 @@ class LibraryLoader {
.groupId("com{}mysql")
.artifactId("mysql-connector-j")
.version("8.3.0")
.relocate("com{}mysql", "xyz{}jonesdev{}sonar{}libs{}mysql")
.build(),
// MariaDB driver
Library.builder()
.groupId("org{}mariadb{}jdbc")
.artifactId("mariadb-java-client")
.version("3.3.3")
.relocate("org{}mariadb", "xyz{}jonesdev{}sonar{}libs{}mariadb")
.build()
);
// Only load adventure dependencies if needed
if (Sonar.get().getPlatform() != SonarPlatform.VELOCITY) {
libraryManager.loadLibraries(
Library.builder()
.groupId("net{}kyori")
.artifactId("adventure-text-minimessage")
.version("4.16.0")
.relocate("net{}kyori", "xyz{}jonesdev{}sonar{}libs{}kyori")
.build(),
Library.builder()
.groupId("net{}kyori")
.artifactId("adventure-text-serializer-gson")
.version("4.16.0")
.relocate("net{}kyori", "xyz{}jonesdev{}sonar{}libs{}kyori")
.build()
);
}
}
}
@@ -19,6 +19,8 @@ package xyz.jonesdev.sonar.common.boot;
import com.alessiodp.libby.LibraryManager;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.j256.ormlite.logger.Level;
import com.j256.ormlite.logger.Logger;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
@@ -29,10 +31,13 @@ import xyz.jonesdev.sonar.api.SonarSupplier;
import xyz.jonesdev.sonar.api.command.subcommand.SubcommandRegistry;
import xyz.jonesdev.sonar.api.config.SonarConfiguration;
import xyz.jonesdev.sonar.api.controller.VerifiedPlayerController;
import xyz.jonesdev.sonar.api.statistics.SonarStatistics;
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.statistics.GlobalSonarStatistics;
import xyz.jonesdev.sonar.common.subcommand.SubcommandRegistryHolder;
import xyz.jonesdev.sonar.common.update.UpdateChecker;
@@ -51,6 +56,7 @@ public abstract class SonarBootstrap<T> implements Sonar {
private VerifiedPlayerController verifiedPlayerController;
private File dataDirectory;
private final LibraryManager libraryManager;
private final SonarStatistics statistics;
private final SonarPlatform platform;
private final SubcommandRegistry subcommandRegistry;
private final SystemTimer launchTimer = new SystemTimer();
@@ -63,16 +69,20 @@ public abstract class SonarBootstrap<T> implements Sonar {
SonarSupplier.set(this);
// Store our library manager for the API
this.libraryManager = libraryManager;
// Load all libraries before anything else
LibraryLoader.loadLibraries(libraryManager);
// Set the plugin instance
this.plugin = plugin;
this.dataDirectory = dataDirectory;
this.platform = platform;
// Load all libraries before anything else
LibraryLoader.loadLibraries(libraryManager);
// Load the rest of the components
this.statistics = new GlobalSonarStatistics();
this.verboseHandler = new Verbose();
this.notificationHandler = new Notification();
this.config = new SonarConfiguration(dataDirectory);
this.subcommandRegistry = new SubcommandRegistryHolder();
// Hide unnecessary debug information
Logger.setGlobalLogLevel(Level.WARNING);
}
public final void initialize() {
@@ -96,6 +106,10 @@ public abstract class SonarBootstrap<T> implements Sonar {
// Run the per-platform initialization method
enable();
// Start threads
getLogger().info("Starting all managed threads...");
SonarServiceThreadManager.start();
// Done
getLogger().info("Done ({}s)!", launchTimer);
} catch (Throwable throwable) {
@@ -118,17 +132,20 @@ public abstract class SonarBootstrap<T> implements Sonar {
// Warn player if they reloaded and changed the database type
if (verifiedPlayerController != null
&& verifiedPlayerController.getCachedDatabaseType() != getConfig().getDatabase().getType()) {
getLogger().warn("Reloading the server after changing the database type"
+ " is generally not recommended as it can sometimes cause data loss.");
getLogger().warn("Reloading after changing the database type is not recommended as it may cause data loss.");
}
// Prepare cached packets
getLogger().info("Taking cached snapshots of all packets...");
FallbackPreparer.prepare();
// Update ratelimiter cache
getFallback().getRatelimiter().setExpiringCache(Caffeine.newBuilder()
// Update ratelimiter caches
getFallback().getRatelimiter().setAttemptCache(Caffeine.newBuilder()
.expireAfterWrite(Duration.ofMillis(getConfig().getVerification().getReconnectDelay()))
.build());
getFallback().getRatelimiter().setFailCountCache(Caffeine.newBuilder()
.expireAfterWrite(Duration.ofMillis(getConfig().getVerification().getRememberTime()))
.build());
// Update blacklist cache
final long blacklistTime = getConfig().getVerification().getBlacklistTime();
@@ -149,14 +166,25 @@ public abstract class SonarBootstrap<T> implements Sonar {
}
// Reinitialize database controller
if (verifiedPlayerController != null) {
// Close the old connection first
verifiedPlayerController.close();
}
verifiedPlayerController = new VerifiedPlayerController();
}
public void shutdown() {
public final void shutdown() {
// Initialize shutdown process
getLogger().info("Starting shutdown process...");
// ...
// Interrupt threads
SonarServiceThreadManager.stop();
// Close database connection
verifiedPlayerController.close();
// Run the per-platform disable method
disable();
// Thank the user for using Sonar
getLogger().info("Successfully shut down. Goodbye!");
}
public abstract void disable();
}
@@ -26,8 +26,8 @@ import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.jetbrains.annotations.NotNull;
import static xyz.jonesdev.sonar.api.statistics.Bandwidth.INCOMING;
import static xyz.jonesdev.sonar.api.statistics.Bandwidth.OUTGOING;
import static xyz.jonesdev.sonar.common.statistics.CachedBandwidthStatistics.INCOMING;
import static xyz.jonesdev.sonar.common.statistics.CachedBandwidthStatistics.OUTGOING;
@ChannelHandler.Sharable
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@@ -0,0 +1,247 @@
/*
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.handler.codec.CorruptedFrameException;
import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.fallback.Fallback;
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer;
import xyz.jonesdev.sonar.common.statistics.GlobalSonarStatistics;
import xyz.jonesdev.sonar.common.util.GeyserDetection;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.util.UUID;
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_BANDWIDTH;
import static xyz.jonesdev.sonar.common.fallback.FallbackUserWrapper.customDisconnect;
import static xyz.jonesdev.sonar.common.fallback.FallbackUserWrapper.deject;
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer.*;
@RequiredArgsConstructor
public class FallbackChannelHandlerAdapter extends ChannelInboundHandlerAdapter {
protected final Channel channel;
protected @Nullable String username;
protected InetAddress inetAddress;
protected ProtocolVersion protocolVersion;
protected @Nullable FallbackUser user;
protected static final Fallback FALLBACK = Sonar.get().getFallback();
@Override
public final void channelActive(final @NotNull ChannelHandlerContext ctx) {
// Increase connections per second for the action bar verbose
GlobalSonarStatistics.countConnection();
// Make sure to let the server handle the rest
ctx.fireChannelActive();
}
@Override
public final void channelInactive(final @NotNull ChannelHandlerContext ctx) {
// 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
if (inetAddress != null) {
// Remove the IP address from the queue
FALLBACK.getQueue().getQueuedPlayers().remove(inetAddress);
}
// Make sure to let the server handle the rest
ctx.fireChannelInactive();
}
// We can override the default exceptionCaught method since this handler
// will run before the connection knows that there has been an error.
// Additionally, this will also run after our custom decoder.
@Override
public final void exceptionCaught(final @NotNull ChannelHandlerContext ctx,
final @NotNull Throwable cause) throws Exception {
// Simply close the channel if we encounter any errors.
ctx.close();
}
/**
* Validates and handles incoming handshake packets
*
* @param hostname Hostname (server address) sent by the client
* @param protocol Protocol version number sent by the client
*/
protected final void handleHandshake(final @NotNull String hostname, final int protocol) throws Exception {
// Check if the hostname is invalid
if (hostname.isEmpty()) {
throw new CorruptedFrameException("Hostname is empty");
}
// Check if the player has already sent a handshake packet
if (protocolVersion != null) {
throw new CorruptedFrameException("Already sent handshake");
}
// Store the protocol version
protocolVersion = ProtocolVersion.fromId(protocol);
// Hook the traffic listener
// TODO: Can we implement this in channelActive?
channel.pipeline().addFirst(FALLBACK_BANDWIDTH, FallbackBandwidthHandler.INSTANCE);
}
/**
* Validates and handles incoming login packets
*
* @param ctx Forwarded channel handler context
* @param loginPacket Login packet sent by the client
* @param username Username sent by the client
* @param socketAddress Socket address of the client
*/
protected final void handleLogin(final @NotNull ChannelHandlerContext ctx,
final @NotNull Object loginPacket,
final @NotNull String username,
final @NotNull InetSocketAddress socketAddress,
final @NotNull String encoder,
final @NotNull String decoder,
final @NotNull String timeout,
final @NotNull String handler) throws Exception {
// Check if the player has already sent a login packet
if (this.username != null) {
throw new CorruptedFrameException("Already sent login");
}
// Connections from unknown protocol versions will be discarded
// as this is the safest way of handling unwanted connections
if (protocolVersion.isUnknown()) {
// Sonar does NOT support snapshots or unknown versions;
// I'll try my best to stay up-to-date!
throw new CorruptedFrameException("Unknown protocol version");
}
// Increase joins per second for the action bar verbose
GlobalSonarStatistics.countLogin();
// Store the username and IP address
this.username = username;
this.inetAddress = socketAddress.getAddress();
// Check the blacklist here since we cannot let the player "ghost join"
if (FALLBACK.getBlacklist().asMap().containsKey(inetAddress)) {
customDisconnect(channel, protocolVersion, blacklisted, encoder, handler);
return;
}
// Don't continue the verification process if the verification is disabled
if (!Sonar.get().getFallback().shouldVerifyNewPlayers()) {
initialLogin(ctx, loginPacket, encoder, handler);
return;
}
// Completely skip Geyser connections if configured
final boolean geyser = GeyserDetection.isGeyserConnection(channel, socketAddress);
if (geyser && !Sonar.get().getConfig().getVerification().isCheckGeyser()) {
initialLogin(ctx, loginPacket, encoder, handler);
return;
}
// Check if the 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())) {
customDisconnect(channel, protocolVersion, protocolBlacklisted, encoder, handler);
return;
}
// Make sure we actually have to verify the player
final String offlineUUIDString = "OfflinePlayer:" + username;
final UUID offlineUUID = UUID.nameUUIDFromBytes(offlineUUIDString.getBytes(StandardCharsets.UTF_8));
if (Sonar.get().getVerifiedPlayerController().has(inetAddress, offlineUUID)) {
initialLogin(ctx, loginPacket, encoder, handler);
return;
}
// Check if the IP address is currently being rate-limited
if (!FALLBACK.getRatelimiter().attempt(inetAddress)) {
customDisconnect(channel, protocolVersion, reconnectedTooFast, encoder, handler);
return;
}
// Check if the protocol ID of the player is allowed to bypass verification
if (Sonar.get().getConfig().getVerification().getWhitelistedProtocols()
.contains(protocolVersion.getProtocol())) {
initialLogin(ctx, loginPacket, encoder, handler);
return;
}
// Queue the connection for further processing
FALLBACK.getQueue().getQueuedPlayers().put(inetAddress, () -> {
// Check if the username matches the valid name regex to prevent
// UTF-16 names or other types of exploits
if (!Sonar.get().getConfig().getVerification().getValidNameRegex()
.matcher(username).matches()) {
customDisconnect(channel, protocolVersion, invalidUsername, encoder, handler);
return;
}
// Create an instance for the Fallback connection
user = new FallbackUserWrapper(channel, inetAddress, protocolVersion, geyser);
// Let the verification handler take over the channel
user.hijack(username, offlineUUID, encoder, decoder, timeout, handler);
});
}
/**
* Executes the maximum accounts per IP limit check before letting the player join
*
* @param ctx Forwarded channel handler context
* @param loginPacket Login packet sent by the client
*/
protected final void initialLogin(final @NotNull ChannelHandlerContext ctx,
final @NotNull Object loginPacket,
final @NotNull String encoder,
final @NotNull String handler) throws Exception {
final int maxOnlinePerIp = Sonar.get().getConfig().getMaxOnlinePerIp();
// Skip the maximum online per IP check if it's disabled in the configuration
if (maxOnlinePerIp > 0) {
// Check if the number of online players using the same IP address as
// the connecting player is greater than the configured amount
if (Sonar.get().hasTooManyAccounts(inetAddress, maxOnlinePerIp)) {
customDisconnect(channel, protocolVersion, FallbackPreparer.tooManyOnlinePerIP, encoder, handler);
return;
}
}
ctx.fireChannelRead(loginPacket);
// Deject the channel since we don't need it anymore
deject(channel.pipeline());
}
}
@@ -19,28 +19,35 @@ package xyz.jonesdev.sonar.common.fallback;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelPipeline;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import net.kyori.adventure.text.Component;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.event.impl.UserBlacklistedEvent;
import xyz.jonesdev.sonar.api.event.impl.UserVerifyFailedEvent;
import xyz.jonesdev.sonar.api.event.impl.UserVerifyJoinEvent;
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
import xyz.jonesdev.sonar.api.fallback.FallbackUserState;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.api.statistics.Statistics;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketDecoder;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketEncoder;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.login.LoginSuccess;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.Disconnect;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.DisconnectPacket;
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.FALLBACK_PACKET_DECODER;
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_PACKET_ENCODER;
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.*;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_20_2;
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry.CONFIG;
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry.GAME;
@Getter
@ToString(of = {"protocolVersion", "inetAddress"})
@@ -49,25 +56,154 @@ 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+
public FallbackUserWrapper(final @NotNull Channel channel,
final @NotNull InetAddress inetAddress,
final @NotNull ProtocolVersion protocolVersion) {
final @NotNull ProtocolVersion protocolVersion,
final boolean geyser) {
this.channel = channel;
this.pipeline = channel.pipeline();
this.inetAddress = inetAddress;
this.protocolVersion = protocolVersion;
this.geyser = geyser;
}
/**
* Disconnect the player during/after verification
* using our custom {@link Disconnect} packet.
*
* @param reason Disconnect message component
*/
@Override
public void disconnect(final @NotNull Component reason) {
closeWith(channel, protocolVersion, Disconnect.create(reason));
closeWith(channel, protocolVersion, DisconnectPacket.create(reason, false));
}
@Override
public void hijack(final @NotNull String username, final @NotNull UUID uuid,
final @NotNull String encoder, final @NotNull String decoder,
final @NotNull String timeout, final @NotNull String handler) {
// The player has joined the verification
GlobalSonarStatistics.totalAttemptedVerifications++;
if (Sonar.get().getConfig().getVerification().isLogConnections()) {
// Only log the processing message if the server isn't under attack.
// We let the user override this through the configuration.
if (Sonar.get().getAttackTracker().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())));
}
}
// 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);
channel.eventLoop().execute(() -> {
// Remove main pipeline to completely take over the channel
if (pipeline.get(handler) != null) {
pipeline.remove(handler);
}
// Send LoginSuccess packet to make the client think they are joining the server
write(FallbackPreparer.LOGIN_SUCCESS);
// The LoginSuccess packet has been sent, now we can change the registry state
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
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();
}
} 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
channel.close();
}
});
}
@Override
public void fail(final @NotNull String reason) {
// Make sure to set the state to failed to drop all handlers
setState(FallbackUserState.FAILED);
// 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));
}
}
// Increment amount of total failed verifications
GlobalSonarStatistics.totalFailedVerifications++;
// Call the VerifyFailedEvent for external API usage
Sonar.get().getEventManager().publish(new UserVerifyFailedEvent(this, reason));
// Use a label, so we can easily add more code beneath this method in the future
blacklist: {
// Check if the player has too many failed attempts
final int blacklistThreshold = Sonar.get().getConfig().getVerification().getBlacklistThreshold();
// The user is allowed to disable the blacklist entirely by setting the threshold to 0
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);
// Now we simply need to check if the threshold is reached
if (failCount < blacklistThreshold) {
// Make sure we increment the number of fails
Sonar.get().getFallback().getRatelimiter().incrementFails(inetAddress, failCount);
break blacklist;
}
// Call the BotBlacklistedEvent for external API usage
Sonar.get().getEventManager().publish(new UserBlacklistedEvent(this));
// Increment amount 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())));
}
// Invalidate the cached entry to ensure memory safety
Sonar.get().getFallback().getRatelimiter().getFailCountCache().invalidate(inetAddress);
}
}
/**
@@ -94,69 +230,60 @@ public final class FallbackUserWrapper implements FallbackUser {
}
/**
* Takes over the channel and begins the verification process
* Disconnect the player before verification (during login)
* by replacing the encoder before running the method.
*
* @param username Username of the player
* @param uuid UUID of the player
* @param encoder Name of the encoder pipeline
* @param decoder Name of the decoder pipeline
* @param timeout Name of the read timeout pipeline
* @param packet Disconnect packet
* @param encoder Encoder to replace
* @param handler Main pipeline to remove
* @param channel Channel of the player
* @param protocolVersion Protocol version of the player
*/
@Override
public void hijack(final @NotNull String username, final @NotNull UUID uuid,
final @NotNull String encoder, final @NotNull String decoder,
final @NotNull String timeout, final @NotNull String boss) {
// This rarely happens when the channel hangs, but the player is still connecting
// This also fixes a unique issue with TCPShield and other reverse proxies
if (pipeline.get(encoder) == null || pipeline.get(decoder) == null) {
channel.close();
return;
public static void customDisconnect(final @NotNull Channel channel,
final @NotNull ProtocolVersion protocolVersion,
final @NotNull FallbackPacket packet,
final @NotNull String encoder,
final @NotNull String handler) {
if (channel.eventLoop().inEventLoop()) {
_customDisconnect(channel, protocolVersion, packet, encoder, handler);
} else {
channel.eventLoop().execute(() -> _customDisconnect(channel, protocolVersion, packet, encoder, handler));
}
}
private static void _customDisconnect(final @NotNull Channel channel,
final @NotNull ProtocolVersion protocolVersion,
final @NotNull FallbackPacket packet,
final @NotNull String encoder,
final @NotNull String boss) {
// Remove main pipeline to completely take over the channel
if (pipeline.get(boss) != null) {
pipeline.remove(boss);
if (channel.pipeline().get(boss) != null) {
channel.pipeline().remove(boss);
}
// The player has joined the verification
Statistics.REAL_TRAFFIC.increment();
if (Sonar.get().getConfig().getVerification().isLogConnections()) {
// Only log the processing message if the server isn't under attack.
// We let the user override this through the configuration.
if (!Sonar.get().getAttackTracker().isCurrentlyUnderAttack()
|| 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())));
final ChannelHandler currentEncoder = channel.pipeline().get(encoder);
// Simply close the channel if no decoder exists
if (currentEncoder != null) {
// We don't need to update the encoder if it's already present
if (!(currentEncoder instanceof FallbackPacketEncoder)) {
final FallbackPacketEncoder newEncoder = new FallbackPacketEncoder(protocolVersion);
channel.pipeline().replace(encoder, FALLBACK_PACKET_ENCODER, newEncoder);
}
closeWith(channel, protocolVersion, packet);
} else {
channel.close();
}
}
// 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);
// Send LoginSuccess packet to make the client think they are joining the server
write(new LoginSuccess(username, uuid));
// The LoginSuccess packet has been sent, now we can change the registry state
newEncoder.updateRegistry(protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_20_2) >= 0
? FallbackPacketRegistry.CONFIG : FallbackPacketRegistry.GAME);
// Replace normal decoder to allow custom packets
pipeline.replace(decoder, FALLBACK_PACKET_DECODER,
new FallbackPacketDecoder(this, new FallbackVerificationHandler(this, username, uuid)));
/**
* 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);
}
}
}
@@ -21,23 +21,24 @@ import io.netty.buffer.ByteBuf;
import io.netty.handler.codec.CorruptedFrameException;
import io.netty.handler.codec.DecoderException;
import lombok.val;
import net.kyori.adventure.text.minimessage.MiniMessage;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.event.impl.UserVerifySuccessEvent;
import xyz.jonesdev.sonar.api.fallback.Fallback;
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
import xyz.jonesdev.sonar.api.fallback.FallbackUserState;
import xyz.jonesdev.sonar.api.model.VerifiedPlayer;
import xyz.jonesdev.sonar.api.timer.SystemTimer;
import xyz.jonesdev.sonar.common.fallback.protocol.*;
import xyz.jonesdev.sonar.common.fallback.protocol.map.ItemMapType;
import xyz.jonesdev.sonar.common.fallback.protocol.map.MapInfoPreparer;
import xyz.jonesdev.sonar.common.fallback.protocol.map.PreparedMapInfo;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.config.FinishConfiguration;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.login.LoginAcknowledged;
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.utility.protocol.ProtocolUtil;
import xyz.jonesdev.sonar.common.statistics.GlobalSonarStatistics;
import xyz.jonesdev.sonar.common.util.ProtocolUtil;
import java.util.Objects;
import java.util.Random;
@@ -47,6 +48,7 @@ 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.*;
@@ -54,41 +56,26 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
private static final Random RANDOM = new Random();
private static final Fallback FALLBACK = Sonar.get().getFallback();
// General
private final SystemTimer login = new SystemTimer();
private final @NotNull FallbackUser user;
private final String username;
private final UUID playerUuid;
private @NotNull State state = State.LOGIN_ACK; // 1.20.2
// Checks
private short expectedTransactionId;
private int expectedKeepAliveId, expectedTeleportId = -1;
private int tick, totalReceivedPackets, ignoredMovementTicks;
private int tick, totalReceivedPackets;
private double posX, posY, posZ, lastY, spawnYPosition;
private boolean resolvedClientBrand, resolvedClientSettings;
private boolean listenForMovements;
private boolean receivedSteerBoat, receivedPlayerInput;
// Map captcha
private final SystemTimer keepAlive = new SystemTimer();
private final SystemTimer actionBar = new SystemTimer();
private @Nullable PreparedMapInfo captcha;
private int captchaTriesLeft;
// CAPTCHA
private @Nullable MapCaptchaInfo captcha;
private int captchaTriesLeft, lastCountdownIndex;
public enum State {
// 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,
// Captcha
MAP_CAPTCHA,
// Done
SUCCESS
}
// 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,
@@ -96,18 +83,26 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
this.user = user;
this.username = username;
this.playerUuid = playerUuid;
if (user.getProtocolVersion().compareTo(MINECRAFT_1_20_2) < 0) {
// Start initializing the actual join process
initialJoinProcess();
}
// 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
state = State.CONFIGURE;
user.setState(CONFIGURE);
// Send the necessary configuration packets to the client
user.delayedWrite(REGISTRY_SYNC);
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();
@@ -130,28 +125,28 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
}
}
private void initialJoinProcess() {
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
state = State.KEEP_ALIVE;
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 KeepAlive(expectedKeepAliveId));
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.
state = State.TRANSACTION;
user.setState(TRANSACTION);
// Generate a random transaction ID
expectedTransactionId = (short) RANDOM.nextInt();
// Send a transaction with the given ID
user.write(new Transaction(0, expectedTransactionId, false));
user.write(new TransactionPacket(0, expectedTransactionId, false));
}
private void sendJoinGamePacket() {
@@ -160,7 +155,7 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
// 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.
state = State.CLIENT_SETTINGS;
user.setState(CLIENT_SETTINGS);
}
// Send the JoinGame packet so we can continue the verification
user.write(joinGame);
@@ -174,7 +169,7 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
private void sendAbilitiesAndTeleport() {
// Set the state to TELEPORT to avoid false positives
// and go on with the flow of the verification.
state = State.TELEPORT;
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);
@@ -184,7 +179,7 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
// 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 PositionLook(
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
@@ -194,16 +189,16 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
}
private void sendChunkData() {
// If we don't have gravity and captcha enabled, simply finish verification
if (!Sonar.get().getConfig().getVerification().getGravity().isEnabled()
&& !FALLBACK.shouldDoMapCaptcha()) {
// 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.
state = State.POSITION;
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
@@ -211,19 +206,17 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
}
// 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);
// Checking gravity is disabled, just finish verification
if (!Sonar.get().getConfig().getVerification().getGravity().isEnabled()) {
// Switch to captcha state if needed
captchaOrFinish();
} else {
// Make sure the player knows we are checking them
user.delayedWrite(youAreBeingChecked);
// Send all packets in one flush
user.getChannel().flush();
}
// Send all packets in one flush
user.getChannel().flush();
}
private static boolean validateClientLocale(final @SuppressWarnings("unused") @NotNull FallbackUser user,
@@ -256,124 +249,167 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
}
}
private void handlePacketDuringCaptcha(final @NotNull FallbackPacket packet) {
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 Chat) {
final Chat chat = (Chat) packet;
Objects.requireNonNull(captcha);
if (!chat.getMessage().equals(captcha.getInfo().getAnswer())) {
// Captcha is incorrect
checkFrame(captchaTriesLeft-- > 0, "failed captcha too often");
user.write(incorrectCaptcha);
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 correct
finish();
// Captcha is incorrect
checkFrame(captchaTriesLeft-- > 0, "failed captcha too often");
user.write(incorrectCaptcha);
return;
}
// Every second
if (actionBar.elapsed(1000L)) {
final String actionBarMessage = Sonar.get().getConfig().getVerification().getMap().getEnterCodeActionBar();
// Only send action bar if the message is actually supposed to be sent
if (!actionBarMessage.isEmpty()) {
final String timeLeft = String.format("%.0f", (maxDuration - login.delay()) / 1000D);
user.write(new Chat(MiniMessage.miniMessage().deserialize(
actionBarMessage.replace("%time-left%", timeLeft)), Chat.GAME_INFO_TYPE));
// Make sure to reset the timer
actionBar.reset();
// 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);
}
}
// Every 10 seconds
if (keepAlive.elapsed(10_000L)) {
// Send the message again to remind the player
user.delayedWrite(enterCodeMessage);
// Send a KeepAlive packet to prevent timeout
user.delayedWrite(CAPTCHA_KEEP_ALIVE);
// Send both packets in one flush
user.getChannel().flush();
// Make sure to reset the timer
keepAlive.reset();
lastCountdownIndex = index;
}
}
@Override
public void handle(final @NotNull FallbackPacket packet) {
// The player has already been verified, drop all other packets
if (state == State.SUCCESS) return;
// The player has already been verified or failed the verification -> drop packet
if (!user.getState().canReceivePackets()) return;
// We are expecting a captcha code in chat, drop all other packets
if (state == State.MAP_CAPTCHA) {
handlePacketDuringCaptcha(packet);
// 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;
}
// Check if the player is not sending a ton of packets to the server
final int maxPackets = Sonar.get().getConfig().getVerification().getMaxLoginPackets() + maxMovementTick;
checkFrame(++totalReceivedPackets < maxPackets, "too many packets");
if (packet instanceof PaddleBoatPacket) {
// Check if we are currently expecting a PaddleBoatPacket packet
assertState(VEHICLE);
// Check for timeout since the player could be sending packets but not important ones
final long timeout = Sonar.get().getConfig().getVerification().getMaxPing();
checkFrame(!login.elapsed(timeout), "time limit exceeded");
// Only pass the player if both checks are passed
if (receivedPlayerInput && receivedSteerBoat) {
captchaOrNext();
}
if (packet instanceof LoginAcknowledged) {
// 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(State.LOGIN_ACK);
assertState(LOGIN_ACK);
// Start the configuration process for 1.20.2 clients
configure();
return;
}
if (packet instanceof FinishConfiguration) {
if (packet instanceof FinishConfigurationPacket) {
// Check if we are currently expecting a FinishConfiguration packet
assertState(State.CONFIGURE);
assertState(CONFIGURE);
// Check if the client has already sent valid ClientSettings and PluginMessage packets
checkFrame(resolvedClientBrand, "did not resolve client brand");
checkFrame(resolvedClientSettings, "did not resolve client settings");
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 KeepAlive) {
final KeepAlive keepAlive = (KeepAlive) packet;
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(State.KEEP_ALIVE);
assertState(KEEP_ALIVE);
checkFrame(keepAlive.getId() == expectedKeepAliveId, "invalid KeepAlive ID");
// The correct KeepAlive packet has been received
sendJoinGamePacket();
return;
}
if (packet instanceof ClientSettings) {
final ClientSettings clientSettings = (ClientSettings) packet;
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.
if (state == State.CLIENT_SETTINGS) {
state = State.PLUGIN_MESSAGE;
// Probably caused by some weird race condition in Minecraft 1.13+
if (user.getState() == CLIENT_SETTINGS) {
user.setState(PLUGIN_MESSAGE);
}
// Make sure we mark the ClientSettings as valid
resolvedClientSettings = true;
return;
}
if (packet instanceof PluginMessage) {
final PluginMessage pluginMessage = (PluginMessage) packet;
if (packet instanceof PluginMessagePacket) {
final PluginMessagePacket pluginMessage = (PluginMessagePacket) packet;
// Only the brand channel is important, drop the rest
if (pluginMessage.getChannel().equals("MC|Brand")
@@ -390,11 +426,11 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
// Validate the client branding using a regex to filter unwanted characters.
if (Sonar.get().getConfig().getVerification().getBrand().isEnabled()) {
checkFrame(validateClientBrand(user, pluginMessage.content()), "invalid client brand");
checkFrame(validateClientBrand(user, pluginMessage.getSlicedBuffer()), "invalid client brand");
}
// Clients sometimes mess up the ClientSettings or PluginMessage packet.
if (state == State.PLUGIN_MESSAGE) {
if (user.getState() == PLUGIN_MESSAGE) {
// Send the transaction packet
sendTransaction();
}
@@ -402,18 +438,19 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
// Make sure we mark the PluginMessage as valid
resolvedClientBrand = true;
}
return;
}
if (packet instanceof Transaction) {
if (packet instanceof TransactionPacket) {
// Check if we are currently expecting a Transaction packet
assertState(State.TRANSACTION);
assertState(TRANSACTION);
final Transaction transaction = (Transaction) packet;
final TransactionPacket transaction = (TransactionPacket) packet;
// The transaction should always be accepted
checkFrame(transaction.isAccepted(), "transaction not accepted?!");
// Check if the transaction ID is valid
checkFrame(transaction.getId() == expectedTransactionId, "invalid transaction id");
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.
@@ -429,181 +466,215 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
// the channel earlier when we teleported the player.
user.getChannel().flush();
}
return;
}
if (packet instanceof TeleportConfirm) {
if (packet instanceof TeleportConfirmPacket) {
// Check if we are currently expecting a TeleportConfirm packet
assertState(State.TELEPORT);
assertState(TELEPORT);
// Check if the teleport ID is correct
final TeleportConfirm teleportConfirm = (TeleportConfirm) packet;
final TeleportConfirmPacket teleportConfirm = (TeleportConfirmPacket) packet;
checkFrame(teleportConfirm.getTeleportId() == expectedTeleportId, "invalid teleport ID");
// Reset all values to ensure safety on teleport
tick = 1;
posY = lastY = -1;
expectedTeleportId = -1;
posY = lastY = expectedTeleportId = -1;
// Now we can send the chunk data
sendChunkData();
return;
}
if (state != State.LOGIN_ACK) {
if (packet instanceof Position) {
final Position position = (Position) packet;
// 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 PositionLook) {
final PositionLook position = (PositionLook) packet;
if (packet instanceof PlayerPositionLookPacket) {
final PlayerPositionLookPacket position = (PlayerPositionLookPacket) packet;
handlePositionUpdate(position.getX(), position.getY(), position.getZ(), position.isOnGround());
return;
}
if (packet instanceof Player) {
final Player player = (Player) packet;
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) {
// 1.8 does not have a TeleportConfirm packet, so we need to handle the spawning differently
if (user.getProtocolVersion().compareTo(MINECRAFT_1_8) <= 0
&& expectedTeleportId != -1 // Check if the teleport ID is currently unset
// Then, check if the position is equal to the spawn position
&& x == SPAWN_X_POSITION && y == spawnYPosition && z == SPAWN_Z_POSITION) {
// Reset all values to ensure safety on teleport
tick = 1;
posY = -1;
expectedTeleportId = -1;
}
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;
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");
}
lastY = spawnYPosition;
return;
}
posX = x;
lastY = posY;
posY = y;
posZ = z;
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) {
// Check for too many ignored Y ticks
final int maxIgnoredTicks = Sonar.get().getConfig().getVerification().getGravity().getMaxIgnoredTicks();
checkFrame(++ignoredMovementTicks < maxIgnoredTicks, "too many ignored ticks");
return;
}
// 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)");
// 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 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;
}
// 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 (!Sonar.get().getConfig().getVerification().getGravity().isCheckCollisions()) {
captchaOrFinish();
lastY = spawnYPosition;
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
captchaOrFinish();
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 {
// Make sure the player is colliding with blocks but is sending an invalid ground state
checkFrame(collisionOffsetY != 0, "no ground: " + collisionOffsetY);
}
} else {
// Check if the player is spoofing the ground state
checkFrame(!ground, "spoofed ground state");
}
// Make sure we don't run out of predicted Y motions
checkFrame(tick < preparedCachedYMotions.length, "too many movements");
if (!ground) {
final double predictedY = preparedCachedYMotions[tick];
final double offsetY = Math.abs(deltaY - predictedY);
// Log/debug position if enabled in the configuration
if (Sonar.get().getConfig().getVerification().isDebugXYZPositions()) {
FALLBACK.getLogger().info("{}: {}/{}/{} - deltaY: {} - prediction: {} - offset: {}",
username, x, y, z, deltaY, predictedY, offsetY);
// Check if the player is spoofing the ground state
checkFrame(!ground, "spoofed ground state");
}
// Check if the y motion is roughly equal to the predicted value
checkFrame(offsetY < 0.005, String.format("invalid gravity: %d, %.7f, %.10f, %.10f != %.10f",
tick, y, offsetY, deltaY, predictedY));
}
tick++;
}
// Make sure we don't run out of predicted Y motions
checkFrame(tick < preparedCachedYMotions.length, "too many movements");
private void captchaOrFinish() {
if (FALLBACK.shouldDoMapCaptcha()) {
if (MapInfoPreparer.getPreparedCAPTCHAs() == 0) {
// This should not happen, but we have to return if there is no captcha prepared
user.disconnect(Sonar.get().getConfig().getVerification().getCurrentlyPreparing());
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;
}
// Set the state to MAP_CAPTCHA, so we don't handle any unnecessary packets
state = State.MAP_CAPTCHA;
if (!Sonar.get().getConfig().getVerification().getGravity().isEnabled()
&& 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);
}
// Initialize the map captcha
handleMapCaptcha();
} else {
// Finish the verification
finish();
// Interrupt the netty eventLoop to immediately stop further processing
throw new DecoderException(exception);
}
}
private void handleMapCaptcha() {
// 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 SetSlot(0, 36, 1, 0,
ItemMapType.FILLED_MAP.getId(user.getProtocolVersion()), SetSlot.MAP_NBT));
user.delayedWrite(new SetSlotPacket(36, 1,
ItemType.FILLED_MAP.getId(user.getProtocolVersion()), SetSlotPacket.MAP_NBT));
// Send random captcha to the player
captcha = MapInfoPreparer.getRandomCaptcha();
Objects.requireNonNull(captcha).write(user);
captcha = MAP_INFO_PREPARER.getRandomCaptcha();
Objects.requireNonNull(captcha).delayedWrite(user);
// Teleport the player to the position above the platform
user.delayedWrite(CAPTCHA_POSITION);
// Make sure the player cannot move
user.delayedWrite(CAPTCHA_ABILITIES);
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
@@ -611,9 +682,11 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
}
private void finish() {
state = State.SUCCESS;
// Make sure we drop all further packets
user.setState(SUCCESS);
FALLBACK.getConnected().remove(username);
// Increment amount of total successful verifications
GlobalSonarStatistics.totalSuccessfulVerifications++;
// Add verified player to the database
final VerifiedPlayer verifiedPlayer = new VerifiedPlayer(user.getInetAddress().toString(), playerUuid,
@@ -623,6 +696,13 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
// 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());
@@ -636,7 +716,8 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
*
* @param expectedState Expected state
*/
private void assertState(final @NotNull State expectedState) {
private void assertState(final @NotNull FallbackUserState expectedState) {
final FallbackUserState state = user.getState();
checkFrame(state == expectedState, "expected " + expectedState + ", got " + state);
}
@@ -646,9 +727,14 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
* @param condition Condition to fail if it's false
* @param message Messages displayed in the stacktrace
*/
private void checkFrame(final boolean condition, final String message) {
private void checkFrame(final boolean condition, final @NotNull String message) {
if (!condition) {
user.fail(message);
// 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,7 +20,10 @@ 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
@@ -28,7 +28,7 @@ import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry.Direction.SERVERBOUND;
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry.GAME;
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry.LOGIN;
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.readVarInt;
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.readVarInt;
public final class FallbackPacketDecoder extends ChannelInboundHandlerAdapter {
private final FallbackUser user;
@@ -98,14 +98,10 @@ public final class FallbackPacketDecoder extends ChannelInboundHandlerAdapter {
// Release the ByteBuf to avoid memory leaks
byteBuf.release();
}
} else {
// Packets are always ByteBufs - if we detect an unknown object, fail verification
user.fail("packet is not a ByteBuf");
throw new CorruptedFrameException("Packet is not a ByteBuf?!");
}
}
private void doLengthSanityChecks(final ByteBuf byteBuf,
private void doLengthSanityChecks(final @NotNull ByteBuf byteBuf,
final @NotNull FallbackPacket packet) throws Exception {
final int expectedMaxLen = packet.expectedMaxLength(byteBuf, user.getProtocolVersion());
if (expectedMaxLen != -1 && byteBuf.readableBytes() > expectedMaxLen) {
@@ -25,7 +25,7 @@ import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry.Direction.CLIENTBOUND;
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry.LOGIN;
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.writeVarInt;
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.writeVarInt;
public final class FallbackPacketEncoder extends MessageToByteEncoder<FallbackPacket> {
private final ProtocolVersion protocolVersion;
@@ -42,10 +42,12 @@ public final class FallbackPacketEncoder extends MessageToByteEncoder<FallbackPa
@Override
protected void encode(final ChannelHandlerContext ctx,
final FallbackPacket msg,
final @NotNull FallbackPacket packet,
final ByteBuf out) throws Exception {
final int packetId = registry.getPacketId(msg);
final FallbackPacket originalPacket = packet instanceof FallbackPacketSnapshot
? ((FallbackPacketSnapshot) packet).getOriginalPacket() : packet;
final int packetId = registry.getPacketId(originalPacket);
writeVarInt(out, packetId);
msg.encode(out, protocolVersion);
packet.encode(out, protocolVersion);
}
}
@@ -22,10 +22,10 @@ import io.netty.util.collection.IntObjectMap;
import lombok.Data;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.config.FinishConfiguration;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.config.RegistryData;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.login.LoginAcknowledged;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.login.LoginSuccess;
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.login.LoginAcknowledgedPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.login.LoginSuccessPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.*;
import java.util.*;
@@ -33,42 +33,53 @@ import java.util.function.Supplier;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
// Most of this is taken from
// Mostly taken from
// https://github.com/PaperMC/Velocity/blob/dev/3.0.0/proxy/src/main/java/com/velocitypowered/proxy/protocol/StateRegistry.java
public enum FallbackPacketRegistry {
LOGIN {
{
serverbound.register(LoginAcknowledged.class, LoginAcknowledged::new,
serverbound.register(LoginAcknowledgedPacket.class, LoginAcknowledgedPacket::new,
map(0x03, MINECRAFT_1_20_2, false));
clientbound.register(LoginSuccess.class, LoginSuccess::new,
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));
}
},
CONFIG {
{
clientbound.register(Disconnect.class, Disconnect::new,
map(0x01, MINECRAFT_1_20_2, true));
clientbound.register(FinishConfiguration.class, FinishConfiguration::new,
map(0x02, MINECRAFT_1_20_2, false));
clientbound.register(KeepAlive.class, KeepAlive::new,
map(0x03, MINECRAFT_1_20_2, false));
clientbound.register(RegistryData.class, RegistryData::new,
map(0x05, MINECRAFT_1_20_2, true));
clientbound.register(DisconnectPacket.class, DisconnectPacket::new,
map(0x01, MINECRAFT_1_20_2, true),
map(0x02, MINECRAFT_1_20_5, true));
clientbound.register(FinishConfigurationPacket.class, FinishConfigurationPacket::new,
map(0x02, MINECRAFT_1_20_2, false),
map(0x03, MINECRAFT_1_20_5, false));
clientbound.register(KeepAlivePacket.class, KeepAlivePacket::new,
map(0x03, MINECRAFT_1_20_2, false),
map(0x04, MINECRAFT_1_20_5, false));
serverbound.register(ClientSettings.class, ClientSettings::new,
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,
map(0x00, MINECRAFT_1_20_2, false));
serverbound.register(PluginMessage.class, PluginMessage::new,
map(0x01, MINECRAFT_1_20_2, false));
serverbound.register(FinishConfiguration.class, FinishConfiguration::new,
map(0x02, MINECRAFT_1_20_2, false));
serverbound.register(KeepAlive.class, KeepAlive::new,
map(0x03, MINECRAFT_1_20_2, false));
serverbound.register(PluginMessagePacket.class, PluginMessagePacket::new,
map(0x01, MINECRAFT_1_20_2, false),
map(0x02, MINECRAFT_1_20_5, false));
serverbound.register(FinishConfigurationPacket.class, FinishConfigurationPacket::new,
map(0x02, MINECRAFT_1_20_2, false),
map(0x03, MINECRAFT_1_20_5, false));
serverbound.register(KeepAlivePacket.class, KeepAlivePacket::new,
map(0x03, MINECRAFT_1_20_2, false),
map(0x04, MINECRAFT_1_20_5, false));
}
},
GAME {
{
clientbound.register(JoinGame.class, JoinGame::new,
clientbound.register(JoinGamePacket.class, JoinGamePacket::new,
map(0x01, MINECRAFT_1_7_2, true),
map(0x23, MINECRAFT_1_9, true),
map(0x25, MINECRAFT_1_13, true),
@@ -81,8 +92,9 @@ public enum FallbackPacketRegistry {
map(0x25, MINECRAFT_1_19_1, true),
map(0x24, MINECRAFT_1_19_3, true),
map(0x28, MINECRAFT_1_19_4, true),
map(0x29, MINECRAFT_1_20_2, true));
clientbound.register(KeepAlive.class, KeepAlive::new,
map(0x29, MINECRAFT_1_20_2, true),
map(0x2B, MINECRAFT_1_20_5, true));
clientbound.register(KeepAlivePacket.class, KeepAlivePacket::new,
map(0x00, MINECRAFT_1_7_2, false),
map(0x1F, MINECRAFT_1_9, false),
map(0x21, MINECRAFT_1_13, false),
@@ -95,8 +107,9 @@ public enum FallbackPacketRegistry {
map(0x20, MINECRAFT_1_19_1, false),
map(0x1F, MINECRAFT_1_19_3, false),
map(0x23, MINECRAFT_1_19_4, false),
map(0x24, MINECRAFT_1_20_2, false));
clientbound.register(Disconnect.class, Disconnect::new,
map(0x24, MINECRAFT_1_20_2, false),
map(0x26, MINECRAFT_1_20_5, false));
clientbound.register(DisconnectPacket.class, DisconnectPacket::new,
map(0x40, MINECRAFT_1_7_2, true),
map(0x1A, MINECRAFT_1_9, true),
map(0x1B, MINECRAFT_1_13, true),
@@ -109,8 +122,9 @@ public enum FallbackPacketRegistry {
map(0x19, MINECRAFT_1_19_1, true),
map(0x17, MINECRAFT_1_19_3, true),
map(0x1A, MINECRAFT_1_19_4, true),
map(0x1B, MINECRAFT_1_20_2, true));
clientbound.register(PositionLook.class, PositionLook::new,
map(0x1B, MINECRAFT_1_20_2, true),
map(0x1D, MINECRAFT_1_20_5, true));
clientbound.register(PlayerPositionLookPacket.class, PlayerPositionLookPacket::new,
map(0x08, MINECRAFT_1_7_2, true),
map(0x2E, MINECRAFT_1_9, true),
map(0x2F, MINECRAFT_1_12_1, true),
@@ -124,8 +138,9 @@ public enum FallbackPacketRegistry {
map(0x39, MINECRAFT_1_19_1, true),
map(0x38, MINECRAFT_1_19_3, true),
map(0x3C, MINECRAFT_1_19_4, true),
map(0x3E, MINECRAFT_1_20_2, true));
clientbound.register(Abilities.class, Abilities::new,
map(0x3E, MINECRAFT_1_20_2, true),
map(0x40, MINECRAFT_1_20_5, true));
clientbound.register(ClientAbilitiesPacket.class, ClientAbilitiesPacket::new,
map(0x39, MINECRAFT_1_7_2, true),
map(0x2B, MINECRAFT_1_9, true),
map(0x2C, MINECRAFT_1_12_1, true),
@@ -139,8 +154,9 @@ public enum FallbackPacketRegistry {
map(0x31, MINECRAFT_1_19_1, true),
map(0x30, MINECRAFT_1_19_3, true),
map(0x34, MINECRAFT_1_19_4, true),
map(0x36, MINECRAFT_1_20_2, true));
clientbound.register(EmptyChunkData.class, EmptyChunkData::new,
map(0x36, MINECRAFT_1_20_2, true),
map(0x38, MINECRAFT_1_20_5, true));
clientbound.register(ChunkDataPacket.class, ChunkDataPacket::new,
map(0x21, MINECRAFT_1_7_2, true),
map(0x20, MINECRAFT_1_9, true),
map(0x22, MINECRAFT_1_13, true),
@@ -153,8 +169,46 @@ public enum FallbackPacketRegistry {
map(0x21, MINECRAFT_1_19_1, true),
map(0x20, MINECRAFT_1_19_3, true),
map(0x24, MINECRAFT_1_19_4, true),
map(0x25, MINECRAFT_1_20_2, true));
clientbound.register(UpdateSectionBlocks.class, UpdateSectionBlocks::new,
map(0x25, MINECRAFT_1_20_2, true),
map(0x27, MINECRAFT_1_20_5, true));
clientbound.register(RemoveEntitiesPacket.class, RemoveEntitiesPacket::new,
map(0x13, MINECRAFT_1_7_2, true),
map(0x30, MINECRAFT_1_9, true),
map(0x31, MINECRAFT_1_12, true),
map(0x32, MINECRAFT_1_12_2, true),
map(0x35, MINECRAFT_1_13, true),
map(0x37, MINECRAFT_1_14, true),
map(0x38, MINECRAFT_1_15, true),
map(0x37, MINECRAFT_1_16, true),
map(0x36, MINECRAFT_1_16_2, true),
map(0x3A, MINECRAFT_1_17, true),
map(0x38, MINECRAFT_1_19, true),
map(0x3B, MINECRAFT_1_19_1, true),
map(0x3A, MINECRAFT_1_19_3, true),
map(0x3E, MINECRAFT_1_19_4, true),
map(0x3E, MINECRAFT_1_19_4, true),
map(0x40, MINECRAFT_1_20_2, true),
map(0x42, MINECRAFT_1_20_5, true));
clientbound.register(SetPassengersPacket.class, SetPassengersPacket::new,
map(0x1B, MINECRAFT_1_7_2, true),
map(0x40, MINECRAFT_1_9, true),
map(0x42, MINECRAFT_1_12, true),
map(0x43, MINECRAFT_1_12_1, true),
map(0x46, MINECRAFT_1_13, true),
map(0x4A, MINECRAFT_1_14, true),
map(0x4B, MINECRAFT_1_15, true),
map(0x54, MINECRAFT_1_17, true),
map(0x57, MINECRAFT_1_19_1, true),
map(0x55, MINECRAFT_1_19_3, true),
map(0x59, MINECRAFT_1_19_4, true),
map(0x5B, MINECRAFT_1_20_2, true),
map(0x5D, MINECRAFT_1_20_3, true),
map(0x5F, MINECRAFT_1_20_5, true));
clientbound.register(SpawnEntityPacket.class, SpawnEntityPacket::new,
map(0x0E, MINECRAFT_1_7_2, true),
map(0x00, MINECRAFT_1_9, true),
map(0x01, MINECRAFT_1_19_4, true));
clientbound.register(UpdateSectionBlocksPacket.class, UpdateSectionBlocksPacket::new,
map(0x22, MINECRAFT_1_7_2, true),
map(0x10, MINECRAFT_1_9, true),
map(0x0F, MINECRAFT_1_13, true),
@@ -167,8 +221,9 @@ public enum FallbackPacketRegistry {
map(0x3F, MINECRAFT_1_19_3, true),
map(0x43, MINECRAFT_1_19_4, true),
map(0x45, MINECRAFT_1_20_2, true),
map(0x47, MINECRAFT_1_20_3, true));
clientbound.register(Transaction.class, Transaction::new,
map(0x47, MINECRAFT_1_20_3, true),
map(0x49, MINECRAFT_1_20_5, true));
clientbound.register(TransactionPacket.class, TransactionPacket::new,
map(0x32, MINECRAFT_1_7_2, true),
map(0x11, MINECRAFT_1_9, true),
map(0x12, MINECRAFT_1_13, true),
@@ -180,8 +235,9 @@ public enum FallbackPacketRegistry {
map(0x2F, MINECRAFT_1_19_1, true),
map(0x2E, MINECRAFT_1_19_3, true),
map(0x32, MINECRAFT_1_19_4, true),
map(0x33, MINECRAFT_1_20_2, true));
clientbound.register(DefaultSpawnPosition.class, DefaultSpawnPosition::new,
map(0x33, MINECRAFT_1_20_2, true),
map(0x35, MINECRAFT_1_20_5, true));
clientbound.register(DefaultSpawnPositionPacket.class, DefaultSpawnPositionPacket::new,
map(0x05, MINECRAFT_1_7_2, true),
map(0x43, MINECRAFT_1_9, true),
map(0x45, MINECRAFT_1_12, true),
@@ -196,8 +252,9 @@ public enum FallbackPacketRegistry {
map(0x4C, MINECRAFT_1_19_3, true),
map(0x50, MINECRAFT_1_19_4, true),
map(0x52, MINECRAFT_1_20_2, true),
map(0x54, MINECRAFT_1_20_3, true));
clientbound.register(MapData.class, MapData::new,
map(0x54, MINECRAFT_1_20_3, true),
map(0x56, MINECRAFT_1_20_5, true));
clientbound.register(MapDataPacket.class, MapDataPacket::new,
map(0x34, MINECRAFT_1_7_2, true),
map(0x24, MINECRAFT_1_9, true),
map(0x26, MINECRAFT_1_13, true),
@@ -209,8 +266,9 @@ public enum FallbackPacketRegistry {
map(0x26, MINECRAFT_1_19_1, true),
map(0x25, MINECRAFT_1_19_3, true),
map(0x29, MINECRAFT_1_19_4, true),
map(0x2A, MINECRAFT_1_20_2, true));
clientbound.register(SetSlot.class, SetSlot::new,
map(0x2A, MINECRAFT_1_20_2, true),
map(0x2C, MINECRAFT_1_20_5, true));
clientbound.register(SetSlotPacket.class, SetSlotPacket::new,
map(0x2F, MINECRAFT_1_7_2, true),
map(0x16, MINECRAFT_1_9, true),
map(0x17, MINECRAFT_1_13, true),
@@ -223,7 +281,22 @@ public enum FallbackPacketRegistry {
map(0x12, MINECRAFT_1_19_3, true),
map(0x14, MINECRAFT_1_19_4, true),
map(0x15, MINECRAFT_1_20_2, true));
clientbound.register(Chat.class, Chat::new,
clientbound.register(SetExperiencePacket.class, SetExperiencePacket::new,
map(0x1F, MINECRAFT_1_7_2, true),
map(0x3D, MINECRAFT_1_9, true),
map(0x3F, MINECRAFT_1_12, true),
map(0x40, MINECRAFT_1_12_1, true),
map(0x43, MINECRAFT_1_13, true),
map(0x47, MINECRAFT_1_14, true),
map(0x48, MINECRAFT_1_15, true),
map(0x51, MINECRAFT_1_17, true),
map(0x54, MINECRAFT_1_19_1, true),
map(0x52, MINECRAFT_1_19_3, true),
map(0x56, MINECRAFT_1_19_4, true),
map(0x58, MINECRAFT_1_20_2, true),
map(0x5A, MINECRAFT_1_20_3, true),
map(0x5C, MINECRAFT_1_20_5, true));
clientbound.register(UniversalChatPacket.class, UniversalChatPacket::new,
map(0x02, MINECRAFT_1_7_2, false),
map(0x0F, MINECRAFT_1_9, false),
map(0x0E, MINECRAFT_1_13, false),
@@ -235,11 +308,45 @@ public enum FallbackPacketRegistry {
map(0x60, MINECRAFT_1_19_3, false),
map(0x64, MINECRAFT_1_19_4, false),
map(0x67, MINECRAFT_1_20_2, false),
map(0x69, MINECRAFT_1_20_3, false));
clientbound.register(GameEvent.class, GameEvent::new,
map(0x20, MINECRAFT_1_20_3, true));
map(0x69, MINECRAFT_1_20_3, false),
map(0x6C, MINECRAFT_1_20_5, false));
clientbound.register(GameEventPacket.class, GameEventPacket::new,
map(0x20, MINECRAFT_1_20_3, true),
map(0x22, MINECRAFT_1_20_5, true));
clientbound.register(TransferPacket.class, TransferPacket::new,
map(0x73, MINECRAFT_1_20_5, true));
serverbound.register(Chat.class, Chat::new,
serverbound.register(PaddleBoatPacket.class, PaddleBoatPacket::new,
map(0x11, MINECRAFT_1_9, false),
map(0x12, MINECRAFT_1_12, false),
map(0x11, MINECRAFT_1_12_1, false),
map(0x14, MINECRAFT_1_13, false),
map(0x16, MINECRAFT_1_14, false),
map(0x17, MINECRAFT_1_16, false),
map(0x16, MINECRAFT_1_17, false),
map(0x18, MINECRAFT_1_19, false),
map(0x19, MINECRAFT_1_19_1, false),
map(0x18, MINECRAFT_1_19_3, false),
map(0x19, MINECRAFT_1_19_4, false),
map(0x1B, MINECRAFT_1_20_2, false),
map(0x1C, MINECRAFT_1_20_3, false),
map(0x1F, MINECRAFT_1_20_5, false));
serverbound.register(PlayerInputPacket.class, PlayerInputPacket::new,
map(0x0C, MINECRAFT_1_7_2, false),
map(0x15, MINECRAFT_1_9, false),
map(0x16, MINECRAFT_1_12, false),
map(0x1A, MINECRAFT_1_13, false),
map(0x1C, MINECRAFT_1_14, false),
map(0x1D, MINECRAFT_1_16, false),
map(0x1C, MINECRAFT_1_17, false),
map(0x1E, MINECRAFT_1_19, false),
map(0x1F, MINECRAFT_1_19_1, false),
map(0x1E, MINECRAFT_1_19_3, false),
map(0x1F, MINECRAFT_1_19_4, false),
map(0x22, MINECRAFT_1_20_2, false),
map(0x23, MINECRAFT_1_20_3, false),
map(0x26, MINECRAFT_1_20_5, false));
serverbound.register(UniversalChatPacket.class, UniversalChatPacket::new,
map(0x01, MINECRAFT_1_7_2, false),
map(0x02, MINECRAFT_1_9, false),
map(0x03, MINECRAFT_1_12, false),
@@ -247,8 +354,9 @@ public enum FallbackPacketRegistry {
map(0x03, MINECRAFT_1_14, false),
map(0x04, MINECRAFT_1_19, false),
map(0x05, MINECRAFT_1_19_1, false),
map(0x05, MINECRAFT_1_19_3, false));
serverbound.register(KeepAlive.class, KeepAlive::new,
map(0x05, MINECRAFT_1_19_3, false),
map(0x06, MINECRAFT_1_20_5, false));
serverbound.register(KeepAlivePacket.class, KeepAlivePacket::new,
map(0x00, MINECRAFT_1_7_2, false),
map(0x0B, MINECRAFT_1_9, false),
map(0x0C, MINECRAFT_1_12, false),
@@ -262,8 +370,9 @@ public enum FallbackPacketRegistry {
map(0x11, MINECRAFT_1_19_3, false),
map(0x12, MINECRAFT_1_19_4, false),
map(0x14, MINECRAFT_1_20_2, false),
map(0x15, MINECRAFT_1_20_3, false));
serverbound.register(ClientSettings.class, ClientSettings::new,
map(0x15, MINECRAFT_1_20_3, false),
map(0x18, MINECRAFT_1_20_5, false));
serverbound.register(ClientSettingsPacket.class, ClientSettingsPacket::new,
map(0x15, MINECRAFT_1_7_2, false),
map(0x04, MINECRAFT_1_9, false),
map(0x05, MINECRAFT_1_12, false),
@@ -273,8 +382,9 @@ public enum FallbackPacketRegistry {
map(0x08, MINECRAFT_1_19_1, false),
map(0x07, MINECRAFT_1_19_3, false),
map(0x08, MINECRAFT_1_19_4, false),
map(0x09, MINECRAFT_1_20_2, false));
serverbound.register(PluginMessage.class, PluginMessage::new,
map(0x09, MINECRAFT_1_20_2, false),
map(0x0A, MINECRAFT_1_20_5, false));
serverbound.register(PluginMessagePacket.class, PluginMessagePacket::new,
map(0x17, MINECRAFT_1_7_2, false),
map(0x09, MINECRAFT_1_9, false),
map(0x0A, MINECRAFT_1_12, false),
@@ -287,7 +397,7 @@ public enum FallbackPacketRegistry {
map(0x0C, MINECRAFT_1_19_3, false),
map(0x0D, MINECRAFT_1_19_4, false),
map(0x0F, MINECRAFT_1_20_2, false));
serverbound.register(PositionLook.class, PositionLook::new,
serverbound.register(PlayerPositionLookPacket.class, PlayerPositionLookPacket::new,
map(0x06, MINECRAFT_1_7_2, false),
map(0x0D, MINECRAFT_1_9, false),
map(0x0F, MINECRAFT_1_12, false),
@@ -301,10 +411,11 @@ public enum FallbackPacketRegistry {
map(0x14, MINECRAFT_1_19_3, false),
map(0x15, MINECRAFT_1_19_4, false),
map(0x17, MINECRAFT_1_20_2, false),
map(0x18, MINECRAFT_1_20_3, false));
serverbound.register(TeleportConfirm.class, TeleportConfirm::new,
map(0x18, MINECRAFT_1_20_3, false),
map(0x1B, MINECRAFT_1_20_5, false));
serverbound.register(TeleportConfirmPacket.class, TeleportConfirmPacket::new,
map(0x00, MINECRAFT_1_9, false));
serverbound.register(Position.class, Position::new,
serverbound.register(PlayerPositionPacket.class, PlayerPositionPacket::new,
map(0x04, MINECRAFT_1_7_2, false),
map(0x0C, MINECRAFT_1_9, false),
map(0x0E, MINECRAFT_1_12, false),
@@ -318,8 +429,9 @@ public enum FallbackPacketRegistry {
map(0x13, MINECRAFT_1_19_3, false),
map(0x14, MINECRAFT_1_19_4, false),
map(0x16, MINECRAFT_1_20_2, false),
map(0x17, MINECRAFT_1_20_3, false));
serverbound.register(Player.class, Player::new,
map(0x17, MINECRAFT_1_20_3, false),
map(0x1A, MINECRAFT_1_20_5, false));
serverbound.register(PlayerPositionIdlePacket.class, PlayerPositionIdlePacket::new,
map(0x03, MINECRAFT_1_7_2, false),
map(0x0F, MINECRAFT_1_9, false),
map(0x0D, MINECRAFT_1_12, false),
@@ -333,8 +445,9 @@ public enum FallbackPacketRegistry {
map(0x16, MINECRAFT_1_19_3, false),
map(0x17, MINECRAFT_1_19_4, false),
map(0x19, MINECRAFT_1_20_2, false),
map(0x1A, MINECRAFT_1_20_3, false));
serverbound.register(Transaction.class, Transaction::new,
map(0x1A, MINECRAFT_1_20_3, false),
map(0x1D, MINECRAFT_1_20_5, false));
serverbound.register(TransactionPacket.class, TransactionPacket::new,
map(0x0F, MINECRAFT_1_7_2, false),
map(0x05, MINECRAFT_1_9, false),
map(0x06, MINECRAFT_1_12, false),
@@ -347,7 +460,8 @@ public enum FallbackPacketRegistry {
map(0x1F, MINECRAFT_1_19_3, false),
map(0x20, MINECRAFT_1_19_4, false),
map(0x23, MINECRAFT_1_20_2, false),
map(0x24, MINECRAFT_1_20_3, false));
map(0x24, MINECRAFT_1_20_3, false),
map(0x27, MINECRAFT_1_20_5, false));
}
};
@@ -370,7 +484,7 @@ public enum FallbackPacketRegistry {
PacketRegistry() {
final Map<ProtocolVersion, ProtocolRegistry> mutableVersions = new EnumMap<>(ProtocolVersion.class);
for (final ProtocolVersion version : ProtocolVersion.values()) {
if (!version.isLegacy() && !version.isUnknown()) {
if (!version.isUnknown()) {
mutableVersions.put(version, new ProtocolRegistry(version));
}
}
@@ -0,0 +1,93 @@
/*
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.protocol;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import lombok.Getter;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import java.util.HashMap;
import java.util.Map;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.ID_TO_PROTOCOL_CONSTANT;
// Mostly taken from
// https://github.com/Nan1t/NanoLimbo/blob/main/src/main/java/ua/nanit/limbo/protocol/PacketSnapshot.java
@Getter
public final class FallbackPacketSnapshot implements FallbackPacket {
private final Map<Integer, Integer> mappings = new HashMap<>(ID_TO_PROTOCOL_CONSTANT.size());
private final Map<Integer, byte[]> cachedBytes = new HashMap<>(ID_TO_PROTOCOL_CONSTANT.size());
private final FallbackPacket originalPacket;
public FallbackPacketSnapshot(final @NotNull FallbackPacket originalPacket) {
final Map<Integer, Integer> hashes = new HashMap<>(ID_TO_PROTOCOL_CONSTANT.size());
for (final ProtocolVersion protocolVersion : ID_TO_PROTOCOL_CONSTANT.values()) {
// Allocate a buffer for each protocol version
final ByteBuf byteBuf = Unpooled.buffer();
try {
originalPacket.encode(byteBuf, protocolVersion);
} catch (Exception exception) {
Sonar.get().getLogger().error("Could not encode packet {} for version {}: {}",
originalPacket.toString(), protocolVersion, exception);
break;
}
// Make sure we don't unnecessarily fill the RAM
final int protocol = protocolVersion.getProtocol();
final int hash = byteBuf.hashCode();
final int hashed = hashes.getOrDefault(hash, -1);
if (hashed != -1) {
mappings.put(protocol, hashed);
} else {
hashes.put(hash, protocol);
mappings.put(protocol, protocol);
// Cache the raw bytes of the encoded packet
final byte[] bytes = new byte[byteBuf.readableBytes()];
byteBuf.readBytes(bytes);
cachedBytes.put(protocol, bytes);
}
// Make sure to release the buffer to avoid memory leaks
byteBuf.release();
}
this.originalPacket = originalPacket;
}
@Override
public void encode(final @NotNull ByteBuf byteBuf,
final @NotNull ProtocolVersion protocolVersion) throws Exception {
final int hash = mappings.get(protocolVersion.getProtocol());
final byte[] bytes = cachedBytes.get(hash);
if (bytes != null) {
byteBuf.writeBytes(bytes);
} else {
Sonar.get().getLogger().error("Could not find cached packet {} for version {}",
toString(), protocolVersion);
throw new IllegalStateException("Unable to find cached packet. Contact the developer!");
}
}
@Override
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) throws Exception {
throw new UnsupportedOperationException();
}
}
@@ -20,40 +20,64 @@ package xyz.jonesdev.sonar.common.fallback.protocol;
import lombok.experimental.UtilityClass;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.config.SonarConfiguration;
import xyz.jonesdev.sonar.common.fallback.protocol.block.BlockPosition;
import xyz.jonesdev.sonar.common.fallback.protocol.block.BlockType;
import xyz.jonesdev.sonar.common.fallback.protocol.block.ChangedBlock;
import xyz.jonesdev.sonar.common.fallback.protocol.map.MapInfoPreparer;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.config.FinishConfiguration;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.config.RegistryData;
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.login.LoginSuccessPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.*;
import java.util.UUID;
import java.util.concurrent.ThreadLocalRandom;
@UtilityClass
public class FallbackPreparer {
// LoginSuccess
public final FallbackPacket LOGIN_SUCCESS = new FallbackPacketSnapshot(new LoginSuccessPacket(new UUID(1L, 1L), "Sonar"));
// Abilities
public final FallbackPacket DEFAULT_ABILITIES = new Abilities(0x00, 0f, 0f);
public final FallbackPacket CAPTCHA_ABILITIES = new Abilities(0x02, 0f, 0f);
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));
// Chunks
public final FallbackPacket EMPTY_CHUNK_DATA = new EmptyChunkData(0, 0);
public final FallbackPacket EMPTY_CHUNK_DATA = new FallbackPacketSnapshot(new ChunkDataPacket(0, 0));
// Finish Configuration
public final FallbackPacket FINISH_CONFIGURATION = new FinishConfiguration();
public final FallbackPacket FINISH_CONFIGURATION = new FinishConfigurationPacket(); // No snapshot needed because it's empty either way
// Synchronize Registry
public final FallbackPacket REGISTRY_SYNC = new RegistryData();
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);
// Keep Alive
public final FallbackPacket CAPTCHA_KEEP_ALIVE = new KeepAlive(0L);
public final FallbackPacket CAPTCHA_KEEP_ALIVE = new FallbackPacketSnapshot(new KeepAlivePacket(-1337L));
// Game Event (1.20.3+)
public final FallbackPacket START_WRITING_CHUNKS = new GameEvent(13, 0);
public final FallbackPacket START_WRITING_CHUNKS = new FallbackPacketSnapshot(new GameEventPacket(13, 0));
// Chat
public FallbackPacket enterCodeMessage;
public FallbackPacket youAreBeingChecked;
public FallbackPacket incorrectCaptcha;
// JoinGame
public final int PLAYER_ENTITY_ID = ThreadLocalRandom.current().nextInt(10);
public FallbackPacket joinGame;
// Update Section Blocks
public FallbackPacket updateSectionBlocks;
// Default Spawn Position
public FallbackPacket dynamicSpawnPosition;
// Transfer packet
public FallbackPacket transferToOrigin;
// Disconnect messages
public FallbackPacket blacklisted;
public FallbackPacket alreadyQueued;
public FallbackPacket alreadyVerifying;
public FallbackPacket reconnectedTooFast;
public FallbackPacket protocolBlacklisted;
public FallbackPacket invalidUsername;
public FallbackPacket tooManyOnlinePerIP;
// Vehicle
public FallbackPacket removeEntities;
public FallbackPacket setPassengers;
public static final int VEHICLE_ENTITY_ID = PLAYER_ENTITY_ID + 1;
// Collisions
public final int BLOCKS_PER_ROW = 8; // 8 * 8 = 64 (protocol maximum)
@@ -62,31 +86,34 @@ 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 PositionLook(
SPAWN_X_POSITION, 1337, SPAWN_Z_POSITION, 0f, 90f, 0, false);
public final FallbackPacket CAPTCHA_SPAWN_POSITION = new DefaultSpawnPosition(
SPAWN_X_POSITION, 1337, SPAWN_Z_POSITION);
public final FallbackPacket CAPTCHA_POSITION = new FallbackPacketSnapshot(new PlayerPositionLookPacket(
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 ChangedBlock[] CHANGED_BLOCKS = new ChangedBlock[BLOCKS_PER_ROW * BLOCKS_PER_ROW];
private final BlockUpdate[] CHANGED_BLOCKS = new BlockUpdate[BLOCKS_PER_ROW * BLOCKS_PER_ROW];
public BlockType blockType = BlockType.BARRIER;
public int maxMovementTick, dynamicSpawnYPosition;
public double[] preparedCachedYMotions;
public double maxFallDistance;
public void prepare() {
joinGame = new JoinGame(0,
Sonar.get().getConfig().getVerification().getGravity().getGamemode().getId(),
0,
false,
0,
true,
true,
false,
new String[]{"minecraft:overworld"},
"minecraft:overworld");
// CAPTCHA
public final CaptchaPreparer MAP_INFO_PREPARER = new CaptchaPreparer();
// XP packets
public FallbackPacket[] xpCountdown;
public void prepare() {
// Prepare JoinGame packet
joinGame = new JoinGamePacket(PLAYER_ENTITY_ID,
Sonar.get().getConfig().getVerification().getGravity().getGamemode().getId(),
0, false, 0,
true, false, false,
new String[]{"minecraft:overworld"}, "minecraft:overworld");
// Prepare cached motions for the gravity check
maxFallDistance = 0;
maxMovementTick = Sonar.get().getConfig().getVerification().getGravity().getMaxMovementTicks();
preparedCachedYMotions = new double[maxMovementTick + 8];
@@ -99,35 +126,61 @@ 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 DefaultSpawnPosition(SPAWN_X_POSITION, dynamicSpawnYPosition, SPAWN_Z_POSITION);
dynamicSpawnPosition = new FallbackPacketSnapshot(new DefaultSpawnPositionPacket(
SPAWN_X_POSITION, dynamicSpawnYPosition, SPAWN_Z_POSITION));
// Prepare collision platform positions
int index = 0;
for (int x = 0; x < BLOCKS_PER_ROW; x++) {
for (int z = 0; z < BLOCKS_PER_ROW; z++) {
final BlockPosition position = new BlockPosition(
final BlockUpdate.BlockPosition position = new BlockUpdate.BlockPosition(
x + (BLOCKS_PER_ROW / 2),
DEFAULT_Y_COLLIDE_POSITION,
z + (BLOCKS_PER_ROW / 2),
0, 0);
CHANGED_BLOCKS[index++] = new ChangedBlock(position, blockType);
CHANGED_BLOCKS[index++] = new BlockUpdate(position, blockType);
}
}
// Prepare UpdateSectionBlocks packet
updateSectionBlocks = new UpdateSectionBlocks(0, 0, CHANGED_BLOCKS);
updateSectionBlocks = new FallbackPacketSnapshot(new UpdateSectionBlocksPacket(0, 0, CHANGED_BLOCKS));
// "You are being checked" message
if (Sonar.get().getConfig().getVerification().getGravity().isEnabled()) {
youAreBeingChecked = new Chat(Sonar.get().getConfig().getVerification().getGravity().getYouAreBeingChecked());
}
// Prepare disconnect packets during login
blacklisted = new FallbackPacketSnapshot(DisconnectPacket.create(Sonar.get().getConfig().getVerification().getBlacklisted(), true));
alreadyVerifying = new FallbackPacketSnapshot(DisconnectPacket.create(Sonar.get().getConfig().getVerification().getAlreadyVerifying(), true));
alreadyQueued = new FallbackPacketSnapshot(DisconnectPacket.create(Sonar.get().getConfig().getVerification().getAlreadyQueued(), true));
protocolBlacklisted = new FallbackPacketSnapshot(DisconnectPacket.create(Sonar.get().getConfig().getVerification().getProtocolBlacklisted(), true));
reconnectedTooFast = new FallbackPacketSnapshot(DisconnectPacket.create(Sonar.get().getConfig().getVerification().getTooFastReconnect(), true));
invalidUsername = new FallbackPacketSnapshot(DisconnectPacket.create(Sonar.get().getConfig().getVerification().getInvalidUsername(), true));
tooManyOnlinePerIP = new FallbackPacketSnapshot(DisconnectPacket.create(Sonar.get().getConfig().getTooManyOnlinePerIp(), true));
if (Sonar.get().getConfig().getVerification().getMap().getTiming() != SonarConfiguration.Verification.Timing.NEVER) {
enterCodeMessage = new Chat(Sonar.get().getConfig().getVerification().getMap().getEnterCode());
incorrectCaptcha = new Chat(Sonar.get().getConfig().getVerification().getMap().getFailedCaptcha());
// Prepare transfer packet
transferToOrigin = new FallbackPacketSnapshot(new TransferPacket(
Sonar.get().getConfig().getVerification().getTransfer().getHost(),
Sonar.get().getConfig().getVerification().getTransfer().getPort()));
// Precompute captcha answers
MapInfoPreparer.prepare();
// Prepare packets for the vehicle check
removeEntities = new FallbackPacketSnapshot(new RemoveEntitiesPacket(VEHICLE_ENTITY_ID));
setPassengers = new FallbackPacketSnapshot(new SetPassengersPacket(VEHICLE_ENTITY_ID, PLAYER_ENTITY_ID));
if (Sonar.get().getConfig().getVerification().getMap().getTiming() != SonarConfiguration.Verification.Timing.NEVER
|| Sonar.get().getConfig().getVerification().getGravity().isCaptchaOnFail()) {
// Prepare CAPTCHA messages
enterCodeMessage = new FallbackPacketSnapshot(new UniversalChatPacket(
Sonar.get().getConfig().getVerification().getMap().getEnterCode(), UniversalChatPacket.SYSTEM_TYPE));
incorrectCaptcha = new FallbackPacketSnapshot(new UniversalChatPacket(
Sonar.get().getConfig().getVerification().getMap().getFailedCaptcha(), UniversalChatPacket.SYSTEM_TYPE));
// Prepare countdown
xpCountdown = new FallbackPacket[Sonar.get().getConfig().getVerification().getMap().getMaxDuration() / 1000];
for (int i = 0; i < xpCountdown.length; i++) {
final float bar = (float) i / xpCountdown.length;
xpCountdown[i] = new FallbackPacketSnapshot(new SetExperiencePacket(bar, i, i));
}
// Prepare CAPTCHA answers
MAP_INFO_PREPARER.prepare();
}
}
}
@@ -78,6 +78,7 @@ public enum BlockType {
return 10225;
case MINECRAFT_1_20_2:
case MINECRAFT_1_20_3:
case MINECRAFT_1_20_5:
return 10366;
}
}, 1D);
@@ -18,15 +18,17 @@
package xyz.jonesdev.sonar.common.fallback.protocol.block;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
import org.jetbrains.annotations.NotNull;
@Getter
public final class ChangedBlock {
public final class BlockUpdate {
private final @NotNull BlockPosition position;
private final BlockType type;
private final int legacyBlockState, blockState;
public ChangedBlock(final @NotNull BlockPosition position, final BlockType type) {
public BlockUpdate(final @NotNull BlockPosition position, final BlockType type) {
this.position = position;
this.type = type;
final int x = position.getX() - (position.getChunkX() << 4);
@@ -35,4 +37,12 @@ public final class ChangedBlock {
this.legacyBlockState = x << 12 | z << 8 | y;
this.blockState = x << 8 | z << 4 | y - ((y >> 4) << 4);
}
@Getter
@ToString
@RequiredArgsConstructor
public static final class BlockPosition {
private final int x, y, z;
private final int chunkX, chunkZ;
}
}
@@ -0,0 +1,129 @@
/*
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.protocol.captcha;
import org.jetbrains.annotations.Nullable;
import xyz.jonesdev.capja.SimpleCaptchaGenerator;
import xyz.jonesdev.capja.filter.SimpleRippleFilter;
import xyz.jonesdev.capja.filter.SimpleScratchFilter;
import xyz.jonesdev.capja.libs.jhlabs.image.AbstractBufferedImageOp;
import xyz.jonesdev.capja.libs.jhlabs.image.BumpFilter;
import xyz.jonesdev.capja.libs.jhlabs.image.SmearFilter;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.config.SonarConfiguration;
import xyz.jonesdev.sonar.api.timer.SystemTimer;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadLocalRandom;
public final class CaptchaPreparer {
private static final ExecutorService PREPARATION_SERVICE = Executors.newSingleThreadExecutor();
private static final Random RANDOM = new Random();
private MapCaptchaInfo[] cached;
private int preparedAmount;
public void prepare() {
// Make sure we're not preparing when Sonar is already preparing answers
if (cached != null && preparedAmount != cached.length) return;
preparedAmount = 0;
final SystemTimer timer = new SystemTimer();
Sonar.get().getLogger().info("Asynchronously preparing CAPTCHA answers...");
Sonar.get().getLogger().info("Players will be able to join even if the preparation isn't finished");
// Prepare cache
final SonarConfiguration.Verification.Map config = Sonar.get().getConfig().getVerification().getMap();
cached = new MapCaptchaInfo[config.getPrecomputeAmount()];
// Prepare everything asynchronously
PREPARATION_SERVICE.execute(() -> {
// Create the images using capja
final @Nullable File backgroundImage = Sonar.get().getConfig().getVerification().getMap().getBackgroundImage();
final SimpleCaptchaGenerator generator = new SimpleCaptchaGenerator(128, 128, backgroundImage);
final char[] dictionary = config.getDictionary().toCharArray();
final SimpleScratchFilter scratchFilter = new SimpleScratchFilter(5);
final BumpFilter bumpFilter = new BumpFilter();
for (preparedAmount = 0; preparedAmount < config.getPrecomputeAmount(); preparedAmount++) {
// Prepare the filters per CAPTCHA for some more randomness
final List<AbstractBufferedImageOp> filters = new ArrayList<>();
if (Sonar.get().getConfig().getVerification().getMap().isScratches()) {
filters.add(scratchFilter);
}
if (Sonar.get().getConfig().getVerification().getMap().isRipple()) {
final SimpleRippleFilter rippleFilter = new SimpleRippleFilter();
rippleFilter.setXAmplitude(0);
float yAmplitude = 10 - ThreadLocalRandom.current().nextInt(20);
if (Math.abs(yAmplitude) < 3) {
yAmplitude = yAmplitude >= 0 ? 3 : -3;
}
rippleFilter.setYAmplitude(yAmplitude);
filters.add(rippleFilter);
}
if (Sonar.get().getConfig().getVerification().getMap().getDistortion().isEnabled()) {
final SmearFilter smearFilter = new SmearFilter();
smearFilter.setShape(Sonar.get().getConfig().getVerification().getMap().getDistortion().getShape());
smearFilter.setMix(Sonar.get().getConfig().getVerification().getMap().getDistortion().getMix());
smearFilter.setDensity(Sonar.get().getConfig().getVerification().getMap().getDistortion().getDensity());
smearFilter.setDistance(Sonar.get().getConfig().getVerification().getMap().getDistortion().getDistance());
filters.add(smearFilter);
}
if (Sonar.get().getConfig().getVerification().getMap().isBump()) {
filters.add(bumpFilter);
}
if (!Sonar.get().getConfig().getVerification().getMap().isAutoColor()) {
// Generate a random gradient color if automatic coloring is disabled
final Color color0 = Color.getHSBColor((float) Math.random(), 1, 1);
final Color color1 = Color.getHSBColor((float) Math.random(), 1, 0.5f);
final GradientPaint gradient = new GradientPaint(0, 0, color0,
generator.getWidth(), generator.getHeight(), color1);
generator.setGradient(gradient);
}
// Generate CAPTCHA answer
final char[] answer = new char[5];
for (int j = 0; j < answer.length; j++) {
answer[j] = dictionary[ThreadLocalRandom.current().nextInt(dictionary.length)];
}
final BufferedImage image = generator.createImage(answer, filters);
// Convert and cache converted Minecraft map bytes
final int[] buffer = MapColorPalette.getBufferFromImage(image);
cached[preparedAmount] = new MapCaptchaInfo(new String(answer), buffer);
}
Sonar.get().getLogger().info("Finished preparing {} CAPTCHA answers ({}s)!", preparedAmount, timer);
});
}
public boolean isCaptchaAvailable() {
return cached != null && cached[0] != null;
}
public MapCaptchaInfo getRandomCaptcha() {
// Give the player a random CAPTCHA out of the ones that we've already prepared
return cached[RANDOM.nextInt(preparedAmount)];
}
}
@@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.protocol.map;
package xyz.jonesdev.sonar.common.fallback.protocol.captcha;
import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.NotNull;
@@ -25,7 +25,7 @@ import java.util.function.Function;
@SuppressWarnings("unused")
@RequiredArgsConstructor
public enum ItemMapType {
public enum ItemType {
FILLED_MAP(protocolVersion -> {
// Useful resources:
// - https://github.com/PrismarineJS/minecraft-data/blob/master/data/pc/
@@ -72,6 +72,8 @@ public enum ItemMapType {
return 941;
case MINECRAFT_1_20_3:
return 979;
case MINECRAFT_1_20_5:
return 982;
}
});
@@ -15,51 +15,50 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.protocol.map;
package xyz.jonesdev.sonar.common.fallback.protocol.captcha;
import lombok.Getter;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.MapData;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketSnapshot;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.MapDataPacket;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_8;
@Getter
public final class PreparedMapInfo {
public static final int DIMENSIONS = (int) Math.pow(2, 7);
public static final int SCALE = DIMENSIONS * DIMENSIONS;
public final class MapCaptchaInfo {
private final String answer;
private final MapInfo info;
private final MapData[] legacy;
private final MapData modern;
private final FallbackPacket[] legacy;
private final FallbackPacket modern;
public PreparedMapInfo(final String answer,
final int columns, final int rows,
final byte @NotNull [] buffer) {
this.info = new MapInfo(answer, columns, rows, 0, 0, buffer);
public MapCaptchaInfo(final @NotNull String answer, final int @NotNull [] buffer) {
this.answer = answer;
// Prepare 1.7 map data using a grid
final byte[][] grid = new byte[DIMENSIONS][DIMENSIONS];
final int[][] grid = new int[128][128];
for (int i = 0; i < buffer.length; i++) {
final byte buf = buffer[i];
final int buf = buffer[i];
grid[i & Byte.MAX_VALUE][i >> 7] = buf;
}
this.legacy = new MapData[grid.length];
this.legacy = new FallbackPacket[grid.length];
for (int i = 0; i < grid.length; i++) {
this.legacy[i] = new MapData(new MapInfo(answer, DIMENSIONS, DIMENSIONS, i, 0, grid[i]));
this.legacy[i] = new FallbackPacketSnapshot(new MapDataPacket(grid[i], i, 0));
}
// Prepare 1.8+ map data
this.modern = new MapData(info);
this.modern = new FallbackPacketSnapshot(new MapDataPacket(buffer, 0, 0));
}
public void write(final @NotNull FallbackUser user) {
if (user.getProtocolVersion().compareTo(MINECRAFT_1_8) < 0) {
for (final MapData data : legacy) {
user.delayedWrite(data);
}
} else {
public void delayedWrite(final @NotNull FallbackUser user) {
if (user.getProtocolVersion().compareTo(MINECRAFT_1_8) >= 0) {
user.delayedWrite(modern);
} else {
// 1.7.2-1.7.10
for (final FallbackPacket legacyPacket : legacy) {
user.delayedWrite(legacyPacket);
}
}
}
}
@@ -0,0 +1,214 @@
/*
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.protocol.captcha;
import lombok.experimental.UtilityClass;
import org.jetbrains.annotations.NotNull;
import java.awt.image.BufferedImage;
@UtilityClass
public class MapColorPalette {
// https://minecraft.wiki/w/Map_item_format#Full_color_tables
public static final int[] COLOR_MAP = {
-16777216,
-16777216,
-16777216,
-16777216,
-10912473,
-9594576,
-8408520,
-12362211,
-5331853,
-2766452,
-530013,
-8225962,
-7566196,
-5526613,
-3684409,
-9868951,
-4980736,
-2359296,
-65536,
-7929856,
-9408332,
-7697700,
-6250241,
-11250553,
-9079435,
-7303024,
-5789785,
-10987432,
-16754944,
-16750080,
-16745472,
-16760576,
-4934476,
-2302756,
-1,
-7895161,
-9210239,
-7499618,
-5986120,
-11118495,
-9810890,
-8233406,
-6853299,
-11585240,
-11579569,
-10461088,
-9408400,
-12895429,
-13816396,
-13158436,
-12566273,
-14605945,
-10202062,
-8690114,
-7375032,
-11845850,
-4935252,
-2303533,
-779,
-7895679,
-6792924,
-4559572,
-2588877,
-9288933,
-8571496,
-6733382,
-5092136,
-10606478,
-12030824,
-10976070,
-10053160,
-13217422,
-6184668,
-3816148,
-1710797,
-8816357,
-10907631,
-9588715,
-8401895,
-12358643,
-5613196,
-3117682,
-884827,
-8371369,
-13290187,
-12500671,
-11776948,
-14145496,
-9671572,
-8092540,
-6710887,
-11447983,
-13280916,
-12489340,
-11763815,
-14138543,
-10933123,
-9619815,
-8437838,
-12377762,
-14404227,
-13876839,
-13415246,
-14997410,
-12045020,
-10993364,
-10073037,
-13228005,
-12035804,
-10982100,
-10059981,
-13221093,
-9690076,
-8115156,
-6737101,
-11461861,
-15658735,
-15395563,
-15132391,
-15921907,
-5199818,
-2634430,
-332211,
-8094168,
-12543338,
-11551561,
-10691627,
-13601936,
-13346124,
-12620068,
-11894529,
-14204025,
-16738008,
-16729294,
-16721606,
-16748002,
-10798046,
-9483734,
-8301007,
-12309223,
-11599616,
-10485504,
-9436672,
-12910336,
};
public int[] getBufferFromImage(final @NotNull BufferedImage bufferedImage) {
final int[] buffer = new int[128 * 128];
bufferedImage.getRGB(0, 0,
bufferedImage.getWidth(), bufferedImage.getHeight(),
buffer, 0, bufferedImage.getWidth());
for (int i = 0; i < buffer.length; i++) {
buffer[i] = mapColorToMinecraft(buffer[i]);
}
return buffer;
}
private byte mapColorToMinecraft(final int argb) {
int minDistance = Integer.MAX_VALUE;
byte closestIndex = 0;
for (int i = 0; i < COLOR_MAP.length; i++) {
final byte index = (byte) (i < 128 ? i : -129 + (i - 127));
final int distance = calculateColorDistance(argb, COLOR_MAP[i]);
if (distance < minDistance) {
minDistance = distance;
closestIndex = index;
}
}
return closestIndex;
}
private int calculateColorDistance(final int argb0, final int argb1) {
final int red0 = (argb0 >> 16) & 0xFF;
final int green0 = (argb0 >> 8) & 0xFF;
final int blue0 = argb0 & 0xFF;
final int red1 = (argb1 >> 16) & 0xFF;
final int green1 = (argb1 >> 8) & 0xFF;
final int blue1 = argb1 & 0xFF;
// Color distance metric (e.g. squared Euclidean distance)
return (red0 - red1) * (red0 - red1)
+ (green0 - green1) * (green0 - green1)
+ (blue0 - blue1) * (blue0 - blue1);
}
}
@@ -0,0 +1,90 @@
/*
* 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.entity;
import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import java.util.function.Function;
@RequiredArgsConstructor
public enum EntityType {
BOAT(protocolVersion -> {
// Useful resources:
// - https://github.com/PrismarineJS/minecraft-data/blob/master/data/pc/
// - https://pokechu22.github.io/Burger/1.20.4.html
switch (protocolVersion) {
default:
case MINECRAFT_1_7_2:
case MINECRAFT_1_7_6:
case MINECRAFT_1_8:
case MINECRAFT_1_9:
case MINECRAFT_1_9_1:
case MINECRAFT_1_9_2:
case MINECRAFT_1_9_4:
case MINECRAFT_1_10:
case MINECRAFT_1_11:
case MINECRAFT_1_11_1:
case MINECRAFT_1_12:
case MINECRAFT_1_12_1:
case MINECRAFT_1_12_2:
case MINECRAFT_1_13:
case MINECRAFT_1_13_1:
case MINECRAFT_1_13_2:
return 1;
case MINECRAFT_1_14:
case MINECRAFT_1_14_1:
case MINECRAFT_1_14_2:
case MINECRAFT_1_14_3:
case MINECRAFT_1_14_4:
return 5;
case MINECRAFT_1_15:
case MINECRAFT_1_15_1:
case MINECRAFT_1_15_2:
case MINECRAFT_1_16:
case MINECRAFT_1_16_1:
case MINECRAFT_1_16_2:
case MINECRAFT_1_16_3:
case MINECRAFT_1_16_4:
return 6;
case MINECRAFT_1_17:
case MINECRAFT_1_17_1:
case MINECRAFT_1_18:
case MINECRAFT_1_18_2:
return 7;
case MINECRAFT_1_19:
case MINECRAFT_1_19_1:
case MINECRAFT_1_19_3:
return 8;
case MINECRAFT_1_19_4:
case MINECRAFT_1_20:
case MINECRAFT_1_20_2:
case MINECRAFT_1_20_3:
return 9;
case MINECRAFT_1_20_5:
return 10;
}
});
private final Function<ProtocolVersion, Integer> function;
public int getId(final @NotNull ProtocolVersion protocolVersion) {
return function.apply(protocolVersion);
}
}
@@ -1,30 +0,0 @@
/*
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.protocol.map;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@Getter
@RequiredArgsConstructor
public final class MapInfo {
private final String answer;
private final int columns, rows;
private final int x, y;
private final byte[] buffer;
}
@@ -1,248 +0,0 @@
/*
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.protocol.map;
import lombok.Getter;
import lombok.experimental.UtilityClass;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.config.SonarConfiguration;
import xyz.jonesdev.sonar.api.timer.SystemTimer;
import java.awt.*;
import java.awt.font.FontRenderContext;
import java.awt.font.GlyphVector;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import static java.awt.Font.*;
import static xyz.jonesdev.sonar.common.fallback.protocol.map.PreparedMapInfo.DIMENSIONS;
@UtilityClass
public class MapInfoPreparer {
private final Random RANDOM = new Random();
private final ExecutorService PREPARATION_SERVICE = Executors.newSingleThreadExecutor();
private final int[] FONT_STYLES = new int[]{
PLAIN,
BOLD,
ITALIC,
ITALIC | BOLD
};
private final int[][] COLOR_PALETTE = new int[][]{
new int[]{ // Blue
48,
49,
50,
51
},
new int[]{ // Orange
60,
61,
62,
63,
},
new int[]{ // Green
4,
28,
29,
30,
},
new int[]{ // Black
27,
45,
46,
47,
},
new int[]{ // Red
16,
17,
18,
19
}
};
private PreparedMapInfo[] cached;
@Getter
private int preparedCAPTCHAs;
private boolean currentlyPreparing;
public PreparedMapInfo getRandomCaptcha() {
return cached[RANDOM.nextInt(preparedCAPTCHAs)];
}
public void prepare() {
final SystemTimer timer = new SystemTimer();
Sonar.get().getLogger().info("Precomputing map captcha answers...");
// Make sure we're not running into concurrency issues
if (currentlyPreparing) {
Sonar.get().getLogger().warn("Did not precompute map captcha answers as another task is running.");
return;
}
// Reset all statistics
preparedCAPTCHAs = 0;
currentlyPreparing = true;
final SonarConfiguration.Verification.Map config = Sonar.get().getConfig().getVerification().getMap();
cached = null;
cached = new PreparedMapInfo[config.getPrecomputeAmount()];
// Prepare fonts from config
final String dictionary = config.getDictionary();
final List<String> fonts = config.getFonts();
if (fonts.isEmpty()) {
Sonar.get().getLogger().warn("No fonts found, using fallback font...");
fonts.add(DIALOG);
}
final String[] fontTypes = fonts.toArray(new String[0]);
// Prepare distortions values from config
final double distortionsFactorX = config.getDistortionsFactorX();
final double distortionsFactorY = config.getDistortionsFactorY();
final double halfDistortionsFactorX = distortionsFactorX / 2D;
final double halfDistortionsFactorY = distortionsFactorY / 2D;
for (int _i = 0; _i < cached.length; _i++) {
final int currentIndex = _i;
final byte[] buffer = new byte[PreparedMapInfo.SCALE];
PREPARATION_SERVICE.execute(() -> {
// Create image
final BufferedImage image = new BufferedImage(DIMENSIONS, DIMENSIONS, BufferedImage.TYPE_3BYTE_BGR);
final Graphics2D graphics = (Graphics2D) image.getGraphics();
try {
// Create random font
final String fontType = fontTypes[RANDOM.nextInt(fontTypes.length)];
final int fontStyle = FONT_STYLES[RANDOM.nextInt(FONT_STYLES.length)];
final int fontSize = 27
+ (config.isRandomizeFontSize()
? RANDOM.nextInt(6) - 2 : 5);
@SuppressWarnings("all") final Font answerFont = new Font(fontType, fontStyle, fontSize);
graphics.setFont(answerFont);
// Build answer to the captcha
final StringBuilder answerBuilder = new StringBuilder();
for (int _j = 0; _j < 5; _j++) {
answerBuilder.append(dictionary.charAt(RANDOM.nextInt(dictionary.length())));
}
final String answer = answerBuilder.toString();
// Calculate text position
final int stringWidth = graphics.getFontMetrics().stringWidth(answer);
final int halfWidth = image.getWidth() / 2;
final int halfHeight = image.getHeight() / 2;
final int spacing = 5;
double _x = halfWidth - stringWidth / 2f - spacing;
double _y = halfHeight + fontSize / 3f;
// Draw each character one by one
final FontRenderContext fontRenderContext = graphics.getFontRenderContext();
for (final char c : answer.toCharArray()) {
// Randomize x and y
if (config.isRandomizePositions()) {
_y += RANDOM.nextInt(8) - 4;
}
// Apply random distortion
final double distortionFactorX = RANDOM.nextDouble() * distortionsFactorX - halfDistortionsFactorX;
final double distortionFactorY = RANDOM.nextDouble() * distortionsFactorY - halfDistortionsFactorY;
// Create a GlyphVector for the character
final String character = String.valueOf(c);
final GlyphVector glyphVector = graphics.getFont().createGlyphVector(fontRenderContext, character);
// Apply the distortion to the GlyphVector using AffineTransform
final AffineTransform transform = AffineTransform.getTranslateInstance(_x, _y);
transform.shear(distortionFactorX, distortionFactorY);
final Shape distortedCharacter = glyphVector.getOutline();
final Shape distorted = transform.createTransformedShape(distortedCharacter);
// Draw the distorted character
graphics.fill(distorted);
// Update x by width
_x += glyphVector.getVisualBounds().getWidth() + spacing;
}
// TODO: Color converter
graphics.setColor(Color.RED);
// Draw random lines
for (int i = 0; i < config.getRandomLinesAmount(); i++) {
final int startX = RANDOM.nextInt(halfWidth);
final int startY = RANDOM.nextInt(halfHeight);
final int endX = halfWidth + RANDOM.nextInt(halfWidth);
final int endY = halfHeight + RANDOM.nextInt(halfHeight);
graphics.drawLine(startX, startY, endX, endY);
}
// Draw random ovals
for (int i = 0; i < config.getRandomOvalsAmount(); i++) {
final int startX = RANDOM.nextInt(halfWidth);
final int startY = RANDOM.nextInt(halfHeight);
final int endX = halfWidth + RANDOM.nextInt(halfWidth);
final int endY = halfHeight + RANDOM.nextInt(halfHeight);
graphics.drawOval(startX, startY, endX, endY);
}
// Select random color palette
final int[] colorPalette = COLOR_PALETTE[currentIndex % COLOR_PALETTE.length];
// Select the next color palette for geometry
final int[] nextColorPalette = COLOR_PALETTE[(currentIndex + 1) % COLOR_PALETTE.length];
// Clear background
Arrays.fill(buffer, (byte) 57);
// Color every pixel individually
final int spacingY = halfHeight - fontSize;
for (int x = spacing; x < image.getWidth() - spacing; x++) {
for (int y = spacingY; y < image.getHeight() - spacingY; y++) {
final int pixel = image.getRGB(x, y);
final int index = y * image.getWidth() + x;
if (pixel == -16777216 && RANDOM.nextInt(100) < 97) continue;
// Set color of pixel to random color from the palette
final byte color = (byte) (pixel == -16777216 ? RANDOM.nextInt(100) < 75 ? 14 :
RANDOM.nextInt(Byte.MAX_VALUE)
: pixel != -65536 ? colorPalette[RANDOM.nextInt(colorPalette.length)]
: nextColorPalette[RANDOM.nextInt(nextColorPalette.length)]);
// Write pixel color to buffer
buffer[index] = color;
}
}
// Cache buffer to map
cached[currentIndex] = new PreparedMapInfo(answer, image.getWidth(), image.getHeight(), buffer);
} finally {
// Dispose graphics
graphics.dispose();
// Finished?
if (currentIndex == cached.length - 1) {
Sonar.get().getLogger().info("Successfully precomputed map captcha answers in {}s!", timer);
currentlyPreparing = false;
}
preparedCAPTCHAs++;
}
});
}
}
}
@@ -1,136 +0,0 @@
/*
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.protocol.netty;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufHolder;
import io.netty.util.IllegalReferenceCountException;
import lombok.AllArgsConstructor;
import lombok.ToString;
// Taken from
// https://github.com/PaperMC/Velocity/blob/dev/3.0.0/proxy/src/main/java/com/velocitypowered/proxy/protocol/util/DeferredByteBufHolder.java
@ToString
@AllArgsConstructor
public class DeferredByteBufHolder implements ByteBufHolder {
private ByteBuf backing;
@Override
public ByteBuf content() {
if (backing == null) {
throw new IllegalStateException("Trying to obtain contents of holder with a null buffer");
}
if (backing.refCnt() <= 0) {
throw new IllegalReferenceCountException(backing.refCnt());
}
return backing;
}
@Override
public ByteBufHolder copy() {
if (backing == null) {
throw new IllegalStateException("Trying to obtain contents of holder with a null buffer");
}
return new DeferredByteBufHolder(backing.copy());
}
@Override
public ByteBufHolder duplicate() {
if (backing == null) {
throw new IllegalStateException("Trying to obtain contents of holder with a null buffer");
}
return new DeferredByteBufHolder(backing.duplicate());
}
@Override
public ByteBufHolder retainedDuplicate() {
if (backing == null) {
throw new IllegalStateException("Trying to obtain contents of holder with a null buffer");
}
return new DeferredByteBufHolder(backing.retainedDuplicate());
}
@Override
public ByteBufHolder replace(final ByteBuf content) {
if (content == null) {
throw new NullPointerException("content");
}
backing = content;
return this;
}
@Override
public int refCnt() {
if (backing == null) {
throw new IllegalStateException("Trying to obtain contents of holder with a null buffer");
}
return backing.refCnt();
}
@Override
public ByteBufHolder retain() {
if (backing == null) {
throw new IllegalStateException("Trying to obtain contents of holder with a null buffer");
}
backing.retain();
return this;
}
@Override
public ByteBufHolder retain(final int increment) {
if (backing == null) {
throw new IllegalStateException("Trying to obtain contents of holder with a null buffer");
}
backing.retain(increment);
return this;
}
@Override
public ByteBufHolder touch() {
if (backing == null) {
throw new IllegalStateException("Trying to obtain contents of holder with a null buffer");
}
backing.touch();
return this;
}
@Override
public ByteBufHolder touch(final Object hint) {
if (backing == null) {
throw new IllegalStateException("Trying to obtain contents of holder with a null buffer");
}
backing.touch(hint);
return this;
}
@Override
public boolean release() {
if (backing == null) {
throw new IllegalStateException("Trying to obtain contents of holder with a null buffer");
}
return backing.release();
}
@Override
public boolean release(final int decrement) {
if (backing == null) {
throw new IllegalStateException("Trying to obtain contents of holder with a null buffer");
}
return backing.release(decrement);
}
}
@@ -23,7 +23,7 @@ import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
@ToString
public final class FinishConfiguration implements FallbackPacket {
public final class FinishConfigurationPacket implements FallbackPacket {
@Override
public void encode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
@@ -1,43 +0,0 @@
/*
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.protocol.packets.config;
import io.netty.buffer.ByteBuf;
import lombok.Getter;
import lombok.ToString;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.dimension.DimensionRegistry;
import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.writeNamelessCompoundTag;
@Getter
@ToString
public final class RegistryData implements FallbackPacket {
@Override
public void encode(final @NotNull ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
writeNamelessCompoundTag(byteBuf, DimensionRegistry.CODEC_1_20);
}
@Override
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
throw new UnsupportedOperationException();
}
}
@@ -0,0 +1,91 @@
/*
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.protocol.packets.config;
import io.netty.buffer.ByteBuf;
import lombok.*;
import net.kyori.adventure.nbt.BinaryTag;
import net.kyori.adventure.nbt.CompoundBinaryTag;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketSnapshot;
import java.util.ArrayList;
import java.util.List;
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.*;
@Getter
@NoArgsConstructor
@AllArgsConstructor
public final class RegistryDataPacket implements FallbackPacket {
private CompoundBinaryTag tag;
private String type;
private List<RegistryDataPacket.Bundle> bundles;
@Override
public void encode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_20_5) < 0) {
writeNamelessCompoundTag(byteBuf, tag);
} else if (type != null) {
writeString(byteBuf, type);
writeVarInt(byteBuf, bundles.size());
for (final RegistryDataPacket.Bundle bundle : bundles) {
writeString(byteBuf, bundle.getName());
// Write the bundle tag
final CompoundBinaryTag tag = bundle.getTag();
if (tag != null) {
byteBuf.writeBoolean(true);
writeNamelessCompoundTag(byteBuf, tag);
} else {
byteBuf.writeBoolean(false);
}
}
}
}
@Override
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
throw new UnsupportedOperationException();
}
public static FallbackPacket @NotNull [] of(final @NotNull CompoundBinaryTag rootTag) {
final FallbackPacket[] packets = new FallbackPacket[rootTag.size()];
int index = 0;
for (final String type : rootTag.keySet()) {
final ArrayList<RegistryDataPacket.Bundle> bundles = new ArrayList<>();
for (final BinaryTag binaryTag : rootTag.getCompound(type).getList("value")) {
final CompoundBinaryTag tag = (CompoundBinaryTag) binaryTag;
bundles.add(new Bundle(tag.getString("name"), tag.getCompound("element")));
}
packets[index++] = new FallbackPacketSnapshot(new RegistryDataPacket(rootTag, type, bundles));
}
return packets;
}
@Data
@Value
public static class Bundle {
@NotNull String name;
// In the protocol, it is able to be null
@Nullable CompoundBinaryTag tag;
}
}
@@ -21,13 +21,20 @@ import io.netty.buffer.ByteBuf;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
public final class LoginAcknowledged implements FallbackPacket {
public final class LoginAcknowledgedPacket implements FallbackPacket {
@Override
public void encode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
throw new UnsupportedOperationException();
}
@Override
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
}
@Override
public int expectedMaxLength(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
// https://github.com/PaperMC/Velocity/blob/dev/3.0.0/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/LoginAcknowledgedPacket.java#L41
return 0;
}
}
@@ -21,23 +21,24 @@ import io.netty.buffer.ByteBuf;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.netty.FastUUID;
import xyz.jonesdev.sonar.common.util.FastUuidSansHyphens;
import java.util.UUID;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.*;
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.writeVarInt;
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.*;
@Getter
@ToString
@NoArgsConstructor
@AllArgsConstructor
public final class LoginSuccess implements FallbackPacket {
private String username;
public final class LoginSuccessPacket implements FallbackPacket {
private UUID uuid;
private String username;
@Override
public void encode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
@@ -48,7 +49,7 @@ public final class LoginSuccess implements FallbackPacket {
} else if (protocolVersion.compareTo(MINECRAFT_1_7_6) >= 0) {
writeString(byteBuf, uuid.toString());
} else {
writeString(byteBuf, FastUUID.toString(uuid));
writeString(byteBuf, FastUuidSansHyphens.toString(uuid));
}
writeString(byteBuf, username);
@@ -56,6 +57,9 @@ public final class LoginSuccess implements FallbackPacket {
if (protocolVersion.compareTo(MINECRAFT_1_19) >= 0) {
writeVarInt(byteBuf, 0); // properties
}
if (protocolVersion.compareTo(MINECRAFT_1_20_5) >= 0) {
byteBuf.writeBoolean(false); // should authenticate
}
}
@Override
@@ -29,14 +29,13 @@ import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.*;
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.writeVarInt;
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.*;
@Getter
@ToString
@NoArgsConstructor
@AllArgsConstructor
public final class EmptyChunkData implements FallbackPacket {
public final class ChunkDataPacket implements FallbackPacket {
private int x, z;
private static final byte[] SECTION_BYTES = new byte[]{0, 0, 0, 0, 0, 0, 1, 0};
@@ -30,7 +30,7 @@ import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class Abilities implements FallbackPacket {
public final class ClientAbilitiesPacket implements FallbackPacket {
private int encodedFlags;
private float flySpeed, walkSpeed;
@@ -43,8 +43,6 @@ public class Abilities implements FallbackPacket {
@Override
public void decode(final @NotNull ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
encodedFlags = byteBuf.readByte();
flySpeed = byteBuf.readFloat();
walkSpeed = byteBuf.readFloat();
throw new UnsupportedOperationException();
}
}
@@ -27,32 +27,27 @@ import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.readString;
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.readVarInt;
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.readString;
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.readVarInt;
@Getter
@ToString
@NoArgsConstructor
@AllArgsConstructor
public final class ClientSettings implements FallbackPacket {
public final class ClientSettingsPacket implements FallbackPacket {
private String locale;
private byte viewDistance;
private int chatVisibility;
private boolean chatColors;
private byte difficulty; // 1.7 Protocol
private byte difficulty; // 1.7
private short skinParts;
private int mainHand;
private boolean chatFilteringEnabled; // Added in 1.17
private boolean clientListingAllowed; // Added in 1.18, overwrites server-list "anonymous" mode
@Override
public int expectedMinLength(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
return 2;
}
@Override
public int expectedMaxLength(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
return 0xff + 1; // 256 as a hard-limit
public void encode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
throw new UnsupportedOperationException();
}
@Override
@@ -82,7 +77,12 @@ public final class ClientSettings implements FallbackPacket {
}
@Override
public void encode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
throw new UnsupportedOperationException();
public int expectedMinLength(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
return 2;
}
@Override
public int expectedMaxLength(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
return 0xff + 1; // 256 as a hard-limit
}
}
@@ -33,7 +33,7 @@ import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT
@ToString
@NoArgsConstructor
@AllArgsConstructor
public final class DefaultSpawnPosition implements FallbackPacket {
public final class DefaultSpawnPositionPacket implements FallbackPacket {
private int x, y, z;
@Override
@@ -1,65 +0,0 @@
/*
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.protocol.packets.play;
import com.google.gson.JsonParser;
import io.netty.buffer.ByteBuf;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
import net.kyori.adventure.nbt.BinaryTag;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.json.JSONComponentSerializer;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
import static xyz.jonesdev.sonar.common.utility.component.ComponentSerializer.serialize;
import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.writeNamelessCompoundTag;
import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.writeString;
@Getter
@ToString
@NoArgsConstructor
@AllArgsConstructor
public final class Disconnect implements FallbackPacket {
private @Nullable String reason;
private BinaryTag binaryTag;
@Override
public void encode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_20_3) >= 0) {
writeNamelessCompoundTag(byteBuf, binaryTag);
} else {
writeString(byteBuf, reason);
}
}
@Override
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
throw new UnsupportedOperationException();
}
public static @NotNull Disconnect create(final Component component) {
final String serialized = JSONComponentSerializer.json().serialize(component);
final BinaryTag binaryTag = serialize(new JsonParser().parse(serialized));
return new Disconnect(serialized, binaryTag);
}
}
@@ -0,0 +1,53 @@
/*
* 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 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;
@Getter
@ToString
@NoArgsConstructor
@AllArgsConstructor
public final class DisconnectPacket implements FallbackPacket {
private @NotNull ComponentHolder componentHolder;
private boolean login;
@Override
public void encode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
componentHolder.write(byteBuf, login ? ProtocolVersion.MINECRAFT_1_20_2 : protocolVersion);
}
@Override
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
throw new UnsupportedOperationException();
}
public static @NotNull DisconnectPacket create(final @NotNull Component component,
final boolean login) {
return new DisconnectPacket(new ComponentHolder(component), login);
}
}
@@ -28,7 +28,7 @@ import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
@Data
@NoArgsConstructor
@AllArgsConstructor
public final class GameEvent implements FallbackPacket {
public final class GameEventPacket implements FallbackPacket {
private int type;
private float value;
@@ -27,15 +27,14 @@ import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
import static xyz.jonesdev.sonar.common.fallback.protocol.dimension.DimensionRegistry.*;
import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.*;
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.writeVarInt;
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.*;
// Mostly taken from
// https://github.com/Nan1t/NanoLimbo/blob/main/src/main/java/ua/nanit/limbo/protocol/packets/play/PacketJoinGame.java
@Data
@NoArgsConstructor
@AllArgsConstructor
public final class JoinGame implements FallbackPacket {
public final class JoinGamePacket implements FallbackPacket {
private int entityId;
private int gamemode;
private long partialHashedSeed;
@@ -57,6 +56,7 @@ public final class JoinGame implements FallbackPacket {
byteBuf.writeByte(0); // difficulty
byteBuf.writeByte(0); // max players
writeString(byteBuf, "flat"); // level type
return;
}
if (protocolVersion.inBetween(MINECRAFT_1_8, MINECRAFT_1_9)) {
@@ -66,6 +66,7 @@ public final class JoinGame implements FallbackPacket {
byteBuf.writeByte(0); // max players
writeString(byteBuf, "flat"); // level type
byteBuf.writeBoolean(reducedDebugInfo);
return;
}
if (protocolVersion.inBetween(MINECRAFT_1_9_1, MINECRAFT_1_13_2)) {
@@ -75,6 +76,7 @@ public final class JoinGame implements FallbackPacket {
byteBuf.writeByte(0); // max players
writeString(byteBuf, "flat"); // level type
byteBuf.writeBoolean(reducedDebugInfo);
return;
}
if (protocolVersion.inBetween(MINECRAFT_1_14, MINECRAFT_1_14_4)) {
@@ -84,6 +86,7 @@ public final class JoinGame implements FallbackPacket {
writeString(byteBuf, "flat"); // level type
writeVarInt(byteBuf, viewDistance);
byteBuf.writeBoolean(reducedDebugInfo);
return;
}
if (protocolVersion.inBetween(MINECRAFT_1_15, MINECRAFT_1_15_2)) {
@@ -95,6 +98,7 @@ public final class JoinGame implements FallbackPacket {
writeVarInt(byteBuf, viewDistance);
byteBuf.writeBoolean(reducedDebugInfo);
byteBuf.writeBoolean(showRespawnScreen);
return;
}
if (protocolVersion.inBetween(MINECRAFT_1_16, MINECRAFT_1_16_1)) {
@@ -111,6 +115,7 @@ public final class JoinGame implements FallbackPacket {
byteBuf.writeBoolean(showRespawnScreen);
byteBuf.writeBoolean(false); // debug type
byteBuf.writeBoolean(false); // flat
return;
}
if (protocolVersion.inBetween(MINECRAFT_1_16_2, MINECRAFT_1_17_1)) {
@@ -128,6 +133,7 @@ public final class JoinGame implements FallbackPacket {
byteBuf.writeBoolean(showRespawnScreen);
byteBuf.writeBoolean(false); // debug type
byteBuf.writeBoolean(false); // flat
return;
}
if (protocolVersion.inBetween(MINECRAFT_1_18, MINECRAFT_1_18_2)) {
@@ -151,6 +157,7 @@ public final class JoinGame implements FallbackPacket {
byteBuf.writeBoolean(showRespawnScreen);
byteBuf.writeBoolean(false); // debug type
byteBuf.writeBoolean(false); // flat
return;
}
if (protocolVersion.inBetween(MINECRAFT_1_19, MINECRAFT_1_19_4)) {
@@ -178,6 +185,7 @@ public final class JoinGame implements FallbackPacket {
byteBuf.writeBoolean(false); // debug type
byteBuf.writeBoolean(false); // flat
byteBuf.writeBoolean(false); // no last death location
return;
}
if (protocolVersion.equals(MINECRAFT_1_20)) {
@@ -198,9 +206,10 @@ public final class JoinGame implements FallbackPacket {
byteBuf.writeBoolean(false); // flat
byteBuf.writeBoolean(false); // no last death location
writeVarInt(byteBuf, 0);
return;
}
if (protocolVersion.compareTo(MINECRAFT_1_20_2) >= 0) {
if (protocolVersion.inBetween(MINECRAFT_1_20_2, MINECRAFT_1_20_3)) {
byteBuf.writeBoolean(isHardcore);
writeStringArray(byteBuf, levelNames);
writeVarInt(byteBuf, 0); // max players
@@ -218,6 +227,29 @@ public final class JoinGame implements FallbackPacket {
byteBuf.writeBoolean(false); // flat
byteBuf.writeBoolean(false); // no last death location
writeVarInt(byteBuf, 0);
return;
}
if (protocolVersion.compareTo(MINECRAFT_1_20_5) >= 0) {
byteBuf.writeBoolean(isHardcore);
writeStringArray(byteBuf, levelNames);
writeVarInt(byteBuf, 0); // max players
writeVarInt(byteBuf, viewDistance);
writeVarInt(byteBuf, viewDistance); // simulation distance
byteBuf.writeBoolean(reducedDebugInfo);
byteBuf.writeBoolean(showRespawnScreen);
byteBuf.writeBoolean(false); // limited crafting
// It depends on the dimension data we send in RegistryData. If we only send one dimension. It should be 0.
writeVarInt(byteBuf, 0);
writeString(byteBuf, levelName);
byteBuf.writeLong(partialHashedSeed);
byteBuf.writeByte(gamemode);
byteBuf.writeByte(-1); // previous gamemode
byteBuf.writeBoolean(false); // debug type
byteBuf.writeBoolean(false); // flat
byteBuf.writeBoolean(false); // no last death location
writeVarInt(byteBuf, 0); // pearl cooldown
byteBuf.writeBoolean(false); // secure profile
}
}
@@ -28,14 +28,14 @@ import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_12_2;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_8;
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.readVarInt;
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.writeVarInt;
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.readVarInt;
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.writeVarInt;
@Getter
@ToString
@NoArgsConstructor
@AllArgsConstructor
public final class KeepAlive implements FallbackPacket {
public final class KeepAlivePacket implements FallbackPacket {
private long id;
@Override
@@ -21,32 +21,29 @@ import io.netty.buffer.ByteBuf;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.map.MapInfo;
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.writeVarInt;
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.writeVarInt;
@Getter
@ToString
@NoArgsConstructor
@AllArgsConstructor
public final class MapData implements FallbackPacket {
private MapInfo mapInfo;
public final class MapDataPacket implements FallbackPacket {
private int[] buffer;
private int x, y;
@Override
public void encode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
writeVarInt(byteBuf, 0);
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_8) < 0) {
byteBuf.writeShort(mapInfo.getBuffer().length + 3);
byteBuf.writeShort(buffer.length + 3);
byteBuf.writeByte(0); // scaling
byteBuf.writeByte(mapInfo.getX());
byteBuf.writeByte(mapInfo.getY());
byteBuf.writeBytes(mapInfo.getBuffer());
byteBuf.writeByte(x);
byteBuf.writeByte(y);
writeArray(byteBuf, buffer);
} else {
byteBuf.writeByte(0); // scaling
@@ -65,13 +62,19 @@ public final class MapData implements FallbackPacket {
writeVarInt(byteBuf, 0);
}
byteBuf.writeByte(mapInfo.getColumns());
byteBuf.writeByte(mapInfo.getRows());
byteBuf.writeByte(mapInfo.getX());
byteBuf.writeByte(mapInfo.getY());
byteBuf.writeByte(128); // rows
byteBuf.writeByte(128); // columns
byteBuf.writeByte(x);
byteBuf.writeByte(y);
writeVarInt(byteBuf, mapInfo.getBuffer().length);
byteBuf.writeBytes(mapInfo.getBuffer());
writeVarInt(byteBuf, buffer.length);
writeArray(byteBuf, buffer);
}
}
private static void writeArray(final ByteBuf byteBuf, final int @NotNull [] array) {
for (final int i : array) {
byteBuf.writeByte(i);
}
}
@@ -0,0 +1,46 @@
/*
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.protocol.packets.play;
import io.netty.buffer.ByteBuf;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
@Getter
@ToString
@NoArgsConstructor
@AllArgsConstructor
public final class PaddleBoatPacket implements FallbackPacket {
private boolean leftPaddle, rightPaddle;
@Override
public void encode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
throw new UnsupportedOperationException();
}
@Override
public void decode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
leftPaddle = byteBuf.readBoolean();
rightPaddle = byteBuf.readBoolean();
}
}
@@ -0,0 +1,55 @@
/*
* 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 PlayerInputPacket implements FallbackPacket {
private float sideways, forward;
private boolean jump, unmount;
@Override
public void encode(final @NotNull ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
throw new UnsupportedOperationException();
}
@Override
public void decode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
sideways = byteBuf.readFloat();
forward = byteBuf.readFloat();
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_8) < 0) {
jump = byteBuf.readBoolean();
unmount = byteBuf.readBoolean();
} else {
final byte flags = byteBuf.readByte();
jump = (flags & 0x01) != 0;
unmount = (flags & 0x02) != 0;
}
}
}
@@ -30,16 +30,16 @@ import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
@ToString
@NoArgsConstructor
@AllArgsConstructor
public final class Player implements FallbackPacket {
public final class PlayerPositionIdlePacket implements FallbackPacket {
private boolean onGround;
@Override
public void decode(final @NotNull ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
onGround = byteBuf.readBoolean();
}
@Override
public void encode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
throw new UnsupportedOperationException();
}
@Override
public void decode(final @NotNull ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
onGround = byteBuf.readBoolean();
}
}
@@ -27,31 +27,18 @@ import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.writeVarInt;
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.writeVarInt;
@Getter
@ToString
@NoArgsConstructor
@AllArgsConstructor
public final class PositionLook implements FallbackPacket {
public final class PlayerPositionLookPacket implements FallbackPacket {
private double x, y, z;
private float yaw, pitch;
private int teleportId;
private boolean onGround;
@Override
public void decode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
x = byteBuf.readDouble();
y = byteBuf.readDouble();
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_8) < 0) {
y = byteBuf.readDouble(); // Account for weird y positions on 1.7
}
z = byteBuf.readDouble();
yaw = byteBuf.readFloat();
pitch = byteBuf.readFloat();
onGround = byteBuf.readBoolean();
}
@Override
public void encode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
byteBuf.writeDouble(x);
@@ -77,7 +64,20 @@ public final class PositionLook implements FallbackPacket {
}
@Override
public int expectedMaxLength(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
public void decode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
x = byteBuf.readDouble();
y = byteBuf.readDouble();
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_8) < 0) {
y = byteBuf.readDouble(); // Account for weird y positions on 1.7
}
z = byteBuf.readDouble();
yaw = byteBuf.readFloat();
pitch = byteBuf.readFloat();
onGround = byteBuf.readBoolean();
}
@Override
public int expectedMaxLength(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
return protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_8) < 0 ? 41 : 33;
}
@@ -30,10 +30,15 @@ import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
@ToString
@NoArgsConstructor
@AllArgsConstructor
public final class Position implements FallbackPacket {
public final class PlayerPositionPacket implements FallbackPacket {
private double x, y, z;
private boolean onGround;
@Override
public void encode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
throw new UnsupportedOperationException();
}
@Override
public void decode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
x = byteBuf.readDouble();
@@ -45,11 +50,6 @@ public final class Position implements FallbackPacket {
onGround = byteBuf.readBoolean();
}
@Override
public void encode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
throw new UnsupportedOperationException();
}
@Override
public int expectedMaxLength(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
return protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_8) < 0 ? 33 : 25;
@@ -23,50 +23,44 @@ import lombok.ToString;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.netty.DeferredByteBufHolder;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_13;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_8;
import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.*;
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.*;
// Mostly taken from
// https://github.com/PaperMC/Velocity/blob/dev/3.0.0/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/PluginMessage.java
@Getter
@ToString
public final class PluginMessage extends DeferredByteBufHolder implements FallbackPacket {
public final class PluginMessagePacket implements FallbackPacket {
private String channel;
public PluginMessage() {
super(null);
}
private ByteBuf slicedBuffer;
@Override
public int expectedMinLength(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
return 8;
}
@Override
public int expectedMaxLength(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
return 0xfff + 1; // limit packet size to 4096
public void encode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
throw new UnsupportedOperationException();
}
@Override
public void decode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
channel = readString(byteBuf);
channel = readString(byteBuf, 512);
if (protocolVersion.compareTo(MINECRAFT_1_13) >= 0) {
channel = transformLegacyToModernChannel(channel);
}
if (protocolVersion.compareTo(MINECRAFT_1_8) >= 0) {
replace(byteBuf.readRetainedSlice(byteBuf.readableBytes()));
slicedBuffer = byteBuf.readRetainedSlice(byteBuf.readableBytes());
} else {
replace(readRetainedByteBufSlice17(byteBuf));
slicedBuffer = readRetainedByteBufSlice17(byteBuf);
}
}
@Override
public void encode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
throw new UnsupportedOperationException();
public int expectedMinLength(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
return 4;
}
@Override
public int expectedMaxLength(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
return 0xFFF; // strict size limit
}
}
@@ -0,0 +1,56 @@
/*
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.protocol.packets.play;
import io.netty.buffer.ByteBuf;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.writeVarInt;
@Getter
@ToString
@NoArgsConstructor
@AllArgsConstructor
public final class RemoveEntitiesPacket implements FallbackPacket {
private int entityId; // We only need one entityId
@Override
public void encode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
// No idea why Mojang decided that this is a good idea, but whatever
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_17) != 0) {
writeVarInt(byteBuf, 1); // size
}
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_8) < 0) {
byteBuf.writeInt(entityId);
} else {
writeVarInt(byteBuf, entityId);
}
}
@Override
public void decode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
throw new UnsupportedOperationException();
}
}
@@ -0,0 +1,56 @@
/*
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.protocol.packets.play;
import io.netty.buffer.ByteBuf;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.writeVarInt;
@Getter
@ToString
@NoArgsConstructor
@AllArgsConstructor
public final class SetExperiencePacket implements FallbackPacket {
private float experienceBar;
private int level;
private int totalExperience;
@Override
public void encode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
byteBuf.writeFloat(experienceBar);
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_8) < 0) {
byteBuf.writeShort(level);
byteBuf.writeShort(totalExperience);
} else {
writeVarInt(byteBuf, level);
writeVarInt(byteBuf, totalExperience);
}
}
@Override
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
throw new UnsupportedOperationException();
}
}
@@ -0,0 +1,58 @@
/*
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.protocol.packets.play;
import io.netty.buffer.ByteBuf;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.writeVarInt;
@Getter
@ToString
@NoArgsConstructor
@AllArgsConstructor
public final class SetPassengersPacket implements FallbackPacket {
private int entityId, passengerId;
@Override
public void encode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
// You can find this in the EntityAttach packet,
// which was later replaced by SetPassengers in 1.9+
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_9) < 0) {
byteBuf.writeInt(passengerId);
byteBuf.writeInt(entityId);
byteBuf.writeByte(0); // leash
return;
}
writeVarInt(byteBuf, entityId);
writeVarInt(byteBuf, 1); // passenger count
writeVarInt(byteBuf, passengerId);
}
@Override
public void decode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
throw new UnsupportedOperationException();
}
}
@@ -29,34 +29,23 @@ import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.writeCompoundTag;
import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.writeNamelessCompoundTag;
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.writeVarInt;
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.*;
@Getter
@ToString
@NoArgsConstructor
@AllArgsConstructor
public final class SetSlot implements FallbackPacket {
private int windowId;
private int slot;
private int count;
private int data;
private int itemId;
public final class SetSlotPacket implements FallbackPacket {
private int slot, count, itemId;
private CompoundBinaryTag compoundBinaryTag;
public static final CompoundBinaryTag MAP_NBT = CompoundBinaryTag.builder()
.put("map", IntBinaryTag.intBinaryTag(0)) // map type
.build();
@Override
public void decode(final @NotNull ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
throw new UnsupportedOperationException();
}
@Override
public void encode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
byteBuf.writeByte(windowId);
byteBuf.writeByte(0); // windowId
if (protocolVersion.compareTo(MINECRAFT_1_17_1) >= 0) {
writeVarInt(byteBuf, 0);
@@ -64,32 +53,53 @@ public final class SetSlot implements FallbackPacket {
byteBuf.writeShort(slot);
if (protocolVersion.compareTo(MINECRAFT_1_13_2) >= 0) {
if (protocolVersion.inBetween(MINECRAFT_1_13_2, MINECRAFT_1_20_3)) {
byteBuf.writeBoolean(true);
}
if (protocolVersion.compareTo(MINECRAFT_1_20_5) >= 0) {
writeVarInt(byteBuf, count);
}
if (protocolVersion.compareTo(MINECRAFT_1_13_2) < 0) {
byteBuf.writeShort(itemId);
} else {
writeVarInt(byteBuf, itemId);
}
byteBuf.writeByte(count);
if (protocolVersion.compareTo(MINECRAFT_1_20_5) < 0) {
byteBuf.writeByte(count);
}
if (protocolVersion.compareTo(MINECRAFT_1_13) < 0) {
byteBuf.writeShort(data);
byteBuf.writeShort(0); // data
}
if (protocolVersion.compareTo(MINECRAFT_1_17) < 0) {
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_8) < 0) {
if (protocolVersion.compareTo(MINECRAFT_1_8) < 0) {
byteBuf.writeShort(-1);
} else {
byteBuf.writeByte(0);
}
} else {
if (protocolVersion.compareTo(MINECRAFT_1_20_2) >= 0) {
writeNamelessCompoundTag(byteBuf, compoundBinaryTag);
} else {
if (protocolVersion.compareTo(MINECRAFT_1_20_2) < 0) {
writeCompoundTag(byteBuf, compoundBinaryTag);
} else if (protocolVersion.compareTo(MINECRAFT_1_20_5) < 0) {
writeNamelessCompoundTag(byteBuf, compoundBinaryTag);
} else { // 1.20.5
// TODO: find a way to improve this
// component
writeVarInt(byteBuf, 1); // component count to add
writeVarInt(byteBuf, 0); // component count to remove
// single VarInt component
writeVarInt(byteBuf, 26); // map component
writeVarInt(byteBuf, 0); // map id
}
}
}
@Override
public void decode(final @NotNull ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
throw new UnsupportedOperationException();
}
}
@@ -0,0 +1,93 @@
/*
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.protocol.packets.play;
import io.netty.buffer.ByteBuf;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.writeUUID;
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.writeVarInt;
@Getter
@ToString
@NoArgsConstructor
@AllArgsConstructor
public final class SpawnEntityPacket implements FallbackPacket {
private int entityId, entityType;
private double x, y, z;
@Override
public void encode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
writeVarInt(byteBuf, entityId);
final boolean v1_9orHigher = protocolVersion.compareTo(MINECRAFT_1_8) > 0;
if (v1_9orHigher) {
writeUUID(byteBuf, EMPTY_UUID);
}
if (protocolVersion.compareTo(MINECRAFT_1_14) >= 0) {
writeVarInt(byteBuf, entityType);
} else {
byteBuf.writeByte(entityType);
}
if (v1_9orHigher) {
byteBuf.writeDouble(x);
byteBuf.writeDouble(y);
byteBuf.writeDouble(z);
} else {
byteBuf.writeInt(floor(x * 32D));
byteBuf.writeInt(floor(y * 32D));
byteBuf.writeInt(floor(z * 32D));
}
byteBuf.writeByte(0); // pitch or yaw
byteBuf.writeByte(0); // yaw or pitch
if (protocolVersion.compareTo(MINECRAFT_1_19) >= 0) {
byteBuf.writeByte(0); // head yaw
writeVarInt(byteBuf, 0); // data
} else {
byteBuf.writeInt(0); // data
}
if (v1_9orHigher) {
byteBuf.writeShort(0); // velocity X
byteBuf.writeShort(0); // velocity Y
byteBuf.writeShort(0); // velocity Z
}
}
private static int floor(final double value) {
final int __value = (int) value;
return value < (double) __value ? __value - 1 : __value;
}
@Override
public void decode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
throw new UnsupportedOperationException();
}
}
@@ -25,13 +25,13 @@ import lombok.ToString;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.readVarInt;
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.readVarInt;
@Getter
@ToString
@NoArgsConstructor
@AllArgsConstructor
public final class TeleportConfirm implements FallbackPacket {
public final class TeleportConfirmPacket implements FallbackPacket {
private int teleportId;
@Override
@@ -32,34 +32,34 @@ import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT
@ToString
@NoArgsConstructor
@AllArgsConstructor
public final class Transaction implements FallbackPacket {
private int windowId, id;
public final class TransactionPacket implements FallbackPacket {
private int windowId, transactionId;
private boolean accepted;
@Override
public void encode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
if (protocolVersion.compareTo(MINECRAFT_1_17) < 0) {
byteBuf.writeByte(windowId);
byteBuf.writeShort((short) id);
// The "accepted" field is actually really unnecessary since
// it's never even used in the client.
byteBuf.writeBoolean(accepted);
} else {
byteBuf.writeInt(id);
}
}
@Override
public void decode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
if (protocolVersion.compareTo(MINECRAFT_1_17) < 0) {
windowId = byteBuf.readByte();
id = byteBuf.readShort();
transactionId = byteBuf.readShort();
accepted = byteBuf.readBoolean();
} else {
id = byteBuf.readInt();
transactionId = byteBuf.readInt();
// Always set accepted to true since 1.17 or higher don't use
// transactions for inventory confirmation anymore.
accepted = true;
}
}
@Override
public void encode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
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,50 @@
/*
* Copyright (C) 2024 jones
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.protocol.packets.play;
import io.netty.buffer.ByteBuf;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.writeString;
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.writeVarInt;
@Getter
@ToString
@NoArgsConstructor
@AllArgsConstructor
public final class TransferPacket implements FallbackPacket {
private String destinationHost;
private int destinationPort;
@Override
public void encode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
writeString(byteBuf, destinationHost);
writeVarInt(byteBuf, destinationPort);
}
@Override
public void decode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
throw new UnsupportedOperationException();
}
}
@@ -17,77 +17,48 @@
package xyz.jonesdev.sonar.common.fallback.protocol.packets.play;
import com.google.gson.JsonParser;
import io.netty.buffer.ByteBuf;
import io.netty.handler.codec.CorruptedFrameException;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import net.kyori.adventure.nbt.BinaryTag;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.json.JSONComponentSerializer;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
import java.time.Instant;
import java.util.UUID;
import xyz.jonesdev.sonar.common.util.ComponentHolder;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
import static xyz.jonesdev.sonar.common.utility.component.ComponentSerializer.serialize;
import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.*;
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.readVarInt;
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.writeVarInt;
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.*;
@Data
@NoArgsConstructor
@AllArgsConstructor
public final class Chat implements FallbackPacket {
private static final UUID PLACEHOLDER_UUID = new UUID(0L, 0L);
public final class UniversalChatPacket 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 Component component;
private ComponentHolder componentHolder;
private String message;
private byte type;
private boolean signedPreview;
private boolean unsigned = false;
private Instant timestamp;
private Instant expiry;
private long salt;
private boolean signed;
private byte[] signature;
// 1.20.3
private BinaryTag nbtMessage;
// Clientbound LegacyChat
public Chat(final Component component) {
this(component, SYSTEM_TYPE);
public UniversalChatPacket(final @NotNull Component component, final byte type) {
this(new ComponentHolder(component), type);
}
// Clientbound LegacyChat
public Chat(final Component component, final byte type) {
this(component, JSONComponentSerializer.json().serialize(component), type);
}
public Chat(final Component component, final String message, final byte type) {
this(component, message, type,
false, false, null, null,
0L, false, null, serialize(new JsonParser().parse(message)));
public UniversalChatPacket(final ComponentHolder componentHolder, final byte type) {
this.componentHolder = componentHolder;
this.type = type;
}
@Override
public void encode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
// Serialized message
if (protocolVersion.compareTo(MINECRAFT_1_20_3) >= 0) {
writeNamelessCompoundTag(byteBuf, nbtMessage);
} else {
writeString(byteBuf, message);
}
componentHolder.write(byteBuf, protocolVersion);
// Type
if (protocolVersion.compareTo(MINECRAFT_1_19_1) >= 0) {
@@ -101,7 +72,7 @@ public final class Chat implements FallbackPacket {
// Sender
if (protocolVersion.compareTo(MINECRAFT_1_16) >= 0
&& protocolVersion.compareTo(MINECRAFT_1_19) < 0) {
writeUUID(byteBuf, PLACEHOLDER_UUID);
writeUUID(byteBuf, EMPTY_UUID);
}
}
@@ -109,28 +80,28 @@ public final class Chat implements FallbackPacket {
public void decode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
message = readString(byteBuf, 256);
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_19) >= 0) {
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_19_1) <= 0) {
final long expiresAt = byteBuf.readLong();
if (protocolVersion.compareTo(MINECRAFT_1_19) >= 0) {
if (protocolVersion.compareTo(MINECRAFT_1_19_1) <= 0) {
byteBuf.readLong(); // expiresAt
final long saltLong = byteBuf.readLong();
final byte[] signatureBytes = readByteArray(byteBuf);
boolean unsigned = false;
if (saltLong != 0L && signatureBytes.length > 0) {
signature = signatureBytes;
expiry = Instant.ofEpochMilli(expiresAt);
} else if ((protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_19_1) >= 0
} else if ((protocolVersion.compareTo(MINECRAFT_1_19_1) >= 0
|| saltLong == 0L) && signatureBytes.length == 0) {
unsigned = true;
} else {
throw new CorruptedFrameException("Invalid signature");
}
signedPreview = byteBuf.readBoolean();
final boolean signedPreview = byteBuf.readBoolean();
if (signedPreview && unsigned) {
throw new CorruptedFrameException("Signature missing");
}
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_19_1) >= 0) {
if (protocolVersion.compareTo(MINECRAFT_1_19_1) >= 0) {
final int size = readVarInt(byteBuf);
if (size < 0 || size > 5) {
throw new CorruptedFrameException("Invalid previous messages");
@@ -147,11 +118,12 @@ public final class Chat implements FallbackPacket {
}
}
} else {
timestamp = Instant.ofEpochMilli(byteBuf.readLong());
salt = byteBuf.readLong();
signed = byteBuf.readBoolean();
byteBuf.readLong(); // timestamp
byteBuf.readLong(); // salt
final boolean signed = byteBuf.readBoolean();
if (signed) {
byte[] sign = new byte[256];
final byte[] sign = new byte[256];
byteBuf.readBytes(sign);
signature = sign;
} else {
@@ -159,7 +131,7 @@ public final class Chat implements FallbackPacket {
}
readVarInt(byteBuf);
byte[] bytes = new byte[DIV_FLOOR];
final byte[] bytes = new byte[DIV_FLOOR];
byteBuf.readBytes(bytes);
}
}
@@ -25,19 +25,19 @@ import lombok.ToString;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.block.ChangedBlock;
import xyz.jonesdev.sonar.common.fallback.protocol.block.BlockUpdate;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.writeVarInt;
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.writeVarLong;
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.writeVarInt;
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.writeVarLong;
@Getter
@NoArgsConstructor
@AllArgsConstructor
@ToString(exclude = {"changedBlocks"})
public final class UpdateSectionBlocks implements FallbackPacket {
@ToString(exclude = {"blockUpdates"})
public final class UpdateSectionBlocksPacket implements FallbackPacket {
private int sectionX, sectionZ;
private ChangedBlock[] changedBlocks;
private BlockUpdate[] blockUpdates;
@Override
public void encode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) throws Exception {
@@ -46,13 +46,13 @@ public final class UpdateSectionBlocks implements FallbackPacket {
byteBuf.writeInt(sectionZ);
if (protocolVersion.compareTo(MINECRAFT_1_8) < 0) {
byteBuf.writeShort(changedBlocks.length);
byteBuf.writeInt(4 * changedBlocks.length);
byteBuf.writeShort(blockUpdates.length);
byteBuf.writeInt(4 * blockUpdates.length);
} else {
writeVarInt(byteBuf, changedBlocks.length);
writeVarInt(byteBuf, blockUpdates.length);
}
for (final ChangedBlock block : changedBlocks) {
for (final BlockUpdate block : blockUpdates) {
byteBuf.writeShort(block.getLegacyBlockState());
final int blockId = block.getType().getId(protocolVersion);
if (protocolVersion.compareTo(MINECRAFT_1_13) >= 0) {
@@ -68,7 +68,7 @@ public final class UpdateSectionBlocks implements FallbackPacket {
}
} else {
// We only need one Y position
final int sectionY = changedBlocks[0].getPosition().getY() >> 4;
final int sectionY = blockUpdates[0].getPosition().getY() >> 4;
// https://wiki.vg/Protocol#Update_Section_Blocks
byteBuf.writeLong(((sectionX & 0x3FFFFFL) << 42) | (sectionY & 0xFFFFF) | ((sectionZ & 0x3FFFFFL) << 20));
@@ -78,9 +78,9 @@ public final class UpdateSectionBlocks implements FallbackPacket {
byteBuf.writeBoolean(true); // suppress light updates
}
writeVarInt(byteBuf, changedBlocks.length);
writeVarInt(byteBuf, blockUpdates.length);
for (final ChangedBlock block : changedBlocks) {
for (final BlockUpdate block : blockUpdates) {
// https://wiki.vg/Protocol#Update_Section_Blocks
final int shiftedBlockId = block.getType().getId(protocolVersion) << 12;
final long positionIdValue = shiftedBlockId | block.getBlockState();
@@ -15,19 +15,28 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.bungee.command;
package xyz.jonesdev.sonar.common.service;
import net.md_5.bungee.api.CommandSender;
import net.md_5.bungee.api.connection.ProxiedPlayer;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.command.InvocationSource;
import xyz.jonesdev.sonar.bungee.SonarBungee;
public final class SonarServiceThread extends Thread {
private final long delay;
private final Runnable runnable;
public final class BungeeInvocationSource extends InvocationSource {
public BungeeInvocationSource(final @NotNull CommandSender sender) {
super(sender.getName(),
SonarBungee.INSTANCE.getBungeeAudiences().sender(sender),
sender instanceof ProxiedPlayer,
sender::hasPermission);
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) {
//
}
}
}
@@ -0,0 +1,57 @@
/*
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.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();
}
}
@@ -15,17 +15,17 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.api.statistics;
package xyz.jonesdev.sonar.common.statistics;
import lombok.Getter;
import lombok.Setter;
import xyz.jonesdev.sonar.api.profiler.JVMProfiler;
import xyz.jonesdev.sonar.api.jvm.JVMProcessInformation;
/**
* This counts all incoming and outgoing traffic.
*/
@Getter
public enum Bandwidth implements JVMProfiler {
public enum CachedBandwidthStatistics {
INCOMING,
OUTGOING;
@@ -47,16 +47,15 @@ public enum Bandwidth implements JVMProfiler {
* (per-second) value and resets the current value.
* This method is called every second.
*/
public static synchronized void reset() {
for (final Bandwidth value : values()) {
value.cacheAndReset();
}
public static void reset() {
INCOMING.cacheAndReset();
OUTGOING.cacheAndReset();
}
private void cacheAndReset() {
ttl += curr; // increment total
cachedSecond = formatMemory(curr);
cachedTtl = formatMemory(ttl);
cachedSecond = JVMProcessInformation.formatMemory(curr);
cachedTtl = JVMProcessInformation.formatMemory(ttl);
curr = 0L; // reset current
}
}
@@ -0,0 +1,162 @@
/*
* 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.statistics;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.jetbrains.annotations.ApiStatus;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.statistics.SonarStatistics;
import java.time.Duration;
import java.util.concurrent.atomic.AtomicInteger;
public final class GlobalSonarStatistics implements SonarStatistics {
private static final Cache<Integer, Byte> LOGINS_PER_SECOND = Caffeine.newBuilder()
.expireAfterWrite(Duration.ofSeconds(1))
.build();
private static final Cache<Integer, Byte> CONNECTIONS_PER_SECOND = Caffeine.newBuilder()
.expireAfterWrite(Duration.ofSeconds(1))
.build();
private static final AtomicInteger ACTION_COUNTER = new AtomicInteger(Integer.MIN_VALUE);
/**
* Helper methods that make it easier to count new statistics
*/
@ApiStatus.Internal
public static void countConnection() {
CONNECTIONS_PER_SECOND.put(ACTION_COUNTER.getAndIncrement(), (byte) 0);
}
@ApiStatus.Internal
public static void countLogin() {
LOGINS_PER_SECOND.put(ACTION_COUNTER.getAndIncrement(), (byte) 0);
totalJoinedPlayers++;
}
// Cache all per-session statistics
private static int totalJoinedPlayers;
public static int totalSuccessfulVerifications;
public static int totalFailedVerifications;
public static int totalAttemptedVerifications;
public static long totalBlacklistedPlayers;
/**
* Called every time the action bar updates to ensure fresh values
*/
@Override
public void cleanUpCache() {
LOGINS_PER_SECOND.cleanUp();
CONNECTIONS_PER_SECOND.cleanUp();
// Also clean up the blacklist
Sonar.get().getFallback().getBlacklist().cleanUp();
}
@Override
public long getConnectionsPerSecond() {
return CONNECTIONS_PER_SECOND.estimatedSize();
}
@Override
public long getLoginsPerSecond() {
return LOGINS_PER_SECOND.estimatedSize();
}
@Override
public long getCurrentIncomingBandwidth() {
return CachedBandwidthStatistics.INCOMING.getCurr();
}
@Override
public long getCurrentOutgoingBandwidth() {
return CachedBandwidthStatistics.OUTGOING.getCurr();
}
@Override
public long getTotalIncomingBandwidth() {
return CachedBandwidthStatistics.INCOMING.getTtl();
}
@Override
public long getTotalOutgoingBandwidth() {
return CachedBandwidthStatistics.OUTGOING.getTtl();
}
@Override
public String getCurrentIncomingBandwidthFormatted() {
return CachedBandwidthStatistics.INCOMING.getCachedSecond();
}
@Override
public String getCurrentOutgoingBandwidthFormatted() {
return CachedBandwidthStatistics.OUTGOING.getCachedSecond();
}
@Override
public String getTotalIncomingBandwidthFormatted() {
return CachedBandwidthStatistics.INCOMING.getCachedTtl();
}
@Override
public String getTotalOutgoingBandwidthFormatted() {
return CachedBandwidthStatistics.OUTGOING.getCachedTtl();
}
@Override
public int getTotalPlayersJoined() {
return totalJoinedPlayers;
}
@Override
public int getTotalPlayersVerified() {
return Sonar.get().getVerifiedPlayerController().estimatedSize();
}
@Override
public int getTotalSuccessfulVerifications() {
return totalSuccessfulVerifications;
}
@Override
public int getTotalFailedVerifications() {
return totalFailedVerifications;
}
@Override
public long getCurrentAttemptedVerifications() {
return Sonar.get().getFallback().getConnected().size();
}
@Override
public int getTotalAttemptedVerifications() {
return totalAttemptedVerifications;
}
@Override
public long getCurrentBlacklistSize() {
return Sonar.get().getFallback().getBlacklist().estimatedSize();
}
@Override
public long getTotalBlacklistSize() {
return totalBlacklistedPlayers;
}
}
Loaded 100 of 141 files, more files were not shown because too many files have changed in this diff. Show more