Compare commits

...
35 Commits
Author SHA1 Message Date
Jones 5068ebcd91 Release 2.0.7 (#89) 2023-11-01 13:48:08 +01:00
Jones f20d162dcc Fix Geyser exemption not working on the standalone variant (#88)
* Revert "https://github.com/jonesdevelopment/sonar/pull/87/commits/648489b01bafcaf711316228fac6d93eac5eba2c"

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

* fix: move UserVerifyFailedEvent publishing after channel active check

* fix: make UserVerifyFailedEvent reason optional

* fix: use final field for UserVerifyFailedEvent reason

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

* fix: normal floodgate players not being detected

* fix: Discord webhook spam

* feat: update value for minimum players for attack

* feat: make minimum attack duration configurable

* feat: implement attack start and stop thresholds

* fix: discord webhook embed description not being reusable

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

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

* feat: [wip] add Discord Webhook support

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

* fix: run most of the webhook attack handling async

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

* fix: don't log too-short attacks

* fix: duration formatting showing milliseconds

* feat: implement webhook embed message footer

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

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

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

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

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

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

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

* feat: [wip] implement Fallback for BungeeCord

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

* fix(bungee): NPE on handshake

* fix: use correct protocol version for channel initialization

* feat: implement login packet spam fix

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

* feat: implement cleaner FallbackHandlerBoss

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

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

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

* fix(bungee): total traffic statistic not working

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

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

* style: fix FallbackChannelHandler comments

* style: don't annotate FallbackUserWrapper as EqualsAndHashCode

* style: fix ToString annotation in EmptyChunkData

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

* feat: use expiring cache for joins per second counter

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

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

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

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

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

* feat: remove /sonar verbose [other player]

* feat: remove ServerWrapper and replace it with the platform

* feat: reset action bar when unsubscribing from verbose

* feat: recode tab suggestions and help message caching

* feat: make Sonar main command translatable/configurable

* feat: don't cache copyright year

* fix: tab suggestions breaking after the first iteration

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

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

* style: improve codestyle in YAML configuration loader

* fix: argumentsRequired property not working at all

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

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

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

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

* feat: remove ActiveFeatures packet

* refactor: create packages for each state

* feat: fully recode JoinGame and RegistrySync packets

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

* fix: encoder/decoder state not being set properly

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

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

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

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

* feat: add todo for 1.20.2 barrier block mappings

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

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

* feat: add already-online disconnect message translation

* feat: implement 1.20.2 support for JoinGame packet

* feat: add new packets for 1.20.2 config state

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

* fix: implement backwards compatibility for Velocity b265 and below

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

* feat: make lockdown bypass permission customizable

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

* feat: [wip] implement config protocol state

* style: add comments for documenting FallbackPacketDecoder

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

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

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

* fix: invalid configuration path for certain settings

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

* feat: update configuration yaml file header

* fix: smaller configuration fixes and relocations

* feat: migrate messages to modern MiniMessage formatting

* fix: verbose not deserializing the component properly

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

* feat: remove unused postReload task/method

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

* feat: make /sonar stats completely configurable

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

* fix: use correct translation for /sonar stats cpu

* style: remove resolved todo comment

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

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

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

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

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

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

This reverts commit 6b092100befa423272f1eb310f5d1f8a0e9afb82.

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

* feat: update several subcommand descriptions to be more accurate

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

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

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

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

* feat: optimize/improve Fallback packet preparing

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

* fix: optimize 1.7 support in UpdateSectionBlocks packet

* style: improve codestyle of the UpdateSectionBlocks packet

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

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

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

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

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

* refactor: setAbilitiesAndTeleport -> sendAbilitiesAndTeleport

* style: fix fields being named incorrectly

* feat: cache 1.7 empty chunk filler bytes

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

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

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

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

* fix: false positive with invalid movement ticks

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

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

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

No files matched your search

+3 -3
View File
@@ -42,8 +42,8 @@
- [Fallback](https://github.com/jonesdevelopment/sonar#fallback) is Sonar's main component designed to prevent
all types of bots.
- Spigot & BungeeCord
- The compression method is changed
to [Velocity's libdeflate](https://github.com/PaperMC/Velocity/tree/dev/3.0.0/native).
- Duplicate login/status packet spam fix
- Run server ping handling asynchronously
- The Varint decoder is updated to Velocity's
improved [MinecraftVarintFrameDecoder](https://github.com/PaperMC/Velocity/blob/dev/3.0.0/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftVarintFrameDecoder.java).
@@ -91,4 +91,4 @@ Sonar is licensed under the [GNU General Public License 3.0](https://www.gnu.org
- Special thanks to the [contributors of Sonar](https://github.com/jonesdevelopment/sonar/graphs/contributors).
- The nbt mappings were taken from [LimboAPI](https://github.com/Elytrium/LimboAPI).
- The compression and Varint decoding was taken from [Velocity](https://github.com/PaperMC/Velocity).
- The Varint decoding was taken from [Velocity](https://github.com/PaperMC/Velocity).
+20 -10
View File
@@ -22,7 +22,7 @@ allprojects {
repositories {
mavenCentral() // Lombok
maven(url = "https://jitpack.io") // simple-yaml
maven(url = "https://repo.papermc.io/repository/maven-public") // Velocity natives
maven(url = "https://repo.papermc.io/repository/maven-public") // Velocity API
maven(url = "https://repo.jonesdev.xyz/releases/") // Bungee & Velocity proxy module
}
}
@@ -38,16 +38,18 @@ subprojects {
testCompileOnly("org.projectlombok:lombok:1.18.30")
testAnnotationProcessor("org.projectlombok:lombok:1.18.30")
// MiniMessage
implementation("net.kyori:adventure-text-minimessage:4.14.0")
implementation("net.kyori:adventure-text-serializer-gson:4.14.0")
implementation("net.kyori:adventure-text-serializer-legacy:4.14.0")
val adventureVersion = "4.14.0"
// adventure
implementation("net.kyori:adventure-text-minimessage:$adventureVersion")
implementation("net.kyori:adventure-text-serializer-gson:$adventureVersion")
// adventure nbt
implementation("net.kyori:adventure-nbt:$adventureVersion")
implementation("net.kyori:adventure-nbt:4.14.0") // nbt
implementation("com.j256.ormlite:ormlite-jdbc:6.1") // ORM
implementation("xyz.jonesdev:cappuccino:0.1.6-SNAPSHOT") // expiring cache
compileOnly("io.netty:netty-all:4.1.99.Final") // netty
compileOnly("io.netty:netty-all:4.1.100.Final") // netty
}
}
@@ -69,10 +71,18 @@ tasks {
}
shadowJar {
// bStats has to be relocated to the Sonar package otherwise it throws an exception.
relocate("org.bstats", "xyz.jonesdev.sonar.bstats")
// Set the file name of the shadowed jar
archiveFileName.set("${rootProject.name}.jar")
archiveFileName.set("Sonar.jar")
// bStats has to be relocated to the Sonar package otherwise it throws an exception
// https://github.com/Bastian/bstats-metrics/blob/master/base/src/main/java/org/bstats/MetricsBase.java#L251
relocate("org.bstats", "xyz.jonesdev.sonar.libs.bstats")
// https://github.com/jonesdevelopment/sonar/issues/46
// Relocate some packages, so we don't run into issues where we accidentally use Velocity's classes
relocate("net.kyori.adventure.nbt", "xyz.jonesdev.sonar.libs.nbt")
relocate("com.google.gson", "xyz.jonesdev.sonar.libs.gson")
relocate("com.j256.ormlite", "xyz.jonesdev.sonar.libs.ormlite")
}
compileJava {
+2 -1
View File
@@ -1,2 +1,3 @@
group=xyz.jonesdev.sonar
version=2.0.2
version=2.0.7
description=Effective Anti-bot plugin for Velocity, BungeeCord, and Bukkit (1.7-latest)
+1 -1
View File
@@ -1,4 +1,4 @@
rootProject.name = "Sonar"
rootProject.name = "sonar"
sequenceOf("api", "bukkit", "bungee", "common", "velocity").forEach {
val path = "sonar-$it"
+3 -1
View File
@@ -1,5 +1,7 @@
dependencies {
implementation("com.github.Carleslc.Simple-YAML:Simple-Yaml:1.8.4") // yaml config
implementation("com.github.Carleslc.Simple-YAML:Simple-Yaml:1.8.4") {
exclude(group = "org.yaml")
}
compileOnly("org.jetbrains:annotations:24.0.1")
}
@@ -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.AttackStatus;
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
@@ -57,6 +63,7 @@ public interface Sonar {
/**
* Set a custom verbose handler
*/
@SuppressWarnings("unused")
void setVerboseHandler(final @NotNull Verbose verboseHandler);
/**
@@ -64,20 +71,26 @@ public interface Sonar {
*/
void reload();
@NotNull
default SonarEventManager getEventManager() {
return SonarEventManager.INSTANCE;
}
@NotNull
default SonarVersion getVersion() {
return SonarVersion.GET;
}
@NotNull
default SonarEventManager getEventManager() {
// We don't want anyone else to create a new instance.
return SonarEventManager.INSTANCE;
}
@NotNull
default AttackStatus getAttackStatus() {
// We don't want anyone else to create a new instance.
return AttackStatus.INSTANCE;
}
@NotNull
default Fallback getFallback() {
// We only have one single instance of Fallback,
// and we don't want anyone else to create a new instance.
// We don't want anyone else to create a new instance.
return Fallback.INSTANCE;
}
@@ -0,0 +1,137 @@
/*
* 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 AttackStatus implements JVMProfiler {
public static final AttackStatus INSTANCE = new AttackStatus();
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;
}
public void checkIfUnderAttack() {
final int joinsPerSecond = Sonar.get().getVerboseHandler().getJoinsPerSecond().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();
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) {
currentAttack.peakJoinsPerSecond = joinsPerSecond;
}
final double processCPUUsage = getProcessCPUUsage();
if (processCPUUsage > currentAttack.peakProcessCPUUsage) {
currentAttack.peakProcessCPUUsage = processCPUUsage;
}
final long processMemoryUsage = getUsedMemory();
if (processMemoryUsage > currentAttack.peakProcessMemoryUsage) {
currentAttack.peakProcessMemoryUsage = processMemoryUsage;
}
} else if (currentAttack != null) {
if (currentAttack.duration.delay() > Sonar.get().getConfig().getMinAttackDuration()
&& currentAttack.timer.delay() > Sonar.get().getConfig().getAttackCooldownDelay()) {
// An 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);
// 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(Sonar.get().getFallback().getBlacklisted().estimatedSize()))
.replace("%total-failed%", Sonar.DECIMAL_FORMAT.format(Statistics.FAILED_VERIFICATIONS.get()))
.replace("%total-success%",
Sonar.DECIMAL_FORMAT.format(Sonar.get().getVerifiedPlayerController().estimatedSize()));
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("all")
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;
}
@@ -21,13 +21,14 @@ import lombok.Getter;
import lombok.RequiredArgsConstructor;
import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
import net.kyori.adventure.text.minimessage.MiniMessage;
@Getter
@RequiredArgsConstructor
public abstract class InvocationSource {
private final String name;
private final Audience audience;
private final boolean player;
/**
* Sends an empty chat message to the command executor
@@ -40,7 +41,7 @@ public abstract class InvocationSource {
* Sends a message to the command executor
*/
public final void sendMessage(final String legacy) {
sendMessage(LegacyComponentSerializer.legacyAmpersand().deserialize(legacy));
sendMessage(MiniMessage.miniMessage().deserialize(legacy));
}
/**
@@ -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,52 +38,55 @@ 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<>();
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)))));
}
default void cacheHelpMessage() {
CACHED_HELP_MESSAGE.addAll(Arrays.asList(
Component.text("Running Sonar " + Sonar.get().getVersion()
+ " on " + Sonar.get().getServer().getPlatform().getDisplayName()
+ ".", NamedTextColor.YELLOW),
Component.text("(C) " + COPYRIGHT_YEAR + " Jones Development and Sonar Contributors", NamedTextColor.YELLOW),
Component.text("https://github.com/jonesdevelopment/sonar", NamedTextColor.GREEN)
.clickEvent(net.kyori.adventure.text.event.ClickEvent.clickEvent(net.kyori.adventure.text.event.ClickEvent.Action.OPEN_URL, "https://github.com/jonesdevelopment/sonar")),
Component.empty(),
Component.text("Need help or have any questions?", NamedTextColor.YELLOW),
Component.textOfChildren(
Component.text("Open a ticket on the Discord ", NamedTextColor.YELLOW)
.hoverEvent(net.kyori.adventure.text.event.HoverEvent.hoverEvent(net.kyori.adventure.text.event.HoverEvent.Action.SHOW_TEXT, Component.text("(Click to open Discord)")))
.clickEvent(net.kyori.adventure.text.event.ClickEvent.clickEvent(net.kyori.adventure.text.event.ClickEvent.Action.OPEN_URL, "https://jonesdev.xyz/discord/")),
Component.text("or open a new issue on GitHub.", NamedTextColor.YELLOW)
.hoverEvent(net.kyori.adventure.text.event.HoverEvent.hoverEvent(net.kyori.adventure.text.event.HoverEvent.Action.SHOW_TEXT, Component.text("(Click to open GitHub)")))
.clickEvent(net.kyori.adventure.text.event.ClickEvent.clickEvent(net.kyori.adventure.text.event.ClickEvent.Action.OPEN_URL, "https://github.com/jonesdevelopment/sonar" +
"/issues"))
),
Component.empty()
));
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 = component.clickEvent(
net.kyori.adventure.text.event.ClickEvent.clickEvent(ClickEvent.Action.SUGGEST_COMMAND,
"/sonar " + sub.getInfo().name() + " ")
).hoverEvent(
net.kyori.adventure.text.event.HoverEvent.hoverEvent(HoverEvent.Action.SHOW_TEXT, Component.text(
"§7Only players: §f" + (sub.getInfo().onlyPlayers() ? "§a✔" : "§c✗")
+ Sonar.LINE_SEPARATOR + "§7Require console: §f" + (sub.getInfo().onlyConsole() ? "§a✔" : "§c✗")
+ Sonar.LINE_SEPARATOR + "§7Permission: §f" + sub.getPermission()
+ Sonar.LINE_SEPARATOR + "§7Aliases: §f" + sub.getAliases()
))
);
CACHED_HELP_MESSAGE.add(component);
final String subcommandFormat = Sonar.get().getConfig().getCommands().getHelpSubcommands();
Sonar.get().getSubcommandRegistry().getSubcommands().forEach(subcommand -> {
final Component deserialized = MiniMessage.miniMessage().deserialize(subcommandFormat
.replace("%subcommand%", subcommand.getInfo().name())
.replace("%description%", subcommand.getInfo().description())
.replace("%only_players%", subcommand.getInfo().onlyPlayers() ? "<green>✔</green>" : "<red>✗</red>")
.replace("%require_console%", subcommand.getInfo().onlyConsole() ? "<green>✔</green>" : "<red>✗</red>")
.replace("%permission%", subcommand.getPermission())
.replace("%aliases%", subcommand.getAliases()));
CACHED_HELP_MESSAGE.add(deserialized);
});
// Don't re-cache tab suggestions
if (!TAB_SUGGESTIONS.isEmpty()) return;
// Cache tab suggestions
for (final Subcommand subcommand : Sonar.get().getSubcommandRegistry().getSubcommands()) {
TAB_SUGGESTIONS.add(subcommand.getInfo().name());
if (subcommand.getInfo().aliases().length > 0) {
TAB_SUGGESTIONS.addAll(Arrays.asList(subcommand.getInfo().aliases()));
}
final List<String> parsedArguments = Arrays.stream(subcommand.getInfo().arguments())
.map(Argument::value)
.collect(Collectors.toList());
ARG_TAB_SUGGESTIONS.put(subcommand.getInfo().name(), parsedArguments);
for (final String alias : subcommand.getInfo().aliases()) {
ARG_TAB_SUGGESTIONS.put(alias, parsedArguments);
}
}
}
default List<String> getCachedTabSuggestions(final String @NotNull [] arguments) {
if (arguments.length <= 1) {
return TAB_SUGGESTIONS;
} else if (arguments.length == 2) {
final String subCommandName = arguments[0].toLowerCase();
return ARG_TAB_SUGGESTIONS.getOrDefault(subCommandName, emptyList());
}
return emptyList();
}
}
@@ -19,12 +19,16 @@ package xyz.jonesdev.sonar.api.command.subcommand;
import lombok.Getter;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.command.CommandInvocation;
import xyz.jonesdev.sonar.api.command.InvocationSource;
import xyz.jonesdev.sonar.api.command.subcommand.argument.Argument;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Arrays;
import java.util.Objects;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
@@ -32,10 +36,8 @@ import java.util.stream.Collectors;
public abstract class Subcommand {
private final @NotNull SubcommandInfo info;
private final String permission, aliases, arguments;
protected static final Sonar SONAR = Sonar.get();
protected static final Pattern IP_PATTERN = Pattern.compile(
"^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])([.])){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$"
);
public Subcommand() {
info = getClass().getAnnotation(SubcommandInfo.class);
@@ -49,10 +51,63 @@ public abstract class Subcommand {
.collect(Collectors.joining(", "));
}
// https://stackoverflow.com/questions/5284147/validating-ipv4-addresses-with-regexp
private static final Pattern IP_PATTERN = Pattern.compile("^((25[0-5]|(2[0-4]|1\\d|[1-9]|)\\d)\\.?\\b){4}$");
protected static @Nullable InetAddress getInetAddressIfValid(final InvocationSource source, final String rawIP) {
// We perform a regex check beforehand,
// so we don't have to create a new InetAddress,
// therefore, potentially saving some performance.
if (!IP_PATTERN.matcher(Objects.requireNonNull(rawIP)).matches()) {
source.sendMessage(SONAR.getConfig().getCommands().getIncorrectIpAddress());
return null;
}
final InetAddress inetAddress;
try {
inetAddress = InetAddress.getByName(rawIP);
if (inetAddress.isAnyLocalAddress() || inetAddress.isLoopbackAddress()) {
source.sendMessage(SONAR.getConfig().getCommands().getIllegalIpAddress());
return null;
}
} catch (UnknownHostException exception) {
source.sendMessage(SONAR.getConfig().getCommands().getUnknownIpAddress());
return null;
}
return inetAddress;
}
protected final void incorrectUsage(final @NotNull InvocationSource sender) {
sender.sendMessage(SONAR.getConfig().getIncorrectCommandUsage()
sender.sendMessage(SONAR.getConfig().getCommands().getIncorrectCommandUsage()
.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,9 +37,10 @@ public final class SimpleYamlConfig {
private static final List<String> HEADER = Arrays.asList(
"",
"Sonar anti-bot version " + Sonar.get().getVersion(),
" Currently running on: " + Sonar.get().getServer().getPlatform().getDisplayName(),
" Need help? https://jonesdev.xyz/discord",
String.format("Running Sonar version %s on %s",
Sonar.get().getVersion(), Sonar.get().getPlatform().getDisplayName()),
"Need help or have questions? https://jonesdev.xyz/discord",
"https://github.com/jonesdevelopment/sonar",
""
);
@@ -46,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?!");
}
@@ -73,9 +75,7 @@ public final class SimpleYamlConfig {
}
// Always load the comment format and options
yaml.setCommentFormat(YamlCommentFormat.PRETTY);
yaml.options().headerFormatter().commentPrefix("# ");
yaml.setCommentFormat(YamlCommentFormat.DEFAULT);
yaml.setHeader(String.join(LINE_SEPARATOR, HEADER));
}
File diff suppressed because it is too large. Load diff
@@ -45,14 +45,14 @@ public final class VerifiedPlayerController {
private Dao<VerifiedPlayer, Integer> dao;
private QueryBuilder<VerifiedPlayer, Integer> queryBuilder;
@Getter
private final @NotNull SonarConfiguration.DatabaseType cachedDatabaseType;
private final @NotNull SonarConfiguration.Database.Type cachedDatabaseType;
public VerifiedPlayerController() {
// Cache selected database type, so we don't need to call Sonar.get() every time
cachedDatabaseType = Sonar.get().getConfig().getDatabaseType();
cachedDatabaseType = Sonar.get().getConfig().getDatabase().getType();
// Don't establish a database connection if the type is NONE
if (cachedDatabaseType == SonarConfiguration.DatabaseType.NONE) {
if (cachedDatabaseType == SonarConfiguration.Database.Type.NONE) {
Sonar.get().getLogger().warn("Make sure to configure a database to save verified players.");
return;
}
@@ -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);
@@ -87,12 +89,17 @@ public final class VerifiedPlayerController {
_remove(inetAddress);
// Don't try to update the column if the database type is NONE
if (cachedDatabaseType == SonarConfiguration.DatabaseType.NONE) {
if (cachedDatabaseType == SonarConfiguration.Database.Type.NONE) {
return;
}
DB_UPDATE_SERVICE.execute(() -> {
Objects.requireNonNull(connectionSource);
// We cannot throw a NullPointerException within the executor service
// because we want to handle the error instead of simply throwing an exception
if (connectionSource == null) {
Sonar.get().getLogger().error("Error (DB_UPDATE_SERVICE): connectionSource is null");
return;
}
try {
final List<VerifiedPlayer> verifiedPlayer = queryBuilder.where()
@@ -129,12 +136,17 @@ public final class VerifiedPlayerController {
_add(player);
// Don't try to update the column if the database type is NONE
if (cachedDatabaseType == SonarConfiguration.DatabaseType.NONE) {
if (cachedDatabaseType == SonarConfiguration.Database.Type.NONE) {
return;
}
DB_UPDATE_SERVICE.execute(() -> {
Objects.requireNonNull(connectionSource);
// We cannot throw a NullPointerException within the executor service
// because we want to handle the error instead of simply throwing an exception
if (connectionSource == null) {
Sonar.get().getLogger().error("Error (DB_UPDATE_SERVICE): connectionSource is null");
return;
}
try {
dao.create(player);
@@ -179,7 +191,7 @@ public final class VerifiedPlayerController {
MAP.clear();
// Only update the column if the database type is not NONE
if (cachedDatabaseType != SonarConfiguration.DatabaseType.NONE) {
if (cachedDatabaseType != SonarConfiguration.Database.Type.NONE) {
dao.deleteBuilder().delete();
}
} catch (SQLException exception) {
@@ -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,
@@ -38,24 +38,19 @@ public class DependencyLoader {
public ConnectionSource setUpDriverAndConnect() throws Throwable {
final SonarConfiguration config = Sonar.get().getConfig();
final URL[] urls = new URL[config.getDatabaseType().getDependencies().length];
for (final Dependency dependency : config.getDatabaseType().getDependencies()) {
final URL url = dependency.getClassLoaderURL();
final ClassLoader currentClassLoader = DependencyLoader.class.getClassLoader();
final URL url = config.getDatabase().getType().getDependency().getClassLoaderURL();
final ClassLoader currentClassLoader = DependencyLoader.class.getClassLoader();
final Method addPath = currentClassLoader.getClass().getDeclaredMethod("addPath", Path.class);
addPath.setAccessible(true);
addPath.invoke(currentClassLoader, new File(url.toURI()).toPath());
final Method addPath = currentClassLoader.getClass().getDeclaredMethod("addPath", Path.class);
addPath.setAccessible(true);
addPath.invoke(currentClassLoader, new File(url.toURI()).toPath());
urls[dependency.ordinal()] = url;
}
final String type = config.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 type = config.getDatabaseType().name().toLowerCase();
final String databaseURL =
"jdbc:" + type + "://" + config.getSqlUrl() + ":" + config.getSqlPort() + "/" + config.getSqlDatabase();
final ExternalClassLoader classLoader = new ExternalClassLoader(urls);
final Connection connection = connect(classLoader, databaseURL, config);
final ExternalClassLoader classLoader = new ExternalClassLoader(new URL[] {url});
final Connection connection = connect(classLoader, databaseURL, config.getDatabase());
return new JdbcSingleConnectionSource(databaseURL, connection);
}
@@ -63,18 +58,16 @@ public class DependencyLoader {
// https://github.com/Elytrium/LimboAuth/blob/master/src/main/java/net/elytrium/limboauth/dependencies/DatabaseLibrary.java#L134
private Connection connect(final @NotNull ClassLoader classLoader,
final @NotNull String databaseURL,
final @NotNull SonarConfiguration configuration) throws Throwable {
final Class<?> driverClass = classLoader.loadClass(configuration.getDatabaseType().getDriverClassName());
final @NotNull SonarConfiguration.Database config) throws Throwable {
final Class<?> driverClass = classLoader.loadClass(config.getType().getDriverClassName());
final Object driver = driverClass.getDeclaredConstructor().newInstance();
DriverManager.registerDriver((Driver) driver);
final Properties properties = new Properties();
if (!configuration.getSqlUser().isEmpty()) {
properties.put("user", configuration.getSqlUser());
}
if (!configuration.getSqlPassword().isEmpty()) {
properties.put("password", configuration.getSqlPassword());
properties.put("user", config.getUsername());
if (!config.getPassword().isEmpty()) {
properties.put("password", config.getPassword());
}
final Method connect = driverClass.getDeclaredMethod("connect", String.class, Properties.class);
@@ -0,0 +1,23 @@
/*
* Copyright (C) 2023 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.api.event.impl;
import xyz.jonesdev.sonar.api.event.SonarEvent;
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.AttackStatus;
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 AttackStatus.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);
}
}
@@ -41,7 +41,7 @@ public final class Fallback {
private final Map<String, InetAddress> connected = new ConcurrentHashMap<>();
// Only block the player for a few minutes to avoid issues
private final ExpiringCache<String> blacklisted = Cappuccino.buildExpiring(
10L, TimeUnit.MINUTES, 2500L
10L, TimeUnit.MINUTES, 5000L
);
private final @NotNull FallbackQueue queue = FallbackQueue.INSTANCE;
private final @NotNull FallbackRatelimiter ratelimiter = FallbackRatelimiter.INSTANCE;
@@ -50,22 +50,17 @@ public final class Fallback {
@Override
public void info(final String message, final Object... args) {
SONAR.getLogger().info("[Fallback] " + message, args);
SONAR.getLogger().info("[fallback] " + message, args);
}
@Override
public void warn(final String message, final Object... args) {
SONAR.getLogger().warn("[Fallback] " + message, args);
SONAR.getLogger().warn("[fallback] " + message, args);
}
@Override
public void error(final String message, final Object... args) {
SONAR.getLogger().error("[Fallback] " + message, args);
SONAR.getLogger().error("[fallback] " + message, args);
}
};
public boolean isPotentiallyUnderAttack() {
return connected.size() > SONAR.getConfig().getMinimumPlayersForAttack()
|| queue.getQueuedPlayers().size() > SONAR.getConfig().getMinimumPlayersForAttack();
}
}
@@ -50,14 +50,14 @@ public final class FallbackQueue {
}
public void poll() {
final int max = Sonar.get().getConfig().getMaximumQueuePolls();
final int maxQueuePolls = Sonar.get().getConfig().getQueue().getMaxQueuePolls();
int index = 0;
// We need to be very careful here since we don't want any concurrency issues.
// 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,8 @@ public final class FallbackQueue {
// Remove runnable from iterator
iterator.remove();
}
// Run the attack check task
Sonar.get().getAttackStatus().checkIfUnderAttack();
}
}
@@ -74,6 +74,12 @@ public interface FallbackUser<X, Y> {
*/
void write(final @NotNull Object msg);
/**
* Queues a buffered message that will be
* sent once all messages are flushed.
*/
void delayedWrite(final @NotNull Object msg);
ExpiringCache<String> PREVIOUS_FAILS = Cappuccino.buildExpiring(3L, TimeUnit.MINUTES);
/**
@@ -86,13 +92,10 @@ 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().getVerificationFailed());
disconnect(Sonar.get().getConfig().getVerification().getVerificationFailed());
if (reason != null) {
getFallback().getLogger().info(Sonar.get().getConfig().getVerificationFailedLog()
getFallback().getLogger().info(Sonar.get().getConfig().getVerification().getFailedLog()
.replace("%ip%", Sonar.get().getConfig().formatAddress(getInetAddress()))
.replace("%protocol%", String.valueOf(getProtocolVersion().getProtocol()))
.replace("%reason%", reason));
@@ -101,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();
@@ -110,7 +116,7 @@ public interface FallbackUser<X, Y> {
Sonar.get().getEventManager().publish(new UserBlacklistedEvent(this));
getFallback().getBlacklisted().put(getInetAddress().toString());
getFallback().getLogger().info(Sonar.get().getConfig().getVerificationBlacklistLog()
getFallback().getLogger().info(Sonar.get().getConfig().getVerification().getBlacklistLog()
.replace("%ip%", Sonar.get().getConfig().formatAddress(getInetAddress()))
.replace("%protocol%", String.valueOf(getProtocolVersion().getProtocol())));
} else {
@@ -17,12 +17,10 @@
package xyz.jonesdev.sonar.api.fallback.protocol;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.*;
@RequiredArgsConstructor
public enum ProtocolVersion {
@@ -66,12 +64,13 @@ public enum ProtocolVersion {
MINECRAFT_1_19_3(761),
MINECRAFT_1_19_4(762),
MINECRAFT_1_20(763),
MINECRAFT_1_20_2(-1);
MINECRAFT_1_20_2(764);
@Getter
private final int protocol;
private final int snapshotProtocol = -1;
public static final ProtocolVersion MINIMUM_VERSION = MINECRAFT_1_7_2;
public static final ProtocolVersion LATEST_VERSION;
public static final Map<Integer, ProtocolVersion> ID_TO_PROTOCOL_CONSTANT;
public static final Set<ProtocolVersion> SUPPORTED_VERSIONS;
@@ -81,7 +80,6 @@ public enum ProtocolVersion {
for (final ProtocolVersion version : values()) {
versions.putIfAbsent(version.protocol, version);
versions.put(version.snapshotProtocol, version);
}
ID_TO_PROTOCOL_CONSTANT = versions;
@@ -96,7 +94,8 @@ public enum ProtocolVersion {
}
}
SUPPORTED_VERSIONS = versions;
SUPPORTED_VERSIONS = Collections.unmodifiableSet(versions);
LATEST_VERSION = new ArrayList<>(versions).get(versions.size() - 1);
}
}
@@ -104,10 +103,6 @@ public enum ProtocolVersion {
return ID_TO_PROTOCOL_CONSTANT.getOrDefault(protocol, UNKNOWN);
}
public int getProtocol() {
return protocol == -1 ? snapshotProtocol : protocol;
}
public boolean isUnknown() {
return this == UNKNOWN;
}
@@ -17,6 +17,10 @@
package xyz.jonesdev.sonar.api.logger;
import org.jetbrains.annotations.NotNull;
import java.lang.reflect.Array;
/**
* The whole purpose of doing this is, so we can have one logger for every module.
* We wouldn't need to get the plugin logger every time we try to use it.
@@ -28,4 +32,62 @@ public interface LoggerWrapper {
void warn(final String message, final Object... args);
void error(final String message, final Object... args);
// Taken from
// https://github.com/j256/ormlite-core/blob/master/src/main/java/com/j256/ormlite/logger/Logger.java
String ARG_STRING = "{}";
int ARG_STRING_LENGTH = ARG_STRING.length();
Object UNKNOWN_ARG = new Object();
default String buildFullMessage(final @NotNull String msg, final Object... args) {
StringBuilder sb = null;
int lastIndex = 0;
int argC = 0;
while (true) {
int argIndex = msg.indexOf(ARG_STRING, lastIndex);
// no more {} arguments?
if (argIndex == -1) {
break;
}
if (sb == null) {
// we build this lazily in case there is no {} in the msg
sb = new StringBuilder(128);
}
// add the string before the arg-string
sb.append(msg, lastIndex, argIndex);
// shift our last-index past the arg-string
lastIndex = argIndex + ARG_STRING_LENGTH;
// add the arguments
if (argC < args.length) {
appendArg(sb, args[argC]);
}
argC++;
}
if (sb == null) {
return msg;
} else {
sb.append(msg, lastIndex, msg.length());
return sb.toString();
}
}
default void appendArg(final StringBuilder stringBuilder, final Object arg) {
if (arg == UNKNOWN_ARG) {
// ignore it
} else if (arg == null) {
stringBuilder.append("null");
} else if (arg.getClass().isArray()) {
stringBuilder.append('[');
int length = Array.getLength(arg);
for (int i = 0; i < length; i++) {
if (i > 0) {
stringBuilder.append(", ");
}
appendArg(stringBuilder, Array.get(arg, i));
}
stringBuilder.append(']');
} else {
stringBuilder.append(arg);
}
}
}
@@ -19,53 +19,60 @@ package xyz.jonesdev.sonar.api.verbose;
import lombok.Getter;
import lombok.val;
import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.cappuccino.Cappuccino;
import xyz.jonesdev.cappuccino.ExpiringCache;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.profiler.JVMProfiler;
import xyz.jonesdev.sonar.api.statistics.Statistics;
import xyz.jonesdev.sonar.api.timer.SystemTimer;
import java.util.Collection;
import java.util.Map;
import java.util.Vector;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import static xyz.jonesdev.sonar.api.Sonar.DECIMAL_FORMAT;
import static xyz.jonesdev.sonar.api.fallback.traffic.TrafficCounter.INCOMING;
import static xyz.jonesdev.sonar.api.fallback.traffic.TrafficCounter.OUTGOING;
@Getter
public abstract class Verbose implements JVMProfiler {
protected final @NotNull Collection<String> subscribers = new Vector<>(0);
private final SystemTimer secondTimer = new SystemTimer();
protected int joinsPerSecond, totalJoins;
private int lastTotalJoins, animationIndex;
public final class Verbose implements JVMProfiler {
private final @NotNull Collection<String> subscribers = new Vector<>(0);
private final @NotNull Map<String, Audience> audiences = new ConcurrentHashMap<>();
private int animationIndex;
private final ExpiringCache<Long> joinsPerSecond = Cappuccino.buildExpiring(1L, TimeUnit.SECONDS);
// Run action bar verbose
public final void update() {
public void update() {
// Clean up all blacklisted IPs
Sonar.get().getFallback().getBlacklisted().cleanUp(false);
joinsPerSecond.cleanUp(false);
totalJoins = Statistics.TOTAL_TRAFFIC.get();
// Statistically determine the joins per second without any caches
if (totalJoins > 0 && secondTimer.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 String prepareActionBarFormat() {
return Sonar.get().getConfig().getActionBarLayout()
public @NotNull Component prepareActionBarFormat() {
return MiniMessage.miniMessage().deserialize(Sonar.get().getConfig().getVerbose().getActionBarLayout()
.replace("%queued%",
DECIMAL_FORMAT.format(Sonar.get().getFallback().getQueue().getQueuedPlayers().size()))
.replace("%verifying%", DECIMAL_FORMAT.format(Sonar.get().getFallback().getConnected().size()))
.replace("%blacklisted%",
DECIMAL_FORMAT.format(Sonar.get().getFallback().getBlacklisted().estimatedSize()))
.replace("%total-joins%", DECIMAL_FORMAT.format(totalJoins))
.replace("%per-second-joins%", DECIMAL_FORMAT.format(joinsPerSecond))
.replace("%total-joins%", DECIMAL_FORMAT.format(Statistics.TOTAL_TRAFFIC.get()))
.replace("%per-second-joins%", DECIMAL_FORMAT.format(joinsPerSecond.estimatedSize()))
.replace("%verify-total%", DECIMAL_FORMAT.format(Statistics.REAL_TRAFFIC.get()))
.replace("%verify-success%",
DECIMAL_FORMAT.format(Sonar.get().getVerifiedPlayerController().estimatedSize()))
@@ -78,39 +85,34 @@ public abstract class Verbose implements JVMProfiler {
.replace("%free-memory%", formatMemory(getFreeMemory()))
.replace("%total-memory%", formatMemory(getTotalMemory()))
.replace("%max-memory%", formatMemory(getMaxMemory()))
.replace("%animation%", nextAnimation());
.replace("%animation%", nextAnimation()));
}
protected final String nextAnimation() {
val animations = Sonar.get().getConfig().getAnimation();
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 void broadcast(final String message) {
// This should be replaced by the wrapper
/**
* @param name Name of the audience
* @return Whether the audience is subscribed or not
*/
public boolean isSubscribed(final @NotNull String name) {
return subscribers.contains(name);
}
/**
* @param subscriber Name of the player who subscribed
* @return Whether the player is subscribed or not
* @param name Name of the audience to subscribe
*/
public final boolean isSubscribed(final @NotNull String subscriber) {
return subscribers.contains(subscriber);
public void subscribe(final String name) {
subscribers.add(name);
}
/**
* @param subscriber Name of the player to subscribe
* @param name Name of the audience to unsubscribe
*/
public final void subscribe(final @NotNull String subscriber) {
subscribers.add(subscriber);
}
/**
* @param subscriber Name of the player to unsubscribe
*/
public final void unsubscribe(final @NotNull String subscriber) {
subscribers.remove(subscriber);
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;
}
}
}
+1 -2
View File
@@ -7,12 +7,11 @@ 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 {
@@ -18,31 +18,32 @@
package xyz.jonesdev.sonar.bukkit;
import lombok.Getter;
import net.kyori.adventure.platform.bukkit.BukkitAudiences;
import org.bstats.bukkit.Metrics;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.SonarPlatform;
import xyz.jonesdev.sonar.api.command.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 +54,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,11 +72,14 @@ 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 audience register listener
getPlugin().getServer().getPluginManager().registerEvents(new AudienceListener(), getPlugin());
// Register Fallback queue task
getPlugin().getServer().getScheduler().runTaskTimerAsynchronously(getPlugin(), getFallback().getQueue()::poll,
10L, 10L);
@@ -105,9 +92,4 @@ public final class SonarBukkit extends SonarBootstrap<SonarBukkitPlugin> {
getPlugin().getServer().getScheduler().runTaskTimerAsynchronously(getPlugin(), getVerboseHandler()::update,
2L, 2L);
}
@Override
public void postReload() {
// Do nothing
}
}
@@ -0,0 +1,42 @@
/*
* Copyright (C) 2023 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.bukkit.audience;
import net.kyori.adventure.audience.Audience;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.bukkit.SonarBukkit;
public final class AudienceListener implements Listener {
@EventHandler(priority = EventPriority.LOWEST)
public void handle(final @NotNull PlayerJoinEvent event) {
final Audience audience = SonarBukkit.INSTANCE.getBukkitAudiences().player(event.getPlayer());
Sonar.get().getVerboseHandler().getAudiences().put(event.getPlayer().getName(), audience);
}
@EventHandler(priority = EventPriority.LOWEST)
public void handle(final @NotNull PlayerQuitEvent event) {
Sonar.get().getVerboseHandler().getAudiences().remove(event.getPlayer().getName());
}
}
@@ -17,16 +17,14 @@
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);
}
}
@@ -18,37 +18,36 @@
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,
final Command command,
final String label,
final String[] args) {
if (!(sender instanceof ConsoleCommandSender)) {
// Create our own invocation source wrapper to handle messages properly
final InvocationSource invocationSource = new BukkitInvocationSource(sender);
if (invocationSource.isPlayer()) {
// Check if the player actually has the permission to run the command
if (!sender.hasPermission("sonar.command")) {
sender.sendMessage(Sonar.get().getConfig().getNoPermission());
invocationSource.sendMessage(Sonar.get().getConfig().getNoPermission());
return false;
}
// Checking if it contains will only break more since it can throw
@@ -60,16 +59,14 @@ public final class BukkitSonarCommand implements CommandExecutor, TabExecutor, S
// Spamming should be prevented, especially if some heavy operations are done,
// which is not the case here but let's still stay safe!
if (mapTimestamp > 0L) {
sender.sendMessage(Sonar.get().getConfig().getCommandCoolDown());
invocationSource.sendMessage(Sonar.get().getConfig().getCommands().getCommandCoolDown());
// Format delay
final long timestamp = System.currentTimeMillis();
final double left = 0.5D - (timestamp - mapTimestamp) / 1000D;
sender.sendMessage(
Sonar.get().getConfig().getCommandCoolDownLeft()
.replace("%time-left%", Sonar.DECIMAL_FORMAT.format(left))
);
invocationSource.sendMessage(Sonar.get().getConfig().getCommands().getCommandCoolDownLeft()
.replace("%time-left%", Sonar.DECIMAL_FORMAT.format(left)));
return false;
}
@@ -78,8 +75,6 @@ public final class BukkitSonarCommand implements CommandExecutor, TabExecutor, S
Optional<Subcommand> subcommand = Optional.empty();
final InvocationSource invocationSource = new BukkitInvocationSource(sender);
if (args.length > 0) {
// Search subcommand if command arguments are present
subcommand = Sonar.get().getSubcommandRegistry().getSubcommands().parallelStream()
@@ -93,38 +88,13 @@ public final class BukkitSonarCommand implements CommandExecutor, TabExecutor, S
if (!subcommand.get().getInfo().onlyConsole()
&& !sender.hasPermission(subcommand.get().getPermission())
) {
invocationSource.sendMessage(Sonar.get().getConfig().getSubCommandNoPerm()
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().getPlayersOnly());
return;
}
if (sub.getInfo().onlyConsole() && !(sender instanceof ConsoleCommandSender)) {
invocationSource.sendMessage(Sonar.get().getConfig().getConsoleOnly());
return;
}
final CommandInvocation commandInvocation = new CommandInvocation(invocationSource, sub, args);
// The subcommands has arguments which are not present in the executed command
if (sub.getInfo().arguments().length > 0
&& commandInvocation.getRawArguments().length <= 1) {
invocationSource.sendMessage(Sonar.get().getConfig().getIncorrectCommandUsage()
.replace("%usage%", sub.getInfo().name() + " (" + sub.getArguments() + ")"));
return;
}
// Execute the sub command with the custom invocation properties
sub.execute(commandInvocation);
});
if (!subcommand.isPresent()) {
// Re-use the old, cached help message since we don't want to scan
@@ -133,12 +103,14 @@ public final class BukkitSonarCommand implements CommandExecutor, TabExecutor, S
for (final Component component : CACHED_HELP_MESSAGE) {
invocationSource.sendMessage(component);
}
} else {
subcommand.get().invoke(invocationSource, args);
}
return false;
}
@Override
public List<String> onTabComplete(final CommandSender sender,
public List<String> onTabComplete(final @NotNull CommandSender sender,
final Command command,
final String commandAlias,
final String @NotNull [] args) {
@@ -146,33 +118,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);
}
}
@@ -1,42 +0,0 @@
/*
* Copyright (C) 2023 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.bukkit.verbose;
import lombok.RequiredArgsConstructor;
import net.md_5.bungee.api.chat.TextComponent;
import org.bukkit.Server;
import org.bukkit.entity.Player;
import xyz.jonesdev.sonar.api.verbose.Verbose;
@RequiredArgsConstructor
public final class ActionBarVerbose extends Verbose {
private final Server server;
public void broadcast(final String message) {
final TextComponent component = new TextComponent(message);
synchronized (subscribers) {
for (final String subscriber : subscribers) {
final Player player = server.getPlayer(subscriber);
if (player != null) {
//player.sendMessage(ChatMessageType.ACTION_BAR, component);
}
}
}
}
}
+1 -8
View File
@@ -5,23 +5,16 @@ 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")
// MiniMessage platform support
@@ -18,34 +18,35 @@
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 +57,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,13 +75,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 audience register listener
getPlugin().getServer().getPluginManager().registerListener(getPlugin(), new AudienceListener());
// Register Fallback queue task
getPlugin().getServer().getScheduler().schedule(getPlugin(), getFallback().getQueue()::poll,
@@ -114,8 +101,4 @@ public final class SonarBungee extends SonarBootstrap<SonarBungeePlugin> {
// Inject base into ProtocolUtils
BaseInjectionHelper.inject(ChildChannelInitializer.INSTANCE);
}
@Override
public void postReload() {
}
}
@@ -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,14 @@
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);
}
}
@@ -19,39 +19,34 @@ package xyz.jonesdev.sonar.bungee.command;
import net.kyori.adventure.text.Component;
import net.md_5.bungee.api.CommandSender;
import net.md_5.bungee.api.chat.TextComponent;
import net.md_5.bungee.api.connection.ProxiedPlayer;
import net.md_5.bungee.api.plugin.Command;
import net.md_5.bungee.api.plugin.TabExecutor;
import net.md_5.bungee.command.ConsoleCommandSender;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.command.CommandInvocation;
import xyz.jonesdev.sonar.api.command.InvocationSource;
import xyz.jonesdev.sonar.api.command.SonarCommand;
import xyz.jonesdev.sonar.api.command.subcommand.Subcommand;
import xyz.jonesdev.sonar.api.command.subcommand.argument.Argument;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import static java.util.Collections.emptyList;
public final class BungeeSonarCommand extends Command implements TabExecutor, SonarCommand {
public BungeeSonarCommand() {
super("sonar");
cacheHelpMessage();
}
@Override
@SuppressWarnings({"redundantSuppression"})
public void execute(final @NotNull CommandSender sender, final String[] args) {
if (!(sender instanceof ConsoleCommandSender)) {
// Create our own invocation source wrapper to handle messages properly
final InvocationSource invocationSource = new BungeeInvocationSource(sender);
if (invocationSource.isPlayer()) {
// Check if the player actually has the permission to run the command
if (!sender.hasPermission("sonar.command")) {
sender.sendMessage(new TextComponent(Sonar.get().getConfig().getNoPermission()));
invocationSource.sendMessage(Sonar.get().getConfig().getNoPermission());
return;
}
// Checking if it contains will only break more since it can throw
@@ -63,14 +58,14 @@ public final class BungeeSonarCommand extends Command implements TabExecutor, So
// Spamming should be prevented, especially if some heavy operations are done,
// which is not the case here but let's still stay safe!
if (mapTimestamp > 0L) {
sender.sendMessage(new TextComponent(Sonar.get().getConfig().getCommandCoolDown()));
invocationSource.sendMessage(Sonar.get().getConfig().getCommands().getCommandCoolDown());
// Format delay
final long timestamp = System.currentTimeMillis();
final double left = 0.5D - (timestamp - mapTimestamp) / 1000D;
sender.sendMessage(new TextComponent(Sonar.get().getConfig().getCommandCoolDownLeft()
.replace("%time-left%", Sonar.DECIMAL_FORMAT.format(left))));
invocationSource.sendMessage(Sonar.get().getConfig().getCommands().getCommandCoolDownLeft()
.replace("%time-left%", Sonar.DECIMAL_FORMAT.format(left)));
return;
}
@@ -79,8 +74,6 @@ public final class BungeeSonarCommand extends Command implements TabExecutor, So
Optional<Subcommand> subcommand = Optional.empty();
final InvocationSource invocationSource = new BungeeInvocationSource(sender);
if (args.length > 0) {
// Search subcommand if command arguments are present
subcommand = Sonar.get().getSubcommandRegistry().getSubcommands().parallelStream()
@@ -94,40 +87,13 @@ public final class BungeeSonarCommand extends Command implements TabExecutor, So
if (!subcommand.get().getInfo().onlyConsole()
&& !sender.hasPermission(subcommand.get().getPermission())
) {
invocationSource.sendMessage(
Sonar.get().getConfig().getSubCommandNoPerm()
.replace("%permission%", 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().getPlayersOnly());
return;
}
if (sub.getInfo().onlyConsole() && !(sender instanceof ConsoleCommandSender)) {
invocationSource.sendMessage(Sonar.get().getConfig().getConsoleOnly());
return;
}
final CommandInvocation commandInvocation = new CommandInvocation(invocationSource, sub, args);
// The subcommands has arguments which are not present in the executed command
if (sub.getInfo().arguments().length > 0
&& commandInvocation.getRawArguments().length <= 1) {
invocationSource.sendMessage(Sonar.get().getConfig().getIncorrectCommandUsage()
.replace("%usage%", sub.getInfo().name() + " (" + sub.getArguments() + ")"));
return;
}
// Execute the sub command with the custom invocation properties
sub.execute(commandInvocation);
});
if (!subcommand.isPresent()) {
// Re-use the old, cached help message since we don't want to scan
@@ -136,42 +102,17 @@ public final class BungeeSonarCommand extends Command implements TabExecutor, So
for (final Component component : CACHED_HELP_MESSAGE) {
invocationSource.sendMessage(component);
}
} else {
subcommand.get().invoke(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,26 @@
/*
* Copyright (C) 2023 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.bungee.fallback;
import io.netty.handler.codec.DecoderException;
final class ConditionFailedException extends DecoderException {
public ConditionFailedException(final String message) {
super(message);
}
}
@@ -0,0 +1,86 @@
/*
* Copyright (C) 2023 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.bungee.fallback;
import io.netty.channel.ChannelHandlerContext;
import net.md_5.bungee.api.ProxyServer;
import net.md_5.bungee.connection.DownstreamBridge;
import net.md_5.bungee.connection.UpstreamBridge;
import net.md_5.bungee.netty.ChannelWrapper;
import net.md_5.bungee.netty.HandlerBoss;
import net.md_5.bungee.netty.PacketHandler;
import org.jetbrains.annotations.NotNull;
public final class FallbackHandlerBoss extends HandlerBoss {
private ChannelWrapper channel;
private PacketHandler handler;
@Override
public void setHandler(final @NotNull PacketHandler handler) {
this.handler = handler;
super.setHandler(handler);
}
@Override
public void channelActive(final ChannelHandlerContext ctx) throws Exception {
if (handler != null) {
channel = new ChannelWrapper(ctx);
handler.connected(channel);
if (handler instanceof DownstreamBridge) {
ProxyServer.getInstance().getLogger().info(handler + " has connected");
}
}
}
@Override
public void channelInactive(final ChannelHandlerContext ctx) throws Exception {
if (handler != null) {
channel.markClosed();
handler.disconnected(channel);
if (handler instanceof DownstreamBridge) {
ProxyServer.getInstance().getLogger().info(handler + " has disconnected");
}
}
}
@Override
public void channelWritabilityChanged(final ChannelHandlerContext ctx) throws Exception {
if (handler != null) {
handler.writabilityChanged(channel);
super.channelWritabilityChanged(ctx);
}
}
@Override
public void exceptionCaught(final @NotNull ChannelHandlerContext ctx, final Throwable throwable) throws Exception {
if (ctx.channel().isActive()) {
if (handler instanceof DownstreamBridge || handler instanceof UpstreamBridge) {
try {
handler.exception(throwable);
} catch (Exception exception) {
ProxyServer.getInstance().getLogger().severe(handler + " - exception processing exception: " + exception);
}
}
channel.markClosed();
ctx.close();
}
}
}
@@ -0,0 +1,341 @@
/*
* Copyright (C) 2023 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.bungee.fallback;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelPipeline;
import io.netty.handler.codec.DecoderException;
import lombok.Getter;
import lombok.val;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.json.JSONComponentSerializer;
import net.md_5.bungee.BungeeCord;
import net.md_5.bungee.EncryptionUtil;
import net.md_5.bungee.api.config.ListenerInfo;
import net.md_5.bungee.connection.InitialHandler;
import net.md_5.bungee.netty.ChannelWrapper;
import net.md_5.bungee.protocol.PlayerPublicKey;
import net.md_5.bungee.protocol.packet.Kick;
import net.md_5.bungee.protocol.packet.LoginRequest;
import net.md_5.bungee.protocol.packet.StatusRequest;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import xyz.jonesdev.sonar.api.ReflectiveOperationException;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.event.impl.UserVerifyJoinEvent;
import xyz.jonesdev.sonar.api.fallback.Fallback;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.api.statistics.Statistics;
import xyz.jonesdev.sonar.common.fallback.FallbackChannelHandler;
import xyz.jonesdev.sonar.common.fallback.FallbackTimeoutHandler;
import xyz.jonesdev.sonar.common.fallback.FallbackVerificationHandler;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketDecoder;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketEncoder;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.login.LoginSuccess;
import xyz.jonesdev.sonar.common.fallback.traffic.TrafficChannelHooker;
import java.net.InetAddress;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.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 @NotNull final BungeeCord bungee;
private @Nullable FallbackUserWrapper player;
@Getter
private ProtocolVersion protocolVersion;
private boolean receivedLoginPacket;
private boolean receivedStatusPacket;
@Override
public void connected(final ChannelWrapper channelWrapper) throws Exception {
this.channelWrapper = channelWrapper;
super.connected(channelWrapper);
}
@Override
public void handle(final StatusRequest statusRequest) throws Exception {
// Fix status packet spam exploit
if (receivedStatusPacket) {
throw new ConditionFailedException("Duplicate status packet");
}
receivedStatusPacket = true;
// Run the rest of the method asynchronously
CompletableFuture.runAsync(() -> {
// The channel always stays open because the client sends
// a StatusRequest and a Ping packet after one another
if (!isConnected()) {
throw new ConditionFailedException("Not connected anymore");
}
try {
super.handle(statusRequest);
} catch (Exception exception) {
throw new DecoderException(exception);
}
});
}
@Override
public void handle(final LoginRequest loginRequest) throws Exception {
// Increase joins per second for the action bar verbose
Sonar.get().getVerboseHandler().getJoinsPerSecond().put(System.nanoTime());
// Fix login packet spam exploit
if (receivedLoginPacket || player != null) {
throw new ConditionFailedException("Duplicate login packet");
}
receivedLoginPacket = true;
// Cache protocol version so other handlers don't throw NPEs
protocolVersion = ProtocolVersion.fromId(getHandshake().getProtocolVersion());
final Channel channel = channelWrapper.getHandle();
// Run in the channel's event loop
channel.eventLoop().execute(() -> {
try {
final ChannelPipeline pipeline = channel.pipeline();
TrafficChannelHooker.hook(pipeline, PACKET_DECODER, PACKET_ENCODER);
// Increase total traffic statistic
Statistics.TOTAL_TRAFFIC.increment();
// Check if the verification is enabled
if (!Sonar.get().getConfig().getVerification().isEnabled()) {
super.handle(loginRequest);
return;
}
final InetAddress inetAddress = getAddress().getAddress();
val uuid = UUID.nameUUIDFromBytes(("OfflinePlayer:" + loginRequest.getData()).getBytes(StandardCharsets.UTF_8));
// Check if the player is already verified
if (Sonar.get().getVerifiedPlayerController().has(inetAddress, uuid)) {
super.handle(loginRequest);
return;
}
// Check the blacklist here since we cannot let the player "ghost join"
if (FALLBACK.getBlacklisted().has(inetAddress.toString())) {
closeWith(getKickPacket(Sonar.get().getConfig().getVerification().getBlacklisted()));
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;
}
// Create wrapped Fallback user
player = new FallbackUserWrapper(
FALLBACK, channelWrapper, this,
channel, channel.pipeline(), inetAddress, protocolVersion
);
// Perform default BungeeCord checks
if (bungee.config.isEnforceSecureProfile()
&& player.getProtocolVersion().compareTo(MINECRAFT_1_19_3) < 0) {
final PlayerPublicKey publicKey = loginRequest.getPublicKey();
if (publicKey == null) {
disconnect(bungee.getTranslation("secure_profile_required"));
return;
}
if (Instant.ofEpochMilli(publicKey.getExpiry()).isBefore(Instant.now())) {
disconnect(bungee.getTranslation("secure_profile_expired"));
return;
}
if (player.getProtocolVersion().compareTo(MINECRAFT_1_19_1) < 0
&& !EncryptionUtil.check(publicKey, null)) {
disconnect(bungee.getTranslation("secure_profile_invalid"));
return;
}
}
// Check if the player is already queued since we don't want bots to flood the queue
if (FALLBACK.getQueue().getQueuedPlayers().containsKey(inetAddress)) {
closeWith(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;
}
// We cannot allow too many players on our Fallback server
// There's technically no reason for limiting this, but we'll better stay safe.
if (FALLBACK.getConnected().size() > Sonar.get().getConfig().getVerification().getMaxVerifyingPlayers()) {
closeWith(getKickPacket(Sonar.get().getConfig().getVerification().getTooManyPlayers()));
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
pipeline.replace(
TIMEOUT_HANDLER,
TIMEOUT_HANDLER,
new FallbackTimeoutHandler(Sonar.get().getConfig().getVerification().getReadTimeout(),
TimeUnit.MILLISECONDS)
);
// Disconnect if the protocol version could not be resolved
if (player.getProtocolVersion().isUnknown()) {
closeWith(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().getAttackStatus().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(player.getProtocolVersion().getProtocol())));
}
}
// Call the VerifyJoinEvent for external API usage
Sonar.get().getEventManager().publish(new UserVerifyJoinEvent(loginRequest.getData(), player));
// Mark the player as connected → verifying players
FALLBACK.getConnected().put(loginRequest.getData(), inetAddress);
// This sometimes happens when the channel hangs, but the player is still connecting
// This also fixes a unique issue with TCPShield and other reverse proxies
if (player.getPipeline().get(PACKET_ENCODER) == null
|| player.getPipeline().get(PACKET_DECODER) == null) {
channelWrapper.close();
return;
}
// Replace normal encoder to allow custom packets
final FallbackPacketEncoder encoder = new FallbackPacketEncoder(player.getProtocolVersion());
player.getPipeline().replace(PACKET_ENCODER, FALLBACK_PACKET_ENCODER, encoder);
// Send LoginSuccess packet to make the client think they are joining the server
player.write(new LoginSuccess(loginRequest.getData(), uuid));
// The LoginSuccess packet has been sent, now we can change the registry state
encoder.updateRegistry(player.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_20_2) >= 0
? FallbackPacketRegistry.CONFIG : FallbackPacketRegistry.GAME);
// Replace normal decoder to allow custom packets
player.getPipeline().replace(
PACKET_DECODER, FALLBACK_PACKET_DECODER, new FallbackPacketDecoder(player,
new FallbackVerificationHandler(player, loginRequest.getData(), uuid)
));
}));
} catch (Throwable throwable) {
throw new ReflectiveOperationException(throwable);
}
});
}
private static final Map<Component, Kick> CACHED_KICK_PACKETS = new ConcurrentHashMap<>(16);
private 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(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,87 @@
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.audience.Audience;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage;
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.LoginEvent;
import net.md_5.bungee.api.event.PostLoginEvent;
import net.md_5.bungee.api.plugin.Listener;
import net.md_5.bungee.compress.PacketCompressor;
import net.md_5.bungee.compress.PacketDecompressor;
import net.md_5.bungee.connection.InitialHandler;
import net.md_5.bungee.event.EventHandler;
import net.md_5.bungee.netty.ChannelWrapper;
import net.md_5.bungee.protocol.packet.Kick;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.ReflectiveOperationException;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.fallback.Fallback;
import xyz.jonesdev.sonar.bungee.fallback.compress.FallbackPacketCompressor;
import xyz.jonesdev.sonar.bungee.fallback.compress.FallbackPacketDecompressor;
import xyz.jonesdev.sonar.bungee.fallback.handler.FallbackInitialHandler;
import 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;
@EventHandler
@SuppressWarnings("deprecation")
public void handle(final @NotNull LoginEvent event) {
final InetAddress inetAddress = event.getConnection().getAddress().getAddress();
static {
try {
CHANNEL_WRAPPER = InitialHandler.class.getDeclaredField("ch");
CHANNEL_WRAPPER.setAccessible(true);
} catch (Throwable throwable) {
throw new ReflectiveOperationException(throwable);
// 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();
final String serialized = JSONComponentSerializer.json().serialize(component);
fallbackInitialHandler.closeWith(new Kick(serialized));
}
}
}
@EventHandler
@SuppressWarnings("deprecation")
public void handle(final @NotNull PostLoginEvent event) throws Throwable {
if (Sonar.get().getConfig().isLockdownEnabled()) {
if (!event.getPlayer().hasPermission("sonar.lockdown")) {
public void handle(final @NotNull PostLoginEvent event) {
if (Sonar.get().getConfig().getLockdown().isEnabled()) {
if (!event.getPlayer().hasPermission(Sonar.get().getConfig().getLockdown().getBypassPermission())) {
final PendingConnection pendingConnection = event.getPlayer().getPendingConnection();
// Try to close the channel with a custom serialized disconnect component
if (pendingConnection instanceof FallbackInitialHandler) {
final FallbackInitialHandler fallbackInitialHandler = (FallbackInitialHandler) pendingConnection;
final Component component = Sonar.get().getConfig().getLockdownDisconnect();
final Component component = Sonar.get().getConfig().getLockdown().getDisconnect();
final String serialized = JSONComponentSerializer.json().serialize(component);
fallbackInitialHandler.closeWith(new Kick(serialized));
} else {
// Fallback by disconnecting without a message
pendingConnection.disconnect();
Sonar.get().getLogger().warn("Fallback handler of {} is missing", event.getPlayer().getName());
return;
}
if (Sonar.get().getConfig().isLockdownLogAttempts()) {
Sonar.get().getLogger().info(Sonar.get().getConfig().getLockdownConsoleLog()
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().isLockdownEnableNotify()) {
event.getPlayer().sendMessage(new TextComponent(Sonar.get().getConfig().getLockdownNotification()));
} else if (Sonar.get().getConfig().getLockdown().isNotifyAdmins()) {
final Audience audience = Sonar.get().getVerboseHandler().getAudiences().get(event.getPlayer().getName());
if (audience != null) {
final String notification = Sonar.get().getConfig().getLockdown().getNotification();
final Component deserialized = MiniMessage.miniMessage().deserialize(notification);
audience.sendMessage(deserialized);
}
}
}
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,8 +28,7 @@ import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.Fallback;
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.bungee.fallback.handler.FallbackInitialHandler;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.Disconnect;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.Disconnect;
import java.net.InetAddress;
@@ -57,4 +56,13 @@ public final class FallbackUserWrapper implements FallbackUser<ChannelWrapper, F
ReferenceCountUtil.release(packet);
}
}
@Override
public void delayedWrite(final @NotNull Object packet) {
if (channel.isActive()) {
channel.write(packet, channel.voidPromise());
} else {
ReferenceCountUtil.release(packet);
}
}
}
@@ -1,115 +0,0 @@
/*
* Copyright (C) 2023 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.bungee.fallback.compress;
import com.velocitypowered.natives.compression.VelocityCompressor;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import lombok.AllArgsConstructor;
import lombok.Getter;
import net.md_5.bungee.compress.PacketCompressor;
import org.jetbrains.annotations.NotNull;
import java.util.zip.DataFormatException;
import static com.velocitypowered.natives.util.MoreByteBufUtils.ensureCompatible;
import static com.velocitypowered.natives.util.MoreByteBufUtils.preferredBuffer;
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.*;
@Getter
@AllArgsConstructor
public final class FallbackPacketCompressor extends PacketCompressor {
private int compressionThreshold;
private final @NotNull VelocityCompressor compressor;
private static final int MAX_COMPRESSED_LENGTH = 1 << 21;
@Override
public void setThreshold(final int compressionThreshold) {
this.compressionThreshold = compressionThreshold;
}
@Override
public void handlerAdded(final ChannelHandlerContext ctx) throws Exception {
// Do nothing
}
@Override
public void handlerRemoved(final ChannelHandlerContext ctx) throws Exception {
compressor.close();
}
@Override
protected void encode(final ChannelHandlerContext ctx,
final @NotNull ByteBuf msg,
final ByteBuf out) throws Exception {
final int uncompressed = msg.readableBytes();
if (uncompressed < compressionThreshold) {
// Under the threshold, do nothing.
writeVarInt(out, uncompressed + 1);
writeVarInt(out, 0);
out.writeBytes(msg);
} else {
handleCompressed(ctx, msg, out);
}
}
private void handleCompressed(final @NotNull ChannelHandlerContext ctx,
final @NotNull ByteBuf msg,
final ByteBuf out) throws DataFormatException {
final int uncompressed = msg.readableBytes();
write21BitVarInt(out, 0); // Fake packet length
writeVarInt(out, uncompressed);
final ByteBuf compatibleIn = ensureCompatible(ctx.alloc(), compressor, msg);
final int startCompressed = out.writerIndex();
try {
compressor.deflate(compatibleIn, out);
} finally {
compatibleIn.release();
}
final int compressedLength = out.writerIndex() - startCompressed;
if (compressedLength >= MAX_COMPRESSED_LENGTH) {
throw new DataFormatException("The server sent a very large (over 2MiB compressed) packet.");
}
int writerIndex = out.writerIndex();
int packetLength = out.readableBytes() - 3;
out.writerIndex(0);
write21BitVarInt(out, packetLength); // Rewrite packet length
out.writerIndex(writerIndex);
}
@Override
protected ByteBuf allocateBuffer(final ChannelHandlerContext ctx,
final @NotNull ByteBuf msg,
final boolean preferDirect) throws Exception {
int uncompressed = msg.readableBytes();
if (uncompressed < compressionThreshold) {
int finalBufferSize = uncompressed + 1;
finalBufferSize += varIntBytes(finalBufferSize);
return ctx.alloc().directBuffer(finalBufferSize);
}
final int initialBufferSize = (uncompressed - 1) + 3 + varIntBytes(uncompressed);
return preferredBuffer(ctx.alloc(), compressor, initialBufferSize);
}
}
@@ -1,98 +0,0 @@
/*
* Copyright (C) 2023 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.bungee.fallback.compress;
import com.velocitypowered.natives.compression.VelocityCompressor;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.CorruptedFrameException;
import lombok.Getter;
import net.md_5.bungee.compress.PacketDecompressor;
import org.jetbrains.annotations.NotNull;
import java.util.List;
import static com.velocitypowered.natives.util.MoreByteBufUtils.ensureCompatible;
import static com.velocitypowered.natives.util.MoreByteBufUtils.preferredBuffer;
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.readVarInt;
@Getter
public final class FallbackPacketDecompressor extends PacketDecompressor {
private final int compressionThreshold;
private final @NotNull VelocityCompressor compressor;
private static final int VANILLA_MAXIMUM_UNCOMPRESSED_SIZE = 8 * 1024 * 1024; // 8MiB
private static final int HARD_MAXIMUM_UNCOMPRESSED_SIZE = 16 * 1024 * 1024; // 16MiB
private static final int UNCOMPRESSED_CAP = Boolean.getBoolean("sonar.increased-compression-cap")
? HARD_MAXIMUM_UNCOMPRESSED_SIZE : VANILLA_MAXIMUM_UNCOMPRESSED_SIZE;
public FallbackPacketDecompressor(final int compressionThreshold,
final @NotNull VelocityCompressor compressor) {
super(); // super(compressionThreshold);
this.compressionThreshold = compressionThreshold;
this.compressor = compressor;
}
@Override
public void handlerAdded(final ChannelHandlerContext ctx) throws Exception {
// Do nothing
}
@Override
public void handlerRemoved(final ChannelHandlerContext ctx) throws Exception {
compressor.close();
}
@Override
protected void decode(final ChannelHandlerContext ctx,
final @NotNull ByteBuf in,
final List<Object> out) throws Exception {
final int claimedUncompressedSize = readVarInt(in);
if (claimedUncompressedSize == 0) {
// This message is not compressed.
out.add(in.retain());
// Check for invalid packet size after the packet is retained
final int size = in.readableBytes();
if (size > compressionThreshold + 2) {
throw new CorruptedFrameException("Invalid uncompressed packet size " + size);
}
return;
}
if (claimedUncompressedSize < compressionThreshold) {
throw new CorruptedFrameException(claimedUncompressedSize + " is less than " + compressionThreshold);
}
if (claimedUncompressedSize > UNCOMPRESSED_CAP) {
throw new CorruptedFrameException(claimedUncompressedSize + " exceeds maximum size");
}
final ByteBuf compatibleIn = ensureCompatible(ctx.alloc(), compressor, in);
final ByteBuf uncompressed = preferredBuffer(ctx.alloc(), compressor, claimedUncompressedSize);
try {
compressor.inflate(compatibleIn, uncompressed, claimedUncompressedSize);
out.add(uncompressed);
} catch (Exception exception) {
uncompressed.release();
throw exception;
} finally {
compatibleIn.release();
}
}
}
@@ -1,91 +0,0 @@
/*
* Copyright (C) 2023 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.bungee.fallback.handler;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFutureListener;
import net.md_5.bungee.BungeeCord;
import net.md_5.bungee.api.config.ListenerInfo;
import net.md_5.bungee.connection.InitialHandler;
import net.md_5.bungee.netty.ChannelWrapper;
import net.md_5.bungee.protocol.packet.LoginRequest;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.fallback.Fallback;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.bungee.fallback.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,43 +0,0 @@
/*
* Copyright (C) 2023 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.bungee.verbose;
import lombok.RequiredArgsConstructor;
import net.md_5.bungee.api.ChatMessageType;
import net.md_5.bungee.api.ProxyServer;
import net.md_5.bungee.api.chat.TextComponent;
import net.md_5.bungee.api.connection.ProxiedPlayer;
import xyz.jonesdev.sonar.api.verbose.Verbose;
@RequiredArgsConstructor
public final class VerboseWrapper extends Verbose {
private final ProxyServer server;
public void broadcast(final String message) {
final TextComponent component = new TextComponent(message);
synchronized (subscribers) {
for (final String subscriber : subscribers) {
final ProxiedPlayer player = server.getPlayer(subscriber);
if (player != null) {
player.sendMessage(ChatMessageType.ACTION_BAR, component);
}
}
}
}
}
@@ -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;
@@ -33,6 +34,7 @@ import xyz.jonesdev.sonar.api.timer.SystemTimer;
import xyz.jonesdev.sonar.api.verbose.Verbose;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer;
import xyz.jonesdev.sonar.common.subcommand.SubcommandRegistryHolder;
import xyz.jonesdev.sonar.common.update.UpdateChecker;
import java.io.File;
import java.net.InetAddress;
@@ -47,19 +49,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();
}
@@ -90,8 +94,12 @@ public abstract class SonarBootstrap<T> implements Sonar {
} catch (Throwable throwable) {
// An error has occurred
getLogger().error("An error has occurred while launching Sonar: {}", throwable);
throwable.printStackTrace();
throwable.printStackTrace(System.err);
return; // Do not check for updates if the launch failed
}
// Check if a new version has been released
UpdateChecker.checkForUpdates();
}
public abstract void enable();
@@ -102,7 +110,7 @@ public abstract class SonarBootstrap<T> implements Sonar {
// Warn player if they reloaded and changed the database type
if (getVerifiedPlayerController() != null
&& getVerifiedPlayerController().getCachedDatabaseType() != getConfig().getDatabaseType()) {
&& getVerifiedPlayerController().getCachedDatabaseType() != getConfig().getDatabase().getType()) {
Sonar.get().getLogger().warn("Reloading the server after changing the database type"
+ " is generally not recommended as it can sometimes cause data loss.");
}
@@ -112,19 +120,13 @@ public abstract class SonarBootstrap<T> implements Sonar {
// Update ratelimiter
final ExpiringCache<InetAddress> expiringCache = Cappuccino.buildExpiring(
getConfig().getVerificationDelay(), TimeUnit.MILLISECONDS, 250L
);
getConfig().getVerification().getReconnectDelay(), TimeUnit.MILLISECONDS, 250L);
FallbackRatelimiter.INSTANCE.setExpiringCache(expiringCache);
// Reinitialize database controller
verifiedPlayerController = new VerifiedPlayerController();
// Call post reload task
postReload();
}
public abstract void postReload();
public void shutdown() {
getLogger().info("Starting shutdown process...");
// ...
@@ -20,24 +20,23 @@ package xyz.jonesdev.sonar.common.fallback;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.fallback.Fallback;
import java.net.InetAddress;
import java.net.InetSocketAddress;
@RequiredArgsConstructor
public final class FallbackChannelHandler extends ChannelInboundHandlerAdapter {
private static final Fallback FALLBACK = Sonar.get().getFallback();
private final String username;
private static final @NotNull Fallback FALLBACK = Sonar.get().getFallback();
private final @NotNull String username;
@Override
public void channelUnregistered(final ChannelHandlerContext ctx) {
final InetAddress inetAddress = ((InetSocketAddress) ctx.channel().remoteAddress()).getAddress();
// Remove the IP address from the queue
public void channelUnregistered(final @NotNull ChannelHandlerContext ctx) {
// Remove the username from the connected players
FALLBACK.getConnected().remove(username);
FALLBACK.getQueue().remove(inetAddress);
// Remove the IP address from the queue
FALLBACK.getQueue().remove(((InetSocketAddress) ctx.channel().remoteAddress()).getAddress());
ctx.fireChannelUnregistered();
}
@@ -20,78 +20,111 @@ package xyz.jonesdev.sonar.common.fallback;
import io.netty.buffer.ByteBuf;
import io.netty.handler.codec.CorruptedFrameException;
import io.netty.handler.codec.DecoderException;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import lombok.val;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.event.impl.UserVerifySuccessEvent;
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
import xyz.jonesdev.sonar.api.model.VerifiedPlayer;
import xyz.jonesdev.sonar.api.timer.SystemTimer;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketListener;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.*;
import xyz.jonesdev.sonar.common.fallback.protocol.*;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.config.FinishConfiguration;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.login.LoginAcknowledged;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.*;
import xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil;
import java.util.Random;
import java.util.UUID;
import java.util.regex.Pattern;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_13;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_8;
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_PACKET_DECODER;
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_PACKET_ENCODER;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer.*;
public final class FallbackVerificationHandler implements FallbackPacketListener {
private final @NotNull FallbackUser<?, ?> user;
private final String username;
private final UUID uuid;
private final short transactionId;
private final long verifyKeepAliveId;
private int movementTick, receivedPackets;
private double lastX, lastY, lastZ;
private final UUID playerUuid;
private short expectedTransactionId;
private long expectedKeepAliveId;
private int expectedTeleportId = -1;
private int tick, totalReceivedPackets;
private int ignoredMovementTicks;
private double posX, posY, posZ, lastY;
private boolean resolvedClientBrand, resolvedClientSettings;
@Setter
private State state;
private @NotNull State state = State.LOGIN_ACK;
private boolean listenForMovements;
private final SystemTimer login = new SystemTimer();
private static final Random RANDOM = new Random();
@RequiredArgsConstructor
public enum State {
// LOGIN
KEEP_ALIVE(false),
// PLAY
CLIENT_SETTINGS(false),
PLUGIN_MESSAGE(false),
TRANSACTION(false),
// IN-GAME
TELEPORT(false),
POSITION(true),
COLLISIONS(true),
// OTHER
SUCCESS(false);
private final boolean canMove;
// 1.20.2 configuration state
LOGIN_ACK, CONFIGURE,
// pre-JOIN ping check
KEEP_ALIVE,
// post-JOIN checks
CLIENT_SETTINGS, PLUGIN_MESSAGE, TRANSACTION,
// PLAY checks
TELEPORT, POSITION, COLLISIONS,
// Done
SUCCESS
}
public FallbackVerificationHandler(final @NotNull FallbackUser<?, ?> user,
final @NotNull String username,
final @NotNull UUID uuid) {
final @NotNull UUID playerUuid) {
this.user = user;
this.username = username;
this.uuid = uuid;
this.transactionId = (short) RANDOM.nextInt();
this.verifyKeepAliveId = RANDOM.nextInt();
this.state = State.KEEP_ALIVE;
this.playerUuid = playerUuid;
// Send LoginSuccess packet to make the client think they are joining the server
user.write(new ServerLoginSuccess(username, uuid));
if (user.getProtocolVersion().compareTo(MINECRAFT_1_20_2) < 0) {
// Start initializing the actual join process
initialJoinProcess();
}
}
private void configure() {
// Set the state to CONFIGURE to avoid false positives
state = State.CONFIGURE;
// Send the necessary configuration packets to the client
user.delayedWrite(synchronizeRegistry);
user.delayedWrite(FINISH_CONFIGURATION);
// Send all packets in one flush
user.getChannel().flush();
// Set decoder state to actually catch all packets
updateEncoderDecoderState(FallbackPacketRegistry.CONFIG);
}
private void updateEncoderDecoderState(final @NotNull FallbackPacketRegistry registry) {
val decoder = (FallbackPacketDecoder) user.getChannel().pipeline().get(FALLBACK_PACKET_DECODER);
val encoder = (FallbackPacketEncoder) user.getChannel().pipeline().get(FALLBACK_PACKET_ENCODER);
if (decoder != null && encoder != null) {
// Update the packet registry state to be able to listen for CONFIG packets
decoder.updateRegistry(registry);
encoder.updateRegistry(registry);
} else {
// Something went wrong - the decoder should not be null
user.getFallback().getLogger().warn("Necessary pipelines for {} not found", username);
// Close the channel to prevent bypasses or other potential exploits
user.getChannel().close();
}
}
private void initialJoinProcess() {
if (user.getProtocolVersion().compareTo(MINECRAFT_1_8) < 0) {
// 1.7 players don't have KeepAlive packets in the login process
sendJoinGamePacket();
} else {
// Set the state to KEEP_ALIVE to avoid false positives
state = State.KEEP_ALIVE;
// Generate a random KeepAlive ID for the pre-join check
expectedKeepAliveId = RANDOM.nextInt();
// Send first KeepAlive to check if the connection is somewhat responsive
user.write(new KeepAlive(verifyKeepAliveId));
user.write(new KeepAlive(expectedKeepAliveId));
}
}
@@ -99,18 +132,41 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
// Set the state to TRANSACTION to avoid false positives
// and go on with the flow of the verification.
state = State.TRANSACTION;
// Send a transaction with a
user.write(new Transaction(
0, transactionId, false
));
// Generate a random transaction ID
expectedTransactionId = (short) RANDOM.nextInt();
// Send a transaction with the given ID
user.write(new Transaction(0, expectedTransactionId, false));
}
private void sendJoinGamePacket() {
// Set the state to CLIENT_SETTINGS to avoid false positives
// and go on with the flow of the verification.
state = State.CLIENT_SETTINGS;
final boolean v1_20_2 = user.getProtocolVersion().compareTo(MINECRAFT_1_20_2) >= 0;
if (!v1_20_2) {
// Set the state to CLIENT_SETTINGS to avoid false positives
// and go on with the flow of the verification.
state = State.CLIENT_SETTINGS;
}
// Select the JoinGame packet for the respective protocol version
user.write(getJoinPacketForVersion(user.getProtocolVersion()));
if (v1_20_2) {
// Perform the transaction check since the ClientSettings
// and PluginMessage packets have already been validated.
sendTransaction();
}
}
private void sendAbilitiesAndTeleport() {
// Set the state to TELEPORT to avoid false positives
// and go on with the flow of the verification.
state = State.TELEPORT;
// Make sure the player is unable to fly (the player is in spectator mode)
user.delayedWrite(DEFAULT_ABILITIES);
// Generate the current teleport ID
expectedTeleportId = RANDOM.nextInt(Short.MAX_VALUE);
// Teleport the player to the spawn position
user.delayedWrite(new PositionLook(
SPAWN_X_POSITION, dynamicSpawnYPosition, SPAWN_Z_POSITION,
0f, 0f, expectedTeleportId, false
));
}
private void sendChunkData() {
@@ -118,16 +174,20 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
// and go on with the flow of the verification.
state = State.POSITION;
// Teleport player into the fake lobby by sending an empty chunk
user.write(EMPTY_CHUNK_DATA);
// Send an UpdateSectionBlocks packet with a platform of blocks
// to check if the player collides with the solid platform.
user.write(UPDATE_SECTION_BLOCKS);
user.delayedWrite(EMPTY_CHUNK_DATA);
if (Sonar.get().getConfig().getVerification().isCheckCollisions()) {
// Send an UpdateSectionBlocks packet with a platform of blocks
// to check if the player collides with the solid platform.
user.delayedWrite(UPDATE_SECTION_BLOCKS);
}
// Send all packets in one flush
user.getChannel().flush();
}
private static boolean validateClientLocale(final @SuppressWarnings("unused") @NotNull FallbackUser<?, ?> user,
final String locale) {
// Check the client locale by performing a simple regex check on it
final Pattern pattern = Sonar.get().getConfig().getValidLocaleRegex();
final Pattern pattern = Sonar.get().getConfig().getVerification().getValidLocaleRegex();
return pattern.matcher(locale).matches(); // Disallow non-ascii characters (by default)
}
@@ -135,14 +195,15 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
// We have to catch every DecoderException, so we can fail and punish
// the player instead of only disconnecting them due to an exception.
try {
// 1.7 has some very weird issues when trying to decode the client brand
// TODO: fix this?
if (user.getProtocolVersion().compareTo(MINECRAFT_1_8) < 0) return true;
// No need to check for empty or too long client brands since
// ProtocolUtil#readString already does exactly that.
final String read = ProtocolUtil.readString(content, Sonar.get().getConfig().getMaximumBrandLength());
// No need to check for empty brands since ProtocolUtil#readBrandMessage
// already performs these checks by default.
final String read = ProtocolUtil.readBrandMessage(content);
// Check if the decoded client brand string is too long
if (read.length() > Sonar.get().getConfig().getVerification().getMaxBrandLength()) {
return false;
}
// Regex pattern for validating client brands
final Pattern pattern = Sonar.get().getConfig().getValidBrandRegex();
final Pattern pattern = Sonar.get().getConfig().getVerification().getValidBrandRegex();
return !read.equals("Vanilla") // The normal brand is always lowercase
&& pattern.matcher(read).matches(); // Disallow non-ascii characters (by default)
} catch (DecoderException exception) {
@@ -155,19 +216,42 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
@Override
public void handle(final @NotNull FallbackPacket packet) {
// The player has already been verified, drop all other packets
if (state == State.SUCCESS) return;
// Check if the player is not sending a ton of packets to the server
final int maxPackets = Sonar.get().getConfig().getMaximumLoginPackets()
+ Sonar.get().getConfig().getMaximumMovementTicks();
checkFrame(++receivedPackets < maxPackets, "too many packets");
final int maxPackets = Sonar.get().getConfig().getVerification().getMaxLoginPackets() + maxPredictionTick;
checkFrame(++totalReceivedPackets < maxPackets, "too many packets");
// Check for timeout since the player could be sending packets but not important ones
final long timeout = Sonar.get().getConfig().getVerificationTimeout();
final long timeout = Sonar.get().getConfig().getVerification().getMaxPing();
// Check if the time limit has exceeded
if (login.elapsed(timeout)) {
user.getChannel().close();
return;
}
if (packet instanceof LoginAcknowledged) {
// Check if we are currently expecting a LoginAcknowledged packet
assertState(State.LOGIN_ACK);
// Start the configuration process for 1.20.2 clients
configure();
}
if (packet instanceof FinishConfiguration) {
// Check if we are currently expecting a FinishConfiguration packet
assertState(State.CONFIGURE);
// Check if the client has already sent valid ClientSettings and PluginMessage packets
checkFrame(resolvedClientBrand, "did not resolve client brand");
checkFrame(resolvedClientSettings, "did not resolve client settings");
// Start initializing the actual join process
updateEncoderDecoderState(FallbackPacketRegistry.GAME);
initialJoinProcess();
}
if (packet instanceof KeepAlive) {
final KeepAlive keepAlive = (KeepAlive) packet;
@@ -177,7 +261,7 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
// Check if we are currently expecting a KeepAlive packet
assertState(State.KEEP_ALIVE);
checkFrame(keepAlive.getId() == verifyKeepAliveId, "invalid KeepAlive ID");
checkFrame(keepAlive.getId() == expectedKeepAliveId, "invalid KeepAlive ID");
// The correct KeepAlive packet has been received
sendJoinGamePacket();
@@ -193,14 +277,20 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
if (state == State.CLIENT_SETTINGS) {
state = State.PLUGIN_MESSAGE;
}
// Make sure we mark the ClientSettings as valid
resolvedClientSettings = true;
}
if (packet instanceof PluginMessage) {
final PluginMessage pluginMessage = (PluginMessage) packet;
// Only the brand channel is important
// Only the brand channel is important, drop the rest
if (pluginMessage.getChannel().equals("MC|Brand")
|| pluginMessage.getChannel().equals("minecraft:brand")) {
// Check if the brand packet was sent twice,
// which is not possible when using a vanilla Minecraft client.
checkFrame(!resolvedClientBrand, "duplicate client brand packet");
// Check if the channel is correct - 1.13 uses the new namespace
// system ('minecraft:' + channel) and anything below 1.13 uses
@@ -216,6 +306,9 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
// Send the transaction packet
sendTransaction();
}
// Make sure we mark the PluginMessage as valid
resolvedClientBrand = true;
}
}
@@ -228,27 +321,33 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
// The transaction should always be accepted
checkFrame(transaction.isAccepted(), "transaction not accepted?!");
// Check if the transaction ID is valid
checkFrame(transaction.getId() == transactionId, "invalid transaction id");
checkFrame(transaction.getId() == expectedTransactionId, "invalid transaction id");
// Checking gravity is disabled, just finish verification
if (!Sonar.get().getConfig().isCheckGravity()) {
if (!Sonar.get().getConfig().getVerification().isCheckGravity()) {
finish();
return;
}
// Make sure the player is unable to fly (the player is in spectator mode)
user.write(DEFAULT_ABILITIES);
// Teleport the player to the spawn position
user.write(SPAWN_TELEPORT);
// TODO: implement ChunkData packet for 1.20.2 and fix block state Ids for barriers
if (user.getProtocolVersion().compareTo(MINECRAFT_1_20_2) >= 0) {
finish();
return;
}
// First, send an Abilities packet to the client to make
// sure the player falls even in spectator mode.
// Then, teleport the player to the spawn position.
sendAbilitiesAndTeleport();
// 1.7-1.8 clients do not have a TeleportConfirm packet
if (user.getProtocolVersion().compareTo(MINECRAFT_1_8) <= 0) {
// Immediately send the chunk data as 1.7-1.8 can't confirm teleports
// Immediately send the chunk data as 1.7-1.8 can't confirm teleport packets
sendChunkData();
} else {
// Set the state to TELEPORT to avoid false positives
// and go on with the flow of the verification.
state = State.TELEPORT;
// Send all previously sent packets in one flush since we didn't flush
// the channel earlier when we teleported the player.
user.getChannel().flush();
}
}
@@ -256,47 +355,53 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
// Check if we are currently expecting a TeleportConfirm packet
assertState(State.TELEPORT);
// Check if the teleport ID is correct
final TeleportConfirm teleportConfirm = (TeleportConfirm) packet;
checkFrame(teleportConfirm.getTeleportId() == expectedTeleportId, "invalid teleport ID");
// Check if the teleport id is correct
final boolean teleportIdCorrect = teleportConfirm.getTeleportId() == SPAWN_TELEPORT.getTeleportId();
checkFrame(teleportIdCorrect, "invalid teleport ID");
// Reset all values to ensure safety on teleport
tick = 1;
posY = lastY = -1;
expectedTeleportId = -1;
// Now we can send the chunk data
sendChunkData();
}
// Only handle positions if the player can send position packets.
if (state.canMove) {
if (state != State.LOGIN_ACK) {
if (packet instanceof Position) {
final Position position = (Position) packet;
// Immediately handle new position update
handlePositionUpdate(position.getX(), position.getY(), position.getZ(), position.isOnGround());
}
if (packet instanceof PositionLook) {
final PositionLook position = (PositionLook) packet;
// Immediately handle new position update
handlePositionUpdate(position.getX(), position.getY(), position.getZ(), position.isOnGround());
}
if (packet instanceof Player) {
final Player player = (Player) packet;
// This packet does not send new position data, reuse the last Y
handlePositionUpdate(lastX, lastY, lastZ, player.isOnGround());
handlePositionUpdate(posX, posY, posZ, player.isOnGround());
}
}
}
private void handlePositionUpdate(final double x, final double y, final double z, final boolean ground) {
final double deltaY = lastY - y;
// 1.8 does not have a TeleportConfirm packet, so we need to handle the spawning differently
if (user.getProtocolVersion().compareTo(MINECRAFT_1_8) <= 0
&& expectedTeleportId != -1 // Check if the teleport ID is currently unset
// Then, check if the position is equal to the spawn position
&& x == SPAWN_X_POSITION && y == dynamicSpawnYPosition && z == SPAWN_Z_POSITION) {
// Reset all values to ensure safety on teleport
tick = 1;
posY = -1;
expectedTeleportId = -1;
}
lastX = x;
lastY = y;
lastZ = z;
posX = x;
lastY = posY;
posY = y;
posZ = z;
// The player is not allowed to move away from the collision platform.
// This should not happen unless the max movement tick is configured to a high number.
@@ -306,45 +411,74 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
// The onGround property can never be true when we aren't checking for collisions
checkFrame(!ground || state == State.COLLISIONS, "invalid ground state");
// Check if the chunk might be unloaded
if (deltaY > 0.07) {
// Verify the player if they sent correct movement packets
if (movementTick++ >= MAX_MOVEMENT_TICK) {
if (Sonar.get().getConfig().isCheckCollisions()) {
if (state != State.COLLISIONS) {
// Set the state to COLLISIONS to avoid false positives
// and go on with the flow of the verification.
// Now we don't care about gravity anymore,
// we just want the player to collide with the blocks.
state = State.COLLISIONS;
} else {
// Calculate the difference between the player's Y coordinate and the expected Y coordinate
final double offsetY = DEFAULT_Y_COLLIDE_POSITION - y;
// Check if the client hasn't moved before sending the first movement packet
if (!listenForMovements) {
if (posX == SPAWN_X_POSITION && posZ == SPAWN_Z_POSITION) {
// Now, once we verified the X and Z position, we can safely check for gravity
listenForMovements = true;
}
// The offset cannot greater than 0 since the blocks will not let the player fall through them.
checkFrame(offsetY <= 0, "no collisions: " + offsetY);
lastY = dynamicSpawnYPosition;
return;
}
// Check if the player is colliding by performing a basic Y offset check.
if (ground) {
// The player is colliding, finish verification
// TODO: Check for: velocity, entities/mounting, interactions
finish();
}
}
} else {
// Checking collisions is disabled, just finish verification
finish();
}
} else if (y <= DYNAMIC_SPAWN_Y_POSITION && y >= DEFAULT_Y_COLLIDE_POSITION) {
// This is a basic gravity check that predicts the next y position
final double predictedY = PREPARED_MOVEMENT_PACKETS[movementTick];
final double offsetY = Math.abs(deltaY - predictedY);
final double deltaY = lastY - y;
// The deltaY is 0 whenever the player sends their first position packet.
// We have to account for this or the player will fail the verification.
if (deltaY == 0) {
// Check for too many ignored Y ticks
final int maxIgnoredTicks = Sonar.get().getConfig().getVerification().getMaxIgnoredTicks();
checkFrame(++ignoredMovementTicks < maxIgnoredTicks, "too many ignored ticks");
return;
}
// Check if the y motion is roughly equal to the predicted value
final String verbose = String.format("%d: %.7f %.10f %.10f!%.10f", movementTick, y, offsetY, deltaY, predictedY);
checkFrame(offsetY < 0.01, verbose);
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);
}
// Check if the player is colliding by performing a basic Y offset check.
// The offset cannot be greater than 0 since the blocks will not let the player fall through them.
checkFrame(offsetY <= 0, "no collisions: " + offsetY);
if (ground) {
// The player is colliding to blocks, finish verification
finish();
return;
}
}
if (!ground && tick < maxPredictionTick) {
final double predictedY = preparedCachedYMotions[tick];
final double offsetY = Math.abs(deltaY - predictedY);
// Log/debug position if enabled in the configuration
if (Sonar.get().getConfig().getVerification().isDebugXYZPositions()) {
user.getFallback().getLogger().info("{}: {}/{}/{} - deltaY: {} - prediction: {} - offset: {}",
username, x, y, z, deltaY, predictedY, offsetY);
}
// Check if the y motion is roughly equal to the predicted value
checkFrame(offsetY < 0.005, String.format("invalid gravity: %d, %.7f, %.10f, %.10f != %.10f",
tick, y, offsetY, deltaY, predictedY));
}
tick++;
}
private void assertState(final @NotNull State expectedState) {
@@ -366,16 +500,17 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
user.getFallback().getConnected().remove(username);
// Add verified player to the database
final VerifiedPlayer verifiedPlayer = new VerifiedPlayer(user.getInetAddress().toString(), uuid, login.getStart());
final VerifiedPlayer verifiedPlayer = new VerifiedPlayer(user.getInetAddress().toString(), playerUuid,
login.getStart());
Sonar.get().getVerifiedPlayerController().add(verifiedPlayer);
// Call the VerifySuccessEvent for external API usage
Sonar.get().getEventManager().publish(new UserVerifySuccessEvent(username, uuid, user, login.delay()));
Sonar.get().getEventManager().publish(new UserVerifySuccessEvent(username, playerUuid, user, login.delay()));
// Disconnect player with the verification success message
user.disconnect(Sonar.get().getConfig().getVerificationSuccess());
user.disconnect(Sonar.get().getConfig().getVerification().getVerificationSuccess());
user.getFallback().getLogger().info(Sonar.get().getConfig().getVerificationSuccessfulLog()
user.getFallback().getLogger().info(Sonar.get().getConfig().getVerification().getSuccessLog()
.replace("%name%", username)
.replace("%time%", login.toString()));
}
@@ -21,65 +21,86 @@ import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.handler.codec.CorruptedFrameException;
import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_20_2;
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry.Direction.SERVERBOUND;
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry.GAME;
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry.LOGIN;
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.readVarInt;
@RequiredArgsConstructor
public final class FallbackPacketDecoder extends ChannelInboundHandlerAdapter {
private final FallbackUser<?, ?> user;
private final FallbackPacketRegistry.ProtocolRegistry registry;
private final FallbackPacketListener listener;
private @NotNull FallbackPacketRegistry.ProtocolRegistry registry;
public FallbackPacketDecoder(final @NotNull FallbackUser<?, ?> user,
final @NotNull FallbackPacketListener listener) {
this.user = user;
this.registry = FallbackPacketRegistry.SONAR.getProtocolRegistry(
FallbackPacketRegistry.Direction.SERVERBOUND, user.getProtocolVersion()
);
this.registry = (user.getProtocolVersion().compareTo(MINECRAFT_1_20_2) >= 0 ? LOGIN : GAME)
.getProtocolRegistry(SERVERBOUND, user.getProtocolVersion());
this.listener = listener;
}
public void updateRegistry(final @NotNull FallbackPacketRegistry registry) {
this.registry = registry.getProtocolRegistry(SERVERBOUND, user.getProtocolVersion());
}
@Override
public void channelRead(final @NotNull ChannelHandlerContext ctx,
final @NotNull Object msg) throws Exception {
if (msg instanceof ByteBuf) {
final ByteBuf byteBuf = (ByteBuf) msg;
// Release the ByteBuf if the connection is not active
// or the ByteBuf doesn't contain any data to avoid
// memory leaks or other potential exploits.
if (!ctx.channel().isActive() || !byteBuf.isReadable()) {
byteBuf.release();
return;
}
final int originalReaderIndex = byteBuf.readerIndex();
// Read the packet ID and then create the packet from it
final int packetId = readVarInt(byteBuf);
final FallbackPacket packet = registry.createPacket(packetId);
// If the packet hasn't been found, skip it
// TODO: Can we fail the verification afterwards?
if (packet == null) {
byteBuf.readerIndex(originalReaderIndex);
} else {
try {
doLengthSanityChecks(byteBuf, packet);
try {
packet.decode(byteBuf, user.getProtocolVersion());
} catch (Throwable throwable) {
user.fail("could not decode packet");
throw new CorruptedFrameException("Failed to decode packet");
}
if (byteBuf.isReadable()) {
user.fail("could not read packet to end (" + byteBuf.readableBytes() + " bytes left)");
throw new CorruptedFrameException("Could not read packet to end");
}
listener.handle(packet);
} finally {
byteBuf.release();
}
return;
}
try {
// Ensure that the packet isn't too large or too small
doLengthSanityChecks(byteBuf, packet);
try {
// Try to decode the packet for the given protocol version
packet.decode(byteBuf, user.getProtocolVersion());
} catch (Throwable throwable) {
user.fail("failed to decode packet (" + byteBuf.readableBytes() + " bytes)");
throw new CorruptedFrameException("Failed to decode packet");
}
// Check if the packet still has bytes left after we decoded it
if (byteBuf.isReadable()) {
user.fail("could not read packet to end (" + byteBuf.readableBytes() + " bytes left)");
throw new CorruptedFrameException("Could not read packet to end");
}
// Let our verification handler process the packet
listener.handle(packet);
} finally {
// Release the ByteBuf to avoid memory leaks
byteBuf.release();
}
} else {
// Packets are always ByteBufs - if we detect an unknown object, fail verification
user.fail("packet is not a ByteBuf");
throw new CorruptedFrameException("Packet is not a ByteBuf?!");
}
}
@@ -87,13 +108,13 @@ public final class FallbackPacketDecoder extends ChannelInboundHandlerAdapter {
final @NotNull FallbackPacket packet) throws Exception {
final int expectedMaxLen = packet.expectedMaxLength(byteBuf, user.getProtocolVersion());
if (expectedMaxLen != -1 && byteBuf.readableBytes() > expectedMaxLen) {
user.fail("packet too large");
user.fail("packet too large (" + byteBuf.readableBytes() + " bytes)");
throw new CorruptedFrameException("Packet too large");
}
final int expectedMinLen = packet.expectedMinLength(byteBuf, user.getProtocolVersion());
if (byteBuf.readableBytes() < expectedMinLen) {
user.fail("packet too small");
user.fail("packet too small (" + byteBuf.readableBytes() + " bytes)");
throw new CorruptedFrameException("Packet too small");
}
}
@@ -20,19 +20,24 @@ package xyz.jonesdev.sonar.common.fallback.protocol;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry.Direction.CLIENTBOUND;
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry.LOGIN;
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.writeVarInt;
public final class FallbackPacketEncoder extends MessageToByteEncoder<FallbackPacket> {
private final ProtocolVersion protocolVersion;
private final FallbackPacketRegistry.ProtocolRegistry registry;
private FallbackPacketRegistry.ProtocolRegistry registry;
public FallbackPacketEncoder(final ProtocolVersion protocolVersion) {
this.protocolVersion = protocolVersion;
this.registry = FallbackPacketRegistry.SONAR.getProtocolRegistry(
FallbackPacketRegistry.Direction.CLIENTBOUND, protocolVersion
);
updateRegistry(LOGIN);
}
public void updateRegistry(final @NotNull FallbackPacketRegistry registry) {
this.registry = registry.getProtocolRegistry(CLIENTBOUND, protocolVersion);
}
@Override
@@ -22,7 +22,11 @@ import io.netty.util.collection.IntObjectMap;
import lombok.Data;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.*;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.config.FinishConfiguration;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.config.RegistrySync;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.login.LoginAcknowledged;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.login.LoginSuccess;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.*;
import java.util.*;
import java.util.function.Supplier;
@@ -32,16 +36,38 @@ import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
// Most of this is taken from
// https://github.com/PaperMC/Velocity/blob/dev/3.0.0/proxy/src/main/java/com/velocitypowered/proxy/protocol/StateRegistry.java
public enum FallbackPacketRegistry {
SONAR {
LOGIN {
{
clientbound.fallback = false;
serverbound.fallback = false;
serverbound.register(LoginAcknowledged.class, LoginAcknowledged::new,
map(0x03, MINECRAFT_1_20_2, false));
// LOGIN
clientbound.register(ServerLoginSuccess.class, ServerLoginSuccess::new,
clientbound.register(LoginSuccess.class, LoginSuccess::new,
map(0x02, MINECRAFT_1_7_2, false));
}
},
CONFIG {
{
clientbound.register(Disconnect.class, Disconnect::new,
map(0x01, MINECRAFT_1_20_2, false));
clientbound.register(FinishConfiguration.class, FinishConfiguration::new,
map(0x02, MINECRAFT_1_20_2, false));
clientbound.register(KeepAlive.class, KeepAlive::new,
map(0x03, MINECRAFT_1_20_2, false));
clientbound.register(RegistrySync.class, RegistrySync::new,
map(0x05, MINECRAFT_1_20_2, false));
// GAME
serverbound.register(ClientSettings.class, ClientSettings::new,
map(0x00, MINECRAFT_1_20_2, false));
serverbound.register(PluginMessage.class, PluginMessage::new,
map(0x01, MINECRAFT_1_20_2, false));
serverbound.register(FinishConfiguration.class, FinishConfiguration::new,
map(0x02, MINECRAFT_1_20_2, false));
serverbound.register(KeepAlive.class, KeepAlive::new,
map(0x03, MINECRAFT_1_20_2, false));
}
},
GAME {
{
clientbound.register(JoinGame.class, JoinGame::new,
map(0x01, MINECRAFT_1_7_2, true),
map(0x23, MINECRAFT_1_9, true),
@@ -54,7 +80,8 @@ public enum FallbackPacketRegistry {
map(0x23, MINECRAFT_1_19, true),
map(0x25, MINECRAFT_1_19_1, true),
map(0x24, MINECRAFT_1_19_3, true),
map(0x28, MINECRAFT_1_19_4, true));
map(0x28, MINECRAFT_1_19_4, true),
map(0x29, MINECRAFT_1_20_2, true));
clientbound.register(KeepAlive.class, KeepAlive::new,
map(0x00, MINECRAFT_1_7_2, false),
map(0x1F, MINECRAFT_1_9, false),
@@ -67,7 +94,8 @@ public enum FallbackPacketRegistry {
map(0x1E, MINECRAFT_1_19, false),
map(0x20, MINECRAFT_1_19_1, false),
map(0x1F, MINECRAFT_1_19_3, false),
map(0x23, MINECRAFT_1_19_4, false));
map(0x23, MINECRAFT_1_19_4, false),
map(0x24, MINECRAFT_1_20_2, false));
clientbound.register(Disconnect.class, Disconnect::new,
map(0x40, MINECRAFT_1_7_2, true),
map(0x1A, MINECRAFT_1_9, true),
@@ -80,7 +108,8 @@ public enum FallbackPacketRegistry {
map(0x17, MINECRAFT_1_19, true),
map(0x19, MINECRAFT_1_19_1, true),
map(0x17, MINECRAFT_1_19_3, true),
map(0x1A, MINECRAFT_1_19_4, true));
map(0x1A, MINECRAFT_1_19_4, true),
map(0x1B, MINECRAFT_1_20_2, true));
clientbound.register(PositionLook.class, PositionLook::new,
map(0x08, MINECRAFT_1_7_2, true),
map(0x2E, MINECRAFT_1_9, true),
@@ -94,7 +123,8 @@ public enum FallbackPacketRegistry {
map(0x36, MINECRAFT_1_19, true),
map(0x39, MINECRAFT_1_19_1, true),
map(0x38, MINECRAFT_1_19_3, true),
map(0x3C, MINECRAFT_1_19_4, true));
map(0x3C, MINECRAFT_1_19_4, true),
map(0x3E, MINECRAFT_1_20_2, true));
clientbound.register(Abilities.class, Abilities::new,
map(0x39, MINECRAFT_1_7_2, true),
map(0x2B, MINECRAFT_1_9, true),
@@ -108,7 +138,8 @@ public enum FallbackPacketRegistry {
map(0x2f, MINECRAFT_1_19, true),
map(0x31, MINECRAFT_1_19_1, true),
map(0x30, MINECRAFT_1_19_3, true),
map(0x34, MINECRAFT_1_19_4, true));
map(0x34, MINECRAFT_1_19_4, true),
map(0x36, MINECRAFT_1_20_2, true));
clientbound.register(EmptyChunkData.class, EmptyChunkData::new,
map(0x21, MINECRAFT_1_7_2, true),
map(0x20, MINECRAFT_1_9, true),
@@ -121,33 +152,21 @@ public enum FallbackPacketRegistry {
map(0x1F, MINECRAFT_1_19, true),
map(0x21, MINECRAFT_1_19_1, true),
map(0x20, MINECRAFT_1_19_3, true),
map(0x24, MINECRAFT_1_19_4, true));
map(0x24, MINECRAFT_1_19_4, true),
map(0x25, MINECRAFT_1_20_2, true));
clientbound.register(UpdateSectionBlocks.class, UpdateSectionBlocks::new,
map(0x22, MINECRAFT_1_7_2, true),
map(16, MINECRAFT_1_9, true),
map(15, MINECRAFT_1_13, true),
map(16, MINECRAFT_1_15, true),
map(15, MINECRAFT_1_16, true),
map(59, MINECRAFT_1_16_2, true),
map(63, MINECRAFT_1_17, true),
map(61, MINECRAFT_1_19, true),
map(64, MINECRAFT_1_19_1, true),
map(63, MINECRAFT_1_19_3, true),
map(67, MINECRAFT_1_19_4, true));
clientbound.register(DefaultSpawnPosition.class, DefaultSpawnPosition::new,
map(0x05, MINECRAFT_1_7_2, true),
map(0x43, MINECRAFT_1_9, true),
map(0x45, MINECRAFT_1_12, true),
map(0x46, MINECRAFT_1_12_1, true),
map(0x49, MINECRAFT_1_13, true),
map(0x4D, MINECRAFT_1_14, true),
map(0x4E, MINECRAFT_1_15, true),
map(0x42, MINECRAFT_1_16, true),
map(0x4B, MINECRAFT_1_17, true),
map(0x4A, MINECRAFT_1_19, true),
map(0x4D, MINECRAFT_1_19_1, true),
map(0x4C, MINECRAFT_1_19_3, true),
map(0x50, MINECRAFT_1_19_4, true));
map(0x10, MINECRAFT_1_9, true),
map(0x0F, MINECRAFT_1_13, true),
map(0x10, MINECRAFT_1_15, true),
map(0x0F, MINECRAFT_1_16, true),
map(0x3B, MINECRAFT_1_16_2, true),
map(0x3F, MINECRAFT_1_17, true),
map(0x3D, MINECRAFT_1_19, true),
map(0x40, MINECRAFT_1_19_1, true),
map(0x3F, MINECRAFT_1_19_3, true),
map(0x43, MINECRAFT_1_19_4, true),
map(0x45, MINECRAFT_1_20_2, true));
clientbound.register(Transaction.class, Transaction::new,
map(0x32, MINECRAFT_1_7_2, true),
map(0x11, MINECRAFT_1_9, true),
@@ -159,7 +178,20 @@ public enum FallbackPacketRegistry {
map(0x2D, MINECRAFT_1_19, true),
map(0x2F, MINECRAFT_1_19_1, true),
map(0x2E, MINECRAFT_1_19_3, true),
map(0x32, MINECRAFT_1_19_4, true));
map(0x32, MINECRAFT_1_19_4, true),
map(0x33, MINECRAFT_1_20_2, true));
clientbound.register(Chat.class, Chat::new,
map(0x02, MINECRAFT_1_7_2, true),
map(0x0F, MINECRAFT_1_9, true),
map(0x0E, MINECRAFT_1_13, true),
map(0x0F, MINECRAFT_1_15, true),
map(0x0E, MINECRAFT_1_16, true),
map(0x0F, MINECRAFT_1_18_2, true),
map(0x5F, MINECRAFT_1_19, true),
map(0x62, MINECRAFT_1_19_1, true),
map(0x60, MINECRAFT_1_19_3, true),
map(0x64, MINECRAFT_1_19_4, true),
map(0x67, MINECRAFT_1_20_2, true));
serverbound.register(KeepAlive.class, KeepAlive::new,
map(0x00, MINECRAFT_1_7_2, false),
@@ -173,7 +205,8 @@ public enum FallbackPacketRegistry {
map(0x11, MINECRAFT_1_19, false),
map(0x12, MINECRAFT_1_19_1, false),
map(0x11, MINECRAFT_1_19_3, false),
map(0x12, MINECRAFT_1_19_4, false));
map(0x12, MINECRAFT_1_19_4, false),
map(0x14, MINECRAFT_1_20_2, false));
serverbound.register(ClientSettings.class, ClientSettings::new,
map(0x15, MINECRAFT_1_7_2, false),
map(0x04, MINECRAFT_1_9, false),
@@ -183,7 +216,8 @@ public enum FallbackPacketRegistry {
map(0x07, MINECRAFT_1_19, false),
map(0x08, MINECRAFT_1_19_1, false),
map(0x07, MINECRAFT_1_19_3, false),
map(0x08, MINECRAFT_1_19_4, false));
map(0x08, MINECRAFT_1_19_4, false),
map(0x09, MINECRAFT_1_20_2, false));
serverbound.register(PluginMessage.class, PluginMessage::new,
map(0x17, MINECRAFT_1_7_2, false),
map(0x09, MINECRAFT_1_9, false),
@@ -195,7 +229,8 @@ public enum FallbackPacketRegistry {
map(0x0C, MINECRAFT_1_19, false),
map(0x0D, MINECRAFT_1_19_1, false),
map(0x0C, MINECRAFT_1_19_3, false),
map(0x0D, MINECRAFT_1_19_4, false));
map(0x0D, MINECRAFT_1_19_4, false),
map(0x0F, MINECRAFT_1_20_2, false));
serverbound.register(PositionLook.class, PositionLook::new,
map(0x06, MINECRAFT_1_7_2, false),
map(0x0D, MINECRAFT_1_9, false),
@@ -208,7 +243,8 @@ public enum FallbackPacketRegistry {
map(0x14, MINECRAFT_1_19, false),
map(0x15, MINECRAFT_1_19_1, false),
map(0x14, MINECRAFT_1_19_3, false),
map(0x15, MINECRAFT_1_19_4, false));
map(0x15, MINECRAFT_1_19_4, false),
map(0x17, MINECRAFT_1_20_2, false));
serverbound.register(TeleportConfirm.class, TeleportConfirm::new,
map(0x00, MINECRAFT_1_9, false));
serverbound.register(Position.class, Position::new,
@@ -223,7 +259,8 @@ public enum FallbackPacketRegistry {
map(0x13, MINECRAFT_1_19, false),
map(0x14, MINECRAFT_1_19_1, false),
map(0x13, MINECRAFT_1_19_3, false),
map(0x14, MINECRAFT_1_19_4, false));
map(0x14, MINECRAFT_1_19_4, false),
map(0x16, MINECRAFT_1_20_2, false));
serverbound.register(Player.class, Player::new,
map(0x03, MINECRAFT_1_7_2, false),
map(0x0F, MINECRAFT_1_9, false),
@@ -236,7 +273,8 @@ public enum FallbackPacketRegistry {
map(0x16, MINECRAFT_1_19, false),
map(0x17, MINECRAFT_1_19_1, false),
map(0x16, MINECRAFT_1_19_3, false),
map(0x17, MINECRAFT_1_19_4, false));
map(0x17, MINECRAFT_1_19_4, false),
map(0x19, MINECRAFT_1_20_2, false));
serverbound.register(Transaction.class, Transaction::new,
map(0x0F, MINECRAFT_1_7_2, false),
map(0x05, MINECRAFT_1_9, false),
@@ -248,7 +286,8 @@ public enum FallbackPacketRegistry {
map(0x1F, MINECRAFT_1_19, false),
map(0x20, MINECRAFT_1_19_1, false),
map(0x1F, MINECRAFT_1_19_3, false),
map(0x20, MINECRAFT_1_19_4, false));
map(0x20, MINECRAFT_1_19_4, false),
map(0x23, MINECRAFT_1_20_2, false));
}
};
@@ -267,7 +306,6 @@ public enum FallbackPacketRegistry {
public static class PacketRegistry {
private final Map<ProtocolVersion, ProtocolRegistry> versions;
private boolean fallback = true;
PacketRegistry() {
Map<ProtocolVersion, ProtocolRegistry> mutableVersions = new EnumMap<>(ProtocolVersion.class);
@@ -283,9 +321,6 @@ public enum FallbackPacketRegistry {
ProtocolRegistry getProtocolRegistry(final ProtocolVersion version) {
final ProtocolRegistry registry = versions.get(version);
if (registry == null) {
if (fallback) {
return getProtocolRegistry(MINIMUM_VERSION);
}
throw new IllegalArgumentException("Could not find data for protocol version " + version);
}
return registry;
@@ -386,7 +421,7 @@ public enum FallbackPacketRegistry {
return supplier.get();
}
public int getPacketId(final FallbackPacket packet) {
public int getPacketId(final @NotNull FallbackPacket packet) {
final int id = packetClassToId.getOrDefault(packet.getClass(), Integer.MIN_VALUE);
if (id == Integer.MIN_VALUE) {
@@ -414,9 +449,9 @@ public enum FallbackPacketRegistry {
}
}
private static PacketMapping map(final int id,
final ProtocolVersion version,
final boolean encodeOnly) {
private static @NotNull PacketMapping map(final int id,
final ProtocolVersion version,
final boolean encodeOnly) {
return new PacketMapping(id, version, null, encodeOnly);
}
}
@@ -24,106 +24,101 @@ import net.kyori.adventure.nbt.CompoundBinaryTag;
import net.kyori.adventure.nbt.ListBinaryTag;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import xyz.jonesdev.sonar.api.ReflectiveOperationException;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.block.BlockPosition;
import xyz.jonesdev.sonar.common.fallback.protocol.block.BlockType;
import xyz.jonesdev.sonar.common.fallback.protocol.block.ChangedBlock;
import xyz.jonesdev.sonar.common.fallback.protocol.dimension.DimensionInfo;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.*;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.config.FinishConfiguration;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.config.RegistrySync;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.Abilities;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.EmptyChunkData;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.JoinGame;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.UpdateSectionBlocks;
import java.io.InputStream;
import java.util.Collections;
import java.util.Objects;
import java.util.Random;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
@UtilityClass
public class FallbackPreparer {
private final Random RANDOM = new Random();
private final DimensionInfo DIMENSION = new DimensionInfo(
"minecraft:overworld",
"sonar", false, false
);
// Mappings
private final CompoundBinaryTag CHAT_TYPE_119;
private final CompoundBinaryTag CHAT_TYPE_1191;
private final CompoundBinaryTag DAMAGE_TYPE_1194;
private final CompoundBinaryTag DAMAGE_TYPE_120;
static {
CHAT_TYPE_119 = getMapping("chat_1_19.nbt");
CHAT_TYPE_1191 = getMapping("chat_1_19_1.nbt");
DAMAGE_TYPE_1194 = getMapping("damage_1_19_4.nbt");
DAMAGE_TYPE_120 = getMapping("damage_type_1_20.nbt");
DAMAGE_TYPE_1194 = getMapping("damage_type_1194.nbt");
DAMAGE_TYPE_120 = getMapping("damage_type_120.nbt");
}
// JoinGame
public JoinGame LEGACY_JOIN_GAME;
private JoinGame JOIN_GAME_1_16;
private JoinGame JOIN_GAME_1_16_2;
private JoinGame JOIN_GAME_1_18_2;
private JoinGame JOIN_GAME_1_19_1;
private JoinGame JOIN_GAME_1_19_4;
private JoinGame JOIN_GAME_1_20;
// Abilities
public final Abilities DEFAULT_ABILITIES = new Abilities((byte) 0, 0f, 0f);
public final FallbackPacket DEFAULT_ABILITIES = new Abilities((byte) 0, 0f, 0f);
// Chunks
public final EmptyChunkData EMPTY_CHUNK_DATA = new EmptyChunkData(0, 0);
public PositionLook SPAWN_TELEPORT;
public final FallbackPacket EMPTY_CHUNK_DATA = new EmptyChunkData(0, 0);
// Finish Configuration
public final FallbackPacket FINISH_CONFIGURATION = new FinishConfiguration();
// Registry Sync
public FallbackPacket synchronizeRegistry;
// JoinGame
private FallbackPacket legacyJoinGame;
private FallbackPacket joinGame116;
private FallbackPacket joinGame1162;
private FallbackPacket joinGame1182;
private FallbackPacket joinGame1191;
private FallbackPacket joinGame1194;
private JoinGame joinGame120;
// Dimension
private final DimensionInfo DIMENSION = new DimensionInfo(
"minecraft:overworld", "sonar", false, false);
// Collisions
public final int BLOCKS_PER_ROW = 8; // 8 * 8 = 64 (max)
private final int SPAWN_BUFFER = 10; // player spawns at 255 + 10 (10 blocks above the platform)
public final int DEFAULT_Y_COLLIDE_POSITION = 255; // 255 is max
private final int SPAWN_BUFFER = 5; // player spawns at 255 + 5 (5 blocks above the platform)
public final int DEFAULT_Y_COLLIDE_POSITION = 255; // 255 is the maximum Y position allowed
public UpdateSectionBlocks UPDATE_SECTION_BLOCKS;
private final ChangedBlock[] CHANGED_BLOCKS = new ChangedBlock[BLOCKS_PER_ROW * BLOCKS_PER_ROW];
public int MAX_MOVEMENT_TICK;
public double[] PREPARED_MOVEMENT_PACKETS;
public int DYNAMIC_SPAWN_BUFFER = SPAWN_BUFFER;
public int DYNAMIC_SPAWN_Y_POSITION;
public int maxMovementTick, maxPredictionTick, dynamicSpawnYPosition;
public double[] preparedCachedYMotions;
public double maxFallDistance;
public final int SPAWN_X_POSITION = BLOCKS_PER_ROW; // middle of the chunk
public final int SPAWN_Z_POSITION = BLOCKS_PER_ROW;
public void prepare() {
LEGACY_JOIN_GAME = createLegacyJoinGamePacket();
JOIN_GAME_1_16 = createJoinGamePacket(MINECRAFT_1_16);
JOIN_GAME_1_16_2 = createJoinGamePacket(MINECRAFT_1_16_2);
JOIN_GAME_1_18_2 = createJoinGamePacket(MINECRAFT_1_18_2);
JOIN_GAME_1_19_1 = createJoinGamePacket(MINECRAFT_1_19_1);
JOIN_GAME_1_19_4 = createJoinGamePacket(MINECRAFT_1_19_4);
JOIN_GAME_1_20 = createJoinGamePacket(MINECRAFT_1_20);
legacyJoinGame = createJoinGamePacket(MINECRAFT_1_8);
joinGame116 = createJoinGamePacket(MINECRAFT_1_16);
joinGame1162 = createJoinGamePacket(MINECRAFT_1_16_2);
joinGame1182 = createJoinGamePacket(MINECRAFT_1_18_2);
joinGame1191 = createJoinGamePacket(MINECRAFT_1_19_1);
joinGame1194 = createJoinGamePacket(MINECRAFT_1_19_4);
joinGame120 = createJoinGamePacket(MINECRAFT_1_20);
synchronizeRegistry = new RegistrySync(joinGame120.getRegistry());
MAX_MOVEMENT_TICK = Sonar.get().getConfig().getMaximumMovementTicks();
PREPARED_MOVEMENT_PACKETS = new double[MAX_MOVEMENT_TICK + 1];
maxMovementTick = Sonar.get().getConfig().getVerification().getMaxMovementTicks();
maxPredictionTick = maxMovementTick + 10;
preparedCachedYMotions = new double[maxPredictionTick + 1];
for (int i = 0; i < MAX_MOVEMENT_TICK + 1; i++) {
PREPARED_MOVEMENT_PACKETS[i] = -((Math.pow(0.98, i) - 1) * 3.92);
for (int i = 0; i < maxPredictionTick + 1; i++) {
preparedCachedYMotions[i] = -((Math.pow(0.98, i) - 1) * 3.92);
}
// Adjust block and collide Y position based on max movement ticks
double maxFallDistance = 0;
for (final double motion : PREPARED_MOVEMENT_PACKETS) {
maxFallDistance += motion;
// Adjust block and collide Y position based on max fall distance
maxFallDistance = 0;
for (int i = 0; i < maxMovementTick; i++) {
maxFallDistance += preparedCachedYMotions[i];
}
// Set the dynamic spawn buffer
DYNAMIC_SPAWN_BUFFER = (int) (SPAWN_BUFFER + maxFallDistance);
final int DYNAMIC_SPAWN_BUFFER = (int) (SPAWN_BUFFER + maxFallDistance);
// Set the dynamic block and collide Y position based on the maximum fall distance
DYNAMIC_SPAWN_Y_POSITION = DEFAULT_Y_COLLIDE_POSITION + DYNAMIC_SPAWN_BUFFER;
// Prepare spawn PositionLook with the dynamic Y position
SPAWN_TELEPORT = new PositionLook(
8, DYNAMIC_SPAWN_Y_POSITION, 8,
0f, 0f,
RANDOM.nextInt(Short.MAX_VALUE),
false // 1.7 support
);
dynamicSpawnYPosition = DEFAULT_Y_COLLIDE_POSITION + DYNAMIC_SPAWN_BUFFER;
// Prepare collision platform positions
int index = 0;
@@ -135,7 +130,7 @@ public class FallbackPreparer {
z + (BLOCKS_PER_ROW / 2),
0, 0
);
CHANGED_BLOCKS[index++] = new ChangedBlock(position, BlockType.STONE);
CHANGED_BLOCKS[index++] = new ChangedBlock(position, BlockType.BARRIER);
}
}
@@ -145,60 +140,50 @@ public class FallbackPreparer {
private @Nullable CompoundBinaryTag getMapping(final @NotNull String fileName) {
try (final InputStream inputStream = Sonar.class.getResourceAsStream("/mappings/" + fileName)) {
return BinaryTagIO.unlimitedReader().read(Objects.requireNonNull(inputStream),
BinaryTagIO.Compression.GZIP
);
return BinaryTagIO.reader().read(Objects.requireNonNull(inputStream), BinaryTagIO.Compression.GZIP);
} catch (Throwable throwable) {
Sonar.get().getLogger().error("Could not load mapping {}", fileName);
throwable.printStackTrace(System.err);
Sonar.get().getLogger().error("Could not load mappings for {}: {}", fileName, throwable);
return null;
}
}
public static JoinGame getJoinPacketForVersion(final ProtocolVersion protocolVersion) {
public static FallbackPacket getJoinPacketForVersion(final @NotNull ProtocolVersion protocolVersion) {
if (protocolVersion.compareTo(MINECRAFT_1_15_2) <= 0) {
return LEGACY_JOIN_GAME; // 1.7-1.15.2
return legacyJoinGame; // 1.7.2-1.15.2
}
if (protocolVersion.compareTo(MINECRAFT_1_16_1) <= 0) {
return JOIN_GAME_1_16; // 1.16-1.16.1
return joinGame116; // 1.16-1.16.1
}
if (protocolVersion.compareTo(MINECRAFT_1_18) <= 0) {
return JOIN_GAME_1_16_2; // 1.16.2-1.18
return joinGame1162; // 1.16.2-1.18
}
if (protocolVersion.compareTo(MINECRAFT_1_19) <= 0) {
return JOIN_GAME_1_18_2; // 1.18.1-1.19
return joinGame1182; // 1.18.1-1.19
}
if (protocolVersion.compareTo(MINECRAFT_1_19_3) <= 0) {
return JOIN_GAME_1_19_1; // 1.19.1-1.19.3
return joinGame1191; // 1.19.1-1.19.3
}
if (protocolVersion.compareTo(MINECRAFT_1_19_4) <= 0) {
return JOIN_GAME_1_19_4; // 1.19.4
if (protocolVersion.compareTo(MINECRAFT_1_19_4) == 0) {
return joinGame1194; // 1.19.4
}
if (protocolVersion.compareTo(MINECRAFT_1_20) <= 0) {
return JOIN_GAME_1_20; // 1.20-1.20.1
}
throw new IllegalStateException("Unsupported protocol version");
}
private @NotNull JoinGame createLegacyJoinGamePacket() {
final JoinGame joinGame = new JoinGame();
joinGame.setLevelType("flat");
joinGame.setGamemode(Sonar.get().getConfig().getGamemodeId());
joinGame.setDimension(0);
joinGame.setReducedDebugInfo(true);
return joinGame;
return joinGame120; // 1.20-latest
}
// Partially taken from
// https://github.com/Elytrium/LimboAPI/blob/master/plugin/src/main/java/net/elytrium/limboapi/server/LimboImpl.java#L607
private @NotNull JoinGame createJoinGamePacket(final ProtocolVersion protocolVersion) {
private @NotNull JoinGame createJoinGamePacket(final @NotNull ProtocolVersion protocolVersion) {
final JoinGame joinGame = new JoinGame();
joinGame.setLevelType("flat");
joinGame.setGamemode(Sonar.get().getConfig().getGamemodeId());
joinGame.setGamemode(Sonar.get().getConfig().getVerification().getGamemodeId());
joinGame.setDimension(0);
joinGame.setReducedDebugInfo(true);
// 1.7.2-1.8.9 don't need any dimension information
if (protocolVersion.compareTo(MINECRAFT_1_8) <= 0) {
return joinGame;
}
joinGame.setDimensionInfo(DIMENSION);
final CompoundBinaryTag.Builder registryContainer = CompoundBinaryTag.builder();
@@ -206,7 +191,7 @@ public class FallbackPreparer {
.add(createDimensionData(protocolVersion))
.build();
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_16_2) >= 0) {
if (protocolVersion.compareTo(MINECRAFT_1_16_2) >= 0) {
final CompoundBinaryTag.Builder dimensionRegistryEntry = CompoundBinaryTag.builder();
dimensionRegistryEntry.putString("type", "minecraft:dimension_type");
@@ -245,34 +230,24 @@ public class FallbackPreparer {
))).build()
);
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_19) == 0) {
registryContainer.put("minecraft:chat_type", CHAT_TYPE_119);
} else if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_19_1) >= 0) {
registryContainer.put("minecraft:chat_type", CHAT_TYPE_1191);
}
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_19_4) == 0) {
if (protocolVersion.compareTo(MINECRAFT_1_19_4) == 0) {
registryContainer.put("minecraft:damage_type", DAMAGE_TYPE_1194);
} else if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_20) >= 0) {
} else if (protocolVersion.compareTo(MINECRAFT_1_20) >= 0) {
registryContainer.put("minecraft:damage_type", DAMAGE_TYPE_120);
}
} else {
registryContainer.put("dimension", encodedDimensionRegistry);
}
try {
CompoundBinaryTag currentDimensionData = encodedDimensionRegistry.getCompound(0);
CompoundBinaryTag currentDimensionData = encodedDimensionRegistry.getCompound(0);
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_16_2) >= 0) {
currentDimensionData = currentDimensionData.getCompound("element");
}
joinGame.setCurrentDimensionData(currentDimensionData);
joinGame.setLevelNames(new String[]{"minecraft:overworld"});
joinGame.setRegistry(registryContainer.build());
} catch (Throwable throwable) {
throw new ReflectiveOperationException(throwable);
if (protocolVersion.compareTo(MINECRAFT_1_16_2) >= 0) {
currentDimensionData = currentDimensionData.getCompound("element");
}
joinGame.setCurrentDimensionData(currentDimensionData);
joinGame.setLevelNames(new String[]{"minecraft:overworld"});
joinGame.setRegistry(registryContainer.build());
return joinGame;
}
@@ -289,7 +264,7 @@ public class FallbackPreparer {
.putBoolean("respawn_anchor_works", false)
.putBoolean("has_raids", false)
.putInt("logical_height", 256)
.putString("infiniburn", version.compareTo(ProtocolVersion.MINECRAFT_1_18_2) >= 0 ? "#minecraft" +
.putString("infiniburn", version.compareTo(MINECRAFT_1_18_2) >= 0 ? "#minecraft" +
":infiniburn_nether" : "minecraft:infiniburn_nether")
.putDouble("coordinate_scale", 1.0)
.putString("effects", "minecraft:overworld")
@@ -299,7 +274,7 @@ public class FallbackPreparer {
.putInt("monster_spawn_light_level", 0)
.build();
if (version.compareTo(ProtocolVersion.MINECRAFT_1_16_2) >= 0) {
if (version.compareTo(MINECRAFT_1_16_2) >= 0) {
return CompoundBinaryTag.builder()
.putString("name", "minecraft:overworld")
.putInt("id", 0)
@@ -17,21 +17,69 @@
package xyz.jonesdev.sonar.common.fallback.protocol.block;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_13;
import java.util.function.Function;
@Getter
@SuppressWarnings("unused")
@RequiredArgsConstructor
public enum BlockType {
STONE {
@Override
public int getId(final ProtocolVersion protocolVersion) {
return protocolVersion.compareTo(MINECRAFT_1_13) >= 0 ? 1 : 16;
STONE(protocolVersion -> 1),
BARRIER(protocolVersion -> {
// Link: https://github.com/PrismarineJS/minecraft-data/blob/master/data/pc/1.20/blocks.json
switch (protocolVersion) {
case MINECRAFT_1_7_2:
case MINECRAFT_1_7_6:
// We have to use glass for 1.7 since 1.7 doesn't have barrier blocks
return 20;
default:
// 1.8-1.12.2
return 166;
case MINECRAFT_1_13:
case MINECRAFT_1_13_1:
return 6493;
case MINECRAFT_1_13_2:
return 6494;
case MINECRAFT_1_14:
case MINECRAFT_1_14_1:
case MINECRAFT_1_14_2:
case MINECRAFT_1_14_3:
case MINECRAFT_1_14_4:
case MINECRAFT_1_15:
case MINECRAFT_1_15_1:
case MINECRAFT_1_15_2:
return 7000;
case MINECRAFT_1_16:
case MINECRAFT_1_16_1:
return 7536;
case MINECRAFT_1_16_2:
case MINECRAFT_1_16_3:
case MINECRAFT_1_16_4:
return 7540;
case MINECRAFT_1_17:
case MINECRAFT_1_17_1:
case MINECRAFT_1_18:
case MINECRAFT_1_18_2:
return 7754;
case MINECRAFT_1_19:
case MINECRAFT_1_19_1:
return 8245;
case MINECRAFT_1_19_3:
return 9889;
case MINECRAFT_1_19_4:
return 10221;
case MINECRAFT_1_20:
return 10225;
case MINECRAFT_1_20_2:
return 1; // TODO: https://github.com/PrismarineJS/minecraft-data/issues/779
}
};
});
public abstract int getId(final ProtocolVersion protocolVersion);
private final Function<ProtocolVersion, Integer> function;
public int getId(final @NotNull ProtocolVersion protocolVersion) {
return function.apply(protocolVersion);
}
}
@@ -15,27 +15,26 @@
* 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.packets.config;
import com.velocitypowered.api.proxy.ProxyServer;
import lombok.RequiredArgsConstructor;
import net.kyori.adventure.text.Component;
import xyz.jonesdev.sonar.api.verbose.Verbose;
import io.netty.buffer.ByteBuf;
import lombok.ToString;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
@RequiredArgsConstructor
public final class VerboseWrapper extends Verbose {
private final ProxyServer server;
@ToString
public final class FinishConfiguration implements FallbackPacket {
@Override
public void broadcast(final String message) {
final Component component = Component.text(message);
public void encode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
}
synchronized (subscribers) {
for (final String subscriber : subscribers) {
server.getPlayer(subscriber).ifPresent(player -> {
player.sendActionBar(component);
});
}
}
@Override
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
}
@Override
public int expectedMaxLength(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
return 0;
}
}
@@ -15,48 +15,41 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.protocol.packets;
package xyz.jonesdev.sonar.common.fallback.protocol.packets.config;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufOutputStream;
import io.netty.handler.codec.EncoderException;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
import net.kyori.adventure.nbt.BinaryTagTypes;
import net.kyori.adventure.nbt.CompoundBinaryTag;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_14;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_17;
import java.io.IOException;
@Getter
@ToString
@NoArgsConstructor
@AllArgsConstructor
public final class DefaultSpawnPosition implements FallbackPacket {
private int x, y, z;
private float angle;
public final class RegistrySync implements FallbackPacket {
private CompoundBinaryTag dimensionRegistry;
@Override
public void encode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
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(angle);
}
public void encode(final @NotNull ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
try (final ByteBufOutputStream stream = new ByteBufOutputStream(byteBuf)) {
stream.writeByte(10); // CompoundTag ID
BinaryTagTypes.COMPOUND.write(dimensionRegistry, stream);
} catch (IOException exception) {
throw new EncoderException("Cannot write NBT CompoundTag");
}
}
@Override
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
throw new UnsupportedOperationException();
}
}
@@ -0,0 +1,33 @@
/*
* 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.login;
import io.netty.buffer.ByteBuf;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
public final class LoginAcknowledged implements FallbackPacket {
@Override
public void encode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
}
@Override
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
}
}
@@ -15,12 +15,13 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.protocol.packets;
package xyz.jonesdev.sonar.common.fallback.protocol.packets.login;
import io.netty.buffer.ByteBuf;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.netty.FastUUID;
@@ -34,12 +35,12 @@ import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.writeVarInt;
@Getter
@NoArgsConstructor
@AllArgsConstructor
public final class ServerLoginSuccess implements FallbackPacket {
public final class LoginSuccess implements FallbackPacket {
private String username;
private UUID uuid;
@Override
public void encode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
public void encode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
if (protocolVersion.compareTo(MINECRAFT_1_19) >= 0) {
writeUUID(byteBuf, uuid);
} else if (protocolVersion.compareTo(MINECRAFT_1_16) >= 0) {
@@ -15,13 +15,14 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.protocol.packets;
package xyz.jonesdev.sonar.common.fallback.protocol.packets.play;
import io.netty.buffer.ByteBuf;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
@@ -34,14 +35,14 @@ public class Abilities implements FallbackPacket {
private float flySpeed, walkSpeed;
@Override
public void encode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
public void encode(final @NotNull ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
byteBuf.writeByte(encodedFlags);
byteBuf.writeFloat(flySpeed);
byteBuf.writeFloat(walkSpeed);
}
@Override
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
public void decode(final @NotNull ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
encodedFlags = byteBuf.readByte();
flySpeed = byteBuf.readFloat();
walkSpeed = byteBuf.readFloat();
@@ -0,0 +1,72 @@
/*
* Copyright (C) 2023 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.protocol.packets.play;
import io.netty.buffer.ByteBuf;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.json.JSONComponentSerializer;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
import java.util.UUID;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.writeString;
import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.writeUUID;
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.writeVarInt;
@Getter
@ToString
@NoArgsConstructor
@AllArgsConstructor
public final class Chat implements FallbackPacket {
private static final UUID PLACEHOLDER_UUID = new UUID(0L, 0L);
private Component component;
private byte position;
@Override
public void encode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
final String serialized = JSONComponentSerializer.json().serialize(component);
writeString(byteBuf, serialized);
// Message type
if (protocolVersion.compareTo(MINECRAFT_1_19_1) >= 0) {
byteBuf.writeBoolean(position == 2);
} else if (protocolVersion.compareTo(MINECRAFT_1_19) >= 0) {
writeVarInt(byteBuf, position);
} else {
byteBuf.writeByte(this.position);
}
// UUID
if (protocolVersion.compareTo(MINECRAFT_1_16) >= 0
&& protocolVersion.compareTo(MINECRAFT_1_19) < 0) {
writeUUID(byteBuf, PLACEHOLDER_UUID);
}
}
@Override
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
throw new UnsupportedOperationException();
}
}
@@ -15,13 +15,14 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.protocol.packets;
package xyz.jonesdev.sonar.common.fallback.protocol.packets.play;
import io.netty.buffer.ByteBuf;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
@@ -55,7 +56,7 @@ public final class ClientSettings implements FallbackPacket {
}
@Override
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
public void decode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
locale = readString(byteBuf, 16);
viewDistance = byteBuf.readByte();
chatVisibility = readVarInt(byteBuf);
@@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.protocol.packets;
package xyz.jonesdev.sonar.common.fallback.protocol.packets.play;
import io.netty.buffer.ByteBuf;
import lombok.AllArgsConstructor;
@@ -24,13 +24,13 @@ import lombok.NoArgsConstructor;
import lombok.ToString;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.json.JSONComponentSerializer;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
import java.util.Objects;
import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.readString;
import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.writeString;
@Getter
@@ -40,17 +40,17 @@ import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.writeStrin
public final class Disconnect implements FallbackPacket {
private @Nullable String reason;
@Override
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
reason = readString(byteBuf);
}
@Override
public void encode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
writeString(byteBuf, Objects.requireNonNull(reason));
}
public static Disconnect create(final Component component) {
@Override
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
throw new UnsupportedOperationException();
}
public static @NotNull Disconnect create(final Component component) {
return new Disconnect(JSONComponentSerializer.json().serialize(component));
}
}
@@ -15,28 +15,26 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.protocol.packets;
package xyz.jonesdev.sonar.common.fallback.protocol.packets.play;
import io.netty.buffer.ByteBuf;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
import net.kyori.adventure.nbt.CompoundBinaryTag;
import net.kyori.adventure.nbt.LongArrayBinaryTag;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
import java.util.BitSet;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.writeArray;
import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.writeCompoundTag;
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.writeVarInt;
// Partially taken from
// https://github.com/Leymooo/BungeeCord/blob/master/protocol/src/main/java/ru/leymooo/botfilter/packets/EmptyChunkPacket.java
@Getter
@ToString
@NoArgsConstructor
@AllArgsConstructor
public final class EmptyChunkData implements FallbackPacket {
@@ -45,23 +43,14 @@ public final class EmptyChunkData implements FallbackPacket {
private static final byte[] SECTION_BYTES = new byte[]{0, 0, 0, 0, 0, 0, 1, 0};
private static final byte[] LIGHT_BYTES = new byte[]{1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 3, -1, -1, 0, 0};
private static final byte[] LEGACY_FILLER_BYTES_17 = new byte[2];
private static final byte[] LEGACY_FILLER_BYTES = new byte[256];
private static final byte[] MODERN_FILLER_BYTES = new byte[1024];
private static final long[] MASK;
private static final byte[] MODERN_FILLER_BYTES = new byte[256 * 4];
// Prepare nbt for 1.18 and pre-1.18
private static final CompoundBinaryTag MODERN_TAG, LEGACY_TAG;
static {
final BitSet bitSet = new BitSet();
for (int i = 0; i < 16; i++) {
bitSet.set(i, false);
}
MASK = bitSet.toLongArray();
MODERN_TAG = prepareNBT(false);
LEGACY_TAG = prepareNBT(true);
}
@@ -79,30 +68,26 @@ public final class EmptyChunkData implements FallbackPacket {
}
@Override
public void encode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
public void encode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
byteBuf.writeInt(x);
byteBuf.writeInt(z);
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_17) >= 0) {
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_17_1) <= 0) {
writeVarInt(byteBuf, MASK.length);
for (final long l : MASK) {
byteBuf.writeLong(l);
}
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
byteBuf.writeBoolean(true); // full chunk
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_16) >= 0
&& protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_16_2) < 0) {
byteBuf.writeBoolean(true); // Ignore old data
if (protocolVersion.compareTo(MINECRAFT_1_16) >= 0
&& protocolVersion.compareTo(MINECRAFT_1_16_2) < 0) {
byteBuf.writeBoolean(true); // ignore old data
}
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_8) > 0) {
if (protocolVersion.compareTo(MINECRAFT_1_8) > 0) {
writeVarInt(byteBuf, 0);
} else {
byteBuf.writeShort(1); // Fix void chunk
byteBuf.writeShort(1); // fix void chunk
}
}
@@ -128,8 +113,8 @@ public final class EmptyChunkData implements FallbackPacket {
if (protocolVersion.compareTo(MINECRAFT_1_8) >= 0) {
writeArray(byteBuf, LEGACY_FILLER_BYTES); // 1.8 - 1.12.2
} else {
byteBuf.writeInt(0); // Compressed size.
byteBuf.writeBytes(new byte[2]); // 1.7
byteBuf.writeInt(0); // compressed size
byteBuf.writeBytes(LEGACY_FILLER_BYTES_17); // 1.7
}
} else if (protocolVersion.compareTo(MINECRAFT_1_15) < 0) {
writeArray(byteBuf, MODERN_FILLER_BYTES); // 1.13 - 1.14.4
@@ -15,26 +15,22 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.protocol.packets;
package xyz.jonesdev.sonar.common.fallback.protocol.packets.play;
import io.netty.buffer.ByteBuf;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import net.kyori.adventure.nbt.CompoundBinaryTag;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.dimension.DimensionInfo;
import java.util.Objects;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.*;
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.writeVarInt;
// Mostly taken from
// https://github.com/PaperMC/Velocity/blob/dev/3.0.0/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/JoinGame.java
@Data
@NoArgsConstructor
@AllArgsConstructor
@@ -49,81 +45,79 @@ public final class JoinGame implements FallbackPacket {
private int viewDistance; // 1.14+
private boolean reducedDebugInfo;
private boolean showRespawnScreen;
private boolean limitedCrafting; // 1.20.2+
private String[] levelNames; // 1.16+
private CompoundBinaryTag registry; // 1.16+
private DimensionInfo dimensionInfo; // 1.16+
private CompoundBinaryTag currentDimensionData; // 1.16.2+
private short previousGamemode; // 1.16+
private int simulationDistance; // 1.18+
private int portalCooldown; // 1.20+
// Mostly taken from
// https://github.com/Nan1t/NanoLimbo/blob/main/src/main/java/ua/nanit/limbo/protocol/packets/play/PacketJoinGame.java
@Override
public void encode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
if (protocolVersion.compareTo(MINECRAFT_1_16) >= 0) {
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_20_2) >= 0) {
// haha funny, they made 1.20.2 more complicated
encode1202Up(byteBuf, protocolVersion);
} else if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_16) >= 0) {
// Minecraft 1.16 and above have significantly more complicated logic for reading this packet,
// so separate it out.
encode116Up(byteBuf, protocolVersion);
} else {
encodeLegacy(byteBuf, protocolVersion);
}
}
private void encodeLegacy(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
private void encodeLegacy(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
byteBuf.writeInt(entityId);
if (protocolVersion.compareTo(MINECRAFT_1_16_2) >= 0) {
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_16_2) >= 0) {
byteBuf.writeBoolean(isHardcore);
byteBuf.writeByte(gamemode);
} else {
byteBuf.writeByte(isHardcore ? gamemode | 0x8 : gamemode);
}
if (protocolVersion.compareTo(MINECRAFT_1_9_1) >= 0) {
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_9_1) >= 0) {
byteBuf.writeInt(dimension);
} else {
byteBuf.writeByte(dimension);
}
if (protocolVersion.compareTo(MINECRAFT_1_13_2) <= 0) {
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_13_2) <= 0) {
byteBuf.writeByte(difficulty);
}
if (protocolVersion.compareTo(MINECRAFT_1_15) >= 0) {
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_15) >= 0) {
byteBuf.writeLong(partialHashedSeed);
}
byteBuf.writeByte(1); // max players
writeString(byteBuf, Objects.requireNonNull(levelType));
if (protocolVersion.compareTo(MINECRAFT_1_14) >= 0) {
if (levelType == null) {
throw new IllegalStateException("No level type specified.");
}
writeString(byteBuf, levelType);
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_14) >= 0) {
writeVarInt(byteBuf, viewDistance);
}
if (protocolVersion.compareTo(MINECRAFT_1_8) >= 0) {
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_8) >= 0) {
byteBuf.writeBoolean(reducedDebugInfo);
}
if (protocolVersion.compareTo(MINECRAFT_1_15) >= 0) {
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_15) >= 0) {
byteBuf.writeBoolean(showRespawnScreen);
}
}
private void encode116Up(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
private void encode116Up(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
byteBuf.writeInt(entityId);
if (protocolVersion.compareTo(MINECRAFT_1_16_2) >= 0) {
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_16_2) >= 0) {
byteBuf.writeBoolean(isHardcore);
byteBuf.writeByte(gamemode);
} else {
byteBuf.writeByte(isHardcore ? gamemode | 0x8 : gamemode);
}
byteBuf.writeByte(previousGamemode);
writeStringArray(byteBuf, levelNames);
writeCompoundTag(byteBuf, registry);
if (protocolVersion.compareTo(MINECRAFT_1_16_2) >= 0
&& protocolVersion.compareTo(MINECRAFT_1_19) < 0) {
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_16_2) >= 0
&& protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_19) < 0) {
writeCompoundTag(byteBuf, currentDimensionData);
writeString(byteBuf, dimensionInfo.getIdentifier());
} else {
@@ -132,15 +126,14 @@ public final class JoinGame implements FallbackPacket {
}
byteBuf.writeLong(partialHashedSeed);
if (protocolVersion.compareTo(MINECRAFT_1_16_2) >= 0) {
writeVarInt(byteBuf, 1); // max players
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_16_2) >= 0) {
writeVarInt(byteBuf, 0); // max players
} else {
byteBuf.writeByte(1); // max players
byteBuf.writeByte(0); // max players
}
writeVarInt(byteBuf, viewDistance);
if (protocolVersion.compareTo(MINECRAFT_1_18) >= 0) {
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_18) >= 0) {
writeVarInt(byteBuf, simulationDistance);
}
@@ -150,16 +143,47 @@ public final class JoinGame implements FallbackPacket {
byteBuf.writeBoolean(dimensionInfo.isDebug());
byteBuf.writeBoolean(dimensionInfo.isFlat());
if (protocolVersion.compareTo(MINECRAFT_1_19) >= 0) {
// no last death location
byteBuf.writeBoolean(false);
// optional death location
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_19) >= 0) {
byteBuf.writeBoolean(false); // no last death position
}
if (protocolVersion.compareTo(MINECRAFT_1_20) >= 0) {
writeVarInt(byteBuf, portalCooldown);
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_20) >= 0) {
writeVarInt(byteBuf, 0); // portal cooldown
}
}
private void encode1202Up(final @NotNull ByteBuf byteBuf,
final @SuppressWarnings("unused") ProtocolVersion protocolVersion) {
byteBuf.writeInt(entityId);
byteBuf.writeBoolean(isHardcore);
writeStringArray(byteBuf, levelNames);
writeVarInt(byteBuf, 1); // max players
writeVarInt(byteBuf, viewDistance);
writeVarInt(byteBuf, simulationDistance);
byteBuf.writeBoolean(reducedDebugInfo);
byteBuf.writeBoolean(showRespawnScreen);
byteBuf.writeBoolean(limitedCrafting);
writeString(byteBuf, dimensionInfo.getIdentifier());
writeString(byteBuf, dimensionInfo.getLevelName());
byteBuf.writeLong(partialHashedSeed);
byteBuf.writeByte(gamemode);
byteBuf.writeByte(previousGamemode);
byteBuf.writeBoolean(dimensionInfo.isDebug());
byteBuf.writeBoolean(dimensionInfo.isFlat());
byteBuf.writeBoolean(false); // no last death position
writeVarInt(byteBuf, 0); // portal cooldown
}
@Override
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
throw new UnsupportedOperationException();
@@ -15,12 +15,14 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.protocol.packets;
package xyz.jonesdev.sonar.common.fallback.protocol.packets.play;
import io.netty.buffer.ByteBuf;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
@@ -30,13 +32,14 @@ import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.readVarInt;
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.writeVarInt;
@Getter
@ToString
@NoArgsConstructor
@AllArgsConstructor
public final class KeepAlive implements FallbackPacket {
private long id;
@Override
public void encode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
public void encode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
if (protocolVersion.compareTo(MINECRAFT_1_12_2) >= 0) {
byteBuf.writeLong(id);
} else if (protocolVersion.compareTo(MINECRAFT_1_8) >= 0) {
@@ -47,7 +50,7 @@ public final class KeepAlive implements FallbackPacket {
}
@Override
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
public void decode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
if (protocolVersion.compareTo(MINECRAFT_1_12_2) >= 0) {
id = byteBuf.readLong();
} else if (protocolVersion.compareTo(MINECRAFT_1_8) >= 0) {
@@ -15,13 +15,14 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.protocol.packets;
package xyz.jonesdev.sonar.common.fallback.protocol.packets.play;
import io.netty.buffer.ByteBuf;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
@@ -33,7 +34,7 @@ public final class Player implements FallbackPacket {
private boolean onGround;
@Override
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
public void decode(final @NotNull ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
onGround = byteBuf.readBoolean();
}
@@ -15,11 +15,12 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.protocol.packets;
package xyz.jonesdev.sonar.common.fallback.protocol.packets.play;
import io.netty.buffer.ByteBuf;
import lombok.Getter;
import lombok.ToString;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.netty.DeferredByteBufHolder;
@@ -45,7 +46,12 @@ public final class PluginMessage extends DeferredByteBufHolder implements Fallba
}
@Override
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
public int expectedMaxLength(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
return 0xfff + 1; // limit packet size to 4096
}
@Override
public void decode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
channel = readString(byteBuf);
if (protocolVersion.compareTo(MINECRAFT_1_13) >= 0) {
@@ -15,13 +15,14 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.protocol.packets;
package xyz.jonesdev.sonar.common.fallback.protocol.packets.play;
import io.netty.buffer.ByteBuf;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
@@ -34,7 +35,7 @@ public final class Position implements FallbackPacket {
private boolean onGround;
@Override
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
public void decode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
x = byteBuf.readDouble();
// https://github.com/jonesdevelopment/sonar/issues/20
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_8) < 0) {
@@ -51,7 +52,7 @@ public final class Position implements FallbackPacket {
}
@Override
public int expectedMaxLength(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
public int expectedMaxLength(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
return protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_8) < 0 ? 33 : 25;
}
@@ -15,13 +15,14 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.protocol.packets;
package xyz.jonesdev.sonar.common.fallback.protocol.packets.play;
import io.netty.buffer.ByteBuf;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
@@ -39,7 +40,7 @@ public final class PositionLook implements FallbackPacket {
private boolean onGround;
@Override
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
public void decode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
x = byteBuf.readDouble();
// https://github.com/jonesdevelopment/sonar/issues/20
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_8) < 0) {
@@ -53,7 +54,7 @@ public final class PositionLook implements FallbackPacket {
}
@Override
public void encode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
public void encode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
byteBuf.writeDouble(x);
byteBuf.writeDouble(y);
byteBuf.writeDouble(z);
@@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.protocol.packets;
package xyz.jonesdev.sonar.common.fallback.protocol.packets.play;
import io.netty.buffer.ByteBuf;
import lombok.AllArgsConstructor;
@@ -15,13 +15,14 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.protocol.packets;
package xyz.jonesdev.sonar.common.fallback.protocol.packets.play;
import io.netty.buffer.ByteBuf;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
@@ -36,7 +37,7 @@ public final class Transaction implements FallbackPacket {
private boolean accepted;
@Override
public void encode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
public void encode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
if (protocolVersion.compareTo(MINECRAFT_1_17) < 0) {
byteBuf.writeByte(windowId);
byteBuf.writeShort((short) id);
@@ -49,7 +50,7 @@ public final class Transaction implements FallbackPacket {
}
@Override
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
public void decode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
if (protocolVersion.compareTo(MINECRAFT_1_17) < 0) {
windowId = byteBuf.readByte();
id = byteBuf.readShort();
@@ -15,13 +15,14 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.protocol.packets;
package xyz.jonesdev.sonar.common.fallback.protocol.packets.play;
import io.netty.buffer.ByteBuf;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.block.ChangedBlock;
@@ -39,37 +40,36 @@ public final class UpdateSectionBlocks implements FallbackPacket {
private ChangedBlock[] changedBlocks;
@Override
public void encode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) throws Exception {
public void encode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) throws Exception {
if (protocolVersion.compareTo(MINECRAFT_1_16_2) < 0) {
byteBuf.writeInt(sectionX);
byteBuf.writeInt(sectionZ);
if (protocolVersion.compareTo(MINECRAFT_1_8) < 0) {
byteBuf.writeShort((short) changedBlocks.length);
byteBuf.writeShort(changedBlocks.length);
byteBuf.writeInt(4 * changedBlocks.length);
for (final ChangedBlock block : changedBlocks) {
byteBuf.writeShort(block.getLegacyChunkPosCrammed());
byteBuf.writeShort(block.getType().getId(protocolVersion));
}
} else {
writeVarInt(byteBuf, changedBlocks.length);
}
for (final ChangedBlock block : changedBlocks) {
byteBuf.writeShort(block.getLegacyChunkPosCrammed());
writeVarInt(byteBuf, block.getType().getId(protocolVersion));
for (final ChangedBlock block : changedBlocks) {
byteBuf.writeShort(block.getLegacyChunkPosCrammed());
final int id = block.getType().getId(protocolVersion);
if (protocolVersion.compareTo(MINECRAFT_1_13) >= 0) {
writeVarInt(byteBuf, id);
} else {
final int shiftedBlockId = id << 4;
if (protocolVersion.compareTo(MINECRAFT_1_8) < 0) {
byteBuf.writeShort(shiftedBlockId);
} else {
writeVarInt(byteBuf, shiftedBlockId);
}
}
}
} else {
final ChangedBlock lastChangedBlock = changedBlocks[changedBlocks.length - 1];
final int sectionY = lastChangedBlock.getPosition().getY() >> 4;
final int sectionY = changedBlocks[0].getPosition().getY() >> 4;
// Why is Mojang doing this? :(
byteBuf.writeLong(
((long) (sectionX & 0x3FFFFF) << 42)
| (sectionY & 0xFFFFF)
| ((long) (sectionZ & 0x3FFFFF) << 20)
);
byteBuf.writeLong(((sectionX & 0x3FFFFFL) << 42) | (sectionY & 0xFFFFF) | ((sectionZ & 0x3FFFFFL) << 20));
// 1.20+ don't have light update suppression
if (protocolVersion.compareTo(MINECRAFT_1_20) < 0) {
@@ -80,7 +80,8 @@ public final class UpdateSectionBlocks implements FallbackPacket {
for (final ChangedBlock block : changedBlocks) {
final int shiftedBlockId = block.getType().getId(protocolVersion) << 12;
writeVarLong(byteBuf, shiftedBlockId | block.getModernChunkPosCrammed());
final long positionIdValue = shiftedBlockId | block.getModernChunkPosCrammed();
writeVarLong(byteBuf, positionIdValue);
}
}
}
@@ -27,13 +27,11 @@ public class TrafficChannelHooker implements FallbackPipelines {
public void hook(final @NotNull ChannelPipeline pipeline,
final @NotNull String decoder,
final @NotNull String encoder) {
pipeline.channel().eventLoop().execute(() -> {
if (pipeline.get(FALLBACK_IN_TRAFFIC) == null && pipeline.get(decoder) != null) {
pipeline.addBefore(decoder, FALLBACK_IN_TRAFFIC, IncomingBandwidthDecoder.INSTANCE);
}
if (pipeline.get(FALLBACK_OUT_TRAFFIC) == null && pipeline.get(encoder) != null) {
pipeline.addBefore(encoder, FALLBACK_OUT_TRAFFIC, OutgoingBandwidthEncoder.INSTANCE);
}
});
if (pipeline.get(FALLBACK_IN_TRAFFIC) == null && pipeline.get(decoder) != null) {
pipeline.addBefore(decoder, FALLBACK_IN_TRAFFIC, IncomingBandwidthDecoder.INSTANCE);
}
if (pipeline.get(FALLBACK_OUT_TRAFFIC) == null && pipeline.get(encoder) != null) {
pipeline.addBefore(encoder, FALLBACK_OUT_TRAFFIC, OutgoingBandwidthEncoder.INSTANCE);
}
}
}
@@ -25,7 +25,6 @@ import xyz.jonesdev.sonar.api.command.subcommand.SubcommandInfo;
import xyz.jonesdev.sonar.api.command.subcommand.argument.Argument;
import java.net.InetAddress;
import java.net.UnknownHostException;
@SubcommandInfo(
name = "blacklist",
@@ -40,83 +39,60 @@ import java.net.UnknownHostException;
public final class BlacklistCommand extends Subcommand {
@Override
public void execute(final @NotNull CommandInvocation invocation) {
protected void execute(final @NotNull CommandInvocation invocation) {
switch (invocation.getRawArguments()[1].toLowerCase()) {
case "add": {
if (invocation.getRawArguments().length <= 2) {
invocation.getSender().sendMessage(SONAR.getConfig().getIncorrectCommandUsage()
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getIncorrectCommandUsage()
.replace("%usage%", "blacklist add <IP address>"));
return;
}
final String rawInetAddress = invocation.getRawArguments()[2];
if (!IP_PATTERN.matcher(rawInetAddress).matches()) {
invocation.getSender().sendMessage(SONAR.getConfig().getIncorrectIpAddress());
return;
}
final InetAddress inetAddress;
try {
inetAddress = InetAddress.getByName(rawInetAddress);
if (inetAddress.isAnyLocalAddress() || inetAddress.isLoopbackAddress()) {
invocation.getSender().sendMessage(SONAR.getConfig().getIllegalIpAddress());
return;
}
} catch (UnknownHostException exception) {
invocation.getSender().sendMessage(SONAR.getConfig().getIllegalIpAddress());
return;
}
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())) {
invocation.getSender().sendMessage(SONAR.getConfig().getBlacklistDuplicate());
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getBlacklistDuplicate());
return;
}
// Display a warning if the IP is verified but being added to the blacklist
if (SONAR.getVerifiedPlayerController().has(inetAddress)) {
invocation.getSender().sendMessage(SONAR.getConfig().getBlacklistAddWarning()
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getBlacklistAddWarning()
.replace("%ip%", rawInetAddress));
}
// Blacklist the given IP address
SONAR.getFallback().getBlacklisted().put(inetAddress.toString());
invocation.getSender().sendMessage(SONAR.getConfig().getBlacklistAdd()
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getBlacklistAdd()
.replace("%ip%", rawInetAddress));
break;
}
case "remove": {
if (invocation.getRawArguments().length <= 2) {
invocation.getSender().sendMessage(SONAR.getConfig().getIncorrectCommandUsage()
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getIncorrectCommandUsage()
.replace("%usage%", "blacklist remove <IP address>"));
return;
}
final String rawInetAddress = invocation.getRawArguments()[2];
if (!IP_PATTERN.matcher(rawInetAddress).matches()) {
invocation.getSender().sendMessage(SONAR.getConfig().getIncorrectIpAddress());
return;
}
final InetAddress inetAddress;
try {
inetAddress = InetAddress.getByName(rawInetAddress);
if (inetAddress.isAnyLocalAddress() || inetAddress.isLoopbackAddress()) {
invocation.getSender().sendMessage(SONAR.getConfig().getIllegalIpAddress());
return;
}
} catch (UnknownHostException exception) {
invocation.getSender().sendMessage(SONAR.getConfig().getIllegalIpAddress());
return;
}
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())) {
invocation.getSender().sendMessage(SONAR.getConfig().getBlacklistNotFound());
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getBlacklistNotFound());
return;
}
// Invalidate the cache entry of the blacklisted IP address
SONAR.getFallback().getBlacklisted().invalidate(inetAddress.toString());
invocation.getSender().sendMessage(SONAR.getConfig().getBlacklistRemove()
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getBlacklistRemove()
.replace("%ip%", rawInetAddress));
break;
}
@@ -125,18 +101,19 @@ public final class BlacklistCommand extends Subcommand {
final int blacklisted = SONAR.getFallback().getBlacklisted().estimatedSize();
if (blacklisted == 0) {
invocation.getSender().sendMessage(SONAR.getConfig().getBlacklistEmpty());
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getBlacklistEmpty());
return;
}
// Invalidate all cache entries
SONAR.getFallback().getBlacklisted().invalidateAll();
invocation.getSender().sendMessage(SONAR.getConfig().getBlacklistCleared()
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getBlacklistCleared()
.replace("%removed%", Sonar.DECIMAL_FORMAT.format(blacklisted)));
break;
}
case "size": {
invocation.getSender().sendMessage(SONAR.getConfig().getBlacklistSize()
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getBlacklistSize()
.replace("%amount%", Sonar.DECIMAL_FORMAT.format(SONAR.getFallback().getBlacklisted().estimatedSize())));
break;
}
@@ -17,14 +17,23 @@
package xyz.jonesdev.sonar.common.subcommand.impl;
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;
import xyz.jonesdev.sonar.api.command.subcommand.SubcommandInfo;
import xyz.jonesdev.sonar.api.profiler.JVMProfiler;
import java.util.List;
import java.util.Map;
import java.util.WeakHashMap;
// This command is only used to display helpful information in order to help
// the Sonar developer(s) to fix issues more quickly
// the Sonar contributors to fix issues more quickly
@SubcommandInfo(
name = "dump",
description = "Print developer information",
@@ -33,22 +42,72 @@ import xyz.jonesdev.sonar.api.profiler.JVMProfiler;
public final class DumpCommand extends Subcommand implements JVMProfiler {
@Override
public void execute(final @NotNull CommandInvocation invocation) {
final String osName = System.getProperty("os.name");
final String osArch = System.getProperty("os.arch");
final String jvmArgs = String.join(",", RUNTIME_MX_BEAN.getInputArguments());
protected void execute(final @NotNull CommandInvocation invocation) {
final String json = Sonar.GENERAL_GSON.toJson(collectMappedInformation());
SONAR.getLogger().info("Generated dump: {}", json);
}
invocation.getSender().sendMessage("Virtual cores: " + getVirtualCores());
invocation.getSender().sendMessage("JVM args: " + jvmArgs);
invocation.getSender().sendMessage("Java name: " + RUNTIME_MX_BEAN.getVmName());
invocation.getSender().sendMessage("Java version: " + RUNTIME_MX_BEAN.getVmVersion());
invocation.getSender().sendMessage("Java vendor: " + RUNTIME_MX_BEAN.getVmVendor());
invocation.getSender().sendMessage("Operating system: " + osName + ", " + osArch);
invocation.getSender().sendMessage("Total memory: " + formatMemory(getTotalMemory()));
invocation.getSender().sendMessage("Max memory: " + formatMemory(getMaxMemory()));
invocation.getSender().sendMessage("Free memory: " + formatMemory(getFreeMemory()));
invocation.getSender().sendMessage("Used Memory: " + formatMemory(getUsedMemory()));
invocation.getSender().sendMessage("Platform: " + SONAR.getServer().getPlatform().getDisplayName());
invocation.getSender().sendMessage("Version: " + SONAR.getVersion().getFull());
@Unmodifiable
private @NotNull Map<String, Object> collectMappedInformation() {
val mappings = new WeakHashMap<String, Object>();
mappings.put("sonar", new Dump.Sonar(
SONAR.getVersion().getFull(),
SONAR.getPlatform(),
SONAR.getVersion().isOnMainBranch()
));
mappings.put("runtime", new Dump.Runtime(
getVirtualCores(),
RUNTIME_MX_BEAN.getInputArguments(),
RUNTIME_MX_BEAN.getVmName(),
RUNTIME_MX_BEAN.getVmVendor(),
RUNTIME_MX_BEAN.getVmVersion()
));
mappings.put("os", new Dump.OS(
System.getProperty("os.name"),
System.getProperty("os.arch"),
System.getProperty("os.version")
));
mappings.put("memory", new Dump.Memory(
formatMemory(getTotalMemory()),
formatMemory(getMaxMemory()),
formatMemory(getFreeMemory()),
formatMemory(getUsedMemory())
));
return mappings;
}
@SuppressWarnings("unused")
private static final class Dump {
@RequiredArgsConstructor
private static final class Sonar {
private final String version;
private final SonarPlatform platform;
private final boolean isOnMainBranch;
}
@RequiredArgsConstructor
private static final class Runtime {
private final int virtualCores;
private final List<String> jvmArguments;
private final String vmName;
private final String vmVendor;
private final String vmVersion;
}
@RequiredArgsConstructor
private static final class Memory {
private final String total;
private final String max;
private final String free;
private final String used;
}
@RequiredArgsConstructor
private static final class OS {
private final String name;
private final String arch;
private final String version;
}
}
}
@@ -24,18 +24,18 @@ import xyz.jonesdev.sonar.api.command.subcommand.SubcommandInfo;
@SubcommandInfo(
name = "lockdown",
description = "Lock the server down"
description = "Prevent all players from joining"
)
public final class LockdownCommand extends Subcommand {
@Override
public void execute(final @NotNull CommandInvocation invocation) {
final boolean newState = !SONAR.getConfig().isLockdownEnabled();
SONAR.getConfig().setLockdownEnabled(newState);
SONAR.getConfig().getGeneralConfig().set("general.lockdown.enabled", newState);
protected void execute(final @NotNull CommandInvocation invocation) {
final boolean newState = !SONAR.getConfig().getLockdown().isEnabled();
SONAR.getConfig().getLockdown().setEnabled(newState);
SONAR.getConfig().getGeneralConfig().set("lockdown.enabled", newState);
SONAR.getConfig().getGeneralConfig().save();
invocation.getSender().sendMessage(newState
? SONAR.getConfig().getLockdownActivated() : SONAR.getConfig().getLockdownDeactivated());
? SONAR.getConfig().getLockdown().getActivated() : SONAR.getConfig().getLockdown().getDeactivated());
}
}
@@ -25,18 +25,18 @@ import xyz.jonesdev.sonar.api.timer.SystemTimer;
@SubcommandInfo(
name = "reload",
description = "Reload the configuration"
description = "Reload all configurations"
)
public final class ReloadCommand extends Subcommand {
@Override
public void execute(final @NotNull CommandInvocation invocation) {
protected void execute(final @NotNull CommandInvocation invocation) {
final SystemTimer timer = new SystemTimer();
invocation.getSender().sendMessage(SONAR.getConfig().getReloading());
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getReloading());
SONAR.reload();
invocation.getSender().sendMessage(SONAR.getConfig().getReloaded()
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getReloaded()
.replace("%taken%", String.valueOf(timer.delay())));
}
}
@@ -40,60 +40,71 @@ import static xyz.jonesdev.sonar.api.Sonar.DECIMAL_FORMAT;
argumentsRequired = false
)
public final class StatisticsCommand extends Subcommand implements JVMProfiler {
private enum StatisticType {
GENERAL,
NETWORK,
MEMORY,
CPU
}
@Override
public void execute(final @NotNull CommandInvocation invocation) {
final String showing = invocation.getRawArguments().length >= 2
? invocation.getRawArguments()[1].toLowerCase() : "bot";
invocation.getSender().sendMessage("§eShowing " + showing + " statistics for this session:");
invocation.getSender().sendMessage("§ePlease note that session statistics are not saved when the server restarts.");
invocation.getSender().sendMessage();
if (!showing.equals("bot")) {
switch (showing) {
case "network": {
invocation.getSender().sendMessage(" §7▪ §aCurrent incoming used bandwidth: §f" + TrafficCounter.INCOMING.getCachedSecond());
invocation.getSender().sendMessage(" §7▪ §aCurrent outgoing used bandwidth: §f" + TrafficCounter.OUTGOING.getCachedSecond());
invocation.getSender().sendMessage(" §7▪ §aTotal incoming used bandwidth: §f" + TrafficCounter.INCOMING.getCachedTtl());
invocation.getSender().sendMessage(" §7▪ §aTotal outgoing used bandwidth: §f" + TrafficCounter.OUTGOING.getCachedTtl());
return;
}
case "memory": {
invocation.getSender().sendMessage(" §7▪ §aTotal free memory (JVM): §f" + formatMemory(getFreeMemory()));
invocation.getSender().sendMessage(" §7▪ §aTotal used memory (JVM): §f" + formatMemory(getUsedMemory()));
invocation.getSender().sendMessage(" §7▪ §aTotal maximum memory (JVM): §f" + formatMemory(getMaxMemory()));
invocation.getSender().sendMessage(" §7▪ §aTotal allocated memory (JVM): §f" + formatMemory(getTotalMemory()));
return;
}
case "cpu": {
invocation.getSender().sendMessage(" §7▪ §aProcess CPU usage right now: §f" + DECIMAL_FORMAT.format(getProcessCPUUsage()) + "%");
invocation.getSender().sendMessage(" §7▪ §aSystem CPU usage right now: §f" + DECIMAL_FORMAT.format(getSystemCPUUsage()) + "%");
invocation.getSender().sendMessage(" §7▪ §aPer-core process CPU usage: §f" + DECIMAL_FORMAT.format(getAverageProcessCPUUsage()) + "%");
invocation.getSender().sendMessage(" §7▪ §aPer-core system CPU usage: §f" + DECIMAL_FORMAT.format(getAverageSystemCPUUsage()) + "%");
invocation.getSender().sendMessage(" §7▪ §aGeneral system load average: §f" + DECIMAL_FORMAT.format(getSystemLoadAverage()) + "%");
invocation.getSender().sendMessage(" §7▪ §aTotal virtual cpu cores (JVM): §f" + DECIMAL_FORMAT.format(getVirtualCores()));
return;
}
protected void execute(final @NotNull CommandInvocation invocation) {
StatisticType type = StatisticType.GENERAL;
if (invocation.getRawArguments().length >= 2) {
try {
type = StatisticType.valueOf(invocation.getRawArguments()[1].toUpperCase());
} catch (Exception exception) {
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getUnknownStatisticType()
.replace("%statistics%", getArguments()));
return;
}
}
final int total = Statistics.TOTAL_TRAFFIC.get();
final int real = Statistics.REAL_TRAFFIC.get();
final int failed = Statistics.FAILED_VERIFICATIONS.get();
final int queued = SONAR.getFallback().getQueue().getQueuedPlayers().size();
final int verifying = SONAR.getFallback().getConnected().size();
final int verified = SONAR.getVerifiedPlayerController().estimatedSize();
final int blacklisted = SONAR.getFallback().getBlacklisted().estimatedSize();
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getStatisticsHeader()
.replace("%type%", type.name().toLowerCase()));
invocation.getSender().sendMessage();
invocation.getSender().sendMessage(" §7▪ §aVerified IP addresses: §f" + DECIMAL_FORMAT.format(verified));
invocation.getSender().sendMessage(" §7▪ §aVerifying IP addresses: §f" + DECIMAL_FORMAT.format(verifying));
invocation.getSender().sendMessage(" §7▪ §aBlacklisted IP addresses: §f" + DECIMAL_FORMAT.format(blacklisted));
invocation.getSender().sendMessage(" §7▪ §aCurrently queued logins: §f" + DECIMAL_FORMAT.format(queued));
invocation.getSender().sendMessage(" §7▪ §aTotal non-unique joins: §f" + DECIMAL_FORMAT.format(total));
invocation.getSender().sendMessage(" §7▪ §aTotal verification attempts: §f" + DECIMAL_FORMAT.format(real));
invocation.getSender().sendMessage(" §7▪ §aTotal failed verifications: §f" + DECIMAL_FORMAT.format(failed));
switch (type) {
case GENERAL: {
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getGeneralStatistics()
.replace("%verified%", DECIMAL_FORMAT.format(SONAR.getVerifiedPlayerController().estimatedSize()))
.replace("%verifying%", DECIMAL_FORMAT.format(SONAR.getFallback().getConnected().size()))
.replace("%blacklisted%", DECIMAL_FORMAT.format(SONAR.getFallback().getBlacklisted().estimatedSize()))
.replace("%queued%", DECIMAL_FORMAT.format(SONAR.getFallback().getQueue().getQueuedPlayers().size()))
.replace("%total_joins%", DECIMAL_FORMAT.format(Statistics.TOTAL_TRAFFIC.get()))
.replace("%total_attempts%", DECIMAL_FORMAT.format(Statistics.REAL_TRAFFIC.get()))
.replace("%total_failed%", DECIMAL_FORMAT.format(Statistics.FAILED_VERIFICATIONS.get())));
break;
}
case CPU: {
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getCpuStatistics()
.replace("%process_cpu%", DECIMAL_FORMAT.format(getProcessCPUUsage()))
.replace("%system_cpu%", DECIMAL_FORMAT.format(getSystemCPUUsage()))
.replace("%average_process_cpu%", DECIMAL_FORMAT.format(getAverageProcessCPUUsage()))
.replace("%average_system_cpu%", DECIMAL_FORMAT.format(getAverageSystemCPUUsage()))
.replace("%load_average%", DECIMAL_FORMAT.format(getSystemLoadAverage()))
.replace("%virtual_cores%", DECIMAL_FORMAT.format(getVirtualCores())));
break;
}
case MEMORY: {
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getMemoryStatistics()
.replace("%free_memory%", formatMemory(getFreeMemory()))
.replace("%used_memory%", formatMemory(getUsedMemory()))
.replace("%max_memory%", formatMemory(getMaxMemory()))
.replace("%total_memory%", formatMemory(getTotalMemory())));
break;
}
case NETWORK: {
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getNetworkStatistics()
.replace("%incoming%", TrafficCounter.INCOMING.getCachedSecond())
.replace("%outgoing%", TrafficCounter.OUTGOING.getCachedSecond())
.replace("%ttl_incoming%", TrafficCounter.INCOMING.getCachedTtl())
.replace("%ttl_outgoing%", TrafficCounter.OUTGOING.getCachedTtl()));
break;
}
}
}
}
@@ -17,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().getVerboseUnsubscribedOther()
.replace("%player%", verboseSubscriber.getName()));
}
verboseSubscriber.sendMessage(SONAR.getConfig().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().getVerboseSubscribedOther()
.replace("%player%", verboseSubscriber.getName()));
}
verboseSubscriber.sendMessage(SONAR.getConfig().getVerboseSubscribed());
SONAR.getVerboseHandler().subscribe(verboseSubscriber.getName());
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getVerboseSubscribed());
SONAR.getVerboseHandler().subscribe(invocation.getSender().getName());
}
}
@@ -24,6 +24,7 @@ import xyz.jonesdev.sonar.api.command.subcommand.Subcommand;
import xyz.jonesdev.sonar.api.command.subcommand.SubcommandInfo;
import xyz.jonesdev.sonar.api.command.subcommand.argument.Argument;
import java.net.InetAddress;
import java.util.Queue;
import java.util.UUID;
import java.util.concurrent.LinkedBlockingQueue;
@@ -39,73 +40,70 @@ import java.util.concurrent.LinkedBlockingQueue;
}
)
public final class VerifiedCommand extends Subcommand {
private static final Queue<String> LOCK = new LinkedBlockingQueue<>(10);
private static final Queue<String> LOCK = new LinkedBlockingQueue<>(1);
@Override
public void execute(final @NotNull CommandInvocation invocation) {
protected void execute(final @NotNull CommandInvocation invocation) {
switch (invocation.getRawArguments()[1].toLowerCase()) {
case "history": {
if (invocation.getRawArguments().length <= 2) {
invocation.getSender().sendMessage(SONAR.getConfig().getIncorrectCommandUsage()
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getIncorrectCommandUsage()
.replace("%usage%", "verified history <IP address>"));
return;
}
final String rawInetAddress = invocation.getRawArguments()[2];
if (!IP_PATTERN.matcher(rawInetAddress).matches()) {
invocation.getSender().sendMessage(SONAR.getConfig().getIncorrectIpAddress());
final InetAddress inetAddress = getInetAddressIfValid(invocation.getSender(), rawInetAddress);
// Make sure the given IP address is valid
if (inetAddress == null) return;
// Make sure the IP is verified already
if (!SONAR.getVerifiedPlayerController().has(inetAddress.toString())) {
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getVerifiedNotFound());
return;
}
// We don't need to parse this, so we can just use the string.
final String realInetAddress = "/" + rawInetAddress;
if (!SONAR.getVerifiedPlayerController().has(realInetAddress)) {
invocation.getSender().sendMessage(SONAR.getConfig().getVerifiedNotFound());
return;
}
invocation.getSender().sendMessage("§ePrevious UUIDs for " + rawInetAddress + ":");
for (final UUID uuid : SONAR.getVerifiedPlayerController().getUUIDs(realInetAddress)) {
invocation.getSender().sendMessage(" §7▪ §f" + uuid.toString());
invocation.getSender().sendMessage("<yellow>Previous UUIDs for " + rawInetAddress + ":");
for (final UUID uuid : SONAR.getVerifiedPlayerController().getUUIDs(inetAddress.toString())) {
invocation.getSender().sendMessage(" <gray>▪ <white>" + uuid.toString());
}
break;
}
case "remove": {
if (invocation.getRawArguments().length <= 2) {
invocation.getSender().sendMessage(SONAR.getConfig().getIncorrectCommandUsage()
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getIncorrectCommandUsage()
.replace("%usage%", "verified remove <IP address>"));
return;
}
final String rawInetAddress = invocation.getRawArguments()[2];
if (!IP_PATTERN.matcher(rawInetAddress).matches()) {
invocation.getSender().sendMessage(SONAR.getConfig().getIncorrectIpAddress());
// Make sure we aren't currently locking the IP address to avoid a double I/O operation
if (LOCK.contains(rawInetAddress)) {
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getVerifiedBlocked());
return;
}
// We don't need to parse this, so we can just use the string.
final String realInetAddress = "/" + rawInetAddress;
final InetAddress inetAddress = getInetAddressIfValid(invocation.getSender(), rawInetAddress);
// Make sure the given IP address is valid
if (inetAddress == null) return;
if (LOCK.contains(realInetAddress)) {
invocation.getSender().sendMessage(SONAR.getConfig().getVerifiedBlocked());
return;
}
if (!SONAR.getVerifiedPlayerController().has(realInetAddress)) {
invocation.getSender().sendMessage(SONAR.getConfig().getVerifiedNotFound());
// Make sure the player is verified already
if (!SONAR.getVerifiedPlayerController().has(inetAddress.toString())) {
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getVerifiedNotFound());
return;
}
// Lock the IP address
// Make sure we don't accidentally run 2 operations at the same time
LOCK.add(realInetAddress);
SONAR.getVerifiedPlayerController().remove(realInetAddress);
LOCK.remove(realInetAddress);
LOCK.add(rawInetAddress);
SONAR.getVerifiedPlayerController().remove(inetAddress.toString());
invocation.getSender().sendMessage(SONAR.getConfig().getVerifiedRemove()
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getVerifiedRemove()
.replace("%ip%", rawInetAddress));
// Unlock the IP address
LOCK.remove(rawInetAddress);
break;
}
@@ -113,18 +111,19 @@ public final class VerifiedCommand extends Subcommand {
final int verified = SONAR.getVerifiedPlayerController().estimatedSize();
if (verified == 0) {
invocation.getSender().sendMessage(SONAR.getConfig().getVerifiedEmpty());
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getVerifiedEmpty());
return;
}
// Invalidate all cache entries
SONAR.getVerifiedPlayerController().clearAll();
invocation.getSender().sendMessage(SONAR.getConfig().getVerifiedCleared()
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getVerifiedCleared()
.replace("%removed%", Sonar.DECIMAL_FORMAT.format(verified)));
break;
}
case "size": {
invocation.getSender().sendMessage(SONAR.getConfig().getVerifiedSize()
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getVerifiedSize()
.replace("%amount%", Sonar.DECIMAL_FORMAT.format(SONAR.getVerifiedPlayerController().estimatedSize())));
break;
}
@@ -0,0 +1,116 @@
package xyz.jonesdev.sonar.common.update;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import lombok.experimental.UtilityClass;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.logger.LoggerWrapper;
import javax.net.ssl.HttpsURLConnection;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@UtilityClass
public class UpdateChecker {
private final ExecutorService ASYNC_EXECUTOR = Executors.newSingleThreadExecutor();
private final LoggerWrapper LOGGER = new LoggerWrapper() {
@Override
public void info(final String message, final Object... args) {
Sonar.get().getLogger().info("[update-checker] " + message, args);
}
@Override
public void warn(final String message, final Object... args) {
Sonar.get().getLogger().warn("[update-checker] " + message, args);
}
@Override
public void error(final String message, final Object... args) {
Sonar.get().getLogger().error("[update-checker] " + message, args);
}
};
private int convertVersion(final @NotNull String version) throws NumberFormatException, IllegalStateException {
final String[] convertedParts = version.split("\\.");
// Validate the length of the converted parts
if (convertedParts.length != 3) {
throw new IllegalStateException("Converted version length mismatch");
}
// Major updates are more important than minor updates,
// and minor updates are more important than patches.
final int convertedMajor = Integer.parseInt(convertedParts[0]) * 100000; // multiply by weight
final int convertedMinor = Integer.parseInt(convertedParts[1]) * 10000; // multiply by weight
final int patch = Integer.parseInt(convertedParts[2]);
return convertedMajor + convertedMinor + patch; // sum the converted version parts
}
public void checkForUpdates() {
ASYNC_EXECUTOR.execute(() -> {
try {
final URL url = new URL("https://api.github.com/repos/jonesdevelopment/sonar/releases/latest");
final HttpsURLConnection urlConnection = prepareConnection(url);
final JsonObject json = parseJson(urlConnection);
final String latestStableRelease = json.get("tag_name").getAsString();
final int convertedLatestVersion = convertVersion(latestStableRelease);
final int convertedCurrentVersion = convertVersion(Sonar.get().getVersion().getSemanticVersion());
if (convertedCurrentVersion < convertedLatestVersion) {
LOGGER.warn("A new version of Sonar is available: {}", latestStableRelease);
LOGGER.warn("Please make sure to update to the latest version to ensure stability and security:");
LOGGER.warn("https://github.com/jonesdevelopment/sonar/releases/tag/{}", latestStableRelease);
} else if (convertedCurrentVersion > convertedLatestVersion || !Sonar.get().getVersion().isOnMainBranch()) {
LOGGER.warn("You are currently using an unreleased version of Sonar!");
LOGGER.warn("The contributors of Sonar are not responsible for any damage done by using an unstable version");
} else {
LOGGER.info("You are currently using the latest stable release of Sonar!");
}
} catch (Exception exception) {
LOGGER.warn("Could not retrieve latest version information: {}", exception);
}
});
}
private @NotNull JsonObject parseJson(final @NotNull 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;
while ((line = bufferedReader.readLine()) != null) {
content.append(line);
}
}
// Parse site content as JSON using Gson
final JsonParser jsonParser = new JsonParser();
return jsonParser.parse(content.toString()).getAsJsonObject();
}
}
}
private @NotNull HttpsURLConnection prepareConnection(final @NotNull URL url) throws Exception {
final HttpsURLConnection urlConnection = (HttpsURLConnection) url.openConnection();
// Set connection timeout
urlConnection.setConnectTimeout(15000);
urlConnection.setReadTimeout(15000);
// Set necessary properties
urlConnection.setRequestMethod("GET");
urlConnection.setRequestProperty("Accept", "application/vnd.github.v3+json");
// Only continue if the connection is a success
if (urlConnection.getResponseCode() != 200) {
throw new IllegalStateException("Unexpected response code " + urlConnection.getResponseCode());
}
return urlConnection;
}
}
@@ -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.hasAttr(playerAttribute());
}
}
@@ -21,6 +21,7 @@ import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufOutputStream;
import io.netty.buffer.ByteBufUtil;
import io.netty.handler.codec.CorruptedFrameException;
import io.netty.handler.codec.DecoderException;
import io.netty.handler.codec.EncoderException;
import lombok.experimental.UtilityClass;
import net.kyori.adventure.nbt.BinaryTagIO;
@@ -33,7 +34,7 @@ import java.nio.charset.StandardCharsets;
import java.util.UUID;
import java.util.regex.Pattern;
// Mostly taken from
// Taken from
// https://github.com/PaperMC/Velocity/blob/dev/3.0.0/proxy/src/main/java/com/velocitypowered/proxy/protocol/ProtocolUtils.java
@UtilityClass
public class ProtocolUtil {
@@ -47,19 +48,33 @@ public class ProtocolUtil {
private static final String UNREGISTER_CHANNEL = "minecraft:unregister";
private static final Pattern INVALID_IDENTIFIER_REGEX = Pattern.compile("[^a-z0-9\\-_]*");
public static @NotNull String readString(final ByteBuf buf) throws CorruptedFrameException {
return readString(buf, Short.MAX_VALUE);
public static @NotNull String readBrandMessage(final @NotNull ByteBuf content) throws DecoderException {
final ByteBuf slice = content.slice();
try {
return readString(slice);
} catch (DecoderException exception) {
return readString(slice, 65536, slice.readableBytes());
}
}
public static @NotNull String readString(final ByteBuf buf,
final int cap) throws CorruptedFrameException {
final int length = VarIntUtil.readVarInt(buf);
public static @NotNull String readString(final ByteBuf byteBuf) throws CorruptedFrameException {
return readString(byteBuf, Short.MAX_VALUE);
}
public static @NotNull String readString(final ByteBuf byteBuf,
final int cap) throws CorruptedFrameException {
final int length = VarIntUtil.readVarInt(byteBuf);
return readString(byteBuf, cap, length);
}
public static @NotNull String readString(final @NotNull ByteBuf byteBuf,
final int cap,
final int length) throws CorruptedFrameException {
checkFrame(length >= 0, "Got a negative-length string");
checkFrame(length <= cap * 3, "Bad string size");
checkFrame(buf.isReadable(length), "Got an invalid string length");
final String str = buf.toString(buf.readerIndex(), length, StandardCharsets.UTF_8);
buf.skipBytes(length);
checkFrame(byteBuf.isReadable(length), "Tried to read a too-long string");
final String str = byteBuf.toString(byteBuf.readerIndex(), length, StandardCharsets.UTF_8);
byteBuf.skipBytes(length);
checkFrame(str.length() <= cap, "Got a too-long string");
return str;
}
+1 -2
View File
@@ -20,9 +20,8 @@ dependencies {
tasks {
processResources {
val props = mapOf(
"name" to rootProject.name,
"version" to rootProject.version.toString().split("-")[0],
"description" to "Effective Anti-bot plugin for Velocity, BungeeCord, and Bukkit (1.7-latest)",
"description" to rootProject.description,
"url" to "https://jonesdev.xyz/discord/",
"main" to "xyz.jonesdev.sonar.velocity.SonarVelocityPlugin"
)
@@ -20,17 +20,13 @@ package xyz.jonesdev.sonar.velocity;
import lombok.Getter;
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.common.boot.SonarBootstrap;
import xyz.jonesdev.sonar.velocity.command.VelocityInvocationSource;
import xyz.jonesdev.sonar.velocity.audience.AudienceListener;
import xyz.jonesdev.sonar.velocity.command.VelocitySonarCommand;
import xyz.jonesdev.sonar.velocity.fallback.FallbackListener;
import xyz.jonesdev.sonar.velocity.verbose.VerboseWrapper;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
@Getter
@@ -38,7 +34,7 @@ public final class SonarVelocity extends SonarBootstrap<SonarVelocityPlugin> {
public static SonarVelocity INSTANCE;
public SonarVelocity(final @NotNull SonarVelocityPlugin plugin) {
super(plugin, plugin.getDataDirectory().toFile(), new VerboseWrapper(plugin.getServer()));
super(plugin, plugin.getDataDirectory().toFile(), SonarPlatform.VELOCITY);
INSTANCE = this;
}
@@ -66,28 +62,11 @@ public final class SonarVelocity extends SonarBootstrap<SonarVelocityPlugin> {
}
};
/**
* Create a wrapper object for our server, so we can use it outside
* the velocity module.
* <br>
* We have to do this, so we can access all necessary API functions.
*/
public final ServerWrapper server = new ServerWrapper(SonarPlatform.VELOCITY) {
@Override
public Optional<InvocationSource> getOnlinePlayer(final String username) {
return getPlugin().getServer().getAllPlayers().stream()
.filter(player -> player.getUsername().equalsIgnoreCase(username))
.findFirst()
.map(VelocityInvocationSource::new);
}
};
@Override
public void enable() {
// Initialize bStats.org metrics
getPlugin().getMetricsFactory().make(getPlugin(), getServer().getPlatform().getMetricsId());
getPlugin().getMetricsFactory().make(getPlugin(), getPlatform().getMetricsId());
// Register Sonar command
getPlugin().getServer().getCommandManager().register("sonar", new VelocitySonarCommand());
@@ -95,6 +74,9 @@ public final class SonarVelocity extends SonarBootstrap<SonarVelocityPlugin> {
// Register Fallback listener
getPlugin().getServer().getEventManager().register(getPlugin(), new FallbackListener(getFallback()));
// Register audience register listener
getPlugin().getServer().getEventManager().register(getPlugin(), new AudienceListener());
// Register Fallback queue task
getPlugin().getServer().getScheduler().buildTask(getPlugin(), getFallback().getQueue()::poll)
.repeat(500L, TimeUnit.MILLISECONDS)
@@ -110,8 +92,4 @@ public final class SonarVelocity extends SonarBootstrap<SonarVelocityPlugin> {
.repeat(100L, TimeUnit.MILLISECONDS)
.schedule();
}
@Override
public void postReload() {
}
}
@@ -0,0 +1,38 @@
/*
* Copyright (C) 2023 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.velocity.audience;
import com.velocitypowered.api.event.PostOrder;
import com.velocitypowered.api.event.Subscribe;
import com.velocitypowered.api.event.connection.DisconnectEvent;
import com.velocitypowered.api.event.connection.PostLoginEvent;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.Sonar;
public final class AudienceListener {
@Subscribe(order = PostOrder.LAST)
public void handle(final @NotNull PostLoginEvent event) {
Sonar.get().getVerboseHandler().getAudiences().put(event.getPlayer().getUsername(), event.getPlayer());
}
@Subscribe(order = PostOrder.LAST)
public void handle(final @NotNull DisconnectEvent event) {
Sonar.get().getVerboseHandler().getAudiences().remove(event.getPlayer().getUsername());
}
}
@@ -24,8 +24,6 @@ import xyz.jonesdev.sonar.api.command.InvocationSource;
public final class VelocityInvocationSource extends InvocationSource {
public VelocityInvocationSource(final @NotNull CommandSource sender) {
super(sender instanceof Player
? ((Player) sender).getUsername()
: "Console", sender);
super(sender instanceof Player ? ((Player) sender).getUsername() : "Console", sender, sender instanceof Player);
}
}
@@ -18,35 +18,30 @@
package xyz.jonesdev.sonar.velocity.command;
import com.velocitypowered.api.command.SimpleCommand;
import com.velocitypowered.api.proxy.ConsoleCommandSource;
import com.velocitypowered.api.proxy.Player;
import net.kyori.adventure.text.Component;
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 VelocitySonarCommand implements SimpleCommand, SonarCommand {
{
cacheHelpMessage();
}
@Override
public void execute(final @NotNull Invocation invocation) {
if (!(invocation.source() instanceof ConsoleCommandSource)) {
// Create our own invocation source wrapper to handle messages properly
final InvocationSource invocationSource = new VelocityInvocationSource(invocation.source());
if (invocationSource.isPlayer()) {
// Check if the player actually has the permission to run the command
if (!invocation.source().hasPermission("sonar.command")) {
invocation.source().sendMessage(Component.text(Sonar.get().getConfig().getNoPermission()));
invocationSource.sendMessage(Sonar.get().getConfig().getNoPermission());
return;
}
// Checking if it contains will only break more since it can throw
@@ -58,14 +53,14 @@ public final class VelocitySonarCommand implements SimpleCommand, SonarCommand {
// 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) {
invocation.source().sendMessage(Component.text(Sonar.get().getConfig().getCommandCoolDown()));
invocationSource.sendMessage(Sonar.get().getConfig().getCommands().getCommandCoolDown());
// Format delay
final long timestamp = System.currentTimeMillis();
final double left = 0.5D - (timestamp - mapTimestamp) / 1000D;
invocation.source().sendMessage(Component.text(Sonar.get().getConfig().getCommandCoolDownLeft()
.replace("%time-left%", Sonar.DECIMAL_FORMAT.format(left))));
invocationSource.sendMessage(Sonar.get().getConfig().getCommands().getCommandCoolDownLeft()
.replace("%time-left%", Sonar.DECIMAL_FORMAT.format(left)));
return;
}
@@ -74,8 +69,6 @@ public final class VelocitySonarCommand implements SimpleCommand, SonarCommand {
Optional<Subcommand> subcommand = Optional.empty();
final InvocationSource invocationSource = new VelocityInvocationSource(invocation.source());
if (invocation.arguments().length > 0) {
// Search subcommand if command arguments are present
subcommand = Sonar.get().getSubcommandRegistry().getSubcommands().parallelStream()
@@ -89,41 +82,13 @@ public final class VelocitySonarCommand implements SimpleCommand, SonarCommand {
if (!subcommand.get().getInfo().onlyConsole()
&& !invocation.source().hasPermission(subcommand.get().getPermission())
) {
invocationSource.sendMessage(
Sonar.get().getConfig().getSubCommandNoPerm()
.replace("%permission%", subcommand.get().getPermission())
);
invocationSource.sendMessage(Sonar.get().getConfig().getCommands().getSubCommandNoPerm()
.replace("%permission%", subcommand.get().getPermission()));
return;
}
}
}
subcommand.ifPresent(sub -> {
if (sub.getInfo().onlyPlayers() && !(invocation.source() instanceof Player)) {
invocationSource.sendMessage(Sonar.get().getConfig().getPlayersOnly());
return;
}
if (sub.getInfo().onlyConsole() && !(invocation.source() instanceof ConsoleCommandSource)) {
invocationSource.sendMessage(Sonar.get().getConfig().getConsoleOnly());
return;
}
final CommandInvocation commandInvocation = new CommandInvocation(invocationSource, sub, invocation.arguments());
// The subcommands has arguments which are not present in the executed command
if (sub.getInfo().arguments().length > 0
&& commandInvocation.getRawArguments().length <= 1
&& sub.getInfo().argumentsRequired()) {
invocationSource.sendMessage(Sonar.get().getConfig().getIncorrectCommandUsage()
.replace("%usage%", sub.getInfo().name() + " (" + sub.getArguments() + ")"));
return;
}
// Execute the sub command with the custom invocation properties
sub.execute(commandInvocation);
});
if (subcommand.isEmpty()) {
// Re-use the old, cached help message since we don't want to scan
@@ -132,6 +97,8 @@ public final class VelocitySonarCommand implements SimpleCommand, SonarCommand {
for (final Component component : CACHED_HELP_MESSAGE) {
invocationSource.sendMessage(component);
}
} else {
subcommand.get().invoke(invocationSource, invocation.arguments());
}
}
@@ -141,33 +108,6 @@ public final class VelocitySonarCommand implements SimpleCommand, SonarCommand {
if (!invocation.source().hasPermission("sonar.command")) {
return emptyList();
}
if (invocation.arguments().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 (invocation.arguments().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.toUnmodifiableList());
ARG_TAB_SUGGESTIONS.put(subcommand.getInfo().name(), parsedArguments);
for (final String alias : subcommand.getInfo().aliases()) {
ARG_TAB_SUGGESTIONS.put(alias, parsedArguments);
}
}
}
final String subCommandName = invocation.arguments()[0].toLowerCase();
return ARG_TAB_SUGGESTIONS.getOrDefault(subCommandName, emptyList());
}
return emptyList();
return getCachedTabSuggestions(invocation.arguments());
}
}
@@ -20,8 +20,11 @@ package xyz.jonesdev.sonar.velocity.fallback;
import com.velocitypowered.api.event.PostOrder;
import com.velocitypowered.api.event.Subscribe;
import com.velocitypowered.api.event.connection.LoginEvent;
import com.velocitypowered.api.event.connection.PostLoginEvent;
import com.velocitypowered.api.event.connection.PreLoginEvent;
import com.velocitypowered.api.proxy.crypto.IdentifiedKey;
import com.velocitypowered.api.util.GameProfile;
import com.velocitypowered.proxy.VelocityServer;
import com.velocitypowered.proxy.connection.MinecraftConnection;
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
@@ -34,26 +37,36 @@ import io.netty.channel.ChannelPipeline;
import lombok.RequiredArgsConstructor;
import lombok.val;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage;
import org.jetbrains.annotations.NotNull;
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 xyz.jonesdev.sonar.velocity.SonarVelocity;
import xyz.jonesdev.sonar.velocity.fallback.dummy.DummyConnection;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.reflect.Field;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import static com.velocitypowered.proxy.network.Connections.*;
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_HANDLER;
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.*;
import static xyz.jonesdev.sonar.common.utility.geyser.GeyserUtil.isGeyserConnection;
@RequiredArgsConstructor
@@ -64,12 +77,35 @@ public final class FallbackListener {
private static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup();
private static final MethodHandle INITIAL_CONNECTION;
private static final MethodHandle CONNECTED_PLAYER;
private static final Field CONNECTION_FIELD;
private static Field SESSION_HANDLER_FIELD;
static {
CLOSED_MINECRAFT_CONNECTION = new DummyConnection(null);
try {
CONNECTED_PLAYER = MethodHandles.privateLookupIn(ConnectedPlayer.class, MethodHandles.lookup())
.findConstructor(ConnectedPlayer.class,
MethodType.methodType(
void.class,
VelocityServer.class,
GameProfile.class,
MinecraftConnection.class,
InetSocketAddress.class,
boolean.class,
IdentifiedKey.class
)
);
try {
SESSION_HANDLER_FIELD = MinecraftConnection.class.getDeclaredField("activeSessionHandler");
} catch (Throwable throwable) {
// Velocity b266 changed the field name to "activeSessionHandler"
SESSION_HANDLER_FIELD = MinecraftConnection.class.getDeclaredField("sessionHandler");
}
SESSION_HANDLER_FIELD.setAccessible(true);
CONNECTION_FIELD = InitialLoginSessionHandler.class.getDeclaredField("mcConnection");
CONNECTION_FIELD.setAccessible(true);
@@ -92,86 +128,14 @@ public final class FallbackListener {
@Subscribe(order = PostOrder.LAST)
public void handle(final @NotNull PreLoginEvent event) throws Throwable {
Statistics.TOTAL_TRAFFIC.increment();
final InetAddress inetAddress = event.getConnection().getRemoteAddress().getAddress();
// Increase joins per second for the action bar verbose
Sonar.get().getVerboseHandler().getJoinsPerSecond().put(System.nanoTime());
val inboundConnection = (LoginInboundConnection) event.getConnection();
val initialConnection = (InitialInboundConnection) INITIAL_CONNECTION.invokeExact(inboundConnection);
final MinecraftConnection mcConnection = initialConnection.getConnection();
final Channel channel = mcConnection.getChannel();
final ChannelPipeline pipeline = channel.pipeline();
// Hook the custom traffic pipeline, so we can count the incoming and outgoing traffic
TrafficChannelHooker.hook(pipeline, MINECRAFT_DECODER, MINECRAFT_ENCODER);
// Check the blacklist here since we cannot let the player "ghost join"
if (fallback.getBlacklisted().has(inetAddress.toString())) {
markConnectionAsDead(mcConnection.getSessionHandler());
initialConnection.getConnection().closeWith(Disconnect.create(
Sonar.get().getConfig().getBlacklisted(),
inboundConnection.getProtocolVersion()
));
return;
}
// Don't continue the verification process if the verification is disabled
if (!Sonar.get().getConfig().isEnableVerification()) return;
// Check if the player is already verified.
// No one wants to be verified over and over again.
final GameProfile gameProfile = GameProfile.forOfflinePlayer(event.getUsername());
if (Sonar.get().getVerifiedPlayerController().has(inetAddress, gameProfile.getId())) return;
// Completely skip Geyser connections (for now)
if (isGeyserConnection(channel)) {
// TODO: Do we need to log this?
fallback.getLogger().info("Allowing Geyser connection: " + inetAddress);
return;
}
// We now mark the connection as dead by using our dummy connection
markConnectionAsDead(mcConnection.getSessionHandler());
// Check if Fallback is already verifying a player
// → is another player with the same IP address connected to Fallback?
if (fallback.getConnected().containsKey(event.getUsername())
|| fallback.getConnected().containsValue(inetAddress)) {
initialConnection.getConnection().closeWith(Disconnect.create(
Sonar.get().getConfig().getAlreadyVerifying(),
inboundConnection.getProtocolVersion()
));
return;
}
// We cannot allow too many players on our Fallback server
// There's technically no reason for limiting this, but we'll better stay safe.
if (fallback.getConnected().size() > Sonar.get().getConfig().getMaximumVerifyingPlayers()) {
initialConnection.getConnection().closeWith(Disconnect.create(
Sonar.get().getConfig().getTooManyPlayers(),
inboundConnection.getProtocolVersion()
));
return;
}
// Check if the IP address is currently being rate-limited
if (!fallback.getRatelimiter().attempt(inetAddress)) {
initialConnection.getConnection().closeWith(Disconnect.create(
Sonar.get().getConfig().getTooFastReconnect(),
inboundConnection.getProtocolVersion()
));
return;
}
// Check if the player is already queued since we don't want bots to flood the queue
if (fallback.getQueue().getQueuedPlayers().containsKey(inetAddress)) {
initialConnection.getConnection().closeWith(Disconnect.create(
Sonar.get().getConfig().getAlreadyQueued(),
inboundConnection.getProtocolVersion()
));
return;
}
// Run in the channel's event loop
channel.eventLoop().execute(() -> {
@@ -179,43 +143,208 @@ public final class FallbackListener {
// Do not continue if the connection is closed or marked as disconnected
if (mcConnection.isClosed() || mcConnection.isKnownDisconnect()) 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(event.getUsername()));
// Hook the custom traffic pipeline, so we can count the incoming and outgoing traffic
final ChannelPipeline pipeline = channel.pipeline();
TrafficChannelHooker.hook(pipeline, MINECRAFT_DECODER, MINECRAFT_ENCODER);
// Queue the connection for further processing
fallback.getQueue().queue(inetAddress, () -> channel.eventLoop().execute(() -> {
final InetAddress inetAddress = event.getConnection().getRemoteAddress().getAddress();
// Do not continue if the connection is closed or marked as disconnected
if (mcConnection.isClosed() || mcConnection.isKnownDisconnect()) return;
// Increase total traffic statistic
Statistics.TOTAL_TRAFFIC.increment();
// Check if the username matches the valid name regex in order to prevent
// UTF-16 names or other types of flood attacks
if (!Sonar.get().getConfig().getValidNameRegex().matcher(event.getUsername()).matches()) {
mcConnection.closeWith(Disconnect.create(Sonar.get().getConfig().getInvalidUsername(), mcConnection.getProtocolVersion()));
try {
// Backwards compatibility for Velocity b265 and below
val activeSessionHandler = (MinecraftSessionHandler) SESSION_HANDLER_FIELD.get(mcConnection);
// Check the blacklist here since we cannot let the player "ghost join"
if (fallback.getBlacklisted().has(inetAddress.toString())) {
markConnectionAsDead(activeSessionHandler);
initialConnection.getConnection().closeWith(Disconnect.create(
Sonar.get().getConfig().getVerification().getBlacklisted(),
inboundConnection.getProtocolVersion()
));
return;
}
// Add better timeout handler to avoid known exploits or issues
// We also want to timeout bots quickly to avoid flooding
pipeline.replace(
READ_TIMEOUT,
READ_TIMEOUT,
new FallbackTimeoutHandler(Sonar.get().getConfig().getVerificationReadTimeout(), TimeUnit.MILLISECONDS)
);
// Don't continue the verification process if the verification is disabled
if (!Sonar.get().getConfig().getVerification().isEnabled()) return;
// We need to determine if the player is premium before we handle the connection,
// so we can create a ConnectedPlayer instance without having to spoof this
final boolean onlineMode = !event.getResult().isForceOfflineMode()
&& (SonarVelocity.INSTANCE.getPlugin().getServer().getConfiguration().isOnlineMode()
|| event.getResult().isOnlineModeAllowed());
// Check if the player is already verified.
// No one wants to be verified over and over again.
final GameProfile gameProfile = GameProfile.forOfflinePlayer(event.getUsername());
if (Sonar.get().getVerifiedPlayerController().has(inetAddress, gameProfile.getId())) return;
// Replace the session handler to intercept all packets and handle them
mcConnection.setSessionHandler(new FallbackSessionHandler(
fallback, mcConnection, inboundConnection,
gameProfile, inetAddress, onlineMode
));
}));
// Completely skip Geyser connections
if (isGeyserConnection(channel)) {
fallback.getLogger().info("Skipping Geyser player: {}{}",
gameProfile.getName(), Sonar.get().getConfig().formatAddress(inetAddress));
return;
}
// We now mark the connection as dead by using our fake connection
markConnectionAsDead(activeSessionHandler);
// Check if the player is already queued since we don't want bots to flood the queue
if (fallback.getQueue().getQueuedPlayers().containsKey(inetAddress)) {
initialConnection.getConnection().closeWith(Disconnect.create(
Sonar.get().getConfig().getVerification().getAlreadyQueued(),
inboundConnection.getProtocolVersion()
));
return;
}
// Check if Fallback is already verifying a player
// → is another player with the same IP address connected to Fallback?
if (fallback.getConnected().containsKey(event.getUsername())
|| fallback.getConnected().containsValue(inetAddress)) {
initialConnection.getConnection().closeWith(Disconnect.create(
Sonar.get().getConfig().getVerification().getAlreadyVerifying(),
inboundConnection.getProtocolVersion()
));
return;
}
// We cannot allow too many players on our Fallback server
// There's technically no reason for limiting this, but we'll better stay safe.
if (fallback.getConnected().size() > Sonar.get().getConfig().getVerification().getMaxVerifyingPlayers()) {
initialConnection.getConnection().closeWith(Disconnect.create(
Sonar.get().getConfig().getVerification().getTooManyPlayers(),
inboundConnection.getProtocolVersion()
));
return;
}
// Check if the IP address is currently being rate-limited
if (!fallback.getRatelimiter().attempt(inetAddress)) {
initialConnection.getConnection().closeWith(Disconnect.create(
Sonar.get().getConfig().getVerification().getTooFastReconnect(),
inboundConnection.getProtocolVersion()
));
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(event.getUsername()));
// 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 (mcConnection.isClosed() || mcConnection.isKnownDisconnect()) 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(event.getUsername()).matches()) {
mcConnection.closeWith(Disconnect.create(Sonar.get().getConfig().getVerification().getInvalidUsername(),
mcConnection.getProtocolVersion()));
return;
}
// Add better timeout handler to avoid known exploits or issues
// We also want to timeout bots quickly to avoid flooding
pipeline.replace(
READ_TIMEOUT,
READ_TIMEOUT,
new FallbackTimeoutHandler(Sonar.get().getConfig().getVerification().getReadTimeout(),
TimeUnit.MILLISECONDS)
);
// We need to determine if the player is premium before we handle the connection,
// so we can create a ConnectedPlayer instance without having to spoof this
final boolean onlineMode = !event.getResult().isForceOfflineMode()
&& (SonarVelocity.INSTANCE.getPlugin().getServer().getConfiguration().isOnlineMode()
|| event.getResult().isOnlineModeAllowed());
// Don't allow exceptions or disconnect messages
mcConnection.setAssociation(null);
// Create an instance for the connected player
final ConnectedPlayer connectedPlayer;
try {
connectedPlayer = (ConnectedPlayer) CONNECTED_PLAYER.invokeExact(
mcConnection.server,
gameProfile,
mcConnection,
inboundConnection.getVirtualHost().orElse(null),
onlineMode,
inboundConnection.getIdentifiedKey()
);
} catch (Throwable throwable) {
// This should not happen
fallback.getLogger().error("Error processing {}: {}", gameProfile.getName(), throwable);
mcConnection.close(true);
return;
}
// Create an instance for the Fallback connection
final FallbackUserWrapper fallbackPlayer = new FallbackUserWrapper(
fallback, connectedPlayer, mcConnection, mcConnection.getChannel(),
mcConnection.getChannel().pipeline(), inetAddress,
ProtocolVersion.fromId(connectedPlayer.getProtocolVersion().getProtocol())
);
// Disconnect if the protocol version could not be resolved
if (fallbackPlayer.getProtocolVersion().isUnknown()) {
fallbackPlayer.disconnect(Sonar.get().getConfig().getVerification().getInvalidProtocol());
return;
}
// Check if the player is already connected to the proxy but still tries to verify
if (!mcConnection.server.canRegisterConnection(connectedPlayer)) {
fallbackPlayer.disconnect(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().getAttackStatus().isCurrentlyUnderAttack()
|| Sonar.get().getConfig().getVerification().isLogDuringAttack()) {
fallback.getLogger().info(Sonar.get().getConfig().getVerification().getConnectLog()
.replace("%name%", connectedPlayer.getUsername())
.replace("%ip%", Sonar.get().getConfig().formatAddress(fallbackPlayer.getInetAddress()))
.replace("%protocol%", String.valueOf(fallbackPlayer.getProtocolVersion().getProtocol())));
}
}
// Call the VerifyJoinEvent for external API usage
Sonar.get().getEventManager().publish(new UserVerifyJoinEvent(gameProfile.getName(), fallbackPlayer));
// Mark the player as connected → verifying players
fallback.getConnected().put(gameProfile.getName(), 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 (fallbackPlayer.getPipeline().get(MINECRAFT_ENCODER) == null
|| fallbackPlayer.getPipeline().get(MINECRAFT_DECODER) == null) {
mcConnection.close(true);
return;
}
// Replace normal encoder to allow custom packets
final FallbackPacketEncoder encoder = new FallbackPacketEncoder(fallbackPlayer.getProtocolVersion());
fallbackPlayer.getPipeline().replace(MINECRAFT_ENCODER, FALLBACK_PACKET_ENCODER, encoder);
// Send LoginSuccess packet to make the client think they are joining the server
fallbackPlayer.write(new LoginSuccess(gameProfile.getName(), gameProfile.getId()));
// The LoginSuccess packet has been sent, now we can change the registry state
encoder.updateRegistry(fallbackPlayer.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_20_2) >= 0
? FallbackPacketRegistry.CONFIG : FallbackPacketRegistry.GAME);
// Replace normal decoder to allow custom packets
fallbackPlayer.getPipeline().replace(
MINECRAFT_DECODER, FALLBACK_PACKET_DECODER, new FallbackPacketDecoder(fallbackPlayer,
new FallbackVerificationHandler(fallbackPlayer, gameProfile.getName(), connectedPlayer.getUniqueId())
));
}));
} catch (Throwable throwable) {
throw new ReflectiveOperationException(throwable);
}
});
}
@@ -223,30 +352,27 @@ public final class FallbackListener {
public void handle(final @NotNull LoginEvent event) {
val connectedPlayer = (ConnectedPlayer) event.getPlayer();
if (Sonar.get().getConfig().isLockdownEnabled()) {
if (!event.getPlayer().hasPermission("sonar.lockdown")) {
connectedPlayer.getConnection().closeWith(Disconnect.create(
Sonar.get().getConfig().getLockdownDisconnect(), connectedPlayer.getProtocolVersion()));
if (Sonar.get().getConfig().getLockdown().isEnabled()
&& !event.getPlayer().hasPermission(Sonar.get().getConfig().getLockdown().getBypassPermission())) {
connectedPlayer.getConnection().closeWith(Disconnect.create(
Sonar.get().getConfig().getLockdown().getDisconnect(), connectedPlayer.getProtocolVersion()));
if (Sonar.get().getConfig().isLockdownLogAttempts()) {
Sonar.get().getLogger().info(Sonar.get().getConfig().getLockdownConsoleLog()
.replace("%player%", event.getPlayer().getUsername())
.replace("%ip%", Sonar.get().getConfig()
.formatAddress(event.getPlayer().getRemoteAddress().getAddress()))
.replace("%protocol%",
String.valueOf(event.getPlayer().getProtocolVersion().getProtocol())));
}
return;
} else if (Sonar.get().getConfig().isLockdownEnableNotify()) {
event.getPlayer().sendMessage(Component.text(Sonar.get().getConfig().getLockdownNotification()));
if (Sonar.get().getConfig().getLockdown().isLogAttempts()) {
Sonar.get().getLogger().info(Sonar.get().getConfig().getLockdown().getConsoleLog()
.replace("%player%", event.getPlayer().getUsername())
.replace("%ip%", Sonar.get().getConfig()
.formatAddress(event.getPlayer().getRemoteAddress().getAddress()))
.replace("%protocol%",
String.valueOf(event.getPlayer().getProtocolVersion().getProtocol())));
}
return;
}
final InetAddress inetAddress = event.getPlayer().getRemoteAddress().getAddress();
// 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().getMaximumOnlinePerIp();
final int maxOnlinePerIp = Sonar.get().getConfig().getMaxOnlinePerIp();
if (maxOnlinePerIp > 0) {
final long onlinePerIp = SonarVelocity.INSTANCE.getPlugin().getServer().getAllPlayers().stream()
@@ -261,4 +387,15 @@ public final class FallbackListener {
}
}
}
@Subscribe(order = PostOrder.LAST)
public void handle(final @NotNull PostLoginEvent event) {
if (Sonar.get().getConfig().getLockdown().isEnabled() && Sonar.get().getConfig().getLockdown().isNotifyAdmins()) {
if (event.getPlayer().hasPermission(Sonar.get().getConfig().getLockdown().getBypassPermission())) {
final String notification = Sonar.get().getConfig().getLockdown().getNotification();
final Component deserialized = MiniMessage.miniMessage().deserialize(notification);
event.getPlayer().sendMessage(deserialized);
}
}
}
}
@@ -1,176 +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.velocity.fallback;
import com.velocitypowered.api.proxy.crypto.IdentifiedKey;
import com.velocitypowered.api.util.GameProfile;
import com.velocitypowered.proxy.VelocityServer;
import com.velocitypowered.proxy.connection.MinecraftConnection;
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
import com.velocitypowered.proxy.connection.client.LoginInboundConnection;
import io.netty.buffer.ByteBuf;
import lombok.Getter;
import org.jetbrains.annotations.NotNull;
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.FallbackVerificationHandler;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketDecoder;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketEncoder;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import static com.velocitypowered.proxy.network.Connections.MINECRAFT_DECODER;
import static com.velocitypowered.proxy.network.Connections.MINECRAFT_ENCODER;
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_PACKET_DECODER;
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_PACKET_ENCODER;
@Getter
public final class FallbackSessionHandler implements MinecraftSessionHandler {
private final Fallback fallback;
private final MinecraftConnection mcConnection;
private final LoginInboundConnection inboundConnection;
private static final MethodHandle CONNECTED_PLAYER;
static {
try {
CONNECTED_PLAYER = MethodHandles.privateLookupIn(ConnectedPlayer.class, MethodHandles.lookup())
.findConstructor(ConnectedPlayer.class,
MethodType.methodType(
void.class,
VelocityServer.class,
GameProfile.class,
MinecraftConnection.class,
InetSocketAddress.class,
boolean.class,
IdentifiedKey.class
)
);
} catch (Throwable throwable) {
throw new ReflectiveOperationException(throwable);
}
}
FallbackSessionHandler(final Fallback fallback,
final @NotNull MinecraftConnection mcConnection,
final LoginInboundConnection inboundConnection,
final GameProfile gameProfile,
final InetAddress inetAddress,
final boolean onlineMode) {
this.fallback = fallback;
this.mcConnection = mcConnection;
this.inboundConnection = inboundConnection;
// Don't allow exceptions or disconnect messages
mcConnection.setAssociation(null);
// Create an instance for the connected player
final ConnectedPlayer connectedPlayer;
try {
connectedPlayer = (ConnectedPlayer) CONNECTED_PLAYER.invokeExact(
mcConnection.server,
gameProfile,
mcConnection,
inboundConnection.getVirtualHost().orElse(null),
onlineMode,
inboundConnection.getIdentifiedKey()
);
} catch (Throwable throwable) {
// This should not happen
fallback.getLogger().error("Error processing {}: {}", gameProfile.getName(), throwable);
mcConnection.close(true);
return;
}
// Create an instance for the Fallback connection
final FallbackUserWrapper fallbackPlayer = new FallbackUserWrapper(
fallback, connectedPlayer, mcConnection, mcConnection.getChannel(),
mcConnection.getChannel().pipeline(), inetAddress,
ProtocolVersion.fromId(connectedPlayer.getProtocolVersion().getProtocol())
);
// Check if the player is already connected to the proxy
if (!mcConnection.server.canRegisterConnection(connectedPlayer)) {
fallbackPlayer.disconnect("Could not find any available servers.");
return;
}
// The player joined the verification
Statistics.REAL_TRAFFIC.increment();
if (Sonar.get().getConfig().isLogConnections()) {
// Only log the processing message if the server isn't under attack.
// We let the user override this through the configuration.
if (!fallback.isPotentiallyUnderAttack() || Sonar.get().getConfig().isLogDuringAttack()) {
fallback.getLogger().info(Sonar.get().getConfig().getVerificationConnectLog()
.replace("%name%", connectedPlayer.getUsername())
.replace("%ip%", Sonar.get().getConfig().formatAddress(fallbackPlayer.getInetAddress()))
.replace("%protocol%", String.valueOf(fallbackPlayer.getProtocolVersion().getProtocol())));
}
}
// Call the VerifyJoinEvent for external API usage
Sonar.get().getEventManager().publish(new UserVerifyJoinEvent(gameProfile.getName(), fallbackPlayer));
// Mark the player as connected → verifying players
fallback.getConnected().put(gameProfile.getName(), inetAddress);
// This sometimes happens when the channel hangs, but the player is still connecting
// This also fixes a weird issue with TCPShield and other reverse proxies
if (fallbackPlayer.getPipeline().get(MINECRAFT_ENCODER) == null
|| fallbackPlayer.getPipeline().get(MINECRAFT_DECODER) == null) {
mcConnection.close(true);
return;
}
// Replace normal encoder to allow custom packets
fallbackPlayer.getPipeline().replace(
MINECRAFT_ENCODER,
FALLBACK_PACKET_ENCODER,
new FallbackPacketEncoder(fallbackPlayer.getProtocolVersion())
);
// Replace normal decoder to allow custom packets
fallbackPlayer.getPipeline().replace(
MINECRAFT_DECODER,
FALLBACK_PACKET_DECODER,
new FallbackPacketDecoder(
fallbackPlayer,
new FallbackVerificationHandler(
fallbackPlayer,
gameProfile.getName(),
connectedPlayer.getUniqueId()
)
)
);
}
@Override
public void handleUnknown(final ByteBuf buf) {
mcConnection.close(true);
}
}
@@ -21,7 +21,6 @@ import com.velocitypowered.proxy.connection.MinecraftConnection;
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
import io.netty.channel.Channel;
import io.netty.channel.ChannelPipeline;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
@@ -30,14 +29,13 @@ 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.common.fallback.protocol.packets.Disconnect;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.Disconnect;
import java.net.InetAddress;
@Getter
@ToString(of = {"protocolVersion"})
@EqualsAndHashCode
@RequiredArgsConstructor
@ToString(of = {"protocolVersion"})
public final class FallbackUserWrapper implements FallbackUser<ConnectedPlayer, MinecraftConnection> {
private final Fallback fallback;
private final ConnectedPlayer player;
@@ -56,4 +54,9 @@ public final class FallbackUserWrapper implements FallbackUser<ConnectedPlayer,
public void write(final @NotNull Object packet) {
connection.write(packet);
}
@Override
public void delayedWrite(final @NotNull Object packet) {
connection.delayedWrite(packet);
}
}
@@ -1,6 +1,6 @@
{
"id": "sonar",
"name": "$name",
"name": "Sonar",
"version": "$version",
"description": "$description",
"url": "$url",