mirror of
https://github.com/jonesdevelopment/sonar.git
synced 2026-11-05 09:20:15 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
595b1378ab | ||
|
|
3012ed0d7a | ||
|
|
4ccecde901 | ||
|
|
87fcfb68f3 | ||
|
|
a17b726798 | ||
|
|
50d3b43f63 | ||
|
|
0ff0194894 | ||
|
|
279fe2e3e9 | ||
|
|
ffd3a7e5c2 | ||
|
|
4474f44b0a | ||
|
|
b159947d6d | ||
|
|
a29dfa15e4 | ||
|
|
cb9fe75db8 |
No files matched your search
+1
-1
@@ -41,7 +41,7 @@ allprojects {
|
||||
|
||||
compileOnly("com.j256.ormlite:ormlite-jdbc:6.1") // ORMLite
|
||||
compileOnly("com.github.ben-manes.caffeine:caffeine:3.1.8") // caching
|
||||
compileOnly("io.netty:netty-all:4.1.108.Final") // netty
|
||||
compileOnly("io.netty:netty-all:4.1.109.Final") // netty
|
||||
|
||||
// Library/dependency loading
|
||||
compileOnly("com.alessiodp.libby:libby-core:2.0.0-SNAPSHOT")
|
||||
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
group=xyz.jonesdev.sonar
|
||||
version=2.0.25
|
||||
version=2.0.28
|
||||
description=Effective Anti-bot plugin for Velocity, BungeeCord, and Bukkit (1.7-latest)
|
||||
@@ -34,6 +34,7 @@ import xyz.jonesdev.sonar.api.verbose.Notification;
|
||||
import xyz.jonesdev.sonar.api.verbose.Verbose;
|
||||
|
||||
import java.io.File;
|
||||
import java.net.InetAddress;
|
||||
import java.text.DecimalFormat;
|
||||
import java.util.UUID;
|
||||
|
||||
@@ -52,6 +53,11 @@ public interface Sonar {
|
||||
*/
|
||||
@NotNull SonarPlatform getPlatform();
|
||||
|
||||
/**
|
||||
* @return If the amount of players online with the same IP address is greater than a maximum
|
||||
*/
|
||||
boolean hasTooManyAccounts(final @NotNull InetAddress inetAddress, final int limit);
|
||||
|
||||
/**
|
||||
* @return A small wrapper for the plugin logger so we can use the logger everywhere
|
||||
*/
|
||||
|
||||
@@ -151,10 +151,4 @@ public final class AttackTracker {
|
||||
attackThreshold = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
|
||||
public boolean isCurrentlyUnderAttack() {
|
||||
final int minDelay = Sonar.get().getConfig().getAttackCooldownDelay();
|
||||
return currentAttack != null && currentAttack.timer.delay() < minDelay;
|
||||
}
|
||||
}
|
||||
@@ -53,13 +53,8 @@ public abstract class Subcommand {
|
||||
final InetAddress inetAddress;
|
||||
try {
|
||||
inetAddress = InetAddress.getByName(rawIP);
|
||||
|
||||
if (inetAddress.isAnyLocalAddress() || inetAddress.isLoopbackAddress()) {
|
||||
source.sendMessage(SONAR.getConfig().getCommands().getIllegalIpAddress());
|
||||
return null;
|
||||
}
|
||||
} catch (UnknownHostException exception) {
|
||||
source.sendMessage(SONAR.getConfig().getCommands().getIncorrectIpAddress());
|
||||
source.sendMessage(SONAR.getConfig().getCommands().getInvalidIpAddress());
|
||||
return null;
|
||||
}
|
||||
return inetAddress;
|
||||
|
||||
@@ -21,6 +21,7 @@ import com.j256.ormlite.db.DatabaseType;
|
||||
import lombok.*;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.minimessage.MiniMessage;
|
||||
import net.kyori.adventure.title.Title;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
@@ -204,6 +205,15 @@ public final class SonarConfiguration {
|
||||
verification.validLocaleRegex = Pattern.compile(generalConfig.getString("verification.checks.valid-locale-regex"));
|
||||
verification.maxLoginPackets = clamp(generalConfig.getInt("verification.checks.max-login-packets"), 128, 8192);
|
||||
|
||||
verification.transfer.enabled = generalConfig.getBoolean("verification.transfer.enabled");
|
||||
verification.transfer.host = generalConfig.getString("verification.transfer.destination-host");
|
||||
verification.transfer.port = clamp(generalConfig.getInt("verification.transfer.destination-port"), 0, 0xffff);
|
||||
|
||||
if (verification.transfer.enabled) {
|
||||
Sonar.get().getLogger().info("Transferring 1.20.5+ clients is enabled. Please make sure to follow the instructions in order for this to work properly.");
|
||||
}
|
||||
|
||||
verification.checkGeyser = generalConfig.getBoolean("verification.check-geyser-players");
|
||||
verification.logConnections = generalConfig.getBoolean("verification.log-connections");
|
||||
verification.logDuringAttack = generalConfig.getBoolean("verification.log-during-attack");
|
||||
verification.debugXYZPositions = generalConfig.getBoolean("verification.debug-xyz-positions");
|
||||
@@ -256,8 +266,7 @@ public final class SonarConfiguration {
|
||||
|
||||
noPermission = formatString(messagesConfig.getString("commands.no-permission"));
|
||||
commands.incorrectCommandUsage = formatString(messagesConfig.getString("commands.incorrect-usage"));
|
||||
commands.incorrectIpAddress = formatString(messagesConfig.getString("commands.invalid-ip-address"));
|
||||
commands.illegalIpAddress = formatString(messagesConfig.getString("commands.illegal-ip-address"));
|
||||
commands.invalidIpAddress = formatString(messagesConfig.getString("commands.invalid-ip-address"));
|
||||
commands.playersOnly = formatString(messagesConfig.getString("commands.player-only"));
|
||||
commands.consoleOnly = formatString(messagesConfig.getString("commands.console-only"));
|
||||
commands.commandCoolDown = formatString(messagesConfig.getString("commands.cool-down"));
|
||||
@@ -290,6 +299,8 @@ public final class SonarConfiguration {
|
||||
commands.verifiedCleared = formatString(messagesConfig.getString("commands.verified.cleared"));
|
||||
commands.verifiedSize = formatString(messagesConfig.getString("commands.verified.size"));
|
||||
commands.verifiedRemove = formatString(messagesConfig.getString("commands.verified.removed"));
|
||||
commands.verifiedAdd = formatString(messagesConfig.getString("commands.verified.added"));
|
||||
commands.verifiedAlready = formatString(messagesConfig.getString("commands.verified.already"));
|
||||
commands.verifiedNotFound = formatString(messagesConfig.getString("commands.verified.ip-not-found"));
|
||||
commands.verifiedBlocked = formatString(messagesConfig.getString("commands.verified.blocked"));
|
||||
|
||||
@@ -318,13 +329,16 @@ public final class SonarConfiguration {
|
||||
verification.verificationSuccess = deserialize(fromList(messagesConfig.getStringList("verification.success")));
|
||||
verification.verificationFailed = deserialize(fromList(messagesConfig.getStringList("verification.failed")));
|
||||
|
||||
verbose.actionBarLayout = formatString(messagesConfig.getString("verbose.layout.normal"));
|
||||
verbose.actionBarLayoutDuringAttack = formatString(messagesConfig.getString("verbose.layout.attack"));
|
||||
verbose.actionBarLayout = deserialize(formatString(messagesConfig.getString("verbose.layout.normal")));
|
||||
verbose.actionBarLayoutDuringAttack = deserialize(formatString(messagesConfig.getString("verbose.layout.attack")));
|
||||
verbose.animation = Collections.unmodifiableList(messagesConfig.getStringList("verbose.animation"));
|
||||
|
||||
notifications.notificationTitle = deserialize(formatString(messagesConfig.getString("notifications.title")));
|
||||
notifications.notificationSubtitle = deserialize(formatString(messagesConfig.getString("notifications.subtitle")));
|
||||
notifications.notificationChat = formatString(fromList(messagesConfig.getStringList("notifications.chat")));
|
||||
notifications.title = Title.title(
|
||||
Sonar.get().getConfig().getNotifications().getNotificationTitle(),
|
||||
Sonar.get().getConfig().getNotifications().getNotificationSubtitle());
|
||||
notifications.notificationChat = deserialize(formatString(fromList(messagesConfig.getStringList("notifications.chat"))));
|
||||
}
|
||||
|
||||
private @NotNull URL getAsset(final String url, final @NotNull Language language) {
|
||||
@@ -399,17 +413,18 @@ public final class SonarConfiguration {
|
||||
@Getter
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public static final class Verbose {
|
||||
private String actionBarLayout;
|
||||
private String actionBarLayoutDuringAttack;
|
||||
private Component actionBarLayout;
|
||||
private Component actionBarLayoutDuringAttack;
|
||||
private List<String> animation;
|
||||
}
|
||||
|
||||
@Getter
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public static final class Notifications {
|
||||
private Title title;
|
||||
private Component notificationTitle;
|
||||
private Component notificationSubtitle;
|
||||
private String notificationChat;
|
||||
private Component notificationChat;
|
||||
}
|
||||
|
||||
@Getter
|
||||
@@ -437,6 +452,7 @@ public final class SonarConfiguration {
|
||||
private final Gravity gravity = new Gravity();
|
||||
private final Vehicle vehicle = new Vehicle();
|
||||
private final Brand brand = new Brand();
|
||||
private final Transfer transfer = new Transfer();
|
||||
|
||||
@Getter
|
||||
public static final class Map {
|
||||
@@ -491,6 +507,14 @@ public final class SonarConfiguration {
|
||||
private Pattern validRegex;
|
||||
}
|
||||
|
||||
@Getter
|
||||
public static final class Transfer {
|
||||
private boolean enabled;
|
||||
private String host;
|
||||
private int port;
|
||||
}
|
||||
|
||||
private boolean checkGeyser;
|
||||
private boolean logConnections;
|
||||
private boolean logDuringAttack;
|
||||
private boolean debugXYZPositions;
|
||||
@@ -525,8 +549,7 @@ public final class SonarConfiguration {
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public static final class Commands {
|
||||
private String incorrectCommandUsage;
|
||||
private String incorrectIpAddress;
|
||||
private String illegalIpAddress;
|
||||
private String invalidIpAddress;
|
||||
private String subCommandNoPerm;
|
||||
private String playersOnly;
|
||||
private String consoleOnly;
|
||||
@@ -543,6 +566,8 @@ public final class SonarConfiguration {
|
||||
private String blacklistSize;
|
||||
|
||||
private String verifiedRemove;
|
||||
private String verifiedAdd;
|
||||
private String verifiedAlready;
|
||||
private String verifiedNotFound;
|
||||
private String verifiedCleared;
|
||||
private String verifiedSize;
|
||||
|
||||
-9
@@ -310,13 +310,4 @@ public final class VerifiedPlayerController {
|
||||
public boolean has(final @NotNull String inetAddress) {
|
||||
return cache.containsKey(inetAddress);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param inetAddress IP address
|
||||
* @return Whether the local cache contains the IP
|
||||
* @see #has(String)
|
||||
*/
|
||||
public boolean has(final @NotNull InetAddress inetAddress) {
|
||||
return has(inetAddress.toString());
|
||||
}
|
||||
}
|
||||
@@ -83,6 +83,6 @@ public final class Fallback {
|
||||
private static boolean shouldPerform(final SonarConfiguration.Verification.Timing timing) {
|
||||
return timing == SonarConfiguration.Verification.Timing.ALWAYS
|
||||
|| (timing == SonarConfiguration.Verification.Timing.DURING_ATTACK
|
||||
&& Sonar.get().getAttackTracker().isCurrentlyUnderAttack());
|
||||
&& Sonar.get().getAttackTracker().getCurrentAttack() != null);
|
||||
}
|
||||
}
|
||||
@@ -26,15 +26,20 @@ import java.net.InetAddress;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.ForkJoinPool;
|
||||
|
||||
@Getter
|
||||
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public final class FallbackQueue {
|
||||
static final FallbackQueue INSTANCE = new FallbackQueue();
|
||||
private final Map<InetAddress, Runnable> queuedPlayers = new ConcurrentHashMap<>(64);
|
||||
// Async executor for all new verifications
|
||||
public static final ExecutorService QUEUE_EXECUTOR = new ForkJoinPool(
|
||||
Math.min(0x7fff, Runtime.getRuntime().availableProcessors()),
|
||||
ForkJoinPool.defaultForkJoinWorkerThreadFactory, null, true);
|
||||
|
||||
// Runnable task executed for polling the queue
|
||||
private final Runnable pollTask = () -> {
|
||||
public void poll() {
|
||||
final int maxQueuePolls = Sonar.get().getConfig().getQueue().getMaxQueuePolls();
|
||||
int index = 0;
|
||||
|
||||
@@ -47,30 +52,9 @@ public final class FallbackQueue {
|
||||
}
|
||||
// Run the cached runnable
|
||||
final Map.Entry<InetAddress, Runnable> entry = iterator.next();
|
||||
entry.getValue().run();
|
||||
QUEUE_EXECUTOR.execute(entry.getValue());
|
||||
// Remove runnable from iterator
|
||||
iterator.remove();
|
||||
}
|
||||
|
||||
// Run the attack check task
|
||||
Sonar.get().getAttackTracker().checkIfUnderAttack();
|
||||
// Clean up the cache of rate-limited IPs
|
||||
Sonar.get().getFallback().getRatelimiter().cleanUpCache();
|
||||
};
|
||||
|
||||
/**
|
||||
* @param inetAddress IP address of the player
|
||||
* @param runnable Queued action on the netty thread
|
||||
* @see #remove
|
||||
*/
|
||||
public void queue(final InetAddress inetAddress, final Runnable runnable) {
|
||||
queuedPlayers.put(inetAddress, runnable);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param inetAddress IP address of the player
|
||||
*/
|
||||
public void remove(final InetAddress inetAddress) {
|
||||
queuedPlayers.remove(inetAddress);
|
||||
}
|
||||
}
|
||||
@@ -28,13 +28,19 @@ import java.net.InetAddress;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface FallbackUser {
|
||||
@NotNull Channel getChannel();
|
||||
@NotNull
|
||||
Channel getChannel();
|
||||
|
||||
@NotNull InetAddress getInetAddress();
|
||||
@NotNull
|
||||
InetAddress getInetAddress();
|
||||
|
||||
@NotNull ProtocolVersion getProtocolVersion();
|
||||
@NotNull
|
||||
ProtocolVersion getProtocolVersion();
|
||||
|
||||
@NotNull FallbackUserState getState();
|
||||
@NotNull
|
||||
FallbackUserState getState();
|
||||
|
||||
boolean isGeyser();
|
||||
|
||||
void setState(final FallbackUserState state);
|
||||
|
||||
@@ -55,11 +61,11 @@ public interface FallbackUser {
|
||||
* @param encoder Name of the encoder pipeline
|
||||
* @param decoder Name of the decoder pipeline
|
||||
* @param timeout Name of the read timeout pipeline
|
||||
* @param boss Name of the main pipeline
|
||||
* @param handler Name of the main pipeline
|
||||
*/
|
||||
void hijack(final @NotNull String username, final @NotNull UUID uuid,
|
||||
final @NotNull String encoder, final @NotNull String decoder,
|
||||
final @NotNull String timeout, final @NotNull String boss);
|
||||
final @NotNull String timeout, final @NotNull String handler);
|
||||
|
||||
/**
|
||||
* Sends a packet/message to the player
|
||||
|
||||
+2
-1
@@ -68,7 +68,8 @@ public enum ProtocolVersion {
|
||||
MINECRAFT_1_19_4(762),
|
||||
MINECRAFT_1_20(763),
|
||||
MINECRAFT_1_20_2(764),
|
||||
MINECRAFT_1_20_3(765);
|
||||
MINECRAFT_1_20_3(765),
|
||||
MINECRAFT_1_20_5(766);
|
||||
|
||||
private final int protocol;
|
||||
|
||||
|
||||
@@ -21,12 +21,14 @@ import com.sun.management.OperatingSystemMXBean;
|
||||
import lombok.experimental.UtilityClass;
|
||||
|
||||
import java.lang.management.ManagementFactory;
|
||||
import java.text.DecimalFormat;
|
||||
|
||||
@UtilityClass
|
||||
public class JVMProcessInformation {
|
||||
private final Runtime RUNTIME = Runtime.getRuntime();
|
||||
private final OperatingSystemMXBean MX = (OperatingSystemMXBean) ManagementFactory.getOperatingSystemMXBean();
|
||||
private final char[] MEMORY_UNITS = {'K', 'M', 'G', 'T', 'P', 'E'};
|
||||
private final DecimalFormat DECIMAL_FORMAT = new DecimalFormat("#.#");
|
||||
|
||||
public String formatMemory(final long size) {
|
||||
if (size < 1024L) {
|
||||
@@ -37,7 +39,7 @@ public class JVMProcessInformation {
|
||||
final double formattedSize = (double) size / (1L << (group * 10));
|
||||
final char unit = MEMORY_UNITS[group - 1];
|
||||
// https://en.wikipedia.org/wiki/Byte#Multiple-byte_units
|
||||
return String.format("%.1f %siB", formattedSize, unit);
|
||||
return DECIMAL_FORMAT.format(formattedSize) + " " + unit + "iB";
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -18,13 +18,12 @@
|
||||
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");
|
||||
public static final SimpleDateFormat DATE_FORMATTER = new SimpleDateFormat("mm:ss");
|
||||
|
||||
private long start = System.currentTimeMillis();
|
||||
|
||||
@@ -40,19 +39,12 @@ public final class SystemTimer {
|
||||
}
|
||||
|
||||
/**
|
||||
* @apiNote Read more: {@link SystemTimer#delay}, {@link SystemTimer#formattedDelay}
|
||||
* @apiNote Read more: {@link SystemTimer#delay}
|
||||
*/
|
||||
public boolean elapsed(final long amount) {
|
||||
return delay() >= amount;
|
||||
}
|
||||
|
||||
/**
|
||||
* @apiNote Read more: {@link SystemTimer#delay}
|
||||
*/
|
||||
public @NotNull String formattedDelay() {
|
||||
return FORMATTER.format(delay());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("%.3f", delay() / 1000D);
|
||||
|
||||
@@ -20,57 +20,33 @@ package xyz.jonesdev.sonar.api.verbose;
|
||||
import lombok.Getter;
|
||||
import net.kyori.adventure.audience.Audience;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.minimessage.MiniMessage;
|
||||
import net.kyori.adventure.title.Title;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.statistics.SonarStatistics;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.UUID;
|
||||
import java.util.Vector;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.Sonar.DECIMAL_FORMAT;
|
||||
|
||||
@Getter
|
||||
public final class Notification implements Observable {
|
||||
private final @NotNull Collection<UUID> subscribers = new Vector<>(0);
|
||||
|
||||
public void sendAttackNotification() {
|
||||
// No need to do anything if we have no subscribers
|
||||
if (subscribers.isEmpty()) return;
|
||||
final SonarStatistics statistics = Sonar.get().getStatistics();
|
||||
// Prepare the title
|
||||
// TODO: Why do custom title times not work?
|
||||
// Title.Times.times(Ticks.duration(5L), Ticks.duration(60L), Ticks.duration(10L));
|
||||
final Title title = Title.title(
|
||||
Sonar.get().getConfig().getNotifications().getNotificationTitle(),
|
||||
Sonar.get().getConfig().getNotifications().getNotificationSubtitle());
|
||||
// Prepare the chat message
|
||||
final Component chat = MiniMessage.miniMessage().deserialize(
|
||||
Sonar.get().getConfig().getNotifications().getNotificationChat()
|
||||
.replace("%queued%",
|
||||
DECIMAL_FORMAT.format(Sonar.get().getFallback().getQueue().getQueuedPlayers().size()))
|
||||
.replace("%verifying%", DECIMAL_FORMAT.format(Sonar.get().getFallback().getConnected().size()))
|
||||
.replace("%blacklisted%",
|
||||
DECIMAL_FORMAT.format(Sonar.get().getFallback().getBlacklist().estimatedSize()))
|
||||
.replace("%total-joins%", DECIMAL_FORMAT.format(statistics.getTotalPlayersJoined()))
|
||||
.replace("%logins-per-second%", DECIMAL_FORMAT.format(statistics.getLoginsPerSecond()))
|
||||
.replace("%connections-per-second%", DECIMAL_FORMAT.format(statistics.getConnectionsPerSecond()))
|
||||
.replace("%verify-total%", DECIMAL_FORMAT.format(statistics.getTotalAttemptedVerifications()))
|
||||
.replace("%verify-success%",
|
||||
DECIMAL_FORMAT.format(Sonar.get().getVerifiedPlayerController().estimatedSize()))
|
||||
.replace("%verify-failed%", DECIMAL_FORMAT.format(statistics.getTotalFailedVerifications()))
|
||||
.replace("%incoming-traffic%", statistics.getCurrentIncomingBandwidthFormatted())
|
||||
.replace("%outgoing-traffic%", statistics.getCurrentOutgoingBandwidthFormatted())
|
||||
.replace("%incoming-traffic-ttl%", statistics.getTotalIncomingBandwidthFormatted())
|
||||
.replace("%outgoing-traffic-ttl%", statistics.getTotalOutgoingBandwidthFormatted()));
|
||||
// Don't prepare component if there are no subscribers
|
||||
Component chat = null;
|
||||
// Send the title and chat messages to all online players
|
||||
for (final UUID subscriber : Sonar.get().getNotificationHandler().getSubscribers()) {
|
||||
final Audience audience = Sonar.get().audience(subscriber);
|
||||
if (audience == null) continue;
|
||||
audience.showTitle(title);
|
||||
|
||||
// Only prepare component if there are subscribers
|
||||
if (chat == null) {
|
||||
chat = replaceStatistic(Sonar.get().getConfig().getNotifications().getNotificationChat());
|
||||
}
|
||||
// Send the message to all online subscribers
|
||||
audience.sendMessage(chat);
|
||||
// Send the title to all online subscribers
|
||||
audience.showTitle(Sonar.get().getConfig().getNotifications().getTitle());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,11 +17,18 @@
|
||||
|
||||
package xyz.jonesdev.sonar.api.verbose;
|
||||
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.TextReplacementConfig;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.statistics.SonarStatistics;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.UUID;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.Sonar.DECIMAL_FORMAT;
|
||||
import static xyz.jonesdev.sonar.api.jvm.JVMProcessInformation.*;
|
||||
|
||||
public interface Observable {
|
||||
Collection<UUID> getSubscribers();
|
||||
|
||||
@@ -46,4 +53,61 @@ public interface Observable {
|
||||
default void unsubscribe(final UUID uuid) {
|
||||
getSubscribers().remove(uuid);
|
||||
}
|
||||
|
||||
// TODO: Make this a bit prettier and better for performance
|
||||
default @NotNull Component replaceStatistic(final @NotNull Component component) {
|
||||
final SonarStatistics statistics = Sonar.get().getStatistics();
|
||||
return component
|
||||
.replaceText(TextReplacementConfig.builder().once().matchLiteral("%queued%")
|
||||
.replacement(DECIMAL_FORMAT.format(Sonar.get().getFallback().getQueue().getQueuedPlayers().size()))
|
||||
.build())
|
||||
.replaceText(TextReplacementConfig.builder().once().matchLiteral("%verifying%")
|
||||
.replacement(DECIMAL_FORMAT.format(Sonar.get().getFallback().getConnected().size()))
|
||||
.build())
|
||||
.replaceText(TextReplacementConfig.builder().once().matchLiteral("%blacklisted%")
|
||||
.replacement(DECIMAL_FORMAT.format(Sonar.get().getFallback().getBlacklist().estimatedSize()))
|
||||
.build())
|
||||
.replaceText(TextReplacementConfig.builder().once().matchLiteral("%total-joins%")
|
||||
.replacement(DECIMAL_FORMAT.format(statistics.getTotalPlayersJoined()))
|
||||
.build())
|
||||
.replaceText(TextReplacementConfig.builder().once().matchLiteral("%logins-per-second%")
|
||||
.replacement(DECIMAL_FORMAT.format(statistics.getLoginsPerSecond()))
|
||||
.build())
|
||||
.replaceText(TextReplacementConfig.builder().once().matchLiteral("%connections-per-second%")
|
||||
.replacement(DECIMAL_FORMAT.format(statistics.getConnectionsPerSecond()))
|
||||
.build())
|
||||
.replaceText(TextReplacementConfig.builder().once().matchLiteral("%verify-total%")
|
||||
.replacement(DECIMAL_FORMAT.format(statistics.getTotalAttemptedVerifications()))
|
||||
.build())
|
||||
.replaceText(TextReplacementConfig.builder().once().matchLiteral("%verify-success%")
|
||||
.replacement(DECIMAL_FORMAT.format(statistics.getTotalSuccessfulVerifications()))
|
||||
.build())
|
||||
.replaceText(TextReplacementConfig.builder().once().matchLiteral("%verify-failed%")
|
||||
.replacement(DECIMAL_FORMAT.format(statistics.getTotalFailedVerifications()))
|
||||
.build())
|
||||
.replaceText(TextReplacementConfig.builder().once().matchLiteral("%incoming-traffic%")
|
||||
.replacement(statistics.getCurrentIncomingBandwidthFormatted())
|
||||
.build())
|
||||
.replaceText(TextReplacementConfig.builder().once().matchLiteral("%outgoing-traffic%")
|
||||
.replacement(statistics.getCurrentOutgoingBandwidthFormatted())
|
||||
.build())
|
||||
.replaceText(TextReplacementConfig.builder().once().matchLiteral("%incoming-traffic-ttl%")
|
||||
.replacement(statistics.getTotalIncomingBandwidthFormatted())
|
||||
.build())
|
||||
.replaceText(TextReplacementConfig.builder().once().matchLiteral("%outgoing-traffic-ttl%")
|
||||
.replacement(statistics.getTotalOutgoingBandwidthFormatted())
|
||||
.build())
|
||||
.replaceText(TextReplacementConfig.builder().once().matchLiteral("%used-memory%")
|
||||
.replacement(formatMemory(getUsedMemory()))
|
||||
.build())
|
||||
.replaceText(TextReplacementConfig.builder().once().matchLiteral("%free-memory%")
|
||||
.replacement(formatMemory(getFreeMemory()))
|
||||
.build())
|
||||
.replaceText(TextReplacementConfig.builder().once().matchLiteral("%total-memory%")
|
||||
.replacement(formatMemory(getTotalMemory()))
|
||||
.build())
|
||||
.replaceText(TextReplacementConfig.builder().once().matchLiteral("%max-memory%")
|
||||
.replacement(formatMemory(getMaxMemory()))
|
||||
.build());
|
||||
}
|
||||
}
|
||||
@@ -21,20 +21,17 @@ import lombok.Getter;
|
||||
import lombok.val;
|
||||
import net.kyori.adventure.audience.Audience;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.minimessage.MiniMessage;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import net.kyori.adventure.text.TextReplacementConfig;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.attack.AttackTracker;
|
||||
import xyz.jonesdev.sonar.api.statistics.SonarStatistics;
|
||||
import xyz.jonesdev.sonar.api.timer.SystemTimer;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.UUID;
|
||||
import java.util.Vector;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.Sonar.DECIMAL_FORMAT;
|
||||
import static xyz.jonesdev.sonar.api.jvm.JVMProcessInformation.*;
|
||||
import static xyz.jonesdev.sonar.api.timer.SystemTimer.DATE_FORMATTER;
|
||||
|
||||
@Getter
|
||||
public final class Verbose implements Observable {
|
||||
@@ -43,53 +40,31 @@ public final class Verbose implements Observable {
|
||||
|
||||
// Run action bar verbose
|
||||
public void update() {
|
||||
// Make sure to clean up the cached statistics
|
||||
Sonar.get().getStatistics().cleanUpCache();
|
||||
// Don't prepare component if there are no subscribers
|
||||
if (subscribers.isEmpty()) return;
|
||||
// Prepare the action bar format component
|
||||
final Component component = prepareActionBarFormat();
|
||||
// Send the action bar to all online players
|
||||
Component component = null;
|
||||
for (final UUID subscriber : subscribers) {
|
||||
final Audience audience = Sonar.get().audience(subscriber);
|
||||
if (audience == null) continue;
|
||||
|
||||
// Only prepare component if there are subscribers
|
||||
if (component == null) {
|
||||
final AttackTracker.AttackStatistics attackStatistics = Sonar.get().getAttackTracker().getCurrentAttack();
|
||||
final SystemTimer attackDuration = attackStatistics == null ? null : attackStatistics.getDuration();
|
||||
component = replaceStatistic(attackDuration != null
|
||||
? Sonar.get().getConfig().getVerbose().getActionBarLayoutDuringAttack()
|
||||
: Sonar.get().getConfig().getVerbose().getActionBarLayout())
|
||||
.replaceText(TextReplacementConfig.builder().once().matchLiteral("%attack-duration%")
|
||||
.replacement(attackDuration == null ? "00:00" : DATE_FORMATTER.format(attackDuration.delay()))
|
||||
.build())
|
||||
.replaceText(TextReplacementConfig.builder().once().matchLiteral("%animation%")
|
||||
.replacement(nextAnimation())
|
||||
.build());
|
||||
}
|
||||
// Send the action bar to all online subscribers
|
||||
audience.sendActionBar(component);
|
||||
}
|
||||
}
|
||||
|
||||
@ApiStatus.Internal
|
||||
public @NotNull Component prepareActionBarFormat() {
|
||||
final SonarStatistics statistics = Sonar.get().getStatistics();
|
||||
final AttackTracker.AttackStatistics attackStatistics = Sonar.get().getAttackTracker().getCurrentAttack();
|
||||
final SystemTimer attackDuration = attackStatistics == null ? null : attackStatistics.getDuration();
|
||||
return MiniMessage.miniMessage().deserialize((attackDuration != null
|
||||
? 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().getBlacklist().estimatedSize()))
|
||||
.replace("%blacklisted%", DECIMAL_FORMAT.format(statistics.getCurrentBlacklistSize()))
|
||||
.replace("%total-joins%", DECIMAL_FORMAT.format(statistics.getTotalPlayersJoined()))
|
||||
.replace("%logins-per-second%", DECIMAL_FORMAT.format(statistics.getLoginsPerSecond()))
|
||||
.replace("%connections-per-second%", DECIMAL_FORMAT.format(statistics.getConnectionsPerSecond()))
|
||||
.replace("%verify-total%", DECIMAL_FORMAT.format(statistics.getTotalAttemptedVerifications()))
|
||||
.replace("%verify-success%",
|
||||
DECIMAL_FORMAT.format(Sonar.get().getVerifiedPlayerController().estimatedSize()))
|
||||
.replace("%verify-failed%", DECIMAL_FORMAT.format(statistics.getTotalFailedVerifications()))
|
||||
.replace("%attack-duration%", attackDuration == null ? "00:00" : attackDuration.formattedDelay())
|
||||
.replace("%incoming-traffic%", statistics.getCurrentIncomingBandwidthFormatted())
|
||||
.replace("%outgoing-traffic%", statistics.getCurrentOutgoingBandwidthFormatted())
|
||||
.replace("%incoming-traffic-ttl%", statistics.getTotalIncomingBandwidthFormatted())
|
||||
.replace("%outgoing-traffic-ttl%", statistics.getTotalOutgoingBandwidthFormatted())
|
||||
.replace("%used-memory%", formatMemory(getUsedMemory()))
|
||||
.replace("%free-memory%", formatMemory(getFreeMemory()))
|
||||
.replace("%total-memory%", formatMemory(getTotalMemory()))
|
||||
.replace("%max-memory%", formatMemory(getMaxMemory()))
|
||||
.replace("%animation%", nextAnimation()));
|
||||
}
|
||||
|
||||
public String nextAnimation() {
|
||||
val animations = Sonar.get().getConfig().getVerbose().getAnimation();
|
||||
final int nextIndex = ++animationIndex % animations.size();
|
||||
|
||||
@@ -23,15 +23,15 @@ import net.kyori.adventure.audience.Audience;
|
||||
import net.kyori.adventure.platform.bukkit.BukkitAudiences;
|
||||
import org.bstats.bukkit.Metrics;
|
||||
import org.bstats.charts.SimplePie;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.SonarPlatform;
|
||||
import xyz.jonesdev.sonar.api.logger.LoggerWrapper;
|
||||
import xyz.jonesdev.sonar.bukkit.command.BukkitSonarCommand;
|
||||
import xyz.jonesdev.sonar.common.boot.SonarBootstrap;
|
||||
import xyz.jonesdev.sonar.common.statistics.CachedBandwidthStatistics;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
|
||||
@@ -59,6 +59,20 @@ public final class SonarBukkit extends SonarBootstrap<SonarBukkitPlugin> {
|
||||
return bukkitAudiences.player(uniqueId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasTooManyAccounts(final @NotNull InetAddress inetAddress, final int limit) {
|
||||
int count = 1;
|
||||
for (final Player player : getPlugin().getServer().getOnlinePlayers()) {
|
||||
// I don't know why, but Bukkit is *special*
|
||||
if (Objects.isNull(player.getAddress())) continue;
|
||||
// Check if the IP address of the player is equal to the IP trying to connect
|
||||
if (!Objects.equals(player.getAddress().getAddress(), inetAddress)) continue;
|
||||
// Increment count of duplicated accounts
|
||||
if (++count >= limit) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a wrapper for the plugin logger, so we can use it outside
|
||||
* the velocity module.
|
||||
@@ -98,18 +112,6 @@ public final class SonarBukkit extends SonarBootstrap<SonarBukkitPlugin> {
|
||||
|
||||
// Register Sonar command
|
||||
Objects.requireNonNull(getPlugin().getCommand("sonar")).setExecutor(new BukkitSonarCommand());
|
||||
|
||||
// Register traffic service
|
||||
getPlugin().getServer().getScheduler().runTaskTimerAsynchronously(getPlugin(),
|
||||
CachedBandwidthStatistics::reset, 20L, 20L);
|
||||
|
||||
// Register queue service
|
||||
getPlugin().getServer().getScheduler().runTaskTimerAsynchronously(getPlugin(),
|
||||
getFallback().getQueue().getPollTask(), 10L, 10L);
|
||||
|
||||
// Register verbose service
|
||||
getPlugin().getServer().getScheduler().runTaskTimerAsynchronously(getPlugin(),
|
||||
Sonar.get().getVerboseHandler()::update, 4L, 4L);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -21,21 +21,20 @@ import com.alessiodp.libby.BungeeLibraryManager;
|
||||
import lombok.Getter;
|
||||
import net.kyori.adventure.audience.Audience;
|
||||
import net.kyori.adventure.platform.bungeecord.BungeeAudiences;
|
||||
import net.md_5.bungee.api.connection.ProxiedPlayer;
|
||||
import org.bstats.bungeecord.Metrics;
|
||||
import org.bstats.charts.SimplePie;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.SonarPlatform;
|
||||
import xyz.jonesdev.sonar.api.logger.LoggerWrapper;
|
||||
import xyz.jonesdev.sonar.bungee.command.BungeeSonarCommand;
|
||||
import xyz.jonesdev.sonar.bungee.fallback.FallbackInjectionHelper;
|
||||
import xyz.jonesdev.sonar.bungee.fallback.FallbackLoginListener;
|
||||
import xyz.jonesdev.sonar.common.boot.SonarBootstrap;
|
||||
import xyz.jonesdev.sonar.common.statistics.CachedBandwidthStatistics;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@Getter
|
||||
public final class SonarBungee extends SonarBootstrap<SonarBungeePlugin> {
|
||||
@@ -59,6 +58,19 @@ public final class SonarBungee extends SonarBootstrap<SonarBungeePlugin> {
|
||||
return bungeeAudiences.player(uniqueId);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("deprecation")
|
||||
public boolean hasTooManyAccounts(final @NotNull InetAddress inetAddress, final int limit) {
|
||||
int count = 1;
|
||||
for (final ProxiedPlayer player : getPlugin().getServer().getPlayers()) {
|
||||
// Check if the IP address of the player is equal to the IP trying to connect
|
||||
if (!Objects.equals(player.getAddress().getAddress(), inetAddress)) continue;
|
||||
// Increment count of duplicated accounts
|
||||
if (++count >= limit) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a wrapper for the plugin logger, so we can use it outside
|
||||
* the velocity module.
|
||||
@@ -99,21 +111,6 @@ public final class SonarBungee extends SonarBootstrap<SonarBungeePlugin> {
|
||||
// Register Sonar command
|
||||
getPlugin().getServer().getPluginManager().registerCommand(getPlugin(), new BungeeSonarCommand());
|
||||
|
||||
// Register Fallback listener
|
||||
getPlugin().getServer().getPluginManager().registerListener(getPlugin(), new FallbackLoginListener());
|
||||
|
||||
// Register traffic service
|
||||
getPlugin().getServer().getScheduler().schedule(getPlugin(), CachedBandwidthStatistics::reset,
|
||||
1L, 1L, TimeUnit.SECONDS);
|
||||
|
||||
// Register queue service
|
||||
getPlugin().getServer().getScheduler().schedule(getPlugin(), getFallback().getQueue().getPollTask(),
|
||||
500L, 500L, TimeUnit.MILLISECONDS);
|
||||
|
||||
// Register verbose service
|
||||
getPlugin().getServer().getScheduler().schedule(getPlugin(), Sonar.get().getVerboseHandler()::update,
|
||||
200L, 200L, TimeUnit.MILLISECONDS);
|
||||
|
||||
// Make sure to inject into the server's connection handler
|
||||
FallbackInjectionHelper.inject();
|
||||
}
|
||||
|
||||
+9
-136
@@ -19,7 +19,6 @@ package xyz.jonesdev.sonar.bungee.fallback;
|
||||
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.handler.codec.CorruptedFrameException;
|
||||
import net.md_5.bungee.netty.ChannelWrapper;
|
||||
import net.md_5.bungee.netty.HandlerBoss;
|
||||
import net.md_5.bungee.protocol.DefinedPacket;
|
||||
@@ -28,24 +27,12 @@ import net.md_5.bungee.protocol.packet.Handshake;
|
||||
import net.md_5.bungee.protocol.packet.LoginRequest;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.ReflectiveOperationException;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.common.fallback.FallbackBandwidthHandler;
|
||||
import xyz.jonesdev.sonar.common.fallback.FallbackChannelHandlerAdapter;
|
||||
import xyz.jonesdev.sonar.common.fallback.FallbackUserWrapper;
|
||||
import xyz.jonesdev.sonar.common.statistics.GlobalSonarStatistics;
|
||||
import xyz.jonesdev.sonar.common.utility.geyser.GeyserUtil;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.UUID;
|
||||
|
||||
import static net.md_5.bungee.netty.PipelineUtils.*;
|
||||
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_BANDWIDTH;
|
||||
import static xyz.jonesdev.sonar.common.fallback.FallbackUserWrapper.customDisconnect;
|
||||
import static xyz.jonesdev.sonar.common.fallback.FallbackUserWrapper.deject;
|
||||
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer.*;
|
||||
|
||||
public final class FallbackChannelHandler extends FallbackChannelHandlerAdapter {
|
||||
|
||||
@@ -74,11 +61,18 @@ public final class FallbackChannelHandler extends FallbackChannelHandlerAdapter
|
||||
if (wrappedPacket != null) {
|
||||
// Intercept any handshake packet by the client
|
||||
if (wrappedPacket instanceof Handshake) {
|
||||
handleHandshake((Handshake) wrappedPacket);
|
||||
final Handshake handshake = (Handshake) wrappedPacket;
|
||||
handleHandshake(handshake.getHost(), handshake.getProtocolVersion());
|
||||
}
|
||||
// Intercept any server login packet by the client
|
||||
if (wrappedPacket instanceof LoginRequest) {
|
||||
handleLogin(ctx, (LoginRequest) wrappedPacket, msg);
|
||||
// Make sure to use the potentially modified, real IP
|
||||
final LoginRequest loginRequest = (LoginRequest) wrappedPacket;
|
||||
final HandlerBoss handlerBoss = channel.pipeline().get(HandlerBoss.class);
|
||||
final ChannelWrapper channelWrapper = (ChannelWrapper) CHANNEL_WRAPPER_FIELD.get(handlerBoss);
|
||||
final InetSocketAddress socketAddress = (InetSocketAddress) channelWrapper.getRemoteAddress();
|
||||
handleLogin(ctx, msg, loginRequest.getData(), socketAddress,
|
||||
PACKET_ENCODER, PACKET_DECODER, TIMEOUT_HANDLER, BOSS_HANDLER);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -86,125 +80,4 @@ public final class FallbackChannelHandler extends FallbackChannelHandlerAdapter
|
||||
// Make sure to let the server handle the rest
|
||||
ctx.fireChannelRead(msg);
|
||||
}
|
||||
|
||||
private void handleHandshake(final @NotNull Handshake handshake) throws Exception {
|
||||
// Check if the player has already sent a handshake packet
|
||||
if (protocolVersion != null) {
|
||||
throw new CorruptedFrameException("Already sent handshake");
|
||||
}
|
||||
// Check if the hostname is invalid
|
||||
if (handshake.getHost().isEmpty()) {
|
||||
throw new CorruptedFrameException("Hostname is empty");
|
||||
}
|
||||
// Store the protocol version
|
||||
protocolVersion = ProtocolVersion.fromId(handshake.getProtocolVersion());
|
||||
// Connections from unknown protocol versions will be discarded
|
||||
// as this is the safest way of handling unwanted connections
|
||||
if (protocolVersion.isUnknown()) {
|
||||
// Sonar does NOT support snapshots or unknown versions;
|
||||
// I'll try my best to stay up-to-date!
|
||||
throw new CorruptedFrameException("Unknown protocol");
|
||||
}
|
||||
// Hook the traffic listener
|
||||
// TODO: Can we implement this in channelActive?
|
||||
channel.pipeline().addFirst(FALLBACK_BANDWIDTH, FallbackBandwidthHandler.INSTANCE);
|
||||
}
|
||||
|
||||
private void handleLogin(final @NotNull ChannelHandlerContext ctx,
|
||||
final @NotNull LoginRequest loginRequest,
|
||||
final @NotNull Object wrappedMessage) throws Exception {
|
||||
// Check if the player has already sent a login packet
|
||||
if (username != null) {
|
||||
throw new CorruptedFrameException("Already logged on");
|
||||
}
|
||||
// Increase joins per second for the action bar verbose
|
||||
GlobalSonarStatistics.countLogin();
|
||||
// Store the username
|
||||
username = loginRequest.getData();
|
||||
// Make sure to use the potentially modified, real IP
|
||||
final HandlerBoss handlerBoss = channel.pipeline().get(HandlerBoss.class);
|
||||
final ChannelWrapper channelWrapper = (ChannelWrapper) CHANNEL_WRAPPER_FIELD.get(handlerBoss);
|
||||
final InetSocketAddress socketAddress = (InetSocketAddress) channelWrapper.getRemoteAddress();
|
||||
inetAddress = socketAddress.getAddress();
|
||||
|
||||
// Check the blacklist here since we cannot let the player "ghost join"
|
||||
if (FALLBACK.getBlacklist().asMap().containsKey(inetAddress)) {
|
||||
customDisconnect(channel, protocolVersion, blacklisted, PACKET_ENCODER, BOSS_HANDLER);
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't continue the verification process if the verification is disabled
|
||||
if (!Sonar.get().getFallback().shouldVerifyNewPlayers()) {
|
||||
ctx.fireChannelRead(wrappedMessage);
|
||||
deject(channel.pipeline());
|
||||
return;
|
||||
}
|
||||
|
||||
// Completely skip Geyser connections
|
||||
if (GeyserUtil.isGeyserConnection(channel, socketAddress)) {
|
||||
ctx.fireChannelRead(wrappedMessage);
|
||||
deject(channel.pipeline());
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the player is already queued since we don't want bots to flood the queue
|
||||
if (FALLBACK.getQueue().getQueuedPlayers().containsKey(inetAddress)) {
|
||||
customDisconnect(channel, protocolVersion, alreadyQueued, PACKET_ENCODER, BOSS_HANDLER);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if Fallback is already verifying a player
|
||||
// → is another player with the same IP address connected to Fallback?
|
||||
if (FALLBACK.getConnected().containsKey(loginRequest.getData())
|
||||
|| FALLBACK.getConnected().containsValue(inetAddress)) {
|
||||
customDisconnect(channel, protocolVersion, alreadyVerifying, PACKET_ENCODER, BOSS_HANDLER);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the protocol ID of the player is not allowed to enter the server
|
||||
if (Sonar.get().getConfig().getVerification().getBlacklistedProtocols()
|
||||
.contains(protocolVersion.getProtocol())) {
|
||||
customDisconnect(channel, protocolVersion, protocolBlacklisted, PACKET_ENCODER, BOSS_HANDLER);
|
||||
return;
|
||||
}
|
||||
|
||||
// Make sure we actually have to verify the player
|
||||
final String offlineUUIDString = "OfflinePlayer:" + loginRequest.getData();
|
||||
final UUID offlineUUID = UUID.nameUUIDFromBytes(offlineUUIDString.getBytes(StandardCharsets.UTF_8));
|
||||
if (Sonar.get().getVerifiedPlayerController().has(inetAddress, offlineUUID)) {
|
||||
ctx.fireChannelRead(wrappedMessage);
|
||||
deject(channel.pipeline());
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the IP address is currently being rate-limited
|
||||
if (!FALLBACK.getRatelimiter().attempt(inetAddress)) {
|
||||
customDisconnect(channel, protocolVersion, reconnectedTooFast, PACKET_ENCODER, BOSS_HANDLER);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the protocol ID of the player is allowed to bypass verification
|
||||
if (Sonar.get().getConfig().getVerification().getWhitelistedProtocols()
|
||||
.contains(protocolVersion.getProtocol())) {
|
||||
ctx.fireChannelRead(wrappedMessage);
|
||||
deject(channel.pipeline());
|
||||
return;
|
||||
}
|
||||
|
||||
// Queue the connection for further processing
|
||||
FALLBACK.getQueue().queue(inetAddress, () -> channel.eventLoop().execute(() -> {
|
||||
// Check if the username matches the valid name regex to prevent
|
||||
// UTF-16 names or other types of exploits
|
||||
if (!Sonar.get().getConfig().getVerification().getValidNameRegex()
|
||||
.matcher(loginRequest.getData()).matches()) {
|
||||
customDisconnect(channel, protocolVersion, invalidUsername, PACKET_ENCODER, BOSS_HANDLER);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create an instance for the Fallback connection
|
||||
user = new FallbackUserWrapper(channel, inetAddress, protocolVersion);
|
||||
// Let the verification handler take over the channel
|
||||
user.hijack(loginRequest.getData(), offlineUUID, PACKET_ENCODER, PACKET_DECODER, TIMEOUT_HANDLER, BOSS_HANDLER);
|
||||
}));
|
||||
}
|
||||
}
|
||||
-84
@@ -1,84 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.bungee.fallback;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer;
|
||||
import net.md_5.bungee.api.connection.PendingConnection;
|
||||
import net.md_5.bungee.api.event.PreLoginEvent;
|
||||
import net.md_5.bungee.api.plugin.Listener;
|
||||
import net.md_5.bungee.chat.ComponentSerializer;
|
||||
import net.md_5.bungee.connection.InitialHandler;
|
||||
import net.md_5.bungee.event.EventHandler;
|
||||
import net.md_5.bungee.event.EventPriority;
|
||||
import net.md_5.bungee.netty.ChannelWrapper;
|
||||
import net.md_5.bungee.protocol.packet.Kick;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.ReflectiveOperationException;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.bungee.SonarBungee;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.net.InetAddress;
|
||||
import java.util.Objects;
|
||||
|
||||
import static xyz.jonesdev.sonar.common.fallback.FallbackUserWrapper.closeWith;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public final class FallbackLoginListener implements Listener {
|
||||
|
||||
private static final Field CHANNEL_FIELD;
|
||||
|
||||
static {
|
||||
try {
|
||||
CHANNEL_FIELD = InitialHandler.class.getDeclaredField("ch");
|
||||
CHANNEL_FIELD.setAccessible(true);
|
||||
} catch (NoSuchFieldException exception) {
|
||||
throw new ReflectiveOperationException(exception);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@EventHandler(priority = EventPriority.LOWEST)
|
||||
public void handle(final @NotNull PreLoginEvent event) throws IllegalAccessException {
|
||||
// Check if the number of online players using the same IP address as
|
||||
// the connecting player is greater than the configured amount
|
||||
final int maxOnlinePerIp = Sonar.get().getConfig().getMaxOnlinePerIp();
|
||||
// Don't do anything if the setting is disabled
|
||||
if (maxOnlinePerIp <= 0) return;
|
||||
|
||||
final InetAddress inetAddress = event.getConnection().getAddress().getAddress();
|
||||
|
||||
final long onlinePerIp = SonarBungee.INSTANCE.getPlugin().getServer().getPlayers().stream()
|
||||
.filter(player -> Objects.equals(player.getAddress().getAddress(), inetAddress))
|
||||
.count()
|
||||
+ 1 /* add 1 because the player hasn't been added to the list of online players yet */;
|
||||
|
||||
// We use '>=' because the player connecting to the server hasn't joined yet
|
||||
if (onlinePerIp >= maxOnlinePerIp) {
|
||||
final Component component = Sonar.get().getConfig().getTooManyOnlinePerIp();
|
||||
final String serialized = GsonComponentSerializer.gson().serialize(component);
|
||||
final PendingConnection connection = event.getConnection();
|
||||
closeWith(((ChannelWrapper) CHANNEL_FIELD.get(connection)).getHandle(),
|
||||
ProtocolVersion.fromId(connection.getVersion()),
|
||||
new Kick(ComponentSerializer.deserialize(serialized)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,7 @@ import xyz.jonesdev.sonar.api.timer.SystemTimer;
|
||||
import xyz.jonesdev.sonar.api.verbose.Notification;
|
||||
import xyz.jonesdev.sonar.api.verbose.Verbose;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer;
|
||||
import xyz.jonesdev.sonar.common.service.SonarServiceThreadManager;
|
||||
import xyz.jonesdev.sonar.common.statistics.GlobalSonarStatistics;
|
||||
import xyz.jonesdev.sonar.common.subcommand.SubcommandRegistryHolder;
|
||||
import xyz.jonesdev.sonar.common.update.UpdateChecker;
|
||||
@@ -105,6 +106,10 @@ public abstract class SonarBootstrap<T> implements Sonar {
|
||||
// Run the per-platform initialization method
|
||||
enable();
|
||||
|
||||
// Start threads
|
||||
getLogger().info("Starting all managed threads...");
|
||||
SonarServiceThreadManager.start();
|
||||
|
||||
// Done
|
||||
getLogger().info("Done ({}s)!", launchTimer);
|
||||
} catch (Throwable throwable) {
|
||||
@@ -170,6 +175,8 @@ public abstract class SonarBootstrap<T> implements Sonar {
|
||||
public final void shutdown() {
|
||||
// Initialize shutdown process
|
||||
getLogger().info("Starting shutdown process...");
|
||||
// Interrupt threads
|
||||
SonarServiceThreadManager.stop();
|
||||
// Close database connection
|
||||
verifiedPlayerController.close();
|
||||
// Run the per-platform disable method
|
||||
|
||||
+171
-7
@@ -20,6 +20,7 @@ package xyz.jonesdev.sonar.common.fallback;
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.channel.ChannelInboundHandlerAdapter;
|
||||
import io.netty.handler.codec.CorruptedFrameException;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
@@ -27,9 +28,19 @@ import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.fallback.Fallback;
|
||||
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer;
|
||||
import xyz.jonesdev.sonar.common.statistics.GlobalSonarStatistics;
|
||||
import xyz.jonesdev.sonar.common.utility.geyser.GeyserUtil;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.UUID;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_BANDWIDTH;
|
||||
import static xyz.jonesdev.sonar.common.fallback.FallbackUserWrapper.customDisconnect;
|
||||
import static xyz.jonesdev.sonar.common.fallback.FallbackUserWrapper.deject;
|
||||
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer.*;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public class FallbackChannelHandlerAdapter extends ChannelInboundHandlerAdapter {
|
||||
@@ -59,13 +70,7 @@ public class FallbackChannelHandlerAdapter extends ChannelInboundHandlerAdapter
|
||||
// The player cannot be in the queue if the IP address is invalid
|
||||
if (inetAddress != null) {
|
||||
// Remove the IP address from the queue
|
||||
FALLBACK.getQueue().remove(inetAddress);
|
||||
}
|
||||
// The player cannot be in the verification if the user object doesn't exist,
|
||||
// or if the user failed/succeeded the bot verification process.
|
||||
if (user != null && user.getState().canReceivePackets()) {
|
||||
// Mark the verification attempt as failed
|
||||
user.fail("disconnected during verification");
|
||||
FALLBACK.getQueue().getQueuedPlayers().remove(inetAddress);
|
||||
}
|
||||
// Make sure to let the server handle the rest
|
||||
ctx.fireChannelInactive();
|
||||
@@ -80,4 +85,163 @@ public class FallbackChannelHandlerAdapter extends ChannelInboundHandlerAdapter
|
||||
// Simply close the channel if we encounter any errors.
|
||||
ctx.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates and handles incoming handshake packets
|
||||
*
|
||||
* @param hostname Hostname (server address) sent by the client
|
||||
* @param protocol Protocol version number sent by the client
|
||||
*/
|
||||
protected final void handleHandshake(final @NotNull String hostname, final int protocol) throws Exception {
|
||||
// Check if the hostname is invalid
|
||||
if (hostname.isEmpty()) {
|
||||
throw new CorruptedFrameException("Hostname is empty");
|
||||
}
|
||||
// Check if the player has already sent a handshake packet
|
||||
if (protocolVersion != null) {
|
||||
throw new CorruptedFrameException("Already sent handshake");
|
||||
}
|
||||
// Store the protocol version
|
||||
protocolVersion = ProtocolVersion.fromId(protocol);
|
||||
// Hook the traffic listener
|
||||
// TODO: Can we implement this in channelActive?
|
||||
channel.pipeline().addFirst(FALLBACK_BANDWIDTH, FallbackBandwidthHandler.INSTANCE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates and handles incoming login packets
|
||||
*
|
||||
* @param ctx Forwarded channel handler context
|
||||
* @param loginPacket Login packet sent by the client
|
||||
* @param username Username sent by the client
|
||||
* @param socketAddress Socket address of the client
|
||||
*/
|
||||
protected final void handleLogin(final @NotNull ChannelHandlerContext ctx,
|
||||
final @NotNull Object loginPacket,
|
||||
final @NotNull String username,
|
||||
final @NotNull InetSocketAddress socketAddress,
|
||||
final @NotNull String encoder,
|
||||
final @NotNull String decoder,
|
||||
final @NotNull String timeout,
|
||||
final @NotNull String handler) throws Exception {
|
||||
// Check if the player has already sent a login packet
|
||||
if (this.username != null) {
|
||||
throw new CorruptedFrameException("Already sent login");
|
||||
}
|
||||
// Connections from unknown protocol versions will be discarded
|
||||
// as this is the safest way of handling unwanted connections
|
||||
if (protocolVersion.isUnknown()) {
|
||||
// Sonar does NOT support snapshots or unknown versions;
|
||||
// I'll try my best to stay up-to-date!
|
||||
throw new CorruptedFrameException("Unknown protocol version");
|
||||
}
|
||||
// Increase joins per second for the action bar verbose
|
||||
GlobalSonarStatistics.countLogin();
|
||||
// Store the username and IP address
|
||||
this.username = username;
|
||||
this.inetAddress = socketAddress.getAddress();
|
||||
|
||||
// Check the blacklist here since we cannot let the player "ghost join"
|
||||
if (FALLBACK.getBlacklist().asMap().containsKey(inetAddress)) {
|
||||
customDisconnect(channel, protocolVersion, blacklisted, encoder, handler);
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't continue the verification process if the verification is disabled
|
||||
if (!Sonar.get().getFallback().shouldVerifyNewPlayers()) {
|
||||
initialLogin(ctx, loginPacket, encoder, handler);
|
||||
return;
|
||||
}
|
||||
|
||||
// Completely skip Geyser connections if configured
|
||||
final boolean geyser = GeyserUtil.isGeyserConnection(channel, socketAddress);
|
||||
if (geyser && !Sonar.get().getConfig().getVerification().isCheckGeyser()) {
|
||||
initialLogin(ctx, loginPacket, encoder, handler);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the player is already queued since we don't want bots to flood the queue
|
||||
if (FALLBACK.getQueue().getQueuedPlayers().containsKey(inetAddress)) {
|
||||
customDisconnect(channel, protocolVersion, alreadyQueued, encoder, handler);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if Fallback is already verifying a player
|
||||
// → is another player with the same IP address connected to Fallback?
|
||||
if (FALLBACK.getConnected().containsKey(username)
|
||||
|| FALLBACK.getConnected().containsValue(inetAddress)) {
|
||||
customDisconnect(channel, protocolVersion, alreadyVerifying, encoder, handler);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the protocol ID of the player is not allowed to enter the server
|
||||
if (Sonar.get().getConfig().getVerification().getBlacklistedProtocols()
|
||||
.contains(protocolVersion.getProtocol())) {
|
||||
customDisconnect(channel, protocolVersion, protocolBlacklisted, encoder, handler);
|
||||
return;
|
||||
}
|
||||
|
||||
// Make sure we actually have to verify the player
|
||||
final String offlineUUIDString = "OfflinePlayer:" + username;
|
||||
final UUID offlineUUID = UUID.nameUUIDFromBytes(offlineUUIDString.getBytes(StandardCharsets.UTF_8));
|
||||
if (Sonar.get().getVerifiedPlayerController().has(inetAddress, offlineUUID)) {
|
||||
initialLogin(ctx, loginPacket, encoder, handler);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the IP address is currently being rate-limited
|
||||
if (!FALLBACK.getRatelimiter().attempt(inetAddress)) {
|
||||
customDisconnect(channel, protocolVersion, reconnectedTooFast, encoder, handler);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the protocol ID of the player is allowed to bypass verification
|
||||
if (Sonar.get().getConfig().getVerification().getWhitelistedProtocols()
|
||||
.contains(protocolVersion.getProtocol())) {
|
||||
initialLogin(ctx, loginPacket, encoder, handler);
|
||||
return;
|
||||
}
|
||||
|
||||
// Queue the connection for further processing
|
||||
FALLBACK.getQueue().getQueuedPlayers().put(inetAddress, () -> {
|
||||
// Check if the username matches the valid name regex to prevent
|
||||
// UTF-16 names or other types of exploits
|
||||
if (!Sonar.get().getConfig().getVerification().getValidNameRegex()
|
||||
.matcher(username).matches()) {
|
||||
customDisconnect(channel, protocolVersion, invalidUsername, encoder, handler);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create an instance for the Fallback connection
|
||||
user = new FallbackUserWrapper(channel, inetAddress, protocolVersion, geyser);
|
||||
// Let the verification handler take over the channel
|
||||
user.hijack(username, offlineUUID, encoder, decoder, timeout, handler);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the maximum accounts per IP limit check before letting the player join
|
||||
*
|
||||
* @param ctx Forwarded channel handler context
|
||||
* @param loginPacket Login packet sent by the client
|
||||
*/
|
||||
protected final void initialLogin(final @NotNull ChannelHandlerContext ctx,
|
||||
final @NotNull Object loginPacket,
|
||||
final @NotNull String encoder,
|
||||
final @NotNull String handler) throws Exception {
|
||||
final int maxOnlinePerIp = Sonar.get().getConfig().getMaxOnlinePerIp();
|
||||
// Skip the maximum online per IP check if it's disabled in the configuration
|
||||
if (maxOnlinePerIp > 0) {
|
||||
// Check if the number of online players using the same IP address as
|
||||
// the connecting player is greater than the configured amount
|
||||
if (Sonar.get().hasTooManyAccounts(inetAddress, maxOnlinePerIp)) {
|
||||
customDisconnect(channel, protocolVersion, FallbackPreparer.tooManyOnlinePerIP, encoder, handler);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
ctx.fireChannelRead(loginPacket);
|
||||
// Deject the channel since we don't need it anymore
|
||||
deject(channel.pipeline());
|
||||
}
|
||||
}
|
||||
+50
-26
@@ -46,6 +46,7 @@ import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.*;
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_20_2;
|
||||
|
||||
@Getter
|
||||
@ToString(of = {"protocolVersion", "inetAddress"})
|
||||
@@ -54,16 +55,19 @@ public final class FallbackUserWrapper implements FallbackUser {
|
||||
private final ChannelPipeline pipeline;
|
||||
private final InetAddress inetAddress;
|
||||
private final ProtocolVersion protocolVersion;
|
||||
private final boolean geyser;
|
||||
@Setter
|
||||
private FallbackUserState state = FallbackUserState.LOGIN_ACK; // 1.20.2+
|
||||
|
||||
public FallbackUserWrapper(final @NotNull Channel channel,
|
||||
final @NotNull InetAddress inetAddress,
|
||||
final @NotNull ProtocolVersion protocolVersion) {
|
||||
final @NotNull ProtocolVersion protocolVersion,
|
||||
final boolean geyser) {
|
||||
this.channel = channel;
|
||||
this.pipeline = channel.pipeline();
|
||||
this.inetAddress = inetAddress;
|
||||
this.protocolVersion = protocolVersion;
|
||||
this.geyser = geyser;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -74,26 +78,14 @@ public final class FallbackUserWrapper implements FallbackUser {
|
||||
@Override
|
||||
public void hijack(final @NotNull String username, final @NotNull UUID uuid,
|
||||
final @NotNull String encoder, final @NotNull String decoder,
|
||||
final @NotNull String timeout, final @NotNull String boss) {
|
||||
// This rarely happens when the channel hangs, but the player is still connecting
|
||||
// This also fixes a unique issue with TCPShield and other reverse proxies
|
||||
if (pipeline.get(encoder) == null || pipeline.get(decoder) == null) {
|
||||
channel.close();
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove main pipeline to completely take over the channel
|
||||
if (pipeline.get(boss) != null) {
|
||||
pipeline.remove(boss);
|
||||
}
|
||||
|
||||
final @NotNull String timeout, final @NotNull String handler) {
|
||||
// The player has joined the verification
|
||||
GlobalSonarStatistics.totalAttemptedVerifications++;
|
||||
|
||||
if (Sonar.get().getConfig().getVerification().isLogConnections()) {
|
||||
// Only log the processing message if the server isn't under attack.
|
||||
// We let the user override this through the configuration.
|
||||
if (!Sonar.get().getAttackTracker().isCurrentlyUnderAttack()
|
||||
if (Sonar.get().getAttackTracker().getCurrentAttack() == null
|
||||
|| Sonar.get().getConfig().getVerification().isLogDuringAttack()) {
|
||||
Sonar.get().getFallback().getLogger().info(Sonar.get().getConfig().getVerification().getConnectLog()
|
||||
.replace("%name%", username)
|
||||
@@ -117,16 +109,36 @@ public final class FallbackUserWrapper implements FallbackUser {
|
||||
final FallbackPacketEncoder newEncoder = new FallbackPacketEncoder(protocolVersion);
|
||||
pipeline.replace(encoder, FALLBACK_PACKET_ENCODER, newEncoder);
|
||||
|
||||
// Send LoginSuccess packet to make the client think they are joining the server
|
||||
write(new LoginSuccessPacket(username, uuid));
|
||||
channel.eventLoop().execute(() -> {
|
||||
// Remove main pipeline to completely take over the channel
|
||||
if (pipeline.get(handler) != null) {
|
||||
pipeline.remove(handler);
|
||||
}
|
||||
|
||||
// The LoginSuccess packet has been sent, now we can change the registry state
|
||||
newEncoder.updateRegistry(protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_20_2) >= 0
|
||||
? FallbackPacketRegistry.CONFIG : FallbackPacketRegistry.GAME);
|
||||
// Send LoginSuccess packet to make the client think they are joining the server
|
||||
write(new LoginSuccessPacket(username, uuid));
|
||||
|
||||
// Replace normal decoder to allow custom packets
|
||||
pipeline.replace(decoder, FALLBACK_PACKET_DECODER,
|
||||
new FallbackPacketDecoder(this, new FallbackVerificationHandler(this, username, uuid)));
|
||||
// The LoginSuccess packet has been sent, now we can change the registry state
|
||||
newEncoder.updateRegistry(protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_20_2) >= 0
|
||||
? FallbackPacketRegistry.CONFIG : FallbackPacketRegistry.GAME);
|
||||
|
||||
try {
|
||||
// Prepare custom packet decoder and verification handler
|
||||
final FallbackVerificationHandler verification = new FallbackVerificationHandler(this, username, uuid);
|
||||
final FallbackPacketDecoder fallbackPacketDecoder = new FallbackPacketDecoder(this, verification);
|
||||
// Replace normal decoder to allow custom packets
|
||||
pipeline.replace(decoder, FALLBACK_PACKET_DECODER, fallbackPacketDecoder);
|
||||
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_20_2) < 0) {
|
||||
// Start initializing the actual join process for pre-1.20.2 clients
|
||||
verification.initialJoinProcess();
|
||||
}
|
||||
} catch (Throwable throwable) {
|
||||
// This rarely happens when the channel hangs and the player is still connecting
|
||||
// I honestly have no idea how else I'm supposed to fix this
|
||||
channel.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -136,7 +148,7 @@ public final class FallbackUserWrapper implements FallbackUser {
|
||||
|
||||
// Only log the failed message if the server isn't currently under attack.
|
||||
// However, we let the user override this through the configuration.
|
||||
final boolean shouldLog = !Sonar.get().getAttackTracker().isCurrentlyUnderAttack()
|
||||
final boolean shouldLog = Sonar.get().getAttackTracker().getCurrentAttack() == null
|
||||
|| Sonar.get().getConfig().getVerification().isLogDuringAttack();
|
||||
|
||||
// Only disconnect the player and log if the channel is active
|
||||
@@ -223,7 +235,7 @@ public final class FallbackUserWrapper implements FallbackUser {
|
||||
*
|
||||
* @param packet Disconnect packet
|
||||
* @param encoder Encoder to replace
|
||||
* @param boss Main pipeline to remove
|
||||
* @param handler Main pipeline to remove
|
||||
* @param channel Channel of the player
|
||||
* @param protocolVersion Protocol version of the player
|
||||
*/
|
||||
@@ -231,7 +243,19 @@ public final class FallbackUserWrapper implements FallbackUser {
|
||||
final @NotNull ProtocolVersion protocolVersion,
|
||||
final @NotNull FallbackPacket packet,
|
||||
final @NotNull String encoder,
|
||||
final @NotNull String boss) {
|
||||
final @NotNull String handler) {
|
||||
if (channel.eventLoop().inEventLoop()) {
|
||||
_customDisconnect(channel, protocolVersion, packet, encoder, handler);
|
||||
} else {
|
||||
channel.eventLoop().execute(() -> _customDisconnect(channel, protocolVersion, packet, encoder, handler));
|
||||
}
|
||||
}
|
||||
|
||||
private static void _customDisconnect(final @NotNull Channel channel,
|
||||
final @NotNull ProtocolVersion protocolVersion,
|
||||
final @NotNull FallbackPacket packet,
|
||||
final @NotNull String encoder,
|
||||
final @NotNull String boss) {
|
||||
// Remove main pipeline to completely take over the channel
|
||||
if (channel.pipeline().get(boss) != null) {
|
||||
channel.pipeline().remove(boss);
|
||||
|
||||
+62
-24
@@ -69,6 +69,7 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
|
||||
private double posX, posY, posZ, lastY, spawnYPosition;
|
||||
private boolean resolvedClientBrand, resolvedClientSettings;
|
||||
private boolean listenForMovements;
|
||||
private boolean receivedSteerBoat, receivedPlayerInput;
|
||||
|
||||
// CAPTCHA
|
||||
private final SystemTimer keepAlive = new SystemTimer();
|
||||
@@ -77,7 +78,7 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
|
||||
private int captchaTriesLeft;
|
||||
|
||||
// Cached options to make sure the original values don't change throughout the verification
|
||||
private final boolean performVehicle, performCaptcha, performGravity, performCollisions;
|
||||
private final boolean performVehicle, performCaptcha, performGravity, performCollisions, transfer;
|
||||
|
||||
public FallbackVerificationHandler(final @NotNull FallbackUser user,
|
||||
final @NotNull String username,
|
||||
@@ -85,22 +86,26 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
|
||||
this.user = user;
|
||||
this.username = username;
|
||||
this.playerUuid = playerUuid;
|
||||
this.performGravity = Sonar.get().getConfig().getVerification().getGravity().isEnabled();
|
||||
// We don't want to check Geyser players for valid gravity, as this might cause issues because of the protocol
|
||||
this.performGravity = !user.isGeyser() && Sonar.get().getConfig().getVerification().getGravity().isEnabled();
|
||||
this.performCollisions = Sonar.get().getConfig().getVerification().getGravity().isCheckCollisions();
|
||||
this.performCaptcha = FALLBACK.shouldPerformCaptcha();
|
||||
this.performVehicle = FALLBACK.shouldPerformVehicleCheck();
|
||||
|
||||
if (user.getProtocolVersion().compareTo(MINECRAFT_1_20_2) < 0) {
|
||||
// Start initializing the actual join process
|
||||
initialJoinProcess();
|
||||
}
|
||||
this.transfer = Sonar.get().getConfig().getVerification().getTransfer().isEnabled()
|
||||
&& user.getProtocolVersion().compareTo(MINECRAFT_1_20_5) >= 0;
|
||||
}
|
||||
|
||||
private void configure() {
|
||||
// Set the state to CONFIGURE to avoid false positives
|
||||
user.setState(CONFIGURE);
|
||||
// Send the necessary configuration packets to the client
|
||||
user.delayedWrite(REGISTRY_SYNC);
|
||||
if (user.getProtocolVersion().compareTo(MINECRAFT_1_20_5) >= 0) {
|
||||
for (final FallbackPacket packet : REGISTRY_SYNC_1_20_5) {
|
||||
user.delayedWrite(packet);
|
||||
}
|
||||
} else {
|
||||
user.delayedWrite(REGISTRY_SYNC_LEGACY);
|
||||
}
|
||||
user.delayedWrite(FINISH_CONFIGURATION);
|
||||
// Send all packets in one flush
|
||||
user.getChannel().flush();
|
||||
@@ -123,7 +128,7 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
|
||||
}
|
||||
}
|
||||
|
||||
private void initialJoinProcess() {
|
||||
public void initialJoinProcess() {
|
||||
if (user.getProtocolVersion().compareTo(MINECRAFT_1_8) < 0) {
|
||||
// 1.7 players don't have KeepAlive packets in the login process
|
||||
sendJoinGamePacket();
|
||||
@@ -307,6 +312,19 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
|
||||
return;
|
||||
}
|
||||
|
||||
if (packet instanceof PaddleBoatPacket) {
|
||||
// Check if we are currently expecting a PaddleBoatPacket packet
|
||||
assertState(VEHICLE);
|
||||
|
||||
// Only pass the player if both checks are passed
|
||||
if (receivedPlayerInput && receivedSteerBoat) {
|
||||
captchaOrNext();
|
||||
}
|
||||
|
||||
// Mark paddle boat check as passed
|
||||
receivedSteerBoat = true;
|
||||
}
|
||||
|
||||
if (packet instanceof PlayerInputPacket) {
|
||||
// Check if we are currently expecting a PlayerInputPacket packet
|
||||
assertState(VEHICLE);
|
||||
@@ -318,11 +336,23 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
|
||||
checkFrame(inputPacket.getSideways() <= 0.98f, "invalid vehicle speed (s)");
|
||||
|
||||
// Don't verify if the player tries unmounting or jumping
|
||||
// We can't fail since the player is able to jump or sneak
|
||||
if (inputPacket.isJump() || inputPacket.isUnmount()) return;
|
||||
checkFrame(!inputPacket.isJump(), "tried to jump in vehicle");
|
||||
checkFrame(!inputPacket.isUnmount(), "tried to escape the vehicle");
|
||||
|
||||
// Continue checking or verify the player
|
||||
captchaOrNext();
|
||||
// Only pass the player if both checks are passed
|
||||
if (receivedPlayerInput && receivedSteerBoat) {
|
||||
captchaOrNext();
|
||||
}
|
||||
|
||||
// pre-1.9 clients do not have a PaddleBoat packet
|
||||
if (user.getProtocolVersion().compareTo(MINECRAFT_1_9) < 0) {
|
||||
receivedSteerBoat = true;
|
||||
} else if (!user.isGeyser()) {
|
||||
// PaddleBoat → PlayerInput → PaddleBoat
|
||||
checkFrame(receivedSteerBoat, "invalid vehicle steering order");
|
||||
}
|
||||
// Mark player input check as passed
|
||||
receivedPlayerInput = true;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -340,8 +370,10 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
|
||||
assertState(CONFIGURE);
|
||||
|
||||
// Check if the client has already sent valid ClientSettings and PluginMessage packets
|
||||
checkFrame(resolvedClientBrand, "did not resolve client brand");
|
||||
checkFrame(resolvedClientSettings, "did not resolve client settings");
|
||||
if (!user.isGeyser()) {
|
||||
checkFrame(resolvedClientBrand, "did not resolve client brand");
|
||||
checkFrame(resolvedClientSettings, "did not resolve client settings");
|
||||
}
|
||||
|
||||
// Start initializing the actual join process
|
||||
updateEncoderDecoderState(FallbackPacketRegistry.GAME);
|
||||
@@ -453,15 +485,15 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
|
||||
|
||||
// Reset all values to ensure safety on teleport
|
||||
tick = 1;
|
||||
posY = lastY = -1;
|
||||
expectedTeleportId = -1;
|
||||
posY = lastY = expectedTeleportId = -1;
|
||||
|
||||
// Now we can send the chunk data
|
||||
sendChunkData();
|
||||
return;
|
||||
}
|
||||
|
||||
if (user.getState() != LOGIN_ACK) {
|
||||
// Only handle position packets if we aren't in certain phases
|
||||
if (user.getState() != LOGIN_ACK && user.getState() != VEHICLE) {
|
||||
if (packet instanceof PlayerPositionPacket) {
|
||||
final PlayerPositionPacket position = (PlayerPositionPacket) packet;
|
||||
handlePositionUpdate(position.getX(), position.getY(), position.getZ(), position.isOnGround());
|
||||
@@ -490,8 +522,7 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
|
||||
&& x == SPAWN_X_POSITION && y == spawnYPosition && z == SPAWN_Z_POSITION) {
|
||||
// Reset all values to ensure safety on teleport
|
||||
tick = 1;
|
||||
posY = -1;
|
||||
expectedTeleportId = -1;
|
||||
posY = expectedTeleportId = -1;
|
||||
// Check for ground state in the first packet
|
||||
checkFrame(!ground, "invalid ground state");
|
||||
}
|
||||
@@ -564,7 +595,7 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
|
||||
// Make sure we don't run out of predicted Y motions
|
||||
checkFrame(tick < preparedCachedYMotions.length, "too many movements");
|
||||
|
||||
if (!ground) {
|
||||
if (!ground && performGravity) {
|
||||
final double predictedY = preparedCachedYMotions[tick];
|
||||
final double offsetY = Math.abs(deltaY - predictedY);
|
||||
|
||||
@@ -575,7 +606,7 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
|
||||
}
|
||||
|
||||
// Check if the y motion is roughly equal to the predicted value
|
||||
checkFrame(offsetY < 0.005, String.format("invalid gravity: %d, %.7f, %.10f, %.10f != %.10f",
|
||||
checkFrame(offsetY < 5e-3, String.format("invalid gravity: %d, %.7f, %.10f, %.10f != %.10f",
|
||||
tick, y, offsetY, deltaY, predictedY));
|
||||
}
|
||||
tick++;
|
||||
@@ -641,7 +672,7 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
|
||||
captchaTriesLeft = Sonar.get().getConfig().getVerification().getMap().getMaxTries();
|
||||
|
||||
// Set slot to map
|
||||
user.delayedWrite(new SetSlotPacket(0, 36, 1, 0,
|
||||
user.delayedWrite(new SetSlotPacket(36, 1,
|
||||
ItemType.FILLED_MAP.getId(user.getProtocolVersion()), SetSlotPacket.MAP_NBT));
|
||||
// Send random captcha to the player
|
||||
captcha = MAP_INFO_PREPARER.getRandomCaptcha();
|
||||
@@ -649,7 +680,7 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
|
||||
// Teleport the player to the position above the platform
|
||||
user.delayedWrite(CAPTCHA_POSITION);
|
||||
// Make sure the player cannot move
|
||||
user.delayedWrite(CAPTCHA_ABILITIES);
|
||||
user.delayedWrite(user.isGeyser() ? CAPTCHA_ABILITIES_BEDROCK : CAPTCHA_ABILITIES);
|
||||
// Make sure the player knows what to do
|
||||
user.delayedWrite(enterCodeMessage);
|
||||
// Send all packets in one flush
|
||||
@@ -671,6 +702,13 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
|
||||
// Call the VerifySuccessEvent for external API usage
|
||||
Sonar.get().getEventManager().publish(new UserVerifySuccessEvent(username, playerUuid, user, login.delay()));
|
||||
|
||||
// If enabled, transfer the player back to the origin server
|
||||
// This feature was introduced by Mojang in Minecraft version 1.20.5
|
||||
// Thanks a lot to @Kadeluxe for helping me out with this!
|
||||
if (transfer) {
|
||||
user.write(transferToOrigin);
|
||||
}
|
||||
|
||||
// Disconnect player with the verification success message
|
||||
user.disconnect(Sonar.get().getConfig().getVerification().getVerificationSuccess());
|
||||
|
||||
|
||||
+1
-1
@@ -101,7 +101,7 @@ public final class FallbackPacketDecoder extends ChannelInboundHandlerAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
private void doLengthSanityChecks(final ByteBuf byteBuf,
|
||||
private void doLengthSanityChecks(final @NotNull ByteBuf byteBuf,
|
||||
final @NotNull FallbackPacket packet) throws Exception {
|
||||
final int expectedMaxLen = packet.expectedMaxLength(byteBuf, user.getProtocolVersion());
|
||||
if (expectedMaxLen != -1 && byteBuf.readableBytes() > expectedMaxLen) {
|
||||
|
||||
+76
-29
@@ -51,22 +51,30 @@ public enum FallbackPacketRegistry {
|
||||
CONFIG {
|
||||
{
|
||||
clientbound.register(DisconnectPacket.class, DisconnectPacket::new,
|
||||
map(0x01, MINECRAFT_1_20_2, true));
|
||||
map(0x01, MINECRAFT_1_20_2, true),
|
||||
map(0x02, MINECRAFT_1_20_5, true));
|
||||
clientbound.register(FinishConfigurationPacket.class, FinishConfigurationPacket::new,
|
||||
map(0x02, MINECRAFT_1_20_2, false));
|
||||
map(0x02, MINECRAFT_1_20_2, false),
|
||||
map(0x03, MINECRAFT_1_20_5, false));
|
||||
clientbound.register(KeepAlivePacket.class, KeepAlivePacket::new,
|
||||
map(0x03, MINECRAFT_1_20_2, false));
|
||||
map(0x03, MINECRAFT_1_20_2, false),
|
||||
map(0x04, MINECRAFT_1_20_5, false));
|
||||
|
||||
clientbound.register(RegistryDataPacket.class, RegistryDataPacket::new,
|
||||
map(0x05, MINECRAFT_1_20_2, true));
|
||||
map(0x05, MINECRAFT_1_20_2, true),
|
||||
map(0x07, MINECRAFT_1_20_5, true));
|
||||
|
||||
serverbound.register(ClientSettingsPacket.class, ClientSettingsPacket::new,
|
||||
map(0x00, MINECRAFT_1_20_2, false));
|
||||
serverbound.register(PluginMessagePacket.class, PluginMessagePacket::new,
|
||||
map(0x01, MINECRAFT_1_20_2, false));
|
||||
map(0x01, MINECRAFT_1_20_2, false),
|
||||
map(0x02, MINECRAFT_1_20_5, false));
|
||||
serverbound.register(FinishConfigurationPacket.class, FinishConfigurationPacket::new,
|
||||
map(0x02, MINECRAFT_1_20_2, false));
|
||||
map(0x02, MINECRAFT_1_20_2, false),
|
||||
map(0x03, MINECRAFT_1_20_5, false));
|
||||
serverbound.register(KeepAlivePacket.class, KeepAlivePacket::new,
|
||||
map(0x03, MINECRAFT_1_20_2, false));
|
||||
map(0x03, MINECRAFT_1_20_2, false),
|
||||
map(0x04, MINECRAFT_1_20_5, false));
|
||||
}
|
||||
},
|
||||
GAME {
|
||||
@@ -84,7 +92,8 @@ public enum FallbackPacketRegistry {
|
||||
map(0x25, MINECRAFT_1_19_1, true),
|
||||
map(0x24, MINECRAFT_1_19_3, true),
|
||||
map(0x28, MINECRAFT_1_19_4, true),
|
||||
map(0x29, MINECRAFT_1_20_2, true));
|
||||
map(0x29, MINECRAFT_1_20_2, true),
|
||||
map(0x2B, MINECRAFT_1_20_5, true));
|
||||
clientbound.register(KeepAlivePacket.class, KeepAlivePacket::new,
|
||||
map(0x00, MINECRAFT_1_7_2, false),
|
||||
map(0x1F, MINECRAFT_1_9, false),
|
||||
@@ -98,7 +107,8 @@ public enum FallbackPacketRegistry {
|
||||
map(0x20, MINECRAFT_1_19_1, false),
|
||||
map(0x1F, MINECRAFT_1_19_3, false),
|
||||
map(0x23, MINECRAFT_1_19_4, false),
|
||||
map(0x24, MINECRAFT_1_20_2, false));
|
||||
map(0x24, MINECRAFT_1_20_2, false),
|
||||
map(0x26, MINECRAFT_1_20_5, false));
|
||||
clientbound.register(DisconnectPacket.class, DisconnectPacket::new,
|
||||
map(0x40, MINECRAFT_1_7_2, true),
|
||||
map(0x1A, MINECRAFT_1_9, true),
|
||||
@@ -112,7 +122,8 @@ public enum FallbackPacketRegistry {
|
||||
map(0x19, MINECRAFT_1_19_1, true),
|
||||
map(0x17, MINECRAFT_1_19_3, true),
|
||||
map(0x1A, MINECRAFT_1_19_4, true),
|
||||
map(0x1B, MINECRAFT_1_20_2, true));
|
||||
map(0x1B, MINECRAFT_1_20_2, true),
|
||||
map(0x1D, MINECRAFT_1_20_5, true));
|
||||
clientbound.register(PlayerPositionLookPacket.class, PlayerPositionLookPacket::new,
|
||||
map(0x08, MINECRAFT_1_7_2, true),
|
||||
map(0x2E, MINECRAFT_1_9, true),
|
||||
@@ -127,7 +138,8 @@ public enum FallbackPacketRegistry {
|
||||
map(0x39, MINECRAFT_1_19_1, true),
|
||||
map(0x38, MINECRAFT_1_19_3, true),
|
||||
map(0x3C, MINECRAFT_1_19_4, true),
|
||||
map(0x3E, MINECRAFT_1_20_2, true));
|
||||
map(0x3E, MINECRAFT_1_20_2, true),
|
||||
map(0x40, MINECRAFT_1_20_5, true));
|
||||
clientbound.register(ClientAbilitiesPacket.class, ClientAbilitiesPacket::new,
|
||||
map(0x39, MINECRAFT_1_7_2, true),
|
||||
map(0x2B, MINECRAFT_1_9, true),
|
||||
@@ -142,7 +154,8 @@ public enum FallbackPacketRegistry {
|
||||
map(0x31, MINECRAFT_1_19_1, true),
|
||||
map(0x30, MINECRAFT_1_19_3, true),
|
||||
map(0x34, MINECRAFT_1_19_4, true),
|
||||
map(0x36, MINECRAFT_1_20_2, true));
|
||||
map(0x36, MINECRAFT_1_20_2, true),
|
||||
map(0x38, MINECRAFT_1_20_5, true));
|
||||
clientbound.register(ChunkDataPacket.class, ChunkDataPacket::new,
|
||||
map(0x21, MINECRAFT_1_7_2, true),
|
||||
map(0x20, MINECRAFT_1_9, true),
|
||||
@@ -156,7 +169,8 @@ public enum FallbackPacketRegistry {
|
||||
map(0x21, MINECRAFT_1_19_1, true),
|
||||
map(0x20, MINECRAFT_1_19_3, true),
|
||||
map(0x24, MINECRAFT_1_19_4, true),
|
||||
map(0x25, MINECRAFT_1_20_2, true));
|
||||
map(0x25, MINECRAFT_1_20_2, true),
|
||||
map(0x27, MINECRAFT_1_20_5, true));
|
||||
clientbound.register(RemoveEntitiesPacket.class, RemoveEntitiesPacket::new,
|
||||
map(0x13, MINECRAFT_1_7_2, true),
|
||||
map(0x30, MINECRAFT_1_9, true),
|
||||
@@ -173,7 +187,8 @@ public enum FallbackPacketRegistry {
|
||||
map(0x3A, MINECRAFT_1_19_3, true),
|
||||
map(0x3E, MINECRAFT_1_19_4, true),
|
||||
map(0x3E, MINECRAFT_1_19_4, true),
|
||||
map(0x40, MINECRAFT_1_20_2, true));
|
||||
map(0x40, MINECRAFT_1_20_2, true),
|
||||
map(0x42, MINECRAFT_1_20_5, true));
|
||||
clientbound.register(SetPassengersPacket.class, SetPassengersPacket::new,
|
||||
map(0x1B, MINECRAFT_1_7_2, true),
|
||||
map(0x40, MINECRAFT_1_9, true),
|
||||
@@ -187,7 +202,8 @@ public enum FallbackPacketRegistry {
|
||||
map(0x55, MINECRAFT_1_19_3, true),
|
||||
map(0x59, MINECRAFT_1_19_4, true),
|
||||
map(0x5B, MINECRAFT_1_20_2, true),
|
||||
map(0x5D, MINECRAFT_1_20_3, true));
|
||||
map(0x5D, MINECRAFT_1_20_3, true),
|
||||
map(0x5F, MINECRAFT_1_20_5, true));
|
||||
clientbound.register(SpawnEntityPacket.class, SpawnEntityPacket::new,
|
||||
map(0x0E, MINECRAFT_1_7_2, true),
|
||||
map(0x00, MINECRAFT_1_9, true),
|
||||
@@ -205,7 +221,8 @@ public enum FallbackPacketRegistry {
|
||||
map(0x3F, MINECRAFT_1_19_3, true),
|
||||
map(0x43, MINECRAFT_1_19_4, true),
|
||||
map(0x45, MINECRAFT_1_20_2, true),
|
||||
map(0x47, MINECRAFT_1_20_3, true));
|
||||
map(0x47, MINECRAFT_1_20_3, true),
|
||||
map(0x49, MINECRAFT_1_20_5, true));
|
||||
clientbound.register(TransactionPacket.class, TransactionPacket::new,
|
||||
map(0x32, MINECRAFT_1_7_2, true),
|
||||
map(0x11, MINECRAFT_1_9, true),
|
||||
@@ -218,7 +235,8 @@ public enum FallbackPacketRegistry {
|
||||
map(0x2F, MINECRAFT_1_19_1, true),
|
||||
map(0x2E, MINECRAFT_1_19_3, true),
|
||||
map(0x32, MINECRAFT_1_19_4, true),
|
||||
map(0x33, MINECRAFT_1_20_2, true));
|
||||
map(0x33, MINECRAFT_1_20_2, true),
|
||||
map(0x35, MINECRAFT_1_20_5, true));
|
||||
clientbound.register(DefaultSpawnPositionPacket.class, DefaultSpawnPositionPacket::new,
|
||||
map(0x05, MINECRAFT_1_7_2, true),
|
||||
map(0x43, MINECRAFT_1_9, true),
|
||||
@@ -234,7 +252,8 @@ public enum FallbackPacketRegistry {
|
||||
map(0x4C, MINECRAFT_1_19_3, true),
|
||||
map(0x50, MINECRAFT_1_19_4, true),
|
||||
map(0x52, MINECRAFT_1_20_2, true),
|
||||
map(0x54, MINECRAFT_1_20_3, true));
|
||||
map(0x54, MINECRAFT_1_20_3, true),
|
||||
map(0x56, MINECRAFT_1_20_5, true));
|
||||
clientbound.register(MapDataPacket.class, MapDataPacket::new,
|
||||
map(0x34, MINECRAFT_1_7_2, true),
|
||||
map(0x24, MINECRAFT_1_9, true),
|
||||
@@ -247,7 +266,8 @@ public enum FallbackPacketRegistry {
|
||||
map(0x26, MINECRAFT_1_19_1, true),
|
||||
map(0x25, MINECRAFT_1_19_3, true),
|
||||
map(0x29, MINECRAFT_1_19_4, true),
|
||||
map(0x2A, MINECRAFT_1_20_2, true));
|
||||
map(0x2A, MINECRAFT_1_20_2, true),
|
||||
map(0x2C, MINECRAFT_1_20_5, true));
|
||||
clientbound.register(SetSlotPacket.class, SetSlotPacket::new,
|
||||
map(0x2F, MINECRAFT_1_7_2, true),
|
||||
map(0x16, MINECRAFT_1_9, true),
|
||||
@@ -273,10 +293,29 @@ public enum FallbackPacketRegistry {
|
||||
map(0x60, MINECRAFT_1_19_3, false),
|
||||
map(0x64, MINECRAFT_1_19_4, false),
|
||||
map(0x67, MINECRAFT_1_20_2, false),
|
||||
map(0x69, MINECRAFT_1_20_3, false));
|
||||
map(0x69, MINECRAFT_1_20_3, false),
|
||||
map(0x6C, MINECRAFT_1_20_5, false));
|
||||
clientbound.register(GameEventPacket.class, GameEventPacket::new,
|
||||
map(0x20, MINECRAFT_1_20_3, true));
|
||||
map(0x20, MINECRAFT_1_20_3, true),
|
||||
map(0x22, MINECRAFT_1_20_5, true));
|
||||
clientbound.register(TransferPacket.class, TransferPacket::new,
|
||||
map(0x73, MINECRAFT_1_20_5, true));
|
||||
|
||||
serverbound.register(PaddleBoatPacket.class, PaddleBoatPacket::new,
|
||||
map(0x11, MINECRAFT_1_9, false),
|
||||
map(0x12, MINECRAFT_1_12, false),
|
||||
map(0x11, MINECRAFT_1_12_1, false),
|
||||
map(0x14, MINECRAFT_1_13, false),
|
||||
map(0x16, MINECRAFT_1_14, false),
|
||||
map(0x17, MINECRAFT_1_16, false),
|
||||
map(0x16, MINECRAFT_1_17, false),
|
||||
map(0x18, MINECRAFT_1_19, false),
|
||||
map(0x19, MINECRAFT_1_19_1, false),
|
||||
map(0x18, MINECRAFT_1_19_3, false),
|
||||
map(0x19, MINECRAFT_1_19_4, false),
|
||||
map(0x1B, MINECRAFT_1_20_2, false),
|
||||
map(0x1C, MINECRAFT_1_20_3, false),
|
||||
map(0x1F, MINECRAFT_1_20_5, false));
|
||||
serverbound.register(PlayerInputPacket.class, PlayerInputPacket::new,
|
||||
map(0x0C, MINECRAFT_1_7_2, false),
|
||||
map(0x15, MINECRAFT_1_9, false),
|
||||
@@ -290,7 +329,8 @@ public enum FallbackPacketRegistry {
|
||||
map(0x1E, MINECRAFT_1_19_3, false),
|
||||
map(0x1F, MINECRAFT_1_19_4, false),
|
||||
map(0x22, MINECRAFT_1_20_2, false),
|
||||
map(0x23, MINECRAFT_1_20_3, false));
|
||||
map(0x23, MINECRAFT_1_20_3, false),
|
||||
map(0x26, MINECRAFT_1_20_5, false));
|
||||
serverbound.register(UniversalChatPacket.class, UniversalChatPacket::new,
|
||||
map(0x01, MINECRAFT_1_7_2, false),
|
||||
map(0x02, MINECRAFT_1_9, false),
|
||||
@@ -299,7 +339,8 @@ public enum FallbackPacketRegistry {
|
||||
map(0x03, MINECRAFT_1_14, false),
|
||||
map(0x04, MINECRAFT_1_19, false),
|
||||
map(0x05, MINECRAFT_1_19_1, false),
|
||||
map(0x05, MINECRAFT_1_19_3, false));
|
||||
map(0x05, MINECRAFT_1_19_3, false),
|
||||
map(0x06, MINECRAFT_1_20_5, false));
|
||||
serverbound.register(KeepAlivePacket.class, KeepAlivePacket::new,
|
||||
map(0x00, MINECRAFT_1_7_2, false),
|
||||
map(0x0B, MINECRAFT_1_9, false),
|
||||
@@ -314,7 +355,8 @@ public enum FallbackPacketRegistry {
|
||||
map(0x11, MINECRAFT_1_19_3, false),
|
||||
map(0x12, MINECRAFT_1_19_4, false),
|
||||
map(0x14, MINECRAFT_1_20_2, false),
|
||||
map(0x15, MINECRAFT_1_20_3, false));
|
||||
map(0x15, MINECRAFT_1_20_3, false),
|
||||
map(0x18, MINECRAFT_1_20_5, false));
|
||||
serverbound.register(ClientSettingsPacket.class, ClientSettingsPacket::new,
|
||||
map(0x15, MINECRAFT_1_7_2, false),
|
||||
map(0x04, MINECRAFT_1_9, false),
|
||||
@@ -325,7 +367,8 @@ public enum FallbackPacketRegistry {
|
||||
map(0x08, MINECRAFT_1_19_1, false),
|
||||
map(0x07, MINECRAFT_1_19_3, false),
|
||||
map(0x08, MINECRAFT_1_19_4, false),
|
||||
map(0x09, MINECRAFT_1_20_2, false));
|
||||
map(0x09, MINECRAFT_1_20_2, false),
|
||||
map(0x0A, MINECRAFT_1_20_5, false));
|
||||
serverbound.register(PluginMessagePacket.class, PluginMessagePacket::new,
|
||||
map(0x17, MINECRAFT_1_7_2, false),
|
||||
map(0x09, MINECRAFT_1_9, false),
|
||||
@@ -353,7 +396,8 @@ public enum FallbackPacketRegistry {
|
||||
map(0x14, MINECRAFT_1_19_3, false),
|
||||
map(0x15, MINECRAFT_1_19_4, false),
|
||||
map(0x17, MINECRAFT_1_20_2, false),
|
||||
map(0x18, MINECRAFT_1_20_3, false));
|
||||
map(0x18, MINECRAFT_1_20_3, false),
|
||||
map(0x1B, MINECRAFT_1_20_5, false));
|
||||
serverbound.register(TeleportConfirmPacket.class, TeleportConfirmPacket::new,
|
||||
map(0x00, MINECRAFT_1_9, false));
|
||||
serverbound.register(PlayerPositionPacket.class, PlayerPositionPacket::new,
|
||||
@@ -370,7 +414,8 @@ public enum FallbackPacketRegistry {
|
||||
map(0x13, MINECRAFT_1_19_3, false),
|
||||
map(0x14, MINECRAFT_1_19_4, false),
|
||||
map(0x16, MINECRAFT_1_20_2, false),
|
||||
map(0x17, MINECRAFT_1_20_3, false));
|
||||
map(0x17, MINECRAFT_1_20_3, false),
|
||||
map(0x1A, MINECRAFT_1_20_5, false));
|
||||
serverbound.register(PlayerTickPacket.class, PlayerTickPacket::new,
|
||||
map(0x03, MINECRAFT_1_7_2, false),
|
||||
map(0x0F, MINECRAFT_1_9, false),
|
||||
@@ -385,7 +430,8 @@ public enum FallbackPacketRegistry {
|
||||
map(0x16, MINECRAFT_1_19_3, false),
|
||||
map(0x17, MINECRAFT_1_19_4, false),
|
||||
map(0x19, MINECRAFT_1_20_2, false),
|
||||
map(0x1A, MINECRAFT_1_20_3, false));
|
||||
map(0x1A, MINECRAFT_1_20_3, false),
|
||||
map(0x1D, MINECRAFT_1_20_5, false));
|
||||
serverbound.register(TransactionPacket.class, TransactionPacket::new,
|
||||
map(0x0F, MINECRAFT_1_7_2, false),
|
||||
map(0x05, MINECRAFT_1_9, false),
|
||||
@@ -399,7 +445,8 @@ public enum FallbackPacketRegistry {
|
||||
map(0x1F, MINECRAFT_1_19_3, false),
|
||||
map(0x20, MINECRAFT_1_19_4, false),
|
||||
map(0x23, MINECRAFT_1_20_2, false),
|
||||
map(0x24, MINECRAFT_1_20_3, false));
|
||||
map(0x24, MINECRAFT_1_20_3, false),
|
||||
map(0x27, MINECRAFT_1_20_5, false));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
+13
-1
@@ -23,11 +23,13 @@ import xyz.jonesdev.sonar.api.config.SonarConfiguration;
|
||||
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.DimensionRegistry;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.map.CaptchaPreparer;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.config.FinishConfigurationPacket;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.config.RegistryDataPacket;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
|
||||
@UtilityClass
|
||||
@@ -36,12 +38,14 @@ public class FallbackPreparer {
|
||||
// Abilities
|
||||
public final FallbackPacket DEFAULT_ABILITIES = new ClientAbilitiesPacket(0x00, 0f, 0f);
|
||||
public final FallbackPacket CAPTCHA_ABILITIES = new ClientAbilitiesPacket(0x02, 0f, 0f);
|
||||
public final FallbackPacket CAPTCHA_ABILITIES_BEDROCK = new ClientAbilitiesPacket(0x06, 0f, 0f);
|
||||
// Chunks
|
||||
public final FallbackPacket EMPTY_CHUNK_DATA = new ChunkDataPacket(0, 0);
|
||||
// Finish Configuration
|
||||
public final FallbackPacket FINISH_CONFIGURATION = new FinishConfigurationPacket();
|
||||
// Synchronize Registry
|
||||
public final FallbackPacket REGISTRY_SYNC = new RegistryDataPacket();
|
||||
public final FallbackPacket REGISTRY_SYNC_LEGACY = new RegistryDataPacket(DimensionRegistry.CODEC_1_20, null, null);
|
||||
public final List<FallbackPacket> REGISTRY_SYNC_1_20_5 = RegistryDataPacket.of(DimensionRegistry.CODEC_1_20);
|
||||
// Keep Alive
|
||||
public final FallbackPacket CAPTCHA_KEEP_ALIVE = new KeepAlivePacket(0L);
|
||||
// Game Event (1.20.3+)
|
||||
@@ -56,6 +60,8 @@ public class FallbackPreparer {
|
||||
public FallbackPacket updateSectionBlocks;
|
||||
// Default Spawn Position
|
||||
public FallbackPacket dynamicSpawnPosition;
|
||||
// Transfer packet
|
||||
public FallbackPacket transferToOrigin;
|
||||
|
||||
// Disconnect messages
|
||||
public FallbackPacket blacklisted;
|
||||
@@ -64,6 +70,7 @@ public class FallbackPreparer {
|
||||
public FallbackPacket reconnectedTooFast;
|
||||
public FallbackPacket protocolBlacklisted;
|
||||
public FallbackPacket invalidUsername;
|
||||
public FallbackPacket tooManyOnlinePerIP;
|
||||
|
||||
// Vehicle
|
||||
public FallbackPacket removeEntities;
|
||||
@@ -137,6 +144,7 @@ public class FallbackPreparer {
|
||||
protocolBlacklisted = DisconnectPacket.create(Sonar.get().getConfig().getVerification().getProtocolBlacklisted(), true);
|
||||
reconnectedTooFast = DisconnectPacket.create(Sonar.get().getConfig().getVerification().getTooFastReconnect(), true);
|
||||
invalidUsername = DisconnectPacket.create(Sonar.get().getConfig().getVerification().getInvalidUsername(), true);
|
||||
tooManyOnlinePerIP = DisconnectPacket.create(Sonar.get().getConfig().getTooManyOnlinePerIp(), true);
|
||||
|
||||
// Precompute captcha answers
|
||||
if (Sonar.get().getConfig().getVerification().getMap().getTiming() != SonarConfiguration.Verification.Timing.NEVER
|
||||
@@ -148,6 +156,10 @@ public class FallbackPreparer {
|
||||
MAP_INFO_PREPARER.prepare();
|
||||
}
|
||||
|
||||
transferToOrigin = new TransferPacket(
|
||||
Sonar.get().getConfig().getVerification().getTransfer().getHost(),
|
||||
Sonar.get().getConfig().getVerification().getTransfer().getPort());
|
||||
|
||||
// Prepare packets for the vehicle check
|
||||
removeEntities = new RemoveEntitiesPacket(VEHICLE_ENTITY_ID);
|
||||
setPassengers = new SetPassengersPacket(VEHICLE_ENTITY_ID, PLAYER_ENTITY_ID);
|
||||
|
||||
+1
@@ -78,6 +78,7 @@ public enum BlockType {
|
||||
return 10225;
|
||||
case MINECRAFT_1_20_2:
|
||||
case MINECRAFT_1_20_3:
|
||||
case MINECRAFT_1_20_5:
|
||||
return 10366;
|
||||
}
|
||||
}, 1D);
|
||||
|
||||
+2
@@ -72,6 +72,8 @@ public enum ItemType {
|
||||
return 941;
|
||||
case MINECRAFT_1_20_3:
|
||||
return 979;
|
||||
case MINECRAFT_1_20_5:
|
||||
return 982;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
+54
-5
@@ -18,26 +18,75 @@
|
||||
package xyz.jonesdev.sonar.common.fallback.protocol.packets.config;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import lombok.Getter;
|
||||
import lombok.ToString;
|
||||
import lombok.*;
|
||||
import net.kyori.adventure.nbt.BinaryTag;
|
||||
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.DimensionRegistry;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.writeNamelessCompoundTag;
|
||||
import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.writeString;
|
||||
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.writeVarInt;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public final class RegistryDataPacket implements FallbackPacket {
|
||||
private CompoundBinaryTag tag;
|
||||
private String type;
|
||||
private List<RegistryDataPacket.Bundle> bundles;
|
||||
|
||||
@Override
|
||||
public void encode(final @NotNull ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
|
||||
writeNamelessCompoundTag(byteBuf, DimensionRegistry.CODEC_1_20);
|
||||
public void encode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
|
||||
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_20_5) < 0) {
|
||||
writeNamelessCompoundTag(byteBuf, tag);
|
||||
} else {
|
||||
writeString(byteBuf, type);
|
||||
writeVarInt(byteBuf, bundles.size());
|
||||
|
||||
for (final RegistryDataPacket.Bundle bundle : bundles) {
|
||||
writeString(byteBuf, bundle.getName());
|
||||
// Write the bundle tag
|
||||
final CompoundBinaryTag tag = bundle.getTag();
|
||||
if (tag != null) {
|
||||
byteBuf.writeBoolean(true);
|
||||
writeNamelessCompoundTag(byteBuf, tag);
|
||||
} else {
|
||||
byteBuf.writeBoolean(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
public static @NotNull List<FallbackPacket> of(final @NotNull CompoundBinaryTag rootTag) {
|
||||
final List<FallbackPacket> packets = new ArrayList<>();
|
||||
for (final String type : rootTag.keySet()) {
|
||||
final ArrayList<RegistryDataPacket.Bundle> bundles = new ArrayList<>();
|
||||
for (final BinaryTag binaryTag : rootTag.getCompound(type).getList("value")) {
|
||||
final CompoundBinaryTag tag = (CompoundBinaryTag) binaryTag;
|
||||
bundles.add(new Bundle(tag.getString("name"), tag.getCompound("element")));
|
||||
}
|
||||
packets.add(new RegistryDataPacket(rootTag, type, bundles));
|
||||
}
|
||||
return packets;
|
||||
}
|
||||
|
||||
@Data
|
||||
@Value
|
||||
public static class Bundle {
|
||||
@NotNull String name;
|
||||
// In the protocol, it is able to be null
|
||||
@Nullable CompoundBinaryTag tag;
|
||||
}
|
||||
}
|
||||
+3
@@ -58,6 +58,9 @@ public final class LoginSuccessPacket implements FallbackPacket {
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_19) >= 0) {
|
||||
writeVarInt(byteBuf, 0); // properties
|
||||
}
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_20_5) >= 0) {
|
||||
byteBuf.writeBoolean(false); // should authenticate
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+1
-1
@@ -30,7 +30,7 @@ import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
|
||||
@ToString
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class ClientAbilitiesPacket implements FallbackPacket {
|
||||
public final class ClientAbilitiesPacket implements FallbackPacket {
|
||||
private int encodedFlags;
|
||||
private float flySpeed, walkSpeed;
|
||||
|
||||
|
||||
+24
-1
@@ -210,7 +210,7 @@ public final class JoinGamePacket implements FallbackPacket {
|
||||
return;
|
||||
}
|
||||
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_20_2) >= 0) {
|
||||
if (protocolVersion.inBetween(MINECRAFT_1_20_2, MINECRAFT_1_20_3)) {
|
||||
byteBuf.writeBoolean(isHardcore);
|
||||
writeStringArray(byteBuf, levelNames);
|
||||
writeVarInt(byteBuf, 0); // max players
|
||||
@@ -228,6 +228,29 @@ public final class JoinGamePacket implements FallbackPacket {
|
||||
byteBuf.writeBoolean(false); // flat
|
||||
byteBuf.writeBoolean(false); // no last death location
|
||||
writeVarInt(byteBuf, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_20_5) >= 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
|
||||
// It depends on the dimension data we send in RegistryData. If we only send one dimension. It should be 0.
|
||||
writeVarInt(byteBuf, 0);
|
||||
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); // pearl cooldown
|
||||
byteBuf.writeBoolean(false); // secure profile
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.common.fallback.protocol.packets.play;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.ToString;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public final class PaddleBoatPacket implements FallbackPacket {
|
||||
private boolean leftPaddle, rightPaddle;
|
||||
|
||||
@Override
|
||||
public void encode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
|
||||
leftPaddle = byteBuf.readBoolean();
|
||||
rightPaddle = byteBuf.readBoolean();
|
||||
}
|
||||
}
|
||||
+23
-10
@@ -38,10 +38,8 @@ import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.writeVarInt;
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public final class SetSlotPacket implements FallbackPacket {
|
||||
private int windowId;
|
||||
private int slot;
|
||||
private int count;
|
||||
private int data;
|
||||
private int itemId;
|
||||
private CompoundBinaryTag compoundBinaryTag;
|
||||
|
||||
@@ -51,7 +49,7 @@ public final class SetSlotPacket implements FallbackPacket {
|
||||
|
||||
@Override
|
||||
public void encode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
|
||||
byteBuf.writeByte(windowId);
|
||||
byteBuf.writeByte(0); // windowId
|
||||
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_17_1) >= 0) {
|
||||
writeVarInt(byteBuf, 0);
|
||||
@@ -59,31 +57,46 @@ public final class SetSlotPacket implements FallbackPacket {
|
||||
|
||||
byteBuf.writeShort(slot);
|
||||
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_13_2) >= 0) {
|
||||
if (protocolVersion.inBetween(MINECRAFT_1_13_2, MINECRAFT_1_20_3)) {
|
||||
byteBuf.writeBoolean(true);
|
||||
}
|
||||
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_20_5) >= 0) {
|
||||
writeVarInt(byteBuf, count);
|
||||
}
|
||||
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_13_2) < 0) {
|
||||
byteBuf.writeShort(itemId);
|
||||
} else {
|
||||
writeVarInt(byteBuf, itemId);
|
||||
}
|
||||
byteBuf.writeByte(count);
|
||||
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_20_5) < 0) {
|
||||
byteBuf.writeByte(count);
|
||||
}
|
||||
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_13) < 0) {
|
||||
byteBuf.writeShort(data);
|
||||
byteBuf.writeShort(0); // data
|
||||
}
|
||||
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_17) < 0) {
|
||||
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_8) < 0) {
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_8) < 0) {
|
||||
byteBuf.writeShort(-1);
|
||||
} else {
|
||||
byteBuf.writeByte(0);
|
||||
}
|
||||
} else {
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_20_2) >= 0) {
|
||||
writeNamelessCompoundTag(byteBuf, compoundBinaryTag);
|
||||
} else {
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_20_2) < 0) {
|
||||
writeCompoundTag(byteBuf, compoundBinaryTag);
|
||||
} else if (protocolVersion.compareTo(MINECRAFT_1_20_5) < 0) {
|
||||
writeNamelessCompoundTag(byteBuf, compoundBinaryTag);
|
||||
} else { // 1.20.5
|
||||
// component
|
||||
writeVarInt(byteBuf, 1); // component count to add
|
||||
writeVarInt(byteBuf, 0); // component count to remove
|
||||
// single VarInt component
|
||||
writeVarInt(byteBuf, 26); // map component
|
||||
writeVarInt(byteBuf, 0); // map id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Copyright (C) 2024 jones
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.common.fallback.protocol.packets.play;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.ToString;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
|
||||
|
||||
import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.writeString;
|
||||
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.writeVarInt;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public final class TransferPacket implements FallbackPacket {
|
||||
private String host;
|
||||
private int port;
|
||||
|
||||
@Override
|
||||
public void encode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
|
||||
writeString(byteBuf, host);
|
||||
writeVarInt(byteBuf, port);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
||||
+2
@@ -77,6 +77,8 @@ public enum EntityType {
|
||||
case MINECRAFT_1_20_2:
|
||||
case MINECRAFT_1_20_3:
|
||||
return 9;
|
||||
case MINECRAFT_1_20_5:
|
||||
return 10;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.common.service;
|
||||
|
||||
public final class SonarServiceThread extends Thread {
|
||||
private final long delay;
|
||||
private final Runnable runnable;
|
||||
|
||||
public SonarServiceThread(final String name, final long delay, final Runnable runnable) {
|
||||
setName(name);
|
||||
this.delay = delay;
|
||||
this.runnable = runnable;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
while (!isInterrupted()) {
|
||||
runnable.run();
|
||||
// Sleep thread by specified delay
|
||||
Thread.sleep(delay);
|
||||
}
|
||||
} catch (InterruptedException interruptedException) {
|
||||
//
|
||||
}
|
||||
}
|
||||
}
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.common.service;
|
||||
|
||||
import lombok.experimental.UtilityClass;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.common.statistics.CachedBandwidthStatistics;
|
||||
|
||||
@UtilityClass
|
||||
public final class SonarServiceThreadManager {
|
||||
private static final SonarServiceThread VERBOSE = new SonarServiceThread("sonar-verbose-thread", 250L,
|
||||
() -> {
|
||||
// Make sure to clean up the cached statistics
|
||||
// since we don't want to display wrong values.
|
||||
Sonar.get().getStatistics().cleanUpCache();
|
||||
// Update the attack tracker
|
||||
Sonar.get().getAttackTracker().checkIfUnderAttack();
|
||||
// Update the action bar verbose
|
||||
Sonar.get().getVerboseHandler().update();
|
||||
});
|
||||
private static final SonarServiceThread FALLBACK_QUEUE = new SonarServiceThread("sonar-fallback-queue-thread", 1000L,
|
||||
() -> Sonar.get().getFallback().getQueue().poll());
|
||||
private static final SonarServiceThread STATISTICS = new SonarServiceThread("sonar-statistics-thread", 1000L,
|
||||
() -> {
|
||||
// Clean up bandwidth statistics
|
||||
CachedBandwidthStatistics.reset();
|
||||
// Clean up the cache of rate-limited IPs
|
||||
Sonar.get().getFallback().getRatelimiter().cleanUpCache();
|
||||
});
|
||||
|
||||
public void start() {
|
||||
VERBOSE.start();
|
||||
FALLBACK_QUEUE.start();
|
||||
STATISTICS.start();
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
VERBOSE.interrupt();
|
||||
FALLBACK_QUEUE.interrupt();
|
||||
STATISTICS.interrupt();
|
||||
}
|
||||
}
|
||||
+3
-4
@@ -47,10 +47,9 @@ public enum CachedBandwidthStatistics {
|
||||
* (per-second) value and resets the current value.
|
||||
* This method is called every second.
|
||||
*/
|
||||
public static synchronized void reset() {
|
||||
for (final CachedBandwidthStatistics value : values()) {
|
||||
value.cacheAndReset();
|
||||
}
|
||||
public static void reset() {
|
||||
INCOMING.cacheAndReset();
|
||||
OUTGOING.cacheAndReset();
|
||||
}
|
||||
|
||||
private void cacheAndReset() {
|
||||
|
||||
+1
-1
@@ -63,7 +63,7 @@ public final class GlobalSonarStatistics implements SonarStatistics {
|
||||
* Called every time the action bar updates to ensure fresh values
|
||||
*/
|
||||
@Override
|
||||
public synchronized void cleanUpCache() {
|
||||
public void cleanUpCache() {
|
||||
LOGINS_PER_SECOND.cleanUp();
|
||||
CONNECTIONS_PER_SECOND.cleanUp();
|
||||
// Also clean up the blacklist
|
||||
|
||||
+1
-1
@@ -60,7 +60,7 @@ public final class BlacklistCommand extends Subcommand {
|
||||
}
|
||||
|
||||
// Display a warning if the IP is verified but being added to the blacklist
|
||||
if (SONAR.getVerifiedPlayerController().has(inetAddress)) {
|
||||
if (SONAR.getVerifiedPlayerController().has(inetAddress.toString())) {
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getBlacklistAddWarning()
|
||||
.replace("%ip%", rawInetAddress));
|
||||
}
|
||||
|
||||
+48
@@ -23,8 +23,10 @@ import xyz.jonesdev.sonar.api.command.CommandInvocation;
|
||||
import xyz.jonesdev.sonar.api.command.subcommand.Subcommand;
|
||||
import xyz.jonesdev.sonar.api.command.subcommand.SubcommandInfo;
|
||||
import xyz.jonesdev.sonar.api.command.subcommand.argument.Argument;
|
||||
import xyz.jonesdev.sonar.api.model.VerifiedPlayer;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Queue;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
@@ -35,6 +37,7 @@ import java.util.concurrent.LinkedBlockingQueue;
|
||||
arguments = {
|
||||
@Argument("history"),
|
||||
@Argument("remove"),
|
||||
@Argument("add"),
|
||||
@Argument("clear"),
|
||||
@Argument("size")
|
||||
}
|
||||
@@ -107,6 +110,51 @@ public final class VerifiedCommand extends Subcommand {
|
||||
break;
|
||||
}
|
||||
|
||||
case "add": {
|
||||
if (invocation.getRawArguments().length <= 3) {
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getIncorrectCommandUsage()
|
||||
.replace("%usage%", "verified add <IP address> <UUID/username>"));
|
||||
return;
|
||||
}
|
||||
|
||||
final String rawInetAddress = invocation.getRawArguments()[2];
|
||||
|
||||
// Make sure we aren't currently locking the IP address to avoid a double I/O operation
|
||||
if (LOCK.contains(rawInetAddress)) {
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getVerifiedBlocked());
|
||||
return;
|
||||
}
|
||||
|
||||
final InetAddress inetAddress = getInetAddressIfValid(invocation.getSender(), rawInetAddress);
|
||||
// Make sure the given IP address is valid
|
||||
if (inetAddress == null) return;
|
||||
|
||||
// Try to parse the UUID (from the username, if needed)
|
||||
final String rawUUID = invocation.getRawArguments()[3];
|
||||
final UUID uuid = rawUUID.length() == 36 ? UUID.fromString(rawUUID)
|
||||
: UUID.nameUUIDFromBytes(("OfflinePlayer:" + rawUUID).getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
// Make sure the player is verified already
|
||||
if (SONAR.getVerifiedPlayerController().has(inetAddress.toString(), uuid)) {
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getVerifiedAlready());
|
||||
return;
|
||||
}
|
||||
|
||||
// Lock the IP address
|
||||
// Make sure we don't accidentally run 2 operations at the same time
|
||||
LOCK.add(rawInetAddress);
|
||||
// Add verified player to the database
|
||||
final long timestamp = System.currentTimeMillis();
|
||||
final VerifiedPlayer verifiedPlayer = new VerifiedPlayer(inetAddress.toString(), uuid, timestamp);
|
||||
Sonar.get().getVerifiedPlayerController().add(verifiedPlayer);
|
||||
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getVerifiedAdd()
|
||||
.replace("%ip%", rawInetAddress));
|
||||
// Unlock the IP address
|
||||
LOCK.remove(rawInetAddress);
|
||||
break;
|
||||
}
|
||||
|
||||
case "clear": {
|
||||
final int verified = SONAR.getVerifiedPlayerController().estimatedSize();
|
||||
|
||||
|
||||
+1
-1
@@ -98,7 +98,7 @@ public class ProtocolUtil {
|
||||
return str;
|
||||
}
|
||||
|
||||
public static void writeString(final ByteBuf byteBuf, final CharSequence str) {
|
||||
public static void writeString(final ByteBuf byteBuf, final @NotNull CharSequence str) {
|
||||
final int size = ByteBufUtil.utf8Bytes(str);
|
||||
VarIntUtil.writeVarInt(byteBuf, size);
|
||||
byteBuf.writeCharSequence(str, StandardCharsets.UTF_8);
|
||||
|
||||
@@ -56,9 +56,9 @@ database:
|
||||
maximum-age: 5
|
||||
|
||||
# Každý nový login bude přidán do řady kvuli prevenci spam útoků
|
||||
# Řada je aktulizována každých 500 milisekund (10 ticků)
|
||||
# Řada je aktulizována každých 1 sekund (20 ticků)
|
||||
queue:
|
||||
# Maximum součastných aktualizací řady za 500 milisekund
|
||||
# Maximum součastných aktualizací řady za 1 sekund
|
||||
max-polls: 30
|
||||
|
||||
# Každý připojený hráč bude přepojen do
|
||||
@@ -89,12 +89,12 @@ verification:
|
||||
# - ADVENTURE: všechny komponenty UI jsou zobrazeny
|
||||
gamemode: ADVENTURE
|
||||
|
||||
# Checks if the client sends proper packets when mounting a vehicle
|
||||
# Kontroluje, zda klient odesílá správné pakety při montáži vozidla
|
||||
vehicle:
|
||||
# Possible types: ALWAYS, DURING_ATTACK, NEVER
|
||||
# - ALWAYS: New players will always be mounted on a vehicle
|
||||
# - DURING_ATTACK: New players will only be mounted on a vehicle during an attack
|
||||
# - NEVER: New players will never be mounted on a vehicle (Recommended)
|
||||
# Typy: ALWAYS, DURING_ATTACK, NEVER
|
||||
# - ALWAYS: Noví hráči budou vždy namontováni na vozidle (Doporučeno)
|
||||
# - DURING_ATTACK: Noví hráči budou namontováni na vozidlo pouze během útoku
|
||||
# - NEVER: Noví hráči nebudou nikdy namontováni na vozidlo
|
||||
timing: ALWAYS
|
||||
|
||||
# Nechá hráče verifikovat pomocí kódu na mapě ve hře
|
||||
@@ -105,21 +105,21 @@ verification:
|
||||
# - DURING_ATTACK: Noví hráči dostanou map captchu jenom při útoku
|
||||
# - NEVER: Noví hráči nikdy nedostanou map CAPTCHU (Doporučeno)
|
||||
timing: NEVER
|
||||
# Options for the effects on the image shown to the player
|
||||
# Možnosti efektů na obrázek zobrazený přehrávači
|
||||
effects:
|
||||
# Adds a glow effect to the image
|
||||
# Přidá do obrázku efekt záře
|
||||
flare: true
|
||||
# Adds random lines to the image
|
||||
# Přidá do obrázku náhodné čáry
|
||||
scratches: true
|
||||
# Applies a ripple (sine) distortion filter
|
||||
# Přidá filtr zvlnění (sinus) zkreslení
|
||||
ripple: true
|
||||
# Applies a horizontal distortion filter
|
||||
# Přidá horizontální filtr zkreslení
|
||||
smear: true
|
||||
# Adds a bit of blurriness/distortion to the image
|
||||
# Dodává obrazu trochu rozmazání/zkreslení
|
||||
pinch: true
|
||||
# Applies color correction (color saturation)
|
||||
# Použije korekci barev (sytost barev)
|
||||
saturation: 0.15
|
||||
# Applies a horizontal triangular ripple filter
|
||||
# Přidá horizontální trojúhelníkový filtr zvlnění
|
||||
distortion: 2
|
||||
# Kolik odpovědí by měl Sonar předgenerovat?
|
||||
# Tento úkol běží souběžně v pozadí
|
||||
@@ -130,7 +130,7 @@ verification:
|
||||
# Kolikrát musí hráč selhat CAPTCHU před celkovém selhání verifikace?
|
||||
max-tries: 3
|
||||
# Znaky (čísla a písmena), které jsou povoleny k zobrazování v CAPTCHE
|
||||
# It is not recommended to add numbers or letters that might look like one another
|
||||
# Nedoporučuje se přidávat čísla nebo písmena, která by se mohla navzájem podobat
|
||||
dictionary: '1235689'
|
||||
# Jaké typy fontů by měl Sonar používat pro map CAPTCHU?
|
||||
font-names:
|
||||
@@ -148,9 +148,9 @@ verification:
|
||||
# Maximální délka "client brand" při verifikaci
|
||||
max-length: 64
|
||||
|
||||
# Regex for validování jmen při verifikaci
|
||||
# Regex pro validování jmen při verifikaci
|
||||
valid-name-regex: ^[a-zA-Z0-9_]+$
|
||||
# Regex for validaci "client locale" při verifikaci
|
||||
# Regex pro validaci "client locale" při verifikaci
|
||||
valid-locale-regex: ^[a-zA-Z_]+$
|
||||
# Maximální odezva hráče aby byl odpojen
|
||||
# (Hodnota udávaná v milisekundách: 1 sekunda = 1000 milisekund)
|
||||
@@ -158,6 +158,24 @@ verification:
|
||||
# Maximální počet login packetů, který musí hřáč poslat aby byl vyhozen
|
||||
max-login-packets: 256
|
||||
|
||||
# Je-li povoleno, přehrávač bude přenesen zpět na původní server
|
||||
# po úspěšném absolvování ověření robota.
|
||||
# Tuto funkci představil Mojang ve verzi Minecraft 1.20.5
|
||||
transfer:
|
||||
# Měl by Sonar přenést hráče na původní server (místo toho, aby je kopl)?
|
||||
# Aby to fungovalo, musíte tuto funkci povolit v konfiguraci vašeho serveru
|
||||
# Kromě toho možná budete chtít snížit množství omezení rychlosti přihlášení
|
||||
# provádí Velocity nebo jiné proxy/pluginy, protože to může zabránit
|
||||
# hráč nebyl převeden správně.
|
||||
enabled: false
|
||||
# Na který server by měl Sonar přenést přehrávač, když projde ověřením?
|
||||
# Zadejte IP serveru, který hráči používají k běžnému připojení k vašemu serveru.
|
||||
# Například můžete zadat "mc.hypixel.net" nebo přímou IP jako "1.1.1.1"
|
||||
destination-host: "play.my-server.com"
|
||||
# Jaký port by měl Sonar použít při přenosu přehrávače na původní server?
|
||||
# Pokud váš server nepotřebuje port pro připojení, můžete toto ponechat jako 25565.
|
||||
destination-port: 25565
|
||||
|
||||
# Měl by Sonar posílat nové verifikace?
|
||||
log-connections: true
|
||||
# Měl by Sonar posílat nové verifikace při útoku?
|
||||
@@ -165,13 +183,17 @@ verification:
|
||||
# Měl by Sonar posílat každý jeden pohyb při verifikaci?
|
||||
# Toto nastavení není doporučeno pro servry v produkci, ale může být nápomocné při vyhledávaní chyb
|
||||
debug-xyz-positions: false
|
||||
# Měl by Sonar také kontrolovat hráče Geyser (Bedrock)?
|
||||
# Tato funkce je experimentální a může způsobit problémy
|
||||
# Pokud je toto zakázáno, hráči Bedrock budou přeskočeni
|
||||
check-geyser-players: true
|
||||
# Množství času který musí uběhnout aby hráč byl odpojen
|
||||
# (Hodnota udávaná v milisekundách: 1 sekunda = 1000 milisekund)
|
||||
read-timeout: 3500
|
||||
# How long should a player wait before reconnecting during verification Jak dlouho by měl hráč
|
||||
read-timeout: 8000
|
||||
# Jak dlouho by měl hráč čekat, než se při ověřování znovu připojí
|
||||
# (Hodnota udávaná v milisekundách: 1 sekunda = 1000 milisekund)
|
||||
rejoin-delay: 8000
|
||||
# How long should Sonar remember the amount of failed verifications for a player?
|
||||
# Jak dlouho by si měl Sonar pamatovat počet neúspěšných ověření hráče?
|
||||
# (Hodnota udávaná v milisekundách: 1 sekunda = 1000 milisekund)
|
||||
remember-time: 120000
|
||||
# Jak dlouho by měla být IP adresa zakázána k připojení po selhání verifikace mockrát?
|
||||
|
||||
@@ -53,9 +53,9 @@ database:
|
||||
maximum-age: 5
|
||||
|
||||
# Jede neue Login-Anfrage wird in eine Warteschlange eingereiht, um Spam-Beitrittsangriffe zu vermeiden
|
||||
# Die Warteschlange wird alle 500 Millisekunden (10 Ticks) aktualisiert
|
||||
# Die Warteschlange wird alle 1 Sekunde (20 Ticks) aktualisiert
|
||||
queue:
|
||||
# Maximale Anzahl an gleichzeitigen Abfragen der Warteschlange pro 500 Millisekunden
|
||||
# Maximale Anzahl an gleichzeitigen Abfragen der Warteschlange pro 1 Sekunde
|
||||
max-polls: 30
|
||||
|
||||
# Jeder neue Spieler, der das erste Mal beitritt, wird zu
|
||||
@@ -86,12 +86,12 @@ verification:
|
||||
# - ADVENTURE: alle UI-Komponenten sind sichtbar
|
||||
gamemode: ADVENTURE
|
||||
|
||||
# Checks if the client sends proper packets when mounting a vehicle
|
||||
# Überprüft, ob der Spieler beim Einsteigen in ein Boot die richtigen Pakete sendet
|
||||
vehicle:
|
||||
# Possible types: ALWAYS, DURING_ATTACK, NEVER
|
||||
# - ALWAYS: New players will always be mounted on a vehicle
|
||||
# - DURING_ATTACK: New players will only be mounted on a vehicle during an attack
|
||||
# - NEVER: New players will never be mounted on a vehicle (Recommended)
|
||||
# Mögliche Typen: ALWAYS, DURING_ATTACK, NEVER
|
||||
# - ALWAYS: Neue Spieler werden immer auf einem Fahrzeug montiert (Empfohlen)
|
||||
# - DURING_ATTACK: Neue Spieler werden nur während eines Angriffs auf ein Fahrzeug gesetzt
|
||||
# - NEVER: Neue Spieler werden niemals in ein Fahrzeug eingebaut
|
||||
timing: ALWAYS
|
||||
|
||||
# Lässt den Spieler einen Code von einer virtuellen Karte im Chat eingeben
|
||||
@@ -102,21 +102,21 @@ verification:
|
||||
# - DURING_ATTACK: Neue Spieler erhalten nur während eines Angriffs ein CAPTCHA
|
||||
# - NEVER: Neue Spieler erhalten nie ein CAPTCHA (Empfohlen)
|
||||
timing: NEVER
|
||||
# Options for the effects on the image shown to the player
|
||||
# Optionen für die Effekte auf das dem Player angezeigte Bild
|
||||
effects:
|
||||
# Adds a glow effect to the image
|
||||
# Fügt dem Bild einen Leuchteffekt hinzu
|
||||
flare: true
|
||||
# Adds random lines to the image
|
||||
# Fügt dem Bild zufällige Linien hinzu
|
||||
scratches: true
|
||||
# Applies a ripple (sine) distortion filter
|
||||
# Wendet einen Sinus-Verzerrungsfilter an
|
||||
ripple: true
|
||||
# Applies a horizontal distortion filter
|
||||
# Wendet einen horizontalen Verzerrungsfilter an
|
||||
smear: true
|
||||
# Adds a bit of blurriness/distortion to the image
|
||||
# Fügt dem Bild etwas Unschärfe/Verzerrung hinzu
|
||||
pinch: true
|
||||
# Applies color correction (color saturation)
|
||||
# Wendet eine Farbkorrektur an (Farbsättigung)
|
||||
saturation: 0.15
|
||||
# Applies a horizontal triangular ripple filter
|
||||
# Wendet einen horizontalen dreieckigen Wellenfilter an
|
||||
distortion: 2
|
||||
# Wie viele Antworten soll Sonar vorbereiten?
|
||||
# Dieser Vorgang erfolgt asynchron im Hintergrund
|
||||
@@ -127,7 +127,7 @@ verification:
|
||||
# Wie oft muss ein Spieler das CAPTCHA nicht bestehen, bevor er die Überprüfung nicht besteht?
|
||||
max-tries: 3
|
||||
# Zeichen (Buchstaben und Zahlen), die im Antwort-CAPTCHA erscheinen dürfen
|
||||
# It is not recommended to add numbers or letters that might look like one another
|
||||
# Es wird nicht empfohlen, Zahlen oder Buchstaben hinzuzufügen, die einander ähneln könnten
|
||||
dictionary: '1235689'
|
||||
# Welche Schriftarten soll Sonar für die Map-CAPTCHA-Codes verwenden?
|
||||
font-names:
|
||||
@@ -155,6 +155,24 @@ verification:
|
||||
# Maximale Anzahl an Login-Paketen, die der Spieler senden muss, um gekickt zu werden
|
||||
max-login-packets: 256
|
||||
|
||||
# Wenn aktiviert, wird der Player zurück zum Ursprungsserver gesendet
|
||||
# falls dieser die der Bot-Verifizierung erfolgreich besteht.
|
||||
# Diese Funktion wurde von Mojang in Minecraft Version 1.20.5 eingeführt
|
||||
transfer:
|
||||
# Sollte Sonar den Spieler auf den Ursprungsserver senden (anstatt ihn zu kicken)?
|
||||
# Damit dies funktioniert, müssen Sie die Funktion in der Konfiguration Ihres Servers aktivieren
|
||||
# Darüber hinaus möchten Sie möglicherweise die Begrenzung der Anmelderate reduzieren
|
||||
# wird von Velocity oder anderen Proxys/Plugins durchgeführt, da dies das verhindern könnte
|
||||
# Spieler konnte nicht korrekt übertragen werden.
|
||||
enabled: false
|
||||
# Auf welchen Server soll Sonar den Spieler übertragen, wenn die Verifizierung bestanden wurde?
|
||||
# Bitte geben Sie die Server-IP ein, die von Spielern verwendet wird, um sich zu verbinden.
|
||||
# Sie können beispielsweise "mc.hypixel.net" oder eine direkte IP wie "1.1.1.1" verwenden.
|
||||
destination-host: "play.my-server.com"
|
||||
# Welchen Port sollte Sonar verwenden, wenn der Spieler auf den Ursprungsserver gesendet wird?
|
||||
# Wenn Ihr Server keinen Port zum Herstellen einer Verbindung benötigt, können Sie diesen auf 25565 belassen.
|
||||
destination-port: 25565
|
||||
|
||||
# Soll Sonar neue Überprüfungsversuche protokollieren?
|
||||
log-connections: true
|
||||
# Soll Sonar neue Überprüfungsversuche während Angriffen protokollieren?
|
||||
@@ -162,9 +180,13 @@ verification:
|
||||
# Soll Sonar jede einzelne Bewegung/Positionsänderung während der Überprüfung protokollieren?
|
||||
# Dies wird für Produktivserver nicht empfohlen, kann aber beim Auffinden von Fehlern hilfreich sein.
|
||||
debug-xyz-positions: false
|
||||
# Sollte Sonar auch Geyser (Bedrock)-Spieler überprüfen?
|
||||
# Diese Funktion ist experimentell und kann Probleme verursachen
|
||||
# Wenn dies deaktiviert ist, werden Bedrock-Spieler übersprungen
|
||||
check-geyser-players: true
|
||||
# Zeit, die verstreichen muss, bevor ein Spieler ein Timeout erleidet
|
||||
# (Dieser Wert stellt die Zeit in Millisekunden dar: 1 Sekunde = 1000 Millisekunden)
|
||||
read-timeout: 3500
|
||||
read-timeout: 8000
|
||||
# Wie lange sollte ein Spieler warten, bevor er während der Überprüfung erneut verbinden kann
|
||||
# (Dieser Wert stellt die Zeit in Millisekunden dar: 1 Sekunde = 1000 Millisekunden)
|
||||
rejoin-delay: 8000
|
||||
|
||||
@@ -55,9 +55,9 @@ database:
|
||||
maximum-age: 5
|
||||
|
||||
# Every new login request will be queued to avoid spam join attacks
|
||||
# The queue is updated every 500 milliseconds (10 ticks)
|
||||
# The queue is updated every 1 second (20 ticks)
|
||||
queue:
|
||||
# Maximum number of concurrent queue polls per 500 milliseconds
|
||||
# Maximum number of concurrent queue polls per 1 second
|
||||
max-polls: 30
|
||||
|
||||
# Every new player that joins for the first time will be sent to
|
||||
@@ -91,9 +91,9 @@ verification:
|
||||
# Checks if the client sends proper packets when mounting a vehicle
|
||||
vehicle:
|
||||
# Possible types: ALWAYS, DURING_ATTACK, NEVER
|
||||
# - ALWAYS: New players will always be mounted on a vehicle
|
||||
# - ALWAYS: New players will always be mounted on a vehicle (Recommended)
|
||||
# - DURING_ATTACK: New players will only be mounted on a vehicle during an attack
|
||||
# - NEVER: New players will never be mounted on a vehicle (Recommended)
|
||||
# - NEVER: New players will never be mounted on a vehicle
|
||||
timing: ALWAYS
|
||||
|
||||
# Make the player type a code from a virtual map in chat
|
||||
@@ -157,6 +157,24 @@ verification:
|
||||
# Maximum number of login packets the player has to send in order to be kicked
|
||||
max-login-packets: 256
|
||||
|
||||
# If enabled, the player will be transferred back to the origin server
|
||||
# after successfully passing the bot verification.
|
||||
# This feature was introduced by Mojang in Minecraft version 1.20.5
|
||||
transfer:
|
||||
# Should Sonar transfer the player to the origin server (instead of kicking them)?
|
||||
# For this to work, you must enable the feature in your server's configuration
|
||||
# Additionally, you might want to reduce the amount of login rate-limiting
|
||||
# performed by Velocity or other proxies/plugins, as this might prevent the
|
||||
# player from being transferred correctly.
|
||||
enabled: false
|
||||
# Which server should Sonar transfer the player to when the verification is passed?
|
||||
# Please enter the server IP that is used by players to normally connect to your server.
|
||||
# For example, you can put "mc.hypixel.net" or a direct IP like "1.1.1.1"
|
||||
destination-host: "play.my-server.com"
|
||||
# Which port should Sonar use when transferring the player to the origin server?
|
||||
# If your server does not need a port to connect to, you can leave this as 25565.
|
||||
destination-port: 25565
|
||||
|
||||
# Should Sonar log new verification attempts?
|
||||
log-connections: true
|
||||
# Should Sonar log new verification attempts during attacks?
|
||||
@@ -164,9 +182,13 @@ verification:
|
||||
# Should Sonar log every single movement/position change during verification?
|
||||
# This is not recommended for production servers but can be helpful for spotting errors.
|
||||
debug-xyz-positions: false
|
||||
# Should Sonar also check Geyser (Bedrock) players?
|
||||
# This feature is experimental and might cause issues
|
||||
# If this is disabled, Bedrock players will be skipped
|
||||
check-geyser-players: true
|
||||
# Amount of time that has to pass before a player times out
|
||||
# (This value represents the time in milliseconds: 1 second = 1000 milliseconds)
|
||||
read-timeout: 3500
|
||||
read-timeout: 8000
|
||||
# How long should a player wait before reconnecting during verification
|
||||
# (This value represents the time in milliseconds: 1 second = 1000 milliseconds)
|
||||
rejoin-delay: 8000
|
||||
|
||||
@@ -55,9 +55,9 @@ database:
|
||||
maximum-age: 5
|
||||
|
||||
# ყოველი ახალი შესვლის მოთხოვნა იქნება რიგში, რათა თავიდან აიცილოთ სპამის მიერთების შეტევები
|
||||
# რიგი განახლდება ყოველ 500 მილიწამში (10 ტიკი)
|
||||
# რიგი განახლდება ყოველ 1000 მილიწამში (20 ტიკი)
|
||||
queue:
|
||||
# ახალი მოთამაშეების მაქსიმალური რაოდენობა, რომლებსაც შეუძლიათ შესვლა სერვერზე ყოველ 500 მილიწამში
|
||||
# ახალი მოთამაშეების მაქსიმალური რაოდენობა, რომლებსაც შეუძლიათ შესვლა სერვერზე ყოველ 1000 მილიწამში
|
||||
max-polls: 30
|
||||
|
||||
# ყოველი ახალი მოთამაშე, რომელიც პირველად შეუერთდება, გაიგზავნება
|
||||
@@ -91,9 +91,9 @@ verification:
|
||||
# Checks if the client sends proper packets when mounting a vehicle
|
||||
vehicle:
|
||||
# Possible types: ALWAYS, DURING_ATTACK, NEVER
|
||||
# - ALWAYS: New players will always be mounted on a vehicle
|
||||
# - ALWAYS: New players will always be mounted on a vehicle (Recommended)
|
||||
# - DURING_ATTACK: New players will only be mounted on a vehicle during an attack
|
||||
# - NEVER: New players will never be mounted on a vehicle (Recommended)
|
||||
# - NEVER: New players will never be mounted on a vehicle
|
||||
timing: ALWAYS
|
||||
|
||||
# დააწერინეთ მოთამაშეს კოდი ჩატში, რომელიც იქნება ნაჩვენები ვირტუალურ მაპზე, რომელიც ხელში ეჭირებად
|
||||
@@ -157,6 +157,24 @@ verification:
|
||||
# მაქსიმალური რაოდენობა ლოგინ პაკეტების, რომელიც მოთამაშემ უნდა გააგზავნოს სანამ გაიკიკება სერვერიდან
|
||||
max-login-packets: 256
|
||||
|
||||
# If enabled, the player will be transferred back to the origin server
|
||||
# after successfully passing the bot verification.
|
||||
# This feature was introduced by Mojang in Minecraft version 1.20.5
|
||||
transfer:
|
||||
# Should Sonar transfer the player to the origin server (instead of kicking them)?
|
||||
# For this to work, you must enable the feature in your server's configuration
|
||||
# Additionally, you might want to reduce the amount of login rate-limiting
|
||||
# performed by Velocity or other proxies/plugins, as this might prevent the
|
||||
# player from being transferred correctly.
|
||||
enabled: false
|
||||
# Which server should Sonar transfer the player to when the verification is passed?
|
||||
# Please enter the server IP that is used by players to normally connect to your server.
|
||||
# For example, you can put "mc.hypixel.net" or a direct IP like "1.1.1.1"
|
||||
destination-host: "play.my-server.com"
|
||||
# Which port should Sonar use when transferring the player to the origin server?
|
||||
# If your server does not need a port to connect to, you can leave this as 25565.
|
||||
destination-port: 25565
|
||||
|
||||
# ჩაიწეროს თუ არა Sonar-მა ახალი ვერიფიკაციის მცდელობები?
|
||||
log-connections: true
|
||||
# ჩაწეროს თუ არა Sonar-მა ახალი ვერიფიკაციის მცდელობები შეტევის დროს?
|
||||
@@ -164,9 +182,13 @@ verification:
|
||||
# ჩაიწეროს თუ არა Sonar-მა ყოველი მოძრაობა/პოზიცია ვერიფიკაციის დროს?
|
||||
# ეს არ არის რეკომენდირებული აქტიურ სერვერებისთვის, მაგრამ შეიძლება გამოსადეგი იყოს ცრუ პოსიტივების გამოსასწორებლად.
|
||||
debug-xyz-positions: false
|
||||
# Should Sonar also check Geyser (Bedrock) players?
|
||||
# This feature is experimental and might cause issues
|
||||
# If this is disabled, Bedrock players will be skipped
|
||||
check-geyser-players: true
|
||||
# დროის ოდენობა, რომელიც უნდა გავიდეს, სანამ მოთამაშე ტაიმაუტდება სერვერიდან.
|
||||
# (ეს არის ნაჩვენები მილიწამებში: 1 წამი = 1000 მილიწამი)
|
||||
read-timeout: 3500
|
||||
read-timeout: 8000
|
||||
# რამდენ ხანს უნდა დაელოდოს მოთამაშე ვერიფიკაციის დროს ხელახლა დაკავშირებამდე?
|
||||
# (ეს არის ნაჩვენები მილიწამებში: 1 წამი = 1000 მილიწამი)
|
||||
rejoin-delay: 8000
|
||||
|
||||
@@ -55,9 +55,9 @@ database:
|
||||
maximum-age: 5
|
||||
|
||||
# Каждый новый запрос на вход на сервер будет ставиться в очередь, чтобы избежать спам-атак.
|
||||
# Очередь обновляется каждые 500 миллисекунд (10 тиков)
|
||||
# Очередь обновляется каждые 1 секунд (20 тиков)
|
||||
queue:
|
||||
# Максимальное количество одновременных опросов очереди за 500 миллисекунд
|
||||
# Максимальное количество одновременных опросов очереди за 1 секунд
|
||||
max-polls: 30
|
||||
|
||||
# Каждый новый игрок, впервые присоединившийся на сервер, будет отправлен на
|
||||
@@ -91,9 +91,9 @@ verification:
|
||||
# Checks if the client sends proper packets when mounting a vehicle
|
||||
vehicle:
|
||||
# Possible types: ALWAYS, DURING_ATTACK, NEVER
|
||||
# - ALWAYS: New players will always be mounted on a vehicle
|
||||
# - ALWAYS: New players will always be mounted on a vehicle (Recommended)
|
||||
# - DURING_ATTACK: New players will only be mounted on a vehicle during an attack
|
||||
# - NEVER: New players will never be mounted on a vehicle (Recommended)
|
||||
# - NEVER: New players will never be mounted on a vehicle
|
||||
timing: ALWAYS
|
||||
|
||||
# Заставить игрока ввести код с картинки в чат
|
||||
@@ -157,6 +157,24 @@ verification:
|
||||
# Максимальное количество пакетов для входа на сервер, которое игрок должен отправить, чтобы быть выгнанным
|
||||
max-login-packets: 256
|
||||
|
||||
# If enabled, the player will be transferred back to the origin server
|
||||
# after successfully passing the bot verification.
|
||||
# This feature was introduced by Mojang in Minecraft version 1.20.5
|
||||
transfer:
|
||||
# Should Sonar transfer the player to the origin server (instead of kicking them)?
|
||||
# For this to work, you must enable the feature in your server's configuration
|
||||
# Additionally, you might want to reduce the amount of login rate-limiting
|
||||
# performed by Velocity or other proxies/plugins, as this might prevent the
|
||||
# player from being transferred correctly.
|
||||
enabled: false
|
||||
# Which server should Sonar transfer the player to when the verification is passed?
|
||||
# Please enter the server IP that is used by players to normally connect to your server.
|
||||
# For example, you can put "mc.hypixel.net" or a direct IP like "1.1.1.1"
|
||||
destination-host: "play.my-server.com"
|
||||
# Which port should Sonar use when transferring the player to the origin server?
|
||||
# If your server does not need a port to connect to, you can leave this as 25565.
|
||||
destination-port: 25565
|
||||
|
||||
# Должен ли Sonar регистрировать новые попытки проверки?
|
||||
log-connections: true
|
||||
# Должен ли Sonar регистрировать новые попытки проверки во время атак?
|
||||
@@ -164,9 +182,13 @@ verification:
|
||||
# Должен ли Sonar регистрировать каждое движение/изменение положения во время проверки?
|
||||
# Это не рекомендуется для рабочих серверов, но может быть полезно для выявления ошибок.
|
||||
debug-xyz-positions: false
|
||||
# Should Sonar also check Geyser (Bedrock) players?
|
||||
# This feature is experimental and might cause issues
|
||||
# If this is disabled, Bedrock players will be skipped
|
||||
check-geyser-players: true
|
||||
# Количество времени, которое должно пройти, прежде чем игрока кикнет.
|
||||
# (Это значение представляет время в миллисекундах: 1 секунда = 1000 миллисекунд)
|
||||
read-timeout: 3500
|
||||
read-timeout: 8000
|
||||
# Сколько времени игрок должен ждать перед повторным подключением во время проверки
|
||||
# (Это значение представляет время в миллисекундах: 1 секунда = 1000 миллисекунд)
|
||||
rejoin-delay: 8000
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
#
|
||||
# /$$$$$$
|
||||
# /$$__ $$
|
||||
# | $$ \__/ /$$$$$$ /$$$$$$$ /$$$$$$ /$$$$$$
|
||||
# | $$$$$$ /$$__ $$| $$__ $$ |____ $$ /$$__ $$
|
||||
# \____ $$| $$ \ $$| $$ \ $$ /$$$$$$$| $$ \__/
|
||||
# /$$ \ $$| $$ | $$| $$ | $$ /$$__ $$| $$
|
||||
# | $$$$$$/| $$$$$$/| $$ | $$| $$$$$$$| $$
|
||||
# \______/ \______/ |__/ |__/ \_______/|__/
|
||||
#
|
||||
# 有问题或需要帮助? https://jonesdev.xyz/discord
|
||||
# 源代码: https://github.com/jonesdevelopment/sonar
|
||||
# Copyright Jones Development and Sonar Contributors, licensed under GPL-3.0
|
||||
#
|
||||
|
||||
# 未归类的设置
|
||||
general:
|
||||
# Sonar 应该在控制台中打印玩家的 IP 地址吗?
|
||||
log-player-addresses: true
|
||||
# 具有相同 IP 地址的在线玩家的最大数量
|
||||
max-online-per-ip: 3
|
||||
|
||||
# 攻击检测器的敏感度设置
|
||||
# 只有当您知道自己在做什么的时候才编辑它
|
||||
attack-tracker:
|
||||
# 在一段时间内至少需要 x 个新玩家加入才满足触发条件.
|
||||
min-players-for-attack: 8
|
||||
# 至少经过多少毫秒后才可以解除遭受攻击模式
|
||||
# (该值为毫秒. 1秒等于1000毫秒.)
|
||||
min-attack-duration: 30000
|
||||
# 攻击检测器要重复检测到多少次攻击 才能确认服务器正在遭受攻击?
|
||||
# 该值作为缓冲区 以避免可能的误判
|
||||
min-attack-threshold: 2
|
||||
# 当从遭受攻击标记为解除攻击时 应该需要等待多久才能再次被标记为正在遭受攻击?
|
||||
# (该值为毫秒. 1秒等于1000毫秒.)
|
||||
attack-cooldown-delay: 3000
|
||||
|
||||
# 您可以让 Sonar 连接到数据库 以便从数据库中保存和读取已经过验证的 IP 和玩家.
|
||||
# 提示: IP 地址将以纯文本形式保存 请确保您的数据库安全!
|
||||
database:
|
||||
# 要连接到的数据库类型
|
||||
# 可用类型: NONE (不使用数据库), MYSQL, MARIADB
|
||||
type: NONE
|
||||
# 数据库的地址
|
||||
host: localhost
|
||||
# 数据库的端口
|
||||
port: 3306
|
||||
# 数据库名称
|
||||
name: sonar
|
||||
# 用于登录数据库的用户名
|
||||
username: ''
|
||||
# 用于登录数据库的密码
|
||||
password: ''
|
||||
# Sonar 应该在数据库中保留在白名单中的玩家最多多少天?
|
||||
maximum-age: 5
|
||||
|
||||
# 在遭到攻击时 每个新玩家的登录请求将排队 以避免大量连接同时进行验证
|
||||
# 队列将每1秒刷新一次
|
||||
queue:
|
||||
# 每秒最多处理多少个登录请求?
|
||||
max-polls: 30
|
||||
|
||||
# 每个需要验证的玩家都将被发送到一个轻量级的虚拟服务器来检查高级机器人
|
||||
verification:
|
||||
# Sonar 应该在什么时候验证玩家?
|
||||
# 可用类型: ALWAYS, DURING_ATTACK, NEVER
|
||||
# - ALWAYS: 玩家始终需要验证 直到他们通过验证为止
|
||||
# - DURING_ATTACK: 玩家仅在服务器疑似遭到攻击时验证
|
||||
# - NEVER: 玩家不会被验证
|
||||
timing: ALWAYS
|
||||
checks:
|
||||
# 检查玩家的下落移动是否遵循原版客户端.
|
||||
gravity:
|
||||
# Sonar 是否应该检查玩家的下落移动? (推荐启用)
|
||||
enabled: true
|
||||
# Sonar 是否应该生成方块平台以检查玩家下落与方块之间的碰撞? (推荐启用)
|
||||
check-collisions: true
|
||||
# 当玩家验证失败时 是否向他们发送验证码?
|
||||
captcha-on-fail: false
|
||||
# 玩家跌落在平台上多少tick后才能被算作站在平台上?
|
||||
max-movement-ticks: 8
|
||||
# 玩家在虚拟服务器中的游戏模式
|
||||
# 可用类型: SURVIVAL, CREATIVE, ADVENTURE
|
||||
# - SURVIVAL: 生存模式, 所有UI可见
|
||||
# - CREATIVE: 创造模式, 隐藏血量和饱食度
|
||||
# - ADVENTURE: 冒险模式, 所有UI可见
|
||||
gamemode: ADVENTURE
|
||||
|
||||
# 检查客户端坐在实体上时是否发送了正确的数据包
|
||||
vehicle:
|
||||
# 可用类型: ALWAYS, DURING_ATTACK, NEVER
|
||||
# - ALWAYS: 始终应用该检查
|
||||
# - DURING_ATTACK: 仅在疑似遭受攻击时应用该检查
|
||||
# - NEVER: 永远不应用该检查
|
||||
timing: ALWAYS
|
||||
|
||||
# 地图验证码
|
||||
map-captcha:
|
||||
# Sonar 应该在什么时候向玩家发送验证码?
|
||||
# 可用类型: ALWAYS, DURING_ATTACK, NEVER
|
||||
# - ALWAYS: 始终应用该检查
|
||||
# - DURING_ATTACK: 仅在疑似遭受攻击时应用该检查
|
||||
# - NEVER: 永远不应用该检查
|
||||
timing: NEVER
|
||||
# 图像效果选项 (用于混淆自动图像识别)
|
||||
effects:
|
||||
# 是否添加局部发光效果
|
||||
flare: true
|
||||
# 是否添加随机的线条
|
||||
scratches: true
|
||||
# 是否应用波纹失真效果
|
||||
ripple: true
|
||||
# 是否应用纵向畸变滤镜
|
||||
smear: true
|
||||
# 是否添加一些失真
|
||||
pinch: true
|
||||
# 图像的色彩饱和度
|
||||
saturation: 0.15
|
||||
# 水平三角形纹波滤镜
|
||||
distortion: 2
|
||||
# Sonar 应该一次准备多少个验证码?
|
||||
precompute: 500
|
||||
# Sonar 应该给玩家多少时间完成验证码? (该值为毫秒. 1秒等于1000毫秒.)
|
||||
max-duration: 45000
|
||||
# 玩家最多能输入多少次错误的验证码?
|
||||
max-tries: 3
|
||||
# 允许出现在验证码答案中的字符
|
||||
# 不建议添加可能看起来彼此相似的数字或字母
|
||||
dictionary: '1235689'
|
||||
# Sonar 应该为验证码使用哪些字体?
|
||||
font-names:
|
||||
- Dialog
|
||||
- DialogInput
|
||||
- SansSerif
|
||||
- Monospaced
|
||||
|
||||
# 检查玩家是否正在向服务器发送有效的客户端标识符
|
||||
client-brand:
|
||||
# 是否启用此检查? (建议启用)
|
||||
enabled: true
|
||||
# 用于检查客户端标识符的正则表达式
|
||||
valid-regex: ^[!-~ ]+$
|
||||
# 客户端标识符的最长长度限制
|
||||
max-length: 64
|
||||
|
||||
# 用于检查用户名是否有效的正则表达式
|
||||
valid-name-regex: ^[a-zA-Z0-9_]+$
|
||||
# 用于检查客户端语言(例如 zh_CN)的正则表达式
|
||||
valid-locale-regex: ^[a-zA-Z_]+$
|
||||
# 玩家必须在多少毫秒内回复Ping?
|
||||
max-ping: 10000
|
||||
# 玩家最多能发送多少个数据包用于登录?
|
||||
max-login-packets: 256
|
||||
|
||||
# 开启后 玩家将在验证成功后被重定向到指定的服务器
|
||||
# 该功能由 Mojang 在 Minecraft 1.20.5 版本中引入
|
||||
transfer:
|
||||
# Sonar 是否应该将玩家重定向到指定的服务器而不是踢出玩家?
|
||||
# 为此您必须在服务器软件的配置文件中允许接受来自transfer的连接.
|
||||
# 此外 您可能希望减少 Velocity 或其它服务器软件/插件的登录速率限制
|
||||
# 因为这可能会阻止玩家快速地重新连接到服务器
|
||||
enabled: false
|
||||
# 当玩家通过验证后, Sonar 应该将玩家转移到哪个服务器
|
||||
# 请输入玩家正常连接服务器时使用的服务器IP或域名
|
||||
# 例如使用域名("mc.hypixel.net") 或使用 IP 地址("1.1.1.1").
|
||||
destination-host: "play.my-server.com"
|
||||
# Sonar 应该告诉玩家通过以上域名的什么端口来连接到服务器?
|
||||
# 如果服务器地址不包含端口 请保留25565.
|
||||
destination-port: 25565
|
||||
|
||||
# 当 Sonar 尝试验证玩家时 是否在控制台中打印它?
|
||||
log-connections: true
|
||||
# Sonar 是否应该记录在攻击期间内的验证会话?
|
||||
log-during-attack: false
|
||||
# Sonar 是否应该记录验证过程中玩家的移动位置的变化?
|
||||
# 有助于发现错误 但不建议在非调试环境下的服务器启用该选项.
|
||||
debug-xyz-positions: false
|
||||
# Sonar 是否应该检查来自 Geyser 的(基岩版)玩家?
|
||||
# 此功能是实验性的. 请报告任何因为误判而导致无法通过检查的问题
|
||||
# 如果关闭该选项 Sonar 将不检查这些玩家以便直接连接到服务器.
|
||||
check-geyser-players: true
|
||||
# 要在客户端未响应多少毫秒后因超时而被踢出服务器?
|
||||
# (该值为毫秒. 1秒等于1000毫秒.)
|
||||
read-timeout: 8000
|
||||
# 当玩家在需要验证时(即还未通过检查的情况下)需要等待多少毫秒才可以重新连接?
|
||||
# (该值为毫秒. 1秒等于1000毫秒.)
|
||||
rejoin-delay: 8000
|
||||
# 当玩家验证失败后, Sonar 应该记住他多久?
|
||||
# (该值为毫秒. 1秒等于1000毫秒.)
|
||||
remember-time: 120000
|
||||
# 黑名单应在多少秒后过期? (可能由于频繁验证失败. 在黑名单中将会被拒绝进入服务器.)
|
||||
# (该值为毫秒. 1秒等于1000毫秒.)
|
||||
blacklist-time: 600000
|
||||
# 玩家必须验证失败多少次才会被列入黑名单? (设置为0以完全禁用)
|
||||
blacklist-threshold: 2
|
||||
# Sonar 不应该检查哪些协议? (即 跳过检查)
|
||||
# 您可以在 https://wiki.vg/Protocol_version_numbers 找到所有协议号
|
||||
# 例如 Minecraft 1.20 对应的协议号为763.
|
||||
whitelisted-protocols: []
|
||||
# 黑名单中的协议将阻止所有使用该协议的客户端.
|
||||
blacklisted-protocols: []
|
||||
@@ -39,8 +39,6 @@ commands:
|
||||
incorrect-usage: '%prefix%<red>Benutzung: /sonar %usage%'
|
||||
# Nachricht, die angezeigt wird, wenn jemand eine ungültige IP-Adresse angibt (Ungültiges Format)
|
||||
invalid-ip-address: '%prefix%Die angegebene IP-Adresse scheint ungültig zu sein.'
|
||||
# Nachricht, die angezeigt wird, wenn jemand eine illegale IP-Adresse angibt (Lokale IP)
|
||||
illegal-ip-address: '%prefix%Die angegebene IP-Adresse scheint entweder eine lokale oder eine Schleifenrückführungs-IP zu sein.'
|
||||
# Nachricht, die angezeigt wird, wenn die Konsole einen Befehl ausführt, der nur für Spieler ist
|
||||
player-only: '%prefix%<red>Diesen Befehl kannst du nur als Spieler ausführen.'
|
||||
# Nachricht, die angezeigt wird, wenn ein Spieler einen Befehl ausführt, der nur für die Konsole ist
|
||||
@@ -123,6 +121,10 @@ commands:
|
||||
size: '%prefix%Es sind derzeit %amount% einzigartige Spieler verifiziert.'
|
||||
# Nachricht, die angezeigt wird, wenn jemand eine IP-Adresse unverifiziert macht
|
||||
removed: '%prefix%Erfolgreich %ip% unverifiziert.'
|
||||
# Nachricht, die angezeigt wird, wenn jemand eine IP-Adresse verifiziert macht
|
||||
added: '%prefix%Erfolgreich %ip% verifiziert.'
|
||||
# Message that is shown when someone tries to verify an IP address/UUID that are already verified
|
||||
already: '%prefix%The IP address and UUID you provided are already verified.'
|
||||
# Nachricht, die angezeigt wird, wenn jemand eine IP-Adresse unverifiziert macht, die nicht verifiziert ist
|
||||
ip-not-found: '%prefix%Die angegebene IP-Adresse ist nicht verifiziert.'
|
||||
# Nachricht, die angezeigt wird, wenn jemand versucht, die gleiche IP-Adresse zweimal zu unverifizieren (doppelte Operation)
|
||||
|
||||
@@ -38,9 +38,6 @@ commands:
|
||||
incorrect-usage: '%prefix%<red>Usage: /sonar %usage%'
|
||||
# Message that is shown when someone provides an invalid IP address (Invalid format)
|
||||
invalid-ip-address: '%prefix%The IP address you provided seems to be invalid.'
|
||||
# Message that is shown when someone provides an illegal IP address (Local IP)
|
||||
illegal-ip-address: '%prefix%The IP address you provided seems to be either a local
|
||||
or loopback IP.'
|
||||
# Message that is shown when the console runs a command that is player-only
|
||||
player-only: '%prefix%<red>You can only execute this command as a player.'
|
||||
# Message that is shown when a player runs a command that is console-only
|
||||
@@ -127,6 +124,10 @@ commands:
|
||||
size: '%prefix%There are currently %amount% unique player(s) verified.'
|
||||
# Message that is shown when someone un-verifies an IP address
|
||||
removed: '%prefix%Successfully unverified %ip%.'
|
||||
# Message that is shown when someone verifies an IP address
|
||||
added: '%prefix%Successfully verified %ip%.'
|
||||
# Message that is shown when someone tries to verify an IP address/UUID that are already verified
|
||||
already: '%prefix%The IP address and UUID you provided are already verified.'
|
||||
# Message that is shown when someone un-verifies an IP address, but it is not verified
|
||||
ip-not-found: '%prefix%The IP address you provided is not verified.'
|
||||
# Message that is shown when someone tries un-verifying the same IP address twice (double operation)
|
||||
|
||||
@@ -38,8 +38,6 @@ commands:
|
||||
incorrect-usage: '%prefix%<red>Применение: /sonar %usage%'
|
||||
# Сообщение, которое отображается, когда кто-то предоставляет неверный формат IP-адреса
|
||||
invalid-ip-address: '%prefix%Указанный вами IP-адрес недействителен'
|
||||
# Сообщение, которое отображается, когда кто-то предоставляет локальный IP-адрес
|
||||
illegal-ip-address: '%prefix%IP-адрес который вы указали является локальным.'
|
||||
# Сообщение, которое отображается, когда консоль выполняет команду, предназначенную только для игроков
|
||||
player-only: '%prefix%<red>Эта команда может быть выполнена только игроком.'
|
||||
# Сообщение, которое отображается, когда игрок выполняет команду, доступную только для консоли
|
||||
@@ -124,6 +122,10 @@ commands:
|
||||
size: '%prefix%В настоящее время проверено %amount% уникальных игроков.'
|
||||
# Сообщение, которое отображается при удалении IP-адреса из списка проверенных игроков
|
||||
removed: '%prefix%Успешно удален %ip%.'
|
||||
# Message that is shown when someone verifies an IP address
|
||||
added: '%prefix%Successfully verified %ip%.'
|
||||
# Message that is shown when someone tries to verify an IP address/UUID that are already verified
|
||||
already: '%prefix%The IP address and UUID you provided are already verified.'
|
||||
# Сообщение, которое показывается когда кто-то удаляет IP из списка но его небыло в списке
|
||||
ip-not-found: '%prefix%Указанный вами IP-адрес не в списке.'
|
||||
# Сообщение, которое показывается когда выполняю сразу две операции (двойная операция)
|
||||
|
||||
@@ -38,8 +38,6 @@ commands:
|
||||
incorrect-usage: '%prefix%<red>Kullanım: /sonar %usage%'
|
||||
# Birisi geçersiz bir IP adresi sağladığında gösterilen mesaj (Geçersiz format)
|
||||
invalid-ip-address: '%prefix%Sağladığınız IP adresi geçersiz görünüyor.'
|
||||
# Birisi yasal olmayan bir IP adresi sağladığında gösterilen mesaj (Yerel IP)
|
||||
illegal-ip-address: '%prefix%Sağladığınız IP adresi yerel veya döngüsel bir IP gibi görünüyor.'
|
||||
# Konsol, sadece oyuncunun çalıştırabileceği bir komut çalıştırdığında gösterilen mesaj
|
||||
player-only: '%prefix%<red>Bu komutu sadece bir oyuncu olarak çalıştırabilirsiniz.'
|
||||
# Bir oyuncu, sadece konsolun çalıştırabileceği bir komutu çalıştırdığında gösterilen mesaj
|
||||
@@ -115,6 +113,10 @@ commands:
|
||||
size: '%prefix%Şu anda %amount% benzersiz oyuncu doğrulanmış.'
|
||||
# Birisi bir IP adresini doğrulanmamış olarak işaretlediğinde gösterilen mesaj
|
||||
removed: '%prefix%Başarıyla %ip% doğrulanmamış olarak işaretlendi.'
|
||||
# Message that is shown when someone verifies an IP address
|
||||
added: '%prefix%Successfully verified %ip%.'
|
||||
# Message that is shown when someone tries to verify an IP address/UUID that are already verified
|
||||
already: '%prefix%The IP address and UUID you provided are already verified.'
|
||||
# Birisi bir IP adresini doğrulanmamış olarak işaretlemeye çalıştığında ama doğrulanmamış olduğunda gösterilen mesaj
|
||||
ip-not-found: '%prefix%Sağladığınız IP adresi doğrulanmış olarak işaretlenmemiş.'
|
||||
# Birisi aynı IP adresini iki kez (çift işlem) doğrulanmamış olarak işaretlemeye çalıştığında gösterilen mesaj
|
||||
|
||||
@@ -0,0 +1,305 @@
|
||||
#
|
||||
# /$$$$$$
|
||||
# /$$__ $$
|
||||
# | $$ \__/ /$$$$$$ /$$$$$$$ /$$$$$$ /$$$$$$
|
||||
# | $$$$$$ /$$__ $$| $$__ $$ |____ $$ /$$__ $$
|
||||
# \____ $$| $$ \ $$| $$ \ $$ /$$$$$$$| $$ \__/
|
||||
# /$$ \ $$| $$ | $$| $$ | $$ /$$__ $$| $$
|
||||
# | $$$$$$/| $$$$$$/| $$ | $$| $$$$$$$| $$
|
||||
# \______/ \______/ |__/ |__/ \_______/|__/
|
||||
#
|
||||
# 有问题或需要帮助? https://jonesdev.xyz/discord
|
||||
# 源代码: https://github.com/jonesdevelopment/sonar
|
||||
# Copyright Jones Development and Sonar Contributors, licensed under GPL-3.0
|
||||
#
|
||||
|
||||
# %prefix% 占位符
|
||||
prefix: <b><yellow>Sonar</b> <gray>» <white>
|
||||
# %support-url% 占位符
|
||||
support-url: https://jonesdev.xyz/discord/
|
||||
# %header% 占位符. (支持多行)
|
||||
header:
|
||||
- <b><yellow>Sonar</b><reset>
|
||||
- ''
|
||||
# %footer% 占位符. (支持多行)
|
||||
footer:
|
||||
- <gray>如果您认为这是一个错误, 请联系服务器管理员.
|
||||
# 当玩家尝试加入但有太多在线玩家同时使用者玩家的 IP 地址时显示的踢出消息.
|
||||
too-many-online-per-ip:
|
||||
- '%header%'
|
||||
- <red>您所使用的 IP 地址已经无法再登录更多账户了.
|
||||
- '%footer%'
|
||||
|
||||
# 有关命令的语言配置.
|
||||
commands:
|
||||
# 当玩家尝试在没有权限的情况下运行 /sonar 时显示的消息
|
||||
no-permission: '%prefix%<red>您没有足够的权限使用该命令.'
|
||||
# 当命令用法不正确时显示的消息
|
||||
incorrect-usage: '%prefix%<red>用法: /sonar %usage%'
|
||||
# 当提供无效的 IP 地址时显示的消息
|
||||
invalid-ip-address: '%prefix%无法正确识别您提供的IP地址 请检查输入是否有误.'
|
||||
# 当控制台尝试执行只有玩家能执行的命令时显示的消息
|
||||
player-only: '%prefix%<red>只有玩家才可以执行该命令.'
|
||||
# 当玩家尝试执行只有控制台能执行的命令时显示的消息
|
||||
console-only: '%prefix%<red>出于安全考虑 只允许通过控制台执行该命令.'
|
||||
# 玩家执行Sonar命令的速度过快时显示的消息
|
||||
cool-down: '%prefix%<red>您只能每 0.5 秒执行一次此命令.'
|
||||
cool-down-left: '%prefix%<red>请等待 <bold>%time-left% 秒<reset><red> 后执行该命令.'
|
||||
# 当玩家没有权限执行某个子命令时显示的消息
|
||||
subcommand-no-permission: '%prefix%<red>您没有足够的权限使用该子命令. <gray>(%permission%)'
|
||||
|
||||
# 根命令
|
||||
main:
|
||||
# 执行根命令时显示在所有输出内容(由该命令输出的消息)上方的消息
|
||||
header:
|
||||
- <yellow>正在在 %platform% 平台上运行 <b>Sonar</b> %version%.
|
||||
- <gray><click:open_url:'https://github.com/jonesdevelopment/sonar'>(C) %copyright_year%
|
||||
Jones Development and Sonar Contributors</click>
|
||||
- ''
|
||||
- <gray>有问题或需要帮助?
|
||||
- <white><click:open_url:'https://jonesdev.xyz/discord/'><hover:show_text:'(Click
|
||||
to open Discord)'>在我们的Discord上打开一张ticket </hover></click><click:open_url:'https://github.com/jonesdevelopment/sonar/issues'><hover:show_text:'(Click
|
||||
to open GitHub)'>或在Github打开新的问题.
|
||||
- ''
|
||||
# 执行根命令时 对于子命令的描述.
|
||||
subcommands: '<click:suggest_command:''/sonar %subcommand% ''><hover:show_text:''<gray>只有玩家可以执行该命令:
|
||||
</gray>%only_players%<br><gray>只有控制台可以执行该命令: </gray>%require_console%<br><gray>需要的权限:
|
||||
</gray><white>%permission%<br><gray>别名: </gray>%aliases%''><dark_aqua>
|
||||
▪ <gray>/sonar %subcommand% <white>%description%'
|
||||
|
||||
# 重新加载命令
|
||||
reload:
|
||||
# 当有人开始重载插件时显示的消息
|
||||
start: '%prefix%正在重新加载 Sonar...'
|
||||
# 插件完成重载时显示的消息
|
||||
finish: '%prefix%<green>成功重新加载配置文件. <gray>(花费 %taken% 毫秒)'
|
||||
|
||||
# '/sonar verbose' 命令的消息
|
||||
verbose:
|
||||
# 玩家订阅详细信息时显示的消息
|
||||
subscribed: '%prefix%已为您显示 Sonar 的详细信息.'
|
||||
# 玩家取消订阅详细信息时显示的消息
|
||||
unsubscribed: '%prefix%不再为您显示 Sonar 的详细信息.'
|
||||
|
||||
# '/sonar notify' 命令的消息
|
||||
notify:
|
||||
# 玩家订阅攻击通知时显示的消息
|
||||
subscribed: '%prefix%已订阅攻击通知. 当服务器受到攻击时将会提醒您.'
|
||||
# 玩家取消订阅攻击通知时显示的消息
|
||||
unsubscribed: '%prefix%已退订攻击通知'
|
||||
|
||||
# '/sonar blacklist' 命令的消息
|
||||
blacklist:
|
||||
# 当尝试清理黑名单但黑名单中没有任何 IP 地址显示的消息
|
||||
empty: '%prefix%黑名单中没有任何 IP 地址.'
|
||||
# 当成功清理黑名单时显示的消息
|
||||
cleared: '%prefix%您已成功从黑名单中删除 %removed% 个 IP 地址.'
|
||||
# 当查看黑名单的大小时显示的消息
|
||||
size: '%prefix%黑名单中目前有 %amount% 个 IP 地址.'
|
||||
# 当手动将一个 IP 地址添加到黑名单时显示的消息
|
||||
added: '%prefix%成功将 IP 地址 %ip% 列入黑名单.'
|
||||
# 当尝试将白名单中的 IP 地址添加到黑名单中时显示的警告消息
|
||||
added-warning: '%prefix%<red>警告: <white>IP 地址 %ip% 在白名单中. 添加到黑名单则意味着它将被同时从白名单中移除.'
|
||||
# 当尝试从黑名单中移除一个 IP 地址时显示的消息
|
||||
removed: '%prefix%成功将 IP 地址 %ip% 移出黑名单.'
|
||||
# 当尝试向黑名单添加一个已经在黑名单中的 IP 地址时显示的消息
|
||||
duplicate-ip: '%prefix%该 IP 地址已经在黑名单里了.'
|
||||
# 当尝试移除一个不在黑名单中的 IP 地址时显示的消息
|
||||
ip-not-found: '%prefix%您提供的 IP 地址并不在黑名单中.'
|
||||
|
||||
# '/sonar verified' 命令的消息
|
||||
verified:
|
||||
# 当尝试清除白名单但为空时显示的消息
|
||||
empty: '%prefix%白名单中没有任何玩家.'
|
||||
# 当成功清理白名单时显示的消息
|
||||
cleared: '%prefix%您已成功从白名单中删除 %removed% 个玩家.'
|
||||
# 当查看白名单的大小时显示的消息
|
||||
size: '%prefix%白名单中目前有 %amount% 个已验证的玩家.'
|
||||
# 当成功将一个 IP 地址内所有已验证的玩家移出白名单时显示的消息
|
||||
removed: '%prefix%成功将所有使用 IP 地址 (%ip%) 的玩家移出白名单.'
|
||||
# 当手动将一个 IP 地址添加到白名单中显示的消息
|
||||
added: '%prefix%成功将 IP 地址 %ip% 列入白名单.'
|
||||
# 当尝试添加一个已经在白名单中的 IP 地址和玩家的UUID 时显示的消息
|
||||
already: '%prefix%您提供的 IP 地址和玩家的UUID已经在白名单中了.'
|
||||
# 当尝试移除一个不在白名单中的 IP 地址时显示的消息
|
||||
ip-not-found: '%prefix%您提供的 IP 地址不在白名单中.'
|
||||
# 当操作冲突时(例如同时将一个 IP 地址移除白名单)显示的消息
|
||||
blocked: '%prefix%请等待当前操作完成.'
|
||||
|
||||
# '/sonar statistics' 命令的消息
|
||||
statistics:
|
||||
# 执行该命令时显示在所有输出内容(由该命令输出的消息)上方的消息
|
||||
header: '%prefix%当前会话中的 %type% 类型的统计信息:'
|
||||
# 当查看一个未知的统计信息时显示的消息
|
||||
unknown-type: '%prefix%<red>未知的统计信息! 可用的统计信息: <gray>%statistics%'
|
||||
# 默认统计信息的消息
|
||||
general:
|
||||
- ' <dark_aqua>▪ <gray>白名单中的 IP 地址数量: <white>%verified%'
|
||||
- ' <dark_aqua>▪ <gray>正在进行验证的 IP 地址数量: <white>%verifying%'
|
||||
- ' <dark_aqua>▪ <gray>黑名单中的 IP 地址数量: <white>%blacklisted%'
|
||||
- ' <dark_aqua>▪ <gray>缓冲队列中的等待登录的数量: <white>%queued%'
|
||||
- ' <dark_aqua>▪ <gray>玩家加入次数: <white>%total_joins%'
|
||||
- ' <dark_aqua>▪ <gray>总验证次数: <white>%total_attempts%'
|
||||
- ' <dark_aqua>▪ <gray>验证失败的次数: <white>%total_failed%'
|
||||
- ' <dark_aqua>▪ <gray>服务器当前会话的运行时间: <white>%uptime%'
|
||||
# CPU 统计信息
|
||||
cpu:
|
||||
- ' <dark_aqua>▪ <gray>当前进程的 CPU 使用率: <white>%process_cpu%%'
|
||||
- ' <dark_aqua>▪ <gray>当前系统的 CPU 使用率: <white>%system_cpu%%'
|
||||
- ' <dark_aqua>▪ <gray>当前进程的多核平均使用率: <white>%average_process_cpu%%'
|
||||
- ' <dark_aqua>▪ <gray>当前系统的多核平均使用率: <white>%average_system_cpu%%'
|
||||
- ' <dark_aqua>▪ <gray>系统平均负载: <white>%load_average%%'
|
||||
- ' <dark_aqua>▪ <gray>虚拟核心数量: <white>%virtual_cores%'
|
||||
# 内存统计信息
|
||||
memory:
|
||||
- ' <dark_aqua>▪ <gray>可用内存: <white>%free_memory%'
|
||||
- ' <dark_aqua>▪ <gray>已使用的内存: <white>%used_memory%'
|
||||
- ' <dark_aqua>▪ <gray>最大可用内存: <white>%max_memory%'
|
||||
- ' <dark_aqua>▪ <gray>已分配的内存: <white>%total_memory%'
|
||||
# 网络统计信息
|
||||
network:
|
||||
- ' <dark_aqua>▪ <gray>当前下行: <white>%incoming%'
|
||||
- ' <dark_aqua>▪ <gray>当前上行: <white>%outgoing%'
|
||||
- ' <dark_aqua>▪ <gray>下行总量: <white>%ttl_incoming%'
|
||||
- ' <dark_aqua>▪ <gray>上行总量: <white>%ttl_outgoing%'
|
||||
|
||||
# 验证过程中所有消息的翻译
|
||||
verification:
|
||||
# 验证过程中向控制台打印的调试信息
|
||||
logs:
|
||||
# 当玩家进入服务器并验证时显示的消息
|
||||
connection: '%name%%ip% (%protocol%) 连接到了服务器.'
|
||||
# 当玩家未通过检查时打印的消息
|
||||
failed: '%ip% (%protocol%) 由于原因 %reason% 无法通过检查.'
|
||||
# 当玩家被自动列入黑名单时打印的消息
|
||||
blacklisted: '%ip% (%protocol%) 因太多次无法完成验证而被列入黑名单.'
|
||||
# 当玩家完成验证时打印的消息
|
||||
successful: '%name% 成功通过了验证 (耗时 %time% 秒!).'
|
||||
|
||||
# 当验证中的玩家需要解决验证码时显示的消息
|
||||
captcha:
|
||||
# 玩家需要输入验证码答案时显示的消息
|
||||
enter-code: '%prefix%<green>请在聊天框中发送地图上的验证码.'
|
||||
# 显示还有多少秒来解决验证码. (设置为空即禁用)
|
||||
action-bar: '%prefix%<green>您还有 %time-left% 秒来解决该验证码.'
|
||||
# 当玩家输入错误的验证码时显示的消息
|
||||
incorrect: '%prefix%<red>您输入了错误的验证码. 请再试一次'
|
||||
|
||||
# 当验证码还在准备的时候显示的踢出消息
|
||||
currently-preparing:
|
||||
- '%header%'
|
||||
- <yellow>反机器人还在准备所需的数据
|
||||
- <gray>请稍后重新加入服务器
|
||||
- '%footer%'
|
||||
# 当玩家需要验证时重新连接速度太快时显示的踢出消息
|
||||
too-fast-reconnect:
|
||||
- '%header%'
|
||||
- <gold>您的重新连接速度过快
|
||||
- <gray>请稍后重新加入服务器
|
||||
- '%footer%'
|
||||
# 当玩家尝试加入但它的 IP 已经有另一位玩家在验证时显示的踢出消息
|
||||
already-verifying:
|
||||
- '%header%'
|
||||
- <red>有另一位玩家正在使用您当前的 IP 地址进行验证
|
||||
- <red>请稍后重新加入服务器
|
||||
- '%footer%'
|
||||
# 已经在排队等待验证的人加入时显示的踢出消息
|
||||
already-queued:
|
||||
- '%header%'
|
||||
- <red>您的 IP 地址当前正在排队等待验证
|
||||
- <red>请稍后重新加入服务器
|
||||
- '%footer%'
|
||||
# 当玩家尝试加入但暂时被列入黑名单时显示的踢出消息
|
||||
blacklisted:
|
||||
- '%header%'
|
||||
- <red>您已被临时禁止加入服务器
|
||||
- <red>请等待几分钟后重新加入服务器
|
||||
- <gold>您认为您不应该被禁止? <gray>%support-url%
|
||||
- '%footer%'
|
||||
# 当玩家尝试加入 但他们的用户名包含不允许的字符时显示的踢出消息
|
||||
invalid-username:
|
||||
- '%header%'
|
||||
- <red>您的用户名包含无效的字符. 请更改您的用户名 然后再试.
|
||||
- '%footer%'
|
||||
# 当玩家使用列入黑名单的版本加入时显示的踢出消息
|
||||
blacklisted-protocol:
|
||||
- '%header%'
|
||||
- <red>您当前所使用的协议/客户端版本已被服务器禁止
|
||||
- <gold>需要帮助以便您能登录服务器? <gray>%support-url%
|
||||
- '%footer%'
|
||||
# 当玩家验证成功时显示的踢出消息
|
||||
success:
|
||||
- '%header%'
|
||||
- <green>您已成功通过验证
|
||||
- <white>请重新连接以加入服务器
|
||||
# 当玩家未能通过验证时显示的消息
|
||||
failed:
|
||||
- '%header%'
|
||||
- <red>无法验证您是否是机器人
|
||||
- <gray>请等待几秒后重新加入服务器以重新验证
|
||||
- <gold>需要帮助? <gray>%support-url%
|
||||
- '%footer%'
|
||||
|
||||
# 状态栏详细信息
|
||||
verbose:
|
||||
layout:
|
||||
# 详细信息状态栏样式
|
||||
# 占位符:
|
||||
# - '%queued%' 正在排队的连接数数量
|
||||
# - '%verifying%' 正在进行验证的连接数数量
|
||||
# - '%blacklisted%' 黑名单大小
|
||||
# - '%total-joins%' 玩家加入次数
|
||||
# - '%verify-total%' 已执行的验证次数
|
||||
# - '%verify-success%' 成功验证的 IP 的数量
|
||||
# - '%verify-failed%' 验证失败的次数
|
||||
# - '%logins-per-second%' 每秒登录服务器次数
|
||||
# - '%connections-per-second%' 每秒接受连接次数
|
||||
# - '%attack-duration%' 攻击持续时间
|
||||
# - '%incoming-traffic%' 每秒下行带宽
|
||||
# - '%outgoing-traffic%' 每秒上行带宽
|
||||
# - '%incoming-traffic-ttl%' 已使用的下行带宽
|
||||
# - '%outgoing-traffic-ttl%' 已使用的上行带宽
|
||||
# - '%used-memory%' 已使用的内存
|
||||
# - '%total-memory%' 已分配的内存
|
||||
# - '%max-memory%' 最大可用内存
|
||||
# - '%free-memory%' 可用内存
|
||||
# - '%animation%' 可自定义的动画符号
|
||||
# 默认的状态栏
|
||||
normal: '%prefix%<gray>每秒连接数 <white>%connections-per-second% <dark_aqua>╺ <gray>每秒登录次数
|
||||
<white>%logins-per-second% <dark_aqua>╺ <gray>正在验证的连接数 <white>%verifying% <dark_aqua>|
|
||||
<green>⬆ <white>%outgoing-traffic%/s <red>⬇ <white>%incoming-traffic%/s <green><bold>%animation%<reset>'
|
||||
# 遭受攻击时的状态栏
|
||||
attack: '%prefix%<gray>每秒连接数 <white>%connections-per-second% <dark_aqua>╺ <gray>每秒登录次数
|
||||
<white>%logins-per-second% <dark_aqua>╺ <gray>排队连接数 <white>%queued% <dark_aqua>╺
|
||||
<gray>正在验证的连接数 <white>%verifying% <dark_aqua>╺ <gray>攻击持续时间 <white>%attack-duration%
|
||||
<dark_aqua>| <green>⬆ <white>%outgoing-traffic%/s <red>⬇ <white>%incoming-traffic%/s <green><bold>%animation%<reset>'
|
||||
# 动画占位符
|
||||
# 其它预设:
|
||||
# - ▙, ▛, ▜, ▟
|
||||
# - ⬈, ⬊, ⬋, ⬉
|
||||
animation:
|
||||
- ◜
|
||||
- ◝
|
||||
- ◞
|
||||
- ◟
|
||||
|
||||
# 攻击通知
|
||||
notifications:
|
||||
# 标题
|
||||
title: <yellow><b>Sonar<reset>
|
||||
# 子标题
|
||||
subtitle: <gray>检测到活动的机器人攻击..
|
||||
# 聊天消息
|
||||
chat:
|
||||
- ''
|
||||
- <yellow><b>Sonar<reset>
|
||||
- ''
|
||||
- <gray>检测到活动的机器人攻击
|
||||
- ' <dark_aqua>▪ <gray>每秒登录次数: <white>%logins-per-second%'
|
||||
- ' <dark_aqua>▪ <gray>正在验证的连接数: <white>%verifying%'
|
||||
- ' <dark_aqua>▪ <gray>排队连接数: <white>%queued%'
|
||||
- ''
|
||||
- <gray>使用 <white><click:run_command:'/sonar verbose'><hover:show_text:'(
|
||||
- 点击执行)'>/sonar verbose</hover></click><gray> 或 <white><click:run_command:'/sonar
|
||||
stats'><hover:show_text:'(点击执行)'>/sonar stats</hover></click><gray> 来查看更多信息.
|
||||
- ''
|
||||
@@ -18,23 +18,22 @@
|
||||
package xyz.jonesdev.sonar.velocity;
|
||||
|
||||
import com.alessiodp.libby.VelocityLibraryManager;
|
||||
import com.velocitypowered.api.proxy.Player;
|
||||
import lombok.Getter;
|
||||
import net.kyori.adventure.audience.Audience;
|
||||
import org.bstats.charts.SimplePie;
|
||||
import org.bstats.velocity.Metrics;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.SonarPlatform;
|
||||
import xyz.jonesdev.sonar.api.logger.LoggerWrapper;
|
||||
import xyz.jonesdev.sonar.common.boot.SonarBootstrap;
|
||||
import xyz.jonesdev.sonar.common.statistics.CachedBandwidthStatistics;
|
||||
import xyz.jonesdev.sonar.velocity.command.VelocitySonarCommand;
|
||||
import xyz.jonesdev.sonar.velocity.fallback.FallbackInjectionHelper;
|
||||
import xyz.jonesdev.sonar.velocity.fallback.FallbackLoginListener;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@Getter
|
||||
public final class SonarVelocity extends SonarBootstrap<SonarVelocityPlugin> {
|
||||
@@ -58,6 +57,18 @@ public final class SonarVelocity extends SonarBootstrap<SonarVelocityPlugin> {
|
||||
return getPlugin().getServer().getPlayer(uniqueId).orElse(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasTooManyAccounts(final @NotNull InetAddress inetAddress, final int limit) {
|
||||
int count = 1;
|
||||
for (final Player player : getPlugin().getServer().getAllPlayers()) {
|
||||
// Check if the IP address of the player is equal to the IP trying to connect
|
||||
if (!Objects.equals(player.getRemoteAddress().getAddress(), inetAddress)) continue;
|
||||
// Increment count of duplicated accounts
|
||||
if (++count >= limit) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a wrapper for the plugin logger, so we can use it outside
|
||||
* the velocity module.
|
||||
@@ -98,24 +109,6 @@ public final class SonarVelocity extends SonarBootstrap<SonarVelocityPlugin> {
|
||||
// Register Sonar command
|
||||
getPlugin().getServer().getCommandManager().register("sonar", new VelocitySonarCommand());
|
||||
|
||||
// Register Fallback listener
|
||||
getPlugin().getServer().getEventManager().register(getPlugin(), new FallbackLoginListener());
|
||||
|
||||
// Register traffic service
|
||||
getPlugin().getServer().getScheduler().buildTask(getPlugin(), CachedBandwidthStatistics::reset)
|
||||
.repeat(1L, TimeUnit.SECONDS)
|
||||
.schedule();
|
||||
|
||||
// Register queue service
|
||||
getPlugin().getServer().getScheduler().buildTask(getPlugin(), getFallback().getQueue().getPollTask())
|
||||
.repeat(500L, TimeUnit.MILLISECONDS)
|
||||
.schedule();
|
||||
|
||||
// Register verbose service
|
||||
getPlugin().getServer().getScheduler().buildTask(getPlugin(), Sonar.get().getVerboseHandler()::update)
|
||||
.repeat(200L, TimeUnit.MILLISECONDS)
|
||||
.schedule();
|
||||
|
||||
// Make sure to inject into the server's connection handler
|
||||
FallbackInjectionHelper.inject(getPlugin().getServer());
|
||||
}
|
||||
|
||||
+6
-139
@@ -22,26 +22,12 @@ import com.velocitypowered.proxy.protocol.packet.HandshakePacket;
|
||||
import com.velocitypowered.proxy.protocol.packet.ServerLoginPacket;
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.handler.codec.CorruptedFrameException;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.common.fallback.FallbackBandwidthHandler;
|
||||
import xyz.jonesdev.sonar.common.fallback.FallbackChannelHandlerAdapter;
|
||||
import xyz.jonesdev.sonar.common.fallback.FallbackUserWrapper;
|
||||
import xyz.jonesdev.sonar.common.statistics.GlobalSonarStatistics;
|
||||
import xyz.jonesdev.sonar.common.utility.geyser.GeyserUtil;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.UUID;
|
||||
|
||||
import static com.velocitypowered.proxy.network.Connections.*;
|
||||
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_BANDWIDTH;
|
||||
import static xyz.jonesdev.sonar.common.fallback.FallbackUserWrapper.customDisconnect;
|
||||
import static xyz.jonesdev.sonar.common.fallback.FallbackUserWrapper.deject;
|
||||
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer.*;
|
||||
|
||||
public final class FallbackChannelHandler extends FallbackChannelHandlerAdapter {
|
||||
|
||||
@@ -49,141 +35,22 @@ public final class FallbackChannelHandler extends FallbackChannelHandlerAdapter
|
||||
super(channel);
|
||||
}
|
||||
|
||||
private @ApiStatus.Internal HandshakePacket handshakePacket;
|
||||
|
||||
@Override
|
||||
public void channelRead(final @NotNull ChannelHandlerContext ctx, final Object msg) throws Exception {
|
||||
// Intercept any handshake packet by the client
|
||||
if (msg instanceof HandshakePacket handshake) {
|
||||
handleHandshake(handshake);
|
||||
handleHandshake(handshake.getServerAddress(), handshake.getProtocolVersion().getProtocol());
|
||||
}
|
||||
// Intercept any server login packet by the client
|
||||
if (msg instanceof ServerLoginPacket serverLogin) {
|
||||
handleLogin(ctx, serverLogin);
|
||||
// Make sure to use the potentially modified, original IP
|
||||
final MinecraftConnection minecraftConnection = (MinecraftConnection) channel.pipeline().get(HANDLER);
|
||||
final InetSocketAddress socketAddress = (InetSocketAddress) minecraftConnection.getRemoteAddress();
|
||||
handleLogin(ctx, serverLogin, serverLogin.getUsername(), socketAddress,
|
||||
MINECRAFT_ENCODER, MINECRAFT_DECODER, READ_TIMEOUT, HANDLER);
|
||||
return;
|
||||
}
|
||||
// Make sure to let the server handle the rest
|
||||
ctx.fireChannelRead(msg);
|
||||
}
|
||||
|
||||
private void handleHandshake(final @NotNull HandshakePacket handshake) throws Exception {
|
||||
// Check if the player has already sent a handshake packet
|
||||
if (handshakePacket != null) {
|
||||
throw new CorruptedFrameException("Already sent handshake");
|
||||
}
|
||||
// Check if the hostname is invalid
|
||||
if (handshake.getServerAddress().isEmpty()) {
|
||||
throw new CorruptedFrameException("Hostname is empty");
|
||||
}
|
||||
// Store the protocol version
|
||||
protocolVersion = ProtocolVersion.fromId(handshake.getProtocolVersion().getProtocol());
|
||||
// Connections from unknown protocol versions will be discarded
|
||||
// as this is the safest way of handling unwanted connections
|
||||
if (protocolVersion.isUnknown()) {
|
||||
// Sonar does NOT support snapshots or unknown versions;
|
||||
// I'll try my best to stay up-to-date!
|
||||
throw new CorruptedFrameException("Unknown protocol");
|
||||
}
|
||||
// Store the handshake packet
|
||||
handshakePacket = handshake;
|
||||
// Hook the traffic listener
|
||||
// TODO: Can we implement this in channelActive?
|
||||
channel.pipeline().addFirst(FALLBACK_BANDWIDTH, FallbackBandwidthHandler.INSTANCE);
|
||||
}
|
||||
|
||||
private void handleLogin(final @NotNull ChannelHandlerContext ctx,
|
||||
final @NotNull ServerLoginPacket serverLogin) throws Exception {
|
||||
// Check if the player has already sent a login packet
|
||||
if (username != null) {
|
||||
throw new CorruptedFrameException("Already logged on");
|
||||
}
|
||||
// Increase joins per second for the action bar verbose
|
||||
GlobalSonarStatistics.countLogin();
|
||||
// Store the username
|
||||
username = serverLogin.getUsername();
|
||||
// Make sure to use the potentially modified, original IP
|
||||
final MinecraftConnection minecraftConnection = (MinecraftConnection) channel.pipeline().get(HANDLER);
|
||||
final InetSocketAddress socketAddress = (InetSocketAddress) minecraftConnection.getRemoteAddress();
|
||||
inetAddress = socketAddress.getAddress();
|
||||
|
||||
// Check the blacklist here since we cannot let the player "ghost join"
|
||||
if (FALLBACK.getBlacklist().asMap().containsKey(inetAddress)) {
|
||||
customDisconnect(channel, protocolVersion, blacklisted, MINECRAFT_DECODER, HANDLER);
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't continue the verification process if the verification is disabled
|
||||
if (!Sonar.get().getFallback().shouldVerifyNewPlayers()) {
|
||||
ctx.fireChannelRead(serverLogin);
|
||||
deject(channel.pipeline());
|
||||
return;
|
||||
}
|
||||
|
||||
// Completely skip Geyser connections
|
||||
if (GeyserUtil.isGeyserConnection(channel, socketAddress)) {
|
||||
ctx.fireChannelRead(serverLogin);
|
||||
deject(channel.pipeline());
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the player is already queued since we don't want bots to flood the queue
|
||||
if (FALLBACK.getQueue().getQueuedPlayers().containsKey(inetAddress)) {
|
||||
customDisconnect(channel, protocolVersion, alreadyQueued, MINECRAFT_DECODER, HANDLER);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if Fallback is already verifying a player
|
||||
// → is another player with the same IP address connected to Fallback?
|
||||
if (FALLBACK.getConnected().containsKey(serverLogin.getUsername())
|
||||
|| FALLBACK.getConnected().containsValue(inetAddress)) {
|
||||
customDisconnect(channel, protocolVersion, alreadyVerifying, MINECRAFT_DECODER, HANDLER);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the protocol ID of the player is not allowed to enter the server
|
||||
if (Sonar.get().getConfig().getVerification().getBlacklistedProtocols()
|
||||
.contains(protocolVersion.getProtocol())) {
|
||||
customDisconnect(channel, protocolVersion, protocolBlacklisted, MINECRAFT_DECODER, HANDLER);
|
||||
return;
|
||||
}
|
||||
|
||||
// Make sure we actually have to verify the player
|
||||
final String offlineUUIDString = "OfflinePlayer:" + serverLogin.getUsername();
|
||||
final UUID offlineUUID = UUID.nameUUIDFromBytes(offlineUUIDString.getBytes(StandardCharsets.UTF_8));
|
||||
if (Sonar.get().getVerifiedPlayerController().has(inetAddress, offlineUUID)) {
|
||||
ctx.fireChannelRead(serverLogin);
|
||||
deject(channel.pipeline());
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the IP address is currently being rate-limited
|
||||
if (!FALLBACK.getRatelimiter().attempt(inetAddress)) {
|
||||
customDisconnect(channel, protocolVersion, reconnectedTooFast, MINECRAFT_DECODER, HANDLER);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the protocol ID of the player is allowed to bypass verification
|
||||
if (Sonar.get().getConfig().getVerification().getWhitelistedProtocols()
|
||||
.contains(protocolVersion.getProtocol())) {
|
||||
ctx.fireChannelRead(serverLogin);
|
||||
deject(channel.pipeline());
|
||||
return;
|
||||
}
|
||||
|
||||
// Queue the connection for further processing
|
||||
FALLBACK.getQueue().queue(inetAddress, () -> channel.eventLoop().execute(() -> {
|
||||
// Check if the username matches the valid name regex to prevent
|
||||
// UTF-16 names or other types of exploits
|
||||
if (!Sonar.get().getConfig().getVerification().getValidNameRegex()
|
||||
.matcher(serverLogin.getUsername()).matches()) {
|
||||
customDisconnect(channel, protocolVersion, invalidUsername, MINECRAFT_DECODER, HANDLER);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create an instance for the Fallback connection
|
||||
user = new FallbackUserWrapper(channel, inetAddress, protocolVersion);
|
||||
// Let the verification handler take over the channel
|
||||
user.hijack(serverLogin.getUsername(), offlineUUID, MINECRAFT_ENCODER, MINECRAFT_DECODER, READ_TIMEOUT, HANDLER);
|
||||
}));
|
||||
}
|
||||
}
|
||||
-51
@@ -1,51 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.velocity.fallback;
|
||||
|
||||
import com.velocitypowered.api.event.PostOrder;
|
||||
import com.velocitypowered.api.event.Subscribe;
|
||||
import com.velocitypowered.api.event.connection.PreLoginEvent;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.velocity.SonarVelocity;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.util.Objects;
|
||||
|
||||
public final class FallbackLoginListener {
|
||||
|
||||
@Subscribe(order = PostOrder.LAST)
|
||||
public void handle(final @NotNull PreLoginEvent event) {
|
||||
final int maxOnlinePerIp = Sonar.get().getConfig().getMaxOnlinePerIp();
|
||||
// Don't do anything if the setting is disabled
|
||||
if (maxOnlinePerIp <= 0) return;
|
||||
|
||||
final InetAddress inetAddress = event.getConnection().getRemoteAddress().getAddress();
|
||||
|
||||
// Check if the number of online players using the same IP address as
|
||||
// the connecting player is greater than the configured amount
|
||||
final long onlinePerIp = SonarVelocity.INSTANCE.getPlugin().getServer().getAllPlayers().stream()
|
||||
.filter(player -> Objects.equals(player.getRemoteAddress().getAddress(), inetAddress))
|
||||
.count();
|
||||
|
||||
// We use '>=' because the player connecting to the server hasn't joined yet
|
||||
if (onlinePerIp >= maxOnlinePerIp) {
|
||||
event.setResult(PreLoginEvent.PreLoginComponentResult.denied(Sonar.get().getConfig().getTooManyOnlinePerIp()));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user