Compare commits

...
139 Commits
Author SHA1 Message Date
Jones e6c3dc9d2b Release 2.1.2 (#368) 2024-08-28 13:06:04 +02:00
Jones a3e98e5e51 Recode CAPTCHA generation (#367)
* feat: fully recode CAPTCHA generation

* style: remove unused explicit cast

* fix(captcha): increase color brightness of second gradient

* style: fix some comments

* chore: fix copyright headers

* refactor: move ttf font loading into separate class

* style: fix some comments

* refactor(captcha): move to array for glyphs

* feat(captcha): recode color gradient algorithm

* fix(captcha): only randomize stroke width once

* feat(captcha): re-add custom background support

* feat(fallback): optimize pre-queue process

* chore: minor code improvements

* feat: update messages for new CAPTCHA

* fix: use 0 for total experience in SetExp packet

* feat(captcha): [wip] circle inverse filter

* fix(captcha): make expected CAPTCHA answer lowercase

* feat: use (actually legal-to-use) typewriter font

* feat: add NoiseOverlayFilter

* feat: automatically adapt the font scale

* feat(captcha): improve text positioning and coloring

* feat(captcha): add optional SmearOverlayFilter

* feat(captcha): add random glyph outlines

* fix: set debug image generation amount to 1

* refactor: use JHLab's smear filter

* style: correct some spelling mistakes in comments

* fix: let the system decide the random seed
2024-08-28 12:01:41 +02:00
Jones 769d1f2ddb Bump h2databases from 2.1.214 to 2.2.220 2024-08-27 17:36:53 +02:00
Jones f977ae4fe5 Bump mysql-connector-j from 8.4.0 to 9.0.0 2024-08-27 17:33:44 +02:00
Jones 9b11c4c1e0 Bump mariadb-java-client from 3.4.0 to 3.4.1 2024-08-27 17:30:16 +02:00
Jones 44afb98b3c Fix Bedrock false positive with rotation packets 2024-08-27 17:06:28 +02:00
Jones 1e246472cc Bump Gradle from 8.9 to 8.10 2024-08-27 14:57:48 +02:00
Jones 77ee422c64 Fix Bedrock false flagging the vehicle speed check 2024-08-27 14:04:27 +02:00
dependabot[bot] 756d7e1f89 Bump bstats from 3.0.2 to 3.0.3 (#364)
Bumps `bstats` from 3.0.2 to 3.0.3.

Updates `org.bstats:bstats-bungeecord` from 3.0.2 to 3.0.3
- [Release notes](https://github.com/Bastian/bStats-Metrics/releases)
- [Commits](https://github.com/Bastian/bStats-Metrics/compare/v3.0.2...v3.0.3)

Updates `org.bstats:bstats-bukkit` from 3.0.2 to 3.0.3
- [Release notes](https://github.com/Bastian/bStats-Metrics/releases)
- [Commits](https://github.com/Bastian/bStats-Metrics/compare/v3.0.2...v3.0.3)

Updates `org.bstats:bstats-velocity` from 3.0.2 to 3.0.3
- [Release notes](https://github.com/Bastian/bStats-Metrics/releases)
- [Commits](https://github.com/Bastian/bStats-Metrics/compare/v3.0.2...v3.0.3)

---
updated-dependencies:
- dependency-name: org.bstats:bstats-bungeecord
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.bstats:bstats-bukkit
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.bstats:bstats-velocity
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-26 14:13:16 +02:00
Jones 2ac4b7aad3 Don't create duplicate entries in database 2024-08-24 14:49:30 +02:00
Jones 5726e2d3d2 Exempt fake (spoofed) players from being checked 2024-08-20 17:04:00 +02:00
Jones 7aea5754a9 Enable org.gradle.daemon to speed up build times 2024-08-18 16:40:45 +02:00
Jones c10adc37be Move net.minecrell.plugin-yml to version catalog 2024-08-18 16:35:50 +02:00
Jones 465d81cbc2 Release 2.1.1 (#363) 2024-08-16 14:53:10 +02:00
Michel Elkenwaat 0152e45190 Log client brand and locale if invalid 2024-08-14 19:25:37 +02:00
Michel Elkenwaat 029f7920d0 Update copyright header 2024-08-13 20:41:03 +02:00
Michel Elkenwaat 0f5184297c Fix incorrect usage of libby since https://github.com/jonesdevelopment/sonar/commit/55030c5073b845a900de444d3724790f29450f66 2024-08-12 18:42:19 +02:00
Michel Elkenwaat d15b3c86ad Bump com.google.code.gson:gson from 2.10.1 to 2.11.0 2024-08-12 18:35:44 +02:00
Michel Elkenwaat 0a26cebaa5 Use updated shadow plugin (https://github.com/GradleUp/shadow/issues/908) 2024-08-12 17:59:34 +02:00
dependabot[bot] 16cf92cd17 Bump org.jetbrains:annotations from 24.0.1 to 24.1.0 (#362)
Bumps [org.jetbrains:annotations](https://github.com/JetBrains/java-annotations) from 24.0.1 to 24.1.0.
- [Release notes](https://github.com/JetBrains/java-annotations/releases)
- [Changelog](https://github.com/JetBrains/java-annotations/blob/master/CHANGELOG.md)
- [Commits](https://github.com/JetBrains/java-annotations/compare/24.0.1...24.1.0)

---
updated-dependencies:
- dependency-name: org.jetbrains:annotations
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-12 02:33:18 +02:00
Michel Elkenwaat 2bac0458dd Use rotation packets to improve the vehicle check 2024-08-11 20:00:41 +02:00
Michel Elkenwaat de9bb44bd0 Add verification.checks.vehicle.minimum-packets to config 2024-08-11 19:44:02 +02:00
Jones 0137fdf7e9 Release 2.1.0 (#361) 2024-08-10 14:25:39 +02:00
Jones df14e42a0b Improve the collision check (#360)
* feat: add iron trapdoor block

* feat: add enchantment table block

* fix: rename Captcha to CAPTCHA

* feat: randomize vehicle entity ID

* feat: make collision platform block type configurable

* fix: only send experience bar if it's visible

* feat: change vehicle "timing" to "enabled"

* fix(fallback): captcha-on-fail not catching every scenario

* style: oopsie doopsie
2024-08-10 14:08:28 +02:00
Michel Elkenwaat c30dd0225b Implement missing "fell through blocks" check 2024-08-09 04:22:31 +02:00
Michel Elkenwaat 626fa422bc Fix some grammar mistakes in comments 2024-08-09 01:32:59 +02:00
Michel Elkenwaat f08d9239fe Fix verification.currently-preparing not being used 2024-08-08 23:29:14 +02:00
Michel Elkenwaat 1edd71df04 Fix Bedrock players not being able to be transferred (https://github.com/jonesdevelopment/sonar/issues/356) 2024-08-08 22:28:39 +02:00
aff3bd4a64 Implement Bukkit Support (#347)
* feat: deject packet listener if not needed anymore

* fix(db): dynamically load drivers libraries

* refactor: outline VerifiedPlayerController#exactSize

* fix(db): small synchronization issues in VerifiedPlayerController

* feat: enforce Netty 4.1.x (or higher)

* feat: [wip] Bukkit injection (NMS/OBC stuff)

* fix: include MIT license header for fast varint reading

* fix: allow Netty version to remain unknown

* fix: optimize "command sender -> audience" logic

* fix: extremely rare NoSuchElementException

* fix: remove equals and hashcode annotations

* build(deps): add annotations and simpleyaml to catalog

* feat: [wip] implement Bukkit injection

* feat: [wip] Bukkit packet reading and general improvements

* refactor: small code style improvements

* style: link wiki.vg in comments

* fix: don't store unnecessary pipeline names

* fix: transfer state not working on Bukkit

* fix: possible bypass with the transfer mode exemption

* fix(bukkit): library folder having the wrong name

* feat: make cached player username configurable

* fix: add missing configuration entry to pl.yml

* fix: pipeline order breaking everything ¯\_(ツ)_/¯

* fix: exempt geyser pipelines from being removed... oops

* fix: injection error on 1.20.5+ servers

* build(bukkit): use 1.13.2 spigot api instead of 1.8.8

* fix: legacy plugin warning by setting the api version to 1.13

* fix: use entity type in SpawnEntityPacket

* feat: improve the vehicle check by a lot

* fix: pipeline exception tail error

* fix: only compute player if actually connected (https://github.com/jonesdevelopment/sonar/issues/349)

* style: static import consistency

* fix: MessageToMessageDecoder literally breaking HAProxy

* feat: add more bStats metrics (language and db type)

* style: small code clean up

* feat: remove unnecessary plugin-yml generation arguments

* fix(bukkit): use the actual server name for debugging

* feat: don't shadow adventure-nbt

* chore: link transfer tutorial in configuration

* fix: CI pipeline build error

* build: mark all gradle tasks as non-deamon

* fix: vehicle check 2.0 not being compatible with Geyser

* refactor: *PacketDecoder -> *InboundHandler

* fix: SetPlayerPositionRotationPacket onGround not being used

* feat: recode vehicle check to work with laggy connections

* fix: remove debug message(s)

* feat: recode vehicle check again

* fix: don't remove the entity twice

* feat: [wip] implement Bukkit support (it's looking good)

* fix: wrong exception being caught

* fix: improve error handling in reflective operations

* feat: optimize verification routines

* feat: optimize and improve queue computing

* fix: double/float comparison being broken haha

* style: small comment improvements

* style: fix invalid comments in vehicle check

* Bug fixes in `bukkit-support` branch (#358)

* Lazy load for ServerConnection to support late-bind

* Remove `sonar-inbound-handler` when join server

* Add missing license header in `Lazy.java`

* Update FallbackBukkitInjector

* Change `Bukkit#getScheduler` to `getPlugin().getServer().getScheduler()`

* Inject the new initializer to close connection if the plugin is not enabled.

* Don't remove `DefaultChannelPipeline$TailContext#0` in pipeline.

* Add listener for close uninject player's connection if `late-bind` is disabled.

* Fixed an issue where `max-online-per-ip` was not working properly.

* Get `ServerConnection` in method.

* Change overwrite methods position.

* Move `ChannelInactiveListener` to `sonar-api`

* style(bukkit): create unreflected fields in a static context

* style(bukkit): use lombok Getter for late bind

* build: fix compilation process

* refactor: move `INITIALIZE_LISTENER` from SonarBukkitPlugin to SonarBukkit

* style(bukkit): improve codestyle in the inbound handler

* style: fix wrong comment

---------

Co-authored-by: Michel Elkenwaat <admin@jonesdev.xyz>

* style: add missing credits

* build(deps): bump adventure-platform from 4.3.3 to 4.3.4

* fix: don't assign unused fields

* fix: use encoder function instead of manual encoder check

* fix: use encoder function, not handler function x)

* fix: optimize bukkit encoder function by using `context`, not `get`

* fix: memory leak in the login packet decoder

---------

Co-authored-by: Akkariin <fallencrystal@outlook.com>
2024-08-08 21:13:16 +02:00
Michel Elkenwaat ce1d623d60 Allow general.max-online-per-ip to be 0 2024-08-03 19:29:15 +02:00
Jones 77716c0fcc Release 2.0.38 (#357) 2024-07-30 19:56:39 +02:00
dependabot[bot] c34a3d7dc2 Bump netty-all from 4.1.111.Final to 4.1.112.Final (#355)
Bumps [io.netty:netty-all](https://github.com/netty/netty) from 4.1.111.Final to 4.1.112.Final.
- [Commits](https://github.com/netty/netty/compare/netty-4.1.111.Final...netty-4.1.112.Final)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-22 17:17:21 +02:00
Michel Elkenwaat 4fdb7cc70c Fix IP addresses being parsed incorrectly 2024-07-19 15:39:57 +02:00
slosacoder 7ddca3e152 Update Czech translations (#354) 2024-07-16 18:40:49 +02:00
Michel Elkenwaat e01a226475 Bump Gradle from 8.8 to 8.9 2024-07-15 19:36:38 +02:00
Michel Elkenwaat 21d983362e Fix an invalid character in the Georgian translations 2024-07-12 19:00:16 +02:00
Michel Elkenwaat ab5203f4c5 Fix a small translation error 2024-07-12 18:59:32 +02:00
GreenedDev 03c209829c Fix grammar errors in Georgian config (#350) 2024-07-12 16:33:32 +02:00
GreenedDev af57d6de42 Add Georgian messages translations (#351) 2024-07-12 16:32:31 +02:00
Michel Elkenwaat d1f8ab4f10 Add Polish translations (thanks to Najek and foren) 2024-07-11 19:14:09 +02:00
Jones c12d77fb8d Release 2.0.37 (#346) 2024-07-04 16:24:03 +02:00
Michel Elkenwaat a9a23aaf2f Fix client brand reading (Revert https://github.com/jonesdevelopment/sonar/commit/e0cc9bdab3ee93a5c328f3015bd3564f31ed7e0c) 2024-07-04 16:19:11 +02:00
Jones 58d0c57ee8 Release 2.0.36 (#345) 2024-07-03 14:47:01 +02:00
Michel Elkenwaat 5546d2695d Add Indonesian translations (thanks to LourenT) 2024-07-02 14:19:43 +02:00
dependabot[bot] 64688ba06b Bump lombok from 1.18.32 to 1.18.34 (#343)
Bumps [org.projectlombok:lombok](https://github.com/projectlombok/lombok) from 1.18.32 to 1.18.34.
- [Changelog](https://github.com/projectlombok/lombok/blob/master/doc/changelog.markdown)
- [Commits](https://github.com/projectlombok/lombok/compare/v1.18.32...v1.18.34)

---
updated-dependencies:
- dependency-name: org.projectlombok:lombok
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-01 15:23:27 +02:00
dependabot[bot] e85036dd13 Bump io.github.goooler.shadow from 8.1.7 to 8.1.8 (#344)
Bumps [io.github.goooler.shadow](https://github.com/Goooler/shadow) from 8.1.7 to 8.1.8.
- [Release notes](https://github.com/Goooler/shadow/releases)
- [Commits](https://github.com/Goooler/shadow/compare/v8.1.7...v8.1.8)

---
updated-dependencies:
- dependency-name: io.github.goooler.shadow
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-01 15:23:08 +02:00
Michel Elkenwaat 2b2b481c14 Globalize and simplify per-platform injection 2024-06-30 13:41:07 +02:00
Michel Elkenwaat e0cc9bdab3 Simplify client brand reading for 1.8+ 2024-06-30 13:17:07 +02:00
Jones 8aa05563d5 Add support for H2 databases (#342)
* build(deps): bump MySQL driver from 8.3.0 to 8.4.0

* build(deps): bump mariadb-java-client from 3.3.3 to 3.4.0

* feat: add h2 local database support

* style: fix javadoc for db type adapters [ci skip]
2024-06-30 12:40:28 +02:00
Jones 21eb949bae Release 2.0.35 (#340) 2024-06-29 13:07:07 +02:00
Michel Elkenwaat 3a978b39ee Simplify transfer packet sending for 1.20.5+ 2024-06-29 12:48:31 +02:00
Michel Elkenwaat efc435df61 Remove an unnecessary try-catch statement 2024-06-29 12:45:51 +02:00
Michel Elkenwaat 8baa920279 Fix possible race condition in the disconnect logic 2024-06-29 12:44:44 +02:00
Michel Elkenwaat 647931c31e Make subcommand descriptions translatable 2024-06-25 17:25:34 +02:00
Michel Elkenwaat c8c3685355 Simplify update notifier json parsing 2024-06-25 15:25:23 +02:00
Michel Elkenwaat b759082416 Use Indra for better global versioning 2024-06-24 16:01:32 +02:00
Michel Elkenwaat 31a09d60ac Add Spanish webhook translations (thanks to Juansitoh) 2024-06-22 00:40:15 +02:00
Michel Elkenwaat 45fb866c1c Fix online per IP being calculated incorrectly 2024-06-21 19:03:11 +02:00
Michel Elkenwaat 9f8d828622 Add Spanish message translations (thanks to Juansitoh) 2024-06-21 14:25:31 +02:00
Jones 6e78c875a2 Release 2.0.34 (#337) 2024-06-18 19:00:10 +02:00
Michel Elkenwaat 18486b9286 Remove unnecessary UncaughtExceptionHandler 2024-06-18 17:06:24 +02:00
Michel Elkenwaat 644d1387ce Fix notifications breaking scheduled tasks 2024-06-18 16:57:20 +02:00
Jones e80311bf24 Release 2.0.33 (#336)
* build: bump version from 2.0.32 to 2.0.33

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

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

* feat: [wip] migrate to MiniMessage placeholders

* fix: recode verbose placeholder handling

* fix: throw away packets if not needed

* fix: main command help hovering being broken

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

* fix: add missing permission check to sonar command

* fix: add missing prefix placeholder to no permission message

* feat: remove system load average from profiler

* feat: add more debug to missing language error

* fix: make sure it can actually compile B)

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

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

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

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

* refactor: don't store movementTick in localVar

---------

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

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

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

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

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

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

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

* fix: plugin message packet id from wiki.vg

* chore: add debug for encoder/decoder

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

* feat: add codec for 1.21

* feat: update codec and encode registry for 1.21

* fix: inline registry packets if-check

* fix: apply 1.20.5 block/item ids for 1.21

* fix: apply 1.20.5 entity id for 1.21

* chore: remove debug message from encoder/decoder

* style: accidental deletion of line breaks in decoder

---------

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

* feat: [wip] add CAPTCHA session handler

* feat: add CAPTCHA xp bar and KeepAlive

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

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

* fix(fallback): NullPointerException

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

* style: fix some doc comments

* style: add doc comment to FallbackVehicleSessionHandler

* style: add doc comment to FallbackCAPTCHASessionHandler

* fix: prevent users from sending multiple login ack packets

* style: rename variable for clarification

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

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

* fix: check for duplicate PluginMessage packets

* fix: oops, I broke the plugin message check

* refactor: restructure verification failing logic

* fix: mark LoginSuccess packet as encode-only

* refactor: store channel pipeline in FallbackUser

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

* refactor(common): small maintainability changes

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

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

* fix: remove unnecessary packet filter in vehicle check

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

* style: improve/simplify initialization of VerifiedPlayer

* fix: wrong name resulting in compilation error

* chore: fix a broken documentation link in the readme

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

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

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

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

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

* fix: recode fallback queue polling and threading

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

* fix: improve Fallback's custom read timeout handler

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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-27 14:08:19 +02:00
Jones 9bc3d73cb7 Release 2.0.31 (#308) 2024-05-25 15:10:26 +02:00
Jones d3aaba4650 Update translations (#307)
* fix: some English grammar mistakes

* fix: add missing Dutch translation string

* fix: add missing Georgian translations
2024-05-23 20:21:49 +02:00
Jones 68d99dd59b Update and improve some packet-related code (#306)
* fix: allow packet encode and decode method to throw exceptions

* fix: remove unnecessary annotations from FinishConfigurationPacket

* fix: don't modify the received plugin message channel
2024-05-23 18:12:02 +02:00
Jones 5f77f21ee9 Improve networking performance (#305)
* refactor: small structural changes

* build(deps): bump adventure-nbt from 4.16.0 to 4.17.0

* feat(velocity): use MethodHandles for better performance on connection

* feat(bungee): use MethodHandles for better performance on connection
2024-05-16 21:18:29 +02:00
Jones c5e1d12424 Fix NullPointerException (#304)
* fix: NullPointerException with VerifiedPlayerController

* fix: NullPointerException with Metrics
2024-05-16 20:15:05 +02:00
Jones 25dbbf0b1f Bump Gradle from 8.6 to 8.7 (#303) 2024-05-16 19:55:32 +02:00
Jones cbf04f6f2e Fix error in French webhook translations (#302) 2024-05-15 20:04:47 +02:00
Jones 8966d7d0e8 Add French translations (#300)
* feat: add French translations

* fix: small mistakes in some translations
2024-05-15 19:42:38 +02:00
dependabot[bot] 064a3cee2a Bump adventure-text-serializer-gson to 4.17.0 (#297)
Bumps [net.kyori:adventure-text-serializer-gson](https://github.com/KyoriPowered/adventure) from 4.16.0 to 4.17.0.
- [Release notes](https://github.com/KyoriPowered/adventure/releases)
- [Commits](https://github.com/KyoriPowered/adventure/compare/v4.16.0...v4.17.0)

---
updated-dependencies:
- dependency-name: net.kyori:adventure-text-serializer-gson
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-13 13:51:56 +02:00
dependabot[bot] 260af8b2be Bump adventure-text-minimessage to 4.17.0 (#298)
Bumps [net.kyori:adventure-text-minimessage](https://github.com/KyoriPowered/adventure) from 4.16.0 to 4.17.0.
- [Release notes](https://github.com/KyoriPowered/adventure/releases)
- [Commits](https://github.com/KyoriPowered/adventure/compare/v4.16.0...v4.17.0)

---
updated-dependencies:
- dependency-name: net.kyori:adventure-text-minimessage
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-13 13:50:07 +02:00
Jones a7d52467b8 Release 2.0.30 (#295) 2024-05-11 14:43:09 +02:00
Jones 0ed6b0351c Bump capja from 1.0.2 to 1.0.3 (#294) 2024-05-11 14:39:57 +02:00
Jones a152633b41 Don't cache map data to avoid memory issues (#293) 2024-05-11 14:34:26 +02:00
Jones f618d1ce77 Release 2.0.29 (#291) 2024-05-11 13:34:46 +02:00
Jones ea35904d33 Add more softdepends for compatibility (#290)
* fix(velocity): add more soft dependencies

* fix(bukkit): add more soft dependencies

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

* feat: add missing Czech translations

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

* fix: typo in messages/en.yml

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

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

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

* fix: color palette sometimes showing incorrect colors

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

* fix: actually let the user customize the background

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

* refactor: port pack modified FastUUID for LoginSuccessPacket

* refactor: rename package utility to util

* fix: don't create a snapshot of FinishConfigurationPacket

* fix: don't allow encoding of LoginAcknowledgedPacket

* refactor: PlayerTickPacket -> PlayerPositionIdlePacket

* style: rename some variables for clarity

* fix: optimize UniversalChatPacket

* refactor: ChangedBlock -> BlockUpdate

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

* fix: make sure to also cache MapInfo packets

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

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

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

* fix: optimize chat message reading

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

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

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

* fix: tick skipping is allowed for bedrock clients.

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

* feat: make geyser checks configurable

* style: small codestyle improvements

* style: make code more human-readable

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

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

* style: fix comment

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

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

* fix: only skip vehicle state in position check

* feat: add missing German translations [ci skip]

* feat: add missing Czech translations [ci skip]

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

* style: inline some stuff for better readability

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

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

* fix: add missing translations [ci skip]

---------

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

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

* fix: vehicle & barrier block id

* fix: missing abilities & remove entities packet id

* refactor: merge RegistryBundlePacket into RegistryDataPacket

* style: clean up some code

* feat: implement transfer packet logic for 1.20.5+

* fix: check hostname before protocolVersion

* fix: transfer configuration

* fix: SetSlotPacket for 1.20.5

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

* style: remove double empty line [ci skip]

---------

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

* fix: remove debug messages
2024-04-24 15:15:00 +02:00
dependabot[bot] 279fe2e3e9 Bump netty-all from 4.1.108.Final to 4.1.109.Final (#273)
Bumps [io.netty:netty-all](https://github.com/netty/netty) from 4.1.108.Final to 4.1.109.Final.
- [Commits](https://github.com/netty/netty/compare/netty-4.1.108.Final...netty-4.1.109.Final)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-23 14:50:13 +02:00
Jones ffd3a7e5c2 Improve performance through multi-threading (#271)
* fix: optimize JVMProcessInformation#formatMemory

* fix: formatting (spacing) in formatMemory

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

* fix: optimize chat and title notifications

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

* feat: run all scheduled tasks asynchronously

* fix: small enable message issue

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

* fix: oh yes I love exclamation marks

* feat: move handshake handling logic to common module

* feat: move login handling logic to common module

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

* style: make FallbackChannelHandlerAdapter#initialLogin final

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

* fix: better debug if protocol is invalid

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

* fix: move the unknown protocol version check to login
2024-04-23 14:49:56 +02:00
225 changed files with 11358 additions and 5123 deletions

No files matched your search

+8
View File
@@ -0,0 +1,8 @@
blank_issues_enabled: false
contact_links:
- name: Documentation
url: https://docs.jonesdev.xyz/
about: Need help configuring Sonar or have questions regarding the installation and usage?
- name: Discord
url: https://jonesdev.xyz/discord/
about: If you have any further questions, please feel free to join the Discord server and open a ticket.
+15 -33
View File
@@ -1,12 +1,11 @@
<div align="center">
<!-- Introduction -->
<p>
<h1>💫 Sonar</h1>
Lightweight and easy-to-use anti-bot plugin for your Minecraft server. Supporting every client and server version.
<br>
An effective and extensible solution for protecting your Minecraft server against all kinds of bot attacks
</p>
<h1>💫 Sonar</h1>
Lightweight and easy-to-use anti-bot plugin for your Minecraft server.
<br>
Sonar keeps your server safe from common and even sophisticated bot attacks.
<br><br>
<!-- Badges & icons -->
[![](https://github.com/jonesdevelopment/sonar/actions/workflows/gradle.yml/badge.svg)](https://github.com/jonesdevelopment/sonar/actions/workflows/gradle.yml)
[![](https://www.codefactor.io/repository/github/jonesdevelopment/sonar/badge/main)](https://www.codefactor.io/repository/github/jonesdevelopment/sonar/overview/main)
@@ -15,7 +14,6 @@
[![](https://img.shields.io/discord/923308209769426994.svg?logo=discord)](https://jonesdev.xyz/discord)
[![](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0)
<br>
<br>
<!-- Quick navigation -->
[Releases](https://github.com/jonesdevelopment/sonar/releases)
|
@@ -29,7 +27,6 @@
</div>
## Design and Goal
* Effective, lightweight, and easy-to-use
* No unnecessary features and clean code
* Protection against all kinds of bot attacks
@@ -38,44 +35,29 @@
* Multi-platform support (See [supported versions](https://docs.jonesdev.xyz/sonar/supported-versions))
## Checks
Sonar analyzes a player's behavior before joining the actual server, therefore stopping malicious traffic from ever reaching the backend. It is supposed to be an instant, powerful, and simple method of verification that should prevent all typical and advanced types of bots.
- [Fallback](https://github.com/jonesdevelopment/sonar#fallback) is Sonar's main anti-bot component
- Sonar queues new player logins to prevent spam login attacks
- Sonar checks the handshake packets for legitimacy
- Sonar makes sure some packets cannot be duplicated illegitimately
1. Sonar sends the player to a lightweight fake server when they connect for the first time.
2. Sonar verifies that players obey the laws of Minecraft's physics, including gravity and proper block collision.
3. Sonar verifies that players send legitimate packets when interacting with vehicles (e.g. boats).
4. Sonar makes sure that players send legitimate packets according to the [vanilla Minecraft protocol](<https://wiki.vg/Protocol>).
### Fallback
Fallback analyzes a player's behavior before joining the actual server, therefore stopping malicious traffic from ever reaching the backend.
It is supposed to be an instant, powerful, and invisible method of verification that should prevent all typical and advanced types of bots.
* Sends the player to a lightweight fake server when they connect for the first time.
* Analyzes if the player is sending the necessary packets.
* Analyzes if the player is sending legitimate packets.
* Checks if the player is obeying client gravity.
* Checks if the player is colliding with blocks correctly.
Fallback also protects against huge spambot attacks since it queues the incoming connections, therefore making it technically impossible to have a ton of bots join the server at the same time.
Sonar also protects against spambot attacks since it queues the incoming connections, therefore making it technically impossible to have a ton of bots join the server at the same time.
### False positives
Fallback is unlikely to ever falsely prevent a player from joining the server since Minecraft uses the TCP protocol, which means that packets are always sent in the correct order. Therefore, lag or ping should not affect the bot check. However, there are some edge cases where Fallback might not receive packets within the necessary time period. In this case, Sonar tries to account for these edge cases in order to prevent false positives. If you or one of your players experiences a false positive, make sure to report it by opening a [GitHub issue](https://github.com/jonesdevelopment/sonar/issues) or a ticket on [Discord](https://jonesdev.xyz/discord/).
Sonar is unlikely to ever falsely prevent a player from joining the server since Minecraft uses the TCP protocol, which means that packets are always sent in the correct order. Therefore, lag or ping should not affect the bot check.
If you or one of your players experiences a false positive, make sure to report it by opening a [GitHub issue](https://github.com/jonesdevelopment/sonar/issues/new/choose) or a ticket on the [Discord server](https://jonesdev.xyz/discord/).
## Building
If you want to build your own version of Sonar, please take a look at the [Sonar building documentation](https://docs.jonesdev.xyz/sonar/building).
If you want to build your own version of Sonar, please take a look at the [Sonar building documentation](https://docs.jonesdev.xyz/development/building).
<br>
You can also take a look at the [gradle documentation](https://docs.gradle.org/current/userguide/userguide.html) for a better understanding of Gradle.
## Contributing
If you are interested in contributing, you can check out the [Contributing Guidelines](https://github.com/jonesdevelopment/sonar/blob/main/.github/CONTRIBUTING.md) for detailed instructions.
## License
Sonar is licensed under the [GNU General Public License 3.0](https://www.gnu.org/licenses/gpl-3.0.en.html).
## Credits
- Special thanks to the [contributors of Sonar](https://github.com/jonesdevelopment/sonar/graphs/contributors).
- The Varint decoding was taken from [Velocity](https://github.com/PaperMC/Velocity).
+31 -33
View File
@@ -1,50 +1,34 @@
buildscript {
dependencies {
classpath("gradle.plugin.io.toolebox:gradle-git-versioner:1.6.7")
}
}
import net.kyori.indra.git.IndraGitExtension
plugins {
id("java")
id("io.github.goooler.shadow") version "8.1.7"
id("io.toolebox.git-versioner") version "1.6.7"
}
apply(plugin = "io.toolebox.git-versioner")
versioner {
pattern {
pattern = "$version-%h-%c-%b"
}
java
alias(libs.plugins.shadow)
alias(libs.plugins.indra.git) apply true
}
allprojects {
repositories {
mavenCentral() // Lombok
mavenCentral()
maven(url = "https://repo.jonesdev.xyz/releases/") // Bungee & Velocity proxy module
maven(url = "https://s01.oss.sonatype.org/content/repositories/snapshots/") // libby
}
apply(plugin = "java")
apply(plugin = "io.github.goooler.shadow")
apply(plugin = "com.gradleup.shadow")
dependencies {
compileOnly("org.projectlombok:lombok:1.18.32")
annotationProcessor("org.projectlombok:lombok:1.18.32")
compileOnly(rootProject.libs.lombok)
annotationProcessor(rootProject.libs.lombok)
testCompileOnly("org.projectlombok:lombok:1.18.32")
testAnnotationProcessor("org.projectlombok:lombok:1.18.32")
testCompileOnly(rootProject.libs.lombok)
testAnnotationProcessor(rootProject.libs.lombok)
// adventure
compileOnly("net.kyori:adventure-text-minimessage:4.16.0")
compileOnly("net.kyori:adventure-text-serializer-gson:4.16.0")
compileOnly("com.j256.ormlite:ormlite-jdbc:6.1") // ORMLite
compileOnly("com.github.ben-manes.caffeine:caffeine:3.1.8") // caching
compileOnly("io.netty:netty-all:4.1.108.Final") // netty
// Library/dependency loading
compileOnly("com.alessiodp.libby:libby-core:2.0.0-SNAPSHOT")
compileOnly(rootProject.libs.adventure.minimessage)
compileOnly(rootProject.libs.adventure.serializer)
compileOnly(rootProject.libs.ormlite)
compileOnly(rootProject.libs.caffeine)
compileOnly(rootProject.libs.netty)
compileOnly(rootProject.libs.libby.core)
}
tasks {
@@ -64,10 +48,16 @@ allprojects {
relocate("com.github.benmanes.caffeine", "xyz.jonesdev.sonar.libs.caffeine")
relocate("com.mysql", "xyz.jonesdev.sonar.libs.mysql")
relocate("org.mariadb", "xyz.jonesdev.sonar.libs.mariadb")
relocate("xyz.jonesdev.capja", "xyz.jonesdev.sonar.libs.capja")
relocate("org.h2", "xyz.jonesdev.sonar.libs.h2")
relocate("com.jhlabs", "xyz.jonesdev.sonar.libs.jhlabs")
// Exclude unnecessary metadata information
exclude("META-INF/*/**")
// Minimize shadowed jar file
minimize {
exclude(project(":api"))
}
}
compileJava {
@@ -76,10 +66,18 @@ allprojects {
jar {
manifest {
val indra = rootProject.extensions.getByType(IndraGitExtension::class.java)
val gitBranch = indra.branchName() ?: "unknown"
val gitCommit = indra.commit()?.name?.substring(0, 8) ?: "unknown"
// Set the implementation version, so we can create exact version
// information in-game and make it accessible to the user.
attributes["Implementation-Title"] = rootProject.name
attributes["Implementation-Version"] = rootProject.version
attributes["Implementation-Vendor"] = "Jones Development, Sonar Contributors"
// Include the Git branch and Git commit SHA
attributes["Git-Branch"] = gitBranch
attributes["Git-Commit"] = gitCommit
}
}
}
+2 -2
View File
@@ -1,3 +1,3 @@
group=xyz.jonesdev.sonar
version=2.0.26
description=Effective Anti-bot plugin for Velocity, BungeeCord, and Bukkit (1.7-latest)
version=2.1.2
description=A lightweight, effective, and easy-to-use anti-bot plugin for Velocity, BungeeCord, and Bukkit.
+38
View File
@@ -0,0 +1,38 @@
[versions]
netty = "4.1.112.Final"
adventure = "4.17.0"
adventure-platform = "4.3.4"
bstats = "3.0.3"
libby = "2.0.0-SNAPSHOT"
pluginyml = "0.6.0"
[plugins]
indra-git = "net.kyori.indra.git:3.1.3"
shadow = "com.gradleup.shadow:8.3.0"
pluginyml-bungee = { id = "net.minecrell.plugin-yml.bungee", version.ref = "pluginyml" }
pluginyml-bukkit = { id = "net.minecrell.plugin-yml.bukkit", version.ref = "pluginyml" }
[libraries]
netty = { module = "io.netty:netty-all", version.ref = "netty" }
adventure-minimessage = { module = "net.kyori:adventure-text-minimessage", version.ref = "adventure" }
adventure-serializer = { module = "net.kyori:adventure-text-serializer-gson", version.ref = "adventure" }
adventure-nbt = { module = "net.kyori:adventure-nbt", version.ref = "adventure" }
adventure-platform-bukkit = { module = "net.kyori:adventure-platform-bukkit", version.ref = "adventure-platform" }
adventure-platform-bungee = { module = "net.kyori:adventure-platform-bungeecord", version.ref = "adventure-platform" }
libby-core = { module = "com.alessiodp.libby:libby-core", version.ref = "libby" }
libby-bungee = { module = "com.alessiodp.libby:libby-bungee", version.ref = "libby" }
libby-bukkit = { module = "com.alessiodp.libby:libby-bukkit", version.ref = "libby" }
libby-velocity = { module = "com.alessiodp.libby:libby-velocity", version.ref = "libby" }
bstats-bungee = { module = "org.bstats:bstats-bungeecord", version.ref = "bstats" }
bstats-bukkit = { module = "org.bstats:bstats-bukkit", version.ref = "bstats" }
bstats-velocity = { module = "org.bstats:bstats-velocity", version.ref = "bstats" }
imagefilters = "com.jhlabs:filters:2.0.235-1"
caffeine = "com.github.ben-manes.caffeine:caffeine:3.1.8"
annotations = "org.jetbrains:annotations:24.1.0"
simpleyaml = "com.github.Carleslc.Simple-YAML:Simple-Yaml:1.8.4"
ormlite = "com.j256.ormlite:ormlite-jdbc:6.1"
lombok = "org.projectlombok:lombok:1.18.34"
bungeecord = "net.md_5:bungeecord-proxy:master-SNAPSHOT"
velocity = "com.velocitypowered:velocity-proxy:3.3.0-SNAPSHOT"
# We have to use 1.8.8 for backwards compatibility
spigot = "org.spigotmc:spigot-api:1.8.8-R0.1-SNAPSHOT"
Binary file not shown.
+1 -1
View File
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
Vendored
+13 -9
View File
@@ -15,6 +15,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
@@ -55,7 +57,7 @@
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
@@ -83,7 +85,9 @@ done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
@@ -144,7 +148,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
@@ -152,7 +156,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
@@ -201,11 +205,11 @@ fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
Vendored
+12 -10
View File
@@ -13,6 +13,8 @@
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@@ -43,11 +45,11 @@ set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
@@ -57,11 +59,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
+1 -1
View File
@@ -1,6 +1,6 @@
rootProject.name = "Sonar"
sequenceOf("api", "bukkit", "bungee", "common", "velocity").forEach {
sequenceOf("api", "captcha", "common", "bukkit", "bungee", "velocity").forEach {
val path = "sonar-$it"
val project = ":$it"
+2 -3
View File
@@ -3,11 +3,10 @@ repositories {
}
dependencies {
compileOnly("com.github.Carleslc.Simple-YAML:Simple-Yaml:1.8.4") {
compileOnly(rootProject.libs.simpleyaml) {
exclude(group = "org.yaml")
}
compileOnly("org.jetbrains:annotations:24.0.1")
compileOnly(rootProject.libs.annotations)
}
java.sourceCompatibility = JavaVersion.VERSION_11
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023-2024 Sonar Contributors
* Copyright (C) 2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023-2024 Sonar Contributors
* Copyright (C) 2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023-2024 Sonar Contributors
* Copyright (C) 2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -33,8 +33,6 @@ import xyz.jonesdev.sonar.api.timer.SystemTimer;
import xyz.jonesdev.sonar.api.verbose.Notification;
import xyz.jonesdev.sonar.api.verbose.Verbose;
import java.io.File;
import java.net.InetAddress;
import java.text.DecimalFormat;
import java.util.UUID;
@@ -48,23 +46,19 @@ public interface Sonar {
@ApiStatus.Internal
@Nullable Audience audience(final @Nullable UUID uniqueId);
@ApiStatus.Internal
@NotNull Audience sender(final @NotNull Object object);
/**
* @return The platform the plugin is being run on
*/
@NotNull SonarPlatform getPlatform();
/**
* @return If the amount of players online with the same IP address is greater than a maximum
*/
boolean hasTooManyAccounts(final @NotNull InetAddress inetAddress, final int limit);
/**
* @return A small wrapper for the plugin logger so we can use the logger everywhere
*/
@NotNull LoggerWrapper getLogger();
@NotNull File getDataDirectory();
@NotNull SonarConfiguration getConfig();
@NotNull SubcommandRegistry getSubcommandRegistry();
@@ -98,7 +92,7 @@ public interface Sonar {
@NotNull
default SonarVersion getVersion() {
return SonarVersion.GET;
return SonarVersion.INSTANCE;
}
@NotNull
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023-2024 Sonar Contributors
* Copyright (C) 2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -17,19 +17,30 @@
package xyz.jonesdev.sonar.api;
import io.netty.channel.ChannelPipeline;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.util.function.Function;
@Getter
@RequiredArgsConstructor
public enum SonarPlatform {
BUKKIT("Bukkit", 19110),
BUNGEE("BungeeCord", 19109),
VELOCITY("Velocity", 19107);
BUKKIT("Bukkit", 19110,
pipeline -> pipeline.context("outbound_config") != null ? "outbound_config" : "encoder",
pipeline -> "packet_handler"),
BUNGEE("BungeeCord", 19109,
pipeline -> "packet-encoder",
pipeline -> "inbound-boss"),
VELOCITY("Velocity", 19107,
pipeline -> "minecraft-encoder",
pipeline -> "handler");
private final String displayName;
/**
* bStats service ID for the respective Sonar platform
*/
private final int metricsId;
private final Function<ChannelPipeline, String> encoderFunction;
private final Function<ChannelPipeline, String> handlerFunction;
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023-2024 Sonar Contributors
* Copyright (C) 2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023-2024 Sonar Contributors
* Copyright (C) 2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -18,35 +18,49 @@
package xyz.jonesdev.sonar.api;
import lombok.Getter;
import org.jetbrains.annotations.NotNull;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.jar.Manifest;
@Getter
public final class SonarVersion {
static final SonarVersion GET = new SonarVersion();
private final String semanticVersion, full, formatted, commitSHA;
private final int build;
private final boolean isOnMainBranch;
static final SonarVersion INSTANCE = new SonarVersion();
private final String version, formatted, gitBranch, gitCommit;
SonarVersion() {
final Package manifest = Sonar.class.getPackage();
final String versionString = manifest.getImplementationVersion();
// No need to null-check since we'll always just throw an exception if
// we can't find the version information in this file.
final Manifest manifest = getManifest();
if (versionString == null) {
this.full = "<could not retrieve version>";
this.semanticVersion = "<unknown version>";
this.commitSHA = "<unknown commit>";
this.build = -1;
this.isOnMainBranch = false;
this.formatted = full;
Sonar.get().getLogger().warn("Could not find version information. Is the manifest missing?");
return;
this.version = manifest.getMainAttributes().getValue("Implementation-Version");
this.gitBranch = manifest.getMainAttributes().getValue("Git-Branch");
this.gitCommit = manifest.getMainAttributes().getValue("Git-Commit");
this.formatted = version + " (" + gitCommit + ")";
}
// Taken from
// https://github.com/PaperMC/Velocity/pull/1336/
private static @NotNull Manifest getManifest() {
final String classLocation = "/" + Sonar.class.getName().replace(".", "/") + ".class";
final URL resource = Sonar.class.getResource(classLocation);
if (resource == null) {
throw new IllegalStateException("Could not find version information. Is the manifest missing?");
}
this.full = versionString;
this.semanticVersion = versionString.split("-")[0];
this.commitSHA = versionString.split("-")[1];
this.build = Integer.parseInt(versionString.split("-")[2]);
this.isOnMainBranch = versionString.split("-")[3].equals("main");
this.formatted = semanticVersion + " build " + build + " (" + commitSHA + ")";
final String classFilePath = resource.toString().replace("\\", "/");
final String archivePath = classFilePath.substring(0, classFilePath.length() - classLocation.length());
final String manifestPath = archivePath + "/META-INF/MANIFEST.MF";
try (final InputStream stream = new URL(manifestPath).openStream()) {
return new Manifest(stream);
} catch (IOException exception) {
throw new IllegalStateException(exception);
}
}
@Override
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023-2024 Sonar Contributors
* Copyright (C) 2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -53,7 +53,7 @@ public final class AttackTracker {
public void checkIfUnderAttack() {
final long joinsPerSecond = Sonar.get().getStatistics().getLoginsPerSecond();
final int verifyingPlayers = Sonar.get().getFallback().getConnected().size();
final int queuedPlayers = Sonar.get().getFallback().getQueue().getQueuedPlayers().size();
final int queuedPlayers = Sonar.get().getFallback().getQueue().getPlayers().size();
final int minPlayers = Sonar.get().getConfig().getMinPlayersForAttack();
if (joinsPerSecond > minPlayers // Check the number of bots/joins per second.
@@ -68,7 +68,7 @@ public final class AttackTracker {
currentAttack.successfulVerifications = Sonar.get().getVerifiedPlayerController().estimatedSize();
currentAttack.failedVerifications = Sonar.get().getStatistics().getTotalFailedVerifications();
Sonar.get().getEventManager().publish(new AttackDetectedEvent());
Sonar.get().getNotificationHandler().sendAttackNotification();
Sonar.get().getNotificationHandler().observe();
} else {
// Reset attack timer if we're still under attack
currentAttack.timer.reset();
@@ -125,16 +125,16 @@ public final class AttackTracker {
webhook.post(() -> {
final SonarConfiguration.Webhook.Embed config = Sonar.get().getConfig().getWebhook().getEmbed();
final String description = config.getDescription()
.replace("%start-timestamp%", startTimestamp)
.replace("%end-timestamp%", endTimestamp)
.replace("%duration%", formattedDuration)
.replace("%peak-cpu%", peakCPU)
.replace("%peak-memory%", peakMem)
.replace("%peak-bps%", peakBPS)
.replace("%peak-cps%", peakCPS)
.replace("%total-blacklisted%", Sonar.DECIMAL_FORMAT.format(blacklisted))
.replace("%total-failed%", Sonar.DECIMAL_FORMAT.format(failed))
.replace("%total-success%", Sonar.DECIMAL_FORMAT.format(verified));
.replace("<start-timestamp>", startTimestamp)
.replace("<end-timestamp>", endTimestamp)
.replace("<attack-duration>", formattedDuration)
.replace("<peak-cpu>", peakCPU)
.replace("<peak-memory>", peakMem)
.replace("<peak-bps>", peakBPS)
.replace("<peak-cps>", peakCPS)
.replace("<total-blacklisted>", Sonar.DECIMAL_FORMAT.format(blacklisted))
.replace("<total-failed>", Sonar.DECIMAL_FORMAT.format(failed))
.replace("<total-success>", Sonar.DECIMAL_FORMAT.format(verified));
return new SonarConfiguration.Webhook.Embed(
config.getTitle(), config.getTitleUrl(), description, config.getR(), config.getG(), config.getB());
});
@@ -151,10 +151,4 @@ public final class AttackTracker {
attackThreshold = 0;
}
}
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
public boolean isCurrentlyUnderAttack() {
final int minDelay = Sonar.get().getConfig().getAttackCooldownDelay();
return currentAttack != null && currentAttack.timer.delay() < minDelay;
}
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023-2024 Sonar Contributors
* Copyright (C) 2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -20,13 +20,11 @@ package xyz.jonesdev.sonar.api.command;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
import xyz.jonesdev.sonar.api.command.subcommand.Subcommand;
@Getter
@RequiredArgsConstructor
@ToString(of = "rawArguments")
public final class CommandInvocation {
private final InvocationSource sender;
private final Subcommand subcommand;
private final InvocationSource source;
private final String[] rawArguments;
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023-2024 Sonar Contributors
* Copyright (C) 2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023-2024 Sonar Contributors
* Copyright (C) 2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -17,19 +17,17 @@
package xyz.jonesdev.sonar.api.command;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.event.ClickEvent;
import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.text.minimessage.tag.Tag;
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.command.subcommand.Subcommand;
import xyz.jonesdev.sonar.api.command.subcommand.argument.Argument;
import java.time.Duration;
import java.util.*;
import java.util.stream.Collectors;
import static java.util.Collections.emptyList;
@@ -38,93 +36,65 @@ public interface SonarCommand {
Map<String, List<String>> ARG_TAB_SUGGESTIONS = new HashMap<>();
Cache<Audience, Long> COMMAND_DELAY = Caffeine.newBuilder()
.expireAfterWrite(Duration.ofMillis(500))
.build();
List<Component> CACHED_HELP_MESSAGE = new ArrayList<>();
default void handle(final @NotNull InvocationSource source, final String[] args) {
if (source.isPlayer()) {
// Check if the player actually has the permission to run the command
if (!source.getPermissionFunction().test("sonar.command")) {
source.sendMessage(Sonar.get().getConfig().getNoPermission());
return;
}
COMMAND_DELAY.cleanUp(); // Clean up the cache
final long timestamp = System.currentTimeMillis();
final long mapTimestamp = COMMAND_DELAY.asMap().getOrDefault(source.getAudience(), -1L);
// There were some exploits with spamming commands in the past.
// Spamming should be prevented, especially if some heavy operations are done,
// which is not the case here but let's still stay safe!
if (mapTimestamp > 0L) {
source.sendMessage(Sonar.get().getConfig().getCommands().getCommandCoolDown());
// Format delay
final double left = 0.5D - (timestamp - mapTimestamp) / 1000D;
source.sendMessage(Sonar.get().getConfig().getCommands().getCommandCoolDownLeft()
.replace("%time-left%", Sonar.DECIMAL_FORMAT.format(left)));
return;
}
COMMAND_DELAY.put(source.getAudience(), timestamp);
default void handle(final @NotNull InvocationSource source, final String @NotNull [] args) {
// Check if the player actually has the permission to run the command
if (source.isPlayer() && !source.getPermissionFunction().test("sonar.command")) {
source.sendMessage(Sonar.get().getConfig().getNoPermission());
return;
}
Optional<Subcommand> subcommand = Optional.empty();
if (args.length > 0) {
// Search subcommand if command arguments are present
final Optional<Subcommand> subcommand = Sonar.get().getSubcommandRegistry().getSubcommands().stream()
subcommand = Sonar.get().getSubcommandRegistry().getSubcommands().stream()
.filter(sub -> sub.getInfo().name().equalsIgnoreCase(args[0])
|| Arrays.stream(sub.getInfo().aliases())
.anyMatch(alias -> alias.equalsIgnoreCase(args[0])))
|| Arrays.stream(sub.getInfo().aliases()).anyMatch(alias -> alias.equalsIgnoreCase(args[0])))
.findFirst();
}
subcommand.ifPresentOrElse(command -> {
// Check permissions for subcommands
if (subcommand.isPresent()) {
if (!subcommand.get().getInfo().onlyConsole()
&& !source.getPermissionFunction().test(subcommand.get().getPermission())) {
source.sendMessage(Sonar.get().getConfig().getCommands().getSubCommandNoPerm()
.replace("%permission%", subcommand.get().getPermission()));
return;
}
subcommand.get().invoke(source, args);
if (!command.getInfo().onlyConsole()
&& !source.getPermissionFunction().test(command.getPermission())) {
source.sendMessage(MiniMessage.miniMessage().deserialize(
Sonar.get().getConfig().getMessagesConfig().getString("commands.subcommand-no-permission"),
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix()),
Placeholder.unparsed("permission", command.getPermission())));
return;
}
}
// Invoke subcommand
command.invoke(source, args);
}, () -> {
// Print standard help message
source.sendMessage(MiniMessage.miniMessage().deserialize(
String.join("<newline>",
Sonar.get().getConfig().getMessagesConfig().getStringList("commands.main.header")),
Placeholder.unparsed("version", Sonar.get().getVersion().getFormatted()),
Placeholder.unparsed("platform", Sonar.get().getPlatform().getDisplayName()),
Placeholder.unparsed("copyright-year", String.valueOf(Calendar.getInstance().get(Calendar.YEAR)))));
// Re-use the old, cached help message since we don't want to scan
// for each subcommand and it's arguments/attributes every time
// someone runs /sonar since the subcommand don't change
for (final Component component : CACHED_HELP_MESSAGE) {
source.sendMessage(component);
}
final Component yes = MiniMessage.miniMessage().deserialize(
Sonar.get().getConfig().getMessagesConfig().getString("commands.main.tick"));
final Component no = MiniMessage.miniMessage().deserialize(
Sonar.get().getConfig().getMessagesConfig().getString("commands.main.cross"));
Sonar.get().getSubcommandRegistry().getSubcommands().forEach(command -> {
source.sendMessage(MiniMessage.miniMessage().deserialize(
Sonar.get().getConfig().getMessagesConfig().getString("commands.main.subcommands"),
TagResolver.resolver("suggest-subcommand",
(queue, context) -> Tag.styling(ClickEvent.suggestCommand("/sonar " + command.getInfo().name()))),
Placeholder.unparsed("subcommand", command.getInfo().name()),
Placeholder.unparsed("description", command.getDescription()),
Placeholder.unparsed("permission", command.getPermission()),
Placeholder.unparsed("aliases", command.getAliases()),
Placeholder.component("only-players", command.getInfo().onlyPlayers() ? yes : no),
Placeholder.component("only-console", command.getInfo().onlyConsole() ? yes : no)));
});
});
}
static void prepareCachedMessages() {
// Cache help message
CACHED_HELP_MESSAGE.clear();
for (final String message : Sonar.get().getConfig().getCommands().getHelpHeader()) {
CACHED_HELP_MESSAGE.add(MiniMessage.miniMessage().deserialize(message
.replace("%version%", Sonar.get().getVersion().getFormatted())
.replace("%platform%", Sonar.get().getPlatform().getDisplayName())
.replace("%copyright_year%", String.valueOf(Calendar.getInstance().get(Calendar.YEAR)))));
}
final String subcommandFormat = Sonar.get().getConfig().getCommands().getHelpSubcommands();
Sonar.get().getSubcommandRegistry().getSubcommands().forEach(subcommand -> {
final Component deserialized = MiniMessage.miniMessage().deserialize(subcommandFormat
.replace("%subcommand%", subcommand.getInfo().name())
.replace("%description%", subcommand.getInfo().description())
.replace("%only_players%", subcommand.getInfo().onlyPlayers() ? "<green>✔</green>" : "<red>✗</red>")
.replace("%require_console%", subcommand.getInfo().onlyConsole() ? "<green>✔</green>" : "<red>✗</red>")
.replace("%permission%", subcommand.getPermission())
.replace("%aliases%", subcommand.getAliases()));
CACHED_HELP_MESSAGE.add(deserialized);
});
// Don't re-cache tab suggestions
if (!TAB_SUGGESTIONS.isEmpty()) return;
// Cache tab suggestions
@@ -133,12 +103,10 @@ public interface SonarCommand {
if (subcommand.getInfo().aliases().length > 0) {
TAB_SUGGESTIONS.addAll(Arrays.asList(subcommand.getInfo().aliases()));
}
final List<String> parsedArguments = Arrays.stream(subcommand.getInfo().arguments())
.map(Argument::value)
.collect(Collectors.toList());
ARG_TAB_SUGGESTIONS.put(subcommand.getInfo().name(), parsedArguments);
final List<String> arguments = Arrays.asList(subcommand.getInfo().arguments());
ARG_TAB_SUGGESTIONS.put(subcommand.getInfo().name(), arguments);
for (final String alias : subcommand.getInfo().aliases()) {
ARG_TAB_SUGGESTIONS.put(alias, parsedArguments);
ARG_TAB_SUGGESTIONS.put(alias, arguments);
}
}
}
@@ -147,8 +115,8 @@ public interface SonarCommand {
if (arguments.length <= 1) {
return TAB_SUGGESTIONS;
} else if (arguments.length == 2) {
final String subCommandName = arguments[0].toLowerCase();
return ARG_TAB_SUGGESTIONS.getOrDefault(subCommandName, emptyList());
final String subcommandName = arguments[0].toLowerCase();
return ARG_TAB_SUGGESTIONS.getOrDefault(subcommandName, emptyList());
}
return emptyList();
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023-2024 Sonar Contributors
* Copyright (C) 2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -18,77 +18,85 @@
package xyz.jonesdev.sonar.api.command.subcommand;
import lombok.Getter;
import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.command.CommandInvocation;
import xyz.jonesdev.sonar.api.command.InvocationSource;
import xyz.jonesdev.sonar.api.command.subcommand.argument.Argument;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Arrays;
import java.util.stream.Collectors;
import java.util.Objects;
import java.util.regex.Pattern;
@Getter
public abstract class Subcommand {
private final @NotNull SubcommandInfo info;
private final String permission, aliases, arguments;
protected static final Sonar SONAR = Sonar.get();
public Subcommand() {
info = getClass().getAnnotation(SubcommandInfo.class);
permission = "sonar." + info.name();
aliases = info.aliases().length == 0 ? "No aliases."
this.info = Objects.requireNonNull(getClass().getAnnotation(SubcommandInfo.class));
this.permission = "sonar." + info.name();
this.aliases = info.aliases().length == 0 ? "No aliases."
: String.join(", ", info.aliases());
arguments = info.arguments().length == 0 ? ""
: Arrays.stream(info.arguments())
.map(Argument::value)
.collect(Collectors.joining(", "));
this.arguments = info.arguments().length == 0 ? ""
: String.join(", ", info.arguments());
}
protected static @Nullable InetAddress getInetAddressIfValid(final InvocationSource source, final String rawIP) {
final InetAddress inetAddress;
try {
inetAddress = InetAddress.getByName(rawIP);
} catch (UnknownHostException exception) {
source.sendMessage(SONAR.getConfig().getCommands().getInvalidIpAddress());
private static final Pattern IPv4_REGEX = Pattern.compile("^((25[0-5]|(2[0-4]|1\\d|[1-9]|)\\d)\\.?\\b){4}$");
private static final Pattern IPv6_REGEX = Pattern.compile("(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))");
protected static @Nullable String validateIP(final InvocationSource source, final String raw) {
if (!IPv4_REGEX.matcher(raw).matches() && !IPv6_REGEX.matcher(raw).matches()) {
source.sendMessage(MiniMessage.miniMessage().deserialize(
Sonar.get().getConfig().getMessagesConfig().getString("commands.invalid-ip-address"),
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix())));
return null;
}
return inetAddress;
return raw;
}
protected final void incorrectUsage(final @NotNull InvocationSource sender) {
sender.sendMessage(SONAR.getConfig().getCommands().getIncorrectCommandUsage()
.replace("%usage%", info.name() + " (" + arguments + ")"));
protected final void incorrectUsage(final @NotNull InvocationSource invocationSource) {
incorrectUsage(invocationSource, info.name() + " (" + arguments + ")");
}
protected final void incorrectUsage(final @NotNull InvocationSource invocationSource, final String usage) {
invocationSource.sendMessage(MiniMessage.miniMessage().deserialize(
Sonar.get().getConfig().getMessagesConfig().getString("commands.incorrect-usage"),
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix()),
Placeholder.unparsed("subcommand-usage", usage)));
}
public final void invoke(final @NotNull InvocationSource invocationSource, final String @NotNull [] arguments) {
// Check if the subcommand can only be executed by players
if (getInfo().onlyPlayers() && !invocationSource.isPlayer()) {
invocationSource.sendMessage(Sonar.get().getConfig().getCommands().getPlayersOnly());
if (info.onlyPlayers() && !invocationSource.isPlayer()) {
invocationSource.sendMessage(MiniMessage.miniMessage().deserialize(
Sonar.get().getConfig().getMessagesConfig().getString("commands.player-only"),
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix())));
return;
}
// Check if the subcommand can only be executed though console
if (getInfo().onlyConsole() && invocationSource.isPlayer()) {
invocationSource.sendMessage(Sonar.get().getConfig().getCommands().getConsoleOnly());
if (info.onlyConsole() && invocationSource.isPlayer()) {
invocationSource.sendMessage(MiniMessage.miniMessage().deserialize(
Sonar.get().getConfig().getMessagesConfig().getString("commands.console-only"),
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix())));
return;
}
final CommandInvocation commandInvocation = new CommandInvocation(invocationSource, this, arguments);
// The subcommands has arguments which are not present in the executed command
if (getInfo().argumentsRequired() && getInfo().arguments().length > 0 && arguments.length <= 1) {
invocationSource.sendMessage(Sonar.get().getConfig().getCommands().getIncorrectCommandUsage()
.replace("%usage%", getInfo().name() + " (" + getArguments() + ")"));
if (info.argumentsRequired() && info.arguments().length > 0 && arguments.length <= 1) {
incorrectUsage(invocationSource);
return;
}
// Execute the sub command
execute(commandInvocation);
// Execute the sub command from the invocation source with the given arguments
execute(new CommandInvocation(invocationSource, arguments));
}
public final @NotNull String getDescription() {
final String path = String.format("commands.%s.description", info.name());
return Objects.requireNonNull(Sonar.get().getConfig().getMessagesConfig().getString(path));
}
protected abstract void execute(final @NotNull CommandInvocation invocation);
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023-2024 Sonar Contributors
* Copyright (C) 2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -17,8 +17,6 @@
package xyz.jonesdev.sonar.api.command.subcommand;
import xyz.jonesdev.sonar.api.command.subcommand.argument.Argument;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -33,11 +31,6 @@ public @interface SubcommandInfo {
*/
String name();
/**
* @return Description of the subcommand
*/
String description();
/**
* @return Aliases of the subcommand
*/
@@ -46,7 +39,7 @@ public @interface SubcommandInfo {
/**
* @return Arguments of the subcommand
*/
Argument[] arguments() default {};
String[] arguments() default {};
/**
* @return Whether the subcommands requires arguments
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023-2024 Sonar Contributors
* Copyright (C) 2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -17,30 +17,24 @@
package xyz.jonesdev.sonar.api.command.subcommand;
import lombok.Getter;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Unmodifiable;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Vector;
public interface SubcommandRegistry {
List<Subcommand> SUBCOMMANDS = new Vector<>();
@NotNull
@Unmodifiable
default List<Subcommand> getSubcommands() {
return Collections.unmodifiableList(SUBCOMMANDS);
}
@Getter
public final class SubcommandRegistry {
private final List<Subcommand> subcommands = new Vector<>();
/**
* Registers a single or multiple subcommand(s)
*
* @param subcommand Array of Subcommands to register
*/
default void register(final Subcommand @NotNull ... subcommand) {
SUBCOMMANDS.addAll(Arrays.asList(subcommand));
public void register(final Subcommand @NotNull ... subcommand) {
subcommands.addAll(Arrays.asList(subcommand));
}
/**
@@ -49,7 +43,7 @@ public interface SubcommandRegistry {
* @param subcommand Array of Subcommands to unregister
*/
@SuppressWarnings("unused")
default void unregister(final Subcommand @NotNull ... subcommand) {
SUBCOMMANDS.removeAll(Arrays.asList(subcommand));
public void unregister(final Subcommand @NotNull ... subcommand) {
subcommands.removeAll(Arrays.asList(subcommand));
}
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023-2024 Sonar Contributors
* Copyright (C) 2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -21,7 +21,7 @@ import lombok.Getter;
import org.jetbrains.annotations.NotNull;
@Getter
enum Language {
public enum Language {
SYSTEM("system"),
AB("Abkhazian"),
AA("Afar"),
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023-2024 Sonar Contributors
* Copyright (C) 2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -88,11 +88,6 @@ public final class SimpleYamlConfig {
yaml.set(path, v);
}
public int getInt(final String path, final int def) {
yaml.addDefault(path, def);
return yaml.getInt(path, def);
}
public int getInt(final String path) {
if (!yaml.contains(path)) {
LOGGER.warn("Could not find {} in {}.", path, file.getName());
@@ -101,11 +96,6 @@ public final class SimpleYamlConfig {
return yaml.getInt(path);
}
public boolean getBoolean(final String path, final boolean def) {
yaml.addDefault(path, def);
return yaml.getBoolean(path, def);
}
public boolean getBoolean(final String path) {
if (!yaml.contains(path)) {
LOGGER.warn("Could not find {} in {}.", path, file.getName());
@@ -114,42 +104,18 @@ public final class SimpleYamlConfig {
return yaml.getBoolean(path);
}
public String getString(final String path, final String def) {
final Object object = getObject(path, def);
if (object instanceof String) {
return (String) object;
}
LOGGER.info("[config] Migrated {} to {}", path, def);
set(path, def);
return def;
}
public @NotNull String getString(final String path) {
if (!yaml.contains(path)) {
LOGGER.warn("Could not find {} in {}.", path, file.getName());
return "";
}
final Object object = getObject(path);
final Object object = yaml.get(path);
if (object instanceof String) {
return (String) object;
}
throw new IllegalStateException("Invalid entry " + path);
}
public Object getObject(final String path, final Object def) {
yaml.addDefault(path, def);
return yaml.get(path, def);
}
public Object getObject(final String path) {
return yaml.get(path);
}
public List<String> getStringList(final String path, final List<String> def) {
yaml.addDefault(path, def);
return yaml.getStringList(path);
}
public List<String> getStringList(final String path) {
if (!yaml.contains(path)) {
LOGGER.warn("Could not find {} in {}.", path, file.getName());
@@ -158,11 +124,6 @@ public final class SimpleYamlConfig {
return yaml.getStringList(path);
}
public List<Integer> getIntList(final String path, final List<Integer> def) {
yaml.addDefault(path, def);
return yaml.getIntegerList(path);
}
public List<Integer> getIntList(final String path) {
if (!yaml.contains(path)) {
LOGGER.warn("Could not find {} in {}.", path, file.getName());
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023-2024 Sonar Contributors
* Copyright (C) 2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -17,15 +17,18 @@
package xyz.jonesdev.sonar.api.config;
import com.alessiodp.libby.Library;
import com.j256.ormlite.db.DatabaseType;
import lombok.*;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.command.SonarCommand;
import xyz.jonesdev.sonar.api.logger.LoggerWrapper;
import xyz.jonesdev.sonar.api.ormlite.H2DatabaseTypeAdapter;
import xyz.jonesdev.sonar.api.ormlite.MariaDbDatabaseTypeAdapter;
import xyz.jonesdev.sonar.api.ormlite.MysqlDatabaseTypeAdapter;
import xyz.jonesdev.sonar.api.webhook.DiscordWebhook;
@@ -44,7 +47,10 @@ import java.util.regex.Pattern;
public final class SonarConfiguration {
@Getter
private final SimpleYamlConfig generalConfig, messagesConfig, webhookConfig;
@Getter
private final File languageFile, pluginFolder;
@Getter
private Language language;
static final LoggerWrapper LOGGER = new LoggerWrapper() {
@@ -101,8 +107,11 @@ public final class SonarConfiguration {
return Language.fromCode(property);
} catch (Throwable throwable) {
LOGGER.error("Could not find requested language: {}", throwable);
LOGGER.error("You can view a full list of valid language codes here:");
LOGGER.error("https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes");
LOGGER.error("If a translation does not exist yet, Sonar will use English (en).");
}
} catch (Exception exception) {
} catch (IOException exception) {
LOGGER.error("Error reading language file: {}", exception);
}
return DEFAULT_FALLBACK_LANGUAGE;
@@ -115,27 +124,27 @@ public final class SonarConfiguration {
}
// Generate the language file and check what it's set to
Language preferredLanguage = getPreferredLanguage();
if (preferredLanguage == Language.SYSTEM) {
language = getPreferredLanguage();
if (language == Language.SYSTEM) {
try {
// Try using the system language to determine the language file
final String property = System.getProperty("user.language", "en");
preferredLanguage = Language.fromCode(property);
language = Language.fromCode(property);
// Make sure the user knows that we're using the system language for translations
LOGGER.info("Using system language ({}) for translations.", preferredLanguage);
LOGGER.info("Using system language ({}) for translations.", language);
} catch (Exception exception) {
LOGGER.warn("Could not use system language for translations.");
LOGGER.warn("Using default language ({}) for translations.", preferredLanguage);
LOGGER.warn("Using default language ({}) for translations.", language);
}
} else {
LOGGER.info("Using custom language ({}) for translations.", preferredLanguage);
LOGGER.info("Using custom language ({}) for translations.", language);
}
// Load all configurations
try {
generalConfig.load(getAsset("config", preferredLanguage));
messagesConfig.load(getAsset("messages", preferredLanguage));
webhookConfig.load(getAsset("webhook", preferredLanguage));
generalConfig.load(getAsset("config", language));
messagesConfig.load(getAsset("messages", language));
webhookConfig.load(getAsset("webhook", language));
} catch (Exception exception) {
throw new IllegalStateException("Error loading configuration", exception);
}
@@ -149,7 +158,7 @@ public final class SonarConfiguration {
// General settings
logPlayerAddresses = generalConfig.getBoolean("general.log-player-addresses");
maxOnlinePerIp = clamp(generalConfig.getInt("general.max-online-per-ip"), 1, 100);
maxOnlinePerIp = clamp(generalConfig.getInt("general.max-online-per-ip"), 0, 99);
// Attack tracker
minPlayersForAttack = clamp(generalConfig.getInt("attack-tracker.min-players-for-attack"), 2, 1024);
@@ -159,11 +168,6 @@ public final class SonarConfiguration {
// Database
database.type = Database.Type.valueOf(generalConfig.getString("database.type").toUpperCase());
database.host = generalConfig.getString("database.host");
database.port = generalConfig.getInt("database.port");
database.name = generalConfig.getString("database.name");
database.username = generalConfig.getString("database.username");
database.password = generalConfig.getString("database.password");
database.maximumAge = clamp(generalConfig.getInt("database.maximum-age"), 1, 365);
// Queue
@@ -171,49 +175,59 @@ public final class SonarConfiguration {
// Verification
verification.timing = Verification.Timing.valueOf(generalConfig.getString("verification.timing"));
// Warn the user if the verification timing is set to NEVER
if (verification.timing == Verification.Timing.NEVER) {
LOGGER.warn(" ");
LOGGER.warn("You have set the verification timing to 'NEVER'.");
LOGGER.warn("Sonar will NOT perform the bot verification at all, therefore making it useless.");
LOGGER.warn("It is highly suggested to set this option to either 'DURING_ATTACK' or 'ALWAYS'.");
LOGGER.warn("Please only edit this option if you really know what you are doing.");
LOGGER.warn(" ");
}
verification.gravity.enabled = generalConfig.getBoolean("verification.checks.gravity.enabled");
verification.gravity.checkCollisions = generalConfig.getBoolean("verification.checks.gravity.check-collisions");
verification.gravity.checkCollisions = generalConfig.getBoolean("verification.checks.collision.enabled");
verification.gravity.captchaOnFail = generalConfig.getBoolean("verification.checks.gravity.captcha-on-fail");
verification.gravity.maxMovementTicks = clamp(generalConfig.getInt("verification.checks.gravity.max-movement-ticks"), 2, 100);
verification.gravity.gamemode = Verification.Gravity.Gamemode.valueOf(generalConfig.getString("verification.checks.gravity.gamemode"));
verification.vehicle.timing = Verification.Timing.valueOf(generalConfig.getString("verification.checks.vehicle.timing"));
verification.vehicle.enabled = generalConfig.getBoolean("verification.checks.vehicle.enabled");
verification.vehicle.minimumPackets = clamp(generalConfig.getInt("verification.checks.vehicle.minimum-packets"), 0, 20);
verification.map.timing = Verification.Timing.valueOf(generalConfig.getString("verification.checks.map-captcha.timing"));
verification.map.flare = generalConfig.getBoolean("verification.checks.map-captcha.effects.flare");
verification.map.scratches = generalConfig.getBoolean("verification.checks.map-captcha.effects.scratches");
verification.map.ripple = generalConfig.getBoolean("verification.checks.map-captcha.effects.ripple");
verification.map.smear = generalConfig.getBoolean("verification.checks.map-captcha.effects.smear");
verification.map.pinch = generalConfig.getBoolean("verification.checks.map-captcha.effects.pinch");
verification.map.saturation = (float) generalConfig.getYaml().getDouble(
"verification.checks.map-captcha.effects.saturation", 0.3);
verification.map.distortion = (float) generalConfig.getYaml().getDouble(
"verification.checks.map-captcha.effects.distortion", 2);
verification.map.precomputeAmount = generalConfig.getInt("verification.checks.map-captcha.precompute");
verification.map.maxDuration = generalConfig.getInt("verification.checks.map-captcha.max-duration");
verification.map.precomputeAmount = clamp(generalConfig.getInt("verification.checks.map-captcha.precompute"), 10, 5000);
verification.map.maxDuration = clamp(generalConfig.getInt("verification.checks.map-captcha.max-duration"), 5000, 360000);
verification.map.maxTries = generalConfig.getInt("verification.checks.map-captcha.max-tries");
verification.map.dictionary = generalConfig.getString("verification.checks.map-captcha.dictionary");
verification.map.fonts = generalConfig.getStringList("verification.checks.map-captcha.font-names");
verification.map.alphabet = generalConfig.getString("verification.checks.map-captcha.alphabet");
verification.map.backgroundImage = null;
final String backgroundPath = generalConfig.getString("verification.checks.map-captcha.background");
if (!backgroundPath.isEmpty()) {
try (final InputStream inputStream = new FileInputStream(backgroundPath)) {
verification.map.backgroundImage = inputStream;
} catch (IOException exception) {
Sonar.get().getLogger().error("Could not find background image", exception);
}
}
verification.brand.enabled = generalConfig.getBoolean("verification.checks.client-brand.enabled");
verification.brand.validRegex = Pattern.compile(generalConfig.getString("verification.checks.client-brand.valid-regex"));
verification.brand.maxLength = generalConfig.getInt("verification.checks.client-brand.max-length");
verification.gamemode = Verification.Gamemode.valueOf(generalConfig.getString("verification.gamemode"));
verification.validNameRegex = Pattern.compile(generalConfig.getString("verification.checks.valid-name-regex"));
verification.validLocaleRegex = Pattern.compile(generalConfig.getString("verification.checks.valid-locale-regex"));
verification.maxLoginPackets = clamp(generalConfig.getInt("verification.checks.max-login-packets"), 128, 8192);
verification.checkGeyser = generalConfig.getBoolean("verification.check-geyser-players");
verification.logConnections = generalConfig.getBoolean("verification.log-connections");
verification.logDuringAttack = generalConfig.getBoolean("verification.log-during-attack");
verification.debugXYZPositions = generalConfig.getBoolean("verification.debug-xyz-positions");
verification.readTimeout = clamp(generalConfig.getInt("verification.read-timeout"), 500, 30000);
verification.readTimeout = clamp(generalConfig.getInt("verification.read-timeout"), 1000, 30000);
verification.writeTimeout = clamp(generalConfig.getInt("verification.write-timeout"), 1000, 30000);
verification.reconnectDelay = clamp(generalConfig.getInt("verification.rejoin-delay"), 0, 100000);
verification.rememberTime = clamp(generalConfig.getInt("verification.remember-time"), 0, 86400000);
verification.blacklistTime = clamp(generalConfig.getInt("verification.blacklist-time"), 0, 86400000);
verification.blacklistThreshold = clamp(generalConfig.getInt("verification.blacklist-threshold"), 0, 100);
verification.whitelistedProtocols.clear();
verification.whitelistedProtocols.addAll(generalConfig.getIntList("verification.whitelisted-protocols"));
verification.blacklistedProtocols.clear();
verification.blacklistedProtocols.addAll(generalConfig.getIntList("verification.blacklisted-protocols"));
@@ -230,8 +244,7 @@ public final class SonarConfiguration {
webhook.embed.title = webhookConfig.getString(embedPath + "title");
webhook.embed.titleUrl = webhookConfig.getString(embedPath + "title-url");
webhook.embed.description = formatString(String.join("\n",
webhookConfig.getStringList(embedPath + "description")));
webhook.embed.description = String.join("\n", webhookConfig.getStringList(embedPath + "description"));
webhook.embed.r = webhookConfig.getInt(embedPath + "color.red");
webhook.embed.g = webhookConfig.getInt(embedPath + "color.green");
webhook.embed.b = webhookConfig.getInt(embedPath + "color.blue");
@@ -247,85 +260,43 @@ public final class SonarConfiguration {
}
// Messages
prefix = formatString(messagesConfig.getString("prefix"));
supportUrl = messagesConfig.getString("support-url");
header = fromList(messagesConfig.getStringList("header"));
footer = fromList(messagesConfig.getStringList("footer"));
tooManyOnlinePerIp = deserialize(fromList(messagesConfig.getStringList("too-many-online-per-ip")));
noPermission = formatString(messagesConfig.getString("commands.no-permission"));
commands.incorrectCommandUsage = formatString(messagesConfig.getString("commands.incorrect-usage"));
commands.invalidIpAddress = formatString(messagesConfig.getString("commands.invalid-ip-address"));
commands.playersOnly = formatString(messagesConfig.getString("commands.player-only"));
commands.consoleOnly = formatString(messagesConfig.getString("commands.console-only"));
commands.commandCoolDown = formatString(messagesConfig.getString("commands.cool-down"));
commands.commandCoolDownLeft = formatString(messagesConfig.getString("commands.cool-down-left"));
commands.subCommandNoPerm = formatString(messagesConfig.getString("commands.subcommand-no-permission"));
commands.helpHeader = messagesConfig.getStringList("commands.main.header");
commands.helpSubcommands = formatString(messagesConfig.getString("commands.main.subcommands"));
prefix = MiniMessage.miniMessage().deserialize(messagesConfig.getString("prefix"));
noPermission = MiniMessage.miniMessage().deserialize(messagesConfig.getString("commands.no-permission"),
Placeholder.component("prefix", prefix));
SonarCommand.prepareCachedMessages();
commands.reloading = formatString(messagesConfig.getString("commands.reload.start"));
commands.reloaded = formatString(messagesConfig.getString("commands.reload.finish"));
supportUrl = messagesConfig.getString("support-url");
header = MiniMessage.miniMessage().deserialize(
String.join("<newline>", messagesConfig.getStringList("header")),
Placeholder.unparsed("support-url", supportUrl),
Placeholder.component("prefix", prefix));
footer = MiniMessage.miniMessage().deserialize(
String.join("<newline>", messagesConfig.getStringList("footer")),
Placeholder.unparsed("support-url", supportUrl),
Placeholder.component("prefix", prefix));
commands.verboseSubscribed = formatString(messagesConfig.getString("commands.verbose.subscribed"));
commands.verboseUnsubscribed = formatString(messagesConfig.getString("commands.verbose.unsubscribed"));
tooManyOnlinePerIp = deserializeDisconnectMessage("too-many-online-per-ip");
verification.currentlyPreparing = deserializeDisconnectMessage("verification.currently-preparing");
verification.tooFastReconnect = deserializeDisconnectMessage("verification.too-fast-reconnect");
verification.alreadyVerifying = deserializeDisconnectMessage("verification.already-verifying");
verification.alreadyQueued = deserializeDisconnectMessage("verification.already-queued");
verification.blacklisted = deserializeDisconnectMessage("verification.blacklisted");
verification.invalidUsername = deserializeDisconnectMessage("verification.invalid-username");
verification.protocolBlacklisted = deserializeDisconnectMessage("verification.blacklisted-protocol");
verification.verificationSuccess = deserializeDisconnectMessage("verification.success");
verification.verificationFailed = deserializeDisconnectMessage("verification.failed");
commands.notificationsSubscribed = formatString(messagesConfig.getString("commands.notify.subscribed"));
commands.notificationsUnsubscribed = formatString(messagesConfig.getString("commands.notify.unsubscribed"));
verboseAnimation = Collections.unmodifiableList(messagesConfig.getStringList("verbose.animation"));
}
commands.blacklistEmpty = formatString(messagesConfig.getString("commands.blacklist.empty"));
commands.blacklistCleared = formatString(messagesConfig.getString("commands.blacklist.cleared"));
commands.blacklistSize = formatString(messagesConfig.getString("commands.blacklist.size"));
commands.blacklistAdd = formatString(messagesConfig.getString("commands.blacklist.added"));
commands.blacklistAddWarning = formatString(messagesConfig.getString("commands.blacklist.added-warning"));
commands.blacklistRemove = formatString(messagesConfig.getString("commands.blacklist.removed"));
commands.blacklistDuplicate = formatString(messagesConfig.getString("commands.blacklist.duplicate-ip"));
commands.blacklistNotFound = formatString(messagesConfig.getString("commands.blacklist.ip-not-found"));
commands.verifiedEmpty = formatString(messagesConfig.getString("commands.verified.empty"));
commands.verifiedCleared = formatString(messagesConfig.getString("commands.verified.cleared"));
commands.verifiedSize = formatString(messagesConfig.getString("commands.verified.size"));
commands.verifiedRemove = formatString(messagesConfig.getString("commands.verified.removed"));
commands.verifiedAdd = formatString(messagesConfig.getString("commands.verified.added"));
commands.verifiedAlready = formatString(messagesConfig.getString("commands.verified.already"));
commands.verifiedNotFound = formatString(messagesConfig.getString("commands.verified.ip-not-found"));
commands.verifiedBlocked = formatString(messagesConfig.getString("commands.verified.blocked"));
commands.statisticsHeader = formatString(messagesConfig.getString("commands.statistics.header"));
commands.unknownStatisticType = formatString(messagesConfig.getString("commands.statistics.unknown-type"));
commands.generalStatistics = formatString(fromList(messagesConfig.getStringList("commands.statistics.general")));
commands.cpuStatistics = formatString(fromList(messagesConfig.getStringList("commands.statistics.cpu")));
commands.memoryStatistics = formatString(fromList(messagesConfig.getStringList("commands.statistics.memory")));
commands.networkStatistics = formatString(fromList(messagesConfig.getStringList("commands.statistics.network")));
verification.connectLog = formatString(messagesConfig.getString("verification.logs.connection"));
verification.failedLog = formatString(messagesConfig.getString("verification.logs.failed"));
verification.blacklistLog = formatString(messagesConfig.getString("verification.logs.blacklisted"));
verification.successLog = formatString(messagesConfig.getString("verification.logs.successful"));
verification.map.enterCode = deserialize(formatString(messagesConfig.getString("verification.captcha.enter-code")));
verification.map.enterCodeActionBar = formatString(messagesConfig.getString("verification.captcha.action-bar"));
verification.map.failedCaptcha = deserialize(formatString(messagesConfig.getString("verification.captcha.incorrect")));
verification.currentlyPreparing = deserialize(fromList(messagesConfig.getStringList("verification.currently-preparing")));
verification.tooFastReconnect = deserialize(fromList(messagesConfig.getStringList("verification.too-fast-reconnect")));
verification.alreadyVerifying = deserialize(fromList(messagesConfig.getStringList("verification.already-verifying")));
verification.alreadyQueued = deserialize(fromList(messagesConfig.getStringList("verification.already-queued")));
verification.blacklisted = deserialize(fromList(messagesConfig.getStringList("verification.blacklisted")));
verification.invalidUsername = deserialize(fromList(messagesConfig.getStringList("verification.invalid-username")));
verification.protocolBlacklisted = deserialize(fromList(messagesConfig.getStringList("verification.blacklisted-protocol")));
verification.verificationSuccess = deserialize(fromList(messagesConfig.getStringList("verification.success")));
verification.verificationFailed = deserialize(fromList(messagesConfig.getStringList("verification.failed")));
verbose.actionBarLayout = formatString(messagesConfig.getString("verbose.layout.normal"));
verbose.actionBarLayoutDuringAttack = formatString(messagesConfig.getString("verbose.layout.attack"));
verbose.animation = Collections.unmodifiableList(messagesConfig.getStringList("verbose.animation"));
notifications.notificationTitle = deserialize(formatString(messagesConfig.getString("notifications.title")));
notifications.notificationSubtitle = deserialize(formatString(messagesConfig.getString("notifications.subtitle")));
notifications.notificationChat = formatString(fromList(messagesConfig.getStringList("notifications.chat")));
private @NotNull Component deserializeDisconnectMessage(final String path) {
return MiniMessage.miniMessage().deserialize(
String.join("<newline>", messagesConfig.getStringList(path)),
Placeholder.component("prefix", prefix),
Placeholder.component("header", header),
Placeholder.component("footer", footer),
Placeholder.unparsed("support-url", supportUrl));
}
private @NotNull URL getAsset(final String url, final @NotNull Language language) {
@@ -339,33 +310,17 @@ public final class SonarConfiguration {
}
private static int clamp(final int v, final int max, final int min) {
return Math.max(Math.min(v, min), max);
final int output = Math.max(Math.min(v, min), max);
if (output != v) {
Sonar.get().getLogger().warn("Clamped configuration value {} to {}", v, output);
}
return output;
}
public String formatAddress(final InetAddress inetAddress) {
return logPlayerAddresses ? inetAddress.toString() : "/<ip address withheld>";
}
private @NotNull String fromList(final @NotNull Collection<String> list) {
return formatString(String.join("<newline>", list));
}
private static @NotNull Component deserialize(final String legacy) {
return MiniMessage.miniMessage().deserialize(legacy);
}
private @NotNull String formatString(final @NotNull String str) {
return str
.replace("%prefix%", prefix == null ? "" : prefix)
.replace("%support-url%", supportUrl == null ? "" : supportUrl)
.replace("%header%", header == null ? "" : header)
.replace("%footer%", footer == null ? "" : footer);
}
@Getter
private final Verbose verbose = new Verbose();
@Getter
private final Commands commands = new Commands();
@Getter
private final Queue queue = new Queue();
@Getter
@@ -374,15 +329,14 @@ public final class SonarConfiguration {
private final Database database = new Database();
@Getter
private final Webhook webhook = new Webhook();
@Getter
private final Notifications notifications = new Notifications();
@Getter
private String prefix;
private Component prefix;
private String supportUrl;
private Component header;
private Component footer;
@Getter
private String noPermission;
private String header, footer;
private Component noPermission;
private boolean logPlayerAddresses;
@Getter
private int maxOnlinePerIp;
@@ -396,22 +350,8 @@ public final class SonarConfiguration {
private int attackCooldownDelay;
@Getter
private Component tooManyOnlinePerIp;
@Getter
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public static final class Verbose {
private String actionBarLayout;
private String actionBarLayoutDuringAttack;
private List<String> animation;
}
@Getter
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public static final class Notifications {
private Component notificationTitle;
private Component notificationSubtitle;
private String notificationChat;
}
private List<String> verboseAnimation;
@Getter
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@@ -442,21 +382,11 @@ public final class SonarConfiguration {
@Getter
public static final class Map {
private Timing timing;
private boolean flare;
private boolean scratches;
private boolean ripple;
private boolean smear;
private boolean pinch;
private float saturation;
private float distortion;
private int precomputeAmount;
private int maxDuration;
private int maxTries;
private String dictionary;
private Component enterCode;
private Component failedCaptcha;
private String enterCodeActionBar;
private List<String> fonts;
private String alphabet;
private InputStream backgroundImage;
}
@Getter
@@ -464,25 +394,13 @@ public final class SonarConfiguration {
private boolean enabled;
private boolean checkCollisions;
private boolean captchaOnFail;
private Gamemode gamemode;
private int maxMovementTicks;
@Getter
@RequiredArgsConstructor
public enum Gamemode {
SURVIVAL(0),
CREATIVE(1),
ADVENTURE(2),
// Keep this for backwards compatibility
SPECTATOR(2);
private final int id;
}
}
@Getter
public static final class Vehicle {
private Timing timing;
private boolean enabled;
private int minimumPackets;
}
@Getter
@@ -492,23 +410,37 @@ public final class SonarConfiguration {
private Pattern validRegex;
}
private Gamemode gamemode;
@Getter
@RequiredArgsConstructor
public enum Gamemode {
NOT_SET(-1),
SURVIVAL(0),
CREATIVE(1),
ADVENTURE(2);
private final int id;
public boolean isSurvivalOrAdventure() {
return this == SURVIVAL || this == ADVENTURE;
}
}
private boolean checkGeyser;
private boolean logConnections;
private boolean logDuringAttack;
private boolean debugXYZPositions;
private Pattern validNameRegex;
private Pattern validLocaleRegex;
private String connectLog;
private String failedLog;
private String successLog;
private String blacklistLog;
private int maxLoginPackets;
private int readTimeout;
private int writeTimeout;
private int reconnectDelay;
private int rememberTime;
private int blacklistTime;
private int blacklistThreshold;
private final Collection<Integer> whitelistedProtocols = new HashSet<>(0);
private final Collection<Integer> blacklistedProtocols = new HashSet<>(0);
private Component tooFastReconnect;
@@ -522,55 +454,6 @@ public final class SonarConfiguration {
private Component currentlyPreparing;
}
@Getter
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public static final class Commands {
private String incorrectCommandUsage;
private String invalidIpAddress;
private String subCommandNoPerm;
private String playersOnly;
private String consoleOnly;
private String commandCoolDown;
private String commandCoolDownLeft;
private String blacklistEmpty;
private String blacklistAdd;
private String blacklistAddWarning;
private String blacklistDuplicate;
private String blacklistNotFound;
private String blacklistRemove;
private String blacklistCleared;
private String blacklistSize;
private String verifiedRemove;
private String verifiedAdd;
private String verifiedAlready;
private String verifiedNotFound;
private String verifiedCleared;
private String verifiedSize;
private String verifiedEmpty;
private String verifiedBlocked;
private String statisticsHeader;
private String unknownStatisticType;
private String generalStatistics;
private String memoryStatistics;
private String networkStatistics;
private String cpuStatistics;
private List<String> helpHeader;
private String helpSubcommands;
private String verboseSubscribed;
private String verboseUnsubscribed;
private String notificationsSubscribed;
private String notificationsUnsubscribed;
private String reloading;
private String reloaded;
}
@Getter
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public static final class Database {
@@ -578,20 +461,38 @@ public final class SonarConfiguration {
@Getter
@RequiredArgsConstructor
public enum Type {
MYSQL("jdbc:mysql://%s:%d/%s", new MysqlDatabaseTypeAdapter()),
MARIADB("jdbc:mariadb://%s:%d/%s", new MariaDbDatabaseTypeAdapter()),
NONE(null, null);
MYSQL("MySQL", "jdbc:mysql://%s:%d/%s", new MysqlDatabaseTypeAdapter(),
Library.builder()
.groupId("com{}mysql")
.artifactId("mysql-connector-j")
.version("9.0.0")
.relocate("com{}mysql", "xyz{}jonesdev{}sonar{}libs{}mysql")
.build()),
MARIADB("MariaDB", "jdbc:mariadb://%s:%d/%s", new MariaDbDatabaseTypeAdapter(),
Library.builder()
.groupId("org{}mariadb{}jdbc")
.artifactId("mariadb-java-client")
.version("3.4.1")
.relocate("org{}mariadb", "xyz{}jonesdev{}sonar{}libs{}mariadb")
.build()),
H2("H2", "jdbc:h2:file:%s", new H2DatabaseTypeAdapter(),
Library.builder()
.groupId("com{}h2database")
.artifactId("h2")
.version("2.2.220")
.relocate("org{}h2", "xyz{}jonesdev{}sonar{}libs{}h2")
.build()),
NONE("None", null, null, null);
private final String displayName;
private final String connectionString;
private final DatabaseType databaseType;
private final Library databaseDriver;
@Setter
private boolean downloaded;
}
private Type type;
private String host;
private int port;
private String name;
private String username;
private String password;
private int maximumAge;
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023-2024 Sonar Contributors
* Copyright (C) 2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -17,6 +17,7 @@
package xyz.jonesdev.sonar.api.controller;
import com.alessiodp.libby.LibraryManager;
import com.j256.ormlite.dao.Dao;
import com.j256.ormlite.dao.DaoManager;
import com.j256.ormlite.jdbc.JdbcPooledConnectionSource;
@@ -33,7 +34,7 @@ import xyz.jonesdev.sonar.api.config.SonarConfiguration;
import xyz.jonesdev.sonar.api.logger.LoggerWrapper;
import xyz.jonesdev.sonar.api.model.VerifiedPlayer;
import java.net.InetAddress;
import java.io.File;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.time.Instant;
@@ -71,7 +72,7 @@ public final class VerifiedPlayerController {
@Getter
private final @NotNull SonarConfiguration.Database.Type cachedDatabaseType;
public VerifiedPlayerController() {
public VerifiedPlayerController(final @NotNull LibraryManager libraryManager) {
final SonarConfiguration.Database database = Sonar.get().getConfig().getDatabase();
// Cache selected database type, so we don't need to call Sonar.get() every time
cachedDatabaseType = database.getType();
@@ -82,16 +83,41 @@ public final class VerifiedPlayerController {
return;
}
// Initialize MySQL/MariaDB driver
database.getType().getDatabaseType().loadDriver();
// Make sure to only download the driver once
if (!cachedDatabaseType.isDownloaded()) {
LOGGER.info("Downloading {} driver version {}",
cachedDatabaseType.getDatabaseType().getDatabaseName(),
cachedDatabaseType.getDatabaseDriver().getVersion());
// Download and load the driver for the current database
libraryManager.loadLibrary(cachedDatabaseType.getDatabaseDriver());
cachedDatabaseType.setDownloaded(true);
}
// Make sure to actually register the loader, so we can use it
cachedDatabaseType.getDatabaseType().loadDriver();
try {
connectionSource = new JdbcPooledConnectionSource(
String.format(database.getType().getConnectionString(),
database.getHost(), database.getPort(), database.getName()),
database.getUsername(), database.getPassword(), database.getType().getDatabaseType());
final String jdbcURL;
// Create table
// H2 has a different JDBC URL layout
// https://www.codejava.net/java-se/jdbc/connect-to-h2-database-examples
if (cachedDatabaseType == SonarConfiguration.Database.Type.H2) {
final File file = new File(Sonar.get().getConfig().getPluginFolder(),
Sonar.get().getConfig().getGeneralConfig().getString("database.filename"));
jdbcURL = String.format(cachedDatabaseType.getConnectionString(), file.getAbsolutePath());
} else {
// Normal JDBC URL layout for MySQL/MariaDB/...
jdbcURL = String.format(cachedDatabaseType.getConnectionString(),
Sonar.get().getConfig().getGeneralConfig().getString("database.host"),
Sonar.get().getConfig().getGeneralConfig().getInt("database.port"),
Sonar.get().getConfig().getGeneralConfig().getString("database.name"));
}
connectionSource = new JdbcPooledConnectionSource(jdbcURL,
Sonar.get().getConfig().getGeneralConfig().getString("database.username"),
Sonar.get().getConfig().getGeneralConfig().getString("database.password"),
cachedDatabaseType.getDatabaseType());
// Create database table
try {
TableUtils.createTableIfNotExists(connectionSource, VerifiedPlayer.class);
} catch (SQLException exception) {
@@ -127,7 +153,6 @@ public final class VerifiedPlayerController {
// The connection source will always be null if the database type is NONE.
if (connectionSource != null) {
try {
// Properly close the connection
connectionSource.close();
} catch (Exception exception) {
LOGGER.error("Error closing database: {}", exception);
@@ -224,7 +249,7 @@ public final class VerifiedPlayerController {
}
try {
dao.create(player);
dao.createIfNotExists(player);
} catch (SQLException exception) {
LOGGER.error("Error trying to add entry: {}", exception);
}
@@ -238,45 +263,52 @@ public final class VerifiedPlayerController {
*/
private void _add(final @NotNull VerifiedPlayer player) {
cache.computeIfAbsent(player.getInetAddress(), v -> new ArrayList<>())
.add(player.getPlayerUUID());
.add(player.getPlayerUuid());
}
/**
* Returns the amount of verified IP addresses
* Returns the number of verified IP addresses
*
* @return Estimated size of the local cache
*/
public synchronized int estimatedSize() {
/*
* TODO: Work on this
* return MAP.values().stream()
* .mapToInt(Collection::size)
* .sum();
*/
public int estimatedSize() {
return cache.size();
}
/**
* @return List of UUIDs associated with one IP address
* Returns the sum of verified IP addresses
* and their respective collection of UUIDs
*
* @return Exact size of all total verified players
*/
@Deprecated
public synchronized int exactSize() {
return cache.values().stream()
.mapToInt(Collection::size)
.sum();
}
/**
* @return {@link java.util.Collection} of UUIDs associated with an IP address
*/
public @Unmodifiable Collection<UUID> getUUIDs(final @NotNull String inetAddress) {
return cache.getOrDefault(inetAddress, Collections.emptyList());
}
/**
* Clear the local cache, and then if the database type is set,
* Clear the local cache, and, if the database type is set,
* remove the table from the database.
*/
public void clearAll() {
try {
cache.clear();
public synchronized void clearAll() {
cache.clear();
// Only update the column if the database type is not NONE
if (cachedDatabaseType != SonarConfiguration.Database.Type.NONE) {
// Delete the entire table from the database, if necessary
if (cachedDatabaseType != SonarConfiguration.Database.Type.NONE) {
try {
dao.deleteBuilder().delete();
} catch (SQLException exception) {
LOGGER.error("Error trying to clear entries: {}", exception);
}
} catch (SQLException exception) {
LOGGER.error("Error trying to clear entries: {}", exception);
}
}
@@ -293,16 +325,6 @@ public final class VerifiedPlayerController {
return false;
}
/**
* @param inetAddress IP address
* @param uuid UUID associated to the IP
* @return Whether the local cache contains the IP and UUID
* @see #has(String, UUID)
*/
public boolean has(final @NotNull InetAddress inetAddress, final @NotNull UUID uuid) {
return has(inetAddress.toString(), uuid);
}
/**
* @param inetAddress IP address
* @return Whether the local cache contains the IP
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023-2024 Sonar Contributors
* Copyright (C) 2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023-2024 Sonar Contributors
* Copyright (C) 2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023-2024 Sonar Contributors
* Copyright (C) 2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023-2024 Sonar Contributors
* Copyright (C) 2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023-2024 Sonar Contributors
* Copyright (C) 2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -17,7 +17,6 @@
package xyz.jonesdev.sonar.api.event.impl;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
@@ -26,7 +25,6 @@ import xyz.jonesdev.sonar.api.event.SonarEvent;
@Getter
@ToString
@EqualsAndHashCode
@RequiredArgsConstructor
public final class AttackMitigatedEvent implements SonarEvent {
private final AttackTracker.AttackStatistics attackStatistics;
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023-2024 Sonar Contributors
* Copyright (C) 2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -17,7 +17,6 @@
package xyz.jonesdev.sonar.api.event.impl;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
@@ -27,7 +26,6 @@ import xyz.jonesdev.sonar.api.fallback.FallbackUser;
@Getter
@ToString
@EqualsAndHashCode
@RequiredArgsConstructor
public final class UserBlacklistedEvent implements SonarEvent {
@ApiStatus.Experimental
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023-2024 Sonar Contributors
* Copyright (C) 2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -17,7 +17,6 @@
package xyz.jonesdev.sonar.api.event.impl;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
@@ -27,7 +26,6 @@ import xyz.jonesdev.sonar.api.fallback.FallbackUser;
@Getter
@ToString
@EqualsAndHashCode
@RequiredArgsConstructor
public final class UserVerifyFailedEvent implements SonarEvent {
private final @NotNull FallbackUser user;
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023-2024 Sonar Contributors
* Copyright (C) 2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -17,7 +17,6 @@
package xyz.jonesdev.sonar.api.event.impl;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
@@ -26,7 +25,6 @@ import xyz.jonesdev.sonar.api.fallback.FallbackUser;
@Getter
@ToString
@EqualsAndHashCode
@RequiredArgsConstructor
public final class UserVerifyJoinEvent implements SonarEvent {
private final String username;
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023-2024 Sonar Contributors
* Copyright (C) 2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -17,7 +17,6 @@
package xyz.jonesdev.sonar.api.event.impl;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
@@ -28,7 +27,6 @@ import java.util.UUID;
@Getter
@ToString
@EqualsAndHashCode
@RequiredArgsConstructor
public final class UserVerifySuccessEvent implements SonarEvent {
private final String username;
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023-2024 Sonar Contributors
* Copyright (C) 2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -25,49 +25,51 @@ import lombok.Setter;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.config.SonarConfiguration;
import xyz.jonesdev.sonar.api.fallback.captcha.CaptchaGenerator;
import xyz.jonesdev.sonar.api.logger.LoggerWrapper;
import java.net.InetAddress;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
@Getter
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class Fallback {
public static final Fallback INSTANCE = new Fallback();
private static final @NotNull Sonar SONAR = Objects.requireNonNull(Sonar.get());
// Map of all connected usernames and their respective IP addresses (used for fast checking)
private final Map<String, InetAddress> connected = new ConcurrentHashMap<>(64, 0.75f);
// Map of all players connected to the server in general
private final ConcurrentMap<InetAddress, Integer> online = new ConcurrentHashMap<>(128);
// Map of all connected IP addresses (used for fast checking)
private final ConcurrentMap<InetAddress, Byte> connected = new ConcurrentHashMap<>(128);
// Cache of all blacklisted IP addresses to ensure each entry can expire after the given time
@Setter
private Cache<InetAddress, Byte> blacklist;
@Setter
private long blacklistTime;
@Setter
private CaptchaGenerator captchaGenerator;
private final @NotNull FallbackQueue queue = FallbackQueue.INSTANCE;
private final @NotNull FallbackRatelimiter ratelimiter = FallbackRatelimiter.INSTANCE;
private final @NotNull FallbackQueue queue = new FallbackQueue();
private final @NotNull FallbackRatelimiter ratelimiter = new FallbackRatelimiter();
private final LoggerWrapper logger = new LoggerWrapper() {
@Override
public void info(final String message, final Object... args) {
SONAR.getLogger().info("[fallback] " + message, args);
Sonar.get().getLogger().info("[fallback] " + message, args);
}
@Override
public void warn(final String message, final Object... args) {
SONAR.getLogger().warn("[fallback] " + message, args);
Sonar.get().getLogger().warn("[fallback] " + message, args);
}
@Override
public void error(final String message, final Object... args) {
SONAR.getLogger().error("[fallback] " + message, args);
Sonar.get().getLogger().error("[fallback] " + message, args);
}
};
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
public boolean shouldVerifyNewPlayers() {
return shouldPerform(Sonar.get().getConfig().getVerification().getTiming());
}
@@ -76,13 +78,9 @@ public final class Fallback {
return shouldPerform(Sonar.get().getConfig().getVerification().getMap().getTiming());
}
public boolean shouldPerformVehicleCheck() {
return shouldPerform(Sonar.get().getConfig().getVerification().getVehicle().getTiming());
}
private static boolean shouldPerform(final SonarConfiguration.Verification.Timing timing) {
return timing == SonarConfiguration.Verification.Timing.ALWAYS
|| (timing == SonarConfiguration.Verification.Timing.DURING_ATTACK
&& Sonar.get().getAttackTracker().isCurrentlyUnderAttack());
&& Sonar.get().getAttackTracker().getCurrentAttack() != null);
}
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023-2024 Sonar Contributors
* Copyright (C) 2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -18,8 +18,14 @@
package xyz.jonesdev.sonar.api.fallback;
public interface FallbackPipelines {
String FALLBACK_HANDLER = "sonar-fallback-handler";
String FALLBACK_PACKET_ENCODER = "sonar-fallback-encoder";
String FALLBACK_PACKET_DECODER = "sonar-fallback-decoder";
String FALLBACK_BANDWIDTH = "sonar-fallback-bandwidth";
String FALLBACK_INACTIVE_LISTENER = "sonar-inactive-listener";
String FALLBACK_INBOUND_HANDLER = "sonar-inbound-handler";
String FALLBACK_FRAME_DECODER = "sonar-frame-decoder";
String FALLBACK_FRAME_ENCODER = "sonar-frame-encoder";
String FALLBACK_TIMEOUT = "sonar-timeout";
String FALLBACK_PACKET_HANDLER = "sonar-packet-handler";
String FALLBACK_PACKET_ENCODER = "sonar-packet-encoder";
String FALLBACK_PACKET_DECODER = "sonar-packet-decoder";
String FALLBACK_TAIL_EXCEPTIONS = "sonar-exception-tail";
String FALLBACK_BANDWIDTH = "sonar-bandwidth-counter";
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023-2024 Sonar Contributors
* Copyright (C) 2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -26,51 +26,33 @@ import java.net.InetAddress;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@Getter
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
@RequiredArgsConstructor(access = AccessLevel.PROTECTED)
public final class FallbackQueue {
static final FallbackQueue INSTANCE = new FallbackQueue();
private final Map<InetAddress, Runnable> queuedPlayers = new ConcurrentHashMap<>(64);
// Fixed thread pool executor for all new verifications
private static final int THREAD_POOL_SIZE = Math.min(0x7fff, Runtime.getRuntime().availableProcessors() * 2);
private static final ExecutorService QUEUE_EXECUTOR = Executors.newFixedThreadPool(THREAD_POOL_SIZE);
// Runnable task executed for polling the queue
private final Runnable pollTask = () -> {
private final ConcurrentMap<InetAddress, Runnable> players = new ConcurrentHashMap<>(
128, 0.75f, Runtime.getRuntime().availableProcessors());
public void poll() {
final int maxQueuePolls = Sonar.get().getConfig().getQueue().getMaxQueuePolls();
int index = 0;
// We need to be cautious here since we don't want any concurrency issues.
final Iterator<Map.Entry<InetAddress, Runnable>> iterator = queuedPlayers.entrySet().iterator();
while (iterator.hasNext()) {
// Break if we reached our maximum entries
if (++index > maxQueuePolls) {
break;
}
// Iterate through the map and process up to maxQueuePolls entries
// We need to be cautious here since we don't want any concurrency issues
final Iterator<Map.Entry<InetAddress, Runnable>> iterator = players.entrySet().iterator();
while (iterator.hasNext() && index++ < maxQueuePolls) {
// Run the cached runnable
final Map.Entry<InetAddress, Runnable> entry = iterator.next();
entry.getValue().run();
// Remove runnable from iterator
QUEUE_EXECUTOR.execute(entry.getValue());
// Remove runnable from the map
iterator.remove();
}
// Run the attack check task
Sonar.get().getAttackTracker().checkIfUnderAttack();
// Clean up the cache of rate-limited IPs
Sonar.get().getFallback().getRatelimiter().cleanUpCache();
};
/**
* @param inetAddress IP address of the player
* @param runnable Queued action on the netty thread
* @see #remove
*/
public void queue(final InetAddress inetAddress, final Runnable runnable) {
queuedPlayers.put(inetAddress, runnable);
}
/**
* @param inetAddress IP address of the player
*/
public void remove(final InetAddress inetAddress) {
queuedPlayers.remove(inetAddress);
}
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023-2024 Sonar Contributors
* Copyright (C) 2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -27,40 +27,25 @@ import org.jetbrains.annotations.NotNull;
import java.net.InetAddress;
@Setter
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public final class FallbackRatelimiter {
static final FallbackRatelimiter INSTANCE = new FallbackRatelimiter();
private Cache<InetAddress, Byte> attemptCache;
private long timeout;
private Cache<InetAddress, Long> attemptCache;
@Getter
private Cache<InetAddress, Integer> failCountCache;
/**
* We don't want to clean up the cache in the
* {@link #attempt(InetAddress)} method because
* it would take up too many resources.
*/
public void cleanUpCache() {
attemptCache.cleanUp();
failCountCache.cleanUp();
}
/**
* Checks if the player has tried verifying too fast
* Attempts to rate-limit the client
*
* @param inetAddress IP address of the player
* @return Whether the player is allowed to verify
*/
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
public boolean attempt(final @NotNull InetAddress inetAddress) {
// Cache the IP address if it's not already cached
if (!attemptCache.asMap().containsKey(inetAddress)) {
// Cache the IP address
attemptCache.put(inetAddress, (byte) 0);
return true;
}
// Deny the connection attempt
return false;
final long expectedTimestamp = System.currentTimeMillis() + timeout;
// Check if the time since the last join has not exceeded the timeout
// The idea for this check was Taken from Velocity
final long last = attemptCache.get(inetAddress, result -> expectedTimestamp);
return expectedTimestamp == last;
}
/**
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023-2024 Sonar Contributors
* Copyright (C) 2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -18,48 +18,60 @@
package xyz.jonesdev.sonar.api.fallback;
import io.netty.channel.Channel;
import io.netty.channel.ChannelPipeline;
import io.netty.util.ReferenceCountUtil;
import net.kyori.adventure.text.Component;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.api.timer.SystemTimer;
import java.net.InetAddress;
import java.util.UUID;
public interface FallbackUser {
@NotNull Channel getChannel();
@NotNull
Channel getChannel();
@NotNull InetAddress getInetAddress();
@NotNull
ChannelPipeline getPipeline();
@NotNull ProtocolVersion getProtocolVersion();
@NotNull
InetAddress getInetAddress();
@NotNull FallbackUserState getState();
@NotNull
ProtocolVersion getProtocolVersion();
void setState(final FallbackUserState state);
@NotNull
SystemTimer getLoginTimer();
@NotNull
UUID getOfflineUuid();
boolean isReceivedClientSettings();
void setReceivedClientSettings(final boolean receivedClientSettings);
boolean isReceivedPluginMessage();
void setReceivedPluginMessage(final boolean receivedPluginMessage);
boolean isGeyser();
/**
* Disconnect the player during/after verification
* using our custom Disconnect packet.
*
* @param reason Disconnect message component
* @param reason Disconnect message component
*/
@ApiStatus.Internal
void disconnect(final @NotNull Component reason);
/**
* Takes over the channel and begins the verification process
*
* @param username Username of the player
* @param uuid UUID of the player
* @param encoder Name of the encoder pipeline
* @param decoder Name of the decoder pipeline
* @param timeout Name of the read timeout pipeline
* @param boss Name of the main pipeline
* @param username Username of the player
* @param offlineUuid UUID of the player
*/
void hijack(final @NotNull String username, final @NotNull UUID uuid,
final @NotNull String encoder, final @NotNull String decoder,
final @NotNull String timeout, final @NotNull String boss);
void hijack(final @NotNull String username, final @NotNull UUID offlineUuid);
/**
* Sends a packet/message to the player
@@ -15,16 +15,11 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.protocol.block;
package xyz.jonesdev.sonar.api.fallback.captcha;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
import java.awt.image.BufferedImage;
@Getter
@ToString
@RequiredArgsConstructor
public final class BlockPosition {
private final int x, y, z;
private final int chunkX, chunkZ;
@FunctionalInterface
public interface CaptchaGenerator {
BufferedImage createImage(final char[] answer);
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023-2024 Sonar Contributors
* Copyright (C) 2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -20,10 +20,8 @@ package xyz.jonesdev.sonar.api.fallback.protocol;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
@Getter
@RequiredArgsConstructor
@@ -68,7 +66,9 @@ public enum ProtocolVersion {
MINECRAFT_1_19_4(762),
MINECRAFT_1_20(763),
MINECRAFT_1_20_2(764),
MINECRAFT_1_20_3(765);
MINECRAFT_1_20_3(765),
MINECRAFT_1_20_5(766),
MINECRAFT_1_21(767);
private final int protocol;
@@ -76,27 +76,14 @@ public enum ProtocolVersion {
public static final Map<Integer, ProtocolVersion> ID_TO_PROTOCOL_CONSTANT;
static {
{
final Map<Integer, ProtocolVersion> versions = new HashMap<>();
final Map<Integer, ProtocolVersion> versions = new HashMap<>();
for (final ProtocolVersion version : values()) {
versions.putIfAbsent(version.protocol, version);
}
ID_TO_PROTOCOL_CONSTANT = versions;
for (final ProtocolVersion version : values()) {
versions.putIfAbsent(version.protocol, version);
}
{
final Set<ProtocolVersion> versions = EnumSet.noneOf(ProtocolVersion.class);
for (final ProtocolVersion value : values()) {
if (!value.isUnknown()) {
versions.add(value);
}
}
LATEST_VERSION = (ProtocolVersion) versions.toArray()[versions.size() - 1];
}
ID_TO_PROTOCOL_CONSTANT = versions;
LATEST_VERSION = values()[values().length - 1];
}
public static ProtocolVersion fromId(final int protocol) {
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023-2024 Sonar Contributors
* Copyright (C) 2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -21,12 +21,14 @@ import com.sun.management.OperatingSystemMXBean;
import lombok.experimental.UtilityClass;
import java.lang.management.ManagementFactory;
import java.text.DecimalFormat;
@UtilityClass
public class JVMProcessInformation {
private final Runtime RUNTIME = Runtime.getRuntime();
private final OperatingSystemMXBean MX = (OperatingSystemMXBean) ManagementFactory.getOperatingSystemMXBean();
private final char[] MEMORY_UNITS = {'K', 'M', 'G', 'T', 'P', 'E'};
private final DecimalFormat DECIMAL_FORMAT = new DecimalFormat("#.#");
public String formatMemory(final long size) {
if (size < 1024L) {
@@ -37,7 +39,7 @@ public class JVMProcessInformation {
final double formattedSize = (double) size / (1L << (group * 10));
final char unit = MEMORY_UNITS[group - 1];
// https://en.wikipedia.org/wiki/Byte#Multiple-byte_units
return String.format("%.1f %siB", formattedSize, unit);
return DECIMAL_FORMAT.format(formattedSize) + " " + unit + "iB";
}
/**
@@ -75,14 +77,6 @@ public class JVMProcessInformation {
return getSystemCPUUsage() / getVirtualCores();
}
/**
* @return Current system load average in percent
* @apiNote This returns the global process load, not the CPU usage
*/
public double getSystemLoadAverage() {
return MX.getSystemLoadAverage() * 100;
}
/**
* @return Memory (in bytes) the JVM will attempt to use
*/
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023-2024 Sonar Contributors
* Copyright (C) 2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023-2024 Sonar Contributors
* Copyright (C) 2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -23,7 +23,9 @@ import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
import org.jetbrains.annotations.NotNull;
import java.net.InetAddress;
import java.sql.Timestamp;
import java.util.UUID;
@@ -50,7 +52,7 @@ public final class VerifiedPlayer {
uniqueIndexName = "ip_address_player_uuid_idx",
width = 36
)
private UUID playerUUID;
private UUID playerUuid;
@DatabaseField(
columnName = "timestamp",
@@ -58,9 +60,17 @@ public final class VerifiedPlayer {
)
private Timestamp timestamp;
public VerifiedPlayer(final String inetAddress, final UUID playerUUID, final long timestamp) {
public VerifiedPlayer(final @NotNull InetAddress inetAddress,
final @NotNull UUID playerUuid,
final long timestamp) {
this(inetAddress.toString(), playerUuid, timestamp);
}
public VerifiedPlayer(final @NotNull String inetAddress,
final @NotNull UUID playerUuid,
final long timestamp) {
this.inetAddress = inetAddress;
this.playerUUID = playerUUID;
this.playerUuid = playerUuid;
this.timestamp = new Timestamp(timestamp);
}
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023-2024 Sonar Contributors
* Copyright (C) 2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -15,27 +15,17 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.api.fallback;
package xyz.jonesdev.sonar.api.ormlite;
public enum FallbackUserState {
// 1.20.2 configuration state
LOGIN_ACK, CONFIGURE,
// pre-JOIN ping check
KEEP_ALIVE,
// post-JOIN checks
CLIENT_SETTINGS, PLUGIN_MESSAGE, TRANSACTION,
// PLAY checks
TELEPORT, POSITION,
// Vehicle check
VEHICLE,
// CAPTCHA
CAPTCHA,
// Done
SUCCESS,
// Placeholder
FAILED;
import com.j256.ormlite.jdbc.db.H2DatabaseType;
import org.jetbrains.annotations.NotNull;
public boolean canReceivePackets() {
return this != FAILED && this != SUCCESS;
public final class H2DatabaseTypeAdapter extends H2DatabaseType {
// We need to override the default driver class name
// to use the custom relocated H2 driver
@Override
protected String @NotNull [] getDriverClassNames() {
return new String[]{"xyz.jonesdev.sonar.libs.h2.Driver"};
}
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023-2024 Sonar Contributors
* Copyright (C) 2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -22,11 +22,8 @@ import org.jetbrains.annotations.NotNull;
public final class MariaDbDatabaseTypeAdapter extends MariaDbDatabaseType {
/**
* We need to override the default driver class name
* in order to use the custom relocated MariaDB driver
* Read more: {@link MysqlDatabaseTypeAdapter}
*/
// We need to override the default driver class name
// to use the custom relocated MariaDB driver
@Override
protected String @NotNull [] getDriverClassNames() {
return new String[]{"xyz.jonesdev.sonar.libs.mariadb.jdbc.Driver"};
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023-2024 Sonar Contributors
* Copyright (C) 2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -22,11 +22,8 @@ import org.jetbrains.annotations.NotNull;
public final class MysqlDatabaseTypeAdapter extends MysqlDatabaseType {
/**
* We need to override the default driver class name
* in order to use the custom relocated MySQL driver
* Read more: {@link MariaDbDatabaseTypeAdapter}
*/
// We need to override the default driver class name
// to use the custom relocated MySQL driver
@Override
protected String @NotNull [] getDriverClassNames() {
return new String[]{"xyz.jonesdev.sonar.libs.mysql.cj.jdbc.Driver"};
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023-2024 Sonar Contributors
* Copyright (C) 2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -19,6 +19,7 @@ package xyz.jonesdev.sonar.api.statistics;
import org.jetbrains.annotations.ApiStatus;
@SuppressWarnings("unused")
public interface SonarStatistics {
/**
@@ -108,7 +109,7 @@ public interface SonarStatistics {
/**
* @return Total number of cases where an IP was blacklisted
* @apiNote This does not describe the current amount of blacklisted IPs
* @apiNote This does not describe the current number of blacklisted IPs
*/
long getTotalBlacklistSize();
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023-2024 Sonar Contributors
* Copyright (C) 2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -18,13 +18,12 @@
package xyz.jonesdev.sonar.api.timer;
import lombok.Getter;
import org.jetbrains.annotations.NotNull;
import java.text.SimpleDateFormat;
@Getter
public final class SystemTimer {
private static final SimpleDateFormat FORMATTER = new SimpleDateFormat("mm:ss");
public static final SimpleDateFormat DATE_FORMATTER = new SimpleDateFormat("mm:ss");
private long start = System.currentTimeMillis();
@@ -40,19 +39,12 @@ public final class SystemTimer {
}
/**
* @apiNote Read more: {@link SystemTimer#delay}, {@link SystemTimer#formattedDelay}
* @apiNote Read more: {@link SystemTimer#delay}
*/
public boolean elapsed(final long amount) {
return delay() >= amount;
}
/**
* @apiNote Read more: {@link SystemTimer#delay}
*/
public @NotNull String formattedDelay() {
return FORMATTER.format(delay());
}
@Override
public String toString() {
return String.format("%.3f", delay() / 1000D);
@@ -21,56 +21,69 @@ import lombok.Getter;
import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
import net.kyori.adventure.title.Title;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.statistics.SonarStatistics;
import java.util.Collection;
import java.util.List;
import java.util.UUID;
import java.util.Vector;
import static xyz.jonesdev.sonar.api.Sonar.DECIMAL_FORMAT;
import static xyz.jonesdev.sonar.api.jvm.JVMProcessInformation.*;
@Getter
public final class Notification implements Observable {
private final @NotNull Collection<UUID> subscribers = new Vector<>(0);
public final class Notification extends Observable {
@Override
public void observe() {
// Don't prepare component if there are no subscribers
if (subscribers.isEmpty()) {
return;
}
// Prepare the chat notification
final Component notificationTitle = MiniMessage.miniMessage().deserialize(
Sonar.get().getConfig().getMessagesConfig().getString("notifications.title"));
final Component notificationSubtitle = MiniMessage.miniMessage().deserialize(
Sonar.get().getConfig().getMessagesConfig().getString("notifications.subtitle"));
final Title title = Title.title(notificationTitle, notificationSubtitle);
final List<String> chatNotification = Sonar.get().getConfig().getMessagesConfig().getStringList("notifications.chat");
final Component[] chatNotificationComponents = new Component[chatNotification.size()];
for (int i = 0; i < chatNotificationComponents.length; i++) {
chatNotificationComponents[i] = MiniMessage.miniMessage().deserialize(chatNotification.get(i),
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix()),
Placeholder.unparsed("queued", DECIMAL_FORMAT.format(Sonar.get().getFallback().getQueue().getPlayers().size())),
Placeholder.unparsed("verifying", DECIMAL_FORMAT.format(Sonar.get().getFallback().getConnected().size())),
Placeholder.unparsed("blacklisted", DECIMAL_FORMAT.format(Sonar.get().getFallback().getBlacklist().estimatedSize())),
Placeholder.unparsed("total-joins", DECIMAL_FORMAT.format(Sonar.get().getStatistics().getTotalPlayersJoined())),
Placeholder.unparsed("logins-per-second", DECIMAL_FORMAT.format(Sonar.get().getStatistics().getLoginsPerSecond())),
Placeholder.unparsed("connections-per-second", DECIMAL_FORMAT.format(Sonar.get().getStatistics().getConnectionsPerSecond())),
Placeholder.unparsed("verify-total", DECIMAL_FORMAT.format(Sonar.get().getStatistics().getTotalAttemptedVerifications())),
Placeholder.unparsed("verify-success", DECIMAL_FORMAT.format(Sonar.get().getStatistics().getTotalSuccessfulVerifications())),
Placeholder.unparsed("verify-failed", DECIMAL_FORMAT.format(Sonar.get().getStatistics().getTotalFailedVerifications())),
Placeholder.unparsed("incoming-traffic", Sonar.get().getStatistics().getCurrentIncomingBandwidthFormatted()),
Placeholder.unparsed("outgoing-traffic", Sonar.get().getStatistics().getCurrentOutgoingBandwidthFormatted()),
Placeholder.unparsed("incoming-traffic-ttl", Sonar.get().getStatistics().getTotalIncomingBandwidthFormatted()),
Placeholder.unparsed("outgoing-traffic-ttl", Sonar.get().getStatistics().getTotalOutgoingBandwidthFormatted()),
Placeholder.unparsed("used-memory", formatMemory(getUsedMemory())),
Placeholder.unparsed("free-memory", formatMemory(getFreeMemory())),
Placeholder.unparsed("total-memory", formatMemory(getTotalMemory())),
Placeholder.unparsed("max-memory", formatMemory(getMaxMemory())));
}
public void sendAttackNotification() {
// No need to do anything if we have no subscribers
if (subscribers.isEmpty()) return;
final SonarStatistics statistics = Sonar.get().getStatistics();
// Prepare the title
// TODO: Why do custom title times not work?
// Title.Times.times(Ticks.duration(5L), Ticks.duration(60L), Ticks.duration(10L));
final Title title = Title.title(
Sonar.get().getConfig().getNotifications().getNotificationTitle(),
Sonar.get().getConfig().getNotifications().getNotificationSubtitle());
// Prepare the chat message
final Component chat = MiniMessage.miniMessage().deserialize(
Sonar.get().getConfig().getNotifications().getNotificationChat()
.replace("%queued%",
DECIMAL_FORMAT.format(Sonar.get().getFallback().getQueue().getQueuedPlayers().size()))
.replace("%verifying%", DECIMAL_FORMAT.format(Sonar.get().getFallback().getConnected().size()))
.replace("%blacklisted%",
DECIMAL_FORMAT.format(Sonar.get().getFallback().getBlacklist().estimatedSize()))
.replace("%total-joins%", DECIMAL_FORMAT.format(statistics.getTotalPlayersJoined()))
.replace("%logins-per-second%", DECIMAL_FORMAT.format(statistics.getLoginsPerSecond()))
.replace("%connections-per-second%", DECIMAL_FORMAT.format(statistics.getConnectionsPerSecond()))
.replace("%verify-total%", DECIMAL_FORMAT.format(statistics.getTotalAttemptedVerifications()))
.replace("%verify-success%",
DECIMAL_FORMAT.format(Sonar.get().getVerifiedPlayerController().estimatedSize()))
.replace("%verify-failed%", DECIMAL_FORMAT.format(statistics.getTotalFailedVerifications()))
.replace("%incoming-traffic%", statistics.getCurrentIncomingBandwidthFormatted())
.replace("%outgoing-traffic%", statistics.getCurrentOutgoingBandwidthFormatted())
.replace("%incoming-traffic-ttl%", statistics.getTotalIncomingBandwidthFormatted())
.replace("%outgoing-traffic-ttl%", statistics.getTotalOutgoingBandwidthFormatted()));
// Send the title and chat messages to all online players
for (final UUID subscriber : Sonar.get().getNotificationHandler().getSubscribers()) {
final Audience audience = Sonar.get().audience(subscriber);
if (audience == null) continue;
// Send the message to all online subscribers
for (Component component : chatNotificationComponents) {
audience.sendMessage(component);
}
// Send the title to all online subscribers
audience.showTitle(title);
audience.sendMessage(chat);
}
}
}
@@ -17,33 +17,38 @@
package xyz.jonesdev.sonar.api.verbose;
import lombok.Getter;
import org.jetbrains.annotations.NotNull;
import java.util.Collection;
import java.util.UUID;
import java.util.Vector;
public interface Observable {
Collection<UUID> getSubscribers();
@Getter
public abstract class Observable {
protected final @NotNull Collection<UUID> subscribers = new Vector<>(0);
public abstract void observe();
/**
* @param uuid UUID of the player
* @return Whether the audience is subscribed or not
*/
default boolean isSubscribed(final @NotNull UUID uuid) {
public final boolean isSubscribed(final @NotNull UUID uuid) {
return getSubscribers().contains(uuid);
}
/**
* @param uuid UUID of the player
*/
default void subscribe(final UUID uuid) {
public final void subscribe(final UUID uuid) {
getSubscribers().add(uuid);
}
/**
* @param uuid UUID of the player
*/
default void unsubscribe(final UUID uuid) {
public final void unsubscribe(final UUID uuid) {
getSubscribers().remove(uuid);
}
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023-2024 Sonar Contributors
* Copyright (C) 2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -18,80 +18,72 @@
package xyz.jonesdev.sonar.api.verbose;
import lombok.Getter;
import lombok.val;
import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.attack.AttackTracker;
import xyz.jonesdev.sonar.api.statistics.SonarStatistics;
import xyz.jonesdev.sonar.api.timer.SystemTimer;
import java.util.Collection;
import java.util.UUID;
import java.util.Vector;
import static xyz.jonesdev.sonar.api.Sonar.DECIMAL_FORMAT;
import static xyz.jonesdev.sonar.api.jvm.JVMProcessInformation.*;
import static xyz.jonesdev.sonar.api.timer.SystemTimer.DATE_FORMATTER;
@Getter
public final class Verbose implements Observable {
private final @NotNull Collection<UUID> subscribers = new Vector<>(0);
public final class Verbose extends Observable {
private int animationIndex;
// Run action bar verbose
public void update() {
// Make sure to clean up the cached statistics
Sonar.get().getStatistics().cleanUpCache();
@Override
public void observe() {
// Don't prepare component if there are no subscribers
if (subscribers.isEmpty()) return;
// Prepare the action bar format component
final Component component = prepareActionBarFormat();
if (subscribers.isEmpty()) {
return;
}
// Prepare the action bar verbose
final AttackTracker.AttackStatistics attackStatistics = Sonar.get().getAttackTracker().getCurrentAttack();
final SystemTimer attackTimer = attackStatistics == null ? null : attackStatistics.getDuration();
final String attackDuration = attackTimer == null ? "00:00" : DATE_FORMATTER.format(attackTimer.delay());
final Component actionBarComponent = MiniMessage.miniMessage().deserialize(attackTimer == null
? Sonar.get().getConfig().getMessagesConfig().getString("verbose.layout.normal")
: Sonar.get().getConfig().getMessagesConfig().getString("verbose.layout.attack"),
Placeholder.component("prefix", Sonar.get().getConfig().getPrefix()),
Placeholder.unparsed("attack-duration", attackDuration),
Placeholder.unparsed("animation", nextAnimation()),
Placeholder.unparsed("queued", DECIMAL_FORMAT.format(Sonar.get().getFallback().getQueue().getPlayers().size())),
Placeholder.unparsed("verifying", DECIMAL_FORMAT.format(Sonar.get().getFallback().getConnected().size())),
Placeholder.unparsed("blacklisted", DECIMAL_FORMAT.format(Sonar.get().getFallback().getBlacklist().estimatedSize())),
Placeholder.unparsed("total-joins", DECIMAL_FORMAT.format(Sonar.get().getStatistics().getTotalPlayersJoined())),
Placeholder.unparsed("logins-per-second", DECIMAL_FORMAT.format(Sonar.get().getStatistics().getLoginsPerSecond())),
Placeholder.unparsed("connections-per-second", DECIMAL_FORMAT.format(Sonar.get().getStatistics().getConnectionsPerSecond())),
Placeholder.unparsed("verify-total", DECIMAL_FORMAT.format(Sonar.get().getStatistics().getTotalAttemptedVerifications())),
Placeholder.unparsed("verify-success", DECIMAL_FORMAT.format(Sonar.get().getStatistics().getTotalSuccessfulVerifications())),
Placeholder.unparsed("verify-failed", DECIMAL_FORMAT.format(Sonar.get().getStatistics().getTotalFailedVerifications())),
Placeholder.unparsed("incoming-traffic", Sonar.get().getStatistics().getCurrentIncomingBandwidthFormatted()),
Placeholder.unparsed("outgoing-traffic", Sonar.get().getStatistics().getCurrentOutgoingBandwidthFormatted()),
Placeholder.unparsed("incoming-traffic-ttl", Sonar.get().getStatistics().getTotalIncomingBandwidthFormatted()),
Placeholder.unparsed("outgoing-traffic-ttl", Sonar.get().getStatistics().getTotalOutgoingBandwidthFormatted()),
Placeholder.unparsed("used-memory", formatMemory(getUsedMemory())),
Placeholder.unparsed("free-memory", formatMemory(getFreeMemory())),
Placeholder.unparsed("total-memory", formatMemory(getTotalMemory())),
Placeholder.unparsed("max-memory", formatMemory(getMaxMemory())));
// Send the action bar to all online players
for (final UUID subscriber : subscribers) {
final Audience audience = Sonar.get().audience(subscriber);
if (audience == null) continue;
audience.sendActionBar(component);
// Send the action bar to all online subscribers
audience.sendActionBar(actionBarComponent);
}
}
@ApiStatus.Internal
public @NotNull Component prepareActionBarFormat() {
final SonarStatistics statistics = Sonar.get().getStatistics();
final AttackTracker.AttackStatistics attackStatistics = Sonar.get().getAttackTracker().getCurrentAttack();
final SystemTimer attackDuration = attackStatistics == null ? null : attackStatistics.getDuration();
return MiniMessage.miniMessage().deserialize((attackDuration != null
? Sonar.get().getConfig().getVerbose().getActionBarLayoutDuringAttack()
: Sonar.get().getConfig().getVerbose().getActionBarLayout())
.replace("%queued%",
DECIMAL_FORMAT.format(Sonar.get().getFallback().getQueue().getQueuedPlayers().size()))
.replace("%verifying%", DECIMAL_FORMAT.format(Sonar.get().getFallback().getConnected().size()))
.replace("%blacklisted%",
DECIMAL_FORMAT.format(Sonar.get().getFallback().getBlacklist().estimatedSize()))
.replace("%blacklisted%", DECIMAL_FORMAT.format(statistics.getCurrentBlacklistSize()))
.replace("%total-joins%", DECIMAL_FORMAT.format(statistics.getTotalPlayersJoined()))
.replace("%logins-per-second%", DECIMAL_FORMAT.format(statistics.getLoginsPerSecond()))
.replace("%connections-per-second%", DECIMAL_FORMAT.format(statistics.getConnectionsPerSecond()))
.replace("%verify-total%", DECIMAL_FORMAT.format(statistics.getTotalAttemptedVerifications()))
.replace("%verify-success%",
DECIMAL_FORMAT.format(Sonar.get().getVerifiedPlayerController().estimatedSize()))
.replace("%verify-failed%", DECIMAL_FORMAT.format(statistics.getTotalFailedVerifications()))
.replace("%attack-duration%", attackDuration == null ? "00:00" : attackDuration.formattedDelay())
.replace("%incoming-traffic%", statistics.getCurrentIncomingBandwidthFormatted())
.replace("%outgoing-traffic%", statistics.getCurrentOutgoingBandwidthFormatted())
.replace("%incoming-traffic-ttl%", statistics.getTotalIncomingBandwidthFormatted())
.replace("%outgoing-traffic-ttl%", statistics.getTotalOutgoingBandwidthFormatted())
.replace("%used-memory%", formatMemory(getUsedMemory()))
.replace("%free-memory%", formatMemory(getFreeMemory()))
.replace("%total-memory%", formatMemory(getTotalMemory()))
.replace("%max-memory%", formatMemory(getMaxMemory()))
.replace("%animation%", nextAnimation()));
}
public String nextAnimation() {
val animations = Sonar.get().getConfig().getVerbose().getAnimation();
final var animations = Sonar.get().getConfig().getVerboseAnimation();
final int nextIndex = ++animationIndex % animations.size();
return animations.toArray(new String[0])[nextIndex];
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023-2024 Sonar Contributors
* Copyright (C) 2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -56,8 +56,7 @@ public final class DiscordWebhook {
outputStream.write(serializedContent.getBytes(StandardCharsets.UTF_8));
}
// Warn the user if the connection might've not been successful
// I don't know why Discord doesn't respond with OK, but I guess this should be fine
// Discord replies with HTTP_NO_CONTENT (204) if the request is successful
final int code = urlConnection.getResponseCode();
if (code != HTTP_NO_CONTENT) {
Sonar.get().getLogger().warn("Unexpected Discord webhook response code {}", code);
+11 -23
View File
@@ -1,19 +1,18 @@
import net.minecrell.pluginyml.bukkit.BukkitPluginDescription
plugins {
id("net.minecrell.plugin-yml.bukkit") version "0.6.0"
alias(libs.plugins.pluginyml.bukkit) apply true
}
apply(plugin = "net.minecrell.plugin-yml.bukkit")
bukkit {
name = rootProject.name
version = rootProject.version.toString().split("-")[0]
main = "xyz.jonesdev.sonar.bukkit.SonarBukkitPlugin"
authors = listOf("Jones Development", "Sonar Contributors")
website = "https://jonesdev.xyz/discord/"
load = BukkitPluginDescription.PluginLoadOrder.POSTWORLD
softDepend = listOf("Geyser-Spigot", "floodgate", "Protocolize", "packetevents", "ProtocolLib", "ViaVersion")
softDepend = listOf("Geyser-Spigot", "floodgate", "Protocolize", "ProtocolSupport",
"ViaVersion", "packetevents", "ProtocolLib", "FastLogin")
apiVersion = "1.13" // ignore legacy plugin warning
foliaSupported = true
commands {
@@ -32,25 +31,14 @@ dependencies {
implementation(project(":api"))
implementation(project(":common"))
// adventure platform support
implementation("net.kyori:adventure-platform-bukkit:4.3.2")
implementation("net.kyori:adventure-platform-api:4.3.2")
implementation("net.kyori:adventure-platform-facet:4.3.2")
compileOnly(rootProject.libs.spigot)
testCompileOnly(rootProject.libs.spigot)
// adventure minimessage
compileOnly("net.kyori:adventure-text-minimessage:4.16.0")
compileOnly("net.kyori:adventure-text-serializer-gson:4.16.0")
// adventure nbt
implementation("net.kyori:adventure-nbt:4.16.0")
// We have to use 1.8 for backwards compatibility
compileOnly("org.spigotmc:spigot-api:1.8.8-R0.1-SNAPSHOT")
// Implement bStats.org for metrics
implementation("org.bstats:bstats-bukkit:3.0.2")
// Library/dependency loading
implementation("com.alessiodp.libby:libby-bukkit:2.0.0-SNAPSHOT")
implementation(rootProject.libs.adventure.platform.bukkit) {
exclude(module = "adventure-nbt")
}
implementation(rootProject.libs.bstats.bukkit)
implementation(rootProject.libs.libby.bukkit)
}
tasks {
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023-2024 Sonar Contributors
* Copyright (C) 2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -23,29 +23,24 @@ import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.platform.bukkit.BukkitAudiences;
import org.bstats.bukkit.Metrics;
import org.bstats.charts.SimplePie;
import org.bukkit.entity.Player;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.SonarPlatform;
import xyz.jonesdev.sonar.api.logger.LoggerWrapper;
import xyz.jonesdev.sonar.bukkit.command.BukkitSonarCommand;
import xyz.jonesdev.sonar.bukkit.fallback.FallbackBukkitInjector;
import xyz.jonesdev.sonar.bukkit.listener.BukkitJoinListener;
import xyz.jonesdev.sonar.common.boot.SonarBootstrap;
import xyz.jonesdev.sonar.common.statistics.CachedBandwidthStatistics;
import java.net.InetAddress;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
@Getter
public final class SonarBukkit extends SonarBootstrap<SonarBukkitPlugin> {
public static SonarBukkit INSTANCE;
public SonarBukkit(final @NotNull SonarBukkitPlugin plugin) {
super(plugin, new BukkitLibraryManager(
plugin, plugin.getDataFolder().getName()),
plugin.getDataFolder(), SonarPlatform.BUKKIT);
INSTANCE = this;
super(plugin, SonarPlatform.BUKKIT, plugin.getDataFolder(), new BukkitLibraryManager(plugin));
}
/**
@@ -62,17 +57,8 @@ public final class SonarBukkit extends SonarBootstrap<SonarBukkitPlugin> {
}
@Override
public boolean hasTooManyAccounts(final @NotNull InetAddress inetAddress, final int limit) {
int count = 1;
for (final Player player : getPlugin().getServer().getOnlinePlayers()) {
// I don't know why, but Bukkit is *special*
if (Objects.isNull(player.getAddress())) continue;
// Check if the IP address of the player is equal to the IP trying to connect
if (!Objects.equals(player.getAddress().getAddress(), inetAddress)) continue;
// Increment count of duplicated accounts
if (++count >= limit) return true;
}
return false;
public @NotNull Audience sender(final @NotNull Object object) {
return bukkitAudiences.sender((CommandSender) object);
}
/**
@@ -101,6 +87,8 @@ public final class SonarBukkit extends SonarBootstrap<SonarBukkitPlugin> {
private Metrics metrics;
public static final CompletableFuture<Void> INITIALIZE_LISTENER = new CompletableFuture<>();
@Override
public void enable() {
// Initialize bStats.org metrics
@@ -111,26 +99,30 @@ public final class SonarBukkit extends SonarBootstrap<SonarBukkitPlugin> {
() -> getConfig().getVerification().getTiming().getDisplayName()));
metrics.addCustomChart(new SimplePie("captcha",
() -> getConfig().getVerification().getMap().getTiming().getDisplayName()));
metrics.addCustomChart(new SimplePie("language",
() -> getConfig().getLanguage().getName()));
metrics.addCustomChart(new SimplePie("database_type",
() -> getConfig().getDatabase().getType().getDisplayName()));
// Register Sonar command
Objects.requireNonNull(getPlugin().getCommand("sonar")).setExecutor(new BukkitSonarCommand());
// Register traffic service
getPlugin().getServer().getScheduler().runTaskTimerAsynchronously(getPlugin(),
CachedBandwidthStatistics::reset, 20L, 20L);
// Try to inject into the server
if (FallbackBukkitInjector.isLateBindEnabled()) {
getPlugin().getServer().getScheduler().runTask(getPlugin(), FallbackBukkitInjector::inject);
} else {
getPlugin().getServer().getPluginManager().registerEvents(new BukkitJoinListener(), getPlugin());
}
// Register queue service
getPlugin().getServer().getScheduler().runTaskTimerAsynchronously(getPlugin(),
getFallback().getQueue().getPollTask(), 10L, 10L);
// Register verbose service
getPlugin().getServer().getScheduler().runTaskTimerAsynchronously(getPlugin(),
Sonar.get().getVerboseHandler()::update, 4L, 4L);
// Let the injector know that the plugin has been enabled
INITIALIZE_LISTENER.complete(null);
}
@Override
public void disable() {
// Make sure to properly shutdown bStats metrics
metrics.shutdown();
if (metrics != null) {
// Make sure to properly shutdown bStats metrics
metrics.shutdown();
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023-2024 Sonar Contributors
* Copyright (C) 2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -18,10 +18,19 @@
package xyz.jonesdev.sonar.bukkit;
import org.bukkit.plugin.java.JavaPlugin;
import xyz.jonesdev.sonar.bukkit.fallback.FallbackBukkitInjector;
public final class SonarBukkitPlugin extends JavaPlugin {
private SonarBukkit bootstrap;
@Override
public void onLoad() {
// Inject early if late-bind is disabled
if (!FallbackBukkitInjector.isLateBindEnabled()) {
FallbackBukkitInjector.inject();
}
}
@Override
public void onEnable() {
bootstrap = new SonarBukkit(this);
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023-2024 Sonar Contributors
* Copyright (C) 2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -23,9 +23,9 @@ import org.bukkit.command.CommandSender;
import org.bukkit.command.TabExecutor;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.command.InvocationSource;
import xyz.jonesdev.sonar.api.command.SonarCommand;
import xyz.jonesdev.sonar.bukkit.SonarBukkit;
import java.util.List;
@@ -41,7 +41,7 @@ public final class BukkitSonarCommand implements CommandExecutor, TabExecutor, S
// Create our own invocation source wrapper to handle messages properly
final InvocationSource invocationSource = new InvocationSource(
sender instanceof Player ? ((Player) sender).getUniqueId() : null,
SonarBukkit.INSTANCE.getBukkitAudiences().sender(sender),
Sonar.get().sender(sender),
sender::hasPermission);
// Pass the invocation source and command arguments to our command handler
handle(invocationSource, args);
@@ -0,0 +1,95 @@
/*
* Copyright (C) 2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.bukkit.fallback;
import lombok.Getter;
@Getter
public enum BukkitServerVersion {
MINECRAFT_1_7_10,
MINECRAFT_1_8,
MINECRAFT_1_8_3,
MINECRAFT_1_8_8,
MINECRAFT_1_9,
MINECRAFT_1_9_2,
MINECRAFT_1_9_4,
MINECRAFT_1_10,
MINECRAFT_1_10_1,
MINECRAFT_1_10_2,
MINECRAFT_1_11,
MINECRAFT_1_11_2,
MINECRAFT_1_12,
MINECRAFT_1_12_1,
MINECRAFT_1_12_2,
MINECRAFT_1_13,
MINECRAFT_1_13_1,
MINECRAFT_1_13_2,
MINECRAFT_1_14,
MINECRAFT_1_14_1,
MINECRAFT_1_14_2,
MINECRAFT_1_14_3,
MINECRAFT_1_14_4,
MINECRAFT_1_15,
MINECRAFT_1_15_1,
MINECRAFT_1_15_2,
MINECRAFT_1_16,
MINECRAFT_1_16_1,
MINECRAFT_1_16_2,
MINECRAFT_1_16_3,
MINECRAFT_1_16_4,
MINECRAFT_1_16_5,
MINECRAFT_1_17,
MINECRAFT_1_17_1,
MINECRAFT_1_18,
MINECRAFT_1_18_1,
MINECRAFT_1_18_2,
MINECRAFT_1_19,
MINECRAFT_1_19_1,
MINECRAFT_1_19_2,
MINECRAFT_1_19_3,
MINECRAFT_1_19_4,
MINECRAFT_1_20,
MINECRAFT_1_20_1,
MINECRAFT_1_20_2,
MINECRAFT_1_20_3,
MINECRAFT_1_20_4,
MINECRAFT_1_20_5,
MINECRAFT_1_20_6,
MINECRAFT_1_21;
static final BukkitServerVersion[] REVERSED_VALUES;
static {
REVERSED_VALUES = values().clone();
final int size = REVERSED_VALUES.length;
final int half = size / 2;
for (int i = 0; i < half; i++) {
final BukkitServerVersion temp = REVERSED_VALUES[i];
REVERSED_VALUES[i] = REVERSED_VALUES[size - 1 - i];
REVERSED_VALUES[size - 1 - i] = temp;
}
}
private final String release;
BukkitServerVersion() {
this.release = name().substring(10).replace("_", ".");
}
}
@@ -0,0 +1,250 @@
package xyz.jonesdev.sonar.bukkit.fallback;
import io.netty.buffer.ByteBufAllocator;
import io.netty.channel.*;
import io.netty.util.Attribute;
import io.netty.util.AttributeKey;
import io.netty.util.concurrent.EventExecutor;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import java.net.SocketAddress;
import java.util.HashSet;
import java.util.Set;
public final class ChannelInactiveListener extends ChannelInboundHandlerAdapter {
private final Set<ChannelInboundHandler> handlers = new HashSet<>();
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
final ProxiedChannelHandlerContext proxy = new ProxiedChannelHandlerContext(ctx);
for (final ChannelInboundHandler handler : handlers) {
try {
handler.channelInactive(proxy);
} catch (Exception exception) {
handler.exceptionCaught(proxy, exception);
}
}
super.channelInactive(ctx);
}
public void add(ChannelHandler handler) {
if (handler instanceof ChannelInboundHandler) {
handlers.add((ChannelInboundHandler) handler);
}
}
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
public static final class ProxiedChannelHandlerContext implements ChannelHandlerContext {
private final ChannelHandlerContext ctx;
// Overwrite to prevent call this method on next handler
@Override
public ChannelHandlerContext fireChannelInactive() {
return this;
}
@Override
public ChannelHandlerContext fireExceptionCaught(Throwable throwable) {
return this;
}
@Override
public Channel channel() {
return ctx.channel();
}
@Override
public EventExecutor executor() {
return ctx.executor();
}
@Override
public String name() {
return ctx.name();
}
@Override
public ChannelHandler handler() {
return ctx.handler();
}
@Override
public boolean isRemoved() {
return ctx.isRemoved();
}
@Override
public ChannelHandlerContext fireChannelRegistered() {
return ctx.fireChannelRegistered();
}
@Override
public ChannelHandlerContext fireChannelUnregistered() {
return ctx.fireChannelUnregistered();
}
@Override
public ChannelHandlerContext fireChannelActive() {
return ctx.fireChannelActive();
}
@Override
public ChannelHandlerContext fireUserEventTriggered(Object o) {
return ctx.fireUserEventTriggered(o);
}
@Override
public ChannelHandlerContext fireChannelRead(Object o) {
return ctx.fireChannelRead(o);
}
@Override
public ChannelHandlerContext fireChannelReadComplete() {
return ctx.fireChannelReadComplete();
}
@Override
public ChannelHandlerContext fireChannelWritabilityChanged() {
return ctx.fireChannelWritabilityChanged();
}
@Override
public ChannelFuture bind(SocketAddress socketAddress) {
return ctx.bind(socketAddress);
}
@Override
public ChannelFuture connect(SocketAddress socketAddress) {
return ctx.connect(socketAddress);
}
@Override
public ChannelFuture connect(SocketAddress socketAddress, SocketAddress socketAddress1) {
return ctx.connect(socketAddress, socketAddress1);
}
@Override
public ChannelFuture disconnect() {
return ctx.disconnect();
}
@Override
public ChannelFuture close() {
return ctx.close();
}
@Override
public ChannelFuture deregister() {
return ctx.deregister();
}
@Override
public ChannelFuture bind(SocketAddress socketAddress, ChannelPromise channelPromise) {
return ctx.bind(socketAddress, channelPromise);
}
@Override
public ChannelFuture connect(SocketAddress socketAddress, ChannelPromise channelPromise) {
return ctx.connect(socketAddress, channelPromise);
}
@Override
public ChannelFuture connect(SocketAddress socketAddress, SocketAddress socketAddress1, ChannelPromise channelPromise) {
return ctx.connect(socketAddress, socketAddress1, channelPromise);
}
@Override
public ChannelFuture disconnect(ChannelPromise channelPromise) {
return ctx.disconnect(channelPromise);
}
@Override
public ChannelFuture close(ChannelPromise channelPromise) {
return ctx.close(channelPromise);
}
@Override
public ChannelFuture deregister(ChannelPromise channelPromise) {
return ctx.deregister(channelPromise);
}
@Override
public ChannelHandlerContext read() {
return ctx.read();
}
@Override
public ChannelFuture write(Object o) {
return ctx.write(o);
}
@Override
public ChannelFuture write(Object o, ChannelPromise channelPromise) {
return ctx.write(o, channelPromise);
}
@Override
public ChannelHandlerContext flush() {
return ctx.flush();
}
@Override
public ChannelFuture writeAndFlush(Object o, ChannelPromise channelPromise) {
return ctx.writeAndFlush(o, channelPromise);
}
@Override
public ChannelFuture writeAndFlush(Object o) {
return ctx.writeAndFlush(o);
}
@Override
public ChannelPromise newPromise() {
return ctx.newPromise();
}
@Override
public ChannelProgressivePromise newProgressivePromise() {
return ctx.newProgressivePromise();
}
@Override
public ChannelFuture newSucceededFuture() {
return ctx.newSucceededFuture();
}
@Override
public ChannelFuture newFailedFuture(Throwable throwable) {
return ctx.newFailedFuture(throwable);
}
@Override
public ChannelPromise voidPromise() {
return ctx.voidPromise();
}
@Override
public ChannelPipeline pipeline() {
return ctx.pipeline();
}
@Override
public ByteBufAllocator alloc() {
return ctx.alloc();
}
@Override
@Deprecated
public <T> Attribute<T> attr(AttributeKey<T> attributeKey) {
return ctx.attr(attributeKey);
}
@Override
@Deprecated
public <T> boolean hasAttr(AttributeKey<T> attributeKey) {
return ctx.hasAttr(attributeKey);
}
}
}
@@ -0,0 +1,149 @@
/*
* Copyright (C) 2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.bukkit.fallback;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.CorruptedFrameException;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.FallbackInboundHandlerAdapter;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.handshake.HandshakePacket;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.login.LoginStartPacket;
import java.net.InetSocketAddress;
import java.util.Objects;
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_INACTIVE_LISTENER;
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_INBOUND_HANDLER;
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry.Direction.SERVERBOUND;
import static xyz.jonesdev.sonar.common.fallback.protocol.packets.handshake.HandshakePacket.*;
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.readVarInt;
final class FallbackBukkitInboundHandler extends FallbackInboundHandlerAdapter {
FallbackBukkitInboundHandler() {
updateRegistry(FallbackPacketRegistry.HANDSHAKE, DEFAULT_PROTOCOL_VERSION);
channelRemovalListener = (pipeline, name, handler) -> {
final var inactiveListener = (ChannelInactiveListener) pipeline.get(FALLBACK_INACTIVE_LISTENER);
if (inactiveListener != null) {
inactiveListener.add(handler);
}
};
}
private FallbackPacketRegistry.ProtocolRegistry registry;
private static final ProtocolVersion DEFAULT_PROTOCOL_VERSION = ProtocolVersion.MINECRAFT_1_7_2;
public void updateRegistry(final @NotNull FallbackPacketRegistry registry,
final @NotNull ProtocolVersion protocolVersion) {
this.registry = registry.getProtocolRegistry(SERVERBOUND, protocolVersion);
}
@Override
public void channelRead(final @NotNull ChannelHandlerContext ctx, final @NotNull Object msg) throws Exception {
if (msg instanceof ByteBuf) {
final ByteBuf byteBuf = (ByteBuf) msg;
// Reset the reader index, so we can read the content
// Make sure to store the original reader index
final int originalReaderIndex = byteBuf.readerIndex();
byteBuf.readerIndex(0);
// Don't continue if the ByteBuf isn't readable or the channel is closed
if (!ctx.channel().isActive() || !byteBuf.isReadable()) {
byteBuf.release();
return;
}
final int packetId = readVarInt(byteBuf);
final FallbackPacket packet = registry.createPacket(packetId);
// Skip the packet if it's not registered within Sonar's packet registry
if (packet == null) {
// Release the ByteBuf to avoid memory leaks
byteBuf.release();
return;
}
try {
// Try to decode the packet for the given protocol version
packet.decode(byteBuf, protocolVersion == null ? DEFAULT_PROTOCOL_VERSION : protocolVersion);
} catch (Throwable throwable) {
// Release the ByteBuf to avoid memory leaks
byteBuf.release();
throw new CorruptedFrameException("Failed to decode packet");
}
// Check if the packet still has bytes left after we decoded it
if (byteBuf.isReadable()) {
// Release the ByteBuf to avoid memory leaks
byteBuf.release();
throw new CorruptedFrameException("Could not read packet to end");
}
// Useful resources:
// https://wiki.vg/Protocol#Handshaking
// https://wiki.vg/Protocol#Login
if (packet instanceof HandshakePacket) {
final HandshakePacket handshake = (HandshakePacket) packet;
switch (handshake.getIntent()) {
case STATUS:
// We don't care about server pings or transfers; remove the handler
ctx.channel().pipeline().remove(this);
break;
case LOGIN:
case TRANSFER:
// Let the actual handler know about the handshake packet
handleHandshake(ctx.channel(), handshake.getHostname(), handshake.getProtocolVersionId());
// Be ready for the next packet (which is supposed to be a login packet)
updateRegistry(FallbackPacketRegistry.LOGIN, Objects.requireNonNull(protocolVersion));
break;
default:
throw new CorruptedFrameException("Unknown handshake intent " + handshake.getIntent());
}
} else if (packet instanceof LoginStartPacket) {
final LoginStartPacket loginStart = (LoginStartPacket) packet;
final InetSocketAddress socketAddress = (InetSocketAddress) ctx.channel().remoteAddress();
// We've done our job - deject this pipeline
ctx.channel().pipeline().remove(this);
// Let Sonar process the login packet
handleLogin(ctx.channel(), ctx, () -> {
byteBuf.readerIndex(originalReaderIndex);
ctx.fireChannelRead(byteBuf.retain());
final ChannelHandler inboundHandler = ctx.channel().pipeline().remove(FALLBACK_INBOUND_HANDLER);
if (inboundHandler != null) {
channelRemovalListener.accept(ctx.pipeline(), FALLBACK_INBOUND_HANDLER, inboundHandler);
}
}, loginStart.getUsername(), socketAddress);
// Release the ByteBuf to avoid memory leaks
byteBuf.release();
return;
}
byteBuf.readerIndex(originalReaderIndex);
ctx.fireChannelRead(byteBuf);
}
}
}
@@ -0,0 +1,272 @@
/*
* Copyright (C) 2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.bukkit.fallback;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelInitializer;
import lombok.Getter;
import lombok.experimental.UtilityClass;
import org.bukkit.Bukkit;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.ReflectiveOperationException;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.fallback.FallbackPipelines;
import xyz.jonesdev.sonar.bukkit.SonarBukkit;
import xyz.jonesdev.sonar.common.fallback.netty.FallbackInjectedChannelInitializer;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.List;
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_PACKET_DECODER;
// Check out these links if you want to see some more magic
// https://github.com/retrooper/packetevents/blob/2.0/spigot/src/main/java/io/github/retrooper/packetevents/util/SpigotReflectionUtil.java
// https://github.com/dmulloy2/ProtocolLib/blob/master/TinyProtocol/src/main/java/com/comphenix/tinyprotocol/TinyProtocol.java
// https://github.com/ViaVersion/ViaVersion/blob/master/bukkit/src/main/java/com/viaversion/viaversion/bukkit/handlers/BukkitChannelInitializer.java
@UtilityClass
public class FallbackBukkitInjector {
private final String LEGACY_NMS_PACKAGE;
private final String OBC_PACKAGE;
private final BukkitServerVersion SERVER_VERSION;
private final boolean USES_LEGACY_PACKAGING, OBFUSCATED;
private final Class<?> MINECRAFT_SERVER_CLASS;
private final Class<?> CRAFTBUKKIT_SERVER_CLASS;
private final Class<?> SERVER_CONNECTION_CLASS;
private final Object SERVER_INSTANCE;
@Getter
private boolean lateBindEnabled;
static {
SERVER_VERSION = resolveServerVersion();
USES_LEGACY_PACKAGING = SERVER_VERSION.compareTo(BukkitServerVersion.MINECRAFT_1_17) < 0;
try {
final String bukkitPackage = Bukkit.getServer().getClass().getPackage().getName();
String modifiedPackageName;
if (SERVER_VERSION.compareTo(BukkitServerVersion.MINECRAFT_1_20_5) >= 0) {
// Example: org.bukkit.craftbukkit
modifiedPackageName = bukkitPackage.split("\\.")[2];
} else {
// Example: org.bukkit.craftbukkit.v1_16_R3
modifiedPackageName = bukkitPackage.split("\\.")[3];
}
LEGACY_NMS_PACKAGE = "net.minecraft.server." + modifiedPackageName + ".";
OBC_PACKAGE = bukkitPackage + ".";
OBFUSCATED = isObfuscated();
// Minecraft classes
MINECRAFT_SERVER_CLASS = getNMSClass("server.MinecraftServer", "MinecraftServer");
SERVER_CONNECTION_CLASS = getNMSClass(OBFUSCATED ? "server.network.ServerConnection" : "server.network.ServerConnectionListener", "ServerConnection");
// CraftBukkit classes
CRAFTBUKKIT_SERVER_CLASS = getOBCClass("CraftServer");
Object minecraftServerInstance;
try {
// 1.20.5+
minecraftServerInstance = getFieldAt(MINECRAFT_SERVER_CLASS, MINECRAFT_SERVER_CLASS, 0).get(null);
} catch (Exception exception) {
minecraftServerInstance = getFieldAt(CRAFTBUKKIT_SERVER_CLASS, MINECRAFT_SERVER_CLASS, 0).get(Bukkit.getServer());
}
SERVER_INSTANCE = minecraftServerInstance;
checkLateBind();
} catch (Exception exception) {
throw new ReflectiveOperationException(exception);
}
}
private void checkLateBind() {
try {
try {
Class.forName("org.bukkit.event.server.ServerLoadEvent");
// late-bind does not exist on these versions, ignore this case
return;
} catch (ClassNotFoundException ignored) {
}
final Class<?> spigotConfiguration = Class.forName("org.spigotmc.SpigotConfig");
final Method initFieldMethod = spigotConfiguration.getDeclaredMethod("lateBind");
initFieldMethod.setAccessible(true);
initFieldMethod.invoke(null);
if ((boolean) spigotConfiguration.getField("lateBind").get(null)) {
lateBindEnabled = true;
}
} catch (java.lang.ReflectiveOperationException ignore) {
}
}
public @NotNull Field getFieldAt(final @NotNull Class<?> clazz,
final @NotNull Class<?> type,
final int index) {
int currentIndex = 0;
for (final Field field : clazz.getDeclaredFields()) {
field.setAccessible(true);
if (type.isAssignableFrom(field.getType())) {
if (currentIndex++ == index) {
return field;
}
}
}
if (clazz.getSuperclass() != null) {
return getFieldAt(clazz.getSuperclass(), type, index);
}
throw new IllegalStateException("Could not find field #" + index + " in " + clazz.getName());
}
public static @NotNull Field findField(final boolean checkSuperClass,
final @NotNull Class<?> clazz,
final String @NotNull ... types) throws NoSuchFieldException {
for (final Field field : clazz.getDeclaredFields()) {
final String fieldTypeName = field.getType().getSimpleName();
for (final String type : types) {
if (fieldTypeName.equals(type)) {
if (!Modifier.isPublic(field.getModifiers())) {
field.setAccessible(true);
}
return field;
}
}
}
if (checkSuperClass && clazz != Object.class && clazz.getSuperclass() != null) {
return findField(true, clazz.getSuperclass(), types);
}
throw new NoSuchFieldException(types[0]);
}
private @NotNull BukkitServerVersion resolveServerVersion() {
final String bukkitVersion = Bukkit.getBukkitVersion();
for (final BukkitServerVersion serverVersion : BukkitServerVersion.REVERSED_VALUES) {
if (bukkitVersion.contains(serverVersion.getRelease())) {
return serverVersion;
}
}
// Throw an error if we cannot find the server version
throw new IllegalStateException("Could not find server version " + bukkitVersion);
}
private boolean isObfuscated() {
try {
Class.forName("net.minecraft.server.network.PlayerConnection");
return true;
} catch (ClassNotFoundException exception) {
return false;
}
}
private @NotNull Class<?> getNMSClass(final String modern, final String legacy) throws ClassNotFoundException {
return Class.forName(USES_LEGACY_PACKAGING ? LEGACY_NMS_PACKAGE + legacy : "net.minecraft." + modern);
}
public @NotNull Class<?> getOBCClass(final String clazz) throws ClassNotFoundException {
return Class.forName(OBC_PACKAGE + clazz);
}
@SuppressWarnings("unchecked")
public void inject() {
try {
for (int i = 0; i < 2; i++) {
final Field field = getFieldAt(SERVER_CONNECTION_CLASS, List.class, i);
// Check if the field has the correct generic type;
// We need this field to be List<ChannelFuture>
if (!field.getGenericType().getTypeName().contains(ChannelFuture.class.getName())) {
continue;
}
final var list = (List<?>) field.get(getFieldAt(MINECRAFT_SERVER_CLASS, SERVER_CONNECTION_CLASS, 0).get(SERVER_INSTANCE));
for (final Object object : list) {
final ChannelFuture channelFuture = (ChannelFuture) object;
final List<String> names = channelFuture.channel().pipeline().names();
ChannelHandler bootstrap = null;
for (final String name : names) {
try {
final ChannelHandler handler = channelFuture.channel().pipeline().get(name);
final Field childHandlerField = handler.getClass().getDeclaredField("childHandler");
childHandlerField.setAccessible(true);
final var childHandler = (ChannelInitializer<?>) childHandlerField.get(handler);
if (childHandler != null) {
bootstrap = handler;
}
break;
} catch (Exception exception) {
// Some plugins modify this, so we better skip any exceptions
}
}
if (bootstrap == null) {
// Default to the first channel future if we couldn't find a childHandler
bootstrap = channelFuture.channel().pipeline().first();
}
final Field childHandlerField = bootstrap.getClass().getDeclaredField("childHandler");
childHandlerField.setAccessible(true);
final var originalInitializer = (ChannelInitializer<Channel>) childHandlerField.get(bootstrap);
ChannelHandler finalBootstrap = bootstrap;
// Inject our own channel initializer into the original field
// If the plugin is not fully enabled but executes the injection,
// put an initializer to close the new connection automatically.
if (!SonarBukkit.INITIALIZE_LISTENER.isDone()) {
final ChannelInitializer<Channel> initializer = new ChannelInitializer<>() {
@Override
protected void initChannel(Channel channel) {
channel.close();
}
};
childHandlerField.set(finalBootstrap, initializer);
}
SonarBukkit.INITIALIZE_LISTENER.thenAccept(v -> {
try {
childHandlerField.set(finalBootstrap, new FallbackInjectedChannelInitializer(originalInitializer,
pipeline -> {
pipeline.addAfter("splitter", FALLBACK_PACKET_DECODER, new FallbackBukkitInboundHandler());
pipeline.addFirst(FallbackPipelines.FALLBACK_INACTIVE_LISTENER, new ChannelInactiveListener());
}
));
} catch (IllegalAccessException exception) {
throw new ReflectiveOperationException(exception);
}
});
}
return;
}
} catch (Exception exception) {
Sonar.get().getLogger().error("An error occurred while injecting {}", exception);
}
}
}
@@ -0,0 +1,99 @@
/*
* Copyright (C) 2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.bukkit.listener;
import io.netty.channel.Channel;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.bukkit.fallback.FallbackBukkitInjector;
import xyz.jonesdev.sonar.common.util.FakeChannelUtil;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_BANDWIDTH;
// Taken from
// https://github.com/ViaVersion/ViaVersion/blob/master/bukkit/src/main/java/com/viaversion/viaversion/bukkit/listeners/JoinListener.java
public final class BukkitJoinListener implements Listener {
private static MethodHandle _handleMethod;
// ServerGamePacketListenerImpl aka. PlayerConnection
private static MethodHandle _listenerField;
private static MethodHandle _networkManagerField;
private static MethodHandle _channelField;
private static boolean initialized;
static {
try {
final Method handleMethod = FallbackBukkitInjector.getOBCClass("entity.CraftPlayer")
.getDeclaredMethod("getHandle");
final Field listenerField = FallbackBukkitInjector.findField(false, handleMethod.getReturnType(),
"PlayerConnection", "ServerGamePacketListenerImpl");
final Field networkManagerField = FallbackBukkitInjector.findField(true, listenerField.getType(),
"NetworkManager", "Connection");
final Field channelField = FallbackBukkitInjector.getFieldAt(networkManagerField.getType(), Channel.class, 0);
_handleMethod = MethodHandles.lookup().unreflect(handleMethod);
_listenerField = MethodHandles.lookup().unreflectGetter(listenerField);
_networkManagerField = MethodHandles.lookup().unreflectGetter(networkManagerField);
_channelField = MethodHandles.lookup().unreflectGetter(channelField);
initialized = true;
} catch (Exception exception) {
initialized = false;
}
}
@EventHandler(priority = EventPriority.HIGHEST)
public void handle(final @NotNull PlayerJoinEvent event) {
// Make sure the player is not an NPC
if (!initialized || event.getPlayer().hasMetadata("NPC")) {
return;
}
try {
final Object handle = _handleMethod.invoke(event.getPlayer());
final Object listener = _listenerField.invoke(handle);
final Object networkManager = _networkManagerField.invoke(listener);
final Channel channel = (Channel) _channelField.invoke(networkManager);
// Don't continue if the channel isn't found or if the channel is inactive
if (channel == null || !channel.isActive()) {
return;
}
// Make sure the player is not a fake player
if (FakeChannelUtil.isFakePlayer(channel)) {
return;
}
// Close the channel if Sonar's handlers are not found when the PlayerJoinEvent is called
if (channel.pipeline().context(FALLBACK_BANDWIDTH) == null) {
channel.close();
}
} catch (Throwable throwable) {
Sonar.get().getLogger().warn("Couldn't find {}: {}", event.getPlayer().getName(), throwable);
}
}
}
+9 -23
View File
@@ -1,40 +1,26 @@
plugins {
id("net.minecrell.plugin-yml.bungee") version "0.6.0"
alias(libs.plugins.pluginyml.bungee) apply true
}
apply(plugin = "net.minecrell.plugin-yml.bungee")
bungee {
name = rootProject.name
version = rootProject.version.toString().split("-")[0]
main = "xyz.jonesdev.sonar.bungee.SonarBungeePlugin"
author = "Jones Development, Sonar Contributors"
softDepends = setOf("Geyser-BungeeCord", "floodgate", "Protocolize", "ViaVersion")
softDepends = setOf("Geyser-BungeeCord", "floodgate", "Protocolize", "ViaVersion", "packetevents", "FastLogin")
}
dependencies {
implementation(project(":api"))
implementation(project(":common"))
compileOnly("net.md_5:bungeecord-proxy:master-SNAPSHOT")
testCompileOnly("net.md_5:bungeecord-proxy:master-SNAPSHOT")
compileOnly(rootProject.libs.bungeecord)
testCompileOnly(rootProject.libs.bungeecord)
// adventure platform support
implementation("net.kyori:adventure-platform-bungeecord:4.3.2")
implementation("net.kyori:adventure-platform-api:4.3.2")
implementation("net.kyori:adventure-platform-facet:4.3.2")
// adventure minimessage
compileOnly("net.kyori:adventure-text-minimessage:4.16.0")
compileOnly("net.kyori:adventure-text-serializer-gson:4.16.0")
// adventure nbt
implementation("net.kyori:adventure-nbt:4.16.0")
// Implement bStats.org for metrics
implementation("org.bstats:bstats-bungeecord:3.0.2")
// Library/dependency loading
implementation("com.alessiodp.libby:libby-bungee:2.0.0-SNAPSHOT")
implementation(rootProject.libs.adventure.platform.bungee) {
exclude(module = "adventure-nbt")
}
implementation(rootProject.libs.bstats.bungee)
implementation(rootProject.libs.libby.bungee)
}
tasks {
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023-2024 Sonar Contributors
* Copyright (C) 2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -21,31 +21,23 @@ import com.alessiodp.libby.BungeeLibraryManager;
import lombok.Getter;
import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.platform.bungeecord.BungeeAudiences;
import net.md_5.bungee.api.connection.ProxiedPlayer;
import net.md_5.bungee.api.CommandSender;
import org.bstats.bungeecord.Metrics;
import org.bstats.charts.SimplePie;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.SonarPlatform;
import xyz.jonesdev.sonar.api.logger.LoggerWrapper;
import xyz.jonesdev.sonar.bungee.command.BungeeSonarCommand;
import xyz.jonesdev.sonar.bungee.fallback.FallbackInjectionHelper;
import xyz.jonesdev.sonar.bungee.fallback.FallbackBungeeInjector;
import xyz.jonesdev.sonar.common.boot.SonarBootstrap;
import xyz.jonesdev.sonar.common.statistics.CachedBandwidthStatistics;
import java.net.InetAddress;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
@Getter
public final class SonarBungee extends SonarBootstrap<SonarBungeePlugin> {
public static SonarBungee INSTANCE;
public SonarBungee(final @NotNull SonarBungeePlugin plugin) {
super(plugin, new BungeeLibraryManager(plugin), plugin.getDataFolder(), SonarPlatform.BUNGEE);
INSTANCE = this;
super(plugin, SonarPlatform.BUNGEE, plugin.getDataFolder(), new BungeeLibraryManager(plugin));
}
/**
@@ -62,16 +54,8 @@ public final class SonarBungee extends SonarBootstrap<SonarBungeePlugin> {
}
@Override
@SuppressWarnings("deprecation")
public boolean hasTooManyAccounts(final @NotNull InetAddress inetAddress, final int limit) {
int count = 1;
for (final ProxiedPlayer player : getPlugin().getServer().getPlayers()) {
// Check if the IP address of the player is equal to the IP trying to connect
if (!Objects.equals(player.getAddress().getAddress(), inetAddress)) continue;
// Increment count of duplicated accounts
if (++count >= limit) return true;
}
return false;
public @NotNull Audience sender(final @NotNull Object object) {
return bungeeAudiences.sender((CommandSender) object);
}
/**
@@ -110,29 +94,23 @@ public final class SonarBungee extends SonarBootstrap<SonarBungeePlugin> {
() -> getConfig().getVerification().getTiming().getDisplayName()));
metrics.addCustomChart(new SimplePie("captcha",
() -> getConfig().getVerification().getMap().getTiming().getDisplayName()));
metrics.addCustomChart(new SimplePie("language",
() -> getConfig().getLanguage().getName()));
metrics.addCustomChart(new SimplePie("database_type",
() -> getConfig().getDatabase().getType().getDisplayName()));
// Register Sonar command
getPlugin().getServer().getPluginManager().registerCommand(getPlugin(), new BungeeSonarCommand());
// Register traffic service
getPlugin().getServer().getScheduler().schedule(getPlugin(), CachedBandwidthStatistics::reset,
1L, 1L, TimeUnit.SECONDS);
// Register queue service
getPlugin().getServer().getScheduler().schedule(getPlugin(), getFallback().getQueue().getPollTask(),
500L, 500L, TimeUnit.MILLISECONDS);
// Register verbose service
getPlugin().getServer().getScheduler().schedule(getPlugin(), Sonar.get().getVerboseHandler()::update,
200L, 200L, TimeUnit.MILLISECONDS);
// Make sure to inject into the server's connection handler
FallbackInjectionHelper.inject();
FallbackBungeeInjector.inject();
}
@Override
public void disable() {
// Make sure to properly shutdown bStats metrics
metrics.shutdown();
if (metrics != null) {
// Make sure to properly shutdown bStats metrics
metrics.shutdown();
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023-2024 Sonar Contributors
* Copyright (C) 2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023-2024 Sonar Contributors
* Copyright (C) 2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -22,9 +22,9 @@ import net.md_5.bungee.api.connection.ProxiedPlayer;
import net.md_5.bungee.api.plugin.Command;
import net.md_5.bungee.api.plugin.TabExecutor;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.command.InvocationSource;
import xyz.jonesdev.sonar.api.command.SonarCommand;
import xyz.jonesdev.sonar.bungee.SonarBungee;
import static java.util.Collections.emptyList;
@@ -38,7 +38,7 @@ public final class BungeeSonarCommand extends Command implements TabExecutor, So
// Create our own invocation source wrapper to handle messages properly
final InvocationSource invocationSource = new InvocationSource(
sender instanceof ProxiedPlayer ? ((ProxiedPlayer) sender).getUniqueId() : null,
SonarBungee.INSTANCE.getBungeeAudiences().sender(sender),
Sonar.get().sender(sender),
sender::hasPermission);
// Pass the invocation source and command arguments to our command handler
handle(invocationSource, args);
@@ -0,0 +1,92 @@
/*
* Copyright (C) 2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.bungee.fallback;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import net.md_5.bungee.netty.ChannelWrapper;
import net.md_5.bungee.netty.HandlerBoss;
import net.md_5.bungee.protocol.DefinedPacket;
import net.md_5.bungee.protocol.PacketWrapper;
import net.md_5.bungee.protocol.packet.Handshake;
import net.md_5.bungee.protocol.packet.LoginRequest;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.ReflectiveOperationException;
import xyz.jonesdev.sonar.common.fallback.FallbackInboundHandlerAdapter;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.net.InetSocketAddress;
final class FallbackBungeeInboundHandler extends FallbackInboundHandlerAdapter {
private static final MethodHandle CHANNEL_WRAPPER_GETTER;
static {
try {
CHANNEL_WRAPPER_GETTER = MethodHandles.privateLookupIn(HandlerBoss.class, MethodHandles.lookup())
.findGetter(HandlerBoss.class, "channel", ChannelWrapper.class);
} catch (Exception exception) {
throw new ReflectiveOperationException(exception);
}
}
@Override
public void channelRead(final @NotNull ChannelHandlerContext ctx, final @NotNull Object msg) throws Exception {
// Intercept any packets processed by BungeeCord
if (msg instanceof PacketWrapper) {
final PacketWrapper packetWrapper = (PacketWrapper) msg;
final DefinedPacket wrappedPacket = packetWrapper.packet;
// Don't handle any invalid packets
if (wrappedPacket != null) {
final Channel channel = ctx.channel();
// Intercept any handshake packet by the client
if (wrappedPacket instanceof Handshake) {
final Handshake handshake = (Handshake) wrappedPacket;
handleHandshake(channel, handshake.getHost(), handshake.getProtocolVersion());
// We don't care about server pings; remove the handler
if (handshake.getRequestedProtocol() == 1) {
ctx.channel().pipeline().remove(this);
}
}
// Intercept any server login packet by the client
else if (wrappedPacket instanceof LoginRequest) {
// Make sure to use the potentially modified, real IP
final LoginRequest loginRequest = (LoginRequest) wrappedPacket;
final HandlerBoss handlerBoss = channel.pipeline().get(HandlerBoss.class);
final ChannelWrapper channelWrapper;
try {
channelWrapper = (ChannelWrapper) CHANNEL_WRAPPER_GETTER.invokeExact(handlerBoss);
} catch (final Throwable throwable) {
throw new ReflectiveOperationException(throwable);
}
final InetSocketAddress socketAddress = (InetSocketAddress) channelWrapper.getRemoteAddress();
// We've done our job - deject this pipeline
ctx.channel().pipeline().remove(this);
// Make sure to mark this packet as released
packetWrapper.trySingleRelease();
// Let Sonar process the login packet
handleLogin(channel, ctx, () -> ctx.fireChannelRead(msg), loginRequest.getData(), socketAddress);
return;
}
}
}
// Make sure to let the server handle the rest
ctx.fireChannelRead(msg);
}
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023-2024 Sonar Contributors
* Copyright (C) 2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -22,12 +22,16 @@ import io.netty.channel.ChannelInitializer;
import lombok.experimental.UtilityClass;
import net.md_5.bungee.netty.PipelineUtils;
import sun.misc.Unsafe;
import xyz.jonesdev.sonar.api.ReflectiveOperationException;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.common.fallback.netty.FallbackInjectedChannelInitializer;
import java.lang.reflect.Field;
import static net.md_5.bungee.netty.PipelineUtils.PACKET_DECODER;
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_PACKET_HANDLER;
@UtilityClass
public class FallbackInjectionHelper {
public class FallbackBungeeInjector {
public void inject() {
try {
final Field childField = PipelineUtils.class.getField("SERVER_CHILD");
@@ -35,7 +39,9 @@ public class FallbackInjectionHelper {
// Make sure to store the original channel initializer
final ChannelInitializer<Channel> originalInitializer = PipelineUtils.SERVER_CHILD;
final ChannelInitializer<Channel> fallbackInitializer = new FallbackChannelInitializer(originalInitializer);
final ChannelInitializer<Channel> injectedInitializer = new FallbackInjectedChannelInitializer(
originalInitializer, pipeline -> pipeline.addAfter(PACKET_DECODER, FALLBACK_PACKET_HANDLER,
new FallbackBungeeInboundHandler()));
final Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe");
unsafeField.setAccessible(true);
@@ -47,9 +53,9 @@ public class FallbackInjectionHelper {
final long offset = unsafe.staticFieldOffset(childField);
// Replace the original channel initializer
unsafe.putObject(base, offset, fallbackInitializer);
unsafe.putObject(base, offset, injectedInitializer);
} catch (Exception exception) {
throw new ReflectiveOperationException(exception);
Sonar.get().getLogger().error("An error occurred while injecting {}", exception);
}
}
}
@@ -1,205 +0,0 @@
/*
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.bungee.fallback;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.CorruptedFrameException;
import net.md_5.bungee.netty.ChannelWrapper;
import net.md_5.bungee.netty.HandlerBoss;
import net.md_5.bungee.protocol.DefinedPacket;
import net.md_5.bungee.protocol.PacketWrapper;
import net.md_5.bungee.protocol.packet.Handshake;
import net.md_5.bungee.protocol.packet.LoginRequest;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.ReflectiveOperationException;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.FallbackBandwidthHandler;
import xyz.jonesdev.sonar.common.fallback.FallbackChannelHandlerAdapter;
import xyz.jonesdev.sonar.common.fallback.FallbackUserWrapper;
import xyz.jonesdev.sonar.common.statistics.GlobalSonarStatistics;
import xyz.jonesdev.sonar.common.utility.geyser.GeyserUtil;
import java.lang.reflect.Field;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.util.UUID;
import static net.md_5.bungee.netty.PipelineUtils.*;
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_BANDWIDTH;
import static xyz.jonesdev.sonar.common.fallback.FallbackUserWrapper.customDisconnect;
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer.*;
public final class FallbackChannelHandler extends FallbackChannelHandlerAdapter {
public FallbackChannelHandler(final @NotNull Channel channel) {
super(channel);
}
private static final Field CHANNEL_WRAPPER_FIELD;
static {
try {
CHANNEL_WRAPPER_FIELD = HandlerBoss.class.getDeclaredField("channel");
CHANNEL_WRAPPER_FIELD.setAccessible(true);
} catch (Exception exception) {
throw new ReflectiveOperationException(exception);
}
}
@Override
public void channelRead(final @NotNull ChannelHandlerContext ctx, final Object msg) throws Exception {
// Intercept any packets processed by BungeeCord
if (msg instanceof PacketWrapper) {
final PacketWrapper packetWrapper = (PacketWrapper) msg;
final DefinedPacket wrappedPacket = packetWrapper.packet;
// Don't handle any invalid packets
if (wrappedPacket != null) {
// Intercept any handshake packet by the client
if (wrappedPacket instanceof Handshake) {
handleHandshake((Handshake) wrappedPacket);
}
// Intercept any server login packet by the client
if (wrappedPacket instanceof LoginRequest) {
handleLogin(ctx, (LoginRequest) wrappedPacket, msg);
return;
}
}
}
// Make sure to let the server handle the rest
ctx.fireChannelRead(msg);
}
private void handleHandshake(final @NotNull Handshake handshake) throws Exception {
// Check if the player has already sent a handshake packet
if (protocolVersion != null) {
throw new CorruptedFrameException("Already sent handshake");
}
// Check if the hostname is invalid
if (handshake.getHost().isEmpty()) {
throw new CorruptedFrameException("Hostname is empty");
}
// Store the protocol version
protocolVersion = ProtocolVersion.fromId(handshake.getProtocolVersion());
// Connections from unknown protocol versions will be discarded
// as this is the safest way of handling unwanted connections
if (protocolVersion.isUnknown()) {
// Sonar does NOT support snapshots or unknown versions;
// I'll try my best to stay up-to-date!
throw new CorruptedFrameException("Unknown protocol");
}
// Hook the traffic listener
// TODO: Can we implement this in channelActive?
channel.pipeline().addFirst(FALLBACK_BANDWIDTH, FallbackBandwidthHandler.INSTANCE);
}
private void handleLogin(final @NotNull ChannelHandlerContext ctx,
final @NotNull LoginRequest loginRequest,
final @NotNull Object wrappedMessage) throws Exception {
// Check if the player has already sent a login packet
if (username != null) {
throw new CorruptedFrameException("Already logged on");
}
// Increase joins per second for the action bar verbose
GlobalSonarStatistics.countLogin();
// Store the username
username = loginRequest.getData();
// Make sure to use the potentially modified, real IP
final HandlerBoss handlerBoss = channel.pipeline().get(HandlerBoss.class);
final ChannelWrapper channelWrapper = (ChannelWrapper) CHANNEL_WRAPPER_FIELD.get(handlerBoss);
final InetSocketAddress socketAddress = (InetSocketAddress) channelWrapper.getRemoteAddress();
inetAddress = socketAddress.getAddress();
// Check the blacklist here since we cannot let the player "ghost join"
if (FALLBACK.getBlacklist().asMap().containsKey(inetAddress)) {
customDisconnect(channel, protocolVersion, blacklisted, PACKET_ENCODER, BOSS_HANDLER);
return;
}
// Don't continue the verification process if the verification is disabled
if (!Sonar.get().getFallback().shouldVerifyNewPlayers()) {
initialLogin(ctx, wrappedMessage, PACKET_ENCODER, BOSS_HANDLER);
return;
}
// Completely skip Geyser connections
if (GeyserUtil.isGeyserConnection(channel, socketAddress)) {
initialLogin(ctx, wrappedMessage, PACKET_ENCODER, BOSS_HANDLER);
return;
}
// Check if the player is already queued since we don't want bots to flood the queue
if (FALLBACK.getQueue().getQueuedPlayers().containsKey(inetAddress)) {
customDisconnect(channel, protocolVersion, alreadyQueued, PACKET_ENCODER, BOSS_HANDLER);
return;
}
// Check if Fallback is already verifying a player
// → is another player with the same IP address connected to Fallback?
if (FALLBACK.getConnected().containsKey(loginRequest.getData())
|| FALLBACK.getConnected().containsValue(inetAddress)) {
customDisconnect(channel, protocolVersion, alreadyVerifying, PACKET_ENCODER, BOSS_HANDLER);
return;
}
// Check if the protocol ID of the player is not allowed to enter the server
if (Sonar.get().getConfig().getVerification().getBlacklistedProtocols()
.contains(protocolVersion.getProtocol())) {
customDisconnect(channel, protocolVersion, protocolBlacklisted, PACKET_ENCODER, BOSS_HANDLER);
return;
}
// Make sure we actually have to verify the player
final String offlineUUIDString = "OfflinePlayer:" + loginRequest.getData();
final UUID offlineUUID = UUID.nameUUIDFromBytes(offlineUUIDString.getBytes(StandardCharsets.UTF_8));
if (Sonar.get().getVerifiedPlayerController().has(inetAddress, offlineUUID)) {
initialLogin(ctx, wrappedMessage, PACKET_ENCODER, BOSS_HANDLER);
return;
}
// Check if the IP address is currently being rate-limited
if (!FALLBACK.getRatelimiter().attempt(inetAddress)) {
customDisconnect(channel, protocolVersion, reconnectedTooFast, PACKET_ENCODER, BOSS_HANDLER);
return;
}
// Check if the protocol ID of the player is allowed to bypass verification
if (Sonar.get().getConfig().getVerification().getWhitelistedProtocols()
.contains(protocolVersion.getProtocol())) {
initialLogin(ctx, wrappedMessage, PACKET_ENCODER, BOSS_HANDLER);
return;
}
// Queue the connection for further processing
FALLBACK.getQueue().queue(inetAddress, () -> channel.eventLoop().execute(() -> {
// Check if the username matches the valid name regex to prevent
// UTF-16 names or other types of exploits
if (!Sonar.get().getConfig().getVerification().getValidNameRegex()
.matcher(loginRequest.getData()).matches()) {
customDisconnect(channel, protocolVersion, invalidUsername, PACKET_ENCODER, BOSS_HANDLER);
return;
}
// Create an instance for the Fallback connection
user = new FallbackUserWrapper(channel, inetAddress, protocolVersion);
// Let the verification handler take over the channel
user.hijack(loginRequest.getData(), offlineUUID, PACKET_ENCODER, PACKET_DECODER, TIMEOUT_HANDLER, BOSS_HANDLER);
}));
}
}
@@ -1,58 +0,0 @@
/*
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.bungee.fallback;
import io.netty.channel.Channel;
import io.netty.channel.ChannelInitializer;
import lombok.RequiredArgsConstructor;
import xyz.jonesdev.sonar.api.ReflectiveOperationException;
import java.lang.reflect.Method;
import static net.md_5.bungee.netty.PipelineUtils.PACKET_DECODER;
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_HANDLER;
@RequiredArgsConstructor
public final class FallbackChannelInitializer extends ChannelInitializer<Channel> {
private static final Method INIT_CHANNEL_METHOD;
static {
try {
INIT_CHANNEL_METHOD = ChannelInitializer.class.getDeclaredMethod("initChannel", Channel.class);
INIT_CHANNEL_METHOD.setAccessible(true);
} catch (Throwable throwable) {
throw new ReflectiveOperationException(throwable);
}
}
private final ChannelInitializer<Channel> originalChannelInitializer;
@Override
protected void initChannel(final Channel channel) throws Exception {
// Invoke the original method
try {
INIT_CHANNEL_METHOD.invoke(originalChannelInitializer, channel);
} catch (Throwable throwable) {
throw new ReflectiveOperationException(throwable);
}
if (channel.pipeline().get(PACKET_DECODER) != null) {
channel.pipeline().addAfter(PACKET_DECODER, FALLBACK_HANDLER, new FallbackChannelHandler(channel));
}
}
}
+10
View File
@@ -0,0 +1,10 @@
dependencies {
compileOnly(project(":api"))
compileOnly(rootProject.libs.imagefilters)
testCompileOnly(project(":api"))
testCompileOnly(rootProject.libs.imagefilters)
}
java.sourceCompatibility = JavaVersion.VERSION_11
java.targetCompatibility = JavaVersion.VERSION_11
@@ -0,0 +1,169 @@
/*
* Copyright (C) 2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.captcha;
import com.jhlabs.image.FBMFilter;
import com.jhlabs.image.SmearFilter;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import xyz.jonesdev.sonar.api.fallback.captcha.CaptchaGenerator;
import xyz.jonesdev.sonar.captcha.filters.CurvesOverlayFilter;
import xyz.jonesdev.sonar.captcha.filters.NoiseOverlayFilter;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.font.FontRenderContext;
import java.awt.font.GlyphVector;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.Random;
import static java.awt.image.BufferedImage.TYPE_3BYTE_BGR;
import static xyz.jonesdev.sonar.captcha.StandardTTFFontProvider.FONTS;
import static xyz.jonesdev.sonar.captcha.StandardTTFFontProvider.STANDARD_FONT_SIZE;
@Getter
@RequiredArgsConstructor
public final class StandardCaptchaGenerator implements CaptchaGenerator {
private static final CurvesOverlayFilter CURVES = new CurvesOverlayFilter(3);
//private static final CircleInverseFilter CIRCLES = new CircleInverseFilter(1, 30, 10);
private static final NoiseOverlayFilter NOISE = new NoiseOverlayFilter(1, 20);
private static final SmearFilter SMEAR = new SmearFilter();
private static final FBMFilter FBM = new FBMFilter();
private static final Random RANDOM = new Random();
private static final Color[] COLORS = new Color[4];
private static final float[] COLOR_FRACTIONS = new float[COLORS.length];
static {
FBM.setAmount(0.6f);
FBM.setScale(15);
SMEAR.setShape(SmearFilter.SQUARES);
SMEAR.setDensity(0.1f);
SMEAR.setDistance(0);
SMEAR.setMix(0.35f);
// Create fractions based on the number of colors
for (int i = 0; i < COLOR_FRACTIONS.length; i++) {
COLOR_FRACTIONS[i] = (float) i / (COLOR_FRACTIONS.length - 1);
}
}
private final int width, height;
private final @Nullable InputStream background;
private BufferedImage backgroundImage;
@Override
public @NotNull BufferedImage createImage(final char[] answer) {
final BufferedImage image = createBackgroundImage();
final Graphics2D graphics = image.createGraphics();
graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
// Draw characters and other effects on the image
applyRandomColorGradient(graphics);
drawCharacters(graphics, answer);
CURVES.transform(image, graphics);
NOISE.transform(image);
//SMEAR.filter(image, image);
//CIRCLES.transform(image); // TODO: check if the text is still easy to read after this
// Make sure to dispose the graphics instance
graphics.dispose();
return image;
}
private @NotNull BufferedImage createBackgroundImage() {
if (background == null) {
final BufferedImage image = new BufferedImage(width, height, TYPE_3BYTE_BGR);
// Fill the entire image with a noise texture
return FBM.filter(image, image);
}
try {
return backgroundImage == null ? backgroundImage = ImageIO.read(background) : backgroundImage;
} catch (IOException exception) {
throw new IllegalStateException("Could not read background image", exception);
}
}
private void applyRandomColorGradient(final @NotNull Graphics2D graphics) {
// Randomize the colors for the gradient effect
for (int i = 0; i < COLORS.length; i++) {
final float random = 0.9f + RANDOM.nextFloat() * 0.1f;
COLORS[i] = Color.getHSBColor(RANDOM.nextFloat(), random, random);
}
// Apply the random gradient effect
graphics.setPaint(new LinearGradientPaint(0, 0, width, height,
COLOR_FRACTIONS, COLORS, MultipleGradientPaint.CycleMethod.REFLECT));
}
private void drawCharacters(final @NotNull Graphics2D graphics,
final char @NotNull [] answer) {
final FontRenderContext ctx = graphics.getFontRenderContext();
final GlyphVector[] glyphs = new GlyphVector[answer.length];
for (int i = 0; i < answer.length; i++) {
// Create a glyph vector for the character with a random font
final Font font = FONTS[RANDOM.nextInt(FONTS.length)];
glyphs[i] = font.createGlyphVector(ctx, String.valueOf(answer[i]));
}
final double scalingXY = 5 - Math.min(answer.length, 5) * 0.65;
// Calculate first X and Y positions
final double totalWidth = Arrays.stream(glyphs)
.mapToDouble(glyph -> glyph.getLogicalBounds().getWidth() * scalingXY - 1)
.sum();
double beginX = Math.max(Math.min(width / 2D - totalWidth / 2D, totalWidth), 0);
double beginY = (height + STANDARD_FONT_SIZE / 2D) / 2D + scalingXY;
// Draw each glyph one by one
for (final GlyphVector glyph : glyphs) {
final AffineTransform transformation = AffineTransform.getTranslateInstance(beginX, beginY);
// Shear the glyph by a random amount
final double shearXY = Math.sin(beginX + beginY) / 6;
transformation.shear(shearXY, shearXY);
// Scale the glyph to perfectly fit the image
transformation.scale(scalingXY, scalingXY);
// Draw the glyph to the buffered image
final Shape transformedShape = transformation.createTransformedShape(glyph.getOutline());
graphics.fill(transformedShape);
// Draw a random outline around the glyph
if (RANDOM.nextFloat() < 0.25f) {
createGlyphOutline(graphics, transformedShape);
}
// Make sure the next glyph isn't drawn at the same position
beginX += glyph.getVisualBounds().getWidth() * scalingXY + 2;
beginY += -10 + RANDOM.nextFloat() * 20;
}
}
private void createGlyphOutline(final @NotNull Graphics2D graphics, final @NotNull Shape shape) {
final float txy = 1.25f + RANDOM.nextFloat();
final float width = 1 + RANDOM.nextFloat();
// Create a randomly translated and stroked shape based on the original glyph shape
final AffineTransform translation = AffineTransform.getTranslateInstance(txy, txy);
final Shape strokedShape = new BasicStroke(width).createStrokedShape(shape);
graphics.fill(translation.createTransformedShape(strokedShape));
}
}
@@ -0,0 +1,49 @@
/*
* Copyright (C) 2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.captcha;
import lombok.experimental.UtilityClass;
import org.jetbrains.annotations.NotNull;
import java.awt.*;
import java.io.InputStream;
import java.util.Objects;
@UtilityClass
class StandardTTFFontProvider {
private static final String[] FONT_NAMES = {"Kingthings_Trypewriter_2"};
static final Font[] FONTS = new Font[FONT_NAMES.length];
static final int STANDARD_FONT_SIZE = 25;
static {
for (int i = 0; i < FONT_NAMES.length; i++) {
FONTS[i] = loadFont(String.format("/assets/fonts/%s.ttf", FONT_NAMES[i]));
}
}
private static Font loadFont(final @NotNull String path) {
try (final InputStream inputStream = StandardCaptchaGenerator.class.getResourceAsStream(path)) {
// Load the font from the TTF file
final Font customFont = Font.createFont(Font.TRUETYPE_FONT, Objects.requireNonNull(inputStream));
// Set the font size and style
return customFont.deriveFont(Font.PLAIN, STANDARD_FONT_SIZE);
} catch (Exception exception) {
throw new IllegalStateException(exception);
}
}
}
@@ -0,0 +1,86 @@
/*
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.captcha.filters;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.NotNull;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.util.Random;
@Getter
@RequiredArgsConstructor
public final class CircleInverseFilter {
private final int amount, minRadius, maxRadiusExpansion;
private static final Random RANDOM = new Random();
public void transform(final @NotNull BufferedImage image) {
final int minX = image.getWidth() / 4;
final int minY = image.getHeight() / 4;
for (int i = 0; i < amount; i++) {
// Generate random position and radius for the circle
final float centerX = minX + RANDOM.nextInt(image.getWidth() - minX * 2);
final float centerY = minY + RANDOM.nextInt(image.getHeight() - minY * 2);
final int radius = minRadius + RANDOM.nextInt(maxRadiusExpansion);
final Circle circle = new Circle(centerX, centerY, radius);
// Draw inverted circle
_addInvertedCircle(image, circle);
}
}
private static void _addInvertedCircle(final @NotNull BufferedImage bufferedImage,
final @NotNull Circle circle) {
for (int x = 0; x < bufferedImage.getWidth(); x++) {
for (int y = 0; y < bufferedImage.getHeight(); y++) {
if (isWithinCircle(x, y, circle, circle.radius)) {
// Invert colors within the circle
final int rgb = bufferedImage.getRGB(x, y);
final int invertedRGB = invertColorAndFilter(rgb);
bufferedImage.setRGB(x, y, invertedRGB);
}
}
}
}
private static boolean isWithinCircle(final float x, final float y, final @NotNull Circle circle, int radius) {
final float distanceX = x - circle.centerX;
final float distanceY = y - circle.centerY;
return distanceX * distanceX + distanceY * distanceY <= radius * radius;
}
@Getter
@RequiredArgsConstructor
static final class Circle {
private final float centerX;
private final float centerY;
private final int radius;
}
private static int invertColorAndFilter(final int rgb) {
final Color color = new Color(rgb);
final int red = Math.max(Color.LIGHT_GRAY.getRed() - color.getRed(), 0);
final int green = Math.max(Color.LIGHT_GRAY.getGreen() - color.getGreen(), 0);
final int blue = Math.max(Color.LIGHT_GRAY.getBlue() - color.getBlue(), 0);
return new Color(red, green, blue).getRGB();
}
}
@@ -0,0 +1,62 @@
/*
* Copyright (C) 2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.captcha.filters;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.NotNull;
import java.awt.*;
import java.awt.geom.CubicCurve2D;
import java.awt.image.BufferedImage;
import java.util.Random;
@Getter
@RequiredArgsConstructor
public final class CurvesOverlayFilter {
private final int amount;
private static final Random RANDOM = new Random();
public void transform(final @NotNull BufferedImage image,
final @NotNull Graphics2D graphics) {
// Randomize the stroke width
graphics.setStroke(new BasicStroke(1 + RANDOM.nextFloat()));
final int halfWidth = image.getWidth() / 2;
for (int i = 0; i < amount; ++i) {
final float randomX = image.getWidth() * RANDOM.nextFloat();
final float randomY = image.getHeight() * RANDOM.nextFloat();
final float amplitude = 6.2831855f * (RANDOM.nextFloat() - 0.5f);
final float sin = (float) Math.sin(amplitude) * halfWidth;
final float cos = (float) Math.cos(amplitude) * halfWidth;
final float x1 = randomX - cos;
final float y1 = randomY - sin;
final float x2 = randomX + cos;
final float y2 = randomY + sin;
// Control points for the cubic curve
final float ctrlX1 = randomX + sin / 2;
final float ctrlY1 = randomY - cos / 2;
final float ctrlX2 = randomX - sin / 2;
final float ctrlY2 = randomY + cos / 2;
// Draw a cubic curve instead of a straight line
graphics.draw(new CubicCurve2D.Float(x1, y1, ctrlX1, ctrlY1, ctrlX2, ctrlY2, x2, y2));
}
}
}
@@ -0,0 +1,51 @@
/*
* Copyright (C) 2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.captcha.filters;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.NotNull;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.util.Random;
@Getter
@RequiredArgsConstructor
public final class GridOverlayFilter {
private final int amount, lineWidth, randomOffset;
private static final Random RANDOM = new Random();
public void transform(final @NotNull BufferedImage image,
final @NotNull Graphics2D graphics) {
final int spacingX = image.getWidth() / amount + lineWidth;
final int spacingY = image.getHeight() / amount + lineWidth;
int currentX = spacingX / 2;
int currentY = spacingY / 2;
for (int i = 0; i < amount; i++) {
graphics.setStroke(new BasicStroke(lineWidth * 0.5f + lineWidth * RANDOM.nextFloat()));
graphics.drawLine(currentX, 0, currentX + RANDOM.nextInt(randomOffset), image.getHeight());
graphics.drawLine(0, currentY, image.getWidth(), currentY + RANDOM.nextInt(randomOffset));
currentX += spacingX + lineWidth;
currentY += spacingY + lineWidth;
}
}
}
@@ -0,0 +1,55 @@
/*
* Copyright (C) 2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.captcha.filters;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.NotNull;
import java.awt.image.BufferedImage;
import java.util.Random;
@Getter
@RequiredArgsConstructor
public final class NoiseOverlayFilter {
private final float density, amount;
private static final Random RANDOM = new Random();
private int randomGaussian(int x) {
x += (int) (RANDOM.nextGaussian() * amount);
return Math.max(Math.min(x, 0xff), 0);
}
public void transform(final @NotNull BufferedImage image) {
for (int x = 0; x < image.getWidth(); x++) {
for (int y = 0; y < image.getHeight(); y++) {
if (RANDOM.nextFloat() <= density) {
final int rgb = image.getRGB(x, y);
int r = (rgb >> 16) & 0xff;
int g = (rgb >> 8) & 0xff;
int b = rgb & 0xff;
r = randomGaussian(r);
g = randomGaussian(g);
b = randomGaussian(b);
image.setRGB(x, y, (r << 16) | (g << 8) | b);
}
}
}
}
}
+51
View File
@@ -0,0 +1,51 @@
/*
* Copyright (C) 2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import lombok.experimental.UtilityClass;
import xyz.jonesdev.sonar.captcha.StandardCaptchaGenerator;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.Random;
@UtilityClass
public class Main {
public void main(final String... args) throws IOException {
final Random random = new Random();
final StandardCaptchaGenerator standardCaptchaGenerator = new StandardCaptchaGenerator(128, 128, null);
final long start = System.currentTimeMillis();
final char[] dictionary = {'a', 'b', 'c', 'd', 'e', 'f'/*, 'g'*/, 'h'/*, 'i'*/, 'j',
'k', 'm', 'n', 'o', 'p'/*, 'q'*/, 'r', 's', 't', 'u'/*, 'v', 'w'*/, 'x', 'y', 'z'};
// Generate image
final int amount = 1;
for (int i = 0; i < amount; i++) {
final char[] answer = new char[3 + random.nextInt(2)];
for (int j = 0; j < answer.length; j++) {
answer[j] = dictionary[random.nextInt(dictionary.length)];
}
final BufferedImage bufferedImage = standardCaptchaGenerator.createImage(answer);
// Save image
ImageIO.write(bufferedImage, "png", new File(i + ".png"));
}
System.out.println("Took " + (System.currentTimeMillis() - start) + "ms to generate " + amount + " image(s)");
}
}
+2 -6
View File
@@ -1,11 +1,7 @@
dependencies {
compileOnly(project(":api"))
// adventure nbt for packets
compileOnly("net.kyori:adventure-nbt:4.16.0")
// CAPTCHA image generation
implementation("xyz.jonesdev.capja:capja:0.1.2")
implementation(project(":captcha"))
compileOnly(rootProject.libs.adventure.nbt)
}
java.sourceCompatibility = JavaVersion.VERSION_11
@@ -21,12 +21,11 @@ import com.alessiodp.libby.Library;
import com.alessiodp.libby.LibraryManager;
import lombok.experimental.UtilityClass;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.SonarPlatform;
@UtilityClass
class LibraryLoader {
void loadLibraries(final @NotNull LibraryManager libraryManager) {
void loadLibraries(final @NotNull LibraryManager libraryManager, final @NotNull SonarPlatform platform) {
libraryManager.addMavenCentral();
libraryManager.addJitPack();
libraryManager.loadLibraries(
@@ -41,12 +40,12 @@ class LibraryLoader {
Library.builder()
.groupId("com{}google{}code{}gson")
.artifactId("gson")
.version("2.10.1")
.version("2.11.0")
.relocate("com{}google{}gson", "xyz{}jonesdev{}sonar{}libs{}gson")
.build(),
// Caffeine
Library.builder()
.groupId("com.github.ben-manes.caffeine")
.groupId("com{}github{}ben-manes{}caffeine")
.artifactId("caffeine")
.version("3.1.8")
.relocate("com{}github{}benmanes{}caffeine", "xyz{}jonesdev{}sonar{}libs{}caffeine")
@@ -58,35 +57,34 @@ class LibraryLoader {
.version("6.1")
.relocate("com{}j256{}ormlite", "xyz{}jonesdev{}sonar{}libs{}ormlite")
.build(),
// MySQL driver
// JHLabs image filters
Library.builder()
.groupId("com{}mysql")
.artifactId("mysql-connector-j")
.version("8.3.0")
.relocate("com{}mysql", "xyz{}jonesdev{}sonar{}libs{}mysql")
.build(),
// MariaDB driver
Library.builder()
.groupId("org{}mariadb{}jdbc")
.artifactId("mariadb-java-client")
.version("3.3.3")
.relocate("org{}mariadb", "xyz{}jonesdev{}sonar{}libs{}mariadb")
.groupId("com{}jhlabs")
.artifactId("filters")
.version("2.0.235-1")
.relocate("com{}jhlabs", "xyz{}jonesdev{}sonar{}libs{}jhlabs")
.build()
);
// Only load adventure dependencies if needed
if (Sonar.get().getPlatform() != SonarPlatform.VELOCITY) {
// Only load adventure if not on Velocity
if (platform != SonarPlatform.VELOCITY) {
libraryManager.loadLibraries(
Library.builder()
.groupId("net{}kyori")
.artifactId("adventure-text-minimessage")
.version("4.16.0")
.version("4.17.0")
.relocate("net{}kyori", "xyz{}jonesdev{}sonar{}libs{}kyori")
.build(),
Library.builder()
.groupId("net{}kyori")
.artifactId("adventure-text-serializer-gson")
.version("4.16.0")
.version("4.17.0")
.relocate("net{}kyori", "xyz{}jonesdev{}sonar{}libs{}kyori")
.build(),
Library.builder()
.groupId("net{}kyori")
.artifactId("adventure-nbt")
.version("4.17.0")
.relocate("net{}kyori", "xyz{}jonesdev{}sonar{}libs{}kyori")
.build()
);
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023-2024 Sonar Contributors
* Copyright (C) 2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -19,6 +19,7 @@ package xyz.jonesdev.sonar.common.boot;
import com.alessiodp.libby.LibraryManager;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.Ticker;
import com.j256.ormlite.logger.Level;
import com.j256.ormlite.logger.Logger;
import lombok.Getter;
@@ -35,10 +36,13 @@ import xyz.jonesdev.sonar.api.statistics.SonarStatistics;
import xyz.jonesdev.sonar.api.timer.SystemTimer;
import xyz.jonesdev.sonar.api.verbose.Notification;
import xyz.jonesdev.sonar.api.verbose.Verbose;
import xyz.jonesdev.sonar.captcha.StandardCaptchaGenerator;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer;
import xyz.jonesdev.sonar.common.service.SonarServiceManager;
import xyz.jonesdev.sonar.common.statistics.GlobalSonarStatistics;
import xyz.jonesdev.sonar.common.subcommand.SubcommandRegistryHolder;
import xyz.jonesdev.sonar.common.subcommands.*;
import xyz.jonesdev.sonar.common.update.UpdateChecker;
import xyz.jonesdev.sonar.common.util.ProtocolUtil;
import java.io.File;
import java.time.Duration;
@@ -53,7 +57,6 @@ public abstract class SonarBootstrap<T> implements Sonar {
private Notification notificationHandler;
private SonarConfiguration config;
private VerifiedPlayerController verifiedPlayerController;
private File dataDirectory;
private final LibraryManager libraryManager;
private final SonarStatistics statistics;
private final SonarPlatform platform;
@@ -61,32 +64,41 @@ public abstract class SonarBootstrap<T> implements Sonar {
private final SystemTimer launchTimer = new SystemTimer();
public SonarBootstrap(final @NotNull T plugin,
final LibraryManager libraryManager,
final File dataDirectory,
final SonarPlatform platform) {
final @NotNull SonarPlatform platform,
final @NotNull File dataDirectory,
final @NotNull LibraryManager libraryManager) {
// Load all libraries before anything else
LibraryLoader.loadLibraries(libraryManager, platform);
this.libraryManager = libraryManager;
// Check if Netty is up to date
ProtocolUtil.checkNettyVersion();
// Set the Sonar API
SonarSupplier.set(this);
// Store our library manager for the API
this.libraryManager = libraryManager;
// Set the plugin instance
this.plugin = plugin;
this.dataDirectory = dataDirectory;
this.platform = platform;
// Load all libraries before anything else
LibraryLoader.loadLibraries(libraryManager);
// Load the rest of the components
this.statistics = new GlobalSonarStatistics();
this.verboseHandler = new Verbose();
this.notificationHandler = new Notification();
this.config = new SonarConfiguration(dataDirectory);
this.subcommandRegistry = new SubcommandRegistryHolder();
// Register all subcommands
this.subcommandRegistry = new SubcommandRegistry();
this.subcommandRegistry.register(
new BlacklistCommand(),
new VerifiedCommand(),
new StatisticsCommand(),
new VerboseCommand(),
new ReloadCommand(),
new DumpCommand(),
new NotifyCommand());
// Hide unnecessary debug information
Logger.setGlobalLogLevel(Level.WARNING);
}
public final void initialize() {
// Check if the branch is not the main branch to warn about unstable versions
if (!getVersion().isOnMainBranch()) {
if (!getVersion().getGitBranch().equals("main")) {
getLogger().warn("You are currently using an unofficial experimental branch.");
getLogger().warn("It is highly recommended to use the latest stable release of Sonar:");
getLogger().warn("https://github.com/jonesdevelopment/sonar/releases");
@@ -105,6 +117,10 @@ public abstract class SonarBootstrap<T> implements Sonar {
// Run the per-platform initialization method
enable();
// Start threads
getLogger().info("Starting all managed threads...");
SonarServiceManager.start();
// Done
getLogger().info("Done ({}s)!", launchTimer);
} catch (Throwable throwable) {
@@ -114,8 +130,10 @@ public abstract class SonarBootstrap<T> implements Sonar {
return; // Do not check for updates if the launch failed
}
// Check if a new version has been released
UpdateChecker.checkForUpdates();
// Check if a new version has been released if enabled in the configuration
if (Sonar.get().getConfig().getGeneralConfig().getBoolean("general.check-for-updates")) {
UpdateChecker.checkForUpdates();
}
}
public abstract void enable();
@@ -130,15 +148,29 @@ public abstract class SonarBootstrap<T> implements Sonar {
getLogger().warn("Reloading after changing the database type is not recommended as it may cause data loss.");
}
// Update the CAPTCHA generator if necessary
if (getConfig().getVerification().getMap().getTiming() == SonarConfiguration.Verification.Timing.NEVER) {
getFallback().setCaptchaGenerator(null);
} else if (getFallback().getCaptchaGenerator() == null
|| getFallback().getCaptchaGenerator() instanceof StandardCaptchaGenerator) {
getFallback().setCaptchaGenerator(new StandardCaptchaGenerator(128, 128,
Sonar.get().getConfig().getVerification().getMap().getBackgroundImage()));
} else {
getLogger().info("Custom CAPTCHA generator detected, skipping reinitialization.");
}
// Prepare cached packets
getLogger().info("Taking cached snapshots of all packets...");
FallbackPreparer.prepare();
// Update ratelimiter caches
getFallback().getRatelimiter().setAttemptCache(Caffeine.newBuilder()
.expireAfterWrite(Duration.ofMillis(getConfig().getVerification().getReconnectDelay()))
.ticker(Ticker.systemTicker())
.build());
getFallback().getRatelimiter().setFailCountCache(Caffeine.newBuilder()
.expireAfterWrite(Duration.ofMillis(getConfig().getVerification().getRememberTime()))
.ticker(Ticker.systemTicker())
.build());
// Update blacklist cache
@@ -147,9 +179,10 @@ public abstract class SonarBootstrap<T> implements Sonar {
// Make sure the blacklist is only set when we need it to prevent data loss
if (!blacklistExists // Make sure we create a new cache if it doesn't exist yet
|| getFallback().getBlacklistTime() != blacklistTime) {
// Create new cache with the configured blacklist time
// Create a new cache with the configured blacklist time
getFallback().setBlacklist(Caffeine.newBuilder()
.expireAfterWrite(Duration.ofMillis(getConfig().getVerification().getBlacklistTime()))
.ticker(Ticker.systemTicker())
.build());
// Store the new blacklist time, so we don't have to reset the blacklist every reload
getFallback().setBlacklistTime(blacklistTime);
@@ -164,14 +197,18 @@ public abstract class SonarBootstrap<T> implements Sonar {
// Close the old connection first
verifiedPlayerController.close();
}
verifiedPlayerController = new VerifiedPlayerController();
verifiedPlayerController = new VerifiedPlayerController(libraryManager);
}
public final void shutdown() {
// Initialize shutdown process
// Initialize the shutdown process
getLogger().info("Starting shutdown process...");
// Close database connection
verifiedPlayerController.close();
// Interrupt threads
SonarServiceManager.stop();
// Close database connection if present
if (verifiedPlayerController != null) {
verifiedPlayerController.close();
}
// Run the per-platform disable method
disable();
// Thank the user for using Sonar
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023-2024 Sonar Contributors
* Copyright (C) 2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -39,7 +39,7 @@ public final class FallbackBandwidthHandler extends ChannelDuplexHandler {
final @NotNull Object msg) throws Exception {
// We can only get the size of a message if it's a ByteBuf
if (msg instanceof ByteBuf) {
// Increment the incoming traffic by the amount of readable bytes
// Increment the incoming traffic by the number of readable bytes
INCOMING.increment(((ByteBuf) msg).readableBytes());
}
// Make sure to let the server handle the rest
@@ -52,7 +52,7 @@ public final class FallbackBandwidthHandler extends ChannelDuplexHandler {
final ChannelPromise promise) throws Exception {
// We can only get the size of a message if it's a ByteBuf
if (msg instanceof ByteBuf) {
// Increment the outgoing traffic by the amount of readable bytes
// Increment the outgoing traffic by the number of readable bytes
OUTGOING.increment(((ByteBuf) msg).readableBytes());
}
// Make sure to let the server handle the rest
@@ -1,113 +0,0 @@
/*
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.fallback.Fallback;
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer;
import xyz.jonesdev.sonar.common.statistics.GlobalSonarStatistics;
import java.net.InetAddress;
import static xyz.jonesdev.sonar.common.fallback.FallbackUserWrapper.customDisconnect;
import static xyz.jonesdev.sonar.common.fallback.FallbackUserWrapper.deject;
@RequiredArgsConstructor
public class FallbackChannelHandlerAdapter extends ChannelInboundHandlerAdapter {
protected final Channel channel;
protected @Nullable String username;
protected InetAddress inetAddress;
protected ProtocolVersion protocolVersion;
protected @Nullable FallbackUser user;
protected static final Fallback FALLBACK = Sonar.get().getFallback();
@Override
public final void channelActive(final @NotNull ChannelHandlerContext ctx) {
// Increase connections per second for the action bar verbose
GlobalSonarStatistics.countConnection();
// Make sure to let the server handle the rest
ctx.fireChannelActive();
}
@Override
public final void channelInactive(final @NotNull ChannelHandlerContext ctx) {
// The player can disconnect without sending the login packet first
if (username != null) {
// Remove the username from the connected players
FALLBACK.getConnected().remove(username);
}
// The player cannot be in the queue if the IP address is invalid
if (inetAddress != null) {
// Remove the IP address from the queue
FALLBACK.getQueue().remove(inetAddress);
}
// The player cannot be in the verification if the user object doesn't exist,
// or if the user failed/succeeded the bot verification process.
if (user != null && user.getState().canReceivePackets()) {
// Mark the verification attempt as failed
user.fail("disconnected during verification");
}
// Make sure to let the server handle the rest
ctx.fireChannelInactive();
}
// We can override the default exceptionCaught method since this handler
// will run before the connection knows that there has been an error.
// Additionally, this will also run after our custom decoder.
@Override
public final void exceptionCaught(final @NotNull ChannelHandlerContext ctx,
final @NotNull Throwable cause) throws Exception {
// Simply close the channel if we encounter any errors.
ctx.close();
}
/**
* Executes the maximum accounts per IP limit check before letting the player join
*
* @param ctx Forwarded channel handler context
* @param packet Login packet sent by the client
*/
protected void initialLogin(final @NotNull ChannelHandlerContext ctx,
final @NotNull Object packet,
final @NotNull String encoder,
final @NotNull String boss) {
final int maxOnlinePerIp = Sonar.get().getConfig().getMaxOnlinePerIp();
// Skip the maximum online per IP check if it's disabled in the configuration
if (maxOnlinePerIp > 0) {
// Check if the number of online players using the same IP address as
// the connecting player is greater than the configured amount
if (Sonar.get().hasTooManyAccounts(inetAddress, maxOnlinePerIp)) {
customDisconnect(channel, protocolVersion, FallbackPreparer.tooManyOnlinePerIP, encoder, boss);
return;
}
}
ctx.fireChannelRead(packet);
// Deject the channel since we don't need it anymore
deject(channel.pipeline());
}
}
@@ -0,0 +1,74 @@
/*
* Copyright (C) 2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelPipeline;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.common.statistics.GlobalSonarStatistics;
import java.net.InetAddress;
import java.util.function.Consumer;
@RequiredArgsConstructor
public final class FallbackInboundHandler extends ChannelInboundHandlerAdapter {
private final Consumer<ChannelPipeline> sonarPipelineInjector;
@Setter
@Getter
private InetAddress inetAddress;
@Override
public void channelActive(final @NotNull ChannelHandlerContext ctx) throws Exception {
// Increase connections per second for the action bar verbose
GlobalSonarStatistics.countConnection();
// Make sure to let the server handle the rest
ctx.fireChannelActive();
// Add the packet handler pipeline
sonarPipelineInjector.accept(ctx.pipeline());
}
@Override
public void channelInactive(final @NotNull ChannelHandlerContext ctx) throws Exception {
// The player can disconnect without sending the login packet first
// Account for this by checking if the inetAddress has been set yet
if (inetAddress != null) {
// Remove the IP address from the connected players
Sonar.get().getFallback().getConnected().compute(inetAddress, (k, v) -> null);
// Remove the IP address from the queue
Sonar.get().getFallback().getQueue().getPlayers().compute(inetAddress, (k, v) -> null);
// Remove this account from the online players or decrement the number of accounts with the same IP
Sonar.get().getFallback().getOnline().compute(inetAddress, (k, v) -> v == null || v <= 1 ? null : v - 1);
}
// Make sure to let the server handle the rest
ctx.fireChannelInactive();
}
// We can override the default exceptionCaught method since this handler
// will run before the connection knows that there has been an error.
@Override
public void exceptionCaught(final @NotNull ChannelHandlerContext ctx,
final @NotNull Throwable cause) throws Exception {
// Close the channel if we encounter any errors.
ctx.close();
}
}
@@ -0,0 +1,288 @@
/*
* Copyright (C) 2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback;
import io.netty.channel.*;
import io.netty.handler.codec.CorruptedFrameException;
import lombok.AllArgsConstructor;
import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.fallback.Fallback;
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketEncoder;
import xyz.jonesdev.sonar.common.statistics.GlobalSonarStatistics;
import xyz.jonesdev.sonar.common.util.GeyserUtil;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.*;
import static xyz.jonesdev.sonar.common.fallback.FallbackUserWrapper.closeWith;
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer.*;
@AllArgsConstructor
@RequiredArgsConstructor
public abstract class FallbackInboundHandlerAdapter extends ChannelInboundHandlerAdapter {
protected @Nullable String username;
protected ProtocolVersion protocolVersion;
protected RemovalListener channelRemovalListener = RemovalListener.EMPTY;
protected static final Fallback FALLBACK = Sonar.get().getFallback();
/**
* Validates and handles incoming handshake packets
*
* @param hostname Hostname (server address) sent by the client
* @param protocol Protocol version number sent by the client
*/
protected final void handleHandshake(final @NotNull Channel channel,
final @NotNull String hostname,
final int protocol) throws Exception {
// Check if the hostname is invalid
if (hostname.isEmpty()) {
throw new CorruptedFrameException("Hostname is empty");
}
// Check if the player has already sent a handshake packet
if (protocolVersion != null) {
throw new CorruptedFrameException("Already sent handshake");
}
// Store the protocol version
protocolVersion = ProtocolVersion.fromId(protocol);
// Hook the traffic listener
channel.pipeline().addFirst(FALLBACK_BANDWIDTH, FallbackBandwidthHandler.INSTANCE);
}
/**
* Validates and handles incoming login packets
*
* @param channel Forwarded client channel
* @param loginPacket Login packet sent by the client
* @param username Username sent by the client
* @param socketAddress Socket address of the client
*/
protected final void handleLogin(final @NotNull Channel channel,
final @NotNull ChannelHandlerContext ctx,
final @NotNull Runnable loginPacket,
final @NotNull String username,
final @NotNull InetSocketAddress socketAddress) throws Exception {
// Check if the player has already sent a login packet
if (this.username != null) {
throw new CorruptedFrameException("Already sent login");
}
// Connections from unknown protocol versions will be discarded
// as this is the safest way of handling unwanted connections
if (protocolVersion.isUnknown()) {
// Sonar does not support snapshots or Minecraft versions older than 1.7.2
throw new CorruptedFrameException("Unknown protocol version");
}
// Increase joins per second for the action bar verbose
GlobalSonarStatistics.countLogin();
// Store the username and IP address
this.username = username;
final FallbackInboundHandler inboundHandler = channel.pipeline().get(FallbackInboundHandler.class);
Objects.requireNonNull(inboundHandler).setInetAddress(socketAddress.getAddress());
// Check the blacklist here since we cannot let the player "ghost join"
if (FALLBACK.getBlacklist().asMap().containsKey(inboundHandler.getInetAddress())) {
customDisconnect(channel, protocolVersion, blacklisted);
return;
}
// Check if Fallback is already verifying a player with the same IP address
if (FALLBACK.getConnected().containsKey(inboundHandler.getInetAddress())) {
customDisconnect(channel, protocolVersion, alreadyVerifying);
return;
}
// Check if the protocol ID of the player is not allowed to enter the server
if (Sonar.get().getConfig().getVerification().getBlacklistedProtocols()
.contains(protocolVersion.getProtocol())) {
customDisconnect(channel, protocolVersion, protocolBlacklisted);
return;
}
// Don't continue the verification process if the verification is disabled
if (!Sonar.get().getFallback().shouldVerifyNewPlayers()) {
initialLogin(ctx, inboundHandler.getInetAddress(), loginPacket);
return;
}
// Completely skip Geyser connections if configured
final boolean geyser = GeyserUtil.isGeyserConnection(channel, socketAddress);
if (geyser && !Sonar.get().getConfig().getVerification().isCheckGeyser()) {
initialLogin(ctx, inboundHandler.getInetAddress(), loginPacket);
return;
}
// Make sure we actually have to verify the player
final String offlineUuidString = "OfflinePlayer:" + username;
final UUID offlineUuid = UUID.nameUUIDFromBytes(offlineUuidString.getBytes(StandardCharsets.UTF_8));
if (Sonar.get().getVerifiedPlayerController().has(inboundHandler.getInetAddress().toString(), offlineUuid)) {
initialLogin(ctx, inboundHandler.getInetAddress(), loginPacket);
return;
}
// Check if the IP address is currently being rate-limited
if (!FALLBACK.getRatelimiter().attempt(inboundHandler.getInetAddress())) {
customDisconnect(channel, protocolVersion, reconnectedTooFast);
return;
}
// Remove all other pipelines that could still mess up something
rewriteProtocol(ctx, channelRemovalListener);
// Queue the connection for further processing
FALLBACK.getQueue().getPlayers().compute(inboundHandler.getInetAddress(), (inetAddress, runnable) -> {
// Check if the player is already queued since we don't want bots to flood the queue
if (runnable != null) {
customDisconnect(channel, protocolVersion, alreadyQueued);
// Remove other instances of this IP address from the queue
return null;
}
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(username).matches()) {
customDisconnect(channel, protocolVersion, invalidUsername);
return;
}
// Create an instance for the Fallback connection
final FallbackUser user = new FallbackUserWrapper(
channel, inboundHandler.getInetAddress(), protocolVersion, offlineUuid, geyser);
// Let the verification handler take over the channel
user.hijack(username, offlineUuid);
};
});
}
/**
* Removes all pipelines and rewrites them using our custom handlers
*/
private static void rewriteProtocol(final @NotNull ChannelHandlerContext ctx,
final @NotNull RemovalListener removalListener) {
for (final Map.Entry<String, ChannelHandler> entry : ctx.pipeline()) {
// Don't accidentally remove Sonar's handlers
if (entry.getKey().startsWith("sonar")
// Don't remove floodgate's pipelines
|| entry.getKey().startsWith("geyser")
|| entry.getKey().startsWith("floodgate")
|| entry.getKey().equals("DefaultChannelPipeline$TailContext#0")) {
continue;
}
ctx.pipeline().remove(entry.getValue());
removalListener.accept(ctx.pipeline(), entry.getKey(), entry.getValue());
}
// Add our read/write timeout handler
ctx.pipeline().addFirst(FALLBACK_TIMEOUT, new FallbackTimeoutHandler(
Sonar.get().getConfig().getVerification().getReadTimeout(),
Sonar.get().getConfig().getVerification().getWriteTimeout(),
TimeUnit.MILLISECONDS));
}
/**
* Executes the maximum accounts per IP limit check before letting the player join
*
* @param ctx Forwarded client channel context
* @param inetAddress Resolved client IP address
* @param loginPacket Login packet sent by the client
*/
protected final void initialLogin(final @NotNull ChannelHandlerContext ctx,
final @NotNull InetAddress inetAddress,
final @NotNull Runnable loginPacket) throws Exception {
// Increment the number of accounts with the same IP
FALLBACK.getOnline().compute(inetAddress, (k, v) -> v == null ? 1 : v + 1);
final int maxOnlinePerIp = Sonar.get().getConfig().getMaxOnlinePerIp();
// Skip the maximum online per IP check if it's disabled in the configuration
if (maxOnlinePerIp > 0) {
// Check if the number of online players using the same IP address as
// the connecting player is greater than the configured amount
final int onlinePerIp = FALLBACK.getOnline().getOrDefault(inetAddress, 0);
if (onlinePerIp >= maxOnlinePerIp) {
customDisconnect(ctx.channel(), protocolVersion, tooManyOnlinePerIP);
return;
}
}
// Let the server know about the login packet
loginPacket.run();
}
/**
* Disconnect the player before verification (during login)
* by replacing the encoder before running the method.
*
* @param packet Disconnect packet
* @param channel Channel of the player
* @param protocolVersion Protocol version of the player
*/
private void customDisconnect(final @NotNull Channel channel,
final @NotNull ProtocolVersion protocolVersion,
final @NotNull FallbackPacket packet) {
if (channel.eventLoop().inEventLoop()) {
_customDisconnect(channel, protocolVersion, packet);
} else {
channel.eventLoop().execute(() -> _customDisconnect(channel, protocolVersion, packet));
}
}
private void _customDisconnect(final @NotNull Channel channel,
final @NotNull ProtocolVersion protocolVersion,
final @NotNull FallbackPacket packet) {
// Remove the main pipeline to completely take over the channel
final String handler = Sonar.get().getPlatform().getHandlerFunction().apply(channel.pipeline());
if (channel.pipeline().context(handler) != null) {
channel.pipeline().remove(handler);
}
final String encoder = Sonar.get().getPlatform().getEncoderFunction().apply(channel.pipeline());
final ChannelHandler currentEncoder = channel.pipeline().get(encoder);
// Close the channel if no decoder exists
if (currentEncoder != null) {
// We don't need to update the encoder if it's already present
if (!(currentEncoder instanceof FallbackPacketEncoder)) {
final FallbackPacketEncoder newEncoder = new FallbackPacketEncoder(protocolVersion);
channel.pipeline().replace(encoder, FALLBACK_PACKET_ENCODER, newEncoder);
}
closeWith(channel, protocolVersion, packet);
} else {
channel.close();
}
}
@FunctionalInterface
public interface RemovalListener {
void accept(final @NotNull ChannelPipeline pipeline,
final @NotNull String name,
final @NotNull ChannelHandler handler);
RemovalListener EMPTY = (pipeline, name, handler) -> {};
}
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023-2024 Sonar Contributors
* Copyright (C) 2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -18,7 +18,6 @@
package xyz.jonesdev.sonar.common.fallback;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.timeout.IdleState;
import io.netty.handler.timeout.IdleStateEvent;
import io.netty.handler.timeout.IdleStateHandler;
import org.jetbrains.annotations.NotNull;
@@ -26,30 +25,18 @@ import org.jetbrains.annotations.NotNull;
import java.util.concurrent.TimeUnit;
public final class FallbackTimeoutHandler extends IdleStateHandler {
private boolean knownDisconnect;
public FallbackTimeoutHandler(final long timeout, final TimeUnit timeUnit) {
super(timeout, 0L, 0L, timeUnit);
public FallbackTimeoutHandler(final int readTimeout, final int writeTimeout, final TimeUnit timeUnit) {
super(readTimeout, writeTimeout, 0L, timeUnit);
}
@Override
protected void channelIdle(final ChannelHandlerContext ctx,
protected void channelIdle(final @NotNull ChannelHandlerContext ctx,
final @NotNull IdleStateEvent idleStateEvent) throws Exception {
assert idleStateEvent.state() == IdleState.READER_IDLE;
readTimedOut(ctx);
}
private void readTimedOut(final ChannelHandlerContext ctx) {
if (!knownDisconnect) {
// The netty (default) ReadTimeoutHandler would normally just throw an Exception
// The default ReadTimeoutHandler does only check for the boolean 'closed' and
// still throws the Exception even if the channel is closed
if (ctx.channel().isActive()) {
ctx.close();
}
knownDisconnect = true;
// The netty (default) ReadTimeoutHandler would normally just throw an Exception
// The default ReadTimeoutHandler does only check for the boolean 'closed' and
// still throws the Exception even if the channel is closed
if (ctx.channel().isActive()) {
ctx.close();
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023-2024 Sonar Contributors
* Copyright (C) 2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -19,8 +19,8 @@ package xyz.jonesdev.sonar.common.fallback;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelPipeline;
import io.netty.handler.codec.CorruptedFrameException;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@@ -31,14 +31,16 @@ import xyz.jonesdev.sonar.api.event.impl.UserBlacklistedEvent;
import xyz.jonesdev.sonar.api.event.impl.UserVerifyFailedEvent;
import xyz.jonesdev.sonar.api.event.impl.UserVerifyJoinEvent;
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
import xyz.jonesdev.sonar.api.fallback.FallbackUserState;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
import xyz.jonesdev.sonar.api.timer.SystemTimer;
import xyz.jonesdev.sonar.common.fallback.netty.FallbackTailExceptionsHandler;
import xyz.jonesdev.sonar.common.fallback.netty.FallbackVarInt21FrameDecoder;
import xyz.jonesdev.sonar.common.fallback.netty.FallbackVarInt21FrameEncoder;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketDecoder;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketEncoder;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.login.LoginSuccessPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.DisconnectPacket;
import xyz.jonesdev.sonar.common.fallback.session.FallbackLoginSessionHandler;
import xyz.jonesdev.sonar.common.statistics.GlobalSonarStatistics;
import java.net.InetAddress;
@@ -46,112 +48,117 @@ import java.util.UUID;
import java.util.concurrent.TimeUnit;
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.*;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry.CONFIG;
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry.GAME;
@Getter
@ToString(of = {"protocolVersion", "inetAddress"})
@ToString(of = {"protocolVersion", "inetAddress", "geyser"})
public final class FallbackUserWrapper implements FallbackUser {
private final Channel channel;
private final ChannelPipeline pipeline;
private final InetAddress inetAddress;
private final ProtocolVersion protocolVersion;
private final UUID offlineUuid;
@Setter
private FallbackUserState state = FallbackUserState.LOGIN_ACK; // 1.20.2+
private boolean receivedClientSettings;
@Setter
private boolean receivedPluginMessage;
private final boolean geyser;
private final SystemTimer loginTimer = new SystemTimer();
public FallbackUserWrapper(final @NotNull Channel channel,
final @NotNull InetAddress inetAddress,
final @NotNull ProtocolVersion protocolVersion) {
final @NotNull ProtocolVersion protocolVersion,
final @NotNull UUID offlineUuid,
final boolean geyser) {
this.channel = channel;
this.pipeline = channel.pipeline();
this.inetAddress = inetAddress;
this.protocolVersion = protocolVersion;
this.offlineUuid = offlineUuid;
this.geyser = geyser;
}
@Override
public void disconnect(final @NotNull Component reason) {
closeWith(channel, protocolVersion, DisconnectPacket.create(reason, false));
final FallbackPacketEncoder encoder = pipeline.get(FallbackPacketEncoder.class);
final boolean duringLogin = encoder != null && encoder.getPacketRegistry() != GAME;
closeWith(channel, protocolVersion, DisconnectPacket.create(reason, duringLogin));
}
@Override
public void hijack(final @NotNull String username, final @NotNull UUID uuid,
final @NotNull String encoder, final @NotNull String decoder,
final @NotNull String timeout, final @NotNull String boss) {
// This rarely happens when the channel hangs, but the player is still connecting
// This also fixes a unique issue with TCPShield and other reverse proxies
if (pipeline.get(encoder) == null || pipeline.get(decoder) == null) {
channel.close();
return;
}
// Remove main pipeline to completely take over the channel
if (pipeline.get(boss) != null) {
pipeline.remove(boss);
}
public void hijack(final @NotNull String username, final @NotNull UUID offlineUuid) {
// The player has joined the verification
GlobalSonarStatistics.totalAttemptedVerifications++;
if (Sonar.get().getConfig().getVerification().isLogConnections()) {
// Only log the processing message if the server isn't under attack.
// We let the user override this through the configuration.
if (!Sonar.get().getAttackTracker().isCurrentlyUnderAttack()
if (Sonar.get().getAttackTracker().getCurrentAttack() == null
|| Sonar.get().getConfig().getVerification().isLogDuringAttack()) {
Sonar.get().getFallback().getLogger().info(Sonar.get().getConfig().getVerification().getConnectLog()
.replace("%name%", username)
.replace("%ip%", Sonar.get().getConfig().formatAddress(inetAddress))
.replace("%protocol%", String.valueOf(protocolVersion.getProtocol())));
Sonar.get().getFallback().getLogger().info(
Sonar.get().getConfig().getMessagesConfig().getString("verification.logs.connection")
.replace("<username>", username)
.replace("<ip>", Sonar.get().getConfig().formatAddress(inetAddress))
.replace("<protocol>", String.valueOf(protocolVersion.getProtocol())));
}
}
// Call the VerifyJoinEvent for external API usage
Sonar.get().getEventManager().publish(new UserVerifyJoinEvent(username, this));
// Mark the player as connected → verifying players
Sonar.get().getFallback().getConnected().put(username, inetAddress);
// Run this in the channel's event loop to avoid issues
channel.eventLoop().execute(() -> {
// Make sure the channel is still active
if (!channel.isActive()) {
return;
}
// Add better timeout handler to avoid known exploits or issues
// We also want to timeout bots quickly to avoid flooding
final int readTimeout = Sonar.get().getConfig().getVerification().getReadTimeout();
pipeline.replace(timeout, timeout, new FallbackTimeoutHandler(readTimeout, TimeUnit.MILLISECONDS));
// Mark the player as connected by caching them in a map of verifying players
Sonar.get().getFallback().getConnected().compute(inetAddress, (_k, _v) -> (byte) 0);
// Replace normal encoder to allow custom packets
final FallbackPacketEncoder newEncoder = new FallbackPacketEncoder(protocolVersion);
pipeline.replace(encoder, FALLBACK_PACKET_ENCODER, newEncoder);
// Replace normal encoder to allow custom packets
final FallbackPacketEncoder newEncoder = new FallbackPacketEncoder(protocolVersion);
pipeline.addFirst(FALLBACK_FRAME_ENCODER, FallbackVarInt21FrameEncoder.INSTANCE);
pipeline.addLast(FALLBACK_PACKET_ENCODER, newEncoder);
// Send LoginSuccess packet to make the client think they are joining the server
write(new LoginSuccessPacket(username, uuid));
// Send LoginSuccess packet to make the client think they are joining the server
write(FallbackPreparer.loginSuccess);
// The LoginSuccess packet has been sent, now we can change the registry state
newEncoder.updateRegistry(protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_20_2) >= 0
? FallbackPacketRegistry.CONFIG : FallbackPacketRegistry.GAME);
// The LoginSuccess packet has been sent, now we can change the registry state
newEncoder.updateRegistry(protocolVersion.compareTo(MINECRAFT_1_20_2) >= 0 ? CONFIG : GAME);
// Replace normal decoder to allow custom packets
pipeline.replace(decoder, FALLBACK_PACKET_DECODER,
new FallbackPacketDecoder(this, new FallbackVerificationHandler(this, username, uuid)));
// Replace normal decoder to allow custom packets
final FallbackPacketDecoder fallbackPacketDecoder = new FallbackPacketDecoder(protocolVersion);
pipeline.addFirst(FALLBACK_FRAME_DECODER, new FallbackVarInt21FrameDecoder());
pipeline.addLast(FALLBACK_PACKET_DECODER, fallbackPacketDecoder);
// Listen for all incoming packets by setting the packet listener
fallbackPacketDecoder.setListener(new FallbackLoginSessionHandler(this, username));
// Make sure to catch all exceptions during the verification
pipeline.addLast(FALLBACK_TAIL_EXCEPTIONS, FallbackTailExceptionsHandler.INSTANCE);
});
}
@Override
public void fail(final @NotNull String reason) {
// Make sure to set the state to failed to drop all handlers
setState(FallbackUserState.FAILED);
disconnect(Sonar.get().getConfig().getVerification().getVerificationFailed());
// Only log the failed message if the server isn't currently under attack.
// However, we let the user override this through the configuration.
final boolean shouldLog = !Sonar.get().getAttackTracker().isCurrentlyUnderAttack()
final boolean shouldLog = Sonar.get().getAttackTracker().getCurrentAttack() == null
|| Sonar.get().getConfig().getVerification().isLogDuringAttack();
// Only disconnect the player and log if the channel is active
if (channel.isActive()) {
disconnect(Sonar.get().getConfig().getVerification().getVerificationFailed());
if (shouldLog) {
Sonar.get().getFallback().getLogger().info(Sonar.get().getConfig().getVerification().getFailedLog()
.replace("%ip%", Sonar.get().getConfig().formatAddress(getInetAddress()))
.replace("%protocol%", String.valueOf(getProtocolVersion().getProtocol()))
.replace("%reason%", reason));
}
if (shouldLog) {
Sonar.get().getFallback().getLogger().info(
Sonar.get().getConfig().getMessagesConfig().getString("verification.logs.failed")
.replace("<ip>", Sonar.get().getConfig().formatAddress(getInetAddress()))
.replace("<protocol>", String.valueOf(getProtocolVersion().getProtocol()))
.replace("<reason>", reason));
}
// Increment amount of total failed verifications
// Increment number of total failed verifications
GlobalSonarStatistics.totalFailedVerifications++;
// Call the VerifyFailedEvent for external API usage
@@ -165,33 +172,35 @@ public final class FallbackUserWrapper implements FallbackUser {
if (blacklistThreshold <= 0) break blacklist;
// Use 1 as the default amount of fails since we haven't cached anything yet
final int failCount = Sonar.get().getFallback().getRatelimiter().getFailCountCache()
.asMap().getOrDefault(inetAddress, 1);
final int fails = Sonar.get().getFallback().getRatelimiter().getFailCountCache().get(inetAddress, ignored -> 1);
// Now we simply need to check if the threshold is reached
if (failCount < blacklistThreshold) {
if (fails < blacklistThreshold) {
// Make sure we increment the number of fails
Sonar.get().getFallback().getRatelimiter().incrementFails(inetAddress, failCount);
Sonar.get().getFallback().getRatelimiter().incrementFails(inetAddress, fails);
break blacklist;
}
// Call the BotBlacklistedEvent for external API usage
Sonar.get().getEventManager().publish(new UserBlacklistedEvent(this));
// Increment amount of total blacklisted players
// Increment number of total blacklisted players
GlobalSonarStatistics.totalBlacklistedPlayers++;
Sonar.get().getFallback().getBlacklist().put(getInetAddress(), (byte) 0);
if (shouldLog) {
Sonar.get().getFallback().getLogger().info(Sonar.get().getConfig().getVerification().getBlacklistLog()
.replace("%ip%", Sonar.get().getConfig().formatAddress(getInetAddress()))
.replace("%protocol%", String.valueOf(getProtocolVersion().getProtocol())));
Sonar.get().getFallback().getLogger().info(
Sonar.get().getConfig().getMessagesConfig().getString("verification.logs.blacklisted")
.replace("<ip>", Sonar.get().getConfig().formatAddress(getInetAddress()))
.replace("<protocol>", String.valueOf(getProtocolVersion().getProtocol())));
}
// Invalidate the cached entry to ensure memory safety
Sonar.get().getFallback().getRatelimiter().getFailCountCache().invalidate(inetAddress);
}
// Throw an exception to avoid further code execution
throw new CorruptedFrameException();
}
/**
@@ -203,8 +212,8 @@ public final class FallbackUserWrapper implements FallbackUser {
final @NotNull ProtocolVersion protocolVersion,
final @NotNull Object msg) {
if (channel.isActive()) {
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_8) < 0
&& protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_7_2) >= 0) {
if (protocolVersion.compareTo(MINECRAFT_1_8) < 0
&& protocolVersion.compareTo(MINECRAFT_1_7_2) >= 0) {
channel.eventLoop().execute(() -> {
channel.config().setAutoRead(false);
channel.eventLoop().schedule(() -> {
@@ -216,50 +225,4 @@ public final class FallbackUserWrapper implements FallbackUser {
}
}
}
/**
* Disconnect the player before verification (during login)
* by replacing the encoder before running the method.
*
* @param packet Disconnect packet
* @param encoder Encoder to replace
* @param boss Main pipeline to remove
* @param channel Channel of the player
* @param protocolVersion Protocol version of the player
*/
public static void customDisconnect(final @NotNull Channel channel,
final @NotNull ProtocolVersion protocolVersion,
final @NotNull FallbackPacket packet,
final @NotNull String encoder,
final @NotNull String boss) {
// Remove main pipeline to completely take over the channel
if (channel.pipeline().get(boss) != null) {
channel.pipeline().remove(boss);
}
final ChannelHandler currentEncoder = channel.pipeline().get(encoder);
// Simply close the channel if no decoder exists
if (currentEncoder != null) {
// We don't need to update the encoder if it's already present
if (!(currentEncoder instanceof FallbackPacketEncoder)) {
final FallbackPacketEncoder newEncoder = new FallbackPacketEncoder(protocolVersion);
channel.pipeline().replace(encoder, FALLBACK_PACKET_ENCODER, newEncoder);
}
closeWith(channel, protocolVersion, packet);
} else {
channel.close();
}
}
/**
* Removes Sonar's pipelines from a channel to save
* unnecessary RAM and CPU usage
*
* @param pipeline Pipeline object of the channel
*/
public static void deject(final @NotNull ChannelPipeline pipeline) {
// Remove Sonar's pipelines if they exist
if (pipeline.get(FALLBACK_HANDLER) != null) {
pipeline.remove(FALLBACK_HANDLER);
}
}
}
@@ -1,709 +0,0 @@
/*
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback;
import io.netty.buffer.ByteBuf;
import io.netty.handler.codec.CorruptedFrameException;
import io.netty.handler.codec.DecoderException;
import lombok.val;
import net.kyori.adventure.text.minimessage.MiniMessage;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.event.impl.UserVerifySuccessEvent;
import xyz.jonesdev.sonar.api.fallback.Fallback;
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
import xyz.jonesdev.sonar.api.fallback.FallbackUserState;
import xyz.jonesdev.sonar.api.model.VerifiedPlayer;
import xyz.jonesdev.sonar.api.timer.SystemTimer;
import xyz.jonesdev.sonar.common.fallback.protocol.*;
import xyz.jonesdev.sonar.common.fallback.protocol.map.ItemType;
import xyz.jonesdev.sonar.common.fallback.protocol.map.MapCaptchaInfo;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.config.FinishConfigurationPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.login.LoginAcknowledgedPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.*;
import xyz.jonesdev.sonar.common.fallback.protocol.vehicle.EntityType;
import xyz.jonesdev.sonar.common.statistics.GlobalSonarStatistics;
import xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil;
import java.util.Objects;
import java.util.Random;
import java.util.UUID;
import java.util.regex.Pattern;
import static xyz.jonesdev.sonar.api.config.SonarConfiguration.Verification.Gravity.Gamemode.CREATIVE;
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_PACKET_DECODER;
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_PACKET_ENCODER;
import static xyz.jonesdev.sonar.api.fallback.FallbackUserState.*;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer.*;
public final class FallbackVerificationHandler implements FallbackPacketListener {
private static final Random RANDOM = new Random();
private static final Fallback FALLBACK = Sonar.get().getFallback();
private final SystemTimer login = new SystemTimer();
private final @NotNull FallbackUser user;
private final String username;
private final UUID playerUuid;
// Checks
private short expectedTransactionId;
private int expectedKeepAliveId, expectedTeleportId = -1;
private int tick, totalReceivedPackets;
private double posX, posY, posZ, lastY, spawnYPosition;
private boolean resolvedClientBrand, resolvedClientSettings;
private boolean listenForMovements;
// CAPTCHA
private final SystemTimer keepAlive = new SystemTimer();
private final SystemTimer actionBar = new SystemTimer();
private @Nullable MapCaptchaInfo captcha;
private int captchaTriesLeft;
// Cached options to make sure the original values don't change throughout the verification
private final boolean performVehicle, performCaptcha, performGravity, performCollisions;
public FallbackVerificationHandler(final @NotNull FallbackUser user,
final @NotNull String username,
final @NotNull UUID playerUuid) {
this.user = user;
this.username = username;
this.playerUuid = playerUuid;
this.performGravity = Sonar.get().getConfig().getVerification().getGravity().isEnabled();
this.performCollisions = Sonar.get().getConfig().getVerification().getGravity().isCheckCollisions();
this.performCaptcha = FALLBACK.shouldPerformCaptcha();
this.performVehicle = FALLBACK.shouldPerformVehicleCheck();
if (user.getProtocolVersion().compareTo(MINECRAFT_1_20_2) < 0) {
// Start initializing the actual join process
initialJoinProcess();
}
}
private void configure() {
// Set the state to CONFIGURE to avoid false positives
user.setState(CONFIGURE);
// Send the necessary configuration packets to the client
user.delayedWrite(REGISTRY_SYNC);
user.delayedWrite(FINISH_CONFIGURATION);
// Send all packets in one flush
user.getChannel().flush();
// Set decoder state to actually catch all packets
updateEncoderDecoderState(FallbackPacketRegistry.CONFIG);
}
private void updateEncoderDecoderState(final @NotNull FallbackPacketRegistry registry) {
val decoder = (FallbackPacketDecoder) user.getChannel().pipeline().get(FALLBACK_PACKET_DECODER);
val encoder = (FallbackPacketEncoder) user.getChannel().pipeline().get(FALLBACK_PACKET_ENCODER);
if (decoder != null && encoder != null) {
// Update the packet registry state to be able to listen for CONFIG packets
decoder.updateRegistry(registry);
encoder.updateRegistry(registry);
} else {
// Something went wrong - the decoder should not be null
FALLBACK.getLogger().warn("Necessary pipelines for {} not found", username);
// Close the channel to prevent bypasses or other potential exploits
user.getChannel().close();
}
}
private void initialJoinProcess() {
if (user.getProtocolVersion().compareTo(MINECRAFT_1_8) < 0) {
// 1.7 players don't have KeepAlive packets in the login process
sendJoinGamePacket();
} else {
// Set the state to KEEP_ALIVE to avoid false positives
user.setState(KEEP_ALIVE);
// Generate a random KeepAlive ID for the pre-join check
expectedKeepAliveId = RANDOM.nextInt();
// Send first KeepAlive to check if the connection is somewhat responsive
user.write(new KeepAlivePacket(expectedKeepAliveId));
}
}
private void sendTransaction() {
// Set the state to TRANSACTION to avoid false positives
// and go on with the flow of the verification.
user.setState(TRANSACTION);
// Generate a random transaction ID
expectedTransactionId = (short) RANDOM.nextInt();
// Send a transaction with the given ID
user.write(new TransactionPacket(0, expectedTransactionId, false));
}
private void sendJoinGamePacket() {
final boolean v1_20_2 = user.getProtocolVersion().compareTo(MINECRAFT_1_20_2) >= 0;
if (!v1_20_2) {
// Set the state to CLIENT_SETTINGS to avoid false positives
// (1.20.2 needs the LOGIN_ACK state) and go on with
// the flow of the verification.
user.setState(CLIENT_SETTINGS);
}
// Send the JoinGame packet so we can continue the verification
user.write(joinGame);
if (v1_20_2) {
// Perform the transaction check since the ClientSettings
// and PluginMessage packets have already been validated.
sendTransaction();
}
}
private void sendAbilitiesAndTeleport() {
// Set the state to TELEPORT to avoid false positives
// and go on with the flow of the verification.
user.setState(TELEPORT);
// Make sure the player is unable to fly (e.g. if the player is in creative mode)
if (Sonar.get().getConfig().getVerification().getGravity().getGamemode() == CREATIVE) {
user.delayedWrite(DEFAULT_ABILITIES);
}
// Generate the current teleport ID
expectedTeleportId = RANDOM.nextInt();
// Add a little randomization to the spawn y coordinate
spawnYPosition = dynamicSpawnYPosition + RANDOM.nextDouble(0.4);
// Teleport the player to the spawn position
user.delayedWrite(new PlayerPositionLookPacket(
SPAWN_X_POSITION, spawnYPosition, SPAWN_Z_POSITION,
0f, -90f, expectedTeleportId, false));
// Make sure the player escapes the 1.18.2+ "Loading terrain" screen
if (user.getProtocolVersion().compareTo(MINECRAFT_1_18_2) >= 0) {
user.delayedWrite(dynamicSpawnPosition);
}
}
private void sendChunkData() {
// If we don't have any checks, simply finish verification
// This is *hopefully* a very rare case though...
if (!performGravity && !performCaptcha && !performVehicle) {
// Save some work by finishing before sending even more packets
finish();
return;
}
// Set the state to POSITION to avoid false positives
// and go on with the flow of the verification.
user.setState(POSITION);
// 1.20.3 packets introduced game events
if (user.getProtocolVersion().compareTo(MINECRAFT_1_20_3) >= 0) {
// Make sure the client knows that we're sending chunks next
user.delayedWrite(START_WRITING_CHUNKS);
}
// Teleport player into the fake lobby by sending an empty chunk
user.delayedWrite(EMPTY_CHUNK_DATA);
// Checking gravity is disabled, just finish verification
if (!performGravity) {
// Switch to captcha state if needed
vehicleCheckOrNext();
return;
}
// Send an UpdateSectionBlocks packet with a platform of blocks
// to check if the player collides with the solid platform.
user.delayedWrite(updateSectionBlocks);
// Send all packets in one flush
user.getChannel().flush();
}
private static boolean validateClientLocale(final @SuppressWarnings("unused") @NotNull FallbackUser user,
final String locale) {
// Check the client locale by performing a simple regex check on it
final Pattern pattern = Sonar.get().getConfig().getVerification().getValidLocaleRegex();
return pattern.matcher(locale).matches(); // Disallow non-ascii characters (by default)
}
private static boolean validateClientBrand(final @NotNull FallbackUser user, final ByteBuf content) {
// We have to catch every DecoderException, so we can fail and punish
// the player instead of only disconnecting them due to an exception.
try {
// No need to check for empty brands since ProtocolUtil#readBrandMessage
// already performs these checks by default.
final String read = ProtocolUtil.readBrandMessage(content);
// Check if the decoded client brand string is too long
if (read.length() > Sonar.get().getConfig().getVerification().getBrand().getMaxLength()) {
return false;
}
// Regex pattern for validating client brands
final Pattern pattern = Sonar.get().getConfig().getVerification().getBrand().getValidRegex();
return !read.equals("Vanilla") // The normal brand is always lowercase
&& pattern.matcher(read).matches(); // Disallow non-ascii characters (by default)
} catch (DecoderException exception) {
// Fail if the string (client brand) could not be decoded properly
user.fail("could not decode string");
// Throw the exception so we don't continue checking
throw exception;
}
}
private void handlePacketsCAPTCHA(final @NotNull FallbackPacket packet) {
// Check if the player took too long to enter the captcha
final int maxDuration = Sonar.get().getConfig().getVerification().getMap().getMaxDuration();
checkFrame(!login.elapsed(maxDuration), "took too long to enter captcha");
// Handle incoming chat messages
if (packet instanceof UniversalChatPacket) {
checkFrame(captcha != null, "Captcha not sent yet");
final UniversalChatPacket chat = (UniversalChatPacket) packet;
if (!chat.getMessage().equals(captcha.getAnswer())) {
// Captcha is incorrect
checkFrame(captchaTriesLeft-- > 0, "failed captcha too often");
user.write(incorrectCaptcha);
return;
}
// Captcha is correct
finish();
return;
}
// Every second
if (actionBar.elapsed(user.getProtocolVersion().compareTo(MINECRAFT_1_8) < 0 ? 10_000L : 1_000L)) {
final String actionBarMessage = Sonar.get().getConfig().getVerification().getMap().getEnterCodeActionBar();
// Only send action bar if the message is actually supposed to be sent
if (!actionBarMessage.isEmpty()) {
final String timeLeft = String.format("%.0f", (maxDuration - login.delay()) / 1000D);
user.write(new UniversalChatPacket(MiniMessage.miniMessage().deserialize(
actionBarMessage.replace("%time-left%", timeLeft)), UniversalChatPacket.GAME_INFO_TYPE));
// Make sure to reset the timer
actionBar.reset();
}
}
// Every 10 seconds
if (keepAlive.elapsed(10_000L)) {
// Send a KeepAlive packet to prevent timeout
user.write(CAPTCHA_KEEP_ALIVE);
// Make sure to reset the timer
keepAlive.reset();
}
}
@Override
public void handle(final @NotNull FallbackPacket packet) {
// The player has already been verified or failed the verification -> drop packet
if (!user.getState().canReceivePackets()) return;
// Calculate maximum amount of packets sent by the client to the server
final int maxPackets = Sonar.get().getConfig().getVerification().getMaxLoginPackets()
+ maxMovementTick // Account for positions/fall check
+ Sonar.get().getConfig().getVerification().getMap().getMaxDuration()
+ Sonar.get().getConfig().getVerification().getMap().getMaxTries();
checkFrame(++totalReceivedPackets < maxPackets, "too many packets");
// We are expecting a code in chat, drop all other packets
if (user.getState() == CAPTCHA) {
handlePacketsCAPTCHA(packet);
return;
}
if (packet instanceof PlayerInputPacket) {
// Check if we are currently expecting a PlayerInputPacket packet
assertState(VEHICLE);
final PlayerInputPacket inputPacket = (PlayerInputPacket) packet;
// Make sure the values sent by the client are legitimate
checkFrame(inputPacket.getForward() <= 0.98f, "invalid vehicle speed (f)");
checkFrame(inputPacket.getSideways() <= 0.98f, "invalid vehicle speed (s)");
// Don't verify if the player tries unmounting or jumping
// We can't fail since the player is able to jump or sneak
if (inputPacket.isJump() || inputPacket.isUnmount()) return;
// Continue checking or verify the player
captchaOrNext();
return;
}
if (packet instanceof LoginAcknowledgedPacket) {
// Check if we are currently expecting a LoginAcknowledged packet
assertState(LOGIN_ACK);
// Start the configuration process for 1.20.2 clients
configure();
return;
}
if (packet instanceof FinishConfigurationPacket) {
// Check if we are currently expecting a FinishConfiguration packet
assertState(CONFIGURE);
// Check if the client has already sent valid ClientSettings and PluginMessage packets
checkFrame(resolvedClientBrand, "did not resolve client brand");
checkFrame(resolvedClientSettings, "did not resolve client settings");
// Start initializing the actual join process
updateEncoderDecoderState(FallbackPacketRegistry.GAME);
initialJoinProcess();
return;
}
if (packet instanceof KeepAlivePacket) {
final KeepAlivePacket keepAlive = (KeepAlivePacket) packet;
// 1.7-1.8.9 are sending a KeepAlive packet with the ID 0 every 20 ticks
if (keepAlive.getId() == 0 && user.getProtocolVersion().compareTo(MINECRAFT_1_8) <= 0) return;
// Check if we are currently expecting a KeepAlive packet
assertState(KEEP_ALIVE);
checkFrame(keepAlive.getId() == expectedKeepAliveId, "invalid KeepAlive ID");
// The correct KeepAlive packet has been received
sendJoinGamePacket();
return;
}
if (packet instanceof ClientSettingsPacket) {
final ClientSettingsPacket clientSettings = (ClientSettingsPacket) packet;
// Validate the locale using a regex to filter unwanted characters.
checkFrame(validateClientLocale(user, clientSettings.getLocale()), "invalid locale");
// Clients sometimes mess up the ClientSettings or PluginMessage packet.
// Probably caused by some weird race condition in Minecraft 1.13+
if (user.getState() == CLIENT_SETTINGS) {
user.setState(PLUGIN_MESSAGE);
}
// Make sure we mark the ClientSettings as valid
resolvedClientSettings = true;
return;
}
if (packet instanceof PluginMessagePacket) {
final PluginMessagePacket pluginMessage = (PluginMessagePacket) packet;
// Only the brand channel is important, drop the rest
if (pluginMessage.getChannel().equals("MC|Brand")
|| pluginMessage.getChannel().equals("minecraft:brand")) {
// Check if the brand packet was sent twice,
// which is not possible when using a vanilla Minecraft client.
checkFrame(!resolvedClientBrand, "duplicate client brand packet");
// Check if the channel is correct - 1.13 uses the new namespace
// system ('minecraft:' + channel) and anything below 1.13 uses
// the legacy namespace system ('MC|' + channel).
final boolean v1_13 = user.getProtocolVersion().compareTo(MINECRAFT_1_13) >= 0;
checkFrame(pluginMessage.getChannel().equals("MC|Brand") || v1_13, "invalid channel");
// Validate the client branding using a regex to filter unwanted characters.
if (Sonar.get().getConfig().getVerification().getBrand().isEnabled()) {
checkFrame(validateClientBrand(user, pluginMessage.getSlicedBuffer()), "invalid client brand");
}
// Clients sometimes mess up the ClientSettings or PluginMessage packet.
if (user.getState() == PLUGIN_MESSAGE) {
// Send the transaction packet
sendTransaction();
}
// Make sure we mark the PluginMessage as valid
resolvedClientBrand = true;
}
return;
}
if (packet instanceof TransactionPacket) {
// Check if we are currently expecting a Transaction packet
assertState(TRANSACTION);
final TransactionPacket transaction = (TransactionPacket) packet;
// The transaction should always be accepted
checkFrame(transaction.isAccepted(), "transaction not accepted?!");
// Check if the transaction ID is valid
checkFrame(transaction.getId() == expectedTransactionId, "invalid transaction id");
// First, send an Abilities packet to the client to make
// sure the player falls even in spectator mode.
// Then, teleport the player to the spawn position.
sendAbilitiesAndTeleport();
// 1.7-1.8 clients do not have a TeleportConfirm packet
if (user.getProtocolVersion().compareTo(MINECRAFT_1_8) <= 0) {
// Immediately send the chunk data as 1.7-1.8 can't confirm teleport packets
sendChunkData();
} else {
// Send all previously sent packets in one flush since we didn't flush
// the channel earlier when we teleported the player.
user.getChannel().flush();
}
return;
}
if (packet instanceof TeleportConfirmPacket) {
// Check if we are currently expecting a TeleportConfirm packet
assertState(TELEPORT);
// Check if the teleport ID is correct
final TeleportConfirmPacket teleportConfirm = (TeleportConfirmPacket) packet;
checkFrame(teleportConfirm.getTeleportId() == expectedTeleportId, "invalid teleport ID");
// Reset all values to ensure safety on teleport
tick = 1;
posY = lastY = -1;
expectedTeleportId = -1;
// Now we can send the chunk data
sendChunkData();
return;
}
if (user.getState() != LOGIN_ACK) {
if (packet instanceof PlayerPositionPacket) {
final PlayerPositionPacket position = (PlayerPositionPacket) packet;
handlePositionUpdate(position.getX(), position.getY(), position.getZ(), position.isOnGround());
return;
}
if (packet instanceof PlayerPositionLookPacket) {
final PlayerPositionLookPacket position = (PlayerPositionLookPacket) packet;
handlePositionUpdate(position.getX(), position.getY(), position.getZ(), position.isOnGround());
return;
}
if (packet instanceof PlayerTickPacket) {
final PlayerTickPacket player = (PlayerTickPacket) packet;
handlePositionUpdate(posX, posY, posZ, player.isOnGround());
}
}
}
private void handlePositionUpdate(final double x, final double y, final double z, final boolean ground) {
try {
// 1.8 does not have a TeleportConfirm packet, so we need to handle the spawning differently
if (user.getProtocolVersion().compareTo(MINECRAFT_1_8) <= 0
&& expectedTeleportId != -1 // Check if the teleport ID is currently unset
// Then, check if the position is equal to the spawn position
&& x == SPAWN_X_POSITION && y == spawnYPosition && z == SPAWN_Z_POSITION) {
// Reset all values to ensure safety on teleport
tick = 1;
posY = -1;
expectedTeleportId = -1;
// Check for ground state in the first packet
checkFrame(!ground, "invalid ground state");
}
posX = x;
lastY = posY;
posY = y;
posZ = z;
// The player is not allowed to move away from the collision platform.
// This should not happen unless the max movement tick is configured to a high number.
checkFrame(Math.abs(x - BLOCKS_PER_ROW) < BLOCKS_PER_ROW, "moved too far (x)");
checkFrame(Math.abs(z - BLOCKS_PER_ROW) < BLOCKS_PER_ROW, "moved too far (z)");
// Check if the client hasn't moved before sending the first movement packet
if (!listenForMovements) {
if (posX == SPAWN_X_POSITION && posZ == SPAWN_Z_POSITION) {
// Now, once we verified the X and Z position, we can safely check for gravity
listenForMovements = true;
}
lastY = spawnYPosition;
return;
}
final double deltaY = lastY - y;
// The deltaY is 0 whenever the player sends their first position packet.
// We have to account for this or the player will fail the verification.
if (deltaY == 0) return;
// Calculate the difference between the player's Y coordinate and the expected Y coordinate
double collisionOffsetY = (DEFAULT_Y_COLLIDE_POSITION + blockType.getBlockHeight()) - y;
if (user.getProtocolVersion().compareTo(MINECRAFT_1_8) < 0) {
collisionOffsetY += 1.62f; // 1.7 is weird and sends the head position instead of the AABB minY
}
// Check if the player is colliding by performing a basic Y offset check.
// The offset cannot be greater than 0 since the blocks will not let the player fall through them.
checkFrame(collisionOffsetY <= 0, "fell through blocks: " + collisionOffsetY);
if (tick > maxMovementTick) {
// Log/debug position if enabled in the configuration
if (Sonar.get().getConfig().getVerification().isDebugXYZPositions()) {
FALLBACK.getLogger().info("{}: {}/{}/{} - deltaY: {} - ground: {} - collision: {}",
username, x, y, z, deltaY, ground, collisionOffsetY);
}
// If the collision check is disabled, finish verification
if (!performCollisions) {
vehicleCheckOrNext();
return;
}
// Perform the collision check
if (ground) {
// Make sure the player is actually colliding with the blocks and not only spoofing ground
checkFrame(collisionOffsetY > -0.03, "illegal collision: " + collisionOffsetY);
// The player is colliding to blocks, finish verification
vehicleCheckOrNext();
return;
} else {
// Make sure the player is colliding with blocks but is sending an invalid ground state
checkFrame(collisionOffsetY != 0, "no ground: " + collisionOffsetY);
}
} else {
// Check if the player is spoofing the ground state
checkFrame(!ground, "spoofed ground state");
}
// Make sure we don't run out of predicted Y motions
checkFrame(tick < preparedCachedYMotions.length, "too many movements");
if (!ground) {
final double predictedY = preparedCachedYMotions[tick];
final double offsetY = Math.abs(deltaY - predictedY);
// Log/debug position if enabled in the configuration
if (Sonar.get().getConfig().getVerification().isDebugXYZPositions()) {
FALLBACK.getLogger().info("{}: {}/{}/{} - deltaY: {} - prediction: {} - offset: {}",
username, x, y, z, deltaY, predictedY, offsetY);
}
// Check if the y motion is roughly equal to the predicted value
checkFrame(offsetY < 0.005, String.format("invalid gravity: %d, %.7f, %.10f, %.10f != %.10f",
tick, y, offsetY, deltaY, predictedY));
}
tick++;
} catch (CorruptedFrameException exception) {
// Do not throw the exception if the user configured to display the CAPTCHA instead
if (Sonar.get().getConfig().getVerification().getGravity().isCaptchaOnFail()) {
handleCAPTCHA();
return;
}
// Interrupt the netty eventLoop to immediately stop further processing
throw new DecoderException(exception);
}
}
// TODO: Maybe recode the flow with a better state system?
private void vehicleCheckOrNext() {
if (performVehicle) {
// Initialize the vehicle check
handleVehicleCheck();
return;
}
captchaOrNext();
}
private void captchaOrNext() {
if (performCaptcha) {
// Initialize the map captcha
handleCAPTCHA();
return;
}
// Finish the verification
finish();
}
private void handleVehicleCheck() {
// Set the state to VEHICLE, so we don't handle any unnecessary packets
user.setState(VEHICLE);
// Send the necessary packets to mount the player on the vehicle
final int vehicleType = EntityType.BOAT.getId(user.getProtocolVersion());
user.delayedWrite(new SpawnEntityPacket(VEHICLE_ENTITY_ID, vehicleType, posX, posY, posZ));
user.delayedWrite(setPassengers);
user.getChannel().flush();
}
private void handleCAPTCHA() {
// Set the state to CAPTCHA, so we don't handle any unnecessary packets
user.setState(CAPTCHA);
if (!MAP_INFO_PREPARER.isCaptchaAvailable()) {
// This should not happen, but we have to return if there is no captcha prepared
user.disconnect(Sonar.get().getConfig().getVerification().getCurrentlyPreparing());
return;
}
if (!performGravity
&& user.getProtocolVersion().compareTo(MINECRAFT_1_18_2) >= 0) {
// Make sure the player escapes the 1.18.2+ "Loading terrain" screen
user.delayedWrite(CAPTCHA_SPAWN_POSITION);
}
if (performVehicle) {
// Make sure the player exits the vehicle before sending the CAPTCHA
user.delayedWrite(removeEntities);
}
// Reset max tries
captchaTriesLeft = Sonar.get().getConfig().getVerification().getMap().getMaxTries();
// Set slot to map
user.delayedWrite(new SetSlotPacket(0, 36, 1, 0,
ItemType.FILLED_MAP.getId(user.getProtocolVersion()), SetSlotPacket.MAP_NBT));
// Send random captcha to the player
captcha = MAP_INFO_PREPARER.getRandomCaptcha();
Objects.requireNonNull(captcha).delayedWrite(user);
// Teleport the player to the position above the platform
user.delayedWrite(CAPTCHA_POSITION);
// Make sure the player cannot move
user.delayedWrite(CAPTCHA_ABILITIES);
// Make sure the player knows what to do
user.delayedWrite(enterCodeMessage);
// Send all packets in one flush
user.getChannel().flush();
}
private void finish() {
// Make sure we drop all further packets
user.setState(SUCCESS);
// Increment amount of total successful verifications
GlobalSonarStatistics.totalSuccessfulVerifications++;
// Add verified player to the database
final VerifiedPlayer verifiedPlayer = new VerifiedPlayer(user.getInetAddress().toString(), playerUuid,
login.getStart());
Sonar.get().getVerifiedPlayerController().add(verifiedPlayer);
// Call the VerifySuccessEvent for external API usage
Sonar.get().getEventManager().publish(new UserVerifySuccessEvent(username, playerUuid, user, login.delay()));
// Disconnect player with the verification success message
user.disconnect(Sonar.get().getConfig().getVerification().getVerificationSuccess());
FALLBACK.getLogger().info(Sonar.get().getConfig().getVerification().getSuccessLog()
.replace("%name%", username)
.replace("%time%", login.toString()));
}
/**
* Fails the verification if a certain state is unexpected.
*
* @param expectedState Expected state
*/
private void assertState(final @NotNull FallbackUserState expectedState) {
final FallbackUserState state = user.getState();
checkFrame(state == expectedState, "expected " + expectedState + ", got " + state);
}
/**
* Checks if a certain condition is met, fails the verification if not.
*
* @param condition Condition to fail if it's false
* @param message Messages displayed in the stacktrace
*/
private void checkFrame(final boolean condition, final @NotNull String message) {
if (!condition) {
// Only fail the player if we are either not in the POSITION state,
// or if the user configured Sonar to display a CAPTCHA instead of failing.
if (user.getState() != POSITION
|| !Sonar.get().getConfig().getVerification().getGravity().isCaptchaOnFail()) {
user.fail(message);
}
throw new CorruptedFrameException(message);
}
}
}
@@ -0,0 +1,77 @@
/*
* Copyright (C) 2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.netty;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.ReflectiveOperationException;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.SonarPlatform;
import xyz.jonesdev.sonar.common.fallback.FallbackInboundHandler;
import xyz.jonesdev.sonar.common.util.FakeChannelUtil;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.util.function.Consumer;
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_INBOUND_HANDLER;
@RequiredArgsConstructor
public final class FallbackInjectedChannelInitializer extends ChannelInitializer<Channel> {
private static final MethodHandle INIT_CHANNEL_METHOD;
static {
try {
INIT_CHANNEL_METHOD = MethodHandles.privateLookupIn(ChannelInitializer.class, MethodHandles.lookup())
.findVirtual(ChannelInitializer.class, "initChannel", MethodType.methodType(void.class, Channel.class));
} catch (Throwable throwable) {
throw new ReflectiveOperationException(throwable);
}
}
private final @NotNull ChannelInitializer<Channel> originalChannelInitializer;
private final @NotNull Consumer<ChannelPipeline> sonarPipelineInjector;
@Override
protected void initChannel(final Channel channel) throws Exception {
// Invoke the original method
try {
INIT_CHANNEL_METHOD.invokeExact(originalChannelInitializer, channel);
} catch (Throwable throwable) {
throw new ReflectiveOperationException(throwable);
}
// Inject Sonar's channel handler into the pipeline;
// Also make sure the player is not a fake player to avoid compatibility issues
if (channel.isActive() && !FakeChannelUtil.isFakePlayer(channel)) {
final ChannelHandler inboundHandler = new FallbackInboundHandler(sonarPipelineInjector);
// We need to be careful on Bukkit, as the encoder can be different
if (Sonar.get().getPlatform() == SonarPlatform.BUKKIT) {
final String encoder = Sonar.get().getPlatform().getEncoderFunction().apply(channel.pipeline());
channel.pipeline().addBefore(encoder, FALLBACK_INBOUND_HANDLER, inboundHandler);
} else {
channel.pipeline().addFirst(FALLBACK_INBOUND_HANDLER, inboundHandler);
}
}
}
}
@@ -0,0 +1,38 @@
/*
* Copyright (C) 2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.netty;
import io.netty.channel.ChannelDuplexHandler;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import org.jetbrains.annotations.NotNull;
@ChannelHandler.Sharable
public final class FallbackTailExceptionsHandler extends ChannelDuplexHandler {
public static final FallbackTailExceptionsHandler INSTANCE = new FallbackTailExceptionsHandler();
// We can override the default exceptionCaught method since the server
// does not have any other pipelines that could handle this error.
// Additionally, this will also run after our custom decoder.
@Override
public void exceptionCaught(final @NotNull ChannelHandlerContext ctx,
final @NotNull Throwable cause) throws Exception {
// Close the channel if we encounter any errors.
ctx.close();
}
}
@@ -0,0 +1,57 @@
/*
* Copyright (C) 2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.netty;
import io.netty.util.ByteProcessor;
import lombok.Getter;
// Taken from
// https://github.com/PaperMC/Velocity/blob/dev/3.0.0/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/VarintByteDecoder.java
@Getter
final class FallbackVarInt21ByteDecoder implements ByteProcessor {
private int readVarInt, bytesRead;
private DecodeResult result = DecodeResult.TOO_SHORT;
@Override
public boolean process(byte k) {
if (k == 0 && bytesRead == 0) {
result = DecodeResult.RUN_OF_ZEROES;
return true;
}
if (result == DecodeResult.RUN_OF_ZEROES) {
return false;
}
readVarInt |= (k & 0x7F) << bytesRead++ * 7;
if (bytesRead > 3) {
result = DecodeResult.TOO_BIG;
return false;
}
if ((k & 0x80) != 128) {
result = DecodeResult.SUCCESS;
return false;
}
return true;
}
public enum DecodeResult {
SUCCESS,
TOO_SHORT,
TOO_BIG,
RUN_OF_ZEROES
}
}
@@ -0,0 +1,77 @@
/*
* Copyright (C) 2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.netty;
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;
// Taken from
// https://github.com/PaperMC/Velocity/blob/dev/3.0.0/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftVarintFrameDecoder.java
public final class FallbackVarInt21FrameDecoder extends ByteToMessageDecoder {
@Override
protected void decode(final @NotNull ChannelHandlerContext ctx, final ByteBuf in, final List<Object> out) {
if (!ctx.channel().isActive()) {
in.clear();
return;
}
final FallbackVarInt21ByteDecoder reader = new FallbackVarInt21ByteDecoder();
final int end = in.forEachByte(reader);
if (end == -1) {
// We tried to go beyond the end of the buffer.
// This is probably a good sign that the buffer was too short to hold a proper varint.
if (reader.getResult() == FallbackVarInt21ByteDecoder.DecodeResult.RUN_OF_ZEROES) {
// Special case where the entire packet is just a run of zeroes. We ignore them all.
in.clear();
}
return;
}
if (reader.getResult() == FallbackVarInt21ByteDecoder.DecodeResult.RUN_OF_ZEROES) {
// This will return to the point where the next varint starts
in.readerIndex(end);
} else if (reader.getResult() == FallbackVarInt21ByteDecoder.DecodeResult.SUCCESS) {
final int readVarInt = reader.getReadVarInt();
final int bytesRead = reader.getBytesRead();
if (readVarInt < 0) {
in.clear();
throw new CorruptedFrameException("Invalid VarInt length " + readVarInt);
} else if (readVarInt == 0) {
// Skip over the empty packet(s) and ignore it
in.readerIndex(end + 1);
} else {
int minimumRead = bytesRead + readVarInt;
if (in.isReadable(minimumRead)) {
out.add(in.retainedSlice(end + 1, readVarInt));
in.skipBytes(minimumRead);
}
}
} else if (reader.getResult() == FallbackVarInt21ByteDecoder.DecodeResult.TOO_BIG) {
in.clear();
throw new CorruptedFrameException("Too big VarInt " + reader.getReadVarInt());
}
}
}
@@ -0,0 +1,50 @@
/*
* Copyright (C) 2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.netty;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;
import org.jetbrains.annotations.NotNull;
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.varIntBytes;
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.writeVarInt;
// Taken from
// https://github.com/PaperMC/Velocity/blob/dev/3.0.0/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftVarintLengthEncoder.java
@ChannelHandler.Sharable
public final class FallbackVarInt21FrameEncoder extends MessageToByteEncoder<ByteBuf> {
public static final FallbackVarInt21FrameEncoder INSTANCE = new FallbackVarInt21FrameEncoder();
@Override
protected void encode(final @NotNull ChannelHandlerContext ctx,
final @NotNull ByteBuf msg,
final @NotNull ByteBuf out) throws Exception {
writeVarInt(out, msg.readableBytes());
out.writeBytes(msg);
}
@Override
protected ByteBuf allocateBuffer(final @NotNull ChannelHandlerContext ctx,
final @NotNull ByteBuf msg,
final boolean preferDirect) throws Exception {
final int anticipatedRequiredCapacity = varIntBytes(msg.readableBytes()) + msg.readableBytes();
return ctx.alloc().buffer(anticipatedRequiredCapacity);
}
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023-2024 Sonar Contributors
* Copyright (C) 2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023-2024 Sonar Contributors
* Copyright (C) 2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -21,29 +21,29 @@ import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.handler.codec.CorruptedFrameException;
import lombok.Setter;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_20_2;
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry.Direction.SERVERBOUND;
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry.GAME;
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry.LOGIN;
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.readVarInt;
import static xyz.jonesdev.sonar.common.util.ProtocolUtil.readVarInt;
public final class FallbackPacketDecoder extends ChannelInboundHandlerAdapter {
private final FallbackUser user;
private final FallbackPacketListener listener;
private final ProtocolVersion protocolVersion;
private FallbackPacketRegistry.ProtocolRegistry registry;
@Setter
private FallbackPacketListener listener;
public FallbackPacketDecoder(final @NotNull FallbackUser user,
final @NotNull FallbackPacketListener listener) {
this.user = user;
this.listener = listener;
updateRegistry(user.getProtocolVersion().compareTo(MINECRAFT_1_20_2) >= 0 ? LOGIN : GAME);
public FallbackPacketDecoder(final @NotNull ProtocolVersion protocolVersion) {
this.protocolVersion = protocolVersion;
updateRegistry(protocolVersion.compareTo(MINECRAFT_1_20_2) >= 0 ? LOGIN : GAME);
}
public void updateRegistry(final @NotNull FallbackPacketRegistry registry) {
this.registry = registry.getProtocolRegistry(SERVERBOUND, user.getProtocolVersion());
this.registry = registry.getProtocolRegistry(SERVERBOUND, protocolVersion);
}
@Override
@@ -52,45 +52,42 @@ public final class FallbackPacketDecoder extends ChannelInboundHandlerAdapter {
if (msg instanceof ByteBuf) {
final ByteBuf byteBuf = (ByteBuf) msg;
// Release the ByteBuf if the connection is not active
// or the ByteBuf doesn't contain any data to avoid
// memory leaks or other potential exploits.
if (!ctx.channel().isActive() || !byteBuf.isReadable()) {
byteBuf.release();
return;
}
final int originalReaderIndex = byteBuf.readerIndex();
// Read the packet ID and then create the packet from it
final int packetId = readVarInt(byteBuf);
final FallbackPacket packet = registry.createPacket(packetId);
// If the packet isn't found, skip it
if (packet == null) {
byteBuf.readerIndex(originalReaderIndex);
return;
}
try {
// Release the ByteBuf if the connection is not active
// or the ByteBuf doesn't contain any data to avoid
// memory leaks or other potential exploits.
if (!ctx.channel().isActive() || !byteBuf.isReadable()) {
return;
}
// Read the packet ID and then create the packet from it
final int packetId = readVarInt(byteBuf);
final FallbackPacket packet = registry.createPacket(packetId);
// Skip the packet if it's not registered within Sonar's packet registry
if (packet == null) {
return;
}
// Ensure that the packet isn't too large or too small
doLengthSanityChecks(byteBuf, packet);
checkPacketSize(byteBuf, packet);
try {
// Try to decode the packet for the given protocol version
packet.decode(byteBuf, user.getProtocolVersion());
packet.decode(byteBuf, protocolVersion);
} catch (Throwable throwable) {
user.fail("failed to decode packet (" + byteBuf.readableBytes() + " bytes)");
throw new CorruptedFrameException("Failed to decode packet");
}
// Check if the packet still has bytes left after we decoded it
if (byteBuf.isReadable()) {
user.fail("could not read packet to end (" + byteBuf.readableBytes() + " bytes left)");
throw new CorruptedFrameException("Could not read packet to end");
}
// Let our verification handler process the packet
listener.handle(packet);
if (listener != null) {
listener.handle(packet);
}
// Fire channel read to avoid timeout
ctx.fireChannelRead(packet);
@@ -101,17 +98,15 @@ public final class FallbackPacketDecoder extends ChannelInboundHandlerAdapter {
}
}
private void doLengthSanityChecks(final ByteBuf byteBuf,
final @NotNull FallbackPacket packet) throws Exception {
final int expectedMaxLen = packet.expectedMaxLength(byteBuf, user.getProtocolVersion());
private void checkPacketSize(final @NotNull ByteBuf byteBuf,
final @NotNull FallbackPacket packet) throws Exception {
final int expectedMaxLen = packet.expectedMaxLength(byteBuf, protocolVersion);
if (expectedMaxLen != -1 && byteBuf.readableBytes() > expectedMaxLen) {
user.fail("packet too large (" + byteBuf.readableBytes() + " bytes)");
throw new CorruptedFrameException("Packet too large");
}
final int expectedMinLen = packet.expectedMinLength(byteBuf, user.getProtocolVersion());
if (byteBuf.readableBytes() < expectedMinLen) {
user.fail("packet too small (" + byteBuf.readableBytes() + " bytes)");
final int expectedMinLen = packet.expectedMinLength(byteBuf, protocolVersion);
if (expectedMinLen != -1 && byteBuf.readableBytes() < expectedMinLen) {
throw new CorruptedFrameException("Packet too small");
}
}
Loaded 100 of 225 files, more files were not shown because too many files have changed in this diff. Show more