mirror of
https://github.com/jonesdevelopment/sonar.git
synced 2026-11-05 01:10:17 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0b4394624f | ||
|
|
d8f7e288e8 | ||
|
|
3f3411ca9d | ||
|
|
14ae85384a | ||
|
|
8204477eb6 | ||
|
|
44754e5027 | ||
|
|
847936ed08 | ||
|
|
aa90a1fa22 | ||
|
|
9ac8447226 | ||
|
|
6b6acb8371 | ||
|
|
f3af903114 | ||
|
|
67bd3786d7 | ||
|
|
2b379b0dfa | ||
|
|
b81cc0e9f5 | ||
|
|
0efc6ae157 | ||
|
|
4ae14c58a6 | ||
|
|
3bb752f435 | ||
|
|
eeb9a446fb | ||
|
|
dfe728f7a9 | ||
|
|
3945e813dc |
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).
|
||||
+2
-4
@@ -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")
|
||||
|
||||
@@ -49,7 +46,7 @@ subprojects {
|
||||
implementation("com.j256.ormlite:ormlite-jdbc:6.1") // ORM
|
||||
implementation("xyz.jonesdev:cappuccino:0.1.6-SNAPSHOT") // expiring cache
|
||||
|
||||
compileOnly("io.netty:netty-all:4.1.100.Final") // netty
|
||||
compileOnly("io.netty:netty-all:4.1.101.Final") // netty
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.7
|
||||
version=2.0.9
|
||||
description=Effective Anti-bot plugin for Velocity, BungeeCord, and Bukkit (1.7-latest)
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
||||
@@ -20,7 +20,7 @@ package xyz.jonesdev.sonar.api;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.attack.AttackStatus;
|
||||
import xyz.jonesdev.sonar.api.attack.AttackTracker;
|
||||
import xyz.jonesdev.sonar.api.command.subcommand.SubcommandRegistry;
|
||||
import xyz.jonesdev.sonar.api.config.SonarConfiguration;
|
||||
import xyz.jonesdev.sonar.api.controller.VerifiedPlayerController;
|
||||
@@ -83,9 +83,9 @@ public interface Sonar {
|
||||
}
|
||||
|
||||
@NotNull
|
||||
default AttackStatus getAttackStatus() {
|
||||
default AttackTracker getAttackTracker() {
|
||||
// We don't want anyone else to create a new instance.
|
||||
return AttackStatus.INSTANCE;
|
||||
return AttackTracker.INSTANCE;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
|
||||
+24
-13
@@ -31,8 +31,8 @@ import java.util.Optional;
|
||||
|
||||
@Getter
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public final class AttackStatus implements JVMProfiler {
|
||||
public static final AttackStatus INSTANCE = new AttackStatus();
|
||||
public final class AttackTracker implements JVMProfiler {
|
||||
public static final AttackTracker INSTANCE = new AttackTracker();
|
||||
private @Nullable AttackStatistics currentAttack;
|
||||
private int attackThreshold;
|
||||
|
||||
@@ -45,23 +45,27 @@ public final class AttackStatus implements JVMProfiler {
|
||||
private int peakJoinsPerSecond;
|
||||
private double peakProcessCPUUsage;
|
||||
private long peakProcessMemoryUsage;
|
||||
// Calculate during-attack-statistics using their deltas
|
||||
private int successfulVerifications, failedVerifications;
|
||||
}
|
||||
|
||||
public void checkIfUnderAttack() {
|
||||
final int joinsPerSecond = Sonar.get().getVerboseHandler().getJoinsPerSecond().estimatedSize();
|
||||
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
|
||||
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();
|
||||
currentAttack.successfulVerifications = Sonar.get().getVerifiedPlayerController().estimatedSize();
|
||||
currentAttack.failedVerifications = Statistics.FAILED_VERIFICATIONS.get();
|
||||
Sonar.get().getEventManager().publish(new AttackDetectedEvent());
|
||||
} else {
|
||||
// Reset attack timer if we're still under attack
|
||||
@@ -70,20 +74,23 @@ public final class AttackStatus implements JVMProfiler {
|
||||
|
||||
// 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()) {
|
||||
// An attack has stopped
|
||||
// The current attack has stopped
|
||||
Sonar.get().getEventManager().publish(new AttackMitigatedEvent(currentAttack));
|
||||
|
||||
if (++attackThreshold > Sonar.get().getConfig().getMinAttackThreshold()) {
|
||||
@@ -97,6 +104,12 @@ public final class AttackStatus implements JVMProfiler {
|
||||
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();
|
||||
// Calculate during-attack-statistics using their deltas
|
||||
final long totalVerified = Sonar.get().getVerifiedPlayerController().estimatedSize();
|
||||
final long verified = Math.max(totalVerified - currentAttack.successfulVerifications, 0);
|
||||
final long totalFailed = Statistics.FAILED_VERIFICATIONS.get();
|
||||
final long failed = Math.max(totalFailed - currentAttack.failedVerifications, 0);
|
||||
|
||||
// Post webhook to Discord
|
||||
Optional.ofNullable(Sonar.get().getConfig().getDiscordWebhook()).ifPresent(webhook -> webhook.post(() -> {
|
||||
@@ -108,11 +121,9 @@ public final class AttackStatus implements JVMProfiler {
|
||||
.replace("%peak-cpu%", peakCPU)
|
||||
.replace("%peak-memory%", peakMem)
|
||||
.replace("%peak-bps%", peakBPS)
|
||||
.replace("%total-blacklisted%",
|
||||
Sonar.DECIMAL_FORMAT.format(Sonar.get().getFallback().getBlacklisted().estimatedSize()))
|
||||
.replace("%total-failed%", Sonar.DECIMAL_FORMAT.format(Statistics.FAILED_VERIFICATIONS.get()))
|
||||
.replace("%total-success%",
|
||||
Sonar.DECIMAL_FORMAT.format(Sonar.get().getVerifiedPlayerController().estimatedSize()));
|
||||
.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());
|
||||
}));
|
||||
@@ -129,7 +140,7 @@ public final class AttackStatus implements JVMProfiler {
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("all")
|
||||
@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();
|
||||
|
||||
@@ -28,8 +28,6 @@ import xyz.jonesdev.sonar.api.command.subcommand.argument.Argument;
|
||||
import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Getter
|
||||
@@ -51,18 +49,7 @@ public abstract class Subcommand {
|
||||
.collect(Collectors.joining(", "));
|
||||
}
|
||||
|
||||
// https://stackoverflow.com/questions/5284147/validating-ipv4-addresses-with-regexp
|
||||
private static final Pattern IP_PATTERN = Pattern.compile("^((25[0-5]|(2[0-4]|1\\d|[1-9]|)\\d)\\.?\\b){4}$");
|
||||
|
||||
protected static @Nullable InetAddress getInetAddressIfValid(final InvocationSource source, final String rawIP) {
|
||||
// We perform a regex check beforehand,
|
||||
// so we don't have to create a new InetAddress,
|
||||
// therefore, potentially saving some performance.
|
||||
if (!IP_PATTERN.matcher(Objects.requireNonNull(rawIP)).matches()) {
|
||||
source.sendMessage(SONAR.getConfig().getCommands().getIncorrectIpAddress());
|
||||
return null;
|
||||
}
|
||||
|
||||
final InetAddress inetAddress;
|
||||
try {
|
||||
inetAddress = InetAddress.getByName(rawIP);
|
||||
@@ -72,7 +59,7 @@ public abstract class Subcommand {
|
||||
return null;
|
||||
}
|
||||
} catch (UnknownHostException exception) {
|
||||
source.sendMessage(SONAR.getConfig().getCommands().getUnknownIpAddress());
|
||||
source.sendMessage(SONAR.getConfig().getCommands().getIncorrectIpAddress());
|
||||
return null;
|
||||
}
|
||||
return inetAddress;
|
||||
|
||||
@@ -94,12 +94,27 @@ 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) {
|
||||
yaml.addDefault(path, def);
|
||||
return yaml.getStringList(path);
|
||||
}
|
||||
|
||||
public List<Integer> getIntList(final String path, final List<Integer> def) {
|
||||
yaml.addDefault(path, def);
|
||||
return yaml.getIntegerList(path);
|
||||
}
|
||||
}
|
||||
@@ -29,10 +29,7 @@ import xyz.jonesdev.sonar.api.webhook.DiscordWebhook;
|
||||
|
||||
import java.io.File;
|
||||
import java.net.InetAddress;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.*;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.Sonar.LINE_SEPARATOR;
|
||||
@@ -85,6 +82,7 @@ public final class SonarConfiguration {
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public static final class Verbose {
|
||||
private String actionBarLayout;
|
||||
private String actionBarLayoutDuringAttack;
|
||||
private List<String> animation;
|
||||
}
|
||||
|
||||
@@ -97,9 +95,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;
|
||||
@@ -110,15 +114,29 @@ 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;
|
||||
private int maxVerifyingPlayers;
|
||||
private int maxLoginPackets;
|
||||
private int maxPing;
|
||||
private int readTimeout;
|
||||
private int reconnectDelay;
|
||||
private final Collection<Integer> whitelistedProtocols = new HashSet<>(0);
|
||||
private final Collection<Integer> blacklistedProtocols = new HashSet<>(0);
|
||||
|
||||
private Component tooManyPlayers;
|
||||
private Component tooFastReconnect;
|
||||
@@ -130,6 +148,7 @@ public final class SonarConfiguration {
|
||||
private Component alreadyVerifying;
|
||||
private Component alreadyQueued;
|
||||
private Component blacklisted;
|
||||
private Component protocolBlacklisted;
|
||||
}
|
||||
|
||||
@Getter
|
||||
@@ -138,7 +157,6 @@ public final class SonarConfiguration {
|
||||
private String incorrectCommandUsage;
|
||||
private String incorrectIpAddress;
|
||||
private String illegalIpAddress;
|
||||
private String unknownIpAddress;
|
||||
private String subCommandNoPerm;
|
||||
private String playersOnly;
|
||||
private String consoleOnly;
|
||||
@@ -281,7 +299,7 @@ public final class SonarConfiguration {
|
||||
|
||||
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");
|
||||
+ 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",
|
||||
@@ -319,8 +337,8 @@ public final class SonarConfiguration {
|
||||
generalConfig.getYaml().setComment("database.type",
|
||||
"Type of database Sonar uses to store verified players"
|
||||
+ LINE_SEPARATOR + "Possible types: NONE, MYSQL");
|
||||
final String newDatabaseType = generalConfig.getString("database.type", Database.Type.NONE.name());
|
||||
database.type = Database.Type.valueOf(newDatabaseType.toUpperCase());
|
||||
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"
|
||||
@@ -381,9 +399,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"
|
||||
@@ -402,16 +425,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?");
|
||||
@@ -426,47 +475,27 @@ 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,
|
||||
Short.MAX_VALUE);
|
||||
|
||||
generalConfig.getYaml().setComment("verification.rejoin-delay",
|
||||
"Minimum number of rejoin delay during verification");
|
||||
verification.reconnectDelay = clamp(generalConfig.getInt("verification.rejoin-delay", 8000), 0, 100000);
|
||||
|
||||
// Webhooks
|
||||
generalConfig.getYaml().setComment("verification.whitelisted-protocols",
|
||||
"List of protocol IDs which are not checked by Sonar (verification bypass)"
|
||||
+ LINE_SEPARATOR + "You can find the full list of all protocol IDs here:"
|
||||
+ LINE_SEPARATOR + "https://wiki.vg/Protocol_version_numbers"
|
||||
+ LINE_SEPARATOR + "For example, Minecraft 1.20 has the ID 763.");
|
||||
verification.whitelistedProtocols.clear();
|
||||
verification.whitelistedProtocols.addAll(generalConfig.getIntList("verification.whitelisted-protocols", new ArrayList<>(0)));
|
||||
|
||||
generalConfig.getYaml().setComment("verification.blacklisted-protocols",
|
||||
"List of protocol IDs which are unable to join the server at all");
|
||||
verification.blacklistedProtocols.clear();
|
||||
verification.blacklistedProtocols.addAll(generalConfig.getIntList("verification.blacklisted-protocols", new ArrayList<>(0)));
|
||||
|
||||
generalConfig.getYaml().setComment("webhook",
|
||||
"Bot attack notifications can also be sent to your Discord server using webhooks");
|
||||
generalConfig.getYaml().setComment("webhook.url",
|
||||
@@ -483,8 +512,8 @@ public final class SonarConfiguration {
|
||||
|
||||
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>");
|
||||
+ 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",
|
||||
@@ -581,7 +610,7 @@ public final class SonarConfiguration {
|
||||
"%prefix%<red>Usage: /sonar %usage%"));
|
||||
|
||||
messagesConfig.getYaml().setComment("commands.invalid-ip-address",
|
||||
"Message that is shown when someone provides an invalid IP address (Invalid characters)");
|
||||
"Message that is shown when someone provides an invalid IP address (Invalid format)");
|
||||
commands.incorrectIpAddress = formatString(messagesConfig.getString("commands.invalid-ip-address",
|
||||
"%prefix%The IP address you provided seems to be invalid."));
|
||||
|
||||
@@ -590,11 +619,6 @@ public final class SonarConfiguration {
|
||||
commands.illegalIpAddress = formatString(messagesConfig.getString("commands.illegal-ip-address",
|
||||
"%prefix%The IP address you provided seems to be either a local or loopback IP."));
|
||||
|
||||
messagesConfig.getYaml().setComment("commands.unknown-ip-address",
|
||||
"Message that is shown when someone provides an unknown IP address (Unknown Host)");
|
||||
commands.unknownIpAddress = formatString(messagesConfig.getString("commands.unknown-ip-address",
|
||||
"%prefix%The IP address you provided seems to be unknown."));
|
||||
|
||||
messagesConfig.getYaml().setComment("commands.player-only",
|
||||
"Message that is shown when the console runs a command that is player-only");
|
||||
commands.playersOnly = formatString(messagesConfig.getString("commands.player-only",
|
||||
@@ -928,6 +952,16 @@ public final class SonarConfiguration {
|
||||
"%footer%"
|
||||
))));
|
||||
|
||||
messagesConfig.getYaml().setComment("verification.blacklisted-protocol",
|
||||
"Disconnect message that is shown when someone joins with a blacklisted version");
|
||||
verification.protocolBlacklisted = deserialize(fromList(messagesConfig.getStringList("verification.blacklisted-protocol",
|
||||
Arrays.asList(
|
||||
"%header%",
|
||||
"<red>You are using a version that is not allowed on our server.",
|
||||
"<gold>Need help logging in? <gray>%support-url%",
|
||||
"%footer%"
|
||||
))));
|
||||
|
||||
messagesConfig.getYaml().setComment("verification.already-online",
|
||||
"Disconnect message that is shown when someone tries verifying with an account that is online");
|
||||
verification.alreadyConnected = deserialize(fromList(messagesConfig.getStringList("verification.already-online",
|
||||
@@ -961,28 +995,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%",
|
||||
@@ -990,12 +1025,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("◜", "◝", "◞", "◟")
|
||||
));
|
||||
|
||||
-2
@@ -97,7 +97,6 @@ public final class VerifiedPlayerController {
|
||||
// We cannot throw a NullPointerException within the executor service
|
||||
// because we want to handle the error instead of simply throwing an exception
|
||||
if (connectionSource == null) {
|
||||
Sonar.get().getLogger().error("Error (DB_UPDATE_SERVICE): connectionSource is null");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -144,7 +143,6 @@ public final class VerifiedPlayerController {
|
||||
// We cannot throw a NullPointerException within the executor service
|
||||
// because we want to handle the error instead of simply throwing an exception
|
||||
if (connectionSource == null) {
|
||||
Sonar.get().getLogger().error("Error (DB_UPDATE_SERVICE): connectionSource is null");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -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,39 +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 url = config.getDatabase().getType().getDependency().getClassLoaderURL();
|
||||
final ClassLoader currentClassLoader = DependencyLoader.class.getClassLoader();
|
||||
ClassLoader classLoader = currentClassLoader;
|
||||
|
||||
final Method addPath = currentClassLoader.getClass().getDeclaredMethod("addPath", Path.class);
|
||||
addPath.setAccessible(true);
|
||||
addPath.invoke(currentClassLoader, new File(url.toURI()).toPath());
|
||||
// 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 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 Method addPath = currentClassLoader.getClass().getDeclaredMethod("addPath", Path.class);
|
||||
addPath.setAccessible(true);
|
||||
addPath.invoke(currentClassLoader, new File(url.toURI()).toPath());
|
||||
|
||||
final ExternalClassLoader classLoader = new ExternalClassLoader(new URL[] {url});
|
||||
final Connection connection = connect(classLoader, databaseURL, config.getDatabase());
|
||||
return new JdbcSingleConnectionSource(databaseURL, connection);
|
||||
classLoader = new ExternalClassLoader(new URL[]{url});
|
||||
}
|
||||
|
||||
final String jdbcURL = String.format("jdbc:%s://%s:%d/%s",
|
||||
database.getType().name().toLowerCase(), database.getUrl(), database.getPort(), database.getName());
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.ToString;
|
||||
import xyz.jonesdev.sonar.api.attack.AttackStatus;
|
||||
import xyz.jonesdev.sonar.api.attack.AttackTracker;
|
||||
import xyz.jonesdev.sonar.api.event.SonarEvent;
|
||||
|
||||
@Getter
|
||||
@@ -29,5 +29,5 @@ import xyz.jonesdev.sonar.api.event.SonarEvent;
|
||||
@EqualsAndHashCode
|
||||
@RequiredArgsConstructor
|
||||
public final class AttackMitigatedEvent implements SonarEvent {
|
||||
private final AttackStatus.AttackStatistics attackStatistics;
|
||||
private final AttackTracker.AttackStatistics attackStatistics;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -38,11 +40,10 @@ public final class Fallback {
|
||||
public static final Fallback INSTANCE = new Fallback();
|
||||
private static final @NotNull Sonar SONAR = Objects.requireNonNull(Sonar.get());
|
||||
|
||||
private final Map<String, InetAddress> connected = new ConcurrentHashMap<>();
|
||||
private final Map<String, InetAddress> connected = new ConcurrentHashMap<>(64, 0.75f);
|
||||
// Only block the player for a few minutes to avoid issues
|
||||
private final ExpiringCache<String> blacklisted = Cappuccino.buildExpiring(
|
||||
10L, TimeUnit.MINUTES, 5000L
|
||||
);
|
||||
private final ExpiringCache<InetAddress> blacklisted = Cappuccino.buildExpiring(
|
||||
10L, TimeUnit.MINUTES, 5000L);
|
||||
private final @NotNull FallbackQueue queue = FallbackQueue.INSTANCE;
|
||||
private final @NotNull FallbackRatelimiter ratelimiter = FallbackRatelimiter.INSTANCE;
|
||||
|
||||
@@ -63,4 +64,12 @@ public final class Fallback {
|
||||
SONAR.getLogger().error("[fallback] " + message, args);
|
||||
}
|
||||
};
|
||||
|
||||
@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());
|
||||
}
|
||||
}
|
||||
@@ -68,6 +68,6 @@ public final class FallbackQueue {
|
||||
}
|
||||
|
||||
// Run the attack check task
|
||||
Sonar.get().getAttackStatus().checkIfUnderAttack();
|
||||
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);
|
||||
|
||||
@@ -115,7 +115,7 @@ public interface FallbackUser<X, Y> {
|
||||
// Call the BotBlacklistedEvent for external API usage
|
||||
Sonar.get().getEventManager().publish(new UserBlacklistedEvent(this));
|
||||
|
||||
getFallback().getBlacklisted().put(getInetAddress().toString());
|
||||
getFallback().getBlacklisted().put(getInetAddress());
|
||||
getFallback().getLogger().info(Sonar.get().getConfig().getVerification().getBlacklistLog()
|
||||
.replace("%ip%", Sonar.get().getConfig().formatAddress(getInetAddress()))
|
||||
.replace("%protocol%", String.valueOf(getProtocolVersion().getProtocol())));
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+51
-43
@@ -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;
|
||||
@@ -72,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;
|
||||
@@ -113,15 +115,16 @@ 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;
|
||||
// Cache protocol version so other handlers don't throw NPEs
|
||||
protocolVersion = ProtocolVersion.fromId(getHandshake().getProtocolVersion());
|
||||
final int protocolId = getHandshake().getProtocolVersion();
|
||||
protocolVersion = ProtocolVersion.fromId(protocolId);
|
||||
final Channel channel = channelWrapper.getHandle();
|
||||
|
||||
// Run in the channel's event loop
|
||||
@@ -133,23 +136,16 @@ public final class FallbackInitialHandler extends InitialHandler {
|
||||
// Increase total traffic statistic
|
||||
Statistics.TOTAL_TRAFFIC.increment();
|
||||
|
||||
// Check if the verification is enabled
|
||||
if (!Sonar.get().getConfig().getVerification().isEnabled()) {
|
||||
super.handle(loginRequest);
|
||||
return;
|
||||
}
|
||||
|
||||
final InetAddress inetAddress = getAddress().getAddress();
|
||||
val uuid = UUID.nameUUIDFromBytes(("OfflinePlayer:" + loginRequest.getData()).getBytes(StandardCharsets.UTF_8));
|
||||
// Check if the player is already verified
|
||||
if (Sonar.get().getVerifiedPlayerController().has(inetAddress, uuid)) {
|
||||
super.handle(loginRequest);
|
||||
// Check the blacklist here since we cannot let the player "ghost join"
|
||||
if (FALLBACK.getBlacklisted().has(inetAddress)) {
|
||||
closeWith(getKickPacket(Sonar.get().getConfig().getVerification().getBlacklisted()));
|
||||
return;
|
||||
}
|
||||
|
||||
// Check the blacklist here since we cannot let the player "ghost join"
|
||||
if (FALLBACK.getBlacklisted().has(inetAddress.toString())) {
|
||||
closeWith(getKickPacket(Sonar.get().getConfig().getVerification().getBlacklisted()));
|
||||
// Check if the verification is enabled
|
||||
if (!Sonar.get().getFallback().shouldVerifyNewPlayers()) {
|
||||
super.handle(loginRequest);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -161,15 +157,34 @@ public final class FallbackInitialHandler extends InitialHandler {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the protocol ID of the player is not allowed to enter the server
|
||||
if (Sonar.get().getConfig().getVerification().getBlacklistedProtocols().contains(protocolId)) {
|
||||
closeWith(getKickPacket(Sonar.get().getConfig().getVerification().getProtocolBlacklisted()));
|
||||
return;
|
||||
}
|
||||
|
||||
val uuid = UUID.nameUUIDFromBytes(("OfflinePlayer:" + loginRequest.getData()).getBytes(StandardCharsets.UTF_8));
|
||||
// Check if the player is already verified
|
||||
if (Sonar.get().getVerifiedPlayerController().has(inetAddress, uuid)) {
|
||||
super.handle(loginRequest);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the protocol ID of the player is allowed to bypass verification
|
||||
if (Sonar.get().getConfig().getVerification().getWhitelistedProtocols().contains(protocolId)) {
|
||||
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"));
|
||||
@@ -179,7 +194,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;
|
||||
@@ -200,13 +215,6 @@ public final class FallbackInitialHandler extends InitialHandler {
|
||||
return;
|
||||
}
|
||||
|
||||
// We cannot allow too many players on our Fallback server
|
||||
// There's technically no reason for limiting this, but we'll better stay safe.
|
||||
if (FALLBACK.getConnected().size() > Sonar.get().getConfig().getVerification().getMaxVerifyingPlayers()) {
|
||||
closeWith(getKickPacket(Sonar.get().getConfig().getVerification().getTooManyPlayers()));
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the IP address is currently being rate-limited
|
||||
if (!FALLBACK.getRatelimiter().attempt(inetAddress)) {
|
||||
closeWith(getKickPacket(Sonar.get().getConfig().getVerification().getTooFastReconnect()));
|
||||
@@ -240,7 +248,7 @@ public final class FallbackInitialHandler extends InitialHandler {
|
||||
);
|
||||
|
||||
// Disconnect if the protocol version could not be resolved
|
||||
if (player.getProtocolVersion().isUnknown()) {
|
||||
if (user.getProtocolVersion().isUnknown()) {
|
||||
closeWith(getKickPacket(Sonar.get().getConfig().getVerification().getInvalidProtocol()));
|
||||
return;
|
||||
}
|
||||
@@ -261,44 +269,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 (!Sonar.get().getAttackStatus().isCurrentlyUnderAttack()
|
||||
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,11 +317,11 @@ public final class FallbackInitialHandler extends InitialHandler {
|
||||
|
||||
private static final Map<Component, Kick> CACHED_KICK_PACKETS = new ConcurrentHashMap<>(16);
|
||||
|
||||
private static @NotNull Kick getKickPacket(final @NotNull Component component) {
|
||||
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(serialized);
|
||||
cachedKickPacket = new Kick(ComponentSerializer.deserialize(serialized));
|
||||
CACHED_KICK_PACKETS.put(component, cachedKickPacket);
|
||||
}
|
||||
return cachedKickPacket;
|
||||
|
||||
@@ -21,13 +21,11 @@ import lombok.RequiredArgsConstructor;
|
||||
import net.kyori.adventure.audience.Audience;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.minimessage.MiniMessage;
|
||||
import net.kyori.adventure.text.serializer.json.JSONComponentSerializer;
|
||||
import net.md_5.bungee.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;
|
||||
@@ -57,8 +55,7 @@ public final class FallbackListener implements Listener {
|
||||
if (onlinePerIp >= maxOnlinePerIp) {
|
||||
final FallbackInitialHandler fallbackInitialHandler = (FallbackInitialHandler) event.getConnection();
|
||||
final Component component = Sonar.get().getConfig().getTooManyOnlinePerIp();
|
||||
final String serialized = JSONComponentSerializer.json().serialize(component);
|
||||
fallbackInitialHandler.closeWith(new Kick(serialized));
|
||||
fallbackInitialHandler.closeWith(FallbackInitialHandler.getKickPacket(component));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -73,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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+26
-42
@@ -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
|
||||
}
|
||||
@@ -91,7 +91,7 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
|
||||
// Set the state to CONFIGURE to avoid false positives
|
||||
state = State.CONFIGURE;
|
||||
// Send the necessary configuration packets to the client
|
||||
user.delayedWrite(synchronizeRegistry);
|
||||
user.delayedWrite(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,12 +327,6 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: implement ChunkData packet for 1.20.2 and fix block state Ids for barriers
|
||||
if (user.getProtocolVersion().compareTo(MINECRAFT_1_20_2) >= 0) {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
// First, send an Abilities packet to the client to make
|
||||
// sure the player falls even in spectator mode.
|
||||
// Then, teleport the player to the spawn position.
|
||||
@@ -405,11 +397,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 +421,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");
|
||||
}
|
||||
|
||||
+6
-3
@@ -29,8 +29,7 @@ import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
|
||||
import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.writeArray;
|
||||
import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.writeCompoundTag;
|
||||
import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.*;
|
||||
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.writeVarInt;
|
||||
|
||||
@Getter
|
||||
@@ -92,7 +91,11 @@ public final class EmptyChunkData implements FallbackPacket {
|
||||
}
|
||||
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_14) >= 0) {
|
||||
writeCompoundTag(byteBuf, protocolVersion.compareTo(MINECRAFT_1_18) < 0 ? LEGACY_TAG : MODERN_TAG);
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_20_2) >= 0) {
|
||||
writeNamelessCompoundTag(byteBuf, MODERN_TAG);
|
||||
} else {
|
||||
writeCompoundTag(byteBuf, protocolVersion.compareTo(MINECRAFT_1_18) < 0 ? LEGACY_TAG : MODERN_TAG);
|
||||
}
|
||||
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_15) >= 0 && protocolVersion.compareTo(MINECRAFT_1_18) < 0) {
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_16_2) >= 0) {
|
||||
|
||||
+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();
|
||||
}
|
||||
}
|
||||
+4
-4
@@ -54,7 +54,7 @@ public final class BlacklistCommand extends Subcommand {
|
||||
if (inetAddress == null) return;
|
||||
|
||||
// Make sure the IP is not blacklisted already
|
||||
if (SONAR.getFallback().getBlacklisted().has(inetAddress.toString())) {
|
||||
if (SONAR.getFallback().getBlacklisted().has(inetAddress)) {
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getBlacklistDuplicate());
|
||||
return;
|
||||
}
|
||||
@@ -66,7 +66,7 @@ public final class BlacklistCommand extends Subcommand {
|
||||
}
|
||||
|
||||
// Blacklist the given IP address
|
||||
SONAR.getFallback().getBlacklisted().put(inetAddress.toString());
|
||||
SONAR.getFallback().getBlacklisted().put(inetAddress);
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getBlacklistAdd()
|
||||
.replace("%ip%", rawInetAddress));
|
||||
break;
|
||||
@@ -85,13 +85,13 @@ public final class BlacklistCommand extends Subcommand {
|
||||
if (inetAddress == null) return;
|
||||
|
||||
// Make sure the IP is blacklisted
|
||||
if (!SONAR.getFallback().getBlacklisted().has(inetAddress.toString())) {
|
||||
if (!SONAR.getFallback().getBlacklisted().has(inetAddress)) {
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getBlacklistNotFound());
|
||||
return;
|
||||
}
|
||||
|
||||
// Invalidate the cache entry of the blacklisted IP address
|
||||
SONAR.getFallback().getBlacklisted().invalidate(inetAddress.toString());
|
||||
SONAR.getFallback().getBlacklisted().invalidate(inetAddress);
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getBlacklistRemove()
|
||||
.replace("%ip%", rawInetAddress));
|
||||
break;
|
||||
|
||||
+1
-1
@@ -32,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",
|
||||
|
||||
@@ -42,6 +42,6 @@ public class GeyserUtil {
|
||||
public boolean isGeyserConnection(final @NotNull Channel channel) {
|
||||
// https://discord.com/channels/613163671870242838/613170125696270357/1168599123889504266
|
||||
// Every floodgate player has a channel attribute called "floodgate-player"
|
||||
return channel.hasAttr(playerAttribute());
|
||||
return channel.attr(playerAttribute()).get() != null;
|
||||
}
|
||||
}
|
||||
+12
-1
@@ -25,6 +25,7 @@ import io.netty.handler.codec.DecoderException;
|
||||
import io.netty.handler.codec.EncoderException;
|
||||
import lombok.experimental.UtilityClass;
|
||||
import net.kyori.adventure.nbt.BinaryTagIO;
|
||||
import net.kyori.adventure.nbt.BinaryTagTypes;
|
||||
import net.kyori.adventure.nbt.CompoundBinaryTag;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
@@ -113,7 +114,17 @@ public class ProtocolUtil {
|
||||
public static void writeCompoundTag(final ByteBuf byteBuf, final CompoundBinaryTag compoundTag) {
|
||||
try {
|
||||
BinaryTagIO.writer().write(compoundTag, (DataOutput) new ByteBufOutputStream(byteBuf));
|
||||
} catch (IOException e) {
|
||||
} catch (IOException exception) {
|
||||
throw new EncoderException("Unable to encode NBT CompoundTag");
|
||||
}
|
||||
}
|
||||
|
||||
public static void writeNamelessCompoundTag(final @NotNull ByteBuf byteBuf, final CompoundBinaryTag compoundTag) {
|
||||
try {
|
||||
final DataOutput output = new ByteBufOutputStream(byteBuf);
|
||||
output.writeByte(BinaryTagTypes.COMPOUND.id());
|
||||
BinaryTagTypes.COMPOUND.write(compoundTag, output);
|
||||
} catch (IOException exception) {
|
||||
throw new EncoderException("Unable to encode NBT CompoundTag");
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
+47
-44
@@ -95,8 +95,7 @@ public final class FallbackListener {
|
||||
InetSocketAddress.class,
|
||||
boolean.class,
|
||||
IdentifiedKey.class
|
||||
)
|
||||
);
|
||||
));
|
||||
|
||||
try {
|
||||
SESSION_HANDLER_FIELD = MinecraftConnection.class.getDeclaredField("activeSessionHandler");
|
||||
@@ -112,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);
|
||||
}
|
||||
@@ -129,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);
|
||||
@@ -157,7 +155,8 @@ public final class FallbackListener {
|
||||
val activeSessionHandler = (MinecraftSessionHandler) SESSION_HANDLER_FIELD.get(mcConnection);
|
||||
|
||||
// Check the blacklist here since we cannot let the player "ghost join"
|
||||
if (fallback.getBlacklisted().has(inetAddress.toString())) {
|
||||
if (fallback.getBlacklisted().has(inetAddress)) {
|
||||
// Mark the connection as dead to avoid unnecessary console logs
|
||||
markConnectionAsDead(activeSessionHandler);
|
||||
initialConnection.getConnection().closeWith(Disconnect.create(
|
||||
Sonar.get().getConfig().getVerification().getBlacklisted(),
|
||||
@@ -167,22 +166,39 @@ 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;
|
||||
|
||||
// Completely skip Geyser connections
|
||||
if (isGeyserConnection(channel)) {
|
||||
fallback.getLogger().info("Skipping Geyser player: {}{}",
|
||||
event.getUsername(), Sonar.get().getConfig().formatAddress(inetAddress));
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the protocol ID of the player is not allowed to enter the server
|
||||
final int protocolId = inboundConnection.getProtocolVersion().getProtocol();
|
||||
if (Sonar.get().getConfig().getVerification().getBlacklistedProtocols().contains(protocolId)) {
|
||||
// Mark the connection as dead to avoid unnecessary console logs
|
||||
markConnectionAsDead(activeSessionHandler);
|
||||
initialConnection.getConnection().closeWith(Disconnect.create(
|
||||
Sonar.get().getConfig().getVerification().getProtocolBlacklisted(),
|
||||
inboundConnection.getProtocolVersion()
|
||||
));
|
||||
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
|
||||
if (isGeyserConnection(channel)) {
|
||||
fallback.getLogger().info("Skipping Geyser player: {}{}",
|
||||
gameProfile.getName(), Sonar.get().getConfig().formatAddress(inetAddress));
|
||||
return;
|
||||
}
|
||||
// Check if the protocol ID of the player is allowed to bypass verification
|
||||
if (Sonar.get().getConfig().getVerification().getWhitelistedProtocols().contains(protocolId)) 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)) {
|
||||
@@ -204,16 +220,6 @@ public final class FallbackListener {
|
||||
return;
|
||||
}
|
||||
|
||||
// We cannot allow too many players on our Fallback server
|
||||
// There's technically no reason for limiting this, but we'll better stay safe.
|
||||
if (fallback.getConnected().size() > Sonar.get().getConfig().getVerification().getMaxVerifyingPlayers()) {
|
||||
initialConnection.getConnection().closeWith(Disconnect.create(
|
||||
Sonar.get().getConfig().getVerification().getTooManyPlayers(),
|
||||
inboundConnection.getProtocolVersion()
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the IP address is currently being rate-limited
|
||||
if (!fallback.getRatelimiter().attempt(inetAddress)) {
|
||||
initialConnection.getConnection().closeWith(Disconnect.create(
|
||||
@@ -256,9 +262,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 {
|
||||
@@ -278,21 +281,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())
|
||||
ProtocolVersion.fromId(protocolId)
|
||||
);
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
@@ -302,44 +305,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 (!Sonar.get().getAttackStatus().isCurrentlyUnderAttack()
|
||||
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) {
|
||||
|
||||
Reference in new issue
Block a user