Compare commits

...
318 Commits
Author SHA1 Message Date
Jones def40be5c0 Release 2.1.3 (#369) 2024-08-28 15:33:53 +02:00
Jones d4bdf86fcb Fix NullPointerException 2024-08-28 15:28:10 +02:00
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
Jones 4474f44b0a Release 2.0.26 (#269) 2024-04-20 10:14:13 +02:00
Jones b159947d6d Recode max accounts per IP check (#268) 2024-04-18 18:39:23 +02:00
Jones a29dfa15e4 Implement max accounts per IP check on Bukkit (#266)
* feat(bukkit): add max online per IP check

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

* fix: remove useless and stupid local ip check

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

* feat: remove obsolete audience listener on Velocity

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

* feat: microoptimize InvocationSource#isPlayer

* build: recode and restructure dependency management

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

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

* build: fix minimizing breaking kyori adventure

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

* build(bukkit): mark as folia supported

* build: recode `build-sonar` task

* fix: remove unused/outdated files

* chore: I think I was dead when writing this

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

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

* style: add link to stackoverflow comment explaining memory formatting

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

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

* chore: update bot check failed message

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

* feat: remove unnecessary max-ping option

* feat: [wip] vehicle check and other improvements

* feat: add RemoveEntitiesPacket

* fix: RemoveEntitiesPacket for 1.7

* fix: PlayerInputPacket for 1.7

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

* fix: RemoveEntitiesPacket not working on 1.17

* fix: boat entity type being broken on 1.13

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

* fix: boat entity type for 1.13-1.13.2

* feat: add missing vehicle configurations

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

* fix: remove unnecessary flush in KeepAlive

* fix: only expect PlayerInput packets when needed
2024-03-30 19:16:27 +01:00
dependabot[bot] b3a7f4de09 Bump netty-all from 4.1.107.Final to 4.1.108.Final (#248)
Bumps [io.netty:netty-all](https://github.com/netty/netty) from 4.1.107.Final to 4.1.108.Final.
- [Commits](https://github.com/netty/netty/compare/netty-4.1.107.Final...netty-4.1.108.Final)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-25 13:45:29 +01:00
dependabot[bot] 5752f1e208 Bump lombok from 1.18.30 to 1.18.32 (#249)
Bumps [org.projectlombok:lombok](https://github.com/projectlombok/lombok) from 1.18.30 to 1.18.32.
- [Changelog](https://github.com/projectlombok/lombok/blob/master/doc/changelog.markdown)
- [Commits](https://github.com/projectlombok/lombok/compare/v1.18.30...v1.18.32)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-25 13:45:16 +01:00
MISHA 1c6ba7050d Add a missed line to Russian translations (#247) 2024-03-24 20:55:35 +01:00
MISHA 340d4a225d Add the missing Russian language in messages (#245)
* Update ru.yml

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

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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-11 19:11:17 +01:00
Jones e15206a382 Implement CAPTCHA-on-fail option (#237)
* feat: add option to display a CAPTCHA on fail

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

* fix: use correct char ids for bStats

* fix: implement shutdown method to correctly stop certain services

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

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

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

* fix: use pooled connection source to remove unnecessary reflection

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

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

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

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

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

* fix: the channel is always closed/unusable if there's no decoder
2024-02-19 20:05:00 +01:00
dependabot[bot] 5b62396f9b Bump netty-all from 4.1.106.Final to 4.1.107.Final (#203)
Bumps [io.netty:netty-all](https://github.com/netty/netty) from 4.1.106.Final to 4.1.107.Final.
- [Commits](https://github.com/netty/netty/compare/netty-4.1.106.Final...netty-4.1.107.Final)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-19 13:50:20 +01:00
Jones 23a472d3aa Release 2.0.18 (#202) 2024-02-18 16:26:17 +01:00
Jones 76f0f4abda Recode and optimize bandwidth counting (#201)
* feat: optimize bandwidth counting by recoding the channel handler

* fix: verification breaking new connections if disabled

* fix: add exception handlers to ensure that no error can reach the server
2024-02-18 16:16:11 +01:00
Jones 720e1cb1cb Fix incompatibility with Velocity 3.3.0 b360+ (#200) 2024-02-18 14:48:09 +01:00
Jones a98d9337c7 Fully recode injection system (#199)
* feat(all): fully recode injection technique

* fix: issues with proxy protocol enabled
2024-02-17 20:25:19 +01:00
Jones 5158111edf Implement automatic language system (#197)
* feat: [wip] better configuration system and better migration system

* feat: [wip] French translations for config.yml and webhook.yml

* fix(api): improve error handling if the config is invalid

* feat: implement better language detection system

* feat: allow user control over languages

* chore: use config logger in other config-related classes as well

* fix: only load MySQL driver if database is being used

* refactor: database url -> database host

* feat: [wip] add Russian translations for config & webhook (credits to @Nullmx/"nullme" on Discord)

* feat: [wip] add Czech translations for config & webhook (credits to @slosa)

* feat: [wip] add Georgian translations for config & webhook (credits to @Chumb3x)

* fix: missing Georgian translation in config/ka.yml

* feat: add Russian messages translation (credits to @Nullmx)

* feat: [wip] add Turkish translation for messages.yml (thanks to @AyazJenkins)

* fix: MySQL driver not being loaded correctly

* fix: some API calls not working because of caffeine

* feat: warn about translations not changing configured values
2024-02-17 16:50:17 +01:00
Jones 2ab829b43e Outline API methods and remove duplicate code (#193) 2024-02-10 20:45:43 +01:00
Jones 04bbfde6b6 Improve collision checks (#192)
* fix(api): edit misleading comment in configuration

* fix: make client brand check toggleable

* fix: only send abilities if needed

* feat: allow teleport ids to be negative

* fix(fallback): add a little randomization to spawn y position

* refactor: move map captcha packet handling to different method

* fix: actually fix 1.7 collisions properly

* feat: implement another simple and strict collision check

* feat: allow more control over the verification steps

* fix(api): hide verification failed logs during attacks if enabled
2024-02-10 19:47:42 +01:00
Jones ef8abcec46 Release 2.0.17 (#191) 2024-02-10 11:23:13 +01:00
Jones 1fb38ec8d4 Revert 343d71d18c (#190) 2024-02-10 00:14:02 +01:00
Jones cc5e958e2a Fix issues with the Geyser standalone proxy (#189) 2024-02-09 21:48:02 +01:00
Jones 1437204a15 Fix incompatibility with Velocity builds 347+ (#188) 2024-02-08 17:54:52 +01:00
Jones c96d733920 Use Caffeine cache for blacklist threshold (#187) 2024-02-07 10:29:22 +01:00
Jones c561b3eeaf Fix command delay not working correctly (#186) 2024-02-07 09:13:30 +01:00
Jones 2310864816 Remove deprecated API functions (#185) 2024-02-06 18:41:44 +01:00
Jones 55030c5073 Make blacklist time configurable (#184)
* feat: allow user to customize/configure blacklist time

* feat: implement customizable blacklist threshold

* feat: use caffeine for caching
2024-02-05 15:38:44 +01:00
Jones 18bb9557b1 Release 2.0.16 (#183) 2024-01-28 15:00:42 +01:00
Jones 5d02050815 Load Sonar after some other plugins for compatibility (#182) 2024-01-26 15:06:29 +01:00
Jones 5914b23ae5 Fix relocations breaking dynamic library loading (#180) 2024-01-26 14:58:23 +01:00
Jones 7ce56a79ab Remove unused relocations (#178) 2024-01-25 21:39:51 +01:00
Jones 7b17ba7e53 Bump adventure-platform to 4.3.2 (#177) 2024-01-25 20:24:10 +01:00
AkkariinandMichel Elkenwaat 8268cda660 Recode packet handling on BungeeCord (#174)
* feat: switch to HandlerBoss instead of using InitialHandler.

* feat: recode channel wrapper initialization structure

* fix: remove unnecessary/duplicate methods

* fix: update initial BungeeCord methods (check upstream)

---------

Co-authored-by: Michel Elkenwaat <admin@jonesdev.xyz>
2024-01-25 19:28:37 +01:00
Jones 89d0c7346f Use scheduler instead of custom threads for services (#176)
* fix(bungee): kyori title being broken on BungeeCord

* fix(all): use server's scheduler instead of own services

* fix(velocity): tasks not being scheduled correctly

* fix(velocity): too-many-online-per-IP check breaking on 1.20.2+
2024-01-23 19:36:59 +01:00
Jones 512faa956e Fix protocol check not using the correct disconnect method (#168)
Related to https://github.com/jonesdevelopment/sonar/issues/167
2024-01-22 21:12:02 +01:00
Jones 1f6781aaa3 Load all libraries dynamically during runtime (#166)
* feat: use libby for dynamic dependency loading

* fix: MySQL driver not working with new library loading

* style: remove debug messages

* style: make setup and invoke void static
2024-01-22 20:35:22 +01:00
Jones bf40333de5 Fix incompatibility with LibreLogin on BungeeCord (#164) 2024-01-22 14:41:23 +01:00
dependabot[bot] 31b961b577 Bump netty-all from 4.1.104.Final to 4.1.106.Final (#162)
Bumps [io.netty:netty-all](https://github.com/netty/netty) from 4.1.104.Final to 4.1.106.Final.
- [Commits](https://github.com/netty/netty/compare/netty-4.1.104.Final...netty-4.1.106.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-01-22 13:52:22 +01:00
Jones e4821781fa Make /sonar notify only executable by players (#161) 2024-01-21 15:05:23 +01:00
Jones bdcb7d28a7 Fix broken disconnect packets during login on 1.20.2+ (#160) 2024-01-21 14:51:05 +01:00
Jones 433fab10f1 Implement chat and title attack notifications (#158)
* feat: move audiences outside the verbose handler

* build(deps): only relocate audience class instead of package

* feat: implement chat and title notifications

* feat: allow chat notifications to contain traffic
2024-01-21 14:44:52 +01:00
Jones 014b956f9e Fix compiler errors since Velocity 3.3.0-329 (#157) 2024-01-20 11:42:27 +01:00
Jones 4be1a970d0 Remove some duplicate code in the API (#156)
* fix(api): remove duplicate code in FallbackUser

* style(api): schedule FallbackUser#disconnect(String) for removal
2024-01-16 17:19:19 +01:00
Jones bea1d292ee Release 2.0.15 (#154) 2024-01-11 18:30:20 +01:00
Jones 468a11e0be Fix Geyser profiles by reverting parts of the new injection (#153)
* fix: check for Geyser by using the port instead of channel attribute

* feat: revert some of https://github.com/jonesdevelopment/sonar/commit/a66f60f1773ab517ec786ba264313f0d0ad7ed76 to fix https://github.com/jonesdevelopment/sonar/issues/152
2024-01-11 18:21:38 +01:00
Jones cf2fce9c29 Run DB cleaning and caching in the same thread (#151) 2024-01-07 17:22:35 +01:00
Jones cc52289500 Release 2.0.14 (#149) 2024-01-06 14:55:30 +01:00
Jones ac1f6d616b Reword and simplify the README (#148) 2024-01-02 21:26:04 +01:00
Jones bdd941171e Use correct security contact email (#147) 2024-01-02 21:09:25 +01:00
Jones 1e18a29d1e Update license year to 2023-2024 (#146) 2024-01-02 18:17:20 +01:00
Jones 5671c2029e Change default color scheme in messages (#145) 2024-01-01 23:55:33 +01:00
Jones 32aeee4f06 Fix license link in README (#144) 2023-12-31 18:10:28 +01:00
Jones 7b5ce41420 Show server uptime in statistics (#143)
* style: small code clean up

* feat: add server uptime to statistics
2023-12-30 21:44:16 +01:00
Jones a66f60f177 Recode Velocity injection (#142)
* feat(bungee): implement empty hostname check

* feat: recode Velocity injection

* feat: recode action bar counters

* fix(bungee): make sure the connection is open
2023-12-30 21:24:53 +01:00
Jones 326a1ecc07 Release 2.0.13 (#140) 2023-12-30 15:57:53 +01:00
Jones 6af5a9fc4b Make sure the workflow uses Java 17 (#139) 2023-12-30 01:17:55 +01:00
Jones ac78d88721 Bump Velocity API from 3.2.0 to 3.3.0 (#138) 2023-12-30 01:12:27 +01:00
Jones 75abebde7c Release 2.0.12 (#136) 2023-12-25 14:24:08 +01:00
Jones 28b1615b78 Implement maximum age to verified players in database (#134) 2023-12-21 22:27:26 +01:00
Jones 600275669b Bump gradle from 8.4 to 8.5 (#133) 2023-12-20 14:41:21 +01:00
Jones 0c3761be11 Fix event priorities for some event listeners (#132)
* fix(bungee): use highest event priority for audience listener

* fix(bungee): use lowest event priority for fallback listener

* fix(bukkit): use highest event priority for audience listener
2023-12-19 16:36:13 +01:00
Jones b7d8ec74d0 Bump adventure from 4.14.0 to 4.15.0 (#131) 2023-12-19 16:26:24 +01:00
dependabot[bot] 96efa574e0 Bump netty-all from 4.1.101.Final to 4.1.104.Final (#130)
Bumps [io.netty:netty-all](https://github.com/netty/netty) from 4.1.101.Final to 4.1.104.Final.
- [Commits](https://github.com/netty/netty/compare/netty-4.1.101.Final...netty-4.1.104.Final)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-19 15:49:58 +01:00
Jones 8db3d0257f Release 2.0.11 (#129) 2023-12-17 14:50:36 +01:00
Jones 106a44f78a Recode map captcha so it's harder for AI to solve (#128)
* feat: make map captcha harder for AI to solve

* feat: make distortions configurable

* fix: use correct distortions factor dividor

* fix: memory issues with map precomputing/preparing

* fix: characters sometimes ended up out of the frame

* fix: don't randomize x position of the character

* fix: font size issue

* feat: optimize FallbackChannelHandler

* feat: make captcha timing configurable

* fix: 1.7 and 1.8 clients dying because of my stupidity
2023-12-17 14:47:42 +01:00
Jones a5872f1b6e Fix Chat packet using the wrong packet id in 1.20.2 (#127) 2023-12-15 17:54:22 +01:00
Jones e9c212b9e4 Release 2.0.10 (#126) 2023-12-15 16:02:44 +01:00
Jones 42da141f2b Remove lockdown mode (#125)
* https://discord.com/channels/923308209769426994/1180500530896842832
2023-12-15 15:49:50 +01:00
Jones e0bc83f8ef Implement Minecraft 1.20.4 support (#124)
* feat: implement GameEvent packet

* fix: make Chat & Disconnect packet work on 1.20.3+

* fix: don't send ChunkData twice

* fix: invalid serverbound PositionLook packet id on 1.20.3+

* fix: remove custom read-timeout value

* fix: trigger channel read after Fallback packet decode

* fix: only expect 10 unique kick packets instead of 16

* fix: map item id mapping

* feat: 1.20.3 barrier block mappings
2023-12-15 15:44:14 +01:00
Jones 59822ef541 Optimize in-class performance during verification (#123)
* style: use Getter for the entire class instead of a single field

* style: reformat code using IntelliSense

* fix: optimize IP address connection ratelimiter
2023-12-07 21:16:20 +01:00
Jones 38fbfff896 Fix README badge line separator not displaying (#122) 2023-12-07 21:00:18 +01:00
Jones 6725975cf1 Add CodeFactor rating to README (#121)
* feat: add 1.20.3 protocol version to allowed protocols

* feat: update packet ids for 1.20.3

* chore: add code factor rating to README
2023-12-07 20:57:41 +01:00
Jones d59fd4b431 Avoid using custom exceptions in the decoder (#119) 2023-12-04 16:48:28 +01:00
Jones a76ca2f91d Add optional map captcha check to verification (#118)
* feat: change field type "encodedFlags" of Abilities packet to int

* feat: add MapData packet
feat: add SetSlot packet

* feat: implement map captcha state

* feat: [wip] implement simple map captcha

* feat: cache captcha spawn position packet

* feat: add a chat message when you verify and enter the captcha

* fix: Chat packet error on 1.7

* feat: make map prepared amount and dictionary customizable

* fix: SetSlot packet error on 1.7

* fix: make map data work on 1.7

* feat: randomize font size & font style

* fix: implement KeepAlive during captcha and add timer

* feat: add color and x/y randomization to map captcha

* feat: add green and red color palette

* feat: implement captcha validation

* fix: chat packet not working on 1.7-1.18.2

* fix: only draw important used pixels

* feat: optimize color palette

* feat: set map image dimensions to 86x86

* feat: set background color of image to white/light gray

* feat: recode map info preparing and map data packet sending

* fix: x/y offset for 1.8+ in map info

* fix: add prefix to translated messages for the map captcha

* feat: make some map captcha options changeable

* fix: set the default font size to the average (instead of max)

* fix: Chat packet not working on 1.17-1.18.1

* fix: only send DefaultSpawnPosition to >=1.18.2 clients

* fix: return if captcha is correct to prevent further code execution

* feat: cache dynamic DefaultSpawnPosition packet
2023-12-02 14:13:30 +01:00
Jones 30803ba6e4 Fix permanent "Loading terrain" screen on 1.18.2+ (#117)
* feat: remove unused Chat packet

* feat: set JoinGame entityId to 0

* fix: add DefaultSpawnPosition to fix "Loading terrain" screen

* fix: 1.20.2 barrier block state id
2023-11-28 17:30:12 +01:00
Jones 194232804b Remove unnecessary metadata in the compilation process (#116)
* build: exclude unnecessary version information

* build: remove unnecessary file timestamps
2023-11-25 16:48:28 +01:00
Jones 19c97b26b6 Relocate some packages of net.kyori (#115)
* build(deps): relocate cappuccino

* build(deps): relocate net.kyori (except for text and audience)
2023-11-25 16:23:31 +01:00
Jones d66719b3e0 Fix state checks in verification not being handled properly (#114) 2023-11-24 21:06:19 +01:00
Michel Elkenwaat 9226908002 style: add copyright header to UpdateChecker.java 2023-11-24 19:37:35 +01:00
Jones ac74f14dc0 Make RegistrySync packet use the implemented nbt writing method (#112) 2023-11-23 17:54:53 +01:00
Michel Elkenwaat 8f11a53b38 fix: use ByteBufOutputStream in an AutoClosable statement 2023-11-23 17:54:21 +01:00
Jones 0b4394624f Release 2.0.9 (#111) 2023-11-22 15:56:26 +01:00
Michel Elkenwaat d8f7e288e8 build(deps): bump gradle from 8.3 to 8.4 2023-11-21 18:18:35 +01:00
Jones 3f3411ca9d Implement nameless compound tag for 1.20.2+ chunks (#110) 2023-11-21 17:41:59 +01:00
Jones 14ae85384a Optimize verification by using different data types (#109)
* fix: optimize ConcurrentHashMap usage in Fallback

* feat: remove max-verifying-players option from config

* fix: use hashset for whitelist and blacklisted protocol ids

* fix: yaml configuration formatting error on empty collections
2023-11-16 16:58:30 +01:00
Jones 8204477eb6 Implement protocol version whitelist and blacklist (#108)
* feat: allow certain protocol Ids to be exempt from verification

* fix(bungee): run blacklist check even if verification is disabled

* fix: run Geyser check before creating a new GameProfile

* feat: the blacklist should cache the InetAddress and not a String

* fix: support IPv6 (and more) in all subcommand arguments

* feat: add blacklisted protocol versions
2023-11-16 16:39:35 +01:00
Jones 44754e5027 Use correct values for webhook attack statistics (#107) 2023-11-14 12:01:05 +01:00
dependabot[bot] 847936ed08 Bump io.netty:netty-all from 4.1.100.Final to 4.1.101.Final (#105)
Bumps [io.netty:netty-all](https://github.com/netty/netty) from 4.1.100.Final to 4.1.101.Final.
- [Commits](https://github.com/netty/netty/compare/netty-4.1.100.Final...netty-4.1.101.Final)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-13 14:06:47 +01:00
Jones aa90a1fa22 Release 2.0.8 (#104) 2023-11-10 14:09:55 +01:00
Jones 9ac8447226 Fix Geyser exemption sometimes skipping Java players (#103) 2023-11-10 14:07:21 +01:00
Jones 6b6acb8371 Rename MySQL table to avoid naming collisions (#102)
* fix: rename MySQL table from "verified_players" to "sonar_verified_players"

* feat: change timestamp field type to datetime (sql timestamp)

* feat(db): allow empty passwords
2023-11-07 18:02:38 +01:00
Jones f3af903114 Fix MySQL driver issues on BungeeCord and Bukkit (#101)
* fix(bungee, bukkit): MySQL not loading correctly

* style: fix link to a credited class

* fix(db): don't log anything if connectionSource is null
2023-11-07 17:19:58 +01:00
Jones 67bd3786d7 Optimize some parts of the code (#100)
* build: simplify main build.gradle.kts

* build(deps): relocate org.simpleyaml

* reformat(all): code clean up using IntelliJ's automatic tool

* fix: optimize packet registry by removing unused statements

* fix: resolve some grammar mistakes in comments

* fix: optimize memory formatting by a bit
2023-11-07 16:40:50 +01:00
Jones 2b379b0dfa Fix NullPointerException in BungeeCord HandlerBoss on 1.20.2 (#98)
* fix: BungeeCord HandlerBoss only storing the channel wrapper locally, therefore, causing a NPE

* fix(bungee): mark channel as closed when it's inactive
2023-11-06 17:54:14 +01:00
Jones b81cc0e9f5 Compress NBT dimension codecs with GZIP (#97)
* feat: compress nbt dimension codecs with GZIP with snbt2nbt

* refactor: mappings -> codecs

* feat: remove credits since I'm using my own program the nbt codecs now
2023-11-04 23:51:49 +01:00
Jones 0efc6ae157 Recode some of the verification and improve networking (#96)
* feat: implement block state id from https://github.com/PrismarineJS/minecraft-data/pull/786

* feat: recode dimension nbt codec for JoinGame packet

* feat(fallback): recode gravity/block collisions check

* fix: remove unnecessary debug message

* fix: dynamicSpawnYPosition using a set Y buffer

* feat: refactor some verification-related configuration values

* fix: forEnumConstant() not saving default config values

* fix: remove Geyser characters from valid name regex

* feat: apply migration fix for every config string

* fix: don't do .toUpperCase() for every string

* feat: add more description to some enum config types

* fix: move "recommended" to listing in comment

* fix: remove mc association before queuing the connection

* refactor: closed -> knownDisconnect

* fix: incompatibility with latest BungeeCord

* refactor: fallbackPlayer -> user

* fix: BungeeCord kick message serialization not working
2023-11-04 22:19:54 +01:00
Jones 4ae14c58a6 Use a different verbose layout during active attacks (#95)
* feat: implement separate action bar during attack

* fix: logins per second not being displayed correctly

* feat: set action bar refresh delay to 200ms instead of 125ms
2023-11-04 15:43:38 +01:00
Jones 3bb752f435 Recode command and subcommand execution logic (#94) 2023-11-02 17:51:56 +01:00
Jones eeb9a446fb Run custom Sonar API events asynchronously (#93)
* feat: run all custom Sonar events asynchronously

* fix: handle potential errors within the event executor service
2023-11-02 17:31:30 +01:00
Jones dfe728f7a9 Implement a custom asynchronous service system (#92)
* feat: use own asynchronous service repository

* feat: also use a custom executor for the verbose handler
2023-11-02 17:20:39 +01:00
Jones 3945e813dc Allow verification to only be active during attacks (#90)
* refactor: AttackStatus -> AttackTracker

* feat: implement different verification timing types (always, never, during_attack)

* fix: invalid suppression warnings

* fix: don't collect total stats async as it could mess with the values
2023-11-02 16:55:24 +01:00
Jones 5068ebcd91 Release 2.0.7 (#89) 2023-11-01 13:48:08 +01:00
Jones f20d162dcc Fix Geyser exemption not working on the standalone variant (#88)
* Revert "https://github.com/jonesdevelopment/sonar/pull/87/commits/648489b01bafcaf711316228fac6d93eac5eba2c"

* fix: avoid unnecessary performance loss by removing the floodgate check

* fix: move UserVerifyFailedEvent publishing after channel active check

* fix: make UserVerifyFailedEvent reason optional

* fix: use final field for UserVerifyFailedEvent reason

* fix: use channel attribute to detect floodgate players
2023-10-31 15:49:01 +01:00
Jones acc9df0b8d Recode attack detection algorithm to avoid spam (#87)
* feat: use floodgate API to determine if a player is joining through Geyser or not

* fix: normal floodgate players not being detected

* fix: Discord webhook spam

* feat: update value for minimum players for attack

* feat: make minimum attack duration configurable

* feat: implement attack start and stop thresholds

* fix: discord webhook embed description not being reusable

* feat: recode Discord webhook post timing algorithm
2023-10-28 22:37:20 +02:00
Jones 3636674eff Implement Discord webhook notifications (#86)
* fix: possible (unconfirmed) concurrency issues

* fix: don't ignore auto-closeable in UpdateChecker

* feat: [wip] add Discord Webhook support

* feat: recode attack detection system
feat: add AttackDetectedEvent, AttackMitigatedEvent
feat: allow more control over attack detection behavior

* fix: run most of the webhook attack handling async

* fix: reset discord webhook if they are disabled after reload

* fix: don't log too-short attacks

* fix: duration formatting showing milliseconds

* feat: implement webhook embed message footer

* fix: change misleading configuration comment
2023-10-28 15:04:44 +02:00
Jones d2b3b0adae Handle database errors instead of just throwing exceptions (#84)
* fix(db): handle the error instead of simply throwing an exception

* feat(db): revert support for multiple dependencies being loaded at once
2023-10-28 12:02:35 +02:00
Jones 194c7645ed Cache BungeeCord kick packets during verification (#82) 2023-10-28 01:32:46 +02:00
Jones 860594400a Implement max-online-per-ip check on BungeeCord (#81)
* feat(bungee): implement missing max online per ip check

* fix(bungee): use correct component serialization for the lockdown notification message

* fix(bungee): use correct disconnect message for max online per ip check

* fix(bungee): add 1 to current online per ip count because the player joining isn't counted

* fix(bungee): use adventure audience for sending the message
2023-10-28 01:20:25 +02:00
Jones 20f5b135aa Fix lockdown notification not working as intended (#80)
* fix: lockdown notification kicking administrators

* fix: lockdown notification not being deserialized properly
2023-10-28 01:04:01 +02:00
Jones ef31dd4c87 Release 2.0.6 (#77) 2023-10-25 17:42:59 +02:00
Jones 8361f1990d Implement BungeeCord support (#76)
* feat: remove Velocity's (de-)compression dependency

* feat: [wip] implement Fallback for BungeeCord

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

* fix(bungee): NPE on handshake

* fix: use correct protocol version for channel initialization

* feat: implement login packet spam fix

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

* feat: implement cleaner FallbackHandlerBoss

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

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

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

* fix(bungee): total traffic statistic not working

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

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

* style: fix FallbackChannelHandler comments

* style: don't annotate FallbackUserWrapper as EqualsAndHashCode

* style: fix ToString annotation in EmptyChunkData

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

* feat: use expiring cache for joins per second counter

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

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

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

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

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

* feat: remove /sonar verbose [other player]

* feat: remove ServerWrapper and replace it with the platform

* feat: reset action bar when unsubscribing from verbose

* feat: recode tab suggestions and help message caching

* feat: make Sonar main command translatable/configurable

* feat: don't cache copyright year

* fix: tab suggestions breaking after the first iteration

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

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

* style: improve codestyle in YAML configuration loader

* fix: argumentsRequired property not working at all

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

* fix: incorrect motion blocking root tag in EmptyChunkData packet (https://github.com/jonesdevelopment/sonar/issues/68)
2023-10-20 14:19:53 +02:00
Jones 971d43df00 Recode IPv4 regex pattern check (#67) 2023-10-19 17:03:52 +02:00
Jones 1671ccac55 Release 2.0.4 (#66) 2023-10-18 16:38:22 +02:00
dependabot[bot] 8e03bdac5f build(deps): bump io.netty:netty-all from 4.1.99.Final to 4.1.100.Final (#64)
Bumps [io.netty:netty-all](https://github.com/netty/netty) from 4.1.99.Final to 4.1.100.Final.
- [Commits](https://github.com/netty/netty/compare/netty-4.1.99.Final...netty-4.1.100.Final)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-18 16:35:22 +02:00
Jones 7c0bdc568e Continuation of the integration of 1.20.2 (#65)
* style(all): general code reformat

* feat: remove ActiveFeatures packet

* refactor: create packages for each state

* feat: fully recode JoinGame and RegistrySync packets

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

* fix: encoder/decoder state not being set properly

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

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

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

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

* feat: add todo for 1.20.2 barrier block mappings

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

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

* feat: add already-online disconnect message translation

* feat: implement 1.20.2 support for JoinGame packet

* feat: add new packets for 1.20.2 config state

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

* fix: implement backwards compatibility for Velocity b265 and below

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

* feat: make lockdown bypass permission customizable

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

* feat: [wip] implement config protocol state

* style: add comments for documenting FallbackPacketDecoder

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

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

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

* fix: invalid configuration path for certain settings

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

* feat: update configuration yaml file header

* fix: smaller configuration fixes and relocations

* feat: migrate messages to modern MiniMessage formatting

* fix: verbose not deserializing the component properly

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

* feat: remove unused postReload task/method

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

* feat: make /sonar stats completely configurable

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

* fix: use correct translation for /sonar stats cpu

* style: remove resolved todo comment

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

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

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

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

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

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

This reverts commit 6b092100befa423272f1eb310f5d1f8a0e9afb82.

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

* feat: update several subcommand descriptions to be more accurate

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

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

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

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

* feat: optimize/improve Fallback packet preparing

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

* fix: optimize 1.7 support in UpdateSectionBlocks packet

* style: improve codestyle of the UpdateSectionBlocks packet

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

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

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

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

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

* refactor: setAbilitiesAndTeleport -> sendAbilitiesAndTeleport

* style: fix fields being named incorrectly

* feat: cache 1.7 empty chunk filler bytes

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

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

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

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

* fix: false positive with invalid movement ticks

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

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

* fix: relocate some packages to avoid classes being loaded twice
2023-10-04 20:48:55 +02:00
260 changed files with 17589 additions and 6885 deletions

No files matched your search

-1
View File
@@ -8,7 +8,6 @@ joining the [Discord](https://jonesdev.xyz/discord).
2. Follow the coding guidelines for your code.
3. Test your code before committing and pushing it.
4. Use [semantic](https://gist.github.com/joshbuchea/6f47e86d2510bce28f8e7f42ae84c716) commit messages for your commits.
5. Create a feature branch or push to the `dev` branch.
## Coding Guidelines:
+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.
+20 -51
View File
@@ -1,20 +1,19 @@
<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)
[![](https://img.shields.io/github/v/release/jonesdevelopment/sonar)](https://github.com/jonesdevelopment/sonar/releases)
[![](https://img.shields.io/github/issues/jonesdevelopment/sonar)](https://github.com/jonesdevelopment/sonar/issues)
[![](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)
|
@@ -24,71 +23,41 @@
|
[Discord](https://jonesdev.xyz/discord)
|
[License](https://github.com/jonesdevelopment/sonar/blob/main/README.md#license)
[License](https://github.com/jonesdevelopment/sonar/?tab=readme-ov-file#license)
</div>
## Design and Goal
* Effective, lightweight, and easy-to-use
* No unnecessary features and clean code
* Protection against all kinds of bot attacks
* No player should be annoyed by any sort of captcha
* No player should be annoyed by any sort of [CAPTCHA](https://en.wikipedia.org/wiki/CAPTCHA)
* No sort of checking for VPNs or proxies
* Multi-platform support (Velocity, BungeeCord, and Bukkit)
* 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.
- New player logins are queued to prevent spam login attacks.
- [Fallback](https://github.com/jonesdevelopment/sonar#fallback) is Sonar's main component designed to prevent
all types of bots.
- Spigot & BungeeCord
- The compression method is changed
to [Velocity's libdeflate](https://github.com/PaperMC/Velocity/tree/dev/3.0.0/native).
- The Varint decoder is updated to Velocity's
improved [MinecraftVarintFrameDecoder](https://github.com/PaperMC/Velocity/blob/dev/3.0.0/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftVarintFrameDecoder.java).
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 from 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. For example, some higher Minecraft versions have a bug where the client sometimes sends a packet out of order. Sonar accounts for that and does not falsely blacklist clients.
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.
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 nbt mappings were taken from [LimboAPI](https://github.com/Elytrium/LimboAPI).
- The compression and Varint decoding was taken from [Velocity](https://github.com/PaperMC/Velocity).
+3 -7
View File
@@ -13,13 +13,9 @@ If you discover a security vulnerability within Sonar, please follow these steps
## Contact
If you have any questions or concerns regarding the security of this project, please open a ticket on Discord or contact me via email at admin@jonesdev.xyz.
If you have any questions or concerns regarding the security of this project, please open a ticket on Discord or contact me via email at contact@jonesdev.xyz.
## Acknowledgments
Special thanks to the following people for their contributions:
- [xism4](https://github.com/xism4)
- fixed serious issues with packet decode logic on BungeeCord [#5](https://github.com/jonesdevelopment/sonar-legacy/pull/5) (Severity: medium)
- fixed a false blacklisting issue when a player looses connection [#9](https://github.com/jonesdevelopment/sonar-legacy/pull/9) (Severity: low)
- [NoGodHenry](https://github.com/NoGodHenry)
- fixed a ConcurrentModificationException in the configuration [#3](https://github.com/jonesdevelopment/sonar-legacy/pull/3) (Severity: low)
- Special thanks to the [contributors of Sonar](https://github.com/jonesdevelopment/sonar/graphs/contributors),
and also those who reported security issues or bugs.
+2 -2
View File
@@ -22,10 +22,10 @@ jobs:
steps:
- uses: actions/checkout@v3
- name: Set up JDK 11
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '11'
java-version: '17'
distribution: 'temurin'
- name: Build with Gradle
uses: gradle/gradle-build-action@bd5760595778326ba7f1441bcf7e88b49de61a25 # v2.6.0
-1
View File
@@ -30,6 +30,5 @@ gradlew*
*.jpg
*.jpeg
*.log
*.yml
*.svg
*.txt
+69 -62
View File
@@ -1,86 +1,93 @@
buildscript {
dependencies {
classpath("gradle.plugin.io.toolebox:gradle-git-versioner:1.6.7")
}
}
import net.kyori.indra.git.IndraGitExtension
plugins {
id("java")
id("com.github.johnrengelman.shadow") version "8.1.1"
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
maven(url = "https://jitpack.io") // simple-yaml
maven(url = "https://repo.papermc.io/repository/maven-public") // Velocity natives
mavenCentral()
maven(url = "https://repo.jonesdev.xyz/releases/") // Bungee & Velocity proxy module
maven(url = "https://s01.oss.sonatype.org/content/repositories/snapshots/") // libby
}
}
subprojects {
apply(plugin = "java")
apply(plugin = "com.github.johnrengelman.shadow")
apply(plugin = "com.gradleup.shadow")
dependencies {
compileOnly("org.projectlombok:lombok:1.18.30")
annotationProcessor("org.projectlombok:lombok:1.18.30")
compileOnly(rootProject.libs.lombok)
annotationProcessor(rootProject.libs.lombok)
testCompileOnly("org.projectlombok:lombok:1.18.30")
testAnnotationProcessor("org.projectlombok:lombok:1.18.30")
testCompileOnly(rootProject.libs.lombok)
testAnnotationProcessor(rootProject.libs.lombok)
// MiniMessage
implementation("net.kyori:adventure-text-minimessage:4.14.0")
implementation("net.kyori:adventure-text-serializer-gson:4.14.0")
implementation("net.kyori:adventure-text-serializer-legacy:4.14.0")
implementation("net.kyori:adventure-nbt:4.14.0") // nbt
implementation("com.j256.ormlite:ormlite-jdbc:6.1") // ORM
implementation("xyz.jonesdev:cappuccino:0.1.6-SNAPSHOT") // expiring cache
compileOnly("io.netty:netty-all:4.1.99.Final") // netty
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)
}
}
dependencies {
sequenceOf("api", "bukkit", "bungee", "common", "velocity").forEach {
// We want the jar to actually contain our modules
implementation(project(":$it"))
tasks {
shadowJar {
// Set the file name of the shadowed jar
archiveFileName.set("${rootProject.name}-${project.name.replaceFirstChar { it.uppercase() }}.jar")
// Remove file timestamps
isPreserveFileTimestamps = false
// Relocate libraries
relocate("org.bstats", "xyz.jonesdev.sonar.libs.bstats")
relocate("com.alessiodp.libby", "xyz.jonesdev.sonar.libs.libby")
relocate("com.simpleyaml", "xyz.jonesdev.sonar.libs.yaml")
relocate("com.google.gson", "xyz.jonesdev.sonar.libs.gson")
relocate("com.j256.ormlite", "xyz.jonesdev.sonar.libs.ormlite")
relocate("com.github.benmanes.caffeine", "xyz.jonesdev.sonar.libs.caffeine")
relocate("com.mysql", "xyz.jonesdev.sonar.libs.mysql")
relocate("org.mariadb", "xyz.jonesdev.sonar.libs.mariadb")
relocate("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 {
options.encoding = "UTF-8"
}
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
}
}
}
}
tasks {
jar {
manifest {
// Set the implementation version, so we can create exact version
// information in-game and make it accessible to the user.
attributes["Implementation-Version"] = version
attributes["Implementation-Vendor"] = "Jones Development, Sonar Contributors"
}
}
shadowJar {
// bStats has to be relocated to the Sonar package otherwise it throws an exception.
relocate("org.bstats", "xyz.jonesdev.sonar.bstats")
// Set the file name of the shadowed jar
archiveFileName.set("${rootProject.name}.jar")
}
compileJava {
options.encoding = "UTF-8"
}
// This is a small wrapper tasks to simplify the building process
register("build-sonar") {
dependsOn(clean, shadowJar)
val subprojects = listOf("api", "common", "bukkit", "bungee", "velocity")
val buildTasks = subprojects.flatMap { listOf("$it:clean", "$it:shadowJar") }
dependsOn(buildTasks)
}
}
-6
View File
@@ -1,6 +0,0 @@
#!/usr/bin/env bash
# Fix permissions so we are actually able to run our gradle script
chmod +x gradle*
# You can also use clean shadowJar if something goes wrong here
# e.g. "./gradlew clean shadowJar --stacktrace"
./gradlew build-sonar --stacktrace
+2 -1
View File
@@ -1,2 +1,3 @@
group=xyz.jonesdev.sonar
version=2.0.2
version=2.1.3
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.3-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"
+11 -6
View File
@@ -1,8 +1,13 @@
dependencies {
implementation("com.github.Carleslc.Simple-YAML:Simple-Yaml:1.8.4") // yaml config
compileOnly("org.jetbrains:annotations:24.0.1")
repositories {
maven(url = "https://jitpack.io/") // simple-yaml
}
java.sourceCompatibility = JavaVersion.VERSION_1_8
java.targetCompatibility = JavaVersion.VERSION_1_8
dependencies {
compileOnly(rootProject.libs.simpleyaml) {
exclude(group = "org.yaml")
}
compileOnly(rootProject.libs.annotations)
}
java.sourceCompatibility = JavaVersion.VERSION_11
java.targetCompatibility = JavaVersion.VERSION_11
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 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 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 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,35 +17,48 @@
package xyz.jonesdev.sonar.api;
import net.kyori.adventure.audience.Audience;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import xyz.jonesdev.sonar.api.attack.AttackTracker;
import xyz.jonesdev.sonar.api.command.subcommand.SubcommandRegistry;
import xyz.jonesdev.sonar.api.config.SonarConfiguration;
import xyz.jonesdev.sonar.api.controller.VerifiedPlayerController;
import xyz.jonesdev.sonar.api.event.SonarEventManager;
import xyz.jonesdev.sonar.api.fallback.Fallback;
import xyz.jonesdev.sonar.api.logger.LoggerWrapper;
import xyz.jonesdev.sonar.api.server.ServerWrapper;
import xyz.jonesdev.sonar.api.statistics.SonarStatistics;
import xyz.jonesdev.sonar.api.timer.SystemTimer;
import xyz.jonesdev.sonar.api.verbose.Notification;
import xyz.jonesdev.sonar.api.verbose.Verbose;
import java.io.File;
import java.text.DecimalFormat;
import java.util.UUID;
public interface Sonar {
DecimalFormat DECIMAL_FORMAT = new DecimalFormat("#,###.##");
String LINE_SEPARATOR = "\n"; // Using System.lineSeparator is broken, for some reason...
/**
* @return A small wrapper for the server
* Since we want to use Adventure on every server platform,
* we have to use their platform module to support BungeeCord and Bukkit
*/
@NotNull ServerWrapper getServer();
@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 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();
@@ -54,30 +67,49 @@ public interface Sonar {
@NotNull Verbose getVerboseHandler();
@NotNull Notification getNotificationHandler();
@NotNull SystemTimer getLaunchTimer();
@NotNull SonarStatistics getStatistics();
/**
* Set a custom verbose handler
*/
@SuppressWarnings("unused")
void setVerboseHandler(final @NotNull Verbose verboseHandler);
/**
* Set a custom notification handler
*/
@SuppressWarnings("unused")
void setNotificationHandler(final @NotNull Notification notificationHandler);
/**
* Reloads the entire plugin
*/
void reload();
@NotNull
default SonarVersion getVersion() {
return SonarVersion.INSTANCE;
}
@NotNull
default SonarEventManager getEventManager() {
// We don't want anyone else to create a new instance.
return SonarEventManager.INSTANCE;
}
@NotNull
default SonarVersion getVersion() {
return SonarVersion.GET;
default AttackTracker getAttackTracker() {
// We don't want anyone else to create a new instance.
return AttackTracker.INSTANCE;
}
@NotNull
default Fallback getFallback() {
// We only have one single instance of Fallback,
// and we don't want anyone else to create a new instance.
// We don't want anyone else to create a new instance.
return Fallback.INSTANCE;
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 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 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 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
@@ -0,0 +1,154 @@
/*
* 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.api.attack;
import lombok.*;
import org.jetbrains.annotations.Nullable;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.config.SonarConfiguration;
import xyz.jonesdev.sonar.api.event.impl.AttackDetectedEvent;
import xyz.jonesdev.sonar.api.event.impl.AttackMitigatedEvent;
import xyz.jonesdev.sonar.api.timer.SystemTimer;
import java.util.Optional;
import static xyz.jonesdev.sonar.api.jvm.JVMProcessInformation.*;
@Getter
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class AttackTracker {
public static final AttackTracker INSTANCE = new AttackTracker();
private @Nullable AttackStatistics currentAttack;
private int attackThreshold;
@Getter
@ToString
@RequiredArgsConstructor
public static final class AttackStatistics {
private final SystemTimer duration = new SystemTimer();
private final SystemTimer timer = new SystemTimer();
private long peakJoinsPerSecond;
private long peakConnectionsPerSecond;
private double peakProcessCPUUsage;
private long peakProcessMemoryUsage;
// Calculate during-attack-statistics using their deltas
private int successfulVerifications, failedVerifications;
}
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().getPlayers().size();
final int minPlayers = Sonar.get().getConfig().getMinPlayersForAttack();
if (joinsPerSecond > minPlayers // Check the number of bots/joins per second.
|| verifyingPlayers > minPlayers // Check the number of verifying players.
|| queuedPlayers > minPlayers) { // Check the number of queued players.
// Increment attack threshold
++attackThreshold;
// An attack has been detected
if (currentAttack == null) {
currentAttack = new AttackStatistics();
currentAttack.successfulVerifications = Sonar.get().getVerifiedPlayerController().estimatedSize();
currentAttack.failedVerifications = Sonar.get().getStatistics().getTotalFailedVerifications();
Sonar.get().getEventManager().publish(new AttackDetectedEvent());
Sonar.get().getNotificationHandler().observe();
} else {
// Reset attack timer if we're still under attack
currentAttack.timer.reset();
}
// Update attack statistics
if (joinsPerSecond > currentAttack.peakJoinsPerSecond) {
// Update joins per second peak if necessary
currentAttack.peakJoinsPerSecond = joinsPerSecond;
}
final long connectionsPerSecond = Sonar.get().getStatistics().getConnectionsPerSecond();
if (connectionsPerSecond > currentAttack.peakConnectionsPerSecond) {
// Update connections per second peak if necessary
currentAttack.peakConnectionsPerSecond = connectionsPerSecond;
}
final double processCPUUsage = getProcessCPUUsage();
if (processCPUUsage > currentAttack.peakProcessCPUUsage) {
// Update cpu usage peak if necessary
currentAttack.peakProcessCPUUsage = processCPUUsage;
}
final long processMemoryUsage = getUsedMemory();
if (processMemoryUsage > currentAttack.peakProcessMemoryUsage) {
// Update memory consumption peak if necessary
currentAttack.peakProcessMemoryUsage = processMemoryUsage;
}
} else if (currentAttack != null) {
if (currentAttack.duration.delay() > Sonar.get().getConfig().getMinAttackDuration()
&& currentAttack.timer.delay() > Sonar.get().getConfig().getAttackCooldownDelay()) {
// The current attack has stopped
Sonar.get().getEventManager().publish(new AttackMitigatedEvent(currentAttack));
if (++attackThreshold > Sonar.get().getConfig().getMinAttackThreshold()) {
// Post webhook to Discord
Optional.ofNullable(Sonar.get().getConfig().getWebhook().getDiscordWebhook())
.ifPresent(webhook -> {
// Save attack statistics
final long deltaInMillis = currentAttack.duration.delay();
final String peakCPU = Sonar.DECIMAL_FORMAT.format(currentAttack.peakProcessCPUUsage);
final String peakMem = formatMemory(currentAttack.peakProcessMemoryUsage);
final String peakBPS = Sonar.DECIMAL_FORMAT.format(currentAttack.peakJoinsPerSecond);
final String peakCPS = Sonar.DECIMAL_FORMAT.format(currentAttack.peakConnectionsPerSecond);
final long minutes = deltaInMillis / (60 * 1000); // Convert milliseconds to minutes
final double seconds = (deltaInMillis % (60 * 1000)) / 1000D; // Convert remaining milliseconds to seconds
final String formattedDuration = String.format("%d minutes, %.0f seconds", minutes, seconds);
final String startTimestamp = String.valueOf(currentAttack.duration.getStart() / 1000L);
final String endTimestamp = String.valueOf(System.currentTimeMillis() / 1000L);
final long blacklisted = Sonar.get().getFallback().getBlacklist().estimatedSize();
// Calculate during-attack-statistics using their deltas
final long totalVerified = Sonar.get().getVerifiedPlayerController().estimatedSize();
final long verified = Math.max(totalVerified - currentAttack.successfulVerifications, 0);
final long totalFailed = Sonar.get().getStatistics().getTotalFailedVerifications();
final long failed = Math.max(totalFailed - currentAttack.failedVerifications, 0);
webhook.post(() -> {
final SonarConfiguration.Webhook.Embed config = Sonar.get().getConfig().getWebhook().getEmbed();
final String description = config.getDescription()
.replace("<start-timestamp>", startTimestamp)
.replace("<end-timestamp>", endTimestamp)
.replace("<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());
});
});
}
// Reset the attack status
currentAttack = null;
// Reset threshold
attackThreshold = 0;
}
} else {
// Reset threshold
attackThreshold = 0;
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 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,12 +19,12 @@ package xyz.jonesdev.sonar.api.command;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import xyz.jonesdev.sonar.api.command.subcommand.Subcommand;
import lombok.ToString;
@Getter
@RequiredArgsConstructor
@ToString(of = "rawArguments")
public final class CommandInvocation {
private final InvocationSource sender;
private final Subcommand command;
private final InvocationSource source;
private final String[] rawArguments;
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 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,32 +21,40 @@ import lombok.Getter;
import lombok.RequiredArgsConstructor;
import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
import net.kyori.adventure.text.minimessage.MiniMessage;
import java.util.UUID;
import java.util.function.Predicate;
@Getter
@RequiredArgsConstructor
public abstract class InvocationSource {
private final String name;
public final class InvocationSource {
private final UUID uuid;
private final Audience audience;
private final Predicate<String> permissionFunction;
/**
* Sends an empty chat message to the command executor
* @return True, if {@link InvocationSource#uuid} is not null
* @apiNote This indicates a player as a player will always have a UUID
*/
public final void sendMessage() {
sendMessage(Component.empty());
public boolean isPlayer() {
return uuid != null;
}
/**
* Sends a message to the command executor
*
* @apiNote We should probably use cached components...
* (See {@link InvocationSource#sendMessage(Component)})
*/
public void sendMessage(final String legacy) {
sendMessage(MiniMessage.miniMessage().deserialize(legacy));
}
/**
* Sends a message to the command executor
*/
public final void sendMessage(final String legacy) {
sendMessage(LegacyComponentSerializer.legacyAmpersand().deserialize(legacy));
}
/**
* Sends a message to the command executor
*/
public final void sendMessage(final Component component) {
public void sendMessage(final Component component) {
audience.sendMessage(component);
}
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 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,67 +19,105 @@ package xyz.jonesdev.sonar.api.command;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.event.ClickEvent;
import net.kyori.adventure.text.event.HoverEvent;
import net.kyori.adventure.text.format.NamedTextColor;
import xyz.jonesdev.cappuccino.Cappuccino;
import xyz.jonesdev.cappuccino.ExpiringCache;
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 java.util.*;
import static java.util.Collections.emptyList;
public interface SonarCommand {
List<String> TAB_SUGGESTIONS = new ArrayList<>();
Map<String, List<String>> ARG_TAB_SUGGESTIONS = new HashMap<>();
ExpiringCache<Object> DELAY = Cappuccino.buildExpiring(500L);
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;
}
int COPYRIGHT_YEAR = Calendar.getInstance().get(Calendar.YEAR);
Optional<Subcommand> subcommand = Optional.empty();
List<Component> CACHED_HELP_MESSAGE = new Vector<>();
if (args.length > 0) {
// Search subcommand if command arguments are present
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])))
.findFirst();
}
default void cacheHelpMessage() {
CACHED_HELP_MESSAGE.addAll(Arrays.asList(
Component.text("Running Sonar " + Sonar.get().getVersion()
+ " on " + Sonar.get().getServer().getPlatform().getDisplayName()
+ ".", NamedTextColor.YELLOW),
Component.text("(C) " + COPYRIGHT_YEAR + " Jones Development and Sonar Contributors", NamedTextColor.YELLOW),
Component.text("https://github.com/jonesdevelopment/sonar", NamedTextColor.GREEN)
.clickEvent(net.kyori.adventure.text.event.ClickEvent.clickEvent(net.kyori.adventure.text.event.ClickEvent.Action.OPEN_URL, "https://github.com/jonesdevelopment/sonar")),
Component.empty(),
Component.text("Need help or have any questions?", NamedTextColor.YELLOW),
Component.textOfChildren(
Component.text("Open a ticket on the Discord ", NamedTextColor.YELLOW)
.hoverEvent(net.kyori.adventure.text.event.HoverEvent.hoverEvent(net.kyori.adventure.text.event.HoverEvent.Action.SHOW_TEXT, Component.text("(Click to open Discord)")))
.clickEvent(net.kyori.adventure.text.event.ClickEvent.clickEvent(net.kyori.adventure.text.event.ClickEvent.Action.OPEN_URL, "https://jonesdev.xyz/discord/")),
Component.text("or open a new issue on GitHub.", NamedTextColor.YELLOW)
.hoverEvent(net.kyori.adventure.text.event.HoverEvent.hoverEvent(net.kyori.adventure.text.event.HoverEvent.Action.SHOW_TEXT, Component.text("(Click to open GitHub)")))
.clickEvent(net.kyori.adventure.text.event.ClickEvent.clickEvent(net.kyori.adventure.text.event.ClickEvent.Action.OPEN_URL, "https://github.com/jonesdevelopment/sonar" +
"/issues"))
),
Component.empty()
));
subcommand.ifPresentOrElse(command -> {
// Check permissions for subcommands
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)))));
Sonar.get().getSubcommandRegistry().getSubcommands().forEach(sub -> {
Component component = Component.textOfChildren(
Component.text(" ▪ ", NamedTextColor.GRAY),
Component.text("/sonar " + sub.getInfo().name(), NamedTextColor.GREEN),
Component.text(" - ", NamedTextColor.GRAY),
Component.text(sub.getInfo().description(), NamedTextColor.WHITE)
);
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"));
component = component.clickEvent(
net.kyori.adventure.text.event.ClickEvent.clickEvent(ClickEvent.Action.SUGGEST_COMMAND,
"/sonar " + sub.getInfo().name() + " ")
).hoverEvent(
net.kyori.adventure.text.event.HoverEvent.hoverEvent(HoverEvent.Action.SHOW_TEXT, Component.text(
"§7Only players: §f" + (sub.getInfo().onlyPlayers() ? "§a✔" : "§c✗")
+ Sonar.LINE_SEPARATOR + "§7Require console: §f" + (sub.getInfo().onlyConsole() ? "§a✔" : "§c✗")
+ Sonar.LINE_SEPARATOR + "§7Permission: §f" + sub.getPermission()
+ Sonar.LINE_SEPARATOR + "§7Aliases: §f" + sub.getAliases()
))
);
CACHED_HELP_MESSAGE.add(component);
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() {
// Don't re-cache tab suggestions
if (!TAB_SUGGESTIONS.isEmpty()) return;
// Cache tab suggestions
for (final Subcommand subcommand : Sonar.get().getSubcommandRegistry().getSubcommands()) {
TAB_SUGGESTIONS.add(subcommand.getInfo().name());
if (subcommand.getInfo().aliases().length > 0) {
TAB_SUGGESTIONS.addAll(Arrays.asList(subcommand.getInfo().aliases()));
}
final List<String> 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, arguments);
}
}
}
default List<String> getCachedTabSuggestions(final String @NotNull [] arguments) {
if (arguments.length <= 1) {
return TAB_SUGGESTIONS;
} else if (arguments.length == 2) {
final String subcommandName = arguments[0].toLowerCase();
return ARG_TAB_SUGGESTIONS.getOrDefault(subcommandName, emptyList());
}
return emptyList();
}
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 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,41 +18,86 @@
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.util.Arrays;
import java.util.Objects;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
@Getter
public abstract class Subcommand {
private final @NotNull SubcommandInfo info;
private final String permission, aliases, arguments;
protected static final Sonar SONAR = Sonar.get();
protected static final Pattern IP_PATTERN = Pattern.compile(
"^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])([.])){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$"
);
public Subcommand() {
info = getClass().getAnnotation(SubcommandInfo.class);
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 final void incorrectUsage(final @NotNull InvocationSource sender) {
sender.sendMessage(SONAR.getConfig().getIncorrectCommandUsage()
.replace("%usage%", info.name() + " (" + arguments + ")"));
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 raw;
}
public abstract void execute(final @NotNull CommandInvocation invocation);
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 (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 (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;
}
// The subcommands has arguments which are not present in the executed command
if (info.argumentsRequired() && info.arguments().length > 0 && arguments.length <= 1) {
incorrectUsage(invocationSource);
return;
}
// 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 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 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 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
@@ -0,0 +1,209 @@
/*
* 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.api.config;
import lombok.Getter;
import org.jetbrains.annotations.NotNull;
@Getter
public enum Language {
SYSTEM("system"),
AB("Abkhazian"),
AA("Afar"),
AF("Afrikaans"),
SQ("Albanian"),
AM("Amharic"),
AR("Arabic"),
AN("Aragonese"),
HY("Armenian"),
AS("Assamese"),
AE("Avestan"),
AY("Aymara"),
AZ("Azerbaijani"),
BA("Bashkir"),
EU("Basque"),
BE("Belarusian"),
BN("Bengali"),
BH("Bihari"),
BI("Bislama"),
BS("Bosnian"),
BR("Breton"),
BG("Bulgarian"),
MY("Burmese"),
CA("Catalan"),
CH("Chamorro"),
CE("Chechen"),
ZH("Chinese"),
CU("Slavic"),
CV("Chuvash"),
KW("Cornish"),
CO("Corsican"),
HR("Croatian"),
CS("Czech"),
DA("Danish"),
DV("Maldivian"),
NL("Dutch"),
DZ("Dzongkha"),
EN("English"),
EO("Esperanto"),
ET("Estonian"),
FO("Faroese"),
FJ("Fijian"),
FI("Finnish"),
FR("French"),
GD("Gaelic"),
GL("Galician"),
KA("Georgian"),
DE("German"),
EL("Greek"),
GN("Guarani"),
GU("Gujarati"),
HT("Haitian Creole"),
HA("Hausa"),
HE("Hebrew"),
HZ("Herero"),
HI("Hindi"),
HO("Hiri Motu"),
HU("Hungarian"),
IS("Icelandic"),
IO("Ido"),
ID("Indonesian"),
IE("Interlingue"),
IU("Inuktitut"),
IK("Inupiaq"),
GA("Irish"),
IT("Italian"),
JA("Japanese"),
JV("Javanese"),
KL("Kalaallisut"),
KN("Kannada"),
KS("Kashmiri"),
KK("Kazakh"),
KM("Khmer"),
KI("Kikuyu"),
RW("Kinyarwanda"),
KY("Kirghiz"),
KV("Komi"),
KO("Korean"),
KJ("Kwanyama"),
KU("Kurdish"),
LO("Lao"),
LA("Latin"),
LV("Latvian"),
LI("Limburgish"),
LN("Lingala"),
LT("Lithuanian"),
LB("Luxembourgish"),
MK("Macedonian"),
MG("Malagasy"),
MS("Malay"),
ML("Malayalam"),
MT("Maltese"),
GV("Manx"),
MI("Maori"),
MR("Marathi"),
MH("Marshallese"),
MO("Moldavian"),
MN("Mongolian"),
NA("Nauru"),
NV("Navajo"),
ND("Ndebele, North"),
NR("Ndebele, South"),
NG("Ndonga"),
NE("Nepali"),
SE("Northern Sami"),
NO("Norwegian"),
NB("Norwegian Bokmal"),
NN("Norwegian Nynorsk"),
NY("Chewa"),
OC("Occitan"),
OR("Oriya"),
OM("Oromo"),
OS("Ossetian"),
PI("Pali"),
PA("Panjabi"),
FA("Persian"),
PL("Polish"),
PT("Portuguese"),
PS("Pushto"),
QU("Quechua"),
RM("Raeto-Romance"),
RO("Romanian"),
RN("Rundi"),
RU("Russian"),
SM("Samoan"),
SG("Sango"),
SA("Sanskrit"),
SC("Sardinian"),
SR("Serbian"),
SN("Shona"),
II("Sichuan Yi"),
SD("Sindhi"),
SI("Sinhala"),
SK("Slovak"),
SL("Slovenian"),
SO("Somali"),
ST("Sotho, Southern"),
ES("Spanish"),
SU("Sundanese"),
SW("Swahili"),
SS("Swati"),
SV("Swedish"),
TL("Tagalog"),
TY("Tahitian"),
TG("Tajik"),
TA("Tamil"),
TT("Tatar"),
TE("Telugu"),
TH("Thai"),
BO("Tibetan"),
TI("Tigrinya"),
TO("Tonga"),
TS("Tsonga"),
TN("Tswana"),
TR("Turkish"),
TK("Turkmen"),
TW("Twi"),
UG("Uighur"),
UK("Ukrainian"),
UR("Urdu"),
UZ("Uzbek"),
VI("Vietnamese"),
VO("Volapuk"),
WA("Walloon"),
CY("Welsh"),
FY("Western Frisian"),
WO("Wolof"),
XH("Xhosa"),
YI("Yiddish"),
YO("Yoruba"),
ZA("Zhuang"),
ZU("Zulu");
private final String name;
private final String code;
Language(final String name) {
this.name = name;
this.code = name().toLowerCase();
}
public static Language fromCode(final @NotNull String code) {
return Language.valueOf(code.toUpperCase());
}
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 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,88 +18,117 @@
package xyz.jonesdev.sonar.api.config;
import lombok.Getter;
import org.simpleyaml.configuration.comments.format.YamlCommentFormat;
import org.jetbrains.annotations.NotNull;
import org.simpleyaml.configuration.MemorySection;
import org.simpleyaml.configuration.file.YamlFile;
import xyz.jonesdev.sonar.api.Sonar;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.io.InputStream;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import static xyz.jonesdev.sonar.api.Sonar.LINE_SEPARATOR;
import static xyz.jonesdev.sonar.api.config.SonarConfiguration.LOGGER;
@Getter
public final class SimpleYamlConfig {
private final File file;
private final YamlFile yaml;
private static final List<String> HEADER = Arrays.asList(
"",
"Sonar anti-bot version " + Sonar.get().getVersion(),
" Currently running on: " + Sonar.get().getServer().getPlatform().getDisplayName(),
" Need help? https://jonesdev.xyz/discord",
""
);
public SimpleYamlConfig(final File dataFolder, final String fileName) {
this(new File(dataFolder, fileName + ".yml"), dataFolder);
}
private SimpleYamlConfig(final File file, final File folder) {
if (!folder.exists() && !folder.mkdir()) {
throw new IllegalStateException("Could not create folder?!");
}
public SimpleYamlConfig(final @NotNull File file) {
this.file = file;
this.yaml = new YamlFile(file.getPath());
}
public void save() {
try {
yaml.save();
} catch (IOException exception) {
exception.printStackTrace(System.err);
private void replaceConfigFile(final @NotNull URL defaultData) throws Exception {
try (final InputStream inputStream = defaultData.openStream()) {
Files.copy(inputStream, file.toPath(), StandardCopyOption.REPLACE_EXISTING);
}
}
public void load() throws Exception {
// https://github.com/jonesdevelopment/sonar/issues/26
// Only load the configuration if the file already exists
if (yaml.exists()) {
yaml.loadWithComments();
} else {
yaml.createOrLoadWithComments();
public void load(final @NotNull URL defaultData) throws Exception {
final boolean exists = yaml.exists();
// Copy the configuration from the translations
if (!exists) {
replaceConfigFile(defaultData);
}
// Always load the comment format and options
yaml.setCommentFormat(YamlCommentFormat.PRETTY);
// Load the configuration
yaml.loadWithComments();
yaml.options().headerFormatter().commentPrefix("# ");
yaml.setHeader(String.join(LINE_SEPARATOR, HEADER));
// I hate my life... but this works... somehow
if (exists) {
// First, we store all values from the current (old) config
final Map<String, Object> values = yaml.getValues(true);
// Then, we replace the old config file with the default one
replaceConfigFile(defaultData);
// Re-load the configuration
yaml.loadWithComments();
// Loop through the new values from the new configuration
yaml.getValues(true).forEach((path, v) -> {
final Object value = values.get(path);
// Make sure we actually need to update the value
if (value != null && !value.equals(v)
&& !(v instanceof MemorySection)
&& !(value instanceof MemorySection)) {
// Set the old values to the new values
yaml.set(path, value);
}
});
// Save the yaml file to ensure we keep the original values
yaml.save();
}
}
public void set(final String path, final Object v) {
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());
return 0;
}
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());
return false;
}
return yaml.getBoolean(path);
}
public String getString(final String path, final String def) {
yaml.addDefault(path, def);
return yaml.getString(path, 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 = yaml.get(path);
if (object instanceof String) {
return (String) object;
}
throw new IllegalStateException("Invalid entry " + path);
}
public List<String> getStringList(final String path, final List<String> def) {
yaml.addDefault(path, def);
public List<String> getStringList(final String path) {
if (!yaml.contains(path)) {
LOGGER.warn("Could not find {} in {}.", path, file.getName());
return new ArrayList<>(0);
}
return yaml.getStringList(path);
}
public List<Integer> getIntList(final String path) {
if (!yaml.contains(path)) {
LOGGER.warn("Could not find {} in {}.", path, file.getName());
return new ArrayList<>(0);
}
return yaml.getIntegerList(path);
}
}
File diff suppressed because it is too large. Load diff
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 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,63 +17,167 @@
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;
import com.j256.ormlite.stmt.QueryBuilder;
import com.j256.ormlite.support.ConnectionSource;
import com.j256.ormlite.table.TableUtils;
import lombok.Getter;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.Range;
import org.jetbrains.annotations.Unmodifiable;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.config.SonarConfiguration;
import xyz.jonesdev.sonar.api.dependencies.DependencyLoader;
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;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public final class VerifiedPlayerController {
private static final Map<String, Collection<UUID>> MAP = new ConcurrentHashMap<>();
private static final ExecutorService DB_UPDATE_SERVICE = Executors.newSingleThreadExecutor();
private static final LoggerWrapper LOGGER = new LoggerWrapper() {
@Override
public void info(final String message, final Object... args) {
Sonar.get().getLogger().info("[database] " + message, args);
}
@Override
public void warn(final String message, final Object... args) {
Sonar.get().getLogger().warn("[database] " + message, args);
}
@Override
public void error(final String message, final Object... args) {
Sonar.get().getLogger().error("[database] " + message, args);
}
};
private final Map<String, Collection<UUID>> cache = new ConcurrentHashMap<>(128);
private @Nullable ConnectionSource connectionSource;
private Dao<VerifiedPlayer, Integer> dao;
private QueryBuilder<VerifiedPlayer, Integer> queryBuilder;
@Getter
private final @NotNull SonarConfiguration.DatabaseType cachedDatabaseType;
private final @NotNull SonarConfiguration.Database.Type cachedDatabaseType;
public VerifiedPlayerController() {
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 = Sonar.get().getConfig().getDatabaseType();
cachedDatabaseType = database.getType();
// Don't establish a database connection if the type is NONE
if (cachedDatabaseType == SonarConfiguration.DatabaseType.NONE) {
Sonar.get().getLogger().warn("Make sure to configure a database to save verified players.");
if (cachedDatabaseType == SonarConfiguration.Database.Type.NONE) {
LOGGER.warn("Configure a database to save verified players.");
return;
}
try (final ConnectionSource connectionSource = DependencyLoader.setUpDriverAndConnect()) {
this.connectionSource = connectionSource;
// 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();
// Create table
try {
final String jdbcURL;
// 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) {
// Duplicate index
// I know this isn't the best method of handling it,
// but I don't know how else I could address this issue.
}
dao = DaoManager.createDao(connectionSource, VerifiedPlayer.class);
queryBuilder = dao.queryBuilder();
dao.queryForAll().forEach(this::_add);
} catch (Throwable throwable) {
Sonar.get().getLogger().error("Error setting up database connection: {}", throwable);
throwable.printStackTrace(System.err);
// Make sure to run the clean task and the caching task in the same thread
// https://github.com/jonesdevelopment/sonar/issues/150
DB_UPDATE_SERVICE.execute(() -> {
try {
// Make sure to clear all outdated entries first
clearOld(database.getMaximumAge());
// Add all entries from the database to the cache
dao.queryForAll().forEach(this::_add);
} catch (SQLException exception) {
LOGGER.error("Error initializing database: {}", exception);
}
});
} catch (SQLException exception) {
LOGGER.error("Error setting up database: {}", exception);
}
}
/**
* Closes the connection if instantiated
*/
public void close() {
// The connection source will always be null if the database type is NONE.
if (connectionSource != null) {
try {
connectionSource.close();
} catch (Exception exception) {
LOGGER.error("Error closing database: {}", exception);
}
}
}
/**
* Clear all old entries using the given timestamp.
*/
private void clearOld(final @Range(from = 1, to = 365) int maximumAge) throws SQLException {
final long timestamp = Instant.now()
.minus(maximumAge, ChronoUnit.DAYS)
.getEpochSecond() * 1000L; // convert to ms
final List<VerifiedPlayer> oldEntries = queryBuilder.where()
.lt("timestamp", new Timestamp(timestamp))
.query();
if (oldEntries != null) {
for (final VerifiedPlayer player : oldEntries) {
dao.delete(player);
}
LOGGER.info("Removed {} database entries older than {} days.",
oldEntries.size(), maximumAge);
}
}
@@ -87,12 +191,16 @@ public final class VerifiedPlayerController {
_remove(inetAddress);
// Don't try to update the column if the database type is NONE
if (cachedDatabaseType == SonarConfiguration.DatabaseType.NONE) {
if (cachedDatabaseType == SonarConfiguration.Database.Type.NONE) {
return;
}
DB_UPDATE_SERVICE.execute(() -> {
Objects.requireNonNull(connectionSource);
// We cannot throw a NullPointerException within the executor service
// because we want to handle the error instead of simply throwing an exception
if (connectionSource == null) {
return;
}
try {
final List<VerifiedPlayer> verifiedPlayer = queryBuilder.where()
@@ -105,7 +213,7 @@ public final class VerifiedPlayerController {
}
}
} catch (SQLException exception) {
Sonar.get().getLogger().error("Error trying to remove entry: {}", exception);
LOGGER.error("Error trying to remove entry: {}", exception);
}
});
}
@@ -116,7 +224,7 @@ public final class VerifiedPlayerController {
* @param inetAddress IP address of the player
*/
private void _remove(final @NotNull String inetAddress) {
MAP.remove(inetAddress);
cache.remove(inetAddress);
}
/**
@@ -129,17 +237,21 @@ public final class VerifiedPlayerController {
_add(player);
// Don't try to update the column if the database type is NONE
if (cachedDatabaseType == SonarConfiguration.DatabaseType.NONE) {
if (cachedDatabaseType == SonarConfiguration.Database.Type.NONE) {
return;
}
DB_UPDATE_SERVICE.execute(() -> {
Objects.requireNonNull(connectionSource);
// We cannot throw a NullPointerException within the executor service
// because we want to handle the error instead of simply throwing an exception
if (connectionSource == null) {
return;
}
try {
dao.create(player);
dao.createIfNotExists(player);
} catch (SQLException exception) {
Sonar.get().getLogger().error("Error trying to add entry: {}", exception);
LOGGER.error("Error trying to add entry: {}", exception);
}
});
}
@@ -150,40 +262,53 @@ public final class VerifiedPlayerController {
* @param player VerifiedPlayer model
*/
private void _add(final @NotNull VerifiedPlayer player) {
MAP.computeIfAbsent(player.getInetAddress(), v -> new Vector<>())
.add(player.getPlayerUUID());
cache.computeIfAbsent(player.getInetAddress(), v -> new ArrayList<>())
.add(player.getPlayerUuid());
}
/**
* Returns the number of verified IP addresses
*
* @return Estimated size of the local cache
*/
public synchronized int estimatedSize() {
return MAP.values().stream()
public int estimatedSize() {
return cache.size();
}
/**
* 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 List of UUIDs associated with one IP address
* @return {@link java.util.Collection} of UUIDs associated with an IP address
*/
public Collection<UUID> getUUIDs(final @NotNull String inetAddress) {
return MAP.getOrDefault(inetAddress, Collections.emptyList());
public @Unmodifiable Collection<UUID> getUUIDs(final @NotNull String inetAddress) {
return cache.getOrDefault(inetAddress, Collections.emptyList());
}
/**
* 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 {
MAP.clear();
public synchronized void clearAll() {
cache.clear();
// Only update the column if the database type is not NONE
if (cachedDatabaseType != SonarConfiguration.DatabaseType.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) {
Sonar.get().getLogger().error("Error trying to clear entries: {}", exception);
}
}
@@ -193,37 +318,18 @@ public final class VerifiedPlayerController {
* @return Whether the local cache contains the IP and UUID
*/
public boolean has(final @NotNull String inetAddress, final @NotNull UUID uuid) {
final Collection<UUID> got = MAP.get(inetAddress);
final Collection<UUID> got = cache.get(inetAddress);
if (got != null) {
return got.contains(uuid);
}
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
*/
public boolean has(final @NotNull String inetAddress) {
return MAP.containsKey(inetAddress);
}
/**
* @param inetAddress IP address
* @return Whether the local cache contains the IP
* @see #has(String)
*/
public boolean has(final @NotNull InetAddress inetAddress) {
return has(inetAddress.toString());
return cache.containsKey(inetAddress);
}
}
@@ -1,91 +0,0 @@
/*
* Copyright (C) 2023 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.api.dependencies;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.timer.SystemTimer;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
// Mostly taken from
// https://github.com/Elytrium/LimboAuth/blob/master/src/main/java/net/elytrium/limboauth/dependencies/BaseLibrary.java
public enum Dependency {
// https://mvnrepository.com/artifact/com.mysql/mysql-connector-j
MYSQL(
"com/mysql",
"mysql-connector-j",
"8.1.0"
),
// https://mvnrepository.com/artifact/org.mariadb.jdbc/mariadb-java-client
MARIADB(
"org/mariadb/jdbc",
"mariadb-java-client",
"3.1.4"
);
private @NotNull
final Path tempFilePath;
private @NotNull
final URL mvnRepoURL;
Dependency(final @NotNull String groupId,
final @NotNull String artifactId,
final @NotNull String version) {
final String archiveFileName = String.format("%s-%s.jar", artifactId, version);
this.tempFilePath = new File(Sonar.get().getDataDirectory(), "libraries/" + archiveFileName).toPath();
try {
final String mvnCentralPath = String.format("%s/%s/%s/%s-%s.jar",
groupId, artifactId, version, artifactId, version);
this.mvnRepoURL = new URL("https://repo1.maven.org/maven2/" + mvnCentralPath);
} catch (MalformedURLException exception) {
throw new IllegalStateException(exception);
}
}
/**
* @return URL for the local file used for the external URLClassLoader
*/
public @NotNull URL getClassLoaderURL() throws MalformedURLException {
if (!Files.exists(tempFilePath)) {
final SystemTimer timer = new SystemTimer();
Sonar.get().getLogger().info("Downloading dependency {}...", tempFilePath.getFileName());
try (final InputStream inputStream = mvnRepoURL.openStream()) {
Files.createDirectories(tempFilePath.getParent());
Files.copy(inputStream, Files.createFile(tempFilePath), REPLACE_EXISTING);
} catch (IOException exception) {
throw new IllegalStateException(exception);
}
Sonar.get().getLogger().info("Finished downloading {} ({}s)!", tempFilePath.getFileName(), timer);
}
return tempFilePath.toUri().toURL();
}
}
@@ -1,84 +0,0 @@
/*
* Copyright (C) 2023 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.api.dependencies;
import com.j256.ormlite.jdbc.JdbcSingleConnectionSource;
import com.j256.ormlite.support.ConnectionSource;
import lombok.experimental.UtilityClass;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.config.SonarConfiguration;
import java.io.File;
import java.lang.reflect.Method;
import java.net.URL;
import java.nio.file.Path;
import java.sql.Connection;
import java.sql.Driver;
import java.sql.DriverManager;
import java.util.Properties;
@UtilityClass
public class DependencyLoader {
public ConnectionSource setUpDriverAndConnect() throws Throwable {
final SonarConfiguration config = Sonar.get().getConfig();
final URL[] urls = new URL[config.getDatabaseType().getDependencies().length];
for (final Dependency dependency : config.getDatabaseType().getDependencies()) {
final URL url = dependency.getClassLoaderURL();
final ClassLoader currentClassLoader = DependencyLoader.class.getClassLoader();
final Method addPath = currentClassLoader.getClass().getDeclaredMethod("addPath", Path.class);
addPath.setAccessible(true);
addPath.invoke(currentClassLoader, new File(url.toURI()).toPath());
urls[dependency.ordinal()] = url;
}
final String type = config.getDatabaseType().name().toLowerCase();
final String databaseURL =
"jdbc:" + type + "://" + config.getSqlUrl() + ":" + config.getSqlPort() + "/" + config.getSqlDatabase();
final ExternalClassLoader classLoader = new ExternalClassLoader(urls);
final Connection connection = connect(classLoader, databaseURL, config);
return new JdbcSingleConnectionSource(databaseURL, connection);
}
// Mostly taken from
// https://github.com/Elytrium/LimboAuth/blob/master/src/main/java/net/elytrium/limboauth/dependencies/DatabaseLibrary.java#L134
private Connection connect(final @NotNull ClassLoader classLoader,
final @NotNull String databaseURL,
final @NotNull SonarConfiguration configuration) throws Throwable {
final Class<?> driverClass = classLoader.loadClass(configuration.getDatabaseType().getDriverClassName());
final Object driver = driverClass.getDeclaredConstructor().newInstance();
DriverManager.registerDriver((Driver) driver);
final Properties properties = new Properties();
if (!configuration.getSqlUser().isEmpty()) {
properties.put("user", configuration.getSqlUser());
}
if (!configuration.getSqlPassword().isEmpty()) {
properties.put("password", configuration.getSqlPassword());
}
final Method connect = driverClass.getDeclaredMethod("connect", String.class, Properties.class);
connect.setAccessible(true);
return (Connection) connect.invoke(driver, databaseURL, properties);
}
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 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 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 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,24 +21,40 @@ import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.Sonar;
import java.util.Arrays;
import java.util.Collection;
import java.util.Vector;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class SonarEventManager {
public static final SonarEventManager INSTANCE = new SonarEventManager();
private static final Collection<SonarEventListener> EVENT_LISTENERS = new Vector<>(0);
private static final ExecutorService EVENT_SERVICE = Executors.newSingleThreadExecutor();
/**
* Publishes the given event to all listeners
*/
@ApiStatus.Internal
public void publish(final @NotNull SonarEvent event) {
for (final SonarEventListener eventListener : EVENT_LISTENERS) {
eventListener.handle(event);
// Don't post a task if there are no listeners
if (EVENT_LISTENERS.isEmpty()) {
return;
}
EVENT_SERVICE.execute(() -> {
for (final SonarEventListener eventListener : EVENT_LISTENERS) {
try {
eventListener.handle(event);
} catch (Throwable throwable) {
Sonar.get().getLogger().error("Could not pass {} to listener: {}",
event.getClass().getSimpleName(), throwable);
}
}
});
}
/**
@@ -47,6 +63,7 @@ public final class SonarEventManager {
* @param listeners One (or more) listeners to register
* @see #unregisterListener(SonarEventListener...) Unregister an event listener
*/
@SuppressWarnings("unused") // External API usage
public synchronized void registerListener(final @NotNull SonarEventListener... listeners) {
EVENT_LISTENERS.addAll(Arrays.asList(listeners));
}
@@ -58,6 +75,7 @@ public final class SonarEventManager {
* @apiNote This does not have any effect if the given listeners are not registered
* @see #registerListener(SonarEventListener...) Register an event listener
*/
@SuppressWarnings("unused") // External API usage
public synchronized void unregisterListener(final @NotNull SonarEventListener... listeners) {
EVENT_LISTENERS.removeAll(Arrays.asList(listeners));
}
@@ -0,0 +1,23 @@
/*
* 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.api.event.impl;
import xyz.jonesdev.sonar.api.event.SonarEvent;
public final class AttackDetectedEvent implements SonarEvent {
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 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,16 +15,17 @@
* 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.event.impl;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
import xyz.jonesdev.sonar.api.attack.AttackTracker;
import xyz.jonesdev.sonar.api.event.SonarEvent;
@Getter
@ToString
@RequiredArgsConstructor
public final class BlockPosition {
private final int x, y, z;
private final int chunkX, chunkZ;
public final class AttackMitigatedEvent implements SonarEvent {
private final AttackTracker.AttackStatistics attackStatistics;
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 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,9 +26,8 @@ import xyz.jonesdev.sonar.api.fallback.FallbackUser;
@Getter
@ToString
@EqualsAndHashCode
@RequiredArgsConstructor
public final class UserBlacklistedEvent implements SonarEvent {
@ApiStatus.Experimental
private final FallbackUser<?, ?> user;
private final FallbackUser user;
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 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,18 +17,17 @@
package xyz.jonesdev.sonar.api.event.impl;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.event.SonarEvent;
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
@Getter
@ToString
@EqualsAndHashCode
@RequiredArgsConstructor
public final class UserVerifyFailedEvent implements SonarEvent {
private final FallbackUser<?, ?> user;
private final String reason;
private final @NotNull FallbackUser user;
private final @NotNull String reason;
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 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,9 +25,8 @@ import xyz.jonesdev.sonar.api.fallback.FallbackUser;
@Getter
@ToString
@EqualsAndHashCode
@RequiredArgsConstructor
public final class UserVerifyJoinEvent implements SonarEvent {
private final String username;
private final FallbackUser<?, ?> user;
private final FallbackUser user;
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 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,11 +27,10 @@ import java.util.UUID;
@Getter
@ToString
@EqualsAndHashCode
@RequiredArgsConstructor
public final class UserVerifySuccessEvent implements SonarEvent {
private final String username;
private final UUID offlineUuid;
private final FallbackUser<?, ?> user;
private final FallbackUser user;
private final long timeTakenToVerify;
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 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,55 +17,70 @@
package xyz.jonesdev.sonar.api.fallback;
import com.github.benmanes.caffeine.cache.Cache;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.cappuccino.Cappuccino;
import xyz.jonesdev.cappuccino.ExpiringCache;
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.TimeUnit;
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());
private final Map<String, InetAddress> connected = new ConcurrentHashMap<>();
// Only block the player for a few minutes to avoid issues
private final ExpiringCache<String> blacklisted = Cappuccino.buildExpiring(
10L, TimeUnit.MINUTES, 2500L
);
private final @NotNull FallbackQueue queue = FallbackQueue.INSTANCE;
private final @NotNull FallbackRatelimiter ratelimiter = FallbackRatelimiter.INSTANCE;
// 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 = 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);
}
};
public boolean isPotentiallyUnderAttack() {
return connected.size() > SONAR.getConfig().getMinimumPlayersForAttack()
|| queue.getQueuedPlayers().size() > SONAR.getConfig().getMinimumPlayersForAttack();
public boolean shouldVerifyNewPlayers() {
return shouldPerform(Sonar.get().getConfig().getVerification().getTiming());
}
public boolean shouldPerformCaptcha() {
return shouldPerform(Sonar.get().getConfig().getVerification().getMap().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().getCurrentAttack() != null);
}
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 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,9 +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_IN_TRAFFIC = "sonar-traffic-in";
String FALLBACK_OUT_TRAFFIC = "sonar-traffic-out";
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 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,44 +26,32 @@ import java.net.InetAddress;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@Getter
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
@RequiredArgsConstructor(access = AccessLevel.PROTECTED)
public final class FallbackQueue {
public static final FallbackQueue INSTANCE = new FallbackQueue();
private final Map<InetAddress, Runnable> queuedPlayers = new ConcurrentHashMap<>(16, 0.5f);
// 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);
/**
* @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);
}
private final ConcurrentMap<InetAddress, Runnable> players = new ConcurrentHashMap<>(
128, 0.75f, Runtime.getRuntime().availableProcessors());
public void poll() {
final int max = Sonar.get().getConfig().getMaximumQueuePolls();
final int maxQueuePolls = Sonar.get().getConfig().getQueue().getMaxQueuePolls();
int index = 0;
// We need to be very careful 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 > max) {
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();
}
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 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,36 +17,49 @@
package xyz.jonesdev.sonar.api.fallback;
import com.github.benmanes.caffeine.cache.Cache;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.cappuccino.ExpiringCache;
import java.net.InetAddress;
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public final class FallbackRatelimiter {
public static final FallbackRatelimiter INSTANCE = new FallbackRatelimiter();
@Setter
private ExpiringCache<InetAddress> expiringCache;
private long timeout;
private Cache<InetAddress, Long> attemptCache;
@Getter
private Cache<InetAddress, Integer> failCountCache;
/**
* 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
*/
public boolean attempt(final @NotNull InetAddress inetAddress) {
// Clean up the cache
expiringCache.cleanUp(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;
}
// Cache the IP address if it's not already cached
if (!expiringCache.has(inetAddress)) {
expiringCache.put(inetAddress);
return true;
/**
* Checks if the player is failing verifications too often
* Increments the number of times this user has failed the verification
*
* @param inetAddress IP address of the player
* @param count Count of previously failed verifications
*/
public void incrementFails(final @NotNull InetAddress inetAddress, final int count) {
// Make sure to remove the old values from the cache
if (count > 0) {
failCountCache.invalidate(inetAddress);
}
// Deny the connection attempt
return false;
failCountCache.put(inetAddress, count + 1);
}
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 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,62 +19,84 @@ 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.NotNull;
import org.jetbrains.annotations.Nullable;
import xyz.jonesdev.cappuccino.Cappuccino;
import xyz.jonesdev.cappuccino.ExpiringCache;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.event.impl.UserBlacklistedEvent;
import xyz.jonesdev.sonar.api.event.impl.UserVerifyFailedEvent;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.api.statistics.Statistics;
import xyz.jonesdev.sonar.api.timer.SystemTimer;
import java.net.InetAddress;
import java.util.concurrent.TimeUnit;
import java.util.UUID;
public interface FallbackUser<X, Y> {
@NotNull Fallback getFallback();
public interface FallbackUser {
@NotNull
Channel getChannel();
@NotNull X getPlayer();
@NotNull
ChannelPipeline getPipeline();
@NotNull Y getConnection();
@NotNull
InetAddress getInetAddress();
@NotNull Channel getChannel();
@NotNull
ProtocolVersion getProtocolVersion();
@NotNull ChannelPipeline getPipeline();
@NotNull
SystemTimer getLoginTimer();
@NotNull InetAddress getInetAddress();
@NotNull
UUID getOfflineUuid();
@NotNull ProtocolVersion getProtocolVersion();
boolean isReceivedClientSettings();
void setReceivedClientSettings(final boolean receivedClientSettings);
boolean isReceivedPluginMessage();
void setReceivedPluginMessage(final boolean receivedPluginMessage);
boolean isGeyser();
/**
* Kicks the player from the server with
* the given disconnect message.
* Disconnect the player during/after verification
* using our custom Disconnect packet.
*
* @param reason Legacy disconnect message string
* @see #disconnect(Component)
*/
default void disconnect(final @NotNull String reason) {
disconnect(Component.text(reason));
}
/**
* Kicks the player from the server with
* the given disconnect message.
*
* @param reason Disconnect message component
* @param reason Disconnect message component
*/
void disconnect(final @NotNull Component reason);
/**
* Takes over the channel and begins the verification process
*
* @param username Username of the player
* @param offlineUuid UUID of the player
*/
void hijack(final @NotNull String username, final @NotNull UUID offlineUuid);
/**
* Sends a packet/message to the player
*
* @param msg Message to send to the player
*/
void write(final @NotNull Object msg);
default void write(final @NotNull Object msg) {
if (getChannel().isActive()) {
getChannel().writeAndFlush(msg, getChannel().voidPromise());
} else {
ReferenceCountUtil.release(msg);
}
}
ExpiringCache<String> PREVIOUS_FAILS = Cappuccino.buildExpiring(3L, TimeUnit.MINUTES);
/**
* Queues a buffered message that will be
* sent once all messages are flushed.
*/
default void delayedWrite(final @NotNull Object msg) {
if (getChannel().isActive()) {
getChannel().write(msg, getChannel().voidPromise());
} else {
ReferenceCountUtil.release(msg);
}
}
/**
* Disconnects the player who failed the verification
@@ -83,39 +105,8 @@ public interface FallbackUser<X, Y> {
* the player will be temporarily denied from verifying.
*
* @param reason Reason for failing the verification
* @apiNote The {@link xyz.jonesdev.sonar.api.event.impl.UserVerifyFailedEvent}
* will not be thrown if no reason is given
*/
default void fail(final @Nullable String reason) {
if (getChannel().isActive()) {
// Call the VerifyFailedEvent for external API usage
Sonar.get().getEventManager().publish(new UserVerifyFailedEvent(this, reason));
disconnect(Sonar.get().getConfig().getVerificationFailed());
if (reason != null) {
getFallback().getLogger().info(Sonar.get().getConfig().getVerificationFailedLog()
.replace("%ip%", Sonar.get().getConfig().formatAddress(getInetAddress()))
.replace("%protocol%", String.valueOf(getProtocolVersion().getProtocol()))
.replace("%reason%", reason));
}
}
Statistics.FAILED_VERIFICATIONS.increment();
// Make sure old entries are removed
PREVIOUS_FAILS.cleanUp();
// Check if the player has too many failed attempts
if (PREVIOUS_FAILS.has(getInetAddress().toString())) {
// Call the BotBlacklistedEvent for external API usage
Sonar.get().getEventManager().publish(new UserBlacklistedEvent(this));
getFallback().getBlacklisted().put(getInetAddress().toString());
getFallback().getLogger().info(Sonar.get().getConfig().getVerificationBlacklistLog()
.replace("%ip%", Sonar.get().getConfig().formatAddress(getInetAddress()))
.replace("%protocol%", String.valueOf(getProtocolVersion().getProtocol())));
} else {
// Cache the InetAddress for 3 minutes
PREVIOUS_FAILS.put(getInetAddress().toString());
}
}
void fail(final @NotNull String reason);
}
@@ -0,0 +1,25 @@
/*
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.api.fallback.captcha;
import java.awt.image.BufferedImage;
@FunctionalInterface
public interface CaptchaGenerator {
BufferedImage createImage(final char[] answer);
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 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,17 +17,16 @@
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
public enum ProtocolVersion {
UNKNOWN(-1),
LEGACY(-2),
MINECRAFT_1_7_2(4),
MINECRAFT_1_7_6(5),
MINECRAFT_1_8(47),
@@ -66,53 +65,36 @@ public enum ProtocolVersion {
MINECRAFT_1_19_3(761),
MINECRAFT_1_19_4(762),
MINECRAFT_1_20(763),
MINECRAFT_1_20_2(-1);
MINECRAFT_1_20_2(764),
MINECRAFT_1_20_3(765),
MINECRAFT_1_20_5(766),
MINECRAFT_1_21(767);
private final int protocol;
private final int snapshotProtocol = -1;
public static final ProtocolVersion MINIMUM_VERSION = MINECRAFT_1_7_2;
public static final ProtocolVersion LATEST_VERSION;
public static final Map<Integer, ProtocolVersion> ID_TO_PROTOCOL_CONSTANT;
public static final Set<ProtocolVersion> SUPPORTED_VERSIONS;
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);
versions.put(version.snapshotProtocol, 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() && !value.isLegacy()) {
versions.add(value);
}
}
SUPPORTED_VERSIONS = versions;
}
ID_TO_PROTOCOL_CONSTANT = versions;
LATEST_VERSION = values()[values().length - 1];
}
public static ProtocolVersion fromId(final int protocol) {
return ID_TO_PROTOCOL_CONSTANT.getOrDefault(protocol, UNKNOWN);
}
public int getProtocol() {
return protocol == -1 ? snapshotProtocol : protocol;
public boolean inBetween(final ProtocolVersion first, final ProtocolVersion last) {
return compareTo(first) >= 0 && compareTo(last) <= 0;
}
public boolean isUnknown() {
return this == UNKNOWN;
}
public boolean isLegacy() {
return this == LEGACY;
}
}
@@ -0,0 +1,107 @@
/*
* 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.api.jvm;
import com.sun.management.OperatingSystemMXBean;
import lombok.experimental.UtilityClass;
import java.lang.management.ManagementFactory;
import java.text.DecimalFormat;
@UtilityClass
public class JVMProcessInformation {
private final Runtime RUNTIME = Runtime.getRuntime();
private final OperatingSystemMXBean MX = (OperatingSystemMXBean) ManagementFactory.getOperatingSystemMXBean();
private final char[] MEMORY_UNITS = {'K', 'M', 'G', 'T', 'P', 'E'};
private final DecimalFormat DECIMAL_FORMAT = new DecimalFormat("#.#");
public String formatMemory(final long size) {
if (size < 1024L) {
return size + " B";
}
// https://stackoverflow.com/questions/3758606/how-can-i-convert-byte-size-into-a-human-readable-format-in-java
final int group = (63 - Long.numberOfLeadingZeros(size)) / 10;
final double formattedSize = (double) size / (1L << (group * 10));
final char unit = MEMORY_UNITS[group - 1];
// https://en.wikipedia.org/wiki/Byte#Multiple-byte_units
return DECIMAL_FORMAT.format(formattedSize) + " " + unit + "iB";
}
/**
* @return Number of available processors for the JVM
*/
public int getVirtualCores() {
return RUNTIME.availableProcessors();
}
/**
* @return Current process CPU usage in percent
*/
public double getProcessCPUUsage() {
return MX.getProcessCpuLoad() * 100;
}
/**
* @return Current per-core process CPU usage in percent
*/
public double getAverageProcessCPUUsage() {
return getProcessCPUUsage() / getVirtualCores();
}
/**
* @return Current system CPU usage in percent
*/
public double getSystemCPUUsage() {
return MX.getSystemCpuLoad() * 100;
}
/**
* @return Current per-core system CPU usage in percent
*/
public double getAverageSystemCPUUsage() {
return getSystemCPUUsage() / getVirtualCores();
}
/**
* @return Memory (in bytes) the JVM will attempt to use
*/
public long getMaxMemory() {
return RUNTIME.maxMemory();
}
/**
* @return Memory (in bytes) available to the host machine
*/
public long getTotalMemory() {
return RUNTIME.totalMemory();
}
/**
* @return Memory (in bytes) the JVM can allocate
*/
public long getFreeMemory() {
return RUNTIME.freeMemory();
}
/**
* @return Memory (in bytes) the JVM is using
*/
public long getUsedMemory() {
return getTotalMemory() - getFreeMemory();
}
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 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,10 @@
package xyz.jonesdev.sonar.api.logger;
import org.jetbrains.annotations.NotNull;
import java.lang.reflect.Array;
/**
* The whole purpose of doing this is, so we can have one logger for every module.
* We wouldn't need to get the plugin logger every time we try to use it.
@@ -28,4 +32,62 @@ public interface LoggerWrapper {
void warn(final String message, final Object... args);
void error(final String message, final Object... args);
// Taken from
// https://github.com/j256/ormlite-core/blob/master/src/main/java/com/j256/ormlite/logger/Logger.java
String ARG_STRING = "{}";
int ARG_STRING_LENGTH = ARG_STRING.length();
Object UNKNOWN_ARG = new Object();
default String buildFullMessage(final @NotNull String msg, final Object... args) {
StringBuilder sb = null;
int lastIndex = 0;
int argC = 0;
while (true) {
int argIndex = msg.indexOf(ARG_STRING, lastIndex);
// no more {} arguments?
if (argIndex == -1) {
break;
}
if (sb == null) {
// we build this lazily in case there is no {} in the msg
sb = new StringBuilder(128);
}
// add the string before the arg-string
sb.append(msg, lastIndex, argIndex);
// shift our last-index past the arg-string
lastIndex = argIndex + ARG_STRING_LENGTH;
// add the arguments
if (argC < args.length) {
appendArg(sb, args[argC]);
}
argC++;
}
if (sb == null) {
return msg;
} else {
sb.append(msg, lastIndex, msg.length());
return sb.toString();
}
}
default void appendArg(final StringBuilder stringBuilder, final Object arg) {
if (arg == UNKNOWN_ARG) {
// ignore it
} else if (arg == null) {
stringBuilder.append("null");
} else if (arg.getClass().isArray()) {
stringBuilder.append('[');
int length = Array.getLength(arg);
for (int i = 0; i < length; i++) {
if (i > 0) {
stringBuilder.append(", ");
}
appendArg(stringBuilder, Array.get(arg, i));
}
stringBuilder.append(']');
} else {
stringBuilder.append(arg);
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 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,13 +23,16 @@ 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;
@Getter
@ToString
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@DatabaseTable(tableName = "verified_players")
@DatabaseTable(tableName = "sonar_verified_players")
public final class VerifiedPlayer {
@SuppressWarnings("unused")
@DatabaseField(generatedId = true)
@@ -49,14 +52,25 @@ public final class VerifiedPlayer {
uniqueIndexName = "ip_address_player_uuid_idx",
width = 36
)
private UUID playerUUID;
private UUID playerUuid;
@DatabaseField(columnName = "timestamp", canBeNull = false)
private long timestamp;
@DatabaseField(
columnName = "timestamp",
canBeNull = false
)
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.timestamp = timestamp;
this.playerUuid = playerUuid;
this.timestamp = new Timestamp(timestamp);
}
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 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,17 +15,17 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.velocity.command;
package xyz.jonesdev.sonar.api.ormlite;
import com.velocitypowered.api.command.CommandSource;
import com.velocitypowered.api.proxy.Player;
import com.j256.ormlite.jdbc.db.H2DatabaseType;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.command.InvocationSource;
public final class VelocityInvocationSource extends InvocationSource {
public VelocityInvocationSource(final @NotNull CommandSource sender) {
super(sender instanceof Player
? ((Player) sender).getUsername()
: "Console", sender);
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 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,21 +15,17 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.subcommand;
package xyz.jonesdev.sonar.api.ormlite;
import xyz.jonesdev.sonar.api.command.subcommand.SubcommandRegistry;
import xyz.jonesdev.sonar.common.subcommand.impl.*;
import com.j256.ormlite.jdbc.db.MariaDbDatabaseType;
import org.jetbrains.annotations.NotNull;
public final class SubcommandRegistryHolder implements SubcommandRegistry {
public SubcommandRegistryHolder() {
register(
new BlacklistCommand(),
new VerifiedCommand(),
new StatisticsCommand(),
new LockdownCommand(),
new VerboseCommand(),
new ReloadCommand(),
new DumpCommand()
);
public final class MariaDbDatabaseTypeAdapter extends MariaDbDatabaseType {
// 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 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,24 +15,17 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.api.dependencies;
package xyz.jonesdev.sonar.api.ormlite;
import java.net.URL;
import java.net.URLClassLoader;
import com.j256.ormlite.jdbc.db.MysqlDatabaseType;
import org.jetbrains.annotations.NotNull;
final class ExternalClassLoader extends URLClassLoader {
public final class MysqlDatabaseTypeAdapter extends MysqlDatabaseType {
/**
* Create a URLClassLoader with the system ClassLoader as parent
*
* @param url URL for of the local file
*/
public ExternalClassLoader(final URL[] url) {
super(url, ClassLoader.getSystemClassLoader().getParent());
}
// Register this URLClassLoader as parallel capable
static {
ClassLoader.registerAsParallelCapable();
// 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,75 +0,0 @@
/*
* Copyright (C) 2023 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.api.profiler;
import com.sun.management.OperatingSystemMXBean;
import java.lang.management.ManagementFactory;
import java.lang.management.RuntimeMXBean;
public interface JVMProfiler {
Runtime RUNTIME = Runtime.getRuntime();
OperatingSystemMXBean OPERATING_SYSTEM_MX_BEAN = (OperatingSystemMXBean) ManagementFactory.getOperatingSystemMXBean();
RuntimeMXBean RUNTIME_MX_BEAN = ManagementFactory.getRuntimeMXBean();
default String formatMemory(final long m) {
if (m < 1024L) return m + " B";
final int z = (63 - Long.numberOfLeadingZeros(m)) / 10;
return String.format("%.1f %sB", (double) m / (1L << (z * 10)), " KMGTPE".charAt(z));
}
default int getVirtualCores() {
return RUNTIME.availableProcessors();
}
default double getProcessCPUUsage() {
return OPERATING_SYSTEM_MX_BEAN.getProcessCpuLoad() * 100;
}
default double getAverageProcessCPUUsage() {
return getProcessCPUUsage() / getVirtualCores();
}
default double getSystemCPUUsage() {
return OPERATING_SYSTEM_MX_BEAN.getSystemCpuLoad() * 100;
}
default double getAverageSystemCPUUsage() {
return getSystemCPUUsage() / getVirtualCores();
}
default double getSystemLoadAverage() {
return OPERATING_SYSTEM_MX_BEAN.getSystemLoadAverage() * 100;
}
default long getMaxMemory() {
return RUNTIME.maxMemory();
}
default long getTotalMemory() {
return RUNTIME.totalMemory();
}
default long getFreeMemory() {
return RUNTIME.freeMemory();
}
default long getUsedMemory() {
return getTotalMemory() - getFreeMemory();
}
}
@@ -1,42 +0,0 @@
/*
* Copyright (C) 2023 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.api.server;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import xyz.jonesdev.sonar.api.SonarPlatform;
import xyz.jonesdev.sonar.api.command.InvocationSource;
import java.util.Optional;
@Getter
@RequiredArgsConstructor
public abstract class ServerWrapper {
/**
* Platform of the server (Velocity, BungeeCord, Spigot)
*/
private final SonarPlatform platform;
/**
* @param username Username of the player
* @return Optional player wrapped as InvocationSender
* @see InvocationSource
*/
public abstract Optional<InvocationSource> getOnlinePlayer(final String username);
}
@@ -0,0 +1,118 @@
/*
* 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.api.statistics;
import org.jetbrains.annotations.ApiStatus;
@SuppressWarnings("unused")
public interface SonarStatistics {
/**
* @return Number of connection establishments per second
*/
long getConnectionsPerSecond();
/**
* @return Number of logins per second
*/
long getLoginsPerSecond();
/**
* @return Currently incoming bytes per second
*/
long getCurrentIncomingBandwidth();
/**
* @return Currently outgoing bytes per second
*/
long getCurrentOutgoingBandwidth();
/**
* @return Total incoming bytes per second
*/
long getTotalIncomingBandwidth();
/**
* @return Total outgoing bytes per second
*/
long getTotalOutgoingBandwidth();
/**
* @return Same as {@link SonarStatistics#getCurrentIncomingBandwidth} but formatted
*/
String getCurrentIncomingBandwidthFormatted();
/**
* @return Same as {@link SonarStatistics#getCurrentOutgoingBandwidth} but formatted
*/
String getCurrentOutgoingBandwidthFormatted();
/**
* @return Same as {@link SonarStatistics#getTotalIncomingBandwidth} but formatted
*/
String getTotalIncomingBandwidthFormatted();
/**
* @return Same as {@link SonarStatistics#getTotalOutgoingBandwidth} but formatted
*/
String getTotalOutgoingBandwidthFormatted();
/**
* @return Total number of players who logged into the server
*/
int getTotalPlayersJoined();
/**
* @return Total number of players who tried to verify
*/
int getTotalPlayersVerified();
/**
* @return Total number of players who passed the verification
*/
int getTotalSuccessfulVerifications();
/**
* @return Total number of players who failed the verification
*/
int getTotalFailedVerifications();
/**
* @return Total number of players who passed the verification
*/
long getCurrentAttemptedVerifications();
/**
* @return Total number of players who tried verifying
*/
int getTotalAttemptedVerifications();
/**
* @return Current number of IP addresses that are blacklisted
*/
long getCurrentBlacklistSize();
/**
* @return Total number of cases where an IP was blacklisted
* @apiNote This does not describe the current number of blacklisted IPs
*/
long getTotalBlacklistSize();
@ApiStatus.Internal
void cleanUpCache();
}
@@ -1,68 +0,0 @@
/*
* Copyright (C) 2023 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.api.statistics;
public enum Statistics {
TOTAL_TRAFFIC,
REAL_TRAFFIC,
FAILED_VERIFICATIONS;
private int val = -1;
/**
* Increments the current value by 1 with the default value of 0
*
* @see #get(int)
* @see #set(int)
*/
public void increment() {
increment(0);
}
/**
* Increments the current value by 1 with a custom default value
*
* @see #get(int)
* @see #set(int)
*/
public void increment(final int def) {
set(get(def) + 1);
}
/**
* @return The current value with the default value of 0
* @see #get(int)
*/
public int get() {
return get(0);
}
/**
* @return The current value with a custom default value
*/
public int get(final int def) {
return val == -1 ? def : val;
}
/**
* Set current value to a new value
*/
public void set(final int value) {
val = value;
}
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 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,18 +19,28 @@ package xyz.jonesdev.sonar.api.timer;
import lombok.Getter;
import java.text.SimpleDateFormat;
@Getter
public final class SystemTimer {
public static final SimpleDateFormat DATE_FORMATTER = new SimpleDateFormat("mm:ss");
private long start = System.currentTimeMillis();
public void reset() {
start = System.currentTimeMillis();
}
/**
* @return Time between the current timestamp and the start timestamp
*/
public long delay() {
return System.currentTimeMillis() - start;
}
/**
* @apiNote Read more: {@link SystemTimer#delay}
*/
public boolean elapsed(final long amount) {
return delay() >= amount;
}
@@ -0,0 +1,89 @@
/*
* 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.api.verbose;
import lombok.Getter;
import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
import net.kyori.adventure.title.Title;
import xyz.jonesdev.sonar.api.Sonar;
import java.util.List;
import java.util.UUID;
import static xyz.jonesdev.sonar.api.Sonar.DECIMAL_FORMAT;
import static xyz.jonesdev.sonar.api.jvm.JVMProcessInformation.*;
@Getter
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())));
}
// 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);
}
}
}
@@ -0,0 +1,54 @@
/*
* 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.api.verbose;
import lombok.Getter;
import org.jetbrains.annotations.NotNull;
import java.util.Collection;
import java.util.UUID;
import java.util.Vector;
@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
*/
public final boolean isSubscribed(final @NotNull UUID uuid) {
return getSubscribers().contains(uuid);
}
/**
* @param uuid UUID of the player
*/
public final void subscribe(final UUID uuid) {
getSubscribers().add(uuid);
}
/**
* @param uuid UUID of the player
*/
public final void unsubscribe(final UUID uuid) {
getSubscribers().remove(uuid);
}
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 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,99 +18,73 @@
package xyz.jonesdev.sonar.api.verbose;
import lombok.Getter;
import lombok.val;
import org.jetbrains.annotations.NotNull;
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 xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.profiler.JVMProfiler;
import xyz.jonesdev.sonar.api.statistics.Statistics;
import xyz.jonesdev.sonar.api.attack.AttackTracker;
import xyz.jonesdev.sonar.api.timer.SystemTimer;
import java.util.Collection;
import java.util.Vector;
import java.util.UUID;
import static xyz.jonesdev.sonar.api.Sonar.DECIMAL_FORMAT;
import static xyz.jonesdev.sonar.api.fallback.traffic.TrafficCounter.INCOMING;
import static xyz.jonesdev.sonar.api.fallback.traffic.TrafficCounter.OUTGOING;
import static xyz.jonesdev.sonar.api.jvm.JVMProcessInformation.*;
import static xyz.jonesdev.sonar.api.timer.SystemTimer.DATE_FORMATTER;
@Getter
public abstract class Verbose implements JVMProfiler {
protected final @NotNull Collection<String> subscribers = new Vector<>(0);
private final SystemTimer secondTimer = new SystemTimer();
protected int joinsPerSecond, totalJoins;
private int lastTotalJoins, animationIndex;
public final class Verbose extends Observable {
private int animationIndex;
// Run action bar verbose
public final void update() {
// Clean up all blacklisted IPs
Sonar.get().getFallback().getBlacklisted().cleanUp(false);
totalJoins = Statistics.TOTAL_TRAFFIC.get();
// Statistically determine the joins per second without any caches
if (totalJoins > 0 && secondTimer.elapsed(1000L)) {
secondTimer.reset();
joinsPerSecond = totalJoins - lastTotalJoins;
lastTotalJoins = totalJoins;
@Override
public void observe() {
// Don't prepare component if there are no subscribers
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
broadcast(prepareActionBarFormat());
for (final UUID subscriber : subscribers) {
final Audience audience = Sonar.get().audience(subscriber);
if (audience == null) continue;
// Send the action bar to all online subscribers
audience.sendActionBar(actionBarComponent);
}
}
protected @NotNull String prepareActionBarFormat() {
return Sonar.get().getConfig().getActionBarLayout()
.replace("%queued%",
DECIMAL_FORMAT.format(Sonar.get().getFallback().getQueue().getQueuedPlayers().size()))
.replace("%verifying%", DECIMAL_FORMAT.format(Sonar.get().getFallback().getConnected().size()))
.replace("%blacklisted%",
DECIMAL_FORMAT.format(Sonar.get().getFallback().getBlacklisted().estimatedSize()))
.replace("%total-joins%", DECIMAL_FORMAT.format(totalJoins))
.replace("%per-second-joins%", DECIMAL_FORMAT.format(joinsPerSecond))
.replace("%verify-total%", DECIMAL_FORMAT.format(Statistics.REAL_TRAFFIC.get()))
.replace("%verify-success%",
DECIMAL_FORMAT.format(Sonar.get().getVerifiedPlayerController().estimatedSize()))
.replace("%verify-failed%", DECIMAL_FORMAT.format(Statistics.FAILED_VERIFICATIONS.get()))
.replace("%incoming-traffic%", INCOMING.getCachedSecond())
.replace("%outgoing-traffic%", OUTGOING.getCachedSecond())
.replace("%incoming-traffic-ttl%", INCOMING.getCachedTtl())
.replace("%outgoing-traffic-ttl%", OUTGOING.getCachedTtl())
.replace("%used-memory%", formatMemory(getUsedMemory()))
.replace("%free-memory%", formatMemory(getFreeMemory()))
.replace("%total-memory%", formatMemory(getTotalMemory()))
.replace("%max-memory%", formatMemory(getMaxMemory()))
.replace("%animation%", nextAnimation());
}
protected final String nextAnimation() {
val animations = Sonar.get().getConfig().getAnimation();
public String nextAnimation() {
final var animations = Sonar.get().getConfig().getVerboseAnimation();
final int nextIndex = ++animationIndex % animations.size();
return animations.toArray(new String[0])[nextIndex];
}
// Run action bar verbose
protected void broadcast(final String message) {
// This should be replaced by the wrapper
}
/**
* @param subscriber Name of the player who subscribed
* @return Whether the player is subscribed or not
*/
public final boolean isSubscribed(final @NotNull String subscriber) {
return subscribers.contains(subscriber);
}
/**
* @param subscriber Name of the player to subscribe
*/
public final void subscribe(final @NotNull String subscriber) {
subscribers.add(subscriber);
}
/**
* @param subscriber Name of the player to unsubscribe
*/
public final void unsubscribe(final @NotNull String subscriber) {
subscribers.remove(subscriber);
}
}
@@ -0,0 +1,133 @@
/*
* 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.api.webhook;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.config.SonarConfiguration;
import javax.net.ssl.HttpsURLConnection;
import java.io.OutputStream;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.function.Supplier;
import static java.net.HttpURLConnection.HTTP_NO_CONTENT;
@RequiredArgsConstructor
public final class DiscordWebhook {
private final String url;
private static final Gson GSON = new GsonBuilder().create();
private static final ExecutorService HTTP_REQUEST_SERVICE = Executors.newSingleThreadExecutor();
/**
* Asynchronously posts a Discord webhook via HTTPS
*/
public void post(final Supplier<SonarConfiguration.Webhook.Embed> embed) {
HTTP_REQUEST_SERVICE.execute(() -> {
try {
final String serializedContent = prepareSerializedContent(embed.get());
final HttpsURLConnection urlConnection = prepareConnection(new URL(url), serializedContent);
try (final OutputStream outputStream = urlConnection.getOutputStream()) {
outputStream.write(serializedContent.getBytes(StandardCharsets.UTF_8));
}
// 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);
}
} catch (Exception exception) {
Sonar.get().getLogger().error("Could not send webhook: {}", exception);
}
});
}
private static String prepareSerializedContent(final SonarConfiguration.Webhook.@NotNull Embed embed) {
final String content = Sonar.get().getConfig().getWebhook().getContent();
final String username = Sonar.get().getConfig().getWebhook().getUsername();
final String avatarUrl = Sonar.get().getConfig().getWebhook().getAvatarUrl();
int rgb = embed.getR();
rgb = (rgb << 8) + embed.getG();
rgb = (rgb << 8) + embed.getB();
final Webhook.EmbedMessage embedMessage = new Webhook.EmbedMessage(
embed.getTitle(), embed.getDescription(), embed.getTitleUrl(), rgb, new Webhook.Field[0],
new Webhook.Footer(Sonar.get().getConfig().getWebhook().getFooter().getText(),
Sonar.get().getConfig().getWebhook().getFooter().getIconUrl()));
final List<Webhook.EmbedMessage> embeds = Collections.singletonList(embedMessage);
final Webhook webhook = new Webhook(content, username, avatarUrl, false, embeds);
return GSON.toJson(webhook);
}
private @NotNull HttpsURLConnection prepareConnection(final @NotNull URL url,
final @NotNull String serializedContent) throws Exception {
final HttpsURLConnection urlConnection = (HttpsURLConnection) url.openConnection();
// Set connection timeout
urlConnection.setConnectTimeout(15000);
// Set necessary properties
urlConnection.setRequestProperty("Content-Type", "application/json");
urlConnection.setRequestProperty("Content-Length", String.valueOf(serializedContent.length()));
urlConnection.setRequestProperty("User-Agent", "Sonar");
urlConnection.setDoOutput(true);
urlConnection.setRequestMethod("POST");
return urlConnection;
}
@SuppressWarnings("unused")
@RequiredArgsConstructor
static final class Webhook {
private final String content, username, avatar_url;
private final boolean tts;
private final List<EmbedMessage> embeds;
@RequiredArgsConstructor
static final class EmbedMessage {
private final String title;
private final String description;
private final String url;
private final int color;
private final Field[] fields;
private final Footer footer;
}
@RequiredArgsConstructor
static final class Field {
private final String name;
private final String value;
private final boolean inline;
}
@RequiredArgsConstructor
static final class Footer {
private final String text;
private final String icon_url;
}
}
}
+23 -17
View File
@@ -1,23 +1,23 @@
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/"
description = "Effective Anti-bot plugin for Velocity, BungeeCord, and Bukkit (1.7-latest)"
load = BukkitPluginDescription.PluginLoadOrder.POSTWORLD
softDepend = listOf("Geyser-Spigot", "floodgate", "Protocolize", "ProtocolSupport",
"ViaVersion", "packetevents", "ProtocolLib", "FastLogin")
apiVersion = "1.13" // ignore legacy plugin warning
foliaSupported = true
commands {
register("sonar") {
// No permission check here
// No permission checks here
}
}
}
@@ -28,18 +28,24 @@ repositories {
}
dependencies {
compileOnly(project(":api"))
compileOnly(project(":common"))
implementation(project(":api"))
implementation(project(":common"))
// MiniMessage platform support
implementation("net.kyori:adventure-platform-bukkit:4.3.1")
compileOnly(rootProject.libs.spigot)
testCompileOnly(rootProject.libs.spigot)
// We have to use 1.8 for backwards compatibility
compileOnly("org.spigotmc:spigot-api:1.8.8-R0.1-SNAPSHOT")
// Implement bStats.org for metrics
implementation("org.bstats:bstats-bukkit:3.0.2")
implementation(rootProject.libs.adventure.platform.bukkit) {
exclude(module = "adventure-nbt")
}
implementation(rootProject.libs.bstats.bukkit)
implementation(rootProject.libs.libby.bukkit)
}
java.sourceCompatibility = JavaVersion.VERSION_1_8
java.targetCompatibility = JavaVersion.VERSION_1_8
tasks {
shadowJar {
relocate("net.kyori", "xyz.jonesdev.sonar.libs.kyori")
}
}
java.sourceCompatibility = JavaVersion.VERSION_11
java.targetCompatibility = JavaVersion.VERSION_11
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 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,48 @@
package xyz.jonesdev.sonar.bukkit;
import com.alessiodp.libby.BukkitLibraryManager;
import lombok.Getter;
import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.platform.bukkit.BukkitAudiences;
import org.bstats.bukkit.Metrics;
import org.bstats.charts.SimplePie;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import xyz.jonesdev.sonar.api.SonarPlatform;
import xyz.jonesdev.sonar.api.command.InvocationSource;
import xyz.jonesdev.sonar.api.fallback.traffic.TrafficCounter;
import xyz.jonesdev.sonar.api.logger.LoggerWrapper;
import xyz.jonesdev.sonar.api.server.ServerWrapper;
import xyz.jonesdev.sonar.bukkit.command.BukkitInvocationSource;
import xyz.jonesdev.sonar.bukkit.command.BukkitSonarCommand;
import xyz.jonesdev.sonar.bukkit.verbose.ActionBarVerbose;
import xyz.jonesdev.sonar.bukkit.fallback.FallbackBukkitInjector;
import xyz.jonesdev.sonar.bukkit.listener.BukkitJoinListener;
import xyz.jonesdev.sonar.common.boot.SonarBootstrap;
import java.util.Objects;
import java.util.Optional;
import java.util.logging.Level;
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, plugin.getDataFolder(), new ActionBarVerbose(plugin.getServer()));
INSTANCE = this;
super(plugin, SonarPlatform.BUKKIT, plugin.getDataFolder(), new BukkitLibraryManager(plugin));
}
/**
* Wrapper for Bukkit audiences
*/
private final BukkitAudiences bukkitAudiences = BukkitAudiences.create(getPlugin());
@Override
public @Nullable Audience audience(final @Nullable UUID uniqueId) {
if (uniqueId == null) {
return null;
}
return bukkitAudiences.player(uniqueId);
}
@Override
public @NotNull Audience sender(final @NotNull Object object) {
return bukkitAudiences.sender((CommandSender) object);
}
/**
@@ -53,61 +71,58 @@ public final class SonarBukkit extends SonarBootstrap<SonarBukkitPlugin> {
@Override
public void info(final String message, final Object... args) {
getPlugin().getLogger().log(Level.INFO, message, args);
getPlugin().getLogger().info(buildFullMessage(message, args));
}
@Override
public void warn(final String message, final Object... args) {
getPlugin().getLogger().log(Level.WARNING, message, args);
getPlugin().getLogger().warning(buildFullMessage(message, args));
}
@Override
public void error(final String message, final Object... args) {
getPlugin().getLogger().log(Level.SEVERE, message, args);
getPlugin().getLogger().severe(buildFullMessage(message, args));
}
};
/**
* Create a wrapper object for our server, so we can use it outside
* the velocity module.
* <br>
* We have to do this, so we can access all necessary API functions.
*/
public final ServerWrapper server = new ServerWrapper(SonarPlatform.BUKKIT) {
private Metrics metrics;
@Override
public Optional<InvocationSource> getOnlinePlayer(final String username) {
return getPlugin().getServer().getOnlinePlayers().stream()
.filter(player -> player.getName().equalsIgnoreCase(username))
.findFirst()
.map(BukkitInvocationSource::new);
}
};
public static final CompletableFuture<Void> INITIALIZE_LISTENER = new CompletableFuture<>();
@Override
public void enable() {
// Initialize bStats.org metrics
new Metrics(getPlugin(), getServer().getPlatform().getMetricsId());
metrics = new Metrics(getPlugin(), getPlatform().getMetricsId());
// Add charts for some configuration options
metrics.addCustomChart(new SimplePie("verification",
() -> getConfig().getVerification().getTiming().getDisplayName()));
metrics.addCustomChart(new SimplePie("captcha",
() -> getConfig().getVerification().getMap().getTiming().getDisplayName()));
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 Fallback queue task
getPlugin().getServer().getScheduler().runTaskTimerAsynchronously(getPlugin(), getFallback().getQueue()::poll,
10L, 10L);
// 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 traffic counter reset task
getPlugin().getServer().getScheduler().runTaskTimerAsynchronously(getPlugin(), TrafficCounter::reset,
20L, 20L);
// Register action bar verbose task
getPlugin().getServer().getScheduler().runTaskTimerAsynchronously(getPlugin(), getVerboseHandler()::update,
2L, 2L);
// Let the injector know that the plugin has been enabled
INITIALIZE_LISTENER.complete(null);
}
@Override
public void postReload() {
// Do nothing
public void disable() {
if (metrics != null) {
// Make sure to properly shutdown bStats metrics
metrics.shutdown();
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 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,32 +0,0 @@
/*
* Copyright (C) 2023 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.bukkit.command;
import net.kyori.adventure.platform.bukkit.BukkitAudiences;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.command.InvocationSource;
import xyz.jonesdev.sonar.bukkit.SonarBukkit;
public final class BukkitInvocationSource extends InvocationSource {
private static final BukkitAudiences AUDIENCES = BukkitAudiences.create(SonarBukkit.INSTANCE.getPlugin());
public BukkitInvocationSource(final @NotNull CommandSender sender) {
super(sender.getName(), AUDIENCES.sender(sender));
}
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 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,128 +17,39 @@
package xyz.jonesdev.sonar.bukkit.command;
import net.kyori.adventure.text.Component;
import org.bukkit.command.*;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabExecutor;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.command.CommandInvocation;
import xyz.jonesdev.sonar.api.command.InvocationSource;
import xyz.jonesdev.sonar.api.command.SonarCommand;
import xyz.jonesdev.sonar.api.command.subcommand.Subcommand;
import xyz.jonesdev.sonar.api.command.subcommand.argument.Argument;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import static java.util.Collections.emptyList;
public final class BukkitSonarCommand implements CommandExecutor, TabExecutor, SonarCommand {
{
cacheHelpMessage();
}
@Override
public boolean onCommand(final CommandSender sender,
final Command command,
final String label,
final String[] args) {
if (!(sender instanceof ConsoleCommandSender)) {
// Check if the player actually has the permission to run the command
if (!sender.hasPermission("sonar.command")) {
sender.sendMessage(Sonar.get().getConfig().getNoPermission());
return false;
}
// Checking if it contains will only break more since it can throw
// a NullPointerException if the cache is being accessed from parallel threads
DELAY.cleanUp(); // Clean up the cache
final long mapTimestamp = DELAY.asMap().getOrDefault(sender, -1L);
// There were some exploits with spamming commands in the past.
// Spamming should be prevented, especially if some heavy operations are done,
// which is not the case here but let's still stay safe!
if (mapTimestamp > 0L) {
sender.sendMessage(Sonar.get().getConfig().getCommandCoolDown());
// Format delay
final long timestamp = System.currentTimeMillis();
final double left = 0.5D - (timestamp - mapTimestamp) / 1000D;
sender.sendMessage(
Sonar.get().getConfig().getCommandCoolDownLeft()
.replace("%time-left%", Sonar.DECIMAL_FORMAT.format(left))
);
return false;
}
DELAY.put(sender);
}
Optional<Subcommand> subcommand = Optional.empty();
final InvocationSource invocationSource = new BukkitInvocationSource(sender);
if (args.length > 0) {
// Search subcommand if command arguments are present
subcommand = Sonar.get().getSubcommandRegistry().getSubcommands().parallelStream()
.filter(sub -> sub.getInfo().name().equalsIgnoreCase(args[0])
|| Arrays.stream(sub.getInfo().aliases())
.anyMatch(alias -> alias.equalsIgnoreCase(args[0])))
.findFirst();
// Check permissions for subcommands
if (subcommand.isPresent()) {
if (!subcommand.get().getInfo().onlyConsole()
&& !sender.hasPermission(subcommand.get().getPermission())
) {
invocationSource.sendMessage(Sonar.get().getConfig().getSubCommandNoPerm()
.replace("%permission%", subcommand.get().getPermission()));
return false;
}
}
}
subcommand.ifPresent(sub -> {
if (sub.getInfo().onlyPlayers() && !(sender instanceof Player)) {
invocationSource.sendMessage(Sonar.get().getConfig().getPlayersOnly());
return;
}
if (sub.getInfo().onlyConsole() && !(sender instanceof ConsoleCommandSender)) {
invocationSource.sendMessage(Sonar.get().getConfig().getConsoleOnly());
return;
}
final CommandInvocation commandInvocation = new CommandInvocation(invocationSource, sub, args);
// The subcommands has arguments which are not present in the executed command
if (sub.getInfo().arguments().length > 0
&& commandInvocation.getRawArguments().length <= 1) {
invocationSource.sendMessage(Sonar.get().getConfig().getIncorrectCommandUsage()
.replace("%usage%", sub.getInfo().name() + " (" + sub.getArguments() + ")"));
return;
}
// Execute the sub command with the custom invocation properties
sub.execute(commandInvocation);
});
if (!subcommand.isPresent()) {
// Re-use the old, cached help message since we don't want to scan
// for each subcommand and it's arguments/attributes every time
// someone runs /sonar since the subcommand don't change
for (final Component component : CACHED_HELP_MESSAGE) {
invocationSource.sendMessage(component);
}
}
return false;
// Create our own invocation source wrapper to handle messages properly
final InvocationSource invocationSource = new InvocationSource(
sender instanceof Player ? ((Player) sender).getUniqueId() : null,
Sonar.get().sender(sender),
sender::hasPermission);
// Pass the invocation source and command arguments to our command handler
handle(invocationSource, args);
return true; // Valid
}
@Override
public List<String> onTabComplete(final CommandSender sender,
public List<String> onTabComplete(final @NotNull CommandSender sender,
final Command command,
final String commandAlias,
final String @NotNull [] args) {
@@ -146,33 +57,6 @@ public final class BukkitSonarCommand implements CommandExecutor, TabExecutor, S
if (!sender.hasPermission("sonar.command")) {
return emptyList();
}
if (args.length <= 1) {
if (TAB_SUGGESTIONS.isEmpty()) {
for (final Subcommand subcommand : Sonar.get().getSubcommandRegistry().getSubcommands()) {
TAB_SUGGESTIONS.add(subcommand.getInfo().name());
if (subcommand.getInfo().aliases().length > 0) {
TAB_SUGGESTIONS.addAll(Arrays.asList(subcommand.getInfo().aliases()));
}
}
}
return TAB_SUGGESTIONS;
} else if (args.length == 2) {
if (ARG_TAB_SUGGESTIONS.isEmpty()) {
for (final Subcommand subcommand : Sonar.get().getSubcommandRegistry().getSubcommands()) {
final List<String> parsedArguments = Arrays.stream(subcommand.getInfo().arguments())
.map(Argument::value)
.collect(Collectors.toList());
ARG_TAB_SUGGESTIONS.put(subcommand.getInfo().name(), parsedArguments);
for (final String alias : subcommand.getInfo().aliases()) {
ARG_TAB_SUGGESTIONS.put(alias, parsedArguments);
}
}
}
final String subCommandName = args[0].toLowerCase();
return ARG_TAB_SUGGESTIONS.getOrDefault(subCommandName, emptyList());
}
return emptyList();
return getCachedTabSuggestions(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);
}
}
}
@@ -1,42 +0,0 @@
/*
* Copyright (C) 2023 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.bukkit.verbose;
import lombok.RequiredArgsConstructor;
import net.md_5.bungee.api.chat.TextComponent;
import org.bukkit.Server;
import org.bukkit.entity.Player;
import xyz.jonesdev.sonar.api.verbose.Verbose;
@RequiredArgsConstructor
public final class ActionBarVerbose extends Verbose {
private final Server server;
public void broadcast(final String message) {
final TextComponent component = new TextComponent(message);
synchronized (subscribers) {
for (final String subscriber : subscribers) {
final Player player = server.getPlayer(subscriber);
if (player != null) {
//player.sendMessage(ChatMessageType.ACTION_BAR, component);
}
}
}
}
}
+19 -21
View File
@@ -1,35 +1,33 @@
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"
description = "Effective Anti-bot plugin for Velocity, BungeeCord, and Bukkit (1.7-latest)"
softDepends = setOf("Geyser-BungeeCord", "floodgate", "Protocolize", "ViaVersion", "packetevents", "FastLogin")
}
dependencies {
compileOnly(project(":api"))
compileOnly(project(":common"))
implementation(project(":api"))
implementation(project(":common"))
implementation("com.velocitypowered:velocity-native:1.1.9") {
exclude(group = "com.google.guava")
exclude(group = "io.netty")
exclude(group = "org.checkerframework")
compileOnly(rootProject.libs.bungeecord)
testCompileOnly(rootProject.libs.bungeecord)
implementation(rootProject.libs.adventure.platform.bungee) {
exclude(module = "adventure-nbt")
}
compileOnly("net.md_5:bungeecord:1.20.2-rc2-SNAPSHOT")
// MiniMessage platform support
implementation("net.kyori:adventure-platform-bungeecord:4.3.1")
// Implement bStats.org for metrics
implementation("org.bstats:bstats-bungeecord:3.0.2")
implementation(rootProject.libs.bstats.bungee)
implementation(rootProject.libs.libby.bungee)
}
java.sourceCompatibility = JavaVersion.VERSION_1_8
java.targetCompatibility = JavaVersion.VERSION_1_8
tasks {
shadowJar {
relocate("net.kyori", "xyz.jonesdev.sonar.libs.kyori")
}
}
java.sourceCompatibility = JavaVersion.VERSION_11
java.targetCompatibility = JavaVersion.VERSION_11
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 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,33 +17,45 @@
package xyz.jonesdev.sonar.bungee;
import com.alessiodp.libby.BungeeLibraryManager;
import lombok.Getter;
import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.platform.bungeecord.BungeeAudiences;
import net.md_5.bungee.api.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.SonarPlatform;
import xyz.jonesdev.sonar.api.command.InvocationSource;
import xyz.jonesdev.sonar.api.fallback.traffic.TrafficCounter;
import xyz.jonesdev.sonar.api.logger.LoggerWrapper;
import xyz.jonesdev.sonar.api.server.ServerWrapper;
import xyz.jonesdev.sonar.bungee.command.BungeeInvocationSource;
import xyz.jonesdev.sonar.bungee.command.BungeeSonarCommand;
import xyz.jonesdev.sonar.bungee.fallback.FallbackListener;
import xyz.jonesdev.sonar.bungee.fallback.injection.BaseInjectionHelper;
import xyz.jonesdev.sonar.bungee.fallback.injection.ChildChannelInitializer;
import xyz.jonesdev.sonar.bungee.verbose.VerboseWrapper;
import xyz.jonesdev.sonar.bungee.fallback.FallbackBungeeInjector;
import xyz.jonesdev.sonar.common.boot.SonarBootstrap;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.UUID;
@Getter
public final class SonarBungee extends SonarBootstrap<SonarBungeePlugin> {
public static SonarBungee INSTANCE;
public SonarBungee(final @NotNull SonarBungeePlugin plugin) {
super(plugin, plugin.getDataFolder(), new VerboseWrapper(plugin.getServer()));
INSTANCE = this;
super(plugin, SonarPlatform.BUNGEE, plugin.getDataFolder(), new BungeeLibraryManager(plugin));
}
/**
* Wrapper for BungeeCord audiences
*/
private final BungeeAudiences bungeeAudiences = BungeeAudiences.create(getPlugin());
@Override
public @Nullable Audience audience(final @Nullable UUID uniqueId) {
if (uniqueId == null) {
return null;
}
return bungeeAudiences.player(uniqueId);
}
@Override
public @NotNull Audience sender(final @NotNull Object object) {
return bungeeAudiences.sender((CommandSender) object);
}
/**
@@ -56,66 +68,49 @@ public final class SonarBungee extends SonarBootstrap<SonarBungeePlugin> {
@Override
public void info(final String message, final Object... args) {
getPlugin().getLogger().log(Level.INFO, message, args);
getPlugin().getLogger().info(buildFullMessage(message, args));
}
@Override
public void warn(final String message, final Object... args) {
getPlugin().getLogger().log(Level.WARNING, message, args);
getPlugin().getLogger().warning(buildFullMessage(message, args));
}
@Override
public void error(final String message, final Object... args) {
getPlugin().getLogger().log(Level.SEVERE, message, args);
getPlugin().getLogger().severe(buildFullMessage(message, args));
}
};
/**
* Create a wrapper object for our server, so we can use it outside
* the velocity module.
* <br>
* We have to do this, so we can access all necessary API functions.
*/
public final ServerWrapper server = new ServerWrapper(SonarPlatform.BUNGEE) {
@Override
public Optional<InvocationSource> getOnlinePlayer(final String username) {
return getPlugin().getServer().getPlayers().stream()
.filter(player -> player.getName().equalsIgnoreCase(username))
.findFirst()
.map(BungeeInvocationSource::new);
}
};
private Metrics metrics;
@Override
public void enable() {
// Initialize bStats.org metrics
new Metrics(getPlugin(), getServer().getPlatform().getMetricsId());
metrics = new Metrics(getPlugin(), getPlatform().getMetricsId());
// Add charts for some configuration options
metrics.addCustomChart(new SimplePie("verification",
() -> getConfig().getVerification().getTiming().getDisplayName()));
metrics.addCustomChart(new SimplePie("captcha",
() -> getConfig().getVerification().getMap().getTiming().getDisplayName()));
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 Fallback listener
getPlugin().getServer().getPluginManager().registerListener(getPlugin(), new FallbackListener(getFallback()));
// Register Fallback queue task
getPlugin().getServer().getScheduler().schedule(getPlugin(), getFallback().getQueue()::poll,
500L, 500L, TimeUnit.MILLISECONDS);
// Register traffic counter reset task
getPlugin().getServer().getScheduler().schedule(getPlugin(), TrafficCounter::reset,
1L, 1L, TimeUnit.SECONDS);
// Register action bar verbose task
getPlugin().getServer().getScheduler().schedule(getPlugin(), getVerboseHandler()::update,
100L, 100L, TimeUnit.MILLISECONDS);
// Inject base into ProtocolUtils
BaseInjectionHelper.inject(ChildChannelInitializer.INSTANCE);
// Make sure to inject into the server's connection handler
FallbackBungeeInjector.inject();
}
@Override
public void postReload() {
public void disable() {
if (metrics != null) {
// Make sure to properly shutdown bStats metrics
metrics.shutdown();
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 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,32 +0,0 @@
/*
* Copyright (C) 2023 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.bungee.command;
import net.kyori.adventure.platform.bungeecord.BungeeAudiences;
import net.md_5.bungee.api.CommandSender;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.command.InvocationSource;
import xyz.jonesdev.sonar.bungee.SonarBungee;
public final class BungeeInvocationSource extends InvocationSource {
private static final BungeeAudiences AUDIENCES = BungeeAudiences.create(SonarBungee.INSTANCE.getPlugin());
public BungeeInvocationSource(final @NotNull CommandSender sender) {
super(sender.getName(), AUDIENCES.sender(sender));
}
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 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,161 +17,39 @@
package xyz.jonesdev.sonar.bungee.command;
import net.kyori.adventure.text.Component;
import net.md_5.bungee.api.CommandSender;
import net.md_5.bungee.api.chat.TextComponent;
import net.md_5.bungee.api.connection.ProxiedPlayer;
import net.md_5.bungee.api.plugin.Command;
import net.md_5.bungee.api.plugin.TabExecutor;
import net.md_5.bungee.command.ConsoleCommandSender;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.command.CommandInvocation;
import xyz.jonesdev.sonar.api.command.InvocationSource;
import xyz.jonesdev.sonar.api.command.SonarCommand;
import xyz.jonesdev.sonar.api.command.subcommand.Subcommand;
import xyz.jonesdev.sonar.api.command.subcommand.argument.Argument;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import static java.util.Collections.emptyList;
public final class BungeeSonarCommand extends Command implements TabExecutor, SonarCommand {
public BungeeSonarCommand() {
super("sonar");
cacheHelpMessage();
}
@Override
@SuppressWarnings({"redundantSuppression"})
public void execute(final @NotNull CommandSender sender, final String[] args) {
if (!(sender instanceof ConsoleCommandSender)) {
// Check if the player actually has the permission to run the command
if (!sender.hasPermission("sonar.command")) {
sender.sendMessage(new TextComponent(Sonar.get().getConfig().getNoPermission()));
return;
}
// Checking if it contains will only break more since it can throw
// a NullPointerException if the cache is being accessed from parallel threads
DELAY.cleanUp(); // Clean up the cache
final long mapTimestamp = DELAY.asMap().getOrDefault(sender, -1L);
// There were some exploits with spamming commands in the past.
// Spamming should be prevented, especially if some heavy operations are done,
// which is not the case here but let's still stay safe!
if (mapTimestamp > 0L) {
sender.sendMessage(new TextComponent(Sonar.get().getConfig().getCommandCoolDown()));
// Format delay
final long timestamp = System.currentTimeMillis();
final double left = 0.5D - (timestamp - mapTimestamp) / 1000D;
sender.sendMessage(new TextComponent(Sonar.get().getConfig().getCommandCoolDownLeft()
.replace("%time-left%", Sonar.DECIMAL_FORMAT.format(left))));
return;
}
DELAY.put(sender);
}
Optional<Subcommand> subcommand = Optional.empty();
final InvocationSource invocationSource = new BungeeInvocationSource(sender);
if (args.length > 0) {
// Search subcommand if command arguments are present
subcommand = Sonar.get().getSubcommandRegistry().getSubcommands().parallelStream()
.filter(sub -> sub.getInfo().name().equalsIgnoreCase(args[0])
|| Arrays.stream(sub.getInfo().aliases())
.anyMatch(alias -> alias.equalsIgnoreCase(args[0])))
.findFirst();
// Check permissions for subcommands
if (subcommand.isPresent()) {
if (!subcommand.get().getInfo().onlyConsole()
&& !sender.hasPermission(subcommand.get().getPermission())
) {
invocationSource.sendMessage(
Sonar.get().getConfig().getSubCommandNoPerm()
.replace("%permission%", subcommand.get().getPermission())
);
return;
}
}
}
subcommand.ifPresent(sub -> {
if (sub.getInfo().onlyPlayers() && !(sender instanceof ProxiedPlayer)) {
invocationSource.sendMessage(Sonar.get().getConfig().getPlayersOnly());
return;
}
if (sub.getInfo().onlyConsole() && !(sender instanceof ConsoleCommandSender)) {
invocationSource.sendMessage(Sonar.get().getConfig().getConsoleOnly());
return;
}
final CommandInvocation commandInvocation = new CommandInvocation(invocationSource, sub, args);
// The subcommands has arguments which are not present in the executed command
if (sub.getInfo().arguments().length > 0
&& commandInvocation.getRawArguments().length <= 1) {
invocationSource.sendMessage(Sonar.get().getConfig().getIncorrectCommandUsage()
.replace("%usage%", sub.getInfo().name() + " (" + sub.getArguments() + ")"));
return;
}
// Execute the sub command with the custom invocation properties
sub.execute(commandInvocation);
});
if (!subcommand.isPresent()) {
// Re-use the old, cached help message since we don't want to scan
// for each subcommand and it's arguments/attributes every time
// someone runs /sonar since the subcommand don't change
for (final Component component : CACHED_HELP_MESSAGE) {
invocationSource.sendMessage(component);
}
}
// Create our own invocation source wrapper to handle messages properly
final InvocationSource invocationSource = new InvocationSource(
sender instanceof ProxiedPlayer ? ((ProxiedPlayer) sender).getUniqueId() : null,
Sonar.get().sender(sender),
sender::hasPermission);
// Pass the invocation source and command arguments to our command handler
handle(invocationSource, args);
}
@Override
public Iterable<String> onTabComplete(final CommandSender sender, final String @NotNull [] args) {
public Iterable<String> onTabComplete(final @NotNull CommandSender sender, final String @NotNull [] args) {
// Do not allow tab completion if the player does not have the required permission
if (!sender.hasPermission("sonar.command")) {
return emptyList();
}
if (args.length <= 1) {
if (TAB_SUGGESTIONS.isEmpty()) {
for (final Subcommand subcommand : Sonar.get().getSubcommandRegistry().getSubcommands()) {
TAB_SUGGESTIONS.add(subcommand.getInfo().name());
if (subcommand.getInfo().aliases().length > 0) {
TAB_SUGGESTIONS.addAll(Arrays.asList(subcommand.getInfo().aliases()));
}
}
}
return TAB_SUGGESTIONS;
} else if (args.length == 2) {
if (ARG_TAB_SUGGESTIONS.isEmpty()) {
for (final Subcommand subcommand : Sonar.get().getSubcommandRegistry().getSubcommands()) {
final List<String> parsedArguments = Arrays.stream(subcommand.getInfo().arguments())
.map(Argument::value)
.collect(Collectors.toList());
ARG_TAB_SUGGESTIONS.put(subcommand.getInfo().name(), parsedArguments);
for (final String alias : subcommand.getInfo().aliases()) {
ARG_TAB_SUGGESTIONS.put(alias, parsedArguments);
}
}
}
final String subCommandName = args[0].toLowerCase();
return ARG_TAB_SUGGESTIONS.getOrDefault(subCommandName, emptyList());
}
return emptyList();
return getCachedTabSuggestions(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 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,34 +15,47 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.bungee.fallback.injection;
package xyz.jonesdev.sonar.bungee.fallback;
import io.netty.channel.Channel;
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 BaseInjectionHelper {
public void inject(final ChannelInitializer<Channel> childHandler) {
public class FallbackBungeeInjector {
public void inject() {
try {
final Field childField = PipelineUtils.class.getField("SERVER_CHILD");
childField.setAccessible(true);
// Make sure to store the original channel initializer
final ChannelInitializer<Channel> originalInitializer = PipelineUtils.SERVER_CHILD;
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);
final Unsafe unsafe = (Unsafe) unsafeField.get(null);
// Get the base object (the object containing the field)
final Object base = unsafe.staticFieldBase(childField);
// Get the offset of the static field within its class
final long offset = unsafe.staticFieldOffset(childField);
unsafe.putObject(base, offset, childHandler);
// Replace the original channel initializer
unsafe.putObject(base, offset, injectedInitializer);
} catch (Exception exception) {
throw new ReflectiveOperationException(exception);
Sonar.get().getLogger().error("An error occurred while injecting {}", exception);
}
}
}
@@ -1,129 +0,0 @@
/*
* Copyright (C) 2023 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.bungee.fallback;
import com.velocitypowered.natives.compression.VelocityCompressor;
import com.velocitypowered.natives.util.Natives;
import io.netty.channel.ChannelPipeline;
import lombok.RequiredArgsConstructor;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.json.JSONComponentSerializer;
import net.md_5.bungee.BungeeCord;
import net.md_5.bungee.api.chat.TextComponent;
import net.md_5.bungee.api.connection.PendingConnection;
import net.md_5.bungee.api.event.PostLoginEvent;
import net.md_5.bungee.api.plugin.Listener;
import net.md_5.bungee.compress.PacketCompressor;
import net.md_5.bungee.compress.PacketDecompressor;
import net.md_5.bungee.connection.InitialHandler;
import net.md_5.bungee.event.EventHandler;
import net.md_5.bungee.netty.ChannelWrapper;
import net.md_5.bungee.protocol.packet.Kick;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.ReflectiveOperationException;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.fallback.Fallback;
import xyz.jonesdev.sonar.bungee.fallback.compress.FallbackPacketCompressor;
import xyz.jonesdev.sonar.bungee.fallback.compress.FallbackPacketDecompressor;
import xyz.jonesdev.sonar.bungee.fallback.handler.FallbackInitialHandler;
import java.lang.reflect.Field;
import static net.md_5.bungee.netty.PipelineUtils.FRAME_PREPENDER;
@RequiredArgsConstructor
public final class FallbackListener implements Listener {
private final Fallback fallback;
private static final Field CHANNEL_WRAPPER;
static {
try {
CHANNEL_WRAPPER = InitialHandler.class.getDeclaredField("ch");
CHANNEL_WRAPPER.setAccessible(true);
} catch (Throwable throwable) {
throw new ReflectiveOperationException(throwable);
}
}
@EventHandler
@SuppressWarnings("deprecation")
public void handle(final @NotNull PostLoginEvent event) throws Throwable {
if (Sonar.get().getConfig().isLockdownEnabled()) {
if (!event.getPlayer().hasPermission("sonar.lockdown")) {
final PendingConnection pendingConnection = event.getPlayer().getPendingConnection();
// Try to close the channel with a custom serialized disconnect component
if (pendingConnection instanceof FallbackInitialHandler) {
final FallbackInitialHandler fallbackInitialHandler = (FallbackInitialHandler) pendingConnection;
final Component component = Sonar.get().getConfig().getLockdownDisconnect();
final String serialized = JSONComponentSerializer.json().serialize(component);
fallbackInitialHandler.closeWith(new Kick(serialized));
} else {
// Fallback by disconnecting without a message
pendingConnection.disconnect();
Sonar.get().getLogger().warn("Fallback handler of {} is missing", event.getPlayer().getName());
}
if (Sonar.get().getConfig().isLockdownLogAttempts()) {
Sonar.get().getLogger().info(Sonar.get().getConfig().getLockdownConsoleLog()
.replace("%player%", event.getPlayer().getName())
.replace("%ip%", Sonar.get().getConfig()
.formatAddress(event.getPlayer().getAddress().getAddress()))
.replace("%protocol%",
String.valueOf(event.getPlayer().getPendingConnection().getVersion())));
}
return;
} else if (Sonar.get().getConfig().isLockdownEnableNotify()) {
event.getPlayer().sendMessage(new TextComponent(Sonar.get().getConfig().getLockdownNotification()));
}
}
final InitialHandler initialHandler = (InitialHandler) event.getPlayer().getPendingConnection();
final ChannelWrapper channelWrapper = (ChannelWrapper) CHANNEL_WRAPPER.get(initialHandler);
final ChannelPipeline pipeline = channelWrapper.getHandle().pipeline();
// You don't need the frame decoder anymore
if (pipeline.get(FRAME_PREPENDER) != null) {
pipeline.remove(FRAME_PREPENDER);
}
final int compressionThreshold = BungeeCord.getInstance().getConfig().getCompressionThreshold();
// Replace compression handlers
if (compressionThreshold > 0
&& pipeline.get(PacketCompressor.class) != null
&& pipeline.get(PacketDecompressor.class) != null) {
final VelocityCompressor compressor = Natives.compress.get().create(-1);
// Replace (de)compressor with Velocity's to ensure better performance
pipeline.replace(
PacketCompressor.class,
"compress",
// Create a new compressor instance with the Velocity compressor
new FallbackPacketCompressor(compressionThreshold, compressor)
);
pipeline.replace(
PacketDecompressor.class,
"decompress",
// Create a new decompressor instance with the Velocity decompressor
new FallbackPacketDecompressor(compressionThreshold, compressor)
);
}
}
}
@@ -1,60 +0,0 @@
/*
* Copyright (C) 2023 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.bungee.fallback;
import io.netty.channel.Channel;
import io.netty.channel.ChannelPipeline;
import io.netty.util.ReferenceCountUtil;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import net.kyori.adventure.text.Component;
import net.md_5.bungee.netty.ChannelWrapper;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.Fallback;
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.bungee.fallback.handler.FallbackInitialHandler;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.Disconnect;
import java.net.InetAddress;
@Getter
@RequiredArgsConstructor
public final class FallbackUserWrapper implements FallbackUser<ChannelWrapper, FallbackInitialHandler> {
private final Fallback fallback;
private final ChannelWrapper player;
private final FallbackInitialHandler connection;
private final Channel channel;
private final ChannelPipeline pipeline;
private final InetAddress inetAddress;
private final ProtocolVersion protocolVersion;
@Override
public void disconnect(final @NotNull Component reason) {
connection.closeWith(Disconnect.create(reason));
}
@Override
public void write(final @NotNull Object packet) {
if (channel.isActive()) {
channel.writeAndFlush(packet, channel.voidPromise());
} else {
ReferenceCountUtil.release(packet);
}
}
}
@@ -1,115 +0,0 @@
/*
* Copyright (C) 2023 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.bungee.fallback.compress;
import com.velocitypowered.natives.compression.VelocityCompressor;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import lombok.AllArgsConstructor;
import lombok.Getter;
import net.md_5.bungee.compress.PacketCompressor;
import org.jetbrains.annotations.NotNull;
import java.util.zip.DataFormatException;
import static com.velocitypowered.natives.util.MoreByteBufUtils.ensureCompatible;
import static com.velocitypowered.natives.util.MoreByteBufUtils.preferredBuffer;
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.*;
@Getter
@AllArgsConstructor
public final class FallbackPacketCompressor extends PacketCompressor {
private int compressionThreshold;
private final @NotNull VelocityCompressor compressor;
private static final int MAX_COMPRESSED_LENGTH = 1 << 21;
@Override
public void setThreshold(final int compressionThreshold) {
this.compressionThreshold = compressionThreshold;
}
@Override
public void handlerAdded(final ChannelHandlerContext ctx) throws Exception {
// Do nothing
}
@Override
public void handlerRemoved(final ChannelHandlerContext ctx) throws Exception {
compressor.close();
}
@Override
protected void encode(final ChannelHandlerContext ctx,
final @NotNull ByteBuf msg,
final ByteBuf out) throws Exception {
final int uncompressed = msg.readableBytes();
if (uncompressed < compressionThreshold) {
// Under the threshold, do nothing.
writeVarInt(out, uncompressed + 1);
writeVarInt(out, 0);
out.writeBytes(msg);
} else {
handleCompressed(ctx, msg, out);
}
}
private void handleCompressed(final @NotNull ChannelHandlerContext ctx,
final @NotNull ByteBuf msg,
final ByteBuf out) throws DataFormatException {
final int uncompressed = msg.readableBytes();
write21BitVarInt(out, 0); // Fake packet length
writeVarInt(out, uncompressed);
final ByteBuf compatibleIn = ensureCompatible(ctx.alloc(), compressor, msg);
final int startCompressed = out.writerIndex();
try {
compressor.deflate(compatibleIn, out);
} finally {
compatibleIn.release();
}
final int compressedLength = out.writerIndex() - startCompressed;
if (compressedLength >= MAX_COMPRESSED_LENGTH) {
throw new DataFormatException("The server sent a very large (over 2MiB compressed) packet.");
}
int writerIndex = out.writerIndex();
int packetLength = out.readableBytes() - 3;
out.writerIndex(0);
write21BitVarInt(out, packetLength); // Rewrite packet length
out.writerIndex(writerIndex);
}
@Override
protected ByteBuf allocateBuffer(final ChannelHandlerContext ctx,
final @NotNull ByteBuf msg,
final boolean preferDirect) throws Exception {
int uncompressed = msg.readableBytes();
if (uncompressed < compressionThreshold) {
int finalBufferSize = uncompressed + 1;
finalBufferSize += varIntBytes(finalBufferSize);
return ctx.alloc().directBuffer(finalBufferSize);
}
final int initialBufferSize = (uncompressed - 1) + 3 + varIntBytes(uncompressed);
return preferredBuffer(ctx.alloc(), compressor, initialBufferSize);
}
}
@@ -1,98 +0,0 @@
/*
* Copyright (C) 2023 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.bungee.fallback.compress;
import com.velocitypowered.natives.compression.VelocityCompressor;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.CorruptedFrameException;
import lombok.Getter;
import net.md_5.bungee.compress.PacketDecompressor;
import org.jetbrains.annotations.NotNull;
import java.util.List;
import static com.velocitypowered.natives.util.MoreByteBufUtils.ensureCompatible;
import static com.velocitypowered.natives.util.MoreByteBufUtils.preferredBuffer;
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.readVarInt;
@Getter
public final class FallbackPacketDecompressor extends PacketDecompressor {
private final int compressionThreshold;
private final @NotNull VelocityCompressor compressor;
private static final int VANILLA_MAXIMUM_UNCOMPRESSED_SIZE = 8 * 1024 * 1024; // 8MiB
private static final int HARD_MAXIMUM_UNCOMPRESSED_SIZE = 16 * 1024 * 1024; // 16MiB
private static final int UNCOMPRESSED_CAP = Boolean.getBoolean("sonar.increased-compression-cap")
? HARD_MAXIMUM_UNCOMPRESSED_SIZE : VANILLA_MAXIMUM_UNCOMPRESSED_SIZE;
public FallbackPacketDecompressor(final int compressionThreshold,
final @NotNull VelocityCompressor compressor) {
super(); // super(compressionThreshold);
this.compressionThreshold = compressionThreshold;
this.compressor = compressor;
}
@Override
public void handlerAdded(final ChannelHandlerContext ctx) throws Exception {
// Do nothing
}
@Override
public void handlerRemoved(final ChannelHandlerContext ctx) throws Exception {
compressor.close();
}
@Override
protected void decode(final ChannelHandlerContext ctx,
final @NotNull ByteBuf in,
final List<Object> out) throws Exception {
final int claimedUncompressedSize = readVarInt(in);
if (claimedUncompressedSize == 0) {
// This message is not compressed.
out.add(in.retain());
// Check for invalid packet size after the packet is retained
final int size = in.readableBytes();
if (size > compressionThreshold + 2) {
throw new CorruptedFrameException("Invalid uncompressed packet size " + size);
}
return;
}
if (claimedUncompressedSize < compressionThreshold) {
throw new CorruptedFrameException(claimedUncompressedSize + " is less than " + compressionThreshold);
}
if (claimedUncompressedSize > UNCOMPRESSED_CAP) {
throw new CorruptedFrameException(claimedUncompressedSize + " exceeds maximum size");
}
final ByteBuf compatibleIn = ensureCompatible(ctx.alloc(), compressor, in);
final ByteBuf uncompressed = preferredBuffer(ctx.alloc(), compressor, claimedUncompressedSize);
try {
compressor.inflate(compatibleIn, uncompressed, claimedUncompressedSize);
out.add(uncompressed);
} catch (Exception exception) {
uncompressed.release();
throw exception;
} finally {
compatibleIn.release();
}
}
}
@@ -1,91 +0,0 @@
/*
* Copyright (C) 2023 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.bungee.fallback.handler;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFutureListener;
import net.md_5.bungee.BungeeCord;
import net.md_5.bungee.api.config.ListenerInfo;
import net.md_5.bungee.connection.InitialHandler;
import net.md_5.bungee.netty.ChannelWrapper;
import net.md_5.bungee.protocol.packet.LoginRequest;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.fallback.Fallback;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.bungee.fallback.FallbackUserWrapper;
import java.net.InetAddress;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
public final class FallbackInitialHandler extends InitialHandler {
public FallbackInitialHandler(final BungeeCord bungee, final ListenerInfo listener) {
super(bungee, listener);
}
private static final @NotNull Fallback FALLBACK = Objects.requireNonNull(Sonar.get().getFallback());
private ChannelWrapper channelWrapper;
@SuppressWarnings("unused") // TODO: remove later
private @Nullable FallbackUserWrapper player;
@Override
public void connected(final ChannelWrapper channelWrapper) throws Exception {
this.channelWrapper = channelWrapper;
super.connected(channelWrapper);
}
@Override
public void handle(final LoginRequest loginRequest) throws Exception {
final InetAddress inetAddress = getAddress().getAddress();
if (player == null && !Sonar.get().getVerifiedPlayerController().has(inetAddress)) {
final Channel channel = channelWrapper.getHandle();
player = new FallbackUserWrapper(
FALLBACK, channelWrapper, this,
channel, channel.pipeline(), inetAddress,
ProtocolVersion.fromId(getHandshake().getProtocolVersion())
);
// TODO: implement Fallback
}
super.handle(loginRequest);
}
// Mostly taken from Velocity
public void closeWith(final Object msg) {
if (player != null && player.getChannel().isActive()) {
boolean is17 = player.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_8) < 0
&& player.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_7_2) >= 0;
if (is17) {
player.getChannel().eventLoop().execute(() -> {
channelWrapper.getHandle().config().setAutoRead(false);
player.getChannel().eventLoop().schedule(() -> {
channelWrapper.markClosed();
player.getChannel().writeAndFlush(msg).addListener(ChannelFutureListener.CLOSE);
}, 250L, TimeUnit.MILLISECONDS);
});
} else {
channelWrapper.markClosed();
player.getChannel().writeAndFlush(msg).addListener(ChannelFutureListener.CLOSE);
}
}
}
}
@@ -1,82 +0,0 @@
/*
* Copyright (C) 2023 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.bungee.fallback.injection;
import io.netty.buffer.PooledByteBufAllocator;
import io.netty.channel.*;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import net.md_5.bungee.BungeeCord;
import net.md_5.bungee.netty.HandlerBoss;
import net.md_5.bungee.netty.PipelineUtils;
import net.md_5.bungee.protocol.Varint21LengthFieldPrepender;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.ReflectiveOperationException;
import xyz.jonesdev.sonar.bungee.fallback.varint.Varint21FrameDecoder;
import xyz.jonesdev.sonar.common.fallback.FallbackTimeoutHandler;
import java.lang.reflect.Field;
import java.util.concurrent.TimeUnit;
import static net.md_5.bungee.netty.PipelineUtils.*;
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class BaseChannelInitializer extends ChannelInitializer<Channel> {
public static final BaseChannelInitializer INSTANCE = new BaseChannelInitializer();
private static final boolean REPLACE_VAR_INT_DECODER = !Boolean.getBoolean("sonar.do-not-replace-decoder");
private static final WriteBufferWaterMark MARK;
private static final Varint21LengthFieldPrepender FRAME_ENCODER;
static {
try {
final Field markField = PipelineUtils.class.getDeclaredField("MARK");
markField.setAccessible(true);
MARK = (WriteBufferWaterMark) markField.get(null);
final Field frameEncoder = PipelineUtils.class.getDeclaredField("framePrepender");
frameEncoder.setAccessible(true);
FRAME_ENCODER = (Varint21LengthFieldPrepender) frameEncoder.get(null);
} catch (Throwable throwable) {
throw new ReflectiveOperationException(throwable);
}
}
// Mostly taken from BungeeCord
// https://github.com/SpigotMC/BungeeCord/blob/master/proxy/src/main/java/net/md_5/bungee/netty/PipelineUtils.java#L162
@Override
protected void initChannel(final @NotNull Channel channel) throws Exception {
try {
channel.config().setOption(ChannelOption.IP_TOS, 0x18);
} catch (ChannelException ignored) {
}
channel.config().setOption(ChannelOption.TCP_NODELAY, true);
channel.config().setAllocator(PooledByteBufAllocator.DEFAULT);
channel.config().setWriteBufferWaterMark(MARK);
channel.pipeline().addLast(FRAME_DECODER, REPLACE_VAR_INT_DECODER
? new Varint21FrameDecoder() : new net.md_5.bungee.protocol.Varint21FrameDecoder());
channel.pipeline().addLast(TIMEOUT_HANDLER, new FallbackTimeoutHandler(
BungeeCord.getInstance().config.getTimeout(), TimeUnit.MILLISECONDS
));
channel.pipeline().addLast(FRAME_PREPENDER, FRAME_ENCODER);
channel.pipeline().addLast(BOSS_HANDLER, new HandlerBoss());
}
}
@@ -1,97 +0,0 @@
/*
* Copyright (C) 2023 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.bungee.fallback.injection;
import io.netty.channel.Channel;
import io.netty.channel.ChannelInitializer;
import io.netty.handler.codec.haproxy.HAProxyMessageDecoder;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import net.md_5.bungee.BungeeCord;
import net.md_5.bungee.api.ProxyServer;
import net.md_5.bungee.api.config.ListenerInfo;
import net.md_5.bungee.api.event.ClientConnectEvent;
import net.md_5.bungee.netty.HandlerBoss;
import net.md_5.bungee.netty.PipelineUtils;
import net.md_5.bungee.protocol.*;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.ReflectiveOperationException;
import xyz.jonesdev.sonar.bungee.fallback.handler.FallbackInitialHandler;
import java.lang.reflect.Field;
import java.net.SocketAddress;
import static net.md_5.bungee.netty.PipelineUtils.*;
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class ChildChannelInitializer extends ChannelInitializer<Channel> {
public static final ChildChannelInitializer INSTANCE = new ChildChannelInitializer();
private static final BaseChannelInitializer BASE = BaseChannelInitializer.INSTANCE;
private static final KickStringWriter LEGACY_KICK;
static {
try {
final Field kickField = PipelineUtils.class.getDeclaredField("legacyKicker");
kickField.setAccessible(true);
LEGACY_KICK = (KickStringWriter) kickField.get(null);
} catch (Throwable throwable) {
throw new ReflectiveOperationException(throwable);
}
}
// Mostly taken from BungeeCord
// https://github.com/SpigotMC/BungeeCord/blob/master/proxy/src/main/java/net/md_5/bungee/netty/PipelineUtils.java
@Override
protected void initChannel(final @NotNull Channel channel) throws Exception {
final SocketAddress remoteAddress = channel.remoteAddress() == null ? channel.parent().localAddress()
: channel.remoteAddress();
if (BungeeCord.getInstance().getConnectionThrottle() != null && BungeeCord.getInstance().getConnectionThrottle().throttle(remoteAddress)) {
channel.close();
} else {
final ListenerInfo listener = channel.attr(PipelineUtils.LISTENER).get();
if (BungeeCord.getInstance().getPluginManager().callEvent(new ClientConnectEvent(remoteAddress, listener)).isCancelled()) {
channel.close();
} else {
try {
BASE.initChannel(channel);
} catch (Exception exception) {
exception.printStackTrace(System.err);
channel.close();
return;
}
channel.pipeline().addBefore(FRAME_DECODER, LEGACY_DECODER, new LegacyDecoder());
channel.pipeline().addAfter(FRAME_DECODER, PACKET_DECODER, new MinecraftDecoder(Protocol.HANDSHAKE, true,
ProxyServer.getInstance().getProtocolVersion()));
channel.pipeline().addAfter(FRAME_PREPENDER, PACKET_ENCODER, new MinecraftEncoder(Protocol.HANDSHAKE,
true, ProxyServer.getInstance().getProtocolVersion()));
channel.pipeline().addBefore(FRAME_PREPENDER, LEGACY_KICKER, LEGACY_KICK);
channel.pipeline().get(HandlerBoss.class).setHandler(new FallbackInitialHandler(BungeeCord.getInstance(),
listener));
if (listener.isProxyProtocol()) {
channel.pipeline().addFirst(new HAProxyMessageDecoder());
}
}
}
}
}
@@ -1,91 +0,0 @@
/*
* Copyright (C) 2023 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.bungee.fallback.varint;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
import io.netty.handler.codec.CorruptedFrameException;
import java.util.List;
// Mostly 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 Varint21FrameDecoder extends ByteToMessageDecoder {
@Override
protected void decode(final ChannelHandlerContext ctx,
final ByteBuf byteBuf,
final List<Object> out) throws Exception {
if (!ctx.channel().isActive() || !byteBuf.isReadable()) {
// MessageToMessageDecoder always do release after decoding
// https://github.com/jonesdevelopment/sonar-legacy/pull/5
return;
}
final VarintByteDecoder reader = new VarintByteDecoder();
final int end = byteBuf.forEachByte(reader);
if (end == -1) {
// This is probably a good sign that the buffer was too short or empty
// since the ByteBuf cannot hold a proper VarInt.
if (reader.getResult() == VarintByteDecoder.DecoderResult.RUN_OF_ZEROES) {
byteBuf.clear();
}
return;
}
switch (reader.getResult()) {
case RUN_OF_ZEROES: {
// this will return to the point where the next varInt starts
byteBuf.readerIndex(end);
break;
}
case SUCCESS: {
final int readVarInt = reader.getReadVarInt();
if (readVarInt < 0) {
byteBuf.clear();
throw new CorruptedFrameException("Result: " + reader);
} else if (readVarInt == 0) {
// Actually, we don't want to throw an Exception if the packet is empty.
// The check would also false flag a lot of legit players since packets
// in 1.7 could sometimes be empty.
byteBuf.readerIndex(end + 1);
return;
}
final int bytesRead = reader.getBytesRead();
final int minimumRead = bytesRead + readVarInt;
if (byteBuf.isReadable(minimumRead)) {
out.add(byteBuf.retainedSlice(end + 1, readVarInt));
byteBuf.skipBytes(minimumRead);
}
break;
}
default: {
byteBuf.clear();
throw new CorruptedFrameException("Result: " + reader);
}
}
}
}
@@ -1,43 +0,0 @@
/*
* Copyright (C) 2023 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.bungee.verbose;
import lombok.RequiredArgsConstructor;
import net.md_5.bungee.api.ChatMessageType;
import net.md_5.bungee.api.ProxyServer;
import net.md_5.bungee.api.chat.TextComponent;
import net.md_5.bungee.api.connection.ProxiedPlayer;
import xyz.jonesdev.sonar.api.verbose.Verbose;
@RequiredArgsConstructor
public final class VerboseWrapper extends Verbose {
private final ProxyServer server;
public void broadcast(final String message) {
final TextComponent component = new TextComponent(message);
synchronized (subscribers) {
for (final String subscriber : subscribers) {
final ProxiedPlayer player = server.getPlayer(subscriber);
if (player != null) {
player.sendMessage(ChatMessageType.ACTION_BAR, component);
}
}
}
}
}
+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;
}
}
}
Loaded 100 of 260 files, more files were not shown because too many files have changed in this diff. Show more