Compare commits

...
12 Commits
Author SHA1 Message Date
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
67 changed files with 1375 additions and 804 deletions

No files matched your search

-1
View File
@@ -30,6 +30,5 @@ gradlew*
*.jpg
*.jpeg
*.log
*.yml
*.svg
*.txt
+17 -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.5"
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,26 @@ 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")
// 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.19
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,9 +17,6 @@
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 xyz.jonesdev.sonar.api.attack.AttackTracker;
@@ -29,6 +26,7 @@ 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;
@@ -40,11 +38,6 @@ import java.util.concurrent.ConcurrentHashMap;
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,
@@ -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;
@@ -117,7 +116,7 @@ public final class AttackTracker implements JVMProfiler {
// 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 totalFailed = Sonar.get().getStatistics().getTotalFailedVerifications();
final long failed = Math.max(totalFailed - currentAttack.failedVerifications, 0);
// Post webhook to Discord
@@ -41,7 +41,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 +62,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 +106,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) {
@@ -217,7 +223,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 +298,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")));
@@ -448,7 +451,6 @@ public final class SonarConfiguration {
private Gamemode gamemode;
private int maxMovementTicks;
private int maxIgnoredTicks;
private Component youAreBeingChecked;
@Getter
@RequiredArgsConstructor
@@ -489,10 +491,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;
@@ -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);
}
@@ -15,20 +15,20 @@
* 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";
}
@@ -38,43 +38,43 @@ public interface JVMProfiler {
return String.format("%.1f %sB", 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();
}
}
@@ -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,21 @@ 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.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 {
public final class Notification implements Observable {
private final @NotNull Collection<String> 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,22 +48,22 @@ 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);
@@ -25,31 +25,24 @@ 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.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 {
public final class Verbose implements Observable {
private final @NotNull Collection<String> 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
@@ -63,6 +56,7 @@ public final class Verbose implements Observable, JVMProfiler, Counters {
}
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 +67,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,
@@ -25,10 +25,10 @@ import org.jetbrains.annotations.NotNull;
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;
@@ -38,7 +38,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;
}
@@ -86,7 +86,7 @@ public final class SonarBukkit extends SonarBootstrap<SonarBukkitPlugin> {
// Register traffic service
getPlugin().getServer().getScheduler().runTaskTimerAsynchronously(getPlugin(),
Bandwidth::reset, 20L, 20L);
CachedBandwidthStatistics::reset, 20L, 20L);
// Register queue service
getPlugin().getServer().getScheduler().runTaskTimerAsynchronously(getPlugin(),
@@ -25,12 +25,12 @@ import org.jetbrains.annotations.NotNull;
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.concurrent.TimeUnit;
@@ -88,7 +88,7 @@ public final class SonarBungee extends SonarBootstrap<SonarBungeePlugin> {
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
@@ -17,56 +17,41 @@
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.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 +64,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 +87,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) {
@@ -173,9 +111,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,8 +122,7 @@ 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;
}
@@ -205,8 +140,7 @@ public final class FallbackChannelHandler extends ChannelInboundHandlerAdapter {
// 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 +148,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;
}
@@ -237,8 +169,7 @@ public final class FallbackChannelHandler extends ChannelInboundHandlerAdapter {
// 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;
}
@@ -255,8 +186,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()
@@ -29,10 +29,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 +53,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,6 +72,7 @@ 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);
@@ -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,21 +19,26 @@ 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;
@@ -59,49 +64,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 +86,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 +116,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 +126,116 @@ public final class FallbackUserWrapper implements FallbackUser {
pipeline.replace(decoder, FALLBACK_PACKET_DECODER,
new FallbackPacketDecoder(this, new FallbackVerificationHandler(this, username, uuid)));
}
@Override
public void fail(@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();
}
}
}
@@ -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());
}
}
@@ -595,8 +592,8 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
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);
@@ -613,7 +610,8 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
private void finish() {
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,
@@ -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),
@@ -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,7 +83,7 @@ public class FallbackPreparer {
public double maxFallDistance;
public void prepare() {
joinGame = new JoinGame(0,
joinGame = new JoinGamePacket(0,
Sonar.get().getConfig().getVerification().getGravity().getGamemode().getId(),
0,
false,
@@ -99,7 +106,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 +122,22 @@ 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);
// Precompute captcha answers
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());
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) {
@@ -35,7 +35,7 @@ import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.writeVarInt;
@Getter
@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;
@@ -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,10 +33,10 @@ 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);
}
@@ -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,159 @@
/*
* 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;
public final class GlobalSonarStatistics implements SonarStatistics {
private static final Cache<Long, Byte> LOGINS_PER_SECOND = Caffeine.newBuilder()
.expireAfterWrite(Duration.ofSeconds(1))
.build();
private static final Cache<Long, Byte> CONNECTIONS_PER_SECOND = Caffeine.newBuilder()
.expireAfterWrite(Duration.ofSeconds(1))
.build();
/**
* Helper methods that make it easier to count new statistics
*/
@ApiStatus.Internal
public static void countConnection() {
CONNECTIONS_PER_SECOND.put(System.nanoTime(), (byte) 0);
}
@ApiStatus.Internal
public static void countLogin() {
LOGINS_PER_SECOND.put(System.nanoTime(), (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);
}
@@ -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;
}
}
@@ -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
}
}
@@ -0,0 +1,171 @@
#
# /$$$$$$
# /$$__ $$
# | $$ \__/ /$$$$$$ /$$$$$$$ /$$$$$$ /$$$$$$
# | $$$$$$ /$$__ $$| $$__ $$ |____ $$ /$$__ $$
# \____ $$| $$ \ $$| $$ \ $$ /$$$$$$$| $$ \__/
# /$$ \ $$| $$ | $$| $$ | $$ /$$__ $$| $$
# | $$$$$$/| $$$$$$/| $$ | $$| $$$$$$$| $$
# \______/ \______/ |__/ |__/ \_______/|__/
#
# 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
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
# 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: []
@@ -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
@@ -23,8 +23,8 @@ import org.jetbrains.annotations.NotNull;
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;
@@ -38,7 +38,7 @@ 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;
}
@@ -83,7 +83,7 @@ public final class SonarVelocity extends SonarBootstrap<SonarVelocityPlugin> {
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();
@@ -17,69 +17,39 @@
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.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 +66,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) {
@@ -133,9 +91,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,9 +101,7 @@ 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;
}
@@ -165,9 +119,7 @@ public final class FallbackChannelHandler extends ChannelInboundHandlerAdapter {
// 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 +127,28 @@ 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);
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;
}
@@ -217,18 +164,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()));
}
}
}