Compare commits

..
61 Commits
Author SHA1 Message Date
Jones ef31dd4c87 Release 2.0.6 (#77) 2023-10-25 17:42:59 +02:00
Jones 8361f1990d Implement BungeeCord support (#76)
* feat: remove Velocity's (de-)compression dependency

* feat: [wip] implement Fallback for BungeeCord

* feat: disable legacy pipelines by default (can be enabled again)

* fix(bungee): NPE on handshake

* fix: use correct protocol version for channel initialization

* feat: implement login packet spam fix

* feat(bungee): implement duplicate login/status packet spam fix

* feat: implement cleaner FallbackHandlerBoss

* fix(bungee): traffic handler not being injected properly

* fix(bungee): run verification check after running everything in the event loop

* fix(bungee): only throw exceptions for up- and downstream bridge

* fix(bungee): total traffic statistic not working

* fix(bukkit, bungee): logger wrapper not supporting arguments

* fix(bungee): perform the already-online check by the username, not the uuid

* style: fix FallbackChannelHandler comments

* style: don't annotate FallbackUserWrapper as EqualsAndHashCode

* style: fix ToString annotation in EmptyChunkData

* feat(fallback): remove 1.19/1.19.1 chat nbt mappings

* feat: use expiring cache for joins per second counter

* fix(bungee): don't log the join attempt if the login is canceled

* fix(all): lockdown mode not being saved correctly

* fix(bungee): closeWith method only working during verification

* fix(bungee): use Velocity's server write mark

* fix(bungee): don't set BB allocator if it's already set
2023-10-22 14:32:37 +02:00
Jones bf9e77a437 Implement better adventure audience system (#75)
* feat: recode verbose handler by implementing per-platform audiences

* feat: remove /sonar verbose [other player]

* feat: remove ServerWrapper and replace it with the platform

* feat: reset action bar when unsubscribing from verbose

* feat: recode tab suggestions and help message caching

* feat: make Sonar main command translatable/configurable

* feat: don't cache copyright year

* fix: tab suggestions breaking after the first iteration

* feat: run only-players and require-console checks in the Subcommand class to remove duplicate code

* fix(all): replace instance check with condition check

* style: improve codestyle in YAML configuration loader

* fix: argumentsRequired property not working at all

* style: small codestyle improvements and comment fixes
2023-10-21 12:11:23 +02:00
Jones 7f604dd4ad Release 2.0.5 (#74) 2023-10-20 17:41:16 +02:00
Jones ab96929070 Fix EmptyChunkData packet being broken on 1.17+ (#73) 2023-10-20 17:36:43 +02:00
Michel Elkenwaat fa20826b8e Merge remote-tracking branch 'origin/main' 2023-10-20 17:22:25 +02:00
Michel Elkenwaat c8c212a3ee Fix issues with the plugin description (#69) 2023-10-20 17:22:08 +02:00
Michel Elkenwaat ad7d3e62eb Fix issues with the plugin description https://github.com/jonesdevelopment/sonar/pull/69 2023-10-20 17:21:30 +02:00
Ismael 343d71d18c Fix incompatibilities with Gradle 7.x (#69)
Fix "java.lang.IllegalStateException: Module entity with name: sonar.bukkit should be available"
2023-10-20 16:38:52 +02:00
Jones a2e252aece Use a better conversion technique to improve update checker (#72) 2023-10-20 15:09:31 +02:00
Jones 124bed86dc Fix invalid root tag in chunk data packet (#70)
* fix: use Velocity's JoinGame packet for consistency (https://github.com/jonesdevelopment/sonar/issues/68)

* fix: incorrect motion blocking root tag in EmptyChunkData packet (https://github.com/jonesdevelopment/sonar/issues/68)
2023-10-20 14:19:53 +02:00
Jones 971d43df00 Recode IPv4 regex pattern check (#67) 2023-10-19 17:03:52 +02:00
Jones 1671ccac55 Release 2.0.4 (#66) 2023-10-18 16:38:22 +02:00
dependabot[bot] 8e03bdac5f build(deps): bump io.netty:netty-all from 4.1.99.Final to 4.1.100.Final (#64)
Bumps [io.netty:netty-all](https://github.com/netty/netty) from 4.1.99.Final to 4.1.100.Final.
- [Commits](https://github.com/netty/netty/compare/netty-4.1.99.Final...netty-4.1.100.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>
2023-10-18 16:35:22 +02:00
Jones 7c0bdc568e Continuation of the integration of 1.20.2 (#65)
* style(all): general code reformat

* feat: remove ActiveFeatures packet

* refactor: create packages for each state

* feat: fully recode JoinGame and RegistrySync packets

* feat: [wip] implement configuration state support to verification handler

* fix: encoder/decoder state not being set properly

* feat: integrate configuration state with the rest of the verification

* fix: PositionLook packet having an invalid packet id on 1.20.2

* fix: don't create and cache chunk mask for 1.17 clients

* style: remove comment; there's not really any BF code left

* feat: add todo for 1.20.2 barrier block mappings

* feat: exempt 1.20.2 clients from gravity and collision checks (TODO)
2023-10-18 16:34:37 +02:00
Jones 354c7a0143 Prepare 1.20.2 support and fix deserialization issue (#62)
* fix: compatibility with the latest version of Velocity (git-eb594fc7-b274)

* fix: implement invalid protocol disconnect if the version is unknown

* feat: add already-online disconnect message translation

* feat: implement 1.20.2 support for JoinGame packet

* feat: add new packets for 1.20.2 config state

* feat: [wip] add packet mappings for 1.20.2 (config state not included)

* fix: implement backwards compatibility for Velocity b265 and below

* fix: optimize login handling by directly running it in the channel's event loop

* feat: make lockdown bypass permission customizable

* fix(common): make all packets more debugging-friendly

* feat: [wip] implement config protocol state

* style: add comments for documenting FallbackPacketDecoder

* fix(all): deserialization not working for some components (https://github.com/jonesdevelopment/sonar/issues/63)
2023-10-14 15:12:39 +02:00
Jones bb33aa3fa4 Release 2.0.3 (#60) 2023-10-08 11:55:52 +02:00
Jones a0e308321d Overhaul all configurations and add more translations (#59)
* feat: create subclasses for different configuration value types

* fix: only send UpdateSectionBlocks packet if the collision check is enabled

* feat: restructure all configurations (config.yml, en.yml)

* fix: invalid configuration path for certain settings

* fix: use default comment stype because empty lines look ugly

* feat: update configuration yaml file header

* fix: smaller configuration fixes and relocations

* feat: migrate messages to modern MiniMessage formatting

* fix: verbose not deserializing the component properly

* feat: refactor commands into their own category in the config

* feat: remove unused postReload task/method

* fix: make hover component in /sonar actually look decent

* feat: make /sonar stats completely configurable

* fix: use MiniMessage newline tag instead of hardcoded newline symbol

* fix: use correct translation for /sonar stats cpu

* style: remove resolved todo comment

* refactor: lang/en.yml -> translations/en.yml
2023-10-07 16:33:29 +02:00
Jones 71b6d39eab Recode some commands and implement new dump system (#58)
* feat: reimplement the dumping system using Gson

* feat(subcommands): recoded blacklist/verified IP checking logic

* feat: add unknown IP address (Unknown Host) translation to config

* fix: start with a default queue size of 1 in the I/O lock for /sonar verified remove

* fix: lock IP addresses in /sonar verified history to avoid double I/O operations

* Revert "fix: lock IP addresses in /sonar verified history to avoid double I/O operations"

This reverts commit 6b092100befa423272f1eb310f5d1f8a0e9afb82.

* feat: recode /sonar stats to be more reliable and stable

* feat: update several subcommand descriptions to be more accurate

* fix: set lockdown bypass permission to sonar.lockdown.bypass
2023-10-07 10:44:51 +02:00
Jones 3faf8c8b7f General code cleanup and improvements (#57)
* style(api): small codestyle improvements and QoL updates

* style: reformat code using IntelliJ's code refactoring tool
2023-10-06 22:06:53 +02:00
Jones b45626eafb Implement update checker (#56)
* feat: implement update checker on server start

* feat: add non-main-branch message to update checker to avoid confusion
2023-10-06 21:46:54 +02:00
Jones 702ea1b370 Use barriers instead of stone blocks for verification (#54)
* feat: replace abstract method with a java.util.function.Function

* fix: make spawn x and z position adapt to platform size

* feat: optimize/improve Fallback packet preparing

* fix: block id shifting being broken on 1.7-1.13

* fix: optimize 1.7 support in UpdateSectionBlocks packet

* style: improve codestyle of the UpdateSectionBlocks packet

* fix: use normal binary io reader for mappings instead of unlimited reader

* feat: implement universal Chat packet and add LOGIN / GAME states for better packet registry control

* feat: [wip] barrier block integration (block ids for 1.13+ are still missing)

* feat: use buffered writing more often to potentially reduce performance impact

* feat: optimize chunk sending by only flushing the packets if necessary

* refactor: setAbilitiesAndTeleport -> sendAbilitiesAndTeleport

* style: fix fields being named incorrectly

* feat: cache 1.7 empty chunk filler bytes

* feat: use barrier blocks instead of stone blocks for verification
2023-10-06 20:27:48 +02:00
Jones a75c409e13 Recode gravity and collision checks (#53)
* feat: recode gravity verification system to avoid issues on 1.7/1.8

* feat: check for too many invalid x/z ticks (changes)

* feat: check for too many ignored Y ticks (changes)

* feat: allow all position changes to be logged to console

* fix: false positive with invalid movement ticks

* feat: improve verbose message in the gravity check
2023-10-05 20:52:04 +02:00
Jones afe9f3f9ea Implement support for delayed writing of packets (#50)
* feat: implement support for sending packets buffered/delayed

* feat: send EmptyChunkData and UpdateSectionBlocks delayed to potentially improve performance
2023-10-04 22:09:59 +02:00
Jones ba63e80fd0 Fix client brand decoding not working on 1.7 clients (#49) 2023-10-04 21:54:42 +02:00
Jones 5ff7e23fcb Apply hardcoded packet size limits to some packets (#48) 2023-10-04 21:43:26 +02:00
Jones cc194b0d62 Fix missing relocations potentially causing classes to clash (#47)
* fix: exclude snakeyaml from being implemented twice

* fix: relocate some packages to avoid classes being loaded twice
2023-10-04 20:48:55 +02:00
Jones 4e4423e2ce Release 2.0.2 (#45) 2023-10-04 14:58:36 +02:00
Jones ddb65f2f5a Bump dependencies (#44)
* build(deps): bump netty from 4.1.98.Final to 4.1.99.Final

* build(deps): bump gradle from 8.2 to 8.3
2023-10-04 14:53:47 +02:00
Jones 254faecc90 Fix verbose animation being unordered and modifiable (#43)
* fix: make sure the animations are stored in a List, not a Collection

* fix: make animations unmodifiable after reading them from the config
2023-10-04 14:41:17 +02:00
Jones 582024eba1 Improve configuration style and remove unused values (#42)
* feat: use semantic field names for the configuration values

* fix: remove unused messages from the configuration

* feat: use lombok getter for all configuration values
2023-10-04 14:37:29 +02:00
Jones e1fe738ffe Make verbose message handler customizable (#41)
* feat: allow custom verbose handler via the API

* fix: allow modifications of the next action bar message

* fix: don't allow modifications of internal methods
2023-10-04 14:00:16 +02:00
Jones 5f3a439113 Fix permission message not showing on Velocity (#40)
* feat: move permission check to the command execution (https://github.com/jonesdevelopment/sonar/issues/39)

* fix: do not allow tab completion if the player does not have the required permission

* fix: handle https://github.com/jonesdevelopment/sonar-legacy/issues/7 using an environment variable
2023-10-04 12:47:59 +02:00
Jones 27cca23412 Fix lockdown message not being serialized properly (#37) 2023-10-02 19:18:07 +02:00
Jones 2d2c576ad4 Implement MiniMessage support (#35)
* fix(bungee): use correct main class for the bungee plugin

* fix(bungee, bukkit): don't reload the plugin on enable

* feat: implement MiniMessage (adventure) support

* feat(all): recode ServerWrapper

* refactor: make SonarCommand platform independent

* Implement automatic Velocity version increment (#32)

* feat: add MiniMessage support to the Disconnect packet

* fix: only use the adventure-platform dependency in the respective modules

* feat: add velocity-plugin.json that automatically adapts the version and plugin description

* fix: only save the configuration when necessary (https://github.com/jonesdevelopment/sonar/issues/33)

* feat: implement MiniMessage legacy deserialization and modern color formatting support

* fix(velocity): use correct sendMessage() function
2023-10-02 18:13:55 +02:00
Michel Elkenwaat 0adf7b0b6a Revert "Bump netty from 4.1.98.Final to 4.1.99.Final (#31)"
This reverts commit c6ff9370c2.
2023-10-02 18:09:29 +02:00
dependabot[bot] c6ff9370c2 Bump netty from 4.1.98.Final to 4.1.99.Final (#31)
Bumps [io.netty:netty-all](https://github.com/netty/netty) from 4.1.98.Final to 4.1.99.Final.
- [Commits](https://github.com/netty/netty/compare/netty-4.1.98.Final...netty-4.1.99.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>
2023-10-02 17:27:14 +02:00
Jones e33aac29c5 Release version 2.0.1 (#29) 2023-09-30 09:15:05 +02:00
Jones 42dc3af6c9 Implement unofficial branch warnings and improve version management (#28)
* feat: check for non-main-branch builds and mark them as experimental

* feat: implement Implementation-Vendor

* feat: store version information in gradle.properties

* fix(bungee, bukkit): only use the semantic version for the plugin.yml/bungee.yml
2023-09-27 16:51:12 +02:00
Jones ff402d5096 Fix configuration reset after InvalidConfigurationException (#27)
* fix: only create a new messages configuration object if the preferred language changed (https://github.com/jonesdevelopment/sonar/issues/26)

* feat(api): better exception handling when loading the configuration

* fix: only load the configuration if the file already exists (https://github.com/jonesdevelopment/sonar/issues/26)

* feat: create a backup of the config if something goes wrong (https://github.com/jonesdevelopment/sonar/issues/26)

* fix: config header not showing and options not applying
2023-09-27 16:13:12 +02:00
Jones 06a72bf3bd Implement simple event system (#25)
* refactor(api): use toString() instead of formattedDelay() for SystemTimer

* fix(api): actually use the elapsed() function in SystemTimer

* feat(api): implement simple event system

* refactor(api): FallbackPlayer -> FallbackUser
2023-09-25 23:40:34 +02:00
Jones 3fb4291163 Merge pull request #24 from jonesdevelopment/create-code-of-conduct
Create CODE_OF_CONDUCT.md
2023-09-25 22:03:08 +02:00
Jones cc57b1d978 Create CODE_OF_CONDUCT.md 2023-09-25 22:02:41 +02:00
Jones 750b42b5ab Merge pull request #23 from jonesdevelopment/dependabot/gradle/org.projectlombok-lombok-1.18.30
build(deps): bump org.projectlombok:lombok from 1.18.28 to 1.18.30
2023-09-25 21:54:40 +02:00
Jones ad017024ae Merge pull request #22 from jonesdevelopment/dependabot/gradle/io.netty-netty-all-4.1.98.Final
build(deps): bump io.netty:netty-all from 4.1.97.Final to 4.1.98.Final
2023-09-25 21:54:19 +02:00
dependabot[bot] 5887d82c19 build(deps): bump org.projectlombok:lombok from 1.18.28 to 1.18.30
Bumps [org.projectlombok:lombok](https://github.com/projectlombok/lombok) from 1.18.28 to 1.18.30.
- [Changelog](https://github.com/projectlombok/lombok/blob/master/doc/changelog.markdown)
- [Commits](https://github.com/projectlombok/lombok/compare/v1.18.28...v1.18.30)

---
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>
2023-09-25 01:06:35 +00:00
dependabot[bot] b1e5d01062 build(deps): bump io.netty:netty-all from 4.1.97.Final to 4.1.98.Final
Bumps [io.netty:netty-all](https://github.com/netty/netty) from 4.1.97.Final to 4.1.98.Final.
- [Commits](https://github.com/netty/netty/compare/netty-4.1.97.Final...netty-4.1.98.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>
2023-09-25 01:06:26 +00:00
Jones a50bf45176 Merge pull request #21 from jonesdevelopment/fix/1.7-support
Implement Minecraft 1.7 support
2023-09-23 11:07:39 +02:00
Michel Elkenwaat e0b5ad2a33 feat(fallback): implement UpdateSectionBlocks packet for 1.7 (https://github.com/jonesdevelopment/sonar/issues/20) 2023-09-23 11:01:32 +02:00
Michel Elkenwaat e5ee94362c fix(fallback): false positive with new fell too fast check 2023-09-23 10:54:01 +02:00
Michel Elkenwaat d877568b38 fix(fallback): 1.7 support for PositionLook packet 2023-09-23 10:53:27 +02:00
Michel Elkenwaat 0edec77135 fix(fallback): recode PositionLook packet (encode) 2023-09-23 01:03:01 +02:00
Michel Elkenwaat 54445cdfdb fix(fallback): fix EmptyChunkData packet being broken 2023-09-23 00:56:23 +02:00
Michel Elkenwaat 8545d60791 fix(fallback): gravity check being broken 2023-09-23 00:52:02 +02:00
Michel Elkenwaat 11327e2890 fix(fallback): 1.7-1.8.9 potentially unloaded chunks causing issues 2023-09-23 00:21:00 +02:00
Michel Elkenwaat ad511836f3 fix: exclude changedBlocks from toString() in UpdateSectionBlocks packet 2023-09-23 00:13:57 +02:00
Michel Elkenwaat 5cb79d3acf fix: implement 1.7 support to DefaultSpawnPosition packet 2023-09-23 00:08:55 +02:00
Michel Elkenwaat 032dd4f94e feat(fallback): implement packet limits to Position/PositionLook 2023-09-23 00:03:06 +02:00
Michel Elkenwaat e831c1a646 fix: remove leftover debug messages 2023-09-23 00:00:17 +02:00
Michel Elkenwaat f50c90a690 fix: KeepAlive state check now ignores 1.7-1.8.9 2023-09-22 23:57:45 +02:00
Michel Elkenwaat 1e7e4ba5f1 fix(fallback): Position and PositionLook packet not decoding correctly 2023-09-22 23:57:18 +02:00
107 changed files with 4314 additions and 3441 deletions

No files matched your search

+128
View File
@@ -0,0 +1,128 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or
advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
contact@jonesdev.xyz.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series
of actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.
+3 -3
View File
@@ -42,8 +42,8 @@
- [Fallback](https://github.com/jonesdevelopment/sonar#fallback) is Sonar's main component designed to prevent
all types of bots.
- Spigot & BungeeCord
- The compression method is changed
to [Velocity's libdeflate](https://github.com/PaperMC/Velocity/tree/dev/3.0.0/native).
- Duplicate login/status packet spam fix
- Run server ping handling asynchronously
- The Varint decoder is updated to Velocity's
improved [MinecraftVarintFrameDecoder](https://github.com/PaperMC/Velocity/blob/dev/3.0.0/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftVarintFrameDecoder.java).
@@ -91,4 +91,4 @@ Sonar is licensed under the [GNU General Public License 3.0](https://www.gnu.org
- Special thanks to the [contributors of Sonar](https://github.com/jonesdevelopment/sonar/graphs/contributors).
- The nbt mappings were taken from [LimboAPI](https://github.com/Elytrium/LimboAPI).
- The compression and Varint decoding was taken from [Velocity](https://github.com/PaperMC/Velocity).
- The Varint decoding was taken from [Velocity](https://github.com/PaperMC/Velocity).
+27 -14
View File
@@ -12,12 +12,9 @@ plugins {
apply(plugin = "io.toolebox.git-versioner")
rootProject.group = "xyz.jonesdev.sonar"
val semanticVersion = "2.0.0"
versioner {
pattern {
pattern = "$semanticVersion-%h(-%c)"
pattern = "$version-%h-%c-%b"
}
}
@@ -25,7 +22,7 @@ allprojects {
repositories {
mavenCentral() // Lombok
maven(url = "https://jitpack.io") // simple-yaml
maven(url = "https://repo.papermc.io/repository/maven-public") // Velocity natives
maven(url = "https://repo.papermc.io/repository/maven-public") // Velocity API
maven(url = "https://repo.jonesdev.xyz/releases/") // Bungee & Velocity proxy module
}
}
@@ -35,17 +32,24 @@ subprojects {
apply(plugin = "com.github.johnrengelman.shadow")
dependencies {
compileOnly("org.projectlombok:lombok:1.18.28")
annotationProcessor("org.projectlombok:lombok:1.18.28")
compileOnly("org.projectlombok:lombok:1.18.30")
annotationProcessor("org.projectlombok:lombok:1.18.30")
testCompileOnly("org.projectlombok:lombok:1.18.28")
testAnnotationProcessor("org.projectlombok:lombok:1.18.28")
testCompileOnly("org.projectlombok:lombok:1.18.30")
testAnnotationProcessor("org.projectlombok:lombok:1.18.30")
compileOnly("io.netty:netty-all:4.1.97.Final") // netty
val adventureVersion = "4.14.0"
// adventure
implementation("net.kyori:adventure-text-minimessage:$adventureVersion")
implementation("net.kyori:adventure-text-serializer-gson:$adventureVersion")
// adventure nbt
implementation("net.kyori:adventure-nbt:$adventureVersion")
implementation("net.kyori:adventure-nbt:4.14.0") // nbt
implementation("com.j256.ormlite:ormlite-jdbc:6.1") // ORM
implementation("xyz.jonesdev:cappuccino:0.1.6-SNAPSHOT") // expiring cache
compileOnly("io.netty:netty-all:4.1.100.Final") // netty
}
}
@@ -62,14 +66,23 @@ tasks {
// 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 {
// bStats has to be relocated to the Sonar package otherwise it throws an exception.
relocate("org.bstats", "xyz.jonesdev.sonar.bstats")
// Set the file name of the shadowed jar
archiveFileName.set("${rootProject.name}.jar")
archiveFileName.set("Sonar.jar")
// 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("net.kyori.adventure.nbt", "xyz.jonesdev.sonar.libs.nbt")
relocate("com.google.gson", "xyz.jonesdev.sonar.libs.gson")
relocate("com.j256.ormlite", "xyz.jonesdev.sonar.libs.ormlite")
}
compileJava {
+3
View File
@@ -0,0 +1,3 @@
group=xyz.jonesdev.sonar
version=2.0.6
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.2-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
+1 -1
View File
@@ -1,4 +1,4 @@
rootProject.name = "Sonar"
rootProject.name = "sonar"
sequenceOf("api", "bukkit", "bungee", "common", "velocity").forEach {
val path = "sonar-$it"
+3 -1
View File
@@ -1,5 +1,7 @@
dependencies {
implementation("com.github.Carleslc.Simple-YAML:Simple-Yaml:1.8.4") // yaml config
implementation("com.github.Carleslc.Simple-YAML:Simple-Yaml:1.8.4") {
exclude(group = "org.yaml")
}
compileOnly("org.jetbrains:annotations:24.0.1")
}
@@ -21,9 +21,9 @@ import org.jetbrains.annotations.NotNull;
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.event.SonarEventManager;
import xyz.jonesdev.sonar.api.fallback.Fallback;
import xyz.jonesdev.sonar.api.logger.LoggerWrapper;
import xyz.jonesdev.sonar.api.server.ServerWrapper;
import xyz.jonesdev.sonar.api.verbose.Verbose;
import java.io.File;
@@ -34,9 +34,9 @@ public interface Sonar {
String LINE_SEPARATOR = "\n"; // Using System.lineSeparator is broken, for some reason...
/**
* @return A small wrapper for the server
* @return The platform the plugin is being run on
*/
@NotNull ServerWrapper getServer();
@NotNull SonarPlatform getPlatform();
/**
* @return A small wrapper for the plugin logger so we can use the logger everywhere
@@ -53,6 +53,12 @@ public interface Sonar {
@NotNull Verbose getVerboseHandler();
/**
* Set a custom verbose handler
*/
@SuppressWarnings("unused")
void setVerboseHandler(final @NotNull Verbose verboseHandler);
/**
* Reloads the entire plugin
*/
@@ -63,10 +69,15 @@ public interface Sonar {
return SonarVersion.GET;
}
@NotNull
default SonarEventManager getEventManager() {
// We don't want anyone else to create a new instance.
return SonarEventManager.INSTANCE;
}
@NotNull
default Fallback getFallback() {
// We only have one single instance of Fallback,
// and we don't want anyone else to create a new instance.
// We don't want anyone else to create a new instance.
return Fallback.INSTANCE;
}
@@ -24,6 +24,7 @@ public final class SonarVersion {
static final SonarVersion GET = new SonarVersion();
private final String semanticVersion, full, formatted, commitSHA;
private final int build;
private final boolean isOnMainBranch;
SonarVersion() {
final Package manifest = Sonar.class.getPackage();
@@ -31,11 +32,12 @@ public final class SonarVersion {
if (versionString == null) {
this.full = "<could not retrieve version>";
this.semanticVersion = "<unknown>";
this.commitSHA = "<unknown>";
this.build = 0;
this.semanticVersion = "<unknown version>";
this.commitSHA = "<unknown commit>";
this.build = -1;
this.isOnMainBranch = false;
this.formatted = full;
Sonar.get().getLogger().error("Could not find version information (Is the manifest missing?)");
Sonar.get().getLogger().warn("Could not find version information. Is the manifest missing?");
return;
}
@@ -43,6 +45,7 @@ public final class SonarVersion {
this.semanticVersion = versionString.split("-")[0];
this.commitSHA = versionString.split("-")[1];
this.build = Integer.parseInt(versionString.split("-")[2]);
this.isOnMainBranch = versionString.split("-")[3].equals("main");
this.formatted = semanticVersion + " build " + build + " (" + commitSHA + ")";
}
@@ -19,12 +19,14 @@ package xyz.jonesdev.sonar.api.command;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
import xyz.jonesdev.sonar.api.command.subcommand.Subcommand;
@Getter
@RequiredArgsConstructor
@ToString(of = "rawArguments")
public final class CommandInvocation {
private final InvocationSender sender;
private final Subcommand command;
private final InvocationSource sender;
private final Subcommand subcommand;
private final String[] rawArguments;
}
@@ -0,0 +1,53 @@
/*
* Copyright (C) 2023 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.command;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage;
@Getter
@RequiredArgsConstructor
public abstract class InvocationSource {
private final String name;
private final Audience audience;
private final boolean player;
/**
* Sends an empty chat message to the command executor
*/
public final void sendMessage() {
sendMessage(Component.empty());
}
/**
* Sends a message to the command executor
*/
public final void sendMessage(final String legacy) {
sendMessage(MiniMessage.miniMessage().deserialize(legacy));
}
/**
* Sends a message to the command executor
*/
public final void sendMessage(final Component component) {
audience.sendMessage(component);
}
}
@@ -0,0 +1,92 @@
/*
* Copyright (C) 2023 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.command;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.cappuccino.Cappuccino;
import xyz.jonesdev.cappuccino.ExpiringCache;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.command.subcommand.Subcommand;
import xyz.jonesdev.sonar.api.command.subcommand.argument.Argument;
import java.util.*;
import java.util.stream.Collectors;
import static java.util.Collections.emptyList;
public interface SonarCommand {
List<String> TAB_SUGGESTIONS = new ArrayList<>();
Map<String, List<String>> ARG_TAB_SUGGESTIONS = new HashMap<>();
ExpiringCache<Object> DELAY = Cappuccino.buildExpiring(500L);
List<Component> CACHED_HELP_MESSAGE = new ArrayList<>();
static void prepareCachedMessages() {
// Cache help message
CACHED_HELP_MESSAGE.clear();
for (final String message : Sonar.get().getConfig().getCommands().getHelpHeader()) {
CACHED_HELP_MESSAGE.add(MiniMessage.miniMessage().deserialize(message
.replace("%version%", Sonar.get().getVersion().getFormatted())
.replace("%platform%", Sonar.get().getPlatform().getDisplayName())
.replace("%copyright_year%", String.valueOf(Calendar.getInstance().get(Calendar.YEAR)))));
}
final String subcommandFormat = Sonar.get().getConfig().getCommands().getHelpSubcommands();
Sonar.get().getSubcommandRegistry().getSubcommands().forEach(subcommand -> {
final Component deserialized = MiniMessage.miniMessage().deserialize(subcommandFormat
.replace("%subcommand%", subcommand.getInfo().name())
.replace("%description%", subcommand.getInfo().description())
.replace("%only_players%", subcommand.getInfo().onlyPlayers() ? "<green>✔</green>" : "<red>✗</red>")
.replace("%require_console%", subcommand.getInfo().onlyConsole() ? "<green>✔</green>" : "<red>✗</red>")
.replace("%permission%", subcommand.getPermission())
.replace("%aliases%", subcommand.getAliases()));
CACHED_HELP_MESSAGE.add(deserialized);
});
// Don't re-cache tab suggestions
if (!TAB_SUGGESTIONS.isEmpty()) return;
// Cache tab suggestions
for (final Subcommand subcommand : Sonar.get().getSubcommandRegistry().getSubcommands()) {
TAB_SUGGESTIONS.add(subcommand.getInfo().name());
if (subcommand.getInfo().aliases().length > 0) {
TAB_SUGGESTIONS.addAll(Arrays.asList(subcommand.getInfo().aliases()));
}
final List<String> parsedArguments = Arrays.stream(subcommand.getInfo().arguments())
.map(Argument::value)
.collect(Collectors.toList());
ARG_TAB_SUGGESTIONS.put(subcommand.getInfo().name(), parsedArguments);
for (final String alias : subcommand.getInfo().aliases()) {
ARG_TAB_SUGGESTIONS.put(alias, parsedArguments);
}
}
}
default List<String> getCachedTabSuggestions(final String @NotNull [] arguments) {
if (arguments.length <= 1) {
return TAB_SUGGESTIONS;
} else if (arguments.length == 2) {
final String subCommandName = arguments[0].toLowerCase();
return ARG_TAB_SUGGESTIONS.getOrDefault(subCommandName, emptyList());
}
return emptyList();
}
}
@@ -19,12 +19,16 @@ package xyz.jonesdev.sonar.api.command.subcommand;
import lombok.Getter;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.command.CommandInvocation;
import xyz.jonesdev.sonar.api.command.InvocationSender;
import xyz.jonesdev.sonar.api.command.InvocationSource;
import xyz.jonesdev.sonar.api.command.subcommand.argument.Argument;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Arrays;
import java.util.Objects;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
@@ -32,10 +36,8 @@ import java.util.stream.Collectors;
public abstract class Subcommand {
private final @NotNull SubcommandInfo info;
private final String permission, aliases, arguments;
protected static final Sonar SONAR = Sonar.get();
protected static final Pattern IP_PATTERN = Pattern.compile(
"^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])([.])){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$"
);
public Subcommand() {
info = getClass().getAnnotation(SubcommandInfo.class);
@@ -49,12 +51,63 @@ public abstract class Subcommand {
.collect(Collectors.joining(", "));
}
protected final void incorrectUsage(final @NotNull InvocationSender sender) {
sender.sendMessage(
SONAR.getConfig().INCORRECT_COMMAND_USAGE
.replace("%usage%", info.name() + " (" + arguments + ")")
);
// https://stackoverflow.com/questions/5284147/validating-ipv4-addresses-with-regexp
private static final Pattern IP_PATTERN = Pattern.compile("^((25[0-5]|(2[0-4]|1\\d|[1-9]|)\\d)\\.?\\b){4}$");
protected static @Nullable InetAddress getInetAddressIfValid(final InvocationSource source, final String rawIP) {
// We perform a regex check beforehand,
// so we don't have to create a new InetAddress,
// therefore, potentially saving some performance.
if (!IP_PATTERN.matcher(Objects.requireNonNull(rawIP)).matches()) {
source.sendMessage(SONAR.getConfig().getCommands().getIncorrectIpAddress());
return null;
}
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().getUnknownIpAddress());
return null;
}
return inetAddress;
}
public abstract void execute(final @NotNull CommandInvocation invocation);
protected final void incorrectUsage(final @NotNull InvocationSource sender) {
sender.sendMessage(SONAR.getConfig().getCommands().getIncorrectCommandUsage()
.replace("%usage%", info.name() + " (" + arguments + ")"));
}
public final void invoke(final @NotNull InvocationSource invocationSource, final String @NotNull [] arguments) {
// Check if the subcommand can only be executed by players
if (getInfo().onlyPlayers() && !invocationSource.isPlayer()) {
invocationSource.sendMessage(Sonar.get().getConfig().getCommands().getPlayersOnly());
return;
}
// Check if the subcommand can only be executed though console
if (getInfo().onlyConsole() && invocationSource.isPlayer()) {
invocationSource.sendMessage(Sonar.get().getConfig().getCommands().getConsoleOnly());
return;
}
final CommandInvocation commandInvocation = new CommandInvocation(invocationSource, this, arguments);
// The subcommands has arguments which are not present in the executed command
if (getInfo().argumentsRequired() && getInfo().arguments().length > 0 && arguments.length <= 1) {
invocationSource.sendMessage(Sonar.get().getConfig().getCommands().getIncorrectCommandUsage()
.replace("%usage%", getInfo().name() + " (" + getArguments() + ")"));
return;
}
// Execute the sub command
execute(commandInvocation);
}
protected abstract void execute(final @NotNull CommandInvocation invocation);
}
@@ -18,6 +18,7 @@
package xyz.jonesdev.sonar.api.config;
import lombok.Getter;
import org.jetbrains.annotations.NotNull;
import org.simpleyaml.configuration.comments.format.YamlCommentFormat;
import org.simpleyaml.configuration.file.YamlFile;
import xyz.jonesdev.sonar.api.Sonar;
@@ -27,15 +28,19 @@ import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import static xyz.jonesdev.sonar.api.Sonar.LINE_SEPARATOR;
@Getter
public final class SimpleYamlConfig {
private final File file;
private final YamlFile yaml;
private static final List<String> HEADER = Arrays.asList(
"",
"Sonar anti-bot version " + Sonar.get().getVersion(),
" Currently running on: " + Sonar.get().getServer().getPlatform().getDisplayName(),
" Need help? https://jonesdev.xyz/discord",
String.format("Running Sonar version %s on %s",
Sonar.get().getVersion(), Sonar.get().getPlatform().getDisplayName()),
"Need help or have questions? https://jonesdev.xyz/discord",
"https://github.com/jonesdevelopment/sonar",
""
);
@@ -43,11 +48,12 @@ public final class SimpleYamlConfig {
this(new File(dataFolder, fileName + ".yml"), dataFolder);
}
private SimpleYamlConfig(final File file, final File folder) {
private SimpleYamlConfig(final File file, final @NotNull File folder) {
if (!folder.exists() && !folder.mkdir()) {
throw new IllegalStateException("Could not create folder?!");
}
this.file = file;
this.yaml = new YamlFile(file.getPath());
}
@@ -59,16 +65,18 @@ public final class SimpleYamlConfig {
}
}
public void load() {
try {
yaml.setCommentFormat(YamlCommentFormat.PRETTY);
public void load() throws Exception {
// https://github.com/jonesdevelopment/sonar/issues/26
// Only load the configuration if the file already exists
if (yaml.exists()) {
yaml.loadWithComments();
} else {
yaml.createOrLoadWithComments();
yaml.options().headerFormatter().commentPrefix("# ");
yaml.setHeader(String.join(Sonar.LINE_SEPARATOR, HEADER));
} catch (IOException exception) {
exception.printStackTrace(System.err);
}
// Always load the comment format and options
yaml.setCommentFormat(YamlCommentFormat.DEFAULT);
yaml.setHeader(String.join(LINE_SEPARATOR, HEADER));
}
public void set(final String path, final Object v) {
File diff suppressed because it is too large. Load diff
@@ -45,16 +45,14 @@ public final class VerifiedPlayerController {
private Dao<VerifiedPlayer, Integer> dao;
private QueryBuilder<VerifiedPlayer, Integer> queryBuilder;
@Getter
private final @NotNull SonarConfiguration.DatabaseType cachedDatabaseType;
private final @NotNull SonarConfiguration.Database.Type cachedDatabaseType;
public VerifiedPlayerController() {
final SonarConfiguration config = Sonar.get().getConfig();
// Cache selected database type, so we don't need to call Sonar.get() every time
cachedDatabaseType = config.DATABASE_TYPE;
cachedDatabaseType = Sonar.get().getConfig().getDatabase().getType();
// Don't establish a database connection if the type is NONE
if (cachedDatabaseType == SonarConfiguration.DatabaseType.NONE) {
if (cachedDatabaseType == SonarConfiguration.Database.Type.NONE) {
Sonar.get().getLogger().warn("Make sure to configure a database to save verified players.");
return;
}
@@ -89,7 +87,7 @@ public final class VerifiedPlayerController {
_remove(inetAddress);
// Don't try to update the column if the database type is NONE
if (cachedDatabaseType == SonarConfiguration.DatabaseType.NONE) {
if (cachedDatabaseType == SonarConfiguration.Database.Type.NONE) {
return;
}
@@ -131,7 +129,7 @@ public final class VerifiedPlayerController {
_add(player);
// Don't try to update the column if the database type is NONE
if (cachedDatabaseType == SonarConfiguration.DatabaseType.NONE) {
if (cachedDatabaseType == SonarConfiguration.Database.Type.NONE) {
return;
}
@@ -181,7 +179,7 @@ public final class VerifiedPlayerController {
MAP.clear();
// Only update the column if the database type is not NONE
if (cachedDatabaseType != SonarConfiguration.DatabaseType.NONE) {
if (cachedDatabaseType != SonarConfiguration.Database.Type.NONE) {
dao.deleteBuilder().delete();
}
} catch (SQLException exception) {
@@ -28,7 +28,8 @@ import java.net.MalformedURLException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
// Mostly taken from
// https://github.com/Elytrium/LimboAuth/blob/master/src/main/java/net/elytrium/limboauth/dependencies/BaseLibrary.java
@@ -77,13 +78,12 @@ public enum Dependency {
try (final InputStream inputStream = mvnRepoURL.openStream()) {
Files.createDirectories(tempFilePath.getParent());
Files.copy(inputStream, Files.createFile(tempFilePath), StandardCopyOption.REPLACE_EXISTING);
Files.copy(inputStream, Files.createFile(tempFilePath), REPLACE_EXISTING);
} catch (IOException exception) {
throw new IllegalStateException(exception);
}
Sonar.get().getLogger().info("Finished downloading {} ({}s)!",
tempFilePath.getFileName(), timer.formattedDelay());
Sonar.get().getLogger().info("Finished downloading {} ({}s)!", tempFilePath.getFileName(), timer);
}
return tempFilePath.toUri().toURL();
@@ -38,8 +38,8 @@ public class DependencyLoader {
public ConnectionSource setUpDriverAndConnect() throws Throwable {
final SonarConfiguration config = Sonar.get().getConfig();
final URL[] urls = new URL[config.DATABASE_TYPE.getDependencies().length];
for (final Dependency dependency : config.DATABASE_TYPE.getDependencies()) {
final URL[] urls = new URL[config.getDatabase().getType().getDependencies().length];
for (final Dependency dependency : config.getDatabase().getType().getDependencies()) {
final URL url = dependency.getClassLoaderURL();
final ClassLoader currentClassLoader = DependencyLoader.class.getClassLoader();
@@ -50,12 +50,12 @@ public class DependencyLoader {
urls[dependency.ordinal()] = url;
}
final String type = config.DATABASE_TYPE.name().toLowerCase();
final String databaseURL =
"jdbc:" + type + "://" + config.SQL_URL + ":" + config.SQL_PORT + "/" + config.SQL_DATABASE;
final String type = config.getDatabase().getType().name().toLowerCase();
final String databaseURL = String.format("jdbc:%s://%s:%d/%s",
type, config.getDatabase().getUrl(), config.getDatabase().getPort(), config.getDatabase().getName());
final ExternalClassLoader classLoader = new ExternalClassLoader(urls);
final Connection connection = connect(classLoader, databaseURL, config);
final Connection connection = connect(classLoader, databaseURL, config.getDatabase());
return new JdbcSingleConnectionSource(databaseURL, connection);
}
@@ -63,18 +63,16 @@ public class DependencyLoader {
// https://github.com/Elytrium/LimboAuth/blob/master/src/main/java/net/elytrium/limboauth/dependencies/DatabaseLibrary.java#L134
private Connection connect(final @NotNull ClassLoader classLoader,
final @NotNull String databaseURL,
final @NotNull SonarConfiguration configuration) throws Throwable {
final Class<?> driverClass = classLoader.loadClass(configuration.DATABASE_TYPE.getDriverClassName());
final @NotNull SonarConfiguration.Database config) throws Throwable {
final Class<?> driverClass = classLoader.loadClass(config.getType().getDriverClassName());
final Object driver = driverClass.getDeclaredConstructor().newInstance();
DriverManager.registerDriver((Driver) driver);
final Properties properties = new Properties();
if (!configuration.SQL_USER.isEmpty()) {
properties.put("user", configuration.SQL_USER);
}
if (!configuration.SQL_PASSWORD.isEmpty()) {
properties.put("password", configuration.SQL_PASSWORD);
properties.put("user", config.getUsername());
if (!config.getPassword().isEmpty()) {
properties.put("password", config.getPassword());
}
final Method connect = driverClass.getDeclaredMethod("connect", String.class, Properties.class);
@@ -0,0 +1,21 @@
/*
* Copyright (C) 2023 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.event;
public interface SonarEvent {
}
@@ -15,26 +15,15 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.api.command;
package xyz.jonesdev.sonar.api.event;
public interface InvocationSender {
@FunctionalInterface
public interface SonarEventListener {
/**
* @return The name of the command executor
*/
String getName();
/**
* Sends a message to the command executor
* Handles the publishing of an event
*
* @param message Deserialized message
* @param event [Cancellable] Sonar event
*/
void sendMessage(final String message);
/**
* Sends an empty chat message to the command executor
*/
default void sendMessage() {
sendMessage("");
}
void handle(final SonarEvent event);
}
@@ -0,0 +1,64 @@
/*
* Copyright (C) 2023 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.event;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import java.util.Arrays;
import java.util.Collection;
import java.util.Vector;
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class SonarEventManager {
public static final SonarEventManager INSTANCE = new SonarEventManager();
private static final Collection<SonarEventListener> EVENT_LISTENERS = new Vector<>(0);
/**
* Publishes the given event to all listeners
*/
@ApiStatus.Internal
public void publish(final @NotNull SonarEvent event) {
for (final SonarEventListener eventListener : EVENT_LISTENERS) {
eventListener.handle(event);
}
}
/**
* Registers one (or more) event listeners
*
* @param listeners One (or more) listeners to register
* @see #unregisterListener(SonarEventListener...) Unregister an event listener
*/
public synchronized void registerListener(final @NotNull SonarEventListener... listeners) {
EVENT_LISTENERS.addAll(Arrays.asList(listeners));
}
/**
* Unregisters one (or more) event listeners
*
* @param listeners One (or more) listeners to unregister
* @apiNote This does not have any effect if the given listeners are not registered
* @see #registerListener(SonarEventListener...) Register an event listener
*/
public synchronized void unregisterListener(final @NotNull SonarEventListener... listeners) {
EVENT_LISTENERS.removeAll(Arrays.asList(listeners));
}
}
@@ -0,0 +1,35 @@
/*
* Copyright (C) 2023 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.event.impl;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
import org.jetbrains.annotations.ApiStatus;
import xyz.jonesdev.sonar.api.event.SonarEvent;
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
@Getter
@ToString
@EqualsAndHashCode
@RequiredArgsConstructor
public final class UserBlacklistedEvent implements SonarEvent {
@ApiStatus.Experimental
private final FallbackUser<?, ?> user;
}
@@ -15,17 +15,20 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.api.command;
package xyz.jonesdev.sonar.api.event.impl;
import xyz.jonesdev.cappuccino.Cappuccino;
import xyz.jonesdev.cappuccino.ExpiringCache;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
import xyz.jonesdev.sonar.api.event.SonarEvent;
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
import java.util.*;
public interface SonarBaseCommand {
List<String> TAB_SUGGESTIONS = new ArrayList<>();
Map<String, List<String>> ARG_TAB_SUGGESTIONS = new HashMap<>();
ExpiringCache<Object> DELAY = Cappuccino.buildExpiring(500L);
List<Object> CACHED_HELP_MESSAGE = new Vector<>();
int COPYRIGHT_YEAR = Calendar.getInstance().get(Calendar.YEAR);
@Getter
@ToString
@EqualsAndHashCode
@RequiredArgsConstructor
public final class UserVerifyFailedEvent implements SonarEvent {
private final FallbackUser<?, ?> user;
private final String reason;
}
@@ -0,0 +1,34 @@
/*
* Copyright (C) 2023 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.event.impl;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
import xyz.jonesdev.sonar.api.event.SonarEvent;
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
@Getter
@ToString
@EqualsAndHashCode
@RequiredArgsConstructor
public final class UserVerifyJoinEvent implements SonarEvent {
private final String username;
private final FallbackUser<?, ?> user;
}
@@ -15,27 +15,24 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.velocity.verbose;
package xyz.jonesdev.sonar.api.event.impl;
import com.velocitypowered.api.proxy.ProxyServer;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import net.kyori.adventure.text.Component;
import xyz.jonesdev.sonar.api.verbose.Verbose;
import lombok.ToString;
import xyz.jonesdev.sonar.api.event.SonarEvent;
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
import java.util.UUID;
@Getter
@ToString
@EqualsAndHashCode
@RequiredArgsConstructor
public final class VerboseWrapper extends Verbose {
private final ProxyServer server;
@Override
public void broadcast(final String message) {
final Component component = Component.text(message);
synchronized (subscribers) {
for (final String subscriber : subscribers) {
server.getPlayer(subscriber).ifPresent(player -> {
player.sendActionBar(component);
});
}
}
}
public final class UserVerifySuccessEvent implements SonarEvent {
private final String username;
private final UUID offlineUuid;
private final FallbackUser<?, ?> user;
private final long timeTakenToVerify;
}
@@ -41,7 +41,7 @@ public final class Fallback {
private final Map<String, InetAddress> connected = new ConcurrentHashMap<>();
// Only block the player for a few minutes to avoid issues
private final ExpiringCache<String> blacklisted = Cappuccino.buildExpiring(
10L, TimeUnit.MINUTES, 2500L
10L, TimeUnit.MINUTES, 5000L
);
private final @NotNull FallbackQueue queue = FallbackQueue.INSTANCE;
private final @NotNull FallbackRatelimiter ratelimiter = FallbackRatelimiter.INSTANCE;
@@ -50,22 +50,22 @@ public final class Fallback {
@Override
public void info(final String message, final Object... args) {
SONAR.getLogger().info("[Fallback] " + message, args);
SONAR.getLogger().info("[fallback] " + message, args);
}
@Override
public void warn(final String message, final Object... args) {
SONAR.getLogger().warn("[Fallback] " + message, args);
SONAR.getLogger().warn("[fallback] " + message, args);
}
@Override
public void error(final String message, final Object... args) {
SONAR.getLogger().error("[Fallback] " + message, args);
SONAR.getLogger().error("[fallback] " + message, args);
}
};
public boolean isPotentiallyUnderAttack() {
return connected.size() > SONAR.getConfig().MINIMUM_PLAYERS_FOR_ATTACK
|| queue.getQueuedPlayers().size() > SONAR.getConfig().MINIMUM_PLAYERS_FOR_ATTACK;
final int min = SONAR.getConfig().getMinPlayersForAttack();
return Sonar.get().getVerboseHandler().getJoinsPerSecond().estimatedSize() > min;
}
}
@@ -50,7 +50,7 @@ public final class FallbackQueue {
}
public void poll() {
final int max = Sonar.get().getConfig().MAXIMUM_QUEUE_POLLS;
final int max = Sonar.get().getConfig().getQueue().getMaxQueuePolls();
int index = 0;
// We need to be very careful here since we don't want any concurrency issues.
@@ -19,18 +19,21 @@ package xyz.jonesdev.sonar.api.fallback;
import io.netty.channel.Channel;
import io.netty.channel.ChannelPipeline;
import net.kyori.adventure.text.Component;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import xyz.jonesdev.cappuccino.Cappuccino;
import xyz.jonesdev.cappuccino.ExpiringCache;
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.util.concurrent.TimeUnit;
public interface FallbackPlayer<X, Y> {
public interface FallbackUser<X, Y> {
@NotNull Fallback getFallback();
@NotNull X getPlayer();
@@ -49,9 +52,20 @@ public interface FallbackPlayer<X, Y> {
* Kicks the player from the server with
* the given disconnect message.
*
* @param reason Disconnect message
* @param reason Legacy disconnect message string
* @see #disconnect(Component)
*/
void disconnect(final @NotNull String reason);
default void disconnect(final @NotNull String reason) {
disconnect(Component.text(reason));
}
/**
* Kicks the player from the server with
* the given disconnect message.
*
* @param reason Disconnect message component
*/
void disconnect(final @NotNull Component reason);
/**
* Sends a packet/message to the player
@@ -60,6 +74,12 @@ public interface FallbackPlayer<X, Y> {
*/
void write(final @NotNull Object msg);
/**
* Queues a buffered message that will be
* sent once all messages are flushed.
*/
void delayedWrite(final @NotNull Object msg);
ExpiringCache<String> PREVIOUS_FAILS = Cappuccino.buildExpiring(3L, TimeUnit.MINUTES);
/**
@@ -72,10 +92,13 @@ public interface FallbackPlayer<X, Y> {
*/
default void fail(final @Nullable String reason) {
if (getChannel().isActive()) {
disconnect(Sonar.get().getConfig().VERIFICATION_FAILED);
// Call the VerifyFailedEvent for external API usage
Sonar.get().getEventManager().publish(new UserVerifyFailedEvent(this, reason));
disconnect(Sonar.get().getConfig().getVerification().getVerificationFailed());
if (reason != null) {
getFallback().getLogger().info(Sonar.get().getConfig().VERIFICATION_FAILED_LOG
getFallback().getLogger().info(Sonar.get().getConfig().getVerification().getFailedLog()
.replace("%ip%", Sonar.get().getConfig().formatAddress(getInetAddress()))
.replace("%protocol%", String.valueOf(getProtocolVersion().getProtocol()))
.replace("%reason%", reason));
@@ -89,8 +112,11 @@ public interface FallbackPlayer<X, Y> {
// Check if the player has too many failed attempts
if (PREVIOUS_FAILS.has(getInetAddress().toString())) {
// Call the BotBlacklistedEvent for external API usage
Sonar.get().getEventManager().publish(new UserBlacklistedEvent(this));
getFallback().getBlacklisted().put(getInetAddress().toString());
getFallback().getLogger().info(Sonar.get().getConfig().VERIFICATION_BLACKLIST_LOG
getFallback().getLogger().info(Sonar.get().getConfig().getVerification().getBlacklistLog()
.replace("%ip%", Sonar.get().getConfig().formatAddress(getInetAddress()))
.replace("%protocol%", String.valueOf(getProtocolVersion().getProtocol())));
} else {
@@ -17,12 +17,10 @@
package xyz.jonesdev.sonar.api.fallback.protocol;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.*;
@RequiredArgsConstructor
public enum ProtocolVersion {
@@ -66,12 +64,13 @@ public enum ProtocolVersion {
MINECRAFT_1_19_3(761),
MINECRAFT_1_19_4(762),
MINECRAFT_1_20(763),
MINECRAFT_1_20_2(-1);
MINECRAFT_1_20_2(764);
@Getter
private final int protocol;
private final int snapshotProtocol = -1;
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;
@@ -81,7 +80,6 @@ public enum ProtocolVersion {
for (final ProtocolVersion version : values()) {
versions.putIfAbsent(version.protocol, version);
versions.put(version.snapshotProtocol, version);
}
ID_TO_PROTOCOL_CONSTANT = versions;
@@ -96,7 +94,8 @@ public enum ProtocolVersion {
}
}
SUPPORTED_VERSIONS = versions;
SUPPORTED_VERSIONS = Collections.unmodifiableSet(versions);
LATEST_VERSION = new ArrayList<>(versions).get(versions.size() - 1);
}
}
@@ -104,10 +103,6 @@ public enum ProtocolVersion {
return ID_TO_PROTOCOL_CONSTANT.getOrDefault(protocol, UNKNOWN);
}
public int getProtocol() {
return protocol == -1 ? snapshotProtocol : protocol;
}
public boolean isUnknown() {
return this == UNKNOWN;
}
@@ -17,6 +17,10 @@
package xyz.jonesdev.sonar.api.logger;
import org.jetbrains.annotations.NotNull;
import java.lang.reflect.Array;
/**
* The whole purpose of doing this is, so we can have one logger for every module.
* We wouldn't need to get the plugin logger every time we try to use it.
@@ -28,4 +32,62 @@ public interface LoggerWrapper {
void warn(final String message, final Object... args);
void error(final String message, final Object... args);
// Taken from
// https://github.com/j256/ormlite-core/blob/master/src/main/java/com/j256/ormlite/logger/Logger.java
String ARG_STRING = "{}";
int ARG_STRING_LENGTH = ARG_STRING.length();
Object UNKNOWN_ARG = new Object();
default String buildFullMessage(final @NotNull String msg, final Object... args) {
StringBuilder sb = null;
int lastIndex = 0;
int argC = 0;
while (true) {
int argIndex = msg.indexOf(ARG_STRING, lastIndex);
// no more {} arguments?
if (argIndex == -1) {
break;
}
if (sb == null) {
// we build this lazily in case there is no {} in the msg
sb = new StringBuilder(128);
}
// add the string before the arg-string
sb.append(msg, lastIndex, argIndex);
// shift our last-index past the arg-string
lastIndex = argIndex + ARG_STRING_LENGTH;
// add the arguments
if (argC < args.length) {
appendArg(sb, args[argC]);
}
argC++;
}
if (sb == null) {
return msg;
} else {
sb.append(msg, lastIndex, msg.length());
return sb.toString();
}
}
default void appendArg(final StringBuilder stringBuilder, final Object arg) {
if (arg == UNKNOWN_ARG) {
// ignore it
} else if (arg == null) {
stringBuilder.append("null");
} else if (arg.getClass().isArray()) {
stringBuilder.append('[');
int length = Array.getLength(arg);
for (int i = 0; i < length; i++) {
if (i > 0) {
stringBuilder.append(", ");
}
appendArg(stringBuilder, Array.get(arg, i));
}
stringBuilder.append(']');
} else {
stringBuilder.append(arg);
}
}
}
@@ -35,7 +35,8 @@ public final class SystemTimer {
return delay() >= amount;
}
public String formattedDelay() {
@Override
public String toString() {
return String.format("%.3f", delay() / 1000D);
}
}
@@ -19,53 +19,60 @@ package xyz.jonesdev.sonar.api.verbose;
import lombok.Getter;
import lombok.val;
import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.cappuccino.Cappuccino;
import xyz.jonesdev.cappuccino.ExpiringCache;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.profiler.JVMProfiler;
import xyz.jonesdev.sonar.api.statistics.Statistics;
import xyz.jonesdev.sonar.api.timer.SystemTimer;
import java.util.Collection;
import java.util.Map;
import java.util.Vector;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import static xyz.jonesdev.sonar.api.Sonar.DECIMAL_FORMAT;
import static xyz.jonesdev.sonar.api.fallback.traffic.TrafficCounter.INCOMING;
import static xyz.jonesdev.sonar.api.fallback.traffic.TrafficCounter.OUTGOING;
@Getter
public abstract class Verbose implements JVMProfiler {
protected final @NotNull Collection<String> subscribers = new Vector<>(0);
private final SystemTimer secondTimer = new SystemTimer();
protected int joinsPerSecond, totalJoins;
private int lastTotalJoins, animationIndex;
public final class Verbose implements JVMProfiler {
private final @NotNull Collection<String> subscribers = new Vector<>(0);
private final @NotNull Map<String, Audience> audiences = new ConcurrentHashMap<>();
private int animationIndex;
private final ExpiringCache<Long> joinsPerSecond = Cappuccino.buildExpiring(1L, TimeUnit.SECONDS);
// Run action bar verbose
public final void update() {
public void update() {
// Clean up all blacklisted IPs
Sonar.get().getFallback().getBlacklisted().cleanUp(false);
joinsPerSecond.cleanUp(false);
totalJoins = Statistics.TOTAL_TRAFFIC.get();
// Statistically determine the joins per second without any caches
if (totalJoins > 0 && secondTimer.delay() >= 1000L) {
secondTimer.reset();
joinsPerSecond = totalJoins - lastTotalJoins;
lastTotalJoins = totalJoins;
}
// 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
broadcast(prepareActionBarFormat());
for (final String subscriber : subscribers) {
final Audience audience = audiences.get(subscriber);
if (audience == null) continue;
audience.sendActionBar(component);
}
}
protected final @NotNull String prepareActionBarFormat() {
return Sonar.get().getConfig().ACTION_BAR_LAYOUT
public @NotNull Component prepareActionBarFormat() {
return MiniMessage.miniMessage().deserialize(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().getBlacklisted().estimatedSize()))
.replace("%total-joins%", DECIMAL_FORMAT.format(totalJoins))
.replace("%per-second-joins%", DECIMAL_FORMAT.format(joinsPerSecond))
.replace("%total-joins%", DECIMAL_FORMAT.format(Statistics.TOTAL_TRAFFIC.get()))
.replace("%per-second-joins%", DECIMAL_FORMAT.format(joinsPerSecond.estimatedSize()))
.replace("%verify-total%", DECIMAL_FORMAT.format(Statistics.REAL_TRAFFIC.get()))
.replace("%verify-success%",
DECIMAL_FORMAT.format(Sonar.get().getVerifiedPlayerController().estimatedSize()))
@@ -78,37 +85,34 @@ public abstract class Verbose implements JVMProfiler {
.replace("%free-memory%", formatMemory(getFreeMemory()))
.replace("%total-memory%", formatMemory(getTotalMemory()))
.replace("%max-memory%", formatMemory(getMaxMemory()))
.replace("%animation%", nextAnimation());
.replace("%animation%", nextAnimation()));
}
protected final String nextAnimation() {
val animations = Sonar.get().getConfig().ANIMATION;
public String nextAnimation() {
val animations = Sonar.get().getConfig().getVerbose().getAnimation();
final int nextIndex = ++animationIndex % animations.size();
return animations.toArray(new String[0])[nextIndex];
}
// Run action bar verbose
protected abstract void broadcast(final String message);
/**
* @param subscriber Name of the player who subscribed
* @return Whether the player is subscribed or not
* @param name Name of the audience
* @return Whether the audience is subscribed or not
*/
public boolean isSubscribed(final @NotNull String subscriber) {
return subscribers.contains(subscriber);
public boolean isSubscribed(final @NotNull String name) {
return subscribers.contains(name);
}
/**
* @param subscriber Name of the player to subscribe
* @param name Name of the audience to subscribe
*/
public void subscribe(final @NotNull String subscriber) {
subscribers.add(subscriber);
public void subscribe(final String name) {
subscribers.add(name);
}
/**
* @param subscriber Name of the player to unsubscribe
* @param name Name of the audience to unsubscribe
*/
public void unsubscribe(final @NotNull String subscriber) {
subscribers.remove(subscriber);
public void unsubscribe(final String name) {
subscribers.remove(name);
}
}
+6 -4
View File
@@ -7,17 +7,16 @@ plugins {
apply(plugin = "net.minecrell.plugin-yml.bukkit")
bukkit {
name = rootProject.name
version = rootProject.version.toString()
name = "Sonar"
version = rootProject.version.toString().split("-")[0]
main = "xyz.jonesdev.sonar.bukkit.SonarBukkitPlugin"
authors = listOf("Jones Development", "Sonar Contributors")
website = "https://jonesdev.xyz/discord/"
description = "Effective Anti-bot plugin for Velocity, BungeeCord, and Bukkit (1.7-latest)"
load = BukkitPluginDescription.PluginLoadOrder.POSTWORLD
commands {
register("sonar") {
permission = "sonar.command"
// No permission check here
}
}
}
@@ -31,6 +30,9 @@ dependencies {
compileOnly(project(":api"))
compileOnly(project(":common"))
// MiniMessage platform support
implementation("net.kyori:adventure-platform-bukkit:4.3.1")
// We have to use 1.8 for backwards compatibility
compileOnly("org.spigotmc:spigot-api:1.8.8-R0.1-SNAPSHOT")
@@ -18,30 +18,32 @@
package xyz.jonesdev.sonar.bukkit;
import lombok.Getter;
import net.kyori.adventure.platform.bukkit.BukkitAudiences;
import org.bstats.bukkit.Metrics;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.SonarPlatform;
import xyz.jonesdev.sonar.api.command.InvocationSender;
import xyz.jonesdev.sonar.api.fallback.traffic.TrafficCounter;
import xyz.jonesdev.sonar.api.logger.LoggerWrapper;
import xyz.jonesdev.sonar.api.server.ServerWrapper;
import xyz.jonesdev.sonar.bukkit.command.SonarCommand;
import xyz.jonesdev.sonar.bukkit.verbose.ActionBarVerbose;
import xyz.jonesdev.sonar.bukkit.audience.AudienceListener;
import xyz.jonesdev.sonar.bukkit.command.BukkitSonarCommand;
import xyz.jonesdev.sonar.common.boot.SonarBootstrap;
import java.util.Objects;
import java.util.Optional;
import java.util.logging.Level;
@Getter
public final class SonarBukkit extends SonarBootstrap<SonarBukkitPlugin> {
public static SonarBukkit INSTANCE;
public SonarBukkit(final @NotNull SonarBukkitPlugin plugin) {
super(plugin, plugin.getDataFolder(), new ActionBarVerbose(plugin.getServer()));
super(plugin, plugin.getDataFolder(), SonarPlatform.BUKKIT);
INSTANCE = this;
}
/**
* Wrapper for Bukkit audiences
*/
private final BukkitAudiences bukkitAudiences = BukkitAudiences.create(getPlugin());
/**
* Create a wrapper for the plugin logger, so we can use it outside
* the velocity module.
@@ -52,64 +54,31 @@ public final class SonarBukkit extends SonarBootstrap<SonarBukkitPlugin> {
@Override
public void info(final String message, final Object... args) {
getPlugin().getLogger().log(Level.INFO, message, args);
getPlugin().getLogger().info(buildFullMessage(message, args));
}
@Override
public void warn(final String message, final Object... args) {
getPlugin().getLogger().log(Level.WARNING, message, args);
getPlugin().getLogger().warning(buildFullMessage(message, args));
}
@Override
public void error(final String message, final Object... args) {
getPlugin().getLogger().log(Level.SEVERE, message, args);
}
};
/**
* Create a wrapper object for our server, so we can use it outside
* the velocity module.
* <br>
* We have to do this, so we can access all necessary API functions.
*/
public final ServerWrapper server = new ServerWrapper() {
@Override
public SonarPlatform getPlatform() {
return SonarPlatform.VELOCITY;
}
@Override
public Optional<InvocationSender> getOnlinePlayer(final String username) {
return getPlugin().getServer().getOnlinePlayers().stream()
.filter(player -> player.getName().equalsIgnoreCase(username))
.findFirst()
.map(player -> new InvocationSender() {
@Override
public String getName() {
return player.getName();
}
@Override
public void sendMessage(final String message) {
player.sendMessage(message);
}
});
getPlugin().getLogger().severe(buildFullMessage(message, args));
}
};
@Override
public void enable() {
// Reload configuration
reload();
// Initialize bStats.org metrics
new Metrics(getPlugin(), getServer().getPlatform().getMetricsId());
new Metrics(getPlugin(), getPlatform().getMetricsId());
// Register Sonar command
Objects.requireNonNull(getPlugin().getCommand("sonar")).setExecutor(new SonarCommand());
Objects.requireNonNull(getPlugin().getCommand("sonar")).setExecutor(new BukkitSonarCommand());
// Register audience register listener
getPlugin().getServer().getPluginManager().registerEvents(new AudienceListener(), getPlugin());
// Register Fallback queue task
getPlugin().getServer().getScheduler().runTaskTimerAsynchronously(getPlugin(), getFallback().getQueue()::poll,
@@ -123,9 +92,4 @@ public final class SonarBukkit extends SonarBootstrap<SonarBukkitPlugin> {
getPlugin().getServer().getScheduler().runTaskTimerAsynchronously(getPlugin(), getVerboseHandler()::update,
2L, 2L);
}
@Override
public void postReload() {
// Do nothing
}
}
@@ -0,0 +1,42 @@
/*
* Copyright (C) 2023 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.LOWEST)
public void handle(final @NotNull PlayerJoinEvent event) {
final Audience audience = SonarBukkit.INSTANCE.getBukkitAudiences().player(event.getPlayer());
Sonar.get().getVerboseHandler().getAudiences().put(event.getPlayer().getName(), audience);
}
@EventHandler(priority = EventPriority.LOWEST)
public void handle(final @NotNull PlayerQuitEvent event) {
Sonar.get().getVerboseHandler().getAudiences().remove(event.getPlayer().getName());
}
}
@@ -15,28 +15,16 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.bukkit.verbose;
package xyz.jonesdev.sonar.bukkit.command;
import lombok.RequiredArgsConstructor;
import net.md_5.bungee.api.chat.TextComponent;
import org.bukkit.Server;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import xyz.jonesdev.sonar.api.verbose.Verbose;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.command.InvocationSource;
import xyz.jonesdev.sonar.bukkit.SonarBukkit;
@RequiredArgsConstructor
public final class ActionBarVerbose extends Verbose {
private final Server server;
public void broadcast(final String message) {
final TextComponent component = new TextComponent(message);
synchronized (subscribers) {
for (final String subscriber : subscribers) {
final Player player = server.getPlayer(subscriber);
if (player != null) {
//player.sendMessage(ChatMessageType.ACTION_BAR, component);
}
}
}
public final class BukkitInvocationSource extends InvocationSource {
public BukkitInvocationSource(final @NotNull CommandSender sender) {
super(sender.getName(), SonarBukkit.INSTANCE.getBukkitAudiences().sender(sender), sender instanceof Player);
}
}
@@ -0,0 +1,123 @@
/*
* Copyright (C) 2023 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.command;
import net.kyori.adventure.text.Component;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabExecutor;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.command.InvocationSource;
import xyz.jonesdev.sonar.api.command.SonarCommand;
import xyz.jonesdev.sonar.api.command.subcommand.Subcommand;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import static java.util.Collections.emptyList;
public final class BukkitSonarCommand implements CommandExecutor, TabExecutor, SonarCommand {
@Override
public boolean onCommand(final CommandSender sender,
final Command command,
final String label,
final String[] args) {
// Create our own invocation source wrapper to handle messages properly
final InvocationSource invocationSource = new BukkitInvocationSource(sender);
if (invocationSource.isPlayer()) {
// Check if the player actually has the permission to run the command
if (!sender.hasPermission("sonar.command")) {
invocationSource.sendMessage(Sonar.get().getConfig().getNoPermission());
return false;
}
// Checking if it contains will only break more since it can throw
// a NullPointerException if the cache is being accessed from parallel threads
DELAY.cleanUp(); // Clean up the cache
final long mapTimestamp = DELAY.asMap().getOrDefault(sender, -1L);
// There were some exploits with spamming commands in the past.
// Spamming should be prevented, especially if some heavy operations are done,
// which is not the case here but let's still stay safe!
if (mapTimestamp > 0L) {
invocationSource.sendMessage(Sonar.get().getConfig().getCommands().getCommandCoolDown());
// Format delay
final long timestamp = System.currentTimeMillis();
final double left = 0.5D - (timestamp - mapTimestamp) / 1000D;
invocationSource.sendMessage(Sonar.get().getConfig().getCommands().getCommandCoolDownLeft()
.replace("%time-left%", Sonar.DECIMAL_FORMAT.format(left)));
return false;
}
DELAY.put(sender);
}
Optional<Subcommand> subcommand = Optional.empty();
if (args.length > 0) {
// Search subcommand if command arguments are present
subcommand = Sonar.get().getSubcommandRegistry().getSubcommands().parallelStream()
.filter(sub -> sub.getInfo().name().equalsIgnoreCase(args[0])
|| Arrays.stream(sub.getInfo().aliases())
.anyMatch(alias -> alias.equalsIgnoreCase(args[0])))
.findFirst();
// Check permissions for subcommands
if (subcommand.isPresent()) {
if (!subcommand.get().getInfo().onlyConsole()
&& !sender.hasPermission(subcommand.get().getPermission())
) {
invocationSource.sendMessage(Sonar.get().getConfig().getCommands().getSubCommandNoPerm()
.replace("%permission%", subcommand.get().getPermission()));
return false;
}
}
}
if (!subcommand.isPresent()) {
// Re-use the old, cached help message since we don't want to scan
// for each subcommand and it's arguments/attributes every time
// someone runs /sonar since the subcommand don't change
for (final Component component : CACHED_HELP_MESSAGE) {
invocationSource.sendMessage(component);
}
} else {
subcommand.get().invoke(invocationSource, args);
}
return false;
}
@Override
public List<String> onTabComplete(final @NotNull CommandSender sender,
final Command command,
final String commandAlias,
final String @NotNull [] args) {
// Do not allow tab completion if the player does not have the required permission
if (!sender.hasPermission("sonar.command")) {
return emptyList();
}
return getCachedTabSuggestions(args);
}
}
@@ -1,245 +0,0 @@
/*
* Copyright (C) 2023 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.command;
import net.md_5.bungee.api.ChatColor;
import net.md_5.bungee.api.chat.ClickEvent;
import net.md_5.bungee.api.chat.ComponentBuilder;
import net.md_5.bungee.api.chat.HoverEvent;
import net.md_5.bungee.api.chat.TextComponent;
import org.bukkit.command.*;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.command.CommandInvocation;
import xyz.jonesdev.sonar.api.command.InvocationSender;
import xyz.jonesdev.sonar.api.command.SonarBaseCommand;
import xyz.jonesdev.sonar.api.command.subcommand.Subcommand;
import xyz.jonesdev.sonar.api.command.subcommand.argument.Argument;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import static java.util.Collections.emptyList;
public final class SonarCommand implements CommandExecutor, TabExecutor, SonarBaseCommand {
private static final TextComponent GITHUB_LINK_COMPONENT = new TextComponent(ChatColor.GREEN + "https://github" +
".com/jonesdevelopment/sonar");
private static final TextComponent DISCORD_SUPPORT = new TextComponent(ChatColor.YELLOW + "Open a ticket on the " +
"Discord ");
private static final TextComponent GITHUB_ISSUES = new TextComponent(ChatColor.YELLOW + "or open a new issue on " +
"GitHub.");
static {
GITHUB_LINK_COMPONENT.setClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, "https://github" +
".com/jonesdevelopment/sonar"));
DISCORD_SUPPORT.setClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, "https://jonesdev.xyz/discord/"));
DISCORD_SUPPORT.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new ComponentBuilder("(Click to open " +
"Discord)").create()));
GITHUB_ISSUES.setClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, "https://github.com/jonesdevelopment/sonar" +
"/issues"));
GITHUB_ISSUES.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new ComponentBuilder("(Click to open " +
"GitHub)").create()));
}
static {
CACHED_HELP_MESSAGE.addAll(Arrays.asList(
new TextComponent(ChatColor.YELLOW + "Running Sonar " + Sonar.get().getVersion()
+ " on " + Sonar.get().getServer().getPlatform().getDisplayName()
+ "."),
new TextComponent(ChatColor.YELLOW + "(C) " + COPYRIGHT_YEAR + " Jones Development and Sonar Contributors"),
GITHUB_LINK_COMPONENT,
new TextComponent(""),
new TextComponent(ChatColor.YELLOW + "Need help or have any questions?"),
new TextComponent(
DISCORD_SUPPORT,
GITHUB_ISSUES
),
new TextComponent("")
));
Sonar.get().getSubcommandRegistry().getSubcommands().forEach(sub -> {
final TextComponent component = new TextComponent(
new TextComponent(ChatColor.GRAY + " ▪ "),
new TextComponent(ChatColor.GREEN + "/sonar " + sub.getInfo().name()),
new TextComponent(ChatColor.GRAY + " - "),
new TextComponent(ChatColor.WHITE + sub.getInfo().description())
);
component.setClickEvent(
new ClickEvent(ClickEvent.Action.SUGGEST_COMMAND, "/sonar " + sub.getInfo().name() + " ")
);
component.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new ComponentBuilder(
"§7Only players: §f" + (sub.getInfo().onlyPlayers() ? "§a✔" : "§c✗")
+ Sonar.LINE_SEPARATOR + "§7Require console: §f" + (sub.getInfo().onlyConsole() ? "§a✔" : "§c✗")
+ Sonar.LINE_SEPARATOR + "§7Permission: §f" + sub.getPermission()
+ Sonar.LINE_SEPARATOR + "§7Aliases: §f" + sub.getAliases()
).create())
);
CACHED_HELP_MESSAGE.add(component);
});
}
@Override
public boolean onCommand(final CommandSender sender,
final Command command,
final String label,
final String[] args) {
if (!(sender instanceof ConsoleCommandSender)) {
// Checking if it contains will only break more since it can throw
// a NullPointerException if the cache is being accessed from parallel threads
DELAY.cleanUp(); // Clean up the cache
final long mapTimestamp = DELAY.asMap().getOrDefault(sender, -1L);
// There were some exploits with spamming commands in the past.
// Spamming should be prevented, especially if some heavy operations are done,
// which is not the case here but let's still stay safe!
if (mapTimestamp > 0L) {
sender.sendMessage(Sonar.get().getConfig().COMMAND_COOL_DOWN);
// Format delay
final long timestamp = System.currentTimeMillis();
final double left = 0.5D - (timestamp - mapTimestamp) / 1000D;
sender.sendMessage(
Sonar.get().getConfig().COMMAND_COOL_DOWN_LEFT
.replace("%time-left%", Sonar.DECIMAL_FORMAT.format(left))
);
return false;
}
DELAY.put(sender);
}
Optional<Subcommand> subcommand = Optional.empty();
final InvocationSender invocationSender = new InvocationSender() {
@Override
public String getName() {
return sender.getName();
}
@Override
public void sendMessage(final String message) {
sender.sendMessage(message);
}
};
if (args.length > 0) {
// Search subcommand if command arguments are present
subcommand = Sonar.get().getSubcommandRegistry().getSubcommands().parallelStream()
.filter(sub -> sub.getInfo().name().equalsIgnoreCase(args[0])
|| Arrays.stream(sub.getInfo().aliases())
.anyMatch(alias -> alias.equalsIgnoreCase(args[0])))
.findFirst();
// Check permissions for subcommands
if (subcommand.isPresent()) {
if (!subcommand.get().getInfo().onlyConsole()
&& !sender.hasPermission(subcommand.get().getPermission())
) {
invocationSender.sendMessage(
Sonar.get().getConfig().SUB_COMMAND_NO_PERM
.replace("%permission%", subcommand.get().getPermission())
);
return false;
}
}
}
subcommand.ifPresent(sub -> {
if (sub.getInfo().onlyPlayers() && !(sender instanceof Player)) {
invocationSender.sendMessage(Sonar.get().getConfig().PLAYERS_ONLY);
return;
}
if (sub.getInfo().onlyConsole() && !(sender instanceof ConsoleCommandSender)) {
invocationSender.sendMessage(Sonar.get().getConfig().CONSOLE_ONLY);
return;
}
final CommandInvocation commandInvocation = new CommandInvocation(invocationSender, sub, args);
// The subcommands has arguments which are not present in the executed command
if (sub.getInfo().arguments().length > 0
&& commandInvocation.getRawArguments().length <= 1) {
invocationSender.sendMessage(
Sonar.get().getConfig().INCORRECT_COMMAND_USAGE
.replace("%usage%", sub.getInfo().name() + " (" + sub.getArguments() + ")")
);
return;
}
// Execute the sub command with the custom invocation properties
sub.execute(commandInvocation);
});
if (!subcommand.isPresent()) {
// Re-use the old, cached help message since we don't want to scan
// for each subcommand and it's arguments/attributes every time
// someone runs /sonar since the subcommand don't change
for (final Object component : CACHED_HELP_MESSAGE) {
if (sender instanceof Player) {
((Player) sender).spigot().sendMessage((TextComponent) component);
} else {
sender.sendMessage(((TextComponent) component).getText());
}
}
}
return false;
}
@Override
public List<String> onTabComplete(final CommandSender sender,
final Command command,
final String commandAlias,
final String @NotNull [] args) {
if (args.length <= 1) {
if (TAB_SUGGESTIONS.isEmpty()) {
for (final Subcommand subcommand : Sonar.get().getSubcommandRegistry().getSubcommands()) {
TAB_SUGGESTIONS.add(subcommand.getInfo().name());
if (subcommand.getInfo().aliases().length > 0) {
TAB_SUGGESTIONS.addAll(Arrays.asList(subcommand.getInfo().aliases()));
}
}
}
return TAB_SUGGESTIONS;
} else if (args.length == 2) {
if (ARG_TAB_SUGGESTIONS.isEmpty()) {
for (final Subcommand subcommand : Sonar.get().getSubcommandRegistry().getSubcommands()) {
final List<String> parsedArguments = Arrays.stream(subcommand.getInfo().arguments())
.map(Argument::value)
.collect(Collectors.toList());
ARG_TAB_SUGGESTIONS.put(subcommand.getInfo().name(), parsedArguments);
for (final String alias : subcommand.getInfo().aliases()) {
ARG_TAB_SUGGESTIONS.put(alias, parsedArguments);
}
}
}
final String subCommandName = args[0].toLowerCase();
return ARG_TAB_SUGGESTIONS.getOrDefault(subCommandName, emptyList());
}
return emptyList();
}
}
+6 -10
View File
@@ -5,25 +5,21 @@ plugins {
apply(plugin = "net.minecrell.plugin-yml.bungee")
bungee {
name = rootProject.name
version = rootProject.version.toString()
main = "xyz.jonesdev.sonar.bukkit.SonarBukkitPlugin"
name = "Sonar"
version = rootProject.version.toString().split("-")[0]
main = "xyz.jonesdev.sonar.bungee.SonarBungeePlugin"
author = "Jones Development, Sonar Contributors"
description = "Effective Anti-bot plugin for Velocity, BungeeCord, and Bukkit (1.7-latest)"
}
dependencies {
compileOnly(project(":api"))
compileOnly(project(":common"))
implementation("com.velocitypowered:velocity-native:1.1.9") {
exclude(group = "com.google.guava")
exclude(group = "io.netty")
exclude(group = "org.checkerframework")
}
compileOnly("net.md_5:bungeecord:1.20.2-rc2-SNAPSHOT")
// MiniMessage platform support
implementation("net.kyori:adventure-platform-bungeecord:4.3.1")
// Implement bStats.org for metrics
implementation("org.bstats:bstats-bungeecord:3.0.2")
}
@@ -18,34 +18,35 @@
package xyz.jonesdev.sonar.bungee;
import lombok.Getter;
import net.md_5.bungee.api.chat.TextComponent;
import net.kyori.adventure.platform.bungeecord.BungeeAudiences;
import org.bstats.bungeecord.Metrics;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.SonarPlatform;
import xyz.jonesdev.sonar.api.command.InvocationSender;
import xyz.jonesdev.sonar.api.fallback.traffic.TrafficCounter;
import xyz.jonesdev.sonar.api.logger.LoggerWrapper;
import xyz.jonesdev.sonar.api.server.ServerWrapper;
import xyz.jonesdev.sonar.bungee.command.SonarCommand;
import xyz.jonesdev.sonar.bungee.audience.AudienceListener;
import xyz.jonesdev.sonar.bungee.command.BungeeSonarCommand;
import xyz.jonesdev.sonar.bungee.fallback.FallbackListener;
import xyz.jonesdev.sonar.bungee.fallback.injection.BaseInjectionHelper;
import xyz.jonesdev.sonar.bungee.fallback.injection.ChildChannelInitializer;
import xyz.jonesdev.sonar.bungee.verbose.VerboseWrapper;
import xyz.jonesdev.sonar.common.boot.SonarBootstrap;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
@Getter
public final class SonarBungee extends SonarBootstrap<SonarBungeePlugin> {
public static SonarBungee INSTANCE;
public SonarBungee(final @NotNull SonarBungeePlugin plugin) {
super(plugin, plugin.getDataFolder(), new VerboseWrapper(plugin.getServer()));
super(plugin, plugin.getDataFolder(), SonarPlatform.BUNGEE);
INSTANCE = this;
}
/**
* Wrapper for BungeeCord audiences
*/
private final BungeeAudiences bungeeAudiences = BungeeAudiences.create(getPlugin());
/**
* Create a wrapper for the plugin logger, so we can use it outside
* the velocity module.
@@ -56,67 +57,34 @@ public final class SonarBungee extends SonarBootstrap<SonarBungeePlugin> {
@Override
public void info(final String message, final Object... args) {
getPlugin().getLogger().log(Level.INFO, message, args);
getPlugin().getLogger().info(buildFullMessage(message, args));
}
@Override
public void warn(final String message, final Object... args) {
getPlugin().getLogger().log(Level.WARNING, message, args);
getPlugin().getLogger().warning(buildFullMessage(message, args));
}
@Override
public void error(final String message, final Object... args) {
getPlugin().getLogger().log(Level.SEVERE, message, args);
}
};
/**
* Create a wrapper object for our server, so we can use it outside
* the velocity module.
* <br>
* We have to do this, so we can access all necessary API functions.
*/
public final ServerWrapper server = new ServerWrapper() {
@Override
public SonarPlatform getPlatform() {
return SonarPlatform.BUNGEE;
}
@Override
public Optional<InvocationSender> getOnlinePlayer(final String username) {
return getPlugin().getServer().getPlayers().stream()
.filter(player -> player.getName().equalsIgnoreCase(username))
.findFirst()
.map(player -> new InvocationSender() {
@Override
public String getName() {
return player.getName();
}
@Override
public void sendMessage(final String message) {
player.sendMessage(new TextComponent(message));
}
});
getPlugin().getLogger().severe(buildFullMessage(message, args));
}
};
@Override
public void enable() {
// Reload configuration
reload();
// Initialize bStats.org metrics
new Metrics(getPlugin(), getServer().getPlatform().getMetricsId());
new Metrics(getPlugin(), getPlatform().getMetricsId());
// Register Sonar command
getPlugin().getServer().getPluginManager().registerCommand(getPlugin(), new SonarCommand());
getPlugin().getServer().getPluginManager().registerCommand(getPlugin(), new BungeeSonarCommand());
// Register Fallback listener
getPlugin().getServer().getPluginManager().registerListener(getPlugin(), new FallbackListener(getFallback()));
getPlugin().getServer().getPluginManager().registerListener(getPlugin(), new FallbackListener());
// Register audience register listener
getPlugin().getServer().getPluginManager().registerListener(getPlugin(), new AudienceListener());
// Register Fallback queue task
getPlugin().getServer().getScheduler().schedule(getPlugin(), getFallback().getQueue()::poll,
@@ -133,11 +101,4 @@ public final class SonarBungee extends SonarBootstrap<SonarBungeePlugin> {
// Inject base into ProtocolUtils
BaseInjectionHelper.inject(ChildChannelInitializer.INSTANCE);
}
@Override
public void postReload() {
// Prepare cached messages
FallbackListener.CachedMessages.update();
}
}
@@ -0,0 +1,42 @@
/*
* Copyright (C) 2023 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.LOWEST)
public void handle(final @NotNull PostLoginEvent event) {
final Audience audience = SonarBungee.INSTANCE.getBungeeAudiences().player(event.getPlayer());
Sonar.get().getVerboseHandler().getAudiences().put(event.getPlayer().getName(), audience);
}
@EventHandler(priority = EventPriority.LOWEST)
public void handle(final @NotNull PlayerDisconnectEvent event) {
Sonar.get().getVerboseHandler().getAudiences().remove(event.getPlayer().getName());
}
}
@@ -0,0 +1,30 @@
/*
* Copyright (C) 2023 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.command;
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 BungeeInvocationSource extends InvocationSource {
public BungeeInvocationSource(final @NotNull CommandSender sender) {
super(sender.getName(), SonarBungee.INSTANCE.getBungeeAudiences().sender(sender), sender instanceof ProxiedPlayer);
}
}
@@ -0,0 +1,118 @@
/*
* Copyright (C) 2023 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.command;
import net.kyori.adventure.text.Component;
import net.md_5.bungee.api.CommandSender;
import net.md_5.bungee.api.plugin.Command;
import net.md_5.bungee.api.plugin.TabExecutor;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.command.InvocationSource;
import xyz.jonesdev.sonar.api.command.SonarCommand;
import xyz.jonesdev.sonar.api.command.subcommand.Subcommand;
import java.util.Arrays;
import java.util.Optional;
import static java.util.Collections.emptyList;
public final class BungeeSonarCommand extends Command implements TabExecutor, SonarCommand {
public BungeeSonarCommand() {
super("sonar");
}
@Override
@SuppressWarnings({"redundantSuppression"})
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);
if (invocationSource.isPlayer()) {
// Check if the player actually has the permission to run the command
if (!sender.hasPermission("sonar.command")) {
invocationSource.sendMessage(Sonar.get().getConfig().getNoPermission());
return;
}
// Checking if it contains will only break more since it can throw
// a NullPointerException if the cache is being accessed from parallel threads
DELAY.cleanUp(); // Clean up the cache
final long mapTimestamp = DELAY.asMap().getOrDefault(sender, -1L);
// There were some exploits with spamming commands in the past.
// Spamming should be prevented, especially if some heavy operations are done,
// which is not the case here but let's still stay safe!
if (mapTimestamp > 0L) {
invocationSource.sendMessage(Sonar.get().getConfig().getCommands().getCommandCoolDown());
// Format delay
final long timestamp = System.currentTimeMillis();
final double left = 0.5D - (timestamp - mapTimestamp) / 1000D;
invocationSource.sendMessage(Sonar.get().getConfig().getCommands().getCommandCoolDownLeft()
.replace("%time-left%", Sonar.DECIMAL_FORMAT.format(left)));
return;
}
DELAY.put(sender);
}
Optional<Subcommand> subcommand = Optional.empty();
if (args.length > 0) {
// Search subcommand if command arguments are present
subcommand = Sonar.get().getSubcommandRegistry().getSubcommands().parallelStream()
.filter(sub -> sub.getInfo().name().equalsIgnoreCase(args[0])
|| Arrays.stream(sub.getInfo().aliases())
.anyMatch(alias -> alias.equalsIgnoreCase(args[0])))
.findFirst();
// Check permissions for subcommands
if (subcommand.isPresent()) {
if (!subcommand.get().getInfo().onlyConsole()
&& !sender.hasPermission(subcommand.get().getPermission())
) {
invocationSource.sendMessage(Sonar.get().getConfig().getCommands().getSubCommandNoPerm()
.replace("%permission%", subcommand.get().getPermission()));
return;
}
}
}
if (!subcommand.isPresent()) {
// Re-use the old, cached help message since we don't want to scan
// for each subcommand and it's arguments/attributes every time
// someone runs /sonar since the subcommand don't change
for (final Component component : CACHED_HELP_MESSAGE) {
invocationSource.sendMessage(component);
}
} else {
subcommand.get().invoke(invocationSource, args);
}
}
@Override
public Iterable<String> onTabComplete(final @NotNull CommandSender sender, final String @NotNull [] args) {
// Do not allow tab completion if the player does not have the required permission
if (!sender.hasPermission("sonar.command")) {
return emptyList();
}
return getCachedTabSuggestions(args);
}
}
@@ -1,242 +0,0 @@
/*
* Copyright (C) 2023 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.command;
import net.md_5.bungee.api.ChatColor;
import net.md_5.bungee.api.CommandSender;
import net.md_5.bungee.api.chat.ClickEvent;
import net.md_5.bungee.api.chat.ComponentBuilder;
import net.md_5.bungee.api.chat.HoverEvent;
import net.md_5.bungee.api.chat.TextComponent;
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 net.md_5.bungee.command.ConsoleCommandSender;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.command.CommandInvocation;
import xyz.jonesdev.sonar.api.command.InvocationSender;
import xyz.jonesdev.sonar.api.command.SonarBaseCommand;
import xyz.jonesdev.sonar.api.command.subcommand.Subcommand;
import xyz.jonesdev.sonar.api.command.subcommand.argument.Argument;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import static java.util.Collections.emptyList;
public final class SonarCommand extends Command implements TabExecutor, SonarBaseCommand {
public SonarCommand() {
super("sonar", "sonar.command");
}
private static final TextComponent GITHUB_LINK_COMPONENT = new TextComponent(ChatColor.GREEN + "https://github" +
".com/jonesdevelopment/sonar");
private static final TextComponent DISCORD_SUPPORT = new TextComponent(ChatColor.YELLOW + "Open a ticket on the " +
"Discord ");
private static final TextComponent GITHUB_ISSUES = new TextComponent(ChatColor.YELLOW + "or open a new issue on " +
"GitHub.");
static {
GITHUB_LINK_COMPONENT.setClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, "https://github" +
".com/jonesdevelopment/sonar"));
DISCORD_SUPPORT.setClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, "https://jonesdev.xyz/discord/"));
DISCORD_SUPPORT.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new ComponentBuilder("(Click to open " +
"Discord)").create()));
GITHUB_ISSUES.setClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, "https://github.com/jonesdevelopment/sonar" +
"/issues"));
GITHUB_ISSUES.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new ComponentBuilder("(Click to open " +
"GitHub)").create()));
}
static {
CACHED_HELP_MESSAGE.addAll(Arrays.asList(
new TextComponent(ChatColor.YELLOW + "Running Sonar " + Sonar.get().getVersion()
+ " on " + Sonar.get().getServer().getPlatform().getDisplayName()
+ "."),
new TextComponent(ChatColor.YELLOW + "(C) " + COPYRIGHT_YEAR + " Jones Development and Sonar Contributors"),
GITHUB_LINK_COMPONENT,
new TextComponent(""),
new TextComponent(ChatColor.YELLOW + "Need help or have any questions?"),
new TextComponent(
DISCORD_SUPPORT,
GITHUB_ISSUES
),
new TextComponent("")
));
Sonar.get().getSubcommandRegistry().getSubcommands().forEach(sub -> {
final TextComponent component = new TextComponent(
new TextComponent(ChatColor.GRAY + " ▪ "),
new TextComponent(ChatColor.GREEN + "/sonar " + sub.getInfo().name()),
new TextComponent(ChatColor.GRAY + " - "),
new TextComponent(ChatColor.WHITE + sub.getInfo().description())
);
component.setClickEvent(
new ClickEvent(ClickEvent.Action.SUGGEST_COMMAND, "/sonar " + sub.getInfo().name() + " ")
);
component.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new ComponentBuilder(
"§7Only players: §f" + (sub.getInfo().onlyPlayers() ? "§a✔" : "§c✗")
+ Sonar.LINE_SEPARATOR + "§7Require console: §f" + (sub.getInfo().onlyConsole() ? "§a✔" : "§c✗")
+ Sonar.LINE_SEPARATOR + "§7Permission: §f" + sub.getPermission()
+ Sonar.LINE_SEPARATOR + "§7Aliases: §f" + sub.getAliases()
).create())
);
CACHED_HELP_MESSAGE.add(component);
});
}
@Override
@SuppressWarnings({"redundantSuppression"})
public void execute(final @NotNull CommandSender sender, final String[] args) {
if (!(sender instanceof ConsoleCommandSender)) {
// Checking if it contains will only break more since it can throw
// a NullPointerException if the cache is being accessed from parallel threads
DELAY.cleanUp(); // Clean up the cache
final long mapTimestamp = DELAY.asMap().getOrDefault(sender, -1L);
// There were some exploits with spamming commands in the past.
// Spamming should be prevented, especially if some heavy operations are done,
// which is not the case here but let's still stay safe!
if (mapTimestamp > 0L) {
sender.sendMessage(new TextComponent(Sonar.get().getConfig().COMMAND_COOL_DOWN));
// Format delay
final long timestamp = System.currentTimeMillis();
final double left = 0.5D - (timestamp - mapTimestamp) / 1000D;
sender.sendMessage(new TextComponent(
Sonar.get().getConfig().COMMAND_COOL_DOWN_LEFT
.replace("%time-left%", Sonar.DECIMAL_FORMAT.format(left))
));
return;
}
DELAY.put(sender);
}
Optional<Subcommand> subcommand = Optional.empty();
final InvocationSender invocationSender = new InvocationSender() {
@Override
public String getName() {
return sender.getName();
}
@Override
public void sendMessage(final String message) {
sender.sendMessage(new TextComponent(message));
}
};
if (args.length > 0) {
// Search subcommand if command arguments are present
subcommand = Sonar.get().getSubcommandRegistry().getSubcommands().parallelStream()
.filter(sub -> sub.getInfo().name().equalsIgnoreCase(args[0])
|| Arrays.stream(sub.getInfo().aliases())
.anyMatch(alias -> alias.equalsIgnoreCase(args[0])))
.findFirst();
// Check permissions for subcommands
if (subcommand.isPresent()) {
if (!subcommand.get().getInfo().onlyConsole()
&& !sender.hasPermission(subcommand.get().getPermission())
) {
invocationSender.sendMessage(
Sonar.get().getConfig().SUB_COMMAND_NO_PERM
.replace("%permission%", subcommand.get().getPermission())
);
return;
}
}
}
subcommand.ifPresent(sub -> {
if (sub.getInfo().onlyPlayers() && !(sender instanceof ProxiedPlayer)) {
invocationSender.sendMessage(Sonar.get().getConfig().PLAYERS_ONLY);
return;
}
if (sub.getInfo().onlyConsole() && !(sender instanceof ConsoleCommandSender)) {
invocationSender.sendMessage(Sonar.get().getConfig().CONSOLE_ONLY);
return;
}
final CommandInvocation commandInvocation = new CommandInvocation(invocationSender, sub, args);
// The subcommands has arguments which are not present in the executed command
if (sub.getInfo().arguments().length > 0
&& commandInvocation.getRawArguments().length <= 1) {
invocationSender.sendMessage(
Sonar.get().getConfig().INCORRECT_COMMAND_USAGE
.replace("%usage%", sub.getInfo().name() + " (" + sub.getArguments() + ")")
);
return;
}
// Execute the sub command with the custom invocation properties
sub.execute(commandInvocation);
});
if (!subcommand.isPresent()) {
// Re-use the old, cached help message since we don't want to scan
// for each subcommand and it's arguments/attributes every time
// someone runs /sonar since the subcommand don't change
for (final Object component : CACHED_HELP_MESSAGE) {
sender.sendMessage((TextComponent) component);
}
}
}
@Override
public Iterable<String> onTabComplete(final CommandSender sender, final String @NotNull [] args) {
if (args.length <= 1) {
if (TAB_SUGGESTIONS.isEmpty()) {
for (final Subcommand subcommand : Sonar.get().getSubcommandRegistry().getSubcommands()) {
TAB_SUGGESTIONS.add(subcommand.getInfo().name());
if (subcommand.getInfo().aliases().length > 0) {
TAB_SUGGESTIONS.addAll(Arrays.asList(subcommand.getInfo().aliases()));
}
}
}
return TAB_SUGGESTIONS;
} else if (args.length == 2) {
if (ARG_TAB_SUGGESTIONS.isEmpty()) {
for (final Subcommand subcommand : Sonar.get().getSubcommandRegistry().getSubcommands()) {
final List<String> parsedArguments = Arrays.stream(subcommand.getInfo().arguments())
.map(Argument::value)
.collect(Collectors.toList());
ARG_TAB_SUGGESTIONS.put(subcommand.getInfo().name(), parsedArguments);
for (final String alias : subcommand.getInfo().aliases()) {
ARG_TAB_SUGGESTIONS.put(alias, parsedArguments);
}
}
}
final String subCommandName = args[0].toLowerCase();
return ARG_TAB_SUGGESTIONS.getOrDefault(subCommandName, emptyList());
}
return emptyList();
}
}
@@ -0,0 +1,26 @@
/*
* Copyright (C) 2023 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 io.netty.handler.codec.DecoderException;
final class ConditionFailedException extends DecoderException {
public ConditionFailedException(final String message) {
super(message);
}
}
@@ -0,0 +1,86 @@
/*
* Copyright (C) 2023 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 io.netty.channel.ChannelHandlerContext;
import net.md_5.bungee.api.ProxyServer;
import net.md_5.bungee.connection.DownstreamBridge;
import net.md_5.bungee.connection.UpstreamBridge;
import net.md_5.bungee.netty.ChannelWrapper;
import net.md_5.bungee.netty.HandlerBoss;
import net.md_5.bungee.netty.PacketHandler;
import org.jetbrains.annotations.NotNull;
public final class FallbackHandlerBoss extends HandlerBoss {
private ChannelWrapper channel;
private PacketHandler handler;
@Override
public void setHandler(final @NotNull PacketHandler handler) {
this.handler = handler;
super.setHandler(handler);
}
@Override
public void channelActive(final ChannelHandlerContext ctx) throws Exception {
if (handler != null) {
channel = new ChannelWrapper(ctx);
handler.connected(channel);
if (handler instanceof DownstreamBridge) {
ProxyServer.getInstance().getLogger().info(handler + " has connected");
}
}
}
@Override
public void channelInactive(final ChannelHandlerContext ctx) throws Exception {
if (handler != null) {
channel.markClosed();
handler.disconnected(channel);
if (handler instanceof DownstreamBridge) {
ProxyServer.getInstance().getLogger().info(handler + " has disconnected");
}
}
}
@Override
public void channelWritabilityChanged(final ChannelHandlerContext ctx) throws Exception {
if (handler != null) {
handler.writabilityChanged(channel);
super.channelWritabilityChanged(ctx);
}
}
@Override
public void exceptionCaught(final @NotNull ChannelHandlerContext ctx, final Throwable throwable) throws Exception {
if (ctx.channel().isActive()) {
if (handler instanceof DownstreamBridge || handler instanceof UpstreamBridge) {
try {
handler.exception(throwable);
} catch (Exception exception) {
ProxyServer.getInstance().getLogger().severe(handler + " - exception processing exception: " + exception);
}
}
channel.markClosed();
ctx.close();
}
}
}
@@ -0,0 +1,331 @@
/*
* Copyright (C) 2023 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 io.netty.channel.Channel;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelPipeline;
import io.netty.handler.codec.DecoderException;
import lombok.Getter;
import lombok.val;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.json.JSONComponentSerializer;
import net.md_5.bungee.BungeeCord;
import net.md_5.bungee.EncryptionUtil;
import net.md_5.bungee.api.config.ListenerInfo;
import net.md_5.bungee.connection.InitialHandler;
import net.md_5.bungee.netty.ChannelWrapper;
import net.md_5.bungee.protocol.PlayerPublicKey;
import net.md_5.bungee.protocol.packet.Kick;
import net.md_5.bungee.protocol.packet.LoginRequest;
import net.md_5.bungee.protocol.packet.StatusRequest;
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.event.impl.UserVerifyJoinEvent;
import xyz.jonesdev.sonar.api.fallback.Fallback;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.api.statistics.Statistics;
import xyz.jonesdev.sonar.common.fallback.FallbackChannelHandler;
import xyz.jonesdev.sonar.common.fallback.FallbackTimeoutHandler;
import xyz.jonesdev.sonar.common.fallback.FallbackVerificationHandler;
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.traffic.TrafficChannelHooker;
import java.net.InetAddress;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import static net.md_5.bungee.netty.PipelineUtils.*;
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.*;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_19_1;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_19_3;
import static xyz.jonesdev.sonar.common.utility.geyser.GeyserUtil.isGeyserConnection;
public final class FallbackInitialHandler extends InitialHandler {
public FallbackInitialHandler(final @NotNull BungeeCord bungee, final ListenerInfo listener) {
super(bungee, listener);
this.bungee = bungee;
}
private static final @NotNull Fallback FALLBACK = Objects.requireNonNull(Sonar.get().getFallback());
@Getter
private ChannelWrapper channelWrapper;
private @NotNull final BungeeCord bungee;
private @Nullable FallbackUserWrapper player;
@Getter
private ProtocolVersion protocolVersion;
private boolean receivedLoginPacket;
private boolean receivedStatusPacket;
@Override
public void connected(final ChannelWrapper channelWrapper) throws Exception {
this.channelWrapper = channelWrapper;
super.connected(channelWrapper);
}
private static @NotNull Kick generateKickPacket(final Component component) {
final String serialized = JSONComponentSerializer.json().serialize(component);
return new Kick(serialized);
}
@Override
public void handle(final StatusRequest statusRequest) throws Exception {
// Fix status packet spam exploit
if (receivedStatusPacket) {
throw new ConditionFailedException("Duplicate status packet");
}
receivedStatusPacket = true;
// Run the rest of the method asynchronously
CompletableFuture.runAsync(() -> {
// The channel always stays open because the client sends
// a StatusRequest and a Ping packet after one another
if (!isConnected()) {
throw new ConditionFailedException("Not connected anymore");
}
try {
super.handle(statusRequest);
} catch (Exception exception) {
throw new DecoderException(exception);
}
});
}
@Override
public void handle(final LoginRequest loginRequest) throws Exception {
// Increase joins per second for the action bar verbose
Sonar.get().getVerboseHandler().getJoinsPerSecond().put(System.nanoTime());
// Fix login packet spam exploit
if (receivedLoginPacket || player != null) {
throw new ConditionFailedException("Duplicate login packet");
}
receivedLoginPacket = true;
// Cache protocol version so other handlers don't throw NPEs
protocolVersion = ProtocolVersion.fromId(getHandshake().getProtocolVersion());
final Channel channel = channelWrapper.getHandle();
// Run in the channel's event loop
channel.eventLoop().execute(() -> {
try {
final ChannelPipeline pipeline = channel.pipeline();
TrafficChannelHooker.hook(pipeline, PACKET_DECODER, PACKET_ENCODER);
// Increase total traffic statistic
Statistics.TOTAL_TRAFFIC.increment();
// Check if the verification is enabled
if (!Sonar.get().getConfig().getVerification().isEnabled()) {
super.handle(loginRequest);
return;
}
final InetAddress inetAddress = getAddress().getAddress();
val uuid = UUID.nameUUIDFromBytes(("OfflinePlayer:" + loginRequest.getData()).getBytes(StandardCharsets.UTF_8));
// Check if the player is already verified
if (Sonar.get().getVerifiedPlayerController().has(inetAddress, uuid)) {
super.handle(loginRequest);
return;
}
// Check the blacklist here since we cannot let the player "ghost join"
if (FALLBACK.getBlacklisted().has(inetAddress.toString())) {
closeWith(generateKickPacket(Sonar.get().getConfig().getVerification().getBlacklisted()));
return;
}
// Completely skip Geyser connections (for now)
if (isGeyserConnection(channel)) {
// TODO: Do we need to log this?
FALLBACK.getLogger().info("Allowing Geyser connection: {}", inetAddress);
super.handle(loginRequest);
return;
}
// Create wrapped Fallback user
player = new FallbackUserWrapper(
FALLBACK, channelWrapper, this,
channel, channel.pipeline(), inetAddress, protocolVersion
);
// Perform default BungeeCord checks
if (bungee.config.isEnforceSecureProfile()
&& player.getProtocolVersion().compareTo(MINECRAFT_1_19_3) < 0) {
final PlayerPublicKey publicKey = loginRequest.getPublicKey();
if (publicKey == null) {
disconnect(bungee.getTranslation("secure_profile_required"));
return;
}
if (Instant.ofEpochMilli(publicKey.getExpiry()).isBefore(Instant.now())) {
disconnect(bungee.getTranslation("secure_profile_expired"));
return;
}
if (player.getProtocolVersion().compareTo(MINECRAFT_1_19_1) < 0
&& !EncryptionUtil.check(publicKey, null)) {
disconnect(bungee.getTranslation("secure_profile_invalid"));
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(generateKickPacket(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(generateKickPacket(Sonar.get().getConfig().getVerification().getAlreadyVerifying()));
return;
}
// We cannot allow too many players on our Fallback server
// There's technically no reason for limiting this, but we'll better stay safe.
if (FALLBACK.getConnected().size() > Sonar.get().getConfig().getVerification().getMaxVerifyingPlayers()) {
closeWith(generateKickPacket(Sonar.get().getConfig().getVerification().getTooManyPlayers()));
return;
}
// Check if the IP address is currently being rate-limited
if (!FALLBACK.getRatelimiter().attempt(inetAddress)) {
closeWith(generateKickPacket(Sonar.get().getConfig().getVerification().getTooFastReconnect()));
return;
}
// We have to add this pipeline to monitor whenever the client disconnects
// to remove them from the list of connected and queued players
pipeline.addFirst(FALLBACK_HANDLER, new FallbackChannelHandler(loginRequest.getData()));
// Queue the connection for further processing
FALLBACK.getQueue().queue(inetAddress, () -> channel.eventLoop().execute(() -> {
// Do not continue if the connection is closed or marked as disconnected
if (channelWrapper.isClosed() || channelWrapper.isClosing()) return;
// Check if the username matches the valid name regex to prevent
// UTF-16 names or other types of exploits
if (!Sonar.get().getConfig().getVerification().getValidNameRegex().matcher(loginRequest.getData()).matches()) {
closeWith(generateKickPacket(Sonar.get().getConfig().getVerification().getInvalidUsername()));
return;
}
// Add better timeout handler to avoid known exploits or issues
// We also want to timeout bots quickly to avoid flooding
pipeline.replace(
TIMEOUT_HANDLER,
TIMEOUT_HANDLER,
new FallbackTimeoutHandler(Sonar.get().getConfig().getVerification().getReadTimeout(),
TimeUnit.MILLISECONDS)
);
// Disconnect if the protocol version could not be resolved
if (player.getProtocolVersion().isUnknown()) {
closeWith(generateKickPacket(Sonar.get().getConfig().getVerification().getInvalidProtocol()));
return;
}
// Check if the player is already connected to the proxy but still tries to verify
final int limit = bungee.config.getPlayerLimit();
if (limit > 0 && bungee.getOnlineCount() >= limit) {
disconnect(bungee.getTranslation("proxy_full"));
return;
} else if (!isOnlineMode() && bungee.getPlayer(loginRequest.getData()) != null) {
closeWith(generateKickPacket(Sonar.get().getConfig().getVerification().getAlreadyConnected()));
return;
}
// The player 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 (!FALLBACK.isPotentiallyUnderAttack() || Sonar.get().getConfig().getVerification().isLogDuringAttack()) {
FALLBACK.getLogger().info(Sonar.get().getConfig().getVerification().getConnectLog()
.replace("%name%", loginRequest.getData())
.replace("%ip%", Sonar.get().getConfig().formatAddress(inetAddress))
.replace("%protocol%", String.valueOf(player.getProtocolVersion().getProtocol())));
}
}
// Call the VerifyJoinEvent for external API usage
Sonar.get().getEventManager().publish(new UserVerifyJoinEvent(loginRequest.getData(), player));
// Mark the player as connected → verifying players
FALLBACK.getConnected().put(loginRequest.getData(), inetAddress);
// This sometimes happens when the channel hangs, but the player is still connecting
// This also fixes a unique issue with TCPShield and other reverse proxies
if (player.getPipeline().get(PACKET_ENCODER) == null
|| player.getPipeline().get(PACKET_DECODER) == null) {
channelWrapper.close();
return;
}
// Replace normal encoder to allow custom packets
final FallbackPacketEncoder encoder = new FallbackPacketEncoder(player.getProtocolVersion());
player.getPipeline().replace(PACKET_ENCODER, FALLBACK_PACKET_ENCODER, encoder);
// Send LoginSuccess packet to make the client think they are joining the server
player.write(new LoginSuccess(loginRequest.getData(), uuid));
// The LoginSuccess packet has been sent, now we can change the registry state
encoder.updateRegistry(player.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_20_2) >= 0
? FallbackPacketRegistry.CONFIG : FallbackPacketRegistry.GAME);
// Replace normal decoder to allow custom packets
player.getPipeline().replace(
PACKET_DECODER, FALLBACK_PACKET_DECODER, new FallbackPacketDecoder(player,
new FallbackVerificationHandler(player, loginRequest.getData(), uuid)
));
}));
} catch (Throwable throwable) {
throw new ReflectiveOperationException(throwable);
}
});
}
// Mostly taken from Velocity
public void closeWith(final Object msg) {
if (channelWrapper.getHandle().isActive()) {
boolean is17 = protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_8) < 0
&& protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_7_2) >= 0;
if (is17) {
channelWrapper.getHandle().eventLoop().execute(() -> {
channelWrapper.getHandle().config().setAutoRead(false);
channelWrapper.getHandle().eventLoop().schedule(() -> {
channelWrapper.markClosed();
channelWrapper.getHandle().writeAndFlush(msg).addListener(ChannelFutureListener.CLOSE);
}, 250L, TimeUnit.MILLISECONDS);
});
} else {
channelWrapper.markClosed();
channelWrapper.getHandle().writeAndFlush(msg).addListener(ChannelFutureListener.CLOSE);
}
}
}
}
@@ -17,110 +17,51 @@
package xyz.jonesdev.sonar.bungee.fallback;
import com.velocitypowered.natives.compression.VelocityCompressor;
import com.velocitypowered.natives.util.Natives;
import io.netty.channel.ChannelPipeline;
import lombok.RequiredArgsConstructor;
import net.md_5.bungee.BungeeCord;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.json.JSONComponentSerializer;
import net.md_5.bungee.api.chat.TextComponent;
import net.md_5.bungee.api.connection.PendingConnection;
import net.md_5.bungee.api.event.PostLoginEvent;
import net.md_5.bungee.api.plugin.Listener;
import net.md_5.bungee.compress.PacketCompressor;
import net.md_5.bungee.compress.PacketDecompressor;
import net.md_5.bungee.connection.InitialHandler;
import net.md_5.bungee.event.EventHandler;
import net.md_5.bungee.netty.ChannelWrapper;
import net.md_5.bungee.protocol.packet.Kick;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.ReflectiveOperationException;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.fallback.Fallback;
import xyz.jonesdev.sonar.bungee.fallback.compress.FallbackPacketCompressor;
import xyz.jonesdev.sonar.bungee.fallback.compress.FallbackPacketDecompressor;
import java.lang.reflect.Field;
import static net.md_5.bungee.netty.PipelineUtils.FRAME_PREPENDER;
import static xyz.jonesdev.sonar.bungee.fallback.FallbackListener.CachedMessages.LOCKDOWN_DISCONNECT;
@RequiredArgsConstructor
public final class FallbackListener implements Listener {
private final Fallback fallback;
private static final Field CHANNEL_WRAPPER;
static {
try {
CHANNEL_WRAPPER = InitialHandler.class.getDeclaredField("ch");
CHANNEL_WRAPPER.setAccessible(true);
} catch (Throwable throwable) {
throw new ReflectiveOperationException(throwable);
}
}
public static class CachedMessages {
static TextComponent LOCKDOWN_DISCONNECT;
public static void update() {
LOCKDOWN_DISCONNECT = new TextComponent(Sonar.get().getConfig().LOCKDOWN_DISCONNECT);
}
}
@EventHandler
@SuppressWarnings("deprecation")
public void handle(final @NotNull PostLoginEvent event) throws Throwable {
if (Sonar.get().getConfig().LOCKDOWN_ENABLED) {
if (!event.getPlayer().hasPermission("sonar.lockdown")) {
event.getPlayer().disconnect(LOCKDOWN_DISCONNECT);
if (Sonar.get().getConfig().LOCKDOWN_LOG_ATTEMPTS) {
Sonar.get().getLogger().info(
Sonar.get().getConfig().LOCKDOWN_CONSOLE_LOG
.replace("%player%", event.getPlayer().getName())
.replace("%ip%", Sonar.get().getConfig()
.formatAddress(event.getPlayer().getAddress().getAddress()))
.replace("%protocol%",
String.valueOf(event.getPlayer().getPendingConnection().getVersion()))
);
public void handle(final @NotNull PostLoginEvent event) {
if (Sonar.get().getConfig().getLockdown().isEnabled()) {
if (!event.getPlayer().hasPermission(Sonar.get().getConfig().getLockdown().getBypassPermission())) {
final PendingConnection pendingConnection = event.getPlayer().getPendingConnection();
// Try to close the channel with a custom serialized disconnect component
if (pendingConnection instanceof FallbackInitialHandler) {
final FallbackInitialHandler fallbackInitialHandler = (FallbackInitialHandler) pendingConnection;
final Component component = Sonar.get().getConfig().getLockdown().getDisconnect();
final String serialized = JSONComponentSerializer.json().serialize(component);
fallbackInitialHandler.closeWith(new Kick(serialized));
} else {
// Fallback by disconnecting without a message
pendingConnection.disconnect();
Sonar.get().getLogger().warn("Fallback handler of {} is missing", event.getPlayer().getName());
return;
}
return;
} else if (Sonar.get().getConfig().LOCKDOWN_ENABLE_NOTIFY) {
event.getPlayer().sendMessage(
new TextComponent(Sonar.get().getConfig().LOCKDOWN_NOTIFICATION)
);
if (Sonar.get().getConfig().getLockdown().isLogAttempts()) {
Sonar.get().getLogger().info(Sonar.get().getConfig().getLockdown().getConsoleLog()
.replace("%player%", event.getPlayer().getName())
.replace("%ip%", Sonar.get().getConfig()
.formatAddress(event.getPlayer().getAddress().getAddress()))
.replace("%protocol%",
String.valueOf(event.getPlayer().getPendingConnection().getVersion())));
}
} else if (Sonar.get().getConfig().getLockdown().isNotifyAdmins()) {
event.getPlayer().sendMessage(new TextComponent(Sonar.get().getConfig().getLockdown().getNotification()));
}
}
final InitialHandler initialHandler = (InitialHandler) event.getPlayer().getPendingConnection();
final ChannelWrapper channelWrapper = (ChannelWrapper) CHANNEL_WRAPPER.get(initialHandler);
final ChannelPipeline pipeline = channelWrapper.getHandle().pipeline();
// You don't need the frame decoder anymore
if (pipeline.get(FRAME_PREPENDER) != null) {
pipeline.remove(FRAME_PREPENDER);
}
final int compressionThreshold = BungeeCord.getInstance().getConfig().getCompressionThreshold();
// Replace compression handlers
if (compressionThreshold > 0
&& pipeline.get(PacketCompressor.class) != null
&& pipeline.get(PacketDecompressor.class) != null) {
final VelocityCompressor compressor = Natives.compress.get().create(-1);
// Replace (de)compressor with Velocity's to ensure better performance
pipeline.replace(
PacketCompressor.class,
"compress",
// Create a new compressor instance with the Velocity compressor
new FallbackPacketCompressor(compressionThreshold, compressor)
);
pipeline.replace(
PacketDecompressor.class,
"decompress",
// Create a new decompressor instance with the Velocity decompressor
new FallbackPacketDecompressor(compressionThreshold, compressor)
);
}
}
}
@@ -22,20 +22,19 @@ import io.netty.channel.ChannelPipeline;
import io.netty.util.ReferenceCountUtil;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import net.md_5.bungee.api.chat.TextComponent;
import net.kyori.adventure.text.Component;
import net.md_5.bungee.netty.ChannelWrapper;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.Fallback;
import xyz.jonesdev.sonar.api.fallback.FallbackPlayer;
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.bungee.fallback.handler.FallbackInitialHandler;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.Disconnect;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.Disconnect;
import java.net.InetAddress;
@Getter
@RequiredArgsConstructor
public final class FallbackPlayerWrapper implements FallbackPlayer<ChannelWrapper, FallbackInitialHandler> {
public final class FallbackUserWrapper implements FallbackUser<ChannelWrapper, FallbackInitialHandler> {
private final Fallback fallback;
private final ChannelWrapper player;
private final FallbackInitialHandler connection;
@@ -45,8 +44,8 @@ public final class FallbackPlayerWrapper implements FallbackPlayer<ChannelWrappe
private final ProtocolVersion protocolVersion;
@Override
public void disconnect(final @NotNull String reason) {
connection.closeWith(Disconnect.create(TextComponent.toLegacyText(new TextComponent(reason))));
public void disconnect(final @NotNull Component reason) {
connection.closeWith(Disconnect.create(reason));
}
@Override
@@ -57,4 +56,13 @@ public final class FallbackPlayerWrapper implements FallbackPlayer<ChannelWrappe
ReferenceCountUtil.release(packet);
}
}
@Override
public void delayedWrite(final @NotNull Object packet) {
if (channel.isActive()) {
channel.write(packet, channel.voidPromise());
} else {
ReferenceCountUtil.release(packet);
}
}
}
@@ -1,115 +0,0 @@
/*
* Copyright (C) 2023 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.compress;
import com.velocitypowered.natives.compression.VelocityCompressor;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import lombok.AllArgsConstructor;
import lombok.Getter;
import net.md_5.bungee.compress.PacketCompressor;
import org.jetbrains.annotations.NotNull;
import java.util.zip.DataFormatException;
import static com.velocitypowered.natives.util.MoreByteBufUtils.ensureCompatible;
import static com.velocitypowered.natives.util.MoreByteBufUtils.preferredBuffer;
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.*;
@Getter
@AllArgsConstructor
public final class FallbackPacketCompressor extends PacketCompressor {
private int compressionThreshold;
private final @NotNull VelocityCompressor compressor;
private static final int MAX_COMPRESSED_LENGTH = 1 << 21;
@Override
public void setThreshold(final int compressionThreshold) {
this.compressionThreshold = compressionThreshold;
}
@Override
public void handlerAdded(final ChannelHandlerContext ctx) throws Exception {
// Do nothing
}
@Override
public void handlerRemoved(final ChannelHandlerContext ctx) throws Exception {
compressor.close();
}
@Override
protected void encode(final ChannelHandlerContext ctx,
final @NotNull ByteBuf msg,
final ByteBuf out) throws Exception {
final int uncompressed = msg.readableBytes();
if (uncompressed < compressionThreshold) {
// Under the threshold, do nothing.
writeVarInt(out, uncompressed + 1);
writeVarInt(out, 0);
out.writeBytes(msg);
} else {
handleCompressed(ctx, msg, out);
}
}
private void handleCompressed(final @NotNull ChannelHandlerContext ctx,
final @NotNull ByteBuf msg,
final ByteBuf out) throws DataFormatException {
final int uncompressed = msg.readableBytes();
write21BitVarInt(out, 0); // Fake packet length
writeVarInt(out, uncompressed);
final ByteBuf compatibleIn = ensureCompatible(ctx.alloc(), compressor, msg);
final int startCompressed = out.writerIndex();
try {
compressor.deflate(compatibleIn, out);
} finally {
compatibleIn.release();
}
final int compressedLength = out.writerIndex() - startCompressed;
if (compressedLength >= MAX_COMPRESSED_LENGTH) {
throw new DataFormatException("The server sent a very large (over 2MiB compressed) packet.");
}
int writerIndex = out.writerIndex();
int packetLength = out.readableBytes() - 3;
out.writerIndex(0);
write21BitVarInt(out, packetLength); // Rewrite packet length
out.writerIndex(writerIndex);
}
@Override
protected ByteBuf allocateBuffer(final ChannelHandlerContext ctx,
final @NotNull ByteBuf msg,
final boolean preferDirect) throws Exception {
int uncompressed = msg.readableBytes();
if (uncompressed < compressionThreshold) {
int finalBufferSize = uncompressed + 1;
finalBufferSize += varIntBytes(finalBufferSize);
return ctx.alloc().directBuffer(finalBufferSize);
}
final int initialBufferSize = (uncompressed - 1) + 3 + varIntBytes(uncompressed);
return preferredBuffer(ctx.alloc(), compressor, initialBufferSize);
}
}
@@ -1,98 +0,0 @@
/*
* Copyright (C) 2023 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.compress;
import com.velocitypowered.natives.compression.VelocityCompressor;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.CorruptedFrameException;
import lombok.Getter;
import net.md_5.bungee.compress.PacketDecompressor;
import org.jetbrains.annotations.NotNull;
import java.util.List;
import static com.velocitypowered.natives.util.MoreByteBufUtils.ensureCompatible;
import static com.velocitypowered.natives.util.MoreByteBufUtils.preferredBuffer;
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.readVarInt;
@Getter
public final class FallbackPacketDecompressor extends PacketDecompressor {
private final int compressionThreshold;
private final @NotNull VelocityCompressor compressor;
private static final int VANILLA_MAXIMUM_UNCOMPRESSED_SIZE = 8 * 1024 * 1024; // 8MiB
private static final int HARD_MAXIMUM_UNCOMPRESSED_SIZE = 16 * 1024 * 1024; // 16MiB
private static final int UNCOMPRESSED_CAP = Boolean.getBoolean("sonar.increased-compression-cap")
? HARD_MAXIMUM_UNCOMPRESSED_SIZE : VANILLA_MAXIMUM_UNCOMPRESSED_SIZE;
public FallbackPacketDecompressor(final int compressionThreshold,
final @NotNull VelocityCompressor compressor) {
super(); // super(compressionThreshold);
this.compressionThreshold = compressionThreshold;
this.compressor = compressor;
}
@Override
public void handlerAdded(final ChannelHandlerContext ctx) throws Exception {
// Do nothing
}
@Override
public void handlerRemoved(final ChannelHandlerContext ctx) throws Exception {
compressor.close();
}
@Override
protected void decode(final ChannelHandlerContext ctx,
final @NotNull ByteBuf in,
final List<Object> out) throws Exception {
final int claimedUncompressedSize = readVarInt(in);
if (claimedUncompressedSize == 0) {
// This message is not compressed.
out.add(in.retain());
// Check for invalid packet size after the packet is retained
final int size = in.readableBytes();
if (size > compressionThreshold + 2) {
throw new CorruptedFrameException("Invalid uncompressed packet size " + size);
}
return;
}
if (claimedUncompressedSize < compressionThreshold) {
throw new CorruptedFrameException(claimedUncompressedSize + " is less than " + compressionThreshold);
}
if (claimedUncompressedSize > UNCOMPRESSED_CAP) {
throw new CorruptedFrameException(claimedUncompressedSize + " exceeds maximum size");
}
final ByteBuf compatibleIn = ensureCompatible(ctx.alloc(), compressor, in);
final ByteBuf uncompressed = preferredBuffer(ctx.alloc(), compressor, claimedUncompressedSize);
try {
compressor.inflate(compatibleIn, uncompressed, claimedUncompressedSize);
out.add(uncompressed);
} catch (Exception exception) {
uncompressed.release();
throw exception;
} finally {
compatibleIn.release();
}
}
}
@@ -1,91 +0,0 @@
/*
* Copyright (C) 2023 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.handler;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFutureListener;
import net.md_5.bungee.BungeeCord;
import net.md_5.bungee.api.config.ListenerInfo;
import net.md_5.bungee.connection.InitialHandler;
import net.md_5.bungee.netty.ChannelWrapper;
import net.md_5.bungee.protocol.packet.LoginRequest;
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.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.bungee.fallback.FallbackPlayerWrapper;
import java.net.InetAddress;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
public final class FallbackInitialHandler extends InitialHandler {
public FallbackInitialHandler(final BungeeCord bungee, final ListenerInfo listener) {
super(bungee, listener);
}
private static final @NotNull Fallback FALLBACK = Objects.requireNonNull(Sonar.get().getFallback());
private ChannelWrapper channelWrapper;
@SuppressWarnings("unused") // TODO: remove later
private @Nullable FallbackPlayerWrapper player;
@Override
public void connected(final ChannelWrapper channelWrapper) throws Exception {
this.channelWrapper = channelWrapper;
super.connected(channelWrapper);
}
@Override
public void handle(final LoginRequest loginRequest) throws Exception {
final InetAddress inetAddress = getAddress().getAddress();
if (player == null && !Sonar.get().getVerifiedPlayerController().has(inetAddress)) {
final Channel channel = channelWrapper.getHandle();
player = new FallbackPlayerWrapper(
FALLBACK, channelWrapper, this,
channel, channel.pipeline(), inetAddress,
ProtocolVersion.fromId(getHandshake().getProtocolVersion())
);
// TODO: implement Fallback
}
super.handle(loginRequest);
}
// Mostly taken from Velocity
public void closeWith(final Object msg) {
if (player != null && player.getChannel().isActive()) {
boolean is17 = player.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_8) < 0
&& player.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_7_2) >= 0;
if (is17) {
player.getChannel().eventLoop().execute(() -> {
channelWrapper.getHandle().config().setAutoRead(false);
player.getChannel().eventLoop().schedule(() -> {
channelWrapper.markClosed();
player.getChannel().writeAndFlush(msg).addListener(ChannelFutureListener.CLOSE);
}, 250L, TimeUnit.MILLISECONDS);
});
} else {
channelWrapper.markClosed();
player.getChannel().writeAndFlush(msg).addListener(ChannelFutureListener.CLOSE);
}
}
}
}
@@ -17,23 +17,19 @@
package xyz.jonesdev.sonar.bungee.fallback.injection;
import io.netty.buffer.PooledByteBufAllocator;
import io.netty.channel.*;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import net.md_5.bungee.BungeeCord;
import net.md_5.bungee.netty.HandlerBoss;
import net.md_5.bungee.netty.PipelineUtils;
import net.md_5.bungee.protocol.Varint21LengthFieldPrepender;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.ReflectiveOperationException;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.config.SonarConfiguration;
import xyz.jonesdev.sonar.bungee.fallback.FallbackHandlerBoss;
import xyz.jonesdev.sonar.bungee.fallback.varint.Varint21FrameDecoder;
import xyz.jonesdev.sonar.common.fallback.FallbackTimeoutHandler;
import java.lang.reflect.Field;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import static net.md_5.bungee.netty.PipelineUtils.*;
@@ -42,18 +38,15 @@ import static net.md_5.bungee.netty.PipelineUtils.*;
public final class BaseChannelInitializer extends ChannelInitializer<Channel> {
public static final BaseChannelInitializer INSTANCE = new BaseChannelInitializer();
// cached object of our configuration to instantly access it
private static final SonarConfiguration CONFIG = Objects.requireNonNull(Sonar.get().getConfig());
private static final boolean REPLACE_VAR_INT_DECODER = !Boolean.getBoolean("sonar.do-not-replace-decoder");
// https://github.com/PaperMC/Velocity/blob/dev/3.0.0/proxy/src/main/java/com/velocitypowered/proxy/network/ConnectionManager.java#L57
private static final WriteBufferWaterMark SERVER_WRITE_MARK = new WriteBufferWaterMark(1 << 20, 1 << 21);
private static final WriteBufferWaterMark MARK;
private static final Varint21LengthFieldPrepender FRAME_ENCODER;
static {
try {
final Field markField = PipelineUtils.class.getDeclaredField("MARK");
markField.setAccessible(true);
MARK = (WriteBufferWaterMark) markField.get(null);
final Field frameEncoder = PipelineUtils.class.getDeclaredField("framePrepender");
frameEncoder.setAccessible(true);
FRAME_ENCODER = (Varint21LengthFieldPrepender) frameEncoder.get(null);
@@ -72,15 +65,13 @@ public final class BaseChannelInitializer extends ChannelInitializer<Channel> {
}
channel.config().setOption(ChannelOption.TCP_NODELAY, true);
channel.config().setAllocator(PooledByteBufAllocator.DEFAULT);
channel.config().setWriteBufferWaterMark(MARK);
channel.config().setWriteBufferWaterMark(SERVER_WRITE_MARK);
channel.pipeline().addLast(FRAME_DECODER, CONFIG.REPLACE_VAR_INT_DECODER
channel.pipeline().addLast(FRAME_DECODER, REPLACE_VAR_INT_DECODER
? new Varint21FrameDecoder() : new net.md_5.bungee.protocol.Varint21FrameDecoder());
channel.pipeline().addLast(TIMEOUT_HANDLER, new FallbackTimeoutHandler(
BungeeCord.getInstance().config.getTimeout(), TimeUnit.MILLISECONDS
));
BungeeCord.getInstance().config.getTimeout(), TimeUnit.MILLISECONDS));
channel.pipeline().addLast(FRAME_PREPENDER, FRAME_ENCODER);
channel.pipeline().addLast(BOSS_HANDLER, new HandlerBoss());
channel.pipeline().addLast(BOSS_HANDLER, new FallbackHandlerBoss());
}
}
@@ -23,15 +23,15 @@ import io.netty.handler.codec.haproxy.HAProxyMessageDecoder;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import net.md_5.bungee.BungeeCord;
import net.md_5.bungee.api.ProxyServer;
import net.md_5.bungee.api.config.ListenerInfo;
import net.md_5.bungee.api.event.ClientConnectEvent;
import net.md_5.bungee.netty.HandlerBoss;
import net.md_5.bungee.netty.PipelineUtils;
import net.md_5.bungee.protocol.*;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.ReflectiveOperationException;
import xyz.jonesdev.sonar.bungee.fallback.handler.FallbackInitialHandler;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.bungee.fallback.FallbackHandlerBoss;
import xyz.jonesdev.sonar.bungee.fallback.FallbackInitialHandler;
import java.lang.reflect.Field;
import java.net.SocketAddress;
@@ -43,7 +43,7 @@ public final class ChildChannelInitializer extends ChannelInitializer<Channel> {
public static final ChildChannelInitializer INSTANCE = new ChildChannelInitializer();
private static final BaseChannelInitializer BASE = BaseChannelInitializer.INSTANCE;
private static final BungeeCord BUNGEE = BungeeCord.getInstance();
private static final KickStringWriter LEGACY_KICK;
static {
@@ -63,7 +63,7 @@ public final class ChildChannelInitializer extends ChannelInitializer<Channel> {
final SocketAddress remoteAddress = channel.remoteAddress() == null ? channel.parent().localAddress()
: channel.remoteAddress();
if (BungeeCord.getInstance().getConnectionThrottle() != null && BungeeCord.getInstance().getConnectionThrottle().throttle(remoteAddress)) {
if (BUNGEE.getConnectionThrottle() != null && BUNGEE.getConnectionThrottle().throttle(remoteAddress)) {
channel.close();
} else {
final ListenerInfo listener = channel.attr(PipelineUtils.LISTENER).get();
@@ -80,13 +80,12 @@ public final class ChildChannelInitializer extends ChannelInitializer<Channel> {
}
channel.pipeline().addBefore(FRAME_DECODER, LEGACY_DECODER, new LegacyDecoder());
channel.pipeline().addAfter(FRAME_DECODER, PACKET_DECODER, new MinecraftDecoder(Protocol.HANDSHAKE, true,
ProxyServer.getInstance().getProtocolVersion()));
channel.pipeline().addAfter(FRAME_PREPENDER, PACKET_ENCODER, new MinecraftEncoder(Protocol.HANDSHAKE,
true, ProxyServer.getInstance().getProtocolVersion()));
channel.pipeline().addAfter(FRAME_DECODER, PACKET_DECODER, new MinecraftDecoder(
Protocol.HANDSHAKE, true, ProtocolVersion.LATEST_VERSION.getProtocol()));
channel.pipeline().addAfter(FRAME_PREPENDER, PACKET_ENCODER, new MinecraftEncoder(
Protocol.HANDSHAKE, true, ProtocolVersion.LATEST_VERSION.getProtocol()));
channel.pipeline().addBefore(FRAME_PREPENDER, LEGACY_KICKER, LEGACY_KICK);
channel.pipeline().get(HandlerBoss.class).setHandler(new FallbackInitialHandler(BungeeCord.getInstance(),
listener));
channel.pipeline().get(FallbackHandlerBoss.class).setHandler(new FallbackInitialHandler(BUNGEE, listener));
if (listener.isProxyProtocol()) {
channel.pipeline().addFirst(new HAProxyMessageDecoder());
@@ -21,6 +21,7 @@ import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
import io.netty.handler.codec.CorruptedFrameException;
import org.jetbrains.annotations.NotNull;
import java.util.List;
@@ -29,7 +30,7 @@ import java.util.List;
public final class Varint21FrameDecoder extends ByteToMessageDecoder {
@Override
protected void decode(final ChannelHandlerContext ctx,
protected void decode(final @NotNull ChannelHandlerContext ctx,
final ByteBuf byteBuf,
final List<Object> out) throws Exception {
if (!ctx.channel().isActive() || !byteBuf.isReadable()) {
@@ -1,43 +0,0 @@
/*
* Copyright (C) 2023 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.verbose;
import lombok.RequiredArgsConstructor;
import net.md_5.bungee.api.ChatMessageType;
import net.md_5.bungee.api.ProxyServer;
import net.md_5.bungee.api.chat.TextComponent;
import net.md_5.bungee.api.connection.ProxiedPlayer;
import xyz.jonesdev.sonar.api.verbose.Verbose;
@RequiredArgsConstructor
public final class VerboseWrapper extends Verbose {
private final ProxyServer server;
public void broadcast(final String message) {
final TextComponent component = new TextComponent(message);
synchronized (subscribers) {
for (final String subscriber : subscribers) {
final ProxiedPlayer player = server.getPlayer(subscriber);
if (player != null) {
player.sendMessage(ChatMessageType.ACTION_BAR, component);
}
}
}
}
}
@@ -19,10 +19,12 @@ package xyz.jonesdev.sonar.common.boot;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.cappuccino.Cappuccino;
import xyz.jonesdev.cappuccino.ExpiringCache;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.SonarPlatform;
import xyz.jonesdev.sonar.api.SonarSupplier;
import xyz.jonesdev.sonar.api.command.subcommand.SubcommandRegistry;
import xyz.jonesdev.sonar.api.config.SonarConfiguration;
@@ -32,6 +34,7 @@ import xyz.jonesdev.sonar.api.timer.SystemTimer;
import xyz.jonesdev.sonar.api.verbose.Verbose;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer;
import xyz.jonesdev.sonar.common.subcommand.SubcommandRegistryHolder;
import xyz.jonesdev.sonar.common.update.UpdateChecker;
import java.io.File;
import java.net.InetAddress;
@@ -41,35 +44,45 @@ import java.util.concurrent.TimeUnit;
@RequiredArgsConstructor
public abstract class SonarBootstrap<T> implements Sonar {
private T plugin;
@Setter
private Verbose verboseHandler;
private SonarConfiguration config;
private VerifiedPlayerController verifiedPlayerController;
private File dataDirectory;
private final SonarPlatform platform;
private final SubcommandRegistry subcommandRegistry;
private final SystemTimer launchTimer = new SystemTimer();
public SonarBootstrap(final @NotNull T plugin,
final File dataDirectory,
final Verbose verboseHandler) {
// Set the API to this instance so the config doesn't have issues
final SonarPlatform platform) {
// Set the Sonar API
SonarSupplier.set(this);
// Set the plugin instance before anything else
this.plugin = plugin;
this.dataDirectory = dataDirectory;
this.verboseHandler = verboseHandler;
this.platform = platform;
this.verboseHandler = new Verbose();
this.config = new SonarConfiguration(dataDirectory);
this.subcommandRegistry = new SubcommandRegistryHolder();
}
public final void initialize() {
getLogger().info("Successfully booted in {}s!", launchTimer.formattedDelay());
// Check if the branch is not the main branch to warn about unstable versions
if (!getVersion().isOnMainBranch()) {
getLogger().warn("You are currently using an unofficial experimental branch.");
getLogger().warn("It is highly recommended to use the latest stable release of Sonar:");
getLogger().warn("https://github.com/jonesdevelopment/sonar/releases");
}
getLogger().info("Successfully booted in {}s!", launchTimer);
getLogger().info("Initializing shared components...");
// Reload configuration
reload();
getLogger().info("Successfully initialized components in {}s!", launchTimer.formattedDelay());
getLogger().info("Successfully initialized components in {}s!", launchTimer);
getLogger().info("Enabling all tasks and features...");
try {
@@ -77,11 +90,16 @@ public abstract class SonarBootstrap<T> implements Sonar {
enable();
// Done
getLogger().info("Done ({}s)!", launchTimer.formattedDelay());
getLogger().info("Done ({}s)!", launchTimer);
} catch (Throwable throwable) {
// An error has occurred
getLogger().error("An error has occurred while launching Sonar: {}", throwable);
throwable.printStackTrace(System.err);
return; // Do not check for updates if the launch failed
}
// Check if a new version has been released
UpdateChecker.checkForUpdates();
}
public abstract void enable();
@@ -92,7 +110,7 @@ public abstract class SonarBootstrap<T> implements Sonar {
// Warn player if they reloaded and changed the database type
if (getVerifiedPlayerController() != null
&& getVerifiedPlayerController().getCachedDatabaseType() != getConfig().DATABASE_TYPE) {
&& getVerifiedPlayerController().getCachedDatabaseType() != getConfig().getDatabase().getType()) {
Sonar.get().getLogger().warn("Reloading the server after changing the database type"
+ " is generally not recommended as it can sometimes cause data loss.");
}
@@ -102,19 +120,13 @@ public abstract class SonarBootstrap<T> implements Sonar {
// Update ratelimiter
final ExpiringCache<InetAddress> expiringCache = Cappuccino.buildExpiring(
getConfig().VERIFICATION_DELAY, TimeUnit.MILLISECONDS, 250L
);
getConfig().getVerification().getReconnectDelay(), TimeUnit.MILLISECONDS, 250L);
FallbackRatelimiter.INSTANCE.setExpiringCache(expiringCache);
// Reinitialize database controller
verifiedPlayerController = new VerifiedPlayerController();
// Call post reload task
postReload();
}
public abstract void postReload();
public void shutdown() {
getLogger().info("Starting shutdown process...");
// ...
@@ -20,24 +20,23 @@ package xyz.jonesdev.sonar.common.fallback;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.fallback.Fallback;
import java.net.InetAddress;
import java.net.InetSocketAddress;
@RequiredArgsConstructor
public final class FallbackChannelHandler extends ChannelInboundHandlerAdapter {
private static final Fallback FALLBACK = Sonar.get().getFallback();
private final String username;
private static final @NotNull Fallback FALLBACK = Sonar.get().getFallback();
private final @NotNull String username;
@Override
public void channelUnregistered(final ChannelHandlerContext ctx) {
final InetAddress inetAddress = ((InetSocketAddress) ctx.channel().remoteAddress()).getAddress();
// Remove the IP address from the queue
public void channelUnregistered(final @NotNull ChannelHandlerContext ctx) {
// Remove the username from the connected players
FALLBACK.getConnected().remove(username);
FALLBACK.getQueue().remove(inetAddress);
// Remove the IP address from the queue
FALLBACK.getQueue().remove(((InetSocketAddress) ctx.channel().remoteAddress()).getAddress());
ctx.fireChannelUnregistered();
}
@@ -20,77 +20,111 @@ package xyz.jonesdev.sonar.common.fallback;
import io.netty.buffer.ByteBuf;
import io.netty.handler.codec.CorruptedFrameException;
import io.netty.handler.codec.DecoderException;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import lombok.val;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.fallback.FallbackPlayer;
import xyz.jonesdev.sonar.api.event.impl.UserVerifySuccessEvent;
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
import xyz.jonesdev.sonar.api.model.VerifiedPlayer;
import xyz.jonesdev.sonar.api.timer.SystemTimer;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketListener;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.*;
import xyz.jonesdev.sonar.common.fallback.protocol.*;
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.packets.play.*;
import xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil;
import java.util.Random;
import java.util.UUID;
import java.util.regex.Pattern;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_13;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_8;
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_PACKET_DECODER;
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_PACKET_ENCODER;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer.*;
public final class FallbackVerificationHandler implements FallbackPacketListener {
private final @NotNull FallbackPlayer<?, ?> player;
private final @NotNull FallbackUser<?, ?> user;
private final String username;
private final UUID uuid;
private final short transactionId;
private final long verifyKeepAliveId;
private int movementTick, receivedPackets;
private double lastX, lastY, lastZ;
private final UUID playerUuid;
private short expectedTransactionId;
private long expectedKeepAliveId;
private int expectedTeleportId = -1;
private int tick, totalReceivedPackets;
private int ignoredMovementTicks;
private double posX, posY, posZ, lastY;
private boolean resolvedClientBrand, resolvedClientSettings;
@Setter
private State state;
private @NotNull State state = State.LOGIN_ACK;
private boolean listenForMovements;
private final SystemTimer login = new SystemTimer();
private static final Random RANDOM = new Random();
@RequiredArgsConstructor
public enum State {
// LOGIN
KEEP_ALIVE(false),
// PLAY
CLIENT_SETTINGS(false),
PLUGIN_MESSAGE(false),
TRANSACTION(false),
// IN-GAME
TELEPORT(false),
POSITION(true),
COLLISIONS(true),
// OTHER
SUCCESS(false);
private final boolean canMove;
// 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, COLLISIONS,
// Done
SUCCESS
}
public FallbackVerificationHandler(final @NotNull FallbackPlayer<?, ?> player,
public FallbackVerificationHandler(final @NotNull FallbackUser<?, ?> user,
final @NotNull String username,
final @NotNull UUID uuid) {
this.player = player;
final @NotNull UUID playerUuid) {
this.user = user;
this.username = username;
this.uuid = uuid;
this.transactionId = (short) RANDOM.nextInt();
this.verifyKeepAliveId = RANDOM.nextInt();
this.state = State.KEEP_ALIVE;
this.playerUuid = playerUuid;
// Send LoginSuccess packet to make the client think they are joining the server
player.write(new ServerLoginSuccess(username, uuid));
if (user.getProtocolVersion().compareTo(MINECRAFT_1_20_2) < 0) {
// Start initializing the actual join process
initialJoinProcess();
}
}
if (player.getProtocolVersion().compareTo(MINECRAFT_1_8) < 0) {
private void configure() {
// Set the state to CONFIGURE to avoid false positives
state = State.CONFIGURE;
// Send necessary configuration packets to the client
user.delayedWrite(synchronizeRegistry);
user.delayedWrite(FINISH_CONFIGURATION);
// Send all packets in one flush
user.getChannel().flush();
// Set decoder state to actually catch all packets
updateEncoderDecoderState(FallbackPacketRegistry.CONFIG);
}
private void updateEncoderDecoderState(final @NotNull FallbackPacketRegistry registry) {
val decoder = (FallbackPacketDecoder) user.getChannel().pipeline().get(FALLBACK_PACKET_DECODER);
val encoder = (FallbackPacketEncoder) user.getChannel().pipeline().get(FALLBACK_PACKET_ENCODER);
if (decoder != null && encoder != null) {
// Update the packet registry state to be able to listen for CONFIG packets
decoder.updateRegistry(registry);
encoder.updateRegistry(registry);
} else {
// Something went wrong - the decoder should not be null
user.getFallback().getLogger().warn("Necessary pipelines for {} not found", username);
// Close the channel to prevent bypasses or other potential exploits
user.getChannel().close();
}
}
private void initialJoinProcess() {
if (user.getProtocolVersion().compareTo(MINECRAFT_1_8) < 0) {
// 1.7 players don't have KeepAlive packets in the login process
sendJoinGamePacket();
} else {
// Set the state to KEEP_ALIVE to avoid false positives
state = State.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
player.write(new KeepAlive(verifyKeepAliveId));
user.write(new KeepAlive(expectedKeepAliveId));
}
}
@@ -98,18 +132,41 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
// Set the state to TRANSACTION to avoid false positives
// and go on with the flow of the verification.
state = State.TRANSACTION;
// Send a transaction with a
player.write(new Transaction(
0, transactionId, false
));
// Generate a random transaction ID
expectedTransactionId = (short) RANDOM.nextInt();
// Send a transaction with the given ID
user.write(new Transaction(0, expectedTransactionId, false));
}
private void sendJoinGamePacket() {
// Set the state to CLIENT_SETTINGS to avoid false positives
// and go on with the flow of the verification.
state = State.CLIENT_SETTINGS;
final boolean v1_20_2 = user.getProtocolVersion().compareTo(MINECRAFT_1_20_2) >= 0;
if (!v1_20_2) {
// Set the state to CLIENT_SETTINGS to avoid false positives
// and go on with the flow of the verification.
state = State.CLIENT_SETTINGS;
}
// Select the JoinGame packet for the respective protocol version
player.write(getJoinPacketForVersion(player.getProtocolVersion()));
user.write(getJoinPacketForVersion(user.getProtocolVersion()));
if (v1_20_2) {
// Perform the transaction check since the ClientSettings
// and PluginMessage packets have already been validated.
sendTransaction();
}
}
private void sendAbilitiesAndTeleport() {
// Set the state to TELEPORT to avoid false positives
// and go on with the flow of the verification.
state = State.TELEPORT;
// Make sure the player is unable to fly (the player is in spectator mode)
user.delayedWrite(DEFAULT_ABILITIES);
// Generate the current teleport ID
expectedTeleportId = RANDOM.nextInt(Short.MAX_VALUE);
// Teleport the player to the spawn position
user.delayedWrite(new PositionLook(
SPAWN_X_POSITION, dynamicSpawnYPosition, SPAWN_Z_POSITION,
0f, 0f, expectedTeleportId, false
));
}
private void sendChunkData() {
@@ -117,38 +174,41 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
// and go on with the flow of the verification.
state = State.POSITION;
// Teleport player into the fake lobby by sending an empty chunk
player.write(EMPTY_CHUNK_DATA);
// Send an UpdateSectionBlocks packet with a platform of blocks
// to check if the player collides with the solid platform.
player.write(UPDATE_SECTION_BLOCKS);
user.delayedWrite(EMPTY_CHUNK_DATA);
if (Sonar.get().getConfig().getVerification().isCheckCollisions()) {
// Send an UpdateSectionBlocks packet with a platform of blocks
// to check if the player collides with the solid platform.
user.delayedWrite(UPDATE_SECTION_BLOCKS);
}
// Send all packets in one flush
user.getChannel().flush();
}
private static boolean validateClientLocale(final @NotNull FallbackPlayer<?, ?> player, final String locale) {
private static boolean validateClientLocale(final @SuppressWarnings("unused") @NotNull FallbackUser<?, ?> user,
final String locale) {
// Check the client locale by performing a simple regex check on it
final Pattern pattern = Sonar.get().getConfig().VALID_LOCALE_REGEX;
final Pattern pattern = Sonar.get().getConfig().getVerification().getValidLocaleRegex();
return pattern.matcher(locale).matches(); // Disallow non-ascii characters (by default)
}
private static boolean validateClientBrand(final @NotNull FallbackPlayer<?, ?> player, final ByteBuf content) {
private static boolean validateClientBrand(final @NotNull FallbackUser<?, ?> user, final ByteBuf content) {
// We have to catch every DecoderException, so we can fail and punish
// the player instead of only disconnecting them due to an exception.
try {
// No need to check for empty brands since ProtocolUtil#readBrandMessage
// already performs these checks by default.
final String read = ProtocolUtil.readBrandMessage(content);
// Check if the decoded client brand string is too long
if (read.length() > Sonar.get().getConfig().getVerification().getMaxBrandLength()) {
return false;
}
// Regex pattern for validating client brands
final Pattern pattern = Sonar.get().getConfig().VALID_BRAND_REGEX;
// 1.7 has some very weird issues when trying to decode the client brand
final boolean legacy = player.getProtocolVersion().compareTo(MINECRAFT_1_8) < 0;
final int cap = Sonar.get().getConfig().MAXIMUM_BRAND_LENGTH;
// Read the client brand using our custom readString method that supports 1.7.
// The legacy version of readString does not compare the string length
// with the VarInt sent by the client.
final String read = ProtocolUtil.readString(content, cap, legacy);
// No need to check for empty or too long client brands since
// ProtocolUtil#readString already does exactly that.
final Pattern pattern = Sonar.get().getConfig().getVerification().getValidBrandRegex();
return !read.equals("Vanilla") // The normal brand is always lowercase
&& pattern.matcher(read).matches(); // Disallow non-ascii characters (by default)
} catch (DecoderException exception) {
// Fail if the string (client brand) could not be decoded properly
player.fail("could not decode string");
user.fail("could not decode string");
// Throw the exception so we don't continue checking
throw exception;
}
@@ -156,26 +216,52 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
@Override
public void handle(final @NotNull FallbackPacket packet) {
// The player has already been verified, drop all other packets
if (state == State.SUCCESS) return;
// Check if the player is not sending a ton of packets to the server
final int maxPackets = Sonar.get().getConfig().MAXIMUM_LOGIN_PACKETS
+ Sonar.get().getConfig().MAXIMUM_MOVEMENT_TICKS;
checkFrame(++receivedPackets < maxPackets, "too many packets");
final int maxPackets = Sonar.get().getConfig().getVerification().getMaxLoginPackets() + maxPredictionTick;
checkFrame(++totalReceivedPackets < maxPackets, "too many packets");
// Check for timeout since the player could be sending packets but not important ones
final long timeout = Sonar.get().getConfig().VERIFICATION_TIMEOUT;
final long timeout = Sonar.get().getConfig().getVerification().getMaxPing();
// Check if the time limit has exceeded
if (login.delay() > timeout) {
player.getChannel().close();
if (login.elapsed(timeout)) {
user.getChannel().close();
return;
}
if (packet instanceof LoginAcknowledged) {
// Check if we are currently expecting a LoginAcknowledged packet
assertState(State.LOGIN_ACK);
// Start the configuration process for 1.20.2 clients
configure();
}
if (packet instanceof FinishConfiguration) {
// Check if we are currently expecting a FinishConfiguration packet
assertState(State.CONFIGURE);
// Check if the client has already sent valid ClientSettings and PluginMessage packets
checkFrame(resolvedClientBrand, "did not resolve client brand");
checkFrame(resolvedClientSettings, "did not resolve client settings");
// Start initializing the actual join process
updateEncoderDecoderState(FallbackPacketRegistry.GAME);
initialJoinProcess();
}
if (packet instanceof KeepAlive) {
final KeepAlive keepAlive = (KeepAlive) 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);
final KeepAlive keepAlive = (KeepAlive) packet;
checkFrame(keepAlive.getId() == verifyKeepAliveId, "invalid KeepAlive ID");
checkFrame(keepAlive.getId() == expectedKeepAliveId, "invalid KeepAlive ID");
// The correct KeepAlive packet has been received
sendJoinGamePacket();
@@ -185,35 +271,44 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
final ClientSettings clientSettings = (ClientSettings) packet;
// Validate the locale using a regex to filter unwanted characters.
checkFrame(validateClientLocale(player, clientSettings.getLocale()), "invalid locale");
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;
}
// Make sure we mark the ClientSettings as valid
resolvedClientSettings = true;
}
if (packet instanceof PluginMessage) {
final PluginMessage pluginMessage = (PluginMessage) packet;
// Only the brand channel is important
// Only the brand channel is important, drop the rest
if (pluginMessage.getChannel().equals("MC|Brand")
|| pluginMessage.getChannel().equals("minecraft:brand")) {
// Check if the brand packet was sent twice,
// which is not possible when using a vanilla Minecraft client.
checkFrame(!resolvedClientBrand, "duplicate client brand packet");
// Check if the channel is correct - 1.13 uses the new namespace
// system ('minecraft:' + channel) and anything below 1.13 uses
// the legacy namespace system ('MC|' + channel).
final boolean v1_13 = player.getProtocolVersion().compareTo(MINECRAFT_1_13) >= 0;
final boolean v1_13 = user.getProtocolVersion().compareTo(MINECRAFT_1_13) >= 0;
checkFrame(pluginMessage.getChannel().equals("MC|Brand") || v1_13, "invalid channel");
// Validate the client branding using a regex to filter unwanted characters.
checkFrame(validateClientBrand(player, pluginMessage.content()), "invalid client brand");
checkFrame(validateClientBrand(user, pluginMessage.content()), "invalid client brand");
// Clients sometimes mess up the ClientSettings or PluginMessage packet.
if (state == State.PLUGIN_MESSAGE) {
// Send the transaction packet
sendTransaction();
}
// Make sure we mark the PluginMessage as valid
resolvedClientBrand = true;
}
}
@@ -226,27 +321,33 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
// The transaction should always be accepted
checkFrame(transaction.isAccepted(), "transaction not accepted?!");
// Check if the transaction ID is valid
checkFrame(transaction.getId() == transactionId, "invalid transaction id");
checkFrame(transaction.getId() == expectedTransactionId, "invalid transaction id");
// Checking gravity is disabled, just finish verification
if (!Sonar.get().getConfig().CHECK_GRAVITY) {
if (!Sonar.get().getConfig().getVerification().isCheckGravity()) {
finish();
return;
}
// Make sure the player is unable to fly (the player is in spectator mode)
player.write(DEFAULT_ABILITIES);
// Teleport the player to the spawn position
player.write(SPAWN_TELEPORT);
// TODO: implement ChunkData packet for 1.20.2 and fix block state Ids for barriers
if (user.getProtocolVersion().compareTo(MINECRAFT_1_20_2) >= 0) {
finish();
return;
}
// First, send an Abilities packet to the client to make
// sure the player falls even in spectator mode.
// Then, teleport the player to the spawn position.
sendAbilitiesAndTeleport();
// 1.7-1.8 clients do not have a TeleportConfirm packet
if (player.getProtocolVersion().compareTo(MINECRAFT_1_8) <= 0) {
// Immediately send the chunk data as 1.7-1.8 can't confirm teleports
if (user.getProtocolVersion().compareTo(MINECRAFT_1_8) <= 0) {
// Immediately send the chunk data as 1.7-1.8 can't confirm teleport packets
sendChunkData();
} else {
// Set the state to TELEPORT to avoid false positives
// and go on with the flow of the verification.
state = State.TELEPORT;
// Send all previously sent packets in one flush since we didn't flush
// the channel earlier when we teleported the player.
user.getChannel().flush();
}
}
@@ -254,47 +355,53 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
// Check if we are currently expecting a TeleportConfirm packet
assertState(State.TELEPORT);
// Check if the teleport ID is correct
final TeleportConfirm teleportConfirm = (TeleportConfirm) packet;
checkFrame(teleportConfirm.getTeleportId() == expectedTeleportId, "invalid teleport ID");
// Check if the teleport id is correct
final boolean teleportIdCorrect = teleportConfirm.getTeleportId() == SPAWN_TELEPORT.getTeleportId();
checkFrame(teleportIdCorrect, "invalid teleport ID");
// Reset all values to ensure safety on teleport
tick = 1;
posY = lastY = -1;
expectedTeleportId = -1;
// Now we can send the chunk data
sendChunkData();
}
// Only handle positions if the player can send position packets.
if (state.canMove) {
if (state != State.LOGIN_ACK) {
if (packet instanceof Position) {
final Position position = (Position) packet;
// Immediately handle new position update
handlePositionUpdate(position.getX(), position.getY(), position.getZ(), position.isOnGround());
}
if (packet instanceof PositionLook) {
final PositionLook position = (PositionLook) packet;
// Immediately handle new position update
handlePositionUpdate(position.getX(), position.getY(), position.getZ(), position.isOnGround());
}
if (packet instanceof Player) {
final Player player = (Player) packet;
// This packet does not send new position data, reuse the last Y
handlePositionUpdate(lastX, lastY, lastZ, player.isOnGround());
handlePositionUpdate(posX, posY, posZ, player.isOnGround());
}
}
}
private void handlePositionUpdate(final double x, final double y, final double z, final boolean ground) {
final double deltaY = lastY - y;
// 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 == dynamicSpawnYPosition && z == SPAWN_Z_POSITION) {
// Reset all values to ensure safety on teleport
tick = 1;
posY = -1;
expectedTeleportId = -1;
}
lastX = x;
lastY = y;
lastZ = z;
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.
@@ -304,56 +411,85 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
// The onGround property can never be true when we aren't checking for collisions
checkFrame(!ground || state == State.COLLISIONS, "invalid ground state");
// Skip teleports using this small check
if (deltaY > 0.07) {
// 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;
}
// Verify the player if they sent correct movement packets
if (movementTick++ >= MAX_MOVEMENT_TICK) {
if (Sonar.get().getConfig().CHECK_COLLISIONS) {
if (state != State.COLLISIONS) {
// Set the state to COLLISIONS to avoid false positives
// and go on with the flow of the verification.
// Now we don't care about gravity anymore,
// we just want the player to collide with the blocks.
state = State.COLLISIONS;
} else {
// Calculate the difference between the player's Y coordinate and the expected Y coordinate
final double offsetY = DEFAULT_Y_COLLIDE_POSITION - y;
lastY = dynamicSpawnYPosition;
return;
}
// The offset cannot greater than 0 since the blocks will not let the player fall through them.
checkFrame(offsetY <= 0, "no collisions: " + offsetY);
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().getMaxIgnoredTicks();
checkFrame(++ignoredMovementTicks < maxIgnoredTicks, "too many ignored ticks");
return;
}
// Check if the player is colliding by performing a basic Y offset check.
if (ground) {
// The player is colliding, finish verification
// TODO: Check for: velocity, entities/mounting, interactions
finish();
}
}
} else {
// Checking collisions is disabled, just finish verification
finish();
}
} else if (y >= DEFAULT_Y_COLLIDE_POSITION && y <= DYNAMIC_SPAWN_Y_POSITION) {
// This is a basic gravity check that predicts the next y position
final double predictedY = PREPARED_MOVEMENT_PACKETS[movementTick];
final double offsetY = Math.abs(deltaY - predictedY);
if (tick > maxMovementTick) {
if (!Sonar.get().getConfig().getVerification().isCheckCollisions()) {
// Checking collisions is disabled, just finish verification
finish();
return;
}
// Check if the y motion is roughly equal to the predicted value
checkFrame(offsetY < 0.01, "invalid offset: " + y + ", " + offsetY);
if (state != State.COLLISIONS) {
// Now we don't care about gravity anymore,
// we just want the player to collide with the blocks.
state = State.COLLISIONS;
}
// Calculate the difference between the player's Y coordinate and the expected Y coordinate
final double offsetY = DEFAULT_Y_COLLIDE_POSITION - y;
// Log/debug position if enabled in the configuration
if (Sonar.get().getConfig().getVerification().isDebugXYZPositions()) {
user.getFallback().getLogger().info("{}: {}/{}/{} - offset: {}", username, x, y, z, offsetY);
}
// 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(offsetY <= 0, "no collisions: " + offsetY);
if (ground) {
// The player is colliding to blocks, finish verification
finish();
return;
}
}
if (!ground && tick < maxPredictionTick) {
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()) {
user.getFallback().getLogger().info("{}: {}/{}/{} - deltaY: {} - prediction: {} - offset: {}",
username, x, y, z, deltaY, predictedY, offsetY);
}
// Check if the y motion is roughly equal to the predicted value
checkFrame(offsetY < 0.005, String.format("invalid gravity: %d, %.7f, %.10f, %.10f != %.10f",
tick, y, offsetY, deltaY, predictedY));
}
tick++;
}
private void assertState(final @NotNull State expectedState) {
if (expectedState != state) {
player.fail("expected " + expectedState + ", got " + state);
user.fail("expected " + expectedState + ", got " + state);
}
}
private void checkFrame(final boolean condition, final String message) {
if (!condition) {
player.fail(message);
user.fail(message);
throw new CorruptedFrameException(message);
}
}
@@ -361,21 +497,21 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
private void finish() {
state = State.SUCCESS;
player.getFallback().getConnected().remove(username);
user.getFallback().getConnected().remove(username);
// Add verified player to the database
final VerifiedPlayer verifiedPlayer = new VerifiedPlayer(
player.getInetAddress().toString(), uuid, login.getStart()
);
final VerifiedPlayer verifiedPlayer = new VerifiedPlayer(user.getInetAddress().toString(), playerUuid,
login.getStart());
Sonar.get().getVerifiedPlayerController().add(verifiedPlayer);
// Disconnect player with the verification success message
player.disconnect(Sonar.get().getConfig().VERIFICATION_SUCCESS);
// Call the VerifySuccessEvent for external API usage
Sonar.get().getEventManager().publish(new UserVerifySuccessEvent(username, playerUuid, user, login.delay()));
player.getFallback().getLogger().info(
Sonar.get().getConfig().VERIFICATION_SUCCESSFUL_LOG
.replace("%name%", username)
.replace("%time%", login.formattedDelay())
);
// Disconnect player with the verification success message
user.disconnect(Sonar.get().getConfig().getVerification().getVerificationSuccess());
user.getFallback().getLogger().info(Sonar.get().getConfig().getVerification().getSuccessLog()
.replace("%name%", username)
.replace("%time%", login.toString()));
}
}
@@ -21,79 +21,100 @@ import io.netty.buffer.ByteBuf;
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 xyz.jonesdev.sonar.api.fallback.FallbackPlayer;
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_20_2;
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry.Direction.SERVERBOUND;
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry.GAME;
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry.LOGIN;
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.readVarInt;
@RequiredArgsConstructor
public final class FallbackPacketDecoder extends ChannelInboundHandlerAdapter {
private final FallbackPlayer<?, ?> connection;
private final FallbackPacketRegistry.ProtocolRegistry registry;
private final FallbackUser<?, ?> user;
private final FallbackPacketListener listener;
private @NotNull FallbackPacketRegistry.ProtocolRegistry registry;
public FallbackPacketDecoder(final @NotNull FallbackPlayer<?, ?> connection,
public FallbackPacketDecoder(final @NotNull FallbackUser<?, ?> user,
final @NotNull FallbackPacketListener listener) {
this.connection = connection;
this.registry = FallbackPacketRegistry.SONAR.getProtocolRegistry(
FallbackPacketRegistry.Direction.SERVERBOUND, connection.getProtocolVersion()
);
this.user = user;
this.registry = (user.getProtocolVersion().compareTo(MINECRAFT_1_20_2) >= 0 ? LOGIN : GAME)
.getProtocolRegistry(SERVERBOUND, user.getProtocolVersion());
this.listener = listener;
}
public void updateRegistry(final @NotNull FallbackPacketRegistry registry) {
this.registry = registry.getProtocolRegistry(SERVERBOUND, user.getProtocolVersion());
}
@Override
public void channelRead(final @NotNull ChannelHandlerContext ctx,
final @NotNull Object msg) throws Exception {
if (msg instanceof ByteBuf) {
final ByteBuf byteBuf = (ByteBuf) msg;
// Release the ByteBuf if the connection is not active
// or the ByteBuf doesn't contain any data to avoid
// memory leaks or other potential exploits.
if (!ctx.channel().isActive() || !byteBuf.isReadable()) {
byteBuf.release();
return;
}
final int originalReaderIndex = byteBuf.readerIndex();
// Read the packet ID and then create the packet from it
final int packetId = readVarInt(byteBuf);
final FallbackPacket packet = registry.createPacket(packetId);
// If the packet hasn't been found, skip it
// TODO: Can we fail the verification afterwards?
if (packet == null) {
byteBuf.readerIndex(originalReaderIndex);
} else {
try {
doLengthSanityChecks(byteBuf, packet);
try {
packet.decode(byteBuf, connection.getProtocolVersion());
} catch (Throwable throwable) {
connection.fail("could not decode packet");
throw new CorruptedFrameException("Failed to decode packet");
}
if (byteBuf.isReadable()) {
connection.fail("could not read packet to end");
throw new CorruptedFrameException("Could not read packet to end");
}
listener.handle(packet);
} finally {
byteBuf.release();
}
return;
}
try {
// Ensure that the packet isn't too large or too small
doLengthSanityChecks(byteBuf, packet);
try {
// Try to decode the packet for the given protocol version
packet.decode(byteBuf, user.getProtocolVersion());
} catch (Throwable throwable) {
user.fail("failed to decode packet (" + byteBuf.readableBytes() + " bytes)");
throw new CorruptedFrameException("Failed to decode packet");
}
// Check if the packet still has bytes left after we decoded it
if (byteBuf.isReadable()) {
user.fail("could not read packet to end (" + byteBuf.readableBytes() + " bytes left)");
throw new CorruptedFrameException("Could not read packet to end");
}
// Let our verification handler process the packet
listener.handle(packet);
} finally {
// 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,
final @NotNull FallbackPacket packet) throws Exception {
final int expectedMaxLen = packet.expectedMaxLength(byteBuf, connection.getProtocolVersion());
final int expectedMaxLen = packet.expectedMaxLength(byteBuf, user.getProtocolVersion());
if (expectedMaxLen != -1 && byteBuf.readableBytes() > expectedMaxLen) {
connection.fail("packet too large");
user.fail("packet too large (" + byteBuf.readableBytes() + " bytes)");
throw new CorruptedFrameException("Packet too large");
}
final int expectedMinLen = packet.expectedMinLength(byteBuf, connection.getProtocolVersion());
final int expectedMinLen = packet.expectedMinLength(byteBuf, user.getProtocolVersion());
if (byteBuf.readableBytes() < expectedMinLen) {
connection.fail("packet too small");
user.fail("packet too small (" + byteBuf.readableBytes() + " bytes)");
throw new CorruptedFrameException("Packet too small");
}
}
@@ -20,19 +20,24 @@ package xyz.jonesdev.sonar.common.fallback.protocol;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry.Direction.CLIENTBOUND;
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry.LOGIN;
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.writeVarInt;
public final class FallbackPacketEncoder extends MessageToByteEncoder<FallbackPacket> {
private final ProtocolVersion protocolVersion;
private final FallbackPacketRegistry.ProtocolRegistry registry;
private FallbackPacketRegistry.ProtocolRegistry registry;
public FallbackPacketEncoder(final ProtocolVersion protocolVersion) {
this.protocolVersion = protocolVersion;
this.registry = FallbackPacketRegistry.SONAR.getProtocolRegistry(
FallbackPacketRegistry.Direction.CLIENTBOUND, protocolVersion
);
updateRegistry(LOGIN);
}
public void updateRegistry(final @NotNull FallbackPacketRegistry registry) {
this.registry = registry.getProtocolRegistry(CLIENTBOUND, protocolVersion);
}
@Override
@@ -22,7 +22,11 @@ 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.*;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.config.FinishConfiguration;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.config.RegistrySync;
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.play.*;
import java.util.*;
import java.util.function.Supplier;
@@ -32,16 +36,38 @@ import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
// Most of this is 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 {
SONAR {
LOGIN {
{
clientbound.fallback = false;
serverbound.fallback = false;
serverbound.register(LoginAcknowledged.class, LoginAcknowledged::new,
map(0x03, MINECRAFT_1_20_2, false));
// LOGIN
clientbound.register(ServerLoginSuccess.class, ServerLoginSuccess::new,
clientbound.register(LoginSuccess.class, LoginSuccess::new,
map(0x02, MINECRAFT_1_7_2, false));
}
},
CONFIG {
{
clientbound.register(Disconnect.class, Disconnect::new,
map(0x01, MINECRAFT_1_20_2, false));
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(RegistrySync.class, RegistrySync::new,
map(0x05, MINECRAFT_1_20_2, false));
// GAME
serverbound.register(ClientSettings.class, ClientSettings::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));
}
},
GAME {
{
clientbound.register(JoinGame.class, JoinGame::new,
map(0x01, MINECRAFT_1_7_2, true),
map(0x23, MINECRAFT_1_9, true),
@@ -54,7 +80,8 @@ public enum FallbackPacketRegistry {
map(0x23, MINECRAFT_1_19, true),
map(0x25, MINECRAFT_1_19_1, true),
map(0x24, MINECRAFT_1_19_3, true),
map(0x28, MINECRAFT_1_19_4, true));
map(0x28, MINECRAFT_1_19_4, true),
map(0x29, MINECRAFT_1_20_2, true));
clientbound.register(KeepAlive.class, KeepAlive::new,
map(0x00, MINECRAFT_1_7_2, false),
map(0x1F, MINECRAFT_1_9, false),
@@ -67,7 +94,8 @@ public enum FallbackPacketRegistry {
map(0x1E, MINECRAFT_1_19, false),
map(0x20, MINECRAFT_1_19_1, false),
map(0x1F, MINECRAFT_1_19_3, false),
map(0x23, MINECRAFT_1_19_4, false));
map(0x23, MINECRAFT_1_19_4, false),
map(0x24, MINECRAFT_1_20_2, false));
clientbound.register(Disconnect.class, Disconnect::new,
map(0x40, MINECRAFT_1_7_2, true),
map(0x1A, MINECRAFT_1_9, true),
@@ -80,7 +108,8 @@ public enum FallbackPacketRegistry {
map(0x17, MINECRAFT_1_19, true),
map(0x19, MINECRAFT_1_19_1, true),
map(0x17, MINECRAFT_1_19_3, true),
map(0x1A, MINECRAFT_1_19_4, true));
map(0x1A, MINECRAFT_1_19_4, true),
map(0x1B, MINECRAFT_1_20_2, true));
clientbound.register(PositionLook.class, PositionLook::new,
map(0x08, MINECRAFT_1_7_2, true),
map(0x2E, MINECRAFT_1_9, true),
@@ -94,7 +123,8 @@ public enum FallbackPacketRegistry {
map(0x36, MINECRAFT_1_19, true),
map(0x39, MINECRAFT_1_19_1, true),
map(0x38, MINECRAFT_1_19_3, true),
map(0x3C, MINECRAFT_1_19_4, true));
map(0x3C, MINECRAFT_1_19_4, true),
map(0x3E, MINECRAFT_1_20_2, true));
clientbound.register(Abilities.class, Abilities::new,
map(0x39, MINECRAFT_1_7_2, true),
map(0x2B, MINECRAFT_1_9, true),
@@ -108,7 +138,8 @@ public enum FallbackPacketRegistry {
map(0x2f, MINECRAFT_1_19, true),
map(0x31, MINECRAFT_1_19_1, true),
map(0x30, MINECRAFT_1_19_3, true),
map(0x34, MINECRAFT_1_19_4, true));
map(0x34, MINECRAFT_1_19_4, true),
map(0x36, MINECRAFT_1_20_2, true));
clientbound.register(EmptyChunkData.class, EmptyChunkData::new,
map(0x21, MINECRAFT_1_7_2, true),
map(0x20, MINECRAFT_1_9, true),
@@ -121,33 +152,21 @@ public enum FallbackPacketRegistry {
map(0x1F, MINECRAFT_1_19, true),
map(0x21, MINECRAFT_1_19_1, true),
map(0x20, MINECRAFT_1_19_3, true),
map(0x24, MINECRAFT_1_19_4, true));
map(0x24, MINECRAFT_1_19_4, true),
map(0x25, MINECRAFT_1_20_2, true));
clientbound.register(UpdateSectionBlocks.class, UpdateSectionBlocks::new,
map(34, MINECRAFT_1_7_2, true),
map(16, MINECRAFT_1_9, true),
map(15, MINECRAFT_1_13, true),
map(16, MINECRAFT_1_15, true),
map(15, MINECRAFT_1_16, true),
map(59, MINECRAFT_1_16_2, true),
map(63, MINECRAFT_1_17, true),
map(61, MINECRAFT_1_19, true),
map(64, MINECRAFT_1_19_1, true),
map(63, MINECRAFT_1_19_3, true),
map(67, MINECRAFT_1_19_4, true));
clientbound.register(DefaultSpawnPosition.class, DefaultSpawnPosition::new,
map(0x05, MINECRAFT_1_7_2, true),
map(0x43, MINECRAFT_1_9, true),
map(0x45, MINECRAFT_1_12, true),
map(0x46, MINECRAFT_1_12_1, true),
map(0x49, MINECRAFT_1_13, true),
map(0x4D, MINECRAFT_1_14, true),
map(0x4E, MINECRAFT_1_15, true),
map(0x42, MINECRAFT_1_16, true),
map(0x4B, MINECRAFT_1_17, true),
map(0x4A, MINECRAFT_1_19, true),
map(0x4D, MINECRAFT_1_19_1, true),
map(0x4C, MINECRAFT_1_19_3, true),
map(0x50, MINECRAFT_1_19_4, true));
map(0x22, MINECRAFT_1_7_2, true),
map(0x10, MINECRAFT_1_9, true),
map(0x0F, MINECRAFT_1_13, true),
map(0x10, MINECRAFT_1_15, true),
map(0x0F, MINECRAFT_1_16, true),
map(0x3B, MINECRAFT_1_16_2, true),
map(0x3F, MINECRAFT_1_17, true),
map(0x3D, MINECRAFT_1_19, true),
map(0x40, MINECRAFT_1_19_1, true),
map(0x3F, MINECRAFT_1_19_3, true),
map(0x43, MINECRAFT_1_19_4, true),
map(0x45, MINECRAFT_1_20_2, true));
clientbound.register(Transaction.class, Transaction::new,
map(0x32, MINECRAFT_1_7_2, true),
map(0x11, MINECRAFT_1_9, true),
@@ -159,7 +178,20 @@ public enum FallbackPacketRegistry {
map(0x2D, MINECRAFT_1_19, true),
map(0x2F, MINECRAFT_1_19_1, true),
map(0x2E, MINECRAFT_1_19_3, true),
map(0x32, MINECRAFT_1_19_4, true));
map(0x32, MINECRAFT_1_19_4, true),
map(0x33, MINECRAFT_1_20_2, true));
clientbound.register(Chat.class, Chat::new,
map(0x02, MINECRAFT_1_7_2, true),
map(0x0F, MINECRAFT_1_9, true),
map(0x0E, MINECRAFT_1_13, true),
map(0x0F, MINECRAFT_1_15, true),
map(0x0E, MINECRAFT_1_16, true),
map(0x0F, MINECRAFT_1_18_2, true),
map(0x5F, MINECRAFT_1_19, true),
map(0x62, MINECRAFT_1_19_1, true),
map(0x60, MINECRAFT_1_19_3, true),
map(0x64, MINECRAFT_1_19_4, true),
map(0x67, MINECRAFT_1_20_2, true));
serverbound.register(KeepAlive.class, KeepAlive::new,
map(0x00, MINECRAFT_1_7_2, false),
@@ -173,7 +205,8 @@ public enum FallbackPacketRegistry {
map(0x11, MINECRAFT_1_19, false),
map(0x12, MINECRAFT_1_19_1, false),
map(0x11, MINECRAFT_1_19_3, false),
map(0x12, MINECRAFT_1_19_4, false));
map(0x12, MINECRAFT_1_19_4, false),
map(0x14, MINECRAFT_1_20_2, false));
serverbound.register(ClientSettings.class, ClientSettings::new,
map(0x15, MINECRAFT_1_7_2, false),
map(0x04, MINECRAFT_1_9, false),
@@ -183,7 +216,8 @@ public enum FallbackPacketRegistry {
map(0x07, MINECRAFT_1_19, false),
map(0x08, MINECRAFT_1_19_1, false),
map(0x07, MINECRAFT_1_19_3, false),
map(0x08, MINECRAFT_1_19_4, false));
map(0x08, MINECRAFT_1_19_4, false),
map(0x09, MINECRAFT_1_20_2, false));
serverbound.register(PluginMessage.class, PluginMessage::new,
map(0x17, MINECRAFT_1_7_2, false),
map(0x09, MINECRAFT_1_9, false),
@@ -195,7 +229,8 @@ public enum FallbackPacketRegistry {
map(0x0C, MINECRAFT_1_19, false),
map(0x0D, MINECRAFT_1_19_1, false),
map(0x0C, MINECRAFT_1_19_3, false),
map(0x0D, MINECRAFT_1_19_4, false));
map(0x0D, MINECRAFT_1_19_4, false),
map(0x0F, MINECRAFT_1_20_2, false));
serverbound.register(PositionLook.class, PositionLook::new,
map(0x06, MINECRAFT_1_7_2, false),
map(0x0D, MINECRAFT_1_9, false),
@@ -208,7 +243,8 @@ public enum FallbackPacketRegistry {
map(0x14, MINECRAFT_1_19, false),
map(0x15, MINECRAFT_1_19_1, false),
map(0x14, MINECRAFT_1_19_3, false),
map(0x15, MINECRAFT_1_19_4, false));
map(0x15, MINECRAFT_1_19_4, false),
map(0x17, MINECRAFT_1_20_2, false));
serverbound.register(TeleportConfirm.class, TeleportConfirm::new,
map(0x00, MINECRAFT_1_9, false));
serverbound.register(Position.class, Position::new,
@@ -223,7 +259,8 @@ public enum FallbackPacketRegistry {
map(0x13, MINECRAFT_1_19, false),
map(0x14, MINECRAFT_1_19_1, false),
map(0x13, MINECRAFT_1_19_3, false),
map(0x14, MINECRAFT_1_19_4, false));
map(0x14, MINECRAFT_1_19_4, false),
map(0x16, MINECRAFT_1_20_2, false));
serverbound.register(Player.class, Player::new,
map(0x03, MINECRAFT_1_7_2, false),
map(0x0F, MINECRAFT_1_9, false),
@@ -236,7 +273,8 @@ public enum FallbackPacketRegistry {
map(0x16, MINECRAFT_1_19, false),
map(0x17, MINECRAFT_1_19_1, false),
map(0x16, MINECRAFT_1_19_3, false),
map(0x17, MINECRAFT_1_19_4, false));
map(0x17, MINECRAFT_1_19_4, false),
map(0x19, MINECRAFT_1_20_2, false));
serverbound.register(Transaction.class, Transaction::new,
map(0x0F, MINECRAFT_1_7_2, false),
map(0x05, MINECRAFT_1_9, false),
@@ -248,7 +286,8 @@ public enum FallbackPacketRegistry {
map(0x1F, MINECRAFT_1_19, false),
map(0x20, MINECRAFT_1_19_1, false),
map(0x1F, MINECRAFT_1_19_3, false),
map(0x20, MINECRAFT_1_19_4, false));
map(0x20, MINECRAFT_1_19_4, false),
map(0x23, MINECRAFT_1_20_2, false));
}
};
@@ -267,7 +306,6 @@ public enum FallbackPacketRegistry {
public static class PacketRegistry {
private final Map<ProtocolVersion, ProtocolRegistry> versions;
private boolean fallback = true;
PacketRegistry() {
Map<ProtocolVersion, ProtocolRegistry> mutableVersions = new EnumMap<>(ProtocolVersion.class);
@@ -283,9 +321,6 @@ public enum FallbackPacketRegistry {
ProtocolRegistry getProtocolRegistry(final ProtocolVersion version) {
final ProtocolRegistry registry = versions.get(version);
if (registry == null) {
if (fallback) {
return getProtocolRegistry(MINIMUM_VERSION);
}
throw new IllegalArgumentException("Could not find data for protocol version " + version);
}
return registry;
@@ -386,7 +421,7 @@ public enum FallbackPacketRegistry {
return supplier.get();
}
public int getPacketId(final FallbackPacket packet) {
public int getPacketId(final @NotNull FallbackPacket packet) {
final int id = packetClassToId.getOrDefault(packet.getClass(), Integer.MIN_VALUE);
if (id == Integer.MIN_VALUE) {
@@ -414,9 +449,9 @@ public enum FallbackPacketRegistry {
}
}
private static PacketMapping map(final int id,
final ProtocolVersion version,
final boolean encodeOnly) {
private static @NotNull PacketMapping map(final int id,
final ProtocolVersion version,
final boolean encodeOnly) {
return new PacketMapping(id, version, null, encodeOnly);
}
}
@@ -24,106 +24,101 @@ import net.kyori.adventure.nbt.CompoundBinaryTag;
import net.kyori.adventure.nbt.ListBinaryTag;
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.protocol.ProtocolVersion;
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.dimension.DimensionInfo;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.*;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.config.FinishConfiguration;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.config.RegistrySync;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.Abilities;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.EmptyChunkData;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.JoinGame;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.UpdateSectionBlocks;
import java.io.InputStream;
import java.util.Collections;
import java.util.Objects;
import java.util.Random;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
@UtilityClass
public class FallbackPreparer {
private final Random RANDOM = new Random();
private final DimensionInfo DIMENSION = new DimensionInfo(
"minecraft:overworld",
"sonar", false, false
);
// Mappings
private final CompoundBinaryTag CHAT_TYPE_119;
private final CompoundBinaryTag CHAT_TYPE_1191;
private final CompoundBinaryTag DAMAGE_TYPE_1194;
private final CompoundBinaryTag DAMAGE_TYPE_120;
static {
CHAT_TYPE_119 = getMapping("chat_1_19.nbt");
CHAT_TYPE_1191 = getMapping("chat_1_19_1.nbt");
DAMAGE_TYPE_1194 = getMapping("damage_1_19_4.nbt");
DAMAGE_TYPE_120 = getMapping("damage_type_1_20.nbt");
DAMAGE_TYPE_1194 = getMapping("damage_type_1194.nbt");
DAMAGE_TYPE_120 = getMapping("damage_type_120.nbt");
}
// JoinGame
public JoinGame LEGACY_JOIN_GAME;
private JoinGame JOIN_GAME_1_16;
private JoinGame JOIN_GAME_1_16_2;
private JoinGame JOIN_GAME_1_18_2;
private JoinGame JOIN_GAME_1_19_1;
private JoinGame JOIN_GAME_1_19_4;
private JoinGame JOIN_GAME_1_20;
// Abilities
public final Abilities DEFAULT_ABILITIES = new Abilities((byte) 0, 0f, 0f);
public final FallbackPacket DEFAULT_ABILITIES = new Abilities((byte) 0, 0f, 0f);
// Chunks
public final EmptyChunkData EMPTY_CHUNK_DATA = new EmptyChunkData(0, 0);
public PositionLook SPAWN_TELEPORT;
public final FallbackPacket EMPTY_CHUNK_DATA = new EmptyChunkData(0, 0);
// Finish Configuration
public final FallbackPacket FINISH_CONFIGURATION = new FinishConfiguration();
// Registry Sync
public FallbackPacket synchronizeRegistry;
// JoinGame
private FallbackPacket legacyJoinGame;
private FallbackPacket joinGame116;
private FallbackPacket joinGame1162;
private FallbackPacket joinGame1182;
private FallbackPacket joinGame1191;
private FallbackPacket joinGame1194;
private JoinGame joinGame120;
// Dimension
private final DimensionInfo DIMENSION = new DimensionInfo(
"minecraft:overworld", "sonar", false, false);
// Collisions
public final int BLOCKS_PER_ROW = 8; // 8 * 8 = 64 (max)
private final int SPAWN_BUFFER = 10; // player spawns at 255 + 10 (10 blocks above the platform)
public final int DEFAULT_Y_COLLIDE_POSITION = 255; // 255 is max
private final int SPAWN_BUFFER = 5; // player spawns at 255 + 5 (5 blocks above the platform)
public final int DEFAULT_Y_COLLIDE_POSITION = 255; // 255 is the maximum Y position allowed
public UpdateSectionBlocks UPDATE_SECTION_BLOCKS;
private final ChangedBlock[] CHANGED_BLOCKS = new ChangedBlock[BLOCKS_PER_ROW * BLOCKS_PER_ROW];
public int MAX_MOVEMENT_TICK;
public double[] PREPARED_MOVEMENT_PACKETS;
public int DYNAMIC_SPAWN_BUFFER = SPAWN_BUFFER;
public int DYNAMIC_SPAWN_Y_POSITION;
public int maxMovementTick, maxPredictionTick, dynamicSpawnYPosition;
public double[] preparedCachedYMotions;
public double maxFallDistance;
public final int SPAWN_X_POSITION = BLOCKS_PER_ROW; // middle of the chunk
public final int SPAWN_Z_POSITION = BLOCKS_PER_ROW;
public void prepare() {
LEGACY_JOIN_GAME = createLegacyJoinGamePacket();
JOIN_GAME_1_16 = createJoinGamePacket(MINECRAFT_1_16);
JOIN_GAME_1_16_2 = createJoinGamePacket(MINECRAFT_1_16_2);
JOIN_GAME_1_18_2 = createJoinGamePacket(MINECRAFT_1_18_2);
JOIN_GAME_1_19_1 = createJoinGamePacket(MINECRAFT_1_19_1);
JOIN_GAME_1_19_4 = createJoinGamePacket(MINECRAFT_1_19_4);
JOIN_GAME_1_20 = createJoinGamePacket(MINECRAFT_1_20);
legacyJoinGame = createJoinGamePacket(MINECRAFT_1_8);
joinGame116 = createJoinGamePacket(MINECRAFT_1_16);
joinGame1162 = createJoinGamePacket(MINECRAFT_1_16_2);
joinGame1182 = createJoinGamePacket(MINECRAFT_1_18_2);
joinGame1191 = createJoinGamePacket(MINECRAFT_1_19_1);
joinGame1194 = createJoinGamePacket(MINECRAFT_1_19_4);
joinGame120 = createJoinGamePacket(MINECRAFT_1_20);
synchronizeRegistry = new RegistrySync(joinGame120.getRegistry());
MAX_MOVEMENT_TICK = Sonar.get().getConfig().MAXIMUM_MOVEMENT_TICKS;
PREPARED_MOVEMENT_PACKETS = new double[MAX_MOVEMENT_TICK + 1];
maxMovementTick = Sonar.get().getConfig().getVerification().getMaxMovementTicks();
maxPredictionTick = maxMovementTick + 10;
preparedCachedYMotions = new double[maxPredictionTick + 1];
for (int i = 0; i < MAX_MOVEMENT_TICK + 1; i++) {
PREPARED_MOVEMENT_PACKETS[i] = -((Math.pow(0.98, i) - 1) * 3.92);
for (int i = 0; i < maxPredictionTick + 1; i++) {
preparedCachedYMotions[i] = -((Math.pow(0.98, i) - 1) * 3.92);
}
// Adjust block and collide Y position based on max movement ticks
double maxFallDistance = 0;
for (final double motion : PREPARED_MOVEMENT_PACKETS) {
maxFallDistance += motion;
// Adjust block and collide Y position based on max fall distance
maxFallDistance = 0;
for (int i = 0; i < maxMovementTick; i++) {
maxFallDistance += preparedCachedYMotions[i];
}
// Set the dynamic spawn buffer
DYNAMIC_SPAWN_BUFFER = (int) (SPAWN_BUFFER + maxFallDistance);
final int DYNAMIC_SPAWN_BUFFER = (int) (SPAWN_BUFFER + maxFallDistance);
// Set the dynamic block and collide Y position based on the maximum fall distance
DYNAMIC_SPAWN_Y_POSITION = DEFAULT_Y_COLLIDE_POSITION + DYNAMIC_SPAWN_BUFFER;
// Prepare spawn PositionLook with the dynamic Y position
SPAWN_TELEPORT = new PositionLook(
8, DYNAMIC_SPAWN_Y_POSITION, 8,
0f, 0f,
RANDOM.nextInt(Short.MAX_VALUE),
true
);
dynamicSpawnYPosition = DEFAULT_Y_COLLIDE_POSITION + DYNAMIC_SPAWN_BUFFER;
// Prepare collision platform positions
int index = 0;
@@ -135,7 +130,7 @@ public class FallbackPreparer {
z + (BLOCKS_PER_ROW / 2),
0, 0
);
CHANGED_BLOCKS[index++] = new ChangedBlock(position, BlockType.STONE);
CHANGED_BLOCKS[index++] = new ChangedBlock(position, BlockType.BARRIER);
}
}
@@ -145,60 +140,50 @@ public class FallbackPreparer {
private @Nullable CompoundBinaryTag getMapping(final @NotNull String fileName) {
try (final InputStream inputStream = Sonar.class.getResourceAsStream("/mappings/" + fileName)) {
return BinaryTagIO.unlimitedReader().read(Objects.requireNonNull(inputStream),
BinaryTagIO.Compression.GZIP
);
return BinaryTagIO.reader().read(Objects.requireNonNull(inputStream), BinaryTagIO.Compression.GZIP);
} catch (Throwable throwable) {
Sonar.get().getLogger().error("Could not load mapping {}", fileName);
throwable.printStackTrace(System.err);
Sonar.get().getLogger().error("Could not load mappings for {}: {}", fileName, throwable);
return null;
}
}
public static JoinGame getJoinPacketForVersion(final ProtocolVersion protocolVersion) {
public static FallbackPacket getJoinPacketForVersion(final @NotNull ProtocolVersion protocolVersion) {
if (protocolVersion.compareTo(MINECRAFT_1_15_2) <= 0) {
return LEGACY_JOIN_GAME; // 1.7-1.15.2
return legacyJoinGame; // 1.7.2-1.15.2
}
if (protocolVersion.compareTo(MINECRAFT_1_16_1) <= 0) {
return JOIN_GAME_1_16; // 1.16-1.16.1
return joinGame116; // 1.16-1.16.1
}
if (protocolVersion.compareTo(MINECRAFT_1_18) <= 0) {
return JOIN_GAME_1_16_2; // 1.16.2-1.18
return joinGame1162; // 1.16.2-1.18
}
if (protocolVersion.compareTo(MINECRAFT_1_19) <= 0) {
return JOIN_GAME_1_18_2; // 1.18.1-1.19
return joinGame1182; // 1.18.1-1.19
}
if (protocolVersion.compareTo(MINECRAFT_1_19_3) <= 0) {
return JOIN_GAME_1_19_1; // 1.19.1-1.19.3
return joinGame1191; // 1.19.1-1.19.3
}
if (protocolVersion.compareTo(MINECRAFT_1_19_4) <= 0) {
return JOIN_GAME_1_19_4; // 1.19.4
if (protocolVersion.compareTo(MINECRAFT_1_19_4) == 0) {
return joinGame1194; // 1.19.4
}
if (protocolVersion.compareTo(MINECRAFT_1_20) <= 0) {
return JOIN_GAME_1_20; // 1.20-1.20.1
}
throw new IllegalStateException("Unsupported protocol version");
}
private @NotNull JoinGame createLegacyJoinGamePacket() {
final JoinGame joinGame = new JoinGame();
joinGame.setLevelType("flat");
joinGame.setGamemode(Sonar.get().getConfig().GAMEMODE_ID);
joinGame.setDimension(0);
joinGame.setReducedDebugInfo(true);
return joinGame;
return joinGame120; // 1.20-latest
}
// Partially taken from
// https://github.com/Elytrium/LimboAPI/blob/master/plugin/src/main/java/net/elytrium/limboapi/server/LimboImpl.java#L607
private @NotNull JoinGame createJoinGamePacket(final ProtocolVersion protocolVersion) {
private @NotNull JoinGame createJoinGamePacket(final @NotNull ProtocolVersion protocolVersion) {
final JoinGame joinGame = new JoinGame();
joinGame.setLevelType("flat");
joinGame.setGamemode(Sonar.get().getConfig().GAMEMODE_ID);
joinGame.setGamemode(Sonar.get().getConfig().getVerification().getGamemodeId());
joinGame.setDimension(0);
joinGame.setReducedDebugInfo(true);
// 1.7.2-1.8.9 don't need any dimension information
if (protocolVersion.compareTo(MINECRAFT_1_8) <= 0) {
return joinGame;
}
joinGame.setDimensionInfo(DIMENSION);
final CompoundBinaryTag.Builder registryContainer = CompoundBinaryTag.builder();
@@ -206,7 +191,7 @@ public class FallbackPreparer {
.add(createDimensionData(protocolVersion))
.build();
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_16_2) >= 0) {
if (protocolVersion.compareTo(MINECRAFT_1_16_2) >= 0) {
final CompoundBinaryTag.Builder dimensionRegistryEntry = CompoundBinaryTag.builder();
dimensionRegistryEntry.putString("type", "minecraft:dimension_type");
@@ -245,34 +230,24 @@ public class FallbackPreparer {
))).build()
);
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_19) == 0) {
registryContainer.put("minecraft:chat_type", CHAT_TYPE_119);
} else if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_19_1) >= 0) {
registryContainer.put("minecraft:chat_type", CHAT_TYPE_1191);
}
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_19_4) == 0) {
if (protocolVersion.compareTo(MINECRAFT_1_19_4) == 0) {
registryContainer.put("minecraft:damage_type", DAMAGE_TYPE_1194);
} else if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_20) >= 0) {
} else if (protocolVersion.compareTo(MINECRAFT_1_20) >= 0) {
registryContainer.put("minecraft:damage_type", DAMAGE_TYPE_120);
}
} else {
registryContainer.put("dimension", encodedDimensionRegistry);
}
try {
CompoundBinaryTag currentDimensionData = encodedDimensionRegistry.getCompound(0);
CompoundBinaryTag currentDimensionData = encodedDimensionRegistry.getCompound(0);
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_16_2) >= 0) {
currentDimensionData = currentDimensionData.getCompound("element");
}
joinGame.setCurrentDimensionData(currentDimensionData);
joinGame.setLevelNames(new String[]{"minecraft:overworld"});
joinGame.setRegistry(registryContainer.build());
} catch (Throwable throwable) {
throw new ReflectiveOperationException(throwable);
if (protocolVersion.compareTo(MINECRAFT_1_16_2) >= 0) {
currentDimensionData = currentDimensionData.getCompound("element");
}
joinGame.setCurrentDimensionData(currentDimensionData);
joinGame.setLevelNames(new String[]{"minecraft:overworld"});
joinGame.setRegistry(registryContainer.build());
return joinGame;
}
@@ -289,7 +264,7 @@ public class FallbackPreparer {
.putBoolean("respawn_anchor_works", false)
.putBoolean("has_raids", false)
.putInt("logical_height", 256)
.putString("infiniburn", version.compareTo(ProtocolVersion.MINECRAFT_1_18_2) >= 0 ? "#minecraft" +
.putString("infiniburn", version.compareTo(MINECRAFT_1_18_2) >= 0 ? "#minecraft" +
":infiniburn_nether" : "minecraft:infiniburn_nether")
.putDouble("coordinate_scale", 1.0)
.putString("effects", "minecraft:overworld")
@@ -299,7 +274,7 @@ public class FallbackPreparer {
.putInt("monster_spawn_light_level", 0)
.build();
if (version.compareTo(ProtocolVersion.MINECRAFT_1_16_2) >= 0) {
if (version.compareTo(MINECRAFT_1_16_2) >= 0) {
return CompoundBinaryTag.builder()
.putString("name", "minecraft:overworld")
.putInt("id", 0)
@@ -17,21 +17,69 @@
package xyz.jonesdev.sonar.common.fallback.protocol.block;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_13;
import java.util.function.Function;
@Getter
@SuppressWarnings("unused")
@RequiredArgsConstructor
public enum BlockType {
STONE {
@Override
public int getId(final ProtocolVersion protocolVersion) {
return protocolVersion.compareTo(MINECRAFT_1_13) >= 0 ? 1 : 16;
STONE(protocolVersion -> 1),
BARRIER(protocolVersion -> {
// Link: https://github.com/PrismarineJS/minecraft-data/blob/master/data/pc/1.20/blocks.json
switch (protocolVersion) {
case MINECRAFT_1_7_2:
case MINECRAFT_1_7_6:
// We have to use glass for 1.7 since 1.7 doesn't have barrier blocks
return 20;
default:
// 1.8-1.12.2
return 166;
case MINECRAFT_1_13:
case MINECRAFT_1_13_1:
return 6493;
case MINECRAFT_1_13_2:
return 6494;
case MINECRAFT_1_14:
case MINECRAFT_1_14_1:
case MINECRAFT_1_14_2:
case MINECRAFT_1_14_3:
case MINECRAFT_1_14_4:
case MINECRAFT_1_15:
case MINECRAFT_1_15_1:
case MINECRAFT_1_15_2:
return 7000;
case MINECRAFT_1_16:
case MINECRAFT_1_16_1:
return 7536;
case MINECRAFT_1_16_2:
case MINECRAFT_1_16_3:
case MINECRAFT_1_16_4:
return 7540;
case MINECRAFT_1_17:
case MINECRAFT_1_17_1:
case MINECRAFT_1_18:
case MINECRAFT_1_18_2:
return 7754;
case MINECRAFT_1_19:
case MINECRAFT_1_19_1:
return 8245;
case MINECRAFT_1_19_3:
return 9889;
case MINECRAFT_1_19_4:
return 10221;
case MINECRAFT_1_20:
return 10225;
case MINECRAFT_1_20_2:
return 1; // TODO: https://github.com/PrismarineJS/minecraft-data/issues/779
}
};
});
public abstract int getId(final ProtocolVersion protocolVersion);
private final Function<ProtocolVersion, Integer> function;
public int getId(final @NotNull ProtocolVersion protocolVersion) {
return function.apply(protocolVersion);
}
}
@@ -15,34 +15,26 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.protocol.packets;
package xyz.jonesdev.sonar.common.fallback.protocol.packets.config;
import io.netty.buffer.ByteBuf;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
@Getter
@ToString
@NoArgsConstructor
@AllArgsConstructor
public final class Position implements FallbackPacket {
private double x, y, z;
private boolean onGround;
@Override
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
x = byteBuf.readDouble();
y = byteBuf.readDouble();
z = byteBuf.readDouble();
onGround = byteBuf.readBoolean();
}
public final class FinishConfiguration 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) {
return 0;
}
}
@@ -15,42 +15,41 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.protocol.packets;
package xyz.jonesdev.sonar.common.fallback.protocol.packets.config;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufOutputStream;
import io.netty.handler.codec.EncoderException;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
import net.kyori.adventure.nbt.BinaryTagTypes;
import net.kyori.adventure.nbt.CompoundBinaryTag;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_14;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_17;
import java.io.IOException;
@Getter
@ToString
@NoArgsConstructor
@AllArgsConstructor
public final class DefaultSpawnPosition implements FallbackPacket {
private int x, y, z;
private float angle;
public final class RegistrySync implements FallbackPacket {
private CompoundBinaryTag dimensionRegistry;
@Override
public void encode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
final long encoded = protocolVersion.compareTo(MINECRAFT_1_14) < 0
? ((x & 0x3FFFFFFL) << 38) | ((y & 0xFFFL) << 26) | (z & 0x3FFFFFFL)
: ((x & 0x3FFFFFFL) << 38) | ((y & 0x3FFFFFFL) << 12) | (z & 0xFFFL);
byteBuf.writeLong(encoded);
if (protocolVersion.compareTo(MINECRAFT_1_17) >= 0) {
byteBuf.writeFloat(angle);
public void encode(final @NotNull ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
try (final ByteBufOutputStream stream = new ByteBufOutputStream(byteBuf)) {
stream.writeByte(10); // CompoundTag ID
BinaryTagTypes.COMPOUND.write(dimensionRegistry, stream);
} catch (IOException exception) {
throw new EncoderException("Cannot write NBT CompoundTag");
}
}
@Override
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
throw new UnsupportedOperationException();
}
}
@@ -15,24 +15,19 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.api.server;
package xyz.jonesdev.sonar.common.fallback.protocol.packets.login;
import xyz.jonesdev.sonar.api.SonarPlatform;
import xyz.jonesdev.sonar.api.command.InvocationSender;
import io.netty.buffer.ByteBuf;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
import java.util.Optional;
public final class LoginAcknowledged implements FallbackPacket {
public interface ServerWrapper {
@Override
public void encode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
}
/**
* @return Platform of the server (Velocity, BungeeCord, Spigot)
*/
SonarPlatform getPlatform();
/**
* @param username Username of the player
* @return Optional player wrapped as InvocationSender
* @see xyz.jonesdev.sonar.api.command.InvocationSender
*/
Optional<InvocationSender> getOnlinePlayer(final String username);
@Override
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
}
}
@@ -15,12 +15,13 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.protocol.packets;
package xyz.jonesdev.sonar.common.fallback.protocol.packets.login;
import io.netty.buffer.ByteBuf;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
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;
@@ -34,12 +35,12 @@ import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.writeVarInt;
@Getter
@NoArgsConstructor
@AllArgsConstructor
public final class ServerLoginSuccess implements FallbackPacket {
public final class LoginSuccess implements FallbackPacket {
private String username;
private UUID uuid;
@Override
public void encode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
public void encode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
if (protocolVersion.compareTo(MINECRAFT_1_19) >= 0) {
writeUUID(byteBuf, uuid);
} else if (protocolVersion.compareTo(MINECRAFT_1_16) >= 0) {
@@ -15,13 +15,14 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.protocol.packets;
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;
@@ -34,14 +35,14 @@ public class Abilities implements FallbackPacket {
private float flySpeed, walkSpeed;
@Override
public void encode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
public void encode(final @NotNull ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
byteBuf.writeByte(encodedFlags);
byteBuf.writeFloat(flySpeed);
byteBuf.writeFloat(walkSpeed);
}
@Override
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
public void decode(final @NotNull ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
encodedFlags = byteBuf.readByte();
flySpeed = byteBuf.readFloat();
walkSpeed = byteBuf.readFloat();
@@ -0,0 +1,72 @@
/*
* Copyright (C) 2023 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 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.util.UUID;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.writeString;
import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.writeUUID;
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.writeVarInt;
@Getter
@ToString
@NoArgsConstructor
@AllArgsConstructor
public final class Chat implements FallbackPacket {
private static final UUID PLACEHOLDER_UUID = new UUID(0L, 0L);
private Component component;
private byte position;
@Override
public void encode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
final String serialized = JSONComponentSerializer.json().serialize(component);
writeString(byteBuf, serialized);
// Message type
if (protocolVersion.compareTo(MINECRAFT_1_19_1) >= 0) {
byteBuf.writeBoolean(position == 2);
} else if (protocolVersion.compareTo(MINECRAFT_1_19) >= 0) {
writeVarInt(byteBuf, position);
} else {
byteBuf.writeByte(this.position);
}
// UUID
if (protocolVersion.compareTo(MINECRAFT_1_16) >= 0
&& protocolVersion.compareTo(MINECRAFT_1_19) < 0) {
writeUUID(byteBuf, PLACEHOLDER_UUID);
}
}
@Override
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
throw new UnsupportedOperationException();
}
}
@@ -15,13 +15,14 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.protocol.packets;
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;
@@ -55,7 +56,7 @@ public final class ClientSettings implements FallbackPacket {
}
@Override
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
public void decode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
locale = readString(byteBuf, 16);
viewDistance = byteBuf.readByte();
chatVisibility = readVarInt(byteBuf);
@@ -15,20 +15,22 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.protocol.packets;
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 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 java.util.Objects;
import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.readString;
import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.writeString;
@Getter
@@ -38,17 +40,17 @@ import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.writeStrin
public final class Disconnect implements FallbackPacket {
private @Nullable String reason;
@Override
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
reason = readString(byteBuf);
}
@Override
public void encode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
writeString(byteBuf, Objects.requireNonNull(reason));
}
public static Disconnect create(final String serialized) {
return new Disconnect(serialized);
@Override
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
throw new UnsupportedOperationException();
}
public static @NotNull Disconnect create(final Component component) {
return new Disconnect(JSONComponentSerializer.json().serialize(component));
}
}
@@ -15,28 +15,26 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.protocol.packets;
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.nbt.CompoundBinaryTag;
import net.kyori.adventure.nbt.LongArrayBinaryTag;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
import java.util.BitSet;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.writeArray;
import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.writeCompoundTag;
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.writeVarInt;
// Partially taken from
// https://github.com/Leymooo/BungeeCord/blob/master/protocol/src/main/java/ru/leymooo/botfilter/packets/EmptyChunkPacket.java
@Getter
@ToString
@NoArgsConstructor
@AllArgsConstructor
public final class EmptyChunkData implements FallbackPacket {
@@ -45,8 +43,9 @@ public final class EmptyChunkData implements FallbackPacket {
private static final byte[] SECTION_BYTES = new byte[]{0, 0, 0, 0, 0, 0, 1, 0};
private static final byte[] LIGHT_BYTES = new byte[]{1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 3, -1, -1, 0, 0};
private static final byte[] LEGACY_FILLER_BYTES_17 = new byte[2];
private static final byte[] LEGACY_FILLER_BYTES = new byte[256];
private static final byte[] MODERN_FILLER_BYTES = new byte[1024];
private static final byte[] MODERN_FILLER_BYTES = new byte[256 * 4];
// Prepare nbt for 1.18 and pre-1.18
private static final CompoundBinaryTag MODERN_TAG, LEGACY_TAG;
@@ -69,36 +68,26 @@ public final class EmptyChunkData implements FallbackPacket {
}
@Override
public void encode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
public void encode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
byteBuf.writeInt(x);
byteBuf.writeInt(z);
if (protocolVersion.compareTo(MINECRAFT_1_17) < 0) {
byteBuf.writeBoolean(true);
}
if (protocolVersion.compareTo(MINECRAFT_1_17) >= 0) {
if (protocolVersion.compareTo(MINECRAFT_1_17_1) <= 0) {
writeVarInt(byteBuf, 0); // mask
}
} else {
byteBuf.writeBoolean(true); // full chunk
if (protocolVersion.compareTo(MINECRAFT_1_16) >= 0 && protocolVersion.compareTo(MINECRAFT_1_16_2) < 0) {
byteBuf.writeBoolean(true);
}
if (protocolVersion.compareTo(MINECRAFT_1_16) >= 0
&& protocolVersion.compareTo(MINECRAFT_1_16_2) < 0) {
byteBuf.writeBoolean(true); // ignore old data
}
if (protocolVersion.compareTo(MINECRAFT_1_17) < 0) {
if (protocolVersion.compareTo(MINECRAFT_1_8) == 0) {
byteBuf.writeShort(1);
} else {
if (protocolVersion.compareTo(MINECRAFT_1_8) > 0) {
writeVarInt(byteBuf, 0);
}
} else if (protocolVersion.compareTo(MINECRAFT_1_18) < 0) {
final BitSet bitSet = new BitSet();
for (int i = 0; i < 16; i++) {
bitSet.set(i, false);
}
long[] mask = bitSet.toLongArray();
writeVarInt(byteBuf, mask.length);
for (long l : mask) {
byteBuf.writeLong(l);
} else {
byteBuf.writeShort(1); // fix void chunk
}
}
@@ -121,7 +110,12 @@ public final class EmptyChunkData implements FallbackPacket {
}
if (protocolVersion.compareTo(MINECRAFT_1_13) < 0) {
writeArray(byteBuf, LEGACY_FILLER_BYTES); // 1.8 - 1.12.2
if (protocolVersion.compareTo(MINECRAFT_1_8) >= 0) {
writeArray(byteBuf, LEGACY_FILLER_BYTES); // 1.8 - 1.12.2
} else {
byteBuf.writeInt(0); // compressed size
byteBuf.writeBytes(LEGACY_FILLER_BYTES_17); // 1.7
}
} else if (protocolVersion.compareTo(MINECRAFT_1_15) < 0) {
writeArray(byteBuf, MODERN_FILLER_BYTES); // 1.13 - 1.14.4
} else if (protocolVersion.compareTo(MINECRAFT_1_18) < 0) {
@@ -15,26 +15,22 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.protocol.packets;
package xyz.jonesdev.sonar.common.fallback.protocol.packets.play;
import io.netty.buffer.ByteBuf;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
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.dimension.DimensionInfo;
import java.util.Objects;
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;
// Mostly taken from
// https://github.com/PaperMC/Velocity/blob/dev/3.0.0/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/JoinGame.java
@Data
@NoArgsConstructor
@AllArgsConstructor
@@ -49,81 +45,79 @@ public final class JoinGame implements FallbackPacket {
private int viewDistance; // 1.14+
private boolean reducedDebugInfo;
private boolean showRespawnScreen;
private boolean limitedCrafting; // 1.20.2+
private String[] levelNames; // 1.16+
private CompoundBinaryTag registry; // 1.16+
private DimensionInfo dimensionInfo; // 1.16+
private CompoundBinaryTag currentDimensionData; // 1.16.2+
private short previousGamemode; // 1.16+
private int simulationDistance; // 1.18+
private int portalCooldown; // 1.20+
// Mostly taken from
// https://github.com/Nan1t/NanoLimbo/blob/main/src/main/java/ua/nanit/limbo/protocol/packets/play/PacketJoinGame.java
@Override
public void encode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
if (protocolVersion.compareTo(MINECRAFT_1_16) >= 0) {
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_20_2) >= 0) {
// haha funny, they made 1.20.2 more complicated
encode1202Up(byteBuf, protocolVersion);
} else if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_16) >= 0) {
// Minecraft 1.16 and above have significantly more complicated logic for reading this packet,
// so separate it out.
encode116Up(byteBuf, protocolVersion);
} else {
encodeLegacy(byteBuf, protocolVersion);
}
}
private void encodeLegacy(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
private void encodeLegacy(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
byteBuf.writeInt(entityId);
if (protocolVersion.compareTo(MINECRAFT_1_16_2) >= 0) {
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_16_2) >= 0) {
byteBuf.writeBoolean(isHardcore);
byteBuf.writeByte(gamemode);
} else {
byteBuf.writeByte(isHardcore ? gamemode | 0x8 : gamemode);
}
if (protocolVersion.compareTo(MINECRAFT_1_9_1) >= 0) {
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_9_1) >= 0) {
byteBuf.writeInt(dimension);
} else {
byteBuf.writeByte(dimension);
}
if (protocolVersion.compareTo(MINECRAFT_1_13_2) <= 0) {
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_13_2) <= 0) {
byteBuf.writeByte(difficulty);
}
if (protocolVersion.compareTo(MINECRAFT_1_15) >= 0) {
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_15) >= 0) {
byteBuf.writeLong(partialHashedSeed);
}
byteBuf.writeByte(1); // max players
writeString(byteBuf, Objects.requireNonNull(levelType));
if (protocolVersion.compareTo(MINECRAFT_1_14) >= 0) {
if (levelType == null) {
throw new IllegalStateException("No level type specified.");
}
writeString(byteBuf, levelType);
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_14) >= 0) {
writeVarInt(byteBuf, viewDistance);
}
if (protocolVersion.compareTo(MINECRAFT_1_8) >= 0) {
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_8) >= 0) {
byteBuf.writeBoolean(reducedDebugInfo);
}
if (protocolVersion.compareTo(MINECRAFT_1_15) >= 0) {
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_15) >= 0) {
byteBuf.writeBoolean(showRespawnScreen);
}
}
private void encode116Up(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
private void encode116Up(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
byteBuf.writeInt(entityId);
if (protocolVersion.compareTo(MINECRAFT_1_16_2) >= 0) {
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_16_2) >= 0) {
byteBuf.writeBoolean(isHardcore);
byteBuf.writeByte(gamemode);
} else {
byteBuf.writeByte(isHardcore ? gamemode | 0x8 : gamemode);
}
byteBuf.writeByte(previousGamemode);
writeStringArray(byteBuf, levelNames);
writeCompoundTag(byteBuf, registry);
if (protocolVersion.compareTo(MINECRAFT_1_16_2) >= 0
&& protocolVersion.compareTo(MINECRAFT_1_19) < 0) {
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_16_2) >= 0
&& protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_19) < 0) {
writeCompoundTag(byteBuf, currentDimensionData);
writeString(byteBuf, dimensionInfo.getIdentifier());
} else {
@@ -132,15 +126,14 @@ public final class JoinGame implements FallbackPacket {
}
byteBuf.writeLong(partialHashedSeed);
if (protocolVersion.compareTo(MINECRAFT_1_16_2) >= 0) {
writeVarInt(byteBuf, 1); // max players
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_16_2) >= 0) {
writeVarInt(byteBuf, 0); // max players
} else {
byteBuf.writeByte(1); // max players
byteBuf.writeByte(0); // max players
}
writeVarInt(byteBuf, viewDistance);
if (protocolVersion.compareTo(MINECRAFT_1_18) >= 0) {
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_18) >= 0) {
writeVarInt(byteBuf, simulationDistance);
}
@@ -150,16 +143,47 @@ public final class JoinGame implements FallbackPacket {
byteBuf.writeBoolean(dimensionInfo.isDebug());
byteBuf.writeBoolean(dimensionInfo.isFlat());
if (protocolVersion.compareTo(MINECRAFT_1_19) >= 0) {
// no last death location
byteBuf.writeBoolean(false);
// optional death location
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_19) >= 0) {
byteBuf.writeBoolean(false); // no last death position
}
if (protocolVersion.compareTo(MINECRAFT_1_20) >= 0) {
writeVarInt(byteBuf, portalCooldown);
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_20) >= 0) {
writeVarInt(byteBuf, 0); // portal cooldown
}
}
private void encode1202Up(final @NotNull ByteBuf byteBuf,
final @SuppressWarnings("unused") ProtocolVersion protocolVersion) {
byteBuf.writeInt(entityId);
byteBuf.writeBoolean(isHardcore);
writeStringArray(byteBuf, levelNames);
writeVarInt(byteBuf, 1); // max players
writeVarInt(byteBuf, viewDistance);
writeVarInt(byteBuf, simulationDistance);
byteBuf.writeBoolean(reducedDebugInfo);
byteBuf.writeBoolean(showRespawnScreen);
byteBuf.writeBoolean(limitedCrafting);
writeString(byteBuf, dimensionInfo.getIdentifier());
writeString(byteBuf, dimensionInfo.getLevelName());
byteBuf.writeLong(partialHashedSeed);
byteBuf.writeByte(gamemode);
byteBuf.writeByte(previousGamemode);
byteBuf.writeBoolean(dimensionInfo.isDebug());
byteBuf.writeBoolean(dimensionInfo.isFlat());
byteBuf.writeBoolean(false); // no last death position
writeVarInt(byteBuf, 0); // portal cooldown
}
@Override
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
throw new UnsupportedOperationException();
@@ -15,12 +15,14 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.protocol.packets;
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;
@@ -30,13 +32,14 @@ import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.readVarInt;
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.writeVarInt;
@Getter
@ToString
@NoArgsConstructor
@AllArgsConstructor
public final class KeepAlive implements FallbackPacket {
private long id;
@Override
public void encode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
public void encode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
if (protocolVersion.compareTo(MINECRAFT_1_12_2) >= 0) {
byteBuf.writeLong(id);
} else if (protocolVersion.compareTo(MINECRAFT_1_8) >= 0) {
@@ -47,7 +50,7 @@ public final class KeepAlive implements FallbackPacket {
}
@Override
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
public void decode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
if (protocolVersion.compareTo(MINECRAFT_1_12_2) >= 0) {
id = byteBuf.readLong();
} else if (protocolVersion.compareTo(MINECRAFT_1_8) >= 0) {
@@ -15,13 +15,14 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.protocol.packets;
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;
@@ -33,7 +34,7 @@ public final class Player implements FallbackPacket {
private boolean onGround;
@Override
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
public void decode(final @NotNull ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
onGround = byteBuf.readBoolean();
}
@@ -15,11 +15,12 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.protocol.packets;
package xyz.jonesdev.sonar.common.fallback.protocol.packets.play;
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.netty.DeferredByteBufHolder;
@@ -45,7 +46,12 @@ public final class PluginMessage extends DeferredByteBufHolder implements Fallba
}
@Override
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
public int expectedMaxLength(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
return 0xfff + 1; // limit packet size to 4096
}
@Override
public void decode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
channel = readString(byteBuf);
if (protocolVersion.compareTo(MINECRAFT_1_13) >= 0) {
@@ -0,0 +1,63 @@
/*
* Copyright (C) 2023 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 Position implements FallbackPacket {
private double x, y, z;
private boolean onGround;
@Override
public void decode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
x = byteBuf.readDouble();
// https://github.com/jonesdevelopment/sonar/issues/20
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_8) < 0) {
byteBuf.skipBytes(8);
}
y = byteBuf.readDouble();
z = byteBuf.readDouble();
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;
}
@Override
public int expectedMinLength(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
return 25;
}
}
@@ -15,13 +15,14 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.protocol.packets;
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;
@@ -39,8 +40,12 @@ public final class PositionLook implements FallbackPacket {
private boolean onGround;
@Override
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
public void decode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
x = byteBuf.readDouble();
// https://github.com/jonesdevelopment/sonar/issues/20
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_8) < 0) {
byteBuf.skipBytes(8);
}
y = byteBuf.readDouble();
z = byteBuf.readDouble();
yaw = byteBuf.readFloat();
@@ -49,20 +54,36 @@ public final class PositionLook implements FallbackPacket {
}
@Override
public void encode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
public void encode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
byteBuf.writeDouble(x);
byteBuf.writeDouble(y);
byteBuf.writeDouble(z);
byteBuf.writeFloat(yaw);
byteBuf.writeFloat(pitch);
byteBuf.writeByte(0x00);
if (protocolVersion.compareTo(MINECRAFT_1_9) >= 0) {
writeVarInt(byteBuf, teleportId);
}
if (protocolVersion.compareTo(MINECRAFT_1_8) < 0) {
byteBuf.writeBoolean(onGround);
} else {
byteBuf.writeByte(0x00);
if (protocolVersion.compareTo(MINECRAFT_1_17) >= 0 && protocolVersion.compareTo(MINECRAFT_1_19_3) <= 0) {
byteBuf.writeBoolean(true); // Dismount vehicle
if (protocolVersion.compareTo(MINECRAFT_1_9) >= 0) {
writeVarInt(byteBuf, teleportId);
}
if (protocolVersion.compareTo(MINECRAFT_1_17) >= 0
&& protocolVersion.compareTo(MINECRAFT_1_19_3) <= 0) {
byteBuf.writeBoolean(true); // Always dismount vehicle
}
}
}
@Override
public int expectedMaxLength(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
return protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_8) < 0 ? 41 : 33;
}
@Override
public int expectedMinLength(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
return 33;
}
}
@@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.protocol.packets;
package xyz.jonesdev.sonar.common.fallback.protocol.packets.play;
import io.netty.buffer.ByteBuf;
import lombok.AllArgsConstructor;
@@ -15,13 +15,14 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.protocol.packets;
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;
@@ -36,12 +37,12 @@ public final class Transaction implements FallbackPacket {
private boolean accepted;
@Override
public void encode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
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
// the client must accept the transaction either way.
// it's never even used in the client.
byteBuf.writeBoolean(accepted);
} else {
byteBuf.writeInt(id);
@@ -49,7 +50,7 @@ public final class Transaction implements FallbackPacket {
}
@Override
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
public void decode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
if (protocolVersion.compareTo(MINECRAFT_1_17) < 0) {
windowId = byteBuf.readByte();
id = byteBuf.readShort();
@@ -15,51 +15,61 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.protocol.packets;
package xyz.jonesdev.sonar.common.fallback.protocol.packets.play;
import io.netty.buffer.ByteBuf;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.block.ChangedBlock;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_16_2;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_20;
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;
@Getter
@ToString
@NoArgsConstructor
@AllArgsConstructor
@ToString(exclude = {"changedBlocks"})
public final class UpdateSectionBlocks implements FallbackPacket {
private int sectionX, sectionZ;
private ChangedBlock[] changedBlocks;
@Override
public void encode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) throws Exception {
public void encode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) throws Exception {
if (protocolVersion.compareTo(MINECRAFT_1_16_2) < 0) {
byteBuf.writeInt(sectionX);
byteBuf.writeInt(sectionZ);
writeVarInt(byteBuf, changedBlocks.length);
if (protocolVersion.compareTo(MINECRAFT_1_8) < 0) {
byteBuf.writeShort(changedBlocks.length);
byteBuf.writeInt(4 * changedBlocks.length);
} else {
writeVarInt(byteBuf, changedBlocks.length);
}
for (final ChangedBlock block : changedBlocks) {
byteBuf.writeShort(block.getLegacyChunkPosCrammed());
writeVarInt(byteBuf, block.getType().getId(protocolVersion));
final int id = block.getType().getId(protocolVersion);
if (protocolVersion.compareTo(MINECRAFT_1_13) >= 0) {
writeVarInt(byteBuf, id);
} else {
final int shiftedBlockId = id << 4;
if (protocolVersion.compareTo(MINECRAFT_1_8) < 0) {
byteBuf.writeShort(shiftedBlockId);
} else {
writeVarInt(byteBuf, shiftedBlockId);
}
}
}
} else {
final ChangedBlock lastChangedBlock = changedBlocks[changedBlocks.length - 1];
final int sectionY = lastChangedBlock.getPosition().getY() >> 4;
final int sectionY = changedBlocks[0].getPosition().getY() >> 4;
// Why is Mojang doing this? :(
byteBuf.writeLong(
((long) (sectionX & 0x3FFFFF) << 42)
| (sectionY & 0xFFFFF)
| ((long) (sectionZ & 0x3FFFFF) << 20)
);
byteBuf.writeLong(((sectionX & 0x3FFFFFL) << 42) | (sectionY & 0xFFFFF) | ((sectionZ & 0x3FFFFFL) << 20));
// 1.20+ don't have light update suppression
if (protocolVersion.compareTo(MINECRAFT_1_20) < 0) {
@@ -70,7 +80,8 @@ public final class UpdateSectionBlocks implements FallbackPacket {
for (final ChangedBlock block : changedBlocks) {
final int shiftedBlockId = block.getType().getId(protocolVersion) << 12;
writeVarLong(byteBuf, shiftedBlockId | block.getModernChunkPosCrammed());
final long positionIdValue = shiftedBlockId | block.getModernChunkPosCrammed();
writeVarLong(byteBuf, positionIdValue);
}
}
}
@@ -27,13 +27,11 @@ public class TrafficChannelHooker implements FallbackPipelines {
public void hook(final @NotNull ChannelPipeline pipeline,
final @NotNull String decoder,
final @NotNull String encoder) {
pipeline.channel().eventLoop().execute(() -> {
if (pipeline.get(FALLBACK_IN_TRAFFIC) == null && pipeline.get(decoder) != null) {
pipeline.addBefore(decoder, FALLBACK_IN_TRAFFIC, IncomingBandwidthDecoder.INSTANCE);
}
if (pipeline.get(FALLBACK_OUT_TRAFFIC) == null && pipeline.get(encoder) != null) {
pipeline.addBefore(encoder, FALLBACK_OUT_TRAFFIC, OutgoingBandwidthEncoder.INSTANCE);
}
});
if (pipeline.get(FALLBACK_IN_TRAFFIC) == null && pipeline.get(decoder) != null) {
pipeline.addBefore(decoder, FALLBACK_IN_TRAFFIC, IncomingBandwidthDecoder.INSTANCE);
}
if (pipeline.get(FALLBACK_OUT_TRAFFIC) == null && pipeline.get(encoder) != null) {
pipeline.addBefore(encoder, FALLBACK_OUT_TRAFFIC, OutgoingBandwidthEncoder.INSTANCE);
}
}
}
@@ -25,7 +25,6 @@ import xyz.jonesdev.sonar.api.command.subcommand.SubcommandInfo;
import xyz.jonesdev.sonar.api.command.subcommand.argument.Argument;
import java.net.InetAddress;
import java.net.UnknownHostException;
@SubcommandInfo(
name = "blacklist",
@@ -40,85 +39,60 @@ import java.net.UnknownHostException;
public final class BlacklistCommand extends Subcommand {
@Override
public void execute(final @NotNull CommandInvocation invocation) {
protected void execute(final @NotNull CommandInvocation invocation) {
switch (invocation.getRawArguments()[1].toLowerCase()) {
case "add": {
if (invocation.getRawArguments().length <= 2) {
invocation.getSender().sendMessage(SONAR.getConfig().INCORRECT_COMMAND_USAGE
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getIncorrectCommandUsage()
.replace("%usage%", "blacklist add <IP address>"));
return;
}
final String rawInetAddress = invocation.getRawArguments()[2];
final InetAddress inetAddress = getInetAddressIfValid(invocation.getSender(), rawInetAddress);
// Make sure the given IP address is valid
if (inetAddress == null) return;
if (!IP_PATTERN.matcher(rawInetAddress).matches()) {
invocation.getSender().sendMessage(SONAR.getConfig().INCORRECT_IP_ADDRESS);
return;
}
final InetAddress inetAddress;
try {
inetAddress = InetAddress.getByName(rawInetAddress);
if (inetAddress.isAnyLocalAddress() || inetAddress.isLoopbackAddress()) {
invocation.getSender().sendMessage(SONAR.getConfig().ILLEGAL_IP_ADDRESS);
return;
}
} catch (UnknownHostException exception) {
invocation.getSender().sendMessage(SONAR.getConfig().ILLEGAL_IP_ADDRESS);
return;
}
// Make sure the IP is not blacklisted already
if (SONAR.getFallback().getBlacklisted().has(inetAddress.toString())) {
invocation.getSender().sendMessage(SONAR.getConfig().BLACKLIST_DUPLICATE);
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getBlacklistDuplicate());
return;
}
// Display a warning if the IP is verified but being added to the blacklist
if (SONAR.getVerifiedPlayerController().has(inetAddress)) {
invocation.getSender().sendMessage(SONAR.getConfig().BLACKLIST_ADD_WARNING
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getBlacklistAddWarning()
.replace("%ip%", rawInetAddress));
}
// Blacklist the given IP address
SONAR.getFallback().getBlacklisted().put(inetAddress.toString());
invocation.getSender().sendMessage(SONAR.getConfig().BLACKLIST_ADD
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getBlacklistAdd()
.replace("%ip%", rawInetAddress));
break;
}
case "remove": {
if (invocation.getRawArguments().length <= 2) {
invocation.getSender().sendMessage(SONAR.getConfig().INCORRECT_COMMAND_USAGE
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getIncorrectCommandUsage()
.replace("%usage%", "blacklist remove <IP address>"));
return;
}
final String rawInetAddress = invocation.getRawArguments()[2];
final InetAddress inetAddress = getInetAddressIfValid(invocation.getSender(), rawInetAddress);
// Make sure the given IP address is valid
if (inetAddress == null) return;
if (!IP_PATTERN.matcher(rawInetAddress).matches()) {
invocation.getSender().sendMessage(SONAR.getConfig().INCORRECT_IP_ADDRESS);
return;
}
final InetAddress inetAddress;
try {
inetAddress = InetAddress.getByName(rawInetAddress);
if (inetAddress.isAnyLocalAddress() || inetAddress.isLoopbackAddress()) {
invocation.getSender().sendMessage(SONAR.getConfig().ILLEGAL_IP_ADDRESS);
return;
}
} catch (UnknownHostException exception) {
invocation.getSender().sendMessage(SONAR.getConfig().ILLEGAL_IP_ADDRESS);
return;
}
// Make sure the IP is blacklisted
if (!SONAR.getFallback().getBlacklisted().has(inetAddress.toString())) {
invocation.getSender().sendMessage(SONAR.getConfig().BLACKLIST_NOT_FOUND);
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getBlacklistNotFound());
return;
}
// Invalidate the cache entry of the blacklisted IP address
SONAR.getFallback().getBlacklisted().invalidate(inetAddress.toString());
invocation.getSender().sendMessage(SONAR.getConfig().BLACKLIST_REMOVE
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getBlacklistRemove()
.replace("%ip%", rawInetAddress));
break;
}
@@ -127,21 +101,19 @@ public final class BlacklistCommand extends Subcommand {
final int blacklisted = SONAR.getFallback().getBlacklisted().estimatedSize();
if (blacklisted == 0) {
invocation.getSender().sendMessage(SONAR.getConfig().BLACKLIST_EMPTY);
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getBlacklistEmpty());
return;
}
// Invalidate all cache entries
SONAR.getFallback().getBlacklisted().invalidateAll();
invocation.getSender().sendMessage(
SONAR.getConfig().BLACKLIST_CLEARED
.replace("%removed%", Sonar.DECIMAL_FORMAT.format(blacklisted))
);
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getBlacklistCleared()
.replace("%removed%", Sonar.DECIMAL_FORMAT.format(blacklisted)));
break;
}
case "size": {
invocation.getSender().sendMessage(SONAR.getConfig().BLACKLIST_SIZE
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getBlacklistSize()
.replace("%amount%", Sonar.DECIMAL_FORMAT.format(SONAR.getFallback().getBlacklisted().estimatedSize())));
break;
}
@@ -17,38 +17,101 @@
package xyz.jonesdev.sonar.common.subcommand.impl;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import lombok.RequiredArgsConstructor;
import lombok.val;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Unmodifiable;
import xyz.jonesdev.sonar.api.SonarPlatform;
import xyz.jonesdev.sonar.api.command.CommandInvocation;
import xyz.jonesdev.sonar.api.command.subcommand.Subcommand;
import xyz.jonesdev.sonar.api.command.subcommand.SubcommandInfo;
import xyz.jonesdev.sonar.api.profiler.JVMProfiler;
import java.util.List;
import java.util.Map;
import java.util.WeakHashMap;
// This command is only used to display helpful information in order to help
// the Sonar developer(s) to fix issues more quickly
// the Sonar contributors to fix issues more quickly
@SubcommandInfo(
name = "dump",
description = "Print developer information",
onlyConsole = true
)
public final class DumpCommand extends Subcommand implements JVMProfiler {
private static final Gson GSON = new GsonBuilder()
.disableInnerClassSerialization()
.create();
@Override
public void execute(final @NotNull CommandInvocation invocation) {
final String osName = System.getProperty("os.name");
final String osArch = System.getProperty("os.arch");
final String jvmArgs = String.join(",", RUNTIME_MX_BEAN.getInputArguments());
protected void execute(final @NotNull CommandInvocation invocation) {
final String json = GSON.toJson(collectMappedInformation());
SONAR.getLogger().info("Generated dump: {}", json);
}
invocation.getSender().sendMessage("Virtual cores: " + getVirtualCores());
invocation.getSender().sendMessage("JVM args: " + jvmArgs);
invocation.getSender().sendMessage("Java name: " + RUNTIME_MX_BEAN.getVmName());
invocation.getSender().sendMessage("Java version: " + RUNTIME_MX_BEAN.getVmVersion());
invocation.getSender().sendMessage("Java vendor: " + RUNTIME_MX_BEAN.getVmVendor());
invocation.getSender().sendMessage("Operating system: " + osName + ", " + osArch);
invocation.getSender().sendMessage("Total memory: " + formatMemory(getTotalMemory()));
invocation.getSender().sendMessage("Max memory: " + formatMemory(getMaxMemory()));
invocation.getSender().sendMessage("Free memory: " + formatMemory(getFreeMemory()));
invocation.getSender().sendMessage("Used Memory: " + formatMemory(getUsedMemory()));
invocation.getSender().sendMessage("Platform: " + SONAR.getServer().getPlatform().getDisplayName());
invocation.getSender().sendMessage("Version: " + SONAR.getVersion().getFull());
@Unmodifiable
private @NotNull Map<String, Object> collectMappedInformation() {
val mappings = new WeakHashMap<String, Object>();
mappings.put("sonar", new Dump.Sonar(
SONAR.getVersion().getFull(),
SONAR.getPlatform(),
SONAR.getVersion().isOnMainBranch()
));
mappings.put("runtime", new Dump.Runtime(
getVirtualCores(),
RUNTIME_MX_BEAN.getInputArguments(),
RUNTIME_MX_BEAN.getVmName(),
RUNTIME_MX_BEAN.getVmVendor(),
RUNTIME_MX_BEAN.getVmVersion()
));
mappings.put("os", new Dump.OS(
System.getProperty("os.name"),
System.getProperty("os.arch"),
System.getProperty("os.version")
));
mappings.put("memory", new Dump.Memory(
formatMemory(getTotalMemory()),
formatMemory(getMaxMemory()),
formatMemory(getFreeMemory()),
formatMemory(getUsedMemory())
));
return mappings;
}
@SuppressWarnings("unused")
private static final class Dump {
@RequiredArgsConstructor
private static final class Sonar {
private final String version;
private final SonarPlatform platform;
private final boolean isOnMainBranch;
}
@RequiredArgsConstructor
private static final class Runtime {
private final int virtualCores;
private final List<String> jvmArguments;
private final String vmName;
private final String vmVendor;
private final String vmVersion;
}
@RequiredArgsConstructor
private static final class Memory {
private final String total;
private final String max;
private final String free;
private final String used;
}
@RequiredArgsConstructor
private static final class OS {
private final String name;
private final String arch;
private final String version;
}
}
}
@@ -24,20 +24,18 @@ import xyz.jonesdev.sonar.api.command.subcommand.SubcommandInfo;
@SubcommandInfo(
name = "lockdown",
description = "Lock the server down"
description = "Prevent all players from joining"
)
public final class LockdownCommand extends Subcommand {
@Override
public void execute(final @NotNull CommandInvocation invocation) {
SONAR.getConfig().LOCKDOWN_ENABLED = !SONAR.getConfig().LOCKDOWN_ENABLED;
SONAR.getConfig().getGeneralConfig().set("general.lockdown.enabled", SONAR.getConfig().LOCKDOWN_ENABLED);
protected void execute(final @NotNull CommandInvocation invocation) {
final boolean newState = !SONAR.getConfig().getLockdown().isEnabled();
SONAR.getConfig().getLockdown().setEnabled(newState);
SONAR.getConfig().getGeneralConfig().set("lockdown.enabled", newState);
SONAR.getConfig().getGeneralConfig().save();
invocation.getSender().sendMessage(
SONAR.getConfig().LOCKDOWN_ENABLED
? SONAR.getConfig().LOCKDOWN_ACTIVATED
: SONAR.getConfig().LOCKDOWN_DEACTIVATED
);
invocation.getSender().sendMessage(newState
? SONAR.getConfig().getLockdown().getActivated() : SONAR.getConfig().getLockdown().getDeactivated());
}
}
@@ -21,24 +21,22 @@ import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.command.CommandInvocation;
import xyz.jonesdev.sonar.api.command.subcommand.Subcommand;
import xyz.jonesdev.sonar.api.command.subcommand.SubcommandInfo;
import xyz.jonesdev.sonar.api.timer.SystemTimer;
@SubcommandInfo(
name = "reload",
description = "Reload the configuration"
description = "Reload all configurations"
)
public final class ReloadCommand extends Subcommand {
@Override
public void execute(final @NotNull CommandInvocation invocation) {
final long startTime = System.currentTimeMillis();
protected void execute(final @NotNull CommandInvocation invocation) {
final SystemTimer timer = new SystemTimer();
invocation.getSender().sendMessage(SONAR.getConfig().RELOADING);
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getReloading());
SONAR.reload();
final long timeTaken = System.currentTimeMillis() - startTime;
invocation.getSender().sendMessage(
SONAR.getConfig().RELOADED
.replace("%taken%", String.valueOf(timeTaken))
);
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getReloaded()
.replace("%taken%", String.valueOf(timer.delay())));
}
}
@@ -40,60 +40,71 @@ import static xyz.jonesdev.sonar.api.Sonar.DECIMAL_FORMAT;
argumentsRequired = false
)
public final class StatisticsCommand extends Subcommand implements JVMProfiler {
private enum StatisticType {
GENERAL,
NETWORK,
MEMORY,
CPU
}
@Override
public void execute(final @NotNull CommandInvocation invocation) {
final String showing = invocation.getRawArguments().length >= 2
? invocation.getRawArguments()[1].toLowerCase() : "bot";
invocation.getSender().sendMessage("§eShowing " + showing + " statistics for this session:");
invocation.getSender().sendMessage("§ePlease note that session statistics are not saved when the server restarts.");
invocation.getSender().sendMessage();
if (!showing.equals("bot")) {
switch (showing) {
case "network": {
invocation.getSender().sendMessage(" §7▪ §aCurrent incoming used bandwidth: §f" + TrafficCounter.INCOMING.getCachedSecond());
invocation.getSender().sendMessage(" §7▪ §aCurrent outgoing used bandwidth: §f" + TrafficCounter.OUTGOING.getCachedSecond());
invocation.getSender().sendMessage(" §7▪ §aTotal incoming used bandwidth: §f" + TrafficCounter.INCOMING.getCachedTtl());
invocation.getSender().sendMessage(" §7▪ §aTotal outgoing used bandwidth: §f" + TrafficCounter.OUTGOING.getCachedTtl());
return;
}
case "memory": {
invocation.getSender().sendMessage(" §7▪ §aTotal free memory (JVM): §f" + formatMemory(getFreeMemory()));
invocation.getSender().sendMessage(" §7▪ §aTotal used memory (JVM): §f" + formatMemory(getUsedMemory()));
invocation.getSender().sendMessage(" §7▪ §aTotal maximum memory (JVM): §f" + formatMemory(getMaxMemory()));
invocation.getSender().sendMessage(" §7▪ §aTotal allocated memory (JVM): §f" + formatMemory(getTotalMemory()));
return;
}
case "cpu": {
invocation.getSender().sendMessage(" §7▪ §aProcess CPU usage right now: §f" + DECIMAL_FORMAT.format(getProcessCPUUsage()) + "%");
invocation.getSender().sendMessage(" §7▪ §aSystem CPU usage right now: §f" + DECIMAL_FORMAT.format(getSystemCPUUsage()) + "%");
invocation.getSender().sendMessage(" §7▪ §aPer-core process CPU usage: §f" + DECIMAL_FORMAT.format(getAverageProcessCPUUsage()) + "%");
invocation.getSender().sendMessage(" §7▪ §aPer-core system CPU usage: §f" + DECIMAL_FORMAT.format(getAverageSystemCPUUsage()) + "%");
invocation.getSender().sendMessage(" §7▪ §aGeneral system load average: §f" + DECIMAL_FORMAT.format(getSystemLoadAverage()) + "%");
invocation.getSender().sendMessage(" §7▪ §aTotal virtual cpu cores (JVM): §f" + DECIMAL_FORMAT.format(getVirtualCores()));
return;
}
protected void execute(final @NotNull CommandInvocation invocation) {
StatisticType type = StatisticType.GENERAL;
if (invocation.getRawArguments().length >= 2) {
try {
type = StatisticType.valueOf(invocation.getRawArguments()[1].toUpperCase());
} catch (Exception exception) {
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getUnknownStatisticType()
.replace("%statistics%", getArguments()));
return;
}
}
final int total = Statistics.TOTAL_TRAFFIC.get();
final int real = Statistics.REAL_TRAFFIC.get();
final int failed = Statistics.FAILED_VERIFICATIONS.get();
final int queued = SONAR.getFallback().getQueue().getQueuedPlayers().size();
final int verifying = SONAR.getFallback().getConnected().size();
final int verified = SONAR.getVerifiedPlayerController().estimatedSize();
final int blacklisted = SONAR.getFallback().getBlacklisted().estimatedSize();
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getStatisticsHeader()
.replace("%type%", type.name().toLowerCase()));
invocation.getSender().sendMessage();
invocation.getSender().sendMessage(" §7▪ §aVerified IP addresses: §f" + DECIMAL_FORMAT.format(verified));
invocation.getSender().sendMessage(" §7▪ §aVerifying IP addresses: §f" + DECIMAL_FORMAT.format(verifying));
invocation.getSender().sendMessage(" §7▪ §aBlacklisted IP addresses: §f" + DECIMAL_FORMAT.format(blacklisted));
invocation.getSender().sendMessage(" §7▪ §aCurrently queued logins: §f" + DECIMAL_FORMAT.format(queued));
invocation.getSender().sendMessage(" §7▪ §aTotal non-unique joins: §f" + DECIMAL_FORMAT.format(total));
invocation.getSender().sendMessage(" §7▪ §aTotal verification attempts: §f" + DECIMAL_FORMAT.format(real));
invocation.getSender().sendMessage(" §7▪ §aTotal failed verifications: §f" + DECIMAL_FORMAT.format(failed));
switch (type) {
case GENERAL: {
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getGeneralStatistics()
.replace("%verified%", DECIMAL_FORMAT.format(SONAR.getVerifiedPlayerController().estimatedSize()))
.replace("%verifying%", DECIMAL_FORMAT.format(SONAR.getFallback().getConnected().size()))
.replace("%blacklisted%", DECIMAL_FORMAT.format(SONAR.getFallback().getBlacklisted().estimatedSize()))
.replace("%queued%", DECIMAL_FORMAT.format(SONAR.getFallback().getQueue().getQueuedPlayers().size()))
.replace("%total_joins%", DECIMAL_FORMAT.format(Statistics.TOTAL_TRAFFIC.get()))
.replace("%total_attempts%", DECIMAL_FORMAT.format(Statistics.REAL_TRAFFIC.get()))
.replace("%total_failed%", DECIMAL_FORMAT.format(Statistics.FAILED_VERIFICATIONS.get())));
break;
}
case CPU: {
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getCpuStatistics()
.replace("%process_cpu%", DECIMAL_FORMAT.format(getProcessCPUUsage()))
.replace("%system_cpu%", DECIMAL_FORMAT.format(getSystemCPUUsage()))
.replace("%average_process_cpu%", DECIMAL_FORMAT.format(getAverageProcessCPUUsage()))
.replace("%average_system_cpu%", DECIMAL_FORMAT.format(getAverageSystemCPUUsage()))
.replace("%load_average%", DECIMAL_FORMAT.format(getSystemLoadAverage()))
.replace("%virtual_cores%", DECIMAL_FORMAT.format(getVirtualCores())));
break;
}
case MEMORY: {
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getMemoryStatistics()
.replace("%free_memory%", formatMemory(getFreeMemory()))
.replace("%used_memory%", formatMemory(getUsedMemory()))
.replace("%max_memory%", formatMemory(getMaxMemory()))
.replace("%total_memory%", formatMemory(getTotalMemory())));
break;
}
case NETWORK: {
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getNetworkStatistics()
.replace("%incoming%", TrafficCounter.INCOMING.getCachedSecond())
.replace("%outgoing%", TrafficCounter.OUTGOING.getCachedSecond())
.replace("%ttl_incoming%", TrafficCounter.INCOMING.getCachedTtl())
.replace("%ttl_outgoing%", TrafficCounter.OUTGOING.getCachedTtl()));
break;
}
}
}
}
@@ -17,50 +17,30 @@
package xyz.jonesdev.sonar.common.subcommand.impl;
import net.kyori.adventure.text.Component;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.command.CommandInvocation;
import xyz.jonesdev.sonar.api.command.InvocationSender;
import xyz.jonesdev.sonar.api.command.subcommand.Subcommand;
import xyz.jonesdev.sonar.api.command.subcommand.SubcommandInfo;
import java.util.Optional;
@SubcommandInfo(
name = "verbose",
description = "Enable and disable Sonar verbose",
description = "Enable or disable Sonar verbose",
onlyPlayers = true
)
public final class VerboseCommand extends Subcommand {
@Override
public void execute(final @NotNull CommandInvocation invocation) {
InvocationSender verboseSubscriber = invocation.getSender();
// Support for '/sonar verbose [username]'
if (invocation.getRawArguments().length >= 2) {
final Optional<InvocationSender> optional = SONAR.getServer().getOnlinePlayer(invocation.getRawArguments()[1]);
if (optional.isPresent()) verboseSubscriber = optional.get();
}
if (SONAR.getVerboseHandler().isSubscribed(verboseSubscriber.getName())) {
SONAR.getVerboseHandler().unsubscribe(verboseSubscriber.getName());
if (verboseSubscriber != invocation.getSender()) {
verboseSubscriber.sendMessage(
SONAR.getConfig().VERBOSE_UNSUBSCRIBED_OTHER
.replace("%player%", verboseSubscriber.getName())
);
}
verboseSubscriber.sendMessage(SONAR.getConfig().VERBOSE_UNSUBSCRIBED);
protected void execute(final @NotNull CommandInvocation invocation) {
if (SONAR.getVerboseHandler().isSubscribed(invocation.getSender().getName())) {
SONAR.getVerboseHandler().unsubscribe(invocation.getSender().getName());
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getVerboseUnsubscribed());
// Reset ActionBar component when unsubscribing
invocation.getSender().getAudience().sendActionBar(Component.empty());
return;
}
if (verboseSubscriber != invocation.getSender()) {
verboseSubscriber.sendMessage(
SONAR.getConfig().VERBOSE_SUBSCRIBED_OTHER
.replace("%player%", verboseSubscriber.getName())
);
}
verboseSubscriber.sendMessage(SONAR.getConfig().VERBOSE_SUBSCRIBED);
SONAR.getVerboseHandler().subscribe(verboseSubscriber.getName());
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getVerboseSubscribed());
SONAR.getVerboseHandler().subscribe(invocation.getSender().getName());
}
}
@@ -24,6 +24,7 @@ import xyz.jonesdev.sonar.api.command.subcommand.Subcommand;
import xyz.jonesdev.sonar.api.command.subcommand.SubcommandInfo;
import xyz.jonesdev.sonar.api.command.subcommand.argument.Argument;
import java.net.InetAddress;
import java.util.Queue;
import java.util.UUID;
import java.util.concurrent.LinkedBlockingQueue;
@@ -39,75 +40,70 @@ import java.util.concurrent.LinkedBlockingQueue;
}
)
public final class VerifiedCommand extends Subcommand {
private static final Queue<String> LOCK = new LinkedBlockingQueue<>(10);
private static final Queue<String> LOCK = new LinkedBlockingQueue<>(1);
@Override
public void execute(final @NotNull CommandInvocation invocation) {
protected void execute(final @NotNull CommandInvocation invocation) {
switch (invocation.getRawArguments()[1].toLowerCase()) {
case "history": {
if (invocation.getRawArguments().length <= 2) {
invocation.getSender().sendMessage(SONAR.getConfig().INCORRECT_COMMAND_USAGE
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getIncorrectCommandUsage()
.replace("%usage%", "verified history <IP address>"));
return;
}
final String inetAddress = invocation.getRawArguments()[2];
final String rawInetAddress = invocation.getRawArguments()[2];
final InetAddress inetAddress = getInetAddressIfValid(invocation.getSender(), rawInetAddress);
// Make sure the given IP address is valid
if (inetAddress == null) return;
if (!IP_PATTERN.matcher(inetAddress).matches()) {
invocation.getSender().sendMessage(SONAR.getConfig().INCORRECT_IP_ADDRESS);
// Make sure the IP is verified already
if (!SONAR.getVerifiedPlayerController().has(inetAddress.toString())) {
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getVerifiedNotFound());
return;
}
// We don't need to parse this, so we can just use the string.
final String realInetAddress = "/" + inetAddress;
if (!SONAR.getVerifiedPlayerController().has(realInetAddress)) {
invocation.getSender().sendMessage(SONAR.getConfig().VERIFIED_NOT_FOUND);
return;
}
invocation.getSender().sendMessage("§ePrevious UUIDs for " + inetAddress + ":");
for (final UUID uuid : SONAR.getVerifiedPlayerController().getUUIDs(realInetAddress)) {
invocation.getSender().sendMessage(" §7▪ §f" + uuid.toString());
invocation.getSender().sendMessage("<yellow>Previous UUIDs for " + rawInetAddress + ":");
for (final UUID uuid : SONAR.getVerifiedPlayerController().getUUIDs(inetAddress.toString())) {
invocation.getSender().sendMessage(" <gray>▪ <white>" + uuid.toString());
}
break;
}
case "remove": {
if (invocation.getRawArguments().length <= 2) {
invocation.getSender().sendMessage(SONAR.getConfig().INCORRECT_COMMAND_USAGE
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getIncorrectCommandUsage()
.replace("%usage%", "verified remove <IP address>"));
return;
}
final String inetAddress = invocation.getRawArguments()[2];
final String rawInetAddress = invocation.getRawArguments()[2];
if (!IP_PATTERN.matcher(inetAddress).matches()) {
invocation.getSender().sendMessage(SONAR.getConfig().INCORRECT_IP_ADDRESS);
// Make sure we aren't currently locking the IP address to avoid a double I/O operation
if (LOCK.contains(rawInetAddress)) {
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getVerifiedBlocked());
return;
}
// We don't need to parse this, so we can just use the string.
final String realInetAddress = "/" + inetAddress;
final InetAddress inetAddress = getInetAddressIfValid(invocation.getSender(), rawInetAddress);
// Make sure the given IP address is valid
if (inetAddress == null) return;
if (LOCK.contains(realInetAddress)) {
invocation.getSender().sendMessage(SONAR.getConfig().VERIFIED_BLOCKED);
return;
}
if (!SONAR.getVerifiedPlayerController().has(realInetAddress)) {
invocation.getSender().sendMessage(SONAR.getConfig().VERIFIED_NOT_FOUND);
// Make sure the player is verified already
if (!SONAR.getVerifiedPlayerController().has(inetAddress.toString())) {
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getVerifiedNotFound());
return;
}
// Lock the IP address
// Make sure we don't accidentally run 2 operations at the same time
LOCK.add(realInetAddress);
SONAR.getVerifiedPlayerController().remove(realInetAddress);
LOCK.remove(realInetAddress);
LOCK.add(rawInetAddress);
SONAR.getVerifiedPlayerController().remove(inetAddress.toString());
invocation.getSender().sendMessage(SONAR.getConfig().VERIFIED_REMOVE
.replace("%ip%", inetAddress));
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getVerifiedRemove()
.replace("%ip%", rawInetAddress));
// Unlock the IP address
LOCK.remove(rawInetAddress);
break;
}
@@ -115,19 +111,19 @@ public final class VerifiedCommand extends Subcommand {
final int verified = SONAR.getVerifiedPlayerController().estimatedSize();
if (verified == 0) {
invocation.getSender().sendMessage(SONAR.getConfig().VERIFIED_EMPTY);
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getVerifiedEmpty());
return;
}
// Invalidate all cache entries
SONAR.getVerifiedPlayerController().clearAll();
invocation.getSender().sendMessage(SONAR.getConfig().VERIFIED_CLEARED
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getVerifiedCleared()
.replace("%removed%", Sonar.DECIMAL_FORMAT.format(verified)));
break;
}
case "size": {
invocation.getSender().sendMessage(SONAR.getConfig().VERIFIED_SIZE
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getVerifiedSize()
.replace("%amount%", Sonar.DECIMAL_FORMAT.format(SONAR.getVerifiedPlayerController().estimatedSize())));
break;
}
@@ -0,0 +1,113 @@
package xyz.jonesdev.sonar.common.update;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import lombok.experimental.UtilityClass;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.logger.LoggerWrapper;
import javax.net.ssl.HttpsURLConnection;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@UtilityClass
public class UpdateChecker {
private final ExecutorService ASYNC_EXECUTOR = Executors.newSingleThreadExecutor();
private final LoggerWrapper LOGGER = new LoggerWrapper() {
@Override
public void info(final String message, final Object... args) {
Sonar.get().getLogger().info("[update-checker] " + message, args);
}
@Override
public void warn(final String message, final Object... args) {
Sonar.get().getLogger().warn("[update-checker] " + message, args);
}
@Override
public void error(final String message, final Object... args) {
Sonar.get().getLogger().error("[update-checker] " + message, args);
}
};
private int convertVersion(final @NotNull String version) throws NumberFormatException, IllegalStateException {
final String[] convertedParts = version.split("\\.");
// Validate the length of the converted parts
if (convertedParts.length != 3) {
throw new IllegalStateException("Converted version length mismatch");
}
// Major updates are more important than minor updates,
// and minor updates are more important than patches.
final int convertedMajor = Integer.parseInt(convertedParts[0]) * 100000; // multiply by weight
final int convertedMinor = Integer.parseInt(convertedParts[1]) * 10000; // multiply by weight
final int patch = Integer.parseInt(convertedParts[2]);
return convertedMajor + convertedMinor + patch; // sum the converted version parts
}
public void checkForUpdates() {
ASYNC_EXECUTOR.execute(() -> {
try {
final URL url = new URL("https://api.github.com/repos/jonesdevelopment/sonar/releases/latest");
final HttpsURLConnection urlConnection = prepareConnection(url);
final JsonObject json = parseJson(urlConnection.getInputStream());
final String latestStableRelease = json.get("tag_name").getAsString();
final int convertedLatestVersion = convertVersion(latestStableRelease);
final int convertedCurrentVersion = convertVersion(Sonar.get().getVersion().getSemanticVersion());
if (convertedCurrentVersion < convertedLatestVersion) {
LOGGER.warn("A new version of Sonar is available: {}", latestStableRelease);
LOGGER.warn("Please make sure to update to the latest version to ensure stability and security:");
LOGGER.warn("https://github.com/jonesdevelopment/sonar/releases/tag/{}", latestStableRelease);
} else if (convertedCurrentVersion > convertedLatestVersion || !Sonar.get().getVersion().isOnMainBranch()) {
LOGGER.warn("You are currently using an unreleased version of Sonar!");
LOGGER.warn("The contributors of Sonar are not responsible for any damage done by using an unstable version");
} else {
LOGGER.info("You are currently using the latest stable release of Sonar!");
}
} catch (Exception exception) {
LOGGER.warn("Could not retrieve latest version information: {}", exception);
}
});
}
private @NotNull JsonObject parseJson(final @NotNull InputStream inputStream) throws IOException {
final InputStreamReader inputStreamReader = new InputStreamReader(inputStream);
final StringBuilder content = new StringBuilder();
// Read the entire page content
try (final BufferedReader bufferedReader = new BufferedReader(inputStreamReader)) {
String line;
while ((line = bufferedReader.readLine()) != null) {
content.append(line);
}
}
// Parse site content as JSON using Gson
final JsonParser jsonParser = new JsonParser();
return jsonParser.parse(content.toString()).getAsJsonObject();
}
private @NotNull HttpsURLConnection prepareConnection(final @NotNull URL url) throws Exception {
final HttpsURLConnection urlConnection = (HttpsURLConnection) url.openConnection();
// Set connection timeout
urlConnection.setConnectTimeout(15000);
urlConnection.setReadTimeout(15000);
// Set necessary properties
urlConnection.setRequestMethod("GET");
urlConnection.setRequestProperty("Accept", "application/vnd.github.v3+json");
// Only continue if the connection is a success
if (urlConnection.getResponseCode() != 200) {
throw new IllegalStateException("Unexpected response code " + urlConnection.getResponseCode());
}
return urlConnection;
}
}
@@ -21,6 +21,7 @@ import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufOutputStream;
import io.netty.buffer.ByteBufUtil;
import io.netty.handler.codec.CorruptedFrameException;
import io.netty.handler.codec.DecoderException;
import io.netty.handler.codec.EncoderException;
import lombok.experimental.UtilityClass;
import net.kyori.adventure.nbt.BinaryTagIO;
@@ -33,7 +34,7 @@ import java.nio.charset.StandardCharsets;
import java.util.UUID;
import java.util.regex.Pattern;
// Mostly taken from
// Taken from
// https://github.com/PaperMC/Velocity/blob/dev/3.0.0/proxy/src/main/java/com/velocitypowered/proxy/protocol/ProtocolUtils.java
@UtilityClass
public class ProtocolUtil {
@@ -47,40 +48,41 @@ public class ProtocolUtil {
private static final String UNREGISTER_CHANNEL = "minecraft:unregister";
private static final Pattern INVALID_IDENTIFIER_REGEX = Pattern.compile("[^a-z0-9\\-_]*");
public static @NotNull String readString(final ByteBuf buf) throws CorruptedFrameException {
return readString(buf, Short.MAX_VALUE);
public static @NotNull String readBrandMessage(final @NotNull ByteBuf content) throws DecoderException {
final ByteBuf slice = content.slice();
try {
return readString(slice);
} catch (DecoderException exception) {
return readString(slice, 65536, slice.readableBytes());
}
}
public static @NotNull String readString(final ByteBuf buf,
public static @NotNull String readString(final ByteBuf byteBuf) throws CorruptedFrameException {
return readString(byteBuf, Short.MAX_VALUE);
}
public static @NotNull String readString(final ByteBuf byteBuf,
final int cap) throws CorruptedFrameException {
return readString(buf, cap, false);
final int length = VarIntUtil.readVarInt(byteBuf);
return readString(byteBuf, cap, length);
}
public static @NotNull String readString(final ByteBuf buf,
public static @NotNull String readString(final @NotNull ByteBuf byteBuf,
final int cap,
final boolean legacy) throws CorruptedFrameException {
final int length = VarIntUtil.readVarInt(buf);
final int length) throws CorruptedFrameException {
checkFrame(length >= 0, "Got a negative-length string");
checkFrame(length <= cap * 3, "Bad string size");
String str;
if (legacy) {
// TODO: length checking?
str = buf.toString(StandardCharsets.UTF_8);
buf.skipBytes(buf.readableBytes());
} else {
checkFrame(buf.isReadable(length), "Got an invalid string length");
str = buf.toString(buf.readerIndex(), length, StandardCharsets.UTF_8);
buf.skipBytes(length);
}
checkFrame(byteBuf.isReadable(length), "Tried to read a too-long string");
final String str = byteBuf.toString(byteBuf.readerIndex(), length, StandardCharsets.UTF_8);
byteBuf.skipBytes(length);
checkFrame(str.length() <= cap, "Got a too-long string");
return str;
}
public static void writeString(final ByteBuf buf, final CharSequence str) {
public static void writeString(final ByteBuf byteBuf, final CharSequence str) {
final int size = ByteBufUtil.utf8Bytes(str);
VarIntUtil.writeVarInt(buf, size);
buf.writeCharSequence(str, StandardCharsets.UTF_8);
VarIntUtil.writeVarInt(byteBuf, size);
byteBuf.writeCharSequence(str, StandardCharsets.UTF_8);
}
public static void writeUUID(final @NotNull ByteBuf byteBuf, final @NotNull UUID uuid) {
+19 -3
View File
@@ -4,17 +4,33 @@ dependencies {
compileOnly(project(":api"))
compileOnly(project(":common"))
compileOnly("com.velocitypowered:velocity-proxy:$velocityVersion") // Proxy module
compileOnly("com.velocitypowered:velocity-proxy:$velocityVersion")
testCompileOnly("com.velocitypowered:velocity-proxy:$velocityVersion")
compileOnly("com.velocitypowered:velocity-api:$velocityVersion")
annotationProcessor("com.velocitypowered:velocity-api:$velocityVersion")
//annotationProcessor("com.velocitypowered:velocity-api:$velocityVersion")
testCompileOnly("com.velocitypowered:velocity-api:$velocityVersion")
testAnnotationProcessor("com.velocitypowered:velocity-api:$velocityVersion")
//testAnnotationProcessor("com.velocitypowered:velocity-api:$velocityVersion")
// Implement bStats.org for metrics
implementation("org.bstats:bstats-velocity:3.0.2")
}
tasks {
processResources {
val props = mapOf(
"version" to rootProject.version.toString().split("-")[0],
"description" to rootProject.description,
"url" to "https://jonesdev.xyz/discord/",
"main" to "xyz.jonesdev.sonar.velocity.SonarVelocityPlugin"
)
inputs.properties(props)
filesMatching("velocity-plugin.json") {
expand(props)
}
}
}
java.sourceCompatibility = JavaVersion.VERSION_11
java.targetCompatibility = JavaVersion.VERSION_11
@@ -18,19 +18,15 @@
package xyz.jonesdev.sonar.velocity;
import lombok.Getter;
import net.kyori.adventure.text.Component;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.SonarPlatform;
import xyz.jonesdev.sonar.api.command.InvocationSender;
import xyz.jonesdev.sonar.api.fallback.traffic.TrafficCounter;
import xyz.jonesdev.sonar.api.logger.LoggerWrapper;
import xyz.jonesdev.sonar.api.server.ServerWrapper;
import xyz.jonesdev.sonar.common.boot.SonarBootstrap;
import xyz.jonesdev.sonar.velocity.command.SonarCommand;
import xyz.jonesdev.sonar.velocity.audience.AudienceListener;
import xyz.jonesdev.sonar.velocity.command.VelocitySonarCommand;
import xyz.jonesdev.sonar.velocity.fallback.FallbackListener;
import xyz.jonesdev.sonar.velocity.verbose.VerboseWrapper;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
@Getter
@@ -38,7 +34,7 @@ public final class SonarVelocity extends SonarBootstrap<SonarVelocityPlugin> {
public static SonarVelocity INSTANCE;
public SonarVelocity(final @NotNull SonarVelocityPlugin plugin) {
super(plugin, plugin.getDataDirectory().toFile(), new VerboseWrapper(plugin.getServer()));
super(plugin, plugin.getDataDirectory().toFile(), SonarPlatform.VELOCITY);
INSTANCE = this;
}
@@ -66,51 +62,21 @@ public final class SonarVelocity extends SonarBootstrap<SonarVelocityPlugin> {
}
};
/**
* Create a wrapper object for our server, so we can use it outside
* the velocity module.
* <br>
* We have to do this, so we can access all necessary API functions.
*/
public final ServerWrapper server = new ServerWrapper() {
@Override
public SonarPlatform getPlatform() {
return SonarPlatform.VELOCITY;
}
@Override
public Optional<InvocationSender> getOnlinePlayer(final String username) {
return getPlugin().getServer().getAllPlayers().stream()
.filter(player -> player.getUsername().equalsIgnoreCase(username))
.findFirst()
.map(player -> new InvocationSender() {
@Override
public String getName() {
return player.getUsername();
}
@Override
public void sendMessage(final String message) {
player.sendMessage(Component.text(message));
}
});
}
};
@Override
public void enable() {
// Initialize bStats.org metrics
getPlugin().getMetricsFactory().make(getPlugin(), getServer().getPlatform().getMetricsId());
getPlugin().getMetricsFactory().make(getPlugin(), getPlatform().getMetricsId());
// Register Sonar command
getPlugin().getServer().getCommandManager().register("sonar", new SonarCommand());
getPlugin().getServer().getCommandManager().register("sonar", new VelocitySonarCommand());
// Register Fallback listener
getPlugin().getServer().getEventManager().register(getPlugin(), new FallbackListener(getFallback()));
// Register audience register listener
getPlugin().getServer().getEventManager().register(getPlugin(), new AudienceListener());
// Register Fallback queue task
getPlugin().getServer().getScheduler().buildTask(getPlugin(), getFallback().getQueue()::poll)
.repeat(500L, TimeUnit.MILLISECONDS)
@@ -126,11 +92,4 @@ public final class SonarVelocity extends SonarBootstrap<SonarVelocityPlugin> {
.repeat(100L, TimeUnit.MILLISECONDS)
.schedule();
}
@Override
public void postReload() {
// Prepare cached messages
FallbackListener.CachedMessages.update();
}
}
@@ -31,9 +31,10 @@ import org.slf4j.Logger;
import java.nio.file.Path;
@Getter
@Plugin(id = "sonar",
@Plugin(
id = "sonar",
name = "Sonar",
version = "2.0.0",
version = "$version",
authors = {"Jones Development", "Sonar Contributors"},
url = "https://jonesdev.xyz/discord/",
description = "Effective Anti-bot plugin for Velocity, BungeeCord, and Bukkit (1.7-latest)"
@@ -0,0 +1,38 @@
/*
* Copyright (C) 2023 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.velocity.audience;
import com.velocitypowered.api.event.PostOrder;
import com.velocitypowered.api.event.Subscribe;
import com.velocitypowered.api.event.connection.DisconnectEvent;
import com.velocitypowered.api.event.connection.PostLoginEvent;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.Sonar;
public final class AudienceListener {
@Subscribe(order = PostOrder.LAST)
public void handle(final @NotNull PostLoginEvent event) {
Sonar.get().getVerboseHandler().getAudiences().put(event.getPlayer().getUsername(), event.getPlayer());
}
@Subscribe(order = PostOrder.LAST)
public void handle(final @NotNull DisconnectEvent event) {
Sonar.get().getVerboseHandler().getAudiences().remove(event.getPlayer().getUsername());
}
}
Loaded 100 of 107 files, more files were not shown because too many files have changed in this diff. Show more