mirror of
https://github.com/jonesdevelopment/sonar.git
synced 2026-11-05 17:22:34 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aa90a1fa22 | ||
|
|
9ac8447226 | ||
|
|
6b6acb8371 | ||
|
|
f3af903114 | ||
|
|
67bd3786d7 | ||
|
|
2b379b0dfa | ||
|
|
b81cc0e9f5 | ||
|
|
0efc6ae157 | ||
|
|
4ae14c58a6 | ||
|
|
3bb752f435 | ||
|
|
eeb9a446fb | ||
|
|
dfe728f7a9 | ||
|
|
3945e813dc | ||
|
|
5068ebcd91 | ||
|
|
f20d162dcc | ||
|
|
acc9df0b8d | ||
|
|
3636674eff | ||
|
|
d2b3b0adae | ||
|
|
194c7645ed | ||
|
|
860594400a | ||
|
|
20f5b135aa | ||
|
|
ef31dd4c87 | ||
|
|
8361f1990d | ||
|
|
bf9e77a437 | ||
|
|
7f604dd4ad | ||
|
|
ab96929070 | ||
|
|
fa20826b8e | ||
|
|
c8c212a3ee | ||
|
|
ad7d3e62eb | ||
|
|
343d71d18c | ||
|
|
a2e252aece | ||
|
|
124bed86dc | ||
|
|
971d43df00 |
No files matched your search
+3
-4
@@ -42,8 +42,8 @@
|
||||
- [Fallback](https://github.com/jonesdevelopment/sonar#fallback) is Sonar's main component designed to prevent
|
||||
all types of bots.
|
||||
- Spigot & BungeeCord
|
||||
- The compression method is changed
|
||||
to [Velocity's libdeflate](https://github.com/PaperMC/Velocity/tree/dev/3.0.0/native).
|
||||
- Duplicate login/status packet spam fix
|
||||
- Run server ping handling asynchronously
|
||||
- The Varint decoder is updated to Velocity's
|
||||
improved [MinecraftVarintFrameDecoder](https://github.com/PaperMC/Velocity/blob/dev/3.0.0/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftVarintFrameDecoder.java).
|
||||
|
||||
@@ -90,5 +90,4 @@ Sonar is licensed under the [GNU General Public License 3.0](https://www.gnu.org
|
||||
## Credits
|
||||
|
||||
- Special thanks to the [contributors of Sonar](https://github.com/jonesdevelopment/sonar/graphs/contributors).
|
||||
- The nbt mappings were taken from [LimboAPI](https://github.com/Elytrium/LimboAPI).
|
||||
- The compression and Varint decoding was taken from [Velocity](https://github.com/PaperMC/Velocity).
|
||||
- The Varint decoding was taken from [Velocity](https://github.com/PaperMC/Velocity).
|
||||
+2
-5
@@ -22,12 +22,9 @@ allprojects {
|
||||
repositories {
|
||||
mavenCentral() // Lombok
|
||||
maven(url = "https://jitpack.io") // simple-yaml
|
||||
maven(url = "https://repo.papermc.io/repository/maven-public") // Velocity natives
|
||||
maven(url = "https://repo.jonesdev.xyz/releases/") // Bungee & Velocity proxy module
|
||||
}
|
||||
}
|
||||
|
||||
subprojects {
|
||||
apply(plugin = "java")
|
||||
apply(plugin = "com.github.johnrengelman.shadow")
|
||||
|
||||
@@ -72,7 +69,7 @@ tasks {
|
||||
|
||||
shadowJar {
|
||||
// Set the file name of the shadowed jar
|
||||
archiveFileName.set("${rootProject.name}.jar")
|
||||
archiveFileName.set("Sonar.jar")
|
||||
|
||||
// bStats has to be relocated to the Sonar package otherwise it throws an exception
|
||||
// https://github.com/Bastian/bstats-metrics/blob/master/base/src/main/java/org/bstats/MetricsBase.java#L251
|
||||
@@ -81,9 +78,9 @@ tasks {
|
||||
// https://github.com/jonesdevelopment/sonar/issues/46
|
||||
// Relocate some packages, so we don't run into issues where we accidentally use Velocity's classes
|
||||
relocate("net.kyori.adventure.nbt", "xyz.jonesdev.sonar.libs.nbt")
|
||||
relocate("org.simpleyaml", "xyz.jonesdev.sonar.libs.yaml")
|
||||
relocate("com.google.gson", "xyz.jonesdev.sonar.libs.gson")
|
||||
relocate("com.j256.ormlite", "xyz.jonesdev.sonar.libs.ormlite")
|
||||
relocate("com.velocitypowered.natives", "xyz.jonesdev.sonar.libs.natives")
|
||||
}
|
||||
|
||||
compileJava {
|
||||
|
||||
+2
-1
@@ -1,2 +1,3 @@
|
||||
group=xyz.jonesdev.sonar
|
||||
version=2.0.4
|
||||
version=2.0.8
|
||||
description=Effective Anti-bot plugin for Velocity, BungeeCord, and Bukkit (1.7-latest)
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
rootProject.name = "Sonar"
|
||||
rootProject.name = "sonar"
|
||||
|
||||
sequenceOf("api", "bukkit", "bungee", "common", "velocity").forEach {
|
||||
val path = "sonar-$it"
|
||||
|
||||
@@ -17,14 +17,16 @@
|
||||
|
||||
package xyz.jonesdev.sonar.api;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.attack.AttackTracker;
|
||||
import xyz.jonesdev.sonar.api.command.subcommand.SubcommandRegistry;
|
||||
import xyz.jonesdev.sonar.api.config.SonarConfiguration;
|
||||
import xyz.jonesdev.sonar.api.controller.VerifiedPlayerController;
|
||||
import xyz.jonesdev.sonar.api.event.SonarEventManager;
|
||||
import xyz.jonesdev.sonar.api.fallback.Fallback;
|
||||
import xyz.jonesdev.sonar.api.logger.LoggerWrapper;
|
||||
import xyz.jonesdev.sonar.api.server.ServerWrapper;
|
||||
import xyz.jonesdev.sonar.api.verbose.Verbose;
|
||||
|
||||
import java.io.File;
|
||||
@@ -34,10 +36,14 @@ public interface Sonar {
|
||||
DecimalFormat DECIMAL_FORMAT = new DecimalFormat("#,###.##");
|
||||
String LINE_SEPARATOR = "\n"; // Using System.lineSeparator is broken, for some reason...
|
||||
|
||||
Gson GENERAL_GSON = new GsonBuilder()
|
||||
.disableInnerClassSerialization()
|
||||
.create();
|
||||
|
||||
/**
|
||||
* @return A small wrapper for the server
|
||||
* @return The platform the plugin is being run on
|
||||
*/
|
||||
@NotNull ServerWrapper getServer();
|
||||
@NotNull SonarPlatform getPlatform();
|
||||
|
||||
/**
|
||||
* @return A small wrapper for the plugin logger so we can use the logger everywhere
|
||||
@@ -76,6 +82,12 @@ public interface Sonar {
|
||||
return SonarEventManager.INSTANCE;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
default AttackTracker getAttackTracker() {
|
||||
// We don't want anyone else to create a new instance.
|
||||
return AttackTracker.INSTANCE;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
default Fallback getFallback() {
|
||||
// We don't want anyone else to create a new instance.
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.api.attack;
|
||||
|
||||
import lombok.*;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.config.SonarConfiguration;
|
||||
import xyz.jonesdev.sonar.api.event.impl.AttackDetectedEvent;
|
||||
import xyz.jonesdev.sonar.api.event.impl.AttackMitigatedEvent;
|
||||
import xyz.jonesdev.sonar.api.profiler.JVMProfiler;
|
||||
import xyz.jonesdev.sonar.api.statistics.Statistics;
|
||||
import xyz.jonesdev.sonar.api.timer.SystemTimer;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
@Getter
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public final class AttackTracker implements JVMProfiler {
|
||||
public static final AttackTracker INSTANCE = new AttackTracker();
|
||||
private @Nullable AttackStatistics currentAttack;
|
||||
private int attackThreshold;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@RequiredArgsConstructor
|
||||
public static final class AttackStatistics {
|
||||
private final SystemTimer duration = new SystemTimer();
|
||||
private final SystemTimer timer = new SystemTimer();
|
||||
private int peakJoinsPerSecond;
|
||||
private double peakProcessCPUUsage;
|
||||
private long peakProcessMemoryUsage;
|
||||
}
|
||||
|
||||
public void checkIfUnderAttack() {
|
||||
final int joinsPerSecond = Sonar.get().getVerboseHandler().getLoginsPerSecond().estimatedSize();
|
||||
final int verifyingPlayers = Sonar.get().getFallback().getConnected().size();
|
||||
final int queuedPlayers = Sonar.get().getFallback().getQueue().getQueuedPlayers().size();
|
||||
final int minPlayers = Sonar.get().getConfig().getMinPlayersForAttack();
|
||||
|
||||
if (joinsPerSecond > minPlayers // Check the number of bots/joins per second.
|
||||
|| verifyingPlayers > minPlayers // Check the number of verifying players.
|
||||
|| queuedPlayers > minPlayers) { // Check the number of queued players.
|
||||
// Increment attack threshold
|
||||
++attackThreshold;
|
||||
|
||||
// An attack has been detected
|
||||
if (currentAttack == null) {
|
||||
currentAttack = new AttackStatistics();
|
||||
Sonar.get().getEventManager().publish(new AttackDetectedEvent());
|
||||
} else {
|
||||
// Reset attack timer if we're still under attack
|
||||
currentAttack.timer.reset();
|
||||
}
|
||||
|
||||
// Update attack statistics
|
||||
if (joinsPerSecond > currentAttack.peakJoinsPerSecond) {
|
||||
// Update joins per second peak if necessary
|
||||
currentAttack.peakJoinsPerSecond = joinsPerSecond;
|
||||
}
|
||||
final double processCPUUsage = getProcessCPUUsage();
|
||||
if (processCPUUsage > currentAttack.peakProcessCPUUsage) {
|
||||
// Update cpu usage peak if necessary
|
||||
currentAttack.peakProcessCPUUsage = processCPUUsage;
|
||||
}
|
||||
final long processMemoryUsage = getUsedMemory();
|
||||
if (processMemoryUsage > currentAttack.peakProcessMemoryUsage) {
|
||||
// Update memory consumption peak if necessary
|
||||
currentAttack.peakProcessMemoryUsage = processMemoryUsage;
|
||||
}
|
||||
} else if (currentAttack != null) {
|
||||
if (currentAttack.duration.delay() > Sonar.get().getConfig().getMinAttackDuration()
|
||||
&& currentAttack.timer.delay() > Sonar.get().getConfig().getAttackCooldownDelay()) {
|
||||
// The current attack has stopped
|
||||
Sonar.get().getEventManager().publish(new AttackMitigatedEvent(currentAttack));
|
||||
|
||||
if (++attackThreshold > Sonar.get().getConfig().getMinAttackThreshold()) {
|
||||
// Save attack statistics
|
||||
final long deltaInMillis = currentAttack.duration.delay();
|
||||
final String peakCPU = Sonar.DECIMAL_FORMAT.format(currentAttack.peakProcessCPUUsage);
|
||||
final String peakMem = formatMemory(currentAttack.peakProcessMemoryUsage);
|
||||
final String peakBPS = Sonar.DECIMAL_FORMAT.format(currentAttack.peakJoinsPerSecond);
|
||||
final long minutes = deltaInMillis / (60 * 1000); // Convert milliseconds to minutes
|
||||
final double seconds = (deltaInMillis % (60 * 1000)) / 1000D; // Convert remaining milliseconds to seconds
|
||||
final String formattedDuration = String.format("%d minutes, %.0f seconds", minutes, seconds);
|
||||
final String startTimestamp = String.valueOf(currentAttack.duration.getStart() / 1000L);
|
||||
final String endTimestamp = String.valueOf(System.currentTimeMillis() / 1000L);
|
||||
final long blacklisted = Sonar.get().getFallback().getBlacklisted().estimatedSize();
|
||||
final long verified = Sonar.get().getVerifiedPlayerController().estimatedSize();
|
||||
final long failed = Statistics.FAILED_VERIFICATIONS.get();
|
||||
|
||||
// Post webhook to Discord
|
||||
Optional.ofNullable(Sonar.get().getConfig().getDiscordWebhook()).ifPresent(webhook -> webhook.post(() -> {
|
||||
final SonarConfiguration.Webhook.Embed config = Sonar.get().getConfig().getWebhook().getEmbed();
|
||||
final String description = config.getDescription()
|
||||
.replace("%start-timestamp%", startTimestamp)
|
||||
.replace("%end-timestamp%", endTimestamp)
|
||||
.replace("%duration%", formattedDuration)
|
||||
.replace("%peak-cpu%", peakCPU)
|
||||
.replace("%peak-memory%", peakMem)
|
||||
.replace("%peak-bps%", peakBPS)
|
||||
.replace("%total-blacklisted%", Sonar.DECIMAL_FORMAT.format(blacklisted))
|
||||
.replace("%total-failed%", Sonar.DECIMAL_FORMAT.format(failed))
|
||||
.replace("%total-success%", Sonar.DECIMAL_FORMAT.format(verified));
|
||||
return new SonarConfiguration.Webhook.Embed(
|
||||
config.getTitle(), config.getTitleUrl(), description, config.getR(), config.getG(), config.getB());
|
||||
}));
|
||||
}
|
||||
|
||||
// Reset the attack status
|
||||
currentAttack = null;
|
||||
// Reset threshold
|
||||
attackThreshold = 0;
|
||||
}
|
||||
} else {
|
||||
// Reset threshold
|
||||
attackThreshold = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
|
||||
public boolean isCurrentlyUnderAttack() {
|
||||
final int minDelay = Sonar.get().getConfig().getAttackCooldownDelay();
|
||||
return currentAttack != null && currentAttack.timer.delay() < minDelay;
|
||||
}
|
||||
}
|
||||
@@ -19,12 +19,14 @@ package xyz.jonesdev.sonar.api.command;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.ToString;
|
||||
import xyz.jonesdev.sonar.api.command.subcommand.Subcommand;
|
||||
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
@ToString(of = "rawArguments")
|
||||
public final class CommandInvocation {
|
||||
private final InvocationSource sender;
|
||||
private final Subcommand command;
|
||||
private final Subcommand subcommand;
|
||||
private final String[] rawArguments;
|
||||
}
|
||||
@@ -23,11 +23,15 @@ import net.kyori.adventure.audience.Audience;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.minimessage.MiniMessage;
|
||||
|
||||
import java.util.function.Predicate;
|
||||
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public abstract class InvocationSource {
|
||||
private final String name;
|
||||
private final Audience audience;
|
||||
private final boolean player;
|
||||
private final Predicate<String> permissionFunction;
|
||||
|
||||
/**
|
||||
* Sends an empty chat message to the command executor
|
||||
|
||||
@@ -18,14 +18,18 @@
|
||||
package xyz.jonesdev.sonar.api.command;
|
||||
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.event.ClickEvent;
|
||||
import net.kyori.adventure.text.event.HoverEvent;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import net.kyori.adventure.text.minimessage.MiniMessage;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.cappuccino.Cappuccino;
|
||||
import xyz.jonesdev.cappuccino.ExpiringCache;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.command.subcommand.Subcommand;
|
||||
import xyz.jonesdev.sonar.api.command.subcommand.argument.Argument;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static java.util.Collections.emptyList;
|
||||
|
||||
public interface SonarCommand {
|
||||
List<String> TAB_SUGGESTIONS = new ArrayList<>();
|
||||
@@ -34,63 +38,114 @@ public interface SonarCommand {
|
||||
|
||||
ExpiringCache<Object> DELAY = Cappuccino.buildExpiring(500L);
|
||||
|
||||
int COPYRIGHT_YEAR = Calendar.getInstance().get(Calendar.YEAR);
|
||||
List<Component> CACHED_HELP_MESSAGE = new ArrayList<>();
|
||||
|
||||
List<Component> CACHED_HELP_MESSAGE = new Vector<>();
|
||||
|
||||
default void cacheHelpMessage() {
|
||||
CACHED_HELP_MESSAGE.addAll(Arrays.asList(
|
||||
Component.text("Running Sonar " + Sonar.get().getVersion()
|
||||
+ " on " + Sonar.get().getServer().getPlatform().getDisplayName()
|
||||
+ ".", NamedTextColor.YELLOW),
|
||||
Component.text("(C) " + COPYRIGHT_YEAR + " Jones Development and Sonar Contributors", NamedTextColor.YELLOW),
|
||||
Component.text("https://github.com/jonesdevelopment/sonar", NamedTextColor.GREEN)
|
||||
.clickEvent(ClickEvent.clickEvent(ClickEvent.Action.OPEN_URL, "https://github.com/jonesdevelopment/sonar")),
|
||||
Component.empty(),
|
||||
Component.text("Need help or have any questions?", NamedTextColor.YELLOW),
|
||||
Component.textOfChildren(
|
||||
Component.text("Open a ticket on the Discord ", NamedTextColor.YELLOW)
|
||||
.hoverEvent(HoverEvent.hoverEvent(HoverEvent.Action.SHOW_TEXT, Component.text("(Click to open Discord)")))
|
||||
.clickEvent(ClickEvent.clickEvent(ClickEvent.Action.OPEN_URL, "https://jonesdev.xyz/discord/")),
|
||||
Component.text("or open a new issue on GitHub.", NamedTextColor.YELLOW)
|
||||
.hoverEvent(HoverEvent.hoverEvent(HoverEvent.Action.SHOW_TEXT, Component.text("(Click to open GitHub)")))
|
||||
.clickEvent(ClickEvent.clickEvent(ClickEvent.Action.OPEN_URL, "https://github.com/jonesdevelopment/sonar" +
|
||||
"/issues"))
|
||||
),
|
||||
Component.empty()
|
||||
));
|
||||
|
||||
Sonar.get().getSubcommandRegistry().getSubcommands().forEach(sub -> {
|
||||
Component component = Component.textOfChildren(
|
||||
Component.text(" ▪ ", NamedTextColor.GRAY),
|
||||
Component.text("/sonar " + sub.getInfo().name(), NamedTextColor.GREEN),
|
||||
Component.text(" - ", NamedTextColor.GRAY),
|
||||
Component.text(sub.getInfo().description(), NamedTextColor.WHITE)
|
||||
);
|
||||
|
||||
Component hoverComponent = Component.textOfChildren(
|
||||
Component.text("Only players: ", NamedTextColor.GRAY),
|
||||
Component.text(sub.getInfo().onlyPlayers() ? "✔" : "✗",
|
||||
sub.getInfo().onlyPlayers() ? NamedTextColor.GREEN : NamedTextColor.RED),
|
||||
Component.newline(),
|
||||
Component.text("Require console: ", NamedTextColor.GRAY),
|
||||
Component.text(sub.getInfo().onlyConsole() ? "✔" : "✗",
|
||||
sub.getInfo().onlyConsole() ? NamedTextColor.GREEN : NamedTextColor.RED),
|
||||
Component.newline(),
|
||||
Component.text("Permission: ", NamedTextColor.GRAY),
|
||||
Component.text(sub.getPermission(), NamedTextColor.WHITE)
|
||||
);
|
||||
if (sub.getInfo().aliases().length > 0) {
|
||||
hoverComponent = hoverComponent
|
||||
.append(Component.newline())
|
||||
.append(Component.text("Aliases: ", NamedTextColor.GRAY))
|
||||
.append(Component.text(sub.getAliases(), NamedTextColor.WHITE));
|
||||
default void handle(final @NotNull InvocationSource source, final String[] args) {
|
||||
if (source.isPlayer()) {
|
||||
// Check if the player actually has the permission to run the command
|
||||
if (!source.getPermissionFunction().test("sonar.command")) {
|
||||
source.sendMessage(Sonar.get().getConfig().getNoPermission());
|
||||
return;
|
||||
}
|
||||
component = component
|
||||
.clickEvent(ClickEvent.clickEvent(ClickEvent.Action.SUGGEST_COMMAND,
|
||||
"/sonar " + sub.getInfo().name() + " "))
|
||||
.hoverEvent(HoverEvent.hoverEvent(HoverEvent.Action.SHOW_TEXT, hoverComponent));
|
||||
CACHED_HELP_MESSAGE.add(component);
|
||||
// Checking if it contains will only break more since it can throw
|
||||
// a NullPointerException if the cache is being accessed from parallel threads
|
||||
DELAY.cleanUp(); // Clean up the cache
|
||||
final long mapTimestamp = DELAY.asMap().getOrDefault(source, -1L);
|
||||
|
||||
// There were some exploits with spamming commands in the past.
|
||||
// Spamming should be prevented, especially if some heavy operations are done,
|
||||
// which is not the case here but let's still stay safe!
|
||||
if (mapTimestamp > 0L) {
|
||||
source.sendMessage(Sonar.get().getConfig().getCommands().getCommandCoolDown());
|
||||
|
||||
// Format delay
|
||||
final long timestamp = System.currentTimeMillis();
|
||||
final double left = 0.5D - (timestamp - mapTimestamp) / 1000D;
|
||||
|
||||
source.sendMessage(Sonar.get().getConfig().getCommands().getCommandCoolDownLeft()
|
||||
.replace("%time-left%", Sonar.DECIMAL_FORMAT.format(left)));
|
||||
return;
|
||||
}
|
||||
|
||||
DELAY.put(source);
|
||||
}
|
||||
|
||||
if (args.length > 0) {
|
||||
// Search subcommand if command arguments are present
|
||||
final Optional<Subcommand> subcommand = Sonar.get().getSubcommandRegistry().getSubcommands().stream()
|
||||
.filter(sub -> sub.getInfo().name().equalsIgnoreCase(args[0])
|
||||
|| Arrays.stream(sub.getInfo().aliases())
|
||||
.anyMatch(alias -> alias.equalsIgnoreCase(args[0])))
|
||||
.findFirst();
|
||||
|
||||
// Check permissions for subcommands
|
||||
if (subcommand.isPresent()) {
|
||||
if (!subcommand.get().getInfo().onlyConsole()
|
||||
&& !source.getPermissionFunction().test(subcommand.get().getPermission())) {
|
||||
source.sendMessage(Sonar.get().getConfig().getCommands().getSubCommandNoPerm()
|
||||
.replace("%permission%", subcommand.get().getPermission()));
|
||||
return;
|
||||
}
|
||||
subcommand.get().invoke(source, args);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Re-use the old, cached help message since we don't want to scan
|
||||
// for each subcommand and it's arguments/attributes every time
|
||||
// someone runs /sonar since the subcommand don't change
|
||||
for (final Component component : CACHED_HELP_MESSAGE) {
|
||||
source.sendMessage(component);
|
||||
}
|
||||
}
|
||||
|
||||
static void prepareCachedMessages() {
|
||||
// Cache help message
|
||||
CACHED_HELP_MESSAGE.clear();
|
||||
for (final String message : Sonar.get().getConfig().getCommands().getHelpHeader()) {
|
||||
CACHED_HELP_MESSAGE.add(MiniMessage.miniMessage().deserialize(message
|
||||
.replace("%version%", Sonar.get().getVersion().getFormatted())
|
||||
.replace("%platform%", Sonar.get().getPlatform().getDisplayName())
|
||||
.replace("%copyright_year%", String.valueOf(Calendar.getInstance().get(Calendar.YEAR)))));
|
||||
}
|
||||
|
||||
final String subcommandFormat = Sonar.get().getConfig().getCommands().getHelpSubcommands();
|
||||
Sonar.get().getSubcommandRegistry().getSubcommands().forEach(subcommand -> {
|
||||
final Component deserialized = MiniMessage.miniMessage().deserialize(subcommandFormat
|
||||
.replace("%subcommand%", subcommand.getInfo().name())
|
||||
.replace("%description%", subcommand.getInfo().description())
|
||||
.replace("%only_players%", subcommand.getInfo().onlyPlayers() ? "<green>✔</green>" : "<red>✗</red>")
|
||||
.replace("%require_console%", subcommand.getInfo().onlyConsole() ? "<green>✔</green>" : "<red>✗</red>")
|
||||
.replace("%permission%", subcommand.getPermission())
|
||||
.replace("%aliases%", subcommand.getAliases()));
|
||||
CACHED_HELP_MESSAGE.add(deserialized);
|
||||
});
|
||||
|
||||
// Don't re-cache tab suggestions
|
||||
if (!TAB_SUGGESTIONS.isEmpty()) return;
|
||||
// Cache tab suggestions
|
||||
for (final Subcommand subcommand : Sonar.get().getSubcommandRegistry().getSubcommands()) {
|
||||
TAB_SUGGESTIONS.add(subcommand.getInfo().name());
|
||||
if (subcommand.getInfo().aliases().length > 0) {
|
||||
TAB_SUGGESTIONS.addAll(Arrays.asList(subcommand.getInfo().aliases()));
|
||||
}
|
||||
final List<String> parsedArguments = Arrays.stream(subcommand.getInfo().arguments())
|
||||
.map(Argument::value)
|
||||
.collect(Collectors.toList());
|
||||
ARG_TAB_SUGGESTIONS.put(subcommand.getInfo().name(), parsedArguments);
|
||||
for (final String alias : subcommand.getInfo().aliases()) {
|
||||
ARG_TAB_SUGGESTIONS.put(alias, parsedArguments);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
default List<String> getCachedTabSuggestions(final String @NotNull [] arguments) {
|
||||
if (arguments.length <= 1) {
|
||||
return TAB_SUGGESTIONS;
|
||||
} else if (arguments.length == 2) {
|
||||
final String subCommandName = arguments[0].toLowerCase();
|
||||
return ARG_TAB_SUGGESTIONS.getOrDefault(subCommandName, emptyList());
|
||||
}
|
||||
return emptyList();
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,7 @@ import xyz.jonesdev.sonar.api.command.subcommand.argument.Argument;
|
||||
import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@@ -35,10 +36,8 @@ import java.util.stream.Collectors;
|
||||
public abstract class Subcommand {
|
||||
private final @NotNull SubcommandInfo info;
|
||||
private final String permission, aliases, arguments;
|
||||
|
||||
protected static final Sonar SONAR = Sonar.get();
|
||||
protected static final Pattern IP_PATTERN = Pattern.compile(
|
||||
"^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])([.])){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$"
|
||||
);
|
||||
|
||||
public Subcommand() {
|
||||
info = getClass().getAnnotation(SubcommandInfo.class);
|
||||
@@ -52,8 +51,14 @@ public abstract class Subcommand {
|
||||
.collect(Collectors.joining(", "));
|
||||
}
|
||||
|
||||
protected static @Nullable InetAddress checkIP(final InvocationSource source, final String rawIP) {
|
||||
if (!IP_PATTERN.matcher(rawIP).matches()) {
|
||||
// https://stackoverflow.com/questions/5284147/validating-ipv4-addresses-with-regexp
|
||||
private static final Pattern IP_PATTERN = Pattern.compile("^((25[0-5]|(2[0-4]|1\\d|[1-9]|)\\d)\\.?\\b){4}$");
|
||||
|
||||
protected static @Nullable InetAddress getInetAddressIfValid(final InvocationSource source, final String rawIP) {
|
||||
// We perform a regex check beforehand,
|
||||
// so we don't have to create a new InetAddress,
|
||||
// therefore, potentially saving some performance.
|
||||
if (!IP_PATTERN.matcher(Objects.requireNonNull(rawIP)).matches()) {
|
||||
source.sendMessage(SONAR.getConfig().getCommands().getIncorrectIpAddress());
|
||||
return null;
|
||||
}
|
||||
@@ -78,5 +83,31 @@ public abstract class Subcommand {
|
||||
.replace("%usage%", info.name() + " (" + arguments + ")"));
|
||||
}
|
||||
|
||||
public abstract void execute(final @NotNull CommandInvocation invocation);
|
||||
public final void invoke(final @NotNull InvocationSource invocationSource, final String @NotNull [] arguments) {
|
||||
// Check if the subcommand can only be executed by players
|
||||
if (getInfo().onlyPlayers() && !invocationSource.isPlayer()) {
|
||||
invocationSource.sendMessage(Sonar.get().getConfig().getCommands().getPlayersOnly());
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the subcommand can only be executed though console
|
||||
if (getInfo().onlyConsole() && invocationSource.isPlayer()) {
|
||||
invocationSource.sendMessage(Sonar.get().getConfig().getCommands().getConsoleOnly());
|
||||
return;
|
||||
}
|
||||
|
||||
final CommandInvocation commandInvocation = new CommandInvocation(invocationSource, this, arguments);
|
||||
|
||||
// The subcommands has arguments which are not present in the executed command
|
||||
if (getInfo().argumentsRequired() && getInfo().arguments().length > 0 && arguments.length <= 1) {
|
||||
invocationSource.sendMessage(Sonar.get().getConfig().getCommands().getIncorrectCommandUsage()
|
||||
.replace("%usage%", getInfo().name() + " (" + getArguments() + ")"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Execute the sub command
|
||||
execute(commandInvocation);
|
||||
}
|
||||
|
||||
protected abstract void execute(final @NotNull CommandInvocation invocation);
|
||||
}
|
||||
@@ -18,6 +18,7 @@
|
||||
package xyz.jonesdev.sonar.api.config;
|
||||
|
||||
import lombok.Getter;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.simpleyaml.configuration.comments.format.YamlCommentFormat;
|
||||
import org.simpleyaml.configuration.file.YamlFile;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
@@ -36,8 +37,8 @@ public final class SimpleYamlConfig {
|
||||
|
||||
private static final List<String> HEADER = Arrays.asList(
|
||||
"",
|
||||
"Running Sonar version " + Sonar.get().getVersion()
|
||||
+ " on " + Sonar.get().getServer().getPlatform().getDisplayName(),
|
||||
String.format("Running Sonar version %s on %s",
|
||||
Sonar.get().getVersion(), Sonar.get().getPlatform().getDisplayName()),
|
||||
"Need help or have questions? https://jonesdev.xyz/discord",
|
||||
"https://github.com/jonesdevelopment/sonar",
|
||||
""
|
||||
@@ -47,7 +48,7 @@ public final class SimpleYamlConfig {
|
||||
this(new File(dataFolder, fileName + ".yml"), dataFolder);
|
||||
}
|
||||
|
||||
private SimpleYamlConfig(final File file, final File folder) {
|
||||
private SimpleYamlConfig(final File file, final @NotNull File folder) {
|
||||
if (!folder.exists() && !folder.mkdir()) {
|
||||
throw new IllegalStateException("Could not create folder?!");
|
||||
}
|
||||
@@ -93,8 +94,18 @@ public final class SimpleYamlConfig {
|
||||
}
|
||||
|
||||
public String getString(final String path, final String def) {
|
||||
final Object object = getObject(path, def);
|
||||
if (object instanceof String) {
|
||||
return (String) object;
|
||||
}
|
||||
Sonar.get().getLogger().info("[config] Migrated {} to {}", path, def);
|
||||
set(path, def);
|
||||
return def;
|
||||
}
|
||||
|
||||
public Object getObject(final String path, final Object def) {
|
||||
yaml.addDefault(path, def);
|
||||
return yaml.getString(path, def);
|
||||
return yaml.get(path, def);
|
||||
}
|
||||
|
||||
public List<String> getStringList(final String path, final List<String> def) {
|
||||
|
||||
@@ -21,8 +21,11 @@ import lombok.*;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.minimessage.MiniMessage;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.command.SonarCommand;
|
||||
import xyz.jonesdev.sonar.api.dependencies.Dependency;
|
||||
import xyz.jonesdev.sonar.api.webhook.DiscordWebhook;
|
||||
|
||||
import java.io.File;
|
||||
import java.net.InetAddress;
|
||||
@@ -51,6 +54,8 @@ public final class SonarConfiguration {
|
||||
private final Lockdown lockdown = new Lockdown();
|
||||
@Getter
|
||||
private final Database database = new Database();
|
||||
@Getter
|
||||
private final Webhook webhook = new Webhook();
|
||||
|
||||
public SonarConfiguration(final @NotNull File pluginFolder) {
|
||||
this.pluginFolder = pluginFolder;
|
||||
@@ -68,12 +73,19 @@ public final class SonarConfiguration {
|
||||
@Getter
|
||||
private int minPlayersForAttack;
|
||||
@Getter
|
||||
private int minAttackDuration;
|
||||
@Getter
|
||||
private int minAttackThreshold;
|
||||
@Getter
|
||||
private int attackCooldownDelay;
|
||||
@Getter
|
||||
private Component tooManyOnlinePerIp;
|
||||
|
||||
@Getter
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public static final class Verbose {
|
||||
private String actionBarLayout;
|
||||
private String actionBarLayoutDuringAttack;
|
||||
private List<String> animation;
|
||||
}
|
||||
|
||||
@@ -86,9 +98,15 @@ public final class SonarConfiguration {
|
||||
@Getter
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public static final class Verification {
|
||||
private boolean enabled;
|
||||
private Timing timing;
|
||||
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public enum Timing {
|
||||
ALWAYS, DURING_ATTACK, NEVER
|
||||
}
|
||||
|
||||
private boolean checkGravity;
|
||||
private boolean checkCollisions;
|
||||
private boolean logConnections;
|
||||
private boolean logDuringAttack;
|
||||
private boolean debugXYZPositions;
|
||||
@@ -99,7 +117,20 @@ public final class SonarConfiguration {
|
||||
private String failedLog;
|
||||
private String successLog;
|
||||
private String blacklistLog;
|
||||
private short gamemodeId;
|
||||
|
||||
private Gamemode gamemode;
|
||||
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public enum Gamemode {
|
||||
SURVIVAL(0),
|
||||
CREATIVE(1),
|
||||
ADVENTURE(2),
|
||||
SPECTATOR(3);
|
||||
|
||||
private final int id;
|
||||
}
|
||||
|
||||
private int maxBrandLength;
|
||||
private int maxMovementTicks;
|
||||
private int maxIgnoredTicks;
|
||||
@@ -157,10 +188,11 @@ public final class SonarConfiguration {
|
||||
private String networkStatistics;
|
||||
private String cpuStatistics;
|
||||
|
||||
private List<String> helpHeader;
|
||||
private String helpSubcommands;
|
||||
|
||||
private String verboseSubscribed;
|
||||
private String verboseUnsubscribed;
|
||||
private String verboseSubscribedOther;
|
||||
private String verboseUnsubscribedOther;
|
||||
|
||||
private String reloading;
|
||||
private String reloaded;
|
||||
@@ -173,11 +205,10 @@ public final class SonarConfiguration {
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public enum Type {
|
||||
MYSQL(new Dependency[]{Dependency.MYSQL}, "com.mysql.cj.jdbc.NonRegisteringDriver"),
|
||||
MARIADB(new Dependency[]{Dependency.MYSQL, Dependency.MARIADB}, "org.mariadb.jdbc.Driver"),
|
||||
MYSQL(Dependency.MYSQL, "com.mysql.cj.jdbc.NonRegisteringDriver"),
|
||||
NONE(null, null);
|
||||
|
||||
private final Dependency[] dependencies;
|
||||
private final Dependency dependency;
|
||||
private final String driverClassName;
|
||||
}
|
||||
|
||||
@@ -204,6 +235,40 @@ public final class SonarConfiguration {
|
||||
private String consoleLog;
|
||||
}
|
||||
|
||||
@Getter
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public static final class Webhook {
|
||||
private String url;
|
||||
private String username;
|
||||
private String avatarUrl;
|
||||
private String content;
|
||||
|
||||
private final Footer footer = new Footer();
|
||||
|
||||
@Getter
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public static final class Footer {
|
||||
private String text;
|
||||
private String iconUrl;
|
||||
}
|
||||
|
||||
private final Embed embed = new Embed();
|
||||
|
||||
@Getter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static final class Embed {
|
||||
private String title;
|
||||
private String titleUrl;
|
||||
@Setter
|
||||
private String description;
|
||||
private int r, g, b;
|
||||
}
|
||||
}
|
||||
|
||||
@Getter
|
||||
private @Nullable DiscordWebhook discordWebhook;
|
||||
|
||||
public void load() {
|
||||
if (generalConfig == null) {
|
||||
generalConfig = new SimpleYamlConfig(pluginFolder, "config");
|
||||
@@ -219,23 +284,32 @@ public final class SonarConfiguration {
|
||||
|
||||
// General options
|
||||
generalConfig.getYaml().setComment("language",
|
||||
"Name of the language file Sonar should use for messages"
|
||||
);
|
||||
"Name of the language file Sonar should use for messages");
|
||||
String language = generalConfig.getString("language", "en");
|
||||
|
||||
generalConfig.getYaml().setComment("max-online-per-ip",
|
||||
"Maximum number of players online with the same IP address"
|
||||
);
|
||||
"Maximum number of players online with the same IP address");
|
||||
maxOnlinePerIp = clamp(generalConfig.getInt("max-online-per-ip", 3), 1, Byte.MAX_VALUE);
|
||||
|
||||
generalConfig.getYaml().setComment("min-players-for-attack",
|
||||
"Minimum number of new players in order for an attack to be detected"
|
||||
);
|
||||
minPlayersForAttack = clamp(generalConfig.getInt("min-players-for-attack", 5), 2, 1024);
|
||||
"Minimum number of new players in order for an attack to be detected");
|
||||
minPlayersForAttack = clamp(generalConfig.getInt("min-players-for-attack", 8), 2, 1024);
|
||||
|
||||
generalConfig.getYaml().setComment("min-attack-duration",
|
||||
"Amount of time (in milliseconds) that has to pass in order for an attack to be over");
|
||||
minAttackDuration = clamp(generalConfig.getInt("min-attack-duration", 30000), 1000, 900000);
|
||||
|
||||
generalConfig.getYaml().setComment("min-attack-threshold",
|
||||
"Number of times an incident has to be reported in order to be acknowledged as an attack"
|
||||
+ LINE_SEPARATOR + "This number acts as a buffer to filter out false attack notifications");
|
||||
minAttackThreshold = clamp(generalConfig.getInt("min-attack-threshold", 2), 0, 20);
|
||||
|
||||
generalConfig.getYaml().setComment("attack-cooldown-delay",
|
||||
"Amount of time (in milliseconds) that has to pass in order for a new attack to be detected");
|
||||
attackCooldownDelay = clamp(generalConfig.getInt("attack-cooldown-delay", 3000), 100, 30000);
|
||||
|
||||
generalConfig.getYaml().setComment("log-player-addresses",
|
||||
"Should Sonar log players' IP addresses in console?"
|
||||
);
|
||||
"Should Sonar log players' IP addresses in console?");
|
||||
logPlayerAddresses = generalConfig.getBoolean("log-player-addresses", true);
|
||||
|
||||
// Message settings
|
||||
@@ -264,10 +338,9 @@ public final class SonarConfiguration {
|
||||
// Database
|
||||
generalConfig.getYaml().setComment("database.type",
|
||||
"Type of database Sonar uses to store verified players"
|
||||
+ LINE_SEPARATOR + "Possible types: NONE, MYSQL, MARIADB (experimental)"
|
||||
);
|
||||
final String newDatabaseType = generalConfig.getString("database.type", Database.Type.NONE.name());
|
||||
database.type = Database.Type.valueOf(newDatabaseType.toUpperCase());
|
||||
+ LINE_SEPARATOR + "Possible types: NONE, MYSQL");
|
||||
database.type = Database.Type.valueOf(
|
||||
generalConfig.getString("database.type", Database.Type.NONE.name()).toUpperCase());
|
||||
|
||||
generalConfig.getYaml().setComment("database",
|
||||
"You can connect Sonar to a database to keep verified players even after restarting your server"
|
||||
@@ -328,9 +401,14 @@ public final class SonarConfiguration {
|
||||
generalConfig.getYaml().setComment("verification",
|
||||
"Every new player that joins for the first time will be sent to"
|
||||
+ LINE_SEPARATOR + "a lightweight limbo server where advanced bot checks are performed");
|
||||
generalConfig.getYaml().setComment("verification.enabled",
|
||||
"Should Sonar verify new players? (Recommended)");
|
||||
verification.enabled = generalConfig.getBoolean("verification.enabled", true);
|
||||
generalConfig.getYaml().setComment("verification.timing",
|
||||
"When should Sonar verify new players?"
|
||||
+ LINE_SEPARATOR + "Possible types: ALWAYS, DURING_ATTACK, NEVER"
|
||||
+ LINE_SEPARATOR + "- ALWAYS: New players will always be checked (Recommended)"
|
||||
+ LINE_SEPARATOR + "- DURING_ATTACK: New players will only be checked during an attack"
|
||||
+ LINE_SEPARATOR + "- NEVER: New players will never be checked");
|
||||
verification.timing = Verification.Timing.valueOf(
|
||||
generalConfig.getString("verification.timing", Verification.Timing.ALWAYS.name()).toUpperCase());
|
||||
|
||||
generalConfig.getYaml().setComment("verification.checks.gravity",
|
||||
"Checks if the players' falling motion is following Minecraft's gravity formula"
|
||||
@@ -349,16 +427,42 @@ public final class SonarConfiguration {
|
||||
verification.maxIgnoredTicks = clamp(generalConfig.getInt("verification.checks.gravity.max-ignored-ticks", 5), 1,
|
||||
128);
|
||||
|
||||
generalConfig.getYaml().setComment("verification.checks.collisions",
|
||||
"Checks if the players collides with barrier blocks spawned below the player"
|
||||
+ LINE_SEPARATOR + "Note: The collision check will be skipped if the gravity check is disabled");
|
||||
generalConfig.getYaml().setComment("verification.checks.collisions.enabled",
|
||||
"Should Sonar check for valid client collisions? (Recommended)");
|
||||
verification.checkCollisions = generalConfig.getBoolean("verification.checks.collisions.enabled", true);
|
||||
generalConfig.getYaml().setComment("verification.checks.valid-name-regex",
|
||||
"Regex for validating usernames during verification");
|
||||
verification.validNameRegex = Pattern.compile(generalConfig.getString(
|
||||
"verification.checks.valid-name-regex", "^[a-zA-Z0-9_]+$"));
|
||||
|
||||
generalConfig.getYaml().setComment("verification.checks.valid-brand-regex",
|
||||
"Regex for validating client brands during verification");
|
||||
verification.validBrandRegex = Pattern.compile(generalConfig.getString(
|
||||
"verification.checks.valid-brand-regex", "^[!-~ ]+$"));
|
||||
|
||||
generalConfig.getYaml().setComment("verification.checks.valid-locale-regex",
|
||||
"Regex for validating client locale during verification");
|
||||
verification.validLocaleRegex = Pattern.compile(generalConfig.getString(
|
||||
"verification.checks.valid-locale-regex", "^[a-zA-Z_]+$"));
|
||||
|
||||
generalConfig.getYaml().setComment("verification.checks.max-brand-length",
|
||||
"Maximum client brand length during verification");
|
||||
verification.maxBrandLength = generalConfig.getInt("verification.checks.max-brand-length", 64);
|
||||
|
||||
generalConfig.getYaml().setComment("verification.checks.max-ping",
|
||||
"Ping (in milliseconds) a player has to have in order to timeout");
|
||||
verification.maxPing = clamp(generalConfig.getInt("verification.checks.max-ping", 10000), 500, 30000);
|
||||
|
||||
generalConfig.getYaml().setComment("verification.checks.max-login-packets",
|
||||
"Maximum number of login packets the player has to send in order to be kicked");
|
||||
verification.maxLoginPackets = clamp(generalConfig.getInt("verification.checks.max-login-packets", 256), 128, 8192);
|
||||
|
||||
generalConfig.getYaml().setComment("verification.gamemode",
|
||||
"The gamemode of the player during verification (0, 1, 2, or 3)");
|
||||
verification.gamemodeId = (short) clamp(generalConfig.getInt("verification.gamemode", 3), 0, 3);
|
||||
"The gamemode of the player during verification"
|
||||
+ LINE_SEPARATOR + "Possible types: SURVIVAL, CREATIVE, ADVENTURE, SPECTATOR"
|
||||
+ LINE_SEPARATOR + "- SURVIVAL: all UI components are visible"
|
||||
+ LINE_SEPARATOR + "- CREATIVE: health and hunger are hidden"
|
||||
+ LINE_SEPARATOR + "- ADVENTURE: all UI components are visible"
|
||||
+ LINE_SEPARATOR + "- SPECTATOR: all UI components are hidden (Recommended)");
|
||||
verification.gamemode = Verification.Gamemode.valueOf(
|
||||
generalConfig.getString("verification.gamemode", Verification.Gamemode.SPECTATOR.name()).toUpperCase());
|
||||
|
||||
generalConfig.getYaml().setComment("verification.log-connections",
|
||||
"Should Sonar log new verification attempts?");
|
||||
@@ -373,37 +477,10 @@ public final class SonarConfiguration {
|
||||
+ LINE_SEPARATOR + "This is not recommended for production servers but can be helpful for spotting errors.");
|
||||
verification.debugXYZPositions = generalConfig.getBoolean("verification.debug-xyz-positions", false);
|
||||
|
||||
generalConfig.getYaml().setComment("verification.valid-name-regex",
|
||||
"Regex for validating usernames during verification");
|
||||
verification.validNameRegex = Pattern.compile(generalConfig.getString(
|
||||
"verification.valid-name-regex", "^[a-zA-Z0-9_.*!]+$"));
|
||||
|
||||
generalConfig.getYaml().setComment("verification.valid-brand-regex",
|
||||
"Regex for validating client brands during verification");
|
||||
verification.validBrandRegex = Pattern.compile(generalConfig.getString(
|
||||
"verification.valid-brand-regex", "^[!-~ ]+$"));
|
||||
|
||||
generalConfig.getYaml().setComment("verification.valid-locale-regex",
|
||||
"Regex for validating client locale during verification");
|
||||
verification.validLocaleRegex = Pattern.compile(generalConfig.getString(
|
||||
"verification.valid-locale-regex", "^[a-zA-Z_]+$"));
|
||||
|
||||
generalConfig.getYaml().setComment("verification.max-brand-length",
|
||||
"Maximum client brand length during verification");
|
||||
verification.maxBrandLength = generalConfig.getInt("verification.max-brand-length", 64);
|
||||
|
||||
generalConfig.getYaml().setComment("verification.max-ping",
|
||||
"Ping (in milliseconds) a player has to have in order to timeout");
|
||||
verification.maxPing = clamp(generalConfig.getInt("verification.max-ping", 10000), 500, 30000);
|
||||
|
||||
generalConfig.getYaml().setComment("verification.read-timeout",
|
||||
"Amount of time that has to pass before a player times out");
|
||||
verification.readTimeout = clamp(generalConfig.getInt("verification.read-timeout", 3500), 500, 30000);
|
||||
|
||||
generalConfig.getYaml().setComment("verification.max-login-packets",
|
||||
"Maximum number of login packets the player has to send in order to be kicked");
|
||||
verification.maxLoginPackets = clamp(generalConfig.getInt("verification.max-login-packets", 256), 128, 8192);
|
||||
|
||||
generalConfig.getYaml().setComment("verification.max-players",
|
||||
"Maximum number of players verifying at the same time");
|
||||
verification.maxVerifyingPlayers = clamp(generalConfig.getInt("verification.max-players", 1024), 1,
|
||||
@@ -413,6 +490,86 @@ public final class SonarConfiguration {
|
||||
"Minimum number of rejoin delay during verification");
|
||||
verification.reconnectDelay = clamp(generalConfig.getInt("verification.rejoin-delay", 8000), 0, 100000);
|
||||
|
||||
// Webhooks
|
||||
generalConfig.getYaml().setComment("webhook",
|
||||
"Bot attack notifications can also be sent to your Discord server using webhooks");
|
||||
generalConfig.getYaml().setComment("webhook.url",
|
||||
"URL of the Discord webhook (Set this to '' to disable webhooks)");
|
||||
webhook.url = generalConfig.getString("webhook.url", "");
|
||||
|
||||
generalConfig.getYaml().setComment("webhook.username",
|
||||
"Username of the Discord webhook sender");
|
||||
webhook.username = generalConfig.getString("webhook.username", "Sonar");
|
||||
|
||||
generalConfig.getYaml().setComment("webhook.avatar-url",
|
||||
"URL to the avatar of the Discord webhook sender (Set this to '' to disable)");
|
||||
webhook.avatarUrl = generalConfig.getString("webhook.avatar-url", "");
|
||||
|
||||
generalConfig.getYaml().setComment("webhook.content",
|
||||
"Content of the Discord webhook message (Set this to '' to disable)"
|
||||
+ LINE_SEPARATOR + "You can use this to e.g. ping staff members using <@userId>"
|
||||
+ LINE_SEPARATOR + "If you want to ping roles, you will need to use <@&roleId>");
|
||||
webhook.content = generalConfig.getString("webhook.content", "");
|
||||
|
||||
generalConfig.getYaml().setComment("webhook.embed.footer",
|
||||
"Small footer message of the Discord webhook embed");
|
||||
generalConfig.getYaml().setComment("webhook.embed.footer.text",
|
||||
"Content of the footer message of the Discord webhook embed");
|
||||
webhook.footer.text = generalConfig.getString("webhook.embed.footer.text",
|
||||
"© Jones Development and Sonar Contributors");
|
||||
|
||||
generalConfig.getYaml().setComment("webhook.embed.footer.icon-url",
|
||||
"URL of the footer message icon of the Discord webhook embed");
|
||||
webhook.footer.iconUrl = generalConfig.getString("webhook.embed.footer.icon-url", "");
|
||||
|
||||
final String realEmbedPath = "webhook.embed";
|
||||
final String embedPath = realEmbedPath + ".";
|
||||
|
||||
generalConfig.getYaml().setComment(realEmbedPath,
|
||||
"Embed Discord webhook message that is sent when an attack has stopped");
|
||||
generalConfig.getYaml().setComment(embedPath + "title",
|
||||
"Title of the Discord webhook embed");
|
||||
webhook.embed.title = generalConfig.getString(embedPath + "title", ":white_check_mark: Attack mitigated");
|
||||
|
||||
generalConfig.getYaml().setComment(embedPath + "title-url",
|
||||
"Clickable URL of the title of the Discord webhook embed");
|
||||
webhook.embed.titleUrl = generalConfig.getString(embedPath + "title-url", "");
|
||||
|
||||
generalConfig.getYaml().setComment(embedPath + "description",
|
||||
"Description (content) of the Discord webhook embed");
|
||||
webhook.embed.description = fromList(generalConfig.getStringList(embedPath + "description", Arrays.asList(
|
||||
"The attack on your server has been mitigated.",
|
||||
"",
|
||||
"Attack start: <t:%start-timestamp%:T>",
|
||||
"Attack end: <t:%end-timestamp%:T>",
|
||||
"Attack duration: %duration%",
|
||||
"",
|
||||
"Peak process CPU usage during the attack: %peak-cpu%%",
|
||||
"Peak process memory usage during the attack: %peak-memory%",
|
||||
"Peak bots per second during the attack: %peak-bps%",
|
||||
"",
|
||||
"Blacklisted IP addresses during the attack: %total-blacklisted%",
|
||||
"Failed verifications during the attack: %total-failed%",
|
||||
"Successful verifications during the attack: %total-success%"
|
||||
)), LINE_SEPARATOR);
|
||||
|
||||
generalConfig.getYaml().setComment(embedPath + "color",
|
||||
"RGB colors of the Discord webhook embed"
|
||||
+ LINE_SEPARATOR + "Color picker: https://www.rapidtables.com/web/color/RGB_Color.html");
|
||||
webhook.embed.r = generalConfig.getInt(embedPath + "color.red", 0);
|
||||
webhook.embed.g = generalConfig.getInt(embedPath + "color.green", 255);
|
||||
webhook.embed.b = generalConfig.getInt(embedPath + "color.blue", 0);
|
||||
|
||||
if (!webhook.url.isEmpty()) {
|
||||
if (webhook.username.isEmpty()) {
|
||||
throw new IllegalStateException("Webhook username cannot be empty");
|
||||
}
|
||||
discordWebhook = new DiscordWebhook(webhook.url);
|
||||
} else if (discordWebhook != null) {
|
||||
// Reset if Discord webhooks were disabled
|
||||
discordWebhook = null;
|
||||
}
|
||||
|
||||
// load this here otherwise it could cause issues
|
||||
messagesConfig.getYaml().setComment("header",
|
||||
"Placeholder for every '%header%' in this configuration file");
|
||||
@@ -507,6 +664,33 @@ public final class SonarConfiguration {
|
||||
"%footer%"
|
||||
))));
|
||||
|
||||
messagesConfig.getYaml().setComment("commands.main",
|
||||
"Translations for '/sonar'");
|
||||
messagesConfig.getYaml().setComment("commands.main.header",
|
||||
"Informational message that is shown above everything when running the main command");
|
||||
commands.helpHeader = messagesConfig.getStringList("commands.main.header",
|
||||
Arrays.asList(
|
||||
"<yellow>Running Sonar %version% on %platform%.",
|
||||
"<yellow>(C) %copyright_year% Jones Development and Sonar Contributors",
|
||||
"<green><click:open_url:'https://github.com/jonesdevelopment/sonar'>https://github.com/jonesdevelopment/sonar",
|
||||
"",
|
||||
"<yellow>Need help or have any questions?",
|
||||
"<yellow><click:open_url:'https://jonesdev.xyz/discord/'><hover:show_text:'(Click to open Discord)'>Open a " +
|
||||
"ticket on the Discord </hover></click></yellow><yellow><click:open_url:'https://github" +
|
||||
".com/jonesdevelopment/sonar/issues'><hover:show_text:'(Click to open GitHub)'>or open a new issue on " +
|
||||
"GitHub.",
|
||||
""
|
||||
));
|
||||
messagesConfig.getYaml().setComment("commands.main.subcommands",
|
||||
"Formatting of the list of subcommands shown when running the main command");
|
||||
commands.helpSubcommands = formatString(messagesConfig.getString("commands.main.subcommands",
|
||||
"<click:suggest_command:'/sonar %subcommand% '><hover:show_text:'<gray>Only players: " +
|
||||
"</gray>%only_players%<br><gray>Require console: </gray>%require_console%<br><gray>Permission: " +
|
||||
"</gray><white>%permission%<br><gray>Aliases: </gray>%aliases%'><gray> ▪ </gray><green>/sonar " +
|
||||
"%subcommand%</green><gray> - </gray><white>%description%"));
|
||||
|
||||
SonarCommand.prepareCachedMessages();
|
||||
|
||||
messagesConfig.getYaml().setComment("commands.reload",
|
||||
"Translations for '/sonar reload'");
|
||||
messagesConfig.getYaml().setComment("commands.reload.start",
|
||||
@@ -543,16 +727,6 @@ public final class SonarConfiguration {
|
||||
commands.verboseUnsubscribed = formatString(messagesConfig.getString("commands.verbose.unsubscribed",
|
||||
"%prefix%You are no longer viewing Sonar verbose."));
|
||||
|
||||
messagesConfig.getYaml().setComment("commands.verbose.subscribed-other",
|
||||
"Message that is shown when a player makes another player subscribe to Sonar verbose");
|
||||
commands.verboseSubscribedOther = formatString(messagesConfig.getString("commands.verbose.subscribed-other",
|
||||
"%prefix%%player% is now viewing Sonar verbose."));
|
||||
|
||||
messagesConfig.getYaml().setComment("commands.verbose.unsubscribed-other",
|
||||
"Message that is shown when a player makes another player unsubscribe from Sonar verbose");
|
||||
commands.verboseUnsubscribedOther = formatString(messagesConfig.getString("commands.verbose.unsubscribed-other",
|
||||
"%prefix%%player% is no longer viewing Sonar verbose."));
|
||||
|
||||
messagesConfig.getYaml().setComment("commands.blacklist",
|
||||
"Translations for '/sonar blacklist'");
|
||||
messagesConfig.getYaml().setComment("commands.blacklist.empty",
|
||||
@@ -811,28 +985,29 @@ public final class SonarConfiguration {
|
||||
|
||||
messagesConfig.getYaml().setComment("verbose",
|
||||
"Translations for all messages regarding Sonar's verbose output");
|
||||
messagesConfig.getYaml().setComment("verbose.layout",
|
||||
"General layout for the verbose action-bar" +
|
||||
LINE_SEPARATOR + "Placeholders and their descriptions:" +
|
||||
LINE_SEPARATOR + "- %queued% Number of queued connections" +
|
||||
LINE_SEPARATOR + "- %verifying% Number of verifying connections" +
|
||||
LINE_SEPARATOR + "- %blacklisted% Number of blacklisted IP addresses" +
|
||||
LINE_SEPARATOR + "- %total-joins% Number of total joins (not unique!)" +
|
||||
LINE_SEPARATOR + "- %per-second-joins% Number of joins per second" +
|
||||
LINE_SEPARATOR + "- %verify-total% Number of total verification attempts" +
|
||||
LINE_SEPARATOR + "- %verify-success% Number of verified IP addresses" +
|
||||
LINE_SEPARATOR + "- %verify-failed% Number of failed verifications" +
|
||||
LINE_SEPARATOR + "- %incoming-traffic% Incoming bandwidth usage per second" +
|
||||
LINE_SEPARATOR + "- %outgoing-traffic% Outgoing bandwidth usage per second" +
|
||||
LINE_SEPARATOR + "- %incoming-traffic-ttl% Total incoming bandwidth usage" +
|
||||
LINE_SEPARATOR + "- %outgoing-traffic-ttl% Total outgoing bandwidth usage" +
|
||||
LINE_SEPARATOR + "- %used-memory% Amount of used memory (JVM process)" +
|
||||
LINE_SEPARATOR + "- %total-memory% Amount of total memory (JVM process)" +
|
||||
LINE_SEPARATOR + "- %max-memory% Amount of max memory (JVM process)" +
|
||||
LINE_SEPARATOR + "- %free-memory% Amount of free memory (JVM process)" +
|
||||
LINE_SEPARATOR + "- %animation% Animated spinning circle (by default)"
|
||||
);
|
||||
verbose.actionBarLayout = formatString(messagesConfig.getString("verbose.layout",
|
||||
messagesConfig.getYaml().setComment("verbose.layout.normal",
|
||||
"General layout for the verbose action-bar"
|
||||
+ LINE_SEPARATOR + "Placeholders:"
|
||||
+ LINE_SEPARATOR + "- '%queued%' Number of queued connections"
|
||||
+ LINE_SEPARATOR + "- '%verifying%' Number of verifying connections"
|
||||
+ LINE_SEPARATOR + "- '%blacklisted%' Number of blacklisted IP addresses"
|
||||
+ LINE_SEPARATOR + "- '%total-joins%' Number of total attempted joins"
|
||||
+ LINE_SEPARATOR + "- '%verify-total%' Number of total verification attempts"
|
||||
+ LINE_SEPARATOR + "- '%verify-success%' Number of verified IP addresses"
|
||||
+ LINE_SEPARATOR + "- '%verify-failed%' Number of failed verifications"
|
||||
+ LINE_SEPARATOR + "- '%logins-per-second%' Number of logins per second"
|
||||
+ LINE_SEPARATOR + "- '%attack-duration%' Duration of the current attack"
|
||||
+ LINE_SEPARATOR + "- '%incoming-traffic%' Incoming bandwidth usage per second"
|
||||
+ LINE_SEPARATOR + "- '%outgoing-traffic%' Outgoing bandwidth usage per second"
|
||||
+ LINE_SEPARATOR + "- '%incoming-traffic-ttl%' Total incoming bandwidth usage"
|
||||
+ LINE_SEPARATOR + "- '%outgoing-traffic-ttl%' Total outgoing bandwidth usage"
|
||||
+ LINE_SEPARATOR + "- '%used-memory%' Amount of used memory of the process"
|
||||
+ LINE_SEPARATOR + "- '%total-memory%' Amount of total memory of the process"
|
||||
+ LINE_SEPARATOR + "- '%max-memory%' Amount of max memory of the process"
|
||||
+ LINE_SEPARATOR + "- '%free-memory%' Amount of free memory of the process"
|
||||
+ LINE_SEPARATOR + "- '%animation%' Customizable animated symbol"
|
||||
+ LINE_SEPARATOR + "Translations for Sonar's normal verbose output");
|
||||
verbose.actionBarLayout = formatString(messagesConfig.getString("verbose.layout.normal",
|
||||
String.join(" <dark_aqua>╺ ", Arrays.asList(
|
||||
"%prefix%<gray>Queued <white>%queued%",
|
||||
"<gray>Verifying <white>%verifying%",
|
||||
@@ -840,12 +1015,22 @@ public final class SonarConfiguration {
|
||||
" <dark_aqua>| <green>⬆ <white>%outgoing-traffic%/s <red>⬇ <white>%incoming-traffic%/s" +
|
||||
" <green><bold>%animation%<reset>"
|
||||
))));
|
||||
messagesConfig.getYaml().setComment("verbose.animation",
|
||||
"Alternative symbols:"
|
||||
+ LINE_SEPARATOR + "- ▙"
|
||||
+ LINE_SEPARATOR + "- ▛"
|
||||
+ LINE_SEPARATOR + "- ▜"
|
||||
+ LINE_SEPARATOR + "- ▟");
|
||||
messagesConfig.getYaml().setComment("verbose.layout.attack",
|
||||
"Translations for Sonar's verbose output during an active attack");
|
||||
verbose.actionBarLayoutDuringAttack = formatString(messagesConfig.getString("verbose.layout.attack",
|
||||
String.join(" <dark_aqua>╺ ", Arrays.asList(
|
||||
"%prefix%<gray>BPS <white>%logins-per-second%",
|
||||
"<gray>Queued <white>%queued%",
|
||||
"<gray>Verifying <white>%verifying%",
|
||||
"<gray>Blacklisted <white>%blacklisted%",
|
||||
"<gray>Duration <white>%attack-duration%" +
|
||||
" <dark_aqua>| <green>⬆ <white>%outgoing-traffic%/s <red>⬇ <white>%incoming-traffic%/s" +
|
||||
" <green><bold>%animation%<reset>"
|
||||
))));
|
||||
messagesConfig.getYaml().setComment("verbose.animation", "Animation for the action bar"
|
||||
+ LINE_SEPARATOR + "Alternatives:"
|
||||
+ LINE_SEPARATOR + "- ▙, ▛, ▜, ▟"
|
||||
+ LINE_SEPARATOR + "- ⬈, ⬊, ⬋, ⬉");
|
||||
verbose.animation = Collections.unmodifiableList(messagesConfig.getStringList("verbose.animation",
|
||||
Arrays.asList("◜", "◝", "◞", "◟")
|
||||
));
|
||||
@@ -866,7 +1051,11 @@ public final class SonarConfiguration {
|
||||
}
|
||||
|
||||
private @NotNull String fromList(final @NotNull Collection<String> list) {
|
||||
return formatString(String.join("<newline>", list));
|
||||
return fromList(list, "<newline>");
|
||||
}
|
||||
|
||||
private @NotNull String fromList(final @NotNull Collection<String> list, final String newline) {
|
||||
return formatString(String.join(newline, list));
|
||||
}
|
||||
|
||||
private static @NotNull Component deserialize(final String legacy) {
|
||||
|
||||
+12
-2
@@ -65,6 +65,8 @@ public final class VerifiedPlayerController {
|
||||
TableUtils.createTableIfNotExists(connectionSource, VerifiedPlayer.class);
|
||||
} catch (SQLException exception) {
|
||||
// Duplicate index
|
||||
// I know this isn't the best method of handling it,
|
||||
// but I don't know how else I could address this issue.
|
||||
}
|
||||
|
||||
dao = DaoManager.createDao(connectionSource, VerifiedPlayer.class);
|
||||
@@ -92,7 +94,11 @@ public final class VerifiedPlayerController {
|
||||
}
|
||||
|
||||
DB_UPDATE_SERVICE.execute(() -> {
|
||||
Objects.requireNonNull(connectionSource);
|
||||
// We cannot throw a NullPointerException within the executor service
|
||||
// because we want to handle the error instead of simply throwing an exception
|
||||
if (connectionSource == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
final List<VerifiedPlayer> verifiedPlayer = queryBuilder.where()
|
||||
@@ -134,7 +140,11 @@ public final class VerifiedPlayerController {
|
||||
}
|
||||
|
||||
DB_UPDATE_SERVICE.execute(() -> {
|
||||
Objects.requireNonNull(connectionSource);
|
||||
// We cannot throw a NullPointerException within the executor service
|
||||
// because we want to handle the error instead of simply throwing an exception
|
||||
if (connectionSource == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
dao.create(player);
|
||||
|
||||
@@ -38,8 +38,9 @@ public enum Dependency {
|
||||
MYSQL(
|
||||
"com/mysql",
|
||||
"mysql-connector-j",
|
||||
"8.1.0"
|
||||
"8.2.0"
|
||||
),
|
||||
// TODO: MariaDB still seems to have some issues; investigate!
|
||||
// https://mvnrepository.com/artifact/org.mariadb.jdbc/mariadb-java-client
|
||||
MARIADB(
|
||||
"org/mariadb/jdbc",
|
||||
@@ -47,10 +48,8 @@ public enum Dependency {
|
||||
"3.1.4"
|
||||
);
|
||||
|
||||
private @NotNull
|
||||
final Path tempFilePath;
|
||||
private @NotNull
|
||||
final URL mvnRepoURL;
|
||||
private final Path tempFilePath;
|
||||
private final URL mvnRepoURL;
|
||||
|
||||
Dependency(final @NotNull String groupId,
|
||||
final @NotNull String artifactId,
|
||||
|
||||
@@ -22,6 +22,7 @@ import com.j256.ormlite.support.ConnectionSource;
|
||||
import lombok.experimental.UtilityClass;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.SonarPlatform;
|
||||
import xyz.jonesdev.sonar.api.config.SonarConfiguration;
|
||||
|
||||
import java.io.File;
|
||||
@@ -36,44 +37,42 @@ import java.util.Properties;
|
||||
@UtilityClass
|
||||
public class DependencyLoader {
|
||||
public ConnectionSource setUpDriverAndConnect() throws Throwable {
|
||||
final SonarConfiguration config = Sonar.get().getConfig();
|
||||
final SonarConfiguration.Database database = Sonar.get().getConfig().getDatabase();
|
||||
|
||||
final URL[] urls = new URL[config.getDatabase().getType().getDependencies().length];
|
||||
for (final Dependency dependency : config.getDatabase().getType().getDependencies()) {
|
||||
final URL url = dependency.getClassLoaderURL();
|
||||
final ClassLoader currentClassLoader = DependencyLoader.class.getClassLoader();
|
||||
final ClassLoader currentClassLoader = DependencyLoader.class.getClassLoader();
|
||||
ClassLoader classLoader = currentClassLoader;
|
||||
|
||||
// Velocity doesn't include the JDBC driver, which is why we need to load it manually
|
||||
if (Sonar.get().getPlatform() == SonarPlatform.VELOCITY) {
|
||||
final URL url = database.getType().getDependency().getClassLoaderURL();
|
||||
|
||||
final Method addPath = currentClassLoader.getClass().getDeclaredMethod("addPath", Path.class);
|
||||
addPath.setAccessible(true);
|
||||
addPath.invoke(currentClassLoader, new File(url.toURI()).toPath());
|
||||
|
||||
urls[dependency.ordinal()] = url;
|
||||
classLoader = new ExternalClassLoader(new URL[]{url});
|
||||
}
|
||||
|
||||
final String type = config.getDatabase().getType().name().toLowerCase();
|
||||
final String databaseURL = String.format("jdbc:%s://%s:%d/%s",
|
||||
type, config.getDatabase().getUrl(), config.getDatabase().getPort(), config.getDatabase().getName());
|
||||
final String jdbcURL = String.format("jdbc:%s://%s:%d/%s",
|
||||
database.getType().name().toLowerCase(), database.getUrl(), database.getPort(), database.getName());
|
||||
|
||||
final ExternalClassLoader classLoader = new ExternalClassLoader(urls);
|
||||
final Connection connection = connect(classLoader, databaseURL, config.getDatabase());
|
||||
return new JdbcSingleConnectionSource(databaseURL, connection);
|
||||
final Connection connection = connect(classLoader, jdbcURL, database);
|
||||
return new JdbcSingleConnectionSource(jdbcURL, connection);
|
||||
}
|
||||
|
||||
// Mostly taken from
|
||||
// https://github.com/Elytrium/LimboAuth/blob/master/src/main/java/net/elytrium/limboauth/dependencies/DatabaseLibrary.java#L134
|
||||
// https://github.com/Elytrium/LimboAuth/blob/master/src/main/java/net/elytrium/limboauth/dependencies/DatabaseLibrary.java#L137
|
||||
private Connection connect(final @NotNull ClassLoader classLoader,
|
||||
final @NotNull String databaseURL,
|
||||
final @NotNull SonarConfiguration.Database config) throws Throwable {
|
||||
final Class<?> driverClass = classLoader.loadClass(config.getType().getDriverClassName());
|
||||
final @NotNull SonarConfiguration.Database database) throws Throwable {
|
||||
final Class<?> driverClass = classLoader.loadClass(database.getType().getDriverClassName());
|
||||
final Object driver = driverClass.getDeclaredConstructor().newInstance();
|
||||
|
||||
DriverManager.registerDriver((Driver) driver);
|
||||
|
||||
final Properties properties = new Properties();
|
||||
properties.put("user", config.getUsername());
|
||||
if (!config.getPassword().isEmpty()) {
|
||||
properties.put("password", config.getPassword());
|
||||
}
|
||||
properties.put("user", database.getUsername());
|
||||
properties.put("password", database.getPassword());
|
||||
|
||||
final Method connect = driverClass.getDeclaredMethod("connect", String.class, Properties.class);
|
||||
connect.setAccessible(true);
|
||||
|
||||
@@ -21,24 +21,40 @@ import lombok.AccessLevel;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Vector;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public final class SonarEventManager {
|
||||
public static final SonarEventManager INSTANCE = new SonarEventManager();
|
||||
private static final Collection<SonarEventListener> EVENT_LISTENERS = new Vector<>(0);
|
||||
private static final ExecutorService EVENT_SERVICE = Executors.newSingleThreadExecutor();
|
||||
|
||||
/**
|
||||
* Publishes the given event to all listeners
|
||||
*/
|
||||
@ApiStatus.Internal
|
||||
public void publish(final @NotNull SonarEvent event) {
|
||||
for (final SonarEventListener eventListener : EVENT_LISTENERS) {
|
||||
eventListener.handle(event);
|
||||
// Don't post a task if there are no listeners
|
||||
if (EVENT_LISTENERS.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
EVENT_SERVICE.execute(() -> {
|
||||
for (final SonarEventListener eventListener : EVENT_LISTENERS) {
|
||||
try {
|
||||
eventListener.handle(event);
|
||||
} catch (Throwable throwable) {
|
||||
Sonar.get().getLogger().error("Could not pass {} to listener: {}",
|
||||
event.getClass().getSimpleName(), throwable);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -47,6 +63,7 @@ public final class SonarEventManager {
|
||||
* @param listeners One (or more) listeners to register
|
||||
* @see #unregisterListener(SonarEventListener...) Unregister an event listener
|
||||
*/
|
||||
@SuppressWarnings("unused") // External API usage
|
||||
public synchronized void registerListener(final @NotNull SonarEventListener... listeners) {
|
||||
EVENT_LISTENERS.addAll(Arrays.asList(listeners));
|
||||
}
|
||||
@@ -58,6 +75,7 @@ public final class SonarEventManager {
|
||||
* @apiNote This does not have any effect if the given listeners are not registered
|
||||
* @see #registerListener(SonarEventListener...) Register an event listener
|
||||
*/
|
||||
@SuppressWarnings("unused") // External API usage
|
||||
public synchronized void unregisterListener(final @NotNull SonarEventListener... listeners) {
|
||||
EVENT_LISTENERS.removeAll(Arrays.asList(listeners));
|
||||
}
|
||||
|
||||
+3
-21
@@ -15,27 +15,9 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.bukkit.verbose;
|
||||
package xyz.jonesdev.sonar.api.event.impl;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import org.bukkit.Server;
|
||||
import org.bukkit.entity.Player;
|
||||
import xyz.jonesdev.sonar.api.verbose.Verbose;
|
||||
import xyz.jonesdev.sonar.api.event.SonarEvent;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public final class ActionBarVerbose extends Verbose {
|
||||
private final Server server;
|
||||
|
||||
@Override
|
||||
public void broadcast(final Component component) {
|
||||
synchronized (subscribers) {
|
||||
for (final String subscriber : subscribers) {
|
||||
final Player player = server.getPlayer(subscriber);
|
||||
if (player != null) {
|
||||
// TODO: use an adventure audience
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
public final class AttackDetectedEvent implements SonarEvent {
|
||||
}
|
||||
+9
-18
@@ -15,28 +15,19 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.api.server;
|
||||
package xyz.jonesdev.sonar.api.event.impl;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import xyz.jonesdev.sonar.api.SonarPlatform;
|
||||
import xyz.jonesdev.sonar.api.command.InvocationSource;
|
||||
|
||||
import java.util.Optional;
|
||||
import lombok.ToString;
|
||||
import xyz.jonesdev.sonar.api.attack.AttackTracker;
|
||||
import xyz.jonesdev.sonar.api.event.SonarEvent;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@EqualsAndHashCode
|
||||
@RequiredArgsConstructor
|
||||
public abstract class ServerWrapper {
|
||||
|
||||
/**
|
||||
* Platform of the server (Velocity, BungeeCord, Spigot)
|
||||
*/
|
||||
private final SonarPlatform platform;
|
||||
|
||||
/**
|
||||
* @param username Username of the player
|
||||
* @return Optional player wrapped as InvocationSender
|
||||
* @see InvocationSource
|
||||
*/
|
||||
public abstract Optional<InvocationSource> getOnlinePlayer(final String username);
|
||||
public final class AttackMitigatedEvent implements SonarEvent {
|
||||
private final AttackTracker.AttackStatistics attackStatistics;
|
||||
}
|
||||
+10
-3
@@ -19,16 +19,23 @@ package xyz.jonesdev.sonar.api.event.impl;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.ToString;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import xyz.jonesdev.sonar.api.event.SonarEvent;
|
||||
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@EqualsAndHashCode
|
||||
@RequiredArgsConstructor
|
||||
public final class UserVerifyFailedEvent implements SonarEvent {
|
||||
private final FallbackUser<?, ?> user;
|
||||
private final String reason;
|
||||
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
|
||||
private final Optional<String> reason;
|
||||
|
||||
public UserVerifyFailedEvent(final FallbackUser<?, ?> user, final @Nullable String reason) {
|
||||
this.user = user;
|
||||
this.reason = Optional.ofNullable(reason);
|
||||
}
|
||||
}
|
||||
@@ -20,10 +20,12 @@ package xyz.jonesdev.sonar.api.fallback;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.val;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.cappuccino.Cappuccino;
|
||||
import xyz.jonesdev.cappuccino.ExpiringCache;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.config.SonarConfiguration;
|
||||
import xyz.jonesdev.sonar.api.logger.LoggerWrapper;
|
||||
|
||||
import java.net.InetAddress;
|
||||
@@ -41,7 +43,7 @@ public final class Fallback {
|
||||
private final Map<String, InetAddress> connected = new ConcurrentHashMap<>();
|
||||
// Only block the player for a few minutes to avoid issues
|
||||
private final ExpiringCache<String> blacklisted = Cappuccino.buildExpiring(
|
||||
10L, TimeUnit.MINUTES, 2500L
|
||||
10L, TimeUnit.MINUTES, 5000L
|
||||
);
|
||||
private final @NotNull FallbackQueue queue = FallbackQueue.INSTANCE;
|
||||
private final @NotNull FallbackRatelimiter ratelimiter = FallbackRatelimiter.INSTANCE;
|
||||
@@ -64,8 +66,11 @@ public final class Fallback {
|
||||
}
|
||||
};
|
||||
|
||||
public boolean isPotentiallyUnderAttack() {
|
||||
final int min = SONAR.getConfig().getMinPlayersForAttack();
|
||||
return connected.size() > min || queue.getQueuedPlayers().size() > min;
|
||||
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
|
||||
public boolean shouldVerifyNewPlayers() {
|
||||
val timing = Sonar.get().getConfig().getVerification().getTiming();
|
||||
return timing == SonarConfiguration.Verification.Timing.ALWAYS
|
||||
|| (timing == SonarConfiguration.Verification.Timing.DURING_ATTACK
|
||||
&& Sonar.get().getAttackTracker().isCurrentlyUnderAttack());
|
||||
}
|
||||
}
|
||||
@@ -50,14 +50,14 @@ public final class FallbackQueue {
|
||||
}
|
||||
|
||||
public void poll() {
|
||||
final int max = Sonar.get().getConfig().getQueue().getMaxQueuePolls();
|
||||
final int maxQueuePolls = Sonar.get().getConfig().getQueue().getMaxQueuePolls();
|
||||
int index = 0;
|
||||
|
||||
// We need to be very careful here since we don't want any concurrency issues.
|
||||
// We need to be cautious here since we don't want any concurrency issues.
|
||||
final Iterator<Map.Entry<InetAddress, Runnable>> iterator = queuedPlayers.entrySet().iterator();
|
||||
while (iterator.hasNext()) {
|
||||
// Break if we reached our maximum entries
|
||||
if (++index > max) {
|
||||
if (++index > maxQueuePolls) {
|
||||
break;
|
||||
}
|
||||
// Run the cached runnable
|
||||
@@ -66,5 +66,8 @@ public final class FallbackQueue {
|
||||
// Remove runnable from iterator
|
||||
iterator.remove();
|
||||
}
|
||||
|
||||
// Run the attack check task
|
||||
Sonar.get().getAttackTracker().checkIfUnderAttack();
|
||||
}
|
||||
}
|
||||
@@ -37,6 +37,7 @@ public final class FallbackRatelimiter {
|
||||
* @param inetAddress IP address of the player
|
||||
* @return Whether the player is allowed to verify
|
||||
*/
|
||||
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
|
||||
public boolean attempt(final @NotNull InetAddress inetAddress) {
|
||||
// Clean up the cache
|
||||
expiringCache.cleanUp(false);
|
||||
|
||||
@@ -92,9 +92,6 @@ public interface FallbackUser<X, Y> {
|
||||
*/
|
||||
default void fail(final @Nullable String reason) {
|
||||
if (getChannel().isActive()) {
|
||||
// Call the VerifyFailedEvent for external API usage
|
||||
Sonar.get().getEventManager().publish(new UserVerifyFailedEvent(this, reason));
|
||||
|
||||
disconnect(Sonar.get().getConfig().getVerification().getVerificationFailed());
|
||||
|
||||
if (reason != null) {
|
||||
@@ -107,6 +104,9 @@ public interface FallbackUser<X, Y> {
|
||||
|
||||
Statistics.FAILED_VERIFICATIONS.increment();
|
||||
|
||||
// Call the VerifyFailedEvent for external API usage
|
||||
Sonar.get().getEventManager().publish(new UserVerifyFailedEvent(this, reason));
|
||||
|
||||
// Make sure old entries are removed
|
||||
PREVIOUS_FAILS.cleanUp();
|
||||
|
||||
|
||||
+9
-5
@@ -20,10 +20,7 @@ package xyz.jonesdev.sonar.api.fallback.protocol;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import java.util.EnumSet;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.*;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public enum ProtocolVersion {
|
||||
@@ -73,6 +70,7 @@ public enum ProtocolVersion {
|
||||
private final int protocol;
|
||||
|
||||
public static final ProtocolVersion MINIMUM_VERSION = MINECRAFT_1_7_2;
|
||||
public static final ProtocolVersion LATEST_VERSION;
|
||||
public static final Map<Integer, ProtocolVersion> ID_TO_PROTOCOL_CONSTANT;
|
||||
public static final Set<ProtocolVersion> SUPPORTED_VERSIONS;
|
||||
|
||||
@@ -96,7 +94,8 @@ public enum ProtocolVersion {
|
||||
}
|
||||
}
|
||||
|
||||
SUPPORTED_VERSIONS = versions;
|
||||
SUPPORTED_VERSIONS = Collections.unmodifiableSet(versions);
|
||||
LATEST_VERSION = new ArrayList<>(versions).get(versions.size() - 1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,10 +103,15 @@ public enum ProtocolVersion {
|
||||
return ID_TO_PROTOCOL_CONSTANT.getOrDefault(protocol, UNKNOWN);
|
||||
}
|
||||
|
||||
public boolean inBetween(final ProtocolVersion first, final ProtocolVersion last) {
|
||||
return compareTo(first) >= 0 && compareTo(last) <= 0;
|
||||
}
|
||||
|
||||
public boolean isUnknown() {
|
||||
return this == UNKNOWN;
|
||||
}
|
||||
|
||||
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
|
||||
public boolean isLegacy() {
|
||||
return this == LEGACY;
|
||||
}
|
||||
|
||||
@@ -17,6 +17,10 @@
|
||||
|
||||
package xyz.jonesdev.sonar.api.logger;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.lang.reflect.Array;
|
||||
|
||||
/**
|
||||
* The whole purpose of doing this is, so we can have one logger for every module.
|
||||
* We wouldn't need to get the plugin logger every time we try to use it.
|
||||
@@ -28,4 +32,62 @@ public interface LoggerWrapper {
|
||||
void warn(final String message, final Object... args);
|
||||
|
||||
void error(final String message, final Object... args);
|
||||
|
||||
// Taken from
|
||||
// https://github.com/j256/ormlite-core/blob/master/src/main/java/com/j256/ormlite/logger/Logger.java
|
||||
String ARG_STRING = "{}";
|
||||
int ARG_STRING_LENGTH = ARG_STRING.length();
|
||||
Object UNKNOWN_ARG = new Object();
|
||||
|
||||
default String buildFullMessage(final @NotNull String msg, final Object... args) {
|
||||
StringBuilder sb = null;
|
||||
int lastIndex = 0;
|
||||
int argC = 0;
|
||||
while (true) {
|
||||
int argIndex = msg.indexOf(ARG_STRING, lastIndex);
|
||||
// no more {} arguments?
|
||||
if (argIndex == -1) {
|
||||
break;
|
||||
}
|
||||
if (sb == null) {
|
||||
// we build this lazily in case there is no {} in the msg
|
||||
sb = new StringBuilder(128);
|
||||
}
|
||||
// add the string before the arg-string
|
||||
sb.append(msg, lastIndex, argIndex);
|
||||
// shift our last-index past the arg-string
|
||||
lastIndex = argIndex + ARG_STRING_LENGTH;
|
||||
// add the arguments
|
||||
if (argC < args.length) {
|
||||
appendArg(sb, args[argC]);
|
||||
}
|
||||
argC++;
|
||||
}
|
||||
if (sb == null) {
|
||||
return msg;
|
||||
} else {
|
||||
sb.append(msg, lastIndex, msg.length());
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
|
||||
default void appendArg(final StringBuilder stringBuilder, final Object arg) {
|
||||
if (arg == UNKNOWN_ARG) {
|
||||
// ignore it
|
||||
} else if (arg == null) {
|
||||
stringBuilder.append("null");
|
||||
} else if (arg.getClass().isArray()) {
|
||||
stringBuilder.append('[');
|
||||
int length = Array.getLength(arg);
|
||||
for (int i = 0; i < length; i++) {
|
||||
if (i > 0) {
|
||||
stringBuilder.append(", ");
|
||||
}
|
||||
appendArg(stringBuilder, Array.get(arg, i));
|
||||
}
|
||||
stringBuilder.append(']');
|
||||
} else {
|
||||
stringBuilder.append(arg);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,12 +24,13 @@ import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.ToString;
|
||||
|
||||
import java.sql.Timestamp;
|
||||
import java.util.UUID;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||
@DatabaseTable(tableName = "verified_players")
|
||||
@DatabaseTable(tableName = "sonar_verified_players")
|
||||
public final class VerifiedPlayer {
|
||||
@SuppressWarnings("unused")
|
||||
@DatabaseField(generatedId = true)
|
||||
@@ -51,12 +52,15 @@ public final class VerifiedPlayer {
|
||||
)
|
||||
private UUID playerUUID;
|
||||
|
||||
@DatabaseField(columnName = "timestamp", canBeNull = false)
|
||||
private long timestamp;
|
||||
@DatabaseField(
|
||||
columnName = "timestamp",
|
||||
canBeNull = false
|
||||
)
|
||||
private Timestamp timestamp;
|
||||
|
||||
public VerifiedPlayer(final String inetAddress, final UUID playerUUID, final long timestamp) {
|
||||
this.inetAddress = inetAddress;
|
||||
this.playerUUID = playerUUID;
|
||||
this.timestamp = timestamp;
|
||||
this.timestamp = new Timestamp(timestamp);
|
||||
}
|
||||
}
|
||||
@@ -26,11 +26,16 @@ public interface JVMProfiler {
|
||||
Runtime RUNTIME = Runtime.getRuntime();
|
||||
OperatingSystemMXBean OPERATING_SYSTEM_MX_BEAN = (OperatingSystemMXBean) ManagementFactory.getOperatingSystemMXBean();
|
||||
RuntimeMXBean RUNTIME_MX_BEAN = ManagementFactory.getRuntimeMXBean();
|
||||
char[] MEMORY_UNITS = {'k', 'M', 'G', 'T', 'P', 'E'};
|
||||
|
||||
default String formatMemory(final long m) {
|
||||
if (m < 1024L) return m + " B";
|
||||
final int z = (63 - Long.numberOfLeadingZeros(m)) / 10;
|
||||
return String.format("%.1f %sB", (double) m / (1L << (z * 10)), " KMGTPE".charAt(z));
|
||||
default String formatMemory(final long size) {
|
||||
if (size < 1024L) {
|
||||
return size + " B";
|
||||
}
|
||||
final int group = (63 - Long.numberOfLeadingZeros(size)) / 10;
|
||||
final double formattedSize = (double) size / (1L << (group * 10));
|
||||
final char unit = MEMORY_UNITS[group - 1];
|
||||
return String.format("%.1f %sB", formattedSize, unit);
|
||||
}
|
||||
|
||||
default int getVirtualCores() {
|
||||
|
||||
@@ -18,9 +18,14 @@
|
||||
package xyz.jonesdev.sonar.api.timer;
|
||||
|
||||
import lombok.Getter;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
|
||||
@Getter
|
||||
public final class SystemTimer {
|
||||
private static final SimpleDateFormat FORMATTER = new SimpleDateFormat("mm:ss");
|
||||
|
||||
private long start = System.currentTimeMillis();
|
||||
|
||||
public void reset() {
|
||||
@@ -35,6 +40,10 @@ public final class SystemTimer {
|
||||
return delay() >= amount;
|
||||
}
|
||||
|
||||
public @NotNull String formattedDelay() {
|
||||
return FORMATTER.format(delay());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("%.3f", delay() / 1000D);
|
||||
|
||||
@@ -19,59 +19,71 @@ package xyz.jonesdev.sonar.api.verbose;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.val;
|
||||
import net.kyori.adventure.audience.Audience;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.minimessage.MiniMessage;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.cappuccino.Cappuccino;
|
||||
import xyz.jonesdev.cappuccino.ExpiringCache;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.attack.AttackTracker;
|
||||
import xyz.jonesdev.sonar.api.profiler.JVMProfiler;
|
||||
import xyz.jonesdev.sonar.api.statistics.Statistics;
|
||||
import xyz.jonesdev.sonar.api.timer.SystemTimer;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
import java.util.Vector;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.Sonar.DECIMAL_FORMAT;
|
||||
import static xyz.jonesdev.sonar.api.fallback.traffic.TrafficCounter.INCOMING;
|
||||
import static xyz.jonesdev.sonar.api.fallback.traffic.TrafficCounter.OUTGOING;
|
||||
|
||||
@Getter
|
||||
public abstract class Verbose implements JVMProfiler {
|
||||
protected final @NotNull Collection<String> subscribers = new Vector<>(0);
|
||||
private final SystemTimer secondTimer = new SystemTimer();
|
||||
protected int joinsPerSecond, totalJoins;
|
||||
private int lastTotalJoins, animationIndex;
|
||||
public final class Verbose implements JVMProfiler {
|
||||
private final @NotNull Collection<String> subscribers = new Vector<>(0);
|
||||
private final @NotNull Map<String, Audience> audiences = new ConcurrentHashMap<>();
|
||||
private int animationIndex;
|
||||
private final ExpiringCache<Long> loginsPerSecond = Cappuccino.buildExpiring(1L, TimeUnit.SECONDS);
|
||||
|
||||
// Run action bar verbose
|
||||
public final void update() {
|
||||
public void update() {
|
||||
// Clean up all blacklisted IPs
|
||||
Sonar.get().getFallback().getBlacklisted().cleanUp(false);
|
||||
loginsPerSecond.cleanUp(false);
|
||||
|
||||
totalJoins = Statistics.TOTAL_TRAFFIC.get();
|
||||
|
||||
// Statistically determine the joins per second without any caches
|
||||
if (totalJoins > 0 && secondTimer.elapsed(1000L)) {
|
||||
secondTimer.reset();
|
||||
joinsPerSecond = totalJoins - lastTotalJoins;
|
||||
lastTotalJoins = totalJoins;
|
||||
}
|
||||
|
||||
// Don't prepare component if there are no subscribers
|
||||
if (subscribers.isEmpty()) return;
|
||||
// Prepare the action bar format component
|
||||
final Component component = prepareActionBarFormat();
|
||||
// Send the action bar to all online players
|
||||
broadcast(prepareActionBarFormat());
|
||||
for (final String subscriber : subscribers) {
|
||||
final Audience audience = audiences.get(subscriber);
|
||||
if (audience == null) continue;
|
||||
audience.sendActionBar(component);
|
||||
}
|
||||
}
|
||||
|
||||
protected @NotNull Component prepareActionBarFormat() {
|
||||
return MiniMessage.miniMessage().deserialize(Sonar.get().getConfig().getVerbose().getActionBarLayout()
|
||||
public @NotNull Component prepareActionBarFormat() {
|
||||
final AttackTracker.AttackStatistics attackStatistics = Sonar.get().getAttackTracker().getCurrentAttack();
|
||||
final SystemTimer attackDuration = attackStatistics == null ? null : attackStatistics.getDuration();
|
||||
return MiniMessage.miniMessage().deserialize((attackDuration != null
|
||||
? Sonar.get().getConfig().getVerbose().getActionBarLayoutDuringAttack()
|
||||
: Sonar.get().getConfig().getVerbose().getActionBarLayout())
|
||||
.replace("%queued%",
|
||||
DECIMAL_FORMAT.format(Sonar.get().getFallback().getQueue().getQueuedPlayers().size()))
|
||||
.replace("%verifying%", DECIMAL_FORMAT.format(Sonar.get().getFallback().getConnected().size()))
|
||||
.replace("%blacklisted%",
|
||||
DECIMAL_FORMAT.format(Sonar.get().getFallback().getBlacklisted().estimatedSize()))
|
||||
.replace("%total-joins%", DECIMAL_FORMAT.format(totalJoins))
|
||||
.replace("%per-second-joins%", DECIMAL_FORMAT.format(joinsPerSecond))
|
||||
.replace("%total-joins%", DECIMAL_FORMAT.format(Statistics.TOTAL_TRAFFIC.get()))
|
||||
.replace("%logins-per-second%", DECIMAL_FORMAT.format(loginsPerSecond.estimatedSize()))
|
||||
.replace("%verify-total%", DECIMAL_FORMAT.format(Statistics.REAL_TRAFFIC.get()))
|
||||
.replace("%verify-success%",
|
||||
DECIMAL_FORMAT.format(Sonar.get().getVerifiedPlayerController().estimatedSize()))
|
||||
.replace("%verify-failed%", DECIMAL_FORMAT.format(Statistics.FAILED_VERIFICATIONS.get()))
|
||||
.replace("%attack-duration%", attackDuration == null ? "00:00" : attackDuration.formattedDelay())
|
||||
.replace("%incoming-traffic%", INCOMING.getCachedSecond())
|
||||
.replace("%outgoing-traffic%", OUTGOING.getCachedSecond())
|
||||
.replace("%incoming-traffic-ttl%", INCOMING.getCachedTtl())
|
||||
@@ -83,34 +95,31 @@ public abstract class Verbose implements JVMProfiler {
|
||||
.replace("%animation%", nextAnimation()));
|
||||
}
|
||||
|
||||
protected final String nextAnimation() {
|
||||
public String nextAnimation() {
|
||||
val animations = Sonar.get().getConfig().getVerbose().getAnimation();
|
||||
final int nextIndex = ++animationIndex % animations.size();
|
||||
return animations.toArray(new String[0])[nextIndex];
|
||||
}
|
||||
|
||||
// Run action bar verbose
|
||||
protected abstract void broadcast(final Component component);
|
||||
|
||||
/**
|
||||
* @param subscriber Name of the player who subscribed
|
||||
* @return Whether the player is subscribed or not
|
||||
* @param name Name of the audience
|
||||
* @return Whether the audience is subscribed or not
|
||||
*/
|
||||
public final boolean isSubscribed(final @NotNull String subscriber) {
|
||||
return subscribers.contains(subscriber);
|
||||
public boolean isSubscribed(final @NotNull String name) {
|
||||
return subscribers.contains(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param subscriber Name of the player to subscribe
|
||||
* @param name Name of the audience to subscribe
|
||||
*/
|
||||
public final void subscribe(final @NotNull String subscriber) {
|
||||
subscribers.add(subscriber);
|
||||
public void subscribe(final String name) {
|
||||
subscribers.add(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param subscriber Name of the player to unsubscribe
|
||||
* @param name Name of the audience to unsubscribe
|
||||
*/
|
||||
public final void unsubscribe(final @NotNull String subscriber) {
|
||||
subscribers.remove(subscriber);
|
||||
public void unsubscribe(final String name) {
|
||||
subscribers.remove(name);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.api.webhook;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.config.SonarConfiguration;
|
||||
|
||||
import javax.net.ssl.HttpsURLConnection;
|
||||
import java.io.OutputStream;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public final class DiscordWebhook {
|
||||
private final String url;
|
||||
|
||||
private static final ExecutorService HTTP_REQUEST_SERVICE = Executors.newSingleThreadExecutor();
|
||||
|
||||
public void post(final Supplier<SonarConfiguration.Webhook.Embed> embed) {
|
||||
HTTP_REQUEST_SERVICE.execute(() -> {
|
||||
try {
|
||||
final String serializedContent = prepareSerializedContent(embed.get());
|
||||
final HttpsURLConnection urlConnection = prepareConnection(new URL(url), serializedContent);
|
||||
|
||||
try (final OutputStream outputStream = urlConnection.getOutputStream()) {
|
||||
outputStream.write(serializedContent.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
// Check if the connection was a success
|
||||
if (urlConnection.getResponseCode() != 204) {
|
||||
throw new IllegalStateException("Unexpected response code " + urlConnection.getResponseCode());
|
||||
}
|
||||
} catch (Exception exception) {
|
||||
Sonar.get().getLogger().error("Could not send webhook: {}", exception);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private String prepareSerializedContent(final SonarConfiguration.Webhook.@NotNull Embed embed) {
|
||||
final String content = Sonar.get().getConfig().getWebhook().getContent();
|
||||
final String username = Sonar.get().getConfig().getWebhook().getUsername();
|
||||
final String avatarUrl = Sonar.get().getConfig().getWebhook().getAvatarUrl();
|
||||
|
||||
int rgb = embed.getR();
|
||||
rgb = (rgb << 8) + embed.getG();
|
||||
rgb = (rgb << 8) + embed.getB();
|
||||
|
||||
final Webhook.EmbedMessage embedMessage = new Webhook.EmbedMessage(
|
||||
embed.getTitle(), embed.getDescription(), embed.getTitleUrl(), rgb, new Webhook.Field[0],
|
||||
new Webhook.Footer(Sonar.get().getConfig().getWebhook().getFooter().getText(),
|
||||
Sonar.get().getConfig().getWebhook().getFooter().getIconUrl()));
|
||||
final List<Webhook.EmbedMessage> embeds = Collections.singletonList(embedMessage);
|
||||
|
||||
final Webhook webhook = new Webhook(content, username, avatarUrl, false, embeds);
|
||||
return Sonar.GENERAL_GSON.toJson(webhook);
|
||||
}
|
||||
|
||||
private @NotNull HttpsURLConnection prepareConnection(final @NotNull URL url,
|
||||
final @NotNull String serializedContent) throws Exception {
|
||||
final HttpsURLConnection urlConnection = (HttpsURLConnection) url.openConnection();
|
||||
// Set connection timeout
|
||||
urlConnection.setConnectTimeout(15000);
|
||||
// Set necessary properties
|
||||
urlConnection.setRequestProperty("Content-Type", "application/json");
|
||||
urlConnection.setRequestProperty("Content-Length", String.valueOf(serializedContent.length()));
|
||||
urlConnection.setRequestProperty("User-Agent", "Sonar");
|
||||
urlConnection.setDoOutput(true);
|
||||
urlConnection.setRequestMethod("POST");
|
||||
return urlConnection;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@RequiredArgsConstructor
|
||||
static final class Webhook {
|
||||
private final String content, username, avatar_url;
|
||||
private final boolean tts;
|
||||
private final List<EmbedMessage> embeds;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
static final class EmbedMessage {
|
||||
private final String title;
|
||||
private final String description;
|
||||
private final String url;
|
||||
private final int color;
|
||||
private final Field[] fields;
|
||||
private final Footer footer;
|
||||
}
|
||||
|
||||
@RequiredArgsConstructor
|
||||
static final class Field {
|
||||
private final String name;
|
||||
private final String value;
|
||||
private final boolean inline;
|
||||
}
|
||||
|
||||
@RequiredArgsConstructor
|
||||
static final class Footer {
|
||||
private final String text;
|
||||
private final String icon_url;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,17 +7,16 @@ plugins {
|
||||
apply(plugin = "net.minecrell.plugin-yml.bukkit")
|
||||
|
||||
bukkit {
|
||||
name = rootProject.name
|
||||
name = "Sonar"
|
||||
version = rootProject.version.toString().split("-")[0]
|
||||
main = "xyz.jonesdev.sonar.bukkit.SonarBukkitPlugin"
|
||||
authors = listOf("Jones Development", "Sonar Contributors")
|
||||
website = "https://jonesdev.xyz/discord/"
|
||||
description = "Effective Anti-bot plugin for Velocity, BungeeCord, and Bukkit (1.7-latest)"
|
||||
load = BukkitPluginDescription.PluginLoadOrder.POSTWORLD
|
||||
|
||||
commands {
|
||||
register("sonar") {
|
||||
// No permission check here
|
||||
// No permission checks here
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,31 +18,31 @@
|
||||
package xyz.jonesdev.sonar.bukkit;
|
||||
|
||||
import lombok.Getter;
|
||||
import net.kyori.adventure.platform.bukkit.BukkitAudiences;
|
||||
import org.bstats.bukkit.Metrics;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.SonarPlatform;
|
||||
import xyz.jonesdev.sonar.api.command.InvocationSource;
|
||||
import xyz.jonesdev.sonar.api.fallback.traffic.TrafficCounter;
|
||||
import xyz.jonesdev.sonar.api.logger.LoggerWrapper;
|
||||
import xyz.jonesdev.sonar.api.server.ServerWrapper;
|
||||
import xyz.jonesdev.sonar.bukkit.command.BukkitInvocationSource;
|
||||
import xyz.jonesdev.sonar.bukkit.audience.AudienceListener;
|
||||
import xyz.jonesdev.sonar.bukkit.command.BukkitSonarCommand;
|
||||
import xyz.jonesdev.sonar.bukkit.verbose.ActionBarVerbose;
|
||||
import xyz.jonesdev.sonar.common.boot.SonarBootstrap;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.logging.Level;
|
||||
|
||||
@Getter
|
||||
public final class SonarBukkit extends SonarBootstrap<SonarBukkitPlugin> {
|
||||
public static SonarBukkit INSTANCE;
|
||||
|
||||
public SonarBukkit(final @NotNull SonarBukkitPlugin plugin) {
|
||||
super(plugin, plugin.getDataFolder(), new ActionBarVerbose(plugin.getServer()));
|
||||
super(plugin, plugin.getDataFolder(), SonarPlatform.BUKKIT);
|
||||
INSTANCE = this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper for Bukkit audiences
|
||||
*/
|
||||
private final BukkitAudiences bukkitAudiences = BukkitAudiences.create(getPlugin());
|
||||
|
||||
/**
|
||||
* Create a wrapper for the plugin logger, so we can use it outside
|
||||
* the velocity module.
|
||||
@@ -53,34 +53,17 @@ public final class SonarBukkit extends SonarBootstrap<SonarBukkitPlugin> {
|
||||
|
||||
@Override
|
||||
public void info(final String message, final Object... args) {
|
||||
getPlugin().getLogger().log(Level.INFO, message, args);
|
||||
getPlugin().getLogger().info(buildFullMessage(message, args));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void warn(final String message, final Object... args) {
|
||||
getPlugin().getLogger().log(Level.WARNING, message, args);
|
||||
getPlugin().getLogger().warning(buildFullMessage(message, args));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void error(final String message, final Object... args) {
|
||||
getPlugin().getLogger().log(Level.SEVERE, message, args);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a wrapper object for our server, so we can use it outside
|
||||
* the velocity module.
|
||||
* <br>
|
||||
* We have to do this, so we can access all necessary API functions.
|
||||
*/
|
||||
public final ServerWrapper server = new ServerWrapper(SonarPlatform.BUKKIT) {
|
||||
|
||||
@Override
|
||||
public Optional<InvocationSource> getOnlinePlayer(final String username) {
|
||||
return getPlugin().getServer().getOnlinePlayers().stream()
|
||||
.filter(player -> player.getName().equalsIgnoreCase(username))
|
||||
.findFirst()
|
||||
.map(BukkitInvocationSource::new);
|
||||
getPlugin().getLogger().severe(buildFullMessage(message, args));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -88,21 +71,12 @@ public final class SonarBukkit extends SonarBootstrap<SonarBukkitPlugin> {
|
||||
public void enable() {
|
||||
|
||||
// Initialize bStats.org metrics
|
||||
new Metrics(getPlugin(), getServer().getPlatform().getMetricsId());
|
||||
new Metrics(getPlugin(), getPlatform().getMetricsId());
|
||||
|
||||
// Register Sonar command
|
||||
Objects.requireNonNull(getPlugin().getCommand("sonar")).setExecutor(new BukkitSonarCommand());
|
||||
|
||||
// Register Fallback queue task
|
||||
getPlugin().getServer().getScheduler().runTaskTimerAsynchronously(getPlugin(), getFallback().getQueue()::poll,
|
||||
10L, 10L);
|
||||
|
||||
// Register traffic counter reset task
|
||||
getPlugin().getServer().getScheduler().runTaskTimerAsynchronously(getPlugin(), TrafficCounter::reset,
|
||||
20L, 20L);
|
||||
|
||||
// Register action bar verbose task
|
||||
getPlugin().getServer().getScheduler().runTaskTimerAsynchronously(getPlugin(), getVerboseHandler()::update,
|
||||
2L, 2L);
|
||||
// Register audience register listener
|
||||
getPlugin().getServer().getPluginManager().registerEvents(new AudienceListener(), getPlugin());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.bukkit.audience;
|
||||
|
||||
import net.kyori.adventure.audience.Audience;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.player.PlayerJoinEvent;
|
||||
import org.bukkit.event.player.PlayerQuitEvent;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.bukkit.SonarBukkit;
|
||||
|
||||
public final class AudienceListener implements Listener {
|
||||
|
||||
@EventHandler(priority = EventPriority.LOWEST)
|
||||
public void handle(final @NotNull PlayerJoinEvent event) {
|
||||
final Audience audience = SonarBukkit.INSTANCE.getBukkitAudiences().player(event.getPlayer());
|
||||
Sonar.get().getVerboseHandler().getAudiences().put(event.getPlayer().getName(), audience);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.LOWEST)
|
||||
public void handle(final @NotNull PlayerQuitEvent event) {
|
||||
Sonar.get().getVerboseHandler().getAudiences().remove(event.getPlayer().getName());
|
||||
}
|
||||
}
|
||||
+5
-4
@@ -17,16 +17,17 @@
|
||||
|
||||
package xyz.jonesdev.sonar.bukkit.command;
|
||||
|
||||
import net.kyori.adventure.platform.bukkit.BukkitAudiences;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.command.InvocationSource;
|
||||
import xyz.jonesdev.sonar.bukkit.SonarBukkit;
|
||||
|
||||
public final class BukkitInvocationSource extends InvocationSource {
|
||||
private static final BukkitAudiences AUDIENCES = BukkitAudiences.create(SonarBukkit.INSTANCE.getPlugin());
|
||||
|
||||
public BukkitInvocationSource(final @NotNull CommandSender sender) {
|
||||
super(sender.getName(), AUDIENCES.sender(sender));
|
||||
super(sender.getName(),
|
||||
SonarBukkit.INSTANCE.getBukkitAudiences().sender(sender),
|
||||
sender instanceof Player,
|
||||
sender::hasPermission);
|
||||
}
|
||||
}
|
||||
+9
-129
@@ -17,28 +17,19 @@
|
||||
|
||||
package xyz.jonesdev.sonar.bukkit.command;
|
||||
|
||||
import net.kyori.adventure.text.Component;
|
||||
import org.bukkit.command.*;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.command.Command;
|
||||
import org.bukkit.command.CommandExecutor;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.command.TabExecutor;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.command.CommandInvocation;
|
||||
import xyz.jonesdev.sonar.api.command.InvocationSource;
|
||||
import xyz.jonesdev.sonar.api.command.SonarCommand;
|
||||
import xyz.jonesdev.sonar.api.command.subcommand.Subcommand;
|
||||
import xyz.jonesdev.sonar.api.command.subcommand.argument.Argument;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static java.util.Collections.emptyList;
|
||||
|
||||
public final class BukkitSonarCommand implements CommandExecutor, TabExecutor, SonarCommand {
|
||||
{
|
||||
cacheHelpMessage();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCommand(final CommandSender sender,
|
||||
@@ -47,97 +38,13 @@ public final class BukkitSonarCommand implements CommandExecutor, TabExecutor, S
|
||||
final String[] args) {
|
||||
// Create our own invocation source wrapper to handle messages properly
|
||||
final InvocationSource invocationSource = new BukkitInvocationSource(sender);
|
||||
|
||||
if (!(sender instanceof ConsoleCommandSender)) {
|
||||
// Check if the player actually has the permission to run the command
|
||||
if (!sender.hasPermission("sonar.command")) {
|
||||
invocationSource.sendMessage(Sonar.get().getConfig().getNoPermission());
|
||||
return false;
|
||||
}
|
||||
// Checking if it contains will only break more since it can throw
|
||||
// a NullPointerException if the cache is being accessed from parallel threads
|
||||
DELAY.cleanUp(); // Clean up the cache
|
||||
final long mapTimestamp = DELAY.asMap().getOrDefault(sender, -1L);
|
||||
|
||||
// There were some exploits with spamming commands in the past.
|
||||
// Spamming should be prevented, especially if some heavy operations are done,
|
||||
// which is not the case here but let's still stay safe!
|
||||
if (mapTimestamp > 0L) {
|
||||
invocationSource.sendMessage(Sonar.get().getConfig().getCommands().getCommandCoolDown());
|
||||
|
||||
// Format delay
|
||||
final long timestamp = System.currentTimeMillis();
|
||||
final double left = 0.5D - (timestamp - mapTimestamp) / 1000D;
|
||||
|
||||
invocationSource.sendMessage(Sonar.get().getConfig().getCommands().getCommandCoolDownLeft()
|
||||
.replace("%time-left%", Sonar.DECIMAL_FORMAT.format(left)));
|
||||
return false;
|
||||
}
|
||||
|
||||
DELAY.put(sender);
|
||||
}
|
||||
|
||||
Optional<Subcommand> subcommand = Optional.empty();
|
||||
|
||||
if (args.length > 0) {
|
||||
// Search subcommand if command arguments are present
|
||||
subcommand = Sonar.get().getSubcommandRegistry().getSubcommands().parallelStream()
|
||||
.filter(sub -> sub.getInfo().name().equalsIgnoreCase(args[0])
|
||||
|| Arrays.stream(sub.getInfo().aliases())
|
||||
.anyMatch(alias -> alias.equalsIgnoreCase(args[0])))
|
||||
.findFirst();
|
||||
|
||||
// Check permissions for subcommands
|
||||
if (subcommand.isPresent()) {
|
||||
if (!subcommand.get().getInfo().onlyConsole()
|
||||
&& !sender.hasPermission(subcommand.get().getPermission())
|
||||
) {
|
||||
invocationSource.sendMessage(Sonar.get().getConfig().getCommands().getSubCommandNoPerm()
|
||||
.replace("%permission%", subcommand.get().getPermission()));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
subcommand.ifPresent(sub -> {
|
||||
if (sub.getInfo().onlyPlayers() && !(sender instanceof Player)) {
|
||||
invocationSource.sendMessage(Sonar.get().getConfig().getCommands().getPlayersOnly());
|
||||
return;
|
||||
}
|
||||
|
||||
if (sub.getInfo().onlyConsole() && !(sender instanceof ConsoleCommandSender)) {
|
||||
invocationSource.sendMessage(Sonar.get().getConfig().getCommands().getConsoleOnly());
|
||||
return;
|
||||
}
|
||||
|
||||
final CommandInvocation commandInvocation = new CommandInvocation(invocationSource, sub, args);
|
||||
|
||||
// The subcommands has arguments which are not present in the executed command
|
||||
if (sub.getInfo().arguments().length > 0
|
||||
&& commandInvocation.getRawArguments().length <= 1) {
|
||||
invocationSource.sendMessage(Sonar.get().getConfig().getCommands().getIncorrectCommandUsage()
|
||||
.replace("%usage%", sub.getInfo().name() + " (" + sub.getArguments() + ")"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Execute the sub command with the custom invocation properties
|
||||
sub.execute(commandInvocation);
|
||||
});
|
||||
|
||||
if (!subcommand.isPresent()) {
|
||||
|
||||
// Re-use the old, cached help message since we don't want to scan
|
||||
// for each subcommand and it's arguments/attributes every time
|
||||
// someone runs /sonar since the subcommand don't change
|
||||
for (final Component component : CACHED_HELP_MESSAGE) {
|
||||
invocationSource.sendMessage(component);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
// Pass the invocation source and command arguments to our command handler
|
||||
handle(invocationSource, args);
|
||||
return true; // Valid
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> onTabComplete(final CommandSender sender,
|
||||
public List<String> onTabComplete(final @NotNull CommandSender sender,
|
||||
final Command command,
|
||||
final String commandAlias,
|
||||
final String @NotNull [] args) {
|
||||
@@ -145,33 +52,6 @@ public final class BukkitSonarCommand implements CommandExecutor, TabExecutor, S
|
||||
if (!sender.hasPermission("sonar.command")) {
|
||||
return emptyList();
|
||||
}
|
||||
if (args.length <= 1) {
|
||||
if (TAB_SUGGESTIONS.isEmpty()) {
|
||||
for (final Subcommand subcommand : Sonar.get().getSubcommandRegistry().getSubcommands()) {
|
||||
TAB_SUGGESTIONS.add(subcommand.getInfo().name());
|
||||
|
||||
if (subcommand.getInfo().aliases().length > 0) {
|
||||
TAB_SUGGESTIONS.addAll(Arrays.asList(subcommand.getInfo().aliases()));
|
||||
}
|
||||
}
|
||||
}
|
||||
return TAB_SUGGESTIONS;
|
||||
} else if (args.length == 2) {
|
||||
if (ARG_TAB_SUGGESTIONS.isEmpty()) {
|
||||
for (final Subcommand subcommand : Sonar.get().getSubcommandRegistry().getSubcommands()) {
|
||||
final List<String> parsedArguments = Arrays.stream(subcommand.getInfo().arguments())
|
||||
.map(Argument::value)
|
||||
.collect(Collectors.toList());
|
||||
ARG_TAB_SUGGESTIONS.put(subcommand.getInfo().name(), parsedArguments);
|
||||
for (final String alias : subcommand.getInfo().aliases()) {
|
||||
ARG_TAB_SUGGESTIONS.put(alias, parsedArguments);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final String subCommandName = args[0].toLowerCase();
|
||||
return ARG_TAB_SUGGESTIONS.getOrDefault(subCommandName, emptyList());
|
||||
}
|
||||
return emptyList();
|
||||
return getCachedTabSuggestions(args);
|
||||
}
|
||||
}
|
||||
@@ -5,24 +5,18 @@ plugins {
|
||||
apply(plugin = "net.minecrell.plugin-yml.bungee")
|
||||
|
||||
bungee {
|
||||
name = rootProject.name
|
||||
name = "Sonar"
|
||||
version = rootProject.version.toString().split("-")[0]
|
||||
main = "xyz.jonesdev.sonar.bungee.SonarBungeePlugin"
|
||||
author = "Jones Development, Sonar Contributors"
|
||||
description = "Effective Anti-bot plugin for Velocity, BungeeCord, and Bukkit (1.7-latest)"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly(project(":api"))
|
||||
compileOnly(project(":common"))
|
||||
|
||||
implementation("com.velocitypowered:velocity-native:1.1.9") {
|
||||
exclude(group = "com.google.guava")
|
||||
exclude(group = "io.netty")
|
||||
exclude(group = "org.checkerframework")
|
||||
}
|
||||
|
||||
compileOnly("net.md_5:bungeecord:1.20.2-rc2-SNAPSHOT")
|
||||
compileOnly("net.md_5:bungeecord-proxy:master-SNAPSHOT")
|
||||
testCompileOnly("net.md_5:bungeecord-proxy:master-SNAPSHOT")
|
||||
|
||||
// MiniMessage platform support
|
||||
implementation("net.kyori:adventure-platform-bungeecord:4.3.1")
|
||||
|
||||
@@ -18,34 +18,32 @@
|
||||
package xyz.jonesdev.sonar.bungee;
|
||||
|
||||
import lombok.Getter;
|
||||
import net.kyori.adventure.platform.bungeecord.BungeeAudiences;
|
||||
import org.bstats.bungeecord.Metrics;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.SonarPlatform;
|
||||
import xyz.jonesdev.sonar.api.command.InvocationSource;
|
||||
import xyz.jonesdev.sonar.api.fallback.traffic.TrafficCounter;
|
||||
import xyz.jonesdev.sonar.api.logger.LoggerWrapper;
|
||||
import xyz.jonesdev.sonar.api.server.ServerWrapper;
|
||||
import xyz.jonesdev.sonar.bungee.command.BungeeInvocationSource;
|
||||
import xyz.jonesdev.sonar.bungee.audience.AudienceListener;
|
||||
import xyz.jonesdev.sonar.bungee.command.BungeeSonarCommand;
|
||||
import xyz.jonesdev.sonar.bungee.fallback.FallbackListener;
|
||||
import xyz.jonesdev.sonar.bungee.fallback.injection.BaseInjectionHelper;
|
||||
import xyz.jonesdev.sonar.bungee.fallback.injection.ChildChannelInitializer;
|
||||
import xyz.jonesdev.sonar.bungee.verbose.VerboseWrapper;
|
||||
import xyz.jonesdev.sonar.common.boot.SonarBootstrap;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.logging.Level;
|
||||
|
||||
@Getter
|
||||
public final class SonarBungee extends SonarBootstrap<SonarBungeePlugin> {
|
||||
public static SonarBungee INSTANCE;
|
||||
|
||||
public SonarBungee(final @NotNull SonarBungeePlugin plugin) {
|
||||
super(plugin, plugin.getDataFolder(), new VerboseWrapper(plugin.getServer()));
|
||||
super(plugin, plugin.getDataFolder(), SonarPlatform.BUNGEE);
|
||||
INSTANCE = this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper for BungeeCord audiences
|
||||
*/
|
||||
private final BungeeAudiences bungeeAudiences = BungeeAudiences.create(getPlugin());
|
||||
|
||||
/**
|
||||
* Create a wrapper for the plugin logger, so we can use it outside
|
||||
* the velocity module.
|
||||
@@ -56,34 +54,17 @@ public final class SonarBungee extends SonarBootstrap<SonarBungeePlugin> {
|
||||
|
||||
@Override
|
||||
public void info(final String message, final Object... args) {
|
||||
getPlugin().getLogger().log(Level.INFO, message, args);
|
||||
getPlugin().getLogger().info(buildFullMessage(message, args));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void warn(final String message, final Object... args) {
|
||||
getPlugin().getLogger().log(Level.WARNING, message, args);
|
||||
getPlugin().getLogger().warning(buildFullMessage(message, args));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void error(final String message, final Object... args) {
|
||||
getPlugin().getLogger().log(Level.SEVERE, message, args);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a wrapper object for our server, so we can use it outside
|
||||
* the velocity module.
|
||||
* <br>
|
||||
* We have to do this, so we can access all necessary API functions.
|
||||
*/
|
||||
public final ServerWrapper server = new ServerWrapper(SonarPlatform.BUNGEE) {
|
||||
|
||||
@Override
|
||||
public Optional<InvocationSource> getOnlinePlayer(final String username) {
|
||||
return getPlugin().getServer().getPlayers().stream()
|
||||
.filter(player -> player.getName().equalsIgnoreCase(username))
|
||||
.findFirst()
|
||||
.map(BungeeInvocationSource::new);
|
||||
getPlugin().getLogger().severe(buildFullMessage(message, args));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -91,25 +72,16 @@ public final class SonarBungee extends SonarBootstrap<SonarBungeePlugin> {
|
||||
public void enable() {
|
||||
|
||||
// Initialize bStats.org metrics
|
||||
new Metrics(getPlugin(), getServer().getPlatform().getMetricsId());
|
||||
new Metrics(getPlugin(), getPlatform().getMetricsId());
|
||||
|
||||
// Register Sonar command
|
||||
getPlugin().getServer().getPluginManager().registerCommand(getPlugin(), new BungeeSonarCommand());
|
||||
|
||||
// Register Fallback listener
|
||||
getPlugin().getServer().getPluginManager().registerListener(getPlugin(), new FallbackListener(getFallback()));
|
||||
getPlugin().getServer().getPluginManager().registerListener(getPlugin(), new FallbackListener());
|
||||
|
||||
// Register Fallback queue task
|
||||
getPlugin().getServer().getScheduler().schedule(getPlugin(), getFallback().getQueue()::poll,
|
||||
500L, 500L, TimeUnit.MILLISECONDS);
|
||||
|
||||
// Register traffic counter reset task
|
||||
getPlugin().getServer().getScheduler().schedule(getPlugin(), TrafficCounter::reset,
|
||||
1L, 1L, TimeUnit.SECONDS);
|
||||
|
||||
// Register action bar verbose task
|
||||
getPlugin().getServer().getScheduler().schedule(getPlugin(), getVerboseHandler()::update,
|
||||
100L, 100L, TimeUnit.MILLISECONDS);
|
||||
// Register audience register listener
|
||||
getPlugin().getServer().getPluginManager().registerListener(getPlugin(), new AudienceListener());
|
||||
|
||||
// Inject base into ProtocolUtils
|
||||
BaseInjectionHelper.inject(ChildChannelInitializer.INSTANCE);
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.bungee.audience;
|
||||
|
||||
import net.kyori.adventure.audience.Audience;
|
||||
import net.md_5.bungee.api.event.PlayerDisconnectEvent;
|
||||
import net.md_5.bungee.api.event.PostLoginEvent;
|
||||
import net.md_5.bungee.api.plugin.Listener;
|
||||
import net.md_5.bungee.event.EventHandler;
|
||||
import net.md_5.bungee.event.EventPriority;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.bungee.SonarBungee;
|
||||
|
||||
public final class AudienceListener implements Listener {
|
||||
|
||||
@EventHandler(priority = EventPriority.LOWEST)
|
||||
public void handle(final @NotNull PostLoginEvent event) {
|
||||
final Audience audience = SonarBungee.INSTANCE.getBungeeAudiences().player(event.getPlayer());
|
||||
Sonar.get().getVerboseHandler().getAudiences().put(event.getPlayer().getName(), audience);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.LOWEST)
|
||||
public void handle(final @NotNull PlayerDisconnectEvent event) {
|
||||
Sonar.get().getVerboseHandler().getAudiences().remove(event.getPlayer().getName());
|
||||
}
|
||||
}
|
||||
+5
-4
@@ -17,16 +17,17 @@
|
||||
|
||||
package xyz.jonesdev.sonar.bungee.command;
|
||||
|
||||
import net.kyori.adventure.platform.bungeecord.BungeeAudiences;
|
||||
import net.md_5.bungee.api.CommandSender;
|
||||
import net.md_5.bungee.api.connection.ProxiedPlayer;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.command.InvocationSource;
|
||||
import xyz.jonesdev.sonar.bungee.SonarBungee;
|
||||
|
||||
public final class BungeeInvocationSource extends InvocationSource {
|
||||
private static final BungeeAudiences AUDIENCES = BungeeAudiences.create(SonarBungee.INSTANCE.getPlugin());
|
||||
|
||||
public BungeeInvocationSource(final @NotNull CommandSender sender) {
|
||||
super(sender.getName(), AUDIENCES.sender(sender));
|
||||
super(sender.getName(),
|
||||
SonarBungee.INSTANCE.getBungeeAudiences().sender(sender),
|
||||
sender instanceof ProxiedPlayer,
|
||||
sender::hasPermission);
|
||||
}
|
||||
}
|
||||
+4
-128
@@ -17,31 +17,18 @@
|
||||
|
||||
package xyz.jonesdev.sonar.bungee.command;
|
||||
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.md_5.bungee.api.CommandSender;
|
||||
import net.md_5.bungee.api.connection.ProxiedPlayer;
|
||||
import net.md_5.bungee.api.plugin.Command;
|
||||
import net.md_5.bungee.api.plugin.TabExecutor;
|
||||
import net.md_5.bungee.command.ConsoleCommandSender;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.command.CommandInvocation;
|
||||
import xyz.jonesdev.sonar.api.command.InvocationSource;
|
||||
import xyz.jonesdev.sonar.api.command.SonarCommand;
|
||||
import xyz.jonesdev.sonar.api.command.subcommand.Subcommand;
|
||||
import xyz.jonesdev.sonar.api.command.subcommand.argument.Argument;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static java.util.Collections.emptyList;
|
||||
|
||||
public final class BungeeSonarCommand extends Command implements TabExecutor, SonarCommand {
|
||||
public BungeeSonarCommand() {
|
||||
super("sonar");
|
||||
cacheHelpMessage();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -49,127 +36,16 @@ public final class BungeeSonarCommand extends Command implements TabExecutor, So
|
||||
public void execute(final @NotNull CommandSender sender, final String[] args) {
|
||||
// Create our own invocation source wrapper to handle messages properly
|
||||
final InvocationSource invocationSource = new BungeeInvocationSource(sender);
|
||||
|
||||
if (!(sender instanceof ConsoleCommandSender)) {
|
||||
// Check if the player actually has the permission to run the command
|
||||
if (!sender.hasPermission("sonar.command")) {
|
||||
invocationSource.sendMessage(Sonar.get().getConfig().getNoPermission());
|
||||
return;
|
||||
}
|
||||
// Checking if it contains will only break more since it can throw
|
||||
// a NullPointerException if the cache is being accessed from parallel threads
|
||||
DELAY.cleanUp(); // Clean up the cache
|
||||
final long mapTimestamp = DELAY.asMap().getOrDefault(sender, -1L);
|
||||
|
||||
// There were some exploits with spamming commands in the past.
|
||||
// Spamming should be prevented, especially if some heavy operations are done,
|
||||
// which is not the case here but let's still stay safe!
|
||||
if (mapTimestamp > 0L) {
|
||||
invocationSource.sendMessage(Sonar.get().getConfig().getCommands().getCommandCoolDown());
|
||||
|
||||
// Format delay
|
||||
final long timestamp = System.currentTimeMillis();
|
||||
final double left = 0.5D - (timestamp - mapTimestamp) / 1000D;
|
||||
|
||||
invocationSource.sendMessage(Sonar.get().getConfig().getCommands().getCommandCoolDownLeft()
|
||||
.replace("%time-left%", Sonar.DECIMAL_FORMAT.format(left)));
|
||||
return;
|
||||
}
|
||||
|
||||
DELAY.put(sender);
|
||||
}
|
||||
|
||||
Optional<Subcommand> subcommand = Optional.empty();
|
||||
|
||||
if (args.length > 0) {
|
||||
// Search subcommand if command arguments are present
|
||||
subcommand = Sonar.get().getSubcommandRegistry().getSubcommands().parallelStream()
|
||||
.filter(sub -> sub.getInfo().name().equalsIgnoreCase(args[0])
|
||||
|| Arrays.stream(sub.getInfo().aliases())
|
||||
.anyMatch(alias -> alias.equalsIgnoreCase(args[0])))
|
||||
.findFirst();
|
||||
|
||||
// Check permissions for subcommands
|
||||
if (subcommand.isPresent()) {
|
||||
if (!subcommand.get().getInfo().onlyConsole()
|
||||
&& !sender.hasPermission(subcommand.get().getPermission())
|
||||
) {
|
||||
invocationSource.sendMessage(Sonar.get().getConfig().getCommands().getSubCommandNoPerm()
|
||||
.replace("%permission%", subcommand.get().getPermission()));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
subcommand.ifPresent(sub -> {
|
||||
if (sub.getInfo().onlyPlayers() && !(sender instanceof ProxiedPlayer)) {
|
||||
invocationSource.sendMessage(Sonar.get().getConfig().getCommands().getPlayersOnly());
|
||||
return;
|
||||
}
|
||||
|
||||
if (sub.getInfo().onlyConsole() && !(sender instanceof ConsoleCommandSender)) {
|
||||
invocationSource.sendMessage(Sonar.get().getConfig().getCommands().getConsoleOnly());
|
||||
return;
|
||||
}
|
||||
|
||||
final CommandInvocation commandInvocation = new CommandInvocation(invocationSource, sub, args);
|
||||
|
||||
// The subcommands has arguments which are not present in the executed command
|
||||
if (sub.getInfo().arguments().length > 0
|
||||
&& commandInvocation.getRawArguments().length <= 1) {
|
||||
invocationSource.sendMessage(Sonar.get().getConfig().getCommands().getIncorrectCommandUsage()
|
||||
.replace("%usage%", sub.getInfo().name() + " (" + sub.getArguments() + ")"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Execute the sub command with the custom invocation properties
|
||||
sub.execute(commandInvocation);
|
||||
});
|
||||
|
||||
if (!subcommand.isPresent()) {
|
||||
|
||||
// Re-use the old, cached help message since we don't want to scan
|
||||
// for each subcommand and it's arguments/attributes every time
|
||||
// someone runs /sonar since the subcommand don't change
|
||||
for (final Component component : CACHED_HELP_MESSAGE) {
|
||||
invocationSource.sendMessage(component);
|
||||
}
|
||||
}
|
||||
// Pass the invocation source and command arguments to our command handler
|
||||
handle(invocationSource, args);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterable<String> onTabComplete(final CommandSender sender, final String @NotNull [] args) {
|
||||
public Iterable<String> onTabComplete(final @NotNull CommandSender sender, final String @NotNull [] args) {
|
||||
// Do not allow tab completion if the player does not have the required permission
|
||||
if (!sender.hasPermission("sonar.command")) {
|
||||
return emptyList();
|
||||
}
|
||||
if (args.length <= 1) {
|
||||
if (TAB_SUGGESTIONS.isEmpty()) {
|
||||
for (final Subcommand subcommand : Sonar.get().getSubcommandRegistry().getSubcommands()) {
|
||||
TAB_SUGGESTIONS.add(subcommand.getInfo().name());
|
||||
|
||||
if (subcommand.getInfo().aliases().length > 0) {
|
||||
TAB_SUGGESTIONS.addAll(Arrays.asList(subcommand.getInfo().aliases()));
|
||||
}
|
||||
}
|
||||
}
|
||||
return TAB_SUGGESTIONS;
|
||||
} else if (args.length == 2) {
|
||||
if (ARG_TAB_SUGGESTIONS.isEmpty()) {
|
||||
for (final Subcommand subcommand : Sonar.get().getSubcommandRegistry().getSubcommands()) {
|
||||
final List<String> parsedArguments = Arrays.stream(subcommand.getInfo().arguments())
|
||||
.map(Argument::value)
|
||||
.collect(Collectors.toList());
|
||||
ARG_TAB_SUGGESTIONS.put(subcommand.getInfo().name(), parsedArguments);
|
||||
for (final String alias : subcommand.getInfo().aliases()) {
|
||||
ARG_TAB_SUGGESTIONS.put(alias, parsedArguments);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final String subCommandName = args[0].toLowerCase();
|
||||
return ARG_TAB_SUGGESTIONS.getOrDefault(subCommandName, emptyList());
|
||||
}
|
||||
return emptyList();
|
||||
return getCachedTabSuggestions(args);
|
||||
}
|
||||
}
|
||||
+5
-18
@@ -15,25 +15,12 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.velocity.verbose;
|
||||
package xyz.jonesdev.sonar.bungee.fallback;
|
||||
|
||||
import com.velocitypowered.api.proxy.ProxyServer;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import xyz.jonesdev.sonar.api.verbose.Verbose;
|
||||
import io.netty.handler.codec.DecoderException;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public final class VerboseWrapper extends Verbose {
|
||||
private final ProxyServer server;
|
||||
|
||||
@Override
|
||||
public void broadcast(final Component component) {
|
||||
synchronized (subscribers) {
|
||||
for (final String subscriber : subscribers) {
|
||||
server.getPlayer(subscriber).ifPresent(player -> {
|
||||
player.sendActionBar(component);
|
||||
});
|
||||
}
|
||||
}
|
||||
final class ConditionFailedException extends DecoderException {
|
||||
public ConditionFailedException(final String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.bungee.fallback;
|
||||
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import net.md_5.bungee.api.ProxyServer;
|
||||
import net.md_5.bungee.connection.DownstreamBridge;
|
||||
import net.md_5.bungee.connection.UpstreamBridge;
|
||||
import net.md_5.bungee.netty.ChannelWrapper;
|
||||
import net.md_5.bungee.netty.HandlerBoss;
|
||||
import net.md_5.bungee.netty.PacketHandler;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public final class FallbackHandlerBoss extends HandlerBoss {
|
||||
private ChannelWrapper channel;
|
||||
private PacketHandler handler;
|
||||
|
||||
@Override
|
||||
public void setHandler(final @NotNull PacketHandler handler) {
|
||||
this.handler = handler;
|
||||
super.setHandler(handler);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelActive(final ChannelHandlerContext ctx) throws Exception {
|
||||
if (handler != null) {
|
||||
channel = new ChannelWrapper(ctx);
|
||||
super.channelActive(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelInactive(final ChannelHandlerContext ctx) throws Exception {
|
||||
if (handler != null) {
|
||||
channel.markClosed();
|
||||
super.channelInactive(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelWritabilityChanged(final ChannelHandlerContext ctx) throws Exception {
|
||||
if (handler != null) {
|
||||
handler.writabilityChanged(channel);
|
||||
super.channelWritabilityChanged(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exceptionCaught(final @NotNull ChannelHandlerContext ctx, final Throwable throwable) throws Exception {
|
||||
if (ctx.channel().isActive()) {
|
||||
if (handler instanceof DownstreamBridge || handler instanceof UpstreamBridge) {
|
||||
try {
|
||||
handler.exception(throwable);
|
||||
} catch (Exception exception) {
|
||||
ProxyServer.getInstance().getLogger().severe(handler + " - exception processing exception: " + exception);
|
||||
}
|
||||
}
|
||||
|
||||
channel.markClosed();
|
||||
ctx.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
+343
@@ -0,0 +1,343 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.bungee.fallback;
|
||||
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelFutureListener;
|
||||
import io.netty.channel.ChannelPipeline;
|
||||
import io.netty.handler.codec.DecoderException;
|
||||
import lombok.Getter;
|
||||
import lombok.val;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.serializer.json.JSONComponentSerializer;
|
||||
import net.md_5.bungee.BungeeCord;
|
||||
import net.md_5.bungee.EncryptionUtil;
|
||||
import net.md_5.bungee.api.config.ListenerInfo;
|
||||
import net.md_5.bungee.chat.ComponentSerializer;
|
||||
import net.md_5.bungee.connection.InitialHandler;
|
||||
import net.md_5.bungee.netty.ChannelWrapper;
|
||||
import net.md_5.bungee.protocol.PlayerPublicKey;
|
||||
import net.md_5.bungee.protocol.packet.Kick;
|
||||
import net.md_5.bungee.protocol.packet.LoginRequest;
|
||||
import net.md_5.bungee.protocol.packet.StatusRequest;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import xyz.jonesdev.sonar.api.ReflectiveOperationException;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.event.impl.UserVerifyJoinEvent;
|
||||
import xyz.jonesdev.sonar.api.fallback.Fallback;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.api.statistics.Statistics;
|
||||
import xyz.jonesdev.sonar.common.fallback.FallbackChannelHandler;
|
||||
import xyz.jonesdev.sonar.common.fallback.FallbackTimeoutHandler;
|
||||
import xyz.jonesdev.sonar.common.fallback.FallbackVerificationHandler;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketDecoder;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketEncoder;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketRegistry;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.login.LoginSuccess;
|
||||
import xyz.jonesdev.sonar.common.fallback.traffic.TrafficChannelHooker;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Instant;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static net.md_5.bungee.netty.PipelineUtils.*;
|
||||
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.*;
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_19_1;
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_19_3;
|
||||
import static xyz.jonesdev.sonar.common.utility.geyser.GeyserUtil.isGeyserConnection;
|
||||
|
||||
public final class FallbackInitialHandler extends InitialHandler {
|
||||
public FallbackInitialHandler(final @NotNull BungeeCord bungee, final ListenerInfo listener) {
|
||||
super(bungee, listener);
|
||||
this.bungee = bungee;
|
||||
}
|
||||
|
||||
private static final @NotNull Fallback FALLBACK = Objects.requireNonNull(Sonar.get().getFallback());
|
||||
@Getter
|
||||
private ChannelWrapper channelWrapper;
|
||||
private final @NotNull BungeeCord bungee;
|
||||
private @Nullable FallbackUserWrapper user;
|
||||
@Getter
|
||||
private ProtocolVersion protocolVersion;
|
||||
private boolean receivedLoginPacket;
|
||||
private boolean receivedStatusPacket;
|
||||
|
||||
@Override
|
||||
public void connected(final ChannelWrapper channelWrapper) throws Exception {
|
||||
this.channelWrapper = channelWrapper;
|
||||
super.connected(channelWrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(final StatusRequest statusRequest) throws Exception {
|
||||
// Fix status packet spam exploit
|
||||
if (receivedStatusPacket) {
|
||||
throw new ConditionFailedException("Duplicate status packet");
|
||||
}
|
||||
receivedStatusPacket = true;
|
||||
// Run the rest of the method asynchronously
|
||||
CompletableFuture.runAsync(() -> {
|
||||
// The channel always stays open because the client sends
|
||||
// a StatusRequest and a Ping packet after one another
|
||||
if (!isConnected()) {
|
||||
throw new ConditionFailedException("Not connected anymore");
|
||||
}
|
||||
try {
|
||||
super.handle(statusRequest);
|
||||
} catch (Exception exception) {
|
||||
throw new DecoderException(exception);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(final LoginRequest loginRequest) throws Exception {
|
||||
// Increase joins per second for the action bar verbose
|
||||
Sonar.get().getVerboseHandler().getLoginsPerSecond().put(System.nanoTime());
|
||||
|
||||
// Fix login packet spam exploit
|
||||
if (receivedLoginPacket || user != null) {
|
||||
throw new ConditionFailedException("Duplicate login packet");
|
||||
}
|
||||
receivedLoginPacket = true;
|
||||
// Cache protocol version so other handlers don't throw NPEs
|
||||
protocolVersion = ProtocolVersion.fromId(getHandshake().getProtocolVersion());
|
||||
final Channel channel = channelWrapper.getHandle();
|
||||
|
||||
// Run in the channel's event loop
|
||||
channel.eventLoop().execute(() -> {
|
||||
try {
|
||||
final ChannelPipeline pipeline = channel.pipeline();
|
||||
TrafficChannelHooker.hook(pipeline, PACKET_DECODER, PACKET_ENCODER);
|
||||
|
||||
// Increase total traffic statistic
|
||||
Statistics.TOTAL_TRAFFIC.increment();
|
||||
|
||||
// Check if the verification is enabled
|
||||
if (!Sonar.get().getFallback().shouldVerifyNewPlayers()) {
|
||||
super.handle(loginRequest);
|
||||
return;
|
||||
}
|
||||
|
||||
final InetAddress inetAddress = getAddress().getAddress();
|
||||
val uuid = UUID.nameUUIDFromBytes(("OfflinePlayer:" + loginRequest.getData()).getBytes(StandardCharsets.UTF_8));
|
||||
// Check if the player is already verified
|
||||
if (Sonar.get().getVerifiedPlayerController().has(inetAddress, uuid)) {
|
||||
super.handle(loginRequest);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check the blacklist here since we cannot let the player "ghost join"
|
||||
if (FALLBACK.getBlacklisted().has(inetAddress.toString())) {
|
||||
closeWith(getKickPacket(Sonar.get().getConfig().getVerification().getBlacklisted()));
|
||||
return;
|
||||
}
|
||||
|
||||
// Completely skip Geyser connections
|
||||
if (isGeyserConnection(channel)) {
|
||||
FALLBACK.getLogger().info("Skipping Geyser player: {}{}",
|
||||
loginRequest.getData(), Sonar.get().getConfig().formatAddress(inetAddress));
|
||||
super.handle(loginRequest);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create wrapped Fallback user
|
||||
user = new FallbackUserWrapper(
|
||||
FALLBACK, channelWrapper, this,
|
||||
channel, channel.pipeline(), inetAddress, protocolVersion
|
||||
);
|
||||
|
||||
// Perform default BungeeCord checks
|
||||
if (bungee.config.isEnforceSecureProfile()
|
||||
&& user.getProtocolVersion().compareTo(MINECRAFT_1_19_3) < 0) {
|
||||
final PlayerPublicKey publicKey = loginRequest.getPublicKey();
|
||||
if (publicKey == null) {
|
||||
disconnect(bungee.getTranslation("secure_profile_required"));
|
||||
return;
|
||||
}
|
||||
if (Instant.ofEpochMilli(publicKey.getExpiry()).isBefore(Instant.now())) {
|
||||
disconnect(bungee.getTranslation("secure_profile_expired"));
|
||||
return;
|
||||
}
|
||||
if (user.getProtocolVersion().compareTo(MINECRAFT_1_19_1) < 0
|
||||
&& !EncryptionUtil.check(publicKey, null)) {
|
||||
disconnect(bungee.getTranslation("secure_profile_invalid"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the player is already queued since we don't want bots to flood the queue
|
||||
if (FALLBACK.getQueue().getQueuedPlayers().containsKey(inetAddress)) {
|
||||
closeWith(getKickPacket(Sonar.get().getConfig().getVerification().getAlreadyQueued()));
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if Fallback is already verifying a player
|
||||
// → is another player with the same IP address connected to Fallback?
|
||||
if (FALLBACK.getConnected().containsKey(loginRequest.getData())
|
||||
|| FALLBACK.getConnected().containsValue(inetAddress)) {
|
||||
closeWith(getKickPacket(Sonar.get().getConfig().getVerification().getAlreadyVerifying()));
|
||||
return;
|
||||
}
|
||||
|
||||
// We cannot allow too many players on our Fallback server
|
||||
// There's technically no reason for limiting this, but we'll better stay safe.
|
||||
if (FALLBACK.getConnected().size() > Sonar.get().getConfig().getVerification().getMaxVerifyingPlayers()) {
|
||||
closeWith(getKickPacket(Sonar.get().getConfig().getVerification().getTooManyPlayers()));
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the IP address is currently being rate-limited
|
||||
if (!FALLBACK.getRatelimiter().attempt(inetAddress)) {
|
||||
closeWith(getKickPacket(Sonar.get().getConfig().getVerification().getTooFastReconnect()));
|
||||
return;
|
||||
}
|
||||
|
||||
// We have to add this pipeline to monitor whenever the client disconnects
|
||||
// to remove them from the list of connected and queued players
|
||||
pipeline.addFirst(FALLBACK_HANDLER, new FallbackChannelHandler(loginRequest.getData()));
|
||||
|
||||
// Queue the connection for further processing
|
||||
FALLBACK.getQueue().queue(inetAddress, () -> channel.eventLoop().execute(() -> {
|
||||
|
||||
// Do not continue if the connection is closed or marked as disconnected
|
||||
if (channelWrapper.isClosed() || channelWrapper.isClosing()) return;
|
||||
|
||||
// Check if the username matches the valid name regex to prevent
|
||||
// UTF-16 names or other types of exploits
|
||||
if (!Sonar.get().getConfig().getVerification().getValidNameRegex().matcher(loginRequest.getData()).matches()) {
|
||||
closeWith(getKickPacket(Sonar.get().getConfig().getVerification().getInvalidUsername()));
|
||||
return;
|
||||
}
|
||||
|
||||
// Add better timeout handler to avoid known exploits or issues
|
||||
// We also want to timeout bots quickly to avoid flooding
|
||||
pipeline.replace(
|
||||
TIMEOUT_HANDLER,
|
||||
TIMEOUT_HANDLER,
|
||||
new FallbackTimeoutHandler(Sonar.get().getConfig().getVerification().getReadTimeout(),
|
||||
TimeUnit.MILLISECONDS)
|
||||
);
|
||||
|
||||
// Disconnect if the protocol version could not be resolved
|
||||
if (user.getProtocolVersion().isUnknown()) {
|
||||
closeWith(getKickPacket(Sonar.get().getConfig().getVerification().getInvalidProtocol()));
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the player is already connected to the proxy but still tries to verify
|
||||
final int limit = bungee.config.getPlayerLimit();
|
||||
if (limit > 0 && bungee.getOnlineCount() >= limit) {
|
||||
disconnect(bungee.getTranslation("proxy_full"));
|
||||
return;
|
||||
} else if (!isOnlineMode() && bungee.getPlayer(loginRequest.getData()) != null) {
|
||||
closeWith(getKickPacket(Sonar.get().getConfig().getVerification().getAlreadyConnected()));
|
||||
return;
|
||||
}
|
||||
|
||||
// The player joined the verification
|
||||
Statistics.REAL_TRAFFIC.increment();
|
||||
|
||||
if (Sonar.get().getConfig().getVerification().isLogConnections()) {
|
||||
// Only log the processing message if the server isn't under attack.
|
||||
// We let the user override this through the configuration.
|
||||
if (!Sonar.get().getAttackTracker().isCurrentlyUnderAttack()
|
||||
|| Sonar.get().getConfig().getVerification().isLogDuringAttack()) {
|
||||
FALLBACK.getLogger().info(Sonar.get().getConfig().getVerification().getConnectLog()
|
||||
.replace("%name%", loginRequest.getData())
|
||||
.replace("%ip%", Sonar.get().getConfig().formatAddress(inetAddress))
|
||||
.replace("%protocol%", String.valueOf(user.getProtocolVersion().getProtocol())));
|
||||
}
|
||||
}
|
||||
|
||||
// Call the VerifyJoinEvent for external API usage
|
||||
Sonar.get().getEventManager().publish(new UserVerifyJoinEvent(loginRequest.getData(), user));
|
||||
|
||||
// Mark the player as connected → verifying players
|
||||
FALLBACK.getConnected().put(loginRequest.getData(), inetAddress);
|
||||
|
||||
// This sometimes happens when the channel hangs, but the player is still connecting
|
||||
// This also fixes a unique issue with TCPShield and other reverse proxies
|
||||
if (user.getPipeline().get(PACKET_ENCODER) == null
|
||||
|| user.getPipeline().get(PACKET_DECODER) == null) {
|
||||
channelWrapper.close();
|
||||
return;
|
||||
}
|
||||
|
||||
// Replace normal encoder to allow custom packets
|
||||
final FallbackPacketEncoder encoder = new FallbackPacketEncoder(user.getProtocolVersion());
|
||||
user.getPipeline().replace(PACKET_ENCODER, FALLBACK_PACKET_ENCODER, encoder);
|
||||
|
||||
// Send LoginSuccess packet to make the client think they are joining the server
|
||||
user.write(new LoginSuccess(loginRequest.getData(), uuid));
|
||||
|
||||
// The LoginSuccess packet has been sent, now we can change the registry state
|
||||
encoder.updateRegistry(user.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_20_2) >= 0
|
||||
? FallbackPacketRegistry.CONFIG : FallbackPacketRegistry.GAME);
|
||||
|
||||
// Replace normal decoder to allow custom packets
|
||||
user.getPipeline().replace(
|
||||
PACKET_DECODER, FALLBACK_PACKET_DECODER, new FallbackPacketDecoder(user,
|
||||
new FallbackVerificationHandler(user, loginRequest.getData(), uuid)
|
||||
));
|
||||
}));
|
||||
} catch (Throwable throwable) {
|
||||
throw new ReflectiveOperationException(throwable);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static final Map<Component, Kick> CACHED_KICK_PACKETS = new ConcurrentHashMap<>(16);
|
||||
|
||||
public static @NotNull Kick getKickPacket(final @NotNull Component component) {
|
||||
Kick cachedKickPacket = CACHED_KICK_PACKETS.get(component);
|
||||
if (cachedKickPacket == null) {
|
||||
final String serialized = JSONComponentSerializer.json().serialize(component);
|
||||
cachedKickPacket = new Kick(ComponentSerializer.deserialize(serialized));
|
||||
CACHED_KICK_PACKETS.put(component, cachedKickPacket);
|
||||
}
|
||||
return cachedKickPacket;
|
||||
}
|
||||
|
||||
// Mostly taken from Velocity
|
||||
public void closeWith(final Object msg) {
|
||||
if (channelWrapper.getHandle().isActive()) {
|
||||
boolean is17 = protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_8) < 0
|
||||
&& protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_7_2) >= 0;
|
||||
if (is17) {
|
||||
channelWrapper.getHandle().eventLoop().execute(() -> {
|
||||
channelWrapper.getHandle().config().setAutoRead(false);
|
||||
channelWrapper.getHandle().eventLoop().schedule(() -> {
|
||||
channelWrapper.markClosed();
|
||||
channelWrapper.getHandle().writeAndFlush(msg).addListener(ChannelFutureListener.CLOSE);
|
||||
}, 250L, TimeUnit.MILLISECONDS);
|
||||
});
|
||||
} else {
|
||||
channelWrapper.markClosed();
|
||||
channelWrapper.getHandle().writeAndFlush(msg).addListener(ChannelFutureListener.CLOSE);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+35
-65
@@ -17,53 +17,52 @@
|
||||
|
||||
package xyz.jonesdev.sonar.bungee.fallback;
|
||||
|
||||
import com.velocitypowered.natives.compression.VelocityCompressor;
|
||||
import com.velocitypowered.natives.util.Natives;
|
||||
import io.netty.channel.ChannelPipeline;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import net.kyori.adventure.audience.Audience;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.serializer.json.JSONComponentSerializer;
|
||||
import net.md_5.bungee.BungeeCord;
|
||||
import net.md_5.bungee.api.chat.TextComponent;
|
||||
import net.kyori.adventure.text.minimessage.MiniMessage;
|
||||
import net.md_5.bungee.api.connection.PendingConnection;
|
||||
import net.md_5.bungee.api.event.LoginEvent;
|
||||
import net.md_5.bungee.api.event.PostLoginEvent;
|
||||
import net.md_5.bungee.api.plugin.Listener;
|
||||
import net.md_5.bungee.compress.PacketCompressor;
|
||||
import net.md_5.bungee.compress.PacketDecompressor;
|
||||
import net.md_5.bungee.connection.InitialHandler;
|
||||
import net.md_5.bungee.event.EventHandler;
|
||||
import net.md_5.bungee.netty.ChannelWrapper;
|
||||
import net.md_5.bungee.protocol.packet.Kick;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.ReflectiveOperationException;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.fallback.Fallback;
|
||||
import xyz.jonesdev.sonar.bungee.fallback.compress.FallbackPacketCompressor;
|
||||
import xyz.jonesdev.sonar.bungee.fallback.compress.FallbackPacketDecompressor;
|
||||
import xyz.jonesdev.sonar.bungee.fallback.handler.FallbackInitialHandler;
|
||||
import xyz.jonesdev.sonar.bungee.SonarBungee;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
|
||||
import static net.md_5.bungee.netty.PipelineUtils.FRAME_PREPENDER;
|
||||
import java.net.InetAddress;
|
||||
import java.util.Objects;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public final class FallbackListener implements Listener {
|
||||
private final Fallback fallback;
|
||||
|
||||
private static final Field CHANNEL_WRAPPER;
|
||||
@EventHandler
|
||||
@SuppressWarnings("deprecation")
|
||||
public void handle(final @NotNull LoginEvent event) {
|
||||
final InetAddress inetAddress = event.getConnection().getAddress().getAddress();
|
||||
|
||||
static {
|
||||
try {
|
||||
CHANNEL_WRAPPER = InitialHandler.class.getDeclaredField("ch");
|
||||
CHANNEL_WRAPPER.setAccessible(true);
|
||||
} catch (Throwable throwable) {
|
||||
throw new ReflectiveOperationException(throwable);
|
||||
// Check if the number of online players using the same IP address as
|
||||
// the connecting player is greater than the configured amount
|
||||
final int maxOnlinePerIp = Sonar.get().getConfig().getMaxOnlinePerIp();
|
||||
|
||||
if (maxOnlinePerIp > 0) {
|
||||
final long onlinePerIp = SonarBungee.INSTANCE.getPlugin().getServer().getPlayers().stream()
|
||||
.filter(player -> Objects.equals(player.getAddress().getAddress(), inetAddress))
|
||||
.count()
|
||||
+ 1 /* add 1 because the player hasn't been added to the list of online players yet */;
|
||||
|
||||
// We use '>=' because the player connecting to the server hasn't joined yet
|
||||
if (onlinePerIp >= maxOnlinePerIp) {
|
||||
final FallbackInitialHandler fallbackInitialHandler = (FallbackInitialHandler) event.getConnection();
|
||||
final Component component = Sonar.get().getConfig().getTooManyOnlinePerIp();
|
||||
fallbackInitialHandler.closeWith(FallbackInitialHandler.getKickPacket(component));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
@SuppressWarnings("deprecation")
|
||||
public void handle(final @NotNull PostLoginEvent event) throws Throwable {
|
||||
public void handle(final @NotNull PostLoginEvent event) {
|
||||
if (Sonar.get().getConfig().getLockdown().isEnabled()) {
|
||||
if (!event.getPlayer().hasPermission(Sonar.get().getConfig().getLockdown().getBypassPermission())) {
|
||||
final PendingConnection pendingConnection = event.getPlayer().getPendingConnection();
|
||||
@@ -71,12 +70,12 @@ public final class FallbackListener implements Listener {
|
||||
if (pendingConnection instanceof FallbackInitialHandler) {
|
||||
final FallbackInitialHandler fallbackInitialHandler = (FallbackInitialHandler) pendingConnection;
|
||||
final Component component = Sonar.get().getConfig().getLockdown().getDisconnect();
|
||||
final String serialized = JSONComponentSerializer.json().serialize(component);
|
||||
fallbackInitialHandler.closeWith(new Kick(serialized));
|
||||
fallbackInitialHandler.closeWith(FallbackInitialHandler.getKickPacket(component));
|
||||
} else {
|
||||
// Fallback by disconnecting without a message
|
||||
pendingConnection.disconnect();
|
||||
Sonar.get().getLogger().warn("Fallback handler of {} is missing", event.getPlayer().getName());
|
||||
return;
|
||||
}
|
||||
|
||||
if (Sonar.get().getConfig().getLockdown().isLogAttempts()) {
|
||||
@@ -87,43 +86,14 @@ public final class FallbackListener implements Listener {
|
||||
.replace("%protocol%",
|
||||
String.valueOf(event.getPlayer().getPendingConnection().getVersion())));
|
||||
}
|
||||
return;
|
||||
} else if (Sonar.get().getConfig().getLockdown().isNotifyAdmins()) {
|
||||
event.getPlayer().sendMessage(new TextComponent(Sonar.get().getConfig().getLockdown().getNotification()));
|
||||
final Audience audience = Sonar.get().getVerboseHandler().getAudiences().get(event.getPlayer().getName());
|
||||
if (audience != null) {
|
||||
final String notification = Sonar.get().getConfig().getLockdown().getNotification();
|
||||
final Component deserialized = MiniMessage.miniMessage().deserialize(notification);
|
||||
audience.sendMessage(deserialized);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final InitialHandler initialHandler = (InitialHandler) event.getPlayer().getPendingConnection();
|
||||
final ChannelWrapper channelWrapper = (ChannelWrapper) CHANNEL_WRAPPER.get(initialHandler);
|
||||
|
||||
final ChannelPipeline pipeline = channelWrapper.getHandle().pipeline();
|
||||
|
||||
// You don't need the frame decoder anymore
|
||||
if (pipeline.get(FRAME_PREPENDER) != null) {
|
||||
pipeline.remove(FRAME_PREPENDER);
|
||||
}
|
||||
|
||||
final int compressionThreshold = BungeeCord.getInstance().getConfig().getCompressionThreshold();
|
||||
|
||||
// Replace compression handlers
|
||||
if (compressionThreshold > 0
|
||||
&& pipeline.get(PacketCompressor.class) != null
|
||||
&& pipeline.get(PacketDecompressor.class) != null) {
|
||||
final VelocityCompressor compressor = Natives.compress.get().create(-1);
|
||||
|
||||
// Replace (de)compressor with Velocity's to ensure better performance
|
||||
pipeline.replace(
|
||||
PacketCompressor.class,
|
||||
"compress",
|
||||
// Create a new compressor instance with the Velocity compressor
|
||||
new FallbackPacketCompressor(compressionThreshold, compressor)
|
||||
);
|
||||
pipeline.replace(
|
||||
PacketDecompressor.class,
|
||||
"decompress",
|
||||
// Create a new decompressor instance with the Velocity decompressor
|
||||
new FallbackPacketDecompressor(compressionThreshold, compressor)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -28,7 +28,6 @@ import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.fallback.Fallback;
|
||||
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.bungee.fallback.handler.FallbackInitialHandler;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.Disconnect;
|
||||
|
||||
import java.net.InetAddress;
|
||||
|
||||
-115
@@ -1,115 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.bungee.fallback.compress;
|
||||
|
||||
import com.velocitypowered.natives.compression.VelocityCompressor;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import net.md_5.bungee.compress.PacketCompressor;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.zip.DataFormatException;
|
||||
|
||||
import static com.velocitypowered.natives.util.MoreByteBufUtils.ensureCompatible;
|
||||
import static com.velocitypowered.natives.util.MoreByteBufUtils.preferredBuffer;
|
||||
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.*;
|
||||
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public final class FallbackPacketCompressor extends PacketCompressor {
|
||||
private int compressionThreshold;
|
||||
private final @NotNull VelocityCompressor compressor;
|
||||
|
||||
private static final int MAX_COMPRESSED_LENGTH = 1 << 21;
|
||||
|
||||
@Override
|
||||
public void setThreshold(final int compressionThreshold) {
|
||||
this.compressionThreshold = compressionThreshold;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handlerAdded(final ChannelHandlerContext ctx) throws Exception {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handlerRemoved(final ChannelHandlerContext ctx) throws Exception {
|
||||
compressor.close();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void encode(final ChannelHandlerContext ctx,
|
||||
final @NotNull ByteBuf msg,
|
||||
final ByteBuf out) throws Exception {
|
||||
final int uncompressed = msg.readableBytes();
|
||||
if (uncompressed < compressionThreshold) {
|
||||
// Under the threshold, do nothing.
|
||||
writeVarInt(out, uncompressed + 1);
|
||||
writeVarInt(out, 0);
|
||||
out.writeBytes(msg);
|
||||
} else {
|
||||
handleCompressed(ctx, msg, out);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleCompressed(final @NotNull ChannelHandlerContext ctx,
|
||||
final @NotNull ByteBuf msg,
|
||||
final ByteBuf out) throws DataFormatException {
|
||||
final int uncompressed = msg.readableBytes();
|
||||
|
||||
write21BitVarInt(out, 0); // Fake packet length
|
||||
writeVarInt(out, uncompressed);
|
||||
|
||||
final ByteBuf compatibleIn = ensureCompatible(ctx.alloc(), compressor, msg);
|
||||
|
||||
final int startCompressed = out.writerIndex();
|
||||
try {
|
||||
compressor.deflate(compatibleIn, out);
|
||||
} finally {
|
||||
compatibleIn.release();
|
||||
}
|
||||
|
||||
final int compressedLength = out.writerIndex() - startCompressed;
|
||||
if (compressedLength >= MAX_COMPRESSED_LENGTH) {
|
||||
throw new DataFormatException("The server sent a very large (over 2MiB compressed) packet.");
|
||||
}
|
||||
|
||||
int writerIndex = out.writerIndex();
|
||||
int packetLength = out.readableBytes() - 3;
|
||||
out.writerIndex(0);
|
||||
write21BitVarInt(out, packetLength); // Rewrite packet length
|
||||
out.writerIndex(writerIndex);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ByteBuf allocateBuffer(final ChannelHandlerContext ctx,
|
||||
final @NotNull ByteBuf msg,
|
||||
final boolean preferDirect) throws Exception {
|
||||
int uncompressed = msg.readableBytes();
|
||||
if (uncompressed < compressionThreshold) {
|
||||
int finalBufferSize = uncompressed + 1;
|
||||
finalBufferSize += varIntBytes(finalBufferSize);
|
||||
return ctx.alloc().directBuffer(finalBufferSize);
|
||||
}
|
||||
|
||||
final int initialBufferSize = (uncompressed - 1) + 3 + varIntBytes(uncompressed);
|
||||
return preferredBuffer(ctx.alloc(), compressor, initialBufferSize);
|
||||
}
|
||||
}
|
||||
-98
@@ -1,98 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.bungee.fallback.compress;
|
||||
|
||||
import com.velocitypowered.natives.compression.VelocityCompressor;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.handler.codec.CorruptedFrameException;
|
||||
import lombok.Getter;
|
||||
import net.md_5.bungee.compress.PacketDecompressor;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static com.velocitypowered.natives.util.MoreByteBufUtils.ensureCompatible;
|
||||
import static com.velocitypowered.natives.util.MoreByteBufUtils.preferredBuffer;
|
||||
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.readVarInt;
|
||||
|
||||
@Getter
|
||||
public final class FallbackPacketDecompressor extends PacketDecompressor {
|
||||
private final int compressionThreshold;
|
||||
private final @NotNull VelocityCompressor compressor;
|
||||
|
||||
private static final int VANILLA_MAXIMUM_UNCOMPRESSED_SIZE = 8 * 1024 * 1024; // 8MiB
|
||||
private static final int HARD_MAXIMUM_UNCOMPRESSED_SIZE = 16 * 1024 * 1024; // 16MiB
|
||||
private static final int UNCOMPRESSED_CAP = Boolean.getBoolean("sonar.increased-compression-cap")
|
||||
? HARD_MAXIMUM_UNCOMPRESSED_SIZE : VANILLA_MAXIMUM_UNCOMPRESSED_SIZE;
|
||||
|
||||
public FallbackPacketDecompressor(final int compressionThreshold,
|
||||
final @NotNull VelocityCompressor compressor) {
|
||||
super(); // super(compressionThreshold);
|
||||
this.compressionThreshold = compressionThreshold;
|
||||
this.compressor = compressor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handlerAdded(final ChannelHandlerContext ctx) throws Exception {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handlerRemoved(final ChannelHandlerContext ctx) throws Exception {
|
||||
compressor.close();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void decode(final ChannelHandlerContext ctx,
|
||||
final @NotNull ByteBuf in,
|
||||
final List<Object> out) throws Exception {
|
||||
final int claimedUncompressedSize = readVarInt(in);
|
||||
if (claimedUncompressedSize == 0) {
|
||||
// This message is not compressed.
|
||||
out.add(in.retain());
|
||||
|
||||
// Check for invalid packet size after the packet is retained
|
||||
final int size = in.readableBytes();
|
||||
if (size > compressionThreshold + 2) {
|
||||
throw new CorruptedFrameException("Invalid uncompressed packet size " + size);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (claimedUncompressedSize < compressionThreshold) {
|
||||
throw new CorruptedFrameException(claimedUncompressedSize + " is less than " + compressionThreshold);
|
||||
}
|
||||
if (claimedUncompressedSize > UNCOMPRESSED_CAP) {
|
||||
throw new CorruptedFrameException(claimedUncompressedSize + " exceeds maximum size");
|
||||
}
|
||||
|
||||
final ByteBuf compatibleIn = ensureCompatible(ctx.alloc(), compressor, in);
|
||||
final ByteBuf uncompressed = preferredBuffer(ctx.alloc(), compressor, claimedUncompressedSize);
|
||||
|
||||
try {
|
||||
compressor.inflate(compatibleIn, uncompressed, claimedUncompressedSize);
|
||||
out.add(uncompressed);
|
||||
} catch (Exception exception) {
|
||||
uncompressed.release();
|
||||
throw exception;
|
||||
} finally {
|
||||
compatibleIn.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
-91
@@ -1,91 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.bungee.fallback.handler;
|
||||
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelFutureListener;
|
||||
import net.md_5.bungee.BungeeCord;
|
||||
import net.md_5.bungee.api.config.ListenerInfo;
|
||||
import net.md_5.bungee.connection.InitialHandler;
|
||||
import net.md_5.bungee.netty.ChannelWrapper;
|
||||
import net.md_5.bungee.protocol.packet.LoginRequest;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.fallback.Fallback;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.bungee.fallback.FallbackUserWrapper;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public final class FallbackInitialHandler extends InitialHandler {
|
||||
public FallbackInitialHandler(final BungeeCord bungee, final ListenerInfo listener) {
|
||||
super(bungee, listener);
|
||||
}
|
||||
|
||||
private static final @NotNull Fallback FALLBACK = Objects.requireNonNull(Sonar.get().getFallback());
|
||||
private ChannelWrapper channelWrapper;
|
||||
@SuppressWarnings("unused") // TODO: remove later
|
||||
private @Nullable FallbackUserWrapper player;
|
||||
|
||||
@Override
|
||||
public void connected(final ChannelWrapper channelWrapper) throws Exception {
|
||||
this.channelWrapper = channelWrapper;
|
||||
super.connected(channelWrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(final LoginRequest loginRequest) throws Exception {
|
||||
final InetAddress inetAddress = getAddress().getAddress();
|
||||
|
||||
if (player == null && !Sonar.get().getVerifiedPlayerController().has(inetAddress)) {
|
||||
final Channel channel = channelWrapper.getHandle();
|
||||
|
||||
player = new FallbackUserWrapper(
|
||||
FALLBACK, channelWrapper, this,
|
||||
channel, channel.pipeline(), inetAddress,
|
||||
ProtocolVersion.fromId(getHandshake().getProtocolVersion())
|
||||
);
|
||||
// TODO: implement Fallback
|
||||
}
|
||||
|
||||
super.handle(loginRequest);
|
||||
}
|
||||
|
||||
// Mostly taken from Velocity
|
||||
public void closeWith(final Object msg) {
|
||||
if (player != null && player.getChannel().isActive()) {
|
||||
boolean is17 = player.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_8) < 0
|
||||
&& player.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_7_2) >= 0;
|
||||
if (is17) {
|
||||
player.getChannel().eventLoop().execute(() -> {
|
||||
channelWrapper.getHandle().config().setAutoRead(false);
|
||||
player.getChannel().eventLoop().schedule(() -> {
|
||||
channelWrapper.markClosed();
|
||||
player.getChannel().writeAndFlush(msg).addListener(ChannelFutureListener.CLOSE);
|
||||
}, 250L, TimeUnit.MILLISECONDS);
|
||||
});
|
||||
} else {
|
||||
channelWrapper.markClosed();
|
||||
player.getChannel().writeAndFlush(msg).addListener(ChannelFutureListener.CLOSE);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+7
-12
@@ -17,16 +17,15 @@
|
||||
|
||||
package xyz.jonesdev.sonar.bungee.fallback.injection;
|
||||
|
||||
import io.netty.buffer.PooledByteBufAllocator;
|
||||
import io.netty.channel.*;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.NoArgsConstructor;
|
||||
import net.md_5.bungee.BungeeCord;
|
||||
import net.md_5.bungee.netty.HandlerBoss;
|
||||
import net.md_5.bungee.netty.PipelineUtils;
|
||||
import net.md_5.bungee.protocol.Varint21LengthFieldPrepender;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.ReflectiveOperationException;
|
||||
import xyz.jonesdev.sonar.bungee.fallback.FallbackHandlerBoss;
|
||||
import xyz.jonesdev.sonar.bungee.fallback.varint.Varint21FrameDecoder;
|
||||
import xyz.jonesdev.sonar.common.fallback.FallbackTimeoutHandler;
|
||||
|
||||
@@ -41,15 +40,13 @@ public final class BaseChannelInitializer extends ChannelInitializer<Channel> {
|
||||
|
||||
private static final boolean REPLACE_VAR_INT_DECODER = !Boolean.getBoolean("sonar.do-not-replace-decoder");
|
||||
|
||||
private static final WriteBufferWaterMark MARK;
|
||||
// https://github.com/PaperMC/Velocity/blob/dev/3.0.0/proxy/src/main/java/com/velocitypowered/proxy/network/ConnectionManager.java#L57
|
||||
private static final WriteBufferWaterMark SERVER_WRITE_MARK = new WriteBufferWaterMark(1 << 20, 1 << 21);
|
||||
|
||||
private static final Varint21LengthFieldPrepender FRAME_ENCODER;
|
||||
|
||||
static {
|
||||
try {
|
||||
final Field markField = PipelineUtils.class.getDeclaredField("MARK");
|
||||
markField.setAccessible(true);
|
||||
MARK = (WriteBufferWaterMark) markField.get(null);
|
||||
|
||||
final Field frameEncoder = PipelineUtils.class.getDeclaredField("framePrepender");
|
||||
frameEncoder.setAccessible(true);
|
||||
FRAME_ENCODER = (Varint21LengthFieldPrepender) frameEncoder.get(null);
|
||||
@@ -68,15 +65,13 @@ public final class BaseChannelInitializer extends ChannelInitializer<Channel> {
|
||||
}
|
||||
|
||||
channel.config().setOption(ChannelOption.TCP_NODELAY, true);
|
||||
channel.config().setAllocator(PooledByteBufAllocator.DEFAULT);
|
||||
channel.config().setWriteBufferWaterMark(MARK);
|
||||
channel.config().setWriteBufferWaterMark(SERVER_WRITE_MARK);
|
||||
|
||||
channel.pipeline().addLast(FRAME_DECODER, REPLACE_VAR_INT_DECODER
|
||||
? new Varint21FrameDecoder() : new net.md_5.bungee.protocol.Varint21FrameDecoder());
|
||||
channel.pipeline().addLast(TIMEOUT_HANDLER, new FallbackTimeoutHandler(
|
||||
BungeeCord.getInstance().config.getTimeout(), TimeUnit.MILLISECONDS
|
||||
));
|
||||
BungeeCord.getInstance().config.getTimeout(), TimeUnit.MILLISECONDS));
|
||||
channel.pipeline().addLast(FRAME_PREPENDER, FRAME_ENCODER);
|
||||
channel.pipeline().addLast(BOSS_HANDLER, new HandlerBoss());
|
||||
channel.pipeline().addLast(BOSS_HANDLER, new FallbackHandlerBoss());
|
||||
}
|
||||
}
|
||||
+10
-11
@@ -23,15 +23,15 @@ import io.netty.handler.codec.haproxy.HAProxyMessageDecoder;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.NoArgsConstructor;
|
||||
import net.md_5.bungee.BungeeCord;
|
||||
import net.md_5.bungee.api.ProxyServer;
|
||||
import net.md_5.bungee.api.config.ListenerInfo;
|
||||
import net.md_5.bungee.api.event.ClientConnectEvent;
|
||||
import net.md_5.bungee.netty.HandlerBoss;
|
||||
import net.md_5.bungee.netty.PipelineUtils;
|
||||
import net.md_5.bungee.protocol.*;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.ReflectiveOperationException;
|
||||
import xyz.jonesdev.sonar.bungee.fallback.handler.FallbackInitialHandler;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.bungee.fallback.FallbackHandlerBoss;
|
||||
import xyz.jonesdev.sonar.bungee.fallback.FallbackInitialHandler;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.net.SocketAddress;
|
||||
@@ -43,7 +43,7 @@ public final class ChildChannelInitializer extends ChannelInitializer<Channel> {
|
||||
public static final ChildChannelInitializer INSTANCE = new ChildChannelInitializer();
|
||||
|
||||
private static final BaseChannelInitializer BASE = BaseChannelInitializer.INSTANCE;
|
||||
|
||||
private static final BungeeCord BUNGEE = BungeeCord.getInstance();
|
||||
private static final KickStringWriter LEGACY_KICK;
|
||||
|
||||
static {
|
||||
@@ -63,7 +63,7 @@ public final class ChildChannelInitializer extends ChannelInitializer<Channel> {
|
||||
final SocketAddress remoteAddress = channel.remoteAddress() == null ? channel.parent().localAddress()
|
||||
: channel.remoteAddress();
|
||||
|
||||
if (BungeeCord.getInstance().getConnectionThrottle() != null && BungeeCord.getInstance().getConnectionThrottle().throttle(remoteAddress)) {
|
||||
if (BUNGEE.getConnectionThrottle() != null && BUNGEE.getConnectionThrottle().throttle(remoteAddress)) {
|
||||
channel.close();
|
||||
} else {
|
||||
final ListenerInfo listener = channel.attr(PipelineUtils.LISTENER).get();
|
||||
@@ -80,13 +80,12 @@ public final class ChildChannelInitializer extends ChannelInitializer<Channel> {
|
||||
}
|
||||
|
||||
channel.pipeline().addBefore(FRAME_DECODER, LEGACY_DECODER, new LegacyDecoder());
|
||||
channel.pipeline().addAfter(FRAME_DECODER, PACKET_DECODER, new MinecraftDecoder(Protocol.HANDSHAKE, true,
|
||||
ProxyServer.getInstance().getProtocolVersion()));
|
||||
channel.pipeline().addAfter(FRAME_PREPENDER, PACKET_ENCODER, new MinecraftEncoder(Protocol.HANDSHAKE,
|
||||
true, ProxyServer.getInstance().getProtocolVersion()));
|
||||
channel.pipeline().addAfter(FRAME_DECODER, PACKET_DECODER, new MinecraftDecoder(
|
||||
Protocol.HANDSHAKE, true, ProtocolVersion.LATEST_VERSION.getProtocol()));
|
||||
channel.pipeline().addAfter(FRAME_PREPENDER, PACKET_ENCODER, new MinecraftEncoder(
|
||||
Protocol.HANDSHAKE, true, ProtocolVersion.LATEST_VERSION.getProtocol()));
|
||||
channel.pipeline().addBefore(FRAME_PREPENDER, LEGACY_KICKER, LEGACY_KICK);
|
||||
channel.pipeline().get(HandlerBoss.class).setHandler(new FallbackInitialHandler(BungeeCord.getInstance(),
|
||||
listener));
|
||||
channel.pipeline().get(FallbackHandlerBoss.class).setHandler(new FallbackInitialHandler(BUNGEE, listener));
|
||||
|
||||
if (listener.isProxyProtocol()) {
|
||||
channel.pipeline().addFirst(new HAProxyMessageDecoder());
|
||||
|
||||
+2
-1
@@ -21,6 +21,7 @@ import io.netty.buffer.ByteBuf;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.handler.codec.ByteToMessageDecoder;
|
||||
import io.netty.handler.codec.CorruptedFrameException;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@@ -29,7 +30,7 @@ import java.util.List;
|
||||
public final class Varint21FrameDecoder extends ByteToMessageDecoder {
|
||||
|
||||
@Override
|
||||
protected void decode(final ChannelHandlerContext ctx,
|
||||
protected void decode(final @NotNull ChannelHandlerContext ctx,
|
||||
final ByteBuf byteBuf,
|
||||
final List<Object> out) throws Exception {
|
||||
if (!ctx.channel().isActive() || !byteBuf.isReadable()) {
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.bungee.verbose;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.md_5.bungee.api.ProxyServer;
|
||||
import net.md_5.bungee.api.connection.ProxiedPlayer;
|
||||
import xyz.jonesdev.sonar.api.verbose.Verbose;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public final class VerboseWrapper extends Verbose {
|
||||
private final ProxyServer server;
|
||||
|
||||
@Override
|
||||
public void broadcast(final Component component) {
|
||||
synchronized (subscribers) {
|
||||
for (final String subscriber : subscribers) {
|
||||
final ProxiedPlayer player = server.getPlayer(subscriber);
|
||||
if (player != null) {
|
||||
// TODO: use an adventure audience
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,7 @@ import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.cappuccino.Cappuccino;
|
||||
import xyz.jonesdev.cappuccino.ExpiringCache;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.SonarPlatform;
|
||||
import xyz.jonesdev.sonar.api.SonarSupplier;
|
||||
import xyz.jonesdev.sonar.api.command.subcommand.SubcommandRegistry;
|
||||
import xyz.jonesdev.sonar.api.config.SonarConfiguration;
|
||||
@@ -32,6 +33,7 @@ import xyz.jonesdev.sonar.api.fallback.FallbackRatelimiter;
|
||||
import xyz.jonesdev.sonar.api.timer.SystemTimer;
|
||||
import xyz.jonesdev.sonar.api.verbose.Verbose;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer;
|
||||
import xyz.jonesdev.sonar.common.service.ServiceRepository;
|
||||
import xyz.jonesdev.sonar.common.subcommand.SubcommandRegistryHolder;
|
||||
import xyz.jonesdev.sonar.common.update.UpdateChecker;
|
||||
|
||||
@@ -48,19 +50,21 @@ public abstract class SonarBootstrap<T> implements Sonar {
|
||||
private SonarConfiguration config;
|
||||
private VerifiedPlayerController verifiedPlayerController;
|
||||
private File dataDirectory;
|
||||
private final SonarPlatform platform;
|
||||
private final SubcommandRegistry subcommandRegistry;
|
||||
private final SystemTimer launchTimer = new SystemTimer();
|
||||
|
||||
public SonarBootstrap(final @NotNull T plugin,
|
||||
final File dataDirectory,
|
||||
final Verbose verboseHandler) {
|
||||
// Set the API to this instance so the config doesn't have issues
|
||||
final SonarPlatform platform) {
|
||||
// Set the Sonar API
|
||||
SonarSupplier.set(this);
|
||||
|
||||
// Set the plugin instance before anything else
|
||||
this.plugin = plugin;
|
||||
this.dataDirectory = dataDirectory;
|
||||
this.verboseHandler = verboseHandler;
|
||||
this.platform = platform;
|
||||
this.verboseHandler = new Verbose();
|
||||
this.config = new SonarConfiguration(dataDirectory);
|
||||
this.subcommandRegistry = new SubcommandRegistryHolder();
|
||||
}
|
||||
@@ -82,6 +86,9 @@ public abstract class SonarBootstrap<T> implements Sonar {
|
||||
getLogger().info("Successfully initialized components in {}s!", launchTimer);
|
||||
getLogger().info("Enabling all tasks and features...");
|
||||
|
||||
// Start all service threads
|
||||
ServiceRepository.register();
|
||||
|
||||
try {
|
||||
// Run the per-platform initialization method
|
||||
enable();
|
||||
@@ -126,7 +133,10 @@ public abstract class SonarBootstrap<T> implements Sonar {
|
||||
|
||||
public void shutdown() {
|
||||
getLogger().info("Starting shutdown process...");
|
||||
// ...
|
||||
|
||||
// Shut down service threads
|
||||
ServiceRepository.shutdown();
|
||||
|
||||
getLogger().info("Successfully shut down. Goodbye!");
|
||||
}
|
||||
}
|
||||
+7
-8
@@ -20,24 +20,23 @@ package xyz.jonesdev.sonar.common.fallback;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.channel.ChannelInboundHandlerAdapter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.fallback.Fallback;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public final class FallbackChannelHandler extends ChannelInboundHandlerAdapter {
|
||||
private static final Fallback FALLBACK = Sonar.get().getFallback();
|
||||
private final String username;
|
||||
private static final @NotNull Fallback FALLBACK = Sonar.get().getFallback();
|
||||
private final @NotNull String username;
|
||||
|
||||
@Override
|
||||
public void channelUnregistered(final ChannelHandlerContext ctx) {
|
||||
final InetAddress inetAddress = ((InetSocketAddress) ctx.channel().remoteAddress()).getAddress();
|
||||
|
||||
// Remove the IP address from the queue
|
||||
public void channelUnregistered(final @NotNull ChannelHandlerContext ctx) {
|
||||
// Remove the username from the connected players
|
||||
FALLBACK.getConnected().remove(username);
|
||||
FALLBACK.getQueue().remove(inetAddress);
|
||||
// Remove the IP address from the queue
|
||||
FALLBACK.getQueue().remove(((InetSocketAddress) ctx.channel().remoteAddress()).getAddress());
|
||||
|
||||
ctx.fireChannelUnregistered();
|
||||
}
|
||||
|
||||
+6
-4
@@ -21,24 +21,26 @@ import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.handler.timeout.IdleState;
|
||||
import io.netty.handler.timeout.IdleStateEvent;
|
||||
import io.netty.handler.timeout.IdleStateHandler;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public final class FallbackTimeoutHandler extends IdleStateHandler {
|
||||
private boolean closed;
|
||||
private boolean knownDisconnect;
|
||||
|
||||
public FallbackTimeoutHandler(final long timeout, final TimeUnit timeUnit) {
|
||||
super(timeout, 0L, 0L, timeUnit);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void channelIdle(final ChannelHandlerContext ctx, final IdleStateEvent idleStateEvent) throws Exception {
|
||||
protected void channelIdle(final ChannelHandlerContext ctx,
|
||||
final @NotNull IdleStateEvent idleStateEvent) throws Exception {
|
||||
assert idleStateEvent.state() == IdleState.READER_IDLE;
|
||||
readTimedOut(ctx);
|
||||
}
|
||||
|
||||
private void readTimedOut(final ChannelHandlerContext ctx) {
|
||||
if (!closed) {
|
||||
if (!knownDisconnect) {
|
||||
|
||||
// The netty (default) ReadTimeoutHandler would normally just throw an Exception
|
||||
// The default ReadTimeoutHandler does only check for the boolean 'closed' and
|
||||
@@ -47,7 +49,7 @@ public final class FallbackTimeoutHandler extends IdleStateHandler {
|
||||
ctx.close();
|
||||
}
|
||||
|
||||
closed = true;
|
||||
knownDisconnect = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
+28
-38
@@ -69,7 +69,7 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
|
||||
// post-JOIN checks
|
||||
CLIENT_SETTINGS, PLUGIN_MESSAGE, TRANSACTION,
|
||||
// PLAY checks
|
||||
TELEPORT, POSITION, COLLISIONS,
|
||||
TELEPORT, POSITION,
|
||||
// Done
|
||||
SUCCESS
|
||||
}
|
||||
@@ -90,8 +90,8 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
|
||||
private void configure() {
|
||||
// Set the state to CONFIGURE to avoid false positives
|
||||
state = State.CONFIGURE;
|
||||
// Send necessary configuration packets to the client
|
||||
user.delayedWrite(synchronizeRegistry);
|
||||
// Send the necessary configuration packets to the client
|
||||
user.delayedWrite(REGISTRY_SYNC);
|
||||
user.delayedWrite(FINISH_CONFIGURATION);
|
||||
// Send all packets in one flush
|
||||
user.getChannel().flush();
|
||||
@@ -146,7 +146,7 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
|
||||
state = State.CLIENT_SETTINGS;
|
||||
}
|
||||
// Select the JoinGame packet for the respective protocol version
|
||||
user.write(getJoinPacketForVersion(user.getProtocolVersion()));
|
||||
user.write(joinGame);
|
||||
if (v1_20_2) {
|
||||
// Perform the transaction check since the ClientSettings
|
||||
// and PluginMessage packets have already been validated.
|
||||
@@ -175,11 +175,9 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
|
||||
state = State.POSITION;
|
||||
// Teleport player into the fake lobby by sending an empty chunk
|
||||
user.delayedWrite(EMPTY_CHUNK_DATA);
|
||||
if (Sonar.get().getConfig().getVerification().isCheckCollisions()) {
|
||||
// Send an UpdateSectionBlocks packet with a platform of blocks
|
||||
// to check if the player collides with the solid platform.
|
||||
user.delayedWrite(UPDATE_SECTION_BLOCKS);
|
||||
}
|
||||
// Send an UpdateSectionBlocks packet with a platform of blocks
|
||||
// to check if the player collides with the solid platform.
|
||||
user.delayedWrite(updateSectionBlocks);
|
||||
// Send all packets in one flush
|
||||
user.getChannel().flush();
|
||||
}
|
||||
@@ -220,7 +218,7 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
|
||||
if (state == State.SUCCESS) return;
|
||||
|
||||
// Check if the player is not sending a ton of packets to the server
|
||||
final int maxPackets = Sonar.get().getConfig().getVerification().getMaxLoginPackets() + maxPredictionTick;
|
||||
final int maxPackets = Sonar.get().getConfig().getVerification().getMaxLoginPackets() + maxMovementTick;
|
||||
checkFrame(++totalReceivedPackets < maxPackets, "too many packets");
|
||||
|
||||
// Check for timeout since the player could be sending packets but not important ones
|
||||
@@ -329,7 +327,7 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: implement ChunkData packet for 1.20.2 and fix block state Ids for barriers
|
||||
// TODO: implement ChunkData packet for 1.20.2
|
||||
if (user.getProtocolVersion().compareTo(MINECRAFT_1_20_2) >= 0) {
|
||||
finish();
|
||||
return;
|
||||
@@ -405,11 +403,8 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
|
||||
|
||||
// The player is not allowed to move away from the collision platform.
|
||||
// This should not happen unless the max movement tick is configured to a high number.
|
||||
checkFrame(Math.abs(x - BLOCKS_PER_ROW) <= BLOCKS_PER_ROW, "moved too far (x)");
|
||||
checkFrame(Math.abs(z - BLOCKS_PER_ROW) <= BLOCKS_PER_ROW, "moved too far (z)");
|
||||
|
||||
// The onGround property can never be true when we aren't checking for collisions
|
||||
checkFrame(!ground || state == State.COLLISIONS, "invalid ground state");
|
||||
checkFrame(Math.abs(x - BLOCKS_PER_ROW) < BLOCKS_PER_ROW, "moved too far (x)");
|
||||
checkFrame(Math.abs(z - BLOCKS_PER_ROW) < BLOCKS_PER_ROW, "moved too far (z)");
|
||||
|
||||
// Check if the client hasn't moved before sending the first movement packet
|
||||
if (!listenForMovements) {
|
||||
@@ -432,39 +427,34 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate the difference between the player's Y coordinate and the expected Y coordinate
|
||||
final double collisionOffsetY = DEFAULT_Y_COLLIDE_POSITION - y;
|
||||
|
||||
// Check if the player is colliding by performing a basic Y offset check.
|
||||
// The offset cannot be greater than 0 since the blocks will not let the player fall through them.
|
||||
checkFrame(collisionOffsetY <= 0, "fell through blocks: " + collisionOffsetY);
|
||||
|
||||
if (tick > maxMovementTick) {
|
||||
if (!Sonar.get().getConfig().getVerification().isCheckCollisions()) {
|
||||
// Checking collisions is disabled, just finish verification
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
if (state != State.COLLISIONS) {
|
||||
// Now we don't care about gravity anymore,
|
||||
// we just want the player to collide with the blocks.
|
||||
state = State.COLLISIONS;
|
||||
}
|
||||
|
||||
// Calculate the difference between the player's Y coordinate and the expected Y coordinate
|
||||
final double offsetY = DEFAULT_Y_COLLIDE_POSITION - y;
|
||||
|
||||
// Log/debug position if enabled in the configuration
|
||||
if (Sonar.get().getConfig().getVerification().isDebugXYZPositions()) {
|
||||
user.getFallback().getLogger().info("{}: {}/{}/{} - offset: {}", username, x, y, z, offsetY);
|
||||
user.getFallback().getLogger().info("{}: {}/{}/{} - deltaY: {} - ground: {} - collision: {}",
|
||||
username, x, y, z, deltaY, ground, collisionOffsetY);
|
||||
}
|
||||
|
||||
// Check if the player is colliding by performing a basic Y offset check.
|
||||
// The offset cannot be greater than 0 since the blocks will not let the player fall through them.
|
||||
checkFrame(offsetY <= 0, "no collisions: " + offsetY);
|
||||
|
||||
// The player is colliding to blocks, finish verification
|
||||
if (ground) {
|
||||
// The player is colliding to blocks, finish verification
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// Check if the player is spoofing the ground state
|
||||
checkFrame(!ground, "spoofed ground state");
|
||||
}
|
||||
|
||||
if (!ground && tick < maxPredictionTick) {
|
||||
// Make sure we don't run out of predicted Y motions
|
||||
checkFrame(tick < preparedCachedYMotions.length, "too many movements");
|
||||
|
||||
if (!ground) {
|
||||
final double predictedY = preparedCachedYMotions[tick];
|
||||
final double offsetY = Math.abs(deltaY - predictedY);
|
||||
|
||||
|
||||
+4
-23
@@ -374,26 +374,10 @@ public enum FallbackPacketRegistry {
|
||||
private static ProtocolVersion getProtocolVersion(final @NotNull PacketMapping current,
|
||||
final @NotNull PacketMapping next,
|
||||
final @NotNull ProtocolVersion from) {
|
||||
final ProtocolVersion lastValid = current.lastValidProtocolVersion;
|
||||
|
||||
if (lastValid != null) {
|
||||
if (next != current) {
|
||||
throw new IllegalArgumentException("Cannot add a mapping after last valid mapping");
|
||||
}
|
||||
|
||||
if (from.compareTo(lastValid) > 0) {
|
||||
throw new IllegalArgumentException(
|
||||
"Last mapping version cannot be higher than highest mapping version");
|
||||
}
|
||||
}
|
||||
|
||||
final ProtocolVersion last = (ProtocolVersion) SUPPORTED_VERSIONS.toArray()[SUPPORTED_VERSIONS.size() - 1];
|
||||
final ProtocolVersion to = current == next ? lastValid != null
|
||||
? lastValid : last : next.protocolVersion;
|
||||
final ProtocolVersion to = current == next ? last : next.protocolVersion;
|
||||
|
||||
final ProtocolVersion lastInList = lastValid != null ? lastValid : last;
|
||||
|
||||
if (from.compareTo(to) >= 0 && from != lastInList) {
|
||||
if (from.compareTo(to) >= 0 && from != last) {
|
||||
throw new IllegalArgumentException(String.format(
|
||||
"Next mapping version (%s) should be lower then current (%s)", to, from));
|
||||
}
|
||||
@@ -402,7 +386,7 @@ public enum FallbackPacketRegistry {
|
||||
}
|
||||
|
||||
public static class ProtocolRegistry {
|
||||
public final ProtocolVersion version;
|
||||
private final ProtocolVersion version;
|
||||
private final IntObjectMap<Supplier<? extends FallbackPacket>> packetIdToSupplier =
|
||||
new IntObjectHashMap<>(16, 0.5f);
|
||||
private final Map<Class<? extends FallbackPacket>, Integer> packetClassToId =
|
||||
@@ -436,15 +420,12 @@ public enum FallbackPacketRegistry {
|
||||
private final int id;
|
||||
private final ProtocolVersion protocolVersion;
|
||||
private final boolean encodeOnly;
|
||||
private final ProtocolVersion lastValidProtocolVersion;
|
||||
|
||||
PacketMapping(final int id,
|
||||
final ProtocolVersion protocolVersion,
|
||||
final ProtocolVersion lastValidProtocolVersion,
|
||||
final boolean packetDecoding) {
|
||||
this.id = id;
|
||||
this.protocolVersion = protocolVersion;
|
||||
this.lastValidProtocolVersion = lastValidProtocolVersion;
|
||||
this.encodeOnly = packetDecoding;
|
||||
}
|
||||
}
|
||||
@@ -452,6 +433,6 @@ public enum FallbackPacketRegistry {
|
||||
private static @NotNull PacketMapping map(final int id,
|
||||
final ProtocolVersion version,
|
||||
final boolean encodeOnly) {
|
||||
return new PacketMapping(id, version, null, encodeOnly);
|
||||
return new PacketMapping(id, version, encodeOnly);
|
||||
}
|
||||
}
|
||||
+28
-223
@@ -18,18 +18,10 @@
|
||||
package xyz.jonesdev.sonar.common.fallback.protocol;
|
||||
|
||||
import lombok.experimental.UtilityClass;
|
||||
import net.kyori.adventure.nbt.BinaryTagIO;
|
||||
import net.kyori.adventure.nbt.BinaryTagTypes;
|
||||
import net.kyori.adventure.nbt.CompoundBinaryTag;
|
||||
import net.kyori.adventure.nbt.ListBinaryTag;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.block.BlockPosition;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.block.BlockType;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.block.ChangedBlock;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.dimension.DimensionInfo;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.config.FinishConfiguration;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.config.RegistrySync;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.Abilities;
|
||||
@@ -37,92 +29,58 @@ import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.EmptyChunkData;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.JoinGame;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.UpdateSectionBlocks;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.util.Collections;
|
||||
import java.util.Objects;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
|
||||
|
||||
@UtilityClass
|
||||
public class FallbackPreparer {
|
||||
|
||||
// Mappings
|
||||
private final CompoundBinaryTag CHAT_TYPE_119;
|
||||
private final CompoundBinaryTag CHAT_TYPE_1191;
|
||||
private final CompoundBinaryTag DAMAGE_TYPE_1194;
|
||||
private final CompoundBinaryTag DAMAGE_TYPE_120;
|
||||
|
||||
static {
|
||||
CHAT_TYPE_119 = getMapping("chat_1_19.nbt");
|
||||
CHAT_TYPE_1191 = getMapping("chat_1_19_1.nbt");
|
||||
DAMAGE_TYPE_1194 = getMapping("damage_1_19_4.nbt");
|
||||
DAMAGE_TYPE_120 = getMapping("damage_type_1_20.nbt");
|
||||
}
|
||||
|
||||
// Abilities
|
||||
public final FallbackPacket DEFAULT_ABILITIES = new Abilities((byte) 0, 0f, 0f);
|
||||
// Chunks
|
||||
public final FallbackPacket EMPTY_CHUNK_DATA = new EmptyChunkData(0, 0);
|
||||
// Finish Configuration
|
||||
public final FallbackPacket FINISH_CONFIGURATION = new FinishConfiguration();
|
||||
// Registry Sync
|
||||
public FallbackPacket synchronizeRegistry;
|
||||
|
||||
// Synchronize Registry
|
||||
public final FallbackPacket REGISTRY_SYNC = new RegistrySync();
|
||||
// JoinGame
|
||||
private FallbackPacket legacyJoinGame;
|
||||
private FallbackPacket joinGame116;
|
||||
private FallbackPacket joinGame1162;
|
||||
private FallbackPacket joinGame1182;
|
||||
private FallbackPacket joinGame1191;
|
||||
private FallbackPacket joinGame1194;
|
||||
private JoinGame joinGame120;
|
||||
|
||||
// Dimension
|
||||
private final DimensionInfo DIMENSION = new DimensionInfo(
|
||||
"minecraft:overworld", "sonar", false, false);
|
||||
public FallbackPacket joinGame;
|
||||
// Update Section Blocks
|
||||
public FallbackPacket updateSectionBlocks;
|
||||
|
||||
// Collisions
|
||||
public final int BLOCKS_PER_ROW = 8; // 8 * 8 = 64 (max)
|
||||
private final int SPAWN_BUFFER = 5; // player spawns at 255 + 5 (5 blocks above the platform)
|
||||
public final int BLOCKS_PER_ROW = 8; // 8 * 8 = 64 (protocol maximum)
|
||||
public final int SPAWN_X_POSITION = 16 / 2; // middle of the chunk
|
||||
public final int SPAWN_Z_POSITION = 16 / 2; // middle of the chunk
|
||||
public final int DEFAULT_Y_COLLIDE_POSITION = 255; // 255 is the maximum Y position allowed
|
||||
|
||||
public UpdateSectionBlocks UPDATE_SECTION_BLOCKS;
|
||||
private final ChangedBlock[] CHANGED_BLOCKS = new ChangedBlock[BLOCKS_PER_ROW * BLOCKS_PER_ROW];
|
||||
|
||||
public int maxMovementTick, maxPredictionTick, dynamicSpawnYPosition;
|
||||
public int maxMovementTick, dynamicSpawnYPosition;
|
||||
public double[] preparedCachedYMotions;
|
||||
public double maxFallDistance;
|
||||
public final int SPAWN_X_POSITION = BLOCKS_PER_ROW; // middle of the chunk
|
||||
public final int SPAWN_Z_POSITION = BLOCKS_PER_ROW;
|
||||
|
||||
public void prepare() {
|
||||
legacyJoinGame = createJoinGamePacket(MINECRAFT_1_8);
|
||||
joinGame116 = createJoinGamePacket(MINECRAFT_1_16);
|
||||
joinGame1162 = createJoinGamePacket(MINECRAFT_1_16_2);
|
||||
joinGame1182 = createJoinGamePacket(MINECRAFT_1_18_2);
|
||||
joinGame1191 = createJoinGamePacket(MINECRAFT_1_19_1);
|
||||
joinGame1194 = createJoinGamePacket(MINECRAFT_1_19_4);
|
||||
joinGame120 = createJoinGamePacket(MINECRAFT_1_20);
|
||||
synchronizeRegistry = new RegistrySync(joinGame120.getRegistry());
|
||||
joinGame = new JoinGame(5,
|
||||
Sonar.get().getConfig().getVerification().getGamemode().getId(),
|
||||
0,
|
||||
false,
|
||||
0,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
new String[]{"minecraft:overworld"},
|
||||
"minecraft:overworld");
|
||||
|
||||
maxMovementTick = Sonar.get().getConfig().getVerification().getMaxMovementTicks();
|
||||
maxPredictionTick = maxMovementTick + 10;
|
||||
preparedCachedYMotions = new double[maxPredictionTick + 1];
|
||||
|
||||
for (int i = 0; i < maxPredictionTick + 1; i++) {
|
||||
preparedCachedYMotions[i] = -((Math.pow(0.98, i) - 1) * 3.92);
|
||||
}
|
||||
|
||||
// Adjust block and collide Y position based on max fall distance
|
||||
maxFallDistance = 0;
|
||||
for (int i = 0; i < maxMovementTick; i++) {
|
||||
maxFallDistance += preparedCachedYMotions[i];
|
||||
maxMovementTick = Sonar.get().getConfig().getVerification().getMaxMovementTicks();
|
||||
preparedCachedYMotions = new double[maxMovementTick + 8];
|
||||
|
||||
for (int i = 0; i < preparedCachedYMotions.length; i++) {
|
||||
final double gravity = -((Math.pow(0.98, i) - 1) * 3.92);
|
||||
preparedCachedYMotions[i] = gravity;
|
||||
maxFallDistance += gravity;
|
||||
}
|
||||
|
||||
// Set the dynamic spawn buffer
|
||||
final int DYNAMIC_SPAWN_BUFFER = (int) (SPAWN_BUFFER + maxFallDistance);
|
||||
// Set the dynamic block and collide Y position based on the maximum fall distance
|
||||
dynamicSpawnYPosition = DEFAULT_Y_COLLIDE_POSITION + DYNAMIC_SPAWN_BUFFER;
|
||||
dynamicSpawnYPosition = DEFAULT_Y_COLLIDE_POSITION + (int) Math.ceil(maxFallDistance);
|
||||
|
||||
// Prepare collision platform positions
|
||||
int index = 0;
|
||||
@@ -139,159 +97,6 @@ public class FallbackPreparer {
|
||||
}
|
||||
|
||||
// Prepare UpdateSectionBlocks packet
|
||||
UPDATE_SECTION_BLOCKS = new UpdateSectionBlocks(0, 0, CHANGED_BLOCKS);
|
||||
}
|
||||
|
||||
private @Nullable CompoundBinaryTag getMapping(final @NotNull String fileName) {
|
||||
try (final InputStream inputStream = Sonar.class.getResourceAsStream("/mappings/" + fileName)) {
|
||||
return BinaryTagIO.reader().read(Objects.requireNonNull(inputStream), BinaryTagIO.Compression.GZIP);
|
||||
} catch (Throwable throwable) {
|
||||
Sonar.get().getLogger().error("Could not load mappings for {}: {}", fileName, throwable);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static FallbackPacket getJoinPacketForVersion(final @NotNull ProtocolVersion protocolVersion) {
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_15_2) <= 0) {
|
||||
return legacyJoinGame; // 1.7.2-1.15.2
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_16_1) <= 0) {
|
||||
return joinGame116; // 1.16-1.16.1
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_18) <= 0) {
|
||||
return joinGame1162; // 1.16.2-1.18
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_19) <= 0) {
|
||||
return joinGame1182; // 1.18.1-1.19
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_19_3) <= 0) {
|
||||
return joinGame1191; // 1.19.1-1.19.3
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_19_4) == 0) {
|
||||
return joinGame1194; // 1.19.4
|
||||
}
|
||||
return joinGame120; // 1.20-latest
|
||||
}
|
||||
|
||||
// Partially taken from
|
||||
// https://github.com/Elytrium/LimboAPI/blob/master/plugin/src/main/java/net/elytrium/limboapi/server/LimboImpl.java#L607
|
||||
private @NotNull JoinGame createJoinGamePacket(final @NotNull ProtocolVersion protocolVersion) {
|
||||
final JoinGame joinGame = new JoinGame();
|
||||
|
||||
joinGame.setLevelType("flat");
|
||||
joinGame.setGamemode(Sonar.get().getConfig().getVerification().getGamemodeId());
|
||||
joinGame.setDimension(0);
|
||||
joinGame.setReducedDebugInfo(true);
|
||||
|
||||
// 1.7.2-1.8.9 don't need any dimension information
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_8) <= 0) {
|
||||
return joinGame;
|
||||
}
|
||||
|
||||
joinGame.setDimensionInfo(DIMENSION);
|
||||
|
||||
final CompoundBinaryTag.Builder registryContainer = CompoundBinaryTag.builder();
|
||||
final ListBinaryTag encodedDimensionRegistry = ListBinaryTag.builder(BinaryTagTypes.COMPOUND)
|
||||
.add(createDimensionData(protocolVersion))
|
||||
.build();
|
||||
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_16_2) >= 0) {
|
||||
final CompoundBinaryTag.Builder dimensionRegistryEntry = CompoundBinaryTag.builder();
|
||||
|
||||
dimensionRegistryEntry.putString("type", "minecraft:dimension_type");
|
||||
dimensionRegistryEntry.put("value", encodedDimensionRegistry);
|
||||
|
||||
registryContainer.put("minecraft:dimension_type", dimensionRegistryEntry.build());
|
||||
|
||||
final CompoundBinaryTag.Builder effectsTagBuilder = CompoundBinaryTag.builder()
|
||||
.putInt("sky_color", 7907327)
|
||||
.putInt("fog_color", 12638463)
|
||||
.putInt("water_color", 0)
|
||||
.putInt("water_fog_color", 0);
|
||||
|
||||
final CompoundBinaryTag.Builder elementTagBuilder = CompoundBinaryTag.builder()
|
||||
.putFloat("depth", 0.125f)
|
||||
.putFloat("temperature", 0.8f)
|
||||
.putFloat("scale", 0.05f)
|
||||
.putFloat("downfall", 0.4f)
|
||||
.putString("category", "plains")
|
||||
.put("effects", effectsTagBuilder.build());
|
||||
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_19_4) >= 0) {
|
||||
elementTagBuilder.putBoolean("has_precipitation", false);
|
||||
} else {
|
||||
elementTagBuilder.putString("precipitation", "rain");
|
||||
}
|
||||
|
||||
registryContainer.put("minecraft:worldgen/biome", CompoundBinaryTag.builder()
|
||||
.putString("type", "minecraft:worldgen/biome")
|
||||
.put("value", ListBinaryTag.from(Collections.singletonList(
|
||||
CompoundBinaryTag.builder()
|
||||
.putString("name", "minecraft:plains")
|
||||
.putInt("id", 1)
|
||||
.put("element", elementTagBuilder.build())
|
||||
.build()
|
||||
))).build()
|
||||
);
|
||||
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_19) == 0) {
|
||||
registryContainer.put("minecraft:chat_type", CHAT_TYPE_119);
|
||||
} else if (protocolVersion.compareTo(MINECRAFT_1_19_1) >= 0) {
|
||||
registryContainer.put("minecraft:chat_type", CHAT_TYPE_1191);
|
||||
}
|
||||
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_19_4) == 0) {
|
||||
registryContainer.put("minecraft:damage_type", DAMAGE_TYPE_1194);
|
||||
} else if (protocolVersion.compareTo(MINECRAFT_1_20) >= 0) {
|
||||
registryContainer.put("minecraft:damage_type", DAMAGE_TYPE_120);
|
||||
}
|
||||
} else {
|
||||
registryContainer.put("dimension", encodedDimensionRegistry);
|
||||
}
|
||||
|
||||
CompoundBinaryTag currentDimensionData = encodedDimensionRegistry.getCompound(0);
|
||||
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_16_2) >= 0) {
|
||||
currentDimensionData = currentDimensionData.getCompound("element");
|
||||
}
|
||||
|
||||
joinGame.setCurrentDimensionData(currentDimensionData);
|
||||
joinGame.setLevelNames(new String[]{"minecraft:overworld"});
|
||||
joinGame.setRegistry(registryContainer.build());
|
||||
return joinGame;
|
||||
}
|
||||
|
||||
public @NotNull CompoundBinaryTag createDimensionData(final @NotNull ProtocolVersion version) {
|
||||
final CompoundBinaryTag details = CompoundBinaryTag.builder()
|
||||
.putBoolean("natural", false)
|
||||
.putFloat("ambient_light", 0f)
|
||||
.putBoolean("shrunk", false)
|
||||
.putBoolean("ultrawarm", false)
|
||||
.putBoolean("has_ceiling", false)
|
||||
.putBoolean("has_skylight", true)
|
||||
.putBoolean("piglin_safe", false)
|
||||
.putBoolean("bed_works", false)
|
||||
.putBoolean("respawn_anchor_works", false)
|
||||
.putBoolean("has_raids", false)
|
||||
.putInt("logical_height", 256)
|
||||
.putString("infiniburn", version.compareTo(MINECRAFT_1_18_2) >= 0 ? "#minecraft" +
|
||||
":infiniburn_nether" : "minecraft:infiniburn_nether")
|
||||
.putDouble("coordinate_scale", 1.0)
|
||||
.putString("effects", "minecraft:overworld")
|
||||
.putInt("min_y", 0)
|
||||
.putInt("height", 256)
|
||||
.putInt("monster_spawn_block_light_limit", 0)
|
||||
.putInt("monster_spawn_light_level", 0)
|
||||
.build();
|
||||
|
||||
if (version.compareTo(MINECRAFT_1_16_2) >= 0) {
|
||||
return CompoundBinaryTag.builder()
|
||||
.putString("name", "minecraft:overworld")
|
||||
.putInt("id", 0)
|
||||
.put("element", details)
|
||||
.build();
|
||||
}
|
||||
|
||||
return details.putString("name", "minecraft:overworld");
|
||||
updateSectionBlocks = new UpdateSectionBlocks(0, 0, CHANGED_BLOCKS);
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -73,7 +73,7 @@ public enum BlockType {
|
||||
case MINECRAFT_1_20:
|
||||
return 10225;
|
||||
case MINECRAFT_1_20_2:
|
||||
return 1; // TODO: https://github.com/PrismarineJS/minecraft-data/issues/779
|
||||
return 10365;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
+4
-2
@@ -20,11 +20,13 @@ package xyz.jonesdev.sonar.common.fallback.protocol.dimension;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.ToString;
|
||||
import net.kyori.adventure.nbt.CompoundBinaryTag;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@RequiredArgsConstructor
|
||||
public final class DimensionInfo {
|
||||
private final String identifier, levelName;
|
||||
private final boolean flat, debug;
|
||||
private final String identifier;
|
||||
private final int id;
|
||||
private final CompoundBinaryTag tag;
|
||||
}
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.common.fallback.protocol.dimension;
|
||||
|
||||
import lombok.experimental.UtilityClass;
|
||||
import net.kyori.adventure.nbt.BinaryTag;
|
||||
import net.kyori.adventure.nbt.BinaryTagIO;
|
||||
import net.kyori.adventure.nbt.CompoundBinaryTag;
|
||||
import net.kyori.adventure.nbt.ListBinaryTag;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.util.Objects;
|
||||
|
||||
@UtilityClass
|
||||
public final class DimensionRegistry {
|
||||
public final DimensionInfo DEFAULT_DIMENSION_1_16;
|
||||
public final DimensionInfo DEFAULT_DIMENSION_1_18_2;
|
||||
|
||||
public final @NotNull CompoundBinaryTag CODEC_1_16;
|
||||
public final @NotNull CompoundBinaryTag CODEC_1_18_2;
|
||||
public final @NotNull CompoundBinaryTag CODEC_1_19;
|
||||
public final @NotNull CompoundBinaryTag CODEC_1_19_1;
|
||||
public final @NotNull CompoundBinaryTag CODEC_1_19_4;
|
||||
public final @NotNull CompoundBinaryTag CODEC_1_20;
|
||||
public final @NotNull CompoundBinaryTag OLD_CODEC;
|
||||
|
||||
static {
|
||||
CODEC_1_16 = getCodec("codec_1_16.nbt");
|
||||
CODEC_1_18_2 = getCodec("codec_1_18_2.nbt");
|
||||
CODEC_1_19 = getCodec("codec_1_19.nbt");
|
||||
CODEC_1_19_1 = getCodec("codec_1_19_1.nbt");
|
||||
CODEC_1_19_4 = getCodec("codec_1_19_4.nbt");
|
||||
CODEC_1_20 = getCodec("codec_1_20.nbt");
|
||||
OLD_CODEC = getCodec("codec_old.nbt");
|
||||
|
||||
DEFAULT_DIMENSION_1_16 = getDimension(CODEC_1_16);
|
||||
DEFAULT_DIMENSION_1_18_2 = getDimension(CODEC_1_18_2);
|
||||
}
|
||||
|
||||
private static @NotNull DimensionInfo getDimension(final @NotNull CompoundBinaryTag tag) {
|
||||
final ListBinaryTag dimensions = tag.getCompound("minecraft:dimension_type").getList("value");
|
||||
final BinaryTag elementTag = ((CompoundBinaryTag) dimensions.get(0)).get("element");
|
||||
return new DimensionInfo("minecraft:overworld", 0, (CompoundBinaryTag) elementTag);
|
||||
}
|
||||
|
||||
private @NotNull CompoundBinaryTag getCodec(final @NotNull String fileName) {
|
||||
try (final InputStream inputStream = Sonar.class.getResourceAsStream("/codecs/" + fileName)) {
|
||||
return BinaryTagIO.reader().read(Objects.requireNonNull(inputStream), BinaryTagIO.Compression.GZIP);
|
||||
} catch (Throwable throwable) {
|
||||
Sonar.get().getLogger().error("Could not load mappings for {}: {}", fileName, throwable);
|
||||
throw new IllegalStateException(throwable);
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-7
@@ -20,30 +20,25 @@ package xyz.jonesdev.sonar.common.fallback.protocol.packets.config;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.ByteBufOutputStream;
|
||||
import io.netty.handler.codec.EncoderException;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.ToString;
|
||||
import net.kyori.adventure.nbt.BinaryTagTypes;
|
||||
import net.kyori.adventure.nbt.CompoundBinaryTag;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.dimension.DimensionRegistry;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public final class RegistrySync implements FallbackPacket {
|
||||
private CompoundBinaryTag dimensionRegistry;
|
||||
|
||||
@Override
|
||||
public void encode(final @NotNull ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
|
||||
try (final ByteBufOutputStream stream = new ByteBufOutputStream(byteBuf)) {
|
||||
stream.writeByte(10); // CompoundTag ID
|
||||
BinaryTagTypes.COMPOUND.write(dimensionRegistry, stream);
|
||||
BinaryTagTypes.COMPOUND.write(DimensionRegistry.CODEC_1_20, stream);
|
||||
} catch (IOException exception) {
|
||||
throw new EncoderException("Cannot write NBT CompoundTag");
|
||||
}
|
||||
|
||||
+11
-6
@@ -56,10 +56,14 @@ public final class EmptyChunkData implements FallbackPacket {
|
||||
}
|
||||
|
||||
private static @NotNull CompoundBinaryTag prepareNBT(final boolean legacy) {
|
||||
final long[] arrayData = new long[legacy ? 36 : 37];
|
||||
final LongArrayBinaryTag longArray = LongArrayBinaryTag.longArrayBinaryTag(arrayData);
|
||||
|
||||
final CompoundBinaryTag motion = CompoundBinaryTag.builder()
|
||||
.put("MOTION_BLOCKING", longArray)
|
||||
.build();
|
||||
return CompoundBinaryTag.builder()
|
||||
.put("root", CompoundBinaryTag.builder()
|
||||
.put("MOTION_BLOCKING", LongArrayBinaryTag.longArrayBinaryTag(new long[legacy ? 36 : 37]))
|
||||
.build())
|
||||
.put("root", motion)
|
||||
.build();
|
||||
}
|
||||
|
||||
@@ -68,9 +72,10 @@ public final class EmptyChunkData implements FallbackPacket {
|
||||
byteBuf.writeInt(x);
|
||||
byteBuf.writeInt(z);
|
||||
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_17) >= 0
|
||||
&& protocolVersion.compareTo(MINECRAFT_1_17_1) <= 0) {
|
||||
writeVarInt(byteBuf, 0); // mask
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_17) >= 0) {
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_17_1) <= 0) {
|
||||
writeVarInt(byteBuf, 0); // mask
|
||||
}
|
||||
} else {
|
||||
byteBuf.writeBoolean(true); // full chunk
|
||||
|
||||
|
||||
+116
-81
@@ -21,168 +21,203 @@ import io.netty.buffer.ByteBuf;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import net.kyori.adventure.nbt.CompoundBinaryTag;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.dimension.DimensionInfo;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
|
||||
import static xyz.jonesdev.sonar.common.fallback.protocol.dimension.DimensionRegistry.*;
|
||||
import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.*;
|
||||
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.writeVarInt;
|
||||
|
||||
// Mostly taken from
|
||||
// https://github.com/Nan1t/NanoLimbo/blob/main/src/main/java/ua/nanit/limbo/protocol/packets/play/PacketJoinGame.java
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public final class JoinGame implements FallbackPacket {
|
||||
private int entityId;
|
||||
private short gamemode;
|
||||
private int dimension;
|
||||
private long partialHashedSeed; // 1.15+
|
||||
private short difficulty;
|
||||
private int gamemode;
|
||||
private long partialHashedSeed;
|
||||
private boolean isHardcore;
|
||||
private @Nullable String levelType;
|
||||
private int viewDistance; // 1.14+
|
||||
private int viewDistance;
|
||||
private boolean reducedDebugInfo;
|
||||
private boolean showRespawnScreen;
|
||||
private boolean limitedCrafting; // 1.20.2+
|
||||
private String[] levelNames; // 1.16+
|
||||
private CompoundBinaryTag registry; // 1.16+
|
||||
private DimensionInfo dimensionInfo; // 1.16+
|
||||
private CompoundBinaryTag currentDimensionData; // 1.16.2+
|
||||
private short previousGamemode; // 1.16+
|
||||
private int simulationDistance; // 1.18+
|
||||
private boolean limitedCrafting;
|
||||
private String[] levelNames;
|
||||
private String levelName;
|
||||
|
||||
// Mostly taken from
|
||||
// https://github.com/Nan1t/NanoLimbo/blob/main/src/main/java/ua/nanit/limbo/protocol/packets/play/PacketJoinGame.java
|
||||
@Override
|
||||
public void encode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
|
||||
public void encode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
|
||||
byteBuf.writeInt(entityId);
|
||||
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_9) <= 0) {
|
||||
byteBuf.writeByte(gamemode);
|
||||
byteBuf.writeByte(dimension);
|
||||
if (protocolVersion.inBetween(MINECRAFT_1_7_2, MINECRAFT_1_7_6)) {
|
||||
byteBuf.writeByte(gamemode == 3 ? 1 : gamemode);
|
||||
byteBuf.writeByte(DEFAULT_DIMENSION_1_16.getId());
|
||||
byteBuf.writeByte(0); // difficulty
|
||||
byteBuf.writeByte(1); // max players
|
||||
byteBuf.writeByte(0); // max players
|
||||
writeString(byteBuf, "flat"); // level type
|
||||
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_8) >= 0) {
|
||||
byteBuf.writeBoolean(reducedDebugInfo);
|
||||
}
|
||||
}
|
||||
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_9_1) >= 0
|
||||
&& protocolVersion.compareTo(MINECRAFT_1_13_2) <= 0) {
|
||||
if (protocolVersion.inBetween(MINECRAFT_1_8, MINECRAFT_1_9)) {
|
||||
byteBuf.writeByte(gamemode);
|
||||
byteBuf.writeInt(dimension);
|
||||
byteBuf.writeByte(DEFAULT_DIMENSION_1_16.getId());
|
||||
byteBuf.writeByte(0); // difficulty
|
||||
byteBuf.writeByte(1); // max players
|
||||
byteBuf.writeByte(0); // max players
|
||||
writeString(byteBuf, "flat"); // level type
|
||||
byteBuf.writeBoolean(reducedDebugInfo);
|
||||
}
|
||||
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_14) >= 0
|
||||
&& protocolVersion.compareTo(MINECRAFT_1_14_4) <= 0) {
|
||||
if (protocolVersion.inBetween(MINECRAFT_1_9_1, MINECRAFT_1_13_2)) {
|
||||
byteBuf.writeByte(gamemode);
|
||||
byteBuf.writeInt(dimension);
|
||||
byteBuf.writeByte(1); // max players
|
||||
byteBuf.writeInt(DEFAULT_DIMENSION_1_16.getId());
|
||||
byteBuf.writeByte(0); // difficulty
|
||||
byteBuf.writeByte(0); // max players
|
||||
writeString(byteBuf, "flat"); // level type
|
||||
byteBuf.writeBoolean(reducedDebugInfo);
|
||||
}
|
||||
|
||||
if (protocolVersion.inBetween(MINECRAFT_1_14, MINECRAFT_1_14_4)) {
|
||||
byteBuf.writeByte(gamemode);
|
||||
byteBuf.writeInt(DEFAULT_DIMENSION_1_16.getId());
|
||||
byteBuf.writeByte(0); // max players
|
||||
writeString(byteBuf, "flat"); // level type
|
||||
writeVarInt(byteBuf, viewDistance);
|
||||
byteBuf.writeBoolean(reducedDebugInfo);
|
||||
}
|
||||
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_15) >= 0
|
||||
&& protocolVersion.compareTo(MINECRAFT_1_15_2) <= 0) {
|
||||
if (protocolVersion.inBetween(MINECRAFT_1_15, MINECRAFT_1_15_2)) {
|
||||
byteBuf.writeByte(gamemode);
|
||||
byteBuf.writeInt(dimension);
|
||||
byteBuf.writeInt(DEFAULT_DIMENSION_1_16.getId());
|
||||
byteBuf.writeLong(partialHashedSeed);
|
||||
byteBuf.writeByte(1); // max players
|
||||
byteBuf.writeByte(0); // max players
|
||||
writeString(byteBuf, "flat"); // level type
|
||||
writeVarInt(byteBuf, viewDistance);
|
||||
byteBuf.writeBoolean(reducedDebugInfo);
|
||||
byteBuf.writeBoolean(showRespawnScreen);
|
||||
}
|
||||
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_16) >= 0
|
||||
&& protocolVersion.compareTo(MINECRAFT_1_16_1) <= 0) {
|
||||
if (protocolVersion.inBetween(MINECRAFT_1_16, MINECRAFT_1_16_1)) {
|
||||
byteBuf.writeByte(gamemode);
|
||||
byteBuf.writeByte(previousGamemode);
|
||||
byteBuf.writeByte(-1); // previous gamemode
|
||||
writeStringArray(byteBuf, levelNames);
|
||||
writeCompoundTag(byteBuf, registry);
|
||||
writeString(byteBuf, dimensionInfo.getIdentifier());
|
||||
writeString(byteBuf, dimensionInfo.getLevelName());
|
||||
writeCompoundTag(byteBuf, OLD_CODEC);
|
||||
writeString(byteBuf, DEFAULT_DIMENSION_1_16.getIdentifier());
|
||||
writeString(byteBuf, levelName);
|
||||
byteBuf.writeLong(partialHashedSeed);
|
||||
byteBuf.writeByte(1); // max players
|
||||
byteBuf.writeByte(0); // max players
|
||||
writeVarInt(byteBuf, viewDistance);
|
||||
byteBuf.writeBoolean(reducedDebugInfo);
|
||||
byteBuf.writeBoolean(showRespawnScreen);
|
||||
byteBuf.writeBoolean(dimensionInfo.isDebug());
|
||||
byteBuf.writeBoolean(dimensionInfo.isFlat());
|
||||
byteBuf.writeBoolean(false); // debug type
|
||||
byteBuf.writeBoolean(false); // flat
|
||||
}
|
||||
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_16_2) >= 0
|
||||
&& protocolVersion.compareTo(MINECRAFT_1_18_2) <= 0) {
|
||||
if (protocolVersion.inBetween(MINECRAFT_1_16_2, MINECRAFT_1_17_1)) {
|
||||
byteBuf.writeBoolean(isHardcore);
|
||||
byteBuf.writeByte(gamemode);
|
||||
byteBuf.writeByte(previousGamemode);
|
||||
byteBuf.writeByte(-1); // previous gamemode
|
||||
writeStringArray(byteBuf, levelNames);
|
||||
writeCompoundTag(byteBuf, registry);
|
||||
writeCompoundTag(byteBuf, currentDimensionData);
|
||||
writeString(byteBuf, dimensionInfo.getIdentifier());
|
||||
writeCompoundTag(byteBuf, CODEC_1_16);
|
||||
writeCompoundTag(byteBuf, DEFAULT_DIMENSION_1_16.getTag());
|
||||
writeString(byteBuf, levelName);
|
||||
byteBuf.writeLong(partialHashedSeed);
|
||||
writeVarInt(byteBuf, 1); // max players
|
||||
writeVarInt(byteBuf, 0); // max players
|
||||
writeVarInt(byteBuf, viewDistance);
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_18) >= 0) {
|
||||
writeVarInt(byteBuf, viewDistance); // simulation distance
|
||||
}
|
||||
byteBuf.writeBoolean(reducedDebugInfo);
|
||||
byteBuf.writeBoolean(showRespawnScreen);
|
||||
byteBuf.writeBoolean(dimensionInfo.isDebug());
|
||||
byteBuf.writeBoolean(dimensionInfo.isFlat());
|
||||
byteBuf.writeBoolean(false); // debug type
|
||||
byteBuf.writeBoolean(false); // flat
|
||||
}
|
||||
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_19) >= 0
|
||||
&& protocolVersion.compareTo(MINECRAFT_1_20) <= 0) {
|
||||
if (protocolVersion.inBetween(MINECRAFT_1_18, MINECRAFT_1_18_2)) {
|
||||
byteBuf.writeBoolean(isHardcore);
|
||||
byteBuf.writeByte(gamemode);
|
||||
byteBuf.writeByte(previousGamemode);
|
||||
byteBuf.writeByte(-1); // previous gamemode
|
||||
writeStringArray(byteBuf, levelNames);
|
||||
writeCompoundTag(byteBuf, registry);
|
||||
writeString(byteBuf, dimensionInfo.getIdentifier());
|
||||
writeString(byteBuf, dimensionInfo.getLevelName());
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_18_2) >= 0) {
|
||||
writeCompoundTag(byteBuf, CODEC_1_18_2);
|
||||
writeCompoundTag(byteBuf, DEFAULT_DIMENSION_1_18_2.getTag());
|
||||
} else {
|
||||
writeCompoundTag(byteBuf, CODEC_1_16);
|
||||
writeCompoundTag(byteBuf, DEFAULT_DIMENSION_1_16.getTag());
|
||||
}
|
||||
writeString(byteBuf, levelName);
|
||||
byteBuf.writeLong(partialHashedSeed);
|
||||
writeVarInt(byteBuf, 1); // max players
|
||||
writeVarInt(byteBuf, 0); // max players
|
||||
writeVarInt(byteBuf, viewDistance);
|
||||
writeVarInt(byteBuf, viewDistance); // simulation distance
|
||||
byteBuf.writeBoolean(reducedDebugInfo);
|
||||
byteBuf.writeBoolean(showRespawnScreen);
|
||||
byteBuf.writeBoolean(dimensionInfo.isDebug());
|
||||
byteBuf.writeBoolean(dimensionInfo.isFlat());
|
||||
byteBuf.writeBoolean(false); // last death location
|
||||
byteBuf.writeBoolean(false); // debug type
|
||||
byteBuf.writeBoolean(false); // flat
|
||||
}
|
||||
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_20) == 0) {
|
||||
writeVarInt(byteBuf, 0); // portal cooldown
|
||||
if (protocolVersion.inBetween(MINECRAFT_1_19, MINECRAFT_1_19_4)) {
|
||||
byteBuf.writeBoolean(isHardcore);
|
||||
byteBuf.writeByte(gamemode);
|
||||
byteBuf.writeByte(-1); // previous gamemode
|
||||
writeStringArray(byteBuf, levelNames);
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_19_1) >= 0) {
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_19_4) >= 0) {
|
||||
writeCompoundTag(byteBuf, CODEC_1_19_4);
|
||||
} else {
|
||||
writeCompoundTag(byteBuf, CODEC_1_19_1);
|
||||
}
|
||||
} else {
|
||||
writeCompoundTag(byteBuf, CODEC_1_19);
|
||||
}
|
||||
writeString(byteBuf, levelName); // world type
|
||||
writeString(byteBuf, levelName);
|
||||
byteBuf.writeLong(partialHashedSeed);
|
||||
writeVarInt(byteBuf, 0); // max players
|
||||
writeVarInt(byteBuf, viewDistance);
|
||||
writeVarInt(byteBuf, viewDistance); // simulation distance
|
||||
byteBuf.writeBoolean(reducedDebugInfo);
|
||||
byteBuf.writeBoolean(showRespawnScreen);
|
||||
byteBuf.writeBoolean(false); // debug type
|
||||
byteBuf.writeBoolean(false); // flat
|
||||
byteBuf.writeBoolean(false); // no last death location
|
||||
}
|
||||
|
||||
if (protocolVersion.equals(MINECRAFT_1_20)) {
|
||||
byteBuf.writeBoolean(isHardcore);
|
||||
byteBuf.writeByte(gamemode);
|
||||
byteBuf.writeByte(-1); // previous gamemode
|
||||
writeStringArray(byteBuf, levelNames);
|
||||
writeCompoundTag(byteBuf, CODEC_1_20);
|
||||
writeString(byteBuf, levelName); // world type
|
||||
writeString(byteBuf, levelName);
|
||||
byteBuf.writeLong(partialHashedSeed);
|
||||
writeVarInt(byteBuf, 0); // max players
|
||||
writeVarInt(byteBuf, viewDistance);
|
||||
writeVarInt(byteBuf, viewDistance); // simulation distance
|
||||
byteBuf.writeBoolean(reducedDebugInfo);
|
||||
byteBuf.writeBoolean(showRespawnScreen);
|
||||
byteBuf.writeBoolean(false); // debug type
|
||||
byteBuf.writeBoolean(false); // flat
|
||||
byteBuf.writeBoolean(false); // no last death location
|
||||
writeVarInt(byteBuf, 0);
|
||||
}
|
||||
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_20_2) >= 0) {
|
||||
byteBuf.writeBoolean(isHardcore);
|
||||
writeStringArray(byteBuf, levelNames);
|
||||
writeVarInt(byteBuf, 1); // max players
|
||||
writeVarInt(byteBuf, 0); // max players
|
||||
writeVarInt(byteBuf, viewDistance);
|
||||
writeVarInt(byteBuf, viewDistance); // simulation distance
|
||||
byteBuf.writeBoolean(reducedDebugInfo);
|
||||
byteBuf.writeBoolean(showRespawnScreen);
|
||||
byteBuf.writeBoolean(limitedCrafting);
|
||||
writeString(byteBuf, dimensionInfo.getIdentifier());
|
||||
writeString(byteBuf, dimensionInfo.getLevelName());
|
||||
byteBuf.writeBoolean(false); // limited crafting
|
||||
writeString(byteBuf, levelName);
|
||||
writeString(byteBuf, levelName);
|
||||
byteBuf.writeLong(partialHashedSeed);
|
||||
byteBuf.writeByte(gamemode);
|
||||
byteBuf.writeByte(previousGamemode);
|
||||
byteBuf.writeBoolean(dimensionInfo.isDebug());
|
||||
byteBuf.writeBoolean(dimensionInfo.isFlat());
|
||||
byteBuf.writeBoolean(false); // last death location
|
||||
writeVarInt(byteBuf, 0); // portal cooldown
|
||||
byteBuf.writeByte(-1); // previous gamemode
|
||||
byteBuf.writeBoolean(false); // debug type
|
||||
byteBuf.writeBoolean(false); // flat
|
||||
byteBuf.writeBoolean(false); // no last death location
|
||||
writeVarInt(byteBuf, 0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.common.service;
|
||||
|
||||
import lombok.experimental.UtilityClass;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.fallback.traffic.TrafficCounter;
|
||||
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@UtilityClass
|
||||
public class ServiceRepository {
|
||||
private final ScheduledExecutorService QUEUE_SERVICE = Executors.newSingleThreadScheduledExecutor();
|
||||
private final ScheduledExecutorService TRAFFIC_SERVICE = Executors.newSingleThreadScheduledExecutor();
|
||||
private final ScheduledExecutorService VERBOSE_SERVICE = Executors.newSingleThreadScheduledExecutor();
|
||||
private boolean registered;
|
||||
|
||||
public void register() {
|
||||
if (registered) {
|
||||
throw new IllegalStateException("Already registered");
|
||||
}
|
||||
registered = true;
|
||||
|
||||
TRAFFIC_SERVICE.scheduleAtFixedRate(TrafficCounter::reset,
|
||||
1L, 1L, TimeUnit.SECONDS);
|
||||
QUEUE_SERVICE.scheduleAtFixedRate(Sonar.get().getFallback().getQueue()::poll,
|
||||
500L, 500L, TimeUnit.MILLISECONDS);
|
||||
VERBOSE_SERVICE.scheduleAtFixedRate(Sonar.get().getVerboseHandler()::update,
|
||||
200L, 200L, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
public synchronized void shutdown() {
|
||||
if (!registered) {
|
||||
throw new IllegalStateException("Not registered");
|
||||
}
|
||||
registered = false;
|
||||
|
||||
QUEUE_SERVICE.shutdown();
|
||||
TRAFFIC_SERVICE.shutdown();
|
||||
VERBOSE_SERVICE.shutdown();
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -39,7 +39,7 @@ import java.net.InetAddress;
|
||||
public final class BlacklistCommand extends Subcommand {
|
||||
|
||||
@Override
|
||||
public void execute(final @NotNull CommandInvocation invocation) {
|
||||
protected void execute(final @NotNull CommandInvocation invocation) {
|
||||
switch (invocation.getRawArguments()[1].toLowerCase()) {
|
||||
case "add": {
|
||||
if (invocation.getRawArguments().length <= 2) {
|
||||
@@ -49,7 +49,7 @@ public final class BlacklistCommand extends Subcommand {
|
||||
}
|
||||
|
||||
final String rawInetAddress = invocation.getRawArguments()[2];
|
||||
final InetAddress inetAddress = checkIP(invocation.getSender(), rawInetAddress);
|
||||
final InetAddress inetAddress = getInetAddressIfValid(invocation.getSender(), rawInetAddress);
|
||||
// Make sure the given IP address is valid
|
||||
if (inetAddress == null) return;
|
||||
|
||||
@@ -80,7 +80,7 @@ public final class BlacklistCommand extends Subcommand {
|
||||
}
|
||||
|
||||
final String rawInetAddress = invocation.getRawArguments()[2];
|
||||
final InetAddress inetAddress = checkIP(invocation.getSender(), rawInetAddress);
|
||||
final InetAddress inetAddress = getInetAddressIfValid(invocation.getSender(), rawInetAddress);
|
||||
// Make sure the given IP address is valid
|
||||
if (inetAddress == null) return;
|
||||
|
||||
|
||||
+5
-9
@@ -17,12 +17,11 @@
|
||||
|
||||
package xyz.jonesdev.sonar.common.subcommand.impl;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.val;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Unmodifiable;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.SonarPlatform;
|
||||
import xyz.jonesdev.sonar.api.command.CommandInvocation;
|
||||
import xyz.jonesdev.sonar.api.command.subcommand.Subcommand;
|
||||
@@ -33,7 +32,7 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.WeakHashMap;
|
||||
|
||||
// This command is only used to display helpful information in order to help
|
||||
// This command is only used to display helpful information to help
|
||||
// the Sonar contributors to fix issues more quickly
|
||||
@SubcommandInfo(
|
||||
name = "dump",
|
||||
@@ -41,13 +40,10 @@ import java.util.WeakHashMap;
|
||||
onlyConsole = true
|
||||
)
|
||||
public final class DumpCommand extends Subcommand implements JVMProfiler {
|
||||
private static final Gson GSON = new GsonBuilder()
|
||||
.disableInnerClassSerialization()
|
||||
.create();
|
||||
|
||||
@Override
|
||||
public void execute(final @NotNull CommandInvocation invocation) {
|
||||
final String json = GSON.toJson(collectMappedInformation());
|
||||
protected void execute(final @NotNull CommandInvocation invocation) {
|
||||
final String json = Sonar.GENERAL_GSON.toJson(collectMappedInformation());
|
||||
SONAR.getLogger().info("Generated dump: {}", json);
|
||||
}
|
||||
|
||||
@@ -56,7 +52,7 @@ public final class DumpCommand extends Subcommand implements JVMProfiler {
|
||||
val mappings = new WeakHashMap<String, Object>();
|
||||
mappings.put("sonar", new Dump.Sonar(
|
||||
SONAR.getVersion().getFull(),
|
||||
SONAR.getServer().getPlatform(),
|
||||
SONAR.getPlatform(),
|
||||
SONAR.getVersion().isOnMainBranch()
|
||||
));
|
||||
mappings.put("runtime", new Dump.Runtime(
|
||||
|
||||
+2
-2
@@ -29,10 +29,10 @@ import xyz.jonesdev.sonar.api.command.subcommand.SubcommandInfo;
|
||||
public final class LockdownCommand extends Subcommand {
|
||||
|
||||
@Override
|
||||
public void execute(final @NotNull CommandInvocation invocation) {
|
||||
protected void execute(final @NotNull CommandInvocation invocation) {
|
||||
final boolean newState = !SONAR.getConfig().getLockdown().isEnabled();
|
||||
SONAR.getConfig().getLockdown().setEnabled(newState);
|
||||
SONAR.getConfig().getGeneralConfig().set("general.lockdown.enabled", newState);
|
||||
SONAR.getConfig().getGeneralConfig().set("lockdown.enabled", newState);
|
||||
SONAR.getConfig().getGeneralConfig().save();
|
||||
|
||||
invocation.getSender().sendMessage(newState
|
||||
|
||||
+1
-1
@@ -30,7 +30,7 @@ import xyz.jonesdev.sonar.api.timer.SystemTimer;
|
||||
public final class ReloadCommand extends Subcommand {
|
||||
|
||||
@Override
|
||||
public void execute(final @NotNull CommandInvocation invocation) {
|
||||
protected void execute(final @NotNull CommandInvocation invocation) {
|
||||
final SystemTimer timer = new SystemTimer();
|
||||
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getReloading());
|
||||
|
||||
+1
-1
@@ -48,7 +48,7 @@ public final class StatisticsCommand extends Subcommand implements JVMProfiler {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(final @NotNull CommandInvocation invocation) {
|
||||
protected void execute(final @NotNull CommandInvocation invocation) {
|
||||
StatisticType type = StatisticType.GENERAL;
|
||||
if (invocation.getRawArguments().length >= 2) {
|
||||
try {
|
||||
|
||||
+10
-26
@@ -17,46 +17,30 @@
|
||||
|
||||
package xyz.jonesdev.sonar.common.subcommand.impl;
|
||||
|
||||
import net.kyori.adventure.text.Component;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.command.CommandInvocation;
|
||||
import xyz.jonesdev.sonar.api.command.InvocationSource;
|
||||
import xyz.jonesdev.sonar.api.command.subcommand.Subcommand;
|
||||
import xyz.jonesdev.sonar.api.command.subcommand.SubcommandInfo;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
@SubcommandInfo(
|
||||
name = "verbose",
|
||||
description = "Enable and disable Sonar verbose",
|
||||
description = "Enable or disable Sonar verbose",
|
||||
onlyPlayers = true
|
||||
)
|
||||
public final class VerboseCommand extends Subcommand {
|
||||
|
||||
@Override
|
||||
public void execute(final @NotNull CommandInvocation invocation) {
|
||||
InvocationSource verboseSubscriber = invocation.getSender();
|
||||
|
||||
// Support for '/sonar verbose [username]'
|
||||
if (invocation.getRawArguments().length >= 2) {
|
||||
final Optional<InvocationSource> optional = SONAR.getServer().getOnlinePlayer(invocation.getRawArguments()[1]);
|
||||
if (optional.isPresent()) verboseSubscriber = optional.get();
|
||||
}
|
||||
|
||||
if (SONAR.getVerboseHandler().isSubscribed(verboseSubscriber.getName())) {
|
||||
SONAR.getVerboseHandler().unsubscribe(verboseSubscriber.getName());
|
||||
if (verboseSubscriber != invocation.getSender()) {
|
||||
verboseSubscriber.sendMessage(SONAR.getConfig().getCommands().getVerboseUnsubscribedOther()
|
||||
.replace("%player%", verboseSubscriber.getName()));
|
||||
}
|
||||
verboseSubscriber.sendMessage(SONAR.getConfig().getCommands().getVerboseUnsubscribed());
|
||||
protected void execute(final @NotNull CommandInvocation invocation) {
|
||||
if (SONAR.getVerboseHandler().isSubscribed(invocation.getSender().getName())) {
|
||||
SONAR.getVerboseHandler().unsubscribe(invocation.getSender().getName());
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getVerboseUnsubscribed());
|
||||
// Reset ActionBar component when unsubscribing
|
||||
invocation.getSender().getAudience().sendActionBar(Component.empty());
|
||||
return;
|
||||
}
|
||||
|
||||
if (verboseSubscriber != invocation.getSender()) {
|
||||
verboseSubscriber.sendMessage(SONAR.getConfig().getCommands().getVerboseSubscribedOther()
|
||||
.replace("%player%", verboseSubscriber.getName()));
|
||||
}
|
||||
verboseSubscriber.sendMessage(SONAR.getConfig().getCommands().getVerboseSubscribed());
|
||||
SONAR.getVerboseHandler().subscribe(verboseSubscriber.getName());
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getVerboseSubscribed());
|
||||
SONAR.getVerboseHandler().subscribe(invocation.getSender().getName());
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -43,7 +43,7 @@ public final class VerifiedCommand extends Subcommand {
|
||||
private static final Queue<String> LOCK = new LinkedBlockingQueue<>(1);
|
||||
|
||||
@Override
|
||||
public void execute(final @NotNull CommandInvocation invocation) {
|
||||
protected void execute(final @NotNull CommandInvocation invocation) {
|
||||
switch (invocation.getRawArguments()[1].toLowerCase()) {
|
||||
case "history": {
|
||||
if (invocation.getRawArguments().length <= 2) {
|
||||
@@ -53,7 +53,7 @@ public final class VerifiedCommand extends Subcommand {
|
||||
}
|
||||
|
||||
final String rawInetAddress = invocation.getRawArguments()[2];
|
||||
final InetAddress inetAddress = checkIP(invocation.getSender(), rawInetAddress);
|
||||
final InetAddress inetAddress = getInetAddressIfValid(invocation.getSender(), rawInetAddress);
|
||||
// Make sure the given IP address is valid
|
||||
if (inetAddress == null) return;
|
||||
|
||||
@@ -85,7 +85,7 @@ public final class VerifiedCommand extends Subcommand {
|
||||
return;
|
||||
}
|
||||
|
||||
final InetAddress inetAddress = checkIP(invocation.getSender(), rawInetAddress);
|
||||
final InetAddress inetAddress = getInetAddressIfValid(invocation.getSender(), rawInetAddress);
|
||||
// Make sure the given IP address is valid
|
||||
if (inetAddress == null) return;
|
||||
|
||||
|
||||
@@ -38,24 +38,39 @@ public class UpdateChecker {
|
||||
}
|
||||
};
|
||||
|
||||
private int convertVersion(final @NotNull String version) throws NumberFormatException, IllegalStateException {
|
||||
final String[] convertedParts = version.split("\\.");
|
||||
// Validate the length of the converted parts
|
||||
if (convertedParts.length != 3) {
|
||||
throw new IllegalStateException("Converted version length mismatch");
|
||||
}
|
||||
// Major updates are more important than minor updates,
|
||||
// and minor updates are more important than patches.
|
||||
final int convertedMajor = Integer.parseInt(convertedParts[0]) * 100000; // multiply by weight
|
||||
final int convertedMinor = Integer.parseInt(convertedParts[1]) * 10000; // multiply by weight
|
||||
final int patch = Integer.parseInt(convertedParts[2]);
|
||||
return convertedMajor + convertedMinor + patch; // sum the converted version parts
|
||||
}
|
||||
|
||||
public void checkForUpdates() {
|
||||
ASYNC_EXECUTOR.execute(() -> {
|
||||
try {
|
||||
final URL url = new URL("https://api.github.com/repos/jonesdevelopment/sonar/releases/latest");
|
||||
final HttpsURLConnection urlConnection = prepareConnection(url);
|
||||
final JsonObject json = parseJson(urlConnection.getInputStream());
|
||||
final String latestStableVersion = json.get("tag_name").getAsString();
|
||||
final JsonObject json = parseJson(urlConnection);
|
||||
final String latestStableRelease = json.get("tag_name").getAsString();
|
||||
final int convertedLatestVersion = convertVersion(latestStableRelease);
|
||||
final int convertedCurrentVersion = convertVersion(Sonar.get().getVersion().getSemanticVersion());
|
||||
|
||||
if (!latestStableVersion.equals(Sonar.get().getVersion().getSemanticVersion())) {
|
||||
LOGGER.warn("A new version of Sonar is available: {}", latestStableVersion);
|
||||
if (convertedCurrentVersion < convertedLatestVersion) {
|
||||
LOGGER.warn("A new version of Sonar is available: {}", latestStableRelease);
|
||||
LOGGER.warn("Please make sure to update to the latest version to ensure stability and security:");
|
||||
LOGGER.warn("https://github.com/jonesdevelopment/sonar/releases");
|
||||
} else if (Sonar.get().getVersion().isOnMainBranch()) {
|
||||
LOGGER.info("You are currently using the latest stable release of Sonar!");
|
||||
LOGGER.warn("https://github.com/jonesdevelopment/sonar/releases/tag/{}", latestStableRelease);
|
||||
} else if (convertedCurrentVersion > convertedLatestVersion || !Sonar.get().getVersion().isOnMainBranch()) {
|
||||
LOGGER.warn("You are currently using an unreleased version of Sonar!");
|
||||
LOGGER.warn("The contributors of Sonar are not responsible for any damage done by using an unstable version");
|
||||
} else {
|
||||
LOGGER.warn("You are currently using the latest development version of Sonar!");
|
||||
LOGGER.warn("The contributors of Sonar are not responsible for any damage done by using an unstable version" +
|
||||
".");
|
||||
LOGGER.info("You are currently using the latest stable release of Sonar!");
|
||||
}
|
||||
} catch (Exception exception) {
|
||||
LOGGER.warn("Could not retrieve latest version information: {}", exception);
|
||||
@@ -63,22 +78,25 @@ public class UpdateChecker {
|
||||
});
|
||||
}
|
||||
|
||||
private @NotNull JsonObject parseJson(final @NotNull InputStream inputStream) throws IOException {
|
||||
final InputStreamReader inputStreamReader = new InputStreamReader(inputStream);
|
||||
final StringBuilder content = new StringBuilder();
|
||||
private @NotNull JsonObject parseJson(final @NotNull HttpsURLConnection urlConnection) throws IOException {
|
||||
try (final InputStream inputStream = urlConnection.getInputStream()) {
|
||||
try (final InputStreamReader inputStreamReader = new InputStreamReader(inputStream)) {
|
||||
final StringBuilder content = new StringBuilder();
|
||||
|
||||
// Read the entire page content
|
||||
try (final BufferedReader bufferedReader = new BufferedReader(inputStreamReader)) {
|
||||
String line;
|
||||
// Read the entire page content
|
||||
try (final BufferedReader bufferedReader = new BufferedReader(inputStreamReader)) {
|
||||
String line;
|
||||
|
||||
while ((line = bufferedReader.readLine()) != null) {
|
||||
content.append(line);
|
||||
while ((line = bufferedReader.readLine()) != null) {
|
||||
content.append(line);
|
||||
}
|
||||
}
|
||||
|
||||
// Parse site content as JSON using Gson
|
||||
final JsonParser jsonParser = new JsonParser();
|
||||
return jsonParser.parse(content.toString()).getAsJsonObject();
|
||||
}
|
||||
}
|
||||
|
||||
// Parse site content as JSON using Gson
|
||||
final JsonParser jsonParser = new JsonParser();
|
||||
return jsonParser.parse(content.toString()).getAsJsonObject();
|
||||
}
|
||||
|
||||
private @NotNull HttpsURLConnection prepareConnection(final @NotNull URL url) throws Exception {
|
||||
|
||||
+11
-10
@@ -18,6 +18,7 @@
|
||||
package xyz.jonesdev.sonar.common.utility.geyser;
|
||||
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.util.AttributeKey;
|
||||
import lombok.experimental.UtilityClass;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
@@ -27,20 +28,20 @@ import org.jetbrains.annotations.NotNull;
|
||||
@UtilityClass
|
||||
public class GeyserUtil {
|
||||
|
||||
/*
|
||||
* Geyser attribute key for every floodgate player
|
||||
*/
|
||||
private AttributeKey<Object> playerAttribute() {
|
||||
return AttributeKey.valueOf("floodgate-player");
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if Geyser holds the parent channel of the player
|
||||
*
|
||||
* @param channel Channel of the player
|
||||
* @return Whether the player is on GeyserMC or not
|
||||
*/
|
||||
public boolean isGeyserConnection(final @NotNull Channel channel) {
|
||||
// Get the parent channel of the original channel
|
||||
final Channel parent = channel.parent();
|
||||
// This shouldn't happen, but we want to stay safe here
|
||||
if (parent == null) return false;
|
||||
|
||||
final Class<? extends Channel> clazz = parent.getClass();
|
||||
// Check if Geyser adapted the channel by checking for the package name
|
||||
return clazz.getCanonicalName().startsWith("org.geysermc");
|
||||
// https://discord.com/channels/613163671870242838/613170125696270357/1168599123889504266
|
||||
// Every floodgate player has a channel attribute called "floodgate-player"
|
||||
return channel.attr(playerAttribute()).get() != null;
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -7,12 +7,6 @@ dependencies {
|
||||
compileOnly("com.velocitypowered:velocity-proxy:$velocityVersion")
|
||||
testCompileOnly("com.velocitypowered:velocity-proxy:$velocityVersion")
|
||||
|
||||
compileOnly("com.velocitypowered:velocity-api:$velocityVersion")
|
||||
//annotationProcessor("com.velocitypowered:velocity-api:$velocityVersion")
|
||||
|
||||
testCompileOnly("com.velocitypowered:velocity-api:$velocityVersion")
|
||||
//testAnnotationProcessor("com.velocitypowered:velocity-api:$velocityVersion")
|
||||
|
||||
// Implement bStats.org for metrics
|
||||
implementation("org.bstats:bstats-velocity:3.0.2")
|
||||
}
|
||||
@@ -20,9 +14,8 @@ dependencies {
|
||||
tasks {
|
||||
processResources {
|
||||
val props = mapOf(
|
||||
"name" to rootProject.name,
|
||||
"version" to rootProject.version.toString().split("-")[0],
|
||||
"description" to "Effective Anti-bot plugin for Velocity, BungeeCord, and Bukkit (1.7-latest)",
|
||||
"description" to rootProject.description,
|
||||
"url" to "https://jonesdev.xyz/discord/",
|
||||
"main" to "xyz.jonesdev.sonar.velocity.SonarVelocityPlugin"
|
||||
)
|
||||
|
||||
@@ -20,25 +20,18 @@ package xyz.jonesdev.sonar.velocity;
|
||||
import lombok.Getter;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.SonarPlatform;
|
||||
import xyz.jonesdev.sonar.api.command.InvocationSource;
|
||||
import xyz.jonesdev.sonar.api.fallback.traffic.TrafficCounter;
|
||||
import xyz.jonesdev.sonar.api.logger.LoggerWrapper;
|
||||
import xyz.jonesdev.sonar.api.server.ServerWrapper;
|
||||
import xyz.jonesdev.sonar.common.boot.SonarBootstrap;
|
||||
import xyz.jonesdev.sonar.velocity.command.VelocityInvocationSource;
|
||||
import xyz.jonesdev.sonar.velocity.audience.AudienceListener;
|
||||
import xyz.jonesdev.sonar.velocity.command.VelocitySonarCommand;
|
||||
import xyz.jonesdev.sonar.velocity.fallback.FallbackListener;
|
||||
import xyz.jonesdev.sonar.velocity.verbose.VerboseWrapper;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@Getter
|
||||
public final class SonarVelocity extends SonarBootstrap<SonarVelocityPlugin> {
|
||||
public static SonarVelocity INSTANCE;
|
||||
|
||||
public SonarVelocity(final @NotNull SonarVelocityPlugin plugin) {
|
||||
super(plugin, plugin.getDataDirectory().toFile(), new VerboseWrapper(plugin.getServer()));
|
||||
super(plugin, plugin.getDataDirectory().toFile(), SonarPlatform.VELOCITY);
|
||||
INSTANCE = this;
|
||||
}
|
||||
|
||||
@@ -66,28 +59,11 @@ public final class SonarVelocity extends SonarBootstrap<SonarVelocityPlugin> {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a wrapper object for our server, so we can use it outside
|
||||
* the velocity module.
|
||||
* <br>
|
||||
* We have to do this, so we can access all necessary API functions.
|
||||
*/
|
||||
public final ServerWrapper server = new ServerWrapper(SonarPlatform.VELOCITY) {
|
||||
|
||||
@Override
|
||||
public Optional<InvocationSource> getOnlinePlayer(final String username) {
|
||||
return getPlugin().getServer().getAllPlayers().stream()
|
||||
.filter(player -> player.getUsername().equalsIgnoreCase(username))
|
||||
.findFirst()
|
||||
.map(VelocityInvocationSource::new);
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void enable() {
|
||||
|
||||
// Initialize bStats.org metrics
|
||||
getPlugin().getMetricsFactory().make(getPlugin(), getServer().getPlatform().getMetricsId());
|
||||
getPlugin().getMetricsFactory().make(getPlugin(), getPlatform().getMetricsId());
|
||||
|
||||
// Register Sonar command
|
||||
getPlugin().getServer().getCommandManager().register("sonar", new VelocitySonarCommand());
|
||||
@@ -95,19 +71,7 @@ public final class SonarVelocity extends SonarBootstrap<SonarVelocityPlugin> {
|
||||
// Register Fallback listener
|
||||
getPlugin().getServer().getEventManager().register(getPlugin(), new FallbackListener(getFallback()));
|
||||
|
||||
// Register Fallback queue task
|
||||
getPlugin().getServer().getScheduler().buildTask(getPlugin(), getFallback().getQueue()::poll)
|
||||
.repeat(500L, TimeUnit.MILLISECONDS)
|
||||
.schedule();
|
||||
|
||||
// Register traffic counter reset task
|
||||
getPlugin().getServer().getScheduler().buildTask(getPlugin(), TrafficCounter::reset)
|
||||
.repeat(1L, TimeUnit.SECONDS)
|
||||
.schedule();
|
||||
|
||||
// Register action bar verbose task
|
||||
getPlugin().getServer().getScheduler().buildTask(getPlugin(), getVerboseHandler()::update)
|
||||
.repeat(100L, TimeUnit.MILLISECONDS)
|
||||
.schedule();
|
||||
// Register audience register listener
|
||||
getPlugin().getServer().getEventManager().register(getPlugin(), new AudienceListener());
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,6 @@ import com.google.inject.Inject;
|
||||
import com.velocitypowered.api.event.Subscribe;
|
||||
import com.velocitypowered.api.event.proxy.ProxyInitializeEvent;
|
||||
import com.velocitypowered.api.event.proxy.ProxyShutdownEvent;
|
||||
import com.velocitypowered.api.plugin.Plugin;
|
||||
import com.velocitypowered.api.plugin.annotation.DataDirectory;
|
||||
import com.velocitypowered.api.proxy.ProxyServer;
|
||||
import lombok.Getter;
|
||||
@@ -31,15 +30,6 @@ import org.slf4j.Logger;
|
||||
import java.nio.file.Path;
|
||||
|
||||
@Getter
|
||||
@Plugin(
|
||||
id = "sonar",
|
||||
name = "Sonar",
|
||||
version = "$version",
|
||||
authors = {"Jones Development", "Sonar Contributors"},
|
||||
url = "https://jonesdev.xyz/discord/",
|
||||
description = "Effective Anti-bot plugin for Velocity, BungeeCord, and Bukkit (1.7-latest)"
|
||||
)
|
||||
@SuppressWarnings("all")
|
||||
public final class SonarVelocityPlugin {
|
||||
private final ProxyServer server;
|
||||
private final Logger logger;
|
||||
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.velocity.audience;
|
||||
|
||||
import com.velocitypowered.api.event.PostOrder;
|
||||
import com.velocitypowered.api.event.Subscribe;
|
||||
import com.velocitypowered.api.event.connection.DisconnectEvent;
|
||||
import com.velocitypowered.api.event.connection.PostLoginEvent;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
|
||||
public final class AudienceListener {
|
||||
|
||||
@Subscribe(order = PostOrder.LAST)
|
||||
public void handle(final @NotNull PostLoginEvent event) {
|
||||
Sonar.get().getVerboseHandler().getAudiences().put(event.getPlayer().getUsername(), event.getPlayer());
|
||||
}
|
||||
|
||||
@Subscribe(order = PostOrder.LAST)
|
||||
public void handle(final @NotNull DisconnectEvent event) {
|
||||
Sonar.get().getVerboseHandler().getAudiences().remove(event.getPlayer().getUsername());
|
||||
}
|
||||
}
|
||||
+4
-3
@@ -24,8 +24,9 @@ import xyz.jonesdev.sonar.api.command.InvocationSource;
|
||||
|
||||
public final class VelocityInvocationSource extends InvocationSource {
|
||||
public VelocityInvocationSource(final @NotNull CommandSource sender) {
|
||||
super(sender instanceof Player
|
||||
? ((Player) sender).getUsername()
|
||||
: "Console", sender);
|
||||
super(sender instanceof Player ? ((Player) sender).getUsername() : "Console",
|
||||
sender,
|
||||
sender instanceof Player,
|
||||
sender::hasPermission);
|
||||
}
|
||||
}
|
||||
+3
-128
@@ -18,120 +18,22 @@
|
||||
package xyz.jonesdev.sonar.velocity.command;
|
||||
|
||||
import com.velocitypowered.api.command.SimpleCommand;
|
||||
import com.velocitypowered.api.proxy.ConsoleCommandSource;
|
||||
import com.velocitypowered.api.proxy.Player;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.command.CommandInvocation;
|
||||
import xyz.jonesdev.sonar.api.command.InvocationSource;
|
||||
import xyz.jonesdev.sonar.api.command.SonarCommand;
|
||||
import xyz.jonesdev.sonar.api.command.subcommand.Subcommand;
|
||||
import xyz.jonesdev.sonar.api.command.subcommand.argument.Argument;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static java.util.Collections.emptyList;
|
||||
|
||||
public final class VelocitySonarCommand implements SimpleCommand, SonarCommand {
|
||||
{
|
||||
cacheHelpMessage();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(final @NotNull Invocation invocation) {
|
||||
// Create our own invocation source wrapper to handle messages properly
|
||||
final InvocationSource invocationSource = new VelocityInvocationSource(invocation.source());
|
||||
|
||||
if (!(invocation.source() instanceof ConsoleCommandSource)) {
|
||||
// Check if the player actually has the permission to run the command
|
||||
if (!invocation.source().hasPermission("sonar.command")) {
|
||||
invocationSource.sendMessage(Sonar.get().getConfig().getNoPermission());
|
||||
return;
|
||||
}
|
||||
// Checking if it contains will only break more since it can throw
|
||||
// a NullPointerException if the cache is being accessed from parallel threads
|
||||
DELAY.cleanUp(); // Clean up the cache
|
||||
final long mapTimestamp = DELAY.asMap().getOrDefault(invocation.source(), -1L);
|
||||
|
||||
// There were some exploits with spamming commands in the past.
|
||||
// Spamming should be prevented, especially if some heavy operations are done,
|
||||
// which is not the case here but let's still stay safe!
|
||||
if (mapTimestamp > 0L) {
|
||||
invocationSource.sendMessage(Sonar.get().getConfig().getCommands().getCommandCoolDown());
|
||||
|
||||
// Format delay
|
||||
final long timestamp = System.currentTimeMillis();
|
||||
final double left = 0.5D - (timestamp - mapTimestamp) / 1000D;
|
||||
|
||||
invocationSource.sendMessage(Sonar.get().getConfig().getCommands().getCommandCoolDownLeft()
|
||||
.replace("%time-left%", Sonar.DECIMAL_FORMAT.format(left)));
|
||||
return;
|
||||
}
|
||||
|
||||
DELAY.put(invocation.source());
|
||||
}
|
||||
|
||||
Optional<Subcommand> subcommand = Optional.empty();
|
||||
|
||||
if (invocation.arguments().length > 0) {
|
||||
// Search subcommand if command arguments are present
|
||||
subcommand = Sonar.get().getSubcommandRegistry().getSubcommands().parallelStream()
|
||||
.filter(sub -> sub.getInfo().name().equalsIgnoreCase(invocation.arguments()[0])
|
||||
|| Arrays.stream(sub.getInfo().aliases())
|
||||
.anyMatch(alias -> alias.equalsIgnoreCase(invocation.arguments()[0])))
|
||||
.findFirst();
|
||||
|
||||
// Check permissions for subcommands
|
||||
if (subcommand.isPresent()) {
|
||||
if (!subcommand.get().getInfo().onlyConsole()
|
||||
&& !invocation.source().hasPermission(subcommand.get().getPermission())
|
||||
) {
|
||||
invocationSource.sendMessage(Sonar.get().getConfig().getCommands().getSubCommandNoPerm()
|
||||
.replace("%permission%", subcommand.get().getPermission()));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
subcommand.ifPresent(sub -> {
|
||||
if (sub.getInfo().onlyPlayers() && !(invocation.source() instanceof Player)) {
|
||||
invocationSource.sendMessage(Sonar.get().getConfig().getCommands().getPlayersOnly());
|
||||
return;
|
||||
}
|
||||
|
||||
if (sub.getInfo().onlyConsole() && !(invocation.source() instanceof ConsoleCommandSource)) {
|
||||
invocationSource.sendMessage(Sonar.get().getConfig().getCommands().getConsoleOnly());
|
||||
return;
|
||||
}
|
||||
|
||||
final CommandInvocation commandInvocation = new CommandInvocation(invocationSource, sub, invocation.arguments());
|
||||
|
||||
// The subcommands has arguments which are not present in the executed command
|
||||
if (sub.getInfo().arguments().length > 0
|
||||
&& commandInvocation.getRawArguments().length <= 1
|
||||
&& sub.getInfo().argumentsRequired()) {
|
||||
invocationSource.sendMessage(Sonar.get().getConfig().getCommands().getIncorrectCommandUsage()
|
||||
.replace("%usage%", sub.getInfo().name() + " (" + sub.getArguments() + ")"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Execute the sub command with the custom invocation properties
|
||||
sub.execute(commandInvocation);
|
||||
});
|
||||
|
||||
if (subcommand.isEmpty()) {
|
||||
|
||||
// Re-use the old, cached help message since we don't want to scan
|
||||
// for each subcommand and it's arguments/attributes every time
|
||||
// someone runs /sonar since the subcommand don't change
|
||||
for (final Component component : CACHED_HELP_MESSAGE) {
|
||||
invocationSource.sendMessage(component);
|
||||
}
|
||||
}
|
||||
// Pass the invocation source and command arguments to our command handler
|
||||
handle(invocationSource, invocation.arguments());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -140,33 +42,6 @@ public final class VelocitySonarCommand implements SimpleCommand, SonarCommand {
|
||||
if (!invocation.source().hasPermission("sonar.command")) {
|
||||
return emptyList();
|
||||
}
|
||||
if (invocation.arguments().length <= 1) {
|
||||
if (TAB_SUGGESTIONS.isEmpty()) {
|
||||
for (final Subcommand subcommand : Sonar.get().getSubcommandRegistry().getSubcommands()) {
|
||||
TAB_SUGGESTIONS.add(subcommand.getInfo().name());
|
||||
|
||||
if (subcommand.getInfo().aliases().length > 0) {
|
||||
TAB_SUGGESTIONS.addAll(Arrays.asList(subcommand.getInfo().aliases()));
|
||||
}
|
||||
}
|
||||
}
|
||||
return TAB_SUGGESTIONS;
|
||||
} else if (invocation.arguments().length == 2) {
|
||||
if (ARG_TAB_SUGGESTIONS.isEmpty()) {
|
||||
for (final Subcommand subcommand : Sonar.get().getSubcommandRegistry().getSubcommands()) {
|
||||
final List<String> parsedArguments = Arrays.stream(subcommand.getInfo().arguments())
|
||||
.map(Argument::value)
|
||||
.collect(Collectors.toUnmodifiableList());
|
||||
ARG_TAB_SUGGESTIONS.put(subcommand.getInfo().name(), parsedArguments);
|
||||
for (final String alias : subcommand.getInfo().aliases()) {
|
||||
ARG_TAB_SUGGESTIONS.put(alias, parsedArguments);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final String subCommandName = invocation.arguments()[0].toLowerCase();
|
||||
return ARG_TAB_SUGGESTIONS.getOrDefault(subCommandName, emptyList());
|
||||
}
|
||||
return emptyList();
|
||||
return getCachedTabSuggestions(invocation.arguments());
|
||||
}
|
||||
}
|
||||
+57
-45
@@ -20,6 +20,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.api.event.connection.PostLoginEvent;
|
||||
import com.velocitypowered.api.event.connection.PreLoginEvent;
|
||||
import com.velocitypowered.api.proxy.crypto.IdentifiedKey;
|
||||
import com.velocitypowered.api.util.GameProfile;
|
||||
@@ -36,6 +37,7 @@ import io.netty.channel.ChannelPipeline;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.val;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.minimessage.MiniMessage;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.ReflectiveOperationException;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
@@ -93,8 +95,7 @@ public final class FallbackListener {
|
||||
InetSocketAddress.class,
|
||||
boolean.class,
|
||||
IdentifiedKey.class
|
||||
)
|
||||
);
|
||||
));
|
||||
|
||||
try {
|
||||
SESSION_HANDLER_FIELD = MinecraftConnection.class.getDeclaredField("activeSessionHandler");
|
||||
@@ -110,8 +111,7 @@ public final class FallbackListener {
|
||||
INITIAL_CONNECTION = MethodHandles.privateLookupIn(LoginInboundConnection.class, LOOKUP)
|
||||
.findGetter(LoginInboundConnection.class,
|
||||
"delegate",
|
||||
InitialInboundConnection.class
|
||||
);
|
||||
InitialInboundConnection.class);
|
||||
} catch (Throwable throwable) {
|
||||
throw new ReflectiveOperationException(throwable);
|
||||
}
|
||||
@@ -126,7 +126,8 @@ public final class FallbackListener {
|
||||
|
||||
@Subscribe(order = PostOrder.LAST)
|
||||
public void handle(final @NotNull PreLoginEvent event) throws Throwable {
|
||||
Statistics.TOTAL_TRAFFIC.increment();
|
||||
// Increase joins per second for the action bar verbose
|
||||
Sonar.get().getVerboseHandler().getLoginsPerSecond().put(System.nanoTime());
|
||||
|
||||
val inboundConnection = (LoginInboundConnection) event.getConnection();
|
||||
val initialConnection = (InitialInboundConnection) INITIAL_CONNECTION.invokeExact(inboundConnection);
|
||||
@@ -146,6 +147,9 @@ public final class FallbackListener {
|
||||
|
||||
final InetAddress inetAddress = event.getConnection().getRemoteAddress().getAddress();
|
||||
|
||||
// Increase total traffic statistic
|
||||
Statistics.TOTAL_TRAFFIC.increment();
|
||||
|
||||
try {
|
||||
// Backwards compatibility for Velocity b265 and below
|
||||
val activeSessionHandler = (MinecraftSessionHandler) SESSION_HANDLER_FIELD.get(mcConnection);
|
||||
@@ -161,22 +165,24 @@ public final class FallbackListener {
|
||||
}
|
||||
|
||||
// Don't continue the verification process if the verification is disabled
|
||||
if (!Sonar.get().getConfig().getVerification().isEnabled()) return;
|
||||
if (!Sonar.get().getFallback().shouldVerifyNewPlayers()) return;
|
||||
|
||||
// Check if the player is already verified.
|
||||
// No one wants to be verified over and over again.
|
||||
final GameProfile gameProfile = GameProfile.forOfflinePlayer(event.getUsername());
|
||||
if (Sonar.get().getVerifiedPlayerController().has(inetAddress, gameProfile.getId())) return;
|
||||
|
||||
// Completely skip Geyser connections (for now)
|
||||
// Completely skip Geyser connections
|
||||
if (isGeyserConnection(channel)) {
|
||||
// TODO: Do we need to log this?
|
||||
fallback.getLogger().info("Allowing Geyser connection: {}", inetAddress);
|
||||
fallback.getLogger().info("Skipping Geyser player: {}{}",
|
||||
gameProfile.getName(), Sonar.get().getConfig().formatAddress(inetAddress));
|
||||
return;
|
||||
}
|
||||
|
||||
// We now mark the connection as dead by using our fake connection
|
||||
markConnectionAsDead(activeSessionHandler);
|
||||
// Don't allow exceptions or disconnect messages
|
||||
mcConnection.setAssociation(null);
|
||||
|
||||
// Check if the player is already queued since we don't want bots to flood the queue
|
||||
if (fallback.getQueue().getQueuedPlayers().containsKey(inetAddress)) {
|
||||
@@ -250,9 +256,6 @@ public final class FallbackListener {
|
||||
&& (SonarVelocity.INSTANCE.getPlugin().getServer().getConfiguration().isOnlineMode()
|
||||
|| event.getResult().isOnlineModeAllowed());
|
||||
|
||||
// Don't allow exceptions or disconnect messages
|
||||
mcConnection.setAssociation(null);
|
||||
|
||||
// Create an instance for the connected player
|
||||
final ConnectedPlayer connectedPlayer;
|
||||
try {
|
||||
@@ -272,21 +275,21 @@ public final class FallbackListener {
|
||||
}
|
||||
|
||||
// Create an instance for the Fallback connection
|
||||
final FallbackUserWrapper fallbackPlayer = new FallbackUserWrapper(
|
||||
final FallbackUserWrapper user = new FallbackUserWrapper(
|
||||
fallback, connectedPlayer, mcConnection, mcConnection.getChannel(),
|
||||
mcConnection.getChannel().pipeline(), inetAddress,
|
||||
ProtocolVersion.fromId(connectedPlayer.getProtocolVersion().getProtocol())
|
||||
);
|
||||
|
||||
// Disconnect if the protocol version could not be resolved
|
||||
if (fallbackPlayer.getProtocolVersion().isUnknown()) {
|
||||
fallbackPlayer.disconnect(Sonar.get().getConfig().getVerification().getInvalidProtocol());
|
||||
if (user.getProtocolVersion().isUnknown()) {
|
||||
user.disconnect(Sonar.get().getConfig().getVerification().getInvalidProtocol());
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the player is already connected to the proxy but still tries to verify
|
||||
if (!mcConnection.server.canRegisterConnection(connectedPlayer)) {
|
||||
fallbackPlayer.disconnect(Sonar.get().getConfig().getVerification().getAlreadyConnected());
|
||||
user.disconnect(Sonar.get().getConfig().getVerification().getAlreadyConnected());
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -296,43 +299,44 @@ public final class FallbackListener {
|
||||
if (Sonar.get().getConfig().getVerification().isLogConnections()) {
|
||||
// Only log the processing message if the server isn't under attack.
|
||||
// We let the user override this through the configuration.
|
||||
if (!fallback.isPotentiallyUnderAttack() || Sonar.get().getConfig().getVerification().isLogDuringAttack()) {
|
||||
if (!Sonar.get().getAttackTracker().isCurrentlyUnderAttack()
|
||||
|| Sonar.get().getConfig().getVerification().isLogDuringAttack()) {
|
||||
fallback.getLogger().info(Sonar.get().getConfig().getVerification().getConnectLog()
|
||||
.replace("%name%", connectedPlayer.getUsername())
|
||||
.replace("%ip%", Sonar.get().getConfig().formatAddress(fallbackPlayer.getInetAddress()))
|
||||
.replace("%protocol%", String.valueOf(fallbackPlayer.getProtocolVersion().getProtocol())));
|
||||
.replace("%ip%", Sonar.get().getConfig().formatAddress(user.getInetAddress()))
|
||||
.replace("%protocol%", String.valueOf(user.getProtocolVersion().getProtocol())));
|
||||
}
|
||||
}
|
||||
|
||||
// Call the VerifyJoinEvent for external API usage
|
||||
Sonar.get().getEventManager().publish(new UserVerifyJoinEvent(gameProfile.getName(), fallbackPlayer));
|
||||
Sonar.get().getEventManager().publish(new UserVerifyJoinEvent(gameProfile.getName(), user));
|
||||
|
||||
// Mark the player as connected → verifying players
|
||||
fallback.getConnected().put(gameProfile.getName(), inetAddress);
|
||||
|
||||
// This sometimes happens when the channel hangs, but the player is still connecting
|
||||
// This also fixes a weird issue with TCPShield and other reverse proxies
|
||||
if (fallbackPlayer.getPipeline().get(MINECRAFT_ENCODER) == null
|
||||
|| fallbackPlayer.getPipeline().get(MINECRAFT_DECODER) == null) {
|
||||
// This also fixes a unique issue with TCPShield and other reverse proxies
|
||||
if (user.getPipeline().get(MINECRAFT_ENCODER) == null
|
||||
|| user.getPipeline().get(MINECRAFT_DECODER) == null) {
|
||||
mcConnection.close(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Replace normal encoder to allow custom packets
|
||||
final FallbackPacketEncoder encoder = new FallbackPacketEncoder(fallbackPlayer.getProtocolVersion());
|
||||
fallbackPlayer.getPipeline().replace(MINECRAFT_ENCODER, FALLBACK_PACKET_ENCODER, encoder);
|
||||
final FallbackPacketEncoder encoder = new FallbackPacketEncoder(user.getProtocolVersion());
|
||||
user.getPipeline().replace(MINECRAFT_ENCODER, FALLBACK_PACKET_ENCODER, encoder);
|
||||
|
||||
// Send LoginSuccess packet to make the client think they are joining the server
|
||||
fallbackPlayer.write(new LoginSuccess(gameProfile.getName(), gameProfile.getId()));
|
||||
user.write(new LoginSuccess(gameProfile.getName(), gameProfile.getId()));
|
||||
|
||||
// The LoginSuccess packet has been sent, now we can change the registry state
|
||||
encoder.updateRegistry(fallbackPlayer.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_20_2) >= 0
|
||||
encoder.updateRegistry(user.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_20_2) >= 0
|
||||
? FallbackPacketRegistry.CONFIG : FallbackPacketRegistry.GAME);
|
||||
|
||||
// Replace normal decoder to allow custom packets
|
||||
fallbackPlayer.getPipeline().replace(
|
||||
MINECRAFT_DECODER, FALLBACK_PACKET_DECODER, new FallbackPacketDecoder(fallbackPlayer,
|
||||
new FallbackVerificationHandler(fallbackPlayer, gameProfile.getName(), connectedPlayer.getUniqueId())
|
||||
user.getPipeline().replace(
|
||||
MINECRAFT_DECODER, FALLBACK_PACKET_DECODER, new FallbackPacketDecoder(user,
|
||||
new FallbackVerificationHandler(user, gameProfile.getName(), connectedPlayer.getUniqueId())
|
||||
));
|
||||
}));
|
||||
} catch (Throwable throwable) {
|
||||
@@ -345,23 +349,20 @@ public final class FallbackListener {
|
||||
public void handle(final @NotNull LoginEvent event) {
|
||||
val connectedPlayer = (ConnectedPlayer) event.getPlayer();
|
||||
|
||||
if (Sonar.get().getConfig().getLockdown().isEnabled()) {
|
||||
if (!event.getPlayer().hasPermission(Sonar.get().getConfig().getLockdown().getBypassPermission())) {
|
||||
connectedPlayer.getConnection().closeWith(Disconnect.create(
|
||||
Sonar.get().getConfig().getLockdown().getDisconnect(), connectedPlayer.getProtocolVersion()));
|
||||
if (Sonar.get().getConfig().getLockdown().isEnabled()
|
||||
&& !event.getPlayer().hasPermission(Sonar.get().getConfig().getLockdown().getBypassPermission())) {
|
||||
connectedPlayer.getConnection().closeWith(Disconnect.create(
|
||||
Sonar.get().getConfig().getLockdown().getDisconnect(), connectedPlayer.getProtocolVersion()));
|
||||
|
||||
if (Sonar.get().getConfig().getLockdown().isLogAttempts()) {
|
||||
Sonar.get().getLogger().info(Sonar.get().getConfig().getLockdown().getConsoleLog()
|
||||
.replace("%player%", event.getPlayer().getUsername())
|
||||
.replace("%ip%", Sonar.get().getConfig()
|
||||
.formatAddress(event.getPlayer().getRemoteAddress().getAddress()))
|
||||
.replace("%protocol%",
|
||||
String.valueOf(event.getPlayer().getProtocolVersion().getProtocol())));
|
||||
}
|
||||
return;
|
||||
} else if (Sonar.get().getConfig().getLockdown().isNotifyAdmins()) {
|
||||
event.getPlayer().sendMessage(Component.text(Sonar.get().getConfig().getLockdown().getNotification()));
|
||||
if (Sonar.get().getConfig().getLockdown().isLogAttempts()) {
|
||||
Sonar.get().getLogger().info(Sonar.get().getConfig().getLockdown().getConsoleLog()
|
||||
.replace("%player%", event.getPlayer().getUsername())
|
||||
.replace("%ip%", Sonar.get().getConfig()
|
||||
.formatAddress(event.getPlayer().getRemoteAddress().getAddress()))
|
||||
.replace("%protocol%",
|
||||
String.valueOf(event.getPlayer().getProtocolVersion().getProtocol())));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
final InetAddress inetAddress = event.getPlayer().getRemoteAddress().getAddress();
|
||||
@@ -383,4 +384,15 @@ public final class FallbackListener {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe(order = PostOrder.LAST)
|
||||
public void handle(final @NotNull PostLoginEvent event) {
|
||||
if (Sonar.get().getConfig().getLockdown().isEnabled() && Sonar.get().getConfig().getLockdown().isNotifyAdmins()) {
|
||||
if (event.getPlayer().hasPermission(Sonar.get().getConfig().getLockdown().getBypassPermission())) {
|
||||
final String notification = Sonar.get().getConfig().getLockdown().getNotification();
|
||||
final Component deserialized = MiniMessage.miniMessage().deserialize(notification);
|
||||
event.getPlayer().sendMessage(deserialized);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-3
@@ -21,7 +21,6 @@ import com.velocitypowered.proxy.connection.MinecraftConnection;
|
||||
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelPipeline;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.ToString;
|
||||
@@ -35,9 +34,8 @@ import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.Disconnect;
|
||||
import java.net.InetAddress;
|
||||
|
||||
@Getter
|
||||
@ToString(of = {"protocolVersion"})
|
||||
@EqualsAndHashCode
|
||||
@RequiredArgsConstructor
|
||||
@ToString(of = {"protocolVersion"})
|
||||
public final class FallbackUserWrapper implements FallbackUser<ConnectedPlayer, MinecraftConnection> {
|
||||
private final Fallback fallback;
|
||||
private final ConnectedPlayer player;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"id": "sonar",
|
||||
"name": "$name",
|
||||
"name": "Sonar",
|
||||
"version": "$version",
|
||||
"description": "$description",
|
||||
"url": "$url",
|
||||
|
||||
Reference in new issue
Block a user