Compare commits

..
55 Commits
Author SHA1 Message Date
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
111 changed files with 3500 additions and 2176 deletions

No files matched your search

+4 -4
View File
@@ -9,6 +9,7 @@
<!-- 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)
@@ -42,8 +43,8 @@
- [Fallback](https://github.com/jonesdevelopment/sonar#fallback) is Sonar's main component designed to prevent
all types of bots.
- Spigot & BungeeCord
- The compression method is changed
to [Velocity's libdeflate](https://github.com/PaperMC/Velocity/tree/dev/3.0.0/native).
- Duplicate login/status packet spam fix
- Run server ping handling asynchronously
- The Varint decoder is updated to Velocity's
improved [MinecraftVarintFrameDecoder](https://github.com/PaperMC/Velocity/blob/dev/3.0.0/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftVarintFrameDecoder.java).
@@ -90,5 +91,4 @@ Sonar is licensed under the [GNU General Public License 3.0](https://www.gnu.org
## 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).
- The Varint decoding was taken from [Velocity](https://github.com/PaperMC/Velocity).
+16 -7
View File
@@ -22,12 +22,9 @@ allprojects {
repositories {
mavenCentral() // Lombok
maven(url = "https://jitpack.io") // simple-yaml
maven(url = "https://repo.papermc.io/repository/maven-public") // Velocity natives
maven(url = "https://repo.jonesdev.xyz/releases/") // Bungee & Velocity proxy module
}
}
subprojects {
apply(plugin = "java")
apply(plugin = "com.github.johnrengelman.shadow")
@@ -49,7 +46,7 @@ subprojects {
implementation("com.j256.ormlite:ormlite-jdbc:6.1") // ORM
implementation("xyz.jonesdev:cappuccino:0.1.6-SNAPSHOT") // expiring cache
compileOnly("io.netty:netty-all:4.1.100.Final") // netty
compileOnly("io.netty:netty-all:4.1.101.Final") // netty
}
}
@@ -72,7 +69,10 @@ tasks {
shadowJar {
// Set the file name of the shadowed jar
archiveFileName.set("${rootProject.name}.jar")
archiveFileName.set("Sonar.jar")
// Remove file timestamps
isPreserveFileTimestamps = false
// bStats has to be relocated to the Sonar package otherwise it throws an exception
// https://github.com/Bastian/bstats-metrics/blob/master/base/src/main/java/org/bstats/MetricsBase.java#L251
@@ -80,10 +80,19 @@ tasks {
// https://github.com/jonesdevelopment/sonar/issues/46
// Relocate some packages, so we don't run into issues where we accidentally use Velocity's classes
relocate("net.kyori.adventure.nbt", "xyz.jonesdev.sonar.libs.nbt")
relocate("org.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.velocitypowered.natives", "xyz.jonesdev.sonar.libs.natives")
relocate("xyz.jonesdev.cappuccino", "xyz.jonesdev.sonar.libs.cappuccino")
relocate("net.kyori.examination", "xyz.jonesdev.sonar.libs.examination")
// We have to be careful here, so we don't accidentally break adventure
relocate("net.kyori.adventure", "xyz.jonesdev.sonar.libs.adventure") {
exclude("net.kyori.adventure.text.**")
exclude("net.kyori.adventure.audience.**")
}
// Exclude unnecessary metadata information
exclude("META-INF/versions/**")
}
compileJava {
+2 -1
View File
@@ -1,2 +1,3 @@
group=xyz.jonesdev.sonar
version=2.0.4
version=2.0.10
description=Effective Anti-bot plugin for Velocity, BungeeCord, and Bukkit (1.7-latest)
+1 -1
View File
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
+1 -1
View File
@@ -1,4 +1,4 @@
rootProject.name = "Sonar"
rootProject.name = "sonar"
sequenceOf("api", "bukkit", "bungee", "common", "velocity").forEach {
val path = "sonar-$it"
@@ -17,14 +17,16 @@
package xyz.jonesdev.sonar.api;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import org.jetbrains.annotations.NotNull;
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.verbose.Verbose;
import java.io.File;
@@ -34,10 +36,14 @@ public interface Sonar {
DecimalFormat DECIMAL_FORMAT = new DecimalFormat("#,###.##");
String LINE_SEPARATOR = "\n"; // Using System.lineSeparator is broken, for some reason...
Gson GENERAL_GSON = new GsonBuilder()
.disableInnerClassSerialization()
.create();
/**
* @return A small wrapper for the server
* @return The platform the plugin is being run on
*/
@NotNull ServerWrapper getServer();
@NotNull SonarPlatform getPlatform();
/**
* @return A small wrapper for the plugin logger so we can use the logger everywhere
@@ -76,6 +82,12 @@ public interface Sonar {
return SonarEventManager.INSTANCE;
}
@NotNull
default AttackTracker getAttackTracker() {
// We don't want anyone else to create a new instance.
return AttackTracker.INSTANCE;
}
@NotNull
default Fallback getFallback() {
// We don't want anyone else to create a new instance.
@@ -0,0 +1,148 @@
/*
* 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.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.profiler.JVMProfiler;
import xyz.jonesdev.sonar.api.statistics.Statistics;
import xyz.jonesdev.sonar.api.timer.SystemTimer;
import java.util.Optional;
@Getter
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class AttackTracker implements JVMProfiler {
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 int peakJoinsPerSecond;
private double peakProcessCPUUsage;
private long peakProcessMemoryUsage;
// Calculate during-attack-statistics using their deltas
private int successfulVerifications, failedVerifications;
}
public void checkIfUnderAttack() {
final int joinsPerSecond = Sonar.get().getVerboseHandler().getLoginsPerSecond().estimatedSize();
final int verifyingPlayers = Sonar.get().getFallback().getConnected().size();
final int queuedPlayers = Sonar.get().getFallback().getQueue().getQueuedPlayers().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 = Statistics.FAILED_VERIFICATIONS.get();
Sonar.get().getEventManager().publish(new AttackDetectedEvent());
} 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 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()) {
// 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 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().getBlacklisted().estimatedSize();
// Calculate during-attack-statistics using their deltas
final long totalVerified = Sonar.get().getVerifiedPlayerController().estimatedSize();
final long verified = Math.max(totalVerified - currentAttack.successfulVerifications, 0);
final long totalFailed = Statistics.FAILED_VERIFICATIONS.get();
final long failed = Math.max(totalFailed - currentAttack.failedVerifications, 0);
// Post webhook to Discord
Optional.ofNullable(Sonar.get().getConfig().getDiscordWebhook()).ifPresent(webhook -> webhook.post(() -> {
final SonarConfiguration.Webhook.Embed config = Sonar.get().getConfig().getWebhook().getEmbed();
final String description = config.getDescription()
.replace("%start-timestamp%", startTimestamp)
.replace("%end-timestamp%", endTimestamp)
.replace("%duration%", formattedDuration)
.replace("%peak-cpu%", peakCPU)
.replace("%peak-memory%", peakMem)
.replace("%peak-bps%", peakBPS)
.replace("%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;
}
}
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
public boolean isCurrentlyUnderAttack() {
final int minDelay = Sonar.get().getConfig().getAttackCooldownDelay();
return currentAttack != null && currentAttack.timer.delay() < minDelay;
}
}
@@ -19,12 +19,14 @@ package xyz.jonesdev.sonar.api.command;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
import xyz.jonesdev.sonar.api.command.subcommand.Subcommand;
@Getter
@RequiredArgsConstructor
@ToString(of = "rawArguments")
public final class CommandInvocation {
private final InvocationSource sender;
private final Subcommand command;
private final Subcommand subcommand;
private final String[] rawArguments;
}
@@ -23,11 +23,15 @@ import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage;
import java.util.function.Predicate;
@Getter
@RequiredArgsConstructor
public abstract class InvocationSource {
private final String name;
private final Audience audience;
private final boolean player;
private final Predicate<String> permissionFunction;
/**
* Sends an empty chat message to the command executor
@@ -18,14 +18,18 @@
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 net.kyori.adventure.text.minimessage.MiniMessage;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.cappuccino.Cappuccino;
import xyz.jonesdev.cappuccino.ExpiringCache;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.command.subcommand.Subcommand;
import xyz.jonesdev.sonar.api.command.subcommand.argument.Argument;
import java.util.*;
import java.util.stream.Collectors;
import static java.util.Collections.emptyList;
public interface SonarCommand {
List<String> TAB_SUGGESTIONS = new ArrayList<>();
@@ -34,63 +38,114 @@ public interface SonarCommand {
ExpiringCache<Object> DELAY = Cappuccino.buildExpiring(500L);
int COPYRIGHT_YEAR = Calendar.getInstance().get(Calendar.YEAR);
List<Component> CACHED_HELP_MESSAGE = new ArrayList<>();
List<Component> CACHED_HELP_MESSAGE = new Vector<>();
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(ClickEvent.clickEvent(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(HoverEvent.hoverEvent(HoverEvent.Action.SHOW_TEXT, Component.text("(Click to open Discord)")))
.clickEvent(ClickEvent.clickEvent(ClickEvent.Action.OPEN_URL, "https://jonesdev.xyz/discord/")),
Component.text("or open a new issue on GitHub.", NamedTextColor.YELLOW)
.hoverEvent(HoverEvent.hoverEvent(HoverEvent.Action.SHOW_TEXT, Component.text("(Click to open GitHub)")))
.clickEvent(ClickEvent.clickEvent(ClickEvent.Action.OPEN_URL, "https://github.com/jonesdevelopment/sonar" +
"/issues"))
),
Component.empty()
));
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)
);
Component hoverComponent = Component.textOfChildren(
Component.text("Only players: ", NamedTextColor.GRAY),
Component.text(sub.getInfo().onlyPlayers() ? "✔" : "✗",
sub.getInfo().onlyPlayers() ? NamedTextColor.GREEN : NamedTextColor.RED),
Component.newline(),
Component.text("Require console: ", NamedTextColor.GRAY),
Component.text(sub.getInfo().onlyConsole() ? "✔" : "✗",
sub.getInfo().onlyConsole() ? NamedTextColor.GREEN : NamedTextColor.RED),
Component.newline(),
Component.text("Permission: ", NamedTextColor.GRAY),
Component.text(sub.getPermission(), NamedTextColor.WHITE)
);
if (sub.getInfo().aliases().length > 0) {
hoverComponent = hoverComponent
.append(Component.newline())
.append(Component.text("Aliases: ", NamedTextColor.GRAY))
.append(Component.text(sub.getAliases(), NamedTextColor.WHITE));
default void handle(final @NotNull InvocationSource source, final String[] args) {
if (source.isPlayer()) {
// Check if the player actually has the permission to run the command
if (!source.getPermissionFunction().test("sonar.command")) {
source.sendMessage(Sonar.get().getConfig().getNoPermission());
return;
}
component = component
.clickEvent(ClickEvent.clickEvent(ClickEvent.Action.SUGGEST_COMMAND,
"/sonar " + sub.getInfo().name() + " "))
.hoverEvent(HoverEvent.hoverEvent(HoverEvent.Action.SHOW_TEXT, hoverComponent));
CACHED_HELP_MESSAGE.add(component);
// 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(source, -1L);
// There were some exploits with spamming commands in the past.
// Spamming should be prevented, especially if some heavy operations are done,
// which is not the case here but let's still stay safe!
if (mapTimestamp > 0L) {
source.sendMessage(Sonar.get().getConfig().getCommands().getCommandCoolDown());
// Format delay
final long timestamp = System.currentTimeMillis();
final double left = 0.5D - (timestamp - mapTimestamp) / 1000D;
source.sendMessage(Sonar.get().getConfig().getCommands().getCommandCoolDownLeft()
.replace("%time-left%", Sonar.DECIMAL_FORMAT.format(left)));
return;
}
DELAY.put(source);
}
if (args.length > 0) {
// Search subcommand if command arguments are present
final Optional<Subcommand> 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();
// Check permissions for subcommands
if (subcommand.isPresent()) {
if (!subcommand.get().getInfo().onlyConsole()
&& !source.getPermissionFunction().test(subcommand.get().getPermission())) {
source.sendMessage(Sonar.get().getConfig().getCommands().getSubCommandNoPerm()
.replace("%permission%", subcommand.get().getPermission()));
return;
}
subcommand.get().invoke(source, args);
return;
}
}
// Re-use the old, cached help message since we don't want to scan
// for each subcommand and it's arguments/attributes every time
// someone runs /sonar since the subcommand don't change
for (final Component component : CACHED_HELP_MESSAGE) {
source.sendMessage(component);
}
}
static void prepareCachedMessages() {
// Cache help message
CACHED_HELP_MESSAGE.clear();
for (final String message : Sonar.get().getConfig().getCommands().getHelpHeader()) {
CACHED_HELP_MESSAGE.add(MiniMessage.miniMessage().deserialize(message
.replace("%version%", Sonar.get().getVersion().getFormatted())
.replace("%platform%", Sonar.get().getPlatform().getDisplayName())
.replace("%copyright_year%", String.valueOf(Calendar.getInstance().get(Calendar.YEAR)))));
}
final String subcommandFormat = Sonar.get().getConfig().getCommands().getHelpSubcommands();
Sonar.get().getSubcommandRegistry().getSubcommands().forEach(subcommand -> {
final Component deserialized = MiniMessage.miniMessage().deserialize(subcommandFormat
.replace("%subcommand%", subcommand.getInfo().name())
.replace("%description%", subcommand.getInfo().description())
.replace("%only_players%", subcommand.getInfo().onlyPlayers() ? "<green>✔</green>" : "<red>✗</red>")
.replace("%require_console%", subcommand.getInfo().onlyConsole() ? "<green>✔</green>" : "<red>✗</red>")
.replace("%permission%", subcommand.getPermission())
.replace("%aliases%", subcommand.getAliases()));
CACHED_HELP_MESSAGE.add(deserialized);
});
// Don't re-cache tab suggestions
if (!TAB_SUGGESTIONS.isEmpty()) return;
// Cache tab suggestions
for (final Subcommand subcommand : Sonar.get().getSubcommandRegistry().getSubcommands()) {
TAB_SUGGESTIONS.add(subcommand.getInfo().name());
if (subcommand.getInfo().aliases().length > 0) {
TAB_SUGGESTIONS.addAll(Arrays.asList(subcommand.getInfo().aliases()));
}
final List<String> parsedArguments = Arrays.stream(subcommand.getInfo().arguments())
.map(Argument::value)
.collect(Collectors.toList());
ARG_TAB_SUGGESTIONS.put(subcommand.getInfo().name(), parsedArguments);
for (final String alias : subcommand.getInfo().aliases()) {
ARG_TAB_SUGGESTIONS.put(alias, parsedArguments);
}
}
}
default List<String> getCachedTabSuggestions(final String @NotNull [] arguments) {
if (arguments.length <= 1) {
return TAB_SUGGESTIONS;
} else if (arguments.length == 2) {
final String subCommandName = arguments[0].toLowerCase();
return ARG_TAB_SUGGESTIONS.getOrDefault(subCommandName, emptyList());
}
return emptyList();
}
}
@@ -28,17 +28,14 @@ import xyz.jonesdev.sonar.api.command.subcommand.argument.Argument;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Arrays;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
@Getter
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);
@@ -52,12 +49,7 @@ public abstract class Subcommand {
.collect(Collectors.joining(", "));
}
protected static @Nullable InetAddress checkIP(final InvocationSource source, final String rawIP) {
if (!IP_PATTERN.matcher(rawIP).matches()) {
source.sendMessage(SONAR.getConfig().getCommands().getIncorrectIpAddress());
return null;
}
protected static @Nullable InetAddress getInetAddressIfValid(final InvocationSource source, final String rawIP) {
final InetAddress inetAddress;
try {
inetAddress = InetAddress.getByName(rawIP);
@@ -67,7 +59,7 @@ public abstract class Subcommand {
return null;
}
} catch (UnknownHostException exception) {
source.sendMessage(SONAR.getConfig().getCommands().getUnknownIpAddress());
source.sendMessage(SONAR.getConfig().getCommands().getIncorrectIpAddress());
return null;
}
return inetAddress;
@@ -78,5 +70,31 @@ public abstract class Subcommand {
.replace("%usage%", info.name() + " (" + arguments + ")"));
}
public abstract void execute(final @NotNull CommandInvocation invocation);
public final void invoke(final @NotNull InvocationSource invocationSource, final String @NotNull [] arguments) {
// Check if the subcommand can only be executed by players
if (getInfo().onlyPlayers() && !invocationSource.isPlayer()) {
invocationSource.sendMessage(Sonar.get().getConfig().getCommands().getPlayersOnly());
return;
}
// Check if the subcommand can only be executed though console
if (getInfo().onlyConsole() && invocationSource.isPlayer()) {
invocationSource.sendMessage(Sonar.get().getConfig().getCommands().getConsoleOnly());
return;
}
final CommandInvocation commandInvocation = new CommandInvocation(invocationSource, this, arguments);
// The subcommands has arguments which are not present in the executed command
if (getInfo().argumentsRequired() && getInfo().arguments().length > 0 && arguments.length <= 1) {
invocationSource.sendMessage(Sonar.get().getConfig().getCommands().getIncorrectCommandUsage()
.replace("%usage%", getInfo().name() + " (" + getArguments() + ")"));
return;
}
// Execute the sub command
execute(commandInvocation);
}
protected abstract void execute(final @NotNull CommandInvocation invocation);
}
@@ -18,6 +18,7 @@
package xyz.jonesdev.sonar.api.config;
import lombok.Getter;
import org.jetbrains.annotations.NotNull;
import org.simpleyaml.configuration.comments.format.YamlCommentFormat;
import org.simpleyaml.configuration.file.YamlFile;
import xyz.jonesdev.sonar.api.Sonar;
@@ -36,8 +37,8 @@ public final class SimpleYamlConfig {
private static final List<String> HEADER = Arrays.asList(
"",
"Running Sonar version " + Sonar.get().getVersion()
+ " on " + Sonar.get().getServer().getPlatform().getDisplayName(),
String.format("Running Sonar version %s on %s",
Sonar.get().getVersion(), Sonar.get().getPlatform().getDisplayName()),
"Need help or have questions? https://jonesdev.xyz/discord",
"https://github.com/jonesdevelopment/sonar",
""
@@ -47,7 +48,7 @@ public final class SimpleYamlConfig {
this(new File(dataFolder, fileName + ".yml"), dataFolder);
}
private SimpleYamlConfig(final File file, final File folder) {
private SimpleYamlConfig(final File file, final @NotNull File folder) {
if (!folder.exists() && !folder.mkdir()) {
throw new IllegalStateException("Could not create folder?!");
}
@@ -93,12 +94,27 @@ public final class SimpleYamlConfig {
}
public String getString(final String path, final String def) {
final Object object = getObject(path, def);
if (object instanceof String) {
return (String) object;
}
Sonar.get().getLogger().info("[config] Migrated {} to {}", path, def);
set(path, def);
return def;
}
public Object getObject(final String path, final Object def) {
yaml.addDefault(path, def);
return yaml.getString(path, def);
return yaml.get(path, def);
}
public List<String> getStringList(final String path, final List<String> def) {
yaml.addDefault(path, def);
return yaml.getStringList(path);
}
public List<Integer> getIntList(final String path, final List<Integer> def) {
yaml.addDefault(path, def);
return yaml.getIntegerList(path);
}
}
@@ -21,15 +21,15 @@ import lombok.*;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.command.SonarCommand;
import xyz.jonesdev.sonar.api.dependencies.Dependency;
import xyz.jonesdev.sonar.api.webhook.DiscordWebhook;
import java.io.File;
import java.net.InetAddress;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.*;
import java.util.regex.Pattern;
import static xyz.jonesdev.sonar.api.Sonar.LINE_SEPARATOR;
@@ -48,9 +48,9 @@ public final class SonarConfiguration {
@Getter
private final Verification verification = new Verification();
@Getter
private final Lockdown lockdown = new Lockdown();
@Getter
private final Database database = new Database();
@Getter
private final Webhook webhook = new Webhook();
public SonarConfiguration(final @NotNull File pluginFolder) {
this.pluginFolder = pluginFolder;
@@ -68,12 +68,19 @@ public final class SonarConfiguration {
@Getter
private int minPlayersForAttack;
@Getter
private int minAttackDuration;
@Getter
private int minAttackThreshold;
@Getter
private int attackCooldownDelay;
@Getter
private Component tooManyOnlinePerIp;
@Getter
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public static final class Verbose {
private String actionBarLayout;
private String actionBarLayoutDuringAttack;
private List<String> animation;
}
@@ -86,9 +93,52 @@ public final class SonarConfiguration {
@Getter
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public static final class Verification {
private boolean enabled;
private boolean checkGravity;
private boolean checkCollisions;
private Timing timing;
@Getter
public enum Timing {
ALWAYS, DURING_ATTACK, NEVER
}
private final Map map = new Map();
private final Gravity gravity = new Gravity();
@Getter
public static final class Map {
private boolean enabled;
private boolean drawBackgroundNoise;
private boolean randomizePositions;
private boolean randomizeFontSize;
private int precomputeAmount;
private int maxDuration;
private int maxTries;
private String dictionary;
private Component enterCode;
private Component failedCaptcha;
private String enterCodeActionBar;
}
@Getter
public static final class Gravity {
private boolean enabled;
private Gamemode gamemode;
private int maxMovementTicks;
private int maxIgnoredTicks;
private Component youAreBeingChecked;
@Getter
@RequiredArgsConstructor
public enum Gamemode {
SURVIVAL(0),
CREATIVE(1),
ADVENTURE(2),
// Keep this for backwards compatibility
SPECTATOR(2);
private final int id;
}
}
private boolean logConnections;
private boolean logDuringAttack;
private boolean debugXYZPositions;
@@ -99,15 +149,14 @@ public final class SonarConfiguration {
private String failedLog;
private String successLog;
private String blacklistLog;
private short gamemodeId;
private int maxBrandLength;
private int maxMovementTicks;
private int maxIgnoredTicks;
private int maxVerifyingPlayers;
private int maxLoginPackets;
private int maxPing;
private int readTimeout;
private int reconnectDelay;
private final Collection<Integer> whitelistedProtocols = new HashSet<>(0);
private final Collection<Integer> blacklistedProtocols = new HashSet<>(0);
private Component tooManyPlayers;
private Component tooFastReconnect;
@@ -119,6 +168,7 @@ public final class SonarConfiguration {
private Component alreadyVerifying;
private Component alreadyQueued;
private Component blacklisted;
private Component protocolBlacklisted;
}
@Getter
@@ -127,7 +177,6 @@ public final class SonarConfiguration {
private String incorrectCommandUsage;
private String incorrectIpAddress;
private String illegalIpAddress;
private String unknownIpAddress;
private String subCommandNoPerm;
private String playersOnly;
private String consoleOnly;
@@ -157,10 +206,11 @@ public final class SonarConfiguration {
private String networkStatistics;
private String cpuStatistics;
private List<String> helpHeader;
private String helpSubcommands;
private String verboseSubscribed;
private String verboseUnsubscribed;
private String verboseSubscribedOther;
private String verboseUnsubscribedOther;
private String reloading;
private String reloaded;
@@ -173,11 +223,10 @@ public final class SonarConfiguration {
@Getter
@RequiredArgsConstructor
public enum Type {
MYSQL(new Dependency[]{Dependency.MYSQL}, "com.mysql.cj.jdbc.NonRegisteringDriver"),
MARIADB(new Dependency[]{Dependency.MYSQL, Dependency.MARIADB}, "org.mariadb.jdbc.Driver"),
MYSQL(Dependency.MYSQL, "com.mysql.cj.jdbc.NonRegisteringDriver"),
NONE(null, null);
private final Dependency[] dependencies;
private final Dependency dependency;
private final String driverClassName;
}
@@ -191,19 +240,38 @@ public final class SonarConfiguration {
@Getter
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public static final class Lockdown {
@Setter
private boolean enabled;
private boolean notifyAdmins;
private boolean logAttempts;
private Component disconnect;
private String bypassPermission;
private String activated;
private String deactivated;
private String notification;
private String consoleLog;
public static final class Webhook {
private String url;
private String username;
private String avatarUrl;
private String content;
private final Footer footer = new Footer();
@Getter
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public static final class Footer {
private String text;
private String iconUrl;
}
private final Embed embed = new Embed();
@Getter
@NoArgsConstructor
@AllArgsConstructor
public static final class Embed {
private String title;
private String titleUrl;
@Setter
private String description;
private int r, g, b;
}
}
@Getter
private @Nullable DiscordWebhook discordWebhook;
public void load() {
if (generalConfig == null) {
generalConfig = new SimpleYamlConfig(pluginFolder, "config");
@@ -219,23 +287,32 @@ public final class SonarConfiguration {
// General options
generalConfig.getYaml().setComment("language",
"Name of the language file Sonar should use for messages"
);
"Name of the language file Sonar should use for messages");
String language = generalConfig.getString("language", "en");
generalConfig.getYaml().setComment("max-online-per-ip",
"Maximum number of players online with the same IP address"
);
"Maximum number of players online with the same IP address");
maxOnlinePerIp = clamp(generalConfig.getInt("max-online-per-ip", 3), 1, Byte.MAX_VALUE);
generalConfig.getYaml().setComment("min-players-for-attack",
"Minimum number of new players in order for an attack to be detected"
);
minPlayersForAttack = clamp(generalConfig.getInt("min-players-for-attack", 5), 2, 1024);
"Minimum number of new players in order for an attack to be detected");
minPlayersForAttack = clamp(generalConfig.getInt("min-players-for-attack", 8), 2, 1024);
generalConfig.getYaml().setComment("min-attack-duration",
"Amount of time (in milliseconds) that has to pass in order for an attack to be over");
minAttackDuration = clamp(generalConfig.getInt("min-attack-duration", 30000), 1000, 900000);
generalConfig.getYaml().setComment("min-attack-threshold",
"Number of times an incident has to be reported in order to be acknowledged as an attack"
+ LINE_SEPARATOR + "This number acts as a buffer to filter out false attack notifications");
minAttackThreshold = clamp(generalConfig.getInt("min-attack-threshold", 2), 0, 20);
generalConfig.getYaml().setComment("attack-cooldown-delay",
"Amount of time (in milliseconds) that has to pass in order for a new attack to be detected");
attackCooldownDelay = clamp(generalConfig.getInt("attack-cooldown-delay", 3000), 100, 30000);
generalConfig.getYaml().setComment("log-player-addresses",
"Should Sonar log players' IP addresses in console?"
);
"Should Sonar log players' IP addresses in console?");
logPlayerAddresses = generalConfig.getBoolean("log-player-addresses", true);
// Message settings
@@ -264,10 +341,9 @@ public final class SonarConfiguration {
// Database
generalConfig.getYaml().setComment("database.type",
"Type of database Sonar uses to store verified players"
+ LINE_SEPARATOR + "Possible types: NONE, MYSQL, MARIADB (experimental)"
);
final String newDatabaseType = generalConfig.getString("database.type", Database.Type.NONE.name());
database.type = Database.Type.valueOf(newDatabaseType.toUpperCase());
+ LINE_SEPARATOR + "Possible types: NONE, MYSQL");
database.type = Database.Type.valueOf(
generalConfig.getString("database.type", Database.Type.NONE.name()).toUpperCase());
generalConfig.getYaml().setComment("database",
"You can connect Sonar to a database to keep verified players even after restarting your server"
@@ -295,27 +371,6 @@ public final class SonarConfiguration {
"Password for authenticating with the SQL database");
database.password = generalConfig.getString("database.password", "");
// Lockdown
generalConfig.getYaml().setComment("lockdown",
"You can lock your server down using '/sonar lockdown' if, for example,"
+ LINE_SEPARATOR + "bots are bypassing the verification or any other reason");
generalConfig.getYaml().setComment("lockdown.enabled",
"Should Sonar prevent all players from joining the server?");
lockdown.enabled = generalConfig.getBoolean("lockdown.enabled", false);
generalConfig.getYaml().setComment("lockdown.log-attempts",
"Should Sonar log new login attempts during lockdown?");
lockdown.logAttempts = generalConfig.getBoolean("lockdown.log-attempts", true);
generalConfig.getYaml().setComment("lockdown.notify-admins",
"Should Sonar notify admins when they join the server during lockdown?");
lockdown.notifyAdmins = generalConfig.getBoolean("lockdown.notify-admins", true);
generalConfig.getYaml().setComment("lockdown.bypass-permission",
"Which permission does a player need in order to bypass the lockdown mode?"
+ LINE_SEPARATOR + "Players with this permission will also receive admin notifications when joining");
lockdown.bypassPermission = generalConfig.getString("lockdown.bypass-permission", "sonar.lockdown.bypass");
// Queue
generalConfig.getYaml().setComment("queue",
"Every new login request will be queued to avoid spam join attacks"
@@ -328,37 +383,106 @@ public final class SonarConfiguration {
generalConfig.getYaml().setComment("verification",
"Every new player that joins for the first time will be sent to"
+ LINE_SEPARATOR + "a lightweight limbo server where advanced bot checks are performed");
generalConfig.getYaml().setComment("verification.enabled",
"Should Sonar verify new players? (Recommended)");
verification.enabled = generalConfig.getBoolean("verification.enabled", true);
generalConfig.getYaml().setComment("verification.timing",
"When should Sonar verify new players?"
+ LINE_SEPARATOR + "Possible types: ALWAYS, DURING_ATTACK, NEVER"
+ LINE_SEPARATOR + "- ALWAYS: New players will always be checked (Recommended)"
+ LINE_SEPARATOR + "- DURING_ATTACK: New players will only be checked during an attack"
+ LINE_SEPARATOR + "- NEVER: New players will never be checked");
verification.timing = Verification.Timing.valueOf(
generalConfig.getString("verification.timing", Verification.Timing.ALWAYS.name()).toUpperCase());
generalConfig.getYaml().setComment("verification.checks.gravity",
"Checks if the players' falling motion is following Minecraft's gravity formula"
+ LINE_SEPARATOR + "All predicted motions are precalculated in order to save performance");
generalConfig.getYaml().setComment("verification.checks.gravity.enabled",
"Should Sonar check for valid client gravity? (Recommended)");
verification.checkGravity = generalConfig.getBoolean("verification.checks.gravity.enabled", true);
verification.gravity.enabled = generalConfig.getBoolean("verification.checks.gravity.enabled", true);
generalConfig.getYaml().setComment("verification.checks.gravity.max-movement-ticks",
"Maximum number of ticks the player has to fall in order to be allowed to hit the platform");
verification.maxMovementTicks = clamp(generalConfig.getInt("verification.checks.gravity.max-movement-ticks", 8),
verification.gravity.maxMovementTicks = clamp(generalConfig.getInt("verification.checks.gravity" +
".max-movement-ticks", 8),
2, 100);
generalConfig.getYaml().setComment("verification.checks.gravity.max-ignored-ticks",
"Maximum number of ignored Y movement changes before a player fails verification");
verification.maxIgnoredTicks = clamp(generalConfig.getInt("verification.checks.gravity.max-ignored-ticks", 5), 1,
128);
verification.gravity.maxIgnoredTicks = clamp(generalConfig.getInt("verification.checks.gravity.max-ignored-ticks"
, 5),
1, 128);
generalConfig.getYaml().setComment("verification.checks.collisions",
"Checks if the players collides with barrier blocks spawned below the player"
+ LINE_SEPARATOR + "Note: The collision check will be skipped if the gravity check is disabled");
generalConfig.getYaml().setComment("verification.checks.collisions.enabled",
"Should Sonar check for valid client collisions? (Recommended)");
verification.checkCollisions = generalConfig.getBoolean("verification.checks.collisions.enabled", true);
generalConfig.getYaml().setComment("verification.checks.gravity.gamemode",
"The gamemode of the player during verification"
+ LINE_SEPARATOR + "Possible types: SURVIVAL, CREATIVE, ADVENTURE, SPECTATOR"
+ LINE_SEPARATOR + "- SURVIVAL: all UI components are visible"
+ LINE_SEPARATOR + "- CREATIVE: health and hunger are hidden"
+ LINE_SEPARATOR + "- ADVENTURE: all UI components are visible");
verification.gravity.gamemode = Verification.Gravity.Gamemode.valueOf(
generalConfig.getString("verification.checks.gravity.gamemode", Verification.Gravity.Gamemode.ADVENTURE.name()).toUpperCase());
generalConfig.getYaml().setComment("verification.gamemode",
"The gamemode of the player during verification (0, 1, 2, or 3)");
verification.gamemodeId = (short) clamp(generalConfig.getInt("verification.gamemode", 3), 0, 3);
generalConfig.getYaml().setComment("verification.checks.map-captcha",
"Make the player type a code from a virtual map in chat after the gravity check");
generalConfig.getYaml().setComment("verification.checks.map-captcha.enabled",
"Should Sonar make the player pass a captcha?");
verification.map.enabled = generalConfig.getBoolean("verification.checks.map-captcha.enabled", false);
generalConfig.getYaml().setComment("verification.checks.map-captcha.background-noise",
"Should Sonar draw a white/light gray rectangle behind the captcha?");
verification.map.drawBackgroundNoise = generalConfig.getBoolean("verification.checks.map-captcha" +
".background-noise", true);
generalConfig.getYaml().setComment("verification.checks.map-captcha.random-position",
"Should Sonar randomize the X and Y position of the captcha?");
verification.map.randomizePositions = generalConfig.getBoolean("verification.checks.map-captcha.random-position",
true);
generalConfig.getYaml().setComment("verification.checks.map-captcha.random-font-size",
"Should Sonar randomize the size of the font used for rendering the captcha?");
verification.map.randomizeFontSize = generalConfig.getBoolean("verification.checks.map-captcha.random-font-size",
true);
generalConfig.getYaml().setComment("verification.checks.map-captcha.precompute",
"How many answers should Sonar precompute (prepare)?");
verification.map.precomputeAmount = generalConfig.getInt("verification.checks.map-captcha.precompute", 1000);
generalConfig.getYaml().setComment("verification.checks.map-captcha.max-duration",
"How long should Sonar wait until the player fails the captcha?");
verification.map.maxDuration = generalConfig.getInt("verification.checks.map-captcha.max-duration", 45000);
generalConfig.getYaml().setComment("verification.checks.map-captcha.max-tries",
"How many times must a player fail the captcha before failing the verification?");
verification.map.maxTries = generalConfig.getInt("verification.checks.map-captcha.max-tries", 3);
generalConfig.getYaml().setComment("verification.checks.map-captcha.dictionary",
"Characters (letters and numbers) that are allowed to appear in the answer to the captcha");
verification.map.dictionary = generalConfig.getString("verification.checks.map-captcha.dictionary", "123456789");
generalConfig.getYaml().setComment("verification.checks.valid-name-regex",
"Regex for validating usernames during verification");
verification.validNameRegex = Pattern.compile(generalConfig.getString(
"verification.checks.valid-name-regex", "^[a-zA-Z0-9_]+$"));
generalConfig.getYaml().setComment("verification.checks.valid-brand-regex",
"Regex for validating client brands during verification");
verification.validBrandRegex = Pattern.compile(generalConfig.getString(
"verification.checks.valid-brand-regex", "^[!-~ ]+$"));
generalConfig.getYaml().setComment("verification.checks.valid-locale-regex",
"Regex for validating client locale during verification");
verification.validLocaleRegex = Pattern.compile(generalConfig.getString(
"verification.checks.valid-locale-regex", "^[a-zA-Z_]+$"));
generalConfig.getYaml().setComment("verification.checks.max-brand-length",
"Maximum client brand length during verification");
verification.maxBrandLength = generalConfig.getInt("verification.checks.max-brand-length", 64);
generalConfig.getYaml().setComment("verification.checks.max-ping",
"Ping (in milliseconds) a player has to have in order to timeout");
verification.maxPing = clamp(generalConfig.getInt("verification.checks.max-ping", 10000), 500, 30000);
generalConfig.getYaml().setComment("verification.checks.max-login-packets",
"Maximum number of login packets the player has to send in order to be kicked");
verification.maxLoginPackets = clamp(generalConfig.getInt("verification.checks.max-login-packets", 256), 128, 8192);
generalConfig.getYaml().setComment("verification.log-connections",
"Should Sonar log new verification attempts?");
@@ -373,46 +497,108 @@ public final class SonarConfiguration {
+ LINE_SEPARATOR + "This is not recommended for production servers but can be helpful for spotting errors.");
verification.debugXYZPositions = generalConfig.getBoolean("verification.debug-xyz-positions", false);
generalConfig.getYaml().setComment("verification.valid-name-regex",
"Regex for validating usernames during verification");
verification.validNameRegex = Pattern.compile(generalConfig.getString(
"verification.valid-name-regex", "^[a-zA-Z0-9_.*!]+$"));
generalConfig.getYaml().setComment("verification.valid-brand-regex",
"Regex for validating client brands during verification");
verification.validBrandRegex = Pattern.compile(generalConfig.getString(
"verification.valid-brand-regex", "^[!-~ ]+$"));
generalConfig.getYaml().setComment("verification.valid-locale-regex",
"Regex for validating client locale during verification");
verification.validLocaleRegex = Pattern.compile(generalConfig.getString(
"verification.valid-locale-regex", "^[a-zA-Z_]+$"));
generalConfig.getYaml().setComment("verification.max-brand-length",
"Maximum client brand length during verification");
verification.maxBrandLength = generalConfig.getInt("verification.max-brand-length", 64);
generalConfig.getYaml().setComment("verification.max-ping",
"Ping (in milliseconds) a player has to have in order to timeout");
verification.maxPing = clamp(generalConfig.getInt("verification.max-ping", 10000), 500, 30000);
generalConfig.getYaml().setComment("verification.read-timeout",
"Amount of time that has to pass before a player times out");
verification.readTimeout = clamp(generalConfig.getInt("verification.read-timeout", 3500), 500, 30000);
generalConfig.getYaml().setComment("verification.max-login-packets",
"Maximum number of login packets the player has to send in order to be kicked");
verification.maxLoginPackets = clamp(generalConfig.getInt("verification.max-login-packets", 256), 128, 8192);
generalConfig.getYaml().setComment("verification.max-players",
"Maximum number of players verifying at the same time");
verification.maxVerifyingPlayers = clamp(generalConfig.getInt("verification.max-players", 1024), 1,
Short.MAX_VALUE);
generalConfig.getYaml().setComment("verification.rejoin-delay",
"Minimum number of rejoin delay during verification");
verification.reconnectDelay = clamp(generalConfig.getInt("verification.rejoin-delay", 8000), 0, 100000);
generalConfig.getYaml().setComment("verification.whitelisted-protocols",
"List of protocol IDs which are not checked by Sonar (verification bypass)"
+ LINE_SEPARATOR + "You can find the full list of all protocol IDs here:"
+ LINE_SEPARATOR + "https://wiki.vg/Protocol_version_numbers"
+ LINE_SEPARATOR + "For example, Minecraft 1.20 has the ID 763.");
verification.whitelistedProtocols.clear();
verification.whitelistedProtocols.addAll(generalConfig.getIntList("verification.whitelisted-protocols",
new ArrayList<>(0)));
generalConfig.getYaml().setComment("verification.blacklisted-protocols",
"List of protocol IDs which are unable to join the server at all");
verification.blacklistedProtocols.clear();
verification.blacklistedProtocols.addAll(generalConfig.getIntList("verification.blacklisted-protocols",
new ArrayList<>(0)));
generalConfig.getYaml().setComment("webhook",
"Bot attack notifications can also be sent to your Discord server using webhooks");
generalConfig.getYaml().setComment("webhook.url",
"URL of the Discord webhook (Set this to '' to disable webhooks)");
webhook.url = generalConfig.getString("webhook.url", "");
generalConfig.getYaml().setComment("webhook.username",
"Username of the Discord webhook sender");
webhook.username = generalConfig.getString("webhook.username", "Sonar");
generalConfig.getYaml().setComment("webhook.avatar-url",
"URL to the avatar of the Discord webhook sender (Set this to '' to disable)");
webhook.avatarUrl = generalConfig.getString("webhook.avatar-url", "");
generalConfig.getYaml().setComment("webhook.content",
"Content of the Discord webhook message (Set this to '' to disable)"
+ LINE_SEPARATOR + "You can use this to e.g. ping staff members using <@userId>"
+ LINE_SEPARATOR + "If you want to ping roles, you will need to use <@&roleId>");
webhook.content = generalConfig.getString("webhook.content", "");
generalConfig.getYaml().setComment("webhook.embed.footer",
"Small footer message of the Discord webhook embed");
generalConfig.getYaml().setComment("webhook.embed.footer.text",
"Content of the footer message of the Discord webhook embed");
webhook.footer.text = generalConfig.getString("webhook.embed.footer.text",
"© Jones Development and Sonar Contributors");
generalConfig.getYaml().setComment("webhook.embed.footer.icon-url",
"URL of the footer message icon of the Discord webhook embed");
webhook.footer.iconUrl = generalConfig.getString("webhook.embed.footer.icon-url", "");
final String realEmbedPath = "webhook.embed";
final String embedPath = realEmbedPath + ".";
generalConfig.getYaml().setComment(realEmbedPath,
"Embed Discord webhook message that is sent when an attack has stopped");
generalConfig.getYaml().setComment(embedPath + "title",
"Title of the Discord webhook embed");
webhook.embed.title = generalConfig.getString(embedPath + "title", ":white_check_mark: Attack mitigated");
generalConfig.getYaml().setComment(embedPath + "title-url",
"Clickable URL of the title of the Discord webhook embed");
webhook.embed.titleUrl = generalConfig.getString(embedPath + "title-url", "");
generalConfig.getYaml().setComment(embedPath + "description",
"Description (content) of the Discord webhook embed");
webhook.embed.description = fromList(generalConfig.getStringList(embedPath + "description", Arrays.asList(
"The attack on your server has been mitigated.",
"",
"Attack start: <t:%start-timestamp%:T>",
"Attack end: <t:%end-timestamp%:T>",
"Attack duration: %duration%",
"",
"Peak process CPU usage during the attack: %peak-cpu%%",
"Peak process memory usage during the attack: %peak-memory%",
"Peak bots per second during the attack: %peak-bps%",
"",
"Blacklisted IP addresses during the attack: %total-blacklisted%",
"Failed verifications during the attack: %total-failed%",
"Successful verifications during the attack: %total-success%"
)), LINE_SEPARATOR);
generalConfig.getYaml().setComment(embedPath + "color",
"RGB colors of the Discord webhook embed"
+ LINE_SEPARATOR + "Color picker: https://www.rapidtables.com/web/color/RGB_Color.html");
webhook.embed.r = generalConfig.getInt(embedPath + "color.red", 0);
webhook.embed.g = generalConfig.getInt(embedPath + "color.green", 255);
webhook.embed.b = generalConfig.getInt(embedPath + "color.blue", 0);
if (!webhook.url.isEmpty()) {
if (webhook.username.isEmpty()) {
throw new IllegalStateException("Webhook username cannot be empty");
}
discordWebhook = new DiscordWebhook(webhook.url);
} else if (discordWebhook != null) {
// Reset if Discord webhooks were disabled
discordWebhook = null;
}
// load this here otherwise it could cause issues
messagesConfig.getYaml().setComment("header",
"Placeholder for every '%header%' in this configuration file");
@@ -448,7 +634,7 @@ public final class SonarConfiguration {
"%prefix%<red>Usage: /sonar %usage%"));
messagesConfig.getYaml().setComment("commands.invalid-ip-address",
"Message that is shown when someone provides an invalid IP address (Invalid characters)");
"Message that is shown when someone provides an invalid IP address (Invalid format)");
commands.incorrectIpAddress = formatString(messagesConfig.getString("commands.invalid-ip-address",
"%prefix%The IP address you provided seems to be invalid."));
@@ -457,11 +643,6 @@ public final class SonarConfiguration {
commands.illegalIpAddress = formatString(messagesConfig.getString("commands.illegal-ip-address",
"%prefix%The IP address you provided seems to be either a local or loopback IP."));
messagesConfig.getYaml().setComment("commands.unknown-ip-address",
"Message that is shown when someone provides an unknown IP address (Unknown Host)");
commands.unknownIpAddress = formatString(messagesConfig.getString("commands.unknown-ip-address",
"%prefix%The IP address you provided seems to be unknown."));
messagesConfig.getYaml().setComment("commands.player-only",
"Message that is shown when the console runs a command that is player-only");
commands.playersOnly = formatString(messagesConfig.getString("commands.player-only",
@@ -484,28 +665,32 @@ public final class SonarConfiguration {
commands.subCommandNoPerm = formatString(messagesConfig.getString("commands.subcommand-no-permission",
"%prefix%<red>You do not have permission to execute this subcommand. <gray>(%permission%)"));
messagesConfig.getYaml().setComment("lockdown",
"Translations for '/sonar lockdown'");
messagesConfig.getYaml().setComment("lockdown.notification",
"Message that is shown when an admin joins the server during lockdown");
lockdown.notification = formatString(messagesConfig.getString("lockdown.notification",
"%prefix%<green>Hey, the server is currently in lockdown mode. If you want to disable the lockdown mode,"
+ " type <white>/sonar lockdown<green>."
));
messagesConfig.getYaml().setComment("lockdown.console-log",
"Message that is shown to console when a normal player tries joining the server during lockdown");
lockdown.consoleLog = messagesConfig.getString("lockdown.console-log",
"%player% (%ip%, %protocol%) tried to join during lockdown mode.");
messagesConfig.getYaml().setComment("lockdown.disconnect-message",
"Message that is shown to a normal player when they try joining the server during lockdown");
lockdown.disconnect = deserialize(fromList(messagesConfig.getStringList("lockdown.disconnect-message",
messagesConfig.getYaml().setComment("commands.main",
"Translations for '/sonar'");
messagesConfig.getYaml().setComment("commands.main.header",
"Informational message that is shown above everything when running the main command");
commands.helpHeader = messagesConfig.getStringList("commands.main.header",
Arrays.asList(
"%header%",
"<red>The server is currently locked down, please try again later.",
"%footer%"
))));
"<yellow>Running Sonar %version% on %platform%.",
"<yellow>(C) %copyright_year% Jones Development and Sonar Contributors",
"<green><click:open_url:'https://github.com/jonesdevelopment/sonar'>https://github.com/jonesdevelopment/sonar",
"",
"<yellow>Need help or have any questions?",
"<yellow><click:open_url:'https://jonesdev.xyz/discord/'><hover:show_text:'(Click to open Discord)'>Open a " +
"ticket on the Discord </hover></click></yellow><yellow><click:open_url:'https://github" +
".com/jonesdevelopment/sonar/issues'><hover:show_text:'(Click to open GitHub)'>or open a new issue on " +
"GitHub.",
""
));
messagesConfig.getYaml().setComment("commands.main.subcommands",
"Formatting of the list of subcommands shown when running the main command");
commands.helpSubcommands = formatString(messagesConfig.getString("commands.main.subcommands",
"<click:suggest_command:'/sonar %subcommand% '><hover:show_text:'<gray>Only players: " +
"</gray>%only_players%<br><gray>Require console: </gray>%require_console%<br><gray>Permission: " +
"</gray><white>%permission%<br><gray>Aliases: </gray>%aliases%'><gray> ▪ </gray><green>/sonar " +
"%subcommand%</green><gray> - </gray><white>%description%"));
SonarCommand.prepareCachedMessages();
messagesConfig.getYaml().setComment("commands.reload",
"Translations for '/sonar reload'");
@@ -519,18 +704,6 @@ public final class SonarConfiguration {
commands.reloaded = formatString(messagesConfig.getString("commands.reload.finish",
"%prefix%<green>Successfully reloaded <gray>(%taken%ms)"));
messagesConfig.getYaml().setComment("commands.lockdown",
"Translations for '/sonar lockdown'");
messagesConfig.getYaml().setComment("commands.lockdown.enabled",
"Message that is shown when a player enables server lockdown");
lockdown.activated = formatString(messagesConfig.getString("commands.lockdown.enabled",
"%prefix%The server is now in lockdown mode."));
messagesConfig.getYaml().setComment("commands.lockdown.disabled",
"Message that is shown when a player disables server lockdown");
lockdown.deactivated = formatString(messagesConfig.getString("commands.lockdown.disabled",
"%prefix%The server is no longer in lockdown mode."));
messagesConfig.getYaml().setComment("commands.verbose",
"Translations for '/sonar verbose'");
messagesConfig.getYaml().setComment("commands.verbose.subscribed",
@@ -543,16 +716,6 @@ public final class SonarConfiguration {
commands.verboseUnsubscribed = formatString(messagesConfig.getString("commands.verbose.unsubscribed",
"%prefix%You are no longer viewing Sonar verbose."));
messagesConfig.getYaml().setComment("commands.verbose.subscribed-other",
"Message that is shown when a player makes another player subscribe to Sonar verbose");
commands.verboseSubscribedOther = formatString(messagesConfig.getString("commands.verbose.subscribed-other",
"%prefix%%player% is now viewing Sonar verbose."));
messagesConfig.getYaml().setComment("commands.verbose.unsubscribed-other",
"Message that is shown when a player makes another player unsubscribe from Sonar verbose");
commands.verboseUnsubscribedOther = formatString(messagesConfig.getString("commands.verbose.unsubscribed-other",
"%prefix%%player% is no longer viewing Sonar verbose."));
messagesConfig.getYaml().setComment("commands.blacklist",
"Translations for '/sonar blacklist'");
messagesConfig.getYaml().setComment("commands.blacklist.empty",
@@ -709,6 +872,25 @@ public final class SonarConfiguration {
verification.successLog = formatString(messagesConfig.getString("verification.logs.successful",
"%name% has been verified successfully (%time%s!)."));
messagesConfig.getYaml().setComment("verification.welcome",
"Message that is shown to the player when they are being checked for valid gravity");
verification.gravity.youAreBeingChecked = deserialize(formatString(messagesConfig.getString("verification.welcome",
"%prefix%<gray>Please wait a moment for the verification to finish...")));
messagesConfig.getYaml().setComment("verification.captcha.enter-code",
"Message that is shown to the player when they have to enter the answer to the captcha");
verification.map.enterCode = deserialize(formatString(messagesConfig.getString("verification.captcha.enter-code",
"%prefix%<green>Please enter the code in chat that is displayed on the map.")));
messagesConfig.getYaml().setComment("verification.captcha.action-bar",
"Timer that is shown to the player when they have to enter the answer to the captcha"
+ LINE_SEPARATOR + "(Set this to '' to disable the action bar message)");
verification.map.enterCodeActionBar = formatString(messagesConfig.getString("verification.captcha.action-bar",
"%prefix%<green>You have %time-left% seconds left to enter the code in chat"));
messagesConfig.getYaml().setComment("verification.captcha.incorrect",
"Message that is shown to the player when they enter the wrong answer in chat");
verification.map.failedCaptcha = deserialize(formatString(messagesConfig.getString("verification.captcha.incorrect",
"%prefix%<red>You have entered the wrong code. Please try again.")));
messagesConfig.getYaml().setComment("verification.too-many-players",
"Disconnect message that is shown when too many players are verifying at the same time");
verification.tooManyPlayers = deserialize(fromList(messagesConfig.getStringList("verification.too-many-players",
@@ -778,6 +960,17 @@ public final class SonarConfiguration {
"%footer%"
))));
messagesConfig.getYaml().setComment("verification.blacklisted-protocol",
"Disconnect message that is shown when someone joins with a blacklisted version");
verification.protocolBlacklisted = deserialize(fromList(messagesConfig.getStringList("verification" +
".blacklisted-protocol",
Arrays.asList(
"%header%",
"<red>You are using a version that is not allowed on our server.",
"<gold>Need help logging in? <gray>%support-url%",
"%footer%"
))));
messagesConfig.getYaml().setComment("verification.already-online",
"Disconnect message that is shown when someone tries verifying with an account that is online");
verification.alreadyConnected = deserialize(fromList(messagesConfig.getStringList("verification.already-online",
@@ -811,28 +1004,29 @@ public final class SonarConfiguration {
messagesConfig.getYaml().setComment("verbose",
"Translations for all messages regarding Sonar's verbose output");
messagesConfig.getYaml().setComment("verbose.layout",
"General layout for the verbose action-bar" +
LINE_SEPARATOR + "Placeholders and their descriptions:" +
LINE_SEPARATOR + "- %queued% Number of queued connections" +
LINE_SEPARATOR + "- %verifying% Number of verifying connections" +
LINE_SEPARATOR + "- %blacklisted% Number of blacklisted IP addresses" +
LINE_SEPARATOR + "- %total-joins% Number of total joins (not unique!)" +
LINE_SEPARATOR + "- %per-second-joins% Number of joins per second" +
LINE_SEPARATOR + "- %verify-total% Number of total verification attempts" +
LINE_SEPARATOR + "- %verify-success% Number of verified IP addresses" +
LINE_SEPARATOR + "- %verify-failed% Number of failed verifications" +
LINE_SEPARATOR + "- %incoming-traffic% Incoming bandwidth usage per second" +
LINE_SEPARATOR + "- %outgoing-traffic% Outgoing bandwidth usage per second" +
LINE_SEPARATOR + "- %incoming-traffic-ttl% Total incoming bandwidth usage" +
LINE_SEPARATOR + "- %outgoing-traffic-ttl% Total outgoing bandwidth usage" +
LINE_SEPARATOR + "- %used-memory% Amount of used memory (JVM process)" +
LINE_SEPARATOR + "- %total-memory% Amount of total memory (JVM process)" +
LINE_SEPARATOR + "- %max-memory% Amount of max memory (JVM process)" +
LINE_SEPARATOR + "- %free-memory% Amount of free memory (JVM process)" +
LINE_SEPARATOR + "- %animation% Animated spinning circle (by default)"
);
verbose.actionBarLayout = formatString(messagesConfig.getString("verbose.layout",
messagesConfig.getYaml().setComment("verbose.layout.normal",
"General layout for the verbose action-bar"
+ LINE_SEPARATOR + "Placeholders:"
+ LINE_SEPARATOR + "- '%queued%' Number of queued connections"
+ LINE_SEPARATOR + "- '%verifying%' Number of verifying connections"
+ LINE_SEPARATOR + "- '%blacklisted%' Number of blacklisted IP addresses"
+ LINE_SEPARATOR + "- '%total-joins%' Number of total attempted joins"
+ LINE_SEPARATOR + "- '%verify-total%' Number of total verification attempts"
+ LINE_SEPARATOR + "- '%verify-success%' Number of verified IP addresses"
+ LINE_SEPARATOR + "- '%verify-failed%' Number of failed verifications"
+ LINE_SEPARATOR + "- '%logins-per-second%' Number of logins per second"
+ LINE_SEPARATOR + "- '%attack-duration%' Duration of the current attack"
+ LINE_SEPARATOR + "- '%incoming-traffic%' Incoming bandwidth usage per second"
+ LINE_SEPARATOR + "- '%outgoing-traffic%' Outgoing bandwidth usage per second"
+ LINE_SEPARATOR + "- '%incoming-traffic-ttl%' Total incoming bandwidth usage"
+ LINE_SEPARATOR + "- '%outgoing-traffic-ttl%' Total outgoing bandwidth usage"
+ LINE_SEPARATOR + "- '%used-memory%' Amount of used memory of the process"
+ LINE_SEPARATOR + "- '%total-memory%' Amount of total memory of the process"
+ LINE_SEPARATOR + "- '%max-memory%' Amount of max memory of the process"
+ LINE_SEPARATOR + "- '%free-memory%' Amount of free memory of the process"
+ LINE_SEPARATOR + "- '%animation%' Customizable animated symbol"
+ LINE_SEPARATOR + "Translations for Sonar's normal verbose output");
verbose.actionBarLayout = formatString(messagesConfig.getString("verbose.layout.normal",
String.join(" <dark_aqua>╺ ", Arrays.asList(
"%prefix%<gray>Queued <white>%queued%",
"<gray>Verifying <white>%verifying%",
@@ -840,12 +1034,22 @@ public final class SonarConfiguration {
" <dark_aqua>| <green>⬆ <white>%outgoing-traffic%/s <red>⬇ <white>%incoming-traffic%/s" +
" <green><bold>%animation%<reset>"
))));
messagesConfig.getYaml().setComment("verbose.animation",
"Alternative symbols:"
+ LINE_SEPARATOR + "- ▙"
+ LINE_SEPARATOR + "- ▛"
+ LINE_SEPARATOR + "- ▜"
+ LINE_SEPARATOR + "- ▟");
messagesConfig.getYaml().setComment("verbose.layout.attack",
"Translations for Sonar's verbose output during an active attack");
verbose.actionBarLayoutDuringAttack = formatString(messagesConfig.getString("verbose.layout.attack",
String.join(" <dark_aqua>╺ ", Arrays.asList(
"%prefix%<gray>BPS <white>%logins-per-second%",
"<gray>Queued <white>%queued%",
"<gray>Verifying <white>%verifying%",
"<gray>Blacklisted <white>%blacklisted%",
"<gray>Duration <white>%attack-duration%" +
" <dark_aqua>| <green>⬆ <white>%outgoing-traffic%/s <red>⬇ <white>%incoming-traffic%/s" +
" <green><bold>%animation%<reset>"
))));
messagesConfig.getYaml().setComment("verbose.animation", "Animation for the action bar"
+ LINE_SEPARATOR + "Alternatives:"
+ LINE_SEPARATOR + "- ▙, ▛, ▜, ▟"
+ LINE_SEPARATOR + "- ⬈, ⬊, ⬋, ⬉");
verbose.animation = Collections.unmodifiableList(messagesConfig.getStringList("verbose.animation",
Arrays.asList("◜", "◝", "◞", "◟")
));
@@ -866,7 +1070,11 @@ public final class SonarConfiguration {
}
private @NotNull String fromList(final @NotNull Collection<String> list) {
return formatString(String.join("<newline>", list));
return fromList(list, "<newline>");
}
private @NotNull String fromList(final @NotNull Collection<String> list, final String newline) {
return formatString(String.join(newline, list));
}
private static @NotNull Component deserialize(final String legacy) {
@@ -65,6 +65,8 @@ public final class VerifiedPlayerController {
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);
@@ -92,7 +94,11 @@ public final class VerifiedPlayerController {
}
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()
@@ -134,7 +140,11 @@ public final class VerifiedPlayerController {
}
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);
@@ -38,8 +38,9 @@ public enum Dependency {
MYSQL(
"com/mysql",
"mysql-connector-j",
"8.1.0"
"8.2.0"
),
// TODO: MariaDB still seems to have some issues; investigate!
// https://mvnrepository.com/artifact/org.mariadb.jdbc/mariadb-java-client
MARIADB(
"org/mariadb/jdbc",
@@ -47,10 +48,8 @@ public enum Dependency {
"3.1.4"
);
private @NotNull
final Path tempFilePath;
private @NotNull
final URL mvnRepoURL;
private final Path tempFilePath;
private final URL mvnRepoURL;
Dependency(final @NotNull String groupId,
final @NotNull String artifactId,
@@ -22,6 +22,7 @@ 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.SonarPlatform;
import xyz.jonesdev.sonar.api.config.SonarConfiguration;
import java.io.File;
@@ -36,44 +37,42 @@ import java.util.Properties;
@UtilityClass
public class DependencyLoader {
public ConnectionSource setUpDriverAndConnect() throws Throwable {
final SonarConfiguration config = Sonar.get().getConfig();
final SonarConfiguration.Database database = Sonar.get().getConfig().getDatabase();
final URL[] urls = new URL[config.getDatabase().getType().getDependencies().length];
for (final Dependency dependency : config.getDatabase().getType().getDependencies()) {
final URL url = dependency.getClassLoaderURL();
final ClassLoader currentClassLoader = DependencyLoader.class.getClassLoader();
final ClassLoader currentClassLoader = DependencyLoader.class.getClassLoader();
ClassLoader classLoader = currentClassLoader;
// Velocity doesn't include the JDBC driver, which is why we need to load it manually
if (Sonar.get().getPlatform() == SonarPlatform.VELOCITY) {
final URL url = database.getType().getDependency().getClassLoaderURL();
final Method addPath = currentClassLoader.getClass().getDeclaredMethod("addPath", Path.class);
addPath.setAccessible(true);
addPath.invoke(currentClassLoader, new File(url.toURI()).toPath());
urls[dependency.ordinal()] = url;
classLoader = new ExternalClassLoader(new URL[]{url});
}
final String type = config.getDatabase().getType().name().toLowerCase();
final String databaseURL = String.format("jdbc:%s://%s:%d/%s",
type, config.getDatabase().getUrl(), config.getDatabase().getPort(), config.getDatabase().getName());
final String jdbcURL = String.format("jdbc:%s://%s:%d/%s",
database.getType().name().toLowerCase(), database.getUrl(), database.getPort(), database.getName());
final ExternalClassLoader classLoader = new ExternalClassLoader(urls);
final Connection connection = connect(classLoader, databaseURL, config.getDatabase());
return new JdbcSingleConnectionSource(databaseURL, connection);
final Connection connection = connect(classLoader, jdbcURL, database);
return new JdbcSingleConnectionSource(jdbcURL, connection);
}
// Mostly taken from
// https://github.com/Elytrium/LimboAuth/blob/master/src/main/java/net/elytrium/limboauth/dependencies/DatabaseLibrary.java#L134
// https://github.com/Elytrium/LimboAuth/blob/master/src/main/java/net/elytrium/limboauth/dependencies/DatabaseLibrary.java#L137
private Connection connect(final @NotNull ClassLoader classLoader,
final @NotNull String databaseURL,
final @NotNull SonarConfiguration.Database config) throws Throwable {
final Class<?> driverClass = classLoader.loadClass(config.getType().getDriverClassName());
final @NotNull SonarConfiguration.Database database) throws Throwable {
final Class<?> driverClass = classLoader.loadClass(database.getType().getDriverClassName());
final Object driver = driverClass.getDeclaredConstructor().newInstance();
DriverManager.registerDriver((Driver) driver);
final Properties properties = new Properties();
properties.put("user", config.getUsername());
if (!config.getPassword().isEmpty()) {
properties.put("password", config.getPassword());
}
properties.put("user", database.getUsername());
properties.put("password", database.getPassword());
final Method connect = driverClass.getDeclaredMethod("connect", String.class, Properties.class);
connect.setAccessible(true);
@@ -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));
}
@@ -15,27 +15,9 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.bukkit.verbose;
package xyz.jonesdev.sonar.api.event.impl;
import lombok.RequiredArgsConstructor;
import net.kyori.adventure.text.Component;
import org.bukkit.Server;
import org.bukkit.entity.Player;
import xyz.jonesdev.sonar.api.verbose.Verbose;
import xyz.jonesdev.sonar.api.event.SonarEvent;
@RequiredArgsConstructor
public final class ActionBarVerbose extends Verbose {
private final Server server;
@Override
public void broadcast(final Component component) {
synchronized (subscribers) {
for (final String subscriber : subscribers) {
final Player player = server.getPlayer(subscriber);
if (player != null) {
// TODO: use an adventure audience
}
}
}
}
public final class AttackDetectedEvent implements SonarEvent {
}
@@ -15,28 +15,19 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.api.server;
package xyz.jonesdev.sonar.api.event.impl;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import xyz.jonesdev.sonar.api.SonarPlatform;
import xyz.jonesdev.sonar.api.command.InvocationSource;
import java.util.Optional;
import lombok.ToString;
import xyz.jonesdev.sonar.api.attack.AttackTracker;
import xyz.jonesdev.sonar.api.event.SonarEvent;
@Getter
@ToString
@EqualsAndHashCode
@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);
public final class AttackMitigatedEvent implements SonarEvent {
private final AttackTracker.AttackStatistics attackStatistics;
}
@@ -19,16 +19,23 @@ package xyz.jonesdev.sonar.api.event.impl;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
import org.jetbrains.annotations.Nullable;
import xyz.jonesdev.sonar.api.event.SonarEvent;
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
import java.util.Optional;
@Getter
@ToString
@EqualsAndHashCode
@RequiredArgsConstructor
public final class UserVerifyFailedEvent implements SonarEvent {
private final FallbackUser<?, ?> user;
private final String reason;
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
private final Optional<String> reason;
public UserVerifyFailedEvent(final FallbackUser<?, ?> user, final @Nullable String reason) {
this.user = user;
this.reason = Optional.ofNullable(reason);
}
}
@@ -20,10 +20,12 @@ package xyz.jonesdev.sonar.api.fallback;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.val;
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.logger.LoggerWrapper;
import java.net.InetAddress;
@@ -38,11 +40,10 @@ 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<>();
private final Map<String, InetAddress> connected = new ConcurrentHashMap<>(64, 0.75f);
// Only block the player for a few minutes to avoid issues
private final ExpiringCache<String> blacklisted = Cappuccino.buildExpiring(
10L, TimeUnit.MINUTES, 2500L
);
private final ExpiringCache<InetAddress> blacklisted = Cappuccino.buildExpiring(
10L, TimeUnit.MINUTES, 5000L);
private final @NotNull FallbackQueue queue = FallbackQueue.INSTANCE;
private final @NotNull FallbackRatelimiter ratelimiter = FallbackRatelimiter.INSTANCE;
@@ -64,8 +65,11 @@ public final class Fallback {
}
};
public boolean isPotentiallyUnderAttack() {
final int min = SONAR.getConfig().getMinPlayersForAttack();
return connected.size() > min || queue.getQueuedPlayers().size() > min;
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
public boolean shouldVerifyNewPlayers() {
val timing = Sonar.get().getConfig().getVerification().getTiming();
return timing == SonarConfiguration.Verification.Timing.ALWAYS
|| (timing == SonarConfiguration.Verification.Timing.DURING_ATTACK
&& Sonar.get().getAttackTracker().isCurrentlyUnderAttack());
}
}
@@ -50,14 +50,14 @@ public final class FallbackQueue {
}
public void poll() {
final int max = Sonar.get().getConfig().getQueue().getMaxQueuePolls();
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.
// We need to be cautious here since we don't want any concurrency issues.
final Iterator<Map.Entry<InetAddress, Runnable>> iterator = queuedPlayers.entrySet().iterator();
while (iterator.hasNext()) {
// Break if we reached our maximum entries
if (++index > max) {
if (++index > maxQueuePolls) {
break;
}
// Run the cached runnable
@@ -66,5 +66,10 @@ public final class FallbackQueue {
// Remove runnable from iterator
iterator.remove();
}
// Run the attack check task
Sonar.get().getAttackTracker().checkIfUnderAttack();
// Clean up the cache of rate-limited IPs
Sonar.get().getFallback().getRatelimiter().cleanUpCache();
}
}
@@ -31,18 +31,26 @@ public final class FallbackRatelimiter {
@Setter
private ExpiringCache<InetAddress> expiringCache;
/**
* We don't want to clean up the cache in the
* {@link #attempt(InetAddress)} method because
* it would take up too many resources.
*/
public void cleanUpCache() {
expiringCache.cleanUp(false);
}
/**
* Checks if the player has tried verifying too fast
*
* @param inetAddress IP address of the player
* @return Whether the player is allowed to verify
*/
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
public boolean attempt(final @NotNull InetAddress inetAddress) {
// Clean up the cache
expiringCache.cleanUp(false);
// Cache the IP address if it's not already cached
if (!expiringCache.has(inetAddress)) {
// Cache the IP address
expiringCache.put(inetAddress);
return true;
}
@@ -92,9 +92,6 @@ public interface FallbackUser<X, Y> {
*/
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().getVerification().getVerificationFailed());
if (reason != null) {
@@ -107,6 +104,9 @@ public interface FallbackUser<X, Y> {
Statistics.FAILED_VERIFICATIONS.increment();
// Call the VerifyFailedEvent for external API usage
Sonar.get().getEventManager().publish(new UserVerifyFailedEvent(this, reason));
// Make sure old entries are removed
PREVIOUS_FAILS.cleanUp();
@@ -115,7 +115,7 @@ public interface FallbackUser<X, Y> {
// Call the BotBlacklistedEvent for external API usage
Sonar.get().getEventManager().publish(new UserBlacklistedEvent(this));
getFallback().getBlacklisted().put(getInetAddress().toString());
getFallback().getBlacklisted().put(getInetAddress());
getFallback().getLogger().info(Sonar.get().getConfig().getVerification().getBlacklistLog()
.replace("%ip%", Sonar.get().getConfig().formatAddress(getInetAddress()))
.replace("%protocol%", String.valueOf(getProtocolVersion().getProtocol())));
@@ -20,11 +20,9 @@ package xyz.jonesdev.sonar.api.fallback.protocol;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.*;
@Getter
@RequiredArgsConstructor
public enum ProtocolVersion {
UNKNOWN(-1),
@@ -67,12 +65,13 @@ public enum ProtocolVersion {
MINECRAFT_1_19_3(761),
MINECRAFT_1_19_4(762),
MINECRAFT_1_20(763),
MINECRAFT_1_20_2(764);
MINECRAFT_1_20_2(764),
MINECRAFT_1_20_3(765);
@Getter
private final int protocol;
public static final ProtocolVersion MINIMUM_VERSION = MINECRAFT_1_7_2;
public static final ProtocolVersion LATEST_VERSION;
public static final Map<Integer, ProtocolVersion> ID_TO_PROTOCOL_CONSTANT;
public static final Set<ProtocolVersion> SUPPORTED_VERSIONS;
@@ -96,7 +95,8 @@ public enum ProtocolVersion {
}
}
SUPPORTED_VERSIONS = versions;
SUPPORTED_VERSIONS = Collections.unmodifiableSet(versions);
LATEST_VERSION = (ProtocolVersion) versions.toArray()[versions.size() - 1];
}
}
@@ -104,10 +104,15 @@ public enum ProtocolVersion {
return ID_TO_PROTOCOL_CONSTANT.getOrDefault(protocol, UNKNOWN);
}
public boolean inBetween(final ProtocolVersion first, final ProtocolVersion last) {
return compareTo(first) >= 0 && compareTo(last) <= 0;
}
public boolean isUnknown() {
return this == UNKNOWN;
}
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
public boolean isLegacy() {
return this == LEGACY;
}
@@ -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);
}
}
}
@@ -24,12 +24,13 @@ import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
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)
@@ -51,12 +52,15 @@ public final class VerifiedPlayer {
)
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) {
this.inetAddress = inetAddress;
this.playerUUID = playerUUID;
this.timestamp = timestamp;
this.timestamp = new Timestamp(timestamp);
}
}
@@ -26,11 +26,16 @@ public interface JVMProfiler {
Runtime RUNTIME = Runtime.getRuntime();
OperatingSystemMXBean OPERATING_SYSTEM_MX_BEAN = (OperatingSystemMXBean) ManagementFactory.getOperatingSystemMXBean();
RuntimeMXBean RUNTIME_MX_BEAN = ManagementFactory.getRuntimeMXBean();
char[] MEMORY_UNITS = {'k', 'M', 'G', 'T', 'P', 'E'};
default String formatMemory(final long 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 String formatMemory(final long size) {
if (size < 1024L) {
return size + " B";
}
final int group = (63 - Long.numberOfLeadingZeros(size)) / 10;
final double formattedSize = (double) size / (1L << (group * 10));
final char unit = MEMORY_UNITS[group - 1];
return String.format("%.1f %sB", formattedSize, unit);
}
default int getVirtualCores() {
@@ -18,9 +18,14 @@
package xyz.jonesdev.sonar.api.timer;
import lombok.Getter;
import org.jetbrains.annotations.NotNull;
import java.text.SimpleDateFormat;
@Getter
public final class SystemTimer {
private static final SimpleDateFormat FORMATTER = new SimpleDateFormat("mm:ss");
private long start = System.currentTimeMillis();
public void reset() {
@@ -35,6 +40,10 @@ public final class SystemTimer {
return delay() >= amount;
}
public @NotNull String formattedDelay() {
return FORMATTER.format(delay());
}
@Override
public String toString() {
return String.format("%.3f", delay() / 1000D);
@@ -19,59 +19,71 @@ package xyz.jonesdev.sonar.api.verbose;
import lombok.Getter;
import lombok.val;
import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.cappuccino.Cappuccino;
import xyz.jonesdev.cappuccino.ExpiringCache;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.attack.AttackTracker;
import xyz.jonesdev.sonar.api.profiler.JVMProfiler;
import xyz.jonesdev.sonar.api.statistics.Statistics;
import xyz.jonesdev.sonar.api.timer.SystemTimer;
import java.util.Collection;
import java.util.Map;
import java.util.Vector;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import static xyz.jonesdev.sonar.api.Sonar.DECIMAL_FORMAT;
import static xyz.jonesdev.sonar.api.fallback.traffic.TrafficCounter.INCOMING;
import static xyz.jonesdev.sonar.api.fallback.traffic.TrafficCounter.OUTGOING;
@Getter
public abstract class Verbose implements JVMProfiler {
protected final @NotNull Collection<String> subscribers = new Vector<>(0);
private final SystemTimer secondTimer = new SystemTimer();
protected int joinsPerSecond, totalJoins;
private int lastTotalJoins, animationIndex;
public final class Verbose implements JVMProfiler {
private final @NotNull Collection<String> subscribers = new Vector<>(0);
private final @NotNull Map<String, Audience> audiences = new ConcurrentHashMap<>();
private int animationIndex;
private final ExpiringCache<Long> loginsPerSecond = Cappuccino.buildExpiring(1L, TimeUnit.SECONDS);
// Run action bar verbose
public final void update() {
public void update() {
// Clean up all blacklisted IPs
Sonar.get().getFallback().getBlacklisted().cleanUp(false);
loginsPerSecond.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;
}
// Don't prepare component if there are no subscribers
if (subscribers.isEmpty()) return;
// Prepare the action bar format component
final Component component = prepareActionBarFormat();
// Send the action bar to all online players
broadcast(prepareActionBarFormat());
for (final String subscriber : subscribers) {
final Audience audience = audiences.get(subscriber);
if (audience == null) continue;
audience.sendActionBar(component);
}
}
protected @NotNull Component prepareActionBarFormat() {
return MiniMessage.miniMessage().deserialize(Sonar.get().getConfig().getVerbose().getActionBarLayout()
public @NotNull Component prepareActionBarFormat() {
final AttackTracker.AttackStatistics attackStatistics = Sonar.get().getAttackTracker().getCurrentAttack();
final SystemTimer attackDuration = attackStatistics == null ? null : attackStatistics.getDuration();
return MiniMessage.miniMessage().deserialize((attackDuration != null
? Sonar.get().getConfig().getVerbose().getActionBarLayoutDuringAttack()
: Sonar.get().getConfig().getVerbose().getActionBarLayout())
.replace("%queued%",
DECIMAL_FORMAT.format(Sonar.get().getFallback().getQueue().getQueuedPlayers().size()))
.replace("%verifying%", DECIMAL_FORMAT.format(Sonar.get().getFallback().getConnected().size()))
.replace("%blacklisted%",
DECIMAL_FORMAT.format(Sonar.get().getFallback().getBlacklisted().estimatedSize()))
.replace("%total-joins%", DECIMAL_FORMAT.format(totalJoins))
.replace("%per-second-joins%", DECIMAL_FORMAT.format(joinsPerSecond))
.replace("%total-joins%", DECIMAL_FORMAT.format(Statistics.TOTAL_TRAFFIC.get()))
.replace("%logins-per-second%", DECIMAL_FORMAT.format(loginsPerSecond.estimatedSize()))
.replace("%verify-total%", DECIMAL_FORMAT.format(Statistics.REAL_TRAFFIC.get()))
.replace("%verify-success%",
DECIMAL_FORMAT.format(Sonar.get().getVerifiedPlayerController().estimatedSize()))
.replace("%verify-failed%", DECIMAL_FORMAT.format(Statistics.FAILED_VERIFICATIONS.get()))
.replace("%attack-duration%", attackDuration == null ? "00:00" : attackDuration.formattedDelay())
.replace("%incoming-traffic%", INCOMING.getCachedSecond())
.replace("%outgoing-traffic%", OUTGOING.getCachedSecond())
.replace("%incoming-traffic-ttl%", INCOMING.getCachedTtl())
@@ -83,34 +95,31 @@ public abstract class Verbose implements JVMProfiler {
.replace("%animation%", nextAnimation()));
}
protected final String nextAnimation() {
public String nextAnimation() {
val animations = Sonar.get().getConfig().getVerbose().getAnimation();
final int nextIndex = ++animationIndex % animations.size();
return animations.toArray(new String[0])[nextIndex];
}
// Run action bar verbose
protected abstract void broadcast(final Component component);
/**
* @param subscriber Name of the player who subscribed
* @return Whether the player is subscribed or not
* @param name Name of the audience
* @return Whether the audience is subscribed or not
*/
public final boolean isSubscribed(final @NotNull String subscriber) {
return subscribers.contains(subscriber);
public boolean isSubscribed(final @NotNull String name) {
return subscribers.contains(name);
}
/**
* @param subscriber Name of the player to subscribe
* @param name Name of the audience to subscribe
*/
public final void subscribe(final @NotNull String subscriber) {
subscribers.add(subscriber);
public void subscribe(final String name) {
subscribers.add(name);
}
/**
* @param subscriber Name of the player to unsubscribe
* @param name Name of the audience to unsubscribe
*/
public final void unsubscribe(final @NotNull String subscriber) {
subscribers.remove(subscriber);
public void unsubscribe(final String name) {
subscribers.remove(name);
}
}
@@ -0,0 +1,123 @@
/*
* Copyright (C) 2023 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.api.webhook;
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;
@RequiredArgsConstructor
public final class DiscordWebhook {
private final String url;
private static final ExecutorService HTTP_REQUEST_SERVICE = Executors.newSingleThreadExecutor();
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));
}
// Check if the connection was a success
if (urlConnection.getResponseCode() != 204) {
throw new IllegalStateException("Unexpected response code " + urlConnection.getResponseCode());
}
} catch (Exception exception) {
Sonar.get().getLogger().error("Could not send webhook: {}", exception);
}
});
}
private 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 Sonar.GENERAL_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;
}
}
}
+2 -3
View File
@@ -7,17 +7,16 @@ plugins {
apply(plugin = "net.minecrell.plugin-yml.bukkit")
bukkit {
name = rootProject.name
name = "Sonar"
version = rootProject.version.toString().split("-")[0]
main = "xyz.jonesdev.sonar.bukkit.SonarBukkitPlugin"
authors = listOf("Jones Development", "Sonar Contributors")
website = "https://jonesdev.xyz/discord/"
description = "Effective Anti-bot plugin for Velocity, BungeeCord, and Bukkit (1.7-latest)"
load = BukkitPluginDescription.PluginLoadOrder.POSTWORLD
commands {
register("sonar") {
// No permission check here
// No permission checks here
}
}
}
@@ -18,31 +18,31 @@
package xyz.jonesdev.sonar.bukkit;
import lombok.Getter;
import net.kyori.adventure.platform.bukkit.BukkitAudiences;
import org.bstats.bukkit.Metrics;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.SonarPlatform;
import xyz.jonesdev.sonar.api.command.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.audience.AudienceListener;
import xyz.jonesdev.sonar.bukkit.command.BukkitSonarCommand;
import xyz.jonesdev.sonar.bukkit.verbose.ActionBarVerbose;
import xyz.jonesdev.sonar.common.boot.SonarBootstrap;
import java.util.Objects;
import java.util.Optional;
import java.util.logging.Level;
@Getter
public final class SonarBukkit extends SonarBootstrap<SonarBukkitPlugin> {
public static SonarBukkit INSTANCE;
public SonarBukkit(final @NotNull SonarBukkitPlugin plugin) {
super(plugin, plugin.getDataFolder(), new ActionBarVerbose(plugin.getServer()));
super(plugin, plugin.getDataFolder(), SonarPlatform.BUKKIT);
INSTANCE = this;
}
/**
* Wrapper for Bukkit audiences
*/
private final BukkitAudiences bukkitAudiences = BukkitAudiences.create(getPlugin());
/**
* Create a wrapper for the plugin logger, so we can use it outside
* the velocity module.
@@ -53,34 +53,17 @@ public final class SonarBukkit extends SonarBootstrap<SonarBukkitPlugin> {
@Override
public void info(final String message, final Object... args) {
getPlugin().getLogger().log(Level.INFO, message, args);
getPlugin().getLogger().info(buildFullMessage(message, args));
}
@Override
public void warn(final String message, final Object... args) {
getPlugin().getLogger().log(Level.WARNING, message, args);
getPlugin().getLogger().warning(buildFullMessage(message, args));
}
@Override
public void error(final String message, final Object... args) {
getPlugin().getLogger().log(Level.SEVERE, message, args);
}
};
/**
* Create a wrapper object for our server, so we can use it outside
* the velocity module.
* <br>
* We have to do this, so we can access all necessary API functions.
*/
public final ServerWrapper server = new ServerWrapper(SonarPlatform.BUKKIT) {
@Override
public Optional<InvocationSource> getOnlinePlayer(final String username) {
return getPlugin().getServer().getOnlinePlayers().stream()
.filter(player -> player.getName().equalsIgnoreCase(username))
.findFirst()
.map(BukkitInvocationSource::new);
getPlugin().getLogger().severe(buildFullMessage(message, args));
}
};
@@ -88,21 +71,12 @@ public final class SonarBukkit extends SonarBootstrap<SonarBukkitPlugin> {
public void enable() {
// Initialize bStats.org metrics
new Metrics(getPlugin(), getServer().getPlatform().getMetricsId());
new Metrics(getPlugin(), getPlatform().getMetricsId());
// Register Sonar command
Objects.requireNonNull(getPlugin().getCommand("sonar")).setExecutor(new BukkitSonarCommand());
// Register Fallback queue task
getPlugin().getServer().getScheduler().runTaskTimerAsynchronously(getPlugin(), getFallback().getQueue()::poll,
10L, 10L);
// 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);
// Register audience register listener
getPlugin().getServer().getPluginManager().registerEvents(new AudienceListener(), getPlugin());
}
}
@@ -0,0 +1,42 @@
/*
* Copyright (C) 2023 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.bukkit.audience;
import net.kyori.adventure.audience.Audience;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.bukkit.SonarBukkit;
public final class AudienceListener implements Listener {
@EventHandler(priority = EventPriority.LOWEST)
public void handle(final @NotNull PlayerJoinEvent event) {
final Audience audience = SonarBukkit.INSTANCE.getBukkitAudiences().player(event.getPlayer());
Sonar.get().getVerboseHandler().getAudiences().put(event.getPlayer().getName(), audience);
}
@EventHandler(priority = EventPriority.LOWEST)
public void handle(final @NotNull PlayerQuitEvent event) {
Sonar.get().getVerboseHandler().getAudiences().remove(event.getPlayer().getName());
}
}
@@ -17,16 +17,17 @@
package xyz.jonesdev.sonar.bukkit.command;
import net.kyori.adventure.platform.bukkit.BukkitAudiences;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
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));
super(sender.getName(),
SonarBukkit.INSTANCE.getBukkitAudiences().sender(sender),
sender instanceof Player,
sender::hasPermission);
}
}
@@ -17,28 +17,19 @@
package xyz.jonesdev.sonar.bukkit.command;
import net.kyori.adventure.text.Component;
import org.bukkit.command.*;
import org.bukkit.entity.Player;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabExecutor;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.command.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,
@@ -47,97 +38,13 @@ public final class BukkitSonarCommand implements CommandExecutor, TabExecutor, S
final String[] args) {
// Create our own invocation source wrapper to handle messages properly
final InvocationSource invocationSource = new BukkitInvocationSource(sender);
if (!(sender instanceof ConsoleCommandSender)) {
// Check if the player actually has the permission to run the command
if (!sender.hasPermission("sonar.command")) {
invocationSource.sendMessage(Sonar.get().getConfig().getNoPermission());
return false;
}
// Checking if it contains will only break more since it can throw
// a NullPointerException if the cache is being accessed from parallel threads
DELAY.cleanUp(); // Clean up the cache
final long mapTimestamp = DELAY.asMap().getOrDefault(sender, -1L);
// There were some exploits with spamming commands in the past.
// Spamming should be prevented, especially if some heavy operations are done,
// which is not the case here but let's still stay safe!
if (mapTimestamp > 0L) {
invocationSource.sendMessage(Sonar.get().getConfig().getCommands().getCommandCoolDown());
// Format delay
final long timestamp = System.currentTimeMillis();
final double left = 0.5D - (timestamp - mapTimestamp) / 1000D;
invocationSource.sendMessage(Sonar.get().getConfig().getCommands().getCommandCoolDownLeft()
.replace("%time-left%", Sonar.DECIMAL_FORMAT.format(left)));
return false;
}
DELAY.put(sender);
}
Optional<Subcommand> subcommand = Optional.empty();
if (args.length > 0) {
// Search subcommand if command arguments are present
subcommand = Sonar.get().getSubcommandRegistry().getSubcommands().parallelStream()
.filter(sub -> sub.getInfo().name().equalsIgnoreCase(args[0])
|| Arrays.stream(sub.getInfo().aliases())
.anyMatch(alias -> alias.equalsIgnoreCase(args[0])))
.findFirst();
// Check permissions for subcommands
if (subcommand.isPresent()) {
if (!subcommand.get().getInfo().onlyConsole()
&& !sender.hasPermission(subcommand.get().getPermission())
) {
invocationSource.sendMessage(Sonar.get().getConfig().getCommands().getSubCommandNoPerm()
.replace("%permission%", subcommand.get().getPermission()));
return false;
}
}
}
subcommand.ifPresent(sub -> {
if (sub.getInfo().onlyPlayers() && !(sender instanceof Player)) {
invocationSource.sendMessage(Sonar.get().getConfig().getCommands().getPlayersOnly());
return;
}
if (sub.getInfo().onlyConsole() && !(sender instanceof ConsoleCommandSender)) {
invocationSource.sendMessage(Sonar.get().getConfig().getCommands().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().getCommands().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;
// 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) {
@@ -145,33 +52,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);
}
}
+3 -9
View File
@@ -5,24 +5,18 @@ plugins {
apply(plugin = "net.minecrell.plugin-yml.bungee")
bungee {
name = rootProject.name
name = "Sonar"
version = rootProject.version.toString().split("-")[0]
main = "xyz.jonesdev.sonar.bungee.SonarBungeePlugin"
author = "Jones Development, Sonar Contributors"
description = "Effective Anti-bot plugin for Velocity, BungeeCord, and Bukkit (1.7-latest)"
}
dependencies {
compileOnly(project(":api"))
compileOnly(project(":common"))
implementation("com.velocitypowered:velocity-native:1.1.9") {
exclude(group = "com.google.guava")
exclude(group = "io.netty")
exclude(group = "org.checkerframework")
}
compileOnly("net.md_5:bungeecord:1.20.2-rc2-SNAPSHOT")
compileOnly("net.md_5:bungeecord-proxy:master-SNAPSHOT")
testCompileOnly("net.md_5:bungeecord-proxy:master-SNAPSHOT")
// MiniMessage platform support
implementation("net.kyori:adventure-platform-bungeecord:4.3.1")
@@ -18,34 +18,32 @@
package xyz.jonesdev.sonar.bungee;
import lombok.Getter;
import net.kyori.adventure.platform.bungeecord.BungeeAudiences;
import org.bstats.bungeecord.Metrics;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.SonarPlatform;
import xyz.jonesdev.sonar.api.command.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.audience.AudienceListener;
import xyz.jonesdev.sonar.bungee.command.BungeeSonarCommand;
import xyz.jonesdev.sonar.bungee.fallback.FallbackListener;
import xyz.jonesdev.sonar.bungee.fallback.injection.BaseInjectionHelper;
import xyz.jonesdev.sonar.bungee.fallback.injection.ChildChannelInitializer;
import xyz.jonesdev.sonar.bungee.verbose.VerboseWrapper;
import xyz.jonesdev.sonar.common.boot.SonarBootstrap;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
@Getter
public final class SonarBungee extends SonarBootstrap<SonarBungeePlugin> {
public static SonarBungee INSTANCE;
public SonarBungee(final @NotNull SonarBungeePlugin plugin) {
super(plugin, plugin.getDataFolder(), new VerboseWrapper(plugin.getServer()));
super(plugin, plugin.getDataFolder(), SonarPlatform.BUNGEE);
INSTANCE = this;
}
/**
* Wrapper for BungeeCord audiences
*/
private final BungeeAudiences bungeeAudiences = BungeeAudiences.create(getPlugin());
/**
* Create a wrapper for the plugin logger, so we can use it outside
* the velocity module.
@@ -56,34 +54,17 @@ public final class SonarBungee extends SonarBootstrap<SonarBungeePlugin> {
@Override
public void info(final String message, final Object... args) {
getPlugin().getLogger().log(Level.INFO, message, args);
getPlugin().getLogger().info(buildFullMessage(message, args));
}
@Override
public void warn(final String message, final Object... args) {
getPlugin().getLogger().log(Level.WARNING, message, args);
getPlugin().getLogger().warning(buildFullMessage(message, args));
}
@Override
public void error(final String message, final Object... args) {
getPlugin().getLogger().log(Level.SEVERE, message, args);
}
};
/**
* Create a wrapper object for our server, so we can use it outside
* the velocity module.
* <br>
* We have to do this, so we can access all necessary API functions.
*/
public final ServerWrapper server = new ServerWrapper(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);
getPlugin().getLogger().severe(buildFullMessage(message, args));
}
};
@@ -91,25 +72,16 @@ public final class SonarBungee extends SonarBootstrap<SonarBungeePlugin> {
public void enable() {
// Initialize bStats.org metrics
new Metrics(getPlugin(), getServer().getPlatform().getMetricsId());
new Metrics(getPlugin(), getPlatform().getMetricsId());
// Register Sonar command
getPlugin().getServer().getPluginManager().registerCommand(getPlugin(), new BungeeSonarCommand());
// Register Fallback listener
getPlugin().getServer().getPluginManager().registerListener(getPlugin(), new FallbackListener(getFallback()));
getPlugin().getServer().getPluginManager().registerListener(getPlugin(), new FallbackListener());
// Register 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);
// Register audience register listener
getPlugin().getServer().getPluginManager().registerListener(getPlugin(), new AudienceListener());
// Inject base into ProtocolUtils
BaseInjectionHelper.inject(ChildChannelInitializer.INSTANCE);
@@ -0,0 +1,42 @@
/*
* Copyright (C) 2023 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.bungee.audience;
import net.kyori.adventure.audience.Audience;
import net.md_5.bungee.api.event.PlayerDisconnectEvent;
import net.md_5.bungee.api.event.PostLoginEvent;
import net.md_5.bungee.api.plugin.Listener;
import net.md_5.bungee.event.EventHandler;
import net.md_5.bungee.event.EventPriority;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.bungee.SonarBungee;
public final class AudienceListener implements Listener {
@EventHandler(priority = EventPriority.LOWEST)
public void handle(final @NotNull PostLoginEvent event) {
final Audience audience = SonarBungee.INSTANCE.getBungeeAudiences().player(event.getPlayer());
Sonar.get().getVerboseHandler().getAudiences().put(event.getPlayer().getName(), audience);
}
@EventHandler(priority = EventPriority.LOWEST)
public void handle(final @NotNull PlayerDisconnectEvent event) {
Sonar.get().getVerboseHandler().getAudiences().remove(event.getPlayer().getName());
}
}
@@ -17,16 +17,17 @@
package xyz.jonesdev.sonar.bungee.command;
import net.kyori.adventure.platform.bungeecord.BungeeAudiences;
import net.md_5.bungee.api.CommandSender;
import net.md_5.bungee.api.connection.ProxiedPlayer;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.command.InvocationSource;
import xyz.jonesdev.sonar.bungee.SonarBungee;
public final class BungeeInvocationSource extends InvocationSource {
private static final BungeeAudiences AUDIENCES = BungeeAudiences.create(SonarBungee.INSTANCE.getPlugin());
public BungeeInvocationSource(final @NotNull CommandSender sender) {
super(sender.getName(), AUDIENCES.sender(sender));
super(sender.getName(),
SonarBungee.INSTANCE.getBungeeAudiences().sender(sender),
sender instanceof ProxiedPlayer,
sender::hasPermission);
}
}
@@ -17,31 +17,18 @@
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.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
@@ -49,127 +36,16 @@ public final class BungeeSonarCommand extends Command implements TabExecutor, So
public void execute(final @NotNull CommandSender sender, final String[] args) {
// Create our own invocation source wrapper to handle messages properly
final InvocationSource invocationSource = new BungeeInvocationSource(sender);
if (!(sender instanceof ConsoleCommandSender)) {
// Check if the player actually has the permission to run the command
if (!sender.hasPermission("sonar.command")) {
invocationSource.sendMessage(Sonar.get().getConfig().getNoPermission());
return;
}
// Checking if it contains will only break more since it can throw
// a NullPointerException if the cache is being accessed from parallel threads
DELAY.cleanUp(); // Clean up the cache
final long mapTimestamp = DELAY.asMap().getOrDefault(sender, -1L);
// There were some exploits with spamming commands in the past.
// Spamming should be prevented, especially if some heavy operations are done,
// which is not the case here but let's still stay safe!
if (mapTimestamp > 0L) {
invocationSource.sendMessage(Sonar.get().getConfig().getCommands().getCommandCoolDown());
// Format delay
final long timestamp = System.currentTimeMillis();
final double left = 0.5D - (timestamp - mapTimestamp) / 1000D;
invocationSource.sendMessage(Sonar.get().getConfig().getCommands().getCommandCoolDownLeft()
.replace("%time-left%", Sonar.DECIMAL_FORMAT.format(left)));
return;
}
DELAY.put(sender);
}
Optional<Subcommand> subcommand = Optional.empty();
if (args.length > 0) {
// Search subcommand if command arguments are present
subcommand = Sonar.get().getSubcommandRegistry().getSubcommands().parallelStream()
.filter(sub -> sub.getInfo().name().equalsIgnoreCase(args[0])
|| Arrays.stream(sub.getInfo().aliases())
.anyMatch(alias -> alias.equalsIgnoreCase(args[0])))
.findFirst();
// Check permissions for subcommands
if (subcommand.isPresent()) {
if (!subcommand.get().getInfo().onlyConsole()
&& !sender.hasPermission(subcommand.get().getPermission())
) {
invocationSource.sendMessage(Sonar.get().getConfig().getCommands().getSubCommandNoPerm()
.replace("%permission%", subcommand.get().getPermission()));
return;
}
}
}
subcommand.ifPresent(sub -> {
if (sub.getInfo().onlyPlayers() && !(sender instanceof ProxiedPlayer)) {
invocationSource.sendMessage(Sonar.get().getConfig().getCommands().getPlayersOnly());
return;
}
if (sub.getInfo().onlyConsole() && !(sender instanceof ConsoleCommandSender)) {
invocationSource.sendMessage(Sonar.get().getConfig().getCommands().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().getCommands().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);
}
}
// 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,87 @@
/*
* Copyright (C) 2023 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.bungee.fallback;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.haproxy.HAProxyMessage;
import net.md_5.bungee.api.ProxyServer;
import net.md_5.bungee.connection.DownstreamBridge;
import net.md_5.bungee.connection.UpstreamBridge;
import net.md_5.bungee.netty.ChannelWrapper;
import net.md_5.bungee.netty.HandlerBoss;
import net.md_5.bungee.netty.PacketHandler;
import net.md_5.bungee.protocol.PacketWrapper;
import org.jetbrains.annotations.NotNull;
public final class FallbackHandlerBoss extends HandlerBoss {
private ChannelWrapper channel;
private PacketHandler handler;
@Override
public void setHandler(final @NotNull PacketHandler handler) {
this.handler = handler;
super.setHandler(handler);
}
@Override
public void channelActive(final ChannelHandlerContext ctx) throws Exception {
if (handler != null) {
channel = new ChannelWrapper(ctx);
super.channelActive(ctx);
}
}
@Override
public void channelInactive(final ChannelHandlerContext ctx) throws Exception {
if (handler != null) {
channel.markClosed();
super.channelInactive(ctx);
}
}
@Override
public void channelWritabilityChanged(final ChannelHandlerContext ctx) throws Exception {
if (handler != null) {
handler.writabilityChanged(channel);
super.channelWritabilityChanged(ctx);
}
}
@Override
public void channelRead(final ChannelHandlerContext ctx, final Object msg) throws Exception {
if (msg instanceof HAProxyMessage || msg instanceof PacketWrapper) {
super.channelRead(ctx, msg);
}
}
@Override
public void exceptionCaught(final @NotNull ChannelHandlerContext ctx, final Throwable throwable) throws Exception {
if (ctx.channel().isActive()) {
if (handler instanceof DownstreamBridge || handler instanceof UpstreamBridge) {
try {
handler.exception(throwable);
} catch (Exception exception) {
ProxyServer.getInstance().getLogger().severe(handler + " - exception processing exception: " + exception);
}
}
channel.markClosed();
ctx.close();
}
}
}
@@ -0,0 +1,345 @@
/*
* Copyright (C) 2023 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.bungee.fallback;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelPipeline;
import io.netty.handler.codec.CorruptedFrameException;
import io.netty.handler.codec.DecoderException;
import lombok.Getter;
import lombok.val;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.json.JSONComponentSerializer;
import net.md_5.bungee.BungeeCord;
import net.md_5.bungee.EncryptionUtil;
import net.md_5.bungee.api.config.ListenerInfo;
import net.md_5.bungee.chat.ComponentSerializer;
import net.md_5.bungee.connection.InitialHandler;
import net.md_5.bungee.netty.ChannelWrapper;
import net.md_5.bungee.protocol.PlayerPublicKey;
import net.md_5.bungee.protocol.packet.Kick;
import net.md_5.bungee.protocol.packet.LoginRequest;
import net.md_5.bungee.protocol.packet.StatusRequest;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import xyz.jonesdev.sonar.api.ReflectiveOperationException;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.event.impl.UserVerifyJoinEvent;
import xyz.jonesdev.sonar.api.fallback.Fallback;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.api.statistics.Statistics;
import xyz.jonesdev.sonar.common.fallback.FallbackChannelHandler;
import xyz.jonesdev.sonar.common.fallback.FallbackTimeoutHandler;
import xyz.jonesdev.sonar.common.fallback.FallbackVerificationHandler;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketDecoder;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketEncoder;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.login.LoginSuccess;
import xyz.jonesdev.sonar.common.fallback.traffic.TrafficChannelHooker;
import java.net.InetAddress;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import static net.md_5.bungee.netty.PipelineUtils.*;
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.*;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_19_1;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_19_3;
import static xyz.jonesdev.sonar.common.utility.geyser.GeyserUtil.isGeyserConnection;
public final class FallbackInitialHandler extends InitialHandler {
public FallbackInitialHandler(final @NotNull BungeeCord bungee, final ListenerInfo listener) {
super(bungee, listener);
this.bungee = bungee;
}
private static final @NotNull Fallback FALLBACK = Objects.requireNonNull(Sonar.get().getFallback());
@Getter
private ChannelWrapper channelWrapper;
private final @NotNull BungeeCord bungee;
private @Nullable FallbackUserWrapper user;
@Getter
private ProtocolVersion protocolVersion;
private boolean receivedLoginPacket;
private boolean receivedStatusPacket;
@Override
public void connected(final ChannelWrapper channelWrapper) throws Exception {
this.channelWrapper = channelWrapper;
super.connected(channelWrapper);
}
@Override
public void handle(final StatusRequest statusRequest) throws Exception {
// Fix status packet spam exploit
if (receivedStatusPacket) {
throw new CorruptedFrameException("Duplicate status packet");
}
receivedStatusPacket = true;
// Run the rest of the method asynchronously
CompletableFuture.runAsync(() -> {
// The channel always stays open because the client sends
// a StatusRequest and a Ping packet after one another
if (!isConnected()) {
throw new CorruptedFrameException("Not connected anymore");
}
try {
super.handle(statusRequest);
} catch (Exception exception) {
throw new DecoderException(exception);
}
});
}
@Override
public void handle(final LoginRequest loginRequest) throws Exception {
// Increase joins per second for the action bar verbose
Sonar.get().getVerboseHandler().getLoginsPerSecond().put(System.nanoTime());
// Fix login packet spam exploit
if (receivedLoginPacket || user != null) {
throw new CorruptedFrameException("Duplicate login packet");
}
receivedLoginPacket = true;
// Cache protocol version so other handlers don't throw NPEs
final int protocolId = getHandshake().getProtocolVersion();
protocolVersion = ProtocolVersion.fromId(protocolId);
final Channel channel = channelWrapper.getHandle();
// Run in the channel's event loop
channel.eventLoop().execute(() -> {
try {
final ChannelPipeline pipeline = channel.pipeline();
TrafficChannelHooker.hook(pipeline, PACKET_DECODER, PACKET_ENCODER);
// Increase total traffic statistic
Statistics.TOTAL_TRAFFIC.increment();
final InetAddress inetAddress = getAddress().getAddress();
// Check the blacklist here since we cannot let the player "ghost join"
if (FALLBACK.getBlacklisted().has(inetAddress)) {
closeWith(getKickPacket(Sonar.get().getConfig().getVerification().getBlacklisted()));
return;
}
// Check if the verification is enabled
if (!Sonar.get().getFallback().shouldVerifyNewPlayers()) {
super.handle(loginRequest);
return;
}
// Completely skip Geyser connections
if (isGeyserConnection(channel)) {
FALLBACK.getLogger().info("Skipping Geyser player: {}{}",
loginRequest.getData(), Sonar.get().getConfig().formatAddress(inetAddress));
super.handle(loginRequest);
return;
}
// Check if the protocol ID of the player is not allowed to enter the server
if (Sonar.get().getConfig().getVerification().getBlacklistedProtocols().contains(protocolId)) {
closeWith(getKickPacket(Sonar.get().getConfig().getVerification().getProtocolBlacklisted()));
return;
}
val uuid = UUID.nameUUIDFromBytes(("OfflinePlayer:" + loginRequest.getData()).getBytes(StandardCharsets.UTF_8));
// Check if the player is already verified
if (Sonar.get().getVerifiedPlayerController().has(inetAddress, uuid)) {
super.handle(loginRequest);
return;
}
// Check if the protocol ID of the player is allowed to bypass verification
if (Sonar.get().getConfig().getVerification().getWhitelistedProtocols().contains(protocolId)) {
super.handle(loginRequest);
return;
}
// Create wrapped Fallback user
user = new FallbackUserWrapper(
FALLBACK, channelWrapper, this,
channel, channel.pipeline(), inetAddress, protocolVersion
);
// Perform default BungeeCord checks
if (bungee.config.isEnforceSecureProfile()
&& user.getProtocolVersion().compareTo(MINECRAFT_1_19_3) < 0) {
final PlayerPublicKey publicKey = loginRequest.getPublicKey();
if (publicKey == null) {
disconnect(bungee.getTranslation("secure_profile_required"));
return;
}
if (Instant.ofEpochMilli(publicKey.getExpiry()).isBefore(Instant.now())) {
disconnect(bungee.getTranslation("secure_profile_expired"));
return;
}
if (user.getProtocolVersion().compareTo(MINECRAFT_1_19_1) < 0
&& !EncryptionUtil.check(publicKey, null)) {
disconnect(bungee.getTranslation("secure_profile_invalid"));
return;
}
}
// Check if the player is already queued since we don't want bots to flood the queue
if (FALLBACK.getQueue().getQueuedPlayers().containsKey(inetAddress)) {
closeWith(getKickPacket(Sonar.get().getConfig().getVerification().getAlreadyQueued()));
return;
}
// Check if Fallback is already verifying a player
// → is another player with the same IP address connected to Fallback?
if (FALLBACK.getConnected().containsKey(loginRequest.getData())
|| FALLBACK.getConnected().containsValue(inetAddress)) {
closeWith(getKickPacket(Sonar.get().getConfig().getVerification().getAlreadyVerifying()));
return;
}
// Check if the IP address is currently being rate-limited
if (!FALLBACK.getRatelimiter().attempt(inetAddress)) {
closeWith(getKickPacket(Sonar.get().getConfig().getVerification().getTooFastReconnect()));
return;
}
// We have to add this pipeline to monitor whenever the client disconnects
// to remove them from the list of connected and queued players
pipeline.addFirst(FALLBACK_HANDLER, new FallbackChannelHandler(loginRequest.getData()));
// Queue the connection for further processing
FALLBACK.getQueue().queue(inetAddress, () -> channel.eventLoop().execute(() -> {
// Do not continue if the connection is closed or marked as disconnected
if (channelWrapper.isClosed() || channelWrapper.isClosing()) return;
// Check if the username matches the valid name regex to prevent
// UTF-16 names or other types of exploits
if (!Sonar.get().getConfig().getVerification().getValidNameRegex().matcher(loginRequest.getData()).matches()) {
closeWith(getKickPacket(Sonar.get().getConfig().getVerification().getInvalidUsername()));
return;
}
// Add better timeout handler to avoid known exploits or issues
// We also want to timeout bots quickly to avoid flooding
final int readTimeout = Sonar.get().getConfig().getVerification().getReadTimeout();
pipeline.replace(TIMEOUT_HANDLER, TIMEOUT_HANDLER,
new FallbackTimeoutHandler(readTimeout, TimeUnit.MILLISECONDS));
// Disconnect if the protocol version could not be resolved
if (user.getProtocolVersion().isUnknown()) {
closeWith(getKickPacket(Sonar.get().getConfig().getVerification().getInvalidProtocol()));
return;
}
// Check if the player is already connected to the proxy but still tries to verify
final int limit = bungee.config.getPlayerLimit();
if (limit > 0 && bungee.getOnlineCount() >= limit) {
disconnect(bungee.getTranslation("proxy_full"));
return;
} else if (!isOnlineMode() && bungee.getPlayer(loginRequest.getData()) != null) {
closeWith(getKickPacket(Sonar.get().getConfig().getVerification().getAlreadyConnected()));
return;
}
// The player joined the verification
Statistics.REAL_TRAFFIC.increment();
if (Sonar.get().getConfig().getVerification().isLogConnections()) {
// Only log the processing message if the server isn't under attack.
// We let the user override this through the configuration.
if (!Sonar.get().getAttackTracker().isCurrentlyUnderAttack()
|| Sonar.get().getConfig().getVerification().isLogDuringAttack()) {
FALLBACK.getLogger().info(Sonar.get().getConfig().getVerification().getConnectLog()
.replace("%name%", loginRequest.getData())
.replace("%ip%", Sonar.get().getConfig().formatAddress(inetAddress))
.replace("%protocol%", String.valueOf(user.getProtocolVersion().getProtocol())));
}
}
// Call the VerifyJoinEvent for external API usage
Sonar.get().getEventManager().publish(new UserVerifyJoinEvent(loginRequest.getData(), user));
// Mark the player as connected → verifying players
FALLBACK.getConnected().put(loginRequest.getData(), inetAddress);
// This sometimes happens when the channel hangs, but the player is still connecting
// This also fixes a unique issue with TCPShield and other reverse proxies
if (user.getPipeline().get(PACKET_ENCODER) == null
|| user.getPipeline().get(PACKET_DECODER) == null) {
channelWrapper.close();
return;
}
// Replace normal encoder to allow custom packets
final FallbackPacketEncoder encoder = new FallbackPacketEncoder(user.getProtocolVersion());
user.getPipeline().replace(PACKET_ENCODER, FALLBACK_PACKET_ENCODER, encoder);
// Send LoginSuccess packet to make the client think they are joining the server
user.write(new LoginSuccess(loginRequest.getData(), uuid));
// The LoginSuccess packet has been sent, now we can change the registry state
encoder.updateRegistry(user.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_20_2) >= 0
? FallbackPacketRegistry.CONFIG : FallbackPacketRegistry.GAME);
// Replace normal decoder to allow custom packets
user.getPipeline().replace(PACKET_DECODER, FALLBACK_PACKET_DECODER,
new FallbackPacketDecoder(user, new FallbackVerificationHandler(user, loginRequest.getData(), uuid)));
}));
} catch (Throwable throwable) {
throw new ReflectiveOperationException(throwable);
}
});
}
private static final Map<Component, Kick> CACHED_KICK_PACKETS = new ConcurrentHashMap<>(10);
public static @NotNull Kick getKickPacket(final @NotNull Component component) {
Kick cachedKickPacket = CACHED_KICK_PACKETS.get(component);
if (cachedKickPacket == null) {
final String serialized = JSONComponentSerializer.json().serialize(component);
cachedKickPacket = new Kick(ComponentSerializer.deserialize(serialized));
CACHED_KICK_PACKETS.put(component, cachedKickPacket);
}
return cachedKickPacket;
}
// Mostly taken from Velocity
public void closeWith(final Object msg) {
if (channelWrapper.getHandle().isActive()) {
boolean is17 = protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_8) < 0
&& protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_7_2) >= 0;
if (is17) {
channelWrapper.getHandle().eventLoop().execute(() -> {
channelWrapper.getHandle().config().setAutoRead(false);
channelWrapper.getHandle().eventLoop().schedule(() -> {
channelWrapper.markClosed();
channelWrapper.getHandle().writeAndFlush(msg).addListener(ChannelFutureListener.CLOSE);
}, 250L, TimeUnit.MILLISECONDS);
});
} else {
channelWrapper.markClosed();
channelWrapper.getHandle().writeAndFlush(msg).addListener(ChannelFutureListener.CLOSE);
}
}
}
}
@@ -17,113 +17,42 @@
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.event.LoginEvent;
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 xyz.jonesdev.sonar.bungee.SonarBungee;
import java.lang.reflect.Field;
import static net.md_5.bungee.netty.PipelineUtils.FRAME_PREPENDER;
import java.net.InetAddress;
import java.util.Objects;
@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().getLockdown().isEnabled()) {
if (!event.getPlayer().hasPermission(Sonar.get().getConfig().getLockdown().getBypassPermission())) {
final PendingConnection pendingConnection = event.getPlayer().getPendingConnection();
// Try to close the channel with a custom serialized disconnect component
if (pendingConnection instanceof FallbackInitialHandler) {
final FallbackInitialHandler fallbackInitialHandler = (FallbackInitialHandler) pendingConnection;
final Component component = Sonar.get().getConfig().getLockdown().getDisconnect();
final String serialized = JSONComponentSerializer.json().serialize(component);
fallbackInitialHandler.closeWith(new Kick(serialized));
} else {
// Fallback by disconnecting without a message
pendingConnection.disconnect();
Sonar.get().getLogger().warn("Fallback handler of {} is missing", event.getPlayer().getName());
}
public void handle(final @NotNull LoginEvent event) {
final InetAddress inetAddress = event.getConnection().getAddress().getAddress();
if (Sonar.get().getConfig().getLockdown().isLogAttempts()) {
Sonar.get().getLogger().info(Sonar.get().getConfig().getLockdown().getConsoleLog()
.replace("%player%", event.getPlayer().getName())
.replace("%ip%", Sonar.get().getConfig()
.formatAddress(event.getPlayer().getAddress().getAddress()))
.replace("%protocol%",
String.valueOf(event.getPlayer().getPendingConnection().getVersion())));
}
return;
} else if (Sonar.get().getConfig().getLockdown().isNotifyAdmins()) {
event.getPlayer().sendMessage(new TextComponent(Sonar.get().getConfig().getLockdown().getNotification()));
// Check if the number of online players using the same IP address as
// the connecting player is greater than the configured amount
final int maxOnlinePerIp = Sonar.get().getConfig().getMaxOnlinePerIp();
if (maxOnlinePerIp > 0) {
final long onlinePerIp = SonarBungee.INSTANCE.getPlugin().getServer().getPlayers().stream()
.filter(player -> Objects.equals(player.getAddress().getAddress(), inetAddress))
.count()
+ 1 /* add 1 because the player hasn't been added to the list of online players yet */;
// We use '>=' because the player connecting to the server hasn't joined yet
if (onlinePerIp >= maxOnlinePerIp) {
final FallbackInitialHandler fallbackInitialHandler = (FallbackInitialHandler) event.getConnection();
final Component component = Sonar.get().getConfig().getTooManyOnlinePerIp();
fallbackInitialHandler.closeWith(FallbackInitialHandler.getKickPacket(component));
}
}
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)
);
}
}
}
@@ -28,7 +28,6 @@ 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.play.Disconnect;
import java.net.InetAddress;
@@ -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);
}
}
}
}
@@ -17,16 +17,15 @@
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.FallbackHandlerBoss;
import xyz.jonesdev.sonar.bungee.fallback.varint.Varint21FrameDecoder;
import xyz.jonesdev.sonar.common.fallback.FallbackTimeoutHandler;
@@ -41,15 +40,13 @@ public final class BaseChannelInitializer extends ChannelInitializer<Channel> {
private static final boolean REPLACE_VAR_INT_DECODER = !Boolean.getBoolean("sonar.do-not-replace-decoder");
private static final WriteBufferWaterMark MARK;
// https://github.com/PaperMC/Velocity/blob/dev/3.0.0/proxy/src/main/java/com/velocitypowered/proxy/network/ConnectionManager.java#L57
private static final WriteBufferWaterMark SERVER_WRITE_MARK = new WriteBufferWaterMark(1 << 20, 1 << 21);
private static final 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);
@@ -68,15 +65,13 @@ public final class BaseChannelInitializer extends ChannelInitializer<Channel> {
}
channel.config().setOption(ChannelOption.TCP_NODELAY, true);
channel.config().setAllocator(PooledByteBufAllocator.DEFAULT);
channel.config().setWriteBufferWaterMark(MARK);
channel.config().setWriteBufferWaterMark(SERVER_WRITE_MARK);
channel.pipeline().addLast(FRAME_DECODER, REPLACE_VAR_INT_DECODER
? new Varint21FrameDecoder() : new net.md_5.bungee.protocol.Varint21FrameDecoder());
channel.pipeline().addLast(TIMEOUT_HANDLER, new FallbackTimeoutHandler(
BungeeCord.getInstance().config.getTimeout(), TimeUnit.MILLISECONDS
));
BungeeCord.getInstance().config.getTimeout(), TimeUnit.MILLISECONDS));
channel.pipeline().addLast(FRAME_PREPENDER, FRAME_ENCODER);
channel.pipeline().addLast(BOSS_HANDLER, new HandlerBoss());
channel.pipeline().addLast(BOSS_HANDLER, new FallbackHandlerBoss());
}
}
@@ -23,15 +23,15 @@ import io.netty.handler.codec.haproxy.HAProxyMessageDecoder;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import net.md_5.bungee.BungeeCord;
import net.md_5.bungee.api.ProxyServer;
import net.md_5.bungee.api.config.ListenerInfo;
import net.md_5.bungee.api.event.ClientConnectEvent;
import net.md_5.bungee.netty.HandlerBoss;
import net.md_5.bungee.netty.PipelineUtils;
import net.md_5.bungee.protocol.*;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.ReflectiveOperationException;
import xyz.jonesdev.sonar.bungee.fallback.handler.FallbackInitialHandler;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.bungee.fallback.FallbackHandlerBoss;
import xyz.jonesdev.sonar.bungee.fallback.FallbackInitialHandler;
import java.lang.reflect.Field;
import java.net.SocketAddress;
@@ -43,7 +43,7 @@ public final class ChildChannelInitializer extends ChannelInitializer<Channel> {
public static final ChildChannelInitializer INSTANCE = new ChildChannelInitializer();
private static final BaseChannelInitializer BASE = BaseChannelInitializer.INSTANCE;
private static final BungeeCord BUNGEE = BungeeCord.getInstance();
private static final KickStringWriter LEGACY_KICK;
static {
@@ -63,7 +63,7 @@ public final class ChildChannelInitializer extends ChannelInitializer<Channel> {
final SocketAddress remoteAddress = channel.remoteAddress() == null ? channel.parent().localAddress()
: channel.remoteAddress();
if (BungeeCord.getInstance().getConnectionThrottle() != null && BungeeCord.getInstance().getConnectionThrottle().throttle(remoteAddress)) {
if (BUNGEE.getConnectionThrottle() != null && BUNGEE.getConnectionThrottle().throttle(remoteAddress)) {
channel.close();
} else {
final ListenerInfo listener = channel.attr(PipelineUtils.LISTENER).get();
@@ -80,13 +80,12 @@ public final class ChildChannelInitializer extends ChannelInitializer<Channel> {
}
channel.pipeline().addBefore(FRAME_DECODER, LEGACY_DECODER, new LegacyDecoder());
channel.pipeline().addAfter(FRAME_DECODER, PACKET_DECODER, new MinecraftDecoder(Protocol.HANDSHAKE, true,
ProxyServer.getInstance().getProtocolVersion()));
channel.pipeline().addAfter(FRAME_PREPENDER, PACKET_ENCODER, new MinecraftEncoder(Protocol.HANDSHAKE,
true, ProxyServer.getInstance().getProtocolVersion()));
channel.pipeline().addAfter(FRAME_DECODER, PACKET_DECODER, new MinecraftDecoder(
Protocol.HANDSHAKE, true, ProtocolVersion.LATEST_VERSION.getProtocol()));
channel.pipeline().addAfter(FRAME_PREPENDER, PACKET_ENCODER, new MinecraftEncoder(
Protocol.HANDSHAKE, true, ProtocolVersion.LATEST_VERSION.getProtocol()));
channel.pipeline().addBefore(FRAME_PREPENDER, LEGACY_KICKER, LEGACY_KICK);
channel.pipeline().get(HandlerBoss.class).setHandler(new FallbackInitialHandler(BungeeCord.getInstance(),
listener));
channel.pipeline().get(FallbackHandlerBoss.class).setHandler(new FallbackInitialHandler(BUNGEE, listener));
if (listener.isProxyProtocol()) {
channel.pipeline().addFirst(new HAProxyMessageDecoder());
@@ -21,6 +21,7 @@ import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
import io.netty.handler.codec.CorruptedFrameException;
import org.jetbrains.annotations.NotNull;
import java.util.List;
@@ -29,7 +30,7 @@ import java.util.List;
public final class Varint21FrameDecoder extends ByteToMessageDecoder {
@Override
protected void decode(final ChannelHandlerContext ctx,
protected void decode(final @NotNull ChannelHandlerContext ctx,
final ByteBuf byteBuf,
final List<Object> out) throws Exception {
if (!ctx.channel().isActive() || !byteBuf.isReadable()) {
@@ -1,41 +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.kyori.adventure.text.Component;
import net.md_5.bungee.api.ProxyServer;
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;
@Override
public void broadcast(final Component component) {
synchronized (subscribers) {
for (final String subscriber : subscribers) {
final ProxiedPlayer player = server.getPlayer(subscriber);
if (player != null) {
// TODO: use an adventure audience
}
}
}
}
}
@@ -24,6 +24,7 @@ import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.cappuccino.Cappuccino;
import xyz.jonesdev.cappuccino.ExpiringCache;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.SonarPlatform;
import xyz.jonesdev.sonar.api.SonarSupplier;
import xyz.jonesdev.sonar.api.command.subcommand.SubcommandRegistry;
import xyz.jonesdev.sonar.api.config.SonarConfiguration;
@@ -32,6 +33,7 @@ import xyz.jonesdev.sonar.api.fallback.FallbackRatelimiter;
import xyz.jonesdev.sonar.api.timer.SystemTimer;
import xyz.jonesdev.sonar.api.verbose.Verbose;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer;
import xyz.jonesdev.sonar.common.service.ServiceRepository;
import xyz.jonesdev.sonar.common.subcommand.SubcommandRegistryHolder;
import xyz.jonesdev.sonar.common.update.UpdateChecker;
@@ -48,19 +50,21 @@ public abstract class SonarBootstrap<T> implements Sonar {
private SonarConfiguration config;
private VerifiedPlayerController verifiedPlayerController;
private File dataDirectory;
private final SonarPlatform platform;
private final SubcommandRegistry subcommandRegistry;
private final SystemTimer launchTimer = new SystemTimer();
public SonarBootstrap(final @NotNull T plugin,
final File dataDirectory,
final Verbose verboseHandler) {
// Set the API to this instance so the config doesn't have issues
final SonarPlatform platform) {
// Set the Sonar API
SonarSupplier.set(this);
// Set the plugin instance before anything else
this.plugin = plugin;
this.dataDirectory = dataDirectory;
this.verboseHandler = verboseHandler;
this.platform = platform;
this.verboseHandler = new Verbose();
this.config = new SonarConfiguration(dataDirectory);
this.subcommandRegistry = new SubcommandRegistryHolder();
}
@@ -82,6 +86,9 @@ public abstract class SonarBootstrap<T> implements Sonar {
getLogger().info("Successfully initialized components in {}s!", launchTimer);
getLogger().info("Enabling all tasks and features...");
// Start all service threads
ServiceRepository.register();
try {
// Run the per-platform initialization method
enable();
@@ -126,7 +133,10 @@ public abstract class SonarBootstrap<T> implements Sonar {
public void shutdown() {
getLogger().info("Starting shutdown process...");
// ...
// Shut down service threads
ServiceRepository.shutdown();
getLogger().info("Successfully shut down. Goodbye!");
}
}
@@ -20,24 +20,23 @@ package xyz.jonesdev.sonar.common.fallback;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.fallback.Fallback;
import java.net.InetAddress;
import java.net.InetSocketAddress;
@RequiredArgsConstructor
public final class FallbackChannelHandler extends ChannelInboundHandlerAdapter {
private static final Fallback FALLBACK = Sonar.get().getFallback();
private final String username;
private static final @NotNull Fallback FALLBACK = Sonar.get().getFallback();
private final @NotNull String username;
@Override
public void channelUnregistered(final ChannelHandlerContext ctx) {
final InetAddress inetAddress = ((InetSocketAddress) ctx.channel().remoteAddress()).getAddress();
// Remove the IP address from the queue
public void channelUnregistered(final @NotNull ChannelHandlerContext ctx) {
// Remove the username from the connected players
FALLBACK.getConnected().remove(username);
FALLBACK.getQueue().remove(inetAddress);
// Remove the IP address from the queue
FALLBACK.getQueue().remove(((InetSocketAddress) ctx.channel().remoteAddress()).getAddress());
ctx.fireChannelUnregistered();
}
@@ -21,24 +21,26 @@ import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.timeout.IdleState;
import io.netty.handler.timeout.IdleStateEvent;
import io.netty.handler.timeout.IdleStateHandler;
import org.jetbrains.annotations.NotNull;
import java.util.concurrent.TimeUnit;
public final class FallbackTimeoutHandler extends IdleStateHandler {
private boolean closed;
private boolean knownDisconnect;
public FallbackTimeoutHandler(final long timeout, final TimeUnit timeUnit) {
super(timeout, 0L, 0L, timeUnit);
}
@Override
protected void channelIdle(final ChannelHandlerContext ctx, final IdleStateEvent idleStateEvent) throws Exception {
protected void channelIdle(final ChannelHandlerContext ctx,
final @NotNull IdleStateEvent idleStateEvent) throws Exception {
assert idleStateEvent.state() == IdleState.READER_IDLE;
readTimedOut(ctx);
}
private void readTimedOut(final ChannelHandlerContext ctx) {
if (!closed) {
if (!knownDisconnect) {
// The netty (default) ReadTimeoutHandler would normally just throw an Exception
// The default ReadTimeoutHandler does only check for the boolean 'closed' and
@@ -47,7 +49,7 @@ public final class FallbackTimeoutHandler extends IdleStateHandler {
ctx.close();
}
closed = true;
knownDisconnect = true;
}
}
}
@@ -20,20 +20,25 @@ package xyz.jonesdev.sonar.common.fallback;
import io.netty.buffer.ByteBuf;
import io.netty.handler.codec.CorruptedFrameException;
import io.netty.handler.codec.DecoderException;
import lombok.Setter;
import lombok.val;
import net.kyori.adventure.text.minimessage.MiniMessage;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.event.impl.UserVerifySuccessEvent;
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
import xyz.jonesdev.sonar.api.model.VerifiedPlayer;
import xyz.jonesdev.sonar.api.timer.SystemTimer;
import xyz.jonesdev.sonar.common.fallback.protocol.*;
import xyz.jonesdev.sonar.common.fallback.protocol.map.ItemMapType;
import xyz.jonesdev.sonar.common.fallback.protocol.map.MapInfoPreparer;
import xyz.jonesdev.sonar.common.fallback.protocol.map.PreparedMapInfo;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.config.FinishConfiguration;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.login.LoginAcknowledged;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.*;
import xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil;
import java.util.Objects;
import java.util.Random;
import java.util.UUID;
import java.util.regex.Pattern;
@@ -44,22 +49,28 @@ import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer.*;
public final class FallbackVerificationHandler implements FallbackPacketListener {
private static final Random RANDOM = new Random();
// General
private final SystemTimer login = new SystemTimer();
private final @NotNull FallbackUser<?, ?> user;
private final String username;
private final UUID playerUuid;
private @NotNull State state = State.LOGIN_ACK; // 1.20.2
// Checks
private short expectedTransactionId;
private long expectedKeepAliveId;
private int expectedTeleportId = -1;
private int tick, totalReceivedPackets;
private int ignoredMovementTicks;
private int expectedKeepAliveId, expectedTeleportId = -1;
private int tick, totalReceivedPackets, ignoredMovementTicks;
private double posX, posY, posZ, lastY;
private boolean resolvedClientBrand, resolvedClientSettings;
@Setter
private @NotNull State state = State.LOGIN_ACK;
private boolean listenForMovements;
private final SystemTimer login = new SystemTimer();
private static final Random RANDOM = new Random();
// Map captcha
private final SystemTimer keepAlive = new SystemTimer();
private final SystemTimer actionBar = new SystemTimer();
private @Nullable PreparedMapInfo captcha;
private int captchaTriesLeft;
public enum State {
// 1.20.2 configuration state
@@ -69,7 +80,9 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
// post-JOIN checks
CLIENT_SETTINGS, PLUGIN_MESSAGE, TRANSACTION,
// PLAY checks
TELEPORT, POSITION, COLLISIONS,
TELEPORT, POSITION,
// Captcha
MAP_CAPTCHA,
// Done
SUCCESS
}
@@ -90,8 +103,8 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
private void configure() {
// Set the state to CONFIGURE to avoid false positives
state = State.CONFIGURE;
// Send necessary configuration packets to the client
user.delayedWrite(synchronizeRegistry);
// Send the necessary configuration packets to the client
user.delayedWrite(REGISTRY_SYNC);
user.delayedWrite(FINISH_CONFIGURATION);
// Send all packets in one flush
user.getChannel().flush();
@@ -142,11 +155,12 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
final boolean v1_20_2 = user.getProtocolVersion().compareTo(MINECRAFT_1_20_2) >= 0;
if (!v1_20_2) {
// Set the state to CLIENT_SETTINGS to avoid false positives
// and go on with the flow of the verification.
// (1.20.2 needs the LOGIN_ACK state) and go on with
// the flow of the verification.
state = State.CLIENT_SETTINGS;
}
// Select the JoinGame packet for the respective protocol version
user.write(getJoinPacketForVersion(user.getProtocolVersion()));
// Send the JoinGame packet so we can continue the verification
user.write(joinGame);
if (v1_20_2) {
// Perform the transaction check since the ClientSettings
// and PluginMessage packets have already been validated.
@@ -165,23 +179,37 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
// Teleport the player to the spawn position
user.delayedWrite(new PositionLook(
SPAWN_X_POSITION, dynamicSpawnYPosition, SPAWN_Z_POSITION,
0f, 0f, expectedTeleportId, false
));
0f, -90f, expectedTeleportId, false));
// Make sure the player escapes the 1.18.2+ "Loading terrain" screen
if (user.getProtocolVersion().compareTo(MINECRAFT_1_18_2) >= 0) {
user.delayedWrite(dynamicSpawnPosition);
}
}
private void sendChunkData() {
// Set the state to POSITION to avoid false positives
// and go on with the flow of the verification.
state = State.POSITION;
// 1.20.3 packets introduced game events
if (user.getProtocolVersion().compareTo(MINECRAFT_1_20_3) >= 0) {
// Make sure the client knows that we're sending chunks next
user.delayedWrite(START_WRITING_CHUNKS);
}
// Teleport player into the fake lobby by sending an empty chunk
user.delayedWrite(EMPTY_CHUNK_DATA);
if (Sonar.get().getConfig().getVerification().isCheckCollisions()) {
// Send an UpdateSectionBlocks packet with a platform of blocks
// to check if the player collides with the solid platform.
user.delayedWrite(UPDATE_SECTION_BLOCKS);
// Send an UpdateSectionBlocks packet with a platform of blocks
// to check if the player collides with the solid platform.
user.delayedWrite(updateSectionBlocks);
// Checking gravity is disabled, just finish verification
if (!Sonar.get().getConfig().getVerification().getGravity().isEnabled()) {
// Switch to captcha state if needed
captchaOrFinish();
} else {
// Make sure the player knows we are checking them
user.delayedWrite(youAreBeingChecked);
// Send all packets in one flush
user.getChannel().flush();
}
// Send all packets in one flush
user.getChannel().flush();
}
private static boolean validateClientLocale(final @SuppressWarnings("unused") @NotNull FallbackUser<?, ?> user,
@@ -218,13 +246,60 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
public void handle(final @NotNull FallbackPacket packet) {
// The player has already been verified, drop all other packets
if (state == State.SUCCESS) return;
// Check if the player is not sending a ton of packets to the server
final int maxPackets = Sonar.get().getConfig().getVerification().getMaxLoginPackets() + maxPredictionTick;
checkFrame(++totalReceivedPackets < maxPackets, "too many packets");
// Check for timeout since the player could be sending packets but not important ones
final long timeout = Sonar.get().getConfig().getVerification().getMaxPing();
// We are expecting a captcha code in chat, drop all other packets
if (state == State.MAP_CAPTCHA) {
// Check if the player took too long to enter the captcha
final int maxDuration = Sonar.get().getConfig().getVerification().getMap().getMaxDuration();
checkFrame(!login.elapsed(maxDuration), "took too long to enter captcha");
// Handle incoming chat messages
if (packet instanceof Chat) {
final Chat chat = (Chat) packet;
Objects.requireNonNull(captcha);
if (!chat.getMessage().equals(captcha.getInfo().getAnswer())) {
// Captcha is incorrect
checkFrame(captchaTriesLeft-- > 0, "failed captcha too often");
user.write(incorrectCaptcha);
return;
}
// Captcha is correct
finish();
return;
}
// Every second
if (actionBar.elapsed(1000L)) {
final String actionBarMessage = Sonar.get().getConfig().getVerification().getMap().getEnterCodeActionBar();
// Only send action bar if the message is actually supposed to be sent
if (!actionBarMessage.isEmpty()) {
final String timeLeft = String.format("%.0f", (maxDuration - login.delay()) / 1000D);
user.write(new Chat(MiniMessage.miniMessage().deserialize(
actionBarMessage.replace("%time-left%", timeLeft)), Chat.GAME_INFO_TYPE));
// Make sure to reset the timer
actionBar.reset();
}
}
// Every about 10 seconds
if (keepAlive.elapsed(timeout)) {
// Send the message again to remind the player
user.delayedWrite(enterCodeMessage);
// Send a KeepAlive packet to prevent timeout
user.delayedWrite(CAPTCHA_KEEP_ALIVE);
// Send both packets in one flush
user.getChannel().flush();
// Make sure to reset the timer
keepAlive.reset();
}
return;
}
// Check if the player is not sending a ton of packets to the server
final int maxPackets = Sonar.get().getConfig().getVerification().getMaxLoginPackets() + maxMovementTick;
checkFrame(++totalReceivedPackets < maxPackets, "too many packets");
// Check if the time limit has exceeded
if (login.elapsed(timeout)) {
user.getChannel().close();
@@ -323,18 +398,6 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
// Check if the transaction ID is valid
checkFrame(transaction.getId() == expectedTransactionId, "invalid transaction id");
// Checking gravity is disabled, just finish verification
if (!Sonar.get().getConfig().getVerification().isCheckGravity()) {
finish();
return;
}
// TODO: implement ChunkData packet for 1.20.2 and fix block state Ids for barriers
if (user.getProtocolVersion().compareTo(MINECRAFT_1_20_2) >= 0) {
finish();
return;
}
// First, send an Abilities packet to the client to make
// sure the player falls even in spectator mode.
// Then, teleport the player to the spawn position.
@@ -405,11 +468,8 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
// The player is not allowed to move away from the collision platform.
// This should not happen unless the max movement tick is configured to a high number.
checkFrame(Math.abs(x - BLOCKS_PER_ROW) <= BLOCKS_PER_ROW, "moved too far (x)");
checkFrame(Math.abs(z - BLOCKS_PER_ROW) <= BLOCKS_PER_ROW, "moved too far (z)");
// The onGround property can never be true when we aren't checking for collisions
checkFrame(!ground || state == State.COLLISIONS, "invalid ground state");
checkFrame(Math.abs(x - BLOCKS_PER_ROW) < BLOCKS_PER_ROW, "moved too far (x)");
checkFrame(Math.abs(z - BLOCKS_PER_ROW) < BLOCKS_PER_ROW, "moved too far (z)");
// Check if the client hasn't moved before sending the first movement packet
if (!listenForMovements) {
@@ -427,44 +487,39 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
// We have to account for this or the player will fail the verification.
if (deltaY == 0) {
// Check for too many ignored Y ticks
final int maxIgnoredTicks = Sonar.get().getConfig().getVerification().getMaxIgnoredTicks();
final int maxIgnoredTicks = Sonar.get().getConfig().getVerification().getGravity().getMaxIgnoredTicks();
checkFrame(++ignoredMovementTicks < maxIgnoredTicks, "too many ignored ticks");
return;
}
// Calculate the difference between the player's Y coordinate and the expected Y coordinate
final double collisionOffsetY = DEFAULT_Y_COLLIDE_POSITION - y;
// Check if the player is colliding by performing a basic Y offset check.
// The offset cannot be greater than 0 since the blocks will not let the player fall through them.
checkFrame(collisionOffsetY <= 0, "fell through blocks: " + collisionOffsetY);
if (tick > maxMovementTick) {
if (!Sonar.get().getConfig().getVerification().isCheckCollisions()) {
// Checking collisions is disabled, just finish verification
finish();
return;
}
if (state != State.COLLISIONS) {
// Now we don't care about gravity anymore,
// we just want the player to collide with the blocks.
state = State.COLLISIONS;
}
// Calculate the difference between the player's Y coordinate and the expected Y coordinate
final double offsetY = DEFAULT_Y_COLLIDE_POSITION - y;
// Log/debug position if enabled in the configuration
if (Sonar.get().getConfig().getVerification().isDebugXYZPositions()) {
user.getFallback().getLogger().info("{}: {}/{}/{} - offset: {}", username, x, y, z, offsetY);
user.getFallback().getLogger().info("{}: {}/{}/{} - deltaY: {} - ground: {} - collision: {}",
username, x, y, z, deltaY, ground, collisionOffsetY);
}
// Check if the player is colliding by performing a basic Y offset check.
// The offset cannot be greater than 0 since the blocks will not let the player fall through them.
checkFrame(offsetY <= 0, "no collisions: " + offsetY);
// The player is colliding to blocks, finish verification
if (ground) {
// The player is colliding to blocks, finish verification
finish();
captchaOrFinish();
return;
}
} else {
// Check if the player is spoofing the ground state
checkFrame(!ground, "spoofed ground state");
}
if (!ground && tick < maxPredictionTick) {
// Make sure we don't run out of predicted Y motions
checkFrame(tick < preparedCachedYMotions.length, "too many movements");
if (!ground) {
final double predictedY = preparedCachedYMotions[tick];
final double offsetY = Math.abs(deltaY - predictedY);
@@ -481,17 +536,41 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
tick++;
}
private void assertState(final @NotNull State expectedState) {
if (expectedState != state) {
user.fail("expected " + expectedState + ", got " + state);
private void captchaOrFinish() {
if (Sonar.get().getConfig().getVerification().getMap().isEnabled()) {
// Set the state to MAP_CAPTCHA, so we don't handle any unnecessary packets
state = State.MAP_CAPTCHA;
if (!Sonar.get().getConfig().getVerification().getGravity().isEnabled()
&& user.getProtocolVersion().compareTo(MINECRAFT_1_18_2) >= 0) {
// Make sure the player escapes the 1.18.2+ "Loading terrain" screen
user.delayedWrite(CAPTCHA_SPAWN_POSITION);
}
// Initialize the map captcha
handleMapCaptcha();
} else {
// Finish the verification
finish();
}
}
private void checkFrame(final boolean condition, final String message) {
if (!condition) {
user.fail(message);
throw new CorruptedFrameException(message);
}
private void handleMapCaptcha() {
// Reset max tries
captchaTriesLeft = Sonar.get().getConfig().getVerification().getMap().getMaxTries();
// Set slot to map
user.delayedWrite(new SetSlot(0, 36, 1, 0,
ItemMapType.FILLED_MAP.getId(user.getProtocolVersion()), SetSlot.MAP_NBT));
// Send random captcha to the player
captcha = MapInfoPreparer.getRandomCaptcha();
Objects.requireNonNull(captcha).write(user);
// Teleport the player to the position above the platform
user.delayedWrite(CAPTCHA_POSITION);
// Make sure the player cannot move
user.delayedWrite(CAPTCHA_ABILITIES);
// Make sure the player knows what to do
user.delayedWrite(enterCodeMessage);
// Send all packets in one flush
user.getChannel().flush();
}
private void finish() {
@@ -514,4 +593,26 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
.replace("%name%", username)
.replace("%time%", login.toString()));
}
/**
* Fails the verification if a certain state is unexpected.
*
* @param expectedState Expected state
*/
private void assertState(final @NotNull State expectedState) {
checkFrame(state == expectedState, "expected " + expectedState + ", got " + state);
}
/**
* Checks if a certain condition is met, fails the verification if not.
*
* @param condition Condition to fail if it's false
* @param message Messages displayed in the stacktrace
*/
private void checkFrame(final boolean condition, final String message) {
if (!condition) {
user.fail(message);
throw new CorruptedFrameException(message);
}
}
}
@@ -33,14 +33,13 @@ import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.readVarInt;
public final class FallbackPacketDecoder extends ChannelInboundHandlerAdapter {
private final FallbackUser<?, ?> user;
private final FallbackPacketListener listener;
private @NotNull FallbackPacketRegistry.ProtocolRegistry registry;
private FallbackPacketRegistry.ProtocolRegistry registry;
public FallbackPacketDecoder(final @NotNull FallbackUser<?, ?> user,
final @NotNull FallbackPacketListener listener) {
this.user = user;
this.registry = (user.getProtocolVersion().compareTo(MINECRAFT_1_20_2) >= 0 ? LOGIN : GAME)
.getProtocolRegistry(SERVERBOUND, user.getProtocolVersion());
this.listener = listener;
updateRegistry(user.getProtocolVersion().compareTo(MINECRAFT_1_20_2) >= 0 ? LOGIN : GAME);
}
public void updateRegistry(final @NotNull FallbackPacketRegistry registry) {
@@ -66,8 +65,7 @@ public final class FallbackPacketDecoder extends ChannelInboundHandlerAdapter {
final int packetId = readVarInt(byteBuf);
final FallbackPacket packet = registry.createPacket(packetId);
// If the packet hasn't been found, skip it
// TODO: Can we fail the verification afterwards?
// If the packet isn't found, skip it
if (packet == null) {
byteBuf.readerIndex(originalReaderIndex);
return;
@@ -93,6 +91,9 @@ public final class FallbackPacketDecoder extends ChannelInboundHandlerAdapter {
// Let our verification handler process the packet
listener.handle(packet);
// Fire channel read to avoid timeout
ctx.fireChannelRead(packet);
} finally {
// Release the ByteBuf to avoid memory leaks
byteBuf.release();
@@ -23,7 +23,7 @@ import lombok.Data;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.config.FinishConfiguration;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.config.RegistrySync;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.config.RegistryData;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.login.LoginAcknowledged;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.login.LoginSuccess;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.*;
@@ -48,13 +48,13 @@ public enum FallbackPacketRegistry {
CONFIG {
{
clientbound.register(Disconnect.class, Disconnect::new,
map(0x01, MINECRAFT_1_20_2, false));
map(0x01, MINECRAFT_1_20_2, true));
clientbound.register(FinishConfiguration.class, FinishConfiguration::new,
map(0x02, MINECRAFT_1_20_2, false));
clientbound.register(KeepAlive.class, KeepAlive::new,
map(0x03, MINECRAFT_1_20_2, false));
clientbound.register(RegistrySync.class, RegistrySync::new,
map(0x05, MINECRAFT_1_20_2, false));
clientbound.register(RegistryData.class, RegistryData::new,
map(0x05, MINECRAFT_1_20_2, true));
serverbound.register(ClientSettings.class, ClientSettings::new,
map(0x00, MINECRAFT_1_20_2, false));
@@ -166,7 +166,8 @@ public enum FallbackPacketRegistry {
map(0x40, MINECRAFT_1_19_1, true),
map(0x3F, MINECRAFT_1_19_3, true),
map(0x43, MINECRAFT_1_19_4, true),
map(0x45, MINECRAFT_1_20_2, true));
map(0x45, MINECRAFT_1_20_2, true),
map(0x47, MINECRAFT_1_20_3, true));
clientbound.register(Transaction.class, Transaction::new,
map(0x32, MINECRAFT_1_7_2, true),
map(0x11, MINECRAFT_1_9, true),
@@ -180,19 +181,73 @@ public enum FallbackPacketRegistry {
map(0x2E, MINECRAFT_1_19_3, true),
map(0x32, MINECRAFT_1_19_4, true),
map(0x33, MINECRAFT_1_20_2, true));
clientbound.register(DefaultSpawnPosition.class, DefaultSpawnPosition::new,
map(0x05, MINECRAFT_1_7_2, true),
map(0x43, MINECRAFT_1_9, true),
map(0x45, MINECRAFT_1_12, true),
map(0x46, MINECRAFT_1_12_1, true),
map(0x49, MINECRAFT_1_13, true),
map(0x4D, MINECRAFT_1_14, true),
map(0x4E, MINECRAFT_1_15, true),
map(0x42, MINECRAFT_1_16, true),
map(0x4B, MINECRAFT_1_17, true),
map(0x4A, MINECRAFT_1_19, true),
map(0x4D, MINECRAFT_1_19_1, true),
map(0x4C, MINECRAFT_1_19_3, true),
map(0x50, MINECRAFT_1_19_4, true),
map(0x52, MINECRAFT_1_20_2, true),
map(0x54, MINECRAFT_1_20_3, true));
clientbound.register(MapData.class, MapData::new,
map(0x34, MINECRAFT_1_7_2, true),
map(0x24, MINECRAFT_1_9, true),
map(0x26, MINECRAFT_1_13, true),
map(0x27, MINECRAFT_1_15, true),
map(0x26, MINECRAFT_1_16, true),
map(0x25, MINECRAFT_1_16_2, true),
map(0x27, MINECRAFT_1_17, true),
map(0x24, MINECRAFT_1_19, true),
map(0x26, MINECRAFT_1_19_1, true),
map(0x25, MINECRAFT_1_19_3, true),
map(0x29, MINECRAFT_1_19_4, true),
map(0x2A, MINECRAFT_1_20_2, true));
clientbound.register(SetSlot.class, SetSlot::new,
map(0x2F, MINECRAFT_1_7_2, true),
map(0x16, MINECRAFT_1_9, true),
map(0x17, MINECRAFT_1_13, true),
map(0x16, MINECRAFT_1_14, true),
map(0x17, MINECRAFT_1_15, true),
map(0x16, MINECRAFT_1_16, true),
map(0x15, MINECRAFT_1_16_2, true),
map(0x16, MINECRAFT_1_17, true),
map(0x13, MINECRAFT_1_19, true),
map(0x12, MINECRAFT_1_19_3, true),
map(0x14, MINECRAFT_1_19_4, true),
map(0x15, MINECRAFT_1_20_2, true));
clientbound.register(Chat.class, Chat::new,
map(0x02, MINECRAFT_1_7_2, true),
map(0x0F, MINECRAFT_1_9, true),
map(0x0E, MINECRAFT_1_13, true),
map(0x0F, MINECRAFT_1_15, true),
map(0x0E, MINECRAFT_1_16, true),
map(0x0F, MINECRAFT_1_18_2, true),
map(0x5F, MINECRAFT_1_19, true),
map(0x62, MINECRAFT_1_19_1, true),
map(0x60, MINECRAFT_1_19_3, true),
map(0x64, MINECRAFT_1_19_4, true),
map(0x67, MINECRAFT_1_20_2, true));
map(0x02, MINECRAFT_1_7_2, false),
map(0x0F, MINECRAFT_1_9, false),
map(0x0E, MINECRAFT_1_13, false),
map(0x0F, MINECRAFT_1_15, false),
map(0x0E, MINECRAFT_1_16, false),
map(0x0F, MINECRAFT_1_17, false),
map(0x5F, MINECRAFT_1_19, false),
map(0x62, MINECRAFT_1_19_1, false),
map(0x60, MINECRAFT_1_19_3, false),
map(0x64, MINECRAFT_1_19_4, false),
map(0x67, MINECRAFT_1_20_2, false),
map(0x69, MINECRAFT_1_20_2, false));
clientbound.register(GameEvent.class, GameEvent::new,
map(0x20, MINECRAFT_1_20_3, true));
serverbound.register(Chat.class, Chat::new,
map(0x01, MINECRAFT_1_7_2, false),
map(0x02, MINECRAFT_1_9, false),
map(0x03, MINECRAFT_1_12, false),
map(0x02, MINECRAFT_1_12_1, false),
map(0x03, MINECRAFT_1_14, false),
map(0x04, MINECRAFT_1_19, false),
map(0x05, MINECRAFT_1_19_1, false),
map(0x05, MINECRAFT_1_19_3, false));
serverbound.register(KeepAlive.class, KeepAlive::new,
map(0x00, MINECRAFT_1_7_2, false),
map(0x0B, MINECRAFT_1_9, false),
@@ -206,7 +261,8 @@ public enum FallbackPacketRegistry {
map(0x12, MINECRAFT_1_19_1, false),
map(0x11, MINECRAFT_1_19_3, false),
map(0x12, MINECRAFT_1_19_4, false),
map(0x14, MINECRAFT_1_20_2, false));
map(0x14, MINECRAFT_1_20_2, false),
map(0x15, MINECRAFT_1_20_3, false));
serverbound.register(ClientSettings.class, ClientSettings::new,
map(0x15, MINECRAFT_1_7_2, false),
map(0x04, MINECRAFT_1_9, false),
@@ -244,7 +300,8 @@ public enum FallbackPacketRegistry {
map(0x15, MINECRAFT_1_19_1, false),
map(0x14, MINECRAFT_1_19_3, false),
map(0x15, MINECRAFT_1_19_4, false),
map(0x17, MINECRAFT_1_20_2, false));
map(0x17, MINECRAFT_1_20_2, false),
map(0x18, MINECRAFT_1_20_3, false));
serverbound.register(TeleportConfirm.class, TeleportConfirm::new,
map(0x00, MINECRAFT_1_9, false));
serverbound.register(Position.class, Position::new,
@@ -260,7 +317,8 @@ public enum FallbackPacketRegistry {
map(0x14, MINECRAFT_1_19_1, false),
map(0x13, MINECRAFT_1_19_3, false),
map(0x14, MINECRAFT_1_19_4, false),
map(0x16, MINECRAFT_1_20_2, false));
map(0x16, MINECRAFT_1_20_2, false),
map(0x17, MINECRAFT_1_20_3, false));
serverbound.register(Player.class, Player::new,
map(0x03, MINECRAFT_1_7_2, false),
map(0x0F, MINECRAFT_1_9, false),
@@ -274,7 +332,8 @@ public enum FallbackPacketRegistry {
map(0x17, MINECRAFT_1_19_1, false),
map(0x16, MINECRAFT_1_19_3, false),
map(0x17, MINECRAFT_1_19_4, false),
map(0x19, MINECRAFT_1_20_2, false));
map(0x19, MINECRAFT_1_20_2, false),
map(0x1A, MINECRAFT_1_20_3, false));
serverbound.register(Transaction.class, Transaction::new,
map(0x0F, MINECRAFT_1_7_2, false),
map(0x05, MINECRAFT_1_9, false),
@@ -287,7 +346,8 @@ public enum FallbackPacketRegistry {
map(0x20, MINECRAFT_1_19_1, false),
map(0x1F, MINECRAFT_1_19_3, false),
map(0x20, MINECRAFT_1_19_4, false),
map(0x23, MINECRAFT_1_20_2, false));
map(0x23, MINECRAFT_1_20_2, false),
map(0x24, MINECRAFT_1_20_3, false));
}
};
@@ -308,14 +368,13 @@ public enum FallbackPacketRegistry {
private final Map<ProtocolVersion, ProtocolRegistry> versions;
PacketRegistry() {
Map<ProtocolVersion, ProtocolRegistry> mutableVersions = new EnumMap<>(ProtocolVersion.class);
for (ProtocolVersion version : ProtocolVersion.values()) {
final Map<ProtocolVersion, ProtocolRegistry> mutableVersions = new EnumMap<>(ProtocolVersion.class);
for (final ProtocolVersion version : ProtocolVersion.values()) {
if (!version.isLegacy() && !version.isUnknown()) {
mutableVersions.put(version, new ProtocolRegistry(version));
}
}
versions = Collections.unmodifiableMap(mutableVersions);
this.versions = Collections.unmodifiableMap(mutableVersions);
}
ProtocolRegistry getProtocolRegistry(final ProtocolVersion version) {
@@ -345,7 +404,7 @@ public enum FallbackPacketRegistry {
break;
}
final ProtocolRegistry registry = this.versions.get(protocol);
final ProtocolRegistry registry = versions.get(protocol);
if (registry == null) {
throw new IllegalArgumentException("Unknown protocol version "
+ current.protocolVersion);
@@ -374,35 +433,12 @@ public enum FallbackPacketRegistry {
private static ProtocolVersion getProtocolVersion(final @NotNull PacketMapping current,
final @NotNull PacketMapping next,
final @NotNull ProtocolVersion from) {
final ProtocolVersion lastValid = current.lastValidProtocolVersion;
if (lastValid != null) {
if (next != current) {
throw new IllegalArgumentException("Cannot add a mapping after last valid mapping");
}
if (from.compareTo(lastValid) > 0) {
throw new IllegalArgumentException(
"Last mapping version cannot be higher than highest mapping version");
}
}
final ProtocolVersion last = (ProtocolVersion) SUPPORTED_VERSIONS.toArray()[SUPPORTED_VERSIONS.size() - 1];
final ProtocolVersion to = current == next ? lastValid != null
? lastValid : last : next.protocolVersion;
final ProtocolVersion lastInList = lastValid != null ? lastValid : last;
if (from.compareTo(to) >= 0 && from != lastInList) {
throw new IllegalArgumentException(String.format(
"Next mapping version (%s) should be lower then current (%s)", to, from));
}
return to;
return current == next ? LATEST_VERSION : next.protocolVersion;
}
}
public static class ProtocolRegistry {
public final ProtocolVersion version;
private final ProtocolVersion version;
private final IntObjectMap<Supplier<? extends FallbackPacket>> packetIdToSupplier =
new IntObjectHashMap<>(16, 0.5f);
private final Map<Class<? extends FallbackPacket>, Integer> packetClassToId =
@@ -436,15 +472,12 @@ public enum FallbackPacketRegistry {
private final int id;
private final ProtocolVersion protocolVersion;
private final boolean encodeOnly;
private final ProtocolVersion lastValidProtocolVersion;
PacketMapping(final int id,
final ProtocolVersion protocolVersion,
final ProtocolVersion lastValidProtocolVersion,
final boolean packetDecoding) {
this.id = id;
this.protocolVersion = protocolVersion;
this.lastValidProtocolVersion = lastValidProtocolVersion;
this.encodeOnly = packetDecoding;
}
}
@@ -452,6 +485,6 @@ public enum FallbackPacketRegistry {
private static @NotNull PacketMapping map(final int id,
final ProtocolVersion version,
final boolean encodeOnly) {
return new PacketMapping(id, version, null, encodeOnly);
return new PacketMapping(id, version, encodeOnly);
}
}
@@ -18,111 +18,87 @@
package xyz.jonesdev.sonar.common.fallback.protocol;
import lombok.experimental.UtilityClass;
import net.kyori.adventure.nbt.BinaryTagIO;
import net.kyori.adventure.nbt.BinaryTagTypes;
import net.kyori.adventure.nbt.CompoundBinaryTag;
import net.kyori.adventure.nbt.ListBinaryTag;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.api.timer.SystemTimer;
import xyz.jonesdev.sonar.common.fallback.protocol.block.BlockPosition;
import xyz.jonesdev.sonar.common.fallback.protocol.block.BlockType;
import xyz.jonesdev.sonar.common.fallback.protocol.block.ChangedBlock;
import xyz.jonesdev.sonar.common.fallback.protocol.dimension.DimensionInfo;
import xyz.jonesdev.sonar.common.fallback.protocol.map.MapInfoPreparer;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.config.FinishConfiguration;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.config.RegistrySync;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.Abilities;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.EmptyChunkData;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.JoinGame;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.UpdateSectionBlocks;
import java.io.InputStream;
import java.util.Collections;
import java.util.Objects;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.config.RegistryData;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.*;
@UtilityClass
public class FallbackPreparer {
// Mappings
private final CompoundBinaryTag CHAT_TYPE_119;
private final CompoundBinaryTag CHAT_TYPE_1191;
private final CompoundBinaryTag DAMAGE_TYPE_1194;
private final CompoundBinaryTag DAMAGE_TYPE_120;
static {
CHAT_TYPE_119 = getMapping("chat_1_19.nbt");
CHAT_TYPE_1191 = getMapping("chat_1_19_1.nbt");
DAMAGE_TYPE_1194 = getMapping("damage_1_19_4.nbt");
DAMAGE_TYPE_120 = getMapping("damage_type_1_20.nbt");
}
// Abilities
public final FallbackPacket DEFAULT_ABILITIES = new Abilities((byte) 0, 0f, 0f);
public final FallbackPacket DEFAULT_ABILITIES = new Abilities(0x00, 0f, 0f);
public final FallbackPacket CAPTCHA_ABILITIES = new Abilities(0x02, 0f, 0f);
// Chunks
public final FallbackPacket EMPTY_CHUNK_DATA = new EmptyChunkData(0, 0);
// Finish Configuration
public final FallbackPacket FINISH_CONFIGURATION = new FinishConfiguration();
// Registry Sync
public FallbackPacket synchronizeRegistry;
// Synchronize Registry
public final FallbackPacket REGISTRY_SYNC = new RegistryData();
// Keep Alive
public final FallbackPacket CAPTCHA_KEEP_ALIVE = new KeepAlive(0L);
// Game Event (1.20.3+)
public final FallbackPacket START_WRITING_CHUNKS = new GameEvent(13, 0);
// Chat
public FallbackPacket enterCodeMessage;
public FallbackPacket youAreBeingChecked;
public FallbackPacket incorrectCaptcha;
// JoinGame
private FallbackPacket legacyJoinGame;
private FallbackPacket joinGame116;
private FallbackPacket joinGame1162;
private FallbackPacket joinGame1182;
private FallbackPacket joinGame1191;
private FallbackPacket joinGame1194;
private JoinGame joinGame120;
// Dimension
private final DimensionInfo DIMENSION = new DimensionInfo(
"minecraft:overworld", "sonar", false, false);
public FallbackPacket joinGame;
// Update Section Blocks
public FallbackPacket updateSectionBlocks;
// Default Spawn Position
public FallbackPacket dynamicSpawnPosition;
// Collisions
public final int BLOCKS_PER_ROW = 8; // 8 * 8 = 64 (max)
private final int SPAWN_BUFFER = 5; // player spawns at 255 + 5 (5 blocks above the platform)
public final int BLOCKS_PER_ROW = 8; // 8 * 8 = 64 (protocol maximum)
public final int SPAWN_X_POSITION = 16 / 2; // middle of the chunk
public final int SPAWN_Z_POSITION = 16 / 2; // middle of the chunk
public final int DEFAULT_Y_COLLIDE_POSITION = 255; // 255 is the maximum Y position allowed
public UpdateSectionBlocks UPDATE_SECTION_BLOCKS;
// Captcha position
public final FallbackPacket CAPTCHA_POSITION = new PositionLook(
SPAWN_X_POSITION, 1337, SPAWN_Z_POSITION, 0f, 90f, 0, false);
public final FallbackPacket CAPTCHA_SPAWN_POSITION = new DefaultSpawnPosition(
SPAWN_X_POSITION, 1337, SPAWN_Z_POSITION);
// Blocks
private final ChangedBlock[] CHANGED_BLOCKS = new ChangedBlock[BLOCKS_PER_ROW * BLOCKS_PER_ROW];
public int maxMovementTick, maxPredictionTick, dynamicSpawnYPosition;
public int maxMovementTick, dynamicSpawnYPosition;
public double[] preparedCachedYMotions;
public double maxFallDistance;
public final int SPAWN_X_POSITION = BLOCKS_PER_ROW; // middle of the chunk
public final int SPAWN_Z_POSITION = BLOCKS_PER_ROW;
public void prepare() {
legacyJoinGame = createJoinGamePacket(MINECRAFT_1_8);
joinGame116 = createJoinGamePacket(MINECRAFT_1_16);
joinGame1162 = createJoinGamePacket(MINECRAFT_1_16_2);
joinGame1182 = createJoinGamePacket(MINECRAFT_1_18_2);
joinGame1191 = createJoinGamePacket(MINECRAFT_1_19_1);
joinGame1194 = createJoinGamePacket(MINECRAFT_1_19_4);
joinGame120 = createJoinGamePacket(MINECRAFT_1_20);
synchronizeRegistry = new RegistrySync(joinGame120.getRegistry());
joinGame = new JoinGame(0,
Sonar.get().getConfig().getVerification().getGravity().getGamemode().getId(),
0,
false,
0,
true,
true,
false,
new String[]{"minecraft:overworld"},
"minecraft:overworld");
maxMovementTick = Sonar.get().getConfig().getVerification().getMaxMovementTicks();
maxPredictionTick = maxMovementTick + 10;
preparedCachedYMotions = new double[maxPredictionTick + 1];
for (int i = 0; i < maxPredictionTick + 1; i++) {
preparedCachedYMotions[i] = -((Math.pow(0.98, i) - 1) * 3.92);
}
// Adjust block and collide Y position based on max fall distance
maxFallDistance = 0;
for (int i = 0; i < maxMovementTick; i++) {
maxFallDistance += preparedCachedYMotions[i];
maxMovementTick = Sonar.get().getConfig().getVerification().getGravity().getMaxMovementTicks();
preparedCachedYMotions = new double[maxMovementTick + 8];
for (int i = 0; i < preparedCachedYMotions.length; i++) {
final double gravity = -((Math.pow(0.98, i) - 1) * 3.92);
preparedCachedYMotions[i] = gravity;
maxFallDistance += gravity;
}
// Set the dynamic spawn buffer
final int DYNAMIC_SPAWN_BUFFER = (int) (SPAWN_BUFFER + maxFallDistance);
// Set the dynamic block and collide Y position based on the maximum fall distance
dynamicSpawnYPosition = DEFAULT_Y_COLLIDE_POSITION + DYNAMIC_SPAWN_BUFFER;
dynamicSpawnYPosition = DEFAULT_Y_COLLIDE_POSITION + (int) Math.ceil(maxFallDistance);
dynamicSpawnPosition = new DefaultSpawnPosition(SPAWN_X_POSITION, dynamicSpawnYPosition, SPAWN_Z_POSITION);
// Prepare collision platform positions
int index = 0;
@@ -132,166 +108,28 @@ public class FallbackPreparer {
x + (BLOCKS_PER_ROW / 2),
DEFAULT_Y_COLLIDE_POSITION,
z + (BLOCKS_PER_ROW / 2),
0, 0
);
0, 0);
CHANGED_BLOCKS[index++] = new ChangedBlock(position, BlockType.BARRIER);
}
}
// Prepare UpdateSectionBlocks packet
UPDATE_SECTION_BLOCKS = new UpdateSectionBlocks(0, 0, CHANGED_BLOCKS);
}
updateSectionBlocks = new UpdateSectionBlocks(0, 0, CHANGED_BLOCKS);
private @Nullable CompoundBinaryTag getMapping(final @NotNull String fileName) {
try (final InputStream inputStream = Sonar.class.getResourceAsStream("/mappings/" + fileName)) {
return BinaryTagIO.reader().read(Objects.requireNonNull(inputStream), BinaryTagIO.Compression.GZIP);
} catch (Throwable throwable) {
Sonar.get().getLogger().error("Could not load mappings for {}: {}", fileName, throwable);
return null;
}
}
public static FallbackPacket getJoinPacketForVersion(final @NotNull ProtocolVersion protocolVersion) {
if (protocolVersion.compareTo(MINECRAFT_1_15_2) <= 0) {
return legacyJoinGame; // 1.7.2-1.15.2
}
if (protocolVersion.compareTo(MINECRAFT_1_16_1) <= 0) {
return joinGame116; // 1.16-1.16.1
}
if (protocolVersion.compareTo(MINECRAFT_1_18) <= 0) {
return joinGame1162; // 1.16.2-1.18
}
if (protocolVersion.compareTo(MINECRAFT_1_19) <= 0) {
return joinGame1182; // 1.18.1-1.19
}
if (protocolVersion.compareTo(MINECRAFT_1_19_3) <= 0) {
return joinGame1191; // 1.19.1-1.19.3
}
if (protocolVersion.compareTo(MINECRAFT_1_19_4) == 0) {
return joinGame1194; // 1.19.4
}
return joinGame120; // 1.20-latest
}
// Partially taken from
// https://github.com/Elytrium/LimboAPI/blob/master/plugin/src/main/java/net/elytrium/limboapi/server/LimboImpl.java#L607
private @NotNull JoinGame createJoinGamePacket(final @NotNull ProtocolVersion protocolVersion) {
final JoinGame joinGame = new JoinGame();
joinGame.setLevelType("flat");
joinGame.setGamemode(Sonar.get().getConfig().getVerification().getGamemodeId());
joinGame.setDimension(0);
joinGame.setReducedDebugInfo(true);
// 1.7.2-1.8.9 don't need any dimension information
if (protocolVersion.compareTo(MINECRAFT_1_8) <= 0) {
return joinGame;
// "You are being checked" message
if (Sonar.get().getConfig().getVerification().getGravity().isEnabled()) {
youAreBeingChecked = new Chat(Sonar.get().getConfig().getVerification().getGravity().getYouAreBeingChecked());
}
joinGame.setDimensionInfo(DIMENSION);
if (Sonar.get().getConfig().getVerification().getMap().isEnabled()) {
enterCodeMessage = new Chat(Sonar.get().getConfig().getVerification().getMap().getEnterCode());
incorrectCaptcha = new Chat(Sonar.get().getConfig().getVerification().getMap().getFailedCaptcha());
final CompoundBinaryTag.Builder registryContainer = CompoundBinaryTag.builder();
final ListBinaryTag encodedDimensionRegistry = ListBinaryTag.builder(BinaryTagTypes.COMPOUND)
.add(createDimensionData(protocolVersion))
.build();
if (protocolVersion.compareTo(MINECRAFT_1_16_2) >= 0) {
final CompoundBinaryTag.Builder dimensionRegistryEntry = CompoundBinaryTag.builder();
dimensionRegistryEntry.putString("type", "minecraft:dimension_type");
dimensionRegistryEntry.put("value", encodedDimensionRegistry);
registryContainer.put("minecraft:dimension_type", dimensionRegistryEntry.build());
final CompoundBinaryTag.Builder effectsTagBuilder = CompoundBinaryTag.builder()
.putInt("sky_color", 7907327)
.putInt("fog_color", 12638463)
.putInt("water_color", 0)
.putInt("water_fog_color", 0);
final CompoundBinaryTag.Builder elementTagBuilder = CompoundBinaryTag.builder()
.putFloat("depth", 0.125f)
.putFloat("temperature", 0.8f)
.putFloat("scale", 0.05f)
.putFloat("downfall", 0.4f)
.putString("category", "plains")
.put("effects", effectsTagBuilder.build());
if (protocolVersion.compareTo(MINECRAFT_1_19_4) >= 0) {
elementTagBuilder.putBoolean("has_precipitation", false);
} else {
elementTagBuilder.putString("precipitation", "rain");
}
registryContainer.put("minecraft:worldgen/biome", CompoundBinaryTag.builder()
.putString("type", "minecraft:worldgen/biome")
.put("value", ListBinaryTag.from(Collections.singletonList(
CompoundBinaryTag.builder()
.putString("name", "minecraft:plains")
.putInt("id", 1)
.put("element", elementTagBuilder.build())
.build()
))).build()
);
if (protocolVersion.compareTo(MINECRAFT_1_19) == 0) {
registryContainer.put("minecraft:chat_type", CHAT_TYPE_119);
} else if (protocolVersion.compareTo(MINECRAFT_1_19_1) >= 0) {
registryContainer.put("minecraft:chat_type", CHAT_TYPE_1191);
}
if (protocolVersion.compareTo(MINECRAFT_1_19_4) == 0) {
registryContainer.put("minecraft:damage_type", DAMAGE_TYPE_1194);
} else if (protocolVersion.compareTo(MINECRAFT_1_20) >= 0) {
registryContainer.put("minecraft:damage_type", DAMAGE_TYPE_120);
}
} else {
registryContainer.put("dimension", encodedDimensionRegistry);
final SystemTimer timer = new SystemTimer();
Sonar.get().getLogger().info("Precomputing map captcha answers...");
// Precompute captcha answers
MapInfoPreparer.prepare();
Sonar.get().getLogger().info("Successfully precomputed captcha answers in {}s!", timer);
}
CompoundBinaryTag currentDimensionData = encodedDimensionRegistry.getCompound(0);
if (protocolVersion.compareTo(MINECRAFT_1_16_2) >= 0) {
currentDimensionData = currentDimensionData.getCompound("element");
}
joinGame.setCurrentDimensionData(currentDimensionData);
joinGame.setLevelNames(new String[]{"minecraft:overworld"});
joinGame.setRegistry(registryContainer.build());
return joinGame;
}
public @NotNull CompoundBinaryTag createDimensionData(final @NotNull ProtocolVersion version) {
final CompoundBinaryTag details = CompoundBinaryTag.builder()
.putBoolean("natural", false)
.putFloat("ambient_light", 0f)
.putBoolean("shrunk", false)
.putBoolean("ultrawarm", false)
.putBoolean("has_ceiling", false)
.putBoolean("has_skylight", true)
.putBoolean("piglin_safe", false)
.putBoolean("bed_works", false)
.putBoolean("respawn_anchor_works", false)
.putBoolean("has_raids", false)
.putInt("logical_height", 256)
.putString("infiniburn", version.compareTo(MINECRAFT_1_18_2) >= 0 ? "#minecraft" +
":infiniburn_nether" : "minecraft:infiniburn_nether")
.putDouble("coordinate_scale", 1.0)
.putString("effects", "minecraft:overworld")
.putInt("min_y", 0)
.putInt("height", 256)
.putInt("monster_spawn_block_light_limit", 0)
.putInt("monster_spawn_light_level", 0)
.build();
if (version.compareTo(MINECRAFT_1_16_2) >= 0) {
return CompoundBinaryTag.builder()
.putString("name", "minecraft:overworld")
.putInt("id", 0)
.put("element", details)
.build();
}
return details.putString("name", "minecraft:overworld");
}
}
@@ -28,7 +28,10 @@ import java.util.function.Function;
public enum BlockType {
STONE(protocolVersion -> 1),
BARRIER(protocolVersion -> {
// Link: https://github.com/PrismarineJS/minecraft-data/blob/master/data/pc/1.20/blocks.json
// Useful resources:
// - https://github.com/PrismarineJS/minecraft-data/blob/master/data/pc/
// - https://pokechu22.github.io/Burger/1.20.4.html
// - https://github.com/ViaVersion/Mappings/tree/main/mappings
switch (protocolVersion) {
case MINECRAFT_1_7_2:
case MINECRAFT_1_7_6:
@@ -73,7 +76,8 @@ public enum BlockType {
case MINECRAFT_1_20:
return 10225;
case MINECRAFT_1_20_2:
return 1; // TODO: https://github.com/PrismarineJS/minecraft-data/issues/779
case MINECRAFT_1_20_3:
return 10366;
}
});
@@ -24,18 +24,15 @@ import org.jetbrains.annotations.NotNull;
public final class ChangedBlock {
private final @NotNull BlockPosition position;
private final BlockType type;
private final int legacyChunkPosCrammed;
private final int modernChunkPosCrammed;
private final int legacyBlockState, blockState;
public ChangedBlock(final @NotNull BlockPosition position, final BlockType type) {
this.position = position;
this.type = type;
this.legacyChunkPosCrammed = position.getX()
- (position.getChunkX() << 4) << 12 | position.getZ()
- (position.getChunkZ() << 4) << 8 | position.getY();
this.modernChunkPosCrammed = position.getX()
- (position.getChunkX() << 4) << 8 | position.getZ()
- (position.getChunkZ() << 4) << 4 | position.getY()
- ((position.getY() >> 4) << 4);
final int x = position.getX() - (position.getChunkX() << 4);
final int y = position.getY();
final int z = position.getZ() - (position.getChunkZ() << 4);
this.legacyBlockState = x << 12 | z << 8 | y;
this.blockState = x << 8 | z << 4 | y - ((y >> 4) << 4);
}
}
@@ -20,11 +20,13 @@ package xyz.jonesdev.sonar.common.fallback.protocol.dimension;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
import net.kyori.adventure.nbt.CompoundBinaryTag;
@Getter
@ToString
@RequiredArgsConstructor
public final class DimensionInfo {
private final String identifier, levelName;
private final boolean flat, debug;
private final String identifier;
private final int id;
private final CompoundBinaryTag tag;
}
@@ -0,0 +1,71 @@
/*
* Copyright (C) 2023 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.protocol.dimension;
import lombok.experimental.UtilityClass;
import net.kyori.adventure.nbt.BinaryTag;
import net.kyori.adventure.nbt.BinaryTagIO;
import net.kyori.adventure.nbt.CompoundBinaryTag;
import net.kyori.adventure.nbt.ListBinaryTag;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.Sonar;
import java.io.InputStream;
import java.util.Objects;
@UtilityClass
public final class DimensionRegistry {
public final DimensionInfo DEFAULT_DIMENSION_1_16;
public final DimensionInfo DEFAULT_DIMENSION_1_18_2;
public final @NotNull CompoundBinaryTag CODEC_1_16;
public final @NotNull CompoundBinaryTag CODEC_1_18_2;
public final @NotNull CompoundBinaryTag CODEC_1_19;
public final @NotNull CompoundBinaryTag CODEC_1_19_1;
public final @NotNull CompoundBinaryTag CODEC_1_19_4;
public final @NotNull CompoundBinaryTag CODEC_1_20;
public final @NotNull CompoundBinaryTag OLD_CODEC;
static {
CODEC_1_16 = getCodec("codec_1_16.nbt");
CODEC_1_18_2 = getCodec("codec_1_18_2.nbt");
CODEC_1_19 = getCodec("codec_1_19.nbt");
CODEC_1_19_1 = getCodec("codec_1_19_1.nbt");
CODEC_1_19_4 = getCodec("codec_1_19_4.nbt");
CODEC_1_20 = getCodec("codec_1_20.nbt");
OLD_CODEC = getCodec("codec_old.nbt");
DEFAULT_DIMENSION_1_16 = getDimension(CODEC_1_16);
DEFAULT_DIMENSION_1_18_2 = getDimension(CODEC_1_18_2);
}
private static @NotNull DimensionInfo getDimension(final @NotNull CompoundBinaryTag tag) {
final ListBinaryTag dimensions = tag.getCompound("minecraft:dimension_type").getList("value");
final BinaryTag elementTag = ((CompoundBinaryTag) dimensions.get(0)).get("element");
return new DimensionInfo("minecraft:overworld", 0, (CompoundBinaryTag) elementTag);
}
private @NotNull CompoundBinaryTag getCodec(final @NotNull String fileName) {
try (final InputStream inputStream = Sonar.class.getResourceAsStream("/codecs/" + fileName)) {
return BinaryTagIO.reader().read(Objects.requireNonNull(inputStream), BinaryTagIO.Compression.GZIP);
} catch (Throwable throwable) {
Sonar.get().getLogger().error("Could not load mappings for {}: {}", fileName, throwable);
throw new IllegalStateException(throwable);
}
}
}
@@ -0,0 +1,83 @@
/*
* Copyright (C) 2023 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.protocol.map;
import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import java.util.function.Function;
@SuppressWarnings("unused")
@RequiredArgsConstructor
public enum ItemMapType {
FILLED_MAP(protocolVersion -> {
// Useful resources:
// - https://github.com/PrismarineJS/minecraft-data/blob/master/data/pc/
// - https://pokechu22.github.io/Burger/1.20.4.html
// - https://github.com/ViaVersion/Mappings/tree/main/mappings
switch (protocolVersion) {
default:
// 1.7-1.12.2
return 358;
case MINECRAFT_1_13:
case MINECRAFT_1_13_1:
return 608;
case MINECRAFT_1_13_2:
return 613;
case MINECRAFT_1_14:
case MINECRAFT_1_14_1:
case MINECRAFT_1_14_2:
case MINECRAFT_1_14_3:
case MINECRAFT_1_14_4:
case MINECRAFT_1_15:
case MINECRAFT_1_15_1:
case MINECRAFT_1_15_2:
return 671;
case MINECRAFT_1_16:
case MINECRAFT_1_16_1:
case MINECRAFT_1_16_2:
case MINECRAFT_1_16_3:
case MINECRAFT_1_16_4:
return 733;
case MINECRAFT_1_17:
case MINECRAFT_1_17_1:
case MINECRAFT_1_18:
case MINECRAFT_1_18_2:
return 847;
case MINECRAFT_1_19:
case MINECRAFT_1_19_1:
return 886;
case MINECRAFT_1_19_3:
return 914;
case MINECRAFT_1_19_4:
return 937;
case MINECRAFT_1_20:
case MINECRAFT_1_20_2:
return 941;
case MINECRAFT_1_20_3:
return 979;
}
});
private final Function<ProtocolVersion, Integer> function;
public int getId(final @NotNull ProtocolVersion protocolVersion) {
return function.apply(protocolVersion);
}
}
@@ -15,25 +15,16 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.velocity.verbose;
package xyz.jonesdev.sonar.common.fallback.protocol.map;
import com.velocitypowered.api.proxy.ProxyServer;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import net.kyori.adventure.text.Component;
import xyz.jonesdev.sonar.api.verbose.Verbose;
@Getter
@RequiredArgsConstructor
public final class VerboseWrapper extends Verbose {
private final ProxyServer server;
@Override
public void broadcast(final Component component) {
synchronized (subscribers) {
for (final String subscriber : subscribers) {
server.getPlayer(subscriber).ifPresent(player -> {
player.sendActionBar(component);
});
}
}
}
public final class MapInfo {
private final String answer;
private final int columns, rows;
private final int x, y;
private final byte[] buffer;
}
@@ -0,0 +1,154 @@
/*
* Copyright (C) 2023 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.protocol.map;
import lombok.experimental.UtilityClass;
import xyz.jonesdev.sonar.api.Sonar;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.util.Random;
@UtilityClass
public class MapInfoPreparer {
private final Random RANDOM = new Random();
private final String[] FONT_TYPES = new String[]{
Font.DIALOG_INPUT,
Font.DIALOG,
Font.SANS_SERIF,
Font.SERIF
};
private final int[] FONT_STYLES = new int[]{
Font.PLAIN,
Font.BOLD,
Font.ITALIC,
Font.ITALIC | Font.BOLD
};
private final int[][] COLOR_PALETTE = new int[][]{
new int[]{ // Blue
48,
49,
50,
51
},
new int[]{ // Black
27,
45,
46,
47,
},
new int[]{ // Green
4,
28,
29,
30,
},
new int[]{ // Red
16,
17,
18,
19
}
};
private PreparedMapInfo[] cached;
public void prepare() {
cached = new PreparedMapInfo[Sonar.get().getConfig().getVerification().getMap().getPrecomputeAmount()];
final String dictionary = Sonar.get().getConfig().getVerification().getMap().getDictionary();
for (int i = 0; i < cached.length; i++) {
// Create image
final BufferedImage image = new BufferedImage(PreparedMapInfo.DIMENSIONS, PreparedMapInfo.DIMENSIONS,
BufferedImage.TYPE_3BYTE_BGR);
final Graphics2D graphics = image.createGraphics();
graphics.setColor(Color.WHITE);
// Create random font
final String fontType = FONT_TYPES[RANDOM.nextInt(FONT_TYPES.length)];
final int fontStyle = FONT_STYLES[RANDOM.nextInt(FONT_STYLES.length)];
final int fontSize = 30
+ (Sonar.get().getConfig().getVerification().getMap().isRandomizeFontSize() ? RANDOM.nextInt(11) : 5);
@SuppressWarnings("all") final Font answerFont = new Font(fontType, fontStyle, fontSize);
graphics.setFont(answerFont);
// Build answer to the captcha
final StringBuilder answerBuilder = new StringBuilder();
for (int _i = 0; _i < 4; _i++) {
answerBuilder.append(dictionary.charAt(RANDOM.nextInt(dictionary.length())));
}
final String answer = answerBuilder.toString();
// Calculate text position
final int stringWidth = graphics.getFontMetrics().stringWidth(answer);
int _x = image.getWidth() / 2 - stringWidth / 2;
int _y = image.getHeight() / 2 + fontSize / 3;
int randomOffsetX = 0, randomOffsetY = 3;
final int firstPixelX = _x, firstPixelY = _y;
// Draw each character one by one
final FontMetrics fontMetrics = graphics.getFontMetrics();
for (final char c : answer.toCharArray()) {
if (Sonar.get().getConfig().getVerification().getMap().isRandomizePositions()) {
_x += randomOffsetX;
_y += randomOffsetY;
randomOffsetX = randomOffsetX < 0 ? 1 + RANDOM.nextInt(2) : -1 - RANDOM.nextInt(2);
randomOffsetY = randomOffsetY < 0 ? 1 + RANDOM.nextInt(4) : -1 - RANDOM.nextInt(4);
}
final String character = String.valueOf(c);
graphics.drawString(character, _x, _y);
_x += fontMetrics.stringWidth(character);
}
// Select random color palette
final int[] colorPalette = COLOR_PALETTE[i % COLOR_PALETTE.length];
// Calculate y and height
final int pixelY = firstPixelY - fontSize;
final int height = firstPixelY + fontSize / 3;
// Store image in buffer
final byte[] buffer = new byte[PreparedMapInfo.SCALE];
for (int x = firstPixelX; x < _x; x++) {
for (int y = pixelY; y < height; y++) {
final int index = y * image.getWidth() + x;
final int pixel = image.getRGB(x, y);
// If the pixel has no color set, set it to white/light gray (if enabled in the config)
if (pixel == -16777216) {
if (Sonar.get().getConfig().getVerification().getMap().isDrawBackgroundNoise()) {
buffer[index] = (byte) (RANDOM.nextInt(100) < 70 ? 14 : 26);
}
continue;
}
// Set color of pixel to random color from the palette
buffer[index] = (byte) colorPalette[RANDOM.nextInt(colorPalette.length)];
}
}
// Cache buffer to map
cached[i] = new PreparedMapInfo(answer, image.getWidth(), image.getHeight(), buffer);
}
}
public PreparedMapInfo getRandomCaptcha() {
return cached[RANDOM.nextInt(cached.length)];
}
}
@@ -0,0 +1,65 @@
/*
* Copyright (C) 2023 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.protocol.map;
import lombok.Getter;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.MapData;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_8;
@Getter
public final class PreparedMapInfo {
public static final int DIMENSIONS = (int) Math.pow(2, 7);
public static final int SCALE = DIMENSIONS * DIMENSIONS;
private final MapInfo info;
private final MapData[] legacy;
private final MapData modern;
public PreparedMapInfo(final String answer,
final int columns, final int rows,
final byte @NotNull [] buffer) {
this.info = new MapInfo(answer, columns, rows, 0, 0, buffer);
// Prepare 1.7 map data using a grid
final byte[][] grid = new byte[DIMENSIONS][DIMENSIONS];
for (int i = 0; i < buffer.length; i++) {
final byte buf = buffer[i];
grid[i & Byte.MAX_VALUE][i >> 7] = buf;
}
this.legacy = new MapData[grid.length];
for (int i = 0; i < grid.length; i++) {
this.legacy[i] = new MapData(new MapInfo(answer, DIMENSIONS, DIMENSIONS, i, 0, grid[i]));
}
// Prepare 1.8+ map data
this.modern = new MapData(info);
}
public void write(final @NotNull FallbackUser<?, ?> user) {
if (user.getProtocolVersion().compareTo(MINECRAFT_1_8) < 0) {
for (final MapData data : legacy) {
user.delayedWrite(data);
}
} else {
user.delayedWrite(modern);
}
}
}
@@ -18,38 +18,26 @@
package xyz.jonesdev.sonar.common.fallback.protocol.packets.config;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufOutputStream;
import io.netty.handler.codec.EncoderException;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
import net.kyori.adventure.nbt.BinaryTagTypes;
import net.kyori.adventure.nbt.CompoundBinaryTag;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.dimension.DimensionRegistry;
import java.io.IOException;
import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.writeNamelessCompoundTag;
@Getter
@ToString
@NoArgsConstructor
@AllArgsConstructor
public final class RegistrySync implements FallbackPacket {
private CompoundBinaryTag dimensionRegistry;
public final class RegistryData implements FallbackPacket {
@Override
public void encode(final @NotNull ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
try (final ByteBufOutputStream stream = new ByteBufOutputStream(byteBuf)) {
stream.writeByte(10); // CompoundTag ID
BinaryTagTypes.COMPOUND.write(dimensionRegistry, stream);
} catch (IOException exception) {
throw new EncoderException("Cannot write NBT CompoundTag");
}
writeNamelessCompoundTag(byteBuf, DimensionRegistry.CODEC_1_20);
}
@Override
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
throw new UnsupportedOperationException();
}
}
@@ -31,7 +31,7 @@ import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
@NoArgsConstructor
@AllArgsConstructor
public class Abilities implements FallbackPacket {
private byte encodedFlags;
private int encodedFlags;
private float flySpeed, walkSpeed;
@Override
@@ -17,48 +17,88 @@
package xyz.jonesdev.sonar.common.fallback.protocol.packets.play;
import com.google.gson.JsonParser;
import io.netty.buffer.ByteBuf;
import io.netty.handler.codec.CorruptedFrameException;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
import net.kyori.adventure.nbt.BinaryTag;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.json.JSONComponentSerializer;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
import java.time.Instant;
import java.util.UUID;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.writeString;
import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.writeUUID;
import static xyz.jonesdev.sonar.common.utility.nbt.NBTMessageUtil.fromJson;
import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.*;
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.readVarInt;
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.writeVarInt;
@Getter
@ToString
@Data
@NoArgsConstructor
@AllArgsConstructor
public final class Chat implements FallbackPacket {
private static final UUID PLACEHOLDER_UUID = new UUID(0L, 0L);
private static final int DIV_FLOOR = -Math.floorDiv(-20, 8);
public static final byte CHAT_TYPE = (byte) 0;
public static final byte SYSTEM_TYPE = (byte) 1;
public static final byte GAME_INFO_TYPE = (byte) 2;
private Component component;
private byte position;
private String message;
private byte type;
private boolean signedPreview;
private boolean unsigned = false;
private Instant timestamp;
private Instant expiry;
private long salt;
private boolean signed;
private byte[] signature;
// 1.20.3
private BinaryTag nbtMessage;
// Clientbound LegacyChat
public Chat(final Component component) {
this(component, SYSTEM_TYPE);
}
// Clientbound LegacyChat
public Chat(final Component component, final byte type) {
this(component, JSONComponentSerializer.json().serialize(component), type);
}
public Chat(final Component component, final String message, final byte type) {
this(component, message, type,
false, false, null, null,
0L, false, null, fromJson(new JsonParser().parse(message)));
}
@Override
public void encode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
final String serialized = JSONComponentSerializer.json().serialize(component);
writeString(byteBuf, serialized);
// Message type
if (protocolVersion.compareTo(MINECRAFT_1_19_1) >= 0) {
byteBuf.writeBoolean(position == 2);
} else if (protocolVersion.compareTo(MINECRAFT_1_19) >= 0) {
writeVarInt(byteBuf, position);
// Serialized message
if (protocolVersion.compareTo(MINECRAFT_1_20_3) >= 0) {
writeNamelessCompoundTag(byteBuf, nbtMessage);
} else {
byteBuf.writeByte(this.position);
writeString(byteBuf, message);
}
// UUID
// Type
if (protocolVersion.compareTo(MINECRAFT_1_19_1) >= 0) {
byteBuf.writeBoolean(type == GAME_INFO_TYPE);
} else if (protocolVersion.compareTo(MINECRAFT_1_19) >= 0) {
writeVarInt(byteBuf, type);
} else if (protocolVersion.compareTo(MINECRAFT_1_8) >= 0) {
byteBuf.writeByte(type);
}
// Sender
if (protocolVersion.compareTo(MINECRAFT_1_16) >= 0
&& protocolVersion.compareTo(MINECRAFT_1_19) < 0) {
writeUUID(byteBuf, PLACEHOLDER_UUID);
@@ -66,7 +106,62 @@ public final class Chat implements FallbackPacket {
}
@Override
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
throw new UnsupportedOperationException();
public void decode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
message = readString(byteBuf, 256);
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_19) >= 0) {
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_19_1) <= 0) {
final long expiresAt = byteBuf.readLong();
final long saltLong = byteBuf.readLong();
final byte[] signatureBytes = readByteArray(byteBuf);
if (saltLong != 0L && signatureBytes.length > 0) {
signature = signatureBytes;
expiry = Instant.ofEpochMilli(expiresAt);
} else if ((protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_19_1) >= 0
|| saltLong == 0L) && signatureBytes.length == 0) {
unsigned = true;
} else {
throw new CorruptedFrameException("Invalid signature");
}
signedPreview = byteBuf.readBoolean();
if (signedPreview && unsigned) {
throw new CorruptedFrameException("Signature missing");
}
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_19_1) >= 0) {
final int size = readVarInt(byteBuf);
if (size < 0 || size > 5) {
throw new CorruptedFrameException("Invalid previous messages");
}
for (int i = 0; i < size; i++) {
readUUID(byteBuf);
readByteArray(byteBuf);
}
if (byteBuf.readBoolean()) {
readUUID(byteBuf);
readByteArray(byteBuf);
}
}
} else {
timestamp = Instant.ofEpochMilli(byteBuf.readLong());
salt = byteBuf.readLong();
signed = byteBuf.readBoolean();
if (signed) {
byte[] sign = new byte[256];
byteBuf.readBytes(sign);
signature = sign;
} else {
signature = new byte[0];
}
readVarInt(byteBuf);
byte[] bytes = new byte[DIV_FLOOR];
byteBuf.readBytes(bytes);
}
}
}
}
@@ -0,0 +1,62 @@
/*
* Copyright (C) 2023 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.protocol.packets.play;
import io.netty.buffer.ByteBuf;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_14;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_17;
@Getter
@ToString
@NoArgsConstructor
@AllArgsConstructor
public final class DefaultSpawnPosition implements FallbackPacket {
private int x, y, z;
@Override
public void encode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_8) < 0) {
byteBuf.writeInt(x);
byteBuf.writeInt(y);
byteBuf.writeInt(z);
} else {
final long encoded = protocolVersion.compareTo(MINECRAFT_1_14) < 0
? ((x & 0x3FFFFFFL) << 38) | ((y & 0xFFFL) << 26) | (z & 0x3FFFFFFL)
: ((x & 0x3FFFFFFL) << 38) | ((y & 0x3FFFFFFL) << 12) | (z & 0xFFFL);
byteBuf.writeLong(encoded);
if (protocolVersion.compareTo(MINECRAFT_1_17) >= 0) {
byteBuf.writeFloat(0f);
}
}
}
@Override
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
throw new UnsupportedOperationException();
}
}
@@ -17,11 +17,13 @@
package xyz.jonesdev.sonar.common.fallback.protocol.packets.play;
import com.google.gson.JsonParser;
import io.netty.buffer.ByteBuf;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
import net.kyori.adventure.nbt.BinaryTag;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.json.JSONComponentSerializer;
import org.jetbrains.annotations.NotNull;
@@ -29,8 +31,8 @@ import org.jetbrains.annotations.Nullable;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
import java.util.Objects;
import static xyz.jonesdev.sonar.common.utility.nbt.NBTMessageUtil.fromJson;
import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.writeNamelessCompoundTag;
import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.writeString;
@Getter
@@ -39,10 +41,15 @@ import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.writeStrin
@AllArgsConstructor
public final class Disconnect implements FallbackPacket {
private @Nullable String reason;
private BinaryTag binaryTag;
@Override
public void encode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
writeString(byteBuf, Objects.requireNonNull(reason));
public void encode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_20_3) >= 0) {
writeNamelessCompoundTag(byteBuf, binaryTag);
} else {
writeString(byteBuf, reason);
}
}
@Override
@@ -51,6 +58,8 @@ public final class Disconnect implements FallbackPacket {
}
public static @NotNull Disconnect create(final Component component) {
return new Disconnect(JSONComponentSerializer.json().serialize(component));
final String serialized = JSONComponentSerializer.json().serialize(component);
final BinaryTag binaryTag = fromJson(new JsonParser().parse(serialized));
return new Disconnect(serialized, binaryTag);
}
}
@@ -29,8 +29,7 @@ import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.writeArray;
import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.writeCompoundTag;
import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.*;
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.writeVarInt;
@Getter
@@ -56,10 +55,14 @@ public final class EmptyChunkData implements FallbackPacket {
}
private static @NotNull CompoundBinaryTag prepareNBT(final boolean legacy) {
final long[] arrayData = new long[legacy ? 36 : 37];
final LongArrayBinaryTag longArray = LongArrayBinaryTag.longArrayBinaryTag(arrayData);
final CompoundBinaryTag motion = CompoundBinaryTag.builder()
.put("MOTION_BLOCKING", longArray)
.build();
return CompoundBinaryTag.builder()
.put("root", CompoundBinaryTag.builder()
.put("MOTION_BLOCKING", LongArrayBinaryTag.longArrayBinaryTag(new long[legacy ? 36 : 37]))
.build())
.put("root", motion)
.build();
}
@@ -68,9 +71,10 @@ public final class EmptyChunkData implements FallbackPacket {
byteBuf.writeInt(x);
byteBuf.writeInt(z);
if (protocolVersion.compareTo(MINECRAFT_1_17) >= 0
&& protocolVersion.compareTo(MINECRAFT_1_17_1) <= 0) {
writeVarInt(byteBuf, 0); // mask
if (protocolVersion.compareTo(MINECRAFT_1_17) >= 0) {
if (protocolVersion.compareTo(MINECRAFT_1_17_1) <= 0) {
writeVarInt(byteBuf, 0); // mask
}
} else {
byteBuf.writeBoolean(true); // full chunk
@@ -87,7 +91,11 @@ public final class EmptyChunkData implements FallbackPacket {
}
if (protocolVersion.compareTo(MINECRAFT_1_14) >= 0) {
writeCompoundTag(byteBuf, protocolVersion.compareTo(MINECRAFT_1_18) < 0 ? LEGACY_TAG : MODERN_TAG);
if (protocolVersion.compareTo(MINECRAFT_1_20_2) >= 0) {
writeNamelessCompoundTag(byteBuf, MODERN_TAG);
} else {
writeCompoundTag(byteBuf, protocolVersion.compareTo(MINECRAFT_1_18) < 0 ? LEGACY_TAG : MODERN_TAG);
}
if (protocolVersion.compareTo(MINECRAFT_1_15) >= 0 && protocolVersion.compareTo(MINECRAFT_1_18) < 0) {
if (protocolVersion.compareTo(MINECRAFT_1_16_2) >= 0) {
@@ -0,0 +1,45 @@
/*
* Copyright (C) 2023 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.protocol.packets.play;
import io.netty.buffer.ByteBuf;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
@Data
@NoArgsConstructor
@AllArgsConstructor
public final class GameEvent implements FallbackPacket {
private int type;
private float value;
@Override
public void encode(final @NotNull ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
byteBuf.writeByte(type);
byteBuf.writeFloat(value);
}
@Override
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
throw new UnsupportedOperationException();
}
}
@@ -21,168 +21,203 @@ import io.netty.buffer.ByteBuf;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import net.kyori.adventure.nbt.CompoundBinaryTag;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.dimension.DimensionInfo;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
import static xyz.jonesdev.sonar.common.fallback.protocol.dimension.DimensionRegistry.*;
import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.*;
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.writeVarInt;
// Mostly taken from
// https://github.com/Nan1t/NanoLimbo/blob/main/src/main/java/ua/nanit/limbo/protocol/packets/play/PacketJoinGame.java
@Data
@NoArgsConstructor
@AllArgsConstructor
public final class JoinGame implements FallbackPacket {
private int entityId;
private short gamemode;
private int dimension;
private long partialHashedSeed; // 1.15+
private short difficulty;
private int gamemode;
private long partialHashedSeed;
private boolean isHardcore;
private @Nullable String levelType;
private int viewDistance; // 1.14+
private int viewDistance;
private boolean reducedDebugInfo;
private boolean showRespawnScreen;
private boolean limitedCrafting; // 1.20.2+
private String[] levelNames; // 1.16+
private CompoundBinaryTag registry; // 1.16+
private DimensionInfo dimensionInfo; // 1.16+
private CompoundBinaryTag currentDimensionData; // 1.16.2+
private short previousGamemode; // 1.16+
private int simulationDistance; // 1.18+
private boolean limitedCrafting;
private String[] levelNames;
private String levelName;
// Mostly taken from
// https://github.com/Nan1t/NanoLimbo/blob/main/src/main/java/ua/nanit/limbo/protocol/packets/play/PacketJoinGame.java
@Override
public void encode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
public void encode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
byteBuf.writeInt(entityId);
if (protocolVersion.compareTo(MINECRAFT_1_9) <= 0) {
byteBuf.writeByte(gamemode);
byteBuf.writeByte(dimension);
if (protocolVersion.inBetween(MINECRAFT_1_7_2, MINECRAFT_1_7_6)) {
byteBuf.writeByte(gamemode == 3 ? 1 : gamemode);
byteBuf.writeByte(DEFAULT_DIMENSION_1_16.getId());
byteBuf.writeByte(0); // difficulty
byteBuf.writeByte(1); // max players
byteBuf.writeByte(0); // max players
writeString(byteBuf, "flat"); // level type
if (protocolVersion.compareTo(MINECRAFT_1_8) >= 0) {
byteBuf.writeBoolean(reducedDebugInfo);
}
}
if (protocolVersion.compareTo(MINECRAFT_1_9_1) >= 0
&& protocolVersion.compareTo(MINECRAFT_1_13_2) <= 0) {
if (protocolVersion.inBetween(MINECRAFT_1_8, MINECRAFT_1_9)) {
byteBuf.writeByte(gamemode);
byteBuf.writeInt(dimension);
byteBuf.writeByte(DEFAULT_DIMENSION_1_16.getId());
byteBuf.writeByte(0); // difficulty
byteBuf.writeByte(1); // max players
byteBuf.writeByte(0); // max players
writeString(byteBuf, "flat"); // level type
byteBuf.writeBoolean(reducedDebugInfo);
}
if (protocolVersion.compareTo(MINECRAFT_1_14) >= 0
&& protocolVersion.compareTo(MINECRAFT_1_14_4) <= 0) {
if (protocolVersion.inBetween(MINECRAFT_1_9_1, MINECRAFT_1_13_2)) {
byteBuf.writeByte(gamemode);
byteBuf.writeInt(dimension);
byteBuf.writeByte(1); // max players
byteBuf.writeInt(DEFAULT_DIMENSION_1_16.getId());
byteBuf.writeByte(0); // difficulty
byteBuf.writeByte(0); // max players
writeString(byteBuf, "flat"); // level type
byteBuf.writeBoolean(reducedDebugInfo);
}
if (protocolVersion.inBetween(MINECRAFT_1_14, MINECRAFT_1_14_4)) {
byteBuf.writeByte(gamemode);
byteBuf.writeInt(DEFAULT_DIMENSION_1_16.getId());
byteBuf.writeByte(0); // max players
writeString(byteBuf, "flat"); // level type
writeVarInt(byteBuf, viewDistance);
byteBuf.writeBoolean(reducedDebugInfo);
}
if (protocolVersion.compareTo(MINECRAFT_1_15) >= 0
&& protocolVersion.compareTo(MINECRAFT_1_15_2) <= 0) {
if (protocolVersion.inBetween(MINECRAFT_1_15, MINECRAFT_1_15_2)) {
byteBuf.writeByte(gamemode);
byteBuf.writeInt(dimension);
byteBuf.writeInt(DEFAULT_DIMENSION_1_16.getId());
byteBuf.writeLong(partialHashedSeed);
byteBuf.writeByte(1); // max players
byteBuf.writeByte(0); // max players
writeString(byteBuf, "flat"); // level type
writeVarInt(byteBuf, viewDistance);
byteBuf.writeBoolean(reducedDebugInfo);
byteBuf.writeBoolean(showRespawnScreen);
}
if (protocolVersion.compareTo(MINECRAFT_1_16) >= 0
&& protocolVersion.compareTo(MINECRAFT_1_16_1) <= 0) {
if (protocolVersion.inBetween(MINECRAFT_1_16, MINECRAFT_1_16_1)) {
byteBuf.writeByte(gamemode);
byteBuf.writeByte(previousGamemode);
byteBuf.writeByte(-1); // previous gamemode
writeStringArray(byteBuf, levelNames);
writeCompoundTag(byteBuf, registry);
writeString(byteBuf, dimensionInfo.getIdentifier());
writeString(byteBuf, dimensionInfo.getLevelName());
writeCompoundTag(byteBuf, OLD_CODEC);
writeString(byteBuf, DEFAULT_DIMENSION_1_16.getIdentifier());
writeString(byteBuf, levelName);
byteBuf.writeLong(partialHashedSeed);
byteBuf.writeByte(1); // max players
byteBuf.writeByte(0); // max players
writeVarInt(byteBuf, viewDistance);
byteBuf.writeBoolean(reducedDebugInfo);
byteBuf.writeBoolean(showRespawnScreen);
byteBuf.writeBoolean(dimensionInfo.isDebug());
byteBuf.writeBoolean(dimensionInfo.isFlat());
byteBuf.writeBoolean(false); // debug type
byteBuf.writeBoolean(false); // flat
}
if (protocolVersion.compareTo(MINECRAFT_1_16_2) >= 0
&& protocolVersion.compareTo(MINECRAFT_1_18_2) <= 0) {
if (protocolVersion.inBetween(MINECRAFT_1_16_2, MINECRAFT_1_17_1)) {
byteBuf.writeBoolean(isHardcore);
byteBuf.writeByte(gamemode);
byteBuf.writeByte(previousGamemode);
byteBuf.writeByte(-1); // previous gamemode
writeStringArray(byteBuf, levelNames);
writeCompoundTag(byteBuf, registry);
writeCompoundTag(byteBuf, currentDimensionData);
writeString(byteBuf, dimensionInfo.getIdentifier());
writeCompoundTag(byteBuf, CODEC_1_16);
writeCompoundTag(byteBuf, DEFAULT_DIMENSION_1_16.getTag());
writeString(byteBuf, levelName);
byteBuf.writeLong(partialHashedSeed);
writeVarInt(byteBuf, 1); // max players
writeVarInt(byteBuf, 0); // max players
writeVarInt(byteBuf, viewDistance);
if (protocolVersion.compareTo(MINECRAFT_1_18) >= 0) {
writeVarInt(byteBuf, viewDistance); // simulation distance
}
byteBuf.writeBoolean(reducedDebugInfo);
byteBuf.writeBoolean(showRespawnScreen);
byteBuf.writeBoolean(dimensionInfo.isDebug());
byteBuf.writeBoolean(dimensionInfo.isFlat());
byteBuf.writeBoolean(false); // debug type
byteBuf.writeBoolean(false); // flat
}
if (protocolVersion.compareTo(MINECRAFT_1_19) >= 0
&& protocolVersion.compareTo(MINECRAFT_1_20) <= 0) {
if (protocolVersion.inBetween(MINECRAFT_1_18, MINECRAFT_1_18_2)) {
byteBuf.writeBoolean(isHardcore);
byteBuf.writeByte(gamemode);
byteBuf.writeByte(previousGamemode);
byteBuf.writeByte(-1); // previous gamemode
writeStringArray(byteBuf, levelNames);
writeCompoundTag(byteBuf, registry);
writeString(byteBuf, dimensionInfo.getIdentifier());
writeString(byteBuf, dimensionInfo.getLevelName());
if (protocolVersion.compareTo(MINECRAFT_1_18_2) >= 0) {
writeCompoundTag(byteBuf, CODEC_1_18_2);
writeCompoundTag(byteBuf, DEFAULT_DIMENSION_1_18_2.getTag());
} else {
writeCompoundTag(byteBuf, CODEC_1_16);
writeCompoundTag(byteBuf, DEFAULT_DIMENSION_1_16.getTag());
}
writeString(byteBuf, levelName);
byteBuf.writeLong(partialHashedSeed);
writeVarInt(byteBuf, 1); // max players
writeVarInt(byteBuf, 0); // max players
writeVarInt(byteBuf, viewDistance);
writeVarInt(byteBuf, viewDistance); // simulation distance
byteBuf.writeBoolean(reducedDebugInfo);
byteBuf.writeBoolean(showRespawnScreen);
byteBuf.writeBoolean(dimensionInfo.isDebug());
byteBuf.writeBoolean(dimensionInfo.isFlat());
byteBuf.writeBoolean(false); // last death location
byteBuf.writeBoolean(false); // debug type
byteBuf.writeBoolean(false); // flat
}
if (protocolVersion.compareTo(MINECRAFT_1_20) == 0) {
writeVarInt(byteBuf, 0); // portal cooldown
if (protocolVersion.inBetween(MINECRAFT_1_19, MINECRAFT_1_19_4)) {
byteBuf.writeBoolean(isHardcore);
byteBuf.writeByte(gamemode);
byteBuf.writeByte(-1); // previous gamemode
writeStringArray(byteBuf, levelNames);
if (protocolVersion.compareTo(MINECRAFT_1_19_1) >= 0) {
if (protocolVersion.compareTo(MINECRAFT_1_19_4) >= 0) {
writeCompoundTag(byteBuf, CODEC_1_19_4);
} else {
writeCompoundTag(byteBuf, CODEC_1_19_1);
}
} else {
writeCompoundTag(byteBuf, CODEC_1_19);
}
writeString(byteBuf, levelName); // world type
writeString(byteBuf, levelName);
byteBuf.writeLong(partialHashedSeed);
writeVarInt(byteBuf, 0); // max players
writeVarInt(byteBuf, viewDistance);
writeVarInt(byteBuf, viewDistance); // simulation distance
byteBuf.writeBoolean(reducedDebugInfo);
byteBuf.writeBoolean(showRespawnScreen);
byteBuf.writeBoolean(false); // debug type
byteBuf.writeBoolean(false); // flat
byteBuf.writeBoolean(false); // no last death location
}
if (protocolVersion.equals(MINECRAFT_1_20)) {
byteBuf.writeBoolean(isHardcore);
byteBuf.writeByte(gamemode);
byteBuf.writeByte(-1); // previous gamemode
writeStringArray(byteBuf, levelNames);
writeCompoundTag(byteBuf, CODEC_1_20);
writeString(byteBuf, levelName); // world type
writeString(byteBuf, levelName);
byteBuf.writeLong(partialHashedSeed);
writeVarInt(byteBuf, 0); // max players
writeVarInt(byteBuf, viewDistance);
writeVarInt(byteBuf, viewDistance); // simulation distance
byteBuf.writeBoolean(reducedDebugInfo);
byteBuf.writeBoolean(showRespawnScreen);
byteBuf.writeBoolean(false); // debug type
byteBuf.writeBoolean(false); // flat
byteBuf.writeBoolean(false); // no last death location
writeVarInt(byteBuf, 0);
}
if (protocolVersion.compareTo(MINECRAFT_1_20_2) >= 0) {
byteBuf.writeBoolean(isHardcore);
writeStringArray(byteBuf, levelNames);
writeVarInt(byteBuf, 1); // max players
writeVarInt(byteBuf, 0); // max players
writeVarInt(byteBuf, viewDistance);
writeVarInt(byteBuf, viewDistance); // simulation distance
byteBuf.writeBoolean(reducedDebugInfo);
byteBuf.writeBoolean(showRespawnScreen);
byteBuf.writeBoolean(limitedCrafting);
writeString(byteBuf, dimensionInfo.getIdentifier());
writeString(byteBuf, dimensionInfo.getLevelName());
byteBuf.writeBoolean(false); // limited crafting
writeString(byteBuf, levelName);
writeString(byteBuf, levelName);
byteBuf.writeLong(partialHashedSeed);
byteBuf.writeByte(gamemode);
byteBuf.writeByte(previousGamemode);
byteBuf.writeBoolean(dimensionInfo.isDebug());
byteBuf.writeBoolean(dimensionInfo.isFlat());
byteBuf.writeBoolean(false); // last death location
writeVarInt(byteBuf, 0); // portal cooldown
byteBuf.writeByte(-1); // previous gamemode
byteBuf.writeBoolean(false); // debug type
byteBuf.writeBoolean(false); // flat
byteBuf.writeBoolean(false); // no last death location
writeVarInt(byteBuf, 0);
}
}
@@ -0,0 +1,82 @@
/*
* Copyright (C) 2023 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.protocol.packets.play;
import io.netty.buffer.ByteBuf;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.map.MapInfo;
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.writeVarInt;
@Getter
@ToString
@NoArgsConstructor
@AllArgsConstructor
public final class MapData implements FallbackPacket {
private MapInfo mapInfo;
@Override
public void encode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
writeVarInt(byteBuf, 0);
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_8) < 0) {
byteBuf.writeShort(mapInfo.getBuffer().length + 3);
byteBuf.writeByte(0); // scaling
byteBuf.writeByte(mapInfo.getX());
byteBuf.writeByte(mapInfo.getY());
byteBuf.writeBytes(mapInfo.getBuffer());
} else {
byteBuf.writeByte(0); // scaling
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_9) >= 0
&& protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_17) < 0) {
byteBuf.writeBoolean(false);
}
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_14) >= 0) {
byteBuf.writeBoolean(false);
}
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_17) >= 0) {
byteBuf.writeBoolean(false);
} else {
writeVarInt(byteBuf, 0);
}
byteBuf.writeByte(mapInfo.getColumns());
byteBuf.writeByte(mapInfo.getRows());
byteBuf.writeByte(mapInfo.getX());
byteBuf.writeByte(mapInfo.getY());
writeVarInt(byteBuf, mapInfo.getBuffer().length);
byteBuf.writeBytes(mapInfo.getBuffer());
}
}
@Override
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
throw new UnsupportedOperationException();
}
}
@@ -0,0 +1,95 @@
/*
* Copyright (C) 2023 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.protocol.packets.play;
import io.netty.buffer.ByteBuf;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
import net.kyori.adventure.nbt.CompoundBinaryTag;
import net.kyori.adventure.nbt.IntBinaryTag;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.writeCompoundTag;
import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.writeNamelessCompoundTag;
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.writeVarInt;
@Getter
@ToString
@NoArgsConstructor
@AllArgsConstructor
public final class SetSlot implements FallbackPacket {
private int windowId;
private int slot;
private int count;
private int data;
private int itemId;
private CompoundBinaryTag compoundBinaryTag;
public static final CompoundBinaryTag MAP_NBT = CompoundBinaryTag.builder()
.put("map", IntBinaryTag.intBinaryTag(0)) // map type
.build();
@Override
public void decode(final @NotNull ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
throw new UnsupportedOperationException();
}
@Override
public void encode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
byteBuf.writeByte(windowId);
if (protocolVersion.compareTo(MINECRAFT_1_17_1) >= 0) {
writeVarInt(byteBuf, 0);
}
byteBuf.writeShort(slot);
if (protocolVersion.compareTo(MINECRAFT_1_13_2) >= 0) {
byteBuf.writeBoolean(true);
}
if (protocolVersion.compareTo(MINECRAFT_1_13_2) < 0) {
byteBuf.writeShort(itemId);
} else {
writeVarInt(byteBuf, itemId);
}
byteBuf.writeByte(count);
if (protocolVersion.compareTo(MINECRAFT_1_13) < 0) {
byteBuf.writeShort(data);
}
if (protocolVersion.compareTo(MINECRAFT_1_17) < 0) {
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_8) < 0) {
byteBuf.writeShort(-1);
} else {
byteBuf.writeByte(0);
}
} else {
if (protocolVersion.compareTo(MINECRAFT_1_20_2) >= 0) {
writeNamelessCompoundTag(byteBuf, compoundBinaryTag);
} else {
writeCompoundTag(byteBuf, compoundBinaryTag);
}
}
}
}
@@ -53,12 +53,12 @@ public final class UpdateSectionBlocks implements FallbackPacket {
}
for (final ChangedBlock block : changedBlocks) {
byteBuf.writeShort(block.getLegacyChunkPosCrammed());
final int id = block.getType().getId(protocolVersion);
byteBuf.writeShort(block.getLegacyBlockState());
final int blockId = block.getType().getId(protocolVersion);
if (protocolVersion.compareTo(MINECRAFT_1_13) >= 0) {
writeVarInt(byteBuf, id);
writeVarInt(byteBuf, blockId);
} else {
final int shiftedBlockId = id << 4;
final int shiftedBlockId = blockId << 4;
if (protocolVersion.compareTo(MINECRAFT_1_8) < 0) {
byteBuf.writeShort(shiftedBlockId);
} else {
@@ -67,8 +67,10 @@ public final class UpdateSectionBlocks implements FallbackPacket {
}
}
} else {
// We only need one Y position
final int sectionY = changedBlocks[0].getPosition().getY() >> 4;
// https://wiki.vg/Protocol#Update_Section_Blocks
byteBuf.writeLong(((sectionX & 0x3FFFFFL) << 42) | (sectionY & 0xFFFFF) | ((sectionZ & 0x3FFFFFL) << 20));
// 1.20+ don't have light update suppression
@@ -79,8 +81,9 @@ public final class UpdateSectionBlocks implements FallbackPacket {
writeVarInt(byteBuf, changedBlocks.length);
for (final ChangedBlock block : changedBlocks) {
// https://wiki.vg/Protocol#Update_Section_Blocks
final int shiftedBlockId = block.getType().getId(protocolVersion) << 12;
final long positionIdValue = shiftedBlockId | block.getModernChunkPosCrammed();
final long positionIdValue = shiftedBlockId | block.getBlockState();
writeVarLong(byteBuf, positionIdValue);
}
}
@@ -0,0 +1,59 @@
/*
* Copyright (C) 2023 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.service;
import lombok.experimental.UtilityClass;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.fallback.traffic.TrafficCounter;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
@UtilityClass
public class ServiceRepository {
private final ScheduledExecutorService QUEUE_SERVICE = Executors.newSingleThreadScheduledExecutor();
private final ScheduledExecutorService TRAFFIC_SERVICE = Executors.newSingleThreadScheduledExecutor();
private final ScheduledExecutorService VERBOSE_SERVICE = Executors.newSingleThreadScheduledExecutor();
private boolean registered;
public void register() {
if (registered) {
throw new IllegalStateException("Already registered");
}
registered = true;
TRAFFIC_SERVICE.scheduleAtFixedRate(TrafficCounter::reset,
1L, 1L, TimeUnit.SECONDS);
QUEUE_SERVICE.scheduleAtFixedRate(Sonar.get().getFallback().getQueue()::poll,
500L, 500L, TimeUnit.MILLISECONDS);
VERBOSE_SERVICE.scheduleAtFixedRate(Sonar.get().getVerboseHandler()::update,
200L, 200L, TimeUnit.MILLISECONDS);
}
public synchronized void shutdown() {
if (!registered) {
throw new IllegalStateException("Not registered");
}
registered = false;
QUEUE_SERVICE.shutdown();
TRAFFIC_SERVICE.shutdown();
VERBOSE_SERVICE.shutdown();
}
}
@@ -26,7 +26,6 @@ public final class SubcommandRegistryHolder implements SubcommandRegistry {
new BlacklistCommand(),
new VerifiedCommand(),
new StatisticsCommand(),
new LockdownCommand(),
new VerboseCommand(),
new ReloadCommand(),
new DumpCommand()
@@ -39,7 +39,7 @@ import java.net.InetAddress;
public final class BlacklistCommand extends Subcommand {
@Override
public void execute(final @NotNull CommandInvocation invocation) {
protected void execute(final @NotNull CommandInvocation invocation) {
switch (invocation.getRawArguments()[1].toLowerCase()) {
case "add": {
if (invocation.getRawArguments().length <= 2) {
@@ -49,12 +49,12 @@ public final class BlacklistCommand extends Subcommand {
}
final String rawInetAddress = invocation.getRawArguments()[2];
final InetAddress inetAddress = checkIP(invocation.getSender(), rawInetAddress);
final InetAddress inetAddress = getInetAddressIfValid(invocation.getSender(), rawInetAddress);
// Make sure the given IP address is valid
if (inetAddress == null) return;
// Make sure the IP is not blacklisted already
if (SONAR.getFallback().getBlacklisted().has(inetAddress.toString())) {
if (SONAR.getFallback().getBlacklisted().has(inetAddress)) {
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getBlacklistDuplicate());
return;
}
@@ -66,7 +66,7 @@ public final class BlacklistCommand extends Subcommand {
}
// Blacklist the given IP address
SONAR.getFallback().getBlacklisted().put(inetAddress.toString());
SONAR.getFallback().getBlacklisted().put(inetAddress);
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getBlacklistAdd()
.replace("%ip%", rawInetAddress));
break;
@@ -80,18 +80,18 @@ public final class BlacklistCommand extends Subcommand {
}
final String rawInetAddress = invocation.getRawArguments()[2];
final InetAddress inetAddress = checkIP(invocation.getSender(), rawInetAddress);
final InetAddress inetAddress = getInetAddressIfValid(invocation.getSender(), rawInetAddress);
// Make sure the given IP address is valid
if (inetAddress == null) return;
// Make sure the IP is blacklisted
if (!SONAR.getFallback().getBlacklisted().has(inetAddress.toString())) {
if (!SONAR.getFallback().getBlacklisted().has(inetAddress)) {
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getBlacklistNotFound());
return;
}
// Invalidate the cache entry of the blacklisted IP address
SONAR.getFallback().getBlacklisted().invalidate(inetAddress.toString());
SONAR.getFallback().getBlacklisted().invalidate(inetAddress);
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getBlacklistRemove()
.replace("%ip%", rawInetAddress));
break;
@@ -17,12 +17,11 @@
package xyz.jonesdev.sonar.common.subcommand.impl;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import lombok.RequiredArgsConstructor;
import lombok.val;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Unmodifiable;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.SonarPlatform;
import xyz.jonesdev.sonar.api.command.CommandInvocation;
import xyz.jonesdev.sonar.api.command.subcommand.Subcommand;
@@ -33,7 +32,7 @@ import java.util.List;
import java.util.Map;
import java.util.WeakHashMap;
// This command is only used to display helpful information in order to help
// This command is only used to display helpful information to help
// the Sonar contributors to fix issues more quickly
@SubcommandInfo(
name = "dump",
@@ -41,13 +40,10 @@ import java.util.WeakHashMap;
onlyConsole = true
)
public final class DumpCommand extends Subcommand implements JVMProfiler {
private static final Gson GSON = new GsonBuilder()
.disableInnerClassSerialization()
.create();
@Override
public void execute(final @NotNull CommandInvocation invocation) {
final String json = GSON.toJson(collectMappedInformation());
protected void execute(final @NotNull CommandInvocation invocation) {
final String json = Sonar.GENERAL_GSON.toJson(collectMappedInformation());
SONAR.getLogger().info("Generated dump: {}", json);
}
@@ -56,7 +52,7 @@ public final class DumpCommand extends Subcommand implements JVMProfiler {
val mappings = new WeakHashMap<String, Object>();
mappings.put("sonar", new Dump.Sonar(
SONAR.getVersion().getFull(),
SONAR.getServer().getPlatform(),
SONAR.getPlatform(),
SONAR.getVersion().isOnMainBranch()
));
mappings.put("runtime", new Dump.Runtime(
@@ -1,41 +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.common.subcommand.impl;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.command.CommandInvocation;
import xyz.jonesdev.sonar.api.command.subcommand.Subcommand;
import xyz.jonesdev.sonar.api.command.subcommand.SubcommandInfo;
@SubcommandInfo(
name = "lockdown",
description = "Prevent all players from joining"
)
public final class LockdownCommand extends Subcommand {
@Override
public void execute(final @NotNull CommandInvocation invocation) {
final boolean newState = !SONAR.getConfig().getLockdown().isEnabled();
SONAR.getConfig().getLockdown().setEnabled(newState);
SONAR.getConfig().getGeneralConfig().set("general.lockdown.enabled", newState);
SONAR.getConfig().getGeneralConfig().save();
invocation.getSender().sendMessage(newState
? SONAR.getConfig().getLockdown().getActivated() : SONAR.getConfig().getLockdown().getDeactivated());
}
}
@@ -30,7 +30,7 @@ import xyz.jonesdev.sonar.api.timer.SystemTimer;
public final class ReloadCommand extends Subcommand {
@Override
public void execute(final @NotNull CommandInvocation invocation) {
protected void execute(final @NotNull CommandInvocation invocation) {
final SystemTimer timer = new SystemTimer();
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getReloading());
@@ -48,7 +48,7 @@ public final class StatisticsCommand extends Subcommand implements JVMProfiler {
}
@Override
public void execute(final @NotNull CommandInvocation invocation) {
protected void execute(final @NotNull CommandInvocation invocation) {
StatisticType type = StatisticType.GENERAL;
if (invocation.getRawArguments().length >= 2) {
try {
@@ -17,46 +17,30 @@
package xyz.jonesdev.sonar.common.subcommand.impl;
import net.kyori.adventure.text.Component;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.command.CommandInvocation;
import xyz.jonesdev.sonar.api.command.InvocationSource;
import xyz.jonesdev.sonar.api.command.subcommand.Subcommand;
import xyz.jonesdev.sonar.api.command.subcommand.SubcommandInfo;
import java.util.Optional;
@SubcommandInfo(
name = "verbose",
description = "Enable and disable Sonar verbose",
description = "Enable or disable Sonar verbose",
onlyPlayers = true
)
public final class VerboseCommand extends Subcommand {
@Override
public void execute(final @NotNull CommandInvocation invocation) {
InvocationSource verboseSubscriber = invocation.getSender();
// Support for '/sonar verbose [username]'
if (invocation.getRawArguments().length >= 2) {
final Optional<InvocationSource> optional = SONAR.getServer().getOnlinePlayer(invocation.getRawArguments()[1]);
if (optional.isPresent()) verboseSubscriber = optional.get();
}
if (SONAR.getVerboseHandler().isSubscribed(verboseSubscriber.getName())) {
SONAR.getVerboseHandler().unsubscribe(verboseSubscriber.getName());
if (verboseSubscriber != invocation.getSender()) {
verboseSubscriber.sendMessage(SONAR.getConfig().getCommands().getVerboseUnsubscribedOther()
.replace("%player%", verboseSubscriber.getName()));
}
verboseSubscriber.sendMessage(SONAR.getConfig().getCommands().getVerboseUnsubscribed());
protected void execute(final @NotNull CommandInvocation invocation) {
if (SONAR.getVerboseHandler().isSubscribed(invocation.getSender().getName())) {
SONAR.getVerboseHandler().unsubscribe(invocation.getSender().getName());
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getVerboseUnsubscribed());
// Reset ActionBar component when unsubscribing
invocation.getSender().getAudience().sendActionBar(Component.empty());
return;
}
if (verboseSubscriber != invocation.getSender()) {
verboseSubscriber.sendMessage(SONAR.getConfig().getCommands().getVerboseSubscribedOther()
.replace("%player%", verboseSubscriber.getName()));
}
verboseSubscriber.sendMessage(SONAR.getConfig().getCommands().getVerboseSubscribed());
SONAR.getVerboseHandler().subscribe(verboseSubscriber.getName());
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getVerboseSubscribed());
SONAR.getVerboseHandler().subscribe(invocation.getSender().getName());
}
}
@@ -43,7 +43,7 @@ public final class VerifiedCommand extends Subcommand {
private static final Queue<String> LOCK = new LinkedBlockingQueue<>(1);
@Override
public void execute(final @NotNull CommandInvocation invocation) {
protected void execute(final @NotNull CommandInvocation invocation) {
switch (invocation.getRawArguments()[1].toLowerCase()) {
case "history": {
if (invocation.getRawArguments().length <= 2) {
@@ -53,7 +53,7 @@ public final class VerifiedCommand extends Subcommand {
}
final String rawInetAddress = invocation.getRawArguments()[2];
final InetAddress inetAddress = checkIP(invocation.getSender(), rawInetAddress);
final InetAddress inetAddress = getInetAddressIfValid(invocation.getSender(), rawInetAddress);
// Make sure the given IP address is valid
if (inetAddress == null) return;
@@ -85,7 +85,7 @@ public final class VerifiedCommand extends Subcommand {
return;
}
final InetAddress inetAddress = checkIP(invocation.getSender(), rawInetAddress);
final InetAddress inetAddress = getInetAddressIfValid(invocation.getSender(), rawInetAddress);
// Make sure the given IP address is valid
if (inetAddress == null) return;
@@ -1,3 +1,20 @@
/*
* Copyright (C) 2023 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.update;
import com.google.gson.JsonObject;
@@ -38,24 +55,39 @@ public class UpdateChecker {
}
};
private int convertVersion(final @NotNull String version) throws NumberFormatException, IllegalStateException {
final String[] convertedParts = version.split("\\.");
// Validate the length of the converted parts
if (convertedParts.length != 3) {
throw new IllegalStateException("Converted version length mismatch");
}
// Major updates are more important than minor updates,
// and minor updates are more important than patches.
final int convertedMajor = Integer.parseInt(convertedParts[0]) * 100000; // multiply by weight
final int convertedMinor = Integer.parseInt(convertedParts[1]) * 10000; // multiply by weight
final int patch = Integer.parseInt(convertedParts[2]);
return convertedMajor + convertedMinor + patch; // sum the converted version parts
}
public void checkForUpdates() {
ASYNC_EXECUTOR.execute(() -> {
try {
final URL url = new URL("https://api.github.com/repos/jonesdevelopment/sonar/releases/latest");
final HttpsURLConnection urlConnection = prepareConnection(url);
final JsonObject json = parseJson(urlConnection.getInputStream());
final String latestStableVersion = json.get("tag_name").getAsString();
final JsonObject json = parseJson(urlConnection);
final String latestStableRelease = json.get("tag_name").getAsString();
final int convertedLatestVersion = convertVersion(latestStableRelease);
final int convertedCurrentVersion = convertVersion(Sonar.get().getVersion().getSemanticVersion());
if (!latestStableVersion.equals(Sonar.get().getVersion().getSemanticVersion())) {
LOGGER.warn("A new version of Sonar is available: {}", latestStableVersion);
if (convertedCurrentVersion < convertedLatestVersion) {
LOGGER.warn("A new version of Sonar is available: {}", latestStableRelease);
LOGGER.warn("Please make sure to update to the latest version to ensure stability and security:");
LOGGER.warn("https://github.com/jonesdevelopment/sonar/releases");
} else if (Sonar.get().getVersion().isOnMainBranch()) {
LOGGER.info("You are currently using the latest stable release of Sonar!");
LOGGER.warn("https://github.com/jonesdevelopment/sonar/releases/tag/{}", latestStableRelease);
} else if (convertedCurrentVersion > convertedLatestVersion || !Sonar.get().getVersion().isOnMainBranch()) {
LOGGER.warn("You are currently using an unreleased version of Sonar!");
LOGGER.warn("The contributors of Sonar are not responsible for any damage done by using an unstable version");
} else {
LOGGER.warn("You are currently using the latest development version of Sonar!");
LOGGER.warn("The contributors of Sonar are not responsible for any damage done by using an unstable version" +
".");
LOGGER.info("You are currently using the latest stable release of Sonar!");
}
} catch (Exception exception) {
LOGGER.warn("Could not retrieve latest version information: {}", exception);
@@ -63,22 +95,25 @@ public class UpdateChecker {
});
}
private @NotNull JsonObject parseJson(final @NotNull InputStream inputStream) throws IOException {
final InputStreamReader inputStreamReader = new InputStreamReader(inputStream);
final StringBuilder content = new StringBuilder();
private @NotNull JsonObject parseJson(final @NotNull HttpsURLConnection urlConnection) throws IOException {
try (final InputStream inputStream = urlConnection.getInputStream()) {
try (final InputStreamReader inputStreamReader = new InputStreamReader(inputStream)) {
final StringBuilder content = new StringBuilder();
// Read the entire page content
try (final BufferedReader bufferedReader = new BufferedReader(inputStreamReader)) {
String line;
// Read the entire page content
try (final BufferedReader bufferedReader = new BufferedReader(inputStreamReader)) {
String line;
while ((line = bufferedReader.readLine()) != null) {
content.append(line);
while ((line = bufferedReader.readLine()) != null) {
content.append(line);
}
}
// Parse site content as JSON using Gson
final JsonParser jsonParser = new JsonParser();
return jsonParser.parse(content.toString()).getAsJsonObject();
}
}
// Parse site content as JSON using Gson
final JsonParser jsonParser = new JsonParser();
return jsonParser.parse(content.toString()).getAsJsonObject();
}
private @NotNull HttpsURLConnection prepareConnection(final @NotNull URL url) throws Exception {
@@ -18,6 +18,7 @@
package xyz.jonesdev.sonar.common.utility.geyser;
import io.netty.channel.Channel;
import io.netty.util.AttributeKey;
import lombok.experimental.UtilityClass;
import org.jetbrains.annotations.NotNull;
@@ -27,20 +28,20 @@ import org.jetbrains.annotations.NotNull;
@UtilityClass
public class GeyserUtil {
/*
* Geyser attribute key for every floodgate player
*/
private AttributeKey<Object> playerAttribute() {
return AttributeKey.valueOf("floodgate-player");
}
/**
* Checks if Geyser holds the parent channel of the player
*
* @param channel Channel of the player
* @return Whether the player is on GeyserMC or not
*/
public boolean isGeyserConnection(final @NotNull Channel channel) {
// Get the parent channel of the original channel
final Channel parent = channel.parent();
// This shouldn't happen, but we want to stay safe here
if (parent == null) return false;
final Class<? extends Channel> clazz = parent.getClass();
// Check if Geyser adapted the channel by checking for the package name
return clazz.getCanonicalName().startsWith("org.geysermc");
// https://discord.com/channels/613163671870242838/613170125696270357/1168599123889504266
// Every floodgate player has a channel attribute called "floodgate-player"
return channel.attr(playerAttribute()).get() != null;
}
}
@@ -0,0 +1,115 @@
/*
* Copyright (C) 2023 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.utility.nbt;
import com.google.gson.*;
import lombok.experimental.UtilityClass;
import net.kyori.adventure.nbt.*;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
// Taken from
// https://github.com/Nan1t/NanoLimbo/pull/79/files#diff-0e355ffd4eb0e15e2c2e628227debc9b6ccefeb167d7b9619311cdc7db2645ec
@UtilityClass
public class NBTMessageUtil {
public static BinaryTag fromJson(final JsonElement json) {
if (json instanceof JsonPrimitive) {
JsonPrimitive jsonPrimitive = (JsonPrimitive) json;
if (jsonPrimitive.isNumber()) {
Number number = json.getAsNumber();
if (number instanceof Byte) {
return ByteBinaryTag.byteBinaryTag((Byte) number);
} else if (number instanceof Short) {
return ShortBinaryTag.shortBinaryTag((Short) number);
} else if (number instanceof Integer) {
return IntBinaryTag.intBinaryTag((Integer) number);
} else if (number instanceof Long) {
return LongBinaryTag.longBinaryTag((Long) number);
} else if (number instanceof Float) {
return FloatBinaryTag.floatBinaryTag((Float) number);
} else if (number instanceof Double) {
return DoubleBinaryTag.doubleBinaryTag((Double) number);
}
} else if (jsonPrimitive.isString()) {
return StringBinaryTag.stringBinaryTag(jsonPrimitive.getAsString());
} else if (jsonPrimitive.isBoolean()) {
return ByteBinaryTag.byteBinaryTag(jsonPrimitive.getAsBoolean() ? (byte) 1 : (byte) 0);
} else {
throw new IllegalArgumentException("Unknown JSON primitive: " + jsonPrimitive);
}
} else if (json instanceof JsonObject) {
final CompoundBinaryTag.Builder builder = CompoundBinaryTag.builder();
for (Map.Entry<String, JsonElement> property : ((JsonObject) json).entrySet()) {
builder.put(property.getKey(), fromJson(property.getValue()));
}
return builder.build();
} else if (json instanceof JsonArray) {
final JsonArray jsonArray = ((JsonArray) json);
if (jsonArray.size() == 0) {
return ListBinaryTag.listBinaryTag(EndBinaryTag.endBinaryTag().type(), Collections.emptyList());
}
final BinaryTagType<?> tagByteType = ByteBinaryTag.ZERO.type();
final BinaryTagType<?> tagIntType = IntBinaryTag.intBinaryTag(0).type();
final BinaryTagType<?> tagLongType = LongBinaryTag.longBinaryTag(0).type();
final BinaryTag listTag;
final BinaryTagType<?> listType = fromJson(jsonArray.get(0)).type();
if (listType.equals(tagByteType)) {
byte[] bytes = new byte[jsonArray.size()];
for (int i = 0; i < bytes.length; i++) {
bytes[i] = (Byte) jsonArray.get(i).getAsNumber();
}
listTag = ByteArrayBinaryTag.byteArrayBinaryTag(bytes);
} else if (listType.equals(tagIntType)) {
int[] ints = new int[jsonArray.size()];
for (int i = 0; i < ints.length; i++) {
ints[i] = (Integer) jsonArray.get(i).getAsNumber();
}
listTag = IntArrayBinaryTag.intArrayBinaryTag(ints);
} else if (listType.equals(tagLongType)) {
long[] longs = new long[jsonArray.size()];
for (int i = 0; i < longs.length; i++) {
longs[i] = (Long) jsonArray.get(i).getAsNumber();
}
listTag = LongArrayBinaryTag.longArrayBinaryTag(longs);
} else {
final List<BinaryTag> tagItems = new ArrayList<>(jsonArray.size());
for (final JsonElement jsonEl : jsonArray) {
final BinaryTag subTag = fromJson(jsonEl);
if (subTag.type() != listType) {
throw new IllegalArgumentException("Cannot convert mixed JsonArray to Tag");
}
tagItems.add(subTag);
}
listTag = ListBinaryTag.listBinaryTag(listType, tagItems);
}
return listTag;
} else if (json instanceof JsonNull) {
return EndBinaryTag.endBinaryTag();
}
throw new IllegalArgumentException("Unknown JSON element: " + json);
}
}
@@ -24,8 +24,7 @@ import io.netty.handler.codec.CorruptedFrameException;
import io.netty.handler.codec.DecoderException;
import io.netty.handler.codec.EncoderException;
import lombok.experimental.UtilityClass;
import net.kyori.adventure.nbt.BinaryTagIO;
import net.kyori.adventure.nbt.CompoundBinaryTag;
import net.kyori.adventure.nbt.*;
import org.jetbrains.annotations.NotNull;
import java.io.DataOutput;
@@ -34,6 +33,8 @@ import java.nio.charset.StandardCharsets;
import java.util.UUID;
import java.util.regex.Pattern;
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.readVarInt;
// Taken from
// https://github.com/PaperMC/Velocity/blob/dev/3.0.0/proxy/src/main/java/com/velocitypowered/proxy/protocol/ProtocolUtils.java
@UtilityClass
@@ -48,6 +49,24 @@ public class ProtocolUtil {
private static final String UNREGISTER_CHANNEL = "minecraft:unregister";
private static final Pattern INVALID_IDENTIFIER_REGEX = Pattern.compile("[^a-z0-9\\-_]*");
public static @NotNull UUID readUUID(final @NotNull ByteBuf byteBuf) {
return new UUID(byteBuf.readLong(), byteBuf.readLong());
}
public static byte @NotNull [] readByteArray(final ByteBuf byteBuf) {
return readByteArray(byteBuf, Short.MAX_VALUE);
}
public static byte @NotNull [] readByteArray(final ByteBuf byteBuf, final int cap) {
int length = readVarInt(byteBuf);
checkFrame(length >= 0, "Got a negative-length array");
checkFrame(length <= cap, "Bad array size");
checkFrame(byteBuf.isReadable(length), "Trying to read an array that is too long");
byte[] array = new byte[length];
byteBuf.readBytes(array);
return array;
}
public static @NotNull String readBrandMessage(final @NotNull ByteBuf content) throws DecoderException {
final ByteBuf slice = content.slice();
try {
@@ -113,7 +132,46 @@ public class ProtocolUtil {
public static void writeCompoundTag(final ByteBuf byteBuf, final CompoundBinaryTag compoundTag) {
try {
BinaryTagIO.writer().write(compoundTag, (DataOutput) new ByteBufOutputStream(byteBuf));
} catch (IOException e) {
} catch (IOException exception) {
throw new EncoderException("Unable to encode NBT CompoundTag");
}
}
// Taken from
// https://github.com/Nan1t/NanoLimbo/pull/79/files#diff-4aa8208044741102c6326c7e85086e6fa8fcc7c064f7df6fd0411baf5f2b4504
public static void writeNamelessCompoundTag(final @NotNull ByteBuf byteBuf, final @NotNull BinaryTag binaryTag) {
try (final ByteBufOutputStream output = new ByteBufOutputStream(byteBuf)) {
// TODO: Find a way to improve this...
output.writeByte(binaryTag.type().id());
if (binaryTag instanceof CompoundBinaryTag) {
CompoundBinaryTag tag = (CompoundBinaryTag) binaryTag;
tag.type().write(tag, output);
} else if (binaryTag instanceof ByteBinaryTag) {
ByteBinaryTag tag = (ByteBinaryTag) binaryTag;
tag.type().write(tag, output);
} else if (binaryTag instanceof ShortBinaryTag) {
ShortBinaryTag tag = (ShortBinaryTag) binaryTag;
tag.type().write(tag, output);
} else if (binaryTag instanceof IntBinaryTag) {
IntBinaryTag tag = (IntBinaryTag) binaryTag;
tag.type().write(tag, output);
} else if (binaryTag instanceof LongBinaryTag) {
LongBinaryTag tag = (LongBinaryTag) binaryTag;
tag.type().write(tag, output);
} else if (binaryTag instanceof DoubleBinaryTag) {
DoubleBinaryTag tag = (DoubleBinaryTag) binaryTag;
tag.type().write(tag, output);
} else if (binaryTag instanceof StringBinaryTag) {
StringBinaryTag tag = (StringBinaryTag) binaryTag;
tag.type().write(tag, output);
} else if (binaryTag instanceof ListBinaryTag) {
ListBinaryTag tag = (ListBinaryTag) binaryTag;
tag.type().write(tag, output);
} else if (binaryTag instanceof EndBinaryTag) {
EndBinaryTag tag = (EndBinaryTag) binaryTag;
tag.type().write(tag, output);
}
} catch (IOException exception) {
throw new EncoderException("Unable to encode NBT CompoundTag");
}
}
Binary file not shown.
Loaded 100 of 111 files, more files were not shown because too many files have changed in this diff. Show more