Compare commits

...
33 Commits
Author SHA1 Message Date
Jones 65e059368c Release 2.0.23 (#239) 2024-03-13 16:15:38 +01:00
dependabot[bot] b7a031da12 Bump io.github.goooler.shadow from 8.1.5 to 8.1.7 (#238)
Bumps [io.github.goooler.shadow](https://github.com/Goooler/shadow) from 8.1.5 to 8.1.7.
- [Release notes](https://github.com/Goooler/shadow/releases)
- [Commits](https://github.com/Goooler/shadow/compare/v8.1.5...v8.1.7)

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

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

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

* fix: use correct char ids for bStats

* fix: implement shutdown method to correctly stop certain services

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

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

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

* fix: use pooled connection source to remove unnecessary reflection

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

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

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

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

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

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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-19 13:50:20 +01:00
92 changed files with 1814 additions and 1103 deletions

No files matched your search

-1
View File
@@ -30,6 +30,5 @@ gradlew*
*.jpg
*.jpeg
*.log
*.yml
*.svg
*.txt
+19 -26
View File
@@ -6,7 +6,7 @@ buildscript {
plugins {
id("java")
id("com.github.johnrengelman.shadow") version "8.1.1"
id("io.github.goooler.shadow") version "8.1.7"
id("io.toolebox.git-versioner") version "1.6.7"
}
@@ -26,7 +26,7 @@ allprojects {
}
apply(plugin = "java")
apply(plugin = "com.github.johnrengelman.shadow")
apply(plugin = "io.github.goooler.shadow")
dependencies {
compileOnly("org.projectlombok:lombok:1.18.30")
@@ -35,17 +35,14 @@ allprojects {
testCompileOnly("org.projectlombok:lombok:1.18.30")
testAnnotationProcessor("org.projectlombok:lombok:1.18.30")
val adventureVersion = "4.15.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-text-minimessage:4.16.0")
implementation("net.kyori:adventure-text-serializer-gson:4.16.0")
implementation("net.kyori:adventure-nbt:4.16.0")
compileOnly("com.j256.ormlite:ormlite-jdbc:6.1") // ORMLite
compileOnly("com.github.ben-manes.caffeine:caffeine:3.1.8") // caching
compileOnly("io.netty:netty-all:4.1.106.Final") // netty
compileOnly("io.netty:netty-all:4.1.107.Final") // netty
// Library/dependency loading
compileOnly("com.alessiodp.libby:libby-core:2.0.0-SNAPSHOT")
@@ -76,32 +73,28 @@ tasks {
// Remove file timestamps
isPreserveFileTimestamps = false
// bStats has to be relocated to the Sonar package otherwise it throws an exception
// https://github.com/Bastian/bstats-metrics/blob/master/base/src/main/java/org/bstats/MetricsBase.java#L251
// Relocate libraries
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("com.alessiodp.libby", "xyz.jonesdev.sonar.libs.libby")
relocate("xyz.jonesdev.cappuccino", "xyz.jonesdev.sonar.libs.cappuccino")
relocate("com.simpleyaml", "xyz.jonesdev.sonar.libs.yaml")
relocate("com.google.gson", "xyz.jonesdev.sonar.libs.gson")
relocate("com.j256.ormlite", "xyz.jonesdev.sonar.libs.ormlite")
relocate("com.github.benmanes.caffeine", "xyz.jonesdev.sonar.libs.caffeine")
relocate("com.mysql", "xyz.jonesdev.sonar.libs.mysql")
relocate("org.mariadb", "xyz.jonesdev.sonar.libs.mariadb")
// TODO: actually fix this :/
// We have to be careful here, so we don't accidentally break adventure
relocate("net.kyori.adventure", "xyz.jonesdev.sonar.libs.adventure") {
relocate("net.kyori", "xyz.jonesdev.sonar.libs.kyori") {
exclude("net.kyori.adventure.text.**")
exclude("net.kyori.adventure.audience.Audience")
exclude("net.kyori.adventure.title.*")
}
relocate("net.kyori.examination", "xyz.jonesdev.sonar.libs.examination")
relocate("net.kyori.option", "xyz.jonesdev.sonar.libs.option")
// Relocate dynamically loaded libraries
relocate("com.simpleyaml", "xyz.jonesdev.sonar.libs.yaml")
relocate("com.google.code.gson", "xyz.jonesdev.sonar.libs.gson")
relocate("com.j256.ormlite", "xyz.jonesdev.sonar.libs.ormlite")
relocate("com.github.benmanes.caffeine", "xyz.jonesdev.sonar.libs.caffeine")
// We want to load our own Gson dynamically
exclude("com/google/gson/**")
// Exclude unnecessary metadata information
exclude("META-INF/versions/**")
exclude("META-INF/*/**")
// We want to load our own Gson dynamically
exclude("com/google/gson/**")
}
compileJava {
+1 -1
View File
@@ -1,3 +1,3 @@
group=xyz.jonesdev.sonar
version=2.0.18
version=2.0.23
description=Effective Anti-bot plugin for Velocity, BungeeCord, and Bukkit (1.7-latest)
+1 -1
View File
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
@@ -17,11 +17,9 @@
package xyz.jonesdev.sonar.api;
import com.alessiodp.libby.LibraryManager;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import net.kyori.adventure.audience.Audience;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import xyz.jonesdev.sonar.api.attack.AttackTracker;
import xyz.jonesdev.sonar.api.command.subcommand.SubcommandRegistry;
import xyz.jonesdev.sonar.api.config.SonarConfiguration;
@@ -29,28 +27,23 @@ 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.statistics.SonarStatistics;
import xyz.jonesdev.sonar.api.timer.SystemTimer;
import xyz.jonesdev.sonar.api.verbose.Notification;
import xyz.jonesdev.sonar.api.verbose.Verbose;
import java.io.File;
import java.text.DecimalFormat;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.UUID;
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();
/**
* Since we want to use Adventure on every server platform,
* we have to use their platform module to support BungeeCord and Bukkit
*/
Map<String, Audience> AUDIENCES = new ConcurrentHashMap<>();
@Nullable Audience audience(final @Nullable UUID uniqueId);
/**
* @return The platform the plugin is being run on
@@ -76,7 +69,7 @@ public interface Sonar {
@NotNull SystemTimer getLaunchTimer();
@NotNull LibraryManager getLibraryManager();
@NotNull SonarStatistics getStatistics();
/**
* Set a custom verbose handler
@@ -23,16 +23,15 @@ 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.Counters;
import xyz.jonesdev.sonar.api.statistics.Statistics;
import xyz.jonesdev.sonar.api.timer.SystemTimer;
import java.util.Optional;
import static xyz.jonesdev.sonar.api.jvm.JVMProcessInformation.*;
@Getter
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class AttackTracker implements JVMProfiler {
public final class AttackTracker {
public static final AttackTracker INSTANCE = new AttackTracker();
private @Nullable AttackStatistics currentAttack;
private int attackThreshold;
@@ -52,7 +51,7 @@ public final class AttackTracker implements JVMProfiler {
}
public void checkIfUnderAttack() {
final long joinsPerSecond = Counters.LOGINS_PER_SECOND.estimatedSize();
final long joinsPerSecond = Sonar.get().getStatistics().getLoginsPerSecond();
final int verifyingPlayers = Sonar.get().getFallback().getConnected().size();
final int queuedPlayers = Sonar.get().getFallback().getQueue().getQueuedPlayers().size();
final int minPlayers = Sonar.get().getConfig().getMinPlayersForAttack();
@@ -67,7 +66,7 @@ public final class AttackTracker implements JVMProfiler {
if (currentAttack == null) {
currentAttack = new AttackStatistics();
currentAttack.successfulVerifications = Sonar.get().getVerifiedPlayerController().estimatedSize();
currentAttack.failedVerifications = Statistics.FAILED_VERIFICATIONS.get();
currentAttack.failedVerifications = Sonar.get().getStatistics().getTotalFailedVerifications();
Sonar.get().getEventManager().publish(new AttackDetectedEvent());
Sonar.get().getNotificationHandler().sendAttackNotification();
} else {
@@ -80,7 +79,7 @@ public final class AttackTracker implements JVMProfiler {
// Update joins per second peak if necessary
currentAttack.peakJoinsPerSecond = joinsPerSecond;
}
final long connectionsPerSecond = Counters.CONNECTIONS_PER_SECOND.estimatedSize();
final long connectionsPerSecond = Sonar.get().getStatistics().getConnectionsPerSecond();
if (connectionsPerSecond > currentAttack.peakConnectionsPerSecond) {
// Update connections per second peak if necessary
currentAttack.peakConnectionsPerSecond = connectionsPerSecond;
@@ -102,42 +101,44 @@ public final class AttackTracker implements JVMProfiler {
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 String peakCPS = Sonar.DECIMAL_FORMAT.format(currentAttack.peakConnectionsPerSecond);
final long minutes = deltaInMillis / (60 * 1000); // Convert milliseconds to minutes
final double seconds = (deltaInMillis % (60 * 1000)) / 1000D; // Convert remaining milliseconds to seconds
final String formattedDuration = String.format("%d minutes, %.0f seconds", minutes, seconds);
final String startTimestamp = String.valueOf(currentAttack.duration.getStart() / 1000L);
final String endTimestamp = String.valueOf(System.currentTimeMillis() / 1000L);
final long blacklisted = Sonar.get().getFallback().getBlacklist().estimatedSize();
// Calculate during-attack-statistics using their deltas
final long totalVerified = Sonar.get().getVerifiedPlayerController().estimatedSize();
final long verified = Math.max(totalVerified - currentAttack.successfulVerifications, 0);
final long totalFailed = Statistics.FAILED_VERIFICATIONS.get();
final long failed = Math.max(totalFailed - currentAttack.failedVerifications, 0);
// Post webhook to Discord
Optional.ofNullable(Sonar.get().getConfig().getWebhook().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("%peak-cps%", peakCPS)
.replace("%total-blacklisted%", Sonar.DECIMAL_FORMAT.format(blacklisted))
.replace("%total-failed%", Sonar.DECIMAL_FORMAT.format(failed))
.replace("%total-success%", Sonar.DECIMAL_FORMAT.format(verified));
return new SonarConfiguration.Webhook.Embed(
config.getTitle(), config.getTitleUrl(), description, config.getR(), config.getG(), config.getB());
}));
.ifPresent(webhook -> {
// Save attack statistics
final long deltaInMillis = currentAttack.duration.delay();
final String peakCPU = Sonar.DECIMAL_FORMAT.format(currentAttack.peakProcessCPUUsage);
final String peakMem = formatMemory(currentAttack.peakProcessMemoryUsage);
final String peakBPS = Sonar.DECIMAL_FORMAT.format(currentAttack.peakJoinsPerSecond);
final String peakCPS = Sonar.DECIMAL_FORMAT.format(currentAttack.peakConnectionsPerSecond);
final long minutes = deltaInMillis / (60 * 1000); // Convert milliseconds to minutes
final double seconds = (deltaInMillis % (60 * 1000)) / 1000D; // Convert remaining milliseconds to seconds
final String formattedDuration = String.format("%d minutes, %.0f seconds", minutes, seconds);
final String startTimestamp = String.valueOf(currentAttack.duration.getStart() / 1000L);
final String endTimestamp = String.valueOf(System.currentTimeMillis() / 1000L);
final long blacklisted = Sonar.get().getFallback().getBlacklist().estimatedSize();
// Calculate during-attack-statistics using their deltas
final long totalVerified = Sonar.get().getVerifiedPlayerController().estimatedSize();
final long verified = Math.max(totalVerified - currentAttack.successfulVerifications, 0);
final long totalFailed = Sonar.get().getStatistics().getTotalFailedVerifications();
final long failed = Math.max(totalFailed - currentAttack.failedVerifications, 0);
webhook.post(() -> {
final SonarConfiguration.Webhook.Embed config = Sonar.get().getConfig().getWebhook().getEmbed();
final String description = config.getDescription()
.replace("%start-timestamp%", startTimestamp)
.replace("%end-timestamp%", endTimestamp)
.replace("%duration%", formattedDuration)
.replace("%peak-cpu%", peakCPU)
.replace("%peak-memory%", peakMem)
.replace("%peak-bps%", peakBPS)
.replace("%peak-cps%", peakCPS)
.replace("%total-blacklisted%", Sonar.DECIMAL_FORMAT.format(blacklisted))
.replace("%total-failed%", Sonar.DECIMAL_FORMAT.format(failed))
.replace("%total-success%", Sonar.DECIMAL_FORMAT.format(verified));
return new SonarConfiguration.Webhook.Embed(
config.getTitle(), config.getTitleUrl(), description, config.getR(), config.getG(), config.getB());
});
});
}
// Reset the attack status
@@ -23,12 +23,13 @@ import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage;
import java.util.UUID;
import java.util.function.Predicate;
@Getter
@RequiredArgsConstructor
public abstract class InvocationSource {
private final String name;
private final UUID uuid;
private final Audience audience;
private final boolean player;
private final Predicate<String> permissionFunction;
@@ -17,6 +17,7 @@
package xyz.jonesdev.sonar.api.config;
import com.j256.ormlite.db.DatabaseType;
import lombok.*;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage;
@@ -25,6 +26,8 @@ import org.jetbrains.annotations.Nullable;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.command.SonarCommand;
import xyz.jonesdev.sonar.api.logger.LoggerWrapper;
import xyz.jonesdev.sonar.api.ormlite.MariaDbDatabaseTypeAdapter;
import xyz.jonesdev.sonar.api.ormlite.MysqlDatabaseTypeAdapter;
import xyz.jonesdev.sonar.api.webhook.DiscordWebhook;
import java.io.File;
@@ -41,7 +44,7 @@ import java.util.regex.Pattern;
public final class SonarConfiguration {
@Getter
private final SimpleYamlConfig generalConfig, messagesConfig, webhookConfig;
private final File languageFile;
private final File languageFile, pluginFolder;
static final LoggerWrapper LOGGER = new LoggerWrapper() {
@@ -62,6 +65,7 @@ public final class SonarConfiguration {
};
public SonarConfiguration(final @NotNull File pluginFolder) {
this.pluginFolder = pluginFolder;
this.messagesConfig = new SimpleYamlConfig(new File(pluginFolder, "messages.yml"));
this.generalConfig = new SimpleYamlConfig(new File(pluginFolder, "config.yml"));
this.webhookConfig = new SimpleYamlConfig(new File(pluginFolder, "webhook.yml"));
@@ -105,6 +109,11 @@ public final class SonarConfiguration {
}
public void load() {
// Make sure the plugin folder actually exists before trying to copy files into it
if (!pluginFolder.exists() && !pluginFolder.mkdirs()) {
throw new IllegalStateException("Could not create plugin folder (insufficient permissions?)");
}
// Generate the language file and check what it's set to
Language preferredLanguage = getPreferredLanguage();
if (preferredLanguage == Language.SYSTEM) {
@@ -164,6 +173,7 @@ public final class SonarConfiguration {
verification.timing = Verification.Timing.valueOf(generalConfig.getString("verification.timing"));
verification.gravity.enabled = generalConfig.getBoolean("verification.checks.gravity.enabled");
verification.gravity.checkCollisions = generalConfig.getBoolean("verification.checks.gravity.check-collisions");
verification.gravity.captchaOnFail = generalConfig.getBoolean("verification.checks.gravity.captcha-on-fail");
verification.gravity.maxMovementTicks = clamp(generalConfig.getInt("verification.checks.gravity.max-movement-ticks"), 2, 100);
verification.gravity.maxIgnoredTicks = clamp(generalConfig.getInt("verification.checks.gravity.max-ignored-ticks"), 1, 128);
verification.gravity.gamemode = Verification.Gravity.Gamemode.valueOf(generalConfig.getString("verification.checks.gravity.gamemode"));
@@ -217,7 +227,8 @@ public final class SonarConfiguration {
webhook.embed.title = webhookConfig.getString(embedPath + "title");
webhook.embed.titleUrl = webhookConfig.getString(embedPath + "title-url");
webhook.embed.description = fromList(webhookConfig.getStringList(embedPath + "description"));
webhook.embed.description = formatString(String.join("\n",
webhookConfig.getStringList(embedPath + "description")));
webhook.embed.r = webhookConfig.getInt(embedPath + "color.red");
webhook.embed.g = webhookConfig.getInt(embedPath + "color.green");
webhook.embed.b = webhookConfig.getInt(embedPath + "color.blue");
@@ -291,20 +302,16 @@ public final class SonarConfiguration {
verification.blacklistLog = formatString(messagesConfig.getString("verification.logs.blacklisted"));
verification.successLog = formatString(messagesConfig.getString("verification.logs.successful"));
verification.gravity.youAreBeingChecked = deserialize(formatString(messagesConfig.getString("verification.welcome")));
verification.map.enterCode = deserialize(formatString(messagesConfig.getString("verification.captcha.enter-code")));
verification.map.enterCodeActionBar = formatString(messagesConfig.getString("verification.captcha.action-bar"));
verification.map.failedCaptcha = deserialize(formatString(messagesConfig.getString("verification.captcha.incorrect")));
verification.currentlyPreparing = deserialize(fromList(messagesConfig.getStringList("verification.currently-preparing")));
verification.tooManyPlayers = deserialize(fromList(messagesConfig.getStringList("verification.too-many-players")));
verification.tooFastReconnect = deserialize(fromList(messagesConfig.getStringList("verification.too-fast-reconnect")));
verification.alreadyVerifying = deserialize(fromList(messagesConfig.getStringList("verification.already-verifying")));
verification.alreadyQueued = deserialize(fromList(messagesConfig.getStringList("verification.already-queued")));
verification.blacklisted = deserialize(fromList(messagesConfig.getStringList("verification.blacklisted")));
verification.invalidUsername = deserialize(fromList(messagesConfig.getStringList("verification.invalid-username")));
verification.protocolBlacklisted = deserialize(fromList(messagesConfig.getStringList("verification.blacklisted-protocol")));
verification.alreadyConnected = deserialize(fromList(messagesConfig.getStringList("verification.already-online")));
verification.verificationSuccess = deserialize(fromList(messagesConfig.getStringList("verification.success")));
verification.verificationFailed = deserialize(fromList(messagesConfig.getStringList("verification.failed")));
@@ -414,8 +421,13 @@ public final class SonarConfiguration {
private Timing timing;
@Getter
@RequiredArgsConstructor
public enum Timing {
ALWAYS, DURING_ATTACK, NEVER
ALWAYS("Always"),
DURING_ATTACK("During Attack"),
NEVER("Never");
private final String displayName;
}
private final Map map = new Map();
@@ -445,10 +457,10 @@ public final class SonarConfiguration {
public static final class Gravity {
private boolean enabled;
private boolean checkCollisions;
private boolean captchaOnFail;
private Gamemode gamemode;
private int maxMovementTicks;
private int maxIgnoredTicks;
private Component youAreBeingChecked;
@Getter
@RequiredArgsConstructor
@@ -489,10 +501,8 @@ public final class SonarConfiguration {
private final Collection<Integer> whitelistedProtocols = new HashSet<>(0);
private final Collection<Integer> blacklistedProtocols = new HashSet<>(0);
private Component tooManyPlayers;
private Component tooFastReconnect;
private Component invalidUsername;
private Component alreadyConnected;
private Component verificationSuccess;
private Component verificationFailed;
private Component alreadyVerifying;
@@ -557,10 +567,12 @@ public final class SonarConfiguration {
@Getter
@RequiredArgsConstructor
public enum Type {
MYSQL("com.mysql.cj.jdbc.NonRegisteringDriver"),
NONE(null);
MYSQL("jdbc:mysql://%s:%d/%s", new MysqlDatabaseTypeAdapter()),
MARIADB("jdbc:mariadb://%s:%d/%s", new MariaDbDatabaseTypeAdapter()),
NONE(null, null);
private final String driverClassName;
private final String connectionString;
private final DatabaseType databaseType;
}
private Type type;
@@ -19,7 +19,7 @@ package xyz.jonesdev.sonar.api.controller;
import com.j256.ormlite.dao.Dao;
import com.j256.ormlite.dao.DaoManager;
import com.j256.ormlite.jdbc.JdbcSingleConnectionSource;
import com.j256.ormlite.jdbc.JdbcPooledConnectionSource;
import com.j256.ormlite.stmt.QueryBuilder;
import com.j256.ormlite.support.ConnectionSource;
import com.j256.ormlite.table.TableUtils;
@@ -27,14 +27,15 @@ import lombok.Getter;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.Range;
import org.jetbrains.annotations.Unmodifiable;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.config.SonarConfiguration;
import xyz.jonesdev.sonar.api.logger.LoggerWrapper;
import xyz.jonesdev.sonar.api.model.VerifiedPlayer;
import java.lang.reflect.Method;
import java.net.InetAddress;
import java.sql.*;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.*;
@@ -43,7 +44,6 @@ import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public final class VerifiedPlayerController {
private static final Map<String, Collection<UUID>> MAP = new ConcurrentHashMap<>();
private static final ExecutorService DB_UPDATE_SERVICE = Executors.newSingleThreadExecutor();
private static final LoggerWrapper LOGGER = new LoggerWrapper() {
@@ -64,6 +64,7 @@ public final class VerifiedPlayerController {
}
};
private final Map<String, Collection<UUID>> cache = new ConcurrentHashMap<>(128);
private @Nullable ConnectionSource connectionSource;
private Dao<VerifiedPlayer, Integer> dao;
private QueryBuilder<VerifiedPlayer, Integer> queryBuilder;
@@ -71,8 +72,9 @@ public final class VerifiedPlayerController {
private final @NotNull SonarConfiguration.Database.Type cachedDatabaseType;
public VerifiedPlayerController() {
final SonarConfiguration.Database database = Sonar.get().getConfig().getDatabase();
// Cache selected database type, so we don't need to call Sonar.get() every time
cachedDatabaseType = Sonar.get().getConfig().getDatabase().getType();
cachedDatabaseType = database.getType();
// Don't establish a database connection if the type is NONE
if (cachedDatabaseType == SonarConfiguration.Database.Type.NONE) {
@@ -80,8 +82,14 @@ public final class VerifiedPlayerController {
return;
}
try (final ConnectionSource connectionSource = setupDriverAndConnect()) {
this.connectionSource = connectionSource;
// Initialize MySQL/MariaDB driver
database.getType().getDatabaseType().loadDriver();
try {
connectionSource = new JdbcPooledConnectionSource(
String.format(database.getType().getConnectionString(),
database.getHost(), database.getPort(), database.getName()),
database.getUsername(), database.getPassword(), database.getType().getDatabaseType());
// Create table
try {
@@ -98,83 +106,53 @@ public final class VerifiedPlayerController {
// Make sure to run the clean task and the caching task in the same thread
// https://github.com/jonesdevelopment/sonar/issues/150
DB_UPDATE_SERVICE.execute(() -> {
// Make sure to clear all outdated entries first
clearOld(Sonar.get().getConfig().getDatabase().getMaximumAge());
// Add all entries from the database to the cache
try {
// Make sure to clear all outdated entries first
clearOld(database.getMaximumAge());
// Add all entries from the database to the cache
dao.queryForAll().forEach(this::_add);
} catch (SQLException exception) {
LOGGER.error("Could not cache entries: {}", exception);
LOGGER.error("Error initializing database: {}", exception);
}
});
} catch (Throwable throwable) {
LOGGER.error("Error setting up database: {}", throwable);
throwable.printStackTrace(System.err);
} catch (SQLException exception) {
LOGGER.error("Error setting up database: {}", exception);
}
}
/**
* Automatically sets up the database connection using the configured credentials
* Closes the connection if instantiated
*/
private static @NotNull ConnectionSource setupDriverAndConnect() throws Throwable {
final SonarConfiguration.Database database = Sonar.get().getConfig().getDatabase();
// Prepare the JDBC connection string
// TODO: automatically build best connection string for database type
final String jdbcURL = String.format("jdbc:%s://%s:%d/%s",
database.getType().name().toLowerCase(), database.getHost(), database.getPort(), database.getName());
// Use reflection to uncover the driver class
final Class<?> driverClass = Class.forName(database.getType().getDriverClassName());
// Make sure we send the username and password
final Properties credentials = new Properties();
credentials.put("user", database.getUsername());
credentials.put("password", database.getPassword());
// Return new JDBC connection instance for our controller to handle
return new JdbcSingleConnectionSource(jdbcURL, invoke(jdbcURL, driverClass, credentials));
}
/**
* Uses reflection to load the database driver from the injected library
*
* @param connectionString URL of the database
* @param driverClass Class of the database driver
* @param credentials Credentials for the database
*/
private static Connection invoke(final @NotNull String connectionString,
final @NotNull Class<?> driverClass,
final @NotNull Properties credentials) throws Throwable {
final Object driver = driverClass.getDeclaredConstructor().newInstance();
DriverManager.registerDriver((Driver) driver);
final Method connect = driverClass.getDeclaredMethod("connect", String.class, Properties.class);
connect.setAccessible(true);
return (Connection) connect.invoke(driver, connectionString, credentials);
public void close() {
// The connection source will always be null if the database type is NONE.
if (connectionSource != null) {
try {
// Properly close the connection
connectionSource.close();
} catch (Exception exception) {
LOGGER.error("Error closing database: {}", exception);
}
}
}
/**
* Clear all old entries using the given timestamp.
*/
private void clearOld(final @Range(from = 1, to = 365) int maximumAge) {
try {
final long timestamp = Instant.now()
.minus(maximumAge, ChronoUnit.DAYS)
.getEpochSecond() * 1000L; // convert to ms
private void clearOld(final @Range(from = 1, to = 365) int maximumAge) throws SQLException {
final long timestamp = Instant.now()
.minus(maximumAge, ChronoUnit.DAYS)
.getEpochSecond() * 1000L; // convert to ms
final List<VerifiedPlayer> oldEntries = queryBuilder.where()
.lt("timestamp", new Timestamp(timestamp))
.query();
final List<VerifiedPlayer> oldEntries = queryBuilder.where()
.lt("timestamp", new Timestamp(timestamp))
.query();
if (oldEntries != null) {
for (final VerifiedPlayer player : oldEntries) {
dao.delete(player);
}
LOGGER.info("Removed {} database entries older than {} days.",
oldEntries.size(), maximumAge);
if (oldEntries != null) {
for (final VerifiedPlayer player : oldEntries) {
dao.delete(player);
}
} catch (SQLException exception) {
LOGGER.error("Error trying to clear old entries: {}", exception);
LOGGER.info("Removed {} database entries older than {} days.",
oldEntries.size(), maximumAge);
}
}
@@ -221,7 +199,7 @@ public final class VerifiedPlayerController {
* @param inetAddress IP address of the player
*/
private void _remove(final @NotNull String inetAddress) {
MAP.remove(inetAddress);
cache.remove(inetAddress);
}
/**
@@ -259,24 +237,30 @@ public final class VerifiedPlayerController {
* @param player VerifiedPlayer model
*/
private void _add(final @NotNull VerifiedPlayer player) {
MAP.computeIfAbsent(player.getInetAddress(), v -> new Vector<>())
cache.computeIfAbsent(player.getInetAddress(), v -> new ArrayList<>())
.add(player.getPlayerUUID());
}
/**
* Returns the amount of verified IP addresses
*
* @return Estimated size of the local cache
*/
public synchronized int estimatedSize() {
return MAP.values().stream()
.mapToInt(Collection::size)
.sum();
/*
* TODO: Work on this
* return MAP.values().stream()
* .mapToInt(Collection::size)
* .sum();
*/
return cache.size();
}
/**
* @return List of UUIDs associated with one IP address
*/
public Collection<UUID> getUUIDs(final @NotNull String inetAddress) {
return MAP.getOrDefault(inetAddress, Collections.emptyList());
public @Unmodifiable Collection<UUID> getUUIDs(final @NotNull String inetAddress) {
return cache.getOrDefault(inetAddress, Collections.emptyList());
}
/**
@@ -285,7 +269,7 @@ public final class VerifiedPlayerController {
*/
public void clearAll() {
try {
MAP.clear();
cache.clear();
// Only update the column if the database type is not NONE
if (cachedDatabaseType != SonarConfiguration.Database.Type.NONE) {
@@ -302,7 +286,7 @@ public final class VerifiedPlayerController {
* @return Whether the local cache contains the IP and UUID
*/
public boolean has(final @NotNull String inetAddress, final @NotNull UUID uuid) {
final Collection<UUID> got = MAP.get(inetAddress);
final Collection<UUID> got = cache.get(inetAddress);
if (got != null) {
return got.contains(uuid);
}
@@ -324,7 +308,7 @@ public final class VerifiedPlayerController {
* @return Whether the local cache contains the IP
*/
public boolean has(final @NotNull String inetAddress) {
return MAP.containsKey(inetAddress);
return cache.containsKey(inetAddress);
}
/**
@@ -24,11 +24,7 @@ import io.netty.util.ReferenceCountUtil;
import net.kyori.adventure.text.Component;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.event.impl.UserBlacklistedEvent;
import xyz.jonesdev.sonar.api.event.impl.UserVerifyFailedEvent;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.api.statistics.Statistics;
import java.net.InetAddress;
import java.time.Duration;
@@ -43,8 +39,8 @@ public interface FallbackUser {
@NotNull ProtocolVersion getProtocolVersion();
/**
* Kicks the player from the server with
* the given disconnect message.
* Disconnect the player during/after verification
* using our custom Disconnect packet.
*
* @param reason Disconnect message component
*/
@@ -131,54 +127,5 @@ public interface FallbackUser {
*
* @param reason Reason for failing the verification
*/
default void fail(final @Nullable String reason) {
if (getChannel().isActive()) {
disconnect(Sonar.get().getConfig().getVerification().getVerificationFailed());
if (reason != null) {
// Only log the failed message if the server isn't under attack.
// We let the user override this through the configuration.
if (!Sonar.get().getAttackTracker().isCurrentlyUnderAttack()
|| Sonar.get().getConfig().getVerification().isLogDuringAttack()) {
Sonar.get().getFallback().getLogger().info(Sonar.get().getConfig().getVerification().getFailedLog()
.replace("%ip%", Sonar.get().getConfig().formatAddress(getInetAddress()))
.replace("%protocol%", String.valueOf(getProtocolVersion().getProtocol()))
.replace("%reason%", reason));
}
}
}
Statistics.FAILED_VERIFICATIONS.increment();
// Call the VerifyFailedEvent for external API usage
Sonar.get().getEventManager().publish(new UserVerifyFailedEvent(this, reason));
// Use a label, so we can easily add more code beneath this method
blacklist: {
// Check if the player has too many failed attempts
final int blacklistThreshold = Sonar.get().getConfig().getVerification().getBlacklistThreshold();
// The user is allowed to disable the blacklist entirely by setting the threshold to 0
if (blacklistThreshold <= 0) break blacklist;
// Add 1 to the amount of fails since we haven't updated the cached entries yet (for performance)
final int failCount = getFailedCount() + 1;
// Now we simply need to check if the threshold is reached
if (failCount >= blacklistThreshold) {
// Call the BotBlacklistedEvent for external API usage
Sonar.get().getEventManager().publish(new UserBlacklistedEvent(this));
Sonar.get().getFallback().getBlacklist().put(getInetAddress(), (byte) 0);
Sonar.get().getFallback().getLogger().info(Sonar.get().getConfig().getVerification().getBlacklistLog()
.replace("%ip%", Sonar.get().getConfig().formatAddress(getInetAddress()))
.replace("%protocol%", String.valueOf(getProtocolVersion().getProtocol())));
// Invalidate the cached entry to ensure memory safety
invalidateFails();
break blacklist;
}
// Make sure we increment the number of fails
incrementFails();
}
}
void fail(final @Nullable String reason);
}
@@ -20,13 +20,15 @@ package xyz.jonesdev.sonar.api.fallback.protocol;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.util.*;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
@Getter
@RequiredArgsConstructor
public enum ProtocolVersion {
UNKNOWN(-1),
LEGACY(-2),
MINECRAFT_1_7_2(4),
MINECRAFT_1_7_6(5),
MINECRAFT_1_8(47),
@@ -70,10 +72,8 @@ public enum ProtocolVersion {
private final int protocol;
public static final ProtocolVersion MINIMUM_VERSION = MINECRAFT_1_7_2;
public static final ProtocolVersion LATEST_VERSION;
public static final Map<Integer, ProtocolVersion> ID_TO_PROTOCOL_CONSTANT;
public static final Set<ProtocolVersion> SUPPORTED_VERSIONS;
static {
{
@@ -90,12 +90,11 @@ public enum ProtocolVersion {
final Set<ProtocolVersion> versions = EnumSet.noneOf(ProtocolVersion.class);
for (final ProtocolVersion value : values()) {
if (!value.isUnknown() && !value.isLegacy()) {
if (!value.isUnknown()) {
versions.add(value);
}
}
SUPPORTED_VERSIONS = Collections.unmodifiableSet(versions);
LATEST_VERSION = (ProtocolVersion) versions.toArray()[versions.size() - 1];
}
}
@@ -111,9 +110,4 @@ public enum ProtocolVersion {
public boolean isUnknown() {
return this == UNKNOWN;
}
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
public boolean isLegacy() {
return this == LEGACY;
}
}
@@ -15,66 +15,67 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.api.profiler;
package xyz.jonesdev.sonar.api.jvm;
import com.sun.management.OperatingSystemMXBean;
import lombok.experimental.UtilityClass;
import java.lang.management.ManagementFactory;
import java.lang.management.RuntimeMXBean;
public interface JVMProfiler {
Runtime RUNTIME = Runtime.getRuntime();
OperatingSystemMXBean OPERATING_SYSTEM_MX_BEAN = (OperatingSystemMXBean) ManagementFactory.getOperatingSystemMXBean();
RuntimeMXBean RUNTIME_MX_BEAN = ManagementFactory.getRuntimeMXBean();
char[] MEMORY_UNITS = {'k', 'M', 'G', 'T', 'P', 'E'};
@UtilityClass
public class JVMProcessInformation {
private final Runtime RUNTIME = Runtime.getRuntime();
private final OperatingSystemMXBean MX = (OperatingSystemMXBean) ManagementFactory.getOperatingSystemMXBean();
private final char[] MEMORY_UNITS = {'K', 'M', 'G', 'T', 'P', 'E'};
default String formatMemory(final long size) {
public String formatMemory(final long size) {
if (size < 1024L) {
return size + " B";
}
final int group = (63 - Long.numberOfLeadingZeros(size)) / 10;
final double formattedSize = (double) size / (1L << (group * 10));
final char unit = MEMORY_UNITS[group - 1];
return String.format("%.1f %sB", formattedSize, unit);
// https://en.wikipedia.org/wiki/Byte#Multiple-byte_units
return String.format("%.1f %siB", formattedSize, unit);
}
default int getVirtualCores() {
public int getVirtualCores() {
return RUNTIME.availableProcessors();
}
default double getProcessCPUUsage() {
return OPERATING_SYSTEM_MX_BEAN.getProcessCpuLoad() * 100;
public double getProcessCPUUsage() {
return MX.getProcessCpuLoad() * 100;
}
default double getAverageProcessCPUUsage() {
public double getAverageProcessCPUUsage() {
return getProcessCPUUsage() / getVirtualCores();
}
default double getSystemCPUUsage() {
return OPERATING_SYSTEM_MX_BEAN.getSystemCpuLoad() * 100;
public double getSystemCPUUsage() {
return MX.getSystemCpuLoad() * 100;
}
default double getAverageSystemCPUUsage() {
public double getAverageSystemCPUUsage() {
return getSystemCPUUsage() / getVirtualCores();
}
default double getSystemLoadAverage() {
return OPERATING_SYSTEM_MX_BEAN.getSystemLoadAverage() * 100;
public double getSystemLoadAverage() {
return MX.getSystemLoadAverage() * 100;
}
default long getMaxMemory() {
public long getMaxMemory() {
return RUNTIME.maxMemory();
}
default long getTotalMemory() {
public long getTotalMemory() {
return RUNTIME.totalMemory();
}
default long getFreeMemory() {
public long getFreeMemory() {
return RUNTIME.freeMemory();
}
default long getUsedMemory() {
public long getUsedMemory() {
return getTotalMemory() - getFreeMemory();
}
}
@@ -0,0 +1,29 @@
/*
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.api.ormlite;
import com.j256.ormlite.jdbc.db.MariaDbDatabaseType;
import org.jetbrains.annotations.NotNull;
public final class MariaDbDatabaseTypeAdapter extends MariaDbDatabaseType {
@Override
protected String @NotNull [] getDriverClassNames() {
return new String[]{"xyz.jonesdev.sonar.libs.mariadb.jdbc.Driver"};
}
}
@@ -0,0 +1,29 @@
/*
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.api.ormlite;
import com.j256.ormlite.jdbc.db.MysqlDatabaseType;
import org.jetbrains.annotations.NotNull;
public final class MysqlDatabaseTypeAdapter extends MysqlDatabaseType {
@Override
protected String @NotNull [] getDriverClassNames() {
return new String[]{"xyz.jonesdev.sonar.libs.mysql.cj.jdbc.Driver"};
}
}
@@ -17,29 +17,45 @@
package xyz.jonesdev.sonar.api.statistics;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.jetbrains.annotations.ApiStatus;
import java.time.Duration;
public interface SonarStatistics {
long getConnectionsPerSecond();
@SuppressWarnings("unused")
public interface Counters {
@ApiStatus.Internal
Cache<Long, Byte> LOGINS_PER_SECOND = Caffeine.newBuilder()
.expireAfterWrite(Duration.ofSeconds(1))
.build();
long getLoginsPerSecond();
long getCurrentIncomingBandwidth();
long getCurrentOutgoingBandwidth();
long getTotalIncomingBandwidth();
long getTotalOutgoingBandwidth();
String getCurrentIncomingBandwidthFormatted();
String getCurrentOutgoingBandwidthFormatted();
String getTotalIncomingBandwidthFormatted();
String getTotalOutgoingBandwidthFormatted();
int getTotalPlayersJoined();
int getTotalPlayersVerified();
int getTotalSuccessfulVerifications();
int getTotalFailedVerifications();
long getCurrentAttemptedVerifications();
int getTotalAttemptedVerifications();
long getCurrentBlacklistSize();
long getTotalBlacklistSize();
@ApiStatus.Internal
Cache<Long, Byte> CONNECTIONS_PER_SECOND = Caffeine.newBuilder()
.expireAfterWrite(Duration.ofSeconds(1))
.build();
static long getConnectionsPerSecond() {
return CONNECTIONS_PER_SECOND.estimatedSize();
}
static long getLoginsPerSecond() {
return LOGINS_PER_SECOND.estimatedSize();
}
void cleanUpCache();
}
@@ -1,68 +0,0 @@
/*
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.api.statistics;
public enum Statistics {
TOTAL_TRAFFIC,
REAL_TRAFFIC,
FAILED_VERIFICATIONS;
private int val = -1;
/**
* Increments the current value by 1 with the default value of 0
*
* @see #get(int)
* @see #set(int)
*/
public void increment() {
increment(0);
}
/**
* Increments the current value by 1 with a custom default value
*
* @see #get(int)
* @see #set(int)
*/
public void increment(final int def) {
set(get(def) + 1);
}
/**
* @return The current value with the default value of 0
* @see #get(int)
*/
public int get() {
return get(0);
}
/**
* @return The current value with a custom default value
*/
public int get(final int def) {
return val == -1 ? def : val;
}
/**
* Set current value to a new value
*/
public void set(final int value) {
val = value;
}
}
@@ -24,25 +24,22 @@ import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.title.Title;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.statistics.Counters;
import xyz.jonesdev.sonar.api.statistics.Statistics;
import xyz.jonesdev.sonar.api.statistics.SonarStatistics;
import java.util.Collection;
import java.util.UUID;
import java.util.Vector;
import static xyz.jonesdev.sonar.api.Sonar.DECIMAL_FORMAT;
import static xyz.jonesdev.sonar.api.statistics.Bandwidth.INCOMING;
import static xyz.jonesdev.sonar.api.statistics.Bandwidth.OUTGOING;
@Getter
public final class Notification implements Observable, Counters {
private final @NotNull Collection<String> subscribers = new Vector<>(0);
public final class Notification implements Observable {
private final @NotNull Collection<UUID> subscribers = new Vector<>(0);
public void sendAttackNotification() {
// No need to do anything if we have no subscribers
if (subscribers.isEmpty()) {
return;
}
if (subscribers.isEmpty()) return;
final SonarStatistics statistics = Sonar.get().getStatistics();
// Prepare the title
// TODO: Why do custom title times not work?
// Title.Times.times(Ticks.duration(5L), Ticks.duration(60L), Ticks.duration(10L));
@@ -52,25 +49,25 @@ public final class Notification implements Observable, Counters {
// Prepare the chat message
final Component chat = MiniMessage.miniMessage().deserialize(
Sonar.get().getConfig().getNotifications().getNotificationChat()
.replace("%queued%",
DECIMAL_FORMAT.format(Sonar.get().getFallback().getQueue().getQueuedPlayers().size()))
.replace("%verifying%", DECIMAL_FORMAT.format(Sonar.get().getFallback().getConnected().size()))
.replace("%blacklisted%",
DECIMAL_FORMAT.format(Sonar.get().getFallback().getBlacklist().estimatedSize()))
.replace("%total-joins%", DECIMAL_FORMAT.format(Statistics.TOTAL_TRAFFIC.get()))
.replace("%logins-per-second%", DECIMAL_FORMAT.format(LOGINS_PER_SECOND.estimatedSize()))
.replace("%connections-per-second%", DECIMAL_FORMAT.format(CONNECTIONS_PER_SECOND.estimatedSize()))
.replace("%verify-total%", DECIMAL_FORMAT.format(Statistics.REAL_TRAFFIC.get()))
.replace("%verify-success%",
DECIMAL_FORMAT.format(Sonar.get().getVerifiedPlayerController().estimatedSize()))
.replace("%verify-failed%", DECIMAL_FORMAT.format(Statistics.FAILED_VERIFICATIONS.get()))
.replace("%incoming-traffic%", INCOMING.getCachedSecond())
.replace("%outgoing-traffic%", OUTGOING.getCachedSecond())
.replace("%incoming-traffic-ttl%", INCOMING.getCachedTtl())
.replace("%outgoing-traffic-ttl%", OUTGOING.getCachedTtl()));
.replace("%queued%",
DECIMAL_FORMAT.format(Sonar.get().getFallback().getQueue().getQueuedPlayers().size()))
.replace("%verifying%", DECIMAL_FORMAT.format(Sonar.get().getFallback().getConnected().size()))
.replace("%blacklisted%",
DECIMAL_FORMAT.format(Sonar.get().getFallback().getBlacklist().estimatedSize()))
.replace("%total-joins%", DECIMAL_FORMAT.format(statistics.getTotalPlayersJoined()))
.replace("%logins-per-second%", DECIMAL_FORMAT.format(statistics.getLoginsPerSecond()))
.replace("%connections-per-second%", DECIMAL_FORMAT.format(statistics.getConnectionsPerSecond()))
.replace("%verify-total%", DECIMAL_FORMAT.format(statistics.getTotalAttemptedVerifications()))
.replace("%verify-success%",
DECIMAL_FORMAT.format(Sonar.get().getVerifiedPlayerController().estimatedSize()))
.replace("%verify-failed%", DECIMAL_FORMAT.format(statistics.getTotalFailedVerifications()))
.replace("%incoming-traffic%", statistics.getCurrentIncomingBandwidthFormatted())
.replace("%outgoing-traffic%", statistics.getCurrentOutgoingBandwidthFormatted())
.replace("%incoming-traffic-ttl%", statistics.getTotalIncomingBandwidthFormatted())
.replace("%outgoing-traffic-ttl%", statistics.getTotalOutgoingBandwidthFormatted()));
// Send the title and chat messages to all online players
for (final String subscriber : Sonar.get().getNotificationHandler().getSubscribers()) {
final Audience audience = Sonar.AUDIENCES.get(subscriber);
for (final UUID subscriber : Sonar.get().getNotificationHandler().getSubscribers()) {
final Audience audience = Sonar.get().audience(subscriber);
if (audience == null) continue;
audience.showTitle(title);
audience.sendMessage(chat);
@@ -20,29 +20,30 @@ package xyz.jonesdev.sonar.api.verbose;
import org.jetbrains.annotations.NotNull;
import java.util.Collection;
import java.util.UUID;
public interface Observable {
Collection<String> getSubscribers();
Collection<UUID> getSubscribers();
/**
* @param name Name of the audience
* @param uuid UUID of the player
* @return Whether the audience is subscribed or not
*/
default boolean isSubscribed(final @NotNull String name) {
return getSubscribers().contains(name);
default boolean isSubscribed(final @NotNull UUID uuid) {
return getSubscribers().contains(uuid);
}
/**
* @param name Name of the audience to subscribe
* @param uuid UUID of the player
*/
default void subscribe(final String name) {
getSubscribers().add(name);
default void subscribe(final UUID uuid) {
getSubscribers().add(uuid);
}
/**
* @param name Name of the audience to unsubscribe
* @param uuid UUID of the player
*/
default void unsubscribe(final String name) {
getSubscribers().remove(name);
default void unsubscribe(final UUID uuid) {
getSubscribers().remove(uuid);
}
}
@@ -25,44 +25,39 @@ import net.kyori.adventure.text.minimessage.MiniMessage;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.attack.AttackTracker;
import xyz.jonesdev.sonar.api.profiler.JVMProfiler;
import xyz.jonesdev.sonar.api.statistics.Counters;
import xyz.jonesdev.sonar.api.statistics.Statistics;
import xyz.jonesdev.sonar.api.statistics.SonarStatistics;
import xyz.jonesdev.sonar.api.timer.SystemTimer;
import java.util.Collection;
import java.util.UUID;
import java.util.Vector;
import static xyz.jonesdev.sonar.api.Sonar.DECIMAL_FORMAT;
import static xyz.jonesdev.sonar.api.statistics.Bandwidth.INCOMING;
import static xyz.jonesdev.sonar.api.statistics.Bandwidth.OUTGOING;
import static xyz.jonesdev.sonar.api.jvm.JVMProcessInformation.*;
@Getter
public final class Verbose implements Observable, JVMProfiler, Counters {
private final @NotNull Collection<String> subscribers = new Vector<>(0);
public final class Verbose implements Observable {
private final @NotNull Collection<UUID> subscribers = new Vector<>(0);
private int animationIndex;
// Run action bar verbose
public void update() {
// Clean up all blacklisted IPs
Sonar.get().getFallback().getBlacklist().cleanUp();
// Clean up all counters
LOGINS_PER_SECOND.cleanUp();
CONNECTIONS_PER_SECOND.cleanUp();
// Make sure to clean up the cached statistics
Sonar.get().getStatistics().cleanUpCache();
// 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
for (final String subscriber : subscribers) {
final Audience audience = Sonar.AUDIENCES.get(subscriber);
for (final UUID subscriber : subscribers) {
final Audience audience = Sonar.get().audience(subscriber);
if (audience == null) continue;
audience.sendActionBar(component);
}
}
public @NotNull Component prepareActionBarFormat() {
final SonarStatistics statistics = Sonar.get().getStatistics();
final AttackTracker.AttackStatistics attackStatistics = Sonar.get().getAttackTracker().getCurrentAttack();
final SystemTimer attackDuration = attackStatistics == null ? null : attackStatistics.getDuration();
return MiniMessage.miniMessage().deserialize((attackDuration != null
@@ -73,18 +68,19 @@ public final class Verbose implements Observable, JVMProfiler, Counters {
.replace("%verifying%", DECIMAL_FORMAT.format(Sonar.get().getFallback().getConnected().size()))
.replace("%blacklisted%",
DECIMAL_FORMAT.format(Sonar.get().getFallback().getBlacklist().estimatedSize()))
.replace("%total-joins%", DECIMAL_FORMAT.format(Statistics.TOTAL_TRAFFIC.get()))
.replace("%logins-per-second%", DECIMAL_FORMAT.format(LOGINS_PER_SECOND.estimatedSize()))
.replace("%connections-per-second%", DECIMAL_FORMAT.format(CONNECTIONS_PER_SECOND.estimatedSize()))
.replace("%verify-total%", DECIMAL_FORMAT.format(Statistics.REAL_TRAFFIC.get()))
.replace("%blacklisted%", DECIMAL_FORMAT.format(statistics.getCurrentBlacklistSize()))
.replace("%total-joins%", DECIMAL_FORMAT.format(statistics.getTotalPlayersJoined()))
.replace("%logins-per-second%", DECIMAL_FORMAT.format(statistics.getLoginsPerSecond()))
.replace("%connections-per-second%", DECIMAL_FORMAT.format(statistics.getConnectionsPerSecond()))
.replace("%verify-total%", DECIMAL_FORMAT.format(statistics.getTotalAttemptedVerifications()))
.replace("%verify-success%",
DECIMAL_FORMAT.format(Sonar.get().getVerifiedPlayerController().estimatedSize()))
.replace("%verify-failed%", DECIMAL_FORMAT.format(Statistics.FAILED_VERIFICATIONS.get()))
.replace("%verify-failed%", DECIMAL_FORMAT.format(statistics.getTotalFailedVerifications()))
.replace("%attack-duration%", attackDuration == null ? "00:00" : attackDuration.formattedDelay())
.replace("%incoming-traffic%", INCOMING.getCachedSecond())
.replace("%outgoing-traffic%", OUTGOING.getCachedSecond())
.replace("%incoming-traffic-ttl%", INCOMING.getCachedTtl())
.replace("%outgoing-traffic-ttl%", OUTGOING.getCachedTtl())
.replace("%incoming-traffic%", statistics.getCurrentIncomingBandwidthFormatted())
.replace("%outgoing-traffic%", statistics.getCurrentOutgoingBandwidthFormatted())
.replace("%incoming-traffic-ttl%", statistics.getTotalIncomingBandwidthFormatted())
.replace("%outgoing-traffic-ttl%", statistics.getTotalOutgoingBandwidthFormatted())
.replace("%used-memory%", formatMemory(getUsedMemory()))
.replace("%free-memory%", formatMemory(getFreeMemory()))
.replace("%total-memory%", formatMemory(getTotalMemory()))
@@ -17,6 +17,8 @@
package xyz.jonesdev.sonar.api.webhook;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.Sonar;
@@ -36,6 +38,8 @@ import java.util.function.Supplier;
public final class DiscordWebhook {
private final String url;
private static final Gson GSON = new GsonBuilder().disableInnerClassSerialization().create();
private static final ExecutorService HTTP_REQUEST_SERVICE = Executors.newSingleThreadExecutor();
public void post(final Supplier<SonarConfiguration.Webhook.Embed> embed) {
@@ -73,7 +77,7 @@ public final class DiscordWebhook {
final List<Webhook.EmbedMessage> embeds = Collections.singletonList(embedMessage);
final Webhook webhook = new Webhook(content, username, avatarUrl, false, embeds);
return Sonar.GENERAL_GSON.toJson(webhook);
return GSON.toJson(webhook);
}
private @NotNull HttpsURLConnection prepareConnection(final @NotNull URL url,
@@ -19,18 +19,21 @@ package xyz.jonesdev.sonar.bukkit;
import com.alessiodp.libby.BukkitLibraryManager;
import lombok.Getter;
import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.platform.bukkit.BukkitAudiences;
import org.bstats.bukkit.Metrics;
import org.bstats.charts.SimplePie;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.SonarPlatform;
import xyz.jonesdev.sonar.api.logger.LoggerWrapper;
import xyz.jonesdev.sonar.api.statistics.Bandwidth;
import xyz.jonesdev.sonar.bukkit.audience.AudienceListener;
import xyz.jonesdev.sonar.bukkit.command.BukkitSonarCommand;
import xyz.jonesdev.sonar.common.boot.SonarBootstrap;
import xyz.jonesdev.sonar.common.statistics.CachedBandwidthStatistics;
import java.util.Objects;
import java.util.UUID;
@Getter
public final class SonarBukkit extends SonarBootstrap<SonarBukkitPlugin> {
@@ -38,7 +41,7 @@ public final class SonarBukkit extends SonarBootstrap<SonarBukkitPlugin> {
public SonarBukkit(final @NotNull SonarBukkitPlugin plugin) {
super(plugin, new BukkitLibraryManager(
plugin, plugin.getDataFolder().getName()),
plugin, plugin.getDataFolder().getName()),
plugin.getDataFolder(), SonarPlatform.BUKKIT);
INSTANCE = this;
}
@@ -48,6 +51,14 @@ public final class SonarBukkit extends SonarBootstrap<SonarBukkitPlugin> {
*/
private final BukkitAudiences bukkitAudiences = BukkitAudiences.create(getPlugin());
@Override
public @NotNull Audience audience(final @Nullable UUID uniqueId) {
if (uniqueId == null) {
return bukkitAudiences.console();
}
return bukkitAudiences.player(uniqueId);
}
/**
* Create a wrapper for the plugin logger, so we can use it outside
* the velocity module.
@@ -72,21 +83,25 @@ public final class SonarBukkit extends SonarBootstrap<SonarBukkitPlugin> {
}
};
private Metrics metrics;
@Override
public void enable() {
// Initialize bStats.org metrics
new Metrics(getPlugin(), getPlatform().getMetricsId());
metrics = new Metrics(getPlugin(), getPlatform().getMetricsId());
// Add charts for some configuration options
metrics.addCustomChart(new SimplePie("verification",
() -> getConfig().getVerification().getTiming().getDisplayName()));
metrics.addCustomChart(new SimplePie("captcha",
() -> getConfig().getVerification().getMap().getTiming().getDisplayName()));
// Register Sonar command
Objects.requireNonNull(getPlugin().getCommand("sonar")).setExecutor(new BukkitSonarCommand());
// Register audience register listener
getPlugin().getServer().getPluginManager().registerEvents(new AudienceListener(), getPlugin());
// Register traffic service
getPlugin().getServer().getScheduler().runTaskTimerAsynchronously(getPlugin(),
Bandwidth::reset, 20L, 20L);
CachedBandwidthStatistics::reset, 20L, 20L);
// Register queue service
getPlugin().getServer().getScheduler().runTaskTimerAsynchronously(getPlugin(),
@@ -96,4 +111,10 @@ public final class SonarBukkit extends SonarBootstrap<SonarBukkitPlugin> {
getPlugin().getServer().getScheduler().runTaskTimerAsynchronously(getPlugin(),
Sonar.get().getVerboseHandler()::update, 4L, 4L);
}
@Override
public void disable() {
// Make sure to properly shutdown bStats metrics
metrics.shutdown();
}
}
@@ -1,42 +0,0 @@
/*
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.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.HIGHEST)
public void handle(final @NotNull PlayerJoinEvent event) {
final Audience audience = SonarBukkit.INSTANCE.getBukkitAudiences().player(event.getPlayer());
Sonar.AUDIENCES.put(event.getPlayer().getName(), audience);
}
@EventHandler(priority = EventPriority.HIGHEST)
public void handle(final @NotNull PlayerQuitEvent event) {
Sonar.AUDIENCES.remove(event.getPlayer().getName());
}
}
@@ -25,7 +25,7 @@ import xyz.jonesdev.sonar.bukkit.SonarBukkit;
public final class BukkitInvocationSource extends InvocationSource {
public BukkitInvocationSource(final @NotNull CommandSender sender) {
super(sender.getName(),
super(sender instanceof Player ? ((Player) sender).getUniqueId() : null,
SonarBukkit.INSTANCE.getBukkitAudiences().sender(sender),
sender instanceof Player,
sender::hasPermission);
@@ -19,19 +19,22 @@ package xyz.jonesdev.sonar.bungee;
import com.alessiodp.libby.BungeeLibraryManager;
import lombok.Getter;
import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.platform.bungeecord.BungeeAudiences;
import org.bstats.bungeecord.Metrics;
import org.bstats.charts.SimplePie;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.SonarPlatform;
import xyz.jonesdev.sonar.api.logger.LoggerWrapper;
import xyz.jonesdev.sonar.api.statistics.Bandwidth;
import xyz.jonesdev.sonar.bungee.audience.AudienceListener;
import xyz.jonesdev.sonar.bungee.command.BungeeSonarCommand;
import xyz.jonesdev.sonar.bungee.fallback.FallbackInjectionHelper;
import xyz.jonesdev.sonar.bungee.fallback.FallbackLoginListener;
import xyz.jonesdev.sonar.common.boot.SonarBootstrap;
import xyz.jonesdev.sonar.common.statistics.CachedBandwidthStatistics;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
@Getter
@@ -48,6 +51,14 @@ public final class SonarBungee extends SonarBootstrap<SonarBungeePlugin> {
*/
private final BungeeAudiences bungeeAudiences = BungeeAudiences.create(getPlugin());
@Override
public @NotNull Audience audience(final @Nullable UUID uniqueId) {
if (uniqueId == null) {
return bungeeAudiences.console();
}
return bungeeAudiences.player(uniqueId);
}
/**
* Create a wrapper for the plugin logger, so we can use it outside
* the velocity module.
@@ -72,11 +83,18 @@ public final class SonarBungee extends SonarBootstrap<SonarBungeePlugin> {
}
};
private Metrics metrics;
@Override
public void enable() {
// Initialize bStats.org metrics
new Metrics(getPlugin(), getPlatform().getMetricsId());
metrics = new Metrics(getPlugin(), getPlatform().getMetricsId());
// Add charts for some configuration options
metrics.addCustomChart(new SimplePie("verification",
() -> getConfig().getVerification().getTiming().getDisplayName()));
metrics.addCustomChart(new SimplePie("captcha",
() -> getConfig().getVerification().getMap().getTiming().getDisplayName()));
// Register Sonar command
getPlugin().getServer().getPluginManager().registerCommand(getPlugin(), new BungeeSonarCommand());
@@ -84,11 +102,8 @@ public final class SonarBungee extends SonarBootstrap<SonarBungeePlugin> {
// Register Fallback listener
getPlugin().getServer().getPluginManager().registerListener(getPlugin(), new FallbackLoginListener());
// Register audience register listener
getPlugin().getServer().getPluginManager().registerListener(getPlugin(), new AudienceListener());
// Register traffic service
getPlugin().getServer().getScheduler().schedule(getPlugin(), Bandwidth::reset,
getPlugin().getServer().getScheduler().schedule(getPlugin(), CachedBandwidthStatistics::reset,
1L, 1L, TimeUnit.SECONDS);
// Register queue service
@@ -102,4 +117,10 @@ public final class SonarBungee extends SonarBootstrap<SonarBungeePlugin> {
// Make sure to inject into the server's connection handler
FallbackInjectionHelper.inject();
}
@Override
public void disable() {
// Make sure to properly shutdown bStats metrics
metrics.shutdown();
}
}
@@ -1,42 +0,0 @@
/*
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.bungee.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.HIGHEST)
public void handle(final @NotNull PostLoginEvent event) {
final Audience audience = SonarBungee.INSTANCE.getBungeeAudiences().player(event.getPlayer());
Sonar.AUDIENCES.put(event.getPlayer().getName(), audience);
}
@EventHandler(priority = EventPriority.HIGHEST)
public void handle(final @NotNull PlayerDisconnectEvent event) {
Sonar.AUDIENCES.remove(event.getPlayer().getName());
}
}
@@ -25,7 +25,7 @@ import xyz.jonesdev.sonar.bungee.SonarBungee;
public final class BungeeInvocationSource extends InvocationSource {
public BungeeInvocationSource(final @NotNull CommandSender sender) {
super(sender.getName(),
super(sender instanceof ProxiedPlayer ? ((ProxiedPlayer) sender).getUniqueId() : null,
SonarBungee.INSTANCE.getBungeeAudiences().sender(sender),
sender instanceof ProxiedPlayer,
sender::hasPermission);
@@ -17,56 +17,42 @@
package xyz.jonesdev.sonar.bungee.fallback;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.handler.codec.CorruptedFrameException;
import lombok.RequiredArgsConstructor;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.json.JSONComponentSerializer;
import net.md_5.bungee.api.chat.BaseComponent;
import net.md_5.bungee.chat.ComponentSerializer;
import net.md_5.bungee.netty.ChannelWrapper;
import net.md_5.bungee.netty.HandlerBoss;
import net.md_5.bungee.protocol.DefinedPacket;
import net.md_5.bungee.protocol.PacketWrapper;
import net.md_5.bungee.protocol.packet.Handshake;
import net.md_5.bungee.protocol.packet.Kick;
import net.md_5.bungee.protocol.packet.LoginRequest;
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.Fallback;
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.api.statistics.Counters;
import xyz.jonesdev.sonar.api.statistics.Statistics;
import xyz.jonesdev.sonar.common.fallback.FallbackBandwidthHandler;
import xyz.jonesdev.sonar.common.fallback.FallbackChannelHandlerAdapter;
import xyz.jonesdev.sonar.common.fallback.FallbackUserWrapper;
import xyz.jonesdev.sonar.common.statistics.GlobalSonarStatistics;
import xyz.jonesdev.sonar.common.utility.geyser.GeyserUtil;
import java.lang.reflect.Field;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.util.UUID;
import static net.md_5.bungee.netty.PipelineUtils.*;
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_BANDWIDTH;
import static xyz.jonesdev.sonar.common.fallback.FallbackUserWrapper.closeWith;
import static xyz.jonesdev.sonar.common.fallback.FallbackUserWrapper.customDisconnect;
import static xyz.jonesdev.sonar.common.fallback.FallbackUserWrapper.deject;
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer.*;
@RequiredArgsConstructor
public final class FallbackChannelHandler extends ChannelInboundHandlerAdapter {
private final Channel channel;
private @Nullable String username;
private ProtocolVersion protocolVersion;
private InetAddress inetAddress;
public final class FallbackChannelHandler extends FallbackChannelHandlerAdapter {
private static final Fallback FALLBACK = Sonar.get().getFallback();
private static final Cache<Component, Kick> CACHED_KICK_PACKETS = Caffeine.newBuilder().build();
public FallbackChannelHandler(final @NotNull Channel channel) {
super(channel);
}
private static final Field CHANNEL_WRAPPER_FIELD;
@@ -79,41 +65,6 @@ public final class FallbackChannelHandler extends ChannelInboundHandlerAdapter {
}
}
public static @NotNull Kick getCachedKickPacket(final Component component) {
Kick packet = CACHED_KICK_PACKETS.asMap().get(component);
if (packet == null) {
final String serialized = JSONComponentSerializer.json().serialize(component);
final BaseComponent baseComponent = ComponentSerializer.deserialize(serialized);
packet = new Kick(baseComponent);
CACHED_KICK_PACKETS.put(component, packet);
}
return packet;
}
@Override
public void channelActive(final @NotNull ChannelHandlerContext ctx) throws Exception {
// Increase connections per second for the action bar verbose
Counters.CONNECTIONS_PER_SECOND.put(System.nanoTime(), (byte) 0);
// Make sure to let the server handle the rest
ctx.fireChannelActive();
}
@Override
public void channelInactive(final @NotNull ChannelHandlerContext ctx) throws Exception {
// The player can disconnect without sending the login packet first
if (username != null) {
// Remove the username from the connected players
FALLBACK.getConnected().remove(username);
}
// The player cannot be in the queue if the IP address is invalid
if (inetAddress != null) {
// Remove the IP address from the queue
FALLBACK.getQueue().remove(inetAddress);
}
// Make sure to let the server handle the rest
ctx.fireChannelInactive();
}
@Override
public void channelRead(final @NotNull ChannelHandlerContext ctx, final Object msg) throws Exception {
// Intercept any packets processed by BungeeCord
@@ -137,18 +88,6 @@ public final class FallbackChannelHandler extends ChannelInboundHandlerAdapter {
ctx.fireChannelRead(msg);
}
// We can override the default exceptionCaught method since this handler
// will run before the MinecraftConnection knows that there has been an error.
// Additionally, this will also run after our custom decoder.
@Override
public void exceptionCaught(final @NotNull ChannelHandlerContext ctx,
final @NotNull Throwable cause) throws Exception {
// Sonar sometimes uses exceptions to quickly close
// the channel and interrupt any other ongoing process.
// Simply close the channel if we encounter any errors.
ctx.close();
}
private void handleHandshake(final @NotNull Handshake handshake) throws Exception {
// Check if the player has already sent a handshake packet
if (protocolVersion != null) {
@@ -160,6 +99,13 @@ public final class FallbackChannelHandler extends ChannelInboundHandlerAdapter {
}
// Store the protocol version
protocolVersion = ProtocolVersion.fromId(handshake.getProtocolVersion());
// Connections from unknown protocol versions will be discarded
// as this is the safest way of handling unwanted connections
if (protocolVersion.isUnknown()) {
// Sonar does NOT support snapshots or unknown versions;
// I'll try my best to stay up-to-date!
throw new CorruptedFrameException("Unknown protocol");
}
// Hook the traffic listener
// TODO: Can we implement this in channelActive?
channel.pipeline().addFirst(FALLBACK_BANDWIDTH, FallbackBandwidthHandler.INSTANCE);
@@ -173,9 +119,7 @@ public final class FallbackChannelHandler extends ChannelInboundHandlerAdapter {
throw new CorruptedFrameException("Already logged on");
}
// Increase joins per second for the action bar verbose
Counters.LOGINS_PER_SECOND.put(System.nanoTime(), (byte) 0);
// Increase total traffic statistic
Statistics.TOTAL_TRAFFIC.increment();
GlobalSonarStatistics.countLogin();
// Store the username
username = loginRequest.getData();
// Make sure to use the potentially modified, real IP
@@ -186,27 +130,27 @@ public final class FallbackChannelHandler extends ChannelInboundHandlerAdapter {
// Check the blacklist here since we cannot let the player "ghost join"
if (FALLBACK.getBlacklist().asMap().containsKey(inetAddress)) {
closeWith(channel, protocolVersion,
getCachedKickPacket(Sonar.get().getConfig().getVerification().getBlacklisted()));
customDisconnect(channel, protocolVersion, blacklisted, PACKET_ENCODER, BOSS_HANDLER);
return;
}
// Don't continue the verification process if the verification is disabled
if (!Sonar.get().getFallback().shouldVerifyNewPlayers()) {
ctx.fireChannelRead(wrappedMessage);
deject(channel.pipeline());
return;
}
// Completely skip Geyser connections
if (GeyserUtil.isGeyserConnection(channel, socketAddress)) {
ctx.fireChannelRead(wrappedMessage);
deject(channel.pipeline());
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(channel, protocolVersion,
getCachedKickPacket(Sonar.get().getConfig().getVerification().getAlreadyQueued()));
customDisconnect(channel, protocolVersion, alreadyQueued, PACKET_ENCODER, BOSS_HANDLER);
return;
}
@@ -214,16 +158,14 @@ public final class FallbackChannelHandler extends ChannelInboundHandlerAdapter {
// → is another player with the same IP address connected to Fallback?
if (FALLBACK.getConnected().containsKey(loginRequest.getData())
|| FALLBACK.getConnected().containsValue(inetAddress)) {
closeWith(channel, protocolVersion,
getCachedKickPacket(Sonar.get().getConfig().getVerification().getAlreadyVerifying()));
customDisconnect(channel, protocolVersion, alreadyVerifying, PACKET_ENCODER, BOSS_HANDLER);
return;
}
// Check if the protocol ID of the player is not allowed to enter the server
if (Sonar.get().getConfig().getVerification().getBlacklistedProtocols()
.contains(protocolVersion.getProtocol())) {
closeWith(channel, protocolVersion,
getCachedKickPacket(Sonar.get().getConfig().getVerification().getProtocolBlacklisted()));
customDisconnect(channel, protocolVersion, protocolBlacklisted, PACKET_ENCODER, BOSS_HANDLER);
return;
}
@@ -232,13 +174,13 @@ public final class FallbackChannelHandler extends ChannelInboundHandlerAdapter {
final UUID offlineUUID = UUID.nameUUIDFromBytes(offlineUUIDString.getBytes(StandardCharsets.UTF_8));
if (Sonar.get().getVerifiedPlayerController().has(inetAddress, offlineUUID)) {
ctx.fireChannelRead(wrappedMessage);
deject(channel.pipeline());
return;
}
// Check if the IP address is currently being rate-limited
if (!FALLBACK.getRatelimiter().attempt(inetAddress)) {
closeWith(channel, protocolVersion,
getCachedKickPacket(Sonar.get().getConfig().getVerification().getTooFastReconnect()));
customDisconnect(channel, protocolVersion, reconnectedTooFast, PACKET_ENCODER, BOSS_HANDLER);
return;
}
@@ -246,6 +188,7 @@ public final class FallbackChannelHandler extends ChannelInboundHandlerAdapter {
if (Sonar.get().getConfig().getVerification().getWhitelistedProtocols()
.contains(protocolVersion.getProtocol())) {
ctx.fireChannelRead(wrappedMessage);
deject(channel.pipeline());
return;
}
@@ -255,8 +198,7 @@ public final class FallbackChannelHandler extends ChannelInboundHandlerAdapter {
// UTF-16 names or other types of exploits
if (!Sonar.get().getConfig().getVerification().getValidNameRegex()
.matcher(loginRequest.getData()).matches()) {
closeWith(channel, protocolVersion,
Sonar.get().getConfig().getVerification().getInvalidUsername());
customDisconnect(channel, protocolVersion, invalidUsername, PACKET_ENCODER, BOSS_HANDLER);
return;
}
@@ -51,6 +51,8 @@ public final class FallbackChannelInitializer extends ChannelInitializer<Channel
throw new ReflectiveOperationException(throwable);
}
channel.pipeline().addAfter(PACKET_DECODER, FALLBACK_HANDLER, new FallbackChannelHandler(channel));
if (channel.pipeline().get(PACKET_DECODER) != null) {
channel.pipeline().addAfter(PACKET_DECODER, FALLBACK_HANDLER, new FallbackChannelHandler(channel));
}
}
}
@@ -19,13 +19,16 @@ package xyz.jonesdev.sonar.bungee.fallback;
import lombok.RequiredArgsConstructor;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer;
import net.md_5.bungee.api.connection.PendingConnection;
import net.md_5.bungee.api.event.LoginEvent;
import net.md_5.bungee.api.event.PreLoginEvent;
import net.md_5.bungee.api.plugin.Listener;
import net.md_5.bungee.chat.ComponentSerializer;
import net.md_5.bungee.connection.InitialHandler;
import net.md_5.bungee.event.EventHandler;
import net.md_5.bungee.event.EventPriority;
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;
@@ -36,7 +39,6 @@ import java.lang.reflect.Field;
import java.net.InetAddress;
import java.util.Objects;
import static xyz.jonesdev.sonar.bungee.fallback.FallbackChannelHandler.getCachedKickPacket;
import static xyz.jonesdev.sonar.common.fallback.FallbackUserWrapper.closeWith;
@RequiredArgsConstructor
@@ -55,7 +57,7 @@ public final class FallbackLoginListener implements Listener {
@SuppressWarnings("deprecation")
@EventHandler(priority = EventPriority.LOWEST)
public void handle(final @NotNull LoginEvent event) throws IllegalAccessException {
public void handle(final @NotNull PreLoginEvent event) throws IllegalAccessException {
// 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();
@@ -72,9 +74,11 @@ public final class FallbackLoginListener implements Listener {
// We use '>=' because the player connecting to the server hasn't joined yet
if (onlinePerIp >= maxOnlinePerIp) {
final Component component = Sonar.get().getConfig().getTooManyOnlinePerIp();
final String serialized = GsonComponentSerializer.gson().serialize(component);
final PendingConnection connection = event.getConnection();
closeWith(((ChannelWrapper) CHANNEL_FIELD.get(connection)).getHandle(),
ProtocolVersion.fromId(connection.getVersion()), getCachedKickPacket(component));
ProtocolVersion.fromId(connection.getVersion()),
new Kick(ComponentSerializer.deserialize(serialized)));
}
}
}
@@ -40,7 +40,7 @@ class LibraryLoader {
.groupId("com{}google{}code{}gson")
.artifactId("gson")
.version("2.10.1")
.relocate("com{}google{}code{}gson", "xyz{}jonesdev{}sonar{}libs{}gson")
.relocate("com{}google{}gson", "xyz{}jonesdev{}sonar{}libs{}gson")
.build(),
// Caffeine
Library.builder()
@@ -61,6 +61,14 @@ class LibraryLoader {
.groupId("com{}mysql")
.artifactId("mysql-connector-j")
.version("8.3.0")
.relocate("com{}mysql", "xyz{}jonesdev{}sonar{}libs{}mysql")
.build(),
// MariaDB driver
Library.builder()
.groupId("org{}mariadb{}jdbc")
.artifactId("mariadb-java-client")
.version("3.3.3")
.relocate("org{}mariadb", "xyz{}jonesdev{}sonar{}libs{}mariadb")
.build()
);
}
@@ -19,6 +19,8 @@ package xyz.jonesdev.sonar.common.boot;
import com.alessiodp.libby.LibraryManager;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.j256.ormlite.logger.Level;
import com.j256.ormlite.logger.Logger;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
@@ -29,10 +31,12 @@ import xyz.jonesdev.sonar.api.SonarSupplier;
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.statistics.SonarStatistics;
import xyz.jonesdev.sonar.api.timer.SystemTimer;
import xyz.jonesdev.sonar.api.verbose.Notification;
import xyz.jonesdev.sonar.api.verbose.Verbose;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer;
import xyz.jonesdev.sonar.common.statistics.GlobalSonarStatistics;
import xyz.jonesdev.sonar.common.subcommand.SubcommandRegistryHolder;
import xyz.jonesdev.sonar.common.update.UpdateChecker;
@@ -51,6 +55,7 @@ public abstract class SonarBootstrap<T> implements Sonar {
private VerifiedPlayerController verifiedPlayerController;
private File dataDirectory;
private final LibraryManager libraryManager;
private final SonarStatistics statistics;
private final SonarPlatform platform;
private final SubcommandRegistry subcommandRegistry;
private final SystemTimer launchTimer = new SystemTimer();
@@ -69,10 +74,13 @@ public abstract class SonarBootstrap<T> implements Sonar {
this.plugin = plugin;
this.dataDirectory = dataDirectory;
this.platform = platform;
this.statistics = new GlobalSonarStatistics();
this.verboseHandler = new Verbose();
this.notificationHandler = new Notification();
this.config = new SonarConfiguration(dataDirectory);
this.subcommandRegistry = new SubcommandRegistryHolder();
// Hide unnecessary debug information
Logger.setGlobalLogLevel(Level.WARNING);
}
public final void initialize() {
@@ -118,8 +126,7 @@ public abstract class SonarBootstrap<T> implements Sonar {
// Warn player if they reloaded and changed the database type
if (verifiedPlayerController != null
&& verifiedPlayerController.getCachedDatabaseType() != getConfig().getDatabase().getType()) {
getLogger().warn("Reloading the server after changing the database type"
+ " is generally not recommended as it can sometimes cause data loss.");
getLogger().warn("Reloading after changing the database type is not recommended as it may cause data loss.");
}
// Prepare cached packets
@@ -149,14 +156,23 @@ public abstract class SonarBootstrap<T> implements Sonar {
}
// Reinitialize database controller
if (verifiedPlayerController != null) {
// Close the old connection first
verifiedPlayerController.close();
}
verifiedPlayerController = new VerifiedPlayerController();
}
public void shutdown() {
public final void shutdown() {
// Initialize shutdown process
getLogger().info("Starting shutdown process...");
// ...
// Close database connection
verifiedPlayerController.close();
// Run the per-platform disable method
disable();
// Thank the user for using Sonar
getLogger().info("Successfully shut down. Goodbye!");
}
public abstract void disable();
}
@@ -26,8 +26,8 @@ import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.jetbrains.annotations.NotNull;
import static xyz.jonesdev.sonar.api.statistics.Bandwidth.INCOMING;
import static xyz.jonesdev.sonar.api.statistics.Bandwidth.OUTGOING;
import static xyz.jonesdev.sonar.common.statistics.CachedBandwidthStatistics.INCOMING;
import static xyz.jonesdev.sonar.common.statistics.CachedBandwidthStatistics.OUTGOING;
@ChannelHandler.Sharable
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@@ -0,0 +1,75 @@
/*
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.fallback.Fallback;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.statistics.GlobalSonarStatistics;
import java.net.InetAddress;
@RequiredArgsConstructor
public class FallbackChannelHandlerAdapter extends ChannelInboundHandlerAdapter {
protected final Channel channel;
protected @Nullable String username;
protected InetAddress inetAddress;
protected ProtocolVersion protocolVersion;
protected static final Fallback FALLBACK = Sonar.get().getFallback();
@Override
public final void channelActive(final @NotNull ChannelHandlerContext ctx) {
// Increase connections per second for the action bar verbose
GlobalSonarStatistics.countConnection();
// Make sure to let the server handle the rest
ctx.fireChannelActive();
}
@Override
public final void channelInactive(final @NotNull ChannelHandlerContext ctx) {
// The player can disconnect without sending the login packet first
if (username != null) {
// Remove the username from the connected players
FALLBACK.getConnected().remove(username);
}
// The player cannot be in the queue if the IP address is invalid
if (inetAddress != null) {
// Remove the IP address from the queue
FALLBACK.getQueue().remove(inetAddress);
}
// Make sure to let the server handle the rest
ctx.fireChannelInactive();
}
// We can override the default exceptionCaught method since this handler
// will run before the connection knows that there has been an error.
// Additionally, this will also run after our custom decoder.
@Override
public final void exceptionCaught(final @NotNull ChannelHandlerContext ctx,
final @NotNull Throwable cause) throws Exception {
// Simply close the channel if we encounter any errors.
ctx.close();
}
}
@@ -19,28 +19,32 @@ package xyz.jonesdev.sonar.common.fallback;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelPipeline;
import lombok.Getter;
import lombok.ToString;
import net.kyori.adventure.text.Component;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.event.impl.UserBlacklistedEvent;
import xyz.jonesdev.sonar.api.event.impl.UserVerifyFailedEvent;
import xyz.jonesdev.sonar.api.event.impl.UserVerifyJoinEvent;
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.api.statistics.Statistics;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
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.protocol.packets.play.Disconnect;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.login.LoginSuccessPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.DisconnectPacket;
import xyz.jonesdev.sonar.common.statistics.GlobalSonarStatistics;
import java.net.InetAddress;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
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.FallbackPipelines.*;
@Getter
@ToString(of = {"protocolVersion", "inetAddress"})
@@ -59,49 +63,11 @@ public final class FallbackUserWrapper implements FallbackUser {
this.protocolVersion = protocolVersion;
}
/**
* Disconnect the player during/after verification
* using our custom {@link Disconnect} packet.
*
* @param reason Disconnect message component
*/
@Override
public void disconnect(final @NotNull Component reason) {
closeWith(channel, protocolVersion, Disconnect.create(reason));
closeWith(channel, protocolVersion, DisconnectPacket.create(reason, false));
}
/**
* @param channel Channel to close
* @param protocolVersion Protocol version
* @param msg Packet to close the channel with
*/
public static void closeWith(final @NotNull Channel channel,
final @NotNull ProtocolVersion protocolVersion,
final @NotNull Object msg) {
if (channel.isActive()) {
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_8) < 0
&& protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_7_2) >= 0) {
channel.eventLoop().execute(() -> {
channel.config().setAutoRead(false);
channel.eventLoop().schedule(() -> {
channel.writeAndFlush(msg).addListener(ChannelFutureListener.CLOSE);
}, 250L, TimeUnit.MILLISECONDS);
});
} else {
channel.writeAndFlush(msg).addListener(ChannelFutureListener.CLOSE);
}
}
}
/**
* Takes over the channel and begins the verification process
*
* @param username Username of the player
* @param uuid UUID of the player
* @param encoder Name of the encoder pipeline
* @param decoder Name of the decoder pipeline
* @param timeout Name of the read timeout pipeline
*/
@Override
public void hijack(final @NotNull String username, final @NotNull UUID uuid,
final @NotNull String encoder, final @NotNull String decoder,
@@ -119,7 +85,7 @@ public final class FallbackUserWrapper implements FallbackUser {
}
// The player has joined the verification
Statistics.REAL_TRAFFIC.increment();
GlobalSonarStatistics.totalAttemptedVerifications++;
if (Sonar.get().getConfig().getVerification().isLogConnections()) {
// Only log the processing message if the server isn't under attack.
@@ -149,7 +115,7 @@ public final class FallbackUserWrapper implements FallbackUser {
pipeline.replace(encoder, FALLBACK_PACKET_ENCODER, newEncoder);
// Send LoginSuccess packet to make the client think they are joining the server
write(new LoginSuccess(username, uuid));
write(new LoginSuccessPacket(username, uuid));
// The LoginSuccess packet has been sent, now we can change the registry state
newEncoder.updateRegistry(protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_20_2) >= 0
@@ -159,4 +125,129 @@ public final class FallbackUserWrapper implements FallbackUser {
pipeline.replace(decoder, FALLBACK_PACKET_DECODER,
new FallbackPacketDecoder(this, new FallbackVerificationHandler(this, username, uuid)));
}
@Override
public void fail(final @Nullable String reason) {
if (channel.isActive()) {
disconnect(Sonar.get().getConfig().getVerification().getVerificationFailed());
if (reason != null) {
// Only log the failed message if the server isn't under attack.
// We let the user override this through the configuration.
if (!Sonar.get().getAttackTracker().isCurrentlyUnderAttack()
|| Sonar.get().getConfig().getVerification().isLogDuringAttack()) {
Sonar.get().getFallback().getLogger().info(Sonar.get().getConfig().getVerification().getFailedLog()
.replace("%ip%", Sonar.get().getConfig().formatAddress(getInetAddress()))
.replace("%protocol%", String.valueOf(getProtocolVersion().getProtocol()))
.replace("%reason%", reason));
}
}
}
// Increment amount of total failed verifications
GlobalSonarStatistics.totalFailedVerifications++;
// Call the VerifyFailedEvent for external API usage
Sonar.get().getEventManager().publish(new UserVerifyFailedEvent(this, reason));
// Use a label, so we can easily add more code beneath this method
blacklist: {
// Check if the player has too many failed attempts
final int blacklistThreshold = Sonar.get().getConfig().getVerification().getBlacklistThreshold();
// The user is allowed to disable the blacklist entirely by setting the threshold to 0
if (blacklistThreshold <= 0) break blacklist;
// Add 1 to the amount of fails since we haven't updated the cached entries yet (for performance)
final int failCount = getFailedCount() + 1;
// Now we simply need to check if the threshold is reached
if (failCount >= blacklistThreshold) {
// Call the BotBlacklistedEvent for external API usage
Sonar.get().getEventManager().publish(new UserBlacklistedEvent(this));
// Increment amount of total blacklisted players
GlobalSonarStatistics.totalBlacklistedPlayers++;
Sonar.get().getFallback().getBlacklist().put(getInetAddress(), (byte) 0);
Sonar.get().getFallback().getLogger().info(Sonar.get().getConfig().getVerification().getBlacklistLog()
.replace("%ip%", Sonar.get().getConfig().formatAddress(getInetAddress()))
.replace("%protocol%", String.valueOf(getProtocolVersion().getProtocol())));
// Invalidate the cached entry to ensure memory safety
invalidateFails();
break blacklist;
}
// Make sure we increment the number of fails
incrementFails();
}
}
/**
* @param channel Channel to close
* @param protocolVersion Protocol version
* @param msg Packet to close the channel with
*/
public static void closeWith(final @NotNull Channel channel,
final @NotNull ProtocolVersion protocolVersion,
final @NotNull Object msg) {
if (channel.isActive()) {
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_8) < 0
&& protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_7_2) >= 0) {
channel.eventLoop().execute(() -> {
channel.config().setAutoRead(false);
channel.eventLoop().schedule(() -> {
channel.writeAndFlush(msg).addListener(ChannelFutureListener.CLOSE);
}, 250L, TimeUnit.MILLISECONDS);
});
} else {
channel.writeAndFlush(msg).addListener(ChannelFutureListener.CLOSE);
}
}
}
/**
* Disconnect the player before verification (during login)
* by replacing the encoder before running the method.
*
* @param packet Disconnect packet
* @param encoder Encoder to replace
* @param boss Main pipeline to remove
* @param channel Channel of the player
* @param protocolVersion Protocol version of the player
*/
public static void customDisconnect(final @NotNull Channel channel,
final @NotNull ProtocolVersion protocolVersion,
final @NotNull FallbackPacket packet,
final @NotNull String encoder,
final @NotNull String boss) {
// Remove main pipeline to completely take over the channel
if (channel.pipeline().get(boss) != null) {
channel.pipeline().remove(boss);
}
final ChannelHandler currentEncoder = channel.pipeline().get(encoder);
// Simply close the channel if no decoder exists
if (currentEncoder != null) {
// We don't need to update the encoder if it's already present
if (!(currentEncoder instanceof FallbackPacketEncoder)) {
final FallbackPacketEncoder newEncoder = new FallbackPacketEncoder(protocolVersion);
channel.pipeline().replace(encoder, FALLBACK_PACKET_ENCODER, newEncoder);
}
closeWith(channel, protocolVersion, packet);
} else {
channel.close();
}
}
/**
* Removes Sonar's pipelines from a channel to save
* unnecessary RAM and CPU usage
*
* @param pipeline Pipeline object of the channel
*/
public static void deject(final @NotNull ChannelPipeline pipeline) {
// Remove Sonar's pipelines if they exist
if (pipeline.get(FALLBACK_HANDLER) != null) {
pipeline.remove(FALLBACK_HANDLER);
}
}
}
@@ -34,9 +34,10 @@ import xyz.jonesdev.sonar.common.fallback.protocol.*;
import xyz.jonesdev.sonar.common.fallback.protocol.map.ItemMapType;
import xyz.jonesdev.sonar.common.fallback.protocol.map.MapInfoPreparer;
import xyz.jonesdev.sonar.common.fallback.protocol.map.PreparedMapInfo;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.config.FinishConfiguration;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.login.LoginAcknowledged;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.config.FinishConfigurationPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.login.LoginAcknowledgedPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.*;
import xyz.jonesdev.sonar.common.statistics.GlobalSonarStatistics;
import xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil;
import java.util.Objects;
@@ -140,7 +141,7 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
// 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(expectedKeepAliveId));
user.write(new KeepAlivePacket(expectedKeepAliveId));
}
}
@@ -151,7 +152,7 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
// Generate a random transaction ID
expectedTransactionId = (short) RANDOM.nextInt();
// Send a transaction with the given ID
user.write(new Transaction(0, expectedTransactionId, false));
user.write(new TransactionPacket(0, expectedTransactionId, false));
}
private void sendJoinGamePacket() {
@@ -184,7 +185,7 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
// Add a little randomization to the spawn y coordinate
spawnYPosition = dynamicSpawnYPosition + RANDOM.nextDouble(0.4);
// Teleport the player to the spawn position
user.delayedWrite(new PositionLook(
user.delayedWrite(new PlayerPositionLookPacket(
SPAWN_X_POSITION, spawnYPosition, SPAWN_Z_POSITION,
0f, -90f, expectedTeleportId, false));
// Make sure the player escapes the 1.18.2+ "Loading terrain" screen
@@ -211,19 +212,17 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
}
// Teleport player into the fake lobby by sending an empty chunk
user.delayedWrite(EMPTY_CHUNK_DATA);
// Send an UpdateSectionBlocks packet with a platform of blocks
// to check if the player collides with the solid platform.
user.delayedWrite(updateSectionBlocks);
// Checking gravity is disabled, just finish verification
if (!Sonar.get().getConfig().getVerification().getGravity().isEnabled()) {
// Switch to captcha state if needed
captchaOrFinish();
} else {
// Make sure the player knows we are checking them
user.delayedWrite(youAreBeingChecked);
// Send all packets in one flush
user.getChannel().flush();
return;
}
// Send an UpdateSectionBlocks packet with a platform of blocks
// to check if the player collides with the solid platform.
user.delayedWrite(updateSectionBlocks);
// Send all packets in one flush
user.getChannel().flush();
}
private static boolean validateClientLocale(final @SuppressWarnings("unused") @NotNull FallbackUser user,
@@ -262,8 +261,8 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
checkFrame(!login.elapsed(maxDuration), "took too long to enter captcha");
// Handle incoming chat messages
if (packet instanceof Chat) {
final Chat chat = (Chat) packet;
if (packet instanceof UniversalChatPacket) {
final UniversalChatPacket chat = (UniversalChatPacket) packet;
Objects.requireNonNull(captcha);
if (!chat.getMessage().equals(captcha.getInfo().getAnswer())) {
// Captcha is incorrect
@@ -282,8 +281,8 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
// Only send action bar if the message is actually supposed to be sent
if (!actionBarMessage.isEmpty()) {
final String timeLeft = String.format("%.0f", (maxDuration - login.delay()) / 1000D);
user.write(new Chat(MiniMessage.miniMessage().deserialize(
actionBarMessage.replace("%time-left%", timeLeft)), Chat.GAME_INFO_TYPE));
user.write(new UniversalChatPacket(MiniMessage.miniMessage().deserialize(
actionBarMessage.replace("%time-left%", timeLeft)), UniversalChatPacket.GAME_INFO_TYPE));
// Make sure to reset the timer
actionBar.reset();
}
@@ -291,8 +290,6 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
// Every 10 seconds
if (keepAlive.elapsed(10_000L)) {
// Send the message again to remind the player
user.delayedWrite(enterCodeMessage);
// Send a KeepAlive packet to prevent timeout
user.delayedWrite(CAPTCHA_KEEP_ALIVE);
// Send both packets in one flush
@@ -321,7 +318,7 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
final long timeout = Sonar.get().getConfig().getVerification().getMaxPing();
checkFrame(!login.elapsed(timeout), "time limit exceeded");
if (packet instanceof LoginAcknowledged) {
if (packet instanceof LoginAcknowledgedPacket) {
// Check if we are currently expecting a LoginAcknowledged packet
assertState(State.LOGIN_ACK);
@@ -329,7 +326,7 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
configure();
}
if (packet instanceof FinishConfiguration) {
if (packet instanceof FinishConfigurationPacket) {
// Check if we are currently expecting a FinishConfiguration packet
assertState(State.CONFIGURE);
@@ -342,8 +339,8 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
initialJoinProcess();
}
if (packet instanceof KeepAlive) {
final KeepAlive keepAlive = (KeepAlive) packet;
if (packet instanceof KeepAlivePacket) {
final KeepAlivePacket keepAlive = (KeepAlivePacket) packet;
// 1.7-1.8.9 are sending a KeepAlive packet with the ID 0 every 20 ticks
if (keepAlive.getId() == 0 && user.getProtocolVersion().compareTo(MINECRAFT_1_8) <= 0) return;
@@ -357,8 +354,8 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
sendJoinGamePacket();
}
if (packet instanceof ClientSettings) {
final ClientSettings clientSettings = (ClientSettings) packet;
if (packet instanceof ClientSettingsPacket) {
final ClientSettingsPacket clientSettings = (ClientSettingsPacket) packet;
// Validate the locale using a regex to filter unwanted characters.
checkFrame(validateClientLocale(user, clientSettings.getLocale()), "invalid locale");
@@ -372,8 +369,8 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
resolvedClientSettings = true;
}
if (packet instanceof PluginMessage) {
final PluginMessage pluginMessage = (PluginMessage) packet;
if (packet instanceof PluginMessagePacket) {
final PluginMessagePacket pluginMessage = (PluginMessagePacket) packet;
// Only the brand channel is important, drop the rest
if (pluginMessage.getChannel().equals("MC|Brand")
@@ -404,11 +401,11 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
}
}
if (packet instanceof Transaction) {
if (packet instanceof TransactionPacket) {
// Check if we are currently expecting a Transaction packet
assertState(State.TRANSACTION);
final Transaction transaction = (Transaction) packet;
final TransactionPacket transaction = (TransactionPacket) packet;
// The transaction should always be accepted
checkFrame(transaction.isAccepted(), "transaction not accepted?!");
@@ -431,12 +428,12 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
}
}
if (packet instanceof TeleportConfirm) {
if (packet instanceof TeleportConfirmPacket) {
// Check if we are currently expecting a TeleportConfirm packet
assertState(State.TELEPORT);
// Check if the teleport ID is correct
final TeleportConfirm teleportConfirm = (TeleportConfirm) packet;
final TeleportConfirmPacket teleportConfirm = (TeleportConfirmPacket) packet;
checkFrame(teleportConfirm.getTeleportId() == expectedTeleportId, "invalid teleport ID");
// Reset all values to ensure safety on teleport
@@ -449,18 +446,18 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
}
if (state != State.LOGIN_ACK) {
if (packet instanceof Position) {
final Position position = (Position) packet;
if (packet instanceof PlayerPositionPacket) {
final PlayerPositionPacket position = (PlayerPositionPacket) packet;
handlePositionUpdate(position.getX(), position.getY(), position.getZ(), position.isOnGround());
}
if (packet instanceof PositionLook) {
final PositionLook position = (PositionLook) packet;
if (packet instanceof PlayerPositionLookPacket) {
final PlayerPositionLookPacket position = (PlayerPositionLookPacket) packet;
handlePositionUpdate(position.getX(), position.getY(), position.getZ(), position.isOnGround());
}
if (packet instanceof Player) {
final Player player = (Player) packet;
if (packet instanceof PlayerTickPacket) {
final PlayerTickPacket player = (PlayerTickPacket) packet;
handlePositionUpdate(posX, posY, posZ, player.isOnGround());
}
}
@@ -476,6 +473,8 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
tick = 1;
posY = -1;
expectedTeleportId = -1;
// Check for ground state in the first packet
checkFrame(!ground, "invalid ground state");
}
posX = x;
@@ -536,8 +535,12 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
if (ground) {
// Make sure the player is actually colliding with the blocks and not only spoofing ground
checkFrame(collisionOffsetY > -0.03, "illegal collision: " + collisionOffsetY);
// The player is colliding to blocks, finish verification
captchaOrFinish();
// Check if the player is not spoofing ground
// We cannot use checkFrame as it interferes with the CAPTCHA
if (collisionOffsetY > -1) {
// The player is colliding to blocks, finish verification
captchaOrFinish();
}
return;
} else {
// Make sure the player is colliding with blocks but is sending an invalid ground state
@@ -570,33 +573,33 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
private void captchaOrFinish() {
if (FALLBACK.shouldDoMapCaptcha()) {
if (MapInfoPreparer.getPreparedCAPTCHAs() == 0) {
// This should not happen, but we have to return if there is no captcha prepared
user.disconnect(Sonar.get().getConfig().getVerification().getCurrentlyPreparing());
return;
}
// Set the state to MAP_CAPTCHA, so we don't handle any unnecessary packets
state = State.MAP_CAPTCHA;
if (!Sonar.get().getConfig().getVerification().getGravity().isEnabled()
&& user.getProtocolVersion().compareTo(MINECRAFT_1_18_2) >= 0) {
// Make sure the player escapes the 1.18.2+ "Loading terrain" screen
user.delayedWrite(CAPTCHA_SPAWN_POSITION);
}
// Initialize the map captcha
handleMapCaptcha();
} else {
// Finish the verification
finish();
handleCAPTCHA();
return;
}
// Finish the verification
finish();
}
private void handleMapCaptcha() {
private void handleCAPTCHA() {
if (MapInfoPreparer.getPreparedCAPTCHAs() == 0) {
// This should not happen, but we have to return if there is no captcha prepared
user.disconnect(Sonar.get().getConfig().getVerification().getCurrentlyPreparing());
return;
}
// Set the state to MAP_CAPTCHA, so we don't handle any unnecessary packets
state = State.MAP_CAPTCHA;
if (!Sonar.get().getConfig().getVerification().getGravity().isEnabled()
&& user.getProtocolVersion().compareTo(MINECRAFT_1_18_2) >= 0) {
// Make sure the player escapes the 1.18.2+ "Loading terrain" screen
user.delayedWrite(CAPTCHA_SPAWN_POSITION);
}
// Reset max tries
captchaTriesLeft = Sonar.get().getConfig().getVerification().getMap().getMaxTries();
// Set slot to map
user.delayedWrite(new SetSlot(0, 36, 1, 0,
ItemMapType.FILLED_MAP.getId(user.getProtocolVersion()), SetSlot.MAP_NBT));
user.delayedWrite(new SetSlotPacket(0, 36, 1, 0,
ItemMapType.FILLED_MAP.getId(user.getProtocolVersion()), SetSlotPacket.MAP_NBT));
// Send random captcha to the player
captcha = MapInfoPreparer.getRandomCaptcha();
Objects.requireNonNull(captcha).write(user);
@@ -611,9 +614,13 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
}
private void finish() {
// Something must've gone horribly wrong...
if (state == State.SUCCESS) return;
state = State.SUCCESS;
FALLBACK.getConnected().remove(username);
// Increment amount of total successful verifications
GlobalSonarStatistics.totalSuccessfulVerifications++;
// Add verified player to the database
final VerifiedPlayer verifiedPlayer = new VerifiedPlayer(user.getInetAddress().toString(), playerUuid,
@@ -648,6 +655,11 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
*/
private void checkFrame(final boolean condition, final String message) {
if (!condition) {
if (state == State.POSITION // Gravity check
&& Sonar.get().getConfig().getVerification().getGravity().isCaptchaOnFail()) {
handleCAPTCHA();
return;
}
user.fail(message);
throw new CorruptedFrameException(message);
}
@@ -22,10 +22,10 @@ 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.config.FinishConfiguration;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.config.RegistryData;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.login.LoginAcknowledged;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.login.LoginSuccess;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.config.FinishConfigurationPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.config.RegistryDataPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.login.LoginAcknowledgedPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.login.LoginSuccessPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.*;
import java.util.*;
@@ -38,37 +38,40 @@ import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
public enum FallbackPacketRegistry {
LOGIN {
{
serverbound.register(LoginAcknowledged.class, LoginAcknowledged::new,
serverbound.register(LoginAcknowledgedPacket.class, LoginAcknowledgedPacket::new,
map(0x03, MINECRAFT_1_20_2, false));
clientbound.register(LoginSuccess.class, LoginSuccess::new,
clientbound.register(LoginSuccessPacket.class, LoginSuccessPacket::new,
map(0x02, MINECRAFT_1_7_2, false));
clientbound.register(DisconnectPacket.class, DisconnectPacket::new,
map(0x00, MINECRAFT_1_7_2, true));
}
},
CONFIG {
{
clientbound.register(Disconnect.class, Disconnect::new,
clientbound.register(DisconnectPacket.class, DisconnectPacket::new,
map(0x01, MINECRAFT_1_20_2, true));
clientbound.register(FinishConfiguration.class, FinishConfiguration::new,
clientbound.register(FinishConfigurationPacket.class, FinishConfigurationPacket::new,
map(0x02, MINECRAFT_1_20_2, false));
clientbound.register(KeepAlive.class, KeepAlive::new,
clientbound.register(KeepAlivePacket.class, KeepAlivePacket::new,
map(0x03, MINECRAFT_1_20_2, false));
clientbound.register(RegistryData.class, RegistryData::new,
clientbound.register(RegistryDataPacket.class, RegistryDataPacket::new,
map(0x05, MINECRAFT_1_20_2, true));
serverbound.register(ClientSettings.class, ClientSettings::new,
serverbound.register(ClientSettingsPacket.class, ClientSettingsPacket::new,
map(0x00, MINECRAFT_1_20_2, false));
serverbound.register(PluginMessage.class, PluginMessage::new,
serverbound.register(PluginMessagePacket.class, PluginMessagePacket::new,
map(0x01, MINECRAFT_1_20_2, false));
serverbound.register(FinishConfiguration.class, FinishConfiguration::new,
serverbound.register(FinishConfigurationPacket.class, FinishConfigurationPacket::new,
map(0x02, MINECRAFT_1_20_2, false));
serverbound.register(KeepAlive.class, KeepAlive::new,
serverbound.register(KeepAlivePacket.class, KeepAlivePacket::new,
map(0x03, MINECRAFT_1_20_2, false));
}
},
GAME {
{
clientbound.register(JoinGame.class, JoinGame::new,
clientbound.register(JoinGamePacket.class, JoinGamePacket::new,
map(0x01, MINECRAFT_1_7_2, true),
map(0x23, MINECRAFT_1_9, true),
map(0x25, MINECRAFT_1_13, true),
@@ -82,7 +85,7 @@ public enum FallbackPacketRegistry {
map(0x24, MINECRAFT_1_19_3, true),
map(0x28, MINECRAFT_1_19_4, true),
map(0x29, MINECRAFT_1_20_2, true));
clientbound.register(KeepAlive.class, KeepAlive::new,
clientbound.register(KeepAlivePacket.class, KeepAlivePacket::new,
map(0x00, MINECRAFT_1_7_2, false),
map(0x1F, MINECRAFT_1_9, false),
map(0x21, MINECRAFT_1_13, false),
@@ -96,7 +99,7 @@ public enum FallbackPacketRegistry {
map(0x1F, MINECRAFT_1_19_3, false),
map(0x23, MINECRAFT_1_19_4, false),
map(0x24, MINECRAFT_1_20_2, false));
clientbound.register(Disconnect.class, Disconnect::new,
clientbound.register(DisconnectPacket.class, DisconnectPacket::new,
map(0x40, MINECRAFT_1_7_2, true),
map(0x1A, MINECRAFT_1_9, true),
map(0x1B, MINECRAFT_1_13, true),
@@ -110,7 +113,7 @@ public enum FallbackPacketRegistry {
map(0x17, MINECRAFT_1_19_3, true),
map(0x1A, MINECRAFT_1_19_4, true),
map(0x1B, MINECRAFT_1_20_2, true));
clientbound.register(PositionLook.class, PositionLook::new,
clientbound.register(PlayerPositionLookPacket.class, PlayerPositionLookPacket::new,
map(0x08, MINECRAFT_1_7_2, true),
map(0x2E, MINECRAFT_1_9, true),
map(0x2F, MINECRAFT_1_12_1, true),
@@ -125,7 +128,7 @@ public enum FallbackPacketRegistry {
map(0x38, MINECRAFT_1_19_3, true),
map(0x3C, MINECRAFT_1_19_4, true),
map(0x3E, MINECRAFT_1_20_2, true));
clientbound.register(Abilities.class, Abilities::new,
clientbound.register(ClientAbilitiesPacket.class, ClientAbilitiesPacket::new,
map(0x39, MINECRAFT_1_7_2, true),
map(0x2B, MINECRAFT_1_9, true),
map(0x2C, MINECRAFT_1_12_1, true),
@@ -140,7 +143,7 @@ public enum FallbackPacketRegistry {
map(0x30, MINECRAFT_1_19_3, true),
map(0x34, MINECRAFT_1_19_4, true),
map(0x36, MINECRAFT_1_20_2, true));
clientbound.register(EmptyChunkData.class, EmptyChunkData::new,
clientbound.register(ChunkDataPacket.class, ChunkDataPacket::new,
map(0x21, MINECRAFT_1_7_2, true),
map(0x20, MINECRAFT_1_9, true),
map(0x22, MINECRAFT_1_13, true),
@@ -154,7 +157,7 @@ public enum FallbackPacketRegistry {
map(0x20, MINECRAFT_1_19_3, true),
map(0x24, MINECRAFT_1_19_4, true),
map(0x25, MINECRAFT_1_20_2, true));
clientbound.register(UpdateSectionBlocks.class, UpdateSectionBlocks::new,
clientbound.register(UpdateSectionBlocksPacket.class, UpdateSectionBlocksPacket::new,
map(0x22, MINECRAFT_1_7_2, true),
map(0x10, MINECRAFT_1_9, true),
map(0x0F, MINECRAFT_1_13, true),
@@ -168,7 +171,7 @@ public enum FallbackPacketRegistry {
map(0x43, MINECRAFT_1_19_4, true),
map(0x45, MINECRAFT_1_20_2, true),
map(0x47, MINECRAFT_1_20_3, true));
clientbound.register(Transaction.class, Transaction::new,
clientbound.register(TransactionPacket.class, TransactionPacket::new,
map(0x32, MINECRAFT_1_7_2, true),
map(0x11, MINECRAFT_1_9, true),
map(0x12, MINECRAFT_1_13, true),
@@ -181,7 +184,7 @@ public enum FallbackPacketRegistry {
map(0x2E, MINECRAFT_1_19_3, true),
map(0x32, MINECRAFT_1_19_4, true),
map(0x33, MINECRAFT_1_20_2, true));
clientbound.register(DefaultSpawnPosition.class, DefaultSpawnPosition::new,
clientbound.register(DefaultSpawnPositionPacket.class, DefaultSpawnPositionPacket::new,
map(0x05, MINECRAFT_1_7_2, true),
map(0x43, MINECRAFT_1_9, true),
map(0x45, MINECRAFT_1_12, true),
@@ -197,7 +200,7 @@ public enum FallbackPacketRegistry {
map(0x50, MINECRAFT_1_19_4, true),
map(0x52, MINECRAFT_1_20_2, true),
map(0x54, MINECRAFT_1_20_3, true));
clientbound.register(MapData.class, MapData::new,
clientbound.register(MapDataPacket.class, MapDataPacket::new,
map(0x34, MINECRAFT_1_7_2, true),
map(0x24, MINECRAFT_1_9, true),
map(0x26, MINECRAFT_1_13, true),
@@ -210,7 +213,7 @@ public enum FallbackPacketRegistry {
map(0x25, MINECRAFT_1_19_3, true),
map(0x29, MINECRAFT_1_19_4, true),
map(0x2A, MINECRAFT_1_20_2, true));
clientbound.register(SetSlot.class, SetSlot::new,
clientbound.register(SetSlotPacket.class, SetSlotPacket::new,
map(0x2F, MINECRAFT_1_7_2, true),
map(0x16, MINECRAFT_1_9, true),
map(0x17, MINECRAFT_1_13, true),
@@ -223,7 +226,7 @@ public enum FallbackPacketRegistry {
map(0x12, MINECRAFT_1_19_3, true),
map(0x14, MINECRAFT_1_19_4, true),
map(0x15, MINECRAFT_1_20_2, true));
clientbound.register(Chat.class, Chat::new,
clientbound.register(UniversalChatPacket.class, UniversalChatPacket::new,
map(0x02, MINECRAFT_1_7_2, false),
map(0x0F, MINECRAFT_1_9, false),
map(0x0E, MINECRAFT_1_13, false),
@@ -236,10 +239,10 @@ public enum FallbackPacketRegistry {
map(0x64, MINECRAFT_1_19_4, false),
map(0x67, MINECRAFT_1_20_2, false),
map(0x69, MINECRAFT_1_20_3, false));
clientbound.register(GameEvent.class, GameEvent::new,
clientbound.register(GameEventPacket.class, GameEventPacket::new,
map(0x20, MINECRAFT_1_20_3, true));
serverbound.register(Chat.class, Chat::new,
serverbound.register(UniversalChatPacket.class, UniversalChatPacket::new,
map(0x01, MINECRAFT_1_7_2, false),
map(0x02, MINECRAFT_1_9, false),
map(0x03, MINECRAFT_1_12, false),
@@ -248,7 +251,7 @@ public enum FallbackPacketRegistry {
map(0x04, MINECRAFT_1_19, false),
map(0x05, MINECRAFT_1_19_1, false),
map(0x05, MINECRAFT_1_19_3, false));
serverbound.register(KeepAlive.class, KeepAlive::new,
serverbound.register(KeepAlivePacket.class, KeepAlivePacket::new,
map(0x00, MINECRAFT_1_7_2, false),
map(0x0B, MINECRAFT_1_9, false),
map(0x0C, MINECRAFT_1_12, false),
@@ -263,7 +266,7 @@ public enum FallbackPacketRegistry {
map(0x12, MINECRAFT_1_19_4, false),
map(0x14, MINECRAFT_1_20_2, false),
map(0x15, MINECRAFT_1_20_3, false));
serverbound.register(ClientSettings.class, ClientSettings::new,
serverbound.register(ClientSettingsPacket.class, ClientSettingsPacket::new,
map(0x15, MINECRAFT_1_7_2, false),
map(0x04, MINECRAFT_1_9, false),
map(0x05, MINECRAFT_1_12, false),
@@ -274,7 +277,7 @@ public enum FallbackPacketRegistry {
map(0x07, MINECRAFT_1_19_3, false),
map(0x08, MINECRAFT_1_19_4, false),
map(0x09, MINECRAFT_1_20_2, false));
serverbound.register(PluginMessage.class, PluginMessage::new,
serverbound.register(PluginMessagePacket.class, PluginMessagePacket::new,
map(0x17, MINECRAFT_1_7_2, false),
map(0x09, MINECRAFT_1_9, false),
map(0x0A, MINECRAFT_1_12, false),
@@ -287,7 +290,7 @@ public enum FallbackPacketRegistry {
map(0x0C, MINECRAFT_1_19_3, false),
map(0x0D, MINECRAFT_1_19_4, false),
map(0x0F, MINECRAFT_1_20_2, false));
serverbound.register(PositionLook.class, PositionLook::new,
serverbound.register(PlayerPositionLookPacket.class, PlayerPositionLookPacket::new,
map(0x06, MINECRAFT_1_7_2, false),
map(0x0D, MINECRAFT_1_9, false),
map(0x0F, MINECRAFT_1_12, false),
@@ -302,9 +305,9 @@ public enum FallbackPacketRegistry {
map(0x15, MINECRAFT_1_19_4, false),
map(0x17, MINECRAFT_1_20_2, false),
map(0x18, MINECRAFT_1_20_3, false));
serverbound.register(TeleportConfirm.class, TeleportConfirm::new,
serverbound.register(TeleportConfirmPacket.class, TeleportConfirmPacket::new,
map(0x00, MINECRAFT_1_9, false));
serverbound.register(Position.class, Position::new,
serverbound.register(PlayerPositionPacket.class, PlayerPositionPacket::new,
map(0x04, MINECRAFT_1_7_2, false),
map(0x0C, MINECRAFT_1_9, false),
map(0x0E, MINECRAFT_1_12, false),
@@ -319,7 +322,7 @@ public enum FallbackPacketRegistry {
map(0x14, MINECRAFT_1_19_4, false),
map(0x16, MINECRAFT_1_20_2, false),
map(0x17, MINECRAFT_1_20_3, false));
serverbound.register(Player.class, Player::new,
serverbound.register(PlayerTickPacket.class, PlayerTickPacket::new,
map(0x03, MINECRAFT_1_7_2, false),
map(0x0F, MINECRAFT_1_9, false),
map(0x0D, MINECRAFT_1_12, false),
@@ -334,7 +337,7 @@ public enum FallbackPacketRegistry {
map(0x17, MINECRAFT_1_19_4, false),
map(0x19, MINECRAFT_1_20_2, false),
map(0x1A, MINECRAFT_1_20_3, false));
serverbound.register(Transaction.class, Transaction::new,
serverbound.register(TransactionPacket.class, TransactionPacket::new,
map(0x0F, MINECRAFT_1_7_2, false),
map(0x05, MINECRAFT_1_9, false),
map(0x06, MINECRAFT_1_12, false),
@@ -370,7 +373,7 @@ public enum FallbackPacketRegistry {
PacketRegistry() {
final Map<ProtocolVersion, ProtocolRegistry> mutableVersions = new EnumMap<>(ProtocolVersion.class);
for (final ProtocolVersion version : ProtocolVersion.values()) {
if (!version.isLegacy() && !version.isUnknown()) {
if (!version.isUnknown()) {
mutableVersions.put(version, new ProtocolRegistry(version));
}
}
@@ -24,29 +24,28 @@ 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.map.MapInfoPreparer;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.config.FinishConfiguration;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.config.RegistryData;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.config.FinishConfigurationPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.config.RegistryDataPacket;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.*;
@UtilityClass
public class FallbackPreparer {
// Abilities
public final FallbackPacket DEFAULT_ABILITIES = new Abilities(0x00, 0f, 0f);
public final FallbackPacket CAPTCHA_ABILITIES = new Abilities(0x02, 0f, 0f);
public final FallbackPacket DEFAULT_ABILITIES = new ClientAbilitiesPacket(0x00, 0f, 0f);
public final FallbackPacket CAPTCHA_ABILITIES = new ClientAbilitiesPacket(0x02, 0f, 0f);
// Chunks
public final FallbackPacket EMPTY_CHUNK_DATA = new EmptyChunkData(0, 0);
public final FallbackPacket EMPTY_CHUNK_DATA = new ChunkDataPacket(0, 0);
// Finish Configuration
public final FallbackPacket FINISH_CONFIGURATION = new FinishConfiguration();
public final FallbackPacket FINISH_CONFIGURATION = new FinishConfigurationPacket();
// Synchronize Registry
public final FallbackPacket REGISTRY_SYNC = new RegistryData();
public final FallbackPacket REGISTRY_SYNC = new RegistryDataPacket();
// Keep Alive
public final FallbackPacket CAPTCHA_KEEP_ALIVE = new KeepAlive(0L);
public final FallbackPacket CAPTCHA_KEEP_ALIVE = new KeepAlivePacket(0L);
// Game Event (1.20.3+)
public final FallbackPacket START_WRITING_CHUNKS = new GameEvent(13, 0);
public final FallbackPacket START_WRITING_CHUNKS = new GameEventPacket(13, 0);
// Chat
public FallbackPacket enterCodeMessage;
public FallbackPacket youAreBeingChecked;
public FallbackPacket incorrectCaptcha;
// JoinGame
public FallbackPacket joinGame;
@@ -55,6 +54,14 @@ public class FallbackPreparer {
// Default Spawn Position
public FallbackPacket dynamicSpawnPosition;
// Disconnect messages
public FallbackPacket blacklisted;
public FallbackPacket alreadyQueued;
public FallbackPacket alreadyVerifying;
public FallbackPacket reconnectedTooFast;
public FallbackPacket protocolBlacklisted;
public FallbackPacket invalidUsername;
// Collisions
public final int BLOCKS_PER_ROW = 8; // 8 * 8 = 64 (protocol maximum)
public final int SPAWN_X_POSITION = 16 / 2; // middle of the chunk
@@ -62,9 +69,9 @@ public class FallbackPreparer {
public final int DEFAULT_Y_COLLIDE_POSITION = 255; // 255 is the maximum Y position allowed
// Captcha position
public final FallbackPacket CAPTCHA_POSITION = new PositionLook(
public final FallbackPacket CAPTCHA_POSITION = new PlayerPositionLookPacket(
SPAWN_X_POSITION, 1337, SPAWN_Z_POSITION, 0f, 90f, 0, false);
public final FallbackPacket CAPTCHA_SPAWN_POSITION = new DefaultSpawnPosition(
public final FallbackPacket CAPTCHA_SPAWN_POSITION = new DefaultSpawnPositionPacket(
SPAWN_X_POSITION, 1337, SPAWN_Z_POSITION);
// Blocks
@@ -76,16 +83,11 @@ public class FallbackPreparer {
public double maxFallDistance;
public void prepare() {
joinGame = new JoinGame(0,
joinGame = new JoinGamePacket(1,
Sonar.get().getConfig().getVerification().getGravity().getGamemode().getId(),
0,
false,
0,
true,
true,
false,
new String[]{"minecraft:overworld"},
"minecraft:overworld");
0, false, 0,
true, false, false,
new String[]{"minecraft:overworld"}, "minecraft:overworld");
maxFallDistance = 0;
maxMovementTick = Sonar.get().getConfig().getVerification().getGravity().getMaxMovementTicks();
@@ -99,7 +101,7 @@ public class FallbackPreparer {
// Set the dynamic block and collide Y position based on the maximum fall distance
dynamicSpawnYPosition = DEFAULT_Y_COLLIDE_POSITION + (int) Math.ceil(maxFallDistance);
dynamicSpawnPosition = new DefaultSpawnPosition(SPAWN_X_POSITION, dynamicSpawnYPosition, SPAWN_Z_POSITION);
dynamicSpawnPosition = new DefaultSpawnPositionPacket(SPAWN_X_POSITION, dynamicSpawnYPosition, SPAWN_Z_POSITION);
// Prepare collision platform positions
int index = 0;
@@ -115,18 +117,23 @@ public class FallbackPreparer {
}
// Prepare UpdateSectionBlocks packet
updateSectionBlocks = new UpdateSectionBlocks(0, 0, CHANGED_BLOCKS);
updateSectionBlocks = new UpdateSectionBlocksPacket(0, 0, CHANGED_BLOCKS);
// "You are being checked" message
if (Sonar.get().getConfig().getVerification().getGravity().isEnabled()) {
youAreBeingChecked = new Chat(Sonar.get().getConfig().getVerification().getGravity().getYouAreBeingChecked());
}
// Prepare disconnect packets during login
blacklisted = DisconnectPacket.create(Sonar.get().getConfig().getVerification().getBlacklisted(), true);
alreadyVerifying = DisconnectPacket.create(Sonar.get().getConfig().getVerification().getAlreadyVerifying(), true);
alreadyQueued = DisconnectPacket.create(Sonar.get().getConfig().getVerification().getAlreadyQueued(), true);
protocolBlacklisted = DisconnectPacket.create(Sonar.get().getConfig().getVerification().getProtocolBlacklisted(), true);
reconnectedTooFast = DisconnectPacket.create(Sonar.get().getConfig().getVerification().getTooFastReconnect(), true);
invalidUsername = DisconnectPacket.create(Sonar.get().getConfig().getVerification().getInvalidUsername(), true);
if (Sonar.get().getConfig().getVerification().getMap().getTiming() != SonarConfiguration.Verification.Timing.NEVER) {
enterCodeMessage = new Chat(Sonar.get().getConfig().getVerification().getMap().getEnterCode());
incorrectCaptcha = new Chat(Sonar.get().getConfig().getVerification().getMap().getFailedCaptcha());
// Precompute captcha answers
if (Sonar.get().getConfig().getVerification().getMap().getTiming() != SonarConfiguration.Verification.Timing.NEVER
|| Sonar.get().getConfig().getVerification().getGravity().isCaptchaOnFail()) {
enterCodeMessage = new UniversalChatPacket(Sonar.get().getConfig().getVerification().getMap().getEnterCode(), UniversalChatPacket.SYSTEM_TYPE);
incorrectCaptcha = new UniversalChatPacket(
Sonar.get().getConfig().getVerification().getMap().getFailedCaptcha(), UniversalChatPacket.SYSTEM_TYPE);
// Precompute captcha answers
MapInfoPreparer.prepare();
}
}
@@ -20,7 +20,7 @@ package xyz.jonesdev.sonar.common.fallback.protocol.map;
import lombok.Getter;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.MapData;
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.MapDataPacket;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_8;
@@ -30,8 +30,8 @@ public final class PreparedMapInfo {
public static final int SCALE = DIMENSIONS * DIMENSIONS;
private final MapInfo info;
private final MapData[] legacy;
private final MapData modern;
private final MapDataPacket[] legacy;
private final MapDataPacket modern;
public PreparedMapInfo(final String answer,
final int columns, final int rows,
@@ -44,18 +44,18 @@ public final class PreparedMapInfo {
final byte buf = buffer[i];
grid[i & Byte.MAX_VALUE][i >> 7] = buf;
}
this.legacy = new MapData[grid.length];
this.legacy = new MapDataPacket[grid.length];
for (int i = 0; i < grid.length; i++) {
this.legacy[i] = new MapData(new MapInfo(answer, DIMENSIONS, DIMENSIONS, i, 0, grid[i]));
this.legacy[i] = new MapDataPacket(new MapInfo(answer, DIMENSIONS, DIMENSIONS, i, 0, grid[i]));
}
// Prepare 1.8+ map data
this.modern = new MapData(info);
this.modern = new MapDataPacket(info);
}
public void write(final @NotNull FallbackUser user) {
if (user.getProtocolVersion().compareTo(MINECRAFT_1_8) < 0) {
for (final MapData data : legacy) {
for (final MapDataPacket data : legacy) {
user.delayedWrite(data);
}
} else {
@@ -23,7 +23,7 @@ import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
@ToString
public final class FinishConfiguration implements FallbackPacket {
public final class FinishConfigurationPacket implements FallbackPacket {
@Override
public void encode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
@@ -29,7 +29,7 @@ import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.writeNamel
@Getter
@ToString
public final class RegistryData implements FallbackPacket {
public final class RegistryDataPacket implements FallbackPacket {
@Override
public void encode(final @NotNull ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
@@ -21,7 +21,7 @@ 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 {
public final class LoginAcknowledgedPacket implements FallbackPacket {
@Override
public void encode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
@@ -21,6 +21,7 @@ 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,9 +34,10 @@ import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.*;
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.writeVarInt;
@Getter
@ToString
@NoArgsConstructor
@AllArgsConstructor
public final class LoginSuccess implements FallbackPacket {
public final class LoginSuccessPacket implements FallbackPacket {
private String username;
private UUID uuid;
@@ -36,7 +36,7 @@ import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.writeVarInt;
@ToString
@NoArgsConstructor
@AllArgsConstructor
public final class EmptyChunkData implements FallbackPacket {
public final class ChunkDataPacket implements FallbackPacket {
private int x, z;
private static final byte[] SECTION_BYTES = new byte[]{0, 0, 0, 0, 0, 0, 1, 0};
@@ -30,7 +30,7 @@ import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class Abilities implements FallbackPacket {
public class ClientAbilitiesPacket implements FallbackPacket {
private int encodedFlags;
private float flySpeed, walkSpeed;
@@ -34,7 +34,7 @@ import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.readVarInt;
@ToString
@NoArgsConstructor
@AllArgsConstructor
public final class ClientSettings implements FallbackPacket {
public final class ClientSettingsPacket implements FallbackPacket {
private String locale;
private byte viewDistance;
private int chatVisibility;
@@ -33,7 +33,7 @@ import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT
@ToString
@NoArgsConstructor
@AllArgsConstructor
public final class DefaultSpawnPosition implements FallbackPacket {
public final class DefaultSpawnPositionPacket implements FallbackPacket {
private int x, y, z;
@Override
@@ -1,65 +0,0 @@
/*
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.protocol.packets.play;
import com.google.gson.JsonParser;
import io.netty.buffer.ByteBuf;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
import net.kyori.adventure.nbt.BinaryTag;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.json.JSONComponentSerializer;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
import static xyz.jonesdev.sonar.common.utility.component.ComponentSerializer.serialize;
import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.writeNamelessCompoundTag;
import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.writeString;
@Getter
@ToString
@NoArgsConstructor
@AllArgsConstructor
public final class Disconnect implements FallbackPacket {
private @Nullable String reason;
private BinaryTag binaryTag;
@Override
public void encode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_20_3) >= 0) {
writeNamelessCompoundTag(byteBuf, binaryTag);
} else {
writeString(byteBuf, reason);
}
}
@Override
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
throw new UnsupportedOperationException();
}
public static @NotNull Disconnect create(final Component component) {
final String serialized = JSONComponentSerializer.json().serialize(component);
final BinaryTag binaryTag = serialize(new JsonParser().parse(serialized));
return new Disconnect(serialized, binaryTag);
}
}
@@ -0,0 +1,53 @@
/*
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.fallback.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 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.utility.component.ComponentHolder;
@Getter
@ToString
@NoArgsConstructor
@AllArgsConstructor
public final class DisconnectPacket implements FallbackPacket {
private @NotNull ComponentHolder componentHolder;
private boolean login;
@Override
public void encode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
componentHolder.write(byteBuf, login ? ProtocolVersion.MINECRAFT_1_20_2 : protocolVersion);
}
@Override
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
throw new UnsupportedOperationException();
}
public static @NotNull DisconnectPacket create(final @NotNull Component component,
final boolean login) {
return new DisconnectPacket(new ComponentHolder(component), login);
}
}
@@ -28,7 +28,7 @@ import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
@Data
@NoArgsConstructor
@AllArgsConstructor
public final class GameEvent implements FallbackPacket {
public final class GameEventPacket implements FallbackPacket {
private int type;
private float value;
@@ -35,7 +35,7 @@ import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.writeVarInt;
@Data
@NoArgsConstructor
@AllArgsConstructor
public final class JoinGame implements FallbackPacket {
public final class JoinGamePacket implements FallbackPacket {
private int entityId;
private int gamemode;
private long partialHashedSeed;
@@ -57,6 +57,7 @@ public final class JoinGame implements FallbackPacket {
byteBuf.writeByte(0); // difficulty
byteBuf.writeByte(0); // max players
writeString(byteBuf, "flat"); // level type
return;
}
if (protocolVersion.inBetween(MINECRAFT_1_8, MINECRAFT_1_9)) {
@@ -66,6 +67,7 @@ public final class JoinGame implements FallbackPacket {
byteBuf.writeByte(0); // max players
writeString(byteBuf, "flat"); // level type
byteBuf.writeBoolean(reducedDebugInfo);
return;
}
if (protocolVersion.inBetween(MINECRAFT_1_9_1, MINECRAFT_1_13_2)) {
@@ -75,6 +77,7 @@ public final class JoinGame implements FallbackPacket {
byteBuf.writeByte(0); // max players
writeString(byteBuf, "flat"); // level type
byteBuf.writeBoolean(reducedDebugInfo);
return;
}
if (protocolVersion.inBetween(MINECRAFT_1_14, MINECRAFT_1_14_4)) {
@@ -84,6 +87,7 @@ public final class JoinGame implements FallbackPacket {
writeString(byteBuf, "flat"); // level type
writeVarInt(byteBuf, viewDistance);
byteBuf.writeBoolean(reducedDebugInfo);
return;
}
if (protocolVersion.inBetween(MINECRAFT_1_15, MINECRAFT_1_15_2)) {
@@ -95,6 +99,7 @@ public final class JoinGame implements FallbackPacket {
writeVarInt(byteBuf, viewDistance);
byteBuf.writeBoolean(reducedDebugInfo);
byteBuf.writeBoolean(showRespawnScreen);
return;
}
if (protocolVersion.inBetween(MINECRAFT_1_16, MINECRAFT_1_16_1)) {
@@ -111,6 +116,7 @@ public final class JoinGame implements FallbackPacket {
byteBuf.writeBoolean(showRespawnScreen);
byteBuf.writeBoolean(false); // debug type
byteBuf.writeBoolean(false); // flat
return;
}
if (protocolVersion.inBetween(MINECRAFT_1_16_2, MINECRAFT_1_17_1)) {
@@ -128,6 +134,7 @@ public final class JoinGame implements FallbackPacket {
byteBuf.writeBoolean(showRespawnScreen);
byteBuf.writeBoolean(false); // debug type
byteBuf.writeBoolean(false); // flat
return;
}
if (protocolVersion.inBetween(MINECRAFT_1_18, MINECRAFT_1_18_2)) {
@@ -151,6 +158,7 @@ public final class JoinGame implements FallbackPacket {
byteBuf.writeBoolean(showRespawnScreen);
byteBuf.writeBoolean(false); // debug type
byteBuf.writeBoolean(false); // flat
return;
}
if (protocolVersion.inBetween(MINECRAFT_1_19, MINECRAFT_1_19_4)) {
@@ -178,6 +186,7 @@ public final class JoinGame implements FallbackPacket {
byteBuf.writeBoolean(false); // debug type
byteBuf.writeBoolean(false); // flat
byteBuf.writeBoolean(false); // no last death location
return;
}
if (protocolVersion.equals(MINECRAFT_1_20)) {
@@ -198,6 +207,7 @@ public final class JoinGame implements FallbackPacket {
byteBuf.writeBoolean(false); // flat
byteBuf.writeBoolean(false); // no last death location
writeVarInt(byteBuf, 0);
return;
}
if (protocolVersion.compareTo(MINECRAFT_1_20_2) >= 0) {
@@ -35,7 +35,7 @@ import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.writeVarInt;
@ToString
@NoArgsConstructor
@AllArgsConstructor
public final class KeepAlive implements FallbackPacket {
public final class KeepAlivePacket implements FallbackPacket {
private long id;
@Override
@@ -33,7 +33,7 @@ import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.writeVarInt;
@ToString
@NoArgsConstructor
@AllArgsConstructor
public final class MapData implements FallbackPacket {
public final class MapDataPacket implements FallbackPacket {
private MapInfo mapInfo;
@Override
@@ -33,7 +33,7 @@ import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.writeVarInt;
@ToString
@NoArgsConstructor
@AllArgsConstructor
public final class PositionLook implements FallbackPacket {
public final class PlayerPositionLookPacket implements FallbackPacket {
private double x, y, z;
private float yaw, pitch;
private int teleportId;
@@ -30,7 +30,7 @@ import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
@ToString
@NoArgsConstructor
@AllArgsConstructor
public final class Position implements FallbackPacket {
public final class PlayerPositionPacket implements FallbackPacket {
private double x, y, z;
private boolean onGround;
@@ -30,7 +30,7 @@ import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
@ToString
@NoArgsConstructor
@AllArgsConstructor
public final class Player implements FallbackPacket {
public final class PlayerTickPacket implements FallbackPacket {
private boolean onGround;
@Override
@@ -33,26 +33,26 @@ import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.*;
// https://github.com/PaperMC/Velocity/blob/dev/3.0.0/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/PluginMessage.java
@Getter
@ToString
public final class PluginMessage extends DeferredByteBufHolder implements FallbackPacket {
public final class PluginMessagePacket extends DeferredByteBufHolder implements FallbackPacket {
private String channel;
public PluginMessage() {
public PluginMessagePacket() {
super(null);
}
@Override
public int expectedMinLength(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
return 8;
return 4;
}
@Override
public int expectedMaxLength(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
return 0xfff + 1; // limit packet size to 4096
return 0xFFF; // strict size limit
}
@Override
public void decode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
channel = readString(byteBuf);
channel = readString(byteBuf, 512);
if (protocolVersion.compareTo(MINECRAFT_1_13) >= 0) {
channel = transformLegacyToModernChannel(channel);
@@ -37,7 +37,7 @@ import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.writeVarInt;
@ToString
@NoArgsConstructor
@AllArgsConstructor
public final class SetSlot implements FallbackPacket {
public final class SetSlotPacket implements FallbackPacket {
private int windowId;
private int slot;
private int count;
@@ -31,7 +31,7 @@ import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.readVarInt;
@ToString
@NoArgsConstructor
@AllArgsConstructor
public final class TeleportConfirm implements FallbackPacket {
public final class TeleportConfirmPacket implements FallbackPacket {
private int teleportId;
@Override
@@ -32,7 +32,7 @@ import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT
@ToString
@NoArgsConstructor
@AllArgsConstructor
public final class Transaction implements FallbackPacket {
public final class TransactionPacket implements FallbackPacket {
private int windowId, id;
private boolean accepted;
@@ -17,24 +17,21 @@
package xyz.jonesdev.sonar.common.fallback.protocol.packets.play;
import com.google.gson.JsonParser;
import io.netty.buffer.ByteBuf;
import io.netty.handler.codec.CorruptedFrameException;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import net.kyori.adventure.nbt.BinaryTag;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.json.JSONComponentSerializer;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
import xyz.jonesdev.sonar.common.utility.component.ComponentHolder;
import java.time.Instant;
import java.util.UUID;
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
import static xyz.jonesdev.sonar.common.utility.component.ComponentSerializer.serialize;
import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.*;
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.readVarInt;
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.writeVarInt;
@@ -42,7 +39,7 @@ import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.writeVarInt;
@Data
@NoArgsConstructor
@AllArgsConstructor
public final class Chat implements FallbackPacket {
public final class UniversalChatPacket implements FallbackPacket {
private static final UUID PLACEHOLDER_UUID = new UUID(0L, 0L);
private static final int DIV_FLOOR = -Math.floorDiv(-20, 8);
@@ -50,7 +47,7 @@ public final class Chat implements FallbackPacket {
public static final byte SYSTEM_TYPE = (byte) 1;
public static final byte GAME_INFO_TYPE = (byte) 2;
private Component component;
private ComponentHolder componentHolder;
private String message;
private byte type;
private boolean signedPreview;
@@ -61,33 +58,20 @@ public final class Chat implements FallbackPacket {
private boolean signed;
private byte[] signature;
// 1.20.3
private BinaryTag nbtMessage;
// Clientbound LegacyChat
public Chat(final Component component) {
this(component, SYSTEM_TYPE);
public UniversalChatPacket(final @NotNull Component component,
final byte type) {
this(new ComponentHolder(component), type);
}
// Clientbound LegacyChat
public Chat(final Component component, final byte type) {
this(component, JSONComponentSerializer.json().serialize(component), type);
}
public Chat(final Component component, final String message, final byte type) {
this(component, message, type,
false, false, null, null,
0L, false, null, serialize(new JsonParser().parse(message)));
public UniversalChatPacket(final ComponentHolder componentHolder, final byte type) {
this.componentHolder = componentHolder;
this.type = type;
}
@Override
public void encode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
// Serialized message
if (protocolVersion.compareTo(MINECRAFT_1_20_3) >= 0) {
writeNamelessCompoundTag(byteBuf, nbtMessage);
} else {
writeString(byteBuf, message);
}
componentHolder.write(byteBuf, protocolVersion);
// Type
if (protocolVersion.compareTo(MINECRAFT_1_19_1) >= 0) {
@@ -35,7 +35,7 @@ import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.writeVarLong
@NoArgsConstructor
@AllArgsConstructor
@ToString(exclude = {"changedBlocks"})
public final class UpdateSectionBlocks implements FallbackPacket {
public final class UpdateSectionBlocksPacket implements FallbackPacket {
private int sectionX, sectionZ;
private ChangedBlock[] changedBlocks;
@@ -15,17 +15,17 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.api.statistics;
package xyz.jonesdev.sonar.common.statistics;
import lombok.Getter;
import lombok.Setter;
import xyz.jonesdev.sonar.api.profiler.JVMProfiler;
import xyz.jonesdev.sonar.api.jvm.JVMProcessInformation;
/**
* This counts all incoming and outgoing traffic.
*/
@Getter
public enum Bandwidth implements JVMProfiler {
public enum CachedBandwidthStatistics {
INCOMING,
OUTGOING;
@@ -48,15 +48,15 @@ public enum Bandwidth implements JVMProfiler {
* This method is called every second.
*/
public static synchronized void reset() {
for (final Bandwidth value : values()) {
for (final CachedBandwidthStatistics value : values()) {
value.cacheAndReset();
}
}
private void cacheAndReset() {
ttl += curr; // increment total
cachedSecond = formatMemory(curr);
cachedTtl = formatMemory(ttl);
cachedSecond = JVMProcessInformation.formatMemory(curr);
cachedTtl = JVMProcessInformation.formatMemory(ttl);
curr = 0L; // reset current
}
}
@@ -0,0 +1,162 @@
/*
* Copyright (C) 2023-2024 Sonar Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package xyz.jonesdev.sonar.common.statistics;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.jetbrains.annotations.ApiStatus;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.statistics.SonarStatistics;
import java.time.Duration;
import java.util.concurrent.atomic.AtomicInteger;
public final class GlobalSonarStatistics implements SonarStatistics {
private static final Cache<Integer, Byte> LOGINS_PER_SECOND = Caffeine.newBuilder()
.expireAfterWrite(Duration.ofSeconds(1))
.build();
private static final Cache<Integer, Byte> CONNECTIONS_PER_SECOND = Caffeine.newBuilder()
.expireAfterWrite(Duration.ofSeconds(1))
.build();
private static final AtomicInteger ACTION_COUNTER = new AtomicInteger(Integer.MIN_VALUE);
/**
* Helper methods that make it easier to count new statistics
*/
@ApiStatus.Internal
public static void countConnection() {
CONNECTIONS_PER_SECOND.put(ACTION_COUNTER.getAndIncrement(), (byte) 0);
}
@ApiStatus.Internal
public static void countLogin() {
LOGINS_PER_SECOND.put(ACTION_COUNTER.getAndIncrement(), (byte) 0);
totalJoinedPlayers++;
}
// Cache all per-session statistics
private static int totalJoinedPlayers;
public static int totalSuccessfulVerifications;
public static int totalFailedVerifications;
public static int totalAttemptedVerifications;
public static long totalBlacklistedPlayers;
/**
* Called every time the action bar updates to ensure fresh values
*/
@Override
public synchronized void cleanUpCache() {
LOGINS_PER_SECOND.cleanUp();
CONNECTIONS_PER_SECOND.cleanUp();
// Also clean up the blacklist
Sonar.get().getFallback().getBlacklist().cleanUp();
}
@Override
public long getConnectionsPerSecond() {
return CONNECTIONS_PER_SECOND.estimatedSize();
}
@Override
public long getLoginsPerSecond() {
return LOGINS_PER_SECOND.estimatedSize();
}
@Override
public long getCurrentIncomingBandwidth() {
return CachedBandwidthStatistics.INCOMING.getCurr();
}
@Override
public long getCurrentOutgoingBandwidth() {
return CachedBandwidthStatistics.OUTGOING.getCurr();
}
@Override
public long getTotalIncomingBandwidth() {
return CachedBandwidthStatistics.INCOMING.getTtl();
}
@Override
public long getTotalOutgoingBandwidth() {
return CachedBandwidthStatistics.OUTGOING.getTtl();
}
@Override
public String getCurrentIncomingBandwidthFormatted() {
return CachedBandwidthStatistics.INCOMING.getCachedSecond();
}
@Override
public String getCurrentOutgoingBandwidthFormatted() {
return CachedBandwidthStatistics.OUTGOING.getCachedSecond();
}
@Override
public String getTotalIncomingBandwidthFormatted() {
return CachedBandwidthStatistics.INCOMING.getCachedTtl();
}
@Override
public String getTotalOutgoingBandwidthFormatted() {
return CachedBandwidthStatistics.OUTGOING.getCachedTtl();
}
@Override
public int getTotalPlayersJoined() {
return totalJoinedPlayers;
}
@Override
public int getTotalPlayersVerified() {
return Sonar.get().getVerifiedPlayerController().estimatedSize();
}
@Override
public int getTotalSuccessfulVerifications() {
return totalSuccessfulVerifications;
}
@Override
public int getTotalFailedVerifications() {
return totalFailedVerifications;
}
@Override
public long getCurrentAttemptedVerifications() {
return Sonar.get().getFallback().getConnected().size();
}
@Override
public int getTotalAttemptedVerifications() {
return totalAttemptedVerifications;
}
@Override
public long getCurrentBlacklistSize() {
return Sonar.get().getFallback().getBlacklist().estimatedSize();
}
@Override
public long getTotalBlacklistSize() {
return totalBlacklistedPlayers;
}
}
@@ -17,21 +17,25 @@
package xyz.jonesdev.sonar.common.subcommand.impl;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import lombok.RequiredArgsConstructor;
import lombok.val;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Unmodifiable;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.SonarPlatform;
import xyz.jonesdev.sonar.api.command.CommandInvocation;
import xyz.jonesdev.sonar.api.command.subcommand.Subcommand;
import xyz.jonesdev.sonar.api.command.subcommand.SubcommandInfo;
import xyz.jonesdev.sonar.api.profiler.JVMProfiler;
import java.lang.management.ManagementFactory;
import java.lang.management.RuntimeMXBean;
import java.util.List;
import java.util.Map;
import java.util.WeakHashMap;
import static xyz.jonesdev.sonar.api.jvm.JVMProcessInformation.*;
// This command is only used to display helpful information to help
// the Sonar contributors to fix issues more quickly
@SubcommandInfo(
@@ -39,11 +43,14 @@ import java.util.WeakHashMap;
description = "Print developer information",
onlyConsole = true
)
public final class DumpCommand extends Subcommand implements JVMProfiler {
public final class DumpCommand extends Subcommand {
private static final RuntimeMXBean RUNTIME_MX_BEAN = ManagementFactory.getRuntimeMXBean();
private static final Gson GSON = new GsonBuilder().create();
@Override
protected void execute(final @NotNull CommandInvocation invocation) {
final String json = Sonar.GENERAL_GSON.toJson(collectMappedInformation());
final String json = GSON.toJson(collectMappedInformation());
SONAR.getLogger().info("Generated dump: {}", json);
}
@@ -31,13 +31,13 @@ public final class NotifyCommand extends Subcommand {
@Override
protected void execute(final @NotNull CommandInvocation invocation) {
if (SONAR.getNotificationHandler().isSubscribed(invocation.getSender().getName())) {
SONAR.getNotificationHandler().unsubscribe(invocation.getSender().getName());
if (SONAR.getNotificationHandler().isSubscribed(invocation.getSender().getUuid())) {
SONAR.getNotificationHandler().unsubscribe(invocation.getSender().getUuid());
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getNotificationsUnsubscribed());
return;
}
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getNotificationsSubscribed());
SONAR.getNotificationHandler().subscribe(invocation.getSender().getName());
SONAR.getNotificationHandler().subscribe(invocation.getSender().getUuid());
}
}
@@ -23,11 +23,10 @@ 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.command.subcommand.argument.Argument;
import xyz.jonesdev.sonar.api.profiler.JVMProfiler;
import xyz.jonesdev.sonar.api.statistics.Bandwidth;
import xyz.jonesdev.sonar.api.statistics.Statistics;
import xyz.jonesdev.sonar.common.statistics.CachedBandwidthStatistics;
import static xyz.jonesdev.sonar.api.Sonar.DECIMAL_FORMAT;
import static xyz.jonesdev.sonar.api.jvm.JVMProcessInformation.*;
@SubcommandInfo(
name = "statistics",
@@ -40,7 +39,7 @@ import static xyz.jonesdev.sonar.api.Sonar.DECIMAL_FORMAT;
},
argumentsRequired = false
)
public final class StatisticsCommand extends Subcommand implements JVMProfiler {
public final class StatisticsCommand extends Subcommand {
private enum StatisticType {
GENERAL,
NETWORK,
@@ -77,9 +76,9 @@ public final class StatisticsCommand extends Subcommand implements JVMProfiler {
.replace("%blacklisted%", DECIMAL_FORMAT.format(SONAR.getFallback().getBlacklist().estimatedSize()))
.replace("%queued%", DECIMAL_FORMAT.format(SONAR.getFallback().getQueue().getQueuedPlayers().size()))
.replace("%uptime%", String.format("%dd %dh %dm %ds", days, hours, minutes, seconds % 60L))
.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())));
.replace("%total_joins%", DECIMAL_FORMAT.format(Sonar.get().getStatistics().getTotalPlayersJoined()))
.replace("%total_attempts%", DECIMAL_FORMAT.format(Sonar.get().getStatistics().getTotalAttemptedVerifications()))
.replace("%total_failed%", DECIMAL_FORMAT.format(Sonar.get().getStatistics().getTotalFailedVerifications())));
break;
}
@@ -105,10 +104,10 @@ public final class StatisticsCommand extends Subcommand implements JVMProfiler {
case NETWORK: {
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getNetworkStatistics()
.replace("%incoming%", Bandwidth.INCOMING.getCachedSecond())
.replace("%outgoing%", Bandwidth.OUTGOING.getCachedSecond())
.replace("%ttl_incoming%", Bandwidth.INCOMING.getCachedTtl())
.replace("%ttl_outgoing%", Bandwidth.OUTGOING.getCachedTtl()));
.replace("%incoming%", CachedBandwidthStatistics.INCOMING.getCachedSecond())
.replace("%outgoing%", CachedBandwidthStatistics.OUTGOING.getCachedSecond())
.replace("%ttl_incoming%", CachedBandwidthStatistics.INCOMING.getCachedTtl())
.replace("%ttl_outgoing%", CachedBandwidthStatistics.OUTGOING.getCachedTtl()));
break;
}
}
@@ -32,8 +32,8 @@ public final class VerboseCommand extends Subcommand {
@Override
protected void execute(final @NotNull CommandInvocation invocation) {
if (SONAR.getVerboseHandler().isSubscribed(invocation.getSender().getName())) {
SONAR.getVerboseHandler().unsubscribe(invocation.getSender().getName());
if (SONAR.getVerboseHandler().isSubscribed(invocation.getSender().getUuid())) {
SONAR.getVerboseHandler().unsubscribe(invocation.getSender().getUuid());
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getVerboseUnsubscribed());
// Reset ActionBar component when unsubscribing
invocation.getSender().getAudience().sendActionBar(Component.empty());
@@ -41,6 +41,6 @@ public final class VerboseCommand extends Subcommand {
}
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getVerboseSubscribed());
SONAR.getVerboseHandler().subscribe(invocation.getSender().getName());
SONAR.getVerboseHandler().subscribe(invocation.getSender().getUuid());
}
}
@@ -17,23 +17,34 @@
package xyz.jonesdev.sonar.common.utility.component;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
import com.google.gson.*;
import com.google.gson.internal.LazilyParsedNumber;
import lombok.experimental.UtilityClass;
import io.netty.buffer.ByteBuf;
import net.kyori.adventure.nbt.*;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
// Taken from
import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.writeNamelessCompoundTag;
import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.writeString;
// Mostly taken from
// https://github.com/PaperMC/Velocity/blob/dev/3.0.0/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/ComponentHolder.java
@UtilityClass
public class ComponentSerializer {
public static BinaryTag serialize(final JsonElement json) {
public final class ComponentHolder {
private final String serializedComponent;
private BinaryTag cachedBinaryTag;
public ComponentHolder(final Component component) {
this.serializedComponent = GsonComponentSerializer.gson().serialize(component);
}
private BinaryTag serialize(final JsonElement json) {
if (json instanceof JsonPrimitive) {
final JsonPrimitive jsonPrimitive = (JsonPrimitive) json;
@@ -65,13 +76,13 @@ public class ComponentSerializer {
} else if (json instanceof JsonObject) {
final CompoundBinaryTag.Builder compound = CompoundBinaryTag.builder();
for (Map.Entry<String, JsonElement> property : ((JsonObject) json).entrySet()) {
for (final Map.Entry<String, JsonElement> property : ((JsonObject) json).entrySet()) {
compound.put(property.getKey(), serialize(property.getValue()));
}
return compound.build();
} else if (json instanceof JsonArray) {
final JsonArray jsonArray = (JsonArray) json;
final JsonArray jsonArray = json.getAsJsonArray();
if (jsonArray.size() == 0) {
return ListBinaryTag.empty();
@@ -91,7 +102,7 @@ public class ComponentSerializer {
}
}
switch (listType.id()) {
switch (Objects.requireNonNull(listType).id()) {
case 1://BinaryTagTypes.BYTE:
final byte[] bytes = new byte[jsonArray.size()];
for (int i = 0; i < bytes.length; i++) {
@@ -123,8 +134,22 @@ public class ComponentSerializer {
});
break;
}
return ListBinaryTag.listBinaryTag(listType, tagItems);
}
return EndBinaryTag.endBinaryTag();
}
public void write(final @NotNull ByteBuf byteBuf,
final @NotNull ProtocolVersion protocolVersion) {
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_20_3) >= 0) {
if (cachedBinaryTag == null) {
cachedBinaryTag = serialize(new JsonParser().parse(serializedComponent));
}
writeNamelessCompoundTag(byteBuf, cachedBinaryTag);
} else {
writeString(byteBuf, serializedComponent);
}
}
}
@@ -39,6 +39,6 @@ public class GeyserUtil {
public boolean isGeyserConnection(final @NotNull Channel channel,
final @NotNull InetSocketAddress originalAddress) {
return originalAddress.getPort() == 0 // check for floodgate on the server
|| channel.attr(PLAYER_ATTRIBUTE).get() != null; // check for standalone Geyser proxy connection
|| channel.attr(PLAYER_ATTRIBUTE).get() != null; // check for standalone Geyser proxy connection
}
}
@@ -40,7 +40,7 @@ attack-tracker:
# Nicméně, IP adresy nemůžou být vystopovány zpátky k hráči, protože Sonar používá UUID na ukládní místo jmeno hráčů
database:
# Typy datábázi se kterýma Sonar bude pracovat
# Typy: NONE, MYSQL (Žádný, MYSQL)
# Typy: NONE, MYSQL, MARIADB
type: NONE
# Host adresa pro verifikování hráčů s SQL databází
host: localhost
@@ -78,6 +78,8 @@ verification:
enabled: true
# Měl by Sonar zkoušet jestli se hráč dotýka bloku? (Doporučováno)
check-collisions: true
# Měl by Sonar přimět hráče, aby vyřešil CAPTCHA, pokud neuspějí při kontrole gravitace?
captcha-on-fail: false
# Maximální počet ticků, který má hráč padat aby byl puštěn na verifikační platformu
max-movement-ticks: 8
# Maximum ignorovaných změn Y-nové souřadnice hráče, aby neprošel verifikací
@@ -0,0 +1,173 @@
#
# /$$$$$$
# /$$__ $$
# | $$ \__/ /$$$$$$ /$$$$$$$ /$$$$$$ /$$$$$$
# | $$$$$$ /$$__ $$| $$__ $$ |____ $$ /$$__ $$
# \____ $$| $$ \ $$| $$ \ $$ /$$$$$$$| $$ \__/
# /$$ \ $$| $$ | $$| $$ | $$ /$$__ $$| $$
# | $$$$$$/| $$$$$$/| $$ | $$| $$$$$$$| $$
# \______/ \______/ |__/ |__/ \_______/|__/
#
# Brauchst du Hilfe? https://jonesdev.xyz/discord
# Source code: https://github.com/jonesdevelopment/sonar
# Copyright Jones Development and Sonar Contributors, licensed under GPL-3.0
#
# Generelle Einstellungen für Sonar
general:
# Soll Sonar die IP Adressen von Spielern in der Konsole anzeigen?
log-player-addresses: true
# Maximale Anzahl an Spielern mit der selben IP Adresse
max-online-per-ip: 3
# Sensitivitätseinstellungen für Angriffstracker
# Bearbeite diese Einstellungen nur wenn du weißt was du tust!
attack-tracker:
# Minimale Anzahl an neuen Spielern damit ein Angriff als solcher erkannt wird
min-players-for-attack: 8
# Anzahl an Millisekunden bis ein Angriff vorbei ist
min-attack-duration: 30000
# Anzahl an gemeldenten Vorfällen damit ein Angriff als solcher erkannt wird
# Diese Zahl repräsentiert einen Puffer um falsche Angriffsbenachrichtigungen zu filtern
min-attack-threshold: 2
# Anzahl an Millisekunden die vergehen muss damit eine neue Attacke erkannt wird
attack-cooldown-delay: 3000
# Du kannst Sonar mit einer Datenbank verbinden um verifizierte Spieler zu speichern
# Beachte: IP adressen werden im Klartext gespeichert. Du bist für die Sicherheit der Datenbank verantwortlich
database:
# Typ von Datenbank die Sonar verwenden soll um verifizierte Spielerdaten zu speichern
# Mögliche Typen: NONE, MYSQL, MARIADB
type: NONE
# HOST für die Authentifizierung mit der SQL Datenbank
host: localhost
# Port für die Authentifizierung mit der SQL Datenbank
port: 3306
# Name der SQL Datenbank
name: sonar
# Username für die Authentifizierung mit der SQL Datenbank
username: ''
# Password für die Authentifizierung mit der SQL Datenbank
password: ''
# Wie viele Tage soll Sonar verifizierte Spielerdaten in der Datenbank behalten?
maximum-age: 5
# Jede neue Login-Anfrage wird in eine Warteschlange eingereiht, um Spam-Beitrittsangriffe zu vermeiden
# Die Warteschlange wird alle 500 Millisekunden (10 Ticks) aktualisiert
queue:
# Maximale Anzahl an gleichzeitigen Abfragen der Warteschlange pro 500 Millisekunden
max-polls: 30
# Jeder neue Spieler, der das erste Mal beitritt, wird zu
# einem leichtgewichtigen Limbo-Server gesendet, wo fortgeschrittene Bot-Überprüfungen durchgeführt werden
verification:
# Wann sollte Sonar neue Spieler überprüfen?
# Mögliche Typen: ALWAYS, DURING_ATTACK, NEVER
# - ALWAYS: Neue Spieler werden immer überprüft (Empfohlen)
# - DURING_ATTACK: Neue Spieler werden nur während eines Angriffs überprüft
# - NEVER: Neue Spieler werden nie überprüft
timing: ALWAYS
checks:
# Überprüft, ob die Fallbewegung der Spieler der Gravitationsformel von Minecraft folgt
# Alle vorhergesagten Bewegungen werden vorausberechnet, um die Leistung zu sparen
gravity:
# Soll Sonar auf gültige Client-Gravitation prüfen? (Empfohlen)
enabled: true
# Soll Sonar prüfen, ob der Spieler mit Blöcken kollidiert? (Empfohlen)
check-collisions: true
# Soll Sonar den Spieler dazu bringen, ein CAPTCHA zu lösen, wenn er die Schwerkraftprüfung nicht besteht?
captcha-on-fail: false
# Maximale Anzahl an Ticks, die der Spieler fallen muss, um die Plattform erreichen zu dürfen
max-movement-ticks: 8
# Maximale Anzahl an ignorierten Y-Bewegungsänderungen, bevor ein Spieler die Überprüfung nicht besteht
max-ignored-ticks: 5
# Der Spielmodus des Spielers während der Überprüfung
# Mögliche Typen: SURVIVAL, CREATIVE, ADVENTURE
# - SURVIVAL: alle UI-Komponenten sind sichtbar
# - CREATIVE: Gesundheit und Hunger sind versteckt
# - ADVENTURE: alle UI-Komponenten sind sichtbar
gamemode: ADVENTURE
# Lässt den Spieler einen Code von einer virtuellen Karte im Chat eingeben
map-captcha:
# Wann sollte Sonar den Spieler ein CAPTCHA lösen lassen?
# Mögliche Typen: ALWAYS, DURING_ATTACK, NEVER
# - ALWAYS: Neue Spieler erhalten immer ein CAPTCHA
# - DURING_ATTACK: Neue Spieler erhalten nur während eines Angriffs ein CAPTCHA
# - NEVER: Neue Spieler erhalten nie ein CAPTCHA (Empfohlen)
timing: NEVER
# Soll Sonar die X- und Y-Position des CAPTCHAs zufällig wählen?
random-position: true
# Wie stark sollen Zeichen verzerrt werden (Faktor für Zufälligkeit)?
distortions-factor-x: 50
# Wie stark sollen Zeichen verzerrt werden (Faktor für Zufälligkeit)?
distortions-factor-y: 50
# Wie viele zufällige Linien hinter dem CAPTCHA soll Sonar zeichnen?
random-lines: 4
# Wie viele zufällige Ovale hinter dem CAPTCHA soll Sonar zeichnen?
random-ovals: 1
# Soll Sonar die Schriftgröße für das Rendern des CAPTCHAs zufällig wählen?
random-font-size: true
# Wie viele Antworten soll Sonar vorbereiten?
# Dieser Vorgang erfolgt asynchron im Hintergrund;
# Spieler können beitreten, sobald ein CAPTCHA vorbereitet wurde
precompute: 1000
# Wie lange soll Sonar warten, bis der Spieler das CAPTCHA nicht besteht?
# (Dieser Wert stellt die Zeit in Millisekunden dar: 1 Sekunde = 1000 Millisekunden)
max-duration: 45000
# Wie oft muss ein Spieler das CAPTCHA nicht bestehen, bevor er die Überprüfung nicht besteht?
max-tries: 3
# Zeichen (Buchstaben und Zahlen), die im Antwort-CAPTCHA erscheinen dürfen
dictionary: '123456789'
# Welche Schriftarten soll Sonar für die Map-CAPTCHA-Codes verwenden?
fonts:
- Dialog
- DialogInput
- Serif
- SansSerif
# Überprüft, ob die Spieler eine gültige Client-Marke an den Server senden
client-brand:
# Soll Sonar auf gültige Client-Marken prüfen? (Empfohlen)
enabled: true
# Regex zur Validierung von Client-Marken während der Überprüfung
valid-regex: ^[!-~ ]+$
# Maximale Länge der Client-Marke während der Überprüfung
max-length: 64
# Regex zur Validierung von Benutzernamen während der Überprüfung
valid-name-regex: ^[a-zA-Z0-9_]+$
# Regex zur Validierung der Client-Spracheinstellung während der Überprüfung
valid-locale-regex: ^[a-zA-Z_]+$
# Ping, den ein Spieler haben muss, um ein Timeout zu erleiden
# (Dieser Wert stellt die Zeit in Millisekunden dar: 1 Sekunde = 1000 Millisekunden)
max-ping: 10000
# Maximale Anzahl an Login-Paketen, die der Spieler senden muss, um gekickt zu werden
max-login-packets: 256
# Soll Sonar neue Überprüfungsversuche protokollieren?
log-connections: true
# Soll Sonar neue Überprüfungsversuche während Angriffen protokollieren?
log-during-attack: false
# Soll Sonar jede einzelne Bewegung/Positionsänderung während der Überprüfung protokollieren?
# Dies wird für Produktivserver nicht empfohlen, kann aber beim Auffinden von Fehlern hilfreich sein.
debug-xyz-positions: false
# Zeit, die verstreichen muss, bevor ein Spieler ein Timeout erleidet
# (Dieser Wert stellt die Zeit in Millisekunden dar: 1 Sekunde = 1000 Millisekunden)
read-timeout: 3500
# Wie lange sollte ein Spieler warten, bevor er während der Überprüfung erneut verbinden kann
# (Dieser Wert stellt die Zeit in Millisekunden dar: 1 Sekunde = 1000 Millisekunden)
rejoin-delay: 8000
# Wie lange sollte eine IP-Adresse vom Einloggen ausgeschlossen werden, wenn die Überprüfung zu oft nicht bestanden wird?
# (Dieser Wert stellt die Zeit in Millisekunden dar: 1 Sekunde = 1000 Millisekunden)
blacklist-time: 600000
# Wie oft muss ein Spieler die Überprüfung nicht bestehen, um auf die Schwarze Liste gesetzt zu werden?
# (Setzen Sie diesen Wert auf 0, um die Schwarze Liste vollständig zu deaktivieren)
blacklist-threshold: 2
# Liste von Protokoll-IDs, die von Sonar nicht überprüft werden (Überprüfungsumgehung)
# Die vollständige Liste aller Protokoll-IDs finden Sie hier:
# https://wiki.vg/Protocol_version_numbers
# Zum Beispiel hat Minecraft 1.20 die ID 763.
whitelisted-protocols: []
# Liste von Protokoll-IDs, die den Server überhaupt nicht betreten dürfen
blacklisted-protocols: []
@@ -39,7 +39,7 @@ attack-tracker:
# Note: IP addresses are saved in plain text. You are responsible for keeping your database safe!
database:
# Type of database Sonar uses to store verified players
# Possible types: NONE, MYSQL
# Possible types: NONE, MYSQL, MARIADB
type: NONE
# Host address for authenticating with the SQL database
host: localhost
@@ -77,6 +77,8 @@ verification:
enabled: true
# Should Sonar check if the player collides with blocks? (Recommended)
check-collisions: true
# Should Sonar make the player solve a CAPTCHA if they fail the gravity check?
captcha-on-fail: false
# Maximum number of ticks the player has to fall in order to be allowed to hit the platform
max-movement-ticks: 8
# Maximum number of ignored Y movement changes before a player fails verification
@@ -39,7 +39,7 @@ attack-tracker:
# შენიშვნა: IP მისამართები ინახება უბრალო ტექსტში. თქვენ ხართ პასუხისმგებელი თქვენი მონაცემთა ბაზის უსაფრთხოებაზე!
database:
# მონაცემთა ბაზის ტიპები, რომლებიც Sonar-ს შეუძლია გამოიყენოს ვერიფირებული მოთამაშეების შესანახად
# შესაძლო ტიპები: NONE, MYSQL
# შესაძლო ტიპები: NONE, MYSQL, MARIADB
type: NONE
# ჰოსტის მისამართი SQL მონაცემთა ბაზის ავთენტიფიკაციისთვის
host: localhost
@@ -77,6 +77,8 @@ verification:
enabled: true
# უნდა შეამოწმოს თუ არა Sonar-მა, ეტაკება თუ არა მოთამაშე ბლოკებს?
check-collisions: true
# უნდა აიძულოს თუ არა Sonar-მა მოთამაშეს გადაჭრას CAPTCHA, თუ ისინი ვერ შეასრულებენ სიმძიმის შემოწმებას?
captcha-on-fail: false
# ტიკების მაქსიმალური რაოდენობა რომელითაც მოთამაშე უნდა დაეცეს, რათა პლატფორმაზე დაცემა შეეძლოს
max-movement-ticks: 8
# იგნორირებული Y მოძრაობის მაქსიმალური რაოდენობა სანამ, მოთამაშე ვერიფიკაციას ვერ გაივლის
@@ -39,7 +39,7 @@ attack-tracker:
# Примечание: IP-адреса сохраняются в виде обычного текста. Вы сами отвечаете за сохранность своей базы данных!
database:
# Тип базы данных, которую Sonar использует для хранения проверенных игроков.
# Возможные типы: NONE, MYSQL
# Возможные типы: NONE, MYSQL, MARIADB
type: NONE
# Адрес хоста для аутентификации в базе данных SQL
host: localhost
@@ -77,6 +77,8 @@ verification:
enabled: true
# Должен ли Sonar проверять, сталкивается ли игрок с блоками? (Рекомендуется)
check-collisions: true
# Должен ли Sonar проверять, игрока решать CAPTCHA, если он не прошел проверку гравитации?
captcha-on-fail: false
# Максимальное количество тиков, на которое должен упасть игрок, чтобы ему было разрешено упасть на платформу
max-movement-ticks: 8
# Максимальное количество проигнорированных изменений движения Y, прежде чем игрок не пройдет проверку
@@ -0,0 +1,308 @@
#
# /$$$$$$
# /$$__ $$
# | $$ \__/ /$$$$$$ /$$$$$$$ /$$$$$$ /$$$$$$
# | $$$$$$ /$$__ $$| $$__ $$ |____ $$ /$$__ $$
# \____ $$| $$ \ $$| $$ \ $$ /$$$$$$$| $$ \__/
# /$$ \ $$| $$ | $$| $$ | $$ /$$__ $$| $$
# | $$$$$$/| $$$$$$/| $$ | $$| $$$$$$$| $$
# \______/ \______/ |__/ |__/ \_______/|__/
#
# Brauchst du Hilfe? https://jonesdev.xyz/discord
# Source code: https://github.com/jonesdevelopment/sonar
# Copyright Jones Development and Sonar Contributors, licensed under GPL-3.0
#
# Platzhalter für jedes '%prefix%' in dieser Konfigurationsdatei
prefix: <b><yellow>Sonar</b> <gray>» <white>
# Platzhalter für jede '%support-url%' in dieser Konfigurationsdatei
support-url: https://jonesdev.xyz/discord/
# Platzhalter für jedes '%header%' in dieser Konfigurationsdatei
header:
- <b><yellow>Sonar</b><reset>
- ''
# Platzhalter für jedes '%footer%' in dieser Konfigurationsdatei
footer:
- <gray>Falls du glaubst, dass dies ein Fehler ist, kontaktiere einen Administrator.
# Nachricht, die angezeigt wird, wenn jemand beitritt, aber zu viele Online-Spieler mit ihrer IP-Adresse vorhanden sind
too-many-online-per-ip:
- '%header%'
- <red>Es sind zu viele Spieler mit deiner IP-Adresse online.
- '%footer%'
# Übersetzungen für Befehle
commands:
# Nachricht, die angezeigt wird, wenn ein Spieler versucht, /sonar ohne Berechtigung auszuführen
no-permission: '%prefix%<red>Du hast keine Berechtigung, diesen Befehl auszuführen.'
# Nachricht, die angezeigt wird, wenn jemand einen Befehl falsch verwendet
incorrect-usage: '%prefix%<red>Benutzung: /sonar %usage%'
# Nachricht, die angezeigt wird, wenn jemand eine ungültige IP-Adresse angibt (Ungültiges Format)
invalid-ip-address: '%prefix%Die angegebene IP-Adresse scheint ungültig zu sein.'
# Nachricht, die angezeigt wird, wenn jemand eine illegale IP-Adresse angibt (Lokale IP)
illegal-ip-address: '%prefix%Die angegebene IP-Adresse scheint entweder eine lokale oder eine Schleifenrückführungs-IP zu sein.'
# Nachricht, die angezeigt wird, wenn die Konsole einen Befehl ausführt, der nur für Spieler ist
player-only: '%prefix%<red>Diesen Befehl kannst du nur als Spieler ausführen.'
# Nachricht, die angezeigt wird, wenn ein Spieler einen Befehl ausführt, der nur für die Konsole ist
console-only: '%prefix%<red>Aus Sicherheitsgründen kannst du diesen Befehl nur über die Konsole ausführen.'
# Nachricht, die angezeigt wird, wenn ein Spieler Sonar-Befehle zu schnell ausführt
cool-down: '%prefix%<red>Du kannst diesen Befehl nur alle 0,5 Sekunden ausführen.'
cool-down-left: '%prefix%<red>Bitte warte noch <bold>%time-left%s<reset><red>.'
# Nachricht, die angezeigt wird, wenn ein Spieler keine Berechtigung hat, einen bestimmten Unterbefehl auszuführen
subcommand-no-permission: '%prefix%<red>Du hast keine Berechtigung, diesen Unterbefehl auszuführen. <gray>(%permission%)'
# Übersetzungen für '/sonar'
main:
# Informationsnachricht, die oben angezeigt wird, wenn der Hauptbefehl ausgeführt wird
header:
- <yellow>Ausführung von <b>Sonar</b> %version% auf %platform%.
- <gray><click:open_url:'https://github.com/jonesdevelopment/sonar'>(C) %copyright_year%
Jones Development und Sonar Mitwirkende</click>
- ''
- <gray>Brauchst du Hilfe oder hast du Fragen?
- <white><click:open_url:'https://jonesdev.xyz/discord/'><hover:show_text:'(Klicken
um Discord zu öffnen)'>Öffne ein Ticket auf Discord </hover></click><click:open_url:'https://github.com/jonesdevelopment/sonar/issues'><hover:show_text:'(Klicken
um GitHub zu öffnen)'>oder erstelle ein neues Issue auf GitHub.
- ''
# Formatierung der Liste der Unterbefehle, die angezeigt wird, wenn der Hauptbefehl ausgeführt wird
subcommands: '<click:suggest_command:''/sonar %subcommand% ''><hover:show_text:''<gray>Nur
Spieler: </gray>%only_players%<br><gray>Erfordert Konsole: </gray>%require_console%<br><gray>Berechtigung:
</gray><white>%permission%<br><gray>Aliase: </gray>%aliases%''><dark_aqua>
▪ <gray>/sonar %subcommand% <white>%description%'
# Übersetzungen für '/sonar reload'
reload:
# Nachricht, die angezeigt wird, wenn jemand mit dem Neuladen von Sonar beginnt
start: '%prefix%Sonar wird neu geladen...'
# Nachricht, die angezeigt wird, wenn Sonar erfolgreich neu geladen wurde
finish: '%prefix%<green>Erfolgreich neu geladen <gray>(%taken%ms)'
# Übersetzungen für '/sonar verbose'
verbose:
# Nachricht, die angezeigt wird, wenn ein Spieler Sonar Verbose-Informationen abonniert
subscribed: '%prefix%Du siehst jetzt Sonar Verbose-Informationen.'
# Nachricht, die angezeigt wird, wenn ein Spieler Sonar Verbose-Informationen abbestellt
unsubscribed: '%prefix%Du siehst keine Sonar Verbose-Informationen mehr.'
# Übersetzungen für '/sonar notify'
notify:
# Nachricht, die angezeigt wird, wenn ein Spieler Sonar Angriffsbenachrichtigungen abonniert
subscribed: '%prefix%Du siehst jetzt Sonar Angriffsbenachrichtigungen.'
# Nachricht, die angezeigt wird, wenn ein Spieler Sonar Angriffsbenachrichtigungen abbestellt
unsubscribed: '%prefix%Du siehst keine Sonar Angriffsbenachrichtigungen mehr.'
# Übersetzungen für '/sonar blacklist'
blacklist:
# Nachricht, die angezeigt wird, wenn jemand versucht, die Blacklist zu leeren, aber sie leer ist
empty: '%prefix%Die Blacklist ist derzeit leer. Daher wurden keine IP-Adressen von der Blacklist entfernt.'
# Nachricht, die angezeigt wird, wenn jemand die Blacklist leert
cleared: '%prefix%Du hast erfolgreich insgesamt %removed% IP-Adresse(n) von der Blacklist entfernt.'
# Nachricht, die angezeigt wird, wenn jemand die Größe der Blacklist überprüft
size: '%prefix%Die Blacklist enthält derzeit %amount% IP-Adresse(n).'
# Nachricht, die angezeigt wird, wenn jemand eine IP-Adresse zur Blacklist hinzufügt
added: '%prefix%Erfolgreich %ip% zur Blacklist hinzugefügt.'
# Nachricht, die angezeigt wird, wenn jemand eine IP-Adresse zur Blacklist hinzufügt, die verifiziert ist
added-warning: '%prefix%<red>Warnung: <white>%ip% ist derzeit auf der Whitelist. Erwäge,
die IP-Adresse aus der Liste der verifizierten Spieler zu entfernen, um potenzielle
Probleme zu vermeiden.'
# Nachricht, die angezeigt wird, wenn jemand eine IP-Adresse von der Blacklist entfernt
removed: '%prefix%Erfolgreich %ip% von der Blacklist entfernt.'
# Nachricht, die angezeigt wird, wenn jemand eine IP-Adresse zur Blacklist hinzufügt, die bereits auf der Blacklist steht
duplicate-ip: '%prefix%Die angegebene IP-Adresse ist bereits auf der Blacklist.'
# Nachricht, die angezeigt wird, wenn jemand eine IP-Adresse von der Blacklist entfernt, die nicht auf der Blacklist steht
ip-not-found: '%prefix%Die angegebene IP-Adresse ist nicht auf der Blacklist.'
# Übersetzungen für '/sonar verified'
verified:
# Nachricht, die angezeigt wird, wenn jemand versucht, die Liste der verifizierten Spieler zu leeren, aber sie leer ist
empty: '%prefix%Die Liste der verifizierten Spieler ist derzeit leer. Daher wurden keine
Spieler unverifiziert.'
# Nachricht, die angezeigt wird, wenn jemand die Liste der verifizierten Spieler leert
cleared: '%prefix%Du hast erfolgreich insgesamt %removed% einzigartige Spieler unverifiziert.'
# Nachricht, die angezeigt wird, wenn jemand die Größe der Liste der verifizierten Spieler überprüft
size: '%prefix%Es sind derzeit %amount% einzigartige Spieler verifiziert.'
# Nachricht, die angezeigt wird, wenn jemand eine IP-Adresse unverifiziert macht
removed: '%prefix%Erfolgreich %ip% unverifiziert.'
# Nachricht, die angezeigt wird, wenn jemand eine IP-Adresse unverifiziert macht, die nicht verifiziert ist
ip-not-found: '%prefix%Die angegebene IP-Adresse ist nicht verifiziert.'
# Nachricht, die angezeigt wird, wenn jemand versucht, die gleiche IP-Adresse zweimal zu unverifizieren (doppelte Operation)
blocked: '%prefix%Bitte warte, bis der aktuelle Vorgang abgeschlossen ist.'
# Übersetzungen für '/sonar statistics'
statistics:
# Informationsnachricht, die oben angezeigt wird, wenn die Statistiken angesehen werden
header: '%prefix%Anzeige der %type%-Statistiken für diese Sitzung:'
# Nachricht, die angezeigt wird, wenn ein Spieler versucht, eine unbekannte Statistik anzusehen
unknown-type: '%prefix%<red>Unbekannter Statistiktyp! Verfügbare Statistiken: <gray>%statistics%'
# Format der allgemeinen Statistiknachricht
general:
- ' <dark_aqua>▪ <gray>Verifizierte IP-Adressen: <white>%verified%'
- ' <dark_aqua>▪ <gray>IP-Adressen in Überprüfung: <white>%verifying%'
- ' <dark_aqua>▪ <gray>Auf der Blacklist stehende IP-Adressen: <white>%blacklisted%'
- ' <dark_aqua>▪ <gray>Aktuell in der Warteschlange befindliche Anmeldungen: <white>%queued%'
- ' <dark_aqua>▪ <gray>Gesamte nicht-einzigartige Beitritte: <white>%total_joins%'
- ' <dark_aqua>▪ <gray>Gesamte Überprüfungsversuche: <white>%total_attempts%'
- ' <dark_aqua>▪ <gray>Gesamte fehlgeschlagene Überprüfungen: <white>%total_failed%'
- ' <dark_aqua>▪ <gray>Server-Laufzeit: <white>%uptime%'
# Format der CPU-Statistiknachricht
cpu:
- ' <dark_aqua>▪ <gray>Aktuelle CPU-Nutzung des Prozesses: <white>%process_cpu%%'
- ' <dark_aqua>▪ <gray>Aktuelle CPU-Nutzung des Systems: <white>%system_cpu%%'
- ' <dark_aqua>▪ <gray>Durchschnittliche CPU-Nutzung des Prozesses pro Kern: <white>%average_process_cpu%%'
- ' <dark_aqua>▪ <gray>Durchschnittliche CPU-Nutzung des Systems pro Kern: <white>%average_system_cpu%%'
- ' <dark_aqua>▪ <gray>Allgemeine Systemlast im Durchschnitt: <white>%load_average%%'
- ' <dark_aqua>▪ <gray>Gesamtanzahl virtueller CPUs: <white>%virtual_cores%'
# Format der Speicherstatistiknachricht
memory:
- ' <dark_aqua>▪ <gray>Gesamter freier Speicher: <white>%free_memory%'
- ' <dark_aqua>▪ <gray>Gesamter genutzter Speicher: <white>%used_memory%'
- ' <dark_aqua>▪ <gray>Gesamter maximaler Speicher: <white>%max_memory%'
- ' <dark_aqua>▪ <gray>Gesamter zugewiesener Speicher: <white>%total_memory%'
# Format der Netzwerkstatistiknachricht
network:
- ' <dark_aqua>▪ <gray>Aktuell genutzte eingehende Bandbreite: <white>%incoming%'
- ' <dark_aqua>▪ <gray>Aktuell genutzte ausgehende Bandbreite: <white>%outgoing%'
- ' <dark_aqua>▪ <gray>Gesamte genutzte eingehende Bandbreite: <white>%ttl_incoming%'
- ' <dark_aqua>▪ <gray>Gesamte genutzte ausgehende Bandbreite: <white>%ttl_outgoing%'
# Übersetzungen für alle Nachrichten während des Übergabeprozesses
verification:
# Übersetzungen für alle Debug-Nachrichten während der Überprüfung
logs:
# Nachricht, die im Protokoll angezeigt wird, wenn ein neuer Spieler den Server betretet
connection: '%name%%ip% (%protocol%) hat sich verbunden.'
# Nachricht, die im Protokoll angezeigt wird, wenn ein Spieler die Verifizierung nicht besteht
failed: '%ip% (%protocol%) hat den Bot-Check nicht bestanden wegen: %reason%'
# Nachricht, die im Protokoll angezeigt wird, wenn ein Spieler aufgrund zu vieler fehlgeschlagener Versuche auf die Blacklist gesetzt wird
blacklisted: '%ip% (%protocol%) wurde aufgrund zu vieler fehlgeschlagener Versuche auf die Blacklist gesetzt'
# Nachricht, die im Protokoll angezeigt wird, wenn ein Spieler erfolgreich verifiziert wurde
successful: '%name% wurde erfolgreich verifiziert (%time%s!).'
# Nachrichten, die dem Spieler angezeigt werden, wenn er den CAPTCHA eingeben soll
captcha:
# Nachricht, die dem Spieler angezeigt wird, wenn er den CAPTCHA-Code im Chat eingeben muss
enter-code: '%prefix%<green>Bitte gib den Code im Chat ein, der auf der Karte angezeigt wird.'
# Timer, der dem Spieler angezeigt wird, wenn er den CAPTCHA-Code im Chat eingeben muss
# (Setze dies auf '' um die Aktionsleisten-Nachricht zu deaktivieren)
action-bar: '%prefix%<green>Du hast %time-left% Sekunden Zeit, um den Code im Chat einzugeben'
# Nachricht, die dem Spieler angezeigt wird, wenn er den falschen Code im Chat eingibt
incorrect: '%prefix%<red>Du hast den falschen Code eingegeben. Bitte versuche es erneut.'
# Trennungs-Nachricht, die angezeigt wird, wenn jemand beitritt, während der CAPTCHA noch nicht vorbereitet wurde
currently-preparing:
- '%header%'
- <yellow>Deine Anti-Bot-Daten wurden noch nicht vorbereitet.
- <gray>Bitte warte einige Sekunden bevor du es erneut versuchst.
- '%footer%'
# Trennungs-Nachricht, die angezeigt wird, wenn jemand zu schnell während der Verifizierung erneut beitritt
too-fast-reconnect:
- '%header%'
- <gold>Du hast zu schnell erneut eine Verbindung hergestellt, versuche es später noch einmal.
- <gray>Bitte warte einige Sekunden bevor du es erneut versuchst.
- '%footer%'
# Trennungs-Nachricht, die angezeigt wird, wenn jemand beitritt, aber bereits verifiziert wird
already-verifying:
- '%header%'
- <red>Deine IP-Adresse wird derzeit verifiziert.
- <red>Bitte warte einige Sekunden bevor du es erneut versuchst.
- '%footer%'
# Trennungs-Nachricht, die angezeigt wird, wenn jemand beitritt, aber bereits in der Warteschlange für die Verifizierung ist
already-queued:
- '%header%'
- <red>Deine IP-Adresse befindet sich derzeit in der Warteschlange für die Verifizierung.
- <red>Bitte warte einige Minuten, bevor du es erneut versuchst.
- '%footer%'
# Trennungs-Nachricht, die angezeigt wird, wenn jemand beitritt, aber vorübergehend auf der Blacklist steht
blacklisted:
- '%header%'
- <red>Du bist derzeit vom Betreten des Servers ausgeschlossen.
- <red>Bitte warte einige Minuten, um den Server wieder betreten zu können.
- <gold>Falschmeldung? <gray>%support-url%
- '%footer%'
# Nachricht, die angezeigt wird, wenn jemand mit einem ungültigen Benutzernamen beitritt
invalid-username:
- '%header%'
- <red>Dein Benutzername enthält ungültige Zeichen.
- '%footer%'
# Nachricht, die angezeigt wird, wenn jemand mit einer auf der Blacklist stehenden Version beitritt
blacklisted-protocol:
- '%header%'
- <red>Du verwendest eine Version, die auf unserem Server nicht erlaubt ist.
- <gold>Brauchst du Hilfe beim Einloggen? <gray>%support-url%
- '%footer%'
# Nachricht, die angezeigt wird, wenn jemand die Verifizierung erfolgreich abschließt
success:
- '%header%'
- <green>Du hast die Verifizierung erfolgreich bestanden.
- <white>Verbinde dich erneut um auf dem Server zu joinen.
# Nachricht, die angezeigt wird, wenn jemand die Verifizierung nicht besteht
failed:
- '%header%'
- <red>Du hast die Verifizierung nicht bestanden.
- <gray>Bitte warte einige Sekunden bevor du es erneut versuchst.
- <gold>Brauchst du Hilfe? <gray>%support-url%
- '%footer%'
# Übersetzungen für Sonars ausführliche Aktionsleisten-Nachrichten
verbose:
layout:
# Layout für Sonars ausführliche Aktionsleiste
# Platzhalter:
# - '%queued%' Anzahl der in der Warteschlange befindlichen Verbindungen
# - '%verifying%' Anzahl der Verifizierungen
# - '%blacklisted%' Anzahl der IP-Adressen auf der Blacklist
# - '%total-joins%' Gesamtzahl der Versuche beizutreten
# - '%verify-total%' Gesamtzahl der Verifizierungsversuche
# - '%verify-success%' Anzahl der verifizierten IP-Adressen
# - '%verify-failed%' Anzahl der gescheiterten Verifizierungen
# - '%logins-per-second%' Anzahl der Anmeldungen pro Sekunde
# - '%connections-per-second%' Anzahl der Verbindungen pro Sekunde
# - '%attack-duration%' Dauer des aktuellen Angriffs
# - '%incoming-traffic%' Eingehende Bandbreitennutzung pro Sekunde
# - '%outgoing-traffic%' Ausgehende Bandbreitennutzung pro Sekunde
# - '%incoming-traffic-ttl%' Gesamte eingehende Bandbreitennutzung
# - '%outgoing-traffic-ttl%' Gesamte ausgehende Bandbreitennutzung
# - '%used-memory%' Menge des genutzten Arbeitsspeichers des Prozesses
# - '%total-memory%' Menge des gesamten Arbeitsspeichers des Prozesses
# - '%max-memory%' Menge des maximalen Arbeitsspeichers des Prozesses
# - '%free-memory%' Menge des freien Arbeitsspeichers des Prozesses
# - '%animation%' Anpassbares animiertes Symbol
# Übersetzungen für Sonars normale ausführliche Ausgabe
normal: '%prefix%<gray>CPS <white>%connections-per-second% <dark_aqua>╺ <gray>Anmeldungen/s
<white>%logins-per-second% <dark_aqua>╺ <gray>Verifizierung <white>%verifying% <dark_aqua>|
<green>⬆ <white>%outgoing-traffic%/s <red>⬇ <white>%incoming-traffic%/s <green><bold>%animation%<reset>'
# Layout für Sonars ausführliche Aktionsleiste während eines Angriffs
attack: '%prefix%<gray>CPS <white>%connections-per-second% <dark_aqua>╺ <gray>Anmeldungen/s
<white>%logins-per-second% <dark_aqua>╺ <gray>Warteschlange <white>%queued% <dark_aqua>╺
<gray>Verifizierung <white>%verifying% <dark_aqua>╺ <gray>Dauer <white>%attack-duration%
<dark_aqua>| <green>⬆ <white>%outgoing-traffic%/s <red>⬇ <white>%incoming-traffic%/s <green><bold>%animation%<reset>'
# Animation für die Aktionsleiste
# Alternativen:
# - ▙, ▛, ▜, ▟
# - ⬈, ⬊, ⬋, ⬉
animation:
- ◜
- ◝
- ◞
- ◟
# Übersetzungen für Sonars Benachrichtigungsnachrichten bei Angriffen
notifications:
# Layout für Sonars Angriffsbenachrichtigungs-Titel
title: <yellow><b>Sonar<reset>
# Layout für Sonars Angriffsbenachrichtigungs-Untertitel
subtitle: <gray>Ein Bot-Angriff wurde erkannt
# Layout für Sonars Angriffsbenachrichtigung im Chat
chat:
- ''
- <yellow><b>Sonar<reset>
- ''
- <gray>Ein Bot-Angriff wurde erkannt.
- ' <dark_aqua>▪ <gray>Anmeldungen pro Sekunde: <white>%logins-per-second%'
- ' <dark_aqua>▪ <gray>Verifizierende Spieler: <white>%verifying%'
- ' <dark_aqua>▪ <gray>Spieler in der Warteschlange: <white>%queued%'
- ''
- <gray>Zeige mehr Informationen mit <white><click:run_command:'/sonar verbose'><hover:show_text:'(Click
to run)'>/sonar verbose</hover></click><gray> oder <white><click:run_command:'/sonar
stats'><hover:show_text:'(Click to run)'>/sonar stats</hover></click><gray>.
- ''
@@ -182,9 +182,6 @@ verification:
# Message that is logged to console whenever a player is verified
successful: '%name% has been verified successfully (%time%s!).'
# Message that is shown to the player when they are being checked for valid gravity
welcome: '%prefix%<gray>Please wait a moment for the verification to finish...'
# Messages that are shown to the player when they are supposed to enter the CAPTCHA
captcha:
# Message that is shown to the player when they have to enter the answer to the captcha
@@ -203,12 +200,6 @@ verification:
- <yellow>Your anti-bot data has not been prepared yet.
- <gray>Please wait a few seconds before trying to verify again.
- '%footer%'
# Disconnect message that is shown when too many players are verifying at the same time
too-many-players:
- '%header%'
- <gold>Too many players are currently trying to log in, try again later.
- <gray>Please wait a few seconds before trying to join again.
- '%footer%'
# Disconnect message that is shown when someone rejoins too fast during verification
too-fast-reconnect:
- '%header%'
@@ -245,13 +236,6 @@ verification:
- <red>You are using a version that is not allowed on our server.
- <gold>Need help logging in? <gray>%support-url%
- '%footer%'
# Disconnect message that is shown when someone tries verifying with an account that is online
already-online:
- '%header%'
- <red>There is someone already online with your account.
- <gray>Please wait a few seconds before trying to verify again.
- <gray>If this keeps occurring, try restarting your game or contact support.
- '%footer%'
# Disconnect message that is shown when someone verifies successfully
success:
- '%header%'
@@ -100,7 +100,7 @@ commands:
cleared: '%prefix%Вы успешно удалили %удалено% IP-адресов из черного списка.'
# Сообщение, которое показывается, когда кто-то проверяет размер черного списка
size: '%prefix%В настоящее время черный список содержит %amount% IP-адрес(ов).'
# Message that is shown when someone adds an IP address to the blacklist
# Сообщение, которое показывается, когда кто-то добавляет IP-адрес в черный список
added: '%prefix%Успешно добавлен %ip% в черный список.'
# Сообщение, которое отображается, когда кто-то добавляет IP-адрес в черный список, который находится в белом списке
added-warning: '%prefix%<red>Предупреждение: <white>%ip% находится в белом списке.
@@ -167,7 +167,7 @@ commands:
# Переводы всех сообщений в процессе проверки
verification:
# TПереводы всех отладочных сообщений во время проверки
# Переводы всех отладочных сообщений во время проверки
logs:
# Сообщение, которое выводится в консоль каждый раз, когда к серверу присоединяется новый игрок
connection: '%name%%ip% (%protocol%) подключился.'
@@ -179,9 +179,6 @@ verification:
# Сообщение, которое выводится в консоль при успешной проверке игрока
successful: '%name% прошел проверку (%time%s!).'
# Сообщение, которое показывается игроку при проверке на наличие ванильной гравитации
welcome: '%prefix%<gray>Пожалуйста, подождите некоторое время для завершения проверки...'
captcha:
# Сообщение, которое показывается игроку, когда он должен ввести ответ на капчу
enter-code: '%prefix%<green>Введите код в чате, который отображается на карте.'
@@ -235,25 +232,12 @@ verification:
- '%header%'
- <red>Ваш ник содержит недопустимые символы.
- '%footer%'
# Сообщение об отключении, которое отображается, когда кто-то присоединяется с неподдерживаемой версией
invalid-protocol:
- '%header%'
- <red>Ваша версия в настоящее время не поддерживается.
- '%footer%'
# Сообщение об отключении, которое показывается, когда кто-то заходит на сервер версией, внесенной в черный список
blacklisted-protocol:
- '%header%'
- <red>Вы используете версию, которая запрещена на нашем сервере.
- <gold>Нужна помощь? <gray>%support-url%
- '%footer%'
# Сообщение об отключении, которое показывается, когда кто-то пытается пройти верификацию с помощью ника, который находится онлайн.
already-online:
- '%header%'
- <red>Пользователь с таким ником уже онлайн.
- <gray>Пожалуйста, подождите несколько секунд, прежде чем повторить попытку.
- <gray>Если это продолжается, попробуйте перезапустить игру или обратитесь к
администратору.
- '%footer%'
# Сообщение об отключении, которое показывается при успешной верификации
success:
- '%header%'
@@ -300,7 +284,7 @@ verbose:
<white>%logins-per-second% <dark_aqua>╺ <gray>В очереди <white>%queued% <dark_aqua>╺
<gray>На проверке <white>%verifying% <dark_aqua>╺ <gray>Продолжительность <white>%attack-duration%
<dark_aqua>| <green>⬆ <white>%outgoing-traffic%/s <red>⬇ <white>%incoming-traffic%/s <green><bold>%animation%<reset>'
# Анимация для action bar (текст с названием предмета)
# Анимация для action bar (текст с названием элемента)
# Альтернативы:
# - ▙, ▛, ▜, ▟
# - ⬈, ⬊, ⬋, ⬉
@@ -312,7 +296,7 @@ verbose:
# Перевод сообщений об атаках
notifications:
# Layout for Sonar's attack notification title
# Макет для заголовка уведомления об атаке
title: <yellow><b>Sonar<reset>
# Макет для подзаголовка уведомления об атаке
subtitle: <gray>Обнаружена бот-атака
@@ -170,9 +170,6 @@ verification:
# Bir oyuncu doğrulandığında konsola kaydedilen mesaj
successful: '%name% başarıyla doğrulandı! (%time%s).'
# Yerçekimi için kontrol edildiklerinde oyuncuya gösterilen mesaj
welcome: '%prefix%<gray>Lütfen doğrulamanın bitmesini bekleyin...'
# CAPTCHA'yı girmeleri gerektiğinde oyuncuya gösterilen mesajlar
captcha:
# Captcha'nın cevabını girmeleri gerektiğinde oyuncuya gösterilen mesaj
@@ -189,12 +186,6 @@ verification:
- <yellow>Anti-bot verileriniz henüz hazır değil.
- <gray>Tekrar doğrulamayı denemeden önce lütfen birkaç saniye bekleyin.
- '%footer%'
# Aynı anda çok fazla oyuncu doğrulama yaptığında gösterilen bağlantıyı kesme mesajı
too-many-players:
- '%header%'
- <gold>Too many players are currently trying to log in, try again later.
- <gray>Please wait a few seconds before trying to join again.
- '%footer%'
# Doğrulama sırasında birisi çok hızlı bir şekilde yeniden katıldığında gösterilen bağlantıyı kesme mesajı
too-fast-reconnect:
- '%header%'
@@ -231,13 +222,6 @@ verification:
- <red>Sunucumuzda izin verilmeyen bir sürüm ile giriş yapmaya çalışıyorsun.
- <gold>Giriş yapma hakkında yardıma mı ihtiyacın var? <gray>%support-url%
- '%footer%'
# Birisi çevrimiçi olan bir hesapla doğrulama yapmaya çalıştığında gösterilen bağlantıyı kesme mesajı
already-online:
- '%header%'
- <red>Hesabınızla zaten çevrimiçi olan biri var.
- <gray>Tekrar doğrulamayı denemeden önce lütfen birkaç saniye bekleyin.
- <gray>Bu durum devam ederse, oyununuzu yeniden başlatmayı deneyin veya destek ekibiyle iletişime geçin.
- '%footer%'
# Birisi başarıyla doğrulama yaptığında gösterilen bağlantıyı kesme mesajı
success:
- '%header%'
@@ -0,0 +1,60 @@
#
# /$$$$$$
# /$$__ $$
# | $$ \__/ /$$$$$$ /$$$$$$$ /$$$$$$ /$$$$$$
# | $$$$$$ /$$__ $$| $$__ $$ |____ $$ /$$__ $$
# \____ $$| $$ \ $$| $$ \ $$ /$$$$$$$| $$ \__/
# /$$ \ $$| $$ | $$| $$ | $$ /$$__ $$| $$
# | $$$$$$/| $$$$$$/| $$ | $$| $$$$$$$| $$
# \______/ \______/ |__/ |__/ \_______/|__/
#
# Brauchst du Hilfe? https://jonesdev.xyz/discord
# Source code: https://github.com/jonesdevelopment/sonar
# Copyright Jones Development and Sonar Contributors, licensed under GPL-3.0
#
# Bot-Angriffsbenachrichtigungen können auch über Webhooks an deinen Discord-Server gesendet werden
webhook:
# URL des Discord-Webhooks (Setze dies auf '' um Webhooks zu deaktivieren)
url: ''
# Benutzername des Discord-Webhook-Senders
username: Sonar
# URL zum Avatar des Discord-Webhook-Senders (Setze dies auf '' um zu deaktivieren)
avatar-url: ''
# Inhalt der Discord-Webhook-Nachricht (Setze dies auf '' um zu deaktivieren)
# Dies kann beispielsweise verwendet werden, um Teammitglieder mit <@userId> anzupingen
# Wenn du Rollen anpingen möchtest, musst du <@&roleId> verwenden
content: ''
# Eingebettete Discord-Webhook-Nachricht, die gesendet wird, wenn ein Angriff gestoppt wurde
embed:
# Kleine Fußzeile der eingebetteten Discord-Webhook-Nachricht
footer:
# Inhalt der Fußzeile der eingebetteten Discord-Webhook-Nachricht
text: © Jones Development und Sonar Mitwirkende
# URL des Icons der Fußzeile der eingebetteten Discord-Webhook-Nachricht
icon-url: ''
# Titel der eingebetteten Discord-Webhook-Nachricht
title: ':white_check_mark: Angriff abgewehrt'
# Klickbare URL des Titels der eingebetteten Discord-Webhook-Nachricht
title-url: ''
# Beschreibung (Inhalt) der eingebetteten Discord-Webhook-Nachricht
description:
- Der Angriff auf deinen Server wurde abgewehrt.
- ''
- 'Angriffsbeginn: <t:%start-timestamp%:T>'
- 'Angriffsende: <t:%end-timestamp%:T>'
- 'Angriffsdauer: %duration%'
- ''
- 'Spitzenauslastung der CPU des Prozesses während des Angriffs: %peak-cpu%%'
- 'Spitzenauslastung des Arbeitsspeichers des Prozesses während des Angriffs: %peak-memory%'
- 'Spitzenwert der Bots pro Sekunde während des Angriffs: %peak-bps%'
- ''
- 'Während des Angriffs auf die Blacklist gesetzte IP-Adressen: %total-blacklisted%'
- 'Gescheiterte Verifizierungen während des Angriffs: %total-failed%'
- 'Erfolgreiche Verifizierungen während des Angriffs: %total-success%'
# RGB-Farben der eingebetteten Discord-Webhook-Nachricht
# Color picker: https://www.rapidtables.com/web/color/RGB_Color.html
color:
red: 0
green: 255
blue: 0
@@ -19,17 +19,24 @@ package xyz.jonesdev.sonar.velocity;
import com.alessiodp.libby.VelocityLibraryManager;
import lombok.Getter;
import net.kyori.adventure.audience.Audience;
import org.bstats.charts.SimplePie;
import org.bstats.velocity.Metrics;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.SonarPlatform;
import xyz.jonesdev.sonar.api.logger.LoggerWrapper;
import xyz.jonesdev.sonar.api.statistics.Bandwidth;
import xyz.jonesdev.sonar.common.boot.SonarBootstrap;
import xyz.jonesdev.sonar.common.statistics.CachedBandwidthStatistics;
import xyz.jonesdev.sonar.velocity.audience.AudienceListener;
import xyz.jonesdev.sonar.velocity.command.VelocitySonarCommand;
import xyz.jonesdev.sonar.velocity.fallback.FallbackInjectionHelper;
import xyz.jonesdev.sonar.velocity.fallback.FallbackLoginListener;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
@Getter
@@ -38,11 +45,24 @@ public final class SonarVelocity extends SonarBootstrap<SonarVelocityPlugin> {
public SonarVelocity(final @NotNull SonarVelocityPlugin plugin) {
super(plugin, new VelocityLibraryManager<>(
plugin, plugin.getLogger(), plugin.getDataDirectory(), plugin.getServer().getPluginManager()),
plugin, plugin.getLogger(), plugin.getDataDirectory(), plugin.getServer().getPluginManager()),
plugin.getDataDirectory().toFile(), SonarPlatform.VELOCITY);
INSTANCE = this;
}
/**
* Custom wrapper for Velocity audiences
*/
public static final Map<UUID, Audience> AUDIENCES = new ConcurrentHashMap<>();
@Override
public @Nullable Audience audience(final @Nullable UUID uniqueId) {
if (uniqueId == null) {
return getPlugin().getServer().getConsoleCommandSource();
}
return AUDIENCES.get(uniqueId);
}
/**
* Create a wrapper for the plugin logger, so we can use it outside
* the velocity module.
@@ -67,11 +87,18 @@ public final class SonarVelocity extends SonarBootstrap<SonarVelocityPlugin> {
}
};
private Metrics metrics;
@Override
public void enable() {
// Initialize bStats.org metrics
getPlugin().getMetricsFactory().make(getPlugin(), getPlatform().getMetricsId());
metrics = getPlugin().getMetricsFactory().make(getPlugin(), getPlatform().getMetricsId());
// Add charts for some configuration options
metrics.addCustomChart(new SimplePie("verification",
() -> getConfig().getVerification().getTiming().getDisplayName()));
metrics.addCustomChart(new SimplePie("captcha",
() -> getConfig().getVerification().getMap().getTiming().getDisplayName()));
// Register Sonar command
getPlugin().getServer().getCommandManager().register("sonar", new VelocitySonarCommand());
@@ -79,11 +106,12 @@ public final class SonarVelocity extends SonarBootstrap<SonarVelocityPlugin> {
// Register Fallback listener
getPlugin().getServer().getEventManager().register(getPlugin(), new FallbackLoginListener());
// TODO: find a proper way of handling Audiences on Velocity
// Register audience register listener
getPlugin().getServer().getEventManager().register(getPlugin(), new AudienceListener());
// Register traffic service
getPlugin().getServer().getScheduler().buildTask(getPlugin(), Bandwidth::reset)
getPlugin().getServer().getScheduler().buildTask(getPlugin(), CachedBandwidthStatistics::reset)
.repeat(1L, TimeUnit.SECONDS)
.schedule();
@@ -100,4 +128,10 @@ public final class SonarVelocity extends SonarBootstrap<SonarVelocityPlugin> {
// Make sure to inject into the server's connection handler
FallbackInjectionHelper.inject(getPlugin().getServer());
}
@Override
public void disable() {
// Make sure to properly shutdown bStats metrics
metrics.shutdown();
}
}
@@ -22,17 +22,17 @@ 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;
import xyz.jonesdev.sonar.velocity.SonarVelocity;
public final class AudienceListener {
@Subscribe(order = PostOrder.LAST)
public void handle(final @NotNull PostLoginEvent event) {
Sonar.AUDIENCES.put(event.getPlayer().getUsername(), event.getPlayer());
SonarVelocity.AUDIENCES.put(event.getPlayer().getUniqueId(), event.getPlayer());
}
@Subscribe(order = PostOrder.LAST)
public void handle(final @NotNull DisconnectEvent event) {
Sonar.AUDIENCES.remove(event.getPlayer().getUsername());
SonarVelocity.AUDIENCES.remove(event.getPlayer().getUniqueId());
}
}
@@ -24,7 +24,7 @@ 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",
super(sender instanceof Player ? ((Player) sender).getUniqueId() : null,
sender,
sender instanceof Player,
sender::hasPermission);
@@ -17,69 +17,40 @@
package xyz.jonesdev.sonar.velocity.fallback;
import com.velocitypowered.api.util.GameProfile;
import com.velocitypowered.proxy.connection.MinecraftConnection;
import com.velocitypowered.proxy.protocol.StateRegistry;
import com.velocitypowered.proxy.protocol.packet.DisconnectPacket;
import com.velocitypowered.proxy.protocol.packet.HandshakePacket;
import com.velocitypowered.proxy.protocol.packet.ServerLoginPacket;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.handler.codec.CorruptedFrameException;
import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.fallback.Fallback;
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
import xyz.jonesdev.sonar.api.statistics.Counters;
import xyz.jonesdev.sonar.api.statistics.Statistics;
import xyz.jonesdev.sonar.common.fallback.FallbackBandwidthHandler;
import xyz.jonesdev.sonar.common.fallback.FallbackChannelHandlerAdapter;
import xyz.jonesdev.sonar.common.fallback.FallbackUserWrapper;
import xyz.jonesdev.sonar.common.statistics.GlobalSonarStatistics;
import xyz.jonesdev.sonar.common.utility.geyser.GeyserUtil;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.util.UUID;
import static com.velocitypowered.proxy.network.Connections.*;
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_BANDWIDTH;
import static xyz.jonesdev.sonar.common.fallback.FallbackUserWrapper.closeWith;
import static xyz.jonesdev.sonar.common.fallback.FallbackUserWrapper.customDisconnect;
import static xyz.jonesdev.sonar.common.fallback.FallbackUserWrapper.deject;
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer.*;
@RequiredArgsConstructor
public final class FallbackChannelHandler extends ChannelInboundHandlerAdapter {
private final Channel channel;
private @Nullable String username;
private InetAddress inetAddress;
private ProtocolVersion protocolVersion;
private HandshakePacket handshakePacket;
public final class FallbackChannelHandler extends FallbackChannelHandlerAdapter {
private static final Fallback FALLBACK = Sonar.get().getFallback();
@Override
public void channelActive(final @NotNull ChannelHandlerContext ctx) {
// Increase connections per second for the action bar verbose
Counters.CONNECTIONS_PER_SECOND.put(System.nanoTime(), (byte) 0);
// Make sure to let the server handle the rest
ctx.fireChannelActive();
public FallbackChannelHandler(final @NotNull Channel channel) {
super(channel);
}
@Override
public void channelInactive(final @NotNull ChannelHandlerContext ctx) {
// The player can disconnect without sending the login packet first
if (username != null) {
// Remove the username from the connected players
FALLBACK.getConnected().remove(username);
}
// The player cannot be in the queue if the IP address is invalid
if (inetAddress != null) {
// Remove the IP address from the queue
FALLBACK.getQueue().remove(inetAddress);
}
// Make sure to let the server handle the rest
ctx.fireChannelInactive();
}
private @ApiStatus.Internal HandshakePacket handshakePacket;
@Override
public void channelRead(final @NotNull ChannelHandlerContext ctx, final Object msg) throws Exception {
@@ -96,18 +67,6 @@ public final class FallbackChannelHandler extends ChannelInboundHandlerAdapter {
ctx.fireChannelRead(msg);
}
// We can override the default exceptionCaught method since this handler
// will run before the MinecraftConnection knows that there has been an error.
// Additionally, this will also run after our custom decoder.
@Override
public void exceptionCaught(final @NotNull ChannelHandlerContext ctx,
final @NotNull Throwable cause) throws Exception {
// Sonar sometimes uses exceptions to quickly close
// the channel and interrupt any other ongoing process.
// Simply close the channel if we encounter any errors.
ctx.close();
}
private void handleHandshake(final @NotNull HandshakePacket handshake) throws Exception {
// Check if the player has already sent a handshake packet
if (handshakePacket != null) {
@@ -119,6 +78,13 @@ public final class FallbackChannelHandler extends ChannelInboundHandlerAdapter {
}
// Store the protocol version
protocolVersion = ProtocolVersion.fromId(handshake.getProtocolVersion().getProtocol());
// Connections from unknown protocol versions will be discarded
// as this is the safest way of handling unwanted connections
if (protocolVersion.isUnknown()) {
// Sonar does NOT support snapshots or unknown versions;
// I'll try my best to stay up-to-date!
throw new CorruptedFrameException("Unknown protocol");
}
// Store the handshake packet
handshakePacket = handshake;
// Hook the traffic listener
@@ -133,9 +99,7 @@ public final class FallbackChannelHandler extends ChannelInboundHandlerAdapter {
throw new CorruptedFrameException("Already logged on");
}
// Increase joins per second for the action bar verbose
Counters.LOGINS_PER_SECOND.put(System.nanoTime(), (byte) 0);
// Increase total traffic statistic
Statistics.TOTAL_TRAFFIC.increment();
GlobalSonarStatistics.countLogin();
// Store the username
username = serverLogin.getUsername();
// Make sure to use the potentially modified, original IP
@@ -145,29 +109,27 @@ public final class FallbackChannelHandler extends ChannelInboundHandlerAdapter {
// Check the blacklist here since we cannot let the player "ghost join"
if (FALLBACK.getBlacklist().asMap().containsKey(inetAddress)) {
closeWith(channel, protocolVersion, DisconnectPacket.create(
Sonar.get().getConfig().getVerification().getBlacklisted(),
handshakePacket.getProtocolVersion(), StateRegistry.LOGIN));
customDisconnect(channel, protocolVersion, blacklisted, MINECRAFT_DECODER, HANDLER);
return;
}
// Don't continue the verification process if the verification is disabled
if (!Sonar.get().getFallback().shouldVerifyNewPlayers()) {
ctx.fireChannelRead(serverLogin);
deject(channel.pipeline());
return;
}
// Completely skip Geyser connections
if (GeyserUtil.isGeyserConnection(channel, socketAddress)) {
ctx.fireChannelRead(serverLogin);
deject(channel.pipeline());
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(channel, protocolVersion, DisconnectPacket.create(
Sonar.get().getConfig().getVerification().getAlreadyQueued(),
handshakePacket.getProtocolVersion(), StateRegistry.LOGIN));
customDisconnect(channel, protocolVersion, alreadyQueued, MINECRAFT_DECODER, HANDLER);
return;
}
@@ -175,33 +137,29 @@ public final class FallbackChannelHandler extends ChannelInboundHandlerAdapter {
// → is another player with the same IP address connected to Fallback?
if (FALLBACK.getConnected().containsKey(serverLogin.getUsername())
|| FALLBACK.getConnected().containsValue(inetAddress)) {
closeWith(channel, protocolVersion, DisconnectPacket.create(
Sonar.get().getConfig().getVerification().getAlreadyVerifying(),
handshakePacket.getProtocolVersion(), StateRegistry.LOGIN));
customDisconnect(channel, protocolVersion, alreadyVerifying, MINECRAFT_DECODER, HANDLER);
return;
}
// Check if the protocol ID of the player is not allowed to enter the server
if (Sonar.get().getConfig().getVerification().getBlacklistedProtocols()
.contains(protocolVersion.getProtocol())) {
closeWith(channel, protocolVersion, DisconnectPacket.create(
Sonar.get().getConfig().getVerification().getProtocolBlacklisted(),
handshakePacket.getProtocolVersion(), StateRegistry.LOGIN));
customDisconnect(channel, protocolVersion, protocolBlacklisted, MINECRAFT_DECODER, HANDLER);
return;
}
// Make sure we actually have to verify the player
final GameProfile gameProfile = GameProfile.forOfflinePlayer(serverLogin.getUsername());
if (Sonar.get().getVerifiedPlayerController().has(inetAddress, gameProfile.getId())) {
final String offlineUUIDString = "OfflinePlayer:" + serverLogin.getUsername();
final UUID offlineUUID = UUID.nameUUIDFromBytes(offlineUUIDString.getBytes(StandardCharsets.UTF_8));
if (Sonar.get().getVerifiedPlayerController().has(inetAddress, offlineUUID)) {
ctx.fireChannelRead(serverLogin);
deject(channel.pipeline());
return;
}
// Check if the IP address is currently being rate-limited
if (!FALLBACK.getRatelimiter().attempt(inetAddress)) {
closeWith(channel, protocolVersion, DisconnectPacket.create(
Sonar.get().getConfig().getVerification().getTooFastReconnect(),
handshakePacket.getProtocolVersion(), StateRegistry.LOGIN));
customDisconnect(channel, protocolVersion, reconnectedTooFast, MINECRAFT_DECODER, HANDLER);
return;
}
@@ -209,6 +167,7 @@ public final class FallbackChannelHandler extends ChannelInboundHandlerAdapter {
if (Sonar.get().getConfig().getVerification().getWhitelistedProtocols()
.contains(protocolVersion.getProtocol())) {
ctx.fireChannelRead(serverLogin);
deject(channel.pipeline());
return;
}
@@ -217,18 +176,15 @@ public final class FallbackChannelHandler extends ChannelInboundHandlerAdapter {
// 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(gameProfile.getName()).matches()) {
closeWith(channel, protocolVersion, DisconnectPacket.create(
Sonar.get().getConfig().getVerification().getInvalidUsername(),
handshakePacket.getProtocolVersion(), StateRegistry.LOGIN));
.matcher(serverLogin.getUsername()).matches()) {
customDisconnect(channel, protocolVersion, invalidUsername, MINECRAFT_DECODER, HANDLER);
return;
}
// Create an instance for the Fallback connection
final FallbackUser user = new FallbackUserWrapper(channel, inetAddress, protocolVersion);
// Let the verification handler take over the channel
user.hijack(
gameProfile.getName(), gameProfile.getId(), MINECRAFT_ENCODER, MINECRAFT_DECODER, READ_TIMEOUT, HANDLER);
user.hijack(serverLogin.getUsername(), offlineUUID, MINECRAFT_ENCODER, MINECRAFT_DECODER, READ_TIMEOUT, HANDLER);
}));
}
}
@@ -51,6 +51,8 @@ public final class FallbackChannelInitializer extends ChannelInitializer<Channel
throw new ReflectiveOperationException(throwable);
}
channel.pipeline().addAfter(MINECRAFT_DECODER, FALLBACK_HANDLER, new FallbackChannelHandler(channel));
if (channel.pipeline().get(MINECRAFT_DECODER) != null) {
channel.pipeline().addAfter(MINECRAFT_DECODER, FALLBACK_HANDLER, new FallbackChannelHandler(channel));
}
}
}
@@ -19,10 +19,7 @@ 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.proxy.connection.client.ConnectedPlayer;
import com.velocitypowered.proxy.protocol.StateRegistry;
import com.velocitypowered.proxy.protocol.packet.DisconnectPacket;
import com.velocitypowered.api.event.connection.PreLoginEvent;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.velocity.SonarVelocity;
@@ -33,12 +30,12 @@ import java.util.Objects;
public final class FallbackLoginListener {
@Subscribe(order = PostOrder.LAST)
public void handle(final @NotNull LoginEvent event) {
public void handle(final @NotNull PreLoginEvent event) {
final int maxOnlinePerIp = Sonar.get().getConfig().getMaxOnlinePerIp();
// Don't do anything if the setting is disabled
if (maxOnlinePerIp <= 0) return;
final InetAddress inetAddress = event.getPlayer().getRemoteAddress().getAddress();
final InetAddress inetAddress = event.getConnection().getRemoteAddress().getAddress();
// Check if the number of online players using the same IP address as
// the connecting player is greater than the configured amount
@@ -48,10 +45,7 @@ public final class FallbackLoginListener {
// We use '>=' because the player connecting to the server hasn't joined yet
if (onlinePerIp >= maxOnlinePerIp) {
final ConnectedPlayer connectedPlayer = (ConnectedPlayer) event.getPlayer();
connectedPlayer.getConnection().closeWith(DisconnectPacket.create(
Sonar.get().getConfig().getTooManyOnlinePerIp(),
connectedPlayer.getProtocolVersion(), StateRegistry.LOGIN));
event.setResult(PreLoginEvent.PreLoginComponentResult.denied(Sonar.get().getConfig().getTooManyOnlinePerIp()));
}
}
}