mirror of
https://github.com/jonesdevelopment/sonar.git
synced 2026-11-05 09:20:15 -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 |
No files matched your search
@@ -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 Varint decoding was taken from [Velocity](https://github.com/PaperMC/Velocity).
|
||||
+1
-3
@@ -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 API
|
||||
maven(url = "https://repo.jonesdev.xyz/releases/") // Bungee & Velocity proxy module
|
||||
}
|
||||
}
|
||||
|
||||
subprojects {
|
||||
apply(plugin = "java")
|
||||
apply(plugin = "com.github.johnrengelman.shadow")
|
||||
|
||||
@@ -81,6 +78,7 @@ 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")
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
group=xyz.jonesdev.sonar
|
||||
version=2.0.6
|
||||
version=2.0.8
|
||||
description=Effective Anti-bot plugin for Velocity, BungeeCord, and Bukkit (1.7-latest)
|
||||
@@ -17,7 +17,10 @@
|
||||
|
||||
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;
|
||||
@@ -33,6 +36,10 @@ 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 The platform the plugin is being run on
|
||||
*/
|
||||
@@ -75,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;
|
||||
}
|
||||
}
|
||||
@@ -23,12 +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
|
||||
|
||||
@@ -40,6 +40,65 @@ public interface SonarCommand {
|
||||
|
||||
List<Component> CACHED_HELP_MESSAGE = new ArrayList<>();
|
||||
|
||||
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;
|
||||
}
|
||||
// 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();
|
||||
|
||||
@@ -94,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,9 +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;
|
||||
@@ -52,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;
|
||||
@@ -69,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;
|
||||
}
|
||||
|
||||
@@ -87,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;
|
||||
@@ -100,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;
|
||||
@@ -175,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;
|
||||
}
|
||||
|
||||
@@ -206,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");
|
||||
@@ -221,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
|
||||
@@ -266,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"
|
||||
@@ -330,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"
|
||||
@@ -351,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?");
|
||||
@@ -375,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,
|
||||
@@ -415,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");
|
||||
@@ -830,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%",
|
||||
@@ -859,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("◜", "◝", "◞", "◟")
|
||||
));
|
||||
@@ -885,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));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.api.event.impl;
|
||||
|
||||
import xyz.jonesdev.sonar.api.event.SonarEvent;
|
||||
|
||||
public final class AttackDetectedEvent implements SonarEvent {
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.api.event.impl;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.ToString;
|
||||
import xyz.jonesdev.sonar.api.attack.AttackTracker;
|
||||
import xyz.jonesdev.sonar.api.event.SonarEvent;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@EqualsAndHashCode
|
||||
@RequiredArgsConstructor
|
||||
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;
|
||||
@@ -64,8 +66,11 @@ public final class Fallback {
|
||||
}
|
||||
};
|
||||
|
||||
public boolean isPotentiallyUnderAttack() {
|
||||
final int min = SONAR.getConfig().getMinPlayersForAttack();
|
||||
return Sonar.get().getVerboseHandler().getJoinsPerSecond().estimatedSize() > 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();
|
||||
|
||||
|
||||
@@ -103,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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -26,8 +26,10 @@ 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;
|
||||
@@ -44,13 +46,13 @@ public final class Verbose implements JVMProfiler {
|
||||
private final @NotNull Collection<String> subscribers = new Vector<>(0);
|
||||
private final @NotNull Map<String, Audience> audiences = new ConcurrentHashMap<>();
|
||||
private int animationIndex;
|
||||
private final ExpiringCache<Long> joinsPerSecond = Cappuccino.buildExpiring(1L, TimeUnit.SECONDS);
|
||||
private final ExpiringCache<Long> loginsPerSecond = Cappuccino.buildExpiring(1L, TimeUnit.SECONDS);
|
||||
|
||||
// Run action bar verbose
|
||||
public void update() {
|
||||
// Clean up all blacklisted IPs
|
||||
Sonar.get().getFallback().getBlacklisted().cleanUp(false);
|
||||
joinsPerSecond.cleanUp(false);
|
||||
loginsPerSecond.cleanUp(false);
|
||||
|
||||
// Don't prepare component if there are no subscribers
|
||||
if (subscribers.isEmpty()) return;
|
||||
@@ -65,18 +67,23 @@ public final class Verbose implements JVMProfiler {
|
||||
}
|
||||
|
||||
public @NotNull Component prepareActionBarFormat() {
|
||||
return MiniMessage.miniMessage().deserialize(Sonar.get().getConfig().getVerbose().getActionBarLayout()
|
||||
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(Statistics.TOTAL_TRAFFIC.get()))
|
||||
.replace("%per-second-joins%", DECIMAL_FORMAT.format(joinsPerSecond.estimatedSize()))
|
||||
.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())
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@ bukkit {
|
||||
|
||||
commands {
|
||||
register("sonar") {
|
||||
// No permission check here
|
||||
// No permission checks here
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,6 @@ 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.fallback.traffic.TrafficCounter;
|
||||
import xyz.jonesdev.sonar.api.logger.LoggerWrapper;
|
||||
import xyz.jonesdev.sonar.bukkit.audience.AudienceListener;
|
||||
import xyz.jonesdev.sonar.bukkit.command.BukkitSonarCommand;
|
||||
@@ -79,17 +78,5 @@ public final class SonarBukkit extends SonarBootstrap<SonarBukkitPlugin> {
|
||||
|
||||
// Register audience register listener
|
||||
getPlugin().getServer().getPluginManager().registerEvents(new AudienceListener(), getPlugin());
|
||||
|
||||
// Register Fallback queue task
|
||||
getPlugin().getServer().getScheduler().runTaskTimerAsynchronously(getPlugin(), getFallback().getQueue()::poll,
|
||||
10L, 10L);
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
+4
-1
@@ -25,6 +25,9 @@ import xyz.jonesdev.sonar.bukkit.SonarBukkit;
|
||||
|
||||
public final class BukkitInvocationSource extends InvocationSource {
|
||||
public BukkitInvocationSource(final @NotNull CommandSender sender) {
|
||||
super(sender.getName(), SonarBukkit.INSTANCE.getBukkitAudiences().sender(sender), sender instanceof Player);
|
||||
super(sender.getName(),
|
||||
SonarBukkit.INSTANCE.getBukkitAudiences().sender(sender),
|
||||
sender instanceof Player,
|
||||
sender::hasPermission);
|
||||
}
|
||||
}
|
||||
+3
-69
@@ -17,20 +17,15 @@
|
||||
|
||||
package xyz.jonesdev.sonar.bukkit.command;
|
||||
|
||||
import net.kyori.adventure.text.Component;
|
||||
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.InvocationSource;
|
||||
import xyz.jonesdev.sonar.api.command.SonarCommand;
|
||||
import xyz.jonesdev.sonar.api.command.subcommand.Subcommand;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import static java.util.Collections.emptyList;
|
||||
|
||||
@@ -43,70 +38,9 @@ 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 (invocationSource.isPlayer()) {
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
} else {
|
||||
subcommand.get().invoke(invocationSource, args);
|
||||
}
|
||||
return false;
|
||||
// Pass the invocation source and command arguments to our command handler
|
||||
handle(invocationSource, args);
|
||||
return true; // Valid
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -15,7 +15,8 @@ dependencies {
|
||||
compileOnly(project(":api"))
|
||||
compileOnly(project(":common"))
|
||||
|
||||
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")
|
||||
|
||||
@@ -22,7 +22,6 @@ 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.fallback.traffic.TrafficCounter;
|
||||
import xyz.jonesdev.sonar.api.logger.LoggerWrapper;
|
||||
import xyz.jonesdev.sonar.bungee.audience.AudienceListener;
|
||||
import xyz.jonesdev.sonar.bungee.command.BungeeSonarCommand;
|
||||
@@ -31,8 +30,6 @@ import xyz.jonesdev.sonar.bungee.fallback.injection.BaseInjectionHelper;
|
||||
import xyz.jonesdev.sonar.bungee.fallback.injection.ChildChannelInitializer;
|
||||
import xyz.jonesdev.sonar.common.boot.SonarBootstrap;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@Getter
|
||||
public final class SonarBungee extends SonarBootstrap<SonarBungeePlugin> {
|
||||
public static SonarBungee INSTANCE;
|
||||
@@ -86,18 +83,6 @@ public final class SonarBungee extends SonarBootstrap<SonarBungeePlugin> {
|
||||
// Register audience register listener
|
||||
getPlugin().getServer().getPluginManager().registerListener(getPlugin(), new AudienceListener());
|
||||
|
||||
// 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);
|
||||
|
||||
// Inject base into ProtocolUtils
|
||||
BaseInjectionHelper.inject(ChildChannelInitializer.INSTANCE);
|
||||
}
|
||||
|
||||
+4
-1
@@ -25,6 +25,9 @@ import xyz.jonesdev.sonar.bungee.SonarBungee;
|
||||
|
||||
public final class BungeeInvocationSource extends InvocationSource {
|
||||
public BungeeInvocationSource(final @NotNull CommandSender sender) {
|
||||
super(sender.getName(), SonarBungee.INSTANCE.getBungeeAudiences().sender(sender), sender instanceof ProxiedPlayer);
|
||||
super(sender.getName(),
|
||||
SonarBungee.INSTANCE.getBungeeAudiences().sender(sender),
|
||||
sender instanceof ProxiedPlayer,
|
||||
sender::hasPermission);
|
||||
}
|
||||
}
|
||||
+2
-69
@@ -17,18 +17,12 @@
|
||||
|
||||
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.plugin.Command;
|
||||
import net.md_5.bungee.api.plugin.TabExecutor;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.command.InvocationSource;
|
||||
import xyz.jonesdev.sonar.api.command.SonarCommand;
|
||||
import xyz.jonesdev.sonar.api.command.subcommand.Subcommand;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Optional;
|
||||
|
||||
import static java.util.Collections.emptyList;
|
||||
|
||||
@@ -42,69 +36,8 @@ 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 (invocationSource.isPlayer()) {
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
} else {
|
||||
subcommand.get().invoke(invocationSource, args);
|
||||
}
|
||||
// Pass the invocation source and command arguments to our command handler
|
||||
handle(invocationSource, args);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+2
-10
@@ -40,11 +40,7 @@ public final class FallbackHandlerBoss extends HandlerBoss {
|
||||
public void channelActive(final ChannelHandlerContext ctx) throws Exception {
|
||||
if (handler != null) {
|
||||
channel = new ChannelWrapper(ctx);
|
||||
handler.connected(channel);
|
||||
|
||||
if (handler instanceof DownstreamBridge) {
|
||||
ProxyServer.getInstance().getLogger().info(handler + " has connected");
|
||||
}
|
||||
super.channelActive(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,11 +48,7 @@ public final class FallbackHandlerBoss extends HandlerBoss {
|
||||
public void channelInactive(final ChannelHandlerContext ctx) throws Exception {
|
||||
if (handler != null) {
|
||||
channel.markClosed();
|
||||
handler.disconnected(channel);
|
||||
|
||||
if (handler instanceof DownstreamBridge) {
|
||||
ProxyServer.getInstance().getLogger().info(handler + " has disconnected");
|
||||
}
|
||||
super.channelInactive(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+49
-37
@@ -28,6 +28,7 @@ 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;
|
||||
@@ -54,9 +55,11 @@ 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.*;
|
||||
@@ -70,11 +73,12 @@ public final class FallbackInitialHandler extends InitialHandler {
|
||||
super(bungee, listener);
|
||||
this.bungee = bungee;
|
||||
}
|
||||
|
||||
private static final @NotNull Fallback FALLBACK = Objects.requireNonNull(Sonar.get().getFallback());
|
||||
@Getter
|
||||
private ChannelWrapper channelWrapper;
|
||||
private @NotNull final BungeeCord bungee;
|
||||
private @Nullable FallbackUserWrapper player;
|
||||
private final @NotNull BungeeCord bungee;
|
||||
private @Nullable FallbackUserWrapper user;
|
||||
@Getter
|
||||
private ProtocolVersion protocolVersion;
|
||||
private boolean receivedLoginPacket;
|
||||
@@ -86,11 +90,6 @@ public final class FallbackInitialHandler extends InitialHandler {
|
||||
super.connected(channelWrapper);
|
||||
}
|
||||
|
||||
private static @NotNull Kick generateKickPacket(final Component component) {
|
||||
final String serialized = JSONComponentSerializer.json().serialize(component);
|
||||
return new Kick(serialized);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(final StatusRequest statusRequest) throws Exception {
|
||||
// Fix status packet spam exploit
|
||||
@@ -116,10 +115,10 @@ public final class FallbackInitialHandler extends InitialHandler {
|
||||
@Override
|
||||
public void handle(final LoginRequest loginRequest) throws Exception {
|
||||
// Increase joins per second for the action bar verbose
|
||||
Sonar.get().getVerboseHandler().getJoinsPerSecond().put(System.nanoTime());
|
||||
Sonar.get().getVerboseHandler().getLoginsPerSecond().put(System.nanoTime());
|
||||
|
||||
// Fix login packet spam exploit
|
||||
if (receivedLoginPacket || player != null) {
|
||||
if (receivedLoginPacket || user != null) {
|
||||
throw new ConditionFailedException("Duplicate login packet");
|
||||
}
|
||||
receivedLoginPacket = true;
|
||||
@@ -137,7 +136,7 @@ public final class FallbackInitialHandler extends InitialHandler {
|
||||
Statistics.TOTAL_TRAFFIC.increment();
|
||||
|
||||
// Check if the verification is enabled
|
||||
if (!Sonar.get().getConfig().getVerification().isEnabled()) {
|
||||
if (!Sonar.get().getFallback().shouldVerifyNewPlayers()) {
|
||||
super.handle(loginRequest);
|
||||
return;
|
||||
}
|
||||
@@ -152,27 +151,27 @@ public final class FallbackInitialHandler extends InitialHandler {
|
||||
|
||||
// Check the blacklist here since we cannot let the player "ghost join"
|
||||
if (FALLBACK.getBlacklisted().has(inetAddress.toString())) {
|
||||
closeWith(generateKickPacket(Sonar.get().getConfig().getVerification().getBlacklisted()));
|
||||
closeWith(getKickPacket(Sonar.get().getConfig().getVerification().getBlacklisted()));
|
||||
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: {}{}",
|
||||
loginRequest.getData(), Sonar.get().getConfig().formatAddress(inetAddress));
|
||||
super.handle(loginRequest);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create wrapped Fallback user
|
||||
player = new FallbackUserWrapper(
|
||||
user = new FallbackUserWrapper(
|
||||
FALLBACK, channelWrapper, this,
|
||||
channel, channel.pipeline(), inetAddress, protocolVersion
|
||||
);
|
||||
|
||||
// Perform default BungeeCord checks
|
||||
if (bungee.config.isEnforceSecureProfile()
|
||||
&& player.getProtocolVersion().compareTo(MINECRAFT_1_19_3) < 0) {
|
||||
&& user.getProtocolVersion().compareTo(MINECRAFT_1_19_3) < 0) {
|
||||
final PlayerPublicKey publicKey = loginRequest.getPublicKey();
|
||||
if (publicKey == null) {
|
||||
disconnect(bungee.getTranslation("secure_profile_required"));
|
||||
@@ -182,7 +181,7 @@ public final class FallbackInitialHandler extends InitialHandler {
|
||||
disconnect(bungee.getTranslation("secure_profile_expired"));
|
||||
return;
|
||||
}
|
||||
if (player.getProtocolVersion().compareTo(MINECRAFT_1_19_1) < 0
|
||||
if (user.getProtocolVersion().compareTo(MINECRAFT_1_19_1) < 0
|
||||
&& !EncryptionUtil.check(publicKey, null)) {
|
||||
disconnect(bungee.getTranslation("secure_profile_invalid"));
|
||||
return;
|
||||
@@ -191,7 +190,7 @@ public final class FallbackInitialHandler extends InitialHandler {
|
||||
|
||||
// Check if the player is already queued since we don't want bots to flood the queue
|
||||
if (FALLBACK.getQueue().getQueuedPlayers().containsKey(inetAddress)) {
|
||||
closeWith(generateKickPacket(Sonar.get().getConfig().getVerification().getAlreadyQueued()));
|
||||
closeWith(getKickPacket(Sonar.get().getConfig().getVerification().getAlreadyQueued()));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -199,20 +198,20 @@ public final class FallbackInitialHandler extends InitialHandler {
|
||||
// → is another player with the same IP address connected to Fallback?
|
||||
if (FALLBACK.getConnected().containsKey(loginRequest.getData())
|
||||
|| FALLBACK.getConnected().containsValue(inetAddress)) {
|
||||
closeWith(generateKickPacket(Sonar.get().getConfig().getVerification().getAlreadyVerifying()));
|
||||
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(generateKickPacket(Sonar.get().getConfig().getVerification().getTooManyPlayers()));
|
||||
closeWith(getKickPacket(Sonar.get().getConfig().getVerification().getTooManyPlayers()));
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the IP address is currently being rate-limited
|
||||
if (!FALLBACK.getRatelimiter().attempt(inetAddress)) {
|
||||
closeWith(generateKickPacket(Sonar.get().getConfig().getVerification().getTooFastReconnect()));
|
||||
closeWith(getKickPacket(Sonar.get().getConfig().getVerification().getTooFastReconnect()));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -229,7 +228,7 @@ public final class FallbackInitialHandler extends InitialHandler {
|
||||
// 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(generateKickPacket(Sonar.get().getConfig().getVerification().getInvalidUsername()));
|
||||
closeWith(getKickPacket(Sonar.get().getConfig().getVerification().getInvalidUsername()));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -243,8 +242,8 @@ public final class FallbackInitialHandler extends InitialHandler {
|
||||
);
|
||||
|
||||
// Disconnect if the protocol version could not be resolved
|
||||
if (player.getProtocolVersion().isUnknown()) {
|
||||
closeWith(generateKickPacket(Sonar.get().getConfig().getVerification().getInvalidProtocol()));
|
||||
if (user.getProtocolVersion().isUnknown()) {
|
||||
closeWith(getKickPacket(Sonar.get().getConfig().getVerification().getInvalidProtocol()));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -254,7 +253,7 @@ public final class FallbackInitialHandler extends InitialHandler {
|
||||
disconnect(bungee.getTranslation("proxy_full"));
|
||||
return;
|
||||
} else if (!isOnlineMode() && bungee.getPlayer(loginRequest.getData()) != null) {
|
||||
closeWith(generateKickPacket(Sonar.get().getConfig().getVerification().getAlreadyConnected()));
|
||||
closeWith(getKickPacket(Sonar.get().getConfig().getVerification().getAlreadyConnected()));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -264,43 +263,44 @@ public final class FallbackInitialHandler extends InitialHandler {
|
||||
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%", loginRequest.getData())
|
||||
.replace("%ip%", Sonar.get().getConfig().formatAddress(inetAddress))
|
||||
.replace("%protocol%", String.valueOf(player.getProtocolVersion().getProtocol())));
|
||||
.replace("%protocol%", String.valueOf(user.getProtocolVersion().getProtocol())));
|
||||
}
|
||||
}
|
||||
|
||||
// Call the VerifyJoinEvent for external API usage
|
||||
Sonar.get().getEventManager().publish(new UserVerifyJoinEvent(loginRequest.getData(), player));
|
||||
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 (player.getPipeline().get(PACKET_ENCODER) == null
|
||||
|| player.getPipeline().get(PACKET_DECODER) == null) {
|
||||
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(player.getProtocolVersion());
|
||||
player.getPipeline().replace(PACKET_ENCODER, FALLBACK_PACKET_ENCODER, encoder);
|
||||
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
|
||||
player.write(new LoginSuccess(loginRequest.getData(), uuid));
|
||||
user.write(new LoginSuccess(loginRequest.getData(), uuid));
|
||||
|
||||
// The LoginSuccess packet has been sent, now we can change the registry state
|
||||
encoder.updateRegistry(player.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_20_2) >= 0
|
||||
encoder.updateRegistry(user.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_20_2) >= 0
|
||||
? FallbackPacketRegistry.CONFIG : FallbackPacketRegistry.GAME);
|
||||
|
||||
// Replace normal decoder to allow custom packets
|
||||
player.getPipeline().replace(
|
||||
PACKET_DECODER, FALLBACK_PACKET_DECODER, new FallbackPacketDecoder(player,
|
||||
new FallbackVerificationHandler(player, loginRequest.getData(), uuid)
|
||||
user.getPipeline().replace(
|
||||
PACKET_DECODER, FALLBACK_PACKET_DECODER, new FallbackPacketDecoder(user,
|
||||
new FallbackVerificationHandler(user, loginRequest.getData(), uuid)
|
||||
));
|
||||
}));
|
||||
} catch (Throwable throwable) {
|
||||
@@ -309,6 +309,18 @@ public final class FallbackInitialHandler extends InitialHandler {
|
||||
});
|
||||
}
|
||||
|
||||
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()) {
|
||||
|
||||
+38
-6
@@ -18,20 +18,48 @@
|
||||
package xyz.jonesdev.sonar.bungee.fallback;
|
||||
|
||||
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.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.event.EventHandler;
|
||||
import net.md_5.bungee.protocol.packet.Kick;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.bungee.SonarBungee;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.util.Objects;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public final class FallbackListener implements Listener {
|
||||
|
||||
@EventHandler
|
||||
@SuppressWarnings("deprecation")
|
||||
public void handle(final @NotNull LoginEvent event) {
|
||||
final InetAddress inetAddress = event.getConnection().getAddress().getAddress();
|
||||
|
||||
// Check if the number of online players using the same IP address as
|
||||
// the connecting player is greater than the configured amount
|
||||
final int maxOnlinePerIp = Sonar.get().getConfig().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) {
|
||||
@@ -42,8 +70,7 @@ 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();
|
||||
@@ -60,7 +87,12 @@ public final class FallbackListener implements Listener {
|
||||
String.valueOf(event.getPlayer().getPendingConnection().getVersion())));
|
||||
}
|
||||
} 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,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;
|
||||
|
||||
@@ -85,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();
|
||||
@@ -129,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!");
|
||||
}
|
||||
}
|
||||
+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
-213
@@ -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,88 +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 DAMAGE_TYPE_1194;
|
||||
private final CompoundBinaryTag DAMAGE_TYPE_120;
|
||||
|
||||
static {
|
||||
DAMAGE_TYPE_1194 = getMapping("damage_type_1194.nbt");
|
||||
DAMAGE_TYPE_120 = getMapping("damage_type_120.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;
|
||||
@@ -135,153 +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_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");
|
||||
}
|
||||
|
||||
+163
-126
@@ -21,167 +21,204 @@ import io.netty.buffer.ByteBuf;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import net.kyori.adventure.nbt.CompoundBinaryTag;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.dimension.DimensionInfo;
|
||||
|
||||
import 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) {
|
||||
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_20_2) >= 0) {
|
||||
// haha funny, they made 1.20.2 more complicated
|
||||
encode1202Up(byteBuf, protocolVersion);
|
||||
} else if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_16) >= 0) {
|
||||
// Minecraft 1.16 and above have significantly more complicated logic for reading this packet,
|
||||
// so separate it out.
|
||||
encode116Up(byteBuf, protocolVersion);
|
||||
} else {
|
||||
encodeLegacy(byteBuf, protocolVersion);
|
||||
}
|
||||
}
|
||||
|
||||
private void encodeLegacy(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
|
||||
public void encode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
|
||||
byteBuf.writeInt(entityId);
|
||||
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_16_2) >= 0) {
|
||||
byteBuf.writeBoolean(isHardcore);
|
||||
|
||||
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(0); // max players
|
||||
writeString(byteBuf, "flat"); // level type
|
||||
}
|
||||
|
||||
if (protocolVersion.inBetween(MINECRAFT_1_8, MINECRAFT_1_9)) {
|
||||
byteBuf.writeByte(gamemode);
|
||||
} else {
|
||||
byteBuf.writeByte(isHardcore ? gamemode | 0x8 : gamemode);
|
||||
}
|
||||
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_9_1) >= 0) {
|
||||
byteBuf.writeInt(dimension);
|
||||
} else {
|
||||
byteBuf.writeByte(dimension);
|
||||
}
|
||||
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_13_2) <= 0) {
|
||||
byteBuf.writeByte(difficulty);
|
||||
}
|
||||
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_15) >= 0) {
|
||||
byteBuf.writeLong(partialHashedSeed);
|
||||
}
|
||||
byteBuf.writeByte(1); // max players
|
||||
if (levelType == null) {
|
||||
throw new IllegalStateException("No level type specified.");
|
||||
}
|
||||
writeString(byteBuf, levelType);
|
||||
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_14) >= 0) {
|
||||
writeVarInt(byteBuf, viewDistance);
|
||||
}
|
||||
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_8) >= 0) {
|
||||
byteBuf.writeByte(DEFAULT_DIMENSION_1_16.getId());
|
||||
byteBuf.writeByte(0); // difficulty
|
||||
byteBuf.writeByte(0); // max players
|
||||
writeString(byteBuf, "flat"); // level type
|
||||
byteBuf.writeBoolean(reducedDebugInfo);
|
||||
}
|
||||
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_15) >= 0) {
|
||||
|
||||
if (protocolVersion.inBetween(MINECRAFT_1_9_1, MINECRAFT_1_13_2)) {
|
||||
byteBuf.writeByte(gamemode);
|
||||
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.inBetween(MINECRAFT_1_15, MINECRAFT_1_15_2)) {
|
||||
byteBuf.writeByte(gamemode);
|
||||
byteBuf.writeInt(DEFAULT_DIMENSION_1_16.getId());
|
||||
byteBuf.writeLong(partialHashedSeed);
|
||||
byteBuf.writeByte(0); // max players
|
||||
writeString(byteBuf, "flat"); // level type
|
||||
writeVarInt(byteBuf, viewDistance);
|
||||
byteBuf.writeBoolean(reducedDebugInfo);
|
||||
byteBuf.writeBoolean(showRespawnScreen);
|
||||
}
|
||||
}
|
||||
|
||||
private void encode116Up(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
|
||||
byteBuf.writeInt(entityId);
|
||||
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_16_2) >= 0) {
|
||||
if (protocolVersion.inBetween(MINECRAFT_1_16, MINECRAFT_1_16_1)) {
|
||||
byteBuf.writeByte(gamemode);
|
||||
byteBuf.writeByte(-1); // previous gamemode
|
||||
writeStringArray(byteBuf, levelNames);
|
||||
writeCompoundTag(byteBuf, OLD_CODEC);
|
||||
writeString(byteBuf, DEFAULT_DIMENSION_1_16.getIdentifier());
|
||||
writeString(byteBuf, levelName);
|
||||
byteBuf.writeLong(partialHashedSeed);
|
||||
byteBuf.writeByte(0); // max players
|
||||
writeVarInt(byteBuf, viewDistance);
|
||||
byteBuf.writeBoolean(reducedDebugInfo);
|
||||
byteBuf.writeBoolean(showRespawnScreen);
|
||||
byteBuf.writeBoolean(false); // debug type
|
||||
byteBuf.writeBoolean(false); // flat
|
||||
}
|
||||
|
||||
if (protocolVersion.inBetween(MINECRAFT_1_16_2, MINECRAFT_1_17_1)) {
|
||||
byteBuf.writeBoolean(isHardcore);
|
||||
byteBuf.writeByte(gamemode);
|
||||
} else {
|
||||
byteBuf.writeByte(isHardcore ? gamemode | 0x8 : gamemode);
|
||||
}
|
||||
byteBuf.writeByte(previousGamemode);
|
||||
|
||||
writeStringArray(byteBuf, levelNames);
|
||||
writeCompoundTag(byteBuf, registry);
|
||||
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_16_2) >= 0
|
||||
&& protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_19) < 0) {
|
||||
writeCompoundTag(byteBuf, currentDimensionData);
|
||||
writeString(byteBuf, dimensionInfo.getIdentifier());
|
||||
} else {
|
||||
writeString(byteBuf, dimensionInfo.getIdentifier());
|
||||
writeString(byteBuf, dimensionInfo.getLevelName());
|
||||
}
|
||||
|
||||
byteBuf.writeLong(partialHashedSeed);
|
||||
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_16_2) >= 0) {
|
||||
byteBuf.writeByte(-1); // previous gamemode
|
||||
writeStringArray(byteBuf, levelNames);
|
||||
writeCompoundTag(byteBuf, CODEC_1_16);
|
||||
writeCompoundTag(byteBuf, DEFAULT_DIMENSION_1_16.getTag());
|
||||
writeString(byteBuf, levelName);
|
||||
byteBuf.writeLong(partialHashedSeed);
|
||||
writeVarInt(byteBuf, 0); // max players
|
||||
} else {
|
||||
byteBuf.writeByte(0); // max players
|
||||
writeVarInt(byteBuf, viewDistance);
|
||||
byteBuf.writeBoolean(reducedDebugInfo);
|
||||
byteBuf.writeBoolean(showRespawnScreen);
|
||||
byteBuf.writeBoolean(false); // debug type
|
||||
byteBuf.writeBoolean(false); // flat
|
||||
}
|
||||
|
||||
writeVarInt(byteBuf, viewDistance);
|
||||
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_18) >= 0) {
|
||||
writeVarInt(byteBuf, simulationDistance);
|
||||
if (protocolVersion.inBetween(MINECRAFT_1_18, MINECRAFT_1_18_2)) {
|
||||
byteBuf.writeBoolean(isHardcore);
|
||||
byteBuf.writeByte(gamemode);
|
||||
byteBuf.writeByte(-1); // previous gamemode
|
||||
writeStringArray(byteBuf, levelNames);
|
||||
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, 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(reducedDebugInfo);
|
||||
byteBuf.writeBoolean(showRespawnScreen);
|
||||
|
||||
byteBuf.writeBoolean(dimensionInfo.isDebug());
|
||||
byteBuf.writeBoolean(dimensionInfo.isFlat());
|
||||
|
||||
// optional death location
|
||||
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_19) >= 0) {
|
||||
byteBuf.writeBoolean(false); // no last death position
|
||||
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.compareTo(ProtocolVersion.MINECRAFT_1_20) >= 0) {
|
||||
writeVarInt(byteBuf, 0); // portal cooldown
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
private void encode1202Up(final @NotNull ByteBuf byteBuf,
|
||||
final @SuppressWarnings("unused") ProtocolVersion protocolVersion) {
|
||||
byteBuf.writeInt(entityId);
|
||||
byteBuf.writeBoolean(isHardcore);
|
||||
|
||||
writeStringArray(byteBuf, levelNames);
|
||||
|
||||
writeVarInt(byteBuf, 1); // max players
|
||||
|
||||
writeVarInt(byteBuf, viewDistance);
|
||||
writeVarInt(byteBuf, simulationDistance);
|
||||
|
||||
byteBuf.writeBoolean(reducedDebugInfo);
|
||||
byteBuf.writeBoolean(showRespawnScreen);
|
||||
byteBuf.writeBoolean(limitedCrafting);
|
||||
|
||||
writeString(byteBuf, dimensionInfo.getIdentifier());
|
||||
writeString(byteBuf, dimensionInfo.getLevelName());
|
||||
byteBuf.writeLong(partialHashedSeed);
|
||||
|
||||
byteBuf.writeByte(gamemode);
|
||||
byteBuf.writeByte(previousGamemode);
|
||||
|
||||
byteBuf.writeBoolean(dimensionInfo.isDebug());
|
||||
byteBuf.writeBoolean(dimensionInfo.isFlat());
|
||||
|
||||
byteBuf.writeBoolean(false); // no last death position
|
||||
|
||||
writeVarInt(byteBuf, 0); // portal cooldown
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_20_2) >= 0) {
|
||||
byteBuf.writeBoolean(isHardcore);
|
||||
writeStringArray(byteBuf, levelNames);
|
||||
writeVarInt(byteBuf, 0); // max players
|
||||
writeVarInt(byteBuf, viewDistance);
|
||||
writeVarInt(byteBuf, viewDistance); // simulation distance
|
||||
byteBuf.writeBoolean(reducedDebugInfo);
|
||||
byteBuf.writeBoolean(showRespawnScreen);
|
||||
byteBuf.writeBoolean(false); // limited crafting
|
||||
writeString(byteBuf, levelName);
|
||||
writeString(byteBuf, levelName);
|
||||
byteBuf.writeLong(partialHashedSeed);
|
||||
byteBuf.writeByte(gamemode);
|
||||
byteBuf.writeByte(-1); // previous gamemode
|
||||
byteBuf.writeBoolean(false); // debug type
|
||||
byteBuf.writeBoolean(false); // flat
|
||||
byteBuf.writeBoolean(false); // no last death location
|
||||
writeVarInt(byteBuf, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -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
-7
@@ -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
|
||||
protected void execute(final @NotNull CommandInvocation invocation) {
|
||||
final String json = GSON.toJson(collectMappedInformation());
|
||||
final String json = Sonar.GENERAL_GSON.toJson(collectMappedInformation());
|
||||
SONAR.getLogger().info("Generated dump: {}", json);
|
||||
}
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ public class UpdateChecker {
|
||||
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 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());
|
||||
@@ -78,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.
@@ -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,15 +20,12 @@ package xyz.jonesdev.sonar.velocity;
|
||||
import lombok.Getter;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.SonarPlatform;
|
||||
import xyz.jonesdev.sonar.api.fallback.traffic.TrafficCounter;
|
||||
import xyz.jonesdev.sonar.api.logger.LoggerWrapper;
|
||||
import xyz.jonesdev.sonar.common.boot.SonarBootstrap;
|
||||
import xyz.jonesdev.sonar.velocity.audience.AudienceListener;
|
||||
import xyz.jonesdev.sonar.velocity.command.VelocitySonarCommand;
|
||||
import xyz.jonesdev.sonar.velocity.fallback.FallbackListener;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@Getter
|
||||
public final class SonarVelocity extends SonarBootstrap<SonarVelocityPlugin> {
|
||||
public static SonarVelocity INSTANCE;
|
||||
@@ -76,20 +73,5 @@ public final class SonarVelocity extends SonarBootstrap<SonarVelocityPlugin> {
|
||||
|
||||
// Register audience register listener
|
||||
getPlugin().getServer().getEventManager().register(getPlugin(), new AudienceListener());
|
||||
|
||||
// Register Fallback queue task
|
||||
getPlugin().getServer().getScheduler().buildTask(getPlugin(), getFallback().getQueue()::poll)
|
||||
.repeat(500L, TimeUnit.MILLISECONDS)
|
||||
.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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
+4
-1
@@ -24,6 +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, sender instanceof Player);
|
||||
super(sender instanceof Player ? ((Player) sender).getUsername() : "Console",
|
||||
sender,
|
||||
sender instanceof Player,
|
||||
sender::hasPermission);
|
||||
}
|
||||
}
|
||||
+2
-68
@@ -18,16 +18,11 @@
|
||||
package xyz.jonesdev.sonar.velocity.command;
|
||||
|
||||
import com.velocitypowered.api.command.SimpleCommand;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.command.InvocationSource;
|
||||
import xyz.jonesdev.sonar.api.command.SonarCommand;
|
||||
import xyz.jonesdev.sonar.api.command.subcommand.Subcommand;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import static java.util.Collections.emptyList;
|
||||
|
||||
@@ -37,69 +32,8 @@ public final class VelocitySonarCommand implements SimpleCommand, SonarCommand {
|
||||
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 (invocationSource.isPlayer()) {
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
} else {
|
||||
subcommand.get().invoke(invocationSource, invocation.arguments());
|
||||
}
|
||||
// Pass the invocation source and command arguments to our command handler
|
||||
handle(invocationSource, invocation.arguments());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+52
-44
@@ -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);
|
||||
}
|
||||
@@ -127,7 +127,7 @@ public final class FallbackListener {
|
||||
@Subscribe(order = PostOrder.LAST)
|
||||
public void handle(final @NotNull PreLoginEvent event) throws Throwable {
|
||||
// Increase joins per second for the action bar verbose
|
||||
Sonar.get().getVerboseHandler().getJoinsPerSecond().put(System.nanoTime());
|
||||
Sonar.get().getVerboseHandler().getLoginsPerSecond().put(System.nanoTime());
|
||||
|
||||
val inboundConnection = (LoginInboundConnection) event.getConnection();
|
||||
val initialConnection = (InitialInboundConnection) INITIAL_CONNECTION.invokeExact(inboundConnection);
|
||||
@@ -165,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)) {
|
||||
@@ -254,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 {
|
||||
@@ -276,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;
|
||||
}
|
||||
|
||||
@@ -300,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 unique issue with TCPShield and other reverse proxies
|
||||
if (fallbackPlayer.getPipeline().get(MINECRAFT_ENCODER) == null
|
||||
|| fallbackPlayer.getPipeline().get(MINECRAFT_DECODER) == null) {
|
||||
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) {
|
||||
@@ -349,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();
|
||||
@@ -387,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user