Compare commits

...
11 Commits
Author SHA1 Message Date
Jones 23815351f0 Release 2.0.25 (#261) 2024-04-07 14:44:06 +02:00
Jones 5a68ea8af6 Restructure dependency management (#260)
* feat: recode command invocation source system

* feat: remove obsolete audience listener on Velocity

* fix: invalid console action bar sending when uuid is null

* feat: microoptimize InvocationSource#isPlayer

* build: recode and restructure dependency management

* build(bungee,bukkit): downgrade to adventure-platform 4.3.2

* build(all): minimize JAR (excluding api)

* build: fix minimizing breaking kyori adventure

* style: mark some api functions as internal as they would require relocating when used

* build(bukkit): mark as folia supported

* build: recode `build-sonar` task

* fix: remove unused/outdated files

* chore: I think I was dead when writing this

* build(deps): load minimessage and serializer-gson using libby

* refactor: remove some duplicate/unnecessary code
2024-04-07 14:38:40 +02:00
Jones 9b54b484fb Update Russian translations (#259) 2024-04-05 15:02:45 +02:00
Jones 4fa56f2f3c Hide blacklist logs during attack (#257) 2024-04-03 13:11:36 +02:00
Jones 51751584dc Improve code readability and maintainability (#255)
* style: add comments explaining certain functions/methods

* style: add link to stackoverflow comment explaining memory formatting

* style: add some more documentation and explanations to the API

* style: improve Discord webhook API readability
2024-04-03 13:04:48 +02:00
Jones 3955f9bc8a Improve verification limits and state system (#254)
* fix: fail for disconnecting during verification

* chore: update bot check failed message

* feat: improve state, failing and blacklist system
2024-04-02 21:20:20 +02:00
Jones e333162032 Implement vehicle mounting check (#251)
* fix: recode captcha-on-fail mechanic

* feat: remove unnecessary max-ping option

* feat: [wip] vehicle check and other improvements

* feat: add RemoveEntitiesPacket

* fix: RemoveEntitiesPacket for 1.7

* fix: PlayerInputPacket for 1.7

* feat: validate jump and unmount state (but don't fail)

* fix: RemoveEntitiesPacket not working on 1.17

* fix: boat entity type being broken on 1.13

* feat: [wip] support all versions (1.13-1.13.2 are broken)

* fix: boat entity type for 1.13-1.13.2

* feat: add missing vehicle configurations

* fix: wait 10s between each action bar on 1.7

* fix: remove unnecessary flush in KeepAlive

* fix: only expect PlayerInput packets when needed
2024-03-30 19:16:27 +01:00
dependabot[bot] b3a7f4de09 Bump netty-all from 4.1.107.Final to 4.1.108.Final (#248)
Bumps [io.netty:netty-all](https://github.com/netty/netty) from 4.1.107.Final to 4.1.108.Final.
- [Commits](https://github.com/netty/netty/compare/netty-4.1.107.Final...netty-4.1.108.Final)

---
updated-dependencies:
- dependency-name: io.netty:netty-all
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-25 13:45:29 +01:00
dependabot[bot] 5752f1e208 Bump lombok from 1.18.30 to 1.18.32 (#249)
Bumps [org.projectlombok:lombok](https://github.com/projectlombok/lombok) from 1.18.30 to 1.18.32.
- [Changelog](https://github.com/projectlombok/lombok/blob/master/doc/changelog.markdown)
- [Commits](https://github.com/projectlombok/lombok/compare/v1.18.30...v1.18.32)

---
updated-dependencies:
- dependency-name: org.projectlombok:lombok
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-25 13:45:16 +01:00
MISHA 1c6ba7050d Add a missed line to Russian translations (#247) 2024-03-24 20:55:35 +01:00
MISHA 340d4a225d Add the missing Russian language in messages (#245)
* Update ru.yml

* Update ru.yml
2024-03-24 20:39:10 +01:00
68 changed files with 1207 additions and 772 deletions

No files matched your search

-1
View File
@@ -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
View File
@@ -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.108.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)
}
}
-6
View File
@@ -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
View File
@@ -1,3 +1,3 @@
group=xyz.jonesdev.sonar
version=2.0.24
version=2.0.25
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;
@@ -43,6 +44,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);
/**
@@ -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);
}
}
@@ -171,15 +171,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 +190,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 +202,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 +209,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();
@@ -435,6 +435,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 +465,6 @@ public final class SonarConfiguration {
private boolean captchaOnFail;
private Gamemode gamemode;
private int maxMovementTicks;
private int maxIgnoredTicks;
@Getter
@RequiredArgsConstructor
@@ -479,6 +479,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 +502,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);
@@ -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,14 +69,18 @@ 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());
@@ -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);
/**
@@ -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);
}
@@ -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;
}
}
@@ -32,6 +32,7 @@ public class JVMProcessInformation {
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];
@@ -39,42 +40,73 @@ public class JVMProcessInformation {
return String.format("%.1f %siB", formattedSize, unit);
}
/**
* @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
@@ -32,14 +32,23 @@ 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}, {@link SystemTimer#formattedDelay}
*/
public boolean elapsed(final long amount) {
return delay() >= amount;
}
/**
* @apiNote Read more: {@link SystemTimer#delay}
*/
public @NotNull String formattedDelay() {
return FORMATTER.format(delay());
}
@@ -22,6 +22,7 @@ import lombok.val;
import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.Sonar;
import xyz.jonesdev.sonar.api.attack.AttackTracker;
@@ -56,6 +57,7 @@ public final class Verbose implements Observable {
}
}
@ApiStatus.Internal
public @NotNull Component prepareActionBarFormat() {
final SonarStatistics statistics = Sonar.get().getStatistics();
final AttackTracker.AttackStatistics attackStatistics = Sonar.get().getAttackTracker().getCurrentAttack();
@@ -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();
+18 -3
View File
@@ -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
@@ -52,9 +52,9 @@ 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);
}
@@ -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.bukkit.command;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import xyz.jonesdev.sonar.api.command.InvocationSource;
import xyz.jonesdev.sonar.bukkit.SonarBukkit;
public final class BukkitInvocationSource extends InvocationSource {
public BukkitInvocationSource(final @NotNull CommandSender sender) {
super(sender instanceof Player ? ((Player) sender).getUniqueId() : null,
SonarBukkit.INSTANCE.getBukkitAudiences().sender(sender),
sender instanceof Player,
sender::hasPermission);
}
}
@@ -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
+17 -3
View File
@@ -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
@@ -52,9 +52,9 @@ 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);
}
@@ -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);
}
}
@@ -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);
}
@@ -29,7 +29,6 @@ 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;
@@ -203,7 +202,7 @@ public final class FallbackChannelHandler extends FallbackChannelHandlerAdapter
}
// Create an instance for the Fallback connection
final FallbackUser user = new FallbackUserWrapper(channel, inetAddress, protocolVersion);
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);
}));
+4
View File
@@ -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()
);
}
}
}
@@ -68,12 +68,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();
@@ -132,10 +133,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();
@@ -25,6 +25,7 @@ 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.statistics.GlobalSonarStatistics;
@@ -36,6 +37,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();
@@ -59,6 +61,12 @@ public class FallbackChannelHandlerAdapter extends ChannelInboundHandlerAdapter
// Remove the IP address from the queue
FALLBACK.getQueue().remove(inetAddress);
}
// The player cannot be in the verification if the user object doesn't exist,
// or if the user failed/succeeded the bot verification process.
if (user != null && user.getState().canReceivePackets()) {
// Mark the verification attempt as failed
user.fail("disconnected during verification");
}
// Make sure to let the server handle the rest
ctx.fireChannelInactive();
}
@@ -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;
@@ -53,6 +54,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,
@@ -127,20 +130,24 @@ public final class FallbackUserWrapper implements FallbackUser {
}
@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().isCurrentlyUnderAttack()
|| 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 +157,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);
}
}
@@ -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,27 @@ 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;
// 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,6 +85,10 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
this.user = user;
this.username = username;
this.playerUuid = playerUuid;
this.performGravity = Sonar.get().getConfig().getVerification().getGravity().isEnabled();
this.performCollisions = Sonar.get().getConfig().getVerification().getGravity().isCheckCollisions();
this.performCaptcha = FALLBACK.shouldPerformCaptcha();
this.performVehicle = FALLBACK.shouldPerformVehicleCheck();
if (user.getProtocolVersion().compareTo(MINECRAFT_1_20_2) < 0) {
// Start initializing the actual join process
@@ -105,7 +98,7 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
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);
@@ -136,7 +129,7 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
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 +140,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 +153,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 +167,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 +187,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 +205,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 +247,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 +268,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 +283,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 +291,53 @@ 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 PlayerInputPacket) {
// Check if we are currently expecting a PlayerInputPacket 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");
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
// We can't fail since the player is able to jump or sneak
if (inputPacket.isJump() || inputPacket.isUnmount()) return;
// Continue checking or verify the player
captchaOrNext();
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 +346,7 @@ public final class FallbackVerificationHandler implements FallbackPacketListener
// Start initializing the actual join process
updateEncoderDecoderState(FallbackPacketRegistry.GAME);
initialJoinProcess();
return;
}
if (packet instanceof KeepAlivePacket) {
@@ -345,12 +356,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 +372,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 +400,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 +412,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 +440,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 +458,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 +482,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 +610,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 +657,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 +684,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 +695,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);
}
}
@@ -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 =
@@ -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,20 @@ public enum FallbackPacketRegistry {
clientbound.register(GameEventPacket.class, GameEventPacket::new,
map(0x20, MINECRAFT_1_20_3, true));
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),
@@ -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;
@@ -62,6 +65,11 @@ public class FallbackPreparer {
public FallbackPacket protocolBlacklisted;
public FallbackPacket invalidUsername;
// 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)
public final int SPAWN_X_POSITION = 16 / 2; // middle of the chunk
@@ -86,7 +94,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,
@@ -139,5 +147,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);
}
}
@@ -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/
@@ -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();
}
}
@@ -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
}
}
@@ -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;
}
}
}
@@ -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;
}
@@ -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;
@@ -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();
}
}
@@ -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
}
}
@@ -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();
}
}
@@ -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();
}
}
@@ -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();
}
}
@@ -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();
}
}
@@ -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);
}
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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: {
@@ -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) {
@@ -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?
@@ -165,6 +171,9 @@ verification:
# 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
@@ -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?
@@ -162,6 +168,9 @@ verification:
# 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
@@ -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?
@@ -164,6 +170,9 @@ verification:
# 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
@@ -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?
@@ -164,6 +170,9 @@ verification:
# რამდენ ხანს უნდა დაელოდოს მოთამაშე ვერიფიკაციის დროს ხელახლა დაკავშირებამდე?
# (ეს არის ნაჩვენები მილიწამებში: 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
@@ -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?
# Эта задача выполняется асинхронно в фоновом режиме
@@ -164,6 +170,9 @@ verification:
# Сколько времени игрок должен ждать перед повторным подключением во время проверки
# (Это значение представляет время в миллисекундах: 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
@@ -238,7 +238,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%'
@@ -244,7 +244,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>У вас нет прав на использование этой команды.'
@@ -79,9 +79,9 @@ commands:
# Перевод для '/sonar verbose'
verbose:
# Сообщение, которое показывается, когда игрок подписывается на Sonar verbose
# Сообщение, которое отображается, когда игрок прослушивает Sonar verbose
subscribed: '%prefix%Вы просматриваете подробную информацию о Sonar.'
# Сообщение, которое показывается, когда игрок отписывается от Sonar verbose
# Сообщение, которое отображается, когда игрок перестает прослушивать Sonar verbose
unsubscribed: '%prefix%Вы больше не просматриваете подробную информацию о Sonar.'
# Перевод для '/sonar notify'
@@ -104,8 +104,8 @@ commands:
added: '%prefix%Успешно добавлен %ip% в черный список.'
# Сообщение, которое отображается, когда кто-то добавляет IP-адрес в черный список, который находится в белом списке
added-warning: '%prefix%<red>Предупреждение: <white>%ip% находится в белом списке.
Рассмотрите возможность удалить IP-адрес из белого списка игроков, чтобы
избежать потенциальных ошибок.'
Рассмотрите возможность удалить IP-адрес из белого списка игроков, чтобы избежать
потенциальных ошибок.'
# Сообщение, которое показывается, когда кто-то удаляет IP-адрес из черного списка
removed: '%prefix%Успешно удален %ip% из черного списка.'
# Сообщение, которое отображается, когда кто-то добавляет IP-адрес в черный список, но он уже находится в черном списке
@@ -116,16 +116,17 @@ commands:
# Перевод для '/sonar verified'
verified:
# Сообщение, которое отображается, когда кто-то пытается очистить список проверенных игроков, но он пустой
empty: '%prefix%Список проверенных игроков в настоящее время пуст. Поэтому, ни один игрок не был удален.'
# Сообщение, которое отображается, когда кто-то очищает список проверенных игроков
empty: '%prefix%Список проверенных игроков в настоящее время пуст. Поэтому, ни
один игрок не был удален.'
# Сообщение, которое отображается когда кто-то отчистил список уникальных игроков
cleared: '%prefix%Вы успешно очистили %removed% уникального игрока(ов).'
# Сообщение, которое показывается, когда кто-то проверяет размер списка проверенных игроков
# Сообщение, которое отображается когда кто-то проверяет размер списка проврененных игроков
size: '%prefix%В настоящее время проверено %amount% уникальных игроков.'
# Сообщение, которое отображается при удалении IP-адреса из списка проверенных игроков
removed: '%prefix%Успешно удален %ip%.'
# Сообщение, которое отображается, когда кто-то удаляет IP-адрес из списка, но его нет в списке
# Сообщение, которое показывается когда кто-то удаляет IP из списка но его небыло в списке
ip-not-found: '%prefix%Указанный вами IP-адрес не в списке.'
# Сообщение, которое отображается, когда кто-то пытается дважды удалить один и тот же IP-адрес (двойная операция)
# Сообщение, которое показывается когда выполняю сразу две операции (двойная операция)
blocked: '%prefix%Пожалуйста, дождитесь завершения текущей операции.'
# Перевод для '/sonar statistics'
@@ -165,15 +166,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 +183,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 +254,7 @@ verification:
# Перевод для action bar (verbose)
verbose:
layout:
# Макет для панели действий Sonar verbose
# Панель действий Sonar verbose
# Placeholders:
# - '%queued%' Количество игроков в очереди
# - '%verifying%' Количество проверяемых игроков
@@ -275,16 +275,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 +296,11 @@ verbose:
# Перевод сообщений об атаках
notifications:
# Макет для заголовка уведомления об атаке
# Заголовок уведомления об атаке
title: <yellow><b>Sonar<reset>
# Макет для подзаголовка уведомления об атаке
# Подзаголовок уведомления об атаке
subtitle: <gray>Обнаружена бот-атака
# Макет для уведомления об атаке в чате
# Уведомление об атаке в чате
chat:
- ''
- <yellow><b>Sonar<reset>
+2 -2
View File
@@ -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")
@@ -29,14 +29,11 @@ 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.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
@Getter
@@ -51,16 +48,14 @@ 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);
}
/**
@@ -106,10 +101,6 @@ public final class SonarVelocity extends SonarBootstrap<SonarVelocityPlugin> {
// 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)
@@ -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());
}
}
@@ -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());
}
@@ -26,7 +26,6 @@ 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;
@@ -182,7 +181,7 @@ public final class FallbackChannelHandler extends FallbackChannelHandlerAdapter
}
// Create an instance for the Fallback connection
final FallbackUser user = new FallbackUserWrapper(channel, inetAddress, protocolVersion);
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);
}));