mirror of
https://github.com/jonesdevelopment/sonar.git
synced 2026-11-05 09:20:15 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a17b726798 | ||
|
|
50d3b43f63 | ||
|
|
0ff0194894 | ||
|
|
279fe2e3e9 | ||
|
|
ffd3a7e5c2 | ||
|
|
4474f44b0a | ||
|
|
b159947d6d | ||
|
|
a29dfa15e4 | ||
|
|
cb9fe75db8 | ||
|
|
23815351f0 | ||
|
|
5a68ea8af6 | ||
|
|
9b54b484fb | ||
|
|
4fa56f2f3c | ||
|
|
51751584dc | ||
|
|
3955f9bc8a | ||
|
|
e333162032 | ||
|
|
b3a7f4de09 | ||
|
|
5752f1e208 | ||
|
|
1c6ba7050d | ||
|
|
340d4a225d |
No files matched your search
@@ -8,7 +8,6 @@ joining the [Discord](https://jonesdev.xyz/discord).
|
||||
2. Follow the coding guidelines for your code.
|
||||
3. Test your code before committing and pushing it.
|
||||
4. Use [semantic](https://gist.github.com/joshbuchea/6f47e86d2510bce28f8e7f42ae84c716) commit messages for your commits.
|
||||
5. Create a feature branch or push to the `dev` branch.
|
||||
|
||||
## Coding Guidelines:
|
||||
|
||||
|
||||
+45
-59
@@ -29,81 +29,67 @@ allprojects {
|
||||
apply(plugin = "io.github.goooler.shadow")
|
||||
|
||||
dependencies {
|
||||
compileOnly("org.projectlombok:lombok:1.18.30")
|
||||
annotationProcessor("org.projectlombok:lombok:1.18.30")
|
||||
compileOnly("org.projectlombok:lombok:1.18.32")
|
||||
annotationProcessor("org.projectlombok:lombok:1.18.32")
|
||||
|
||||
testCompileOnly("org.projectlombok:lombok:1.18.30")
|
||||
testAnnotationProcessor("org.projectlombok:lombok:1.18.30")
|
||||
testCompileOnly("org.projectlombok:lombok:1.18.32")
|
||||
testAnnotationProcessor("org.projectlombok:lombok:1.18.32")
|
||||
|
||||
// adventure
|
||||
implementation("net.kyori:adventure-text-minimessage:4.16.0")
|
||||
implementation("net.kyori:adventure-text-serializer-gson:4.16.0")
|
||||
implementation("net.kyori:adventure-nbt:4.16.0")
|
||||
compileOnly("net.kyori:adventure-text-minimessage:4.16.0")
|
||||
compileOnly("net.kyori:adventure-text-serializer-gson:4.16.0")
|
||||
|
||||
compileOnly("com.j256.ormlite:ormlite-jdbc:6.1") // ORMLite
|
||||
compileOnly("com.github.ben-manes.caffeine:caffeine:3.1.8") // caching
|
||||
compileOnly("io.netty:netty-all:4.1.107.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")
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
sequenceOf("api", "bukkit", "bungee", "common", "velocity").forEach {
|
||||
// We want the jar to actually contain our modules
|
||||
implementation(project(":$it"))
|
||||
tasks {
|
||||
shadowJar {
|
||||
// Set the file name of the shadowed jar
|
||||
archiveFileName.set("${rootProject.name}-${project.name.replaceFirstChar { it.uppercase() }}.jar")
|
||||
|
||||
// Remove file timestamps
|
||||
isPreserveFileTimestamps = false
|
||||
|
||||
// Relocate libraries
|
||||
relocate("org.bstats", "xyz.jonesdev.sonar.libs.bstats")
|
||||
relocate("com.alessiodp.libby", "xyz.jonesdev.sonar.libs.libby")
|
||||
relocate("com.simpleyaml", "xyz.jonesdev.sonar.libs.yaml")
|
||||
relocate("com.google.gson", "xyz.jonesdev.sonar.libs.gson")
|
||||
relocate("com.j256.ormlite", "xyz.jonesdev.sonar.libs.ormlite")
|
||||
relocate("com.github.benmanes.caffeine", "xyz.jonesdev.sonar.libs.caffeine")
|
||||
relocate("com.mysql", "xyz.jonesdev.sonar.libs.mysql")
|
||||
relocate("org.mariadb", "xyz.jonesdev.sonar.libs.mariadb")
|
||||
relocate("xyz.jonesdev.capja", "xyz.jonesdev.sonar.libs.capja")
|
||||
|
||||
// Exclude unnecessary metadata information
|
||||
exclude("META-INF/*/**")
|
||||
}
|
||||
|
||||
compileJava {
|
||||
options.encoding = "UTF-8"
|
||||
}
|
||||
|
||||
jar {
|
||||
manifest {
|
||||
// Set the implementation version, so we can create exact version
|
||||
// information in-game and make it accessible to the user.
|
||||
attributes["Implementation-Version"] = rootProject.version
|
||||
attributes["Implementation-Vendor"] = "Jones Development, Sonar Contributors"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks {
|
||||
jar {
|
||||
manifest {
|
||||
// Set the implementation version, so we can create exact version
|
||||
// information in-game and make it accessible to the user.
|
||||
attributes["Implementation-Version"] = version
|
||||
attributes["Implementation-Vendor"] = "Jones Development, Sonar Contributors"
|
||||
}
|
||||
}
|
||||
|
||||
shadowJar {
|
||||
// Set the file name of the shadowed jar
|
||||
archiveFileName.set("${rootProject.name}.jar")
|
||||
|
||||
// Remove file timestamps
|
||||
isPreserveFileTimestamps = false
|
||||
|
||||
// Relocate libraries
|
||||
relocate("org.bstats", "xyz.jonesdev.sonar.libs.bstats")
|
||||
relocate("com.alessiodp.libby", "xyz.jonesdev.sonar.libs.libby")
|
||||
relocate("com.simpleyaml", "xyz.jonesdev.sonar.libs.yaml")
|
||||
relocate("com.google.gson", "xyz.jonesdev.sonar.libs.gson")
|
||||
relocate("com.j256.ormlite", "xyz.jonesdev.sonar.libs.ormlite")
|
||||
relocate("com.github.benmanes.caffeine", "xyz.jonesdev.sonar.libs.caffeine")
|
||||
relocate("com.mysql", "xyz.jonesdev.sonar.libs.mysql")
|
||||
relocate("org.mariadb", "xyz.jonesdev.sonar.libs.mariadb")
|
||||
relocate("xyz.jonesdev.capja", "xyz.jonesdev.sonar.libs.capja")
|
||||
|
||||
// TODO: actually fix this :/
|
||||
// We have to be careful here, so we don't accidentally break adventure
|
||||
relocate("net.kyori", "xyz.jonesdev.sonar.libs.kyori") {
|
||||
exclude("net.kyori.adventure.text.**")
|
||||
exclude("net.kyori.adventure.audience.Audience")
|
||||
exclude("net.kyori.adventure.title.*")
|
||||
}
|
||||
|
||||
// Exclude unnecessary metadata information
|
||||
exclude("META-INF/*/**")
|
||||
// We want to load our own Gson dynamically
|
||||
exclude("com/google/gson/**")
|
||||
}
|
||||
|
||||
compileJava {
|
||||
options.encoding = "UTF-8"
|
||||
}
|
||||
|
||||
// This is a small wrapper tasks to simplify the building process
|
||||
register("build-sonar") {
|
||||
dependsOn(clean, shadowJar)
|
||||
val subprojects = listOf("api", "common", "bukkit", "bungee", "velocity")
|
||||
val buildTasks = subprojects.flatMap { listOf("$it:clean", "$it:shadowJar") }
|
||||
dependsOn(buildTasks)
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Fix permissions so we are actually able to run our gradle script
|
||||
chmod +x gradle*
|
||||
# You can also use clean shadowJar if something goes wrong here
|
||||
# e.g. "./gradlew clean shadowJar --stacktrace"
|
||||
./gradlew build-sonar --stacktrace
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
group=xyz.jonesdev.sonar
|
||||
version=2.0.24
|
||||
version=2.0.27
|
||||
description=Effective Anti-bot plugin for Velocity, BungeeCord, and Bukkit (1.7-latest)
|
||||
@@ -18,6 +18,7 @@
|
||||
package xyz.jonesdev.sonar.api;
|
||||
|
||||
import net.kyori.adventure.audience.Audience;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import xyz.jonesdev.sonar.api.attack.AttackTracker;
|
||||
@@ -33,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;
|
||||
|
||||
@@ -43,6 +45,7 @@ public interface Sonar {
|
||||
* Since we want to use Adventure on every server platform,
|
||||
* we have to use their platform module to support BungeeCord and Bukkit
|
||||
*/
|
||||
@ApiStatus.Internal
|
||||
@Nullable Audience audience(final @Nullable UUID uniqueId);
|
||||
|
||||
/**
|
||||
@@ -50,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;
|
||||
}
|
||||
}
|
||||
@@ -28,30 +28,33 @@ import java.util.function.Predicate;
|
||||
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public abstract class InvocationSource {
|
||||
public final class InvocationSource {
|
||||
private final UUID uuid;
|
||||
private final Audience audience;
|
||||
private final boolean player;
|
||||
private final Predicate<String> permissionFunction;
|
||||
|
||||
/**
|
||||
* Sends an empty chat message to the command executor
|
||||
* @return True, if {@link InvocationSource#uuid} is not null
|
||||
* @apiNote This indicates a player as a player will always have a UUID
|
||||
*/
|
||||
public final void sendMessage() {
|
||||
sendMessage(Component.empty());
|
||||
public boolean isPlayer() {
|
||||
return uuid != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a message to the command executor
|
||||
*
|
||||
* @apiNote We should probably use cached components...
|
||||
* (See {@link InvocationSource#sendMessage(Component)})
|
||||
*/
|
||||
public final void sendMessage(final String legacy) {
|
||||
public void sendMessage(final String legacy) {
|
||||
sendMessage(MiniMessage.miniMessage().deserialize(legacy));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a message to the command executor
|
||||
*/
|
||||
public final void sendMessage(final Component component) {
|
||||
public void sendMessage(final Component component) {
|
||||
audience.sendMessage(component);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -171,15 +172,16 @@ public final class SonarConfiguration {
|
||||
|
||||
// Verification
|
||||
verification.timing = Verification.Timing.valueOf(generalConfig.getString("verification.timing"));
|
||||
|
||||
verification.gravity.enabled = generalConfig.getBoolean("verification.checks.gravity.enabled");
|
||||
verification.gravity.checkCollisions = generalConfig.getBoolean("verification.checks.gravity.check-collisions");
|
||||
verification.gravity.captchaOnFail = generalConfig.getBoolean("verification.checks.gravity.captcha-on-fail");
|
||||
verification.gravity.maxMovementTicks = clamp(generalConfig.getInt("verification.checks.gravity.max-movement-ticks"), 2, 100);
|
||||
verification.gravity.maxIgnoredTicks = clamp(generalConfig.getInt("verification.checks.gravity.max-ignored-ticks"), 1, 128);
|
||||
verification.gravity.gamemode = Verification.Gravity.Gamemode.valueOf(generalConfig.getString("verification.checks.gravity.gamemode"));
|
||||
|
||||
verification.map.timing = Verification.Timing.valueOf(generalConfig.getString("verification.checks.map-captcha.timing"));
|
||||
verification.vehicle.timing = Verification.Timing.valueOf(generalConfig.getString("verification.checks.vehicle.timing"));
|
||||
|
||||
verification.map.timing = Verification.Timing.valueOf(generalConfig.getString("verification.checks.map-captcha.timing"));
|
||||
verification.map.flare = generalConfig.getBoolean("verification.checks.map-captcha.effects.flare");
|
||||
verification.map.scratches = generalConfig.getBoolean("verification.checks.map-captcha.effects.scratches");
|
||||
verification.map.ripple = generalConfig.getBoolean("verification.checks.map-captcha.effects.ripple");
|
||||
@@ -189,7 +191,6 @@ public final class SonarConfiguration {
|
||||
"verification.checks.map-captcha.effects.saturation", 0.3);
|
||||
verification.map.distortion = (float) generalConfig.getYaml().getDouble(
|
||||
"verification.checks.map-captcha.effects.distortion", 2);
|
||||
|
||||
verification.map.precomputeAmount = generalConfig.getInt("verification.checks.map-captcha.precompute");
|
||||
verification.map.maxDuration = generalConfig.getInt("verification.checks.map-captcha.max-duration");
|
||||
verification.map.maxTries = generalConfig.getInt("verification.checks.map-captcha.max-tries");
|
||||
@@ -202,7 +203,6 @@ public final class SonarConfiguration {
|
||||
|
||||
verification.validNameRegex = Pattern.compile(generalConfig.getString("verification.checks.valid-name-regex"));
|
||||
verification.validLocaleRegex = Pattern.compile(generalConfig.getString("verification.checks.valid-locale-regex"));
|
||||
verification.maxPing = clamp(generalConfig.getInt("verification.checks.max-ping"), 500, 30000);
|
||||
verification.maxLoginPackets = clamp(generalConfig.getInt("verification.checks.max-login-packets"), 128, 8192);
|
||||
|
||||
verification.logConnections = generalConfig.getBoolean("verification.log-connections");
|
||||
@@ -210,6 +210,7 @@ public final class SonarConfiguration {
|
||||
verification.debugXYZPositions = generalConfig.getBoolean("verification.debug-xyz-positions");
|
||||
verification.readTimeout = clamp(generalConfig.getInt("verification.read-timeout"), 500, 30000);
|
||||
verification.reconnectDelay = clamp(generalConfig.getInt("verification.rejoin-delay"), 0, 100000);
|
||||
verification.rememberTime = clamp(generalConfig.getInt("verification.remember-time"), 0, 86400000);
|
||||
verification.blacklistTime = clamp(generalConfig.getInt("verification.blacklist-time"), 0, 86400000);
|
||||
verification.blacklistThreshold = clamp(generalConfig.getInt("verification.blacklist-threshold"), 0, 100);
|
||||
verification.whitelistedProtocols.clear();
|
||||
@@ -256,8 +257,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 +290,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 +320,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 +404,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
|
||||
@@ -435,6 +441,7 @@ public final class SonarConfiguration {
|
||||
|
||||
private final Map map = new Map();
|
||||
private final Gravity gravity = new Gravity();
|
||||
private final Vehicle vehicle = new Vehicle();
|
||||
private final Brand brand = new Brand();
|
||||
|
||||
@Getter
|
||||
@@ -464,7 +471,6 @@ public final class SonarConfiguration {
|
||||
private boolean captchaOnFail;
|
||||
private Gamemode gamemode;
|
||||
private int maxMovementTicks;
|
||||
private int maxIgnoredTicks;
|
||||
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
@@ -479,6 +485,11 @@ public final class SonarConfiguration {
|
||||
}
|
||||
}
|
||||
|
||||
@Getter
|
||||
public static final class Vehicle {
|
||||
private Timing timing;
|
||||
}
|
||||
|
||||
@Getter
|
||||
public static final class Brand {
|
||||
private boolean enabled;
|
||||
@@ -497,9 +508,9 @@ public final class SonarConfiguration {
|
||||
private String blacklistLog;
|
||||
|
||||
private int maxLoginPackets;
|
||||
private int maxPing;
|
||||
private int readTimeout;
|
||||
private int reconnectDelay;
|
||||
private int rememberTime;
|
||||
private int blacklistTime;
|
||||
private int blacklistThreshold;
|
||||
private final Collection<Integer> whitelistedProtocols = new HashSet<>(0);
|
||||
@@ -520,8 +531,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;
|
||||
@@ -538,6 +548,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());
|
||||
}
|
||||
}
|
||||
+5
-11
@@ -19,23 +19,17 @@ package xyz.jonesdev.sonar.api.event.impl;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.ToString;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.event.SonarEvent;
|
||||
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@EqualsAndHashCode
|
||||
@RequiredArgsConstructor
|
||||
public final class UserVerifyFailedEvent implements SonarEvent {
|
||||
private final FallbackUser user;
|
||||
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
|
||||
private final Optional<String> reason;
|
||||
|
||||
public UserVerifyFailedEvent(final FallbackUser user, final @Nullable String reason) {
|
||||
this.user = user;
|
||||
this.reason = Optional.ofNullable(reason);
|
||||
}
|
||||
private final @NotNull FallbackUser user;
|
||||
private final @NotNull String reason;
|
||||
}
|
||||
@@ -69,16 +69,20 @@ public final class Fallback {
|
||||
|
||||
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
|
||||
public boolean shouldVerifyNewPlayers() {
|
||||
return performTimingCheck(Sonar.get().getConfig().getVerification().getTiming());
|
||||
return shouldPerform(Sonar.get().getConfig().getVerification().getTiming());
|
||||
}
|
||||
|
||||
public boolean shouldDoMapCaptcha() {
|
||||
return performTimingCheck(Sonar.get().getConfig().getVerification().getMap().getTiming());
|
||||
public boolean shouldPerformCaptcha() {
|
||||
return shouldPerform(Sonar.get().getConfig().getVerification().getMap().getTiming());
|
||||
}
|
||||
|
||||
private boolean performTimingCheck(final SonarConfiguration.Verification.Timing timing) {
|
||||
public boolean shouldPerformVehicleCheck() {
|
||||
return shouldPerform(Sonar.get().getConfig().getVerification().getVehicle().getTiming());
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ package xyz.jonesdev.sonar.api.fallback;
|
||||
|
||||
import com.github.benmanes.caffeine.cache.Cache;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
@@ -29,7 +30,10 @@ import java.net.InetAddress;
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public final class FallbackRatelimiter {
|
||||
static final FallbackRatelimiter INSTANCE = new FallbackRatelimiter();
|
||||
private Cache<InetAddress, Byte> expiringCache;
|
||||
|
||||
private Cache<InetAddress, Byte> attemptCache;
|
||||
@Getter
|
||||
private Cache<InetAddress, Integer> failCountCache;
|
||||
|
||||
/**
|
||||
* We don't want to clean up the cache in the
|
||||
@@ -37,7 +41,8 @@ public final class FallbackRatelimiter {
|
||||
* it would take up too many resources.
|
||||
*/
|
||||
public void cleanUpCache() {
|
||||
expiringCache.cleanUp();
|
||||
attemptCache.cleanUp();
|
||||
failCountCache.cleanUp();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -49,12 +54,27 @@ public final class FallbackRatelimiter {
|
||||
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
|
||||
public boolean attempt(final @NotNull InetAddress inetAddress) {
|
||||
// Cache the IP address if it's not already cached
|
||||
if (!expiringCache.asMap().containsKey(inetAddress)) {
|
||||
if (!attemptCache.asMap().containsKey(inetAddress)) {
|
||||
// Cache the IP address
|
||||
expiringCache.put(inetAddress, (byte) 0);
|
||||
attemptCache.put(inetAddress, (byte) 0);
|
||||
return true;
|
||||
}
|
||||
// Deny the connection attempt
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the player is failing verifications too often
|
||||
* Increments the number of times this user has failed the verification
|
||||
*
|
||||
* @param inetAddress IP address of the player
|
||||
* @param count Count of previously failed verifications
|
||||
*/
|
||||
public void incrementFails(final @NotNull InetAddress inetAddress, final int count) {
|
||||
// Make sure to remove the old values from the cache
|
||||
if (count > 0) {
|
||||
failCountCache.invalidate(inetAddress);
|
||||
}
|
||||
failCountCache.put(inetAddress, count + 1);
|
||||
}
|
||||
}
|
||||
@@ -17,18 +17,14 @@
|
||||
|
||||
package xyz.jonesdev.sonar.api.fallback;
|
||||
|
||||
import com.github.benmanes.caffeine.cache.Cache;
|
||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.util.ReferenceCountUtil;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.time.Duration;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface FallbackUser {
|
||||
@@ -38,12 +34,17 @@ public interface FallbackUser {
|
||||
|
||||
@NotNull ProtocolVersion getProtocolVersion();
|
||||
|
||||
@NotNull FallbackUserState getState();
|
||||
|
||||
void setState(final FallbackUserState state);
|
||||
|
||||
/**
|
||||
* Disconnect the player during/after verification
|
||||
* using our custom Disconnect packet.
|
||||
*
|
||||
* @param reason Disconnect message component
|
||||
*/
|
||||
@ApiStatus.Internal
|
||||
void disconnect(final @NotNull Component reason);
|
||||
|
||||
/**
|
||||
@@ -54,11 +55,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
|
||||
@@ -85,40 +86,6 @@ public interface FallbackUser {
|
||||
}
|
||||
}
|
||||
|
||||
Cache<InetAddress, Integer> GLOBAL_FAIL_COUNT = Caffeine.newBuilder()
|
||||
.expireAfterWrite(Duration.ofMinutes(5)) // expire after 5 minutes
|
||||
.build();
|
||||
|
||||
/**
|
||||
* Increments the number of times this user (by IP address) has failed the verification
|
||||
*/
|
||||
default void incrementFails() {
|
||||
final InetAddress inetAddress = Objects.requireNonNull(getInetAddress());
|
||||
final int failCount = getFailedCount();
|
||||
// Make sure to remove the old values from the cache
|
||||
if (failCount > 0) {
|
||||
GLOBAL_FAIL_COUNT.invalidate(inetAddress);
|
||||
}
|
||||
GLOBAL_FAIL_COUNT.put(inetAddress, failCount + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The number of times this user (by IP address) has failed the verification
|
||||
* @apiNote Returns 0 if no key/value is present in the cache
|
||||
*/
|
||||
default int getFailedCount() {
|
||||
// Make sure to clean up the cache before we try to get cached values
|
||||
GLOBAL_FAIL_COUNT.cleanUp();
|
||||
return GLOBAL_FAIL_COUNT.asMap().getOrDefault(Objects.requireNonNull(getInetAddress()), 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all previously failed verifications
|
||||
*/
|
||||
default void invalidateFails() {
|
||||
GLOBAL_FAIL_COUNT.invalidate(Objects.requireNonNull(getInetAddress()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnects the player who failed the verification
|
||||
* and caches them in FAILED_VERIFICATIONS.
|
||||
@@ -126,6 +93,8 @@ public interface FallbackUser {
|
||||
* the player will be temporarily denied from verifying.
|
||||
*
|
||||
* @param reason Reason for failing the verification
|
||||
* @apiNote The {@link xyz.jonesdev.sonar.api.event.impl.UserVerifyFailedEvent}
|
||||
* will not be thrown if no reason is given
|
||||
*/
|
||||
void fail(final @Nullable String reason);
|
||||
void fail(final @NotNull String reason);
|
||||
}
|
||||
+20
-11
@@ -15,18 +15,27 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.velocity.command;
|
||||
package xyz.jonesdev.sonar.api.fallback;
|
||||
|
||||
import com.velocitypowered.api.command.CommandSource;
|
||||
import com.velocitypowered.api.proxy.Player;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.command.InvocationSource;
|
||||
public enum FallbackUserState {
|
||||
// 1.20.2 configuration state
|
||||
LOGIN_ACK, CONFIGURE,
|
||||
// pre-JOIN ping check
|
||||
KEEP_ALIVE,
|
||||
// post-JOIN checks
|
||||
CLIENT_SETTINGS, PLUGIN_MESSAGE, TRANSACTION,
|
||||
// PLAY checks
|
||||
TELEPORT, POSITION,
|
||||
// Vehicle check
|
||||
VEHICLE,
|
||||
// CAPTCHA
|
||||
CAPTCHA,
|
||||
// Done
|
||||
SUCCESS,
|
||||
// Placeholder
|
||||
FAILED;
|
||||
|
||||
public final class VelocityInvocationSource extends InvocationSource {
|
||||
public VelocityInvocationSource(final @NotNull CommandSource sender) {
|
||||
super(sender instanceof Player ? ((Player) sender).getUniqueId() : null,
|
||||
sender,
|
||||
sender instanceof Player,
|
||||
sender::hasPermission);
|
||||
public boolean canReceivePackets() {
|
||||
return this != FAILED && this != SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -21,60 +21,94 @@ 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) {
|
||||
return size + " B";
|
||||
}
|
||||
// https://stackoverflow.com/questions/3758606/how-can-i-convert-byte-size-into-a-human-readable-format-in-java
|
||||
final int group = (63 - Long.numberOfLeadingZeros(size)) / 10;
|
||||
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";
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Number of available processors for the JVM
|
||||
*/
|
||||
public int getVirtualCores() {
|
||||
return RUNTIME.availableProcessors();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Current process CPU usage in percent
|
||||
*/
|
||||
public double getProcessCPUUsage() {
|
||||
return MX.getProcessCpuLoad() * 100;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Current per-core process CPU usage in percent
|
||||
*/
|
||||
public double getAverageProcessCPUUsage() {
|
||||
return getProcessCPUUsage() / getVirtualCores();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Current system CPU usage in percent
|
||||
*/
|
||||
public double getSystemCPUUsage() {
|
||||
return MX.getSystemCpuLoad() * 100;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Current per-core system CPU usage in percent
|
||||
*/
|
||||
public double getAverageSystemCPUUsage() {
|
||||
return getSystemCPUUsage() / getVirtualCores();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Current system load average in percent
|
||||
* @apiNote This returns the global process load, not the CPU usage
|
||||
*/
|
||||
public double getSystemLoadAverage() {
|
||||
return MX.getSystemLoadAverage() * 100;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Memory (in bytes) the JVM will attempt to use
|
||||
*/
|
||||
public long getMaxMemory() {
|
||||
return RUNTIME.maxMemory();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Memory (in bytes) available to the host machine
|
||||
*/
|
||||
public long getTotalMemory() {
|
||||
return RUNTIME.totalMemory();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Memory (in bytes) the JVM can allocate
|
||||
*/
|
||||
public long getFreeMemory() {
|
||||
return RUNTIME.freeMemory();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Memory (in bytes) the JVM is using
|
||||
*/
|
||||
public long getUsedMemory() {
|
||||
return getTotalMemory() - getFreeMemory();
|
||||
}
|
||||
|
||||
@@ -22,6 +22,11 @@ import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public final class MariaDbDatabaseTypeAdapter extends MariaDbDatabaseType {
|
||||
|
||||
/**
|
||||
* We need to override the default driver class name
|
||||
* in order to use the custom relocated MariaDB driver
|
||||
* Read more: {@link MysqlDatabaseTypeAdapter}
|
||||
*/
|
||||
@Override
|
||||
protected String @NotNull [] getDriverClassNames() {
|
||||
return new String[]{"xyz.jonesdev.sonar.libs.mariadb.jdbc.Driver"};
|
||||
|
||||
@@ -22,6 +22,11 @@ import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public final class MysqlDatabaseTypeAdapter extends MysqlDatabaseType {
|
||||
|
||||
/**
|
||||
* We need to override the default driver class name
|
||||
* in order to use the custom relocated MySQL driver
|
||||
* Read more: {@link MariaDbDatabaseTypeAdapter}
|
||||
*/
|
||||
@Override
|
||||
protected String @NotNull [] getDriverClassNames() {
|
||||
return new String[]{"xyz.jonesdev.sonar.libs.mysql.cj.jdbc.Driver"};
|
||||
|
||||
@@ -20,40 +20,96 @@ package xyz.jonesdev.sonar.api.statistics;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
|
||||
public interface SonarStatistics {
|
||||
|
||||
/**
|
||||
* @return Number of connection establishments per second
|
||||
*/
|
||||
long getConnectionsPerSecond();
|
||||
|
||||
/**
|
||||
* @return Number of logins per second
|
||||
*/
|
||||
long getLoginsPerSecond();
|
||||
|
||||
/**
|
||||
* @return Currently incoming bytes per second
|
||||
*/
|
||||
long getCurrentIncomingBandwidth();
|
||||
|
||||
/**
|
||||
* @return Currently outgoing bytes per second
|
||||
*/
|
||||
long getCurrentOutgoingBandwidth();
|
||||
|
||||
/**
|
||||
* @return Total incoming bytes per second
|
||||
*/
|
||||
long getTotalIncomingBandwidth();
|
||||
|
||||
/**
|
||||
* @return Total outgoing bytes per second
|
||||
*/
|
||||
long getTotalOutgoingBandwidth();
|
||||
|
||||
/**
|
||||
* @return Same as {@link SonarStatistics#getCurrentIncomingBandwidth} but formatted
|
||||
*/
|
||||
String getCurrentIncomingBandwidthFormatted();
|
||||
|
||||
/**
|
||||
* @return Same as {@link SonarStatistics#getCurrentOutgoingBandwidth} but formatted
|
||||
*/
|
||||
String getCurrentOutgoingBandwidthFormatted();
|
||||
|
||||
/**
|
||||
* @return Same as {@link SonarStatistics#getTotalIncomingBandwidth} but formatted
|
||||
*/
|
||||
String getTotalIncomingBandwidthFormatted();
|
||||
|
||||
/**
|
||||
* @return Same as {@link SonarStatistics#getTotalOutgoingBandwidth} but formatted
|
||||
*/
|
||||
String getTotalOutgoingBandwidthFormatted();
|
||||
|
||||
/**
|
||||
* @return Total number of players who logged into the server
|
||||
*/
|
||||
int getTotalPlayersJoined();
|
||||
|
||||
/**
|
||||
* @return Total number of players who tried to verify
|
||||
*/
|
||||
int getTotalPlayersVerified();
|
||||
|
||||
/**
|
||||
* @return Total number of players who passed the verification
|
||||
*/
|
||||
int getTotalSuccessfulVerifications();
|
||||
|
||||
/**
|
||||
* @return Total number of players who failed the verification
|
||||
*/
|
||||
int getTotalFailedVerifications();
|
||||
|
||||
/**
|
||||
* @return Total number of players who passed the verification
|
||||
*/
|
||||
long getCurrentAttemptedVerifications();
|
||||
|
||||
/**
|
||||
* @return Total number of players who tried verifying
|
||||
*/
|
||||
int getTotalAttemptedVerifications();
|
||||
|
||||
/**
|
||||
* @return Current number of IP addresses that are blacklisted
|
||||
*/
|
||||
long getCurrentBlacklistSize();
|
||||
|
||||
/**
|
||||
* @return Total number of cases where an IP was blacklisted
|
||||
* @apiNote This does not describe the current amount of blacklisted IPs
|
||||
*/
|
||||
long getTotalBlacklistSize();
|
||||
|
||||
@ApiStatus.Internal
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -32,18 +31,20 @@ public final class SystemTimer {
|
||||
start = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Time between the current timestamp and the start timestamp
|
||||
*/
|
||||
public long delay() {
|
||||
return System.currentTimeMillis() - start;
|
||||
}
|
||||
|
||||
/**
|
||||
* @apiNote Read more: {@link SystemTimer#delay}
|
||||
*/
|
||||
public boolean elapsed(final long amount) {
|
||||
return delay() >= amount;
|
||||
}
|
||||
|
||||
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,19 +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 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 {
|
||||
@@ -42,52 +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);
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
@@ -34,14 +34,18 @@ import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import static java.net.HttpURLConnection.HTTP_NO_CONTENT;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public final class DiscordWebhook {
|
||||
private final String url;
|
||||
|
||||
private static final Gson GSON = new GsonBuilder().disableInnerClassSerialization().create();
|
||||
|
||||
private static final Gson GSON = new GsonBuilder().create();
|
||||
private static final ExecutorService HTTP_REQUEST_SERVICE = Executors.newSingleThreadExecutor();
|
||||
|
||||
/**
|
||||
* Asynchronously posts a Discord webhook via HTTPS
|
||||
*/
|
||||
public void post(final Supplier<SonarConfiguration.Webhook.Embed> embed) {
|
||||
HTTP_REQUEST_SERVICE.execute(() -> {
|
||||
try {
|
||||
@@ -51,9 +55,12 @@ public final class DiscordWebhook {
|
||||
try (final OutputStream outputStream = urlConnection.getOutputStream()) {
|
||||
outputStream.write(serializedContent.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
// Check if the connection was a success
|
||||
if (urlConnection.getResponseCode() != 204) {
|
||||
throw new IllegalStateException("Unexpected response code " + urlConnection.getResponseCode());
|
||||
|
||||
// Warn the user if the connection might've not been successful
|
||||
// I don't know why Discord doesn't respond with OK, but I guess this should be fine
|
||||
final int code = urlConnection.getResponseCode();
|
||||
if (code != HTTP_NO_CONTENT) {
|
||||
Sonar.get().getLogger().warn("Unexpected Discord webhook response code {}", code);
|
||||
}
|
||||
} catch (Exception exception) {
|
||||
Sonar.get().getLogger().error("Could not send webhook: {}", exception);
|
||||
@@ -61,7 +68,7 @@ public final class DiscordWebhook {
|
||||
});
|
||||
}
|
||||
|
||||
private String prepareSerializedContent(final SonarConfiguration.Webhook.@NotNull Embed embed) {
|
||||
private static String prepareSerializedContent(final SonarConfiguration.Webhook.@NotNull Embed embed) {
|
||||
final String content = Sonar.get().getConfig().getWebhook().getContent();
|
||||
final String username = Sonar.get().getConfig().getWebhook().getUsername();
|
||||
final String avatarUrl = Sonar.get().getConfig().getWebhook().getAvatarUrl();
|
||||
|
||||
@@ -14,6 +14,7 @@ bukkit {
|
||||
website = "https://jonesdev.xyz/discord/"
|
||||
load = BukkitPluginDescription.PluginLoadOrder.POSTWORLD
|
||||
softDepend = listOf("Geyser-Spigot", "floodgate", "Protocolize", "packetevents", "ProtocolLib", "ViaVersion")
|
||||
foliaSupported = true
|
||||
|
||||
commands {
|
||||
register("sonar") {
|
||||
@@ -28,11 +29,19 @@ repositories {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly(project(":api"))
|
||||
compileOnly(project(":common"))
|
||||
implementation(project(":api"))
|
||||
implementation(project(":common"))
|
||||
|
||||
// MiniMessage platform support
|
||||
// adventure platform support
|
||||
implementation("net.kyori:adventure-platform-bukkit:4.3.2")
|
||||
implementation("net.kyori:adventure-platform-api:4.3.2")
|
||||
implementation("net.kyori:adventure-platform-facet:4.3.2")
|
||||
|
||||
// adventure minimessage
|
||||
compileOnly("net.kyori:adventure-text-minimessage:4.16.0")
|
||||
compileOnly("net.kyori:adventure-text-serializer-gson:4.16.0")
|
||||
// adventure nbt
|
||||
implementation("net.kyori:adventure-nbt:4.16.0")
|
||||
|
||||
// We have to use 1.8 for backwards compatibility
|
||||
compileOnly("org.spigotmc:spigot-api:1.8.8-R0.1-SNAPSHOT")
|
||||
@@ -44,5 +53,11 @@ dependencies {
|
||||
implementation("com.alessiodp.libby:libby-bukkit:2.0.0-SNAPSHOT")
|
||||
}
|
||||
|
||||
tasks {
|
||||
shadowJar {
|
||||
relocate("net.kyori", "xyz.jonesdev.sonar.libs.kyori")
|
||||
}
|
||||
}
|
||||
|
||||
java.sourceCompatibility = JavaVersion.VERSION_11
|
||||
java.targetCompatibility = JavaVersion.VERSION_11
|
||||
@@ -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;
|
||||
|
||||
@@ -52,13 +52,27 @@ public final class SonarBukkit extends SonarBootstrap<SonarBukkitPlugin> {
|
||||
private final BukkitAudiences bukkitAudiences = BukkitAudiences.create(getPlugin());
|
||||
|
||||
@Override
|
||||
public @NotNull Audience audience(final @Nullable UUID uniqueId) {
|
||||
public @Nullable Audience audience(final @Nullable UUID uniqueId) {
|
||||
if (uniqueId == null) {
|
||||
return bukkitAudiences.console();
|
||||
return null;
|
||||
}
|
||||
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
|
||||
|
||||
+6
-1
@@ -21,9 +21,11 @@ import org.bukkit.command.Command;
|
||||
import org.bukkit.command.CommandExecutor;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.command.TabExecutor;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.command.InvocationSource;
|
||||
import xyz.jonesdev.sonar.api.command.SonarCommand;
|
||||
import xyz.jonesdev.sonar.bukkit.SonarBukkit;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@@ -37,7 +39,10 @@ public final class BukkitSonarCommand implements CommandExecutor, TabExecutor, S
|
||||
final String label,
|
||||
final String[] args) {
|
||||
// Create our own invocation source wrapper to handle messages properly
|
||||
final InvocationSource invocationSource = new BukkitInvocationSource(sender);
|
||||
final InvocationSource invocationSource = new InvocationSource(
|
||||
sender instanceof Player ? ((Player) sender).getUniqueId() : null,
|
||||
SonarBukkit.INSTANCE.getBukkitAudiences().sender(sender),
|
||||
sender::hasPermission);
|
||||
// Pass the invocation source and command arguments to our command handler
|
||||
handle(invocationSource, args);
|
||||
return true; // Valid
|
||||
|
||||
@@ -13,14 +13,22 @@ bungee {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly(project(":api"))
|
||||
compileOnly(project(":common"))
|
||||
implementation(project(":api"))
|
||||
implementation(project(":common"))
|
||||
|
||||
compileOnly("net.md_5:bungeecord-proxy:master-SNAPSHOT")
|
||||
testCompileOnly("net.md_5:bungeecord-proxy:master-SNAPSHOT")
|
||||
|
||||
// MiniMessage platform support
|
||||
// adventure platform support
|
||||
implementation("net.kyori:adventure-platform-bungeecord:4.3.2")
|
||||
implementation("net.kyori:adventure-platform-api:4.3.2")
|
||||
implementation("net.kyori:adventure-platform-facet:4.3.2")
|
||||
|
||||
// adventure minimessage
|
||||
compileOnly("net.kyori:adventure-text-minimessage:4.16.0")
|
||||
compileOnly("net.kyori:adventure-text-serializer-gson:4.16.0")
|
||||
// adventure nbt
|
||||
implementation("net.kyori:adventure-nbt:4.16.0")
|
||||
|
||||
// Implement bStats.org for metrics
|
||||
implementation("org.bstats:bstats-bungeecord:3.0.2")
|
||||
@@ -29,5 +37,11 @@ dependencies {
|
||||
implementation("com.alessiodp.libby:libby-bungee:2.0.0-SNAPSHOT")
|
||||
}
|
||||
|
||||
tasks {
|
||||
shadowJar {
|
||||
relocate("net.kyori", "xyz.jonesdev.sonar.libs.kyori")
|
||||
}
|
||||
}
|
||||
|
||||
java.sourceCompatibility = JavaVersion.VERSION_11
|
||||
java.targetCompatibility = JavaVersion.VERSION_11
|
||||
@@ -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> {
|
||||
@@ -52,13 +51,26 @@ public final class SonarBungee extends SonarBootstrap<SonarBungeePlugin> {
|
||||
private final BungeeAudiences bungeeAudiences = BungeeAudiences.create(getPlugin());
|
||||
|
||||
@Override
|
||||
public @NotNull Audience audience(final @Nullable UUID uniqueId) {
|
||||
public @Nullable Audience audience(final @Nullable UUID uniqueId) {
|
||||
if (uniqueId == null) {
|
||||
return bungeeAudiences.console();
|
||||
return null;
|
||||
}
|
||||
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();
|
||||
}
|
||||
|
||||
-33
@@ -1,33 +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.command;
|
||||
|
||||
import net.md_5.bungee.api.CommandSender;
|
||||
import net.md_5.bungee.api.connection.ProxiedPlayer;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.command.InvocationSource;
|
||||
import xyz.jonesdev.sonar.bungee.SonarBungee;
|
||||
|
||||
public final class BungeeInvocationSource extends InvocationSource {
|
||||
public BungeeInvocationSource(final @NotNull CommandSender sender) {
|
||||
super(sender instanceof ProxiedPlayer ? ((ProxiedPlayer) sender).getUniqueId() : null,
|
||||
SonarBungee.INSTANCE.getBungeeAudiences().sender(sender),
|
||||
sender instanceof ProxiedPlayer,
|
||||
sender::hasPermission);
|
||||
}
|
||||
}
|
||||
+6
-1
@@ -18,11 +18,13 @@
|
||||
package xyz.jonesdev.sonar.bungee.command;
|
||||
|
||||
import net.md_5.bungee.api.CommandSender;
|
||||
import net.md_5.bungee.api.connection.ProxiedPlayer;
|
||||
import net.md_5.bungee.api.plugin.Command;
|
||||
import net.md_5.bungee.api.plugin.TabExecutor;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.command.InvocationSource;
|
||||
import xyz.jonesdev.sonar.api.command.SonarCommand;
|
||||
import xyz.jonesdev.sonar.bungee.SonarBungee;
|
||||
|
||||
import static java.util.Collections.emptyList;
|
||||
|
||||
@@ -34,7 +36,10 @@ public final class BungeeSonarCommand extends Command implements TabExecutor, So
|
||||
@Override
|
||||
public void execute(final @NotNull CommandSender sender, final String[] args) {
|
||||
// Create our own invocation source wrapper to handle messages properly
|
||||
final InvocationSource invocationSource = new BungeeInvocationSource(sender);
|
||||
final InvocationSource invocationSource = new InvocationSource(
|
||||
sender instanceof ProxiedPlayer ? ((ProxiedPlayer) sender).getUniqueId() : null,
|
||||
SonarBungee.INSTANCE.getBungeeAudiences().sender(sender),
|
||||
sender::hasPermission);
|
||||
// Pass the invocation source and command arguments to our command handler
|
||||
handle(invocationSource, args);
|
||||
}
|
||||
|
||||
+9
-137
@@ -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,25 +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.FallbackUser;
|
||||
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 {
|
||||
|
||||
@@ -75,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;
|
||||
}
|
||||
}
|
||||
@@ -87,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
|
||||
final FallbackUser 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)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,10 @@
|
||||
dependencies {
|
||||
compileOnly(project(":api"))
|
||||
|
||||
// adventure nbt for packets
|
||||
compileOnly("net.kyori:adventure-nbt:4.16.0")
|
||||
|
||||
// CAPTCHA image generation
|
||||
implementation("xyz.jonesdev.capja:capja:0.1.2")
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,8 @@ import com.alessiodp.libby.Library;
|
||||
import com.alessiodp.libby.LibraryManager;
|
||||
import lombok.experimental.UtilityClass;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.SonarPlatform;
|
||||
|
||||
@UtilityClass
|
||||
class LibraryLoader {
|
||||
@@ -71,5 +73,23 @@ class LibraryLoader {
|
||||
.relocate("org{}mariadb", "xyz{}jonesdev{}sonar{}libs{}mariadb")
|
||||
.build()
|
||||
);
|
||||
|
||||
// Only load adventure dependencies if needed
|
||||
if (Sonar.get().getPlatform() != SonarPlatform.VELOCITY) {
|
||||
libraryManager.loadLibraries(
|
||||
Library.builder()
|
||||
.groupId("net{}kyori")
|
||||
.artifactId("adventure-text-minimessage")
|
||||
.version("4.16.0")
|
||||
.relocate("net{}kyori", "xyz{}jonesdev{}sonar{}libs{}kyori")
|
||||
.build(),
|
||||
Library.builder()
|
||||
.groupId("net{}kyori")
|
||||
.artifactId("adventure-text-serializer-gson")
|
||||
.version("4.16.0")
|
||||
.relocate("net{}kyori", "xyz{}jonesdev{}sonar{}libs{}kyori")
|
||||
.build()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -68,12 +69,13 @@ public abstract class SonarBootstrap<T> implements Sonar {
|
||||
SonarSupplier.set(this);
|
||||
// Store our library manager for the API
|
||||
this.libraryManager = libraryManager;
|
||||
// Load all libraries before anything else
|
||||
LibraryLoader.loadLibraries(libraryManager);
|
||||
// Set the plugin instance
|
||||
this.plugin = plugin;
|
||||
this.dataDirectory = dataDirectory;
|
||||
this.platform = platform;
|
||||
// Load all libraries before anything else
|
||||
LibraryLoader.loadLibraries(libraryManager);
|
||||
// Load the rest of the components
|
||||
this.statistics = new GlobalSonarStatistics();
|
||||
this.verboseHandler = new Verbose();
|
||||
this.notificationHandler = new Notification();
|
||||
@@ -104,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) {
|
||||
@@ -132,10 +138,13 @@ public abstract class SonarBootstrap<T> implements Sonar {
|
||||
// Prepare cached packets
|
||||
FallbackPreparer.prepare();
|
||||
|
||||
// Update ratelimiter cache
|
||||
getFallback().getRatelimiter().setExpiringCache(Caffeine.newBuilder()
|
||||
// Update ratelimiter caches
|
||||
getFallback().getRatelimiter().setAttemptCache(Caffeine.newBuilder()
|
||||
.expireAfterWrite(Duration.ofMillis(getConfig().getVerification().getReconnectDelay()))
|
||||
.build());
|
||||
getFallback().getRatelimiter().setFailCountCache(Caffeine.newBuilder()
|
||||
.expireAfterWrite(Duration.ofMillis(getConfig().getVerification().getRememberTime()))
|
||||
.build());
|
||||
|
||||
// Update blacklist cache
|
||||
final long blacklistTime = getConfig().getVerification().getBlacklistTime();
|
||||
@@ -166,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
|
||||
|
||||
+172
-1
@@ -20,15 +20,27 @@ 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;
|
||||
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 {
|
||||
@@ -36,6 +48,7 @@ public class FallbackChannelHandlerAdapter extends ChannelInboundHandlerAdapter
|
||||
protected @Nullable String username;
|
||||
protected InetAddress inetAddress;
|
||||
protected ProtocolVersion protocolVersion;
|
||||
protected @Nullable FallbackUser user;
|
||||
|
||||
protected static final Fallback FALLBACK = Sonar.get().getFallback();
|
||||
|
||||
@@ -57,7 +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);
|
||||
FALLBACK.getQueue().getQueuedPlayers().remove(inetAddress);
|
||||
}
|
||||
// Make sure to let the server handle the rest
|
||||
ctx.fireChannelInactive();
|
||||
@@ -72,4 +85,162 @@ 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 player has already sent a handshake packet
|
||||
if (protocolVersion != null) {
|
||||
throw new CorruptedFrameException("Already sent handshake");
|
||||
}
|
||||
// Check if the hostname is invalid
|
||||
if (hostname.isEmpty()) {
|
||||
throw new CorruptedFrameException("Hostname is empty");
|
||||
}
|
||||
// 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 (GeyserUtil.isGeyserConnection(channel, socketAddress)) {
|
||||
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);
|
||||
// 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());
|
||||
}
|
||||
}
|
||||
+88
-55
@@ -22,15 +22,16 @@ import io.netty.channel.ChannelFutureListener;
|
||||
import io.netty.channel.ChannelHandler;
|
||||
import io.netty.channel.ChannelPipeline;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.event.impl.UserBlacklistedEvent;
|
||||
import xyz.jonesdev.sonar.api.event.impl.UserVerifyFailedEvent;
|
||||
import xyz.jonesdev.sonar.api.event.impl.UserVerifyJoinEvent;
|
||||
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
|
||||
import xyz.jonesdev.sonar.api.fallback.FallbackUserState;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacketDecoder;
|
||||
@@ -45,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"})
|
||||
@@ -53,6 +55,8 @@ public final class FallbackUserWrapper implements FallbackUser {
|
||||
private final ChannelPipeline pipeline;
|
||||
private final InetAddress inetAddress;
|
||||
private final ProtocolVersion protocolVersion;
|
||||
@Setter
|
||||
private FallbackUserState state = FallbackUserState.LOGIN_ACK; // 1.20.2+
|
||||
|
||||
public FallbackUserWrapper(final @NotNull Channel channel,
|
||||
final @NotNull InetAddress inetAddress,
|
||||
@@ -71,26 +75,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)
|
||||
@@ -114,33 +106,57 @@ 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
|
||||
public void fail(final @Nullable String reason) {
|
||||
public void fail(final @NotNull String reason) {
|
||||
// Make sure to set the state to failed to drop all handlers
|
||||
setState(FallbackUserState.FAILED);
|
||||
|
||||
// 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().getCurrentAttack() == null
|
||||
|| Sonar.get().getConfig().getVerification().isLogDuringAttack();
|
||||
|
||||
// Only disconnect the player and log if the channel is active
|
||||
if (channel.isActive()) {
|
||||
disconnect(Sonar.get().getConfig().getVerification().getVerificationFailed());
|
||||
|
||||
if (reason != null) {
|
||||
// Only log the failed message if the server isn't under attack.
|
||||
// We let the user override this through the configuration.
|
||||
if (!Sonar.get().getAttackTracker().isCurrentlyUnderAttack()
|
||||
|| Sonar.get().getConfig().getVerification().isLogDuringAttack()) {
|
||||
Sonar.get().getFallback().getLogger().info(Sonar.get().getConfig().getVerification().getFailedLog()
|
||||
.replace("%ip%", Sonar.get().getConfig().formatAddress(getInetAddress()))
|
||||
.replace("%protocol%", String.valueOf(getProtocolVersion().getProtocol()))
|
||||
.replace("%reason%", reason));
|
||||
}
|
||||
if (shouldLog) {
|
||||
Sonar.get().getFallback().getLogger().info(Sonar.get().getConfig().getVerification().getFailedLog()
|
||||
.replace("%ip%", Sonar.get().getConfig().formatAddress(getInetAddress()))
|
||||
.replace("%protocol%", String.valueOf(getProtocolVersion().getProtocol()))
|
||||
.replace("%reason%", reason));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,35 +166,40 @@ public final class FallbackUserWrapper implements FallbackUser {
|
||||
// Call the VerifyFailedEvent for external API usage
|
||||
Sonar.get().getEventManager().publish(new UserVerifyFailedEvent(this, reason));
|
||||
|
||||
// Use a label, so we can easily add more code beneath this method
|
||||
// Use a label, so we can easily add more code beneath this method in the future
|
||||
blacklist: {
|
||||
// Check if the player has too many failed attempts
|
||||
final int blacklistThreshold = Sonar.get().getConfig().getVerification().getBlacklistThreshold();
|
||||
// The user is allowed to disable the blacklist entirely by setting the threshold to 0
|
||||
if (blacklistThreshold <= 0) break blacklist;
|
||||
|
||||
// Add 1 to the amount of fails since we haven't updated the cached entries yet (for performance)
|
||||
final int failCount = getFailedCount() + 1;
|
||||
// Use 1 as the default amount of fails since we haven't cached anything yet
|
||||
final int failCount = Sonar.get().getFallback().getRatelimiter().getFailCountCache()
|
||||
.asMap().getOrDefault(inetAddress, 1);
|
||||
|
||||
// Now we simply need to check if the threshold is reached
|
||||
if (failCount >= blacklistThreshold) {
|
||||
// Call the BotBlacklistedEvent for external API usage
|
||||
Sonar.get().getEventManager().publish(new UserBlacklistedEvent(this));
|
||||
|
||||
// Increment amount of total blacklisted players
|
||||
GlobalSonarStatistics.totalBlacklistedPlayers++;
|
||||
|
||||
Sonar.get().getFallback().getBlacklist().put(getInetAddress(), (byte) 0);
|
||||
Sonar.get().getFallback().getLogger().info(Sonar.get().getConfig().getVerification().getBlacklistLog()
|
||||
.replace("%ip%", Sonar.get().getConfig().formatAddress(getInetAddress()))
|
||||
.replace("%protocol%", String.valueOf(getProtocolVersion().getProtocol())));
|
||||
|
||||
// Invalidate the cached entry to ensure memory safety
|
||||
invalidateFails();
|
||||
if (failCount < blacklistThreshold) {
|
||||
// Make sure we increment the number of fails
|
||||
Sonar.get().getFallback().getRatelimiter().incrementFails(inetAddress, failCount);
|
||||
break blacklist;
|
||||
}
|
||||
|
||||
// Make sure we increment the number of fails
|
||||
incrementFails();
|
||||
// Call the BotBlacklistedEvent for external API usage
|
||||
Sonar.get().getEventManager().publish(new UserBlacklistedEvent(this));
|
||||
|
||||
// Increment amount of total blacklisted players
|
||||
GlobalSonarStatistics.totalBlacklistedPlayers++;
|
||||
|
||||
Sonar.get().getFallback().getBlacklist().put(getInetAddress(), (byte) 0);
|
||||
|
||||
if (shouldLog) {
|
||||
Sonar.get().getFallback().getLogger().info(Sonar.get().getConfig().getVerification().getBlacklistLog()
|
||||
.replace("%ip%", Sonar.get().getConfig().formatAddress(getInetAddress()))
|
||||
.replace("%protocol%", String.valueOf(getProtocolVersion().getProtocol())));
|
||||
}
|
||||
|
||||
// Invalidate the cached entry to ensure memory safety
|
||||
Sonar.get().getFallback().getRatelimiter().getFailCountCache().invalidate(inetAddress);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -211,7 +232,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
|
||||
*/
|
||||
@@ -219,7 +240,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);
|
||||
|
||||
+238
-174
@@ -28,14 +28,16 @@ import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.event.impl.UserVerifySuccessEvent;
|
||||
import xyz.jonesdev.sonar.api.fallback.Fallback;
|
||||
import xyz.jonesdev.sonar.api.fallback.FallbackUser;
|
||||
import xyz.jonesdev.sonar.api.fallback.FallbackUserState;
|
||||
import xyz.jonesdev.sonar.api.model.VerifiedPlayer;
|
||||
import xyz.jonesdev.sonar.api.timer.SystemTimer;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.*;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.map.ItemMapType;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.map.ItemType;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.map.MapCaptchaInfo;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.config.FinishConfigurationPacket;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.login.LoginAcknowledgedPacket;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.*;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.vehicle.EntityType;
|
||||
import xyz.jonesdev.sonar.common.statistics.GlobalSonarStatistics;
|
||||
import xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil;
|
||||
|
||||
@@ -47,6 +49,7 @@ import java.util.regex.Pattern;
|
||||
import static xyz.jonesdev.sonar.api.config.SonarConfiguration.Verification.Gravity.Gamemode.CREATIVE;
|
||||
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_PACKET_DECODER;
|
||||
import static xyz.jonesdev.sonar.api.fallback.FallbackPipelines.FALLBACK_PACKET_ENCODER;
|
||||
import static xyz.jonesdev.sonar.api.fallback.FallbackUserState.*;
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
|
||||
import static xyz.jonesdev.sonar.common.fallback.protocol.FallbackPreparer.*;
|
||||
|
||||
@@ -54,41 +57,28 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
|
||||
private static final Random RANDOM = new Random();
|
||||
private static final Fallback FALLBACK = Sonar.get().getFallback();
|
||||
|
||||
// General
|
||||
private final SystemTimer login = new SystemTimer();
|
||||
private final @NotNull FallbackUser user;
|
||||
private final String username;
|
||||
private final UUID playerUuid;
|
||||
private @NotNull State state = State.LOGIN_ACK; // 1.20.2
|
||||
|
||||
// Checks
|
||||
private short expectedTransactionId;
|
||||
private int expectedKeepAliveId, expectedTeleportId = -1;
|
||||
private int tick, totalReceivedPackets, ignoredMovementTicks;
|
||||
private int tick, totalReceivedPackets;
|
||||
private double posX, posY, posZ, lastY, spawnYPosition;
|
||||
private boolean resolvedClientBrand, resolvedClientSettings;
|
||||
private boolean listenForMovements;
|
||||
private boolean receivedSteerBoat, receivedPlayerInput;
|
||||
|
||||
// Map captcha
|
||||
// CAPTCHA
|
||||
private final SystemTimer keepAlive = new SystemTimer();
|
||||
private final SystemTimer actionBar = new SystemTimer();
|
||||
private @Nullable MapCaptchaInfo captcha;
|
||||
private int captchaTriesLeft;
|
||||
|
||||
public enum State {
|
||||
// 1.20.2 configuration state
|
||||
LOGIN_ACK, CONFIGURE,
|
||||
// pre-JOIN ping check
|
||||
KEEP_ALIVE,
|
||||
// post-JOIN checks
|
||||
CLIENT_SETTINGS, PLUGIN_MESSAGE, TRANSACTION,
|
||||
// PLAY checks
|
||||
TELEPORT, POSITION,
|
||||
// Captcha
|
||||
MAP_CAPTCHA,
|
||||
// Done
|
||||
SUCCESS
|
||||
}
|
||||
// Cached options to make sure the original values don't change throughout the verification
|
||||
private final boolean performVehicle, performCaptcha, performGravity, performCollisions;
|
||||
|
||||
public FallbackVerificationHandler(final @NotNull FallbackUser user,
|
||||
final @NotNull String username,
|
||||
@@ -96,16 +86,15 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
|
||||
this.user = user;
|
||||
this.username = username;
|
||||
this.playerUuid = playerUuid;
|
||||
|
||||
if (user.getProtocolVersion().compareTo(MINECRAFT_1_20_2) < 0) {
|
||||
// Start initializing the actual join process
|
||||
initialJoinProcess();
|
||||
}
|
||||
this.performGravity = Sonar.get().getConfig().getVerification().getGravity().isEnabled();
|
||||
this.performCollisions = Sonar.get().getConfig().getVerification().getGravity().isCheckCollisions();
|
||||
this.performCaptcha = FALLBACK.shouldPerformCaptcha();
|
||||
this.performVehicle = FALLBACK.shouldPerformVehicleCheck();
|
||||
}
|
||||
|
||||
private void configure() {
|
||||
// Set the state to CONFIGURE to avoid false positives
|
||||
state = State.CONFIGURE;
|
||||
user.setState(CONFIGURE);
|
||||
// Send the necessary configuration packets to the client
|
||||
user.delayedWrite(REGISTRY_SYNC);
|
||||
user.delayedWrite(FINISH_CONFIGURATION);
|
||||
@@ -130,13 +119,13 @@ 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();
|
||||
} else {
|
||||
// Set the state to KEEP_ALIVE to avoid false positives
|
||||
state = State.KEEP_ALIVE;
|
||||
user.setState(KEEP_ALIVE);
|
||||
// Generate a random KeepAlive ID for the pre-join check
|
||||
expectedKeepAliveId = RANDOM.nextInt();
|
||||
// Send first KeepAlive to check if the connection is somewhat responsive
|
||||
@@ -147,7 +136,7 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
|
||||
private void sendTransaction() {
|
||||
// Set the state to TRANSACTION to avoid false positives
|
||||
// and go on with the flow of the verification.
|
||||
state = State.TRANSACTION;
|
||||
user.setState(TRANSACTION);
|
||||
// Generate a random transaction ID
|
||||
expectedTransactionId = (short) RANDOM.nextInt();
|
||||
// Send a transaction with the given ID
|
||||
@@ -160,7 +149,7 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
|
||||
// Set the state to CLIENT_SETTINGS to avoid false positives
|
||||
// (1.20.2 needs the LOGIN_ACK state) and go on with
|
||||
// the flow of the verification.
|
||||
state = State.CLIENT_SETTINGS;
|
||||
user.setState(CLIENT_SETTINGS);
|
||||
}
|
||||
// Send the JoinGame packet so we can continue the verification
|
||||
user.write(joinGame);
|
||||
@@ -174,7 +163,7 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
|
||||
private void sendAbilitiesAndTeleport() {
|
||||
// Set the state to TELEPORT to avoid false positives
|
||||
// and go on with the flow of the verification.
|
||||
state = State.TELEPORT;
|
||||
user.setState(TELEPORT);
|
||||
// Make sure the player is unable to fly (e.g. if the player is in creative mode)
|
||||
if (Sonar.get().getConfig().getVerification().getGravity().getGamemode() == CREATIVE) {
|
||||
user.delayedWrite(DEFAULT_ABILITIES);
|
||||
@@ -194,16 +183,16 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
|
||||
}
|
||||
|
||||
private void sendChunkData() {
|
||||
// If we don't have gravity and captcha enabled, simply finish verification
|
||||
if (!Sonar.get().getConfig().getVerification().getGravity().isEnabled()
|
||||
&& !FALLBACK.shouldDoMapCaptcha()) {
|
||||
// If we don't have any checks, simply finish verification
|
||||
// This is *hopefully* a very rare case though...
|
||||
if (!performGravity && !performCaptcha && !performVehicle) {
|
||||
// Save some work by finishing before sending even more packets
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
// Set the state to POSITION to avoid false positives
|
||||
// and go on with the flow of the verification.
|
||||
state = State.POSITION;
|
||||
user.setState(POSITION);
|
||||
// 1.20.3 packets introduced game events
|
||||
if (user.getProtocolVersion().compareTo(MINECRAFT_1_20_3) >= 0) {
|
||||
// Make sure the client knows that we're sending chunks next
|
||||
@@ -212,9 +201,9 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
|
||||
// Teleport player into the fake lobby by sending an empty chunk
|
||||
user.delayedWrite(EMPTY_CHUNK_DATA);
|
||||
// Checking gravity is disabled, just finish verification
|
||||
if (!Sonar.get().getConfig().getVerification().getGravity().isEnabled()) {
|
||||
if (!performGravity) {
|
||||
// Switch to captcha state if needed
|
||||
captchaOrFinish();
|
||||
vehicleCheckOrNext();
|
||||
return;
|
||||
}
|
||||
// Send an UpdateSectionBlocks packet with a platform of blocks
|
||||
@@ -254,7 +243,7 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
|
||||
}
|
||||
}
|
||||
|
||||
private void handlePacketDuringCaptcha(final @NotNull FallbackPacket packet) {
|
||||
private void handlePacketsCAPTCHA(final @NotNull FallbackPacket packet) {
|
||||
// Check if the player took too long to enter the captcha
|
||||
final int maxDuration = Sonar.get().getConfig().getVerification().getMap().getMaxDuration();
|
||||
checkFrame(!login.elapsed(maxDuration), "took too long to enter captcha");
|
||||
@@ -275,7 +264,7 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
|
||||
}
|
||||
|
||||
// Every second
|
||||
if (actionBar.elapsed(1000L)) {
|
||||
if (actionBar.elapsed(user.getProtocolVersion().compareTo(MINECRAFT_1_8) < 0 ? 10_000L : 1_000L)) {
|
||||
final String actionBarMessage = Sonar.get().getConfig().getVerification().getMap().getEnterCodeActionBar();
|
||||
// Only send action bar if the message is actually supposed to be sent
|
||||
if (!actionBarMessage.isEmpty()) {
|
||||
@@ -290,9 +279,7 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
|
||||
// Every 10 seconds
|
||||
if (keepAlive.elapsed(10_000L)) {
|
||||
// Send a KeepAlive packet to prevent timeout
|
||||
user.delayedWrite(CAPTCHA_KEEP_ALIVE);
|
||||
// Send both packets in one flush
|
||||
user.getChannel().flush();
|
||||
user.write(CAPTCHA_KEEP_ALIVE);
|
||||
// Make sure to reset the timer
|
||||
keepAlive.reset();
|
||||
}
|
||||
@@ -300,34 +287,78 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
|
||||
|
||||
@Override
|
||||
public void handle(final @NotNull FallbackPacket packet) {
|
||||
// The player has already been verified, drop all other packets
|
||||
if (state == State.SUCCESS) return;
|
||||
// The player has already been verified or failed the verification -> drop packet
|
||||
if (!user.getState().canReceivePackets()) return;
|
||||
|
||||
// We are expecting a captcha code in chat, drop all other packets
|
||||
if (state == State.MAP_CAPTCHA) {
|
||||
handlePacketDuringCaptcha(packet);
|
||||
// Calculate maximum amount of packets sent by the client to the server
|
||||
final int maxPackets = Sonar.get().getConfig().getVerification().getMaxLoginPackets()
|
||||
+ maxMovementTick // Account for positions/fall check
|
||||
+ Sonar.get().getConfig().getVerification().getMap().getMaxDuration()
|
||||
+ Sonar.get().getConfig().getVerification().getMap().getMaxTries();
|
||||
checkFrame(++totalReceivedPackets < maxPackets, "too many packets");
|
||||
|
||||
// We are expecting a code in chat, drop all other packets
|
||||
if (user.getState() == CAPTCHA) {
|
||||
handlePacketsCAPTCHA(packet);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the player is not sending a ton of packets to the server
|
||||
final int maxPackets = Sonar.get().getConfig().getVerification().getMaxLoginPackets() + maxMovementTick;
|
||||
checkFrame(++totalReceivedPackets < maxPackets, "too many packets");
|
||||
if (packet instanceof PaddleBoatPacket) {
|
||||
// Check if we are currently expecting a PaddleBoatPacket packet
|
||||
assertState(VEHICLE);
|
||||
|
||||
// Check for timeout since the player could be sending packets but not important ones
|
||||
final long timeout = Sonar.get().getConfig().getVerification().getMaxPing();
|
||||
checkFrame(!login.elapsed(timeout), "time limit exceeded");
|
||||
// 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);
|
||||
|
||||
final PlayerInputPacket inputPacket = (PlayerInputPacket) packet;
|
||||
|
||||
// Make sure the values sent by the client are legitimate
|
||||
checkFrame(inputPacket.getForward() <= 0.98f, "invalid vehicle speed (f)");
|
||||
checkFrame(inputPacket.getSideways() <= 0.98f, "invalid vehicle speed (s)");
|
||||
|
||||
// Don't verify if the player tries unmounting or jumping
|
||||
checkFrame(!inputPacket.isJump(), "tried to jump in vehicle");
|
||||
checkFrame(!inputPacket.isUnmount(), "tried to escape the vehicle");
|
||||
|
||||
// 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 {
|
||||
// PaddleBoat → PlayerInput → PaddleBoat
|
||||
checkFrame(receivedSteerBoat, "invalid vehicle steering order");
|
||||
}
|
||||
// Mark player input check as passed
|
||||
receivedPlayerInput = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (packet instanceof LoginAcknowledgedPacket) {
|
||||
// Check if we are currently expecting a LoginAcknowledged packet
|
||||
assertState(State.LOGIN_ACK);
|
||||
assertState(LOGIN_ACK);
|
||||
|
||||
// Start the configuration process for 1.20.2 clients
|
||||
configure();
|
||||
return;
|
||||
}
|
||||
|
||||
if (packet instanceof FinishConfigurationPacket) {
|
||||
// Check if we are currently expecting a FinishConfiguration packet
|
||||
assertState(State.CONFIGURE);
|
||||
assertState(CONFIGURE);
|
||||
|
||||
// Check if the client has already sent valid ClientSettings and PluginMessage packets
|
||||
checkFrame(resolvedClientBrand, "did not resolve client brand");
|
||||
@@ -336,6 +367,7 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
|
||||
// Start initializing the actual join process
|
||||
updateEncoderDecoderState(FallbackPacketRegistry.GAME);
|
||||
initialJoinProcess();
|
||||
return;
|
||||
}
|
||||
|
||||
if (packet instanceof KeepAlivePacket) {
|
||||
@@ -345,12 +377,13 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
|
||||
if (keepAlive.getId() == 0 && user.getProtocolVersion().compareTo(MINECRAFT_1_8) <= 0) return;
|
||||
|
||||
// Check if we are currently expecting a KeepAlive packet
|
||||
assertState(State.KEEP_ALIVE);
|
||||
assertState(KEEP_ALIVE);
|
||||
|
||||
checkFrame(keepAlive.getId() == expectedKeepAliveId, "invalid KeepAlive ID");
|
||||
|
||||
// The correct KeepAlive packet has been received
|
||||
sendJoinGamePacket();
|
||||
return;
|
||||
}
|
||||
|
||||
if (packet instanceof ClientSettingsPacket) {
|
||||
@@ -360,12 +393,14 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
|
||||
checkFrame(validateClientLocale(user, clientSettings.getLocale()), "invalid locale");
|
||||
|
||||
// Clients sometimes mess up the ClientSettings or PluginMessage packet.
|
||||
if (state == State.CLIENT_SETTINGS) {
|
||||
state = State.PLUGIN_MESSAGE;
|
||||
// Probably caused by some weird race condition in Minecraft 1.13+
|
||||
if (user.getState() == CLIENT_SETTINGS) {
|
||||
user.setState(PLUGIN_MESSAGE);
|
||||
}
|
||||
|
||||
// Make sure we mark the ClientSettings as valid
|
||||
resolvedClientSettings = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (packet instanceof PluginMessagePacket) {
|
||||
@@ -386,11 +421,11 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
|
||||
|
||||
// Validate the client branding using a regex to filter unwanted characters.
|
||||
if (Sonar.get().getConfig().getVerification().getBrand().isEnabled()) {
|
||||
checkFrame(validateClientBrand(user, pluginMessage.content()), "invalid client brand");
|
||||
checkFrame(validateClientBrand(user, pluginMessage.getSlicedBuffer()), "invalid client brand");
|
||||
}
|
||||
|
||||
// Clients sometimes mess up the ClientSettings or PluginMessage packet.
|
||||
if (state == State.PLUGIN_MESSAGE) {
|
||||
if (user.getState() == PLUGIN_MESSAGE) {
|
||||
// Send the transaction packet
|
||||
sendTransaction();
|
||||
}
|
||||
@@ -398,11 +433,12 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
|
||||
// Make sure we mark the PluginMessage as valid
|
||||
resolvedClientBrand = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (packet instanceof TransactionPacket) {
|
||||
// Check if we are currently expecting a Transaction packet
|
||||
assertState(State.TRANSACTION);
|
||||
assertState(TRANSACTION);
|
||||
|
||||
final TransactionPacket transaction = (TransactionPacket) packet;
|
||||
|
||||
@@ -425,11 +461,12 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
|
||||
// the channel earlier when we teleported the player.
|
||||
user.getChannel().flush();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (packet instanceof TeleportConfirmPacket) {
|
||||
// Check if we are currently expecting a TeleportConfirm packet
|
||||
assertState(State.TELEPORT);
|
||||
assertState(TELEPORT);
|
||||
|
||||
// Check if the teleport ID is correct
|
||||
final TeleportConfirmPacket teleportConfirm = (TeleportConfirmPacket) packet;
|
||||
@@ -442,17 +479,20 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
|
||||
|
||||
// Now we can send the chunk data
|
||||
sendChunkData();
|
||||
return;
|
||||
}
|
||||
|
||||
if (state != State.LOGIN_ACK) {
|
||||
if (user.getState() != LOGIN_ACK) {
|
||||
if (packet instanceof PlayerPositionPacket) {
|
||||
final PlayerPositionPacket position = (PlayerPositionPacket) packet;
|
||||
handlePositionUpdate(position.getX(), position.getY(), position.getZ(), position.isOnGround());
|
||||
return;
|
||||
}
|
||||
|
||||
if (packet instanceof PlayerPositionLookPacket) {
|
||||
final PlayerPositionLookPacket position = (PlayerPositionLookPacket) packet;
|
||||
handlePositionUpdate(position.getX(), position.getY(), position.getZ(), position.isOnGround());
|
||||
return;
|
||||
}
|
||||
|
||||
if (packet instanceof PlayerTickPacket) {
|
||||
@@ -463,115 +503,126 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
|
||||
}
|
||||
|
||||
private void handlePositionUpdate(final double x, final double y, final double z, final boolean ground) {
|
||||
// 1.8 does not have a TeleportConfirm packet, so we need to handle the spawning differently
|
||||
if (user.getProtocolVersion().compareTo(MINECRAFT_1_8) <= 0
|
||||
&& expectedTeleportId != -1 // Check if the teleport ID is currently unset
|
||||
// Then, check if the position is equal to the spawn position
|
||||
&& x == SPAWN_X_POSITION && y == spawnYPosition && z == SPAWN_Z_POSITION) {
|
||||
// Reset all values to ensure safety on teleport
|
||||
tick = 1;
|
||||
posY = -1;
|
||||
expectedTeleportId = -1;
|
||||
// Check for ground state in the first packet
|
||||
checkFrame(!ground, "invalid ground state");
|
||||
}
|
||||
|
||||
posX = x;
|
||||
lastY = posY;
|
||||
posY = y;
|
||||
posZ = z;
|
||||
|
||||
// The player is not allowed to move away from the collision platform.
|
||||
// This should not happen unless the max movement tick is configured to a high number.
|
||||
checkFrame(Math.abs(x - BLOCKS_PER_ROW) < BLOCKS_PER_ROW, "moved too far (x)");
|
||||
checkFrame(Math.abs(z - BLOCKS_PER_ROW) < BLOCKS_PER_ROW, "moved too far (z)");
|
||||
|
||||
// Check if the client hasn't moved before sending the first movement packet
|
||||
if (!listenForMovements) {
|
||||
if (posX == SPAWN_X_POSITION && posZ == SPAWN_Z_POSITION) {
|
||||
// Now, once we verified the X and Z position, we can safely check for gravity
|
||||
listenForMovements = true;
|
||||
try {
|
||||
// 1.8 does not have a TeleportConfirm packet, so we need to handle the spawning differently
|
||||
if (user.getProtocolVersion().compareTo(MINECRAFT_1_8) <= 0
|
||||
&& expectedTeleportId != -1 // Check if the teleport ID is currently unset
|
||||
// Then, check if the position is equal to the spawn position
|
||||
&& x == SPAWN_X_POSITION && y == spawnYPosition && z == SPAWN_Z_POSITION) {
|
||||
// Reset all values to ensure safety on teleport
|
||||
tick = 1;
|
||||
posY = -1;
|
||||
expectedTeleportId = -1;
|
||||
// Check for ground state in the first packet
|
||||
checkFrame(!ground, "invalid ground state");
|
||||
}
|
||||
|
||||
lastY = spawnYPosition;
|
||||
return;
|
||||
}
|
||||
posX = x;
|
||||
lastY = posY;
|
||||
posY = y;
|
||||
posZ = z;
|
||||
|
||||
final double deltaY = lastY - y;
|
||||
// The deltaY is 0 whenever the player sends their first position packet.
|
||||
// We have to account for this or the player will fail the verification.
|
||||
if (deltaY == 0) {
|
||||
// Check for too many ignored Y ticks
|
||||
final int maxIgnoredTicks = Sonar.get().getConfig().getVerification().getGravity().getMaxIgnoredTicks();
|
||||
checkFrame(++ignoredMovementTicks < maxIgnoredTicks, "too many ignored ticks");
|
||||
return;
|
||||
}
|
||||
// The player is not allowed to move away from the collision platform.
|
||||
// This should not happen unless the max movement tick is configured to a high number.
|
||||
checkFrame(Math.abs(x - BLOCKS_PER_ROW) < BLOCKS_PER_ROW, "moved too far (x)");
|
||||
checkFrame(Math.abs(z - BLOCKS_PER_ROW) < BLOCKS_PER_ROW, "moved too far (z)");
|
||||
|
||||
// Calculate the difference between the player's Y coordinate and the expected Y coordinate
|
||||
double collisionOffsetY = (DEFAULT_Y_COLLIDE_POSITION + blockType.getBlockHeight()) - y;
|
||||
if (user.getProtocolVersion().compareTo(MINECRAFT_1_8) < 0) {
|
||||
collisionOffsetY += 1.62f; // 1.7 is weird and sends the head position instead of the AABB minY
|
||||
}
|
||||
|
||||
// Check if the player is colliding by performing a basic Y offset check.
|
||||
// The offset cannot be greater than 0 since the blocks will not let the player fall through them.
|
||||
checkFrame(collisionOffsetY <= 0, "fell through blocks: " + collisionOffsetY);
|
||||
|
||||
if (tick > maxMovementTick) {
|
||||
// Log/debug position if enabled in the configuration
|
||||
if (Sonar.get().getConfig().getVerification().isDebugXYZPositions()) {
|
||||
FALLBACK.getLogger().info("{}: {}/{}/{} - deltaY: {} - ground: {} - collision: {}",
|
||||
username, x, y, z, deltaY, ground, collisionOffsetY);
|
||||
}
|
||||
|
||||
// If the collision check is disabled, finish verification
|
||||
if (!Sonar.get().getConfig().getVerification().getGravity().isCheckCollisions()) {
|
||||
captchaOrFinish();
|
||||
return;
|
||||
}
|
||||
|
||||
// Perform the collision check
|
||||
if (ground) {
|
||||
// Make sure the player is actually colliding with the blocks and not only spoofing ground
|
||||
checkFrame(collisionOffsetY > -0.03, "illegal collision: " + collisionOffsetY);
|
||||
// Check if the player is not spoofing ground
|
||||
// We cannot use checkFrame as it interferes with the CAPTCHA
|
||||
if (collisionOffsetY > -1) {
|
||||
// The player is colliding to blocks, finish verification
|
||||
captchaOrFinish();
|
||||
// Check if the client hasn't moved before sending the first movement packet
|
||||
if (!listenForMovements) {
|
||||
if (posX == SPAWN_X_POSITION && posZ == SPAWN_Z_POSITION) {
|
||||
// Now, once we verified the X and Z position, we can safely check for gravity
|
||||
listenForMovements = true;
|
||||
}
|
||||
|
||||
lastY = spawnYPosition;
|
||||
return;
|
||||
}
|
||||
|
||||
final double deltaY = lastY - y;
|
||||
// The deltaY is 0 whenever the player sends their first position packet.
|
||||
// We have to account for this or the player will fail the verification.
|
||||
if (deltaY == 0) return;
|
||||
|
||||
// Calculate the difference between the player's Y coordinate and the expected Y coordinate
|
||||
double collisionOffsetY = (DEFAULT_Y_COLLIDE_POSITION + blockType.getBlockHeight()) - y;
|
||||
if (user.getProtocolVersion().compareTo(MINECRAFT_1_8) < 0) {
|
||||
collisionOffsetY += 1.62f; // 1.7 is weird and sends the head position instead of the AABB minY
|
||||
}
|
||||
|
||||
// Check if the player is colliding by performing a basic Y offset check.
|
||||
// The offset cannot be greater than 0 since the blocks will not let the player fall through them.
|
||||
checkFrame(collisionOffsetY <= 0, "fell through blocks: " + collisionOffsetY);
|
||||
|
||||
if (tick > maxMovementTick) {
|
||||
// Log/debug position if enabled in the configuration
|
||||
if (Sonar.get().getConfig().getVerification().isDebugXYZPositions()) {
|
||||
FALLBACK.getLogger().info("{}: {}/{}/{} - deltaY: {} - ground: {} - collision: {}",
|
||||
username, x, y, z, deltaY, ground, collisionOffsetY);
|
||||
}
|
||||
|
||||
// If the collision check is disabled, finish verification
|
||||
if (!performCollisions) {
|
||||
vehicleCheckOrNext();
|
||||
return;
|
||||
}
|
||||
|
||||
// Perform the collision check
|
||||
if (ground) {
|
||||
// Make sure the player is actually colliding with the blocks and not only spoofing ground
|
||||
checkFrame(collisionOffsetY > -0.03, "illegal collision: " + collisionOffsetY);
|
||||
// The player is colliding to blocks, finish verification
|
||||
vehicleCheckOrNext();
|
||||
return;
|
||||
} else {
|
||||
// Make sure the player is colliding with blocks but is sending an invalid ground state
|
||||
checkFrame(collisionOffsetY != 0, "no ground: " + collisionOffsetY);
|
||||
}
|
||||
} else {
|
||||
// Make sure the player is colliding with blocks but is sending an invalid ground state
|
||||
checkFrame(collisionOffsetY != 0, "no ground: " + collisionOffsetY);
|
||||
}
|
||||
} else {
|
||||
// Check if the player is spoofing the ground state
|
||||
checkFrame(!ground, "spoofed ground state");
|
||||
}
|
||||
|
||||
// Make sure we don't run out of predicted Y motions
|
||||
checkFrame(tick < preparedCachedYMotions.length, "too many movements");
|
||||
|
||||
if (!ground) {
|
||||
final double predictedY = preparedCachedYMotions[tick];
|
||||
final double offsetY = Math.abs(deltaY - predictedY);
|
||||
|
||||
// Log/debug position if enabled in the configuration
|
||||
if (Sonar.get().getConfig().getVerification().isDebugXYZPositions()) {
|
||||
FALLBACK.getLogger().info("{}: {}/{}/{} - deltaY: {} - prediction: {} - offset: {}",
|
||||
username, x, y, z, deltaY, predictedY, offsetY);
|
||||
// Check if the player is spoofing the ground state
|
||||
checkFrame(!ground, "spoofed ground state");
|
||||
}
|
||||
|
||||
// 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",
|
||||
tick, y, offsetY, deltaY, predictedY));
|
||||
// Make sure we don't run out of predicted Y motions
|
||||
checkFrame(tick < preparedCachedYMotions.length, "too many movements");
|
||||
|
||||
if (!ground) {
|
||||
final double predictedY = preparedCachedYMotions[tick];
|
||||
final double offsetY = Math.abs(deltaY - predictedY);
|
||||
|
||||
// Log/debug position if enabled in the configuration
|
||||
if (Sonar.get().getConfig().getVerification().isDebugXYZPositions()) {
|
||||
FALLBACK.getLogger().info("{}: {}/{}/{} - deltaY: {} - prediction: {} - offset: {}",
|
||||
username, x, y, z, deltaY, predictedY, offsetY);
|
||||
}
|
||||
|
||||
// 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",
|
||||
tick, y, offsetY, deltaY, predictedY));
|
||||
}
|
||||
tick++;
|
||||
} catch (CorruptedFrameException exception) {
|
||||
// Do not throw the exception if the user configured to display the CAPTCHA instead
|
||||
if (Sonar.get().getConfig().getVerification().getGravity().isCaptchaOnFail()) {
|
||||
handleCAPTCHA();
|
||||
return;
|
||||
}
|
||||
// Interrupt the netty eventLoop to immediately stop further processing
|
||||
throw new DecoderException(exception);
|
||||
}
|
||||
tick++;
|
||||
}
|
||||
|
||||
private void captchaOrFinish() {
|
||||
if (FALLBACK.shouldDoMapCaptcha()) {
|
||||
// TODO: Maybe recode the flow with a better state system?
|
||||
private void vehicleCheckOrNext() {
|
||||
if (performVehicle) {
|
||||
// Initialize the vehicle check
|
||||
handleVehicleCheck();
|
||||
return;
|
||||
}
|
||||
captchaOrNext();
|
||||
}
|
||||
|
||||
private void captchaOrNext() {
|
||||
if (performCaptcha) {
|
||||
// Initialize the map captcha
|
||||
handleCAPTCHA();
|
||||
return;
|
||||
@@ -580,25 +631,39 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
|
||||
finish();
|
||||
}
|
||||
|
||||
private void handleVehicleCheck() {
|
||||
// Set the state to VEHICLE, so we don't handle any unnecessary packets
|
||||
user.setState(VEHICLE);
|
||||
// Send the necessary packets to mount the player on the vehicle
|
||||
final int vehicleType = EntityType.BOAT.getId(user.getProtocolVersion());
|
||||
user.delayedWrite(new SpawnEntityPacket(VEHICLE_ENTITY_ID, vehicleType, posX, posY, posZ));
|
||||
user.delayedWrite(setPassengers);
|
||||
user.getChannel().flush();
|
||||
}
|
||||
|
||||
private void handleCAPTCHA() {
|
||||
// Set the state to CAPTCHA, so we don't handle any unnecessary packets
|
||||
user.setState(CAPTCHA);
|
||||
if (!MAP_INFO_PREPARER.isCaptchaAvailable()) {
|
||||
// This should not happen, but we have to return if there is no captcha prepared
|
||||
user.disconnect(Sonar.get().getConfig().getVerification().getCurrentlyPreparing());
|
||||
return;
|
||||
}
|
||||
// Set the state to MAP_CAPTCHA, so we don't handle any unnecessary packets
|
||||
state = State.MAP_CAPTCHA;
|
||||
if (!Sonar.get().getConfig().getVerification().getGravity().isEnabled()
|
||||
if (!performGravity
|
||||
&& user.getProtocolVersion().compareTo(MINECRAFT_1_18_2) >= 0) {
|
||||
// Make sure the player escapes the 1.18.2+ "Loading terrain" screen
|
||||
user.delayedWrite(CAPTCHA_SPAWN_POSITION);
|
||||
}
|
||||
if (performVehicle) {
|
||||
// Make sure the player exits the vehicle before sending the CAPTCHA
|
||||
user.delayedWrite(removeEntities);
|
||||
}
|
||||
// Reset max tries
|
||||
captchaTriesLeft = Sonar.get().getConfig().getVerification().getMap().getMaxTries();
|
||||
|
||||
// Set slot to map
|
||||
user.delayedWrite(new SetSlotPacket(0, 36, 1, 0,
|
||||
ItemMapType.FILLED_MAP.getId(user.getProtocolVersion()), SetSlotPacket.MAP_NBT));
|
||||
ItemType.FILLED_MAP.getId(user.getProtocolVersion()), SetSlotPacket.MAP_NBT));
|
||||
// Send random captcha to the player
|
||||
captcha = MAP_INFO_PREPARER.getRandomCaptcha();
|
||||
Objects.requireNonNull(captcha).delayedWrite(user);
|
||||
@@ -613,10 +678,8 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
|
||||
}
|
||||
|
||||
private void finish() {
|
||||
// Something must've gone horribly wrong...
|
||||
if (state == State.SUCCESS) return;
|
||||
|
||||
state = State.SUCCESS;
|
||||
// Make sure we drop all further packets
|
||||
user.setState(SUCCESS);
|
||||
|
||||
// Increment amount of total successful verifications
|
||||
GlobalSonarStatistics.totalSuccessfulVerifications++;
|
||||
@@ -642,7 +705,8 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
|
||||
*
|
||||
* @param expectedState Expected state
|
||||
*/
|
||||
private void assertState(final @NotNull State expectedState) {
|
||||
private void assertState(final @NotNull FallbackUserState expectedState) {
|
||||
final FallbackUserState state = user.getState();
|
||||
checkFrame(state == expectedState, "expected " + expectedState + ", got " + state);
|
||||
}
|
||||
|
||||
@@ -652,14 +716,14 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
|
||||
* @param condition Condition to fail if it's false
|
||||
* @param message Messages displayed in the stacktrace
|
||||
*/
|
||||
private void checkFrame(final boolean condition, final String message) {
|
||||
private void checkFrame(final boolean condition, final @NotNull String message) {
|
||||
if (!condition) {
|
||||
if (state == State.POSITION // Gravity check
|
||||
&& Sonar.get().getConfig().getVerification().getGravity().isCaptchaOnFail()) {
|
||||
handleCAPTCHA();
|
||||
return;
|
||||
// Only fail the player if we are either not in the POSITION state,
|
||||
// or if the user configured Sonar to display a CAPTCHA instead of failing.
|
||||
if (user.getState() != POSITION
|
||||
|| !Sonar.get().getConfig().getVerification().getGravity().isCaptchaOnFail()) {
|
||||
user.fail(message);
|
||||
}
|
||||
user.fail(message);
|
||||
throw new CorruptedFrameException(message);
|
||||
}
|
||||
}
|
||||
|
||||
-136
@@ -1,136 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023-2024 Sonar Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.common.fallback.netty;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.ByteBufHolder;
|
||||
import io.netty.util.IllegalReferenceCountException;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.ToString;
|
||||
|
||||
// Taken from
|
||||
// https://github.com/PaperMC/Velocity/blob/dev/3.0.0/proxy/src/main/java/com/velocitypowered/proxy/protocol/util/DeferredByteBufHolder.java
|
||||
@ToString
|
||||
@AllArgsConstructor
|
||||
public class DeferredByteBufHolder implements ByteBufHolder {
|
||||
private ByteBuf backing;
|
||||
|
||||
@Override
|
||||
public ByteBuf content() {
|
||||
if (backing == null) {
|
||||
throw new IllegalStateException("Trying to obtain contents of holder with a null buffer");
|
||||
}
|
||||
if (backing.refCnt() <= 0) {
|
||||
throw new IllegalReferenceCountException(backing.refCnt());
|
||||
}
|
||||
return backing;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ByteBufHolder copy() {
|
||||
if (backing == null) {
|
||||
throw new IllegalStateException("Trying to obtain contents of holder with a null buffer");
|
||||
}
|
||||
return new DeferredByteBufHolder(backing.copy());
|
||||
}
|
||||
|
||||
@Override
|
||||
public ByteBufHolder duplicate() {
|
||||
if (backing == null) {
|
||||
throw new IllegalStateException("Trying to obtain contents of holder with a null buffer");
|
||||
}
|
||||
return new DeferredByteBufHolder(backing.duplicate());
|
||||
}
|
||||
|
||||
@Override
|
||||
public ByteBufHolder retainedDuplicate() {
|
||||
if (backing == null) {
|
||||
throw new IllegalStateException("Trying to obtain contents of holder with a null buffer");
|
||||
}
|
||||
return new DeferredByteBufHolder(backing.retainedDuplicate());
|
||||
}
|
||||
|
||||
@Override
|
||||
public ByteBufHolder replace(final ByteBuf content) {
|
||||
if (content == null) {
|
||||
throw new NullPointerException("content");
|
||||
}
|
||||
backing = content;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int refCnt() {
|
||||
if (backing == null) {
|
||||
throw new IllegalStateException("Trying to obtain contents of holder with a null buffer");
|
||||
}
|
||||
return backing.refCnt();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ByteBufHolder retain() {
|
||||
if (backing == null) {
|
||||
throw new IllegalStateException("Trying to obtain contents of holder with a null buffer");
|
||||
}
|
||||
backing.retain();
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ByteBufHolder retain(final int increment) {
|
||||
if (backing == null) {
|
||||
throw new IllegalStateException("Trying to obtain contents of holder with a null buffer");
|
||||
}
|
||||
backing.retain(increment);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ByteBufHolder touch() {
|
||||
if (backing == null) {
|
||||
throw new IllegalStateException("Trying to obtain contents of holder with a null buffer");
|
||||
}
|
||||
backing.touch();
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ByteBufHolder touch(final Object hint) {
|
||||
if (backing == null) {
|
||||
throw new IllegalStateException("Trying to obtain contents of holder with a null buffer");
|
||||
}
|
||||
backing.touch(hint);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean release() {
|
||||
if (backing == null) {
|
||||
throw new IllegalStateException("Trying to obtain contents of holder with a null buffer");
|
||||
}
|
||||
return backing.release();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean release(final int decrement) {
|
||||
if (backing == null) {
|
||||
throw new IllegalStateException("Trying to obtain contents of holder with a null buffer");
|
||||
}
|
||||
return backing.release(decrement);
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,7 @@ import java.util.UUID;
|
||||
// Taken from
|
||||
// https://github.com/PaperMC/Velocity/blob/dev/3.0.0/api/src/main/java/com/velocitypowered/api/util/FastUuidSansHyphens.java
|
||||
public final class FastUUID {
|
||||
public static final UUID EMPTY_UUID = new UUID(0L, 0L);
|
||||
private static final int MOJANG_BROKEN_UUID_LENGTH = 32;
|
||||
|
||||
private static final char[] HEX_DIGITS =
|
||||
|
||||
+63
@@ -157,6 +157,41 @@ public enum FallbackPacketRegistry {
|
||||
map(0x20, MINECRAFT_1_19_3, true),
|
||||
map(0x24, MINECRAFT_1_19_4, true),
|
||||
map(0x25, MINECRAFT_1_20_2, true));
|
||||
clientbound.register(RemoveEntitiesPacket.class, RemoveEntitiesPacket::new,
|
||||
map(0x13, MINECRAFT_1_7_2, true),
|
||||
map(0x30, MINECRAFT_1_9, true),
|
||||
map(0x31, MINECRAFT_1_12, true),
|
||||
map(0x32, MINECRAFT_1_12_2, true),
|
||||
map(0x35, MINECRAFT_1_13, true),
|
||||
map(0x37, MINECRAFT_1_14, true),
|
||||
map(0x38, MINECRAFT_1_15, true),
|
||||
map(0x37, MINECRAFT_1_16, true),
|
||||
map(0x36, MINECRAFT_1_16_2, true),
|
||||
map(0x3A, MINECRAFT_1_17, true),
|
||||
map(0x38, MINECRAFT_1_19, true),
|
||||
map(0x3B, MINECRAFT_1_19_1, true),
|
||||
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));
|
||||
clientbound.register(SetPassengersPacket.class, SetPassengersPacket::new,
|
||||
map(0x1B, MINECRAFT_1_7_2, true),
|
||||
map(0x40, MINECRAFT_1_9, true),
|
||||
map(0x42, MINECRAFT_1_12, true),
|
||||
map(0x43, MINECRAFT_1_12_1, true),
|
||||
map(0x46, MINECRAFT_1_13, true),
|
||||
map(0x4A, MINECRAFT_1_14, true),
|
||||
map(0x4B, MINECRAFT_1_15, true),
|
||||
map(0x54, MINECRAFT_1_17, true),
|
||||
map(0x57, MINECRAFT_1_19_1, true),
|
||||
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));
|
||||
clientbound.register(SpawnEntityPacket.class, SpawnEntityPacket::new,
|
||||
map(0x0E, MINECRAFT_1_7_2, true),
|
||||
map(0x00, MINECRAFT_1_9, true),
|
||||
map(0x01, MINECRAFT_1_19_4, true));
|
||||
clientbound.register(UpdateSectionBlocksPacket.class, UpdateSectionBlocksPacket::new,
|
||||
map(0x22, MINECRAFT_1_7_2, true),
|
||||
map(0x10, MINECRAFT_1_9, true),
|
||||
@@ -242,6 +277,34 @@ public enum FallbackPacketRegistry {
|
||||
clientbound.register(GameEventPacket.class, GameEventPacket::new,
|
||||
map(0x20, MINECRAFT_1_20_3, 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));
|
||||
serverbound.register(PlayerInputPacket.class, PlayerInputPacket::new,
|
||||
map(0x0C, MINECRAFT_1_7_2, false),
|
||||
map(0x15, MINECRAFT_1_9, false),
|
||||
map(0x16, MINECRAFT_1_12, false),
|
||||
map(0x1A, MINECRAFT_1_13, false),
|
||||
map(0x1C, MINECRAFT_1_14, false),
|
||||
map(0x1D, MINECRAFT_1_16, false),
|
||||
map(0x1C, MINECRAFT_1_17, false),
|
||||
map(0x1E, MINECRAFT_1_19, false),
|
||||
map(0x1F, MINECRAFT_1_19_1, false),
|
||||
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));
|
||||
serverbound.register(UniversalChatPacket.class, UniversalChatPacket::new,
|
||||
map(0x01, MINECRAFT_1_7_2, false),
|
||||
map(0x02, MINECRAFT_1_9, false),
|
||||
|
||||
+15
-1
@@ -28,6 +28,8 @@ import xyz.jonesdev.sonar.common.fallback.protocol.packets.config.FinishConfigur
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.config.RegistryDataPacket;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.packets.play.*;
|
||||
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
|
||||
@UtilityClass
|
||||
public class FallbackPreparer {
|
||||
|
||||
@@ -48,6 +50,7 @@ public class FallbackPreparer {
|
||||
public FallbackPacket enterCodeMessage;
|
||||
public FallbackPacket incorrectCaptcha;
|
||||
// JoinGame
|
||||
public final int PLAYER_ENTITY_ID = ThreadLocalRandom.current().nextInt(10);
|
||||
public FallbackPacket joinGame;
|
||||
// Update Section Blocks
|
||||
public FallbackPacket updateSectionBlocks;
|
||||
@@ -61,6 +64,12 @@ public class FallbackPreparer {
|
||||
public FallbackPacket reconnectedTooFast;
|
||||
public FallbackPacket protocolBlacklisted;
|
||||
public FallbackPacket invalidUsername;
|
||||
public FallbackPacket tooManyOnlinePerIP;
|
||||
|
||||
// Vehicle
|
||||
public FallbackPacket removeEntities;
|
||||
public FallbackPacket setPassengers;
|
||||
public static final int VEHICLE_ENTITY_ID = PLAYER_ENTITY_ID + 1;
|
||||
|
||||
// Collisions
|
||||
public final int BLOCKS_PER_ROW = 8; // 8 * 8 = 64 (protocol maximum)
|
||||
@@ -86,7 +95,7 @@ public class FallbackPreparer {
|
||||
public final CaptchaPreparer MAP_INFO_PREPARER = new CaptchaPreparer();
|
||||
|
||||
public void prepare() {
|
||||
joinGame = new JoinGamePacket(1,
|
||||
joinGame = new JoinGamePacket(PLAYER_ENTITY_ID,
|
||||
Sonar.get().getConfig().getVerification().getGravity().getGamemode().getId(),
|
||||
0, false, 0,
|
||||
true, false, false,
|
||||
@@ -129,6 +138,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
|
||||
@@ -139,5 +149,9 @@ public class FallbackPreparer {
|
||||
|
||||
MAP_INFO_PREPARER.prepare();
|
||||
}
|
||||
|
||||
// Prepare packets for the vehicle check
|
||||
removeEntities = new RemoveEntitiesPacket(VEHICLE_ENTITY_ID);
|
||||
setPassengers = new SetPassengersPacket(VEHICLE_ENTITY_ID, PLAYER_ENTITY_ID);
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -25,7 +25,7 @@ import java.util.function.Function;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@RequiredArgsConstructor
|
||||
public enum ItemMapType {
|
||||
public enum ItemType {
|
||||
FILLED_MAP(protocolVersion -> {
|
||||
// Useful resources:
|
||||
// - https://github.com/PrismarineJS/minecraft-data/blob/master/data/pc/
|
||||
+1
-3
@@ -43,8 +43,6 @@ public class ClientAbilitiesPacket implements FallbackPacket {
|
||||
|
||||
@Override
|
||||
public void decode(final @NotNull ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
|
||||
encodedFlags = byteBuf.readByte();
|
||||
flySpeed = byteBuf.readFloat();
|
||||
walkSpeed = byteBuf.readFloat();
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
||||
+9
-9
@@ -46,13 +46,8 @@ public final class ClientSettingsPacket implements FallbackPacket {
|
||||
private boolean clientListingAllowed; // Added in 1.18, overwrites server-list "anonymous" mode
|
||||
|
||||
@Override
|
||||
public int expectedMinLength(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
|
||||
return 2;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int expectedMaxLength(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
|
||||
return 0xff + 1; // 256 as a hard-limit
|
||||
public void encode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -82,7 +77,12 @@ public final class ClientSettingsPacket implements FallbackPacket {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void encode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
|
||||
throw new UnsupportedOperationException();
|
||||
public int expectedMinLength(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
|
||||
return 2;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int expectedMaxLength(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
|
||||
return 0xff + 1; // 256 as a hard-limit
|
||||
}
|
||||
}
|
||||
+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();
|
||||
}
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* 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 class PlayerInputPacket implements FallbackPacket {
|
||||
private float sideways, forward;
|
||||
private boolean jump, unmount;
|
||||
|
||||
@Override
|
||||
public void encode(final @NotNull ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
|
||||
sideways = byteBuf.readFloat();
|
||||
forward = byteBuf.readFloat();
|
||||
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_8) < 0) {
|
||||
jump = byteBuf.readBoolean();
|
||||
unmount = byteBuf.readBoolean();
|
||||
} else {
|
||||
final byte flags = byteBuf.readByte();
|
||||
jump = (flags & 0x01) != 0;
|
||||
unmount = (flags & 0x02) != 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
+14
-14
@@ -39,19 +39,6 @@ public final class PlayerPositionLookPacket implements FallbackPacket {
|
||||
private int teleportId;
|
||||
private boolean onGround;
|
||||
|
||||
@Override
|
||||
public void decode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
|
||||
x = byteBuf.readDouble();
|
||||
y = byteBuf.readDouble();
|
||||
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_8) < 0) {
|
||||
y = byteBuf.readDouble(); // Account for weird y positions on 1.7
|
||||
}
|
||||
z = byteBuf.readDouble();
|
||||
yaw = byteBuf.readFloat();
|
||||
pitch = byteBuf.readFloat();
|
||||
onGround = byteBuf.readBoolean();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void encode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
|
||||
byteBuf.writeDouble(x);
|
||||
@@ -77,7 +64,20 @@ public final class PlayerPositionLookPacket implements FallbackPacket {
|
||||
}
|
||||
|
||||
@Override
|
||||
public int expectedMaxLength(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
|
||||
public void decode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
|
||||
x = byteBuf.readDouble();
|
||||
y = byteBuf.readDouble();
|
||||
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_8) < 0) {
|
||||
y = byteBuf.readDouble(); // Account for weird y positions on 1.7
|
||||
}
|
||||
z = byteBuf.readDouble();
|
||||
yaw = byteBuf.readFloat();
|
||||
pitch = byteBuf.readFloat();
|
||||
onGround = byteBuf.readBoolean();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int expectedMaxLength(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
|
||||
return protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_8) < 0 ? 41 : 33;
|
||||
}
|
||||
|
||||
|
||||
+5
-5
@@ -34,6 +34,11 @@ public final class PlayerPositionPacket implements FallbackPacket {
|
||||
private double x, y, z;
|
||||
private boolean onGround;
|
||||
|
||||
@Override
|
||||
public void encode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
|
||||
x = byteBuf.readDouble();
|
||||
@@ -45,11 +50,6 @@ public final class PlayerPositionPacket implements FallbackPacket {
|
||||
onGround = byteBuf.readBoolean();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void encode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int expectedMaxLength(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
|
||||
return protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_8) < 0 ? 33 : 25;
|
||||
|
||||
+5
-5
@@ -33,13 +33,13 @@ import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
|
||||
public final class PlayerTickPacket implements FallbackPacket {
|
||||
private boolean onGround;
|
||||
|
||||
@Override
|
||||
public void decode(final @NotNull ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
|
||||
onGround = byteBuf.readBoolean();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void encode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decode(final @NotNull ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
|
||||
onGround = byteBuf.readBoolean();
|
||||
}
|
||||
}
|
||||
+13
-19
@@ -22,32 +22,21 @@ import lombok.Getter;
|
||||
import lombok.ToString;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
import xyz.jonesdev.sonar.common.fallback.netty.DeferredByteBufHolder;
|
||||
import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_13;
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.MINECRAFT_1_8;
|
||||
import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.*;
|
||||
|
||||
// Mostly taken from
|
||||
// https://github.com/PaperMC/Velocity/blob/dev/3.0.0/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/PluginMessage.java
|
||||
@Getter
|
||||
@ToString
|
||||
public final class PluginMessagePacket extends DeferredByteBufHolder implements FallbackPacket {
|
||||
public final class PluginMessagePacket implements FallbackPacket {
|
||||
private String channel;
|
||||
|
||||
public PluginMessagePacket() {
|
||||
super(null);
|
||||
}
|
||||
private ByteBuf slicedBuffer;
|
||||
|
||||
@Override
|
||||
public int expectedMinLength(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
|
||||
return 4;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int expectedMaxLength(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
|
||||
return 0xFFF; // strict size limit
|
||||
public void encode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -59,14 +48,19 @@ public final class PluginMessagePacket extends DeferredByteBufHolder implements
|
||||
}
|
||||
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_8) >= 0) {
|
||||
replace(byteBuf.readRetainedSlice(byteBuf.readableBytes()));
|
||||
slicedBuffer = byteBuf.readRetainedSlice(byteBuf.readableBytes());
|
||||
} else {
|
||||
replace(readRetainedByteBufSlice17(byteBuf));
|
||||
slicedBuffer = readRetainedByteBufSlice17(byteBuf);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void encode(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
|
||||
throw new UnsupportedOperationException();
|
||||
public int expectedMinLength(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
|
||||
return 4;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int expectedMaxLength(final ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
|
||||
return 0xFFF; // strict size limit
|
||||
}
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.writeVarInt;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public final class RemoveEntitiesPacket implements FallbackPacket {
|
||||
private int entityId; // We only need one entityId
|
||||
|
||||
@Override
|
||||
public void encode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
|
||||
// No idea why Mojang decided that this is a good idea, but whatever
|
||||
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_17) != 0) {
|
||||
writeVarInt(byteBuf, 1); // size
|
||||
}
|
||||
|
||||
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_8) < 0) {
|
||||
byteBuf.writeInt(entityId);
|
||||
} else {
|
||||
writeVarInt(byteBuf, entityId);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.writeVarInt;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public final class SetPassengersPacket implements FallbackPacket {
|
||||
private int entityId;
|
||||
private int passengerId;
|
||||
|
||||
@Override
|
||||
public void encode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
|
||||
// You can find this in the EntityAttach packet,
|
||||
// which was later replaced by SetPassengers in 1.9+
|
||||
if (protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_9) < 0) {
|
||||
byteBuf.writeInt(passengerId);
|
||||
byteBuf.writeInt(entityId);
|
||||
byteBuf.writeByte(0); // leash
|
||||
return;
|
||||
}
|
||||
|
||||
writeVarInt(byteBuf, entityId);
|
||||
writeVarInt(byteBuf, 1); // passenger count
|
||||
writeVarInt(byteBuf, passengerId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
||||
+5
-5
@@ -49,11 +49,6 @@ public final class SetSlotPacket implements FallbackPacket {
|
||||
.put("map", IntBinaryTag.intBinaryTag(0)) // map type
|
||||
.build();
|
||||
|
||||
@Override
|
||||
public void decode(final @NotNull ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void encode(final @NotNull ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
|
||||
byteBuf.writeByte(windowId);
|
||||
@@ -92,4 +87,9 @@ public final class SetSlotPacket implements FallbackPacket {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decode(final @NotNull ByteBuf byteBuf, final ProtocolVersion protocolVersion) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
|
||||
import static xyz.jonesdev.sonar.common.fallback.netty.FastUUID.EMPTY_UUID;
|
||||
import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.writeUUID;
|
||||
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.writeVarInt;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public final class SpawnEntityPacket implements FallbackPacket {
|
||||
private int entityId, entityType;
|
||||
private double x, y, z;
|
||||
|
||||
@Override
|
||||
public void encode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
|
||||
writeVarInt(byteBuf, entityId);
|
||||
|
||||
final boolean v1_9orHigher = protocolVersion.compareTo(MINECRAFT_1_8) > 0;
|
||||
|
||||
if (v1_9orHigher) {
|
||||
writeUUID(byteBuf, EMPTY_UUID);
|
||||
}
|
||||
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_14) >= 0) {
|
||||
writeVarInt(byteBuf, entityType);
|
||||
} else {
|
||||
byteBuf.writeByte(entityType);
|
||||
}
|
||||
|
||||
if (v1_9orHigher) {
|
||||
byteBuf.writeDouble(x);
|
||||
byteBuf.writeDouble(y);
|
||||
byteBuf.writeDouble(z);
|
||||
} else {
|
||||
byteBuf.writeInt(floor(x * 32D));
|
||||
byteBuf.writeInt(floor(y * 32D));
|
||||
byteBuf.writeInt(floor(z * 32D));
|
||||
}
|
||||
|
||||
byteBuf.writeByte(0); // pitch or yaw
|
||||
byteBuf.writeByte(0); // yaw or pitch
|
||||
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_19) >= 0) {
|
||||
byteBuf.writeByte(0); // head yaw
|
||||
writeVarInt(byteBuf, 0); // data
|
||||
} else {
|
||||
byteBuf.writeInt(0); // data
|
||||
}
|
||||
|
||||
if (v1_9orHigher) {
|
||||
byteBuf.writeShort(0); // velocity X
|
||||
byteBuf.writeShort(0); // velocity Y
|
||||
byteBuf.writeShort(0); // velocity Z
|
||||
}
|
||||
}
|
||||
|
||||
private static int floor(final double value) {
|
||||
final int __value = (int) value;
|
||||
return value < (double) __value ? __value - 1 : __value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
||||
+13
-13
@@ -36,19 +36,6 @@ public final class TransactionPacket implements FallbackPacket {
|
||||
private int windowId, id;
|
||||
private boolean accepted;
|
||||
|
||||
@Override
|
||||
public void encode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_17) < 0) {
|
||||
byteBuf.writeByte(windowId);
|
||||
byteBuf.writeShort((short) id);
|
||||
// The "accepted" field is actually really unnecessary since
|
||||
// it's never even used in the client.
|
||||
byteBuf.writeBoolean(accepted);
|
||||
} else {
|
||||
byteBuf.writeInt(id);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_17) < 0) {
|
||||
@@ -62,4 +49,17 @@ public final class TransactionPacket implements FallbackPacket {
|
||||
accepted = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void encode(final ByteBuf byteBuf, final @NotNull ProtocolVersion protocolVersion) {
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_17) < 0) {
|
||||
byteBuf.writeByte(windowId);
|
||||
byteBuf.writeShort((short) id);
|
||||
// The "accepted" field is actually really unnecessary since
|
||||
// it's never even used in the client.
|
||||
byteBuf.writeBoolean(accepted);
|
||||
} else {
|
||||
byteBuf.writeInt(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-3
@@ -29,9 +29,9 @@ import xyz.jonesdev.sonar.common.fallback.protocol.FallbackPacket;
|
||||
import xyz.jonesdev.sonar.common.utility.component.ComponentHolder;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.UUID;
|
||||
|
||||
import static xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion.*;
|
||||
import static xyz.jonesdev.sonar.common.fallback.netty.FastUUID.EMPTY_UUID;
|
||||
import static xyz.jonesdev.sonar.common.utility.protocol.ProtocolUtil.*;
|
||||
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.readVarInt;
|
||||
import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.writeVarInt;
|
||||
@@ -40,7 +40,6 @@ import static xyz.jonesdev.sonar.common.utility.protocol.VarIntUtil.writeVarInt;
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public final class UniversalChatPacket implements FallbackPacket {
|
||||
private static final UUID PLACEHOLDER_UUID = new UUID(0L, 0L);
|
||||
private static final int DIV_FLOOR = -Math.floorDiv(-20, 8);
|
||||
|
||||
public static final byte CHAT_TYPE = (byte) 0;
|
||||
@@ -85,7 +84,7 @@ public final class UniversalChatPacket implements FallbackPacket {
|
||||
// Sender
|
||||
if (protocolVersion.compareTo(MINECRAFT_1_16) >= 0
|
||||
&& protocolVersion.compareTo(MINECRAFT_1_19) < 0) {
|
||||
writeUUID(byteBuf, PLACEHOLDER_UUID);
|
||||
writeUUID(byteBuf, EMPTY_UUID);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* 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.vehicle;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.fallback.protocol.ProtocolVersion;
|
||||
|
||||
import java.util.function.Function;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public enum EntityType {
|
||||
BOAT(protocolVersion -> {
|
||||
// Useful resources:
|
||||
// - https://github.com/PrismarineJS/minecraft-data/blob/master/data/pc/
|
||||
// - https://pokechu22.github.io/Burger/1.20.4.html
|
||||
switch (protocolVersion) {
|
||||
default:
|
||||
case MINECRAFT_1_7_2:
|
||||
case MINECRAFT_1_7_6:
|
||||
case MINECRAFT_1_8:
|
||||
case MINECRAFT_1_9:
|
||||
case MINECRAFT_1_9_1:
|
||||
case MINECRAFT_1_9_2:
|
||||
case MINECRAFT_1_9_4:
|
||||
case MINECRAFT_1_10:
|
||||
case MINECRAFT_1_11:
|
||||
case MINECRAFT_1_11_1:
|
||||
case MINECRAFT_1_12:
|
||||
case MINECRAFT_1_12_1:
|
||||
case MINECRAFT_1_12_2:
|
||||
case MINECRAFT_1_13:
|
||||
case MINECRAFT_1_13_1:
|
||||
case MINECRAFT_1_13_2:
|
||||
return 1;
|
||||
case MINECRAFT_1_14:
|
||||
case MINECRAFT_1_14_1:
|
||||
case MINECRAFT_1_14_2:
|
||||
case MINECRAFT_1_14_3:
|
||||
case MINECRAFT_1_14_4:
|
||||
return 5;
|
||||
case MINECRAFT_1_15:
|
||||
case MINECRAFT_1_15_1:
|
||||
case MINECRAFT_1_15_2:
|
||||
case MINECRAFT_1_16:
|
||||
case MINECRAFT_1_16_1:
|
||||
case MINECRAFT_1_16_2:
|
||||
case MINECRAFT_1_16_3:
|
||||
case MINECRAFT_1_16_4:
|
||||
return 6;
|
||||
case MINECRAFT_1_17:
|
||||
case MINECRAFT_1_17_1:
|
||||
case MINECRAFT_1_18:
|
||||
case MINECRAFT_1_18_2:
|
||||
return 7;
|
||||
case MINECRAFT_1_19:
|
||||
case MINECRAFT_1_19_1:
|
||||
case MINECRAFT_1_19_3:
|
||||
return 8;
|
||||
case MINECRAFT_1_19_4:
|
||||
case MINECRAFT_1_20:
|
||||
case MINECRAFT_1_20_2:
|
||||
case MINECRAFT_1_20_3:
|
||||
return 9;
|
||||
}
|
||||
});
|
||||
|
||||
private final Function<ProtocolVersion, Integer> function;
|
||||
|
||||
public int getId(final @NotNull ProtocolVersion protocolVersion) {
|
||||
return function.apply(protocolVersion);
|
||||
}
|
||||
}
|
||||
+21
-12
@@ -15,19 +15,28 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.jonesdev.sonar.bukkit.command;
|
||||
package xyz.jonesdev.sonar.common.service;
|
||||
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.command.InvocationSource;
|
||||
import xyz.jonesdev.sonar.bukkit.SonarBukkit;
|
||||
public final class SonarServiceThread extends Thread {
|
||||
private final long delay;
|
||||
private final Runnable runnable;
|
||||
|
||||
public final class BukkitInvocationSource extends InvocationSource {
|
||||
public BukkitInvocationSource(final @NotNull CommandSender sender) {
|
||||
super(sender instanceof Player ? ((Player) sender).getUniqueId() : null,
|
||||
SonarBukkit.INSTANCE.getBukkitAudiences().sender(sender),
|
||||
sender instanceof Player,
|
||||
sender::hasPermission);
|
||||
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));
|
||||
}
|
||||
|
||||
+2
-1
@@ -17,6 +17,7 @@
|
||||
|
||||
package xyz.jonesdev.sonar.common.subcommand.impl;
|
||||
|
||||
import net.kyori.adventure.text.Component;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.Sonar;
|
||||
import xyz.jonesdev.sonar.api.command.CommandInvocation;
|
||||
@@ -62,7 +63,7 @@ public final class StatisticsCommand extends Subcommand {
|
||||
|
||||
invocation.getSender().sendMessage(SONAR.getConfig().getCommands().getStatisticsHeader()
|
||||
.replace("%type%", type.name().toLowerCase()));
|
||||
invocation.getSender().sendMessage();
|
||||
invocation.getSender().sendMessage(Component.empty());
|
||||
|
||||
switch (type) {
|
||||
case GENERAL: {
|
||||
|
||||
+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
@@ -176,7 +176,7 @@ public class ProtocolUtil {
|
||||
}
|
||||
}
|
||||
|
||||
private static int readExtendedForgeShort(@NotNull final ByteBuf byteBuf) {
|
||||
private static int readExtendedForgeShort(final @NotNull ByteBuf byteBuf) {
|
||||
int low = byteBuf.readUnsignedShort();
|
||||
int high = 0;
|
||||
if ((low & 0x8000) != 0) {
|
||||
|
||||
@@ -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
|
||||
@@ -82,8 +82,6 @@ verification:
|
||||
captcha-on-fail: false
|
||||
# Maximální počet ticků, který má hráč padat aby byl puštěn na verifikační platformu
|
||||
max-movement-ticks: 8
|
||||
# Maximum ignorovaných změn Y-nové souřadnice hráče, aby neprošel verifikací
|
||||
max-ignored-ticks: 5
|
||||
# Gamemode hráče při verifikaci
|
||||
# Typy: SURVIVAL, CREATIVE, ADVENTURE
|
||||
# - SURVIVAL: všechny komponenty UI jsou zobrazeny
|
||||
@@ -91,6 +89,14 @@ verification:
|
||||
# - ADVENTURE: všechny komponenty UI jsou zobrazeny
|
||||
gamemode: ADVENTURE
|
||||
|
||||
# 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
|
||||
# - 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)
|
||||
timing: ALWAYS
|
||||
|
||||
# Nechá hráče verifikovat pomocí kódu na mapě ve hře
|
||||
map-captcha:
|
||||
# Kdy by měl Sonar nechat hráče verifikovat pomocí mapy?
|
||||
@@ -161,10 +167,13 @@ verification:
|
||||
debug-xyz-positions: false
|
||||
# Množství času který musí uběhnout aby hráč byl odpojen
|
||||
# (Hodnota udávaná v milisekundách: 1 sekunda = 1000 milisekund)
|
||||
read-timeout: 3500
|
||||
read-timeout: 8000
|
||||
# How long should a player wait before reconnecting during verification Jak dlouho by měl hráč
|
||||
# (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?
|
||||
# (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?
|
||||
# (Hodnota udávaná v milisekundách: 1 sekunda = 1000 milisekund)
|
||||
blacklist-time: 600000
|
||||
|
||||
@@ -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
|
||||
@@ -79,8 +79,6 @@ verification:
|
||||
captcha-on-fail: false
|
||||
# Maximale Anzahl an Ticks, die der Spieler fallen muss, um die Plattform erreichen zu dürfen
|
||||
max-movement-ticks: 8
|
||||
# Maximale Anzahl an ignorierten Y-Bewegungsänderungen, bevor ein Spieler die Überprüfung nicht besteht
|
||||
max-ignored-ticks: 5
|
||||
# Der Spielmodus des Spielers während der Überprüfung
|
||||
# Mögliche Typen: SURVIVAL, CREATIVE, ADVENTURE
|
||||
# - SURVIVAL: alle UI-Komponenten sind sichtbar
|
||||
@@ -88,6 +86,14 @@ verification:
|
||||
# - ADVENTURE: alle UI-Komponenten sind sichtbar
|
||||
gamemode: ADVENTURE
|
||||
|
||||
# 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
|
||||
# - 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)
|
||||
timing: ALWAYS
|
||||
|
||||
# Lässt den Spieler einen Code von einer virtuellen Karte im Chat eingeben
|
||||
map-captcha:
|
||||
# Wann sollte Sonar den Spieler ein CAPTCHA lösen lassen?
|
||||
@@ -158,10 +164,13 @@ verification:
|
||||
debug-xyz-positions: false
|
||||
# Zeit, die verstreichen muss, bevor ein Spieler ein Timeout erleidet
|
||||
# (Dieser Wert stellt die Zeit in Millisekunden dar: 1 Sekunde = 1000 Millisekunden)
|
||||
read-timeout: 3500
|
||||
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
|
||||
# How long should Sonar remember the amount of failed verifications for a player?
|
||||
# (Dieser Wert stellt die Zeit in Millisekunden dar: 1 Sekunde = 1000 Millisekunden)
|
||||
remember-time: 120000
|
||||
# Wie lange sollte eine IP-Adresse vom Einloggen ausgeschlossen werden, wenn die Überprüfung zu oft nicht bestanden wird?
|
||||
# (Dieser Wert stellt die Zeit in Millisekunden dar: 1 Sekunde = 1000 Millisekunden)
|
||||
blacklist-time: 600000
|
||||
|
||||
@@ -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
|
||||
@@ -81,8 +81,6 @@ verification:
|
||||
captcha-on-fail: false
|
||||
# Maximum number of ticks the player has to fall in order to be allowed to hit the platform
|
||||
max-movement-ticks: 8
|
||||
# Maximum number of ignored Y movement changes before a player fails verification
|
||||
max-ignored-ticks: 5
|
||||
# The gamemode of the player during verification
|
||||
# Possible types: SURVIVAL, CREATIVE, ADVENTURE
|
||||
# - SURVIVAL: all UI components are visible
|
||||
@@ -90,6 +88,14 @@ verification:
|
||||
# - ADVENTURE: all UI components are visible
|
||||
gamemode: ADVENTURE
|
||||
|
||||
# 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
|
||||
# - 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)
|
||||
timing: ALWAYS
|
||||
|
||||
# Make the player type a code from a virtual map in chat
|
||||
map-captcha:
|
||||
# When should Sonar make the player solve a CAPTCHA?
|
||||
@@ -160,10 +166,13 @@ verification:
|
||||
debug-xyz-positions: false
|
||||
# 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
|
||||
# How long should Sonar remember the amount of failed verifications for a player?
|
||||
# (This value represents the time in milliseconds: 1 second = 1000 milliseconds)
|
||||
remember-time: 120000
|
||||
# How long should an IP address be denied from logging in when failing the verification too often?
|
||||
# (This value represents the time in milliseconds: 1 second = 1000 milliseconds)
|
||||
blacklist-time: 600000
|
||||
|
||||
@@ -55,9 +55,9 @@ database:
|
||||
maximum-age: 5
|
||||
|
||||
# ყოველი ახალი შესვლის მოთხოვნა იქნება რიგში, რათა თავიდან აიცილოთ სპამის მიერთების შეტევები
|
||||
# რიგი განახლდება ყოველ 500 მილიწამში (10 ტიკი)
|
||||
# რიგი განახლდება ყოველ 1000 მილიწამში (20 ტიკი)
|
||||
queue:
|
||||
# ახალი მოთამაშეების მაქსიმალური რაოდენობა, რომლებსაც შეუძლიათ შესვლა სერვერზე ყოველ 500 მილიწამში
|
||||
# ახალი მოთამაშეების მაქსიმალური რაოდენობა, რომლებსაც შეუძლიათ შესვლა სერვერზე ყოველ 1000 მილიწამში
|
||||
max-polls: 30
|
||||
|
||||
# ყოველი ახალი მოთამაშე, რომელიც პირველად შეუერთდება, გაიგზავნება
|
||||
@@ -81,8 +81,6 @@ verification:
|
||||
captcha-on-fail: false
|
||||
# ტიკების მაქსიმალური რაოდენობა რომელითაც მოთამაშე უნდა დაეცეს, რათა პლატფორმაზე დაცემა შეეძლოს
|
||||
max-movement-ticks: 8
|
||||
# იგნორირებული Y მოძრაობის მაქსიმალური რაოდენობა სანამ, მოთამაშე ვერიფიკაციას ვერ გაივლის
|
||||
max-ignored-ticks: 5
|
||||
# მოთამაშის თამაშის რეჟიმი გადამოწმების დროს
|
||||
# შესაძლო ტიპები: SURVIVAL, CREATIVE, ADVENTURE
|
||||
# - SURVIVAL: ყველა UI კომპონენტი ჩანს
|
||||
@@ -90,6 +88,14 @@ verification:
|
||||
# - ADVENTURE: ყველა UI კომპონენტი ჩანს
|
||||
gamemode: ADVENTURE
|
||||
|
||||
# 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
|
||||
# - 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)
|
||||
timing: ALWAYS
|
||||
|
||||
# დააწერინეთ მოთამაშეს კოდი ჩატში, რომელიც იქნება ნაჩვენები ვირტუალურ მაპზე, რომელიც ხელში ეჭირებად
|
||||
map-captcha:
|
||||
# როდის უნდა გააკეთებინოს Sonar-მა მოთამაშეს CAPTCHA?
|
||||
@@ -160,10 +166,13 @@ verification:
|
||||
debug-xyz-positions: false
|
||||
# დროის ოდენობა, რომელიც უნდა გავიდეს, სანამ მოთამაშე ტაიმაუტდება სერვერიდან.
|
||||
# (ეს არის ნაჩვენები მილიწამებში: 1 წამი = 1000 მილიწამი)
|
||||
read-timeout: 3500
|
||||
read-timeout: 8000
|
||||
# რამდენ ხანს უნდა დაელოდოს მოთამაშე ვერიფიკაციის დროს ხელახლა დაკავშირებამდე?
|
||||
# (ეს არის ნაჩვენები მილიწამებში: 1 წამი = 1000 მილიწამი)
|
||||
rejoin-delay: 8000
|
||||
# How long should Sonar remember the amount of failed verifications for a player?
|
||||
# (ეს არის ნაჩვენები მილიწამებში: 1 წამი = 1000 მილიწამი)
|
||||
remember-time: 120000
|
||||
# რამდენ ხნით უნდა აეკრძალოს IP მისამართს შესვლა, როდესაც ვერიფიკაციას ძალიან ხშირად ვერ აკეთებს?
|
||||
# (ეს არის ნაჩვენები მილიწამებში: 1 წამი = 1000 მილიწამი)
|
||||
blacklist-time: 600000
|
||||
|
||||
@@ -55,9 +55,9 @@ database:
|
||||
maximum-age: 5
|
||||
|
||||
# Каждый новый запрос на вход на сервер будет ставиться в очередь, чтобы избежать спам-атак.
|
||||
# Очередь обновляется каждые 500 миллисекунд (10 тиков)
|
||||
# Очередь обновляется каждые 1 секунд (20 тиков)
|
||||
queue:
|
||||
# Максимальное количество одновременных опросов очереди за 500 миллисекунд
|
||||
# Максимальное количество одновременных опросов очереди за 1 секунд
|
||||
max-polls: 30
|
||||
|
||||
# Каждый новый игрок, впервые присоединившийся на сервер, будет отправлен на
|
||||
@@ -81,8 +81,6 @@ verification:
|
||||
captcha-on-fail: false
|
||||
# Максимальное количество тиков, на которое должен упасть игрок, чтобы ему было разрешено упасть на платформу
|
||||
max-movement-ticks: 8
|
||||
# Максимальное количество проигнорированных изменений движения Y, прежде чем игрок не пройдет проверку
|
||||
max-ignored-ticks: 5
|
||||
# Режим игрока во время проверки
|
||||
# Возможные типы: SURVIVAL, CREATIVE, ADVENTURE
|
||||
# - SURVIVAL: все компоненты пользовательского интерфейса видны
|
||||
@@ -90,6 +88,14 @@ verification:
|
||||
# - ADVENTURE: все компоненты пользовательского интерфейса видны
|
||||
gamemode: ADVENTURE
|
||||
|
||||
# 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
|
||||
# - 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)
|
||||
timing: ALWAYS
|
||||
|
||||
# Заставить игрока ввести код с картинки в чат
|
||||
map-captcha:
|
||||
# Когда Sonar должен заставить игрока решить CAPTCHA?
|
||||
@@ -98,21 +104,21 @@ verification:
|
||||
# - DURING_ATTACK: Новые игроки будут получать капчу только во время атаки
|
||||
# - NEVER: Новые игроки никогда не получат капчу (Рекомендуется)
|
||||
timing: NEVER
|
||||
# Options for the effects on the image shown to the player
|
||||
# Кастомизация эффектов на картинке, которые показываются игроку
|
||||
effects:
|
||||
# Adds a glow effect to the image
|
||||
# Добавляет свечение к изображению
|
||||
flare: true
|
||||
# Adds random lines to the image
|
||||
# Добавляет случайные линии к изображению
|
||||
scratches: true
|
||||
# Applies a ripple (sine) distortion filter
|
||||
# Применяет искажение ряби (синусоидальное)
|
||||
ripple: true
|
||||
# Applies a horizontal distortion filter
|
||||
# Применяет горизонтальное искажение
|
||||
smear: true
|
||||
# Adds a bit of blurriness/distortion to the image
|
||||
# Добавляет немного размытости/искажения к изображению
|
||||
pinch: true
|
||||
# Applies color correction (color saturation)
|
||||
# Применяет цветокоррекцию (насыщенность цвета)
|
||||
saturation: 0.15
|
||||
# Applies a horizontal triangular ripple filter
|
||||
# Применяет горизонтальное треугольное искажение
|
||||
distortion: 2
|
||||
# Сколько капч должен подготовить Sonar?
|
||||
# Эта задача выполняется асинхронно в фоновом режиме
|
||||
@@ -160,10 +166,13 @@ verification:
|
||||
debug-xyz-positions: false
|
||||
# Количество времени, которое должно пройти, прежде чем игрока кикнет.
|
||||
# (Это значение представляет время в миллисекундах: 1 секунда = 1000 миллисекунд)
|
||||
read-timeout: 3500
|
||||
read-timeout: 8000
|
||||
# Сколько времени игрок должен ждать перед повторным подключением во время проверки
|
||||
# (Это значение представляет время в миллисекундах: 1 секунда = 1000 миллисекунд)
|
||||
rejoin-delay: 8000
|
||||
# How long should Sonar remember the amount of failed verifications for a player?
|
||||
# (Это значение представляет время в миллисекундах: 1 секунда = 1000 миллисекунд)
|
||||
remember-time: 120000
|
||||
# Как долго IP-адресу должно быть запрещено входить на сервер, если он слишком часто не проходит проверку?
|
||||
# (Это значение представляет время в миллисекундах: 1 секунда = 1000 миллисекунд)
|
||||
blacklist-time: 600000
|
||||
|
||||
@@ -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)
|
||||
@@ -238,7 +240,7 @@ verification:
|
||||
# Nachricht, die angezeigt wird, wenn jemand die Verifizierung nicht besteht
|
||||
failed:
|
||||
- '%header%'
|
||||
- <red>Du hast die Verifizierung nicht bestanden.
|
||||
- <red>Du hast die Bot-Verifizierung nicht bestanden.
|
||||
- <gray>Bitte warte einige Sekunden bevor du es erneut versuchst.
|
||||
- <gold>Brauchst du Hilfe? <gray>%support-url%
|
||||
- '%footer%'
|
||||
|
||||
@@ -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)
|
||||
@@ -244,7 +245,7 @@ verification:
|
||||
# Disconnect message that is shown when someone fails verification
|
||||
failed:
|
||||
- '%header%'
|
||||
- <red>You have failed the verification.
|
||||
- <red>You have failed the bot verification.
|
||||
- <gray>Please wait a few seconds before trying to verify again.
|
||||
- <gold>Need help? <gray>%support-url%
|
||||
- '%footer%'
|
||||
|
||||
@@ -13,24 +13,24 @@
|
||||
# Copyright Jones Development and Sonar Contributors, licensed under GPL-3.0
|
||||
#
|
||||
|
||||
# Плейсхолдер для каждого '%prefix%' в файле конфигурации
|
||||
# Плейсхолдер для '%prefix%' в конфиге
|
||||
prefix: <b><yellow>Sonar</b> <gray>» <white>
|
||||
# Плейсхолдер для каждого '%support-url%' в файле конфигурации
|
||||
# Плейсхолдер для '%support-url%' в конфиге
|
||||
support-url: https://jonesdev.xyz/discord/
|
||||
# Плейсхолдер для каждого '%header%' в файле конфигурации
|
||||
# Плейсхолдер для '%header%' в конфиге
|
||||
header:
|
||||
- <b><yellow>Sonar</b><reset>
|
||||
- ''
|
||||
# Плейсхолдер для каждого '%footer%' в файле конфигурации
|
||||
# Плейсхолдер для '%footer%' в конфиге
|
||||
footer:
|
||||
- <gray>Если вы считаете что это ошибка, обратитесь к администратору.
|
||||
# Сообщение отключения, которое показывается, когда кто-то присоединяется, но в сети слишком много игроков с его IP-адресом
|
||||
# Сообщение, которое показывается когда кто-то заходит на сервер, но в сети слишком много игроков с его IP-адресом
|
||||
too-many-online-per-ip:
|
||||
- '%header%'
|
||||
- <red>С вашего IP-адреса в сети слишком много игроков.
|
||||
- '%footer%'
|
||||
|
||||
# Перевод для команды
|
||||
# Перевод для команд
|
||||
commands:
|
||||
# Сообщение, которое будет показано, когда игрок попытается выполнить команду /sonar без разрешения
|
||||
no-permission: '%prefix%<red>У вас нет прав на использование этой команды.'
|
||||
@@ -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>Эта команда может быть выполнена только игроком.'
|
||||
# Сообщение, которое отображается, когда игрок выполняет команду, доступную только для консоли
|
||||
@@ -79,9 +77,9 @@ commands:
|
||||
|
||||
# Перевод для '/sonar verbose'
|
||||
verbose:
|
||||
# Сообщение, которое показывается, когда игрок подписывается на Sonar verbose
|
||||
# Сообщение, которое отображается, когда игрок прослушивает Sonar verbose
|
||||
subscribed: '%prefix%Вы просматриваете подробную информацию о Sonar.'
|
||||
# Сообщение, которое показывается, когда игрок отписывается от Sonar verbose
|
||||
# Сообщение, которое отображается, когда игрок перестает прослушивать Sonar verbose
|
||||
unsubscribed: '%prefix%Вы больше не просматриваете подробную информацию о Sonar.'
|
||||
|
||||
# Перевод для '/sonar notify'
|
||||
@@ -104,8 +102,8 @@ commands:
|
||||
added: '%prefix%Успешно добавлен %ip% в черный список.'
|
||||
# Сообщение, которое отображается, когда кто-то добавляет IP-адрес в черный список, который находится в белом списке
|
||||
added-warning: '%prefix%<red>Предупреждение: <white>%ip% находится в белом списке.
|
||||
Рассмотрите возможность удалить IP-адрес из белого списка игроков, чтобы
|
||||
избежать потенциальных ошибок.'
|
||||
Рассмотрите возможность удалить IP-адрес из белого списка игроков, чтобы избежать
|
||||
потенциальных ошибок.'
|
||||
# Сообщение, которое показывается, когда кто-то удаляет IP-адрес из черного списка
|
||||
removed: '%prefix%Успешно удален %ip% из черного списка.'
|
||||
# Сообщение, которое отображается, когда кто-то добавляет IP-адрес в черный список, но он уже находится в черном списке
|
||||
@@ -116,16 +114,21 @@ commands:
|
||||
# Перевод для '/sonar verified'
|
||||
verified:
|
||||
# Сообщение, которое отображается, когда кто-то пытается очистить список проверенных игроков, но он пустой
|
||||
empty: '%prefix%Список проверенных игроков в настоящее время пуст. Поэтому, ни один игрок не был удален.'
|
||||
# Сообщение, которое отображается, когда кто-то очищает список проверенных игроков
|
||||
empty: '%prefix%Список проверенных игроков в настоящее время пуст. Поэтому, ни
|
||||
один игрок не был удален.'
|
||||
# Сообщение, которое отображается когда кто-то отчистил список уникальных игроков
|
||||
cleared: '%prefix%Вы успешно очистили %removed% уникального игрока(ов).'
|
||||
# Сообщение, которое показывается, когда кто-то проверяет размер списка проверенных игроков
|
||||
# Сообщение, которое отображается когда кто-то проверяет размер списка проврененных игроков
|
||||
size: '%prefix%В настоящее время проверено %amount% уникальных игроков.'
|
||||
# Сообщение, которое отображается при удалении IP-адреса из списка проверенных игроков
|
||||
removed: '%prefix%Успешно удален %ip%.'
|
||||
# Сообщение, которое отображается, когда кто-то удаляет 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-адрес не в списке.'
|
||||
# Сообщение, которое отображается, когда кто-то пытается дважды удалить один и тот же IP-адрес (двойная операция)
|
||||
# Сообщение, которое показывается когда выполняю сразу две операции (двойная операция)
|
||||
blocked: '%prefix%Пожалуйста, дождитесь завершения текущей операции.'
|
||||
|
||||
# Перевод для '/sonar statistics'
|
||||
@@ -165,15 +168,15 @@ commands:
|
||||
- ' <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%) был занесен в черный список за слишком большое
|
||||
количество неудачных попыток'
|
||||
# Сообщение, которое выводится в консоль при успешной проверке игрока
|
||||
@@ -182,71 +185,70 @@ verification:
|
||||
captcha:
|
||||
# Сообщение, которое показывается игроку, когда он должен ввести ответ на капчу
|
||||
enter-code: '%prefix%<green>Введите код в чате, который отображается на карте.'
|
||||
# Таймер, который показывается игроку, когда он должен ввести ответ на капчу
|
||||
# Таймер, который показывается игроку, когда он должен ввести капчу
|
||||
# (Установите это значение на '' что-бы отключить эту функцию)
|
||||
action-bar: '%prefix%<green>У вас осталось %time-left% секунд, чтобы ввести код
|
||||
в чате'
|
||||
# Сообщение, которое показывается игроку, когда он вводит неправильный код капчи в чате
|
||||
incorrect: '%prefix%<red>Вы ввели неправильный код. Пожалуйста, попробуйте еще
|
||||
раз.'
|
||||
# Сообщение, которое показывается игроку, когда он вводит неправильную капчу в чат
|
||||
incorrect: '%prefix%<red>Вы ввели неправильный код. Пожалуйста, попробуйте еще раз.'
|
||||
|
||||
# Сообщение об отключении, которое отображается, когда кто-то присоединяется но капча еще не подготовлена
|
||||
# Сообщение, которое показывается когда капча еще не подготовлена
|
||||
currently-preparing:
|
||||
- '%header%'
|
||||
- <yellow>Капча еще не подготовлена.
|
||||
- <gray>Пожалуйста, подождите несколько секунд, прежде чем повторить попытку.
|
||||
- '%footer%'
|
||||
# Сообщение об отключении, которое отображается при одновременной верификации слишком большого количества игроков
|
||||
# Сообщение, которое показывается когда слишком много людей проходят проверку
|
||||
too-many-players:
|
||||
- '%header%'
|
||||
- <gold>Слишком много игроков пытаются войти на сервер.
|
||||
- <gray>Пожалуйста, подождите несколько секунд, прежде чем повторить попытку.
|
||||
- '%footer%'
|
||||
# Сообщение об отключении, которое отображается, когда кто-то слишком быстро перезаходит во время проверки
|
||||
# Сообщение, которое показывается когда кто-то слишком быстро перезаходит
|
||||
too-fast-reconnect:
|
||||
- '%header%'
|
||||
- <gold>Вы перезаходите слишком быстро, повторите попытку позже.
|
||||
- <gray>Пожалуйста, подождите несколько секунд, прежде чем повторить попытку.
|
||||
- '%footer%'
|
||||
# Сообщение об отключении, которое отображается, когда кто-то присоединяется, но его IP-адрес уже проходит верификацию
|
||||
# Сообщение, которое показывается когда чей то IP-адрес уже проходит верефикацию
|
||||
already-verifying:
|
||||
- '%header%'
|
||||
- <red>Ваш IP-адрес в настоящее время проверяется.
|
||||
- <gray>Пожалуйста, подождите несколько секунд, прежде чем повторить попытку.
|
||||
- '%footer%'
|
||||
# Сообщение об отключении, которое показывается, когда кто-то присоединяется, но уже находится в очереди на проверку
|
||||
# Сообщение, которое показывается когда кто-то находится в очереди на проверку
|
||||
already-queued:
|
||||
- '%header%'
|
||||
- <red>Ваш IP-адрес в настоящее время находится в очереди на проверку.
|
||||
- <gray>Пожалуйста, подождите несколько секунд, прежде чем повторить попытку.
|
||||
- '%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>Вы не прошли проверку.
|
||||
- <red>Вы не прошли проверку бота.
|
||||
- <gray>Пожалуйста, подождите несколько секунд, прежде чем повторить попытку.
|
||||
- <gold>Нужна помощь? <gray>%support-url%
|
||||
- '%footer%'
|
||||
@@ -254,7 +256,7 @@ verification:
|
||||
# Перевод для action bar (verbose)
|
||||
verbose:
|
||||
layout:
|
||||
# Макет для панели действий Sonar verbose
|
||||
# Панель действий Sonar verbose
|
||||
# Placeholders:
|
||||
# - '%queued%' Количество игроков в очереди
|
||||
# - '%verifying%' Количество проверяемых игроков
|
||||
@@ -275,16 +277,16 @@ verbose:
|
||||
# - '%max-memory%' Объем максимальной памяти
|
||||
# - '%free-memory%' Количество свободной памяти
|
||||
# - '%animation%' Настраиваемый анимированный символ
|
||||
# Перевод статистики в режиме реального времени (verbose)
|
||||
# Статистика в режиме реального времени (verbose)
|
||||
normal: '%prefix%<gray>CPS <white>%connections-per-second% <dark_aqua>╺ <gray>Logins/s
|
||||
<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>'
|
||||
# Макет action bar, отображающей информацию во время атаки
|
||||
# Action bar отображающей информацию во время атаки
|
||||
attack: '%prefix%<gray>CPS <white>%connections-per-second% <dark_aqua>╺ <gray>Logins/s
|
||||
<white>%logins-per-second% <dark_aqua>╺ <gray>В очереди <white>%queued% <dark_aqua>╺
|
||||
<gray>На проверке <white>%verifying% <dark_aqua>╺ <gray>Продолжительность <white>%attack-duration%
|
||||
<dark_aqua>| <green>⬆ <white>%outgoing-traffic%/s <red>⬇ <white>%incoming-traffic%/s <green><bold>%animation%<reset>'
|
||||
# Анимация для action bar (текст с названием элемента)
|
||||
# Анимация для загрузки
|
||||
# Альтернативы:
|
||||
# - ▙, ▛, ▜, ▟
|
||||
# - ⬈, ⬊, ⬋, ⬉
|
||||
@@ -296,11 +298,11 @@ verbose:
|
||||
|
||||
# Перевод сообщений об атаках
|
||||
notifications:
|
||||
# Макет для заголовка уведомления об атаке
|
||||
# Заголовок уведомления об атаке
|
||||
title: <yellow><b>Sonar<reset>
|
||||
# Макет для подзаголовка уведомления об атаке
|
||||
# Подзаголовок уведомления об атаке
|
||||
subtitle: <gray>Обнаружена бот-атака
|
||||
# Макет для уведомления об атаке в чате
|
||||
# Уведомление об атаке в чате
|
||||
chat:
|
||||
- ''
|
||||
- <yellow><b>Sonar<reset>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
val velocityVersion = "3.3.0-SNAPSHOT"
|
||||
|
||||
dependencies {
|
||||
compileOnly(project(":api"))
|
||||
compileOnly(project(":common"))
|
||||
implementation(project(":api"))
|
||||
implementation(project(":common"))
|
||||
|
||||
compileOnly("com.velocitypowered:velocity-proxy:$velocityVersion")
|
||||
testCompileOnly("com.velocitypowered:velocity-proxy:$velocityVersion")
|
||||
|
||||
@@ -18,26 +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.audience.AudienceListener;
|
||||
import xyz.jonesdev.sonar.velocity.command.VelocitySonarCommand;
|
||||
import xyz.jonesdev.sonar.velocity.fallback.FallbackInjectionHelper;
|
||||
import xyz.jonesdev.sonar.velocity.fallback.FallbackLoginListener;
|
||||
|
||||
import java.util.Map;
|
||||
import java.net.InetAddress;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@Getter
|
||||
public final class SonarVelocity extends SonarBootstrap<SonarVelocityPlugin> {
|
||||
@@ -51,16 +47,26 @@ public final class SonarVelocity extends SonarBootstrap<SonarVelocityPlugin> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom wrapper for Velocity audiences
|
||||
* Wrapper for Velocity audiences
|
||||
*/
|
||||
public static final Map<UUID, Audience> AUDIENCES = new ConcurrentHashMap<>();
|
||||
|
||||
@Override
|
||||
public @Nullable Audience audience(final @Nullable UUID uniqueId) {
|
||||
if (uniqueId == null) {
|
||||
return getPlugin().getServer().getConsoleCommandSource();
|
||||
return null;
|
||||
}
|
||||
return AUDIENCES.get(uniqueId);
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -103,28 +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());
|
||||
|
||||
// TODO: find a proper way of handling Audiences on Velocity
|
||||
// Register audience register listener
|
||||
getPlugin().getServer().getEventManager().register(getPlugin(), new AudienceListener());
|
||||
|
||||
// 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());
|
||||
}
|
||||
|
||||
-38
@@ -1,38 +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.audience;
|
||||
|
||||
import com.velocitypowered.api.event.PostOrder;
|
||||
import com.velocitypowered.api.event.Subscribe;
|
||||
import com.velocitypowered.api.event.connection.DisconnectEvent;
|
||||
import com.velocitypowered.api.event.connection.PostLoginEvent;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.velocity.SonarVelocity;
|
||||
|
||||
public final class AudienceListener {
|
||||
|
||||
@Subscribe(order = PostOrder.LAST)
|
||||
public void handle(final @NotNull PostLoginEvent event) {
|
||||
SonarVelocity.AUDIENCES.put(event.getPlayer().getUniqueId(), event.getPlayer());
|
||||
}
|
||||
|
||||
@Subscribe(order = PostOrder.LAST)
|
||||
public void handle(final @NotNull DisconnectEvent event) {
|
||||
SonarVelocity.AUDIENCES.remove(event.getPlayer().getUniqueId());
|
||||
}
|
||||
}
|
||||
+5
-1
@@ -18,6 +18,7 @@
|
||||
package xyz.jonesdev.sonar.velocity.command;
|
||||
|
||||
import com.velocitypowered.api.command.SimpleCommand;
|
||||
import com.velocitypowered.api.proxy.Player;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import xyz.jonesdev.sonar.api.command.InvocationSource;
|
||||
import xyz.jonesdev.sonar.api.command.SonarCommand;
|
||||
@@ -31,7 +32,10 @@ public final class VelocitySonarCommand implements SimpleCommand, SonarCommand {
|
||||
@Override
|
||||
public void execute(final @NotNull Invocation invocation) {
|
||||
// Create our own invocation source wrapper to handle messages properly
|
||||
final InvocationSource invocationSource = new VelocityInvocationSource(invocation.source());
|
||||
final InvocationSource invocationSource = new InvocationSource(
|
||||
invocation.source() instanceof Player ? ((Player) invocation.source()).getUniqueId() : null,
|
||||
invocation.source(),
|
||||
invocation.source()::hasPermission);
|
||||
// Pass the invocation source and command arguments to our command handler
|
||||
handle(invocationSource, invocation.arguments());
|
||||
}
|
||||
|
||||
+6
-140
@@ -22,27 +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.FallbackUser;
|
||||
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 {
|
||||
|
||||
@@ -50,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
|
||||
final FallbackUser 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